discord-bot / cogs /queue_system.py
TanmayGarg2009's picture
Upload 64 files
a804436 verified
Raw
History Blame Contribute Delete
150 kB
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)
@discord.ui.button(
label="Enter Waitlist",
style=discord.ButtonStyle.primary,
emoji="📝",
custom_id="enter_waitlist_button"
)
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
@discord.ui.button(
label="Enter Queue",
style=discord.ButtonStyle.success,
emoji="🎮",
custom_id="enter_queue_button"
)
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
@discord.ui.button(
label="Confirm Close",
style=discord.ButtonStyle.danger,
custom_id="confirm_close_queue"
)
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)
@discord.ui.button(
label="Cancel",
style=discord.ButtonStyle.secondary,
custom_id="cancel_close_queue"
)
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)
@discord.ui.button(
label="crystal",
style=discord.ButtonStyle.success,
custom_id="open_high_tier_ticket"
)
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)
@discord.ui.button(
label="Start Unretire Process",
style=discord.ButtonStyle.primary,
custom_id="open_unretire_ticket"
)
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
)
@app_commands.command(name="openqueue", description="Open a queue for testing")
@app_commands.describe(region="Region: AS, NA, ME, EU, or AU")
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")
@app_commands.command(name="closequeue", description="Close a queue for testing")
@app_commands.describe(region="Region to close queue for (AS/NA/ME/EU/AU)")
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")
@app_commands.command(name="pickfromqueue", description="Pick the next person from the queue")
@app_commands.describe(region="Region to pick from (AS/NA/ME/EU/AU)")
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")
@app_commands.command(name="queuesystemsetup", description="Set up the queue system channels and roles")
@app_commands.describe(request_channel="Channel to use for the request-a-test panel")
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)
@app_commands.command(name="leave", description="Leave the active queue")
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
)
@app_commands.command(name="clearwaitlist", description="Remove a user from queue (optional temp ban)")
@app_commands.describe(
user="User to remove",
region="Region (AS/NA/ME/EU/AU)",
days="Temporarily ban from joining (0-30 days, leave empty for no ban)"
)
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
)
@app_commands.command(name="clearcooldown", description="Clear the 7-day cooldown for a user")
@app_commands.describe(user="Member to clear cooldown for")
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)
@app_commands.command(name="queuestatus", description="Check queue status for all regions")
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)
@app_commands.command(name="log", description="Show queue openings for the last N days (default 7)")
@app_commands.describe(days="Days to look back (optional, default 7)")
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)
@app_commands.command(name="setuproles", description="Set up staff roles with vibrant colors and hierarchy")
@app_commands.describe(
network_owner="Network Owner role (optional)",
manager="Manager role (optional)",
overseer="Overseer role (optional)",
administrator="Administrator role (optional)",
regulator="Regulator role (optional)",
network_mod="Network Mod role (optional)",
senior_mod="Senior Mod role (optional)",
screensharer="Screensharer role (optional)",
discord_mod="Discord Mod role (optional)",
discord_helper="Discord Helper role (optional)",
senior_tester="Senior Tester role (optional)",
verified_tester="Verified Tester role (optional)",
staff_team="Staff Team role (optional)"
)
@app_commands.guild_only()
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.")
@app_commands.command(name="add", description="Add a user to this ticket")
@app_commands.guild_only()
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}.")
@app_commands.command(name="remove", description="Remove a user from this ticket")
@app_commands.guild_only()
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}.")
@app_commands.command(name="close", description="Close the current ticket with tier assignment")
@app_commands.describe(tier="Tier to assign to the tested player")
@app_commands.choices(tier=TIER_CHOICES)
@app_commands.guild_only()
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())
@app_commands.command(name="passevel", description="Move queue ticket to high test category (for worthy candidates)")
@app_commands.guild_only()
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)
@app_commands.command(name="addrole", description="Force add a role to the ticket with tester permissions")
@app_commands.guild_only()
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)
@app_commands.command(name="addrandom", description="Add an online user with the given role, or assign randomly if none online.")
@app_commands.guild_only()
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)
@app_commands.command(name="retire", description="Retire a user (vanilla only)")
@app_commands.describe(user="User to retire")
@app_commands.guild_only()
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
)
@app_commands.command(name="highticketpanel", description="Display the High Tier Test panel")
@app_commands.guild_only()
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)
@app_commands.command(name="unretire_pannel", description="Display the Unretire Test panel")
@app_commands.guild_only()
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