UNPKG

iobroker.telegram

Version:

The adapter allows to send and receive telegram messages from ioBroker and to be a broker.

1,208 lines (1,117 loc) 98.4 kB
'use strict'; // https://github.com/yagop/node-telegram-bot-api/issues/319 (because of bluebird) process.env.NTBA_FIX_319 = 1; const TelegramBot = require('node-telegram-bot-api'); const utils = require('@iobroker/adapter-core'); // Get common adapter utils const adapterName = require('./package.json').name.split('.').pop(); const _ = require('./lib/words.js'); const fs = require('node:fs'); const path = require('node:path'); const { WebServer } = require('@iobroker/webserver'); const https = require('node:https'); const axios = require('axios').default; let bot; let users = {}; let systemLang = 'en'; let reconnectTimer = null; let pollConnectionStatus = null; let isConnected = null; let lastMessageTime = 0; let lastMessageText = ''; const enums = {}; const protection = {}; let gcInterval = null; const commands = {}; const callbackQueryId = {}; const mediaGroupExport = {}; let tmpDirName; const server = { app: null, server: null, settings: null, }; let adapter; const systemLang2CallMe = { en: 'en-GB-Standard-A', de: 'de-DE-Standard-A', ru: 'ru-RU-Standard-A', pt: 'pt-BR-Standard-A', nl: 'nl-NL-Standard-A', fr: 'fr-FR-Standard-A', it: 'it-IT-Standard-A', es: 'es-ES-Standard-A', pl: 'pl-PL-Standard-A', uk: 'uk-UA-Standard-A', 'zh-cn': 'en-GB-Standard-A', }; function startAdapter(options) { options = options || {}; Object.assign(options, { name: adapterName, error: err => { // Identify unhandled errors originating from callbacks in scripts // These are not caught by wrapping the execution code in try-catch if (err) { const errStr = err.toString(); if ( errStr.includes('getaddrinfo') || errStr.includes('api.telegram.org') || errStr.includes('EAI_AGAIN') ) { return true; } } return false; }, }); adapter = new utils.Adapter(options); adapter.on('message', obj => { if (obj) { if (obj.command === 'adminuser') { let adminUserData; adapter.getState('communicate.users', (err, state) => { err && adapter.log.error(err); if (state && state.val) { try { adminUserData = JSON.parse(state.val); adapter.sendTo(obj.from, obj.command, adminUserData, obj.callback); } catch (err) { err && adapter.log.error(err); adapter.log.error('Cannot parse stored user IDs!'); } } }); } else if (obj.command === 'delUser') { const userID = obj.message; let userObj = {}; adapter.getState('communicate.users', (err, state) => { err && adapter.log.error(err); if (state && state.val) { try { userObj = JSON.parse(state.val); delete userObj[userID]; adapter.setState('communicate.users', JSON.stringify(userObj), true, err => { if (!err) { adapter.sendTo(obj.from, obj.command, userID, obj.callback); updateUsers(); adapter.log.warn(`User ${userID} has been deleted!`); } }); } catch (err) { err && adapter.log.error(err); adapter.log.error(`Cannot delete user ${userID}!`); } } }); } else if (obj.command === 'systemMessages') { const userID = obj.message.itemId; const checked = obj.message.checked; let userObj = {}; adapter.getState('communicate.users', (err, state) => { err && adapter.log.error(err); if (state && state.val) { try { userObj = JSON.parse(state.val); userObj[userID].sysMessages = checked; adapter.setState('communicate.users', JSON.stringify(userObj), true, err => { if (!err) { adapter.sendTo(obj.from, obj.command, userID, obj.callback); updateUsers(); adapter.log.info( `Receiving of system messages for user "${userID}" has been changed to ${checked}!`, ); } }); } catch (err) { err && adapter.log.error(err); adapter.log.error(`Cannot change user ${userID}!`); } } }); } else if (obj.command === 'delAllUser') { try { adapter.setState('communicate.users', '{}', true, err => { if (!err) { adapter.sendTo(obj.from, obj.command, true, obj.callback); updateUsers(); adapter.log.warn( 'List of saved users has been wiped. Every User has to reauthenticate with the new password!', ); } }); } catch (err) { err && adapter.log.error(err); adapter.log.error('Cannot wipe list of saved users!'); } } else if (obj.command === 'sendNotification') { processNotification(obj); } else { processMessage(obj); } } }); adapter.on('ready', () => { adapter.config.server = adapter.config.server === 'true'; adapter._questions = []; adapter.garbageCollectorinterval = setInterval(() => { const now = Date.now(); Object.keys(callbackQueryId).forEach(id => { if (now - callbackQueryId[id].ts > 120000) { delete callbackQueryId[id]; } }); }, 10000); tmpDirName = path.join(utils.getAbsoluteDefaultDataDir(), adapter.namespace.replace('.', '_')); // Create file system directories for media files if (adapter.config.saveFilesTo == 'filesystem') { try { !fs.existsSync(tmpDirName) && fs.mkdirSync(tmpDirName); const subDirectories = ['voice']; if (adapter.config.saveFiles) { // Create subdirs for other attachment types subDirectories.push('photo', 'video', 'audio', 'document'); } for (const subDir of subDirectories) { const subDirPath = path.join(tmpDirName, subDir); !fs.existsSync(subDirPath) && fs.mkdirSync(subDirPath); } } catch (err) { adapter.log.error(`Cannot create tmp directory: ${tmpDirName}: ${err}`); } } if (adapter.config.server) { adapter.config.port = parseInt(adapter.config.port, 10); // Load certificates adapter.getCertificates(async (err, certificates, leConfig) => { adapter.config.certificates = certificates; adapter.config.leConfig = leConfig; adapter.config.secure = true; try { const webserver = new WebServer({ app: handleWebHook, adapter, secure: adapter.config.secure, }); server.server = await webserver.init(); } catch (err) { adapter.log.error(`Cannot create webserver: ${err}`); adapter.terminate ? adapter.terminate(utils.EXIT_CODES.ADAPTER_REQUESTED_TERMINATION) : process.exit(utils.EXIT_CODES.ADAPTER_REQUESTED_TERMINATION); return; } if (server.server) { server.server.__server = server; let serverListening = false; let serverPort = adapter.config.port; server.server.on('error', e => { if (e.toString().includes('EACCES') && serverPort <= 1024) { adapter.log.error( `node.js process has no rights to start server on the port ${serverPort}.\n` + `Do you know that on linux you need special permissions for ports under 1024?\n` + `You can call in shell following scrip to allow it for node.js: "iobroker fix"`, ); } else { adapter.log.error( `Cannot start server on ${adapter.config.bind || '0.0.0.0'}:${serverPort}: ${e}`, ); } if (!serverListening) { adapter.terminate ? adapter.terminate(utils.EXIT_CODES.ADAPTER_REQUESTED_TERMINATION) : process.exit(utils.EXIT_CODES.ADAPTER_REQUESTED_TERMINATION); } }); adapter.getPort( adapter.config.port, !adapter.config.bind || adapter.config.bind === '0.0.0.0' ? undefined : adapter.config.bind || undefined, port => { if (parseInt(port, 10) !== adapter.config.port && !adapter.config.findNextPort) { adapter.log.error(`port ${adapter.config.port} already in use`); adapter.terminate ? adapter.terminate() : process.exit(1); } serverPort = port; server.server.listen( port, !adapter.config.bind || adapter.config.bind === '0.0.0.0' ? undefined : adapter.config.bind || undefined, () => (serverListening = true), ); adapter.log.info(`https server listening on port ${port}`); main().catch(e => adapter.log.error(`Cannot start adapter: ${e}`)); }, ); } }); } else { main().catch(e => adapter.log.error(`Cannot start adapter: ${e}`)); } }); adapter.on('unload', () => { reconnectTimer && clearInterval(reconnectTimer); reconnectTimer = null; gcInterval && clearInterval(gcInterval); gcInterval = null; pollConnectionStatus && clearInterval(pollConnectionStatus); pollConnectionStatus = null; adapter.garbageCollectorinterval && clearInterval(adapter.garbageCollectorinterval); adapter.garbageCollectorinterval = null; if (adapter && adapter.config) { if (adapter.config.restarting !== '') { // default text if ( adapter.config.restarting === '_' || adapter.config.restarting === null || adapter.config.restarting === undefined ) { sendSystemMessage( adapter.config.rememberUsers ? _('Restarting...') : _('Restarting... Reauthenticate!'), ); } else { sendSystemMessage(adapter.config.restarting); } } try { if (server.server) { server.server.close(); } } catch (e) { console.error(`Cannot close server: ${e}`); } } isConnected && adapter && adapter.setState && adapter.setState('info.connection', false, true); isConnected = false; }); // This handler is called if a subscribed state changes adapter.on('stateChange', async (id, state) => { if (state) { if (!state.ack) { if (id.endsWith('communicate.response')) { if (typeof state.val === 'object') { adapter.log.error( `communicate.response only supports passing a message to send as string. You provided ${JSON.stringify(state.val)}. Please use "communicate.responseJson" instead with a stringified JSON object!`, ); return; } // Send to someone this message await sendMessage(state.val); await adapter.setStateAsync('communicate.response', { val: state.val, ack: true }); } else if (id.endsWith('communicate.responseSilent')) { if (typeof state.val === 'object') { adapter.log.error( `communicate.responseSilent only supports passing a message to send as string. You provided ${JSON.stringify(state.val)}. Please use "communicate.responseSilentJson" instead with a stringified JSON object!`, ); return; } // Send to someone this message await sendMessage(state.val, null, null, { disable_notification: true }); await adapter.setStateAsync('communicate.responseSilent', { val: state.val, ack: true }); } else if (id.endsWith('communicate.responseJson')) { try { const val = JSON.parse(state.val); // Send to someone this message await sendMessage(val); await adapter.setStateAsync('communicate.responseJson', { val: state.val, ack: true }); } catch (err) { adapter.log.error(`could not parse Json in communicate.responseJon state: ${err.message}`); } } else if (id.endsWith('communicate.responseSilentJson')) { try { const val = JSON.parse(state.val); // Send to someone this message await sendMessage(val, null, null, { disable_notification: true }); await adapter.setStateAsync('communicate.responseSilent', { val: state.val, ack: true }); } catch (err) { adapter.log.error( `could not parse Json in communicate.responseSilentJon state: ${err.message}`, ); } } else if (id.endsWith('communicate.requestResponse')) { try { const text = state.val; const chatIdState = await adapter.getStateAsync('communicate.requestChatId'); const threadIdState = await adapter.getStateAsync('communicate.requestMessageThreadId'); const options = {}; if (threadIdState && threadIdState.val > 0) { options.message_thread_id = threadIdState.val; } // Send to someone this message await sendMessage(text, null, chatIdState ? chatIdState.val : null, options); await adapter.setStateAsync('communicate.requestResponse', { val: state.val, ack: true }); } catch (err) { adapter.log.error( `could not parse Json in communicate.responseSilentJon state: ${err.message}`, ); } } } else if (commands[id] && commands[id].report) { adapter.log.debug(`reporting state change of ${id}: ${JSON.stringify(commands[id])}`); const options = commands[id].reportSilent == true ? { disable_notification: true } : {}; let users = null; if (commands[id]?.recipients && commands[id].recipients !== '') { users = commands[id].recipients; } if (commands[id].reportChanges) { if (state.val !== commands[id].lastState) { commands[id].lastState = state.val; sendMessage(getStatus(id, state), users, null, options); } } else { sendMessage(getStatus(id, state), users, null, options); } } } }); adapter.on('objectChange', (id, obj) => { if ( obj && obj.common && obj.common.custom && obj.common.custom[adapter.namespace] && obj.common.custom[adapter.namespace].enabled ) { const alias = getName(obj); if (!commands[id]) { adapter.log.info(`enabled logging of ${id}, Alias=${alias}`); setImmediate(() => adapter.subscribeForeignStates(id)); } commands[id] = obj.common.custom[adapter.namespace]; commands[id].type = obj.common.type; commands[id].states = parseStates(obj.common.states); commands[id].unit = obj.common && obj.common.unit; commands[id].min = obj.common && obj.common.min; commands[id].max = obj.common && obj.common.max; commands[id].recipients = obj.common && obj.common.recipients; commands[id].alias = alias; // read actual state to detect changes if (commands[id].reportChanges) { adapter .getForeignStateAsync(id) .then(state => (commands[id].lastState = state ? state.val : undefined)); } } else if (commands[id]) { adapter.log.debug(`Removed command: ${id}`); delete commands[id]; setImmediate(() => adapter.unsubscribeForeignStates(id)); } else if (id.startsWith('enum.rooms') && adapter.config.rooms) { if (obj && obj.common && obj.common.members && obj.common.members.length) { enums.rooms[id] = obj.common; } else if (enums.rooms[id]) { delete enums.rooms[id]; } } }); server.settings = adapter.config; return adapter; } /** * Send a message to all system users * * @param text text to send * @param options additional options, e.g. parse_mode * @returns */ async function sendSystemMessage(text, options = {}) { const _users = Object.keys(users) .filter(id => users[id].sysMessages !== false) .map(id => (adapter.config.useUsername ? users[id].userName : users[id].firstName)); await sendMessage(text, _users, null, { ...options, disable_notification: true }); } function getStatus(id, state) { if (!state) { state = { val: 'State not set' }; } if (commands[id].type === 'boolean') { return `${commands[id].alias} => ${state.val ? commands[id].onStatus || _('ON-Status') : commands[id].offStatus || _('OFF-Status')}`; } if (commands[id].states && commands[id].states[state.val] !== undefined) { state.val = commands[id].states[state.val]; } return `${commands[id].alias} => ${state.val}${commands[id].unit ? ` ${commands[id].unit}` : ''}`; } function connectionState(connected, logSuccess) { let errorCounter = 0; function checkConnection() { pollConnectionStatus = null; bot && bot.getMe && bot .getMe() .then(data => { adapter.log.debug(`getMe (reconnect): ${JSON.stringify(data)}`); connectionState(true, errorCounter > 0); }) .catch(error => { errorCounter % 10 === 0 && adapter.log.error(`getMe (reconnect #${errorCounter}) Error:${error}`); errorCounter++; pollConnectionStatus && clearTimeout(pollConnectionStatus); pollConnectionStatus = setTimeout(checkConnection, 1000); }); } if (connected && logSuccess) { adapter.log.info('getMe (reconnect): Success'); } if (isConnected !== connected) { isConnected = connected; adapter.setState('info.connection', isConnected, true); if (isConnected && pollConnectionStatus) { clearTimeout(pollConnectionStatus); pollConnectionStatus = null; } else if (!isConnected) { checkConnection(); } } } function parseStates(states) { // todo return states; } function getName(obj) { if (obj.common.custom[adapter.namespace].alias) { return obj.common.custom[adapter.namespace].alias; } let name = obj.common.name; if (typeof name === 'object') { name = name[systemLang] || name.en; } return name || obj._id; } const actions = [ 'typing', 'upload_photo', 'upload_video', 'record_video', 'record_audio', 'upload_document', 'find_location', ]; function handleWebHook(req, res) { if (req.method === 'POST' && req.url === `/${adapter.config.token}`) { // //{ // "update_id":10000, // "message":{ // "date":1441645532, // "chat":{ // "last_name":"Test Lastname", // "id":1111111, // "first_name":"Test", // "username":"Test" // }, // "message_id":1365, // "from": { // "last_name":"Test Lastname", // "id":1111111, // "first_name":"Test", // "username":"Test" // }, // "text":"/start" // } //} let body = ''; req.on('data', data => { body += data; if (body.length > 100_000) { res.writeHead(413, 'Request Entity Too Large', { 'Content-Type': 'text/html', }); res.end( '<!doctype html><html><head><title>413</title></head><body>413: Request Entity Too Large</body></html>', ); } }); req.on('end', () => { let msg; try { msg = JSON.parse(body); } catch (e) { adapter.log.error(`Cannot parse webhook response!: ${e}`); return; } res.end('OK'); bot.processUpdate(msg); }); } else { res.writeHead(404, 'Resource Not Found', { 'Content-Type': 'text/html', }); res.end('<!doctype html><html><head><title>404</title></head><body>404: Resource Not Found</body></html>'); } } function saveSendRequest(msg) { adapter.log.debug(`Request [saveSendRequest]: ${JSON.stringify(msg)}`); if (typeof msg === 'object') { if (adapter.config.storeRawRequest) { adapter.setState( 'communicate.botSendRaw', JSON.stringify(msg, null, 2), true, err => err && adapter.log.error(err), ); } if (msg?.message_id) { adapter.setState( 'communicate.botSendMessageId', msg.message_id, true, err => err && adapter.log.error(err), ); } if (msg?.message_thread_id) { adapter.setState( 'communicate.botSendMessageThreadId', msg.message_thread_id, true, err => err && adapter.log.error(err), ); } if (msg?.chat && msg.chat.id) { adapter.setState( 'communicate.botSendChatId', msg.chat.id.toString(), true, err => err && adapter.log.error(err), ); } } } function _sendMessageHelper(dest, name, text, options) { return new Promise(resolve => { const messageIds = {}; if (options && options.chatId !== undefined && options.user === undefined) { options.user = adapter.config.useUsername ? users[options.chatId].userName : users[options.chatId].firstName; } // to push chatId value for the group chats - useful to process the errors, and list of processed messages. if (options.chatId === undefined && options.user === undefined && name === 'chat' && dest) { options.chatId = dest; } if (options && options.editMessageReplyMarkup !== undefined) { adapter.log.debug(`Send editMessageReplyMarkup to "${name}"`); bot && executeSending( () => bot.editMessageReplyMarkup( options.editMessageReplyMarkup.reply_markup, options.editMessageReplyMarkup.options, ), options, resolve, ); } else if (options && options.editMessageText !== undefined) { adapter.log.debug(`Send editMessageText to "${name}"`); bot && executeSending(() => bot.editMessageText(text, options.editMessageText.options), options, resolve); } else if (options && options.editMessageMedia !== undefined) { adapter.log.debug(`Send editMessageMedia to "${name}"`); if (text) { let mediaInput; if ( (typeof text === 'string' && text.match(/\.(jpg|png|jpeg|bmp|gif)$/i) && (fs.existsSync(text) || text.match(/^(https|http)/i))) || (options && options.type === 'photo') ) { mediaInput = { type: 'photo', media: text, }; } else if ( (typeof text === 'string' && text.match(/\.(gif)/i) && fs.existsSync(text)) || (options && options.type === 'animation') ) { mediaInput = { type: 'animation', media: text, }; } else if ( (typeof text === 'string' && text.match(/\.(mp4)$/i) && fs.existsSync(text)) || (options && options.type === 'video') ) { mediaInput = { type: 'video', media: text, }; } else if ( (typeof text === 'string' && text.match(/\.(wav|mp3|ogg)$/i) && fs.existsSync(text)) || (options && options.type === 'audio') ) { mediaInput = { type: 'audio', media: text, }; } else if ( (typeof text === 'string' && text.match(/\.(txt|doc|docx|csv|pdf|xls|xlsx)$/i) && fs.existsSync(text)) || (options && options.type === 'document') ) { mediaInput = { type: 'document', media: text, }; } if (mediaInput) { const opts = { qs: options.editMessageMedia.options, }; opts.formData = {}; const payload = Object.assign({}, mediaInput); delete payload.media; delete payload.fileOptions; try { const attachName = String(0); const [formData, fileId] = bot._formatSendData( attachName, mediaInput.media, mediaInput.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); bot && executeSending(() => bot._request('editMessageMedia', opts), options, resolve); } else { adapter.log.error( `Cannot send editMessageMedia [chatId - ${options.chatId}]: unsupported media type`, ); options = null; resolve(JSON.stringify(messageIds)); } } else { adapter.log.error( `Cannot send editMessageMedia [chatId - ${options.chatId}]: no media found. "text" may not be empty`, ); options = null; resolve(JSON.stringify(messageIds)); } } else if (options && options.editMessageCaption !== undefined) { adapter.log.debug(`Send editMessageCaption to "${name}"`); bot && executeSending( () => bot.editMessageCaption(text, options.editMessageCaption.options), options, resolve, ); } else if (options && options.deleteMessage !== undefined) { adapter.log.debug(`Send deleteMessage to "${name}"`); bot && executeSending( () => bot.deleteMessage( options.deleteMessage.options.chat_id, options.deleteMessage.options.message_id, ), options, resolve, ); } else if ( options && options.latitude !== undefined && options.longitude !== undefined && options.title !== undefined && options.address !== undefined ) { adapter.log.debug(`Send venue to "${name}": ${options.latitude},${options.longitude}`); bot && executeSending( () => bot.sendVenue( dest, parseFloat(options.latitude), parseFloat(options.longitude), options.title, options.address, options, ), options, resolve, ); } else if (options && options.latitude !== undefined && options.longitude !== undefined) { adapter.log.debug(`Send location to "${name}": ${options.latitude},${options.longitude}`); bot && executeSending( () => bot.sendLocation(dest, parseFloat(options.latitude), parseFloat(options.longitude), options), options, resolve, ); } else if (options && options.type === 'mediagroup') { adapter.log.debug(`Send media group to "${name}": `); if (bot) { const { media: fileNames } = options; if (fileNames instanceof Array) { bot.sendChatAction(dest, 'upload_photo') .then(() => { if (fileNames.every(name => fs.existsSync(name))) { const filesAsArray = fileNames .map(element => { try { return { type: 'photo', media: fs.readFileSync(element) }; } catch (err) { adapter.log.error(`Cannot read file ${element}: ${err}`); return undefined; } }) .filter(element => element !== undefined); const size = filesAsArray .map(element => element.media.length) .reduce((acc, val) => acc + val); adapter.log.info(`Send media group to "${name}": ${size} bytes`); if (filesAsArray.length > 0) { executeSending(() => bot.sendMediaGroup(dest, filesAsArray), options, resolve); } } else { adapter.log.debug('files must exists'); options = null; resolve(JSON.stringify(messageIds)); } }) .catch(error => { adapter.log.error(`upload Error: ${error}`); }); } else { adapter.log.debug('option media should be an array'); resolve(JSON.stringify(messageIds)); } } else { adapter.log.debug('no files added!'); options = null; resolve(JSON.stringify(messageIds)); } } else if (text && typeof text === 'string' && actions.includes(text)) { adapter.log.debug(`Send action to "${name}": ${text}`); bot && executeSending(() => bot.sendChatAction(dest, text), options, resolve); } else if ( text && ((typeof text === 'string' && text.match(/\.webp$/i) && fs.existsSync(text)) || (options && options.type === 'sticker')) ) { if (typeof text === 'string') { adapter.log.debug(`Send sticker to "${name}": ${text}`); } else { adapter.log.debug(`Send sticker to "${name}": ${text.length} bytes`); } bot && executeSending(() => bot.sendSticker(dest, text, options), options, resolve); } else if ( text && ((typeof text === 'string' && text.match(/\.(gif)/i) && fs.existsSync(text)) || (options && options.type === 'animation')) ) { if (typeof text === 'string') { adapter.log.debug(`Send animation to "${name}": ${text}`); } else { adapter.log.debug(`Send animation to "${name}": ${text.length} bytes`); } bot && executeSending(() => bot.sendAnimation(dest, text, options), options, resolve); } else if ( text && ((typeof text === 'string' && text.match(/\.(mp4)$/i) && fs.existsSync(text)) || (options && options.type === 'video')) ) { if (typeof text === 'string') { adapter.log.debug(`Send video to "${name}": ${text}`); } else { adapter.log.debug(`Send video to "${name}": ${text.length} bytes`); } bot && executeSending(() => bot.sendVideo(dest, text, options), options, resolve); } else if ( text && ((typeof text === 'string' && text.match(/\.(txt|doc|docx|csv|pdf|xls|xlsx)$/i) && fs.existsSync(text)) || (options && options.type === 'document')) ) { adapter.log.debug(`Send document to "${name}": ${typeof text === 'string' ? text : text.length}`); bot && executeSending(() => bot.sendDocument(dest, text, options), options, resolve); } else if ( text && ((typeof text === 'string' && text.match(/\.(wav|mp3|ogg)$/i) && fs.existsSync(text)) || (options && options?.type === 'audio')) ) { adapter.log.debug(`Send audio to "${name}": ${typeof text === 'string' ? text : text.length}`); bot && executeSending(() => bot.sendAudio(dest, text, options), options, resolve); } else if ( text && ((typeof text === 'string' && // if the message is a string, and it is a path to file or URL text.match(/\.(jpg|png|jpeg|bmp|gif)$/i) && (fs.existsSync(text) || text.match(/^(https|http)/i))) || (options && options.type === 'photo')) // if the type of message is photo ) { adapter.log.debug(`Send photo to "${name}": ${typeof text === 'string' ? text : text.length}`); bot && executeSending(() => bot.sendPhoto(dest, text, options), options, resolve); } else if (options && options.answerCallbackQuery !== undefined) { adapter.log.debug(`Send answerCallbackQuery to "${name}"`); if (options.answerCallbackQuery.showAlert === undefined) { options.answerCallbackQuery.showAlert = false; } if (bot && callbackQueryId[options.chatId]) { const originalChatId = callbackQueryId[options.chatId].id; delete callbackQueryId[options.chatId]; executeSending( () => bot.answerCallbackQuery( originalChatId, options.answerCallbackQuery.text, options.answerCallbackQuery.showAlert, ), options, resolve, ); } } else { adapter.log.debug(`Send message to [${name}]: "${text}"`); if (text && typeof text === 'string') { options = options || {}; if (text.startsWith('<MarkdownV2>') && text.endsWith('</MarkdownV2>')) { options.parse_mode = 'MarkdownV2'; text = text.substring(12, text.length - 13); } else if (text.startsWith('<HTML>') && text.endsWith('</HTML>')) { options.parse_mode = 'HTML'; text = text.substring(6, text.length - 7); } else if (text.startsWith('<Markdown>') && text.endsWith('</Markdown>')) { options.parse_mode = 'Markdown'; text = text.substring(10, text.length - 11); } } bot && executeSending(() => bot.sendMessage(dest, text || '', options), options, resolve); } }); } /** * executes the given method and handles, what to do next * * @param action * @param options * @param resolve */ function executeSending(action, options, resolve) { // create an empty object, to store chat id and message id of successfully sent messages const messageIds = {}; action() .then(response => { // put chat id and message id to the object, that will be returned // delete message command return only true in response, // to return deleted message id and chat id the next if construction is used: if (response?.message_id) { // The chatId is mostly used in code, instead of chat_id. messageIds[options.chat_id ? options.chat_id : options.chatId] = response.message_id; } else if ( typeof response === 'boolean' && options?.deleteMessage?.options?.chat_id && options?.deleteMessage?.options?.message_id ) { messageIds[options.deleteMessage.options.chat_id] = options.deleteMessage.options.message_id; } // puts ids to the ioBroker database saveSendRequest(response); }) .then(() => { adapter.log.debug('Message sent'); options = null; // return all the collected message ids to the callback resolve(JSON.stringify(messageIds)); }) .catch(error => { // add the error to the message ids object messageIds.error = { [options.chat_id ? options.chat_id : options.chatId]: error }; // log error to the system adapter.log.error( `Failed sending [${options.chatId ? 'chatId' : 'user'} - ${options.chatId ? options.chatId : options.user}]: ${error}`, ); options = null; // send the successfully sent messages as callback resolve(JSON.stringify(messageIds)); }); } // https://core.telegram.org/bots/api function sendMessage(text, user, chatId, options) { if (!text && typeof options !== 'object' && text !== 0 && (!options || !options.latitude)) { adapter.log.warn('Invalid text: null'); return Promise.resolve({}); } if ( text && typeof text === 'object' && text.text !== undefined && typeof text.text === 'string' && options === undefined ) { options = text; text = options.text; if (options.chatId) { chatId = options.chatId; } if (options.user) { user = options.user; } } if (options && typeof options === 'object') { if (options.chatId !== undefined) { delete options.chatId; } if (options.text !== undefined) { delete options.text; } if (options.user !== undefined) { delete options.user; } } options = options || {}; if (text && typeof text === 'string') { if (text && text.startsWith('<MarkdownV2>') && text.endsWith('</MarkdownV2>')) { options.parse_mode = 'MarkdownV2'; text = text.substring(12, text.length - 13); } else if (text && text.startsWith('<HTML>') && text.endsWith('</HTML>')) { options.parse_mode = 'HTML'; text = text.substring(6, text.length - 7); } else if (text && text.startsWith('<Markdown>') && text.endsWith('</Markdown>')) { options.parse_mode = 'Markdown'; text = text.substring(10, text.length - 11); } } const tPromiseList = []; // convert if (text !== undefined && text !== null && typeof text !== 'object') { text = text.toString(); } if (chatId) { tPromiseList.push(_sendMessageHelper(chatId, 'chat', text, options)); return Promise.all(tPromiseList).catch(e => e); } else if (user) { if (typeof user !== 'string' && !(user instanceof Array)) { adapter.log.warn(`Invalid type of user parameter: ${typeof user}. Expected is string or array.`); } const userArray = Array.isArray(user) ? user : (user || '') .toString() .split(/[,;\s]/) .map(u => u.trim()) .filter(u => !!u); let matches = 0; userArray.forEach(userName => { for (const id in users) { if (!Object.prototype.hasOwnProperty.call(users, id)) { continue; } if ( (adapter.config.useUsername && users[id].userName === userName) || (!adapter.config.useUsername && users[id].firstName === userName) ) { if (options) { options.chatId = id; } matches++; tPromiseList.push(_sendMessageHelper(id, userName, text, options)); break; } } }); if (userArray.length !== matches) { adapter.log.warn(`${userArray.length - matches} of ${userArray.length} recipients are unknown!`); } return Promise.all(tPromiseList).catch(e => e); } const m = typeof text === 'string' ? text.match(/^@(.+?)\b/) : null; if (m) { text = (text || '').toString(); text = text.replace(`@${m[1]}`, '').trim().replace(/\s\s/g, ' '); const re = new RegExp(m[1], 'i'); let id = ''; for (const id_t in users) { if (!Object.prototype.hasOwnProperty.call(users, id_t)) { continue; } if ( (adapter.config.useUsername && users[id_t].userName.match(re)) || (!adapter.config.useUsername && users[id_t].firstName.match(re)) ) { id = id_t; break; } } if (id) { if (options) { options.chatId = id; } tPromiseList.push(_sendMessageHelper(id, m[1], text, options)); } } else { // Send to all users Object.keys(users).forEach(id => { if (options) { options.chatId = id; } tPromiseList.push( _sendMessageHelper( id, adapter.config.useUsername ? users[id].userName : users[id].firstName, text, options, ), ); }); } return Promise.all(tPromiseList).catch(e => e); } function saveFile(fileID, fileName, callback) { adapter.log.debug(`Saving media file ${fileID} to ${fileName} (location = ${adapter.config.saveFilesTo})`); bot.getFileLink(fileID) .then(url => { adapter.log.debug(`Received message: ${url}`); https.get(url, res => { if (res.statusCode === 200) { const buf = []; res.on('data', data => buf.push(data)); res.on('end', () => { if (adapter.config.saveFilesTo == 'filesystem') { const fileLocation = path.join(tmpDirName, fileName); try { fs.writeFileSync(fileLocation, Buffer.concat(buf)); callback({ info: `media file has been saved to "${adapter.config.saveFilesTo}": ${fileLocation}`, location: adapter.config.saveFilesTo, path: fileLocation, }); } catch (err) { return callback({ error: `Error: ${err}` }); } } else if (adapter.config.saveFilesTo == 'iobroker') { try { const fileLocation = path.join(tmpDirName, fileName); // TODO: check new urn format https://github.com/ioBroker/ioBroker.js-controller/issues/2710 adapter.writeFileAsync(adapter.namespace, fileName, Buffer.concat(buf)).then(() => { callback({ info: `media file has been saved to "${adapter.config.saveFilesTo}": ${fileLocation}`, location: adapter.config.saveFilesTo, path: fileLocation, }); }); } catch (err) { return callback({ error: `Error: ${err}` }); } } }); res.on('error', err => callback({ error: `Error: ${err}` })); } else { callback({ error: 'Error: statusCode !== 200' }); } }); }) .catch(err => callback({ error: `Error bot.getFileLink: ${err}` })); } function getMessage(msg) { const date = new Date().toISOString().replace(/T/, '_').replace(/\..+/, '').replace(/:/g, '-'); adapter.log.debug(`Received message: ${JSON.stringify(msg)}`); if (msg.voice) { try { saveFile(msg.voice.file_id, adapter.config.saveFiles ? `/voice/${date}.ogg` : '/voice/temp.ogg', res => { if (!res.error) { adapter.log.info(res.info); adap