Skip to content

Latest commit

ย 

History

History
773 lines (654 loc) ยท 29.4 KB

File metadata and controls

773 lines (654 loc) ยท 29.4 KB

import re from telegram import Update from telegram.ext import ContextTypes from bot.config import OWNER_ID, UPLOAD_CHANNEL_ID, LOG_CHAT_ID from bot.database.mongo import users_collection, characters_collection, sudos_collection from bot.utils.formatters import escape_markdown

Rarity mapping and emojis

RARITY_MAP = { "1": "Common", "2": "Uncommon", "3": "Rare", "4": "Legendary", "5": "Mystical", "6": "Divine", "7": "Crossverse", "8": "Supreme", "9": "Cataphract", "Common": "Common", "Uncommon": "Uncommon", "Rare": "Rare", "Legendary": "Legendary", "Mystical": "Mystical", "Divine": "Divine", "Crossverse": "Crossverse", "Supreme": "Supreme", "Cataphract": "Cataphract", "Medium": "Uncommon" } RARITY_EMOJI = { "Common": "๐Ÿ”ต", "Uncommon": "๐ŸŸฃ", "Rare": "๐ŸŸ ", "Legendary": "๐ŸŸก", "Mystical": "๐Ÿ’ฎ", "Divine": "โšœ๏ธ", "Crossverse": "โšก", "Supreme": "๐Ÿชž", "Cataphract": "โœจ" }

def clean_character_name(name: str) -> str: """Consistently clean character names for matching and storage.""" name = name.strip() # Remove decorative prefixes (symbols, "V ", etc.) name = re.sub(r"^[^\w\s]+\s*", "", name) name = re.sub(r"^V\s+", "", name, flags=re.IGNORECASE) return name.strip()

async def get_next_char_id(): """Find the absolute maximum numeric ID to ensure uniqueness.""" # Fetch characters with numeric IDs cursor = characters_collection.find({"id": {"$regex": "^[0-9]+$"}}) max_id = 0 async for char in cursor: try: cid = int(char["id"]) if cid > max_id: max_id = cid except (ValueError, KeyError): continue

# Also check total count as a safety measure
count = await characters_collection.count_documents({})
max_id = max(max_id, count)

return str(max_id + 1)

async def send_log(context: ContextTypes.DEFAULT_TYPE, text: str, file_id: str = None, file_type: str = "photo"): """Send a log message (with optional media) to the log chat.""" if not LOG_CHAT_ID: return try: if file_id: if file_type == "video": await context.bot.send_video(chat_id=LOG_CHAT_ID, video=file_id, caption=text, parse_mode="HTML") elif file_type == "document": await context.bot.send_document(chat_id=LOG_CHAT_ID, document=file_id, caption=text, parse_mode="HTML") else: await context.bot.send_photo(chat_id=LOG_CHAT_ID, photo=file_id, caption=text, parse_mode="HTML") else: await context.bot.send_message(chat_id=LOG_CHAT_ID, text=text, parse_mode="HTML") except Exception as e: print(f"Error sending log: {e}")

async def is_sudo(user_id: int) -> bool: """Check if a user is a sudo user or the owner.""" if user_id == OWNER_ID: return True sudo = await sudos_collection.find_one({"user_id": user_id}) return sudo is not None

async def has_power(user_id: int, power: str) -> bool: """Check if a user has a specific power.""" if user_id == OWNER_ID: return True sudo = await sudos_collection.find_one({"user_id": user_id}) if not sudo: return False return power in sudo.get("powers", [])

async def check_admin(update: Update, power: str = None) -> bool: """Check if the user is the owner or a sudo with the required power.""" user_id = update.effective_user.id if user_id == OWNER_ID: return True if power: if await has_power(user_id, power): return True elif await is_sudo(user_id): return True if update.message: await update.message.reply_text("โŒ You don't have permission to use this command.") return False

async def check_owner(update: Update) -> bool: """Check if the user is the owner only (not sudo).""" if update.effective_user.id != OWNER_ID: if update.message: await update.message.reply_text("โŒ Only the bot owner can use this command.") return False return True

