fix(bluebubbles): apply the require_mention gate before downloading attachments
`_handle_webhook` ran `_collect_attachments` (one REST download per attachment) before the `is_group and self.require_mention` check, so every attachment on an unmentioned group message was fetched onto the host and then dropped. The gate only needs the text, which is available pre-download, so it now runs first; the "(attachment)" placeholder and the missing-fields check follow the download as before. Same class as the Teams fix in this PR (review follow-up). One invariant test: unmentioned group record with an attachment -> 0 `_download_attachment` calls, mentioned -> 1 and dispatched.
This commit is contained in:
@@ -579,19 +579,21 @@ class BlueBubblesAdapter(BasePlatformAdapter):
|
||||
if isinstance(assoc_type, int) and assoc_type in _TAPBACK_CODES: # tapback reactions delivered as messages
|
||||
return _ok()
|
||||
text = self._value(record.get("text"), record.get("message"), record.get("body")) or ""
|
||||
media_urls, media_types, msg_type = await self._collect_attachments(record)
|
||||
if not text and media_urls:
|
||||
text = "(attachment)"
|
||||
chat_guid, chat_identifier, sender = self._resolve_chat_and_sender(payload, record)
|
||||
if not sender or not (chat_guid or chat_identifier) or not text:
|
||||
return web.json_response({"error": "missing message fields"}, status=400)
|
||||
session_chat_id = chat_guid or chat_identifier
|
||||
is_group = bool(record.get("isGroup")) or (";+;" in (chat_guid or ""))
|
||||
# Mention gate BEFORE the attachment downloads: an unmentioned group message must not
|
||||
# pull every attachment through the REST API only to be dropped.
|
||||
if is_group and self.require_mention:
|
||||
if not self._message_matches_mention_patterns(text):
|
||||
logger.debug("[bluebubbles] ignoring group message (require_mention=true, no mention pattern matched)")
|
||||
return _ok()
|
||||
text = self._clean_mention_text(text)
|
||||
media_urls, media_types, msg_type = await self._collect_attachments(record)
|
||||
if not text and media_urls:
|
||||
text = "(attachment)"
|
||||
if not sender or not (chat_guid or chat_identifier) or not text:
|
||||
return web.json_response({"error": "missing message fields"}, status=400)
|
||||
source = self.build_source(chat_id=session_chat_id, chat_name=chat_identifier or sender,
|
||||
chat_type="group" if is_group else "dm", user_id=sender, user_name=sender,
|
||||
chat_id_alt=chat_identifier)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""Tests for the BlueBubbles iMessage gateway adapter."""
|
||||
import asyncio
|
||||
import json
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
@@ -567,3 +568,41 @@ class TestBlueBubblesTimeoutErrorNormalization:
|
||||
assert "500 Internal Server Error" in (result.error or "")
|
||||
|
||||
|
||||
|
||||
|
||||
class TestBlueBubblesGateBeforeDownload:
|
||||
"""The require_mention gate must run BEFORE attachments are downloaded (review follow-up)."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("text, downloads, handled_count", [
|
||||
("look at this", 0, 0), # unmentioned group attachment: never fetched
|
||||
("hermes look at this", 1, 1), # mentioned: fetched and dispatched
|
||||
])
|
||||
async def test_unmentioned_group_attachment_is_not_downloaded(
|
||||
self, monkeypatch, text, downloads, handled_count):
|
||||
adapter = _make_adapter(monkeypatch, require_mention=True, send_read_receipts=False)
|
||||
handled = []
|
||||
|
||||
async def fake_handle_message(event):
|
||||
handled.append(event)
|
||||
|
||||
download = AsyncMock(return_value="/tmp/cached.jpg")
|
||||
monkeypatch.setattr(adapter, "handle_message", fake_handle_message)
|
||||
monkeypatch.setattr(adapter, "_download_attachment", download)
|
||||
response = await adapter._handle_webhook(_FakeBlueBubblesRequest({
|
||||
"type": "new-message",
|
||||
"data": {
|
||||
"guid": "msg-att-1",
|
||||
"text": text,
|
||||
"handle": {"address": "+15555550100"},
|
||||
"isFromMe": False,
|
||||
"isGroup": True,
|
||||
"chats": [{"guid": "iMessage;+;group-chat"}],
|
||||
"attachments": [{"guid": "att-1", "mimeType": "image/jpeg"}],
|
||||
},
|
||||
}))
|
||||
await asyncio.sleep(0)
|
||||
|
||||
assert response.status == 200
|
||||
assert download.await_count == downloads
|
||||
assert len(handled) == handled_count
|
||||
|
||||
Reference in New Issue
Block a user