feat(relay): named routes and replies routed back to a route #2

Merged
mxm merged 4 commits from feat/named-routes into main 2026-10-14 15:58:51 +02:00
Owner

Implements the relay-bot part of mxm/homelab#36.

Named routes

  • @bot !route set <name> <url>, !route reset <name>, !route lists all routes. Names: 1–32 of a-z0-9-, not set|reset|route|name|help.
  • !route set <url> / !route reset act on default as before. The old single-route routes table is migrated at startup (one transaction) to (room_id, name) with each room's route as default.
  • Dispatch: first word of the cleaned text (also after a Display Name: prefix) names a route → post there with the rest as text; otherwise the whole text goes to default. Payload gains "route".

Who may trigger what

  • Named routes and reply routing: only command users (--command-user; everyone with --command-everyone).
  • Anyone else: a mention goes to default with its whole text (unchanged behaviour); an unmentioned reply is not forwarded. So default should stay harmless (echo).

Replies

  • /send accepts replyRoute (route name) and context (JSON object ≤ 4 KiB, default {}; context alone → 400). After sending, event_id → room, route, context goes to reply_targets in outbox.sqlite, newest 1000 entries, max 30 days. If storing fails: HTTP 500 with the sent event_id.
  • A Matrix reply (m.relates_to.m.in_reply_to, ignoring thread is_falling_back) to such an event in the same room from a command user goes to that route without a mention, with route, inReplyTo, context, and text = the reply without its > fallback. If the route was removed meanwhile, the bot answers with a notice. Other replies are ordinary messages.
  • The > fallback is now stripped from the text of every reply.

Tests

cargo fmt --check, cargo clippy --all-targets --all-features -- -D warnings, and cargo test --all-features (90 passed) with rustc/clippy 1.98.1. New tests cover parsing, migration, dispatch and permissions, reply routing, store bounds and expiry, /send validation, and reply detection from raw events. No live Matrix test yet. One open point: the code relies on the SDK's decrypted event content containing m.relates_to.

README also fixes the /send response example (event_id, as the code returns).

🤖 Generated with Claude Code

Implements the relay-bot part of mxm/homelab#36. ## Named routes - `@bot !route set <name> <url>`, `!route reset <name>`, `!route` lists all routes. Names: 1–32 of `a-z0-9-`, not `set|reset|route|name|help`. - `!route set <url>` / `!route reset` act on `default` as before. The old single-route `routes` table is migrated at startup (one transaction) to `(room_id, name)` with each room's route as `default`. - Dispatch: first word of the cleaned text (also after a `Display Name:` prefix) names a route → post there with the rest as `text`; otherwise the whole text goes to `default`. Payload gains `"route"`. ## Who may trigger what - Named routes and reply routing: only command users (`--command-user`; everyone with `--command-everyone`). - Anyone else: a mention goes to `default` with its whole text (unchanged behaviour); an unmentioned reply is not forwarded. So `default` should stay harmless (echo). ## Replies - `/send` accepts `replyRoute` (route name) and `context` (JSON object ≤ 4 KiB, default `{}`; `context` alone → 400). After sending, `event_id → room, route, context` goes to `reply_targets` in `outbox.sqlite`, newest 1000 entries, max 30 days. If storing fails: HTTP 500 with the sent `event_id`. - A Matrix reply (`m.relates_to.m.in_reply_to`, ignoring thread `is_falling_back`) to such an event in the same room from a command user goes to that route without a mention, with `route`, `inReplyTo`, `context`, and `text` = the reply without its `> ` fallback. If the route was removed meanwhile, the bot answers with a notice. Other replies are ordinary messages. - The `> ` fallback is now stripped from the text of every reply. ## Tests `cargo fmt --check`, `cargo clippy --all-targets --all-features -- -D warnings`, and `cargo test --all-features` (90 passed) with rustc/clippy 1.98.1. New tests cover parsing, migration, dispatch and permissions, reply routing, store bounds and expiry, `/send` validation, and reply detection from raw events. No live Matrix test yet. One open point: the code relies on the SDK's decrypted event content containing `m.relates_to`. README also fixes the `/send` response example (`event_id`, as the code returns). 🤖 Generated with [Claude Code](https://claude.com/claude-code)
A room can now have several routes. !route set <name> <url> adds or
changes route <name> (1 to 32 of a-z, 0-9 and -, not a command word),
!route reset <name> removes it and !route lists them all. Without a name,
!route set <url> and !route reset act on the route default, as before.

An ordinary mention whose first word (after an optional "Display Name:"
prefix) names a route goes to that route with the rest as text; any other
mention goes to default with its whole text. The payload gains "route".

Only command users reach named routes, since those carry side effects;
from anyone else a mention goes to default with its whole text, as before.
With --command-everyone, everyone may use them.

The routes table is keyed by (room_id, name). At startup a table from the
single-route version is rewritten in one transaction, each room's route
becoming its default route.

Also satisfy clippy 1.98: two nested ifs become let chains and a test
passes a slice with std::slice::from_ref.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
/send accepts replyRoute (a route name) and context (a JSON object of at
most 4 KiB, {} by default; context alone is refused). After sending, the
bot stores event ID -> room, route and context in outbox.sqlite, keeping
the newest 1000 entries for at most 30 days. If storing fails the answer
is HTTP 500 with the sent event_id.

A Matrix reply (m.relates_to.m.in_reply_to, not a thread's
is_falling_back) to such an event, in the same room and from a command
user, goes to that route without a mention. The payload adds route,
inReplyTo and context, and text is the reply without its quoted fallback.
If the route was removed meanwhile, the bot answers with a notice.
Replies to other events, and from other senders, are ordinary messages:
forwarded to default only when they mention the bot. A malformed relation
is ignored instead of failing the sync.

The quoted fallback is now stripped from the text of every reply, so the
first word of a reply that mentions the bot can name a route.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Also correct the /send response example: the field is event_id.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
mxm merged commit 95a6297259 into main 2026-10-14 15:58:51 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
mxm/matrix-relay-bot!2
No description provided.