diff --git a/.env b/.env new file mode 100644 index 000000000..a4969f8f0 --- /dev/null +++ b/.env @@ -0,0 +1,2 @@ +REDIRECTOR_DOMAIN=sukuna.site +ENCRYPTION_KEY=e2d7c581a9f032b4c6e917d84b2c1f3a5e8d9b0a1f2c3d4e5f6a7b8c9d0e1f2a diff --git a/Dockerfile b/Dockerfile index c3a46a136..4dc5a34fa 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,11 @@ FROM python:3.10 + WORKDIR /app + COPY . . + +RUN pip install --no-cache-dir -r requirements.txt + RUN chmod +x start.sh + CMD ["bash", "start.sh"] diff --git a/bot.py b/bot.py index 8db3a0701..ceeb47191 100644 --- a/bot.py +++ b/bot.py @@ -139,6 +139,13 @@ async def start(self): self.tutorial_link = SHORT_TUT self.shortner_enabled = True + # Load bot settings + try: + self.auto_watermark = await self.mongodb.get_bot_setting('auto_watermark', True) + except Exception as e: + self.LOGGER(__name__, self.name).warning(f"Error loading bot settings: {e}") + self.auto_watermark = True + try: db_channel = await self.get_chat(self.db) self.db_channel = db_channel diff --git a/config.py b/config.py index 96052157a..d27b5ad32 100644 --- a/config.py +++ b/config.py @@ -3,28 +3,28 @@ # Bot Configuration LOG_FILE_NAME = "bot.log" -PORT = '5010' -OWNER_ID = 6497757690 +PORT = 5010 +OWNER_ID = 6123108288 MSG_EFFECT = 5046509860389126442 SHORT_URL = "linkshortify.com" # shortner url -SHORT_API = "" -SHORT_TUT = "https://t.me/How_to_Download_7x/26" +SHORT_API = "573350da0e10a5a44f7e6fec3bc2b3f836b47805" +SHORT_TUT = "https://t.me/Infinix_Tutorial/10" # Bot Configuration SESSION = "yato" -TOKEN = "642712" -API_ID = "" -API_HASH = "" +TOKEN = "7851665401:AAF3WH1TYu_wDd506riW6py1c9nZswyrO3c" +API_ID = "21446955" +API_HASH = "e6f34a6186963663342b88b88c2b4750" WORKERS = 5 -DB_URI = "mongodb" -DB_NAME = "yato" +DB_URI = "mongodb+srv://souravagarwal14092007:szXRs8g7fErCnn4@cluster0.xlsbf3o.mongodb.net/?retryWrites=true&w=majority&appName=Cluster0" +DB_NAME = "Cluster0" -FSUBS = [[-1003016571084, True, 10]] # Force Subscription Channels [channel_id, request_enabled, timer_in_minutes] +FSUBS = [] # Temporarily disabled so bot can run locally without being an admin in the channel # Database Channel (Primary) -DB_CHANNEL = # just put channel id dont add "" +DB_CHANNEL = -1002558171315 # just put channel id dont add "" # Multiple Database Channels (can be set via bot settings) # DB_CHANNELS = { # "-1002595092736": {"name": "Primary DB", "is_primary": True, "is_active": True}, @@ -33,22 +33,22 @@ # Auto Delete Timer (seconds) AUTO_DEL = 300 # Admin IDs -ADMINS = [6497757690, 6103092779] +ADMINS = [6123108288] # Bot Settings DISABLE_BTN = True PROTECT = True # Messages Configuration MESSAGES = { - "START": "›› ʜᴇʏ!!, {first} ~
ʟᴏᴠᴇ ᴘᴏʀɴʜᴡᴀ? ɪ ᴀᴍ ᴍᴀᴅᴇ ᴛᴏ ʜᴇʟᴘ ʏᴏᴜ ᴛᴏ ғɪɴᴅ ᴡʜᴀᴛ ʏᴏᴜ aʀᴇ ʟᴏᴏᴋɪɴɢ ꜰᴏʀ.
", + "START": "›› ʜᴇʏ!!, {first} ~
ʟᴏᴠᴇ ᴘᴏʀɴ? ɪ ᴀᴍ ᴍᴀᴅᴇ ᴛᴏ ʜᴇʟᴘ ʏᴏᴜ ᴛᴏ ғɪɴᴅ ᴡʜᴀᴛ ʏᴏᴜ aʀᴇ ʟᴏᴏᴋɪɴɢ ꜰᴏʀ.
", "FSUB": "
›› ʜᴇʏ ×
\n ʏᴏᴜʀ ғɪʟᴇ ɪs ʀᴇᴀᴅʏ ‼️ ʟᴏᴏᴋs ʟɪᴋᴇ ʏᴏᴜ ʜᴀᴠᴇɴ'ᴛ sᴜʙsᴄʀɪʙᴇᴅ ᴛᴏ ᴏᴜʀ ᴄʜᴀɴɴᴇʟs ʏᴇᴛ, sᴜʙsᴄʀɪʙᴇ ɴᴏᴡ ᴛᴏ ɢᴇᴛ ʏᴏᴜʀ ғɪʟᴇs
", - "ABOUT": "›› ғᴏʀ ᴍᴏʀᴇ: @Nova_Flix \n
›› ᴜᴘᴅᴀᴛᴇs ᴄʜᴀɴɴᴇʟ: Cʟɪᴄᴋ ʜᴇʀᴇ \n›› ᴏᴡɴᴇʀ: @ProYato\n›› ʟᴀɴɢᴜᴀɢᴇ: Pʏᴛʜᴏɴ 3 \n›› ʟɪʙʀᴀʀʏ: Pʏʀᴏɢʀᴀᴍ ᴠ2 \n›› ᴅᴀᴛᴀʙᴀsᴇ: Mᴏɴɢᴏ ᴅʙ \n›› ᴅᴇᴠᴇʟᴏᴘᴇʀ: @cosmic_freak
", - "REPLY": "For More Join - @Hanime_Arena", + "ABOUT": "
◈ ᴏᴡɴᴇʀ : ɪᴍ•Ꮪᴜ͢ᴋᴜɴᴀ\n◈ ꜰᴏᴜɴᴅᴇʀ ᴏꜰ : ɪɴғɪɴɪx sʏɴᴅɪᴄᴀᴛᴇ\n◈ ᴍᴏᴠɪᴇs ᴄʜᴀɴɴᴇʟ : ɪɴғɪɴɪx ᴍᴏᴠɪᴇs\n◈ ᴅᴇᴠᴇʟᴏᴘᴇʀ : Sᴜᴋᴜɴᴀ
", + "REPLY": "For More Join - @Infinix_Adult", "SHORT_MSG": "📊 ʜᴇʏ {first}, \n\n‼️ ɢᴇᴛ ᴀʟʟ ꜰɪʟᴇꜱ ɪɴ ᴀ ꜱɪɴɢʟᴇ ʟɪɴᴋ ‼️\n\n ⌯ ʏᴏᴜʀ ʟɪɴᴋ ɪꜱ ʀᴇᴀᴅʏ, ᴋɪɴᴅʟʏ ᴄʟɪᴄᴋ ᴏɴ ᴏᴘᴇɴ ʟɪɴᴋ ʙᴜᴛᴛᴏɴ..", - "START_PHOTO": "https://graph.org/file/510affa3d4b6c911c12e3.jpg", - "FSUB_PHOTO": "https://telegra.ph/file/7a16ef7abae23bd238c82-b8fbdcb05422d71974.jpg", - "SHORT_PIC": "https://telegra.ph/file/7a16ef7abae23bd238c82-b8fbdcb05422d71974.jpg", - "SHORT": "https://telegra.ph/file/8aaf4df8c138c6685dcee-05d3b183d4978ec347.jpg" + "START_PHOTO": "https://i.ibb.co/GvJsGpyL/x.jpg", + "FSUB_PHOTO": "https://i.ibb.co/GvJsGpyL/x.jpg", + "SHORT_PIC": "https://i.ibb.co/GvJsGpyL/x.jpg", + "SHORT": "https://i.ibb.co/GvJsGpyL/x.jpg" } def LOGGER(name: str, client_name: str) -> logging.Logger: diff --git a/helper/helper_func.py b/helper/helper_func.py index 51b14fcb3..e218268da 100644 --- a/helper/helper_func.py +++ b/helper/helper_func.py @@ -480,7 +480,7 @@ async def batch_auto_del_notification(bot_username, messages, delay_time, transf # Update notification with get files button try: - if transfer_link: + if transfer_link and getattr(client, 'get_files_btn', True): try: name = "• ɢᴇᴛ ғɪʟᴇs •" link = f"https://t.me/{bot_username}?start={transfer_link}" diff --git a/plugins/broadcast.py b/plugins/broadcast.py index 21e9e195d..30a5d0d5a 100644 --- a/plugins/broadcast.py +++ b/plugins/broadcast.py @@ -1,123 +1,349 @@ -from pyrogram import Client, filters -from pyrogram.raw.types import MessageActionPinMessage -from pyrogram.errors import FloodWait, UserIsBlocked, InputUserDeactivated, UserNotParticipant, Forbidden, PeerIdInvalid, ChatAdminRequired +import time import asyncio +import re +from pyrogram import Client, filters +from pyrogram.types import InlineKeyboardMarkup, InlineKeyboardButton +from pyrogram.errors import FloodWait, UserIsBlocked, InputUserDeactivated, ChatForwardsRestricted -#===============================================================# +# ── Helpers ──────────────────────────────────────────────────────────────── +BAR_LEN = 20 +UPDATE_INTERVAL = 0.05 # refresh every 5% + +MODE_LABELS = { + "pin": "📌 ᴘɪɴ", + "delete": "🗑 ᴅᴇʟᴇᴛᴇ", + "silent": "🔕 ꜱɪʟᴇɴᴛ", + "normal": "📢 ɴᴏʀᴍᴀʟ", +} + +def build_bar(pct: float) -> str: + filled = int(pct * BAR_LEN) + return "●" * filled + "○" * (BAR_LEN - filled) + +def fmt_time(seconds: int) -> str: + if seconds < 60: + return f"{seconds}s" + m, s = divmod(seconds, 60) + return f"{m}m {s}s" + +def build_message( + header: str, + icon: str, + pct: float, + bar: str, + elapsed: int, + eta: int | None, + total: int, + successful: int, + blocked: int, + deleted: int, + unsuccessful: int, + footer: str = "", +) -> str: + time_line = f"⏱ {fmt_time(elapsed)}" + if eta is not None: + time_line += f" › ETA {fmt_time(eta)}" + stats = ( + f"{total} total · " + f"{successful} ✅ " + f"{blocked} 🚫 " + f"{deleted} 🗑 " + f"{unsuccessful} ❌" + ) + + msg = ( + f"{header}\n\n" + f"
{icon}: [{bar}] {pct:.0%}\n" + f"{time_line}
\n\n" + f"{stats}" + ) + if footer: + msg += f"\n\n{footer}" + return msg + +async def auto_delete(sent_msg, duration: int): + await asyncio.sleep(duration) + try: + await sent_msg.delete() + except Exception: + pass + +# Global state for cancellation +cancel_lock = asyncio.Lock() +is_canceled = False + +# ── /users ──────────────────────────────────────────────────────────────── @Client.on_message(filters.command('users')) async def user_count(client, message): if not message.from_user.id in client.admins: return await client.send_message(message.from_user.id, client.reply_text) total_users = await client.mongodb.full_userbase() - await message.reply(f"**{len(total_users)} Users are using this bot currently!**") + await message.reply(f"**{len(total_users)} Usᴇʀs ᴀʀᴇ ᴜsɪɴɢ ᴛʜɪs ʙᴏᴛ ᴄᴜʀʀᴇɴᴛʟʏ!**") -#===============================================================# +# ── /cancel ──────────────────────────────────────────────────────────────── +@Client.on_message(filters.command("cancel") & filters.private) +async def cancel_broadcast(client, message): + if not message.from_user.id in client.admins: + return + global is_canceled + async with cancel_lock: + is_canceled = True + await message.reply("✅ Bʀᴏᴀᴅᴄᴀsᴛ ᴄᴀɴᴄᴇʟʟᴀᴛɪᴏɴ ʀᴇǫᴜᴇsᴛᴇᴅ. Iᴛ ᴡɪʟʟ sᴛᴏᴘ sʜᴏʀᴛʟʏ.") -@Client.on_message(filters.private & filters.command('broadcast')) -async def send_text(client, message): - admin_ids = client.admins - user_id = message.from_user.id - if user_id in admin_ids: - - if message.reply_to_message: - query = await client.mongodb.full_userbase() - broadcast_msg = message.reply_to_message - total = 0 - successful = 0 - blocked = 0 - deleted = 0 - unsuccessful = 0 - - pls_wait = await message.reply("
Broadcasting Message.. This will Take Some Time
") - for chat_id in query: - try: - await broadcast_msg.copy(chat_id) - successful += 1 - except FloodWait as e: - await asyncio.sleep(e.x) - await broadcast_msg.copy(chat_id) - successful += 1 - except UserIsBlocked: - await client.mongodb.del_user(chat_id) - blocked += 1 - except InputUserDeactivated: - await client.mongodb.del_user(chat_id) - deleted += 1 - except Exception as e: - print(f"Failed to send message to {chat_id}: {e}") - unsuccessful += 1 - pass - total += 1 - - status = f"""
Broadcast Completed
-
Total Users : {total} - Successful : {successful} - Blocked Users : {blocked} - Deleted Accounts : {deleted} - Unsuccessful : {unsuccessful}
""" +# ── Broadcast Engine ─────────────────────────────────────────────────────── +async def do_broadcast(client, status_message_or_callback, broadcast_msg, mode_str, do_pin, do_delete, duration, silent, use_copy=False): + global is_canceled + async with cancel_lock: + is_canceled = False + + query = await client.mongodb.full_userbase() + total = len(query) + + if total == 0: + msg_text = "⚠️ Nᴏ ᴜsᴇʀs ғᴏᴜɴᴅ ɪɴ ᴛʜᴇ ᴅᴀᴛᴀʙᴀsᴇ." + if hasattr(status_message_or_callback, 'message'): + return await status_message_or_callback.message.edit(msg_text) + return await status_message_or_callback.reply(msg_text) + + successful = blocked = deleted = unsuccessful = 0 + + if hasattr(status_message_or_callback, 'message'): + pls_wait = status_message_or_callback.message + await pls_wait.edit(f"
Sᴛᴀʀᴛɪɴɢ ʙʀᴏᴀᴅᴄᴀsᴛ · {mode_str} · {total} ᴜsᴇʀs…
") + else: + pls_wait = await status_message_or_callback.reply(f"
Sᴛᴀʀᴛɪɴɢ ʙʀᴏᴀᴅᴄᴀsᴛ · {mode_str} · {total} ᴜsᴇʀs…
") + + bar = build_bar(0) + last_update_pct = -1.0 + start_time = time.time() + + for idx, chat_id in enumerate(query, start=1): + async with cancel_lock: + if is_canceled: + pct = idx / total + elapsed = int(time.time() - start_time) + await pls_wait.edit( + build_message( + f"
Bʀᴏᴀᴅᴄᴀsᴛɪɴɢ Sᴛᴏᴘᴇᴅ 🔴
\n\n
{mode_str}
", + "🔴", pct, build_bar(pct), + elapsed, None, + total, successful, blocked, deleted, unsuccessful, + ) + ) + return + + async def _send(): + nonlocal successful + if use_copy: + sent = await broadcast_msg.copy(chat_id, disable_notification=silent) + else: + sent = await client.forward_messages(chat_id=chat_id, from_chat_id=broadcast_msg.chat.id, message_ids=broadcast_msg.id, disable_notification=silent) - return await pls_wait.edit(status) + if do_pin: + await client.pin_chat_message(chat_id, sent.id, both_sides=True) + if do_delete: + asyncio.create_task(auto_delete(sent, duration)) + successful += 1 + + try: + await _send() + except FloodWait as e: + await asyncio.sleep(e.value) + try: + await _send() + except Exception: + unsuccessful += 1 + except UserIsBlocked: + await client.mongodb.del_user(chat_id) + blocked += 1 + except InputUserDeactivated: + await client.mongodb.del_user(chat_id) + deleted += 1 + except Exception: + unsuccessful += 1 + + # ── Progress update ──────────────────────────────────────────────── + pct = idx / total + if pct - last_update_pct >= UPDATE_INTERVAL: + bar = build_bar(pct) + elapsed = int(time.time() - start_time) + done = successful + blocked + deleted + unsuccessful + eta = int((total - done) * (elapsed / done)) if done else None + await pls_wait.edit( + build_message( + f"
Bʀᴏᴀᴅᴄᴀsᴛɪɴɢ...
\n\n
{mode_str}
", + "⏳", pct, bar, + elapsed, eta, + total, successful, blocked, deleted, unsuccessful, + footer="➪ To stop: /cancel", + ) + ) + last_update_pct = pct + + # ── Final ────────────────────────────────────────────────────────────── + elapsed = int(time.time() - start_time) + await pls_wait.edit( + build_message( + f"
Bʀᴏᴀᴅᴄᴀsᴛ Dᴏɴᴇ ✅
\n\n
{mode_str}
", + "✅", 1.0, "●" * BAR_LEN, + elapsed, 0, + total, successful, blocked, deleted, unsuccessful, + ) + ) + +# ── /broadcast ───────────────────────────────────────────────────────────── +def parse_broadcast_modes(args): + do_pin = False + do_delete = False + duration = 0 + silent = False + mode_labels = [] + + i = 0 + while i < len(args): + arg = args[i].lower() + if arg == "pin" or arg == "pbroadcast": + do_pin = True + if MODE_LABELS["pin"] not in mode_labels: + mode_labels.append(MODE_LABELS["pin"]) + elif arg == "delete": + do_delete = True + try: + duration = int(args[i + 1]) + i += 1 + except (IndexError, ValueError): + return None, "⚠️ Provide a valid duration.\nUsage: /broadcast delete 30" + mode_labels.append(f"🗑 DELETE({duration}s)") + elif arg == "silent": + silent = True + if MODE_LABELS["silent"] not in mode_labels: + mode_labels.append(MODE_LABELS["silent"]) + elif arg == "normal": + if MODE_LABELS["normal"] not in mode_labels: + mode_labels.append(MODE_LABELS["normal"]) + elif not arg.startswith("http"): + mode_labels.append(arg.upper()) + i += 1 + + if not mode_labels: + mode_labels.append(MODE_LABELS["normal"]) + + mode_str = " + ".join(mode_labels) + return (do_pin, do_delete, duration, silent, mode_str), None + + +@Client.on_message(filters.private & filters.command(['broadcast', 'pbroadcast'])) +async def broadcast_command(client, message): + if not message.from_user.id in client.admins: + return + + args = message.text.split()[1:] - else: - msg = await message.reply(f"Use This Command As A Reply To Any Telegram Message Without Any Spaces.") - await asyncio.sleep(8) - await msg.delete() + if message.command[0].lower() == 'pbroadcast': + args.insert(0, "pin") -#===============================================================# + link = None + for arg in args: + if arg.startswith("http"): + link = arg + break -@Client.on_message(filters.private & filters.command('pbroadcast')) -async def pin_bdcst_text(client, message): - admin_ids = client.admins - user_id = message.from_user.id - if user_id in admin_ids: - if message.reply_to_message: - query = await client.mongodb.full_userbase() - broadcast_msg = message.reply_to_message - total = 0 - successful = 0 - blocked = 0 - deleted = 0 - unsuccessful = 0 + parsed_modes, err = parse_broadcast_modes(args) + if err: + return await message.reply(err) - pls_wait = await message.reply("
Broadcasting Message.. This will Take Some Time
") + do_pin, do_delete, duration, silent, mode_str = parsed_modes + + if link: + pattern = r"https://t.me/(?:c/)?(.*)/(\d+)" + match = re.match(pattern, link) + if match: + chat_id = match.group(1) + if chat_id.isdigit(): + chat_id = int(f"-100{chat_id}") + msg_id = int(match.group(2)) - for chat_id in query: + try: + broadcast_msg = await client.get_messages(chat_id, msg_id) + if not broadcast_msg or broadcast_msg.empty: + return await message.reply("Could not fetch the message. Make sure I am an admin in that channel.") + try: - # Send the message and capture the result - sent_msg = await broadcast_msg.copy(chat_id) - successful += 1 - - # Pin the sent message immediately after broadcasting - await client.pin_chat_message(chat_id=chat_id, message_id=sent_msg.id, both_sides=True) - except FloodWait as e: - await asyncio.sleep(e.x) - # Retry sending and pinning after flood wait - sent_msg = await broadcast_msg.copy(chat_id) - successful += 1 - await client.pin_chat_message(chat_id=chat_id, message_id=sent_msg.id) - except UserIsBlocked: - await client.mongodb.del_user(chat_id) - blocked += 1 - except InputUserDeactivated: - await client.mongodb.del_user(chat_id) - deleted += 1 - except Exception as e: - print(f"Failed to send message to {chat_id}: {e}") - unsuccessful += 1 - total += 1 - - status = f"""
Broadcast Completed
- Total Users : {total} - Successful : {successful} - Blocked Users : {blocked} - Deleted Accounts : {deleted} - Unsuccessful : {unsuccessful}""" + await client.forward_messages(message.from_user.id, chat_id, msg_id, disable_notification=silent) + use_copy = 0 + except ChatForwardsRestricted: + await broadcast_msg.copy(message.from_user.id, disable_notification=silent) + use_copy = 1 + + flags = f"{int(do_pin)}_{int(do_delete)}_{duration}_{int(silent)}_{use_copy}" + + reply_markup = InlineKeyboardMarkup([ + [InlineKeyboardButton("🟢 𝗬𝗲𝘀 𝗕𝗿𝗼𝗮𝗱𝗰𝗮𝘀𝘁", callback_data=f"bdcst_y_{chat_id}_{msg_id}_{flags}")], + [InlineKeyboardButton("🔴 𝗖𝗮𝗻𝗰𝗲𝗹", callback_data="bdcst_n")] + ]) + await message.reply(f"
📢 Bʀᴏᴀᴅᴄᴀsᴛ Cᴏɴғɪʀᴍᴀᴛɪᴏɴ
\n\nMᴏᴅᴇꜱ: **{mode_str}**\nSʜᴏᴜʟᴅ I ʙʀᴏᴀᴅᴄᴀsᴛ ᴛʜᴇ ᴍᴇssᴀɢᴇ ᴀʙᴏᴠᴇ? Pʟᴇᴀsᴇ ʀᴇᴠɪᴇᴡ ɪᴛ ᴄᴀʀᴇғᴜʟʟʏ.", reply_markup=reply_markup) + except Exception as e: + await message.reply(f"Error fetching message: {e}") + else: + await message.reply("
Iɴᴠᴀʟɪᴅ ʟɪɴᴋ ғᴏʀᴍᴀᴛ🔴
\n Pʟᴇᴀsᴇ ᴜsᴇ ᴀ ᴠᴀʟɪᴅ Tᴇʟᴇɢʀᴀᴍ ᴘᴏsᴛ ʟɪɴᴋ.") + + elif message.reply_to_message: + await do_broadcast(client, message, message.reply_to_message, mode_str, do_pin, do_delete, duration, silent, use_copy=True) + else: + msg = await message.reply( + "
Rᴇᴘʟʏ ᴛᴏ ᴀɴʏ Tᴇʟᴇɢʀᴀᴍ ᴍᴇssᴀɢᴇ ᴏʀ ᴘᴀss ᴀ Tᴇʟᴇɢʀᴀᴍ ᴘᴏsᴛ ʟɪɴᴋ ᴛᴏ ᴜsᴇ ᴛʜɪs ᴄᴏᴍᴍᴀɴᴅ 💬
\n\n" + "
Mᴏᴅᴇꜱ:
\n" + "› /broadcastꜱᴇɴᴅ ɴᴏʀᴍᴀʟʟʏ\n" + "› /broadcast pinꜱᴇɴᴅ & ᴘɪɴ\n" + "› /broadcast silentɴᴏ ɴᴏᴛɪꜰɪᴄᴀᴛɪᴏɴ\n" + "› /broadcast delete 30ᴀᴜᴛᴏ-ᴅᴇʟᴇᴛᴇ ᴀꜰᴛᴇʀ 30ꜱ\n" + "› /broadcast pin delete 30ᴘɪɴ, ᴛʜᴇɴ ᴅᴇʟᴇᴛᴇ\n" + "› /broadcast silent pin delete 60ᴀʟʟ ᴛʜʀᴇᴇ\n\n" + "
Yᴏᴜ ᴄᴀɴ ᴀʟꜱᴏ ᴘᴀꜱꜱ ᴀ ʟɪɴᴋ ᴀʟᴏɴɢꜱɪᴅᴇ ᴍᴏᴅᴇꜱ:
\n" + "/broadcast silent https://t.me/Infinix_Adult/123" + ) + await asyncio.sleep(30) + await msg.delete() + +#===============================================================# + +@Client.on_callback_query(filters.regex(r"^bdcst_y_(.+)_(.+)_(\d+)_(\d+)_(\d+)_(\d+)_(\d+)$")) +async def broadcast_yes(client, query): + if query.from_user.id not in client.admins: + return await query.answer("Only admins can use this.", show_alert=True) + + chat_id = query.matches[0].group(1) + msg_id = int(query.matches[0].group(2)) - return await pls_wait.edit(status) + do_pin = bool(int(query.matches[0].group(3))) + do_delete = bool(int(query.matches[0].group(4))) + duration = int(query.matches[0].group(5)) + silent = bool(int(query.matches[0].group(6))) + use_copy = bool(int(query.matches[0].group(7))) - else: - msg = await message.reply("Use This Command As A Reply To Any Telegram Message Without Any Spaces.") - await asyncio.sleep(8) - await msg.delete() - \ No newline at end of file + if chat_id.lstrip('-').isdigit(): + chat_id = int(chat_id) + + mode_labels = [] + if do_pin: mode_labels.append(MODE_LABELS["pin"]) + if do_delete: mode_labels.append(f"🗑 DELETE({duration}s)") + if silent: mode_labels.append(MODE_LABELS["silent"]) + if not mode_labels: mode_labels.append(MODE_LABELS["normal"]) + mode_str = " + ".join(mode_labels) + + try: + broadcast_msg = await client.get_messages(chat_id, msg_id) + except Exception as e: + return await query.answer(f"Failed to fetch message: {e}", show_alert=True) + + await do_broadcast(client, query, broadcast_msg, mode_str, do_pin, do_delete, duration, silent, use_copy) + +#===============================================================# + +@Client.on_callback_query(filters.regex(r"^bdcst_n$")) +async def broadcast_no(client, query): + if query.from_user.id not in client.admins: + return await query.answer("𝗙𝘂𝗰𝗸 𝗢𝗳𝗳 𝗚𝗼𝗼𝗻𝗲𝗿", show_alert=True) + + await query.message.edit("
❌ Bʀᴏᴀᴅᴄᴀsᴛ ᴄᴀɴᴄᴇʟʟᴇᴅ
") \ No newline at end of file diff --git a/plugins/channel_post.py b/plugins/channel_post.py index d4f6b61a2..35aa5fed7 100644 --- a/plugins/channel_post.py +++ b/plugins/channel_post.py @@ -3,7 +3,7 @@ from pyrogram.types import Message, InlineKeyboardMarkup, InlineKeyboardButton from pyrogram.errors import FloodWait from helper.helper_func import encode - +import re #===============================================================# @Client.on_message(filters.private & ~filters.command(['start', 'shortner','users','broadcast','batch','genlink','stats', 'pbroadcast', 'db', 'adddb', 'add_db', 'removedb', 'rm_db', 'ban', 'unban', 'addpremium', 'delpremium', 'premiumusers', 'request', 'profile'])) @@ -54,6 +54,3 @@ async def new_post(client: Client, message: Message): pass - - - diff --git a/plugins/settings.py b/plugins/settings.py index 66e6e5b43..04d1623b2 100644 --- a/plugins/settings.py +++ b/plugins/settings.py @@ -65,6 +65,8 @@ async def settings_page_2(client, query): ›› **ᴀᴜᴛᴏ ᴅᴇʟᴇᴛᴇ ᴛɪᴍᴇʀ:** `{client.auto_del}` ›› **ᴘʀᴏᴛᴇᴄᴛ ᴄᴏɴᴛᴇɴᴛ:** `{"✓ ᴛʀᴜᴇ" if client.protect else "✗ ꜰᴀʟsᴇ"}` ›› **ᴅɪsᴀʙʟᴇ ʙᴜᴛᴛᴏɴ:** `{"✓ ᴛʀᴜᴇ" if client.disable_btn else "✗ ꜰᴀʟsᴇ"}` +›› **ᴀᴜᴛᴏ ᴡᴀᴛᴇʀᴍᴀʀᴋ:** `{"✓ ᴛʀᴜᴇ" if getattr(client, 'auto_watermark', True) else "✗ ꜰᴀʟsᴇ"}` +›› **ɢᴇᴛ ꜰɪʟᴇs ʙᴛɴ:** `{"✓ ᴛʀᴜᴇ" if getattr(client, 'get_files_btn', True) else "✗ ꜰᴀʟsᴇ"}` ›› **ʀᴇᴘʟʏ ᴛᴇxᴛ:** `{client.reply_text if client.reply_text else 'ɴᴏɴᴇ'}` ›› **ᴀᴅᴍɪɴs:** `{len(client.admins)}` ›› **sʜᴏʀᴛɴᴇʀ ᴜʀʟ:** `{getattr(client, 'short_url', 'ɴᴏᴛ sᴇᴛ')}` @@ -83,6 +85,7 @@ async def settings_page_2(client, query): reply_markup = InlineKeyboardMarkup([ [InlineKeyboardButton('ᴘʀᴏᴛᴇᴄᴛ ᴄᴏɴᴛᴇɴᴛ', 'protect'), InlineKeyboardButton('ᴘʜᴏᴛᴏs', 'photos')], [InlineKeyboardButton('ᴛᴇxᴛs', 'texts'), InlineKeyboardButton('sʜᴏʀᴛɴᴇʀ', 'shortner')], + [InlineKeyboardButton('ᴀᴜᴛᴏ ᴡᴀᴛᴇʀᴍᴀʀᴋ', 'toggle_watermark'), InlineKeyboardButton('ɢᴇᴛ ꜰɪʟᴇs ʙᴛɴ', 'toggle_get_files')], [InlineKeyboardButton('‹ ᴘʀᴇᴠ', 'settings'), InlineKeyboardButton('ʜᴏᴍᴇ', 'home')] ]) await query.message.edit_text(msg, reply_markup=reply_markup) @@ -474,6 +477,23 @@ async def protect(client, query): #===============================================================# +@Client.on_callback_query(filters.regex("^toggle_get_files$")) +async def toggle_get_files(client, query): + current = getattr(client, 'get_files_btn', True) + client.get_files_btn = not current + return await settings_page_2(client, query) + +#===============================================================# + +@Client.on_callback_query(filters.regex("^toggle_watermark$")) +async def toggle_watermark(client, query): + current = getattr(client, 'auto_watermark', True) + client.auto_watermark = not current + await client.mongodb.update_bot_setting('auto_watermark', client.auto_watermark) + return await settings_page_2(client, query) + +#===============================================================# + @Client.on_callback_query(filters.regex("^auto_del$")) async def auto_del(client, query): msg = f"""
**Change Auto Delete Time:**
diff --git a/plugins/shortner.py b/plugins/shortner.py index 344185322..e032f910b 100644 --- a/plugins/shortner.py +++ b/plugins/shortner.py @@ -1,14 +1,17 @@ import requests import random import string -from config import SHORT_URL, SHORT_API, MESSAGES +import base64 +import time +from config import SHORT_URL, SHORT_API, MESSAGES, OWNER_ID from pyrogram import Client, filters from pyrogram.types import Message, InlineKeyboardMarkup, InlineKeyboardButton, CallbackQuery, InputMediaPhoto from pyrogram.errors.pyromod import ListenerTimeout from helper.helper_func import force_sub -# ✅ In-memory cache +# ✅ In-memory cache with TTL (24 hours) shortened_urls_cache = {} +CACHE_TTL = 86400 # 24 hours in seconds def generate_random_alphanumeric(): characters = string.ascii_letters + string.digits @@ -21,24 +24,38 @@ def get_short(url, client): if not shortner_enabled: return url # Return original URL if shortner is disabled - # Step 2: Check cache + # Step 2: Check cache with TTL + current_time = time.time() if url in shortened_urls_cache: - return shortened_urls_cache[url] + cached_link, cached_time = shortened_urls_cache[url] + if current_time - cached_time < CACHE_TTL: + return cached_link try: - alias = generate_random_alphanumeric() # Use dynamic shortner settings from client if available short_url = getattr(client, 'short_url', SHORT_URL) short_api = getattr(client, 'short_api', SHORT_API) - api_url = f"https://{short_url}/api?api={short_api}&url={url}&alias={alias}" - response = requests.get(api_url) + api_url = f"https://{short_url}/api?api={short_api}&url={url}" + response = requests.get(api_url, timeout=10) rjson = response.json() if rjson.get("status") == "success" and response.status_code == 200: - short_url = rjson.get("shortenedUrl", url) - shortened_urls_cache[url] = short_url - return short_url + # Get the original shortener link (lksfy.com) + original_short_link = rjson.get("shortenedUrl", url) + + # --- CUSTOM DOMAIN REDIRECT LOGIC --- + # 1. Encode the lksfy link to Base64 + encoded_link = base64.b64encode(original_short_link.encode("utf-8")).decode("utf-8") + + # 2. Build the new sukuna.site link + secure_domain_link = f"https://sukuna.site/?to={encoded_link}" + + # 3. Save to cache with timestamp and return the new custom domain link + shortened_urls_cache[url] = (secure_domain_link, current_time) + return secure_domain_link + # ------------------------------------ + except Exception as e: print(f"[Shortener Error] {e}") @@ -48,6 +65,8 @@ def get_short(url, client): @Client.on_message(filters.command('shortner') & filters.private) async def shortner_command(client: Client, message: Message): + if message.from_user.id != OWNER_ID: + return await shortner_panel(client, message) #===============================================================# @@ -62,7 +81,7 @@ async def shortner_panel(client, query_or_message): # Check if shortner is working (only if enabled) if shortner_enabled: try: - test_response = requests.get(f"https://{short_url}/api?api={short_api}&url=https://google.com&alias=test", timeout=5) + test_response = requests.get(f"https://{short_url}/api?api={short_api}&url=https://google.com", timeout=5) status = "✓ ᴡᴏʀᴋɪɴɢ" if test_response.status_code == 200 else "✗ ɴᴏᴛ ᴡᴏʀᴋɪɴɢ" except: status = "✗ ɴᴏᴛ ᴡᴏʀᴋɪɴɢ" @@ -104,8 +123,8 @@ async def shortner_panel(client, query_or_message): @Client.on_callback_query(filters.regex("^shortner$")) async def shortner_callback(client, query): - if not query.from_user.id in client.admins: - return await query.answer('❌ ᴏɴʟʏ ᴀᴅᴍɪɴꜱ ᴄᴀɴ ᴜꜱᴇ ᴛʜɪꜱ!', show_alert=True) + if query.from_user.id != OWNER_ID: + return await query.answer('❌ ᴏɴʟʏ ᴏᴡɴᴇʀ ᴄᴀɴ ᴜꜱᴇ ᴛʜɪꜱ!', show_alert=True) await query.answer() await shortner_panel(client, query) @@ -113,8 +132,8 @@ async def shortner_callback(client, query): @Client.on_callback_query(filters.regex("^toggle_shortner$")) async def toggle_shortner(client: Client, query: CallbackQuery): - if not query.from_user.id in client.admins: - return await query.answer('❌ ᴏɴʟʏ ᴀᴅᴍɪɴꜱ ᴄᴀɴ ᴜꜱᴇ ᴛʜɪꜱ!', show_alert=True) + if query.from_user.id != OWNER_ID: + return await query.answer('❌ ᴏɴʟʏ ᴏᴡɴᴇʀ ᴄᴀɴ ᴜꜱᴇ ᴛʜɪꜱ!', show_alert=True) # Toggle the shortner status current_status = getattr(client, 'shortner_enabled', True) new_status = not current_status @@ -133,8 +152,8 @@ async def toggle_shortner(client: Client, query: CallbackQuery): @Client.on_callback_query(filters.regex("^add_shortner$")) async def add_shortner(client: Client, query: CallbackQuery): - if not query.from_user.id in client.admins: - return await query.answer('❌ ᴏɴʟʏ ᴀᴅᴍɪɴꜱ ᴄᴀɴ ᴜꜱᴇ ᴛʜɪꜱ!', show_alert=True) + if query.from_user.id != OWNER_ID: + return await query.answer('❌ ᴏɴʟʏ ᴏᴡɴᴇʀ ᴄᴀɴ ᴜꜱᴇ ᴛʜɪꜱ!', show_alert=True) await query.answer() @@ -187,8 +206,8 @@ async def add_shortner(client: Client, query: CallbackQuery): @Client.on_callback_query(filters.regex("^set_tutorial_link$")) async def set_tutorial_link(client: Client, query: CallbackQuery): - if not query.from_user.id in client.admins: - return await query.answer('❌ ᴏɴʟʏ ᴀᴅᴍɪɴꜱ ᴄᴀɴ ᴜꜱᴇ ᴛʜɪꜱ!', show_alert=True) + if query.from_user.id != OWNER_ID: + return await query.answer('❌ ᴏɴʟʏ ᴏᴡɴᴇʀ ᴄᴀɴ ᴜꜱᴇ ᴛʜɪꜱ!', show_alert=True) await query.answer() @@ -221,8 +240,8 @@ async def set_tutorial_link(client: Client, query: CallbackQuery): @Client.on_callback_query(filters.regex("^test_shortner$")) async def test_shortner(client: Client, query: CallbackQuery): - if not query.from_user.id in client.admins: - return await query.answer('❌ ᴏɴʟʏ ᴀᴅᴍɪɴꜱ ᴄᴀɴ ᴜꜱᴇ ᴛʜɪꜱ!', show_alert=True) + if query.from_user.id != OWNER_ID: + return await query.answer('❌ ᴏɴʟʏ ᴏᴡɴᴇʀ ᴄᴀɴ ᴜꜱᴇ ᴛʜɪꜱ!', show_alert=True) await query.answer() @@ -233,8 +252,7 @@ async def test_shortner(client: Client, query: CallbackQuery): try: test_url = "https://google.com" - alias = generate_random_alphanumeric() - api_url = f"https://{short_url}/api?api={short_api}&url={test_url}&alias={alias}" + api_url = f"https://{short_url}/api?api={short_api}&url={test_url}" response = requests.get(api_url, timeout=10) rjson = response.json() diff --git a/plugins/start.py b/plugins/start.py index 29ff40003..1d0f9b998 100644 --- a/plugins/start.py +++ b/plugins/start.py @@ -6,6 +6,7 @@ from plugins.shortner import get_short from helper.helper_func import get_messages, force_sub, decode, batch_auto_del_notification import asyncio +import re #===============================================================# @@ -69,7 +70,7 @@ async def start_command(client: Client, message: Message): InlineKeyboardButton("ᴛᴜᴛᴏʀɪᴀʟ •", url=tutorial_link) ], [ - InlineKeyboardButton(" • ʙᴜʏ ᴘʀᴇᴍɪᴜᴍ •", url="https://t.me/Premium_Fliix/21") + InlineKeyboardButton(" • ʙᴜʏ ᴘʀᴇᴍɪᴜᴍ •", url="https://t.me/Infinix_Adult/27") ] ]) ) @@ -161,7 +162,7 @@ async def start_command(client: Client, message: Message): return await message.reply("⚠️ Invalid or expired link.") # 7. Get messages from the specific source channel first - temp_msg = await message.reply("Wait A Sec..") + temp_msg = await message.reply("⏳") messages = [] try: @@ -206,12 +207,26 @@ async def start_command(client: Client, message: Message): yugen_msgs = [] for msg in messages: - caption = ( - client.messages.get('CAPTION', '').format( - previouscaption=msg.caption.html if msg.caption else msg.document.file_name - ) if bool(client.messages.get('CAPTION', '')) and bool(msg.document) - else ("" if not msg.caption else msg.caption.html) - ) + if getattr(client, 'auto_watermark', True): + if msg.video or (msg.document and msg.document.mime_type and msg.document.mime_type.startswith("video/")): + file_name = getattr(msg.video, "file_name", None) or getattr(msg.document, "file_name", None) or "" + quality = "" + match = re.search(r"(144p|240p|360p|480p|720p|1080p|1440p|2160p|4k)", file_name, re.IGNORECASE) + if match: + quality = match.group(1).lower() + elif msg.video and getattr(msg.video, "height", None): + quality = f"{msg.video.height}p" + + if quality: + caption = f'{quality} • ʙʏ ɪɴꜰɪɴɪx ᴀᴅᴜʟᴛ' + else: + caption = 'ʙʏ ɪɴꜰɪɴɪx ᴀᴅᴜʟᴛ' + elif msg.document or msg.photo: + caption = 'ʙʏ ɪɴꜰɪɴɪx ᴀᴅᴜʟᴛ' + else: + caption = "" + else: + caption = "" if not msg.caption else msg.caption.html reply_markup = msg.reply_markup if not client.disable_btn else None try: @@ -237,8 +252,8 @@ async def start_command(client: Client, message: Message): # 8. Auto delete timer if messages and client.auto_del > 0: - # Create transfer link for getting files again (original base64_string) - transfer_link = original_payload + # Create transfer link with the raw base64 string to force the shortener ad again + transfer_link = base64_string # Start batch auto delete notification - single notification for all files asyncio.create_task(batch_auto_del_notification( @@ -296,7 +311,7 @@ async def request_command(client: Client, message: Message): return if not is_user_premium: - BUTTON_URL = "https://t.me/hanime_arena/5" + BUTTON_URL = "https://t.me/Infinix_Adult/27" reply_markup = InlineKeyboardMarkup([ [InlineKeyboardButton("💎 Upgrade to Premium", url=BUTTON_URL)] ])