def build_sudo_keyboard(sudo_id: int, powers: list): """Build inline keyboard for managing sudo powers.""" from telegram import InlineKeyboardButton, InlineKeyboardMarkup upload_status = "โœ…" if "upload" in powers else "โŒ" delete_status = "โœ…" if "delete" in powers else "โŒ" keyboard = [ [InlineKeyboardButton(f"{upload_status} Upload Power", callback_data=f"sudo_toggle{sudo_id}upload")], [InlineKeyboardButton(f"{delete_status} Delete Power", callback_data=f"sudo_toggle{sudo_id}delete")], [InlineKeyboardButton("๐Ÿ—‘๏ธ Remove Sudo", callback_data=f"sudo_remove{sudo_id}")], [InlineKeyboardButton("โœ… Done", callback_data="sudo_done")] ] return InlineKeyboardMarkup(keyboard)

async def addsudo_cmd(update: Update, context: ContextTypes.DEFAULT_TYPE): """/addsudo <telegram_id> - Add a sudo user with powers (owner only).""" if not await check_owner(update): return

if not context.args:
    await update.message.reply_text("Usage: /addsudo <telegram_id>")
    return

try:
    sudo_id = int(context.args[0])
except ValueError:
    await update.message.reply_text("โŒ Please provide a valid Telegram user ID.")
    return

existing = await sudos_collection.find_one({"user_id": sudo_id})
if existing:
    powers = existing.get("powers", [])
    keyboard = _build_sudo_keyboard(sudo_id, powers)
    await update.message.reply_text(
        f"โš™๏ธ Manage powers for sudo <code>{sudo_id}</code>:",
        parse_mode="HTML", reply_markup=keyboard
    )
    return

await sudos_collection.insert_one({"user_id": sudo_id, "powers": ["upload", "delete"]})
keyboard = _build_sudo_keyboard(sudo_id, ["upload", "delete"])
await update.message.reply_text(
    f"โœ… User <code>{sudo_id}</code> added as sudo.\nโš™๏ธ Manage powers:",
    parse_mode="HTML", reply_markup=keyboard
)
await send_log(context, f"๐Ÿ‘ค <b>Sudo Added</b>\nBy: {update.effective_user.first_name} (<code>{update.effective_user.id}</code>)\nTarget: <code>{sudo_id}</code>")

async def resudo_cmd(update: Update, context: ContextTypes.DEFAULT_TYPE): """/resudo <telegram_id> - Remove a sudo user (owner only).""" if not await check_owner(update): return

if not context.args:
    await update.message.reply_text("Usage: /resudo <telegram_id>")
    return

try:
    sudo_id = int(context.args[0])
except ValueError:
    await update.message.reply_text("โŒ Please provide a valid Telegram user ID.")
    return

result = await sudos_collection.delete_one({"user_id": sudo_id})
if result.deleted_count > 0:
    await update.message.reply_text(f"โœ… User <code>{sudo_id}</code> has been removed from sudo.", parse_mode="HTML")
    await send_log(context, f"๐Ÿ—‘๏ธ <b>Sudo Removed</b>\nBy: {update.effective_user.first_name} (<code>{update.effective_user.id}</code>)\nTarget: <code>{sudo_id}</code>")
else:
    await update.message.reply_text(f"โŒ User {sudo_id} is not a sudo.")

async def sudo_callback_handler(update: Update, context: ContextTypes.DEFAULT_TYPE): """Handle inline button clicks for sudo power management.""" query = update.callback_query user_id = query.from_user.id

if user_id != OWNER_ID:
    await query.answer("โŒ Only the owner can manage sudos.", show_alert=True)
    return

data = query.data

if data == "sudo_done":
    await query.edit_message_text("โœ… Sudo management complete.")
    await query.answer()
    return

if data.startswith("sudo_remove_"):
    sudo_id = int(data.split("_")[2])
    await sudos_collection.delete_one({"user_id": sudo_id})
    await query.edit_message_text(f"๐Ÿ—‘๏ธ User <code>{sudo_id}</code> removed from sudo.", parse_mode="HTML")
    await query.answer("Removed!")
    return

