diff --git a/services/admin_bot.py b/services/admin_bot.py index 53b0393..eac7c7b 100644 --- a/services/admin_bot.py +++ b/services/admin_bot.py @@ -42,15 +42,10 @@ def clamp_for_telegram(text: str, has_media: bool) -> str: def get_persian_main_menu(is_paused: bool = False): - pause_btn = "▶️ راه‌اندازی سیستم" if is_paused else "🛑 توقف سیستم" return [ - [Button.text("🏠 منوی اصلی", resize=True), Button.text("📊 آمار ناوگان", resize=True)], - [Button.text("📡 کانال‌های مبدا", resize=True), Button.text("🎯 کانال‌های مقصد", resize=True)], - [Button.text("🌐 وبسایت‌های مبدا", resize=True), Button.text("📂 دسته‌بندی‌ها", resize=True)], - [Button.text("🧠 تنظیمات AI", resize=True), Button.text("⚠️ خطاهای سیستم", resize=True)], - [Button.text("➕ افزودن مبدا / مقصد", resize=True), Button.text("📨 پست‌های بررسی‌نشده", resize=True)], - [Button.text("🔑 کد لاگین", resize=True), Button.text(pause_btn, resize=True)], - [Button.text("❓ راهنمای سیستم", resize=True)] + [Button.text("📋 Copy (محتوا)", resize=True), Button.text("🧠 AI (هوش مصنوعی)", resize=True)], + [Button.text("🤖 Bots (کانال‌ها و ربات‌ها)", resize=True), Button.text("📊 Monitor (مانیتورینگ)", resize=True)], + [Button.text("⚙️ System (سیستم)", resize=True), Button.text("🏠 منوی اصلی", resize=True)] ] @@ -1060,6 +1055,84 @@ class AdminBotService: return text, buttons + async def _render_copy_hub(self) -> Tuple[str, List[List[Button]]]: + unreviewed = await self.repo.get_unreviewed_posts(limit=50) + text = ( + "📋 بخش کپی و محتوا (Copy Hub):\n\n" + f"• 📨 پست‌های منتظر بررسی: {len(unreviewed)} پست\n\n" + "از گزینه‌های زیر برای مدیریت جریان محتوا و انتشار استفاده کنید:" + ) + buttons = [ + [Button.inline(f"📨 ارسال پست‌های بررسی‌نشده ({len(unreviewed)})", data="hub_pending")], + [Button.inline("➕ افزودن مبدا / مقصد جدید", data="hub_add")], + [Button.inline("📂 دسته‌بندی موضوعی کانال‌ها", data="hub_cats")], + [Button.inline("🔙 بازگشت به منوی اصلی", data="back_to_main")] + ] + return text, buttons + + async def _render_bots_hub(self) -> Tuple[str, List[List[Button]]]: + sources = await self.repo.get_active_sources() + targets = await self.repo.get_active_targets() + websites = await self.repo.get_active_source_websites() + text = ( + f"🤖 بخش کانال‌ها و ربات‌ها (Bots Hub):\n\n" + f"• 📡 کانال‌های مبدا: {len(sources)} کانال\n" + f"• 🎯 کانال‌های مقصد: {len(targets)} کانال\n" + f"• 🌐 وبسایت‌های مبدا: {len(websites)} وبسایت\n\n" + f"بخش مورد نظر را برای مشاهده و مدیریت انتخاب کنید:" + ) + buttons = [ + [ + Button.inline(f"📡 کانال‌های مبدا ({len(sources)})", data="list_src"), + Button.inline(f"🎯 کانال‌های مقصد ({len(targets)})", data="list_trg") + ], + [ + Button.inline(f"🌐 وبسایت‌های مبدا ({len(websites)})", data="list_web"), + Button.inline("🔑 کد لاگین یوزربات", data="hub_req_code") + ], + [Button.inline("🔙 بازگشت به منوی اصلی", data="back_to_main")] + ] + return text, buttons + + async def _render_system_hub(self) -> Tuple[str, List[List[Button]]]: + is_paused = await self.repo.is_system_paused() + st_icon = "🛑 متوقف (Paused)" if is_paused else "🟢 فعال و در حال کار (Running)" + pause_btn_text = "▶️ راه‌اندازی و ادامه سیستم" if is_paused else "🛑 توقف اضطراری سیستم" + pause_cb = "sys_resume" if is_paused else "sys_pause" + text = ( + f"⚙️ بخش کنترل و مدیریت سیستم (System Hub):\n\n" + f"• ⚡️ وضعیت کنونی سیستم: {st_icon}\n" + f"• 📋 کانال نظارت ادمین: {self.review_channel_id}\n\n" + f"عملیات مورد نظر را انتخاب کنید:" + ) + buttons = [ + [Button.inline(pause_btn_text, data=pause_cb)], + [ + Button.inline("📝 گزارش تغییرات (Release Notes)", data="sys_changes"), + Button.inline("❓ راهنمای سیستم", data="sys_help") + ], + [Button.inline("🔙 بازگشت به منوی اصلی", data="back_to_main")] + ] + return text, buttons + + async def _render_monitor_hub(self) -> Tuple[str, List[List[Button]]]: + rep = await get_instant_metrics_report() + buttons = [ + [ + Button.inline("🔄 بروزرسانی آمار", data="rep_refresh"), + Button.inline("⚠️ بررسی خطاهای سیستم", data="hub_errors") + ], + [ + Button.inline("⏱ نمودار ۱۵ دقیقه", data="rep_gr:15m"), + Button.inline("⏱ نمودار ۳ ساعت", data="rep_gr:3h") + ], + [ + Button.inline("⏱ نمودار ۲۴ ساعت", data="rep_gr:24h"), + Button.inline("🔙 بازگشت به منوی اصلی", data="back_to_main") + ] + ] + return rep, buttons + async def _render_auto_sources(self, target_id: int): """Toggle screen listing every source with its on/off state for this target.""" target = await self.repo.get_target_by_id(target_id) @@ -1291,6 +1364,42 @@ class AdminBotService: menu = await self.get_menu() await event.reply(welcome_text, parse_mode="html", buttons=menu) + # --- 5 Primary Category Hubs: Copy, AI, Bots, System, Monitor --- + @self.client.on(events.NewMessage(pattern=r"(?i)^(/copy|📋 copy \(محتوا\)|📋 copy|copy)$")) + async def cmd_copy_hub(event: events.NewMessage.Event): + if not self.is_admin(event.sender_id): + return + text, buttons = await self._render_copy_hub() + await event.reply(text, parse_mode="html", buttons=buttons) + + @self.client.on(events.NewMessage(pattern=r"(?i)^(/ai|🧠 ai \(هوش مصنوعی\)|🧠 ai|ai)$")) + async def cmd_ai_hub(event: events.NewMessage.Event): + if not self.is_admin(event.sender_id): + return + text, buttons = await self._render_ai_menu() + await event.reply(text, parse_mode="html", buttons=buttons) + + @self.client.on(events.NewMessage(pattern=r"(?i)^(/bots|🤖 bots \(کانال‌ها و ربات‌ها\)|🤖 bots|bots)$")) + async def cmd_bots_hub(event: events.NewMessage.Event): + if not self.is_admin(event.sender_id): + return + text, buttons = await self._render_bots_hub() + await event.reply(text, parse_mode="html", buttons=buttons) + + @self.client.on(events.NewMessage(pattern=r"(?i)^(/system|⚙️ system \(سیستم\)|⚙️ system|system)$")) + async def cmd_system_hub(event: events.NewMessage.Event): + if not self.is_admin(event.sender_id): + return + text, buttons = await self._render_system_hub() + await event.reply(text, parse_mode="html", buttons=buttons) + + @self.client.on(events.NewMessage(pattern=r"(?i)^(/monitor|📊 monitor \(مانیتورینگ\)|📊 monitor|monitor)$")) + async def cmd_monitor_hub(event: events.NewMessage.Event): + if not self.is_admin(event.sender_id): + return + text, buttons = await self._render_monitor_hub() + await event.reply(text, parse_mode="html", buttons=buttons) + # --- Statistics & Grafana Metrics Report --- @self.client.on(events.NewMessage(pattern=r"(?i)^(/stats|/report|📊 آمار ناوگان|📊 آمار و وضعیت ناوگان|📊 گزارش و آمار سیستم)$")) async def cmd_stats(event: events.NewMessage.Event): @@ -1309,15 +1418,6 @@ class AdminBotService: ] await event.reply(report_text, parse_mode="html", buttons=buttons) - - # --- AI Settings & Logs --- - @self.client.on(events.NewMessage(pattern=r"(?i)^(/ai|🧠 تنظیمات AI|🧠 تنظیمات و لاگ‌های AI)$")) - async def cmd_ai(event: events.NewMessage.Event): - if not self.is_admin(event.sender_id): - return - text, buttons = await self._render_ai_menu() - await event.reply(text, parse_mode="html", buttons=buttons) - # --- System Errors --- @self.client.on(events.NewMessage(pattern=r"(?i)^(/errors|/err|⚠️ خطاهای سیستم)$")) async def cmd_errors(event: events.NewMessage.Event): @@ -1583,13 +1683,17 @@ class AdminBotService: # Ignore main menu commands if text in ["/start", "/menu", "منو", "منوی اصلی", "🏠 منوی اصلی", + "/copy", "📋 copy (محتوا)", "📋 Copy (محتوا)", "📋 کپی", "Copy", + "/ai", "🧠 ai (هوش مصنوعی)", "🧠 AI (هوش مصنوعی)", "🧠 تنظیمات AI", "🧠 تنظیمات و لاگ‌های AI", "AI", + "/bots", "🤖 bots (کانال‌ها و ربات‌ها)", "🤖 Bots (کانال‌ها و ربات‌ها)", "🤖 کانال‌ها و ربات‌ها", "Bots", + "/system", "⚙️ system (سیستم)", "⚙️ System (سیستم)", "⚙️ مدیریت سیستم", "System", + "/monitor", "📊 monitor (مانیتورینگ)", "📊 Monitor (مانیتورینگ)", "📊 مانیتورینگ", "Monitor", "/stats", "/report", "📊 آمار ناوگان", "📊 آمار و وضعیت ناوگان", "📊 گزارش و آمار سیستم", "/sources", "📡 کانال‌های مبدا", "/targets", "🎯 کانال‌های مقصد", "/websites", "/sites", "🌐 وبسایت‌های مبدا", "/request_code", "🔑 کد لاگین", "🔑 درخواست کد لاگین", "/pending", "📨 پست‌های بررسی‌نشده", "📨 ارسال پست‌های بررسی‌نشده", "/errors", "/err", "⚠️ خطاهای سیستم", - "/ai", "🧠 تنظیمات AI", "🧠 تنظیمات و لاگ‌های AI", "/categories", "/cats", "📂 دسته‌بندی‌ها", "📂 دسته‌بندی کانال‌ها", "/add", "➕ افزودن مبدا / مقصد", "➕ افزودن کانال مبدا", "➕ افزودن کانال مقصد", "/pause", "/stop", "🛑 توقف سیستم", "🛑 توقف اضطراری سیستم", @@ -2697,6 +2801,105 @@ class AdminBotService: await event.reply(guide, parse_mode="html", buttons=get_cancel_button()) await event.answer() + elif data == "hub_add": + text = "➕ افزودن منبع یا کانال جدید:\n\nلطفاً نوع مورد نظر را انتخاب کنید:" + buttons = [ + [Button.inline("📡 افزودن کانال تلگرام مبدا", data="hub_add_src")], + [Button.inline("🎯 افزودن کانال تلگرام مقصد", data="hub_add_trg")], + [Button.inline("🌐 افزودن وبسایت مبدا (Web)", data="add_web")], + [Button.inline("🔙 بازگشت به منوی کپی", data="hub_copy_back")] + ] + await event.edit(text, parse_mode="html", buttons=buttons) + await event.answer() + + elif data == "hub_copy_back": + text, buttons = await self._render_copy_hub() + await event.edit(text, parse_mode="html", buttons=buttons) + await event.answer() + + elif data == "hub_pending": + posts = await self.repo.get_unreviewed_posts(limit=50) + if not posts: + await event.answer("✅ هیچ پست بررسی‌نشده‌ای در صف نیست.", alert=True) + return + for post in posts: + await self.send_raw_review_post(post.id) + await event.answer(f"✅ تعداد {len(posts)} پست بررسی‌نشده به کانال ادمین ارسال شد.", alert=True) + + elif data == "hub_cats": + text, buttons = await self._render_categories_menu() + await event.edit(text, parse_mode="html", buttons=buttons or None) + await event.answer() + + elif data == "hub_req_code": + if not self.collector: + await event.answer("سرویس کالکتور متصل نیست.", alert=True) + return + try: + sent = await self.collector.client.send_code_request(self.collector.phone) + self.collector.phone_code_hash = sent.phone_code_hash + self.user_states[event.sender_id] = {"action": "wait_login_code"} + await event.reply( + f"📩 کد تایید ارسال شد!\n\n" + f"لطفا کد دریافت شده به شماره {self.collector.phone} را ارسال کنید:\n" + f"(یا به فرمت /code 12345)", + parse_mode="html", + buttons=get_cancel_button() + ) + await event.answer("کد ارسال شد.") + except Exception as e: + await event.answer(f"خطا در ارسال کد: {e}", alert=True) + + elif data == "hub_errors": + text, buttons = await self._render_error_report() + await event.edit(text, parse_mode="html", buttons=buttons or None) + await event.answer() + + elif data == "sys_pause": + await self.repo.set_system_paused(True) + text, buttons = await self._render_system_hub() + await event.edit(text, parse_mode="html", buttons=buttons) + await event.answer("🛑 سیستم متوقف شد.", alert=True) + + elif data == "sys_resume": + await self.repo.set_system_paused(False) + text, buttons = await self._render_system_hub() + await event.edit(text, parse_mode="html", buttons=buttons) + await event.answer("▶️ سیستم مجدداً فعال شد.", alert=True) + + elif data == "sys_changes": + notes = self.get_latest_change_notes() + await event.reply(notes, parse_mode="html") + await event.answer() + + elif data == "sys_help": + help_text = ( + "📖 راهنمای جامع سیستم کپی‌کار:\n\n" + "• 📋 کپی (Copy): مدیریت پست‌های بررسی‌نشده، دسته‌بندی‌ها و اضافه کردن منابع.\n" + "• 🧠 هوش مصنوعی (AI): تنظیم ارائه‌دهنده‌ها، زنجیره پشتیبان (Fallback)، Vision و پرامپت‌ها.\n" + "• 🤖 ربات‌ها (Bots): کانال‌های مبدا، کانال‌های مقصد، وبسایت‌های مبدا و لاگین یوزربات.\n" + "• 📊 مانیتورینگ (Monitor): آمار لحظه‌ای، صف‌ها، نرخ پردازش و لاگ خطاهای سیستم.\n" + "• ⚙️ سیستم (System): توقف اضطراری، راه‌اندازی مجدد، گزارش تغییرات و راهنما." + ) + await event.reply(help_text, parse_mode="html") + await event.answer() + + elif data == "back_to_main": + userbot_status = "🔴 قطع / نیاز به لاگین" + if self.collector and self.collector.client.is_connected() and await self.collector.client.is_user_authorized(): + me = await self.collector.client.get_me() + userbot_status = f"🟢 آنلاین ({me.first_name})" + + welcome_text = ( + "👋 منوی اصلی سیستم هوشمند کپی‌کار:\n\n" + f"• 🤖 وضعیت ربات جمع‌آوری‌کننده: {userbot_status}\n" + f"• 📋 شناسه کانال ادمین‌ها: {self.review_channel_id}\n\n" + "برای مدیریت بخش‌های مختلف از دکمه‌های زیر استفاده کنید:" + ) + menu = await self.get_menu() + await event.edit(welcome_text, parse_mode="html", buttons=menu) + await event.answer() + elif data.startswith("auto_src:"): target_id = int(data.split(":")[1]) text, buttons = await self._render_auto_sources(target_id) diff --git a/tests/test_fallback_and_stop.py b/tests/test_fallback_and_stop.py index 6b92c72..b1ff8e4 100644 --- a/tests/test_fallback_and_stop.py +++ b/tests/test_fallback_and_stop.py @@ -5,16 +5,18 @@ from core.llm import LLMClient from services.admin_bot import get_persian_main_menu, AdminBotService -def test_main_menu_pause_toggle(): - menu_running = get_persian_main_menu(is_paused=False) - button_texts_running = [getattr(b.button, 'text', '') for row in menu_running for b in row] - assert any("🛑 توقف" in t for t in button_texts_running) - assert not any("▶️ راه‌اندازی" in t for t in button_texts_running) +async def test_system_hub_pause_toggle(): + repo = AsyncMock() + repo.is_system_paused.return_value = False + admin_bot = AdminBotService(repo=repo, ai_processor=None, queue=None, review_channel_id=-100) + text, buttons = await admin_bot._render_system_hub() + btn_texts = [getattr(b, 'text', '') for row in buttons for b in row] + assert any("🛑 توقف" in t for t in btn_texts) - menu_paused = get_persian_main_menu(is_paused=True) - button_texts_paused = [getattr(b.button, 'text', '') for row in menu_paused for b in row] - assert any("▶️ راه‌اندازی" in t for t in button_texts_paused) - assert not any("🛑 توقف" in t for t in button_texts_paused) + repo.is_system_paused.return_value = True + text, buttons = await admin_bot._render_system_hub() + btn_texts = [getattr(b, 'text', '') for row in buttons for b in row] + assert any("▶️ راه‌اندازی" in t for t in btn_texts) async def test_llm_fallback_chain_success_on_fallback(): @@ -137,7 +139,7 @@ async def test_llm_fallback_chain_all_fail_alert(): async def main(): - test_main_menu_pause_toggle() + await test_system_hub_pause_toggle() await test_llm_fallback_chain_success_on_fallback() await test_llm_fallback_chain_all_fail_alert() print("All system stop and multi-provider AI fallback chain tests passed successfully!") diff --git a/tests/test_target_context_and_website_custom_needs.py b/tests/test_target_context_and_website_custom_needs.py index 8e849aa..2d0e919 100644 --- a/tests/test_target_context_and_website_custom_needs.py +++ b/tests/test_target_context_and_website_custom_needs.py @@ -112,11 +112,12 @@ async def test_website_custom_needs_and_prompt_injection(): async def test_menu_layout(): menu = get_persian_main_menu(is_paused=False) - assert len(menu) == 7 - first_row_texts = [getattr(getattr(b, "button", None), "text", str(b)) for b in menu[0]] - assert any("🏠 منوی اصلی" in t for t in first_row_texts) - fifth_row_texts = [getattr(getattr(b, "button", None), "text", str(b)) for b in menu[4]] - assert any("➕ افزودن مبدا / مقصد" in t for t in fifth_row_texts) + all_texts = [getattr(getattr(b, "button", None), "text", str(b)) for row in menu for b in row] + assert any("Copy" in t for t in all_texts) + assert any("AI" in t for t in all_texts) + assert any("Bots" in t for t in all_texts) + assert any("Monitor" in t for t in all_texts) + assert any("System" in t for t in all_texts) async def main():