Something went wrong. Try again.
[READ-ONLY] Mirror of https://github.com/openstatusHQ/openstatus. ๐ซ Status page with uptime monitoring & API monitoring as code ๐ซ openstatus.dev
bun drizzle-orm monitoring monitoring-as-code nextjs observability on-call open-source shadcn-ui status-page statuspage synthetic-monitoring tinybird turso uptime uptime-checker uptime-monitor
Something went wrong. Try again.
TypeScript
at incident-api
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325import { getLogger } from "@logtape/logtape";import { ForbiddenError } from "@openstatus/services";import { createSlackSubscriber, listSlackSubscribersForChannel, removeSlackSubscriber,} from "@openstatus/services/page-subscriber";import { WebClient } from "@slack/web-api";import type { Context } from "hono";import { z } from "zod";
import { runInBackground } from "@/libs/background";
import { type Block, buildLinkAccountBlocks, LINK_ACCOUNT_TEXT,} from "./blocks";import type { SlackConfig, SlackEnv } from "./config";import { runIncidentCommand } from "./incident-commands";import { openDeclareIncidentModal } from "./incident-modal";import { linkAccountUrl, planRequiredMessage, requireSlackMember, slackAgentAllowed,} from "./require-slack-member";import { resolvePageFromUrl } from "./resolve-page";import { resolveWorkspace } from "./workspace-resolver";
const logger = getLogger("api-server");
const slashCommandSchema = z.object({ text: z.string().optional().default(""), team_id: z.string(), user_id: z.string(), channel_id: z.string(), channel_name: z.string().optional(), response_url: z.string().optional(), trigger_id: z.string().optional(),});
type SlashCommand = z.infer<typeof slashCommandSchema>;
const HELP = [ "*openstatus*", "โข `/openstatus incident declare` โ declare an incident (opens a form)", "โข `/openstatus incident help` โ run incidents: notes, status, postmortem", "โข `/openstatus subscribe <status-page-url>` โ subscribe this channel to a status page", "โข `/openstatus unsubscribe <status-page-url>` โ unsubscribe", "โข `/openstatus subscriptions` โ show this channel's subscriptions",].join("\n");
type CommandReply = { text: string; blocks?: Block[] };
function ephemeral(c: Context, reply: CommandReply) { return c.json({ response_type: "ephemeral", ...reply });}
/** Deliver a reply after the ack, via the command's single-use response URL. */async function respondLater( responseUrl: string, reply: CommandReply,): Promise<void> { const res = await fetch(responseUrl, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ response_type: "ephemeral", ...reply }), }); if (!res.ok) { logger.error("slack response_url delivery failed", { status: res.status, body: await res.text().catch(() => ""), }); }}
async function joinChannel(teamId: string, channelId: string): Promise<void> { try { const resolved = await resolveWorkspace(teamId); if (!resolved) return; const client = new WebClient(resolved.botToken); await client.conversations.join({ channel: channelId }); } catch (error) { // Private channels can't be self-joined โ the bot must be /invited. logger.warn("slack conversations.join failed", { error, channelId }); }}
export function handleSlackCommand(c: Context<SlackEnv>) { const config = c.get("slackConfig"); const parsed = slashCommandSchema.safeParse(c.get("slackBody")); if (!parsed.success) { return ephemeral(c, { text: "Could not read the command." }); } const command = parsed.data; const sub = subcommand(command);
const triggerId = command.trigger_id; if (triggerId && opensDeclareModal(command)) { // `trigger_id` stays valid for 3s after the user's action whether or not // we have acked, so ack now and open the form in the background. const open = () => openDeclareIncidentModal({ teamId: command.team_id, slackUserId: command.user_id, triggerId, channelId: command.channel_id, config, }).then( (notice): CommandReply | undefined => notice ? { text: notice } : undefined, (error) => { logger.error("slack declare modal open failed", { error }); return { text: ":x: Could not open the form. Please try again." }; }, ); const responseUrl = command.response_url; if (!responseUrl) { return open().then((reply) => reply ? ephemeral(c, reply) : c.body(null, 200), ); } runInBackground( "command declare modal", async () => { const reply = await open(); if (reply) await respondLater(responseUrl, reply); }, { teamId: command.team_id, channelId: command.channel_id }, ); return c.body(null, 200); }
// `help` โ and anything unrecognised, which falls through to it โ needs no // I/O, so it is answered in the ack itself. if ( sub !== "subscribe" && sub !== "unsubscribe" && sub !== "subscriptions" && sub !== "incident" ) { return ephemeral(c, { text: HELP }); }
// The rest resolve a page, write to the DB and call the Slack API, which can // outrun the 3s a slash command has to be acknowledged in. Ack now and // deliver the reply to `response_url` (valid for 30 minutes). const responseUrl = command.response_url; if (!responseUrl) { // Slack always sends one; without it there is nowhere to deliver a late // reply, so fall back to answering inline. return runMemberCommand(command, config).then((reply) => ephemeral(c, reply), ); }
runInBackground( `command ${sub}`, async () => { // The 200 above is the only other thing the user gets: without this the // command fails silently on their side. const reply = await runMemberCommand(command, config).catch((error) => { logger.error("slack command failed", { error, teamId: command.team_id, channelId: command.channel_id, }); return { text: ":x: Something went wrong. Please try again." }; }); await respondLater(responseUrl, reply); }, { teamId: command.team_id, channelId: command.channel_id }, );
return c.body(null, 200);}
function subcommand(command: SlashCommand): string { const tokens = command.text.trim().split(/\s+/).filter(Boolean); return (tokens[0] ?? "help").toLowerCase();}
/** `/openstatus incident declare` with no title opens the form instead. */function opensDeclareModal(command: SlashCommand): boolean { const words = command.text.trim().split(/\s+/).filter(Boolean); return ( words.length === 2 && words[0].toLowerCase() === "incident" && words[1].toLowerCase() === "declare" );}
function argument(command: SlashCommand): string | undefined { return command.text.trim().split(/\s+/).filter(Boolean)[1];}
/** Only linked members of the connected workspace may run commands. */async function runMemberCommand( command: SlashCommand, config: SlackConfig,): Promise<CommandReply> { const resolved = await resolveWorkspace(command.team_id); if (!resolved) { return { text: "openstatus isn't connected to this Slack workspace. Connect it from the openstatus dashboard.", }; } if (!slackAgentAllowed(resolved.workspace)) { return planRequiredMessage(config); } const actor = await requireSlackMember({ workspace: resolved.workspace, teamId: command.team_id, slackUserId: command.user_id, slack: new WebClient(resolved.botToken), }); if (!actor) { const url = await linkAccountUrl(config, { workspaceId: resolved.workspace.id, teamId: command.team_id, slackUserId: command.user_id, }); return { text: LINK_ACCOUNT_TEXT, blocks: buildLinkAccountBlocks(url) }; } if (subcommand(command) === "incident") { const words = command.text.trim().split(/\s+/).filter(Boolean).slice(1); return { text: await runIncidentCommand({ words, teamId: command.team_id, channelId: command.channel_id, resolved, actor, config, }), }; } return { text: await runCommand(command) };}
/** Runs the subcommand and returns the message to show the user. */async function runCommand(command: SlashCommand): Promise<string> { const { team_id: teamId, channel_id: channelId, channel_name: channelName, } = command; const sub = subcommand(command); const arg = argument(command);
if (sub === "subscribe") { if (!arg) { return "Usage: `/openstatus subscribe <status-page-url>`"; } const page = await resolvePageFromUrl(arg); if (!page) { return `Couldn't find a status page at \`${arg}\`.`; } try { const result = await createSlackSubscriber({ input: { pageId: page.id, teamId, channelId, channelName, }, }); await joinChannel(teamId, channelId); if (result.alreadySubscribed) { return `This channel is already subscribed to *${page.title}*.`; } return `๐ก This channel is now subscribed to *${page.title}*. Incident updates will appear here.`; } catch (error) { if (error instanceof ForbiddenError) { return `*${page.title}* isn't on a plan that supports subscribers.`; } logger.error("slack /openstatus subscribe failed", { error, teamId, channelId, }); return "Something went wrong subscribing this channel."; } }
if (sub === "unsubscribe") { if (!arg) { const subs = await listSlackSubscribersForChannel({ input: { teamId, channelId }, }); if (subs.length === 0) { return "This channel isn't subscribed to any status page."; } if (subs.length === 1) { await removeSlackSubscriber({ input: { pageId: subs[0].pageId, teamId, channelId }, }); return `Unsubscribed from *${subs[0].pageName}*.`; } const list = subs.map((s) => `โข ${s.pageName}`).join("\n"); return `This channel is subscribed to several pages โ specify which:\n${list}\n\nUsage: \`/openstatus unsubscribe <status-page-url>\``; } const page = await resolvePageFromUrl(arg); if (!page) { return `Couldn't find a status page at \`${arg}\`.`; } const { removed } = await removeSlackSubscriber({ input: { pageId: page.id, teamId, channelId }, }); return removed ? `Unsubscribed from *${page.title}*.` : `This channel wasn't subscribed to *${page.title}*.`; }
const subs = await listSlackSubscribersForChannel({ input: { teamId, channelId }, }); if (subs.length === 0) { return "This channel isn't subscribed to any status page."; } const list = subs.map((s) => `โข *${s.pageName}*`).join("\n"); return `This channel is subscribed to:\n${list}`;}