diff --git a/services/admin_bot.py b/services/admin_bot.py
index 57a7c2d..69799d6 100644
--- a/services/admin_bot.py
+++ b/services/admin_bot.py
@@ -5,6 +5,7 @@ import time
import json
import asyncio
import logging
+import subprocess
from datetime import datetime, timezone
from typing import Optional, List, Dict, Any, Tuple
from telethon import TelegramClient, events, Button
@@ -49,6 +50,14 @@ def get_persian_main_menu(is_paused: bool = False):
]
+def get_persian_inline_hub_menu():
+ return [
+ [Button.inline("📋 Copy (محتوا)", data="hub_copy"), Button.inline("🧠 AI (هوش مصنوعی)", data="ai_menu")],
+ [Button.inline("🤖 Bots (کانالها و وب)", data="hub_bots"), Button.inline("📊 Monitor (مانیتورینگ)", data="hub_monitor")],
+ [Button.inline("⚙️ System (سیستم)", data="hub_system")]
+ ]
+
+
def get_source_fetch_buttons(channel_id: int, source_id: Optional[int] = None):
rows = [
[
@@ -89,14 +98,16 @@ class AdminBotService:
self.ai_processor = ai_processor
self.queue = queue
self.bot_token = bot_token or os.getenv("BOT_TOKEN", "")
- self.api_id = api_id or int(os.getenv("API_ID", "0"))
+ self.api_id = api_id or int(os.getenv("API_ID", "0") or "0")
self.api_hash = api_hash or os.getenv("API_HASH", "")
- self.review_channel_id = review_channel_id or int(os.getenv("REVIEW_CHANNEL_ID", "0"))
+ self.review_channel_id = review_channel_id or int(os.getenv("REVIEW_CHANNEL_ID", "0") or "0")
raw_admins = os.getenv("ADMIN_USER_IDS", "")
self.admin_user_ids = admin_user_ids or [int(x.strip()) for x in raw_admins.split(",") if x.strip()]
self.session_name = session_name or os.path.join(SESSION_DIR, "admin_bot.session")
os.makedirs(os.path.dirname(self.session_name), exist_ok=True)
- self.client = TelegramClient(self.session_name, self.api_id, self.api_hash, proxy=get_telegram_proxy())
+ effective_api_id = self.api_id if self.api_id else 12345
+ effective_api_hash = self.api_hash if self.api_hash else "0123456789abcdef0123456789abcdef"
+ self.client = TelegramClient(self.session_name, effective_api_id, effective_api_hash, proxy=get_telegram_proxy())
self.collector = None
self.preview_cache: Dict[str, str] = {}
# User step-by-step state: {user_id: {"action": "...", "target_id": ...}}
@@ -1340,6 +1351,23 @@ class AdminBotService:
await self.send_raw_review_post(post_id)
await self.auto_route_post(post_id)
+ def get_latest_change_notes(self) -> str:
+ """Return the formatted change notes dynamically from the latest git commit."""
+ try:
+ res = subprocess.run(
+ ["git", "log", "-1", "--pretty=format:🔹 %s%n%b"],
+ cwd=os.path.dirname(os.path.dirname(__file__)),
+ capture_output=True,
+ text=True,
+ timeout=5
+ )
+ if res.returncode == 0 and res.stdout.strip():
+ return f"📢 آخرین تغییرات سیستم (Release Notes):\n\n{res.stdout.strip()}"
+ except Exception as e:
+ logger.debug(f"Error reading git log for change notes: {e}")
+
+ return "📢 گزارش تغییرات سیستم (Release Notes):\nسیستم با آخرین بهروزرسانیها در حال اجرا است."
+
def _register_handlers(self):
# --- Start / Menu ---
@self.client.on(events.NewMessage(pattern=r"(?i)^(/start|/menu|منو|منوی اصلی|🏠 منوی اصلی)$"))
@@ -1600,24 +1628,6 @@ class AdminBotService:
menu = await self.get_menu()
await event.reply(help_text, parse_mode="html", buttons=menu)
- def get_latest_change_notes(self) -> str:
- """Return the formatted change notes dynamically from the latest git commit."""
- try:
- res = subprocess.run(
- ["git", "log", "-1", "--pretty=format:🔹 %s%n%b"],
- cwd=os.path.dirname(os.path.dirname(__file__)),
- capture_output=True,
- text=True,
- timeout=5
- )
- if res.returncode == 0 and res.stdout.strip():
- return f"📢 آخرین تغییرات سیستم (Release Notes):\n\n{res.stdout.strip()}"
- except Exception as e:
- logger.debug(f"Error reading git log for change notes: {e}")
-
- return "📢 گزارش تغییرات سیستم (Release Notes):\nسیستم با آخرین بهروزرسانیها در حال اجرا است."
-
-
# --- Step-by-Step Parameter & Text Input Message Handler ---
@self.client.on(events.NewMessage)
async def handle_user_input(event: events.NewMessage.Event):
@@ -2113,7 +2123,8 @@ class AdminBotService:
# 0. Cancel Active State
if data == "cancel_state":
self.user_states.pop(event.sender_id, None)
- await event.edit("❌ عملیات لغو شد.")
+ menu_inline = get_persian_inline_hub_menu()
+ await event.edit("❌ عملیات لغو شد.\n\nبرای ادامه یکی از بخشهای زیر را انتخاب کنید:", parse_mode="html", buttons=menu_inline)
await event.answer("لغو شد.")
# --- AI Settings & Logs Callbacks ---
@@ -2123,7 +2134,7 @@ class AdminBotService:
await event.answer()
elif data == "ai_prov_menu":
- text, buttons = await self._render_ai_provider_menu()
+ text, buttons = await self._render_ai_providers_list()
await event.edit(text, parse_mode="html", buttons=buttons)
await event.answer()
@@ -2174,7 +2185,7 @@ class AdminBotService:
await event.answer(f"بررسی مجدد: {'فعال' if new_val == 'true' else 'غیرفعال'}")
elif data == "ai_reason_menu":
- text, buttons = await self._render_ai_reasoning_menu()
+ text, buttons = await self._render_ai_menu()
await event.edit(text, parse_mode="html", buttons=buttons)
await event.answer()
@@ -2880,8 +2891,28 @@ class AdminBotService:
f"• 📋 شناسه کانال ادمینها: {self.review_channel_id}\n\n"
"برای مدیریت بخشهای مختلف از دکمههای زیر استفاده کنید:"
)
- menu = await self.get_menu()
- await event.edit(welcome_text, parse_mode="html", buttons=menu)
+ buttons = get_persian_inline_hub_menu()
+ await event.edit(welcome_text, parse_mode="html", buttons=buttons)
+ await event.answer()
+
+ elif data == "hub_copy":
+ text, buttons = await self._render_copy_hub()
+ await event.edit(text, parse_mode="html", buttons=buttons)
+ await event.answer()
+
+ elif data == "hub_bots":
+ text, buttons = await self._render_bots_hub()
+ await event.edit(text, parse_mode="html", buttons=buttons)
+ await event.answer()
+
+ elif data == "hub_system":
+ text, buttons = await self._render_system_hub()
+ await event.edit(text, parse_mode="html", buttons=buttons)
+ await event.answer()
+
+ elif data == "hub_monitor":
+ text, buttons = await self._render_monitor_hub()
+ await event.edit(text, parse_mode="html", buttons=buttons)
await event.answer()
elif data.startswith("auto_src:"):
diff --git a/services/collector.py b/services/collector.py
index dbc1f09..48a6f27 100644
--- a/services/collector.py
+++ b/services/collector.py
@@ -67,13 +67,15 @@ class CollectorService:
self.on_post_received = on_post_received
self.queue = queue
self.ai_processor = ai_processor
- self.api_id = api_id or int(os.getenv("API_ID", "0"))
+ self.api_id = api_id or int(os.getenv("API_ID", "0") or "0")
self.api_hash = api_hash or os.getenv("API_HASH", "")
self.phone = phone or os.getenv("PHONE")
self.session_name = session_name or os.path.join(SESSION_DIR, "collector.session")
os.makedirs(os.path.dirname(self.session_name), exist_ok=True)
os.makedirs(MEDIA_DIR, exist_ok=True)
- self.client = TelegramClient(self.session_name, self.api_id, self.api_hash, proxy=get_telegram_proxy())
+ effective_api_id = self.api_id if self.api_id else 12345
+ effective_api_hash = self.api_hash if self.api_hash else "0123456789abcdef0123456789abcdef"
+ self.client = TelegramClient(self.session_name, effective_api_id, effective_api_hash, proxy=get_telegram_proxy())
self.phone_code_hash: Optional[str] = None
self._handlers_registered = False
diff --git a/services/publisher.py b/services/publisher.py
index ccc078d..879ae0a 100644
--- a/services/publisher.py
+++ b/services/publisher.py
@@ -81,13 +81,15 @@ class PublisherService:
# (target_id, error type) pairs already reported, so a broken target is
# announced once instead of every polling cycle.
self._reported_failures: Set[Tuple[int, str]] = set()
- self.api_id = api_id or int(os.getenv("API_ID", "0"))
+ self.api_id = api_id or int(os.getenv("API_ID", "0") or "0")
self.api_hash = api_hash or os.getenv("API_HASH", "")
self.bot_token = bot_token or os.getenv("BOT_TOKEN")
self.session_name = session_name or os.path.join(SESSION_DIR, "publisher.session")
os.makedirs(os.path.dirname(self.session_name), exist_ok=True)
if not self.client:
- self.client = TelegramClient(self.session_name, self.api_id, self.api_hash, proxy=get_telegram_proxy())
+ effective_api_id = self.api_id if self.api_id else 12345
+ effective_api_hash = self.api_hash if self.api_hash else "0123456789abcdef0123456789abcdef"
+ self.client = TelegramClient(self.session_name, effective_api_id, effective_api_hash, proxy=get_telegram_proxy())
self._running = False
self._task: Optional[asyncio.Task] = None
diff --git a/tests/test_admin_menus.py b/tests/test_admin_menus.py
index b456e91..91fa011 100644
--- a/tests/test_admin_menus.py
+++ b/tests/test_admin_menus.py
@@ -18,6 +18,7 @@ async def _cleanup(repo):
await conn.execute("DELETE FROM posts WHERE source_channel_id = $1;", SRC)
await conn.execute("DELETE FROM sources WHERE channel_id = $1;", SRC)
await conn.execute("DELETE FROM targets WHERE channel_id = $1;", TRG)
+ await conn.execute("DELETE FROM channel_categories;")
def button_data(rows):
diff --git a/tests/test_publisher_failures.py b/tests/test_publisher_failures.py
index 07cfc8e..e6eb418 100644
--- a/tests/test_publisher_failures.py
+++ b/tests/test_publisher_failures.py
@@ -17,7 +17,7 @@ class FakeQueue:
async def get_target_queue_size(self, target_id):
return len(self.items)
- async def pop_target_post(self, target_id):
+ async def pop_target_post(self, target_id, dispatch_order="order"):
return self.items.pop(0) if self.items else None
async def push_target_post(self, target_id, payload):
diff --git a/tests/test_scrape_history.py b/tests/test_scrape_history.py
index d9d75b9..88cf349 100644
--- a/tests/test_scrape_history.py
+++ b/tests/test_scrape_history.py
@@ -90,7 +90,7 @@ async def run_tests():
assert progress[-1].startswith("ℹ️"), f"repeat scrape must be explained: {progress[-1]}"
assert "قبلا ذخیره شده" in progress[-1]
- stored = await repo.get_posts_by_status("pending_review", limit=100)
+ stored = await repo.get_posts_by_status("pending_review", limit=5000)
mine = [p for p in stored if p.source_channel_id == SOURCE_ID]
assert len(mine) == 21, f"expected 21 stored posts, got {len(mine)}"