510 lines
19 KiB
Python
510 lines
19 KiB
Python
import json
|
||
import os
|
||
import discord
|
||
import random
|
||
import asyncio
|
||
import re
|
||
from textwrap import dedent
|
||
from discord.ext import commands
|
||
from openai import OpenAI
|
||
|
||
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
|
||
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
|
||
|
||
token = get_token()
|
||
if not token:
|
||
raise ValueError("Bot token niet gevonden in config.php")
|
||
|
||
# Intents instellen
|
||
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
|
||
|
||
bot = commands.Bot(command_prefix="!", intents=intents)
|
||
client = OpenAI(api_key=OPENAI_API_KEY)
|
||
@bot.event
|
||
async def on_ready():
|
||
print(f'Bot is ingelogd als {bot.user}')
|
||
channel = bot.get_channel(759006368832159745)
|
||
if channel:
|
||
await channel.send("Ben er weer!")
|
||
|
||
@bot.command()
|
||
async def test(ctx):
|
||
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
|
||
|
||
# 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, top=3):
|
||
|
||
|
||
if category.lower() == "help":
|
||
help_message = (
|
||
"**Gebruik van het commando `whoisbest`:**\n"
|
||
"`!whoisbest [category] [matchesback] [top]`\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"
|
||
"Typ `!whoisbest help` om deze uitleg opnieuw te zien."
|
||
)
|
||
await ctx.send(help_message)
|
||
return
|
||
|
||
|
||
# Bestandspad
|
||
file_path = os.path.join("..", "data", "player_last_stats.json")
|
||
|
||
try:
|
||
# JSON-bestand lezen
|
||
with open(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()}
|
||
|
||
# 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}")
|
||
return
|
||
|
||
# Gebruik de juiste (originele) categorie naam uit de mapping
|
||
actual_category = category_mapping[lower_category]
|
||
|
||
players = [player for player in data.get(actual_category, []) if player.get("matches", 0) >= matchesback]
|
||
|
||
if not players:
|
||
await ctx.send(f"Geen spelersstatistieken gevonden voor '{actual_category}' met minimaal {matchesback} gespeelde matches!")
|
||
return
|
||
|
||
# Sorteer spelers op winratio (aflopend)
|
||
top_winratio = sorted(players, key=lambda x: x.get("winratio", 0), reverse=True)[:top]
|
||
|
||
# Sorteer spelers op gemiddelde damage (aflopend)
|
||
top_ahd = sorted(players, key=lambda x: x.get("ahd", 0), reverse=True)[:top]
|
||
|
||
# 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"
|
||
|
||
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"
|
||
##AI
|
||
|
||
system_prompt = dedent(f"""
|
||
Je bent een Discord announcer-bot op de PUBG-server van DTCH.
|
||
Stijl: brutaal/competitief, licht denigrerend maar leesbaar.
|
||
AHD = Average Human Damage
|
||
Regels:
|
||
- Gebruik uitsluitend de meegeleverde stats-tekst.
|
||
- Output ALLEEN Discord-markdown (geen JSON, geen codeblokken).
|
||
- Structuur:
|
||
1) Titel met category en korte snedige ondertitel.
|
||
2) **🏆 Top {top} Winratio** en **💀 Top {top} AHD** (exact die koppen).
|
||
3) Per regel: 🥇/🥈/🥉 + **naam** + waarde (winratio met %).
|
||
4) De rest van de regels doe je zonder medaille gewoon een cijfer
|
||
5) Sluit af met 1 of 2 regels analyse van de stats, gebruik humor.
|
||
- Opgegeven parameters:
|
||
Categorie: {category}
|
||
Minimaal aantal matches: {matchesback}
|
||
Top: {top}
|
||
- verdere info
|
||
1) Als de aantal matches laag is (onder de 15) dan zijn cijfers niet echt meer representatief. Meld dat dan ook.
|
||
- Max ~1800 tekens.
|
||
""").strip()
|
||
user_prompt = dedent(f"""
|
||
Verpak onderstaande stats-tekst in één strakke Discord-post volgens de regels.
|
||
Wijzig geen waardes, haal alles uit de tekst tussen START/EINDE.
|
||
|
||
[STATS-TEKST START]
|
||
{message}
|
||
[STATS-TEKST EINDE]
|
||
""").strip()
|
||
response = client.chat.completions.create(
|
||
model="gpt-5-nano",
|
||
#temperature=0.6,
|
||
#presence_penalty=0.2,
|
||
messages=[
|
||
{"role": "system", "content": system_prompt},
|
||
{"role": "user", "content": user_prompt},
|
||
],
|
||
)
|
||
|
||
antwoord = response.choices[0].message.content
|
||
await ctx.send(f"{ctx.author.mention} {antwoord[:1900]}")
|
||
except Exception as e:
|
||
await ctx.send(f"{ctx.author.mention} Er ging iets mis: {e}")
|
||
|
||
|
||
##AIEND
|
||
|
||
|
||
@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:
|
||
welcome_message = (
|
||
f"🎉 Welcome {member.mention} to **{member.guild.name}**!\n\n"
|
||
"👋 We're glad to have you here.\n"
|
||
"👉 Want to jump right in? Type `!iamgamer` in the chat and you'll get the **Tourists** role.\n"
|
||
"With that role you can join the fun and games with everyone else. 🎮\n"
|
||
"Use !dtch_help for more info\n\n"
|
||
"Enjoy your stay and have a great time! 🚀"
|
||
)
|
||
await logging_channel.send(welcome_message)
|
||
|
||
@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()
|
||
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.")
|
||
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!")
|
||
return
|
||
|
||
moved_members = 0
|
||
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.")
|
||
else:
|
||
await ctx.send("Er waren geen spelers om te verplaatsen.")
|
||
@bot.command()
|
||
async def iamgamer(ctx):
|
||
role = discord.utils.get(ctx.guild.roles, name="Tourists")
|
||
if role is None:
|
||
await ctx.send("De rol **Tourists** bestaat niet!")
|
||
return
|
||
|
||
try:
|
||
await ctx.author.add_roles(role)
|
||
await ctx.send(f"✅ {ctx.author.mention}, je bent nu een **Tourist**! Veel plezier! 🎮")
|
||
except Exception as e:
|
||
await ctx.send(f"Er is iets misgegaan bij het toekennen van de rol: {e}")
|
||
@bot.command(name="dtch_help", aliases=["commands"])
|
||
async def dtch_help_command(ctx):
|
||
embed = discord.Embed(
|
||
title="📖 DTCH Bot Command Help",
|
||
description="Here’s a list of all available commands and how to use them:",
|
||
color=discord.Color.blue()
|
||
)
|
||
|
||
embed.add_field(
|
||
name="👉 !teamify",
|
||
value=(
|
||
"`!teamify` - Split players into random teams of max 4.\n"
|
||
"`!teamify <number_of_teams>` - Split players into a given number of teams.\n"
|
||
"`!teamify <number_of_teams> move` - Split & move players into temporary voice channels.\n"
|
||
"`!teamify move` - Auto split & move players.\n"
|
||
"🔹 Works only in the **#teamify** channel."
|
||
),
|
||
inline=False
|
||
)
|
||
|
||
embed.add_field(
|
||
name="👉 !moveall",
|
||
value="`!moveall` - Moves all players from other voice channels into the **teamify** channel.",
|
||
inline=False
|
||
)
|
||
|
||
embed.add_field(
|
||
name="👉 !whoisbest",
|
||
value=(
|
||
"`!whoisbest [category] [matchesback]`\n"
|
||
"Shows the top 3 players based on win ratio and average damage.\n"
|
||
"`category` = e.g. Casual, Ranked\n"
|
||
"`matchesback` = minimum matches required (default: 18)\n"
|
||
"Example: `!whoisbest Casual 18`"
|
||
),
|
||
inline=False
|
||
)
|
||
|
||
embed.add_field(
|
||
name="👉 !iamgamer",
|
||
value="`!iamgamer` - Gives you the **Tourists** role 🎮 so you can join games and unlock more fun.",
|
||
inline=False
|
||
)
|
||
|
||
embed.set_footer(text="✨ For advanced options, use: !teamify help or !whoisbest help")
|
||
|
||
await ctx.send(embed=embed)
|
||
|
||
@bot.command()
|
||
@commands.cooldown(1, 15, commands.BucketType.user)
|
||
async def ask(ctx, *, vraag: str):
|
||
## CLAN MEMBERS
|
||
file_path = os.path.join("..", "config", "clanmembers.json")
|
||
# Inlezen als string
|
||
with open(file_path, "r", encoding="utf-8") as f:
|
||
clanmembers_str = f.read()
|
||
|
||
#LIFETIME STATS
|
||
|
||
file_path_lifetimestats = os.path.join("..", "data", "player_lifetime_data.json")
|
||
with open(file_path_lifetimestats, "r", encoding="utf-8") as file:
|
||
data_lifetimestats = json.load(file)
|
||
squad_str = json.dumps(data_lifetimestats.get("squad", {}), indent=2)
|
||
|
||
#Last stats
|
||
file_path_laststats = os.path.join("..", "data", "player_last_stats.json")
|
||
with open(file_path_laststats, "r", encoding="utf-8") as file:
|
||
data_laststat = json.load(file)
|
||
casual_str = json.dumps(data_laststat.get("Casual", []), indent=2)
|
||
ranked_str = json.dumps(data_laststat.get("Ranked", []), indent=2)
|
||
custom_str = json.dumps(data_laststat.get("custom", []), indent=2)
|
||
# Online members
|
||
voice_members = [
|
||
member.display_name
|
||
for vc in ctx.guild.voice_channels
|
||
for member in vc.members
|
||
]
|
||
|
||
voice_str = ", ".join(voice_members) if voice_members else "Niemand zit in een voice kanaal"
|
||
|
||
"""Stuur een vraag naar OpenAI"""
|
||
try:
|
||
async with ctx.typing():
|
||
response = client.chat.completions.create(
|
||
model="gpt-5-nano",
|
||
messages=[
|
||
{
|
||
"role": "system",
|
||
"content": (
|
||
"Je bent een licht denigrerende chatbot in Discord, actief op de PUBG server van clan DTCH. "
|
||
f"De vraag is gesteld door {ctx.author.display_name}. "
|
||
|
||
# Context
|
||
f"Clanleden: {clanmembers_str}. "
|
||
f"Momenteel in voice: {voice_str}. "
|
||
f"Lifetime stats (squad): {squad_str}. "
|
||
f"Casual (spelen we toch wel het vaakst met elkaar, belangrijke stat dus) stats: {casual_str}. "
|
||
f"Ranked stats: {ranked_str}. "
|
||
f"Custom stats (clan games meestal tegen elkaar): {custom_str}. "
|
||
|
||
# Richtlijnen voor antwoord
|
||
"Houd je antwoorden kort en bondig, met maximaal 1600 tekens. "
|
||
"Kill Death Human (KD_H) en winrato zijn de belangrijkste stats omdat bots heel makkelijk te killen zijn vinden de Human Kill death het belangrijkste"
|
||
"Gebruik Discord markdown (``` of **vetgedrukt**) wanneer je stats of tabellen toont. "
|
||
"Wees een beetje sarcastisch, maar wel begrijpelijk. "
|
||
"Stel geen vragen want je hebt geen context bij de volgende prompt."
|
||
|
||
)
|
||
},
|
||
{"role": "user", "content": vraag},
|
||
],
|
||
)
|
||
|
||
antwoord = response.choices[0].message.content
|
||
await ctx.send(f"{ctx.author.mention} {antwoord[:1900]}")
|
||
except Exception as e:
|
||
await ctx.send(f"{ctx.author.mention} Er ging iets mis: {e}")
|
||
|
||
@ask.error
|
||
async def ask_error(ctx, error):
|
||
if isinstance(error, commands.CommandOnCooldown):
|
||
retry_after = int(error.retry_after + 0.999)
|
||
await ctx.reply(f"Rustig {ctx.author.display_name}, probeer het over {retry_after}s nog eens.")
|
||
else:
|
||
raise error
|
||
|
||
@bot.command()
|
||
async def loterij(ctx, *members: discord.Member):
|
||
"""
|
||
Gebruik:
|
||
!loterij @naam1 @naam2 @naam3
|
||
"""
|
||
# Alleen in #teamify? -> uncomment als je dat ook wilt
|
||
# if ctx.channel.name != "teamify":
|
||
# await ctx.send("Dit commando kan alleen worden gebruikt in het #teamify kanaal.")
|
||
# return
|
||
|
||
if not members or len(members) < 2:
|
||
await ctx.send("Gebruik: `!loterij @naam1 @naam2 ...` (minimaal 2 mensen, anders is het wel héél zielig).")
|
||
return
|
||
|
||
# Uniek maken (als iemand 2x getagd wordt telt 'ie maar 1x mee)
|
||
unique_members = []
|
||
seen_ids = set()
|
||
for m in members:
|
||
if m.id not in seen_ids:
|
||
unique_members.append(m)
|
||
seen_ids.add(m.id)
|
||
|
||
# Tekst “tussen A, B en C” netjes bouwen
|
||
mentions = [m.mention for m in unique_members]
|
||
if len(mentions) == 2:
|
||
between_text = f"{mentions[0]} en {mentions[1]}"
|
||
else:
|
||
between_text = f"{', '.join(mentions[:-1])} en {mentions[-1]}"
|
||
|
||
winnaar = random.choice(unique_members)
|
||
|
||
await ctx.send(f"En de loterij gaat tussen {between_text}.")
|
||
|
||
# Countdown
|
||
await asyncio.sleep(1)
|
||
await ctx.send("in 3")
|
||
await asyncio.sleep(1)
|
||
await ctx.send("2")
|
||
await asyncio.sleep(1)
|
||
await ctx.send("1")
|
||
await asyncio.sleep(1)
|
||
|
||
await ctx.send("trom gerofel 🥁")
|
||
await asyncio.sleep(1)
|
||
|
||
await ctx.send(f"De winnaar is {winnaar.mention} 🎆🎇")
|
||
|
||
@loterij.error
|
||
async def loterij_error(ctx, error):
|
||
if isinstance(error, commands.BadArgument):
|
||
await ctx.send(
|
||
"Ik snap er niks van 🤨\n"
|
||
"Gebruik het zo:\n"
|
||
"`!loterij @naam1 @naam2 @naam3`\n"
|
||
"Zorg dat je **echte Discord-mentions** gebruikt."
|
||
)
|
||
else:
|
||
raise error
|
||
|
||
|
||
bot.run(token)
|