Skip to content

Commit 1c5428a

Browse files
committed
Merge :)
2 parents c241242 + 2fc73b7 commit 1c5428a

10 files changed

Lines changed: 450 additions & 9 deletions

File tree

app/database/schema.sql.ts

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,42 @@
1-
import { text, pgTable, timestamp, jsonb } from "drizzle-orm/pg-core";
1+
import { text, pgTable, timestamp, jsonb, boolean, serial, integer, primaryKey } from "drizzle-orm/pg-core";
2+
3+
export const SlackInstallationTable = pgTable("slack_installations", {
4+
id: serial("id").primaryKey(),
5+
teamId: text("team_id").notNull().unique(),
6+
teamName: text("team_name").notNull(),
7+
bot: jsonb("bot")
8+
.$type<{
9+
id: string;
10+
token: string;
11+
scopes: string[];
12+
userId: string;
13+
}>()
14+
.notNull(),
15+
incomingWebhook: jsonb("incoming_webhook")
16+
.$type<{
17+
channel: string;
18+
channelId: string;
19+
configurationUrl: string;
20+
url: string;
21+
}>()
22+
.notNull(),
23+
});
24+
25+
export const ConfigTable = pgTable(
26+
"config",
27+
{
28+
installationId: integer("installation_id").references(() => SlackInstallationTable.id),
29+
product: text("product").notNull(),
30+
services: jsonb("services").$type<string[]>().notNull().default([]),
31+
},
32+
(table) => [primaryKey({ columns: [table.installationId, table.product] })],
33+
);
234

335
export const StatusMessageTable = pgTable("status_messages", {
436
guid: text("guid").primaryKey(),
537
title: text("title").notNull(),
638
content: text("content").notNull(),
7-
pubDate: timestamp("pubDate", { mode: "date" }).notNull(),
39+
pubDate: timestamp("pub_date", { mode: "date" }).notNull(),
840
product: text("product").notNull(),
9-
affectedServices: jsonb("affectedServices").$type<string[]>().notNull().default([]),
41+
affectedServices: jsonb("affected_services").$type<string[]>().notNull().default([]),
1042
});

app/functions/run.ts

Lines changed: 43 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
1+
import { ConfigTable, SlackInstallationTable } from "@/database/schema.sql";
2+
import { db } from "@/database/db";
3+
import { eq, inArray, and, getTableColumns } from "drizzle-orm";
14
import { getProducts } from "../../products";
25
import { ClassifiedMessage } from "@/lib/interfaces";
6+
import { client } from "integrations/slack/client";
37

48
export async function handler() {
59
console.log(`[${new Date().toISOString()}] Running Status Check`);
@@ -12,13 +16,48 @@ export async function handler() {
1216
async (acc, product) => {
1317
const messages = await product.refreshStatusMessages();
1418
const accResolved = await acc;
15-
return {
16-
...accResolved,
17-
[product.name]: messages,
18-
};
19+
return messages.length > 0
20+
? {
21+
...accResolved,
22+
[product.name]: messages,
23+
}
24+
: accResolved;
1925
},
2026
Promise.resolve({} as Record<string, ClassifiedMessage[]>),
2127
);
2228

2329
// Notify relevant slack apps
30+
Object.entries(newMessages).forEach(async ([product, messages]) => {
31+
// const affectedServices = messages.map((message) => message.affectedServices).flat();
32+
console.log(
33+
`[${new Date().toISOString()}] Notifying users for ${product} of ${messages.length} new messages`,
34+
);
35+
const installations = await db
36+
.select({
37+
...getTableColumns(SlackInstallationTable),
38+
product: ConfigTable.product,
39+
services: ConfigTable.services,
40+
})
41+
.from(ConfigTable)
42+
.innerJoin(SlackInstallationTable, eq(ConfigTable.installationId, SlackInstallationTable.id))
43+
.where(eq(ConfigTable.product, product));
44+
45+
messages.forEach((message) => {
46+
const toNotify = installations.filter((installation) =>
47+
installation.services.some((service) => message.affectedServices.includes(service)),
48+
);
49+
toNotify.forEach(async (installation) => {
50+
const response = await client.chat.postMessage({
51+
token: installation.bot.token,
52+
channel: installation.incomingWebhook.channelId,
53+
text: message.content,
54+
});
55+
if (!response.ok) {
56+
console.error(
57+
`[${new Date().toISOString()}] Failed to notify ${product} for ${message.affectedServices.join(", ")}: ${response.error}`,
58+
);
59+
}
60+
});
61+
});
62+
});
2463
}

