feat(integration): slash commands framework (LC-76) #165

Merged
nrupard merged 2 commits from feat/lc-76-slash-commands into main 2026-05-20 19:54:03 +02:00
Owner

Summary

Slash-command framework (LC-76). Typing /<command> in the compose box runs a command instead of posting literal text. Built-ins: /help, /me, /shrug, /poll (LC-66), /remind (LC-63), plus admin-defined custom commands.

Design

  • Dispatch: post_message routes a leading-/ body through routes::slash::try_dispatch, which runs after every access / posting / rate-limit gate (no RBAC bypass). Returns None for unrecognized commands -> falls back to posting the literal text (commands can't grief a stray /).
  • Registry: crate::commands holds built-in metadata (name, description, usage, admin_only) - one source of truth for /help + autocomplete. Execution in routes::slash.
  • Results: /help renders an ephemeral panel OOB into #lc-command-result (invoker-only, never broadcast, no DB write). /me posts an italic action; /shrug appends the glyph. /poll reuses polls::create_poll; /remind posts the note + sets an LC-63 reminder on it for the author.
  • Custom commands (migration 0039, slash_commands_custom): managed at /admin/slash-commands. static_text substitutes {args}; webhook_post POSTs args as JSON and posts the (capped) response. admin_only refused for non-admins.
  • Autocomplete: GET /api/slash-commands?q= returns a role-filtered dropdown, wired into the composer with a /word trigger + click-to-insert (parallel to @mention).

Acceptance criteria

  • /help lists registered commands with descriptions.
  • /me <text> posts a third-person styled message.
  • /remind and /poll invoke their features.
  • Auto-complete shows matching commands + usage.
  • Unknown /foo falls back to literal text.
  • Custom commands (webhook-POST + static-text).
  • Slash authorization respects RBAC (admin-only refused for users).
  • Ephemeral results render only to the invoker, never broadcast.

/giphy omitted (no Giphy integration shipped, per the issue's "if shipped").

Tests

routes_slash.rs: /help ephemeral (no message), /me, /shrug, /remind (post + reminder), unknown fallback, custom static_text, admin-only RBAC, autocomplete. Migration 0039 appended to hand-rolled lists. just check, just test, just test-saas green.

🤖 Generated with Claude Code

## Summary Slash-command framework (LC-76). Typing `/<command>` in the compose box runs a command instead of posting literal text. Built-ins: `/help`, `/me`, `/shrug`, `/poll` (LC-66), `/remind` (LC-63), plus admin-defined custom commands. ## Design - **Dispatch**: `post_message` routes a leading-`/` body through `routes::slash::try_dispatch`, which runs **after** every access / posting / rate-limit gate (no RBAC bypass). Returns `None` for unrecognized commands -> falls back to posting the literal text (commands can't grief a stray `/`). - **Registry**: `crate::commands` holds built-in metadata (name, description, usage, `admin_only`) - one source of truth for `/help` + autocomplete. Execution in `routes::slash`. - **Results**: `/help` renders an **ephemeral** panel OOB into `#lc-command-result` (invoker-only, never broadcast, no DB write). `/me` posts an italic action; `/shrug` appends the glyph. `/poll` reuses `polls::create_poll`; `/remind` posts the note + sets an LC-63 reminder on it for the author. - **Custom commands** (migration `0039`, `slash_commands_custom`): managed at `/admin/slash-commands`. `static_text` substitutes `{args}`; `webhook_post` POSTs args as JSON and posts the (capped) response. `admin_only` refused for non-admins. - **Autocomplete**: `GET /api/slash-commands?q=` returns a role-filtered dropdown, wired into the composer with a `/word` trigger + click-to-insert (parallel to `@mention`). ## Acceptance criteria - [x] `/help` lists registered commands with descriptions. - [x] `/me <text>` posts a third-person styled message. - [x] `/remind` and `/poll` invoke their features. - [x] Auto-complete shows matching commands + usage. - [x] Unknown `/foo` falls back to literal text. - [x] Custom commands (webhook-POST + static-text). - [x] Slash authorization respects RBAC (admin-only refused for users). - [x] Ephemeral results render only to the invoker, never broadcast. `/giphy` omitted (no Giphy integration shipped, per the issue's "if shipped"). ## Tests `routes_slash.rs`: `/help` ephemeral (no message), `/me`, `/shrug`, `/remind` (post + reminder), unknown fallback, custom static_text, admin-only RBAC, autocomplete. Migration `0039` appended to hand-rolled lists. `just check`, `just test`, `just test-saas` green. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
feat(integration): slash commands framework (LC-76)
Some checks failed
Check / clippy + fmt + tests (pull_request) Failing after 4s
1d837f49b4
Typing /<command> in the compose box now runs a command instead of posting literal text. Built-ins: /help, /me, /shrug, /poll (LC-66), /remind (LC-63). Plus admin-defined custom commands. Unknown /foo falls back to posting the literal text, so commands never grief a user who happens to type a slash.

Dispatch: post_message routes a leading-/ body through routes::slash::try_dispatch, which runs AFTER every access / posting / rate-limit gate, so a command cannot bypass room RBAC. try_dispatch returns None for unrecognized commands (fall through to a normal message) and Some(response) when handled.

Built-in registry: crate::commands holds the built-in metadata (name, description, usage, admin_only) - the single source of truth for /help and the autocomplete dropdown. Execution lives in routes::slash:
- /help renders an ephemeral panel OOB into #lc-command-result (visible only to the invoker, never broadcast, no DB write).
- /me posts an italic third-person action; /shrug appends the shrug glyph.
- /poll reuses polls::parse_command + create_poll; /remind posts the note and sets a LC-63 reminder on it for the author at the parsed time (15m/1h/3h/1d).

Custom commands (migration 0039, slash_commands_custom): admins add them on /admin/slash-commands. static_text substitutes {args} into a template; webhook_post POSTs the args as JSON to a URL and posts the (capped) response body. admin_only commands are refused for non-admins (RBAC acceptance). Admin CRUD page mirrors the link-filter pattern (validate + re-render on error, redirect on success, mod-log entries).

Autocomplete: GET /api/slash-commands?q= returns a dropdown of matching commands (role-filtered), wired into the composer with a `/word` trigger + click-to-insert, parallel to the @mention typeahead.

Tests: routes_slash.rs covers /help ephemeral (not broadcast, no message), /me, /shrug, /remind (posts + reminder), unknown fallback to literal, custom static_text substitution, admin-only RBAC rejection, and the autocomplete endpoint. Migration 0039 appended to the hand-rolled migration lists. just check, just test, just test-saas all green.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
fix(slash): SSRF guard on webhook command URLs; overflow-safe duration (LC-76)
Some checks failed
Check / clippy + fmt + tests (pull_request) Failing after 5s
cd181a3b7a
webhook_post custom commands made an outbound POST to an admin-supplied URL with only an http(s) scheme check, so an admin could point one at a loopback / private / link-local address or a cloud metadata endpoint (169.254.169.254). Add webhook_url_ok: require http(s), reject localhost / *.internal hosts and IP literals in loopback, private, link-local, unspecified, or 0.0.0.0/8 ranges (v4 + v6). Enforced both at admin save time and again at execution time (defense in depth). Hostnames that are not IP literals still pass (admin-trusted; full DNS-resolution checks are out of scope for v1).

Also make parse_duration_minutes overflow-safe (checked_mul) so a huge /remind duration returns an error instead of panicking in debug.

Tests: unit tests for webhook_url_ok (accepts public, rejects loopback/private/metadata/internal/bad-scheme) and duration parsing (incl. the no-overflow case); an integration test that an admin-only command is hidden from a non-admin's autocomplete.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
nrupard deleted branch feat/lc-76-slash-commands 2026-05-20 19:54:03 +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
psa-systems/lets-chat!165
No description provided.