Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
320 changes: 305 additions & 15 deletions main.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,310 @@
import os
import telebot
from dotenv import load_dotenv
import os, json, uuid
from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup, ReplyKeyboardMarkup
from telegram.ext import (
ApplicationBuilder, CommandHandler,
CallbackQueryHandler, MessageHandler,
ContextTypes, filters
)
from config import TOKEN, OWNER_ID, FORCE_CHANNEL
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🚨 CRITICAL: Exposed Telegram Bot Token - Revoke Immediately

The PR description shows config.py contains a hardcoded bot token (8526112717:AAHCpL4uGVHIYCt9Fl0zJXgNS7u5hXLxNB0). This token is now publicly exposed in the PR and git history. You must immediately:

  1. Revoke this token via @BotFather on Telegram
  2. Generate a new token
  3. Use environment variables instead of hardcoding credentials
import os
TOKEN = os.environ["TELEGRAM_BOT_TOKEN"]
OWNER_ID = int(os.environ["TELEGRAM_OWNER_ID"])
FORCE_CHANNEL = os.environ.get("FORCE_CHANNEL", "@Hq_Cracker")

Even after the PR is closed or reverted, the token remains in git history and can be exploited.

🤖 Prompt for AI Agents
In main.py around line 8, the repo currently imports a hardcoded Telegram bot
token from config.py which has been exposed; immediately revoke the exposed
token in BotFather and generate a new one, remove the hardcoded secret from the
repository, and stop committing secrets. Replace the config import with a
runtime configuration that reads TELEGRAM_BOT_TOKEN and TELEGRAM_OWNER_ID (and
optional FORCE_CHANNEL) from environment variables (or a .env loaded only in
development), update code to parse OWNER_ID as an integer, add any local secrets
files to .gitignore, and rotate the token everywhere; finally purge the secret
from git history using git filter-repo or BFG to prevent further exposure.


# Load environment variables
load_dotenv()
# ---------- PATHS ----------
FILES_DB = "data/files.json"
ADMINS_DB = "data/admins.json"
USERS_DB = "data/users.json"
MESSAGES_DB = "data/messages.json"
CLOSED_CHATS_DB = "data/closed_chats.json"

# Replace 'TELEGRAM_BOT_TOKEN' with the token you received from BotFather
TOKEN = os.getenv('TELEGRAM_BOT_TOKEN')
bot = telebot.TeleBot(TOKEN)
os.makedirs("files/free", exist_ok=True)
os.makedirs("files/paid", exist_ok=True)
os.makedirs("data", exist_ok=True)

@bot.message_handler(commands=['start', 'hello'])
def send_welcome(message):
bot.reply_to(message, "Hello! I'm a simple Telegram bot.")
for db in [FILES_DB, ADMINS_DB, USERS_DB, MESSAGES_DB, CLOSED_CHATS_DB]:
if not os.path.exists(db):
if db == ADMINS_DB:
json.dump([OWNER_ID], open(db, "w", encoding="utf-8"))
elif db == CLOSED_CHATS_DB:
json.dump([], open(db, "w", encoding="utf-8"))
else:
json.dump([], open(db, "w", encoding="utf-8"))
Comment on lines +21 to +28
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Resource leak: file handles not closed

Using open() directly inside json.dump() without a context manager leaves file handles unclosed. This can cause resource exhaustion.

Proposed fix
 for db in [FILES_DB, ADMINS_DB, USERS_DB, MESSAGES_DB, CLOSED_CHATS_DB]:
     if not os.path.exists(db):
         if db == ADMINS_DB:
-            json.dump([OWNER_ID], open(db, "w", encoding="utf-8"))
-        elif db == CLOSED_CHATS_DB:
-            json.dump([], open(db, "w", encoding="utf-8"))
+            with open(db, "w", encoding="utf-8") as f:
+                json.dump([OWNER_ID], f)
         else:
-            json.dump([], open(db, "w", encoding="utf-8"))
+            with open(db, "w", encoding="utf-8") as f:
+                json.dump([], f)
🤖 Prompt for AI Agents
In main.py around lines 21 to 28, the code calls open() inside json.dump() which
leaves file handles unclosed; replace those calls with context managers so each
file is opened via with open(db, "w", encoding="utf-8") as f: json.dump(..., f)
(use the same conditional logic for ADMINS_DB and CLOSED_CHATS_DB but write
through the managed file object) to ensure files are properly closed and
resources are not leaked.


