mirror of
https://github.com/kikootwo/ReadMeABook.git
synced 2026-06-03 12:50:09 +00:00
af0eaceb98
Introduce a provider-based notification system and wire it through the API and admin UI. Added INotificationProvider + notification service implementation and providers (apprise, discord, ntfy, pushover), plus a GET /api/admin/notifications/providers endpoint to expose provider metadata. Refactored code to use provider type strings (removed enum coupling), updated masking/encryption calls, and simplified the test notification endpoint to accept backendId or type+config and call sendToBackend directly. UI: NotificationsTab now fetches provider metadata and renders provider cards and dynamic config forms (fields driven by provider metadata). Added config field rendering, improved backend cards, and edit/delete actions. APIs: New providers route, updated admin notification CRUD routes to validate provider types dynamically, updated test route schema. Added download-client categories POST API to fetch categories from clients and wired postImportCategory handling in download-client routes. Other notable changes: BookDate now fetches Claude models dynamically from Anthropic's Models API; added paginated model fetch helper. Added ALLOW_WEAK_PASSWORD flag exposure to auth providers and password change logic. Doc updates and various tests added/updated. File-organization doc clarifies EPERM fix using stream-based copy.
233 lines
9.1 KiB
Markdown
233 lines
9.1 KiB
Markdown
# Notification System
|
|
|
|
**Status:** ✅ Implemented | Extensible notification system with Discord, ntfy, and Pushover support
|
|
|
|
## Overview
|
|
Sends notifications for audiobook request events (pending approval, approved, available, error) to configured backends. Non-blocking, atomic per-backend failure handling. Proper notification timing for all request flows including interactive search.
|
|
|
|
## Key Details
|
|
- **Backends:** Apprise (API), Discord (webhooks), ntfy (API), Pushover (API)
|
|
- **Events:** request_pending_approval, request_approved, request_available, request_error
|
|
- **Encryption:** AES-256-GCM for sensitive config (webhook URLs, API keys, notification URLs)
|
|
- **Delivery:** Async via Bull job queue (priority 5)
|
|
- **Failure Handling:** Non-blocking, Promise.allSettled (one backend fails, others succeed)
|
|
|
|
## Database Schema
|
|
|
|
```prisma
|
|
model NotificationBackend {
|
|
id String @id @default(uuid())
|
|
type String // 'apprise' | 'discord' | 'ntfy' | 'pushover'
|
|
name String // User-friendly label
|
|
config Json // Encrypted sensitive values
|
|
events Json // Array of subscribed events
|
|
enabled Boolean @default(true)
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
}
|
|
```
|
|
|
|
## Event Types
|
|
|
|
| Event | Trigger | Notification Sent When |
|
|
|-------|---------|------------------------|
|
|
| request_pending_approval | User creates request | Request needs admin approval |
|
|
| request_approved | Admin approves OR auto-approval | Request approved (manual or auto) |
|
|
| request_available | Plex/ABS scan completes | Audiobook available in library |
|
|
| request_error | Download/import fails | Request failed at any stage |
|
|
|
|
## Notification Triggers
|
|
|
|
**Request Creation (POST /api/requests)**
|
|
- Automatic search, approval needed: `status === 'awaiting_approval'` → request_pending_approval
|
|
- Automatic search, auto-approved: `status === 'pending'` → request_approved
|
|
- Interactive search: NO notification yet (deferred until torrent selection)
|
|
|
|
**BookDate Swipe (POST /api/bookdate/swipe)**
|
|
- Right swipe, approval needed: `status === 'awaiting_approval'` → request_pending_approval
|
|
- Right swipe, auto-approved: `status === 'pending'` → request_approved
|
|
|
|
**Request with Pre-Selected Torrent (POST /api/audiobooks/request-with-torrent)**
|
|
- Approval needed: `status === 'awaiting_approval'` → request_pending_approval
|
|
- Auto-approved: `status === 'downloading'` → request_approved
|
|
|
|
**Torrent Selection for Existing Request (POST /api/requests/[id]/select-torrent)**
|
|
- Approval needed: `status === 'awaiting_approval'` → request_pending_approval
|
|
- Auto-approved: `status === 'downloading'` → request_approved
|
|
|
|
**Admin Approval (POST /api/admin/requests/[id]/approve)**
|
|
- Approve (with or without pre-selected torrent): After job triggered → request_approved
|
|
- Deny: No notification
|
|
|
|
**Request Available (processors: scan-plex, plex-recently-added)**
|
|
- After `status: 'available'` update → request_available
|
|
- Includes user info in query (plexUsername)
|
|
|
|
**Request Error (processors: monitor-download, organize-files)**
|
|
- After `status: 'failed'` or `status: 'warn'` update → request_error
|
|
- Includes error message in payload
|
|
|
|
## Configuration Encryption
|
|
|
|
**Encrypted Values:**
|
|
- Apprise: `urls`, `authToken`
|
|
- Discord: `webhookUrl`
|
|
- ntfy: `accessToken`
|
|
- Pushover: `userKey`, `appToken`
|
|
|
|
**Pattern:** `iv:authTag:encryptedData` (base64)
|
|
|
|
**Masking:** Sensitive values returned as `••••••••` in API responses
|
|
|
|
**Preservation:** Masked values preserved on update (if value === '••••••••', use existing encrypted value)
|
|
|
|
## Message Formatting
|
|
|
|
**Apprise (JSON via Apprise API):**
|
|
- Type: info (pending), success (approved/available), failure (error)
|
|
- Modes: Stateless (send URLs directly) or Stateful (use persistent configKey, optional tag filter)
|
|
- Endpoint: `{serverUrl}/notify/` (stateless) or `{serverUrl}/notify/{configKey}` (stateful)
|
|
- Auth: Optional Bearer token via `authToken` config field
|
|
- Format: Event title + book details + user + error (if applicable)
|
|
|
|
**Discord (Rich Embeds):**
|
|
- Color-coded by event (yellow=pending, green=approved, blue=available, red=error)
|
|
- Fields: Title, Author, Requested By, Error (if applicable)
|
|
- Footer: Request ID
|
|
- Timestamp: Event time
|
|
|
|
**ntfy (JSON with Tags):**
|
|
- Tags: mailbox_with_mail, white_check_mark, tada, x (rendered as emojis by ntfy)
|
|
- Priority: Default (3) for pending/approved, High (4) for available/error
|
|
- Format: Event title + book details + user + error (if applicable)
|
|
- Auth: Optional Bearer token via `accessToken` config field
|
|
- Server: Configurable `serverUrl` (default: https://ntfy.sh)
|
|
|
|
**Pushover (Plain Text with Emojis):**
|
|
- Emojis: 📬 📬 🎉 ❌
|
|
- Priority: Normal (0) for pending/approved, High (1) for available/error
|
|
- Format: Event title + book details + user + error (if applicable)
|
|
|
|
## API Endpoints
|
|
|
|
**GET /api/admin/notifications**
|
|
- Returns all backends (sensitive values masked)
|
|
|
|
**POST /api/admin/notifications**
|
|
- Create backend (encrypts sensitive values)
|
|
- Body: `{type, name, config, events, enabled}`
|
|
|
|
**GET /api/admin/notifications/[id]**
|
|
- Get single backend (sensitive values masked)
|
|
|
|
**PUT /api/admin/notifications/[id]**
|
|
- Update backend (preserves masked values, encrypts new values)
|
|
|
|
**DELETE /api/admin/notifications/[id]**
|
|
- Delete backend
|
|
|
|
**POST /api/admin/notifications/test**
|
|
- Test notification (synchronous, not via job queue)
|
|
- Body: `{type, config}` (plaintext for testing)
|
|
- Sends test payload: "The Hitchhiker's Guide to the Galaxy" by Douglas Adams
|
|
|
|
## UI Components
|
|
|
|
**NotificationsTab (src/app/admin/settings/tabs/NotificationsTab)**
|
|
- Type selector cards (Discord: indigo "D", Pushover: blue "P")
|
|
- Configured backends grid (3 columns)
|
|
- Backend cards: type icon, name, enabled status, event count, edit/delete actions
|
|
- Modal: type-specific forms, event checkboxes, enable toggle, test button
|
|
|
|
**Modal Features:**
|
|
- Type-first selection (user clicks "Add Discord" or "Add Pushover")
|
|
- Password inputs for sensitive values
|
|
- Event subscription checkboxes (4 events, default: available + error)
|
|
- Test button (sends synchronous test notification)
|
|
- Save button (validates and creates/updates backend)
|
|
|
|
## Job Queue Integration
|
|
|
|
**Job Type:** `send_notification` (priority 5, concurrency 5)
|
|
|
|
**Payload:**
|
|
```typescript
|
|
{
|
|
jobId?: string,
|
|
event: string,
|
|
requestId: string,
|
|
title: string,
|
|
author: string,
|
|
userName: string,
|
|
message?: string,
|
|
timestamp: Date
|
|
}
|
|
```
|
|
|
|
**Processor:** `src/lib/processors/send-notification.processor.ts`
|
|
- Calls NotificationService.sendNotification()
|
|
- Non-blocking error handling (logs but doesn't throw)
|
|
|
|
**Queue Method:** `addNotificationJob(event, requestId, title, author, userName, message?)`
|
|
|
|
## Architecture
|
|
|
|
**Provider Pattern:** `INotificationProvider` interface + registry (matches `IAuthProvider` pattern)
|
|
|
|
```
|
|
src/lib/services/notification/
|
|
INotificationProvider.ts # Interface + shared types
|
|
notification.service.ts # Core service with registry
|
|
index.ts # Re-exports
|
|
providers/
|
|
apprise.provider.ts # Apprise API (100+ services)
|
|
discord.provider.ts # Discord webhook
|
|
ntfy.provider.ts # ntfy API
|
|
pushover.provider.ts # Pushover API
|
|
```
|
|
|
|
**Registry:** Module-level `Map<string, INotificationProvider>` with `registerProvider()` / `getProvider()`
|
|
|
|
**INotificationProvider interface:**
|
|
- `type: string` — provider identifier (registry key)
|
|
- `sensitiveFields: string[]` — fields needing encryption/masking
|
|
- `metadata: ProviderMetadata` — self-describing UI/validation metadata
|
|
- `send(config, payload): Promise<void>` — receives decrypted config
|
|
|
|
**ProviderMetadata:** `{ type, displayName, description, iconLabel, iconColor, configFields[] }`
|
|
**ProviderConfigField:** `{ name, label, type, required, placeholder?, defaultValue?, options? }`
|
|
|
|
**Helper functions:**
|
|
- `getRegisteredProviderTypes(): string[]` — all registered type keys
|
|
- `getAllProviderMetadata(): ProviderMetadata[]` — metadata for all providers
|
|
|
|
**API Endpoint:** `GET /api/admin/notifications/providers` — returns all provider metadata (admin-only)
|
|
|
|
## Extensibility
|
|
|
|
**Adding New Backend (2 steps):**
|
|
1. Create `providers/email.provider.ts` implementing `INotificationProvider`:
|
|
- Set `type = 'email'`, `sensitiveFields = ['smtpPassword']`
|
|
- Set `metadata` with displayName, description, iconLabel, iconColor, configFields
|
|
- Implement `send()` with email-specific logic
|
|
2. Register in `notification.service.ts`: `registerProvider(new EmailProvider())` + re-export from `index.ts`
|
|
|
|
No UI changes, no API route changes, no Zod schema changes needed — the UI renders dynamically from provider metadata.
|
|
|
|
**Adding New Event (e.g., download_complete):**
|
|
1. Add 'download_complete' to NotificationEvent enum
|
|
2. Add to event labels in UI
|
|
3. Add trigger point in processor
|
|
4. Add message formatting in Discord/Pushover formatters
|
|
|
|
## Tech Stack
|
|
- Bull (job queue)
|
|
- Node.js crypto (AES-256-GCM encryption)
|
|
- Apprise API, Discord webhooks, ntfy API, Pushover API
|
|
- React (UI), Tailwind CSS (styling)
|
|
|
|
## Related
|
|
- [Job Queue System](jobs.md)
|
|
- [Config Encryption](config.md)
|
|
- [Settings Pages](../../settings-pages.md)
|