diff --git a/apps/dashboard/src/app/(dashboard)/settings/integrations/page.tsx b/apps/dashboard/src/app/(dashboard)/settings/integrations/page.tsx index adb43aa4..9d010330 100644 --- a/apps/dashboard/src/app/(dashboard)/settings/integrations/page.tsx +++ b/apps/dashboard/src/app/(dashboard)/settings/integrations/page.tsx @@ -59,6 +59,7 @@ export default function Page() { data: slackIntegration.data as { teamName?: string; }, + missingScopes: slackIntegration.missingScopes, } : null } diff --git a/apps/dashboard/src/app/(dashboard)/settings/integrations/slack-card.tsx b/apps/dashboard/src/app/(dashboard)/settings/integrations/slack-card.tsx index d276f461..d7065ab3 100644 --- a/apps/dashboard/src/app/(dashboard)/settings/integrations/slack-card.tsx +++ b/apps/dashboard/src/app/(dashboard)/settings/integrations/slack-card.tsx @@ -17,6 +17,7 @@ import { FormCardTitle, FormCardUpgrade, } from "@/components/forms/form-card"; +import { useFeature } from "@/hooks/use-feature"; import { useTRPC } from "@/lib/trpc/client"; const SERVER_URL = @@ -30,6 +31,7 @@ interface SlackIntegrationCardProps { id: number; externalId: string; data: { teamName?: string }; + missingScopes: string[]; } | null; } @@ -41,6 +43,9 @@ export function SlackIntegrationCard({ const trpc = useTRPC(); const queryClient = useQueryClient(); const isConnected = !!integration; + const incidents = useFeature("incident-management"); + const needsReconnect = + incidents && (integration?.missingScopes.length ?? 0) > 0; const deleteIntegration = useMutation( trpc.integrationRouter.deleteIntegration.mutationOptions({ @@ -97,6 +102,21 @@ export function SlackIntegrationCard({ {integration.data?.teamName ?? "Slack workspace"} . Only members with a linked Slack account can use it.
+ {needsReconnect ? ( +
Loading linked accounts…
diff --git a/apps/server/slack-manifest.json b/apps/server/slack-manifest.json
index c12e8d22..895a2505 100644
--- a/apps/server/slack-manifest.json
+++ b/apps/server/slack-manifest.json
@@ -40,8 +40,8 @@
{
"command": "/openstatus",
"url": "https://api.openstatus.dev/slack/commands",
- "description": "Manage this channel's status page subscriptions",
- "usage_hint": "subscribe | unsubscribe | subscriptions | help",
+ "description": "Incidents and status page subscriptions from Slack",
+ "usage_hint": "incident declare | incident note | incident list | subscribe | help",
"should_escape": false
}
]
@@ -56,12 +56,17 @@
"assistant:write",
"channels:history",
"channels:join",
+ "channels:manage",
+ "channels:write.invites",
"chat:write",
"commands",
"groups:history",
"groups:read",
"groups:write",
"im:history",
+ "pins:write",
+ "reactions:read",
+ "reactions:write",
"users:read",
"users:read.email"
]
@@ -77,9 +82,14 @@
"app_context_changed",
"app_home_opened",
"app_mention",
+ "app_uninstalled",
+ "channel_archive",
+ "channel_deleted",
"message.channels",
"message.groups",
- "message.im"
+ "message.im",
+ "reaction_added",
+ "tokens_revoked"
]
},
"interactivity": {
diff --git a/apps/server/src/routes/slack/handler.test.ts b/apps/server/src/routes/slack/handler.test.ts
index d325579f..17c4bcee 100644
--- a/apps/server/src/routes/slack/handler.test.ts
+++ b/apps/server/src/routes/slack/handler.test.ts
@@ -1932,3 +1932,36 @@ describe("hardening", () => {
}
});
});
+
+describe("reconnect banner", () => {
+ const app = createTestApp();
+
+ beforeEach(resetSlackTestState);
+
+ test("the home tab asks an old install to reconnect", async () => {
+ slackTestState.resolveWorkspace = () =>
+ Promise.resolve({
+ workspace: {
+ id: 1,
+ name: "Test Workspace",
+ slug: "test",
+ plan: "team",
+ limits: { "slack-agent": true },
+ },
+ botToken: "xoxb-test",
+ botUserId: "UBOT",
+ scopes: "chat:write,users:read,users:read.email",
+ });
+ await signAndPost(app, {
+ type: "event_callback",
+ team_id: "T_KNOWN",
+ event_id: `evt_reconnect_${Date.now()}`,
+ event: { type: "app_home_opened", tab: "home", user: "U1" },
+ });
+ const publish = await waitForCall("views.publish");
+ const view = publish?.args.view as {
+ blocks: { type: string; text?: { text: string } }[];
+ };
+ expect(view.blocks[0].text?.text).toContain("Reconnect openstatus");
+ });
+});
diff --git a/apps/server/src/routes/slack/handler.ts b/apps/server/src/routes/slack/handler.ts
index ac8fc4d2..f2cafd77 100644
--- a/apps/server/src/routes/slack/handler.ts
+++ b/apps/server/src/routes/slack/handler.ts
@@ -1,5 +1,9 @@
import { getLogger } from "@logtape/logtape";
-import { uninstallSlackTeam } from "@openstatus/services/integration";
+import { isFeatureEnabled } from "@openstatus/services";
+import {
+ missingSlackScopes,
+ uninstallSlackTeam,
+} from "@openstatus/services/integration";
import { WebClient } from "@slack/web-api";
import type { Context } from "hono";
import { z } from "zod";
@@ -367,7 +371,14 @@ async function processEvent(body: SlackEvent, config: SlackConfig) {
slackUserId: userId,
});
if (actor) {
- await publishHomeView(slack, userId);
+ const needsReconnect =
+ isFeatureEnabled(resolved.workspace, "incident-management") &&
+ missingSlackScopes(resolved.scopes).length > 0;
+ await publishHomeView(slack, userId, {
+ reconnectUrl: needsReconnect
+ ? `${config.dashboardUrl}/settings/integrations`
+ : undefined,
+ });
} else {
const url = await linkAccountUrl(config, {
workspaceId: resolved.workspace.id,
diff --git a/apps/server/src/routes/slack/home.ts b/apps/server/src/routes/slack/home.ts
index 824df333..3fc7008e 100644
--- a/apps/server/src/routes/slack/home.ts
+++ b/apps/server/src/routes/slack/home.ts
@@ -5,8 +5,23 @@ import { buildLinkAccountBlocks } from "./blocks";
export const DOCS_URL = "https://www.openstatus.dev/docs";
-export function buildHomeBlocks(): KnownBlock[] {
+export function buildHomeBlocks(
+ opts: { reconnectUrl?: string } = {},
+): KnownBlock[] {
+ const reconnect: KnownBlock[] = opts.reconnectUrl
+ ? [
+ {
+ type: "section",
+ text: {
+ type: "mrkdwn",
+ text: `:warning: *Reconnect openstatus to enable incident channels.* This install is missing permissions openstatus needs to open a channel per incident. <${opts.reconnectUrl}|Reconnect from the dashboard>.`,
+ },
+ },
+ { type: "divider" },
+ ]
+ : [];
return [
+ ...reconnect,
{
type: "header",
text: { type: "plain_text", text: "openstatus", emoji: true },
@@ -48,10 +63,11 @@ export function buildHomeBlocks(): KnownBlock[] {
export async function publishHomeView(
slack: WebClient,
userId: string,
+ opts: { reconnectUrl?: string } = {},
): Promise