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 = []
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:
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))
redis_q = await self.collector.queue.qsize() if (self.collector and self.collector.queue) else 0
text = (
"📊 آمار زنده سیستم کپیکار:\n\n"
f"• 📥 پستهای موجود در صف ردیس: {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:
lines.append(
f"• {t.title} (شناسه: {t.channel_id} | ID دیتابیس: {t.id})\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()
)
# --- 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. 👁 پیشنمایش زنده: پیشنمایش بازنویسی شده به همراه دکمه تایید نهایی نمایش داده میشود.\n"
"4. 🚀 انتشار و ارسال مجدد: پس از انتشار، پست اصلی در کانال ادمین بازگردانده شده و سابقه انتشار نمایش مییابد تا بتوانید آن را به سایر کانالها نیز ارسال کنید."
)
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"⏳ در حال دریافت {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}...")
# Show loading placeholder
loading_caption = (
f"🤖 در حال بازنویسی هوشمند برای کانال: {target.title}...\n"
f"(اعمال لحن اختصاصی و حذف تگهای مبدا)"
)
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"🎯 پیشنمایش بازنویسی شده برای: {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. 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"✅ با موفقیت در {target.title} منتشر شد!\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❌ این پست توسط ادمین رد و بایگانی شد.",
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.")