@bot.message_handler(func=lambda msg: True)
def echo_all(message):
bot.reply_to(message, message.text)
# ---------- UTILS ----------
def load(path):
with open(path, encoding="utf-8") as f:
return json.load(f)

bot.polling()
def save(path, data):
with open(path, "w", encoding="utf-8") as f:
json.dump(data, f, indent=2, ensure_ascii=False)

def is_admin(uid):
return uid in load(ADMINS_DB)

async def is_member(bot, uid):
try:
m = await bot.get_chat_member(FORCE_CHANNEL, uid)
return m.status in ["member", "administrator", "creator"]
except:
return False
Comment on lines +42 to +47
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Bare except silently swallows all exceptions

This catches everything including KeyboardInterrupt, SystemExit, and programming errors, making debugging difficult. At minimum, catch Exception and consider logging failures.

Proposed fix
 async def is_member(bot, uid):
     try:
         m = await bot.get_chat_member(FORCE_CHANNEL, uid)
-        return m.status in ["member", "administrator", "creator"]
-    except:
+        if m.status in ["member", "administrator", "creator"]:
+            return True
         return False
+    except Exception:
+        return False
🧰 Tools
🪛 Ruff (0.14.10)

45-45: Consider moving this statement to an else block

(TRY300)


46-46: Do not use bare except

(E722)

🤖 Prompt for AI Agents
In main.py around lines 42 to 47, the function uses a bare except which swallows
all exceptions (including KeyboardInterrupt/SystemExit) — change it to catch
Exception (e.g. except Exception as e), log the exception (using the module
logger or bot.logger) with context (failed to get_chat_member FORCED_CHANNEL and
uid), and then return False; optionally consider catching and handling more
specific API exceptions separately if needed.


def load_users():
data = load(USERS_DB)
return data if isinstance(data, list) else []

def add_user(uid):
users = load_users()
if uid not in users:
users.append(uid)
save(USERS_DB, users)

def load_files():
data = load(FILES_DB)
return data if isinstance(data, list) else []

# ---------- KEYBOARDS ----------
def main_reply_keyboard():
# منوی ثابت کنار دکمه سنجاق
return ReplyKeyboardMarkup(
[["🔙 Back to Main Menu"]],
resize_keyboard=True
)

def join_keyboard():
return InlineKeyboardMarkup([
[InlineKeyboardButton("📢 Join Channel", url="https://t.me/Hq_Cracker")],
[InlineKeyboardButton("✅ Verify Membership", callback_data="check_join")]
])

def user_menu(is_admin_user=False):
kb = [
[InlineKeyboardButton("📂 Free Files", callback_data="free_list")],
[InlineKeyboardButton("💰 Paid Files", callback_data="paid_list")],
[InlineKeyboardButton("📞 Contact Admin", callback_data="contact")],
[InlineKeyboardButton("📩 Message Admin", callback_data="message_admin")]
]
if is_admin_user:
kb.append([InlineKeyboardButton("👑 Admin Panel", callback_data="admin_panel")])
return InlineKeyboardMarkup(kb)

def admin_panel():
return InlineKeyboardMarkup([
[InlineKeyboardButton("📤 Upload Free File", callback_data="upload_free")],
[InlineKeyboardButton("💰 Upload Paid File", callback_data="upload_paid")],
[InlineKeyboardButton("📊 File Stats", callback_data="stats")],
[InlineKeyboardButton("📣 Broadcast Message", callback_data="admin_broadcast")],
[InlineKeyboardButton("👥 Manage Admins", callback_data="manage_admins")],
[InlineKeyboardButton("👁 User Stats", callback_data="user_stats")]
])

def back_button(is_admin_user=False):
return InlineKeyboardMarkup([[InlineKeyboardButton("🔙 Back", callback_data="back_main")]])

def close_chat_kb():
return InlineKeyboardMarkup([[InlineKeyboardButton("🔒 Close Chat", callback_data="close_this_chat")]])

