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:
1 parent
e90213d1ff
commit
dfa0580859
23 files changed
+5324
-347
No files matched your search
+145
-46
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user