From 0e91672420537e9ea358655c369b8889d1c34008 Mon Sep 17 00:00:00 2001 From: Antonio Rodriguez <102554647+antoniorodr@users.noreply.github.com> Date: Sat, 9 May 2026 17:24:02 +0200 Subject: [PATCH 01/24] style: add tree sync to fetch the slash commands --- launcher.py | 86 +++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 64 insertions(+), 22 deletions(-) diff --git a/launcher.py b/launcher.py index ca61b05..3174e59 100644 --- a/launcher.py +++ b/launcher.py @@ -39,9 +39,14 @@ async def _create_direct_connection(self, *args, **kwargs): try: return await super()._create_direct_connection(*args, **kwargs) except ClientConnectorDNSError as e: - sleep_delay = (self.retry_delay + retry)**self.retry_exponent - logger.warning("DNS error occurred: %s. Retrying in %.2f seconds (attempt %d/%d)", - e, sleep_delay, retry + 1, self.retry_count) + sleep_delay = (self.retry_delay + retry) ** self.retry_exponent + logger.warning( + "DNS error occurred: %s. Retrying in %.2f seconds (attempt %d/%d)", + e, + sleep_delay, + retry + 1, + self.retry_count, + ) await asyncio.sleep(sleep_delay) continue @@ -60,12 +65,9 @@ def __init__(self): members=True, messages=True, message_content=True, - presences=True - ) - mentions = discord.AllowedMentions( - everyone=False, - replied_user=False + presences=True, ) + mentions = discord.AllowedMentions(everyone=False, replied_user=False) super().__init__( allowed_mentions=mentions, command_prefix=_get_prefix, @@ -88,19 +90,34 @@ async def on_ready(self): if not hasattr(self, "appinfo"): self.appinfo = await self.application_info() - self.logger.info("Logged in as: %s in %s servers.", self.user.name, len(self.guilds)) + self.logger.info( + "Logged in as: %s in %s servers.", self.user.name, len(self.guilds) + ) self.logger.info("DiscordPY: %s", discord.__version__) self.logger.debug("Bot Ready;Prefixes: %s", ", ".join(settings.prefix)) async def setup_hook(self): - extensions = ["cogs.norprog", "cogs.misc", "cogs.poeng", "cogs.bokmerker", - "cogs.errors", "cogs.github", "cogs.broder", "cogs.workplace"] + extensions = [ + "cogs.norprog", + "cogs.misc", + "cogs.poeng", + "cogs.bokmerker", + "cogs.errors", + "cogs.github", + "cogs.broder", + "cogs.workplace", + "cogs.birthday", + ] for extension in extensions: try: self.logger.debug("Loading extension %s", extension) await self.load_extension(extension) except Exception as _e: - self.logger.exception("Loading of extension %s failed: %s", extension, _e) + self.logger.exception( + "Loading of extension %s failed: %s", extension, _e + ) + + await self.tree.sync() async def close(self): self.logger.info("Logging out") @@ -114,17 +131,36 @@ async def main(token): async with Bot() as bot: await bot.start(token) + if __name__ == "__main__": - parser = ArgumentParser(prog="Roxedus' ProgBott", - description="Programmeringsbot for Norsk programmering", - formatter_class=RawTextHelpFormatter) + parser = ArgumentParser( + prog="Roxedus' ProgBott", + description="Programmeringsbot for Norsk programmering", + formatter_class=RawTextHelpFormatter, + ) parser.add_argument("-D", "--debug", action="store_true", help="Sets debug to true") - parser.add_argument("-l", "--level", help="Sets debug level", - choices=["critical", "error", "warning", "info", "debug"], default="warning") - parser.add_argument("-d", "--data-directory", - help="Define an alternate data directory location", default="data", type=str) - parser.add_argument("-f", "--log-to-file", action="store_true", help="Save log to file", default=True) + parser.add_argument( + "-l", + "--level", + help="Sets debug level", + choices=["critical", "error", "warning", "info", "debug"], + default="warning", + ) + parser.add_argument( + "-d", + "--data-directory", + help="Define an alternate data directory location", + default="data", + type=str, + ) + parser.add_argument( + "-f", + "--log-to-file", + action="store_true", + help="Save log to file", + default=True, + ) args = parser.parse_args() @@ -134,9 +170,15 @@ async def main(token): if args.debug: LEVEL = "DEBUG" - settings = Settings(data_dir=data_dir, log_level=LEVEL, log_to_file=args.log_to_file) + settings = Settings( + data_dir=data_dir, log_level=LEVEL, log_to_file=args.log_to_file + ) - logger = Logger(location=settings.data_dir, level=settings.log_level, to_file=settings.log_to_file).logger + logger = Logger( + location=settings.data_dir, + level=settings.log_level, + to_file=settings.log_to_file, + ).logger logger.debug("Data folder: %s", settings.data_dir) # pylint: disable=arguments-differ From b6fab7d6c083322f27bd2d073fd787d02eb445f1 Mon Sep 17 00:00:00 2001 From: Antonio Rodriguez <102554647+antoniorodr@users.noreply.github.com> Date: Sat, 9 May 2026 17:25:22 +0200 Subject: [PATCH 02/24] feat: create birthday feature This creates a slash command `/bursdag` which accepts a date in the format `DD.MM`. This will be stores in a json file and checked daily. If a match is found, a message will be send to the chosen channel. --- cogs/birthday.py | 116 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 116 insertions(+) create mode 100644 cogs/birthday.py diff --git a/cogs/birthday.py b/cogs/birthday.py new file mode 100644 index 0000000..61c8a13 --- /dev/null +++ b/cogs/birthday.py @@ -0,0 +1,116 @@ +""" +Modul for bursdagsfunksjonalitet +""" + +import discord +from discord.ext import commands, tasks +import json +import os +from datetime import datetime, time +from zoneinfo import ZoneInfo + +NORWEGIAN_TIME = ZoneInfo("Europe/Oslo") +BIRTHDAY_CHECK_TIME = time(hour=17, minute=21, tzinfo=NORWEGIAN_TIME) + + +class Birthday(commands.Cog): + """ + Klasse for bursdagsfunksjonalitet + """ + + def __init__(self, bot): + self.bot = bot + self.channel_id = 1502653422682378242 + self.birthdays_file = bot.data_dir + "/birthday/birthdays.json" + self.data_cache = self.load_existing_data() + self.check_todays_birthday.start() + + @discord.app_commands.command(name="bursdag", description="Legg til bursdag") + @discord.app_commands.describe(date="Bursdag i format DD.MM") + async def add_birthday(self, interaction: discord.Interaction, date: str): + """ + Funksjon som lagrer bursdag + """ + try: + day, month = date.split(".") + day = int(day) + month = int(month) + + if not (1 <= day <= 31 and 1 <= month <= 12): + raise ValueError + + except (ValueError, IndexError): + return await interaction.response.send_message( + "Feil format! Bruk `DD.MM`(f.eks. 24.12)", ephemeral=True + ) + + user = interaction.user + user_id = str(interaction.user.id) + data = self.load_existing_data() + data[user_id] = {"username": interaction.user.name, "birthday": date} + self.data_cache = data + self.save_data(data) + + await interaction.response.send_message( + f"Lagret bursdag for {user.mention}: {date}" + ) + + def load_existing_data(self): + data = {} + if os.path.exists(self.birthdays_file): + with open(self.birthdays_file, "r") as file: + data = json.load(file) + return data + + def save_data(self, data): + os.makedirs(os.path.dirname(self.birthdays_file), exist_ok=True) + with open(self.birthdays_file, "w") as file: + json.dump(data, file, indent=4) + + @tasks.loop(time=BIRTHDAY_CHECK_TIME) + async def check_todays_birthday(self): + """ + Funksjon som sjekker om noen har bursdag en gang om dagen på et bestemt tidspunkt + """ + + today = datetime.now() + day = today.day + month = today.month + + matches = [] + + for user_id, data in self.data_cache.items(): + try: + b_day, b_month = map(int, data["birthday"].split(".")) + + if b_day == day and b_month == month: + guild = self.bot.guilds[0] + member = guild.get_member(int(user_id)) + + if member: + matches.append(member.mention) + else: + matches.append(data["username"]) + + except (ValueError, KeyError): + continue + + if matches: + birthday_list = "\n".join([f"🎂 {name}" for name in matches]) + embed = discord.Embed( + title="🎉 Dagens bursdagsbarn! 🎉", + description=birthday_list, + color=discord.Color.gold(), + ) + embed.set_footer(text="Gratulerer med dagen!") + channel = self.bot.get_channel(self.channel_id) + if channel: + await channel.send(embed=embed) + + @check_todays_birthday.before_loop + async def before_check(self): + await self.bot.wait_until_ready() + + +async def setup(bot): + await bot.add_cog(Birthday(bot)) From 4f7790f5f4813964a5f4cb70f6ec4edfa3287142 Mon Sep 17 00:00:00 2001 From: Antonio Rodriguez <102554647+antoniorodr@users.noreply.github.com> Date: Sat, 9 May 2026 17:29:50 +0200 Subject: [PATCH 03/24] refactor: change the time of the check --- cogs/birthday.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cogs/birthday.py b/cogs/birthday.py index 61c8a13..89fcc42 100644 --- a/cogs/birthday.py +++ b/cogs/birthday.py @@ -10,7 +10,7 @@ from zoneinfo import ZoneInfo NORWEGIAN_TIME = ZoneInfo("Europe/Oslo") -BIRTHDAY_CHECK_TIME = time(hour=17, minute=21, tzinfo=NORWEGIAN_TIME) +BIRTHDAY_CHECK_TIME = time(hour=8, minute=0, tzinfo=NORWEGIAN_TIME) class Birthday(commands.Cog): From f7acddc8d2719a870dc19431f53ed02f32a4dc97 Mon Sep 17 00:00:00 2001 From: Antonio Rodriguez <102554647+antoniorodr@users.noreply.github.com> Date: Sun, 10 May 2026 09:57:53 +0200 Subject: [PATCH 04/24] feat(birthday): create on_member_remove action When a user leaves or is kicked from the server, the birthday information is deleted --- cogs/birthday.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/cogs/birthday.py b/cogs/birthday.py index 89fcc42..53d62e5 100644 --- a/cogs/birthday.py +++ b/cogs/birthday.py @@ -52,7 +52,7 @@ async def add_birthday(self, interaction: discord.Interaction, date: str): self.save_data(data) await interaction.response.send_message( - f"Lagret bursdag for {user.mention}: {date}" + f"Lagret bursdag for {user.mention}: {date} \nDette blir slettet når serveren forlates." ) def load_existing_data(self): @@ -107,6 +107,18 @@ async def check_todays_birthday(self): if channel: await channel.send(embed=embed) + @commands.Cog.listener() + async def on_member_remove(self, member): + """ + Fjerner bursdagsdata når en bruker forlater serveren + """ + user_id = str(member.id) + + if user_id in self.data_cache: + del self.data_cache[user_id] + self.save_data(self.data_cache) + self.bot.logger.debug(f"Birthday from {member.name} deletred") + @check_todays_birthday.before_loop async def before_check(self): await self.bot.wait_until_ready() From 3dfcc2fa384a3b6f71a22ad2fc8df5409934867c Mon Sep 17 00:00:00 2001 From: Antonio Rodriguez <102554647+antoniorodr@users.noreply.github.com> Date: Mon, 11 May 2026 23:03:04 +0200 Subject: [PATCH 05/24] fix: fix typo --- cogs/birthday.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cogs/birthday.py b/cogs/birthday.py index 53d62e5..7c598e4 100644 --- a/cogs/birthday.py +++ b/cogs/birthday.py @@ -117,7 +117,7 @@ async def on_member_remove(self, member): if user_id in self.data_cache: del self.data_cache[user_id] self.save_data(self.data_cache) - self.bot.logger.debug(f"Birthday from {member.name} deletred") + self.bot.logger.debug(f"Birthday from {member.name} deleted") @check_todays_birthday.before_loop async def before_check(self): From e0529555a0acc4491deb99b09df13a57a655fe3c Mon Sep 17 00:00:00 2001 From: Antonio Rodriguez <102554647+antoniorodr@users.noreply.github.com> Date: Tue, 12 May 2026 09:33:02 +0200 Subject: [PATCH 06/24] feat: auto role assign when birthday The role will be removed when the birthday is over --- cogs/birthday.py | 33 +++++++++++++++++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/cogs/birthday.py b/cogs/birthday.py index 7c598e4..636d426 100644 --- a/cogs/birthday.py +++ b/cogs/birthday.py @@ -10,7 +10,8 @@ from zoneinfo import ZoneInfo NORWEGIAN_TIME = ZoneInfo("Europe/Oslo") -BIRTHDAY_CHECK_TIME = time(hour=8, minute=0, tzinfo=NORWEGIAN_TIME) +BIRTHDAY_CHECK_TIME = time(hour=8, minute=00, tzinfo=NORWEGIAN_TIME) +BIRTHDAY_ROLE_TIME = time(hour=0, minute=0, tzinfo=NORWEGIAN_TIME) class Birthday(commands.Cog): @@ -21,9 +22,11 @@ class Birthday(commands.Cog): def __init__(self, bot): self.bot = bot self.channel_id = 1502653422682378242 + self.birthday_role_name = "årsdag" self.birthdays_file = bot.data_dir + "/birthday/birthdays.json" self.data_cache = self.load_existing_data() self.check_todays_birthday.start() + self.remove_birthday_roles.start() @discord.app_commands.command(name="bursdag", description="Legg til bursdag") @discord.app_commands.describe(date="Bursdag i format DD.MM") @@ -89,6 +92,11 @@ async def check_todays_birthday(self): if member: matches.append(member.mention) + role = discord.utils.get( + guild.roles, name=self.birthday_role_name + ) + if role: + await member.add_roles(role) else: matches.append(data["username"]) @@ -107,6 +115,26 @@ async def check_todays_birthday(self): if channel: await channel.send(embed=embed) + @tasks.loop(time=BIRTHDAY_ROLE_TIME) + async def remove_birthday_roles(self): + """ + Fjerner bursdagsrollen fra alle brukere hver dag ved midnatt + """ + guild = self.bot.guilds[0] + role = discord.utils.get(guild.roles, name=self.birthday_role_name) + if not role: + return + + for user_id in self.data_cache: + member = guild.get_member(int(user_id)) + if member and role in member.roles: + try: + await member.remove_roles(role) + except discord.Forbidden as e: + self.bot.logger.error( + f"Failed to remove 'årsdag' role from {member.name}: {e}" + ) + @commands.Cog.listener() async def on_member_remove(self, member): """ @@ -120,7 +148,8 @@ async def on_member_remove(self, member): self.bot.logger.debug(f"Birthday from {member.name} deleted") @check_todays_birthday.before_loop - async def before_check(self): + @remove_birthday_roles.before_loop + async def before_task(self): await self.bot.wait_until_ready() From f54db5dd9f92775e9faf70f6a5bd1c4f0781db1e Mon Sep 17 00:00:00 2001 From: Antonio Rodriguez <102554647+antoniorodr@users.noreply.github.com> Date: Sat, 20 Jun 2026 16:01:19 +0200 Subject: [PATCH 07/24] chore: update `gitignore` file to ignore ruff config --- .gitignore | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 05f5d15..7564f4c 100644 --- a/.gitignore +++ b/.gitignore @@ -200,4 +200,8 @@ fabric.properties # Android studio 3.1+ serialized cache file .idea/caches/build_file_checksums.ser -db/*.sqlite \ No newline at end of file +db/*.sqlite + +# Ruff +ruff.toml +.ruff_cache/ \ No newline at end of file From 5d27fa8c04295c3fea6a1d13a5b4e22eb09a4c93 Mon Sep 17 00:00:00 2001 From: Antonio Rodriguez <102554647+antoniorodr@users.noreply.github.com> Date: Sat, 20 Jun 2026 16:14:11 +0200 Subject: [PATCH 08/24] style: avoid aggresive linting, making ruff follow `setup.cfg` rules --- cogs/birthday.py | 41 ++++++++++++++++++++++++----------------- 1 file changed, 24 insertions(+), 17 deletions(-) diff --git a/cogs/birthday.py b/cogs/birthday.py index 636d426..8432ef8 100644 --- a/cogs/birthday.py +++ b/cogs/birthday.py @@ -2,13 +2,14 @@ Modul for bursdagsfunksjonalitet """ -import discord -from discord.ext import commands, tasks import json import os from datetime import datetime, time from zoneinfo import ZoneInfo +import discord +from discord.ext import commands, tasks + NORWEGIAN_TIME = ZoneInfo("Europe/Oslo") BIRTHDAY_CHECK_TIME = time(hour=8, minute=00, tzinfo=NORWEGIAN_TIME) BIRTHDAY_ROLE_TIME = time(hour=0, minute=0, tzinfo=NORWEGIAN_TIME) @@ -43,16 +44,26 @@ async def add_birthday(self, interaction: discord.Interaction, date: str): raise ValueError except (ValueError, IndexError): - return await interaction.response.send_message( - "Feil format! Bruk `DD.MM`(f.eks. 24.12)", ephemeral=True - ) + return await interaction.response.send_message("Feil format! Bruk `DD.MM`(f.eks. 24.12)", ephemeral=True) user = interaction.user - user_id = str(interaction.user.id) + today = datetime.now() data = self.load_existing_data() - data[user_id] = {"username": interaction.user.name, "birthday": date} - self.data_cache = data - self.save_data(data) + user_id = str(interaction.user.id) + + if user_id not in data: + data[user_id] = {"username": interaction.user.name, "birthday": date, "updated": today.isoformat()} + self.data_cache = data + self.save_data(data) + else: + last_updated = datetime.fromisoformat(data[user_id].get("updated")) + if (today - last_updated).days < 180: + return await interaction.response.send_message( + "Du kan bare endre bursdagen din hver 6 måned. Prøv igjen senere.", ephemeral=True + ) + data[user_id] = {"username": interaction.user.name, "birthday": date, "updated": today.isoformat()} + self.data_cache = data + self.save_data(data) await interaction.response.send_message( f"Lagret bursdag for {user.mention}: {date} \nDette blir slettet når serveren forlates." @@ -92,9 +103,7 @@ async def check_todays_birthday(self): if member: matches.append(member.mention) - role = discord.utils.get( - guild.roles, name=self.birthday_role_name - ) + role = discord.utils.get(guild.roles, name=self.settings_file.get("birthday_role_name")) if role: await member.add_roles(role) else: @@ -111,7 +120,7 @@ async def check_todays_birthday(self): color=discord.Color.gold(), ) embed.set_footer(text="Gratulerer med dagen!") - channel = self.bot.get_channel(self.channel_id) + channel = self.bot.get_channel(self.settings_file.get("channel_id")) if channel: await channel.send(embed=embed) @@ -121,7 +130,7 @@ async def remove_birthday_roles(self): Fjerner bursdagsrollen fra alle brukere hver dag ved midnatt """ guild = self.bot.guilds[0] - role = discord.utils.get(guild.roles, name=self.birthday_role_name) + role = discord.utils.get(guild.roles, name=self.settings_file.get("birthday_role_name")) if not role: return @@ -131,9 +140,7 @@ async def remove_birthday_roles(self): try: await member.remove_roles(role) except discord.Forbidden as e: - self.bot.logger.error( - f"Failed to remove 'årsdag' role from {member.name}: {e}" - ) + self.bot.logger.error(f"Failed to remove 'årsdag' role from {member.name}: {e}") @commands.Cog.listener() async def on_member_remove(self, member): From f94141d0520f9359c7dbefde1f6ff8196c413c65 Mon Sep 17 00:00:00 2001 From: Antonio Rodriguez <102554647+antoniorodr@users.noreply.github.com> Date: Sat, 20 Jun 2026 16:15:46 +0200 Subject: [PATCH 09/24] refactor: use try/except block to check if user exists If user does exists, check if the birthday was updated over 6 months ago before update it again --- cogs/birthday.py | 40 +++++++++++++++++++++++----------------- 1 file changed, 23 insertions(+), 17 deletions(-) diff --git a/cogs/birthday.py b/cogs/birthday.py index 8432ef8..df0ff1e 100644 --- a/cogs/birthday.py +++ b/cogs/birthday.py @@ -51,23 +51,29 @@ async def add_birthday(self, interaction: discord.Interaction, date: str): data = self.load_existing_data() user_id = str(interaction.user.id) - if user_id not in data: - data[user_id] = {"username": interaction.user.name, "birthday": date, "updated": today.isoformat()} - self.data_cache = data - self.save_data(data) - else: - last_updated = datetime.fromisoformat(data[user_id].get("updated")) - if (today - last_updated).days < 180: - return await interaction.response.send_message( - "Du kan bare endre bursdagen din hver 6 måned. Prøv igjen senere.", ephemeral=True - ) - data[user_id] = {"username": interaction.user.name, "birthday": date, "updated": today.isoformat()} - self.data_cache = data - self.save_data(data) - - await interaction.response.send_message( - f"Lagret bursdag for {user.mention}: {date} \nDette blir slettet når serveren forlates." - ) + try: + if user_id not in data: + data[user_id] = {"username": interaction.user.name, "birthday": date, "updated": today.isoformat()} + self.data_cache = data + self.save_data(data) + else: + last_updated_str = data[user_id].get("updated") + if last_updated_str: + last_updated = datetime.fromisoformat(last_updated_str) + if (today - last_updated).days < 180: + return await interaction.response.send_message( + "Du kan bare endre bursdagen din hver 6 måned. Prøv igjen senere.", ephemeral=True + ) + data[user_id] = {"username": interaction.user.name, "birthday": date, "updated": today.isoformat()} + self.data_cache = data + self.save_data(data) + + await interaction.response.send_message( + f"Lagret bursdag for {user.mention}: {date} \nDette blir slettet når serveren forlates." + ) + except Exception as e: + self.bot.logger.error(f"Error in add_birthday: {e}") + await interaction.response.send_message("En feil oppstod. Prøv igjen senere.", ephemeral=True) def load_existing_data(self): data = {} From 1de21f4efd5f357f276296870ef8e830acaf0a4b Mon Sep 17 00:00:00 2001 From: Antonio Rodriguez <102554647+antoniorodr@users.noreply.github.com> Date: Sat, 20 Jun 2026 16:17:39 +0200 Subject: [PATCH 10/24] refactor: move bot config to json file --- cogs/birthday.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/cogs/birthday.py b/cogs/birthday.py index df0ff1e..d4d76b0 100644 --- a/cogs/birthday.py +++ b/cogs/birthday.py @@ -22,8 +22,7 @@ class Birthday(commands.Cog): def __init__(self, bot): self.bot = bot - self.channel_id = 1502653422682378242 - self.birthday_role_name = "årsdag" + self.settings_file = bot.data_dir + "/birthday/innstilinger.json" self.birthdays_file = bot.data_dir + "/birthday/birthdays.json" self.data_cache = self.load_existing_data() self.check_todays_birthday.start() From b36f1510a0316c1365b78cb4b479fb5d4c5d6f44 Mon Sep 17 00:00:00 2001 From: Antonio Rodriguez <102554647+antoniorodr@users.noreply.github.com> Date: Sun, 21 Jun 2026 18:55:33 +0200 Subject: [PATCH 11/24] style: fix format --- launcher.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/launcher.py b/launcher.py index 3174e59..e711e1b 100644 --- a/launcher.py +++ b/launcher.py @@ -90,9 +90,7 @@ async def on_ready(self): if not hasattr(self, "appinfo"): self.appinfo = await self.application_info() - self.logger.info( - "Logged in as: %s in %s servers.", self.user.name, len(self.guilds) - ) + self.logger.info("Logged in as: %s in %s servers.", self.user.name, len(self.guilds)) self.logger.info("DiscordPY: %s", discord.__version__) self.logger.debug("Bot Ready;Prefixes: %s", ", ".join(settings.prefix)) @@ -113,9 +111,7 @@ async def setup_hook(self): self.logger.debug("Loading extension %s", extension) await self.load_extension(extension) except Exception as _e: - self.logger.exception( - "Loading of extension %s failed: %s", extension, _e - ) + self.logger.exception("Loading of extension %s failed: %s", extension, _e) await self.tree.sync() @@ -170,9 +166,7 @@ async def main(token): if args.debug: LEVEL = "DEBUG" - settings = Settings( - data_dir=data_dir, log_level=LEVEL, log_to_file=args.log_to_file - ) + settings = Settings(data_dir=data_dir, log_level=LEVEL, log_to_file=args.log_to_file) logger = Logger( location=settings.data_dir, From 21218ffea1f71c5cd00748fe2b237612dc797f3d Mon Sep 17 00:00:00 2001 From: Antonio Rodriguez <102554647+antoniorodr@users.noreply.github.com> Date: Fri, 26 Jun 2026 12:44:52 +0200 Subject: [PATCH 12/24] style(launcher): fix linting --- launcher.py | 74 ++++++++++++++--------------------------------------- 1 file changed, 19 insertions(+), 55 deletions(-) diff --git a/launcher.py b/launcher.py index e711e1b..9624106 100644 --- a/launcher.py +++ b/launcher.py @@ -39,14 +39,9 @@ async def _create_direct_connection(self, *args, **kwargs): try: return await super()._create_direct_connection(*args, **kwargs) except ClientConnectorDNSError as e: - sleep_delay = (self.retry_delay + retry) ** self.retry_exponent - logger.warning( - "DNS error occurred: %s. Retrying in %.2f seconds (attempt %d/%d)", - e, - sleep_delay, - retry + 1, - self.retry_count, - ) + sleep_delay = (self.retry_delay + retry)**self.retry_exponent + logger.warning("DNS error occurred: %s. Retrying in %.2f seconds (attempt %d/%d)", + e, sleep_delay, retry + 1, self.retry_count) await asyncio.sleep(sleep_delay) continue @@ -65,9 +60,12 @@ def __init__(self): members=True, messages=True, message_content=True, - presences=True, + presences=True + ) + mentions = discord.AllowedMentions( + everyone=False, + replied_user=False ) - mentions = discord.AllowedMentions(everyone=False, replied_user=False) super().__init__( allowed_mentions=mentions, command_prefix=_get_prefix, @@ -95,17 +93,8 @@ async def on_ready(self): self.logger.debug("Bot Ready;Prefixes: %s", ", ".join(settings.prefix)) async def setup_hook(self): - extensions = [ - "cogs.norprog", - "cogs.misc", - "cogs.poeng", - "cogs.bokmerker", - "cogs.errors", - "cogs.github", - "cogs.broder", - "cogs.workplace", - "cogs.birthday", - ] + extensions = ["cogs.norprog", "cogs.misc", "cogs.poeng", "cogs.bokmerker", + "cogs.errors", "cogs.github", "cogs.broder", "cogs.workplace", "cogs.birthday"] for extension in extensions: try: self.logger.debug("Loading extension %s", extension) @@ -113,8 +102,6 @@ async def setup_hook(self): except Exception as _e: self.logger.exception("Loading of extension %s failed: %s", extension, _e) - await self.tree.sync() - async def close(self): self.logger.info("Logging out") await super().close() @@ -127,36 +114,17 @@ async def main(token): async with Bot() as bot: await bot.start(token) - if __name__ == "__main__": - parser = ArgumentParser( - prog="Roxedus' ProgBott", - description="Programmeringsbot for Norsk programmering", - formatter_class=RawTextHelpFormatter, - ) + parser = ArgumentParser(prog="Roxedus' ProgBott", + description="Programmeringsbot for Norsk programmering", + formatter_class=RawTextHelpFormatter) parser.add_argument("-D", "--debug", action="store_true", help="Sets debug to true") - parser.add_argument( - "-l", - "--level", - help="Sets debug level", - choices=["critical", "error", "warning", "info", "debug"], - default="warning", - ) - parser.add_argument( - "-d", - "--data-directory", - help="Define an alternate data directory location", - default="data", - type=str, - ) - parser.add_argument( - "-f", - "--log-to-file", - action="store_true", - help="Save log to file", - default=True, - ) + parser.add_argument("-l", "--level", help="Sets debug level", + choices=["critical", "error", "warning", "info", "debug"], default="warning") + parser.add_argument("-d", "--data-directory", + help="Define an alternate data directory location", default="data", type=str) + parser.add_argument("-f", "--log-to-file", action="store_true", help="Save log to file", default=True) args = parser.parse_args() @@ -168,11 +136,7 @@ async def main(token): settings = Settings(data_dir=data_dir, log_level=LEVEL, log_to_file=args.log_to_file) - logger = Logger( - location=settings.data_dir, - level=settings.log_level, - to_file=settings.log_to_file, - ).logger + logger = Logger(location=settings.data_dir, level=settings.log_level, to_file=settings.log_to_file).logger logger.debug("Data folder: %s", settings.data_dir) # pylint: disable=arguments-differ From 287802f32d99367ea47a1ba46551138ad36942c3 Mon Sep 17 00:00:00 2001 From: Antonio Rodriguez <102554647+antoniorodr@users.noreply.github.com> Date: Fri, 26 Jun 2026 13:00:32 +0200 Subject: [PATCH 13/24] fix: add back tree sync --- launcher.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/launcher.py b/launcher.py index 9624106..119296e 100644 --- a/launcher.py +++ b/launcher.py @@ -102,6 +102,8 @@ async def setup_hook(self): except Exception as _e: self.logger.exception("Loading of extension %s failed: %s", extension, _e) + await self.tree.sync() + async def close(self): self.logger.info("Logging out") await super().close() From 84ce2f01eefbc5aa3be18e04289f2092e373597e Mon Sep 17 00:00:00 2001 From: Antonio Rodriguez <102554647+antoniorodr@users.noreply.github.com> Date: Sat, 27 Jun 2026 16:28:09 +0200 Subject: [PATCH 14/24] feat: create `check_files` function to create default config at start --- cogs/birthday.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/cogs/birthday.py b/cogs/birthday.py index d4d76b0..20673bd 100644 --- a/cogs/birthday.py +++ b/cogs/birthday.py @@ -165,5 +165,24 @@ async def before_task(self): await self.bot.wait_until_ready() +def check_files(bot): + files = [ + { + f"{bot.data_dir}/birthday/innstilinger.json": { + "guild": 1502653421877199008, + "channel_id": 1502653422682378242, + "birthday_role_name": "årsdag", + } + }, + {f"{bot.data_dir}/birthday/birthdays.json": {}}, + ] + for i in files: + for file, default in i.items(): + if not os.path.exists(file): + with open(file, "w", encoding="utf8") as outfile: + json.dump(default, outfile) + + async def setup(bot): + check_files(bot) await bot.add_cog(Birthday(bot)) From 51cbb48f8fab2a0c4ecaa4b4ad1d85049670aeba Mon Sep 17 00:00:00 2001 From: Antonio Rodriguez <102554647+antoniorodr@users.noreply.github.com> Date: Sat, 27 Jun 2026 16:29:29 +0200 Subject: [PATCH 15/24] feat: create `load_config` and save it as cache --- cogs/birthday.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/cogs/birthday.py b/cogs/birthday.py index 20673bd..2189ec3 100644 --- a/cogs/birthday.py +++ b/cogs/birthday.py @@ -24,6 +24,7 @@ def __init__(self, bot): self.bot = bot self.settings_file = bot.data_dir + "/birthday/innstilinger.json" self.birthdays_file = bot.data_dir + "/birthday/birthdays.json" + self.settings = self.load_settings() self.data_cache = self.load_existing_data() self.check_todays_birthday.start() self.remove_birthday_roles.start() @@ -81,6 +82,12 @@ def load_existing_data(self): data = json.load(file) return data + def load_settings(self): + if os.path.exists(self.settings_file): + with open(self.settings_file, "r") as file: + return json.load(file) + return {} + def save_data(self, data): os.makedirs(os.path.dirname(self.birthdays_file), exist_ok=True) with open(self.birthdays_file, "w") as file: From 0d69e9199cee0067966816e9b01739281dee1eb6 Mon Sep 17 00:00:00 2001 From: Antonio Rodriguez <102554647+antoniorodr@users.noreply.github.com> Date: Sat, 27 Jun 2026 16:31:44 +0200 Subject: [PATCH 16/24] fix: search `guild_id` between all the bot guilds to find a match --- cogs/birthday.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/cogs/birthday.py b/cogs/birthday.py index 2189ec3..8c6845a 100644 --- a/cogs/birthday.py +++ b/cogs/birthday.py @@ -110,7 +110,13 @@ async def check_todays_birthday(self): b_day, b_month = map(int, data["birthday"].split(".")) if b_day == day and b_month == month: - guild = self.bot.guilds[0] + try: + guild_id = self.settings.get("guild") + guild = self.bot.get_guild(guild_id) + except AttributeError: + self.bot.logger.error("Guild not found") + return + member = guild.get_member(int(user_id)) if member: From ddd31e1b4c8081f003606e15cadee210e3a86a12 Mon Sep 17 00:00:00 2001 From: Antonio Rodriguez <102554647+antoniorodr@users.noreply.github.com> Date: Sat, 27 Jun 2026 16:32:20 +0200 Subject: [PATCH 17/24] fix: `AttributeError: 'str' object has no attribute 'get'` bug --- cogs/birthday.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cogs/birthday.py b/cogs/birthday.py index 8c6845a..96656e6 100644 --- a/cogs/birthday.py +++ b/cogs/birthday.py @@ -121,14 +121,14 @@ async def check_todays_birthday(self): if member: matches.append(member.mention) - role = discord.utils.get(guild.roles, name=self.settings_file.get("birthday_role_name")) + role = discord.utils.get(guild.roles, name=self.settings.get("birthday_role_name")) if role: await member.add_roles(role) else: matches.append(data["username"]) except (ValueError, KeyError): - continue + self.bot.logger.error(f"Invalid birthday format for user {user_id}: {data.get('birthday')}") if matches: birthday_list = "\n".join([f"🎂 {name}" for name in matches]) @@ -138,7 +138,7 @@ async def check_todays_birthday(self): color=discord.Color.gold(), ) embed.set_footer(text="Gratulerer med dagen!") - channel = self.bot.get_channel(self.settings_file.get("channel_id")) + channel = self.bot.get_channel(self.settings.get("channel_id")) if channel: await channel.send(embed=embed) @@ -148,7 +148,7 @@ async def remove_birthday_roles(self): Fjerner bursdagsrollen fra alle brukere hver dag ved midnatt """ guild = self.bot.guilds[0] - role = discord.utils.get(guild.roles, name=self.settings_file.get("birthday_role_name")) + role = discord.utils.get(guild.roles, name=self.settings.get("birthday_role_name")) if not role: return From 375a39c5d6f421562544d02372a3233dd6d34883 Mon Sep 17 00:00:00 2001 From: Antonio Rodriguez <102554647+antoniorodr@users.noreply.github.com> Date: Sat, 27 Jun 2026 23:00:31 +0200 Subject: [PATCH 18/24] refactor: creates configs esqueleton at start --- cogs/birthday.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cogs/birthday.py b/cogs/birthday.py index 96656e6..951b3ca 100644 --- a/cogs/birthday.py +++ b/cogs/birthday.py @@ -182,9 +182,9 @@ def check_files(bot): files = [ { f"{bot.data_dir}/birthday/innstilinger.json": { - "guild": 1502653421877199008, - "channel_id": 1502653422682378242, - "birthday_role_name": "årsdag", + "guild": "", + "channel_id": "", + "birthday_role_name": "", } }, {f"{bot.data_dir}/birthday/birthdays.json": {}}, From 01f16fd26c119ed74535042e12b07c871e472e14 Mon Sep 17 00:00:00 2001 From: Antonio Rodriguez <102554647+antoniorodr@users.noreply.github.com> Date: Sat, 27 Jun 2026 23:01:05 +0200 Subject: [PATCH 19/24] feat: create function to save settings in json --- cogs/birthday.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/cogs/birthday.py b/cogs/birthday.py index 951b3ca..3d4d25d 100644 --- a/cogs/birthday.py +++ b/cogs/birthday.py @@ -88,6 +88,20 @@ def load_settings(self): return json.load(file) return {} + def save_settings(self, setting): + os.makedirs(os.path.dirname(self.settings_file), exist_ok=True) + + try: + with open(self.settings_file, "r") as file: + settings = json.load(file) + except (FileNotFoundError, json.JSONDecodeError): + settings = {} + + settings.update(setting) + + with open(self.settings_file, "w") as file: + json.dump(settings, file, indent=4) + def save_data(self, data): os.makedirs(os.path.dirname(self.birthdays_file), exist_ok=True) with open(self.birthdays_file, "w") as file: From de30afa33eaa6abef6de205d57b2527a8b0d4ba2 Mon Sep 17 00:00:00 2001 From: Antonio Rodriguez <102554647+antoniorodr@users.noreply.github.com> Date: Sat, 27 Jun 2026 23:01:29 +0200 Subject: [PATCH 20/24] feat: create admin commands to update config file --- cogs/birthday.py | 62 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/cogs/birthday.py b/cogs/birthday.py index 3d4d25d..852efbe 100644 --- a/cogs/birthday.py +++ b/cogs/birthday.py @@ -107,6 +107,68 @@ def save_data(self, data): with open(self.birthdays_file, "w") as file: json.dump(data, file, indent=4) + @commands.guild_only() + @commands.group(name="bursdag") + async def birthday_group(self, ctx): + """ + Kategori for styring av bursdagsfunksjonalitet + """ + + if ctx.invoked_subcommand is None: + await ctx.send_help(ctx.command) + + @commands.is_owner() + @birthday_group.group() + async def admin(self, ctx): + """ + Kategori for innstillinger + """ + + if ctx.invoked_subcommand is None: + await ctx.send_help(ctx.command) + + @admin.command(name="guild") + async def set_guild(self, ctx, guild_id): + """ + Kommando for å sette guild_id i innstillinger + """ + try: + self.settings["guild"] = int(guild_id) + await ctx.send(f"La til guild med guild id {guild_id} i lista") + except KeyError: + self.settings["guild"] = "" + self.settings["guild"] = int(guild_id) + except Exception: + return self.bot.logger.error(f"Failed to set guild_id: {guild_id}") + self.save_settings(self.settings) + self.load_settings() + + @admin.command(name="channel") + async def set_channel(self, ctx, channel_id): + """ + Kommando for å sette channel_id i innstillinger + """ + try: + self.settings["channel_id"] = int(channel_id) + await ctx.send(f"Satt kanal med kanal id {channel_id} som bursdagskanal") + except Exception: + return self.bot.logger.error(f"Failed to set channel_id: {channel_id}") + self.save_settings(self.settings) + self.load_settings() + + @admin.command(name="role") + async def set_role(self, ctx, role_name): + """ + Kommando for å sette bursdagsrolle i innstillinger + """ + try: + self.settings["birthday_role_name"] = role_name + await ctx.send(f"Satt rolle med navn {role_name} som bursdagsrolle") + except Exception: + return self.bot.logger.error(f"Failed to set birthday_role_name: {role_name}") + self.save_settings(self.settings) + self.load_settings() + @tasks.loop(time=BIRTHDAY_CHECK_TIME) async def check_todays_birthday(self): """ From 531f15b448618f41c4966cc423b1a56dc8c217b4 Mon Sep 17 00:00:00 2001 From: Antonio Rodriguez <102554647+antoniorodr@users.noreply.github.com> Date: Sun, 28 Jun 2026 20:09:47 +0200 Subject: [PATCH 21/24] refactor: use `discord.Role` instead of string Co-authored-by: Simen --- cogs/birthday.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cogs/birthday.py b/cogs/birthday.py index 852efbe..84610d0 100644 --- a/cogs/birthday.py +++ b/cogs/birthday.py @@ -157,15 +157,15 @@ async def set_channel(self, ctx, channel_id): self.load_settings() @admin.command(name="role") - async def set_role(self, ctx, role_name): + async def set_role(self, ctx, role: discord.Role): """ Kommando for å sette bursdagsrolle i innstillinger """ try: - self.settings["birthday_role_name"] = role_name - await ctx.send(f"Satt rolle med navn {role_name} som bursdagsrolle") + self.settings["birthday_role_name"] = role.name + await ctx.send(f"Satt rolle med navn {role.name} som bursdagsrolle") except Exception: - return self.bot.logger.error(f"Failed to set birthday_role_name: {role_name}") + return self.bot.logger.error(f"Failed to set birthday_role_name: {role.name}") self.save_settings(self.settings) self.load_settings() From 40d0141b66cfc9f8d474ba395399502ed2fc9965 Mon Sep 17 00:00:00 2001 From: Antonio Rodriguez <102554647+antoniorodr@users.noreply.github.com> Date: Sun, 28 Jun 2026 20:10:50 +0200 Subject: [PATCH 22/24] refactor: use `discord.TextChannel` instead of int Co-authored-by: Simen --- cogs/birthday.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cogs/birthday.py b/cogs/birthday.py index 84610d0..511654f 100644 --- a/cogs/birthday.py +++ b/cogs/birthday.py @@ -144,15 +144,15 @@ async def set_guild(self, ctx, guild_id): self.load_settings() @admin.command(name="channel") - async def set_channel(self, ctx, channel_id): + async def set_channel(self, ctx, channel: discord.TextChannel): """ Kommando for å sette channel_id i innstillinger """ try: - self.settings["channel_id"] = int(channel_id) - await ctx.send(f"Satt kanal med kanal id {channel_id} som bursdagskanal") + self.settings["channel_id"] = int(channel.id) + await ctx.send(f"Satt kanal med kanal id {channel.id} som bursdagskanal") except Exception: - return self.bot.logger.error(f"Failed to set channel_id: {channel_id}") + return self.bot.logger.error(f"Failed to set channel_id: {channel.id}") self.save_settings(self.settings) self.load_settings() From 5dba68b1f3aae6e96d695de81ff7fff6564f14d7 Mon Sep 17 00:00:00 2001 From: Antonio Rodriguez <102554647+antoniorodr@users.noreply.github.com> Date: Sun, 28 Jun 2026 20:24:13 +0200 Subject: [PATCH 23/24] refactor: use `role.id` instead of `role.name` --- cogs/birthday.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/cogs/birthday.py b/cogs/birthday.py index 511654f..a232470 100644 --- a/cogs/birthday.py +++ b/cogs/birthday.py @@ -162,10 +162,10 @@ async def set_role(self, ctx, role: discord.Role): Kommando for å sette bursdagsrolle i innstillinger """ try: - self.settings["birthday_role_name"] = role.name - await ctx.send(f"Satt rolle med navn {role.name} som bursdagsrolle") + self.settings["birthday_role_id"] = int(role.id) + await ctx.send(f"Satt rolle med id {role.id} som bursdagsrolle") except Exception: - return self.bot.logger.error(f"Failed to set birthday_role_name: {role.name}") + return self.bot.logger.error(f"Failed to set birthday_role_id: {role.id}") self.save_settings(self.settings) self.load_settings() @@ -197,7 +197,7 @@ async def check_todays_birthday(self): if member: matches.append(member.mention) - role = discord.utils.get(guild.roles, name=self.settings.get("birthday_role_name")) + role = discord.utils.get(guild.roles, id=self.settings.get("birthday_role_id")) if role: await member.add_roles(role) else: @@ -224,7 +224,7 @@ async def remove_birthday_roles(self): Fjerner bursdagsrollen fra alle brukere hver dag ved midnatt """ guild = self.bot.guilds[0] - role = discord.utils.get(guild.roles, name=self.settings.get("birthday_role_name")) + role = discord.utils.get(guild.roles, id=self.settings.get("birthday_role_id")) if not role: return @@ -260,7 +260,7 @@ def check_files(bot): f"{bot.data_dir}/birthday/innstilinger.json": { "guild": "", "channel_id": "", - "birthday_role_name": "", + "birthday_role_id": "", } }, {f"{bot.data_dir}/birthday/birthdays.json": {}}, From b71a84806a2681f933741c6c8c5b36d2ef85a68e Mon Sep 17 00:00:00 2001 From: Antonio Rodriguez <102554647+antoniorodr@users.noreply.github.com> Date: Mon, 6 Jul 2026 09:01:16 +0200 Subject: [PATCH 24/24] fix: fix a startup bug when the `birthday` folder does not exist --- cogs/birthday.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/cogs/birthday.py b/cogs/birthday.py index a232470..26d3b44 100644 --- a/cogs/birthday.py +++ b/cogs/birthday.py @@ -265,6 +265,11 @@ def check_files(bot): }, {f"{bot.data_dir}/birthday/birthdays.json": {}}, ] + + folder = f"{bot.data_dir}/birthday" + if not os.path.exists(folder): + os.makedirs(folder) + for i in files: for file, default in i.items(): if not os.path.exists(file):