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