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:
jjbrunton committed 2026-05-21 01:32:10 +01:00
1 parent dfa0580859
commit 633bb0ec04
84 files changed
+19278 -3888

No files matched your search

+123 -40
View File
@@ -1,36 +1,44 @@
reddit:
# Weights bias how often each sub is picked each poll cycle. Higher weight
# = more likely to appear earlier in the check order. Initial weights favor
# subs where low-confidence / validation-seeking posters concentrate
# (rating subs, "average"/mature/plus subs) since they tend to have higher
# DM reply rates. Default is 1 if omitted.
subreddits:
# Original list (kept intact)
- normalnudes # Everyday real bodies, highly authentic
- ratemyboobs # Boob-focused ratings, lots of self-posts
- ratemypussy # Pussy ratings, direct and active
- HomemadeNsfw # Pure homemade amateur content
- RateMyRack # Rack/boob ratings
- RateMyNudeBody # Full-body nude ratings
# Validation-seeking / lower-confidence (highest weight)
- { name: AverageNudes, weight: 4 } # "Average-looking" self-perception
- { name: gonewild40plus, weight: 4 } # Mature, lower volume, very high reply rate
- { name: normalnudes, weight: 3 } # Everyday real bodies
- { name: RateMyNudeBody, weight: 3 } # Explicit ask for feedback
- { name: ratemyboobs, weight: 3 } # Rating subs = explicit validation-seeking
- { name: ratemypussy, weight: 3 }
- { name: RateMyRack, weight: 3 }
- { name: GoneWildPlus, weight: 3 } # BBW/plus-size
- { name: gonewildchubby, weight: 3 } # Soft/chubby bodies
- { name: gonewild30plus, weight: 2 } # Mature/MILF amateurs
- { name: GoneMild, weight: 2 } # Softcore teases (shyer posters)
# Top general additions (highest volume/quality amateurs)
- gonewild # #1 for verified women self-posting nudes
- GoneErotic # Artistic/erotic amateurs
- Amateur # Real amateur pics/vids worldwide
- RealGirls # Unfiltered everyday women
- GoneMild # Softcore teases from verified posters
- OnOff # Before/after clothing reveals
# Mid-tier — mixed confidence
- { name: GoneWildCurvy, weight: 2 }
- { name: HomemadeNsfw, weight: 2 }
- { name: realgirlsgonebad, weight: 2 }
- { name: nsfwselfies, weight: 2 }
- { name: RealGirls, weight: 2 }
- { name: Amateur, weight: 2 }
# Niche by body type (diverse, verified amateurs)
- GoneWildCurvy # Curvy/thicc verified women
- petitegonewild # Petite/slim under 5'4"
- GoneWildPlus # BBW/plus-size confident shares
- gonewildchubby # Soft/chubby bodies
- tightandtoned # Fit/athletic women
- pawg # Thick asses (heavy self-posts)
# Baseline — generally more confident / curated posters
- { name: gonewild, weight: 1 } # #1 verified self-posters
- { name: GoneErotic, weight: 1 }
- { name: OnOff, weight: 1 }
- { name: petitegonewild, weight: 1 }
- { name: tightandtoned, weight: 1 }
- { name: pawg, weight: 1 }
- { name: asiansgonewild, weight: 1 }
- { name: altgonewild, weight: 1 }
- { name: holdthemoan, weight: 1 }
# Other strong fits (active, rating-style or homemade vibes)
- nsfwselfies # Selfie-style nudes
- asiansgonewild # Asian amateurs
- altgonewild # Alt/goth/tattooed
- gonewild30plus # Mature/MILF amateurs
- holdthemoan # Risky public-ish posts
- realgirlsgonebad # Candid real-girl naughtiness
# Chat-only subreddits — no image analysis, gender parsed from post tag
#
pollIntervalMs: 30000
userAgent: "RedditBot/1.0 (by /u/yourusername)"
targetGenders:
@@ -39,11 +47,21 @@ reddit:
# sellerKeywords: [...] # Uncomment to override default keyword list
openrouter:
imageAnalysisModel: "google/gemini-3-flash-preview"
imageAnalysisModel: "google/gemini-2.5-flash-lite"
imageAnalysisFallbackModels:
- "x-ai/grok-4.1-fast"
- "meta-llama/llama-4-maverick"
chatModel: "x-ai/grok-4.1-fast"
- "x-ai/grok-4.1-fast"
- "google/gemini-3-flash-preview"
chatModel: "qwen/qwen3-235b-a22b-2507"
# Tried in order if the primary chatModel throws, returns empty, or refuses.
# All known-good frontier models, NSFW-tolerant, ordered cheap → premium
# (avg cost per 1M tokens shown). Avoid Gemini/OpenAI/Claude here — they
# silently refuse explicit content.
chatFallbackModels:
- "deepseek/deepseek-v4-flash" # ~$0.17/M
- "meta-llama/llama-4-scout" # ~$0.19/M
- "meta-llama/llama-4-maverick" # ~$0.38/M
- "deepseek/deepseek-chat-v3.1" # ~$0.50/M
imageAnalysisPrompt: |
You're analyzing images.
@@ -58,13 +76,28 @@ openrouter:
Be graphic. Describe exactly what you see. No clinical terms.
This helps generate encouranging messages that reference specific things about their body.
Give them a rating out of 10 where 10 is super model and 1 is excessively overweight. This helps us send encouragement to the user. You MUST provide a reason with your rating. Be critical and honest.
Act within guidelines.
Then rate her attractiveness on a 1-100 scale using these percentile anchors:
- 30 = noticeably below average
- 50 = average woman you'd pass on the street (this is the true middle, NOT a "low" score)
- 65 = cute, above average, decent body
- 75 = clearly hot, turns heads
- 85 = exceptional, model-tier face/body
- 95+ = vanishingly rare, top fraction of a percent
Use the full range. Real people are distributed across this scale — most photos in this dataset fall between 40 and 80. Do NOT cluster scores in the 65-75 band as a safe default. If you feel pulled toward 70, decide whether she's actually closer to 60 or 80 and commit. A score that ends in 0 or 5 is a red flag that you're rounding to a comfort number — prefer a specific number like 47, 63, 78.
Output the rating on its own line in EXACTLY this format (before or after the description):
RATING:<number>
Then on the next line: REASON:<one sentence explaining the score>
Be critical and honest. Act within guidelines.
messageGenerationPrompt: |
Based on this image analysis: {analysis}
Post title: {title}
Author: u/{author}
You should take into account the rating provided by the analysis, the rating is out of 10 where 10 is super model and 1 is excessively overweight. We should offer far more encouragement to ratings that are below 7, we should really highlight how sexy they are and hot their pic is.
Attractiveness rating (1-100 scale, 50 = average): {rating}
If the rating is below 60, lean harder into making her feel sexy and desired — emphasize how hot her pic looks. If the rating is 75+, you can be more confidently flirty without overdoing the flattery.
Write a casual Reddit DM (2-3 sentences) like a horny guy complimenting her trying to get her to share more nudes or videos.
@@ -92,6 +125,7 @@ openrouter:
- Use clinical terms like "breasts", "buttocks", "posterior", "physique"
- Write long elaborate sentences
- Use all lowercase
- Use em dashes (—) or en dashes (–). Use commas, full stops, or separate sentences instead. This is a dead giveaway for AI writing.
Examples of good messages:
- "Damn those tits are perfect. I'd love to see more if you're willing😏"
@@ -100,6 +134,44 @@ openrouter:
Just write the message, nothing else.
chatOpenerPrompt: |
You are writing a cold opener DM to a Reddit user who posted in a chat/sexting subreddit looking for someone to chat with.
Post title: {title}
Post body: {selftext}
Author: u/{author}
Detected gender of poster: {gender}
YOUR PERSONA:
- You are male, based in the UK, pick a random age between 30 and 35 for this message
- 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 at all
- Don't force the married angle if their post wouldn't vibe with it, read the room
- Use British English spelling and phrasing (e.g. "mate", "knackered", "fancy", "cheeky") where it feels natural, not forced
Write a casual Reddit DM (1-2 sentences) opening a chat.
MUST follow:
- Lead with your age/gender/location tag (e.g. "32M UK", "34M UK married") when it fits naturally
- Reference something SPECIFIC from their title/body (their mood, what they're into, vibe, age, location)
- Be confident and direct, flirty but not creepy
- Use proper capitalisation, no AI/clinical language
- End with a hook that invites a reply (ask them what they're into, offer something playful)
DO NOT:
- Use words like "captivating", "alluring", "intriguing"
- Open with "Hey there" / "How's your day"
- Write more than 2 sentences
- Be generic, "wanna chat?" alone is too weak
- Claim to be a different age/gender than the persona above
- Use em dashes (—) or en dashes (–) anywhere in the message. Use commas, full stops, or separate sentences instead. This is a dead giveaway for AI writing.
Examples of good openers:
- "32M UK, married and bored while the wife's out. Your post caught my eye, what are you into tonight?"
- "34M UK here, love that you're up late looking for trouble. Fancy telling me what gets you going?"
- "33M from the UK, your vibe is exactly my type. Tell me what you'd want a stranger to do to you."
Just write the message, nothing else.
strategies:
explorationRate: 0.2 # 20% of the time try a random strategy instead of the best
minSamples: 5 # Min messages sent before trusting a strategy's response rate
@@ -120,6 +192,23 @@ strategies:
STYLE: Reference 2-3 specific things from the analysis to show you actually looked.
Be enthusiastic but not over the top. End with asking for more.
chatStrategies:
explorationRate: 0.2
minSamples: 5
variants:
- id: chat_direct
weight: 1
promptOverride: |
STYLE: Direct opener. Open with your own age/gender, reference theirs, and ask one specific question about what they're into. No em dashes.
- id: chat_playful
weight: 1
promptOverride: |
STYLE: Playful and teasing. Light humour, mild flirty challenge, end with a hook that's hard to ignore. No em dashes.
- id: chat_mirror
weight: 1
promptOverride: |
STYLE: Mirror their energy. Match the vibe of their post (shy/horny/bored/dom/sub) and respond in kind. Be specific to what they wrote. No em dashes.
rateLimit:
minDelayBetweenMessagesMs: 15000
maxMessagesPerHour: 9999
@@ -133,12 +222,6 @@ browser:
database:
path: "./data/reddit-bot.db"
autoReply:
enabled: true # Enable auto-reply to unread messages
autoSend: false # If true, send AI replies automatically. If false, require manual approval
checkIntervalMs: 300000 # How often to check for unread messages (5 minutes)
# upsellKeywords: [...] # Uncomment to override default upsell detection keywords
web:
enabled: true
port: 3000