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
JavaScript
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;