Update source code and remove runtime data from tracking

Add gitignore rules for browser session data, SQLite databases,
media downloads, and build artifacts. Remove previously tracked
runtime files from the repository.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
jjbruntonandClaude Opus 4.6 committed 2026-05-20 18:54:07 +01:00
1 parent e90213d1ff
commit dfa0580859
23 files changed
+5324 -347

No files matched your search

+145 -46
View File
@@ -8,7 +8,12 @@ import { OpenRouterClient } from './ai/openrouter.js';
import { MonitorService } from './services/monitor.js';
import { ProcessorService } from './services/processor.js';
import { ResponseCheckerService } from './services/response-checker.js';
import { StrategySelector } from './services/strategy-selector.js';
import { ChatService } from './services/chat.js';
import { MediaService } from './services/media.js';
import { MediaSyncService } from './services/media-sync.js';
import { AutoReplyService } from './services/auto-reply.js';
import { PlaywrightMediaDownloader } from './browser/media-downloader.js';
import { RateLimiter } from './utils/rate-limiter.js';
import { createLogger } from './utils/logger.js';
import { withRetry } from './utils/retry.js';
@@ -82,19 +87,66 @@ async function main(): Promise<void> {
// Initialize services
const redditApi = new RedditApiClient(config, logger);
redditApi.setBrowserContext(browserClient.getContext()!);
// Create separate pages for concurrent operations
// 1. chatPage: For background response checking
const chatPage = await browserClient.getContext()!.newPage();
// 2. webUIPage: For user-driven chat browsing in the web dashboard
const webUIPage = await browserClient.getContext()!.newPage();
const aiClient = new OpenRouterClient(config, logger);
const monitorService = new MonitorService(redditApi, postsRepo, messagesRepo, config, logger);
const processorService = new ProcessorService(redditApi, aiClient, config, logger);
const strategySelector = new StrategySelector(config, messagesRepo, logger);
processorService.setStrategySelector(strategySelector);
const responseChecker = new ResponseCheckerService(
page,
chatPage,
messagesRepo,
successfulMessagesRepo,
postsRepo,
logger
);
const chatService = new ChatService(page, logger, successfulMessagesRepo);
const mediaDownloader = new PlaywrightMediaDownloader(webUIPage, logger);
const mediaService = new MediaService(mediaDownloader, logger);
const chatService = new ChatService(webUIPage, logger, successfulMessagesRepo, mediaService);
const mediaSyncService = new MediaSyncService(chatService, logger, config.media.syncIntervalMs);
const rateLimiter = new RateLimiter(config, logger);
await chatService.loadCache();
// Initialize auto-reply service
const autoReplyService = new AutoReplyService(
chatService,
aiClient,
config,
logger,
// onReplyGenerated
(chat) => {
webServer?.logActivity('info', `Auto-reply pending approval for u/${chat.username}`, chat.username);
webServer?.pushLog('info', `Reply pending approval for u/${chat.username}: "${chat.pendingReply?.slice(0, 80)}..."`);
},
// onUpsellDetected
(chat, keyword) => {
webServer?.logActivity('skipped', `Upsell detected from u/${chat.username} (keyword: "${keyword}")`, chat.username);
webServer?.pushLog('warn', `Upsell detected from u/${chat.username} — keyword: "${keyword}"`);
},
// onAutoSent
(chat) => {
webServer?.logActivity('sent', `Auto-reply sent to u/${chat.username}`, chat.username);
webServer?.pushLog('info', `Auto-reply sent to u/${chat.username}`);
},
);
// Start background media sync
if (config.media.syncEnabled) {
mediaSyncService.start();
}
// Start auto-reply service
if (config.autoReply.enabled) {
autoReplyService.start();
}
// Initialize web dashboard
let webServer: WebServer | null = null;
if (config.web.enabled) {
@@ -107,7 +159,14 @@ async function main(): Promise<void> {
logger
);
webServer.setChatService(chatService);
webServer.setMediaService(mediaService);
webServer.setMediaSyncService(mediaSyncService);
webServer.setResponseChecker(responseChecker);
webServer.setRateLimiter(rateLimiter);
webServer.setAiClient(aiClient);
webServer.setStrategySelector(strategySelector);
webServer.setAutoReplyService(autoReplyService);
webServer.setOnConfigChange(() => logger.info('Runtime config updated via dashboard'));
webServer.start();
}
@@ -121,11 +180,19 @@ async function main(): Promise<void> {
// Graceful shutdown handler
let isShuttingDown = false;
let responseCheckInterval: NodeJS.Timeout | null = null;
const shutdown = async (): Promise<void> => {
if (isShuttingDown) return;
isShuttingDown = true;
if (responseCheckInterval) {
clearInterval(responseCheckInterval);
}
mediaSyncService.stop();
autoReplyService.stop();
logger.info('Shutting down gracefully...');
webServer?.stop();
await browserClient.close();
@@ -145,28 +212,37 @@ async function main(): Promise<void> {
logger.info(`Max messages per hour: ${config.rateLimit.maxMessagesPerHour}`);
logger.info('');
// Start background response checking
const RESPONSE_CHECK_INTERVAL_MS = Math.max(config.reddit.pollIntervalMs * 5, 60000);
const checkResponses = async () => {
if (isShuttingDown) return;
try {
logger.debug('Checking for responses to sent messages (Background Thread)...');
const responses = await responseChecker.checkForResponses();
if (responses.some((r) => r.gotResponse)) {
// Reload successful examples
const updatedExamples = responseChecker.getSuccessfulExamples(3);
processorService.setSuccessfulExamples(updatedExamples);
}
} catch (error) {
logger.error('Error in background response checker:', error);
}
};
// Run immediately then schedule
checkResponses();
responseCheckInterval = setInterval(checkResponses, RESPONSE_CHECK_INTERVAL_MS);
// Track poll states
let pollCycle = 0;
let lastResponseCheck = 0;
const RESPONSE_CHECK_INTERVAL_MS = Math.max(config.reddit.pollIntervalMs * 5, 60000);
let lastPausedLog = 0;
// Main loop
while (!isShuttingDown) {
try {
// Periodically check for responses to learn from (run this even if paused)
if (Date.now() - lastResponseCheck > RESPONSE_CHECK_INTERVAL_MS) {
logger.debug('Checking for responses to sent messages...');
const responses = await responseChecker.checkForResponses();
lastResponseCheck = Date.now();
if (responses.some((r) => r.gotResponse)) {
// Reload successful examples
const updatedExamples = responseChecker.getSuccessfulExamples(3);
processorService.setSuccessfulExamples(updatedExamples);
}
}
// Check if bot is running (controlled via web UI)
if (webServer && !webServer.isRunning()) {
if (Date.now() - lastPausedLog > 60000) { // Log every minute
@@ -184,6 +260,7 @@ async function main(): Promise<void> {
if (posts.length > 0) {
logger.info(`Found ${posts.length} unprocessed posts`);
webServer?.pushLog('info', `Found ${posts.length} unprocessed posts`);
} else {
logger.info('No new posts found. Monitoring...');
}
@@ -193,15 +270,32 @@ async function main(): Promise<void> {
try {
// Mark as processed immediately to avoid duplicates on restart
const postImageUrl = redditApi.getImageUrl(post);
postsRepo.insert({
id: post.id,
subreddit: post.subreddit,
author: post.author,
title: post.title,
imageUrl: redditApi.getImageUrl(post),
imageUrl: postImageUrl,
processedAt: Date.now(),
});
// Check if user is a seller/OnlyFans profile (before spending on AI)
if (config.reddit.filterSellers && config.reddit.sellerKeywords.length > 0) {
const isSeller = await redditApi.isSellerProfile(post.author, config.reddit.sellerKeywords);
if (isSeller) {
logger.info(`Skipping u/${post.author} - seller/OnlyFans profile detected`);
webServer?.logActivity('skipped', `Skipped u/${post.author} (seller profile)`, post.author);
webServer?.pushLog('info', `🚫 Skipped u/${post.author} - seller/OnlyFans profile`);
postsRepo.setError(post.id, 'Seller profile detected');
continue;
}
}
// Refresh examples weighted toward this post's subreddit
const subredditExamples = responseChecker.getSuccessfulExamples(3, post.subreddit);
processorService.setSuccessfulExamples(subredditExamples);
// Process post (download image, analyze, generate message)
const processed = await withRetry(
() => processorService.processPost(post),
@@ -217,12 +311,19 @@ async function main(): Promise<void> {
// Update database with analysis
postsRepo.updateAnalysis(post.id, processed.imageAnalysis, processed.generatedMessage);
// Log image analysis and strategy to realtime log
webServer?.pushLog('info', `🖼️ Image analysis for u/${post.author}: ${processed.imageAnalysis}`);
if (processed.strategyId) {
webServer?.pushLog('info', `🎯 Strategy: ${processed.strategyId}`);
}
// Create subject line
const subject = "Stunning";
// Send message with rate limiting
await rateLimiter.throttle(async () => {
logger.info(`Sending message to u/${post.author}...`);
webServer?.pushLog('info', `Sending message to u/${post.author}...`);
const success = await messagePage.sendMessage(
post.author,
@@ -231,37 +332,35 @@ async function main(): Promise<void> {
);
if (success) {
// Verify the message actually appears in chat
const verified = await chatService.verifyMessageSent(
post.author,
processed.generatedMessage
// Message was sent successfully via /message/compose
// Note: We skip chat.reddit.com verification since /message/compose sends to inbox, not chat
// The sendMessage function already confirms success via form submission indicators
postsRepo.markMessageSent(post.id);
// Refresh top chats in background so dashboard is up-to-date
chatService.refreshTopChats(5).catch(err => logger.error('Failed to trigger chat refresh:', err));
messagesRepo.insert({
postId: post.id,
recipient: post.author,
subject,
message: processed.generatedMessage,
sentAt: Date.now(),
strategyId: processed.strategyId,
});
const stats = rateLimiter.getStats();
const currentCount = stats.messagesThisHour + 1;
logger.info(
`Message sent successfully! (${currentCount}/${config.rateLimit.maxMessagesPerHour} this hour)`
);
if (verified) {
postsRepo.markMessageSent(post.id);
messagesRepo.insert({
postId: post.id,
recipient: post.author,
subject,
message: processed.generatedMessage,
sentAt: Date.now(),
});
const stats = rateLimiter.getStats();
const currentCount = stats.messagesThisHour + 1;
logger.info(
`Message sent and verified! (${currentCount}/${config.rateLimit.maxMessagesPerHour} this hour)`
);
webServer?.logActivity('sent', `Sent message to u/${post.author}`, post.author);
webServer?.updateStats(currentCount);
} else {
logger.warn(`Message to u/${post.author} likely sent but not found in chat verification.`);
postsRepo.setError(post.id, 'Message sent but verification failed');
webServer?.logActivity('error', `Verification failed for u/${post.author}`, post.author);
}
webServer?.logActivity('sent', `Sent message to u/${post.author}`, post.author, postImageUrl ?? undefined);
webServer?.pushLog('info', `✅ Message sent to u/${post.author} (${currentCount}/${config.rateLimit.maxMessagesPerHour} this hour)`);
webServer?.updateStats(currentCount);
} else {
postsRepo.setError(post.id, 'Failed to send message');
webServer?.logActivity('error', `Failed to send to u/${post.author}`, post.author);
webServer?.logActivity('error', `Failed to send to u/${post.author}`, post.author, postImageUrl ?? undefined);
webServer?.pushLog('error', `❌ Failed to send message to u/${post.author}`);
}
return success;