@openclaw/bluebubbles 2026.2.13 → 2026.2.14

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.
@@ -198,6 +198,108 @@ function readFirstChatRecord(message: Record<string, unknown>): Record<string, u
198
198
  return asRecord(first);
199
199
  }
200
200
 
201
+ function extractSenderInfo(message: Record<string, unknown>): {
202
+ senderId: string;
203
+ senderName?: string;
204
+ } {
205
+ const handleValue = message.handle ?? message.sender;
206
+ const handle =
207
+ asRecord(handleValue) ?? (typeof handleValue === "string" ? { address: handleValue } : null);
208
+ const senderId =
209
+ readString(handle, "address") ??
210
+ readString(handle, "handle") ??
211
+ readString(handle, "id") ??
212
+ readString(message, "senderId") ??
213
+ readString(message, "sender") ??
214
+ readString(message, "from") ??
215
+ "";
216
+ const senderName =
217
+ readString(handle, "displayName") ??
218
+ readString(handle, "name") ??
219
+ readString(message, "senderName") ??
220
+ undefined;
221
+
222
+ return { senderId, senderName };
223
+ }
224
+
225
+ function extractChatContext(message: Record<string, unknown>): {
226
+ chatGuid?: string;
227
+ chatIdentifier?: string;
228
+ chatId?: number;
229
+ chatName?: string;
230
+ isGroup: boolean;
231
+ participants: unknown[];
232
+ } {
233
+ const chat = asRecord(message.chat) ?? asRecord(message.conversation) ?? null;
234
+ const chatFromList = readFirstChatRecord(message);
235
+ const chatGuid =
236
+ readString(message, "chatGuid") ??
237
+ readString(message, "chat_guid") ??
238
+ readString(chat, "chatGuid") ??
239
+ readString(chat, "chat_guid") ??
240
+ readString(chat, "guid") ??
241
+ readString(chatFromList, "chatGuid") ??
242
+ readString(chatFromList, "chat_guid") ??
243
+ readString(chatFromList, "guid");
244
+ const chatIdentifier =
245
+ readString(message, "chatIdentifier") ??
246
+ readString(message, "chat_identifier") ??
247
+ readString(chat, "chatIdentifier") ??
248
+ readString(chat, "chat_identifier") ??
249
+ readString(chat, "identifier") ??
250
+ readString(chatFromList, "chatIdentifier") ??
251
+ readString(chatFromList, "chat_identifier") ??
252
+ readString(chatFromList, "identifier") ??
253
+ extractChatIdentifierFromChatGuid(chatGuid);
254
+ const chatId =
255
+ readNumberLike(message, "chatId") ??
256
+ readNumberLike(message, "chat_id") ??
257
+ readNumberLike(chat, "chatId") ??
258
+ readNumberLike(chat, "chat_id") ??
259
+ readNumberLike(chat, "id") ??
260
+ readNumberLike(chatFromList, "chatId") ??
261
+ readNumberLike(chatFromList, "chat_id") ??
262
+ readNumberLike(chatFromList, "id");
263
+ const chatName =
264
+ readString(message, "chatName") ??
265
+ readString(chat, "displayName") ??
266
+ readString(chat, "name") ??
267
+ readString(chatFromList, "displayName") ??
268
+ readString(chatFromList, "name") ??
269
+ undefined;
270
+
271
+ const chatParticipants = chat ? chat["participants"] : undefined;
272
+ const messageParticipants = message["participants"];
273
+ const chatsParticipants = chatFromList ? chatFromList["participants"] : undefined;
274
+ const participants = Array.isArray(chatParticipants)
275
+ ? chatParticipants
276
+ : Array.isArray(messageParticipants)
277
+ ? messageParticipants
278
+ : Array.isArray(chatsParticipants)
279
+ ? chatsParticipants
280
+ : [];
281
+ const participantsCount = participants.length;
282
+ const groupFromChatGuid = resolveGroupFlagFromChatGuid(chatGuid);
283
+ const explicitIsGroup =
284
+ readBoolean(message, "isGroup") ??
285
+ readBoolean(message, "is_group") ??
286
+ readBoolean(chat, "isGroup") ??
287
+ readBoolean(message, "group");
288
+ const isGroup =
289
+ typeof groupFromChatGuid === "boolean"
290
+ ? groupFromChatGuid
291
+ : (explicitIsGroup ?? participantsCount > 2);
292
+
293
+ return {
294
+ chatGuid,
295
+ chatIdentifier,
296
+ chatId,
297
+ chatName,
298
+ isGroup,
299
+ participants,
300
+ };
301
+ }
302
+
201
303
  function normalizeParticipantEntry(entry: unknown): BlueBubblesParticipant | null {
202
304
  if (typeof entry === "string" || typeof entry === "number") {
203
305
  const raw = String(entry).trim();
@@ -555,84 +657,10 @@ export function normalizeWebhookMessage(
555
657
  readString(message, "subject") ??
556
658
  "";
557
659
 
558
- const handleValue = message.handle ?? message.sender;
559
- const handle =
560
- asRecord(handleValue) ?? (typeof handleValue === "string" ? { address: handleValue } : null);
561
- const senderId =
562
- readString(handle, "address") ??
563
- readString(handle, "handle") ??
564
- readString(handle, "id") ??
565
- readString(message, "senderId") ??
566
- readString(message, "sender") ??
567
- readString(message, "from") ??
568
- "";
569
-
570
- const senderName =
571
- readString(handle, "displayName") ??
572
- readString(handle, "name") ??
573
- readString(message, "senderName") ??
574
- undefined;
575
-
576
- const chat = asRecord(message.chat) ?? asRecord(message.conversation) ?? null;
577
- const chatFromList = readFirstChatRecord(message);
578
- const chatGuid =
579
- readString(message, "chatGuid") ??
580
- readString(message, "chat_guid") ??
581
- readString(chat, "chatGuid") ??
582
- readString(chat, "chat_guid") ??
583
- readString(chat, "guid") ??
584
- readString(chatFromList, "chatGuid") ??
585
- readString(chatFromList, "chat_guid") ??
586
- readString(chatFromList, "guid");
587
- const chatIdentifier =
588
- readString(message, "chatIdentifier") ??
589
- readString(message, "chat_identifier") ??
590
- readString(chat, "chatIdentifier") ??
591
- readString(chat, "chat_identifier") ??
592
- readString(chat, "identifier") ??
593
- readString(chatFromList, "chatIdentifier") ??
594
- readString(chatFromList, "chat_identifier") ??
595
- readString(chatFromList, "identifier") ??
596
- extractChatIdentifierFromChatGuid(chatGuid);
597
- const chatId =
598
- readNumberLike(message, "chatId") ??
599
- readNumberLike(message, "chat_id") ??
600
- readNumberLike(chat, "chatId") ??
601
- readNumberLike(chat, "chat_id") ??
602
- readNumberLike(chat, "id") ??
603
- readNumberLike(chatFromList, "chatId") ??
604
- readNumberLike(chatFromList, "chat_id") ??
605
- readNumberLike(chatFromList, "id");
606
- const chatName =
607
- readString(message, "chatName") ??
608
- readString(chat, "displayName") ??
609
- readString(chat, "name") ??
610
- readString(chatFromList, "displayName") ??
611
- readString(chatFromList, "name") ??
612
- undefined;
613
-
614
- const chatParticipants = chat ? chat["participants"] : undefined;
615
- const messageParticipants = message["participants"];
616
- const chatsParticipants = chatFromList ? chatFromList["participants"] : undefined;
617
- const participants = Array.isArray(chatParticipants)
618
- ? chatParticipants
619
- : Array.isArray(messageParticipants)
620
- ? messageParticipants
621
- : Array.isArray(chatsParticipants)
622
- ? chatsParticipants
623
- : [];
660
+ const { senderId, senderName } = extractSenderInfo(message);
661
+ const { chatGuid, chatIdentifier, chatId, chatName, isGroup, participants } =
662
+ extractChatContext(message);
624
663
  const normalizedParticipants = normalizeParticipantList(participants);
625
- const participantsCount = participants.length;
626
- const groupFromChatGuid = resolveGroupFlagFromChatGuid(chatGuid);
627
- const explicitIsGroup =
628
- readBoolean(message, "isGroup") ??
629
- readBoolean(message, "is_group") ??
630
- readBoolean(chat, "isGroup") ??
631
- readBoolean(message, "group");
632
- const isGroup =
633
- typeof groupFromChatGuid === "boolean"
634
- ? groupFromChatGuid
635
- : (explicitIsGroup ?? participantsCount > 2);
636
664
 
637
665
  const fromMe = readBoolean(message, "isFromMe") ?? readBoolean(message, "is_from_me");
638
666
  const messageId =
@@ -731,82 +759,8 @@ export function normalizeWebhookReaction(
731
759
  const emoji = (associatedEmoji?.trim() || mapping?.emoji) ?? `reaction:${associatedType}`;
732
760
  const action = mapping?.action ?? resolveTapbackActionHint(associatedType) ?? "added";
733
761
 
734
- const handleValue = message.handle ?? message.sender;
735
- const handle =
736
- asRecord(handleValue) ?? (typeof handleValue === "string" ? { address: handleValue } : null);
737
- const senderId =
738
- readString(handle, "address") ??
739
- readString(handle, "handle") ??
740
- readString(handle, "id") ??
741
- readString(message, "senderId") ??
742
- readString(message, "sender") ??
743
- readString(message, "from") ??
744
- "";
745
- const senderName =
746
- readString(handle, "displayName") ??
747
- readString(handle, "name") ??
748
- readString(message, "senderName") ??
749
- undefined;
750
-
751
- const chat = asRecord(message.chat) ?? asRecord(message.conversation) ?? null;
752
- const chatFromList = readFirstChatRecord(message);
753
- const chatGuid =
754
- readString(message, "chatGuid") ??
755
- readString(message, "chat_guid") ??
756
- readString(chat, "chatGuid") ??
757
- readString(chat, "chat_guid") ??
758
- readString(chat, "guid") ??
759
- readString(chatFromList, "chatGuid") ??
760
- readString(chatFromList, "chat_guid") ??
761
- readString(chatFromList, "guid");
762
- const chatIdentifier =
763
- readString(message, "chatIdentifier") ??
764
- readString(message, "chat_identifier") ??
765
- readString(chat, "chatIdentifier") ??
766
- readString(chat, "chat_identifier") ??
767
- readString(chat, "identifier") ??
768
- readString(chatFromList, "chatIdentifier") ??
769
- readString(chatFromList, "chat_identifier") ??
770
- readString(chatFromList, "identifier") ??
771
- extractChatIdentifierFromChatGuid(chatGuid);
772
- const chatId =
773
- readNumberLike(message, "chatId") ??
774
- readNumberLike(message, "chat_id") ??
775
- readNumberLike(chat, "chatId") ??
776
- readNumberLike(chat, "chat_id") ??
777
- readNumberLike(chat, "id") ??
778
- readNumberLike(chatFromList, "chatId") ??
779
- readNumberLike(chatFromList, "chat_id") ??
780
- readNumberLike(chatFromList, "id");
781
- const chatName =
782
- readString(message, "chatName") ??
783
- readString(chat, "displayName") ??
784
- readString(chat, "name") ??
785
- readString(chatFromList, "displayName") ??
786
- readString(chatFromList, "name") ??
787
- undefined;
788
-
789
- const chatParticipants = chat ? chat["participants"] : undefined;
790
- const messageParticipants = message["participants"];
791
- const chatsParticipants = chatFromList ? chatFromList["participants"] : undefined;
792
- const participants = Array.isArray(chatParticipants)
793
- ? chatParticipants
794
- : Array.isArray(messageParticipants)
795
- ? messageParticipants
796
- : Array.isArray(chatsParticipants)
797
- ? chatsParticipants
798
- : [];
799
- const participantsCount = participants.length;
800
- const groupFromChatGuid = resolveGroupFlagFromChatGuid(chatGuid);
801
- const explicitIsGroup =
802
- readBoolean(message, "isGroup") ??
803
- readBoolean(message, "is_group") ??
804
- readBoolean(chat, "isGroup") ??
805
- readBoolean(message, "group");
806
- const isGroup =
807
- typeof groupFromChatGuid === "boolean"
808
- ? groupFromChatGuid
809
- : (explicitIsGroup ?? participantsCount > 2);
762
+ const { senderId, senderName } = extractSenderInfo(message);
763
+ const { chatGuid, chatIdentifier, chatId, chatName, isGroup } = extractChatContext(message);
810
764
 
811
765
  const fromMe = readBoolean(message, "isFromMe") ?? readBoolean(message, "is_from_me");
812
766
  const timestampRaw =
@@ -32,12 +32,14 @@ import {
32
32
  resolveBlueBubblesMessageId,
33
33
  resolveReplyContextFromCache,
34
34
  } from "./monitor-reply-cache.js";
35
+ import { getCachedBlueBubblesPrivateApiStatus } from "./probe.js";
35
36
  import { normalizeBlueBubblesReactionInput, sendBlueBubblesReaction } from "./reactions.js";
36
37
  import { resolveChatGuidForTarget, sendMessageBlueBubbles } from "./send.js";
37
38
  import { formatBlueBubblesChatTarget, isAllowedBlueBubblesSender } from "./targets.js";
38
39
 
39
40
  const DEFAULT_TEXT_LIMIT = 4000;
40
41
  const invalidAckReactions = new Set<string>();
42
+ const REPLY_DIRECTIVE_TAG_RE = /\[\[\s*(?:reply_to_current|reply_to\s*:\s*[^\]\n]+)\s*\]\]/gi;
41
43
 
42
44
  export function logVerbose(
43
45
  core: BlueBubblesCoreRuntime,
@@ -110,6 +112,7 @@ export async function processMessage(
110
112
  target: WebhookTarget,
111
113
  ): Promise<void> {
112
114
  const { account, config, runtime, core, statusSink } = target;
115
+ const privateApiEnabled = getCachedBlueBubblesPrivateApiStatus(account.accountId) !== false;
113
116
 
114
117
  const groupFlag = resolveGroupFlagFromChatGuid(message.chatGuid);
115
118
  const isGroup = typeof groupFlag === "boolean" ? groupFlag : message.isGroup;
@@ -503,7 +506,15 @@ export async function processMessage(
503
506
  ? `${rawBody} ${replyTag}`
504
507
  : `${replyTag} ${rawBody}`
505
508
  : rawBody;
506
- const fromLabel = isGroup ? undefined : message.senderName || `user:${message.senderId}`;
509
+ // Build fromLabel the same way as iMessage/Signal (formatInboundFromLabel):
510
+ // group label + id for groups, sender for DMs.
511
+ // The sender identity is included in the envelope body via formatInboundEnvelope.
512
+ const senderLabel = message.senderName || `user:${message.senderId}`;
513
+ const fromLabel = isGroup
514
+ ? `${message.chatName?.trim() || "Group"} id:${peerId}`
515
+ : senderLabel !== message.senderId
516
+ ? `${senderLabel} id:${message.senderId}`
517
+ : senderLabel;
507
518
  const groupSubject = isGroup ? message.chatName?.trim() || undefined : undefined;
508
519
  const groupMembers = isGroup
509
520
  ? formatGroupMembers({
@@ -519,13 +530,15 @@ export async function processMessage(
519
530
  storePath,
520
531
  sessionKey: route.sessionKey,
521
532
  });
522
- const body = core.channel.reply.formatAgentEnvelope({
533
+ const body = core.channel.reply.formatInboundEnvelope({
523
534
  channel: "BlueBubbles",
524
535
  from: fromLabel,
525
536
  timestamp: message.timestamp,
526
537
  previousTimestamp,
527
538
  envelope: envelopeOptions,
528
539
  body: baseBody,
540
+ chatType: isGroup ? "group" : "direct",
541
+ sender: { name: message.senderName || undefined, id: message.senderId },
529
542
  });
530
543
  let chatGuidForActions = chatGuid;
531
544
  if (!chatGuidForActions && baseUrl && password) {
@@ -639,10 +652,19 @@ export async function processMessage(
639
652
  contextKey: `bluebubbles:outbound:${outboundTarget}:${trimmed}`,
640
653
  });
641
654
  };
655
+ const sanitizeReplyDirectiveText = (value: string): string => {
656
+ if (privateApiEnabled) {
657
+ return value;
658
+ }
659
+ return value
660
+ .replace(REPLY_DIRECTIVE_TAG_RE, " ")
661
+ .replace(/[ \t]+/g, " ")
662
+ .trim();
663
+ };
642
664
 
643
- const ctxPayload = {
665
+ const ctxPayload = core.channel.reply.finalizeInboundContext({
644
666
  Body: body,
645
- BodyForAgent: body,
667
+ BodyForAgent: rawBody,
646
668
  RawBody: rawBody,
647
669
  CommandBody: rawBody,
648
670
  BodyForCommands: rawBody,
@@ -677,7 +699,7 @@ export async function processMessage(
677
699
  OriginatingTo: `bluebubbles:${outboundTarget}`,
678
700
  WasMentioned: effectiveWasMentioned,
679
701
  CommandAuthorized: commandAuthorized,
680
- };
702
+ });
681
703
 
682
704
  let sentMessage = false;
683
705
  let streamingActive = false;
@@ -721,7 +743,9 @@ export async function processMessage(
721
743
  ...prefixOptions,
722
744
  deliver: async (payload, info) => {
723
745
  const rawReplyToId =
724
- typeof payload.replyToId === "string" ? payload.replyToId.trim() : "";
746
+ privateApiEnabled && typeof payload.replyToId === "string"
747
+ ? payload.replyToId.trim()
748
+ : "";
725
749
  // Resolve short ID (e.g., "5") to full UUID
726
750
  const replyToMessageGuid = rawReplyToId
727
751
  ? resolveBlueBubblesMessageId(rawReplyToId, { requireKnownShortId: true })
@@ -737,7 +761,9 @@ export async function processMessage(
737
761
  channel: "bluebubbles",
738
762
  accountId: account.accountId,
739
763
  });
740
- const text = core.channel.text.convertMarkdownTables(payload.text ?? "", tableMode);
764
+ const text = sanitizeReplyDirectiveText(
765
+ core.channel.text.convertMarkdownTables(payload.text ?? "", tableMode),
766
+ );
741
767
  let first = true;
742
768
  for (const mediaUrl of mediaList) {
743
769
  const caption = first ? text : undefined;
@@ -771,7 +797,9 @@ export async function processMessage(
771
797
  channel: "bluebubbles",
772
798
  accountId: account.accountId,
773
799
  });
774
- const text = core.channel.text.convertMarkdownTables(payload.text ?? "", tableMode);
800
+ const text = sanitizeReplyDirectiveText(
801
+ core.channel.text.convertMarkdownTables(payload.text ?? "", tableMode),
802
+ );
775
803
  const chunks =
776
804
  chunkMode === "newline"
777
805
  ? core.channel.text.chunkTextWithMode(text, textLimit, chunkMode)