@xmanrui/dsh-im 1.1.0 → 1.3.0
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/README.en.md +5 -3
- package/README.md +5 -3
- package/lib/client.js +212 -0
- package/lib/index.js +166 -163
- package/package.json +1 -1
- package/plugin-src/client/channels/whatsapp/api.js +11 -0
- package/plugin-src/client/channels/whatsapp/index.js +125 -0
- package/plugin-src/client/channels/whatsapp/styles.js +25 -0
- package/plugin-src/client/i18n.js +18 -0
- package/plugin-src/host/channels/dingtalk/production.mjs +3 -1
- package/plugin-src/host/channels/feishu/production.mjs +3 -1
- package/plugin-src/host/channels/qq/production.mjs +3 -1
- package/plugin-src/host/channels/shared/production.mjs +3 -1
- package/plugin-src/host/channels/slack/production.mjs +3 -1
- package/plugin-src/host/channels/wecom/production.mjs +3 -1
- package/plugin-src/host/channels/weixin/production.mjs +3 -1
- package/plugin-src/host/channels/whatsapp/production.mjs +3 -1
- package/plugin-src/host/channels/whatsapp/rpc.mjs +19 -1
- package/plugin-src/host/harness-session-coordinator.mjs +32 -5
- package/src/channels/dingtalk/dingtalk-api.mjs +93 -27
- package/src/channels/dingtalk/dingtalk-bridge.mjs +60 -13
- package/src/channels/discord/discord-runtime.mjs +23 -0
- package/src/channels/feishu/bridge.mjs +18 -10
- package/src/channels/feishu/message-utils.mjs +47 -0
- package/src/channels/qq/qq-bridge.mjs +80 -28
- package/src/channels/shared/file-download.mjs +64 -0
- package/src/channels/shared/harness-client.mjs +45 -0
- package/src/channels/shared/inbound-file.mjs +206 -0
- package/src/channels/shared/text-harness-bridge.mjs +31 -11
- package/src/channels/slack/slack-api.mjs +27 -4
- package/src/channels/slack/slack-runtime.mjs +55 -5
- package/src/channels/telegram/telegram-api.mjs +21 -6
- package/src/channels/telegram/telegram-runtime.mjs +24 -3
- package/src/channels/wecom/wecom-bridge.mjs +73 -10
- package/src/channels/weixin/weixin-api.mjs +45 -0
- package/src/channels/weixin/weixin-bridge.mjs +52 -18
- package/src/channels/whatsapp/config-store.mjs +43 -0
- package/src/channels/whatsapp/whatsapp-controller.mjs +22 -1
- package/src/channels/whatsapp/whatsapp-runtime.mjs +83 -2
- package/src/channels/whatsapp/whatsapp-web-session.mjs +1 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
|
+
extractWeixinFiles,
|
|
2
3
|
extractWeixinImages,
|
|
3
4
|
extractWeixinText,
|
|
4
5
|
splitWeixinText,
|
|
@@ -31,6 +32,11 @@ import {
|
|
|
31
32
|
imagePromptUserMessage,
|
|
32
33
|
promptContentForMessage,
|
|
33
34
|
} from '../shared/image-prompt.mjs';
|
|
35
|
+
import {
|
|
36
|
+
hasInboundFiles,
|
|
37
|
+
inboundFileUserMessage,
|
|
38
|
+
prefetchInboundFiles,
|
|
39
|
+
} from '../shared/inbound-file.mjs';
|
|
34
40
|
import { rememberConnectionTestTarget } from '../shared/connection-test.mjs';
|
|
35
41
|
import {
|
|
36
42
|
materializeOutboundArtifact,
|
|
@@ -49,7 +55,7 @@ const GENERIC_PROCESSING_ERROR = '消息处理失败,请稍后重试。';
|
|
|
49
55
|
const HELP_TEXT = [
|
|
50
56
|
'微信已连接 DeepSeek Harness。',
|
|
51
57
|
'',
|
|
52
|
-
'
|
|
58
|
+
'直接发送文字、图片、文件或带文字识别结果的语音即可继续当前会话。',
|
|
53
59
|
'/new 开启一个全新会话',
|
|
54
60
|
'/compact 压缩当前会话的较早上下文',
|
|
55
61
|
'/workspace 工作区绝对路径 切换工作区',
|
|
@@ -77,15 +83,33 @@ function nonEmptyString(value) {
|
|
|
77
83
|
return typeof value === 'string' && value.trim() ? value.trim() : null;
|
|
78
84
|
}
|
|
79
85
|
|
|
86
|
+
export function weixinInboundMessage(message, api) {
|
|
87
|
+
return {
|
|
88
|
+
content: extractWeixinText(message) ?? '',
|
|
89
|
+
images: typeof api?.inboundImages === 'function'
|
|
90
|
+
? api.inboundImages(message)
|
|
91
|
+
: extractWeixinImages(message),
|
|
92
|
+
files: typeof api?.inboundFiles === 'function'
|
|
93
|
+
? api.inboundFiles(message)
|
|
94
|
+
: extractWeixinFiles(message),
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
80
98
|
function hasWeixinImageItems(message) {
|
|
81
99
|
return Array.isArray(message?.item_list)
|
|
82
100
|
&& message.item_list.some((item) => item?.image_item && typeof item.image_item === 'object');
|
|
83
101
|
}
|
|
84
102
|
|
|
103
|
+
function hasWeixinFileItems(message) {
|
|
104
|
+
return Array.isArray(message?.item_list)
|
|
105
|
+
&& message.item_list.some((item) => item?.file_item && typeof item.file_item === 'object');
|
|
106
|
+
}
|
|
107
|
+
|
|
85
108
|
function canClaimInteractionReply(message, pending) {
|
|
86
109
|
return pending.questions[pending.index]
|
|
87
110
|
&& nonEmptyString(message?.from_user_id) === pending.actor
|
|
88
111
|
&& !hasWeixinImageItems(message)
|
|
112
|
+
&& !hasWeixinFileItems(message)
|
|
89
113
|
&& nonEmptyString(extractWeixinText(message));
|
|
90
114
|
}
|
|
91
115
|
|
|
@@ -204,7 +228,7 @@ export class WeixinHarnessBridge {
|
|
|
204
228
|
const runId = nonEmptyString(message?.run_id) ?? undefined;
|
|
205
229
|
const pending = this.#pendingInteractions.get(key);
|
|
206
230
|
const commandText = nonEmptyString(extractWeixinText(message)) ?? '';
|
|
207
|
-
const commandRunner = isControlCommand(commandText)
|
|
231
|
+
const commandRunner = hasWeixinFileItems(message) ? null : isControlCommand(commandText)
|
|
208
232
|
? runControlCommand
|
|
209
233
|
: (isModelCommand(commandText)
|
|
210
234
|
? runModelCommand
|
|
@@ -238,7 +262,9 @@ export class WeixinHarnessBridge {
|
|
|
238
262
|
key,
|
|
239
263
|
actor: sender,
|
|
240
264
|
messageId,
|
|
241
|
-
text: hasWeixinImageItems(message)
|
|
265
|
+
text: hasWeixinImageItems(message) || hasWeixinFileItems(message)
|
|
266
|
+
? ''
|
|
267
|
+
: extractWeixinText(message),
|
|
242
268
|
addressed: true,
|
|
243
269
|
hasPendingQuestion: Boolean(pending),
|
|
244
270
|
questionCompletion: pending?.submitting || pending?.claimedReplyMessageId
|
|
@@ -290,10 +316,16 @@ export class WeixinHarnessBridge {
|
|
|
290
316
|
releaseMessageId = true,
|
|
291
317
|
alreadyRecorded = false,
|
|
292
318
|
} = {}) {
|
|
319
|
+
const preparedMessage = message.from_user_id === this.#ownerUserId
|
|
320
|
+
? prefetchInboundFiles(
|
|
321
|
+
weixinInboundMessage(message, this.#api),
|
|
322
|
+
{ signal: this.#signal },
|
|
323
|
+
)
|
|
324
|
+
: undefined;
|
|
293
325
|
const previous = this.#queues.get(key) ?? Promise.resolve();
|
|
294
326
|
const current = previous
|
|
295
327
|
.catch(() => undefined)
|
|
296
|
-
.then(() => this.#process(message, key, { alreadyRecorded }))
|
|
328
|
+
.then(() => this.#process(message, key, { alreadyRecorded, preparedMessage }))
|
|
297
329
|
.finally(() => {
|
|
298
330
|
if (releaseMessageId) this.#acceptedMessageIds.delete(messageId);
|
|
299
331
|
if (this.#queues.get(key) === current) this.#queues.delete(key);
|
|
@@ -331,6 +363,7 @@ export class WeixinHarnessBridge {
|
|
|
331
363
|
const result = await runner(text, this.#harness, this.#state, key, {
|
|
332
364
|
signal: this.#signal,
|
|
333
365
|
hasImages: hasWeixinImageItems(message),
|
|
366
|
+
hasFiles: hasWeixinFileItems(message),
|
|
334
367
|
pendingInteraction: this.#pendingInteractions.has(key)
|
|
335
368
|
|| this.#approvals.hasPending(key),
|
|
336
369
|
control: { owner: this, key },
|
|
@@ -348,7 +381,7 @@ export class WeixinHarnessBridge {
|
|
|
348
381
|
this.#status.lastMessageError = null;
|
|
349
382
|
}
|
|
350
383
|
|
|
351
|
-
async #process(message, key, { alreadyRecorded = false } = {}) {
|
|
384
|
+
async #process(message, key, { alreadyRecorded = false, preparedMessage } = {}) {
|
|
352
385
|
this.#signal?.throwIfAborted();
|
|
353
386
|
const messageId = weixinMessageId(message);
|
|
354
387
|
const sender = nonEmptyString(message?.from_user_id);
|
|
@@ -366,38 +399,36 @@ export class WeixinHarnessBridge {
|
|
|
366
399
|
|
|
367
400
|
const contextToken = typeof message.context_token === 'string' ? message.context_token : undefined;
|
|
368
401
|
const runId = typeof message.run_id === 'string' ? message.run_id : undefined;
|
|
369
|
-
const text = extractWeixinText(message) ?? '';
|
|
370
402
|
try {
|
|
371
|
-
const
|
|
372
|
-
|
|
373
|
-
: extractWeixinImages(message);
|
|
374
|
-
const promptMessage = { content: text, images };
|
|
403
|
+
const promptMessage = preparedMessage ?? weixinInboundMessage(message, this.#api);
|
|
404
|
+
const text = promptMessage.content;
|
|
375
405
|
const hasImages = hasInboundImages(promptMessage);
|
|
376
|
-
|
|
377
|
-
|
|
406
|
+
const hasFiles = hasInboundFiles(promptMessage);
|
|
407
|
+
if (!text && !hasImages && !hasFiles) {
|
|
408
|
+
await this.#send(sender, '目前支持文字、图片、文件,以及微信已转成文字的语音消息。', contextToken, runId);
|
|
378
409
|
await this.#state.markSeen(messageId);
|
|
379
410
|
return;
|
|
380
411
|
}
|
|
381
412
|
|
|
382
413
|
const command = text.trim().toLowerCase();
|
|
383
|
-
if (!hasImages && command === '/help') {
|
|
414
|
+
if (!hasImages && !hasFiles && command === '/help') {
|
|
384
415
|
await this.#send(sender, HELP_TEXT, contextToken, runId);
|
|
385
416
|
await this.#state.markSeen(messageId);
|
|
386
417
|
return;
|
|
387
418
|
}
|
|
388
|
-
if (!hasImages && command === '/status') {
|
|
419
|
+
if (!hasImages && !hasFiles && command === '/status') {
|
|
389
420
|
await this.#harness.ensureRunning({ signal: this.#signal });
|
|
390
421
|
await this.#send(sender, '微信与 DeepSeek Harness 连接正常。', contextToken, runId);
|
|
391
422
|
await this.#state.markSeen(messageId);
|
|
392
423
|
return;
|
|
393
424
|
}
|
|
394
|
-
if (!hasImages && command === '/new') {
|
|
425
|
+
if (!hasImages && !hasFiles && command === '/new') {
|
|
395
426
|
await this.#state.clearSession(key);
|
|
396
427
|
await this.#send(sender, '已开启新会话。请发送你的问题。', contextToken, runId);
|
|
397
428
|
await this.#state.markSeen(messageId);
|
|
398
429
|
return;
|
|
399
430
|
}
|
|
400
|
-
const workspaceCommand = hasImages
|
|
431
|
+
const workspaceCommand = hasImages || hasFiles
|
|
401
432
|
? null
|
|
402
433
|
: await runWorkspaceCommand(text, this.#harness, key);
|
|
403
434
|
if (workspaceCommand) {
|
|
@@ -407,7 +438,7 @@ export class WeixinHarnessBridge {
|
|
|
407
438
|
await this.#state.markSeen(messageId);
|
|
408
439
|
return;
|
|
409
440
|
}
|
|
410
|
-
const compactCommand = hasImages
|
|
441
|
+
const compactCommand = hasImages || hasFiles
|
|
411
442
|
? null
|
|
412
443
|
: await runCompactCommand(
|
|
413
444
|
text,
|
|
@@ -446,6 +477,7 @@ export class WeixinHarnessBridge {
|
|
|
446
477
|
runId,
|
|
447
478
|
}),
|
|
448
479
|
onInteractionResolved: (resolution) => this.#handleInteractionResolved(resolution),
|
|
480
|
+
files: promptMessage.files,
|
|
449
481
|
},
|
|
450
482
|
}));
|
|
451
483
|
} finally {
|
|
@@ -490,7 +522,9 @@ export class WeixinHarnessBridge {
|
|
|
490
522
|
}
|
|
491
523
|
if (this.#signal?.aborted) return;
|
|
492
524
|
this.#status.lastError = error?.message ?? String(error);
|
|
493
|
-
const userMessage =
|
|
525
|
+
const userMessage = inboundFileUserMessage(error)
|
|
526
|
+
?? imagePromptUserMessage(error)
|
|
527
|
+
?? GENERIC_PROCESSING_ERROR;
|
|
494
528
|
this.#status.lastMessageError = safeMessageError(error, userMessage);
|
|
495
529
|
this.#logger.error?.('[dsh-weixin] failed to process an inbound message:', error);
|
|
496
530
|
try {
|
|
@@ -5,6 +5,13 @@ import { dirname } from 'node:path';
|
|
|
5
5
|
const EMPTY_DOCUMENT = Object.freeze({ version: 2, bots: Object.freeze([]) });
|
|
6
6
|
const BOT_ID_PATTERN = /^whatsapp_[a-f0-9]{24}$/;
|
|
7
7
|
const AUTH_DIRECTORY_PATTERN = /^[a-f0-9-]{36}$/;
|
|
8
|
+
const WHATSAPP_PHONE_NUMBER = /^[1-9]\d{4,14}$/;
|
|
9
|
+
|
|
10
|
+
export const WHATSAPP_ACCESS_MODES = Object.freeze({
|
|
11
|
+
selfOnly: 'self-only',
|
|
12
|
+
privateAllowlist: 'private-allowlist',
|
|
13
|
+
open: 'open',
|
|
14
|
+
});
|
|
8
15
|
|
|
9
16
|
function cleanString(value) {
|
|
10
17
|
return typeof value === 'string' && value.trim() ? value.trim() : null;
|
|
@@ -28,6 +35,35 @@ export function maskWhatsappAccount(accountJid) {
|
|
|
28
35
|
return `${digits.slice(0, 4)}••••${digits.slice(-4)}`;
|
|
29
36
|
}
|
|
30
37
|
|
|
38
|
+
export function normalizeWhatsappAllowedNumbers(value) {
|
|
39
|
+
if (value === undefined) return Object.freeze([]);
|
|
40
|
+
if (!Array.isArray(value)) {
|
|
41
|
+
throw new TypeError('allowedNumbers must be an array of WhatsApp phone numbers');
|
|
42
|
+
}
|
|
43
|
+
const normalized = value.map((entry) => {
|
|
44
|
+
const number = typeof entry === 'string' ? entry.trim().replace(/^\+/, '') : '';
|
|
45
|
+
if (!WHATSAPP_PHONE_NUMBER.test(number)) {
|
|
46
|
+
throw new TypeError('allowedNumbers contains an invalid WhatsApp phone number');
|
|
47
|
+
}
|
|
48
|
+
return number;
|
|
49
|
+
});
|
|
50
|
+
return Object.freeze([...new Set(normalized)]);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function normalizeWhatsappAccessPolicy(value = {}) {
|
|
54
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
55
|
+
throw new TypeError('WhatsApp access policy must be an object');
|
|
56
|
+
}
|
|
57
|
+
const accessMode = value.accessMode ?? WHATSAPP_ACCESS_MODES.selfOnly;
|
|
58
|
+
if (!Object.values(WHATSAPP_ACCESS_MODES).includes(accessMode)) {
|
|
59
|
+
throw new TypeError('WhatsApp accessMode is invalid');
|
|
60
|
+
}
|
|
61
|
+
return Object.freeze({
|
|
62
|
+
accessMode,
|
|
63
|
+
allowedNumbers: normalizeWhatsappAllowedNumbers(value.allowedNumbers),
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
31
67
|
export class WhatsappConfigStore {
|
|
32
68
|
#path;
|
|
33
69
|
#value = EMPTY_DOCUMENT;
|
|
@@ -112,6 +148,12 @@ export class WhatsappConfigStore {
|
|
|
112
148
|
if (!accountJid || !botId || !authDirectory || !name
|
|
113
149
|
|| !BOT_ID_PATTERN.test(botId) || !AUTH_DIRECTORY_PATTERN.test(authDirectory)
|
|
114
150
|
|| deriveWhatsappBotId(accountJid) !== botId) return null;
|
|
151
|
+
let accessPolicy;
|
|
152
|
+
try {
|
|
153
|
+
accessPolicy = normalizeWhatsappAccessPolicy(value);
|
|
154
|
+
} catch {
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
115
157
|
return Object.freeze({
|
|
116
158
|
botId,
|
|
117
159
|
accountJid,
|
|
@@ -119,6 +161,7 @@ export class WhatsappConfigStore {
|
|
|
119
161
|
name,
|
|
120
162
|
createdAt: cleanString(value.createdAt) ?? new Date().toISOString(),
|
|
121
163
|
connectedAt: cleanString(value.connectedAt),
|
|
164
|
+
...accessPolicy,
|
|
122
165
|
});
|
|
123
166
|
}
|
|
124
167
|
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
2
|
|
|
3
3
|
import { connectionTestMessage } from '../shared/connection-test.mjs';
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
deriveWhatsappBotId,
|
|
6
|
+
maskWhatsappAccount,
|
|
7
|
+
normalizeWhatsappAccessPolicy,
|
|
8
|
+
} from './config-store.mjs';
|
|
5
9
|
|
|
6
10
|
const ACTIVE_ATTEMPT_STATES = new Set(['starting', 'pending', 'connecting']);
|
|
7
11
|
const TERMINAL_ATTEMPT_STATES = new Set(['connected', 'failed', 'cancelled']);
|
|
@@ -218,6 +222,20 @@ export class WhatsappController {
|
|
|
218
222
|
});
|
|
219
223
|
}
|
|
220
224
|
|
|
225
|
+
async setAccessPolicy(botId, value) {
|
|
226
|
+
if (this.#closed) throw new Error('WhatsApp controller is closed');
|
|
227
|
+
const accessPolicy = normalizeWhatsappAccessPolicy(value);
|
|
228
|
+
await this.#withBotTransition(botId, async () => {
|
|
229
|
+
if (this.#closed) throw new Error('WhatsApp controller is closed');
|
|
230
|
+
const config = this.#configStore.get(botId);
|
|
231
|
+
if (!config) throw new Error('Unknown WhatsApp bot');
|
|
232
|
+
const saved = await this.#configStore.save({ ...config, ...accessPolicy });
|
|
233
|
+
this.#runtimes.get(botId)?.setAccessPolicy?.(saved);
|
|
234
|
+
this.#touch();
|
|
235
|
+
});
|
|
236
|
+
return this.status();
|
|
237
|
+
}
|
|
238
|
+
|
|
221
239
|
async deleteBot(botId) {
|
|
222
240
|
const config = this.#configStore.get(botId);
|
|
223
241
|
if (!config) throw new Error('Unknown WhatsApp bot');
|
|
@@ -266,6 +284,7 @@ export class WhatsappController {
|
|
|
266
284
|
messagesReceived: runtimeStatus?.messagesReceived ?? 0,
|
|
267
285
|
messagesReplied: runtimeStatus?.messagesReplied ?? 0,
|
|
268
286
|
},
|
|
287
|
+
accessPolicy: normalizeWhatsappAccessPolicy(config),
|
|
269
288
|
error: structuredClone(this.#errors.get(config.botId) ?? null),
|
|
270
289
|
};
|
|
271
290
|
});
|
|
@@ -310,6 +329,8 @@ export class WhatsappController {
|
|
|
310
329
|
name: identity.name,
|
|
311
330
|
createdAt: previous?.createdAt ?? new Date().toISOString(),
|
|
312
331
|
connectedAt: new Date().toISOString(),
|
|
332
|
+
accessMode: previous?.accessMode,
|
|
333
|
+
allowedNumbers: previous?.allowedNumbers,
|
|
313
334
|
};
|
|
314
335
|
try {
|
|
315
336
|
if (record.controller.signal.aborted || this.#closed) throw Object.assign(new Error(), { name: 'AbortError' });
|
|
@@ -10,6 +10,10 @@ import { splitMessageText } from '../shared/editable-message-stream.mjs';
|
|
|
10
10
|
import { ImagePromptError } from '../shared/image-prompt.mjs';
|
|
11
11
|
import { trackOutboundArtifactProviderPromise } from '../shared/semantic/artifact.mjs';
|
|
12
12
|
import { createWhatsappBridgeStatus, WhatsappHarnessBridge } from './whatsapp-bridge.mjs';
|
|
13
|
+
import {
|
|
14
|
+
WHATSAPP_ACCESS_MODES,
|
|
15
|
+
normalizeWhatsappAccessPolicy,
|
|
16
|
+
} from './config-store.mjs';
|
|
13
17
|
import { createWhatsappWebSession } from './whatsapp-web-session.mjs';
|
|
14
18
|
|
|
15
19
|
const IMAGE_MEDIA_TYPES = new Set(['image/jpeg', 'image/png', 'image/webp', 'image/gif']);
|
|
@@ -166,6 +170,39 @@ function whatsappImageSource(message, content, download, { viewOnce = false } =
|
|
|
166
170
|
};
|
|
167
171
|
}
|
|
168
172
|
|
|
173
|
+
function whatsappFileSource(message, content, download) {
|
|
174
|
+
const media = content?.documentMessage;
|
|
175
|
+
if (!media) return null;
|
|
176
|
+
const mediaType = typeof media.mimetype === 'string' && media.mimetype
|
|
177
|
+
? media.mimetype.toLowerCase() : undefined;
|
|
178
|
+
if (IMAGE_MEDIA_TYPES.has(mediaType)) return null;
|
|
179
|
+
return {
|
|
180
|
+
name: typeof media.fileName === 'string' && media.fileName
|
|
181
|
+
? media.fileName : 'whatsapp-file',
|
|
182
|
+
...(mediaType ? { mediaType } : {}),
|
|
183
|
+
size: mediaSize(media.fileLength),
|
|
184
|
+
async load({ signal } = {}) {
|
|
185
|
+
signal?.throwIfAborted();
|
|
186
|
+
const stream = await download(message, 'stream', { options: { signal } });
|
|
187
|
+
signal?.throwIfAborted();
|
|
188
|
+
return { stream };
|
|
189
|
+
},
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export function createWhatsappMediaDownloader({
|
|
194
|
+
socket,
|
|
195
|
+
logger = console,
|
|
196
|
+
download = downloadMediaMessage,
|
|
197
|
+
} = {}) {
|
|
198
|
+
if (typeof download !== 'function') throw new TypeError('WhatsApp media downloader is required');
|
|
199
|
+
if (typeof socket?.updateMediaMessage !== 'function') return download;
|
|
200
|
+
return (message, type, options) => download(message, type, options, {
|
|
201
|
+
logger,
|
|
202
|
+
reuploadRequest: (candidate) => socket.updateMediaMessage(candidate),
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
|
|
169
206
|
export function normalizeWhatsappMessage(message, accountJid, {
|
|
170
207
|
download = downloadMediaMessage,
|
|
171
208
|
} = {}) {
|
|
@@ -181,6 +218,7 @@ export function normalizeWhatsappMessage(message, accountJid, {
|
|
|
181
218
|
&& [remoteJid, alternateRemoteJid].some((jid) => jid && areJidsSameUser(jid, accountJid));
|
|
182
219
|
if (fromMe && !selfChat) return null;
|
|
183
220
|
const senderJid = selfChat ? accountJid : group ? message.key.participant : remoteJid;
|
|
221
|
+
const senderAlternateJid = group ? message.key.participantAlt : alternateRemoteJid;
|
|
184
222
|
if (typeof senderJid !== 'string' || !senderJid) return null;
|
|
185
223
|
const viewOnce = hasViewOnceWrapper(message.message);
|
|
186
224
|
const content = normalizeMessageContent(message.message);
|
|
@@ -190,21 +228,40 @@ export function normalizeWhatsappMessage(message, accountJid, {
|
|
|
190
228
|
const replyToSelf = typeof context?.participant === 'string'
|
|
191
229
|
&& areJidsSameUser(context.participant, accountJid);
|
|
192
230
|
const image = whatsappImageSource(message, content, download, { viewOnce });
|
|
231
|
+
const file = whatsappFileSource(message, content, download);
|
|
193
232
|
return {
|
|
194
233
|
messageId: `${remoteJid}:${messageId}`,
|
|
195
234
|
providerMessageId: messageId,
|
|
196
235
|
senderId: senderJid,
|
|
236
|
+
senderAlternateId: typeof senderAlternateJid === 'string' ? senderAlternateJid : '',
|
|
197
237
|
senderIsBot: false,
|
|
198
238
|
kind: group ? 'group' : 'direct',
|
|
199
239
|
conversationId: remoteJid,
|
|
200
240
|
content: messageText(content),
|
|
201
241
|
images: image ? [image] : [],
|
|
242
|
+
files: file ? [file] : [],
|
|
202
243
|
addressed: !group || mentioned || replyToSelf,
|
|
203
244
|
selfChat,
|
|
204
245
|
replyTarget: { jid: remoteJid, quoted: message, selfChat },
|
|
205
246
|
};
|
|
206
247
|
}
|
|
207
248
|
|
|
249
|
+
export function whatsappInboundAllowed(message, {
|
|
250
|
+
accessMode = WHATSAPP_ACCESS_MODES.selfOnly,
|
|
251
|
+
allowedNumbers = new Set(),
|
|
252
|
+
} = {}) {
|
|
253
|
+
if (accessMode === WHATSAPP_ACCESS_MODES.open) return true;
|
|
254
|
+
if (message?.kind !== 'direct') return false;
|
|
255
|
+
if (message.selfChat === true) return true;
|
|
256
|
+
if (accessMode !== WHATSAPP_ACCESS_MODES.privateAllowlist
|
|
257
|
+
|| !(allowedNumbers instanceof Set)) return false;
|
|
258
|
+
const senderJids = [message.senderId, message.senderAlternateId]
|
|
259
|
+
.filter((jid) => typeof jid === 'string' && jid.endsWith('@s.whatsapp.net'));
|
|
260
|
+
return [...allowedNumbers].some((number) => senderJids.some((jid) => (
|
|
261
|
+
areJidsSameUser(jid, `${number}@s.whatsapp.net`)
|
|
262
|
+
)));
|
|
263
|
+
}
|
|
264
|
+
|
|
208
265
|
class RecentWhatsappOutboundIds {
|
|
209
266
|
#ids = new Map();
|
|
210
267
|
|
|
@@ -390,6 +447,8 @@ export class WhatsappRuntime {
|
|
|
390
447
|
#replyTimeoutMs;
|
|
391
448
|
#connectTimeoutMs;
|
|
392
449
|
#mediaUploadTimeoutMs;
|
|
450
|
+
#accessMode;
|
|
451
|
+
#allowedPrivateNumbers;
|
|
393
452
|
#createSession;
|
|
394
453
|
#status = createWhatsappRuntimeStatus();
|
|
395
454
|
#abortController = null;
|
|
@@ -427,12 +486,21 @@ export class WhatsappRuntime {
|
|
|
427
486
|
WHATSAPP_MEDIA_UPLOAD_TIMEOUT_MS,
|
|
428
487
|
);
|
|
429
488
|
this.#createSession = createSession;
|
|
489
|
+
this.setAccessPolicy(config);
|
|
430
490
|
}
|
|
431
491
|
|
|
432
492
|
get status() {
|
|
433
493
|
return structuredClone(this.#status);
|
|
434
494
|
}
|
|
435
495
|
|
|
496
|
+
setAccessPolicy(value) {
|
|
497
|
+
const policy = normalizeWhatsappAccessPolicy(value);
|
|
498
|
+
this.#accessMode = policy.accessMode;
|
|
499
|
+
this.#allowedPrivateNumbers = new Set(policy.allowedNumbers);
|
|
500
|
+
this.#config = { ...this.#config, ...policy };
|
|
501
|
+
return policy;
|
|
502
|
+
}
|
|
503
|
+
|
|
436
504
|
async start() {
|
|
437
505
|
if (this.#status.ready && this.#session) return this.status;
|
|
438
506
|
if (this.#starting) return this.#starting;
|
|
@@ -462,10 +530,23 @@ export class WhatsappRuntime {
|
|
|
462
530
|
new Error('WhatsApp linked-device session must be scanned again'),
|
|
463
531
|
{ code: 'relink-required' },
|
|
464
532
|
)),
|
|
465
|
-
onMessage: async (raw) => {
|
|
466
|
-
const message = normalizeWhatsappMessage(raw, this.#config.accountJid
|
|
533
|
+
onMessage: async (raw, context) => {
|
|
534
|
+
const message = normalizeWhatsappMessage(raw, this.#config.accountJid, {
|
|
535
|
+
download: createWhatsappMediaDownloader({
|
|
536
|
+
socket: context?.socket,
|
|
537
|
+
logger: this.#logger,
|
|
538
|
+
}),
|
|
539
|
+
});
|
|
467
540
|
if (!message || outboundIds.has(message.providerMessageId) || !this.#bridge) return;
|
|
468
541
|
this.#status.lastCheckedAt = Date.now();
|
|
542
|
+
if (!whatsappInboundAllowed(message, {
|
|
543
|
+
accessMode: this.#accessMode,
|
|
544
|
+
allowedNumbers: this.#allowedPrivateNumbers,
|
|
545
|
+
})) {
|
|
546
|
+
this.#status.messagesRejected += 1;
|
|
547
|
+
this.#status.lastRejectedAt = new Date().toISOString();
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
469
550
|
await this.#bridge.accept(message);
|
|
470
551
|
},
|
|
471
552
|
onDisconnect: ({ error }) => {
|
|
@@ -190,7 +190,7 @@ export async function createWhatsappWebSession({
|
|
|
190
190
|
const timestamp = messageTimestampMs(message?.messageTimestamp);
|
|
191
191
|
if (timestamp === null || timestamp < sessionStartedAt - APPEND_RECENT_GRACE_MS) continue;
|
|
192
192
|
}
|
|
193
|
-
Promise.resolve(onMessage(message)).catch(() => {
|
|
193
|
+
Promise.resolve(onMessage(message, { socket: nextSocket })).catch(() => {
|
|
194
194
|
logger.error?.('[dsh-im:whatsapp] failed to process an inbound WhatsApp message');
|
|
195
195
|
});
|
|
196
196
|
}
|