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.
855 lines
21 KiB
Markdown
855 lines
21 KiB
Markdown
# 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<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
|
|
|
|
```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<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
|
|
|
|
```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<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
|
|
|
|
```typescript
|
|
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()`:
|
|
|
|
```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<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:
|
|
```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<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
|
|
|
|
```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<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
|
|
|
|
```typescript
|
|
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
|
|
|
|
```typescript
|
|
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:
|
|
|
|
```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<void>
|
|
|
|
/** Verify browser session is active */
|
|
verifySession(): Promise<boolean>
|
|
|
|
/** Perform automated login */
|
|
login(): Promise<void>
|
|
|
|
/** Get page context for operations */
|
|
getPage(): Promise<Page>
|
|
|
|
/** Close browser */
|
|
close(): Promise<void>
|
|
}
|
|
```
|
|
|
|
### 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<boolean>
|
|
|
|
/** Check if recipient field shows an error */
|
|
hasRecipientError(page: Page): Promise<boolean>
|
|
}
|
|
```
|
|
|
|
### 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<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
|