169 lines
6.4 KiB
TypeScript
169 lines
6.4 KiB
TypeScript
import { Router } from 'express';
|
|
import type { WebContext } from '../context.js';
|
|
import { ConfigSchema, SubredditConfigSchema, type SubredditConfig } from '../../config/index.js';
|
|
import { TONE_KEYS } from '../../config/tone-profiles.js';
|
|
import { z } from 'zod';
|
|
|
|
const SubredditsUpdateSchema = z.array(SubredditConfigSchema).min(1);
|
|
const AdvancedConfigSchema = z.strictObject({
|
|
reddit: ConfigSchema.shape.reddit.omit({
|
|
subreddits: true,
|
|
prioritizeRecentPosts: true,
|
|
recentWindowMinutes: true,
|
|
maxReadyCandidates: true,
|
|
}),
|
|
openrouter: ConfigSchema.shape.openrouter.omit({ apiKey: true }),
|
|
browser: z.strictObject({ sessionDir: z.string() }),
|
|
media: ConfigSchema.shape.media,
|
|
strategies: ConfigSchema.shape.strategies,
|
|
chatStrategies: ConfigSchema.shape.chatStrategies,
|
|
});
|
|
|
|
function advancedConfig(ctx: WebContext): z.infer<typeof AdvancedConfigSchema> {
|
|
const {
|
|
subreddits: _subreddits,
|
|
prioritizeRecentPosts: _prioritize,
|
|
recentWindowMinutes: _window,
|
|
maxReadyCandidates: _maxReady,
|
|
...reddit
|
|
} = ctx.config.reddit;
|
|
const { apiKey: _apiKey, ...openrouter } = ctx.config.openrouter;
|
|
const { dryRun: _dryRun, ...browser } = ctx.config.browser;
|
|
return {
|
|
reddit,
|
|
openrouter,
|
|
browser,
|
|
media: ctx.config.media,
|
|
strategies: ctx.config.strategies,
|
|
chatStrategies: ctx.config.chatStrategies,
|
|
};
|
|
}
|
|
|
|
export function createConfigRouter(ctx: WebContext): Router {
|
|
const router = Router();
|
|
|
|
router.get('/config', (_req, res) => {
|
|
res.json({
|
|
rateLimit: ctx.runtimeConfig.rateLimit,
|
|
browser: { dryRun: ctx.runtimeConfig.browser.dryRun },
|
|
reddit: ctx.runtimeConfig.reddit,
|
|
subreddits: ctx.config.reddit.subreddits,
|
|
tones: TONE_KEYS,
|
|
advanced: advancedConfig(ctx),
|
|
});
|
|
});
|
|
|
|
router.put('/config', (req, res) => {
|
|
try {
|
|
const updates = req.body;
|
|
const configStore = ctx.services.configStore;
|
|
if (!configStore) throw new Error('Configuration persistence is unavailable');
|
|
|
|
const parsedSubreddits = updates.subreddits === undefined
|
|
? null
|
|
: SubredditsUpdateSchema.safeParse(updates.subreddits);
|
|
if (parsedSubreddits && !parsedSubreddits.success) {
|
|
res.status(400).json({ error: 'Invalid subreddits payload', detail: parsedSubreddits.error.format() });
|
|
return;
|
|
}
|
|
const parsedAdvanced = updates.advanced === undefined
|
|
? null
|
|
: AdvancedConfigSchema.safeParse(updates.advanced);
|
|
if (parsedAdvanced && !parsedAdvanced.success) {
|
|
res.status(400).json({ error: 'Invalid advanced configuration', detail: parsedAdvanced.error.format() });
|
|
return;
|
|
}
|
|
|
|
if (updates.rateLimit) {
|
|
if (typeof updates.rateLimit.minDelayBetweenMessagesMs === 'number') {
|
|
ctx.runtimeConfig.rateLimit.minDelayBetweenMessagesMs = Math.max(
|
|
0,
|
|
updates.rateLimit.minDelayBetweenMessagesMs,
|
|
);
|
|
}
|
|
if (typeof updates.rateLimit.maxMessagesPerHour === 'number') {
|
|
ctx.runtimeConfig.rateLimit.maxMessagesPerHour = Math.max(
|
|
1,
|
|
updates.rateLimit.maxMessagesPerHour,
|
|
);
|
|
}
|
|
if (typeof updates.rateLimit.jitterMs === 'number') {
|
|
ctx.runtimeConfig.rateLimit.jitterMs = Math.max(0, updates.rateLimit.jitterMs);
|
|
}
|
|
}
|
|
|
|
if (updates.browser) {
|
|
if (typeof updates.browser.dryRun === 'boolean') {
|
|
ctx.runtimeConfig.browser.dryRun = updates.browser.dryRun;
|
|
}
|
|
}
|
|
|
|
if (updates.reddit) {
|
|
if (typeof updates.reddit.prioritizeRecentPosts === 'boolean') {
|
|
ctx.runtimeConfig.reddit.prioritizeRecentPosts = updates.reddit.prioritizeRecentPosts;
|
|
}
|
|
if (typeof updates.reddit.recentWindowMinutes === 'number' && updates.reddit.recentWindowMinutes > 0) {
|
|
ctx.runtimeConfig.reddit.recentWindowMinutes = updates.reddit.recentWindowMinutes;
|
|
}
|
|
if (typeof updates.reddit.maxReadyCandidates === 'number'
|
|
&& Number.isInteger(updates.reddit.maxReadyCandidates)
|
|
&& updates.reddit.maxReadyCandidates >= 1
|
|
&& updates.reddit.maxReadyCandidates <= 500) {
|
|
ctx.runtimeConfig.reddit.maxReadyCandidates = updates.reddit.maxReadyCandidates;
|
|
}
|
|
}
|
|
|
|
// Mirror into the loaded config so getStats reflects new values.
|
|
ctx.config.rateLimit.minDelayBetweenMessagesMs = ctx.runtimeConfig.rateLimit.minDelayBetweenMessagesMs;
|
|
ctx.config.rateLimit.maxMessagesPerHour = ctx.runtimeConfig.rateLimit.maxMessagesPerHour;
|
|
ctx.config.rateLimit.jitterMs = ctx.runtimeConfig.rateLimit.jitterMs;
|
|
ctx.config.browser.dryRun = ctx.runtimeConfig.browser.dryRun;
|
|
ctx.config.reddit.prioritizeRecentPosts = ctx.runtimeConfig.reddit.prioritizeRecentPosts;
|
|
ctx.config.reddit.recentWindowMinutes = ctx.runtimeConfig.reddit.recentWindowMinutes;
|
|
ctx.config.reddit.maxReadyCandidates = ctx.runtimeConfig.reddit.maxReadyCandidates;
|
|
|
|
if (parsedSubreddits?.success) {
|
|
const next: SubredditConfig[] = parsedSubreddits.data;
|
|
const seen = new Set<string>();
|
|
for (const s of next) {
|
|
const key = s.name.toLowerCase();
|
|
if (seen.has(key)) {
|
|
res.status(400).json({ error: `Duplicate subreddit: ${s.name}` });
|
|
return;
|
|
}
|
|
seen.add(key);
|
|
}
|
|
ctx.config.reddit.subreddits = next;
|
|
}
|
|
|
|
if (parsedAdvanced?.success) {
|
|
Object.assign(ctx.config.reddit, parsedAdvanced.data.reddit);
|
|
Object.assign(ctx.config.openrouter, parsedAdvanced.data.openrouter);
|
|
Object.assign(ctx.config.browser, parsedAdvanced.data.browser);
|
|
ctx.config.media = parsedAdvanced.data.media;
|
|
ctx.config.strategies = parsedAdvanced.data.strategies;
|
|
ctx.config.chatStrategies = parsedAdvanced.data.chatStrategies;
|
|
}
|
|
|
|
configStore.save(ctx.config);
|
|
|
|
ctx.getOnConfigChange()?.(ctx.runtimeConfig);
|
|
|
|
ctx.activity.push('info', 'Config updated via dashboard');
|
|
ctx.broadcaster.push('info', 'Configuration updated via dashboard', updates);
|
|
|
|
res.json({
|
|
success: true,
|
|
config: ctx.runtimeConfig,
|
|
subreddits: ctx.config.reddit.subreddits,
|
|
advanced: advancedConfig(ctx),
|
|
});
|
|
} catch (error) {
|
|
ctx.logger.error('Failed to update config:', error);
|
|
res.status(400).json({ error: String(error) });
|
|
}
|
|
});
|
|
|
|
return router;
|
|
}
|