feat(simplex): groups, native attachments, text batching, auto-accept

Salvage of PR #27978 cherry-picked onto current main, resolving conflicts
with main's intervening SimpleX plugin fixes (resp-envelope normalization,
health-monitor reconnect-churn fix, bare-form DM addressing).

What's new:
- Group support via SIMPLEX_GROUP_ALLOWED (comma-separated IDs or '*');
  inbound items surface chat_id=group:<id> + chat_type=group. Disabled by
  default so a bot in a group doesn't process every member's traffic.
- Inbound files/voice via rcvFileDescrReady (immediate /freceive) deferred
  through _pending_file_transfers, replayed on rcvFileComplete. Voice notes
  -> MessageType.VOICE.
- Native outbound media: send_image (PNG/JPEG + inline thumbnail), send_voice
  (msgContent.type=voice), send_video, send_document. All addressed by numeric
  ID via /_send ... json [...].
- MEDIA:<path> tags in agent replies stripped and dispatched as voice/document.
- Text-burst batching (HERMES_SIMPLEX_TEXT_BATCH_DELAY, default 0.8s).
- Auto-accept contact requests (SIMPLEX_AUTO_ACCEPT, default true).
- Group send path uses structured /_send #<id> json form (the bracket
  #[<id>] form is parsed as display-name lookup and silently drops).

plugin.yaml bumped to 1.1.0; docs updated. All inside plugins/platforms/simplex/
- no core edits.

Co-authored-by: Juraj Bednar <juraj@bednar.io>
This commit is contained in:
Juraj Bednar 2026-06-08 20:40:47 -07:00 committed by Teknium
parent a46462ec65
commit 0c2e81df00
4 changed files with 845 additions and 225 deletions

File diff suppressed because it is too large Load diff

View file

