From 8a588cf858e775cff43ea825cb855ed9f17a232f Mon Sep 17 00:00:00 2001 From: Tzahi12345 Date: Tue, 28 Nov 2023 22:36:58 -0500 Subject: [PATCH 1/5] config_api now broadcasts when a config item has changed Updated config_api module exports syntax to match rest of the app --- backend/config.js | 91 ++++++++++++++++++++++++------------------- backend/test/tests.js | 60 ++++++++++++++++++++++++++++ 2 files changed, 112 insertions(+), 39 deletions(-) diff --git a/backend/config.js b/backend/config.js index a5ee5df..c981867 100644 --- a/backend/config.js +++ b/backend/config.js @@ -1,22 +1,26 @@ const logger = require('./logger'); const fs = require('fs'); +const { BehaviorSubject } = require('rxjs'); + +exports.CONFIG_ITEMS = require('./consts.js')['CONFIG_ITEMS']; +exports.descriptors = {}; // to get rid of file locks when needed, TODO: move to youtube-dl.js -let CONFIG_ITEMS = require('./consts.js')['CONFIG_ITEMS']; const debugMode = process.env.YTDL_MODE === 'debug'; let configPath = debugMode ? '../src/assets/default.json' : 'appdata/default.json'; +exports.config_updated = new BehaviorSubject(); -function initialize() { +exports.initialize = () => { ensureConfigFileExists(); ensureConfigItemsExist(); } function ensureConfigItemsExist() { - const config_keys = Object.keys(CONFIG_ITEMS); + const config_keys = Object.keys(exports.CONFIG_ITEMS); for (let i = 0; i < config_keys.length; i++) { const config_key = config_keys[i]; - getConfigItem(config_key); + exports.getConfigItem(config_key); } } @@ -57,17 +61,17 @@ function getElementNameInConfig(path) { /** * Check if config exists. If not, write default config to config path */ -function configExistsCheck() { +exports.configExistsCheck = () => { let exists = fs.existsSync(configPath); if (!exists) { - setConfigFile(DEFAULT_CONFIG); + exports.setConfigFile(DEFAULT_CONFIG); } } /* * Gets config file and returns as a json */ -function getConfigFile() { +exports.getConfigFile = () => { try { let raw_data = fs.readFileSync(configPath); let parsed_data = JSON.parse(raw_data); @@ -78,8 +82,13 @@ function getConfigFile() { } } -function setConfigFile(config) { +exports.setConfigFile = (config) => { try { + const old_config = exports.getConfigFile(); + const changes = exports.findChangedConfigItems(old_config, config); + if (changes.length > 0) { + for (const change of changes) exports.config_updated.next(change); + } fs.writeFileSync(configPath, JSON.stringify(config, null, 2)); return true; } catch(e) { @@ -87,26 +96,26 @@ function setConfigFile(config) { } } -function getConfigItem(key) { - let config_json = getConfigFile(); - if (!CONFIG_ITEMS[key]) { +exports.getConfigItem = (key) => { + let config_json = exports.getConfigFile(); + if (!exports.CONFIG_ITEMS[key]) { logger.error(`Config item with key '${key}' is not recognized.`); return null; } - let path = CONFIG_ITEMS[key]['path']; + let path = exports.CONFIG_ITEMS[key]['path']; const val = Object.byString(config_json, path); if (val === undefined && Object.byString(DEFAULT_CONFIG, path) !== undefined) { logger.warn(`Cannot find config with key '${key}'. Creating one with the default value...`); - setConfigItem(key, Object.byString(DEFAULT_CONFIG, path)); + exports.setConfigItem(key, Object.byString(DEFAULT_CONFIG, path)); return Object.byString(DEFAULT_CONFIG, path); } return Object.byString(config_json, path); } -function setConfigItem(key, value) { +exports.setConfigItem = (key, value) => { let success = false; - let config_json = getConfigFile(); - let path = CONFIG_ITEMS[key]['path']; + let config_json = exports.getConfigFile(); + let path = exports.CONFIG_ITEMS[key]['path']; let element_name = getElementNameInConfig(path); let parent_path = getParentPath(path); let parent_object = Object.byString(config_json, parent_path); @@ -118,20 +127,18 @@ function setConfigItem(key, value) { parent_parent_object[parent_parent_single_key] = {}; parent_object = Object.byString(config_json, parent_path); } + if (value === 'false') value = false; + if (value === 'true') value = true; + parent_object[element_name] = value; - if (value === 'false' || value === 'true') { - parent_object[element_name] = (value === 'true'); - } else { - parent_object[element_name] = value; - } - success = setConfigFile(config_json); + success = exports.setConfigFile(config_json); return success; } -function setConfigItems(items) { +exports.setConfigItems = (items) => { let success = false; - let config_json = getConfigFile(); + let config_json = exports.getConfigFile(); for (let i = 0; i < items.length; i++) { let key = items[i].key; let value = items[i].value; @@ -141,7 +148,7 @@ function setConfigItems(items) { value = (value === 'true'); } - let item_path = CONFIG_ITEMS[key]['path']; + let item_path = exports.CONFIG_ITEMS[key]['path']; let item_parent_path = getParentPath(item_path); let item_element_name = getElementNameInConfig(item_path); @@ -149,28 +156,34 @@ function setConfigItems(items) { item_parent_object[item_element_name] = value; } - success = setConfigFile(config_json); + success = exports.setConfigFile(config_json); return success; } -function globalArgsRequiresSafeDownload() { - const globalArgs = getConfigItem('ytdl_custom_args').split(',,'); +exports.globalArgsRequiresSafeDownload = () => { + const globalArgs = exports.getConfigItem('ytdl_custom_args').split(',,'); const argsThatRequireSafeDownload = ['--write-sub', '--write-srt', '--proxy']; const failedArgs = globalArgs.filter(arg => argsThatRequireSafeDownload.includes(arg)); return failedArgs && failedArgs.length > 0; } -module.exports = { - getConfigItem: getConfigItem, - setConfigItem: setConfigItem, - setConfigItems: setConfigItems, - getConfigFile: getConfigFile, - setConfigFile: setConfigFile, - configExistsCheck: configExistsCheck, - CONFIG_ITEMS: CONFIG_ITEMS, - initialize: initialize, - descriptors: {}, - globalArgsRequiresSafeDownload: globalArgsRequiresSafeDownload +exports.findChangedConfigItems = (old_config, new_config, key = '', changedConfigItems = [], depth = 0) => { + if (typeof old_config === 'object' && typeof new_config === 'object' && depth < 3) { + for (const key in old_config) { + if (Object.prototype.hasOwnProperty.call(new_config, key)) { + exports.findChangedConfigItems(old_config[key], new_config[key], key, changedConfigItems, depth + 1); + } + } + } else { + if (JSON.stringify(old_config) !== JSON.stringify(new_config)) { + changedConfigItems.push({ + key: key, + old_value: JSON.parse(JSON.stringify(old_config)), + new_value: JSON.parse(JSON.stringify(new_config)) + }); + } + } + return changedConfigItems; } const DEFAULT_CONFIG = { diff --git a/backend/test/tests.js b/backend/test/tests.js index b926b95..b069fe2 100644 --- a/backend/test/tests.js +++ b/backend/test/tests.js @@ -1037,6 +1037,66 @@ describe('Categories', async function() { }); }); +describe('Config', async function() { + it('findChangedConfigItems', async function() { + const old_config = { + "YoutubeDLMaterial": { + "test_object1": { + "test_prop1": true, + "test_prop2": false + }, + "test_object2": { + "test_prop3": { + "test_prop3_1": true, + "test_prop3_2": false + }, + "test_prop4": false + }, + "test_object3": { + "test_prop5": { + "test_prop5_1": true, + "test_prop5_2": false + }, + "test_prop6": false + } + } + }; + + const new_config = { + "YoutubeDLMaterial": { + "test_object1": { + "test_prop1": false, + "test_prop2": false + }, + "test_object2": { + "test_prop3": { + "test_prop3_1": false, + "test_prop3_2": false + }, + "test_prop4": true + }, + "test_object3": { + "test_prop5": { + "test_prop5_1": true, + "test_prop5_2": false + }, + "test_prop6": true + } + } + }; + + const changes = config_api.findChangedConfigItems(old_config, new_config); + assert(changes[0]['key'] === 'test_prop1' && changes[0]['old_value'] === true && changes[0]['new_value'] === false); + assert(changes[1]['key'] === 'test_prop3' && + changes[1]['old_value']['test_prop3_1'] === true && + changes[1]['new_value']['test_prop3_1'] === false && + changes[1]['old_value']['test_prop3_2'] === false && + changes[1]['new_value']['test_prop3_2'] === false); + assert(changes[2]['key'] === 'test_prop4' && changes[2]['old_value'] === false && changes[2]['new_value'] === true); + assert(changes[3]['key'] === 'test_prop6' && changes[3]['old_value'] === false && changes[3]['new_value'] === true); + }); +}); + const generateEmptyVideoFile = async (file_path) => { if (fs.existsSync(file_path)) fs.unlinkSync(file_path); return await exec(`ffmpeg -t 1 -f lavfi -i color=c=black:s=640x480 -c:v libx264 -tune stillimage -pix_fmt yuv420p "${file_path}"`); From 3151200d33a2a393d6f6ba685d4fe802473fca1f Mon Sep 17 00:00:00 2001 From: Tzahi12345 Date: Wed, 29 Nov 2023 00:52:48 -0500 Subject: [PATCH 2/5] Updated findChangedConfigItems function --- backend/config.js | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/backend/config.js b/backend/config.js index c981867..afb02fd 100644 --- a/backend/config.js +++ b/backend/config.js @@ -167,17 +167,18 @@ exports.globalArgsRequiresSafeDownload = () => { return failedArgs && failedArgs.length > 0; } -exports.findChangedConfigItems = (old_config, new_config, key = '', changedConfigItems = [], depth = 0) => { +exports.findChangedConfigItems = (old_config, new_config, path = '', changedConfigItems = [], depth = 0) => { if (typeof old_config === 'object' && typeof new_config === 'object' && depth < 3) { for (const key in old_config) { if (Object.prototype.hasOwnProperty.call(new_config, key)) { - exports.findChangedConfigItems(old_config[key], new_config[key], key, changedConfigItems, depth + 1); + exports.findChangedConfigItems(old_config[key], new_config[key], `${path}${path ? '.' : ''}${key}`, changedConfigItems, depth + 1); } } } else { if (JSON.stringify(old_config) !== JSON.stringify(new_config)) { + const key = getConfigItemKeyByPath(path); changedConfigItems.push({ - key: key, + key: key ? key : path.split('.')[path.split('.').length - 1], // return key in CONFIG_ITEMS or the object key old_value: JSON.parse(JSON.stringify(old_config)), new_value: JSON.parse(JSON.stringify(new_config)) }); @@ -186,6 +187,12 @@ exports.findChangedConfigItems = (old_config, new_config, key = '', changedConfi return changedConfigItems; } +function getConfigItemKeyByPath(path) { + const found_item = Object.values(exports.CONFIG_ITEMS).find(item => item.path === path); + if (found_item) return found_item['key']; + else return null; +} + const DEFAULT_CONFIG = { "YoutubeDLMaterial": { "Host": { From 2c974030279e5c97169a7f5e5a7a5d777b076f42 Mon Sep 17 00:00:00 2001 From: Tzahi12345 Date: Wed, 29 Nov 2023 00:53:56 -0500 Subject: [PATCH 3/5] Added ability to request video downloads through telegram bots --- backend/app.js | 24 +++++++++++++++- backend/notifications.js | 59 +++++++++++++++++++++++++++++++++++----- 2 files changed, 75 insertions(+), 8 deletions(-) diff --git a/backend/app.js b/backend/app.js index e183a0c..114f7ae 100644 --- a/backend/app.js +++ b/backend/app.js @@ -685,7 +685,7 @@ app.use(function(req, res, next) { next(); } else if (req.query.apiKey && config_api.getConfigItem('ytdl_use_api_key') && req.query.apiKey === config_api.getConfigItem('ytdl_api_key')) { next(); - } else if (req.path.includes('/api/stream/') || req.path.includes('/api/thumbnail/') || req.path.includes('/api/rss')) { + } else if (req.path.includes('/api/stream/') || req.path.includes('/api/thumbnail/') || req.path.includes('/api/rss') || req.path.includes('/api/telegramRequest')) { next(); } else { logger.verbose(`Rejecting request - invalid API use for endpoint: ${req.path}. API key received: ${req.query.apiKey}`); @@ -1784,6 +1784,10 @@ app.post('/api/cancelDownload', optionalJwt, async (req, res) => { app.post('/api/getTasks', optionalJwt, async (req, res) => { const tasks = await db_api.getRecords('tasks'); for (let task of tasks) { + if (!tasks_api.TASKS[task['key']]) { + logger.verbose(`Task ${task['key']} does not exist!`); + continue; + } if (task['schedule']) task['next_invocation'] = tasks_api.TASKS[task['key']]['job'].nextInvocation().getTime(); } res.send({tasks: tasks}); @@ -2092,6 +2096,24 @@ app.post('/api/deleteAllNotifications', optionalJwt, async (req, res) => { res.send({success: success}); }); +app.post('/api/telegramRequest', async (req, res) => { + if (!req.body.message && !req.body.message.text) { + logger.error('Invalid Telegram request received!'); + res.sendStatus(400); + return; + } + const text = req.body.message.text; + const regex_exp = /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)?/gi; + const url_regex = new RegExp(regex_exp); + if (text.match(url_regex)) { + downloader_api.createDownload(text, 'video', {}, req.query.user_uid ? req.query.user_uid : null); + res.sendStatus(200); + } else { + logger.error('Invalid Telegram request received! Make sure you only send a valid URL.'); + res.sendStatus(400); + } +}); + // rss feed app.get('/api/rss', async function (req, res) { diff --git a/backend/notifications.js b/backend/notifications.js index 0cb51d1..a2f4810 100644 --- a/backend/notifications.js +++ b/backend/notifications.js @@ -8,11 +8,14 @@ const { uuid } = require('uuidv4'); const fetch = require('node-fetch'); const { gotify } = require("gotify"); -const TelegramBot = require('node-telegram-bot-api'); +const TelegramBotAPI = require('node-telegram-bot-api'); +let telegram_bot = null; const REST = require('@discordjs/rest').REST; const API = require('@discordjs/core').API; const EmbedBuilder = require('@discordjs/builders').EmbedBuilder; +const debugMode = process.env.YTDL_MODE === 'debug'; + const NOTIFICATION_TYPE_TO_TITLE = { task_finished: 'Task finished', download_complete: 'Download complete', @@ -113,6 +116,8 @@ function notificationEnabled(type) { return config_api.getConfigItem('ytdl_enable_notifications') && (config_api.getConfigItem('ytdl_enable_all_notifications') || config_api.getConfigItem('ytdl_allowed_notification_types').includes(type)); } +// ntfy + function sendNtfyNotification({body, title, type, url, thumbnail}) { logger.verbose('Sending notification to ntfy'); fetch(config_api.getConfigItem('ytdl_ntfy_topic_url'), { @@ -127,6 +132,8 @@ function sendNtfyNotification({body, title, type, url, thumbnail}) { }); } +// Gotify + async function sendGotifyNotification({body, title, type, url, thumbnail}) { logger.verbose('Sending notification to gotify'); await gotify({ @@ -145,15 +152,49 @@ async function sendGotifyNotification({body, title, type, url, thumbnail}) { }); } -async function sendTelegramNotification({body, title, type, url, thumbnail}) { - logger.verbose('Sending notification to Telegram'); +// Telegram + +setupTelegramBot(); +config_api.config_updated.subscribe(change => { + const use_telegram_api = config_api.getConfigItem('ytdl_use_telegram_API'); const bot_token = config_api.getConfigItem('ytdl_telegram_bot_token'); - const chat_id = config_api.getConfigItem('ytdl_telegram_chat_id'); - const bot = new TelegramBot(bot_token); - if (thumbnail) await bot.sendPhoto(chat_id, thumbnail); - bot.sendMessage(chat_id, `${title}\n\n${body}\n${url}`, {parse_mode: 'HTML'}); + if (!use_telegram_api || !bot_token) return; + if (!change) return; + if (change['key'] === 'ytdl_use_telegram_API' || change['key'] === 'ytdl_telegram_bot_token') { + logger.debug('Telegram bot setting up'); + setupTelegramBot(); + } +}); + +async function setupTelegramBot() { + const use_telegram_api = config_api.getConfigItem('ytdl_use_telegram_API'); + const bot_token = config_api.getConfigItem('ytdl_telegram_bot_token'); + if (!use_telegram_api || !bot_token) return; + + telegram_bot = new TelegramBotAPI(bot_token); + const webhook_url = `${utils.getBaseURL()}/api/telegramRequest`; + telegram_bot.setWebHook(webhook_url); } +async function sendTelegramNotification({body, title, type, url, thumbnail}) { + if (!telegram_bot){ + logger.error('Telegram bot not found!'); + return; + } + + const chat_id = config_api.getConfigItem('ytdl_telegram_chat_id'); + if (!chat_id){ + logger.error('Telegram chat ID required!'); + return; + } + + logger.verbose('Sending notification to Telegram'); + if (thumbnail) await telegram_bot.sendPhoto(chat_id, thumbnail); + telegram_bot.sendMessage(chat_id, `${title}\n\n${body}\n${url}`, {parse_mode: 'HTML'}); +} + +// Discord + async function sendDiscordNotification({body, title, type, url, thumbnail}) { const discord_webhook_url = config_api.getConfigItem('ytdl_discord_webhook_url'); const url_split = discord_webhook_url.split('webhooks/'); @@ -177,6 +218,8 @@ async function sendDiscordNotification({body, title, type, url, thumbnail}) { return result; } +// Slack + function sendSlackNotification({body, title, type, url, thumbnail}) { const slack_webhook_url = config_api.getConfigItem('ytdl_slack_webhook_url'); logger.verbose(`Sending slack notification to ${slack_webhook_url}`); @@ -236,6 +279,8 @@ function sendSlackNotification({body, title, type, url, thumbnail}) { }); } +// Generic + function sendGenericNotification(data) { const webhook_url = config_api.getConfigItem('ytdl_webhook_url'); logger.verbose(`Sending generic notification to ${webhook_url}`); From 91c2fdc7016b7216136195d18c1fc62cc997b173 Mon Sep 17 00:00:00 2001 From: Tzahi12345 Date: Wed, 29 Nov 2023 21:04:27 -0500 Subject: [PATCH 4/5] Failed telegram request now sends a response to telegram --- backend/app.js | 2 ++ backend/notifications.js | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/backend/app.js b/backend/app.js index 114f7ae..4784f2a 100644 --- a/backend/app.js +++ b/backend/app.js @@ -30,6 +30,7 @@ const twitch_api = require('./twitch'); const youtubedl_api = require('./youtube-dl'); const archive_api = require('./archive'); const files_api = require('./files'); +const notifications_api = require('./notifications'); var app = express(); @@ -2110,6 +2111,7 @@ app.post('/api/telegramRequest', async (req, res) => { res.sendStatus(200); } else { logger.error('Invalid Telegram request received! Make sure you only send a valid URL.'); + notifications_api.sendTelegramNotification({title: 'Invalid Telegram Request', body: 'Make sure you only send a valid URL.', url: text}); res.sendStatus(400); } }); diff --git a/backend/notifications.js b/backend/notifications.js index a2f4810..af5be13 100644 --- a/backend/notifications.js +++ b/backend/notifications.js @@ -59,7 +59,7 @@ exports.sendNotification = async (notification) => { sendGotifyNotification(data); } if (config_api.getConfigItem('ytdl_use_telegram_API') && config_api.getConfigItem('ytdl_telegram_bot_token') && config_api.getConfigItem('ytdl_telegram_chat_id')) { - sendTelegramNotification(data); + exports.sendTelegramNotification(data); } if (config_api.getConfigItem('ytdl_webhook_url')) { sendGenericNotification(data); @@ -176,7 +176,7 @@ async function setupTelegramBot() { telegram_bot.setWebHook(webhook_url); } -async function sendTelegramNotification({body, title, type, url, thumbnail}) { +exports.sendTelegramNotification = async ({body, title, type, url, thumbnail}) => { if (!telegram_bot){ logger.error('Telegram bot not found!'); return; From 8f246d905fc8ede0b29b741c901544746e27a163 Mon Sep 17 00:00:00 2001 From: Tzahi12345 Date: Thu, 30 Nov 2023 00:28:28 -0500 Subject: [PATCH 5/5] Added ability to set a webhook proxy for telegram requests Fixed issue where config changes were broadcast before they were written --- backend/appdata/default.json | 1 + backend/config.js | 3 ++- backend/consts.js | 4 ++++ backend/notifications.js | 9 ++++----- src/app/settings/settings.component.html | 7 +++++++ 5 files changed, 18 insertions(+), 6 deletions(-) diff --git a/backend/appdata/default.json b/backend/appdata/default.json index 1bd7305..ea1b448 100644 --- a/backend/appdata/default.json +++ b/backend/appdata/default.json @@ -49,6 +49,7 @@ "use_telegram_API": false, "telegram_bot_token": "", "telegram_chat_id": "", + "telegram_webhook_proxy": "", "webhook_URL": "", "discord_webhook_URL": "", "slack_webhook_URL": "" diff --git a/backend/config.js b/backend/config.js index afb02fd..43f7d17 100644 --- a/backend/config.js +++ b/backend/config.js @@ -85,11 +85,11 @@ exports.getConfigFile = () => { exports.setConfigFile = (config) => { try { const old_config = exports.getConfigFile(); + fs.writeFileSync(configPath, JSON.stringify(config, null, 2)); const changes = exports.findChangedConfigItems(old_config, config); if (changes.length > 0) { for (const change of changes) exports.config_updated.next(change); } - fs.writeFileSync(configPath, JSON.stringify(config, null, 2)); return true; } catch(e) { return false; @@ -239,6 +239,7 @@ const DEFAULT_CONFIG = { "use_telegram_API": false, "telegram_bot_token": "", "telegram_chat_id": "", + "telegram_webhook_proxy": "", "webhook_URL": "", "discord_webhook_URL": "", "slack_webhook_URL": "", diff --git a/backend/consts.js b/backend/consts.js index b092ca5..86cbb8c 100644 --- a/backend/consts.js +++ b/backend/consts.js @@ -154,6 +154,10 @@ exports.CONFIG_ITEMS = { 'key': 'ytdl_telegram_chat_id', 'path': 'YoutubeDLMaterial.API.telegram_chat_id' }, + 'ytdl_telegram_webhook_proxy': { + 'key': 'ytdl_telegram_webhook_proxy', + 'path': 'YoutubeDLMaterial.API.telegram_webhook_proxy' + }, 'ytdl_webhook_url': { 'key': 'ytdl_webhook_url', 'path': 'YoutubeDLMaterial.API.webhook_URL' diff --git a/backend/notifications.js b/backend/notifications.js index af5be13..5000029 100644 --- a/backend/notifications.js +++ b/backend/notifications.js @@ -14,8 +14,6 @@ const REST = require('@discordjs/rest').REST; const API = require('@discordjs/core').API; const EmbedBuilder = require('@discordjs/builders').EmbedBuilder; -const debugMode = process.env.YTDL_MODE === 'debug'; - const NOTIFICATION_TYPE_TO_TITLE = { task_finished: 'Task finished', download_complete: 'Download complete', @@ -160,7 +158,7 @@ config_api.config_updated.subscribe(change => { const bot_token = config_api.getConfigItem('ytdl_telegram_bot_token'); if (!use_telegram_api || !bot_token) return; if (!change) return; - if (change['key'] === 'ytdl_use_telegram_API' || change['key'] === 'ytdl_telegram_bot_token') { + if (change['key'] === 'ytdl_use_telegram_API' || change['key'] === 'ytdl_telegram_bot_token' || change['key'] === 'ytdl_telegram_webhook_proxy') { logger.debug('Telegram bot setting up'); setupTelegramBot(); } @@ -170,9 +168,10 @@ async function setupTelegramBot() { const use_telegram_api = config_api.getConfigItem('ytdl_use_telegram_API'); const bot_token = config_api.getConfigItem('ytdl_telegram_bot_token'); if (!use_telegram_api || !bot_token) return; - + telegram_bot = new TelegramBotAPI(bot_token); - const webhook_url = `${utils.getBaseURL()}/api/telegramRequest`; + const webhook_proxy = config_api.getConfigItem('ytdl_telegram_webhook_proxy'); + const webhook_url = webhook_proxy ? webhook_proxy : `${utils.getBaseURL()}/api/telegramRequest`; telegram_bot.setWebHook(webhook_url); } diff --git a/src/app/settings/settings.component.html b/src/app/settings/settings.component.html index 6d4ae4d..950820f 100644 --- a/src/app/settings/settings.component.html +++ b/src/app/settings/settings.component.html @@ -426,6 +426,13 @@ How do I get the chat ID? +
+ + Telegram webhook proxy + + Example service + +