if data.startswith("sudo_toggle_"):
    parts = data.split("_")
    sudo_id = int(parts[2])
    power = parts[3]

    sudo = await sudos_collection.find_one({"user_id": sudo_id})
    if not sudo:
        await query.answer("โŒ User is no longer a sudo.", show_alert=True)
        return

    powers = sudo.get("powers", [])
    if power in powers:
        powers.remove(power)
    else:
        powers.append(power)

    await sudos_collection.update_one(
        {"user_id": sudo_id},
        {"$set": {"powers": powers}}
    )

    keyboard = _build_sudo_keyboard(sudo_id, powers)
    await query.edit_message_reply_markup(reply_markup=keyboard)
    status = "enabled" if power in powers else "disabled"
    await query.answer(f"{power.capitalize()} power {status}!")
    await send_log(context, f"โš™๏ธ <b>Sudo Power Toggle</b>\nSudo: <code>{sudo_id}</code>\nPower: {power.capitalize()} is now {status.upper()}")

async def forward_save_handler(update: Update, context: ContextTypes.DEFAULT_TYPE): """ Auto-saves characters when the owner sends/forwards an image with caption. Works in private chat, groups, and channels. """ message = update.message if not message: return

print(f"[AUTO-SAVE] Triggered! Chat: {message.chat.type} | From: {message.from_user.id if message.from_user else 'None'} | Has photo: {bool(message.photo)} | Has caption: {bool(message.caption)}")

# Only work for the owner
if not message.from_user or message.from_user.id != OWNER_ID:
    print(f"[AUTO-SAVE] Skipped - not owner")
    return

# Must have media
file_type = "photo"
file_id = None
if message.photo:
    file_id = message.photo[-1].file_id
elif message.video:
    file_id = message.video.file_id
    file_type = "video"
elif message.document:
    file_id = message.document.file_id
    file_type = "document"

if not file_id:
    print(f"[AUTO-SAVE] Skipped - no media")
    return

# Must have a caption
caption = message.caption
if not caption:
    print(f"[AUTO-SAVE] Skipped - no caption")
    return

print(f"[AUTO-SAVE] Caption: {caption[:80]}...")

# Parse caption
anime_match = re.search(r"(?:๐Ÿซง Anime:|Anime:|โžค From:)\s*(.+)", caption, re.IGNORECASE)
name_match = re.search(r"(?:๐Ÿ–๏ธ Character Name:|Character Name:|Name:)\s*(.+?)(?:\s*\[.+\])?(?:\n|$)", caption, re.IGNORECASE)

if not name_match or not anime_match:
    print(f"[AUTO-SAVE] Skipped - caption parsing failed. Name: {bool(name_match)}, Anime: {bool(anime_match)}")
    return  # Silently skip if caption doesn't match character format

name = name_match.group(1).strip()
anime = anime_match.group(1).split('\n')[0].split('|')[0].strip()

rarity_match = re.search(r"๐™๐˜ผ๐™๐™„๐™๐™”:\s*(\w+)", caption, re.IGNORECASE)
if rarity_match:
    rarity = rarity_match.group(1).capitalize()
else:
    old_rarity_match = re.search(r"Rarity:\s*(.+)", caption, re.IGNORECASE)
    if old_rarity_match:
        rarity = old_rarity_match.group(1).strip().capitalize()
    else:
        rarity = "Mystical"

final_rarity = RARITY_MAP.get(rarity, rarity)
rem_emoji = RARITY_EMOJI.get(final_rarity, "๐Ÿ’ฎ")

# CATAPHRACT ENFORCEMENT: 
# Cataphract (9) MUST be a video/document.
# Others MUST be a photo.
if final_rarity == "Cataphract":
    if file_type == "photo":
        await message.reply_text(f"โŒ <b>Cataphract</b> rarity requires a <b>Video</b> or <b>Document</b> upload.", parse_mode="HTML")
        return
else:
    if file_type != "photo":
        await message.reply_text(f"โŒ <b>{final_rarity}</b> rarity requires a <b>Photo</b> upload. Only <b>Cataphract</b> can be a video.", parse_mode="HTML")
        return

# Generate next ID
char_id = await get_next_char_id()

# Create character object
character = {
    "id": char_id,
    "name": name,
    "anime": anime,
    "rarity": final_rarity,
    "file_id": file_id,
    "file_type": file_type
}

await characters_collection.insert_one(character)

success_msg = (
    f"โœ… <b>Successfully Auto-Saved!</b>\n\n"
    f"๐Ÿท๏ธ <b>Name:</b> {escape_markdown(name)} (ID: {char_id})\n"
    f"๐Ÿซง <b>Anime:</b> {escape_markdown(anime)}\n"
    f"{rem_emoji} <b>Rarity:</b> {final_rarity}"
)

