@patricktobias86/node-red-telegram-account 1.1.16 → 1.1.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,14 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## [1.1.18] - 2026-01-05
6
+ ### Fixed
7
+ - Receiver node now populates `payload.chatId` / `payload.senderId` when Telegram IDs arrive as numeric strings (common in debug/output), so downstream filters work reliably.
8
+
9
+ ## [1.1.17] - 2026-01-05
10
+ ### Fixed
11
+ - Receiver node now listens to Raw MTProto updates and derives sender/chat identity safely so valid messages (channel posts, anonymous admins, service messages, missing fromId) are no longer dropped.
12
+
5
13
  ## [1.1.16] - 2025-09-22
6
14
  ### Added
7
15
  - Receiver node option to ignore configurable message types (such as videos or documents) to prevent oversized uploads.
package/README.md CHANGED
@@ -18,7 +18,7 @@ See [docs/NODES.md](docs/NODES.md) for a detailed description of every node. Bel
18
18
 
19
19
  - **config** – stores your API credentials and caches sessions for reuse.
20
20
  - **auth** – interactive login that outputs a `stringSession` (also set on `msg.stringSession`).
21
- - **receiver** – emits messages for every incoming update (with optional ignore list, message type filter, and media size limit). Event listeners are cleaned up on node close so redeploys won't duplicate messages.
21
+ - **receiver** – emits messages for every incoming Telegram message using Raw MTProto updates (with optional ignore list, message type filter, and media size limit), including channel posts and service messages. Event listeners are cleaned up on node close so redeploys won't duplicate messages.
22
22
  - **command** – triggers when an incoming message matches a command or regex. Event listeners are removed on redeploy to prevent duplicates.
23
23
  - **send-message** – sends text or media messages with rich options.
24
24
  - **send-files** – uploads one or more files with captions and buttons.
package/docs/NODES.md CHANGED
@@ -8,7 +8,7 @@ Below is a short description of each node. For a full list of configuration opti
8
8
  |------|-------------|
9
9
  | **config** | Configuration node storing API credentials and connection options. Other nodes reference this to share a Telegram client and reuse the session. Connections are tracked in a Map with a reference count so multiple nodes can wait for the same connection. |
10
10
  | **auth** | Starts an interactive login flow. Produces a `stringSession` (available in both <code>msg.payload.stringSession</code> and <code>msg.stringSession</code>) that can be reused with the `config` node. |
11
- | **receiver** | Emits an output message for every incoming Telegram message. Can ignore specific user IDs, skip selected message types (e.g. videos or documents), and optionally drop media above a configurable size. Event handlers are automatically removed when the node is closed. |
11
+ | **receiver** | Emits an output message for every incoming Telegram message using Raw MTProto updates (so channel posts, anonymous admins and service messages are not missed). Can ignore specific user IDs, skip selected message types (e.g. videos or documents), and optionally drop media above a configurable size. Output includes derived `peer`, `sender`, `senderType`, `senderId`, `chatId`, `isSilent`, and `messageTypes`. `chatId` is normalized to the MTProto dialog id (userId for private chats, chatId for legacy groups, channelId for supergroups/channels — not the Bot API `-100...` form). Event handlers are automatically removed when the node is closed. |
12
12
  | **command** | Listens for new messages and triggers when a message matches a configured command or regular expression. The event listener is cleaned up on node close to avoid duplicates. |
13
13
  | **send-message** | Sends text messages or media files to a chat. Supports parse mode, buttons, scheduling, and more. |
14
14
  | **send-files** | Uploads one or more files to a chat with optional caption, thumbnails and other parameters. |
@@ -99,6 +99,10 @@
99
99
  <span class="property-type">object</span>
100
100
  </dt>
101
101
  <dd>The raw Telegram update object containing details about the incoming message, sender, chat, and metadata.</dd>
102
+ <dt>payload.chatId
103
+ <span class="property-type">number</span>
104
+ </dt>
105
+ <dd>The normalized conversation identifier (user ID for private chats, chat ID for legacy groups, channel ID for supergroups/channels).</dd>
102
106
  </dl>
103
107
 
104
108
  <h3>Configuration</h3>
