From 2133c593b0fa7857be6843a2588c73c1dea5f804 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 17 Oct 2025 01:43:40 +0000 Subject: [PATCH 1/2] Refactor: Inject activity reporter at runtime This change defers reporter initialization until runtime, preventing multiple MongoDB connections and ensuring proper handling in different environments. Co-authored-by: amirbiron --- activity_reporter.py | 32 ++++++++++++++- bot_handlers.py | 23 +++++------ conversation_handlers.py | 55 ++++++++++++++++++++------ main.py | 85 ++++++++++++++++++++++++++++++++-------- refactor_handlers.py | 23 +++++++---- 5 files changed, 167 insertions(+), 51 deletions(-) diff --git a/activity_reporter.py b/activity_reporter.py index 23f28c78c..280da2d40 100644 --- a/activity_reporter.py +++ b/activity_reporter.py @@ -8,6 +8,35 @@ MongoClient = None # type: ignore _HAS_PYMONGO = False from datetime import datetime, timezone +import atexit + +# חיבור MongoDB גלובלי יחיד לכל האפליקציה +_client = None # type: ignore + +def get_mongo_client(mongodb_uri: str): + """החזרת מופע MongoClient יחיד (singleton) לכל האפליקציה. + + בכל קריאה מחזיר את אותו אובייקט, ויוצר רק בפעם הראשונה. + """ + global _client + if not _HAS_PYMONGO: + raise RuntimeError("pymongo not available") + if _client is None: + # שימוש ב-timezone מודע כדי לשמור אחידות זמנים במסד + _client = MongoClient(mongodb_uri, tz_aware=True, tzinfo=timezone.utc) + return _client + +def close_mongo_client() -> None: + """סגירת החיבור הגלובלי בבטחה בזמן כיבוי השירות.""" + global _client + try: + if _client is not None: + _client.close() + finally: + _client = None + +# סגירה אוטומטית ביציאה מהתהליך +atexit.register(close_mongo_client) try: from metrics import note_active_user # type: ignore @@ -25,7 +54,8 @@ def __init__(self, mongodb_uri, service_id, service_name=None): try: if not _HAS_PYMONGO: raise RuntimeError("pymongo not available") - self.client = MongoClient(mongodb_uri, tz_aware=True, tzinfo=timezone.utc) + # שימוש ב-singleton של MongoClient כדי למנוע חיבורים מרובים + self.client = get_mongo_client(mongodb_uri) self.db = self.client["render_bot_monitor"] self.service_id = service_id self.service_name = service_name or service_id diff --git a/bot_handlers.py b/bot_handlers.py index 4991e575d..b0bfb1ee7 100644 --- a/bot_handlers.py +++ b/bot_handlers.py @@ -24,7 +24,16 @@ from config import config from database import CodeSnippet, db from conversation_handlers import MAIN_KEYBOARD -from activity_reporter import create_reporter +# Reporter מוזרק בזמן ריצה כדי למנוע יצירה בזמן import +class _NoopReporter: + def report_activity(self, user_id): + return None + +reporter = _NoopReporter() + +def set_activity_reporter(new_reporter): + global reporter + reporter = new_reporter or _NoopReporter() import json try: import aiohttp # for GitHub rate limit check @@ -34,18 +43,6 @@ logger = logging.getLogger(__name__) import os as _os -_DISABLE_REPORTER = bool(int((_os.getenv("DISABLE_ACTIVITY_REPORTER", "0") or "0").strip() or 0)) -if _DISABLE_REPORTER: - class _NoopReporter: - def report_activity(self, user_id): - return None - reporter = _NoopReporter() -else: - reporter = create_reporter( - mongodb_uri="mongodb+srv://mumin:M43M2TFgLfGvhBwY@muminai.tm6x81b.mongodb.net/?retryWrites=true&w=majority&appName=muminAI", - service_id="srv-d29d72adbo4c73bcuep0", - service_name="CodeBot" - ) class AdvancedBotHandlers: """פקודות מתקדמות של הבוט""" diff --git a/conversation_handlers.py b/conversation_handlers.py index 5795b8563..07e8a8672 100644 --- a/conversation_handlers.py +++ b/conversation_handlers.py @@ -17,7 +17,16 @@ ) from database import DatabaseManager from file_manager import backup_manager -from activity_reporter import create_reporter +# Reporter מוזרק בזמן ריצה כדי להימנע מפתיחת חיבור בעת import +class _NoopReporter: + def report_activity(self, user_id): + return None + +reporter = _NoopReporter() + +def set_activity_reporter(new_reporter): + global reporter + reporter = new_reporter or _NoopReporter() from utils import get_language_emoji as get_file_emoji from user_stats import user_stats from typing import List, Optional, Dict, cast @@ -103,11 +112,7 @@ def _format_bytes(num: int) -> str: ["☁️ Google Drive", "ℹ️ הסבר על הבוט"] ] -reporter = create_reporter( - mongodb_uri="mongodb+srv://mumin:M43M2TFgLfGvhBwY@muminai.tm6x81b.mongodb.net/?retryWrites=true&w=majority&appName=muminAI", - service_id="srv-d29d72adbo4c73bcuep0", - service_name="CodeBot" -) +# ה-reporters יוגדרו ב-main בזמן ריצה async def start_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: """Handle /start and show the main menu.""" @@ -153,7 +158,10 @@ async def start_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> i reply_markup=reply_markup, parse_mode=ParseMode.HTML, ) - reporter.report_activity(user_id) + try: + reporter.report_activity(user_id) + except Exception: + pass return ConversationHandler.END except Exception: # אם משהו נכשל ביצירת קישור — נמשיך לזרימת ברירת המחדל @@ -163,7 +171,10 @@ async def start_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> i welcome_text = MESSAGES["welcome"].format(name=safe_user_name) keyboard = ReplyKeyboardMarkup(MAIN_KEYBOARD, resize_keyboard=True) await update.message.reply_text(welcome_text, reply_markup=keyboard) +try: reporter.report_activity(user_id) +except Exception: + pass return ConversationHandler.END HELP_PAGES = [ @@ -414,7 +425,10 @@ async def start_repo_zip_import(update: Update, context: ContextTypes.DEFAULT_TY "🔖 אצמיד תגית repo:owner/name (אם קיימת ב-metadata). לא מתבצעת מחיקה.", reply_markup=cancel_markup ) - reporter.report_activity(update.effective_user.id) + try: + reporter.report_activity(update.effective_user.id) + except Exception: + pass return ConversationHandler.END async def start_zip_create_flow(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: @@ -433,7 +447,10 @@ async def start_zip_create_flow(update: Update, context: ContextTypes.DEFAULT_TY "כשתסיים/י, לחצ/י 'סיום' וניצור עבורך ZIP מוכן.", reply_markup=InlineKeyboardMarkup(keyboard) ) - reporter.report_activity(update.effective_user.id) + try: + reporter.report_activity(update.effective_user.id) + except Exception: + pass return ConversationHandler.END async def show_by_repo_menu(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: @@ -527,7 +544,10 @@ async def show_all_files(update: Update, context: ContextTypes.DEFAULT_TYPE) -> reply_markup=ReplyKeyboardMarkup(MAIN_KEYBOARD, resize_keyboard=True) ) - reporter.report_activity(user_id) + try: + reporter.report_activity(user_id) + except Exception: + pass return ConversationHandler.END async def show_large_files_direct(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: @@ -553,7 +573,10 @@ async def show_github_menu(update: Update, context: ContextTypes.DEFAULT_TYPE) - github_handler = context.bot_data['github_handler'] await github_handler.github_menu_command(update, context) - reporter.report_activity(update.effective_user.id) + try: + reporter.report_activity(update.effective_user.id) + except Exception: + pass return ConversationHandler.END @@ -583,7 +606,10 @@ async def show_all_files_callback(update: Update, context: ContextTypes.DEFAULT_ "בחר/י דרך להצגת הקבצים:", reply_markup=reply_markup ) - reporter.report_activity(update.effective_user.id) + try: + reporter.report_activity(update.effective_user.id) + except Exception: + pass except Exception as e: # אל תרשום ERROR אם זו רק הודעה שלא השתנתה msg = str(e) @@ -672,7 +698,10 @@ async def show_regular_files_callback(update: Update, context: ContextTypes.DEFA if "message is not modified" not in str(br).lower(): raise - reporter.report_activity(user_id) + try: + reporter.report_activity(user_id) + except Exception: + pass except Exception as e: logger.error(f"Error in show_regular_files_callback: {e}") diff --git a/main.py b/main.py index edb9cdb23..3cc473d16 100644 --- a/main.py +++ b/main.py @@ -62,8 +62,10 @@ class _DummyErrors: from database import CodeSnippet, DatabaseManager, db from services import code_service as code_processor from bot_handlers import AdvancedBotHandlers # still used by legacy code +from bot_handlers import set_activity_reporter as set_bh_activity_reporter from conversation_handlers import MAIN_KEYBOARD, get_save_conversation_handler -from activity_reporter import create_reporter +from conversation_handlers import set_activity_reporter as set_ch_activity_reporter +from activity_reporter import create_reporter, SimpleActivityReporter from github_menu_handler import GitHubMenuHandler from backup_menu_handler import BackupMenuHandler from handlers.drive.menu import GoogleDriveMenuHandler @@ -148,12 +150,8 @@ def _register_catch_all_callback(application, callback_fn) -> None: logging.getLogger("telegram.ext.Updater").setLevel(logging.ERROR) logging.getLogger("telegram.ext.Application").setLevel(logging.WARNING) -# יצירת אובייקט reporter גלובלי -reporter = create_reporter( - mongodb_uri=(os.getenv('REPORTER_MONGODB_URL') or os.getenv('REPORTER_MONGODB_URI') or config.MONGODB_URL), - service_id=os.getenv('REPORTER_SERVICE_ID', 'srv-d29d72adbo4c73bcuep0'), - service_name="CodeBot" -) +# Reporter יווצר ויוזרק בזמן ריצה לאחר בניית האפליקציה והקונפיג +reporter = None # type: ignore # ===== עזר: שליחת הודעת אדמין ===== def get_admin_ids() -> list[int]: @@ -650,6 +648,49 @@ async def run_polling(self, *a, **k): except Exception: pass + # יצירת והזרקת Activity Reporter בזמן ריצה (מונע חיבורים מרובים בזמן import) + try: + mongodb_uri = ( + os.getenv('REPORTER_MONGODB_URL') + or os.getenv('REPORTER_MONGODB_URI') + or getattr(config, 'MONGODB_URL', None) + ) + service_id = os.getenv('REPORTER_SERVICE_ID', getattr(config, 'BOT_LABEL', 'CodeBot')) + # תמיכה בנטרול דיווח פעילות דרך ENV + disable_reporter = bool(int((os.getenv('DISABLE_ACTIVITY_REPORTER', '0') or '0').strip() or 0)) + if disable_reporter: + class _NoopReporter: + def report_activity(self, user_id): + return None + created_reporter = _NoopReporter() + else: + # יצירה בטוחה: SimpleActivityReporter מטפל בחוסר pymongo בסביבה + created_reporter = create_reporter( + mongodb_uri=mongodb_uri, + service_id=service_id, + service_name="CodeBot", + ) + # עדכון גלובלי במודול זה + global reporter + reporter = created_reporter + # הזרקה למודולים שתלויים ב-report_activity + try: + set_bh_activity_reporter(created_reporter) + except Exception: + pass + try: + set_ch_activity_reporter(created_reporter) + except Exception: + pass + try: + from refactor_handlers import set_activity_reporter as set_rh_activity_reporter + set_rh_activity_reporter(created_reporter) + except Exception: + pass + except Exception: + # בסביבות CI/טסטים, אל נכשיל את הבנייה + reporter = None # type: ignore + self.setup_handlers() self.advanced_handlers = AdvancedBotHandlers(self.application) # רישום קטגוריית "⭐ מועדפים" לתפריט "📚 הקבצים" @@ -1173,7 +1214,8 @@ async def show_docs(update: Update, context: ContextTypes.DEFAULT_TYPE): async def help_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE): """פקודת עזרה מפורטת""" - reporter.report_activity(update.effective_user.id) + if reporter is not None: + reporter.report_activity(update.effective_user.id) await log_user_activity(update, context) response = """ 📚 רשימת הפקודות המלאה: @@ -1220,7 +1262,8 @@ async def help_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) async def save_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE): """פקודת שמירת קוד""" - reporter.report_activity(update.effective_user.id) + if reporter is not None: + reporter.report_activity(update.effective_user.id) await log_user_activity(update, context) user_id = update.effective_user.id @@ -1268,7 +1311,8 @@ async def save_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) async def list_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE): """הצגת רשימת הקטעים של המשתמש""" - reporter.report_activity(update.effective_user.id) + if reporter is not None: + reporter.report_activity(update.effective_user.id) user_id = update.effective_user.id files = db.get_user_files(user_id, limit=20) @@ -1306,7 +1350,8 @@ async def list_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) async def search_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE): """חיפוש קטעי קוד""" - reporter.report_activity(update.effective_user.id) + if reporter is not None: + reporter.report_activity(update.effective_user.id) await log_user_activity(update, context) user_id = update.effective_user.id @@ -1399,7 +1444,8 @@ async def check_commands(self, update: Update, context: ContextTypes.DEFAULT_TYP async def stats_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE): """הצגת סטטיסטיקות המשתמש או מנהל""" - reporter.report_activity(update.effective_user.id) + if reporter is not None: + reporter.report_activity(update.effective_user.id) await log_user_activity(update, context) # הוספת רישום משתמש לסטטיסטיקות user_id = update.effective_user.id @@ -2198,7 +2244,8 @@ def _parse_repo_full_from_label(label: str) -> str: else: await update.message.reply_text("❌ שגיאה בשמירת הקובץ") - reporter.report_activity(user_id) + if reporter is not None: + reporter.report_activity(user_id) except Exception as e: logger.error(f"שגיאה בטיפול בקובץ: {e}") @@ -2212,7 +2259,8 @@ def _parse_repo_full_from_label(label: str) -> str: async def handle_text_message(self, update: Update, context: ContextTypes.DEFAULT_TYPE): """טיפול בהודעות טקסט (קוד פוטנציאלי)""" - reporter.report_activity(update.effective_user.id) + if reporter is not None: + reporter.report_activity(update.effective_user.id) await log_user_activity(update, context) user_id = update.effective_user.id text = update.message.text @@ -2349,7 +2397,8 @@ async def handle_text_message(self, update: Update, context: ContextTypes.DEFAUL async def _save_code_snippet(self, update: Update, context: ContextTypes.DEFAULT_TYPE, code: str): """שמירה בפועל של קטע קוד""" - reporter.report_activity(update.effective_user.id) + if reporter is not None: + reporter.report_activity(update.effective_user.id) saving_data = context.user_data.pop('saving_file') if len(code) > config.MAX_CODE_SIZE: @@ -2587,7 +2636,8 @@ async def start_command(update: Update, context: ContextTypes.DEFAULT_TYPE): # # שמור משתמש במסד נתונים (INSERT OR IGNORE) db_manager.save_user(user_id, username) - reporter.report_activity(user_id) + if reporter is not None: + reporter.report_activity(user_id) await log_user_activity(update, context) # הוספת רישום משתמש לסטטיסטיקות # בדיקה אם המשתמש הגיע מה-Web App או רוצה להוסיף קובץ @@ -2655,7 +2705,8 @@ async def start_command(update: Update, context: ContextTypes.DEFAULT_TYPE): # ) async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE): # noqa: D401 - reporter.report_activity(update.effective_user.id) + if reporter is not None: + reporter.report_activity(update.effective_user.id) await log_user_activity(update, context) # הוספת רישום משתמש לסטטיסטיקות await update.message.reply_text( "ℹ️ השתמש ב/start כדי להתחיל.\n\n" diff --git a/refactor_handlers.py b/refactor_handlers.py index 8fc2d9461..d28cbb3e1 100644 --- a/refactor_handlers.py +++ b/refactor_handlers.py @@ -21,18 +21,27 @@ RefactorType, RefactorProposal, ) -from activity_reporter import create_reporter +from typing import Protocol + +# הזרקת reporter בזמן ריצה כדי להימנע מיצירה בזמן import +class _ReporterProto(Protocol): + def report_activity(self, user_id: int) -> None: ... + +class _NoopReporter: + def report_activity(self, user_id: int) -> None: + return None + +reporter: _ReporterProto = _NoopReporter() + +def set_activity_reporter(new_reporter: _ReporterProto) -> None: + global reporter + reporter = new_reporter or _NoopReporter() from config import config from utils import TelegramUtils logger = logging.getLogger(__name__) -# Reporter לפעילות -reporter = create_reporter( - mongodb_uri=config.MONGODB_URL, - service_id=config.BOT_LABEL, - service_name="CodeBot", -) +# reporter יוגדר ב-main בזמן ריצה דרך set_activity_reporter class RefactorHandlers: From cd824f3e800c7640cd135cf74e067aeb8d077c9e Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 17 Oct 2025 01:50:15 +0000 Subject: [PATCH 2/2] Refactor: Move reporter.report_activity to ConversationHandler Co-authored-by: amirbiron --- conversation_handlers.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/conversation_handlers.py b/conversation_handlers.py index 07e8a8672..0007ea361 100644 --- a/conversation_handlers.py +++ b/conversation_handlers.py @@ -171,10 +171,10 @@ async def start_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> i welcome_text = MESSAGES["welcome"].format(name=safe_user_name) keyboard = ReplyKeyboardMarkup(MAIN_KEYBOARD, resize_keyboard=True) await update.message.reply_text(welcome_text, reply_markup=keyboard) -try: - reporter.report_activity(user_id) -except Exception: - pass + try: + reporter.report_activity(user_id) + except Exception: + pass return ConversationHandler.END HELP_PAGES = [