EverydayTech Platform - Developer Reference
Complete Source Code Documentation - All Applications
Loading...
Searching...
No Matches
meshmessaging.js
Go to the documentation of this file.
1/**
2* @description MeshCentral user messaging communication module
3* @author Ylian Saint-Hilaire
4* @copyright Intel Corporation 2022
5* @license Apache-2.0
6* @version v0.0.1
7*/
8
9/*xjslint node: true */
10/*xjslint plusplus: true */
11/*xjslint maxlen: 256 */
12/*jshint node: true */
13/*jshint strict: false */
14/*jshint esversion: 6 */
15"use strict";
16
17/*
18// For Telegram user login, add this in config.json
19"messaging": {
20 "telegram": {
21 "apiid": 00000000,
22 "apihash": "00000000000000000000000",
23 "session": "aaaaaaaaaaaaaaaaaaaaaaa"
24 }
25}
26
27// For Telegram bot login, add this in config.json
28"messaging": {
29 "telegram": {
30 "apiid": 00000000,
31 "apihash": "00000000000000000000000",
32 "bottoken": "00000000:aaaaaaaaaaaaaaaaaaaaaaaa"
33 }
34}
35
36// For Telegram login with proxy settings, add this in config.json
37{
38 "messaging": {
39 "telegram": {
40 "apiid": 0,
41 "apihash": "00000000000000000000000",
42 "session": "aaaaaaaaaaaaaaaaaaaaaaa",
43 "useWSS": false, // Important. Most proxies cannot use SSL.
44 "proxy": {
45 "ip": "123.123.123.123", // Proxy host (IP or hostname)
46 "port": 123, // Proxy port
47 "MTProxy": false, // Whether it's an MTProxy or a normal Socks one
48 "secret": "00000000000000000000000000000000", // If used MTProxy then you need to provide a secret (or zeros).
49 "socksType": 5, // If used Socks you can choose 4 or 5.
50 "timeout": 2 // Timeout (in seconds) for connection,
51 }
52 }
53 }
54}
55
56// For Discord login, add this in config.json
57"messaging": {
58 "discord": {
59 "inviteurl": "https://discord.gg/xxxxxxxxx",
60 "token": "xxxxxxxxxxxxxxxxxx.xxxxxxxxxxxxxxxxxx"
61 }
62}
63
64// For XMPP login, add this in config.json
65{
66 "messaging": {
67 "xmpp": {
68 "service": "xmppserver.com",
69 "credentials": {
70 "username": "username",
71 "password": "password"
72 }
73 }
74 }
75}
76
77// For CallMeBot
78// For Signal Messenger: https://www.callmebot.com/blog/free-api-signal-send-messages/
79{
80 "messaging": {
81 "callmebot": true
82 }
83}
84
85// For Pushover
86{
87 "messaging": {
88 "pushover": {
89 "token": "xxxxxxx"
90 }
91 }
92}
93
94// For ntfy
95{
96 "messaging": {
97 "ntfy": true
98 }
99}
100
101// For zulip
102{
103 "messaging": {
104 "site": "https://api.zulip.com",
105 "email": "your-bot@zulip.com",
106 "api_key": "your_32_character_api_key"
107 }
108}
109
110// For Slack Webhook
111{
112 "messaging": {
113 "slack": true
114 }
115}
116
117*/
118
119// Construct a messaging server object
120module.exports.CreateServer = function (parent) {
121 var obj = {};
122 obj.parent = parent;
123 obj.providers = 0; // 1 = Telegram, 2 = Signal, 4 = Discord, 8 = XMPP, 16 = CallMeBot, 32 = Pushover, 64 = ntfy, 128 = Zulip, 256 = Slack
124 obj.telegramClient = null;
125 obj.discordClient = null;
126 obj.discordUrl = null;
127 obj.xmppClient = null;
128 var xmppXml = null;
129 obj.callMeBotClient = null;
130 obj.pushoverClient = null;
131 obj.zulipClient = null;
132 obj.slackClient = null;
133 const sortCollator = new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' })
134
135 // Telegram client setup
136 if (parent.config.messaging.telegram) {
137 // Validate Telegram configuration values
138 var telegramOK = true;
139 if (typeof parent.config.messaging.telegram.apiid != 'number') { console.log('Invalid or missing Telegram apiid.'); telegramOK = false; }
140 if (typeof parent.config.messaging.telegram.apihash != 'string') { console.log('Invalid or missing Telegram apihash.'); telegramOK = false; }
141 if ((typeof parent.config.messaging.telegram.session != 'string') && (typeof parent.config.messaging.telegram.bottoken != 'string')) { console.log('Invalid or missing Telegram session or bottoken.'); telegramOK = false; }
142
143 if (telegramOK) {
144 // Setup Telegram
145 async function setupTelegram() {
146 const { TelegramClient } = require('telegram');
147 const { StringSession } = require('telegram/sessions');
148 const { Logger } = require('telegram/extensions/Logger');
149 const logger = new Logger({ LogLevel : 'none' });
150 const input = require('input');
151 var client;
152 var options = { connectionRetries: 5, baseLogger: logger };
153 if (parent.config.messaging.telegram.usewss == false) { options.useWSS = false; }
154 if (typeof parent.config.messaging.telegram.connectionretries == 'number') { options.connectionRetries = parent.config.messaging.telegram.connectionretries; }
155 if (typeof parent.config.messaging.telegram.proxy == 'object') { options.proxy = parent.config.messaging.telegram.proxy; }
156 if (parent.config.messaging.telegram.bottoken == null) {
157 // User login
158 var stringSession = new StringSession(parent.config.messaging.telegram.session);
159 const client = new TelegramClient(stringSession, parent.config.messaging.telegram.apiid, parent.config.messaging.telegram.apihash, options);
160 await client.start({ onError: function (err) { console.log('Telegram error', err); } });
161 obj.telegramClient = client;
162 obj.providers += 1; // Enable Telegram messaging
163 console.log("MeshCentral Telegram client is user connected.");
164 } else {
165 // Bot login
166 var stringSession = new StringSession('');
167 const client = new TelegramClient(stringSession, parent.config.messaging.telegram.apiid, parent.config.messaging.telegram.apihash, options);
168 await client.start({ botAuthToken: parent.config.messaging.telegram.bottoken, onError: function (err) { console.log('Telegram error', err); } });
169 obj.telegramClient = client;
170 obj.providers += 1; // Enable Telegram messaging
171 console.log("MeshCentral Telegram client is bot connected.");
172 }
173 }
174 setupTelegram();
175 }
176 }
177
178 // Discord client setup
179 if (parent.config.messaging.discord) {
180 // Validate Discord configuration values
181 var discordOK = true;
182 if (typeof parent.config.messaging.discord.token != 'string') { console.log('Invalid or missing Discord token.'); discordOK = false; }
183
184 if (discordOK) {
185 // Setup Discord
186 const { Client, GatewayIntentBits } = require('discord.js');
187 var discordClient = new Client({
188 intents: [
189 GatewayIntentBits.Guilds,
190 GatewayIntentBits.GuildMessages,
191 GatewayIntentBits.MessageContent,
192 GatewayIntentBits.GuildMembers,
193 GatewayIntentBits.DirectMessages
194 ]
195 });
196
197 // Called when Discord client is connected
198 discordClient.on('ready', function() {
199 console.log(`MeshCentral Discord client is connected as ${discordClient.user.tag}!`);
200 obj.discordClient = discordClient;
201 obj.discordUrl = parent.config.messaging.discord.serverurl;
202 obj.providers += 4; // Enable Discord messaging
203 });
204
205 // Receives incoming messages, ignore for now
206 discordClient.on('messageCreate', function(message) {
207 if (message.author.bot) return false;
208 console.log(`Discord message from ${message.author.username}: ${message.content}`, message.channel.type);
209 //message.channel.send("Channel Hello");
210 //message.author.send('Private Hello');
211 });
212
213 // Called when Discord client received an interaction
214 discordClient.on('interactionCreate', async function(interaction) {
215 console.log('Discord interaction', interaction);
216 if (!interaction.isChatInputCommand()) return;
217 if (interaction.commandName === 'ping') { await interaction.reply('Pong!'); }
218 });
219
220 // Connect Discord client
221 discordClient.login(parent.config.messaging.discord.token);
222 }
223 }
224
225 // XMPP client setup
226 if (parent.config.messaging.xmpp) {
227 // Validate XMPP configuration values
228 var xmppOK = true;
229 if (typeof parent.config.messaging.xmpp.service != 'string') { console.log('Invalid or missing XMPP service.'); xmppOK = false; }
230
231 if (xmppOK) {
232 // Setup XMPP
233 const { client, xml } = require('@xmpp/client');
234 const xmpp = client(parent.config.messaging.xmpp);
235 xmpp.on('error', function (err) { parent.debug('email', 'XMPP error: ' + err); console.error('XMPP error', err); });
236 xmpp.on('offline', function () { parent.debug('email', 'XMPP client is offline.'); console.log('XMPP offline'); });
237 //xmpp.on('stanza', async function (stanza) { if (stanza.is("message")) { await xmpp.send(xml('presence', { type: 'unavailable' })); await xmpp.stop(); } });
238 xmpp.on('online', async function (address) {
239 // await xmpp.send(xml("presence")); const message = xml("message", { type: "chat", to: "username@server.com" }, xml("body", {}, "hello world")); await xmpp.send(message);
240 xmppXml = xml;
241 obj.xmppClient = xmpp;
242 obj.providers += 8; // Enable XMPP messaging
243 console.log("MeshCentral XMPP client is connected.");
244 });
245 xmpp.start().catch(console.error);
246 }
247 }
248
249 // CallMeBot client setup (https://www.callmebot.com/)
250 if (parent.config.messaging.callmebot) {
251 obj.callMeBotClient = true;
252 obj.providers += 16; // Enable CallMeBot messaging
253 }
254
255 // Pushover client setup (https://pushover.net)
256 if (typeof parent.config.messaging.pushover == 'object') {
257 // Validate Pushover configuration values
258 var pushoverOK = true;
259 if (typeof parent.config.messaging.pushover.token != 'string') { console.log('Invalid or missing Pushover token.'); pushoverOK = false; }
260
261 if (pushoverOK) {
262 // Setup PushOver
263 obj.pushoverClient = true;
264 obj.providers += 32; // Enable Pushover messaging
265 }
266 }
267
268 // ntfy client setup (https://ntfy.sh/)
269 if (parent.config.messaging.ntfy) {
270 obj.ntfyClient = true;
271 obj.providers += 64; // Enable ntfy messaging
272 }
273
274 // Zulip client setup (https://zulip.com/)
275 if (typeof parent.config.messaging.zulip == 'object') {
276 var zulip = require('zulip');
277 obj.zulipClient = new zulip.Client(parent.config.messaging.zulip);
278 obj.providers += 128; // Enable zulip messaging
279 }
280
281 // Slack Webhook setup (https://slack.com)
282 if (parent.config.messaging.slack) {
283 obj.slackClient = true;
284 obj.providers += 256; // Enable slack messaging
285 }
286
287 // Send a direct message to a specific userid
288 async function discordSendMsg(userId, message) {
289 const user = await obj.discordClient.users.fetch(userId).catch(function () { return null; });
290 if (!user) return;
291 await user.send(message).catch(function (ex) { console.log('Discord Error', ex); });
292 }
293
294 // Convert a userTag to a userId. We need to query the Discord server to find this information.
295 // Example: findUserByTab('aaaa#0000', function (userid) { sendMsg(userid, 'message'); });
296 async function discordFindUserByTag(userTag, func) {
297 var username = userTag.split('#')[0];
298 const guilds = await obj.discordClient.guilds.fetch();
299 guilds.forEach(async function (value, key) {
300 var guild = await value.fetch();
301 const guildMembers = await guild.members.search({ query: username });
302 guildMembers.forEach(async function (value, key) {
303 if ((value.user.username + (value.user.discriminator != '0' ? '#' + value.user.discriminator : ''))== userTag) { func(key); return; }
304 });
305 });
306 }
307
308 // Send an XMPP message
309 async function sendXmppMessage(to, msg, func) {
310 const message = xmppXml('message', { type: 'chat', to: to.substring(5) }, xmppXml('body', {}, msg));
311 await obj.xmppClient.send(message);
312 if (func != null) { func(true); }
313 }
314
315 // Send an user message
316 obj.sendMessage = function(to, msg, domain, func) {
317 if ((to.startsWith('telegram:')) && (obj.telegramClient != null)) { // Telegram
318 async function sendTelegramMessage(to, msg, func) {
319 if (obj.telegramClient == null) return;
320 parent.debug('email', 'Sending Telegram message to: ' + to.substring(9) + ': ' + msg);
321 try { await obj.telegramClient.sendMessage(to.substring(9), { message: msg }); if (func != null) { func(true); } } catch (ex) { if (func != null) { func(false, ex); } }
322 }
323 sendTelegramMessage(to, msg, func);
324 } else if ((to.startsWith('discord:')) && (obj.discordClient != null)) { // Discord
325 discordFindUserByTag(to.substring(8), function (userid) {
326 parent.debug('email', 'Sending Discord message to: ' + to.substring(9) + ', ' + userid + ': ' + msg);
327 discordSendMsg(userid, msg); if (func != null) { func(true); }
328 });
329 } else if ((to.startsWith('xmpp:')) && (obj.xmppClient != null)) { // XMPP
330 parent.debug('email', 'Sending XMPP message to: ' + to.substring(5) + ': ' + msg);
331 sendXmppMessage(to, msg, func);
332 } else if ((to.startsWith('callmebot:')) && (obj.callMeBotClient != null)) { // CallMeBot
333 parent.debug('email', 'Sending CallMeBot message to: ' + to.substring(10) + ': ' + msg);
334 console.log('Sending CallMeBot message to: ' + to.substring(10) + ': ' + msg);
335 var toData = to.substring(10).split('|');
336 if ((toData[0] == 'signal') && (toData.length == 3)) {
337 var url = 'https://api.callmebot.com/signal/send.php?phone=' + encodeURIComponent(toData[1]) + '&apikey=' + encodeURIComponent(toData[2]) + '&text=' + encodeURIComponent(msg);
338 require('https').get(url, function (r) { if (func != null) { func(r.statusCode == 200); } });
339 } else if ((toData[0] == 'whatsapp') && (toData.length == 3)) {
340 var url = 'https://api.callmebot.com/whatsapp.php?phone=' + encodeURIComponent(toData[1]) + '&apikey=' + encodeURIComponent(toData[2]) + '&text=' + encodeURIComponent(msg);
341 require('https').get(url, function (r) { if (func != null) { func(r.statusCode == 200); } });
342 } else if ((toData[0] == 'facebook') && (toData.length == 2)) {
343 var url = 'https://api.callmebot.com/facebook/send.php?apikey=' + encodeURIComponent(toData[1]) + '&text=' + encodeURIComponent(msg);
344 require('https').get(url, function (r) { if (func != null) { func(r.statusCode == 200); } });
345 } else if ((toData[0] == 'telegram') && (toData.length == 2)) {
346 var url = 'https://api.callmebot.com/text.php?user=' + encodeURIComponent(toData[1]) + '&text=' + encodeURIComponent(msg);
347 require('https').get(url, function (r) { if (func != null) { func(r.statusCode == 200); } });
348 }
349 } else if ((to.startsWith('pushover:')) && (obj.pushoverClient != null)) { // Pushover
350 const Pushover = require('node-pushover');
351 const push = new Pushover({ token: parent.config.messaging.pushover.token, user: to.substring(9) });
352 push.send(domain.title ? domain.title : 'MeshCentral', msg, function (err, res) { if (func != null) { func(err == null); } });
353 } else if ((to.startsWith('ntfy:')) && (obj.ntfyClient != null)) { // ntfy
354 const url = 'https://' + (((typeof parent.config.messaging.ntfy == 'object') && (typeof parent.config.messaging.ntfy.host == 'string')) ? parent.config.messaging.ntfy.host : 'ntfy.sh') + '/' + encodeURIComponent(to.substring(5));
355 const headers = { 'User-Agent': 'MeshCentral v' + parent.currentVer };
356 if (typeof parent.config.messaging.ntfy.authorization == 'string') { headers['Authorization'] = parent.config.messaging.ntfy.authorization; }
357 const req = require('https').request(new URL(url), { method: 'POST', headers: headers }, function (res) { if (func != null) { func(res.statusCode == 200); } });
358 req.end(msg);
359 } else if ((to.startsWith('zulip:')) && (obj.zulipClient != null)) { // zulip
360 obj.zulipClient.sendMessage({
361 type: 'private',
362 content: msg,
363 to: [ to.substring(6) ],
364 subject: domain.title ? domain.title : 'MeshCentral'
365 });
366 if (func != null) { func(true); }
367 }else if ((to.startsWith('slack:')) && (obj.slackClient != null)) { //slack
368 const req = require('https').request(new URL(to.substring(6)), { method: 'POST' }, function (res) { if (func != null) { func(true); } });
369 req.on('error', function (err) { if (func != null) { func(false); } });
370 req.write(JSON.stringify({"text": msg }));
371 req.end();
372 } else {
373 // No providers found
374 if (func != null) { func(false, "No messaging providers found for this message."); }
375 }
376 }
377
378 // Convert a CallMeBot URL into a handle
379 obj.callmebotUrlToHandle = function (xurl) {
380 var url = null;
381 try { url = require('url').parse(xurl); } catch (ex) { return; }
382 if ((url == null) || (url.host != 'api.callmebot.com') || (url.query == null)) return;
383 var urlArgs = {}, urlArgs2 = url.query.split('&');
384 for (var i in urlArgs2) { var j = urlArgs2[i].indexOf('='); if (j > 0) { urlArgs[urlArgs2[i].substring(0, j)] = urlArgs2[i].substring(j + 1); } }
385 if ((urlArgs['phone'] != null) && (urlArgs['phone'].indexOf('|') >= 0)) return;
386 if ((urlArgs['apikey'] != null) && (urlArgs['apikey'].indexOf('|') >= 0)) return;
387 if ((urlArgs['user'] != null) && (urlArgs['user'].indexOf('|') >= 0)) return;
388 // Signal Messenger, Whatapp, Facebook and Telegram
389 if (url.path.startsWith('/signal') && (urlArgs['phone'] != null) && (urlArgs['apikey'] != null)) { return 'callmebot:signal|' + urlArgs['phone'] + '|' + urlArgs['apikey']; }
390 if (url.path.startsWith('/whatsapp') && (urlArgs['phone'] != null) && (urlArgs['apikey'] != null)) { return 'callmebot:whatsapp|' + urlArgs['phone'] + '|' + urlArgs['apikey']; }
391 if (url.path.startsWith('/facebook') && (urlArgs['apikey'] != null)) { return 'callmebot:facebook|' + urlArgs['apikey']; }
392 if (url.path.startsWith('/text') && (urlArgs['user'] != null)) { return 'callmebot:telegram|' + urlArgs['user']; }
393 return null;
394 }
395
396 // Get the correct SMS template
397 function getTemplate(templateNumber, domain, lang) {
398 parent.debug('email', 'Getting SMS template #' + templateNumber + ', lang: ' + lang);
399 if (Array.isArray(lang)) { lang = lang[0]; } // TODO: For now, we only use the first language given.
400 if (lang != null) { lang = lang.split('-')[0]; } // Take the first part of the language, "xx-xx"
401
402 var r = {}, emailsPath = null;
403 if ((domain != null) && (domain.webemailspath != null)) { emailsPath = domain.webemailspath; }
404 else if (obj.parent.webEmailsOverridePath != null) { emailsPath = obj.parent.webEmailsOverridePath; }
405 else if (obj.parent.webEmailsPath != null) { emailsPath = obj.parent.webEmailsPath; }
406 if ((emailsPath == null) || (obj.parent.fs.existsSync(emailsPath) == false)) { return null }
407
408 // Get the non-english sms if needed
409 var txtfile = null;
410 if ((lang != null) && (lang != 'en')) {
411 var translationsPath = obj.parent.path.join(emailsPath, 'translations');
412 var translationsPathTxt = obj.parent.path.join(emailsPath, 'translations', 'sms-messages_' + lang + '.txt');
413 if (obj.parent.fs.existsSync(translationsPath) && obj.parent.fs.existsSync(translationsPathTxt)) {
414 txtfile = obj.parent.fs.readFileSync(translationsPathTxt).toString();
415 }
416 }
417
418 // Get the english sms
419 if (txtfile == null) {
420 var pathTxt = obj.parent.path.join(emailsPath, 'sms-messages.txt');
421 if (obj.parent.fs.existsSync(pathTxt)) {
422 txtfile = obj.parent.fs.readFileSync(pathTxt).toString();
423 }
424 }
425
426 // If no english sms and a non-english language is requested, try to get the default translated sms
427 if (txtfile == null && (lang != null) && (lang != 'en')) {
428 var translationsPath = obj.parent.path.join(obj.parent.webEmailsPath, 'translations');
429 var translationsPathTxt = obj.parent.path.join(obj.parent.webEmailsPath, 'translations', 'sms-messages_' + lang + '.txt');
430 if (obj.parent.fs.existsSync(translationsPath) && obj.parent.fs.existsSync(translationsPathTxt)) {
431 txtfile = obj.parent.fs.readFileSync(translationsPathTxt).toString();
432 }
433 }
434
435 // If no default translated sms, try to get the default english sms
436 if (txtfile == null) {
437 var pathTxt = obj.parent.path.join(obj.parent.webEmailsPath, 'sms-messages.txt');
438 if (obj.parent.fs.existsSync(pathTxt)) {
439 txtfile = obj.parent.fs.readFileSync(pathTxt).toString();
440 }
441 }
442
443 // No email templates
444 if (txtfile == null) { return null; }
445
446 // Decode the TXT file
447 var lines = txtfile.split('\r\n').join('\n').split('\n')
448 if (lines.length <= templateNumber) return null;
449
450 return lines[templateNumber];
451 }
452
453 // Send messaging account verification
454 obj.sendMessagingCheck = function (domain, to, verificationCode, language, func) {
455 parent.debug('email', "Sending verification message to " + to);
456
457 var sms = getTemplate(0, domain, language);
458 if (sms == null) { parent.debug('email', "Error: Failed to get SMS template"); return; } // No SMS template found
459
460 // Setup the template
461 sms = sms.split('[[0]]').join(domain.title ? domain.title : 'MeshCentral');
462 sms = sms.split('[[1]]').join(verificationCode);
463
464 // Send the message
465 obj.sendMessage(to, sms, domain, func);
466 };
467
468 // Send 2FA verification
469 obj.sendToken = function (domain, to, verificationCode, language, func) {
470 parent.debug('email', "Sending login token message to " + to);
471
472 var sms = getTemplate(1, domain, language);
473 if (sms == null) { parent.debug('email', "Error: Failed to get SMS template"); return; } // No SMS template found
474
475 // Setup the template
476 sms = sms.split('[[0]]').join(domain.title ? domain.title : 'MeshCentral');
477 sms = sms.split('[[1]]').join(verificationCode);
478
479 // Send the message
480 obj.sendMessage(to, sms, domain, func);
481 };
482
483 // Send device state change notification
484 obj.sendDeviceNotify = function (domain, username, to, connections, disconnections, lang) {
485 if (to == null) return;
486 parent.debug('email', "Sending device state change message to " + to);
487
488 // Format the message
489 var sms = [];
490 if (connections.length > 0) { sms.push('Connections: ' + connections.join(', ')); } // TODO: Translate 'Connections: '
491 if (disconnections.length > 0) { sms.push('Disconnections: ' + disconnections.join(', ')); } // TODO: Translate 'Disconnections: '
492 if (sms.length == 0) return;
493 sms = sms.join(' - ');
494 if (sms.length > 1000) { sms = sms.substring(0, 997) + '...'; } // Limit messages to 1000 characters
495
496 // Send the message
497 obj.sendMessage(to, sms, domain, null);
498 };
499
500 // Send help request notification
501 obj.sendDeviceHelpRequest = function (domain, username, to, devicename, nodeid, helpusername, helprequest, lang) {
502 if (to == null) return;
503 parent.debug('email', "Sending device help request message to " + to);
504
505 // Format the message
506 var sms = "Help Request from " + devicename + ': ' + helprequest; // TODO: Translate 'Help Request from {0}:'
507 if (sms.length > 1000) { sms = sms.substring(0, 997) + '...'; } // Limit messages to 1000 characters
508
509 // Send the message
510 obj.sendMessage(to, sms, domain, null);
511 }
512
513
514 //
515 // Device connection and disconnection notifications
516 //
517
518 obj.deviceNotifications = {}; // UserId --> { timer, nodes: nodeid --> connectType }
519
520 // A device connected and a user needs to be notified about it.
521 obj.notifyDeviceConnect = function (user, meshid, nodeid, connectTime, connectType, powerState, serverid, extraInfo) {
522 const mesh = parent.webserver.meshes[meshid];
523 if (mesh == null) return;
524
525 // Add the user and start a timer
526 if (obj.deviceNotifications[user._id] == null) {
527 obj.deviceNotifications[user._id] = { nodes: {} };
528 obj.deviceNotifications[user._id].timer = setTimeout(function () { sendDeviceNotifications(user._id); }, 1 * 60 * 1000); // 1 minute before message is sent
529 }
530
531 // Add the device
532 if (obj.deviceNotifications[user._id].nodes[nodeid] == null) {
533 obj.deviceNotifications[user._id].nodes[nodeid] = { c: connectType }; // This device connection need to be added
534 } else {
535 const info = obj.deviceNotifications[user._id].nodes[nodeid];
536 if ((info.d != null) && ((info.d & connectType) != 0)) {
537 info.d -= connectType; // This device disconnect cancels out a device connection
538 if (((info.c == null) || (info.c == 0)) && ((info.d == null) || (info.d == 0))) {
539 // This device no longer needs a notification
540 delete obj.deviceNotifications[user._id].nodes[nodeid];
541 if (Object.keys(obj.deviceNotifications[user._id].nodes).length == 0) {
542 // This user no longer needs a notification
543 clearTimeout(obj.deviceNotifications[user._id].timer);
544 delete obj.deviceNotifications[user._id];
545 }
546 return;
547 }
548 } else {
549 if (info.c != null) {
550 info.c |= connectType; // This device disconnect needs to be added
551 } else {
552 info.c = connectType; // This device disconnect needs to be added
553 }
554 }
555 }
556
557 // Set the device group name
558 if ((extraInfo != null) && (extraInfo.name != null)) { obj.deviceNotifications[user._id].nodes[nodeid].nn = extraInfo.name; }
559 obj.deviceNotifications[user._id].nodes[nodeid].mn = mesh.name;
560 }
561
562 // Cancel a device disconnect notification
563 obj.cancelNotifyDeviceDisconnect = function (user, meshid, nodeid, connectTime, connectType, powerState, serverid, extraInfo) {
564 const mesh = parent.webserver.meshes[meshid];
565 if (mesh == null) return;
566
567 if ((obj.deviceNotifications[user._id] != null) && (obj.deviceNotifications[user._id].nodes[nodeid] != null)) {
568 const info = obj.deviceNotifications[user._id].nodes[nodeid];
569 if ((info.d != null) && ((info.d & connectType) != 0)) {
570 info.d -= connectType; // This device disconnect cancels out a device connection
571 if (((info.c == null) || (info.c == 0)) && ((info.d == null) || (info.d == 0))) {
572 // This device no longer needs a notification
573 delete obj.deviceNotifications[user._id].nodes[nodeid];
574 if (Object.keys(obj.deviceNotifications[user._id].nodes).length == 0) {
575 // This user no longer needs a notification
576 clearTimeout(obj.deviceNotifications[user._id].timer);
577 delete obj.deviceNotifications[user._id];
578 }
579 }
580 }
581 }
582 }
583
584 // A device disconnected and a user needs to be notified about it.
585 obj.notifyDeviceDisconnect = function (user, meshid, nodeid, connectTime, connectType, powerState, serverid, extraInfo) {
586 const mesh = parent.webserver.meshes[meshid];
587 if (mesh == null) return;
588
589 // Add the user and start a timer
590 if (obj.deviceNotifications[user._id] == null) {
591 obj.deviceNotifications[user._id] = { nodes: {} };
592 obj.deviceNotifications[user._id].timer = setTimeout(function () { sendDeviceNotifications(user._id); }, 1 * 60 * 1000); // 1 minute before message is sent
593 }
594
595 // Add the device
596 if (obj.deviceNotifications[user._id].nodes[nodeid] == null) {
597 obj.deviceNotifications[user._id].nodes[nodeid] = { d: connectType }; // This device disconnect need to be added
598 } else {
599 const info = obj.deviceNotifications[user._id].nodes[nodeid];
600 if ((info.c != null) && ((info.c & connectType) != 0)) {
601 info.c -= connectType; // This device disconnect cancels out a device connection
602 if (((info.d == null) || (info.d == 0)) && ((info.c == null) || (info.c == 0))) {
603 // This device no longer needs a notification
604 delete obj.deviceNotifications[user._id].nodes[nodeid];
605 if (Object.keys(obj.deviceNotifications[user._id].nodes).length == 0) {
606 // This user no longer needs a notification
607 clearTimeout(obj.deviceNotifications[user._id].timer);
608 delete obj.deviceNotifications[user._id];
609 }
610 return;
611 }
612 } else {
613 if (info.d != null) {
614 info.d |= connectType; // This device disconnect needs to be added
615 } else {
616 info.d = connectType; // This device disconnect needs to be added
617 }
618 }
619 }
620
621 // Set the device group name
622 if ((extraInfo != null) && (extraInfo.name != null)) { obj.deviceNotifications[user._id].nodes[nodeid].nn = extraInfo.name; }
623 obj.deviceNotifications[user._id].nodes[nodeid].mn = mesh.name;
624 }
625
626 // Cancel a device connect notification
627 obj.cancelNotifyDeviceConnect = function (user, meshid, nodeid, connectTime, connectType, powerState, serverid, extraInfo) {
628 const mesh = parent.webserver.meshes[meshid];
629 if (mesh == null) return;
630
631 if ((obj.deviceNotifications[user._id] != null) && (obj.deviceNotifications[user._id].nodes[nodeid] != null)) {
632 const info = obj.deviceNotifications[user._id].nodes[nodeid];
633 if ((info.c != null) && ((info.c & connectType) != 0)) {
634 info.c -= connectType; // This device disconnect cancels out a device connection
635 if (((info.d == null) || (info.d == 0)) && ((info.c == null) || (info.c == 0))) {
636 // This device no longer needs a notification
637 delete obj.deviceNotifications[user._id].nodes[nodeid];
638 if (Object.keys(obj.deviceNotifications[user._id].nodes).length == 0) {
639 // This user no longer needs a notification
640 clearTimeout(obj.deviceNotifications[user._id].timer);
641 delete obj.deviceNotifications[user._id];
642 }
643 }
644 }
645 }
646 }
647
648 // Send a notification about device connections and disconnections to a user
649 function sendDeviceNotifications(userid) {
650 if (obj.deviceNotifications[userid] == null) return;
651 clearTimeout(obj.deviceNotifications[userid].timer);
652
653 var connections = [];
654 var disconnections = [];
655
656 for (var nodeid in obj.deviceNotifications[userid].nodes) {
657 var info = obj.deviceNotifications[userid].nodes[nodeid];
658 if ((info.c != null) && (info.c > 0) && (info.nn != null) && (info.mn != null)) {
659 /*
660 var c = [];
661 if (info.c & 1) { c.push("Agent"); }
662 if (info.c & 2) { c.push("CIRA"); }
663 if (info.c & 4) { c.push("AMT"); }
664 if (info.c & 8) { c.push("AMT-Relay"); }
665 if (info.c & 16) { c.push("MQTT"); }
666 connections.push(info.mn + ', ' + info.nn + ': ' + c.join(', '));
667 */
668 if (info.c & 1) { connections.push(info.nn); }
669 }
670 if ((info.d != null) && (info.d > 0) && (info.nn != null) && (info.mn != null)) {
671 /*
672 var d = [];
673 if (info.d & 1) { d.push("Agent"); }
674 if (info.d & 2) { d.push("CIRA"); }
675 if (info.d & 4) { d.push("AMT"); }
676 if (info.d & 8) { d.push("AMT-Relay"); }
677 if (info.d & 16) { d.push("MQTT"); }
678 disconnections.push(info.mn + ', ' + info.nn + ': ' + d.join(', '));
679 */
680 if (info.d & 1) { disconnections.push(info.nn); }
681 }
682 }
683
684 // Sort the notifications
685 connections.sort(sortCollator.compare);
686 disconnections.sort(sortCollator.compare);
687
688 // Get the user and domain
689 const user = parent.webserver.users[userid];
690 if ((user == null) || (user.msghandle == null)) return;
691 const domain = obj.parent.config.domains[user.domain];
692 if (domain == null) return;
693
694 // Send the message
695 obj.sendDeviceNotify(domain, user.name, user.msghandle, connections, disconnections, user.llang);
696
697 // Clean up
698 delete obj.deviceNotifications[userid];
699 }
700
701 return obj;
702};
703
704// Called to setup the Telegram session key
705module.exports.SetupTelegram = async function (parent) {
706 // If basic telegram values are not setup, instruct the user on how to get them.
707 if ((typeof parent.config.messaging != 'object') || (typeof parent.config.messaging.telegram != 'object') || (typeof parent.config.messaging.telegram.apiid != 'number') || (typeof parent.config.messaging.telegram.apihash != 'string')) {
708 console.log('Login to your Telegram account at this URL: https://my.telegram.org/.');
709 console.log('Click "API development tools" and fill your application details (only app title and short name required).');
710 console.log('Click "Create application"');
711 console.log('Set this apiid and apihash values in the messaging section of the config.json like this:');
712 console.log('{');
713 console.log(' "messaging": {');
714 console.log(' "telegram": {');
715 console.log(' "apiid": 123456,');
716 console.log(' "apihash": "123456abcdfg"');
717 console.log(' }');
718 console.log(' }');
719 console.log('}');
720 console.log('Then, run --setuptelegram again to continue.');
721 process.exit();
722 return;
723 }
724
725 // If the session value is missing, perform the process to get it
726 if (((parent.config.messaging.telegram.session == null) || (parent.config.messaging.telegram.session == '') || (typeof parent.config.messaging.telegram.session != 'string')) && ((parent.config.messaging.telegram.bottoken == null) || (parent.config.messaging.telegram.bottoken == '') || (typeof parent.config.messaging.telegram.bottoken != 'string'))) {
727 if (parent.args.setuptelegram == 'user') {
728 const { TelegramClient } = require('telegram');
729 const { StringSession } = require('telegram/sessions');
730 const { Logger } = require('telegram/extensions/Logger');
731 const logger = new Logger({ LogLevel: 'none' });
732 const input = require('input');
733 const stringSession = new StringSession('');
734 const client = new TelegramClient(stringSession, parent.config.messaging.telegram.apiid, parent.config.messaging.telegram.apihash, { connectionRetries: 5, baseLogger: logger });
735 await client.start({
736 phoneNumber: async function () { return await input.text("Please enter your number (+1-111-222-3333): "); },
737 password: async function () { return await input.text("Please enter your password: "); },
738 phoneCode: async function () { return await input.text("Please enter the code you received: "); },
739 onError: function (err) { console.log('Telegram error', err); }
740 });
741 console.log('Set this session value in the messaging section of the config.json like this:');
742 console.log('{');
743 console.log(' "messaging": {');
744 console.log(' "telegram": {');
745 console.log(' "apiid": ' + parent.config.messaging.telegram.apiid + ',');
746 console.log(' "apihash": "' + parent.config.messaging.telegram.apihash + '",');
747 console.log(' "session": "' + client.session.save() + '"');
748 console.log(' }');
749 console.log(' }');
750 console.log('}');
751 process.exit();
752 } else if (parent.args.setuptelegram == 'bot') {
753 console.log('Login to your Telegram account, search for "BotFather", message him and create a bot.');
754 console.log('Once you get the HTTP API token, add it in the config.json as "bottoken" like so:');
755 console.log('{');
756 console.log(' "messaging": {');
757 console.log(' "telegram": {');
758 console.log(' "apiid": ' + parent.config.messaging.telegram.apiid + ',');
759 console.log(' "apihash": "' + parent.config.messaging.telegram.apihash + '",');
760 console.log(' "bottoken": "00000000:aaaaaaaaaaaaaaaaaaaaaaaa"');
761 console.log(' }');
762 console.log(' }');
763 console.log('}');
764 process.exit();
765 } else {
766 console.log('run "--setuptelegram bot" to setup Telegram login as a bot (typical).');
767 console.log('run "--setuptelegram user" to setup Telegram login as a user.');
768 process.exit();
769 }
770 }
771
772 // All Telegram values seem ok
773 console.log('Telegram seems to be configured correctly in the config.json, no need to run --setuptelegram.');
774 process.exit();
775};