Files
copykar/services/admin_bot.py
T

774 lines
41 KiB
Python

import os
import re
import logging
from datetime import datetime, timezone
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
from core.metrics import ADMIN_ACTIONS_TOTAL, TARGET_ACTIVITY_TOTAL
from core.proxy import get_telegram_proxy
logger = logging.getLogger(__name__)
SESSION_DIR = os.getenv("SESSION_DIR", "/app/sessions" if os.path.exists("/app") else "/projects/telegram-bots/copykar/sessions")
def get_persian_main_menu():
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("❓ راهنمای سیستم", resize=True)]
]
def get_cancel_button():
return [[Button.inline("❌ انصراف و بازگشت به منو", data="cancel_state")]]
class AdminBotService:
def __init__(
self,
repo: Repository,
ai_processor = None,
queue: Optional[RedisQueue] = None,
bot_token: Optional[str] = None,
api_id: Optional[int] = None,
api_hash: Optional[str] = None,
review_channel_id: Optional[int] = None,
admin_user_ids: Optional[List[int]] = None,
session_name: Optional[str] = None,
):
self.repo = repo
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_hash = api_hash or os.getenv("API_HASH", "")
self.review_channel_id = review_channel_id or int(os.getenv("REVIEW_CHANNEL_ID", "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())
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
def set_ai_processor(self, ai_processor):
self.ai_processor = ai_processor
def set_queue(self, queue: RedisQueue):
self.queue = queue
def is_admin(self, user_id: int) -> bool:
return not self.admin_user_ids or user_id in self.admin_user_ids
async def notify_admins(self, text: str):
"""Broadcast Persian message to review channel and admin DMs."""
if self.review_channel_id:
try:
await self.client.send_message(self.review_channel_id, text, parse_mode="html")
except Exception as e:
logger.error(f"Failed to notify review channel: {e}")
for admin_id in self.admin_user_ids:
try:
await self.client.send_message(admin_id, text, parse_mode="html", buttons=get_persian_main_menu())
except Exception as e:
logger.debug(f"Could not send DM to admin {admin_id}: {e}")
async def start(self):
logger.info("Starting Admin Bot Service...")
await self.client.start(bot_token=self.bot_token)
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"🎯 <b>تنظیمات کانال مقصد:</b> <b>{target.title}</b>\n\n"
f"• 🆔 <b>شناسه کانال:</b> <code>{target.channel_id}</code> (ID: <code>{target.id}</code>)\n"
f"• 🔗 <b>یوزرنیم:</b> @{target.username or 'ندارد'}\n"
f"• ⏱ <b>فاصله ارسال پست‌ها:</b> هر <b>{target.post_interval_min} دقیقه</b>\n"
f"• 🌙 <b>وضعیت ساعت خواب:</b> <b>{sleep_st}</b>\n"
f"• 📥 <b>پست‌های منتظر در صف:</b> <b>{qsize} پست</b>\n\n"
f"🎭 <b>شخصیت و لحن نگارش:</b>\n"
f"<i>{target.personality or 'پیش‌فرض (رسمی و روان)'}</i>\n\n"
f"🏷 <b>فوتر و هشتگ‌های اختصاصی:</b>\n"
f"<code>{target.custom_footer or 'ندارد'}</code>\n\n"
"<i>👇 برای ویرایش هر بخش روی دکمه مربوطه کلیک کنید:</i>"
)
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()
if post.published_to:
for item in post.published_to:
if isinstance(item, dict) and "target_id" in item:
sent_target_ids.add(int(item["target_id"]))
row = []
for t in targets:
is_sent = t.id in sent_target_ids
label = f"✅ {t.title}" if is_sent else f"🎯 {t.title}"
row.append(Button.inline(label, data=f"sel_trg:{post.id}:{t.id}"))
if len(row) == 2:
buttons.append(row)
row = []
if row:
buttons.append(row)
buttons.append([
Button.inline("❌ رد و بایگانی", data=f"rej:{post.id}"),
Button.inline("🗑 حذف پیام از کانال", data=f"del_msg:{post.id}")
])
return buttons
def _format_raw_post_caption(self, post: Post) -> str:
published_lines = ""
if post.published_to:
published_lines = "📤 <b>ارسال شده / در صف ارسال کانال‌های:</b>\n"
for item in post.published_to:
if isinstance(item, dict):
t_title = item.get("target_title", "کانال مقصد")
published_lines += f" • <b>{t_title}</b>\n"
published_lines += "➖➖➖➖➖➖➖➖➖➖\n\n"
caption = (
f"📥 <b>پست جدید از مبدا (<code>{post.source_channel_id}</code>):</b>\n\n"
f"{published_lines}"
f"{post.raw_text or ''}\n\n"
f"<i>👇 کانال مقصد مورد نظر را برای بازنویسی هوشمند انتخاب کنید:</i>"
)
return caption
async def send_raw_review_post(self, post_id: int):
post = await self.repo.get_post_by_id(post_id)
if not post or not self.review_channel_id or post.is_deleted:
return
targets = await self.repo.get_active_targets()
keyboard = self._build_raw_post_keyboard(post, targets)
caption = self._format_raw_post_caption(post)
try:
if post.media_path and os.path.exists(post.media_path):
msg = await self.client.send_file(
self.review_channel_id,
file=post.media_path,
caption=caption,
parse_mode="html",
buttons=keyboard
)
else:
msg = await self.client.send_message(
self.review_channel_id,
caption,
parse_mode="html",
buttons=keyboard
)
await self.repo.update_review_message_id(post.id, msg.id)
except Exception as e:
logger.error(f"Failed to send raw post {post.id} to review channel: {e}", exc_info=True)
def _register_handlers(self):
# --- Start / Menu ---
@self.client.on(events.NewMessage(pattern=r"(?i)^(/start|/menu|منو)$"))
async def cmd_start(event: events.NewMessage.Event):
if not self.is_admin(event.sender_id):
await event.reply(f"⛔ دسترسی غیرمجاز. شناسه عددی شما: <code>{event.sender_id}</code>", 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()
userbot_status = f"🟢 آنلاین ({me.first_name})"
welcome_text = (
"👋 <b>به پنل مدیریت سیستم هوشمند کپی‌کار خوش آمدید!</b>\n\n"
f"• 🤖 <b>وضعیت ربات جمع‌آوری‌کننده:</b> {userbot_status}\n"
f"• 📋 <b>شناسه کانال ادمین‌ها:</b> <code>{self.review_channel_id}</code>\n\n"
"برای مدیریت کانال‌ها از دکمه‌های زیر استفاده کنید:"
)
await event.reply(welcome_text, parse_mode="html", buttons=get_persian_main_menu())
# --- Statistics ---
@self.client.on(events.NewMessage(pattern=r"(?i)^(/stats|📊 آمار و وضعیت ناوگان)$"))
async def cmd_stats(event: events.NewMessage.Event):
if not self.is_admin(event.sender_id):
return
pending_review = len(await self.repo.get_posts_by_status("pending_review", limit=5000))
published = len(await self.repo.get_posts_by_status("published", limit=5000))
rejected = len(await self.repo.get_posts_by_status("rejected", limit=5000))
total_redis_q = await self.queue.get_total_queued_posts() if self.queue else 0
text = (
"📊 <b>آمار زنده سیستم کپی‌کار:</b>\n\n"
f"• 📥 <b>مجموع پست‌های در صف ارسال کانال‌های مقصد:</b> <b>{total_redis_q}</b>\n"
f"• 📋 <b>پست‌های در انتظار بررسی ادمین:</b> <b>{pending_review}</b>\n"
f"• 🚀 <b>پست‌های منتشر شده:</b> <b>{published}</b>\n"
f"• ❌ <b>پست‌های رد شده:</b> <b>{rejected}</b>\n\n"
"📈 <i>داشبورد مانیتورینگ گرانافا:</i> http://localhost:3000"
)
await event.reply(text, parse_mode="html", buttons=get_persian_main_menu())
# --- Userbot Authentication ---
@self.client.on(events.NewMessage(pattern=r"(?i)^(/request_code|🔑 درخواست کد لاگین)$"))
async def cmd_request_code(event: events.NewMessage.Event):
if not self.is_admin(event.sender_id):
return
if not self.collector:
await event.reply("سرویس کالکتور متصل نیست.")
return
msg = await event.reply("⏳ در حال ارسال درخواست کد لاگین به تلگرام...")
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"📩 <b>کد تایید ارسال شد!</b>\n\n"
f"لطفا کد دریافت شده به شماره <code>{self.collector.phone}</code> را در پیام بعدی ارسال کنید:\n"
f"<i>(یا به فرمت /code 12345)</i>",
parse_mode="html",
buttons=get_cancel_button()
)
except Exception as e:
await msg.edit(f"❌ خطا در درخواست کد: {e}")
# --- Sources Management ---
@self.client.on(events.NewMessage(pattern=r"(?i)^(/sources|📡 کانال‌های مبدا)$"))
async def cmd_sources(event: events.NewMessage.Event):
if not self.is_admin(event.sender_id):
return
sources = await self.repo.get_active_sources()
if not sources:
await event.reply("هیچ کانال مبدایی ثبت نشده است. روی <b>➕ افزودن کانال مبدا</b> بزنید.", parse_mode="html")
return
await event.reply(f"📡 <b>کانال‌های مبدا فعال ({len(sources)} کانال):</b>", parse_mode="html")
for s in sources:
card = (
f"📢 <b>{s.title or 'کانال مبدا'}</b>\n"
f"• شناسه: <code>{s.channel_id}</code>\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"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_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 = (
"📡 <b>افزودن کانال مبدا جدید:</b>\n\n"
"ساده‌ترین روش: <b>یک پیام از کانال مورد نظر به این ربات فوروارد (Forward) کنید!</b>\n\n"
"<i>(یا می‌توانید آیدی، یوزرنیم یا لینک کانال مثل @channel_username را بفرستید)</i>"
)
await event.reply(guide, parse_mode="html", buttons=get_cancel_button())
# --- Targets Management ---
@self.client.on(events.NewMessage(pattern=r"(?i)^(/targets|🎯 کانال‌های مقصد)$"))
async def cmd_targets(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("هیچ کانال مقصدی ثبت نشده است. روی <b>➕ افزودن کانال مقصد</b> بزنید.", parse_mode="html")
return
await event.reply(f"🎯 <b>کانال‌های مقصد برای انتشار ({len(targets)} کانال):</b>\nبرای تنظیمات هر کانال، روی دکمه آن بزنید:", parse_mode="html")
for t in targets:
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_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 = (
"🎯 <b>افزودن کانال مقصد جدید:</b>\n\n"
"ساده‌ترین روش: <b>یک پیام از کانال مورد نظر به این ربات فوروارد (Forward) کنید!</b>\n\n"
"<i>(یا می‌توانید آیدی، یوزرنیم یا لینک کانال مثل @channel_username را بفرستید)</i>"
)
await event.reply(guide, parse_mode="html", buttons=get_cancel_button())
# --- Help ---
@self.client.on(events.NewMessage(pattern=r"(?i)^(/help|❓ راهنمای سیستم)$"))
async def cmd_help(event: events.NewMessage.Event):
if not self.is_admin(event.sender_id):
return
help_text = (
"📖 <b>راهنمای سیستم کپی‌کار:</b>\n\n"
"• <b>افزودن آسان کانال‌ها:</b> فقط کافیست یک پیام از کانال را به ربات فوروارد کنید تا شناسه و نام آن خودکار ثبت شود!\n"
"• <b>تنظیمات با دکمه:</b> در بخش کانال‌های مقصد، روی هر کانال دکمه‌های تغییر لحن، تگ‌ها، فاصله زمانی و ساعت خواب وجود دارد.\n"
"• <b>بازنویسی هوشمند:</b> پست‌های مبدا به کانال ادمین می‌آیند و با لمس دکمه هر مقصد، متن با هوش مصنوعی و لحن همان کانال بازنویسی می‌شود."
)
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"✅ کانال مبدا <b>{title}</b> با شناسه <code>{ch_id}</code> با موفقیت افزوده شد!\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"✅ کانال مقصد <b>{title}</b> افزوده شد!\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("✅ <b>لحن و شخصیت کانال با موفقیت به روز شد!</b>", 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("✅ <b>فوتر و تگ‌های اختصاصی کانال با موفقیت به روز شد!</b>", 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"✅ <b>فاصله ارسال به هر {interval_min} دقیقه تغییر یافت!</b>", 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("⚠️ لطفا ساعت شروع و پایان را به این شکل بفرستید: <code>23 8</code> (برای ۲۳:۰۰ تا ۰۸:۰۰)", 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"🌙 <b>ساعت خواب از {start_h}:00 تا {end_h}:00 فعال شد!</b>", 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):
if not self.is_admin(event.sender_id):
await event.answer("⛔ دسترسی غیرمجاز.", alert=True)
return
data = event.data.decode("utf-8")
# 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"🎭 <b>تغییر لحن و شخصیت کانال «{target.title}»:</b>\n\n"
"لطفا در پیام بعدی، لحن و استایل نگارش مورد نظرتان را بفرستید:\n"
"<i>(مثال: لحن دوستانه و پرانرژی همراه با ایموجی و تیترهای جذاب)</i>",
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"🏷 <b>تغییر فوتر و تگ‌های اختصاصی «{target.title}»:</b>\n\n"
"لطفا متن فوتر، آیدی یا هشتگ‌هایی که می‌خواهید انتهای هر پست قرار بگیرد را بفرستید:\n"
"<i>(مثال: 🆔 @my_chan\n#فناوری #اخبار)</i>",
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"⏱ <b>تغییر فاصله ارسال کانال «{target.title}»:</b>\n\n"
"فاصله زمانی بین ارسال هر پست را به <b>دقیقه</b> بفرستید:\n"
"<i>(مثال: 30 برای ارسال هر نیم ساعت یک پست)</i>",
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"🌙 <b>تنظیم ساعت خواب کانال «{target.title}»:</b>\n\n"
"ساعت شروع و پایان خواب را با یک فاصله بفرستید:\n"
"<i>(مثال: <code>23 8</code> برای عدم ارسال پست از ساعت ۲۳:۰۰ تا ۰۸:۰۰ صبح)</i>",
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("🗑 <b>کانال مقصد با موفقیت حذف شد.</b>", 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("🗑 <b>کانال مبدا با موفقیت حذف شد.</b>", 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("🎯 <b>لیست کانال‌های مقصد به روز شد:</b>")
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)
if not self.collector:
await event.answer("کالکتور در دسترس نیست.", alert=True)
return
await event.edit(f"⏳ <b>در حال دریافت {limit} پست گذشته</b> از کانال <code>{ch_id}</code>...", parse_mode="html")
async def progress_notify(txt: str):
await event.edit(txt, parse_mode="html")
await self.collector.scrape_channel_history(channel_id=ch_id, limit=limit, progress_callback=progress_notify)
await event.answer("فرآیند دریافت آغاز شد.")
elif data.startswith("sel_trg:"):
_, post_id_str, target_id_str = data.split(":")
post_id = int(post_id_str)
target_id = int(target_id_str)
post = await self.repo.get_post_by_id(post_id)
target = await self.repo.get_target_by_id(target_id)
if not post or not target:
await event.answer("پست یا کانال مقصد یافت نشد.", alert=True)
return
await event.answer(f"در حال بازنویسی برای {target.title}...")
loading_caption = (
f"🤖 <b>در حال بازنویسی هوشمند برای کانال:</b> <b>{target.title}</b>...\n"
f"<i>(اعمال لحن اختصاصی و حذف تگ‌های مبدا)</i>"
)
try:
await event.edit(loading_caption, parse_mode="html", buttons=None)
except Exception:
pass
rewritten_text = await self.ai_processor.rewrite_for_target(post.raw_text or "", target)
cache_key = f"{post_id}:{target_id}"
self.preview_cache[cache_key] = rewritten_text
preview_caption = (
f"🎯 <b>پیش‌نمایش بازنویسی شده برای: {target.title}</b>\n"
f"🎭 <b>شخصیت و لحن:</b> <i>{target.personality or 'پیش‌فرض'}</i>\n"
f"➖➖➖➖➖➖➖➖➖➖\n\n"
f"{rewritten_text}\n\n"
f"➖➖➖➖➖➖➖➖➖➖\n"
f"<i>آیا این متن برای صف انتشار تایید است؟</i>"
)
preview_buttons = [
[
Button.inline(f"✅ تایید و افزودن به صف {target.title}", data=f"pub:{post_id}:{target_id}"),
],
[
Button.inline("🔙 انصراف / بازگشت به پست اصلی", data=f"cancel:{post_id}")
]
]
await event.edit(preview_caption, parse_mode="html", buttons=preview_buttons)
elif data.startswith("pub:"):
_, post_id_str, target_id_str = data.split(":")
post_id = int(post_id_str)
target_id = int(target_id_str)
post = await self.repo.get_post_by_id(post_id)
target = await self.repo.get_target_by_id(target_id)
if not post or not target:
await event.answer("اطلاعات یافت نشد.", alert=True)
return
cache_key = f"{post_id}:{target_id}"
text_to_publish = self.preview_cache.get(cache_key) or post.raw_text or ""
payload = {
"post_id": post.id,
"text": text_to_publish,
"media_path": post.media_path,
"target_id": target.id,
"target_title": target.title
}
if self.queue:
await self.queue.push_target_post(target.id, payload)
ADMIN_ACTIONS_TOTAL.labels(action="approved").inc()
await self.repo.record_post_published_to_target(post_id, target.id, target.title or "Target")
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)
new_buttons = self._build_raw_post_keyboard(updated_post, targets)
await event.edit(
f"✅ <b>به صف انتشار کانال {target.title} اضافه شد!</b>\n\n{new_caption}",
parse_mode="html",
buttons=new_buttons
)
await event.answer(f"به صف {target.title} افزوده شد!")
elif data.startswith("cancel:"):
_, post_id_str = data.split(":")
post_id = int(post_id_str)
post = await self.repo.get_post_by_id(post_id)
if not post:
return
targets = await self.repo.get_active_targets()
caption = self._format_raw_post_caption(post)
buttons = self._build_raw_post_keyboard(post, targets)
await event.edit(caption, parse_mode="html", buttons=buttons)
await event.answer("پیش‌نمایش لغو شد.")
elif data.startswith("rej:"):
_, post_id_str = data.split(":")
post_id = int(post_id_str)
await self.repo.reject_post(post_id)
ADMIN_ACTIONS_TOTAL.labels(action="rejected").inc()
await event.edit(
f"{event.text}\n\n❌ <b>این پست توسط ادمین رد و بایگانی شد.</b>",
parse_mode="html",
buttons=None
)
await event.answer("پست بایگانی شد.")
elif data.startswith("del_msg:"):
_, post_id_str = data.split(":")
post_id = int(post_id_str)
await self.repo.soft_delete_post(post_id)
try:
await event.delete()
except Exception as e:
logger.error(f"Could not delete message from review channel: {e}")
await event.edit("🗑 <b>این پیام حذف و بایگانی شد.</b>", parse_mode="html", buttons=None)
await event.answer("🗑 پیام از کانال ادمین حذف شد.", alert=False)
async def stop(self):
if self.client.is_connected():
await self.client.disconnect()
logger.info("Admin Bot disconnected.")