feat(workflow): implement target channel personalities, raw review cards, on-demand AI rewrites and multi-target dispatch
This commit is contained in:
+491
-330
@@ -1,29 +1,30 @@
|
||||
import os
|
||||
import logging
|
||||
from typing import Optional, List
|
||||
from datetime import datetime, timezone
|
||||
from typing import Optional, List, Dict
|
||||
from telethon import TelegramClient, events, Button
|
||||
from db.models import Post, TargetChannel
|
||||
from db.models import Post, TargetChannel, SourceChannel
|
||||
from db.repository import Repository
|
||||
from bot.keyboards import get_review_keyboard
|
||||
from core.metrics import ADMIN_ACTIONS_TOTAL
|
||||
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_main_menu_keyboard():
|
||||
def get_persian_main_menu():
|
||||
return [
|
||||
[Button.text("🔑 Request Login Code", resize=True), Button.text("📊 Fleet Statistics", resize=True)],
|
||||
[Button.text("📡 Monitored Sources", resize=True), Button.text("🎯 Target Channels", resize=True)],
|
||||
[Button.text("➕ Add Source Guide", resize=True), Button.text("➕ Add Target Guide", resize=True)],
|
||||
[Button.text("❓ Help & Documentation", 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,
|
||||
bot_token: Optional[str] = None,
|
||||
api_id: Optional[int] = None,
|
||||
api_hash: Optional[str] = None,
|
||||
@@ -32,6 +33,7 @@ class AdminBotService:
|
||||
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", "")
|
||||
@@ -42,15 +44,20 @@ class AdminBotService:
|
||||
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 message to review channel and all admin DMs."""
|
||||
"""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")
|
||||
@@ -59,340 +66,65 @@ class AdminBotService:
|
||||
|
||||
for admin_id in self.admin_user_ids:
|
||||
try:
|
||||
await self.client.send_message(admin_id, text, parse_mode="html", buttons=get_main_menu_keyboard())
|
||||
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 Review Bot...")
|
||||
logger.info("Starting Admin Bot Service...")
|
||||
await self.client.start(bot_token=self.bot_token)
|
||||
logger.info("Admin Review Bot connected successfully.")
|
||||
logger.info("Admin Bot connected successfully.")
|
||||
self._register_handlers()
|
||||
|
||||
def _register_handlers(self):
|
||||
# --- /start and Main Menu ---
|
||||
@self.client.on(events.NewMessage(pattern=r"(?i)^(/start|/menu|menu)$"))
|
||||
async def cmd_start(event: events.NewMessage.Event):
|
||||
if not self.is_admin(event.sender_id):
|
||||
await event.reply(f"⛔ Unauthorized user ID: <code>{event.sender_id}</code>. Please add this ID to <code>ADMIN_USER_IDS</code> in .env.", parse_mode="html")
|
||||
return
|
||||
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"]))
|
||||
|
||||
userbot_status = "🔴 Not Authorized"
|
||||
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"🟢 Online ({me.first_name})"
|
||||
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)
|
||||
|
||||
welcome_text = (
|
||||
"👋 <b>Welcome to Copykar Admin Console!</b>\n\n"
|
||||
f"• 🤖 <b>Userbot Status:</b> {userbot_status}\n"
|
||||
f"• 📋 <b>Review Channel:</b> <code>{self.review_channel_id}</code>\n\n"
|
||||
"Use the interactive menu buttons below to manage the fleet:"
|
||||
)
|
||||
await event.reply(welcome_text, parse_mode="html", buttons=get_main_menu_keyboard())
|
||||
buttons.append([Button.inline("❌ رد و بایگانی پست", data=f"rej:{post.id}")])
|
||||
return buttons
|
||||
|
||||
# --- Interactive Userbot Authentication Commands ---
|
||||
@self.client.on(events.NewMessage(pattern=r"(?i)^(/request_code|🔑 Request Login 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("Collector service not linked.")
|
||||
return
|
||||
msg = await event.reply("⏳ Contacting Telegram to request login code...")
|
||||
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>Code Sent!</b>\n\n"
|
||||
f"Telegram sent a verification code to <code>{self.collector.phone}</code>.\n\n"
|
||||
f"Please reply with:\n"
|
||||
f"<code>/code <your_code></code>\n\n"
|
||||
f"<i>Example:</i> <code>/code 12345</code>",
|
||||
parse_mode="html"
|
||||
)
|
||||
except Exception as e:
|
||||
await msg.edit(f"❌ Could not request code: {e}")
|
||||
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"
|
||||
|
||||
@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:
|
||||
await event.reply("Collector service not linked.")
|
||||
return
|
||||
code = event.pattern_match.group(1).strip()
|
||||
status_msg = await event.reply("⏳ Submitting code to Telegram...")
|
||||
result = await self.collector.submit_code(code)
|
||||
await status_msg.edit(result, parse_mode="html", buttons=get_main_menu_keyboard())
|
||||
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
|
||||
|
||||
@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:
|
||||
await event.reply("Collector service not linked.")
|
||||
return
|
||||
pwd = event.pattern_match.group(1).strip()
|
||||
status_msg = await event.reply("⏳ Verifying 2FA password...")
|
||||
result = await self.collector.submit_password(pwd)
|
||||
await status_msg.edit(result, parse_mode="html", buttons=get_main_menu_keyboard())
|
||||
|
||||
# --- Direct History Scraper Command ---
|
||||
@self.client.on(events.NewMessage(pattern=r"^/scrape_history\s+(-?\d+)(?:\s+(\d+))?"))
|
||||
async def cmd_scrape_history(event: events.NewMessage.Event):
|
||||
if not self.is_admin(event.sender_id):
|
||||
return
|
||||
if not self.collector:
|
||||
await event.reply("Collector service not linked.")
|
||||
return
|
||||
ch_id = int(event.pattern_match.group(1))
|
||||
limit = int(event.pattern_match.group(2)) if event.pattern_match.group(2) else 20
|
||||
|
||||
status_msg = await event.reply(f"⏳ Scraping the last <b>{limit}</b> posts from <code>{ch_id}</code> in the background...", parse_mode="html")
|
||||
|
||||
async def progress_notify(txt: str):
|
||||
await status_msg.edit(txt, parse_mode="html", buttons=get_main_menu_keyboard())
|
||||
|
||||
await self.collector.scrape_channel_history(channel_id=ch_id, limit=limit, progress_callback=progress_notify)
|
||||
|
||||
# --- Statistics ---
|
||||
@self.client.on(events.NewMessage(pattern=r"(?i)^(/stats|📊 Fleet Statistics)$"))
|
||||
async def cmd_stats(event: events.NewMessage.Event):
|
||||
if not self.is_admin(event.sender_id):
|
||||
return
|
||||
pending_ai = len(await self.repo.get_posts_by_status("pending_ai", limit=1000))
|
||||
pending_review = len(await self.repo.get_posts_by_status("pending_review", limit=1000))
|
||||
approved = len(await self.repo.get_posts_by_status("approved", limit=1000))
|
||||
published = len(await self.repo.get_posts_by_status("published", limit=1000))
|
||||
rejected = len(await self.repo.get_posts_by_status("rejected", limit=1000))
|
||||
redis_q = await self.collector.queue.qsize() if (self.collector and self.collector.queue) else 0
|
||||
|
||||
text = (
|
||||
"📊 <b>Copykar Fleet Metrics</b>\n\n"
|
||||
f"• 📥 <b>Redis Incoming Queue:</b> {redis_q} (Pacing: 1 post / 2m)\n"
|
||||
f"• ⏳ <b>Pending AI:</b> {pending_ai}\n"
|
||||
f"• 📋 <b>Pending Review:</b> {pending_review}\n"
|
||||
f"• 🚀 <b>Approved (In Queue):</b> {approved}\n"
|
||||
f"• ✅ <b>Published:</b> {published}\n"
|
||||
f"• ❌ <b>Rejected:</b> {rejected}\n\n"
|
||||
"📈 <i>Grafana Dashboard:</i> http://localhost:3000"
|
||||
)
|
||||
await event.reply(text, parse_mode="html", buttons=get_main_menu_keyboard())
|
||||
|
||||
# --- Sources Management with Interactive Scrape Buttons ---
|
||||
@self.client.on(events.NewMessage(pattern=r"(?i)^(/sources|📡 Monitored 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("No active source channels configured.\nTap <b>➕ Add Source Guide</b> below to add one.", parse_mode="html", buttons=get_main_menu_keyboard())
|
||||
return
|
||||
|
||||
await event.reply(f"📡 <b>Monitored Sources ({len(sources)} Active):</b>\nTap any button below to scrape past posts:", parse_mode="html")
|
||||
|
||||
for s in sources:
|
||||
card = (
|
||||
f"📢 <b>{s.title or 'Channel'}</b>\n"
|
||||
f"• ID: <code>{s.channel_id}</code>\n"
|
||||
f"• Username: @{s.username or 'none'}"
|
||||
)
|
||||
buttons = [
|
||||
[
|
||||
Button.inline(f"📥 Scrape 20 Posts", data=f"hist:{s.channel_id}:20"),
|
||||
Button.inline(f"📥 Scrape 50 Posts", data=f"hist:{s.channel_id}:50"),
|
||||
]
|
||||
]
|
||||
await event.reply(card, parse_mode="html", buttons=buttons)
|
||||
|
||||
@self.client.on(events.NewMessage(pattern=r"(?i)^(➕ Add Source Guide)$"))
|
||||
async def cmd_add_source_guide(event: events.NewMessage.Event):
|
||||
if not self.is_admin(event.sender_id):
|
||||
return
|
||||
guide = (
|
||||
"➕ <b>How to Add a Source Channel:</b>\n\n"
|
||||
"Send the command in this format:\n"
|
||||
"<code>/add_source <channel_id> <title> [username]</code>\n\n"
|
||||
"<i>Example:</i>\n"
|
||||
"<code>/add_source -1001234567890 TechNews technews_chan</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(f"📥 Scrape 20 Posts Now", data=f"hist:{ch_id}:20"),
|
||||
Button.inline(f"📥 Scrape 50 Posts Now", data=f"hist:{ch_id}:50")
|
||||
]
|
||||
]
|
||||
await event.reply(
|
||||
f"✅ Added source channel <b>{title}</b> (<code>{ch_id}</code>).\n\nWould you like to scrape past posts now?",
|
||||
parse_mode="html",
|
||||
buttons=buttons
|
||||
)
|
||||
|
||||
# --- Targets Management ---
|
||||
@self.client.on(events.NewMessage(pattern=r"(?i)^(/targets|🎯 Target Channels)$"))
|
||||
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("No target channels configured.\nUse <code>➕ Add Target Guide</code> to add one.", parse_mode="html")
|
||||
return
|
||||
lines = ["<b>🎯 Target Publishing Channels:</b>\n"]
|
||||
for t in targets:
|
||||
lines.append(f"• ID: <code>{t.id}</code> (Channel: <code>{t.channel_id}</code>)\n Title: <b>{t.title}</b> | Interval: <b>{t.post_interval_min}m</b>")
|
||||
await event.reply("\n".join(lines), parse_mode="html", buttons=get_main_menu_keyboard())
|
||||
|
||||
@self.client.on(events.NewMessage(pattern=r"(?i)^(➕ Add Target Guide)$"))
|
||||
async def cmd_add_target_guide(event: events.NewMessage.Event):
|
||||
if not self.is_admin(event.sender_id):
|
||||
return
|
||||
guide = (
|
||||
"🎯 <b>How to Add a Target Channel:</b>\n\n"
|
||||
"Send the command in this format:\n"
|
||||
"<code>/add_target <channel_id> <title> <interval_minutes> [username]</code>\n\n"
|
||||
"<i>Example (posts every 30 minutes):</i>\n"
|
||||
"<code>/add_target -1009876543210 MyMainChannel 30 my_main_chan</code>"
|
||||
)
|
||||
await event.reply(guide, parse_mode="html")
|
||||
|
||||
@self.client.on(events.NewMessage(pattern=r"^/add_target\s+(-?\d+)\s+([^\s]+)\s+(\d+)(?:\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)
|
||||
interval_min = int(event.pattern_match.group(3))
|
||||
username = event.pattern_match.group(4)
|
||||
await self.repo.add_target(channel_id=ch_id, title=title, username=username, post_interval_min=interval_min)
|
||||
await event.reply(f"✅ Added target channel <b>{title}</b> with interval <b>{interval_min}m</b>.", parse_mode="html", buttons=get_main_menu_keyboard())
|
||||
|
||||
@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))
|
||||
new_interval = int(event.pattern_match.group(2))
|
||||
target = await self.repo.get_target_by_id(target_id)
|
||||
if not target:
|
||||
await event.reply("Target channel not found.")
|
||||
return
|
||||
await self.repo.add_target(
|
||||
channel_id=target.channel_id,
|
||||
title=target.title,
|
||||
username=target.username,
|
||||
post_interval_min=new_interval
|
||||
)
|
||||
await event.reply(f"✅ Updated interval for <b>{target.title}</b> to <b>{new_interval} minutes</b>.", parse_mode="html", buttons=get_main_menu_keyboard())
|
||||
|
||||
@self.client.on(events.NewMessage(pattern=r"(?i)^(/help|❓ Help & Documentation)$"))
|
||||
async def cmd_help(event: events.NewMessage.Event):
|
||||
if not self.is_admin(event.sender_id):
|
||||
return
|
||||
help_text = (
|
||||
"📖 <b>Copykar Bot Quick Help</b>\n\n"
|
||||
"1. <b>Monitored Sources:</b> Tap <code>📡 Monitored Sources</code> to view channels and click <code>[📥 Scrape Posts]</code> on any channel.\n"
|
||||
"2. <b>Review Flow:</b> AI scans posts, checks duplicates, and sends drafts to the review channel with inline approval buttons.\n"
|
||||
"3. <b>Publishing:</b> Approved posts are published to your target channels strictly according to their interval minutes."
|
||||
)
|
||||
await event.reply(help_text, parse_mode="html", buttons=get_main_menu_keyboard())
|
||||
|
||||
# --- 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("⛔ You are not authorized.", alert=True)
|
||||
return
|
||||
|
||||
data = event.data.decode("utf-8")
|
||||
|
||||
# 1. Historical Scraping Callbacks
|
||||
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("Collector service not linked.", alert=True)
|
||||
return
|
||||
|
||||
await event.edit(f"⏳ <b>Scraping the last {limit} posts</b> from <code>{ch_id}</code>...", parse_mode="html", buttons=None)
|
||||
|
||||
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(f"Started scraping {limit} posts!")
|
||||
|
||||
# 2. Approval Callbacks
|
||||
elif data.startswith("appr:"):
|
||||
_, post_id_str, target_id_str = data.split(":")
|
||||
post_id = int(post_id_str)
|
||||
target_id = int(target_id_str)
|
||||
|
||||
target = await self.repo.get_target_by_id(target_id)
|
||||
target_title = target.title if target else f"Target #{target_id}"
|
||||
|
||||
await self.repo.approve_post(post_id, target_id)
|
||||
ADMIN_ACTIONS_TOTAL.labels(action="approved").inc()
|
||||
|
||||
await event.edit(
|
||||
f"{event.text}\n\n✅ <b>Approved for {target_title}</b> by admin.",
|
||||
parse_mode="html",
|
||||
buttons=None
|
||||
)
|
||||
await event.answer(f"Approved for {target_title}!")
|
||||
|
||||
# 3. Reject Callbacks
|
||||
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>Rejected</b> by admin.",
|
||||
parse_mode="html",
|
||||
buttons=None
|
||||
)
|
||||
await event.answer("Post rejected.")
|
||||
|
||||
async def send_review_post(self, post_id: int):
|
||||
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 = get_review_keyboard(post.id, targets)
|
||||
|
||||
tags_str = ", ".join(post.tags) if post.tags else "None"
|
||||
dup_warning = ""
|
||||
if post.is_duplicate:
|
||||
dup_warning = (
|
||||
f"⚠️ <b>[DUPLICATE DETECTED]</b>\n"
|
||||
f"<b>Reason:</b> {post.similarity_reason or 'Similar story already published'}\n"
|
||||
f"<b>Matched Post ID:</b> #{post.duplicate_of_id}\n\n"
|
||||
)
|
||||
|
||||
caption = (
|
||||
f"📌 <b>Subject:</b> {post.subject or 'N/A'}\n"
|
||||
f"🏷 <b>Tags:</b> <code>{tags_str}</code>\n\n"
|
||||
f"{dup_warning}"
|
||||
f"📝 <b>Generated Post Draft:</b>\n"
|
||||
f"{post.ai_text or post.raw_text}\n\n"
|
||||
f"<i>Source: Channel <code>{post.source_channel_id}</code> | Msg #{post.source_message_id}</i>"
|
||||
)
|
||||
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):
|
||||
@@ -410,12 +142,441 @@ class AdminBotService:
|
||||
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 review post {post.id} to review channel: {e}", exc_info=True)
|
||||
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 Review Bot disconnected.")
|
||||
logger.info("Admin Bot disconnected.")
|
||||
|
||||
Reference in New Issue
Block a user