await message.reply_text(success_msg, parse_mode="HTML")
await send_log(
    context, 
    f"๐Ÿ†• <b>Auto-Saved Character</b>\n"
    f"Name: {escape_markdown(name)}\n"
    f"Anime: {escape_markdown(anime)}\n"
    f"Rarity: {rem_emoji} {final_rarity}\n"
    f"ID: {char_id}", 
    file_id=file_id, 
    file_type=file_type
)

async def channel_post_handler(update: Update, context: ContextTypes.DEFAULT_TYPE): """ Auto uploads waifus from a specific channel. """ message = update.channel_post print(f"DEBUG: Received channel post in {message.chat.id if message else 'None'}")

if not message or message.chat.id != UPLOAD_CHANNEL_ID:
    print(f"DEBUG: Message skipped. Target: {UPLOAD_CHANNEL_ID}")
    return

file_type = "photo"
file_id = None
if message.photo:
    file_id = message.photo[-1].file_id
elif message.video:
    file_id = message.video.file_id
    file_type = "video"
elif message.document:
    file_id = message.document.file_id
    file_type = "document"

if not file_id:
    print("DEBUG: No media found in message.")
    return

caption = message.caption
print(f"DEBUG: Caption received: {repr(caption)}")

if not caption:
    return

# Parse caption
anime_match = re.search(r"(?:๐Ÿซง Anime:|Anime:|โžค From:)\s*(.+)", caption, re.IGNORECASE)
name_match = re.search(r"(?:๐Ÿ–๏ธ Character Name:|Character Name:|Name:)\s*(.+?)(?:\s*\[.+\])?(?:\n|$)", caption, re.IGNORECASE)

if name_match and anime_match:
    name = clean_character_name(name_match.group(1))
    anime = anime_match.group(1).split('\n')[0].split('|')[0].strip()
    
    rarity_match = re.search(r"๐™๐˜ผ๐™๐™„๐™๐™”:\s*(\w+)", caption, re.IGNORECASE)
    if rarity_match:
        rarity = rarity_match.group(1).capitalize()
    else:
        old_rarity_match = re.search(r"Rarity:\s*(.+)", caption, re.IGNORECASE)
        if old_rarity_match:
            rarity = old_rarity_match.group(1).strip().capitalize()
        else:
            rarity = "Mystical"

    final_rarity = RARITY_MAP.get(rarity, rarity)
    rem_emoji = RARITY_EMOJI.get(final_rarity, "๐Ÿ’ฎ")

    # CATAPHRACT ENFORCEMENT
    if final_rarity == "Cataphract":
        if file_type == "photo":
            return # Silently skip invalid uploads in channel
    else:
        if file_type != "photo":
            return

    char_id = await get_next_char_id()

    character = {
        "id": char_id,
        "name": name,
        "anime": anime,
        "rarity": final_rarity,
        "file_id": file_id,
        "file_type": file_type
    }

    await characters_collection.insert_one(character)
    await send_log(
        context, 
        f"๐Ÿ†• <b>Auto-Saved Character</b>\n"
        f"Name: {escape_markdown(name)}\n"
        f"Anime: {escape_markdown(anime)}\n"
        f"Rarity: {rem_emoji} {final_rarity}\n"
        f"ID: {char_id} (from Channel)",
        file_id=file_id,
        file_type=file_type
    )
else:
    # Optional: log parsing failure
    pass

async def upload_cmd(update: Update, context: ContextTypes.DEFAULT_TYPE): """Fallback manual upload command: /upload name, anime, rarity OR reply to a message with a caption and just use /upload """ if not await check_admin(update, power="upload"): return

message = update.message
if not message.reply_to_message or not (message.reply_to_message.photo or message.reply_to_message.document or message.reply_to_message.video):
    await message.reply_text("Please reply to an image/video/document to upload.")
    return

# Check for manual arguments
raw_args = " ".join(context.args).strip()
char_type = None
if raw_args:
    args = [a.strip() for a in raw_args.split(",")]
    if len(args) < 3:
        await message.reply_text("Manual Usage: /upload Name, Anime, Rarity, [Type]\nOr reply to a message with a caption containing Name and Anime.")
        return
    
    name = args[0]
    anime = args[1]
    rarity = args[2]
    if len(args) >= 4:
        char_type = args[3]
