@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.
@@ -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 { getCachedBlueBubblesPrivateApiStatus } from "./probe.js";
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 = getCachedBlueBubblesPrivateApiStatus(account.accountId) !== false;
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 = [...configAllowFrom, ...storeAllowFrom]
332
- .map((entry) => String(entry).trim())
333
- .filter(Boolean);
334
- const effectiveGroupAllowFrom = [
335
- ...(configGroupAllowFrom.length > 0 ? configGroupAllowFrom : configAllowFrom),
336
- ...storeAllowFrom,
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 (isGroup) {
348
- if (groupPolicy === "disabled") {
349
- logVerbose(core, runtime, "Blocked BlueBubbles group message (groupPolicy=disabled)");
350
- logGroupAllowlistHint({
351
- runtime,
352
- reason: "groupPolicy=disabled",
353
- entry: groupAllowEntry,
354
- chatName: groupName,
355
- accountId: account.accountId,
356
- });
357
- return;
358
- }
359
- if (groupPolicy === "allowlist") {
360
- if (effectiveGroupAllowFrom.length === 0) {
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
- const allowed = isAllowedBlueBubblesSender({
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
- } else {
400
- if (dmPolicy === "disabled") {
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
- if (dmPolicy !== "open") {
406
- const allowed = isAllowedBlueBubblesSender({
407
- allowFrom: effectiveAllowFrom,
408
- sender: message.senderId,
409
- chatId: message.chatId ?? undefined,
410
- chatGuid: message.chatGuid ?? undefined,
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
- if (!allowed) {
414
- if (dmPolicy === "pairing") {
415
- const { code, created } = await core.channel.pairing.upsertPairingRequest({
416
- channel: "bluebubbles",
417
- id: message.senderId,
418
- meta: { name: message.senderName },
419
- });
420
- runtime.log?.(
421
- `[bluebubbles] pairing request sender=${message.senderId} created=${created}`,
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
- if (created) {
424
- logVerbose(core, runtime, `bluebubbles pairing request sender=${message.senderId}`);
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
- `Blocked unauthorized BlueBubbles sender ${message.senderId} (dmPolicy=${dmPolicy})`,
612
+ `bluebubbles pairing reply failed for ${message.senderId}: ${String(err)}`,
452
613
  );
453
- logVerbose(
454
- core,
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 = [...configAllowFrom, ...storeAllowFrom]
1115
- .map((entry) => String(entry).trim())
1116
- .filter(Boolean);
1117
- const effectiveGroupAllowFrom = [
1118
- ...(configGroupAllowFrom.length > 0 ? configGroupAllowFrom : configAllowFrom),
1119
- ...storeAllowFrom,
1120
- ]
1121
- .map((entry) => String(entry).trim())
1122
- .filter(Boolean);
1123
-
1124
- if (reaction.isGroup) {
1125
- if (groupPolicy === "disabled") {
1126
- return;
1127
- }
1128
- if (groupPolicy === "allowlist") {
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
- if (!allowed) {
1156
- return;
1157
- }
1158
- }
1411
+ }),
1412
+ });
1413
+ if (accessDecision.decision !== "allow") {
1414
+ return;
1159
1415
  }
1160
1416
 
1161
1417
  const chatId = reaction.chatId ?? undefined;