@patricktobias86/node-red-telegram-account 1.1.15 → 1.1.17
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 +10 -0
- package/README.md +1 -1
- package/docs/NODES.md +1 -1
- package/nodes/receiver.html +18 -0
- package/nodes/receiver.js +411 -39
- package/package.json +1 -1
- package/test/receiver-debug.test.js +7 -4
- package/test/receiver.test.js +42 -6
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [1.1.17] - 2026-01-05
|
|
6
|
+
### Fixed
|
|
7
|
+
- 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.
|
|
8
|
+
|
|
9
|
+
## [1.1.16] - 2025-09-22
|
|
10
|
+
### Added
|
|
11
|
+
- Receiver node option to ignore configurable message types (such as videos or documents) to prevent oversized uploads.
|
|
12
|
+
### Changed
|
|
13
|
+
- Receiver node collects detailed media type metadata to power the new filter while keeping debug logging informative.
|
|
14
|
+
|
|
5
15
|
## [1.1.15] - 2025-09-21
|
|
6
16
|
### Added
|
|
7
17
|
- Receiver node option to drop updates when media exceeds a configurable size threshold, preventing large downloads.
|
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
|
|
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 and optionally
|
|
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`. 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/receiver.html
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
name: { value: '' },
|
|
8
8
|
config: { type: 'config', required: false },
|
|
9
9
|
ignore: { value:""},
|
|
10
|
+
ignoreMessageTypes: { value: "" },
|
|
10
11
|
debug: { value: false },
|
|
11
12
|
maxFileSizeMb: { value: "" }
|
|
12
13
|
},
|
|
@@ -59,6 +60,17 @@
|
|
|
59
60
|
style="width: 60%"
|
|
60
61
|
></textarea>
|
|
61
62
|
</div>
|
|
63
|
+
<div class="form-row">
|
|
64
|
+
<label for="node-input-ignoreMessageTypes">
|
|
65
|
+
<i class="fa fa-ban"></i> Ignore message types
|
|
66
|
+
</label>
|
|
67
|
+
<textarea
|
|
68
|
+
id="node-input-ignoreMessageTypes"
|
|
69
|
+
placeholder="e.g. video, document, sticker"
|
|
70
|
+
style="width: 60%"
|
|
71
|
+
></textarea>
|
|
72
|
+
<p class="form-tips">Separate types with commas or new lines.</p>
|
|
73
|
+
</div>
|
|
62
74
|
<div class="form-row">
|
|
63
75
|
<label for="node-input-maxFileSizeMb">
|
|
64
76
|
<i class="fa fa-download"></i> Max media size (MB)
|
|
@@ -101,6 +113,11 @@
|
|
|
101
113
|
</dt>
|
|
102
114
|
<dd>A newline-separated list of user IDs to ignore. Messages from these users will not trigger the output.</dd>
|
|
103
115
|
|
|
116
|
+
<dt>Ignore message types
|
|
117
|
+
<span class="property-type">string</span>
|
|
118
|
+
</dt>
|
|
119
|
+
<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>
|
|
120
|
+
|
|
104
121
|
<dt>Max media size (MB)
|
|
105
122
|
<span class="property-type">number</span>
|
|
106
123
|
</dt>
|
|
@@ -147,6 +164,7 @@
|
|
|
147
164
|
<ul>
|
|
148
165
|
<li>Ensure the Telegram bot has sufficient permissions to receive messages in the configured chat or channel.</li>
|
|
149
166
|
<li>The <b>Ignore List</b> only filters messages based on the sender's user ID.</li>
|
|
167
|
+
<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>
|
|
150
168
|
<li>For advanced filtering based on message content, consider chaining this node with additional processing nodes in Node-RED.</li>
|
|
151
169
|
</ul>
|
|
152
170
|
</script>
|
package/nodes/receiver.js
CHANGED
|
@@ -1,6 +1,331 @@
|
|
|
1
|
-
const {
|
|
1
|
+
const { Raw } = require("telegram/events");
|
|
2
2
|
const util = require("util");
|
|
3
3
|
|
|
4
|
+
const splitList = (value) => {
|
|
5
|
+
if (typeof value !== 'string') {
|
|
6
|
+
return [];
|
|
7
|
+
}
|
|
8
|
+
return value
|
|
9
|
+
.split(/[\n,\r]/)
|
|
10
|
+
.map((entry) => entry.trim())
|
|
11
|
+
.filter(Boolean);
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const toLowerCaseSet = (values) => {
|
|
15
|
+
const result = new Set();
|
|
16
|
+
for (const value of values) {
|
|
17
|
+
result.add(value.toLowerCase());
|
|
18
|
+
}
|
|
19
|
+
return result;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const addType = (target, value) => {
|
|
23
|
+
if (!value) {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
target.add(String(value).toLowerCase());
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const collectDocumentTypes = (document, types) => {
|
|
30
|
+
if (!document) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
addType(types, 'document');
|
|
35
|
+
|
|
36
|
+
if (Array.isArray(document.attributes)) {
|
|
37
|
+
for (const attribute of document.attributes) {
|
|
38
|
+
if (!attribute) {
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
const attributeName = attribute.className || attribute._;
|
|
42
|
+
addType(types, attributeName);
|
|
43
|
+
switch (attributeName) {
|
|
44
|
+
case 'DocumentAttributeVideo':
|
|
45
|
+
addType(types, 'video');
|
|
46
|
+
break;
|
|
47
|
+
case 'DocumentAttributeAudio':
|
|
48
|
+
addType(types, 'audio');
|
|
49
|
+
if (attribute.voice) {
|
|
50
|
+
addType(types, 'voice');
|
|
51
|
+
}
|
|
52
|
+
break;
|
|
53
|
+
case 'DocumentAttributeAnimated':
|
|
54
|
+
addType(types, 'animation');
|
|
55
|
+
break;
|
|
56
|
+
case 'DocumentAttributeSticker':
|
|
57
|
+
addType(types, 'sticker');
|
|
58
|
+
break;
|
|
59
|
+
default:
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (typeof document.mimeType === 'string') {
|
|
66
|
+
const mimeType = document.mimeType.toLowerCase();
|
|
67
|
+
addType(types, mimeType);
|
|
68
|
+
const slashIndex = mimeType.indexOf('/');
|
|
69
|
+
if (slashIndex > 0) {
|
|
70
|
+
addType(types, mimeType.slice(0, slashIndex));
|
|
71
|
+
}
|
|
72
|
+
if (mimeType.startsWith('video/')) {
|
|
73
|
+
addType(types, 'video');
|
|
74
|
+
} else if (mimeType.startsWith('audio/')) {
|
|
75
|
+
addType(types, 'audio');
|
|
76
|
+
} else if (mimeType.startsWith('image/')) {
|
|
77
|
+
addType(types, 'image');
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const collectMediaTypes = (media, types) => {
|
|
83
|
+
if (!media) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
addType(types, 'media');
|
|
87
|
+
addType(types, media.className || media._);
|
|
88
|
+
|
|
89
|
+
if (media.document) {
|
|
90
|
+
collectDocumentTypes(media.document, types);
|
|
91
|
+
}
|
|
92
|
+
if (media.photo) {
|
|
93
|
+
addType(types, 'photo');
|
|
94
|
+
}
|
|
95
|
+
if (media.webpage) {
|
|
96
|
+
addType(types, 'webpage');
|
|
97
|
+
if (media.webpage.document) {
|
|
98
|
+
collectDocumentTypes(media.webpage.document, types);
|
|
99
|
+
}
|
|
100
|
+
if (media.webpage.photo) {
|
|
101
|
+
addType(types, 'photo');
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
if (media.poll) {
|
|
105
|
+
addType(types, 'poll');
|
|
106
|
+
}
|
|
107
|
+
if (media.contact) {
|
|
108
|
+
addType(types, 'contact');
|
|
109
|
+
}
|
|
110
|
+
if (media.geo || media.geoPoint) {
|
|
111
|
+
addType(types, 'location');
|
|
112
|
+
}
|
|
113
|
+
if (media.venue) {
|
|
114
|
+
addType(types, 'venue');
|
|
115
|
+
}
|
|
116
|
+
if (media.game) {
|
|
117
|
+
addType(types, 'game');
|
|
118
|
+
}
|
|
119
|
+
if (media.sticker) {
|
|
120
|
+
addType(types, 'sticker');
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const collectMessageTypes = (message) => {
|
|
125
|
+
const types = new Set();
|
|
126
|
+
if (!message || typeof message !== 'object') {
|
|
127
|
+
return types;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
collectMediaTypes(message.media, types);
|
|
131
|
+
|
|
132
|
+
if (typeof message.message === 'string' && message.message.length > 0 && !message.media) {
|
|
133
|
+
addType(types, 'text');
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (message.action) {
|
|
137
|
+
const actionName = message.action.className || message.action._;
|
|
138
|
+
addType(types, actionName);
|
|
139
|
+
if (actionName) {
|
|
140
|
+
addType(types, 'service');
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (message.ttlPeriod) {
|
|
145
|
+
addType(types, 'self-destructing');
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return types;
|
|
149
|
+
};
|
|
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 === 'bigint') {
|
|
192
|
+
const result = Number(value);
|
|
193
|
+
return Number.isFinite(result) ? result : Number.MAX_SAFE_INTEGER;
|
|
194
|
+
}
|
|
195
|
+
return null;
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
const peerChatId = (peerInfo) => {
|
|
199
|
+
if (!peerInfo) {
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
202
|
+
// For Node-RED flows "chatId" typically means "the conversation identifier".
|
|
203
|
+
// For private chats that's the userId; for groups it's chatId; for channels it's channelId.
|
|
204
|
+
if (peerInfo.type === 'user') {
|
|
205
|
+
return toSafeNumber(peerInfo.userId);
|
|
206
|
+
}
|
|
207
|
+
if (peerInfo.type === 'chat') {
|
|
208
|
+
return toSafeNumber(peerInfo.chatId);
|
|
209
|
+
}
|
|
210
|
+
if (peerInfo.type === 'channel') {
|
|
211
|
+
return toSafeNumber(peerInfo.channelId);
|
|
212
|
+
}
|
|
213
|
+
return null;
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
const buildPeerUser = (userId) => ({ className: 'PeerUser', userId });
|
|
217
|
+
const buildPeerChat = (chatId) => ({ className: 'PeerChat', chatId });
|
|
218
|
+
|
|
219
|
+
const toDerivedMessage = (update) => {
|
|
220
|
+
const className = getClassName(update);
|
|
221
|
+
if (className === 'UpdateShortMessage') {
|
|
222
|
+
const userId = update.userId ?? update.user_id;
|
|
223
|
+
return {
|
|
224
|
+
className: 'Message',
|
|
225
|
+
id: update.id,
|
|
226
|
+
date: update.date,
|
|
227
|
+
out: update.out,
|
|
228
|
+
silent: update.silent,
|
|
229
|
+
// In UpdateShortMessage, userId is the peer and also the sender for incoming messages.
|
|
230
|
+
peerId: userId != null ? buildPeerUser(userId) : undefined,
|
|
231
|
+
fromId: userId != null ? buildPeerUser(userId) : undefined,
|
|
232
|
+
message: update.message,
|
|
233
|
+
entities: update.entities,
|
|
234
|
+
fwdFrom: update.fwdFrom,
|
|
235
|
+
viaBotId: update.viaBotId,
|
|
236
|
+
replyTo: update.replyTo,
|
|
237
|
+
ttlPeriod: update.ttlPeriod,
|
|
238
|
+
media: update.media
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (className === 'UpdateShortChatMessage') {
|
|
243
|
+
const chatId = update.chatId ?? update.chat_id;
|
|
244
|
+
const fromId = update.fromId ?? update.from_id;
|
|
245
|
+
return {
|
|
246
|
+
className: 'Message',
|
|
247
|
+
id: update.id,
|
|
248
|
+
date: update.date,
|
|
249
|
+
out: update.out,
|
|
250
|
+
silent: update.silent,
|
|
251
|
+
peerId: chatId != null ? buildPeerChat(chatId) : undefined,
|
|
252
|
+
fromId: fromId != null ? buildPeerUser(fromId) : undefined,
|
|
253
|
+
message: update.message,
|
|
254
|
+
entities: update.entities,
|
|
255
|
+
fwdFrom: update.fwdFrom,
|
|
256
|
+
viaBotId: update.viaBotId,
|
|
257
|
+
replyTo: update.replyTo,
|
|
258
|
+
ttlPeriod: update.ttlPeriod,
|
|
259
|
+
media: update.media
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (className === 'UpdateShortSentMessage') {
|
|
264
|
+
// This update doesn't include a peer; it's still a real MTProto message update.
|
|
265
|
+
// We emit it with a best-effort message shape so flows can still react.
|
|
266
|
+
return {
|
|
267
|
+
className: 'Message',
|
|
268
|
+
id: update.id,
|
|
269
|
+
date: update.date,
|
|
270
|
+
out: true,
|
|
271
|
+
silent: update.silent,
|
|
272
|
+
message: update.message,
|
|
273
|
+
entities: update.entities,
|
|
274
|
+
media: update.media
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return null;
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
const extractMessageEvents = (rawUpdate) => {
|
|
282
|
+
// Raw MTProto updates can be nested (Updates / UpdatesCombined / UpdateShort).
|
|
283
|
+
// We must unwrap them here instead of relying on NewMessage(), which can miss
|
|
284
|
+
// valid message updates (e.g. channel posts, edits, anonymous admins).
|
|
285
|
+
const results = [];
|
|
286
|
+
|
|
287
|
+
const walk = (update) => {
|
|
288
|
+
if (!update || typeof update !== 'object') {
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const className = getClassName(update);
|
|
293
|
+
|
|
294
|
+
if (Array.isArray(update.updates)) {
|
|
295
|
+
for (const nested of update.updates) {
|
|
296
|
+
walk(nested);
|
|
297
|
+
}
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (update.update) {
|
|
302
|
+
walk(update.update);
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (className === 'UpdateNewMessage' ||
|
|
307
|
+
className === 'UpdateNewChannelMessage' ||
|
|
308
|
+
className === 'UpdateEditMessage' ||
|
|
309
|
+
className === 'UpdateEditChannelMessage') {
|
|
310
|
+
if (update.message) {
|
|
311
|
+
results.push({ update, message: update.message });
|
|
312
|
+
} else {
|
|
313
|
+
results.push({ update, message: null });
|
|
314
|
+
}
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const derived = toDerivedMessage(update);
|
|
319
|
+
if (derived) {
|
|
320
|
+
results.push({ update, message: derived });
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
walk(rawUpdate);
|
|
326
|
+
return results;
|
|
327
|
+
};
|
|
328
|
+
|
|
4
329
|
module.exports = function (RED) {
|
|
5
330
|
function Receiver(config) {
|
|
6
331
|
RED.nodes.createNode(this, config);
|
|
@@ -8,26 +333,13 @@ module.exports = function (RED) {
|
|
|
8
333
|
this.debugEnabled = config.debug;
|
|
9
334
|
var node = this;
|
|
10
335
|
const client = this.config.client;
|
|
11
|
-
const ignore = (config.ignore || "")
|
|
12
|
-
|
|
13
|
-
.map((entry) => entry.trim())
|
|
14
|
-
.filter(Boolean);
|
|
336
|
+
const ignore = splitList(config.ignore || "");
|
|
337
|
+
const ignoredMessageTypes = toLowerCaseSet(splitList(config.ignoreMessageTypes || ""));
|
|
15
338
|
const maxFileSizeMb = Number(config.maxFileSizeMb);
|
|
16
339
|
const maxFileSizeBytes = Number.isFinite(maxFileSizeMb) && maxFileSizeMb > 0
|
|
17
340
|
? maxFileSizeMb * 1024 * 1024
|
|
18
341
|
: null;
|
|
19
342
|
|
|
20
|
-
const toNumber = (value) => {
|
|
21
|
-
if (typeof value === 'number') {
|
|
22
|
-
return value;
|
|
23
|
-
}
|
|
24
|
-
if (typeof value === 'bigint') {
|
|
25
|
-
const result = Number(value);
|
|
26
|
-
return Number.isFinite(result) ? result : Number.MAX_SAFE_INTEGER;
|
|
27
|
-
}
|
|
28
|
-
return undefined;
|
|
29
|
-
};
|
|
30
|
-
|
|
31
343
|
const extractPhotoSize = (photo) => {
|
|
32
344
|
if (!photo || !Array.isArray(photo.sizes)) {
|
|
33
345
|
return null;
|
|
@@ -39,13 +351,13 @@ module.exports = function (RED) {
|
|
|
39
351
|
}
|
|
40
352
|
if (Array.isArray(size.sizes)) {
|
|
41
353
|
for (const nested of size.sizes) {
|
|
42
|
-
const nestedValue =
|
|
354
|
+
const nestedValue = toSafeNumber(nested);
|
|
43
355
|
if (nestedValue != null && nestedValue > max) {
|
|
44
356
|
max = nestedValue;
|
|
45
357
|
}
|
|
46
358
|
}
|
|
47
359
|
}
|
|
48
|
-
const value =
|
|
360
|
+
const value = toSafeNumber(size.size ?? size.length ?? size.bytes);
|
|
49
361
|
if (value != null && value > max) {
|
|
50
362
|
max = value;
|
|
51
363
|
}
|
|
@@ -59,7 +371,7 @@ module.exports = function (RED) {
|
|
|
59
371
|
}
|
|
60
372
|
|
|
61
373
|
if (media.document && media.document.size != null) {
|
|
62
|
-
const value =
|
|
374
|
+
const value = toSafeNumber(media.document.size);
|
|
63
375
|
if (value != null) {
|
|
64
376
|
return value;
|
|
65
377
|
}
|
|
@@ -74,7 +386,7 @@ module.exports = function (RED) {
|
|
|
74
386
|
|
|
75
387
|
if (media.webpage) {
|
|
76
388
|
const { document, photo } = media.webpage;
|
|
77
|
-
const docSize = document &&
|
|
389
|
+
const docSize = document && toSafeNumber(document.size);
|
|
78
390
|
if (docSize != null) {
|
|
79
391
|
return docSize;
|
|
80
392
|
}
|
|
@@ -86,7 +398,7 @@ module.exports = function (RED) {
|
|
|
86
398
|
|
|
87
399
|
const className = media.className || media._;
|
|
88
400
|
if ((className === 'MessageMediaDocument' || className === 'MessageMediaPhoto') && media.size != null) {
|
|
89
|
-
const value =
|
|
401
|
+
const value = toSafeNumber(media.size);
|
|
90
402
|
if (value != null) {
|
|
91
403
|
return value;
|
|
92
404
|
}
|
|
@@ -95,34 +407,94 @@ module.exports = function (RED) {
|
|
|
95
407
|
return null;
|
|
96
408
|
};
|
|
97
409
|
|
|
98
|
-
const
|
|
99
|
-
|
|
410
|
+
const debugLog = (message) => {
|
|
411
|
+
if (node.debugEnabled) {
|
|
412
|
+
node.log(message);
|
|
413
|
+
}
|
|
414
|
+
};
|
|
415
|
+
|
|
416
|
+
const event = new Raw({});
|
|
417
|
+
const handler = (rawUpdate) => {
|
|
100
418
|
const debug = node.debugEnabled;
|
|
101
419
|
if (debug) {
|
|
102
|
-
node.log('receiver update: ' + util.inspect(
|
|
420
|
+
node.log('receiver raw update: ' + util.inspect(rawUpdate, { depth: null }));
|
|
103
421
|
}
|
|
104
|
-
|
|
105
|
-
|
|
422
|
+
|
|
423
|
+
const extracted = extractMessageEvents(rawUpdate);
|
|
424
|
+
if (extracted.length === 0) {
|
|
425
|
+
// Raw emits *all* MTProto updates; many are not message-bearing updates (typing, reads, etc.).
|
|
426
|
+
// We do not output those by default, but we also do not silently hide that they occurred.
|
|
427
|
+
debugLog(`receiver ignoring non-message MTProto update: ${getClassName(rawUpdate) || 'unknown'}`);
|
|
106
428
|
return;
|
|
107
429
|
}
|
|
108
430
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
431
|
+
for (const { update, message } of extracted) {
|
|
432
|
+
if (!message) {
|
|
433
|
+
debugLog(`receiver ignoring message update without message payload: ${getClassName(update) || 'unknown'}`);
|
|
434
|
+
continue;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
const messageTypes = collectMessageTypes(message);
|
|
438
|
+
const isSilent = Boolean(message.silent || update.silent);
|
|
439
|
+
|
|
440
|
+
const peer = toPeerInfo(message.peerId || message.toId);
|
|
441
|
+
|
|
442
|
+
// Do NOT assume message.fromId.userId exists.
|
|
443
|
+
// fromId can be PeerUser / PeerChat / PeerChannel, can be missing (channel posts),
|
|
444
|
+
// and can represent anonymous admins. Prefer fromId when present; otherwise fall back
|
|
445
|
+
// to the chat peer for channel posts to avoid dropping valid messages.
|
|
446
|
+
const sender =
|
|
447
|
+
toPeerInfo(message.fromId) ||
|
|
448
|
+
(message.senderId != null ? toPeerInfo(buildPeerUser(message.senderId)) : null) ||
|
|
449
|
+
(message.post ? peer : null);
|
|
450
|
+
|
|
451
|
+
const senderType = sender ? sender.type : 'unknown';
|
|
452
|
+
const senderId =
|
|
453
|
+
senderType === 'user' ? toSafeNumber(sender.userId) :
|
|
454
|
+
senderType === 'chat' ? toSafeNumber(sender.chatId) :
|
|
455
|
+
senderType === 'channel' ? toSafeNumber(sender.channelId) :
|
|
456
|
+
null;
|
|
457
|
+
|
|
458
|
+
const chatId = peerChatId(peer);
|
|
459
|
+
|
|
460
|
+
if (ignoredMessageTypes.size > 0) {
|
|
461
|
+
const shouldIgnoreType = Array.from(messageTypes).some((type) => ignoredMessageTypes.has(type));
|
|
462
|
+
if (shouldIgnoreType) {
|
|
463
|
+
debugLog(`receiver ignoring message due to ignoreMessageTypes; types=${Array.from(messageTypes).join(', ')}`);
|
|
464
|
+
continue;
|
|
114
465
|
}
|
|
115
|
-
return;
|
|
116
466
|
}
|
|
117
|
-
}
|
|
118
467
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
468
|
+
if (maxFileSizeBytes != null) {
|
|
469
|
+
const mediaSize = extractMediaSize(message.media);
|
|
470
|
+
if (mediaSize != null && mediaSize > maxFileSizeBytes) {
|
|
471
|
+
debugLog(`receiver ignoring message due to maxFileSizeMb; mediaSize=${mediaSize} limitBytes=${maxFileSizeBytes}`);
|
|
472
|
+
continue;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// Preserve existing behavior: ignore list is a list of user IDs.
|
|
477
|
+
// Previously the node assumed message.fromId.userId always existed, which dropped valid updates.
|
|
478
|
+
// Now we only apply the ignore list when we can confidently identify a user sender.
|
|
479
|
+
if (senderType === 'user' && senderId != null && ignore.includes(String(senderId))) {
|
|
480
|
+
debugLog(`receiver ignoring message due to ignore list; userId=${senderId}`);
|
|
481
|
+
continue;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
const out = {
|
|
485
|
+
payload: {
|
|
486
|
+
update,
|
|
487
|
+
message,
|
|
488
|
+
peer,
|
|
489
|
+
sender,
|
|
490
|
+
senderType,
|
|
491
|
+
senderId,
|
|
492
|
+
chatId,
|
|
493
|
+
isSilent,
|
|
494
|
+
messageTypes: Array.from(messageTypes)
|
|
495
|
+
}
|
|
496
|
+
};
|
|
123
497
|
|
|
124
|
-
if (message.fromId != null && message.fromId.userId != null) {
|
|
125
|
-
const out = { payload: { update } };
|
|
126
498
|
node.send(out);
|
|
127
499
|
if (debug) {
|
|
128
500
|
node.log('receiver output: ' + util.inspect(out, { depth: null }));
|
package/package.json
CHANGED
|
@@ -8,7 +8,7 @@ function load() {
|
|
|
8
8
|
addEventHandler(fn, event) { addCalls.push({fn, event}); }
|
|
9
9
|
removeEventHandler() {}
|
|
10
10
|
}
|
|
11
|
-
class
|
|
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': {
|
|
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({
|
|
41
|
-
|
|
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
|
});
|
package/test/receiver.test.js
CHANGED
|
@@ -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
|
|
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': {
|
|
27
|
+
'telegram/events': { Raw: RawStub }
|
|
28
28
|
})(RED);
|
|
29
29
|
|
|
30
30
|
return { NodeCtor, addCalls, removeCalls };
|
|
@@ -44,11 +44,14 @@ describe('Receiver node', function() {
|
|
|
44
44
|
it('skips media updates when size exceeds threshold', function() {
|
|
45
45
|
const { NodeCtor, addCalls } = load();
|
|
46
46
|
const sent = [];
|
|
47
|
-
const node = new NodeCtor({config:'c', ignore:'', maxFileSizeMb:'5'});
|
|
47
|
+
const node = new NodeCtor({config:'c', ignore:'', ignoreMessageTypes:'', maxFileSizeMb:'5'});
|
|
48
48
|
node.send = (msg) => sent.push(msg);
|
|
49
49
|
const handler = addCalls[0].fn;
|
|
50
50
|
|
|
51
|
-
handler({
|
|
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
|
});
|
|
@@ -56,11 +59,44 @@ describe('Receiver node', function() {
|
|
|
56
59
|
it('delivers media updates when size is below threshold', function() {
|
|
57
60
|
const { NodeCtor, addCalls } = load();
|
|
58
61
|
const sent = [];
|
|
59
|
-
const node = new NodeCtor({config:'c', ignore:'', maxFileSizeMb:'5'});
|
|
62
|
+
const node = new NodeCtor({config:'c', ignore:'', ignoreMessageTypes:'', maxFileSizeMb:'5'});
|
|
60
63
|
node.send = (msg) => sent.push(msg);
|
|
61
64
|
const handler = addCalls[0].fn;
|
|
62
65
|
|
|
63
|
-
handler({
|
|
66
|
+
handler({
|
|
67
|
+
className: 'UpdateNewMessage',
|
|
68
|
+
message: { fromId: { userId: 123 }, peerId:{ userId: 123 }, media: { document: { size: 3 * 1024 * 1024 } } }
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
assert.strictEqual(sent.length, 1);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('skips updates when media type is ignored', function() {
|
|
75
|
+
const { NodeCtor, addCalls } = load();
|
|
76
|
+
const sent = [];
|
|
77
|
+
const node = new NodeCtor({config:'c', ignore:'', ignoreMessageTypes:'video\ndocument', maxFileSizeMb:''});
|
|
78
|
+
node.send = (msg) => sent.push(msg);
|
|
79
|
+
const handler = addCalls[0].fn;
|
|
80
|
+
|
|
81
|
+
handler({
|
|
82
|
+
className: 'UpdateNewMessage',
|
|
83
|
+
message: { fromId: { userId: 123 }, peerId:{ userId: 123 }, media: { document: { mimeType: 'video/mp4', attributes: [{ className: 'DocumentAttributeVideo' }] } } }
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
assert.strictEqual(sent.length, 0);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('delivers updates when media type is not ignored', function() {
|
|
90
|
+
const { NodeCtor, addCalls } = load();
|
|
91
|
+
const sent = [];
|
|
92
|
+
const node = new NodeCtor({config:'c', ignore:'', ignoreMessageTypes:'voice', maxFileSizeMb:''});
|
|
93
|
+
node.send = (msg) => sent.push(msg);
|
|
94
|
+
const handler = addCalls[0].fn;
|
|
95
|
+
|
|
96
|
+
handler({
|
|
97
|
+
className: 'UpdateNewMessage',
|
|
98
|
+
message: { fromId: { userId: 123 }, peerId:{ userId: 123 }, media: { document: { mimeType: 'video/mp4', attributes: [{ className: 'DocumentAttributeVideo' }] } } }
|
|
99
|
+
});
|
|
64
100
|
|
|
65
101
|
assert.strictEqual(sent.length, 1);
|
|
66
102
|
});
|