feat(teams): phase 8 — the notifications bridge, and the gate §7.2 could not check
The same Team event as §6, delivered a third time: push, email, and now a Discord channel the operator configured. Not a second pipeline — teamNotify.js already computed the recipient set once, so the bridge is a sink beside the two that were there. The design's gate has no data source. §7.2 bridges an event only if "its visibility is public, or its destination channel is configured for a members-only Team context". The four team.* streams carry no visibility; forum threads have no public/members column because a forum is members-only by construction; and core cannot see a Discord channel's permissions. So §7.2's own example config names exactly the two events that are never public. The gate is therefore an attributed operator acknowledgement, in the shape teams_forum_uploads_ack already uses. It is a precondition — 422, not a quiet drop at delivery — it is re-asked at delivery as well as at the save, and changing the channel clears it, because an acknowledgement is about a destination and cannot survive the destination changing underneath it. The design's DDL cannot hold its own default row: MariaDB coerces every PRIMARY KEY column to NOT NULL, so `team_id NULL` — the deployment-wide default every override overrides — is unrepresentable. Proved on a real MariaDB (error 1048). Replaced with a surrogate id, a generated team_key AS IFNULL(team_id, 0) in the unique key, and the foreign key the original had no room for. One-shot, not queued: "identical to announce and mod-reverse" names two different reliability models, and a Team notification is the moment it describes. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1305,6 +1305,54 @@ CREATE TABLE IF NOT EXISTS team_notification_prefs (
|
||||
INDEX idx_tnp_digest (email_mode, last_digest_at)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- ── The integration bridge's configuration (TEAMS.md §7.2, phase 8) ─────────
|
||||
--
|
||||
-- The SAME events as §6, delivered to a second consumer. Not a second pipeline:
|
||||
-- `utils/teamNotify.js` computes the recipient set once and hands the event to
|
||||
-- push, to email and now to this bridge.
|
||||
--
|
||||
-- `team_id NULL` is the deployment-wide default and a per-Team row overrides it,
|
||||
-- which is what §7.2 asks for — but its `PRIMARY KEY (platform, team_id)` cannot
|
||||
-- express it: MariaDB coerces every PRIMARY KEY column to NOT NULL, so the
|
||||
-- default row is unrepresentable and the whole override mechanism has no base
|
||||
-- case. Hence the surrogate key plus a generated `team_key`, the same trick
|
||||
-- `teams.active_key` and `content_reports.open_marker` use: IFNULL folds the
|
||||
-- default row onto 0, which no `teams.id` can be, so one default and one row per
|
||||
-- Team coexist under a single UNIQUE key. It also buys the foreign key the
|
||||
-- original DDL had no room for — without it, deleting a Team leaves its bridge
|
||||
-- config behind to be inherited by the next Team that lands on the id.
|
||||
--
|
||||
-- **`members_ack` is a precondition, not a preference.** Forum posts and
|
||||
-- announcements are members-only ALWAYS — there is no public forum thread, and
|
||||
-- §7.2's gate ("visibility is public, or the channel is configured for a
|
||||
-- members-only context") has no data source on either side: the streams carry no
|
||||
-- visibility and core cannot see a Discord channel's permissions. Only the
|
||||
-- operator can. So enabling a members-only event requires an explicit, attributed
|
||||
-- acknowledgement that the destination is restricted to that Team, recorded the
|
||||
-- way `teams_forum_uploads_ack` records the image-policy one. Changing the channel
|
||||
-- CLEARS it (see the model): an acknowledgement is about a destination, and it
|
||||
-- cannot survive the destination changing underneath it.
|
||||
CREATE TABLE IF NOT EXISTS team_integration_config (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
platform VARCHAR(32) NOT NULL, -- 'discord'; opaque here, phase 10 makes it a registry key
|
||||
team_id INT NULL, -- NULL = the deployment-wide default
|
||||
events JSON NOT NULL, -- ['team.announcement','team.forum.post']
|
||||
channel_ref VARCHAR(64) NULL, -- destination on that platform, opaque to core
|
||||
enabled TINYINT(1) NOT NULL DEFAULT 0,
|
||||
-- The §7.2 gate, as an operator assertion with a name against it.
|
||||
members_ack TINYINT(1) NOT NULL DEFAULT 0,
|
||||
members_ack_by INT NULL,
|
||||
members_ack_at DATETIME NULL,
|
||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
team_key INT AS (IFNULL(team_id, 0)) STORED,
|
||||
UNIQUE KEY uq_tic_platform_team (platform, team_key),
|
||||
CONSTRAINT fk_tic_team FOREIGN KEY (team_id) REFERENCES teams(id) ON DELETE CASCADE,
|
||||
-- SET NULL rather than CASCADE, for the same reason every other snapshot in
|
||||
-- this file is: deleting the admin's account must not silently un-acknowledge a
|
||||
-- policy and start withholding messages the deployment is configured to send.
|
||||
CONSTRAINT fk_tic_ack_by FOREIGN KEY (members_ack_by) REFERENCES users(id) ON DELETE SET NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- Migrations for databases created before the wiki upgrade. Each statement uses
|
||||
-- IF NOT EXISTS so re-running on every boot is a harmless no-op. New installs get
|
||||
-- these columns from the CREATE TABLE above; existing installs get them here.
|
||||
|
||||
Reference in New Issue
Block a user