Openstatus www.openstatus.dev

๐Ÿ”” ntfy.sh (#1218)

* ๐Ÿ”” ntfy.sh

* Apply suggestions from code review

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* ๐Ÿ”” ntfy.sh

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

authored by

Thibault Le Ouay
Copilot
and committed by
GitHub
407c23a4 6cef13d6

+626 -37
+13
apps/docs/src/content/docs/alerting/providers/ntfy.mdx
··· 1 + --- 2 + title: ntfy.sh 3 + description: "How to set up ntfy.sh notifications in OpenStatus to get alerts when your synthetic check fail" 4 + --- 5 + 6 + [ntfy.sh](https://ntfy.sh/) is an open-source servicer that allows you to receive push notifications on your phone. 7 + 8 + 9 + 10 + 11 + Go to the Notifications Page . Select `ntfy.sh` from the list of available integrations. 12 + 13 + Enter your ntfy topic (required), your custom server URL, and your bearer token.
+1 -1
apps/docs/src/content/docs/alerting/providers/pagerduty.mdx
··· 12 12 13 13 ## How to connect PagerDuty 14 14 15 - Go to the Alerts Page . Select `PagerDuty` from the list of available integrations. 15 + Go to the Notifications Page . Select `PagerDuty` from the list of available integrations. 16 16 17 17 18 18
+1
apps/web/package.json
··· 25 25 "@openstatus/header-analysis": "workspace:*", 26 26 "@openstatus/notification-discord": "workspace:*", 27 27 "@openstatus/notification-emails": "workspace:*", 28 + "@openstatus/notification-ntfy": "workspace:*", 28 29 "@openstatus/notification-pagerduty": "workspace:*", 29 30 "@openstatus/notification-slack": "workspace:*", 30 31 "@openstatus/notification-opsgenie": "workspace:*",
apps/web/public/assets/changelog/ntfy.png

This is a binary file and will not be displayed.

+14 -7
apps/web/src/app/app/[workspaceSlug]/(dashboard)/notifications/(overview)/_components/channel-table.tsx
··· 1 1 import { env } from "@/env"; 2 2 import type { Workspace } from "@openstatus/db/src/schema"; 3 - import { getLimit } from "@openstatus/db/src/schema/plan/utils"; 4 3 5 4 import { Button, Separator } from "@openstatus/ui"; 6 5 import Link from "next/link"; ··· 33 32 /> 34 33 <Separator /> 35 34 <Channel 35 + title="ntfy.sh" 36 + description="Send notifications by ntfy.sh." 37 + href="./notifications/new/ntfy" 38 + disabled={disabled} 39 + /> 40 + <Separator /> 41 + <Channel 42 + title="OpsGenie" 43 + description="Send notifications to OpsGenie." 44 + href="./notifications/new/opsgenie" 45 + disabled={disabled || !workspace.limits.opsgenie} 46 + /> 47 + <Separator /> 48 + <Channel 36 49 title="PagerDuty" 37 50 description="Send notifications to PagerDuty." 38 51 href={`https://app.pagerduty.com/install/integration?app_id=${env.PAGERDUTY_APP_ID}&redirect_url=${ ··· 57 70 disabled={disabled || !workspace.limits.sms} 58 71 /> 59 72 <Separator /> 60 - <Channel 61 - title="OpsGenie" 62 - description="Send notifications to OpsGenie." 63 - href="./notifications/new/opsgenie" 64 - disabled={disabled || !workspace.limits.opsgenie} 65 - /> 66 73 </div> 67 74 </div> 68 75 );
-1
apps/web/src/app/app/[workspaceSlug]/(dashboard)/notifications/new/[channel]/page.tsx
··· 32 32 if (isLimitReached) { 33 33 return <ProFeatureAlert feature="More notification channel" />; 34 34 } 35 - 36 35 return ( 37 36 <NotificationForm 38 37 workspacePlan={workspace.plan}
+4
apps/web/src/components/forms/notification/general.tsx
··· 20 20 import { SectionHeader } from "../shared/section-header"; 21 21 import { SectionDiscord } from "./provider/section-discord"; 22 22 import { SectionEmail } from "./provider/section-email"; 23 + import { SectionNtfy } from "./provider/section-ntfy"; 23 24 import { SectionOpsGenie } from "./provider/section-opsgenie"; 24 25 import { SectionPagerDuty } from "./provider/section-pagerduty"; 25 26 import { SectionSlack } from "./provider/section-slack"; ··· 32 33 pagerduty: "PagerDuty", 33 34 opsgenie: "OpsGenie", 34 35 email: "Email", 36 + ntfy: "Ntfy.sh", 35 37 }; 36 38 37 39 interface Props { ··· 56 58 return <SectionOpsGenie form={form} plan={plan} />; 57 59 case "email": 58 60 return <SectionEmail form={form} />; 61 + case "ntfy": 62 + return <SectionNtfy form={form} />; 59 63 default: 60 64 return <div>No provider selected</div>; 61 65 }
+15
apps/web/src/components/forms/notification/provider/actions.ts
··· 3 3 import { sendTest as sendOpsGenieAlert } from "@openstatus/notification-opsgenie"; 4 4 5 5 import { sendTest as sendPagerDutyAlert } from "@openstatus/notification-pagerduty"; 6 + 7 + import { sendTest as sendNtfyAlert } from "@openstatus/notification-ntfy"; 6 8 export async function sendOpsGenieTestAlert( 7 9 apiKey: string, 8 10 region: "us" | "eu", ··· 17 19 }); 18 20 return isSuccessfull; 19 21 } 22 + 23 + export async function sendNtfyTestAlert({ 24 + topic, 25 + serverUrl, 26 + token, 27 + }: { 28 + topic: string; 29 + serverUrl?: string; 30 + token?: string; 31 + }) { 32 + const isSuccessfull = await sendNtfyAlert({ topic, serverUrl, token }); 33 + return isSuccessfull; 34 + }
+106
apps/web/src/components/forms/notification/provider/section-ntfy.tsx
··· 1 + "use client"; 2 + 3 + import { toastAction } from "@/lib/toast"; 4 + import type { UseFormReturn } from "react-hook-form"; 5 + 6 + import { LoadingAnimation } from "@/components/loading-animation"; 7 + import type { InsertNotificationWithData } from "@openstatus/db/src/schema"; 8 + import { 9 + Button, 10 + FormControl, 11 + FormDescription, 12 + FormField, 13 + FormItem, 14 + FormLabel, 15 + FormMessage, 16 + Input, 17 + } from "@openstatus/ui"; 18 + import { useTransition } from "react"; 19 + import { sendNtfyTestAlert } from "./actions"; 20 + 21 + interface Props { 22 + form: UseFormReturn<InsertNotificationWithData>; 23 + } 24 + 25 + export function SectionNtfy({ form }: Props) { 26 + const [isTestPending, startTestTransition] = useTransition(); 27 + 28 + const watchTopic = form.watch("data.ntfy.topic"); 29 + const watchUrl = form.watch("data.ntfy.serverUrl"); 30 + const watchToken = form.watch("data.ntfy.token"); 31 + 32 + async function sendTestAlert() { 33 + if (!watchTopic) return; 34 + startTestTransition(async () => { 35 + const isSuccessfull = await sendNtfyTestAlert({ 36 + topic: watchTopic, 37 + serverUrl: watchUrl || undefined, 38 + token: watchToken || undefined, 39 + }); 40 + if (isSuccessfull) { 41 + toastAction("test-success"); 42 + } else { 43 + toastAction("test-error"); 44 + } 45 + }); 46 + } 47 + 48 + return ( 49 + <> 50 + <FormField 51 + control={form.control} 52 + name="data.ntfy.topic" 53 + render={({ field }) => ( 54 + <FormItem className="sm:col-span-full"> 55 + <FormLabel>Topic</FormLabel> 56 + <FormControl> 57 + <Input type="text" placeholder="your-topic" required {...field} /> 58 + </FormControl> 59 + <FormMessage /> 60 + </FormItem> 61 + )} 62 + /> 63 + <FormField 64 + control={form.control} 65 + name="data.ntfy.serverUrl" 66 + render={({ field }) => ( 67 + <FormItem className="sm:col-span-full"> 68 + <FormLabel>URL</FormLabel> 69 + <FormControl> 70 + <Input type="url" placeholder="https://ntfy.sh" {...field} /> 71 + </FormControl> 72 + <FormMessage /> 73 + </FormItem> 74 + )} 75 + /> 76 + <FormField 77 + control={form.control} 78 + name="data.ntfy.token" 79 + render={({ field }) => ( 80 + <FormItem className="sm:col-span-full"> 81 + <FormLabel>Bearer Token</FormLabel> 82 + <FormControl> 83 + <Input type="url" placeholder="tk_iloveopenstatus" {...field} /> 84 + </FormControl> 85 + <FormMessage /> 86 + </FormItem> 87 + )} 88 + /> 89 + <div className="col-span-full text-right"> 90 + <Button 91 + type="button" 92 + variant="secondary" 93 + className="w-full sm:w-auto" 94 + disabled={isTestPending || !watchTopic} 95 + onClick={sendTestAlert} 96 + > 97 + {!isTestPending ? ( 98 + "Test Webhook" 99 + ) : ( 100 + <LoadingAnimation variant="inverse" /> 101 + )} 102 + </Button> 103 + </div> 104 + </> 105 + ); 106 + }
+1 -1
apps/web/src/config/pricing-table.tsx
··· 110 110 features: [ 111 111 { 112 112 value: "notifications", 113 - label: "Slack, Discord, Email", 113 + label: "Slack, Discord, Email, ntfy.sh", 114 114 }, 115 115 { 116 116 value: "sms",
+13
apps/web/src/content/changelog/ntfh-sh-integration.mdx
··· 1 + --- 2 + title: ntfy.sh Integration 3 + description: Get notified via ntfy.sh if we detect an Incident. 4 + image: /assets/changelog/ntfy.png 5 + publishedAt: 2025-03-17 6 + --- 7 + You can now receive notifications via ntfy.sh when we detect an incident. 8 + 9 + [ntfy.sh](https://ntfy.sh/) is an open-source servicer that allows you to receive push notifications on your phone. 10 + 11 + We love open source, so we've added this integration to our free plan. 12 + 13 + Open source FTW ๐Ÿš€
+1
apps/workflows/Dockerfile
··· 13 13 --mount=type=bind,target=packages/emails/package.json,source=packages/emails/package.json \ 14 14 --mount=type=bind,target=packages/notifications/discord/package.json,source=packages/notifications/discord/package.json \ 15 15 --mount=type=bind,target=packages/notifications/email/package.json,source=packages/notifications/email/package.json \ 16 + --mount=type=bind,target=packages/notifications/ntfy/package.json,source=packages/notifications/ntfy/package.json \ 16 17 --mount=type=bind,target=packages/notifications/opsgenie/package.json,source=packages/notifications/opsgenie/package.json \ 17 18 --mount=type=bind,target=packages/notifications/pagerduty/package.json,source=packages/notifications/pagerduty/package.json \ 18 19 --mount=type=bind,target=packages/notifications/slack/package.json,source=packages/notifications/slack/package.json \
+4 -1
apps/workflows/dofigen.lock
··· 51 51 source: packages/notifications/discord/package.json 52 52 - target: packages/notifications/email/package.json 53 53 source: packages/notifications/email/package.json 54 + - target: packages/notifications/ntfy/package.json 55 + source: packages/notifications/ntfy/package.json 54 56 - target: packages/notifications/opsgenie/package.json 55 57 source: packages/notifications/opsgenie/package.json 56 58 - target: packages/notifications/pagerduty/package.json ··· 92 94 digest: sha256:e831d9a884d63734fe3dd9c491ed9a5a3d4c6a6d32c5b14f2067357c49b0b7e1 93 95 resources: 94 96 dofigen.yml: 95 - hash: 89efc794b70865f5718e42302021382f7ed0c0ea9459a7bd9a8047cdd28757ae 97 + hash: 756c481a0bf4efc02d00ba1da6a9d84960b3424e625d492ef4c93ec841d075f6 96 98 content: | 97 99 ignore: 98 100 - node_modules ··· 117 119 - packages/emails/package.json 118 120 - packages/notifications/discord/package.json 119 121 - packages/notifications/email/package.json 122 + - packages/notifications/ntfy/package.json 120 123 - packages/notifications/opsgenie/package.json 121 124 - packages/notifications/pagerduty/package.json 122 125 - packages/notifications/slack/package.json
+1
apps/workflows/dofigen.yml
··· 21 21 - packages/emails/package.json 22 22 - packages/notifications/discord/package.json 23 23 - packages/notifications/email/package.json 24 + - packages/notifications/ntfy/package.json 24 25 - packages/notifications/opsgenie/package.json 25 26 - packages/notifications/pagerduty/package.json 26 27 - packages/notifications/slack/package.json
+1
apps/workflows/package.json
··· 11 11 "@openstatus/emails": "workspace:*", 12 12 "@openstatus/notification-discord": "workspace:*", 13 13 "@openstatus/notification-emails": "workspace:*", 14 + "@openstatus/notification-ntfy": "workspace:*", 14 15 "@openstatus/notification-opsgenie": "workspace:*", 15 16 "@openstatus/notification-pagerduty": "workspace:*", 16 17 "@openstatus/notification-slack": "workspace:*",
+25 -14
apps/workflows/src/checker/utils.ts
··· 14 14 sendRecovery as sendEmailRecovery, 15 15 } from "@openstatus/notification-emails"; 16 16 import { 17 + sendAlert as sendNtfyAlert, 18 + sendDegraded as sendNtfyDegraded, 19 + sendRecovery as sendNtfyRecovery, 20 + } from "@openstatus/notification-ntfy"; 21 + import { 17 22 sendAlert as sendSlackAlert, 18 23 sendDegraded as sendSlackDegraded, 19 24 sendRecovery as sendSlackRecovery, ··· 32 37 33 38 import type { Region } from "@openstatus/db/src/schema/constants"; 34 39 import { 40 + sendAlert, 35 41 sendAlert as sendOpsGenieAlert, 36 42 sendDegraded as sendOpsGenieDegraded, 37 43 sendRecovery as sendOpsGenieRecovery, ··· 64 70 }; 65 71 66 72 export const providerToFunction = { 73 + discord: { 74 + sendAlert: sendDiscordAlert, 75 + sendRecovery: sendDiscordRecovery, 76 + sendDegraded: sendDiscordDegraded, 77 + }, 67 78 email: { 68 79 sendAlert: sendEmailAlert, 69 80 sendRecovery: sendEmailRecovery, 70 81 sendDegraded: sendEmailDegraded, 71 82 }, 72 - slack: { 73 - sendAlert: sendSlackAlert, 74 - sendRecovery: sendSlackRecovery, 75 - sendDegraded: sendSlackDegraded, 76 - }, 77 - discord: { 78 - sendAlert: sendDiscordAlert, 79 - sendRecovery: sendDiscordRecovery, 80 - sendDegraded: sendDiscordDegraded, 81 - }, 82 - sms: { 83 - sendAlert: sendSmsAlert, 84 - sendRecovery: sendSmsRecovery, 85 - sendDegraded: sendSmsDegraded, 83 + ntfy: { 84 + sendAlert: sendNtfyAlert, 85 + sendRecovery: sendNtfyRecovery, 86 + sendDegraded: sendNtfyDegraded, 86 87 }, 87 88 opsgenie: { 88 89 sendAlert: sendOpsGenieAlert, ··· 93 94 sendAlert: sendPagerdutyAlert, 94 95 sendRecovery: sendPagerDutyRecovery, 95 96 sendDegraded: sendPagerDutyDegraded, 97 + }, 98 + slack: { 99 + sendAlert: sendSlackAlert, 100 + sendRecovery: sendSlackRecovery, 101 + sendDegraded: sendSlackDegraded, 102 + }, 103 + sms: { 104 + sendAlert: sendSmsAlert, 105 + sendRecovery: sendSmsRecovery, 106 + sendDegraded: sendSmsDegraded, 96 107 }, 97 108 } satisfies Record<NotificationProvider, Notif>;
+3 -2
packages/db/src/schema/notifications/constants.ts
··· 1 1 export const notificationProvider = [ 2 2 "email", 3 3 "discord", 4 + "ntfy", 5 + "pagerduty", 6 + "opsgenie", 4 7 "slack", 5 8 "sms", 6 - "pagerduty", 7 - "opsgenie", 8 9 ] as const;
+23 -10
packages/db/src/schema/notifications/validation.ts
··· 36 36 export const emailSchema = z.string().email(); 37 37 export const urlSchema = z.string().url(); 38 38 39 + export const ntfyDataSchema = z.object({ 40 + ntfy: z.object({ 41 + topic: z.string().default(""), 42 + serverUrl: z.string().default("https://ntfy.sh"), 43 + token: z.string().optional(), 44 + }), 45 + }); 39 46 export const webhookDataSchema = z.object({ webhook: urlSchema }); 40 47 export const emailDataSchema = z.object({ email: emailSchema }); 41 48 export const phoneDataSchema = z.object({ sms: phoneSchema }); ··· 63 70 [ 64 71 insertNotificationSchema.merge( 65 72 z.object({ 73 + provider: z.literal("discord"), 74 + data: discordDataSchema, 75 + }), 76 + ), 77 + insertNotificationSchema.merge( 78 + z.object({ 66 79 provider: z.literal("email"), 67 80 data: emailDataSchema, 68 81 }), 69 82 ), 70 83 insertNotificationSchema.merge( 71 84 z.object({ 72 - provider: z.literal("sms"), 73 - data: phoneDataSchema, 85 + provider: z.literal("ntfy"), 86 + data: ntfyDataSchema, 74 87 }), 75 88 ), 76 89 insertNotificationSchema.merge( 77 90 z.object({ 78 - provider: z.literal("slack"), 79 - data: slackDataSchema, 91 + provider: z.literal("pagerduty"), 92 + data: pagerdutyDataSchema, 80 93 }), 81 94 ), 82 95 insertNotificationSchema.merge( 83 96 z.object({ 84 - provider: z.literal("discord"), 85 - data: discordDataSchema, 97 + provider: z.literal("opsgenie"), 98 + data: opsgenieDataSchema, 86 99 }), 87 100 ), 88 101 insertNotificationSchema.merge( 89 102 z.object({ 90 - provider: z.literal("pagerduty"), 91 - data: pagerdutyDataSchema, 103 + provider: z.literal("sms"), 104 + data: phoneDataSchema, 92 105 }), 93 106 ), 94 107 insertNotificationSchema.merge( 95 108 z.object({ 96 - provider: z.literal("opsgenie"), 97 - data: opsgenieDataSchema, 109 + provider: z.literal("slack"), 110 + data: slackDataSchema, 98 111 }), 99 112 ), 100 113 ],
+169
packages/notifications/ntfy/.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.\*
+15
packages/notifications/ntfy/README.md
··· 1 + # @openstatus/notification-ntfy 2 + To install dependencies: 3 + 4 + ```bash 5 + bun install 6 + ``` 7 + 8 + To run: 9 + 10 + ```bash 11 + bun run src/index.ts 12 + ``` 13 + 14 + This project was created using `bun init` in bun v1.0.0. [Bun](https://bun.sh) 15 + is a fast all-in-one JavaScript runtime.
+14
packages/notifications/ntfy/package.json
··· 1 + { 2 + "name": "@openstatus/notification-ntfy", 3 + "version": "1.0.0", 4 + "main": "src/index.ts", 5 + "dependencies": { 6 + "@openstatus/db": "workspace:*", 7 + "zod": "3.23.8" 8 + }, 9 + "devDependencies": { 10 + "@openstatus/tsconfig": "workspace:*", 11 + "@types/node": "22.10.2", 12 + "typescript": "5.6.2" 13 + } 14 + }
+166
packages/notifications/ntfy/src/index.ts
··· 1 + import type { Monitor, Notification } from "@openstatus/db/src/schema"; 2 + 3 + import type { Region } from "@openstatus/db/src/schema/constants"; 4 + import { NtfySchema } from "./schema"; 5 + 6 + export const sendAlert = async ({ 7 + monitor, 8 + notification, 9 + statusCode, 10 + message, 11 + // biome-ignore lint/correctness/noUnusedVariables: <explanation> 12 + incidentId, 13 + }: { 14 + monitor: Monitor; 15 + notification: Notification; 16 + statusCode?: number; 17 + message?: string; 18 + incidentId?: string; 19 + cronTimestamp: number; 20 + latency?: number; 21 + region?: Region; 22 + }) => { 23 + const notificationData = NtfySchema.parse(JSON.parse(notification.data)); 24 + const { name } = monitor; 25 + 26 + const body = `Your monitor ${name} / ${monitor.url} is down with ${ 27 + statusCode ? `status code ${statusCode}` : `error: ${message}` 28 + }`; 29 + 30 + const authorization = notificationData.token 31 + ? { Authorization: `Bearer ${notificationData.token}` } 32 + : undefined; 33 + 34 + const url = notificationData.serverUrl 35 + ? `${notificationData.serverUrl}/${notificationData.topic}` 36 + : `https://ntfy.sh/${notificationData.topic}`; 37 + 38 + try { 39 + await fetch(url, { 40 + method: "post", 41 + body, 42 + headers: { 43 + ...authorization, 44 + }, 45 + }); 46 + } catch (err) { 47 + console.log(err); 48 + // Do something 49 + } 50 + }; 51 + 52 + export const sendRecovery = async ({ 53 + monitor, 54 + notification, 55 + // biome-ignore lint/correctness/noUnusedVariables: <explanation> 56 + statusCode, 57 + // biome-ignore lint/correctness/noUnusedVariables: <explanation> 58 + message, 59 + // biome-ignore lint/correctness/noUnusedVariables: <explanation> 60 + incidentId, 61 + }: { 62 + monitor: Monitor; 63 + notification: Notification; 64 + statusCode?: number; 65 + message?: string; 66 + incidentId?: string; 67 + cronTimestamp: number; 68 + latency?: number; 69 + region?: Region; 70 + }) => { 71 + const notificationData = NtfySchema.parse(JSON.parse(notification.data)); 72 + const { name } = monitor; 73 + 74 + const body = `Your monitor ${name} / ${monitor.url} is up again`; 75 + const authorization = notificationData.token 76 + ? { Authorization: `Bearer ${notificationData.token}` } 77 + : undefined; 78 + 79 + const url = notificationData.serverUrl 80 + ? `${notificationData.serverUrl}/${notificationData.topic}` 81 + : `https://ntfy.sh/${notificationData.topic}`; 82 + try { 83 + await fetch(url, { 84 + method: "post", 85 + body, 86 + headers: { 87 + ...authorization, 88 + }, 89 + }); 90 + } catch (err) { 91 + console.log(err); 92 + // Do something 93 + } 94 + }; 95 + 96 + export const sendDegraded = async ({ 97 + monitor, 98 + notification, 99 + // biome-ignore lint/correctness/noUnusedVariables: <explanation> 100 + statusCode, 101 + // biome-ignore lint/correctness/noUnusedVariables: <explanation> 102 + message, 103 + }: { 104 + monitor: Monitor; 105 + notification: Notification; 106 + statusCode?: number; 107 + message?: string; 108 + incidentId?: string; 109 + cronTimestamp: number; 110 + latency?: number; 111 + region?: Region; 112 + }) => { 113 + const notificationData = NtfySchema.parse(JSON.parse(notification.data)); 114 + const { name } = monitor; 115 + 116 + const body = `Your monitor ${name} / ${monitor.url} is degraded `; 117 + 118 + const authorization = notificationData.token 119 + ? { Authorization: `Bearer ${notificationData.token}` } 120 + : undefined; 121 + 122 + const url = notificationData.serverUrl 123 + ? `${notificationData.serverUrl}/${notificationData.topic}` 124 + : `https://ntfy.sh/${notificationData.topic}`; 125 + 126 + try { 127 + await fetch(url, { 128 + method: "post", 129 + body, 130 + headers: { 131 + ...authorization, 132 + }, 133 + }); 134 + } catch (err) { 135 + console.log(err); 136 + // Do something 137 + } 138 + }; 139 + 140 + export const sendTest = async ({ 141 + serverUrl, 142 + topic, 143 + token, 144 + }: { 145 + topic: string; 146 + serverUrl?: string; 147 + token?: string; 148 + }) => { 149 + const authorization = token 150 + ? { Authorization: `Bearer ${token}` } 151 + : undefined; 152 + const url = serverUrl ? `${serverUrl}/${topic}` : `https://ntfy.sh/${topic}`; 153 + try { 154 + await fetch(url, { 155 + method: "post", 156 + body: "This is a test message from OpenStatus", 157 + headers: { 158 + ...authorization, 159 + }, 160 + }); 161 + } catch (err) { 162 + console.log(err); 163 + return false; 164 + } 165 + return true; 166 + };
+7
packages/notifications/ntfy/src/schema.ts
··· 1 + import { z } from "zod"; 2 + 3 + export const NtfySchema = z.object({ 4 + topic: z.string(), 5 + serverUrl: z.string().default("https://ntfy.sh"), 6 + token: z.string().optional(), 7 + });
+4
packages/notifications/ntfy/tsconfig.json
··· 1 + { 2 + "extends": "@openstatus/tsconfig/nextjs.json", 3 + "include": ["src", "*.ts"] 4 + }
+25
pnpm-lock.yaml
··· 254 254 '@openstatus/notification-emails': 255 255 specifier: workspace:* 256 256 version: link:../../packages/notifications/email 257 + '@openstatus/notification-ntfy': 258 + specifier: workspace:* 259 + version: link:../../packages/notifications/ntfy 257 260 '@openstatus/notification-opsgenie': 258 261 specifier: workspace:* 259 262 version: link:../../packages/notifications/opsgenie ··· 495 498 '@openstatus/notification-emails': 496 499 specifier: workspace:* 497 500 version: link:../../packages/notifications/email 501 + '@openstatus/notification-ntfy': 502 + specifier: workspace:* 503 + version: link:../../packages/notifications/ntfy 498 504 '@openstatus/notification-opsgenie': 499 505 specifier: workspace:* 500 506 version: link:../../packages/notifications/opsgenie ··· 800 806 resend: 801 807 specifier: 4.0.1 802 808 version: 4.0.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0) 809 + zod: 810 + specifier: 3.23.8 811 + version: 3.23.8 812 + devDependencies: 813 + '@openstatus/tsconfig': 814 + specifier: workspace:* 815 + version: link:../../tsconfig 816 + '@types/node': 817 + specifier: 22.10.2 818 + version: 22.10.2 819 + typescript: 820 + specifier: 5.6.2 821 + version: 5.6.2 822 + 823 + packages/notifications/ntfy: 824 + dependencies: 825 + '@openstatus/db': 826 + specifier: workspace:* 827 + version: link:../../db 803 828 zod: 804 829 specifier: 3.23.8 805 830 version: 3.23.8