mirror of
https://github.com/kikootwo/ReadMeABook.git
synced 2026-06-03 21:00:09 +00:00
89422fc77a
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.
130 lines
4.1 KiB
TypeScript
130 lines
4.1 KiB
TypeScript
/**
|
|
* Component: Apprise Notification Provider
|
|
* Documentation: documentation/backend/services/notifications.md
|
|
*/
|
|
|
|
import { INotificationProvider, NotificationPayload, ProviderMetadata } from '../INotificationProvider';
|
|
import { getEventMeta, getEventTitle, type NotificationSeverity } from '@/lib/constants/notification-events';
|
|
|
|
export interface AppriseConfig {
|
|
serverUrl: string;
|
|
urls?: string;
|
|
configKey?: string;
|
|
tag?: string;
|
|
authToken?: string;
|
|
}
|
|
|
|
// Apprise notification types by severity
|
|
const SEVERITY_TYPES: Record<NotificationSeverity, string> = {
|
|
info: 'info',
|
|
success: 'success',
|
|
error: 'failure',
|
|
warning: 'warning',
|
|
};
|
|
|
|
export class AppriseProvider implements INotificationProvider {
|
|
type = 'apprise' as const;
|
|
sensitiveFields = ['urls', 'authToken'];
|
|
metadata: ProviderMetadata = {
|
|
type: 'apprise',
|
|
displayName: 'Apprise',
|
|
description: 'Send notifications via Apprise API to 100+ services',
|
|
iconLabel: 'A',
|
|
iconColor: 'bg-purple-500',
|
|
configFields: [
|
|
{ name: 'serverUrl', label: 'Server URL', type: 'text', required: true, placeholder: 'http://apprise:8000' },
|
|
{ name: 'urls', label: 'Notification URLs', type: 'password', required: false, placeholder: 'slack://token, discord://webhook_id/token, ...' },
|
|
{ name: 'configKey', label: 'Config Key', type: 'text', required: false, placeholder: 'Persistent configuration key' },
|
|
{ name: 'tag', label: 'Tag', type: 'text', required: false, placeholder: 'Filter tag for stateful config' },
|
|
{ name: 'authToken', label: 'Auth Token', type: 'password', required: false, placeholder: 'Optional API auth token' },
|
|
],
|
|
};
|
|
|
|
async send(config: Record<string, any>, payload: NotificationPayload): Promise<void> {
|
|
const appriseConfig = config as unknown as AppriseConfig;
|
|
const meta = getEventMeta(payload.event);
|
|
const { title, body } = this.formatMessage(payload);
|
|
|
|
const serverUrl = appriseConfig.serverUrl.replace(/\/+$/, '');
|
|
const notificationType = SEVERITY_TYPES[meta.severity];
|
|
|
|
const headers: Record<string, string> = {
|
|
'Content-Type': 'application/json',
|
|
};
|
|
|
|
if (appriseConfig.authToken) {
|
|
headers['Authorization'] = `Bearer ${appriseConfig.authToken}`;
|
|
}
|
|
|
|
// Stateful mode: use configKey endpoint
|
|
if (appriseConfig.configKey) {
|
|
const url = `${serverUrl}/notify/${appriseConfig.configKey}`;
|
|
const requestBody: Record<string, string> = {
|
|
title,
|
|
body,
|
|
type: notificationType,
|
|
};
|
|
|
|
if (appriseConfig.tag) {
|
|
requestBody.tag = appriseConfig.tag;
|
|
}
|
|
|
|
const response = await fetch(url, {
|
|
method: 'POST',
|
|
headers,
|
|
body: JSON.stringify(requestBody),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const errorText = await response.text().catch(() => 'Unknown error');
|
|
throw new Error(`Apprise API failed: ${response.status} ${errorText}`);
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Stateless mode: send URLs directly
|
|
if (!appriseConfig.urls) {
|
|
throw new Error('Apprise requires either notification URLs or a config key');
|
|
}
|
|
|
|
const url = `${serverUrl}/notify/`;
|
|
const requestBody = {
|
|
urls: appriseConfig.urls,
|
|
title,
|
|
body,
|
|
type: notificationType,
|
|
};
|
|
|
|
const response = await fetch(url, {
|
|
method: 'POST',
|
|
headers,
|
|
body: JSON.stringify(requestBody),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const errorText = await response.text().catch(() => 'Unknown error');
|
|
throw new Error(`Apprise API failed: ${response.status} ${errorText}`);
|
|
}
|
|
}
|
|
|
|
private formatMessage(payload: NotificationPayload): { title: string; body: 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),
|
|
body: messageLines.join('\n'),
|
|
};
|
|
}
|
|
}
|