@openclaw/bluebubbles 2026.2.21 → 2026.2.22
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/package.json +1 -1
- package/src/actions.test.ts +4 -11
- package/src/attachments.test.ts +91 -4
- package/src/attachments.ts +47 -15
- package/src/chat.test.ts +193 -1
- package/src/chat.ts +74 -124
- package/src/history.ts +177 -0
- package/src/monitor-normalize.test.ts +78 -0
- package/src/monitor-normalize.ts +41 -12
- package/src/monitor-processing.ts +383 -127
- package/src/monitor.test.ts +396 -4
- package/src/probe.ts +8 -0
- package/src/reactions.test.ts +4 -11
- package/src/request-url.ts +12 -0
- package/src/runtime.ts +20 -0
- package/src/send.test.ts +53 -3
- package/src/send.ts +49 -7
- package/src/targets.test.ts +19 -0
- package/src/targets.ts +46 -37
- package/src/test-harness.ts +31 -2
|
@@ -1,15 +1,21 @@
|
|
|
1
1
|
import type { OpenClawConfig } from "openclaw/plugin-sdk";
|
|
2
2
|
import {
|
|
3
3
|
createReplyPrefixOptions,
|
|
4
|
+
evictOldHistoryKeys,
|
|
4
5
|
logAckFailure,
|
|
5
6
|
logInboundDrop,
|
|
6
7
|
logTypingFailure,
|
|
8
|
+
recordPendingHistoryEntryIfEnabled,
|
|
7
9
|
resolveAckReaction,
|
|
10
|
+
resolveDmGroupAccessDecision,
|
|
11
|
+
resolveEffectiveAllowFromLists,
|
|
8
12
|
resolveControlCommandGate,
|
|
9
13
|
stripMarkdown,
|
|
14
|
+
type HistoryEntry,
|
|
10
15
|
} from "openclaw/plugin-sdk";
|
|
11
16
|
import { downloadBlueBubblesAttachment } from "./attachments.js";
|
|
12
17
|
import { markBlueBubblesChatRead, sendBlueBubblesTyping } from "./chat.js";
|
|
18
|
+
import { fetchBlueBubblesHistory } from "./history.js";
|
|
13
19
|
import { sendBlueBubblesMedia } from "./media-send.js";
|
|
14
20
|
import {
|
|
15
21
|
buildMessagePlaceholder,
|
|
@@ -33,7 +39,7 @@ import type {
|
|
|
33
39
|
BlueBubblesRuntimeEnv,
|
|
34
40
|
WebhookTarget,
|
|
35
41
|
} from "./monitor-shared.js";
|
|
36
|
-
import {
|
|
42
|
+
import { isBlueBubblesPrivateApiEnabled } from "./probe.js";
|
|
37
43
|
import { normalizeBlueBubblesReactionInput, sendBlueBubblesReaction } from "./reactions.js";
|
|
38
44
|
import { resolveChatGuidForTarget, sendMessageBlueBubbles } from "./send.js";
|
|
39
45
|
import { formatBlueBubblesChatTarget, isAllowedBlueBubblesSender } from "./targets.js";
|
|
@@ -237,12 +243,184 @@ function resolveBlueBubblesAckReaction(params: {
|
|
|
237
243
|
}
|
|
238
244
|
}
|
|
239
245
|
|
|
246
|
+
/**
|
|
247
|
+
* In-memory rolling history map keyed by account + chat identifier.
|
|
248
|
+
* Populated from incoming messages during the session.
|
|
249
|
+
* API backfill is attempted until one fetch resolves (or retries are exhausted).
|
|
250
|
+
*/
|
|
251
|
+
const chatHistories = new Map<string, HistoryEntry[]>();
|
|
252
|
+
type HistoryBackfillState = {
|
|
253
|
+
attempts: number;
|
|
254
|
+
firstAttemptAt: number;
|
|
255
|
+
nextAttemptAt: number;
|
|
256
|
+
resolved: boolean;
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
const historyBackfills = new Map<string, HistoryBackfillState>();
|
|
260
|
+
const HISTORY_BACKFILL_BASE_DELAY_MS = 5_000;
|
|
261
|
+
const HISTORY_BACKFILL_MAX_DELAY_MS = 2 * 60 * 1000;
|
|
262
|
+
const HISTORY_BACKFILL_MAX_ATTEMPTS = 6;
|
|
263
|
+
const HISTORY_BACKFILL_RETRY_WINDOW_MS = 30 * 60 * 1000;
|
|
264
|
+
const MAX_STORED_HISTORY_ENTRY_CHARS = 2_000;
|
|
265
|
+
const MAX_INBOUND_HISTORY_ENTRY_CHARS = 1_200;
|
|
266
|
+
const MAX_INBOUND_HISTORY_TOTAL_CHARS = 12_000;
|
|
267
|
+
|
|
268
|
+
function buildAccountScopedHistoryKey(accountId: string, historyIdentifier: string): string {
|
|
269
|
+
return `${accountId}\u0000${historyIdentifier}`;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function historyDedupKey(entry: HistoryEntry): string {
|
|
273
|
+
const messageId = entry.messageId?.trim();
|
|
274
|
+
if (messageId) {
|
|
275
|
+
return `id:${messageId}`;
|
|
276
|
+
}
|
|
277
|
+
return `fallback:${entry.sender}\u0000${entry.body}\u0000${entry.timestamp ?? ""}`;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function truncateHistoryBody(body: string, maxChars: number): string {
|
|
281
|
+
const trimmed = body.trim();
|
|
282
|
+
if (!trimmed) {
|
|
283
|
+
return "";
|
|
284
|
+
}
|
|
285
|
+
if (trimmed.length <= maxChars) {
|
|
286
|
+
return trimmed;
|
|
287
|
+
}
|
|
288
|
+
return `${trimmed.slice(0, maxChars).trimEnd()}...`;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function mergeHistoryEntries(params: {
|
|
292
|
+
apiEntries: HistoryEntry[];
|
|
293
|
+
currentEntries: HistoryEntry[];
|
|
294
|
+
limit: number;
|
|
295
|
+
}): HistoryEntry[] {
|
|
296
|
+
if (params.limit <= 0) {
|
|
297
|
+
return [];
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const merged: HistoryEntry[] = [];
|
|
301
|
+
const seen = new Set<string>();
|
|
302
|
+
const appendUnique = (entry: HistoryEntry) => {
|
|
303
|
+
const key = historyDedupKey(entry);
|
|
304
|
+
if (seen.has(key)) {
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
seen.add(key);
|
|
308
|
+
merged.push(entry);
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
for (const entry of params.apiEntries) {
|
|
312
|
+
appendUnique(entry);
|
|
313
|
+
}
|
|
314
|
+
for (const entry of params.currentEntries) {
|
|
315
|
+
appendUnique(entry);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
if (merged.length <= params.limit) {
|
|
319
|
+
return merged;
|
|
320
|
+
}
|
|
321
|
+
return merged.slice(merged.length - params.limit);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function pruneHistoryBackfillState(): void {
|
|
325
|
+
for (const key of historyBackfills.keys()) {
|
|
326
|
+
if (!chatHistories.has(key)) {
|
|
327
|
+
historyBackfills.delete(key);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function markHistoryBackfillResolved(historyKey: string): void {
|
|
333
|
+
const state = historyBackfills.get(historyKey);
|
|
334
|
+
if (state) {
|
|
335
|
+
state.resolved = true;
|
|
336
|
+
historyBackfills.set(historyKey, state);
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
historyBackfills.set(historyKey, {
|
|
340
|
+
attempts: 0,
|
|
341
|
+
firstAttemptAt: Date.now(),
|
|
342
|
+
nextAttemptAt: Number.POSITIVE_INFINITY,
|
|
343
|
+
resolved: true,
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function planHistoryBackfillAttempt(historyKey: string, now: number): HistoryBackfillState | null {
|
|
348
|
+
const existing = historyBackfills.get(historyKey);
|
|
349
|
+
if (existing?.resolved) {
|
|
350
|
+
return null;
|
|
351
|
+
}
|
|
352
|
+
if (existing && now - existing.firstAttemptAt > HISTORY_BACKFILL_RETRY_WINDOW_MS) {
|
|
353
|
+
markHistoryBackfillResolved(historyKey);
|
|
354
|
+
return null;
|
|
355
|
+
}
|
|
356
|
+
if (existing && existing.attempts >= HISTORY_BACKFILL_MAX_ATTEMPTS) {
|
|
357
|
+
markHistoryBackfillResolved(historyKey);
|
|
358
|
+
return null;
|
|
359
|
+
}
|
|
360
|
+
if (existing && now < existing.nextAttemptAt) {
|
|
361
|
+
return null;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const attempts = (existing?.attempts ?? 0) + 1;
|
|
365
|
+
const firstAttemptAt = existing?.firstAttemptAt ?? now;
|
|
366
|
+
const backoffDelay = Math.min(
|
|
367
|
+
HISTORY_BACKFILL_BASE_DELAY_MS * 2 ** (attempts - 1),
|
|
368
|
+
HISTORY_BACKFILL_MAX_DELAY_MS,
|
|
369
|
+
);
|
|
370
|
+
const state: HistoryBackfillState = {
|
|
371
|
+
attempts,
|
|
372
|
+
firstAttemptAt,
|
|
373
|
+
nextAttemptAt: now + backoffDelay,
|
|
374
|
+
resolved: false,
|
|
375
|
+
};
|
|
376
|
+
historyBackfills.set(historyKey, state);
|
|
377
|
+
return state;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function buildInboundHistorySnapshot(params: {
|
|
381
|
+
entries: HistoryEntry[];
|
|
382
|
+
limit: number;
|
|
383
|
+
}): Array<{ sender: string; body: string; timestamp?: number }> | undefined {
|
|
384
|
+
if (params.limit <= 0 || params.entries.length === 0) {
|
|
385
|
+
return undefined;
|
|
386
|
+
}
|
|
387
|
+
const recent = params.entries.slice(-params.limit);
|
|
388
|
+
const selected: Array<{ sender: string; body: string; timestamp?: number }> = [];
|
|
389
|
+
let remainingChars = MAX_INBOUND_HISTORY_TOTAL_CHARS;
|
|
390
|
+
|
|
391
|
+
for (let i = recent.length - 1; i >= 0; i--) {
|
|
392
|
+
const entry = recent[i];
|
|
393
|
+
const body = truncateHistoryBody(entry.body, MAX_INBOUND_HISTORY_ENTRY_CHARS);
|
|
394
|
+
if (!body) {
|
|
395
|
+
continue;
|
|
396
|
+
}
|
|
397
|
+
if (selected.length > 0 && body.length > remainingChars) {
|
|
398
|
+
break;
|
|
399
|
+
}
|
|
400
|
+
selected.push({
|
|
401
|
+
sender: entry.sender,
|
|
402
|
+
body,
|
|
403
|
+
timestamp: entry.timestamp,
|
|
404
|
+
});
|
|
405
|
+
remainingChars -= body.length;
|
|
406
|
+
if (remainingChars <= 0) {
|
|
407
|
+
break;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
if (selected.length === 0) {
|
|
412
|
+
return undefined;
|
|
413
|
+
}
|
|
414
|
+
selected.reverse();
|
|
415
|
+
return selected;
|
|
416
|
+
}
|
|
417
|
+
|
|
240
418
|
export async function processMessage(
|
|
241
419
|
message: NormalizedWebhookMessage,
|
|
242
420
|
target: WebhookTarget,
|
|
243
421
|
): Promise<void> {
|
|
244
422
|
const { account, config, runtime, core, statusSink } = target;
|
|
245
|
-
const privateApiEnabled =
|
|
423
|
+
const privateApiEnabled = isBlueBubblesPrivateApiEnabled(account.accountId);
|
|
246
424
|
|
|
247
425
|
const groupFlag = resolveGroupFlagFromChatGuid(message.chatGuid);
|
|
248
426
|
const isGroup = typeof groupFlag === "boolean" ? groupFlag : message.isGroup;
|
|
@@ -323,41 +501,51 @@ export async function processMessage(
|
|
|
323
501
|
|
|
324
502
|
const dmPolicy = account.config.dmPolicy ?? "pairing";
|
|
325
503
|
const groupPolicy = account.config.groupPolicy ?? "allowlist";
|
|
326
|
-
const configAllowFrom = (account.config.allowFrom ?? []).map((entry) => String(entry));
|
|
327
|
-
const configGroupAllowFrom = (account.config.groupAllowFrom ?? []).map((entry) => String(entry));
|
|
328
504
|
const storeAllowFrom = await core.channel.pairing
|
|
329
505
|
.readAllowFromStore("bluebubbles")
|
|
330
506
|
.catch(() => []);
|
|
331
|
-
const effectiveAllowFrom =
|
|
332
|
-
|
|
333
|
-
.
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
]
|
|
338
|
-
.map((entry) => String(entry).trim())
|
|
339
|
-
.filter(Boolean);
|
|
507
|
+
const { effectiveAllowFrom, effectiveGroupAllowFrom } = resolveEffectiveAllowFromLists({
|
|
508
|
+
allowFrom: account.config.allowFrom,
|
|
509
|
+
groupAllowFrom: account.config.groupAllowFrom,
|
|
510
|
+
storeAllowFrom,
|
|
511
|
+
dmPolicy,
|
|
512
|
+
});
|
|
340
513
|
const groupAllowEntry = formatGroupAllowlistEntry({
|
|
341
514
|
chatGuid: message.chatGuid,
|
|
342
515
|
chatId: message.chatId ?? undefined,
|
|
343
516
|
chatIdentifier: message.chatIdentifier ?? undefined,
|
|
344
517
|
});
|
|
345
518
|
const groupName = message.chatName?.trim() || undefined;
|
|
519
|
+
const accessDecision = resolveDmGroupAccessDecision({
|
|
520
|
+
isGroup,
|
|
521
|
+
dmPolicy,
|
|
522
|
+
groupPolicy,
|
|
523
|
+
effectiveAllowFrom,
|
|
524
|
+
effectiveGroupAllowFrom,
|
|
525
|
+
isSenderAllowed: (allowFrom) =>
|
|
526
|
+
isAllowedBlueBubblesSender({
|
|
527
|
+
allowFrom,
|
|
528
|
+
sender: message.senderId,
|
|
529
|
+
chatId: message.chatId ?? undefined,
|
|
530
|
+
chatGuid: message.chatGuid ?? undefined,
|
|
531
|
+
chatIdentifier: message.chatIdentifier ?? undefined,
|
|
532
|
+
}),
|
|
533
|
+
});
|
|
346
534
|
|
|
347
|
-
if (
|
|
348
|
-
if (
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
if (
|
|
535
|
+
if (accessDecision.decision !== "allow") {
|
|
536
|
+
if (isGroup) {
|
|
537
|
+
if (accessDecision.reason === "groupPolicy=disabled") {
|
|
538
|
+
logVerbose(core, runtime, "Blocked BlueBubbles group message (groupPolicy=disabled)");
|
|
539
|
+
logGroupAllowlistHint({
|
|
540
|
+
runtime,
|
|
541
|
+
reason: "groupPolicy=disabled",
|
|
542
|
+
entry: groupAllowEntry,
|
|
543
|
+
chatName: groupName,
|
|
544
|
+
accountId: account.accountId,
|
|
545
|
+
});
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
if (accessDecision.reason === "groupPolicy=allowlist (empty allowlist)") {
|
|
361
549
|
logVerbose(core, runtime, "Blocked BlueBubbles group message (no allowlist)");
|
|
362
550
|
logGroupAllowlistHint({
|
|
363
551
|
runtime,
|
|
@@ -368,14 +556,7 @@ export async function processMessage(
|
|
|
368
556
|
});
|
|
369
557
|
return;
|
|
370
558
|
}
|
|
371
|
-
|
|
372
|
-
allowFrom: effectiveGroupAllowFrom,
|
|
373
|
-
sender: message.senderId,
|
|
374
|
-
chatId: message.chatId ?? undefined,
|
|
375
|
-
chatGuid: message.chatGuid ?? undefined,
|
|
376
|
-
chatIdentifier: message.chatIdentifier ?? undefined,
|
|
377
|
-
});
|
|
378
|
-
if (!allowed) {
|
|
559
|
+
if (accessDecision.reason === "groupPolicy=allowlist (not allowlisted)") {
|
|
379
560
|
logVerbose(
|
|
380
561
|
core,
|
|
381
562
|
runtime,
|
|
@@ -395,70 +576,60 @@ export async function processMessage(
|
|
|
395
576
|
});
|
|
396
577
|
return;
|
|
397
578
|
}
|
|
579
|
+
return;
|
|
398
580
|
}
|
|
399
|
-
|
|
400
|
-
if (
|
|
581
|
+
|
|
582
|
+
if (accessDecision.reason === "dmPolicy=disabled") {
|
|
401
583
|
logVerbose(core, runtime, `Blocked BlueBubbles DM from ${message.senderId}`);
|
|
402
584
|
logVerbose(core, runtime, `drop: dmPolicy disabled sender=${message.senderId}`);
|
|
403
585
|
return;
|
|
404
586
|
}
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
chatIdentifier: message.chatIdentifier ?? undefined,
|
|
587
|
+
|
|
588
|
+
if (accessDecision.decision === "pairing") {
|
|
589
|
+
const { code, created } = await core.channel.pairing.upsertPairingRequest({
|
|
590
|
+
channel: "bluebubbles",
|
|
591
|
+
id: message.senderId,
|
|
592
|
+
meta: { name: message.senderName },
|
|
412
593
|
});
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
594
|
+
runtime.log?.(`[bluebubbles] pairing request sender=${message.senderId} created=${created}`);
|
|
595
|
+
if (created) {
|
|
596
|
+
logVerbose(core, runtime, `bluebubbles pairing request sender=${message.senderId}`);
|
|
597
|
+
try {
|
|
598
|
+
await sendMessageBlueBubbles(
|
|
599
|
+
message.senderId,
|
|
600
|
+
core.channel.pairing.buildPairingReply({
|
|
601
|
+
channel: "bluebubbles",
|
|
602
|
+
idLine: `Your BlueBubbles sender id: ${message.senderId}`,
|
|
603
|
+
code,
|
|
604
|
+
}),
|
|
605
|
+
{ cfg: config, accountId: account.accountId },
|
|
422
606
|
);
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
try {
|
|
426
|
-
await sendMessageBlueBubbles(
|
|
427
|
-
message.senderId,
|
|
428
|
-
core.channel.pairing.buildPairingReply({
|
|
429
|
-
channel: "bluebubbles",
|
|
430
|
-
idLine: `Your BlueBubbles sender id: ${message.senderId}`,
|
|
431
|
-
code,
|
|
432
|
-
}),
|
|
433
|
-
{ cfg: config, accountId: account.accountId },
|
|
434
|
-
);
|
|
435
|
-
statusSink?.({ lastOutboundAt: Date.now() });
|
|
436
|
-
} catch (err) {
|
|
437
|
-
logVerbose(
|
|
438
|
-
core,
|
|
439
|
-
runtime,
|
|
440
|
-
`bluebubbles pairing reply failed for ${message.senderId}: ${String(err)}`,
|
|
441
|
-
);
|
|
442
|
-
runtime.error?.(
|
|
443
|
-
`[bluebubbles] pairing reply failed sender=${message.senderId}: ${String(err)}`,
|
|
444
|
-
);
|
|
445
|
-
}
|
|
446
|
-
}
|
|
447
|
-
} else {
|
|
607
|
+
statusSink?.({ lastOutboundAt: Date.now() });
|
|
608
|
+
} catch (err) {
|
|
448
609
|
logVerbose(
|
|
449
610
|
core,
|
|
450
611
|
runtime,
|
|
451
|
-
`
|
|
612
|
+
`bluebubbles pairing reply failed for ${message.senderId}: ${String(err)}`,
|
|
452
613
|
);
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
runtime,
|
|
456
|
-
`drop: dm sender not allowed sender=${message.senderId} allowFrom=${effectiveAllowFrom.join(",")}`,
|
|
614
|
+
runtime.error?.(
|
|
615
|
+
`[bluebubbles] pairing reply failed sender=${message.senderId}: ${String(err)}`,
|
|
457
616
|
);
|
|
458
617
|
}
|
|
459
|
-
return;
|
|
460
618
|
}
|
|
619
|
+
return;
|
|
461
620
|
}
|
|
621
|
+
|
|
622
|
+
logVerbose(
|
|
623
|
+
core,
|
|
624
|
+
runtime,
|
|
625
|
+
`Blocked unauthorized BlueBubbles sender ${message.senderId} (dmPolicy=${dmPolicy})`,
|
|
626
|
+
);
|
|
627
|
+
logVerbose(
|
|
628
|
+
core,
|
|
629
|
+
runtime,
|
|
630
|
+
`drop: dm sender not allowed sender=${message.senderId} allowFrom=${effectiveAllowFrom.join(",")}`,
|
|
631
|
+
);
|
|
632
|
+
return;
|
|
462
633
|
}
|
|
463
634
|
|
|
464
635
|
const chatId = message.chatId ?? undefined;
|
|
@@ -813,9 +984,118 @@ export async function processMessage(
|
|
|
813
984
|
.trim();
|
|
814
985
|
};
|
|
815
986
|
|
|
987
|
+
// History: in-memory rolling map with bounded API backfill retries
|
|
988
|
+
const historyLimit = isGroup
|
|
989
|
+
? (account.config.historyLimit ?? 0)
|
|
990
|
+
: (account.config.dmHistoryLimit ?? 0);
|
|
991
|
+
|
|
992
|
+
const historyIdentifier =
|
|
993
|
+
chatGuid ||
|
|
994
|
+
chatIdentifier ||
|
|
995
|
+
(chatId ? String(chatId) : null) ||
|
|
996
|
+
(isGroup ? null : message.senderId) ||
|
|
997
|
+
"";
|
|
998
|
+
const historyKey = historyIdentifier
|
|
999
|
+
? buildAccountScopedHistoryKey(account.accountId, historyIdentifier)
|
|
1000
|
+
: "";
|
|
1001
|
+
|
|
1002
|
+
// Record the current message into rolling history
|
|
1003
|
+
if (historyKey && historyLimit > 0) {
|
|
1004
|
+
const nowMs = Date.now();
|
|
1005
|
+
const senderLabel = message.fromMe ? "me" : message.senderName || message.senderId;
|
|
1006
|
+
const normalizedHistoryBody = truncateHistoryBody(text, MAX_STORED_HISTORY_ENTRY_CHARS);
|
|
1007
|
+
const currentEntries = recordPendingHistoryEntryIfEnabled({
|
|
1008
|
+
historyMap: chatHistories,
|
|
1009
|
+
limit: historyLimit,
|
|
1010
|
+
historyKey,
|
|
1011
|
+
entry: normalizedHistoryBody
|
|
1012
|
+
? {
|
|
1013
|
+
sender: senderLabel,
|
|
1014
|
+
body: normalizedHistoryBody,
|
|
1015
|
+
timestamp: message.timestamp ?? nowMs,
|
|
1016
|
+
messageId: message.messageId ?? undefined,
|
|
1017
|
+
}
|
|
1018
|
+
: null,
|
|
1019
|
+
});
|
|
1020
|
+
pruneHistoryBackfillState();
|
|
1021
|
+
|
|
1022
|
+
const backfillAttempt = planHistoryBackfillAttempt(historyKey, nowMs);
|
|
1023
|
+
if (backfillAttempt) {
|
|
1024
|
+
try {
|
|
1025
|
+
const backfillResult = await fetchBlueBubblesHistory(historyIdentifier, historyLimit, {
|
|
1026
|
+
cfg: config,
|
|
1027
|
+
accountId: account.accountId,
|
|
1028
|
+
});
|
|
1029
|
+
if (backfillResult.resolved) {
|
|
1030
|
+
markHistoryBackfillResolved(historyKey);
|
|
1031
|
+
}
|
|
1032
|
+
if (backfillResult.entries.length > 0) {
|
|
1033
|
+
const apiEntries: HistoryEntry[] = [];
|
|
1034
|
+
for (const entry of backfillResult.entries) {
|
|
1035
|
+
const body = truncateHistoryBody(entry.body, MAX_STORED_HISTORY_ENTRY_CHARS);
|
|
1036
|
+
if (!body) {
|
|
1037
|
+
continue;
|
|
1038
|
+
}
|
|
1039
|
+
apiEntries.push({
|
|
1040
|
+
sender: entry.sender,
|
|
1041
|
+
body,
|
|
1042
|
+
timestamp: entry.timestamp,
|
|
1043
|
+
messageId: entry.messageId,
|
|
1044
|
+
});
|
|
1045
|
+
}
|
|
1046
|
+
const merged = mergeHistoryEntries({
|
|
1047
|
+
apiEntries,
|
|
1048
|
+
currentEntries:
|
|
1049
|
+
currentEntries.length > 0 ? currentEntries : (chatHistories.get(historyKey) ?? []),
|
|
1050
|
+
limit: historyLimit,
|
|
1051
|
+
});
|
|
1052
|
+
if (chatHistories.has(historyKey)) {
|
|
1053
|
+
chatHistories.delete(historyKey);
|
|
1054
|
+
}
|
|
1055
|
+
chatHistories.set(historyKey, merged);
|
|
1056
|
+
evictOldHistoryKeys(chatHistories);
|
|
1057
|
+
logVerbose(
|
|
1058
|
+
core,
|
|
1059
|
+
runtime,
|
|
1060
|
+
`backfilled ${backfillResult.entries.length} history messages for ${isGroup ? "group" : "DM"}: ${historyIdentifier}`,
|
|
1061
|
+
);
|
|
1062
|
+
} else if (!backfillResult.resolved) {
|
|
1063
|
+
const remainingAttempts = HISTORY_BACKFILL_MAX_ATTEMPTS - backfillAttempt.attempts;
|
|
1064
|
+
const nextBackoffMs = Math.max(backfillAttempt.nextAttemptAt - nowMs, 0);
|
|
1065
|
+
logVerbose(
|
|
1066
|
+
core,
|
|
1067
|
+
runtime,
|
|
1068
|
+
`history backfill unresolved for ${historyIdentifier}; retries left=${Math.max(remainingAttempts, 0)} next_in_ms=${nextBackoffMs}`,
|
|
1069
|
+
);
|
|
1070
|
+
}
|
|
1071
|
+
} catch (err) {
|
|
1072
|
+
const remainingAttempts = HISTORY_BACKFILL_MAX_ATTEMPTS - backfillAttempt.attempts;
|
|
1073
|
+
const nextBackoffMs = Math.max(backfillAttempt.nextAttemptAt - nowMs, 0);
|
|
1074
|
+
logVerbose(
|
|
1075
|
+
core,
|
|
1076
|
+
runtime,
|
|
1077
|
+
`history backfill failed for ${historyIdentifier}: ${String(err)} (retries left=${Math.max(remainingAttempts, 0)} next_in_ms=${nextBackoffMs})`,
|
|
1078
|
+
);
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
// Build inbound history from the in-memory map
|
|
1084
|
+
let inboundHistory: Array<{ sender: string; body: string; timestamp?: number }> | undefined;
|
|
1085
|
+
if (historyKey && historyLimit > 0) {
|
|
1086
|
+
const entries = chatHistories.get(historyKey);
|
|
1087
|
+
if (entries && entries.length > 0) {
|
|
1088
|
+
inboundHistory = buildInboundHistorySnapshot({
|
|
1089
|
+
entries,
|
|
1090
|
+
limit: historyLimit,
|
|
1091
|
+
});
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
|
|
816
1095
|
const ctxPayload = core.channel.reply.finalizeInboundContext({
|
|
817
1096
|
Body: body,
|
|
818
1097
|
BodyForAgent: rawBody,
|
|
1098
|
+
InboundHistory: inboundHistory,
|
|
819
1099
|
RawBody: rawBody,
|
|
820
1100
|
CommandBody: rawBody,
|
|
821
1101
|
BodyForCommands: rawBody,
|
|
@@ -1106,56 +1386,32 @@ export async function processReaction(
|
|
|
1106
1386
|
|
|
1107
1387
|
const dmPolicy = account.config.dmPolicy ?? "pairing";
|
|
1108
1388
|
const groupPolicy = account.config.groupPolicy ?? "allowlist";
|
|
1109
|
-
const configAllowFrom = (account.config.allowFrom ?? []).map((entry) => String(entry));
|
|
1110
|
-
const configGroupAllowFrom = (account.config.groupAllowFrom ?? []).map((entry) => String(entry));
|
|
1111
1389
|
const storeAllowFrom = await core.channel.pairing
|
|
1112
1390
|
.readAllowFromStore("bluebubbles")
|
|
1113
1391
|
.catch(() => []);
|
|
1114
|
-
const effectiveAllowFrom =
|
|
1115
|
-
|
|
1116
|
-
.
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
if (effectiveGroupAllowFrom.length === 0) {
|
|
1130
|
-
return;
|
|
1131
|
-
}
|
|
1132
|
-
const allowed = isAllowedBlueBubblesSender({
|
|
1133
|
-
allowFrom: effectiveGroupAllowFrom,
|
|
1134
|
-
sender: reaction.senderId,
|
|
1135
|
-
chatId: reaction.chatId ?? undefined,
|
|
1136
|
-
chatGuid: reaction.chatGuid ?? undefined,
|
|
1137
|
-
chatIdentifier: reaction.chatIdentifier ?? undefined,
|
|
1138
|
-
});
|
|
1139
|
-
if (!allowed) {
|
|
1140
|
-
return;
|
|
1141
|
-
}
|
|
1142
|
-
}
|
|
1143
|
-
} else {
|
|
1144
|
-
if (dmPolicy === "disabled") {
|
|
1145
|
-
return;
|
|
1146
|
-
}
|
|
1147
|
-
if (dmPolicy !== "open") {
|
|
1148
|
-
const allowed = isAllowedBlueBubblesSender({
|
|
1149
|
-
allowFrom: effectiveAllowFrom,
|
|
1392
|
+
const { effectiveAllowFrom, effectiveGroupAllowFrom } = resolveEffectiveAllowFromLists({
|
|
1393
|
+
allowFrom: account.config.allowFrom,
|
|
1394
|
+
groupAllowFrom: account.config.groupAllowFrom,
|
|
1395
|
+
storeAllowFrom,
|
|
1396
|
+
dmPolicy,
|
|
1397
|
+
});
|
|
1398
|
+
const accessDecision = resolveDmGroupAccessDecision({
|
|
1399
|
+
isGroup: reaction.isGroup,
|
|
1400
|
+
dmPolicy,
|
|
1401
|
+
groupPolicy,
|
|
1402
|
+
effectiveAllowFrom,
|
|
1403
|
+
effectiveGroupAllowFrom,
|
|
1404
|
+
isSenderAllowed: (allowFrom) =>
|
|
1405
|
+
isAllowedBlueBubblesSender({
|
|
1406
|
+
allowFrom,
|
|
1150
1407
|
sender: reaction.senderId,
|
|
1151
1408
|
chatId: reaction.chatId ?? undefined,
|
|
1152
1409
|
chatGuid: reaction.chatGuid ?? undefined,
|
|
1153
1410
|
chatIdentifier: reaction.chatIdentifier ?? undefined,
|
|
1154
|
-
})
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
}
|
|
1411
|
+
}),
|
|
1412
|
+
});
|
|
1413
|
+
if (accessDecision.decision !== "allow") {
|
|
1414
|
+
return;
|
|
1159
1415
|
}
|
|
1160
1416
|
|
|
1161
1417
|
const chatId = reaction.chatId ?? undefined;
|