# ---------- FUNCTIONS ----------
async def start(update: Update, context: ContextTypes.DEFAULT_TYPE):
uid = update.effective_user.id
if not await is_member(context.bot, uid):
await update.message.reply_text("❌ You must join the channel first:", reply_markup=join_keyboard())
return
add_user(uid)
welcome_text = "🚀 Welcome to Hq_Cracker!\n\nPremium cracked accounts & high-quality combo lists."
# ارسال منوی شیشه‌ای + فعال کردن منوی کنار سنجاق
await update.message.reply_text(
welcome_text,
reply_markup=user_menu(is_admin(uid))
)
await update.message.reply_text("⚡ Quick Navigation Enabled:", reply_markup=main_reply_keyboard())

async def unblock_user(update, context):
if update.effective_user.id != OWNER_ID: return
try:
target_uid = int(context.args[0])
closed = load(CLOSED_CHATS_DB)
if target_uid in closed:
closed.remove(target_uid); save(CLOSED_CHATS_DB, closed)
await update.message.reply_text(f"✅ User {target_uid} unblocked.")
else: await update.message.reply_text("❌ Not blocked.")
except: await update.message.reply_text("❌ Usage: /unblock USER_ID")

async def broadcast_new_file(context, file_name, file_type, description=None, price=None, fid=None):
users = load_users()
text = f"📢 New {file_type} file: {file_name}"
for uid in users:
try: await context.bot.send_message(uid, text)
except: continue
Comment on lines +130 to +135
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Unused parameters and silent exception swallowing

The description, price, and fid parameters are declared but never used, and the bare except: continue silently drops broadcast failures. Consider logging failures for debugging.

Proposed fix
-async def broadcast_new_file(context, file_name, file_type, description=None, price=None, fid=None):
+import logging
+
+async def broadcast_new_file(context, file_name, file_type):
     users = load_users()
     text = f"📢 New {file_type} file: {file_name}"
     for uid in users:
-        try: await context.bot.send_message(uid, text)
-        except: continue
+        try:
+            await context.bot.send_message(uid, text)
+        except Exception as e:
+            logging.warning(f"Failed to broadcast to {uid}: {e}")
+            continue

Committable suggestion skipped: line range outside the PR's diff.

🧰 Tools
🪛 Ruff (0.14.10)

130-130: Unused function argument: description

(ARG001)


130-130: Unused function argument: price

(ARG001)


130-130: Unused function argument: fid

(ARG001)


134-134: Multiple statements on one line (colon)

(E701)


135-135: Do not use bare except

(E722)


135-135: try-except-continue detected, consider logging the exception

(S112)


135-135: Multiple statements on one line (colon)

(E701)

🤖 Prompt for AI Agents
In main.py around lines 130 to 135, the function broadcast_new_file declares
unused parameters (description, price, fid) and silently swallows all exceptions
with a bare except; update the function to either remove unused parameters or
incorporate them into the broadcast message (e.g., append description, price,
and fid when present), and replace the bare except with a specific Exception
catch that logs the failure (including the uid, file_name and the caught
exception) before continuing so broadcast errors are visible for debugging.


# ---------- BUTTONS ----------
async def buttons(update: Update, context: ContextTypes.DEFAULT_TYPE):
q = update.callback_query
try: await q.answer()
except: pass
uid = q.from_user.id
data = load_files()

if q.data == "check_join":
if await is_member(context.bot, uid):
await q.message.edit_text("✅ Verified.", reply_markup=user_menu(is_admin(uid)))
else: await q.answer("❌ Join first!", show_alert=True)

elif q.data == "back_main":
context.user_data.clear()
await q.message.edit_text("🔹 Main Menu", reply_markup=user_menu(is_admin(uid)))

elif q.data == "free_list":
kb = [[InlineKeyboardButton(f"📄 {f['name']} ({f['downloads']})", callback_data=f"get_{f['id']}")] for f in data if f["type"] == "free"]
await q.message.reply_text("📂 Free Files:", reply_markup=InlineKeyboardMarkup(kb) if kb else back_button(is_admin(uid)))

elif q.data.startswith("get_"):
fid = q.data.replace("get_", "")
for f in data:
if f["id"] == fid:
path = f"files/free/{f['name']}"
f["downloads"] += 1; save(FILES_DB, data)
await q.message.reply_document(open(path, "rb"))
return
Comment on lines +158 to +165
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Resource leak: file handle never closed

open(path, "rb") creates a file handle that is never explicitly closed. Use a context manager or ensure the handle is closed after use.

Proposed fix
     elif q.data.startswith("get_"):
         fid = q.data.replace("get_", "")
         for f in data:
             if f["id"] == fid:
                 path = f"files/free/{f['name']}"
