Restructure chat service, web routes, and add test suite
Split monolithic chat service into modular structure with Matrix client support, extract web server into per-resource route modules, introduce bootstrap/main-loop separation, and add comprehensive vitest coverage across services, repositories, and utilities. Adds project docs and CLAUDE.md agent instructions.
This commit is contained in:
1 parent
dfa0580859
commit
633bb0ec04
84 files changed
+19278
-3888
No files matched your search
+59
-18
@@ -6,9 +6,21 @@ 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(z.string()).min(1),
|
||||
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']),
|
||||
@@ -52,6 +64,35 @@ export const ConfigSchema = z.object({
|
||||
'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(),
|
||||
}),
|
||||
@@ -60,39 +101,30 @@ export const ConfigSchema = z.object({
|
||||
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 }),
|
||||
autoReply: z.object({
|
||||
enabled: z.boolean().default(false),
|
||||
autoSend: z.boolean().default(false), // If true, send replies automatically. If false, require manual approval
|
||||
checkIntervalMs: z.number().default(300000), // 5 minutes
|
||||
upsellKeywords: z.array(z.string()).default([
|
||||
'onlyfans', 'only fans', 'fansly', 'manyvids', 'linktree', 'linktr.ee',
|
||||
'subscribe', 'premium', 'paid content', 'custom content', 'custom vid',
|
||||
'cashapp', 'cash app', 'venmo', 'paypal', 'tip me', 'spoil me',
|
||||
'wishlist', 'throne.com', 'link in bio', 'check my profile',
|
||||
'fanvue', 'loyalfans', 'cam show', 'buy my', 'selling',
|
||||
'dm me for price', 'menu', 'rates', 'ppv',
|
||||
]),
|
||||
}).default({ enabled: false, autoSend: false, checkIntervalMs: 300000, upsellKeywords: [] }),
|
||||
strategies: z.object({
|
||||
explorationRate: z.number().min(0).max(1).default(0.2),
|
||||
minSamples: z.number().default(5),
|
||||
@@ -102,10 +134,19 @@ export const ConfigSchema = z.object({
|
||||
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>;
|
||||
|
||||
Reference in new issue
Block a user