morok-bot-sdk 1.0.1
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/LICENSE +201 -0
- package/README.md +602 -0
- package/README.ru.md +602 -0
- package/dist/bot.d.ts +232 -0
- package/dist/bot.d.ts.map +1 -0
- package/dist/bot.js +558 -0
- package/dist/bot.js.map +1 -0
- package/dist/crypto/channel-cipher.d.ts +32 -0
- package/dist/crypto/channel-cipher.d.ts.map +1 -0
- package/dist/crypto/channel-cipher.js +77 -0
- package/dist/crypto/channel-cipher.js.map +1 -0
- package/dist/crypto/channel-key-store.d.ts +37 -0
- package/dist/crypto/channel-key-store.d.ts.map +1 -0
- package/dist/crypto/channel-key-store.js +149 -0
- package/dist/crypto/channel-key-store.js.map +1 -0
- package/dist/crypto/cross-signing.d.ts +57 -0
- package/dist/crypto/cross-signing.d.ts.map +1 -0
- package/dist/crypto/cross-signing.js +111 -0
- package/dist/crypto/cross-signing.js.map +1 -0
- package/dist/crypto/file-cipher.d.ts +36 -0
- package/dist/crypto/file-cipher.d.ts.map +1 -0
- package/dist/crypto/file-cipher.js +61 -0
- package/dist/crypto/file-cipher.js.map +1 -0
- package/dist/crypto/group-secret-cipher.d.ts +49 -0
- package/dist/crypto/group-secret-cipher.d.ts.map +1 -0
- package/dist/crypto/group-secret-cipher.js +69 -0
- package/dist/crypto/group-secret-cipher.js.map +1 -0
- package/dist/crypto/group-secret-store.d.ts +35 -0
- package/dist/crypto/group-secret-store.d.ts.map +1 -0
- package/dist/crypto/group-secret-store.js +149 -0
- package/dist/crypto/group-secret-store.js.map +1 -0
- package/dist/crypto/signal.d.ts +81 -0
- package/dist/crypto/signal.d.ts.map +1 -0
- package/dist/crypto/signal.js +125 -0
- package/dist/crypto/signal.js.map +1 -0
- package/dist/crypto/stores.d.ts +130 -0
- package/dist/crypto/stores.d.ts.map +1 -0
- package/dist/crypto/stores.js +314 -0
- package/dist/crypto/stores.js.map +1 -0
- package/dist/flow/attachments.d.ts +110 -0
- package/dist/flow/attachments.d.ts.map +1 -0
- package/dist/flow/attachments.js +409 -0
- package/dist/flow/attachments.js.map +1 -0
- package/dist/flow/conv-cache.d.ts +36 -0
- package/dist/flow/conv-cache.d.ts.map +1 -0
- package/dist/flow/conv-cache.js +84 -0
- package/dist/flow/conv-cache.js.map +1 -0
- package/dist/flow/direct.d.ts +109 -0
- package/dist/flow/direct.d.ts.map +1 -0
- package/dist/flow/direct.js +346 -0
- package/dist/flow/direct.js.map +1 -0
- package/dist/flow/groups.d.ts +146 -0
- package/dist/flow/groups.d.ts.map +1 -0
- package/dist/flow/groups.js +768 -0
- package/dist/flow/groups.js.map +1 -0
- package/dist/flow/prekeys.d.ts +45 -0
- package/dist/flow/prekeys.d.ts.map +1 -0
- package/dist/flow/prekeys.js +111 -0
- package/dist/flow/prekeys.js.map +1 -0
- package/dist/flow/receive.d.ts +125 -0
- package/dist/flow/receive.d.ts.map +1 -0
- package/dist/flow/receive.js +773 -0
- package/dist/flow/receive.js.map +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/morokbot-file.d.ts +14 -0
- package/dist/morokbot-file.d.ts.map +1 -0
- package/dist/morokbot-file.js +88 -0
- package/dist/morokbot-file.js.map +1 -0
- package/dist/ratelimit.d.ts +40 -0
- package/dist/ratelimit.d.ts.map +1 -0
- package/dist/ratelimit.js +76 -0
- package/dist/ratelimit.js.map +1 -0
- package/dist/sessions.d.ts +34 -0
- package/dist/sessions.d.ts.map +1 -0
- package/dist/sessions.js +69 -0
- package/dist/sessions.js.map +1 -0
- package/dist/state-lock.d.ts +17 -0
- package/dist/state-lock.d.ts.map +1 -0
- package/dist/state-lock.js +66 -0
- package/dist/state-lock.js.map +1 -0
- package/dist/transport/http.d.ts +48 -0
- package/dist/transport/http.d.ts.map +1 -0
- package/dist/transport/http.js +112 -0
- package/dist/transport/http.js.map +1 -0
- package/dist/transport/ws.d.ts +65 -0
- package/dist/transport/ws.d.ts.map +1 -0
- package/dist/transport/ws.js +219 -0
- package/dist/transport/ws.js.map +1 -0
- package/dist/types.d.ts +254 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +59 -0
|
@@ -0,0 +1,773 @@
|
|
|
1
|
+
import { TextDecoder } from 'node:util';
|
|
2
|
+
import { downloadAttachment, isEncryptedFileRef, parseGalleryItem, GALLERY_MIN_ITEMS, GALLERY_MAX_ITEMS, } from './attachments.js';
|
|
3
|
+
const PEER_CACHE_TTL_MS = 5 * 60 * 1000;
|
|
4
|
+
const DEDUP_MAX_ENTRIES = 1024;
|
|
5
|
+
const DEDUP_TTL_MS = 5 * 60 * 1000;
|
|
6
|
+
const BACKFILL_REQ_THROTTLE_MS = 5_000;
|
|
7
|
+
const BACKFILL_REQ_THROTTLE_MAX = 4096;
|
|
8
|
+
class DedupCache {
|
|
9
|
+
entries = new Map();
|
|
10
|
+
has(key) {
|
|
11
|
+
const at = this.entries.get(key);
|
|
12
|
+
if (at === undefined)
|
|
13
|
+
return false;
|
|
14
|
+
if (Date.now() - at > DEDUP_TTL_MS) {
|
|
15
|
+
this.entries.delete(key);
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
record(key) {
|
|
21
|
+
if (this.entries.size >= DEDUP_MAX_ENTRIES) {
|
|
22
|
+
const oldest = this.entries.keys().next().value;
|
|
23
|
+
if (oldest !== undefined)
|
|
24
|
+
this.entries.delete(oldest);
|
|
25
|
+
}
|
|
26
|
+
this.entries.set(key, Date.now());
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
const PEER_CACHE_MAX = 1024;
|
|
30
|
+
export class ReceiveFlow {
|
|
31
|
+
botUserId;
|
|
32
|
+
http;
|
|
33
|
+
ws;
|
|
34
|
+
signal;
|
|
35
|
+
convCache;
|
|
36
|
+
emitter;
|
|
37
|
+
groups;
|
|
38
|
+
opts;
|
|
39
|
+
logger;
|
|
40
|
+
peerCache = new Map();
|
|
41
|
+
dedup = new DedupCache();
|
|
42
|
+
textDec = new TextDecoder('utf-8', { fatal: false });
|
|
43
|
+
backfillReqThrottle = new Map();
|
|
44
|
+
constructor(botUserId, http, ws, signal, convCache, emitter, groups, opts, logger) {
|
|
45
|
+
this.botUserId = botUserId;
|
|
46
|
+
this.http = http;
|
|
47
|
+
this.ws = ws;
|
|
48
|
+
this.signal = signal;
|
|
49
|
+
this.convCache = convCache;
|
|
50
|
+
this.emitter = emitter;
|
|
51
|
+
this.groups = groups;
|
|
52
|
+
this.opts = opts;
|
|
53
|
+
this.logger = logger;
|
|
54
|
+
this.ws.on('frame', (frame) => {
|
|
55
|
+
void this.onFrame(frame).catch(err => {
|
|
56
|
+
this.logger?.warn({ err: err.message, frameType: frame.type }, '[receive] frame handler threw');
|
|
57
|
+
this.emitter.emit({ kind: 'error', error: err });
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
async onFrame(frame) {
|
|
62
|
+
switch (frame.type) {
|
|
63
|
+
case 'message': return this.onMessageFrame(frame);
|
|
64
|
+
case 'bot_started': return this.onBotStartedFrame(frame);
|
|
65
|
+
case 'bot_stopped': return this.onBotStoppedFrame(frame);
|
|
66
|
+
case 'conversation_added': return this.onConversationAddedFrame(frame);
|
|
67
|
+
case 'conversation_kicked': return this.onConversationKickedFrame(frame);
|
|
68
|
+
case 'channel_key_rotated': return this.onChannelKeyRotatedFrame(frame);
|
|
69
|
+
case 'channel_key_backfill_request': return this.onChannelKeyBackfillRequestFrame(frame);
|
|
70
|
+
case 'member_count_changed': return this.onMemberCountChangedFrame(frame);
|
|
71
|
+
case 'member_updated': return this.onMemberUpdatedFrame(frame);
|
|
72
|
+
case 'group_updated': return this.onGroupUpdatedFrame(frame);
|
|
73
|
+
case 'reaction_encrypted': return this.onReactionEncryptedFrame(frame);
|
|
74
|
+
case 'reaction_removed': return this.onReactionRemovedFrame(frame);
|
|
75
|
+
default: return;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
async onMessageFrame(frame) {
|
|
79
|
+
const senderId = frame.senderId;
|
|
80
|
+
const messageType = frame.messageType;
|
|
81
|
+
if (typeof senderId === 'number' && senderId === this.botUserId) {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
if (messageType === 8)
|
|
85
|
+
return this.onGroupInbound(frame);
|
|
86
|
+
if (messageType !== 1 && messageType !== 3) {
|
|
87
|
+
this.logger?.debug({ messageType }, '[receive] skipping non-DM message');
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
return this.onDirectInbound(frame);
|
|
91
|
+
}
|
|
92
|
+
async onDirectInbound(frame) {
|
|
93
|
+
const senderId = frame.senderId;
|
|
94
|
+
const recipientId = frame.recipientId;
|
|
95
|
+
const messageType = frame.messageType;
|
|
96
|
+
const ciphertext = frame.ciphertext;
|
|
97
|
+
if (typeof senderId !== 'number'
|
|
98
|
+
|| typeof ciphertext !== 'string'
|
|
99
|
+
|| typeof frame.senderDeviceId !== 'number'
|
|
100
|
+
|| typeof frame.id !== 'number'
|
|
101
|
+
|| typeof frame.conversationId !== 'number'
|
|
102
|
+
|| (messageType !== 1 && messageType !== 3)) {
|
|
103
|
+
this.logger?.warn({ frame }, '[receive] malformed message frame');
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
if (typeof recipientId === 'number' && recipientId !== this.botUserId) {
|
|
107
|
+
this.logger?.debug({ recipientId, botUserId: this.botUserId }, '[receive] message not for us');
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
const clientMsgId = typeof frame.clientMsgId === 'string' ? frame.clientMsgId : null;
|
|
111
|
+
const dedupKey = clientMsgId !== null
|
|
112
|
+
? `c:${senderId}.${frame.senderDeviceId}:${frame.conversationId}:${clientMsgId}`
|
|
113
|
+
: `s:${senderId}.${frame.senderDeviceId}:${frame.conversationId}:${frame.id}`;
|
|
114
|
+
if (dedupKey && this.dedup.has(dedupKey)) {
|
|
115
|
+
this.logger?.debug({ messageId: frame.id, clientMsgId }, '[receive] deduped replay');
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
const senderDeviceId = frame.senderDeviceId;
|
|
119
|
+
let plaintextBytes;
|
|
120
|
+
try {
|
|
121
|
+
plaintextBytes = await this.signal.withPeerLock(senderId, senderDeviceId, () => this.signal.decrypt(senderId, senderDeviceId, messageType, ciphertext));
|
|
122
|
+
}
|
|
123
|
+
catch (err) {
|
|
124
|
+
this.logger?.warn({ err: err.message, senderId, deviceId: frame.senderDeviceId, messageType }, '[receive] decrypt failed');
|
|
125
|
+
this.emitter.emit({
|
|
126
|
+
kind: 'error',
|
|
127
|
+
error: new Error(`decrypt failed for senderId=${senderId}.${frame.senderDeviceId} type=${messageType}: ${err.message}`),
|
|
128
|
+
});
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
if (dedupKey)
|
|
132
|
+
this.dedup.record(dedupKey);
|
|
133
|
+
const decoded = this.textDec.decode(plaintextBytes);
|
|
134
|
+
const action = parseBotAction(decoded);
|
|
135
|
+
if (action) {
|
|
136
|
+
const actionPeer = await this.resolvePeer(senderId).catch(() => null);
|
|
137
|
+
this.emitter.emit({
|
|
138
|
+
kind: 'control',
|
|
139
|
+
payload: {
|
|
140
|
+
controlId: action.controlId,
|
|
141
|
+
sender: actionPeer ?? { userId: senderId, username: `user_${senderId}`, displayName: null },
|
|
142
|
+
conversationId: frame.conversationId,
|
|
143
|
+
conversationType: 'DIRECT',
|
|
144
|
+
},
|
|
145
|
+
});
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
const sniffed = sniffStructuredPayload(decoded);
|
|
149
|
+
const text = sniffed?.caption ?? (sniffed === null ? decoded : '');
|
|
150
|
+
const attachment = sniffed?.attachment;
|
|
151
|
+
const peer = await this.resolvePeer(senderId).catch(() => null);
|
|
152
|
+
if (!peer) {
|
|
153
|
+
this.logger?.warn({ senderId }, '[receive] peer resolution failed; emitting with stub');
|
|
154
|
+
}
|
|
155
|
+
const createdAt = typeof frame.createdAt === 'string'
|
|
156
|
+
? new Date(frame.createdAt)
|
|
157
|
+
: new Date();
|
|
158
|
+
let attachmentObj;
|
|
159
|
+
let galleryObj;
|
|
160
|
+
if (attachment) {
|
|
161
|
+
attachmentObj = buildIncomingAttachment(attachment, () => downloadAttachment(this.http, attachment.ref, this.logger));
|
|
162
|
+
}
|
|
163
|
+
else if (sniffed?.gallery) {
|
|
164
|
+
galleryObj = buildIncomingGallery(sniffed.gallery, (ref) => () => downloadAttachment(this.http, ref, this.logger));
|
|
165
|
+
}
|
|
166
|
+
const baseMsg = {
|
|
167
|
+
messageId: frame.id,
|
|
168
|
+
conversationId: frame.conversationId,
|
|
169
|
+
conversationType: 'DIRECT',
|
|
170
|
+
sender: peer ?? { userId: senderId, username: `user_${senderId}`, displayName: null },
|
|
171
|
+
senderDeviceId: frame.senderDeviceId,
|
|
172
|
+
text,
|
|
173
|
+
...(attachmentObj ? { attachment: attachmentObj } : {}),
|
|
174
|
+
...(galleryObj ? { gallery: galleryObj } : {}),
|
|
175
|
+
clientMsgId,
|
|
176
|
+
replyToId: typeof frame.replyToId === 'number' ? frame.replyToId : null,
|
|
177
|
+
threadRootId: typeof frame.threadRootId === 'number' ? frame.threadRootId : null,
|
|
178
|
+
createdAt,
|
|
179
|
+
};
|
|
180
|
+
const parsed = parseCommand(text);
|
|
181
|
+
if (parsed) {
|
|
182
|
+
const cmd = {
|
|
183
|
+
...baseMsg,
|
|
184
|
+
command: parsed.command,
|
|
185
|
+
args: parsed.args,
|
|
186
|
+
argv: parsed.argv,
|
|
187
|
+
};
|
|
188
|
+
this.emitter.emit({ kind: 'command', payload: cmd });
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
this.emitter.emit({ kind: 'message', payload: baseMsg });
|
|
192
|
+
}
|
|
193
|
+
async onBotStartedFrame(frame) {
|
|
194
|
+
const userId = frame.userId;
|
|
195
|
+
const botUserId = frame.botUserId;
|
|
196
|
+
const startedAt = frame.startedAt;
|
|
197
|
+
if (typeof userId !== 'number' || (typeof botUserId === 'number' && botUserId !== this.botUserId)) {
|
|
198
|
+
this.logger?.warn({ frame }, '[receive] malformed bot_started');
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
const peer = await this.resolvePeer(userId).catch(() => null);
|
|
202
|
+
const startedAtDate = typeof startedAt === 'string' ? new Date(startedAt) : new Date();
|
|
203
|
+
this.emitter.emit({
|
|
204
|
+
kind: 'start',
|
|
205
|
+
payload: {
|
|
206
|
+
peer: peer ?? { userId, username: `user_${userId}`, displayName: null },
|
|
207
|
+
startedAt: startedAtDate,
|
|
208
|
+
},
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
async onBotStoppedFrame(frame) {
|
|
212
|
+
const userId = frame.userId;
|
|
213
|
+
const botUserId = frame.botUserId;
|
|
214
|
+
const stoppedAt = frame.stoppedAt;
|
|
215
|
+
if (typeof userId !== 'number' || (typeof botUserId === 'number' && botUserId !== this.botUserId)) {
|
|
216
|
+
this.logger?.warn({ frame }, '[receive] malformed bot_stopped');
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
this.peerCache.delete(userId);
|
|
220
|
+
const peer = await this.resolvePeer(userId).catch(() => null);
|
|
221
|
+
const stoppedAtDate = typeof stoppedAt === 'string' ? new Date(stoppedAt) : new Date();
|
|
222
|
+
this.emitter.emit({
|
|
223
|
+
kind: 'stop',
|
|
224
|
+
payload: {
|
|
225
|
+
peer: peer ?? { userId, username: `user_${userId}`, displayName: null },
|
|
226
|
+
stoppedAt: stoppedAtDate,
|
|
227
|
+
},
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
async onReactionEncryptedFrame(frame) {
|
|
231
|
+
const reactorId = frame.reactorUserId;
|
|
232
|
+
const reactorDevice = frame.reactorDeviceId;
|
|
233
|
+
const messageId = frame.messageId;
|
|
234
|
+
const messageType = frame.messageType;
|
|
235
|
+
const ciphertext = frame.ciphertext;
|
|
236
|
+
const conversationId = frame.conversationId;
|
|
237
|
+
const clientMsgId = typeof frame.clientMsgId === 'string' ? frame.clientMsgId : null;
|
|
238
|
+
if (typeof reactorId !== 'number'
|
|
239
|
+
|| typeof reactorDevice !== 'number'
|
|
240
|
+
|| typeof messageId !== 'number'
|
|
241
|
+
|| typeof messageType !== 'number'
|
|
242
|
+
|| typeof ciphertext !== 'string') {
|
|
243
|
+
this.logger?.warn({ frame }, '[receive] malformed reaction_encrypted');
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
if (reactorId === this.botUserId)
|
|
247
|
+
return;
|
|
248
|
+
let unicode = null;
|
|
249
|
+
try {
|
|
250
|
+
let plaintext;
|
|
251
|
+
if (messageType === 8) {
|
|
252
|
+
if (!this.groups || typeof conversationId !== 'number') {
|
|
253
|
+
this.logger?.debug({ messageId }, '[receive] group reaction dropped: no GroupsFlow / convId');
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
plaintext = await this.groups.decryptGroupMessage(conversationId, ciphertext);
|
|
257
|
+
}
|
|
258
|
+
else if (messageType === 1 || messageType === 3) {
|
|
259
|
+
plaintext = await this.signal.withPeerLock(reactorId, reactorDevice, () => this.signal.decrypt(reactorId, reactorDevice, messageType, ciphertext));
|
|
260
|
+
}
|
|
261
|
+
else {
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
unicode = parseReactionUnicode(this.textDec.decode(plaintext));
|
|
265
|
+
}
|
|
266
|
+
catch (err) {
|
|
267
|
+
this.logger?.warn({ messageId, reactorId, messageType, err: err.message }, '[receive] reaction decrypt failed; emitting with unicode=null');
|
|
268
|
+
}
|
|
269
|
+
const peer = await this.resolvePeer(reactorId).catch(() => null);
|
|
270
|
+
this.emitter.emit({
|
|
271
|
+
kind: 'reaction',
|
|
272
|
+
payload: {
|
|
273
|
+
messageId,
|
|
274
|
+
clientMsgId,
|
|
275
|
+
reactor: peer ?? { userId: reactorId, username: `user_${reactorId}`, displayName: null },
|
|
276
|
+
unicode,
|
|
277
|
+
added: true,
|
|
278
|
+
},
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
async onReactionRemovedFrame(frame) {
|
|
282
|
+
const reactorId = frame.reactorUserId;
|
|
283
|
+
const messageId = frame.messageId;
|
|
284
|
+
const clientMsgId = typeof frame.clientMsgId === 'string' ? frame.clientMsgId : null;
|
|
285
|
+
if (typeof reactorId !== 'number' || typeof messageId !== 'number') {
|
|
286
|
+
this.logger?.warn({ frame }, '[receive] malformed reaction_removed');
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
if (reactorId === this.botUserId)
|
|
290
|
+
return;
|
|
291
|
+
const peer = await this.resolvePeer(reactorId).catch(() => null);
|
|
292
|
+
this.emitter.emit({
|
|
293
|
+
kind: 'reaction',
|
|
294
|
+
payload: {
|
|
295
|
+
messageId,
|
|
296
|
+
clientMsgId,
|
|
297
|
+
reactor: peer ?? { userId: reactorId, username: `user_${reactorId}`, displayName: null },
|
|
298
|
+
unicode: null,
|
|
299
|
+
added: false,
|
|
300
|
+
},
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
async onGroupInbound(frame) {
|
|
304
|
+
if (!this.groups) {
|
|
305
|
+
this.logger?.debug({ messageType: frame.messageType }, '[receive] group-message dropped: GroupsFlow not wired');
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
const senderId = frame.senderId;
|
|
309
|
+
const senderDeviceId = frame.senderDeviceId;
|
|
310
|
+
const conversationId = frame.conversationId;
|
|
311
|
+
const ciphertext = frame.ciphertext;
|
|
312
|
+
const messageId = frame.id;
|
|
313
|
+
const clientMsgId = typeof frame.clientMsgId === 'string' ? frame.clientMsgId : null;
|
|
314
|
+
if (typeof senderId !== 'number'
|
|
315
|
+
|| typeof senderDeviceId !== 'number'
|
|
316
|
+
|| typeof conversationId !== 'number'
|
|
317
|
+
|| typeof ciphertext !== 'string'
|
|
318
|
+
|| typeof messageId !== 'number') {
|
|
319
|
+
this.logger?.warn({ frame }, '[receive] malformed group-chat/channel frame');
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
const dedupKey = clientMsgId !== null
|
|
323
|
+
? `c:${senderId}.${senderDeviceId}:${conversationId}:${clientMsgId}`
|
|
324
|
+
: `s:${senderId}.${senderDeviceId}:${conversationId}:${messageId}`;
|
|
325
|
+
if (dedupKey && this.dedup.has(dedupKey)) {
|
|
326
|
+
this.logger?.debug({ messageId, clientMsgId }, '[receive] deduped group replay');
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
let plaintextBytes;
|
|
330
|
+
try {
|
|
331
|
+
plaintextBytes = await this.groups.decryptGroupMessage(conversationId, ciphertext);
|
|
332
|
+
}
|
|
333
|
+
catch (err) {
|
|
334
|
+
this.logger?.warn({
|
|
335
|
+
conversationId, senderId, senderDeviceId,
|
|
336
|
+
err: err.message,
|
|
337
|
+
}, '[receive] group_message decrypt failed');
|
|
338
|
+
this.emitter.emit({
|
|
339
|
+
kind: 'error',
|
|
340
|
+
error: new Error(`group decrypt failed for conv=${conversationId} msg=${messageId}: ${err.message}`),
|
|
341
|
+
});
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
if (dedupKey)
|
|
345
|
+
this.dedup.record(dedupKey);
|
|
346
|
+
let conversationType = 'GROUP';
|
|
347
|
+
if (this.convCache) {
|
|
348
|
+
const info = this.convCache.peek(conversationId)
|
|
349
|
+
?? await this.convCache.load(conversationId).catch(() => null);
|
|
350
|
+
if (info && info.isChannel)
|
|
351
|
+
conversationType = 'CHANNEL';
|
|
352
|
+
}
|
|
353
|
+
const decoded = this.textDec.decode(plaintextBytes);
|
|
354
|
+
const sniffed = sniffStructuredPayload(decoded);
|
|
355
|
+
const text = sniffed?.caption ?? (sniffed === null ? decoded : '');
|
|
356
|
+
const attachment = sniffed?.attachment;
|
|
357
|
+
const peer = await this.resolvePeer(senderId).catch(() => null);
|
|
358
|
+
if (!peer) {
|
|
359
|
+
this.logger?.warn({ senderId }, '[receive] peer resolution failed; emitting with stub');
|
|
360
|
+
}
|
|
361
|
+
const createdAt = typeof frame.createdAt === 'string'
|
|
362
|
+
? new Date(frame.createdAt)
|
|
363
|
+
: new Date();
|
|
364
|
+
let attachmentObj;
|
|
365
|
+
let galleryObj;
|
|
366
|
+
if (attachment) {
|
|
367
|
+
attachmentObj = buildIncomingAttachment(attachment, () => downloadAttachment(this.http, attachment.ref, this.logger));
|
|
368
|
+
}
|
|
369
|
+
else if (sniffed?.gallery) {
|
|
370
|
+
galleryObj = buildIncomingGallery(sniffed.gallery, (ref) => () => downloadAttachment(this.http, ref, this.logger));
|
|
371
|
+
}
|
|
372
|
+
const baseMsg = {
|
|
373
|
+
messageId: messageId,
|
|
374
|
+
conversationId: conversationId,
|
|
375
|
+
conversationType,
|
|
376
|
+
sender: peer ?? { userId: senderId, username: `user_${senderId}`, displayName: null },
|
|
377
|
+
senderDeviceId: senderDeviceId,
|
|
378
|
+
text,
|
|
379
|
+
...(attachmentObj ? { attachment: attachmentObj } : {}),
|
|
380
|
+
...(galleryObj ? { gallery: galleryObj } : {}),
|
|
381
|
+
clientMsgId,
|
|
382
|
+
replyToId: typeof frame.replyToId === 'number' ? frame.replyToId : null,
|
|
383
|
+
threadRootId: typeof frame.threadRootId === 'number' ? frame.threadRootId : null,
|
|
384
|
+
createdAt,
|
|
385
|
+
};
|
|
386
|
+
const parsed = parseCommand(text);
|
|
387
|
+
if (parsed) {
|
|
388
|
+
const cmd = {
|
|
389
|
+
...baseMsg,
|
|
390
|
+
command: parsed.command,
|
|
391
|
+
args: parsed.args,
|
|
392
|
+
argv: parsed.argv,
|
|
393
|
+
};
|
|
394
|
+
this.emitter.emit({ kind: 'command', payload: cmd });
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
this.emitter.emit({ kind: 'message', payload: baseMsg });
|
|
398
|
+
}
|
|
399
|
+
async onConversationAddedFrame(frame) {
|
|
400
|
+
if (!this.groups)
|
|
401
|
+
return;
|
|
402
|
+
const conversationId = frame.conversationId;
|
|
403
|
+
const conv = frame.conversation;
|
|
404
|
+
if (typeof conversationId !== 'number' || !conv || typeof conv.id !== 'number') {
|
|
405
|
+
this.logger?.warn({ frame }, '[receive] malformed conversation_added');
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
const isChannel = conv.isChannel === true;
|
|
409
|
+
const isGroup = conv.isGroup === true || isChannel;
|
|
410
|
+
if (!isGroup)
|
|
411
|
+
return;
|
|
412
|
+
await this.groups.installGroupSecret(conversationId).catch(err => {
|
|
413
|
+
this.logger?.warn({ conversationId, err: err.message }, '[receive] group_secret install on conversation_added failed');
|
|
414
|
+
});
|
|
415
|
+
await Promise.all([
|
|
416
|
+
this.groups.installFromServer(conversationId, { sinceEpoch: -1 }).catch(err => {
|
|
417
|
+
this.logger?.warn({ conversationId, err: err.message }, '[receive] channel-key install on conversation_added failed');
|
|
418
|
+
}),
|
|
419
|
+
this.convCache?.load(conversationId).catch(err => {
|
|
420
|
+
this.logger?.warn({ conversationId, err: err.message }, '[receive] conv-cache load on conversation_added failed');
|
|
421
|
+
}) ?? Promise.resolve(),
|
|
422
|
+
]);
|
|
423
|
+
const addedAtStr = frame.addedAt;
|
|
424
|
+
const addedAtDate = typeof addedAtStr === 'string' ? new Date(addedAtStr) : new Date();
|
|
425
|
+
this.emitter.emit({
|
|
426
|
+
kind: 'conversation_added',
|
|
427
|
+
payload: {
|
|
428
|
+
conversationId,
|
|
429
|
+
conversationType: isChannel ? 'CHANNEL' : 'GROUP',
|
|
430
|
+
title: typeof conv.title === 'string' ? conv.title : null,
|
|
431
|
+
addedAt: addedAtDate,
|
|
432
|
+
},
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
async onConversationKickedFrame(frame) {
|
|
436
|
+
const conversationId = frame.conversationId;
|
|
437
|
+
const reasonStr = frame.reason;
|
|
438
|
+
const actor = frame.actorUserId;
|
|
439
|
+
if (typeof conversationId !== 'number') {
|
|
440
|
+
this.logger?.warn({ frame }, '[receive] malformed conversation_kicked');
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
const reason = reasonStr === 'left' ? 'left' : 'kicked';
|
|
444
|
+
if (this.groups) {
|
|
445
|
+
try {
|
|
446
|
+
await this.groups.forgetConversation(conversationId);
|
|
447
|
+
}
|
|
448
|
+
catch (err) {
|
|
449
|
+
this.logger?.warn({ conversationId, err: err.message }, '[receive] forgetConversation failed');
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
this.convCache?.drop(conversationId);
|
|
453
|
+
this.emitter.emit({
|
|
454
|
+
kind: 'conversation_kicked',
|
|
455
|
+
payload: {
|
|
456
|
+
conversationId,
|
|
457
|
+
reason,
|
|
458
|
+
actorUserId: typeof actor === 'number' ? actor : null,
|
|
459
|
+
removedAt: new Date(),
|
|
460
|
+
},
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
onMemberUpdatedFrame(frame) {
|
|
464
|
+
const conversationId = frame.conversationId;
|
|
465
|
+
if (typeof conversationId !== 'number')
|
|
466
|
+
return;
|
|
467
|
+
const memberUserId = frame.memberUserId;
|
|
468
|
+
if (typeof memberUserId !== 'number' || memberUserId !== this.botUserId)
|
|
469
|
+
return;
|
|
470
|
+
this.convCache?.invalidate(conversationId);
|
|
471
|
+
}
|
|
472
|
+
onGroupUpdatedFrame(frame) {
|
|
473
|
+
const conversationId = frame.conversationId;
|
|
474
|
+
if (typeof conversationId !== 'number')
|
|
475
|
+
return;
|
|
476
|
+
this.convCache?.invalidate(conversationId);
|
|
477
|
+
}
|
|
478
|
+
async onMemberCountChangedFrame(frame) {
|
|
479
|
+
const conversationId = frame.conversationId;
|
|
480
|
+
if (typeof conversationId !== 'number')
|
|
481
|
+
return;
|
|
482
|
+
this.convCache?.invalidate(conversationId);
|
|
483
|
+
if (!this.opts.autoBackfillOnJoin)
|
|
484
|
+
return;
|
|
485
|
+
if (!this.groups)
|
|
486
|
+
return;
|
|
487
|
+
const change = frame.change;
|
|
488
|
+
if (!change || change.kind !== 'added')
|
|
489
|
+
return;
|
|
490
|
+
if (typeof change.userId !== 'number' || change.userId === this.botUserId)
|
|
491
|
+
return;
|
|
492
|
+
const newUserId = change.userId;
|
|
493
|
+
const groups = this.groups;
|
|
494
|
+
const p = groups.backfillChannelKeys(conversationId, { userId: newUserId })
|
|
495
|
+
.then(({ accepted }) => {
|
|
496
|
+
this.logger?.info({ conversationId, newUserId, accepted }, '[receive] auto-backfill on join complete');
|
|
497
|
+
})
|
|
498
|
+
.catch((err) => {
|
|
499
|
+
this.logger?.warn({
|
|
500
|
+
conversationId, newUserId,
|
|
501
|
+
err: err.message,
|
|
502
|
+
}, '[receive] auto-backfill on join failed');
|
|
503
|
+
this.emitter.emit({
|
|
504
|
+
kind: 'error',
|
|
505
|
+
error: new Error(`auto-backfill failed for conv=${conversationId} user=${newUserId}: ${err.message}`),
|
|
506
|
+
});
|
|
507
|
+
});
|
|
508
|
+
groups.trackBackfill(p);
|
|
509
|
+
}
|
|
510
|
+
async onChannelKeyBackfillRequestFrame(frame) {
|
|
511
|
+
if (!this.opts.serveBackfillRequests)
|
|
512
|
+
return;
|
|
513
|
+
if (!this.groups)
|
|
514
|
+
return;
|
|
515
|
+
const conversationId = frame.conversationId;
|
|
516
|
+
const recipientUserId = frame.recipientUserId;
|
|
517
|
+
const recipientDeviceId = frame.recipientDeviceId;
|
|
518
|
+
if (typeof conversationId !== 'number'
|
|
519
|
+
|| typeof recipientUserId !== 'number'
|
|
520
|
+
|| typeof recipientDeviceId !== 'number') {
|
|
521
|
+
this.logger?.warn({ frame }, '[receive] malformed channel_key_backfill_request');
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
if (recipientUserId === this.botUserId)
|
|
525
|
+
return;
|
|
526
|
+
const key = `${conversationId}:${recipientUserId}:${recipientDeviceId}`;
|
|
527
|
+
const now = Date.now();
|
|
528
|
+
const last = this.backfillReqThrottle.get(key);
|
|
529
|
+
if (last !== undefined && now - last < BACKFILL_REQ_THROTTLE_MS) {
|
|
530
|
+
this.logger?.debug({ conversationId, recipientUserId, recipientDeviceId }, '[receive] channel_key_backfill_request throttled');
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
this.pruneBackfillThrottle(now);
|
|
534
|
+
this.backfillReqThrottle.set(key, now);
|
|
535
|
+
const groups = this.groups;
|
|
536
|
+
const p = groups.backfillChannelKeys(conversationId, { userId: recipientUserId, deviceIds: [recipientDeviceId] })
|
|
537
|
+
.then(({ accepted }) => {
|
|
538
|
+
this.logger?.info({ conversationId, recipientUserId, recipientDeviceId, accepted }, '[receive] served channel_key_backfill_request');
|
|
539
|
+
})
|
|
540
|
+
.catch((err) => {
|
|
541
|
+
this.logger?.warn({
|
|
542
|
+
conversationId, recipientUserId, recipientDeviceId,
|
|
543
|
+
err: err.message,
|
|
544
|
+
}, '[receive] channel_key_backfill_request: backfill failed');
|
|
545
|
+
this.emitter.emit({
|
|
546
|
+
kind: 'error',
|
|
547
|
+
error: new Error(`backfill_request failed for conv=${conversationId} ` +
|
|
548
|
+
`user=${recipientUserId}.${recipientDeviceId}: ${err.message}`),
|
|
549
|
+
});
|
|
550
|
+
});
|
|
551
|
+
groups.trackBackfill(p);
|
|
552
|
+
}
|
|
553
|
+
pruneBackfillThrottle(now) {
|
|
554
|
+
if (this.backfillReqThrottle.size < BACKFILL_REQ_THROTTLE_MAX)
|
|
555
|
+
return;
|
|
556
|
+
for (const [k, t] of this.backfillReqThrottle) {
|
|
557
|
+
if (now - t >= BACKFILL_REQ_THROTTLE_MS)
|
|
558
|
+
this.backfillReqThrottle.delete(k);
|
|
559
|
+
}
|
|
560
|
+
while (this.backfillReqThrottle.size >= BACKFILL_REQ_THROTTLE_MAX) {
|
|
561
|
+
const oldest = this.backfillReqThrottle.keys().next().value;
|
|
562
|
+
if (oldest === undefined)
|
|
563
|
+
break;
|
|
564
|
+
this.backfillReqThrottle.delete(oldest);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
async onChannelKeyRotatedFrame(frame) {
|
|
568
|
+
if (!this.groups)
|
|
569
|
+
return;
|
|
570
|
+
const conversationId = frame.conversationId;
|
|
571
|
+
const epoch = frame.epoch;
|
|
572
|
+
if (typeof conversationId !== 'number' || typeof epoch !== 'number') {
|
|
573
|
+
this.logger?.warn({ frame }, '[receive] malformed channel_key_rotated');
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
576
|
+
try {
|
|
577
|
+
await this.groups.installFromServer(conversationId, {
|
|
578
|
+
sinceEpoch: Math.max(-1, epoch - 1),
|
|
579
|
+
});
|
|
580
|
+
}
|
|
581
|
+
catch (err) {
|
|
582
|
+
this.logger?.warn({ conversationId, epoch, err: err.message }, '[receive] channel-key install on rotation failed');
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
async resolvePeer(userId) {
|
|
586
|
+
const cached = this.peerCache.get(userId);
|
|
587
|
+
if (cached && Date.now() - cached.fetched < PEER_CACHE_TTL_MS) {
|
|
588
|
+
return cached.peer;
|
|
589
|
+
}
|
|
590
|
+
const res = await this.http.get(`/users/id/${userId}`);
|
|
591
|
+
const u = res.data.user;
|
|
592
|
+
const peer = {
|
|
593
|
+
userId: u.id,
|
|
594
|
+
username: u.username,
|
|
595
|
+
displayName: u.displayName,
|
|
596
|
+
};
|
|
597
|
+
this.peerCache.set(userId, { peer, fetched: Date.now() });
|
|
598
|
+
while (this.peerCache.size > PEER_CACHE_MAX) {
|
|
599
|
+
const oldest = this.peerCache.keys().next().value;
|
|
600
|
+
if (oldest === undefined)
|
|
601
|
+
break;
|
|
602
|
+
this.peerCache.delete(oldest);
|
|
603
|
+
}
|
|
604
|
+
return peer;
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
function parseReactionUnicode(plaintext) {
|
|
608
|
+
if (plaintext.length === 0 || plaintext.length > 512)
|
|
609
|
+
return null;
|
|
610
|
+
let parsed;
|
|
611
|
+
try {
|
|
612
|
+
parsed = JSON.parse(plaintext);
|
|
613
|
+
}
|
|
614
|
+
catch {
|
|
615
|
+
return null;
|
|
616
|
+
}
|
|
617
|
+
if (!parsed || typeof parsed !== 'object')
|
|
618
|
+
return null;
|
|
619
|
+
const e = parsed.emoji;
|
|
620
|
+
return typeof e === 'string' && e.length > 0 && e.length <= 64 ? e : null;
|
|
621
|
+
}
|
|
622
|
+
const COMMAND_LINE_RE = /^\/([a-z][a-z0-9_]{0,31})(?:\s+([\s\S]*))?$/;
|
|
623
|
+
function parseCommand(text) {
|
|
624
|
+
if (text.includes('\n'))
|
|
625
|
+
return null;
|
|
626
|
+
const m = COMMAND_LINE_RE.exec(text.trim());
|
|
627
|
+
if (!m)
|
|
628
|
+
return null;
|
|
629
|
+
const args = (m[2] ?? '').trim();
|
|
630
|
+
const argv = args.length === 0 ? [] : args.split(/\s+/);
|
|
631
|
+
return { command: m[1], args, argv };
|
|
632
|
+
}
|
|
633
|
+
export const _parseCommand = parseCommand;
|
|
634
|
+
export const _parseReactionUnicode = parseReactionUnicode;
|
|
635
|
+
function sniffStructuredPayload(plaintext) {
|
|
636
|
+
if (plaintext.length === 0 || plaintext.charCodeAt(0) !== 0x7b) {
|
|
637
|
+
return null;
|
|
638
|
+
}
|
|
639
|
+
let parsed;
|
|
640
|
+
try {
|
|
641
|
+
parsed = JSON.parse(plaintext);
|
|
642
|
+
}
|
|
643
|
+
catch {
|
|
644
|
+
return null;
|
|
645
|
+
}
|
|
646
|
+
if (!parsed || typeof parsed !== 'object')
|
|
647
|
+
return null;
|
|
648
|
+
const p = parsed;
|
|
649
|
+
const type = p.type;
|
|
650
|
+
if (type === 'file') {
|
|
651
|
+
if (!isEncryptedFileRef(p.ref))
|
|
652
|
+
return null;
|
|
653
|
+
const caption = typeof p.caption === 'string' ? p.caption : '';
|
|
654
|
+
return {
|
|
655
|
+
caption,
|
|
656
|
+
attachment: { kind: 'file', ref: p.ref },
|
|
657
|
+
};
|
|
658
|
+
}
|
|
659
|
+
if (type === 'voice') {
|
|
660
|
+
if (!isEncryptedFileRef(p.ref))
|
|
661
|
+
return null;
|
|
662
|
+
const duration = typeof p.duration === 'number' && isFinite(p.duration) && p.duration > 0
|
|
663
|
+
? Math.min(p.duration, 600) : 0;
|
|
664
|
+
const waveform = Array.isArray(p.waveform)
|
|
665
|
+
? p.waveform
|
|
666
|
+
.filter((n) => typeof n === 'number' && isFinite(n))
|
|
667
|
+
.slice(0, 64)
|
|
668
|
+
.map(n => {
|
|
669
|
+
const v = Math.round(n);
|
|
670
|
+
return v < 0 ? 0 : v > 100 ? 100 : v;
|
|
671
|
+
})
|
|
672
|
+
: [];
|
|
673
|
+
return {
|
|
674
|
+
attachment: { kind: 'voice', ref: p.ref, duration, waveform },
|
|
675
|
+
};
|
|
676
|
+
}
|
|
677
|
+
if (type === 'video_note') {
|
|
678
|
+
if (!isEncryptedFileRef(p.ref))
|
|
679
|
+
return null;
|
|
680
|
+
const duration = typeof p.duration === 'number' && isFinite(p.duration) && p.duration > 0
|
|
681
|
+
? Math.min(p.duration, 300) : 0;
|
|
682
|
+
const shape = (typeof p.shape === 'string' && /^[a-zA-Z0-9]{1,32}$/.test(p.shape))
|
|
683
|
+
? p.shape
|
|
684
|
+
: 'circle';
|
|
685
|
+
return {
|
|
686
|
+
attachment: { kind: 'video_note', ref: p.ref, duration, shape },
|
|
687
|
+
};
|
|
688
|
+
}
|
|
689
|
+
if (type === 'gallery') {
|
|
690
|
+
if (!Array.isArray(p.items))
|
|
691
|
+
return null;
|
|
692
|
+
const items = [];
|
|
693
|
+
for (const raw of p.items) {
|
|
694
|
+
const it = parseGalleryItem(raw);
|
|
695
|
+
if (it)
|
|
696
|
+
items.push(it);
|
|
697
|
+
}
|
|
698
|
+
if (items.length < GALLERY_MIN_ITEMS)
|
|
699
|
+
return null;
|
|
700
|
+
if (items.length > GALLERY_MAX_ITEMS) {
|
|
701
|
+
items.length = GALLERY_MAX_ITEMS;
|
|
702
|
+
}
|
|
703
|
+
const caption = typeof p.caption === 'string' ? p.caption : '';
|
|
704
|
+
return {
|
|
705
|
+
caption,
|
|
706
|
+
gallery: { items },
|
|
707
|
+
};
|
|
708
|
+
}
|
|
709
|
+
return null;
|
|
710
|
+
}
|
|
711
|
+
function buildIncomingAttachment(parsed, download) {
|
|
712
|
+
const base = {
|
|
713
|
+
kind: parsed.kind,
|
|
714
|
+
fileId: parsed.ref.fileId,
|
|
715
|
+
sha256: parsed.ref.sha256,
|
|
716
|
+
mime: parsed.ref.mime,
|
|
717
|
+
size: parsed.ref.size,
|
|
718
|
+
name: parsed.ref.name ?? null,
|
|
719
|
+
virusTotalVerdict: null,
|
|
720
|
+
download,
|
|
721
|
+
};
|
|
722
|
+
if (parsed.duration !== undefined)
|
|
723
|
+
base.duration = parsed.duration;
|
|
724
|
+
if (parsed.waveform !== undefined)
|
|
725
|
+
base.waveform = parsed.waveform;
|
|
726
|
+
if (parsed.shape !== undefined)
|
|
727
|
+
base.shape = parsed.shape;
|
|
728
|
+
return base;
|
|
729
|
+
}
|
|
730
|
+
function buildIncomingGallery(parsed, makeDownload) {
|
|
731
|
+
const items = parsed.items.map(it => {
|
|
732
|
+
if (it.type === 'file') {
|
|
733
|
+
const fileAtt = { kind: 'file', ref: it.ref };
|
|
734
|
+
return { kind: 'file', attachment: buildIncomingAttachment(fileAtt, makeDownload(it.ref)) };
|
|
735
|
+
}
|
|
736
|
+
if (it.type === 'contact') {
|
|
737
|
+
const out = { kind: 'contact', userId: it.userId };
|
|
738
|
+
if (it.username !== undefined)
|
|
739
|
+
out.username = it.username;
|
|
740
|
+
if (it.displayName !== undefined)
|
|
741
|
+
out.displayName = it.displayName;
|
|
742
|
+
if (it.avatarUrl !== undefined)
|
|
743
|
+
out.avatarUrl = it.avatarUrl;
|
|
744
|
+
if (it.avatarEmoji !== undefined)
|
|
745
|
+
out.avatarUnicode = it.avatarEmoji;
|
|
746
|
+
return out;
|
|
747
|
+
}
|
|
748
|
+
return { kind: 'location', lat: it.lat, lng: it.lng };
|
|
749
|
+
});
|
|
750
|
+
return { items };
|
|
751
|
+
}
|
|
752
|
+
function parseBotAction(plaintext) {
|
|
753
|
+
if (plaintext.length === 0 || plaintext.charCodeAt(0) !== 0x7b)
|
|
754
|
+
return null;
|
|
755
|
+
let parsed;
|
|
756
|
+
try {
|
|
757
|
+
parsed = JSON.parse(plaintext);
|
|
758
|
+
}
|
|
759
|
+
catch {
|
|
760
|
+
return null;
|
|
761
|
+
}
|
|
762
|
+
if (!parsed || typeof parsed !== 'object')
|
|
763
|
+
return null;
|
|
764
|
+
const p = parsed;
|
|
765
|
+
if (p.type !== 'morok_bot_action')
|
|
766
|
+
return null;
|
|
767
|
+
if (typeof p.controlId !== 'string' || p.controlId.length === 0)
|
|
768
|
+
return null;
|
|
769
|
+
return { controlId: p.controlId };
|
|
770
|
+
}
|
|
771
|
+
export const _sniffStructuredPayload = sniffStructuredPayload;
|
|
772
|
+
export const _buildIncomingAttachment = buildIncomingAttachment;
|
|
773
|
+
//# sourceMappingURL=receive.js.map
|