| import asyncio |
| import random |
| import string |
| import time |
|
|
| from aiogram import types, F |
| from aiogram.filters import Command |
| from aiogram.fsm.context import FSMContext |
| from aiogram.types import ( |
| Message, InlineKeyboardMarkup, InlineKeyboardButton, BufferedInputFile |
| ) |
|
|
| from config import ADMINS, logger |
| from database import ( |
| db_pool, ensure_user, update_user_status, get_top_buyers, |
| get_manga_queue, get_manga_queue_count, save_manga_queue, |
| get_weekly_stats, build_weekly_report_text, |
| get_cached_file_id, save_cached_file_id |
| ) |
| from scraper import ( |
| collect_manga_links, get_manga_chapters_api, |
| fetch_chapter_images_api, download_images_as_zip_async, |
| download_images_as_pdf_async, get_manga_slug |
| ) |
| from handlers import AdminState, safe_edit, safe_answer, back_kb |
|
|
| |
| |
| |
| caching_control = {"is_running": False, "stop_requested": False} |
|
|
| |
| |
| |
| def get_admin_kb(): |
| return InlineKeyboardMarkup(inline_keyboard=[ |
| [InlineKeyboardButton(text="๐ Stats & Leaderboard", callback_data="admin_stats")], |
| [ |
| InlineKeyboardButton(text="๐ฐ Add Coins", callback_data="admin_add_coins"), |
| InlineKeyboardButton(text="๐ Set VIP", callback_data="admin_set_vip"), |
| ], |
| [ |
| InlineKeyboardButton(text="๐ก Broadcast", callback_data="admin_broadcast"), |
| InlineKeyboardButton(text="โ๏ธ Send PM", callback_data="admin_pm"), |
| ], |
| [InlineKeyboardButton(text="๐ Voucher Guide", callback_data="admin_voucher_guide")], |
| [InlineKeyboardButton(text="๐ Fetch Links (71 Pages)", callback_data="admin_fetch_links")], |
| [ |
| InlineKeyboardButton(text="๐ Cache ZIP (Saved Links)", callback_data="admin_start_cache_saved_zip"), |
| InlineKeyboardButton(text="๐ Cache PDF (Saved Links)", callback_data="admin_start_cache_saved_pdf"), |
| ], |
| [InlineKeyboardButton(text="ยซ Back", callback_data="back_to_start")], |
| ]) |
|
|
| |
| |
| |
| async def background_cache_task(bot, admin_id: int, manga_url: str, file_format: str = "zip"): |
| from database import get_manga_slug as _slug |
| slug = _slug(manga_url) |
| chapters = await get_manga_chapters_api(manga_url) |
| if not chapters: |
| return |
| for chap in reversed(chapters): |
| if caching_control["stop_requested"]: |
| break |
| ch_id = chap["id"] |
| if get_cached_file_id(slug, ch_id, file_format): |
| continue |
| images = await fetch_chapter_images_api(ch_id) |
| if not images: |
| continue |
| try: |
| if file_format == "pdf": |
| file_data = await download_images_as_pdf_async(images, slug, ch_id) |
| file_name = f"{slug}_chapter-{chap['slug']}.pdf" |
| else: |
| file_data = await download_images_as_zip_async(images, slug, ch_id) |
| file_name = f"{slug}_chapter-{chap['slug']}.zip" |
| sent_msg = await bot.send_document( |
| chat_id=admin_id, |
| document=BufferedInputFile(file_data, filename=file_name), |
| caption=f"โ๏ธ Cached ({file_format.upper()}): {slug} โ {chap['title']}", |
| disable_notification=True |
| ) |
| new_file_id = sent_msg.document.file_id |
| save_cached_file_id(slug, ch_id, file_format, new_file_id) |
| await asyncio.sleep(20) |
| except Exception as e: |
| logger.error(f"Error caching chapter {ch_id}: {e}") |
| await asyncio.sleep(10) |
|
|
| async def _fetch_and_save_links(bot, admin_id: int): |
| async def progress_cb(page_num, count): |
| try: |
| await bot.send_message(admin_id, f"๐ ุชู
ุณุญุจ ุตูุญุฉ {page_num}/71 โ ุฑูุงุจุท ุญุชู ุงูุขู: {count}") |
| except Exception: |
| pass |
|
|
| all_links = await collect_manga_links( |
| start_page=1, end_page=71, |
| stop_flag=caching_control, |
| progress_cb=progress_cb |
| ) |
| save_manga_queue(all_links) |
| try: |
| await bot.send_message( |
| admin_id, |
| f"โ
*ุงูุชู
ู ุณุญุจ ุงูุฑูุงุจุท!*\n\n" |
| f"๐ ุฅุฌู
ุงูู ุงูุฑูุงุจุท ุงูู
ุญููุธุฉ: *{len(all_links)}*\n\n" |
| f"ุงุถุบุท ุนูู ุฒุฑ ZIP ุฃู PDF ูุจุฏุก ุงูุฃุฑุดูุฉ." |
| ) |
| except Exception: |
| pass |
|
|
| async def _cache_from_queue(bot, admin_id: int, manga_links: list, file_format: str = "zip"): |
| caching_control["is_running"] = True |
| caching_control["stop_requested"] = False |
| total = len(manga_links) |
| try: |
| await bot.send_message(admin_id, f"๐ฐ ุจุฏุฃุช ุงูุฃุฑุดูุฉ ({file_format.upper()}): {total} ู
ุงูุฌุง ูู ุงูุทุงุจูุฑ.") |
| for index, manga_url in enumerate(manga_links, 1): |
| if caching_control["stop_requested"]: |
| await bot.send_message(admin_id, "๐ ุชู
ุฅููุงู ุงูุฃุฑุดูุฉ ุจูุงุกู ุนูู ุทูุจู.") |
| break |
| try: |
| if index % 10 == 0 or index == 1: |
| await bot.send_message(admin_id, f"๐ ุงูุชูุฏู
: {index}/{total}") |
| await background_cache_task(bot, admin_id, manga_url, file_format=file_format) |
| await asyncio.sleep(5) |
| except Exception as e: |
| logger.error(f"Error caching {manga_url}: {e}") |
| finally: |
| caching_control["is_running"] = False |
| caching_control["stop_requested"] = False |
| try: |
| await bot.send_message(admin_id, f"โ
ุงูุชู
ูุช ุนู
ููุฉ ุงูุฃุฑุดูุฉ ({file_format.upper()}) ู
ู ุงูุทุงุจูุฑ ุงูู
ุญููุธ.") |
| except Exception: |
| pass |
|
|
| |
| |
| |
| def register_admin_handlers(dp, bot): |
|
|
| @dp.callback_query(F.data == "admin_btn") |
| async def admin_panel(cb: types.CallbackQuery): |
| if cb.from_user.id not in ADMINS: |
| return await cb.answer("Unauthorized!", show_alert=True) |
| await cb.answer() |
| await safe_edit(cb.message, "โ๏ธ *Admin Panel*\n\nChoose an action:", reply_markup=get_admin_kb()) |
|
|
| @dp.callback_query(F.data == "admin_stats") |
| async def admin_stats(cb: types.CallbackQuery): |
| if cb.from_user.id not in ADMINS: return await cb.answer("Unauthorized!", show_alert=True) |
| await cb.answer() |
| conn = db_pool.getconn() |
| try: |
| with conn.cursor() as c: |
| c.execute("SELECT COUNT(*) FROM users") |
| total_users = c.fetchone()[0] |
| c.execute("SELECT COUNT(*) FROM opened_chapters") |
| total_opened = c.fetchone()[0] |
| c.execute("SELECT COUNT(*) FROM users WHERE vip_until > %s", (int(time.time()),)) |
| total_vip = c.fetchone()[0] |
| c.execute("SELECT COUNT(*) FROM manga_queue") |
| queued_links = c.fetchone()[0] |
| c.execute("SELECT COUNT(*) FROM chapter_cache") |
| cached_chapters = c.fetchone()[0] |
| finally: |
| db_pool.putconn(conn) |
| top = get_top_buyers(5) |
| medals = ["๐ฅ","๐ฅ","๐ฅ","4๏ธโฃ","5๏ธโฃ"] |
| board = "\n".join( |
| f"{medals[i]} {('@'+uname) if uname else uid} โ *{cnt}* chapters" |
| for i, (uid, uname, cnt) in enumerate(top) |
| ) or "No data yet" |
| await safe_edit( |
| cb.message, |
| f"๐ *Bot Stats*\n\n" |
| f"๐ฅ Total Users: *{total_users}*\n" |
| f"๐ VIP Users: *{total_vip}*\n" |
| f"๐ Total Chapters Opened: *{total_opened}*\n" |
| f"๐พ Cached Chapters: *{cached_chapters}*\n" |
| f"๐ Queued Links: *{queued_links}*\n\n" |
| f"๐ *Top Buyers:*\n{board}", |
| reply_markup=back_kb("admin_btn") |
| ) |
|
|
| @dp.callback_query(F.data == "admin_voucher_guide") |
| async def admin_voucher_guide(cb: types.CallbackQuery): |
| if cb.from_user.id not in ADMINS: return await cb.answer("Unauthorized!", show_alert=True) |
| await safe_edit( |
| cb.message, |
| "๐ *Voucher Creation Guide*\n\n" |
| "โข `/gen_code coins 1000` โ Creates a 1000 coin code\n" |
| "โข `/gen_code vip 7` โ Creates a 7-day VIP code\n" |
| "โข `/gen_code vip inf` โ Creates a Lifetime VIP code\n", |
| reply_markup=back_kb("admin_btn") |
| ) |
|
|
| @dp.callback_query(F.data == "admin_add_coins") |
| async def admin_add_coins_btn(cb: types.CallbackQuery, state: FSMContext): |
| if cb.from_user.id not in ADMINS: return await cb.answer("Unauthorized!", show_alert=True) |
| await cb.answer() |
| await state.set_state(AdminState.waiting_add_coins) |
| await safe_edit( |
| cb.message, |
| "๐ฐ *Add Coins*\n\nSend: `USER_ID AMOUNT [Optional Message]`\nExample: `12345 500 Enjoy!`", |
| reply_markup=back_kb("admin_btn") |
| ) |
|
|
| @dp.message(AdminState.waiting_add_coins) |
| async def admin_add_coins_input(m: Message, state: FSMContext): |
| if m.from_user.id not in ADMINS: return |
| try: |
| parts = m.text.split(maxsplit=2) |
| uid, amount = int(parts[0]), int(parts[1]) |
| msg_to_user = parts[2] if len(parts) > 2 else "An admin has sent you some coins! ๐" |
| user = ensure_user(uid) |
| update_user_status(uid, user[2] + amount) |
| await state.clear() |
| await safe_answer( |
| m, |
| f"โ
Added *{amount}* coins to `{uid}`\nNew balance: *{user[2] + amount}*", |
| reply_markup=back_kb("admin_btn") |
| ) |
| try: |
| await bot.send_message(uid, f"๐ฐ *{amount} Coins Added!*\n\nMessage: _{msg_to_user}_") |
| except Exception: |
| pass |
| except Exception: |
| await m.answer("โ Format: `USER_ID AMOUNT [MESSAGE]`") |
|
|
| @dp.callback_query(F.data == "admin_set_vip") |
| async def admin_set_vip_btn(cb: types.CallbackQuery, state: FSMContext): |
| if cb.from_user.id not in ADMINS: return await cb.answer("Unauthorized!", show_alert=True) |
| await cb.answer() |
| await state.set_state(AdminState.waiting_vip) |
| await safe_edit( |
| cb.message, |
| "๐ *Set VIP*\n\nSend: `USER_ID DAYS [Message]` or `USER_ID inf [Message]`", |
| reply_markup=back_kb("admin_btn") |
| ) |
|
|
| @dp.message(AdminState.waiting_vip) |
| async def admin_vip_input(m: Message, state: FSMContext): |
| if m.from_user.id not in ADMINS: return |
| try: |
| parts = m.text.split(maxsplit=2) |
| uid, d = int(parts[0]), parts[1].lower() |
| msg_to_user = parts[2] if len(parts) > 2 else "An admin has upgraded your account to VIP! ๐" |
| expire = 9999999999 if d == "inf" else int(time.time()) + int(d) * 86400 |
| user = ensure_user(uid) |
| update_user_status(uid, user[2], expire) |
| await state.clear() |
| vip_text = "Lifetime" if d == "inf" else f"{d} days" |
| await safe_answer(m, f"โ
VIP set for `{uid}` โ *{vip_text}*", reply_markup=back_kb("admin_btn")) |
| try: |
| await bot.send_message(uid, f"๐ *VIP Status Activated!* ({vip_text})\n\nMessage: _{msg_to_user}_") |
| except Exception: |
| pass |
| except Exception: |
| await m.answer("โ Format: `USER_ID DAYS/inf [MESSAGE]`") |
|
|
| @dp.callback_query(F.data == "admin_pm") |
| async def admin_pm_btn(cb: types.CallbackQuery, state: FSMContext): |
| if cb.from_user.id not in ADMINS: return await cb.answer("Unauthorized!", show_alert=True) |
| await cb.answer() |
| await state.set_state(AdminState.waiting_pm) |
| await safe_edit(cb.message, "โ๏ธ *Send PM*\n\nSend: `USER_ID Your Message Here`", |
| reply_markup=back_kb("admin_btn")) |
|
|
| @dp.message(AdminState.waiting_pm) |
| async def admin_pm_input(m: Message, state: FSMContext): |
| if m.from_user.id not in ADMINS: return |
| try: |
| parts = m.text.split(maxsplit=1) |
| uid = int(parts[0]) |
| msg = parts[1] |
| await state.clear() |
| try: |
| await bot.send_message(uid, f"๐ฉ *Message from Admin:*\n\n{msg}") |
| await safe_answer(m, f"โ
PM sent to `{uid}`", reply_markup=back_kb("admin_btn")) |
| except Exception as e: |
| await safe_answer(m, f"โ Failed: {e}", reply_markup=back_kb("admin_btn")) |
| except Exception: |
| await m.answer("โ Format: `USER_ID Message`") |
|
|
| @dp.callback_query(F.data == "admin_broadcast") |
| async def admin_broadcast_btn(cb: types.CallbackQuery, state: FSMContext): |
| if cb.from_user.id not in ADMINS: return await cb.answer("Unauthorized!", show_alert=True) |
| await cb.answer() |
| await state.set_state(AdminState.waiting_broadcast) |
| await safe_edit(cb.message, "๐ก *Broadcast*\n\nSend your message now:", reply_markup=back_kb("admin_btn")) |
|
|
| @dp.message(AdminState.waiting_broadcast) |
| async def admin_broadcast_input(m: Message, state: FSMContext): |
| if m.from_user.id not in ADMINS: return |
| text = m.text.strip() |
| await state.clear() |
| conn = db_pool.getconn() |
| try: |
| with conn.cursor() as c: |
| c.execute("SELECT user_id FROM users") |
| users = c.fetchall() |
| finally: |
| db_pool.putconn(conn) |
| sent = 0 |
| msg_obj = await m.answer(f"๐ก Sending to {len(users)} users...") |
| for i, (uid,) in enumerate(users): |
| try: |
| await bot.send_message(uid, text) |
| sent += 1 |
| except Exception: |
| pass |
| if i % 20 == 0: |
| try: await msg_obj.edit_text(f"๐ก {i}/{len(users)}") |
| except Exception: pass |
| await msg_obj.edit_text(f"โ
Done: *{sent}/{len(users)}*", reply_markup=back_kb("admin_btn")) |
|
|
| |
| def _gen_code(length=10): |
| return ''.join(random.choices(string.ascii_uppercase + string.digits, k=length)) |
|
|
| @dp.message(Command("gen_code")) |
| async def generate_voucher(m: Message): |
| if m.from_user.id not in ADMINS: return |
| parts = m.text.split() |
| if len(parts) < 3: |
| return await m.answer("Usage:\n`/gen_code coins 100`\n`/gen_code vip 7`\n`/gen_code vip inf`") |
| code_type = parts[1].lower() |
| value = parts[2].lower() |
| code = _gen_code() |
| conn = db_pool.getconn() |
| try: |
| with conn.cursor() as c: |
| if code_type == "coins": |
| coins = int(value) |
| c.execute("INSERT INTO vouchers (code, coins) VALUES (%s,%s)", (code, coins)) |
| msg = f"๐ Code Created!\n\nCode: `{code}`\n๐ฐ Coins: *{coins}*" |
| elif code_type == "vip": |
| vip_days = -1 if value == "inf" else int(value) |
| c.execute("INSERT INTO vouchers (code, vip_days) VALUES (%s,%s)", (code, vip_days)) |
| msg = f"๐ Code Created!\n\nCode: `{code}`\n๐ VIP: *{'Lifetime' if value=='inf' else value + ' days'}*" |
| else: |
| return await m.answer("โ Type must be `coins` or `vip`") |
| conn.commit() |
| finally: |
| db_pool.putconn(conn) |
| await m.answer(msg) |
|
|
| @dp.message(Command("pm")) |
| async def pm_cmd(m: Message): |
| if m.from_user.id not in ADMINS: return |
| try: |
| parts = m.text.split(maxsplit=2) |
| uid = int(parts[1]) |
| msg = parts[2] |
| await bot.send_message(uid, f"๐ฉ *Message from Admin:*\n\n{msg}") |
| await m.answer(f"โ
Sent to `{uid}`") |
| except Exception: |
| await m.answer("Usage: `/pm USER_ID Message`") |
|
|
| |
| @dp.callback_query(F.data == "admin_fetch_links") |
| async def admin_fetch_links_btn(cb: types.CallbackQuery): |
| if cb.from_user.id not in ADMINS: |
| return await cb.answer("Unauthorized!", show_alert=True) |
| if caching_control["is_running"]: |
| return await cb.answer("โ ๏ธ ุนู
ููุฉ ุฃุฑุดูุฉ ุชุนู
ู ุงูุขู! ุฃููููุง ุฃููุงู.", show_alert=True) |
| await cb.answer("๐ ุฌุงุฑู ุณุญุจ ุงูุฑูุงุจุท...", show_alert=True) |
| await safe_edit( |
| cb.message, |
| "๐ *ุฌุงุฑู ุณุญุจ ุฑูุงุจุท ุงูู 71 ุตูุญุฉ...*\n\n" |
| "ุณูุชู
ุฅุฑุณุงู ุชุญุฏูุซ ูู 10 ุตูุญุงุช.\n" |
| "ุจุนุฏ ุงูุงูุชูุงุก ุงุถุบุท ุฒุฑ ุงููุงุดููุฌ.", |
| reply_markup=back_kb("admin_btn") |
| ) |
| asyncio.create_task(_fetch_and_save_links(bot, cb.from_user.id)) |
|
|
| @dp.callback_query(F.data.in_({"admin_start_cache_saved_zip", "admin_start_cache_saved_pdf"})) |
| async def admin_start_cache_saved_btn(cb: types.CallbackQuery): |
| if cb.from_user.id not in ADMINS: |
| return await cb.answer("Unauthorized!", show_alert=True) |
| if caching_control["is_running"]: |
| return await cb.answer("โ ๏ธ ุงูุฃุฑุดูุฉ ุชุนู
ู ุจุงููุนู! ุงุณุชุฎุฏู
/stop_cache ุฃููุงู.", show_alert=True) |
| queue = get_manga_queue() |
| if not queue: |
| return await cb.answer("โ ูุง ุชูุฌุฏ ุฑูุงุจุท ู
ุญููุธุฉ! ุงุณุญุจ ุงูุฑูุงุจุท ุฃููุงู.", show_alert=True) |
| file_format = "pdf" if cb.data.endswith("_pdf") else "zip" |
| await cb.answer(f"๐ ุจุฏุฃ ุงููุงุดููุฌ ({file_format.upper()}) ู
ู ุงูุฑูุงุจุท ุงูู
ุญููุธุฉ...", show_alert=True) |
| await safe_edit( |
| cb.message, |
| f"๐ *ุจุฏุฃ ุงููุงุดููุฌ!*\n\n" |
| f"๐ ุนุฏุฏ ุงูุฑูุงุจุท: *{len(queue)}*\n" |
| f"๐ ุงูููุน: *{file_format.upper()}*\n" |
| f"ุงุณุชุฎุฏู
/stop_cache ููุฅููุงู.", |
| reply_markup=back_kb("admin_btn") |
| ) |
| asyncio.create_task(_cache_from_queue(bot, cb.from_user.id, queue, file_format=file_format)) |
|
|
| @dp.message(Command("cache_site")) |
| async def cache_site_cmd(m: Message): |
| if m.from_user.id not in ADMINS: return |
| if caching_control["is_running"]: |
| return await m.answer("โ ๏ธ ุงูุฃุฑุดูุฉ ุชุนู
ู! ุงุณุชุฎุฏู
/stop_cache ุฃููุงู.") |
| parts = m.text.split() |
| start_p = int(parts[1]) if len(parts) >= 2 and parts[1].isdigit() else 1 |
| end_p = int(parts[2]) if len(parts) >= 3 and parts[2].isdigit() else 71 |
| queue = get_manga_queue() |
| if not queue: |
| await m.answer(f"๐ ูุง ุชูุฌุฏ ุฑูุงุจุท ู
ุญููุธุฉ. ุณูุชู
ุณุญุจูุง ุฃููุงู ู
ู ุตูุญุฉ {start_p} ุฅูู {end_p}.") |
| asyncio.create_task(_fetch_and_save_links(bot, m.from_user.id)) |
| else: |
| asyncio.create_task(_cache_from_queue(bot, m.from_user.id, queue, file_format="zip")) |
| await m.answer(f"๐ฐ ุจุฏุฃุช ุงูุฃุฑุดูุฉ ZIP ู
ู ุงูุฑูุงุจุท ุงูู
ุญููุธุฉ ({len(queue)} ู
ุงูุฌุง).") |
|
|
| @dp.message(Command("stop_cache")) |
| async def stop_cache_cmd(m: Message): |
| if m.from_user.id not in ADMINS: return |
| if not caching_control["is_running"]: |
| return await m.answer("โ ูุง ุชูุฌุฏ ุนู
ููุฉ ุฃุฑุดูุฉ ุชุนู
ู ุญุงููุงู.") |
| caching_control["stop_requested"] = True |
| await m.answer("โณ ุฌุงุฑู ุทูุจ ุงูุฅููุงู... ุณูุชููู ุจุนุฏ ุงูู
ูู
ุฉ ุงูุญุงููุฉ.") |
|
|