app/lib/interfaces.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,8 @@ export interface IService {
2323

2424
export type StatusMessage = Omit<typeof StatusMessageTable.$inferSelect, "product" | "affectedServices">;
2525
export type ClassifiedMessage = typeof StatusMessageTable.$inferSelect;
26+
27+
export type AlertConfiguration = {
28+
product: string;
29+
services: string[];
30+
};

integrations/index.ts

Whitespace-only changes.

integrations/slack/client.ts

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import { db } from "@/database/db";
2+
import { SlackInstallationTable } from "@/database/schema.sql";
3+
import { eq } from "drizzle-orm";
4+
import { InstallProvider, Installation } from "@slack/oauth";
5+
import { Resource } from "sst";
6+
import { WebClient } from "@slack/web-api";
7+
8+
export const client = new WebClient();
9+
10+
export const installer = new InstallProvider({
11+
clientId: Resource.SlackClientId.value,
12+
clientSecret: Resource.SlackClientSecret.value,
13+
stateSecret: Resource.SlackSigningSecret.value,
14+
installationStore: {
15+
storeInstallation: async (installation) => {
16+
if (installation.isEnterpriseInstall) {
17+
throw new Error("Enterprise installations are not supported");
18+
}
19+
await db
20+
.insert(SlackInstallationTable)
21+
.values({
22+
teamId: installation.team!.id,
23+
teamName: installation.team!.name!,
24+
bot: {
25+
id: installation.bot!.id,
26+
token: installation.bot!.token,
27+
scopes: installation.bot!.scopes,
28+
userId: installation.bot!.userId,
29+
},
30+
incomingWebhook: {
31+
channel: installation.incomingWebhook!.channel!,
32+
channelId: installation.incomingWebhook!.channelId!,
33+
configurationUrl: installation.incomingWebhook!.configurationUrl!,
34+
url: installation.incomingWebhook!.url!,
35+
},
36+
})
37+
.onConflictDoNothing();
38+
},
39+
fetchInstallation: async (installQuery) => {
40+
if (!installQuery.teamId) {
41+
throw new Error("Team ID is required");
42+
}
43+
44+
const [installation] = await db
45+
.select()
46+
.from(SlackInstallationTable)
47+
.where(eq(SlackInstallationTable.teamId, installQuery.teamId!));
48+
49+
if (!installation) {
50+
throw new Error("Installation not found");
51+
}
52+
53+
return {
54+
team: {
55+
id: installation.teamId,
56+
name: installation.teamName,
57+
},
58+
bot: {
59+
id: installation.bot.id,
60+
token: installation.bot.token,
61+
scopes: installation.bot.scopes,
62+
userId: installation.bot.userId,
63+
},
64+
incomingWebhook: installation.incomingWebhook,
65+
authVersion: "v2",
66+
isEnterpriseInstall: false,
67+
} as Installation<"v2", false>;
68+
},
69+
deleteInstallation: async (installQuery) => {
70+
if (!installQuery.teamId) {
71+
throw new Error("Team ID is required");
72+
}
73+
await db.delete(SlackInstallationTable).where(eq(SlackInstallationTable.teamId, installQuery.teamId!));
74+
},
75+
},
76+
});

integrations/webhook.ts

Whitespace-only changes.

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,9 @@
5454
"@radix-ui/react-toggle": "^1.1.0",
5555
"@radix-ui/react-toggle-group": "^1.1.0",
5656
"@radix-ui/react-tooltip": "^1.1.4",
57+
"@slack/oauth": "^3.0.1",
58+
"@slack/web-api": "^7.8.0",
59+
"@slack/webhook": "^7.0.4",
5760
"@tanstack/react-query": "^5.60.2",
5861
"@tanstack/react-router": "^1.81.10",
5962
"@tanstack/react-router-with-query": "^1.81.10",

0 commit comments

Comments
 (0)