feat(mentions): @here/@channel politeness gate (LC-476) #464

Merged
longjacksonle merged 4 commits from feat/LC-476-broadcast-mention-gate into main 2026-06-27 23:56:16 +02:00

What

Add the politeness gate for @here / @channel (LC-476). The broadcast mentions themselves already worked end to end (resolve on send, write rows, fan out Mentioned over WS/push/email, respect mute) - the only missing piece was controlling who may use them, to curb abuse.

How

  • New per-room policy rooms.broadcast_allowed_for (migration 0069): all (default, preserves today's behavior) / moderators_only / admins_only, mirroring posting_allowed_for. Read on demand via a getter (not carried on the Room struct), like retention_days.
  • Enforcement at the resolver chokepoint: resolve_tokens_for_room drops @here/@channel to plain text when the author's effective room role doesn't satisfy the policy. Covers send, edit, and every reconcile path. Synthetic actors (webhook / email / bridge) bypass - they're operator/owner-configured, not a member abuse vector.
  • Honest UI: the mention autocomplete stops offering the tokens, and the broadcast-count preview shows zero, when the viewer isn't allowed - so the composer never suggests something the send path would silently drop.
  • Manage UI: a "Broadcast mentions" card next to Posting policy; POST /room/{id}/broadcast-policy reuses the posting-policy handler shape (same require_can_manage gate, mod_actions audit, htmx-fragment / redirect dual response). i18n in en + es.

Tests

New gate tests in routes_broadcast_mentions.rs: admins_only drops a plain member's @channel (zero rows) while the admin still broadcasts; default all lets a member broadcast. just check, just test, just test-saas all green (ran check last).

Notes

  • Default all means no behavior change on upgrade - existing rooms keep open broadcast until an admin restricts them. Additive migration, no operator action.

Verify

just verify builds the release binary clean; boot fails only on the missing SSO env (environmental, as in prior PRs).

## What Add the politeness gate for `@here` / `@channel` (LC-476). The broadcast mentions themselves already worked end to end (resolve on send, write rows, fan out `Mentioned` over WS/push/email, respect mute) - the only missing piece was controlling **who** may use them, to curb abuse. ## How - **New per-room policy** `rooms.broadcast_allowed_for` (migration 0069): `all` (default, preserves today's behavior) / `moderators_only` / `admins_only`, mirroring `posting_allowed_for`. Read on demand via a getter (not carried on the `Room` struct), like `retention_days`. - **Enforcement at the resolver chokepoint**: `resolve_tokens_for_room` drops `@here`/`@channel` to plain text when the author's effective room role doesn't satisfy the policy. Covers send, edit, and every reconcile path. Synthetic actors (webhook / email / bridge) bypass - they're operator/owner-configured, not a member abuse vector. - **Honest UI**: the mention autocomplete stops offering the tokens, and the broadcast-count preview shows zero, when the viewer isn't allowed - so the composer never suggests something the send path would silently drop. - **Manage UI**: a "Broadcast mentions" card next to Posting policy; `POST /room/{id}/broadcast-policy` reuses the posting-policy handler shape (same `require_can_manage` gate, `mod_actions` audit, htmx-fragment / redirect dual response). i18n in en + es. ## Tests New gate tests in `routes_broadcast_mentions.rs`: `admins_only` drops a plain member's `@channel` (zero rows) while the admin still broadcasts; default `all` lets a member broadcast. `just check`, `just test`, `just test-saas` all green (ran check last). ## Notes - Default `all` means **no behavior change on upgrade** - existing rooms keep open broadcast until an admin restricts them. Additive migration, no operator action. ## Verify `just verify` builds the release binary clean; boot fails only on the missing SSO env (environmental, as in prior PRs).
Per-room politeness gate for @here / @channel. Migration 0069 adds the column (default 'all', CHECK in {all, moderators_only, admins_only}); get/set accessors mirror the retention/posting-policy pattern and read on demand rather than riding the Room struct.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
resolve_tokens_for_room drops @here/@channel to plain text when the room's broadcast policy doesn't permit the author (moderators_only / admins_only), via effective room role; 'all' and synthetic actors (webhook/email/bridge) bypass. The autocomplete stops suggesting the tokens and the broadcast-count preview shows zero when the viewer isn't allowed, so the UI never offers what the send path would silently drop.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A "Broadcast mentions" card on the room Manage page (next to Posting policy) flips the gate between Everyone / Moderators only / Admins only. POST /room/{id}/broadcast-policy mirrors the posting-policy handler: same require_can_manage gate, mod_actions audit row, and htmx-fragment / redirect dual response. i18n in en + es.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
test(mentions): cover the broadcast politeness gate (LC-476)
Some checks failed
check-secrets / Kingfisher (push) Failing after 4s
check-secrets / TruffleHog (push) Successful in 4s
check-secrets / Nosey parker (push) Successful in 4s
check-secrets / Nosey parker (pull_request) Successful in 6s
check-secrets / TruffleHog (pull_request) Successful in 5s
check-secrets / Kingfisher (pull_request) Failing after 6s
Check / clippy + fmt + tests (pull_request) Successful in 2m37s
Create release / Create release from merged PR (pull_request) Has been skipped
b571d25ff5
admins_only drops a plain member's @channel (zero rows) while the admin still broadcasts; the default 'all' policy lets a member broadcast.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
longjacksonle deleted branch feat/LC-476-broadcast-mention-gate 2026-06-27 23:56:16 +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!464
No description provided.