mirror of
https://github.com/Portabase/portabase.git
synced 2026-07-14 11:16:13 +02:00
### Description Fixes three issues with the `ntfy` notification provider to improve mobile delivery and readability. **Previous Behavior:** 1. **Silent Delivery:** Priority was hardcoded to `1` (Min). On Android and iOS, priority 1 notifications are delivered completely silently (no sound, no vibration, hidden from lock screen). 2. **Malformed Payload:** The `payload.data` object was appended as a raw, stringified JSON blob, making the notification body difficult to read. 3. **Tag Fallthrough:** The chained ternary logic for tags caused `error` level events to fall through to the default `information_source` icon. ### Changes Made - **Dynamic Priority Mapping:** Added `getPriority` to map severity levels to Ntfy's 1-5 scale. `info`/default maps to `2` (quiet ping), `warning` to `3` (default alert), `error` to `4` (high), and `critical` to `5` (max). - **Plain Text Data Formatting:** Added `formatData` to parse the JSON payload into a clean `- key: value` list. Markdown was purposely avoided to ensure clean rendering in mobile OS lock screen previews. - **Improved Tagging:** Refactored into a `getTags` helper. Added a base `floppy_disk` icon to instantly identify the notifications, and properly mapped `error` payloads to the `x` icon. ### Related Issues None (Direct Fix)
81 lines
2.3 KiB
TypeScript
81 lines
2.3 KiB
TypeScript
import type {EventPayload, DispatchResult} from '../types';
|
|
|
|
const getPriority = (level?: string): number => {
|
|
switch (level) {
|
|
case 'critical': return 5;
|
|
case 'error': return 4;
|
|
case 'warning': return 3;
|
|
case 'info':
|
|
default: return 2;
|
|
}
|
|
};
|
|
|
|
const formatData = (data: any): string => {
|
|
if (!data || typeof data !== 'object') return '';
|
|
return '\n\nDetails:\n' + Object.entries(data)
|
|
.map(([key, value]) => {
|
|
const stringVal = value !== null && typeof value === 'object' ? JSON.stringify(value) : String(value);
|
|
return `- ${key}: ${stringVal}`;
|
|
})
|
|
.join('\n');
|
|
};
|
|
|
|
const getTags = (level?: string): string[] => {
|
|
const baseTags = ['floppy_disk'];
|
|
|
|
if (level === 'critical') baseTags.push('rotating_light');
|
|
else if (level === 'error') baseTags.push('x');
|
|
else if (level === 'warning') baseTags.push('warning');
|
|
else baseTags.push('information_source');
|
|
|
|
return baseTags;
|
|
};
|
|
|
|
export async function sendNtfy(
|
|
config: { ntfyServerUrl?: string; ntfyTopic: string; ntfyToken?: string, ntfyUsername?: string, ntfyPassword?: string },
|
|
payload: EventPayload
|
|
): Promise<DispatchResult> {
|
|
const {ntfyServerUrl, ntfyTopic, ntfyToken, ntfyUsername, ntfyPassword} = config;
|
|
|
|
const baseUrl = (ntfyServerUrl || "https://ntfy.sh").replace(/\/$/, "");
|
|
|
|
const body = {
|
|
topic: ntfyTopic,
|
|
title: payload.title,
|
|
message: payload.message + formatData(payload.data),
|
|
priority: getPriority(payload.level),
|
|
tags: getTags(payload.level),
|
|
};
|
|
|
|
const headers: Record<string, string> = {
|
|
'Content-Type': 'application/json',
|
|
};
|
|
|
|
if (ntfyToken) {
|
|
headers['Authorization'] = `Bearer ${ntfyToken}`;
|
|
}
|
|
|
|
if (ntfyUsername && ntfyPassword) {
|
|
const credentials = btoa(`${ntfyUsername}:${ntfyPassword}`);
|
|
headers['Authorization'] = `Basic ${credentials}`;
|
|
}
|
|
|
|
const res = await fetch(`${baseUrl}`, {
|
|
method: 'POST',
|
|
body: JSON.stringify(body),
|
|
headers: headers,
|
|
});
|
|
|
|
if (!res.ok) {
|
|
const err = await res.text();
|
|
throw new Error(`ntfy error: ${res.status} ${err}`);
|
|
}
|
|
|
|
return {
|
|
success: true,
|
|
provider: 'ntfy',
|
|
message: 'Sent to ntfy',
|
|
response: await res.json(),
|
|
};
|
|
}
|