Spaces:
Running
Running
| import asyncio | |
| import datetime | |
| def get_all_shorteners(admin_settings): | |
| import json | |
| shorteners = [] | |
| if hasattr(admin_settings, "shorteners_list") and admin_settings.shorteners_list: | |
| try: | |
| shorteners = json.loads(admin_settings.shorteners_list) | |
| except Exception: | |
| pass | |
| if not shorteners and getattr(admin_settings, "shortener_api", None): | |
| default_site = admin_settings.shortener_site or "gplinks.in" | |
| default_api = admin_settings.shortener_api | |
| default_how = admin_settings.how_to_download or "https://telegram.me/FIREBOLTTutorial/23" | |
| default_status = getattr(admin_settings, "shortener_status", True) | |
| if default_status is None: default_status = True | |
| shorteners = [{ | |
| "site": default_site, | |
| "api": default_api, | |
| "how_to_download": default_how, | |
| "status": default_status | |
| }] | |
| return shorteners | |
| def parse_duration_to_seconds(val: str): | |
| if not val: | |
| return None | |
| val = val.strip().lower() | |
| if val == "off": | |
| return 0 | |
| if val.endswith("d"): | |
| try: | |
| return int(float(val[:-1]) * 86400) | |
| except ValueError: | |
| return None | |
| elif val.endswith("h"): | |
| try: | |
| return int(float(val[:-1]) * 3600) | |
| except ValueError: | |
| return None | |
| elif val.endswith("m"): | |
| try: | |
| return int(float(val[:-1]) * 60) | |
| except ValueError: | |
| return None | |
| elif val.endswith("s"): | |
| try: | |
| return int(float(val[:-1])) | |
| except ValueError: | |
| return None | |
| else: | |
| try: | |
| return int(val) | |
| except ValueError: | |
| return None | |
| import shlex | |
| from pyrogram import Client, filters | |
| from mfinder.db.settings_sql import ( | |
| get_admin_settings, | |
| set_repair_mode, | |
| set_auto_delete, | |
| set_custom_caption, | |
| set_force_sub, | |
| set_channel_link, | |
| get_link, | |
| set_username, | |
| set_log_channel, | |
| get_log_channel, | |
| set_how_to_download | |
| ) | |
| from mfinder.db.ban_sql import is_banned, ban_user, unban_user | |
| from mfinder.db.filters_sql import add_filter, rem_filter, list_filters | |
| from mfinder.db.files_sql import count_files | |
| from mfinder import ADMINS, DB_CHANNELS | |
| async def auto_delete_(bot, update): | |
| data = update.text.split() | |
| if len(data) == 2: | |
| dur_str = data[-1] | |
| dur = parse_duration_to_seconds(dur_str) | |
| if dur is None or dur < 0: | |
| await update.reply_text("❌ **Invalid format!** Please use e.g. `2m` (2 minutes), `5h` (5 hours), `3600` (seconds), or `off`.") | |
| return | |
| await set_auto_delete(dur) | |
| if dur: | |
| if dur >= 86400: | |
| desc = f"{dur / 86400:.1f} days" if (dur / 86400) % 1 != 0 else f"{int(dur / 86400)} days" | |
| elif dur >= 3600: | |
| desc = f"{dur / 3600:.1f} hours" if (dur / 3600) % 1 != 0 else f"{int(dur / 3600)} hours" | |
| elif dur >= 60: | |
| desc = f"{dur / 60:.1f} minutes" if (dur / 60) % 1 != 0 else f"{int(dur / 60)} minutes" | |
| else: | |
| desc = f"{dur} seconds" | |
| await update.reply_text(f"✅ File auto delete set to `{dur}` seconds ({desc}).") | |
| else: | |
| await update.reply_text("✅ File auto delete disabled.") | |
| else: | |
| await update.reply_text("Please send in proper format `/autodelete <duration>` (e.g. `/autodelete 2m`, `/autodelete 5h` or `/autodelete off`)") | |
| async def repair_mode_(bot, update): | |
| data = update.text.split() | |
| if len(data) == 2: | |
| toggle = data[-1] | |
| if toggle.lower() == "off": | |
| mode = False | |
| elif toggle.lower() == "on": | |
| mode = True | |
| else: | |
| await update.reply_text( | |
| "Please send in proper format `/repairmode <on/off>`" | |
| ) | |
| return | |
| await set_repair_mode(mode) | |
| await update.reply_text(f"Repair mode set to `{toggle.upper()}`") | |
| else: | |
| await update.reply_text("Please send in proper format `/repairmode on/off`") | |
| return | |
| async def custom_caption_(bot, update): | |
| data = update.text.split() | |
| caption = " ".join(data[1:]) | |
| if len(data) >= 2: | |
| if caption.lower() == "off": | |
| caption = None | |
| await set_custom_caption(caption) | |
| if caption: | |
| await update.reply_text(f"Custom caption set to `{caption}`") | |
| else: | |
| await update.reply_text("Custom caption disabled") | |
| else: | |
| await update.reply_text( | |
| "Please send in proper format `/customcaption caption/off`" | |
| ) | |
| return | |
| ADMIN_INPUT_STATE = {} | |
| async def get_settings_screen(cat: str): | |
| from mfinder.db.settings_sql import get_admin_settings | |
| from mfinder import ADMINS, DB_CHANNELS | |
| admin_settings = await get_admin_settings() | |
| auto_delete = admin_settings.auto_delete | |
| custom_caption = admin_settings.custom_caption | |
| fsub_channel = admin_settings.fsub_channel | |
| fsub_channels_list = admin_settings.fsub_channels_list | |
| caption_uname = admin_settings.caption_uname | |
| invite_link = admin_settings.channel_link | |
| repair_mode = admin_settings.repair_mode | |
| shortener_site = admin_settings.shortener_site | |
| shortener_api = admin_settings.shortener_api | |
| shortener_status = admin_settings.shortener_status | |
| if shortener_status is None: | |
| shortener_status = True | |
| token_shortener_enabled = admin_settings.token_shortener_enabled | |
| if token_shortener_enabled is None: | |
| token_shortener_enabled = False | |
| token_timeout = admin_settings.token_timeout | |
| if token_timeout is None: | |
| token_timeout = 3600 | |
| log_channel = admin_settings.log_channel | |
| from pyrogram.types import InlineKeyboardMarkup, InlineKeyboardButton | |
| if cat == "main": | |
| text = "🛠️ **Admin Settings**\n\nSelect a category to configure:" | |
| keyboard = InlineKeyboardMarkup([ | |
| [ | |
| InlineKeyboardButton("⚙️ General Settings", callback_data="adm_cat:gen"), | |
| InlineKeyboardButton("🔗 Shortener Settings", callback_data="adm_cat:sho") | |
| ], | |
| [ | |
| InlineKeyboardButton("📢 Channel Settings", callback_data="adm_cat:cha"), | |
| InlineKeyboardButton("📝 Caption Settings", callback_data="adm_cat:cap") | |
| ], | |
| [ | |
| InlineKeyboardButton("📰 Newsletter Settings", callback_data="adm_cat:newsl"), | |
| InlineKeyboardButton("❌ Close Menu", callback_data="adm_close") | |
| ] | |
| ]) | |
| return text, keyboard | |
| elif cat == "gen": | |
| repair_status_label = "🟢 Enabled" if repair_mode else "🔴 Disabled" | |
| repair_btn_label = "🔴 Disable Repair Mode" if repair_mode else "🟢 Enable Repair Mode" | |
| if auto_delete: | |
| parsed_auto_del = int(auto_delete) | |
| if parsed_auto_del >= 86400: | |
| days = parsed_auto_del / 86400 | |
| auto_del_label = f"{days:.1f} days" if days % 1 != 0 else f"{int(days)} days" | |
| elif parsed_auto_del >= 3600: | |
| hours = parsed_auto_del / 3600 | |
| auto_del_label = f"{hours:.1f} hours" if hours % 1 != 0 else f"{int(hours)} hours" | |
| elif parsed_auto_del >= 60: | |
| mins = parsed_auto_del / 60 | |
| auto_del_label = f"{mins:.1f} minutes" if mins % 1 != 0 else f"{int(mins)} minutes" | |
| else: | |
| auto_del_label = f"{parsed_auto_del} seconds" | |
| else: | |
| auto_del_label = "Disabled" | |
| text = ( | |
| "⚙️ **General Settings**\n\n" | |
| f"🛠️ **Repair Mode:** `{repair_status_label}`\n" | |
| "↳ When enabled, the bot ignores all incoming search queries.\n\n" | |
| f"⏱️ **Auto Delete:** `{auto_del_label}`\n" | |
| "↳ Duration before sent files are deleted from the chat." | |
| ) | |
| keyboard = InlineKeyboardMarkup([ | |
| [InlineKeyboardButton(repair_btn_label, callback_data="adm_toggle:repair")], | |
| [InlineKeyboardButton("⏱️ Set Auto Delete Duration", callback_data="adm_ask:auto_delete")], | |
| [InlineKeyboardButton("🔙 Back to Main Settings", callback_data="adm_cat:main")] | |
| ]) | |
| return text, keyboard | |
| elif cat == "sho": | |
| status_label = "🟢 ON" if shortener_status else "🔴 OFF" | |
| toggle_label = "🔴 Turn OFF Shortener" if shortener_status else "🟢 Turn ON Shortener" | |
| smart_label = "🟢 ON" if getattr(admin_settings, "smart_rotator", False) else "🔴 OFF" | |
| smart_toggle_btn = "🔴 Turn OFF Smart Rotator" if getattr(admin_settings, "smart_rotator", False) else "🟢 Turn ON Smart Rotator" | |
| token_status_label = "🟢 ON" if token_shortener_enabled else "🔴 OFF" | |
| token_toggle_label = "🔴 Turn OFF Token Shortener" if token_shortener_enabled else "🟢 Turn ON Token Shortener" | |
| parsed_seconds = int(token_timeout) | |
| if parsed_seconds >= 3600: | |
| hours = parsed_seconds / 3600 | |
| time_desc = f"{hours:.1f} hours" if hours % 1 != 0 else f"{int(hours)} hours" | |
| elif parsed_seconds >= 60: | |
| mins = parsed_seconds / 60 | |
| time_desc = f"{mins:.1f} minutes" if mins % 1 != 0 else f"{int(mins)} minutes" | |
| else: | |
| time_desc = f"{parsed_seconds} seconds" | |
| text = ( | |
| "🔗 **Shortener Settings**\n\n" | |
| f"📄 **Standard Shortener:** `{status_label}`\n" | |
| "↳ If ON, standard file-level shortening is applied.\n\n" | |
| f"🔄 **Smart Rotator:** `{smart_label}`\n" | |
| "↳ Rotates active shorteners per user per day to maximize revenue.\n\n" | |
| f"🎟️ **Token Shortener:** `{token_status_label}`\n" | |
| f"⏱️ **Token Timeout:** `{time_desc}`\n" | |
| "↳ If ON, users complete ONE link for direct access for the duration above." | |
| ) | |
| keyboard = InlineKeyboardMarkup([ | |
| [InlineKeyboardButton(toggle_label, callback_data="adm_toggle:shortener")], | |
| [InlineKeyboardButton(smart_toggle_btn, callback_data="adm_rot_toggle_smart")], | |
| [InlineKeyboardButton(token_toggle_label, callback_data="adm_toggle:token_shortener")], | |
| [InlineKeyboardButton("⏱️ Set Token Timeout", callback_data="adm_ask:token_timeout")], | |
| [InlineKeyboardButton("📋 Shortener List", callback_data="adm_cat:rot")], | |
| [InlineKeyboardButton("🔙 Back to Main Settings", callback_data="adm_cat:main")] | |
| ]) | |
| return text, keyboard | |
| elif cat == "cha": | |
| admins_str = "\n".join(f"• `{adm}`" for adm in ADMINS) | |
| db_channels_str = "\n".join(f"• `{ch}`" for ch in DB_CHANNELS) if DB_CHANNELS else "• None" | |
| fsub_label = fsub_channels_list if fsub_channels_list else (fsub_channel if fsub_channel else "Disabled") | |
| link_label = invite_link if invite_link else "Disabled" | |
| log_label = log_channel if log_channel else "Disabled" | |
| ann_enabled = bool(getattr(admin_settings, "announcement_enabled", False)) | |
| ann_status_label = "🟢 Enabled" if ann_enabled else "🔴 Disabled" | |
| ann_channel = getattr(admin_settings, "announcement_channel", None) | |
| ann_channel_label = f"`{ann_channel}`" if ann_channel else "`Disabled`" | |
| text = ( | |
| "📢 **Channel Settings**\n\n" | |
| f"👤 **Admins**:\n{admins_str}\n\n" | |
| f"📦 **Database Channels**:\n{db_channels_str}\n\n" | |
| f"• **Force Sub Channel**: `{fsub_label}`\n" | |
| f"• **Channel Link**: `{link_label}`\n" | |
| f"• **User Logs Channel**: `{log_label}`\n" | |
| f"• **Upload Announcements**: {ann_status_label}\n" | |
| f"• **Announcement Channel**: {ann_channel_label}" | |
| ) | |
| keyboard = InlineKeyboardMarkup([ | |
| [ | |
| InlineKeyboardButton("👤 Set Admins List", callback_data="adm_ask:admins"), | |
| InlineKeyboardButton("📦 Set DB Channels", callback_data="adm_ask:db_channels") | |
| ], | |
| [ | |
| InlineKeyboardButton("⚡ Set Force Sub ID", callback_data="adm_ask:force_sub"), | |
| InlineKeyboardButton("🔗 Set Channel Link", callback_data="adm_ask:channel_link") | |
| ], | |
| [ | |
| InlineKeyboardButton("📊 Set Log Channel ID", callback_data="adm_ask:log_channel") | |
| ], | |
| [ | |
| InlineKeyboardButton("📢 Toggle Announcements", callback_data="adm_toggle:announcement_enabled"), | |
| InlineKeyboardButton("📢 Set Ann Channel ID", callback_data="adm_ask:announcement_channel") | |
| ], | |
| [InlineKeyboardButton("🔙 Back to Main Settings", callback_data="adm_cat:main")] | |
| ]) | |
| return text, keyboard | |
| elif cat == "cap": | |
| caption_label = custom_caption if custom_caption else "Disabled" | |
| uname_label = caption_uname if caption_uname else "Disabled" | |
| text = ( | |
| "📝 **Caption Settings**\n\n" | |
| f"• **Custom Caption**:\n`{caption_label}`\n\n" | |
| f"• **Caption Username**: `{uname_label}`" | |
| ) | |
| keyboard = InlineKeyboardMarkup([ | |
| [InlineKeyboardButton("✏️ Set Custom Caption", callback_data="adm_ask:custom_caption")], | |
| [InlineKeyboardButton("🏷️ Set Caption Username", callback_data="adm_ask:caption_uname")], | |
| [InlineKeyboardButton("🔙 Back to Main Settings", callback_data="adm_cat:main")] | |
| ]) | |
| return text, keyboard | |
| elif cat == "newsl": | |
| ns_enabled = bool(getattr(admin_settings, "newsletter_enabled", False)) | |
| ns_status_label = "🟢 Enabled" if ns_enabled else "🔴 Disabled" | |
| ns_toggle_label = "🔴 Disable Newsletter" if ns_enabled else "🟢 Enable Newsletter" | |
| ns_days = getattr(admin_settings, "newsletter_days", "Friday") | |
| ns_time = getattr(admin_settings, "newsletter_time", "20:00") | |
| ns_count = getattr(admin_settings, "newsletter_count", 10) | |
| ns_target = getattr(admin_settings, "newsletter_target", "subscribed") | |
| target_label = "👥 All Users" if ns_target == "all" else "🔔 Subscribed Users Only" | |
| target_toggle_label = "🔔 Set Target to Subscribed" if ns_target == "all" else "👥 Set Target to All Users" | |
| text = ( | |
| "📰 **Newsletter Settings**\n\n" | |
| f"🔄 **Newsletter Status:** `{ns_status_label}`\n" | |
| "↳ Toggle automated weekly broadcast feature.\n\n" | |
| f"📅 **Send Weekdays:** `{ns_days}`\n" | |
| "↳ Weekday(s) to broadcast the newsletter (comma separated).\n\n" | |
| f"🕒 **Send Time (24h):** `{ns_time}`\n" | |
| "↳ Time of day to broadcast the newsletter.\n\n" | |
| f"🍿 **Max Movies Count:** `{ns_count}`\n" | |
| "↳ Max newly uploaded movies to include in newsletter.\n\n" | |
| f"🎯 **Target Audience:** `{target_label}`\n" | |
| "↳ Who will receive the auto-broadcast." | |
| ) | |
| keyboard = InlineKeyboardMarkup([ | |
| [InlineKeyboardButton(ns_toggle_label, callback_data="adm_toggle:newsletter_enabled")], | |
| [InlineKeyboardButton(target_toggle_label, callback_data="adm_toggle:newsletter_target")], | |
| [ | |
| InlineKeyboardButton("📅 Set Send Days", callback_data="adm_ask:newsletter_days"), | |
| InlineKeyboardButton("🕒 Set Send Time", callback_data="adm_ask:newsletter_time") | |
| ], | |
| [ | |
| InlineKeyboardButton("🍿 Set Max Movie Count", callback_data="adm_ask:newsletter_count") | |
| ], | |
| [InlineKeyboardButton("🔙 Back to Main Settings", callback_data="adm_cat:main")] | |
| ]) | |
| return text, keyboard | |
| elif cat == "rot": | |
| shorteners = get_all_shorteners(admin_settings) | |
| # Sync it back to database if list is migrated in memory | |
| if hasattr(admin_settings, "shorteners_list") and not admin_settings.shorteners_list: | |
| from mfinder.db.settings_sql import update_shorteners_list | |
| asyncio.create_task(update_shorteners_list(json.dumps(shorteners))) | |
| text = ( | |
| "📋 **Shortener List Settings**\n\n" | |
| "📂 **Configured Shorteners List (Priority Order):**\n\n" | |
| ) | |
| for i, sh in enumerate(shorteners): | |
| status_icon = "🟢 ON" if sh.get("status", True) else "🔴 OFF" | |
| site_url = sh.get("site") | |
| api_key = sh.get("api") | |
| how_to = sh.get("how_to_download") or "https://telegram.me/FIREBOLTTutorial/23" | |
| api_masked = api_key[:4] + "*"*(len(api_key)-4) if len(api_key) > 4 else "Set" | |
| text += f"**{i+1}.** `{site_url}`\n↳ Key: `{api_masked}`\n↳ Tutorial: `{how_to}`\n↳ Status: `{status_icon}`\n\n" | |
| keyboard_buttons = [] | |
| for i, sh in enumerate(shorteners): | |
| site_url = sh.get("site") | |
| keyboard_buttons.append([ | |
| InlineKeyboardButton(f"⚙️ {i+1}. {site_url}", callback_data=f"adm_rot_manage:{i}"), | |
| ]) | |
| keyboard_buttons.append([InlineKeyboardButton("➕ Add New Shortener", callback_data="adm_rot_add")]) | |
| if len(shorteners) > 0: | |
| keyboard_buttons.append([InlineKeyboardButton("🗑️ Clear All Rotators", callback_data="adm_rot_clear")]) | |
| keyboard_buttons.append([InlineKeyboardButton("🔙 Back to Shortener Settings", callback_data="adm_cat:sho")]) | |
| keyboard = InlineKeyboardMarkup(keyboard_buttons) | |
| return text, keyboard | |
| elif cat.startswith("rot_manage:"): | |
| idx = int(cat.split(":")[1]) | |
| shorteners = get_all_shorteners(admin_settings) | |
| if idx >= len(shorteners): | |
| return "⚠️ Shortener not found.", InlineKeyboardMarkup([[InlineKeyboardButton("🔙 Back", callback_data="adm_cat:rot")]]) | |
| sh = shorteners[idx] | |
| site_url = sh.get("site") | |
| status_label = "🟢 ON" if sh.get("status", True) else "🔴 OFF" | |
| toggle_label = "🔴 Turn OFF" if sh.get("status", True) else "🟢 Turn ON" | |
| text = ( | |
| f"⚙️ **Manage Shortener #{idx+1}:** `{site_url}`\n\n" | |
| f"🔘 **Status:** `{status_label}`\n" | |
| f"🔑 **API Key:** `{sh.get('api')}`\n" | |
| f"📖 **Download Tutorial:** `{sh.get('how_to_download', 'Not set')}`" | |
| ) | |
| keyboard = InlineKeyboardMarkup([ | |
| [InlineKeyboardButton(toggle_label, callback_data=f"adm_rot_toggle:{idx}")], | |
| [InlineKeyboardButton("✏️ Edit Shortener", callback_data=f"adm_rot_edit:{idx}")], | |
| [InlineKeyboardButton("🗑️ Delete Shortener", callback_data=f"adm_rot_delete:{idx}")], | |
| [InlineKeyboardButton("🔙 Back to List", callback_data="adm_cat:rot")] | |
| ]) | |
| return text, keyboard | |
| async def admin_settings_(bot, update): | |
| text, keyboard = await get_settings_screen("main") | |
| await update.reply_text( | |
| text=text, | |
| reply_markup=keyboard, | |
| quote=True | |
| ) | |
| async def admin_category_callback(bot, query): | |
| cat = query.data.split(":")[1] | |
| ADMIN_INPUT_STATE.pop(query.from_user.id, None) | |
| text, keyboard = await get_settings_screen(cat) | |
| try: | |
| await query.message.edit_text( | |
| text=text, | |
| reply_markup=keyboard | |
| ) | |
| except Exception: | |
| pass | |
| await query.answer() | |
| async def admin_toggle_callback(bot, query): | |
| setting = query.data.split(":")[1] | |
| from mfinder.db.settings_sql import ( | |
| get_admin_settings, | |
| set_repair_mode, | |
| set_shortener_settings, | |
| set_token_shortener_state | |
| ) | |
| admin_settings = await get_admin_settings() | |
| if setting == "repair": | |
| new_val = not admin_settings.repair_mode | |
| await set_repair_mode(new_val) | |
| await query.answer(f"Repair mode {'Enabled' if new_val else 'Disabled'}") | |
| text, keyboard = await get_settings_screen("gen") | |
| elif setting == "shortener": | |
| current_status = admin_settings.shortener_status | |
| if current_status is None: | |
| current_status = True | |
| new_val = not current_status | |
| await set_shortener_settings(status=new_val) | |
| from mfinder.utils.helpers import clear_short_url_cache | |
| clear_short_url_cache() | |
| try: | |
| from mfinder.db.settings_sql import clear_all_user_shortener_usages | |
| await clear_all_user_shortener_usages() | |
| except Exception: | |
| pass | |
| if new_val: | |
| await set_token_shortener_state(False) | |
| await query.answer(f"Shortener {'ON' if new_val else 'OFF'}") | |
| text, keyboard = await get_settings_screen("sho") | |
| elif setting == "token_shortener": | |
| current_status = admin_settings.token_shortener_enabled | |
| if current_status is None: | |
| current_status = False | |
| new_val = not current_status | |
| await set_token_shortener_state(new_val) | |
| try: | |
| from mfinder.db.settings_sql import clear_all_user_shortener_usages | |
| await clear_all_user_shortener_usages() | |
| except Exception: | |
| pass | |
| if new_val: | |
| await set_shortener_settings(status=False) | |
| await query.answer(f"Token shortener {'Enabled' if new_val else 'Disabled'}") | |
| text, keyboard = await get_settings_screen("sho") | |
| elif setting == "newsletter_enabled": | |
| current_status = bool(getattr(admin_settings, "newsletter_enabled", False)) | |
| new_val = not current_status | |
| from mfinder.db.settings_sql import set_newsletter_settings | |
| await set_newsletter_settings(enabled=new_val) | |
| await query.answer(f"Newsletter {'Enabled' if new_val else 'Disabled'}") | |
| text, keyboard = await get_settings_screen("newsl") | |
| elif setting == "newsletter_target": | |
| current_target = getattr(admin_settings, "newsletter_target", "subscribed") | |
| new_target = "all" if current_target == "subscribed" else "subscribed" | |
| from mfinder.db.settings_sql import set_newsletter_settings | |
| await set_newsletter_settings(target=new_target) | |
| await query.answer(f"Target audience set to {'All Users' if new_target == 'all' else 'Subscribed Users'}") | |
| text, keyboard = await get_settings_screen("newsl") | |
| elif setting == "announcement_enabled": | |
| current_status = bool(getattr(admin_settings, "announcement_enabled", False)) | |
| new_val = not current_status | |
| from mfinder.db.settings_sql import set_announcement_settings | |
| await set_announcement_settings(enabled=new_val) | |
| await query.answer(f"Upload Announcements {'Enabled' if new_val else 'Disabled'}") | |
| text, keyboard = await get_settings_screen("cha") | |
| else: | |
| await query.answer("Unknown toggle.") | |
| return | |
| try: | |
| await query.message.edit_text( | |
| text=text, | |
| reply_markup=keyboard | |
| ) | |
| except Exception: | |
| pass | |
| async def admin_del_shortener_callback(bot, query): | |
| from mfinder.db.settings_sql import delete_shortener_settings | |
| await delete_shortener_settings() | |
| await query.answer("Shortener settings cleared.") | |
| text, keyboard = await get_settings_screen("sho") | |
| try: | |
| await query.message.edit_text( | |
| text=text, | |
| reply_markup=keyboard | |
| ) | |
| except Exception: | |
| pass | |
| async def admin_close_callback(bot, query): | |
| ADMIN_INPUT_STATE.pop(query.from_user.id, None) | |
| try: | |
| await query.message.delete() | |
| except Exception: | |
| pass | |
| await query.answer("Settings menu closed.") | |
| async def admin_ask_callback(bot, query): | |
| action = query.data.split(":")[1] | |
| user_id = query.from_user.id | |
| import time | |
| ADMIN_INPUT_STATE[user_id] = {"action": f"set_{action}", "message_id": query.message.id, "timestamp": time.time()} | |
| prompt_map = { | |
| "add_rotator_shortener": "Send the new shortener site and API key separated by a space (e.g. gplinks.in API_KEY):", | |
| "auto_delete": "Send the new Auto Delete duration in seconds (or 'off' to disable):", | |
| "custom_caption": "Send the new Custom Caption text (or 'off' to disable):", | |
| "caption_uname": "Send the new Caption Username starting with '@' (or 'off' to disable):", | |
| "shortener_site": "Send the new Site name (e.g. shortxlinks.com):", | |
| "shortener_api": "Send the new Shortener API Key:", | |
| "token_timeout": "Send the new Token validity time (e.g., '1h' for 1 hour, '30m' for 30 mins, or raw seconds):", | |
| "force_sub": "Send the new Force Subscription Channel ID (or 'off' to disable):", | |
| "channel_link": "Send the new Force Subscription Channel Link (or 'off' to disable):", | |
| "admins": "Send a comma-separated list of Admin User IDs (e.g. 12345, 67890):", | |
| "db_channels": "Send a comma-separated list of DB Channel IDs (e.g. -100123, -100456):", | |
| "log_channel": "Send the new User Search Logs Channel ID or username (or 'off' to disable):", | |
| "how_to_download": "Send the new How to Download/Verify URL (or 'off' to disable):", | |
| "announcement_channel": "Send the new Upload Announcements Channel ID (e.g. -100123456) or 'off' to disable:", | |
| "newsletter_days": "Send the days of week separated by comma (e.g. Friday,Sunday):", | |
| "newsletter_time": "Send the new Send Time in 24-hour HH:MM format (e.g. 20:00):", | |
| "newsletter_count": "Send the maximum number of new uploads to include in the newsletter (e.g. 10):" | |
| } | |
| prompt = "Send the new details (format: `domain api_key tutorial_url`):" if action.startswith("edit_rotator_shortener") or action == "add_rotator_shortener" else prompt_map.get(action, "Send the new parameter value:") | |
| from pyrogram.types import InlineKeyboardMarkup, InlineKeyboardButton | |
| category_map = { | |
| "add_rotator_shortener": "rot", | |
| "auto_delete": "gen", | |
| "custom_caption": "cap", | |
| "caption_uname": "cap", | |
| "shortener_site": "sho", | |
| "shortener_api": "sho", | |
| "token_timeout": "sho", | |
| "force_sub": "cha", | |
| "channel_link": "cha", | |
| "admins": "cha", | |
| "db_channels": "cha", | |
| "log_channel": "cha", | |
| "announcement_channel": "cha", | |
| "how_to_download": "gen", | |
| "newsletter_days": "newsl", | |
| "newsletter_time": "newsl", | |
| "newsletter_count": "newsl" | |
| } | |
| cat = "rot" if action.startswith("edit_rotator_shortener") else category_map.get(action, "main") | |
| keyboard = InlineKeyboardMarkup([[InlineKeyboardButton("❌ Cancel Configuration", callback_data=f"adm_cat:{cat}")]]) | |
| await query.message.edit_text(text=f"⌨️ **{prompt}**", reply_markup=keyboard) | |
| await query.answer() | |
| # Schedule the auto-cancel background task | |
| async def cancel_after_timeout(b, u_id, msg_id, cat_name): | |
| await asyncio.sleep(90) # 1.30 minutes | |
| state = ADMIN_INPUT_STATE.get(u_id) | |
| if state and state.get("message_id") == msg_id: | |
| import time | |
| elapsed = time.time() - state.get("timestamp", 0) | |
| if elapsed >= 90: | |
| ADMIN_INPUT_STATE.pop(u_id, None) | |
| try: | |
| text, keyboard = await get_settings_screen(cat_name) | |
| alert_text = "⚠️ **Operation cancelled due to timeout (1.30 min)!**\n\n" + text | |
| await b.edit_message_text( | |
| chat_id=u_id, | |
| message_id=msg_id, | |
| text=alert_text, | |
| reply_markup=keyboard | |
| ) | |
| except Exception: | |
| pass | |
| else: | |
| asyncio.create_task(cancel_after_timeout_loop(b, u_id, msg_id, cat_name)) | |
| async def cancel_after_timeout_loop(b, u_id, msg_id, cat_name): | |
| state = ADMIN_INPUT_STATE.get(u_id) | |
| if state and state.get("message_id") == msg_id: | |
| import time | |
| elapsed = time.time() - state.get("timestamp", 0) | |
| remaining = 90 - elapsed | |
| if remaining > 0: | |
| await asyncio.sleep(remaining) | |
| state = ADMIN_INPUT_STATE.get(u_id) | |
| if state and state.get("message_id") == msg_id: | |
| elapsed = time.time() - state.get("timestamp", 0) | |
| if elapsed >= 90: | |
| ADMIN_INPUT_STATE.pop(u_id, None) | |
| try: | |
| text, keyboard = await get_settings_screen(cat_name) | |
| alert_text = "⚠️ **Operation cancelled due to timeout (1.30 min)!**\n\n" + text | |
| await b.edit_message_text( | |
| chat_id=u_id, | |
| message_id=msg_id, | |
| text=alert_text, | |
| reply_markup=keyboard | |
| ) | |
| except Exception: | |
| pass | |
| asyncio.create_task(cancel_after_timeout(bot, user_id, query.message.id, cat)) | |
| async def handle_admin_input(bot, message): | |
| user_id = message.from_user.id | |
| state = ADMIN_INPUT_STATE.get(user_id) | |
| if not state: | |
| return | |
| action = state.get("action") | |
| menu_message_id = state.get("message_id") | |
| input_text = message.text.strip() | |
| try: | |
| await message.delete() | |
| except Exception: | |
| pass | |
| from mfinder.db.settings_sql import ( | |
| set_auto_delete, set_custom_caption, set_force_sub, set_username, set_channel_link, | |
| set_shortener_settings, update_admins_list, update_db_channels_list, get_link, | |
| set_token_timeout, set_log_channel, set_how_to_download | |
| ) | |
| from mfinder import ADMINS, DB_CHANNELS | |
| success = True | |
| err_msg = "" | |
| success_msg = "" | |
| if action == "add_rotator_shortener": | |
| parts = input_text.split(None, 2) | |
| if len(parts) != 3: | |
| success = False | |
| err_msg = "Please send domain name, API key, and How to Download URL separated by spaces (e.g. `gplinks.in API_KEY TUTORIAL_URL`)." | |
| else: | |
| site = parts[0].strip() | |
| api = parts[1].strip() | |
| how_to_download = parts[2].strip() | |
| if " " in site or "." not in site: | |
| success = False | |
| err_msg = "Invalid domain name. It should not contain spaces and must contain at least one dot (e.g. gplinks.in)." | |
| elif not how_to_download.startswith("http://") and not how_to_download.startswith("https://") and not how_to_download.startswith("t.me/") and not how_to_download.startswith("telegram.me/"): | |
| success = False | |
| err_msg = "Invalid How to Download URL. It must start with http://, https://, t.me/, or telegram.me/." | |
| else: | |
| import json | |
| admin_settings = await get_admin_settings() | |
| shorteners = [] | |
| if hasattr(admin_settings, "shorteners_list") and admin_settings.shorteners_list: | |
| try: | |
| shorteners = json.loads(admin_settings.shorteners_list) | |
| except Exception: | |
| pass | |
| # Check if already exists | |
| exists = False | |
| for sh in shorteners: | |
| if sh.get("site").lower() == site.lower(): | |
| sh["api"] = api | |
| sh["how_to_download"] = how_to_download | |
| sh["status"] = True | |
| exists = True | |
| break | |
| if not exists: | |
| shorteners.append({"site": site, "api": api, "how_to_download": how_to_download, "status": True}) | |
| from mfinder.db.settings_sql import update_shorteners_list | |
| await update_shorteners_list(json.dumps(shorteners)) | |
| try: | |
| from mfinder.db.settings_sql import clear_all_user_shortener_usages | |
| await clear_all_user_shortener_usages() | |
| except Exception: | |
| pass | |
| from mfinder.utils.helpers import clear_short_url_cache | |
| clear_short_url_cache() | |
| success_msg = f"Added shortener `{site}` to the rotator list." | |
| elif action.startswith("edit_rotator_shortener:"): | |
| idx = int(action.split(":")[1]) | |
| parts = input_text.split(None, 2) | |
| if len(parts) != 3: | |
| success = False | |
| err_msg = "Please send domain name, API key, and How to Download URL separated by spaces (e.g. `gplinks.in API_KEY TUTORIAL_URL`)." | |
| else: | |
| site = parts[0].strip() | |
| api = parts[1].strip() | |
| how_to_download = parts[2].strip() | |
| if " " in site or "." not in site: | |
| success = False | |
| err_msg = "Invalid domain name. It should not contain spaces and must contain at least one dot (e.g. gplinks.in)." | |
| elif not how_to_download.startswith("http://") and not how_to_download.startswith("https://") and not how_to_download.startswith("t.me/") and not how_to_download.startswith("telegram.me/"): | |
| success = False | |
| err_msg = "Invalid How to Download URL. It must start with http://, https://, t.me/, or telegram.me/." | |
| else: | |
| import json | |
| admin_settings = await get_admin_settings() | |
| shorteners = get_all_shorteners(admin_settings) | |
| if idx < len(shorteners): | |
| shorteners[idx] = { | |
| "site": site, | |
| "api": api, | |
| "how_to_download": how_to_download, | |
| "status": shorteners[idx].get("status", True) | |
| } | |
| from mfinder.db.settings_sql import update_shorteners_list | |
| await update_shorteners_list(json.dumps(shorteners)) | |
| try: | |
| from mfinder.db.settings_sql import clear_all_user_shortener_usages | |
| await clear_all_user_shortener_usages() | |
| except Exception: | |
| pass | |
| from mfinder.utils.helpers import clear_short_url_cache | |
| clear_short_url_cache() | |
| success_msg = f"Updated shortener #{idx+1} (`{site}`) successfully." | |
| else: | |
| success = False | |
| err_msg = "Shortener not found." | |
| elif action == "set_auto_delete": | |
| dur = parse_duration_to_seconds(input_text) | |
| if dur is None or dur < 0: | |
| success = False | |
| err_msg = "Invalid format. Use e.g. '1h' (1 hour), '2m' (2 minutes), '30s' (30 seconds), or 'off'." | |
| else: | |
| await set_auto_delete(dur) | |
| if dur: | |
| if dur >= 86400: | |
| desc = f"{dur / 86400:.1f} days" if (dur / 86400) % 1 != 0 else f"{int(dur / 86400)} days" | |
| elif dur >= 3600: | |
| desc = f"{dur / 3600:.1f} hours" if (dur / 3600) % 1 != 0 else f"{int(dur / 3600)} hours" | |
| elif dur >= 60: | |
| desc = f"{dur / 60:.1f} minutes" if (dur / 60) % 1 != 0 else f"{int(dur / 60)} minutes" | |
| else: | |
| desc = f"{dur} seconds" | |
| success_msg = f"Auto delete duration set to `{dur}` seconds ({desc})." | |
| else: | |
| success_msg = "Auto delete duration has been disabled." | |
| elif action == "set_custom_caption": | |
| if input_text.lower() in ("off", "disable", "disabled"): | |
| caption = None | |
| success_msg = "Custom caption has been disabled." | |
| else: | |
| caption = input_text | |
| success_msg = "Custom caption updated successfully." | |
| await set_custom_caption(caption) | |
| elif action == "set_caption_uname": | |
| if input_text.lower() in ("off", "disable", "disabled"): | |
| uname = None | |
| success_msg = "Caption username has been disabled." | |
| await set_username(uname) | |
| elif input_text.startswith("@"): | |
| uname = input_text | |
| success_msg = f"Caption username set to `{uname}`." | |
| await set_username(uname) | |
| else: | |
| success = False | |
| err_msg = "Must start with '@' (e.g. @mychannel) or 'off' to disable." | |
| elif action == "set_shortener_site": | |
| if input_text.lower() in ("off", "disable", "disabled"): | |
| site = None | |
| success_msg = "Shortener site has been cleared/disabled." | |
| await set_shortener_settings(site=site) | |
| elif " " in input_text or "." not in input_text: | |
| success = False | |
| err_msg = "Invalid domain name. It should not contain spaces and must contain at least one dot (e.g., shortxlinks.com)." | |
| else: | |
| site = input_text | |
| success_msg = f"Shortener site updated successfully to `{site}`." | |
| await set_shortener_settings(site=site) | |
| elif action == "set_shortener_api": | |
| if input_text.lower() in ("off", "disable", "disabled"): | |
| api = None | |
| success_msg = "Shortener API key has been cleared/disabled." | |
| await set_shortener_settings(api=api) | |
| elif not input_text: | |
| success = False | |
| err_msg = "API key cannot be empty." | |
| else: | |
| api = input_text | |
| success_msg = "Shortener API key updated successfully." | |
| await set_shortener_settings(api=api) | |
| elif action == "set_force_sub": | |
| if input_text.lower() in ("off", "disable", "disabled", "0"): | |
| await set_channel_link(None) | |
| await set_force_sub(0) | |
| success_msg = "Force subscription channel has been disabled." | |
| else: | |
| try: | |
| resolved_ids, links_list = await parse_and_set_forcesub(bot, input_text) | |
| success_msg = f"Force subscription channels set to: `{', '.join(resolved_ids)}`. Links: {', '.join(links_list)}" | |
| except Exception as e: | |
| success = False | |
| err_msg = f"Could not find or access channels: {str(e)}. Make sure the channels exist and the bot is added as an administrator." | |
| elif action == "set_channel_link": | |
| if input_text.lower() in ("off", "disable", "disabled"): | |
| await set_channel_link(None) | |
| success_msg = "Channel invite link has been removed." | |
| elif not (input_text.startswith("http://") or input_text.startswith("https://")): | |
| success = False | |
| err_msg = "Invalid URL. The link must start with 'http://' or 'https://'." | |
| else: | |
| await set_channel_link(input_text) | |
| success_msg = f"Channel invite link set to `{input_text}`." | |
| elif action == "set_admins": | |
| try: | |
| raw_ids = input_text.split(",") | |
| parsed_ids = [] | |
| is_list_valid = True | |
| invalid_val = "" | |
| from mfinder import id_pattern | |
| for r_id in raw_ids: | |
| r_id = r_id.strip() | |
| if not r_id: | |
| continue | |
| if id_pattern.search(r_id): | |
| parsed_ids.append(int(r_id)) | |
| else: | |
| is_list_valid = False | |
| invalid_val = r_id | |
| break | |
| if not is_list_valid: | |
| success = False | |
| err_msg = f"Invalid Admin ID `{invalid_val}`. Admins list must contain only comma-separated integer IDs." | |
| elif not parsed_ids: | |
| success = False | |
| err_msg = "Admins list cannot be empty." | |
| else: | |
| from mfinder import OWNER_ID | |
| if OWNER_ID not in parsed_ids: | |
| parsed_ids.append(OWNER_ID) | |
| admins_str = ",".join(str(i) for i in parsed_ids) | |
| await update_admins_list(admins_str) | |
| ADMINS.clear() | |
| ADMINS.extend(parsed_ids) | |
| success_msg = f"Admins list updated successfully with {len(parsed_ids)} admins." | |
| except Exception as e: | |
| success = False | |
| err_msg = f"Error parsing admins: {e}" | |
| elif action == "set_db_channels": | |
| try: | |
| raw_ids = input_text.split(",") | |
| parsed_ids = [] | |
| is_list_valid = True | |
| invalid_val = "" | |
| from mfinder import id_pattern | |
| for r_id in raw_ids: | |
| r_id = r_id.strip() | |
| if not r_id: | |
| continue | |
| if id_pattern.search(r_id): | |
| parsed_ids.append(int(r_id)) | |
| else: | |
| is_list_valid = False | |
| invalid_val = r_id | |
| break | |
| if not is_list_valid: | |
| success = False | |
| err_msg = f"Invalid Channel ID `{invalid_val}`. DB Channels list must contain only comma-separated integer IDs." | |
| else: | |
| channels_str = ",".join(str(i) for i in parsed_ids) | |
| await update_db_channels_list(channels_str) | |
| DB_CHANNELS.clear() | |
| DB_CHANNELS.extend(parsed_ids) | |
| success_msg = f"Database channels list updated successfully with {len(parsed_ids)} channels." | |
| except Exception as e: | |
| success = False | |
| err_msg = f"Error parsing DB channels: {e}" | |
| elif action == "set_token_timeout": | |
| val = input_text.lower().strip() | |
| parsed_seconds = parse_duration_to_seconds(val) | |
| if parsed_seconds is None or parsed_seconds <= 0: | |
| success = False | |
| err_msg = "Invalid format. Use e.g. '1h' (1 hour), '30m' (30 minutes), or a positive number of seconds." | |
| else: | |
| await set_token_timeout(parsed_seconds) | |
| if parsed_seconds >= 86400: | |
| days = parsed_seconds / 86400 | |
| time_desc = f"{days:.1f} days" if days % 1 != 0 else f"{int(days)} days" | |
| elif parsed_seconds >= 3600: | |
| hours = parsed_seconds / 3600 | |
| time_desc = f"{hours:.1f} hours" if hours % 1 != 0 else f"{int(hours)} hours" | |
| elif parsed_seconds >= 60: | |
| mins = parsed_seconds / 60 | |
| time_desc = f"{mins:.1f} minutes" if mins % 1 != 0 else f"{int(mins)} minutes" | |
| else: | |
| time_desc = f"{parsed_seconds} seconds" | |
| success_msg = f"Token validity duration set to `{parsed_seconds}` seconds ({time_desc})." | |
| elif action == "set_log_channel": | |
| if input_text.lower() in ("off", "disable", "disabled", "0"): | |
| await set_log_channel(None) | |
| success_msg = "User search logging channel has been disabled." | |
| else: | |
| channel_str = input_text | |
| is_valid_id = False | |
| if channel_str.startswith("@") and len(channel_str) > 1: | |
| is_valid_id = True | |
| channel_param = channel_str | |
| else: | |
| try: | |
| channel_param = int(channel_str) | |
| is_valid_id = True | |
| except ValueError: | |
| pass | |
| if not is_valid_id: | |
| success = False | |
| err_msg = "Must be a valid integer ID (e.g., -100123456789) or username starting with '@'." | |
| else: | |
| try: | |
| chat = await bot.get_chat(channel_param) | |
| access_hash = None | |
| try: | |
| resolved = await bot.resolve_peer(chat.id) | |
| if getattr(resolved, "access_hash", None): | |
| access_hash = resolved.access_hash | |
| except Exception: | |
| pass | |
| await set_log_channel(chat.id, access_hash=access_hash) | |
| success_msg = f"User search logging channel set to `{chat.id}` ({chat.title or 'Channel'})." | |
| except Exception as e: | |
| success = False | |
| err_msg = f"Could not find or access the channel: {str(e)}. Make sure the channel exists and the bot is added as an administrator." | |
| elif action == "set_how_to_download": | |
| if input_text.lower() in ("off", "disable", "disabled"): | |
| await set_how_to_download(None) | |
| success_msg = "How to Download/Verify URL has been disabled/removed." | |
| elif not (input_text.startswith("http://") or input_text.startswith("https://")): | |
| success = False | |
| err_msg = "Invalid URL. The link must start with 'http://' or 'https://'." | |
| else: | |
| await set_how_to_download(input_text) | |
| success_msg = f"How to Download URL set to `{input_text}`." | |
| elif action == "set_newsletter_days": | |
| valid_days = {"monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday"} | |
| input_days = [d.strip().lower() for d in input_text.split(",") if d.strip()] | |
| if not input_days or any(d not in valid_days for d in input_days): | |
| success = False | |
| err_msg = "Must be a comma-separated list of valid weekdays (e.g. Friday,Sunday)." | |
| else: | |
| days_str = ",".join(d.capitalize() for d in input_days) | |
| from mfinder.db.settings_sql import set_newsletter_settings | |
| await set_newsletter_settings(days=days_str) | |
| success_msg = f"Newsletter days updated to `{days_str}`." | |
| elif action == "set_newsletter_time": | |
| import re | |
| if not re.match(r"^(0\d|1\d|2[0-3]):[0-5]\d$", input_text.strip()): | |
| success = False | |
| err_msg = "Must be in valid 24-hour HH:MM format (e.g. 20:00 or 08:30)." | |
| else: | |
| time_str = input_text.strip() | |
| from mfinder.db.settings_sql import set_newsletter_settings | |
| await set_newsletter_settings(time_str=time_str) | |
| success_msg = f"Newsletter send time set to `{time_str}`." | |
| elif action == "set_newsletter_count": | |
| try: | |
| val = int(input_text.strip()) | |
| if val <= 0 or val > 100: | |
| raise ValueError | |
| from mfinder.db.settings_sql import set_newsletter_settings | |
| await set_newsletter_settings(count=val) | |
| success_msg = f"Newsletter max movie count set to `{val}`." | |
| except ValueError: | |
| success = False | |
| err_msg = "Must be a positive integer between 1 and 100." | |
| elif action == "set_announcement_channel": | |
| if input_text.lower() == "off": | |
| from mfinder.db.settings_sql import set_announcement_settings | |
| await set_announcement_settings(channel="off") | |
| success_msg = "Upload Announcements Channel disabled." | |
| else: | |
| try: | |
| chat = await bot.get_chat(input_text) | |
| if chat.type.value not in ("channel", "supergroup"): | |
| success = False | |
| err_msg = "Target must be a channel or supergroup." | |
| else: | |
| access_hash = None | |
| try: | |
| resolved = await bot.resolve_peer(chat.id) | |
| if getattr(resolved, "access_hash", None): | |
| access_hash = resolved.access_hash | |
| except Exception: | |
| pass | |
| from mfinder.db.settings_sql import set_announcement_settings | |
| await set_announcement_settings(channel=chat.id, access_hash=access_hash) | |
| success_msg = f"Upload Announcements Channel set to `{chat.title}` ({chat.id}) with access hash resolved!" | |
| except Exception as e: | |
| success = False | |
| err_msg = f"Failed to resolve channel '{input_text}': {e}. Make sure the bot is an admin/member in the channel." | |
| else: | |
| success = False | |
| err_msg = "Unknown action." | |
| from pyrogram.types import InlineKeyboardMarkup, InlineKeyboardButton | |
| category_map = { | |
| "set_auto_delete": "gen", | |
| "set_custom_caption": "cap", | |
| "set_caption_uname": "cap", | |
| "set_shortener_site": "sho", | |
| "set_shortener_api": "sho", | |
| "set_token_timeout": "sho", | |
| "set_force_sub": "cha", | |
| "set_channel_link": "cha", | |
| "set_admins": "cha", | |
| "set_db_channels": "cha", | |
| "set_log_channel": "cha", | |
| "set_announcement_channel": "cha", | |
| "set_how_to_download": "gen", | |
| "set_newsletter_days": "newsl", | |
| "set_newsletter_time": "newsl", | |
| "set_newsletter_count": "newsl" | |
| } | |
| cat = "rot" if action.startswith("edit_rotator_shortener") or action == "add_rotator_shortener" else category_map.get(action, "main") | |
| if success: | |
| ADMIN_INPUT_STATE.pop(user_id, None) | |
| keyboard = InlineKeyboardMarkup([ | |
| [InlineKeyboardButton("⬅️ Back", callback_data=f"adm_cat:{cat}")] | |
| ]) | |
| text_to_send = f"✅ {success_msg}\n\nClick back to configure other settings." | |
| else: | |
| keyboard = InlineKeyboardMarkup([ | |
| [InlineKeyboardButton("❌ Cancel Configuration", callback_data=f"adm_cat:{cat}")] | |
| ]) | |
| prompt_map = { | |
| "edit_rotator_shortener": "Send the new details (format: `domain api_key tutorial_url`):", | |
| "set_auto_delete": "Send the new Auto Delete duration in seconds (or 'off' to disable):", | |
| "set_custom_caption": "Send the new Custom Caption text (or 'off' to disable):", | |
| "set_caption_uname": "Send the new Caption Username starting with '@' (or 'off' to disable):", | |
| "set_shortener_site": "Send the new Site name (e.g. shortxlinks.com):", | |
| "set_shortener_api": "Send the new Shortener API Key:", | |
| "set_token_timeout": "Send the new Token validity time (e.g., '1h' for 1 hour, '30m' for 30 mins, or raw seconds):", | |
| "set_force_sub": "Send the new Force Subscription Channel ID (or 'off' to disable):", | |
| "set_channel_link": "Send the new Force Subscription Channel Link (or 'off' to disable):", | |
| "set_admins": "Send a comma-separated list of Admin User IDs (e.g. 12345, 67890):", | |
| "set_db_channels": "Send a comma-separated list of DB Channel IDs (e.g. -100123, -100456):", | |
| "set_log_channel": "Send the new User Search Logs Channel ID or username (or 'off' to disable):", | |
| "set_how_to_download": "Send the new How to Download/Verify URL (or 'off' to disable):", | |
| "set_announcement_channel": "Send the new Upload Announcements Channel ID (e.g. -100123456) or 'off' to disable:", | |
| "set_newsletter_days": "Send the days of week separated by comma (e.g. Friday,Sunday):", | |
| "set_newsletter_time": "Send the new Send Time in 24-hour HH:MM format (e.g. 20:00):", | |
| "set_newsletter_count": "Send the maximum number of new uploads to include in the newsletter (e.g. 10):" | |
| } | |
| if action.startswith("edit_rotator_shortener") or action == "add_rotator_shortener": | |
| original_prompt = "Send the details (format: `domain api_key tutorial_url`):" | |
| else: | |
| original_prompt = prompt_map.get(action, "Send the new parameter value:") | |
| import time | |
| text_to_send = f"⚠️ **Invalid input:** `{err_msg}`\n\n⌨️ **{original_prompt}**" + ("\u200b" * int(time.time() % 5)) | |
| from pyrogram.errors import MessageNotModified | |
| try: | |
| await bot.edit_message_text( | |
| chat_id=user_id, | |
| message_id=menu_message_id, | |
| text=text_to_send, | |
| reply_markup=keyboard | |
| ) | |
| except MessageNotModified: | |
| pass | |
| except Exception: | |
| try: | |
| sent_msg = await bot.send_message(user_id, text_to_send, reply_markup=keyboard) | |
| if not success and state: | |
| state["message_id"] = sent_msg.id | |
| import time | |
| state["timestamp"] = time.time() | |
| except Exception: | |
| pass | |
| async def banuser(bot, update): | |
| data = update.text.split() | |
| if len(data) == 2: | |
| user_id = data[-1] | |
| banned = await is_banned(int(user_id)) | |
| if not banned: | |
| await ban_user(int(user_id)) | |
| await update.reply_text(f"User {user_id} banned") | |
| else: | |
| await update.reply_text(f"User {user_id} is already banned") | |
| else: | |
| await update.reply_text("Please send in proper format `/ban user_id`") | |
| async def unbanuser(bot, update): | |
| data = update.text.split() | |
| if len(data) == 2: | |
| user_id = data[-1] | |
| banned = await is_banned(int(user_id)) | |
| if banned: | |
| await unban_user(int(user_id)) | |
| await update.reply_text(f"User {user_id} unbanned") | |
| else: | |
| await update.reply_text(f"User {user_id} is not in ban list") | |
| else: | |
| await update.reply_text("Please send in proper format `/unban user_id`") | |
| async def addfilter(bot, update): | |
| data = shlex.split(update.text) | |
| if len(data) >= 3: | |
| fltr = data[1].strip('"').lower() | |
| message = " ".join(data[2:]) | |
| add = await add_filter(fltr, message) | |
| if add: | |
| await update.reply_text(f"Filter `{fltr}` added") | |
| else: | |
| await update.reply_text(f"Filter `{fltr}` already exists") | |
| else: | |
| await update.reply_text( | |
| "Please send in proper format `/addfilter filter message`" | |
| ) | |
| async def delfilter(bot, update): | |
| data = update.text.split() | |
| if len(data) >= 2: | |
| fltr = " ".join(data[1:]) | |
| rem = await rem_filter(fltr) | |
| if rem: | |
| await update.reply_text(f"Filter `{fltr}` removed") | |
| else: | |
| await update.reply_text(f"Filter `{fltr}` not found") | |
| else: | |
| await update.reply_text("Please send in proper format `/delfilter filter`") | |
| async def list_filter(bot, update): | |
| fltr = await list_filters() | |
| fltr_msg = "" | |
| if fltr: | |
| for fltrs in fltr: | |
| fltr_msg += "\n" + "`" + fltrs + "`" | |
| await update.reply_text(f"**Available Filters:** {fltr_msg}") | |
| else: | |
| await update.reply_text("No filters found") | |
| async def force_sub(bot, update): | |
| data = update.text.split() | |
| if len(data) == 2: | |
| channel_str = data[-1] | |
| if channel_str.lower() == "off": | |
| await set_channel_link(None) | |
| await set_force_sub(0) | |
| await update.reply_text("Force Subscription disabled") | |
| return | |
| try: | |
| resolved_ids, links_list = await parse_and_set_forcesub(bot, channel_str) | |
| await update.reply_text(f"Force subscription channels set to: `{', '.join(resolved_ids)}`. Links: {', '.join(links_list)}") | |
| except Exception as e: | |
| await update.reply_text( | |
| f"❌ Error setting force subscription: {str(e)}. Make sure the channels exist and the bot is added as an administrator." | |
| ) | |
| else: | |
| await update.reply_text( | |
| "Please send in proper format `/forcesub channel_id/off`" | |
| ) | |
| async def log_channel_(bot, update): | |
| data = update.text.split() | |
| if len(data) == 2: | |
| channel_str = data[-1] | |
| if channel_str.lower() == "off": | |
| await set_log_channel(None) | |
| await update.reply_text("User search logging disabled.") | |
| return | |
| try: | |
| if channel_str.startswith("@"): | |
| channel_param = channel_str | |
| else: | |
| channel_param = int(channel_str) | |
| chat = await bot.get_chat(channel_param) | |
| access_hash = None | |
| try: | |
| resolved = await bot.resolve_peer(chat.id) | |
| if getattr(resolved, "access_hash", None): | |
| access_hash = resolved.access_hash | |
| except Exception: | |
| pass | |
| await set_log_channel(chat.id, access_hash=access_hash) | |
| await update.reply_text(f"User search logging channel set to `{chat.id}` ({chat.title or 'Channel'}).") | |
| except Exception as e: | |
| await update.reply_text( | |
| f"❌ Error setting log channel: {str(e)}. Make sure the channel exists and the bot is added as an administrator." | |
| ) | |
| else: | |
| await update.reply_text( | |
| "Please send in proper format `/logchannel channel_id/off`" | |
| ) | |
| async def testlink(bot, update): | |
| link = await get_link() | |
| if link: | |
| await update.reply_text(f"Invite link for force subscription channel: {link}") | |
| else: | |
| await update.reply_text( | |
| "Force Subscription is disabled, please enable it first" | |
| ) | |
| async def caption_username(bot, update): | |
| data = update.text.split() | |
| if len(data) == 2: | |
| username = data[-1] | |
| if username.lower() == "off": | |
| username = 0 | |
| elif username.startswith("@"): | |
| username = username | |
| else: | |
| await update.reply_text("This is not a username, please check.") | |
| return | |
| await set_username(username) | |
| if username: | |
| await update.reply_text(f"File caption username set to `{username}`") | |
| else: | |
| await update.reply_text("File caption username disabled") | |
| else: | |
| await update.reply_text( | |
| "Please send in proper format `/setusername username/off`" | |
| ) | |
| async def count_f(bot, update): | |
| count = await count_files() | |
| await update.reply_text(f"**Total no. of files in DB:** `{count}`") | |
| async def parse_and_set_forcesub(bot, channel_str): | |
| channels = [ch.strip() for ch in channel_str.split(',') if ch.strip()] | |
| resolved_ids = [] | |
| hashes_map = {} | |
| links_list = [] | |
| for ch in channels: | |
| try: | |
| ch_param = int(ch) | |
| except ValueError: | |
| ch_param = ch if ch.startswith("@") else f"@{ch}" | |
| chat = await bot.get_chat(ch_param) | |
| resolved_ids.append(str(chat.id)) | |
| # Resolve access hash | |
| access_hash = None | |
| try: | |
| resolved = await bot.resolve_peer(chat.id) | |
| if getattr(resolved, "access_hash", None): | |
| access_hash = resolved.access_hash | |
| except Exception: | |
| pass | |
| if access_hash is not None: | |
| hashes_map[str(chat.id)] = access_hash | |
| # Link resolving | |
| link = None | |
| try: | |
| link_obj = await bot.create_chat_invite_link(chat.id) | |
| link = link_obj.invite_link | |
| except Exception: | |
| if chat.username: | |
| link = f"https://t.me/{chat.username}" | |
| if link: | |
| links_list.append(link) | |
| if not resolved_ids: | |
| raise ValueError("No valid channels could be resolved.") | |
| from mfinder.db.settings_sql import set_force_sub_channels, set_channel_link | |
| await set_force_sub_channels(",".join(resolved_ids), hashes_map) | |
| if links_list: | |
| await set_channel_link(links_list[0]) | |
| return resolved_ids, links_list | |
| async def verify_user_token(bot, update, param): | |
| import time | |
| import asyncio | |
| from mfinder import LOGGER | |
| from mfinder.db.settings_sql import get_user_token, activate_user_token, get_admin_settings, prune_all_expired_tokens | |
| # Clean up any other expired or stale tokens | |
| await prune_all_expired_tokens() | |
| user_id = update.from_user.id | |
| token_str = param | |
| tok_rec = await get_user_token(user_id) | |
| if not tok_rec or tok_rec.token != token_str: | |
| if tok_rec and tok_rec.req_msg_id: | |
| try: | |
| await bot.delete_messages(chat_id=user_id, message_ids=int(tok_rec.req_msg_id)) | |
| except Exception as e: | |
| LOGGER.warning(f"Failed to delete original token request message: {e}") | |
| err_msg = await update.reply_text( | |
| "❌ **Invalid token link!**\n\nPlease search for a movie again to generate a new verification link.", | |
| quote=True | |
| ) | |
| async def delete_after_delay(b, u_id, err_msg_id, incoming_msg_id): | |
| await asyncio.sleep(10) | |
| try: | |
| await b.delete_messages(chat_id=u_id, message_ids=[err_msg_id, incoming_msg_id]) | |
| except Exception: | |
| pass | |
| asyncio.create_task(delete_after_delay(bot, user_id, err_msg.id, update.id)) | |
| return | |
| # Check if the token has already been claimed/activated | |
| if tok_rec.status == "active": | |
| if tok_rec.target_file_id: | |
| update.text = f"/start {tok_rec.target_file_id}" | |
| from mfinder.plugins.serve import get_files | |
| await get_files(bot, update) | |
| return | |
| elif tok_rec.search_query: | |
| from mfinder.plugins.serve import filter_ | |
| update.text = tok_rec.search_query | |
| await filter_(bot, update) | |
| return | |
| else: | |
| await update.reply_text( | |
| "✅ **Token Verified Successfully!**\n\nYou already have direct, ad-free access to all movies and files.", | |
| quote=True | |
| ) | |
| return | |
| current_time = time.time() | |
| if tok_rec.status == "pending" and (current_time - float(tok_rec.created_at)) > 1800: | |
| if tok_rec.req_msg_id: | |
| try: | |
| await bot.delete_messages(chat_id=user_id, message_ids=int(tok_rec.req_msg_id)) | |
| except Exception as e: | |
| LOGGER.warning(f"Failed to delete original token request message: {e}") | |
| from mfinder.db.settings_sql import delete_user_token | |
| await delete_user_token(user_id) | |
| err_msg = await update.reply_text( | |
| "❌ **Verification link has expired!**\n\nThe link is only valid to be claimed within 30 minutes of generation. Please search for a movie again to get a new link.", | |
| quote=True | |
| ) | |
| async def delete_after_delay(b, u_id, err_msg_id, incoming_msg_id): | |
| await asyncio.sleep(10) | |
| try: | |
| await b.delete_messages(chat_id=u_id, message_ids=[err_msg_id, incoming_msg_id]) | |
| except Exception: | |
| pass | |
| asyncio.create_task(delete_after_delay(bot, user_id, err_msg.id, update.id)) | |
| return | |
| admin_settings = await get_admin_settings() | |
| timeout = float(admin_settings.token_timeout) if admin_settings and admin_settings.token_timeout else 3600 | |
| expires_at = current_time + timeout | |
| success = await activate_user_token(user_id, expires_at) | |
| if success: | |
| try: | |
| from mfinder.db.settings_sql import advance_user_shortener_index | |
| await advance_user_shortener_index(user_id) | |
| except Exception as rot_err: | |
| LOGGER.warning(f"Error advancing smart rotator index: {rot_err}") | |
| if not success: | |
| await update.reply_text( | |
| "❌ **Verification failed due to database error!** Please try again later.", | |
| quote=True | |
| ) | |
| return | |
| if timeout >= 3600: | |
| hours = timeout / 3600 | |
| time_desc = f"{hours:.1f} hours" if hours % 1 != 0 else f"{int(hours)} hours" | |
| elif timeout >= 60: | |
| mins = timeout / 60 | |
| time_desc = f"{mins:.1f} minutes" if mins % 1 != 0 else f"{int(mins)} minutes" | |
| else: | |
| time_desc = f"{timeout} seconds" | |
| if tok_rec.req_msg_id: | |
| try: | |
| await bot.delete_messages(chat_id=user_id, message_ids=int(tok_rec.req_msg_id)) | |
| except Exception as e: | |
| LOGGER.warning(f"Failed to delete original token request message: {e}") | |
| verified_msg = await update.reply_text( | |
| f"✅ **Token Verified Successfully!**\n\nYou now have direct, ad-free access to all movies and files for the next **{time_desc}**.\n\nEnjoy!", | |
| quote=True | |
| ) | |
| async def delete_after_delay(b, u_id, success_msg_id, incoming_msg_id): | |
| await asyncio.sleep(10) | |
| try: | |
| await b.delete_messages(chat_id=u_id, message_ids=[success_msg_id, incoming_msg_id]) | |
| except Exception as e: | |
| LOGGER.warning(f"Failed to auto-delete verification messages: {e}") | |
| asyncio.create_task(delete_after_delay(bot, user_id, verified_msg.id, update.id)) | |
| search_q = tok_rec.search_query | |
| target_f_id = tok_rec.target_file_id | |
| if search_q: | |
| try: | |
| from mfinder.plugins.serve import filter_ | |
| update.text = search_q | |
| await asyncio.sleep(0.5) | |
| await filter_(bot, update) | |
| except Exception as e: | |
| LOGGER.warning(f"Error auto-resuming search query '{search_q}': {e}") | |
| elif target_f_id: | |
| try: | |
| from mfinder.plugins.serve import get_files | |
| update.text = f"/start {target_f_id}" | |
| await asyncio.sleep(0.5) | |
| await get_files(bot, update) | |
| except Exception as e: | |
| LOGGER.warning(f"Error auto-resuming file download for '{target_f_id}': {e}") | |
| async def admin_rot_toggle_smart_callback(bot, query): | |
| from mfinder.db.settings_sql import get_admin_settings, set_smart_rotator_state, clear_all_user_shortener_usages | |
| admin_settings = await get_admin_settings() | |
| new_state = not getattr(admin_settings, "smart_rotator", False) | |
| await set_smart_rotator_state(new_state) | |
| try: | |
| from mfinder.db.settings_sql import clear_all_user_shortener_usages | |
| await clear_all_user_shortener_usages() | |
| except Exception: | |
| pass | |
| from mfinder.utils.helpers import clear_short_url_cache | |
| clear_short_url_cache() | |
| await query.answer(f"Smart Rotator {'ON' if new_state else 'OFF'}") | |
| text, keyboard = await get_settings_screen("sho") | |
| try: | |
| await query.message.edit_text(text=text, reply_markup=keyboard) | |
| except Exception: | |
| pass | |
| async def admin_rot_manage_callback(bot, query): | |
| idx = int(query.data.split(":")[1]) | |
| text, keyboard = await get_settings_screen(f"rot_manage:{idx}") | |
| try: | |
| await query.message.edit_text(text=text, reply_markup=keyboard) | |
| except Exception: | |
| pass | |
| await query.answer() | |
| async def admin_rot_toggle_callback(bot, query): | |
| idx = int(query.data.split(":")[1]) | |
| import json | |
| from mfinder.db.settings_sql import get_admin_settings, update_shorteners_list | |
| admin_settings = await get_admin_settings() | |
| shorteners = get_all_shorteners(admin_settings) | |
| if idx < len(shorteners): | |
| shorteners[idx]["status"] = not shorteners[idx].get("status", True) | |
| await update_shorteners_list(json.dumps(shorteners)) | |
| try: | |
| from mfinder.db.settings_sql import clear_all_user_shortener_usages | |
| await clear_all_user_shortener_usages() | |
| except Exception: | |
| pass | |
| from mfinder.utils.helpers import clear_short_url_cache | |
| clear_short_url_cache() | |
| await query.answer("Status toggled!") | |
| text, keyboard = await get_settings_screen(f"rot_manage:{idx}") | |
| try: | |
| await query.message.edit_text(text=text, reply_markup=keyboard) | |
| except Exception: | |
| pass | |
| async def admin_rot_edit_callback(bot, query): | |
| idx = int(query.data.split(":")[1]) | |
| user_id = query.from_user.id | |
| from pyrogram.types import InlineKeyboardMarkup, InlineKeyboardButton | |
| ADMIN_INPUT_STATE[user_id] = {"action": f"edit_rotator_shortener:{idx}", "message_id": query.message.id} | |
| keyboard = InlineKeyboardMarkup([[InlineKeyboardButton("❌ Cancel", callback_data=f"adm_rot_manage:{idx}")]]) | |
| await query.message.edit_text( | |
| text=f"⌨️ **Send the new details for Shortener #{idx+1} (format: `domain api_key tutorial_url`):**", | |
| reply_markup=keyboard | |
| ) | |
| await query.answer() | |
| async def admin_rot_delete_callback(bot, query): | |
| idx = int(query.data.split(":")[1]) | |
| import json | |
| from mfinder.db.settings_sql import get_admin_settings, update_shorteners_list | |
| admin_settings = await get_admin_settings() | |
| shorteners = get_all_shorteners(admin_settings) | |
| if idx < len(shorteners): | |
| removed = shorteners.pop(idx) | |
| await update_shorteners_list(json.dumps(shorteners)) | |
| try: | |
| from mfinder.db.settings_sql import clear_all_user_shortener_usages | |
| await clear_all_user_shortener_usages() | |
| except Exception: | |
| pass | |
| from mfinder.utils.helpers import clear_short_url_cache | |
| clear_short_url_cache() | |
| await query.answer(f"Removed {removed.get('site')}!") | |
| text, keyboard = await get_settings_screen("sho") | |
| try: | |
| await query.message.edit_text(text=text, reply_markup=keyboard) | |
| except Exception: | |
| pass | |
| async def admin_rot_clear_callback(bot, query): | |
| from mfinder.db.settings_sql import update_shorteners_list | |
| await update_shorteners_list("[]") | |
| try: | |
| from mfinder.db.settings_sql import clear_all_user_shortener_usages | |
| await clear_all_user_shortener_usages() | |
| except Exception: | |
| pass | |
| from mfinder.utils.helpers import clear_short_url_cache | |
| clear_short_url_cache() | |
| await query.answer("All rotator shorteners cleared!") | |
| text, keyboard = await get_settings_screen("sho") | |
| try: | |
| await query.message.edit_text(text=text, reply_markup=keyboard) | |
| except Exception: | |
| pass | |
| async def admin_rot_add_callback(bot, query): | |
| user_id = query.from_user.id | |
| from pyrogram.types import InlineKeyboardMarkup, InlineKeyboardButton | |
| ADMIN_INPUT_STATE[user_id] = {"action": "add_rotator_shortener", "message_id": query.message.id} | |
| keyboard = InlineKeyboardMarkup([[InlineKeyboardButton("❌ Cancel", callback_data="adm_cat:rot")]]) | |
| await query.message.edit_text( | |
| text="⌨️ **Send the new shortener site, API key, and How to Download URL separated by spaces (e.g. `gplinks.in api_key_here https://t.me/tutorial`):**", | |
| reply_markup=keyboard | |
| ) | |
| await query.answer() | |