# Service Catalog Detailed documentation for each service in the RedditBot codebase. --- ## Table of Contents 1. [MonitorService](#monitorservice) 2. [ProcessorService](#processorservice) 3. [StrategySelector](#strategypicker) 4. [ResponseChecker](#responsechecker) 5. [ChatService](#chatservice) 6. [MediaService](#mediaservice) 7. [MediaSyncService](#mediasyncservice) 8. [AutoReplyService](#autoreplyservice) 9. [RedditApi](#redditapi) 10. [OpenRouterService](#openrouterservice) 11. [BrowserClient](#browserclient) 12. [MessagePage](#messagepage) 13. [RateLimiter](#ratelimiter) --- ## MonitorService **File**: `src/services/monitor.ts` **Lines**: 72 **Responsibility**: Discover new image posts across configured subreddits ### Purpose Polls configured subreddits at regular intervals, discovers new image posts, filters out already-processed posts and previously-messaged users, and returns the single newest valid post for processing. ### Public Methods ```typescript class MonitorService { constructor( private redditApi: RedditApi, private postsRepo: PostsRepository, private messagesRepo: MessagesRepository, private config: Config ) /** * Discover the next post to process. * Returns null if no valid posts are available. */ discoverNextPost(): Promise } ``` ### Algorithm 1. Shuffle subreddit order (randomize polling order each cycle) 2. For each subreddit: - Fetch new posts via `RedditApi.fetchPosts()` - Filter: image posts only - Filter: author not deleted - Filter: post not already in `processed_posts` - Filter: author not previously messaged (or meets re-message criteria) 3. Return the newest valid post across all subreddits 4. If no valid posts found, return `null` ### Dependencies - `RedditApi` - Fetch posts from Reddit - `PostsRepository` - Check for already-processed posts - `MessagesRepository` - Check for previously-messaged users - `Config` - Subreddit list, poll interval, target gender ### Error Handling - Skips subreddits that return errors (continues to next) - Logs errors at `warn` level - Returns `null` on any unrecoverable error --- ## ProcessorService **File**: `src/services/processor.ts` **Lines**: 132 **Responsibility**: Core processing pipeline for discovered posts ### Purpose Orchestrates the full processing pipeline for a single discovered post: image download, gender detection, AI analysis, strategy selection, message generation, and DM sending. ### Public Methods ```typescript class ProcessorService { constructor( private config: Config, private redditApi: RedditApi, private openrouter: OpenRouterService, private strategySelector: StrategySelector, private messageSender: MessagePage, private postsRepo: PostsRepository, private messagesRepo: MessagesRepository, private successfulRepo: SuccessfulMessagesRepository, private logger: Logger ) /** * Process a single discovered post through the full pipeline. */ processPost(post: Post): Promise } ``` ### Pipeline Steps ``` 1. Download image └─► ImageService.downloadImage(url) └─► Validate MIME type (image/jpeg, image/png, image/webp) └─► Validate file size └─► Convert to base64 2. Quick gender detection (cost-saving pre-filter) └─► OpenRouter.detectGenderOnly(image) └─► Skip if gender doesn't match targetGender 3. Select A/B strategy variant └─► StrategySelector.selectStrategy(subreddit, author) 4. Full AI analysis └─► OpenRouter.analyzeImage(image) └─► Detect gender, describe image └─► Handle refusal (mark post, try next) 5. Generate message └─► OpenRouter.generateMessage(analysis, title, examples, strategy) └─► Few-shot examples from SuccessfulMessagesRepository 6. Send DM └─► MessagePage.sendDM(author, "Stunning", message) └─► Handle failure (log, don't mark as sent) 7. Record in database └─► Mark post as processed └─► Mark message_sent = 1 if successful └─► Store generated_message for reference ``` ### Error Handling - AI refusal: Post is marked as processed with error, skipped - Image download failure: Post is marked with error, skipped - DM send failure: Post is marked as not sent, logged - All errors logged with context (post ID, subreddit, author) ### Cost Optimization - Quick gender detection costs less than full analysis - Non-matching genders are skipped before expensive analysis - AI refusal detection prevents wasted tokens on rejected prompts --- ## StrategySelector **File**: `src/services/strategy-selector.ts` **Lines**: 101 **Responsibility**: A/B testing with explore/exploit algorithm ### Purpose Selects message variants (strategies) for each post using an explore/exploit algorithm based on historical response rates. Optimizes for maximum response rate over time while continuing to explore under-tested variants. ### Public Methods ```typescript class StrategySelector { constructor( private config: Config, private messagesRepo: MessagesRepository ) /** * Select the best strategy variant for a given subreddit/author. */ selectStrategy(subreddit: string, author: string): string } ``` ### Algorithm 1. **Get strategy stats** from `MessagesRepository`: - Response count per variant - Sent count per variant - Response rate per variant 2. **Apply minimum sample threshold**: - If a variant has fewer than `minSamples` sends, treat it as untested - Untested variants are excluded from exploitation 3. **Explore vs Exploit decision**: - With probability `explorationRate`, select a random variant (explore) - Otherwise, select the variant with the highest response rate (exploit) 4. **Fallback**: - If no variants have enough data, select randomly from enabled variants ### Configuration ```yaml strategyVariants: - name: "variant_a" prompt: "Compliment-based message..." enabled: true - name: "variant_b" prompt: "Question-based message..." enabled: true ``` ### Dependencies - `Config` - Strategy variants, exploration rate, min samples - `MessagesRepository` - Response rate statistics --- ## ResponseChecker **File**: `src/services/response-checker.ts` **Lines**: 179 **Responsibility**: Monitor inbox for user replies ### Purpose Background service that navigates the Reddit inbox via Playwright, identifies responses to bot-sent messages, updates the database, and stores successful message/response pairs for few-shot learning. ### Public Methods ```typescript class ResponseCheckerService { constructor( private browser: BrowserClient, private messagesRepo: MessagesRepository, private successfulRepo: SuccessfulMessagesRepository, private logger: Logger ) /** * Check inbox for responses to sent messages. */ checkResponses(): Promise } ``` ### Algorithm 1. Navigate to `/message/inbox` via Playwright 2. Parse inbox items (messages, conversations) 3. For each response item: - Extract recipient username and message text - Match to `sent_messages` by recipient + timestamp proximity - Update `got_response = 1` and `response_checked_at` - Store the message/response pair in `successful_messages` 4. Update strategy statistics (response rates) ### Dependencies - `BrowserClient` - Playwright browser context - `MessagesRepository` - Update response flags - `SuccessfulMessagesRepository` - Store learning pairs - `Logger` - Activity logging --- ## ChatService **File**: `src/services/chat.ts` **Lines**: 1123+ **Responsibility**: Reddit chat scraping, messaging, and conversation management ### Purpose The most complex service in the codebase. Manages Reddit chat interactions including scraping active conversations, loading conversation history, sending messages with human-like delays, detecting upsell/spam, and managing conversation caches. ### Public Methods ```typescript class ChatService { constructor( private browser: BrowserClient, private config: Config, private logger: Logger ) /** Fetch active conversations from Reddit */ fetchChats(): Promise /** Load full conversation history */ loadConversation(chatId: string): Promise /** Send a message in a conversation */ sendMessage(chatId: string, text: string): Promise /** Get cached conversations */ getCachedConversations(): Conversation[] /** Check for new messages in conversations */ checkForUpdates(): Promise /** Detect upsell/spam in a message */ detectUpsell(message: string): boolean } ``` ### Shadow DOM Handling Reddit uses custom `faceplate-*` web components with shadow DOM. Standard CSS selectors cannot pierce these boundaries. The service uses JavaScript injection via `page.evaluate()`: ```javascript // Traverse shadow roots to find chat elements function traverseShadowRoots(root) { const elements = root.querySelectorAll('.some-class'); const shadowChildren = root.querySelectorAll('*::shadow, ::deep'); // ... recursive traversal } ``` ### Conversation Cache Conversations are cached in two layers: 1. **In-memory**: Fast access for active conversations 2. **Disk JSON**: Persistence across restarts at `data/chats/` Cache structure: ```json { "chatId": "abc123", "participants": ["user1", "bot"], "messages": [ { "author": "user1", "text": "Hey!", "timestamp": 1234567890 } ], "lastUpdated": 1234567890, "hasNewMessages": false } ``` ### Human-Like Messaging Messages are sent with: - Typing delay simulation (random delay before "typing" starts) - Variable typing speed - Random delays between messages - Delivery verification ### Known Issues - **Hardcoded username**: `myUsername = 'Upset_Marsupial1006'` is hardcoded in JS evaluation scripts. Should be parameterized from `Config`. - **Size**: 1123+ lines suggests this should be split into smaller services. ### Dependencies - `BrowserClient` - Playwright browser context - `Config` - Rate limits, username (should be), check intervals - `Logger` - Activity logging --- ## MediaService **File**: `src/services/media.ts` **Lines**: 254 **Responsibility**: Media download, caching, and indexing ### Purpose Downloads images from chat conversations, resolves Matrix blurred thumbnails to full resolution, stores files locally, and maintains a deduplicated index. ### Public Methods ```typescript class MediaService { constructor( private config: Config, private browser: BrowserClient, private logger: Logger ) /** Download media from a conversation */ downloadMedia(chatId: string): Promise /** Get media index */ getIndex(): MediaIndex /** Get a specific media file path */ getPath(hash: string): string | null } ``` ### Deduplication Files are deduplicated using SHA-1 hashing: ``` SHA-1(image_bytes) → data/media/{hash}.{ext} ``` The index maps hashes to file paths: ```json { "abc123def456": "abc123def456.jpg", "789xyz012abc": "789xyz012abc.png" } ``` ### Matrix Thumbnail Resolution Reddit's Matrix system uses blurred placeholder thumbnails. The service resolves these to full resolution by: 1. Detecting Matrix thumbnail URLs 2. Extracting the full-resolution URL from the response 3. Downloading the full-resolution image ### Dependencies - `Config` - Media directory path - `BrowserClient` - Playwright for Matrix resolution - `Logger` - Download activity --- ## MediaSyncService **File**: `src/services/media-sync.ts` **Lines**: 119 **Responsibility**: Periodic background media synchronization ### Purpose Runs periodic background syncs to download media from chat conversations. Supports both incremental (top 10 chats) and full (all conversations) sync modes. ### Public Methods ```typescript class MediaSyncService { constructor( private config: Config, private mediaService: MediaService, private chatService: ChatService, private logger: Logger ) /** Start periodic sync */ start(): void /** Stop periodic sync */ stop(): void /** Trigger incremental sync */ syncIncremental(): Promise /** Trigger full sync */ syncFull(): Promise /** Get sync status */ getStatus(): SyncStatus } ``` ### Sync Modes | Mode | Description | Use Case | |---|---|---| | Incremental | Top 10 most recent conversations | Regular background sync | | Full | All conversations | Initial setup, catch-up | ### Dependencies - `Config` - Sync interval, enabled flag - `MediaService` - Actual download logic - `ChatService` - Conversation list - `Logger` - Sync progress --- ## AutoReplyService **File**: `src/services/auto-reply.ts` **Lines**: 199 **Responsibility**: Automated reply to chat conversations ### Purpose Monitors cached conversations for unread messages, detects upsell/spam, generates AI reply suggestions, and either auto-sends or marks as pending for manual approval. ### Public Methods ```typescript class AutoReplyService { constructor( private config: Config, private chatService: ChatService, private openrouter: OpenRouterService, private logger: Logger ) /** Start auto-reply checking */ start(): void /** Stop auto-reply checking */ stop(): void /** Check for unread messages and generate replies */ checkAndReply(): Promise } ``` ### Workflow ``` 1. Check cached conversations for unread messages │ ├─► Skip if no new messages │ ├─► Detect upsell/spam keywords │ └─► Flag conversation, skip reply │ ├─► Generate AI reply suggestion │ └─► OpenRouter.generateReply(conversation history) │ ├─► If autoSend: send reply immediately │ └─► ChatService.sendMessage() │ └─► If !autoSend: mark as pending └─► Dashboard shows pending replies for approval ``` ### Dependencies - `Config` - Enabled flag, autoSend flag, check interval - `ChatService` - Conversation access, message sending - `OpenRouterService` - Reply generation - `Logger` - Activity logging --- ## RedditApi **File**: `src/reddit/api.ts` **Lines**: 233 **Responsibility**: Reddit API client ### Purpose Axios-based client for interacting with Reddit's public API. Handles post fetching, user profile checking, and authenticated requests using Playwright browser cookies. ### Public Methods ```typescript class RedditApi { constructor( private browser: BrowserClient, private logger: Logger ) /** Fetch new posts from a subreddit */ fetchPosts(subreddit: string, limit?: number): Promise /** Check if a user is a seller/spam */ checkUserProfile(username: string): Promise /** Make authenticated request using browser cookies */ authenticatedRequest(url: string): Promise } ``` ### Image URL Resolution Fallback chain for extracting image URLs from post data: ``` 1. post.preview.reddits[0].url (Reddit CDN preview) 2. post.url (i.redd.it direct) 3. post.thumbnail (if not self/spam) 4. Scan post.selftext for imgur/image links ``` ### Seller/Spam Detection Checks user profile for keywords: - "seller" - "onlyfans" - "link in bio" - Other configured `sellerKeywords` ### Dependencies - `BrowserClient` - Browser cookies for authentication - `Logger` - Request logging --- ## OpenRouterService **File**: `src/ai/openrouter.ts` **Lines**: 307 **Responsibility**: OpenRouter AI client ### Purpose Client for the OpenRouter API (OpenAI-compatible SDK). Provides image analysis, message generation, reply suggestions, and upsell detection. ### Public Methods ```typescript class OpenRouterService { constructor(private config: Config, private logger: Logger) /** Full image analysis with model fallback */ analyzeImage(imageBase64: string): Promise /** Quick gender detection (cost-saving) */ detectGenderOnly(imageBase64: string): Promise /** Generate DM message */ generateMessage( imageAnalysis: ImageAnalysis, postTitle: string, strategy: string ): Promise /** Generate chat reply */ generateReply(conversationHistory: Message[]): Promise /** Detect upsell/spam keywords */ detectUpsell(message: string): boolean } ``` ### Model Fallback Chain ``` Primary model (config.openrouter.model) │ └─► If refusal detected: │ └─► Try each fallback model (config.openrouter.fallbackModels) │ └─► If all fail: throw error ``` ### Refusal Detection Heuristic patterns to detect when AI models refuse to process prompts: ```typescript const refusalPatterns = [ /cannot.*analyze/i, /unable.*process/i, /against.*policy/i, /cannot.*view/i, // ... ]; ``` ### Few-Shot Learning `generateMessage()` retrieves successful message/response pairs from the database and includes them as examples in the prompt: ``` "Here are examples of messages that got responses: - Example 1: {original_message} → {response_snippet} - Example 2: ... Generate a similar message for this image..." ``` ### Dependencies - `Config` - API key, model selection, prompts - `MessagesRepository` - Successful examples for few-shot learning --- ## BrowserClient **File**: `src/browser/client.ts` **Lines**: 302 **Responsibility**: Playwright persistent context manager ### Purpose Manages the Playwright browser session with persistent cookies/storage. Handles automated login, session verification, and 2FA/captcha detection. ### Public Methods ```typescript class BrowserClient { constructor(private config: Config, private logger: Logger) /** Initialize browser context */ initialize(): Promise /** Verify browser session is active */ verifySession(): Promise /** Perform automated login */ login(): Promise /** Get page context for operations */ getPage(): Promise /** Close browser */ close(): Promise } ``` ### Login Flow ``` 1. Navigate to Reddit login page 2. Fill username and password 3. Submit form 4. Check for 2FA prompt └─► If present: wait for manual input (with timeout) 5. Check for captcha └─► If present: log warning, wait 6. Verify login success: └─► Check for user menu indicator └─► Check for profile avatar └─► Fallback: API request to /api/me 7. Save cookies to persistent directory 8. On failure: capture debug screenshot ``` ### Session Persistence - Cookies and storage are saved to `config.browser.userDataDir` - Subsequent launches restore the session automatically - Session expires when Reddit cookies expire (requires re-login) ### Dependencies - `Config` - Credentials, headless mode, user data dir - `Logger` - Login progress, errors --- ## MessagePage **File**: `src/browser/message-page.ts` **Lines**: 560 **Responsibility**: Reddit DM compose page automation ### Purpose Automates the Reddit DM compose page: filling recipient/subject/body fields, handling shadow DOM components, submitting messages, and verifying success. ### Public Methods ```typescript class MessagePage { constructor( private browser: BrowserClient, private config: Config, private logger: Logger ) /** Send a DM to a recipient */ sendDM(recipient: string, subject: string, body: string): Promise /** Check if recipient field shows an error */ hasRecipientError(page: Page): Promise } ``` ### Shadow DOM Handling Reddit's compose form uses `faceplate-*` web components: ```javascript // Fill shadow DOM input fields page.evaluate(({ selector, value }) => { const shadowInput = document.querySelector(`${selector} input`); if (shadowInput) { shadowInput.value = value; shadowInput.dispatchEvent(new Event('input', { bubbles: true })); } }, { selector: 'faceplate-textarea[name="body"]', value: body }); ``` ### Dry Run Mode When `config.dryRun` is enabled: - All fields are filled as normal - Submit button is NOT clicked - Message is logged but not sent - Returns `true` (simulates success) ### Dependencies - `BrowserClient` - Playwright page access - `Config` - Dry run mode - `Logger` - Send activity --- ## RateLimiter **File**: `src/utils/rate-limiter.ts` **Lines**: 114 **Responsibility**: Token-bucket rate limiting ### Purpose Enforces rate limits on bot actions (primarily message sending) to avoid detection and bans. Uses a combination of minimum delay, random jitter, and hourly caps. ### Public Methods ```typescript class RateLimiter { constructor(config: RateLimitConfig) /** Wait until a slot is available */ waitForSlot(): Promise /** Get remaining time until next slot */ getTimeUntilSlot(): number /** Get current hourly count */ getHourlyCount(): number /** Reset hourly counter */ resetHourlyCounter(): void } ``` ### Rate Limiting Strategy ``` 1. Check hourly cap: └─► If maxMessagesPerHour reached: wait until hour rolls over 2. Enforce minimum delay: └─► lastActionTime + minDelayMs + random(0, jitterMs) 3. Track action timestamp 4. Increment hourly counter ``` ### Dependencies - `Config` - Rate limit settings - `p-queue` - Concurrency control