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:
@@ -1,5 +1,6 @@
|
||||
const discordManager = require('../discord/discordManager')
|
||||
const newsAnnounce = require('../discord/newsAnnounce')
|
||||
const teamNotify = require('../discord/teamNotify')
|
||||
const modLog = require('../discord/modLog')
|
||||
const createLogger = require('../utils/logger')
|
||||
|
||||
@@ -121,4 +122,42 @@ async function refreshCommands(req, res) {
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { setConfig, getStatus: getStatusHandler, announce, reverseModAction, refreshCommands }
|
||||
// POST /internal/team-notify — a Team notification the site has already decided
|
||||
// belongs in a channel (TEAMS.md §7.2). Body: { channel_id, stream, team_name,
|
||||
// team_url, title, body, url }.
|
||||
//
|
||||
// **The site chose the channel and the site checked the access.** Whether
|
||||
// members-only forum text may reach this channel is an acknowledgement recorded
|
||||
// against team_integration_config, and re-deciding it here would mean the bot
|
||||
// holding a copy of a policy it cannot see the inputs to.
|
||||
//
|
||||
// 503 when disconnected and 400 for a channel the bot cannot post to, matching
|
||||
// /internal/announce — the caller is one-shot and best-effort and only logs the
|
||||
// difference, but an operator debugging a silent channel needs the two to read
|
||||
// differently in the bot's log.
|
||||
async function teamNotifyHandler(req, res) {
|
||||
const connection = discordManager.getConnection()
|
||||
if (!connection) return res.status(503).json({ message: 'Bot is not connected' })
|
||||
|
||||
const { channel_id: channelId, stream, team_name: teamName, team_url: teamUrl, title, body, url } = req.body || {}
|
||||
if (!channelId || !stream) {
|
||||
return res.status(400).json({ message: 'channel_id and stream are required' })
|
||||
}
|
||||
|
||||
try {
|
||||
await teamNotify.postTeamNotification(connection.client, { channelId, stream, teamName, teamUrl, title, body, url })
|
||||
return res.json({ posted: true })
|
||||
} catch (err) {
|
||||
log.warn('team-notify failed', { message: err.message, stream, channelId })
|
||||
return res.status(400).json({ message: err.message })
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
setConfig,
|
||||
getStatus: getStatusHandler,
|
||||
announce,
|
||||
reverseModAction,
|
||||
refreshCommands,
|
||||
teamNotify: teamNotifyHandler,
|
||||
}
|
||||
|
||||
@@ -12,5 +12,6 @@ router.get('/status', ctrl.getStatus)
|
||||
router.post('/announce', ctrl.announce)
|
||||
router.post('/mod-reverse', ctrl.reverseModAction)
|
||||
router.post('/refresh-commands', ctrl.refreshCommands)
|
||||
router.post('/team-notify', ctrl.teamNotify)
|
||||
|
||||
module.exports = router
|
||||
|
||||
Reference in New Issue
Block a user