diff --git a/config.py b/config.py index fcefa545..f198c4b6 100644 --- a/config.py +++ b/config.py @@ -1,7 +1,3 @@ -# Copyright (c) 2025 devgagan : https://github.com/devgaganin. -# Licensed under the GNU General Public License v3.0. -# See LICENSE file in the repository root for full license text. - import os from dotenv import load_dotenv load_dotenv() @@ -27,10 +23,10 @@ DB_NAME = os.getenv("DB_NAME", "telegram_downloader") # ─── OWNER / CONTROL SETTINGS ─────────────────────────────────────────────────── -OWNER_ID = list(map(int, os.getenv("OWNER_ID", "").split())) # space-separated list +OWNER_ID = list(map(int, os.getenv("OWNER_ID", "").split())) if os.getenv("OWNER_ID", "") else [] STRING = os.getenv("STRING", None) # optional session string -LOG_GROUP = int(os.getenv("LOG_GROUP", "-1001234456")) -FORCE_SUB = int(os.getenv("FORCE_SUB", "-10012345567")) +LOG_GROUP = int(os.getenv("LOG_GROUP", "0")) if os.getenv("LOG_GROUP") else 0 +FORCE_SUB = int(os.getenv("FORCE_SUB", "0")) if os.getenv("FORCE_SUB") else 0 # ─── SECURITY KEYS ────────────────────────────────────────────────────────────── MASTER_KEY = os.getenv("MASTER_KEY", "gK8HzLfT9QpViJcYeB5wRa3DmN7P2xUq") # session encryption diff --git a/main.py b/main.py index edac6aac..f2473051 100644 --- a/main.py +++ b/main.py @@ -25,17 +25,11 @@ async def main(): await asyncio.sleep(1) if __name__ == "__main__": - loop = asyncio.get_event_loop() print("Starting clients ...") try: - loop.run_until_complete(main()) + asyncio.run(main()) except KeyboardInterrupt: print("Shutting down...") except Exception as e: - print(e) + print(f"Error: {e}") sys.exit(1) - finally: - try: - loop.close() - except Exception: - pass diff --git a/plugins/batch.py b/plugins/batch.py index 59fc23fb..d912b87c 100644 --- a/plugins/batch.py +++ b/plugins/batch.py @@ -6,9 +6,9 @@ from pyrogram import Client, filters from pyrogram.types import Message from pyrogram.errors import UserNotParticipant -from config import API_ID, API_HASH, LOG_GROUP, STRING, FORCE_SUB, FREEMIUM_LIMIT, PREMIUM_LIMIT +from config import API_ID, API_HASH, LOG_GROUP, STRING, FORCE_SUB from utils.func import get_user_data, screenshot, thumbnail, get_video_metadata -from utils.func import get_user_data_key, process_text_with_rules, is_premium_user, E +from utils.func import get_user_data_key, process_text_with_rules, E from shared_client import app as X from plugins.settings import rename_file from plugins.start import subscribe as sub @@ -86,7 +86,17 @@ async def upd_dlg(c): return False # fixed the old group of 2021-2022 extraction 🌝 (buy krne ka fayda nhi ab old group) ✅ -async def get_msg(c, u, i, d, lt): +async def get_msg(c, u, i, d, lt, topic_id=None): + """ + Fetch a message from Telegram. + Args: + c: Bot client + u: User client + i: Chat ID + d: Message ID + lt: Link type ('public' or 'private') + topic_id: Optional topic/forum ID for supergroup topics + """ try: if lt == 'public': try: @@ -395,9 +405,6 @@ async def process_cmd(c, m): uid = m.from_user.id cmd = m.command[0] - if FREEMIUM_LIMIT == 0 and not await is_premium_user(uid): - await m.reply_text("This bot does not provide free servies, get subscription from OWNER") - return if await sub(c, m) == 1: return pro = await m.reply_text('Doing some checks hold on...') @@ -427,7 +434,7 @@ async def cancel_cmd(c, m): @X.on_message(filters.text & filters.private & ~login_in_progress & ~filters.command([ 'start', 'batch', 'cancel', 'login', 'logout', 'stop', 'set', - 'pay', 'redeem', 'gencode', 'single', 'generate', 'keyinfo', 'encrypt', 'decrypt', 'keys', 'setbot', 'rembot'])) + 'help', 'terms', 'plan', 'single', 'generate', 'keyinfo', 'encrypt', 'decrypt', 'keys', 'setbot', 'rembot'])) async def text_handler(c, m): uid = m.from_user.id if uid not in Z: return @@ -439,24 +446,25 @@ async def text_handler(c, m): if s == 'start': L = m.text - i, d, lt = E(L) + i, d, lt, subgroup = E(L) if not i or not d: await m.reply_text('Invalid link format.') Z.pop(uid, None) return - Z[uid].update({'step': 'count', 'cid': i, 'sid': d, 'lt': lt}) + Z[uid].update({'step': 'count', 'cid': i, 'sid': d, 'lt': lt, 'subgroup': subgroup}) await m.reply_text('How many messages?') elif s == 'start_single': L = m.text - i, d, lt = E(L) + i, d, lt, subgroup = E(L) if not i or not d: await m.reply_text('Invalid link format.') Z.pop(uid, None) return - Z[uid].update({'step': 'process_single', 'cid': i, 'sid': d, 'lt': lt}) + Z[uid].update({'step': 'process_single', 'cid': i, 'sid': d, 'lt': lt, 'subgroup': subgroup}) i, s, lt = Z[uid]['cid'], Z[uid]['sid'], Z[uid]['lt'] + subgroup = Z[uid].get('subgroup') pt = await m.reply_text('Processing...') ubot = UB.get(uid) @@ -477,12 +485,19 @@ async def text_handler(c, m): return try: - msg = await get_msg(ubot, uc, i, s, lt) + # Handle range or single message + if isinstance(s, tuple): + start_msg, end_msg = s + msg_id = start_msg + else: + msg_id = s + + msg = await get_msg(ubot, uc, i, msg_id, lt, subgroup) if msg: res = await process_msg(ubot, uc, msg, str(m.chat.id), lt, uid, i) await pt.edit(f'1/1: {res}') else: - await pt.edit('Message not found') + await pt.edit(f'Message {msg_id} not found. Make sure you have access to this channel/group.') except Exception as e: await pt.edit(f'Error: {str(e)[:50]}') finally: @@ -494,7 +509,7 @@ async def text_handler(c, m): return count = int(m.text) - maxlimit = PREMIUM_LIMIT if await is_premium_user(uid) else FREEMIUM_LIMIT + maxlimit = 1000 if count > maxlimit: await m.reply_text(f'Maximum limit is {maxlimit}.') @@ -502,14 +517,20 @@ async def text_handler(c, m): Z[uid].update({'step': 'process', 'did': str(m.chat.id), 'num': count}) i, s, n, lt = Z[uid]['cid'], Z[uid]['sid'], Z[uid]['num'], Z[uid]['lt'] + subgroup = Z[uid].get('subgroup') success = 0 pt = await m.reply_text('Processing batch...') uc = await get_uclient(uid) ubot = UB.get(uid) - if not uc or not ubot: - await pt.edit('Missing client setup') + if not uc: + await pt.edit('❌ User client not found.\n\nFor private channels/groups, use /login to authenticate first.') + Z.pop(uid, None) + return + + if not ubot: + await pt.edit('❌ Bot not configured.\n\nUse /setbot to add your custom bot token.') Z.pop(uid, None) return @@ -518,6 +539,15 @@ async def text_handler(c, m): Z.pop(uid, None) return + # Handle range vs single message + count + if isinstance(s, tuple): + start_msg, end_msg = s + message_ids = list(range(start_msg, end_msg + 1)) + n = len(message_ids) + else: + start_msg = s + message_ids = list(range(start_msg, start_msg + n)) + await add_active_batch(uid, { "total": n, "current": 0, @@ -527,7 +557,7 @@ async def text_handler(c, m): }) try: - for j in range(n): + for j, mid in enumerate(message_ids): if should_cancel(uid): await pt.edit(f'Cancelled at {j}/{n}. Success: {success}') @@ -535,24 +565,56 @@ async def text_handler(c, m): await update_batch_progress(uid, j, success) - mid = int(s) + j - try: - msg = await get_msg(ubot, uc, i, mid, lt) + print(f"Fetching message: chat_id={i}, msg_id={mid}, type={lt}, topic={subgroup}") + msg = await get_msg(ubot, uc, i, mid, lt, subgroup) if msg: + # Check if message is empty + if getattr(msg, "empty", False): + print(f"Message {mid} is empty, skipping...") + continue + + # Filter by topic/sub-group if specified + if subgroup is not None: + # Check if message belongs to the specified topic + # Pyrogram exposes forum_topic_id for forum/topic messages + msg_topic_id = getattr(msg, 'forum_topic_id', None) or getattr(msg, 'reply_to_top_message_id', None) + + # Accept if: message IS the topic, OR message belongs to the topic + if msg.id != subgroup and msg_topic_id != subgroup: + print(f"Message {mid} (forum_topic_id={msg_topic_id}) not in topic {subgroup}, skipping...") + continue + print(f"Message {mid} belongs to topic {subgroup} ✓") + res = await process_msg(ubot, uc, msg, str(m.chat.id), lt, uid, i) + print(f"Message {mid} result: {res}") if 'Done' in res or 'Copied' in res or 'Sent' in res: success += 1 + + # Update progress every 5 messages + if (j + 1) % 5 == 0: + try: + await pt.edit(f'Progress: {j+1}/{n} | Success: {success}') + except: + pass else: - pass + print(f"Message {mid} not found - check if you have access to this channel") + # Update progress with warning + if (j + 1) % 5 == 0: + try: + await pt.edit(f'Progress: {j+1}/{n} | Success: {success} | Not found: {j+1-success}') + except: + pass except Exception as e: - try: await pt.edit(f'{j+1}/{n}: Error - {str(e)[:30]}') - except: pass + print(f"Error processing message {mid}: {str(e)}") + try: + await pt.edit(f'{j+1}/{n}: Error - {str(e)[:30]}') + except: + pass await asyncio.sleep(10) - if j+1 == n: - await m.reply_text(f'Batch Completed ✅ Success: {success}/{n}') + await m.reply_text(f'Batch Completed ✅ Success: {success}/{n}') finally: await remove_active_batch(uid) diff --git a/plugins/pay.py b/plugins/pay.py.disabled similarity index 74% rename from plugins/pay.py rename to plugins/pay.py.disabled index 1d1fc72e..1769bd9b 100644 --- a/plugins/pay.py +++ b/plugins/pay.py.disabled @@ -34,6 +34,10 @@ async def p(c, m): @app.on_callback_query(f.regex("^p_")) async def i(c, q): + if not q.from_user: + await q.answer("Error: Invalid user", show_alert=True) + return + pl = q.data.split("_")[1] pi = P0[pl] try: @@ -55,8 +59,9 @@ async def pc(c, q: Q): @app.on_message(f.successful_payment) async def sp(c, m): + from config import OWNER_ID p = m.successful_payment - u = m.from_user.id + u = m.from_user.id if m.from_user else 0 pl = p.invoice_payload.split("_")[0] pi = P0[pl] ok, r = await apu(u, pi['du'], pi['u']) @@ -70,15 +75,21 @@ async def sp(c, m): f"⏰ Till: {d} IST\n" f"🔖 Txn: `{p.telegram_payment_charge_id}`" ) - for o in OWNER_ID: - await c.send_message(f"User {u} just purchased the premium, txn id is {p.telegram_payment_charge_id}.") + if OWNER_ID: + for o in OWNER_ID: + try: + await c.send_message(o, f"User {u} just purchased the premium, txn id is {p.telegram_payment_charge_id}.") + except Exception: + pass else: await m.reply_text( f"⚠️ Paid but premium failed.\nTxn `{p.telegram_payment_charge_id}`" ) - for o in OWNER_ID: - await c.send_message(o, - f"⚠️ Issue!\nUser {u}\nPlan {pi['l']}\nTxn {p.telegram_payment_charge_id}\nErr {r}" - ) - - + if OWNER_ID: + for o in OWNER_ID: + try: + await c.send_message(o, + f"⚠️ Issue!\nUser {u}\nPlan {pi['l']}\nTxn {p.telegram_payment_charge_id}\nErr {r}" + ) + except Exception: + pass diff --git a/plugins/premium.py b/plugins/premium.py.disabled similarity index 97% rename from plugins/premium.py rename to plugins/premium.py.disabled index 4025b193..b5c7992d 100644 --- a/plugins/premium.py +++ b/plugins/premium.py.disabled @@ -102,4 +102,4 @@ async def start_handler(client, message): fd, caption=b6, reply_markup=kb - ) \ No newline at end of file + ) diff --git a/plugins/start.py b/plugins/start.py index 81791507..2d5177d3 100644 --- a/plugins/start.py +++ b/plugins/start.py @@ -9,24 +9,35 @@ from config import LOG_GROUP, OWNER_ID, FORCE_SUB async def subscribe(app, message): - if FORCE_SUB: + if not FORCE_SUB or FORCE_SUB == 0: + return 0 + + if not message.from_user: + return 1 + + try: + user = await app.get_chat_member(FORCE_SUB, message.from_user.id) + if str(user.status) == "ChatMemberStatus.BANNED": + await message.reply_text("You are Banned. Contact -- Team SPY") + return 1 + except UserNotParticipant: try: - user = await app.get_chat_member(FORCE_SUB, message.from_user.id) - if str(user.status) == "ChatMemberStatus.BANNED": - await message.reply_text("You are Banned. Contact -- Team SPY") - return 1 - except UserNotParticipant: link = await app.export_chat_invite_link(FORCE_SUB) caption = f"Join our channel to use the bot" await message.reply_photo(photo="https://graph.org/file/d44f024a08ded19452152.jpg",caption=caption, reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton("Join Now...", url=f"{link}")]])) return 1 - except Exception as ggn: - await message.reply_text(f"Something Went Wrong. Contact admins... with following message {ggn}") - return 1 + except Exception: + await message.reply_text("Please join the required channel to use this bot.") + return 1 + except Exception as ggn: + print(f"Error in subscribe check: {ggn}") + return 0 + + return 0 @app.on_message(filters.command("set")) async def set(_, message): - if message.from_user.id not in OWNER_ID: + if not message.from_user or message.from_user.id not in OWNER_ID: await message.reply("You are not authorized to use this command.") return @@ -38,14 +49,8 @@ async def set(_, message): BotCommand("logout", "🚪 Get out of the bot"), BotCommand("adl", "👻 Download audio from 30+ sites"), BotCommand("dl", "💀 Download videos from 30+ sites"), - BotCommand("status", "⟳ Refresh Payment status"), - BotCommand("transfer", "💘 Gift premium to others"), - BotCommand("add", "➕ Add user to premium"), - BotCommand("rem", "➖ Remove from premium"), BotCommand("rembot", "🤨 Remove your custom bot"), BotCommand("settings", "⚙️ Personalize things"), - BotCommand("plan", "🗓️ Check our premium plans"), - BotCommand("terms", "🥺 Terms and conditions"), BotCommand("help", "❓ If you're a noob, still!"), BotCommand("cancel", "🚫 Cancel login/batch/settings process"), BotCommand("stop", "🚫 Cancel batch process") @@ -59,51 +64,42 @@ async def set(_, message): help_pages = [ ( "📝 **Bot Commands Overview (1/2)**:\n\n" - "1. **/add userID**\n" - "> Add user to premium (Owner only)\n\n" - "2. **/rem userID**\n" - "> Remove user from premium (Owner only)\n\n" - "3. **/transfer userID**\n" - "> Transfer premium to your beloved major purpose for resellers (Premium members only)\n\n" - "4. **/get**\n" - "> Get all user IDs (Owner only)\n\n" - "5. **/lock**\n" - "> Lock channel from extraction (Owner only)\n\n" - "6. **/dl link**\n" - "> Download videos (Not available in v3 if you are using)\n\n" - "7. **/adl link**\n" - "> Download audio (Not available in v3 if you are using)\n\n" - "8. **/login**\n" + "1. **/login**\n" "> Log into the bot for private channel access\n\n" - "9. **/batch**\n" - "> Bulk extraction for posts (After login)\n\n" + "2. **/logout**\n" + "> Logout from the bot\n\n" + "3. **/batch**\n" + "> Bulk extraction for posts (Free for all users, limit: 1000)\n\n" + "4. **/single**\n" + "> Process a single message link\n\n" + "5. **/dl link**\n" + "> Download videos from 30+ sites\n\n" + "6. **/adl link**\n" + "> Download audio from 30+ sites\n\n" + "7. **/setbot**\n" + "> Add your custom bot for handling files\n\n" + "8. **/rembot**\n" + "> Remove your custom bot\n\n" ), ( "📝 **Bot Commands Overview (2/2)**:\n\n" - "10. **/logout**\n" - "> Logout from the bot\n\n" + "9. **/session**\n" + "> Generate Pyrogram V2 session\n\n" + "10. **/settings**\n" + "> Personalize bot settings:\n" + "> • SETCHATID: Upload directly to channel/group/DM\n" + "> • SETRENAME: Add custom rename tag\n" + "> • CAPTION: Add custom caption\n" + "> • REPLACEWORDS: Replace words in filenames\n" + "> • RESET: Reset to default settings\n\n" "11. **/stats**\n" - "> Get bot stats\n\n" - "12. **/plan**\n" - "> Check premium plans\n\n" - "13. **/speedtest**\n" - "> Test the server speed (not available in v3)\n\n" - "14. **/terms**\n" - "> Terms and conditions\n\n" - "15. **/cancel**\n" + "> Get bot statistics\n\n" + "12. **/cancel** or **/stop**\n" "> Cancel ongoing batch process\n\n" - "16. **/myplan**\n" - "> Get details about your plans\n\n" - "17. **/session**\n" - "> Generate Pyrogram V2 session\n\n" - "18. **/settings**\n" - "> 1. SETCHATID : To directly upload in channel or group or user's dm use it with -100[chatID]\n" - "> 2. SETRENAME : To add custom rename tag or username of your channels\n" - "> 3. CAPTION : To add custom caption\n" - "> 4. REPLACEWORDS : Can be used for words in deleted set via REMOVE WORDS\n" - "> 5. RESET : To set the things back to default\n\n" - "> You can set CUSTOM THUMBNAIL, PDF WATERMARK, VIDEO WATERMARK, SESSION-based login, etc. from settings\n\n" - "**__Powered by Team SPY__**" + "13. **/help**\n" + "> Show this help message\n\n" + "**__Powered by Team SPY__**\n" + "**All features are FREE!**" ) ] @@ -159,77 +155,3 @@ async def on_help_navigation(client, callback_query): await callback_query.answer() -@app.on_message(filters.command("terms") & filters.private) -async def terms(client, message): - terms_text = ( - "> 📜 **Terms and Conditions** 📜\n\n" - "✨ We are not responsible for user deeds, and we do not promote copyrighted content. If any user engages in such activities, it is solely their responsibility.\n" - "✨ Upon purchase, we do not guarantee the uptime, downtime, or the validity of the plan. __Authorization and banning of users are at our discretion; we reserve the right to ban or authorize users at any time.__\n" - "✨ Payment to us **__does not guarantee__** authorization for the /batch command. All decisions regarding authorization are made at our discretion and mood.\n" - ) - - buttons = InlineKeyboardMarkup( - [ - [InlineKeyboardButton("📋 See Plans", callback_data="see_plan")], - [InlineKeyboardButton("💬 Contact Now", url="https://t.me/kingofpatal")], - ] - ) - await message.reply_text(terms_text, reply_markup=buttons) - - -@app.on_message(filters.command("plan") & filters.private) -async def plan(client, message): - plan_text = ( - "> 💰 **Premium Price**:\n\n Starting from $2 or 200 INR accepted via **__Amazon Gift Card__** (terms and conditions apply).\n" - "📥 **Download Limit**: Users can download up to 100,000 files in a single batch command.\n" - "🛑 **Batch**: You will get two modes /bulk and /batch.\n" - " - Users are advised to wait for the process to automatically cancel before proceeding with any downloads or uploads.\n\n" - "📜 **Terms and Conditions**: For further details and complete terms and conditions, please send /terms.\n" - ) - - buttons = InlineKeyboardMarkup( - [ - [InlineKeyboardButton("📜 See Terms", callback_data="see_terms")], - [InlineKeyboardButton("💬 Contact Now", url="https://t.me/kingofpatal")], - ] - ) - await message.reply_text(plan_text, reply_markup=buttons) - - -@app.on_callback_query(filters.regex("see_plan")) -async def see_plan(client, callback_query): - plan_text = ( - "> 💰**Premium Price**\n\n Starting from $2 or 200 INR accepted via **__Amazon Gift Card__** (terms and conditions apply).\n" - "📥 **Download Limit**: Users can download up to 100,000 files in a single batch command.\n" - "🛑 **Batch**: You will get two modes /bulk and /batch.\n" - " - Users are advised to wait for the process to automatically cancel before proceeding with any downloads or uploads.\n\n" - "📜 **Terms and Conditions**: For further details and complete terms and conditions, please send /terms or click See Terms👇\n" - ) - - buttons = InlineKeyboardMarkup( - [ - [InlineKeyboardButton("📜 See Terms", callback_data="see_terms")], - [InlineKeyboardButton("💬 Contact Now", url="https://t.me/kingofpatal")], - ] - ) - await callback_query.message.edit_text(plan_text, reply_markup=buttons) - - -@app.on_callback_query(filters.regex("see_terms")) -async def see_terms(client, callback_query): - terms_text = ( - "> 📜 **Terms and Conditions** 📜\n\n" - "✨ We are not responsible for user deeds, and we do not promote copyrighted content. If any user engages in such activities, it is solely their responsibility.\n" - "✨ Upon purchase, we do not guarantee the uptime, downtime, or the validity of the plan. __Authorization and banning of users are at our discretion; we reserve the right to ban or authorize users at any time.__\n" - "✨ Payment to us **__does not guarantee__** authorization for the /batch command. All decisions regarding authorization are made at our discretion and mood.\n" - ) - - buttons = InlineKeyboardMarkup( - [ - [InlineKeyboardButton("📋 See Plans", callback_data="see_plan")], - [InlineKeyboardButton("💬 Contact Now", url="https://t.me/kingofpatal")], - ] - ) - await callback_query.message.edit_text(terms_text, reply_markup=buttons) - - diff --git a/shared_client.py b/shared_client.py index 4a2960df..b4b8a325 100644 --- a/shared_client.py +++ b/shared_client.py @@ -7,22 +7,41 @@ from pyrogram import Client import sys -client = TelegramClient("telethonbot", API_ID, API_HASH) -app = Client("pyrogrambot", api_id=API_ID, api_hash=API_HASH, bot_token=BOT_TOKEN) -userbot = Client("4gbbot", api_id=API_ID, api_hash=API_HASH, session_string=STRING) +client = None +app = None +userbot = None async def start_client(): - if not client.is_connected(): - await client.start(bot_token=BOT_TOKEN) - print("SpyLib started...") - if STRING: - try: - await userbot.start() - print("Userbot started...") - except Exception as e: - print(f"Hey honey!! check your premium string session, it may be invalid of expire {e}") - sys.exit(1) - await app.start() - print("Pyro App Started...") - return client, app, userbot - + global client, app, userbot + + if not API_ID or not API_HASH or not BOT_TOKEN: + print("ERROR: API_ID, API_HASH, and BOT_TOKEN are required!") + print("Please set them in Replit Secrets.") + sys.exit(1) + + try: + client = TelegramClient("telethonbot", int(API_ID), API_HASH) + app = Client("pyrogrambot", api_id=int(API_ID), api_hash=API_HASH, bot_token=BOT_TOKEN) + + if STRING: + userbot = Client("4gbbot", api_id=int(API_ID), api_hash=API_HASH, session_string=STRING) + + if not client.is_connected(): + await client.start(bot_token=BOT_TOKEN) + print("SpyLib started...") + + if STRING and userbot: + try: + await userbot.start() + print("Userbot started...") + except Exception as e: + print(f"Warning: Could not start userbot - {e}") + userbot = None + + await app.start() + print("Pyro App Started...") + + return client, app, userbot + except Exception as e: + print(f"Error starting clients: {e}") + sys.exit(1) diff --git a/utils/custom_filters.py b/utils/custom_filters.py index 436f0bb2..d3d84949 100644 --- a/utils/custom_filters.py +++ b/utils/custom_filters.py @@ -7,6 +7,8 @@ user_steps = {} def login_filter_func(_, __, message): + if not message.from_user: + return False user_id = message.from_user.id return user_id in user_steps @@ -20,4 +22,4 @@ def set_user_step(user_id, step=None): def get_user_step(user_id): - return user_steps.get(user_id) \ No newline at end of file + return user_steps.get(user_id) diff --git a/utils/func.py b/utils/func.py index 4a4db951..6ad5814b 100644 --- a/utils/func.py +++ b/utils/func.py @@ -57,15 +57,55 @@ def hhmmss(seconds): def E(L): - private_match = re.match(r'https://t\.me/c/(\d+)/(?:\d+/)?(\d+)', L) - public_match = re.match(r'https://t\.me/([^/]+)/(?:\d+/)?(\d+)', L) + """ + Extract Telegram link information with support for: + - Sub-group/Topic IDs: https://t.me/c/3181292229/76838/84203 + - Ranges: https://t.me/c/3181292229/76838/84203-84206 + - Normal links: https://t.me/c/3181292229/17575 + - Supports http://, https://, telegram.me, t.me - if private_match: - return f'-100{private_match.group(1)}', int(private_match.group(2)), 'private' - elif public_match: - return public_match.group(1), int(public_match.group(2)), 'public' + Returns: (chat_id, message_id/range, link_type, topic_id) + """ + # Normalize the link to handle various formats + L = L.strip().rstrip('/') - return None, None, None + # Private link with topic/sub-group and optional range: (http(s)://)(t.me|telegram.me)/c/CHAT/TOPIC/MSG(-MSG2) + private_topic = re.match(r'(?:https?://)?(?:t\.me|telegram\.me)/c/(\d+)/(\d+)/(\d+)(?:-(\d+))?', L) + # Private link without topic: (http(s)://)(t.me|telegram.me)/c/CHAT/MSG(-MSG2) + private_normal = re.match(r'(?:https?://)?(?:t\.me|telegram\.me)/c/(\d+)/(\d+)(?:-(\d+))?$', L) + # Public link with topic: (http(s)://)(t.me|telegram.me)/USERNAME/TOPIC/MSG(-MSG2) + public_topic = re.match(r'(?:https?://)?(?:t\.me|telegram\.me)/([^/]+)/(\d+)/(\d+)(?:-(\d+))?', L) + # Public link without topic: (http(s)://)(t.me|telegram.me)/USERNAME/MSG(-MSG2) + public_normal = re.match(r'(?:https?://)?(?:t\.me|telegram\.me)/([^/]+)/(\d+)(?:-(\d+))?$', L) + + if private_topic: + chat_id = f'-100{private_topic.group(1)}' + topic_id = int(private_topic.group(2)) + start_msg = int(private_topic.group(3)) + end_msg = int(private_topic.group(4)) if private_topic.group(4) else None + msg_range = (start_msg, end_msg) if end_msg else start_msg + return chat_id, msg_range, 'private', topic_id + elif private_normal: + chat_id = f'-100{private_normal.group(1)}' + start_msg = int(private_normal.group(2)) + end_msg = int(private_normal.group(3)) if private_normal.group(3) else None + msg_range = (start_msg, end_msg) if end_msg else start_msg + return chat_id, msg_range, 'private', None + elif public_topic: + chat_id = public_topic.group(1) + topic_id = int(public_topic.group(2)) + start_msg = int(public_topic.group(3)) + end_msg = int(public_topic.group(4)) if public_topic.group(4) else None + msg_range = (start_msg, end_msg) if end_msg else start_msg + return chat_id, msg_range, 'public', topic_id + elif public_normal: + chat_id = public_normal.group(1) + start_msg = int(public_normal.group(2)) + end_msg = int(public_normal.group(3)) if public_normal.group(3) else None + msg_range = (start_msg, end_msg) if end_msg else start_msg + return chat_id, msg_range, 'public', None + + return None, None, None, None def get_display_name(user): @@ -194,8 +234,9 @@ async def process_text_with_rules(user_id, text): delete_words = await get_user_data_key(user_id, "delete_words", []) processed_text = text - for word, replacement in replacements.items(): - processed_text = processed_text.replace(word, replacement) + if replacements and isinstance(replacements, dict): + for word, replacement in replacements.items(): + processed_text = processed_text.replace(word, replacement) if delete_words: words = processed_text.split()