Slack support alongside Discord - #23
Draft
anthonybaldwin wants to merge 6 commits into
Draft
anthonybaldwin wants to merge 6 commits into
anthonybaldwin wants to merge 6 commits into
Conversation
Squawk drives one chat platform per deployment. PLATFORM=discord or PLATFORM=slack picks it (inferred from whichever bot token is set), and every feature — polling, threaded incidents, pinning, ghosting, and all six commands — works the same on either. All bot logic previously lived in src/index.ts bound directly to discord.js, so the lifecycle first had to stop knowing which chat service it talks to: - config/state/icons/render: env, persistence, favicon caching and rendering, none of them Discord-aware. render emits a structural Embed plus inline markup through a per-platform TextFormat, so Discord markdown and Slack mrkdwn come out of one set of render functions. - core: incident lifecycle and command handlers, driven only through platform/types.ts. Platform errors meaning "this is gone" arrive as null returns, so the core prunes state without knowing any error codes. - platform/discord.ts: the existing behavior, unchanged, behind the seam. - platform/slack.ts: Socket Mode, so Slack needs no public HTTP endpoint and deploys exactly like the Discord bot. Embeds become Block Kit attachments; threads are thread_ts replies; update IDs travel as message metadata. Thread archiving, pin notices and bot presence do not exist on Slack, and the core skips them via capability flags. Slack command names are unique per workspace and cannot be registered over an API, so the six slash commands become subcommands of a single manifest-declared command (/squawk by default, SLACK_COMMAND_NAME). Slack also has no per-command permission model, so SLACK_ADMIN_USER_IDS gates the destructive subcommands the way Manage Server does on Discord. core.test.ts exercises the full lifecycle against an in-memory platform to hold the Discord behavior unchanged through the split. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GLa4zi5Jmyb6HrGCTme2m4
Adds docs/wiki/Slack-Setup.md (app manifest, scopes, both tokens, admin gating, and a table of what Slack has no equivalent for) and reworks the existing pages that assumed Discord: Architecture's module map, the Configuration variable tables, per-command Slack syntax, the platform-scoped state file, and the two-instance deployment note. Also fixes a startup crash the new template would have made worse: a `.env` exports every key it lists, so a placeholder `DISCORD_GUILD_ID=` arrived as "" and failed its optional non-empty string. Blank values are now treated as unset. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GLa4zi5Jmyb6HrGCTme2m4
discord.js routes a one-ID bulkDelete to a plain delete but returns the message only when it is in cache, so /clean could report zero deletions and leave state pointing at a message it had just removed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GLa4zi5Jmyb6HrGCTme2m4
Slack fails a whole chat.postMessage with invalid_blocks when an image element's URL is malformed, so a status page with an odd favicon (a data: URI, a protocol-relative href) would take down every incident post for that monitor. Discord just omits an icon it cannot render; match that by keeping only plain http(s) URLs. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GLa4zi5Jmyb6HrGCTme2m4
Slack reports a private channel the app was never invited to as channel_not_found, indistinguishable from a wrong ID, so the old message sent people to re-check a channel ID that was already correct. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GLa4zi5Jmyb6HrGCTme2m4
Resolves the wiki conflicts with #24, which corrected stale content across the same pages this branch rewrites. Kept #24's fixes: - The mermaid data-flow diagram (<br/> rather than \n in quoted labels, and a quoted "normalized Incident[]" edge label so the brackets are not parsed as a node) — this branch had reintroduced both faults. - Presence rotation lists three activities, not four. - AGENTS.md no longer claims a retryWithBackoff helper that does not exist; status page adapters throw and the poll loop retries next cycle. - The generic Dockerfile snippet with its APP_VERSION build arg. Carried the branch's own layout forward where #24 still described the single file, and repointed the references #24 added that the split invalidated: the monitorSchema provider enum now lives in config.ts and the color maps in render.ts, and Contributing's single-file section is now the two adapter seams. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GLa4zi5Jmyb6HrGCTme2m4
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Squawk can now run on Slack instead of Discord, with the same features, commands, and incident lifecycle. One deployment drives one platform —
PLATFORM=discordorPLATFORM=slack, inferred from whichever bot token is configured.Why the refactor came first
All bot logic lived in
src/index.tsbound directly to discord.js, so nothing could be reused. Rather than write a second copy that would drift, the lifecycle was made platform-neutral and Discord moved behind an adapter:src/config.tsmonitors.jsonI/Osrc/state.tsdata/state.jsonread/write + legacy migrationsrc/icons.tssrc/render.tsEmbedbuilders + theTextFormatmarkup interfacesrc/core.tssrc/platform/types.tsChatPlatformseamsrc/platform/discord.tssrc/platform/slack.tssrc/core.tsimports neitherdiscord.jsnor@slack/*. Two conventions keep platform quirks out of it:nullmeans "gone". Adapters translate Discord's10003/10008/50001/50013/50035and Slack'schannel_not_found/message_not_foundintonullreturns; the core prunes state without knowing any error codes. Everything else throws.threadArchive,pinNotices,deletableThreads,presence,autocomplete,maxMessageDeleteAgeMs. Slack turns the first five off and the core skips that work rather than branching on platform identity.Rendering goes through one set of
render*()functions that emit a structuralEmbedplus inline markup via the platform'sTextFormat— Discord markdown and itst:timestamp tags, or Slack mrkdwn and its!date^tags. Status-page text is passed throughfmt.escape()so Slack's reserved&,<,>characters render literally; markup Squawk generates itself is not escaped.The Slack side
Socket Mode, so Slack needs no public HTTP endpoint and deploys exactly like the Discord bot. Embeds become Block Kit attachments (the attachment
colorsupplies the same accent bar), threads arethread_tsreplies, and update IDs travel as Slack message metadata instead of being scraped back out of a rendered embed.Two places where Slack has no equivalent and a decision was needed:
/squawk status,/squawk monitor add …— renameable viaSLACK_COMMAND_NAME. Options are positional orkey=value. Since Slack has no autocomplete,/squawk helplists whatever is enabled.SLACK_ADMIN_USER_IDSrestrictstestpost/replay/clean/cleanup/monitorto named users. Left unset, any workspace member can run them — worth a look, since it's the one place the Slack default is more permissive than Discord's.What Slack simply doesn't have: thread archiving, pin system notices, bot presence, and command autocomplete. Those are capability-flagged off, documented, and skipped.
Also fixed
DISCORD_GUILD_ID=— exactly what.env.exampleships and whatdocker compose env_fileproduces — crashed startup, because a.envexports every key it lists and""failed its optional non-empty string. Blank env values are now treated as unset. Pre-existing, but the new template adds more optional-and-usually-empty vars, so it would have gotten worse.bulkDeleteon Discord only reported success when the message happened to be cached, so/cleancould report zero deletions and leave state pointing at a message it had just removed.Verification
bun run typecheckandbun testpass — 76 tests, up from 29.src/core.test.tsis the regression net for the refactor: it runs the full lifecycle (parent + thread + pin → follow-up update → resolve → ghost → self-heal after manual deletion) against an in-memoryChatPlatform, so Discord's behavior is pinned down by tests rather than by inspection.bun install --frozen-lockfile --production(what the Dockerfile runs) verified with the new dependencies.discord.comandslack.com, so neither adapter has talked to a real API. The Discord path was confirmed to reach the REST call before the proxy refused it. The Slack adapter wants a real workspace smoke test before this leaves draft — particularly pin/unpin, the single-messageconversations.repliesfetches, and ghosting an incident.Note that this repo has no CI workflow running
bun testorbun run typecheck— the only workflows are the Docker build, lockfile sync, and wiki sync. Nothing gates this PR on the test suite. Adding one felt outside the scope of this change, but it's worth doing.Docs
docs/wiki/Slack-Setup.mdis new (app manifest, scopes, both tokens, admin gating, and a table of Discord/Slack differences). Architecture, Configuration, Commands, Incident-Lifecycle, State-Management, Deployment, Development, Home, README, AGENTS.md, and.env.examplewere updated per the documentation rules in AGENTS.md.Compatibility
No migration needed for existing Discord deployments — same env vars, same state format, same commands.
data/state.jsonstores one platform's opaque handles, so switching platforms (or running both) means a separate instance and data volume; that's called out in Deployment and State-Management.