Files
RedditBot/docs/services.md
T

19 KiB

Service Catalog

Detailed documentation for each service in the RedditBot codebase.


Table of Contents

  1. MonitorService
  2. ProcessorService
  3. StrategySelector
  4. ResponseChecker
  5. ChatService
  6. MediaService
  7. MediaSyncService
  8. AutoReplyService
  9. RedditApi
  10. OpenRouterService
  11. BrowserClient
  12. MessagePage
  13. 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

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<Post | null>
}

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

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<void>
}

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

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

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

class ResponseCheckerService {
  constructor(
    private browser: BrowserClient,
    private messagesRepo: MessagesRepository,
    private successfulRepo: SuccessfulMessagesRepository,
    private logger: Logger
  )

  /**
   * Check inbox for responses to sent messages.
   */
  checkResponses(): Promise<void>
}

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

class ChatService {
  constructor(
    private browser: BrowserClient,
    private config: Config,
    private logger: Logger
  )

  /** Fetch active conversations from Reddit */
  fetchChats(): Promise<ChatSummary[]>

  /** Load full conversation history */
  loadConversation(chatId: string): Promise<Conversation>

  /** Send a message in a conversation */
  sendMessage(chatId: string, text: string): Promise<boolean>

  /** Get cached conversations */
  getCachedConversations(): Conversation[]

  /** Check for new messages in conversations */
  checkForUpdates(): Promise<Conversation[]>

  /** 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():

// 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:

{
  "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

class MediaService {
  constructor(
    private config: Config,
    private browser: BrowserClient,
    private logger: Logger
  )

  /** Download media from a conversation */
  downloadMedia(chatId: string): Promise<string[]>

  /** 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:

{
  "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

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<void>

  /** Trigger full sync */
  syncFull(): Promise<void>

  /** 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

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<void>
}

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

class RedditApi {
  constructor(
    private browser: BrowserClient,
    private logger: Logger
  )

  /** Fetch new posts from a subreddit */
  fetchPosts(subreddit: string, limit?: number): Promise<Post[]>

  /** Check if a user is a seller/spam */
  checkUserProfile(username: string): Promise<UserProfile>

  /** Make authenticated request using browser cookies */
  authenticatedRequest(url: string): Promise<any>
}

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

class OpenRouterService {
  constructor(private config: Config, private logger: Logger)

  /** Full image analysis with model fallback */
  analyzeImage(imageBase64: string): Promise<ImageAnalysis>

  /** Quick gender detection (cost-saving) */
  detectGenderOnly(imageBase64: string): Promise<string>

  /** Generate DM message */
  generateMessage(
    imageAnalysis: ImageAnalysis,
    postTitle: string,
    strategy: string
  ): Promise<string>

  /** Generate chat reply */
  generateReply(conversationHistory: Message[]): Promise<string>

  /** 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:

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 headless Playwright context used after an extension dump is imported. It accepts Reddit cookies, verifies account identity, and supplies authenticated context to the remaining browser-backed operations. It never performs login.

Public Methods

class BrowserClient {
  constructor(private config: Config, private logger: Logger)

  /** Initialize browser context */
  initialize(): Promise<void>

  /** Verify browser session is active */
  verifySession(): Promise<boolean>

  /** Import Reddit cookies from a validated extension dump */
  importRedditCookies(cookies: Cookie[]): Promise<void>

  /** Get page context for operations */
  getPage(): Promise<Page>

  /** Close browser */
  close(): Promise<void>
}

Import Flow

The dashboard validates a combined extension dump, verifies Matrix with whoami, imports Reddit cookies, verifies Reddit identity, and only then marks the shared account gate authenticated.

Session Persistence

  • Cookies and storage are saved to config.browser.userDataDir
  • Subsequent launches restore the session automatically
  • Session expiry pauses account work until a fresh extension dump is imported

Dependencies

  • Config - Persistent session directory
  • Logger - Import verification progress and errors

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

class RateLimiter {
  constructor(config: RateLimitConfig)

  /** Wait until a slot is available */
  waitForSlot(): Promise<void>

  /** 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