583 lines
30 KiB
Python
583 lines
30 KiB
Python
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.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)]
|
||
]
|
||
|
||
class AdminBotService:
|
||
def __init__(
|
||
self,
|
||
repo: Repository,
|
||
ai_processor = 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.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
|
||
# In-memory store for pending rewritten previews: {f"{post_id}:{target_id}": rewritten_text}
|
||
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 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 = []
|
||
# Check which targets this post has already been sent to
|
||
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}")])
|
||
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:
|
||
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
|
||
|
||
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))
|
||
redis_q = await self.collector.queue.qsize() if (self.collector and self.collector.queue) else 0
|
||
|
||
text = (
|
||
"📊 <b>آمار زنده سیستم کپیکار:</b>\n\n"
|
||
f"• 📥 <b>پستهای موجود در صف ردیس:</b> <b>{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
|
||
await msg.edit(
|
||
f"📩 <b>کد تایید ارسال شد!</b>\n\n"
|
||
f"کد ارسال شده به شماره <code>{self.collector.phone}</code> را به این صورت ارسال کنید:\n"
|
||
f"<code>/code 12345</code>",
|
||
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("هیچ کانال مبدایی ثبت نشده است. از دکمه <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"),
|
||
]
|
||
]
|
||
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 = (
|
||
"➕ <b>راهنمای افزودن کانال مبدا:</b>\n\n"
|
||
"دستور را با فرمت زیر ارسال کنید:\n"
|
||
"<code>/add_source <شناسه_عددی_کانال> <عنوان> [یوزرنیم]</code>\n\n"
|
||
"<i>مثال:</i>\n"
|
||
"<code>/add_source -1001234567890 اخبار_فوری fouri_news</code>"
|
||
)
|
||
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"✅ کانال مبدا <b>{title}</b> (<code>{ch_id}</code>) با موفقیت افزوده شد.\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("هیچ کانال مقصدی ثبت نشده است. از دکمه <b>➕ افزودن کانال مقصد</b> استفاده کنید.", parse_mode="html")
|
||
return
|
||
lines = ["<b>🎯 کانالهای مقصد برای انتشار:</b>\n"]
|
||
for t in targets:
|
||
lines.append(
|
||
f"• <b>{t.title}</b> (شناسه: <code>{t.channel_id}</code> | ID دیتابیس: <code>{t.id}</code>)\n"
|
||
f" 🎭 <b>شخصیت و لحن:</b> {t.personality or 'پیشفرض'}\n"
|
||
f" 🏷 <b>فوتر / تگها:</b> <code>{t.custom_footer or 'ندارد'}</code>\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 = (
|
||
"🎯 <b>راهنمای افزودن کانال مقصد:</b>\n\n"
|
||
"دستور را با فرمت زیر ارسال کنید:\n"
|
||
"<code>/add_target <شناسه_کانال> <عنوان> [یوزرنیم]</code>\n\n"
|
||
"<i>مثال:</i>\n"
|
||
"<code>/add_target -1009876543210 دنیای_هوش_مصنوعی ai_world_chan</code>"
|
||
)
|
||
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"✅ کانال مقصد <b>{title}</b> افزوده شد (ID دیتابیس: <code>{tid}</code>).\n\n"
|
||
f"اکنون میتوانید با دکمه <b>🎭 تنظیم شخصیت کانالها</b> لحن و تگهای آن را تنظیم کنید.",
|
||
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("ابتدا با استفاده از <b>➕ افزودن کانال مقصد</b> یک کانال مقصد اضافه کنید.", parse_mode="html")
|
||
return
|
||
|
||
text = (
|
||
"🎭 <b>تنظیم شخصیت، لحن و تگهای کانالهای مقصد:</b>\n\n"
|
||
"برای تغییر لحن و استایل نگارش کانال از دستور زیر استفاده کنید:\n"
|
||
"<code>/set_personality <شناسه_دیتابیس_کانال> <توضیحات لحن></code>\n\n"
|
||
"<i>مثال:</i>\n"
|
||
"<code>/set_personality 1 لحن جذاب و ژورنالیستی، استفاده از تیترهای بولد و ایموجیهای مرتبط</code>\n\n"
|
||
"برای تنظیم فوتر و هشتگهای اختصاصی انتهای پست:\n"
|
||
"<code>/set_footer <شناسه_دیتابیس_کانال> <تگها یا آیدی کانال></code>\n\n"
|
||
"<i>مثال:</i>\n"
|
||
"<code>/set_footer 1 🆔 @my_tech_chan\n#تکنولوژی #هوش_مصنوعی</code>\n\n"
|
||
"<b>کانالهای موجود و شخصیت فعلی:</b>\n"
|
||
)
|
||
for t in targets:
|
||
text += (
|
||
f"• ID: <code>{t.id}</code> | <b>{t.title}</b>\n"
|
||
f" 🎭 لحن: <i>{t.personality or 'پیشفرض'}</i>\n"
|
||
f" 🏷 فوتر: <code>{t.custom_footer or 'ندارد'}</code>\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"✅ شخصیت و لحن کانال <b>{target.title}</b> با موفقیت به روز شد:\n\n<i>{personality}</i>",
|
||
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"✅ فوتر اختصاصی کانال <b>{target.title}</b> به روز شد:\n\n<code>{footer}</code>",
|
||
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 = (
|
||
"📖 <b>راهنمای فرآیند کاری سیستم کپیکار:</b>\n\n"
|
||
"1. 📥 <b>دریافت خام پستها:</b> پستها بدون پردازش هوش مصنوعی مستقیماً به کانال ادمینها ارسال میشوند.\n"
|
||
"2. 🎯 <b>انتخاب کانال مقصد:</b> با لمس دکمه هر کانال، هوش مصنوعی پست را متناسب با شخصیت، استایل و فوتر اختصاصی همان کانال بازنویسی کرده و تمام تگها و لینکهای مبدا را حذف میکند.\n"
|
||
"3. 👁 <b>پیشنمایش زنده:</b> پیشنمایش بازنویسی شده به همراه دکمه تایید نهایی نمایش داده میشود.\n"
|
||
"4. 🚀 <b>انتشار و ارسال مجدد:</b> پس از انتشار، پست اصلی در کانال ادمین بازگردانده شده و سابقه انتشار نمایش مییابد تا بتوانید آن را به سایر کانالها نیز ارسال کنید."
|
||
)
|
||
await event.reply(help_text, parse_mode="html", buttons=get_persian_main_menu())
|
||
|
||
# --- Interactive Inline Callbacks for Reviews & Target Rewrites ---
|
||
@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"⏳ <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("فرآیند دریافت آغاز شد.")
|
||
|
||
# 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}...")
|
||
|
||
# Show loading placeholder
|
||
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
|
||
|
||
# Run AI Rewrite
|
||
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
|
||
|
||
# Build Preview Card
|
||
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)
|
||
|
||
# 3. Publish to Target Confirmed
|
||
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 ""
|
||
|
||
await event.answer(f"در حال ارسال به {target.title}...")
|
||
|
||
try:
|
||
# Publish via userbot or bot
|
||
client_to_use = self.collector.client if (self.collector and self.collector.client.is_connected()) else self.client
|
||
if post.media_path and os.path.exists(post.media_path):
|
||
await client_to_use.send_file(
|
||
target.channel_id,
|
||
file=post.media_path,
|
||
caption=text_to_publish,
|
||
parse_mode="html"
|
||
)
|
||
else:
|
||
await client_to_use.send_message(
|
||
target.channel_id,
|
||
text_to_publish,
|
||
parse_mode="html"
|
||
)
|
||
|
||
# Record publication in database
|
||
await self.repo.record_post_published_to_target(post_id, target.id, target.title or "Target")
|
||
await self.repo.update_target_last_post(target.id)
|
||
ADMIN_ACTIONS_TOTAL.labels(action="approved").inc()
|
||
TARGET_ACTIVITY_TOTAL.labels(channel_id=str(target.channel_id), title=target.title or '').inc()
|
||
|
||
# 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"✅ <b>با موفقیت در {target.title} منتشر شد!</b>\n\n{new_caption}",
|
||
parse_mode="html",
|
||
buttons=new_buttons
|
||
)
|
||
except Exception as e:
|
||
logger.error(f"Failed to publish to {target.channel_id}: {e}", exc_info=True)
|
||
await event.answer(f"❌ خطا در ارسال به کانال: {e}", alert=True)
|
||
|
||
# 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❌ <b>این پست توسط ادمین رد و بایگانی شد.</b>",
|
||
parse_mode="html",
|
||
buttons=None
|
||
)
|
||
await event.answer("پست بایگانی شد.")
|
||
|
||
async def stop(self):
|
||
if self.client.is_connected():
|
||
await self.client.disconnect()
|
||
logger.info("Admin Bot disconnected.")
|