else:
    # Auto-parse from caption
    caption = message.reply_to_message.caption or message.reply_to_message.text or ""
    if not caption:
         await message.reply_text("Original message has no caption. Please provide manual arguments.")
         return
         
    anime_match = re.search(r"(?:๐Ÿซง Anime:|Anime:|โžค From:)\s*(.+)", caption, re.IGNORECASE)
    name_match = re.search(r"(?:๐Ÿ–๏ธ Character Name:|Character Name:|Name:)\s*(.+?)(?:\s*\[.+\])?(?:\n|$)", caption, re.IGNORECASE)
    
    if not name_match or not anime_match:
         await message.reply_text("Could not parse Name or Anime from the caption. Please use the manual format: /upload Name, Anime, Rarity, [Type]")
         return
         
    name = clean_character_name(name_match.group(1))
    anime = anime_match.group(1).split('\n')[0].split('|')[0].strip()
    
    rarity_match = re.search(r"๐™๐˜ผ๐™๐™„๐™๐™”:\s*(\w+)", caption, re.IGNORECASE)
    if rarity_match:
        rarity = rarity_match.group(1).capitalize()
    else:
        old_rarity_match = re.search(r"Rarity:\s*(.+)", caption, re.IGNORECASE)
        if old_rarity_match:
            rarity = old_rarity_match.group(1).strip().capitalize()
        else:
            rarity = "Mystical"

file_type = "photo"
if message.reply_to_message.photo:
    file_id = message.reply_to_message.photo[-1].file_id
elif message.reply_to_message.video:
    file_id = message.reply_to_message.video.file_id
    file_type = "video"
elif message.reply_to_message.document:
    file_id = message.reply_to_message.document.file_id
    file_type = "document"

final_rarity = RARITY_MAP.get(rarity, rarity)
rem_emoji = RARITY_EMOJI.get(final_rarity, "๐Ÿ’ฎ")

# CATAPHRACT ENFORCEMENT
if final_rarity == "Cataphract":
    if file_type == "photo":
        await message.reply_text(f"โŒ <b>Cataphract</b> rarity requires a <b>Video</b> or <b>Document</b> upload.", parse_mode="HTML")
        return
else:
    if file_type != "photo":
        await message.reply_text(f"โŒ <b>{final_rarity}</b> rarity requires a <b>Photo</b> upload. Only <b>Cataphract</b> can be a video.", parse_mode="HTML")
        return

char_id = await get_next_char_id()

character = {
    "id": char_id,
    "name": name,
    "anime": anime,
    "rarity": final_rarity,
    "file_id": file_id,
    "file_type": file_type
}
if char_type:
    character["type"] = char_type

await characters_collection.insert_one(character)

success_msg = (
    f"โœ… <b>Successfully Uploaded!</b>\n\n"
    f"๐Ÿท๏ธ <b>Name:</b> {escape_markdown(name)} (ID: {char_id})\n"
    f"๐Ÿซง <b>Anime:</b> {escape_markdown(anime)}\n"
    f"{rem_emoji} <b>Rarity:</b> {final_rarity}"
)
if char_type:
    success_msg += f"\n๐Ÿ”– <b>Type:</b> {escape_markdown(char_type)}"

await message.reply_text(success_msg, parse_mode="HTML")
await send_log(
    context, 
    f"๐Ÿ“ค <b>Character Uploaded</b>\n"
    f"By: {update.effective_user.first_name} (<code>{update.effective_user.id}</code>)\n"
    f"Name: {escape_markdown(name)}\n"
    f"Anime: {escape_markdown(anime)}\n"
    f"Rarity: {rem_emoji} {final_rarity}\n"
    f"ID: {char_id}" + (f"\nType: {char_type}" if char_type else ""),
    file_id=file_id,
    file_type=file_type
)

async def delete_cmd(update: Update, context: ContextTypes.DEFAULT_TYPE): """/delete ID1, ID2, ... or range like 1-100""" if not await check_admin(update, power="delete"): return

if not context.args:
    await update.message.reply_text("Usage: /delete Character ID(s) (comma separated or range 1-100)")
    return

