import type { Page } from 'playwright'; import type { Logger } from '../utils/logger.js'; import type { MessagesRepository, SuccessfulMessagesRepository, SentMessageRecord } from '../database/repositories.js'; import type { PostsRepository } from '../database/repositories.js'; export interface ResponseCheckResult { messageId: number; recipient: string; gotResponse: boolean; responseSnippet?: string; } export class ResponseCheckerService { constructor( private page: Page, private messagesRepo: MessagesRepository, private successfulMessagesRepo: SuccessfulMessagesRepository, private postsRepo: PostsRepository, private logger: Logger ) { } async checkForResponses(): Promise { const results: ResponseCheckResult[] = []; // Get messages that haven't been checked recently (at least 1 hour old, recheck every 6 hours) const minAgeMs = 60 * 60 * 1000; // 1 hour const recheckAfterMs = 6 * 60 * 60 * 1000; // 6 hours const uncheckedMessages = this.messagesRepo.getUncheckedMessages(minAgeMs, recheckAfterMs, 5); if (uncheckedMessages.length === 0) { this.logger.debug('No messages to check for responses'); return results; } this.logger.info(`Checking ${uncheckedMessages.length} messages for responses...`); // Navigate to inbox try { await this.page.goto('https://www.reddit.com/message/inbox/', { waitUntil: 'domcontentloaded', timeout: 30000, }); await this.page.waitForTimeout(2000); // Get all message threads/conversations const conversations = await this.getInboxConversations(); this.logger.debug(`Found ${conversations.size} conversations in inbox`); for (const message of uncheckedMessages) { const result = await this.checkMessageResponse(message, conversations); results.push(result); if (result.gotResponse) { // Mark as got response this.messagesRepo.markGotResponse(message.id); // Get the original post info const post = message.postId ? this.postsRepo.getById(message.postId) : undefined; // Store as successful message for learning this.successfulMessagesRepo.insert({ sentMessageId: message.id, originalMessage: message.message, imageAnalysis: post?.imageAnalysis || null, postTitle: post?.title || null, responseSnippet: result.responseSnippet || null, subreddit: post?.subreddit || null, }); this.logger.info(`Got response from u/${message.recipient}! Stored for learning.`); } else { // Just mark as checked this.messagesRepo.markChecked(message.id); } } } catch (error) { this.logger.error('Failed to check inbox for responses:', error); } return results; } private async getInboxConversations(): Promise> { const conversations = new Map(); try { // Try to get conversation items from inbox const messageItems = await this.page.$$('.message, [data-testid="message"], .thing.message'); for (const item of messageItems) { try { // Get the author/recipient of the conversation const authorEl = await item.$('.author, [data-testid="message-author"], .sender a'); const author = authorEl ? await authorEl.textContent() : null; if (!author) continue; const cleanAuthor = author.replace(/^u\//, '').replace(/^\/u\//, '').trim(); // Check if this is a reply (not from us) const isReply = await item.evaluate((el) => { // Check various indicators that this is a received message, not sent return !el.classList.contains('sent') && !el.querySelector('.sent'); }); // Get message snippet const bodyEl = await item.$('.md, .message-body, [data-testid="message-body"]'); const snippet = bodyEl ? (await bodyEl.textContent())?.slice(0, 200) || '' : ''; this.logger.debug(`Checking inbox item from: ${cleanAuthor}, isReply: ${isReply}`); if (isReply && cleanAuthor) { conversations.set(cleanAuthor.toLowerCase(), { hasReply: true, snippet: snippet.trim(), }); this.logger.info(`Found reply from ${cleanAuthor}`); } } catch { // Skip problematic items } } } catch (error) { this.logger.warn('Failed to parse inbox conversations:', error); } return conversations; } private async checkMessageResponse( message: SentMessageRecord, conversations: Map ): Promise { const recipientLower = message.recipient.toLowerCase(); const conversation = conversations.get(recipientLower); this.logger.debug(`Checking response for ${message.recipient} (key: ${recipientLower}) - Found: ${!!conversation}`); if (conversation?.hasReply) { return { messageId: message.id, recipient: message.recipient, gotResponse: true, responseSnippet: conversation.snippet, }; } return { messageId: message.id, recipient: message.recipient, gotResponse: false, }; } getSuccessfulExamples(limit: number = 3, subreddit?: string): string[] { const successful = subreddit ? this.successfulMessagesRepo.getWeightedExamples(subreddit, limit) : this.successfulMessagesRepo.getRecent(limit); return successful.map((s) => { let example = `Message: "${s.originalMessage}"`; if (s.imageAnalysis) { example = `Context (Image Analysis): "${s.imageAnalysis.slice(0, 300)}..."\n${example}`; } if (s.postTitle) { example = `Post Title: "${s.postTitle}"\n${example}`; } if (s.responseSnippet) { example += `\nTheir reply: "${s.responseSnippet.slice(0, 150)}"`; } return example; }); } getSuccessCount(): number { return this.successfulMessagesRepo.count(); } }