@patricktobias86/node-red-telegram-account 1.2.0 → 1.2.2

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.
@@ -0,0 +1,11 @@
1
+ ---
2
+ version: 2
3
+ updates:
4
+ - package-ecosystem: "github-actions"
5
+ directory: "/"
6
+ schedule:
7
+ interval: "weekly"
8
+ - package-ecosystem: npm
9
+ directory: "/"
10
+ schedule:
11
+ interval: "weekly"
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.2.1] - 2026-01-22
6
+ ### Added
7
+ - Receiver node option to disable emitting edited message updates (useful to prevent duplicate outputs when counters/markup change on channel posts).
8
+
9
+ ## [1.2.2] - 2026-01-31
10
+ ### Fixed
11
+ - Receiver node chat/sender filters now work (include/exclude chats and senders).
12
+
5
13
  ## [1.2.0] - 2026-01-12
6
14
  ### Changed
7
15
  - Switched underlying Telegram MTProto client dependency from `telegram` (GramJS) to `teleproto`.
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 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.
21
+ - **receiver** – emits messages for every incoming Telegram message using Raw MTProto updates (with optional ignore list, message type filter, media size limit, and optional edit filtering), 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 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). When Debug is enabled, a second output emits debug messages that can be wired to a Node-RED debug node. 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), optionally drop media above a configurable size, and optionally ignore edited message updates to prevent duplicate outputs when counters/markup change. 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). When Debug is enabled, a second output emits debug messages that can be wired to a Node-RED debug node. 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. |
@@ -17,11 +17,25 @@ module.exports = function (RED) {
17
17
  try {
18
18
  let entity;
19
19
 
20
- // Check if the input is a URL
21
- if (input.includes('https://t.me/')) {
22
- const username = input.split('/').pop();
23
- entity = await client.getEntity(username);
24
- } else {
20
+ // Check if the input is a Telegram URL and extract the username safely
21
+ try {
22
+ const url = new URL(input);
23
+ const hostname = url.hostname.toLowerCase();
24
+
25
+ if (hostname === 't.me') {
26
+ const segments = url.pathname.split('/').filter(Boolean);
27
+ const username = segments[segments.length - 1];
28
+
29
+ if (username) {
30
+ entity = await client.getEntity(username);
31
+ } else {
32
+ entity = await client.getEntity(input);
33
+ }
34
+ } else {
35
+ entity = await client.getEntity(input);
36
+ }
37
+ } catch (e) {
38
+ // Not a valid URL, treat input as a plain identifier
25
39
  entity = await client.getEntity(input);
26
40
  }
27
41
 
@@ -9,6 +9,7 @@
9
9
  ignore: { value:""},
10
10
  ignoreMessageTypes: { value: "" },
11
11
  debug: { value: false },
12
+ emitEdits: { value: true },
12
13
  maxFileSizeMb: { value: "" },
13
14
  includeChats: { value: "" },
14
15
  excludeChats: { value: "" },
@@ -53,6 +54,13 @@
53
54
  </label>
54
55
  <input type="checkbox" id="node-input-debug">
55
56
  </div>
57
+ <div class="form-row">
58
+ <label for="node-input-emitEdits">
59
+ <i class="fa fa-pencil"></i> Emit edits
60
+ </label>
61
+ <input type="checkbox" id="node-input-emitEdits">
62
+ <p class="form-tips">When disabled, edited message updates (e.g. like/view counters changing on channel posts) are ignored.</p>
63
+ </div>
56
64
  <div class="form-row">
57
65
  <label for="node-input-ignore">
58
66
  <i class="fa fa-user-times"></i> Ignore list
@@ -178,6 +186,11 @@
178
186
  </dt>
179
187
  <dd>List of message or media types to skip (for example <code>video</code>, <code>document</code>, or <code>sticker</code>). Separate multiple entries with commas or new lines.</dd>
180
188
 
189
+ <dt>Emit edits
190
+ <span class="property-type">boolean</span>
191
+ </dt>
192
+ <dd>When disabled, the receiver ignores edited message updates (such as <code>UpdateEditChannelMessage</code>) to avoid duplicate outputs for the same post.</dd>
193
+
181
194
  <dt>Max media size (MB)
182
195
  <span class="property-type">number</span>
183
196
  </dt>
@@ -205,7 +218,7 @@
205
218
  </dl>
206
219
 
207
220
  <h3>Details</h3>
208
- <p>The <b>receiver</b> node uses the Telegram client to listen for all new messages in real-time. It emits a message to the next connected Node-RED node whenever a new Telegram message is received, provided the sender's user ID is not in the ignore list.</p>
221
+ <p>The <b>receiver</b> node uses the Telegram client to listen for all new messages in real-time. It emits a message to the next connected Node-RED node whenever a new Telegram message is received, provided it passes the configured filters.</p>
209
222
 
210
223
  <h3>Example</h3>
211
224
  <pre>
@@ -243,7 +256,9 @@
243
256
  <h3>Notes</h3>
244
257
  <ul>
245
258
  <li>Ensure the Telegram bot has sufficient permissions to receive messages in the configured chat or channel.</li>
246
- <li>The <b>Ignore List</b> only filters messages based on the sender's user ID.</li>
259
+ <li>The <b>Ignore List</b> only filters messages where the sender can be identified as a user.</li>
260
+ <li><b>Include/Exclude chats</b> match against <code>payload.chatId</code>.</li>
261
+ <li><b>Include/Exclude senders</b> match against <code>payload.senderId</code> (user/chat/channel IDs depending on the update).</li>
247
262
  <li>Use <b>Ignore message types</b> to drop updates that contain media you don't want to process, such as large videos or documents.</li>
248
263
  <li>For advanced filtering based on message content, consider chaining this node with additional processing nodes in Node-RED.</li>
249
264
  </ul>
package/nodes/receiver.js CHANGED
@@ -366,6 +366,7 @@ module.exports = function (RED) {
366
366
  const client = this.config.client;
367
367
  const ignore = splitList(config.ignore || "");
368
368
  const ignoredMessageTypes = toLowerCaseSet(splitList(config.ignoreMessageTypes || ""));
369
+ const emitEdits = config.emitEdits !== false;
369
370
  const maxFileSizeMb = Number(config.maxFileSizeMb);
370
371
  const maxFileSizeBytes = Number.isFinite(maxFileSizeMb) && maxFileSizeMb > 0
371
372
  ? maxFileSizeMb * 1024 * 1024
@@ -376,6 +377,26 @@ module.exports = function (RED) {
376
377
  const includeSenders = splitList(config.includeSenders || "");
377
378
  const excludeSenders = splitList(config.excludeSenders || "");
378
379
 
380
+ const toNormalizedIdSet = (values) => {
381
+ const result = new Set();
382
+ for (const value of values) {
383
+ if (value == null) {
384
+ continue;
385
+ }
386
+ const normalized = String(value).trim();
387
+ if (!normalized) {
388
+ continue;
389
+ }
390
+ result.add(normalized);
391
+ }
392
+ return result;
393
+ };
394
+
395
+ const includeChatSet = toNormalizedIdSet(includeChats);
396
+ const excludeChatSet = toNormalizedIdSet(excludeChats);
397
+ const includeSenderSet = toNormalizedIdSet(includeSenders);
398
+ const excludeSenderSet = toNormalizedIdSet(excludeSenders);
399
+
379
400
  const extractPhotoSize = (photo) => {
380
401
  if (!photo || !Array.isArray(photo.sizes)) {
381
402
  return null;
@@ -456,13 +477,9 @@ module.exports = function (RED) {
456
477
  node.send([null, { payload }]);
457
478
  };
458
479
 
459
- let rawOptions = {};
460
- if (includeChats.length > 0) rawOptions.chats = includeChats;
461
- if (excludeChats.length > 0) rawOptions.blacklistChats = excludeChats;
462
- if (includeSenders.length > 0) rawOptions.senders = includeSenders;
463
- if (excludeSenders.length > 0) rawOptions.blacklistSenders = excludeSenders;
464
-
465
- const event = new Raw(rawOptions);
480
+ // teleproto's Raw event builder only supports `types` and `func` options.
481
+ // Chat/sender filtering is implemented below after we normalize the message metadata.
482
+ const event = new Raw({});
466
483
  const handler = (rawUpdate) => {
467
484
  const debug = node.debugEnabled;
468
485
  if (debug) {
@@ -480,9 +497,23 @@ module.exports = function (RED) {
480
497
  }
481
498
 
482
499
  for (const { update, message } of extracted) {
500
+ const updateClassName = getClassName(update) || 'unknown';
501
+
502
+ if (!emitEdits) {
503
+ const isEditUpdate =
504
+ updateClassName === 'UpdateEditMessage' ||
505
+ updateClassName === 'UpdateEditChannelMessage' ||
506
+ updateClassName === 'UpdateBotEditBusinessMessage';
507
+ if (isEditUpdate) {
508
+ debugLog(`receiver ignoring edit update due to emitEdits=false; updateClassName=${updateClassName}`);
509
+ debugSend({ event: 'ignored', reason: 'emitEdits', updateClassName });
510
+ continue;
511
+ }
512
+ }
513
+
483
514
  if (!message) {
484
- debugLog(`receiver ignoring message update without message payload: ${getClassName(update) || 'unknown'}`);
485
- debugSend({ event: 'ignored', reason: 'missing-message', updateClassName: getClassName(update) || 'unknown' });
515
+ debugLog(`receiver ignoring message update without message payload: ${updateClassName}`);
516
+ debugSend({ event: 'ignored', reason: 'missing-message', updateClassName });
486
517
  continue;
487
518
  }
488
519
 
@@ -508,6 +539,44 @@ module.exports = function (RED) {
508
539
  null;
509
540
 
510
541
  const chatId = peerChatId(peer);
542
+ const chatIdString = chatId != null ? String(chatId) : null;
543
+ const senderIdString = senderId != null ? String(senderId) : null;
544
+
545
+ if (includeChatSet.size > 0) {
546
+ const allowed = chatIdString != null && includeChatSet.has(chatIdString);
547
+ if (!allowed) {
548
+ debugLog(`receiver ignoring message due to includeChats; chatId=${chatIdString ?? 'unknown'}`);
549
+ debugSend({ event: 'ignored', reason: 'includeChats', chatId });
550
+ continue;
551
+ }
552
+ }
553
+
554
+ if (excludeChatSet.size > 0) {
555
+ const blocked = chatIdString != null && excludeChatSet.has(chatIdString);
556
+ if (blocked) {
557
+ debugLog(`receiver ignoring message due to excludeChats; chatId=${chatIdString}`);
558
+ debugSend({ event: 'ignored', reason: 'excludeChats', chatId });
559
+ continue;
560
+ }
561
+ }
562
+
563
+ if (includeSenderSet.size > 0) {
564
+ const allowed = senderIdString != null && includeSenderSet.has(senderIdString);
565
+ if (!allowed) {
566
+ debugLog(`receiver ignoring message due to includeSenders; senderId=${senderIdString ?? 'unknown'} senderType=${senderType}`);
567
+ debugSend({ event: 'ignored', reason: 'includeSenders', senderId, senderType });
568
+ continue;
569
+ }
570
+ }
571
+
572
+ if (excludeSenderSet.size > 0) {
573
+ const blocked = senderIdString != null && excludeSenderSet.has(senderIdString);
574
+ if (blocked) {
575
+ debugLog(`receiver ignoring message due to excludeSenders; senderId=${senderIdString} senderType=${senderType}`);
576
+ debugSend({ event: 'ignored', reason: 'excludeSenders', senderId, senderType });
577
+ continue;
578
+ }
579
+ }
511
580
 
512
581
  if (ignoredMessageTypes.size > 0) {
513
582
  const shouldIgnoreType = Array.from(messageTypes).some((type) => ignoredMessageTypes.has(type));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@patricktobias86/node-red-telegram-account",
3
- "version": "1.2.0",
3
+ "version": "1.2.2",
4
4
  "description": "Node-RED nodes to communicate with TeleProto.",
5
5
  "main": "nodes/config.js",
6
6
  "keywords": [
@@ -101,6 +101,87 @@ describe('Receiver node', function() {
101
101
  assert.strictEqual(sent.length, 1);
102
102
  });
103
103
 
104
+ it('filters by includeChats', function() {
105
+ const { NodeCtor, addCalls } = load();
106
+ const sent = [];
107
+ const node = new NodeCtor({config:'c', ignore:'', ignoreMessageTypes:'', maxFileSizeMb:'', includeChats:'111'});
108
+ node.send = (msg) => sent.push(msg);
109
+ const handler = addCalls[0].fn;
110
+
111
+ handler({
112
+ className: 'UpdateNewMessage',
113
+ message: { fromId: { userId: 123 }, peerId:{ userId: 111 }, message: 'allowed' }
114
+ });
115
+ handler({
116
+ className: 'UpdateNewMessage',
117
+ message: { fromId: { userId: 123 }, peerId:{ userId: 222 }, message: 'blocked' }
118
+ });
119
+
120
+ assert.strictEqual(sent.length, 1);
121
+ assert.strictEqual(sent[0].payload.chatId, 111);
122
+ assert.strictEqual(sent[0].payload.message.message, 'allowed');
123
+ });
124
+
125
+ it('filters by excludeChats', function() {
126
+ const { NodeCtor, addCalls } = load();
127
+ const sent = [];
128
+ const node = new NodeCtor({config:'c', ignore:'', ignoreMessageTypes:'', maxFileSizeMb:'', excludeChats:'222'});
129
+ node.send = (msg) => sent.push(msg);
130
+ const handler = addCalls[0].fn;
131
+
132
+ handler({
133
+ className: 'UpdateNewMessage',
134
+ message: { fromId: { userId: 123 }, peerId:{ userId: 111 }, message: 'allowed' }
135
+ });
136
+ handler({
137
+ className: 'UpdateNewMessage',
138
+ message: { fromId: { userId: 123 }, peerId:{ userId: 222 }, message: 'blocked' }
139
+ });
140
+
141
+ assert.strictEqual(sent.length, 1);
142
+ assert.strictEqual(sent[0].payload.chatId, 111);
143
+ });
144
+
145
+ it('filters by includeSenders', function() {
146
+ const { NodeCtor, addCalls } = load();
147
+ const sent = [];
148
+ const node = new NodeCtor({config:'c', ignore:'', ignoreMessageTypes:'', maxFileSizeMb:'', includeSenders:'123'});
149
+ node.send = (msg) => sent.push(msg);
150
+ const handler = addCalls[0].fn;
151
+
152
+ handler({
153
+ className: 'UpdateNewMessage',
154
+ message: { fromId: { userId: 123 }, peerId:{ userId: 111 }, message: 'allowed' }
155
+ });
156
+ handler({
157
+ className: 'UpdateNewMessage',
158
+ message: { fromId: { userId: 456 }, peerId:{ userId: 111 }, message: 'blocked' }
159
+ });
160
+
161
+ assert.strictEqual(sent.length, 1);
162
+ assert.strictEqual(sent[0].payload.senderId, 123);
163
+ });
164
+
165
+ it('filters by excludeSenders', function() {
166
+ const { NodeCtor, addCalls } = load();
167
+ const sent = [];
168
+ const node = new NodeCtor({config:'c', ignore:'', ignoreMessageTypes:'', maxFileSizeMb:'', excludeSenders:'456'});
169
+ node.send = (msg) => sent.push(msg);
170
+ const handler = addCalls[0].fn;
171
+
172
+ handler({
173
+ className: 'UpdateNewMessage',
174
+ message: { fromId: { userId: 123 }, peerId:{ userId: 111 }, message: 'allowed' }
175
+ });
176
+ handler({
177
+ className: 'UpdateNewMessage',
178
+ message: { fromId: { userId: 456 }, peerId:{ userId: 111 }, message: 'blocked' }
179
+ });
180
+
181
+ assert.strictEqual(sent.length, 1);
182
+ assert.strictEqual(sent[0].payload.senderId, 123);
183
+ });
184
+
104
185
  it('populates chatId and senderId for string ids', function() {
105
186
  const { NodeCtor, addCalls } = load();
106
187
  const sent = [];
@@ -207,6 +288,25 @@ describe('Receiver node', function() {
207
288
  assert.strictEqual(sent[0].payload.message.message, 'edited business message');
208
289
  });
209
290
 
291
+ it('can ignore edited message updates when emitEdits is disabled', function() {
292
+ const { NodeCtor, addCalls } = load();
293
+ const sent = [];
294
+ const node = new NodeCtor({config:'c', ignore:'', ignoreMessageTypes:'', maxFileSizeMb:'', emitEdits:false});
295
+ node.send = (msg) => sent.push(msg);
296
+ const handler = addCalls[0].fn;
297
+
298
+ handler({
299
+ className: 'UpdateEditChannelMessage',
300
+ message: {
301
+ id: 171429,
302
+ peerId: { channelId: '1050289360', className: 'PeerChannel' },
303
+ message: 'same content'
304
+ }
305
+ });
306
+
307
+ assert.strictEqual(sent.length, 0);
308
+ });
309
+
210
310
  it('handles UpdateQuickReplyMessage updates', function() {
211
311
  const { NodeCtor, addCalls } = load();
212
312
  const sent = [];