package/nodes/receiver.js CHANGED
@@ -1,4 +1,4 @@
1
- const { NewMessage } = require("telegram/events");
1
+ const { Raw } = require("telegram/events");
2
2
  const util = require("util");
3
3
 
4
4
  const splitList = (value) => {
@@ -148,6 +148,195 @@ const collectMessageTypes = (message) => {
148
148
  return types;
149
149
  };
150
150
 
151
+ const getClassName = (value) => {
152
+ if (!value || typeof value !== 'object') {
153
+ return undefined;
154
+ }
155
+ return value.className || value._;
156
+ };
157
+
158
+ const toPeerInfo = (peer) => {
159
+ if (!peer || typeof peer !== 'object') {
160
+ return null;
161
+ }
162
+
163
+ const className = getClassName(peer);
164
+ const userId = peer.userId ?? peer.user_id;
165
+ const chatId = peer.chatId ?? peer.chat_id;
166
+ const channelId = peer.channelId ?? peer.channel_id;
167
+
168
+ // GramJS TL objects represent peers as PeerUser / PeerChat / PeerChannel.
169
+ // MTProto updates can omit fromId (anonymous admins, channel posts, etc.), so
170
+ // we normalize peer identity from whichever peer object we have.
171
+ if (userId != null) {
172
+ return { type: 'user', userId, chatId: null, channelId: null, peer };
173
+ }
174
+ if (chatId != null) {
175
+ return { type: 'chat', userId: null, chatId, channelId: null, peer };
176
+ }
177
+ if (channelId != null) {
178
+ return { type: 'channel', userId: null, chatId: null, channelId, peer };
179
+ }
180
+
181
+ if (className) {
182
+ return { type: className, userId: null, chatId: null, channelId: null, peer };
183
+ }
184
+ return { type: 'unknown', userId: null, chatId: null, channelId: null, peer };
185
+ };
186
+
187
+ const toSafeNumber = (value) => {
188
+ if (typeof value === 'number') {
189
+ return Number.isFinite(value) ? value : null;
190
+ }
191
+ if (typeof value === 'string') {
192
+ const trimmed = value.trim();
193
+ if (/^-?\d+$/.test(trimmed)) {
194
+ try {
195
+ return toSafeNumber(BigInt(trimmed));
196
+ } catch (err) {
197
+ return null;
198
+ }
199
+ }
200
+ return null;
201
+ }
202
+ if (typeof value === 'bigint') {
203
+ const result = Number(value);
204
+ return Number.isFinite(result) ? result : Number.MAX_SAFE_INTEGER;
205
+ }
206
+ return null;
207
+ };
208
+
209
+ const peerChatId = (peerInfo) => {
210
+ if (!peerInfo) {
211
+ return null;
212
+ }
213
+ // For Node-RED flows "chatId" typically means "the conversation identifier".
214
+ // For private chats that's the userId; for groups it's chatId; for channels it's channelId.
215
+ if (peerInfo.type === 'user') {
216
+ return toSafeNumber(peerInfo.userId);
217
+ }
218
+ if (peerInfo.type === 'chat') {
219
+ return toSafeNumber(peerInfo.chatId);
220
+ }
221
+ if (peerInfo.type === 'channel') {
222
+ return toSafeNumber(peerInfo.channelId);
223
+ }
224
+ return null;
225
+ };
226
+
227
+ const buildPeerUser = (userId) => ({ className: 'PeerUser', userId });
228
+ const buildPeerChat = (chatId) => ({ className: 'PeerChat', chatId });
229
+
230
+ const toDerivedMessage = (update) => {
231
+ const className = getClassName(update);
232
+ if (className === 'UpdateShortMessage') {
233
+ const userId = update.userId ?? update.user_id;
234
+ return {
235
+ className: 'Message',
236
+ id: update.id,
237
+ date: update.date,
238
+ out: update.out,
239
+ silent: update.silent,
240
+ // In UpdateShortMessage, userId is the peer and also the sender for incoming messages.
241
+ peerId: userId != null ? buildPeerUser(userId) : undefined,
242
+ fromId: userId != null ? buildPeerUser(userId) : undefined,
243
+ message: update.message,
244
+ entities: update.entities,
245
+ fwdFrom: update.fwdFrom,
246
+ viaBotId: update.viaBotId,
247
+ replyTo: update.replyTo,
248
+ ttlPeriod: update.ttlPeriod,
249
+ media: update.media
250
+ };
251
+ }
252
+
253
+ if (className === 'UpdateShortChatMessage') {
254
+ const chatId = update.chatId ?? update.chat_id;
255
+ const fromId = update.fromId ?? update.from_id;
256
+ return {
257
+ className: 'Message',
258
+ id: update.id,
259
+ date: update.date,
260
+ out: update.out,
261
+ silent: update.silent,
262
+ peerId: chatId != null ? buildPeerChat(chatId) : undefined,
263
+ fromId: fromId != null ? buildPeerUser(fromId) : undefined,
264
+ message: update.message,
265
+ entities: update.entities,
266
+ fwdFrom: update.fwdFrom,
267
+ viaBotId: update.viaBotId,
268
+ replyTo: update.replyTo,
269
+ ttlPeriod: update.ttlPeriod,
270
+ media: update.media
271
+ };
272
+ }
273
+
274
+ if (className === 'UpdateShortSentMessage') {
275
+ // This update doesn't include a peer; it's still a real MTProto message update.
276
+ // We emit it with a best-effort message shape so flows can still react.
277
+ return {
278
+ className: 'Message',
279
+ id: update.id,
280
+ date: update.date,
281
+ out: true,
282
+ silent: update.silent,
283
+ message: update.message,
284
+ entities: update.entities,
285
+ media: update.media
286
+ };
287
+ }
288
+
289
+ return null;
290
+ };
291
+
292
+ const extractMessageEvents = (rawUpdate) => {
293
+ // Raw MTProto updates can be nested (Updates / UpdatesCombined / UpdateShort).
294
+ // We must unwrap them here instead of relying on NewMessage(), which can miss
295
+ // valid message updates (e.g. channel posts, edits, anonymous admins).
296
+ const results = [];
297
+
298
+ const walk = (update) => {
299
+ if (!update || typeof update !== 'object') {
300
+ return;
301
+ }
302
+
303
+ const className = getClassName(update);
304
+
305
+ if (Array.isArray(update.updates)) {
306
+ for (const nested of update.updates) {
307
+ walk(nested);
308
+ }
309
+ return;
310
+ }
311
+
312
+ if (update.update) {
313
+ walk(update.update);
314
+ return;
315
+ }
316
+
317
+ if (className === 'UpdateNewMessage' ||
318
+ className === 'UpdateNewChannelMessage' ||
319
+ className === 'UpdateEditMessage' ||
320
+ className === 'UpdateEditChannelMessage') {
321
+ if (update.message) {
322
+ results.push({ update, message: update.message });
323
+ } else {
324
+ results.push({ update, message: null });
325
+ }
326
+ return;
327
+ }
328
+
329
+ const derived = toDerivedMessage(update);
330
+ if (derived) {
331
+ results.push({ update, message: derived });
332
+ return;
333
+ }
334
+ };
335
+
336
+ walk(rawUpdate);
337
+ return results;
338
+ };
339
+
151
340
  module.exports = function (RED) {
152
341
  function Receiver(config) {
153
342
  RED.nodes.createNode(this, config);
@@ -162,17 +351,6 @@ module.exports = function (RED) {
162
351
  ? maxFileSizeMb * 1024 * 1024
163
352
  : null;
164
353
 
165
- const toNumber = (value) => {
166
- if (typeof value === 'number') {
167
- return value;
168
- }
169
- if (typeof value === 'bigint') {
170
- const result = Number(value);
171
- return Number.isFinite(result) ? result : Number.MAX_SAFE_INTEGER;
172
- }
173
- return undefined;
174
- };
175
-
176
354
  const extractPhotoSize = (photo) => {
177
355
  if (!photo || !Array.isArray(photo.sizes)) {
178
356
  return null;
@@ -184,13 +362,13 @@ module.exports = function (RED) {
184
362
  }
185
363
  if (Array.isArray(size.sizes)) {
186
364
  for (const nested of size.sizes) {
187
- const nestedValue = toNumber(nested);
365
+ const nestedValue = toSafeNumber(nested);
188
366
  if (nestedValue != null && nestedValue > max) {
189
367
  max = nestedValue;
190
368
  }
191
369
  }
192
370
  }
193
- const value = toNumber(size.size ?? size.length ?? size.bytes);
371
+ const value = toSafeNumber(size.size ?? size.length ?? size.bytes);
194
372
  if (value != null && value > max) {
195
373
  max = value;
196
374
  }
@@ -204,7 +382,7 @@ module.exports = function (RED) {
204
382
  }
205
383
 
206
384
  if (media.document && media.document.size != null) {
207
- const value = toNumber(media.document.size);
385
+ const value = toSafeNumber(media.document.size);
208
386
  if (value != null) {
209
387
  return value;
210
388
  }
@@ -219,7 +397,7 @@ module.exports = function (RED) {
219
397
 
220
398
  if (media.webpage) {
221
399
  const { document, photo } = media.webpage;
222
- const docSize = document && toNumber(document.size);
400
+ const docSize = document && toSafeNumber(document.size);
223
401
  if (docSize != null) {
224
402
  return docSize;
225
403
  }
@@ -231,7 +409,7 @@ module.exports = function (RED) {
231
409
 
232
410
  const className = media.className || media._;
233
411
  if ((className === 'MessageMediaDocument' || className === 'MessageMediaPhoto') && media.size != null) {
234
- const value = toNumber(media.size);
412
+ const value = toSafeNumber(media.size);
235
413
  if (value != null) {
236
414
  return value;
237
415
  }
@@ -240,45 +418,94 @@ module.exports = function (RED) {
240
418
  return null;
241
419
  };
242
420
 
243
- const event = new NewMessage();
244
- const handler = (update) => {
421
+ const debugLog = (message) => {
422
+ if (node.debugEnabled) {
423
+ node.log(message);
424
+ }
425
+ };
426
+
427
+ const event = new Raw({});
428
+ const handler = (rawUpdate) => {
245
429
  const debug = node.debugEnabled;
246
430
  if (debug) {
247
- node.log('receiver update: ' + util.inspect(update, { depth: null }));
431
+ node.log('receiver raw update: ' + util.inspect(rawUpdate, { depth: null }));
248
432
  }
249
- const message = update && update.message;
250
- if (!message) {
433
+
434
+ const extracted = extractMessageEvents(rawUpdate);
435
+ if (extracted.length === 0) {
436
+ // Raw emits *all* MTProto updates; many are not message-bearing updates (typing, reads, etc.).
437
+ // We do not output those by default, but we also do not silently hide that they occurred.
438
+ debugLog(`receiver ignoring non-message MTProto update: ${getClassName(rawUpdate) || 'unknown'}`);
251
439
  return;
252
440
  }
253
441
 
254
- if (ignoredMessageTypes.size > 0) {
442
+ for (const { update, message } of extracted) {
443
+ if (!message) {
444
+ debugLog(`receiver ignoring message update without message payload: ${getClassName(update) || 'unknown'}`);
445
+ continue;
446
+ }
447
+
255
448
  const messageTypes = collectMessageTypes(message);
256
- const shouldIgnoreType = Array.from(messageTypes).some((type) => ignoredMessageTypes.has(type));
257
- if (shouldIgnoreType) {
258
- if (debug) {
259
- node.log(`receiver ignoring update with types: ${Array.from(messageTypes).join(', ')}`);
449
+ const isSilent = Boolean(message.silent || update.silent);
450
+
451
+ const peer = toPeerInfo(message.peerId || message.toId);
452
+
453
+ // Do NOT assume message.fromId.userId exists.
454
+ // fromId can be PeerUser / PeerChat / PeerChannel, can be missing (channel posts),
455
+ // and can represent anonymous admins. Prefer fromId when present; otherwise fall back
456
+ // to the chat peer for channel posts to avoid dropping valid messages.
457
+ const sender =
458
+ toPeerInfo(message.fromId) ||
459
+ (message.senderId != null ? toPeerInfo(buildPeerUser(message.senderId)) : null) ||
460
+ (message.post ? peer : null);
461
+
462
+ const senderType = sender ? sender.type : 'unknown';
463
+ const senderId =
464
+ senderType === 'user' ? toSafeNumber(sender.userId) :
465
+ senderType === 'chat' ? toSafeNumber(sender.chatId) :
466
+ senderType === 'channel' ? toSafeNumber(sender.channelId) :
467
+ null;
468
+
469
+ const chatId = peerChatId(peer);
470
+
471
+ if (ignoredMessageTypes.size > 0) {
472
+ const shouldIgnoreType = Array.from(messageTypes).some((type) => ignoredMessageTypes.has(type));
473
+ if (shouldIgnoreType) {
474
+ debugLog(`receiver ignoring message due to ignoreMessageTypes; types=${Array.from(messageTypes).join(', ')}`);
475
+ continue;
260
476
  }
261
- return;
262
477
  }
263
- }
264
478
 
265
- if (maxFileSizeBytes != null) {
266
- const mediaSize = extractMediaSize(message.media);
267
- if (mediaSize != null && mediaSize > maxFileSizeBytes) {
268
- if (debug) {
269
- node.log(`receiver ignoring update with media size ${mediaSize} bytes exceeding limit ${maxFileSizeBytes}`);
479
+ if (maxFileSizeBytes != null) {
480
+ const mediaSize = extractMediaSize(message.media);
481
+ if (mediaSize != null && mediaSize > maxFileSizeBytes) {
482
+ debugLog(`receiver ignoring message due to maxFileSizeMb; mediaSize=${mediaSize} limitBytes=${maxFileSizeBytes}`);
483
+ continue;
270
484
  }
271
- return;
272
485
  }
273
- }
274
486
 
275
- const senderId = message.fromId && message.fromId.userId;
276
- if (senderId != null && ignore.includes(senderId.toString())) {
277
- return;
278
- }
487
+ // Preserve existing behavior: ignore list is a list of user IDs.
488
+ // Previously the node assumed message.fromId.userId always existed, which dropped valid updates.
489
+ // Now we only apply the ignore list when we can confidently identify a user sender.
490
+ if (senderType === 'user' && senderId != null && ignore.includes(String(senderId))) {
491
+ debugLog(`receiver ignoring message due to ignore list; userId=${senderId}`);
492
+ continue;
493
+ }
494
+
495
+ const out = {
496
+ payload: {
497
+ update,
498
+ message,
499
+ peer,
500
+ sender,
501
+ senderType,
502
+ senderId,
503
+ chatId,
504
+ isSilent,
505
+ messageTypes: Array.from(messageTypes)
506
+ }
507
+ };
279
508
 
280
- if (message.fromId != null && message.fromId.userId != null) {
281
- const out = { payload: { update } };
282
509
  node.send(out);
283
510
  if (debug) {
284
511
  node.log('receiver output: ' + util.inspect(out, { depth: null }));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@patricktobias86/node-red-telegram-account",
3
- "version": "1.1.16",
3
+ "version": "1.1.18",
4
4
  "description": "Node-RED nodes to communicate with GramJS.",
5
5
  "main": "nodes/config.js",
6
6
  "keywords": [
@@ -8,7 +8,7 @@ function load() {
8
8
  addEventHandler(fn, event) { addCalls.push({fn, event}); }
9
9
  removeEventHandler() {}
10
10
  }
11
- class NewMessageStub {}
11
+ class RawStub {}
12
12
 
13
13
  let NodeCtor;
14
14
  const configNode = { client: new TelegramClientStub() };
@@ -26,7 +26,7 @@ function load() {
26
26
  };
27
27
 
28
28
  proxyquire('../nodes/receiver.js', {
29
- 'telegram/events': { NewMessage: NewMessageStub }
29
+ 'telegram/events': { Raw: RawStub }
30
30
  })(RED);
31
31
 
32
32
  return { NodeCtor, addCalls, logs };
@@ -37,8 +37,11 @@ describe('Receiver node debug with BigInt', function() {
37
37
  const { NodeCtor, addCalls, logs } = load();
38
38
  const node = new NodeCtor({config:'c', ignore:'', debug:true});
39
39
  const handler = addCalls[0].fn;
40
- assert.doesNotThrow(() => handler({ message: { fromId:{userId:1n} } }));
41
- assert(logs.some(l => l.includes('receiver update')));
40
+ assert.doesNotThrow(() => handler({
41
+ className: 'UpdateNewMessage',
42
+ message: { fromId:{userId:1n}, peerId:{userId:1n}, message:'hi' }
43
+ }));
44
+ assert(logs.some(l => l.includes('receiver raw update')));
42
45
  assert(logs.some(l => l.includes('receiver output')));
43
46
  });
44
47
  });
@@ -8,7 +8,7 @@ function load() {
8
8
  addEventHandler(fn, event) { addCalls.push({fn, event}); }
9
9
  removeEventHandler(fn, event) { removeCalls.push({fn, event}); }
10
10
  }
11
- class NewMessageStub {}
11
+ class RawStub {}
12
12
 
13
13
  let NodeCtor;
14
14
  const configNode = { client: new TelegramClientStub() };
@@ -24,7 +24,7 @@ function load() {
24
24
  };
25
25
 
26
26
  proxyquire('../nodes/receiver.js', {
27
- 'telegram/events': { NewMessage: NewMessageStub }
27
+ 'telegram/events': { Raw: RawStub }
28
28
  })(RED);
29
29
 
30
30
  return { NodeCtor, addCalls, removeCalls };
@@ -48,7 +48,10 @@ describe('Receiver node', function() {
48
48
  node.send = (msg) => sent.push(msg);
49
49
  const handler = addCalls[0].fn;
50
50
 
51
- handler({ message: { fromId: { userId: 123 }, media: { document: { size: 6 * 1024 * 1024 } } } });
51
+ handler({
52
+ className: 'UpdateNewMessage',
53
+ message: { fromId: { userId: 123 }, peerId:{ userId: 123 }, media: { document: { size: 6 * 1024 * 1024 } } }
54
+ });
52
55
 
53
56
  assert.strictEqual(sent.length, 0);
54
57
  });
@@ -60,7 +63,10 @@ describe('Receiver node', function() {
60
63
  node.send = (msg) => sent.push(msg);
61
64
  const handler = addCalls[0].fn;
62
65
 
63
- handler({ message: { fromId: { userId: 123 }, media: { document: { size: 3 * 1024 * 1024 } } } });
66
+ handler({
67
+ className: 'UpdateNewMessage',
68
+ message: { fromId: { userId: 123 }, peerId:{ userId: 123 }, media: { document: { size: 3 * 1024 * 1024 } } }
69
+ });
64
70
 
65
71
  assert.strictEqual(sent.length, 1);
66
72
  });
@@ -72,7 +78,10 @@ describe('Receiver node', function() {
72
78
  node.send = (msg) => sent.push(msg);
73
79
  const handler = addCalls[0].fn;
74
80
 
75
- handler({ message: { fromId: { userId: 123 }, media: { document: { mimeType: 'video/mp4', attributes: [{ className: 'DocumentAttributeVideo' }] } } } });
81
+ handler({
82
+ className: 'UpdateNewMessage',
83
+ message: { fromId: { userId: 123 }, peerId:{ userId: 123 }, media: { document: { mimeType: 'video/mp4', attributes: [{ className: 'DocumentAttributeVideo' }] } } }
84
+ });
76
85
 
77
86
  assert.strictEqual(sent.length, 0);
78
87
  });
@@ -84,7 +93,10 @@ describe('Receiver node', function() {
84
93
  node.send = (msg) => sent.push(msg);
85
94
  const handler = addCalls[0].fn;
86
95
 
87
- handler({ message: { fromId: { userId: 123 }, media: { document: { mimeType: 'video/mp4', attributes: [{ className: 'DocumentAttributeVideo' }] } } } });
96
+ handler({
97
+ className: 'UpdateNewMessage',
98
+ message: { fromId: { userId: 123 }, peerId:{ userId: 123 }, media: { document: { mimeType: 'video/mp4', attributes: [{ className: 'DocumentAttributeVideo' }] } } }
99
+ });
88
100
 
89
101
  assert.strictEqual(sent.length, 1);
90
102
  });