Adds a Notifications tab to Settings so an operator can pick which events reach their phone, published to a single self-hosted ntfy topic. The notifier subscribes to the realtime event hub the dashboard already consumes, so every notifiable event keeps exactly one publisher. Inbound media had no event, so `media.received` is added and published from the existing durable `gotImage` gate in the chat service — one notification per conversation, never repeated when the room re-syncs. It feeds the in-app notification bell as well. - Best-effort delivery: a rejecting or unreachable ntfy server is logged and never propagated into the path that produced the event. - Events buffered before the notifier attaches are skipped, so a restart cannot replay a backlog to somebody's phone. - Settings are applied field by field onto the live config object, so a save takes effect without a restart. - The access token is environment-owned (NTFY_AUTH_TOKEN) like the other secrets: stripped before the settings record is written, never returned by the API, which reports only `hasAuthToken`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
119 lines
3.3 KiB
TypeScript
119 lines
3.3 KiB
TypeScript
import Database from 'better-sqlite3';
|
|
import type { Config } from '../src/config/index.js';
|
|
import { runMigrations } from '../src/database/client.js';
|
|
import { PostsRepository, MessagesRepository, SuccessfulMessagesRepository } from '../src/database/repositories.js';
|
|
|
|
export const authenticatedAccountAuth = {
|
|
isFullyAuthenticated: () => true,
|
|
invalidate: () => undefined,
|
|
};
|
|
|
|
export function createTestDb(): Database.Database {
|
|
const db = new Database(':memory:');
|
|
db.pragma('journal_mode = WAL');
|
|
// Disable FK for tests - we manage referential integrity manually
|
|
db.pragma('foreign_keys = OFF');
|
|
runMigrations(db);
|
|
db.pragma('foreign_keys = OFF');
|
|
return db;
|
|
}
|
|
|
|
export function createTestRepositories(db?: Database.Database) {
|
|
const database = db || createTestDb();
|
|
return {
|
|
postsRepo: new PostsRepository(database),
|
|
messagesRepo: new MessagesRepository(database),
|
|
successfulMessagesRepo: new SuccessfulMessagesRepository(database),
|
|
db,
|
|
};
|
|
}
|
|
|
|
export function getTestConfig(overrides?: Partial<Config>): Config {
|
|
return {
|
|
reddit: {
|
|
subreddits: [
|
|
{ name: 'test1', type: 'image' as const },
|
|
{ name: 'test2', type: 'image' as const },
|
|
],
|
|
pollIntervalMs: 1000,
|
|
userAgent: 'TestBot/1.0',
|
|
targetGenders: ['female'],
|
|
filterSellers: true,
|
|
sellerKeywords: ['onlyfans', 'fansly'],
|
|
sellerDomains: [],
|
|
prioritizeRecentPosts: false,
|
|
recentWindowMinutes: 60,
|
|
maxReadyCandidates: 25,
|
|
...overrides?.reddit,
|
|
},
|
|
openrouter: {
|
|
apiKey: 'test-key',
|
|
imageAnalysisModel: 'google/gemini-flash',
|
|
imageAnalysisFallbackModels: ['fallback-model'],
|
|
chatModel: 'chat-model',
|
|
chatFallbackModels: [],
|
|
imageAnalysisPrompt: 'Analyze this image',
|
|
messageGenerationPrompt: 'Generate a message for {analysis}',
|
|
chatOpenerPrompt: 'Chat opener for {title} {selftext} {author} {gender}',
|
|
...overrides?.openrouter,
|
|
},
|
|
rateLimit: {
|
|
minDelayBetweenMessagesMs: 1000,
|
|
maxMessagesPerHour: 10,
|
|
jitterMs: 500,
|
|
...overrides?.rateLimit,
|
|
},
|
|
browser: {
|
|
sessionDir: './data/browser-session',
|
|
dryRun: false,
|
|
...overrides?.browser,
|
|
},
|
|
database: {
|
|
path: './data/test.db',
|
|
...overrides?.database,
|
|
},
|
|
media: {
|
|
syncIntervalMs: 1800000,
|
|
syncEnabled: true,
|
|
},
|
|
strategies: {
|
|
explorationRate: 0.2,
|
|
minSamples: 5,
|
|
responseWindowHours: 48,
|
|
variants: [
|
|
{ id: 'variant-a', weight: 1, promptOverride: 'Be direct' },
|
|
{ id: 'variant-b', weight: 1, promptOverride: 'Be teasing' },
|
|
],
|
|
...overrides?.strategies,
|
|
},
|
|
chatStrategies: {
|
|
explorationRate: 0.2,
|
|
minSamples: 5,
|
|
responseWindowHours: 48,
|
|
variants: [],
|
|
...overrides?.chatStrategies,
|
|
},
|
|
pushNotifications: {
|
|
enabled: false,
|
|
serverUrl: 'https://ntfy.sh',
|
|
topic: '',
|
|
priority: 'default',
|
|
events: {
|
|
imageReceived: true,
|
|
replyReceived: false,
|
|
messageFailed: false,
|
|
botStopped: false,
|
|
rateLimited: false,
|
|
sessionExpired: true,
|
|
syncFailed: false,
|
|
},
|
|
...overrides?.pushNotifications,
|
|
},
|
|
web: {
|
|
enabled: true,
|
|
port: 3000,
|
|
...overrides?.web,
|
|
},
|
|
};
|
|
}
|