diff --git a/db/repository.py b/db/repository.py
index 6870ffd..ac20d04 100644
--- a/db/repository.py
+++ b/db/repository.py
@@ -48,12 +48,22 @@ class Repository:
rows = await conn.fetch("SELECT * FROM sources WHERE is_active = TRUE ORDER BY id ASC;")
return [SourceChannel(**dict(r)) for r in rows]
- async def get_source_by_channel_id(self, channel_id: int) -> Optional[SourceChannel]:
+ async def get_source_by_id(self, source_id: int) -> Optional[SourceChannel]:
pool = await self._get_pool()
async with pool.acquire() as conn:
- row = await conn.fetchrow("SELECT * FROM sources WHERE channel_id = $1;", channel_id)
+ row = await conn.fetchrow("SELECT * FROM sources WHERE id = $1;", source_id)
return SourceChannel(**dict(row)) if row else None
+ async def delete_source(self, source_id: int) -> None:
+ pool = await self._get_pool()
+ async with pool.acquire() as conn:
+ await conn.execute("UPDATE sources SET is_active = FALSE WHERE id = $1;", source_id)
+
+ async def delete_target(self, target_id: int) -> None:
+ pool = await self._get_pool()
+ async with pool.acquire() as conn:
+ await conn.execute("UPDATE targets SET is_active = FALSE WHERE id = $1;", target_id)
+
# --- Target Channels ---
async def add_target(
self,
diff --git a/services/admin_bot.py b/services/admin_bot.py
index 4324bd7..636757c 100644
--- a/services/admin_bot.py
+++ b/services/admin_bot.py
@@ -1,8 +1,10 @@
import os
+import re
import logging
from datetime import datetime, timezone
-from typing import Optional, List, Dict
+from typing import Optional, List, Dict, Any, Tuple
from telethon import TelegramClient, events, Button
+from telethon.tl.types import PeerChannel, PeerChat, PeerUser, Channel, Chat
from db.models import Post, TargetChannel, SourceChannel
from db.repository import Repository
from core.queue import RedisQueue
@@ -18,10 +20,12 @@ def get_persian_main_menu():
[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("🎭 تنظیم شخصیت کانالها", resize=True), Button.text("⏰ زمانبندی و خواب کانالها", resize=True)],
[Button.text("❓ راهنمای سیستم", resize=True)]
]
+def get_cancel_button():
+ return [[Button.inline("❌ انصراف و بازگشت به منو", data="cancel_state")]]
+
class AdminBotService:
def __init__(
self,
@@ -49,6 +53,8 @@ class AdminBotService:
self.client = TelegramClient(self.session_name, self.api_id, self.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": ...}}
+ self.user_states: Dict[int, Dict[str, Any]] = {}
def set_collector(self, collector):
self.collector = collector
@@ -82,6 +88,98 @@ class AdminBotService:
logger.info("Admin Bot connected successfully.")
self._register_handlers()
+ # --- Helper to resolve channel from forwarded message or text ---
+ async def _resolve_channel(self, event: events.NewMessage.Event) -> Tuple[Optional[int], Optional[str], Optional[str], str]:
+ """Extract (channel_id, title, username, error_message) using Userbot or Bot Client."""
+ client_to_use = self.collector.client if (self.collector and self.collector.client.is_connected()) else self.client
+
+ # 1. Forwarded Message
+ if event.message.fwd_from:
+ fwd = event.message.fwd_from
+ if fwd.from_id:
+ try:
+ entity = await client_to_use.get_entity(fwd.from_id)
+ raw_id = entity.id
+ ch_id = int(f"-100{raw_id}") if not str(raw_id).startswith("-100") else raw_id
+ title = getattr(entity, "title", getattr(entity, "first_name", "کانال"))
+ username = getattr(entity, "username", None)
+ return ch_id, title, username, ""
+ except Exception as e:
+ logger.debug(f"Failed to resolve fwd entity: {e}")
+
+ if fwd.from_name:
+ return None, None, None, f"⚠️ فوروارد از کانال به صورت ناشناس ارسال شده است. لطفا آیدی، یوزرنیم یا لینک کانال (مثل @channel یا https://t.me/channel) را ارسال کنید."
+
+ # 2. Text input (Username / Link / Numeric ID)
+ text = (event.raw_text or "").strip()
+ if not text:
+ return None, None, None, "پیامی دریافت نشد."
+
+ clean = text
+ if "t.me/" in clean:
+ clean = clean.split("t.me/")[-1].replace("/", "").strip()
+ if clean.startswith("@"):
+ clean = clean[1:]
+
+ try:
+ # Try numeric ID directly
+ if clean.replace("-", "").isdigit():
+ num_id = int(clean)
+ entity = await client_to_use.get_entity(num_id)
+ else:
+ entity = await client_to_use.get_entity(clean)
+
+ raw_id = entity.id
+ ch_id = int(f"-100{raw_id}") if not str(raw_id).startswith("-100") else raw_id
+ title = getattr(entity, "title", getattr(entity, "first_name", clean))
+ username = getattr(entity, "username", clean if not clean.replace("-", "").isdigit() else None)
+ return ch_id, title, username, ""
+ except Exception as e:
+ return None, None, None, f"❌ امکان واکشی اطلاعات کانال وجود نداشت ({e}). لطفا مطمئن شوید ربات یا یوزربات در کانال عضو است."
+
+ # --- Target Management Card Builder ---
+ async def _render_target_config(self, target_id: int) -> Tuple[str, List[List[Button]]]:
+ target = await self.repo.get_target_by_id(target_id)
+ if not target:
+ return "❌ کانال یافت نشد.", []
+
+ qsize = await self.queue.get_target_queue_size(target.id) if self.queue else 0
+ sleep_st = f"🌙 فعال ({target.sleep_start_hour}:00 تا {target.sleep_end_hour}:00)" if target.is_sleep_enabled else "☀️ غیرفعال"
+
+ card_text = (
+ f"🎯 تنظیمات کانال مقصد: {target.title}\n\n"
+ f"• 🆔 شناسه کانال: {target.channel_id} (ID: {target.id})\n"
+ f"• 🔗 یوزرنیم: @{target.username or 'ندارد'}\n"
+ f"• ⏱ فاصله ارسال پستها: هر {target.post_interval_min} دقیقه\n"
+ f"• 🌙 وضعیت ساعت خواب: {sleep_st}\n"
+ f"• 📥 پستهای منتظر در صف: {qsize} پست\n\n"
+ f"🎭 شخصیت و لحن نگارش:\n"
+ f"{target.personality or 'پیشفرض (رسمی و روان)'}\n\n"
+ f"🏷 فوتر و هشتگهای اختصاصی:\n"
+ f"{target.custom_footer or 'ندارد'}\n\n"
+ "👇 برای ویرایش هر بخش روی دکمه مربوطه کلیک کنید:"
+ )
+
+ buttons = [
+ [
+ Button.inline("🎭 تغییر لحن و استایل", data=f"st_pers:{target.id}"),
+ Button.inline("🏷 تغییر فوتر و تگها", data=f"st_foot:{target.id}")
+ ],
+ [
+ Button.inline("⏱ تغییر فاصله ارسال", data=f"st_intv:{target.id}"),
+ Button.inline("🌙 تنظیم ساعت خواب", data=f"st_slp:{target.id}")
+ ]
+ ]
+ if target.is_sleep_enabled:
+ buttons.append([Button.inline("☀️ غیرفعالسازی ساعت خواب", data=f"dis_slp:{target.id}")])
+
+ buttons.append([
+ Button.inline("🗑 حذف این کانال مقصد", data=f"del_trg:{target.id}"),
+ Button.inline("🔙 بازگشت به لیست کانالها", data="list_trg")
+ ])
+ return card_text, buttons
+
+ # --- Review Post UI Helpers ---
def _build_raw_post_keyboard(self, post: Post, targets: List[TargetChannel]):
buttons = []
sent_target_ids = set()
@@ -162,6 +260,8 @@ class AdminBotService:
await event.reply(f"⛔ دسترسی غیرمجاز. شناسه عددی شما: {event.sender_id}", parse_mode="html")
return
+ self.user_states.pop(event.sender_id, None)
+
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()
@@ -171,7 +271,7 @@ class AdminBotService:
"👋 به پنل مدیریت سیستم هوشمند کپیکار خوش آمدید!\n\n"
f"• 🤖 وضعیت ربات جمعآوریکننده: {userbot_status}\n"
f"• 📋 شناسه کانال ادمینها: {self.review_channel_id}\n\n"
- "از دکمههای زیر برای مدیریت کانالها، تنظیم شخصیت و زمانبندی استفاده کنید:"
+ "برای مدیریت کانالها از دکمههای زیر استفاده کنید:"
)
await event.reply(welcome_text, parse_mode="html", buttons=get_persian_main_menu())
@@ -207,37 +307,17 @@ class AdminBotService:
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 msg.edit(
f"📩 کد تایید ارسال شد!\n\n"
- f"کد ارسال شده به شماره {self.collector.phone} را به این صورت ارسال کنید:\n"
- f"/code 12345",
- parse_mode="html"
+ f"لطفا کد دریافت شده به شماره {self.collector.phone} را در پیام بعدی ارسال کنید:\n"
+ f"(یا به فرمت /code 12345)",
+ parse_mode="html",
+ buttons=get_cancel_button()
)
except Exception as e:
await msg.edit(f"❌ خطا در درخواست کد: {e}")
- @self.client.on(events.NewMessage(pattern=r"^/code\s+(\S+)"))
- async def cmd_code(event: events.NewMessage.Event):
- if not self.is_admin(event.sender_id):
- return
- if not self.collector:
- return
- code = event.pattern_match.group(1).strip()
- msg = await event.reply("⏳ در حال بررسی کد تایید...")
- result = await self.collector.submit_code(code)
- await msg.edit(result, parse_mode="html", buttons=get_persian_main_menu())
-
- @self.client.on(events.NewMessage(pattern=r"^/password\s+(.+)"))
- async def cmd_password(event: events.NewMessage.Event):
- if not self.is_admin(event.sender_id):
- return
- if not self.collector:
- return
- pwd = event.pattern_match.group(1).strip()
- msg = await event.reply("⏳ در حال تایید رمز دو مرحلهای...")
- result = await self.collector.submit_password(pwd)
- await msg.edit(result, parse_mode="html", buttons=get_persian_main_menu())
-
# --- Sources Management ---
@self.client.on(events.NewMessage(pattern=r"(?i)^(/sources|📡 کانالهای مبدا)$"))
async def cmd_sources(event: events.NewMessage.Event):
@@ -245,57 +325,38 @@ class AdminBotService:
return
sources = await self.repo.get_active_sources()
if not sources:
- await event.reply("هیچ کانال مبدایی ثبت نشده است. از دکمه ➕ افزودن کانال مبدا استفاده کنید.", parse_mode="html")
+ await event.reply("هیچ کانال مبدایی ثبت نشده است. روی ➕ افزودن کانال مبدا بزنید.", parse_mode="html")
return
await event.reply(f"📡 کانالهای مبدا فعال ({len(sources)} کانال):", parse_mode="html")
for s in sources:
card = (
- f"📢 {s.title or 'کانال'}\n"
+ f"📢 {s.title or 'کانال مبدا'}\n"
f"• شناسه: {s.channel_id}\n"
f"• یوزرنیم: @{s.username or 'ندارد'}"
)
buttons = [
[
- Button.inline("📥 استخراج ۲۰ پست گذشته", data=f"hist:{s.channel_id}:20"),
- Button.inline("📥 استخراج ۵۰ پست گذشته", data=f"hist:{s.channel_id}:50"),
+ Button.inline("📥 استخراج ۲۰ پست", data=f"hist:{s.channel_id}:20"),
+ Button.inline("📥 استخراج ۵۰ پست", data=f"hist:{s.channel_id}:50"),
+ ],
+ [
+ Button.inline("🗑 حذف این کانال مبدا", data=f"del_src:{s.id}")
]
]
await event.reply(card, parse_mode="html", buttons=buttons)
@self.client.on(events.NewMessage(pattern=r"(?i)^(➕ افزودن کانال مبدا)$"))
- async def cmd_add_source_guide(event: events.NewMessage.Event):
+ async def cmd_add_source_prompt(event: events.NewMessage.Event):
if not self.is_admin(event.sender_id):
return
+ self.user_states[event.sender_id] = {"action": "wait_source_fwd"}
guide = (
- "➕ راهنمای افزودن کانال مبدا:\n\n"
- "دستور را با فرمت زیر ارسال کنید:\n"
- "/add_source <شناسه_عددی_کانال> <عنوان> [یوزرنیم]\n\n"
- "مثال:\n"
- "/add_source -1001234567890 اخبار_فوری fouri_news"
- )
- await event.reply(guide, parse_mode="html")
-
- @self.client.on(events.NewMessage(pattern=r"^/add_source\s+(-?\d+)\s+([^\s]+)(?:\s+([^\s]+))?"))
- async def cmd_add_source(event: events.NewMessage.Event):
- if not self.is_admin(event.sender_id):
- return
- ch_id = int(event.pattern_match.group(1))
- title = event.pattern_match.group(2)
- username = event.pattern_match.group(3)
- await self.repo.add_source(channel_id=ch_id, title=title, username=username)
-
- buttons = [
- [
- Button.inline("📥 استخراج ۲۰ پست گذشته این کانال", data=f"hist:{ch_id}:20"),
- Button.inline("📥 استخراج ۵۰ پست گذشته این کانال", data=f"hist:{ch_id}:50")
- ]
- ]
- await event.reply(
- f"✅ کانال مبدا {title} ({ch_id}) با موفقیت افزوده شد.\nآیا میخواهید پستهای قبلی این کانال را هم دریافت کنید؟",
- parse_mode="html",
- buttons=buttons
+ "📡 افزودن کانال مبدا جدید:\n\n"
+ "سادهترین روش: یک پیام از کانال مورد نظر به این ربات فوروارد (Forward) کنید!\n\n"
+ "(یا میتوانید آیدی، یوزرنیم یا لینک کانال مثل @channel_username را بفرستید)"
)
+ await event.reply(guide, parse_mode="html", buttons=get_cancel_button())
# --- Targets Management ---
@self.client.on(events.NewMessage(pattern=r"(?i)^(/targets|🎯 کانالهای مقصد)$"))
@@ -304,196 +365,25 @@ class AdminBotService:
return
targets = await self.repo.get_active_targets()
if not targets:
- await event.reply("هیچ کانال مقصدی ثبت نشده است. از دکمه ➕ افزودن کانال مقصد استفاده کنید.", parse_mode="html")
+ await event.reply("هیچ کانال مقصدی ثبت نشده است. روی ➕ افزودن کانال مقصد بزنید.", parse_mode="html")
return
- lines = ["🎯 کانالهای مقصد برای انتشار:\n"]
+
+ await event.reply(f"🎯 کانالهای مقصد برای انتشار ({len(targets)} کانال):\nبرای تنظیمات هر کانال، روی دکمه آن بزنید:", parse_mode="html")
for t in targets:
- qsize = await self.queue.get_target_queue_size(t.id) if self.queue else 0
- sleep_info = f"{t.sleep_start_hour}:00 تا {t.sleep_end_hour}:00" if t.is_sleep_enabled else "غیرفعال"
- lines.append(
- f"• {t.title} (شناسه: {t.channel_id} | ID دیتابیس: {t.id})\n"
- f" ⏱ فاصله ارسال: هر {t.post_interval_min} دقیقه\n"
- f" 🌙 ساعت خواب: {sleep_info}\n"
- f" 📥 تعداد در صف ارسال: {qsize} پست\n"
- f" 🎭 شخصیت: {t.personality or 'پیشفرض'}\n"
- f" 🏷 فوتر: {t.custom_footer or 'ندارد'}\n"
- )
- await event.reply("\n".join(lines), parse_mode="html", buttons=get_persian_main_menu())
+ card, buttons = await self._render_target_config(t.id)
+ await event.reply(card, parse_mode="html", buttons=buttons)
@self.client.on(events.NewMessage(pattern=r"(?i)^(➕ افزودن کانال مقصد)$"))
- async def cmd_add_target_guide(event: events.NewMessage.Event):
+ async def cmd_add_target_prompt(event: events.NewMessage.Event):
if not self.is_admin(event.sender_id):
return
+ self.user_states[event.sender_id] = {"action": "wait_target_fwd"}
guide = (
- "🎯 راهنمای افزودن کانال مقصد:\n\n"
- "دستور را با فرمت زیر ارسال کنید:\n"
- "/add_target <شناسه_کانال> <عنوان> [یوزرنیم]\n\n"
- "مثال:\n"
- "/add_target -1009876543210 دنیای_هوش_مصنوعی ai_world_chan"
- )
- await event.reply(guide, parse_mode="html")
-
- @self.client.on(events.NewMessage(pattern=r"^/add_target\s+(-?\d+)\s+([^\s]+)(?:\s+([^\s]+))?"))
- async def cmd_add_target(event: events.NewMessage.Event):
- if not self.is_admin(event.sender_id):
- return
- ch_id = int(event.pattern_match.group(1))
- title = event.pattern_match.group(2)
- username = event.pattern_match.group(3)
- tid = await self.repo.add_target(channel_id=ch_id, title=title, username=username)
- await event.reply(
- f"✅ کانال مقصد {title} افزوده شد (ID دیتابیس: {tid}).\n\n"
- f"اکنون میتوانید با دکمه 🎭 تنظیم شخصیت کانالها لحن و با ⏰ زمانبندی و خواب فواصل ارسال را تنظیم کنید.",
- parse_mode="html",
- buttons=get_persian_main_menu()
- )
-
- # --- Schedule & Sleep Management ---
- @self.client.on(events.NewMessage(pattern=r"(?i)^(/schedule|⏰ زمانبندی و خواب کانالها)$"))
- async def cmd_schedule(event: events.NewMessage.Event):
- if not self.is_admin(event.sender_id):
- return
- targets = await self.repo.get_active_targets()
- if not targets:
- await event.reply("ابتدا یک کانال مقصد اضافه کنید.", parse_mode="html")
- return
-
- text = (
- "⏰ مدیریت فاصله ارسال و ساعت خواب کانالها:\n\n"
- "1. تنظیم فاصله ارسال پستها (به دقیقه):\n"
- "/set_interval <شناسه_دیتابیس_کانال> <دقیقه>\n"
- "مثال (ارسال هر ۲۰ دقیقه یک پست):\n"
- "/set_interval 1 20\n\n"
- "2. تنظیم ساعت خواب (عدم ارسال پیام در این ساعات):\n"
- "/set_sleep <شناسه_دیتابیس_کانال> <ساعت_شروع> <ساعت_پایان>\n"
- "مثال (خواب از ساعت ۲۳ شب تا ۸ صبح):\n"
- "/set_sleep 1 23 8\n\n"
- "3. غیرفعال کردن ساعت خواب:\n"
- "/disable_sleep <شناسه_دیتابیس_کانال>\n\n"
- "وضعیت فعلی کانالها:\n"
- )
- for t in targets:
- qsize = await self.queue.get_target_queue_size(t.id) if self.queue else 0
- sleep_st = f"🌙 خواب از {t.sleep_start_hour}:00 تا {t.sleep_end_hour}:00" if t.is_sleep_enabled else "☀️ بدون ساعت خواب"
- text += f"• ID: {t.id} | {t.title} ➔ هر {t.post_interval_min} دقیقه | {sleep_st} (صف: {qsize} پست)\n"
-
- await event.reply(text, parse_mode="html", buttons=get_persian_main_menu())
-
- @self.client.on(events.NewMessage(pattern=r"^/set_interval\s+(\d+)\s+(\d+)"))
- async def cmd_set_interval(event: events.NewMessage.Event):
- if not self.is_admin(event.sender_id):
- return
- target_id = int(event.pattern_match.group(1))
- interval_min = int(event.pattern_match.group(2))
- target = await self.repo.get_target_by_id(target_id)
- if not target:
- await event.reply(f"❌ کانال مقصد با شناسه {target_id} یافت نشد.")
- return
- await self.repo.update_target_schedule(target_id=target_id, post_interval_min=interval_min)
- await event.reply(
- f"✅ فاصله ارسال برای کانال {target.title} به هر {interval_min} دقیقه تنظیم شد.",
- parse_mode="html",
- buttons=get_persian_main_menu()
- )
-
- @self.client.on(events.NewMessage(pattern=r"^/set_sleep\s+(\d+)\s+(\d+)\s+(\d+)"))
- async def cmd_set_sleep(event: events.NewMessage.Event):
- if not self.is_admin(event.sender_id):
- return
- target_id = int(event.pattern_match.group(1))
- start_h = int(event.pattern_match.group(2))
- end_h = int(event.pattern_match.group(3))
- target = await self.repo.get_target_by_id(target_id)
- if not target:
- await event.reply(f"❌ کانال مقصد با شناسه {target_id} یافت نشد.")
- return
- await self.repo.update_target_schedule(
- target_id=target_id,
- sleep_start_hour=start_h,
- sleep_end_hour=end_h,
- is_sleep_enabled=True
- )
- await event.reply(
- f"🌙 ساعت خواب برای کانال {target.title} از ساعت {start_h}:00 تا {end_h}:00 فعال شد.\n(در این بازه هیچ پیامی ارسال نخواهد شد و در صف منتظر میماند).",
- parse_mode="html",
- buttons=get_persian_main_menu()
- )
-
- @self.client.on(events.NewMessage(pattern=r"^/disable_sleep\s+(\d+)"))
- async def cmd_disable_sleep(event: events.NewMessage.Event):
- if not self.is_admin(event.sender_id):
- return
- target_id = int(event.pattern_match.group(1))
- target = await self.repo.get_target_by_id(target_id)
- if not target:
- await event.reply(f"❌ کانال مقصد با شناسه {target_id} یافت نشد.")
- return
- await self.repo.update_target_schedule(target_id=target_id, is_sleep_enabled=False)
- await event.reply(f"☀️ ساعت خواب برای کانال {target.title} غیرفعال شد.", parse_mode="html", buttons=get_persian_main_menu())
-
- # --- Channel Personality & Tags Configuration ---
- @self.client.on(events.NewMessage(pattern=r"(?i)^(/personality|🎭 تنظیم شخصیت کانالها)$"))
- async def cmd_personality(event: events.NewMessage.Event):
- if not self.is_admin(event.sender_id):
- return
- targets = await self.repo.get_active_targets()
- if not targets:
- await event.reply("ابتدا با استفاده از ➕ افزودن کانال مقصد یک کانال مقصد اضافه کنید.", parse_mode="html")
- return
-
- text = (
- "🎭 تنظیم شخصیت، لحن و تگهای کانالهای مقصد:\n\n"
- "برای تغییر لحن و استایل نگارش کانال از دستور زیر استفاده کنید:\n"
- "/set_personality <شناسه_دیتابیس_کانال> <توضیحات لحن>\n\n"
- "مثال:\n"
- "/set_personality 1 لحن جذاب و ژورنالیستی، استفاده از تیترهای بولد و ایموجیهای مرتبط\n\n"
- "برای تنظیم فوتر و هشتگهای اختصاصی انتهای پست:\n"
- "/set_footer <شناسه_دیتابیس_کانال> <تگها یا آیدی کانال>\n\n"
- "مثال:\n"
- "/set_footer 1 🆔 @my_tech_chan\n#تکنولوژی #هوش_مصنوعی\n\n"
- "کانالهای موجود و شخصیت فعلی:\n"
- )
- for t in targets:
- text += (
- f"• ID: {t.id} | {t.title}\n"
- f" 🎭 لحن: {t.personality or 'پیشفرض'}\n"
- f" 🏷 فوتر: {t.custom_footer or 'ندارد'}\n\n"
- )
- await event.reply(text, parse_mode="html", buttons=get_persian_main_menu())
-
- @self.client.on(events.NewMessage(pattern=r"^/set_personality\s+(\d+)\s+(.+)"))
- async def cmd_set_personality(event: events.NewMessage.Event):
- if not self.is_admin(event.sender_id):
- return
- target_id = int(event.pattern_match.group(1))
- personality = event.pattern_match.group(2).strip()
- target = await self.repo.get_target_by_id(target_id)
- if not target:
- await event.reply(f"❌ کانال مقصد با شناسه {target_id} یافت نشد.")
- return
- await self.repo.update_target_personality(target_id, personality)
- await event.reply(
- f"✅ شخصیت و لحن کانال {target.title} با موفقیت به روز شد:\n\n{personality}",
- parse_mode="html",
- buttons=get_persian_main_menu()
- )
-
- @self.client.on(events.NewMessage(pattern=r"^/set_footer\s+(\d+)\s+([\s\S]+)"))
- async def cmd_set_footer(event: events.NewMessage.Event):
- if not self.is_admin(event.sender_id):
- return
- target_id = int(event.pattern_match.group(1))
- footer = event.pattern_match.group(2).strip()
- target = await self.repo.get_target_by_id(target_id)
- if not target:
- await event.reply(f"❌ کانال مقصد با شناسه {target_id} یافت نشد.")
- return
- await self.repo.update_target_footer(target_id, footer)
- await event.reply(
- f"✅ فوتر اختصاصی کانال {target.title} به روز شد:\n\n{footer}",
- parse_mode="html",
- buttons=get_persian_main_menu()
+ "🎯 افزودن کانال مقصد جدید:\n\n"
+ "سادهترین روش: یک پیام از کانال مورد نظر به این ربات فوروارد (Forward) کنید!\n\n"
+ "(یا میتوانید آیدی، یوزرنیم یا لینک کانال مثل @channel_username را بفرستید)"
)
+ await event.reply(guide, parse_mode="html", buttons=get_cancel_button())
# --- Help ---
@self.client.on(events.NewMessage(pattern=r"(?i)^(/help|❓ راهنمای سیستم)$"))
@@ -501,13 +391,141 @@ class AdminBotService:
if not self.is_admin(event.sender_id):
return
help_text = (
- "📖 راهنمای فرآیند کاری سیستم کپیکار:\n\n"
- "1. 📥 دریافت آنی: پستهای مبدا فوری و بدون تاخیر در کانال ادمین قرار میگیرند.\n"
- "2. 🎯 بازنویسی بر اساس مقصد: با زدن دکمه کانال مقصد، هوش مصنوعی پست را متناسب با شخصیت و فوتر آن کانال بازنویسی میکند.\n"
- "3. ⏳ صف ارسال زمانبندی شده: پس از تایید، پست در صف ردیس کانال مقصد قرار میگیرد و با رعایت فاصله زمانی (Interval) و ساعات خواب (Sleep) به ترتیب منتشر میشود."
+ "📖 راهنمای سیستم کپیکار:\n\n"
+ "• افزودن آسان کانالها: فقط کافیست یک پیام از کانال را به ربات فوروارد کنید تا شناسه و نام آن خودکار ثبت شود!\n"
+ "• تنظیمات با دکمه: در بخش کانالهای مقصد، روی هر کانال دکمههای تغییر لحن، تگها، فاصله زمانی و ساعت خواب وجود دارد.\n"
+ "• بازنویسی هوشمند: پستهای مبدا به کانال ادمین میآیند و با لمس دکمه هر مقصد، متن با هوش مصنوعی و لحن همان کانال بازنویسی میشود."
)
await event.reply(help_text, parse_mode="html", buttons=get_persian_main_menu())
+ # --- Step-by-Step Parameter & Text Input Message Handler ---
+ @self.client.on(events.NewMessage)
+ async def handle_user_input(event: events.NewMessage.Event):
+ if not self.is_admin(event.sender_id):
+ return
+
+ text = (event.raw_text or "").strip()
+
+ # Ignore main menu commands
+ if text in ["/start", "/menu", "منو", "/stats", "📊 آمار و وضعیت ناوگان",
+ "/sources", "📡 کانالهای مبدا", "/targets", "🎯 کانالهای مقصد",
+ "➕ افزودن کانال مبدا", "➕ افزودن کانال مقصد", "❓ راهنمای سیستم",
+ "/request_code", "🔑 درخواست کد لاگین"]:
+ return
+
+ # Cancel command
+ if text in ["/cancel", "انصراف", "لغو"]:
+ self.user_states.pop(event.sender_id, None)
+ await event.reply("❌ عملیات لغو شد.", buttons=get_persian_main_menu())
+ return
+
+ state = self.user_states.get(event.sender_id)
+ if not state:
+ # Direct /code or /password handling
+ if text.startswith("/code "):
+ code = text.split("/code ", 1)[1].strip()
+ res = await self.collector.submit_code(code)
+ await event.reply(res, parse_mode="html", buttons=get_persian_main_menu())
+ elif text.startswith("/password "):
+ pwd = text.split("/password ", 1)[1].strip()
+ res = await self.collector.submit_password(pwd)
+ await event.reply(res, parse_mode="html", buttons=get_persian_main_menu())
+ return
+
+ action = state.get("action")
+
+ # 1. Waiting for Source Channel Forward or Text
+ if action == "wait_source_fwd":
+ ch_id, title, username, err = await self._resolve_channel(event)
+ if err or not ch_id:
+ await event.reply(err or "خطا در دریافت اطلاعات کانال. لطفا مجدد ارسال کنید:", buttons=get_cancel_button())
+ return
+
+ self.user_states.pop(event.sender_id, None)
+ await self.repo.add_source(channel_id=ch_id, title=title, username=username)
+
+ buttons = [
+ [
+ Button.inline("📥 استخراج ۲۰ پست گذشته", data=f"hist:{ch_id}:20"),
+ Button.inline("📥 استخراج ۵۰ پست گذشته", data=f"hist:{ch_id}:50")
+ ]
+ ]
+ await event.reply(
+ f"✅ کانال مبدا {title} با شناسه {ch_id} با موفقیت افزوده شد!\n\n"
+ f"آیا مایلید پستهای قبلی این کانال هم دریافت شود؟",
+ parse_mode="html",
+ buttons=buttons
+ )
+
+ # 2. Waiting for Target Channel Forward or Text
+ elif action == "wait_target_fwd":
+ ch_id, title, username, err = await self._resolve_channel(event)
+ if err or not ch_id:
+ await event.reply(err or "خطا در دریافت اطلاعات کانال. لطفا مجدد ارسال کنید:", buttons=get_cancel_button())
+ return
+
+ self.user_states.pop(event.sender_id, None)
+ target_id = await self.repo.add_target(channel_id=ch_id, title=title, username=username)
+
+ card, buttons = await self._render_target_config(target_id)
+ await event.reply(
+ f"✅ کانال مقصد {title} افزوده شد!\n\nاکنون میتوانید با دکمههای زیر لحن و زمانبندی آن را تنظیم کنید:",
+ parse_mode="html"
+ )
+ await event.reply(card, parse_mode="html", buttons=buttons)
+
+ # 3. Waiting for Target Personality
+ elif action == "wait_personality":
+ target_id = state.get("target_id")
+ self.user_states.pop(event.sender_id, None)
+ await self.repo.update_target_personality(target_id, text)
+ card, buttons = await self._render_target_config(target_id)
+ await event.reply("✅ لحن و شخصیت کانال با موفقیت به روز شد!", parse_mode="html")
+ await event.reply(card, parse_mode="html", buttons=buttons)
+
+ # 4. Waiting for Target Custom Footer
+ elif action == "wait_footer":
+ target_id = state.get("target_id")
+ self.user_states.pop(event.sender_id, None)
+ await self.repo.update_target_footer(target_id, text)
+ card, buttons = await self._render_target_config(target_id)
+ await event.reply("✅ فوتر و تگهای اختصاصی کانال با موفقیت به روز شد!", parse_mode="html")
+ await event.reply(card, parse_mode="html", buttons=buttons)
+
+ # 5. Waiting for Target Post Interval
+ elif action == "wait_interval":
+ target_id = state.get("target_id")
+ if not text.isdigit() or int(text) < 1:
+ await event.reply("⚠️ لطفا یک عدد معتبر (به دقیقه) ارسال کنید:", buttons=get_cancel_button())
+ return
+ interval_min = int(text)
+ self.user_states.pop(event.sender_id, None)
+ await self.repo.update_target_schedule(target_id=target_id, post_interval_min=interval_min)
+ card, buttons = await self._render_target_config(target_id)
+ await event.reply(f"✅ فاصله ارسال به هر {interval_min} دقیقه تغییر یافت!", parse_mode="html")
+ await event.reply(card, parse_mode="html", buttons=buttons)
+
+ # 6. Waiting for Sleep Window Hours
+ elif action == "wait_sleep":
+ target_id = state.get("target_id")
+ parts = re.findall(r"\d+", text)
+ if len(parts) < 2:
+ await event.reply("⚠️ لطفا ساعت شروع و پایان را به این شکل بفرستید: 23 8 (برای ۲۳:۰۰ تا ۰۸:۰۰)", parse_mode="html", buttons=get_cancel_button())
+ return
+ start_h = int(parts[0]) % 24
+ end_h = int(parts[1]) % 24
+ self.user_states.pop(event.sender_id, None)
+ await self.repo.update_target_schedule(target_id=target_id, sleep_start_hour=start_h, sleep_end_hour=end_h, is_sleep_enabled=True)
+ card, buttons = await self._render_target_config(target_id)
+ await event.reply(f"🌙 ساعت خواب از {start_h}:00 تا {end_h}:00 فعال شد!", parse_mode="html")
+ await event.reply(card, parse_mode="html", buttons=buttons)
+
+ # 7. Waiting for Login Code
+ elif action == "wait_login_code":
+ self.user_states.pop(event.sender_id, None)
+ res = await self.collector.submit_code(text)
+ await event.reply(res, parse_mode="html", buttons=get_persian_main_menu())
+
# --- Inline Callback Queries ---
@self.client.on(events.CallbackQuery)
async def on_callback(event: events.CallbackQuery.Event):
@@ -517,8 +535,96 @@ class AdminBotService:
data = event.data.decode("utf-8")
- # 1. Historical Scraping Callback
- if data.startswith("hist:"):
+ # 0. Cancel Active State
+ if data == "cancel_state":
+ self.user_states.pop(event.sender_id, None)
+ await event.edit("❌ عملیات لغو شد.")
+ await event.answer("لغو شد.")
+
+ # --- Target Channel Config Buttons ---
+ elif data.startswith("st_pers:"):
+ target_id = int(data.split(":")[1])
+ target = await self.repo.get_target_by_id(target_id)
+ self.user_states[event.sender_id] = {"action": "wait_personality", "target_id": target_id}
+ await event.reply(
+ f"🎭 تغییر لحن و شخصیت کانال «{target.title}»:\n\n"
+ "لطفا در پیام بعدی، لحن و استایل نگارش مورد نظرتان را بفرستید:\n"
+ "(مثال: لحن دوستانه و پرانرژی همراه با ایموجی و تیترهای جذاب)",
+ parse_mode="html",
+ buttons=get_cancel_button()
+ )
+ await event.answer()
+
+ elif data.startswith("st_foot:"):
+ target_id = int(data.split(":")[1])
+ target = await self.repo.get_target_by_id(target_id)
+ self.user_states[event.sender_id] = {"action": "wait_footer", "target_id": target_id}
+ await event.reply(
+ f"🏷 تغییر فوتر و تگهای اختصاصی «{target.title}»:\n\n"
+ "لطفا متن فوتر، آیدی یا هشتگهایی که میخواهید انتهای هر پست قرار بگیرد را بفرستید:\n"
+ "(مثال: 🆔 @my_chan\n#فناوری #اخبار)",
+ parse_mode="html",
+ buttons=get_cancel_button()
+ )
+ await event.answer()
+
+ elif data.startswith("st_intv:"):
+ target_id = int(data.split(":")[1])
+ target = await self.repo.get_target_by_id(target_id)
+ self.user_states[event.sender_id] = {"action": "wait_interval", "target_id": target_id}
+ await event.reply(
+ f"⏱ تغییر فاصله ارسال کانال «{target.title}»:\n\n"
+ "فاصله زمانی بین ارسال هر پست را به دقیقه بفرستید:\n"
+ "(مثال: 30 برای ارسال هر نیم ساعت یک پست)",
+ parse_mode="html",
+ buttons=get_cancel_button()
+ )
+ await event.answer()
+
+ elif data.startswith("st_slp:"):
+ target_id = int(data.split(":")[1])
+ target = await self.repo.get_target_by_id(target_id)
+ self.user_states[event.sender_id] = {"action": "wait_sleep", "target_id": target_id}
+ await event.reply(
+ f"🌙 تنظیم ساعت خواب کانال «{target.title}»:\n\n"
+ "ساعت شروع و پایان خواب را با یک فاصله بفرستید:\n"
+ "(مثال: 23 8 برای عدم ارسال پست از ساعت ۲۳:۰۰ تا ۰۸:۰۰ صبح)",
+ parse_mode="html",
+ buttons=get_cancel_button()
+ )
+ await event.answer()
+
+ elif data.startswith("dis_slp:"):
+ target_id = int(data.split(":")[1])
+ await self.repo.update_target_schedule(target_id=target_id, is_sleep_enabled=False)
+ card, buttons = await self._render_target_config(target_id)
+ await event.edit(card, parse_mode="html", buttons=buttons)
+ await event.answer("☀️ ساعت خواب غیرفعال شد.")
+
+ elif data.startswith("del_trg:"):
+ target_id = int(data.split(":")[1])
+ await self.repo.delete_target(target_id)
+ await event.edit("🗑 کانال مقصد با موفقیت حذف شد.", parse_mode="html", buttons=None)
+ await event.answer("کانال مقصد حذف شد.")
+
+ elif data.startswith("del_src:"):
+ source_id = int(data.split(":")[1])
+ await self.repo.delete_source(source_id)
+ await event.edit("🗑 کانال مبدا با موفقیت حذف شد.", parse_mode="html", buttons=None)
+ await event.answer("کانال مبدا حذف شد.")
+
+ elif data == "list_trg":
+ targets = await self.repo.get_active_targets()
+ if not targets:
+ await event.edit("هیچ کانال مقصدی ثبت نشده است.")
+ return
+ await event.edit("🎯 لیست کانالهای مقصد به روز شد:")
+ for t in targets:
+ card, buttons = await self._render_target_config(t.id)
+ await event.reply(card, parse_mode="html", buttons=buttons)
+
+ # --- Review Channel Buttons ---
+ elif data.startswith("hist:"):
_, ch_id_str, limit_str = data.split(":")
ch_id = int(ch_id_str)
limit = int(limit_str)
@@ -535,7 +641,6 @@ class AdminBotService:
await self.collector.scrape_channel_history(channel_id=ch_id, limit=limit, progress_callback=progress_notify)
await event.answer("فرآیند دریافت آغاز شد.")
- # 2. Target Selected -> Trigger On-Demand AI Rewrite for that Target
elif data.startswith("sel_trg:"):
_, post_id_str, target_id_str = data.split(":")
post_id = int(post_id_str)
@@ -579,10 +684,8 @@ class AdminBotService:
Button.inline("🔙 انصراف / بازگشت به پست اصلی", data=f"cancel:{post_id}")
]
]
-
await event.edit(preview_caption, parse_mode="html", buttons=preview_buttons)
- # 3. Add to Target's Redis Queue
elif data.startswith("pub:"):
_, post_id_str, target_id_str = data.split(":")
post_id = int(post_id_str)
@@ -597,7 +700,6 @@ class AdminBotService:
cache_key = f"{post_id}:{target_id}"
text_to_publish = self.preview_cache.get(cache_key) or post.raw_text or ""
- # Push to Target's Redis Queue
payload = {
"post_id": post.id,
"text": text_to_publish,
@@ -609,10 +711,8 @@ class AdminBotService:
await self.queue.push_target_post(target.id, payload)
ADMIN_ACTIONS_TOTAL.labels(action="approved").inc()
- # Update database record
await self.repo.record_post_published_to_target(post_id, target.id, target.title or "Target")
- # Reload updated post with publication history
updated_post = await self.repo.get_post_by_id(post_id)
targets = await self.repo.get_active_targets()
new_caption = self._format_raw_post_caption(updated_post)
@@ -625,7 +725,6 @@ class AdminBotService:
)
await event.answer(f"به صف {target.title} افزوده شد!")
- # 4. Cancel Preview & Restore Original Card
elif data.startswith("cancel:"):
_, post_id_str = data.split(":")
post_id = int(post_id_str)
@@ -641,7 +740,6 @@ class AdminBotService:
await event.edit(caption, parse_mode="html", buttons=buttons)
await event.answer("پیشنمایش لغو شد.")
- # 5. Reject Post
elif data.startswith("rej:"):
_, post_id_str = data.split(":")
post_id = int(post_id_str)
@@ -656,7 +754,6 @@ class AdminBotService:
)
await event.answer("پست بایگانی شد.")
- # 6. Soft Delete & Remove Message from Admin Channel
elif data.startswith("del_msg:"):
_, post_id_str = data.split(":")
post_id = int(post_id_str)