feat: direct messages & private channels, server side (#114) #115

Merged
icub3d merged 6 commits from feature/114-direct-messages into main 2026-07-11 18:29:41 +00:00
Owner

Implements Phases 1–4 of #114 (server side). Phases 5 (private calls, blocked on the SFU work in #22–#24) and 6 (client UX) are follow-ups; this PR does not close the issue.

Summary

Private 1:1 DMs and group DMs as access-controlled channels: a channel_members pubkey ACL on ordinary channels, new dm/group_dm channel types, /api/v1/dms + /api/v1/group-dms endpoints, and ACL-gated message access and gateway delivery.

Changes

  • Storage: migration 012 rebuilds channels with channel_type + dm_key (canonical sorted-pubkey pair, unique, makes 1:1 opens idempotent) and adds the channel_members ACL table (keyed on pubkey so DMs survive leave/rejoin). New PrivateChannelStore trait + SQLite impl. Public channel-name uniqueness became a partial index — private channels are unnamed/duplicable.
  • Config: features.direct_messages (default on), membership.require_invite_for_dm (default on: DM targets must be server members), content.allow_group_dms (default on), content.max_group_dm_members (default 10).
  • REST: POST/GET /api/v1/dms, POST /api/v1/group-dms, POST /group-dms/{id}/members, DELETE /group-dms/{id}/members/{pubkey}. Existing message/reaction/thread/attachment endpoints work in private channels via the ChannelMember extractor, which now grants a fixed permission set to ACL members and 404s non-members (existence not revealed).
  • Visibility: private channels are excluded from GET /channels, the gateway HELLO channel list, and the public /channels/{id} get/update/delete surface.
  • Gateway: SUBSCRIBE to a private channel is ACL-checked (previously any member could subscribe to any channel id). New DM_CHANNEL_CREATE / DM_CHANNEL_MEMBER_ADD / DM_CHANNEL_MEMBER_REMOVE events, delivered per-user to ACL members; bot-sdk gets matching typed events/handlers.

Decisions on open questions (adjust if you disagree)

  • Group DMs are in v1.
  • require_invite_for_dm = “target must be a current server member”; when off, any valid pubkey can be in an ACL and sees the channel if they join.
  • Group DM membership: any member can add; members can only remove themselves (ownership is still an open question — no created_by is stored). A group DM whose last member leaves is deleted.
  • DM channels are reused across leave/rejoin (ACL is pubkey-keyed).

Testing

  • 155 server tests pass (17 new: storage ACL CRUD/canonicalization/cascade, DM idempotency, invite-gating, feature flags, ACL-gated message access + 404 hiding, group DM lifecycle/size limits, config parsing)
  • cargo test --workspace: all green; cargo clippy --workspace --all-targets -- -D warnings: clean
  • Client untouched (Phase 6 is a follow-up)

🤖 Generated with Claude Code

Implements Phases 1–4 of #114 (server side). Phases 5 (private calls, blocked on the SFU work in #22–#24) and 6 (client UX) are follow-ups; this PR does not close the issue. ## Summary Private 1:1 DMs and group DMs as access-controlled channels: a `channel_members` pubkey ACL on ordinary channels, new `dm`/`group_dm` channel types, `/api/v1/dms` + `/api/v1/group-dms` endpoints, and ACL-gated message access and gateway delivery. ## Changes - **Storage:** migration 012 rebuilds `channels` with `channel_type` + `dm_key` (canonical sorted-pubkey pair, unique, makes 1:1 opens idempotent) and adds the `channel_members` ACL table (keyed on pubkey so DMs survive leave/rejoin). New `PrivateChannelStore` trait + SQLite impl. Public channel-name uniqueness became a partial index — private channels are unnamed/duplicable. - **Config:** `features.direct_messages` (default on), `membership.require_invite_for_dm` (default on: DM targets must be server members), `content.allow_group_dms` (default on), `content.max_group_dm_members` (default 10). - **REST:** `POST/GET /api/v1/dms`, `POST /api/v1/group-dms`, `POST /group-dms/{id}/members`, `DELETE /group-dms/{id}/members/{pubkey}`. Existing message/reaction/thread/attachment endpoints work in private channels via the `ChannelMember` extractor, which now grants a fixed permission set to ACL members and 404s non-members (existence not revealed). - **Visibility:** private channels are excluded from `GET /channels`, the gateway HELLO channel list, and the public `/channels/{id}` get/update/delete surface. - **Gateway:** SUBSCRIBE to a private channel is ACL-checked (previously any member could subscribe to any channel id). New `DM_CHANNEL_CREATE` / `DM_CHANNEL_MEMBER_ADD` / `DM_CHANNEL_MEMBER_REMOVE` events, delivered per-user to ACL members; bot-sdk gets matching typed events/handlers. ## Decisions on open questions (adjust if you disagree) - Group DMs are in v1. - `require_invite_for_dm` = “target must be a current server member”; when off, any valid pubkey can be in an ACL and sees the channel if they join. - Group DM membership: any member can add; members can only remove **themselves** (ownership is still an open question — no `created_by` is stored). A group DM whose last member leaves is deleted. - DM channels are reused across leave/rejoin (ACL is pubkey-keyed). ## Testing - 155 server tests pass (17 new: storage ACL CRUD/canonicalization/cascade, DM idempotency, invite-gating, feature flags, ACL-gated message access + 404 hiding, group DM lifecycle/size limits, config parsing) - `cargo test --workspace`: all green; `cargo clippy --workspace --all-targets -- -D warnings`: clean - Client untouched (Phase 6 is a follow-up) 🤖 Generated with [Claude Code](https://claude.com/claude-code)
docs(design): add direct messages & private channels design (#114)
Some checks failed
Security Audit / Gitleaks (Secrets) (push) Successful in 8s
Security Audit / PNPM Audit (JS SCA) (push) Failing after 20s
Release / Server binary (linux/amd64) (push) Has been cancelled
Security Audit / Semgrep (SAST) (push) Has been cancelled
Release / Docker image (push) Has been cancelled
Security Audit / Cargo Audit (Rust SCA) (push) Has been cancelled
Security Audit / Trivy (SCA & IaC) (push) Has been cancelled
Release / Tauri client (linux/amd64) (push) Has been cancelled
0ef8e618c0
Add docs/design/direct-messages.md describing server-brokered, ACL-driven
private channels (per-server scoped, trust-the-operator, E2E deferred),
reusing the channels / membership / gateway / SFU primitives. Includes a
phased punchlist to follow over sessions.

Cross-link from overview.md and server-model.md, and add the
`direct_messages` feature flag to the server-model feature table.

Tracked in #114 (v1); E2E hardening remains #16.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
feat(gateway): ACL-gate private channel subscriptions (#114)
Some checks failed
Security Audit / Gitleaks (Secrets) (pull_request) Successful in 11s
Security Audit / PNPM Audit (JS SCA) (pull_request) Failing after 18s
Security Audit / Trivy (SCA & IaC) (pull_request) Failing after 20s
CI / Server — test & lint (pull_request) Failing after 35s
CI / Client — test & lint (pull_request) Successful in 37s
Security Audit / Semgrep (SAST) (pull_request) Failing after 53s
Security Audit / Cargo Audit (Rust SCA) (pull_request) Failing after 3m40s
ab55d49194
feat(client): direct messages UI — DM sidebar, group DMs, message-user action (#114)
Some checks failed
Security Audit / Gitleaks (Secrets) (pull_request) Successful in 8s
Security Audit / PNPM Audit (JS SCA) (pull_request) Failing after 26s
Security Audit / Trivy (SCA & IaC) (pull_request) Failing after 26s
CI / Server — test & lint (pull_request) Failing after 36s
CI / Client — test & lint (pull_request) Successful in 42s
Security Audit / Semgrep (SAST) (pull_request) Failing after 53s
Security Audit / Cargo Audit (Rust SCA) (pull_request) Failing after 3m45s
f27337c267
Author
Owner

Phase 6 (client UX) is now included in this PR (commit f27337c):

  • DM sidebar section — a "Direct Messages" area above the channel groups, listing 1:1 DMs (named after the other party) and group DMs (name + member count), with a + button opening a member picker that opens a DM (one selection) or creates a group DM with an optional name (multiple selections).
  • Message-user entry point — a "Message" item in the member context menu (shown to everyone except yourself, no moderation permissions required).
  • Group DM management — the message header for a group DM shows a members button that opens a dialog to view members, add server members, or leave (leaving is the only removal the server allows). Leaving or being removed hides the channel and re-points selection to the first public channel.
  • Gateway wiringDM_CHANNEL_CREATE / DM_CHANNEL_MEMBER_ADD / DM_CHANNEL_MEMBER_REMOVE update the store live; messages in private channels reuse the existing message flow.
  • Feature-flag handling — a 403 from GET /dms (feature disabled) hides all DM UI.

Testing: 104 client tests pass (10 new: DM gateway event handling, DM label derivation, Message menu item), tsc --noEmit and pnpm lint clean; server suite unchanged and green.

Not done from the Phase 6 punchlist: profile/mention start-DM entry points (the profile popover component is currently unwired in the app) and the private-call UI (blocked on Phase 5 / SFU).

Phase 6 (client UX) is now included in this PR (commit f27337c): - **DM sidebar section** — a "Direct Messages" area above the channel groups, listing 1:1 DMs (named after the other party) and group DMs (name + member count), with a `+` button opening a member picker that opens a DM (one selection) or creates a group DM with an optional name (multiple selections). - **Message-user entry point** — a "Message" item in the member context menu (shown to everyone except yourself, no moderation permissions required). - **Group DM management** — the message header for a group DM shows a members button that opens a dialog to view members, add server members, or leave (leaving is the only removal the server allows). Leaving or being removed hides the channel and re-points selection to the first public channel. - **Gateway wiring** — `DM_CHANNEL_CREATE` / `DM_CHANNEL_MEMBER_ADD` / `DM_CHANNEL_MEMBER_REMOVE` update the store live; messages in private channels reuse the existing message flow. - **Feature-flag handling** — a 403 from `GET /dms` (feature disabled) hides all DM UI. Testing: 104 client tests pass (10 new: DM gateway event handling, DM label derivation, Message menu item), `tsc --noEmit` and `pnpm lint` clean; server suite unchanged and green. Not done from the Phase 6 punchlist: profile/mention start-DM entry points (the profile popover component is currently unwired in the app) and the private-call UI (blocked on Phase 5 / SFU).
chore(dev): two-client dev environment and punchlist status (#114)
Some checks failed
Security Audit / Trivy (SCA & IaC) (pull_request) Failing after 19s
Security Audit / PNPM Audit (JS SCA) (pull_request) Failing after 21s
Security Audit / Gitleaks (Secrets) (pull_request) Successful in 5s
CI / Server — test & lint (pull_request) Failing after 38s
CI / Client — test & lint (pull_request) Successful in 46s
Security Audit / Semgrep (SAST) (pull_request) Failing after 58s
Security Audit / Cargo Audit (Rust SCA) (pull_request) Failing after 3m29s
8f8821ffae
- docs: mark implemented punchlist items in direct-messages.md and record
  the remaining follow-ups (profile/mention DM entry points, group DM
  ownership UI, private-call UI blocked on Phase 5)
- make dev now starts two client instances: client (com.jmarsh.client,
  port 1420, boots as alice) and client2 (com.jmarsh.client2, port 1421,
  boots as bob) so DMs can be tested end-to-end locally
- DECENTCOM_DEFAULT_ACCOUNT env var picks the default keychain account
  (by pubkey or label) per instance; the shared gnome-keyring entries are
  untouched and in-app account switching still works
- test-setup seeds/cleans WebView localStorage for both app identifiers
- client2 gets its own cargo target dir (target/client2) to avoid rebuild
  thrash from the differing embedded Tauri config
icub3d merged commit 2a362cf8a5 into main 2026-07-11 18:29:41 +00:00
icub3d deleted branch feature/114-direct-messages 2026-07-11 18:29:41 +00:00
Sign in to join this conversation.
No reviewers
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
icub3d/decentcom!115
No description provided.