From 6692c431026295b1771775dc0980b1f371793c92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Math=C3=A9o=20Nebulo9=20DESSAUVAGES?= Date: Thu, 16 Jul 2026 13:11:29 +0200 Subject: [PATCH] feat: Add intern feature, assignment to a mentor every week, follows him/her in voice channel when they join/move or disconnect --- .dockerignore | 4 ++ .gitignore | 4 +- cogs/insult.py | 4 +- cogs/intern.py | 149 +++++++++++++++++++++++++++++++++++++++++++++++ main.py | 1 - requirements.txt | 10 +++- setup/bot.py | 11 ++-- setup/config.py | 2 + 8 files changed, 174 insertions(+), 11 deletions(-) create mode 100644 .dockerignore create mode 100644 cogs/intern.py diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..f062639 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,4 @@ +.gitea/ +__pycache__/ +.vscode +tests/ \ No newline at end of file diff --git a/.gitignore b/.gitignore index b6e1cad..4c4e301 100644 --- a/.gitignore +++ b/.gitignore @@ -164,4 +164,6 @@ data/ *.disabled -config/ \ No newline at end of file +config/ + +tests/ \ No newline at end of file diff --git a/cogs/insult.py b/cogs/insult.py index c73ec48..ecec83d 100644 --- a/cogs/insult.py +++ b/cogs/insult.py @@ -1,11 +1,11 @@ import discord, json, os, random, re import collections -from discord.ext import commands +from discord.ext import commands, bridge from setup.logger import LOGGER from setup.config import Config class InsultsCog(commands.Cog): - def __init__(self, bot:discord.Bot): + def __init__(self, bot:bridge.Bot): self.bot = bot with open(os.path.join(Config.CONFIG_PATH, "insults.txt")) as f: diff --git a/cogs/intern.py b/cogs/intern.py new file mode 100644 index 0000000..783d199 --- /dev/null +++ b/cogs/intern.py @@ -0,0 +1,149 @@ +import discord, json, os, random, re, datetime +import collections +from discord.ext import commands, bridge, tasks +from setup.logger import LOGGER +from setup.config import Config + +class InternCog(commands.Cog): + def __init__(self, bot:commands.Bot) -> None: + self.bot = bot + self.in_voice_channel = False + self.connected_guild_ids = set() + + def _get_guild_data(self, guild_id): + return next((data for data in self.data if data["guild_id"] == guild_id), None) + + def _get_guild_voice_client(self, guild_id): + return next( + ( + voice_client + for voice_client in self.bot.voice_clients + if getattr(getattr(voice_client, "guild", None), "id", None) == guild_id + ), + None, + ) + + def _resolve_voice_action(self, before_channel, after_channel, guild_voice_client): + if after_channel is None and before_channel is not None: + return "disconnect" + + if before_channel is not None and after_channel is not None: + if before_channel.id != after_channel.id: + return "move" + return None + + if after_channel is not None and before_channel is None: + if guild_voice_client is not None: + return "move" + return "connect" if random.random() <= 0.25 else None + + return None + + def load_from_file(self): + try: + with open(Config.INTERN_MEMORY_PATH) as f: + self.data = json.load(f) + if len(self.data) == 0: raise FileNotFoundError + for guild_data in self.data: + guild = self.bot.get_guild(guild_data["guild_id"]) + if guild: + mentor = guild.get_member(guild_data["mentor_id"]) + LOGGER.debug(f"Loaded mentor: {mentor.name} for guild {guild.name}") + except FileNotFoundError: + today = datetime.datetime.now() + next_roll = datetime.datetime(year=today.year,month=today.month,day=today.day) + datetime.timedelta(days=7) + self.data = [ + { + "mentor_id": random.choice(guild.members).id, + "guild_id": guild.id, + "next_roll": next_roll.isoformat() + } + for guild in self.bot.guilds + ] + + def load_from_database(self): + pass + + def save_to_file(self): + with open(Config.INTERN_MEMORY_PATH, 'w') as f: + json.dump(self.data,f) + + def save_to_database(self): + pass + + @tasks.loop(hours=24) + async def update(self): + today = datetime.datetime.now() + for guild_data in self.data: + next_roll = datetime.datetime.fromisoformat(guild_data["next_roll"]) + if today > next_roll: + guild = self.bot.get_guild(guild_data["guild_id"]) + if guild: + members = [ + member + for member in guild.get_role(1516232292576788530).members + if member not in guild.get_role(1526911990495445143).members + and member.id != guild_data["mentor_id"] + ] + new_mentor = random.choice(members) + new_mentor_id = new_mentor.id + guild_data["mentor_id"] = new_mentor_id + new_next_roll = datetime.datetime(year=today.year,month=today.month,day=today.day) + datetime.timedelta(days=7) + guild_data["next_roll"] = new_next_roll.isoformat() + LOGGER.debug(f"New mentor: {new_mentor.name} for guild {guild.name}") + self.save_to_file() + + @commands.Cog.listener() + async def on_ready(self): + self.load_from_file() + self.update.start() + + @commands.Cog.listener() + async def on_voice_state_update(self, member:discord.Member, before:discord.VoiceState, after:discord.VoiceState): + if not self.data: + return + + guild_id = None + if before and before.channel and before.channel.guild: + guild_id = before.channel.guild.id + elif after and after.channel and after.channel.guild: + guild_id = after.channel.guild.id + + if guild_id is None: + return + + guild_data = self._get_guild_data(guild_id) + if not guild_data or member.id != guild_data["mentor_id"]: + return + + if before and before.channel: + channel_name = getattr(before.channel, "name", "unknown") + LOGGER.debug(f"Mentor left channel {channel_name}") + + if after and after.channel: + channel_name = getattr(after.channel, "name", "unknown") + LOGGER.debug(f"Mentor joined channel {channel_name}") + + guild_voice_client = self._get_guild_voice_client(guild_id) + + action = self._resolve_voice_action(before.channel if before else None, after.channel if after else None, guild_voice_client) + + if action == "connect": + try: + await after.channel.connect() + self.connected_guild_ids.add(guild_id) + self.in_voice_channel = True + except discord.ClientException as exc: + LOGGER.warning(f"Could not connect to voice channel: {exc}") + elif action == "move": + if guild_voice_client: + await guild_voice_client.move_to(after.channel) + else: + await after.channel.connect() + self.connected_guild_ids.add(guild_id) + self.in_voice_channel = True + elif action == "disconnect": + if guild_voice_client: + await guild_voice_client.disconnect(force=True) + self.connected_guild_ids.discard(guild_id) + self.in_voice_channel = False \ No newline at end of file diff --git a/main.py b/main.py index 8d0f5b9..081d999 100644 --- a/main.py +++ b/main.py @@ -2,7 +2,6 @@ import os, discord, json, sys, random from argparse import ArgumentParser from datetime import datetime from asyncio import create_task -from discord import option from discord.ext import tasks, commands from setup.bot import bot, reload_feature, register_cogs from setup.logger import LOGGER diff --git a/requirements.txt b/requirements.txt index 956c7f9..cffcf9f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,10 +1,13 @@ -aiohappyeyeballs==2.6.2 +aiohappyeyeballs==2.7.1 aiohttp==3.14.1 aiosignal==1.4.0 asarPy==1.0.1 +async-timeout==5.0.1 attrs==26.1.0 +audioop-lts==0.2.2 beautifulsoup4==4.15.0 certifi==2026.5.20 +cffi==2.1.0 charset-normalizer==3.4.7 frozenlist==1.8.0 h11==0.16.0 @@ -15,6 +18,8 @@ outcome==1.3.0.post0 propcache==0.5.2 psycopg2-binary==2.9.12 py-cord==2.8.0 +pycparser==3.0 +PyNaCl==1.6.2 PySocks==1.7.1 python-dotenv==1.2.2 requests==2.34.2 @@ -24,8 +29,9 @@ sortedcontainers==2.4.0 soupsieve==2.8.4 trio==0.33.0 trio-websocket==0.12.2 -typing_extensions==4.15.0 +typing_extensions==4.16.0 urllib3==2.7.0 +wavelink==3.5.2 websocket-client==1.9.0 wsproto==1.3.2 yarl==1.24.2 diff --git a/setup/bot.py b/setup/bot.py index bb0612d..83b3e68 100644 --- a/setup/bot.py +++ b/setup/bot.py @@ -1,10 +1,9 @@ import discord -from discord.ext import bridge -# from cogs.birthday import BirthdayCog -# from cogs.productivity import ProductivityCog +from discord.ext import commands from cogs.cs import CSCog from cogs.insult import InsultsCog from cogs.roulette import RouletteCog +from cogs.intern import InternCog from setup.logger import LOGGER bot_intents = discord.Intents.default() @@ -13,13 +12,15 @@ bot_intents.members = True bot_intents.presences = True bot_intents.guilds = True bot_intents.reactions = True +bot_intents.voice_states = True -bot = bridge.Bot(command_prefix='$', intents=bot_intents) +bot = commands.Bot(command_prefix='$', intents=bot_intents) COG_REGISTRY = { 'cs': CSCog, 'insult': InsultsCog, - 'roulette': RouletteCog + 'roulette': RouletteCog, + 'intern': InternCog } diff --git a/setup/config.py b/setup/config.py index 2678ff2..cebacc6 100644 --- a/setup/config.py +++ b/setup/config.py @@ -7,6 +7,8 @@ class Config: TOKEN = str(os.getenv("TOKEN", "")) CONFIG_PATH = str(os.getenv("CONFIG_PATH")) + INTERN_MEMORY_PATH = os.path.join(CONFIG_PATH,"intern.json") + INTERN_TAKES_PATH = os.path.join(CONFIG_PATH,"intern_takes.csv") COGS = os.getenv("COGS", "") @classmethod