-                f["downloads"] += 1; save(FILES_DB, data)
-                await q.message.reply_document(open(path, "rb"))
+                f["downloads"] += 1
+                save(FILES_DB, data)
+                with open(path, "rb") as doc:
+                    await q.message.reply_document(doc)
                 return
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
elif q.data.startswith("get_"):
fid = q.data.replace("get_", "")
for f in data:
if f["id"] == fid:
path = f"files/free/{f['name']}"
f["downloads"] += 1; save(FILES_DB, data)
await q.message.reply_document(open(path, "rb"))
return
elif q.data.startswith("get_"):
fid = q.data.replace("get_", "")
for f in data:
if f["id"] == fid:
path = f"files/free/{f['name']}"
f["downloads"] += 1
save(FILES_DB, data)
with open(path, "rb") as doc:
await q.message.reply_document(doc)
return
🧰 Tools
🪛 Ruff (0.14.10)

163-163: Multiple statements on one line (semicolon)

(E702)

🤖 Prompt for AI Agents
In main.py around lines 158 to 165 the code opens the file with open(path, "rb")
and passes the raw file object to reply_document without ever closing it,
causing a resource leak; change this to use a context manager so the file is
opened inside a with block (with open(path, "rb") as fh:) and call await
q.message.reply_document(fh) from within that block, ensuring the file handle is
closed automatically after sending; keep the downloads increment and
save(FILES_DB, data) as-is but ensure the save happens before or inside the
with-block as needed.


elif q.data == "paid_list":
kb = [[InlineKeyboardButton(f"📄 {f['name']} - ${f['price']}", callback_data=f"buy_{f['id']}")] for f in data if f["type"] == "paid"]
await q.message.reply_text("💰 Paid Files:", reply_markup=InlineKeyboardMarkup(kb) if kb else back_button(is_admin(uid)))

elif q.data.startswith("buy_"):
fid = q.data.replace("buy_", "")
for f in data:
if f["id"] == fid:
await context.bot.send_message(OWNER_ID, f"📥 Paid file request from user {uid}:\nFile: {f['name']}\nPrice: ${f['price']}")
await q.message.reply_text(f"✅ Purchase request sent to admin.")
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

f-string without placeholders

This f-string has no interpolation; it's a plain string. Remove the f prefix.

-                await q.message.reply_text(f"✅ Purchase request sent to admin.")
+                await q.message.reply_text("✅ Purchase request sent to admin.")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
await q.message.reply_text(f"✅ Purchase request sent to admin.")
await q.message.reply_text("✅ Purchase request sent to admin.")
🧰 Tools
🪛 Ruff (0.14.10)

176-176: f-string without any placeholders

Remove extraneous f prefix

(F541)

