diff --git a/.env.template b/.env.template deleted file mode 100644 index a9a427544e3f95d75fbfd3fa88b7745286896096..0000000000000000000000000000000000000000 --- a/.env.template +++ /dev/null @@ -1,34 +0,0 @@ -# General -DISCORD_TOKEN=<Bot Token> -DISCORD_GUILD=<ID of Guild, this Bot should be used at> -DISCORD_ACTIVITY=<What should be shown, Bot is playing right now> -DISCORD_INVITE_LINK=<Invitation Link to this Discord Server> - -# IDs -DISCORD_ADMIN_ROLE=<ID of Admin Role> -DISCORD_CALMDOWN_ROLE=<ID of calmdown role> -DISCORD_MOD_ROLE=<ID of Mod Role> -DISCORD_BOTUEBUNGSPLATZ_CHANNEL=<ID of channel for bot experiments> -DISCORD_GREETING_CHANNEL=<ID of channel where users will be greeted when they join> -DISCORD_OFFTOPIC_CHANNEL=<ID of channel for greeting & everydays subjects and questions> -DISCORD_POLL_SUGG_CHANNEL=<ID of Channel, where poll suggestions are posted> -DISCORD_ROLE_CHANNEL=<ID of channel for attribution of server roles> -DISCORD_ROLE_MSG=<ID of role assignment message> -DISCORD_SUPPORT_CHANNEL=<ID of channel where modmail & user news should be forwarded> -DISCORD_WELCOME_CHANNEL=<ID of welcome channel> -DISCORD_WELCOME_MSG=<ID of welcome message> -DISCORD_SEASONAL_EVENTS_CATEGORY=<ID of Seasonal Events Category> -DISCORD_ADVENT_CALENDAR_CHANNEL_2021=<ID of advent calendar chanel for 2021> - -# JSON Files -DISCORD_CALMDOWN_FILE=<File name for calmdowns JSON file> -DISCORD_LINKS_FILE=<File name for links JSON file> -DISCORD_ROLES_FILE=<File name for roles JSON file> -DISCORD_TEXT_COMMANDS_FILE=<File name for text commands JSON file> -DISCORD_TIMER_FILE=<File name for running timers JSON file> -DISCORD_APPOINTMENTS_FILE=<File name for running appointments JSON file> -DISCORD_ADVENT_CALENDAR_FILE=<File name for advent calendar JSON file> - -# Misc -DISCORD_DATE_TIME_FORMAT=<Date and time format used for commands like %d.%m.%Y %H:%M> -DISCORD_ADVENT_CALENDAR_START=<Start date and time for advent calendar. Something like "01.12.2021 00:00"> diff --git a/.gitignore b/.gitignore index 2fc0bd9f14c4b640eac20222b265df0e530eaeed..caad635e08dd9bde559cedecf3bfdcac58e441fa 100644 --- a/.gitignore +++ b/.gitignore @@ -115,3 +115,5 @@ GitHub.sublime-settings !.vscode/extensions.json .history /data/*.json +/config.json +/root.db diff --git a/.idea/misc.xml b/.idea/misc.xml index 4a30df3dde00e26c0f7eea1e4e929c4f446a6810..af35c9219da620e96c191c4feeb992b32fca8419 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -3,5 +3,5 @@ <component name="JavaScriptSettings"> <option name="languageLevel" value="ES6" /> </component> - <component name="ProjectRootManager" version="2" project-jdk-name="Python 3.9 (root)" project-jdk-type="Python SDK" /> + <component name="ProjectRootManager" version="2" project-jdk-name="Python 3.10 (root)" project-jdk-type="Python SDK" /> </project> \ No newline at end of file diff --git a/.idea/root.iml b/.idea/root.iml index cfcc6c453c88dbe403c74e65b0840860f63859ba..659c7ca8cf62577ee52e2b0e6e135b7ef42dd27f 100644 --- a/.idea/root.iml +++ b/.idea/root.iml @@ -4,7 +4,7 @@ <content url="file://$MODULE_DIR$"> <excludeFolder url="file://$MODULE_DIR$/venv" /> </content> - <orderEntry type="jdk" jdkName="Python 3.9 (root)" jdkType="Python SDK" /> + <orderEntry type="jdk" jdkName="Python 3.10 (root)" jdkType="Python SDK" /> <orderEntry type="sourceFolder" forTests="false" /> </component> </module> \ No newline at end of file diff --git a/README.md b/README.md index 7aa38a19de2507ff08d77bc469b7ddb1ea7ee0cf..dc5bfd7c15d9992f803a65451dde21e63c9dcd15 100644 --- a/README.md +++ b/README.md @@ -7,25 +7,32 @@ ### Juli 2021 ✨*Geburtsstunde* -* [Begrüßung](https://github.com/FU-Hagen-Discord/root/blob/master/cogs/welcome.py) von neuen Servermitgliedern - * Text in #info - * Direktnachricht & persönliche Begrüßung in der 🌱campuswiese +* [Begrüßung](https://github.com/FU-Hagen-Discord/root/blob/master/extensions/welcome.py) von neuen Servermitgliedern + * Text in #info + * Direktnachricht & persönliche Begrüßung in der 🌱campuswiese -* Nachrichten über die 📌-Reaktion anheften, +* ~~Nachrichten über die 📌-Reaktion anheften,~~ -* [Rollenzuordnung](https://github.com/FU-Hagen-Discord/root/blob/master/cogs/roles.py) durch Verwendung von Reaktionen +* [Rollenzuordnung](https://github.com/FU-Hagen-Discord/root/blob/master/extensions/roles.py) durch Verwendung von + Reaktionen * Produktivität - * [Timer](https://github.com/FU-Hagen-Discord/root/blob/master/cogs/timer.py) für stilles Lernen oder Veranstaltungen - * [Kalenderfunktion](https://github.com/FU-Hagen-Discord/root/tree/master/cogs/appointments.py) - * [Umfragefunktion](https://github.com/FU-Hagen-Discord/root/tree/master/cogs/polls.py) - * Sammlung Nützlicher [Links](https://github.com/FU-Hagen-Discord/root/tree/master/cogs/links.py) in den Channels + * [Timer](https://github.com/FU-Hagen-Discord/root/blob/master/extensions/pomodoro.py) für stilles Lernen oder + Veranstaltungen + * [Kalenderfunktion](https://github.com/FU-Hagen-Discord/root/tree/master/extensions/appointments.py) + * [Umfragefunktion](https://github.com/FU-Hagen-Discord/root/tree/master/extensions/polls.py) + * Sammlung Nützlicher [Links](https://github.com/FU-Hagen-Discord/root/tree/master/extensions/links.py) in den + Channels -* [Text-Commands](https://github.com/FU-Hagen-Discord/root/tree/master/cogs/text_commands.py) +* [Text-Commands](https://github.com/FU-Hagen-Discord/root/tree/master/extensions/text_commands.py) * Moderationswerkzeuge - * [Calmdown-Rollenzuweisung](https://github.com/FU-Hagen-Discord/root/blob/master/cogs/calmdown.py) - * [ModMail](https://github.com/FU-Hagen-Discord/root/blob/master/cogs/support.py) + * [ModMail](https://github.com/FU-Hagen-Discord/root/blob/master/extensions/mod_mail.py) + +## Januar 2023 + +* Umbau des Bots auf den aktuellsten Stand unter Nutzung von Slash-Commands. +* Nachrichten anheften über das Kontextmenü, anstatt durch Reaktion unter der Nachricht ## Mitwirkung diff --git a/cogs/appointments.py b/cogs/appointments.py deleted file mode 100644 index 8b1030eae61675ef6cceced987f1a74a401a2c66..0000000000000000000000000000000000000000 --- a/cogs/appointments.py +++ /dev/null @@ -1,258 +0,0 @@ -import asyncio -import datetime -import io -import json -import os -import uuid - -import disnake -from disnake.ext import tasks, commands - -import utils -from cogs.help import help, handle_error, help_category - - -def get_ics_file(title, date_time, reminder, recurring): - fmt = "%Y%m%dT%H%M" - appointment = f"BEGIN:VCALENDAR\n" \ - f"PRODID:root\n" \ - f"VERSION:2.0\n" \ - f"BEGIN:VTIMEZONE\n" \ - f"TZID:Europe/Berlin\n" \ - f"BEGIN:DAYLIGHT\n" \ - f"TZOFFSETFROM:+0100\n" \ - f"TZOFFSETTO:+0200\n" \ - f"TZNAME:CEST\n" \ - f"DTSTART:19700329T020000\n" \ - f"RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=3\n" \ - f"END:DAYLIGHT\n" \ - f"BEGIN:STANDARD\n" \ - f"TZOFFSETFROM:+0200\n" \ - f"TZOFFSETTO:+0100\n" \ - f"TZNAME:CET\n" \ - f"DTSTART:19701025T030000\n" \ - f"RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10\n" \ - f"END:STANDARD\n" \ - f"END:VTIMEZONE\n" \ - f"BEGIN:VEVENT\n" \ - f"DTSTAMP:{datetime.datetime.now().strftime(fmt)}00Z\n" \ - f"UID:{uuid.uuid4()}\n" \ - f"SUMMARY:{title}\n" - appointment += f"RRULE:FREQ=DAILY;INTERVAL={recurring}\n" if recurring else f"" - appointment += f"DTSTART;TZID=Europe/Berlin:{date_time.strftime(fmt)}00\n" \ - f"DTEND;TZID=Europe/Berlin:{date_time.strftime(fmt)}00\n" \ - f"TRANSP:OPAQUE\n" \ - f"BEGIN:VALARM\n" \ - f"ACTION:DISPLAY\n" \ - f"TRIGGER;VALUE=DURATION:-PT{reminder}M\n" \ - f"DESCRIPTION:Halloooo, dein Termin findest bald statt!!!!\n" \ - f"END:VALARM\n" \ - f"END:VEVENT\n" \ - f"END:VCALENDAR" - ics_file = io.BytesIO(appointment.encode("utf-8")) - return ics_file - - -@help_category("appointments", "Appointments", "Mit Appointments kannst du Termine zu einem Kanal hinzufügen. " - "Sehr praktisches Feature zum Organisieren von Lerngruppen.") -class Appointments(commands.Cog): - def __init__(self, bot): - self.bot = bot - self.fmt = os.getenv("DISCORD_DATE_TIME_FORMAT") - self.timer.start() - self.appointments = {} - self.app_file = os.getenv("DISCORD_APPOINTMENTS_FILE") - self.load_appointments() - - def load_appointments(self): - """ Loads all appointments from APPOINTMENTS_FILE """ - - appointments_file = open(self.app_file, mode='r') - self.appointments = json.load(appointments_file) - - @tasks.loop(minutes=1) - async def timer(self): - delete = [] - - for channel_id, channel_appointments in self.appointments.items(): - channel = None - for message_id, appointment in channel_appointments.items(): - now = datetime.datetime.now() - date_time = datetime.datetime.strptime(appointment["date_time"], self.fmt) - remind_at = date_time - datetime.timedelta(minutes=appointment["reminder"]) - - if now >= remind_at: - try: - channel = await self.bot.fetch_channel(int(channel_id)) - message = await channel.fetch_message(int(message_id)) - reactions = message.reactions - diff = int(round(((date_time - now).total_seconds() / 60), 0)) - answer = f"Benachrichtigung!\nDer Termin \"{appointment['title']}\" ist " - - if appointment["reminder"] > 0 and diff > 0: - answer += f"in {diff} Minuten fällig." - if (reminder := appointment.get("reminder")) and appointment.get("recurring"): - appointment["original_reminder"] = str(reminder) - appointment["reminder"] = 0 - else: - answer += f"jetzt fällig. :loudspeaker: " - delete.append(message_id) - - answer += f"\n" - for reaction in reactions: - if reaction.emoji == "ðŸ‘": - async for user in reaction.users(): - if user != self.bot.user: - answer += f"<@!{str(user.id)}> " - - await channel.send(answer) - - if str(message.id) in delete: - await message.delete() - except disnake.errors.NotFound: - delete.append(message_id) - - if len(delete) > 0: - for key in delete: - channel_appointment = channel_appointments.get(key) - if channel_appointment: - if channel_appointment.get("recurring"): - recurring = channel_appointment["recurring"] - date_time_str = channel_appointment["date_time"] - date_time = datetime.datetime.strptime(date_time_str, self.fmt) - new_date_time = date_time + datetime.timedelta(days=recurring) - new_date_time_str = new_date_time.strftime(self.fmt) - splitted_new_date_time_str = new_date_time_str.split(" ") - reminder = channel_appointment.get("original_reminder") - reminder = reminder if reminder else 0 - await self.add_appointment(channel, channel_appointment["author_id"], - splitted_new_date_time_str[0], - splitted_new_date_time_str[1], - str(reminder), - channel_appointment["title"], - channel_appointment["recurring"]) - channel_appointments.pop(key) - self.save_appointments() - - @timer.before_loop - async def before_timer(self): - await asyncio.sleep(60 - datetime.datetime.now().second) - - @help( - category="appointments", - brief="Fügt eine neue Erinnerung zu einem Kanal hinzu.", - example="!add-appointment 20.12.2021 10:00 0 \"Toller Event\" 7", - parameters={ - "date": "Datum des Termins im Format DD.MM.YYYY (z. B. 22.10.2022).", - "time": "Uhrzeit des Termins im Format hh:mm (z. B. 10:00).", - "reminder": "Anzahl an Minuten die vor dem Termin erinnert werden soll.", - "title": "der Titel des Termins (in Anführungszeichen).", - "recurring": "*(optional)* Interval für die Terminwiederholung in Tagen" - } - ) - @commands.command(name="add-appointment") - async def cmd_add_appointment(self, ctx, date, time, reminder, title, recurring: int = None): - await self.add_appointment(ctx.channel, ctx.author.id, date, time, reminder, title, recurring) - - async def add_appointment(self, channel, author_id, date, time, reminder, title, recurring: int = None): - """ Add appointment to a channel """ - - try: - date_time = datetime.datetime.strptime(f"{date} {time}", self.fmt) - except ValueError: - await channel.send("Fehler! Ungültiges Datums und/oder Zeit Format!") - return - - if not utils.is_valid_time(reminder): - await channel.send("Fehler! Benachrichtigung in ungültigem Format!") - return - else: - reminder = utils.to_minutes(reminder) - - embed = disnake.Embed(title="Neuer Termin hinzugefügt!", - description=f"Wenn du eine Benachrichtigung zum Beginn des Termins" - f"{f', sowie {reminder} Minuten vorher, ' if reminder > 0 else f''} " - f"erhalten möchtest, reagiere mit :thumbsup: auf diese Nachricht.", - color=19607) - - embed.add_field(name="Titel", value=title, inline=False) - embed.add_field(name="Startzeitpunkt", value=f"{date_time.strftime(self.fmt)}", inline=False) - if reminder > 0: - embed.add_field(name="Benachrichtigung", value=f"{reminder} Minuten vor dem Start", inline=False) - if recurring: - embed.add_field(name="Wiederholung", value=f"Alle {recurring} Tage", inline=False) - - message = await channel.send(embed=embed, file=disnake.File(get_ics_file(title, date_time, reminder, recurring), - filename=f"{title}.ics")) - await message.add_reaction("ðŸ‘") - await message.add_reaction("🗑ï¸") - - if str(channel.id) not in self.appointments: - self.appointments[str(channel.id)] = {} - - channel_appointments = self.appointments.get(str(channel.id)) - channel_appointments[str(message.id)] = {"date_time": date_time.strftime(self.fmt), "reminder": reminder, - "title": title, "author_id": author_id, "recurring": recurring} - - self.save_appointments() - - @help( - category="appointments", - brief="Zeigt alle Termine des momentanen Kanals an." - ) - @commands.command(name="appointments") - async def cmd_appointments(self, ctx): - """ List (and link) all Appointments in the current channel """ - - if str(ctx.channel.id) in self.appointments: - channel_appointments = self.appointments.get(str(ctx.channel.id)) - answer = f'Termine dieses Channels:\n' - delete = [] - - for message_id, appointment in channel_appointments.items(): - try: - message = await ctx.channel.fetch_message(int(message_id)) - answer += f'{appointment["date_time"]}: {appointment["title"]} => ' \ - f'{message.jump_url}\n' - except disnake.errors.NotFound: - delete.append(message_id) - - if len(delete) > 0: - for key in delete: - channel_appointments.pop(key) - self.save_appointments() - - await ctx.channel.send(answer) - else: - await ctx.send("Für diesen Channel existieren derzeit keine Termine") - - def save_appointments(self): - appointments_file = open(self.app_file, mode='w') - json.dump(self.appointments, appointments_file) - - async def handle_reactions(self, payload): - channel = await self.bot.fetch_channel(payload.channel_id) - channel_appointments = self.appointments.get(str(payload.channel_id)) - if channel_appointments: - appointment = channel_appointments.get(str(payload.message_id)) - if appointment: - if payload.user_id == appointment["author_id"]: - message = await channel.fetch_message(payload.message_id) - await message.delete() - channel_appointments.pop(str(payload.message_id)) - - self.save_appointments() - - @commands.Cog.listener() - async def on_raw_reaction_add(self, payload): - if payload.user_id == self.bot.user.id: - return - - if payload.emoji.name in ["🗑ï¸"]: - channel = await self.bot.fetch_channel(payload.channel_id) - message = await channel.fetch_message(payload.message_id) - if len(message.embeds) > 0 and message.embeds[0].title == "Neuer Termin hinzugefügt!": - await self.handle_reactions(payload) - - async def cog_command_error(self, ctx, error): - await handle_error(ctx, error) diff --git a/cogs/calmdown.py b/cogs/calmdown.py deleted file mode 100644 index b01b738cb2b9d9bc715595bd0a64ba83aa0e1320..0000000000000000000000000000000000000000 --- a/cogs/calmdown.py +++ /dev/null @@ -1,107 +0,0 @@ -import datetime -import json -import os -import re - -import disnake -from disnake.ext import commands, tasks - -import utils -from cogs.help import help - -""" - DISCORD_CALMDOWN_ROLE - Die Rollen-ID der "Mute"-Rolle. - DISCORD_CALMDOWN_FILE - Datendatei. Wenn diese noch nicht existiert wird sie angelegt. - DISCORD_DATE_TIME_FORMAT - Datumsformat für die interne Speicherung. -""" - -class Calmdown(commands.Cog): - def __init__(self, bot): - self.bot = bot - self.role_id = int(os.getenv("DISCORD_CALMDOWN_ROLE")) - self.file = os.getenv("DISCORD_CALMDOWN_FILE") - self.fmt = os.getenv("DISCORD_DATE_TIME_FORMAT") - self.silenced_users = {} - self.load() - self.timer.start() - - - def load(self): - try: - file = open(self.file, mode='r') - self.silenced_users = json.load(file) - except FileNotFoundError: - pass - - def save(self): - file = open(self.file, mode='w') - json.dump(self.silenced_users, file) - - async def unsilence(self, user_id, guild_id, inform_user=False): - guild = await self.bot.fetch_guild(int(guild_id)) - role = guild.get_role(self.role_id) - - try: - user = await guild.fetch_member(int(user_id)) - if inform_user: - await utils.send_dm(user, f"Die Mute-Rolle wurde nun wieder entfernt.") - await user.remove_roles(role) - except disnake.errors.NotFound: - pass - - if self.silenced_users.get(str(user_id)): - del self.silenced_users[str(user_id)] - self.save() - - @tasks.loop(minutes=1) - async def timer(self): - now = datetime.datetime.now() - silenced_users = self.silenced_users.copy() - for user_id, data in silenced_users.items(): - duration = data.get('duration') - if not duration: - return - till = datetime.datetime.strptime(duration, self.fmt) - if now >= till: - await self.unsilence(user_id, data['guild_id'], inform_user=True) - - @help( - brief="Weist einem User die Mute-Rolle zu.", - example="!mute @user 1d", - parameters={ - "user": "Mention des Users, der eine Auszeit benötigt", - "duration": "Länge der Auszeit (24h für 24 Stunden 7d für 7 Tage oder 10m oder 10 für 10 Minuten. 0 hebt die Sperre auf).", - }, - description="In der Auszeit darf das Servermitglied noch alle Kanäle lesen. Das Schreiben und Sprechen ist für ihn oder sie allerdings bis zum Ablauf der Zeit gesperrt.", - mod=True - ) - @commands.command(name="calmdown", aliases=["auszeit", "mute"]) - @commands.check(utils.is_mod) - async def cmd_calmdown(self, ctx, user: disnake.Member, duration): - if re.match(r"^[0-9]+$", duration): - duration = f"{duration}m" - if not utils.is_valid_time(duration): - await ctx.channel.send("Fehler! Wiederholung in ungültigem Format!") - return - else: - guild = ctx.guild - role = guild.get_role(self.role_id) - if not role: - ctx.channel.send("Fehler! Rolle nicht vorhanden!") - return - duration = utils.to_minutes(duration) - if duration == 0: - await ctx.channel.send(f"{ctx.author.mention} hat {user.mention} aus der **Auszeit** geholt.") - await self.unsilence(user.id, guild.id, inform_user=False) - return - - now = datetime.datetime.now() - till = now + datetime.timedelta(minutes=duration) - self.silenced_users[str(user.id)] = {"duration": till.strftime(self.fmt), "guild_id": guild.id} - self.save() - await ctx.channel.send(f"{ctx.author.mention} hat an {user.mention} die **Mute-Rolle** vergeben.") - await user.add_roles(role) - if duration < 300: - await utils.send_dm(user, f"Dir wurde für {duration} Minuten die **Mute-Rolle** zugewiesen. Du kannst weiterhin alle Kanäle lesen, aber erst nach Ablauf der Zeit wieder an Gesprächen teilnehmen.") - else: - await utils.send_dm(user, f"Bis {till.strftime(self.fmt)} Uhr trägst du die Mute-Rolle. Du kannst weiterhin alle Kanäle lesen, aber erst nach Ablauf der Zeit wieder an Gesprächen teilnehmen.") \ No newline at end of file diff --git a/cogs/christmas.py b/cogs/christmas.py deleted file mode 100644 index 9c530f52b33cb2263233e1b1b6e85f4d1aa64d3d..0000000000000000000000000000000000000000 --- a/cogs/christmas.py +++ /dev/null @@ -1,169 +0,0 @@ -import asyncio -import json -import os -from datetime import datetime, timedelta - -from disnake import ApplicationCommandInteraction, Member -from disnake.ext import commands, tasks -from dotenv import load_dotenv - -import utils - -load_dotenv() - - -def create_advent_calendar(): - advent_calendar = [] - startdate = utils.date_from_string(os.getenv("DISCORD_ADVENT_CALENDAR_START")).astimezone() - - for i in range(0, 24): - advent_calendar.append({ - "number": i + 1, - "date": utils.date_to_string(startdate + timedelta(days=i)), - "assigned": False, - "opened": False - }) - - return advent_calendar - - -class Christmas(commands.Cog): - def __init__(self, bot): - self.bot = bot - self.seasonal_events_category = int(os.getenv("DISCORD_SEASONAL_EVENTS_CATEGORY")) - self.advent_calendar_channel = int(os.getenv("DISCORD_ADVENT_CALENDAR_CHANNEL_2021")) - self.file_name = os.getenv("DISCORD_ADVENT_CALENDAR_FILE") - self.advent_calendar = self.load() - self.advent_calendar_loop.start() - - def load(self): - with open(self.file_name, mode='r') as f: - advent_calendar = json.load(f) - - if len(advent_calendar) == 0: - advent_calendar = create_advent_calendar() - - return advent_calendar - - def save(self): - with open(self.file_name, mode='w') as f: - json.dump(self.advent_calendar, f) - - @commands.slash_command(name="advent", guild_ids=[int(os.getenv('DISCORD_GUILD'))]) - async def cmd_advent(self, interaction: ApplicationCommandInteraction): - pass - - @cmd_advent.sub_command(name="list", description="Erhalte die Liste aller Türchen mit Zuordnung und Thema") - @commands.check(utils.is_mod) - async def cmd_advent_list(self, interaction: ApplicationCommandInteraction): - message = f"__**Adventskalender 2021**__\n\n" - - for day in self.advent_calendar: - message += f"{day['number']}. " - if day["assigned"]: - message += f"<@!{day['assignee']}>: \"{day['name']}\"" - else: - message += f"noch nicht zugewiesen" - - message += "\n" - - await interaction.response.send_message(message, ephemeral=True) - - @cmd_advent.sub_command(name="assign", description="Einer Person ein Türchen zuweisen", - guild_ids=[int(os.getenv('DISCORD_GUILD'))]) - @commands.check(utils.is_mod) - async def cmd_advent_assign(self, interaction: ApplicationCommandInteraction, day: int, member: Member, name: str): - if self.advent_calendar[day - 1]["assigned"]: - await interaction.response.send_message("Das gewählte Türchen ist bereits vergeben. \n" - "Wenn du das Türchen an jemand anderen vergeben möchtest, oder das " - "Thema ändern möchtest, verwende `/advent reassign`.", - ephemeral=True) - else: - await interaction.response.defer(ephemeral=True) - await self.assign_day(day, member, name) - await interaction.edit_original_message(content="Das gewählte Türchen wurde vergeben.") - - @cmd_advent.sub_command(name="reassign", description="Ein Türchen neu zuweisen", - guild_ids=[int(os.getenv('DISCORD_GUILD'))]) - @commands.check(utils.is_mod) - async def cmd_advent_reassign(self, interaction: ApplicationCommandInteraction, day: int, member: Member, - name: str): - if not self.advent_calendar[day - 1]["assigned"]: - await interaction.response.send_message("Das gewählte Türchen ist noch nicht vergeben. \n" - "Bitte verwende `/advent assign` um das Türchen an " - "jemanden zu vergeben.", ephemeral=True) - else: - await interaction.response.defer(ephemeral=True) - channel = await self.bot.fetch_channel(self.advent_calendar[day - 1]["channel"]) - old_member = await self.bot.fetch_user(self.advent_calendar[day - 1]["assignee"]) - await channel.set_permissions(old_member, overwrite=None) - await self.assign_day(day, member, name) - await interaction.edit_original_message(content="Das gewählte Türchen wurde neu vergeben.") - - @cmd_advent.sub_command(name="remaining", description="Noch nicht zugewiesene Türchen ausgeben lassen.", - guild_ids=[int(os.getenv('DISCORD_GUILD'))]) - @commands.check(utils.is_mod) - async def cmd_advent_remaining(self, interaction: ApplicationCommandInteraction): - message = f"Noch verfügbare Türchen: " - - for day in self.advent_calendar: - if not day["assigned"]: - message += f"{day['number']}, " - - await interaction.response.send_message(message[:-2], ephemeral=True) - - async def assign_day(self, day: int, member: Member, name: str): - category = await self.bot.fetch_channel(self.seasonal_events_category) - channel = await category.create_text_channel(f"{day}-{name}") - await channel.set_permissions(member.roles[0], view_channel=False) - await channel.set_permissions(member, view_channel=True) - await channel.send(f"Vielen Dank {member.mention}, dass du für das {day}. Türchen etwas zum Thema {name} " - f"vorbereiten möchtest. Dieser Channel ist für dich gedacht. Du kannst hier deinen Beitrag " - f"vorbereiten.\n\n" - f"Am {day}.12.2021 um 00:00 werden alle Nachrichten von dir, die in diesem Channel bis " - f"dahin geschrieben wurden, in einen eigenen Thread für diesen Tag übernommen.\n\n" - f"Beachte bitte, dass Sticker nicht verwendet werden können. Das gleiche gilt für Emojis, " - f"die nicht von diesem Server sind.\n\n" - f"Das Mod-Team wünscht dir viel Spaß bei der Vorbereitung.") - self.advent_calendar[day - 1]["channel"] = channel.id - self.advent_calendar[day - 1]["assigned"] = True - self.advent_calendar[day - 1]["assignee"] = member.id - self.advent_calendar[day - 1]["name"] = name - self.save() - - async def open(self, day): - source_channel = await self.bot.fetch_channel(day["channel"]) - assignee = await self.bot.fetch_user(day["assignee"]) - target_channel = await self.bot.fetch_channel(self.advent_calendar_channel) - thread_name = f"{day['number']}. {day['name']}" - - message = await target_channel.send(f"{day['number']}. \"{day['name']}\", ein Beitrag von {assignee.mention}:") - thread = await message.create_thread(name=thread_name, auto_archive_duration=1440) - day["thread"] = thread.id - day["opened"] = True - - async for msg in source_channel.history(limit=None, oldest_first=True): - if msg.author == assignee: - if len(msg.stickers) > 0: - continue - files = await utils.files_from_attachments(msg.attachments) - await thread.send(content=msg.content, embeds=msg.embeds, files=files) - - await thread.send("--------------------------\nBeginn der Diskussion\n--------------------------") - - self.save() - - @tasks.loop(seconds=10) - async def advent_calendar_loop(self): - now = datetime.now() - for day in self.advent_calendar: - if not day["opened"]: - due_date = utils.date_from_string(day["date"]) - if due_date <= now: - await self.open(day) - else: - return - - @advent_calendar_loop.before_loop - async def before_advent_calendar_loop(self): - await asyncio.sleep(10 - datetime.now().second % 10) diff --git a/cogs/components/poll/poll.py b/cogs/components/poll/poll.py deleted file mode 100644 index 22cd555028395f0f216651254e5eb133a23da0c4..0000000000000000000000000000000000000000 --- a/cogs/components/poll/poll.py +++ /dev/null @@ -1,146 +0,0 @@ -import disnake -import emoji - -DEFAULT_OPTIONS = ["🇦", "🇧", "🇨", "🇩", "🇪", "🇫", "🇬", "ðŸ‡", "🇮", "🇯", "🇰", "🇱", "🇲", "🇳", "🇴", "🇵", "🇶", - "🇷"] -DELETE_POLL = "🗑ï¸" -CLOSE_POLL = "🛑" - - -def is_emoji(word): - if word in emoji.UNICODE_EMOJI_ALIAS_ENGLISH: - return True - elif word[:-1] in emoji.UNICODE_EMOJI_ALIAS_ENGLISH: - return True - - -def get_unique_option(options): - for option in DEFAULT_OPTIONS: - if option not in options: - return option - - -def get_options(bot, answers): - options = [] - - for i in range(min(len(answers), len(DEFAULT_OPTIONS))): - option = "" - answer = answers[i].strip() - index = answer.find(" ") - - if index > -1: - possible_option = answer[:index] - if is_emoji(possible_option): - if len(answer[index:].strip()) > 0: - option = possible_option - answers[i] = answer[index:].strip() - elif len(possible_option) > 1: - if possible_option[0:2] == "<:" and possible_option[-1] == ">": - splitted_custom_emoji = possible_option.strip("<:>").split(":") - if len(splitted_custom_emoji) == 2: - id = splitted_custom_emoji[1] - custom_emoji = bot.get_emoji(int(id)) - if custom_emoji and len(answer[index:].strip()) > 0: - option = custom_emoji - answers[i] = answer[index:].strip() - - if (isinstance(option, str) and len(option) == 0) or option in options or option in [DELETE_POLL, - CLOSE_POLL]: - option = get_unique_option(options) - options.append(option) - - return options - - -class Poll: - def __init__(self, bot, question=None, answers=None, author=None, message=None): - self.bot = bot - self.question = question - self.answers = answers - self.author = author - - if message: - self.message = message - self.answers = [] - embed = message.embeds[0] - self.author = embed.fields[0].value[3:-1] - self.question = embed.description - for i in range(2, len(embed.fields)): - self.answers.append(f"{embed.fields[i].name} {embed.fields[i].value}") - - self.options = get_options(self.bot, self.answers) - - async def send_poll(self, channel, result=False, message=None): - option_ctr = 0 - title = "Umfrage" - participants = {} - - if result: - title += " Ergebnis" - - if len(self.answers) > len(DEFAULT_OPTIONS): - await channel.send( - f"Fehler beim Erstellen der Umfrage! Es werden nicht mehr als {len(DEFAULT_OPTIONS)} Optionen unterstützt!") - return - - embed = disnake.Embed(title=title, description=self.question) - embed.add_field(name="Erstellt von", value=f'<@!{self.author}>', inline=False) - embed.add_field(name="\u200b", value="\u200b", inline=False) - - for i in range(0, len(self.answers)): - name = f'{self.options[i]}' - value = f'{self.answers[i]}' - - if result: - reaction = self.get_reaction(name) - if reaction: - name += f' : {reaction.count - 1}' - async for user in reaction.users(): - if user != self.bot.user: - participants[str(user.id)] = 1 - - embed.add_field(name=name, value=value, inline=False) - option_ctr += 1 - - if result: - embed.add_field(name="\u200b", value="\u200b", inline=False) - embed.add_field(name="Anzahl Teilnehmer an der Umfrage", value=f"{len(participants)}", inline=False) - - if message: - await message.edit(embed=embed) - else: - message = await channel.send("", embed=embed) - - reactions = [] - for reaction in message.reactions: - reactions.append(reaction.emoji) - - if not result: - await message.clear_reaction("🗑ï¸") - await message.clear_reaction("🛑") - - for reaction in reactions: - if reaction not in self.options: - await message.clear_reaction(reaction) - - for i in range(0, len(self.answers)): - if self.options[i] not in reactions: - await message.add_reaction(self.options[i]) - - await message.add_reaction("🗑ï¸") - await message.add_reaction("🛑") - - async def close_poll(self): - await self.send_poll(self.message.channel, result=True) - await self.delete_poll() - - async def delete_poll(self): - await self.message.delete() - - def get_reaction(self, reaction): - if self.message: - reactions = self.message.reactions - - for react in reactions: - if react.emoji == reaction: - return react diff --git a/cogs/help.py b/cogs/help.py deleted file mode 100644 index 73b09df529b4a2d678239ac2b7287eeb86fcf8f5..0000000000000000000000000000000000000000 --- a/cogs/help.py +++ /dev/null @@ -1,247 +0,0 @@ -import inspect -import re - -import disnake -from disnake.ext import commands - -import utils - -data = {"category": {"__none__": {"title": "Sonstiges", "description": "Die Kategorie für die Kategorielosen."}}, - "command": {}} - - -def help_category(name=None, title=None, description=None, mod_description=None): - def decorator_help(cmd): - data["category"][name] = {"title": title, "description": description, - "mod_description": mod_description if mod_description else description} - # if not data["category"][name]: - # data["category"][name] = {"description": description} - # else: - # data["category"][name]["description"] = description - return cmd - - return decorator_help - - -@help_category("help", "Hilfe", "Wenn du nicht weiter weißt, gib `!help` ein.", - "Wenn du nicht weiter weißt, gib `!mod-help` ein.") -def text_command_help(name, syntax=None, example=None, brief=None, description=None, mod=False, parameters=None, - category=None): - if parameters is None: - parameters = {} - cmd = re.sub(r"^!", "", name) - if syntax is None: - syntax = name - add_help(cmd, syntax, example, brief, description, mod, parameters, category) - - -def remove_help_for(name): - data["command"].pop(name) - - -def help(syntax=None, example=None, brief=None, description=None, mod=False, parameters=None, category=None, - command_group=''): - if parameters is None: - parameters = {} - - def decorator_help(cmd): - nonlocal syntax, parameters - cmd_name = f"{command_group} {cmd.name}" if command_group else f"{cmd.name}" - if syntax is None: - arguments = inspect.signature(cmd.callback).parameters - function_arguments = [ - f"<{item[1].name}{'?' if item[1].default != inspect._empty else ''}>" for item in - list(arguments.items())[2:]] - syntax = f"!{cmd_name} {' '.join(function_arguments)}" - add_help(cmd_name, syntax, example, brief, - description, mod, parameters, category) - return cmd - - return decorator_help - - -def add_help(cmd, syntax, example, brief, description, mod, parameters, category=None): - if not category: - category = "__none__" - - data["command"][cmd] = { - "name": cmd, - "syntax": syntax.strip(), - "brief": brief, - "example": example, - "description": description, - "parameters": parameters, - "mod": mod, - "category": category - } - - -async def handle_error(ctx, error): - if isinstance(error, commands.errors.MissingRequiredArgument): - # syntax = data[ctx.command.name]['syntax'] - # example = data[ctx.command.name]['example'] - - msg = ( - f"Fehler! Du hast ein Argument vergessen. Für weitere Hilfe gib `!help {ctx.command.name}` ein. \n" - f"`Syntax: {data['command'][ctx.command.name]['syntax']}`\n" - ) - await ctx.channel.send(msg) - else: - raise error - - -class Help(commands.Cog): - def __init__(self, bot): - self.bot = bot - - @help( - category="help", - brief="Zeigt die verfügbaren Kommandos an. Wenn ein Kommando übergeben wird, wird eine ausführliche Hilfe zu diesem Kommando angezeigt.", - ) - @commands.command(name="help") - async def cmd_help(self, ctx, *command): - if len(command) > 0: - command = re.sub(r"^!", "", ' '.join(command)) - await self.help_card(ctx, command) - return - await self.help_overview(ctx) - - @help( - category="help", - brief="Zeigt die verfügbaren Hilfe-Kategorien an.", - mod=True - ) - @commands.command(name="help-categories") - @commands.check(utils.is_mod) - async def cmd_categories(self, ctx): - sorted_groups = {k: v for k, v in sorted(data["category"].items(), key=lambda item: item[1]['title'])} - text = "" - for key, value in sorted_groups.items(): - text += f"**{key} => {value['title']}**\n" - text += f"- {value['description']}\n" if value['description'] else "" - - await ctx.channel.send(text) - - @help( - category="help", - brief="Zeigt die verfügbaren Kommandos *für Mods* an. Wenn ein Kommando übergeben wird, wird eine ausführliche Hilfe zu diesem Kommando angezeigt. ", - mod=True - ) - @commands.command(name="mod-help") - @commands.check(utils.is_mod) - async def cmd_mod_help(self, ctx, command=None): - if not command is None: - command = re.sub(r"^!", "", command) - if command == "*" or command == "all": - await self.help_overview(ctx, mod=True, all=True) - return - await self.help_card(ctx, command) - return - await self.help_overview(ctx, mod=True) - - async def help_overview(self, ctx, mod=False, all=False): - sorted_groups = {k: v for k, v in sorted(data["category"].items(), key=lambda item: item[1]['title'] if item[ - 0] != '__none__' else 'zzzzzzzzzzzzzz')} - sorted_commands = {k: v for k, v in sorted(data["command"].items(), key=lambda item: item[1]['syntax'])} - - title = "root hilft dir!" - help_command = "!help" if not mod else "!mod-help" - helptext = ( - f"Um ausführliche Hilfe zu einem bestimmten Kommando zu erhalten, gib **{help_command} <command>** ein. " - f"Also z.B. **{help_command} stats** um mehr über das Statistik-Kommando zu erfahren.") - helptext += "`!mod-help *` gibt gleichzeitig mod und nicht-mod Kommandos in der Liste aus." if mod else "" - helptext += "\n\n" - msgcount = 1 - - for key, group in sorted_groups.items(): - text = f"\n__**{group['title']}**__\n" - text += f"{group['mod_description']}\n" if group.get('mod_description') and mod else "" - text += f"{group['description']}\n" if group.get('description') and not mod else "" - text += "\n" - for command in sorted_commands.values(): - - if (not all and command['mod'] != mod) or command['category'] != key: - continue - # {'*' if command['description'] else ''}\n" - text += f"**{command['syntax']}**\n" - text += f"{command['brief']}\n\n" if command['brief'] else "\n" - if (len(helptext) + len(text) > 2048): - embed = disnake.Embed(title=title, - description=helptext, - color=19607) - await utils.send_dm(ctx.author, "", embed=embed) - helptext = "" - msgcount = msgcount + 1 - title = f"root hilft dir! (Fortsetzung {msgcount})" - helptext += text - text = "" - - embed = disnake.Embed(title=title, - description=helptext, - color=19607) - await utils.send_dm(ctx.author, "", embed=embed) - - async def help_card(self, ctx, name): - try: - command = data['command'][name] - if command['mod'] and not utils.is_mod(ctx): - raise KeyError - except KeyError: - await ctx.channel.send( - "Fehler! Für dieses Kommando habe ich keinen Hilfe-Eintrag. Gib `!help` ein um eine Ãœbersicht zu erhalten. ") - return - title = command['name'] - text = f"**{title}**\n" - text += f"{command['brief']}\n\n" if command['brief'] else "" - text += f"**Syntax:**\n `{command['syntax']}`\n" - text += "**Parameter:**\n" if len(command['parameters']) > 0 else "" - for param, desc in command['parameters'].items(): - text += f"`{param}` - {desc}\n" - text += f"**Beispiel:**\n `{command['example']}`\n" if command['example'] else "" - text += f"\n{command['description']}\n" if command['description'] else "" - embed = disnake.Embed(title=title, - description=text, - color=19607) - await utils.send_dm(ctx.author, text) # , embed=embed) - - @commands.command(name="debug-help") - @commands.check(utils.is_mod) - async def help_all(self, ctx, mod=False): - sorted_groups = {k: v for k, v in sorted(data["category"].items(), key=lambda item: item[1]['title'] if item[ - 0] != '__none__' else 'zzzzzzzzzzzzzz')} - sorted_commands = {k: v for k, v in sorted(data["command"].items(), key=lambda item: item[1]['syntax'])} - title = "root hilft dir!" - helptext = ("Um ausführliche Hilfe zu einem bestimmten Kommando zu erhalten, gib **!help <command>** ein. " - "Also z.B. **!help stats** um mehr über das Statistik-Kommando zu erfahren.\n\n\n") - msgcount = 1 - for key, group in sorted_groups.items(): - text = f"\n__**{group['title']}**__\n" - text += f"{group['description']}\n\n" if group['description'] else "\n" - for command in sorted_commands.values(): - if command['category'] != key: - continue - text += f"**{command['name']}**{' (mods only)' if command['mod'] else ''}\n" - text += f"{command['brief']}\n\n" if command['brief'] else "" - text += f"**Syntax:**\n `{command['syntax']}`\n" - text += "**Parameter:**\n" if len( - command['parameters']) > 0 else "" - for param, desc in command['parameters'].items(): - text += f"`{param}` - {desc}\n" - text += f"**Beispiel:**\n `{command['example']}`\n" if command['example'] else "" - text += f"\n{command['description']}\n" if command['description'] else "" - text += "=====================================================\n" - if (len(helptext) + len(text) > 2048): - embed = disnake.Embed(title=title, - description=helptext, - color=19607) - await utils.send_dm(ctx.author, "", embed=embed) - helptext = "" - msgcount = msgcount + 1 - title = f"root hilft dir! (Fortsetzung {msgcount})" - helptext += text - text = "" - - embed = disnake.Embed(title=title, - description=helptext, - color=19607) - await utils.send_dm(ctx.author, "", embed=embed) diff --git a/cogs/links.py b/cogs/links.py deleted file mode 100644 index 6adcf40369d23c9557238df71142b29309286624..0000000000000000000000000000000000000000 --- a/cogs/links.py +++ /dev/null @@ -1,210 +0,0 @@ -import json - -import disnake -from disnake.ext import commands -from cogs.help import help, handle_error, help_category - - -@help_category("links", "Links", "Feature zum Verwalten von Links innerhalb eines Channels.") -class Links(commands.Cog): - def __init__(self, bot): - self.bot = bot - self.links = {} - self.links_file = "data/links.json" - self.load_links() - - def load_links(self): - links_file = open(self.links_file, 'r') - self.links = json.load(links_file) - - def save_links(self): - links_file = open(self.links_file, 'w') - json.dump(self.links, links_file) - - @help( - category="links", - brief="Zeigt die Links an, die in diesem Channel (evtl. unter Berücksichtigung eines Themas) hinterlegt sind.", - parameters={ - "topic": "*(optional)* Schränkt die angezeigten Links auf das übergebene Thema ein. " - } - ) - @commands.group(name="links", pass_context=True, invoke_without_command=True) - async def cmd_links(self, ctx, topic=None): - if channel_links := self.links.get(str(ctx.channel.id)): - embed = disnake.Embed(title=f"Folgende Links sind in diesem Channel hinterlegt:\n") - if topic: - topic = topic.lower() - if topic_links := channel_links.get(topic): - value = f"" - for title, link in topic_links.items(): - value += f"- [{title}]({link})\n" - embed.add_field(name=topic.capitalize(), value=value, inline=False) - await ctx.send(embed=embed) - else: - await ctx.send( - f" Für das Thema `{topic}` sind in diesem Channel keine Links hinterlegt. Versuch es noch mal " - f"mit einem anderen Thema, oder lass dir mit `!links` alle Links in diesem Channel ausgeben") - else: - for topic, links in channel_links.items(): - value = f"" - for title, link in links.items(): - value += f"- [{title}]({link})\n" - embed.add_field(name=topic.capitalize(), value=value, inline=False) - await ctx.send(embed=embed) - else: - await ctx.send("Für diesen Channel sind noch keine Links hinterlegt.") - - @help( - category="links", - syntax="!links add <topic> <link> <title...>", - brief="Fügt einen Link zum Channel hinzu.", - parameters={ - "topic": "Name des Themas, dem der Link zugeordnet werden soll. ", - "link": "die URL, die aufgerufen werden soll (z. B. https://www.fernuni-hagen.de). ", - "title...": "Titel, der für diesen Link angezeigt werden soll (darf Leerzeichen enthalten). " - }, - description="Die mit !links add zu einem Kanal hinzugefügten Links können über das Kommando !links in diesem " - "Kanal wieder abgerufen werden.", - command_group="links" - ) - @cmd_links.command(name="add") - async def cmd_add_link(self, ctx, topic, link, *title): - topic = topic.lower() - if not (channel_links := self.links.get(str(ctx.channel.id))): - self.links[str(ctx.channel.id)] = {} - channel_links = self.links.get(str(ctx.channel.id)) - - if not (topic_links := channel_links.get(topic)): - channel_links[topic] = {} - topic_links = channel_links.get(topic) - - self.add_link(topic_links, link, " ".join(title)) - self.save_links() - - def add_link(self, topic_links, link, title): - if topic_links.get(title): - self.add_link(topic_links, link, title + str(1)) - else: - topic_links[title] = link - - @help( - category="links", - syntax="!links remove-link <topic> <title...>", - brief="Löscht einen Link aus dem Channel.", - parameters={ - "topic": "Name des Themas, aus dem der Link entfernt werden soll. ", - "title...": "Titel des Links, der entfernt werden soll. " - }, - description="Mit !links remove-link kann ein fehlerhafter oder veralteter Link aus der Linkliste des Channels " - "entfernt werden.", - command_group="links" - ) - @cmd_links.command(name="remove-link", aliases=['rl']) - async def cmd_remove_link(self, ctx, topic, *title): - topic = topic.lower() - title = " ".join(title) - - if channel_links := self.links.get(str(ctx.channel.id)): - if topic_links := channel_links.get(topic): - if title in topic_links: - topic_links.pop(title) - if not topic_links: - channel_links.pop(topic) - else: - await ctx.channel.send('Ich konnte den Link leider nicht finden.') - else: - await ctx.channel.send('Ich konnte das Thema leider nicht finden.') - else: - await ctx.channel.send('Für diesen Channel sind keine Links hinterlegt.') - - self.save_links() - - @help( - category="links", - syntax="!links remove-topic <topic>", - brief="Löscht eine komplette Themenkategorie aus dem Channel.", - parameters={ - "topic": "Name des Themas, das entfernt werden soll. ", - }, - description="Mit !links remove-topic kann ein Thema aus der Linkliste des Channels entfernt werden.", - command_group="links" - ) - @cmd_links.command(name="remove-topic", aliases=['rt']) - async def cmd_remove_topic(self, ctx, topic): - topic = topic.lower() - - if channel_links := self.links.get(str(ctx.channel.id)): - if channel_links.get(topic): - channel_links.pop(topic) - else: - await ctx.channel.send('Ich konnte das Thema leider nicht finden.') - else: - await ctx.channel.send('Für diesen Channel sind keine Links hinterlegt.') - - self.save_links() - - - @help( - category="links", - syntax="!links edit-link <topic> <title> <new_title> <new_topic?> <new_link?>", - brief="Bearbeitet einen Link.", - parameters={ - "topic": "Name des Themas, aus dem der zu bearbeitende Link stammt. ", - "title": "Titel des Links, der bearbeitet werden soll. ", - "new_title": "Neuer Titel für den geänderten Link. ", - "new_topic": "*(optional)* Neues Thema für den geänderten Link. ", - "new_link": "*(optional)* Der neue Link. " - }, - description="Mit !links edit-link kann ein fehlerhafter oder veralteter Link bearbeitet werden.", - command_group="links" - ) - @cmd_links.command(name="edit-link", aliases=["el"]) - async def cmd_edit_link(self, ctx, topic, title, new_title, new_topic=None, new_link=None): - topic = topic.lower() - - if not new_topic: - new_topic = topic - - if not new_link: - if channel_links := self.links.get(str(ctx.channel.id)): - if topic_links := channel_links.get(topic): - if topic_links.get(title): - new_link = topic_links.get(title) - else: - await ctx.channel.send('Ich konnte den Link leider nicht finden.') - else: - await ctx.channel.send('Ich konnte das Thema leider nicht finden.') - else: - await ctx.channel.send('Für diesen Channel sind keine Links hinterlegt.') - - await self.cmd_remove_link(ctx, topic, title) - await self.cmd_add_link(ctx, new_topic, new_link, new_title) - - @help( - category="links", - syntax="!links edit-topic <topic> <new_topic>", - brief="Bearbeitet den Namen eines Themas.", - parameters={ - "topic": "Name des Themas, das bearbeitet werden soll. ", - "new_topic": "Neuer Name des Themas. " - }, - description="Mit !links edit-topic kann der Name eines Themas geändert werden.", - command_group="links" - ) - @cmd_links.command(name="edit-topic", aliases=["et"]) - async def cmd_edit_topic(self, ctx, topic, new_topic): - topic = topic.lower() - new_topic = new_topic.lower() - if channel_links := self.links.get(str(ctx.channel.id)): - if topic_links := channel_links.get(topic): - channel_links[new_topic] = topic_links - await self.cmd_remove_topic(ctx, topic) - else: - await ctx.channel.send('Ich konnte das Thema leider nicht finden.') - else: - await ctx.channel.send('Für diesen Channel sind keine Links hinterlegt.') - - self.save_links() - - async def cog_command_error(self, ctx, error): - await handle_error(ctx, error) diff --git a/cogs/polls.py b/cogs/polls.py deleted file mode 100644 index 09b9f84a5adf8a514203b7bf88a69f375b787911..0000000000000000000000000000000000000000 --- a/cogs/polls.py +++ /dev/null @@ -1,97 +0,0 @@ -import os - -from disnake.ext import commands - -import utils -from cogs.components.poll.poll import Poll -from cogs.help import help, handle_error, help_category - - -@help_category("poll", "Umfragen", "Erstelle eine Umfrage in einem Kanal oder schlage eine Server-Umfrage vor.", - "Umfragen erstellen oder bearbeiten.") -class Polls(commands.Cog): - def __init__(self, bot): - self.bot = bot - self.poll_sugg_channel = int(os.getenv("DISCORD_POLL_SUGG_CHANNEL")) - - @help( - category="poll", - syntax="!poll <question> <answers...>", - brief="Erstellt eine Umfrage im aktuellen Kanal.", - parameters={ - "question": "Die Frage, die gestellt werden soll (in Anführungszeichen).", - "answers...": "Durch Leerzeichen getrennte Antwortmöglichkeiten (die einzelnen Antworten in Anführungszeichen einschließen)." - }, - example="!poll \"Wie ist das Wetter?\" \"echt gut\" \"weniger gut\" \"Boar nee, nicht schon wieder Regen\"" - ) - @commands.group(name="poll", pass_context=True, invoke_without_command=True) - async def cmd_poll(self, ctx, question, *answers): - """ Create poll """ - - await Poll(self.bot, question, list(answers), ctx.author.id).send_poll(ctx) - - @help( - category="poll", - syntax="!poll suggest <question> <answers...>", - brief="Schlägt eine Umfrage für den Umfrage-Kanal vor.", - parameters={ - "question": "Die Frage die gestellt werden soll (in Anführungszeichen).", - "answers...": "Durch Leerzeichen getrennte Antwortmöglichkeiten (die einzelnen Antworten in Anführungszeichen einschließen)." - }, - example="!poll suggest \"Wie ist das Wetter?\" \"echt gut\" \"weniger gut\" \"Boar nee, nicht schon wieder Regen\"" - ) - @cmd_poll.command(name="suggest") - async def cmd_add_poll(self, ctx, question, *answers): - channel = await self.bot.fetch_channel(self.poll_sugg_channel) - msg = f"<@!{ctx.author.id}> hat folgende Umfrage vorgeschlagen:\nFrage:{question}\n\nAntwortoptionen:\n" - poll = f"!poll \"{question}\"" - - for answer in answers: - msg += f"{answer}\n" - poll += f" \"{answer}\"" - - await channel.send(f"{msg}\n{poll}") - - @help( - category="poll", - brief="Bearbeitet eine bereits vorhandene Umfrage.", - syntax="!poll edit <message_id> <question> <answers...>", - parameters={ - "message_id": "die Message-ID ist der Nachricht mit einem Rechtsklick auf die Umfrage zu entnehmen (Entwicklermodus in Discord müssen aktiv sein).", - "question": "Die Frage, die gestellt werden soll (in Anführungszeichen).", - "answers...": "Durch Leerzeichen getrennte Antwortmöglichkeiten (die einzelnen Antworten in Anführungszeichen einschließen).", - }, - example="!poll edit 838752355595059230 \"Wie ist das Wetter?\" \"echt gut\" \"weniger gut\" \"Boar nee, nicht schon wieder Regen\"", - mod=True - ) - @cmd_poll.command(name="edit") - @commands.check(utils.is_mod) - async def cmd_edit_poll(self, ctx, message_id, question, *answers): - message = await ctx.fetch_message(message_id) - if message: - if message.embeds[0].title == "Umfrage": - old_poll = Poll(self.bot, message=message) - new_poll = Poll(self.bot, question=question, answers=list(answers), author=old_poll.author) - await new_poll.send_poll(ctx.channel, message=message) - else: - ctx.send("Fehler! Umfrage nicht gefunden!") - pass - - @commands.Cog.listener() - async def on_raw_reaction_add(self, payload): - if payload.user_id == self.bot.user.id: - return - - if payload.emoji.name in ["🗑ï¸", "🛑"]: - channel = await self.bot.fetch_channel(payload.channel_id) - message = await channel.fetch_message(payload.message_id) - if len(message.embeds) > 0 and message.embeds[0].title == "Umfrage": - poll = Poll(self.bot, message=message) - if str(payload.user_id) == poll.author: - if payload.emoji.name == "🗑ï¸": - await poll.delete_poll() - else: - await poll.close_poll() - - async def cog_command_error(self, ctx, error): - await handle_error(ctx, error) diff --git a/cogs/roles.py b/cogs/roles.py deleted file mode 100644 index 34e1191a9e473bf002a3e6028e684b3525ef1d87..0000000000000000000000000000000000000000 --- a/cogs/roles.py +++ /dev/null @@ -1,141 +0,0 @@ -import json -import os - -import disnake -import emoji -from disnake.ext import commands - -import utils -from cogs.help import help, handle_error, help_category - - -@help_category("updater", "Updater", - "Diese Kommandos werden zum Updaten von Nachrichten benutzt, die 🌱root automatisch erzeugt.") -@help_category("info", "Informationen", "Kleine Helferlein, um schnell an Informationen zu kommen.") -class Roles(commands.Cog): - def __init__(self, bot): - self.bot = bot - self.roles_file = os.getenv("DISCORD_ROLES_FILE") - self.channel_id = int(os.getenv("DISCORD_ROLE_CHANNEL")) - self.role_message_id = int(os.getenv("DISCORD_ROLE_MSG", "0")) - self.assignable_roles = {} - self.load_roles() - - def load_roles(self): - """ Loads all assignable roles from ROLES_FILE """ - - roles_file = open(self.roles_file, mode='r') - self.assignable_roles = json.load(roles_file) - - def get_key(self, role): - """ Get the key for a given role. This role is used for adding or removing a role from a user. """ - - for key, role_name in self.assignable_roles.items(): - if role_name == role.name: - return key - - @help( - category="info", - brief="Gibt die Mitgliederstatistik aus." - ) - @commands.command(name="stats") - async def cmd_stats(self, ctx): - """ Sends stats in Chat. """ - - guild = ctx.guild - members = await guild.fetch_members().flatten() - answer = f'' - embed = disnake.Embed(title="Statistiken", - description=f'Wir haben aktuell {len(members)} Mitglieder auf diesem Server, verteilt auf folgende Rollen:') - - for role in guild.roles: - if not self.get_key(role): - continue - role_members = role.members - if len(role_members) > 0 and not role.name.startswith("Farbe"): - embed.add_field(name=role.name, value=f'{len(role_members)} Mitglieder', inline=False) - - no_role = 0 - for member in members: - # ToDo Search for study roles only! - if len(member.roles) == 1: - no_role += 1 - - embed.add_field(name="\u200B", value="\u200b", inline=False) - embed.add_field(name="Mitglieder ohne Rolle", value=str(no_role), inline=False) - - await ctx.channel.send(answer, embed=embed) - - @help( - category="updater", - brief="Aktualisiert die Vergabe-Nachricht von Studiengangs-Rollen.", - mod=True - ) - @commands.command("update-roles") - @commands.check(utils.is_mod) - async def cmd_update_degree_program(self, ctx): - channel = await self.bot.fetch_channel(self.channel_id) - message = None if self.role_message_id == 0 else await channel.fetch_message(self.role_message_id) - - embed = disnake.Embed(title="Vergabe von Fakultäts-Rollen", - description="Durch klicken auf die entsprechende Reaktion kannst du dir die damit assoziierte Rolle zuweisen, oder entfernen. Dies funktioniert so, dass ein Klick auf die Reaktion die aktuelle Zuordnung dieser Rolle ändert. Das bedeutet, wenn du die Rolle, die mit :scales: assoziiert ist, schon hast, aber die Reaktion noch nicht ausgewählt hast, dann wird dir bei einem Klick auf die Reaktion diese Rolle wieder weggenommen. ") - - value = f"" - for role_emoji, name in self.assignable_roles.items(): - if emoji.EMOJI_ALIAS_UNICODE_ENGLISH.get(role_emoji): - value += f"{role_emoji} : {name}\n" - else: - value += f"<{role_emoji}> : {name}\n" - - embed.add_field(name="Rollen", - value=value, - inline=False) - - if message: - await message.edit(content="", embed=embed) - await message.clear_reactions() - else: - message = await channel.send(embed=embed) - - for key in self.assignable_roles.keys(): - if role_emoji := emoji.EMOJI_ALIAS_UNICODE_ENGLISH.get(key): - await message.add_reaction(role_emoji) - else: - await message.add_reaction(f"<{key}>") - - @commands.Cog.listener() - async def on_raw_reaction_add(self, payload): - if payload.user_id == self.bot.user.id or payload.message_id != self.role_message_id: - return - - role_emoji = emoji.UNICODE_EMOJI_ALIAS_ENGLISH.get(payload.emoji.name) - - if not role_emoji: - role_emoji = str(payload.emoji)[1:-1] - if role_emoji not in self.assignable_roles: - return - - role_name = self.assignable_roles.get(role_emoji) - guild = await self.bot.fetch_guild(payload.guild_id) - member = await guild.fetch_member(payload.user_id) - channel = await self.bot.fetch_channel(payload.channel_id) - message = await channel.fetch_message(payload.message_id) - roles = member.roles - - await message.remove_reaction(payload.emoji, member) - - for role in roles: - if role.name == role_name: - await member.remove_roles(role) - await utils.send_dm(member, f"Rolle \"{role.name}\" erfolgreich entfernt") - break - else: - guild_roles = guild.roles - - for role in guild_roles: - if role.name == role_name: - await member.add_roles(role) - await utils.send_dm(member, f"Rolle \"{role.name}\" erfolgreich hinzugefügt") - - async def cog_command_error(self, ctx, error): - await handle_error(ctx, error) diff --git a/cogs/support.py b/cogs/support.py deleted file mode 100644 index 4044fbd3a96c1e498f806c13e7112cda8cac7ef2..0000000000000000000000000000000000000000 --- a/cogs/support.py +++ /dev/null @@ -1,28 +0,0 @@ -import io -import os - -import disnake -from disnake.ext import commands - - -class Support(commands.Cog): - def __init__(self, bot): - self.bot = bot - self.channel_id = int(os.getenv("DISCORD_SUPPORT_CHANNEL")) - - @commands.Cog.listener() - async def on_message(self, message): - if message.author == self.bot.user: - return - - if type(message.channel) is disnake.DMChannel: - channel = await self.bot.fetch_channel(self.channel_id) - files = [] - - for attachment in message.attachments: - fp = io.BytesIO() - await attachment.save(fp) - files.append(disnake.File(fp, filename=attachment.filename)) - - await channel.send(f"Support Nachricht von <@!{message.author.id}>:") - await channel.send(message.content, files=files) diff --git a/cogs/text_commands.py b/cogs/text_commands.py deleted file mode 100644 index b1c94dd833a863560e446a6a2aa7aa9ac0cefd61..0000000000000000000000000000000000000000 --- a/cogs/text_commands.py +++ /dev/null @@ -1,303 +0,0 @@ -import json -import os -import random -import re - -import disnake -from disnake.ext import commands - -import utils -from cogs.help import text_command_help, help, handle_error, remove_help_for, help_category - - -@help_category("textcommands", "Text-Kommandos", "", "Alle Werkzeuge zum Anlegen und Verwalten von Textkommandos.") -class TextCommands(commands.Cog): - def __init__(self, bot): - self.bot = bot - self.text_commands = {} - self.cmd_file = os.getenv("DISCORD_TEXT_COMMANDS_FILE") - self.mod_channel_id = int(os.getenv("DISCORD_SUPPORT_CHANNEL")) - self.load_text_commands() - - def load_text_commands(self): - """ Loads all appointments from APPOINTMENTS_FILE """ - - text_commands_file = open(self.cmd_file, mode='r') - self.text_commands = json.load(text_commands_file) - for cmd in self.text_commands: - help_for_cmd = self.text_commands[cmd].get('help') - - if not help_for_cmd: - continue - - brief = help_for_cmd.get('brief') - category = help_for_cmd.get('category') - if not brief: - text_command_help(cmd) - continue - - text_command_help(cmd, brief=brief, category=category) - - def save_text_commands(self): - text_commands_file = open(self.cmd_file, mode='w') - json.dump(self.text_commands, text_commands_file) - - @commands.group(name="commands") - async def cmd_commands(self, ctx): - if not ctx.invoked_subcommand: - await ctx.send("Fehlerhafte Nutzung von `!commands`. " - "Bitte benutze `!help` um herauszufinden, wie dieses Kommando benutzt wird.") - - @help( - category="textcommands", - brief="Listet alle verfügbaren Text-Commands auf oder die Texte, die zu einem Text-Command hinterlegt sind.", - syntax="!commands list <cmd?>", - example="!commands list !motivation", - description="Gibt bei Angabe eines Kommandos (optionaler Parameter cmd) die Texte, die für dieses Kommandu hinterlegt sind. ", - parameters={ - "cmd": "*(optional)* Name des Kommandos, dessen Texte ausgegeben werden sollen." - } - ) - @cmd_commands.command(name="list") - async def cmd_commands_list(self, ctx, cmd=None): - await self.list_commands(ctx, cmd) - - @help( - category="textcommands", - brief="Schlägt ein Text-Kommando oder einen Text für ein bestehendes Text-Kommando vor.", - syntax="!commands add <cmd> <text> <help_message?> <category?>", - example="!command add !newcommand \"immer wenn newcommand aufgerufen wird wird das hier ausgegeben\" \"Hilfetext zu diesem Kommando\"", - description="Ein Text-Kommando ist ein Kommando welches über !<name des textkommandos> aufgerufen werden kann und dann zufällig einen der hinterlegten Texte ausgibt.", - parameters={ - "cmd": "Name des anzulegenden Kommandos (z. B. !horoskop). ", - "text": "in Anführungszeichen eingeschlossene Textnachricht, die ausgegeben werden soll, wenn das Kommando aufgerufen wird (z. B. \"Wassermann: Findet diese Woche wahrscheinlich seinen Dreizack wieder.\").", - "help_message": "*(optional)* Die Hilfenachricht, die bei `!help` für dieses Kommando erscheinen soll (in Anführungszeichen). ", - "category": "*(optional)* gibt die Kategorie an in der das Kommando angezeigt werden soll. " - } - ) - @cmd_commands.command(name="add") - async def cmd_commands_add(self, ctx, cmd, text, help_message=None, category=None): - if utils.is_mod(ctx): - await self.add_command(cmd, text, help_message=help_message, category=category) - else: - await self.suggest_command(ctx, cmd, text, help_message=help_message, category=category) - - @help( - category="textcommands", - brief="Ändert den Text eines Text-Kommandos.", - syntax="!commands edit <cmd> <id> <text>", - example="!command edit !command 1 \"Neuer Text\"", - description="Ändert den Text eines Text-Kommandos an angegebenem Index.", - parameters={ - "cmd": "Name des anzulegenden Kommandos (z. B. !horoskop). ", - "id": "Index des zu ändernden Texts.", - "text": "in Anführungszeichen eingeschlossene Textnachricht, die ausgegeben werden soll, wenn das Kommando aufgerufen wird (z. B. \"Wassermann: Findet diese Woche wahrscheinlich seinen Dreizack wieder.\").", - }, - mod=True - ) - @cmd_commands.command(name="edit") - @commands.check(utils.is_mod) - async def cmd_command_edit(self, ctx, cmd, id, text): - texts = self.text_commands.get(cmd).get('data') - - if texts: - i = int(id) - if 0 <= i < len(texts): - texts[i] = text - await ctx.send(f"Text {i} für Command {cmd} wurde erfolgreich geändert") - self.save_text_commands() - else: - await ctx.send(f"Ungültiger Index") - else: - await ctx.send("Command {cmd} nicht vorhanden!") - - @help( - category="textcommands", - brief="Entfernt einen Text oder ein gesamtes Text-Kommando.", - syntax="!commands remove <cmd> <id?>", - example="!command remove !command 0", - description="Entfernt den Text des angegebenen Text-Kommandos an entsprechendem Index. War das der einzige Text für dieses Text-Kommando, wird das gesamte Kommando entfernt. Wird kein Index übergeben, so wird ebenfalls das gesamte Text-Kommando entfernt.", - parameters={ - "cmd": "Name des zu entfernenden Kommandos (z. B. !horoskop). ", - "id": "*(optional)* Id des zu löschenden Texts" - }, - mod=True - ) - @cmd_commands.command(name="remove") - @commands.check(utils.is_mod) - async def cmd_command_remove(self, ctx, cmd, id=None): - texts = self.text_commands.get(cmd).get('data') - - if texts: - if id: # checkt erst, ob man lediglich einen Eintrag (und nicht das ganze Command) löschen möchte - i = int(id) - if 0 <= i < len(texts): # schließt Aufrufe von Indizen aus, die außerhalb des Felds wären - del texts[i] - await ctx.send(f"Text {i} für Command {cmd} wurde erfolgreich entfernt") - - if len(texts) == 0: - self.text_commands.pop(cmd) - - self.save_text_commands() - else: - await ctx.send(f"Ungültiger Index") - else: # jetzt kommt man zum vollständigen command removal (ursprünglich "remove-text-command") - # Hier könnte eine Bestätigung angefordert werden (Möchtest du wirklich das Command vollständig löschen? ðŸ‘👎) - if cmd in self.text_commands: - self.text_commands.pop(cmd) - remove_help_for(re.sub(r"^!", "", cmd)) - await ctx.send(f"Text Command {cmd} wurde erfolgreich entfernt.") - self.save_text_commands() - else: - await ctx.send(f"Text Command {cmd} nicht vorhanden!") - else: - await ctx.send("Command {cmd} nicht vorhanden!") - - @cmd_commands.command(name="edit-help") - @commands.check(utils.is_mod) - async def cmd_command_edit_help(self, ctx, cmd, help_message): - help_object = None - try: - cmd = re.sub(r"^!*", "!", cmd) - help_object = self.text_commands.get(cmd).get('help') - except: - pass - - if not help_object: - self.text_commands[cmd]['help'] = {} - help_object = self.text_commands[cmd]['help'] - - help_object['brief'] = help_message - text_command_help(cmd, brief=help_message, category=help_object.get('category')) - self.save_text_commands() - - await ctx.send(f"[{cmd}] => Hilfe [{help_message}] erfolgreich hinzugefügt.") - - @cmd_commands.command(name="edit-category") - @commands.check(utils.is_mod) - async def cmd_command_edit_category(self, ctx, cmd, category): - help_object = None - try: - help_object = self.text_commands.get(re.sub("^!*", "!", cmd)).get('help') - except: - pass - - if not help_object: - help_object = {} - - help_object['category'] = category - text_command_help(cmd, category=category, brief=help_object.get('brief')) - self.save_text_commands() - - await ctx.send(f"[{cmd}] => Erfolgreich auf Kategorie [{category}] geändert.") - - async def list_commands(self, ctx, cmd=None): - if cmd and not self.text_commands.get(cmd): - await ctx.send(f"{cmd} ist kein verfügbares Text-Command") - return - - answer = f"Text Commands:\n" if cmd is None else f"Für {cmd} hinterlegte Texte:\n" - cmd_list = list(self.text_commands.keys()) if cmd is None else self.text_commands.get(cmd).get('data') - - for i in range(len(cmd_list)): - text = cmd_list[i] - if len(answer) + len(text) > 2000: - await ctx.send(answer) - answer = f"" - - answer += f"{i}: {text}\n" - - await ctx.send(answer) - - async def add_command(self, cmd, text, help_message=None, category=None): - mod_channel = await self.bot.fetch_channel(self.mod_channel_id) - command = self.get_or_init_command(cmd) - texts = command.get("data") - texts.append(text) - - if help_message and not command.get("help"): - command["help"] = {"brief": help_message} - if category: - command.get("help")["category"] = category - - await mod_channel.send(f"[{cmd}] => [{text}] erfolgreich hinzugefügt.") - - self.save_text_commands() - - async def suggest_command(self, ctx, cmd, text, help_message=None, category=None): - mod_channel = await self.bot.fetch_channel(self.mod_channel_id) - command = self.text_commands.get(cmd) - title = "Vorschlag für neuen Command Text" if command else "Vorschlag für neues Command" - - embed = disnake.Embed(title=title, - description=f"<@!{ctx.author.id}> hat folgenden Vorschlag eingereicht.\n" - f"👠um den Vorschlag anzunehmen\n" - f"👎 um den Vorschlag abzulehnen") - embed.add_field(name="\u200b", value="\u200b") - embed.add_field(name="Command", value=f'{cmd}', inline=False) - embed.add_field(name="Text", value=f'{text}', inline=False) - if help_message: - embed.add_field(name="Hilfetext", value=f'{help_message}', inline=False) - if category: - embed.add_field(name="Kategorie", value=f'{category}', inline=False) - - message = await mod_channel.send(embed=embed) - await message.add_reaction("ðŸ‘") - await message.add_reaction("👎") - await utils.send_dm(ctx.author, - "Dein Vorschlag wurde den Mods zur Genehmigung vorgelegt. " - "Sobald darüber entschieden wurde, erhältst du eine Benachrichtigung.") - - def get_or_init_command(self, cmd): - if command := self.text_commands.get(cmd): - return command - - self.text_commands[cmd] = {"data": []} - return self.text_commands.get(cmd) - - async def handle_command_reaction(self, message, approved=True): - embed = message.embeds[0] - fields = {field.name: field.value for field in embed.fields} - cmd = fields.get("Command") - text = fields.get("Text") - help_message = fields.get("Hilfetext") - category = fields.get("Kategorie") - member = await message.guild.fetch_member(embed.description[3:21]) - - if approved: - await self.add_command(cmd, text, help_message=help_message, category=category) - await utils.send_dm(member, - f"Herzlichen Glückwunsch, dein Vorschlag für {cmd} wurde angenommen:\n{text}") - else: - await utils.send_dm(member, - f"Vielen Dank, dass du dir Gedanken darüber machst, wie man 🌱root mit neuen Textkommandos noch nützlicher für alle machen kann.\n" \ - f"Es können allerdings nicht alle Einreichungen angenommen werden, weswegen dein Vorschlag für {cmd} leider abgelehnt wurde:\n{text}\n" \ - f"Eine Vertreterin des Mod-Teams wird sich in Kürze mit dir in Verbindung setzen und dir erklären, was die Beweggründe der Ablehnung sind.") - await message.delete() - - @commands.Cog.listener() - async def on_raw_reaction_add(self, payload): - if payload.user_id == self.bot.user.id: - return - - if payload.emoji.name in ["ðŸ‘", "👎"] and payload.channel_id == self.mod_channel_id: - channel = await self.bot.fetch_channel(payload.channel_id) - message = await channel.fetch_message(payload.message_id) - if len(message.embeds) > 0 and message.embeds[0].title in ["Vorschlag für neuen Command Text", - "Vorschlag für neues Command"]: - await self.handle_command_reaction(message, approved=(payload.emoji.name == "ðŸ‘")) - - async def cog_command_error(self, ctx, error): - await handle_error(ctx, error) - - @commands.Cog.listener(name="on_message") - async def process_text_commands(self, message): - if message.author == self.bot.user: - return - - cmd = message.content.split(" ")[0] - cmd_object = self.text_commands.get(cmd) - if cmd_object: - texts = cmd_object.get('data') - if texts: - await message.channel.send(random.choice(texts)) diff --git a/cogs/timer.py b/cogs/timer.py deleted file mode 100644 index 96620ac9d937223baffefe87ba5a0288be200e0b..0000000000000000000000000000000000000000 --- a/cogs/timer.py +++ /dev/null @@ -1,305 +0,0 @@ -import json -import os -import random -from asyncio import sleep -from copy import deepcopy -from datetime import datetime, timedelta - -import disnake -from disnake import MessageInteraction, ApplicationCommandInteraction -from disnake.ext import commands, tasks -from disnake.ui import Button - -from views import timer_view - - -class Timer(commands.Cog): - - def __init__(self, bot): - self.bot = bot - self.guild_id = int(os.getenv('DISCORD_GUILD')) - self.default_names = ["Noam Chomsky", "Leonardo da Vinci", "René Descartes", "Hypatia von Alexandria", - "Fritz Bauer", "Rosalind Franklin", "Marie-Anne Paulze Lavoisier", - "Marie SkÅ‚odowska Curie", "Umberto Eco", "Ada Lovelace", "Clinton Richard Dawkins", - "Daniel Kahneman", "Judith Rich Harris", "Laura Maria Caterina Bassi"] - self.timer_file_path = os.getenv("DISCORD_TIMER_FILE") - self.running_timers = self.load() - self.load() - self.run_timer.start() - - def load(self): - with open(self.timer_file_path, mode='r') as timer_file: - return json.load(timer_file) - - def save(self): - with open(self.timer_file_path, mode='w') as timer_file: - json.dump(self.running_timers, timer_file) - - def get_view(self, disabled=False): - view = timer_view.TimerView(callback=self.on_button_click) - - if disabled: - view.disable() - - return view - - async def on_button_click(self, button: Button, interaction: MessageInteraction): - custom_id = button.custom_id - - if custom_id == timer_view.SUBSCRIBE: - await self.on_subscribe(button, interaction) - elif custom_id == timer_view.UNSUBSCRIBE: - await self.on_unsubscribe(button, interaction) - elif custom_id == timer_view.SKIP: - await self.on_skip(button, interaction) - elif custom_id == timer_view.RESTART: - await self.on_restart(button, interaction) - elif custom_id == timer_view.STOP: - await self.on_stop(button, interaction) - - async def on_subscribe(self, button: Button, interaction: MessageInteraction): - msg_id = str(interaction.message.id) - if timer := self.running_timers.get(msg_id): - if str(interaction.author.id) not in timer['registered']: - timer['registered'].append(str(interaction.author.id)) - self.save() - name, status, wt, bt, remaining, registered, _ = self.get_details(msg_id) - embed = self.create_embed(name, status, wt, bt, remaining, registered) - await interaction.message.edit(embed=embed, view=self.get_view()) - await interaction.response.send_message("Du hast dich erfolgreich angemeldet", ephemeral=True) - else: - await interaction.response.send_message("Du bist bereits angemeldet.", ephemeral=True) - else: - await interaction.response.send_message("Etwas ist schiefgelaufen...", ephemeral=True) - - async def on_unsubscribe(self, button: Button, interaction: MessageInteraction): - msg_id = str(interaction.message.id) - if timer := self.running_timers.get(msg_id): - registered = timer['registered'] - if str(interaction.author.id) in registered: - if len(registered) == 1: - await self.on_stop(button, interaction) - return - else: - timer['registered'].remove(str(interaction.author.id)) - self.save() - name, status, wt, bt, remaining, registered, _ = self.get_details(msg_id) - embed = self.create_embed(name, status, wt, bt, remaining, registered) - await interaction.message.edit(embed=embed, view=self.get_view()) - await interaction.response.send_message("Du hast dich erfolgreich abgemeldet", ephemeral=True) - else: - await interaction.response.send_message("Du warst gar nicht angemeldet.", ephemeral=True) - else: - await interaction.response.send_message("Etwas ist schiefgelaufen...", ephemeral=True) - - async def on_skip(self, button: Button, interaction: MessageInteraction): - msg_id = str(interaction.message.id) - if timer := self.running_timers.get(msg_id): - registered = timer['registered'] - if str(interaction.author.id) in timer['registered']: - new_phase = await self.switch_phase(msg_id) - if new_phase == "Pause": - await self.make_sound(registered, 'groove-intro.mp3') - else: - await self.make_sound(registered, 'roll_with_it-outro.mp3') - await interaction.response.send_message("Erfolgreich übersprungen", ephemeral=True) - else: - await interaction.response.send_message("Nur angemeldete Personen können den Timer bedienen.", - ephemeral=True) - else: - await interaction.response.send_message("Etwas ist schiefgelaufen...", ephemeral=True) - - async def on_restart(self, button: Button, interaction: MessageInteraction): - msg_id = str(interaction.message.id) - if timer := self.running_timers.get(msg_id): - registered = timer['registered'] - if str(interaction.author.id) in timer['registered']: - timer['status'] = 'Arbeiten' - timer['remaining'] = timer['working_time'] - self.save() - - await self.edit_message(msg_id) - await self.make_sound(registered, 'roll_with_it-outro.mp3') - await interaction.response.send_message("Erfolgreich neugestartet", ephemeral=True) - else: - await interaction.response.send_message("Nur angemeldete Personen können den Timer neu starten.", - ephemeral=True) - else: - await interaction.response.send_message("Etwas ist schiefgelaufen...", ephemeral=True) - - async def on_stop(self, button: Button, interaction: MessageInteraction): - msg_id = str(interaction.message.id) - if timer := self.running_timers.get(msg_id): - registered = timer['registered'] - if str(interaction.author.id) in timer['registered']: - mentions = self.get_mentions(msg_id) - timer['status'] = "Beendet" - timer['remaining'] = 0 - timer['registered'] = [] - - await interaction.response.send_message("Erfolgreich beendet", ephemeral=True) - if new_msg_id := await self.edit_message(msg_id, mentions=mentions): - await self.make_sound(registered, 'applause.mp3') - self.running_timers.pop(new_msg_id) - self.save() - else: - # Reply with a hidden message - await interaction.response.send_message("Nur angemeldete Personen können den Timer beenden.", - ephemeral=True) - else: - await interaction.response.send_message("Etwas ist schiefgelaufen...", ephemeral=True) - - def create_embed(self, name, status, working_time, break_time, remaining, registered): - color = disnake.Colour.green() if status == "Arbeiten" else 0xFFC63A if status == "Pause" else disnake.Colour.red() - descr = f"👠beim Timer anmelden\n\n" \ - f"👎 beim Timer abmelden\n\n" \ - f"â© Phase überspringen\n\n" \ - f"🔄 Timer neu starten\n\n" \ - f"🛑 Timer beenden\n" - zeiten = f"{working_time} Minuten Arbeiten\n{break_time} Minuten Pause" - remaining_value = f"{remaining} Minuten" - endzeit = (datetime.now() + timedelta(minutes=remaining)).strftime("%H:%M") - end_value = f" [bis {endzeit} Uhr]" if status != "Beendet" else "" - user_list = [self.bot.get_user(int(user_id)) for user_id in registered] - angemeldet_value = ", ".join([user.mention for user in user_list]) - - embed = disnake.Embed(title=name, - description=f'Jetzt: {status}', - color=color) - embed.add_field(name="Bedienung:", value=descr, inline=False) - embed.add_field(name="Zeiten:", value=zeiten, inline=False) - embed.add_field(name="verbleibende Zeit:", value=remaining_value + end_value, inline=False) - embed.add_field(name="angemeldete User:", value=angemeldet_value if registered else "-", inline=False) - - return embed - - @commands.slash_command(name="timer", description="Erstelle deine persönliche Eieruhr") - async def cmd_timer(self, interaction: ApplicationCommandInteraction, working_time: int = 25, - break_time: int = 5, - name: str = None): - name = name if name else random.choice(self.default_names) - remaining = working_time - status = "Arbeiten" - registered = [str(interaction.author.id)] - - embed = self.create_embed(name, status, working_time, break_time, remaining, registered) - await interaction.response.send_message(embed=embed, view=self.get_view()) - message = await interaction.original_message() - - self.running_timers[str(message.id)] = {'name': name, - 'status': status, - 'working_time': working_time, - 'break_time': break_time, - 'remaining': remaining, - 'registered': registered, - 'channel': interaction.channel_id} - self.save() - await self.make_sound(registered, 'roll_with_it-outro.mp3') - - - async def switch_phase(self, msg_id): - if timer := self.running_timers.get(msg_id): - if timer['status'] == "Arbeiten": - timer['status'] = "Pause" - timer['remaining'] = timer['break_time'] - elif timer['status'] == "Pause": - timer['status'] = "Arbeiten" - timer['remaining'] = timer['working_time'] - else: - self.running_timers.pop(msg_id) - return "Beendet" - self.save() - - if new_msg_id := await self.edit_message(msg_id): - return self.running_timers[new_msg_id]['status'] - else: - return "Beendet" - - def get_details(self, msg_id): - name = self.running_timers[msg_id]['name'] - status = self.running_timers[msg_id]['status'] - wt = self.running_timers[msg_id]['working_time'] - bt = self.running_timers[msg_id]['break_time'] - remaining = self.running_timers[msg_id]['remaining'] - registered = self.running_timers[msg_id]['registered'] - channel = self.running_timers[msg_id]['channel'] - return name, status, wt, bt, remaining, registered, channel - - async def edit_message(self, msg_id, mentions=None, create_new=True): - if timer := self.running_timers.get(msg_id): - channel_id = timer['channel'] - channel = await self.bot.fetch_channel(int(channel_id)) - try: - msg = await channel.fetch_message(int(msg_id)) - - name, status, wt, bt, remaining, registered, _ = self.get_details(msg_id) - embed = self.create_embed(name, status, wt, bt, remaining, registered) - - if create_new: - await msg.delete() - if not mentions: - mentions = self.get_mentions(msg_id) - if status == "Beendet": - new_msg = await channel.send(mentions, embed=embed, - view=self.get_view(disabled=True)) - else: - new_msg = await channel.send(mentions, embed=embed, view=self.get_view()) - self.running_timers[str(new_msg.id)] = self.running_timers[msg_id] - self.running_timers.pop(msg_id) - self.save() - msg = new_msg - else: - await msg.edit(embed=embed, view=self.get_view()) - return str(msg.id) - except disnake.errors.NotFound: - self.running_timers.pop(msg_id) - self.save() - return None - - def get_mentions(self, msg_id): - guild = self.bot.get_guild(self.guild_id) - registered = self.running_timers.get(msg_id)['registered'] - members = [guild.get_member(int(user_id)) for user_id in registered] - mentions = ", ".join([member.mention for member in members]) - return mentions - - async def make_sound(self, registered_users, filename): - guild = self.bot.get_guild(self.guild_id) - for user_id in registered_users: - member = guild.get_member(int(user_id)) - if member.voice: - channel = member.voice.channel - if channel: # If user is in a channel - try: - voice_client = await channel.connect() - voice_client.play(disnake.FFmpegPCMAudio(f'cogs/sounds/{filename}')) - await sleep(3) - except disnake.errors.ClientException as e: - print(e) - for vc in self.bot.voice_clients: - await vc.disconnect() - break - - @tasks.loop(minutes=1) - async def run_timer(self): - timers_copy = deepcopy(self.running_timers) - for msg_id in timers_copy: - registered = self.running_timers[msg_id]['registered'] - self.running_timers[msg_id]['remaining'] -= 1 - if self.running_timers[msg_id]['remaining'] <= 0: - new_phase = await self.switch_phase(msg_id) - if new_phase == "Pause": - await self.make_sound(registered, 'groove-intro.mp3') - elif new_phase == "Arbeiten": - await self.make_sound(registered, 'roll_with_it-outro.mp3') - else: - await self.edit_message(msg_id, create_new=False) - - @run_timer.before_loop - async def before_timer(self): - await sleep(60) - - @cmd_timer.error - async def timer_error(self, ctx, error): - await ctx.send("Das habe ich nicht verstanden. Die Timer-Syntax ist:\n" - "`!timer <learning-time?> <break-time?> <name?>`\n") diff --git a/cogs/welcome.py b/cogs/welcome.py deleted file mode 100644 index 573cc770c5ae90d765ec3bd8916b459ac14c0422..0000000000000000000000000000000000000000 --- a/cogs/welcome.py +++ /dev/null @@ -1,74 +0,0 @@ -import os - -import disnake -from disnake.ext import commands - -import utils -from cogs.help import help, handle_error - - -class Welcome(commands.Cog): - def __init__(self, bot): - self.bot = bot - self.channel_id = int(os.getenv("DISCORD_WELCOME_CHANNEL")) - self.message_id = int(os.getenv("DISCORD_WELCOME_MSG", "0")) - - @help( - category="updater", - brief="aktualisiert die Willkommensnachricht.", - mod=True - ) - @commands.command("update-welcome") - @commands.check(utils.is_mod) - async def cmd_update_welcome(self, ctx): - channel = await self.bot.fetch_channel(self.channel_id) - message = None if self.message_id == 0 else await channel.fetch_message(self.message_id) - - embed = disnake.Embed(title=":rocket: __FernUni Föderation__ :rocket:", - description="Willkommen auf dem interdisziplinären Server von und für FernUni-Studierende! Hier können FernUni-Studierende aus allen Fachrichtungen in Austausch treten, Ideen austauschen und gemeinsam an Projekten arbeiten: viel Potenzial für gegenseitige Bereicherung!") - - embed.add_field(name=":sparkles: Entstehung", - value="Die Betreiber:innen der verschiedenen FernUni-Discordserver haben sich vernetzt, um zusammenzuarbeiten. Aus mehreren Richtungen wurde der Wunsch nach einer fachübergreifender Plattform geäußert und daraufhin ist dieser Föderationsserver entstanden!", - inline=False) - - embed.add_field(name=":robot: Server-Bot", - value=f"Ich bin root. Beim <#{os.getenv('DISCORD_BOTUEBUNGSPLATZ_CHANNEL')}> kannst du meine verschiedenen Befehle ausprobieren. Wenn du dort `!help` schreibst, sende ich dir per Direktnachricht einen Ãœberblick meiner Funktionen.", - inline=False) - - embed.add_field(name=":placard: Rollen", - value=f"Du kannst dir eine Discord-Rolle bei <#{os.getenv('DISCORD_ROLE_CHANNEL')}> aussuchen, die deine Fakultätszugehörigkeit widerspiegelt.", - inline=False) - - embed.add_field(name=":scroll: Regeln", - value="Verhalte dich respektvoll und versuche Rücksicht auf deine Mitmenschen zu nehmen. Außerdem sind - wie überall auf Discord - diese Community-Richtlinien zu beachten: <https://discord.com/guidelines>.", - inline=False) - - embed.add_field(name=":link: Einladungslink", - value=f"Mitstudierende kannst du mit folgendem Link einladen: {os.getenv('DISCORD_INVITE_LINK')}.", - inline=False) - - embed.add_field(name="\u200b", - value="Viel Vergnügen auf dem Server!", - inline=False) - - if message: - await message.edit(content="", embed=embed) - else: - await channel.send(embed=embed) - - @commands.Cog.listener() - async def on_member_join(self, member): - await utils.send_dm(member, - f"Herzlich Willkommen bei der FernUni Föderation! Alle notwendigen Informationen, die du für den Einstieg brauchst, sowie die wenige Regeln, die aufgestellt sind, findest du in <#{self.channel_id}>\n" - f"Du darfst dir außerdem gerne im Channel <#{os.getenv('DISCORD_ROLE_CHANNEL')}> die passende Rolle zu deiner Fakultät zuweisen lassen. \n\n" - f"Falls du Fragen haben solltest, kannst du sie gerne bei der <#{os.getenv('DISCORD_OFFTOPIC_CHANNEL')}> stellen. Wenn du bei etwas Hilfe vom Moderationsteam brauchst, schreib mir doch eine private Nachricht, ich werde sie weiterleiten :writing_hand:.\n\n" - f"Viel Spaß beim erkunden des Servers und bis bald!") - - @commands.Cog.listener() - async def on_member_update(self, before, after): - if before.pending != after.pending and not after.pending: - channel = await self.bot.fetch_channel(int(os.getenv("DISCORD_GREETING_CHANNEL"))) - await channel.send(f"Willkommen <@!{before.id}> im Kreise der FernUni-Studierenden :student:") - - async def cog_command_error(self, ctx, error): - await handle_error(ctx, error) diff --git a/data/.gitignore b/data/.gitignore deleted file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 diff --git a/extensions/appointments.py b/extensions/appointments.py new file mode 100644 index 0000000000000000000000000000000000000000..5c43301046b86956a4e54f9f0e04cc5a19205d54 --- /dev/null +++ b/extensions/appointments.py @@ -0,0 +1,118 @@ +import asyncio +from datetime import datetime, timedelta + +from discord import app_commands, errors, Interaction +from discord.ext import tasks, commands + +from models import Appointment +from views.appointment_view import AppointmentView + + +async def send_notification(appointment, channel): + message = f"Benachrichtigung!\nDer Termin \"{appointment.title}\" startet " + + if appointment.reminder_sent: + message += f"jetzt! :loudspeaker: " + else: + message += f"<t:{int(appointment.date_time.timestamp())}:R>." + + message += f"\n" + message += " ".join([f"<@!{str(attendee.member_id)}>" for attendee in appointment.attendees]) + + await channel.send(message) + + +@app_commands.guild_only() +class Appointments(commands.GroupCog, name="appointments", description="Handle Appointments in Channels"): + def __init__(self, bot): + self.bot = bot + self.timer.start() + + @tasks.loop(minutes=1) + async def timer(self): + for appointment in Appointment.select().order_by(Appointment.channel): + now = datetime.now() + date_time = appointment.date_time + remind_at = date_time - timedelta( + minutes=appointment.reminder) if not appointment.reminder_sent else date_time + + if now >= remind_at: + try: + channel = await self.bot.fetch_channel(appointment.channel) + message = await channel.fetch_message(appointment.message) + await send_notification(appointment, channel) + + if appointment.reminder_sent: + await message.delete() + + if appointment.recurring == 0: + appointment.delete_instance(recursive=True) + else: + new_date_time = appointment.date_time + timedelta(days=appointment.recurring) + reminder_sent = appointment.reminder == 0 + Appointment.update(reminder_sent=reminder_sent, date_time=new_date_time).where( + Appointment.id == appointment.id).execute() + updated_appointment = Appointment.get(Appointment.id == appointment.id) + new_message = await channel.send(embed=updated_appointment.get_embed(), + view=AppointmentView()) + Appointment.update(message=new_message.id).where(Appointment.id == appointment.id).execute() + else: + Appointment.update(reminder_sent=True).where(Appointment.id == appointment.id).execute() + except errors.NotFound: + appointment.delete_instance(recursive=True) + + @timer.before_loop + async def before_timer(self): + await asyncio.sleep(60 - datetime.now().second) + + @app_commands.command(name="add", description="Füge dem Kanal einen neuen Termin hinzu.") + @app_commands.describe(date="Tag des Termins im Format TT.MM.JJJJ", time="Uhrzeit des Termins im Format HH:MM", + reminder="Wie viele Minuten bevor der Termin startet, soll eine Erinnerung verschickt werden?", + title="Titel des Termins (so wie er dann evtl. auch im Kalender steht).", + description="Detailliertere Beschreibung, was gemacht werden soll.", + recurring="In welchem Intervall (in Tagen) soll der Termin wiederholt werden?") + async def cmd_add_appointment(self, interaction: Interaction, date: str, time: str, reminder: int, title: str, + description: str = "", recurring: int = 0): + """ Add an appointment to a channel """ + channel = interaction.channel + author_id = interaction.user.id + try: + date_time = datetime.strptime(f"{date} {time}", "%d.%m.%Y %H:%M") + except ValueError: + await channel.send("Fehler! Ungültiges Datums und/oder Zeit Format!") + return + + appointment = Appointment.create(channel=channel.id, message=0, date_time=date_time, reminder=reminder, + title=title, description=description, author=author_id, recurring=recurring, + reminder_sent=reminder == 0) + + await interaction.response.send_message(embed=appointment.get_embed(), view=AppointmentView()) + message = await interaction.original_response() + Appointment.update(message=message.id).where(Appointment.id == appointment.id).execute() + + @app_commands.command(name="list", description="Listet alle Termine dieses Kanals auf.") + @app_commands.describe(show_all="Zeige die Liste für alle an.") + async def cmd_appointments_list(self, interaction: Interaction, show_all: bool = False): + """ List (and link) all Appointments in the current channel """ + await interaction.response.defer(ephemeral=not show_all) + + appointments = Appointment.select().where(Appointment.channel == interaction.channel_id) + if appointments: + answer = f'Termine dieses Channels:\n' + + for appointment in appointments: + try: + message = await interaction.channel.fetch_message(appointment.message) + answer += f'<t:{int(appointment.date_time.timestamp())}:F>: {appointment.title} => ' \ + f'{message.jump_url}\n' + except errors.NotFound: + appointment.delete_instance(recursive=True) + + await interaction.edit_original_response(content=answer) + else: + await interaction.edit_original_response(content="Für diesen Channel existieren derzeit keine Termine") + + +async def setup(bot: commands.Bot) -> None: + await bot.add_cog(Appointments(bot)) + bot.add_view(AppointmentView()) diff --git a/extensions/links.py b/extensions/links.py new file mode 100644 index 0000000000000000000000000000000000000000..962050d9c57c00397de17bae1a15fb3ae48cd555 --- /dev/null +++ b/extensions/links.py @@ -0,0 +1,129 @@ +import discord +from discord import app_commands, Interaction +from discord.ext import commands + +from models import Topic, Link + + +@app_commands.guild_only() +class Links(commands.GroupCog, name="links", description="Linkverwaltung für Kanäle."): + def __init__(self, bot): + self.bot = bot + + @app_commands.command(name="show", description="Zeige Links für diesen Kanal an.") + @app_commands.describe(topic="Zeige nur Links für dieses Thema an.", public="Zeige die Linkliste für alle.") + async def cmd_links(self, interaction: Interaction, topic: str = None, public: bool = False): + await interaction.response.defer(ephemeral=not public) + + if not Topic.has_links(interaction.channel_id): + await interaction.edit_original_response(content="Für diesen Channel sind noch keine Links hinterlegt.") + return + if topic and not Topic.has_links(interaction.channel_id, topic=topic): + await interaction.edit_original_response(content=f"Für das Thema `{topic}` sind in diesem Channel keine " + f"Links hinterlegt. Versuch es noch mal mit einem anderen " + f"Thema, oder lass dir mit `/links show` alle Links in " + f"diesem Channel ausgeben") + return + + embed = discord.Embed(title=f"Folgende Links sind in diesem Channel hinterlegt:\n") + + for topic in Topic.get_topics(interaction.channel_id, topic=topic): + topic.append_field(embed) + + await interaction.edit_original_response(embed=embed) + + @app_commands.command(name="add", description="Füge einen neuen Link hinzu.") + @app_commands.describe(topic="Thema, zu dem dieser Link hinzugefügt werden soll.", + link="Link, der hinzugefügt werden soll.", title="Titel des Links.") + async def cmd_add_link(self, interaction: Interaction, topic: str, link: str, title: str): + await interaction.response.defer(ephemeral=True) + topic = topic.lower() + topic_entity = Topic.get_or_create(channel=interaction.channel_id, name=topic) + Link.create(link=link, title=title, topic=topic_entity[0].id) + await interaction.edit_original_response(content="Link erfolgreich hinzugefügt.") + + @app_commands.command(name="remove-link", description="Einen Link entfernen.") + @app_commands.describe(topic="Theme zu dem der zu entfernende Link gehört.", + title="Titel des zu entfernenden Links.") + async def cmd_remove_link(self, interaction: Interaction, topic: str, title: str): + await interaction.response.defer(ephemeral=True) + topic = topic.lower() + + if not Topic.has_links(interaction.channel_id): + await interaction.edit_original_response(content="Für diesen Channel sind noch keine Links hinterlegt.") + return + if topic_entity := Topic.get_or_none(Topic.channel == interaction.channel_id, Topic.name == topic): + if link := Link.get_or_none(Link.title == title, Link.topic == topic_entity.id): + link.delete_instance(recursive=True) + await interaction.edit_original_response(content=f'Link {title} entfernt') + else: + await interaction.edit_original_response(content='Ich konnte den Link leider nicht finden.') + else: + await interaction.edit_original_response(content='Ich konnte das Thema leider nicht finden.') + return + + @app_commands.command(name="remove-topic", description="Ein Thema mit allen zugehörigen Links entfernen.") + @app_commands.describe(topic="Zu entfernendes Thema.") + async def cmd_remove_topic(self, interaction: Interaction, topic: str): + await interaction.response.defer(ephemeral=True) + topic = topic.lower() + + if not Topic.has_links(interaction.channel_id): + await interaction.edit_original_response(content="Für diesen Channel sind noch keine Links hinterlegt.") + return + if topic_entity := Topic.get_or_none(Topic.channel == interaction.channel_id, Topic.name == topic): + topic_entity.delete_instance(recursive=True) + await interaction.edit_original_response(content=f'Thema {topic} mit allen zugehörigen Links entfernt') + else: + await interaction.edit_original_response(content='Ich konnte das Thema leider nicht finden.') + return + + @app_commands.command(name="edit-link", description="Einen bestehenden Link in der Liste bearbeiten.") + @app_commands.describe(topic="Thema zu dem der zu bearbeitende Link gehört.", + title="Titel des zu bearbeitenden Links.", new_title="Neuer Titel des Links.", + new_topic="Neues Thema des Links.", new_link="Neuer Link.") + async def cmd_edit_link(self, interaction: Interaction, topic: str, title: str, new_title: str, + new_topic: str = None, new_link: str = None): + await interaction.response.defer(ephemeral=True) + topic = topic.lower() + new_title = title if not new_title else new_title + new_topic = topic if not new_topic else new_topic.lower() + + if not Topic.has_links(interaction.channel_id): + await interaction.edit_original_response(content="Für diesen Channel sind noch keine Links hinterlegt.") + return + + if topic_entity := Topic.get_or_none(Topic.channel == interaction.channel_id, Topic.name == topic): + if link := Link.get_or_none(Link.title == title, Link.topic == topic_entity.id): + new_link = link.link if not new_link else new_link + new_topic_entity = Topic.get_or_create(channel=interaction.channel_id, name=new_topic) + link.update(title=new_title, link=new_link, topic=new_topic_entity[0].id).where( + Link.id == link.id).execute() + if len(list(topic_entity.links)) == 0: + topic_entity.delete_instance() + await interaction.edit_original_response(content=f'Link {title} bearbeitet.') + else: + await interaction.edit_original_response(content='Ich konnte den Link leider nicht finden.') + else: + await interaction.edit_original_response(content='Ich konnte das Thema leider nicht finden.') + + @app_commands.command(name="edit-topic", description="Thema bearbeiten.") + @app_commands.describe(topic="Zu bearbeitendes Thema", new_topic="Neues Thema") + async def cmd_edit_topic(self, interaction: Interaction, topic: str, new_topic: str): + await interaction.response.defer(ephemeral=True) + topic = topic.lower() + new_topic = new_topic.lower() + + if not Topic.has_links(interaction.channel_id): + await interaction.edit_original_response(content="Für diesen Channel sind noch keine Links hinterlegt.") + return + + if topic_entity := Topic.get_or_none(Topic.channel == interaction.channel_id, Topic.name == topic): + topic_entity.update(name=new_topic).execute() + await interaction.edit_original_response(content=f"Thema {topic} bearbeitet.") + else: + await interaction.edit_original_response(content='Ich konnte das Thema leider nicht finden.') + + +async def setup(bot: commands.Bot) -> None: + await bot.add_cog(Links(bot)) diff --git a/extensions/mod_mail.py b/extensions/mod_mail.py new file mode 100644 index 0000000000000000000000000000000000000000..880fd4b38be63f62dac97a0510b24fc6aba97a88 --- /dev/null +++ b/extensions/mod_mail.py @@ -0,0 +1,32 @@ +import io + +import discord +from discord.ext import commands + + +class ModMail(commands.Cog): + def __init__(self, bot): + self.bot = bot + self.config = bot.config["extensions"][__name__.split(".")[-1]] + + @commands.Cog.listener() + async def on_message(self, message): + if message.author == self.bot.user: + return + + if type(message.channel) is discord.DMChannel: + channel = await self.bot.fetch_channel(self.config["channel_id"]) + files = [] + + for attachment in message.attachments: + fp = io.BytesIO() + await attachment.save(fp) + files.append(discord.File(fp, filename=attachment.filename)) + + await channel.send(f"Nachricht von <@!{message.author.id}>:") + await channel.send(message.content, files=files) + await message.channel.send("Danke, deine Nachricht wurde an das Admin-/Mod-Team weitergeleitet.") + + +async def setup(bot: commands.Bot) -> None: + await bot.add_cog(ModMail(bot)) diff --git a/extensions/poll.py b/extensions/poll.py new file mode 100644 index 0000000000000000000000000000000000000000..af4967411f794dae239a97e9a8e234350f044c13 --- /dev/null +++ b/extensions/poll.py @@ -0,0 +1,73 @@ +import emoji +from discord import app_commands, Interaction +from discord.ext import commands + +from models import * +from views.poll_view import PollView + +DEFAULT_CHOICES = ["🇦", "🇧", "🇨", "🇩", "🇪", "🇫", "🇬", "ðŸ‡", "🇮", "🇯", "🇰", "🇱", "🇲", "🇳", "🇴", "🇵", "🇶", + "🇷", "🇸", "🇹"] + + +@app_commands.guild_only() +class Polls(commands.GroupCog, name="poll", description="Handle Polls in Channels"): + def __init__(self, bot): + self.bot = bot + + @app_commands.command(name="add", description="Erstelle eine Umfrage mit bis zu 20 Antwortmöglichkeiten.") + @app_commands.describe(question="Welche Frage möchtest du stellen?", choice_a="1. Antwortmöglichkeit", + choice_b="2. Antwortmöglichkeit", choice_c="3. Antwortmöglichkeit", + choice_d="4. Antwortmöglichkeit", choice_e="5. Antwortmöglichkeit", + choice_f="6. Antwortmöglichkeit", choice_g="7. Antwortmöglichkeit", + choice_h="8. Antwortmöglichkeit", choice_i="9. Antwortmöglichkeit", + choice_j="10. Antwortmöglichkeit", choice_k="11. Antwortmöglichkeit", + choice_l="12. Antwortmöglichkeit", choice_m="13. Antwortmöglichkeit", + choice_n="14. Antwortmöglichkeit", choice_o="15. Antwortmöglichkeit", + choice_p="16. Antwortmöglichkeit", choice_q="17. Antwortmöglichkeit", + choice_r="18. Antwortmöglichkeit", choice_s="19. Antwortmöglichkeit", + choice_t="20. Antwortmöglichkeit") + async def cmd_poll(self, interaction: Interaction, question: str, choice_a: str, choice_b: str, + choice_c: str = None, choice_d: str = None, choice_e: str = None, choice_f: str = None, + choice_g: str = None, choice_h: str = None, choice_i: str = None, choice_j: str = None, + choice_k: str = None, choice_l: str = None, choice_m: str = None, choice_n: str = None, + choice_o: str = None, choice_p: str = None, choice_q: str = None, choice_r: str = None, + choice_s: str = None, choice_t: str = None): + """ Create a new poll """ + choices = [self.parse_choice(index, choice) for index, choice in enumerate( + [choice_a, choice_b, choice_c, choice_d, choice_e, choice_f, choice_g, choice_h, choice_i, choice_j, + choice_k, choice_l, choice_m, choice_n, choice_o, choice_p, choice_q, choice_r, choice_s, choice_t]) if + choice] + + await interaction.response.send_message("Bereite Umfrage vor, bitte warten...", view=PollView()) + message = await interaction.original_response() + poll = Poll.create(question=question, author=interaction.user.id, channel=interaction.channel_id, + message=message.id) + for choice in choices: + PollChoice.create(poll_id=poll.id, emoji=choice[0], text=choice[1]) + + await interaction.edit_original_response(content="", embed=poll.get_embed(), view=PollView()) + + def parse_choice(self, idx: int, choice: str): + choice = choice.strip() + index = choice.find(" ") + + if index > -1: + possible_option = choice[:index] + if emoji.is_emoji(possible_option) or possible_option in DEFAULT_CHOICES: + if len(choice[index:].strip()) > 0: + return [possible_option, choice[index:].strip()] + elif len(possible_option) > 1: + if (possible_option[0:2] == "<:" or possible_option[0:3] == "<a:") and possible_option[-1] == ">": + splitted_custom_emoji = possible_option.strip("<a:>").split(":") + if len(splitted_custom_emoji) == 2: + id = splitted_custom_emoji[1] + custom_emoji = self.bot.get_emoji(int(id)) + if custom_emoji and len(choice[index:].strip()) > 0: + return [custom_emoji, choice[index:].strip()] + + return [DEFAULT_CHOICES[idx], choice] + + +async def setup(bot: commands.Bot) -> None: + await bot.add_cog(Polls(bot)) + bot.add_view(PollView()) diff --git a/extensions/pomodoro.py b/extensions/pomodoro.py new file mode 100644 index 0000000000000000000000000000000000000000..145c44d3185638335c7f1a5ae6d109d337679c5c --- /dev/null +++ b/extensions/pomodoro.py @@ -0,0 +1,147 @@ +import asyncio +import random +from typing import List + +import discord +from discord import Interaction, app_commands, Guild, VoiceChannel +from discord.ext import commands, tasks + +from models import Timer, TimerAttendee +from views.pomodoro_view import PomodoroView + +STATUS = ["Arbeiten", "Pause", "Beendet"] + + +def get_soundfile(status: str): + return { + "Arbeiten": "roll_with_it-outro.mp3", + "Pause": "groove-intro.mp3", + "Beendet": "applause.mp3" + }.get(status) + + +def get_remaining(timer, new_status): + if new_status == STATUS[0]: + return timer.working_time + return timer.break_time + + +def get_mentions(timer: Timer): + return ", ".join( + [f"<@{attendee.member}>" for attendee in TimerAttendee.select().where(TimerAttendee.timer == timer.id)]) + + +async def get_voice_channels(guild: Guild, attendees: List[TimerAttendee]): + voice_channels = {} + for attendee in attendees: + member = await guild.fetch_member(attendee.member) + if vc := member.voice: + voice_channels[vc.channel.id] = vc.channel + + return voice_channels + + +async def play_sound(voice_channel: VoiceChannel, filename: str): + try: + voice_client = await voice_channel.connect() + voice_client.play(discord.FFmpegPCMAudio(f'sounds/{filename}')) + while voice_client.is_playing(): + await asyncio.sleep(1) + await voice_client.disconnect() + except discord.errors.ClientException as e: + print(e) + + +@app_commands.guild_only() +class Pomodoro(commands.Cog): + + def __init__(self, bot): + self.bot = bot + self.config = bot.config["extensions"][__name__.split(".")[-1]] + self.default_names = self.config["default_names"] + self.run_timer.start() + + @app_commands.command(name="timer", description="Erstelle deine persönliche Eieruhr") + async def cmd_timer(self, interaction: Interaction, working_time: int = 25, break_time: int = 5, name: str = None): + await interaction.response.defer() + message = await interaction.original_response() + name = random.choice(self.default_names) if not name else name + remaining = working_time + status = STATUS[0] + + timer = Timer.create(name=name, status=status, working_time=working_time, break_time=break_time, + remaining=remaining, channel=interaction.channel_id, message=message.id, + guild=interaction.guild.id) + TimerAttendee.create(timer=timer.id, member=interaction.user.id) + embed = timer.create_embed() + await interaction.edit_original_response(embed=embed, view=self.get_view()) + await self.send_acoustic_notification(timer) + + def get_view(self, disabled=False): + view = PomodoroView(self) + + if disabled: + view.disable() + + return view + + async def switch_phase(self, timer: Timer, new_status_idx: int = None): + if not new_status_idx: + current_status_index = STATUS.index(timer.status) + new_status_idx = (current_status_index + 1) % 2 + new_status = STATUS[new_status_idx] + remaining = get_remaining(timer, new_status) + timer.update(status=new_status, remaining=remaining).where(Timer.id == timer.id).execute() + await self.edit_message(Timer.get(Timer.id == timer.id)) + await self.send_acoustic_notification(timer) + + async def edit_message(self, timer: Timer, mentions=None, create_new=True): + channel = await self.bot.fetch_channel(timer.channel) + message_id = timer.message + try: + message = await channel.fetch_message(message_id) + embed = timer.create_embed() + + if create_new: + await message.delete() + view = self.get_view(disabled=timer.status == STATUS[-1]) + mentions = get_mentions(timer) if not mentions else mentions + new_msg = await channel.send(mentions, embed=embed, view=view) + timer.update(message=new_msg.id).where(Timer.id == timer.id).execute() + message = new_msg + else: + await message.edit(embed=embed, view=self.get_view()) + return str(message.id) + except discord.errors.NotFound: + timer.update(status=STATUS[-1]).where(Timer.id == timer.id).execute() + return None + + async def send_acoustic_notification(self, timer: Timer): + guild = self.bot.get_guild(timer.guild) + filename = get_soundfile(timer.status) + voice_channels = await get_voice_channels(guild, list(timer.attendees)) + + for id, voice_channel in voice_channels.items(): + await play_sound(voice_channel, filename) + + for vc in self.bot.voice_clients: + await vc.disconnect() + + @tasks.loop(minutes=1) + async def run_timer(self): + for timer in Timer.select().where(Timer.status != STATUS[-1]): + timer.update(remaining=timer.remaining - 1).where(Timer.id == timer.id).execute() + if timer.remaining <= 1: + await self.switch_phase(timer) + else: + await self.edit_message(timer, create_new=False) + + @run_timer.before_loop + async def before_timer(self): + await asyncio.sleep(60) + + +async def setup(bot: commands.Bot) -> None: + pomodoro = Pomodoro(bot) + await bot.add_cog(pomodoro) + bot.add_view(PomodoroView(pomodoro)) diff --git a/extensions/roles.py b/extensions/roles.py new file mode 100644 index 0000000000000000000000000000000000000000..dcf9cc5f80f33d67f6ad2fa6f21f75148fddf579 --- /dev/null +++ b/extensions/roles.py @@ -0,0 +1,67 @@ +import discord +from discord import app_commands, Interaction +from discord.ext import commands + +from views.role_view import RoleView + + +class Roles(commands.Cog): + def __init__(self, bot): + self.bot = bot + self.config = bot.config["extensions"][__name__.split(".")[-1]] + self.assignable_roles = self.config["assignable_roles"] + self.channel_id = self.config["channel_id"] + self.message_id = self.config.get("message_id") + + def get_key(self, role): + """ Get the key for a given role. This role is used for adding or removing a role from a user. """ + + for key, role_name in self.assignable_roles.items(): + if role_name == role.name: + return key + + # @commands.command(name="stats") + @app_commands.command(name="stats", description="Statistik zur Rollenzuweisung auf diesem Server.") + @app_commands.describe(public="Zeige die Statistik öffentlich für alle sichtbar.") + async def cmd_stats(self, interaction: Interaction, public: bool): + """ Sends stats in Chat. """ + await interaction.response.defer(ephemeral=not public) + + roles = {} + for role_category in self.assignable_roles.values(): + if role_category["in_stats"]: + for role in role_category["roles"].values(): + roles[role["name"]] = role + + embed = discord.Embed(title="Statistiken", + description=f'Wir haben aktuell {interaction.guild.member_count} Mitglieder auf diesem Server, verteilt auf folgende Rollen:') + + for role in interaction.guild.roles: + if roles.get(role.name): + embed.add_field(name=role.name, value=f'{len(role.members)} Mitglieder', inline=False) + + await interaction.edit_original_response(embed=embed) + + @app_commands.command(name="update-roles", description="Aktualisiere die Nachricht zur Rollenvergabe.") + @app_commands.guild_only() + @app_commands.default_permissions(manage_roles=True) + async def cmd_update_roles(self, interaction: Interaction): + await interaction.response.defer(ephemeral=True) + channel = await self.bot.fetch_channel(self.channel_id) + message = await channel.fetch_message(self.message_id) if self.message_id else None + view = RoleView(assignable_roles=self.config["extensions"][__name__.split(".")[-1]]["assignable_roles"]) + + embed = discord.Embed(title="Such dir deine Rollen aus", + description="Durch Klicken auf den Button unter dieser Nachricht kannst du dir selbst " + "einige Rollen vergeben.") + + if message: + await message.edit(content="", embed=embed, view=view) + else: + await channel.send(embed=embed, view=view) + await interaction.edit_original_response(content="Rollenvergabe aktualisiert.") + + +async def setup(bot: commands.Bot) -> None: + await bot.add_cog(Roles(bot)) + bot.add_view(RoleView(assignable_roles=bot.config["extensions"][__name__.split(".")[-1]]["assignable_roles"])) diff --git a/extensions/text_commands.py b/extensions/text_commands.py new file mode 100644 index 0000000000000000000000000000000000000000..5e3417c74fd25d2f69b01856a68f263c4192d007 --- /dev/null +++ b/extensions/text_commands.py @@ -0,0 +1,201 @@ +import random +import re + +import discord +from discord import app_commands, Interaction +from discord.ext import commands + +import utils +from models import Command, CommandText +from views.text_command_view import TextCommandView + + +@app_commands.guild_only() +class TextCommands(commands.GroupCog, name="commands", description="Text Commands auflisten und verwalten"): + def __init__(self, bot): + self.bot = bot + self.config = bot.config["extensions"][__name__.split(".")[-1]] + self.mod_channel_id = self.config["mod_channel"] + + @app_commands.command(name="list", description="Listet die Text Commands dieses Servers auf.") + async def cmd_list(self, interaction: Interaction, cmd: str = None): + await interaction.response.defer(ephemeral=True) + items = [] + if cmd: + if command := Command.get_or_none(Command.command == cmd): + items = [command_text.text for command_text in command.texts] + + if len(items) == 0: + await interaction.edit_original_response(content=f"{cmd} ist kein verfügbares Text-Command") + return + else: + for command in Command.select(): + if command.texts.count() > 0: + items.append(command.command) + + answer = f"Text Commands:\n" if cmd is None else f"Für {cmd} hinterlegte Texte:\n" + first = True + for i, item in enumerate(items): + if len(answer) + len(item) > 2000: + if first: + await interaction.edit_original_response(content=answer) + first = False + else: + await interaction.followup.send(answer, ephemeral=True) + answer = f"" + + answer += f"{i}: {item}\n" + + if first: + await interaction.edit_original_response(content=answer) + else: + await interaction.followup.send(answer, ephemeral=True) + + @app_commands.command(name="add", + description="Ein neues Text Command hinzufügen, oder zu einem bestehenden einen weiteren Text hinzufügen") + @app_commands.describe(cmd="Command. Bsp: \"link\" für das Command \"/link\".", + text="Text, der bei Benutzung des Commands ausgegeben werden soll.", + description="Beschreibung des Commands, die bei Benutzung angezeigt wird. Wird nur übernommen, bei neuen Commands.") + async def cmd_add(self, interaction: Interaction, cmd: str, text: str, description: str): + await interaction.response.defer(ephemeral=True) + if not re.match(r"^[a-z0-9]+(-[a-z0-9]+)*$", cmd): + await interaction.edit_original_response( + content="Ein Command darf nur aus Kleinbuchstaben und Zahlen bestehen, die durch Bindestriche getrennt werden können.") + return + + if utils.is_mod(interaction.user, self.bot): + if await self.add_command(cmd, text, description): + await interaction.edit_original_response(content="Dein Command wurde erfolgreich hinzugefügt!") + else: + await interaction.edit_original_response( + content="Das Command, dass du hinzufügen möchtest existiert bereits.") + else: + await self.suggest_command(cmd, text, description) + await interaction.edit_original_response(content="Dein Vorschlag wurde den Mods zur Genehmigung vorgelegt.") + + @app_commands.command(name="edit", description="Bearbeite bestehende Text Commands") + @app_commands.describe(cmd="Command, dass du bearbeiten möchtest", id="ID des zu bearbeitenden Texts", + text="Neuer Text, der statt des alten ausgegeben werden soll.") + async def cmd_edit(self, interaction: Interaction, cmd: str, id: int, text: str): + await interaction.response.defer(ephemeral=True) + + if not utils.is_mod(interaction.user, self.bot): + await interaction.edit_original_response(content="Du hast nicht die notwendigen Berechtigungen, " + "um dieses Command zu benutzen!") + return + + if command := Command.get_or_none(Command.command == cmd): + command_texts = list(command.texts) + if 0 <= id < len(command_texts): + CommandText.update(text=text).where(CommandText.id == command_texts[id].id).execute() + await interaction.edit_original_response( + content=f"Text {id} für Command {cmd} wurde erfolgreich geändert") + else: + await interaction.edit_original_response(content="Ungültiger Index") + else: + await interaction.edit_original_response(content=f"Command `{cmd}` nicht vorhanden!") + + @app_commands.command(name="remove", + description="Entferne ein gesamtes Command oder einen einzelnen Text von einem Command.") + @app_commands.describe(cmd="Command, dass du entfernen möchtest, oder von dem du einen Text entfernen möchtest.", + id="ID des zu entfernenden Texts.") + async def cmd_command_remove(self, interaction: Interaction, cmd: str, id: int = None): + await interaction.response.defer(ephemeral=True) + + if not utils.is_mod(interaction.user, self.bot): + await interaction.edit_original_response(content="Du hast nicht die notwendigen Berechtigungen, " + "um dieses Command zu benutzen!") + return + + if command := Command.get_or_none(Command.command == cmd): + if id >= 0: + command_texts = list(command.texts) + if 0 <= id < len(command_texts): + await self.remove_text(command, command_texts, id) + await interaction.edit_original_response( + content=f"Text {id} für Command `{cmd}` wurde erfolgreich entfernt") + else: + await interaction.edit_original_response(content=f"Ungültiger Index") + else: + await self.remove_command(command) + await interaction.edit_original_response(content=f"Text Command `{cmd}` wurde erfolgreich entfernt.") + else: + await interaction.edit_original_response(content=f"Command `{cmd}` nicht vorhanden!") + + async def add_command(self, cmd: str, text: str, description: str): + mod_channel = await self.bot.fetch_channel(self.mod_channel_id) + if command := Command.get_or_none(Command.command == cmd): + CommandText.create(text=text, command=command.id) + else: + if self.exists(cmd): + return False + command = Command.create(command=cmd, description=description) + CommandText.create(text=text, command=command.id) + await self.register_command(command) + + await mod_channel.send(f"[{cmd}] => [{text}] erfolgreich hinzugefügt.") + return True + + async def suggest_command(self, cmd: str, text: str, description: str): + mod_channel = await self.bot.fetch_channel(self.mod_channel_id) + command = Command.get_or_none(Command.command == cmd) + title = "Vorschlag für neuen Command Text" if command else "Vorschlag für neues Command" + + embed = discord.Embed(title=title) + embed.add_field(name="Command", value=cmd, inline=False) + embed.add_field(name="Text", value=text, inline=False) + if not command: + embed.add_field(name="Beschreibung", value=description, inline=False) + + await mod_channel.send(embed=embed, view=TextCommandView(self)) + + async def remove_text(self, command, command_texts, id): + command_text = list(command_texts)[id] + command_text.delete_instance(recursive=True) + if command.texts.count() == 0: + await self.remove_command(command) + + async def remove_command(self, command): + await self.unregister_command(command) + command.delete_instance(recursive=True) + + def exists(self, cmd): + for command in self.bot.tree.get_commands(): + if command.name == cmd: + return True + + return False + + async def init_commands(self): + for command in Command.select(): + if command.texts.count() > 0: + await self.register_command(command, sync=False) + + async def register_command(self, command: Command, sync: bool = True): + @app_commands.command(name=command.command, description=command.description) + @app_commands.guild_only() + @app_commands.describe(public="Zeige die Ausgabe des Commands öffentlich, für alle Mitglieder sichtbar.") + async def process_command(interaction: Interaction, public: bool): + await interaction.response.defer(ephemeral=not public) + if cmd := Command.get_or_none(Command.command == interaction.command.name): + texts = list(cmd.texts) + if len(texts) > 0: + await interaction.edit_original_response(content=(random.choice(texts)).text) + return + + await interaction.edit_original_response(content="FEHLER! Command wurde nicht gefunden!") + + self.bot.tree.add_command(process_command) + if sync: + await self.bot.tree.sync() + + async def unregister_command(self, command: Command): + self.bot.tree.remove_command(command.command) + await self.bot.tree.sync() + + +async def setup(bot: commands.Bot) -> None: + text_commands = TextCommands(bot) + await bot.add_cog(text_commands) + await text_commands.init_commands() + bot.add_view(TextCommandView(text_commands)) diff --git a/extensions/welcome.py b/extensions/welcome.py new file mode 100644 index 0000000000000000000000000000000000000000..9d335e6deb9b05f639a75c9a02e404366438111a --- /dev/null +++ b/extensions/welcome.py @@ -0,0 +1,80 @@ +import discord +from discord import app_commands, Interaction +from discord.ext import commands + + +@app_commands.guild_only() +@app_commands.default_permissions(manage_guild=True) +class Welcome(commands.GroupCog, name="welcome", description="Neue Mitglieder Willkommen heißen."): + def __init__(self, bot): + self.bot = bot + self.config = bot.config["extensions"][__name__.split(".")[-1]] + self.channel_id = self.config.get("channel_id") + self.message_id = self.config.get("welcome_message") + + @app_commands.command(name="update", description="Allgemeine Willkommensnachricht aktualisieren.") + @app_commands.default_permissions(manage_guild=True) + async def cmd_update_welcome(self, interaction: Interaction): + await interaction.response.defer(ephemeral=True) + channel = await self.bot.fetch_channel(self.channel_id) + message = None if self.message_id == 0 else await channel.fetch_message(self.message_id) + + embed = discord.Embed(title=":rocket: __FernUni Föderation__ :rocket:", + description="Willkommen auf dem interdisziplinären Server von und für FernUni-Studierende! Hier können FernUni-Studierende aus allen Fachrichtungen in Austausch treten, Ideen austauschen und gemeinsam an Projekten arbeiten: viel Potenzial für gegenseitige Bereicherung!") + + embed.add_field(name=":sparkles: Entstehung", + value="Die Betreiber:innen der verschiedenen FernUni-Discordserver haben sich vernetzt, um zusammenzuarbeiten. Aus mehreren Richtungen wurde der Wunsch nach einer fachübergreifender Plattform geäußert und daraufhin ist dieser Föderationsserver entstanden!", + inline=False) + + embed.add_field(name=":robot: Server-Bot", + value=f"Ich bin root. Beim <#{self.config['botuebungsplatz_channel']}> kannst du meine verschiedenen Befehle ausprobieren. Wenn du dort `!help` schreibst, sende ich dir per Direktnachricht einen Ãœberblick meiner Funktionen.", + inline=False) + + embed.add_field(name=":placard: Rollen", + value=f"Du kannst dir eine Discord-Rolle bei <#{self.config['role_channel']}> aussuchen, die deine Fakultätszugehörigkeit widerspiegelt.", + inline=False) + + embed.add_field(name=":scroll: Regeln", + value="Verhalte dich respektvoll und versuche Rücksicht auf deine Mitmenschen zu nehmen. Außerdem sind - wie überall auf Discord - diese Community-Richtlinien zu beachten: <https://discord.com/guidelines>.", + inline=False) + + embed.add_field(name=":link: Einladungslink", + value=f"Mitstudierende kannst du mit folgendem Link einladen: {self.config['invite_link']}.", + inline=False) + + embed.add_field(name="\u200b", + value="Viel Vergnügen auf dem Server!", + inline=False) + + if message: + await message.edit(content="", embed=embed) + else: + await channel.send(embed=embed) + + await interaction.edit_original_response(content="Willkommensnachricht erfolgreich aktualisiert!") + + @commands.Cog.listener() + async def on_member_join(self, member): + if member.dm_channel is None: + await member.create_dm() + + await member.dm_channel.send(f"Herzlich Willkommen bei der FernUni Föderation! Alle notwendigen Informationen, " + f"die du für den Einstieg brauchst, sowie die wenige Regeln, die aufgestellt " + f"sind, findest du in <#{self.channel_id}>\n" + f"Du darfst dir außerdem gerne im Channel <#{self.config['role_channel']}> " + f"die passende Rolle zu deiner Fakultät zuweisen lassen. \n\n" + f"Falls du Fragen haben solltest, kannst du sie gerne bei der " + f"<#{self.config['offtopic_channel']}> stellen. Wenn du bei etwas Hilfe vom " + f"Moderationsteam brauchst, schreib mir doch eine private Nachricht, ich werde " + f"sie weiterleiten :writing_hand:.\n\n" + f"Viel Spaß beim erkunden des Servers und bis bald!") + + @commands.Cog.listener() + async def on_member_update(self, before, after): + if before.pending != after.pending and not after.pending: + channel = await self.bot.fetch_channel(self.config["greeting_channel"]) + await channel.send(f"Willkommen <@!{before.id}> im Kreise der FernUni-Studierenden :student:") + + +async def setup(bot: commands.Bot) -> None: + await bot.add_cog(Welcome(bot)) diff --git a/models.py b/models.py new file mode 100644 index 0000000000000000000000000000000000000000..a10b81d0baa6d41245dfcb9bc830f2672a054a96 --- /dev/null +++ b/models.py @@ -0,0 +1,210 @@ +import io +import uuid +from datetime import datetime, timedelta + +import discord +from peewee import * +from peewee import ModelSelect + +db = SqliteDatabase("root.db") + + +class BaseModel(Model): + class Meta: + database = db + + +class Poll(BaseModel): + question = CharField() + author = IntegerField() + channel = IntegerField() + message = IntegerField() + + def get_embed(self) -> discord.Embed: + embed = discord.Embed(title="Umfrage", description=self.question) + embed.add_field(name="Erstellt von", value=f'<@!{self.author}>', inline=False) + embed.add_field(name="\u200b", value="\u200b", inline=False) + + for choice in self.choices: + name = f'{choice.emoji} {choice.text}' + value = f'{len(choice.choice_chosen)}' + + embed.add_field(name=name, value=value, inline=False) + + participants = {str(choice_chosen.member_id): 1 for choice_chosen in + PollChoiceChosen.select().join(PollChoice, on=PollChoiceChosen.poll_choice).where( + PollChoice.poll == self)} + + embed.add_field(name="\u200b", value="\u200b", inline=False) + embed.add_field(name="Anzahl der Teilnehmer an der Umfrage", value=f"{len(participants)}", inline=False) + + return embed + + +class PollChoice(BaseModel): + poll = ForeignKeyField(Poll, backref='choices') + text = CharField() + emoji = CharField() + + +class PollChoiceChosen(BaseModel): + poll_choice = ForeignKeyField(PollChoice, backref='choice_chosen') + member_id = IntegerField() + + +class Appointment(BaseModel): + channel = IntegerField() + message = IntegerField() + date_time = DateTimeField() + reminder = IntegerField() + title = CharField() + description = CharField() + author = IntegerField() + recurring = IntegerField() + reminder_sent = BooleanField() + uuid = UUIDField(default=uuid.uuid4()) + + def get_embed(self) -> discord.Embed: + attendees = self.attendees + embed = discord.Embed(title=self.title, + description=f"Wenn du eine Benachrichtigung zum Beginn des Termins" + f"{f', sowie {self.reminder} Minuten vorher, ' if self.reminder > 0 else f''}" + f" erhalten möchtest, reagiere mit :thumbsup: auf diese Nachricht.", + color=19607) + + if len(self.description) > 0: + embed.add_field(name="Beschreibung", value=self.description, inline=False) + embed.add_field(name="Startzeitpunkt", value=f"<t:{int(self.date_time.timestamp())}:F>", inline=False) + if self.reminder > 0: + embed.add_field(name="Benachrichtigung", value=f"{self.reminder} Minuten vor dem Start", inline=False) + if self.recurring > 0: + embed.add_field(name="Wiederholung", value=f"Alle {self.recurring} Tage", inline=False) + if len(attendees) > 0: + embed.add_field(name=f"Teilnehmerinnen ({len(attendees)})", + value=",".join([f"<@{attendee.member_id}>" for attendee in attendees])) + + return embed + + def get_ics_file(self) -> io.BytesIO: + fmt: str = "%Y%m%dT%H%M" + appointment: str = f"BEGIN:VCALENDAR\n" \ + f"PRODID:Boty McBotface\n" \ + f"VERSION:2.0\n" \ + f"BEGIN:VTIMEZONE\n" \ + f"TZID:Europe/Berlin\n" \ + f"BEGIN:DAYLIGHT\n" \ + f"TZOFFSETFROM:+0100\n" \ + f"TZOFFSETTO:+0200\n" \ + f"TZNAME:CEST\n" \ + f"DTSTART:19700329T020000\n" \ + f"RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=3\n" \ + f"END:DAYLIGHT\n" \ + f"BEGIN:STANDARD\n" \ + f"TZOFFSETFROM:+0200\n" \ + f"TZOFFSETTO:+0100\n" \ + f"TZNAME:CET\n" \ + f"DTSTART:19701025T030000\n" \ + f"RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10\n" \ + f"END:STANDARD\n" \ + f"END:VTIMEZONE\n" \ + f"BEGIN:VEVENT\n" \ + f"DTSTAMP:{datetime.now().strftime(fmt)}00Z\n" \ + f"UID:{self.uuid}\n" \ + f"SUMMARY:{self.title}\n" + appointment += f"RRULE:FREQ=DAILY;INTERVAL={self.recurring}\n" if self.recurring else f"" + appointment += f"DTSTART;TZID=Europe/Berlin:{self.date_time.strftime(fmt)}00\n" \ + f"DTEND;TZID=Europe/Berlin:{self.date_time.strftime(fmt)}00\n" \ + f"TRANSP:OPAQUE\n" \ + f"BEGIN:VALARM\n" \ + f"ACTION:DISPLAY\n" \ + f"TRIGGER;VALUE=DURATION:-PT{self.reminder}M\n" \ + f"DESCRIPTION:{self.description}\n" \ + f"END:VALARM\n" \ + f"END:VEVENT\n" \ + f"END:VCALENDAR" + ics_file: io.BytesIO = io.BytesIO(appointment.encode("utf-8")) + return ics_file + + +class Attendee(BaseModel): + appointment = ForeignKeyField(Appointment, backref='attendees') + member_id = IntegerField() + + +class Topic(BaseModel): + channel = IntegerField() + name = CharField() + + @classmethod + def get_topics(cls, channel: int, topic: str = None) -> ModelSelect: + topics: ModelSelect = cls.select().where(Topic.channel == channel) + return topics.where(Topic.name == topic) if topic else topics + + @classmethod + def has_links(cls, channel: int, topic: str = None) -> bool: + for topic in cls.get_topics(channel, topic=topic): + if len(list(topic.links)) > 0: + return True + + return False + + def append_field(self, embed: discord.Embed): + value = "" + for link in self.links: + value += f"- [{link.title}]({link.link})\n" + + embed.add_field(name=self.name.capitalize(), value=value, inline=False) + + +class Link(BaseModel): + link = CharField() + title = CharField() + topic = ForeignKeyField(Topic, backref='links') + + +class Timer(BaseModel): + name = CharField() + status = CharField() + working_time = IntegerField() + break_time = IntegerField() + remaining = IntegerField() + guild = IntegerField() + channel = IntegerField() + message = IntegerField() + + def create_embed(self): + color = discord.Colour.green() if self.status == "Arbeiten" else 0xFFC63A if self.status == "Pause" else discord.Colour.red() + zeiten = f"{self.working_time} Minuten Arbeiten\n{self.break_time} Minuten Pause" + remaining_value = f"{self.remaining} Minuten" + endzeit = (datetime.now() + timedelta(minutes=self.remaining)).strftime("%H:%M") + end_value = f" [bis {endzeit} Uhr]" if self.status != "Beendet" else "" + angemeldet_value = ", ".join([f"<@{attendee.member}>" for attendee in self.attendees]) + + embed = discord.Embed(title=self.name, + color=color) + embed.add_field(name="Status:", value=self.status, inline=False) + embed.add_field(name="Zeiten:", value=zeiten, inline=False) + embed.add_field(name="verbleibende Zeit:", value=remaining_value + end_value, inline=False) + embed.add_field(name="angemeldete User:", value=angemeldet_value if len(angemeldet_value) > 0 else "-", + inline=False) + + return embed + + +class TimerAttendee(BaseModel): + timer = ForeignKeyField(Timer, backref="attendees") + member = IntegerField() + + +class Command(BaseModel): + command = CharField(unique=True) + description = CharField() + + +class CommandText(BaseModel): + text = CharField() + command = ForeignKeyField(Command, backref="texts") + + +db.create_tables([Poll, PollChoice, PollChoiceChosen, Appointment, Attendee, Topic, Link, Timer, TimerAttendee, Command, + CommandText], safe=True) diff --git a/requirements.txt b/requirements.txt index bd37fed31aca7c32526c4743b02e052bf17e87cf..273dd1150088fe5f5e8f31815d9fdd33397a9e3c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,21 +1,18 @@ -aiohttp==3.7.4 -async-timeout==3.0.1 -attrs==20.3.0 -beautifulsoup4==4.9.3 -certifi==2020.12.5 -cffi==1.14.5 -chardet==3.0.4 -disnake==2.2.2 -emoji==1.2.0 -idna==2.10 -multidict==5.1.0 -pycparser==2.20 -PyNaCl==1.4.0 -python-dotenv==0.17.0 -requests==2.25.1 -six==1.16.0 -soupsieve==2.2.1 -tinydb==4.4.0 -typing-extensions==3.7.4.3 -urllib3==1.26.5 -yarl==1.6.3 +aiohttp==3.8.3 +aiosignal==1.3.1 +async-timeout==4.0.2 +attrs==22.2.0 +certifi==2022.12.7 +cffi==1.15.1 +charset-normalizer==2.1.1 +discord.py==2.1.0 +emoji==2.0.0 +frozenlist==1.3.3 +idna==3.4 +multidict==6.0.3 +peewee==3.15.2 +pycparser==2.21 +PyNaCl==1.5.0 +requests==2.28.1 +urllib3==1.26.13 +yarl==1.8.2 diff --git a/root.py b/root.py index 384ff54544844fe78888f02a8a5c4147b73bb04a..f5c36f478ee9ba1f51148360d5b32dcb621bc482 100644 --- a/root.py +++ b/root.py @@ -1,92 +1,38 @@ -import os +import json +from typing import Dict -import disnake -from disnake.ext import commands -from dotenv import load_dotenv - -from cogs import appointments, calmdown, help, links, polls, roles, support, text_commands, timer, welcome, christmas - -# .env file is necessary in the same directory, that contains several strings. -load_dotenv() -TOKEN = os.getenv('DISCORD_TOKEN') -GUILD = int(os.getenv('DISCORD_GUILD')) -ACTIVITY = os.getenv('DISCORD_ACTIVITY') -PIN_EMOJI = "📌" +import discord +from discord import Interaction, Message +from discord.ext import commands class Root(commands.Bot): - def __init__(self): - super().__init__(command_prefix='!', help_command=None, activity=disnake.Game(ACTIVITY), - intents=disnake.Intents.all()) - self.add_cogs() - self.persistent_views_added = False - - async def on_ready(self): - if not self.persistent_views_added: - if timer_cog := self.get_cog("Timer"): - self.add_view(timer_cog.get_view()) - print("Client started!") - - def add_cogs(self): - self.add_cog(appointments.Appointments(self)) - self.add_cog(calmdown.Calmdown(self)) - self.add_cog(help.Help(self)) - self.add_cog(links.Links(self)) - self.add_cog(polls.Polls(self)) - self.add_cog(roles.Roles(self)) - self.add_cog(support.Support(self)) - self.add_cog(text_commands.TextCommands(self)) - self.add_cog(timer.Timer(self)) - self.add_cog(welcome.Welcome(self)) - self.add_cog(christmas.Christmas(self)) - - -bot = Root() - - -def get_reaction(reactions): - """ Returns the reaction, that is equal to the specified PIN_EMOJI, - or if that reaction does not exist in list of reactions, None will be returned""" - - for reaction in reactions: - if reaction.emoji == PIN_EMOJI: - return reaction - return None - - -async def pin_message(message): - """ Pin the given message, if it is not already pinned """ - - if not message.pinned: - await message.pin() + def __init__(self, *args, config: Dict, **kwargs): + super().__init__(*args, **kwargs) + self.config = config + async def setup_hook(self) -> None: + for extension, extension_config in self.config["extensions"].items(): + await self.load_extension(f"extensions.{extension}") -async def unpin_message(message): - """ Unpin the given message, if it is pinned, and it has no pin reaction remaining. """ + await self.tree.sync() - if message.pinned: - reaction = get_reaction(message.reactions) - if reaction is None: - await message.unpin() +def load_config(): + fp = open("config.json", mode="r") + return json.load(fp) -@bot.event -async def on_raw_reaction_add(payload): - if payload.user_id == bot.user.id: - return - if payload.emoji.name == PIN_EMOJI: - channel = await bot.fetch_channel(payload.channel_id) - message = await channel.fetch_message(payload.message_id) - await pin_message(message) +config = load_config() +bot = Root(command_prefix='!', help_command=None, activity=discord.Game(config["activity"]), + intents=discord.Intents.all(), config=config) -@bot.event -async def on_raw_reaction_remove(payload): - if payload.emoji.name == PIN_EMOJI: - channel = await bot.fetch_channel(payload.channel_id) - message = await channel.fetch_message(payload.message_id) - await unpin_message(message) +@bot.tree.context_menu(name="📌 Nachricht anpinnen") +async def pin_message(interaction: Interaction, message: Message): + await interaction.response.defer(ephemeral=True, thinking=True) + await message.pin() + await interaction.edit_original_response(content="Nachricht erfolgreich angepinnt!") -bot.run(TOKEN) +bot.run(config["token"]) diff --git a/cogs/sounds/applause.mp3 b/sounds/applause.mp3 similarity index 100% rename from cogs/sounds/applause.mp3 rename to sounds/applause.mp3 diff --git a/cogs/sounds/groove-intro.mp3 b/sounds/groove-intro.mp3 similarity index 100% rename from cogs/sounds/groove-intro.mp3 rename to sounds/groove-intro.mp3 diff --git a/cogs/sounds/roll_with_it-outro.mp3 b/sounds/roll_with_it-outro.mp3 similarity index 100% rename from cogs/sounds/roll_with_it-outro.mp3 rename to sounds/roll_with_it-outro.mp3 diff --git a/utils.py b/utils.py index 01ee764a104abda7fafc5b35f98057d3aff5b825..a6739eef05de41edfb3c6f1a7f649a5b945e5278 100644 --- a/utils.py +++ b/utils.py @@ -1,64 +1,9 @@ -import os -import re -from datetime import datetime +from discord import Member -import disnake -from dotenv import load_dotenv -load_dotenv() -DATE_TIME_FMT = os.getenv("DISCORD_DATE_TIME_FORMAT") - - -async def send_dm(user, message, embed=None): - """ Send DM to a user/member """ - - if type(user) is disnake.User or type(user) is disnake.Member: - if user.dm_channel is None: - await user.create_dm() - - await user.dm_channel.send(message, embed=embed) - - -def is_mod(ctx): - author = ctx.author - roles = author.roles - - for role in roles: - if role.id in [int(os.getenv("DISCORD_MOD_ROLE")), int(os.getenv("DISCORD_ADMIN_ROLE"))]: +def is_mod(user: Member, bot): + for mod_role in bot.config["mod_roles"]: + if user.get_role(mod_role): return True return False - - -def is_valid_time(time): - return re.match(r"^\d+[mhd]?$", time) - - -def to_minutes(time): - if time[-1:] == "m": - return int(time[:-1]) - elif time[-1:] == "h": - h = int(time[:-1]) - return h * 60 - elif time[-1:] == "d": - d = int(time[:-1]) - h = d * 24 - return h * 60 - - return int(time) - - -def date_to_string(date: datetime): - return date.strftime(DATE_TIME_FMT) - - -def date_from_string(date: str): - return datetime.strptime(date, DATE_TIME_FMT) - - -async def files_from_attachments(attachments): - files = [] - for attachment in attachments: - files.append(await attachment.to_file(spoiler=attachment.is_spoiler())) - - return files diff --git a/views/appointment_view.py b/views/appointment_view.py new file mode 100644 index 0000000000000000000000000000000000000000..89391640765281ec0127c4409f2e54febab927c0 --- /dev/null +++ b/views/appointment_view.py @@ -0,0 +1,61 @@ +import discord +from discord import File + +from models import Appointment, Attendee + + +class AppointmentView(discord.ui.View): + def __init__(self): + super().__init__(timeout=None) + + @discord.ui.button(label='Zusagen', style=discord.ButtonStyle.green, custom_id='appointment_view:accept', emoji="ðŸ‘") + async def accept(self, interaction: discord.Interaction, button: discord.ui.Button): + appointment = Appointment.select().where(Appointment.message == interaction.message.id) + if appointment: + appointment = appointment[0] + attendee = appointment.attendees.filter(member_id=interaction.user.id) + if attendee: + await interaction.response.send_message("Du bist bereits Teilnehmerin dieses Termins.", + ephemeral=True) + return + else: + Attendee.create(appointment=appointment.id, member_id=interaction.user.id) + await interaction.message.edit(embed=appointment.get_embed()) + + await interaction.response.defer(thinking=False) + + @discord.ui.button(label='Absagen', style=discord.ButtonStyle.red, custom_id='appointment_view:decline', emoji="👎") + async def decline(self, interaction: discord.Interaction, button: discord.ui.Button): + appointment = Appointment.select().where(Appointment.message == interaction.message.id) + if appointment: + appointment = appointment[0] + attendee = appointment.attendees.filter(member_id=interaction.user.id) + if attendee: + attendee = attendee[0] + attendee.delete_instance() + await interaction.message.edit(embed=appointment.get_embed()) + else: + await interaction.response.send_message("Du kannst nur absagen, wenn du vorher zugesagt hast.", + ephemeral=True) + return + + await interaction.response.defer(thinking=False) + + @discord.ui.button(label='Download .ics', style=discord.ButtonStyle.blurple, custom_id='appointment_view:ics', + emoji="📅") + async def ics(self, interaction: discord.Interaction, button: discord.ui.Button): + appointment = Appointment.select().where(Appointment.message == interaction.message.id) + if appointment: + appointment = appointment[0] + await interaction.response.send_message("", file=File(appointment.get_ics_file(), + filename=f"{appointment.title}.ics"), ephemeral=True) + + @discord.ui.button(label='Löschen', style=discord.ButtonStyle.gray, custom_id='appointment_view:delete', emoji="🗑") + async def delete(self, interaction: discord.Interaction, button: discord.ui.Button): + await interaction.response.defer(thinking=False) + appointment = Appointment.select().where(Appointment.message == interaction.message.id) + if appointment: + appointment = appointment[0] + if interaction.user.id == appointment.author: + appointment.delete_instance(recursive=True) + await interaction.message.delete() diff --git a/views/poll_view.py b/views/poll_view.py new file mode 100644 index 0000000000000000000000000000000000000000..ee604a4bc863666d79e2b9e03898b7c544295c4a --- /dev/null +++ b/views/poll_view.py @@ -0,0 +1,65 @@ +import discord + +from models import Poll, PollChoiceChosen + + +class PollView(discord.ui.View): + def __init__(self): + super().__init__(timeout=None) + + @discord.ui.button(label='Abstimmen', style=discord.ButtonStyle.green, custom_id='poll_view:vote', emoji="✅") + async def vote(self, interaction: discord.Interaction, button: discord.ui.Button): + if poll := Poll.get_or_none(Poll.message == interaction.message.id): + await interaction.response.send_message( + f"{poll.question}\n\n*(Nach der Abstimmung kannst du diese Nachricht verwerfen. Wenn die Abstimmung " + f"nicht funktioniert, bitte verwirf die Nachricht und Klicke erneut auf den Abstimmen Button der " + f"Abstimmung.)*", view=PollChoiceView(poll, interaction.user), + ephemeral=True) + + @discord.ui.button(label='Beenden', style=discord.ButtonStyle.gray, custom_id='poll_view:close', emoji="🛑") + async def close(self, interaction: discord.Interaction, button: discord.ui.Button): + await interaction.response.defer(thinking=False) + if poll := Poll.get_or_none(Poll.message == interaction.message.id): + if interaction.user.id == poll.author: + poll.delete_instance(recursive=True) + await interaction.edit_original_response(view=None) + + @discord.ui.button(label='Löschen', style=discord.ButtonStyle.gray, custom_id='poll_view:delete', emoji="🗑") + async def delete(self, interaction: discord.Interaction, button: discord.ui.Button): + await interaction.response.defer(thinking=False) + if poll := Poll.get_or_none(Poll.message == interaction.message.id): + if interaction.user.id == poll.author: + poll.delete_instance(recursive=True) + await interaction.message.delete() + + +class PollChoiceView(discord.ui.View): + def __init__(self, poll, user): + super().__init__(timeout=None) + self.poll = poll + self.user = user + self.add_item(PollDropdown(poll, user)) + + +class PollDropdown(discord.ui.Select): + def __init__(self, poll, user): + self.poll = poll + self.user = user + options = [discord.SelectOption(label=choice.text, emoji=choice.emoji, + default=len(choice.choice_chosen.filter(member_id=user.id)) > 0) for choice in + poll.choices] + + super().__init__(placeholder='Gib deine Stimme(n) jetzt ab....', min_values=0, max_values=len(options), + options=options) + + async def callback(self, interaction: discord.Interaction): + await interaction.response.defer(thinking=False) + for choice in self.poll.choices: + chosen = choice.choice_chosen.filter(member_id=self.user.id) + if chosen and choice.text not in self.values: + chosen[0].delete_instance() + elif not chosen and choice.text in self.values: + PollChoiceChosen.create(poll_choice_id=choice.id, member_id=self.user.id) + + message = await interaction.channel.fetch_message(self.poll.message) + await message.edit(embed=self.poll.get_embed(), view=PollView()) diff --git a/views/pomodoro_view.py b/views/pomodoro_view.py new file mode 100644 index 0000000000000000000000000000000000000000..a94798929851850268c2479de0ec51b6633d4eb9 --- /dev/null +++ b/views/pomodoro_view.py @@ -0,0 +1,94 @@ +import discord +from discord import ButtonStyle, Interaction +from discord.ui import Button, View + +from models import Timer, TimerAttendee + + +class PomodoroView(View): + def __init__(self, pomodoro): + super().__init__(timeout=None) + self.pomodoro = pomodoro + + @discord.ui.button(label="Anmelden", emoji="ðŸ‘", style=ButtonStyle.green, custom_id="timerview:subscribe") + async def btn_subscribe(self, interaction: Interaction, button: Button): + await interaction.response.defer(ephemeral=True, thinking=False) + if timer := Timer.get_or_none(Timer.message == interaction.message.id): + if TimerAttendee.get_or_none(TimerAttendee.timer == timer.id, TimerAttendee.member == interaction.user.id): + await interaction.followup.send(content="Du bist bereits angemeldet.", ephemeral=True) + return + + TimerAttendee.create(timer=timer.id, member=interaction.user.id) + embed = timer.create_embed() + await interaction.message.edit(embed=embed, view=self) + await interaction.followup.send(content="Du hast dich erfolgreich angemeldet", ephemeral=True) + else: + await interaction.followup.send(content="Etwas ist schiefgelaufen...", ephemeral=True) + + @discord.ui.button(label="Abmelden", emoji="👎", style=ButtonStyle.red, custom_id="timerview:unsubscribe") + async def btn_unsubscribe(self, interaction: Interaction, button: Button): + await interaction.response.defer(ephemeral=True, thinking=False) + if timer := Timer.get_or_none(Timer.message == interaction.message.id): + if timer_attendee := TimerAttendee.get_or_none(TimerAttendee.timer == timer.id, + TimerAttendee.member == interaction.user.id): + timer_attendee.delete_instance() + if TimerAttendee.select().where(TimerAttendee.timer == timer.id).count() > 0: + embed = timer.create_embed() + await interaction.message.edit(embed=embed, view=self) + else: + await self.pomodoro.switch_phase(timer, new_status_idx=-1) + await interaction.followup.send(content="Du hast dich erfolgreich abgemeldet", ephemeral=True) + else: + await interaction.followup.send(content="Du musst erst angemeldet sein, um dich abmelden zu " + "können.", ephemeral=True) + else: + await interaction.followup.send(content="Etwas ist schiefgelaufen...", ephemeral=True) + + @discord.ui.button(label="Phase überspringen", emoji="â©", style=ButtonStyle.blurple, custom_id="timverview:skip") + async def btn_skip(self, interaction: Interaction, button: Button): + await interaction.response.defer(ephemeral=True, thinking=False) + if timer := Timer.get_or_none(Timer.message == interaction.message.id): + if TimerAttendee.get_or_none(TimerAttendee.timer == timer.id, TimerAttendee.member == interaction.user.id): + new_phase = await self.pomodoro.switch_phase(timer) + await interaction.followup.send(content="Erfolgreich übersprungen", ephemeral=True) + return + + await interaction.followup.send(content="Nur angemeldete Personen können den Timer bedienen.", + ephemeral=True) + return + + await interaction.followup.send(content="Etwas ist schiefgelaufen...", ephemeral=True) + + @discord.ui.button(label="Neustarten", emoji="🔄", style=ButtonStyle.blurple, custom_id="timverview:restart") + async def btn_restart(self, interaction: Interaction, button: Button): + await interaction.response.defer(ephemeral=True, thinking=True) + if timer := Timer.get_or_none(Timer.message == interaction.message.id): + if TimerAttendee.get_or_none(TimerAttendee.timer == timer.id, TimerAttendee.member == interaction.user.id): + new_phase = await self.pomodoro.switch_phase(timer, new_status_idx=0) + await interaction.followup.send(content="Erfolgreich neugestartet", ephemeral=True) + return + + await interaction.followup.send(content="Nur angemeldete Personen können den Timer bedienen.", + ephemeral=True) + return + + await interaction.followup.send(content="Etwas ist schiefgelaufen...", ephemeral=True) + + @discord.ui.button(label="Beenden", emoji="🛑", style=ButtonStyle.grey, custom_id="timverview:stop") + async def btn_stop(self, interaction: Interaction, button: Button): + await interaction.response.defer(ephemeral=True, thinking=True) + if timer := Timer.get_or_none(Timer.message == interaction.message.id): + if TimerAttendee.get_or_none(TimerAttendee.timer == timer.id, TimerAttendee.member == interaction.user.id): + new_phase = await self.pomodoro.switch_phase(timer, new_status_idx=-1) + await interaction.followup.send(content="Erfolgreich neugestartet", ephemeral=True) + return + + await interaction.followup.send(content="Nur angemeldete Personen können den Timer bedienen.", + ephemeral=True) + return + + await interaction.followup.send(content="Etwas ist schiefgelaufen...", ephemeral=True) + + def disable(self): + for button in self.children: + button.disabled = True diff --git a/views/role_view.py b/views/role_view.py new file mode 100644 index 0000000000000000000000000000000000000000..eb7f0540e46c02e3d09d2e225d0029b9bcca2e9f --- /dev/null +++ b/views/role_view.py @@ -0,0 +1,84 @@ +import discord + + +class RoleView(discord.ui.View): + def __init__(self, assignable_roles): + super().__init__(timeout=None) + self.assignable_roles = assignable_roles + + @discord.ui.button(label='Rollen auswählen', style=discord.ButtonStyle.green, custom_id='role_view:select') + async def vote(self, interaction: discord.Interaction, button: discord.ui.Button): + await interaction.response.send_message( + f"Wähle zunächst eine Kategorie von Rollen aus. Anschließend kannst du dir innerhalb dieser Kategorie " + f"Rollen zuweisen, oder entfernen.\n\n*(Nach Auswahl der Rollen kannst du diese Nachricht verwerfen. " + f"Wenn die Rollenauswahl nicht funktioniert, bitte verwirf die Nachricht und Klicke erneut auf den Button " + f"über dieser Nachricht.)*", view=RoleSelectionView(self.assignable_roles, interaction.user), + ephemeral=True) + + +class RoleSelectionView(discord.ui.View): + def __init__(self, assignable_roles, user, category=None): + super().__init__(timeout=None) + self.add_item(RoleCategoryDropdown(assignable_roles, category=category)) + if category: + self.add_item(RoleSelectionDropdown(assignable_roles, user, category)) + + +class RoleCategoryDropdown(discord.ui.Select): + def __init__(self, assignable_roles, category=None): + self.assignable_roles = assignable_roles + options = [discord.SelectOption(label=role_category["name"], value=key, default=(category == key)) for + key, role_category in assignable_roles.items()] + + super().__init__(placeholder='Wähle eine Kategorie...', min_values=1, max_values=1, options=options) + + async def callback(self, interaction: discord.Interaction): + await interaction.response.defer(thinking=False) + await interaction.edit_original_response( + view=RoleSelectionView(self.assignable_roles, interaction.user, category=self.values[0])) + + +def has_role(role, user): + for r in user.roles: + if r.name == role["name"]: + return True + return False + + +class RoleSelectionDropdown(discord.ui.Select): + def __init__(self, assignable_roles, user, category): + self.assignable_roles = assignable_roles + self.user = user + self.category = category + options = [ + discord.SelectOption(label=role["name"], emoji=role.get("emoji"), value=key, default=has_role(role, user)) + for + key, role in assignable_roles[category]["roles"].items()] + + super().__init__(placeholder='Wähle deine Rolle(n)....', min_values=0, max_values=len(options), options=options) + + async def callback(self, interaction: discord.Interaction): + await interaction.response.defer(thinking=False) + guild_roles = {role.name: role for role in interaction.guild.roles} + member_roles = {role.name: role for role in interaction.user.roles} + new_roles = [] + deprecated_roles = [] + for key, role in self.assignable_roles[self.category]["roles"].items(): + if key in self.values: + if not member_roles.get(role["name"]): + if new_role := guild_roles.get(role["name"]): + new_roles.append(new_role) + else: + if member_role := member_roles.get(role["name"]): + deprecated_roles.append(member_role) + + [await interaction.user.add_roles(role) for role in new_roles] + [await interaction.user.remove_roles(role) for role in deprecated_roles] + + reply = "" + if len(new_roles) > 0: + reply += f"Rollen {', '.join([f'`{role.name}`' for role in new_roles])} hinzugefügt!\n" + if len(deprecated_roles) > 0: + reply += f"Rollen {', '.join([f'`{role.name}`' for role in deprecated_roles])} entfernt!" + if len(reply) > 0: + await interaction.followup.send(reply, ephemeral=True) diff --git a/views/text_command_view.py b/views/text_command_view.py new file mode 100644 index 0000000000000000000000000000000000000000..3d2dbefd310d3971ee54704f1c769a733e2a2727 --- /dev/null +++ b/views/text_command_view.py @@ -0,0 +1,42 @@ +import discord +from discord import Message + +from models import CommandText, Command + + +def get_from_embed(message: Message) -> tuple[str, str, str] | tuple[str, str, None]: + embed = message.embeds[0] + fields = {field.name: field.value for field in embed.fields} + + return fields.get("Command"), fields.get("Text"), fields.get("Beschreibung") + + +class TextCommandView(discord.ui.View): + def __init__(self, text_commands): + super().__init__(timeout=None) + self.text_commands = text_commands + + @discord.ui.button(emoji="ðŸ‘", style=discord.ButtonStyle.green, custom_id='text_command_view:approve') + async def approve(self, interaction: discord.Interaction, button: discord.ui.Button): + await interaction.response.defer(thinking=True) + cmd, text, description = get_from_embed(interaction.message) + + if command := Command.get_or_none(Command.command == cmd): + CommandText.create(text=text, command=command.id) + else: + command = Command.create(command=cmd, description=description) + CommandText.create(text=text, command=command.id) + await self.text_commands.register_command(command) + await interaction.followup.send(content=f"Command `{cmd}` mit Text " + f"`{text}` wurde akzeptiert.") + + await interaction.message.delete() + + @discord.ui.button(emoji="👎", style=discord.ButtonStyle.red, custom_id='text_command_view:decline') + async def decline(self, interaction: discord.Interaction, button: discord.ui.Button): + await interaction.response.defer(thinking=True) + cmd, text, _ = get_from_embed(interaction.message) + + await interaction.followup.send(content=f"Command `{cmd}` mit Text " + f"`{text}` wurde abgelehnt.") + await interaction.message.delete() diff --git a/views/timer_view.py b/views/timer_view.py deleted file mode 100644 index 5c892b13353bbeb517c7ced1699beb18165607cd..0000000000000000000000000000000000000000 --- a/views/timer_view.py +++ /dev/null @@ -1,39 +0,0 @@ -import disnake -from disnake import MessageInteraction, ButtonStyle -from disnake.ui import Button, View - -SUBSCRIBE = "timerview:subscribe" -UNSUBSCRIBE = "timerview:unsubscribe" -SKIP = "timverview:skip" -RESTART = "timverview:restart" -STOP = "timverview:stop" - - -class TimerView(View): - def __init__(self, callback): - super().__init__(timeout=None) - self.callback = callback - - @disnake.ui.button(emoji="ðŸ‘", style=ButtonStyle.grey, custom_id=SUBSCRIBE) - async def btn_subscribe(self, button: Button, interaction: MessageInteraction): - await self.callback(button, interaction) - - @disnake.ui.button(emoji="👎", style=ButtonStyle.grey, custom_id=UNSUBSCRIBE) - async def btn_unsubscribe(self, button: Button, interaction: MessageInteraction): - await self.callback(button, interaction) - - @disnake.ui.button(emoji="â©", style=ButtonStyle.grey, custom_id=SKIP) - async def btn_skip(self, button: Button, interaction: MessageInteraction): - await self.callback(button, interaction) - - @disnake.ui.button(emoji="🔄", style=ButtonStyle.grey, custom_id=RESTART) - async def btn_restart(self, button: Button, interaction: MessageInteraction): - await self.callback(button, interaction) - - @disnake.ui.button(emoji="🛑", style=ButtonStyle.grey, custom_id=STOP) - async def btn_stop(self, button: Button, interaction: MessageInteraction): - await self.callback(button, interaction) - - def disable(self): - for button in self.children: - button.disabled = True