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():