import asyncio import os import time import logging import re from aiogram import Bot, Dispatcher, types, BaseMiddleware, F from aiogram.filters import Command from aiogram.types import FSInputFile, InlineKeyboardMarkup, InlineKeyboardButton, CallbackQuery from aiogram.client.default import DefaultBotProperties from aiogram.client.session.aiohttp import AiohttpSession from dotenv import load_dotenv from apscheduler.schedulers.asyncio import AsyncIOScheduler from yt_engine import process_youtube_request, get_recommendations, search_youtube_list from db_manager import db from s3_manager import upload_audio, download_audio, delete_audio logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) load_dotenv() BOT_TOKEN = os.getenv("BOT_TOKEN") # --- KONFIGURASI ADMIN & USERS --- ADMIN_ID = int(os.getenv("ADMIN_ID", 0)) auth_users_str = os.getenv("AUTHORIZED_USER_IDS", "") AUTHORIZED_USER_IDS = {int(uid.strip()) for uid in auth_users_str.split(",") if uid.strip().isdigit()} # Pastikan Admin selalu masuk whitelist if ADMIN_ID != 0: AUTHORIZED_USER_IDS.add(ADMIN_ID) # Penyimpanan sementara user yang sedang menunggu persetujuan (agar admin tidak di-spam) pending_users = set() session = AiohttpSession(timeout=300) bot = Bot(token=BOT_TOKEN, session=session, default=DefaultBotProperties(parse_mode=None)) dp = Dispatcher() music_queue = asyncio.Queue() user_searches = {} async def queue_worker(): while True: query, chat_id, msg_id = await music_queue.get() try: await process_music_request(query, chat_id, msg_id) except Exception as e: logger.error(f"Worker Error: {e}") finally: music_queue.task_done() # --- SATPAM BOT DENGAN FITUR NOTIFIKASI ADMIN --- class SecurityMiddleware(BaseMiddleware): async def __call__(self, handler, event: types.Message | CallbackQuery, data: dict): user_id = event.from_user.id if user_id not in AUTHORIZED_USER_IDS: # Jika user baru dan belum ada di antrean pending if user_id not in pending_users and ADMIN_ID != 0: pending_users.add(user_id) username = event.from_user.username or event.from_user.first_name or "Unknown" # 1. Kirim pesan ke Admin dengan tombol kb = InlineKeyboardMarkup(inline_keyboard=[ [ InlineKeyboardButton(text="āœ… Izinkan", callback_data=f"approve_{user_id}"), InlineKeyboardButton(text="āŒ Tolak", callback_data=f"reject_{user_id}") ] ]) try: await bot.send_message( ADMIN_ID, f"šŸ”” **IZIN AKSES BARU!**\n\nšŸ‘¤ Nama: `{username}`\nšŸ†” ID: `{user_id}`\n\n_Apakah kamu mengizinkan orang ini menggunakan bot musikmu?_", reply_markup=kb, parse_mode="Markdown" ) except Exception as e: logger.error(f"Gagal mengirim notif ke admin: {e}") # 2. Kirim pesan ke User asing if isinstance(event, types.Message): await event.answer("šŸ”’ **Akses Terkunci.**\nBot ini bersifat private. Permintaan izin telah dikirim ke Admin. Harap tunggu persetujuan.", parse_mode="Markdown") elif isinstance(event, types.CallbackQuery): await event.answer("Akses terkunci! Permintaan izin dikirim ke Admin.", show_alert=True) # Abaikan semua perintah dari user asing ini return return await handler(event, data) dp.message.middleware(SecurityMiddleware()) dp.callback_query.middleware(SecurityMiddleware()) # --- FUNGSI UPDATE .ENV OTOMATIS --- def add_user_to_env(new_id): try: with open(".env", "r") as f: lines = f.readlines() with open(".env", "w") as f: for line in lines: if line.startswith("AUTHORIZED_USER_IDS="): clean_line = line.strip().rstrip(',') line = f"{clean_line},{new_id}\n" f.write(line) logger.info(f"User {new_id} berhasil ditambahkan permanen ke .env") except Exception as e: logger.error(f"Gagal update .env: {e}") # --- HANDLER TOMBOL ADMIN (APPROVE / REJECT) --- @dp.callback_query(F.data.startswith("approve_") | F.data.startswith("reject_")) async def handle_admin_action(callback: CallbackQuery): if callback.from_user.id != ADMIN_ID: return await callback.answer("Kamu bukan Admin!", show_alert=True) action, target_id_str = callback.data.split("_") target_id = int(target_id_str) if target_id in pending_users: pending_users.remove(target_id) await callback.message.edit_reply_markup(reply_markup=None) if action == "approve": AUTHORIZED_USER_IDS.add(target_id) add_user_to_env(target_id) await callback.message.edit_text(f"āœ… User `{target_id}` telah **DISETUJUI** dan ditambahkan permanen ke sistem.", parse_mode="Markdown") try: await bot.send_message(target_id, "šŸŽ‰ **AKSES DISETUJUI!**\nAdmin telah memberikan izin. Silakan ketik /start atau /play untuk mulai mencari lagu.", parse_mode="Markdown") except: pass else: await callback.message.edit_text(f"āŒ User `{target_id}` telah **DITOLAK**.", parse_mode="Markdown") try: await bot.send_message(target_id, "āŒ **AKSES DITOLAK.**\nAdmin tidak memberikan izin untuk menggunakan bot ini.", parse_mode="Markdown") except: pass def extract_video_id(query): match = re.search(r"(?:v=|\/)([0-9A-Za-z_-]{11}).*", query) return match.group(1) if match else None def get_search_keyboard(chat_id, page=0): data = user_searches.get(chat_id) if not data: return None results = data['results'] start_idx = page * 10 end_idx = start_idx + 10 page_results = results[start_idx:end_idx] kb = [] for rec in page_results: display_text = f"šŸŽµ {rec['title']} | {rec.get('uploader', 'YT')}" short_text = display_text[:55] + "..." if len(display_text) > 55 else display_text kb.append([InlineKeyboardButton(text=short_text, callback_data=f"play_{rec['id']}")]) nav_buttons = [] if page > 0: nav_buttons.append(InlineKeyboardButton(text="ā¬…ļø Prev", callback_data=f"page_{page-1}")) if end_idx < len(results): nav_buttons.append(InlineKeyboardButton(text="Next āž”ļø", callback_data=f"page_{page+1}")) if nav_buttons: kb.append(nav_buttons) return InlineKeyboardMarkup(inline_keyboard=kb) @dp.message(Command("start")) async def cmd_start(message: types.Message): await message.answer("šŸŽ§ **Music Bot VIP Ready!**\nKirim format: `/play `", parse_mode="Markdown") @dp.message(Command("play")) async def cmd_play(message: types.Message): query = message.text.replace("/play", "").strip() if not query: return await message.answer("āš ļø Masukkan judul lagu.") status_msg = await message.answer("šŸ” *Mencari daftar lagu...*", parse_mode="Markdown") results = await search_youtube_list(query, max_results=30) if not results: return await status_msg.edit_text("āŒ Lagu tidak ditemukan.") user_searches[message.chat.id] = { 'header': f"šŸ”Ž Hasil pencarian: **{query}**", 'results': results } kb = get_search_keyboard(message.chat.id, page=0) await status_msg.edit_text(text=f"{user_searches[message.chat.id]['header']}\n_Halaman 1_", reply_markup=kb, parse_mode="Markdown") @dp.callback_query(F.data.startswith("page_")) async def handle_page_click(callback: CallbackQuery): chat_id = callback.message.chat.id if chat_id not in user_searches: return await callback.answer("Sesi kadaluarsa. Ketik /play lagi.", show_alert=True) page = int(callback.data.split("_")[1]) kb = get_search_keyboard(chat_id, page) if kb: header = user_searches[chat_id]['header'] await callback.message.edit_text(text=f"{header}\n_Halaman {page+1}_", reply_markup=kb, parse_mode="Markdown") await callback.answer() @dp.callback_query(F.data.startswith("play_")) async def handle_suggestion_click(callback: CallbackQuery): video_id = callback.data.split("_", 1)[1] await callback.message.edit_reply_markup(reply_markup=None) status_msg = await bot.send_message(callback.message.chat.id, "ā³ Mengunduh lagu pilihanmu...") query_url = f"https://www.youtube.com/watch?v={video_id}" await music_queue.put((query_url, callback.message.chat.id, status_msg.message_id)) await callback.answer("Siapp! Diproses...") async def process_music_request(query: str, chat_id: int, status_msg_id: int): local_file = None try: video_id = extract_video_id(query) cached_data = None if video_id: cached_data = await db.get_cache(video_id) if cached_data: title = cached_data.get('title', 'Lagu') s3_object_name = cached_data.get('s3_object_key', f"{video_id}.m4a") ext = s3_object_name.split('.')[-1] local_file = f"downloads/cache_{video_id}.{ext}" await bot.edit_message_text("⚔ Mengambil dari Cache...", chat_id, status_msg_id) await download_audio(s3_object_name, local_file) if not cached_data: await bot.edit_message_text("šŸ” Mengunduh dari YouTube...", chat_id, status_msg_id) yt_result = await process_youtube_request(query) if yt_result["status"] == "error": return await bot.edit_message_text(f"āŒ Gagal: {yt_result['message']}", chat_id, status_msg_id) video_id = yt_result["video_id"] title = yt_result["title"] local_file = yt_result["file_path"] s3_object_name = f"{video_id}.{yt_result['ext']}" await bot.edit_message_text("ā˜ļø Menyimpan ke Server...", chat_id, status_msg_id) if local_file and os.path.exists(local_file): await upload_audio(local_file, s3_object_name) await db.save_cache(video_id, title, s3_object_name) await bot.edit_message_text("šŸ“¤ Mengirim ke Telegram...", chat_id, status_msg_id) if local_file and os.path.exists(local_file): audio = FSInputFile(local_file) await bot.send_audio(chat_id=chat_id, audio=audio, title=title, performer="Music Bot", request_timeout=300) if status_msg_id: await bot.delete_message(chat_id, status_msg_id) # --- TAMPILKAN 30 REKOMENDASI DENGAN PAGING --- recs = await get_recommendations(video_id) if recs: user_searches[chat_id] = { 'header': f"šŸ’” Rekomendasi lanjutan dari: **{title}**", 'results': recs } kb = get_search_keyboard(chat_id, page=0) if kb: await bot.send_message(chat_id, text=f"{user_searches[chat_id]['header']}\n_Halaman 1_", reply_markup=kb, parse_mode="Markdown") except Exception as e: logger.error(f"Error: {e}") finally: if local_file and os.path.exists(local_file): os.remove(local_file) async def cleanup_expired_cache(): expired_items = await db.get_expired_cache(days=7) for item in expired_items: await delete_audio(item['s3_object_key']) await db.delete_cache(item['youtube_id']) async def main(): await db.connect() asyncio.create_task(queue_worker()) scheduler = AsyncIOScheduler() scheduler.add_job(cleanup_expired_cache, 'cron', hour=3, minute=0) scheduler.start() logger.info("šŸš€ Bot Music V4 (Admin Approval & Super Paging) Berjalan!") await bot.delete_webhook(drop_pending_updates=True) await dp.start_polling(bot) if __name__ == "__main__": asyncio.run(main())