raw_args = " ".join(context.args)
parts = [p.strip() for p in raw_args.split(",")]

# We'll first find the actual ID strings in the DB to ensure robust deletion
actual_ids_to_delete = set()

for part in parts:
    if "-" in part:
        try:
            start_str, end_str = part.split("-")
            start = int(start_str)
            end = int(end_str)
            # For ranges, we look for anything that matches ^0*N$ for N in [start, end]
            # To be efficient for large ranges, we'll search by regex on the ID field
            # If the range is small, we can generate all regexes. For large ranges,
            # we might just fetch all characters and filter in Python or use a clever regex.
            if end - start < 1000:
                for i in range(start, end + 1):
                    cursor = characters_collection.find({"id": re.compile(f"^0*{i}$")})
                    async for char in cursor:
                        actual_ids_to_delete.add(char['id'])
            else:
                # Very large range? Let's be careful.
                # We can use a regex that matches any number within the range if possible,
                # but simpler to just fetch all numeric IDs and check.
                async for char in characters_collection.find({"id": {"$regex": "^[0-9]+$"}}):
                    try:
                        val = int(char['id'])
                        if start <= val <= end:
                            actual_ids_to_delete.add(char['id'])
                    except: pass
        except ValueError:
            pass
    elif part.isdigit():
        # Robust match for digits (e.g., 0023 should match 23)
        norm_val = str(int(part))
        cursor = characters_collection.find({"id": re.compile(f"^0*{norm_val}$")})
        async for char in cursor:
            actual_ids_to_delete.add(char['id'])
    else:
        # Exact match for non-digit IDs or IDs with emojis
        char = await characters_collection.find_one({"id": part})
        if char:
            actual_ids_to_delete.add(char['id'])
        else:
            # Try matching by suffix in case of emojis
            cursor = characters_collection.find({"id": re.compile(f"{re.escape(part)}$")})
            async for char in cursor:
                actual_ids_to_delete.add(char['id'])

if not actual_ids_to_delete:
    await update.message.reply_text("โŒ No characters found matching the provided IDs.")
    return

target_list = list(actual_ids_to_delete)
# Perform the deletion
res = await characters_collection.delete_many({"id": {"$in": target_list}})

if res.deleted_count > 0:
    # Cleanup user collections
    from bot.database.mongo import users_collection
    await users_collection.update_many(
        {"waifus": {"$in": target_list}},
        {"$pull": {"waifus": {"$in": target_list}}}
    )
    await users_collection.update_many(
        {"favorite": {"$in": target_list}},
        {"$unset": {"favorite": ""}}
    )

    await update.message.reply_text(f"โœ… Successfully deleted {res.deleted_count} character(s) and updated user harems.")
    await send_log(context, f"๐Ÿ—‘๏ธ <b>Characters Deleted</b>\nBy: {update.effective_user.first_name} (<code>{update.effective_user.id}</code>)\nCount: {res.deleted_count}\nIDs: <code>{', '.join(list(target_list)[:20])}{'...' if len(target_list) > 20 else ''}</code>")
else:
    await update.message.reply_text("โŒ Failed to delete characters.")

async def stats_cmd(update: Update, context: ContextTypes.DEFAULT_TYPE): """/stats or /total - Owner command to see character counts by rarity and totals.""" if not await check_owner(update): return

# Count characters by rarity
pipeline = [
    {"$group": {"_id": "$rarity", "count": {"$sum": 1}}}
]
rarity_counts = await characters_collection.aggregate(pipeline).to_list(length=20)

total_chars = await characters_collection.count_documents({})
total_users = await users_collection.count_documents({})
from bot.database.mongo import groups_collection
total_groups = await groups_collection.count_documents({})

text = "๐Ÿ“Š <b>Bot Statistics</b>\n\n"
text += f"๐Ÿ‘ฅ <b>Total Users:</b> {total_users}\n"
text += f"๐Ÿ  <b>Total Groups:</b> {total_groups}\n"
text += f"๐Ÿท๏ธ <b>Total Characters:</b> {total_chars}\n\n"

text += "โœจ <b>Rarity Breakdown:</b>\n"
# Sort by my defined rarity order if possible, or just alphabetically
sorted_counts = sorted(rarity_counts, key=lambda x: x["count"], reverse=True)
for rc in sorted_counts:
    rarity = rc["_id"] or "Unknown"
    count = rc["count"]
    text += f"โ€ข {rarity}: {count}\n"
    
