mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-06 02:37:05 +08:00
Compare commits
1 Commits
dependabot
...
nanoclaw-p
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
61b65eeb0c |
@@ -460,9 +460,19 @@ class SignalAdapter(BasePlatformAdapter):
|
||||
if not data_message:
|
||||
return
|
||||
|
||||
# Check for group message
|
||||
# Check for group message.
|
||||
# Modern Signal groups surface on dataMessage.groupV2.id; legacy V1
|
||||
# groups still arrive under dataMessage.groupInfo.groupId. signal-cli
|
||||
# versions differ in which field they expose for V2 groups — some
|
||||
# forward the underlying libsignal envelope verbatim (groupV2), others
|
||||
# normalize everything into groupInfo. Read groupV2 first and fall
|
||||
# back to groupInfo so V2-only groups aren't misrouted as DMs.
|
||||
group_info = data_message.get("groupInfo")
|
||||
group_id = group_info.get("groupId") if group_info else None
|
||||
group_v2 = data_message.get("groupV2")
|
||||
group_id = (
|
||||
(group_v2.get("id") if isinstance(group_v2, dict) else None)
|
||||
or (group_info.get("groupId") if isinstance(group_info, dict) else None)
|
||||
)
|
||||
is_group = bool(group_id)
|
||||
|
||||
# Group message filtering — derived from SIGNAL_GROUP_ALLOWED_USERS:
|
||||
@@ -515,7 +525,7 @@ class SignalAdapter(BasePlatformAdapter):
|
||||
# Build session source
|
||||
source = self.build_source(
|
||||
chat_id=chat_id,
|
||||
chat_name=group_info.get("groupName") if group_info else sender_name,
|
||||
chat_name=(group_info.get("groupName") if isinstance(group_info, dict) else None) or sender_name,
|
||||
chat_type=chat_type,
|
||||
user_id=sender,
|
||||
user_name=sender_name or sender,
|
||||
|
||||
@@ -997,3 +997,162 @@ class TestSignalTypingBackoff:
|
||||
|
||||
assert "+155****4567" not in adapter._typing_failures
|
||||
assert "+155****4567" not in adapter._typing_skip_until
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Envelope handling — group routing (legacy groupInfo vs modern groupV2)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestSignalGroupV2Routing:
|
||||
"""Regression coverage for groupV2 envelope handling.
|
||||
|
||||
signal-cli's JSON-RPC ``subscribeReceive`` envelope shape has drifted across
|
||||
versions: some forward the underlying libsignal V2 envelope as
|
||||
``dataMessage.groupV2.id`` while older / normalized paths still use
|
||||
``dataMessage.groupInfo.groupId``. The adapter must read groupV2 first and
|
||||
fall back to groupInfo so V2-only groups aren't misrouted as DMs.
|
||||
|
||||
Ported from qwibitai/nanoclaw#1962 (V2 adapter improvements).
|
||||
"""
|
||||
|
||||
def _base_envelope(self, data_message: dict) -> dict:
|
||||
return {
|
||||
"envelope": {
|
||||
"sourceNumber": "+15559998888",
|
||||
"sourceUuid": "uuid-sender",
|
||||
"sourceName": "Alice",
|
||||
"timestamp": 1700000000000,
|
||||
"dataMessage": data_message,
|
||||
}
|
||||
}
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_group_v2_id_routes_as_group(self, monkeypatch):
|
||||
adapter = _make_signal_adapter(monkeypatch, group_allowed="*")
|
||||
captured = []
|
||||
|
||||
async def _capture(event):
|
||||
captured.append(event)
|
||||
|
||||
adapter.handle_message = _capture
|
||||
|
||||
env = self._base_envelope({
|
||||
"message": "hello v2",
|
||||
"groupV2": {"id": "v2group=="},
|
||||
})
|
||||
|
||||
await adapter._handle_envelope(env)
|
||||
|
||||
assert len(captured) == 1
|
||||
assert captured[0].source.chat_id == "group:v2group=="
|
||||
assert captured[0].source.chat_type == "group"
|
||||
assert captured[0].text == "hello v2"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_legacy_group_info_still_works(self, monkeypatch):
|
||||
adapter = _make_signal_adapter(monkeypatch, group_allowed="*")
|
||||
captured = []
|
||||
|
||||
async def _capture(event):
|
||||
captured.append(event)
|
||||
|
||||
adapter.handle_message = _capture
|
||||
|
||||
env = self._base_envelope({
|
||||
"message": "hello v1",
|
||||
"groupInfo": {"groupId": "legacy=="},
|
||||
})
|
||||
|
||||
await adapter._handle_envelope(env)
|
||||
|
||||
assert len(captured) == 1
|
||||
assert captured[0].source.chat_id == "group:legacy=="
|
||||
assert captured[0].source.chat_type == "group"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_group_v2_preferred_over_group_info(self, monkeypatch):
|
||||
"""When both fields are present, groupV2 wins — it's the authoritative V2 id."""
|
||||
adapter = _make_signal_adapter(monkeypatch, group_allowed="*")
|
||||
captured = []
|
||||
|
||||
async def _capture(event):
|
||||
captured.append(event)
|
||||
|
||||
adapter.handle_message = _capture
|
||||
|
||||
env = self._base_envelope({
|
||||
"message": "hello",
|
||||
"groupV2": {"id": "v2=="},
|
||||
"groupInfo": {"groupId": "v1=="},
|
||||
})
|
||||
|
||||
await adapter._handle_envelope(env)
|
||||
|
||||
assert len(captured) == 1
|
||||
assert captured[0].source.chat_id == "group:v2=="
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_no_group_fields_routes_as_dm(self, monkeypatch):
|
||||
adapter = _make_signal_adapter(monkeypatch)
|
||||
captured = []
|
||||
|
||||
async def _capture(event):
|
||||
captured.append(event)
|
||||
|
||||
adapter.handle_message = _capture
|
||||
|
||||
env = self._base_envelope({"message": "direct message"})
|
||||
|
||||
await adapter._handle_envelope(env)
|
||||
|
||||
assert len(captured) == 1
|
||||
assert captured[0].source.chat_type == "dm"
|
||||
assert captured[0].source.chat_id == "+15559998888"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_group_v2_respects_allowlist(self, monkeypatch):
|
||||
"""V2 group ids flow through the same SIGNAL_GROUP_ALLOWED_USERS filter."""
|
||||
adapter = _make_signal_adapter(monkeypatch, group_allowed="allowed-v2==")
|
||||
captured = []
|
||||
|
||||
async def _capture(event):
|
||||
captured.append(event)
|
||||
|
||||
adapter.handle_message = _capture
|
||||
|
||||
# Blocked group (not in allowlist)
|
||||
await adapter._handle_envelope(self._base_envelope({
|
||||
"message": "blocked",
|
||||
"groupV2": {"id": "blocked-v2=="},
|
||||
}))
|
||||
assert len(captured) == 0
|
||||
|
||||
# Allowed group
|
||||
await adapter._handle_envelope(self._base_envelope({
|
||||
"message": "allowed",
|
||||
"groupV2": {"id": "allowed-v2=="},
|
||||
}))
|
||||
assert len(captured) == 1
|
||||
assert captured[0].source.chat_id == "group:allowed-v2=="
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_malformed_group_fields_fall_through_to_dm(self, monkeypatch):
|
||||
"""Non-dict groupV2 / groupInfo shouldn't crash — treat as DM."""
|
||||
adapter = _make_signal_adapter(monkeypatch)
|
||||
captured = []
|
||||
|
||||
async def _capture(event):
|
||||
captured.append(event)
|
||||
|
||||
adapter.handle_message = _capture
|
||||
|
||||
env = self._base_envelope({
|
||||
"message": "malformed",
|
||||
"groupV2": "not-a-dict",
|
||||
"groupInfo": 42,
|
||||
})
|
||||
|
||||
await adapter._handle_envelope(env)
|
||||
|
||||
assert len(captured) == 1
|
||||
assert captured[0].source.chat_type == "dm"
|
||||
|
||||
Reference in New Issue
Block a user