@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.
- package/.github/dependabot.yml +11 -0
- package/CHANGELOG.md +8 -0
- package/README.md +1 -1
- package/docs/NODES.md +1 -1
- package/nodes/get-entity.js +19 -5
- package/nodes/receiver.html +17 -2
- package/nodes/receiver.js +78 -9
- package/package.json +1 -1
- package/test/receiver.test.js +100 -0
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,
|
|
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),
|
|
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. |
|
package/nodes/get-entity.js
CHANGED
|
@@ -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
|
-
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
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
|
|
package/nodes/receiver.html
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
460
|
-
|
|
461
|
-
|
|
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: ${
|
|
485
|
-
debugSend({ event: 'ignored', reason: 'missing-message', updateClassName
|
|
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
package/test/receiver.test.js
CHANGED
|
@@ -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 = [];
|