🤖 Prompt for AI Agents
In main.py around line 176, the reply_text call uses an f-string with no
placeholders; remove the unnecessary f prefix so the string is a normal literal
(change f"✅ Purchase request sent to admin." to "✅ Purchase request sent to
admin."). Ensure there are no other empty f-strings on the same line.


elif q.data == "contact":
await q.message.reply_text("📞 Contact: @Cracker_Proo / @Ali_storr", reply_markup=back_button(is_admin(uid)))

elif q.data == "message_admin":
context.user_data["msg_to_admin"] = True
await q.message.reply_text("✏️ Send your message to admin:", reply_markup=back_button(is_admin(uid)))

elif q.data == "admin_panel" and is_admin(uid):
await q.message.reply_text("👑 Admin Panel", reply_markup=admin_panel())

elif q.data == "upload_free" and is_admin(uid):
context.user_data["upload"] = "free"
await q.message.reply_text("📤 Send free file")

elif q.data == "upload_paid" and is_admin(uid):
context.user_data["upload"] = "paid"
await q.message.reply_text("💰 Send paid file")

elif q.data == "admin_broadcast" and is_admin(uid):
context.user_data["broadcasting"] = True
await q.message.reply_text("📣 Send message/photo to broadcast:")

elif q.data == "stats" and is_admin(uid):
text = "📊 File Stats:\n\n" + "\n".join([f"📄 {f['name']} -> {f['downloads']}" for f in data])
await q.message.reply_text(text, reply_markup=back_button(True))

elif q.data == "user_stats" and is_admin(uid):
users = load_users()
await q.message.reply_text(f"👥 Total Users: {len(users)}", reply_markup=back_button(True))

elif q.data == "close_this_chat" and uid == OWNER_ID:
try:
target_uid = int(q.message.text.split("user ")[1].split(":")[0].strip())
closed = load(CLOSED_CHATS_DB)
if target_uid not in closed:
closed.append(target_uid); save(CLOSED_CHATS_DB, closed)
await q.message.edit_text(q.message.text + "\n\n🔒 (CHAT CLOSED)")
await context.bot.send_message(target_uid, "🚫 Admin closed this chat.")
except: await q.answer("Error")
Comment on lines +208 to +216
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Fragile user ID parsing from message text

Parsing target_uid from message text using string splitting is brittle and error-prone. If the message format changes, this will fail silently. Consider storing user context in context.user_data or message metadata instead.

Proposed fix
     elif q.data == "close_this_chat" and uid == OWNER_ID:
         try:
-            target_uid = int(q.message.text.split("user ")[1].split(":")[0].strip())
+            # Store target_uid in callback_data instead: e.g., "close_chat_12345"
+            msg_text = q.message.text
+            if "Message from user " not in msg_text:
+                await q.answer("Cannot parse user ID", show_alert=True)
+                return
+            target_uid = int(msg_text.split("Message from user ")[1].split(":")[0].strip())
             closed = load(CLOSED_CHATS_DB)
             if target_uid not in closed:
-                closed.append(target_uid); save(CLOSED_CHATS_DB, closed)
+                closed.append(target_uid)
+                save(CLOSED_CHATS_DB, closed)
             await q.message.edit_text(q.message.text + "\n\n🔒 (CHAT CLOSED)")
             await context.bot.send_message(target_uid, "🚫 Admin closed this chat.")
-        except: await q.answer("Error")
+        except Exception as e:
+            await q.answer(f"Error: {e}", show_alert=True)

Committable suggestion skipped: line range outside the PR's diff.

🧰 Tools
🪛 Ruff (0.14.10)

213-213: Multiple statements on one line (semicolon)

(E702)


216-216: Do not use bare except

(E722)


216-216: Multiple statements on one line (colon)

(E701)

🤖 Prompt for AI Agents
In main.py around lines 208 to 216, the code parses target_uid by fragile string
splitting of q.message.text which will break if message format changes; instead
retrieve the target user id from a reliable source (e.g. context.user_data,
callback data, or message.entities/metadata) and validate it before use: look up
the saved user id in context.user_data or extract it from q.data (or
q.message.reply_to_message.from_user.id if applicable), ensure it's an int with
safe conversion and explicit error handling, replace the bare except with a
specific except (ValueError/KeyError) that logs the error and notifies the
admin, and then update CLOSED_CHATS_DB and send notifications as before.


# ---------- HANDLERS ----------
async def add_admin(update, context):
if update.effective_user.id != OWNER_ID: return
try:
uid = int(context.args[0])
admins = load(ADMINS_DB)
if uid not in admins: admins.append(uid); save(ADMINS_DB, admins)
await update.message.reply_text("✅ Admin added")
except: pass
Comment on lines +219 to +226
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Silent failures in admin management commands

Both add_admin and del_admin use bare except: pass, completely hiding any errors (invalid input, file I/O issues, etc.). This makes debugging impossible.

Proposed fix for add_admin
 async def add_admin(update, context):
-    if update.effective_user.id != OWNER_ID: return
+    if update.effective_user.id != OWNER_ID:
+        return
     try:
         uid = int(context.args[0])
         admins = load(ADMINS_DB)
-        if uid not in admins: admins.append(uid); save(ADMINS_DB, admins)
+        if uid not in admins:
+            admins.append(uid)
+            save(ADMINS_DB, admins)
         await update.message.reply_text("✅ Admin added")
-    except: pass
+    except (IndexError, ValueError):
+        await update.message.reply_text("❌ Usage: /addadmin USER_ID")
+    except Exception as e:
+        await update.message.reply_text(f"❌ Error: {e}")

Also applies to: 228-235

🧰 Tools
🪛 Ruff (0.14.10)

220-220: Multiple statements on one line (colon)

(E701)


224-224: Multiple statements on one line (colon)

(E701)


224-224: Multiple statements on one line (semicolon)

(E702)


226-226: Do not use bare except

(E722)


226-226: try-except-pass detected, consider logging the exception

(S110)


226-226: Multiple statements on one line (colon)

(E701)

🤖 Prompt for AI Agents
In main.py around lines 219-226 (and similarly 228-235 for del_admin), the
handlers use a bare "except: pass" which silences all errors; update them to
validate inputs (check context.args length and that args[0] is an int), restrict
exception handling to expected exceptions (ValueError for int conversion,
OSError/IOError for file I/O, or a generic Exception logged), and on error reply
to the user with a clear message (e.g., "Invalid user id" or "Failed to update
admins") while logging the full exception details for debugging; ensure the
OWNER_ID check remains and remove the silent pass so failures are visible and
recoverable.


async def del_admin(update, context):
if update.effective_user.id != OWNER_ID: return
try:
uid = int(context.args[0])
admins = load(ADMINS_DB)
if uid in admins and uid != OWNER_ID: admins.remove(uid); save(ADMINS_DB, admins)
await update.message.reply_text("✅ Admin removed")
except: pass

async def handle_upload(update, context):
if not is_admin(update.effective_user.id): return
upload_type = context.user_data.get("upload")
if not upload_type or not update.message.document: return
doc = update.message.document
fid = uuid.uuid4().hex[:6]
path = f"files/{upload_type}/{doc.file_name}"
await (await doc.get_file()).download_to_drive(path)
if upload_type == "free":
data = load_files(); data.append({"id":fid,"name":doc.file_name,"type":"free","downloads":0}); save(FILES_DB, data)
await update.message.reply_text("✅ Uploaded"); await broadcast_new_file(context, doc.file_name, "free")
context.user_data.clear()
else:
context.user_data["paid_file"] = {"id":fid,"file_path":path,"type":"paid"}
await update.message.reply_text("💰 Name | Description | Price")

async def handle_paid_details(update, context):
try:
name, desc, price = [x.strip() for x in update.message.text.split("|")]
finfo = context.user_data["paid_file"]
finfo.update({"name":name,"description":desc,"price":price,"downloads":0})
data = load_files(); data.append(finfo); save(FILES_DB, data)
await update.message.reply_text("✅ Paid File Saved"); await broadcast_new_file(context, name, "paid", desc, price, finfo["id"])
context.user_data.clear()
except: await update.message.reply_text("❌ Format: Name | Description | Price")

async def message_router(update: Update, context: ContextTypes.DEFAULT_TYPE):
uid = update.effective_user.id
text = update.message.text or ""

# واکنش به دکمه بازگشت کنار سنجاق
if text == "🔙 Back to Main Menu":
context.user_data.clear()
await update.message.reply_text("🔹 Main Menu", reply_markup=user_menu(is_admin(uid)))
return

if context.user_data.get("broadcasting") and is_admin(uid):
users = load_users()
count = 0
for user_id in users:
try: await update.message.copy(chat_id=user_id); count += 1
except: continue
context.user_data.pop("broadcasting")
await update.message.reply_text(f"✅ Sent to {count} users.")
return

if uid == OWNER_ID and update.message.reply_to_message:
msg_text = update.message.reply_to_message.text
if "Message from user" in msg_text:
try:
target_uid = int(msg_text.split("user ")[1].split(":")[0].strip())
await context.bot.send_message(target_uid, f"💬 Admin Reply:\n{update.message.text}")
await update.message.reply_text("✅ Sent.")
return
except: pass

if context.user_data.get("msg_to_admin"):
if uid in load(CLOSED_CHATS_DB):
await update.message.reply_text("❌ Chat is closed.")
return
await context.bot.send_message(OWNER_ID, f"📩 Message from user {uid}:\n{text}", reply_markup=close_chat_kb())
await update.message.reply_text("✅ Sent to admin.")

# ---------- RUN ----------
app = ApplicationBuilder().token(TOKEN).connect_timeout(30).read_timeout(30).build()
app.add_handler(CommandHandler("start", start))
app.add_handler(CommandHandler("addadmin", add_admin))
app.add_handler(CommandHandler("deladmin", del_admin))
app.add_handler(CommandHandler("unblock", unblock_user))
app.add_handler(CallbackQueryHandler(buttons))
app.add_handler(MessageHandler(filters.Document.ALL, handle_upload))
app.add_handler(MessageHandler(filters.ALL & ~filters.COMMAND, message_router))
print("Bot is running...")
app.run_polling()