await update.message.reply_text(text, parse_mode="HTML")

async def broadcast_cmd(update: Update, context: ContextTypes.DEFAULT_TYPE): """/broadcast message""" if not await check_admin(update): return

if not context.args:
    await update.message.reply_text("Usage: /broadcast message")
    return

# In a real scenario, you'd iterate over all users/groups. We will implement this later.
await update.message.reply_text("Broadcasting...")
# The actual broadcast logic needs users_collection or groups_collection iterating.

async def changetime_cmd(update: Update, context: ContextTypes.DEFAULT_TYPE): """/changetime interval""" if not await check_admin(update): return

if not context.args:
    await update.message.reply_text("Usage: /changetime <number_of_messages>")
    return
    
try:
    target = int(context.args[0])
    if target < 1:
        raise ValueError
except ValueError:
    await update.message.reply_text("Please provide a valid positive integer.")
    return
    
chat_id = update.effective_chat.id

from bot.database.mongo import groups_collection
await groups_collection.update_one(
    {"id": chat_id},
    {"$set": {"spawn_target": target}},
    upsert=True
)

await update.message.reply_text(f"โœ… Spawn rate for this chat set to every {target} messages.")

async def ping_cmd(update: Update, context: ContextTypes.DEFAULT_TYPE): """Check bot health, latency, and system resources.""" import time try: import psutil import shutil except ImportError: psutil = None

start_time = time.time()
msg = await update.message.reply_text("Pinging...")
end_time = time.time()
latency = (end_time - start_time) * 1000

# System Info
sys_info = ""
if psutil:
    mem = psutil.virtual_memory()
    cpu = psutil.cpu_percent()
    # Storage
    usage = shutil.disk_usage("/")
    free_gb = usage.free / (1024**3)
    total_gb = usage.total / (1024**3)
    
    sys_info = (
        f"๐Ÿ’ป <b>System Stats:</b>\n"
        f"โ”œ RAM: <code>{mem.percent}%</code> ({mem.used//1024//1024}MB / {mem.total//1024//1024}MB)\n"
        f"โ”œ CPU: <code>{cpu}%</code>\n"
        f"โ”” Storage: <code>{free_gb:.1f}GB Free</code> / {total_gb:.1f}GB\n"
    )

await msg.edit_text(
    f"๐Ÿ <b>Pong!</b>\n"
    f"Latency: <code>{latency:.2f}ms</code>\n"
    f"Status: ๐ŸŸข Healthy\n\n"
    f"{sys_info}",
    parse_mode="HTML"
)

async def enable_cmd(update: Update, context: ContextTypes.DEFAULT_TYPE): """/enable <on/off> - Toggle games globally (owner only).""" if not await check_owner(update): return

if not context.args:
    await update.message.reply_text("Usage: /enable <on/off>")
    return

val = context.args[0].lower()
enabled = val == "on"

from bot.database.mongo import settings_collection
await settings_collection.update_one(
    {"id": "global"},
    {"$set": {"games_enabled": enabled}},
    upsert=True
)

status = "ENABLED" if enabled else "DISABLED"
await update.message.reply_text(f"โœ… Games have been {status} globally.")
await send_log(context, f"โš™๏ธ <b>Global Toggle</b>\nBy: {update.effective_user.first_name}\nGames: {status}")

async def spwanglobal_cmd(update: Update, context: ContextTypes.DEFAULT_TYPE): """/spwanglobal <on/off> - Toggle spawning globally in all groups (owner only).""" if not await check_owner(update): return

if not context.args:
    await update.message.reply_text("Usage: /spwanglobal <on/off>")
    return

val = context.args[0].lower()
enabled = val == "on"

from bot.database.mongo import settings_collection
await settings_collection.update_one(
    {"id": "global"},
    {"$set": {"spawn_enabled": enabled}},
    upsert=True
)

status = "ENABLED" if enabled else "DISABLED"
await update.message.reply_text(f"๐Ÿš€ Global spawning has been {status} across all groups.")
await send_log(context, f"โš™๏ธ <b>Global Spawn Toggle</b>\nBy: {update.effective_user.first_name}\nSpawning: {status}")