diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000000000000000000000000000000000000..99fa790034c1beca165936b69fca2db95ce9168d --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,47 @@ +# This is a basic workflow to help you get started with Actions + +name: Deploy QA + +# Controls when the workflow will run +on: + # Triggers the workflow on push or pull request events but only for the develop branch + push: + branches: [ develop ] + + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +# A workflow run is made up of one or more jobs that can run sequentially or in parallel +jobs: + # This workflow contains a single job called "build" + deploy: + # The type of runner that the job will run on + runs-on: boty-qa + # Steps represent a sequence of tasks that will be executed as part of the job + steps: + + # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it + - uses: actions/checkout@v2 + + - name: Copy to deploy directory + run: cp ./ ../../../../fernuni-bot/ -Rf + + - name: Update Pip requirements + run: ../../../../fernuni-bot/venv/bin/python -m pip install -r ../../../../fernuni-bot/requirements.txt + + - name: Restart Boty-QA + run: sudo systemctl restart boty-qa + # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it + #- name: Check Git + # run: git status + + # Runs a single command using the runners shell + # - name: Run a one-line script + # run: echo Hello, world! + + # Runs a set of commands using the runners shell + # - name: Run a multi-line script + # run: | + # echo Add other actions to build, + # echo test, and deploy your project. + diff --git a/.idea/fernuni-bot.iml b/.idea/fernuni-bot.iml index 74d515a027de98657e9d3d5f0f1831882fd81374..7ad0d735c6a577ba054a8af3bfe23c05b1453573 100644 --- a/.idea/fernuni-bot.iml +++ b/.idea/fernuni-bot.iml @@ -1,10 +1,10 @@ -<?xml version="1.0" encoding="UTF-8"?> -<module type="PYTHON_MODULE" version="4"> - <component name="NewModuleRootManager"> - <content url="file://$MODULE_DIR$"> - <excludeFolder url="file://$MODULE_DIR$/venv" /> - </content> - <orderEntry type="inheritedJdk" /> - <orderEntry type="sourceFolder" forTests="false" /> - </component> +<?xml version="1.0" encoding="UTF-8"?> +<module type="PYTHON_MODULE" version="4"> + <component name="NewModuleRootManager"> + <content url="file://$MODULE_DIR$"> + <excludeFolder url="file://$MODULE_DIR$/venv" /> + </content> + <orderEntry type="jdk" jdkName="Python 3.9 (fernuni-bot)" jdkType="Python SDK" /> + <orderEntry type="sourceFolder" forTests="false" /> + </component> </module> \ No newline at end of file diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml index 105ce2da2d6447d11dfe32bfb846c3d5b199fc99..dd4c951ef44ebdc37bbe4a453aab974c815ca6f6 100644 --- a/.idea/inspectionProfiles/profiles_settings.xml +++ b/.idea/inspectionProfiles/profiles_settings.xml @@ -1,5 +1,6 @@ <component name="InspectionProjectProfileManager"> <settings> + <option name="PROJECT_PROFILE" value="Default" /> <option name="USE_PROJECT_PROFILE" value="false" /> <version value="1.0" /> </settings> diff --git a/cogs/appointments.py b/cogs/appointments.py index 789c27f0c236a6f019b177f05886faf6c0081954..11f6d76acc8dd0956956d6cb1598e37867f8eb2b 100644 --- a/cogs/appointments.py +++ b/cogs/appointments.py @@ -1,258 +1,258 @@ -import asyncio -import datetime -import io -import json -import os -import uuid - -import discord -from discord.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: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.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 discord.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 = discord.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=discord.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 discord.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) +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: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.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/armin.py b/cogs/armin.py index 2dd5d5b89eb9450775713ef44e917b54280d4a48..f017848a1c33c52fea87e7fdeedf8b962933577c 100644 --- a/cogs/armin.py +++ b/cogs/armin.py @@ -1,35 +1,35 @@ -import random -from discord.ext import commands -from cogs.help import help, handle_error - -class Armin(commands.Cog): - def __init__(self, bot): - self.bot = bot - self.a = ["ein", "zwei", "drei", "vier", "fünf", "sechs"] - self.b = ["tägige", "wöchige", "monatige", "fache", "malige", "hebige"] - self.c = ["harte", "softe", "optionale", "intransparente", "alternativlose", "unumkehrbare"] - self.d = ["Wellenbrecher-", "Brücken-", "Treppen-", "Wende-", "Impf-", "Ehren-"] - self.e = ["Lockdown", "Stopp", "Maßnahme", "Kampagne", "Sprint", "Matrix"] - self.f = ["zum Sommer", "auf Weiteres", "zur Bundestagswahl", "2030", "nach den Abiturprüfungen", - "in die Puppen"] - self.g = ["sofortigen", "nachhaltigen", "allmählichen", "unausweichlichen", "wirtschaftsschonenden", - "willkürlichen"] - self.h = ["Senkung", "Steigerung", "Beendigung", "Halbierung", "Vernichtung", "Beschönigung"] - self.i = ["Infektionszahlen", "privaten Treffen", "Wirtschaftsleistung", "Wahlprognosen", "dritten Welle", - "Bundeskanzlerin"] - - @help( - brief="Wenn du wissen willst, was Armin sagt, dann `!arminsagt`.", - description="Ähnlichkeiten zu Äußerungen eines Ministerpräsidenten sind nicht beabsichtigt und rein zufällig." - ) - @commands.command(name="arminsagt") - async def cmd_arminsagt(self, ctx): - rNum = random.randint(0, 5) - n = "n" if rNum not in [2, 3, 5] else "" - await ctx.send(f"Was wir jetzt brauchen, ist eine{n} {random.choice(self.a)}{random.choice(self.b)}{n} " - f"{random.choice(self.c)}{n} {random.choice(self.d)}{self.e[rNum]} " - f"bis {random.choice(self.f)} zur {random.choice(self.g)} {random.choice(self.h)} " - f"der {random.choice(self.i)}.") - - async def cog_command_error(self, ctx, error): - await handle_error(ctx, error) +import random +from disnake.ext import commands +from cogs.help import help, handle_error + +class Armin(commands.Cog): + def __init__(self, bot): + self.bot = bot + self.a = ["ein", "zwei", "drei", "vier", "fünf", "sechs"] + self.b = ["tägige", "wöchige", "monatige", "fache", "malige", "hebige"] + self.c = ["harte", "softe", "optionale", "intransparente", "alternativlose", "unumkehrbare"] + self.d = ["Wellenbrecher-", "Brücken-", "Treppen-", "Wende-", "Impf-", "Ehren-"] + self.e = ["Lockdown", "Stopp", "Maßnahme", "Kampagne", "Sprint", "Matrix"] + self.f = ["zum Sommer", "auf Weiteres", "zur Bundestagswahl", "2030", "nach den Abiturprüfungen", + "in die Puppen"] + self.g = ["sofortigen", "nachhaltigen", "allmählichen", "unausweichlichen", "wirtschaftsschonenden", + "willkürlichen"] + self.h = ["Senkung", "Steigerung", "Beendigung", "Halbierung", "Vernichtung", "Beschönigung"] + self.i = ["Infektionszahlen", "privaten Treffen", "Wirtschaftsleistung", "Wahlprognosen", "dritten Welle", + "Bundeskanzlerin"] + + @help( + brief="Wenn du wissen willst, was Armin sagt, dann `!arminsagt`.", + description="Ähnlichkeiten zu Äußerungen eines Ministerpräsidenten sind nicht beabsichtigt und rein zufällig." + ) + @commands.command(name="arminsagt") + async def cmd_arminsagt(self, ctx): + rNum = random.randint(0, 5) + n = "n" if rNum not in [2, 3, 5] else "" + await ctx.send(f"Was wir jetzt brauchen, ist eine{n} {random.choice(self.a)}{random.choice(self.b)}{n} " + f"{random.choice(self.c)}{n} {random.choice(self.d)}{self.e[rNum]} " + f"bis {random.choice(self.f)} zur {random.choice(self.g)} {random.choice(self.h)} " + f"der {random.choice(self.i)}.") + + async def cog_command_error(self, ctx, error): + await handle_error(ctx, error) diff --git a/cogs/calmdown.py b/cogs/calmdown.py index 28c98775d5353fec97471f28abf33fe79b62e1f5..d9a1122c70af4b00b0add895afeb02fc8418daa4 100644 --- a/cogs/calmdown.py +++ b/cogs/calmdown.py @@ -1,107 +1,107 @@ -import datetime -import json -import os -import re - -import discord -from discord.ext import commands, tasks - -import utils -from cogs.help import help - -""" - DISCORD_CALMDOWN_ROLE - Die Rollen-ID der "Calmdown"-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 Calmdown-Rolle wurde nun wieder entfernt.") - await user.remove_roles(role) - except discord.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 Calmdown-Rolle zu.", - example="!calmdown @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: discord.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 **Calmdown-Rolle** vergeben.") - await user.add_roles(role) - if duration < 300: - await utils.send_dm(user, f"Dir wurde für {duration} Minuten die **Calmdown-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 Calmdown-Rolle. Du kannst weiterhin alle Kanäle lesen, aber erst nach Ablauf der Zeit wieder an Gesprächen teilnehmen.") +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 "Calmdown"-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 Calmdown-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 Calmdown-Rolle zu.", + example="!calmdown @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 **Calmdown-Rolle** vergeben.") + await user.add_roles(role) + if duration < 300: + await utils.send_dm(user, f"Dir wurde für {duration} Minuten die **Calmdown-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 Calmdown-Rolle. Du kannst weiterhin alle Kanäle lesen, aber erst nach Ablauf der Zeit wieder an Gesprächen teilnehmen.") diff --git a/cogs/change_log.py b/cogs/change_log.py index cd803450487266a5b1e217d26e36e161f1c807a2..b928e1c38e35456e313eb40e6861ac0223e90ba7 100644 --- a/cogs/change_log.py +++ b/cogs/change_log.py @@ -1,27 +1,27 @@ -from discord.ext import commands - -import os - - -class ChangeLog(commands.Cog): - def __init__(self, bot): - self.bot = bot - self.channel_id = os.getenv("DISCORD_CHANGE_LOG_CHANNEL") - - @commands.Cog.listener() - async def on_message_edit(self, before, after): - if self.bot.user == before.author: - return - - channel = await self.bot.fetch_channel(self.channel_id) - await channel.send(f"Message edited by <@!{before.author.id}> in <#{before.channel.id}>:") - await channel.send(before.content) - - @commands.Cog.listener() - async def on_message_delete(self, message): - if self.bot.user == message.author: - return - - channel = await self.bot.fetch_channel(self.channel_id) - await channel.send(f"Message deleted by <@!{message.author.id}> in <#{message.channel.id}>:") - await channel.send(message.content) +from disnake.ext import commands + +import os + + +class ChangeLog(commands.Cog): + def __init__(self, bot): + self.bot = bot + self.channel_id = os.getenv("DISCORD_CHANGE_LOG_CHANNEL") + + @commands.Cog.listener() + async def on_message_edit(self, before, after): + if self.bot.user == before.author: + return + + channel = await self.bot.fetch_channel(self.channel_id) + await channel.send(f"Message edited by <@!{before.author.id}> in <#{before.channel.id}>:") + await channel.send(before.content) + + @commands.Cog.listener() + async def on_message_delete(self, message): + if self.bot.user == message.author: + return + + channel = await self.bot.fetch_channel(self.channel_id) + await channel.send(f"Message deleted by <@!{message.author.id}> in <#{message.channel.id}>:") + await channel.send(message.content) diff --git a/cogs/christmas.py b/cogs/christmas.py index ffed4bbc6c94c1370743f685e74f564d5abc6c30..90d67c460d3c0a4344937a0e91aa00e1537913ee 100644 --- a/cogs/christmas.py +++ b/cogs/christmas.py @@ -2,7 +2,7 @@ import json import os from datetime import datetime -from discord.ext import commands +from disnake.ext import commands import utils @@ -10,7 +10,7 @@ import utils class Christmas(commands.Cog): def __init__(self, bot): self.bot = bot - self.channel_id = int(os.getenv("DISCORD_ADVENT_CALENDAR_CHANNEL")) + self.channel_id = int(os.getenv("DISCORD_ADVENT_CALENDAR_CHANNEL", "0")) self.advent_calendar = [] self.load_advent_calendar() diff --git a/cogs/components/poll/poll.py b/cogs/components/poll/poll.py index 3c95a62f56d8ab0945d26c8f3a559577ace2d1e1..89eb0a77780cfa2e803c62d39ea0a01bfaf3cae2 100644 --- a/cogs/components/poll/poll.py +++ b/cogs/components/poll/poll.py @@ -1,146 +1,146 @@ -import discord -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 = discord.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 +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/easter.py b/cogs/easter.py index 19cbe8d8558855fccff8798d172e0c7b81655e13..3859dcd7488eae4b82d4b82688707acc65a08377 100644 --- a/cogs/easter.py +++ b/cogs/easter.py @@ -1,107 +1,107 @@ -import json -import discord -from discord.ext import commands -from cogs.help import handle_error - - -class Easter(commands.Cog): - def __init__(self, bot): - self.bot = bot - self.data = self.load_data() - # self.messages = [] - # self.reaction_timer.start() - - def load_data(self): - data_file = open("data/easter.json", mode="r") - return json.load(data_file) - - def save_data(self): - data_file = open("data/easter.json", mode="w") - json.dump(self.data, data_file) - - # @commands.Cog.listener(name="on_message") - # async def hide(self, message): - # if message.author == self.bot.user: - # return - # - # if message.channel.id in self.data["channels"]: - # if random.random() < self.data["probability"]: - # self.messages.append(message) - # - # @commands.Cog.listener(name="on_raw_reaction_add") - # async def seek(self, payload): - # - # if payload.member == self.bot.user or payload.message_id not in self.data["message_ids"]: - # return - # - # modifier = 1 if payload.emoji.name in self.data["reactions_add"] else -1 if payload.emoji.name in self.data[ - # "reactions_remove"] else 0 - # if modifier != 0: - # self.data["message_ids"].remove(payload.message_id) - # self.modify_leaderboard(payload.user_id, modifier) - # - # channel = await self.bot.fetch_channel(payload.channel_id) - # message = await channel.fetch_message(payload.message_id) - # await message.clear_reaction(payload.emoji.name) - # self.save_data() - # - # def modify_leaderboard(self, user_id, modifier): - # if score := self.data["leaderboard"].get(str(user_id)): - # self.data["leaderboard"][str(user_id)] = score + modifier - # else: - # self.data["leaderboard"][str(user_id)] = modifier - # - # self.save_data() - #@help() - @commands.command(name="leaderboard") - async def cmd_leaderboard(self, ctx, all=None): - leaderboard = self.data["leaderboard"] - embed = discord.Embed(title="Egg-Hunt Leaderboard", description="Wer hat bisher die meisten Eier gefunden???") - embed.set_thumbnail(url="https://www.planet-wissen.de/kultur/religion/ostern/tempxostereiergjpg100~_v-gseagaleriexl.jpg") - - places = scores = "\u200b" - place = 0 - max = 0 if all == "all" else 10 - ready = False - for key, value in sorted(leaderboard.items(), key=lambda item: item[1], reverse=True): - try: - place += 1 - - if 0 < max < place: - if ready: - break - elif str(ctx.author.id) != key: - continue - places += f"{place}: <@!{key}>\n" - scores += f"{value:,}\n".replace(",", ".") - - if str(ctx.author.id) == key: - ready = True - except: - pass - - embed.add_field(name=f"Sucherin", value=places) - embed.add_field(name=f"Eier", value=scores) - await ctx.send("", embed=embed) - - # @tasks.loop(seconds=1) - # async def reaction_timer(self): - # delete = [] - # - # for message in self.messages: - # if random.random() < 0.6: - # if random.random() < 0.85: - # await message.add_reaction(random.choice(self.data["reactions_add"])) - # else: - # await message.add_reaction(random.choice(self.data["reactions_remove"])) - # - # self.data["message_ids"].append(message.id) - # delete.append(message) - # self.save_data() - # - # if len(delete) > 0: - # for message in delete: - # self.messages.remove(message) - - async def cog_command_error(self, ctx, error): - await handle_error(ctx, error) +import json +import disnake +from disnake.ext import commands +from cogs.help import handle_error + + +class Easter(commands.Cog): + def __init__(self, bot): + self.bot = bot + self.data = self.load_data() + # self.messages = [] + # self.reaction_timer.start() + + def load_data(self): + data_file = open("data/easter.json", mode="r") + return json.load(data_file) + + def save_data(self): + data_file = open("data/easter.json", mode="w") + json.dump(self.data, data_file) + + # @commands.Cog.listener(name="on_message") + # async def hide(self, message): + # if message.author == self.bot.user: + # return + # + # if message.channel.id in self.data["channels"]: + # if random.random() < self.data["probability"]: + # self.messages.append(message) + # + # @commands.Cog.listener(name="on_raw_reaction_add") + # async def seek(self, payload): + # + # if payload.member == self.bot.user or payload.message_id not in self.data["message_ids"]: + # return + # + # modifier = 1 if payload.emoji.name in self.data["reactions_add"] else -1 if payload.emoji.name in self.data[ + # "reactions_remove"] else 0 + # if modifier != 0: + # self.data["message_ids"].remove(payload.message_id) + # self.modify_leaderboard(payload.user_id, modifier) + # + # channel = await self.bot.fetch_channel(payload.channel_id) + # message = await channel.fetch_message(payload.message_id) + # await message.clear_reaction(payload.emoji.name) + # self.save_data() + # + # def modify_leaderboard(self, user_id, modifier): + # if score := self.data["leaderboard"].get(str(user_id)): + # self.data["leaderboard"][str(user_id)] = score + modifier + # else: + # self.data["leaderboard"][str(user_id)] = modifier + # + # self.save_data() + #@help() + @commands.command(name="leaderboard") + async def cmd_leaderboard(self, ctx, all=None): + leaderboard = self.data["leaderboard"] + embed = disnake.Embed(title="Egg-Hunt Leaderboard", description="Wer hat bisher die meisten Eier gefunden???") + embed.set_thumbnail(url="https://www.planet-wissen.de/kultur/religion/ostern/tempxostereiergjpg100~_v-gseagaleriexl.jpg") + + places = scores = "\u200b" + place = 0 + max = 0 if all == "all" else 10 + ready = False + for key, value in sorted(leaderboard.items(), key=lambda item: item[1], reverse=True): + try: + place += 1 + + if 0 < max < place: + if ready: + break + elif str(ctx.author.id) != key: + continue + places += f"{place}: <@!{key}>\n" + scores += f"{value:,}\n".replace(",", ".") + + if str(ctx.author.id) == key: + ready = True + except: + pass + + embed.add_field(name=f"Sucherin", value=places) + embed.add_field(name=f"Eier", value=scores) + await ctx.send("", embed=embed) + + # @tasks.loop(seconds=1) + # async def reaction_timer(self): + # delete = [] + # + # for message in self.messages: + # if random.random() < 0.6: + # if random.random() < 0.85: + # await message.add_reaction(random.choice(self.data["reactions_add"])) + # else: + # await message.add_reaction(random.choice(self.data["reactions_remove"])) + # + # self.data["message_ids"].append(message.id) + # delete.append(message) + # self.save_data() + # + # if len(delete) > 0: + # for message in delete: + # self.messages.remove(message) + + async def cog_command_error(self, ctx, error): + await handle_error(ctx, error) diff --git a/cogs/github.py b/cogs/github.py index c114f7d510da0ed7bbe394c3db46df484d6c5cd1..124fd15128b88bbb194c49e5db6bff9c1f3086d6 100644 --- a/cogs/github.py +++ b/cogs/github.py @@ -1,99 +1,99 @@ -import base64 -import json -import os - -from aiohttp import ClientSession -from discord.ext import commands - -import utils -from cogs.help import help, handle_error, help_category - - -@help_category("github", "Github", "Github Integration in Discord.") -class Github(commands.Cog): - def __init__(self, bot): - self.bot = bot - self.github_file = "data/github.json" - self.data = self.load() - - def load(self): - github_file = open(self.github_file, 'r') - return json.load(github_file) - - def save(self): - github_file = open(self.github_file, 'w') - json.dump(self.data, github_file) - - @help( - category="github", - syntax="!idee <text>", - brief="Stellt eine Idee für Boty zur Abstimmung.", - parameters={ - "text": "Text der Idee.", - }, - description="Mit diesem Kommando kannst du eine Idee für Boty zur Abstimmung einreichen. Sobald genug " - "Reaktionen von anderen Mitgliedern vorhanden sind, wird aus deiner Idee ein Issue in Github " - "erstellt, und sobald möglich kümmert sich jemand darum." - ) - @commands.command(name="idee") - async def cmd_idee(self, ctx): - if ctx.channel.id == int(os.getenv("DISCORD_IDEE_CHANNEL")): - self.data[str(ctx.message.id)] = {"created": False} - await ctx.message.add_reaction(self.bot.get_emoji(int(os.getenv("DISCORD_IDEE_EMOJI")))) - self.save() - - @help( - category="github", - syntax="!card <text>", - brief="Erstellt einen Issue in Github.", - parameters={ - "text": "Text der Idee.", - }, - description="Mit diesem Kommando kannst du einen Issue in Github anlegen.", - mod=True - ) - @commands.command(name="card") - @commands.check(utils.is_mod) - async def cmd_card(self, ctx): - self.data[str(ctx.message.id)] = {"created": False} - await self.create_issue(self.data[str(ctx.message.id)], ctx.message) - self.save() - - @commands.Cog.listener() - async def on_raw_reaction_add(self, payload): - if payload.member == self.bot.user: - return - - if idea := self.data.get(str(payload.message_id)): - if payload.emoji.id == int(os.getenv("DISCORD_IDEE_EMOJI")): - channel = await self.bot.fetch_channel(payload.channel_id) - message = await channel.fetch_message(payload.message_id) - for reaction in message.reactions: - if reaction.emoji.id == int(os.getenv("DISCORD_IDEE_EMOJI")): - if reaction.count >= int(os.getenv("DISCORD_IDEE_REACT_QTY")) and not idea.get("created"): - await self.create_issue(idea, message) - - self.save() - - async def cog_command_error(self, ctx, error): - await handle_error(ctx, error) - - async def create_issue(self, idea, message): - async with ClientSession() as session: - auth = base64.b64encode( - f'{os.getenv("DISCORD_GITHUB_USER")}:{os.getenv("DISCORD_GITHUB_TOKEN")}'.encode('utf-8')).decode( - "utf-8") - headers = {"Authorization": f"Basic {auth}", "Content-Type": "application/json"} - - async with session.post(os.getenv("DISCORD_GITHUB_ISSUE_URL"), - headers=headers, - json={'title': message.content[6:]}) as r: - if r.status == 201: - js = await r.json() - - idea["created"] = True - idea["number"] = js["number"] - idea["html_url"] = js["html_url"] - - await message.reply( - f"Danke <@!{message.author.id}> für deinen Vorschlag. Ich habe für dich gerade folgenden Issue in Github erstellt: {idea['html_url']}") +import base64 +import json +import os + +from aiohttp import ClientSession +from disnake.ext import commands + +import utils +from cogs.help import help, handle_error, help_category + + +@help_category("github", "Github", "Github Integration in Discord.") +class Github(commands.Cog): + def __init__(self, bot): + self.bot = bot + self.github_file = "data/github.json" + self.data = self.load() + + def load(self): + github_file = open(self.github_file, 'r') + return json.load(github_file) + + def save(self): + github_file = open(self.github_file, 'w') + json.dump(self.data, github_file) + + @help( + category="github", + syntax="!idee <text>", + brief="Stellt eine Idee für Boty zur Abstimmung.", + parameters={ + "text": "Text der Idee.", + }, + description="Mit diesem Kommando kannst du eine Idee für Boty zur Abstimmung einreichen. Sobald genug " + "Reaktionen von anderen Mitgliedern vorhanden sind, wird aus deiner Idee ein Issue in Github " + "erstellt, und sobald möglich kümmert sich jemand darum." + ) + @commands.command(name="idee") + async def cmd_idee(self, ctx): + if ctx.channel.id == int(os.getenv("DISCORD_IDEE_CHANNEL")): + self.data[str(ctx.message.id)] = {"created": False} + await ctx.message.add_reaction(self.bot.get_emoji(int(os.getenv("DISCORD_IDEE_EMOJI")))) + self.save() + + @help( + category="github", + syntax="!card <text>", + brief="Erstellt einen Issue in Github.", + parameters={ + "text": "Text der Idee.", + }, + description="Mit diesem Kommando kannst du einen Issue in Github anlegen.", + mod=True + ) + @commands.command(name="card") + @commands.check(utils.is_mod) + async def cmd_card(self, ctx): + self.data[str(ctx.message.id)] = {"created": False} + await self.create_issue(self.data[str(ctx.message.id)], ctx.message) + self.save() + + @commands.Cog.listener() + async def on_raw_reaction_add(self, payload): + if payload.member == self.bot.user: + return + + if idea := self.data.get(str(payload.message_id)): + if payload.emoji.id == int(os.getenv("DISCORD_IDEE_EMOJI")): + channel = await self.bot.fetch_channel(payload.channel_id) + message = await channel.fetch_message(payload.message_id) + for reaction in message.reactions: + if reaction.emoji.id == int(os.getenv("DISCORD_IDEE_EMOJI")): + if reaction.count >= int(os.getenv("DISCORD_IDEE_REACT_QTY")) and not idea.get("created"): + await self.create_issue(idea, message) + + self.save() + + async def cog_command_error(self, ctx, error): + await handle_error(ctx, error) + + async def create_issue(self, idea, message): + async with ClientSession() as session: + auth = base64.b64encode( + f'{os.getenv("DISCORD_GITHUB_USER")}:{os.getenv("DISCORD_GITHUB_TOKEN")}'.encode('utf-8')).decode( + "utf-8") + headers = {"Authorization": f"Basic {auth}", "Content-Type": "application/json"} + + async with session.post(os.getenv("DISCORD_GITHUB_ISSUE_URL"), + headers=headers, + json={'title': message.content[6:]}) as r: + if r.status == 201: + js = await r.json() + + idea["created"] = True + idea["number"] = js["number"] + idea["html_url"] = js["html_url"] + + await message.reply( + f"Danke <@!{message.author.id}> für deinen Vorschlag. Ich habe für dich gerade folgenden Issue in Github erstellt: {idea['html_url']}") diff --git a/cogs/help.py b/cogs/help.py index ae37ee443d3d4f6d2ddf9b48352345bffe04bdde..3d652ae27468c5a7a3b1ec95a8e3fac41da42c1e 100644 --- a/cogs/help.py +++ b/cogs/help.py @@ -1,240 +1,241 @@ -from discord.ext import commands -import inspect -import utils -import re -import discord -import collections - -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={}, - category=None): - 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={}, category=None, command_group=''): - 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'] - - cmd_name = f"{ctx.command.parent} {ctx.command.name}" if ctx.command.parent else f"{ctx.command.name}" - - msg = ( - f"Fehler! Du hast ein Argument vergessen. Für weitere Hilfe gib `!help {cmd_name}` ein. \n" - f"`Syntax: {data['command'][cmd_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 = "Boty 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 = discord.Embed(title=title, - description=helptext, - color=19607) - await utils.send_dm(ctx.author, "", embed=embed) - helptext = "" - msgcount = msgcount + 1 - title = f"Boty hilft dir! (Fortsetzung {msgcount})" - helptext += text - text = "" - - embed = discord.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 = discord.Embed(title=title, - description=text, - color=19607) - await utils.send_dm(ctx.author, text) # , embed=embed) - - for subname in data['command']: - if subname.startswith(f"{name} "): - await self.help_card(ctx, subname) - - @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 = "Boty 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 = discord.Embed(title=title, - description=helptext, - color=19607) - await utils.send_dm(ctx.author, "", embed=embed) - helptext = "" - msgcount = msgcount + 1 - title = f"Boty hilft dir! (Fortsetzung {msgcount})" - helptext += text - text = "" - - embed = discord.Embed(title=title, - description=helptext, - color=19607) - await utils.send_dm(ctx.author, "", embed=embed) +from disnake.ext import commands +import inspect +import utils +import re +import disnake +import collections + +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={}, + category=None): + 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={}, category=None, command_group=''): + 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'] + + cmd_name = f"{ctx.command.parent} {ctx.command.name}" if ctx.command.parent else f"{ctx.command.name}" + + msg = ( + f"Fehler! Du hast ein Argument vergessen. Für weitere Hilfe gib `!help {cmd_name}` ein. \n" + f"`Syntax: {data['command'][cmd_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 = "Boty 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"Boty 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): + return #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) + text += "==========================\n" + await utils.send_dm(ctx.author, text) # , embed=embed) + + for subname in data['command']: + if subname.startswith(f"{name} "): + await self.help_card(ctx, subname) + + @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 = "Boty 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"Boty 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/learninggroups.py b/cogs/learninggroups.py index 02f20a65ab197a18077ad9314d32e4eee4711c03..675db08148ed4c2b1a404a1d1b5e17b408dabdcb 100644 --- a/cogs/learninggroups.py +++ b/cogs/learninggroups.py @@ -1,482 +1,994 @@ -import json -import os -import re -import time - -import discord -from discord.ext import commands - -import utils -from cogs.help import help, handle_error, help_category - -""" - Environment Variablen: - DISCORD_LEARNINGGROUPS_OPEN - ID der Kategorie für offene Lerngruppen - DISCORD_LEARNINGGROUPS_CLOSE - ID der Kategorie für geschlossene Lerngruppen - DISCORD_LEARNINGGROUPS_ARCHIVE - ID der Kategorie für archivierte Lerngruppen - DISCORD_LEARNINGGROUPS_REQUEST - ID des Channels in welchem Requests vom Bot eingestellt werden - DISCORD_LEARNINGGROUPS_INFO - ID des Channels in welchem die Lerngruppen-Informationen gepostet/aktualisert werden - DISCORD_LEARNINGGROUPS_FILE - Name der Datei mit Verwaltungsdaten der Lerngruppen (minimaler Inhalt: {"requested": {},"groups": {}}) - DISCORD_LEARNINGGROUPS_COURSE_FILE - Name der Datei welche die Kursnamen für die Lerngruppen-Informationen enthält (minimalter Inhalt: {}) - DISCORD_MOD_ROLE - ID der Moderator Rolle von der erweiterte Lerngruppen-Actionen ausgeführt werden dürfen -""" - - -@help_category("learninggroups", "Lerngruppen", - "Mit dem Lerngruppen-Feature kannst du Lerngruppen-Kanäle beantragen und/oder diese rudimentär verwalten.", - "Hier kannst du Lerngruppen-Kanäle anlegen, beantragen und verwalten.") -class LearningGroups(commands.Cog): - def __init__(self, bot): - self.bot = bot - # ratelimit 2 in 10 minutes (305 * 2 = 610 = 10 minutes and 10 seconds) - self.rename_ratelimit = 305 - self.msg_max_len = 2000 - self.category_open = os.getenv('DISCORD_LEARNINGGROUPS_OPEN') - self.category_close = os.getenv('DISCORD_LEARNINGGROUPS_CLOSE') - self.category_archive = os.getenv('DISCORD_LEARNINGGROUPS_ARCHIVE') - self.channel_request = os.getenv('DISCORD_LEARNINGGROUPS_REQUEST') - self.channel_info = os.getenv('DISCORD_LEARNINGGROUPS_INFO') - self.group_file = os.getenv('DISCORD_LEARNINGGROUPS_FILE') - self.header_file = os.getenv('DISCORD_LEARNINGGROUPS_COURSE_FILE') - self.mod_role = os.getenv("DISCORD_MOD_ROLE") - self.groups = {} - self.header = {} - self.load_groups() - self.load_header() - - def load_header(self): - file = open(self.header_file, mode='r') - self.header = json.load(file) - - def save_header(self): - file = open(self.header_file, mode='w') - json.dump(self.header, file) - - def load_groups(self): - group_file = open(self.group_file, mode='r') - self.groups = json.load(group_file) - - def save_groups(self): - group_file = open(self.group_file, mode='w') - json.dump(self.groups, group_file) - - def arg_open_to_bool(self, arg_open): - if arg_open in ["offen", "open"]: - return True - if arg_open in ["geschlossen", "closed", "close"]: - return False - return None - - def is_request_owner(self, request, member): - return request["owner_id"] == member.id - - def is_group_owner(self, channel, member): - channel_config = self.groups["groups"].get(str(channel.id)) - if channel_config: - return channel_config["owner_id"] == member.id - return False - - def is_mod(self, member): - roles = member.roles - for role in roles: - if role.id == int(self.mod_role): - return True - - return False - - def is_group_request_message(self, message): - return len(message.embeds) > 0 and message.embeds[0].title == "Lerngruppenanfrage!" - - async def is_channel_config_valid(self, ctx, channel_config, command=None): - if channel_config['is_open'] is None: - if command: - await ctx.channel.send( - f"Fehler! Bitte gib an ob die Gruppe **offen** (**open**) oder **geschlossen** (**closed**) ist. Gib `!help {command}` für Details ein.") - return False - if not re.match(r"^[0-9]+$", channel_config['course']): - if command: - await ctx.channel.send( - f"Fehler! Die Kursnummer muss numerisch sein. Gib `!help {command}` für Details ein.") - return False - if not re.match(r"^(sose|wise)[0-9]{2}$", channel_config['semester']): - if command: - await ctx.channel.send( - f"Fehler! Das Semester muss mit **sose** oder **wise** angegeben werden gefolgt von der **zweistelligen Jahreszahl**. Gib `!help {command}` für Details ein.") - return False - return True - - async def check_rename_rate_limit(self, channel_config): - if channel_config.get("last_rename") is None: - return False - now = int(time.time()) - seconds = channel_config["last_rename"] + self.rename_ratelimit - now - if seconds > 0: - channel = await self.bot.fetch_channel(int(channel_config["channel_id"])) - await channel.send(f"Fehler! Du kannst diese Aktion erst wieder in {seconds} Sekunden ausführen.") - return seconds > 0 - - async def category_of_channel(self, is_open): - category_to_fetch = self.category_open if is_open else self.category_close - category = await self.bot.fetch_channel(category_to_fetch) - return category - - def full_channel_name(self, channel_config): - return (f"{f'🌲' if channel_config['is_open'] else f'🛑'}" - f"{channel_config['course']}-{channel_config['name']}-{channel_config['semester']}") - - async def update_groupinfo(self): - info_message_ids = self.groups.get("messageids") - channel = await self.bot.fetch_channel(int(self.channel_info)) - - for info_message_id in info_message_ids: - message = await channel.fetch_message(info_message_id) - await message.delete() - - info_message_ids = [] - - msg = f"**Lerngruppen**\n\n" - course_msg = "" - sorted_groups = sorted(self.groups["groups"].values( - ), key=lambda group: f"{group['course']}-{group['name']}") - open_groups = [group for group in sorted_groups if group['is_open']] - courseheader = None - for group in open_groups: - - if group['course'] != courseheader: - if len(msg) + len(course_msg) > self.msg_max_len: - message = await channel.send(msg) - info_message_ids.append(message.id) - msg = course_msg - course_msg = "" - else: - msg += course_msg - course_msg = "" - header = self.header.get(group['course']) - if header: - course_msg += f"**{header}**\n" - else: - course_msg += f"**{group['course']} - -------------------------------------**\n" - courseheader = group['course'] - - groupchannel = await self.bot.fetch_channel(int(group['channel_id'])) - course_msg += f" {groupchannel.mention}\n" - - msg += course_msg - message = await channel.send(msg) - info_message_ids.append(message.id) - self.groups["messageids"] = info_message_ids - self.save_groups() - - async def archive(self, channel): - category = await self.bot.fetch_channel(self.category_archive) - await self.move_channel(channel, category) - await channel.edit(name=f"archiv-${channel.name[1:]}") - self.remove_group(channel) - - async def set_channel_state(self, channel, is_open): - channel_config = self.groups["groups"][str(channel.id)] - if await self.check_rename_rate_limit(channel_config): - return # prevent api requests when ratelimited - - was_open = channel_config["is_open"] - if (was_open == is_open): - return # prevent api requests when nothing changed - - channel_config["is_open"] = is_open - channel_config["last_rename"] = int(time.time()) - - await channel.edit(name=self.full_channel_name(channel_config)) - category = await self.category_of_channel(is_open) - await self.move_channel(channel, category) - await self.update_groupinfo() - self.save_groups() - - async def set_channel_name(self, channel, name): - channel_config = self.groups["groups"][str(channel.id)] - - if await self.check_rename_rate_limit(channel_config): - return # prevent api requests when ratelimited - - channel_config["name"] = name - channel_config["last_rename"] = int(time.time()) - - await channel.edit(name=self.full_channel_name(channel_config)) - await self.update_groupinfo() - self.save_groups() - - async def move_channel(self, channel, category): - for sortchannel in category.text_channels: - if sortchannel.name[1:] > channel.name[1:]: - await channel.move(category=category, before=sortchannel, sync_permissions=True) - return - await channel.move(category=category, sync_permissions=True, end=True) - - async def add_requested_group_channel(self, message, direct=False): - channel_config = self.groups["requested"].get(str(message.id)) - - category = await self.category_of_channel(channel_config["is_open"]) - channel_name = self.full_channel_name(channel_config) - channel = await category.create_text_channel(channel_name) - channel_config["channel_id"] = str(channel.id) - - user = await self.bot.fetch_user(channel_config["owner_id"]) - await utils.send_dm(user, - f"Deine Lerngruppe <#{channel.id}> wurde eingerichtet. Du kannst mit **!open** und **!close** den Status dieser Gruppe setzen. Bedenke aber bitte, dass die Discord API die möglichen Namensänderungen stark limitiert. Daher ist nur ein Statuswechsel alle **5 Minuten** möglich.") - - self.groups["groups"][str(channel.id)] = channel_config - - self.remove_group_request(message) - if not direct: - await message.delete() - - await self.update_groupinfo() - self.save_groups() - - def remove_group_request(self, message): - del self.groups["requested"][str(message.id)] - self.save_groups() - - def remove_group(self, channel): - del self.groups["groups"][str(channel.id)] - self.save_groups() - - @help( - category="learninggroups", - brief="Erstellt aus den Lerngruppen-Kanälen eine Datendatei. ", - description=( - "Initialisiert alle Gruppen in den Kategorien für offene und geschlossene Lerngruppen und baut die Verwaltungsdaten dazu auf. " - "Die Lerngruppen-Kanal-Namen müssen hierfür zuvor ins Format #{symbol}{kursnummer}-{name}-{semester} gebracht werden. " - "Als Owner wird der ausführende Account für alle Lerngruppen gesetzt. " - "Wenn die Verwaltungsdatenbank nicht leer ist, wird das Kommando nicht ausgeführt. " - ), - mod=True - ) - @commands.command(name="init-groups") - @commands.check(utils.is_mod) - async def cmd_init_groups(self, ctx): - if len(self.groups["groups"]) > 0: - await ctx.channel.send("Nope. Das sollte ich lieber nicht tun.") - return - - msg = "Initialisierung abgeschlossen:\n" - for is_open in [True, False]: - category = await self.category_of_channel(is_open) - msg += f"**{category.name}**\n" - - for channel in category.text_channels: - result = re.match( - r"([0-9]{4,6})-(.*)-([a-z0-9]+)$", channel.name[1:]) - if result is None: - await utils.send_dm(ctx.author, f"Abbruch! Channelname hat falsches Format: {channel.name}") - self.groups["groups"] = {} - return - - course, name, semester = result.group(1, 2, 3) - - channel_config = {"owner_id": ctx.author.id, "course": course, "name": name, "semester": semester, - "is_open": is_open, "channel_id": str(channel.id)} - if not await self.is_channel_config_valid(ctx, channel_config): - await utils.send_dm(ctx.author, f"Abbruch! Channelname hat falsches Format: {channel.name}") - self.groups["groups"] = {} - return - - self.groups["groups"][str(channel.id)] = channel_config - msg += f" #{course}-{name}-{semester}\n" - - await utils.send_dm(ctx.author, msg) - await self.update_groupinfo() - self.save_groups() - - @help( - category="learninggroups", - syntax="!add-course <coursenumber> <name...>", - brief="Fügt einen Kurs als neue Ãœberschrift in Botys Lerngruppen-Liste (Kanal #lerngruppen) hinzu. Darf Leerzeichen enthalten, Anführungszeichen sind nicht erforderlich.", - example="!add-course 1141 Mathematische Grundlagen", - parameters={ - "coursenumber": "Nummer des Kurses wie von der Fernuni angegeben (ohne führende Nullen z. B. 1142).", - "name...": "Ein frei wählbarer Text (darf Leerzeichen enthalten).", - }, - description="Kann auch zum Bearbeiten einer Ãœberschrift genutzt werden. Bei bereits existierender Kursnummer wird die Ãœberschrift abgeändert", - mod=True - ) - @commands.command(name="add-course") - @commands.check(utils.is_mod) - async def cmd_add_course(self, ctx, arg_course, *arg_name): - if not re.match(r"[0-9]+", arg_course): - await ctx.channel.send( - f"Fehler! Die Kursnummer muss numerisch sein. Gib `!help add-course` für Details ein.") - return - - self.header[arg_course] = f"{arg_course} - {' '.join(arg_name)}" - self.save_header() - await self.update_groupinfo() - - @help( - category="learninggroups", - syntax="!add-group <coursenumber> <name> <semester> <status> <@usermention>", - example="!add-group 1142 mathegenies sose22 clsoed @someuser", - brief="Fügt einen Lerngruppen-Kanal hinzu. Der Name darf keine Leerzeichen enthalten.", - parameters={ - "coursenumber": "Nummer des Kurses wie von der Fernuni angegeben (ohne führende Nullen z. B. 1142).", - "name": "Ein frei wählbarer Text ohne Leerzeichen. Bindestriche sind zulässig.", - "semester": "Das Semester, für welches diese Lerngruppe erstellt werden soll. sose oder wise gefolgt von der zweistelligen Jahreszahl (z. B. sose22).", - "status": "Gibt an ob die Lerngruppe für weitere Lernwillige geöffnet ist (open) oder nicht (closed).", - "@usermention": "Der so erwähnte Benutzer wird als Besitzer für die Lerngruppe gesetzt." - }, - mod=True - ) - @commands.command(name="add-group") - @commands.check(utils.is_mod) - async def cmd_add_group(self, ctx, arg_course, arg_name, arg_semester, arg_open, arg_owner: discord.Member): - is_open = self.arg_open_to_bool(arg_open) - channel_config = {"owner_id": arg_owner.id, "course": arg_course, "name": arg_name, "semester": arg_semester, - "is_open": is_open} - - if not await self.is_channel_config_valid(ctx, channel_config, ctx.command.name): - return - - self.groups["requested"][str(ctx.message.id)] = channel_config - self.save_groups() - await self.add_requested_group_channel(ctx.message, direct=True) - - @help( - category="learninggroups", - syntax="!request-group <coursenumber> <name> <semester> <status>", - brief="Stellt eine Anfrage für einen neuen Lerngruppen-Kanal.", - example="!request-group 1142 mathegenies sose22 closed", - description=("Moderatorinnen können diese Anfrage bestätigen, dann wird die Gruppe eingerichtet. " - "Der Besitzer der Gruppe ist der Benutzer der die Anfrage eingestellt hat."), - parameters={ - "coursenumber": "Nummer des Kurses, wie von der FernUni angegeben (ohne führende Nullen z. B. 1142).", - "name": "Ein frei wählbarer Text ohne Leerzeichen.", - "semester": "Das Semester, für welches diese Lerngruppe erstellt werden soll. sose oder wise gefolgt von der zweistelligen Jahrenszahl (z. B. sose22).", - "status": "Gibt an ob die Lerngruppe für weitere Lernwillige geöffnet ist (open) oder nicht (closed)." - } - ) - @commands.command(name="request-group") - async def cmd_request_group(self, ctx, arg_course, arg_name, arg_semester, arg_open): - is_open = self.arg_open_to_bool(arg_open) - channel_config = {"owner_id": ctx.author.id, "course": arg_course, "name": arg_name, "semester": arg_semester, - "is_open": is_open} - - if not await self.is_channel_config_valid(ctx, channel_config, ctx.command.name): - return - - channel_name = self.full_channel_name(channel_config) - embed = discord.Embed(title="Lerngruppenanfrage!", - description=f"<@!{ctx.author.id}> möchte gerne die Lerngruppe **#{channel_name}** eröffnen", - color=19607) - - channel_request = await self.bot.fetch_channel(int(self.channel_request)) - message = await channel_request.send(embed=embed) - await message.add_reaction("ðŸ‘") - await message.add_reaction("🗑ï¸") - - self.groups["requested"][str(message.id)] = channel_config - self.save_groups() - - @help( - category="learninggroups", - brief="Öffnet den Lerngruppen-Kanal wenn du die Besitzerin bist. ", - description=("Muss im betreffenden Lerngruppen-Kanal ausgeführt werden. " - "Verschiebt den Lerngruppen-Kanal in die Kategorie für offene Kanäle und ändert das Icon. " - "Diese Aktion kann nur vom Besitzer der Lerngruppe ausgeführt werden. ") - ) - @commands.command(name="open") - async def cmd_open(self, ctx): - if self.is_group_owner(ctx.channel, ctx.author) or utils.is_mod(ctx): - await self.set_channel_state(ctx.channel, is_open=True) - - @help( - category="learninggroups", - brief="Schließt den Lerngruppen-Kanal wenn du die Besitzerin bist. ", - description=("Muss im betreffenden Lerngruppen-Kanal ausgeführt werden. " - "Verschiebt den Lerngruppen-Kanal in die Kategorie für geschlossene Kanäle und ändert das Icon. " - "Diese Aktion kann nur vom Besitzer der Lerngruppe ausgeführt werden. ") - ) - @commands.command(name="close") - async def cmd_close(self, ctx): - if self.is_group_owner(ctx.channel, ctx.author) or utils.is_mod(ctx): - await self.set_channel_state(ctx.channel, is_open=False) - - @help( - category="learninggroups", - syntax="!rename <name>", - brief="Ändert den Namen des Lerngruppen-Kanals, in dem das Komando ausgeführt wird.", - example="!rename matheluschen", - description="Aus #1142-matheprofis-sose22 wird nach dem Aufruf des Beispiels #1142-matheluschen-sose22.", - parameters={ - "name": "Der neue Name der Lerngruppe ohne Leerzeichen." - }, - mod=True - ) - @commands.command(name="rename") - @commands.check(utils.is_mod) - async def cmd_rename(self, ctx, arg_name): - await self.set_channel_name(ctx.channel, arg_name) - - @help( - category="learninggroups", - brief="Archiviert den Lerngruppen-Kanal", - description="Verschiebt den Lerngruppen-Kanal, in welchem dieses Kommando ausgeführt wird, ins Archiv.", - mod=True - ) - @commands.command(name="archive") - @commands.check(utils.is_mod) - async def cmd_archive(self, ctx): - await self.archive(ctx.channel) - - @help( - category="learninggroups", - syntax="!owner <@usermention>", - example="!owner @someuser", - brief="Setzt die Besitzerin eines Lerngruppen-Kanals", - description="Muss im betreffenden Lerngruppen-Kanal ausgeführt werden. ", - parameters={ - "@usermention": "Der neue Besitzer der Lerngruppe." - }, - mod=True - ) - @commands.command(name="owner") - @commands.check(utils.is_mod) - async def cmd_owner(self, ctx, arg_owner: discord.Member): - channel_config = self.groups["groups"].get(str(ctx.channel.id)) - if channel_config: - channel_config["owner_id"] = arg_owner.id - self.save_groups() - await ctx.channel.send(f"Glückwunsch {arg_owner.mention}! Du bist jetzt die Besitzerin dieser Lerngruppe.") - - @help( - category="learninggroups", - brief="Zeigt die Besitzerin eines Lerngruppen-Kanals an.", - description="Muss im betreffenden Lerngruppen-Kanal ausgeführt werden.", - mod=True - ) - @commands.command(name="show-owner") - @commands.check(utils.is_mod) - async def cmd_show_owner(self, ctx): - channel_config = self.groups["groups"].get(str(ctx.channel.id)) - owner_id = channel_config.get("owner_id") - if owner_id: - user = await self.bot.fetch_user(owner_id) - await ctx.channel.send(f"Besitzer: @{user.name}") - - @commands.Cog.listener() - async def on_raw_reaction_add(self, payload): - if payload.user_id == self.bot.user.id: - return - - channel = await self.bot.fetch_channel(payload.channel_id) - message = await channel.fetch_message(payload.message_id) - request = self.groups["requested"].get(str(message.id)) - - if payload.emoji.name in ["ðŸ‘"] and self.is_group_request_message(message) and self.is_mod(payload.member): - await self.add_requested_group_channel(message, direct=False) - - if payload.emoji.name in ["🗑ï¸"] and self.is_group_request_message(message) and ( - self.is_request_owner(request, payload.member) or self.is_mod(payload.member)): - self.remove_group_request(message) - await message.delete() - - async def cog_command_error(self, ctx, error): - await handle_error(ctx, error) +import copy +import json +import os +import re +import time +from enum import Enum +from typing import Union + +import disnake +from disnake import InteractionMessage +from disnake.ext import commands +from disnake.ui import Button + +import utils +from cogs.help import help, handle_error, help_category + +""" + Environment Variablen: + DISCORD_LEARNINGGROUPS_OPEN - ID der Kategorie für offene Lerngruppen + DISCORD_LEARNINGGROUPS_CLOSE - ID der Kategorie für private Lerngruppen + DISCORD_LEARNINGGROUPS_ARCHIVE - ID der Kategorie für archivierte Lerngruppen + DISCORD_LEARNINGGROUPS_REQUEST - ID des Channels in welchem Requests vom Bot eingestellt werden + DISCORD_LEARNINGGROUPS_INFO - ID des Channels in welchem die Lerngruppen-Informationen gepostet/aktualisert werden + DISCORD_LEARNINGGROUPS_FILE - Name der Datei mit Verwaltungsdaten der Lerngruppen (minimaler Inhalt: {"requested": {},"groups": {}}) + DISCORD_LEARNINGGROUPS_COURSE_FILE - Name der Datei welche die Kursnamen für die Lerngruppen-Informationen enthält (minimalter Inhalt: {}) + DISCORD_MOD_ROLE - ID der Moderator Rolle von der erweiterte Lerngruppen-Actionen ausgeführt werden dürfen +""" + +LG_OPEN_SYMBOL = f'🌲' +LG_CLOSE_SYMBOL = f'🛑' +LG_PRIVATE_SYMBOL = f'🚪' +LG_LISTED_SYMBOL = f'📖' + + +class GroupState(Enum): + OPEN = "OPEN" + CLOSED = "CLOSED" + PRIVATE = "PRIVATE" + ARCHIVED = "ARCHIVED" + REMOVED = "REMOVED" + + +@help_category("learninggroups", "Lerngruppen", + "Mit dem Lerngruppen-Feature kannst du Lerngruppen-Kanäle beantragen und verwalten.", + "Hier kannst du Lerngruppen-Kanäle anlegen, beantragen und verwalten.") +class LearningGroups(commands.Cog): + def __init__(self, bot): + self.bot = bot + # ratelimit 2 in 10 minutes (305 * 2 = 610 = 10 minutes and 10 seconds) + self.rename_ratelimit = 305 + self.msg_max_len = 2000 + + self.categories = { + GroupState.OPEN: os.getenv('DISCORD_LEARNINGGROUPS_OPEN'), + GroupState.CLOSED: os.getenv('DISCORD_LEARNINGGROUPS_CLOSE'), + GroupState.PRIVATE: os.getenv('DISCORD_LEARNINGGROUPS_PRIVATE'), + GroupState.ARCHIVED: os.getenv('DISCORD_LEARNINGGROUPS_ARCHIVE') + } + self.symbols = { + GroupState.OPEN: LG_OPEN_SYMBOL, + GroupState.CLOSED: LG_CLOSE_SYMBOL, + GroupState.PRIVATE: LG_PRIVATE_SYMBOL + } + self.channel_request = os.getenv('DISCORD_LEARNINGGROUPS_REQUEST') + self.channel_info = os.getenv('DISCORD_LEARNINGGROUPS_INFO') + self.group_file = os.getenv('DISCORD_LEARNINGGROUPS_FILE') + self.header_file = os.getenv('DISCORD_LEARNINGGROUPS_COURSE_FILE') + self.support_channel = os.getenv('DISCORD_SUPPORT_CHANNEL') + self.mod_role = os.getenv("DISCORD_MOD_ROLE") + self.guild_id = os.getenv("DISCORD_GUILD") + self.groups = {} # owner and learninggroup-member ids + self.channels = {} # complete channel configs + self.header = {} # headlines for statusmessage + self.load_groups() + self.load_header() + + @commands.Cog.listener() + async def on_button_click(self, interaction: InteractionMessage): + button: Button = interaction.component + + if button.custom_id == "learninggroups:group_yes": + await self.on_group_request(True, button, interaction) + elif button.custom_id == "learninggroups:group_no": + await self.on_group_request(False, button, interaction) + elif button.custom_id == "learninggroups:join_yes": + await self.on_join_request(True, button, interaction) + elif button.custom_id == "learninggroups:join_no": + await self.on_join_request(False, button, interaction) + + + @commands.Cog.listener(name="on_ready") + async def on_ready(self): + await self.update_channels() + + def load_header(self): + file = open(self.header_file, mode='r') + self.header = json.load(file) + + def save_header(self): + file = open(self.header_file, mode='w') + json.dump(self.header, file) + + def load_groups(self): + group_file = open(self.group_file, mode='r') + self.groups = json.load(group_file) + if not self.groups.get("groups"): + self.groups['groups'] = {} + if not self.groups.get("requested"): + self.groups['requested'] = {} + if not self.groups.get("messageids"): + self.groups['messageids'] = [] + + for _, group in self.groups['requested'].items(): + group["state"] = GroupState[group["state"]] + + async def save_groups(self): + await self.update_channels() + group_file = open(self.group_file, mode='w') + + groups = copy.deepcopy(self.groups) + + for _, group in groups['requested'].items(): + group["state"] = group["state"].name + json.dump(groups, group_file) + + def arg_state_to_group_state(self, state: str): + if state in ["offen", "open", "o"]: + return GroupState.OPEN + if state in ["geschlossen", "closed", "close"]: + return GroupState.CLOSED + if state in ["private", "privat"]: + return GroupState.PRIVATE + return None + + def is_request_owner(self, request, member): + return request["owner_id"] == member.id + + def is_group_owner(self, channel, member): + channel_config = self.groups["groups"].get(str(channel.id)) + if channel_config: + return channel_config["owner_id"] == member.id + return False + + def is_mod(self, member): + roles = member.roles + for role in roles: + if role.id == int(self.mod_role): + return True + + return False + + def is_group_request_message(self, message): + return len(message.embeds) > 0 and message.embeds[0].title == "Lerngruppenanfrage!" + + async def is_channel_config_valid(self, ctx, channel_config, command=None): + if channel_config['state'] is None: + if command: + await ctx.channel.send( + f"Fehler! Bitte gib an ob die Gruppe **offen** (**open**) **geschlossen** (**closed**) oder **privat** (**private**) ist. Gib `!help {command}` für Details ein.") + return False + if not re.match(r"^[0-9]+$", channel_config['course']): + if command: + await ctx.channel.send( + f"Fehler! Die Kursnummer muss numerisch sein. Gib `!help {command}` für Details ein.") + return False + if not re.match(r"^(sose|wise)[0-9]{2}$", channel_config['semester']): + if command: + await ctx.channel.send( + f"Fehler! Das Semester muss mit **sose** oder **wise** angegeben werden gefolgt von der **zweistelligen Jahreszahl**. Gib `!help {command}` für Details ein.") + return False + return True + + async def check_rename_rate_limit(self, channel_config): + if channel_config.get("last_rename") is None: + return False + now = int(time.time()) + seconds = channel_config["last_rename"] + self.rename_ratelimit - now + if seconds > 0: + channel = await self.bot.fetch_channel(int(channel_config["channel_id"])) + await channel.send(f"Discord limitiert die Aufrufe für manche Funktionen, daher kannst du diese Aktion erst wieder in {seconds} Sekunden ausführen.") + return seconds > 0 + + async def category_of_channel(self, state: GroupState): + category_to_fetch = self.categories[state] + category = await self.bot.fetch_channel(category_to_fetch) + return category + + def full_channel_name(self, channel_config): + return (f"{self.symbols[channel_config['state']]}" + f"{channel_config['course']}-{channel_config['name']}-{channel_config['semester']}" + f"{LG_LISTED_SYMBOL if channel_config['is_listed'] else ''}") + + async def update_statusmessage(self): + info_message_ids = self.groups.get("messageids") + channel = await self.bot.fetch_channel(int(self.channel_info)) + + for info_message_id in info_message_ids: + message = await channel.fetch_message(info_message_id) + await message.delete() + + info_message_ids = [] + + msg = f"**Lerngruppen**\n\n" + course_msg = "" + sorted_channels = sorted(self.channels.values( + ), key=lambda channel: f"{channel['course']}-{channel['name']}") + open_channels = [channel for channel in sorted_channels if channel['state'] in [GroupState.OPEN] + or channel['is_listed']] + courseheader = None + no_headers = [] + for lg_channel in open_channels: + + if lg_channel['course'] != courseheader: + if len(msg) + len(course_msg) > self.msg_max_len: + message = await channel.send(msg) + info_message_ids.append(message.id) + msg = course_msg + course_msg = "" + else: + msg += course_msg + course_msg = "" + header = self.header.get(lg_channel['course']) + if header: + course_msg += f"**{header}**\n" + else: + course_msg += f"**{lg_channel['course']} - -------------------------------------**\n" + no_headers.append(lg_channel['course']) + courseheader = lg_channel['course'] + + groupchannel = await self.bot.fetch_channel(int(lg_channel['channel_id'])) + course_msg += f" {groupchannel.mention}" + + if lg_channel['is_listed'] and lg_channel['state'] == GroupState.PRIVATE: + group_config = self.groups["groups"].get(lg_channel['channel_id']) + if group_config: + user = await self.bot.fetch_user(group_config['owner_id']) + if user: + course_msg += f" **@{user.name}#{user.discriminator}**" + course_msg += f"\n **↳** `!lg join {groupchannel.id}`" + course_msg += "\n" + + msg += course_msg + message = await channel.send(msg) + if len(no_headers) > 0: + support_channel = await self.bot.fetch_channel(int(self.support_channel)) + if support_channel: + await support_channel.send(f"Es fehlen noch Ãœberschriften für folgende Kurse in der Lerngruppenübersicht: **{', '.join(no_headers)}**") + info_message_ids.append(message.id) + self.groups["messageids"] = info_message_ids + await self.save_groups() + + async def archive(self, channel): + group_config = self.groups["groups"].get(str(channel.id)) + if not group_config: + await channel.send("Das ist kein Lerngruppenkanal.") + return + category = await self.bot.fetch_channel(self.categories[GroupState.ARCHIVED]) + await self.move_channel(channel, category) + await channel.edit(name=f"archiv-${channel.name[1:]}") + await self.remove_group(channel) + await self.update_permissions(channel) + + async def set_channel_state(self, channel, state: GroupState = None): + channel_config = self.channels[str(channel.id)] + if await self.check_rename_rate_limit(channel_config): + return False # prevent api requests when ratelimited + + if state is not None: + old_state = channel_config["state"] + if old_state == state: + return False # prevent api requests when nothing changed + channel_config["state"] = state + await self.alter_channel(channel, channel_config) + return True + + async def set_channel_listing(self, channel, is_listed): + channel_config = self.channels[str(channel.id)] + if await self.check_rename_rate_limit(channel_config): + return False # prevent api requests when ratelimited + if channel_config["state"] in [GroupState.CLOSED, GroupState.PRIVATE]: + was_listed = channel_config["is_listed"] + if was_listed == is_listed: + return False # prevent api requests when nothing changed + channel_config["is_listed"] = is_listed + await self.alter_channel(channel, channel_config) + return True + + async def alter_channel(self, channel, channel_config): + self.groups["groups"][str(channel.id)]["last_rename"] = int(time.time()) + await channel.edit(name=self.full_channel_name(channel_config)) + category = await self.category_of_channel(channel_config["state"]) + await self.move_channel(channel, category, + sync=True if channel_config["state"] in [GroupState.OPEN, GroupState.CLOSED] else False) + await self.save_groups() + await self.update_statusmessage() + return True + + async def set_channel_name(self, channel, name): + channel_config = self.channels[str(channel.id)] + + if await self.check_rename_rate_limit(channel_config): + return # prevent api requests when ratelimited + + self.groups["groups"][str(channel.id)]["last_rename"] = int(time.time()) + channel_config["name"] = name + + await channel.edit(name=self.full_channel_name(channel_config)) + await self.save_groups() + await self.update_statusmessage() + + async def move_channel(self, channel, category, sync=True): + for sortchannel in category.text_channels: + if sortchannel.name[1:] > channel.name[1:]: + await channel.move(category=category, before=sortchannel, sync_permissions=sync) + return + await channel.move(category=category, sync_permissions=sync, end=True) + + async def add_requested_group_channel(self, message, direct=False): + requested_channel_config = self.groups["requested"].get(str(message.id)) + + category = await self.category_of_channel(requested_channel_config["state"]) + full_channel_name = self.full_channel_name(requested_channel_config) + channel = await category.create_text_channel(full_channel_name) + await self.move_channel(channel, category, False) + user = await self.bot.fetch_user(requested_channel_config["owner_id"]) + + await channel.send(f":wave: <@!{user.id}>, hier ist deine neue Lerngruppe.\n" + "Es gibt offene und private Lerngruppen. Eine offene Lerngruppe ist für jeden sichtbar " + "und jeder kann darin schreiben. Eine private Lerngruppe ist unsichtbar und auf eine " + "Gruppe an Kommilitoninnen beschränkt." + "```" + "Besitzerinfunktionen:\n" + "!lg addmember <@newmember>: Fügt ein Mitglied zur Lerngruppe hinzu.\n" + "!lg owner <@newowner>: Ändert die Besitzerin der Lerngruppe auf @newowner.\n" + "!lg open: Öffnet eine Lerngruppe.\n" + "!lg close: Schließt eine Lerngruppe.\n" + "!lg private: Stellt die Lerngruppe auf privat.\n" + "!lg show: Zeigt eine private oder geschlossene Lerngruppe in der Lerngruppenliste an.\n" + "!lg hide: Entfernt eine private oder geschlossene Lerngruppe aus der Lerngruppenliste.\n" + "!lg kick <@user>: Schließt eine Benutzerin von der Lerngruppe aus.\n" + "\nKommandos für alle:\n" + "!lg id: Zeigt die ID der Lerngruppe an mit der andere Kommilitoninnen beitreten können.\n" + "!lg members: Zeigt die Mitglieder der Lerngruppe an.\n" + "!lg owner: Zeigt die Besitzerin der Lerngruppe.\n" + "!lg leave: Du verlässt die Lerngruppe.\n" + "!lg join: Anfrage stellen in die Lerngruppe aufgenommen zu werden.\n" + "\nMit dem nachfolgenden Kommando kann eine Kommilitonin darum " + "bitten in die Lerngruppe aufgenommen zu werden wenn diese bereits privat ist.\n" + f"!lg join {channel.id}" + "\n(manche Kommandos sind von Discord limitiert und können nur einmal alle 5 Minuten ausgeführt werden)" + "```" + ) + self.groups["groups"][str(channel.id)] = { + "owner_id": requested_channel_config["owner_id"], + "last_rename": int(time.time()) + } + + await self.remove_group_request(message) + if not direct: + await message.delete() + + await self.save_groups() + await self.update_statusmessage() + if requested_channel_config["state"] is GroupState.PRIVATE: + await self.update_permissions(channel) + + async def remove_group_request(self, message): + del self.groups["requested"][str(message.id)] + await self.save_groups() + + async def remove_group(self, channel): + del self.groups["groups"][str(channel.id)] + await self.save_groups() + + def channel_to_channel_config(self, channel): + cid = str(channel.id) + is_listed = channel.name[-1] == LG_LISTED_SYMBOL + result = re.match(r"([0-9]+)-(.*)-([a-z0-9]+)$", channel.name[1:] if not is_listed else channel.name[1:-1]) + + state = None + if channel.name[0] == LG_OPEN_SYMBOL: + state = GroupState.OPEN + elif channel.name[0] == LG_CLOSE_SYMBOL: + state = GroupState.CLOSED + elif channel.name[0] == LG_PRIVATE_SYMBOL: + state = GroupState.PRIVATE + + course, name, semester = result.group(1, 2, 3) + + channel_config = {"course": course, "name": name, "category": channel.category_id, "semester": semester, + "state": state, "is_listed": is_listed, "channel_id": cid} + if self.groups["groups"].get(cid): + channel_config.update(self.groups["groups"].get(cid)) + return channel_config + + async def update_channels(self): + self.channels = {} + for state in [GroupState.OPEN, GroupState.CLOSED, GroupState.PRIVATE]: + category = await self.category_of_channel(state) + + for channel in category.text_channels: + channel_config = self.channel_to_channel_config(channel) + + self.channels[str(channel.id)] = channel_config + + async def add_member_to_group(self, channel: disnake.TextChannel, arg_member: disnake.Member, send_message=True): + group_config = self.groups["groups"].get(str(channel.id)) + if not group_config: + await channel.send("Das ist kein Lerngruppenkanal.") + return + + users = group_config.get("users") + if not users: + users = {} + mid = str(arg_member.id) + if not users.get(mid): + users[mid] = True + user = await self.bot.fetch_user(mid) + if user and send_message: + await utils.send_dm(user, f"Du wurdest in die Lerngruppe <#{channel.id}> aufgenommen. " + "Viel Spass beim gemeinsamen Lernen!\n" + "Dieser Link führt dich direkt zum Lerngruppen-Channel. " + "Diese Nachricht kannst du bei Bedarf in unserer Unterhaltung " + "über Rechtsklick anpinnen.") + + group_config["users"] = users + + await self.save_groups() + + async def remove_member_from_group(self, channel: disnake.TextChannel, arg_member: disnake.Member, send_message=True): + group_config = self.groups["groups"].get(str(channel.id)) + if not group_config: + await channel.send("Das ist kein Lerngruppenkanal.") + return + + users = group_config.get("users") + if not users: + return + mid = str(arg_member.id) + if users.pop(mid, None): + user = await self.bot.fetch_user(mid) + if user and send_message: + await utils.send_dm(user, f"Du wurdest aus der Lerngruppe {channel.name} entfernt") + + await self.save_groups() + + async def update_permissions(self, channel): + channel_config = self.channels[str(channel.id)] + if channel_config.get("state") == GroupState.PRIVATE: + overwrites = await self.overwrites(channel) + await channel.edit(overwrites=overwrites) + else: + await channel.edit(sync_permissions=True) + + async def overwrites(self, channel): + channel = await self.bot.fetch_channel(str(channel.id)) + group_config = self.groups["groups"].get(str(channel.id)) + guild = await self.bot.fetch_guild(int(self.guild_id)) + mods = guild.get_role(int(self.mod_role)) + + overwrites = { + mods: disnake.PermissionOverwrite(read_messages=True), + guild.default_role: disnake.PermissionOverwrite(read_messages=False) + } + + if not group_config: + return overwrites + + owner = self.bot.get_user(group_config["owner_id"]) + if not owner: + return overwrites + + overwrites[owner] = disnake.PermissionOverwrite(read_messages=True) + users = group_config.get("users") + if not users: + return overwrites + + for userid in users.keys(): + user = await self.bot.fetch_user(userid) + overwrites[user] = disnake.PermissionOverwrite(read_messages=True) + + return overwrites + + @help( + category="learninggroups", + syntax="!lg <command>", + brief="Lerngruppenverwaltung" + ) + @commands.group(name="lg", aliases=["learninggroup", "lerngruppe"], pass_context=True) + async def cmd_lg(self, ctx): + if not ctx.invoked_subcommand: + await ctx.channel.send("Gib `!help lg` ein um eine Ãœbersicht über die Lerngruppen-Kommandos zu erhalten.") + + @help( + command_group="lg", + category="learninggroups", + brief="Updated die Lerngruppenliste", + mod=True + ) + @cmd_lg.command(name="update") + @commands.check(utils.is_mod) + async def cmd_update(self, ctx): + await self.update_channels() + await self.update_statusmessage() + + @help( + command_group="lg", + category="learninggroups", + syntax="!lg header <coursenumber> <name...>", + brief="Fügt einen Kurs als neue Ãœberschrift in Botys Lerngruppen-Liste (Kanal #lerngruppen) hinzu. " + "Darf Leerzeichen enthalten, Anführungszeichen sind nicht erforderlich.", + example="!lg header 1141 Mathematische Grundlagen", + parameters={ + "coursenumber": "Nummer des Kurses wie von der Fernuni angegeben (ohne führende Nullen z. B. 1142).", + "name...": "Ein frei wählbarer Text (darf Leerzeichen enthalten).", + }, + description="Kann auch zum Bearbeiten einer Ãœberschrift genutzt werden. Bei bereits existierender " + "Kursnummer wird die Ãœberschrift abgeändert", + mod=True + ) + @cmd_lg.command(name="header") + @commands.check(utils.is_mod) + async def cmd_add_header(self, ctx, arg_course, *arg_name): + if not re.match(r"[0-9]+", arg_course): + await ctx.channel.send( + f"Fehler! Die Kursnummer muss numerisch sein. Gib `!help add-course` für Details ein.") + return + + self.header[arg_course] = f"{arg_course} - {' '.join(arg_name)}" + self.save_header() + await self.update_statusmessage() + + @help( + command_group="lg", + category="learninggroups", + syntax="!lg add <coursenumber> <name> <semester> <status> <@usermention>", + example="!lg add 1142 mathegenies sose22 closed @someuser", + brief="Fügt einen Lerngruppen-Kanal hinzu. Der Name darf keine Leerzeichen enthalten.", + parameters={ + "coursenumber": "Nummer des Kurses wie von der Fernuni angegeben (ohne führende Nullen z. B. 1142).", + "name": "Ein frei wählbarer Text ohne Leerzeichen. Bindestriche sind zulässig.", + "semester": ("Das Semester, für welches diese Lerngruppe erstellt werden soll." + "sose oder wise gefolgt von der zweistelligen Jahreszahl (z. B. sose22)."), + "status": "Gibt an ob die Lerngruppe für weitere Lernwillige geöffnet ist (open) oder nicht (closed).", + "@usermention": "Die so erwähnte Benutzerin wird als Besitzerin für die Lerngruppe gesetzt." + }, + mod=True + ) + @cmd_lg.command(name="add") + @commands.check(utils.is_mod) + async def cmd_add_group(self, ctx, arg_course, arg_name, arg_semester, arg_state, arg_owner: disnake.Member): + state = self.arg_state_to_group_state(arg_state) + channel_config = {"owner_id": arg_owner.id, "course": arg_course, "name": arg_name, "semester": arg_semester, + "state": state, "is_listed": False} + + if not await self.is_channel_config_valid(ctx, channel_config, ctx.command.name): + return + + self.groups["requested"][str(ctx.message.id)] = channel_config + await self.save_groups() + await self.add_requested_group_channel(ctx.message, direct=True) + + @help( + command_group="lg", + category="learninggroups", + syntax="!lg request <coursenumber> <name> <semester> <status>", + brief="Stellt eine Anfrage für einen neuen Lerngruppen-Kanal.", + example="!lg request 1142 mathegenies sose22 closed", + description=("Moderatorinnen können diese Anfrage bestätigen, dann wird die Gruppe eingerichtet. " + "Die Besitzerin der Gruppe ist die Benutzerin die die Anfrage eingestellt hat."), + parameters={ + "coursenumber": "Nummer des Kurses, wie von der FernUni angegeben (ohne führende Nullen z. B. 1142).", + "name": "Ein frei wählbarer Text ohne Leerzeichen.", + "semester": "Das Semester, für welches diese Lerngruppe erstellt werden soll. sose oder wise gefolgt " + "von der zweistelligen Jahreszahl (z. B. sose22).", + "status": "Gibt an ob die Lerngruppe für weitere Lernwillige geöffnet ist (open) oder nicht (closed)." + } + ) + @cmd_lg.command(name="request", aliases=["r", "req"]) + async def cmd_request_group(self, ctx, arg_course, arg_name, arg_semester, arg_state): + state = self.arg_state_to_group_state(arg_state) + arg_name = re.sub( + r"[^A-Za-zäöüß0-9-]", + "", + arg_name.lower().replace(" ", "-") + ) + arg_semester = arg_semester.lower() + if len(arg_semester) == 8: + arg_semester = f"{arg_semester[0:4]}{arg_semester[-2:]}" + channel_config = {"owner_id": ctx.author.id, "course": arg_course, "name": arg_name, "semester": arg_semester, + "state": state, "is_listed": False} + + if not await self.is_channel_config_valid(ctx, channel_config, ctx.command.name): + return + + channel = await self.bot.fetch_channel(int(self.channel_request)) + channel_name = self.full_channel_name(channel_config) + + message = await utils.confirm( + channel=channel, + title="Lerngruppenanfrage", + description=f"<@!{ctx.author.id}> möchte gerne die Lerngruppe **#{channel_name}** eröffnen.", + custom_prefix="learninggroups:group" + ) + self.groups["requested"][str(message.id)] = channel_config + await self.save_groups() + + @help( + command_group="lg", + category="learninggroups", + syntax="!lg show", + brief="Zeigt einen privaten Lerngruppenkanal trotzdem in der Liste an.", + description=("Muss im betreffenden Lerngruppen-Kanal ausgeführt werden. " + "Die Lerngruppe wird in der Ãœbersicht der Lerngruppen gelistet, so können Kommilitoninnen noch " + "Anfragen stellen, um in die Lerngruppe aufgenommen zu werden." + "Diese Aktion kann nur von der Besitzerin der Lerngruppe ausgeführt werden. ") + ) + @cmd_lg.command(name="show") + async def cmd_show(self, ctx): + if self.is_group_owner(ctx.channel, ctx.author) or utils.is_mod(ctx): + channel_config = self.channels[str(ctx.channel.id)] + if channel_config: + if channel_config.get("state") == GroupState.PRIVATE: + if await self.set_channel_listing(ctx.channel, True): + await ctx.channel.send("Die Lerngruppe wird nun in der Lerngruppenliste angezeigt.") + elif channel_config.get("state") == GroupState.OPEN: + await ctx.channel.send("Nichts zu tun. Offene Lerngruppen werden sowieso in der Liste angezeigt.") + elif channel_config.get("state") == GroupState.CLOSED: + await ctx.channel.send("Möchtest du die Gruppen öffnen? Versuch‘s mit `!lg open`") + + + @help( + command_group="lg", + category="learninggroups", + syntax="!lg hide", + brief="Versteckt einen privaten Lerngruppenkanal. ", + description=("Muss im betreffenden Lerngruppen-Kanal ausgeführt werden. " + "Die Lerngruppe wird nicht mehr in der Liste der Lerngruppen aufgeführt. " + "Diese Aktion kann nur von der Besitzerin der Lerngruppe ausgeführt werden. ") + ) + @cmd_lg.command(name="hide") + async def cmd_hide(self, ctx): + if self.is_group_owner(ctx.channel, ctx.author) or utils.is_mod(ctx): + channel_config = self.channels[str(ctx.channel.id)] + if channel_config: + if channel_config.get("state") == GroupState.PRIVATE: + if await self.set_channel_listing(ctx.channel, False): + await ctx.channel.send("Die Lerngruppe wird nun nicht mehr in der Lerngruppenliste angezeigt.") + return + + elif channel_config.get("state") == GroupState.OPEN: + await ctx.channel.send("Offene Lerngruppen können nicht aus der Lerngruppenliste entfernt werden. " + "Führe `!lg close` aus um die Lerngruppe zu schließen, " + "oder `!lg private` um diese auf " + "privat zu schalten.") + elif channel_config.get("state") == GroupState.CLOSED: + await ctx.channel.send("Wenn diese Gruppe privat werden soll, ist das Kommando das du brauchst: `!lg private`") + + @cmd_lg.command(name="debug") + @commands.check(utils.is_mod) + async def cmd_debug(self, ctx): + channel_config = self.channels[str(ctx.channel.id)] + if not channel_config: + await ctx.channel.send("None") + return + await ctx.channel.send(str(channel_config)) + + + @help( + command_group="lg", + category="learninggroups", + syntax="!lg open", + brief="Öffnet den Lerngruppen-Kanal wenn du die Besitzerin bist. ", + description=("Muss im betreffenden Lerngruppen-Kanal ausgeführt werden. " + "Verschiebt den Lerngruppen-Kanal in die Kategorie für offene Kanäle und ändert das Icon. " + "Diese Aktion kann nur von der Besitzerin der Lerngruppe ausgeführt werden. ") + ) + @cmd_lg.command(name="open", aliases=["opened", "offen"]) + async def cmd_open(self, ctx): + if self.is_group_owner(ctx.channel, ctx.author) or utils.is_mod(ctx): + await self.set_channel_state(ctx.channel, state=GroupState.OPEN) + + @help( + command_group="lg", + category="learninggroups", + syntax="!lg close", + brief="Schließt den Lerngruppen-Kanal wenn du die Besitzerin bist. ", + description=("Muss im betreffenden Lerngruppen-Kanal ausgeführt werden. " + "Stellt die Lerngruppe auf geschlossen. Dies ist rein symbolisch und zeigt an, " + "dass keine neuen Mitglieder mehr aufgenommen werden. " + "Diese Aktion kann nur von der Besitzerin der Lerngruppe ausgeführt werden. ") + ) + @cmd_lg.command(name="close", aliases=["closed", "geschlossen"]) + async def cmd_close(self, ctx): + if self.is_group_owner(ctx.channel, ctx.author) or utils.is_mod(ctx): + await self.set_channel_state(ctx.channel, state=GroupState.CLOSED) + + @help( + command_group="lg", + category="learninggroups", + syntax="!lg private", + brief="Macht aus deiner Lerngruppe eine private Lerngruppe wenn du die Besitzerin bist. ", + description=("Muss im betreffenden Lerngruppen-Kanal ausgeführt werden. " + "Stellt die Lerngruppe auf privat. Es haben nur noch Mitglieder " + "der Lerngruppe zugriff auf den Kanal. (siehe `!lg members`)" + "Diese Aktion kann nur von der Besitzerin der Lerngruppe ausgeführt werden. ") + ) + @cmd_lg.command(name="private", aliases=["privat"]) + async def cmd_private(self, ctx): + if self.is_group_owner(ctx.channel, ctx.author) or utils.is_mod(ctx): + if await self.set_channel_state(ctx.channel, state=GroupState.PRIVATE): + await self.update_permissions(ctx.channel) + + + + @help( + command_group="lg", + category="learninggroups", + syntax="!lg rename <name>", + brief="Ändert den Namen des Lerngruppen-Kanals, in dem das Komando ausgeführt wird.", + example="!lg rename matheluschen", + description="Aus #1142-matheprofis-sose22 wird nach dem Aufruf des Beispiels #1142-matheluschen-sose22.", + parameters={ + "name": "Der neue Name der Lerngruppe ohne Leerzeichen." + }, + mod=True + ) + @cmd_lg.command(name="rename") + @commands.check(utils.is_mod) + async def cmd_rename(self, ctx, arg_name): + await self.set_channel_name(ctx.channel, arg_name) + + @help( + command_group="lg", + syntax="!lg archive", + category="learninggroups", + brief="Archiviert den Lerngruppen-Kanal", + description="Verschiebt den Lerngruppen-Kanal, in welchem dieses Kommando ausgeführt wird, ins Archiv.", + mod=True + ) + @cmd_lg.command(name="archive", aliases=["archiv"]) + @commands.check(utils.is_mod) + async def cmd_archive(self, ctx): + await self.archive(ctx.channel) + + @help( + command_group="lg", + category="learninggroups", + syntax="!lg owner <@usermention>", + example="!owner @someuser", + brief="Setzt die Besitzerin eines Lerngruppen-Kanals", + description="Muss im betreffenden Lerngruppen-Kanal ausgeführt werden. ", + parameters={ + "@usermention": "Die neue Besitzerin der Lerngruppe." + } + ) + @cmd_lg.command(name="owner") + async def cmd_owner(self, ctx, new_owner: disnake.Member = None): + group_config = self.groups["groups"].get(str(ctx.channel.id)) + + if not group_config: + self.groups["groups"][str(ctx.channel.id)] = {} + group_config = self.groups["groups"][str(ctx.channel.id)] + + owner_id = group_config.get("owner_id") + + if not owner_id: + return + + if not new_owner: + user = await self.bot.fetch_user(owner_id) + await ctx.channel.send(f"Besitzerin: @{user.name}#{user.discriminator}") + + elif isinstance(group_config, dict): + owner = await self.bot.fetch_user(owner_id) + if self.is_group_owner(ctx.channel, ctx.author) or utils.is_mod(ctx): + group_config["owner_id"] = new_owner.id + await self.remove_member_from_group(ctx.channel, new_owner, False) + if new_owner != owner: + await self.add_member_to_group(ctx.channel, owner, False) + await self.save_groups() + await self.update_permissions(ctx.channel) + await ctx.channel.send( + f"Glückwunsch {new_owner.mention}! Du bist jetzt die Besitzerin dieser Lerngruppe.") + + @help( + command_group="lg", + category="learninggroups", + syntax="!lg addmember <@usermention> <#channel>", + example="!lg addmember @someuser #1141-mathegl-lerngruppe-sose21", + brief="Fügt eine Benutzerin zu einer Lerngruppe hinzu.", + parameters={ + "@usermention": "Die so erwähnte Benutzerin wird zur Lerngruppe hinzugefügt.", + "#channel": "(optional) Der Kanal dem die Benutzerin hinzugefügt werden soll." + } + ) + @cmd_lg.command(name="addmember", aliases=["addm", "am"]) + async def cmd_add_member(self, ctx, arg_member: disnake.Member, arg_channel: disnake.TextChannel = None): + if not arg_channel: + if not self.channels.get(str(ctx.channel.id)): + await ctx.channel.send("Wenn das Kommando außerhalb eines Lerngruppenkanals aufgerufen wird, muss der" + "Lerngruppenkanal angehängt werden. `!lg addmember <@usermention> <#channel>`") + return + arg_channel = ctx.channel + if self.is_group_owner(arg_channel, ctx.author) or utils.is_mod(ctx): + await self.add_member_to_group(arg_channel, arg_member) + await self.update_permissions(arg_channel) + + @help( + command_group="lg", + category="learninggroups", + syntax="!lg removemember <@usermention> <#channel>", + example="!lg removemember @someuser #1141-mathegl-lerngruppe-sose21", + brief="Entfernt eine Benutzerin aus einer Lerngruppe.", + parameters={ + "#channel": "Der Kanal aus dem die Benutzerin gelöscht werden soll.", + "@usermention": "Die so erwähnte Benutzerin wird aus der Lerngruppe entfernt." + }, + mod=True + ) + @cmd_lg.command(name="removemember", aliases=["remm", "rm"]) + @commands.check(utils.is_mod) + async def cmd_remove_member(self, ctx, arg_member: disnake.Member, arg_channel: disnake.TextChannel): + await self.remove_member_from_group(arg_channel, arg_member) + await self.update_permissions(arg_channel) + + @help( + command_group="lg", + category="learninggroups", + syntax="!lg members", + brief="Listet die Mitglieder der Lerngruppe auf.", + ) + @cmd_lg.command(name="members") + async def cmd_members(self, ctx): + group_config = self.groups["groups"].get(str(ctx.channel.id)) + if not group_config: + await ctx.channel.send("Das ist kein Lerngruppenkanal.") + return + owner_id = group_config.get("owner_id") + + if not owner_id: + return + + owner = await self.bot.fetch_user(owner_id) + users = group_config.get("users", {}) + if not users and not owner: + await ctx.channel.send("Keine Lerngruppenmitglieder vorhanden.") + return + + names = [] + + for user_id in users: + user = await self.bot.fetch_user(user_id) + names.append("@" + user.name + "#" + user.discriminator) + + await ctx.channel.send(f"Besitzerin: **@{owner.name}#{owner.discriminator}**\nMitglieder: " + + (f"{', '.join(names)}" if len(names) > 0 else "Keine")) + + @help( + command_group="lg", + category="learninggroups", + syntax="!lg id", + brief="Zeigt die ID für deine Lerngruppe an.", + ) + @cmd_lg.command(name="id") + async def cmd_id(self, ctx): + if self.is_group_owner(ctx.channel, ctx.author) or utils.is_mod(ctx): + group_config = self.groups["groups"].get(str(ctx.channel.id)) + if not group_config: + await ctx.channel.send("Das ist kein Lerngruppenkanal.") + return + await ctx.channel.send(f"Die ID dieser Lerngruppe lautet: `{str(ctx.channel.id)}`.\n" + f"Beitrittsanfrage mit: `!lg join {str(ctx.channel.id)}`") + + @help( + command_group="lg", + category="learninggroups", + syntax="!lg join <lg-id>", + brief="Fragt bei der Besitzerin einer Lerngruppe um Aufnahme.", + parameters={ + "id": "Die ID zur Lerngruppe." + } + ) + @cmd_lg.command(name="join") + async def cmd_join(self, ctx, arg_id_or_channel: Union[int, disnake.TextChannel] = None): + + if arg_id_or_channel is None: + arg_id_or_channel = ctx.channel + + cid = arg_id_or_channel.id if type(arg_id_or_channel) is disnake.TextChannel else arg_id_or_channel + + group_config = self.groups["groups"].get(str(cid)) + if not group_config: + await ctx.channel.send("Das ist keine gültiger Lerngruppenkanal.") + return + + channel = await self.bot.fetch_channel(int(cid)) + + await utils.confirm( + channel=channel, + title="Jemand möchte deiner Lerngruppe beitreten!", + description=f"<@!{ctx.author.id}> möchte gerne der Lerngruppe **#{channel.name}** beitreten.", + message=f"Anfrage von <@!{ctx.author.id}>", + custom_prefix="learninggroups:join" + ) + await utils.send_dm(ctx.author, f"Deine Anfrage wurde an **#{channel.name}** gesendet. " + "Sobald die Besitzerin der Lerngruppe darüber " + "entschieden hat bekommst du Bescheid.") + + @help( + command_group="lg", + category="learninggroups", + syntax="!lg kick <@usermention>", + brief="Wirft @usermention aus der Gruppe." + ) + @cmd_lg.command(name="kick") + async def cmd_kick(self, ctx, arg_member: disnake.Member): + if self.is_group_owner(ctx.channel, ctx.author) or utils.is_mod(ctx): + group_config = self.groups["groups"].get(str(ctx.channel.id)) + if not group_config: + await ctx.channel.send("Das ist keine gültiger Lerngruppenkanal.") + return + + await self.remove_member_from_group(ctx.channel, arg_member) + await self.update_permissions(ctx.channel) + + @help( + command_group="lg", + category="learninggroups", + syntax="!lg leave", + brief="Du verlässt die Lerngruppe." + ) + @cmd_lg.command(name="leave") + async def cmd_leave(self, ctx): + group_config = self.groups["groups"].get(str(ctx.channel.id)) + if not group_config: + await ctx.channel.send("Das ist keine gültiger Lerngruppenkanal.") + return + + if group_config["owner_id"] == ctx.author.id: + await ctx.channel.send("Du kannst nicht aus deiner eigenen Lerngruppe flüchten. Ãœbertrage erst den Besitz.") + return + + await self.remove_member_from_group(ctx.channel, ctx.author) + await self.update_permissions(ctx.channel) + + async def on_group_request(self, confirmed, button, interaction: InteractionMessage): + channel = interaction.channel + member = interaction.author + message = interaction.message + + if str(channel.id) == str(self.channel_request): + request = self.groups["requested"].get(str(message.id)) + if confirmed and self.is_mod(member): + await self.add_requested_group_channel(message, direct=False) + + elif not confirmed and (self.is_request_owner(request, member) or self.is_mod(member)): + if self.is_mod(member): + user = await self.bot.fetch_user(request["owner_id"] ) + if user: + await utils.send_dm(user, f"Deine Lerngruppenanfrage für #{self.full_channel_name(request)} wurde abgelehnt.") + await self.remove_group_request(message) + + await message.delete() + + async def on_join_request(self, confirmed, button, interaction: InteractionMessage): + channel = interaction.channel + member = interaction.author + message = interaction.message + group_config = self.groups["groups"].get(str(channel.id)) + + if not group_config: + return + + if self.is_group_owner(channel, member) or self.is_mod(member): + if confirmed: + if message.mentions and len(message.mentions) == 1: + await self.add_member_to_group(channel, message.mentions[0]) + await self.update_permissions(channel) + + else: + await channel.send(f"Leider ist ein Fehler aufgetreten.") + else: + if message.mentions and len(message.mentions) == 1: + await utils.send_dm(message.mentions[0], f"Deine Anfrage für die Lerngruppe **#{channel.name}**" + "wurde abgelehnt.") + await message.delete() + + async def cog_command_error(self, ctx, error): + try: + await handle_error(ctx, error) + except: + pass + diff --git a/cogs/links.py b/cogs/links.py index 33456daf11157a988dfd3ea28bceba74d58af8e9..7e469f7c0cea3a7a3580132390b2a336820504ba 100644 --- a/cogs/links.py +++ b/cogs/links.py @@ -1,210 +1,210 @@ -import json - -import discord -from discord.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 = discord.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) +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/module_information.py b/cogs/module_information.py index 4fe34745ab3ab4f4fc4a3f920a16107cc5e17b9b..cb6572e899459514b4f841e53f6533099a230167 100644 --- a/cogs/module_information.py +++ b/cogs/module_information.py @@ -1,408 +1,408 @@ -import json -import os -import re - -import discord -from discord.ext import commands, tasks - -import utils -from cogs.components.module_information.scraper import Scraper -from cogs.help import help, help_category, handle_error - - -class ModuleInformationNotFoundError(Exception): - pass - - -class NoCourseChannelError(Exception): - pass - - -class NoCourseOfStudyError(Exception): - pass - - -""" - Environment Variablen: - DISCORD_MODULE_COURSE_FILE - Datei mit Studiengangsinformationen - DISCORD_MODULE_DATA_FILE - In dieser Datei werden die gescrappten Daten gespeichert -""" - - -@help_category("moduleinformation", "Modulinformationen", - "Mit diesen Kommandos kannst du dir Informationen zu einem Kurs/Modul anzeigen lassen. Die angezeigten Informationen sind abhängig von deinem Studiengang (also der Rolle die du gewählt hast).") -class ModuleInformation(commands.Cog): - def __init__(self, bot): - self.bot = bot - self.data = [] - self.roles_channel_id = int(os.getenv("DISCORD_ROLLEN_CHANNEL")) - self.data_file = os.getenv("DISCORD_MODULE_DATA_FILE") - self.courses_file = os.getenv("DISCORD_MODULE_COURSE_FILE") - self.load_data() - self.update_loop.start() - - @tasks.loop(hours=24) - async def update_loop(self): - await self.refresh_data() - - async def refresh_data(self): - try: - scrapper = Scraper(self.courses_file) - print("Refresh started") - data = await scrapper.scrape() - self.data = data - self.save_data() - print("Refresh finished") - except: - print("Can't refresh data") - pass - - @update_loop.before_loop - async def before_update_loop(self): - await self.bot.wait_until_ready() - - def save_data(self): - data_file = open(self.data_file, mode='w') - json.dump(self.data, data_file) - - def load_data(self): - try: - data_file = open(self.data_file, mode='r') - self.data = json.load(data_file) - except FileNotFoundError: - self.data = {} - - def number_of_channel(self, channel): - try: - number = re.search(r"^([0-9]*)-", channel.name)[1] - return number - except TypeError: - raise NoCourseChannelError - - def stg_string_for_desc(self, module): - desc = f"\n*({module['stg']})*" - desc += ("\n*Es wurden keine Informationen für deinen Studiengang gefunden," - "daher wird der erste Eintrag angezeigt*" - if 'notfound' in module else "") - return desc - - async def execute_subcommand(self, ctx, arg_stg, subcommand=None): - try: - module = await self.find_module(ctx, arg_stg) - await subcommand(ctx, module) - except NoCourseOfStudyError: - shorts = [] - for course_of_studies in self.data: - shorts.append(f"`{course_of_studies['short']}`") - await ctx.channel.send( - f"Fehler! Wähle entweder eine Studiengangs-Rolle aus oder gebe ein Studiengangskürzel" - f"nach dem Kommando an.\nMögliche Kürzel: {', '.join(shorts)}" - ) - return None - except NoCourseChannelError: - return None - except ModuleInformationNotFoundError as e: - if e.args and e.args[0]: - await ctx.channel.send(e.args[0]) - else: - await ctx.channel.send("Leider konnte ich keine Informationen zu diesem Modul/Kurs finden.") - - return None - - async def get_stg_short(self, ctx, stg): - if not stg: - stg = await self.get_stg_short_from_role(ctx.author) - if not stg: - raise NoCourseOfStudyError - return stg - - async def get_valid_modules_for_course_number(self, number): - valid_modules = [] - try: - for course_of_studies in self.data: - if course_of_studies['modules'] is not None: - for module in course_of_studies['modules']: - if module['page']['courses'] is not None: - for course in module['page']['courses']: - cn = re.sub(r'^0+', '', course['number']) - n = re.sub(r'^0+', '', number) - if n == cn: - valid_modules.append({ - "stg": course_of_studies['name'], - "short": course_of_studies['short'], - "data": module - }) - else: - print(f"[ModuleInformation] {module['number']} is an invalid Module") - return valid_modules - except: - return [] - - async def find_module(self, ctx, arg_stg): - short = await self.get_stg_short(ctx, arg_stg) - number = self.number_of_channel(ctx.channel) - valid_modules = await self.get_valid_modules_for_course_number(number) - - if len(valid_modules) == 0: - raise ModuleInformationNotFoundError - - for module in valid_modules: - if module.get('short') == short: - return module - - module = valid_modules[0] - module['notfound'] = True - return module - - async def get_stg_short_from_role(self, user): - try: - for course_of_studies in self.data: - if 'role' in course_of_studies: - for r in user.roles: - if str(r.id) == course_of_studies['role']: - return course_of_studies['short'] - return None - except discord.ext.commands.errors.CommandInvokeError: - return None - - async def download_for(self, ctx, title, module): - try: - data = module['data']['page']['downloads'] - if not data: - raise KeyError - except KeyError: - raise ModuleInformationNotFoundError - - desc = "" - found = False - for download in data: - if re.search(title, download['title']): - found = True - desc += f"[{download['title']}]({download['url']})\n" - desc += self.stg_string_for_desc(module) - if not found: - raise ModuleInformationNotFoundError - - embed = discord.Embed(title=title, - description=desc, - color=19607) - await ctx.channel.send(embed=embed) - - async def handbook(self, ctx, module): - try: - await self.download_for(ctx, "Modulhandbuch", module) - except ModuleInformationNotFoundError: - raise ModuleInformationNotFoundError("Leider habe ich kein Modulhandbuch gefunden.") - - async def reading_sample(self, ctx, module): - try: - await self.download_for(ctx, "Leseprobe", module) - except ModuleInformationNotFoundError: - raise ModuleInformationNotFoundError("Leider habe ich keine Leseprobe gefunden.") - - async def info(self, ctx, module): - try: - data = module['data'] - info = data['page']['infos'] - if not data or not info: - raise KeyError - except KeyError: - raise ModuleInformationNotFoundError - - desc = (f"Wie viele Credits bekomme ich? **{info['ects']} ECTS**\n" - f"Wie lange geht das Modul? **{info['duration']}**\n" - f"Wie oft wird das Modul angeboten? **{info['interval']}**\n" - ) - - if (requirements := info.get('requirements')) and len(requirements) > 0 and requirements != 'keine': - desc += f"\nInhaltliche Voraussetzungen: \n{requirements}\n" - - if (notes := info.get('notes')) and len(notes) > 0 and notes != '-': - desc += f"\nAnmerkungen: \n\n{notes}\n" - - if (persons := data['page'].get('persons')) and len(persons) > 0: - desc += f"\nAnsprechparnter: \n" - desc += ', '.join(persons) + "\n" - - if (courses := data['page'].get('courses')) and len(courses) > 0: - desc += f"\nKurse: \n" - for course in courses: - desc += f"[{course['number']} - {course['name']}]({course['url']})\n" - - desc += self.stg_string_for_desc(module) - embed = discord.Embed(title=f"Modul {data['title']}", - description=desc, - color=19607) - await ctx.channel.send(embed=embed) - - async def load(self, ctx, module): - try: - data = module['data']['page']['infos']['time'] - if not data: - raise KeyError - except KeyError: - raise ModuleInformationNotFoundError - - time = re.sub(r': *(\r*\n*)*', ':\n', data) - desc = f"{time}" - desc += self.stg_string_for_desc(module) - embed = discord.Embed(title=f"Arbeitsaufwand", - description=desc, - color=19607) - await ctx.channel.send(embed=embed) - - async def support(self, ctx, module): - try: - data = module['data']['page']['support'] - if not data: - raise KeyError - except KeyError: - raise ModuleInformationNotFoundError(f"Leider habe ich keine Mentoriate gefunden.") - - desc = "" - for support in data: - desc += f"[{support['title']}]({support['url']})\n" - desc += self.stg_string_for_desc(module) - embed = discord.Embed(title=f"Mentoriate ", - description=desc, - color=19607) - await ctx.channel.send(embed=embed) - - async def exams(self, ctx, module): - try: - data = module['data']['page']['exams'] - if not data: - raise KeyError - except KeyError: - raise ModuleInformationNotFoundError(f"Leider habe ich keine Prüfungsinformationen gefunden.") - - desc = "" - for exam in data: - desc += f"**{exam['name']}**\n{exam['type']}\n" - if (weight := exam.get('weight')) and len(weight) > 0 and weight != '-': - desc += f"Gewichtung: **{weight}**\n" - desc += "\n" - - if (requirements := exam.get('requirements')) and len(requirements) > 0 and requirements != 'keine': - desc += f"Inhaltliche Voraussetzungen: \n{requirements}\n\n" - - if (hard_requirements := exam.get('hard_requirements')) and len(hard_requirements) > 0 \ - and hard_requirements != 'keine': - desc += f"Formale Voraussetzungen: \n{hard_requirements}\n\n" - # desc += self.stg_string_for_desc(module) - - embed = discord.Embed(title=f"Prüfungsinformationen", - description=desc, - color=19607) - await ctx.channel.send(embed=embed) - - @help( - category="moduleinformation", - syntax="!module <command> <stg?>", - parameters={ - "command": "Das Kommando, welches ausgeführt werden soll (aufwand, handbuch, info, leseprobe, mentoriate, prüfungen)", - "stg": "*(optional)* Kürzel des Studiengangs, für den die Informationen angezeigt werden sollen (bainf, bamath, bscmatse, bawiinf, mscma, mscinf, mawiinf, mscprinf)" - }, - brief="Ruft Modulinformation ab. " - ) - @commands.group(name="module", aliases=["modul"], pass_context=True) - async def cmd_module(self, ctx): - if not ctx.invoked_subcommand: - await self.cmd_module_info(ctx) - - @help( - command_group="module", - category="moduleinformation", - syntax="!module update <stg?>", - parameters={ - "stg": "*(optional)* Kürzel des Studiengangs, für den die Informationen angezeigt werden sollen (bainf, bamath, bscmatse, bawiinf, mscma, mscinf, mawiinf, mscprinf)" - }, - mod=True, - brief="Aktualisiert die Daten über die Module (manueller Aufruf im Normalfall nicht nötig). " - ) - @cmd_module.command("update") - @commands.check(utils.is_mod) - async def cmd_module_update(self, ctx): - await ctx.channel.send("Refreshing...") - await self.refresh_data() - - @help( - command_group="module", - category="moduleinformation", - syntax="!module handbuch <stg?>", - parameters={ - "stg": "*(optional)* Kürzel des Studiengangs, für den die Informationen angezeigt werden sollen (bainf, bamath, bscmatse, bawiinf, mscma, mscinf, mawiinf, mscprinf)" - }, - brief="Zeigt den Link zum Modulhandbuch für dieses Modul an. " - ) - @cmd_module.command("handbuch", aliases=["mhb", "hb", "modulhandbuch"]) - async def cmd_module_handbuch(self, ctx, arg_stg=None): - await self.execute_subcommand(ctx, arg_stg, self.handbook) - - @help( - command_group="module", - category="moduleinformation", - syntax="!module leseprobe <stg?>", - parameters={ - "stg": "*(optional)* Kürzel des Studiengangs, für den die Informationen angezeigt werden sollen (bainf, bamath, bscmatse, bawiinf, mscma, mscinf, mawiinf, mscprinf)" - }, - brief="Zeigt den Link zur Leseprobe für diesen Kurs an." - ) - @cmd_module.command("probe", aliases=["leseprobe"]) - async def cmd_module_probe(self, ctx, arg_stg=None): - await self.execute_subcommand(ctx, arg_stg, self.reading_sample) - - @help( - command_group="module", - category="moduleinformation", - syntax="!module info <stg?>", - parameters={ - "stg": "*(optional)* Kürzel des Studiengangs, für den die Informationen angezeigt werden sollen (bainf, bamath, bscmatse, bawiinf, mscma, mscinf, mawiinf, mscprinf)" - }, - brief="Zeigt allgemeine Informationen zum Modul an." - ) - @cmd_module.command("info") - async def cmd_module_info(self, ctx, arg_stg=None): - await self.execute_subcommand(ctx, arg_stg, self.info) - - @help( - command_group="module", - category="moduleinformation", - syntax="!module aufwand <stg?>", - parameters={ - "stg": "*(optional)* Kürzel des Studiengangs, für den die Informationen angezeigt werden sollen (bainf, bamath, bscmatse, bawiinf, mscma, mscinf, mawiinf, mscprinf)" - }, - brief="Zeigt die Informationen zum zeitlichen Aufwand an. " - ) - @cmd_module.command("aufwand", aliases=["workload", "load", "zeit", "arbeitzeit"]) - async def cmd_module_aufwand(self, ctx, arg_stg=None): - await self.execute_subcommand(ctx, arg_stg, self.load) - - @help( - command_group="module", - category="moduleinformation", - syntax="!module mentoriate <stg?>", - parameters={ - "stg": "*(optional)* Kürzel des Studiengangs, für den die Informationen angezeigt werden sollen (bainf, bamath, bscmatse, bawiinf, mscma, mscinf, mawiinf, mscprinf)" - }, - brief="Zeigt eine Liste der verfügbaren Mentoriate an." - ) - @cmd_module.command("mentoriate", aliases=["mentoriat", "support"]) - async def cmd_module_mentoriate(self, ctx, arg_stg=None): - await self.execute_subcommand(ctx, arg_stg, self.support) - - @help( - command_group="module", - category="moduleinformation", - syntax="!module prüfungen <stg?>", - parameters={ - "stg": "*(optional)* Kürzel des Studiengangs, für den die Informationen angezeigt werden sollen (bainf, bamath, bscmatse, bawiinf, mscma, mscinf, mawiinf, mscprinf)" - }, - brief="Zeigt Informationen zur Prüfung an. " - ) - @cmd_module.command("prüfungen", aliases=["exam", "exams", "prüfung"]) - async def cmd_module_pruefungen(self, ctx, arg_stg=None): - await self.execute_subcommand(ctx, arg_stg, self.exams) - - async def cog_command_error(self, ctx, error): - await handle_error(ctx, error) +import json +import os +import re + +import disnake +from disnake.ext import commands, tasks + +import utils +from cogs.components.module_information.scraper import Scraper +from cogs.help import help, help_category, handle_error + + +class ModuleInformationNotFoundError(Exception): + pass + + +class NoCourseChannelError(Exception): + pass + + +class NoCourseOfStudyError(Exception): + pass + + +""" + Environment Variablen: + DISCORD_MODULE_COURSE_FILE - Datei mit Studiengangsinformationen + DISCORD_MODULE_DATA_FILE - In dieser Datei werden die gescrappten Daten gespeichert +""" + + +@help_category("moduleinformation", "Modulinformationen", + "Mit diesen Kommandos kannst du dir Informationen zu einem Kurs/Modul anzeigen lassen. Die angezeigten Informationen sind abhängig von deinem Studiengang (also der Rolle die du gewählt hast).") +class ModuleInformation(commands.Cog): + def __init__(self, bot): + self.bot = bot + self.data = [] + self.roles_channel_id = int(os.getenv("DISCORD_ROLLEN_CHANNEL")) + self.data_file = os.getenv("DISCORD_MODULE_DATA_FILE") + self.courses_file = os.getenv("DISCORD_MODULE_COURSE_FILE") + self.load_data() + self.update_loop.start() + + @tasks.loop(hours=24) + async def update_loop(self): + await self.refresh_data() + + async def refresh_data(self): + try: + scrapper = Scraper(self.courses_file) + print("Refresh started") + data = await scrapper.scrape() + self.data = data + self.save_data() + print("Refresh finished") + except: + print("Can't refresh data") + pass + + @update_loop.before_loop + async def before_update_loop(self): + await self.bot.wait_until_ready() + + def save_data(self): + data_file = open(self.data_file, mode='w') + json.dump(self.data, data_file) + + def load_data(self): + try: + data_file = open(self.data_file, mode='r') + self.data = json.load(data_file) + except FileNotFoundError: + self.data = {} + + def number_of_channel(self, channel): + try: + number = re.search(r"^([0-9]*)-", channel.name)[1] + return number + except TypeError: + raise NoCourseChannelError + + def stg_string_for_desc(self, module): + desc = f"\n*({module['stg']})*" + desc += ("\n*Es wurden keine Informationen für deinen Studiengang gefunden," + "daher wird der erste Eintrag angezeigt*" + if 'notfound' in module else "") + return desc + + async def execute_subcommand(self, ctx, arg_stg, subcommand=None): + try: + module = await self.find_module(ctx, arg_stg) + await subcommand(ctx, module) + except NoCourseOfStudyError: + shorts = [] + for course_of_studies in self.data: + shorts.append(f"`{course_of_studies['short']}`") + await ctx.channel.send( + f"Fehler! Wähle entweder eine Studiengangs-Rolle aus oder gebe ein Studiengangskürzel" + f"nach dem Kommando an.\nMögliche Kürzel: {', '.join(shorts)}" + ) + return None + except NoCourseChannelError: + return None + except ModuleInformationNotFoundError as e: + if e.args and e.args[0]: + await ctx.channel.send(e.args[0]) + else: + await ctx.channel.send("Leider konnte ich keine Informationen zu diesem Modul/Kurs finden.") + + return None + + async def get_stg_short(self, ctx, stg): + if not stg: + stg = await self.get_stg_short_from_role(ctx.author) + if not stg: + raise NoCourseOfStudyError + return stg + + async def get_valid_modules_for_course_number(self, number): + valid_modules = [] + try: + for course_of_studies in self.data: + if course_of_studies['modules'] is not None: + for module in course_of_studies['modules']: + if module['page']['courses'] is not None: + for course in module['page']['courses']: + cn = re.sub(r'^0+', '', course['number']) + n = re.sub(r'^0+', '', number) + if n == cn: + valid_modules.append({ + "stg": course_of_studies['name'], + "short": course_of_studies['short'], + "data": module + }) + else: + print(f"[ModuleInformation] {module['number']} is an invalid Module") + return valid_modules + except: + return [] + + async def find_module(self, ctx, arg_stg): + short = await self.get_stg_short(ctx, arg_stg) + number = self.number_of_channel(ctx.channel) + valid_modules = await self.get_valid_modules_for_course_number(number) + + if len(valid_modules) == 0: + raise ModuleInformationNotFoundError + + for module in valid_modules: + if module.get('short') == short: + return module + + module = valid_modules[0] + module['notfound'] = True + return module + + async def get_stg_short_from_role(self, user): + try: + for course_of_studies in self.data: + if 'role' in course_of_studies: + for r in user.roles: + if str(r.id) == course_of_studies['role']: + return course_of_studies['short'] + return None + except disnake.ext.commands.errors.CommandInvokeError: + return None + + async def download_for(self, ctx, title, module): + try: + data = module['data']['page']['downloads'] + if not data: + raise KeyError + except KeyError: + raise ModuleInformationNotFoundError + + desc = "" + found = False + for download in data: + if re.search(title, download['title']): + found = True + desc += f"[{download['title']}]({download['url']})\n" + desc += self.stg_string_for_desc(module) + if not found: + raise ModuleInformationNotFoundError + + embed = disnake.Embed(title=title, + description=desc, + color=19607) + await ctx.channel.send(embed=embed) + + async def handbook(self, ctx, module): + try: + await self.download_for(ctx, "Modulhandbuch", module) + except ModuleInformationNotFoundError: + raise ModuleInformationNotFoundError("Leider habe ich kein Modulhandbuch gefunden.") + + async def reading_sample(self, ctx, module): + try: + await self.download_for(ctx, "Leseprobe", module) + except ModuleInformationNotFoundError: + raise ModuleInformationNotFoundError("Leider habe ich keine Leseprobe gefunden.") + + async def info(self, ctx, module): + try: + data = module['data'] + info = data['page']['infos'] + if not data or not info: + raise KeyError + except KeyError: + raise ModuleInformationNotFoundError + + desc = (f"Wie viele Credits bekomme ich? **{info['ects']} ECTS**\n" + f"Wie lange geht das Modul? **{info['duration']}**\n" + f"Wie oft wird das Modul angeboten? **{info['interval']}**\n" + ) + + if (requirements := info.get('requirements')) and len(requirements) > 0 and requirements != 'keine': + desc += f"\nInhaltliche Voraussetzungen: \n{requirements}\n" + + if (notes := info.get('notes')) and len(notes) > 0 and notes != '-': + desc += f"\nAnmerkungen: \n\n{notes}\n" + + if (persons := data['page'].get('persons')) and len(persons) > 0: + desc += f"\nAnsprechparnter: \n" + desc += ', '.join(persons) + "\n" + + if (courses := data['page'].get('courses')) and len(courses) > 0: + desc += f"\nKurse: \n" + for course in courses: + desc += f"[{course['number']} - {course['name']}]({course['url']})\n" + + desc += self.stg_string_for_desc(module) + embed = disnake.Embed(title=f"Modul {data['title']}", + description=desc, + color=19607) + await ctx.channel.send(embed=embed) + + async def load(self, ctx, module): + try: + data = module['data']['page']['infos']['time'] + if not data: + raise KeyError + except KeyError: + raise ModuleInformationNotFoundError + + time = re.sub(r': *(\r*\n*)*', ':\n', data) + desc = f"{time}" + desc += self.stg_string_for_desc(module) + embed = disnake.Embed(title=f"Arbeitsaufwand", + description=desc, + color=19607) + await ctx.channel.send(embed=embed) + + async def support(self, ctx, module): + try: + data = module['data']['page']['support'] + if not data: + raise KeyError + except KeyError: + raise ModuleInformationNotFoundError(f"Leider habe ich keine Mentoriate gefunden.") + + desc = "" + for support in data: + desc += f"[{support['title']}]({support['url']})\n" + desc += self.stg_string_for_desc(module) + embed = disnake.Embed(title=f"Mentoriate ", + description=desc, + color=19607) + await ctx.channel.send(embed=embed) + + async def exams(self, ctx, module): + try: + data = module['data']['page']['exams'] + if not data: + raise KeyError + except KeyError: + raise ModuleInformationNotFoundError(f"Leider habe ich keine Prüfungsinformationen gefunden.") + + desc = "" + for exam in data: + desc += f"**{exam['name']}**\n{exam['type']}\n" + if (weight := exam.get('weight')) and len(weight) > 0 and weight != '-': + desc += f"Gewichtung: **{weight}**\n" + desc += "\n" + + if (requirements := exam.get('requirements')) and len(requirements) > 0 and requirements != 'keine': + desc += f"Inhaltliche Voraussetzungen: \n{requirements}\n\n" + + if (hard_requirements := exam.get('hard_requirements')) and len(hard_requirements) > 0 \ + and hard_requirements != 'keine': + desc += f"Formale Voraussetzungen: \n{hard_requirements}\n\n" + # desc += self.stg_string_for_desc(module) + + embed = disnake.Embed(title=f"Prüfungsinformationen", + description=desc, + color=19607) + await ctx.channel.send(embed=embed) + + @help( + category="moduleinformation", + syntax="!module <command> <stg?>", + parameters={ + "command": "Das Kommando, welches ausgeführt werden soll (aufwand, handbuch, info, leseprobe, mentoriate, prüfungen)", + "stg": "*(optional)* Kürzel des Studiengangs, für den die Informationen angezeigt werden sollen (bainf, bamath, bscmatse, bawiinf, mscma, mscinf, mawiinf, mscprinf)" + }, + brief="Ruft Modulinformation ab. " + ) + @commands.group(name="module", aliases=["modul"], pass_context=True) + async def cmd_module(self, ctx): + if not ctx.invoked_subcommand: + await self.cmd_module_info(ctx) + + @help( + command_group="module", + category="moduleinformation", + syntax="!module update <stg?>", + parameters={ + "stg": "*(optional)* Kürzel des Studiengangs, für den die Informationen angezeigt werden sollen (bainf, bamath, bscmatse, bawiinf, mscma, mscinf, mawiinf, mscprinf)" + }, + mod=True, + brief="Aktualisiert die Daten über die Module (manueller Aufruf im Normalfall nicht nötig). " + ) + @cmd_module.command("update") + @commands.check(utils.is_mod) + async def cmd_module_update(self, ctx): + await ctx.channel.send("Refreshing...") + await self.refresh_data() + + @help( + command_group="module", + category="moduleinformation", + syntax="!module handbuch <stg?>", + parameters={ + "stg": "*(optional)* Kürzel des Studiengangs, für den die Informationen angezeigt werden sollen (bainf, bamath, bscmatse, bawiinf, mscma, mscinf, mawiinf, mscprinf)" + }, + brief="Zeigt den Link zum Modulhandbuch für dieses Modul an. " + ) + @cmd_module.command("handbuch", aliases=["mhb", "hb", "modulhandbuch"]) + async def cmd_module_handbuch(self, ctx, arg_stg=None): + await self.execute_subcommand(ctx, arg_stg, self.handbook) + + @help( + command_group="module", + category="moduleinformation", + syntax="!module leseprobe <stg?>", + parameters={ + "stg": "*(optional)* Kürzel des Studiengangs, für den die Informationen angezeigt werden sollen (bainf, bamath, bscmatse, bawiinf, mscma, mscinf, mawiinf, mscprinf)" + }, + brief="Zeigt den Link zur Leseprobe für diesen Kurs an." + ) + @cmd_module.command("probe", aliases=["leseprobe"]) + async def cmd_module_probe(self, ctx, arg_stg=None): + await self.execute_subcommand(ctx, arg_stg, self.reading_sample) + + @help( + command_group="module", + category="moduleinformation", + syntax="!module info <stg?>", + parameters={ + "stg": "*(optional)* Kürzel des Studiengangs, für den die Informationen angezeigt werden sollen (bainf, bamath, bscmatse, bawiinf, mscma, mscinf, mawiinf, mscprinf)" + }, + brief="Zeigt allgemeine Informationen zum Modul an." + ) + @cmd_module.command("info") + async def cmd_module_info(self, ctx, arg_stg=None): + await self.execute_subcommand(ctx, arg_stg, self.info) + + @help( + command_group="module", + category="moduleinformation", + syntax="!module aufwand <stg?>", + parameters={ + "stg": "*(optional)* Kürzel des Studiengangs, für den die Informationen angezeigt werden sollen (bainf, bamath, bscmatse, bawiinf, mscma, mscinf, mawiinf, mscprinf)" + }, + brief="Zeigt die Informationen zum zeitlichen Aufwand an. " + ) + @cmd_module.command("aufwand", aliases=["workload", "load", "zeit", "arbeitzeit"]) + async def cmd_module_aufwand(self, ctx, arg_stg=None): + await self.execute_subcommand(ctx, arg_stg, self.load) + + @help( + command_group="module", + category="moduleinformation", + syntax="!module mentoriate <stg?>", + parameters={ + "stg": "*(optional)* Kürzel des Studiengangs, für den die Informationen angezeigt werden sollen (bainf, bamath, bscmatse, bawiinf, mscma, mscinf, mawiinf, mscprinf)" + }, + brief="Zeigt eine Liste der verfügbaren Mentoriate an." + ) + @cmd_module.command("mentoriate", aliases=["mentoriat", "support"]) + async def cmd_module_mentoriate(self, ctx, arg_stg=None): + await self.execute_subcommand(ctx, arg_stg, self.support) + + @help( + command_group="module", + category="moduleinformation", + syntax="!module prüfungen <stg?>", + parameters={ + "stg": "*(optional)* Kürzel des Studiengangs, für den die Informationen angezeigt werden sollen (bainf, bamath, bscmatse, bawiinf, mscma, mscinf, mawiinf, mscprinf)" + }, + brief="Zeigt Informationen zur Prüfung an. " + ) + @cmd_module.command("prüfungen", aliases=["exam", "exams", "prüfung"]) + async def cmd_module_pruefungen(self, ctx, arg_stg=None): + await self.execute_subcommand(ctx, arg_stg, self.exams) + + async def cog_command_error(self, ctx, error): + await handle_error(ctx, error) diff --git a/cogs/news.py b/cogs/news.py index a1ed733ca63162c40d80f5a29eefe61c7636cf93..ec49112821ef3ce10474b37c72a0c5091f66b4c8 100644 --- a/cogs/news.py +++ b/cogs/news.py @@ -1,55 +1,55 @@ -import json -import os - -from aiohttp import ClientSession -from bs4 import BeautifulSoup -from discord.ext import commands, tasks - - -class News(commands.Cog): - def __init__(self, bot): - self.bot = bot - self.channel_id = int(os.getenv("DISCORD_NEWS_CHANNEL")) - self.news_role = int(os.getenv("DISCORD_NEWS_ROLE")) - self.url = "https://www.fernuni-hagen.de/mi/studium/aktuelles/index.shtml" - self.news = {} - self.load_news() - self.news_loop.start() - - def load_news(self): - news_file = open("data/news.json", mode="r") - self.news = json.load(news_file) - - def save_news(self): - news_file = open("data/news.json", mode="w") - json.dump(self.news, news_file) - - @tasks.loop(hours=1) - async def news_loop(self): - async with ClientSession() as session: - async with session.get(self.url) as r: - if r.status == 200: - content = await r.read() - soup = BeautifulSoup(content, "html.parser") - channel = await self.bot.fetch_channel(self.channel_id) - - for news in soup.find("ul", attrs={"class": "fu-link-list"}).find_all("li"): - date = news.span.text - title = str(news.a.text) - link = news.a['href'] - - if link[0] == "/": - link = f"https://www.fernuni-hagen.de" + link - - if not self.news.get(link): - await channel.send( - f":loudspeaker: <@&{self.news_role}> Neues aus der Fakultät vom {date} :loudspeaker: \n{title} \n{link}") - self.news[link] = date - else: - prev_date = self.news[link] - if date != prev_date: - await channel.send( - f":loudspeaker: <@&{self.news_role}> Neues aus der Fakultät vom {date} :loudspeaker: \n{title} \n{link}") - self.news[link] = date - - self.save_news() +import json +import os + +from aiohttp import ClientSession +from bs4 import BeautifulSoup +from disnake.ext import commands, tasks + + +class News(commands.Cog): + def __init__(self, bot): + self.bot = bot + self.channel_id = int(os.getenv("DISCORD_NEWS_CHANNEL")) + self.news_role = int(os.getenv("DISCORD_NEWS_ROLE")) + self.url = "https://www.fernuni-hagen.de/mi/studium/aktuelles/index.shtml" + self.news = {} + self.load_news() + self.news_loop.start() + + def load_news(self): + news_file = open("data/news.json", mode="r") + self.news = json.load(news_file) + + def save_news(self): + news_file = open("data/news.json", mode="w") + json.dump(self.news, news_file) + + @tasks.loop(hours=1) + async def news_loop(self): + async with ClientSession() as session: + async with session.get(self.url) as r: + if r.status == 200: + content = await r.read() + soup = BeautifulSoup(content, "html.parser") + channel = await self.bot.fetch_channel(self.channel_id) + + for news in soup.find("ul", attrs={"class": "fu-link-list"}).find_all("li"): + date = news.span.text + title = str(news.a.text) + link = news.a['href'] + + if link[0] == "/": + link = f"https://www.fernuni-hagen.de" + link + + if not self.news.get(link): + await channel.send( + f":loudspeaker: <@&{self.news_role}> Neues aus der Fakultät vom {date} :loudspeaker: \n{title} \n{link}") + self.news[link] = date + else: + prev_date = self.news[link] + if date != prev_date: + await channel.send( + f":loudspeaker: <@&{self.news_role}> Neues aus der Fakultät vom {date} :loudspeaker: \n{title} \n{link}") + self.news[link] = date + + self.save_news() diff --git a/cogs/polls.py b/cogs/polls.py index 5f2ab706982aa34522dc334d0369fb0ba299597e..90c0a740ce362890206b1c6efcb03d3162e34489 100644 --- a/cogs/polls.py +++ b/cogs/polls.py @@ -1,97 +1,97 @@ -import os - -from discord.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) +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 index ec8fe2187e68439cc4dc677405dc4beb71941f17..fa3a4d4d1112fe7f8808f727359426b0e03e7e0e 100644 --- a/cogs/roles.py +++ b/cogs/roles.py @@ -1,8 +1,8 @@ import json import os -import discord -from discord.ext import commands +import disnake +from disnake.ext import commands import utils from cogs.help import help, handle_error, help_category @@ -14,10 +14,10 @@ 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_ROLLEN_CHANNEL")) - self.degree_program_message_id = int(os.getenv("DISCORD_DEGREE_PROGRAM_MSG")) - self.color_message_id = int(os.getenv("DISCORD_COLOR_MSG")) - self.special_message_id = int(os.getenv("DISCORD_SPECIAL_MSG")) + self.channel_id = int(os.getenv("DISCORD_ROLLEN_CHANNEL", "0")) + self.degree_program_message_id = int(os.getenv("DISCORD_DEGREE_PROGRAM_MSG", "0")) + self.color_message_id = int(os.getenv("DISCORD_COLOR_MSG", "0")) + self.special_message_id = int(os.getenv("DISCORD_SPECIAL_MSG", "0")) self.assignable_roles = {} self.load_roles() @@ -81,7 +81,7 @@ class Roles(commands.Cog): guild = ctx.guild members = await guild.fetch_members().flatten() answer = f'' - embed = discord.Embed(title="Statistiken", + 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: @@ -114,7 +114,7 @@ class Roles(commands.Cog): message = await channel.fetch_message(self.degree_program_message_id) degree_program_emojis = self.get_degree_program_emojis() - embed = discord.Embed(title="Vergabe von Studiengangs-Rollen", + embed = disnake.Embed(title="Vergabe von Studiengangs-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 <:St:763126549327118366> 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"" @@ -145,7 +145,7 @@ class Roles(commands.Cog): message = await channel.fetch_message(self.color_message_id) color_emojis = self.get_color_emojis() - embed = discord.Embed(title="Vergabe von Farb-Rollen", + embed = disnake.Embed(title="Vergabe von Farb-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 <:FarbeGruen:771451407916204052> 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. ") await message.edit(content="", embed=embed) @@ -167,7 +167,7 @@ class Roles(commands.Cog): message = await channel.fetch_message(self.special_message_id) special_emojis = self.get_special_emojis() - embed = discord.Embed(title="Vergabe von Spezial-Rollen", + embed = disnake.Embed(title="Vergabe von Spezial-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 :exclamation: 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"" diff --git a/cogs/support.py b/cogs/support.py index ec88ad0abe9f1e90748dde9f265ef82020c186e7..1a8f182f66b27f26eebea115816d874055fc8d60 100644 --- a/cogs/support.py +++ b/cogs/support.py @@ -1,28 +1,28 @@ -import io -import os - -import discord -from discord.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 discord.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(discord.File(fp, filename=attachment.filename)) - - await channel.send(f"Support Nachricht von <@!{message.author.id}>:") - await channel.send(message.content, files=files) +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 index 62a2210be9215fac7e9db97577f70e428ed2d70a..1358c2fb4756cb8139190f2b79c1f8e99f3b2260 100644 --- a/cogs/text_commands.py +++ b/cogs/text_commands.py @@ -1,303 +1,303 @@ -import json -import os -import random -import re - -import discord -from discord.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 = discord.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 Boty 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)) +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 Boty 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 index e77ce62840aa494566b0917fe79c77f93b582f22..8d28271d57109d42ced019941979c107dcdca1e3 100644 --- a/cogs/timer.py +++ b/cogs/timer.py @@ -1,330 +1,337 @@ -import json -import os -import random -from asyncio import sleep -from copy import deepcopy -from datetime import datetime, timedelta - -import discord -from discord.ext import commands, tasks -from dislash import * - -from cogs.help import help - - -class Timer(commands.Cog): - - def __init__(self, bot): - self.bot = bot - self.guild_id = int(os.getenv('DISCORD_GUILD')) - self.default_names = ["Rapunzel", "Aschenputtel", "Schneewittchen", "Frau Holle", "Schneeweißchen und Rosenrot", - "Gestiefelter Kater", "Bremer Stadtmusikanten"] - self.running_timers = {} - self.timer_file_path = os.getenv("DISCORD_TIMER_FILE") - self.load_timers() - self.run_timer.start() - - def load_timers(self): - timer_file = open(self.timer_file_path, mode='r') - self.running_timers = json.load(timer_file) - - def save_timers(self): - timer_file = open(self.timer_file_path, mode='w') - json.dump(self.running_timers, timer_file) - - def get_button_row(self, enabled=True): - button_row = ActionRow( - Button( - style=ButtonStyle.grey, - emoji="ðŸ‘", - custom_id="anmelden" - ), - Button( - style=ButtonStyle.grey, - emoji="👎", - custom_id="abmelden" - ), - Button( - style=ButtonStyle.grey, - emoji="â©", - custom_id="skip" - ), - Button( - style=ButtonStyle.grey, - emoji="🔄", - custom_id="neustart" - ), - Button( - style=ButtonStyle.grey, - emoji="🛑", - custom_id="beenden" - ) - ) - if enabled: - return button_row - else: - button_row.disable_buttons() - return button_row - - def create_embed(self, name, status, working_time, break_time, remaining, registered): - color = discord.Colour.green() if status == "Arbeiten" else 0xFFC63A if status == "Pause" else discord.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 = discord.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 - - @help( - syntax="!timer <working-time?> <break-time?> <name?>", - brief="Deine persönliche Eieruhr", - parameters={ - "learning-time": "Länge der Arbeitsphase in Minuten. Default: 25", - "break-time": "Länge der Pausenphase in Minuten. Default: 5", - "name": "So soll der Timer heißen. Wird ihm kein Name gegeben, nimmt er sich selbst einen." - } - ) - @commands.command(name="timer") - async def cmd_timer(self, ctx, working_time=25, break_time=5, name=None): - name = name if name else random.choice(self.default_names) - remaining = working_time - status = "Arbeiten" - registered = [str(ctx.author.id)] - - embed = self.create_embed(name, status, working_time, break_time, remaining, registered) - msg = await ctx.send(embed=embed, components=[self.get_button_row()]) - - self.running_timers[str(msg.id)] = {'name': name, - 'status': status, - 'working_time': working_time, - 'break_time': break_time, - 'remaining': remaining, - 'registered': registered, - 'channel': ctx.channel.id} - self.save_timers() - await self.make_sound(registered, 'roll_with_it-outro.mp3') - - @commands.Cog.listener() - async def on_button_click(self, inter): - clicked_button = inter.clicked_button.custom_id - - if clicked_button == "beenden": - await self.on_beenden_button(inter) - elif clicked_button == "neustart": - await self.on_neustart_button(inter) - elif clicked_button == "skip": - await self.on_skip_button(inter) - elif clicked_button == 'anmelden': - await self.on_anmelden_button(inter) - elif clicked_button == "abmelden": - await self.on_abmelden_button(inter) - - async def on_beenden_button(self, inter): - msg_id = str(inter.message.id) - if timer := self.running_timers.get(msg_id): - registered = timer['registered'] - if str(inter.author.id) in timer['registered']: - mentions = self.get_mentions(msg_id) - timer['status'] = "Beendet" - timer['remaining'] = 0 - timer['registered'] = [] - - await inter.reply(type=7) - 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_timers() - else: - # Reply with a hidden message - await inter.reply("Nur angemeldete Personen können den Timer beenden.", ephemeral=True) - else: - await inter.reply("Etwas ist schiefgelaufen...", ephemeral=True) - - async def on_neustart_button(self, inter): - msg_id = str(inter.message.id) - if timer := self.running_timers.get(msg_id): - registered = timer['registered'] - if str(inter.author.id) in timer['registered']: - timer['status'] = 'Arbeiten' - timer['remaining'] = timer['working_time'] - self.save_timers() - - await inter.reply(type=7) - await self.edit_message(msg_id) - await self.make_sound(registered, 'roll_with_it-outro.mp3') - else: - # Reply with a hidden message - await inter.reply("Nur angemeldete Personen können den Timer neu starten.", ephemeral=True) - else: - await inter.reply("Etwas ist schiefgelaufen...", ephemeral=True) - - async def on_skip_button(self, inter): - msg_id = str(inter.message.id) - if timer := self.running_timers.get(msg_id): - registered = timer['registered'] - if str(inter.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') - else: - # Reply with a hidden message - await inter.reply("Nur angemeldete Personen können den Timer bedienen.", ephemeral=True) - else: - await inter.reply("Etwas ist schiefgelaufen...", ephemeral=True) - - async def on_anmelden_button(self, inter): - msg_id = str(inter.message.id) - if timer := self.running_timers.get(msg_id): - if str(inter.author.id) not in timer['registered']: - timer['registered'].append(str(inter.author.id)) - self.save_timers() - name, status, wt, bt, remaining, registered, _ = self.get_details(msg_id) - embed = self.create_embed(name, status, wt, bt, remaining, registered) - await inter.reply(embed=embed, components=[self.get_button_row()], type=7) - else: - await inter.reply(type=7) - else: - await inter.reply("Etwas ist schiefgelaufen...", ephemeral=True) - - async def on_abmelden_button(self, inter): - msg_id = str(inter.message.id) - if timer := self.running_timers.get(msg_id): - registered = timer['registered'] - if str(inter.author.id) in registered: - if len(registered) == 1: - await self.on_beenden_button(inter) - return - else: - timer['registered'].remove(str(inter.author.id)) - self.save_timers() - name, status, wt, bt, remaining, registered, _ = self.get_details(msg_id) - embed = self.create_embed(name, status, wt, bt, remaining, registered) - await inter.reply(embed=embed, components=[self.get_button_row()], type=7) - else: - await inter.reply(type=7) - else: - await inter.reply("Etwas ist schiefgelaufen...", ephemeral=True) - - 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_timers() - - 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, - components=[self.get_button_row(enabled=False)]) - else: - new_msg = await channel.send(mentions, embed=embed, components=[self.get_button_row()]) - self.running_timers[str(new_msg.id)] = self.running_timers[msg_id] - self.running_timers.pop(msg_id) - self.save_timers() - msg = new_msg - else: - await msg.edit(embed=embed, components=[self.get_button_row()]) - return str(msg.id) - except discord.errors.NotFound: - self.running_timers.pop(msg_id) - self.save_timers() - 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(discord.FFmpegPCMAudio(f'cogs/sounds/{filename}')) - await sleep(3) - except discord.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") +import json +import os +import random +from asyncio import sleep +from copy import deepcopy +from datetime import datetime, timedelta + +import disnake +from disnake import errors, FFmpegPCMAudio, Embed, Colour, MessageInteraction, ApplicationCommandInteraction +from disnake.ext import commands, tasks +from disnake.ui import Button +from dotenv import load_dotenv + +from cogs.help import help +from views import timer_view + +load_dotenv() + + +class Timer(commands.Cog): + + def __init__(self, bot): + self.bot = bot + self.guild_id = int(os.getenv('DISCORD_GUILD')) + self.default_names = ["Rapunzel", "Aschenputtel", "Schneewittchen", "Frau Holle", "Schneeweißchen und Rosenrot", + "Gestiefelter Kater", "Bremer Stadtmusikanten"] + self.running_timers = {} + self.timer_file_path = os.getenv("DISCORD_TIMER_FILE") + self.load_timers() + self.run_timer.start() + + def load_timers(self): + timer_file = open(self.timer_file_path, mode='r') + self.running_timers = json.load(timer_file) + + def save_timers(self): + timer_file = open(self.timer_file_path, mode='w') + json.dump(self.running_timers, timer_file) + + @help( + syntax="!timer <working-time?> <break-time?> <name?>", + brief="Deine persönliche Eieruhr", + parameters={ + "learning-time": "Länge der Arbeitsphase in Minuten. Default: 25", + "break-time": "Länge der Pausenphase in Minuten. Default: 5", + "name": "So soll der Timer heißen. Wird ihm kein Name gegeben, nimmt er sich selbst einen." + } + ) + @commands.command(name="timer") + async def cmd_timer(self, ctx: disnake.ext.commands.Context, 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(ctx.author.id)] + + embed = self.create_embed(name, status, working_time, break_time, remaining, registered) + msg = await ctx.send(embed=embed, view=timer_view.TimerView()) + + self.running_timers[str(msg.id)] = {'name': name, + 'status': status, + 'working_time': working_time, + 'break_time': break_time, + 'remaining': remaining, + 'registered': registered, + 'channel': ctx.channel.id} + self.save_timers() + await self.make_sound(registered, 'roll_with_it-outro.mp3') + + @commands.slash_command(name="timer", description="Erstelle deine persönliche Eieruhr", + guild_ids=[int(os.getenv('DISCORD_GUILD'))]) + async def cmd_slash_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=timer_view.TimerView()) + 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_timers() + await self.make_sound(registered, 'roll_with_it-outro.mp3') + + def create_embed(self, name, status, working_time, break_time, remaining, registered): + color = Colour.green() if status == "Arbeiten" else 0xFFC63A if status == "Pause" else 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 = 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.Cog.listener() + async def on_button_click(self, interaction: MessageInteraction): + button: Button = interaction.component + + if button.custom_id == "timerview:subscribe": + await self.on_subscribe(interaction) + elif button.custom_id == "timerview:unsubscribe": + await self.on_unsubscribe(interaction) + elif button.custom_id == "timverview:skip": + await self.on_skip(interaction) + elif button.custom_id == "timerview:restart": + await self.on_restart(interaction) + elif button.custom_id == "timerview:stop": + await self.on_stop(interaction) + + async def on_subscribe(self, interaction: MessageInteraction): + message = interaction.message + message_id = str(message.id) + author = interaction.author + + if timer := self.running_timers.get(message_id): + if str(author.id) not in timer['registered']: + timer['registered'].append(str(author.id)) + self.save_timers() + name, status, wt, bt, remaining, registered, _ = self.get_details(message_id) + embed = self.create_embed(name, status, wt, bt, remaining, registered) + await message.edit(embed=embed, view=timer_view.TimerView()) + else: + await interaction.response.send_message("Etwas ist schiefgelaufen...", ephemeral=True) + + async def on_unsubscribe(self, interaction: MessageInteraction): + message = interaction.message + message_id = str(message.id) + author = interaction.author + + if timer := self.running_timers.get(message_id): + registered = timer['registered'] + if str(author.id) in registered: + if len(registered) == 1: + await self.on_stop(interaction) + return + else: + timer['registered'].remove(str(author.id)) + self.save_timers() + name, status, wt, bt, remaining, registered, _ = self.get_details(message_id) + embed = self.create_embed(name, status, wt, bt, remaining, registered) + await message.edit(embed=embed, view=timer_view.TimerView()) + else: + await interaction.response.send_message("Etwas ist schiefgelaufen...", ephemeral=True) + + async def on_skip(self, interaction: MessageInteraction): + message = interaction.message + message_id = str(message.id) + author = interaction.author + + if timer := self.running_timers.get(message_id): + registered = timer['registered'] + if str(author.id) in timer['registered']: + new_phase = await self.switch_phase(message_id) + if new_phase == "Pause": + await self.make_sound(registered, 'groove-intro.mp3') + else: + await self.make_sound(registered, 'roll_with_it-outro.mp3') + else: + # Reply with a hidden message + 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, interaction: MessageInteraction): + message = interaction.message + message_id = str(message.id) + author = interaction.author + + if timer := self.running_timers.get(message_id): + registered = timer['registered'] + if str(author.id) in timer['registered']: + timer['status'] = 'Arbeiten' + timer['remaining'] = timer['working_time'] + self.save_timers() + + await self.edit_message(message_id) + await self.make_sound(registered, 'roll_with_it-outro.mp3') + else: + # Reply with a hidden message + 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, interaction: MessageInteraction): + message = interaction.message + message_id = str(message.id) + author = interaction.author + + if timer := self.running_timers.get(message_id): + registered = timer['registered'] + if str(author.id) in timer['registered']: + mentions = self.get_mentions(message_id) + timer['status'] = "Beendet" + timer['remaining'] = 0 + timer['registered'] = [] + + if new_msg_id := await self.edit_message(message_id, mentions=mentions): + await self.make_sound(registered, 'applause.mp3') + self.running_timers.pop(new_msg_id) + self.save_timers() + 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) + + 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_timers() + + 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) + else: + new_msg = await channel.send(mentions, embed=embed, view=timer_view.TimerView()) + self.running_timers[str(new_msg.id)] = self.running_timers[msg_id] + self.running_timers.pop(msg_id) + self.save_timers() + msg = new_msg + else: + await msg.edit(embed=embed, view=timer_view.TimerView()) + return str(msg.id) + except errors.NotFound: + self.running_timers.pop(msg_id) + self.save_timers() + 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(FFmpegPCMAudio(f'cogs/sounds/{filename}')) + await sleep(3) + except 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/tops.py b/cogs/tops.py deleted file mode 100644 index 3ed444ce1924c8d838168868a260c99665a047bf..0000000000000000000000000000000000000000 --- a/cogs/tops.py +++ /dev/null @@ -1,108 +0,0 @@ -import json -import os -import re - -import discord -from discord.ext import commands -from cogs.help import help, handle_error - - -class Tops(commands.Cog): - def __init__(self, bot): - self.bot = bot - self.tops_file = os.getenv('DISCORD_TOPS_FILE') - self.tops = {} - self.load_tops() - - def load_tops(self): - """ Loads all TOPs from TOPS_FILE """ - - tops_file = open(self.tops_file, mode='r') - self.tops = json.load(tops_file) - - @help( - brief="Fügt einen Tagesordnungspunkt zum Channel hinzu.", - parameters={ - "top": "Der hinzuzufügende Tagesordnungspunkt." - } - ) - @commands.command(name="add-top") - async def cmd_add_top(self, ctx, top): - """ Add TOP to a channel """ - - channel = ctx.channel - - if str(channel.id) not in self.tops: - self.tops[str(channel.id)] = [] - - channel_tops = self.tops.get(str(channel.id)) - channel_tops.append(top) - - tops_file = open(self.tops_file, mode='w') - json.dump(self.tops, tops_file) - - @help( - brief="Löscht einen Tagesordnungspunkt in einem Channel.", - parameters={ - "top": "Numerischer Index des zu löschenden Tagesordnungspunkts." - } - ) - @commands.command(name="remove-top") - async def cmd_remove_top(self, ctx, top): - """ Remove TOP from a channel """ - channel = ctx.channel - - if not re.match(r'^-?\d+$', top): - await ctx.send("Fehler! Der übergebene Parameter muss eine Zahl sein.") - else: - if str(channel.id) in self.tops: - channel_tops = self.tops.get(str(channel.id)) - - if 0 < int(top) <= len(channel_tops): - del channel_tops[int(top) - 1] - - if len(channel_tops) == 0: - self.tops.pop(str(channel.id)) - - tops_file = open(self.tops_file, mode='w') - json.dump(self.tops, tops_file) - - @help( - brief="Löscht alle Tagesordnungspunkte in einem Channel.", - ) - @commands.command(name="clear-tops") - async def cmd_clear_tops(self, ctx): - """ Clear all TOPs from a channel """ - - channel = ctx.channel - - if str(channel.id) in self.tops: - self.tops.pop(str(channel.id)) - tops_file = open(self.tops_file, mode='w') - json.dump(self.tops, tops_file) - - @help( - brief="Zeigt alle Tagesordnungspunkte in einem Channel an.", - ) - @commands.command(name="tops") - async def cmd_tops(self, ctx): - """ Get all TOPs from a channel """ - - channel = ctx.channel - - embed = discord.Embed(title="Tagesordnungspunkte", - color=19607) - embed.add_field(name="\u200B", value="\u200B", inline=False) - - if str(channel.id) in self.tops: - channel_tops = self.tops.get(str(channel.id)) - - for i in range(0, len(channel_tops)): - embed.add_field(name=f"TOP {i + 1}", value=channel_tops[i], inline=False) - else: - embed.add_field(name="Keine Tagesordnungspunkte vorhanden.", value="\u200B", inline=False) - - await ctx.send(embed=embed) - - async def cog_command_error(self, ctx, error): - await handle_error(ctx, error) diff --git a/cogs/voice.py b/cogs/voice.py index c0f18e37e4aa7a976bb79e9f28537ae6c2db4cae..20c1b5d62b0365008d4aea772050d700eb72e2e5 100644 --- a/cogs/voice.py +++ b/cogs/voice.py @@ -1,32 +1,32 @@ -from discord.ext import commands - -import utils -from cogs.help import help, handle_error - - -class Voice(commands.Cog): - def __init__(self, bot): - self.bot = bot - - @help( - brief="Öffnet und schließt die Voice-Kanäle.", - parameters={ - "switch": "open öffnet die Voice-Kanäle, close schließt die Voice-Kanäle." - }, - example="!voice close", - mod=True - ) - @commands.command(name="voice") - @commands.check(utils.is_mod) - async def cmd_voice(self, ctx, switch): - voice_channels = ctx.guild.voice_channels - print(voice_channels[0].user_limit) - if switch == "open": - for voice_channel in voice_channels: - await voice_channel.edit(user_limit=0) - elif switch == "close": - for voice_channel in voice_channels: - await voice_channel.edit(user_limit=1) - - async def cog_command_error(self, ctx, error): - await handle_error(ctx, error) +from disnake.ext import commands + +import utils +from cogs.help import help, handle_error + + +class Voice(commands.Cog): + def __init__(self, bot): + self.bot = bot + + @help( + brief="Öffnet und schließt die Voice-Kanäle.", + parameters={ + "switch": "open öffnet die Voice-Kanäle, close schließt die Voice-Kanäle." + }, + example="!voice close", + mod=True + ) + @commands.command(name="voice") + @commands.check(utils.is_mod) + async def cmd_voice(self, ctx, switch): + voice_channels = ctx.guild.voice_channels + print(voice_channels[0].user_limit) + if switch == "open": + for voice_channel in voice_channels: + await voice_channel.edit(user_limit=0) + elif switch == "close": + for voice_channel in voice_channels: + await voice_channel.edit(user_limit=1) + + async def cog_command_error(self, ctx, error): + await handle_error(ctx, error) diff --git a/cogs/welcome.py b/cogs/welcome.py index 70eefa078ec3eb3771fed1adba8c0bdf3c7d9be6..8b4ce90d17f1889cce0ef4ebdaa77ef4f7a6b49d 100644 --- a/cogs/welcome.py +++ b/cogs/welcome.py @@ -1,7 +1,7 @@ import os -import discord -from discord.ext import commands +import disnake +from disnake.ext import commands import utils from cogs.help import help, handle_error @@ -10,8 +10,8 @@ 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")) + self.channel_id = int(os.getenv("DISCORD_WELCOME_CHANNEL", "0")) + self.message_id = int(os.getenv("DISCORD_WELCOME_MSG", "0")) @help( category="updater", @@ -24,7 +24,7 @@ class Welcome(commands.Cog): channel = await self.bot.fetch_channel(self.channel_id) message = await channel.fetch_message(self.message_id) - embed = discord.Embed(title="Herzlich Willkommen auf dem Discord von Studierenden für Studierende.", + embed = disnake.Embed(title="Herzlich Willkommen auf dem Discord von Studierenden für Studierende.", description="Disclaimer: Das hier ist kein offizieller Kanal der Fernuni. Hier findet auch keine offizielle Betreuung durch die Fernuni statt. Dieser Discord dient zum Austausch unter Studierenden über einzelne Kurse, um sich gegenseitig helfen zu können, aber auch um über andere Themen in einen Austausch zu treten. Es soll KEIN Ersatz für die Kanäle der Lehrgebiete sein, wie die Newsgroups, Moodle-Foren und was es noch so gibt. Der Discord soll die Möglichkeit bieten, feste Lerngruppen zu finden und sich in diesen gegenseitig zu helfen und zu treffen. Zudem soll er durch den Austausch in den Kanälen auch eine Art flexible Lerngruppe zu einzelnen Kursen ermöglichen. Daher ist unser Apell an euch: Nutzt bitte auch die Betreuungsangebote der entsprechenden Kurse, in die ihr eingeschrieben seid. ") #kürzen embed.set_thumbnail( diff --git a/cogs/xkcd.py b/cogs/xkcd.py index a0e01bc38cf91159611be10ecddec8b8a63ed61b..aaab79552b547687052a12f25a7d40ef93cc5c14 100644 --- a/cogs/xkcd.py +++ b/cogs/xkcd.py @@ -1,56 +1,56 @@ -import random -import aiohttp - -import discord -from discord.ext import commands -from cogs.help import help - - -class Xkcd(commands.Cog): - def __init__(self, bot): - self.bot = bot - - @help( - brief="Ruft einen xkcd Comic ab.", - syntax="!xkcd <number>", - parameters={ - "number": "*(optional)* Entweder die Nummer eines spezifischen xkcd Comics, oder `latest`, für den aktuellsten.", - }, - ) - @commands.command(name="xkcd") - async def cmd_xkcd(self, ctx, number=None): - - async with aiohttp.ClientSession() as session: - - # Daten vom aktuellsten Comic holen, um max zu bestimmen - async with session.get('http://xkcd.com/info.0.json') as request: - data = await request.json() - max = data['num'] - - # Nummer übernehmen wenn vorhanden und zwischen 1 und max, sonst random Nummer wählen - if number == 'latest': - n = max - else: - try: - n = number if (number and 0 < int(number) <= max) else str(random.randint(1, max)) - except ValueError: - n = str(random.randint(1, max)) - - # Daten zum Bild holen - async with session.get(f'http://xkcd.com/{n}/info.0.json') as request: - n_data = await request.json() - - img = n_data['img'] - num = n_data['num'] - title = n_data['title'] - text = n_data['alt'] - - # Comic embedden - e = discord.Embed() - e.set_image(url=img) - e.url = img - e.title = f'xkcd #{num}' - e.add_field(name=title, value=text) - e.set_footer(text='https://xkcd.com', icon_url='https://xkcd.com/s/0b7742.png') - - await ctx.send(embed=e) +import random +import aiohttp + +import disnake +from disnake.ext import commands +from cogs.help import help + + +class Xkcd(commands.Cog): + def __init__(self, bot): + self.bot = bot + + @help( + brief="Ruft einen xkcd Comic ab.", + syntax="!xkcd <number>", + parameters={ + "number": "*(optional)* Entweder die Nummer eines spezifischen xkcd Comics, oder `latest`, für den aktuellsten.", + }, + ) + @commands.command(name="xkcd") + async def cmd_xkcd(self, ctx, number=None): + + async with aiohttp.ClientSession() as session: + + # Daten vom aktuellsten Comic holen, um max zu bestimmen + async with session.get('http://xkcd.com/info.0.json') as request: + data = await request.json() + max = data['num'] + + # Nummer übernehmen wenn vorhanden und zwischen 1 und max, sonst random Nummer wählen + if number == 'latest': + n = max + else: + try: + n = number if (number and 0 < int(number) <= max) else str(random.randint(1, max)) + except ValueError: + n = str(random.randint(1, max)) + + # Daten zum Bild holen + async with session.get(f'http://xkcd.com/{n}/info.0.json') as request: + n_data = await request.json() + + img = n_data['img'] + num = n_data['num'] + title = n_data['title'] + text = n_data['alt'] + + # Comic embedden + e = disnake.Embed() + e.set_image(url=img) + e.url = img + e.title = f'xkcd #{num}' + e.add_field(name=title, value=text) + e.set_footer(text='https://xkcd.com', icon_url='https://xkcd.com/s/0b7742.png') + + await ctx.send(embed=e) diff --git a/fernuni_bot.py b/fernuni_bot.py index dee6719d7f983dd118b640c603910569fad3f273..22dbe8857bb0d94b3e46c5242326064ea916aad0 100644 --- a/fernuni_bot.py +++ b/fernuni_bot.py @@ -1,114 +1,121 @@ -import os - -import discord -from discord.ext import commands -from dislash import * -from dotenv import load_dotenv - -from cogs import appointments, armin, calmdown, christmas, easter, github, help, learninggroups, links, \ - module_information, news, polls, roles, support, text_commands, voice, welcome, xkcd, timer - -# .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') -OWNER = int(os.getenv('DISCORD_OWNER')) -ROLES_FILE = os.getenv('DISCORD_ROLES_FILE') -HELP_FILE = os.getenv('DISCORD_HELP_FILE') -CATEGORY_LERNGRUPPEN = os.getenv("DISCORD_CATEGORY_LERNGRUPPEN") -PIN_EMOJI = "📌" - -intents = discord.Intents.default() -intents.members = True -bot = commands.Bot(command_prefix='!', help_command=None, activity=discord.Game(ACTIVITY), owner_id=OWNER, - intents=intents) -bot.add_cog(appointments.Appointments(bot)) -bot.add_cog(text_commands.TextCommands(bot)) -bot.add_cog(polls.Polls(bot)) -bot.add_cog(roles.Roles(bot)) -bot.add_cog(welcome.Welcome(bot)) -bot.add_cog(christmas.Christmas(bot)) -bot.add_cog(support.Support(bot)) -bot.add_cog(news.News(bot)) -bot.add_cog(links.Links(bot)) -bot.add_cog(voice.Voice(bot)) -bot.add_cog(easter.Easter(bot)) -bot.add_cog(armin.Armin(bot)) -bot.add_cog(learninggroups.LearningGroups(bot)) -bot.add_cog(module_information.ModuleInformation(bot)) -bot.add_cog(xkcd.Xkcd(bot)) -bot.add_cog(help.Help(bot)) -bot.add_cog(calmdown.Calmdown(bot)) -bot.add_cog(github.Github(bot)) -bot.add_cog(timer.Timer(bot)) - -# bot.add_cog(ChangeLogCog(bot)) - -SlashClient(bot, show_warnings=True) # Stellt den Zugriff auf die Buttons bereit - - -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() - - -async def unpin_message(message): - """ Unpin the given message, if it is pinned, and it has no pin reaction remaining. """ - - if message.pinned: - reaction = get_reaction(message.reactions) - if reaction is None: - await message.unpin() - - -@bot.event -async def on_ready(): - print("Client started!") - - -@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) - - -@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.event -async def on_voice_state_update(member, before, after): - if before.channel != after.channel and after.channel and "Lerngruppen-Voicy" in after.channel.name: - category = await bot.fetch_channel(CATEGORY_LERNGRUPPEN) - voice_channels = category.voice_channels - - for voice_channel in voice_channels: - if len(voice_channel.members) == 0: - return - - await category.create_voice_channel(f"Lerngruppen-Voicy-{len(voice_channels) + 1}", bitrate=256000) - - -bot.run(TOKEN) +import os + +import disnake +from disnake.ext import commands +from dotenv import load_dotenv + +from cogs import appointments, calmdown, christmas, easter, github, help, learninggroups, links, \ + news, polls, roles, support, text_commands, voice, welcome, xkcd +# , timer + +# .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') +OWNER = int(os.getenv('DISCORD_OWNER')) +ROLES_FILE = os.getenv('DISCORD_ROLES_FILE') +HELP_FILE = os.getenv('DISCORD_HELP_FILE') +CATEGORY_LERNGRUPPEN = os.getenv("DISCORD_CATEGORY_LERNGRUPPEN") +PIN_EMOJI = "📌" + +intents = disnake.Intents.default() +intents.members = True + + +class Boty(commands.Bot): + def __init__(self): + super().__init__(command_prefix='!', help_command=None, activity=disnake.Game(ACTIVITY), owner_id=OWNER, + intents=intents) + self.add_cog(appointments.Appointments(self)) + self.add_cog(text_commands.TextCommands(self)) + self.add_cog(polls.Polls(self)) + self.add_cog(roles.Roles(self)) + self.add_cog(welcome.Welcome(self)) + self.add_cog(christmas.Christmas(self)) + self.add_cog(support.Support(self)) + self.add_cog(news.News(self)) + self.add_cog(links.Links(self)) + self.add_cog(voice.Voice(self)) + self.add_cog(easter.Easter(self)) + self.add_cog(learninggroups.LearningGroups(self)) + # self.add_cog(module_information.ModuleInformation(self)) + self.add_cog(xkcd.Xkcd(self)) + self.add_cog(help.Help(self)) + self.add_cog(calmdown.Calmdown(self)) + self.add_cog(github.Github(self)) + # self.add_cog(timer.Timer(self)) + + +bot = Boty() + + +# bot.add_cog(ChangeLogCog(bot)) + +# SlashClient(bot, show_warnings=True) # Stellt den Zugriff auf die Buttons bereit + + +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() + + +async def unpin_message(message): + """ Unpin the given message, if it is pinned, and it has no pin reaction remaining. """ + + if message.pinned: + reaction = get_reaction(message.reactions) + if reaction is None: + await message.unpin() + + +@bot.event +async def on_ready(): + print("Client started!") + + +@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) + + +@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.event +async def on_voice_state_update(member, before, after): + if before.channel != after.channel and after.channel and "Lerngruppen-Voicy" in after.channel.name: + category = await bot.fetch_channel(CATEGORY_LERNGRUPPEN) + voice_channels = category.voice_channels + + for voice_channel in voice_channels: + if len(voice_channel.members) == 0: + return + + await category.create_voice_channel(f"Lerngruppen-Voicy-{len(voice_channels) + 1}", bitrate=256000) + + +bot.run(TOKEN) diff --git a/requirements.txt b/requirements.txt index 9e7bf4d9c881da6710fcad849534651c6c02aa97..8e9f2d2ec46e5f83572f293219e5bb10f1e1d5e9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,18 +1,21 @@ -aiohttp==3.7.4 -async-timeout==3.0.1 -attrs==20.3.0 -beautifulsoup4==4.9.3 -certifi==2020.12.5 -chardet==3.0.4 -discord.py==1.7.3 -emoji==1.2.0 -idna==2.10 -multidict==5.1.0 -python-dotenv==0.17.0 -soupsieve==2.2.1 -typing-extensions==3.7.4.3 -urllib3==1.26.5 -yarl==1.6.3 -dislash.py==1.2.4 -discord.py[voice]==1.7.3 -pynacl==1.4.0 +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.1.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 diff --git a/utils.py b/utils.py index 8eaf3c868142f417fb3b55eebbdc47bd62b10efa..f66beb2bd5aa74dfcc8e4bc7603619258f323d7a 100644 --- a/utils.py +++ b/utils.py @@ -1,43 +1,57 @@ -import os - -import discord -import re - -async def send_dm(user, message, embed=None): - """ Send DM to a user/member """ - - if type(user) is discord.User or type(user) is discord.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 == int(os.getenv("DISCORD_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) - +import os + +import disnake +import re + +from disnake import ButtonStyle + +from views.dialog_view import DialogView + + +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() + + return 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 == int(os.getenv("DISCORD_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) + + +async def confirm(channel, title, description, message="", custom_prefix="", callback=None): + embed = disnake.Embed(title=title, + description=description, + color=19607) + return await channel.send(message, embed=embed, view=DialogView([ + {"emoji": "ðŸ‘", "custom_id": f"{custom_prefix}_yes", "style": ButtonStyle.green}, + {"emoji": "👎", "custom_id": f"{custom_prefix}_no", "style": ButtonStyle.red}, + ])) diff --git a/views/dialog_view.py b/views/dialog_view.py new file mode 100644 index 0000000000000000000000000000000000000000..2f62a79ddb72c99de29b835c190138f757607baf --- /dev/null +++ b/views/dialog_view.py @@ -0,0 +1,30 @@ +import disnake +from disnake import ButtonStyle + +class DialogView(disnake.ui.View): + def __init__(self, buttons=None, callback=None): + super().__init__(timeout=None) + self.callback = callback + for button_config in buttons: + self.add_button(button_config) + + def add_button(self, config): + button = disnake.ui.Button( + style=config.get("style", ButtonStyle.grey), + label=config.get("label", None), + disabled=config.get("disabled", False), + custom_id=config.get("custom_id", None), + url=config.get("url", None), + emoji=config.get("emoji", None), + row=config.get("row", None) + ) + button.value = config.get("value") + if self.callback: + button.callback = self.internal_callback(button) + self.add_item(button) + + def internal_callback(self, button): + async def button_callback(interaction): + await self.callback(button, interaction, value=button.value) + return button_callback + diff --git a/views/timer_view.py b/views/timer_view.py new file mode 100644 index 0000000000000000000000000000000000000000..29385136201a416cda29fdb835c502a079cbc88b --- /dev/null +++ b/views/timer_view.py @@ -0,0 +1,26 @@ +import disnake +from disnake import MessageInteraction, ButtonStyle +from disnake.ui import Button + + +class TimerView(disnake.ui.View): + + @disnake.ui.button(emoji="ðŸ‘", style=ButtonStyle.grey, custom_id="timerview:subscribe") + async def btn_subscribe(self, button: Button, interaction: MessageInteraction): + pass + + @disnake.ui.button(emoji="👎", style=ButtonStyle.grey, custom_id="timerview:unsubscribe") + async def btn_unsubscribe(self, button: Button, interaction: MessageInteraction): + pass + + @disnake.ui.button(emoji="â©", style=ButtonStyle.grey, custom_id="timverview:skip") + async def btn_skip(self, button: Button, interaction: MessageInteraction): + pass + + @disnake.ui.button(emoji="🔄", style=ButtonStyle.grey, custom_id="timerview:restart") + async def btn_restart(self, button: Button, interaction: MessageInteraction): + pass + + @disnake.ui.button(emoji="🛑", style=ButtonStyle.grey, custom_id="timerview:stop") + async def btn_stop(self, button: Button, interaction: MessageInteraction): + pass