node-red-contrib-telegrambot
Version:
Telegram bot nodes for Node-RED
909 lines (830 loc) • 44.8 kB
JavaScript
module.exports = function (RED) {
const path = require('path');
const { pipeline } = require('stream');
const fs = require('fs');
// --------------------------------------------------------------------------------------------
// The output node sends to the chat and passes the msg through.
// The payload needs three fields
// chatId : string destination chat
// type : string type of message to send
// content : message content
// The type is a string can be any of the following:
// message content is String
// photo content is String|stream.Stream|Buffer
// audio content is String|stream.Stream|Buffer
// document content is String|stream.Stream|Buffer
// sticker content is String|stream.Stream|Buffer
// video content is String|stream.Stream|Buffer
// voice content is String|stream.Stream|Buffer
// location content is an object that contains latitude and logitude
// contact content is full contact object
// mediaGroup content is array of mediaObject
// action content is one of the following:
// typing, upload_photo, record_video, upload_video, record_audio, upload_audio,
// upload_document, find_location, record_video_note, upload_video_note
function TelegramOutNode(config) {
RED.nodes.createNode(this, config);
let node = this;
this.bot = config.bot;
let haserroroutput = config.haserroroutput || false;
this.hasContent = function (msg) {
let hasContent;
if (msg.payload.content) {
hasContent = true;
} else {
node.warn('msg.payload.content is empty');
hasContent = false;
}
return hasContent;
};
this.start = function () {
node.status({
fill: 'green',
shape: 'ring',
text: 'connected',
});
};
this.stop = function () {
node.status({
fill: 'red',
shape: 'ring',
text: 'disconnected',
});
};
this.addChatIdToOptions = function (chatId, options) {
if (chatId !== undefined && options !== undefined) {
if (options.chat_id === undefined) {
options.chat_id = chatId;
}
}
};
// adds the caption of the message into the options.
this.addCaptionToMessageOptions = function (msg) {
let options = msg.payload.options;
if (options === undefined) {
options = {};
}
if (msg.payload.caption !== undefined) {
options.caption = msg.payload.caption;
}
msg.payload.options = options;
return msg;
};
this.processError = function (exception, msg, nodeSend, nodeDone) {
let errorMessage = 'Caught exception in sender node:\r\n' + exception + '\r\nwhen processing message: \r\n' + JSON.stringify(msg);
node.status({
fill: 'red',
shape: 'ring',
text: exception.message,
});
if (haserroroutput) {
let sendMessage = RED.util.cloneMessage(msg);
sendMessage.error = errorMessage;
nodeSend([null, sendMessage]);
} else {
if (nodeDone) {
node.error(errorMessage, msg);
nodeDone(errorMessage);
} else {
node.error(errorMessage, msg);
}
}
};
this.processResult = function (result, msg, nodeSend, nodeDone) {
if (result !== undefined) {
msg.payload.content = result;
msg.payload.sentMessageId = result.message_id;
nodeSend(msg);
}
if (nodeDone) {
nodeDone();
}
};
this.processMessage = function (chatId, msg, nodeSend, nodeDone) {
let telegramBot = this.config.getTelegramBot();
if (msg.payload.forward) {
// the message should be forwarded
let toChatId = msg.payload.forward.chatId;
let messageId = msg.payload.messageId;
telegramBot
.forwardMessage(toChatId, chatId, messageId, msg.payload.forward.options)
.catch(function (ex) {
node.processError(ex, msg, nodeSend, nodeDone);
})
.then(function (result) {
node.processResult(result, msg, nodeSend, nodeDone);
});
} else if (msg.payload.copy) {
// the message should be copied
let toChatId = msg.payload.copy.chatId;
let messageId = msg.payload.messageId;
telegramBot
.copyMessage(toChatId, chatId, messageId, msg.payload.copy.options)
.catch(function (ex) {
node.processError(ex, msg, nodeSend, nodeDone);
})
.then(function (result) {
node.processResult(result, msg, nodeSend, nodeDone);
});
} else if (msg.payload.download) {
let fileId = msg.payload.download.fileId;
let filePath = msg.payload.download.filePath;
let fileName = msg.payload.download.fileName;
node.downloadFile(fileId, filePath, fileName)
.catch(function (ex) {
node.processError(ex, msg, nodeSend, nodeDone);
})
.then(function (result) {
node.processResult(result, msg, nodeSend, nodeDone);
});
} else if (msg.payload.getfile) {
let fileId = msg.payload.getfile.fileId;
telegramBot
.getFile(fileId)
.catch(function (ex) {
node.processError(ex, msg, nodeSend, nodeDone);
})
.then(function (result) {
node.processResult(result, msg, nodeSend, nodeDone);
});
} else {
if (msg.payload.type) {
let type = msg.payload.type;
node.addCaptionToMessageOptions(msg);
switch (type) {
// --------------------------------------------------------------------
case 'message':
if (this.hasContent(msg)) {
// the maximum message size is 4096 so we must split the message into smaller chunks.
let chunkSize = 4000;
let message = msg.payload.content;
let done = false;
do {
let messageToSend;
if (message.length > chunkSize) {
messageToSend = message.substr(0, chunkSize);
message = message.substr(chunkSize);
} else {
messageToSend = message;
done = true;
}
telegramBot
.sendMessage(chatId, messageToSend, msg.payload.options || {})
.then(function (result) {
node.processResult(result, msg, nodeSend, nodeDone);
})
.catch(function (err) {
// markdown error? try plain mode
// TODO: MarkdownV2 issues error "Error: ETELEGRAM: 400 Bad Request: can't parse entities:"
// adapt the following if so that MarkdownV2 also works.
if (
String(err).includes(
// eslint-disable-next-line quotes
"can't parse entities in message text:"
) &&
msg.payload.options &&
msg.payload.options.parse_mode === 'Markdown'
) {
delete msg.payload.options.parse_mode;
telegramBot
.sendMessage(chatId, messageToSend, msg.payload.options || {})
.catch(function (ex) {
node.processError(ex, msg, nodeSend, nodeDone);
})
.then(function (result) {
node.processResult(result, msg, nodeSend, nodeDone);
});
return;
} else {
node.processError(err, msg, nodeSend, nodeDone);
}
});
} while (!done);
}
break;
case 'photo':
if (this.hasContent(msg)) {
telegramBot
.sendPhoto(chatId, msg.payload.content, msg.payload.options || {}, msg.payload.fileOptions)
.catch(function (ex) {
node.processError(ex, msg, nodeSend, nodeDone);
})
.then(function (result) {
node.processResult(result, msg, nodeSend, nodeDone);
});
}
break;
case 'mediaGroup':
if (this.hasContent(msg)) {
if (Array.isArray(msg.payload.content)) {
for (let i = 0; i < msg.payload.content.length; i++) {
let mediaItem = msg.payload.content[i];
if (typeof mediaItem.type !== 'string') {
node.warn('msg.payload.content[' + i + '].type is not a string it is ' + typeof mediaItem.type);
break;
}
if (mediaItem.media === undefined) {
node.warn('msg.payload.content[' + i + '].media is not defined');
break;
}
}
telegramBot
.sendMediaGroup(chatId, msg.payload.content, msg.payload.options || {})
.catch(function (ex) {
node.processError(ex, msg, nodeSend, nodeDone);
})
.then(function (result) {
node.processResult(result, msg, nodeSend, nodeDone);
});
} else {
node.warn('msg.payload.content for mediaGroup is not an array of mediaItem');
}
}
break;
case 'audio':
if (this.hasContent(msg)) {
telegramBot
.sendAudio(chatId, msg.payload.content, msg.payload.options || {}, msg.payload.fileOptions)
.catch(function (ex) {
node.processError(ex, msg, nodeSend, nodeDone);
})
.then(function (result) {
msg.payload.content = result;
msg.payload.sentMessageId = result.message_id;
nodeSend(msg);
if (nodeDone) {
nodeDone();
}
});
}
break;
case 'document':
if (this.hasContent(msg)) {
telegramBot
.sendDocument(chatId, msg.payload.content, msg.payload.options || {}, msg.payload.fileOptions)
.catch(function (ex) {
node.processError(ex, msg, nodeSend, nodeDone);
})
.then(function (result) {
node.processResult(result, msg, nodeSend, nodeDone);
});
}
break;
case 'poll':
if (this.hasContent(msg)) {
telegramBot
.sendPoll(chatId, msg.payload.content, msg.payload.options || {}, msg.payload.optional)
.catch(function (ex) {
node.processError(ex, msg, nodeSend, nodeDone);
})
.then(function (result) {
node.processResult(result, msg, nodeSend, nodeDone);
});
}
break;
case 'sticker':
if (this.hasContent(msg)) {
telegramBot
.sendSticker(chatId, msg.payload.content, msg.payload.options || {}, msg.payload.fileOptions)
.catch(function (ex) {
node.processError(ex, msg, nodeSend, nodeDone);
})
.then(function (result) {
node.processResult(result, msg, nodeSend, nodeDone);
});
}
break;
case 'dice':
if (this.hasContent(msg)) {
telegramBot
.sendDice(chatId, msg.payload.content, msg.payload.options || {})
.catch(function (ex) {
node.processError(ex, msg, nodeSend, nodeDone);
})
.then(function (result) {
node.processResult(result, msg, nodeSend, nodeDone);
});
}
break;
case 'animation':
if (this.hasContent(msg)) {
telegramBot
.sendAnimation(chatId, msg.payload.content, msg.payload.options || {}, msg.payload.fileOptions)
.catch(function (ex) {
node.processError(ex, msg, nodeSend, nodeDone);
})
.then(function (result) {
node.processResult(result, msg, nodeSend, nodeDone);
});
}
break;
case 'video':
if (this.hasContent(msg)) {
telegramBot
.sendVideo(chatId, msg.payload.content, msg.payload.options || {}, msg.payload.fileOptions)
.catch(function (ex) {
node.processError(ex, msg, nodeSend, nodeDone);
})
.then(function (result) {
node.processResult(result, msg, nodeSend, nodeDone);
});
}
break;
case 'video_note':
if (this.hasContent(msg)) {
telegramBot
.sendVideoNote(chatId, msg.payload.content, msg.payload.options || {}, msg.payload.fileOptions)
.catch(function (ex) {
node.processError(ex, msg, nodeSend, nodeDone);
})
.then(function (result) {
node.processResult(result, msg, nodeSend, nodeDone);
});
}
break;
case 'voice':
if (this.hasContent(msg)) {
telegramBot
.sendVoice(chatId, msg.payload.content, msg.payload.options || {}, msg.payload.fileOptions)
.catch(function (ex) {
node.processError(ex, msg, nodeSend, nodeDone);
})
.then(function (result) {
node.processResult(result, msg, nodeSend, nodeDone);
});
}
break;
case 'location':
if (this.hasContent(msg)) {
telegramBot
.sendLocation(chatId, msg.payload.content.latitude, msg.payload.content.longitude, msg.payload.options || {})
.catch(function (ex) {
node.processError(ex, msg, nodeSend, nodeDone);
})
.then(function (result) {
node.processResult(result, msg, nodeSend, nodeDone);
});
}
break;
case 'venue':
if (this.hasContent(msg)) {
telegramBot
.sendVenue(
chatId,
msg.payload.content.latitude,
msg.payload.content.longitude,
msg.payload.content.title,
msg.payload.content.address,
msg.payload.options || {}
)
.catch(function (ex) {
node.processError(ex, msg, nodeSend, nodeDone);
})
.then(function (result) {
node.processResult(result, msg, nodeSend, nodeDone);
});
}
break;
case 'contact':
if (this.hasContent(msg)) {
if (msg.payload.content.last_name) {
if (!msg.payload.options) {
msg.payload.options = {};
}
msg.payload.options.last_name = msg.payload.content.last_name;
}
telegramBot
.sendContact(chatId, msg.payload.content.phone_number, msg.payload.content.first_name, msg.payload.options || {})
.catch(function (ex) {
node.processError(ex, msg, nodeSend, nodeDone);
})
.then(function (result) {
node.processResult(result, msg, nodeSend, nodeDone);
});
}
break;
// --------------------------------------------------------------------
case 'editMessageLiveLocation':
if (this.hasContent(msg)) {
node.addChatIdToOptions(chatId, msg.payload.options);
telegramBot
.editMessageLiveLocation(msg.payload.content.latitude, msg.payload.content.longitude, msg.payload.options || {})
.catch(function (ex) {
node.processError(ex, msg, nodeSend, nodeDone);
})
.then(function (result) {
node.processResult(result, msg, nodeSend, nodeDone);
});
}
break;
case 'stopMessageLiveLocation':
// This message requires the options to be set!
//if (this.hasContent(msg)) {
node.addChatIdToOptions(chatId, msg.payload.options);
telegramBot
.stopMessageLiveLocation(msg.payload.options)
.catch(function (ex) {
node.processError(ex, msg, nodeSend, nodeDone);
})
.then(function (result) {
node.processResult(result, msg, nodeSend, nodeDone);
});
//}
break;
case 'callback_query':
case 'answerCallbackQuery':
{
let callbackQueryId = msg.payload.callbackQueryId;
let options = msg.payload.options;
if (options === undefined) {
options = {};
}
if (options.text === undefined && msg.payload.content !== undefined) {
options.text = msg.payload.content;
}
telegramBot
.answerCallbackQuery(callbackQueryId, options)
.catch(function (ex) {
node.processError(ex, msg, nodeSend, nodeDone);
})
.then(function (result) {
node.processResult(result, msg, nodeSend, nodeDone);
});
}
break;
case 'inline_query':
case 'answerInlineQuery':
//if (this.hasContent(msg)) {
// this type requires results to be set: see https://core.telegram.org/bots/api#inlinequeryresult
telegramBot
.answerInlineQuery(msg.payload.inlineQueryId, msg.payload.results, msg.payload.options || {})
.catch(function (ex) {
node.processError(ex, msg, nodeSend, nodeDone);
})
.then(function (result) {
node.processResult(result, msg, nodeSend, nodeDone);
});
//}
break;
case 'answerWebAppQuery':
//if (this.hasContent(msg)) {
// this type requires results to be set: see https://core.telegram.org/bots/api#inlinequeryresult
telegramBot
.answerWebAppQuery(msg.payload.webAppQueryId, msg.payload.results, msg.payload.options || {})
.catch(function (ex) {
node.processError(ex, msg, nodeSend, nodeDone);
})
.then(function (result) {
node.processResult(result, msg, nodeSend, nodeDone);
});
//}
break;
case 'sendChatAction':
case 'action':
if (this.hasContent(msg)) {
telegramBot
.sendChatAction(chatId, msg.payload.content, msg.payload.options || {})
.catch(function (ex) {
node.processError(ex, msg, nodeSend, nodeDone);
})
.then(function (result) {
node.processResult(result, msg, nodeSend, nodeDone);
});
}
break;
// --------------------------------------------------------------------
// Some of the following functions require the bot to be administrator of the chat/channel
// 1 argument: chatId
case 'getChatAdministrators':
case 'getChatMemberCount':
case 'getChat':
case 'leaveChat':
case 'exportChatInviteLink':
case 'createChatInviteLink':
case 'unpinAllChatMessages':
case 'deleteChatPhoto':
case 'getForumTopicIconStickers':
case 'getChatMenuButton':
case 'closeGeneralForumTopic':
case 'reopenGeneralForumTopic':
case 'hideGeneralForumTopic':
case 'unhideGeneralForumTopic':
case 'deleteChatStickerSet':
telegramBot[type](chatId, msg.payload.options || {})
.catch(function (ex) {
node.processError(ex, msg, nodeSend, nodeDone);
})
.then(function (result) {
node.processResult(result, msg, nodeSend, nodeDone);
});
break;
// 2 arguments: content, options
case 'editMessageCaption':
case 'editMessageText':
case 'editMessageReplyMarkup':
if (this.hasContent(msg)) {
node.addChatIdToOptions(chatId, msg.payload.options);
telegramBot[type](msg.payload.content, msg.payload.options || {})
.catch(function (ex) {
node.processError(ex, msg, nodeSend, nodeDone);
})
.then(function (result) {
node.processResult(result, msg, nodeSend, nodeDone);
});
}
break;
// TODO: https://github.com/windkh/node-red-contrib-telegrambot/issues/178
// https://github.com/yagop/node-telegram-bot-api/issues/876
case 'editMessageMedia':
if (this.hasContent(msg)) {
node.editMessageMedia(msg.payload.content, msg.payload.options || {})
.catch(function (ex) {
node.processError(ex, msg, nodeSend, nodeDone);
})
.then(function (result) {
node.processResult(result, msg, nodeSend, nodeDone);
});
}
break;
// 2 arguments: chatId, content
case 'setChatTitle':
case 'setChatPhoto':
case 'setChatDescription':
case 'setChatMenuButton':
case 'setChatStickerSet':
case 'unpinChatMessage':
case 'deleteMessage':
if (this.hasContent(msg)) {
telegramBot[type](chatId, msg.payload.content)
.catch(function (ex) {
node.processError(ex, msg, nodeSend, nodeDone);
})
.then(function (result) {
node.processResult(result, msg, nodeSend, nodeDone);
});
}
break;
// 3 arguments: chatId, content, options
case 'pinChatMessage':
case 'unbanChatMember':
case 'banChatMember':
case 'restrictChatMember':
case 'promoteChatMember':
case 'getChatMember':
case 'approveChatJoinRequest':
case 'declineChatJoinRequest':
case 'setChatAdministratorCustomTitle':
case 'stopPoll':
case 'setMessageReaction':
// The userId must be passed in msg.payload.content: note that this is is a number not the username.
// Right now there is no way for resolving the user_id by username in the official API.
if (this.hasContent(msg)) {
telegramBot[type](chatId, msg.payload.content, msg.payload.options || {})
.catch(function (ex) {
node.processError(ex, msg, nodeSend, nodeDone);
})
.then(function (result) {
node.processResult(result, msg, nodeSend, nodeDone);
});
}
break;
// 3 arguments: chatId, content, options
case 'createForumTopic':
case 'editForumTopic':
case 'closeForumTopic':
case 'reopenForumTopic':
case 'deleteForumTopic':
case 'unpinAllForumTopicMessages':
case 'editGeneralForumTopic':
// The message_thread_id must be passed in msg.payload.content: note that this is is a number not the username.
// Right now there is no way for resolving the user_id by username in the official API.
if (this.hasContent(msg)) {
telegramBot[type](chatId, msg.payload.content, msg.payload.options || {})
.catch(function (ex) {
node.processError(ex, msg, nodeSend, nodeDone);
})
.then(function (result) {
node.processResult(result, msg, nodeSend, nodeDone);
});
}
break;
// --------------------------------------------------------------------
// See https://core.telegram.org/bots/payments
// See https://core.telegram.org/bots/api#sendinvoice
case 'sendInvoice':
//if (this.hasContent(msg)) {
telegramBot[type](
chatId,
msg.payload.content.title,
msg.payload.content.description,
msg.payload.content.payload,
msg.payload.content.providerToken,
msg.payload.content.currency,
msg.payload.content.prices,
msg.payload.options || {}
)
.catch(function (ex) {
node.processError(ex, msg, nodeSend, nodeDone);
})
.then(function (result) {
node.processResult(result, msg, nodeSend, nodeDone);
});
//}
break;
case 'shipping_query':
case 'answerShippingQuery':
//if (this.hasContent(msg)) {
// this type requires ok to be set: see https://core.telegram.org/bots/api#answershippingquery
telegramBot
.answerShippingQuery(msg.payload.shippingQueryId, msg.payload.ok, msg.payload.options || {})
.catch(function (ex) {
node.processError(ex, msg, nodeSend, nodeDone);
})
.then(function (result) {
node.processResult(result, msg, nodeSend, nodeDone);
});
//}
break;
case 'pre_checkout_query':
case 'answerPreCheckoutQuery':
//if (this.hasContent(msg)) {
// this type requires ok to be set: see https://core.telegram.org/bots/api#answerprecheckoutquery
telegramBot
.answerPreCheckoutQuery(msg.payload.preCheckoutQueryId, msg.payload.ok, msg.payload.options || {})
.catch(function (ex) {
node.processError(ex, msg, nodeSend, nodeDone);
})
.then(function (result) {
node.processResult(result, msg, nodeSend, nodeDone);
});
//}
break;
// TODO:
// setChatPermissions
// editChatInviteLink, revokeChatInviteLink
// getUserProfilePhotos,
// getMyCommands
// sendGame, setGameScore, getGameHighScores
// uploadStickerFile, createNewStickerSet, addStickerToSet, setStickerPositionInSet, deleteStickerFromSet
default:
// unknown type we try the unthinkable.
if (type in telegramBot) {
if (this.hasContent(msg)) {
telegramBot[type](chatId, msg.payload.content, msg.payload.options || {})
.catch(function (ex) {
node.processError(ex, msg, nodeSend, nodeDone);
})
.then(function (result) {
node.processResult(result, msg, nodeSend, nodeDone);
});
}
} else {
// type is not supported.
node.warn('msg.payload.type is not supported');
}
}
} else {
node.warn('msg.payload.type is empty');
}
} // forward
};
// Derived from original code but with optional fileName
this.downloadFile = function (fileId, downloadDir, fileName) {
let resolve;
let reject;
const promise = new Promise((a, b) => {
resolve = a;
reject = b;
});
let form = {};
let telegramBot = this.config.getTelegramBot();
const fileStream = telegramBot.getFileStream(fileId, form);
fileStream.on('info', (info) => {
if (fileName === undefined) {
fileName = info.uri.slice(info.uri.lastIndexOf('/') + 1);
}
const filePath = path.join(downloadDir, fileName);
pipeline(fileStream, fs.createWriteStream(filePath), (error) => {
if (!error) {
return resolve(filePath);
} else {
return reject(error);
}
});
});
fileStream.on('error', (err) => {
reject(err);
});
return promise;
};
// TODO: https://github.com/windkh/node-red-contrib-telegrambot/issues/178
// TODO: https://github.com/yagop/node-telegram-bot-api/issues/876
this.editMessageMedia = function (media, form = {}) {
const opts = {
qs: form,
};
opts.formData = {};
const payload = Object.assign({}, media);
delete payload.media;
delete payload.fileOptions;
let telegramBot = this.config.getTelegramBot();
try {
const attachName = String(0);
const [formData, fileId] = telegramBot._formatSendData(attachName, media.media, media.fileOptions);
if (formData) {
opts.formData[attachName] = formData[attachName];
payload.media = `attach://${attachName}`;
} else {
payload.media = fileId;
}
} catch (ex) {
return Promise.reject(ex);
}
opts.qs.media = JSON.stringify(payload);
return telegramBot._request('editMessageMedia', opts);
};
this.config = RED.nodes.getNode(this.bot);
if (this.config) {
node.status({ fill: 'red', shape: 'ring', text: 'not connected' });
node.onStatusChanged = function (status, nodeStatus) {
node.status(nodeStatus);
switch (status) {
case 'started':
node.start();
break;
case 'stopped':
node.stop();
break;
default:
break;
}
};
node.config.addListener('status', node.onStatusChanged);
let telegramBot = this.config.getTelegramBot();
if (telegramBot) {
node.status({
fill: 'green',
shape: 'ring',
text: 'connected',
});
} else {
node.warn('bot not initialized.');
node.status({
fill: 'red',
shape: 'ring',
text: 'bot not initialized',
});
}
} else {
node.warn('config node failed to initialize.');
node.status({
fill: 'red',
shape: 'ring',
text: 'config node failed to initialize',
});
}
this.on('input', function (msg, nodeSend, nodeDone) {
nodeSend =
nodeSend ||
function () {
node.send.apply(node, arguments);
};
node.status({ fill: 'green', shape: 'ring', text: 'connected' });
if (msg.payload) {
let telegramBot = this.config.getTelegramBot();
if (telegramBot) {
if (!Array.isArray(msg.payload.chatId)) {
this.processMessage(msg.payload.chatId, msg, nodeSend, nodeDone);
} else {
let chatIds = msg.payload.chatId;
let length = chatIds.length;
for (let i = 0; i < length; i++) {
let chatId = chatIds[i];
let clonedMsg = RED.util.cloneMessage(msg);
clonedMsg.payload.chatId = chatId;
this.processMessage(chatId, clonedMsg, nodeSend, nodeDone);
}
}
} else {
node.warn('bot not initialized.');
node.status({
fill: 'red',
shape: 'ring',
text: 'bot not initialized',
});
}
} else {
node.warn('msg.payload is empty');
}
});
this.on('close', function (removed, done) {
node.stop();
if (node.onStatusChanged) {
node.config.removeListener('status', node.onStatusChanged);
}
node.status({});
done();
});
}
return TelegramOutNode;
};