Openstatus www.openstatus.dev

feat: notifications (#418)

* 🚨 Discord alerts (#363)

* add `@openstatus/notication-discord`

* add `@openstatus/notification-discord` dependency

* map discord key to discord message function

* enable discord webhooks setup

* `@openstatus/notification-discord` initialization

* add functions to send messages to discord webhook

* format with prettier

* change toast type from "error" to "test-error"

* return false when webhook url is undefined

* resolve pr comments

* Add OpenStatus.jpg logo

* refactor setProviderData function to reduce complexity

---------

Co-authored-by: mxkaske <maximilian@kaske.org>

* slack notifications integration (#412)

Co-authored-by: mxkaske <maximilian@kaske.org>

* chore: small changes

* fix: server

* fix: ts

* fix: deploy

* revert: change

* 🚀

* chore: remove image

---------

Co-authored-by: Kelvin Amoaba <97001695+AmoabaKelvin@users.noreply.github.com>
Co-authored-by: Arshdeep K <58404935+arshkkk@users.noreply.github.com>
Co-authored-by: Thibault Le Ouay <thibaultleouay@gmail.Com>

+628 -102
+2
apps/server/package.json
··· 13 13 "@hono/sentry": "^1.0.0", 14 14 "@hono/zod-openapi": "0.7.1", 15 15 "@openstatus/db": "workspace:*", 16 + "@openstatus/notification-discord": "workspace:*", 16 17 "@openstatus/notification-emails": "workspace:*", 18 + "@openstatus/notification-slack": "workspace:*", 17 19 "@openstatus/plans": "workspace:*", 18 20 "@openstatus/tinybird": "workspace:*", 19 21 "@openstatus/upstash": "workspace:*",
+8 -5
apps/server/src/checker/alerting.ts
··· 1 - import type { z } from "zod"; 2 - 3 1 import { db, eq, schema } from "@openstatus/db"; 4 - import { selectNotificationSchema } from "@openstatus/db/src/schema"; 2 + import type { MonitorStatus } from "@openstatus/db/src/schema"; 3 + import { 4 + selectMonitorSchema, 5 + selectNotificationSchema, 6 + } from "@openstatus/db/src/schema"; 5 7 6 8 import { publishPingRetryPolicy } from "./checker"; 7 9 import type { Payload } from "./schema"; ··· 34 36 .where(eq(schema.monitor.id, Number(monitorId))) 35 37 .all(); 36 38 for (const notif of notifications) { 39 + const monitor = selectMonitorSchema.parse(notif.monitor); 37 40 await providerToFunction[notif.notification.provider]({ 38 - monitor: notif.monitor, 41 + monitor, 39 42 notification: selectNotificationSchema.parse(notif.notification), 40 43 }); 41 44 } ··· 46 49 status, 47 50 }: { 48 51 monitorId: string; 49 - status: z.infer<typeof schema.statusSchema>; 52 + status: MonitorStatus; 50 53 }) => { 51 54 await db 52 55 .update(schema.monitor)
+3 -3
apps/server/src/checker/schema.ts
··· 1 1 import { z } from "zod"; 2 2 3 - import { methods, status } from "@openstatus/db/src/schema"; 3 + import { monitorMethods, monitorStatus } from "@openstatus/db/src/schema"; 4 4 5 5 export const payloadSchema = z.object({ 6 6 workspaceId: z.string(), 7 7 monitorId: z.string(), 8 - method: z.enum(methods), 8 + method: z.enum(monitorMethods), 9 9 body: z.string().optional(), 10 10 headers: z.array(z.object({ key: z.string(), value: z.string() })).optional(), 11 11 url: z.string(), 12 12 cronTimestamp: z.number(), 13 13 pageIds: z.array(z.string()), 14 - status: z.enum(status), 14 + status: z.enum(monitorStatus), 15 15 }); 16 16 17 17 export type Payload = z.infer<typeof payloadSchema>;
+11 -29
apps/server/src/checker/utils.ts
··· 1 - import type { z } from "zod"; 2 - 3 1 import type { 4 - basicMonitorSchema, 5 - providerName, 6 - selectNotificationSchema, 2 + Monitor, 3 + Notification, 4 + NotificationProvider, 7 5 } from "@openstatus/db/src/schema"; 6 + import { sendDiscordMessage } from "@openstatus/notification-discord"; 8 7 import { send as sendEmail } from "@openstatus/notification-emails"; 9 - 10 - type ProviderName = (typeof providerName)[number]; 8 + import { sendSlackMessage } from "@openstatus/notification-slack"; 11 9 12 - type sendNotificationType = ({ 10 + type SendNotification = ({ 13 11 monitor, 14 12 notification, 15 13 }: { 16 - monitor: z.infer<typeof basicMonitorSchema>; 17 - notification: z.infer<typeof selectNotificationSchema>; 14 + monitor: Monitor; 15 + notification: Notification; 18 16 }) => Promise<void>; 19 17 20 18 export const providerToFunction = { 21 19 email: sendEmail, 22 - slack: async ({ 23 - monitor, 24 - notification, 25 - }: { 26 - monitor: any; 27 - notification: any; 28 - }) => { 29 - /* TODO: implement */ 30 - }, 31 - discord: async ({ 32 - monitor, 33 - notification, 34 - }: { 35 - monitor: any; 36 - notification: any; 37 - }) => { 38 - /* TODO: implement */ 39 - }, 40 - } satisfies Record<ProviderName, sendNotificationType>; 20 + slack: sendSlackMessage, 21 + discord: sendDiscordMessage, 22 + } satisfies Record<NotificationProvider, SendNotification>;
+3 -3
apps/server/src/v1/incident.ts
··· 2 2 3 3 import { and, db, eq } from "@openstatus/db"; 4 4 import { 5 - availableStatus, 6 5 incident, 6 + incidentStatus, 7 7 incidentUpdate, 8 8 } from "@openstatus/db/src/schema"; 9 9 ··· 27 27 }); 28 28 29 29 const incidentUpdateSchema = z.object({ 30 - status: z.enum(availableStatus).openapi({ 30 + status: z.enum(incidentStatus).openapi({ 31 31 description: "The status of the update", 32 32 }), 33 33 date: z.string().openapi({ ··· 43 43 example: "Documenso", 44 44 description: "The title of the incident", 45 45 }), 46 - status: z.enum(availableStatus).openapi({ 46 + status: z.enum(incidentStatus).openapi({ 47 47 description: "The current status of the incident", 48 48 }), 49 49 });
+6 -6
apps/server/src/v1/monitor.ts
··· 3 3 import { db, eq, sql } from "@openstatus/db"; 4 4 import { 5 5 flyRegions, 6 - methods, 7 6 monitor, 8 - periodicity, 9 - } from "@openstatus/db/src/schema/monitor"; 7 + monitorMethods, 8 + monitorPeriodicity, 9 + } from "@openstatus/db/src/schema"; 10 10 11 11 import type { Variables } from "./index"; 12 12 import { ErrorSchema } from "./shared"; ··· 25 25 }), 26 26 }); 27 27 28 - export const periodicityEnum = z.enum(periodicity); 28 + export const periodicityEnum = z.enum(monitorPeriodicity); 29 29 export const regionEnum = z 30 30 .enum(flyRegions) 31 31 .or(z.literal("")) ··· 63 63 description: "The description of your monitor", 64 64 }) 65 65 .nullable(), 66 - method: z.enum(methods).default("GET").openapi({ example: "GET" }), 66 + method: z.enum(monitorMethods).default("GET").openapi({ example: "GET" }), 67 67 body: z 68 68 .preprocess((val) => { 69 69 return String(val); ··· 120 120 example: "Documenso website", 121 121 description: "The description of your monitor", 122 122 }), 123 - method: z.enum(methods).default("GET").openapi({ example: "GET" }), 123 + method: z.enum(monitorMethods).default("GET").openapi({ example: "GET" }), 124 124 body: z.string().openapi({ 125 125 example: "Hello World", 126 126 description: "The body",
+1
apps/web/next-env.d.ts
··· 1 1 /// <reference types="next" /> 2 2 /// <reference types="next/image-types/global" /> 3 + /// <reference types="next/navigation-types/compat/navigation" /> 3 4 4 5 // NOTE: This file should not be edited 5 6 // see https://nextjs.org/docs/basic-features/typescript for more information.
+2
apps/web/package.json
··· 19 19 "@openstatus/db": "workspace:*", 20 20 "@openstatus/emails": "workspace:*", 21 21 "@openstatus/notification-emails": "workspace:*", 22 + "@openstatus/notification-discord": "workspace:*", 23 + "@openstatus/notification-slack": "workspace:*", 22 24 "@openstatus/plans": "workspace:*", 23 25 "@openstatus/react": "workspace:*", 24 26 "@openstatus/tinybird": "workspace:*",
+4 -18
apps/web/src/app/api/checker/utils.ts
··· 3 3 Notification, 4 4 NotificationProvider, 5 5 } from "@openstatus/db/src/schema"; 6 + import { sendDiscordMessage } from "@openstatus/notification-discord"; 6 7 import { send as sendEmail } from "@openstatus/notification-emails"; 8 + import { sendSlackMessage } from "@openstatus/notification-slack"; 7 9 8 10 type sendNotificationType = ({ 9 11 monitor, ··· 15 17 16 18 export const providerToFunction = { 17 19 email: sendEmail, 18 - slack: async ({ 19 - monitor, 20 - notification, 21 - }: { 22 - monitor: any; 23 - notification: any; 24 - }) => { 25 - /* TODO: implement */ 26 - }, 27 - discord: async ({ 28 - monitor, 29 - notification, 30 - }: { 31 - monitor: any; 32 - notification: any; 33 - }) => { 34 - /* TODO: implement */ 35 - }, 20 + slack: sendSlackMessage, 21 + discord: sendDiscordMessage, 36 22 } satisfies Record<NotificationProvider, sendNotificationType>;
+2
apps/web/src/components/data-table/notification/columns.tsx
··· 7 7 8 8 import { DataTableRowActions } from "./data-table-row-actions"; 9 9 10 + // TODO: use the getProviderMetaData function from the notification form to access the data 11 + 10 12 export const columns: ColumnDef<Notification>[] = [ 11 13 { 12 14 accessorKey: "name",
+131 -38
apps/web/src/components/forms/notification-form.tsx
··· 1 1 "use client"; 2 2 3 - import { useTransition } from "react"; 3 + import { useMemo, useTransition } from "react"; 4 4 import { useRouter } from "next/navigation"; 5 5 import { zodResolver } from "@hookform/resolvers/zod"; 6 6 import { useForm } from "react-hook-form"; 7 7 8 - import type { InsertNotification } from "@openstatus/db/src/schema"; 8 + import type { 9 + InsertNotification, 10 + NotificationProvider, 11 + } from "@openstatus/db/src/schema"; 9 12 import { 10 13 insertNotificationSchema, 11 14 notificationProvider, 12 15 notificationProviderSchema, 13 16 } from "@openstatus/db/src/schema"; 17 + import { sendTestDiscordMessage } from "@openstatus/notification-discord"; 18 + import { sendTestSlackMessage } from "@openstatus/notification-slack"; 14 19 import { 15 20 Button, 16 21 Form, ··· 30 35 31 36 import { LoadingAnimation } from "@/components/loading-animation"; 32 37 import { useToastAction } from "@/hooks/use-toast-action"; 38 + import { toCapitalize } from "@/lib/utils"; 33 39 import { api } from "@/trpc/client"; 34 40 35 - /** 36 - * TODO: based on the providers `data` structure, create dynamic form inputs 37 - * e.g. Provider: Email will need an `email` input field and 38 - * we store it like `data: { email: "" }` 39 - * But Provider: Slack will maybe require `webhook` and `channel` and 40 - * we store it like `data: { webhook: "", channel: "" }` 41 - */ 41 + function getDefaultProviderData(defaultValues?: InsertNotification) { 42 + if (!defaultValues?.provider) return ""; // FIXME: input can empty - needs to be undefined 43 + return JSON.parse(defaultValues?.data || "{}")[defaultValues?.provider]; 44 + } 45 + 46 + function setProviderData(provider: NotificationProvider, data: string) { 47 + return { [provider]: data }; 48 + } 49 + 50 + function getProviderMetaData(provider: NotificationProvider) { 51 + switch (provider) { 52 + case "email": 53 + return { 54 + dataType: "email", 55 + placeholder: "dev@documenso.com", 56 + setupDocLink: null, 57 + sendTest: null, 58 + }; 59 + 60 + case "slack": 61 + return { 62 + dataType: "url", 63 + placeholder: "https://hooks.slack.com/services/xxx...", 64 + setupDocLink: 65 + "https://api.slack.com/messaging/webhooks#getting_started", 66 + sendTest: sendTestSlackMessage, 67 + }; 68 + 69 + case "discord": 70 + return { 71 + dataType: "url", 72 + placeholder: "https://hooks.slack.com/services/xxx...", // FIXME: 73 + setupDocLink: 74 + "https://api.slack.com/messaging/webhooks#getting_started", // FIXME: 75 + sendTest: sendTestDiscordMessage, 76 + }; 77 + 78 + default: 79 + return { 80 + dataType: "url", 81 + placeholder: "xxxx", 82 + setupDocLink: `https://docs.openstatus.dev/integrations/${provider}`, 83 + send: null, 84 + }; 85 + } 86 + } 42 87 43 88 interface Props { 44 89 defaultValues?: InsertNotification; ··· 52 97 onSubmit: onExternalSubmit, 53 98 }: Props) { 54 99 const [isPending, startTransition] = useTransition(); 100 + const [isTestPending, startTestTransition] = useTransition(); 55 101 const { toast } = useToastAction(); 56 102 const router = useRouter(); 57 103 const form = useForm<InsertNotification>({ ··· 59 105 defaultValues: { 60 106 ...defaultValues, 61 107 name: defaultValues?.name || "", 62 - data: 63 - defaultValues?.provider === "email" 64 - ? JSON.parse(defaultValues?.data).email 65 - : "", 108 + data: getDefaultProviderData(defaultValues), 66 109 }, 67 110 }); 111 + const watchProvider = form.watch("provider"); 112 + const watchWebhookUrl = form.watch("data"); 113 + const providerMetaData = useMemo( 114 + () => getProviderMetaData(watchProvider), 115 + [watchProvider], 116 + ); 68 117 69 118 async function onSubmit({ provider, data, ...rest }: InsertNotification) { 70 119 startTransition(async () => { 71 120 try { 72 121 if (defaultValues) { 73 122 await api.notification.updateNotification.mutate({ 74 - provider: "email", 75 - data: JSON.stringify({ email: data }), 123 + provider, 124 + data: JSON.stringify(setProviderData(provider, data)), 76 125 ...rest, 77 126 }); 78 127 } else { 79 128 await api.notification.createNotification.mutate({ 80 129 workspaceSlug, 81 - provider: "email", 82 - data: JSON.stringify({ email: data }), 130 + provider, 131 + data: JSON.stringify(setProviderData(provider, data)), 83 132 ...rest, 84 133 }); 85 134 } ··· 93 142 }); 94 143 } 95 144 145 + async function sendTestWebhookPing() { 146 + const webhookUrl = form.getValues("data"); 147 + if (!webhookUrl) return; 148 + startTestTransition(async () => { 149 + const isSuccessfull = await providerMetaData.sendTest?.(webhookUrl); 150 + if (isSuccessfull) { 151 + toast("test-success"); 152 + } else { 153 + toast("test-error"); 154 + } 155 + }); 156 + } 157 + 96 158 return ( 97 159 <Form {...form}> 98 160 <form ··· 129 191 <SelectItem 130 192 key={provider} 131 193 value={provider} 132 - disabled={provider !== "email"} // only allow email for now 133 194 className="capitalize" 134 195 > 135 196 {provider} ··· 160 221 </FormItem> 161 222 )} 162 223 /> 163 - <FormField 164 - control={form.control} 165 - name="data" 166 - render={({ field }) => ( 167 - <FormItem className="sm:col-span-full"> 168 - <FormLabel>Email</FormLabel> 169 - <FormControl> 170 - <Input 171 - type="email" 172 - required 173 - placeholder="dev@documenso.com" 174 - {...field} 175 - /> 176 - </FormControl> 177 - <FormDescription>The data required.</FormDescription> 178 - <FormMessage /> 179 - </FormItem> 180 - )} 181 - /> 224 + {watchProvider && ( 225 + <FormField 226 + control={form.control} 227 + name="data" 228 + render={({ field }) => ( 229 + <FormItem className="sm:col-span-full"> 230 + {/* make the first letter capital */} 231 + <div className="flex items-center justify-between"> 232 + <FormLabel>{toCapitalize(watchProvider)}</FormLabel> 233 + </div> 234 + <FormControl> 235 + <Input 236 + type={providerMetaData.dataType} 237 + placeholder={providerMetaData.placeholder} 238 + {...field} 239 + /> 240 + </FormControl> 241 + <FormDescription className="flex items-center justify-between"> 242 + The data required. 243 + {providerMetaData.setupDocLink && ( 244 + <a 245 + href={providerMetaData.setupDocLink} 246 + target="_blank" 247 + className="underline hover:no-underline" 248 + > 249 + How to setup your {toCapitalize(watchProvider)}{" "} 250 + webhook 251 + </a> 252 + )} 253 + </FormDescription> 254 + <FormMessage /> 255 + </FormItem> 256 + )} 257 + /> 258 + )} 182 259 </div> 183 260 </div> 184 - <div className="flex sm:justify-end"> 261 + <div className="flex gap-4 sm:justify-end"> 262 + {providerMetaData.sendTest && ( 263 + <Button 264 + type="button" 265 + variant="secondary" 266 + className="w-full sm:w-auto" 267 + size="lg" 268 + disabled={!watchWebhookUrl || isTestPending} 269 + onClick={sendTestWebhookPing} 270 + > 271 + {!isTestPending ? ( 272 + "Test Webhook" 273 + ) : ( 274 + <LoadingAnimation variant="inverse" /> 275 + )} 276 + </Button> 277 + )} 185 278 <Button className="w-full sm:w-auto" size="lg" disabled={isPending}> 186 279 {!isPending ? "Confirm" : <LoadingAnimation />} 187 280 </Button>
+27
apps/web/src/components/layout/breadcrumbs.tsx
··· 1 + "use client"; 2 + 3 + import { Fragment } from "react"; 4 + import { usePathname } from "next/navigation"; 5 + import { ChevronRight } from "lucide-react"; 6 + 7 + // TODO: create place to put into layout.tsx 8 + 9 + export function Breadcrumbs() { 10 + const pathname = usePathname(); 11 + const result = pathname.split("/").slice(3); 12 + 13 + return ( 14 + <ul className="flex items-center"> 15 + {result.map((path, i) => { 16 + return ( 17 + <Fragment key={i}> 18 + <li>{path}</li> 19 + {i !== result.length - 1 ? ( 20 + <ChevronRight className="text-muted-foreground mx-2 h-3 w-3" /> 21 + ) : null} 22 + </Fragment> 23 + ); 24 + })} 25 + </ul> 26 + ); 27 + }
+9
apps/web/src/lib/utils.ts
··· 59 59 toDate: isToDateMidnight ? addOneDayToDate : date?.to?.getTime() || null, 60 60 }; 61 61 } 62 + 63 + export function toCapitalize(inputString: string) { 64 + const words = inputString.split(/[\s_]+/); // Split the input string by spaces or underscores 65 + 66 + // Capitalize the first letter of each word 67 + return words 68 + .map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()) 69 + .join(""); 70 + }
+169
packages/notifications/discord/.gitignore
··· 1 + # Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore 2 + 3 + # Logs 4 + 5 + logs 6 + _.log 7 + npm-debug.log_ 8 + yarn-debug.log* 9 + yarn-error.log* 10 + lerna-debug.log* 11 + .pnpm-debug.log* 12 + 13 + # Diagnostic reports (https://nodejs.org/api/report.html) 14 + 15 + report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json 16 + 17 + # Runtime data 18 + 19 + pids 20 + _.pid 21 + _.seed 22 + \*.pid.lock 23 + 24 + # Directory for instrumented libs generated by jscoverage/JSCover 25 + 26 + lib-cov 27 + 28 + # Coverage directory used by tools like istanbul 29 + 30 + coverage 31 + \*.lcov 32 + 33 + # nyc test coverage 34 + 35 + .nyc_output 36 + 37 + # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 38 + 39 + .grunt 40 + 41 + # Bower dependency directory (https://bower.io/) 42 + 43 + bower_components 44 + 45 + # node-waf configuration 46 + 47 + .lock-wscript 48 + 49 + # Compiled binary addons (https://nodejs.org/api/addons.html) 50 + 51 + build/Release 52 + 53 + # Dependency directories 54 + 55 + node_modules/ 56 + jspm_packages/ 57 + 58 + # Snowpack dependency directory (https://snowpack.dev/) 59 + 60 + web_modules/ 61 + 62 + # TypeScript cache 63 + 64 + \*.tsbuildinfo 65 + 66 + # Optional npm cache directory 67 + 68 + .npm 69 + 70 + # Optional eslint cache 71 + 72 + .eslintcache 73 + 74 + # Optional stylelint cache 75 + 76 + .stylelintcache 77 + 78 + # Microbundle cache 79 + 80 + .rpt2_cache/ 81 + .rts2_cache_cjs/ 82 + .rts2_cache_es/ 83 + .rts2_cache_umd/ 84 + 85 + # Optional REPL history 86 + 87 + .node_repl_history 88 + 89 + # Output of 'npm pack' 90 + 91 + \*.tgz 92 + 93 + # Yarn Integrity file 94 + 95 + .yarn-integrity 96 + 97 + # dotenv environment variable files 98 + 99 + .env 100 + .env.development.local 101 + .env.test.local 102 + .env.production.local 103 + .env.local 104 + 105 + # parcel-bundler cache (https://parceljs.org/) 106 + 107 + .cache 108 + .parcel-cache 109 + 110 + # Next.js build output 111 + 112 + .next 113 + out 114 + 115 + # Nuxt.js build / generate output 116 + 117 + .nuxt 118 + dist 119 + 120 + # Gatsby files 121 + 122 + .cache/ 123 + 124 + # Comment in the public line in if your project uses Gatsby and not Next.js 125 + 126 + # https://nextjs.org/blog/next-9-1#public-directory-support 127 + 128 + # public 129 + 130 + # vuepress build output 131 + 132 + .vuepress/dist 133 + 134 + # vuepress v2.x temp and cache directory 135 + 136 + .temp 137 + .cache 138 + 139 + # Docusaurus cache and generated files 140 + 141 + .docusaurus 142 + 143 + # Serverless directories 144 + 145 + .serverless/ 146 + 147 + # FuseBox cache 148 + 149 + .fusebox/ 150 + 151 + # DynamoDB Local files 152 + 153 + .dynamodb/ 154 + 155 + # TernJS port file 156 + 157 + .tern-port 158 + 159 + # Stores VSCode versions used for testing VSCode extensions 160 + 161 + .vscode-test 162 + 163 + # yarn v2 164 + 165 + .yarn/cache 166 + .yarn/unplugged 167 + .yarn/build-state.yml 168 + .yarn/install-state.gz 169 + .pnp.\*
+16
packages/notifications/discord/README.md
··· 1 + # @openstatus/notifications-discord 2 + 3 + To install dependencies: 4 + 5 + ```bash 6 + bun install 7 + ``` 8 + 9 + To run: 10 + 11 + ```bash 12 + bun run src/index.ts 13 + ``` 14 + 15 + This project was created using `bun init` in bun v1.0.0. [Bun](https://bun.sh) 16 + is a fast all-in-one JavaScript runtime.
+16
packages/notifications/discord/package.json
··· 1 + { 2 + "name": "@openstatus/notification-discord", 3 + "version": "1.0.0", 4 + "main": "src/index.ts", 5 + "dependencies": { 6 + "@openstatus/db": "workspace:*" 7 + }, 8 + "devDependencies": { 9 + "@openstatus/tsconfig": "workspace:*", 10 + "@types/node": "20.8.0", 11 + "@types/react": "18.2.24", 12 + "@types/react-dom": "18.2.8", 13 + "next": "13.5.3", 14 + "typescript": "5.2.2" 15 + } 16 + }
+54
packages/notifications/discord/src/index.ts
··· 1 + import type { Monitor, Notification } from "@openstatus/db/src/schema"; 2 + 3 + const postToWebhook = async (content: string, webhookUrl: string) => { 4 + await fetch(webhookUrl, { 5 + method: "POST", 6 + headers: { 7 + "Content-Type": "application/json", 8 + }, 9 + body: JSON.stringify({ 10 + content, 11 + avatar_url: 12 + "https://img.stackshare.io/service/104872/default_dc6948366d9bae553adbb8f51252eafbc5d2043a.jpg", 13 + username: "OpenStatus Notifications", 14 + }), 15 + }); 16 + }; 17 + 18 + export const sendDiscordMessage = async ({ 19 + monitor, 20 + notification, 21 + }: { 22 + monitor: Monitor; 23 + notification: Notification; 24 + }) => { 25 + const notificationData = JSON.parse(notification.data); 26 + const { discord: webhookUrl } = notificationData; // webhook url 27 + const { name } = monitor; 28 + 29 + try { 30 + await postToWebhook( 31 + `Your monitor ${name} is down 🚨 32 + 33 + Your monitor with url ${monitor.url} is down.`, 34 + webhookUrl, 35 + ); 36 + } catch (err) { 37 + // Do something 38 + } 39 + }; 40 + 41 + export const sendTestDiscordMessage = async (webhookUrl: string) => { 42 + if (!webhookUrl) { 43 + return false; 44 + } 45 + try { 46 + await postToWebhook( 47 + "This is a test notification from OpenStatus. \nIf you see this, it means that your webhook is working! 🎉", 48 + webhookUrl, 49 + ); 50 + return true; 51 + } catch (err) { 52 + return false; 53 + } 54 + };
+4
packages/notifications/discord/tsconfig.json
··· 1 + { 2 + "extends": "@openstatus/tsconfig/nextjs.json", 3 + "include": ["src", "*.ts"] 4 + }
+16
packages/notifications/slack/package.json
··· 1 + { 2 + "name": "@openstatus/notification-slack", 3 + "version": "0.0.0", 4 + "main": "src/index.ts", 5 + "dependencies": { 6 + "@openstatus/db": "workspace:*" 7 + }, 8 + "devDependencies": { 9 + "@openstatus/tsconfig": "workspace:*", 10 + "@types/node": "20.8.0", 11 + "@types/react": "18.2.24", 12 + "@types/react-dom": "18.2.8", 13 + "next": "13.5.3", 14 + "typescript": "5.2.2" 15 + } 16 + }
+77
packages/notifications/slack/src/index.ts
··· 1 + import type { Monitor, Notification } from "@openstatus/db/src/schema"; 2 + 3 + const postToWebhook = async (body: any, webhookUrl: string) => { 4 + try { 5 + await fetch(webhookUrl, { 6 + method: "POST", 7 + body: JSON.stringify(body), 8 + }); 9 + } catch (e) { 10 + console.log(e); 11 + throw e; 12 + } 13 + }; 14 + 15 + export const sendSlackMessage = async ({ 16 + monitor, 17 + notification, 18 + }: { 19 + monitor: Monitor; 20 + notification: Notification; 21 + }) => { 22 + const notificationData = JSON.parse(notification.data); 23 + const { slack: webhookUrl } = notificationData; // webhook url 24 + const { name } = monitor; 25 + 26 + try { 27 + await postToWebhook( 28 + { 29 + blocks: [ 30 + { 31 + type: "section", 32 + text: { 33 + type: "mrkdwn", 34 + text: `Your monitor <${monitor.url}/|${name}> is down 🚨 \n\n Powered by <https://www.openstatus.dev/|OpenStatus>.`, 35 + }, 36 + accessory: { 37 + type: "button", 38 + text: { 39 + type: "plain_text", 40 + text: "Open Monitor", 41 + emoji: true, 42 + }, 43 + value: `monitor_url_${monitor.url}`, 44 + url: monitor.url, 45 + }, 46 + }, 47 + ], 48 + }, 49 + webhookUrl, 50 + ); 51 + } catch (err) { 52 + console.log(err); 53 + // Do something 54 + } 55 + }; 56 + 57 + export const sendTestSlackMessage = async (webhookUrl: string) => { 58 + try { 59 + await postToWebhook( 60 + { 61 + blocks: [ 62 + { 63 + type: "section", 64 + text: { 65 + type: "mrkdwn", 66 + text: "This is a test notification from <https://www.openstatus.dev/|OpenStatus>.\n If you can read this, your Slack webhook is functioning correctly!", 67 + }, 68 + }, 69 + ], 70 + }, 71 + webhookUrl, 72 + ); 73 + return true; 74 + } catch (err) { 75 + return false; 76 + } 77 + };
+4
packages/notifications/slack/tsconfig.json
··· 1 + { 2 + "extends": "@openstatus/tsconfig/nextjs.json", 3 + "include": ["src", "*.ts"] 4 + }
+63
pnpm-lock.yaml
··· 87 87 '@openstatus/db': 88 88 specifier: workspace:* 89 89 version: link:../../packages/db 90 + '@openstatus/notification-discord': 91 + specifier: workspace:* 92 + version: link:../../packages/notifications/discord 90 93 '@openstatus/notification-emails': 91 94 specifier: workspace:* 92 95 version: link:../../packages/notifications/email 96 + '@openstatus/notification-slack': 97 + specifier: workspace:* 98 + version: link:../../packages/notifications/slack 93 99 '@openstatus/plans': 94 100 specifier: workspace:* 95 101 version: link:../../packages/plans ··· 151 157 '@openstatus/emails': 152 158 specifier: workspace:* 153 159 version: link:../../packages/emails 160 + '@openstatus/notification-discord': 161 + specifier: workspace:* 162 + version: link:../../packages/notifications/discord 154 163 '@openstatus/notification-emails': 155 164 specifier: workspace:* 156 165 version: link:../../packages/notifications/email 166 + '@openstatus/notification-slack': 167 + specifier: workspace:* 168 + version: link:../../packages/notifications/slack 157 169 '@openstatus/plans': 158 170 specifier: workspace:* 159 171 version: link:../../packages/plans ··· 585 597 specifier: 5.2.2 586 598 version: 5.2.2 587 599 600 + packages/notifications/discord: 601 + dependencies: 602 + '@openstatus/db': 603 + specifier: workspace:* 604 + version: link:../../db 605 + devDependencies: 606 + '@openstatus/tsconfig': 607 + specifier: workspace:* 608 + version: link:../../tsconfig 609 + '@types/node': 610 + specifier: 20.8.0 611 + version: 20.8.0 612 + '@types/react': 613 + specifier: 18.2.24 614 + version: 18.2.24 615 + '@types/react-dom': 616 + specifier: 18.2.8 617 + version: 18.2.8 618 + next: 619 + specifier: 13.5.3 620 + version: 13.5.3(@babel/core@7.23.2)(@opentelemetry/api@1.4.1)(react-dom@18.2.0)(react@18.2.0) 621 + typescript: 622 + specifier: 5.2.2 623 + version: 5.2.2 624 + 588 625 packages/notifications/email: 589 626 dependencies: 590 627 '@openstatus/db': ··· 611 648 zod: 612 649 specifier: 3.22.2 613 650 version: 3.22.2 651 + devDependencies: 652 + '@openstatus/tsconfig': 653 + specifier: workspace:* 654 + version: link:../../tsconfig 655 + '@types/node': 656 + specifier: 20.8.0 657 + version: 20.8.0 658 + '@types/react': 659 + specifier: 18.2.24 660 + version: 18.2.24 661 + '@types/react-dom': 662 + specifier: 18.2.8 663 + version: 18.2.8 664 + next: 665 + specifier: 13.5.3 666 + version: 13.5.3(@babel/core@7.23.2)(@opentelemetry/api@1.4.1)(react-dom@18.2.0)(react@18.2.0) 667 + typescript: 668 + specifier: 5.2.2 669 + version: 5.2.2 670 + 671 + packages/notifications/slack: 672 + dependencies: 673 + '@openstatus/db': 674 + specifier: workspace:* 675 + version: link:../../db 614 676 devDependencies: 615 677 '@openstatus/tsconfig': 616 678 specifier: workspace:* ··· 4923 4985 4924 4986 /@swagger-api/apidom-ns-json-schema-draft-4@0.78.0: 4925 4987 resolution: {integrity: sha512-19NR9lTHMOQTIEV4tJq+FlHQAYnjyH+DgI4mmRu6UMFSZjRjutYF7B8lCGogSus9Uwy8YpUk00prLFTld00wgA==} 4988 + requiresBuild: true 4926 4989 dependencies: 4927 4990 '@babel/runtime-corejs3': 7.23.2 4928 4991 '@swagger-api/apidom-ast': 0.78.0