mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-25 00:51:20 +00:00
fix(gateway/bluebubbles): fall back to data.chats[0].guid when chatGuid missing
BlueBubbles v1.9+ webhook payloads for new-message events do not always include a top-level chatGuid field on the message data object. Instead, the chat GUID is nested under data.chats[0].guid. The adapter currently checks five top-level fallback locations (record and payload, snake_case and camelCase, plus payload.guid) but never looks inside the chats array. When none of those top-level fields contain the GUID, the adapter falls through to using the sender's phone/email as the session chat ID. This causes two observable bugs when a user is a participant in both a DM and a group chat with the bot: 1. DM and group sessions merge. Every message from that user ends up with the same session_chat_id (their own address), so the bot cannot distinguish which thread the message came from. 2. Outbound routing becomes ambiguous. _resolve_chat_guid() iterates all chats and returns the first one where the address appears as a participant; group chats typically sort ahead of DMs by activity, so replies and cron messages intended for the DM can land in a group. This was observed in production: a user's morning brief cron delivered to a group chat with his spouse instead of his DM thread. The fix adds a single fallback that extracts chat_guid from record["chats"][0]["guid"] when the top-level fields are empty. The chats array is included in every new-message webhook payload in BB v1.9.9 (verified against a live server). It is backwards compatible: if a future BB version starts including chatGuid at the top level, that still wins. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
064f8d74de
commit
8b52356849
2 changed files with 63 additions and 0 deletions
|
|
@ -835,6 +835,12 @@ class BlueBubblesAdapter(BasePlatformAdapter):
|
|||
payload.get("chat_guid"),
|
||||
payload.get("guid"),
|
||||
)
|
||||
# Fallback: BlueBubbles v1.9+ webhook payloads omit top-level chatGuid;
|
||||
# the chat GUID is nested under data.chats[0].guid instead.
|
||||
if not chat_guid:
|
||||
_chats = record.get("chats") or []
|
||||
if _chats and isinstance(_chats[0], dict):
|
||||
chat_guid = _chats[0].get("guid") or _chats[0].get("chatGuid")
|
||||
chat_identifier = self._value(
|
||||
record.get("chatIdentifier"),
|
||||
record.get("identifier"),
|
||||
|
|
|
|||
|
|
@ -167,6 +167,63 @@ class TestBlueBubblesWebhookParsing:
|
|||
chat_identifier = sender
|
||||
assert chat_identifier == "user@example.com"
|
||||
|
||||
def test_webhook_extracts_chat_guid_from_chats_array_dm(self, monkeypatch):
|
||||
"""BB v1.9+ webhook payloads omit top-level chatGuid; GUID is in chats[0].guid."""
|
||||
adapter = _make_adapter(monkeypatch)
|
||||
payload = {
|
||||
"type": "new-message",
|
||||
"data": {
|
||||
"guid": "MESSAGE-GUID",
|
||||
"text": "hello",
|
||||
"handle": {"address": "+15551234567"},
|
||||
"isFromMe": False,
|
||||
"chats": [
|
||||
{"guid": "any;-;+15551234567", "chatIdentifier": "+15551234567"}
|
||||
],
|
||||
},
|
||||
}
|
||||
record = adapter._extract_payload_record(payload) or {}
|
||||
chat_guid = adapter._value(
|
||||
record.get("chatGuid"),
|
||||
payload.get("chatGuid"),
|
||||
record.get("chat_guid"),
|
||||
payload.get("chat_guid"),
|
||||
payload.get("guid"),
|
||||
)
|
||||
if not chat_guid:
|
||||
_chats = record.get("chats") or []
|
||||
if _chats and isinstance(_chats[0], dict):
|
||||
chat_guid = _chats[0].get("guid") or _chats[0].get("chatGuid")
|
||||
assert chat_guid == "any;-;+15551234567"
|
||||
|
||||
def test_webhook_extracts_chat_guid_from_chats_array_group(self, monkeypatch):
|
||||
"""Group chat GUIDs contain ;+; and must be extracted from chats array."""
|
||||
adapter = _make_adapter(monkeypatch)
|
||||
payload = {
|
||||
"type": "new-message",
|
||||
"data": {
|
||||
"guid": "MESSAGE-GUID",
|
||||
"text": "hello everyone",
|
||||
"handle": {"address": "+15551234567"},
|
||||
"isFromMe": False,
|
||||
"isGroup": True,
|
||||
"chats": [{"guid": "any;+;chat-uuid-abc123"}],
|
||||
},
|
||||
}
|
||||
record = adapter._extract_payload_record(payload) or {}
|
||||
chat_guid = adapter._value(
|
||||
record.get("chatGuid"),
|
||||
payload.get("chatGuid"),
|
||||
record.get("chat_guid"),
|
||||
payload.get("chat_guid"),
|
||||
payload.get("guid"),
|
||||
)
|
||||
if not chat_guid:
|
||||
_chats = record.get("chats") or []
|
||||
if _chats and isinstance(_chats[0], dict):
|
||||
chat_guid = _chats[0].get("guid") or _chats[0].get("chatGuid")
|
||||
assert chat_guid == "any;+;chat-uuid-abc123"
|
||||
|
||||
def test_extract_payload_record_accepts_list_data(self, monkeypatch):
|
||||
adapter = _make_adapter(monkeypatch)
|
||||
payload = {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue