Files
ReadMeABook/src/lib/services/notification/providers/ntfy.provider.ts
T
kikootwo 89422fc77a Add authors pages and requestType notifications
Introduce full authors browsing/detail feature and enhance notifications to support type-specific titles.

- Add server APIs: authors search, author detail, and author books routes (audnexus integration) that require auth and enrich results with library matches.
- Add frontend pages/components: /authors listing and /authors/[asin] detail pages; AuthorCard, AuthorGrid, AuthorDetailCard, SimilarAuthorsRow, and related skeletons.
- Add hook and integration stubs: new useAuthors hook and audnexus-authors integration; update audible service to expose audibleBaseUrl.
- Update AudiobookDetailsModal to use audibleBaseUrl and link author names to author detail pages.
- Add header navigation link to Authors.
- Notifications: extend docs and code to include requestType (audiobook|ebook), add getEventTitle/getEventMeta helpers, update queue signature and providers/processors/tests to pass/handle requestType so titles can be resolved per request type.
- Misc: job queue, processors, provider tests and notification tests updated to reflect new behavior.

This change enables browsing authors and provides type-aware notification titles without per-provider changes.
2026-02-12 15:21:42 -05:00

105 lines
3.4 KiB
TypeScript

/**
* Component: ntfy Notification Provider
* Documentation: documentation/backend/services/notifications.md
*/
import { INotificationProvider, NotificationPayload, ProviderMetadata } from '../INotificationProvider';
import { getEventMeta, getEventTitle, type NotificationSeverity, type NotificationPriority } from '@/lib/constants/notification-events';
export interface NtfyConfig {
serverUrl?: string;
topic: string;
accessToken?: string;
priority?: number;
}
const DEFAULT_SERVER_URL = 'https://ntfy.sh';
// ntfy priorities by notification priority (1=min, 2=low, 3=default, 4=high, 5=urgent)
const PRIORITY_MAP: Record<NotificationPriority, number> = {
normal: 3,
high: 4,
};
// ntfy tags (emojis) by severity
const SEVERITY_TAGS: Record<NotificationSeverity, string[]> = {
info: ['mailbox_with_mail'],
success: ['white_check_mark'],
error: ['x'],
warning: ['triangular_flag_on_post'],
};
export class NtfyProvider implements INotificationProvider {
type = 'ntfy' as const;
sensitiveFields = ['accessToken'];
metadata: ProviderMetadata = {
type: 'ntfy',
displayName: 'ntfy',
description: 'Send notifications via ntfy pub/sub',
iconLabel: 'N',
iconColor: 'bg-teal-500',
configFields: [
{ name: 'serverUrl', label: 'Server URL', type: 'text', required: false, placeholder: 'https://ntfy.sh', defaultValue: 'https://ntfy.sh' },
{ name: 'topic', label: 'Topic', type: 'text', required: true, placeholder: 'readmeabook' },
{ name: 'accessToken', label: 'Access Token', type: 'password', required: false, placeholder: 'tk_...' },
],
};
async send(config: Record<string, any>, payload: NotificationPayload): Promise<void> {
const ntfyConfig = config as unknown as NtfyConfig;
const meta = getEventMeta(payload.event);
const { title, message } = this.formatMessage(payload);
// ntfy JSON publishing requires POSTing to the base server URL (not the topic URL).
// The topic is included in the JSON body. See: https://docs.ntfy.sh/publish/#publish-as-json
const url = (ntfyConfig.serverUrl || DEFAULT_SERVER_URL).replace(/\/+$/, '');
const headers: Record<string, string> = {
'Content-Type': 'application/json',
};
if (ntfyConfig.accessToken) {
headers['Authorization'] = `Bearer ${ntfyConfig.accessToken}`;
}
const body = {
topic: ntfyConfig.topic,
title,
message,
priority: ntfyConfig.priority ?? PRIORITY_MAP[meta.priority],
tags: SEVERITY_TAGS[meta.severity],
};
const response = await fetch(url, {
method: 'POST',
headers,
body: JSON.stringify(body),
});
if (!response.ok) {
const errorText = await response.text().catch(() => 'Unknown error');
throw new Error(`ntfy API failed: ${response.status} ${errorText}`);
}
}
private formatMessage(payload: NotificationPayload): { title: string; message: string } {
const { event, title, author, userName, message, requestType } = payload;
const isIssue = event === 'issue_reported';
const messageLines = [
`\u{1F4DA} ${title}`,
`\u270D\uFE0F ${author}`,
`\u{1F464} ${isIssue ? 'Reported by' : 'Requested by'}: ${userName}`,
];
if (message) {
messageLines.push(isIssue ? `\u{1F4DD} Reason: ${message}` : `\u26A0\uFE0F Error: ${message}`);
}
return {
title: getEventTitle(event, requestType),
message: messageLines.join('\n'),
};
}
}