219 lines
9.9 KiB
Python
219 lines
9.9 KiB
Python
import os
|
|
import logging
|
|
from typing import Optional, List
|
|
from telethon import TelegramClient, events, Button
|
|
from db.models import Post, TargetChannel
|
|
from db.repository import Repository
|
|
from bot.keyboards import get_review_keyboard
|
|
from core.metrics import ADMIN_ACTIONS_TOTAL
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
class AdminBotService:
|
|
def __init__(
|
|
self,
|
|
repo: Repository,
|
|
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: str = "/projects/telegram-bots/copykar/sessions/admin_bot.session",
|
|
):
|
|
self.repo = repo
|
|
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
|
|
self.client = TelegramClient(self.session_name, self.api_id, self.api_hash)
|
|
|
|
def is_admin(self, user_id: int) -> bool:
|
|
return not self.admin_user_ids or user_id in self.admin_user_ids
|
|
|
|
async def start(self):
|
|
os.makedirs(os.path.dirname(self.session_name), exist_ok=True)
|
|
logger.info("Starting Admin Review Bot...")
|
|
await self.client.start(bot_token=self.bot_token)
|
|
logger.info("Admin Review Bot connected successfully.")
|
|
|
|
self._register_handlers()
|
|
|
|
def _register_handlers(self):
|
|
@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")
|
|
if 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}!")
|
|
|
|
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.")
|
|
|
|
# --- Admin Commands ---
|
|
@self.client.on(events.NewMessage(pattern="/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. Use /add_source <channel_id> <title> [username]")
|
|
return
|
|
lines = ["<b>Active Monitored Sources:</b>"]
|
|
for s in sources:
|
|
lines.append(f"• ID: <code>{s.channel_id}</code> | Title: <b>{s.title or 'N/A'}</b> (@{s.username or 'none'})")
|
|
await event.reply("\n".join(lines), 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)
|
|
await event.reply(f"✅ Added source channel <b>{title}</b> (<code>{ch_id}</code>).", parse_mode="html")
|
|
|
|
@self.client.on(events.NewMessage(pattern="/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("No target channels configured. Use /add_target <channel_id> <title> <interval_min> [username]")
|
|
return
|
|
lines = ["<b>Target Publishing Channels:</b>"]
|
|
for t in targets:
|
|
lines.append(f"• ID: <code>{t.id}</code> (Channel: <code>{t.channel_id}</code>) | <b>{t.title}</b> | Interval: <b>{t.post_interval_min}m</b>")
|
|
await event.reply("\n".join(lines), 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")
|
|
|
|
@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")
|
|
|
|
@self.client.on(events.NewMessage(pattern="/stats"))
|
|
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))
|
|
|
|
text = (
|
|
"📊 <b>Copykar Bot Statistics</b>\n\n"
|
|
f"• ⏳ Pending AI: <b>{pending_ai}</b>\n"
|
|
f"• 📋 Pending Admin Review: <b>{pending_review}</b>\n"
|
|
f"• 🚀 Approved (Queued): <b>{approved}</b>\n"
|
|
f"• ✅ Published: <b>{published}</b>\n"
|
|
)
|
|
await event.reply(text, parse_mode="html")
|
|
|
|
async def send_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>"
|
|
)
|
|
|
|
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 review post {post.id} to review channel: {e}", exc_info=True)
|
|
|
|
async def stop(self):
|
|
if self.client.is_connected():
|
|
await self.client.disconnect()
|
|
logger.info("Admin Review Bot disconnected.")
|