UNPKG

node-red-contrib-chatbot

Version:

REDBot a Chat bot for a full featured chat bot for Telegram, Facebook Messenger and Slack. Almost no coding skills required

1,255 lines (1,165 loc) 41.4 kB
const _ = require('lodash'); const validators = require('../helpers/validators'); const dayjs = require('dayjs'); const { ChatExpress } = require('chat-platform'); // node-telegram-bot-api is ESM-only since v1.0.0, so it cannot be loaded with a // synchronous require() on Node < 22. Load it lazily via dynamic import() instead // (the class is only instantiated inside the async onStart handler). let TelegramBot; const getTelegramBot = async () => { if (TelegramBot == null) { ({ default: TelegramBot } = await import('node-telegram-bot-api')); } return TelegramBot; }; const fetch = require('node-fetch'); const utils = require('../../lib/helpers/utils'); const helpers = require('../../lib/helpers/regexps'); const { getMessageId, when, params } = require('../../lib/helpers/utils'); // set the messageId in a returning payload const setMessageId = (message, messageId) => ({ ...message, payload: { ...message.payload, messageId } }); const MESSAGE_MAX_SIZE = 4096; // rich messages support a much larger payload than regular messages // (up to 32768 UTF-8 bytes / 500 top-level blocks) const MESSAGE_RICH_MAX_SIZE = 32768; const Telegram = new ChatExpress({ color: '#336699', inboundMessageEvent: 'message', transport: 'telegram', transportDescription: 'Telegram', relaxChatId: true, // sometimes chatId is not necessary (for example inline_query_id) chatIdKey: function(payload) { return payload.chat != null ? payload.chat.id : null; }, userIdKey: function(payload) { return payload.from.id; }, messageIdKey: function(payload) { return payload.message_id; }, tsKey: function(payload) { return dayjs.unix(payload.date).toISOString(); }, type: function() { // todo remove this }, language: function(payload) { return payload != null && payload.from != null ? payload.from.language_code : null; }, onStop: function() { const options = this.getOptions(); return new Promise(function(resolve) { if (options.connector != null) { if (options.connectMode === 'webHook') { // cancel the web hook on stop options.connector.deleteWebhook() .then(function() { options.connector = null; resolve(); }, function() { resolve(); }); } else if (options.connectMode === 'polling') { // stop polling options.connector.stopPolling() .then(function() { options.connector = null; resolve(); }, function() { resolve(); }); } } else { resolve(); } }); }, onStart: async function() { const options = this.getOptions(); const TelegramBot = await getTelegramBot(); if (options.connectMode === 'webHook') { options.connector = new TelegramBot(options.token); return options.connector.setWebhook(options.webHook); } else if (options.connectMode === 'polling') { options.connector = new TelegramBot(options.token, { polling: { params: { timeout: 10 }, interval: !isNaN(parseInt(options.polling, 10)) ? parseInt(options.polling, 10) : 1000 } }); return true; } else { throw 'Unkown connection mode'; } }, routes: { '/redbot/telegram/test': function(req, res) { res.send('ok'); }, '/redbot/telegram': function(req, res) { const chatServer = this; const json = req.body; if (json.message != null) { chatServer.receive(json.message); } else if (json.edited_message != null) { json.edited_message._edited = true; chatServer.receive(json.edited_message); } else if (json.channel_post != null) { chatServer.receive(json.channel_post); } else if (json.edited_channel_post != null) { json.edited_channel_post._edited = true; chatServer.receive(json.edited_channel_post); } else if (json.callback_query != null) { handleCallbackQuery.call(chatServer, json.callback_query); } else if (json.inline_query != null) { handleInlineQuery.call(chatServer, json.inline_query); } else if (json.pre_checkout_query != null) { handlePreCheckoutQuery.call(chatServer, json.pre_checkout_query); } else if (json.shipping_query != null) { handleShippingQuery.call(chatServer, json.shipping_query); } res.send({ status: 'ok' }); } }, multiWebHook: true, webHookScheme: function() { const { connectMode, webHook } = this.getOptions(); // if polling return the base link, if not extract the subpath after /redbot/telegram/ if (connectMode === 'webHook') { const match = (webHook || '').match(/\/redbot\/telegram\/(.*)$/); return match != null ? match[1] : ''; } return ''; }, events: { inline_query: function(botMsg) { handleInlineQuery.call(this, botMsg); }, callback_query: function(botMsg) { handleCallbackQuery.call(this, botMsg); }, pre_checkout_query: function(botMsg) { handlePreCheckoutQuery.call(this, botMsg); }, shipping_query: function(botMsg) { handleShippingQuery.call(this, botMsg); }, channel_post: function(botMsg) { this.receive(botMsg); }, edited_channel_post: function(botMsg) { botMsg._edited = true; this.receive(botMsg); }, edited_message: function(botMsg) { botMsg._edited = true; this.receive(botMsg); } } }); // Shared event handlers — used by both webhook routes and polling events function handleInlineQuery(botMsg) { botMsg.inlineQueryId = botMsg.id; delete botMsg.id; this.receive(botMsg); } function handleCallbackQuery(botMsg) { this.callbackQuery(botMsg); } function handlePreCheckoutQuery(botMsg) { const chatServer = this; const connector = this.getOptions().connector; connector.answerPreCheckoutQuery(botMsg.id, true) .then( function() {}, function() { chatServer.error('Error on .answerPreCheckoutQuery(), checkout: ' + botMsg.id); } ); } function handleShippingQuery(botMsg) { this.receive({ from: botMsg.from, shipping_query_id: botMsg.id, invoice_payload: botMsg.invoice_payload, shipping_address: botMsg.shipping_address }); } // mark edited messages so downstream nodes can distinguish them Telegram.in(function(message) { if (message.originalMessage._edited === true) { message.payload.edited = true; } return Promise.resolve(message); }); // propagate generic Telegram metadata fields (reply, forwarding, topics, business) Telegram.in(function(message) { const botMsg = message.originalMessage; if (botMsg.reply_to_message != null) { message.payload.replyToMessage = botMsg.reply_to_message; } if (botMsg.forward_origin != null) { message.payload.forwardOrigin = botMsg.forward_origin; } if (botMsg.message_thread_id != null) { message.payload.messageThreadId = botMsg.message_thread_id; } if (botMsg.business_connection_id != null) { message.payload.businessConnectionId = botMsg.business_connection_id; } return Promise.resolve(message); }); // detect new user in group Telegram.in(function(message) { var botMsg = message.originalMessage; return new Promise(function(resolve) { if (_.isArray(botMsg.new_chat_members) && !_.isEmpty(botMsg.new_chat_members)) { message.payload.newUsers = botMsg.new_chat_members; message.payload.type = 'event'; message.payload.eventType = 'new-user'; resolve(message); } else { resolve(message); } }); }); // detect user leaving group Telegram.in(function(message) { var botMsg = message.originalMessage; return new Promise(function(resolve) { if (botMsg.left_chat_member != null) { message.payload.leftUser = botMsg.left_chat_member; message.payload.type = 'event'; message.payload.eventType = 'user-left'; resolve(message); } else { resolve(message); } }); }); // detect dice Telegram.in(function(message) { var botMsg = message.originalMessage; return new Promise(function(resolve) { if (botMsg.dice != null) { message.payload.content = botMsg.dice; message.payload.type = 'dice'; resolve(message); } else { resolve(message); } }); }); // detect poll Telegram.in(function(message) { var botMsg = message.originalMessage; return new Promise(function(resolve) { if (botMsg.poll != null) { message.payload.content = botMsg.poll; message.payload.type = 'poll'; resolve(message); } else { resolve(message); } }); }); // detect venue (location with title/address) Telegram.in(function(message) { var botMsg = message.originalMessage; return new Promise(function(resolve) { if (botMsg.venue != null) { message.payload.content = botMsg.venue; message.payload.type = 'venue'; resolve(message); } else { resolve(message); } }); }); // detect inline query Telegram.in(function(message) { return new Promise(function(resolve) { if (message.originalMessage.query != null) { message.payload.content = message.originalMessage.query; message.payload.type = 'inline-query'; resolve(message); } else { resolve(message); } }); }); Telegram.in(function(message) { var chatServer = this; return new Promise(function(resolve, reject) { var connector = message.client(); if (message.originalMessage.sticker != null) { connector.getFileLink(message.originalMessage.sticker.file_id) .then(function(imageUrl) { return chatServer.request({ url: imageUrl }); }) .then( function(image) { message.payload.type = 'photo'; message.payload.content = image; resolve(message); }, function(err) { reject(err); } ); } else { resolve(message); } }); }); Telegram.in(function(message) { return new Promise(function(resolve) { if (_.isString(message.originalMessage.text) && !_.isEmpty(message.originalMessage.text)) { message.payload.content = message.originalMessage.text; message.payload.type = 'message'; if (helpers.isCommand(message.originalMessage.text)) { message.payload.arguments = message.originalMessage.text.split(' ').slice(1); } resolve(message); } else { resolve(message); } }); }); Telegram.in(function(message) { var botMsg = message.originalMessage; return new Promise(function(resolve) { if (botMsg.location != null) { message.payload.content = botMsg.location; message.payload.type = 'location'; resolve(message); } else { resolve(message); } }); }); Telegram.in(function(message) { var botMsg = message.originalMessage; return new Promise(function(resolve) { if (botMsg.contact != null) { message.payload.content = botMsg.contact; message.payload.type = 'contact'; resolve(message); } else { resolve(message); } }); }); Telegram.in(function(message) { var botMsg = message.originalMessage; return new Promise(function(resolve) { if (botMsg.successful_payment != null) { message.payload.content = botMsg.successful_payment; message.payload.type = 'payment'; resolve(message); } else { resolve(message); } }); }); Telegram.in(function(message) { var botMsg = message.originalMessage; return new Promise(function(resolve) { if (botMsg.shipping_query_id != null) { message.payload.shippingQueryId = botMsg.shipping_query_id; message.payload.payload = botMsg.payload; message.payload.shippingAddress = botMsg.shipping_address; message.payload.type = 'invoice-shipping'; resolve(message); } else { resolve(message); } }); }); Telegram.in(async function(message) { const context = message.chat(); const vars = {}; const { firstName, lastName, username } = await when(context.get('firstName', 'lastName', 'username')) || {}; // only change if not defined if (_.isEmpty(firstName) && !_.isEmpty(message.originalMessage.from.first_name)) { vars.firstName = message.originalMessage.from.first_name; } if (_.isEmpty(lastName) && !_.isEmpty(message.originalMessage.from.last_name)) { vars.lastName = message.originalMessage.from.last_name; } if (_.isEmpty(username) && !_.isEmpty(message.originalMessage.from.username)) { vars.username = message.originalMessage.from.username; } // store back if (!_.isEmpty(vars)) { await when(context.set(vars)) } return message; }); Telegram.in(async function(message) { const options = this.getOptions(); const connector = options.connector; const skipMediaFiles = options.skipMediaFiles === true; const botMsg = message.originalMessage; const chatServer = this; let type = null; let fileId = null; let duration = null; // download one of these binary types // note: order matters — animation arrives with a `document` sibling, so test it first if (botMsg.animation != null) { type = 'animation'; fileId = botMsg.animation.file_id; duration = botMsg.animation.duration; } else if (botMsg.photo != null) { type = 'photo'; fileId = _.last(botMsg.photo).file_id; } else if (botMsg.video != null) { type = 'video'; fileId = botMsg.video.file_id; } else if (botMsg.voice != null) { type = 'audio'; fileId = botMsg.voice.file_id; duration = botMsg.voice.duration; } else if (botMsg.audio != null) { type = 'audio-file'; fileId = botMsg.audio.file_id; duration = botMsg.audio.duration; } else if (botMsg.document != null) { type = 'document'; fileId = botMsg.document.file_id; } else if (botMsg.video_note != null) { type = 'video_note'; fileId = botMsg.video_note.file_id; duration = botMsg.video_note.duration; } // if not one of these pass thru if (type != null) { // skip media files (for example for performance reason) if (skipMediaFiles) { // eslint-disable-next-line no-console console.log(`Skipped download file id (${fileId})`); return message; } message.payload.type = type; try { const path = await connector.getFileLink(fileId); const buffer = await chatServer.downloadFile(path); message.payload.content = buffer; message.payload.caption = botMsg.caption; if (botMsg.caption_entities != null) { message.payload.captionEntities = botMsg.caption_entities; } message.payload.duration = duration; return message; } catch(e) { throw 'Error downloading photo.' + e; } } return message; }); Telegram.out('photo', async function(message) { const connector = this.getOptions().connector; const param = params(message); const result = await connector.sendPhoto(message.payload.chatId, message.payload.content, { caption: message.payload.caption, disable_notification: param('silent', false), parse_mode: param('parseMode', null), has_spoiler: param('hasSpoiler', false), protect_content: param('protectContent', false), show_caption_above_media: param('showCaptionAboveMedia', false), reply_parameters: param('replyToMessage', false) ? { message_id: getMessageId(message) } : undefined, message_effect_id: param('messageEffectId', null), }); return setMessageId(message, result.message_id); }); Telegram.out('video', async function(message) { const connector = this.getOptions().connector; const param = params(message); const result = await connector.sendVideo( message.payload.chatId, message.payload.content, { caption: message.payload.caption, disable_notification: param('silent', false), parse_mode: param('parseMode', null), has_spoiler: param('hasSpoiler', false), protect_content: param('protectContent', false), reply_parameters: param('replyToMessage', false) ? { message_id: getMessageId(message) } : undefined, message_effect_id: param('messageEffectId', null), }, { contentType: message.payload.mimeType, filename: message.payload.filename } ); return setMessageId(message, result.message_id); }); Telegram.out('document', async function(message) { const connector = this.getOptions().connector; const param = params(message); const result = await connector.sendDocument( message.payload.chatId, message.payload.content, { caption: message.payload.caption, parse_mode: param('parseMode', null), disable_notification: param('silent', false), protect_content: param('protectContent', false), reply_parameters: param('replyToMessage', false) ? { message_id: getMessageId(message) } : undefined, message_effect_id: param('messageEffectId', null), }, { // remove extension, telegram will put on the end filename: !_.isEmpty(message.payload.filename) ? message.payload.filename.replace(/\.[^.]+$/, '') : null } ); return setMessageId(message, result.message_id); }); Telegram.out('audio', async function(message) { const connector = this.getOptions().connector; const param = params(message); const result = await connector.sendVoice( message.payload.chatId, message.payload.content, { caption: message.payload.caption, parse_mode: param('parseMode', null), duration: message.payload.duration, disable_notification: param('silent', false), protect_content: param('protectContent', false), reply_parameters: param('replyToMessage', false) ? { message_id: getMessageId(message) } : undefined, message_effect_id: param('messageEffectId', null), }, { filename: message.payload.filename } ); return setMessageId(message, result.message_id); }); Telegram.out('telegram-reset-buttons', async function(message) { const options = this.getOptions(); const connector = options.connector; const param = params(message); await connector .sendMessage( message.payload.chatId, message.payload.content, { reply_markup: { remove_keyboard: true }, parse_mode: param('parseMode', null), disable_web_page_preview: param('disableWebPagePreview', false), disable_notification: param('silent', false), reply_parameters: param('replyToMessage', false) ? { message_id: getMessageId(message) } : undefined, allow_paid_broadcast: param('allowPaidBroadcast', false), message_effect_id: param('messageEffectId', null), } ); return message; }); Telegram.out('telegram-buttons', async function(message) { const options = this.getOptions(); const connector = options.connector; const param = params(message); const chatServer = this; if (_.isEmpty(message.payload.content)) { throw 'Buttons node needs a non-empty message'; } const keyboard = [[]]; _.each(message.payload.buttons, button => { let json = null; switch(button.type) { case 'keyboardButton': json = button.label; break; case 'newline': keyboard.push([]); break; default: chatServer.warn('Telegram is not able to handle this button type "' + button.type + '"'); } if (json != null) { // add the button to the last row, if any keyboard[keyboard.length - 1].push(json); } }); const buttons = { reply_markup: JSON.stringify({ keyboard: keyboard, resize_keyboard: true, one_time_keyboard: _.isBoolean(message.payload.oneTimeKeyboard) ? message.payload.oneTimeKeyboard : false, is_persistent: _.isBoolean(message.payload.isPersistent) ? message.payload.isPersistent : false, input_field_placeholder: !_.isEmpty(message.payload.placeholder) ? message.payload.placeholder : undefined }), parse_mode: param('parseMode', null), disable_web_page_preview: param('disableWebPagePreview', false), disable_notification: param('silent', false), reply_parameters: param('replyToMessage', false) ? { message_id: getMessageId(message) } : undefined, allow_paid_broadcast: param('allowPaidBroadcast', false), message_effect_id: param('messageEffectId', null), }; // finally send const result = await connector.sendMessage( message.payload.chatId, message.payload.content, buttons ); return setMessageId(message, result.message_id); }); Telegram.out('request', async function(message) { const options = this.getOptions(); const connector = options.connector; const param = params(message); let keyboard = null; if (message.payload.requestType === 'location') { keyboard = [ [{ text: !_.isEmpty(message.payload.label) ? message.payload.label : 'Send your position', request_location: true }] ]; } else if (message.payload.requestType === 'phone-number') { keyboard = [ [{ text: !_.isEmpty(message.payload.label) ? message.payload.label : 'Send your phone number', request_contact: true }] ]; } if (keyboard != null) { const result = await connector .sendMessage(message.payload.chatId, message.payload.content, { reply_markup: JSON.stringify({ keyboard: keyboard, 'resize_keyboard': true, 'one_time_keyboard': true }), parse_mode: param('parseMode', null), disable_web_page_preview: param('disableWebPagePreview', false), disable_notification: param('silent', false), reply_parameters: param('replyToMessage', false) ? { message_id: getMessageId(message) } : undefined, allow_paid_broadcast: param('allowPaidBroadcast', false), message_effect_id: param('messageEffectId', null), }); return setMessageId(message, result.message_id); } else { throw 'Request type not supported'; } }); Telegram.out('inline-buttons', async function(message) { const options = this.getOptions(); const connector = options.connector; const chatServer = this; const context = message.chat(); const param = params(message); const modifyMessageId = param('modifyMessageId') || message.originalMessage.modifyMessageId; // for retro compatibility // create inline buttons, docs for this is https://core.telegram.org/bots/api#inlinekeyboardmarkup // create the first array of array const inlineKeyboard = [[]]; const chatId = message.payload.chatId; // cycle through buttons, add new line at the end if flag message.payload.buttons.forEach(button => { let json = null; switch(button.type) { case 'url': json = { text: button.label, url: button.url }; break; case 'postback': json = { text: button.label, callback_data: !_.isEmpty(button.value) ? button.value : button.label }; break; case 'newline': inlineKeyboard.push([]); break; default: chatServer.warn('Telegram is not able to handle this button type "' + button.type + '"'); } if (json != null) { // add the button to the last row, if any inlineKeyboard[inlineKeyboard.length - 1].push(json); } }); // store the last buttons, this will be handled by the receiver if (connector.lastInlineButtons == null) { connector.lastInlineButtons = {}; } connector.lastInlineButtons[chatId] = message.payload.buttons; // send buttons or edit let result; if (modifyMessageId != null) { // change content of message await connector.editMessageText(message.payload.content, { chat_id: chatId, message_id: modifyMessageId }); // change buttons of message result = await connector.editMessageReplyMarkup(JSON.stringify({ inline_keyboard: inlineKeyboard }), { chat_id: chatId, message_id: modifyMessageId }); } else { // finally send result = await connector.sendMessage(chatId, message.payload.content, { reply_markup: JSON.stringify({ inline_keyboard: inlineKeyboard }), parse_mode: param('parseMode', null), disable_web_page_preview: param('disableWebPagePreview', false), disable_notification: param('silent', false), reply_parameters: param('replyToMessage', false) ? { message_id: getMessageId(message) } : undefined, allow_paid_broadcast: param('allowPaidBroadcast', false), message_effect_id: param('messageEffectId', null), }); } // store message id await when(context.set({ messageId: result.message_id, outboundMessageId: result.message_id })); return setMessageId(message, result.message_id); }); Telegram.out(async function(message) { const options = this.getOptions(); const connector = options.connector; const param = params(message); const deleteMessageId = param('deleteMessageId'); if (deleteMessageId) { connector.deleteMessage( message.originalMessage.chatId, deleteMessageId ); message.payload = {}; } return message; }); Telegram.out('message', async function(message) { const options = this.getOptions(); const connector = options.connector ; const context = message.chat(); const param = params(message); const modifyMessageId = param('modifyMessageId') || message.originalMessage.modifyMessageId; // for retro compatibility let content = message.payload.content; const richMessaging = param('richMessaging', false); let result = null; if (modifyMessageId != null) { result = await connector.editMessageText(content, { chat_id: message.payload.chatId, message_id: modifyMessageId, disable_notification: param('silent', false) }); } else if (richMessaging) { // rich messages allow a much larger payload, so chunk accordingly const chunks = utils.split(content, MESSAGE_RICH_MAX_SIZE); const parseMode = param('parseMode', null); for(let idx = 0; idx < chunks.length; idx++) { // InputRichMessage carries the formatting flavour: HTML or markdown const richMessage = parseMode === 'HTML' ? { html: chunks[idx] } : { markdown: chunks[idx] }; result = await connector.sendRichMessage(message.payload.chatId, richMessage, { disable_notification: param('silent', false), reply_parameters: param('replyToMessage', false) ? { message_id: getMessageId(message) } : undefined, protect_content: param('protectContent', false), allow_paid_broadcast: param('allowPaidBroadcast', false), message_effect_id: param('messageEffectId', null) }); } } else { const chunks = utils.split(content, MESSAGE_MAX_SIZE); const hasSpoiler = param('hasSpoiler', false); for(let idx = 0; idx < chunks.length; idx++) { // simulate spoiler with entities — Telegram ignores parse_mode when entities are set, // so we drop it here to avoid the surprise of silently-stripped formatting let entities = undefined; if (hasSpoiler) { entities = JSON.stringify([ { offset: 0, length: chunks[idx].length, type: 'spoiler' } ]); } result = await connector.sendMessage(message.payload.chatId, chunks[idx], { parse_mode: hasSpoiler ? undefined : param('parseMode', null), disable_web_page_preview: param('disableWebPagePreview', false), disable_notification: param('silent', false), reply_parameters: param('replyToMessage', false) ? { message_id: getMessageId(message) } : undefined, protect_content: param('protectContent', false), allow_paid_broadcast: param('allowPaidBroadcast', false), message_effect_id: param('messageEffectId', null), entities }); } } // store message id await when(context.set({ messageId: result.message_id, outboundMessageId: result.message_id })); return setMessageId(message, result.message_id); }); Telegram.out('action', async function(message) { const options = this.getOptions(); const connector = options.connector; await connector.sendChatAction(message.payload.chatId, message.payload.waitingType != null ? message.payload.waitingType : 'typing') return message; }); Telegram.out('location', async function(message) { const options = this.getOptions(); const connector = options.connector; const param = params(message); const result = await connector.sendLocation( message.payload.chatId, message.payload.content.latitude, message.payload.content.longitude, { ...message.payload.options, protect_content: param('protectContent', false), allow_paid_broadcast: param('allowPaidBroadcast', false), disable_notification: param('silent', false), message_effect_id: param('messageEffectId', null), } ); return setMessageId(message, result.message_id); }); Telegram.out('inline-query-answer', async function(message) { const options = this.getOptions(); const connector = options.connector; const inlineQueryId = message.originalMessage.inlineQueryId; // do some checks if (_.isEmpty(inlineQueryId)) { throw 'Empty inlineQueryId'; } if (!_.isArray(message.payload.content)) { throw 'Invalid inline query answer'; } const inlineOptions = {}; if (validators.integer(message.payload.caching)) { inlineOptions.cache_time = message.payload.caching; } if (validators.boolean(message.payload.personal)) { inlineOptions.is_personal = message.payload.personal; } await connector.answerInlineQuery(inlineQueryId, message.payload.content, inlineOptions); return message; }); Telegram.out('invoice', async function(message) { const options = this.getOptions(); const connector = options.connector; const param = params(message); if (_.isEmpty(options.providerToken)) { throw 'Missing providerToken in Telegram chatbot configuration, needed to send an invoice.'; } const invoiceOptions = { need_name: message.payload.needName, need_phone_number: message.payload.needPhoneNumber, need_email: message.payload.needEmail, need_shipping_address: message.payload.needShippingAddress, is_flexible: message.payload.isFlexible, protect_content: param('protectContent', false), start_parameter: message.payload.startParameter, message_effect_id: param('messageEffectId', null), }; const prices = _.map(message.payload.prices, price => { return { label: price.label, amount: Math.floor(parseFloat(price.amount) * 100) } }); if (!_.isEmpty(message.payload.photoUrl)) { invoiceOptions.photo_url = message.payload.photoUrl; invoiceOptions.photo_width = message.payload.photoWidth; invoiceOptions.photo_height = message.payload.photoHeight; } const result = await connector.sendInvoice( message.payload.chatId, message.payload.title, message.payload.description, message.payload.payload, options.providerToken, message.payload.currency, prices, invoiceOptions ); return setMessageId(message, result.message_id); }); Telegram.out('invoice-shipping', async function(message) { const options = this.getOptions(); const connector = options.connector; const shippingOptions = _.map(message.payload.shippingOptions, function(shippingOption) { return { id: shippingOption.id, title: shippingOption.label, prices: [{ label: shippingOption.id, amount: Math.floor(shippingOption.amount * 100) }] }; }); const canShip = !_.isEmpty(shippingOptions); await connector.answerShippingQuery(message.payload.shippingQueryId, canShip, { shipping_options: shippingOptions }); return message; }); Telegram.out('sticker', async function(message) { const options = this.getOptions(); const connector = options.connector; const param = params(message); const result = await connector.sendSticker( message.payload.chatId, message.payload.content, { disable_notification: param('silent', false), reply_parameters: param('replyToMessage', false) ? { message_id: getMessageId(message) } : undefined, message_effect_id: param('messageEffectId', null), } ); return setMessageId(message, result.message_id); }); Telegram.out('animation', async function(message) { const options = this.getOptions(); const connector = options.connector; const param = params(message); const result = await connector.sendAnimation( message.payload.chatId, message.payload.content, { disable_notification: param('silent', false), reply_parameters: param('replyToMessage', false) ? { message_id: getMessageId(message) } : undefined, message_effect_id: param('messageEffectId', null), } ); return setMessageId(message, result.message_id); }); Telegram.mixin({ downloadFile: async function (url, token) { const headers = token != null ? { Authorization: 'Bearer ' + token } : {}; const response = await fetch(url, { headers }); if (!response.ok) { throw new Error(`Failed to download file (${response.status} ${response.statusText})`); } return Buffer.from(await response.arrayBuffer()); }, callbackQuery: function(botMsg) { var chatServer = this; var options = this.getOptions(); var connector = options.connector; var chatId = botMsg.message.chat.id; var alert = false; var answer = null; if (connector.lastInlineButtons != null && connector.lastInlineButtons[chatId] != null) { // find the button with the right value, takes the answer and alert if any var button = _.find(connector.lastInlineButtons[chatId], {value: botMsg.data}); if (button != null) { answer = button.answer; alert = button.alert; } // do not remove from hash, the user could click again } // copy the "from" of the message containing user information, not chatbot detail botMsg.message.from = botMsg.from; // send answer back to client connector.answerCallbackQuery(botMsg.id, { text: answer, show_alert: alert }) .then(function() { // send through the message as usual botMsg.message.text = botMsg.data; chatServer.receive(botMsg.message); }); } }); const videoExtensions = ['.mp4', '.gif']; const audioExtensions = ['.mp3']; const documentExtensions = ['.pdf', '.gif', '.zip']; const photoExtensions = ['.jpg', '.jpeg', '.png', '.gif']; Telegram.registerMessageType('action', 'Action', 'Send an action message (like typing, ...)'); Telegram.registerMessageType('telegram-buttons', 'Buttons', 'Open keyboard buttons in the client'); Telegram.registerMessageType('command', 'Command', 'Detect command-like messages'); Telegram.registerMessageType('contact', 'Contact', 'Send a contact'); Telegram.registerMessageType('inline-buttons', 'Inline buttons', 'Send a message with inline buttons'); Telegram.registerMessageType('inline-query', 'Inline Query', 'Receives inline queries'); Telegram.registerMessageType('invoice', 'Invoice', 'Send a payment invoice'); Telegram.registerMessageType('invoice-shipping', 'Invoice Shipping Query', 'Define an invoice shipping option'); Telegram.registerMessageType('location', 'Location', 'Send a map location message'); Telegram.registerMessageType('message', 'Message', 'Send a plain text message'); Telegram.registerMessageType('payment', 'Payment', 'Payment received message'); Telegram.registerMessageType('request', 'Request', 'Trigger a request of location or contact'); Telegram.registerMessageType('sticker', 'Sticker', 'Send a sticker'); Telegram.registerMessageType( 'animation', 'Animation', 'Send an animation', file => { if (!_.isEmpty(file.extension) && !videoExtensions.includes(file.extension.toLowerCase())) { return `Unsupported file format for video node "${file.filename}", allowed formats: ${videoExtensions.join(', ')}`; } if (file.size > (50 * 1024 * 1024)) { return 'Video is too large, max 50 Mb'; } return null; } ); Telegram.registerMessageType('event', 'Event', 'Event from platform'); Telegram.registerMessageType( 'video', 'Video', 'Send video message', file => { if (!_.isEmpty(file.extension) && !videoExtensions.includes(file.extension.toLowerCase())) { return `Unsupported file format for video node "${file.filename}", allowed formats: ${videoExtensions.join(', ')}`; } if (file.size > (50 * 1024 * 1024)) { return 'Video is too large, max 50 Mb'; } return null; } ); Telegram.registerMessageType('video_note', 'Video Note', 'Video note (short video)'); Telegram.registerMessageType( 'document', 'Document', 'Send a document or generic file', file => { if (!_.isEmpty(file.extension) && !documentExtensions.includes(file.extension.toLowerCase())) { return `Unsupported file format for document node "${file.filename}", allowed formats: ${documentExtensions.join(', ')}`; } return null; } ); Telegram.registerMessageType( 'audio', 'Audio', 'Send an audio message', file => { if (!_.isEmpty(file.extension) && !audioExtensions.includes(file.extension.toLowerCase())) { return `Unsupported file format for audio node "${file.filename}", allowed formats: ${audioExtensions.join(', ')}`; } return null; } ); Telegram.registerMessageType( 'photo', 'Photo', 'Send a photo message', file => { if (!_.isEmpty(file.extension) && !photoExtensions.includes(file.extension.toLowerCase())) { return `Unsupported file format for image node "${file.filename}", allowed formats: ${photoExtensions.join(', ')}`; } } ); Telegram.registerParam( 'replyToMessage', 'boolean', { label: 'Reply to message', default: false } ); Telegram.registerParam( 'richMessaging', 'boolean', { label: 'Rich Messaging', default: false, description: 'Send the message with sendRichMessage, allowing bots to send rich messages with highly structured text (tables, headings, code blocks, collapsible details) and a much larger payload (up to 32768 UTF-8 bytes) than regular messages' } ); Telegram.registerParam( 'allowPaidBroadcast', 'boolean', { label: 'Allow paid broadcast', default: false, description: 'Allow up to 1000 messages per second in paid private channels' } ); Telegram.registerParam( 'hasSpoiler', 'boolean', { label: 'Cover the text, video or photo with spolier animation', default: false } ); Telegram.registerParam( 'showCaptionAboveMedia', 'boolean', { label: 'Show caption above media', default: false, description: 'Show the caption above the photo instead of below' } ); Telegram.registerParam( 'protectContent', 'boolean', { label: 'Protects the contents of the sent message from forwarding and saving', default: false } ); Telegram.registerParam( 'messageEffectId', 'select', { label: 'Message effect', default: null, description: 'Visual effect animation shown when the message is received (private chats only)', options: [ { value: null, label: 'None' }, { value: '5107584321108051014', label: '👍 Like' }, { value: '5159385139981059251', label: '❤️ Heart' }, { value: '5104841245755180586', label: '🔥 Fire' }, { value: '5104858069142078462', label: '👎 Dislike' }, { value: '5046509860389126442', label: '🎉 Party' }, { value: '5046589136895476101', label: '💩 Poop' }, ] } ); Telegram.registerParam( 'silent', 'boolean', { label: 'Silent', default: false, description: 'Deliver the message silently' } ); Telegram.registerParam( 'modifyMessageId', 'string', { label: 'Modify message', description: 'The id of the sent message to be modified', placeholder: 'Message Id', suggestions: [ '{{outboundMessageId}}', '{{inboundMessageId}}' ] } ); Telegram.registerParam( 'parseMode', 'select', { label: 'Parse Mode', default: 'Markdown', description: 'Parse message with HTML or Markdown', placeholder: 'Parse mode', options: [ { value: '', label: 'Select parse mode'}, { value: 'Markdown', label: 'Markdown'}, { value: 'MarkdownV2', label: 'MarkdownV2'}, { value: 'HTML', label: 'HTML' } ] } ); Telegram.registerParam( 'disableWebPagePreview', 'boolean', { label: 'Disable web page preview', description: 'Disables link previews for links in this message' } ); Telegram.registerParam( 'deleteMessageId', 'string', { label: 'Delete Message', description: 'Delete a message given the message id', placeholder: 'Message Id', suggestions: [ '{{outboundMessageId}}', '{{inboundMessageId}}' ] } ); Telegram.registerMessageType('dice', 'Dice', 'Receive a dice/emoji animation'); Telegram.registerMessageType('poll', 'Poll', 'Receive a poll'); Telegram.registerMessageType('venue', 'Venue', 'Receive a venue (location with title/address)'); Telegram.registerMessageType('audio-file', 'Audio File', 'Receive a music/audio file (not a voice note)'); Telegram.registerEvent('new-user', 'New user'); Telegram.registerEvent('user-left', 'User left'); module.exports = Telegram;