| """ |
| Bot Engine - Background Processing Loop |
| Handles automatic DM and Comment reply cycles |
| """ |
|
|
| import time |
| import random |
| import logging |
| import threading |
| from datetime import datetime, timedelta |
| from typing import Optional, Dict, List, Callable |
|
|
| from config import BOT_CONFIG, DEFAULT_SYSTEM_PROMPT |
| from instagram_handler import InstagramHandler |
| from ai_engine import AIEngine |
|
|
| logger = logging.getLogger("BotEngine") |
|
|
|
|
| class BotEngine: |
| """ |
| Background bot that: |
| - Checks for new DMs every 2-3 minutes |
| - Checks for new comments on posts |
| - Generates AI replies using Llama 3.1 |
| - Sends replies with anti-ban delays |
| - Tracks replied messages to avoid duplicates |
| """ |
|
|
| def __init__( |
| self, |
| ig_handler: InstagramHandler, |
| ai_engine: AIEngine, |
| log_callback: Optional[Callable] = None, |
| ): |
| self.ig = ig_handler |
| self.ai = ai_engine |
| self.log_callback = log_callback |
|
|
| |
| self.is_running = False |
| self.thread: Optional[threading.Thread] = None |
| self.stop_event = threading.Event() |
|
|
| |
| self.auto_dm_enabled = False |
| self.auto_comment_enabled = False |
| self.system_prompt = "" |
|
|
| |
| self.replied_dm_ids = set() |
| self.replied_comment_ids = set() |
| self.dm_conversation_context: Dict[str, List[Dict]] = {} |
|
|
| |
| self.total_dms_replied = 0 |
| self.total_comments_replied = 0 |
| self.last_check_time = None |
| self.errors_count = 0 |
| self.start_time = None |
|
|
| |
| self.actions_this_hour = 0 |
| self.hour_start = datetime.now() |
|
|
| def _log(self, message: str, log_type: str = "info"): |
| """Log message and call callback if set""" |
| logger.info(f"[{log_type.upper()}] {message}") |
| if self.log_callback: |
| try: |
| self.log_callback(message, log_type) |
| except Exception: |
| pass |
|
|
| def _check_rate_limit(self) -> bool: |
| """Check if we're within rate limits""" |
| now = datetime.now() |
|
|
| |
| if (now - self.hour_start).total_seconds() > 3600: |
| self.actions_this_hour = 0 |
| self.hour_start = now |
|
|
| if self.actions_this_hour >= BOT_CONFIG["max_actions_per_hour"]: |
| self._log( |
| f"Rate limit reached ({self.actions_this_hour} actions this hour). Waiting...", |
| "warning" |
| ) |
| return False |
|
|
| return True |
|
|
| def _smart_delay(self): |
| """Wait between actions to avoid Instagram detection""" |
| delay = random.uniform( |
| BOT_CONFIG["action_delay_min"], |
| BOT_CONFIG["action_delay_max"], |
| ) |
| self._log(f"Waiting {delay:.0f}s before next action...", "info") |
|
|
| |
| self.stop_event.wait(timeout=delay) |
|
|
| def _get_context_for_thread(self, thread_id: str) -> List[Dict]: |
| """Get conversation context for a DM thread""" |
| return self.dm_conversation_context.get(thread_id, []) |
|
|
| def _save_context(self, thread_id: str, role: str, text: str): |
| """Save message to conversation context""" |
| if thread_id not in self.dm_conversation_context: |
| self.dm_conversation_context[thread_id] = [] |
|
|
| self.dm_conversation_context[thread_id].append({ |
| "role": role, |
| "text": text, |
| }) |
|
|
| |
| max_ctx = BOT_CONFIG["max_context_messages"] |
| if len(self.dm_conversation_context[thread_id]) > max_ctx: |
| self.dm_conversation_context[thread_id] = \ |
| self.dm_conversation_context[thread_id][-max_ctx:] |
|
|
| def _process_dms(self): |
| """Check and reply to new DMs""" |
| if not self.auto_dm_enabled: |
| return |
|
|
| if not self._check_rate_limit(): |
| return |
|
|
| try: |
| self._log("Checking for new DMs... 💌", "info") |
|
|
| |
| unread = self.ig.get_unread_dms(count=BOT_CONFIG["max_dm_fetch"]) |
|
|
| if not unread: |
| self._log("No new DMs found", "info") |
| return |
|
|
| self._log(f"Found {len(unread)} new DMs!", "dm") |
|
|
| for dm in unread: |
| |
| msg_id = dm.get("message_id", "") |
| if msg_id in self.replied_dm_ids: |
| continue |
|
|
| |
| if self.stop_event.is_set(): |
| return |
|
|
| |
| if not self._check_rate_limit(): |
| return |
|
|
| thread_id = dm["thread_id"] |
| sender = dm["sender_username"] |
| sender_name = dm.get("sender_full_name", sender) |
| message_text = dm["message_text"] |
|
|
| self._log( |
| f"📩 DM from @{sender}: \"{message_text[:50]}...\"", |
| "dm" |
| ) |
|
|
| |
| context = self._get_context_for_thread(thread_id) |
|
|
| |
| self._save_context(thread_id, "user", message_text) |
|
|
| |
| result = self.ai.generate_reply( |
| message=message_text, |
| system_prompt=self.system_prompt, |
| context=context, |
| message_type="dm", |
| sender_name=sender_name, |
| ) |
|
|
| reply = result.get("reply", "") |
| method = result.get("method", "unknown") |
|
|
| if not reply: |
| self._log(f"Skipped DM from @{sender} (no reply generated)", "warning") |
| self.replied_dm_ids.add(msg_id) |
| continue |
|
|
| |
| self._log(f"Sending reply to @{sender}: \"{reply[:50]}...\" [{method}]", "dm") |
|
|
| success = self.ig.send_dm(thread_id, reply) |
|
|
| if success: |
| self.replied_dm_ids.add(msg_id) |
| self._save_context(thread_id, "assistant", reply) |
| self.total_dms_replied += 1 |
| self.actions_this_hour += 1 |
| self._log( |
| f"✅ Replied to @{sender} successfully! [{method}]", |
| "success" |
| ) |
| else: |
| self.errors_count += 1 |
| self._log(f"❌ Failed to send DM to @{sender}", "error") |
|
|
| |
| if not self.stop_event.is_set(): |
| self._smart_delay() |
|
|
| except Exception as e: |
| self.errors_count += 1 |
| self._log(f"Error processing DMs: {str(e)[:100]}", "error") |
|
|
| def _process_comments(self): |
| """Check and reply to new comments on posts""" |
| if not self.auto_comment_enabled: |
| return |
|
|
| if not self._check_rate_limit(): |
| return |
|
|
| try: |
| self._log("Checking for new comments... 💬", "info") |
|
|
| |
| comments = self.ig.get_recent_comments( |
| count=BOT_CONFIG["max_comment_fetch"] |
| ) |
|
|
| if not comments: |
| self._log("No new comments found", "info") |
| return |
|
|
| self._log(f"Found {len(comments)} unreplied comments!", "comment") |
|
|
| for comment in comments: |
| comment_id = comment.get("comment_id", "") |
|
|
| |
| if comment_id in self.replied_comment_ids: |
| continue |
|
|
| |
| if self.stop_event.is_set(): |
| return |
|
|
| |
| if not self._check_rate_limit(): |
| return |
|
|
| commenter = comment["commenter_username"] |
| commenter_name = comment.get("commenter_full_name", commenter) |
| comment_text = comment["comment_text"] |
| media_id = comment["media_id"] |
|
|
| self._log( |
| f"💬 Comment from @{commenter}: \"{comment_text[:50]}...\"", |
| "comment" |
| ) |
|
|
| |
| result = self.ai.generate_reply( |
| message=comment_text, |
| system_prompt=self.system_prompt, |
| context=None, |
| message_type="comment", |
| sender_name=commenter_name, |
| ) |
|
|
| reply = result.get("reply", "") |
| method = result.get("method", "unknown") |
|
|
| if not reply: |
| self._log( |
| f"Skipped comment from @{commenter} (no reply generated)", |
| "warning" |
| ) |
| self.replied_comment_ids.add(comment_id) |
| continue |
|
|
| |
| reply_with_mention = f"@{commenter} {reply}" |
|
|
| |
| if len(reply_with_mention) > 200: |
| reply_with_mention = reply_with_mention[:197] + "..." |
|
|
| self._log( |
| f"Replying to @{commenter}: \"{reply[:50]}...\" [{method}]", |
| "comment" |
| ) |
|
|
| |
| success = self.ig.reply_to_comment(media_id, comment_id, reply_with_mention) |
|
|
| if success: |
| self.replied_comment_ids.add(comment_id) |
| self.total_comments_replied += 1 |
| self.actions_this_hour += 1 |
| self._log( |
| f"✅ Replied to @{commenter}'s comment! [{method}]", |
| "success" |
| ) |
| else: |
| self.errors_count += 1 |
| self._log( |
| f"❌ Failed to reply to @{commenter}'s comment", |
| "error" |
| ) |
|
|
| |
| if not self.stop_event.is_set(): |
| self._smart_delay() |
|
|
| except Exception as e: |
| self.errors_count += 1 |
| self._log(f"Error processing comments: {str(e)[:100]}", "error") |
|
|
| def _bot_loop(self): |
| """Main bot loop - runs in background thread""" |
| self._log("🚀 Bot loop started!", "bot") |
|
|
| while not self.stop_event.is_set(): |
| try: |
| |
| if not self.ig.is_logged_in: |
| self._log("❌ Instagram session expired! Stopping bot.", "error") |
| self.is_running = False |
| break |
|
|
| |
| if self.auto_dm_enabled and not self.stop_event.is_set(): |
| self._process_dms() |
|
|
| |
| if not self.stop_event.is_set(): |
| self.stop_event.wait(timeout=random.uniform(10, 20)) |
|
|
| |
| if self.auto_comment_enabled and not self.stop_event.is_set(): |
| self._process_comments() |
|
|
| |
| self.last_check_time = datetime.now() |
|
|
| |
| if not self.stop_event.is_set(): |
| wait_time = random.uniform( |
| BOT_CONFIG["check_interval_min"], |
| BOT_CONFIG["check_interval_max"], |
| ) |
| self._log( |
| f"💤 Next check in {wait_time:.0f} seconds...", |
| "info" |
| ) |
| self.stop_event.wait(timeout=wait_time) |
|
|
| except Exception as e: |
| self.errors_count += 1 |
| self._log(f"❌ Bot loop error: {str(e)[:150]}", "error") |
|
|
| |
| if not self.stop_event.is_set(): |
| self.stop_event.wait(timeout=60) |
|
|
| self._log("⏹️ Bot loop stopped.", "bot") |
|
|
| def start( |
| self, |
| auto_dm: bool = True, |
| auto_comment: bool = True, |
| system_prompt: str = "", |
| ) -> Dict: |
| """Start the bot in a background thread""" |
| if self.is_running: |
| return {"success": False, "message": "Bot is already running!"} |
|
|
| if not self.ig.is_logged_in: |
| return {"success": False, "message": "Not logged in to Instagram!"} |
|
|
| if not auto_dm and not auto_comment: |
| return { |
| "success": False, |
| "message": "Enable at least one feature (DM or Comment reply)!", |
| } |
|
|
| |
| self.auto_dm_enabled = auto_dm |
| self.auto_comment_enabled = auto_comment |
| self.system_prompt = system_prompt.strip() if system_prompt.strip() else "" |
|
|
| |
| self.stop_event.clear() |
|
|
| |
| self.thread = threading.Thread(target=self._bot_loop, daemon=True) |
| self.thread.start() |
|
|
| self.is_running = True |
| self.start_time = datetime.now() |
| self.errors_count = 0 |
|
|
| features = [] |
| if auto_dm: |
| features.append("DM Reply") |
| if auto_comment: |
| features.append("Comment Reply") |
|
|
| prompt_info = "Custom prompt" if self.system_prompt else "Default prompt" |
|
|
| self._log( |
| f"Bot started! Features: {', '.join(features)} | {prompt_info}", |
| "bot" |
| ) |
|
|
| return { |
| "success": True, |
| "message": f"Bot started! 🚀 ({', '.join(features)})", |
| } |
|
|
| def stop(self) -> Dict: |
| """Stop the bot""" |
| if not self.is_running: |
| return {"success": False, "message": "Bot is not running!"} |
|
|
| self._log("Stopping bot...", "bot") |
|
|
| |
| self.stop_event.set() |
|
|
| |
| if self.thread and self.thread.is_alive(): |
| self.thread.join(timeout=10) |
|
|
| self.is_running = False |
| self.start_time = None |
|
|
| self._log("Bot stopped successfully! ⏹️", "bot") |
|
|
| return { |
| "success": True, |
| "message": "Bot stopped! ⏹️", |
| } |
|
|
| def get_stats(self) -> Dict: |
| """Get bot statistics""" |
| uptime = None |
| if self.start_time: |
| uptime = datetime.now() - self.start_time |
|
|
| return { |
| "is_running": self.is_running, |
| "auto_dm_enabled": self.auto_dm_enabled, |
| "auto_comment_enabled": self.auto_comment_enabled, |
| "total_dms_replied": self.total_dms_replied, |
| "total_comments_replied": self.total_comments_replied, |
| "errors_count": self.errors_count, |
| "last_check_time": str(self.last_check_time) if self.last_check_time else None, |
| "uptime_seconds": int(uptime.total_seconds()) if uptime else 0, |
| "actions_this_hour": self.actions_this_hour, |
| "replied_dm_count": len(self.replied_dm_ids), |
| "replied_comment_count": len(self.replied_comment_ids), |
| } |
|
|
| def update_settings( |
| self, |
| auto_dm: Optional[bool] = None, |
| auto_comment: Optional[bool] = None, |
| system_prompt: Optional[str] = None, |
| ): |
| """Update bot settings while running""" |
| if auto_dm is not None: |
| self.auto_dm_enabled = auto_dm |
| if auto_comment is not None: |
| self.auto_comment_enabled = auto_comment |
| if system_prompt is not None: |
| self.system_prompt = system_prompt.strip() if system_prompt.strip() else "" |
|
|
| self._log("Settings updated! ⚙️", "info") |