@ -1,7 +1,7 @@
name: simplex-platform
label: SimpleX Chat
kind: platform
version: 1.0.0
version: 1.1.0
description: >
SimpleX Chat gateway adapter for Hermes Agent.
Connects to a local simplex-chat daemon via WebSocket and relays
@ -9,7 +9,7 @@ description: >
SimpleX is decentralised and assigns no persistent user IDs —
every contact is an opaque internal ID generated at connection
time, making it one of the most private messengers available.
author: Mibayy
author: Mibayy, jooray
# ``requires_env`` and ``optional_env`` entries are surfaced in the
# ``hermes config`` UI via the platform-plugin env var injector in
# ``hermes_cli/config.py``.
@ -27,6 +27,18 @@ optional_env:
description: "Allow any contact to talk to the bot (dev only — disables allowlist)"
prompt: "Allow all contacts? (true/false)"
password: false
- name: SIMPLEX_AUTO_ACCEPT
description: "Auto-accept incoming contact requests (default: true)"
prompt: "Auto-accept contact requests? (true/false)"
password: false
- name: SIMPLEX_GROUP_ALLOWED
description: >-
Comma-separated SimpleX group IDs the bot should participate in, or
'*' to allow any group. Omit to ignore group messages entirely
(safer default — a bot in a group otherwise processes every
member's traffic).
prompt: "Allowed group IDs (comma-separated, or '*' for any)"
password: false
- name: SIMPLEX_HOME_CHANNEL
description: "Default contact/group ID for cron / notification delivery"
prompt: "Home channel contact/group ID (or empty)"
@ -35,3 +47,10 @@ optional_env:
description: "Human label for the home channel (defaults to the ID)"
prompt: "Home channel display name (or empty)"
password: false
- name: HERMES_SIMPLEX_TEXT_BATCH_DELAY
description: >-
Quiet-period seconds (default: 0.8) used to concatenate rapid-fire
inbound text messages into a single MessageEvent — same pattern as
Telegram's text batching.
prompt: "Text batch flush delay in seconds (default 0.8)"
password: false

View file

@ -205,6 +205,13 @@ def test_corr_id_pending_set_self_trims():
@pytest.mark.asyncio
async def test_send_dm():
"""DMs use the bare ``@<id> text`` chat-command form.
The bracketed form ``@[<id>] text`` is what the daemon's man page
documents, but in practice both addressing styles route through
the same chat-command parser; bare ``@<id>`` matches what every
Hermes deployment has been using in production for months.
"""
from gateway.config import PlatformConfig
cfg = PlatformConfig(enabled=True, extra={"ws_url": "ws://localhost:5225"})
adapter = SimplexAdapter(cfg)
@ -222,6 +229,14 @@ async def test_send_dm():
@pytest.mark.asyncio
async def test_send_group():
"""Groups use the structured ``/_send #<id> json [...]`` form.
The bracket chat-command form ``#[<id>] text`` *looks* like an exact
ID match in the daemon docs but is parsed as a display-name lookup
so messages to groups whose display name isn't literally the ID
silently drop. The structured ``/_send`` form addresses by numeric
ID and survives newlines/quoting through ``json.dumps``.
"""
from gateway.config import PlatformConfig
cfg = PlatformConfig(enabled=True, extra={"ws_url": "ws://localhost:5225"})
adapter = SimplexAdapter(cfg)
@ -231,7 +246,11 @@ async def test_send_group():
result = await adapter.send("group:grp-99", "Hello, group!")
payload = json.loads(mock_ws.send.call_args[0][0])
assert payload["cmd"] == "#[grp-99] Hello, group!"
assert payload["cmd"].startswith("/_send #grp-99 json ")
msg_content = json.loads(payload["cmd"].split(" json ", 1)[1])[0][
"msgContent"
]
assert msg_content == {"type": "text", "text": "Hello, group!"}
assert result.success is True

View file

@ -54,8 +54,11 @@ SIMPLEX_HOME_CHANNEL=<contact-id>
| `SIMPLEX_WS_URL` | Yes | WebSocket URL of the simplex-chat daemon |
| `SIMPLEX_ALLOWED_USERS` | Recommended | Comma-separated allowlist. Each entry can be a numeric `contactId` **or** a display name — both forms work. |
| `SIMPLEX_ALLOW_ALL_USERS` | Optional | Set `true` to allow every contact (use carefully) |
| `SIMPLEX_HOME_CHANNEL` | Optional | Default contact ID for cron job delivery |
| `SIMPLEX_AUTO_ACCEPT` | Optional | Auto-accept incoming contact requests (default: `true`) |
| `SIMPLEX_GROUP_ALLOWED` | Optional | Comma-separated group IDs the bot participates in, or `*` for any group. Omit to ignore group messages entirely |
| `SIMPLEX_HOME_CHANNEL` | Optional | Default contact/group ID for cron job delivery |
| `SIMPLEX_HOME_CHANNEL_NAME` | Optional | Human label for the home channel |
| `HERMES_SIMPLEX_TEXT_BATCH_DELAY` | Optional | Quiet-period seconds (default: `0.8`) used to concatenate rapid-fire inbound text messages into one event |
## Find your contact ID or display name
@ -68,6 +71,37 @@ By default **all contacts are denied**. You must either:
1. Set `SIMPLEX_ALLOWED_USERS` to a comma-separated list of `contactId`s and/or display names (e.g. `SIMPLEX_ALLOWED_USERS=4,alice` matches either contactId 4 or the contact whose display name is "alice"), or
2. Use **DM pairing** — send any message to the bot and it will reply with a pairing code. Enter that code via `hermes pairing approve simplex <CODE>`.
## Group chats
By default the adapter ignores group messages — a bot in a group otherwise
processes every member's traffic. Opt-in explicitly:
```
SIMPLEX_GROUP_ALLOWED=12,34 # specific group IDs
# or
SIMPLEX_GROUP_ALLOWED=* # any group the bot is in
```
Address groups by prefixing the chat ID with `group:`, e.g.
`simplex:group:12` in `send_message` or as a cron `deliver=` target.
## Attachments
The adapter supports native SimpleX attachments in both directions:
- **Inbound** — incoming images, voice notes, and files are accepted via
the daemon's XFTP flow (`rcvFileDescrReady``/freceive` → wait for
`rcvFileComplete`) and surfaced as `MessageEvent.media_urls` with the
appropriate `MessageType` (`PHOTO`, `VOICE`, `TEXT` + document).
- **Outbound**`send_image_file`, `send_voice`, `send_document`, and
`send_video` all use the structured `/_send` form with `filePath`, so
the receiving SimpleX client renders images inline and plays voice
notes inline rather than offering them as downloads.
Agent replies can also embed `MEDIA:/path/to/file` tags in plain text —
the adapter strips the tag from the body and sends the file as either a
voice note (audio extensions) or a document.
## Using SimpleX with cron jobs
```python