Files
RedditBot/src/config/index.ts
T
jjbruntonandClaude Opus 4.7 2260388dc8 Blur gallery images by default, sync progress UI, and add chat/monitor tests
- Blur NSFW media gallery images until hover/reveal
- Surface chat sync progress in the dashboard header
- Extend chat service, cache, and monitor with delta-media handling
- Add tests for chat cooldown, delta media, and monitor flows

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 10:01:09 +01:00

201 lines
7.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { z } from 'zod';
import fs from 'fs';
import path from 'path';
import YAML from 'yaml';
import dotenv from 'dotenv';
dotenv.config();
export const SubredditConfigSchema = z.object({
name: z.string(),
type: z.enum(['image', 'chat']).default('image'),
weight: z.number().positive().optional(),
});
export type SubredditConfig = z.infer<typeof SubredditConfigSchema>;
const SubredditEntrySchema = z
.union([z.string().transform((s) => ({ name: s })), z.object({}).passthrough()])
.pipe(SubredditConfigSchema);
export const ConfigSchema = z.object({
reddit: z.object({
subreddits: z.array(SubredditEntrySchema).min(1),
pollIntervalMs: z.number().default(120000),
userAgent: z.string().default('RedditBot/1.0'),
targetGenders: z.array(z.enum(['male', 'female', 'other', 'unknown'])).default(['female']),
filterSellers: z.boolean().default(true),
prioritizeRecentPosts: z.boolean().default(false),
recentWindowMinutes: z.number().positive().default(60),
sellerKeywords: z
.array(z.string())
.default([
'onlyfans',
'only fans',
'fansly',
'manyvids',
'linktree',
'linktr.ee',
'allmylinks',
'beacons.ai',
'cash.app',
'cashapp',
'venmo',
'paypal',
'subscribe',
'premium snap',
'premiumsnap',
'fan.ly',
'fanvue',
'loyalfans',
'pornhub.com',
'xvideos.com',
'chaturbate',
'myfreecams',
'cam girl',
'camgirl',
'buy my',
'custom content',
'custom vid',
'selling content',
'paid content',
'tip me',
'spoil me',
'findom',
'send tribute',
'wishlist',
'throne.com',
]),
sellerDomains: z
.array(z.string())
.default([
'onlyfans.com',
'fansly.com',
'fanvue.com',
'loyalfans.com',
'manyvids.com',
'linktr.ee',
'allmylinks.com',
'beacons.ai',
'beacons.page',
'throne.com',
'throne.me',
'fan.ly',
'chaturbate.com',
'myfreecams.com',
'pornhub.com',
'xvideos.com',
'cash.app',
'cashapp.com',
'venmo.com',
'paypal.me',
'paypal.com',
'admireme.vip',
'fancentro.com',
'justforfans.app',
'justfor.fans',
]),
username: z.string().optional(),
password: z.string().optional(),
}),
openrouter: z.object({
apiKey: z.string(),
imageAnalysisModel: z.string().default('google/gemini-3-flash-preview'),
imageAnalysisFallbackModels: z.array(z.string()).default([]),
chatModel: z.string().default('google/gemini-3-flash-preview'),
chatFallbackModels: z.array(z.string()).default([]),
imageAnalysisPrompt: z.string(),
messageGenerationPrompt: z.string(),
chatOpenerPrompt: z.string().default(
'You are writing a cold opener DM to a Reddit user who just posted in a chat/sexting subreddit looking for someone to chat with.\n\nPost title: {title}\nPost body: {selftext}\nAuthor: u/{author}\nDetected gender of poster: {gender}\n\nYOUR PERSONA:\n- You are male, based in the UK, pick a random age between 30 and 35 for this message\n- Roughly half the time, mention (or imply) you\'re married/have a wife and are looking for something on the side; the other half, don\'t mention relationship status\n- Don\'t force the married angle if their post wouldn\'t vibe with it\n- Use British English spelling and phrasing where it feels natural\n\nWrite a casual Reddit DM (1-2 sentences) opening a chat.\n\nMUST follow:\n- Lead with your age/gender/location tag (e.g. "32M UK", "34M UK married") when it fits naturally\n- Reference something SPECIFIC from their title/body (their mood, what they\'re into, vibe)\n- Be confident and direct, not formal\n- Use proper capitalisation, no AI/clinical language\n- End with a hook that invites a reply (ask them something, or offer something playful)\n\nDO NOT:\n- Use words like "captivating", "alluring", "intriguing"\n- Open with "Hey there" / "How\'s your day"\n- Write more than 2 sentences\n- Claim to be a different age/gender than the persona above\n- Use em dashes (—) or en dashes (–). Use commas, full stops, or separate sentences instead.\n\nJust write the message, nothing else.',
),
}),
rateLimit: z.object({
minDelayBetweenMessagesMs: z.number().default(300000),
maxMessagesPerHour: z.number().default(8),
jitterMs: z.number().default(60000),
}).default({ minDelayBetweenMessagesMs: 300000, maxMessagesPerHour: 8, jitterMs: 60000 }),
browser: z.object({
sessionDir: z.string().default('./data/browser-session'),
headless: z.boolean().default(false),
dryRun: z.boolean().default(false),
}).default({ sessionDir: './data/browser-session', headless: false, dryRun: false }),
database: z.object({
path: z.string().default('./data/reddit-bot.db'),
}).default({ path: './data/reddit-bot.db' }),
media: z.object({
syncIntervalMs: z.number().default(1800000), // 30 minutes
syncEnabled: z.boolean().default(true),
}).default({ syncIntervalMs: 1800000, syncEnabled: true }),
strategies: z.object({
explorationRate: z.number().min(0).max(1).default(0.2),
minSamples: z.number().default(5),
variants: z.array(z.object({
id: z.string(),
weight: z.number().default(1),
promptOverride: z.string(),
})).default([]),
}).default({ explorationRate: 0.2, minSamples: 5, variants: [] }),
chatStrategies: z.object({
explorationRate: z.number().min(0).max(1).default(0.2),
minSamples: z.number().default(5),
variants: z.array(z.object({
id: z.string(),
weight: z.number().default(1),
promptOverride: z.string(),
})).default([]),
}).default({ explorationRate: 0.2, minSamples: 5, variants: [] }),
web: z.object({
enabled: z.boolean().default(true),
port: z.number().default(3000),
}).default({ enabled: true, port: 3000 }),
});
export type Config = z.infer<typeof ConfigSchema>;
export function loadConfig(): Config {
const configPath = path.resolve(process.cwd(), 'config.yaml');
if (!fs.existsSync(configPath)) {
throw new Error(`Config file not found at ${configPath}`);
}
const configFile = fs.readFileSync(configPath, 'utf-8');
const rawConfig = YAML.parse(configFile);
// Inject API key from environment
if (!rawConfig.openrouter) {
rawConfig.openrouter = {};
}
rawConfig.openrouter.apiKey = process.env.OPENROUTER_API_KEY || rawConfig.openrouter.apiKey;
// Inject Reddit credentials from environment
if (process.env.REDDIT_USERNAME) {
rawConfig.reddit.username = process.env.REDDIT_USERNAME;
}
if (process.env.REDDIT_PASSWORD) {
rawConfig.reddit.password = process.env.REDDIT_PASSWORD;
}
// Override browser settings from environment
if (process.env.BROWSER_HEADLESS !== undefined) {
if (!rawConfig.browser) rawConfig.browser = {};
rawConfig.browser.headless = process.env.BROWSER_HEADLESS === 'true';
}
if (!rawConfig.openrouter.apiKey) {
throw new Error('OPENROUTER_API_KEY environment variable is required');
}
const result = ConfigSchema.safeParse(rawConfig);
if (!result.success) {
const errors = result.error.issues
.map((e) => ` - ${String(e.path.join('.'))}: ${e.message}`)
.join('\n');
throw new Error(`Invalid configuration:\n${errors}`);
}
return result.data;
}