import os
import logging
from datetime import datetime, timezone
from typing import Optional, List, Dict
from telethon import TelegramClient, events, Button
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), Button.text("⏰ زمانبندی و خواب کانالها", resize=True)],
[Button.text("❓ راهنمای سیستم", resize=True)]
]
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] = {}
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()
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 = "📤 ارسال شده / در صف ارسال کانالهای:\n"
for item in post.published_to:
if isinstance(item, dict):
t_title = item.get("target_title", "کانال مقصد")
published_lines += f" • {t_title}\n"
published_lines += "➖➖➖➖➖➖➖➖➖➖\n\n"
caption = (
f"📥 پست جدید از مبدا ({post.source_channel_id}):\n\n"
f"{published_lines}"
f"{post.raw_text or ''}\n\n"
f"👇 کانال مقصد مورد نظر را برای بازنویسی هوشمند انتخاب کنید:"
)
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"⛔ دسترسی غیرمجاز. شناسه عددی شما: {event.sender_id}", parse_mode="html")
return
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"
"از دکمههای زیر برای مدیریت کانالها، تنظیم شخصیت و زمانبندی استفاده کنید:"
)
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 = (
"📊 آمار زنده سیستم کپیکار:\n\n"
f"• 📥 مجموع پستهای در صف ارسال کانالهای مقصد: {total_redis_q}\n"
f"• 📋 پستهای در انتظار بررسی ادمین: {pending_review}\n"
f"• 🚀 پستهای منتشر شده: {published}\n"
f"• ❌ پستهای رد شده: {rejected}\n\n"
"📈 داشبورد مانیتورینگ گرانافا: 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
await msg.edit(
f"📩 کد تایید ارسال شد!\n\n"
f"کد ارسال شده به شماره {self.collector.phone} را به این صورت ارسال کنید:\n"
f"/code 12345",
parse_mode="html"
)
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):
if not self.is_admin(event.sender_id):
return
sources = await self.repo.get_active_sources()
if not sources:
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.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"),
]
]
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):
if not self.is_admin(event.sender_id):
return
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
)
# --- 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("هیچ کانال مقصدی ثبت نشده است. از دکمه ➕ افزودن کانال مقصد استفاده کنید.", parse_mode="html")
return
lines = ["🎯 کانالهای مقصد برای انتشار:\n"]
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())
@self.client.on(events.NewMessage(pattern=r"(?i)^(➕ افزودن کانال مقصد)$"))
async def cmd_add_target_guide(event: events.NewMessage.Event):
if not self.is_admin(event.sender_id):
return
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()
)
# --- 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 = (
"📖 راهنمای فرآیند کاری سیستم کپیکار:\n\n"
"1. 📥 دریافت آنی: پستهای مبدا فوری و بدون تاخیر در کانال ادمین قرار میگیرند.\n"
"2. 🎯 بازنویسی بر اساس مقصد: با زدن دکمه کانال مقصد، هوش مصنوعی پست را متناسب با شخصیت و فوتر آن کانال بازنویسی میکند.\n"
"3. ⏳ صف ارسال زمانبندی شده: پس از تایید، پست در صف ردیس کانال مقصد قرار میگیرد و با رعایت فاصله زمانی (Interval) و ساعات خواب (Sleep) به ترتیب منتشر میشود."
)
await event.reply(help_text, 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")
# 1. Historical Scraping Callback
if 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"⏳ در حال دریافت {limit} پست گذشته از کانال {ch_id}...", 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("فرآیند دریافت آغاز شد.")
# 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)
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"🤖 در حال بازنویسی هوشمند برای کانال: {target.title}...\n"
f"(اعمال لحن اختصاصی و حذف تگهای مبدا)"
)
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"🎯 پیشنمایش بازنویسی شده برای: {target.title}\n"
f"🎭 شخصیت و لحن: {target.personality or 'پیشفرض'}\n"
f"➖➖➖➖➖➖➖➖➖➖\n\n"
f"{rewritten_text}\n\n"
f"➖➖➖➖➖➖➖➖➖➖\n"
f"آیا این متن برای صف انتشار تایید است؟"
)
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)
# 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)
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 ""
# Push to Target's Redis Queue
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()
# 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)
new_buttons = self._build_raw_post_keyboard(updated_post, targets)
await event.edit(
f"✅ به صف انتشار کانال {target.title} اضافه شد!\n\n{new_caption}",
parse_mode="html",
buttons=new_buttons
)
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)
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("پیشنمایش لغو شد.")
# 5. Reject Post
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❌ این پست توسط ادمین رد و بایگانی شد.",
parse_mode="html",
buttons=None
)
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)
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("🗑 این پیام حذف و بایگانی شد.", 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.")