Spaces:
Configuration error
Configuration error
| import discord | |
| from discord.ext import commands | |
| from discord import app_commands, ui | |
| import asyncio | |
| from datetime import datetime, timedelta | |
| import time | |
| import os | |
| import json | |
| import config | |
| import random | |
| import io | |
| import re | |
| import aiohttp | |
| from typing import Dict, Any, Optional | |
| from gamemode_config import get_gamemode_for_guild, is_valid_gamemode_server | |
| # constants | |
| MISC_DIR = os.path.normpath(os.path.join(os.path.dirname(__file__), "..", "misc")) | |
| OPEN_HISTORY_PATH = os.path.join(MISC_DIR, "open_history.json") | |
| BANS_PATH = os.path.join(MISC_DIR, "bans.json") | |
| COOLDOWNS_PATH = os.path.join(MISC_DIR, "cooldowns.json") | |
| DATABASE_PATH = os.path.join(MISC_DIR, "database.json") | |
| RESULTS_DATA_FILE = os.path.join(MISC_DIR, "results.json") | |
| QueueTICKET_LOGS_CHANNEL_NAME = "queue-ticket-logs" | |
| COMMAND_LOGS_CHANNEL_NAME = "command-logs" | |
| TIER_ROLES = ["HT1", "HT2", "HT3", "HT4", "HT5", "LT1", "LT2", "LT3", "LT4", "LT5"] | |
| TIER_CHOICES = [app_commands.Choice(name=t, value=t) for t in TIER_ROLES] | |
| REGION_CHOICES = [app_commands.Choice(name=r, value=r) for r in ["AS", "EU", "ME", "AU", "NA"]] | |
| # Helper function to get gamemode-specific database path | |
| def get_gamemode_database_path(gamemode: str, db_type: str = "database") -> str: | |
| """Get the database path for a specific gamemode""" | |
| if db_type == "database": | |
| return os.path.join(MISC_DIR, f"database_{gamemode}.json") | |
| elif db_type == "results": | |
| return os.path.join(MISC_DIR, f"results_{gamemode}.json") | |
| elif db_type == "bans": | |
| return os.path.join(MISC_DIR, f"bans_{gamemode}.json") | |
| elif db_type == "cooldowns": | |
| return os.path.join(MISC_DIR, f"cooldowns_{gamemode}.json") | |
| elif db_type == "open_history": | |
| return os.path.join(MISC_DIR, f"open_history_{gamemode}.json") | |
| else: | |
| return os.path.join(MISC_DIR, f"{db_type}_{gamemode}.json") | |
| # Staff roles hierarchy (highest to lowest) | |
| STAFF_ROLES = [ | |
| getattr(config, "NETWORK_OWNER_ROLE_ID", None), | |
| getattr(config, "MANAGER_ROLE_ID", None), | |
| getattr(config, "OVERSEER_ROLE_ID", None), | |
| getattr(config, "ADMINISTRATOR_ROLE_ID", None), | |
| getattr(config, "REGULATOR_ROLE_ID", None), | |
| getattr(config, "NETWORK_MOD_ROLE_ID", None), | |
| getattr(config, "SENIOR_MOD_ROLE_ID", None), | |
| getattr(config, "SCREENSHARER_ROLE_ID", None), | |
| getattr(config, "DISCORD_MOD_ROLE_ID", None), | |
| getattr(config, "DISCORD_HELPER_ROLE_ID", None), | |
| getattr(config, "SENIOR_TESTER_ROLE_ID", None), | |
| getattr(config, "VERIFIED_TESTER_ROLE_ID", None), | |
| getattr(config, "STAFF_TEAM_ROLE_ID", None), | |
| ] | |
| RESULT_CHANNEL_ID = getattr(config, "RESULT_CHANNEL_ID", None) | |
| # ensure misc files / helpers | |
| def _ensure_misc_files(): | |
| try: | |
| os.makedirs(MISC_DIR, exist_ok=True) | |
| defaults = { | |
| OPEN_HISTORY_PATH: {"AS": [], "NA": [], "ME": [], "EU": [], "AU": []}, | |
| BANS_PATH: {}, | |
| COOLDOWNS_PATH: {}, | |
| DATABASE_PATH: {} | |
| } | |
| for path, default in defaults.items(): | |
| if not os.path.isfile(path): | |
| with open(path, "w", encoding="utf-8") as f: | |
| json.dump(default, f, ensure_ascii=False, indent=2) | |
| except Exception as e: | |
| print(f"Failed to ensure misc files: {e}") | |
| def load_json(path: str, default: Any): | |
| try: | |
| _ensure_misc_files() | |
| if not os.path.isfile(path): | |
| with open(path, "w", encoding="utf-8") as f: | |
| json.dump(default, f, ensure_ascii=False, indent=2) | |
| with open(path, "r", encoding="utf-8") as f: | |
| return json.load(f) | |
| except Exception as e: | |
| print(f"Failed to load {path}: {e}") | |
| return default | |
| def save_json(path: str, data: Any): | |
| try: | |
| os.makedirs(os.path.dirname(path), exist_ok=True) | |
| tmp = path + ".tmp" | |
| with open(tmp, "w", encoding="utf-8") as f: | |
| json.dump(data, f, ensure_ascii=False, indent=2) | |
| os.replace(tmp, path) | |
| except Exception as e: | |
| print(f"Failed to save {path}: {e}") | |
| # database helpers for verified users | |
| def add_verified_user(discord_id: int, username: str, region: str): | |
| """Add a verified user to database in format: discord_id-username-region""" | |
| try: | |
| database = load_json(DATABASE_PATH, {}) | |
| user_key = f"{discord_id}-{username}-{region}" | |
| database[user_key] = { | |
| "discord_id": discord_id, | |
| "username": username, | |
| "region": region, | |
| "verified_at": time.time() | |
| } | |
| save_json(DATABASE_PATH, database) | |
| return True | |
| except Exception as e: | |
| print(f"Failed to add verified user: {e}") | |
| return False | |
| def get_verified_user(discord_id: int): | |
| """Get verified user(s) by Discord ID""" | |
| try: | |
| database = load_json(DATABASE_PATH, {}) | |
| user_records = [] | |
| for key, data in database.items(): | |
| if data.get("discord_id") == discord_id: | |
| user_records.append(data) | |
| return user_records | |
| except Exception as e: | |
| print(f"Failed to get verified user: {e}") | |
| return [] | |
| def is_user_verified(discord_id: int) -> bool: | |
| """Check if a user is verified""" | |
| return len(get_verified_user(discord_id)) > 0 | |
| def get_all_verified_users(): | |
| """Get all verified users from database""" | |
| try: | |
| return load_json(DATABASE_PATH, {}) | |
| except Exception as e: | |
| print(f"Failed to get all verified users: {e}") | |
| return {} | |
| # Results data helpers | |
| def load_results_data(gamemode: Optional[str] = None): | |
| """Load results data from results.json (gamemode-specific if provided)""" | |
| if gamemode: | |
| path = get_gamemode_database_path(gamemode, "results") | |
| else: | |
| path = RESULTS_DATA_FILE | |
| return load_json(path, {"results": {}, "counter": 0}) | |
| def save_results_data(data, gamemode: Optional[str] = None): | |
| """Save results data to results.json (gamemode-specific if provided)""" | |
| if gamemode: | |
| path = get_gamemode_database_path(gamemode, "results") | |
| else: | |
| path = RESULTS_DATA_FILE | |
| save_json(path, data) | |
| def log_result(member_id, username, previous_tier, new_tier, region, tester_id, cooldown_type="queue", gamemode: Optional[str] = None): | |
| """Log a tier result and set cooldown""" | |
| data = load_results_data(gamemode) | |
| now = datetime.now().strftime("%Y-%m-%d %H:%M") | |
| key = str(member_id) | |
| data["results"][key] = { | |
| "username": username, | |
| "previous_tier": previous_tier, | |
| "new_tier": new_tier, | |
| "region": region, | |
| "tester": tester_id, | |
| "time": now | |
| } | |
| data["counter"] = data.get("counter", 0) + 1 | |
| save_results_data(data, gamemode) | |
| # Apply cooldown separately (7 days for queue, 14 days for high tier) | |
| cooldowns_path = get_gamemode_database_path(gamemode, "cooldowns") if gamemode else COOLDOWNS_PATH | |
| cooldowns = load_json(cooldowns_path, {}) | |
| cooldown_days = 14 if cooldown_type == "high_ticket" else 7 | |
| expiry_time = time.time() + (cooldown_days * 86400) | |
| # Store in separate keys to avoid interference | |
| if cooldown_type == "high_ticket": | |
| if "high_ticket" not in cooldowns: | |
| cooldowns["high_ticket"] = {} | |
| cooldowns["high_ticket"][str(member_id)] = { | |
| "expires_at": expiry_time | |
| } | |
| else: | |
| if "queue" not in cooldowns: | |
| cooldowns["queue"] = {} | |
| cooldowns["queue"][str(member_id)] = { | |
| "expires_at": expiry_time | |
| } | |
| save_json(COOLDOWNS_PATH, cooldowns) | |
| # permission helpers (check if member has staff role) | |
| def _member_has_role(member: discord.Member, role_cfg) -> bool: | |
| if not member or role_cfg is None: | |
| return False | |
| try: | |
| role_id = int(role_cfg) | |
| except Exception: | |
| role_id = None | |
| if role_id: | |
| return any(r.id == role_id for r in member.roles) | |
| return any(r.name == str(role_cfg) for r in member.roles) | |
| def is_allowed_member(member: discord.Member) -> bool: | |
| """Check if member has any staff role or is global owner""" | |
| # Global owner check | |
| GLOBAL_OWNER_ID = 966338171958886420 | |
| if member.id == GLOBAL_OWNER_ID: | |
| return True | |
| for role_id in STAFF_ROLES: | |
| if _member_has_role(member, role_id): | |
| return True | |
| return False | |
| # ================== ROLE LEVEL CHECKING ================== | |
| # Role hierarchy (lower index = higher authority) | |
| ROLE_LEVELS = { | |
| "owner": [0, "NETWORK_OWNER_ROLE_ID"], # Network Owner | |
| "manager": [1, "MANAGER_ROLE_ID"], # Manager | |
| "overseer": [2, "OVERSEER_ROLE_ID"], # Overseer | |
| "administrator": [3, "ADMINISTRATOR_ROLE_ID"], # Administrator | |
| "regulator": [4, "REGULATOR_ROLE_ID"], # Regulator | |
| "network_mod": [5, "NETWORK_MOD_ROLE_ID"], # Network Mod | |
| "senior_mod": [6, "SENIOR_MOD_ROLE_ID"], # Senior Mod | |
| "screensharer": [7, "SCREENSHARER_ROLE_ID"], # Screensharer | |
| "discord_mod": [8, "DISCORD_MOD_ROLE_ID"], # Discord Mod | |
| "discord_helper": [9, "DISCORD_HELPER_ROLE_ID"], # Discord Helper | |
| "senior_tester": [10, "SENIOR_TESTER_ROLE_ID"], # Senior Tester | |
| "tester": [11, "VERIFIED_TESTER_ROLE_ID"], # Tester (Verified Tester) | |
| "staff_team": [12, "STAFF_TEAM_ROLE_ID"], # Staff Team (lowest) | |
| } | |
| def _get_member_role_level(member: discord.Member) -> int: | |
| """Get the role level of a member (lower = higher authority). Returns 999 if no staff role.""" | |
| # Global owner check - can run any command anywhere | |
| GLOBAL_OWNER_ID = 966338171958886420 | |
| if member.id == GLOBAL_OWNER_ID: | |
| return 0 # Highest authority (level 0) | |
| # Load role IDs directly from roles.json to ensure we have the latest values | |
| roles_data = load_json(os.path.join(MISC_DIR, "roles.json"), {}) | |
| for level_name, (level_val, role_config_name) in ROLE_LEVELS.items(): | |
| # Try to get role ID from roles.json first, then fall back to config | |
| role_id = roles_data.get(role_config_name) or getattr(config, role_config_name, None) | |
| if _member_has_role(member, role_id): | |
| return level_val | |
| return 999 # Not a staff member | |
| def _is_member_at_least(member: discord.Member, min_role_level_name: str) -> bool: | |
| """Check if member has at least the specified role level or higher""" | |
| if min_role_level_name not in ROLE_LEVELS: | |
| return False | |
| min_level = ROLE_LEVELS[min_role_level_name][0] | |
| member_level = _get_member_role_level(member) | |
| return member_level <= min_level | |
| def _get_role_name_for_level(level_name: str) -> str: | |
| """Get the display name for a role level""" | |
| display_names = { | |
| "owner": "Owner", | |
| "manager": "Manager", | |
| "overseer": "Overseer", | |
| "administrator": "Administrator", | |
| "regulator": "Regulator", | |
| "network_mod": "Network Mod", | |
| "senior_mod": "Senior Mod", | |
| "screensharer": "Screensharer", | |
| "discord_mod": "Discord Mod", | |
| "discord_helper": "Discord Helper", | |
| "senior_tester": "Senior Tester", | |
| "tester": "Tester", | |
| "staff_team": "Staff Team", | |
| } | |
| return display_names.get(level_name, level_name) | |
| # ================== COMMAND REGISTRY ================== | |
| COMMAND_REGISTRY = { | |
| "openqueue": {"description": "Open a queue for testing", "min_role": "tester"}, | |
| "closequeue": {"description": "Close a queue for testing", "min_role": "tester"}, | |
| "pickfromqueue": {"description": "Pick the next person from the queue", "min_role": "tester"}, | |
| "queuesystemsetup": {"description": "Set up the queue system channels and roles", "min_role": "owner"}, | |
| "leave": {"description": "Leave the active queue", "min_role": "staff_team"}, # Anyone can use | |
| "clearwaitlist": {"description": "Remove a user from queue (optional temp ban)", "min_role": "tester"}, | |
| "clearcooldown": {"description": "Clear the 7-day cooldown for a user", "min_role": "regulator"}, | |
| "queuestatus": {"description": "Check queue status for all regions", "min_role": "senior_tester"}, | |
| "log": {"description": "Show queue openings for the last N days (default 7)", "min_role": "administrator"}, | |
| "setuproles": {"description": "Set up staff roles with vibrant colors and hierarchy", "min_role": "owner"}, | |
| "add": {"description": "Add a user to this ticket", "min_role": "tester"}, | |
| "remove": {"description": "Remove a user from this ticket", "min_role": "tester"}, | |
| "close": {"description": "Close the current ticket with tier assignment", "min_role": "tester"}, | |
| "passevel": {"description": "Move queue ticket to high test category (for worthy candidates)", "min_role": "tester"}, | |
| "addrole": {"description": "Force add a role to the ticket with tester permissions", "min_role": "tester"}, | |
| "addrandom": {"description": "Add an online user with the given role, or assign randomly if none online", "min_role": "overseer"}, | |
| "retire": {"description": "Retire a user and remove their tier", "min_role": "senior_tester"}, | |
| "highticketpanel": {"description": "Display the High Tier Test panel", "min_role": "overseer"}, | |
| "unretire_pannel": {"description": "Display the Unretire Test panel", "min_role": "owner"}, | |
| "remove_tier": {"description": "Remove a tier from a user", "min_role": "regulator"}, | |
| "give_tier": {"description": "Give a specific tier to a user", "min_role": "regulator"}, | |
| "clearresultdata": {"description": "Delete stored result history of a player", "min_role": "regulator"}, | |
| "delete_player": {"description": "Delete all data for a player", "min_role": "overseer"}, | |
| "delete_gamemode": {"description": "Delete a specific gamemode tier for a player", "min_role": "overseer"}, | |
| "clear_all_players": {"description": "Clear all player data from database (creates backup)", "min_role": "overseer"}, | |
| "merge": {"description": "Merge two player accounts, keeping higher tier for each gamemode", "min_role": "overseer"}, | |
| "result": {"description": "Manually upload player test result to website", "min_role": "tester"}, | |
| "upload": {"description": "Upload player tier to Supabase database", "min_role": "overseer"}, | |
| "loadresult": {"description": "Load the latest result of a player (ephemeral)", "min_role": "staff_team"}, # Anyone with staff role | |
| } | |
| async def get_minecraft_skin_url(username: str) -> Optional[str]: | |
| """Get Minecraft player bust with full options""" | |
| try: | |
| # Use Visage API bust view with shadow, cape, ears, helmet, overlay | |
| return f"https://visage.surgeplay.com/bust/{username}?angle=15&shadow=true&cape=true&ears=true&helmet=true&overlay=true" | |
| except Exception as e: | |
| print(f"Error getting skin for {username}: {e}") | |
| return None | |
| # ensure files at import | |
| _ensure_misc_files() | |
| # ===================== PERSISTENT VIEWS ===================== | |
| class QueuePanelView(discord.ui.View): | |
| def __init__(self): | |
| super().__init__(timeout=None) | |
| async def enter_waitlist(self, interaction: discord.Interaction, button: discord.ui.Button): | |
| cog = interaction.client.get_cog('QueueSystem') | |
| if cog: | |
| ban_ts = cog.temp_banned_users.get(str(interaction.user.id)) | |
| if ban_ts: | |
| remaining = float(ban_ts) - time.time() | |
| if remaining > 0: | |
| days = int(remaining // 86400) | |
| hours = int((remaining % 86400) // 3600) | |
| return await interaction.response.send_message( | |
| f"❌ You are temporarily banned from joining waitlists for {days}d {hours}h", | |
| ephemeral=True | |
| ) | |
| else: | |
| cog.temp_banned_users.pop(str(interaction.user.id), None) | |
| save_json(BANS_PATH, cog.temp_banned_users) | |
| await interaction.response.send_modal(WaitlistModal()) | |
| class QueueEnterView(discord.ui.View): | |
| def __init__(self, bot, region): | |
| super().__init__(timeout=None) | |
| self.bot = bot | |
| self.region = region | |
| async def enter_queue(self, interaction: discord.Interaction, button: discord.ui.Button): | |
| cog = self.bot.get_cog('QueueSystem') | |
| if not cog: | |
| return await interaction.response.send_message("❌ Queue system not loaded!", ephemeral=True) | |
| await cog.handle_queue_join(interaction, self.region) | |
| class CloseQueueView(discord.ui.View): | |
| def __init__(self, cog, region): | |
| # timeout is reasonable for a confirmation prompt | |
| super().__init__(timeout=300) | |
| self.cog = cog | |
| self.region = region | |
| async def confirm_close(self, interaction: discord.Interaction, button: discord.ui.Button): | |
| # Only someone in tester_ids can confirm the close | |
| region_data = self.cog.queue_data.get(self.region) | |
| if not region_data or interaction.user.id not in region_data.get('tester_ids', []): | |
| return await interaction.response.send_message( | |
| "❌ Only a tester for this queue can confirm closing.", | |
| ephemeral=True | |
| ) | |
| if not region_data: | |
| return await interaction.response.send_message("❌ Invalid region data.", ephemeral=True) | |
| # Perform close actions | |
| region_data['is_open'] = False | |
| region_data['tester_ids'] = [] | |
| region_data['last_closed'] = time.time() | |
| try: | |
| # Update the queue embed (will reflect closed state) and the waitlist channel status | |
| await self.cog.update_queue_embed(self.region) | |
| await self.cog.update_waitlist_status(self.region) | |
| # Log the action if possible | |
| try: | |
| await self.cog.log_action("Queue Closed", interaction.user, details=f"Region: {self.region}") | |
| except Exception: | |
| pass | |
| except Exception as e: | |
| print(f"Error while closing queue {self.region}: {e}") | |
| # disable buttons on this view message to prevent re-use | |
| for item in self.children: | |
| item.disabled = True | |
| try: | |
| await interaction.message.edit(view=self) | |
| except Exception: | |
| pass | |
| await interaction.response.send_message(f"✅ {self.region} queue has been closed.", ephemeral=True) | |
| async def cancel_close(self, interaction: discord.Interaction, button: discord.ui.Button): | |
| # disable the buttons and edit original message to reflect cancellation | |
| for item in self.children: | |
| item.disabled = True | |
| try: | |
| await interaction.message.edit(view=self) | |
| except Exception: | |
| pass | |
| await interaction.response.send_message("❌ Close request cancelled.", ephemeral=True) | |
| # High Tier Test Button View | |
| class HighTierButtonView(discord.ui.View): | |
| def __init__(self): | |
| super().__init__(timeout=None) | |
| async def open_high_ticket(self, interaction: discord.Interaction, button: discord.ui.Button): | |
| """Open a high tier test ticket""" | |
| user = interaction.user | |
| guild = interaction.guild | |
| # Get user's region from database | |
| database = get_all_verified_users() | |
| user_region = None | |
| for key, data in database.items(): | |
| if data.get("discord_id") == user.id: | |
| user_region = data.get("region") | |
| break | |
| if not user_region: | |
| return await interaction.response.send_message( | |
| "❌ You haven't verified your account yet! Please use the verification system first.", | |
| ephemeral=True | |
| ) | |
| # Check high ticket cooldown (14 days) | |
| cooldowns = load_json(COOLDOWNS_PATH, {}) | |
| high_ticket_cooldowns = cooldowns.get("high_ticket", {}) | |
| user_cooldown = high_ticket_cooldowns.get(str(user.id)) | |
| if user_cooldown: | |
| remaining = float(user_cooldown.get("expires_at", 0)) - time.time() | |
| if remaining > 0: | |
| days = int(remaining // 86400) | |
| hours = int((remaining % 86400) // 3600) | |
| return await interaction.response.send_message( | |
| f"❌ You have a cooldown on high tier tests for {days}d {hours}h", | |
| ephemeral=True | |
| ) | |
| try: | |
| await interaction.response.defer() | |
| # Create or get "high test" category | |
| category = discord.utils.get(guild.categories, name="high test") | |
| if not category: | |
| category = await guild.create_category("high test") | |
| # Create ticket channel: region-dc_username | |
| ticket_name = f"{user_region.lower()}-{user.name.lower()}" | |
| # Check if ticket already exists | |
| existing = discord.utils.get(guild.text_channels, name=ticket_name) | |
| if existing: | |
| return await interaction.followup.send( | |
| "❌ You already have an open high tier test ticket!", | |
| ephemeral=True | |
| ) | |
| # Create the ticket channel | |
| ticket_channel = await category.create_text_channel( | |
| ticket_name, | |
| topic=f"High Tier Test for {user.mention} | Region: {user_region}" | |
| ) | |
| # Set permissions | |
| await ticket_channel.set_permissions(guild.default_role, view_channel=False) | |
| await ticket_channel.set_permissions(user, view_channel=True, send_messages=True, read_message_history=True) | |
| # Notify user | |
| await interaction.followup.send( | |
| f"✅ High tier test ticket opened: {ticket_channel.mention}", | |
| ephemeral=True | |
| ) | |
| # Send welcome message in ticket matching the image format | |
| # Get user info from database | |
| database = get_all_verified_users() | |
| username = "Unknown" | |
| for key, data in database.items(): | |
| if data.get("discord_id") == user.id: | |
| username = data.get("username", user.name) | |
| break | |
| embed = discord.Embed( | |
| color=discord.Color.red() | |
| ) | |
| embed.add_field(name="User", value=f"{user.mention}", inline=False) | |
| embed.add_field(name="Gamemode", value="Crystal", inline=True) | |
| embed.add_field(name="Account Type", value="Java Premium", inline=True) | |
| embed.add_field(name="Region", value=user_region, inline=True) | |
| embed.add_field(name="Username", value=username, inline=True) | |
| embed.add_field(name="Previous Rank (Crystal)", value="N/A", inline=False) | |
| await ticket_channel.send(f"{user.mention} Information", embed=embed) | |
| # Ping tester roles about the high ticket | |
| staff_role_ids = [getattr(config, role_id, None) for role_id in [ | |
| 'SENIOR_TESTER_ROLE_ID', 'REGULATOR_ROLE_ID', 'VERIFIED_TESTER_ROLE_ID' | |
| ]] | |
| staff_mentions = [] | |
| for role_id in staff_role_ids: | |
| if role_id: | |
| role = guild.get_role(role_id) | |
| if role: | |
| staff_mentions.append(role.mention) | |
| if staff_mentions: | |
| await ticket_channel.send( | |
| f"📢 **High Tier Test Alert!** {' '.join(staff_mentions)}\n" | |
| f"New high tier test from {user.mention} - {user_region} region" | |
| ) | |
| except Exception as e: | |
| print(f"ERROR opening high tier ticket: {e}") | |
| import traceback | |
| traceback.print_exc() | |
| await interaction.followup.send( | |
| f"❌ Failed to create high tier test ticket: {str(e)}", | |
| ephemeral=True | |
| ) | |
| class UnretireButtonView(discord.ui.View): | |
| """Button view for opening unretire tickets""" | |
| def __init__(self): | |
| super().__init__(timeout=None) | |
| async def open_unretire(self, interaction: discord.Interaction, button: discord.ui.Button): | |
| """Open an unretire test ticket""" | |
| user = interaction.user | |
| guild = interaction.guild | |
| # Get user's region from database | |
| database = get_all_verified_users() | |
| user_region = None | |
| for key, data in database.items(): | |
| if data.get("discord_id") == user.id: | |
| user_region = data.get("region") | |
| break | |
| if not user_region: | |
| return await interaction.response.send_message( | |
| "❌ You haven't verified your account yet! Please use the verification system first.", | |
| ephemeral=True | |
| ) | |
| try: | |
| await interaction.response.defer() | |
| # Create or get "unretire" category | |
| category = discord.utils.get(guild.categories, name="unretire") | |
| if not category: | |
| category = await guild.create_category("unretire") | |
| # Create ticket channel: region-unretire | |
| ticket_name = f"{user_region.lower()}-unretire" | |
| # Check if ticket already exists for this user today | |
| existing = discord.utils.get(guild.text_channels, name=ticket_name) | |
| if existing: | |
| return await interaction.followup.send( | |
| "❌ You already have an open unretire ticket!", | |
| ephemeral=True | |
| ) | |
| # Create the ticket channel | |
| ticket_channel = await category.create_text_channel( | |
| ticket_name, | |
| topic=f"Unretire Test for {user.mention} | Region: {user_region} | Gamemode: vanilla" | |
| ) | |
| # Set permissions | |
| await ticket_channel.set_permissions(guild.default_role, view_channel=False) | |
| await ticket_channel.set_permissions(user, view_channel=True, send_messages=True, read_message_history=True) | |
| # Notify user | |
| await interaction.followup.send( | |
| f"✅ Unretire test ticket opened: {ticket_channel.mention}", | |
| ephemeral=True | |
| ) | |
| # Send welcome message in ticket | |
| embed = discord.Embed( | |
| title=f"🔄 Unretire Test Ticket - {user.name}", | |
| description=f"Region: {user_region}\n\nYour comeback journey begins here! A tester will be with you shortly to evaluate your skills and determine your new tier.", | |
| color=discord.Color.blue() | |
| ) | |
| embed.set_footer(text="Stay patient and ready!") | |
| await ticket_channel.send(embed=embed) | |
| # Ping tester roles about the unretire request | |
| staff_role_ids = [getattr(config, role_id, None) for role_id in [ | |
| 'SENIOR_TESTER_ROLE_ID', 'REGULATOR_ROLE_ID', 'VERIFIED_TESTER_ROLE_ID' | |
| ]] | |
| staff_mentions = [] | |
| for role_id in staff_role_ids: | |
| if role_id: | |
| role = guild.get_role(role_id) | |
| if role: | |
| staff_mentions.append(role.mention) | |
| if staff_mentions: | |
| await ticket_channel.send( | |
| f"📢 **Unretire Request Alert!** {' '.join(staff_mentions)}\n" | |
| f"New unretire test from {user.mention} - {user_region} region" | |
| ) | |
| except Exception as e: | |
| print(f"ERROR opening unretire ticket: {e}") | |
| import traceback | |
| traceback.print_exc() | |
| await interaction.followup.send( | |
| f"❌ Failed to create unretire test ticket: {str(e)}", | |
| ephemeral=True | |
| ) | |
| # Modal for Waitlist | |
| class WaitlistModal(discord.ui.Modal, title="Join Testing Waitlist"): | |
| def __init__(self): | |
| super().__init__(timeout=300) | |
| minecraft_username = discord.ui.TextInput( | |
| label="Minecraft Username", | |
| placeholder="Enter your Minecraft IGN...", | |
| required=True, | |
| max_length=16 | |
| ) | |
| region = discord.ui.TextInput( | |
| label="Region", | |
| placeholder="Enter: AS, NA, ME, EU, or AU", | |
| required=True, | |
| max_length=2 | |
| ) | |
| preferred_server = discord.ui.TextInput( | |
| label="Preferred Server", | |
| placeholder="Enter your preferred server name...", | |
| required=True, | |
| max_length=50 | |
| ) | |
| async def on_submit(self, interaction: discord.Interaction): | |
| cog = interaction.client.get_cog('QueueSystem') | |
| if not cog: | |
| return await interaction.response.send_message("❌ Queue system not loaded!", ephemeral=True) | |
| await cog.handle_waitlist_join(interaction, self) | |
| # Helper functions for gamemode-specific queue data | |
| def get_default_queue_data() -> dict: | |
| """Get the default queue data structure""" | |
| return { | |
| 'AS': {'is_open': False, 'queue': [], 'tester_ids': [], 'message_id': None, 'channel_id': None, 'last_closed': None}, | |
| 'NA': {'is_open': False, 'queue': [], 'tester_ids': [], 'message_id': None, 'channel_id': None, 'last_closed': None}, | |
| 'ME': {'is_open': False, 'queue': [], 'tester_ids': [], 'message_id': None, 'channel_id': None, 'last_closed': None}, | |
| 'EU': {'is_open': False, 'queue': [], 'tester_ids': [], 'message_id': None, 'channel_id': None, 'last_closed': None}, | |
| 'AU': {'is_open': False, 'queue': [], 'tester_ids': [], 'message_id': None, 'channel_id': None, 'last_closed': None} | |
| } | |
| def load_queue_data(gamemode: Optional[str] = None) -> dict: | |
| """Load queue data from gamemode-specific file or return default""" | |
| if gamemode: | |
| path = get_gamemode_database_path(gamemode, "queue_data") | |
| return load_json(path, get_default_queue_data()) | |
| return get_default_queue_data() | |
| def save_queue_data(data: dict, gamemode: Optional[str] = None): | |
| """Save queue data to gamemode-specific file""" | |
| if gamemode: | |
| path = get_gamemode_database_path(gamemode, "queue_data") | |
| save_json(path, data) | |
| # Main Queue System Cog | |
| class QueueSystem(commands.Cog): | |
| def __init__(self, bot): | |
| self.bot = bot | |
| # Initialize queue_data as gamemode-aware structure | |
| # Structure: self.queue_data[gamemode][region] = {...} | |
| self.queue_data = {} | |
| # Initialize default queues for known gamemodes | |
| from gamemode_config import GAMEMODE_SERVERS | |
| for gamemode in GAMEMODE_SERVERS.keys(): | |
| self.queue_data[gamemode] = get_default_queue_data() | |
| # persistent temporary bans (stored to disk) -> keys as strings | |
| self.temp_banned_users: Dict[str, float] = load_json(BANS_PATH, {}) | |
| # persistent cooldowns for picked users (stored to disk) | |
| self.cooldowns: Dict[str, float] = load_json(COOLDOWNS_PATH, {}) | |
| # persistent open history (disk-backed) | |
| self.open_history = load_json(OPEN_HISTORY_PATH, {"AS": [], "NA": [], "ME": [], "EU": [], "AU": []}) | |
| def get_queue_data_for_gamemode(self, gamemode: str) -> dict: | |
| """Get queue data for a specific gamemode, initializing if needed""" | |
| if gamemode not in self.queue_data: | |
| self.queue_data[gamemode] = get_default_queue_data() | |
| return self.queue_data[gamemode] | |
| async def cog_load(self): | |
| self.bot.add_view(QueuePanelView()) | |
| for region in ['AS', 'NA', 'ME', 'EU', 'AU']: | |
| self.bot.add_view(QueueEnterView(self.bot, region)) | |
| print("Queue System: Persistent views loaded") | |
| async def log_action(self, action: str, user: discord.Member = None, details: str = None, ticket_channel: discord.TextChannel = None): | |
| """Log an action to the log channel""" | |
| if not getattr(config, "logs_channel_id", None): | |
| return | |
| channel = self.bot.get_channel(getattr(config, "logs_channel_id")) | |
| if not channel: | |
| return | |
| embed = discord.Embed( | |
| title=f"📝 {action}", | |
| color=0x5865f2, | |
| timestamp=discord.utils.utcnow() | |
| ) | |
| if user: | |
| embed.add_field(name="User", value=f"{user.mention} ({user.id})", inline=False) | |
| if details: | |
| embed.add_field(name="Details", value=details, inline=False) | |
| if ticket_channel: | |
| embed.add_field(name="Ticket", value=ticket_channel.mention, inline=False) | |
| embed.set_footer(text="CrystalTiers Logging System") | |
| try: | |
| await channel.send(embed=embed) | |
| except Exception as e: | |
| print(f"Failed to send log: {e}") | |
| async def log_command(self, interaction: discord.Interaction, command_name: str, parameters: dict = None, result: str = "executed"): | |
| """Log a command to the command-logs channel""" | |
| guild = interaction.guild | |
| if not guild: | |
| return | |
| # Get or create command-logs channel (admin private only) | |
| cmd_logs_channel = discord.utils.get(guild.text_channels, name=COMMAND_LOGS_CHANNEL_NAME) | |
| if not cmd_logs_channel: | |
| try: | |
| # Create channel with admin only permissions | |
| overwrites = { | |
| guild.default_role: discord.PermissionOverwrite(read_messages=False), | |
| guild.me: discord.PermissionOverwrite(read_messages=True, send_messages=True) | |
| } | |
| cmd_logs_channel = await guild.create_text_channel( | |
| COMMAND_LOGS_CHANNEL_NAME, | |
| overwrites=overwrites, | |
| topic="Audit log for all commands executed. For admin review only." | |
| ) | |
| except Exception as e: | |
| print(f"Failed to create command-logs channel: {e}") | |
| return | |
| # Check if user has access to the command-logs channel | |
| user_can_see_logs = cmd_logs_channel.permissions_for(interaction.user).read_messages | |
| # Build parameter details without pinging | |
| param_str = "" | |
| if parameters: | |
| for key, value in parameters.items(): | |
| if isinstance(value, discord.Member): | |
| # Don't ping members in the embed | |
| param_str += f"• **{key}**: @{value.name} ({value.id})\n" | |
| elif isinstance(value, discord.Role): | |
| param_str += f"• **{key}**: {value.mention}\n" | |
| elif isinstance(value, discord.TextChannel): | |
| param_str += f"• **{key}**: {value.mention}\n" | |
| else: | |
| param_str += f"• **{key}**: `{value}`\n" | |
| # Format user mention (pinged only if they can't see the logs) | |
| if user_can_see_logs: | |
| user_mention_display = f"**@{interaction.user.name}** ({interaction.user.id})" | |
| else: | |
| user_mention_display = f"{interaction.user.mention} ({interaction.user.id})" | |
| # Create audit log embed | |
| embed = discord.Embed( | |
| title=f"⚙️ Command: `/{command_name}`", | |
| color=0x3498db, | |
| timestamp=discord.utils.utcnow() | |
| ) | |
| embed.add_field( | |
| name="👤 Executor", | |
| value=user_mention_display, | |
| inline=False | |
| ) | |
| if interaction.guild: | |
| embed.add_field( | |
| name="🏢 Guild", | |
| value=interaction.guild.name, | |
| inline=False | |
| ) | |
| if param_str: | |
| embed.add_field( | |
| name="📋 Parameters", | |
| value=param_str, | |
| inline=False | |
| ) | |
| embed.add_field( | |
| name="✅ Result", | |
| value=result, | |
| inline=False | |
| ) | |
| embed.set_footer(text="CrystalTiers Command Audit Log") | |
| try: | |
| await cmd_logs_channel.send(embed=embed) | |
| except Exception as e: | |
| print(f"Failed to log command: {e}") | |
| async def update_waitlist_status(self, region, gamemode: Optional[str] = None): | |
| """Update the waitlist channel status when queue is closed""" | |
| if gamemode is None: | |
| # Fallback for calls without gamemode (will use default queue_data) | |
| queue_data_use = self.queue_data.get("vanilla", get_default_queue_data()) | |
| else: | |
| queue_data_use = self.get_queue_data_for_gamemode(gamemode) | |
| region_data = queue_data_use[region] | |
| channel = self.bot.get_channel(region_data['channel_id']) | |
| if not channel: | |
| return | |
| try: | |
| embed = discord.Embed( | |
| title=f"Welcome to #waitlist-{region.lower()}!", | |
| description=f"This is the start of the #waitlist-{region.lower()} private channel.", | |
| color=0x5865f2 | |
| ) | |
| status_embed = discord.Embed( | |
| title="PvP Ranked", | |
| description="No testers for your region are available at this time.\n\nYou will be pinged when a tester is available.", | |
| color=0xff0000 | |
| ) | |
| if region_data['last_closed']: | |
| last_closed_ts = int(region_data['last_closed']) | |
| status_embed.add_field( | |
| name="Last Testing Session:", | |
| value=f"<t:{last_closed_ts}:f>", | |
| inline=False | |
| ) | |
| try: | |
| await channel.purge(limit=10) | |
| except Exception: | |
| pass | |
| await channel.send(embed=embed) | |
| await channel.send(embed=status_embed) | |
| except Exception as e: | |
| print(f"Error updating {region} waitlist status: {e}") | |
| async def update_queue_embed(self, region, gamemode: Optional[str] = None): | |
| """Update the queue embed for a specific region""" | |
| if gamemode is None: | |
| # Fallback for calls without gamemode | |
| queue_data_use = self.queue_data.get("vanilla", get_default_queue_data()) | |
| else: | |
| queue_data_use = self.get_queue_data_for_gamemode(gamemode) | |
| region_data = queue_data_use[region] | |
| if not all([region_data['message_id'], region_data['channel_id']]): | |
| return | |
| try: | |
| channel = self.bot.get_channel(region_data['channel_id']) | |
| if not channel: | |
| return | |
| message = await channel.fetch_message(region_data['message_id']) | |
| embed = discord.Embed( | |
| title=f"Queue for ({region}) is {'open' if region_data['is_open'] else 'closed'}!", | |
| color=0x00ff00 if region_data['is_open'] else 0xff0000, | |
| description="The queue updates in real-time\n\nUse `/leave` if you wish to be removed from the queue" | |
| ) | |
| queue_list = region_data['queue'] | |
| if queue_list: | |
| queue_text = "" | |
| for i, user_id in enumerate(queue_list[:10]): | |
| user = self.bot.get_user(user_id) | |
| username = getattr(user, "display_name", None) or getattr(user, "name", None) or f"User({user_id})" | |
| queue_text += f"#{i+1} {username}\n" | |
| embed.add_field(name="Current Queue:", value=queue_text, inline=False) | |
| else: | |
| embed.add_field(name="Current Queue:", value="No one in queue", inline=False) | |
| if region_data['tester_ids']: | |
| testers_mention = ", ".join([f"<@{tid}>" for tid in region_data['tester_ids']]) | |
| embed.add_field(name="Current Testers:", value=testers_mention, inline=False) | |
| view = QueueEnterView(self.bot, region) if region_data['is_open'] else None | |
| await message.edit(embed=embed, view=view) | |
| except Exception as e: | |
| print(f"Error updating {region} queue embed: {e}") | |
| async def handle_queue_join(self, interaction: discord.Interaction, region): | |
| """Handle a user joining the queue""" | |
| # Get gamemode from guild | |
| gamemode = get_gamemode_for_guild(interaction.guild_id) | |
| if not gamemode: | |
| return await interaction.response.send_message("❌ This command can only be used in a gamemode server!", ephemeral=True) | |
| user = interaction.user | |
| guild = interaction.guild | |
| queue_data_gamemode = self.get_queue_data_for_gamemode(gamemode) | |
| region_data = queue_data_gamemode[region] | |
| print(f"\n[QUEUE-JOIN] [USER] User {user.name} ({user.id}) attempting to join {region} queue in {gamemode}") | |
| print(f"[QUEUE-JOIN] User roles: {[role.name for role in user.roles]}") | |
| # Check if user is retired | |
| if any(role.name.startswith("Retired ") for role in user.roles): | |
| print(f"[QUEUE-JOIN] [BLOCKED] USER IS RETIRED - blocking") | |
| return await interaction.response.send_message( | |
| "❌ You are retired and cannot join the queue. Use `/unretire_pannel` to make a comeback!", | |
| ephemeral=True | |
| ) | |
| # Check tier restriction - deny those with LT3 or higher (only HT4 and HT5, LT4 and LT5 allowed) | |
| restricted_tiers = ["HT1", "LT1", "HT2", "LT2", "HT3", "LT3"] | |
| has_restricted_tier = any(role.name in restricted_tiers for role in user.roles) | |
| print(f"[QUEUE-JOIN] Checking tier restriction: {has_restricted_tier}") | |
| if has_restricted_tier: | |
| user_tier = next((role.name for role in user.roles if role.name in restricted_tiers), None) | |
| print(f"[QUEUE-JOIN] [BLOCKED] USER HAS RESTRICTED TIER: {user_tier} - blocking") | |
| return await interaction.response.send_message( | |
| f"❌ You cannot join the queue with tier **{user_tier}**. Only **HT4, HT5, LT4, LT5** tiers can join the queue.", | |
| ephemeral=True | |
| ) | |
| print(f"[QUEUE-JOIN] [OK] Tier check passed") | |
| # check persistent ban | |
| print(f"[QUEUE-JOIN] Checking for bans...") | |
| ban_val = self.temp_banned_users.get(str(user.id)) | |
| if ban_val: | |
| remaining = float(ban_val) - time.time() | |
| if remaining > 0: | |
| days = int(remaining // 86400) | |
| hours = int((remaining % 86400) // 3600) | |
| print(f"[QUEUE-JOIN] ❌ USER IS BANNED for {days}d {hours}h - blocking") | |
| return await interaction.response.send_message( | |
| f"❌ You are temporarily banned from joining queues for {days}d {hours}h", | |
| ephemeral=True | |
| ) | |
| else: | |
| print(f"[QUEUE-JOIN] Ban expired, removing...") | |
| self.temp_banned_users.pop(str(user.id), None) | |
| save_json(BANS_PATH, self.temp_banned_users) | |
| print(f"[QUEUE-JOIN] [OK] Ban check passed") | |
| # check persistent queue cooldown | |
| print(f"\n[QUEUE-JOIN] [COOLDOWN CHECK] Checking queue cooldown for user {user.id} ({user.name})") | |
| cooldowns_path = get_gamemode_database_path(gamemode, "cooldowns") | |
| cooldowns_data = load_json(cooldowns_path, {"queue": {}, "high_ticket": {}}) | |
| print(f"[QUEUE-JOIN] [DATA] Loaded cooldowns data: {cooldowns_data}") | |
| print(f"[QUEUE-JOIN] [DATA] Data type: {type(cooldowns_data)}") | |
| queue_cooldowns = cooldowns_data.get("queue", {}) | |
| print(f"[QUEUE-JOIN] [QUEUE] Queue cooldowns dict: {queue_cooldowns}") | |
| print(f"[QUEUE-JOIN] [QUEUE] Queue cooldowns type: {type(queue_cooldowns)}") | |
| user_id_str = str(user.id) | |
| print(f"[QUEUE-JOIN] [USER_ID] User ID string: '{user_id_str}'") | |
| print(f"[QUEUE-JOIN] [KEYS] Available keys in queue_cooldowns: {list(queue_cooldowns.keys())}") | |
| cd_val = queue_cooldowns.get(user_id_str) | |
| print(f"[QUEUE-JOIN] [LOOKUP] Cooldown value for {user.id}: {cd_val}") | |
| print(f"[QUEUE-JOIN] [TYPE] Cooldown value type: {type(cd_val)}") | |
| if cd_val: | |
| expires_at = cd_val.get("expires_at", 0) | |
| print(f"[QUEUE-JOIN] [EXPIRY] expires_at value: {expires_at}") | |
| print(f"[QUEUE-JOIN] [EXPIRY] expires_at type: {type(expires_at)}") | |
| current_time = time.time() | |
| print(f"[QUEUE-JOIN] [TIME] Current time: {current_time}") | |
| remaining_cd = float(expires_at) - current_time | |
| print(f"[QUEUE-JOIN] [REMAINING] Remaining cooldown time: {remaining_cd} seconds") | |
| print(f"[QUEUE-JOIN] [REMAINING] Remaining type: {type(remaining_cd)}") | |
| print(f"[QUEUE-JOIN] [CHECK] Is remaining > 0? {remaining_cd > 0}") | |
| if remaining_cd > 0: | |
| days = int(remaining_cd // 86400) | |
| hours = int((remaining_cd % 86400) // 3600) | |
| minutes = int((remaining_cd % 3600) // 60) | |
| print(f"[QUEUE-JOIN] [BLOCKED] User has {days}d {hours}h {minutes}m cooldown remaining") | |
| return await interaction.response.send_message( | |
| f"[ERROR] You have a 7-day queue cooldown. Time left: {days}d {hours}h {minutes}m", | |
| ephemeral=True | |
| ) | |
| else: | |
| print(f"[QUEUE-JOIN] [OK] Cooldown expired, removing from file...") | |
| queue_cooldowns.pop(user_id_str, None) | |
| cooldowns_data["queue"] = queue_cooldowns | |
| save_json(cooldowns_path, cooldowns_data) | |
| print(f"[QUEUE-JOIN] [OK] Cooldown removed and file saved") | |
| else: | |
| print(f"[QUEUE-JOIN] [OK] No cooldown found for this user\n") | |
| waitlist_role = discord.utils.get(guild.roles, name=f"{region}_WAITLIST") | |
| if not waitlist_role or waitlist_role not in user.roles: | |
| return await interaction.response.send_message( | |
| f"❌ You must be in the {region} waitlist to join the queue!", | |
| ephemeral=True | |
| ) | |
| if not region_data['is_open']: | |
| return await interaction.response.send_message( | |
| f"❌ The {region} queue is currently closed!", | |
| ephemeral=True | |
| ) | |
| if user.id in region_data['queue']: | |
| position = region_data['queue'].index(user.id) + 1 | |
| return await interaction.response.send_message( | |
| f"⚠️ You're already in the queue at position #{position}!", | |
| ephemeral=True | |
| ) | |
| region_data['queue'].append(user.id) | |
| await self.update_queue_embed(region, gamemode) | |
| await interaction.response.send_message( | |
| f"✅ Added to {region} queue at position #{len(region_data['queue'])}!", | |
| ephemeral=True | |
| ) | |
| async def handle_waitlist_join(self, interaction: discord.Interaction, modal: WaitlistModal): | |
| """Handle a user joining the waitlist""" | |
| region_upper = modal.region.value.upper().strip() | |
| if region_upper not in ['AS', 'NA', 'ME', 'EU', 'AU']: | |
| return await interaction.response.send_message( | |
| "❌ Invalid region! Please enter AS, NA, ME, EU, or AU.", | |
| ephemeral=True | |
| ) | |
| # check persistent ban | |
| ban_val = self.temp_banned_users.get(str(interaction.user.id)) | |
| if ban_val: | |
| remaining = float(ban_val) - time.time() | |
| if remaining > 0: | |
| days = int(remaining // 86400) | |
| hours = int((remaining % 86400) // 3600) | |
| return await interaction.response.send_message( | |
| f"❌ You are temporarily banned from joining waitlists for {days}d {hours}h", | |
| ephemeral=True | |
| ) | |
| else: | |
| self.temp_banned_users.pop(str(interaction.user.id), None) | |
| save_json(BANS_PATH, self.temp_banned_users) | |
| # Note: Removed cooldown check from waitlist join to allow users to verify for different regions | |
| try: | |
| guild = interaction.guild | |
| user = interaction.user | |
| role_name = f"{region_upper}_WAITLIST" | |
| channel_name = f"waitlist-{region_upper.lower()}" | |
| role = discord.utils.get(guild.roles, name=role_name) | |
| channel = discord.utils.get(guild.channels, name=channel_name) | |
| if not role or not channel: | |
| return await interaction.response.send_message( | |
| f"❌ Queue system not properly set up for {region_upper}!", | |
| ephemeral=True | |
| ) | |
| if role in user.roles: | |
| return await interaction.response.send_message( | |
| f"⚠️ You're already in the {region_upper} waitlist!", | |
| ephemeral=True | |
| ) | |
| await user.add_roles(role) | |
| # Add to verified database | |
| add_verified_user(user.id, modal.minecraft_username.value, region_upper) | |
| # Log action | |
| await self.log_action( | |
| "Account Verified", | |
| user, | |
| f"Region: {region_upper}, IGN: {modal.minecraft_username.value}, Server: {modal.preferred_server.value}" | |
| ) | |
| await interaction.response.send_message( | |
| f"✅ Successfully added to {region_upper} waitlist!\n" | |
| f"📝 Minecraft IGN: `{modal.minecraft_username.value}`\n" | |
| f"🌍 Region: `{region_upper}`\n" | |
| f"🖥️ Preferred Server: `{modal.preferred_server.value}`\n\n" | |
| f"You can now access <#{channel.id}>", | |
| ephemeral=True | |
| ) | |
| except Exception as e: | |
| print(f"Error in waitlist join: {e}") | |
| if not interaction.response.is_done(): | |
| await interaction.response.send_message( | |
| "❌ An error occurred while processing your request. Please try again.", | |
| ephemeral=True | |
| ) | |
| async def open_queue(self, interaction: discord.Interaction, region: str): | |
| print(f"[OPENQUEUE] Command started by {interaction.user.name} ({interaction.user.id}) - Region: {region}") | |
| # Get gamemode from guild | |
| gamemode = get_gamemode_for_guild(interaction.guild_id) | |
| if not gamemode: | |
| print(f"[OPENQUEUE] ⚠️ Not a valid gamemode server") | |
| return await interaction.response.send_message("❌ This command can only be used in a gamemode server!", ephemeral=True) | |
| print(f"[OPENQUEUE] 🎮 Gamemode: {gamemode}") | |
| try: | |
| if not _is_member_at_least(interaction.user, "tester"): | |
| print(f"[OPENQUEUE] ⚠️ Permission denied - {interaction.user.name} is not a tester") | |
| return await interaction.response.send_message("❌ You don't have permission to use this command.", ephemeral=True) | |
| if not interaction.user.guild_permissions.manage_channels: | |
| print(f"[OPENQUEUE] ⚠️ Missing manage_channels permission - {interaction.user.name}") | |
| return await interaction.response.send_message( | |
| "❌ You need Manage Channels permission!", | |
| ephemeral=True | |
| ) | |
| # Convert to uppercase and validate | |
| region_upper = region.upper().strip() | |
| if region_upper not in ['AS', 'NA', 'ME', 'EU', 'AU']: | |
| print(f"[OPENQUEUE] ⚠️ Invalid region: {region_upper}") | |
| return await interaction.response.send_message( | |
| "❌ Invalid region! Use AS, NA, ME, EU, or AU.", | |
| ephemeral=True | |
| ) | |
| # Get gamemode-specific queue data | |
| queue_data_gamemode = self.get_queue_data_for_gamemode(gamemode) | |
| if region_upper not in queue_data_gamemode: | |
| print(f"[OPENQUEUE] ⚠️ Region not in queue_data: {region_upper}") | |
| return await interaction.response.send_message( | |
| "❌ Invalid region! Use AS, NA, ME, EU, or AU.", | |
| ephemeral=True | |
| ) | |
| channel_name = f"waitlist-{region_upper.lower()}" | |
| channel = discord.utils.get(interaction.guild.channels, name=channel_name) | |
| if not channel: | |
| print(f"[OPENQUEUE] ⚠️ Channel not found: {channel_name}") | |
| return await interaction.response.send_message( | |
| f"❌ Channel {channel_name} not found!", | |
| ephemeral=True | |
| ) | |
| # If queue already open, append to testers; otherwise start fresh | |
| is_new_queue = False | |
| if queue_data_gamemode[region_upper]['is_open']: | |
| if interaction.user.id not in queue_data_gamemode[region_upper]['tester_ids']: | |
| queue_data_gamemode[region_upper]['tester_ids'].append(interaction.user.id) | |
| print(f"[OPENQUEUE] ✅ Added tester to existing {region_upper} queue") | |
| else: | |
| queue_data_gamemode[region_upper].update({ | |
| 'is_open': True, | |
| 'tester_ids': [interaction.user.id], | |
| 'queue': [], | |
| 'channel_id': channel.id | |
| }) | |
| is_new_queue = True | |
| print(f"[OPENQUEUE] ✅ Created new queue for {region_upper}") | |
| # Record opening time + opener in persistent history | |
| self.open_history.setdefault(region_upper, []).append({'ts': time.time(), 'user_id': interaction.user.id}) | |
| save_json(OPEN_HISTORY_PATH, self.open_history) | |
| # Log command | |
| # Log command | |
| await self.log_command( | |
| interaction, | |
| "openqueue", | |
| {"region": region_upper}, | |
| f"Opened {region_upper} queue" | |
| ) | |
| # Create or update the queue embed | |
| embed = discord.Embed( | |
| title=f"Queue for ({region_upper}) is open!", | |
| color=0xFFFF00, | |
| description="The queue updates in real-time\n\nUse `/leave` if you wish to be removed from the queue" | |
| ) | |
| embed.add_field(name="Current Queue:", value="No one in queue yet", inline=False) | |
| testers_mention = ", ".join([f"<@{tid}>" for tid in queue_data_gamemode[region_upper]['tester_ids']]) | |
| embed.add_field(name="Current Testers:", value=testers_mention, inline=False) | |
| # Add last closed time if available | |
| if queue_data_gamemode[region_upper]['last_closed']: | |
| last_closed_ts = int(queue_data_gamemode[region_upper]['last_closed']) | |
| embed.add_field(name="Last Opened:", value=f"<t:{last_closed_ts}:R>", inline=False) | |
| view = QueueEnterView(self.bot, region_upper) | |
| if is_new_queue: | |
| # Send new queue message for first time | |
| await channel.send("@here The queue is now open!") | |
| queue_message = await channel.send(embed=embed, view=view) | |
| queue_data_gamemode[region_upper]['message_id'] = queue_message.id | |
| # Optional: log the action to configured logs channel (if configured) | |
| try: | |
| await self.log_action("Queue Opened", interaction.user, details=f"Region: {region_upper}", ticket_channel=channel) | |
| except: | |
| pass | |
| print(f"[OPENQUEUE] ✅ Queue opened successfully for {region_upper}") | |
| await interaction.response.send_message( | |
| f"✅ {region_upper} queue opened in {channel.mention}!", | |
| ephemeral=True | |
| ) | |
| else: | |
| # Edit existing queue message with new testers, or resend if message not found | |
| try: | |
| queue_message = await channel.fetch_message(queue_data_gamemode[region_upper]['message_id']) | |
| await queue_message.edit(embed=embed, view=view) | |
| print(f"[OPENQUEUE] ✅ Updated queue message for {region_upper}") | |
| await interaction.response.send_message( | |
| f"✅ You've been added as a tester to the {region_upper} queue!", | |
| ephemeral=True | |
| ) | |
| except (discord.NotFound, discord.HTTPException): | |
| # Message was deleted or channel changed, resend it | |
| queue_message = await channel.send(embed=embed, view=view) | |
| self.queue_data[region_upper]['message_id'] = queue_message.id | |
| print(f"[OPENQUEUE] ✅ Resent queue message for {region_upper}") | |
| await interaction.response.send_message( | |
| f"✅ You've been added as a tester to the {region_upper} queue!", | |
| ephemeral=True | |
| ) | |
| except Exception as e: | |
| print(f"[OPENQUEUE] ❌ Error updating queue message: {str(e)}") | |
| await interaction.response.send_message( | |
| f"❌ Error updating queue message: {str(e)}", | |
| ephemeral=True | |
| ) | |
| except Exception as e: | |
| error_msg = f"[OPENQUEUE] ❌ Unexpected error: {str(e)}" | |
| print(error_msg) | |
| try: | |
| await interaction.response.send_message(f"❌ Error: {str(e)}", ephemeral=True) | |
| except: | |
| print(f"[OPENQUEUE] ❌ Failed to send error response") | |
| async def closequeue(self, interaction: discord.Interaction, region: str): | |
| print(f"[CLOSEQUEUE] Command started by {interaction.user.name} ({interaction.user.id}) - Region: {region}") | |
| # Get gamemode from guild | |
| gamemode = get_gamemode_for_guild(interaction.guild_id) | |
| if not gamemode: | |
| return await interaction.response.send_message("❌ This command can only be used in a gamemode server!", ephemeral=True) | |
| try: | |
| if not _is_member_at_least(interaction.user, "tester"): | |
| print(f"[CLOSEQUEUE] ⚠️ Permission denied - {interaction.user.name}") | |
| return await interaction.response.send_message("❌ You don't have permission to use this command.", ephemeral=True) | |
| region_upper = region.upper() | |
| queue_data_gamemode = self.get_queue_data_for_gamemode(gamemode) | |
| if region_upper not in queue_data_gamemode: | |
| print(f"[CLOSEQUEUE] ⚠️ Invalid region: {region_upper}") | |
| return await interaction.response.send_message("❌ Invalid region. Use AS, NA, ME, EU, or AU.", ephemeral=True) | |
| if not queue_data_gamemode[region_upper]['is_open']: | |
| print(f"[CLOSEQUEUE] ⚠️ Queue not open for {region_upper}") | |
| return await interaction.response.send_message(f"⚠️ {region_upper} queue is not open.", ephemeral=True) | |
| tester_ids = queue_data_gamemode[region_upper].get('tester_ids', []) | |
| # If user is a tester, remove them from the list | |
| if interaction.user.id in tester_ids: | |
| tester_ids.remove(interaction.user.id) | |
| print(f"[CLOSEQUEUE] ✅ Removed {interaction.user.name} from testers") | |
| await interaction.response.send_message( | |
| f"✅ You have been removed from the {region_upper} queue testers.", | |
| ephemeral=True | |
| ) | |
| # If no more testers, close the queue | |
| if not tester_ids: | |
| queue_count = len(queue_data_gamemode[region_upper]['queue']) | |
| queue_data_gamemode[region_upper].update({ | |
| 'is_open': False, | |
| 'tester_ids': [], | |
| 'queue': [], | |
| 'last_closed': time.time() | |
| }) | |
| # Delete all messages in the channel | |
| try: | |
| channel = self.bot.get_channel(queue_data_gamemode[region_upper]['channel_id']) | |
| if channel: | |
| await channel.purge() | |
| await channel.send(f"✅ Queue for {region_upper} has been closed.") | |
| print(f"[CLOSEQUEUE] ✅ Purged queue channel for {region_upper}") | |
| except Exception as e: | |
| print(f"[CLOSEQUEUE] ❌ Error purging channel for {region_upper}: {e}") | |
| # update embeds / waitlist | |
| try: | |
| await self.update_queue_embed(region_upper) | |
| await self.update_waitlist_status(region_upper) | |
| print(f"[CLOSEQUEUE] ✅ Updated queue/waitlist status") | |
| except Exception as e: | |
| print(f"[CLOSEQUEUE] ❌ Error updating after close: {e}") | |
| # log if possible | |
| try: | |
| await self.log_action("Queue Closed", interaction.user, details=f"Region: {region_upper}") | |
| except Exception: | |
| pass | |
| else: | |
| # Update the embed to show remaining testers | |
| try: | |
| await self.update_queue_embed(region_upper) | |
| print(f"[CLOSEQUEUE] ✅ Updated queue embed") | |
| except Exception as e: | |
| print(f"[CLOSEQUEUE] ❌ Error updating queue embed: {e}") | |
| return | |
| # If user is not a tester, they can only close if they have manage_channels | |
| if not interaction.user.guild_permissions.manage_channels: | |
| print(f"[CLOSEQUEUE] ⚠️ {interaction.user.name} lacks manage_channels permission") | |
| testers_mention = ", ".join([f"<@{tid}>" for tid in tester_ids]) | |
| return await interaction.response.send_message( | |
| f"❌ Only current testers ({testers_mention}) or a member with Manage Channels can close this queue.", | |
| ephemeral=True | |
| ) | |
| # perform close (if manage_channels user) | |
| queue_count = len(self.queue_data[region_upper]['queue']) | |
| self.queue_data[region_upper].update({ | |
| 'is_open': False, | |
| 'tester_ids': [], | |
| 'queue': [], | |
| 'last_closed': time.time() | |
| }) | |
| try: | |
| channel = self.bot.get_channel(self.queue_data[region_upper]['channel_id']) | |
| if channel: | |
| await channel.purge() | |
| print(f"[CLOSEQUEUE] ✅ Purged queue channel for {region_upper}") | |
| except Exception as e: | |
| print(f"[CLOSEQUEUE] ❌ Error purging channel: {e}") | |
| # update embeds / waitlist | |
| try: | |
| await self.update_queue_embed(region_upper) | |
| await self.update_waitlist_status(region_upper) | |
| print(f"[CLOSEQUEUE] ✅ Updated queue/waitlist status") | |
| except Exception as e: | |
| print(f"[CLOSEQUEUE] ❌ Error updating after close: {e}") | |
| # log if possible | |
| try: | |
| await self.log_action("Queue Closed", interaction.user, details=f"Region: {region_upper}") | |
| except Exception: | |
| pass | |
| print(f"[CLOSEQUEUE] ✅ Queue closed for {region_upper}, removed {queue_count} users") | |
| await interaction.response.send_message(f"✅ {region_upper} queue closed! Removed {queue_count} users.", ephemeral=True) | |
| except Exception as e: | |
| error_msg = f"[CLOSEQUEUE] ❌ Unexpected error: {str(e)}" | |
| print(error_msg) | |
| try: | |
| await interaction.response.send_message(f"❌ Error: {str(e)}", ephemeral=True) | |
| except: | |
| print(f"[CLOSEQUEUE] ❌ Failed to send error response") | |
| async def pick_from_queue(self, interaction: discord.Interaction, region: str): | |
| print(f"[PICKFROMQUEUE] Command started by {interaction.user.name} ({interaction.user.id}) - Region: {region}") | |
| # Get gamemode from guild | |
| gamemode = get_gamemode_for_guild(interaction.guild_id) | |
| if not gamemode: | |
| return await interaction.response.send_message("❌ This command can only be used in a gamemode server!", ephemeral=True) | |
| try: | |
| if not _is_member_at_least(interaction.user, "tester"): | |
| print(f"[PICKFROMQUEUE] ⚠️ Permission denied - {interaction.user.name}") | |
| return await interaction.response.send_message("❌ You don't have permission to use this command.", ephemeral=True) | |
| if not interaction.user.guild_permissions.manage_channels: | |
| print(f"[PICKFROMQUEUE] ⚠️ Missing manage_channels permission - {interaction.user.name}") | |
| return await interaction.response.send_message( | |
| "❌ You need Manage Channels permission!", | |
| ephemeral=True | |
| ) | |
| region_upper = region.upper() | |
| queue_data_gamemode = self.get_queue_data_for_gamemode(gamemode) | |
| if region_upper not in queue_data_gamemode: | |
| print(f"[PICKFROMQUEUE] ⚠️ Invalid region: {region_upper}") | |
| return await interaction.response.send_message( | |
| "❌ Invalid region! Use AS, NA, ME, EU, or AU.", | |
| ephemeral=True | |
| ) | |
| if not queue_data_gamemode[region_upper]['is_open']: | |
| print(f"[PICKFROMQUEUE] ⚠️ Queue not open for {region_upper}") | |
| return await interaction.response.send_message( | |
| f"❌ {region_upper} queue is not open!", | |
| ephemeral=True | |
| ) | |
| if not queue_data_gamemode[region_upper]['queue']: | |
| print(f"[PICKFROMQUEUE] ⚠️ No users in queue for {region_upper}") | |
| return await interaction.response.send_message( | |
| f"❌ No one in {region_upper} queue!", | |
| ephemeral=True | |
| ) | |
| next_user_id = queue_data_gamemode[region_upper]['queue'].pop(0) | |
| next_user = interaction.guild.get_member(next_user_id) | |
| if not next_user: | |
| print(f"[PICKFROMQUEUE] ⚠️ User {next_user_id} not found in guild") | |
| return await interaction.response.send_message( | |
| "❌ Couldn't find the next user!", | |
| ephemeral=True | |
| ) | |
| print(f"[PICKFROMQUEUE] ✅ Picked {next_user.name} from {region_upper} queue") | |
| ticket_category = discord.utils.get(interaction.guild.categories, name="Tier Testing") | |
| if not ticket_category: | |
| ticket_category = await interaction.guild.create_category("Tier Testing") | |
| print(f"[PICKFROMQUEUE] ✅ Created 'Tier Testing' category") | |
| ticket_channel = await interaction.guild.create_text_channel( | |
| name=f"{region_upper}-{next_user.display_name}".replace(" ", "-").lower(), | |
| category=ticket_category, | |
| overwrites={ | |
| interaction.guild.default_role: discord.PermissionOverwrite(view_channel=False), | |
| next_user: discord.PermissionOverwrite(view_channel=True, send_messages=True), | |
| interaction.guild.me: discord.PermissionOverwrite(view_channel=True, send_messages=True), | |
| interaction.user: discord.PermissionOverwrite(view_channel=True, send_messages=True) | |
| }, | |
| topic=f"Queue Test for {next_user.mention} | Region: {region_upper}" | |
| ) | |
| print(f"[PICKFROMQUEUE] ✅ Created ticket channel for {next_user.name}") | |
| await self.update_queue_embed(region_upper) | |
| await ticket_channel.send( | |
| f"🎫 Ticket for {next_user.mention} - Region: **{region_upper}**\n" | |
| f"Tester: {interaction.user.mention}\n\n" | |
| f"👤 **User:** {next_user.mention}\n" | |
| f"🧪 **Tester:** {interaction.user.mention}" | |
| ) | |
| try: | |
| await next_user.send( | |
| f"🎉 You've been selected from the {region_upper} queue!\n" | |
| f"Tester: {interaction.user.mention}\n" | |
| f"Ticket: {ticket_channel.mention}" | |
| ) | |
| print(f"[PICKFROMQUEUE] ✅ Sent DM to {next_user.name}") | |
| except Exception: | |
| print(f"[PICKFROMQUEUE] ⚠️ Could not send DM to {next_user.name}") | |
| # apply 7-day cooldown to the picked user (persisted) | |
| cooldown_until = time.time() + (7 * 86400) | |
| cooldowns_data = load_json(COOLDOWNS_PATH, {"queue": {}, "high_ticket": {}}) | |
| print(f"[PICKFROMQUEUE] 🎫 Setting 7-day queue cooldown for {next_user.name} (ID: {next_user.id})") | |
| user_id_str = str(next_user.id) | |
| cooldowns_data["queue"][user_id_str] = {"expires_at": cooldown_until} | |
| save_json(COOLDOWNS_PATH, cooldowns_data) | |
| print(f"[PICKFROMQUEUE] ✅ Cooldown saved to file") | |
| # inform in ticket about cooldown | |
| await ticket_channel.send(f"🔒 {next_user.mention} has a 7-day cooldown from rejoining after being picked.") | |
| # Log ticket creation | |
| await self.log_action( | |
| "Ticket Created", | |
| next_user, | |
| f"Created by {interaction.user.display_name}", | |
| ticket_channel | |
| ) | |
| # Log command | |
| try: | |
| await self.log_command( | |
| interaction, | |
| "pickfromqueue", | |
| {"region": region_upper, "picked_user": next_user, "tester": interaction.user}, | |
| f"Picked {next_user} from {region_upper} queue for tier testing" | |
| ) | |
| except Exception as log_err: | |
| print(f"[PICKFROMQUEUE] ⚠️ Could not log command: {log_err}") | |
| print(f"[PICKFROMQUEUE] ✅ Successfully picked {next_user.name} from {region_upper} queue") | |
| await interaction.response.send_message( | |
| f"✅ Picked {next_user.mention} from {region_upper} queue!\n" | |
| f"Ticket: {ticket_channel.mention}\n" | |
| f"Remaining: {len(self.queue_data[region_upper]['queue'])}", | |
| ephemeral=True | |
| ) | |
| except Exception as e: | |
| error_msg = f"[PICKFROMQUEUE] ❌ Unexpected error: {str(e)}" | |
| print(error_msg) | |
| try: | |
| await interaction.response.send_message(f"❌ Error: {str(e)}", ephemeral=True) | |
| except: | |
| print(f"[PICKFROMQUEUE] ❌ Failed to send error response") | |
| async def queue_system_setup(self, interaction: discord.Interaction, request_channel: discord.TextChannel): | |
| if not _is_member_at_least(interaction.user, "owner"): | |
| return await interaction.response.send_message("❌ You don't have permission to use this command.", ephemeral=True) | |
| if not interaction.user.guild_permissions.administrator: | |
| return await interaction.response.send_message( | |
| "❌ You need Administrator permissions!", | |
| ephemeral=True | |
| ) | |
| await interaction.response.defer(ephemeral=True) | |
| try: | |
| guild = interaction.guild | |
| # Get gamemode for this guild | |
| gamemode = get_gamemode_for_guild(guild.id) | |
| if not gamemode: | |
| return await interaction.followup.send("❌ This server is not configured as a valid gamemode server!", ephemeral=True) | |
| regions = { | |
| 'AS': {'role': 'AS_WAITLIST', 'channel': 'waitlist-as', 'color': 0xff5733}, | |
| 'NA': {'role': 'NA_WAITLIST', 'channel': 'waitlist-na', 'color': 0x3498db}, | |
| 'ME': {'role': 'ME_WAITLIST', 'channel': 'waitlist-me', 'color': 0x9b59b6}, | |
| 'EU': {'role': 'EU_WAITLIST', 'channel': 'waitlist-eu', 'color': 0x2ecc71}, | |
| 'AU': {'role': 'AU_WAITLIST', 'channel': 'waitlist-au', 'color': 0xe74c3c} | |
| } | |
| # Handle request-a-test channel | |
| request_setup_info = "" | |
| try: | |
| # Clear all messages in request channel | |
| await request_channel.purge(limit=None) | |
| # Rename channel to "📩┃request-a-test" | |
| await request_channel.edit(name="📩┃request-a-test") | |
| request_setup_info = f"✅ Request channel prepared: {request_channel.mention}\n" | |
| except Exception as e: | |
| request_setup_info = f"⚠️ Could not prepare request channel: {str(e)}\n" | |
| # Create/find Waitlist category for regional waitlist channels | |
| waitlist_category = discord.utils.find(lambda c: c.name.lower().startswith("waitlist"), guild.categories) | |
| if not waitlist_category: | |
| waitlist_category = await guild.create_category("Waitlist", reason="Queue system setup") | |
| created_roles = [] | |
| created_channels = [] | |
| # Tier roles with vibrant hierarchical colors (LT5=bright red at top, fading down) | |
| tier_roles_config = { | |
| 'LT5': 0xFF0000, # Bright Red | |
| 'LT4': 0xFF3333, # Dark Red | |
| 'HT5': 0xFF6666, # Medium Red | |
| 'LT3': 0xFF9999, # Light Red | |
| 'HT4': 0xFFCC00, # Bright Yellow | |
| 'LT2': 0xFFDD00, # Gold | |
| 'HT3': 0x00FF00, # Bright Green | |
| 'HT2': 0x00CCFF, # Bright Cyan | |
| 'LT1': 0x0099FF, # Bright Blue | |
| 'HT1': 0x0050FF, # Deep Blue | |
| } | |
| # Get bot role for positioning | |
| bot_role = guild.me.top_role | |
| # Check and create tier roles with positioning | |
| for tier_name, color_val in tier_roles_config.items(): | |
| tier_role = discord.utils.get(guild.roles, name=tier_name) | |
| if not tier_role: | |
| tier_role = await guild.create_role( | |
| name=tier_name, | |
| color=discord.Color(color_val), | |
| mentionable=True, | |
| reason="Queue system setup - Tier roles" | |
| ) | |
| created_roles.append(tier_role.name) | |
| # Position below bot role (ensure position is at least 1) | |
| try: | |
| position = max(1, bot_role.position - 1) | |
| await tier_role.edit(position=position) | |
| except discord.Forbidden: | |
| pass | |
| # Create Retired roles for each tier | |
| for tier_name, color_val in tier_roles_config.items(): | |
| retired_role_name = f"Retired {tier_name}" | |
| retired_role = discord.utils.get(guild.roles, name=retired_role_name) | |
| if not retired_role: | |
| retired_role = await guild.create_role( | |
| name=retired_role_name, | |
| color=discord.Color(color_val), | |
| mentionable=True, | |
| reason="Queue system setup - Retired tier roles" | |
| ) | |
| created_roles.append(retired_role.name) | |
| # Position below corresponding tier role (ensure position is at least 1) | |
| try: | |
| tier_role_obj = discord.utils.get(guild.roles, name=tier_name) | |
| if tier_role_obj: | |
| position = max(1, tier_role_obj.position - 1) | |
| await retired_role.edit(position=position) | |
| except discord.Forbidden: | |
| pass | |
| # Track which channels were already created vs newly created | |
| channels_status = { | |
| 'AS': False, 'NA': False, 'ME': False, 'EU': False, 'AU': False | |
| } | |
| for region, config_data in regions.items(): | |
| role = discord.utils.get(guild.roles, name=config_data['role']) | |
| if not role: | |
| role = await guild.create_role( | |
| name=config_data['role'], | |
| color=discord.Color(config_data['color']), | |
| mentionable=True, | |
| reason="Queue system setup" | |
| ) | |
| created_roles.append(role.name) | |
| overwrites = { | |
| guild.default_role: discord.PermissionOverwrite(read_messages=False), | |
| role: discord.PermissionOverwrite(read_messages=True, send_messages=True), | |
| guild.me: discord.PermissionOverwrite(read_messages=True, send_messages=True) | |
| } | |
| channel = discord.utils.get(guild.text_channels, name=config_data['channel']) | |
| if not channel: | |
| channel = await guild.create_text_channel( | |
| config_data['channel'], | |
| category=waitlist_category, | |
| overwrites=overwrites, | |
| topic=f"Waitlist for {region} region testers", | |
| reason="Queue system setup" | |
| ) | |
| created_channels.append(channel.mention) | |
| channels_status[region] = True | |
| self.queue_data[gamemode][region]['channel_id'] = channel.id | |
| await self.update_waitlist_status(region, gamemode) | |
| # Send panel to the provided request-a-test channel | |
| welcome_embed = discord.Embed( | |
| title="Welcome to #📩┃request-a-test!", | |
| description="This is the start of the #📩┃request-a-test channel.", | |
| color=0x2f3136 | |
| ) | |
| await request_channel.send(embed=welcome_embed) | |
| eval_embed = discord.Embed( | |
| title="📝 Evaluation Testing Waitlist", | |
| description=( | |
| "Upon applying, you will be added to a waitlist channel.\n" | |
| "Here you will be pinged when a tester of your region is available.\n\n" | |
| "• **Region:** Your server region\n" | |
| "• **Username:** Your Minecraft IGN\n" | |
| "• **Preferred Server:** The server you'd like to test on\n\n" | |
| "🔴 Failure to provide authentic information will result in a denied test." | |
| ), | |
| color=0x5865f2 | |
| ) | |
| view = QueuePanelView() | |
| await request_channel.send(embed=eval_embed, view=view) | |
| # Check if all channels already existed | |
| all_channels_existed = not any(channels_status.values()) | |
| channels_text = "\n".join(created_channels) if created_channels else "None (already existed)" | |
| if all_channels_existed: | |
| channels_text = "All channels already created!" | |
| success_embed = discord.Embed( | |
| title="✅ Queue System Setup Complete!", | |
| color=0x00ff00, | |
| timestamp=discord.utils.utcnow() | |
| ) | |
| success_embed.add_field( | |
| name="📋 Roles Created", | |
| value="\n".join([f"• {role}" for role in created_roles]) or "None (already existed)", | |
| inline=False | |
| ) | |
| success_embed.add_field( | |
| name="📺 Channels Status", | |
| value=channels_text, | |
| inline=False | |
| ) | |
| success_embed.add_field( | |
| name="🎛️ Panel Location", | |
| value=f"Queue panel created in {request_channel.mention}", | |
| inline=False | |
| ) | |
| if request_setup_info: | |
| success_embed.add_field( | |
| name="📩 Request-a-Test Channel", | |
| value=request_setup_info, | |
| inline=False | |
| ) | |
| success_embed.set_footer(text="Queue system is now ready!") | |
| await interaction.followup.send(embed=success_embed, ephemeral=True) | |
| except Exception as e: | |
| print(f"Error in queue setup: {e}") | |
| error_embed = discord.Embed( | |
| title="❌ Setup Failed", | |
| description=f"An error occurred: {str(e)}", | |
| color=0xff0000 | |
| ) | |
| await interaction.followup.send(embed=error_embed, ephemeral=True) | |
| async def leave_waitlist(self, interaction: discord.Interaction): | |
| if not is_allowed_member(interaction.user): | |
| return await interaction.response.send_message("❌ You don't have permission to use this command.", ephemeral=True) | |
| # Get gamemode from guild | |
| gamemode = get_gamemode_for_guild(interaction.guild_id) | |
| if not gamemode: | |
| return await interaction.response.send_message("❌ This command can only be used in a gamemode server!", ephemeral=True) | |
| user = interaction.user | |
| queue_data_gamemode = self.get_queue_data_for_gamemode(gamemode) | |
| removed_from = [] | |
| for region in ['AS', 'NA', 'ME', 'EU', 'AU']: | |
| if user.id in queue_data_gamemode[region]['queue']: | |
| queue_data_gamemode[region]['queue'].remove(user.id) | |
| removed_from.append(region) | |
| await self.update_queue_embed(region) | |
| if removed_from: | |
| await interaction.response.send_message( | |
| f"✅ Removed from {', '.join(removed_from)} queue!", | |
| ephemeral=True | |
| ) | |
| else: | |
| await interaction.response.send_message( | |
| "❌ You're not in any active queue!", | |
| ephemeral=True | |
| ) | |
| async def clear_waitlist(self, interaction: discord.Interaction, | |
| user: discord.Member, | |
| region: str, | |
| days: int = 0): | |
| if not _is_member_at_least(interaction.user, "tester"): | |
| return await interaction.response.send_message("❌ You don't have permission to use this command.", ephemeral=True) | |
| # Only senior tester+ can apply temp bans | |
| if days > 0 and not _is_member_at_least(interaction.user, "senior_tester"): | |
| return await interaction.response.send_message("❌ Only Senior Tester+ can apply temp bans.", ephemeral=True) | |
| if not interaction.user.guild_permissions.manage_roles: | |
| return await interaction.response.send_message( | |
| "❌ You need Manage Roles permission!", | |
| ephemeral=True | |
| ) | |
| region_upper = region.upper() | |
| if region_upper not in ['AS', 'NA', 'ME', 'EU', 'AU']: | |
| return await interaction.response.send_message( | |
| "❌ Invalid region! Use AS, NA, ME, EU, or AU.", | |
| ephemeral=True | |
| ) | |
| if days < 0 or days > 30: | |
| return await interaction.response.send_message( | |
| "❌ Days must be between 0 and 30!", | |
| ephemeral=True | |
| ) | |
| if user.id in self.queue_data[region_upper]['queue']: | |
| self.queue_data[region_upper]['queue'].remove(user.id) | |
| await self.update_queue_embed(region_upper) | |
| if days > 0: | |
| ban_until = time.time() + (days * 86400) | |
| self.temp_banned_users[str(user.id)] = ban_until | |
| save_json(BANS_PATH, self.temp_banned_users) | |
| await interaction.response.send_message( | |
| f"✅ Removed {user.mention} from {region_upper} queue and temporarily banned from joining for {days} days!", | |
| ephemeral=True | |
| ) | |
| else: | |
| await interaction.response.send_message( | |
| f"✅ Removed {user.mention} from {region_upper} queue!", | |
| ephemeral=True | |
| ) | |
| async def clear_cooldown(self, interaction: discord.Interaction, user: discord.Member): | |
| if not _is_member_at_least(interaction.user, "regulator"): | |
| return await interaction.response.send_message("❌ You don't have permission to use this command.", ephemeral=True) | |
| if not interaction.user.guild_permissions.manage_roles: | |
| return await interaction.response.send_message("❌ You need Manage Roles permission!", ephemeral=True) | |
| # Get gamemode from guild | |
| gamemode = get_gamemode_for_guild(interaction.guild_id) | |
| if not gamemode: | |
| return await interaction.response.send_message("❌ This command can only be used in a gamemode server!", ephemeral=True) | |
| # Check and clear from both queue and high_ticket cooldowns for this gamemode | |
| cooldowns_path = get_gamemode_database_path(gamemode, "cooldowns") | |
| cooldowns_data = load_json(cooldowns_path, {"queue": {}, "high_ticket": {}}) | |
| queue_cooldowns = cooldowns_data.get("queue", {}) | |
| high_ticket_cooldowns = cooldowns_data.get("high_ticket", {}) | |
| user_str = str(user.id) | |
| found = False | |
| if user_str in queue_cooldowns: | |
| queue_cooldowns.pop(user_str, None) | |
| found = True | |
| if user_str in high_ticket_cooldowns: | |
| high_ticket_cooldowns.pop(user_str, None) | |
| found = True | |
| if found: | |
| cooldowns_data["queue"] = queue_cooldowns | |
| cooldowns_data["high_ticket"] = high_ticket_cooldowns | |
| save_json(cooldowns_path, cooldowns_data) | |
| await interaction.response.send_message(f"✅ Cleared cooldown for {user.mention}.", ephemeral=True) | |
| else: | |
| await interaction.response.send_message(f"⚠️ {user.mention} has no active cooldown.", ephemeral=True) | |
| async def queue_status(self, interaction: discord.Interaction): | |
| if not _is_member_at_least(interaction.user, "senior_tester"): | |
| return await interaction.response.send_message("❌ You don't have permission to use this command.", ephemeral=True) | |
| # Get gamemode from guild | |
| gamemode = get_gamemode_for_guild(interaction.guild_id) | |
| if not gamemode: | |
| return await interaction.response.send_message("❌ This command can only be used in a gamemode server!", ephemeral=True) | |
| await interaction.response.defer() | |
| embed = discord.Embed( | |
| title="📊 Queue Status", | |
| color=0x5865f2, | |
| timestamp=discord.utils.utcnow() | |
| ) | |
| queue_data_gamemode = self.get_queue_data_for_gamemode(gamemode) | |
| for region in ['AS', 'NA', 'ME']: | |
| role = discord.utils.get(interaction.guild.roles, name=f"{region}_WAITLIST") | |
| member_count = len(role.members) if role else 0 | |
| queue_count = len(queue_data_gamemode[region]['queue']) | |
| status = "🟢 Open" if queue_data_gamemode[region]['is_open'] else "🔴 Closed" | |
| last_closed = "" | |
| if queue_data_gamemode[region].get('last_closed'): | |
| last_closed = f"\nLast Closed: <t:{int(queue_data_gamemode[region]['last_closed'])}:R>" | |
| embed.add_field( | |
| name=f"{region} Region", | |
| value=f"👥 {member_count} in waitlist\n🎮 {queue_count} in queue\n{status}{last_closed}", | |
| inline=True | |
| ) | |
| embed.set_footer(text="CrystalTiers Queue System") | |
| await interaction.followup.send(embed=embed) | |
| async def log_openings(self, interaction: discord.Interaction, days: int = 7): | |
| if not _is_member_at_least(interaction.user, "administrator"): | |
| return await interaction.response.send_message("❌ You don't have permission to use this command.", ephemeral=True) | |
| # ephemeral response as requested | |
| if days < 1 or days > 365: | |
| return await interaction.response.send_message("❌ Days must be between 1 and 365.", ephemeral=True) | |
| cutoff = time.time() - (days * 86400) | |
| embed = discord.Embed( | |
| title=f"📜 Queue Openings (last {days} days)", | |
| color=0x5865f2, | |
| timestamp=discord.utils.utcnow() | |
| ) | |
| for region in ['AS', 'NA', 'ME']: | |
| history = self.open_history.get(region, []) | |
| # support legacy numeric timestamps as well as new dict entries | |
| recent = [] | |
| for entry in history: | |
| if isinstance(entry, (int, float)): | |
| ts = entry | |
| user_id = None | |
| else: | |
| ts = entry.get('ts') | |
| user_id = entry.get('user_id') | |
| if ts and ts >= cutoff: | |
| recent.append({'ts': ts, 'user_id': user_id}) | |
| if not recent: | |
| value = "No openings in this period." | |
| else: | |
| # show up to the last 10 openings for brevity | |
| lines = [] | |
| for entry in recent[-10:]: | |
| ts = int(entry['ts']) | |
| opener = f" - Opened by <@{entry['user_id']}>" if entry.get('user_id') else "" | |
| lines.append(f"<t:{ts}:F> (<t:{ts}:R>){opener}") | |
| value = "\n".join(lines) | |
| embed.add_field(name=f"{region} Region", value=value, inline=False) | |
| await interaction.response.send_message(embed=embed, ephemeral=True) | |
| async def setup_staff_roles( | |
| self, | |
| interaction: discord.Interaction, | |
| network_owner: discord.Role = None, | |
| manager: discord.Role = None, | |
| overseer: discord.Role = None, | |
| administrator: discord.Role = None, | |
| regulator: discord.Role = None, | |
| network_mod: discord.Role = None, | |
| senior_mod: discord.Role = None, | |
| screensharer: discord.Role = None, | |
| discord_mod: discord.Role = None, | |
| discord_helper: discord.Role = None, | |
| senior_tester: discord.Role = None, | |
| verified_tester: discord.Role = None, | |
| staff_team: discord.Role = None | |
| ): | |
| """Set up staff roles with vibrant colors and proper hierarchy""" | |
| # Only allow owner+ | |
| if not _is_member_at_least(interaction.user, "owner"): | |
| return await interaction.response.send_message("❌ You don't have permission to use this command.", ephemeral=True) | |
| if not interaction.user.guild_permissions.administrator: | |
| return await interaction.response.send_message("❌ You need Administrator permissions!", ephemeral=True) | |
| await interaction.response.defer(ephemeral=True) | |
| try: | |
| guild = interaction.guild | |
| bot_role = guild.me.top_role | |
| # Staff roles with vibrant colors (highest to lowest) | |
| staff_roles_config = [ | |
| ('Network Owner', network_owner, 0xFF0000, 'NETWORK_OWNER_ROLE_ID'), | |
| ('Manager', manager, 0xFF3333, 'MANAGER_ROLE_ID'), | |
| ('Overseer', overseer, 0xFF6666, 'OVERSEER_ROLE_ID'), | |
| ('Administrator', administrator, 0xFF9999, 'ADMINISTRATOR_ROLE_ID'), | |
| ('Regulator', regulator, 0xFFCC00, 'REGULATOR_ROLE_ID'), | |
| ('Network Mod', network_mod, 0xFFDD00, 'NETWORK_MOD_ROLE_ID'), | |
| ('Senior Mod', senior_mod, 0x00FF00, 'SENIOR_MOD_ROLE_ID'), | |
| ('Screensharer', screensharer, 0x00CCFF, 'SCREENSHARER_ROLE_ID'), | |
| ('Discord Mod', discord_mod, 0x0099FF, 'DISCORD_MOD_ROLE_ID'), | |
| ('Discord Helper', discord_helper, 0x0050FF, 'DISCORD_HELPER_ROLE_ID'), | |
| ('Senior Tester', senior_tester, 0x6B5BFF, 'SENIOR_TESTER_ROLE_ID'), | |
| ('Verified Tester', verified_tester, 0x9D4EDD, 'VERIFIED_TESTER_ROLE_ID'), | |
| ('Staff Team', staff_team, 0xC77DFF, 'STAFF_TEAM_ROLE_ID'), | |
| ] | |
| created_roles = [] | |
| above_bot_roles = [] | |
| new_roles_to_save = {} # For saving to roles.json | |
| next_position = max(1, bot_role.position - 1) # Ensure position is at least 1 | |
| # Process provided roles or create them | |
| for role_display_name, provided_role, color_val, json_key in staff_roles_config: | |
| if provided_role: # Role was provided | |
| # Check if role is above bot | |
| if provided_role.position >= bot_role.position: | |
| above_bot_roles.append(f"• {role_display_name}: `{provided_role.name}` (Above bot, skipped)") | |
| else: | |
| # Update color if not above bot | |
| try: | |
| await provided_role.edit( | |
| color=discord.Color(color_val), | |
| reason=f"Staff role setup by {interaction.user.name}" | |
| ) | |
| created_roles.append(f"• {role_display_name}: `{provided_role.name}` (Updated)") | |
| except discord.Forbidden: | |
| created_roles.append(f"• {role_display_name}: `{provided_role.name}` (Updated - no color perms)") | |
| else: # Create new role | |
| # Check if role already exists | |
| existing_role = discord.utils.get(guild.roles, name=role_display_name) | |
| if existing_role: | |
| created_roles.append(f"• {role_display_name}: Already exists") | |
| else: | |
| # Create role below bot | |
| try: | |
| new_role = await guild.create_role( | |
| name=role_display_name, | |
| color=discord.Color(color_val), | |
| mentionable=True, | |
| reason=f"Staff role setup by {interaction.user.name}" | |
| ) | |
| # Position consecutively without gaps (ensure position >= 1) | |
| try: | |
| position = max(1, next_position) | |
| await new_role.edit(position=position) | |
| except Exception: | |
| pass # Position might fail but role still created | |
| next_position = max(1, next_position - 1) # Decrement but don't go below 1 | |
| created_roles.append(f"• {role_display_name}: `{new_role.name}` (Created)") | |
| new_roles_to_save[json_key] = new_role.id | |
| except discord.Forbidden: | |
| created_roles.append(f"• {role_display_name}: Failed to create (insufficient permissions)") | |
| except Exception as e: | |
| created_roles.append(f"• {role_display_name}: Failed - {str(e)}") | |
| # Save new role IDs to roles.json | |
| if new_roles_to_save: | |
| roles_json_path = os.path.join(MISC_DIR, "roles.json") | |
| existing_roles = load_json(roles_json_path, {}) | |
| existing_roles.update(new_roles_to_save) | |
| save_json(roles_json_path, existing_roles) | |
| # Build response embed | |
| embed = discord.Embed( | |
| title="✅ Staff Roles Setup Complete!", | |
| color=0x00FF00, | |
| timestamp=discord.utils.utcnow() | |
| ) | |
| if created_roles: | |
| embed.add_field( | |
| name="✨ Processed Roles", | |
| value="\n".join(created_roles), | |
| inline=False | |
| ) | |
| if above_bot_roles: | |
| embed.add_field( | |
| name="⚠️ Roles Above Bot (Skipped)", | |
| value="These roles are already above the bot in hierarchy, so they were left unchanged:\n" + "\n".join(above_bot_roles), | |
| inline=False | |
| ) | |
| if new_roles_to_save: | |
| embed.add_field( | |
| name="💾 Saved to roles.json", | |
| value=f"✅ {len(new_roles_to_save)} new role IDs saved", | |
| inline=False | |
| ) | |
| embed.set_footer(text="All roles positioned consecutively below bot role for proper hierarchy") | |
| await interaction.followup.send(embed=embed, ephemeral=True) | |
| except Exception as e: | |
| print(f"Error in setup_staff_roles: {e}") | |
| import traceback | |
| traceback.print_exc() | |
| await interaction.followup.send( | |
| f"❌ Error setting up roles: {str(e)}", | |
| ephemeral=True | |
| ) | |
| # Ticket System Cog | |
| class QueueTicketSystem(commands.Cog, name="QueueTicketSystem"): | |
| def __init__(self, bot): | |
| self.bot = bot | |
| async def _get_logs_channel(self, guild: discord.Guild) -> discord.TextChannel: | |
| """Get or create the queue-ticket-logs channel""" | |
| ch = discord.utils.get(guild.text_channels, name=QueueTICKET_LOGS_CHANNEL_NAME) | |
| if ch: | |
| return ch | |
| print(f"[LOGS] Creating {QueueTICKET_LOGS_CHANNEL_NAME} channel") | |
| ch = await guild.create_text_channel(QueueTICKET_LOGS_CHANNEL_NAME) | |
| print(f"[LOGS] Created {QueueTICKET_LOGS_CHANNEL_NAME} channel") | |
| return ch | |
| async def log_command(self, interaction: discord.Interaction, command_name: str, parameters: dict = None, result: str = "executed"): | |
| """Log a command to the command-logs channel""" | |
| guild = interaction.guild | |
| if not guild: | |
| return | |
| # Get or create command-logs channel (admin private only) | |
| cmd_logs_channel = discord.utils.get(guild.text_channels, name=COMMAND_LOGS_CHANNEL_NAME) | |
| if not cmd_logs_channel: | |
| try: | |
| # Create channel with admin only permissions | |
| cmd_logs_channel = await guild.create_text_channel( | |
| COMMAND_LOGS_CHANNEL_NAME, | |
| overwrites={ | |
| guild.default_role: discord.PermissionOverwrite(view_channel=False), | |
| guild.me: discord.PermissionOverwrite(view_channel=True, send_messages=True) | |
| } | |
| ) | |
| except Exception as e: | |
| print(f"[LOG] Failed to create command-logs channel: {e}") | |
| return | |
| # Build embed | |
| embed = discord.Embed( | |
| title=f"⌘ Command: {command_name}", | |
| color=0x5865f2, | |
| timestamp=discord.utils.utcnow() | |
| ) | |
| embed.add_field(name="User", value=f"{interaction.user.mention}", inline=False) | |
| if parameters: | |
| params_str = "\n".join([f"• {k}: {v}" for k, v in parameters.items()]) | |
| embed.add_field(name="Parameters", value=params_str, inline=False) | |
| embed.add_field(name="Result", value=result, inline=False) | |
| embed.set_footer(text=f"User ID: {interaction.user.id}") | |
| try: | |
| await cmd_logs_channel.send(embed=embed) | |
| except Exception as e: | |
| print(f"[LOG] Failed to send command log: {e}") | |
| async def close_ticket(self, channel: discord.TextChannel, closed_by: discord.Member): | |
| guild = channel.guild | |
| network_owner_role_id = getattr(config, "NETWORK_OWNER_ROLE_ID", None) | |
| owner_role = guild.get_role(network_owner_role_id) if network_owner_role_id else None | |
| # make read-only | |
| overwrites = channel.overwrites_for(guild.default_role) | |
| overwrites.send_messages = False | |
| overwrites.view_channel = True | |
| await channel.set_permissions(guild.default_role, overwrite=overwrites) | |
| if owner_role: | |
| await channel.set_permissions(owner_role, send_messages=True, view_channel=True) | |
| await channel.send("Channel will be closed in 5 seconds...") | |
| await asyncio.sleep(5) | |
| # Collect messages from channel history | |
| messages = [] | |
| async for msg in channel.history(limit=None, oldest_first=True): | |
| attachments = "" | |
| if msg.attachments: | |
| attachments = " | Attachments: " + ", ".join(a.url for a in msg.attachments) | |
| time_str = msg.created_at.strftime("%Y-%m-%d %H:%M:%S") | |
| content = msg.content.replace("\n", " ") if msg.content else "" | |
| messages.append(f"[{time_str}] {msg.author} ({msg.author.id}): {content}{attachments}") | |
| transcript_text = "\n".join(messages) if messages else "No messages found." | |
| logs = await self._get_logs_channel(guild) | |
| filename = f"{channel.name}-transcript.txt" | |
| file = discord.File(fp=io.BytesIO(transcript_text.encode()), filename=filename) | |
| await logs.send(content=f"Transcript for {channel.name} closed by {closed_by.mention}", file=file) | |
| await channel.set_permissions(guild.default_role, view_channel=False) | |
| if owner_role: | |
| await channel.set_permissions(owner_role, view_channel=True, send_messages=True) | |
| await logs.send(f"{closed_by.mention} closed {channel.name} and transcript has been saved.") | |
| async def slash_add(self, interaction: discord.Interaction, member: discord.Member): | |
| if not _is_member_at_least(interaction.user, "tester"): | |
| return await interaction.response.send_message("❌ You don't have permission to use this command.", ephemeral=True) | |
| await interaction.response.defer(ephemeral=True) | |
| channel = interaction.channel | |
| if not isinstance(channel, discord.TextChannel): | |
| await interaction.followup.send("This command must be used in a ticket channel.", ephemeral=True) | |
| return | |
| prefix = channel.name[:2].lower() if channel.name and len(channel.name) >= 2 else "" | |
| if prefix not in ("as", "na", "me", "eu", "au"): | |
| await interaction.followup.send("This command must be used in a ticket channel starting with AS / NA / ME / EU / AU.", ephemeral=True) | |
| return | |
| await channel.set_permissions(member, view_channel=True, send_messages=True, read_message_history=True) | |
| await interaction.followup.send(f"{member.mention} was added to the ticket.", ephemeral=True) | |
| await channel.send(f"{member.mention} has been added to the ticket by {interaction.user.mention}.") | |
| async def slash_remove(self, interaction: discord.Interaction, member: discord.Member): | |
| if not _is_member_at_least(interaction.user, "tester"): | |
| return await interaction.response.send_message("❌ You don't have permission to use this command.", ephemeral=True) | |
| await interaction.response.defer(ephemeral=True) | |
| channel = interaction.channel | |
| if not isinstance(channel, discord.TextChannel): | |
| await interaction.followup.send("This command must be used in a ticket channel.", ephemeral=True) | |
| return | |
| prefix = channel.name[:2].lower() if channel.name and len(channel.name) >= 2 else "" | |
| if prefix not in ("as", "na", "me", "eu", "au"): | |
| await interaction.followup.send("This command must be used in a ticket channel starting with AS / NA / ME / EU / AU.", ephemeral=True) | |
| return | |
| await channel.set_permissions(member, overwrite=None) | |
| await interaction.followup.send(f"{member.mention} was removed from the ticket.", ephemeral=True) | |
| await channel.send(f"{member.mention} has been removed from the ticket by {interaction.user.mention}.") | |
| async def close(self, interaction: discord.Interaction, tier: app_commands.Choice[str]): | |
| print(f"\n\n{'='*80}") | |
| print(f"[CLOSE-COMMAND] /close command was CALLED by {interaction.user.name}") | |
| print(f"[CLOSE-COMMAND] Tier selected: {tier.value}") | |
| print(f"[CLOSE-COMMAND] Channel: {interaction.channel.name if interaction.channel else 'NO CHANNEL'}") | |
| print(f"{'='*80}\n") | |
| # Get the gamemode for this guild | |
| gamemode = get_gamemode_for_guild(interaction.guild_id) | |
| if not gamemode: | |
| print(f"[CLOSE-COMMAND] BLOCKED: Not a valid gamemode server") | |
| return await interaction.response.send_message("❌ This command can only be used in a gamemode server!", ephemeral=True) | |
| print(f"[CLOSE-COMMAND] 🎮 Gamemode: {gamemode}") | |
| if not _is_member_at_least(interaction.user, "tester"): | |
| print(f"[CLOSE-COMMAND] BLOCKED: User not allowed (_is_member_at_least check failed)") | |
| return await interaction.response.send_message("❌ You don't have permission to use this command.", ephemeral=True) | |
| print(f"[CLOSE-COMMAND] ✅ Permission check passed") | |
| channel = interaction.channel | |
| guild = interaction.guild | |
| # ✅ Only allow closing if it's a valid ticket channel (format: region-username) | |
| if not isinstance(channel, discord.TextChannel): | |
| print(f"[CLOSE-COMMAND] BLOCKED: Not a text channel") | |
| return await interaction.response.send_message("❌ This command can only be used in a text channel.", ephemeral=True) | |
| prefix = channel.name[:2].lower() if channel.name and len(channel.name) >= 2 else "" | |
| if prefix not in ("as", "na", "me", "eu", "au"): | |
| print(f"[CLOSE-COMMAND] BLOCKED: Invalid channel prefix '{prefix}'") | |
| return await interaction.response.send_message("❌ This command can only be used inside a ticket channel starting with AS / NA / ME / EU / AU.", ephemeral=True) | |
| print(f"[CLOSE-COMMAND] ✅ Channel validation passed") | |
| await interaction.response.defer(ephemeral=True) | |
| print(f"[CLOSE-COMMAND] ✅ Interaction deferred") | |
| # Extract player and get their database info | |
| region = prefix.upper() | |
| username = None | |
| player_member = None | |
| # Check if this is an unretire or high test ticket (special naming) | |
| is_unretire = channel.category and channel.category.name.lower() == "unretire" | |
| is_high_tier = channel.category and channel.category.name.lower() == "high test" | |
| unretire_gamemode = gamemode # Use the guild's gamemode for unretire | |
| # For unretire/high test tickets, extract user from topic | |
| if is_unretire or is_high_tier: | |
| topic = channel.topic or "" | |
| # Extract user mention from topic like "Unretire Test for <@123456> | Region: AS" | |
| if "<@" in topic and ">" in topic: | |
| user_id_str = topic.split("<@")[1].split(">")[0] | |
| try: | |
| player_member = guild.get_member(int(user_id_str)) | |
| print(f"[CLOSE-COMMAND] Found player from topic: {player_member.name if player_member else 'None'}") | |
| except (ValueError, AttributeError): | |
| pass | |
| # For regular queue tickets, extract from channel name | |
| if not player_member: | |
| username_from_channel = channel.name[3:].split("-")[0] if len(channel.name) > 3 else "" | |
| print(f"[CLOSE-COMMAND] Extracted username from channel: '{username_from_channel}'") | |
| print(f"[CLOSE-COMMAND] Full channel name: '{channel.name}'") | |
| print(f"[CLOSE-COMMAND] Channel topic: '{channel.topic}'") | |
| # Try method 1: Match by Discord username (exact case-insensitive) | |
| for member in guild.members: | |
| if member.name.lower() == username_from_channel.lower(): | |
| player_member = member | |
| print(f"[CLOSE-COMMAND] ✓ Found player by username: {member.name} ({member.id})") | |
| break | |
| # Try method 2: Match by display name if username didn't work | |
| if not player_member: | |
| for member in guild.members: | |
| if hasattr(member, 'display_name') and member.display_name.lower() == username_from_channel.lower(): | |
| player_member = member | |
| print(f"[CLOSE-COMMAND] ✓ Found player by display name: {member.display_name} ({member.id})") | |
| break | |
| # Try method 3: Match by nickname if available | |
| if not player_member: | |
| for member in guild.members: | |
| if member.nick and member.nick.lower() == username_from_channel.lower(): | |
| player_member = member | |
| print(f"[CLOSE-COMMAND] ✓ Found player by nickname: {member.nick} ({member.id})") | |
| break | |
| # Try method 4: Check channel permissions - who has access to this channel | |
| if not player_member: | |
| print(f"[CLOSE-COMMAND] No member found by name, checking channel permissions...") | |
| try: | |
| # Get all members who have explicit permissions in this channel | |
| for target, overwrite in channel.overwrites.items(): | |
| if isinstance(target, discord.Member) and overwrite.view_channel in (True, None): | |
| print(f"[CLOSE-COMMAND] Found member with channel access: {target.name} ({target.id})") | |
| # Use the first member found with access (likely the player) | |
| if player_member is None: | |
| player_member = target | |
| except Exception as e: | |
| print(f"[CLOSE-COMMAND] Could not check channel permissions: {e}") | |
| if not player_member: | |
| print(f"[CLOSE-COMMAND] ❌ Could not identify player after trying all methods") | |
| print(f"[CLOSE-COMMAND] Guild has {len(guild.members)} total members") | |
| return await interaction.followup.send( | |
| f"❌ Could not identify the player for this ticket.\n" | |
| f"*Try pinning a user mention in the channel topic or make sure the player is in the server.*", | |
| ephemeral=True | |
| ) | |
| # Get player info from gamemode-specific database | |
| db_path = get_gamemode_database_path(gamemode) | |
| database = load_json(db_path, {}) | |
| for key, data in database.items(): | |
| if data.get("discord_id") == player_member.id and data.get("region") == region: | |
| username = data.get("username", player_member.name) | |
| break | |
| if not username: | |
| username = player_member.name | |
| # Get previous tier | |
| previous_tier = "N/A" | |
| is_unretire = channel.category and channel.category.name.lower() == "unretire" | |
| unretire_gamemode = "vanilla" # Always vanilla for unretire | |
| # Handle tier role removal | |
| for role in player_member.roles: | |
| if role.name in TIER_ROLES: | |
| previous_tier = role.name | |
| await player_member.remove_roles(role) | |
| break | |
| # If unretire ticket, also remove the Retired role and call API to unretire | |
| if is_unretire: | |
| for role in player_member.roles: | |
| if role.name.startswith("Retired "): | |
| await player_member.remove_roles(role) | |
| break | |
| # Call API to update Supabase retired status | |
| try: | |
| import aiohttp | |
| api_url = os.getenv("API_BASE_URL", "http://localhost:5000") | |
| async with aiohttp.ClientSession() as session: | |
| payload = { | |
| "username": username, | |
| "gamemode": unretire_gamemode | |
| } | |
| async with session.post(f"{api_url}/unretire", json=payload) as response: | |
| api_result = await response.json() | |
| if api_result.get("success"): | |
| print(f"[CLOSE] ✅ Updated database: Player unretired from {unretire_gamemode}") | |
| else: | |
| print(f"[CLOSE] ⚠️ Failed to update database: {api_result.get('error')}") | |
| except Exception as api_err: | |
| print(f"[CLOSE] ⚠️ API error during unretire: {str(api_err)}") | |
| # Tier role colors mapping | |
| tier_colors = { | |
| 'LT5': 0xFF0000, 'LT4': 0xFF3333, 'HT5': 0xFF6666, 'LT3': 0xFF9999, | |
| 'HT4': 0xFFCC00, 'LT2': 0xFFDD00, 'HT3': 0x00FF00, 'HT2': 0x00CCFF, | |
| 'LT1': 0x0099FF, 'HT1': 0x0050FF, | |
| } | |
| # Assign new tier with color | |
| tier_role = discord.utils.get(guild.roles, name=tier.value) | |
| if not tier_role: | |
| tier_color = tier_colors.get(tier.value, 0x808080) | |
| tier_role = await guild.create_role( | |
| name=tier.value, | |
| color=discord.Color(tier_color), | |
| mentionable=True | |
| ) | |
| await player_member.add_roles(tier_role) | |
| # Determine if this is a high tier test (check if in "high test" category) | |
| is_high_tier = channel.category and channel.category.name.lower() == "high test" | |
| # ALWAYS apply cooldown to tested players (7 days for queue, 14 days for high tier) | |
| cooldown_duration = 14 * 86400 if is_high_tier else 7 * 86400 | |
| cooldown_until = time.time() + cooldown_duration | |
| cooldowns_path = get_gamemode_database_path(gamemode, "cooldowns") | |
| cooldowns_data = load_json(cooldowns_path, {"queue": {}, "high_ticket": {}}) | |
| user_id_str = str(player_member.id) | |
| # Apply cooldown to appropriate category | |
| if is_high_tier: | |
| print(f"\n[CLOSE] 🔴 HIGH TIER TEST - Setting 14-day cooldown") | |
| print(f"[CLOSE] 👤 Player: {player_member.name} (ID: {player_member.id})") | |
| print(f"[CLOSE] 🆔 User ID string: '{user_id_str}'") | |
| print(f"[CLOSE] ⏰ Cooldown until (unix): {cooldown_until}") | |
| cooldowns_data["high_ticket"][user_id_str] = {"expires_at": cooldown_until} | |
| print(f"[CLOSE] 📦 Saved to high_ticket: {cooldowns_data['high_ticket'][user_id_str]}") | |
| else: | |
| print(f"\n[CLOSE] 🟡 QUEUE TEST - Setting 7-day cooldown") | |
| print(f"[CLOSE] 👤 Player: {player_member.name} (ID: {player_member.id})") | |
| print(f"[CLOSE] 🆔 User ID string: '{user_id_str}'") | |
| print(f"[CLOSE] ⏰ Cooldown until (unix): {cooldown_until}") | |
| cooldowns_data["queue"][user_id_str] = {"expires_at": cooldown_until} | |
| print(f"[CLOSE] 📦 Saved to queue: {cooldowns_data['queue'][user_id_str]}") | |
| print(f"[CLOSE] 📋 Full queue dict after update: {cooldowns_data['queue']}") | |
| save_json(cooldowns_path, cooldowns_data) | |
| print(f"[CLOSE] ✅ Cooldown saved to file successfully") | |
| # Log the result with appropriate cooldown type | |
| cooldown_type = "high_ticket" if is_high_tier else "queue" | |
| log_result(player_member.id, username, previous_tier, tier.value, region, interaction.user.id, cooldown_type=cooldown_type, gamemode=gamemode) | |
| # Post result to configured results channel or default | |
| results_channel = None | |
| if RESULT_CHANNEL_ID: | |
| results_channel = guild.get_channel(RESULT_CHANNEL_ID) | |
| if not results_channel: | |
| results_channel = discord.utils.get(guild.text_channels, name="results") | |
| if not results_channel: | |
| results_channel = await guild.create_text_channel("results") | |
| results_data = load_results_data(gamemode) | |
| position = results_data.get("counter", 0) | |
| # Create result embed | |
| embed = discord.Embed( | |
| title=f"{username}'s Test Results 🏆", | |
| color=discord.Color.gold() | |
| ) | |
| embed.add_field(name="Tester:", value=interaction.user.mention, inline=False) | |
| embed.add_field(name="Region:", value=region, inline=False) | |
| embed.add_field(name="Username:", value=username, inline=False) | |
| embed.add_field(name="Previous Rank:", value=previous_tier, inline=False) | |
| embed.add_field(name="Rank Earned:", value=tier.value, inline=False) | |
| # Fetch and set Minecraft skin as thumbnail (positioned to right, full body view) | |
| skin_url = await get_minecraft_skin_url(username) | |
| print(f"DEBUG: Skin URL for {username}: {skin_url}") | |
| if skin_url: | |
| embed.set_thumbnail(url=skin_url) | |
| else: | |
| print(f"DEBUG: No skin URL returned for {username}") | |
| embed.set_footer(text=f"Position: #{position}") | |
| # Send mention first | |
| await results_channel.send(content=f"{player_member.mention}") | |
| # Then send embed with reactions | |
| result_msg = await results_channel.send(embed=embed) | |
| # Add reactions | |
| reactions = ["👑", "🎉", "😊", "😢", "💀"] | |
| for emoji in reactions: | |
| try: | |
| await result_msg.add_reaction(emoji) | |
| except Exception as e: | |
| print(f"Error adding reaction: {e}") | |
| # ==================== AUTO-UPLOAD TO WEBSITE ==================== | |
| # Automatically upload tier result to website using the guild's gamemode | |
| print(f"[CLOSE] 📤 Uploading result to website API for gamemode: {gamemode}...") | |
| try: | |
| from .website_cog import upload_player_tier_to_api | |
| # Use the actual gamemode detected from this guild | |
| upload_result = await upload_player_tier_to_api(username, region, gamemode, tier.value) | |
| if upload_result.get("success"): | |
| print(f"[CLOSE] ✅ Successfully uploaded to website: {username} ({region}) - {gamemode}: {tier.value}") | |
| else: | |
| print(f"[CLOSE] ⚠️ Failed to upload to website: {upload_result.get('error', 'Unknown error')}") | |
| except Exception as upload_err: | |
| print(f"[CLOSE] ⚠️ Error during website upload: {upload_err}") | |
| # Send cooldown notice to ticket channel | |
| cooldown_duration_text = "14 days" if is_high_tier else "7 days" | |
| await channel.send(f"🔒 **Cooldown Applied:** {player_member.mention} has a **{cooldown_duration_text}** cooldown from retesting.") | |
| # Log command | |
| print(f"[CLOSE-COMMAND] About to log command...") | |
| try: | |
| await self.log_command( | |
| interaction, | |
| "close", | |
| {"player": player_member, "previous_tier": previous_tier, "new_tier": tier.value, "region": region}, | |
| f"Closed ticket and assigned {tier.value} to {player_member} (was {previous_tier})" | |
| ) | |
| print(f"[CLOSE-COMMAND] ✅ Command logged") | |
| except Exception as log_err: | |
| print(f"[CLOSE-COMMAND] ⚠️ Could not log command: {log_err}") | |
| print(f"[CLOSE-COMMAND] Continuing anyway...") | |
| print(f"[CLOSE-COMMAND] About to send followup...") | |
| cooldown_duration_text = "14 days" if is_high_tier else "7 days" | |
| await interaction.followup.send( | |
| f"✅ Result posted for {player_member.mention} → {tier.value}\n" | |
| f"🔒 Cooldown: {cooldown_duration_text}", | |
| ephemeral=True | |
| ) | |
| print(f"[CLOSE-COMMAND] ✅ Followup sent!") | |
| # Now handle transcript and deletion immediately (synchronously) | |
| print(f"\n{'='*60}") | |
| print(f"[CLOSE] ===== STARTING CLOSE SEQUENCE FOR {channel.name} =====") | |
| print(f"[CLOSE] User: {interaction.user.name} ({interaction.user.id})") | |
| print(f"[CLOSE] Channel: {channel.name} ({channel.id})") | |
| print(f"[CLOSE] Guild: {guild.name}") | |
| print(f"{'='*60}\n") | |
| # Step 1: Save transcript using proven method | |
| try: | |
| print(f"[CLOSE] STEP 1: Collecting transcript...") | |
| # Collect all messages (like the old working code) | |
| messages_list = [] | |
| message_count = 0 | |
| async for msg in channel.history(limit=100, oldest_first=True): | |
| try: | |
| message_count += 1 | |
| # Format: [timestamp] author (id): content | attachments | |
| attachments_str = "" | |
| if msg.attachments: | |
| attachments_str = " | Attachments: " + ", ".join(a.url for a in msg.attachments) | |
| time_str = msg.created_at.strftime("%Y-%m-%d %H:%M:%S") | |
| content = msg.content.replace("\n", " ") if msg.content else "[no text]" | |
| msg_line = f"[{time_str}] {msg.author} ({msg.author.id}): {content}{attachments_str}" | |
| messages_list.append(msg_line) | |
| except Exception as me: | |
| print(f"[CLOSE] ERROR processing message {message_count}: {me}") | |
| continue | |
| print(f"[CLOSE] ✅ Collected {len(messages_list)} messages") | |
| # Build transcript | |
| transcript_text = "\n".join(messages_list) if messages_list else "[No messages recorded]" | |
| print(f"[CLOSE] ✅ Transcript built: {len(transcript_text)} bytes") | |
| # Save to logs channel | |
| print(f"[CLOSE] STEP 2: Getting logs channel...") | |
| try: | |
| logs_channel = await self._get_logs_channel(guild) | |
| print(f"[CLOSE] ✅ Got logs channel: {logs_channel.name} ({logs_channel.id})") | |
| except Exception as lc_err: | |
| print(f"[CLOSE] ❌ ERROR getting logs channel: {lc_err}") | |
| import traceback | |
| traceback.print_exc() | |
| raise | |
| print(f"[CLOSE] STEP 3: Creating file object...") | |
| try: | |
| filename = f"{channel.name}-transcript.txt" | |
| file_obj = discord.File( | |
| io.BytesIO(transcript_text.encode('utf-8')), | |
| filename=filename | |
| ) | |
| print(f"[CLOSE] ✅ File created: {filename}") | |
| except Exception as fo_err: | |
| print(f"[CLOSE] ❌ ERROR creating file: {fo_err}") | |
| import traceback | |
| traceback.print_exc() | |
| raise | |
| print(f"[CLOSE] STEP 4: Sending to logs channel...") | |
| try: | |
| msg = await logs_channel.send( | |
| content=f"Transcript for `{channel.name}` closed by {interaction.user.mention}", | |
| file=file_obj | |
| ) | |
| print(f"[CLOSE] ✅ Transcript sent! Message ID: {msg.id}") | |
| except Exception as send_err: | |
| print(f"[CLOSE] ❌ ERROR sending transcript: {send_err}") | |
| import traceback | |
| traceback.print_exc() | |
| raise | |
| except Exception as e: | |
| print(f"[CLOSE] ❌❌❌ CRITICAL ERROR IN TRANSCRIPT SAVE: {e}") | |
| import traceback | |
| traceback.print_exc() | |
| return # Don't proceed with deletion if transcript fails | |
| print(f"\n[CLOSE] Transcript save COMPLETED successfully!\n") | |
| # Step 2: Delete the channel (in background to be safe) | |
| async def delete_channel_task(): | |
| try: | |
| print(f"\n{'='*60}") | |
| print(f"[DELETE-BG] ===== STARTING CHANNEL DELETION (BACKGROUND) =====") | |
| await asyncio.sleep(2.0) | |
| print(f"[DELETE-BG] Waiting 2 seconds... done") | |
| print(f"[DELETE-BG] STEP 1: Checking channel validity...") | |
| # Refresh channel object to ensure it's valid | |
| try: | |
| fresh_channel = guild.get_channel(channel.id) | |
| if not fresh_channel: | |
| print(f"[DELETE-BG] ❌ Channel not found by ID {channel.id}") | |
| print(f"[DELETE-BG] Available channels: {[ch.name for ch in guild.text_channels][:10]}") | |
| return | |
| print(f"[DELETE-BG] ✅ Channel found: {fresh_channel.name}") | |
| except Exception as ref_err: | |
| print(f"[DELETE-BG] ❌ ERROR refreshing channel: {ref_err}") | |
| import traceback | |
| traceback.print_exc() | |
| return | |
| # Check bot permissions | |
| print(f"[DELETE-BG] STEP 2: Checking permissions...") | |
| try: | |
| bot_perms = fresh_channel.permissions_for(guild.me) | |
| print(f"[DELETE-BG] Bot user: {guild.me.name} ({guild.me.id})") | |
| print(f"[DELETE-BG] - manage_channels: {bot_perms.manage_channels}") | |
| print(f"[DELETE-BG] - manage_guild: {bot_perms.manage_guild}") | |
| print(f"[DELETE-BG] - administrator: {bot_perms.administrator}") | |
| if not bot_perms.manage_channels: | |
| print(f"[DELETE-BG] ❌ Bot missing manage_channels permission!") | |
| return | |
| print(f"[DELETE-BG] ✅ Bot has manage_channels permission") | |
| except Exception as perm_err: | |
| print(f"[DELETE-BG] ❌ ERROR checking permissions: {perm_err}") | |
| import traceback | |
| traceback.print_exc() | |
| return | |
| # Try to delete | |
| print(f"[DELETE-BG] STEP 3: Deleting channel {fresh_channel.name}...") | |
| try: | |
| await fresh_channel.delete(reason=f"Closed by {interaction.user.name}") | |
| print(f"[DELETE-BG] ✅✅✅ Channel DELETED successfully!") | |
| except discord.Forbidden as fb: | |
| print(f"[DELETE-BG] ❌ PERMISSION DENIED: {fb}") | |
| print(f"[DELETE-BG] Bot may not have permission or role hierarchy issue") | |
| except discord.HTTPException as he: | |
| print(f"[DELETE-BG] ❌ HTTP ERROR: {he}") | |
| print(f"[DELETE-BG] Status code: {he.status}") | |
| except discord.NotFound as nf: | |
| print(f"[DELETE-BG] ❌ CHANNEL NOT FOUND: {nf}") | |
| print(f"[DELETE-BG] Channel was already deleted or doesn't exist") | |
| except Exception as de: | |
| print(f"[DELETE-BG] ❌ UNEXPECTED ERROR: {type(de).__name__}: {de}") | |
| import traceback | |
| traceback.print_exc() | |
| except Exception as e: | |
| print(f"[DELETE-BG] ❌❌❌ TASK CRITICAL ERROR: {e}") | |
| import traceback | |
| traceback.print_exc() | |
| finally: | |
| print(f"{'='*60}\n") | |
| # Fire the deletion task | |
| print(f"[CLOSE] Starting background deletion task...") | |
| asyncio.create_task(delete_channel_task()) | |
| async def pass_to_high_tier(self, interaction: discord.Interaction): | |
| """Move a queue ticket to high test category for high tier testing""" | |
| if not _is_member_at_least(interaction.user, "tester"): | |
| return await interaction.response.send_message("❌ You don't have permission to use this command.", ephemeral=True) | |
| channel = interaction.channel | |
| if not isinstance(channel, discord.TextChannel): | |
| return await interaction.response.send_message("❌ This command must be used in a ticket channel.", ephemeral=True) | |
| # Check if channel is in "Tier Testing" category | |
| if not channel.category or channel.category.name.lower() != "tier testing": | |
| return await interaction.response.send_message( | |
| "❌ This command can only be used on queue tickets in the 'Tier Testing' category.", | |
| ephemeral=True | |
| ) | |
| # Get or create "high test" category | |
| guild = interaction.guild | |
| high_test_category = discord.utils.get(guild.categories, name="high test") | |
| if not high_test_category: | |
| high_test_category = await guild.create_category("high test") | |
| # Move channel to high test category | |
| try: | |
| await channel.edit(category=high_test_category) | |
| await interaction.response.send_message( | |
| "✅ Ticket moved to high test category! User can now be tested as a high tier candidate.", | |
| ephemeral=True | |
| ) | |
| await channel.send( | |
| f"🎯 This ticket has been promoted to **High Tier Testing**! {interaction.user.mention} has moved it here for high tier evaluation." | |
| ) | |
| except Exception as e: | |
| await interaction.response.send_message(f"❌ Failed to move ticket: {str(e)}", ephemeral=True) | |
| async def add_role_to_ticket(self, interaction: discord.Interaction, role: discord.Role): | |
| """Add a role to the ticket with same permissions as the tester""" | |
| if not _is_member_at_least(interaction.user, "tester"): | |
| return await interaction.response.send_message("❌ You don't have permission to use this command.", ephemeral=True) | |
| channel = interaction.channel | |
| if not isinstance(channel, discord.TextChannel): | |
| return await interaction.response.send_message("This command must be used in a ticket channel.", ephemeral=True) | |
| # Get tester permissions from the channel | |
| tester_perms = channel.permissions_for(interaction.user) | |
| # Add the role with the same permissions | |
| try: | |
| await channel.set_permissions( | |
| role, | |
| view_channel=tester_perms.view_channel, | |
| send_messages=tester_perms.send_messages, | |
| read_message_history=tester_perms.read_message_history, | |
| embed_links=tester_perms.embed_links, | |
| attach_files=tester_perms.attach_files | |
| ) | |
| await channel.send(f"✅ {role.mention} has been added to the ticket with tester permissions (added by {interaction.user.mention}).") | |
| await interaction.response.send_message(f"✅ {role.mention} was added to the ticket.", ephemeral=True) | |
| except Exception as e: | |
| await interaction.response.send_message(f"❌ Failed to add role: {str(e)}", ephemeral=True) | |
| async def slash_addrole(self, interaction: discord.Interaction, role: discord.Role): | |
| if not _is_member_at_least(interaction.user, "overseer"): | |
| return await interaction.response.send_message("❌ You don't have permission to use this command.", ephemeral=True) | |
| await interaction.response.defer(ephemeral=True) | |
| channel = interaction.channel | |
| if not isinstance(channel, discord.TextChannel): | |
| await interaction.followup.send("This command must be used in a ticket channel.", ephemeral=True) | |
| return | |
| prefix = channel.name[:2].lower() if channel.name and len(channel.name) >= 2 else "" | |
| if prefix not in ("as", "na", "me", "eu", "au"): | |
| await interaction.followup.send("This command must be used in a ticket channel starting with AS / NA / ME / EU / AU.", ephemeral=True) | |
| return | |
| candidates = [m for m in role.members if not m.bot and m.status != discord.Status.offline] | |
| picked = None | |
| if candidates: | |
| picked = random.choice(candidates) | |
| else: | |
| nonbots = [m for m in role.members if not m.bot] | |
| if nonbots: | |
| picked = random.choice(nonbots) | |
| if not picked: | |
| await interaction.followup.send("No available human members found in that role.", ephemeral=True) | |
| return | |
| await channel.set_permissions(picked, view_channel=True, send_messages=True, read_message_history=True) | |
| await channel.send(f"{picked.mention} has been added to the ticket (role add by {interaction.user.mention}).") | |
| await interaction.followup.send(f"{picked.mention} was added to the ticket.", ephemeral=True) | |
| async def retire_user(self, interaction: discord.Interaction, user: discord.Member): | |
| """Retire a user in their gamemode""" | |
| if not _is_member_at_least(interaction.user, "senior_tester"): | |
| return await interaction.response.send_message("❌ You don't have permission to use this command.", ephemeral=True) | |
| if not interaction.user.guild_permissions.manage_roles: | |
| return await interaction.response.send_message("❌ You need Manage Roles permission!", ephemeral=True) | |
| # Check if this is a valid gamemode server | |
| gamemode = get_gamemode_for_guild(interaction.guild_id) | |
| if not gamemode: | |
| return await interaction.response.send_message( | |
| "❌ This command can only be used in a gamemode server!", | |
| ephemeral=True | |
| ) | |
| guild = interaction.guild | |
| # Get player username from gamemode-specific database | |
| db_path = get_gamemode_database_path(gamemode) | |
| database = load_json(db_path, {}) | |
| player_username = None | |
| for key, data in database.items(): | |
| if data.get("discord_id") == user.id: | |
| player_username = data.get("username") | |
| break | |
| if not player_username: | |
| return await interaction.response.send_message( | |
| f"❌ {user.mention} is not verified in the {gamemode} database!", | |
| ephemeral=True | |
| ) | |
| await interaction.response.defer(ephemeral=True) | |
| # Tier colors mapping | |
| tier_colors = { | |
| 'LT5': 0xFF0000, 'LT4': 0xFF3333, 'HT5': 0xFF6666, 'LT3': 0xFF9999, | |
| 'HT4': 0xFFCC00, 'LT2': 0xFFDD00, 'HT3': 0x00FF00, 'HT2': 0x00CCFF, | |
| 'LT1': 0x0099FF, 'HT1': 0x0050FF, | |
| } | |
| # Find current tier role for this user | |
| current_tier = None | |
| for role in user.roles: | |
| if role.name in TIER_ROLES: | |
| current_tier = role.name | |
| break | |
| if not current_tier: | |
| return await interaction.followup.send( | |
| f"❌ {user.mention} doesn't have a tier role!", | |
| ephemeral=True | |
| ) | |
| # Check if already retired for this gamemode | |
| retired_role_name = f"Retired {current_tier}" | |
| already_retired = any(r.name == retired_role_name for r in user.roles) | |
| if already_retired: | |
| return await interaction.followup.send( | |
| f"❌ {user.mention} is already retired!", | |
| ephemeral=True | |
| ) | |
| try: | |
| # Call API to update Supabase retired status | |
| import aiohttp | |
| api_url = os.getenv("API_BASE_URL", "http://localhost:5000") | |
| async with aiohttp.ClientSession() as session: | |
| payload = { | |
| "username": player_username, | |
| "gamemode": gamemode | |
| } | |
| async with session.post(f"{api_url}/retire", json=payload) as response: | |
| api_result = await response.json() | |
| if not api_result.get("success"): | |
| return await interaction.followup.send( | |
| f"❌ Failed to update database: {api_result.get('error', 'Unknown error')}", | |
| ephemeral=True | |
| ) | |
| # Remove tier role | |
| tier_role = discord.utils.get(guild.roles, name=current_tier) | |
| if tier_role: | |
| await user.remove_roles(tier_role) | |
| # Create or get retired role with color based on tier | |
| retired_role = discord.utils.get(guild.roles, name=retired_role_name) | |
| if not retired_role: | |
| tier_color = tier_colors.get(current_tier, 0x808080) | |
| retired_role = await guild.create_role( | |
| name=retired_role_name, | |
| color=discord.Color(tier_color), | |
| reason=f"Retired tier role created by {interaction.user.mention}" | |
| ) | |
| # Add retired role | |
| await user.add_roles(retired_role) | |
| await interaction.followup.send( | |
| f"✅ {user.mention} has been retired from **{gamemode}**!\n" | |
| f"**Username:** {player_username}\n" | |
| f"**Removed:** {current_tier}\n" | |
| f"**Assigned:** {retired_role_name}", | |
| ephemeral=True | |
| ) | |
| print(f"[RETIRE] ✅ {user.name} ({player_username}) retired from {gamemode} by {interaction.user.name}") | |
| except Exception as e: | |
| print(f"[RETIRE] ❌ Error retiring user: {e}") | |
| import traceback | |
| traceback.print_exc() | |
| await interaction.followup.send( | |
| f"❌ Failed to retire {user.mention}: {str(e)}", | |
| ephemeral=True | |
| ) | |
| async def high_ticket_panel(self, interaction: discord.Interaction): | |
| """Send the High Tier Test panel with Crystal button""" | |
| if not _is_member_at_least(interaction.user, "overseer"): | |
| return await interaction.response.send_message("❌ You don't have permission to use this command.", ephemeral=True) | |
| embed = discord.Embed( | |
| title="🏆 High Tier Test", | |
| description="**Elite Testing Challenge**\n\nAre you ready to prove yourself at the highest level? Click the button below to open a high tier test ticket.\n\n**Requirements:**\n• LT3 or above tier\n• Verified account\n• 14-day cooldown after completion\n\nOur expert testers will evaluate your skills comprehensively. Good luck! 🍀", | |
| color=discord.Color.gold() | |
| ) | |
| embed.set_footer(text="Only users with LT3+ tiers can participate") | |
| view = HighTierButtonView() | |
| await interaction.response.send_message(embed=embed, view=view) | |
| async def unretire_panel(self, interaction: discord.Interaction): | |
| """Send the Unretire Test panel with button""" | |
| if not _is_member_at_least(interaction.user, "owner"): | |
| return await interaction.response.send_message("❌ You don't have permission to use this command.", ephemeral=True) | |
| embed = discord.Embed( | |
| title="🔄 Unretire Program", | |
| description="**Return to Active Testing**\n\nThinking of making a comeback? This is your chance! If you've been retired, use this program to get re-evaluated and return to the testing community.\n\n**How it works:**\n• Open an unretire test ticket\n• Our testers will assess your current skills\n• If successful, your retired status will be removed\n• Get assigned to a new tier based on your performance\n\nNever give up - your comeback story starts here! 💪", | |
| color=discord.Color.blue() | |
| ) | |
| embed.set_footer(text="Available for all retired members") | |
| view = UnretireButtonView() | |
| await interaction.response.send_message(embed=embed, view=view) | |
| async def setup(bot: commands.Bot): | |
| await bot.add_cog(QueueSystem(bot)) # your main queue cog | |
| await bot.add_cog(QueueTicketSystem(bot)) # ticket commands for queue | |
| bot.add_view(HighTierButtonView()) # persistent view for high tier button | |
| bot.add_view(UnretireButtonView()) # persistent view for unretire button | |