diff --git a/src/lib/server/notifications.ts b/src/lib/server/notifications.ts index d9b5b98..c651bfc 100644 --- a/src/lib/server/notifications.ts +++ b/src/lib/server/notifications.ts @@ -129,6 +129,9 @@ async function sendToAppriseUrl(url: string, payload: NotificationPayload): Prom case 'ntfy': case 'ntfys': return await sendNtfy(url, payload); + case 'bark': + case 'barks': + return await sendBark(url, payload); case 'pushover': return await sendPushover(url, payload); case 'json': @@ -435,6 +438,66 @@ async function sendNtfy(appriseUrl: string, payload: NotificationPayload): Promi } } +// Bark +async function sendBark(appriseUrl: string, payload: NotificationPayload): Promise { + // Supported formats: + // bark://device_key (official api.day.app server) + // bark://host/device_key (custom server over HTTP) + // barks://host/device_key (custom server over HTTPS) + const isSecure = appriseUrl.startsWith('barks'); + const path = appriseUrl.replace(/^barks?:\/\//, ''); + + let url: string; + let deviceKey: string; + + if (!path.includes('/')) { + if (!path) { + return { success: false, error: 'Invalid Bark URL format. Expected: bark://device_key, bark://host/device_key, or barks://host/device_key' }; + } + url = 'https://api.day.app/push'; + deviceKey = path; + } else { + const parts = path.split('/'); + if (parts.length !== 2) { + return { success: false, error: 'Invalid Bark URL format. Expected: bark://device_key, bark://host/device_key, or barks://host/device_key' }; + } + const [host, key] = parts; + deviceKey = key; + if (!host || !deviceKey) { + return { success: false, error: 'Invalid Bark URL format. Expected: bark://device_key, bark://host/device_key, or barks://host/device_key' }; + } + url = `${isSecure ? 'https' : 'http'}://${host}/push`; + } + + const titleWithEnv = payload.environmentName ? `${payload.title} [${payload.environmentName}]` : payload.title; + const body: Record = { + device_key: deviceKey, + title: titleWithEnv, + body: payload.message + }; + + if (payload.type === 'error') { + body.level = 'timeSensitive'; + } + + try { + const response = await fetch(url, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body) + }); + + if (!response.ok) { + const text = await response.text().catch(() => ''); + return { success: false, error: `Bark error ${response.status}: ${text || response.statusText}` }; + } + await drainResponse(response); + return { success: true }; + } catch (error) { + return { success: false, error: `Bark connection failed: ${error instanceof Error ? error.message : String(error)}` }; + } +} + // Pushover async function sendPushover(appriseUrl: string, payload: NotificationPayload): Promise { // pushover://user_key/api_token diff --git a/src/routes/settings/notifications/NotificationModal.svelte b/src/routes/settings/notifications/NotificationModal.svelte index e29e997..e176d06 100644 --- a/src/routes/settings/notifications/NotificationModal.svelte +++ b/src/routes/settings/notifications/NotificationModal.svelte @@ -427,11 +427,14 @@ ntfy://host/topic?auth=base64token&priority=3 ntfys://host/topic?auth=base64token pushover://user_key/api_token workflows://hostname/workflow/signature +bark://bark_key +bark://host/bark_key +barks://host/bark_key jsons://hostname/webhook/path" class="flex min-h-[220px] w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring" >

- Supports Gotify (gotify:// or gotifys:// for HTTPS), Discord, Slack, Mattermost (mmost:// or mmosts://), Telegram, ntfy, Pushover, Workflows (for e.g. Microsoft Teams), and generic JSON webhooks. + Supports Gotify (gotify:// or gotifys:// for HTTPS), Discord, Slack, Mattermost (mmost:// or mmosts://), Telegram, ntfy, Bark, Pushover, Workflows (for e.g. Microsoft Teams), and generic JSON webhooks.