AI
This commit is contained in:
parent
1be9732279
commit
01aa843d0b
18 changed files with 4122 additions and 2393 deletions
|
|
@ -1,255 +1,567 @@
|
|||
import json
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Discord bot for team creation, stats reporting, and event logging.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import discord
|
||||
import random
|
||||
import asyncio
|
||||
import re
|
||||
import logging
|
||||
from discord.ext import commands
|
||||
|
||||
# --- Configuration ---
|
||||
|
||||
def get_token():
|
||||
with open("config.php", "r") as file:
|
||||
content = file.read()
|
||||
match = re.search(r"bottoken\s*=\s*'(.+?)'", content)
|
||||
if match:
|
||||
return match.group(1)
|
||||
return None
|
||||
# TODO: Move hardcoded channel names to a config file or environment variables
|
||||
TEAMIFY_VOICE_CHANNEL = "teamify"
|
||||
TEAMIFY_TEXT_CHANNEL = "teamify"
|
||||
TEMP_CATEGORY_NAME = "Temporary Teams"
|
||||
LOGGING_CHANNEL = "logging"
|
||||
WELCOME_CHANNEL = "raadhuisplein"
|
||||
GOD_CHANNEL = "GOD_CHANNEL" # Channel to ignore for voice logging
|
||||
|
||||
token = get_token()
|
||||
# TODO: Move stats file path to config
|
||||
STATS_FILE_PATH = os.path.join("..", "data", "player_last_stats.json")
|
||||
CONFIG_PHP_PATH = "config.php" # Path relative to this script's location
|
||||
|
||||
# --- Logging Setup ---
|
||||
# Basic logging to console
|
||||
logging.basicConfig(level=logging.INFO, format='%(asctime)s:%(levelname)s:%(name)s: %(message)s')
|
||||
logger = logging.getLogger('discord_bot')
|
||||
|
||||
# --- Helper Functions ---
|
||||
|
||||
def get_token_from_php(config_path):
|
||||
"""
|
||||
Reads the bot token from a PHP config file.
|
||||
|
||||
WARNING: This method of storing secrets is insecure.
|
||||
Consider using environment variables or a dedicated secrets management solution.
|
||||
"""
|
||||
try:
|
||||
with open(config_path, "r", encoding="utf-8") as file:
|
||||
content = file.read()
|
||||
# Regex to find $bottoken = 'TOKEN_VALUE';
|
||||
match = re.search(r"\$bottoken\s*=\s*'(.+?)'", content)
|
||||
if match:
|
||||
logger.info("Successfully extracted bot token from %s", config_path)
|
||||
return match.group(1)
|
||||
else:
|
||||
logger.error("Bot token pattern not found in %s", config_path)
|
||||
return None
|
||||
except FileNotFoundError:
|
||||
logger.error("Config file not found at %s", config_path)
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.exception("Error reading or parsing config file %s: %s", config_path, e)
|
||||
return None
|
||||
|
||||
async def cleanup_empty_channels(guild, temp_channels, ctx_channel):
|
||||
"""
|
||||
Periodically checks and removes empty temporary voice channels.
|
||||
"""
|
||||
while temp_channels:
|
||||
await asyncio.sleep(60) # Check every 60 seconds
|
||||
for channel in temp_channels[:]: # Iterate over a copy
|
||||
try:
|
||||
# Refresh channel state
|
||||
current_channel = guild.get_channel(channel.id)
|
||||
if current_channel and isinstance(current_channel, discord.VoiceChannel):
|
||||
if len(current_channel.members) == 0:
|
||||
logger.info("Deleting empty temporary channel: %s", current_channel.name)
|
||||
await current_channel.delete(reason="Channel empty")
|
||||
temp_channels.remove(channel)
|
||||
# Optionally notify in the original context channel
|
||||
# await ctx_channel.send(f"Kanaal {channel.name} is opgeruimd omdat het leeg was!")
|
||||
elif current_channel is None:
|
||||
# Channel might have been deleted manually
|
||||
logger.warning("Temporary channel %s (ID: %d) not found, removing from tracking list.", channel.name, channel.id)
|
||||
temp_channels.remove(channel)
|
||||
|
||||
except discord.Forbidden:
|
||||
logger.error("Missing permissions to delete channel %s", channel.name)
|
||||
# Remove from list to avoid repeated attempts if permissions are missing
|
||||
temp_channels.remove(channel)
|
||||
except discord.NotFound:
|
||||
logger.warning("Attempted to delete channel %s but it was already gone.", channel.name)
|
||||
if channel in temp_channels:
|
||||
temp_channels.remove(channel)
|
||||
except Exception as e:
|
||||
logger.exception("Error during temporary channel cleanup for %s: %s", channel.name, e)
|
||||
# Remove from list to avoid potential infinite loops on persistent errors
|
||||
if channel in temp_channels:
|
||||
temp_channels.remove(channel)
|
||||
|
||||
# --- Bot Setup ---
|
||||
|
||||
token = get_token_from_php(CONFIG_PHP_PATH)
|
||||
if not token:
|
||||
raise ValueError("Bot token niet gevonden in config.php")
|
||||
logger.critical("Bot token not found or could not be read. Exiting.")
|
||||
exit() # Exit if token is essential
|
||||
|
||||
# Intents instellen
|
||||
# Define Intents
|
||||
intents = discord.Intents.default()
|
||||
intents.voice_states = True
|
||||
intents.guilds = True
|
||||
intents.messages = True
|
||||
intents.message_content = True
|
||||
intents.presences = True # Nodig als de bot presences moet zien
|
||||
intents.members = True # Nodig om leden in een voice channel te zien
|
||||
intents.presences = True # Required if the bot needs to see presences
|
||||
intents.members = True # Required to see members in voice channels and for join/remove events
|
||||
|
||||
bot = commands.Bot(command_prefix="!", intents=intents)
|
||||
|
||||
# --- Events ---
|
||||
|
||||
@bot.event
|
||||
async def on_ready():
|
||||
print(f'Bot is ingelogd als {bot.user}')
|
||||
"""Called when the bot is ready and connected."""
|
||||
logger.info('Bot is ingelogd als %s (ID: %s)', bot.user, bot.user.id)
|
||||
print(f'Bot is ingelogd als {bot.user}') # Keep console output for convenience
|
||||
|
||||
@bot.event
|
||||
async def on_command_error(ctx, error):
|
||||
"""Global command error handler."""
|
||||
if isinstance(error, commands.CommandNotFound):
|
||||
# Optionally ignore 'Command not found' errors or send a message
|
||||
# await ctx.send("Onbekend commando.")
|
||||
logger.warning("Command not found: %s", ctx.message.content)
|
||||
elif isinstance(error, commands.MissingRequiredArgument):
|
||||
await ctx.send(f"Je mist een argument: {error.param.name}")
|
||||
elif isinstance(error, commands.BadArgument):
|
||||
await ctx.send("Ongeldig argument opgegeven.")
|
||||
elif isinstance(error, commands.CheckFailure):
|
||||
await ctx.send("Je hebt geen permissies voor dit commando of kanaal.")
|
||||
else:
|
||||
# Log other errors
|
||||
logger.exception("Unhandled command error in '%s': %s", ctx.command, error)
|
||||
try:
|
||||
# Attempt to notify user about unexpected error
|
||||
await ctx.send("Er is een onverwachte fout opgetreden.")
|
||||
except discord.HTTPException:
|
||||
pass # Ignore if we can't send the error message
|
||||
|
||||
@bot.event
|
||||
async def on_voice_state_update(member, before, after):
|
||||
"""Logs user movements between voice channels."""
|
||||
# Ignore bots
|
||||
if member.bot:
|
||||
return
|
||||
|
||||
logging_channel = discord.utils.get(member.guild.text_channels, name=LOGGING_CHANNEL)
|
||||
if not logging_channel:
|
||||
logger.warning("Logging channel '%s' not found.", LOGGING_CHANNEL)
|
||||
return
|
||||
|
||||
# Ignore movements involving the GOD_CHANNEL
|
||||
if (before.channel and before.channel.name == GOD_CHANNEL) or \
|
||||
(after.channel and after.channel.name == GOD_CHANNEL):
|
||||
return
|
||||
|
||||
try:
|
||||
if before.channel is None and after.channel is not None:
|
||||
# Member joins a voice channel
|
||||
logger.info("%s joined voice channel: %s", member.display_name, after.channel.name)
|
||||
await logging_channel.send(f"🔊 {member.mention} is gejoined in voice kanaal: **{after.channel.name}**")
|
||||
elif before.channel is not None and after.channel is None:
|
||||
# Member leaves a voice channel
|
||||
logger.info("%s left voice channel: %s", member.display_name, before.channel.name)
|
||||
await logging_channel.send(f"🔇 {member.mention} heeft het voice kanaal **{before.channel.name}** verlaten.")
|
||||
elif before.channel != after.channel:
|
||||
# Member switches voice channels
|
||||
logger.info("%s switched from %s to %s", member.display_name, before.channel.name, after.channel.name)
|
||||
await logging_channel.send(f"🔄 {member.mention} is van **{before.channel.name}** naar **{after.channel.name}** gegaan.")
|
||||
except discord.Forbidden:
|
||||
logger.error("Missing permissions to send message in logging channel '%s'", LOGGING_CHANNEL)
|
||||
except Exception as e:
|
||||
logger.exception("Error during on_voice_state_update: %s", e)
|
||||
|
||||
@bot.event
|
||||
async def on_member_join(member):
|
||||
"""Welcomes new members."""
|
||||
if member.bot: return # Ignore bots
|
||||
|
||||
welcome_channel = discord.utils.get(member.guild.text_channels, name=WELCOME_CHANNEL)
|
||||
if welcome_channel:
|
||||
logger.info("Member joined: %s", member.display_name)
|
||||
try:
|
||||
await welcome_channel.send(f"🎉 Welkom {member.mention} op de server! We hopen dat je een leuke tijd hebt!")
|
||||
except discord.Forbidden:
|
||||
logger.error("Missing permissions to send message in welcome channel '%s'", WELCOME_CHANNEL)
|
||||
except Exception as e:
|
||||
logger.exception("Error during on_member_join: %s", e)
|
||||
else:
|
||||
logger.warning("Welcome channel '%s' not found.", WELCOME_CHANNEL)
|
||||
|
||||
@bot.event
|
||||
async def on_member_remove(member):
|
||||
"""Logs when members leave."""
|
||||
if member.bot: return # Ignore bots
|
||||
|
||||
goodbye_channel = discord.utils.get(member.guild.text_channels, name=WELCOME_CHANNEL) # Using same channel as welcome
|
||||
if goodbye_channel:
|
||||
logger.info("Member left: %s", member.display_name)
|
||||
try:
|
||||
await goodbye_channel.send(f"😢 {member.display_name} heeft de server verlaten. We zullen je missen!") # Use display_name as mention won't work
|
||||
except discord.Forbidden:
|
||||
logger.error("Missing permissions to send message in goodbye channel '%s'", WELCOME_CHANNEL)
|
||||
except Exception as e:
|
||||
logger.exception("Error during on_member_remove: %s", e)
|
||||
else:
|
||||
logger.warning("Goodbye channel '%s' not found.", WELCOME_CHANNEL)
|
||||
|
||||
|
||||
# --- Commands ---
|
||||
|
||||
@bot.command()
|
||||
async def test(ctx):
|
||||
"""A simple test command."""
|
||||
await ctx.send("Test geslaagd!")
|
||||
|
||||
@bot.command()
|
||||
async def teamify(ctx, *args):
|
||||
for arg in args:
|
||||
if arg.lower() == "help":
|
||||
help_message = (
|
||||
"**Gebruik van !teamify:**\n"
|
||||
"`!teamify` - Verdeel spelers in teams van max. 4 personen.\n"
|
||||
"`!teamify <aantal_teams>` - Verdeel spelers in een opgegeven aantal teams.\n"
|
||||
"`!teamify <aantal_teams> move` - Verdeel spelers en verplaats ze naar tijdelijke voice-kanalen.\n"
|
||||
"`!teamify move` - Verdeel spelers automatisch en verplaats ze naar tijdelijke voice-kanalen."
|
||||
)
|
||||
await ctx.send(help_message)
|
||||
return
|
||||
"""
|
||||
Splits members in the 'teamify' voice channel into random teams.
|
||||
|
||||
# Beperk het commando tot alleen het kanaal "teamify"
|
||||
if ctx.channel.name != "teamify":
|
||||
await ctx.send("Dit commando kan alleen worden gebruikt in het #teamify kanaal.")
|
||||
return
|
||||
|
||||
guild = ctx.guild
|
||||
voice_channel = discord.utils.get(guild.voice_channels, name="teamify")
|
||||
|
||||
if not voice_channel or len(voice_channel.members) == 0:
|
||||
await ctx.send("Er zijn geen mensen in het kanaal 'teamify' om teams van te maken!")
|
||||
return
|
||||
|
||||
members = voice_channel.members
|
||||
random.shuffle(members)
|
||||
|
||||
# Standaardwaarden
|
||||
num_teams = 0
|
||||
move_players = False
|
||||
|
||||
# Verwerk argumenten
|
||||
for arg in args:
|
||||
if arg.isdigit(): # Als het een getal is, gebruik het als het aantal teams
|
||||
num_teams = int(arg)
|
||||
elif arg.lower() == "move": # Als 'true' is opgegeven, verplaats spelers
|
||||
move_players = True
|
||||
|
||||
# Bepaal het aantal teams als niet opgegeven
|
||||
if num_teams <= 0:
|
||||
num_teams = len(members) // 4 if len(members) >= 4 else 1
|
||||
|
||||
num_teams = min(num_teams, len(members))
|
||||
teams = [[] for _ in range(num_teams)]
|
||||
for i, member in enumerate(members):
|
||||
teams[i % num_teams].append(member)
|
||||
|
||||
# Zoek het tekstkanaal "teamify"
|
||||
text_channel = discord.utils.get(guild.text_channels, name="teamify")
|
||||
if not text_channel:
|
||||
await ctx.send("Het kanaal 'teamify' bestaat niet!")
|
||||
return
|
||||
|
||||
message = f"Willekeurige teams uit {voice_channel.name}:\n\n"
|
||||
category = discord.utils.get(guild.categories, name="Temporary Teams")
|
||||
if not category:
|
||||
category = await guild.create_category("Temporary Teams")
|
||||
|
||||
temp_channels = []
|
||||
|
||||
for i, team in enumerate(teams, start=1):
|
||||
team_names = ', '.join([member.mention for member in team])
|
||||
message += f"**Team {i}:** {team_names}\n"
|
||||
|
||||
if move_players:
|
||||
temp_channel = await guild.create_voice_channel(f"Squad {i}", category=category)
|
||||
temp_channels.append(temp_channel)
|
||||
for member in team:
|
||||
await member.move_to(temp_channel)
|
||||
|
||||
await text_channel.send(message)
|
||||
await ctx.send("Teams zijn gepost in #teamify!" + (" Spelers zijn verplaatst naar tijdelijke kanalen." if move_players else ""))
|
||||
|
||||
# Controleer continu of de kanalen leeg zijn en verwijder ze
|
||||
if move_players:
|
||||
while temp_channels:
|
||||
await asyncio.sleep(60) # Controleer elke minuut
|
||||
for channel in temp_channels[:]:
|
||||
if len(channel.members) == 0:
|
||||
if channel in guild.voice_channels: # Controleer of het kanaal nog bestaat
|
||||
await channel.delete()
|
||||
temp_channels.remove(channel)
|
||||
await ctx.send(f"Kanaal {channel.name} is opgeruimd omdat het leeg was!")
|
||||
|
||||
@bot.command()
|
||||
async def whoisbest(ctx, category="Casual", matchesback=18):
|
||||
|
||||
|
||||
if category.lower() == "help":
|
||||
Usage:
|
||||
!teamify - Auto-split into teams of 4.
|
||||
!teamify <num_teams> - Split into a specific number of teams.
|
||||
!teamify move - Auto-split and move to temporary channels.
|
||||
!teamify <num_teams> move - Split into specific teams and move.
|
||||
!teamify help - Show help message.
|
||||
"""
|
||||
# Handle help argument first
|
||||
if "help" in [arg.lower() for arg in args]:
|
||||
help_message = (
|
||||
"**Gebruik van het commando `whoisbest`:**\n"
|
||||
"`!whoisbest [category] [matchesback]`\n\n"
|
||||
"**Parameters:**\n"
|
||||
"`category` - De categorie van de stats, bijv. 'Casual' of 'Ranked'. Niet hoofdlettergevoelig.\n"
|
||||
"`matchesback` - Het minimum aantal matches dat een speler gespeeld moet hebben om mee te tellen (standaard 18).\n\n"
|
||||
"**Voorbeeld:**\n"
|
||||
"`!whoisbest Casual 18`\n"
|
||||
"Laat de top 3 spelers zien op basis van winratio en gemiddelde damage in de Casual categorie met minimaal 18 matches.\n\n"
|
||||
"Typ `!whoisbest help` om deze uitleg opnieuw te zien."
|
||||
"**Gebruik van !teamify:**\n"
|
||||
f"`!teamify` - Verdeel spelers in `{TEAMIFY_VOICE_CHANNEL}` in teams van max. 4.\n"
|
||||
"`!teamify <aantal_teams>` - Verdeel spelers in een opgegeven aantal teams.\n"
|
||||
"`!teamify move` - Verdeel spelers automatisch en verplaats ze naar tijdelijke kanalen.\n"
|
||||
"`!teamify <aantal_teams> move` - Verdeel spelers en verplaats ze.\n"
|
||||
"`!teamify help` - Toon dit bericht."
|
||||
)
|
||||
await ctx.send(help_message)
|
||||
return
|
||||
|
||||
# Check if command is used in the correct text channel
|
||||
if ctx.channel.name != TEAMIFY_TEXT_CHANNEL:
|
||||
await ctx.send(f"Dit commando kan alleen worden gebruikt in het #{TEAMIFY_TEXT_CHANNEL} kanaal.")
|
||||
return
|
||||
|
||||
# Bestandspad
|
||||
file_path = os.path.join("..", "data", "player_last_stats.json")
|
||||
|
||||
guild = ctx.guild
|
||||
source_voice_channel = discord.utils.get(guild.voice_channels, name=TEAMIFY_VOICE_CHANNEL)
|
||||
|
||||
if not source_voice_channel:
|
||||
await ctx.send(f"Voice kanaal '{TEAMIFY_VOICE_CHANNEL}' niet gevonden!")
|
||||
return
|
||||
|
||||
if not source_voice_channel.members:
|
||||
await ctx.send(f"Er zijn geen mensen in het kanaal '{TEAMIFY_VOICE_CHANNEL}' om teams van te maken!")
|
||||
return
|
||||
|
||||
members = list(source_voice_channel.members) # Get a mutable list
|
||||
random.shuffle(members)
|
||||
member_count = len(members)
|
||||
|
||||
# Parse arguments
|
||||
num_teams_arg = 0
|
||||
move_players = False
|
||||
for arg in args:
|
||||
if arg.isdigit():
|
||||
num_teams_arg = int(arg)
|
||||
elif arg.lower() == "move":
|
||||
move_players = True
|
||||
|
||||
# Determine number of teams
|
||||
if num_teams_arg <= 0:
|
||||
# Auto-calculate based on team size 4, minimum 1 team
|
||||
num_teams = (member_count + 3) // 4 if member_count > 0 else 1
|
||||
else:
|
||||
num_teams = num_teams_arg
|
||||
|
||||
# Ensure num_teams is valid
|
||||
num_teams = max(1, min(num_teams, member_count)) # At least 1 team, max members count
|
||||
|
||||
# Create teams
|
||||
teams = [[] for _ in range(num_teams)]
|
||||
for i, member in enumerate(members):
|
||||
teams[i % num_teams].append(member)
|
||||
|
||||
# Find the output text channel
|
||||
output_text_channel = discord.utils.get(guild.text_channels, name=TEAMIFY_TEXT_CHANNEL)
|
||||
if not output_text_channel:
|
||||
# Fallback to context channel if specific channel not found
|
||||
logger.warning("Output text channel '%s' not found, using context channel.", TEAMIFY_TEXT_CHANNEL)
|
||||
output_text_channel = ctx.channel
|
||||
# await ctx.send(f"Tekst kanaal '{TEAMIFY_TEXT_CHANNEL}' bestaat niet! Resultaten worden hier gepost.")
|
||||
|
||||
# Prepare message and temporary channels if moving
|
||||
message = f"Willekeurige teams uit {source_voice_channel.mention}:\n\n"
|
||||
temp_channels = []
|
||||
category = None
|
||||
|
||||
if move_players:
|
||||
try:
|
||||
category = discord.utils.get(guild.categories, name=TEMP_CATEGORY_NAME)
|
||||
if not category:
|
||||
logger.info("Creating category '%s'", TEMP_CATEGORY_NAME)
|
||||
category = await guild.create_category(TEMP_CATEGORY_NAME, reason="Teamify temporary channels")
|
||||
except discord.Forbidden:
|
||||
await ctx.send(f"Ik heb geen permissies om de categorie '{TEMP_CATEGORY_NAME}' te maken.")
|
||||
move_players = False # Disable moving if category creation fails
|
||||
logger.error("Missing permissions to create category '%s'", TEMP_CATEGORY_NAME)
|
||||
except Exception as e:
|
||||
await ctx.send("Fout bij het maken/vinden van de categorie.")
|
||||
move_players = False
|
||||
logger.exception("Error finding/creating category '%s': %s", TEMP_CATEGORY_NAME, e)
|
||||
|
||||
# Build message and move players
|
||||
move_tasks = []
|
||||
for i, team in enumerate(teams, start=1):
|
||||
team_names = ', '.join([member.mention for member in team])
|
||||
message += f"**Team {i}:** {team_names}\n"
|
||||
|
||||
if move_players and category:
|
||||
try:
|
||||
channel_name = f"Squad {i}"
|
||||
logger.info("Creating temporary voice channel: %s", channel_name)
|
||||
temp_channel = await guild.create_voice_channel(channel_name, category=category, reason="Teamify command")
|
||||
temp_channels.append(temp_channel)
|
||||
for member in team:
|
||||
# Add move operation to a list to run concurrently
|
||||
move_tasks.append(member.move_to(temp_channel))
|
||||
except discord.Forbidden:
|
||||
await ctx.send(f"Ik heb geen permissies om voice kanaal 'Squad {i}' te maken of leden te verplaatsen.")
|
||||
logger.error("Missing permissions to create/move for Squad %d", i)
|
||||
# Optionally stop creating more channels if one fails
|
||||
except Exception as e:
|
||||
await ctx.send(f"Fout bij het maken van kanaal 'Squad {i}'.")
|
||||
logger.exception("Error creating channel 'Squad %d': %s", i, e)
|
||||
|
||||
# Send the team message
|
||||
try:
|
||||
# JSON-bestand lezen
|
||||
with open(file_path, "r", encoding="utf-8") as file:
|
||||
await output_text_channel.send(message)
|
||||
logger.info("Posted team message to #%s", output_text_channel.name)
|
||||
except discord.Forbidden:
|
||||
await ctx.send(f"Ik heb geen permissies om berichten te sturen in #{output_text_channel.name}.")
|
||||
logger.error("Missing permissions to send message in #%s", output_text_channel.name)
|
||||
except Exception as e:
|
||||
await ctx.send("Fout bij het versturen van het team bericht.")
|
||||
logger.exception("Error sending team message: %s", e)
|
||||
|
||||
# Execute moves concurrently
|
||||
if move_tasks:
|
||||
logger.info("Moving %d members to temporary channels...", len(move_tasks))
|
||||
results = await asyncio.gather(*move_tasks, return_exceptions=True)
|
||||
moved_count = 0
|
||||
for i, result in enumerate(results):
|
||||
member_to_move = move_tasks[i].__self__ # Get the member object from the coroutine
|
||||
if isinstance(result, Exception):
|
||||
logger.error("Failed to move member %s: %s", member_to_move.display_name, result)
|
||||
try:
|
||||
# Try to notify in context channel about failed move
|
||||
await ctx.send(f"Kon {member_to_move.mention} niet verplaatsen: {result}")
|
||||
except discord.HTTPException: pass # Ignore if sending fails
|
||||
else:
|
||||
moved_count += 1
|
||||
logger.info("Finished moving members. Successful moves: %d", moved_count)
|
||||
await ctx.send(f"Teams zijn gepost in #{output_text_channel.name}! {moved_count} speler(s) verplaatst.")
|
||||
elif move_players: # If move was intended but failed (e.g., category issue)
|
||||
await ctx.send(f"Teams zijn gepost in #{output_text_channel.name}. Kon spelers niet verplaatsen.")
|
||||
else:
|
||||
await ctx.send(f"Teams zijn gepost in #{output_text_channel.name}.")
|
||||
|
||||
|
||||
# Start background task for cleaning up empty channels if channels were created
|
||||
if temp_channels:
|
||||
logger.info("Starting background task to clean up %d temporary channels.", len(temp_channels))
|
||||
bot.loop.create_task(cleanup_empty_channels(guild, temp_channels, ctx.channel))
|
||||
|
||||
|
||||
@bot.command()
|
||||
async def whoisbest(ctx, category="Casual", matchesback: int = 18):
|
||||
"""
|
||||
Shows top 3 players by winrate and AHD for a given category.
|
||||
|
||||
Usage: !whoisbest [category] [min_matches]
|
||||
Example: !whoisbest Ranked 10
|
||||
"""
|
||||
if category.lower() == "help":
|
||||
help_message = (
|
||||
"**Gebruik van `!whoisbest`:**\n"
|
||||
"`!whoisbest [category] [min_matches]`\n\n"
|
||||
"**Parameters:**\n"
|
||||
"`category` - Stats categorie (bijv. 'Casual', 'Ranked', 'Intense'). Niet hoofdlettergevoelig. Standaard: 'Casual'.\n"
|
||||
"`min_matches` - Minimum aantal matches gespeeld. Standaard: 18.\n\n"
|
||||
"**Voorbeeld:** `!whoisbest Ranked 10`\n"
|
||||
"Toont top 3 Win% en AHD voor Ranked met minimaal 10 matches.\n"
|
||||
)
|
||||
await ctx.send(help_message)
|
||||
return
|
||||
|
||||
logger.info("Whoisbest command invoked: category=%s, matchesback=%d", category, matchesback)
|
||||
|
||||
try:
|
||||
# Read JSON file
|
||||
if not os.path.exists(STATS_FILE_PATH):
|
||||
await ctx.send(f"Statistieken bestand niet gevonden ({STATS_FILE_PATH}).")
|
||||
logger.error("Stats file not found: %s", STATS_FILE_PATH)
|
||||
return
|
||||
|
||||
with open(STATS_FILE_PATH, "r", encoding="utf-8") as file:
|
||||
data = json.load(file)
|
||||
|
||||
# Mapping maken (lowercase -> originele categorie)
|
||||
category_mapping = {cat.lower(): cat for cat in data.keys()}
|
||||
# Create case-insensitive mapping for categories
|
||||
category_mapping = {cat.lower(): cat for cat in data.keys() if isinstance(data[cat], list)} # Only map list categories
|
||||
|
||||
# Zet de opgegeven category om naar lowercase
|
||||
lower_category = category.lower()
|
||||
|
||||
if lower_category not in category_mapping:
|
||||
available_categories = ', '.join(data.keys())
|
||||
await ctx.send(f"Ongeldige categorie '{category}'! Beschikbare categorieën: {available_categories}")
|
||||
available_categories = ', '.join(category_mapping.values()) # Show original names
|
||||
await ctx.send(f"Ongeldige categorie '{category}'. Beschikbaar: {available_categories}")
|
||||
logger.warning("Invalid category requested: %s", category)
|
||||
return
|
||||
|
||||
# Gebruik de juiste (originele) categorie naam uit de mapping
|
||||
|
||||
actual_category = category_mapping[lower_category]
|
||||
logger.info("Processing category: %s", actual_category)
|
||||
|
||||
players = [player for player in data.get(actual_category, []) if player.get("matches", 0) >= matchesback]
|
||||
# Filter players based on minimum matches
|
||||
players_in_category = data.get(actual_category, [])
|
||||
if not isinstance(players_in_category, list):
|
||||
await ctx.send(f"Data voor categorie '{actual_category}' is niet in het verwachte formaat.")
|
||||
logger.error("Invalid data format for category '%s'", actual_category)
|
||||
return
|
||||
|
||||
if not players:
|
||||
await ctx.send(f"Geen spelersstatistieken gevonden voor '{actual_category}' met minimaal {matchesback} gespeelde matches!")
|
||||
filtered_players = [
|
||||
player for player in players_in_category
|
||||
if isinstance(player, dict) and player.get("matches", 0) >= matchesback
|
||||
]
|
||||
logger.info("Found %d players meeting criteria (>=%d matches) in %s", len(filtered_players), matchesback, actual_category)
|
||||
|
||||
if not filtered_players:
|
||||
await ctx.send(f"Geen spelers gevonden voor '{actual_category}' met ≥ {matchesback} matches.")
|
||||
return
|
||||
|
||||
# Sorteer spelers op winratio (aflopend)
|
||||
top_winratio = sorted(players, key=lambda x: x.get("winratio", 0), reverse=True)[:3]
|
||||
# Safely sort players, handling potential missing keys or non-numeric data
|
||||
def safe_get_float(player_dict, key, default=0.0):
|
||||
val = player_dict.get(key)
|
||||
if isinstance(val, (int, float)):
|
||||
return float(val)
|
||||
# Try converting comma decimal separator if needed
|
||||
if isinstance(val, str):
|
||||
try: return float(val.replace(',', '.'))
|
||||
except ValueError: return default
|
||||
return default
|
||||
|
||||
# Sorteer spelers op gemiddelde damage (aflopend)
|
||||
top_ahd = sorted(players, key=lambda x: x.get("ahd", 0), reverse=True)[:3]
|
||||
top_winratio = sorted(filtered_players, key=lambda x: safe_get_float(x, "winratio"), reverse=True)[:3]
|
||||
top_ahd = sorted(filtered_players, key=lambda x: safe_get_float(x, "ahd"), reverse=True)[:3]
|
||||
|
||||
# Bouw het bericht op
|
||||
message = f"**\U0001F3C6 Top 3 Winratio ({actual_category})**\n"
|
||||
for i, player in enumerate(top_winratio, start=1):
|
||||
message += f"{i}. **{player['playername']}** - {player['winratio']:.2f}%\n"
|
||||
# Build the response message
|
||||
response_message = f"**📊 Top Stats voor '{actual_category}' (min {matchesback} matches)**\n\n"
|
||||
|
||||
message += f"\n**\U0001F480 Top 3 AHD ({actual_category})**\n"
|
||||
for i, player in enumerate(top_ahd, start=1):
|
||||
message += f"{i}. **{player['playername']}** - {player['ahd']:.2f}\n"
|
||||
response_message += "**\U0001F3C6 Top 3 Winratio**\n"
|
||||
if top_winratio:
|
||||
for i, player in enumerate(top_winratio, start=1):
|
||||
player_name = player.get('playername', 'Onbekend')
|
||||
win_ratio = safe_get_float(player, 'winratio')
|
||||
response_message += f"{i}. **{player_name}** - {win_ratio:.2f}%\n"
|
||||
else:
|
||||
response_message += "_Geen data beschikbaar_\n"
|
||||
|
||||
await ctx.send(message)
|
||||
response_message += f"\n**\U0001F4A5 Top 3 Average Human Damage (AHD)**\n" # Changed emoji
|
||||
if top_ahd:
|
||||
for i, player in enumerate(top_ahd, start=1):
|
||||
player_name = player.get('playername', 'Onbekend')
|
||||
ahd_val = safe_get_float(player, 'ahd')
|
||||
response_message += f"{i}. **{player_name}** - {ahd_val:.2f}\n"
|
||||
else:
|
||||
response_message += "_Geen data beschikbaar_\n"
|
||||
|
||||
await ctx.send(response_message)
|
||||
logger.info("Successfully sent whoisbest stats for %s", actual_category)
|
||||
|
||||
except FileNotFoundError:
|
||||
await ctx.send(f"Statistieken bestand niet gevonden ({STATS_FILE_PATH}).")
|
||||
logger.error("Stats file not found: %s", STATS_FILE_PATH)
|
||||
except json.JSONDecodeError:
|
||||
await ctx.send("Fout bij het lezen van de statistieken (ongeldig JSON).")
|
||||
logger.exception("JSONDecodeError reading stats file: %s", STATS_FILE_PATH)
|
||||
except Exception as e:
|
||||
await ctx.send(f"Fout bij het laden van de statistieken: {str(e)}")
|
||||
await ctx.send(f"Onverwachte fout bij het ophalen van statistieken: {e}")
|
||||
logger.exception("Error in whoisbest command: %s", e)
|
||||
|
||||
@bot.event
|
||||
async def on_voice_state_update(member, before, after):
|
||||
logging_channel = discord.utils.get(member.guild.text_channels, name="logging")
|
||||
|
||||
if not logging_channel:
|
||||
return
|
||||
if (before.channel and before.channel.name == "GOD_CHANNEL") or (after.channel and after.channel.name == "GOD_CHANNEL"):
|
||||
return # Als het God_channel is, doe niks
|
||||
|
||||
if before.channel is None and after.channel is not None:
|
||||
# Lid joint een voice channel
|
||||
await logging_channel.send(f"🔊 {member.mention} is gejoined in voice kanaal: **{after.channel.name}**")
|
||||
elif before.channel is not None and after.channel is None:
|
||||
# Lid verlaat een voice channel
|
||||
await logging_channel.send(f"🔇 {member.mention} heeft het voice kanaal **{before.channel.name}** verlaten.")
|
||||
elif before.channel != after.channel:
|
||||
# Lid switched van voice kanaal
|
||||
await logging_channel.send(f"🔄 {member.mention} is van **{before.channel.name}** naar **{after.channel.name}** gegaan.")
|
||||
|
||||
@bot.event
|
||||
async def on_member_join(member):
|
||||
logging_channel = discord.utils.get(member.guild.text_channels, name="raadhuisplein")
|
||||
if logging_channel:
|
||||
await logging_channel.send(f"🎉 Welkom {member.mention} op de server! We hopen dat je een leuke tijd hebt!")
|
||||
|
||||
@bot.event
|
||||
async def on_member_remove(member):
|
||||
logging_channel = discord.utils.get(member.guild.text_channels, name="raadhuisplein")
|
||||
if logging_channel:
|
||||
await logging_channel.send(f"😢 {member.name} heeft de server verlaten. We zullen je missen!")
|
||||
|
||||
@bot.command()
|
||||
@commands.has_permissions(move_members=True) # Add permission check
|
||||
async def moveall(ctx):
|
||||
# Controleer of het commando in het juiste kanaal wordt uitgevoerd
|
||||
if ctx.channel.name != "teamify":
|
||||
await ctx.send("Dit commando kan alleen worden gebruikt in het #teamify tekstkanaal.")
|
||||
"""Moves all members from other voice channels to the 'teamify' channel."""
|
||||
# Check if command is used in the correct text channel
|
||||
if ctx.channel.name != TEAMIFY_TEXT_CHANNEL:
|
||||
await ctx.send(f"Dit commando kan alleen worden gebruikt in het #{TEAMIFY_TEXT_CHANNEL} tekstkanaal.")
|
||||
return
|
||||
|
||||
|
||||
guild = ctx.guild
|
||||
teamify_channel = discord.utils.get(guild.voice_channels, name="teamify")
|
||||
|
||||
if not teamify_channel:
|
||||
await ctx.send("Het teamify voice-kanaal bestaat niet!")
|
||||
target_channel = discord.utils.get(guild.voice_channels, name=TEAMIFY_VOICE_CHANNEL)
|
||||
|
||||
if not target_channel:
|
||||
await ctx.send(f"Het '{TEAMIFY_VOICE_CHANNEL}' voice-kanaal bestaat niet!")
|
||||
logger.error("Target voice channel '%s' not found for moveall.", TEAMIFY_VOICE_CHANNEL)
|
||||
return
|
||||
|
||||
moved_members = 0
|
||||
|
||||
moved_members_count = 0
|
||||
move_tasks = []
|
||||
members_to_move = []
|
||||
|
||||
logger.info("Moveall command initiated by %s", ctx.author.display_name)
|
||||
for channel in guild.voice_channels:
|
||||
if channel != teamify_channel:
|
||||
for member in channel.members:
|
||||
try:
|
||||
await member.move_to(teamify_channel)
|
||||
moved_members += 1
|
||||
except Exception as e:
|
||||
await ctx.send(f"Kon {member.mention} niet verplaatsen: {e}")
|
||||
|
||||
if moved_members > 0:
|
||||
await ctx.send(f"{moved_members} speler(s) zijn verplaatst naar het teamify kanaal.")
|
||||
# Skip the target channel itself and channels with no members
|
||||
if channel == target_channel or not channel.members:
|
||||
continue
|
||||
|
||||
for member in channel.members:
|
||||
# Avoid moving bots unless intended
|
||||
# if member.bot: continue
|
||||
members_to_move.append(member)
|
||||
move_tasks.append(member.move_to(target_channel))
|
||||
|
||||
if not move_tasks:
|
||||
await ctx.send("Er waren geen spelers in andere kanalen om te verplaatsen.")
|
||||
logger.info("Moveall: No members found in other channels.")
|
||||
return
|
||||
|
||||
logger.info("Attempting to move %d members to #%s", len(move_tasks), target_channel.name)
|
||||
results = await asyncio.gather(*move_tasks, return_exceptions=True)
|
||||
|
||||
for i, result in enumerate(results):
|
||||
member = members_to_move[i]
|
||||
if isinstance(result, Exception):
|
||||
logger.error("Failed to move member %s: %s", member.display_name, result)
|
||||
try:
|
||||
await ctx.send(f"Kon {member.mention} niet verplaatsen: {result}")
|
||||
except discord.HTTPException: pass
|
||||
else:
|
||||
moved_members_count += 1
|
||||
|
||||
logger.info("Moveall finished. Successfully moved %d members.", moved_members_count)
|
||||
if moved_members_count > 0:
|
||||
await ctx.send(f"{moved_members_count} speler(s) zijn verplaatst naar het {target_channel.mention} kanaal.")
|
||||
else:
|
||||
await ctx.send("Er waren geen spelers om te verplaatsen.")
|
||||
|
||||
bot.run(token)
|
||||
await ctx.send("Kon geen spelers verplaatsen (controleer permissies?).")
|
||||
|
||||
|
||||
@moveall.error
|
||||
async def moveall_error(ctx, error):
|
||||
"""Error handler specific to moveall command."""
|
||||
if isinstance(error, commands.MissingPermissions):
|
||||
await ctx.send("Je hebt geen permissies om leden te verplaatsen.")
|
||||
else:
|
||||
# Log and notify for other errors related to moveall
|
||||
logger.exception("Error in moveall command: %s", error)
|
||||
await ctx.send("Er is een fout opgetreden bij het uitvoeren van `moveall`.")
|
||||
|
||||
|
||||
# --- Run Bot ---
|
||||
if __name__ == "__main__":
|
||||
logger.info("Starting bot...")
|
||||
bot.run(token)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue