diff --git a/.env.template b/.env.template
index dc2d457745dee8468d4dfdce6e5f3700f931e103..76ca452d1a9db008a1ee95e3b165de613c55b3f2 100644
--- a/.env.template
+++ b/.env.template
@@ -2,9 +2,6 @@
 DISCORD_TOKEN=<Bot Token>
 DISCORD_GUILD=<ID of Guild, this Bot should be used at>
 DISCORD_ACTIVITY=<What should be shown, Bot is playing right now>
-DISCORD_GITHUB_USER=<Github username used to create issues>
-DISCORD_GITHUB_TOKEN=<Github personal access token, can be created in Settings > Developer settings > Personal access tokens (repo scope neccessary)>
-DISCORD_GITHUB_ISSUE_URL=<URL of Github API to create Issues in a repo>
 DISCORD_PROD=<True, if running in an productive environment, otherwise False>
 
 # IDs
@@ -27,8 +24,6 @@ DISCORD_LEARNINGGROUPS_PRIVATE=<ID of the channel category for private learning
 DISCORD_LEARNINGGROUPS_ARCHIVE=<ID of the channel category for archived learning groups>
 DISCORD_LEARNINGGROUPS_REQUEST=<ID of the channel category for learning group requests made via the bot>
 DISCORD_LEARNINGGROUPS_INFO=<ID of the channel category for posting or updating information about the learning group>
-DISCORD_IDEE_CHANNEL=<ID of Channel, where bot ideas can be submitted>
-DISCORD_IDEE_EMOJI=<ID of Idee Emoji, used for reactions>
 DISCORD_CALMDOWN_ROLE=<ID of "Calmdown" role>
 DISCORD_BOTUEBUNGSPLATZ_CHANNEL=<ID of 'bot-übungsplatz' channel>
 DISCORD_ELM_STREET_CHANNEL=<ID of elm street channel>
@@ -51,5 +46,4 @@ DISCORD_ADVENT_CALENDAR_FILE=<File name for advent calendar JSON file>
 
 # Misc
 DISCORD_DATE_TIME_FORMAT=<Date and time format used for commands like %d.%m.%Y %H:%M>
-DISCORD_IDEE_REACT_QTY=<Amount of reactions to a submitted idea, neccessary to create a github issue (amount is including Boty's own reaction)>
 DISCORD_ADVENT_CALENDAR_START=<Start date and time for advent calendar. Something like "01.12.2021 00:00">
diff --git a/.gitignore b/.gitignore
index 2fc0bd9f14c4b640eac20222b265df0e530eaeed..89bb454a88a868f1b2f9c19f58b87e5a93fa9f0a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -9,7 +9,8 @@ media
 # Backup files # 
 *.bak 
 
-# If you are using PyCharm # 
+# If you are using PyCharm #
+.idea
 .idea/**/workspace.xml 
 .idea/**/tasks.xml 
 .idea/dictionaries 
diff --git a/.idea/.gitignore b/.idea/.gitignore
deleted file mode 100644
index e7e9d11d4bf243bffe4bb60b4ac1f9392297f4bf..0000000000000000000000000000000000000000
--- a/.idea/.gitignore
+++ /dev/null
@@ -1,2 +0,0 @@
-# Default ignored files
-/workspace.xml
diff --git a/.idea/fernuni-bot.iml b/.idea/fernuni-bot.iml
deleted file mode 100644
index 6b3214ed4bb27c60df903757034ff0cdaa7b1d3e..0000000000000000000000000000000000000000
--- a/.idea/fernuni-bot.iml
+++ /dev/null
@@ -1,10 +0,0 @@
-<?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.10 (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
deleted file mode 100644
index dd4c951ef44ebdc37bbe4a453aab974c815ca6f6..0000000000000000000000000000000000000000
--- a/.idea/inspectionProfiles/profiles_settings.xml
+++ /dev/null
@@ -1,7 +0,0 @@
-<component name="InspectionProjectProfileManager">
-  <settings>
-    <option name="PROJECT_PROFILE" value="Default" />
-    <option name="USE_PROJECT_PROFILE" value="false" />
-    <version value="1.0" />
-  </settings>
-</component>
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
deleted file mode 100644
index 0fb7607056aaf6fa8258d09a35a49cac44d4c344..0000000000000000000000000000000000000000
--- a/.idea/misc.xml
+++ /dev/null
@@ -1,7 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<project version="4">
-  <component name="JavaScriptSettings">
-    <option name="languageLevel" value="ES6" />
-  </component>
-  <component name="ProjectRootManager" version="2" project-jdk-name="Python 3.10 (fernuni-bot)" project-jdk-type="Python SDK" />
-</project>
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
deleted file mode 100644
index 8f3c7c989576f00dbfa4e302079ae1806071164c..0000000000000000000000000000000000000000
--- a/.idea/modules.xml
+++ /dev/null
@@ -1,8 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<project version="4">
-  <component name="ProjectModuleManager">
-    <modules>
-      <module fileurl="file://$PROJECT_DIR$/.idea/fernuni-bot.iml" filepath="$PROJECT_DIR$/.idea/fernuni-bot.iml" />
-    </modules>
-  </component>
-</project>
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
deleted file mode 100644
index 94a25f7f4cb416c083d265558da75d457237d671..0000000000000000000000000000000000000000
--- a/.idea/vcs.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<project version="4">
-  <component name="VcsDirectoryMappings">
-    <mapping directory="$PROJECT_DIR$" vcs="Git" />
-  </component>
-</project>
\ No newline at end of file
diff --git a/cogs/appointments.py b/cogs/appointments.py
deleted file mode 100644
index 11f6d76acc8dd0956956d6cb1598e37867f8eb2b..0000000000000000000000000000000000000000
--- a/cogs/appointments.py
+++ /dev/null
@@ -1,258 +0,0 @@
-import asyncio
-import datetime
-import io
-import json
-import os
-import uuid
-
-import disnake
-from disnake.ext import tasks, commands
-
-import utils
-from cogs.help import help, handle_error, help_category
-
-
-def get_ics_file(title, date_time, reminder, recurring):
-    fmt = "%Y%m%dT%H%M"
-    appointment = f"BEGIN:VCALENDAR\n" \
-                  f"PRODID: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
deleted file mode 100644
index f017848a1c33c52fea87e7fdeedf8b962933577c..0000000000000000000000000000000000000000
--- a/cogs/armin.py
+++ /dev/null
@@ -1,35 +0,0 @@
-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
deleted file mode 100644
index d9a1122c70af4b00b0add895afeb02fc8418daa4..0000000000000000000000000000000000000000
--- a/cogs/calmdown.py
+++ /dev/null
@@ -1,107 +0,0 @@
-import datetime
-import json
-import os
-import re
-
-import disnake
-from disnake.ext import commands, tasks
-
-import utils
-from cogs.help import help
-
-"""
-    DISCORD_CALMDOWN_ROLE - Die Rollen-ID der "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/elm_street.py b/cogs/elm_street.py
deleted file mode 100644
index d3e693d7dedf25e81ab8a6393c0cdef4108099c0..0000000000000000000000000000000000000000
--- a/cogs/elm_street.py
+++ /dev/null
@@ -1,732 +0,0 @@
-import json
-import os
-from asyncio import sleep
-from copy import deepcopy
-from random import SystemRandom
-from typing import Union
-
-import disnake
-from disnake import ApplicationCommandInteraction, ButtonStyle
-from disnake.ext import commands, tasks
-from dotenv import load_dotenv
-
-from utils import send_dm
-
-load_dotenv()
-
-
-def get_player_from_embed(embed: disnake.Embed):
-    return embed.description.split()[0].strip("<@!>")
-
-
-def calculate_sweets(event):
-    sweets_min = event.get("sweets_min")
-    sweets_max = event.get("sweets_max")
-
-    if sweets_min and sweets_max:
-        return SystemRandom().randint(sweets_min, sweets_max)
-
-    return None
-
-
-def calculate_courage(event):
-    courage_min = event.get("courage_min")
-    courage_max = event.get("courage_max")
-
-    if courage_min and courage_max:
-        return SystemRandom().randint(courage_min, courage_max)
-
-    return None
-
-
-ShowOption = commands.option_enum(["10", "all"])
-
-
-def get_doors_visited(group):
-    if doors_visited := group.get("doors_visited"):
-        return doors_visited
-    else:
-        doors_visited = []
-        group["doors_visited"] = doors_visited
-        return doors_visited
-
-
-class ElmStreet(commands.Cog):
-    def __init__(self, bot):
-
-        self.max_courage = 100
-        self.min_courage = 20
-        self.min_group_courage = 20
-
-        self.inc_courage_step = 10
-
-        self.bot = bot
-        self.groups = {}
-        self.players = {}
-        self.story = {}
-        self.load()
-        self.elm_street_channel_id = int(os.getenv("DISCORD_ELM_STREET_CHANNEL"))
-        self.halloween_category_id = int(os.getenv("DISCORD_HALLOWEEN_CATEGORY"))
-        self.bot.view_manager.register("on_join", self.on_join)
-        self.bot.view_manager.register("on_joined", self.on_joined)
-        self.bot.view_manager.register("on_start", self.on_start)
-        self.bot.view_manager.register("on_stop", self.on_stop)
-        self.bot.view_manager.register("on_story", self.on_story)
-        self.bot.view_manager.register("on_leave", self.on_leave)
-
-        self.increase_courage.start()
-
-    def load(self):
-        with open("data/elm_street_groups.json", "r") as groups_file:
-            self.groups = json.load(groups_file)
-        with open("data/elm_street_players.json", "r") as players_file:
-            self.players = json.load(players_file)
-        with open("data/elm_street_story.json", "r") as story_file:
-            self.story = json.load(story_file)
-
-    def save(self):
-        with open("data/elm_street_groups.json", "w") as groups_file:
-            json.dump(self.groups, groups_file)
-        with open("data/elm_street_players.json", "w") as players_file:
-            json.dump(self.players, players_file)
-
-    @commands.slash_command(name="leaderboard",
-                            description="Zeigt das Leaderboard der Elm Street Sammlerinnen-Gemeinschaft an.",
-                            guild_ids=[int(os.getenv('DISCORD_GUILD'))])
-    async def cmd_leaderboard(self, interaction: ApplicationCommandInteraction, show: ShowOption = "10"):
-        embed = await self.leaderboard(all=show)
-        await interaction.response.send_message(embed=embed, ephemeral=True)
-
-    @commands.slash_command(name="group-stats",
-                            description="Zeigt die aktuelle Gruppenstatistik an.",
-                            guild_ids=[int(os.getenv('DISCORD_GUILD'))])
-    async def cmd_group_stats(self, interaction: ApplicationCommandInteraction):
-        thread_id = interaction.channel_id
-        if str(thread_id) in self.groups.keys():
-            embed = await self.get_group_stats_embed(interaction.channel_id)
-            await interaction.response.send_message(embed=embed)
-        else:
-            await interaction.response.send_message("Gruppenstatistiken können nur in Gruppenthreads ausgegeben werden."
-                                                    , ephemeral=True)
-
-    @commands.slash_command(name="leave-group",
-                            description="Hiermit verlässt du deine aktuelle Gruppe",
-                            guild_ids=[int(os.getenv('DISCORD_GUILD'))])
-    async def cmd_leave_group(self, interaction: ApplicationCommandInteraction):
-        thread_id = interaction.channel_id
-        player_id = interaction.author.id
-        if group := self.groups.get(str(thread_id)):
-            if not player_id == group['owner']:
-                if player_id in group.get('players'):
-                    self.leave_group(thread_id, player_id)
-                    await interaction.response.send_message(f"<@{player_id}> hat die Gruppe verlassen.")
-                else:
-                    await interaction.response.send_message(
-                        "Du bist garnicht Teil dieser Gruppe.", ephemeral=True)
-            else:
-                await interaction.response.send_message(
-                    "Du darfst deine Gruppe nicht im Stich lassen. Als Gruppenleiterin kannst du sie höchstens beenden, "
-                    "aber nicht verlassen.", ephemeral=True)
-        else:
-            await interaction.response.send_message("Dieses Kommando kann nur in einem Gruppenthread ausgeführt werden."
-                                                    , ephemeral=True)
-
-    @commands.slash_command(name="stats",
-                            description="Zeigt deine persönliche Statistik an.",
-                            guild_ids=[int(os.getenv('DISCORD_GUILD'))])
-    async def cmd_stats(self, interaction: ApplicationCommandInteraction):
-        embed = self.get_personal_stats_embed(interaction.author.id)
-        await interaction.response.send_message(embed=embed, ephemeral=True)
-
-    @commands.slash_command(name="start-group",
-                            description="Erstelle eine Gruppe für einen Streifzug durch die Elm Street",
-                            guild_ids=[int(os.getenv('DISCORD_GUILD'))])
-    async def cmd_start_group(self, interaction: ApplicationCommandInteraction, name: str):
-        author = interaction.author
-        category = await self.bot.fetch_channel(self.halloween_category_id)
-        channel = await self.bot.fetch_channel(self.elm_street_channel_id)
-        channel_type = None if self.bot.is_prod() else disnake.ChannelType.public_thread
-
-        player = self.get_player(author)
-
-        if interaction.channel == channel:
-            if self.can_play(player):
-                if not self.is_playing(author.id):
-                    if player["courage"] >= 50:
-                        thread = await channel.create_thread(name=name, auto_archive_duration=1440, type=channel_type)
-                        voice_channel = await category.create_voice_channel(name)
-                        await voice_channel.set_permissions(interaction.author, view_channel=True, connect=True)
-
-                        await thread.send(
-                            f"Hallo {author.mention}. Der Streifzug deiner Gruppe durch die Elm-Street findet "
-                            f"in diesem Thread statt. Sobald deine Gruppe sich zusammen gefunden hat, kannst "
-                            f"du über einen Klick auf den Start Button eure Reise starten.\n\n"
-                            f"Für das volle Gruselerlebnis könnt ihr euch während des Abenteuers gegenseitig "
-                            f"Schauermärchen in eurem Voice Channel {voice_channel.mention} erzählen.",
-                            view=self.get_start_view())
-
-                        await interaction.response.send_message(self.get_invite_message(author),
-                                                                view=self.get_join_view(thread.id))
-
-                        message = await interaction.original_message()
-                        self.groups[str(thread.id)] = {"message": message.id, "players": [author.id],
-                                                       "owner": author.id,
-                                                       "requests": [], 'stats': {'sweets': 0, 'courage': 0, 'doors': 0},
-                                                       "voice_channel": voice_channel.id}
-                        self.save()
-                    else:
-                        await interaction.response.send_message(
-                            "Du fühlst dich derzeit noch nicht mutig genug, um aus Süßigkeitenjagd zu gehen. Warte, bis deine Mutpunkte wieder mindestens 50 betragen. Den aktuellen Stand deiner Mutpunkte kannst du über /stats prüfen.",
-                            ephemeral=True)
-                else:
-                    await interaction.response.send_message(
-                        "Es tut mir leid, aber du kannst nicht an mehr als einer Jagd gleichzeitig teilnehmen. "
-                        "Beende erst das bisherige Abenteuer, bevor du dich einer neuen Gruppe anschließen kannst.",
-                        ephemeral=True)
-            else:
-                await interaction.response.send_message(
-                    "Du zitterst noch zu sehr von deiner letzten Runde. Ruh dich noch ein wenig aus bevor du weiter spielst.",
-                    ephemeral=True)
-        else:
-            await interaction.response.send_message(
-                f"Gruppen können nur in <#{self.elm_street_channel_id}> gestartet werden.",
-                ephemeral=True)
-
-    async def on_join(self, button: disnake.ui.Button, interaction: disnake.MessageInteraction, value=None):
-        player = self.get_player(interaction.author)
-
-        try:
-            if group := self.groups.get(str(value)):
-                requests = [r['player'] for r in group.get('requests')]
-                if interaction.author.id not in requests:
-                    if self.can_play(player):
-                        if not self.is_already_in_this_group(interaction.author.id, interaction.message.id):
-                            if not self.is_playing(interaction.author.id):
-                                if player["courage"] >= 50:
-                                    thread = await self.bot.fetch_channel(value)
-                                    msg = await self.bot.view_manager.confirm(thread, "Neuer Rekrut",
-                                                                              f"{interaction.author.mention} würde sich gerne der Gruppe anschließen.",
-                                                                              fields=[{'name': 'aktuelle Mutpunkte',
-                                                                                       'value': self.get_courage_message(
-                                                                                           player)}],
-                                                                              custom_prefix="rekrut",
-                                                                              callback_key="on_joined")
-                                    player.get('messages').append({'id': msg.id, 'channel': thread.id})
-                                    group.get('requests').append({'player': interaction.author.id, 'id': msg.id})
-                                    self.save()
-                                else:
-                                    await interaction.response.send_message(
-                                        "Du fühlst dich derzeit noch nicht mutig genug, um aus Süßigkeitenjagd zu gehen. Warte, bis deine Mutpunkte wieder mindestens 50 betragen. Den aktuellen Stand deiner Mutpunkte kannst du über /stats prüfen.",
-                                        ephemeral=True)
-                            else:
-                                await interaction.response.send_message(
-                                    "Es tut mir leid, aber du kannst nicht an mehr als einer Jagd gleichzeitig teilnehmen. "
-                                    "Beende erst das bisherige Abenteuer, bevor du dich einer neuen Gruppe anschließen kannst.",
-                                    ephemeral=True)
-                        else:
-                            await interaction.response.send_message(
-                                "Du bist schon Teil dieser Gruppe! Schau doch mal in eurem "
-                                "Thread vorbei.", ephemeral=True)
-                    else:
-                        await interaction.response.send_message(
-                            "Du zitterst noch zu sehr von deiner letzten Runde. Ruh dich noch ein wenig aus bevor du weiter spielst.",
-                            ephemeral=True)
-                else:
-                    await interaction.response.send_message(
-                        "Für diese Gruppe hast du dich schon beworben. Warte auf eine Entscheidung des Gruppenleiters.",
-                        ephemeral=True)
-        except Exception as e:
-            await interaction.response.send_message(
-                "Ein Fehler ist aufgetreten. Überprüfe bitte, ob du der richtigen Gruppe beitreten wolltest. "
-                "Sollte der Fehler erneut auftreten, sende mir (Boty McBotface) bitte eine Direktnachricht.",
-                ephemeral=True)
-
-        if not interaction.response.is_done():
-            await interaction.response.send_message("Dein Wunsch, der Gruppe beizutreten wurde weitergeleitet.",
-                                                    ephemeral=True)
-
-    async def on_joined(self, button: disnake.ui.Button, interaction: disnake.MessageInteraction, value=None):
-        player_id = int(get_player_from_embed(interaction.message.embeds[0]))
-        thread_id = interaction.channel_id
-        owner_id = self.groups.get(str(thread_id)).get('owner')
-
-        if interaction.author.id == owner_id:
-            if group := self.groups.get(str(interaction.channel_id)):
-                if value:
-                    if not self.is_playing(player_id):
-                        group["players"].append(player_id)
-
-                        # Request-Nachrichten aus allen Threads und aus players löschen
-                        for thread_id in self.groups:
-                            requests = self.groups.get(str(thread_id)).get('requests')
-                            for request in requests:
-                                if request['player'] == player_id:
-                                    thread = await self.bot.fetch_channel(int(thread_id))
-                                    message = await thread.fetch_message(request['id'])
-                                    player = await self.bot.fetch_user(player_id)
-                                    voice_channel = await self.bot.fetch_channel(group["voice_channel"])
-                                    await voice_channel.set_permissions(player, view_channel=True, connect=True)
-                                    await message.delete()
-                                    self.delete_message_from_player(player_id, request['id'])
-                                    requests.remove(request)
-
-                        await interaction.message.channel.send(
-                            f"<@!{player_id}> ist jetzt Teil der Crew! Herzlich willkommen.",
-                            view=self.get_leave_view())
-                        self.save()
-                else:
-                    await self.deny_join_request(group, interaction.message, player_id)
-        else:
-            await interaction.response.send_message("Nur die Gruppenerstellerin kann User annehmen oder ablehnen.",
-                                                    ephemeral=True)
-
-    async def on_start(self, button: disnake.ui.Button, interaction: disnake.MessageInteraction, value=None):
-        thread_id = interaction.channel_id
-        owner_id = self.groups.get(str(thread_id)).get('owner')
-        if interaction.author.id == owner_id:
-            if group := self.groups.get(str(interaction.channel.id)):
-
-                elm_street_channel = await self.bot.fetch_channel(self.elm_street_channel_id)
-                group_message = await elm_street_channel.fetch_message(group["message"])
-                await group_message.delete()
-                await interaction.message.edit(view=self.get_start_view(disabled=True))
-
-                if value:  # auf Start geklickt
-                    await self.deny_open_join_requests(thread_id, group)
-                    random_player = await self.bot.fetch_user(SystemRandom().choice(group.get('players')))
-                    bags = ["einen Putzeimer, der", "eine Plastiktüte von Aldi, die", "einen Einhorn-Rucksack, der",
-                            "eine Reisetasche, die", "eine Wickeltasche mit zweifelhaftem Inhalt, die",
-                            "einen Rucksack, der", "eine alte Holzkiste, die", "einen Leinensack, der",
-                            "einen Müllsack, der", "einen Jutebeutel mit verwaschener gotischer Schrift, die",
-                            "eine blaue Ikea-Tasche, die"]
-                    await interaction.response.send_message(
-                        f"```\nSeid ihr bereit? Taschenlampe am Gürtel, Schminke im Gesicht? Dann kann es losgehen!\n"
-                        f"Doch als ihr gerade in euer Abenteuer starten wollt, fällt {random_player.name} auf, dass ihr euch erst noch Behälter für die erwarteten Süßigkeiten suchen müsst. \nIhr schnappt euch also {SystemRandom().choice(bags)} gerade da ist. \nNun aber los!\n```")
-                    await self.on_story(button, interaction, "doors")
-                else:  # auf Abbrechen geklickt
-                    # voice channel löschen
-                    voice_channel_id = self.groups[str(thread_id)]["voice_channel"]
-                    voice_channel = await self.bot.fetch_channel(voice_channel_id)
-                    if len(voice_channel.members) == 0:
-                        await voice_channel.delete()
-
-                    self.groups.pop(str(thread_id))
-                    self.save()
-                    await interaction.response.send_message(f"Du hast die Runde abgebrochen. Dieser Thread wurde "
-                                                            f"archiviert und du kannst in <#{self.elm_street_channel_id}>"
-                                                            f" eine neue Runde starten.", ephemeral=True)
-                    await interaction.channel.send(f"Dieses Abenteuer ist beendet und zum Nachlesen archiviert."
-                                                   f"\nFür mehr Halloween-Spaß, schau in <#{self.elm_street_channel_id}>"
-                                                   f"vorbei")
-                    await interaction.channel.edit(archived=True)
-        else:
-            await interaction.response.send_message(
-                "Nur die Gruppenerstellerin kann die Gruppe starten lassen oder die "
-                "Tour abbrechen.",
-                ephemeral=True)
-
-    async def on_stop(self, button: disnake.ui.Button, interaction: disnake.MessageInteraction, value=None):
-        thread_id = interaction.channel_id
-
-        # Button disablen
-        await interaction.message.edit(view=self.get_stop_view(disabled=True))
-
-        # Gruppenstatistik in elm-street posten
-        stats_embed = await self.get_group_stats_embed(thread_id)
-        elm_street = await self.bot.fetch_channel(self.elm_street_channel_id)
-        await elm_street.send("", embed=stats_embed)
-
-        # jedem Spieler seine Süßigkeiten geben
-        sweets = self.groups.get(str(thread_id)).get('stats').get('sweets')
-        self.share_sweets(sweets, thread_id)
-
-        # aktuelles leaderboard in elm-street posten
-        leaderboard_embed = await self.leaderboard(all="all")
-        await elm_street.send("", embed=leaderboard_embed)
-
-        # voice channel löschen
-        voice_channel_id = self.groups[str(thread_id)]["voice_channel"]
-        voice_channel = await self.bot.fetch_channel(voice_channel_id)
-        if len(voice_channel.members) == 0:
-            await voice_channel.delete()
-
-        # Gruppe aus json löschen
-        self.groups.pop(str(thread_id))
-        self.save()
-
-        # Thread archivieren
-        await interaction.channel.send(f"Dieses Abenteuer ist beendet und zum Nachlesen archiviert."
-                                       f"\nFür mehr Halloween-Spaß, schau in <#{self.elm_street_channel_id}>"
-                                       f"vorbei")
-        await interaction.channel.edit(archived=True)
-
-    async def on_story(self, button: disnake.ui.Button, interaction: disnake.MessageInteraction, value=None):
-        thread_id = interaction.channel_id
-        group = self.groups.get(str(thread_id))
-        owner_id = group.get('owner')
-        if interaction.author.id == owner_id:
-            if value == "stop":
-                await self.on_stop(button, interaction, value)
-
-            elif not self.can_proceed_story(interaction.channel_id):
-                value = "fear"
-
-            if events := self.story.get("events"):
-                if value == "knock_on_door":
-                    group = self.groups.get(str(thread_id))
-                    group_stats = group.get('stats')
-                    group_stats['doors'] += 1
-                    self.save()
-
-                if event := events.get(value):
-                    channel = interaction.message.channel
-                    choice = self.get_choice(value, event, group)
-                    if not choice:
-                        view = self.get_story_view("fear")
-                        await channel.send("```\nAls ihr euch auf den Weg zur nächsten Tür macht, seht ihr am Horizont "
-                                           "langsam die Sonne aufgehen. Ihr betrachtet eure Beute und beschließt, "
-                                           "für dieses Jahr die Jagd zu beenden und tretet den Heimweg an.\n```",
-                                           view=view)
-                        await interaction.message.delete()
-                    else:
-                        text = choice["text"]
-                        view = self.get_story_view(choice.get("view"))
-                        sweets = calculate_sweets(choice)
-                        courage = calculate_courage(choice)
-                        text = self.apply_sweets_and_courage(text, sweets, courage, interaction.channel_id)
-                        await channel.send(f"```\n{text}\n```")
-                        if view:
-                            await channel.send("Was wollt ihr als nächstes tun?", view=view)
-                        if next := choice.get("next"):
-                            await self.on_story(button, interaction, next)
-                        else:
-                            await interaction.message.delete()
-        else:
-            await interaction.response.send_message("Nur die Gruppenleiterin kann die Gruppe steuern.",
-                                                    ephemeral=True)
-
-    async def on_leave(self, button: disnake.ui.Button, interaction: disnake.MessageInteraction, value=None):
-        thread_id = interaction.channel_id
-        player_id = interaction.author.id
-        msg_player = interaction.message.mentions[0]
-
-        if msg_player.id == player_id:
-            self.leave_group(thread_id, player_id)
-            await interaction.response.send_message(f"<@{player_id}> hat die Gruppe verlassen.")
-            await interaction.message.edit(view=self.get_leave_view(disabled=True))
-        else:
-            await interaction.response.send_message(
-                f"Nur <@{player_id}> darf diesen Button bedienen. Wenn du die Gruppe "
-                f"verlassen willst, versuche es mit `/leave-group`", ephemeral=True)
-
-    def get_choice(self, key, event, group):
-        if key == "doors":
-            doors_visited = get_doors_visited(group)
-            r = list(range(0, len(event) - 1))
-            for door_visited in doors_visited:
-                r.remove(door_visited)
-
-            if len(r) == 0:
-                return None
-
-            i = SystemRandom().choice(r)
-            doors_visited.append(i)
-            self.save()
-            return event[i]
-        else:
-            return SystemRandom().choice(event)
-
-    def get_story_view(self, view_name: str):
-        if views := self.story.get("views"):
-            if buttons := views.get(view_name):
-                return self.bot.view_manager.view(deepcopy(buttons), "on_story")
-
-        return None
-
-    def get_join_view(self, group_id: int):
-        buttons = [
-            {"label": "Join", "style": ButtonStyle.green, "value": group_id, "custom_id": "elm_street:join"}
-        ]
-        return self.bot.view_manager.view(buttons, "on_join")
-
-    def get_start_view(self, disabled=False):
-        buttons = [
-            {"label": "Start", "style": ButtonStyle.green, "value": True, "custom_id": "elm_street:start",
-             "disabled": disabled},
-            {"label": "Abbrechen", "style": ButtonStyle.gray, "value": False, "custom_id": "elm_street:cancel",
-             "disabled": disabled}
-        ]
-        return self.bot.view_manager.view(buttons, "on_start")
-
-    def get_stop_view(self, disabled=False):
-        buttons = [
-            {"label": "Beendet", "style": ButtonStyle.red, "custom_id": "elm_street:stop", "disabled": disabled}
-        ]
-        return self.bot.view_manager.view(buttons, "on_stop")
-
-    def get_leave_view(self, disabled=False):
-        buttons = [
-            {"label": "Verlassen", "style": ButtonStyle.gray, "custom_id": "elm_street:leave", "disabled": disabled}
-        ]
-        return self.bot.view_manager.view(buttons, "on_leave")
-
-    def is_playing(self, user_id: int = None):
-        for group in self.groups.values():
-            if players := group.get("players"):
-                if user_id in players:
-                    return True
-        return False
-
-    def is_already_in_this_group(self, user_id, message_id):
-        for group in self.groups.values():
-            if message_id == group.get('message'):
-                if user_id in group.get('players'):
-                    return True
-        return False
-
-    def can_proceed_story(self, thread_id):
-        group = self.groups.get(str(thread_id))
-
-        player_ids = group.get("players")
-        num_players = 0
-        group_courage = 0
-
-        for player_id in player_ids:
-            player = self.players.get(str(player_id))
-            num_players += 1
-            group_courage += player["courage"]
-        average_courage = group_courage / num_players
-
-        return self.min_group_courage < average_courage
-
-    def can_play(self, player):
-        if player.get('courage') < self.min_courage:
-            return False
-        return True
-
-    def get_player(self, user: Union[disnake.User, disnake.Member]):
-        if player := self.players.get(str(user.id)):
-            return player
-        else:
-            player = {"courage": self.max_courage, "sweets": 0, "messages": []}
-            self.players[str(user.id)] = player
-            self.save()
-            return player
-
-    def get_courage_message(self, player):
-        courage = player.get('courage')
-        message = f"{courage}"
-        return message
-
-    def delete_message_from_player(self, player_id, message_id):
-        if player := self.players.get(str(player_id)):
-            messages = player.get('messages')
-            for msg in messages:
-                if msg['id'] == message_id:
-                    messages.remove(msg)
-                    self.save()
-
-    async def leaderboard(self, all: ShowOption = 10, interaction: ApplicationCommandInteraction = None):
-        places = scores = "\u200b"
-        place = 0
-        max = 0 if all == "all" else 10
-        ready = False
-        embed = disnake.Embed(title="Elm-Street Leaderboard",
-                              description="Wie süß bist du wirklich??\n" +
-                                          (":jack_o_lantern: " * 8))
-        last_score = -1
-        for player_id, player_data in sorted(self.players.items(), key=lambda item: item[1]["sweets"], reverse=True):
-            value = player_data["sweets"]
-            # embed.set_thumbnail(
-            #     url="https://www.planet-wissen.de/kultur/religion/ostern/tempxostereiergjpg100~_v-gseagaleriexl.jpg")
-            elm_street_channel = await self.bot.fetch_channel(self.elm_street_channel_id)
-            try:
-                if last_score != value:
-                    place += 1
-                last_score = value
-                if 0 < max < place:
-                    if ready:
-                        break
-                    # elif str(ctx.author.id) != player_id:
-                    #    continue
-                places += f"{place}: <@!{player_id}>\n"
-                scores += f"{value:,}\n".replace(",", ".")
-
-                # if str(ctx.author.id) == player_id:
-                #    ready = True
-            except:
-                pass
-
-        embed.add_field(name=f"Sammlerin", value=places)
-        embed.add_field(name=f"Süßigkeiten", value=scores)
-        return embed
-        # await elm_street_channel.send("", embed=embed)
-
-    async def get_group_stats_embed(self, thread_id):
-        thread = await self.bot.fetch_channel(thread_id)
-        players = self.groups.get(str(thread_id)).get('players')
-        stats = self.groups.get(str(thread_id)).get('stats')
-
-        players_value = ', '.join([f'<@{int(player)}>' for player in players])
-        doors_value = stats.get('doors')
-        sweets_value = stats.get('sweets')
-        courage_value = stats.get('courage')
-
-        embed = disnake.Embed(title=f'Erfolge der Gruppe "{thread.name}"')
-        embed.add_field(name='Mitspieler', value=players_value, inline=False)
-        embed.add_field(name="Besuchte Türen", value=doors_value)
-        embed.add_field(name="Gesammelte Süßigkeiten", value=sweets_value)
-        embed.add_field(name="Verlorene Mutpunkte", value=courage_value)
-
-        return embed
-
-    def get_personal_stats_embed(self, player_id):
-        player = self.players.get(str(player_id))
-        embed = disnake.Embed(title="Deine persönlichen Erfolge")
-        embed.add_field(name="Süßigkeiten", value=player['sweets'])
-        embed.add_field(name="Mutpunkte", value=player['courage'])
-        return embed
-
-    def get_invite_message(self, author):
-        texts = [f"Du bist mitten in einer Großstadt gelandet.\n"
-                 f"Der leise Wind weht Papier die Straße lang. "
-                 f"Ansonsten hörst du nur in der Ferne das Geräusch vorbeifahrender Autos.\n"
-                 f"Da, was war das?\n"
-                 f"Hat sich da nicht etwas bewegt?\n"
-                 f"Ein Schatten an der Mauer?\n"
-                 f"Ein Geräusch wie von Krallen auf Asphalt.\n"
-                 f"Du drehst dich im Kreis.\n"
-                 f"Ein leises Lachen in deinem Rücken.\n"
-                 f"Und da, gerade außerhalb deines Sichtfeldes eine Tür die sich quietschend öffnet.\n"
-                 f"Eine laute Stimme ruft fragend: \"Ich zieh los um die Häuser, wäre ja gelacht wenn nur Kinder heute "
-                 f"abend Süßkram bekommen. Wer ist mit dabei?\"\n"
-                 f"Du drehst dich zur Tür und siehst {author.mention}s entschlossenen Gesichtsausdruck.",
-                 f"Eine Einladung über die Sozialen Netzwerke hat dich Aufmerksam werden lassen. \n"
-                 f"Darin war von einer großen Halloween Party die Rede, "
-                 f"als Treffpunkt war ein Park in der Innenstadt angegeben.\n"
-                 f"Schon beim eintreffen merkst du, dass es keine angemeldete Party ist: "
-                 f"überall ist Blaulicht und du siehst einige Polizeiwagen.\n"
-                 f"Du entscheidest dich die Pläne für den Abend noch mal zu überdenken. "
-                 f"Aber was tun? \n"
-                 f"Deine Verkleidung ist zu aufwendig um schon wieder nach Hause zu gehen.\n"
-                 f"In deiner Nähe stehen noch andere Menschen in Verkleidung die nicht wissen was sie mit dem angebrochenen Abend anfangen sollen. "
-                 f"Da fragt {author.mention} laut in die Runde: \"Wer hat Lust um die Häuser zu ziehen und gemeinsam Süßigkeiten zu sammeln?\""
-                 ]
-
-        return SystemRandom().choice(texts)
-
-    def get_group_by_voice_id(self, voice_id):
-        for group in self.groups.values():
-            if vc := group.get("voice_channel"):
-                if vc == voice_id:
-                    return group
-
-        return None
-
-    def apply_sweets_and_courage(self, text, sweets, courage, thread_id):
-        group = self.groups.get(str(thread_id))
-        player_ids = group.get("players")
-        group_stats = group.get('stats')
-
-        if sweets:
-            if sweets > 0:
-                text += f"\n\nIhr erhaltet jeweils {sweets} Süßigkeiten."
-            if sweets == 0:
-                text += f"\n\nIhr habt genau so viele Süßigkeiten wie vorher."
-            if sweets < 0:
-                text += f"\n\nIhr verliert jeweils {sweets} Süßigkeiten."
-            group_stats['sweets'] += sweets
-        if courage:
-            if courage > 0:
-                text += f"\n\nIhr verliert jeweils {courage} Mutpunkte."
-            for player_id in player_ids:
-                player = self.players.get(str(player_id))
-                player["courage"] -= courage
-            group_stats['courage'] += courage
-
-        self.save()
-        # TODO Was passiert wenn die courage eines Players zu weit sinkt?
-        return text
-
-    def share_sweets(self, sweets, thread_id):
-        group = self.groups.get(str(thread_id))
-        player_ids = group.get("players")
-        for player_id in player_ids:
-            player = self.players.get(str(player_id))
-            player["sweets"] += sweets
-
-    def leave_group(self, thread_id, player_id):
-        group = self.groups.get(str(thread_id))
-        group_players = group.get('players')
-        player = self.players.get(str(player_id))
-
-        # Spieler auszahlen
-        group_stats = group.get('stats')
-        player["sweets"] += group_stats['sweets']
-
-        # Spieler aus Gruppe löschen
-        group_players.remove(player_id)
-        self.save()
-
-    async def deny_join_request(self, group, message, player_id):
-        user = self.bot.get_user(player_id)
-        outfit = ["Piraten", "Einhörner", "Geister", "Katzen", "Weihnachtswichtel"]
-        dresscode = ["Werwölfe", "Vampire", "Alice im Wunderland", "Hexen", "Zombies"]
-        texts = [
-            "Wir wollen um die Häuser ziehen und Kinder erschrecken. Du schaust aus, als würdest du den "
-            "Kindern lieber unsere Süßigkeiten geben. Versuch es woanders.",
-            f"Ich glaub du hast dich verlaufen, in dieser Gruppe können wir keine "
-            f"{SystemRandom().choice(outfit)} gebrauchen. Unser Dresscode ist: {SystemRandom().choice(dresscode)}."]
-        await send_dm(user, SystemRandom().choice(texts))
-        group["requests"].remove({'player': player_id, 'id': message.id})
-        self.save()
-        # Request Nachricht aus diesem Thread und aus players löschen
-        self.delete_message_from_player(player_id, message.id)
-        await message.delete()
-
-    async def deny_open_join_requests(self, thread_id, group):
-        thread = await self.bot.fetch_channel(thread_id)
-
-        if requests := group.get("requests"):
-            for request in requests:
-                message = await thread.fetch_message(request["id"])
-                await self.deny_join_request(group, message, request["player"])
-
-    @tasks.loop(minutes=5)
-    async def increase_courage(self):
-        actual_playing = []
-        for p in (self.groups.get(group).get('players') for group in self.groups):
-            actual_playing += p
-        # pro Spieler: courage erhöhen
-        for player in self.players:
-            # nur wenn Spieler nicht gerade spielt
-            if int(player) not in actual_playing:
-                player = self.players.get(player)
-                courage = player.get('courage')
-                if courage < self.max_courage:
-                    courage += self.inc_courage_step
-                    player['courage'] = courage if courage < self.max_courage else self.max_courage
-                    self.save()
-
-                    # pro Nachricht: Nachricht erneuern
-                    if messages := player.get('messages'):
-                        for message in messages:
-                            channel = await self.bot.fetch_channel(message['channel'])
-                            msg = await channel.fetch_message(message['id'])
-                            embed = msg.embeds[0]
-                            embed.clear_fields()
-                            embed.add_field(name='aktuelle Mutpunkte', value=self.get_courage_message(player))
-                            await msg.edit(embed=embed)
-
-    @increase_courage.before_loop
-    async def before_increase(self):
-        await sleep(10)
-
-    @commands.Cog.listener(name="on_voice_state_update")
-    async def voice_state_changed(self, member, before, after):
-        if not after.channel:
-            voice_channel_left = before.channel
-            if len(voice_channel_left.members) == 0 and \
-                    voice_channel_left.category_id == self.halloween_category_id and \
-                    not self.get_group_by_voice_id(voice_channel_left.id):
-                await voice_channel_left.delete()
diff --git a/cogs/github.py b/cogs/github.py
deleted file mode 100644
index 124fd15128b88bbb194c49e5db6bff9c1f3086d6..0000000000000000000000000000000000000000
--- a/cogs/github.py
+++ /dev/null
@@ -1,99 +0,0 @@
-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
deleted file mode 100644
index 3d652ae27468c5a7a3b1ec95a8e3fac41da42c1e..0000000000000000000000000000000000000000
--- a/cogs/help.py
+++ /dev/null
@@ -1,241 +0,0 @@
-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/links.py b/cogs/links.py
deleted file mode 100644
index 7e469f7c0cea3a7a3580132390b2a336820504ba..0000000000000000000000000000000000000000
--- a/cogs/links.py
+++ /dev/null
@@ -1,210 +0,0 @@
-import json
-
-import disnake
-from disnake.ext import commands
-from cogs.help import help, handle_error, help_category
-
-
-@help_category("links", "Links", "Feature zum Verwalten von Links innerhalb eines Channels.")
-class Links(commands.Cog):
-    def __init__(self, bot):
-        self.bot = bot
-        self.links = {}
-        self.links_file = "data/links.json"
-        self.load_links()
-
-    def load_links(self):
-        links_file = open(self.links_file, 'r')
-        self.links = json.load(links_file)
-
-    def save_links(self):
-        links_file = open(self.links_file, 'w')
-        json.dump(self.links, links_file)
-
-    @help(
-        category="links",
-        brief="Zeigt die Links an, die in diesem Channel (evtl. unter Berücksichtigung eines Themas) hinterlegt sind.",
-        parameters={
-            "topic": "*(optional)* Schränkt die angezeigten Links auf das übergebene Thema ein. "
-        }
-    )
-    @commands.group(name="links", pass_context=True, invoke_without_command=True)
-    async def cmd_links(self, ctx, topic=None):
-        if channel_links := self.links.get(str(ctx.channel.id)):
-            embed = disnake.Embed(title=f"Folgende Links sind in diesem Channel hinterlegt:\n")
-            if topic:
-                topic = topic.lower()
-                if topic_links := channel_links.get(topic):
-                    value = f""
-                    for title, link in topic_links.items():
-                        value += f"- [{title}]({link})\n"
-                    embed.add_field(name=topic.capitalize(), value=value, inline=False)
-                    await ctx.send(embed=embed)
-                else:
-                    await ctx.send(
-                        f" Für das Thema `{topic}` sind in diesem Channel keine Links hinterlegt. Versuch es noch mal "
-                        f"mit einem anderen Thema, oder lass dir mit `!links` alle Links in diesem Channel ausgeben")
-            else:
-                for topic, links in channel_links.items():
-                    value = f""
-                    for title, link in links.items():
-                        value += f"- [{title}]({link})\n"
-                    embed.add_field(name=topic.capitalize(), value=value, inline=False)
-                await ctx.send(embed=embed)
-        else:
-            await ctx.send("Für diesen Channel sind noch keine Links hinterlegt.")
-
-    @help(
-        category="links",
-        syntax="!links add <topic> <link> <title...>",
-        brief="Fügt einen Link zum Channel hinzu.",
-        parameters={
-            "topic": "Name des Themas, dem der Link zugeordnet werden soll. ",
-            "link": "die URL, die aufgerufen werden soll (z. B. https://www.fernuni-hagen.de). ",
-            "title...": "Titel, der für diesen Link angezeigt werden soll (darf Leerzeichen enthalten). "
-        },
-        description="Die mit !links add zu einem Kanal hinzugefügten Links können über das Kommando !links in diesem "
-                    "Kanal wieder abgerufen werden.",
-        command_group="links"
-    )
-    @cmd_links.command(name="add")
-    async def cmd_add_link(self, ctx, topic, link, *title):
-        topic = topic.lower()
-        if not (channel_links := self.links.get(str(ctx.channel.id))):
-            self.links[str(ctx.channel.id)] = {}
-            channel_links = self.links.get(str(ctx.channel.id))
-
-        if not (topic_links := channel_links.get(topic)):
-            channel_links[topic] = {}
-            topic_links = channel_links.get(topic)
-
-        self.add_link(topic_links, link, " ".join(title))
-        self.save_links()
-
-    def add_link(self, topic_links, link, title):
-        if topic_links.get(title):
-            self.add_link(topic_links, link, title + str(1))
-        else:
-            topic_links[title] = link
-
-    @help(
-        category="links",
-        syntax="!links remove-link <topic> <title...>",
-        brief="Löscht einen Link aus dem Channel.",
-        parameters={
-            "topic": "Name des Themas, aus dem der Link entfernt werden soll. ",
-            "title...": "Titel des Links, der entfernt werden soll. "
-        },
-        description="Mit !links remove-link kann ein fehlerhafter oder veralteter Link aus der Linkliste des Channels "
-                    "entfernt werden.",
-        command_group="links"
-    )
-    @cmd_links.command(name="remove-link", aliases=['rl'])
-    async def cmd_remove_link(self, ctx, topic, *title):
-        topic = topic.lower()
-        title = " ".join(title)
-
-        if channel_links := self.links.get(str(ctx.channel.id)):
-            if topic_links := channel_links.get(topic):
-                if title in topic_links:
-                    topic_links.pop(title)
-                    if not topic_links:
-                        channel_links.pop(topic)
-                else:
-                    await ctx.channel.send('Ich konnte den Link leider nicht finden.')
-            else:
-                await ctx.channel.send('Ich konnte das Thema leider nicht finden.')
-        else:
-            await ctx.channel.send('Für diesen Channel sind keine Links hinterlegt.')
-
-        self.save_links()
-
-    @help(
-        category="links",
-        syntax="!links remove-topic <topic>",
-        brief="Löscht eine komplette Themenkategorie aus dem Channel.",
-        parameters={
-            "topic": "Name des Themas, das entfernt werden soll. ",
-        },
-        description="Mit !links remove-topic kann ein Thema aus der Linkliste des Channels entfernt werden.",
-        command_group="links"
-    )
-    @cmd_links.command(name="remove-topic", aliases=['rt'])
-    async def cmd_remove_topic(self, ctx, topic):
-        topic = topic.lower()
-
-        if channel_links := self.links.get(str(ctx.channel.id)):
-            if channel_links.get(topic):
-                channel_links.pop(topic)
-            else:
-                await ctx.channel.send('Ich konnte das Thema leider nicht finden.')
-        else:
-            await ctx.channel.send('Für diesen Channel sind keine Links hinterlegt.')
-
-        self.save_links()
-
-
-    @help(
-        category="links",
-        syntax="!links edit-link <topic> <title> <new_title> <new_topic?> <new_link?>",
-        brief="Bearbeitet einen Link.",
-        parameters={
-            "topic": "Name des Themas, aus dem der zu bearbeitende Link stammt. ",
-            "title": "Titel des Links, der bearbeitet werden soll. ",
-            "new_title": "Neuer Titel für den geänderten Link. ",
-            "new_topic": "*(optional)* Neues Thema für den geänderten Link. ",
-            "new_link": "*(optional)* Der neue Link. "
-        },
-        description="Mit !links edit-link kann ein fehlerhafter oder veralteter Link bearbeitet werden.",
-        command_group="links"
-    )
-    @cmd_links.command(name="edit-link", aliases=["el"])
-    async def cmd_edit_link(self, ctx, topic, title, new_title, new_topic=None, new_link=None):
-        topic = topic.lower()
-
-        if not new_topic:
-            new_topic = topic
-
-        if not new_link:
-            if channel_links := self.links.get(str(ctx.channel.id)):
-                if topic_links := channel_links.get(topic):
-                    if topic_links.get(title):
-                        new_link = topic_links.get(title)
-                    else:
-                        await ctx.channel.send('Ich konnte den Link leider nicht finden.')
-                else:
-                    await ctx.channel.send('Ich konnte das Thema leider nicht finden.')
-            else:
-                await ctx.channel.send('Für diesen Channel sind keine Links hinterlegt.')
-
-        await self.cmd_remove_link(ctx, topic, title)
-        await self.cmd_add_link(ctx, new_topic, new_link, new_title)
-
-    @help(
-        category="links",
-        syntax="!links edit-topic <topic> <new_topic>",
-        brief="Bearbeitet den Namen eines Themas.",
-        parameters={
-            "topic": "Name des Themas, das bearbeitet werden soll. ",
-            "new_topic": "Neuer Name des Themas. "
-        },
-        description="Mit !links edit-topic kann der Name eines Themas geändert werden.",
-        command_group="links"
-    )
-    @cmd_links.command(name="edit-topic", aliases=["et"])
-    async def cmd_edit_topic(self, ctx, topic, new_topic):
-        topic = topic.lower()
-        new_topic = new_topic.lower()
-        if channel_links := self.links.get(str(ctx.channel.id)):
-            if topic_links := channel_links.get(topic):
-                channel_links[new_topic] = topic_links
-                await self.cmd_remove_topic(ctx, topic)
-            else:
-                await ctx.channel.send('Ich konnte das Thema leider nicht finden.')
-        else:
-            await ctx.channel.send('Für diesen Channel sind keine Links hinterlegt.')
-
-        self.save_links()
-
-    async def cog_command_error(self, ctx, error):
-        await handle_error(ctx, error)
diff --git a/cogs/polls.py b/cogs/polls.py
deleted file mode 100644
index 90c0a740ce362890206b1c6efcb03d3162e34489..0000000000000000000000000000000000000000
--- a/cogs/polls.py
+++ /dev/null
@@ -1,97 +0,0 @@
-import os
-
-from disnake.ext import commands
-
-import utils
-from cogs.components.poll.poll import Poll
-from cogs.help import help, handle_error, help_category
-
-
-@help_category("poll", "Umfragen", "Erstelle eine Umfrage in einem Kanal oder schlage eine Server-Umfrage vor.",
-               "Umfragen erstellen oder bearbeiten.")
-class Polls(commands.Cog):
-    def __init__(self, bot):
-        self.bot = bot
-        self.poll_sugg_channel = int(os.getenv("DISCORD_POLL_SUGG_CHANNEL"))
-
-    @help(
-        category="poll",
-        syntax="!poll <question> <answers...>",
-        brief="Erstellt eine Umfrage im aktuellen Kanal.",
-        parameters={
-            "question": "Die Frage, die gestellt werden soll (in Anführungszeichen).",
-            "answers...": "Durch Leerzeichen getrennte Antwortmöglichkeiten (die einzelnen Antworten in Anführungszeichen einschließen)."
-        },
-        example="!poll \"Wie ist das Wetter?\" \"echt gut\" \"weniger gut\" \"Boar nee, nicht schon wieder Regen\""
-    )
-    @commands.group(name="poll", pass_context=True, invoke_without_command=True)
-    async def cmd_poll(self, ctx, question, *answers):
-        """ Create poll """
-
-        await Poll(self.bot, question, list(answers), ctx.author.id).send_poll(ctx)
-
-    @help(
-        category="poll",
-        syntax="!poll suggest <question> <answers...>",
-        brief="Schlägt eine Umfrage für den Umfrage-Kanal vor.",
-        parameters={
-            "question": "Die Frage die gestellt werden soll (in Anführungszeichen).",
-            "answers...": "Durch Leerzeichen getrennte Antwortmöglichkeiten (die einzelnen Antworten in Anführungszeichen einschließen)."
-        },
-        example="!poll suggest \"Wie ist das Wetter?\" \"echt gut\" \"weniger gut\" \"Boar nee, nicht schon wieder Regen\""
-    )
-    @cmd_poll.command(name="suggest")
-    async def cmd_add_poll(self, ctx, question, *answers):
-        channel = await self.bot.fetch_channel(self.poll_sugg_channel)
-        msg = f"<@!{ctx.author.id}> hat folgende Umfrage vorgeschlagen:\nFrage:{question}\n\nAntwortoptionen:\n"
-        poll = f"!poll \"{question}\""
-
-        for answer in answers:
-            msg += f"{answer}\n"
-            poll += f" \"{answer}\""
-
-        await channel.send(f"{msg}\n{poll}")
-
-    @help(
-        category="poll",
-        brief="Bearbeitet eine bereits vorhandene Umfrage.",
-        syntax="!poll edit <message_id> <question> <answers...>",
-        parameters={
-            "message_id": "die Message-ID ist der Nachricht mit einem Rechtsklick auf die Umfrage zu entnehmen (Entwicklermodus in Discord müssen aktiv sein).",
-            "question": "Die Frage, die gestellt werden soll (in Anführungszeichen).",
-            "answers...": "Durch Leerzeichen getrennte Antwortmöglichkeiten (die einzelnen Antworten in Anführungszeichen einschließen).",
-        },
-        example="!poll edit 838752355595059230 \"Wie ist das Wetter?\" \"echt gut\" \"weniger gut\" \"Boar nee, nicht schon wieder Regen\"",
-        mod=True
-    )
-    @cmd_poll.command(name="edit")
-    @commands.check(utils.is_mod)
-    async def cmd_edit_poll(self, ctx, message_id, question, *answers):
-        message = await ctx.fetch_message(message_id)
-        if message:
-            if message.embeds[0].title == "Umfrage":
-                old_poll = Poll(self.bot, message=message)
-                new_poll = Poll(self.bot, question=question, answers=list(answers), author=old_poll.author)
-                await new_poll.send_poll(ctx.channel, message=message)
-        else:
-            ctx.send("Fehler! Umfrage nicht gefunden!")
-        pass
-
-    @commands.Cog.listener()
-    async def on_raw_reaction_add(self, payload):
-        if payload.user_id == self.bot.user.id:
-            return
-
-        if payload.emoji.name in ["🗑️", "🛑"]:
-            channel = await self.bot.fetch_channel(payload.channel_id)
-            message = await channel.fetch_message(payload.message_id)
-            if len(message.embeds) > 0 and message.embeds[0].title == "Umfrage":
-                poll = Poll(self.bot, message=message)
-                if str(payload.user_id) == poll.author:
-                    if payload.emoji.name == "🗑️":
-                        await poll.delete_poll()
-                    else:
-                        await poll.close_poll()
-
-    async def cog_command_error(self, ctx, error):
-        await handle_error(ctx, error)
diff --git a/cogs/roles.py b/cogs/roles.py
deleted file mode 100644
index 8038f21e4564f4bc326607b4adc27303d4e140de..0000000000000000000000000000000000000000
--- a/cogs/roles.py
+++ /dev/null
@@ -1,124 +0,0 @@
-import json
-import os
-
-import disnake
-import emoji
-from disnake.ext import commands
-
-
-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", "0"))
-        self.assignable_roles = {}
-        self.load_roles()
-        self.register_views()
-
-    def load_roles(self):
-        """ Loads all assignable roles from ROLES_FILE """
-
-        roles_file = open(self.roles_file, mode='r')
-        self.assignable_roles = json.load(roles_file)
-
-    def register_views(self):
-        """ Register view for each category at view manager """
-
-        for role_category, roles in self.assignable_roles.items():
-            prefix = f"assign_{role_category}"
-            self.bot.view_manager.register(prefix, self.on_button_clicked)
-
-    def get_stat_roles(self):
-        """ Get all roles that should be part of the stats Command """
-
-        stat_roles = []
-        for category in self.assignable_roles.values():
-            if category["in_stats"]:
-                for role in category["roles"].values():
-                    stat_roles.append(role["name"])
-
-        return stat_roles
-
-    async def on_button_clicked(self, button: disnake.ui.Button, interaction: disnake.MessageInteraction, value=None):
-        """
-        Add or Remove Roles, when Button is clicked. Role gets added, if the user clicking the button doesn't have
-        the role already assigned, and removed, if the role is already assigned
-        """
-
-        guild_roles = {str(role.id): role for role in interaction.guild.roles}
-        role = guild_roles.get(value)
-
-        if role in interaction.author.roles:
-            await interaction.author.remove_roles(role)
-            await interaction.send(f"Rolle \"{role.name}\" erfolgreich entfernt", ephemeral=True)
-        else:
-            await interaction.author.add_roles(role)
-            await interaction.send(f"Rolle \"{role.name}\" erfolgreich hinzugefügt", ephemeral=True)
-
-    @commands.slash_command(name="update-roles", description="Update Self-Assignable Roles")
-    @commands.default_member_permissions(moderate_members=True)
-    async def cmd_update_roles(self, interaction: disnake.ApplicationCommandInteraction):
-        """ Update all role assignment messages in role assignment channel """
-        await interaction.response.defer(ephemeral=True)
-
-        channel = await interaction.guild.fetch_channel(self.channel_id)
-        await channel.purge()
-        for role_category, roles in self.assignable_roles.items():
-            prefix = f"assign_{role_category}"
-            fields = []
-            buttons = []
-            value = f""
-            guild_roles = {role.name: role for role in interaction.guild.roles}
-
-            for key, role in roles.get("roles").items():
-                role_emoji = role.get('emoji') if role.get(
-                    'emoji') in emoji.UNICODE_EMOJI_ALIAS_ENGLISH else f"<{role.get('emoji')}>"
-                value += f"{role_emoji} : {role.get('name')}\n"
-                buttons.append({"emoji": role_emoji, "custom_id": f"{prefix}_{key}",
-                                "value": f"{str(guild_roles.get(role.get('name')).id)}"})
-
-            if roles.get("list_roles"):
-                fields.append({"name": "Rollen", "value": value, "inline": False})
-
-            await self.bot.view_manager.dialog(
-                channel=channel,
-                title=f"Vergabe von {roles.get('name')}",
-                description="Durch klicken auf den entsprechenden Button kannst du dir die damit "
-                            "assoziierte Rolle zuweisen, bzw. entfernen.",
-                message="",
-                fields=fields,
-                callback_key=prefix,
-                buttons=buttons
-            )
-
-        await interaction.edit_original_message("Rollen erfolgreich aktualisiert.")
-
-    @commands.slash_command(name="stats", description="Rollen Statistik abrufen")
-    async def cmd_stats(self, interaction: disnake.ApplicationCommandInteraction, show: bool = False):
-        """
-        Send role statistics into chat, by default as ephemeral
-
-        Parameters
-        ----------
-        show: Sichtbar für alle?
-        """
-
-        await interaction.response.defer(ephemeral=not show)
-
-        guild = interaction.guild
-        members = await guild.fetch_members().flatten()
-        guild_roles = {role.name: role for role in interaction.guild.roles}
-        stat_roles = self.get_stat_roles()
-        embed = disnake.Embed(title="Statistiken",
-                              description=f'Wir haben aktuell {len(members)} Mitglieder auf diesem Server, '
-                                          f'verteilt auf folgende Rollen:')
-
-        for role_name in stat_roles:
-            role = guild_roles[role_name]
-            role_members = role.members
-            num_members = len(role_members)
-            if num_members > 0:
-                embed.add_field(name=role.name,
-                                value=f'{num_members} {"Mitglieder" if num_members > 1 else "Mitglied"}', inline=False)
-
-        await interaction.edit_original_message(embed=embed)
diff --git a/cogs/text_commands.py b/cogs/text_commands.py
deleted file mode 100644
index c09692c7ec1a11df5d70a8efb6e6a9f800f78050..0000000000000000000000000000000000000000
--- a/cogs/text_commands.py
+++ /dev/null
@@ -1,303 +0,0 @@
-import json
-import os
-import random
-import re
-
-import disnake
-from disnake.ext import commands
-
-import utils
-from cogs.help import text_command_help, help, handle_error, remove_help_for, help_category
-
-
-@help_category("textcommands", "Text-Kommandos", "", "Alle Werkzeuge zum Anlegen und Verwalten von Textkommandos.")
-class TextCommands(commands.Cog):
-    def __init__(self, bot):
-        self.bot = bot
-        self.text_commands = {}
-        self.cmd_file = os.getenv("DISCORD_TEXT_COMMANDS_FILE")
-        self.mod_channel_id = int(os.getenv("DISCORD_SUPPORT_CHANNEL"))
-        self.load_text_commands()
-
-    def load_text_commands(self):
-        """ Loads all appointments from APPOINTMENTS_FILE """
-
-        text_commands_file = open(self.cmd_file, mode='r')
-        self.text_commands = json.load(text_commands_file)
-        for cmd in self.text_commands:
-            help_for_cmd = self.text_commands[cmd].get('help')
-
-            if not help_for_cmd:
-                continue
-
-            brief = help_for_cmd.get('brief')
-            category = help_for_cmd.get('category')
-            if not brief:
-                text_command_help(cmd)
-                continue
-
-            text_command_help(cmd, brief=brief, category=category)
-
-    def save_text_commands(self):
-        text_commands_file = open(self.cmd_file, mode='w')
-        json.dump(self.text_commands, text_commands_file)
-
-    @commands.group(name="commands")
-    async def cmd_commands(self, ctx):
-        if not ctx.invoked_subcommand:
-            await ctx.send("Fehlerhafte Nutzung von `!commands`. "
-                           "Bitte benutze `!help` um herauszufinden, wie dieses Kommando benutzt wird.")
-
-    @help(
-        category="textcommands",
-        brief="Listet alle verfügbaren Text-Commands auf oder die Texte, die zu einem Text-Command hinterlegt sind.",
-        syntax="!commands list <cmd?>",
-        example="!commands list !motivation",
-        description="Gibt bei Angabe eines Kommandos (optionaler Parameter cmd) die Texte, die für dieses Kommandu hinterlegt sind.  ",
-        parameters={
-            "cmd": "*(optional)* Name des Kommandos, dessen Texte ausgegeben werden sollen."
-        }
-    )
-    @cmd_commands.command(name="list")
-    async def cmd_commands_list(self, ctx, cmd=None):
-        await self.list_commands(ctx, cmd)
-
-    @help(
-        category="textcommands",
-        brief="Schlägt ein Text-Kommando oder einen Text für ein bestehendes Text-Kommando vor.",
-        syntax="!commands add <cmd> <text> <help_message?> <category?>",
-        example="!command add !newcommand \"immer wenn newcommand aufgerufen wird wird das hier ausgegeben\" \"Hilfetext zu diesem Kommando\"",
-        description="Ein Text-Kommando ist ein Kommando welches über !<name des textkommandos> aufgerufen werden kann und dann zufällig einen der hinterlegten Texte ausgibt.",
-        parameters={
-            "cmd": "Name des anzulegenden Kommandos (z. B. !horoskop). ",
-            "text": "in Anführungszeichen eingeschlossene Textnachricht, die ausgegeben werden soll, wenn das Kommando aufgerufen wird (z. B. \"Wassermann: Findet diese Woche wahrscheinlich seinen Dreizack wieder.\").",
-            "help_message": "*(optional)* Die Hilfenachricht, die bei `!help` für dieses Kommando erscheinen soll (in Anführungszeichen). ",
-            "category": "*(optional)* gibt die Kategorie an in der das Kommando angezeigt werden soll. "
-        }
-    )
-    @cmd_commands.command(name="add")
-    async def cmd_commands_add(self, ctx, cmd, text, help_message=None, category=None):
-        if utils.is_mod(ctx):
-            await self.add_command(cmd, text, help_message=help_message, category=category)
-        else:
-            await self.suggest_command(ctx, cmd, text, help_message=help_message, category=category)
-
-    @help(
-        category="textcommands",
-        brief="Ändert den Text eines Text-Kommandos.",
-        syntax="!commands edit <cmd> <id> <text>",
-        example="!command edit !command 1 \"Neuer Text\"",
-        description="Ändert den Text eines Text-Kommandos an angegebenem Index.",
-        parameters={
-            "cmd": "Name des anzulegenden Kommandos (z. B. !horoskop). ",
-            "id": "Index des zu ändernden Texts.",
-            "text": "in Anführungszeichen eingeschlossene Textnachricht, die ausgegeben werden soll, wenn das Kommando aufgerufen wird (z. B. \"Wassermann: Findet diese Woche wahrscheinlich seinen Dreizack wieder.\").",
-        },
-        mod=True
-    )
-    @cmd_commands.command(name="edit")
-    @commands.check(utils.is_mod)
-    async def cmd_command_edit(self, ctx, cmd, id, text):
-        texts = self.text_commands.get(cmd).get('data')
-
-        if texts:
-            i = int(id)
-            if 0 <= i < len(texts):
-                texts[i] = text
-                await ctx.send(f"Text {i} für Command {cmd} wurde erfolgreich geändert")
-                self.save_text_commands()
-            else:
-                await ctx.send(f"Ungültiger Index")
-        else:
-            await ctx.send("Command {cmd} nicht vorhanden!")
-
-    @help(
-        category="textcommands",
-        brief="Entfernt einen Text oder ein gesamtes Text-Kommando.",
-        syntax="!commands remove <cmd> <id?>",
-        example="!command remove !command 0",
-        description="Entfernt den Text des angegebenen Text-Kommandos an entsprechendem Index. War das der einzige Text für dieses Text-Kommando, wird das gesamte Kommando entfernt. Wird kein Index übergeben, so wird ebenfalls das gesamte Text-Kommando entfernt.",
-        parameters={
-            "cmd": "Name des zu entfernenden Kommandos (z. B. !horoskop). ",
-            "id": "*(optional)* Id des zu löschenden Texts"
-        },
-        mod=True
-    )
-    @cmd_commands.command(name="remove")
-    @commands.check(utils.is_mod)
-    async def cmd_command_remove(self, ctx, cmd, id=None):
-        texts = self.text_commands.get(cmd).get('data')
-
-        if texts:
-            if id:  # checkt erst, ob man lediglich einen Eintrag (und nicht das ganze Command) löschen möchte
-                i = int(id)
-                if 0 <= i < len(texts):  # schließt Aufrufe von Indizen aus, die außerhalb des Felds wären
-                    del texts[i]
-                    await ctx.send(f"Text {i} für Command {cmd} wurde erfolgreich entfernt")
-
-                    if len(texts) == 0:
-                        self.text_commands.pop(cmd)
-
-                    self.save_text_commands()
-                else:
-                    await ctx.send(f"Ungültiger Index")
-            else:  # jetzt kommt man zum vollständigen command removal (ursprünglich "remove-text-command")
-                # Hier könnte eine Bestätigung angefordert werden (Möchtest du wirklich das Command vollständig löschen? 👍👎)
-                if cmd in self.text_commands:
-                    self.text_commands.pop(cmd)
-                    remove_help_for(re.sub(r"^!", "", cmd))
-                    await ctx.send(f"Text Command {cmd} wurde erfolgreich entfernt.")
-                    self.save_text_commands()
-                else:
-                    await ctx.send(f"Text Command {cmd} nicht vorhanden!")
-        else:
-            await ctx.send("Command {cmd} nicht vorhanden!")
-
-    @cmd_commands.command(name="edit-help")
-    @commands.check(utils.is_mod)
-    async def cmd_command_edit_help(self, ctx, cmd, help_message):
-        help_object = None
-        try:
-            cmd = re.sub(r"^!*", "!", cmd)
-            help_object = self.text_commands.get(cmd).get('help')
-        except:
-            pass
-
-        if not help_object:
-            self.text_commands[cmd]['help'] = {}
-            help_object = self.text_commands[cmd]['help']
-
-        help_object['brief'] = help_message
-        text_command_help(cmd, brief=help_message, category=help_object.get('category'))
-        self.save_text_commands()
-
-        await ctx.send(f"[{cmd}] => Hilfe [{help_message}] erfolgreich hinzugefügt.")
-
-    @cmd_commands.command(name="edit-category")
-    @commands.check(utils.is_mod)
-    async def cmd_command_edit_category(self, ctx, cmd, category):
-        help_object = None
-        try:
-            help_object = self.text_commands.get(re.sub("^!*", "!", cmd)).get('help')
-        except:
-            pass
-
-        if not help_object:
-            help_object = {}
-
-        help_object['category'] = category
-        text_command_help(cmd, category=category, brief=help_object.get('brief'))
-        self.save_text_commands()
-
-        await ctx.send(f"[{cmd}] => Erfolgreich auf Kategorie [{category}] geändert.")
-
-    async def list_commands(self, ctx, cmd=None):
-        if cmd and not self.text_commands.get(cmd):
-            await ctx.send(f"{cmd} ist kein verfügbares Text-Command")
-            return
-
-        answer = f"Text Commands:\n" if cmd is None else f"Für {cmd} hinterlegte Texte:\n"
-        cmd_list = list(self.text_commands.keys()) if cmd is None else self.text_commands.get(cmd).get('data')
-
-        for i in range(len(cmd_list)):
-            text = cmd_list[i]
-            if len(answer) + len(text) > 2000:
-                await ctx.send(answer)
-                answer = f""
-
-            answer += f"{i}: {text}\n"
-
-        await ctx.send(answer)
-
-    async def add_command(self, cmd, text, help_message=None, category=None):
-        mod_channel = await self.bot.fetch_channel(self.mod_channel_id)
-        command = self.get_or_init_command(cmd)
-        texts = command.get("data")
-        texts.append(text)
-
-        if help_message and not command.get("help"):
-            command["help"] = {"brief": help_message}
-            if category:
-                command.get("help")["category"] = category
-
-        await mod_channel.send(f"[{cmd}] => [{text}] erfolgreich hinzugefügt.")
-
-        self.save_text_commands()
-
-    async def suggest_command(self, ctx, cmd, text, help_message=None, category=None):
-        mod_channel = await self.bot.fetch_channel(self.mod_channel_id)
-        command = self.text_commands.get(cmd)
-        title = "Vorschlag für neuen Command Text" if command else "Vorschlag für neues Command"
-
-        embed = disnake.Embed(title=title,
-                              description=f"<@!{ctx.author.id}> hat folgenden Vorschlag eingereicht.\n"
-                                          f"👍 um den Vorschlag anzunehmen\n"
-                                          f"👎 um den Vorschlag abzulehnen")
-        embed.add_field(name="\u200b", value="\u200b")
-        embed.add_field(name="Command", value=f'{cmd}', inline=False)
-        embed.add_field(name="Text", value=f'{text}', inline=False)
-        if help_message:
-            embed.add_field(name="Hilfetext", value=f'{help_message}', inline=False)
-        if category:
-            embed.add_field(name="Kategorie", value=f'{category}', inline=False)
-
-        message = await mod_channel.send(embed=embed)
-        await message.add_reaction("👍")
-        await message.add_reaction("👎")
-        await utils.send_dm(ctx.author,
-                            "Dein Vorschlag wurde den Mods zur Genehmigung vorgelegt. "
-                            "Sobald darüber entschieden wurde, erhältst du eine Benachrichtigung.")
-
-    def get_or_init_command(self, cmd):
-        if command := self.text_commands.get(cmd):
-            return command
-
-        self.text_commands[cmd] = {"data": []}
-        return self.text_commands.get(cmd)
-
-    async def handle_command_reaction(self, message, approved=True):
-        embed = message.embeds[0]
-        fields = {field.name: field.value for field in embed.fields}
-        cmd = fields.get("Command")
-        text = fields.get("Text")
-        help_message = fields.get("Hilfetext")
-        category = fields.get("Kategorie")
-        member = await message.guild.fetch_member(embed.description[3:21])
-
-        if approved:
-            await self.add_command(cmd, text, help_message=help_message, category=category)
-            await utils.send_dm(member,
-                                f"Herzlichen Glückwunsch, dein Vorschlag für {cmd} wurde angenommen:\n{text}")
-        else:
-            await utils.send_dm(member,
-                                f"Vielen Dank, dass du dir Gedanken darüber machst, wie man 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
deleted file mode 100644
index 9ac1f4dea985b20f43da59d7fb9938a305ba3c07..0000000000000000000000000000000000000000
--- a/cogs/timer.py
+++ /dev/null
@@ -1,303 +0,0 @@
-import json
-import os
-import random
-from asyncio import sleep
-from copy import deepcopy
-from datetime import datetime, timedelta
-
-import disnake
-from disnake import MessageInteraction, ApplicationCommandInteraction
-from disnake.ext import commands, tasks
-from disnake.ui import Button
-
-from views import timer_view
-
-
-class Timer(commands.Cog):
-
-    def __init__(self, bot):
-        self.bot = bot
-        self.guild_id = int(os.getenv('DISCORD_GUILD'))
-        self.default_names = ["Rapunzel", "Aschenputtel", "Schneewittchen", "Frau Holle", "Schneeweißchen und Rosenrot",
-                              "Gestiefelter Kater", "Bremer Stadtmusikanten"]
-        self.timer_file_path = os.getenv("DISCORD_TIMER_FILE")
-        self.running_timers = self.load()
-        self.load()
-        self.run_timer.start()
-
-    def load(self):
-        with open(self.timer_file_path, mode='r') as timer_file:
-            return json.load(timer_file)
-
-    def save(self):
-        with open(self.timer_file_path, mode='w') as timer_file:
-            json.dump(self.running_timers, timer_file)
-
-    def get_view(self, disabled=False):
-        view = timer_view.TimerView(callback=self.on_button_click)
-
-        if disabled:
-            view.disable()
-
-        return view
-
-    async def on_button_click(self, button: Button, interaction: MessageInteraction):
-        custom_id = button.custom_id
-
-        if custom_id == timer_view.SUBSCRIBE:
-            await self.on_subscribe(button, interaction)
-        elif custom_id == timer_view.UNSUBSCRIBE:
-            await self.on_unsubscribe(button, interaction)
-        elif custom_id == timer_view.SKIP:
-            await self.on_skip(button, interaction)
-        elif custom_id == timer_view.RESTART:
-            await self.on_restart(button, interaction)
-        elif custom_id == timer_view.STOP:
-            await self.on_stop(button, interaction)
-
-    async def on_subscribe(self, button: Button, interaction: MessageInteraction):
-        msg_id = str(interaction.message.id)
-        if timer := self.running_timers.get(msg_id):
-            if str(interaction.author.id) not in timer['registered']:
-                timer['registered'].append(str(interaction.author.id))
-                self.save()
-                name, status, wt, bt, remaining, registered, _ = self.get_details(msg_id)
-                embed = self.create_embed(name, status, wt, bt, remaining, registered)
-                await interaction.message.edit(embed=embed, view=self.get_view())
-                await interaction.response.send_message("Du hast dich erfolgreich angemeldet", ephemeral=True)
-            else:
-                await interaction.response.send_message("Du bist bereits angemeldet.", ephemeral=True)
-        else:
-            await interaction.response.send_message("Etwas ist schiefgelaufen...", ephemeral=True)
-
-    async def on_unsubscribe(self, button: Button, interaction: MessageInteraction):
-        msg_id = str(interaction.message.id)
-        if timer := self.running_timers.get(msg_id):
-            registered = timer['registered']
-            if str(interaction.author.id) in registered:
-                if len(registered) == 1:
-                    await self.on_stop(button, interaction)
-                    return
-                else:
-                    timer['registered'].remove(str(interaction.author.id))
-                    self.save()
-                    name, status, wt, bt, remaining, registered, _ = self.get_details(msg_id)
-                    embed = self.create_embed(name, status, wt, bt, remaining, registered)
-                    await interaction.message.edit(embed=embed, view=self.get_view())
-                    await interaction.response.send_message("Du hast dich erfolgreich abgemeldet", ephemeral=True)
-            else:
-                await interaction.response.send_message("Du warst gar nicht angemeldet.", ephemeral=True)
-        else:
-            await interaction.response.send_message("Etwas ist schiefgelaufen...", ephemeral=True)
-
-    async def on_skip(self, button: Button, interaction: MessageInteraction):
-        msg_id = str(interaction.message.id)
-        if timer := self.running_timers.get(msg_id):
-            registered = timer['registered']
-            if str(interaction.author.id) in timer['registered']:
-                new_phase = await self.switch_phase(msg_id)
-                if new_phase == "Pause":
-                    await self.make_sound(registered, 'groove-intro.mp3')
-                else:
-                    await self.make_sound(registered, 'roll_with_it-outro.mp3')
-                await interaction.response.send_message("Erfolgreich übersprungen", ephemeral=True)
-            else:
-                await interaction.response.send_message("Nur angemeldete Personen können den Timer bedienen.",
-                                                        ephemeral=True)
-        else:
-            await interaction.response.send_message("Etwas ist schiefgelaufen...", ephemeral=True)
-
-    async def on_restart(self, button: Button, interaction: MessageInteraction):
-        msg_id = str(interaction.message.id)
-        if timer := self.running_timers.get(msg_id):
-            registered = timer['registered']
-            if str(interaction.author.id) in timer['registered']:
-                timer['status'] = 'Arbeiten'
-                timer['remaining'] = timer['working_time']
-                self.save()
-
-                await self.edit_message(msg_id)
-                await self.make_sound(registered, 'roll_with_it-outro.mp3')
-                await interaction.response.send_message("Erfolgreich neugestartet", ephemeral=True)
-            else:
-                await interaction.response.send_message("Nur angemeldete Personen können den Timer neu starten.",
-                                                        ephemeral=True)
-        else:
-            await interaction.response.send_message("Etwas ist schiefgelaufen...", ephemeral=True)
-
-    async def on_stop(self, button: Button, interaction: MessageInteraction):
-        msg_id = str(interaction.message.id)
-        if timer := self.running_timers.get(msg_id):
-            registered = timer['registered']
-            if str(interaction.author.id) in timer['registered']:
-                mentions = self.get_mentions(msg_id)
-                timer['status'] = "Beendet"
-                timer['remaining'] = 0
-                timer['registered'] = []
-
-                await interaction.response.send_message("Erfolgreich beendet", ephemeral=True)
-                if new_msg_id := await self.edit_message(msg_id, mentions=mentions):
-                    await self.make_sound(registered, 'applause.mp3')
-                    self.running_timers.pop(new_msg_id)
-                    self.save()
-            else:
-                # Reply with a hidden message
-                await interaction.response.send_message("Nur angemeldete Personen können den Timer beenden.",
-                                                        ephemeral=True)
-        else:
-            await interaction.response.send_message("Etwas ist schiefgelaufen...", ephemeral=True)
-
-    def create_embed(self, name, status, working_time, break_time, remaining, registered):
-        color = disnake.Colour.green() if status == "Arbeiten" else 0xFFC63A if status == "Pause" else disnake.Colour.red()
-        descr = f"👍 beim Timer anmelden\n\n" \
-                f"👎 beim Timer abmelden\n\n" \
-                f"⏩ Phase überspringen\n\n" \
-                f"🔄 Timer neu starten\n\n" \
-                f"🛑 Timer beenden\n"
-        zeiten = f"{working_time} Minuten Arbeiten\n{break_time} Minuten Pause"
-        remaining_value = f"{remaining} Minuten"
-        endzeit = (datetime.now() + timedelta(minutes=remaining)).strftime("%H:%M")
-        end_value = f" [bis {endzeit} Uhr]" if status != "Beendet" else ""
-        user_list = [self.bot.get_user(int(user_id)) for user_id in registered]
-        angemeldet_value = ", ".join([user.mention for user in user_list])
-
-        embed = disnake.Embed(title=name,
-                              description=f'Jetzt: {status}',
-                              color=color)
-        embed.add_field(name="Bedienung:", value=descr, inline=False)
-        embed.add_field(name="Zeiten:", value=zeiten, inline=False)
-        embed.add_field(name="verbleibende Zeit:", value=remaining_value + end_value, inline=False)
-        embed.add_field(name="angemeldete User:", value=angemeldet_value if registered else "-", inline=False)
-
-        return embed
-
-    @commands.slash_command(name="timer", description="Erstelle deine persönliche  Eieruhr",
-                            guild_ids=[int(os.getenv('DISCORD_GUILD'))])
-    async def cmd_timer(self, interaction: ApplicationCommandInteraction, working_time: int = 25,
-                        break_time: int = 5,
-                        name: str = None):
-        name = name if name else random.choice(self.default_names)
-        remaining = working_time
-        status = "Arbeiten"
-        registered = [str(interaction.author.id)]
-
-        embed = self.create_embed(name, status, working_time, break_time, remaining, registered)
-        await interaction.response.send_message(embed=embed, view=self.get_view())
-        message = await interaction.original_message()
-
-        self.running_timers[str(message.id)] = {'name': name,
-                                                'status': status,
-                                                'working_time': working_time,
-                                                'break_time': break_time,
-                                                'remaining': remaining,
-                                                'registered': registered,
-                                                'channel': interaction.channel_id}
-        self.save()
-        await self.make_sound(registered, 'roll_with_it-outro.mp3')
-
-    async def switch_phase(self, msg_id):
-        if timer := self.running_timers.get(msg_id):
-            if timer['status'] == "Arbeiten":
-                timer['status'] = "Pause"
-                timer['remaining'] = timer['break_time']
-            elif timer['status'] == "Pause":
-                timer['status'] = "Arbeiten"
-                timer['remaining'] = timer['working_time']
-            else:
-                self.running_timers.pop(msg_id)
-                return "Beendet"
-            self.save()
-
-            if new_msg_id := await self.edit_message(msg_id):
-                return self.running_timers[new_msg_id]['status']
-            else:
-                return "Beendet"
-
-    def get_details(self, msg_id):
-        name = self.running_timers[msg_id]['name']
-        status = self.running_timers[msg_id]['status']
-        wt = self.running_timers[msg_id]['working_time']
-        bt = self.running_timers[msg_id]['break_time']
-        remaining = self.running_timers[msg_id]['remaining']
-        registered = self.running_timers[msg_id]['registered']
-        channel = self.running_timers[msg_id]['channel']
-        return name, status, wt, bt, remaining, registered, channel
-
-    async def edit_message(self, msg_id, mentions=None, create_new=True):
-        if timer := self.running_timers.get(msg_id):
-            channel_id = timer['channel']
-            channel = await self.bot.fetch_channel(int(channel_id))
-            try:
-                msg = await channel.fetch_message(int(msg_id))
-
-                name, status, wt, bt, remaining, registered, _ = self.get_details(msg_id)
-                embed = self.create_embed(name, status, wt, bt, remaining, registered)
-
-                if create_new:
-                    await msg.delete()
-                    if not mentions:
-                        mentions = self.get_mentions(msg_id)
-                    if status == "Beendet":
-                        new_msg = await channel.send(mentions, embed=embed,
-                                                     view=self.get_view(disabled=True))
-                    else:
-                        new_msg = await channel.send(mentions, embed=embed, view=self.get_view())
-                    self.running_timers[str(new_msg.id)] = self.running_timers[msg_id]
-                    self.running_timers.pop(msg_id)
-                    self.save()
-                    msg = new_msg
-                else:
-                    await msg.edit(embed=embed, view=self.get_view())
-                return str(msg.id)
-            except disnake.errors.NotFound:
-                self.running_timers.pop(msg_id)
-                self.save()
-                return None
-
-    def get_mentions(self, msg_id):
-        guild = self.bot.get_guild(self.guild_id)
-        registered = self.running_timers.get(msg_id)['registered']
-        members = [guild.get_member(int(user_id)) for user_id in registered]
-        mentions = ", ".join([member.mention for member in members])
-        return mentions
-
-    async def make_sound(self, registered_users, filename):
-        guild = self.bot.get_guild(self.guild_id)
-        for user_id in registered_users:
-            member = guild.get_member(int(user_id))
-            if member.voice:
-                channel = member.voice.channel
-                if channel:  # If user is in a channel
-                    try:
-                        voice_client = await channel.connect()
-                        voice_client.play(disnake.FFmpegPCMAudio(f'cogs/sounds/{filename}'))
-                        await sleep(3)
-                    except disnake.errors.ClientException as e:
-                        print(e)
-                    for vc in self.bot.voice_clients:
-                        await vc.disconnect()
-                break
-
-    @tasks.loop(minutes=1)
-    async def run_timer(self):
-        timers_copy = deepcopy(self.running_timers)
-        for msg_id in timers_copy:
-            registered = self.running_timers[msg_id]['registered']
-            self.running_timers[msg_id]['remaining'] -= 1
-            if self.running_timers[msg_id]['remaining'] <= 0:
-                new_phase = await self.switch_phase(msg_id)
-                if new_phase == "Pause":
-                    await self.make_sound(registered, 'groove-intro.mp3')
-                elif new_phase == "Arbeiten":
-                    await self.make_sound(registered, 'roll_with_it-outro.mp3')
-            else:
-                await self.edit_message(msg_id, create_new=False)
-
-    @run_timer.before_loop
-    async def before_timer(self):
-        await sleep(60)
-
-    @cmd_timer.error
-    async def timer_error(self, ctx, error):
-        await ctx.send("Das habe ich nicht verstanden. Die Timer-Syntax ist:\n"
-                       "`!timer <learning-time?> <break-time?> <name?>`\n")
diff --git a/cogs/voice.py b/cogs/voice.py
deleted file mode 100644
index 20c1b5d62b0365008d4aea772050d700eb72e2e5..0000000000000000000000000000000000000000
--- a/cogs/voice.py
+++ /dev/null
@@ -1,32 +0,0 @@
-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
deleted file mode 100644
index 8b4ce90d17f1889cce0ef4ebdaa77ef4f7a6b49d..0000000000000000000000000000000000000000
--- a/cogs/welcome.py
+++ /dev/null
@@ -1,80 +0,0 @@
-import os
-
-import disnake
-from disnake.ext import commands
-
-import utils
-from cogs.help import help, handle_error
-
-
-class Welcome(commands.Cog):
-    def __init__(self, bot):
-        self.bot = bot
-        self.channel_id = int(os.getenv("DISCORD_WELCOME_CHANNEL", "0"))
-        self.message_id = int(os.getenv("DISCORD_WELCOME_MSG", "0"))
-
-    @help(
-      category="updater",
-      brief="aktualisiert die Willkommensnachricht.",
-      mod=True
-      )
-    @commands.command("update-welcome")
-    @commands.check(utils.is_mod)
-    async def cmd_update_welcome(self, ctx):
-        channel = await self.bot.fetch_channel(self.channel_id)
-        message = await channel.fetch_message(self.message_id)
-
-        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(
-            url="https://cdn.discordapp.com/avatars/697842294279241749/c7d3063f39d33862e9b950f72ab71165.webp")
-               
-        embed.add_field(name="Boty McBotface",
-                        value=f"Boty ist der Server-Bot und kann dein Freund und Helfer sein, wenn es um die Organisation deines Studiums geht. In <#{os.getenv('DISCORD_BOTUEBUNGSPLATZ_CHANNEL')}> kann man mit den verschiedenen Befehlen rumprobieren, bei `!help` wird er dir per Direktnachricht einen Ãœberblick von seinen Funktionen geben.", 
-                        #channelverlinkung anders?
-                        inline=False)
-
-        embed.add_field(name="Vorstellung",
-                        value=f"Es gibt einen <#{os.getenv('DISCORD_VORSTELLUNGSCHANNEL')}>. Wir würden uns freuen, wenn ihr euch kurz vorstellen würdet. So ist es möglich, Gemeinsamkeiten zu entdecken und man weiß ungefähr, mit wem man es zu tun hat. Hier soll auch gar nicht der komplette Lebenslauf stehen, schreibt einfach das, was ihr so über euch mitteilen möchtet.",
-                        inline=False)
-                
-        embed.add_field(name="Rollen",
-                        value=f"Es gibt verschiedene Rollen hier. Derzeit sind das zum einen Rollen zu den verschiedenen Studiengängen unserer Fakultät (sowie allgemeinere Rollen), Farbrollen. Wirf doch mal einen Blick in <#{os.getenv('DISCORD_ROLLEN_CHANNEL')}>",
-                        inline=False)
-        
-        embed.add_field(name="Lerngruppen",
-                        value="Wenn ihr eine feste Lerngruppe gründen möchtet, dann könnt ihr dafür gerne einen eigenen Textchannel bekommen. Sagt einfach bescheid, dann kann dieser erstellt werden. Ihr könnt dann auch entscheiden, ob nur ihr Zugang zu diesem Channel haben möchtet, oder ob dieser für alle zugänglich sein soll.",
-                        inline=False)
-
-        embed.add_field(name="Nachrichten anpinnen",
-                        value="Wenn ihr Nachrichten in einem Channel anpinnen möchtet, könnt ihr dafür unseren Bot verwenden. Setzt einfach eine :pushpin: Reaktion auf die entsprechende Nachricht und der pin-bot erledigt den Rest.", 
-                        #eventuell bei Boty ansiedeln
-                        inline=False)    
-                
-        embed.add_field(name="Regeln",
-                        value="Es gibt hier ein paar, wenige Regeln, an die wir uns alle halten wollen. Diese findet ihr hier https://discordapp.com/channels/353315134678106113/697729059173433344/709475694157234198",
-                        inline=False)
-        
-        embed.add_field(name="Discord Tipps",
-                        value="Mit `Strg` + `#` (deutscher Tastaturlayout) erhält man einen Überblick über die Discord-Shortcuts. \n- Zur Übersichtlichkeit kann man stummgeschaltete Channels ausblenden: https://support.discord.com/hc/de/articles/213599277-Wie-verstecke-Ich-stumme-Kanäle-,\n- Markdown (und damit Code-Blöcke) gibt es hier auch: https://support.discord.com/hc/en-us/articles/210298617-Markdown-Text-101-Chat-Formatting-Bold-Italic-Underline-",
-                        inline=False)   
-
-        await message.edit(content="", embed=embed)
-
-    @commands.Cog.listener()
-    async def on_member_join(self, member):
-        await utils.send_dm(member,
-                            f"Herzlich Willkommen auf diesem Discord-Server. Wir hoffen sehr, dass du dich hier wohl fühlst. Alle notwendigen Informationen, die du für den Einstieg brauchst, findest du in <#{self.channel_id}>\n"
-                            f"Wir würden uns sehr freuen, wenn du dich in <#{os.getenv('DISCORD_VORSTELLUNGSCHANNEL')}> allen kurz vorstellen würdest. Es gibt nicht viele Regeln zu beachten, doch die Regeln, die aufgestellt sind, findest du hier:  https://discordapp.com/channels/353315134678106113/697729059173433344/709475694157234198 .\n"
-                            f"Du darfst dir außerdem gerne im Channel <#{os.getenv('DISCORD_ROLLEN_CHANNEL')}> die passende Rolle zu den Studiengängen in denen du eingeschrieben bist zuweisen. \n\n"
-                            f"Abschließend bleibt mir nur noch, dir hier viel Spaß zu wünschen, und falls du bei etwas hilfe brauchen solltest, schreib mir doch eine private Nachricht, das Moderatoren Team wird sich dann darum kümmern.")
-
-    @commands.Cog.listener()
-    async def on_member_update(self, before, after):
-        if before.pending != after.pending and not after.pending:
-            channel = await self.bot.fetch_channel(int(os.getenv("DISCORD_GREETING_CHANNEL")))
-            await channel.send(f"Herzlich Willkommen <@!{before.id}> im Kreise der Studentinnen :wave:")
-
-    async def cog_command_error(self, ctx, error):
-        await handle_error(ctx, error)
diff --git a/cogs/change_log.py b/deprecated/change_log.py
similarity index 88%
rename from cogs/change_log.py
rename to deprecated/change_log.py
index b928e1c38e35456e313eb40e6861ac0223e90ba7..dea63c0f41fc16ff6291493bece2974f7cb3a2a6 100644
--- a/cogs/change_log.py
+++ b/deprecated/change_log.py
@@ -1,27 +1,31 @@
-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)
+import os
+
+from discord.ext import commands
+
+
+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)
+
+
+async def setup(bot: commands.Bot) -> None:
+    await bot.add_cog(ChangeLog(bot))
diff --git a/cogs/christmas.py b/deprecated/christmas.py
similarity index 73%
rename from cogs/christmas.py
rename to deprecated/christmas.py
index 9c530f52b33cb2263233e1b1b6e85f4d1aa64d3d..b21258ae7d4873efec2bdb74e2bc67dec437b871 100644
--- a/cogs/christmas.py
+++ b/deprecated/christmas.py
@@ -3,8 +3,8 @@ import json
 import os
 from datetime import datetime, timedelta
 
-from disnake import ApplicationCommandInteraction, Member
-from disnake.ext import commands, tasks
+from discord import Interaction, Member, app_commands
+from discord.ext import commands, tasks
 from dotenv import load_dotenv
 
 import utils
@@ -27,11 +27,12 @@ def create_advent_calendar():
     return advent_calendar
 
 
-class Christmas(commands.Cog):
+@app_commands.guild_only()
+class Christmas(commands.GroupCog, name="advent"):
     def __init__(self, bot):
         self.bot = bot
         self.seasonal_events_category = int(os.getenv("DISCORD_SEASONAL_EVENTS_CATEGORY"))
-        self.advent_calendar_channel = int(os.getenv("DISCORD_ADVENT_CALENDAR_CHANNEL_2021"))
+        self.advent_calendar_channel = int(os.getenv("DISCORD_ADVENT_CALENDAR_CHANNEL_2022"))
         self.file_name = os.getenv("DISCORD_ADVENT_CALENDAR_FILE")
         self.advent_calendar = self.load()
         self.advent_calendar_loop.start()
@@ -49,15 +50,11 @@ class Christmas(commands.Cog):
         with open(self.file_name, mode='w') as f:
             json.dump(self.advent_calendar, f)
 
-    @commands.slash_command(name="advent", guild_ids=[int(os.getenv('DISCORD_GUILD'))])
-    async def cmd_advent(self, interaction: ApplicationCommandInteraction):
-        pass
-
-    @cmd_advent.sub_command(name="list", description="Erhalte die Liste aller Türchen mit Zuordnung und Thema")
-    @commands.check(utils.is_mod)
-    async def cmd_advent_list(self, interaction: ApplicationCommandInteraction):
+    @app_commands.command(name="list", description="Erhalte die Liste aller Türchen mit Zuordnung und Thema")
+    @app_commands.check(utils.is_mod)
+    async def cmd_advent_list(self, interaction: Interaction):
         message = f"__**Adventskalender 2021**__\n\n"
-
+        await interaction.response.defer(ephemeral=True)
         for day in self.advent_calendar:
             message += f"{day['number']}. "
             if day["assigned"]:
@@ -67,29 +64,31 @@ class Christmas(commands.Cog):
 
             message += "\n"
 
-        await interaction.response.send_message(message, ephemeral=True)
+        await interaction.followup.send(message, ephemeral=True)
 
-    @cmd_advent.sub_command(name="assign", description="Einer Person ein Türchen zuweisen",
-                            guild_ids=[int(os.getenv('DISCORD_GUILD'))])
-    @commands.check(utils.is_mod)
-    async def cmd_advent_assign(self, interaction: ApplicationCommandInteraction, day: int, member: Member, name: str):
-        if self.advent_calendar[day - 1]["assigned"]:
-            await interaction.response.send_message("Das gewählte Türchen ist bereits vergeben. \n"
+    @app_commands.command(name="assign", description="Einer Person ein Türchen zuweisen")
+    @app_commands.describe(day="Adventstag des Türchens", member="User der das Türchen bespielt", name="Kanalname")
+    @app_commands.guild_only()
+    @app_commands.check(utils.is_mod)
+    async def cmd_advent_assign(self, interaction: Interaction, day: int, member: Member, name: str):
+        if "assigned" in self.advent_calendar[day - 1] and self.advent_calendar[day - 1]["assigned"]:
+            await interaction.response.send("Das gewählte Türchen ist bereits vergeben. \n"
                                                     "Wenn du das Türchen an jemand anderen vergeben möchtest, oder das "
                                                     "Thema ändern möchtest, verwende `/advent reassign`.",
                                                     ephemeral=True)
         else:
             await interaction.response.defer(ephemeral=True)
             await self.assign_day(day, member, name)
-            await interaction.edit_original_message(content="Das gewählte Türchen wurde vergeben.")
+            await interaction.followup.send(content="Das gewählte Türchen wurde vergeben.")
 
-    @cmd_advent.sub_command(name="reassign", description="Ein Türchen neu zuweisen",
-                            guild_ids=[int(os.getenv('DISCORD_GUILD'))])
+    @app_commands.command(name="reassign", description="Ein Türchen neu zuweisen")
+    @app_commands.describe(day="Adventstag des Türchens", member="User der das Türchen bespielt", name="Kanalname")
+    @app_commands.guild_only()
     @commands.check(utils.is_mod)
-    async def cmd_advent_reassign(self, interaction: ApplicationCommandInteraction, day: int, member: Member,
+    async def cmd_advent_reassign(self, interaction: Interaction, day: int, member: Member,
                                   name: str):
         if not self.advent_calendar[day - 1]["assigned"]:
-            await interaction.response.send_message("Das gewählte Türchen ist noch nicht vergeben. \n"
+            await interaction.response.send("Das gewählte Türchen ist noch nicht vergeben. \n"
                                                     "Bitte verwende `/advent assign` um das Türchen an "
                                                     "jemanden zu vergeben.", ephemeral=True)
         else:
@@ -98,19 +97,20 @@ class Christmas(commands.Cog):
             old_member = await self.bot.fetch_user(self.advent_calendar[day - 1]["assignee"])
             await channel.set_permissions(old_member, overwrite=None)
             await self.assign_day(day, member, name)
-            await interaction.edit_original_message(content="Das gewählte Türchen wurde neu vergeben.")
+            await interaction.followup.send(content="Das gewählte Türchen wurde neu vergeben.")
 
-    @cmd_advent.sub_command(name="remaining", description="Noch nicht zugewiesene Türchen ausgeben lassen.",
-                            guild_ids=[int(os.getenv('DISCORD_GUILD'))])
+    @app_commands.command(name="remaining", description="Noch nicht zugewiesene Türchen ausgeben lassen.")
+    @app_commands.guild_only()
     @commands.check(utils.is_mod)
-    async def cmd_advent_remaining(self, interaction: ApplicationCommandInteraction):
+    async def cmd_advent_remaining(self, interaction: Interaction):
+        await interaction.response.defer(ephemeral=True)
         message = f"Noch verfügbare Türchen: "
 
         for day in self.advent_calendar:
             if not day["assigned"]:
                 message += f"{day['number']}, "
 
-        await interaction.response.send_message(message[:-2], ephemeral=True)
+        await interaction.followup.send(message[:-2], ephemeral=True)
 
     async def assign_day(self, day: int, member: Member, name: str):
         category = await self.bot.fetch_channel(self.seasonal_events_category)
@@ -167,3 +167,7 @@ class Christmas(commands.Cog):
     @advent_calendar_loop.before_loop
     async def before_advent_calendar_loop(self):
         await asyncio.sleep(10 - datetime.now().second % 10)
+
+
+async def setup(bot: commands.Bot) -> None:
+    await bot.add_cog(Christmas(bot))
diff --git a/cogs/emoji_hunt.py b/deprecated/emoji_hunt.py
similarity index 81%
rename from cogs/emoji_hunt.py
rename to deprecated/emoji_hunt.py
index 93c4991a0bc7be9f0d383bc925f0f11adc8256f8..e645d2f4a2747dcef2953f6f2271a28c7f025b51 100644
--- a/cogs/emoji_hunt.py
+++ b/deprecated/emoji_hunt.py
@@ -1,107 +1,116 @@
-import json
-import random
-import disnake
-from disnake.ext import commands, tasks
-from cogs.help import handle_error
-
-
-class EmojiHunt(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/emoji_hunt.json", mode="r")
-        return json.load(data_file)
-
-    def save_data(self):
-        data_file = open("data/emoji_hunt.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()
-
-    @commands.command(name="leaderboard")
-    async def cmd_leaderboard(self, ctx, all=None):
-        leaderboard = self.data["leaderboard"]
-        embed = disnake.Embed(title="Emojijagd Leaderboard", description="Wer hat am meisten Emojis gefunden?")
-        embed.set_thumbnail(url="https://external-preview.redd.it/vFsRraBXc5hfUGRWtPPF-NG5maHEPRWTIqamB24whF8.jpg?width=960&crop=smart&auto=webp&s=24d42c9b4f5239a4c3cac79e704b7129c9e2e4d3")
-
-        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"Jägerin", value=places)
-        embed.add_field(name=f"Emojis", 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 random
+
+from cogs.help import handle_error
+from discord import app_commands, Embed
+from discord.app_commands import Choice
+from discord.ext import commands, tasks
+
+
+class EmojiHunt(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/emoji_hunt.json", mode="r")
+        return json.load(data_file)
+
+    def save_data(self):
+        data_file = open("data/emoji_hunt.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()
+
+    @app_commands.command(name="leaderboard")
+    @app_commands.choices(show=[Choice(name='10', value=10), Choice(name='all', value=0)])
+    async def cmd_leaderboard(self, interaction, show: int = 10):
+        await interaction.response.defer()
+        leaderboard = self.data["leaderboard"]
+        embed = Embed(title="Emojijagd Leaderboard", description="Wer hat am meisten Emojis gefunden?")
+        embed.set_thumbnail(url="https://external-preview.redd.it/vFsRraBXc5hfUGRWtPPF-NG5maHEPRWTIqamB24whF8.jpg?width=960&crop=smart&auto=webp&s=24d42c9b4f5239a4c3cac79e704b7129c9e2e4d3")
+
+        places = scores = "\u200b"
+        place = 0
+        max_entries = show
+        ready = False
+        for key, value in sorted(leaderboard.items(), key=lambda item: item[1], reverse=True):
+            try:
+                place += 1
+
+                if 0 < max_entries < place:
+                    if ready:
+                        break
+                    elif str(interaction.user.id) != key:
+                        continue
+                places += f"{place}: <@!{key}>\n"
+                scores += f"{value:,}\n".replace(",", ".")
+
+                if str(interaction.user.id) == key:
+                    ready = True
+            except:
+                pass
+
+        embed.add_field(name=f"Jägerin", value=places)
+        embed.add_field(name=f"Emojis", value=scores)
+        await interaction.followup.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)
+
+
+async def setup(bot: commands.Bot) -> None:
+    await bot.add_cog(EmojiHunt(bot))
+
diff --git a/deprecated/github.py b/deprecated/github.py
new file mode 100644
index 0000000000000000000000000000000000000000..7dcaa29d83836b73baf1f0a3506ed9383612b920
--- /dev/null
+++ b/deprecated/github.py
@@ -0,0 +1,87 @@
+import base64
+import json
+import os
+
+from aiohttp import ClientSession
+from discord import app_commands
+from discord.ext import commands
+
+
+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)
+
+    @app_commands.command(name="idee", description="Sendet eine Idee für Boty zur Abstimmung ein.")
+    @app_commands.describe(text="Text der Idee.")
+    @app_commands.guild_only()
+    async def cmd_idee(self, interaction, text: str):
+        await interaction.response.defer(ephemeral=True)
+        channel_id = int(os.getenv("DISCORD_IDEE_CHANNEL"))
+        channel = await self.bot.fetch_channel(channel_id)
+        message = await channel.send(f"[Neue Weltidee] {text}")
+        self.data[str(message.id)] = {"created": False, "user_id": interaction.user.id, "content": text, "html_url": ""}
+        await message.add_reaction("👍")
+        await interaction.followup.send(f"Danke. Deine Idee wurde in <#{channel_id}> zur Abstimmung vorgeschlagen.")
+        self.save()
+
+    @app_commands.command(name="card", description="Mit diesem Kommando kannst du einen Issue in Github anlegen.")
+    @app_commands.describe(text="Text der Idee.")
+    @app_commands.default_permissions(manage_roles=True)
+    @app_commands.guild_only()
+    async def cmd_card(self, interaction, text: str):
+        await interaction.response.send_message("Lege neues Github-Issue an", ephemeral=True)
+        idea = {"created": False, "user_id": interaction.user.id, "content": text, "html_url": ""}
+        await self.create_issue(idea)
+        await interaction.followup.send(
+            f"Danke <@!{interaction.user.id}> für deinen Vorschlag. Ich habe für dich gerade folgenden Issue in Github erstellt: {idea['html_url']}")
+        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.name == 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 == 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)
+                            await channel.send(
+                                f"Danke <@!{idea['user_id']}> für deinen Vorschlag. Ich habe für dich gerade folgenden Issue in Github erstellt: {idea['html_url']}")
+                            self.save()
+
+    async def create_issue(self, idea):
+        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': idea["content"]}) as r:
+                if r.status == 201:
+                    js = await r.json()
+
+                    idea["created"] = True
+                    idea["number"] = js["number"]
+                    idea["html_url"] = js["html_url"]
+                    idea.pop("content", None)
+
+
+async def setup(bot: commands.Bot) -> None:
+    await bot.add_cog(Github(bot))
+
diff --git a/extensions/appointments.py b/extensions/appointments.py
new file mode 100644
index 0000000000000000000000000000000000000000..5ecf1475d0b12e4ad73101b38579e6fe5ced71bc
--- /dev/null
+++ b/extensions/appointments.py
@@ -0,0 +1,251 @@
+import asyncio
+import json
+import os
+import uuid
+from datetime import datetime, timedelta
+from typing import NewType, Union, Dict
+
+from discord import app_commands, errors, Embed, Interaction, VoiceChannel, StageChannel, TextChannel, \
+    ForumChannel, CategoryChannel, Thread, PartialMessageable
+from discord.ext import tasks, commands
+
+from views.appointment_view import AppointmentView
+
+Channel = NewType('Channel', Union[
+    VoiceChannel, StageChannel, TextChannel, ForumChannel, CategoryChannel, Thread, PartialMessageable, None])
+
+
+@app_commands.guild_only()
+class Appointments(commands.GroupCog, name="appointments", description="Verwaltet Termine in Kanälen"):
+    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):
+        appointments_file = open(self.app_file, mode='r')
+        self.appointments = json.load(appointments_file)
+
+    def save_appointments(self):
+        appointments_file = open(self.app_file, mode='w')
+        json.dump(self.appointments, 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.now()
+                date_time = datetime.strptime(appointment["date_time"], self.fmt)
+                remind_at = date_time - 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']}\" startet "
+
+                        if appointment["reminder"] > 0 and diff > 0:
+                            answer += f"<t:{int(date_time.timestamp())}:R>."
+                            if (reminder := appointment.get("reminder")) and appointment.get("recurring"):
+                                appointment["original_reminder"] = str(reminder)
+                            appointment["reminder"] = 0
+                        else:
+                            answer += f"jetzt! :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 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.strptime(date_time_str, self.fmt)
+                            new_date_time = date_time + timedelta(days=recurring)
+                            reminder = channel_appointment.get("original_reminder")
+                            reminder = reminder if reminder else 0
+                            await self.add_appointment(channel, channel_appointment["author_id"],
+                                                       new_date_time,
+                                                       reminder,
+                                                       channel_appointment["title"],
+                                                       channel_appointment["attendees"],
+                                                       channel_appointment["ics_uuid"],
+                                                       channel_appointment["description"],
+                                                       channel_appointment["recurring"])
+                        channel_appointments.pop(key)
+        self.save_appointments()
+
+    @timer.before_loop
+    async def before_timer(self):
+        await asyncio.sleep(60 - datetime.now().second)
+
+    async def add_appointment(self, channel: Channel, author_id: int, date_time: datetime, reminder: int, title: str,
+                              attendees: Dict, ics_uuid: str, description: str = "", recurring: int = None) -> None:
+        message = await self.send_or_update_appointment(channel, author_id, description, title, date_time, reminder,
+                                                        recurring, attendees)
+
+        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,
+                                                 "description": description, "attendees": attendees,
+                                                 "ics_uuid": ics_uuid}
+
+        self.save_appointments()
+
+    @app_commands.command(name="add", description="Fügt dem Kanal einen neunen Termin hinzu.")
+    @app_commands.describe(date="Tag des Termins (z. B. 21.10.2015).", time="Uhrzeit des Termins (z. B. 13:37).",
+                           reminder="Wie viele Minuten bevor der Termin startet, soll eine Erinnerung verschickt werden?",
+                           title="Titel des Termins.", description="Beschreibung des Termins.",
+                           recurring="In welchem Intervall (in Tagen) soll der Termin wiederholt werden?")
+    async def cmd_add_appointment(self, interaction: Interaction, date: str, time: str, reminder: int, title: str,
+                                  description: str = "", recurring: int = None):
+
+        await interaction.response.defer(ephemeral=True)
+        try:
+            attendees = {str(interaction.user.id): 1}
+            date_time = datetime.strptime(f"{date} {time}", self.fmt)
+            if date_time < datetime.now():
+                await interaction.edit_original_response(
+                    content="Fehler! Der Termin darf nicht in der Vergangenheit liegen.")
+                return
+            await self.add_appointment(interaction.channel, interaction.user.id, date_time, reminder, title, attendees,
+                                       str(uuid.uuid4()), description, recurring)
+            await interaction.edit_original_response(content="Termin erfolgreich erstellt!")
+        except ValueError:
+            await interaction.edit_original_response(content="Fehler! Ungültiges Datums und/oder Zeit Format!")
+
+    def get_embed(self, title: str, organizer: int, description: str, date_time: datetime, reminder: int,
+                  recurring: int, attendees: Dict):
+        embed = Embed(title=title,
+                      description="Benutze die Buttons unter dieser Nachricht, um dich für Benachrichtigungen zu "
+                                  "diesem Termin an- bzw. abzumelden.",
+                      color=19607)
+
+        embed.add_field(name="Erstellt von", value=f"<@{organizer}>", inline=False)
+        if len(description) > 0:
+            embed.add_field(name="Beschreibung", value=description, 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)
+        embed.add_field(name=f"Teilnehmerinnen ({len(attendees)})",
+                        value=",".join([f"<@{attendee}>" for attendee in attendees.keys()]))
+
+        return embed
+
+    @app_commands.command(name="list", description="Listet alle Termine dieses Channels auf")
+    async def cmd_appointments(self, interaction: Interaction):
+        await interaction.response.defer(ephemeral=False)
+
+        if str(interaction.channel.id) in self.appointments:
+            channel_appointments = self.appointments.get(str(interaction.channel.id))
+            answer = f'Termine dieses Channels:\n'
+            delete = []
+
+            for message_id, appointment in channel_appointments.items():
+                try:
+                    message = await interaction.channel.fetch_message(int(message_id))
+                    answer += f'{appointment["date_time"]}: {appointment["title"]} => ' \
+                              f'{message.jump_url}\n'
+                except errors.NotFound:
+                    delete.append(message_id)
+
+            if len(delete) > 0:
+                for key in delete:
+                    channel_appointments.pop(key)
+                self.save_appointments()
+
+            await interaction.followup.send(answer, ephemeral=False)
+        else:
+            await interaction.followup.send("Für diesen Kanal existieren derzeit keine Termine.", ephemeral=True)
+
+    async def send_or_update_appointment(self, channel, organizer, description, title, date_time, reminder, recurring,
+                                         attendees, message=None):
+        embed = self.get_embed(title, organizer, description, date_time, reminder, recurring, attendees)
+        if message:
+            return await message.edit(embed=embed, view=AppointmentView(self))
+        else:
+            return await channel.send(embed=embed, view=AppointmentView(self))
+
+    async def update_legacy_appointments(self):
+        new_appointments = {}
+        for channel_id, appointments in self.appointments.items():
+            channel_appointments = {}
+            try:
+                channel = await self.bot.fetch_channel(int(channel_id))
+
+                for message_id, appointment in appointments.items():
+                    if appointment.get("attendees") is not None:
+                        continue
+                    try:
+                        message = await channel.fetch_message(int(message_id))
+                        title = appointment.get("title")
+                        date_time = appointment.get("date_time")
+                        reminder = appointment.get("reminder")
+                        recurring = appointment.get("recurring")
+                        author_id = appointment.get("author_id")
+                        description = ""
+                        attendees = {}
+                        ics_uuid = str(uuid.uuid4())
+
+                        for reaction in message.reactions:
+                            if reaction.emoji == "👍":
+                                async for user in reaction.users():
+                                    if user.id != self.bot.user.id:
+                                        attendees[str(user.id)] = 1
+
+                        dt = datetime.strptime(f"{date_time}", self.fmt)
+                        await self.send_or_update_appointment(channel, author_id, description, title, dt, reminder,
+                                                              recurring, attendees, message=message)
+                        channel_appointments[message_id] = {"date_time": date_time,
+                                                            "reminder": reminder,
+                                                            "title": title,
+                                                            "author_id": author_id,
+                                                            "recurring": recurring,
+                                                            "description": description,
+                                                            "attendees": attendees,
+                                                            "ics_uuid": ics_uuid}
+
+                    except:
+                        pass
+            except:
+                pass
+
+            if len(channel_appointments) > 0:
+                new_appointments[channel_id] = channel_appointments
+
+        self.appointments = new_appointments
+        self.save_appointments()
+
+
+async def setup(bot: commands.Bot) -> None:
+    appointments = Appointments(bot)
+    await bot.add_cog(appointments)
+    bot.add_view(AppointmentView(appointments))
+    await appointments.update_legacy_appointments()
diff --git a/cogs/components/module_information/scraper.py b/extensions/components/module_information/scraper.py
similarity index 100%
rename from cogs/components/module_information/scraper.py
rename to extensions/components/module_information/scraper.py
diff --git a/cogs/components/poll/poll.py b/extensions/components/poll/poll.py
similarity index 88%
rename from cogs/components/poll/poll.py
rename to extensions/components/poll/poll.py
index 89eb0a77780cfa2e803c62d39ea0a01bfaf3cae2..52ee24f47536fe44d5f85853e2ae0d0a63ba78c8 100644
--- a/cogs/components/poll/poll.py
+++ b/extensions/components/poll/poll.py
@@ -1,146 +1,151 @@
-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
+import discord
+import emoji
+from discord import TextChannel
+
+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, interaction, result=False, message=None):
+        option_ctr = 0
+        title = "Umfrage"
+        participants = {}
+
+        if result:
+            title += " Ergebnis"
+
+        if len(self.answers) > len(DEFAULT_OPTIONS):
+            await interaction.edit_original_response(
+                content=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:
+            if type(interaction) is TextChannel:
+                message = await interaction.send(embed=embed)
+            else:
+                await interaction.edit_original_response(embed=embed)
+                message = await interaction.original_response()
+
+        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/learninggroups.py b/extensions/learninggroups.py
similarity index 50%
rename from cogs/learninggroups.py
rename to extensions/learninggroups.py
index 030cbe3f45534572c707891e036fff891ffb61cb..0997cb0ec7d6a4a132a57168872baa251efae127 100644
--- a/cogs/learninggroups.py
+++ b/extensions/learninggroups.py
@@ -1,1020 +1,835 @@
-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
-
-"""
-  Umgebungsvariablen:
-  DISCORD_LEARNINGGROUPS_OPEN - Kategorie-ID der offenen Lerngruppen
-  DISCORD_LEARNINGGROUPS_CLOSE - Kategorie-ID der geschlossenen Lerngruppen
-  DISCORD_LEARNINGGROUPS_PRIVATE - Kategorie-ID der privaten Lerngruppen
-  DISCORD_LEARNINGGROUPS_ARCHIVE - Kategorie-ID der archivierten Lerngruppen
-  DISCORD_LEARNINGGROUPS_REQUEST - ID des Kanals, in dem Anfragen, die über den Bot gestellt wurden, eingetragen werden
-  DISCORD_LEARNINGGROUPS_INFO - ID des Kanals, in dem 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 (minimaler Inhalt: {})
-  DISCORD_MOD_ROLE - ID der Moderations-Rolle, die erweiterte Lerngruppen-Aktionen ausführen darf
-"""
-
-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 Lerngruppenkanäle beantragen und verwalten.",
-               "Hier kannst du Lerngruppenkanä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 = 1900
-
-        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 = {}  # organizer and learninggroup-member ids
-        self.channels = {}  # complete channel configs
-        self.header = {}  # headlines for status message
-        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_organizer(self, request, member):
-        return request["organizer_id"] == member.id
-
-    def is_group_organizer(self, channel, member):
-        channel_config = self.groups["groups"].get(str(channel.id))
-        if channel_config:
-            return channel_config["organizer_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 schränkt die Anzahl der Aufrufe für manche Funktionen ein, 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['organizer_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"In der Lerngruppenübersicht fehlen noch Überschriften für die folgenden Kurse: **{', '.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.update_permissions(channel)
-        await self.remove_group(channel)
-        await self.update_statusmessage()
-
-
-    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 has 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 has 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["organizer_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."
-                           "```"
-                           "Funktionen für Lerngruppenorganisatorinnen:\n"
-                           "!lg addmember <@newmember>: Fügt ein Mitglied zur Lerngruppe hinzu.\n"                           
-                           "!lg organizer <@neworganizer>: Ändert die Organisatorin der Lerngruppe auf @neworganizer.\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 organizer: Zeigt die Organisatorin der Lerngruppe an.\n"
-                           "!lg leave: Du verlässt die Lerngruppe.\n"
-                           "!lg join: Anfrage, um der Lerngruppe beizutreten.\n"
-                           "\nMit dem nachfolgenden Kommando kann eine Kommilitonin darum "
-                           "bitten in die Lerngruppe aufgenommen zu werden wenn die Gruppe privat ist.\n"
-                           f"!lg join {channel.id}"
-                            "\n(Manche Kommandos werden von Discord eingeschränkt und können nur einmal alle 5 Minuten ausgeführt werden.)"
-                           "```"
-                           )
-        self.groups["groups"][str(channel.id)] = {
-            "organizer_id": requested_channel_config["organizer_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:
-                try:
-                    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 Lerngruppenkanal. " 
-                                              "Diese Nachricht kannst du in unserer Unterhaltung mit Rechtsklick anpinnen, " 
-                                              "wenn du möchtest.")
-                except:
-                    pass
-
-        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
-
-        organizer = self.bot.get_user(group_config["organizer_id"])
-        if not organizer:
-            return overwrites
-
-        overwrites[organizer] = 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="Aktualisiert 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. "
-              "Der Name 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. Existiert die Kursnummer bereits, "
-                    "wird die Überschrift geä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 Lerngruppenkanal 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 Organisatorin der Lerngruppe eingesetzt."
-        },
-        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_organizer: disnake.Member):
-        state = self.arg_state_to_group_state(arg_state)
-        channel_config = {"organizer_id": arg_organizer.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 Lerngruppenkanal.",
-        example="!lg request 1142 mathegenies sose22 closed",
-        description=("Moderatorinnen können diese Anfrage bestätigen, dann wird die Gruppe eingerichtet. "
-                     "Die Organisatorin der Gruppe ist die Benutzerin, die die Anfrage gestellt 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) oder ob es sich um eine private Lerngruppe handelt (private)."
-        }
-    )
-    @cmd_lg.command(name="request", aliases=["r", "req"])
-    async def cmd_request_group(self, ctx, arg_course, arg_name, arg_semester, arg_state):
-
-        arg_state = re.sub(r"[^a-z0-9]", "", arg_state.lower())
-        arg_semester = re.sub(r"[^a-z0-9]", "", arg_semester.lower())
-
-        if re.match(r"(wise)|(sose)[0-9]+", arg_state) and re.match(r"(open)|(closed*)|(private)", arg_semester):
-            tmp = arg_state
-            arg_state = arg_semester
-            arg_semester = tmp
-
-        arg_semester = re.sub(r"[^wiseo0-9]", "", arg_semester)
-
-        arg_state = re.sub(r"[^a-z]", "", arg_state)
-
-        state = self.arg_state_to_group_state(arg_state)
-
-        arg_course = re.sub(r"[^0-9]", "", arg_course)
-        arg_course = re.sub(r"^0+", "", arg_course)
-
-        arg_name = re.sub(
-            r"[^A-Za-zäöüß0-9-]",
-            "",
-            arg_name.lower().replace(" ", "-")
-        )
-
-
-
-
-        if len(arg_semester) == 8:
-            arg_semester = f"{arg_semester[0:4]}{arg_semester[-2:]}"
-        channel_config = {"organizer_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 Lerngruppenkanal ausgeführt werden. "
-                     "Die Lerngruppe wird in der Übersicht der Lerngruppen aufgeführt, so dass Kommilitoninnen noch "
-                     "anfragen können, in die Lerngruppe aufgenommen zu werden."
-                     "Diese Aktion kann nur von der Organisatorin der Lerngruppe ausgeführt werden. ")
-    )
-    @cmd_lg.command(name="show")
-    async def cmd_show(self, ctx):
-        if self.is_group_organizer(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 Lerngruppenkanal ausgeführt werden. "
-                     "Die Lerngruppe wird nicht mehr in der Liste der Lerngruppen angezeigt. "
-                     "Diese Aktion kann nur von der Organisatorin der Lerngruppe ausgeführt werden. ")
-    )
-    @cmd_lg.command(name="hide")
-    async def cmd_hide(self, ctx):
-        if self.is_group_organizer(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 Lerngruppenkanal, wenn du die Organisatorin bist. ",
-        description=("Muss im betreffenden Lerngruppenkanal ausgeführt werden. "
-                     "Verschiebt den Lerngruppenkanal in die Kategorie für offene Kanäle und ändert das Icon. "
-                     "Diese Aktion kann nur von der Organisatorin der Lerngruppe ausgeführt werden. ")
-    )
-    @cmd_lg.command(name="open", aliases=["opened", "offen"])
-    async def cmd_open(self, ctx):
-        if self.is_group_organizer(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 Lerngruppenkanal, wenn du die Organisatorin bist. ",
-        description=("Muss im betreffenden Lerngruppenkanal 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 Organisatorin der Lerngruppe ausgeführt werden. ")
-    )
-    @cmd_lg.command(name="close", aliases=["closed", "geschlossen"])
-    async def cmd_close(self, ctx):
-        if self.is_group_organizer(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 Organisatorin bist. ",
-        description=("Muss im betreffenden Lerngruppenkanal 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 Organisatorin der Lerngruppe ausgeführt werden. ")
-    )
-    @cmd_lg.command(name="private", aliases=["privat"])
-    async def cmd_private(self, ctx):
-        if self.is_group_organizer(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 Lerngruppenkanals, in dem das Kommando 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 Lerngruppenkanal.",
-        description="Verschiebt den Lerngruppenkanal, 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 organizer <@usermention>",
-        example="!lg organizer @someuser",
-        brief="Bestimmt die Organisatorin eines Lerngruppenkanals.",
-        description="Muss im betreffenden Lerngruppenkanal ausgeführt werden. ",
-        parameters={
-            "@usermention": "Die neue Organisatorin der Lerngruppe."
-        }
-    )
-    @cmd_lg.command(name="organizer")
-    async def cmd_organizer(self, ctx, new_organizer: 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)]
-
-        organizer_id = group_config.get("organizer_id")
-
-        if not organizer_id:
-            return
-
-        if not new_organizer:
-                user = await self.bot.fetch_user(organizer_id)
-                await ctx.channel.send(f"Organisatorin: @{user.name}#{user.discriminator}")
-
-        elif isinstance(group_config, dict):
-            organizer = await self.bot.fetch_user(organizer_id)
-            if self.is_group_organizer(ctx.channel, ctx.author) or utils.is_mod(ctx):
-                group_config["organizer_id"] = new_organizer.id
-                await self.remove_member_from_group(ctx.channel, new_organizer, False)
-                if new_organizer != organizer:
-                    await self.add_member_to_group(ctx.channel, organizer, False)
-                await self.save_groups()
-                await self.update_permissions(ctx.channel)
-                await ctx.channel.send(
-                    f"Glückwunsch {new_organizer.mention}! Du bist jetzt die Organisatorin 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, zu 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 angefügt werden. `!lg addmember <@usermention> <#channel>`")
-                return
-            arg_channel = ctx.channel
-        if self.is_group_organizer(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="Zählt 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
-        organizer_id = group_config.get("organizer_id")
-
-        if not organizer_id:
-            return
-
-        organizer = await self.bot.fetch_user(organizer_id)
-        users = group_config.get("users", {})
-        if not users and not organizer:
-            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"Organisatorin: **@{organizer.name}#{organizer.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_organizer(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 Organisatorin einer Lerngruppe um Aufnahme an.",
-        parameters={
-            "id": "Die ID der 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"<@!{group_config['organizer_id']}>, du wirst gebraucht. 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 Organisatorin 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_organizer(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["organizer_id"] == ctx.author.id:
-            await ctx.channel.send("Du kannst nicht aus deiner eigenen Lerngruppe flüchten. Gib erst die Verantwortung ab.")
-            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_organizer(request, member) or self.is_mod(member)):
-                if self.is_mod(member):
-                    user = await self.bot.fetch_user(request["organizer_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_organizer(channel, member) or self.is_mod(member):
-            if confirmed:
-                if message.mentions and len(message.mentions) == 2:
-                    await self.add_member_to_group(channel, message.mentions[1])
-                    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
-
+import copy
+import enum
+import json
+import os
+import re
+import time
+from enum import Enum
+
+import discord
+from discord import InteractionMessage, app_commands, Interaction
+from discord.ext import commands
+from discord.ui import Button
+
+import utils
+
+"""
+  Umgebungsvariablen:
+  DISCORD_LEARNINGGROUPS_OPEN - Kategorie-ID der offenen Lerngruppen
+  DISCORD_LEARNINGGROUPS_CLOSE - Kategorie-ID der geschlossenen Lerngruppen
+  DISCORD_LEARNINGGROUPS_PRIVATE - Kategorie-ID der privaten Lerngruppen
+  DISCORD_LEARNINGGROUPS_ARCHIVE - Kategorie-ID der archivierten Lerngruppen
+  DISCORD_LEARNINGGROUPS_REQUEST - ID des Kanals, in dem Anfragen, die über den Bot gestellt wurden, eingetragen werden
+  DISCORD_LEARNINGGROUPS_INFO - ID des Kanals, in dem 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 (minimaler Inhalt: {})
+  DISCORD_MOD_ROLE - ID der Moderations-Rolle, die erweiterte Lerngruppen-Aktionen ausführen darf
+"""
+
+LG_OPEN_SYMBOL = f'🌲'
+LG_CLOSE_SYMBOL = f'🛑'
+LG_PRIVATE_SYMBOL = f'🚪'
+LG_LISTED_SYMBOL = f'📖'
+
+
+class LearningGroupState(enum.Enum):
+    open = "open"
+    private = "private"
+
+
+class GroupState(Enum):
+    OPEN = "OPEN"
+    CLOSED = "CLOSED"
+    PRIVATE = "PRIVATE"
+    ARCHIVED = "ARCHIVED"
+    REMOVED = "REMOVED"
+
+
+@app_commands.guild_only()
+class LearningGroups(commands.GroupCog, name="lg", description="Lerngruppenverwaltung."):
+    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 = 1900
+
+        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 = {}  # organizer and learninggroup-member ids
+        self.channels = {}  # complete channel configs
+        self.header = {}  # headlines for status message
+        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: LearningGroupState):
+        if state == LearningGroupState.open:
+            return GroupState.OPEN
+        if state == LearningGroupState.private:
+            return GroupState.PRIVATE
+        return None
+
+    def is_request_organizer(self, request, member):
+        return request["organizer_id"] == member.id
+
+    def is_group_organizer(self, channel, member):
+        channel_config = self.groups["groups"].get(str(channel.id))
+        if channel_config:
+            return channel_config["organizer_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!"
+
+    def is_channel_config_valid(self, channel_config, command):
+        if channel_config['state'] is None:
+            return False, f"Fehler! Bitte gib an ob die Gruppe **offen** (**open**) **geschlossen** (**closed**) oder **privat** (**private**) ist. Gib `!help {command}` für Details ein."
+        if not re.match(r"^(sose|wise)[0-9]{2}$", channel_config['semester']):
+            return False, f"Fehler! Das Semester muss mit **sose** oder **wise** angegeben werden gefolgt von der **zweistelligen Jahreszahl**. Gib `!help {command}` für Details ein."
+        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 schränkt die Anzahl der Aufrufe für manche Funktionen ein, 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['organizer_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"In der Lerngruppenübersicht fehlen noch Überschriften für die folgenden Kurse: **{', '.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.update_permissions(channel)
+        await self.remove_group(channel)
+        await self.update_statusmessage()
+
+    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 has 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 has 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["organizer_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."
+                           "```"
+                           "Funktionen für Lerngruppenorganisatorinnen:\n"
+                           "!lg addmember <@newmember>: Fügt ein Mitglied zur Lerngruppe hinzu.\n"
+                           "!lg organizer <@neworganizer>: Ändert die Organisatorin der Lerngruppe auf @neworganizer.\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 organizer: Zeigt die Organisatorin der Lerngruppe an.\n"
+                           "!lg leave: Du verlässt die Lerngruppe.\n"
+                           "!lg join: Anfrage, um der Lerngruppe beizutreten.\n"
+                           "\nMit dem nachfolgenden Kommando kann eine Kommilitonin darum "
+                           "bitten in die Lerngruppe aufgenommen zu werden wenn die Gruppe privat ist.\n"
+                           f"!lg join {channel.id}"
+                           "\n(Manche Kommandos werden von Discord eingeschränkt und können nur einmal alle 5 Minuten ausgeführt werden.)"
+                           "```"
+                           )
+        self.groups["groups"][str(channel.id)] = {
+            "organizer_id": requested_channel_config["organizer_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: discord.TextChannel, arg_member: discord.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:
+                try:
+                    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 Lerngruppenkanal. "
+                                              "Diese Nachricht kannst du in unserer Unterhaltung mit Rechtsklick anpinnen, "
+                                              "wenn du möchtest.")
+                except:
+                    pass
+
+        group_config["users"] = users
+
+        await self.save_groups()
+
+    async def remove_member_from_group(self, channel: discord.TextChannel, arg_member: discord.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: discord.PermissionOverwrite(read_messages=True),
+            guild.default_role: discord.PermissionOverwrite(read_messages=False)
+        }
+
+        if not group_config:
+            return overwrites
+
+        organizer = self.bot.get_user(group_config["organizer_id"])
+        if not organizer:
+            return overwrites
+
+        overwrites[organizer] = discord.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] = discord.PermissionOverwrite(read_messages=True)
+
+        return overwrites
+
+    @app_commands.command(name="update", description="Aktualisiert die Lerngruppenliste")
+    @app_commands.checks.has_role("Mod")
+    async def cmd_update(self, interaction: Interaction):
+        await interaction.response.send_message("Update der Lerngruppenliste gestartet...")
+        await self.update_channels()
+        await self.update_statusmessage()
+        await interaction.edit_original_response(content="Update der Lerngruppenliste abgeschlossen!")
+
+    @app_commands.command(name="header",
+                          description="Fügt einen Kurs als neue Überschrift in Botys Lerngruppen-Liste hinzu.")
+    @app_commands.describe(
+        course="Nummer des Kurses wie von der Fernuni angegeben (ohne führende Nullen z. B. 1142).",
+        name="Ein frei wählbarer Text (darf Leerzeichen enthalten).")
+    @app_commands.checks.has_role("Mod")
+    async def cmd_add_header(self, interaction: Interaction, course: int, name: str):
+        await interaction.response.defer()
+
+        self.header[course] = f"{course} - {name}"
+        self.save_header()
+        await self.update_statusmessage()
+        await interaction.edit_original_response(content=f"Überschrift {name} für Kurs {course} hinzugefügt.")
+
+    @app_commands.command(name="request", description="Stellt eine Anfrage für einen neuen Lerngruppenkanal.")
+    @app_commands.describe(
+        course="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)",
+        state="Gibt an ob die Lerngruppe für weitere Lernwillige geöffnet ist (open) oder nicht (closed) oder ob es sich um eine private Lerngruppe handelt (private).")
+    async def cmd_request_group(self, interaction: Interaction, course: int, name: str, semester: str,
+                                state: LearningGroupState):
+        await interaction.response.defer(ephemeral=True)
+        arg_semester = re.sub(r"[^wiseo0-9]", "", semester)
+        state = self.arg_state_to_group_state(state)
+        name = re.sub(r"[^A-Za-zäöüß0-9-]", "", name.lower().replace(" ", "-"))
+
+        if len(arg_semester) == 8:
+            arg_semester = f"{arg_semester[0:4]}{arg_semester[-2:]}"
+        channel_config = {"organizer_id": interaction.user.id, "course": course, "name": name,
+                          "semester": arg_semester,
+                          "state": state, "is_listed": False}
+
+        is_valid, error = self.is_channel_config_valid(channel_config, interaction.command.name)
+        if not is_valid:
+            await interaction.edit_original_response(content=error)
+            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"{interaction.user.mention} 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()
+        await interaction.edit_original_response(content="Deine Lerngruppenanfrage wurde an die Moderatorinnen zur "
+                                                         "Genehmigung weitergeleitet. Du erhältst eine Nachricht, "
+                                                         "wenn über deine Anfrage entschieden wurde.")
+
+    @app_commands.command(name="add",
+                          description="Fügt einen Lerngruppenkanal hinzu. Der Name darf keine Leerzeichen enthalten.")
+    @app_commands.describe(
+        course="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).",
+        state="Gibt an ob die Lerngruppe für weitere Lernwillige geöffnet ist (open) oder nicht (private).",
+        organizer="Die so erwähnte Benutzerin wird als Organisatorin der Lerngruppe eingesetzt.")
+    @commands.check(utils.is_mod)
+    async def cmd_add_group(self, interaction: Interaction, course: int, name: str, semester: str,
+                            state: LearningGroupState, organizer: discord.Member):
+        await interaction.response.defer(ephemeral=True)
+        state = self.arg_state_to_group_state(state)
+        channel_config = {"organizer_id": organizer.id, "course": course, "name": name,
+                          "semester": semester,
+                          "state": state, "is_listed": False}
+
+        is_valid, error = await self.is_channel_config_valid(channel_config, interaction.command.name)
+        if not is_valid:
+            await interaction.edit_original_response(content=error)
+            return
+
+        self.groups["requested"][str(interaction.message.id)] = channel_config
+        await self.save_groups()
+        await self.add_requested_group_channel(interaction.message.id, direct=True)
+
+    @app_commands.command(name="show", description="Zeigt einen privaten Lerngruppenkanal trotzdem in der Liste an.")
+    async def cmd_show(self, interaction: Interaction):
+        await interaction.response.defer(ephemeral=True)
+        if self.is_group_organizer(interaction.channel, interaction.user) or utils.is_mod(interaction):
+            channel_config = self.channels[str(interaction.channel.id)]
+            if channel_config:
+                if channel_config.get("state") == GroupState.PRIVATE:
+                    if await self.set_channel_listing(interaction.channel, True):
+                        await interaction.edit_original_response(
+                            content="Die Lerngruppe wird nun in der Lerngruppenliste angezeigt.")
+                elif channel_config.get("state") == GroupState.OPEN:
+                    await interaction.edit_original_response(
+                        content="Nichts zu tun. Offene Lerngruppen werden sowieso in der Liste angezeigt.")
+                elif channel_config.get("state") == GroupState.CLOSED:
+                    await interaction.edit_original_response(
+                        content="Möchtest du die Gruppen öffnen? Versuch's mit `!lg open`")
+
+    @app_commands.command(name="hide", description="Versteckt einen privaten Lerngruppenkanal.")
+    async def cmd_hide(self, interaction: Interaction):
+        await interaction.response.defer(ephemeral=True)
+        if self.is_group_organizer(interaction.channel, interaction.user) or utils.is_mod(interaction):
+            channel_config = self.channels[str(interaction.channel.id)]
+            if channel_config:
+                if channel_config.get("state") == GroupState.PRIVATE:
+                    if await self.set_channel_listing(interaction.channel, False):
+                        await interaction.edit_original_response(
+                            content="Die Lerngruppe wird nun nicht mehr in der Lerngruppenliste angezeigt.")
+                    return
+
+                elif channel_config.get("state") == GroupState.OPEN:
+                    await interaction.edit_original_response(
+                        content="Offene Lerngruppen können nicht aus der Lerngruppenliste entfernt werden. "
+                                "Führe `!lg private`, um diese auf privat zu schalten.")
+                elif channel_config.get("state") == GroupState.CLOSED:
+                    await interaction.edit_original_response(content=
+                                                             "Wenn diese Gruppe privat werden soll, ist das Kommando das du brauchst: `!lg private`")
+
+    @app_commands.command(name="debug", description="Irgendwelche Debug-Kacke")
+    @app_commands.checks.has_role("Mod")
+    async def cmd_debug(self, interaction: Interaction):
+        await interaction.response.defer(ephemeral=True)
+        channel_config = self.channels[str(interaction.channel.id)]
+        if not channel_config:
+            await interaction.edit_original_response(content="None")
+            return
+        await interaction.edit_original_response(content=str(channel_config))
+
+    @app_commands.command(name="open", description="Öffnet den Lerngruppenkanal, wenn du die Organisatorin bist.")
+    async def cmd_open(self, interaction: Interaction):
+        await interaction.response.defer(ephemeral=True)
+        if self.is_group_organizer(interaction.channel, interaction.user) or utils.is_mod(interaction):
+            await self.set_channel_state(interaction.channel, state=GroupState.OPEN)
+        await interaction.edit_original_response(content="Die Lerngruppe wurde geöffnet.")
+
+    @app_commands.command(name="private",
+                          description="Macht aus deiner Lerngruppe eine private Lerngruppe, wenn du die Organisatorin bist.")
+    async def cmd_private(self, interaction: Interaction):
+        await interaction.response.defer(ephemeral=True)
+        if self.is_group_organizer(interaction.channel, interaction.user) or utils.is_mod(interaction):
+            if await self.set_channel_state(interaction.channel, state=GroupState.PRIVATE):
+                await self.update_permissions(interaction.channel)
+        await interaction.edit_original_response(content="Die Lerngruppe wurde geöffnet.")
+
+    @app_commands.command(name="organizer", description="Bestimmt die Organisatorin eines Lerngruppenkanals.")
+    @app_commands.describe(new_organizer="Die neue Organisatorin der Lerngruppe.")
+    async def cmd_organizer(self, interaction: Interaction, new_organizer: discord.Member = None):
+        await interaction.response.defer(defer=True)
+        group_config = self.groups["groups"].get(str(interaction.channel.id))
+
+        if not group_config:
+            self.groups["groups"][str(interaction.channel.id)] = {}
+            group_config = self.groups["groups"][str(interaction.channel.id)]
+
+        organizer_id = group_config.get("organizer_id")
+
+        if not organizer_id:
+            return
+
+        if not new_organizer:
+            user = await self.bot.fetch_user(organizer_id)
+            await interaction.edit_original_response(content=f"Organisatorin: @{user.name}#{user.discriminator}")
+        elif isinstance(group_config, dict):
+            organizer = await self.bot.fetch_user(organizer_id)
+            if self.is_group_organizer(interaction.channel, interaction.user) or utils.is_mod(interaction):
+                group_config["organizer_id"] = new_organizer.id
+                await self.remove_member_from_group(interaction.channel, new_organizer, False)
+                if new_organizer != organizer:
+                    await self.add_member_to_group(interaction.channel, organizer, False)
+                await self.save_groups()
+                await self.update_permissions(interaction.channel)
+                await interaction.edit_original_response(content=
+                                                         f"Glückwunsch {new_organizer.mention}! Du bist jetzt die Organisatorin dieser Lerngruppe.")
+
+    @app_commands.command(name="add-member", description="Fügt eine Benutzerin zu einer Lerngruppe hinzu.")
+    @app_commands.describe(member="Die so erwähnte Benutzerin wird zur Lerngruppe hinzugefügt.",
+                           channel="Der Kanal, zu dem die Benutzerin hinzugefügt werden soll.")
+    async def cmd_add_member(self, interaction: Interaction, member: discord.Member,
+                             channel: discord.TextChannel = None):
+        await interaction.response.defer(ephemeral=True)
+        if not channel:
+            if not self.channels.get(str(interaction.channel.id)):
+                await interaction.edit_original_response(content="Wenn das Kommando außerhalb eines Lerngruppenkanals "
+                                                                 "aufgerufen wird, muss der Lerngruppenkanal angefügt "
+                                                                 "werden. `!lg addmember <@usermention> <#channel>`")
+                return
+            channel = interaction.channel
+        if self.is_group_organizer(channel, interaction.user) or utils.is_mod(interaction):
+            await self.add_member_to_group(channel, member)
+            await self.update_permissions(channel)
+        await interaction.edit_original_response(content=f"{member.mention} wurde der Lerngruppe hinzugefügt.")
+
+    @app_commands.command(name="remove-member", description="Entfernt eine Benutzerin aus einer Lerngruppe.")
+    @app_commands.describe(member="Die so erwähnte Benutzerin wird aus der Lerngruppe entfernt.",
+                           channel="Der Kanal, aus dem die Benutzerin gelöscht werden soll.")
+    @app_commands.checks.has_role("Mod")
+    async def cmd_remove_member(self, interaction: Interaction, member: discord.Member,
+                                channel: discord.TextChannel):
+        await interaction.response.defer(ephemeral=True)
+        await self.remove_member_from_group(channel, member)
+        await self.update_permissions(channel)
+        await interaction.edit_original_response(content=f"{member.mention} wurde aus der Lerngruppe entfernt.")
+
+    @app_commands.command(name="members", description="Zählt die Mitglieder der Lerngruppe auf.")
+    async def cmd_members(self, interaction: Interaction):
+        await interaction.response.defer(ephemeral=True)
+        group_config = self.groups["groups"].get(str(interaction.channel.id))
+        if not group_config:
+            await interaction.edit_original_response(content="Das ist kein Lerngruppenkanal.")
+            return
+        organizer_id = group_config.get("organizer_id")
+
+        if not organizer_id:
+            await interaction.edit_original_response(
+                content="Scheinbar hat die Gruppe aus irgendnem Grund keinen Organizer. Keine Ahnung... ")
+            return
+
+        organizer = await self.bot.fetch_user(organizer_id)
+        users = group_config.get("users", {})
+        if not users and not organizer:
+            await interaction.edit_original_response(content="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 interaction.edit_original_response(
+            content=f"Organisatorin: **@{organizer.name}#{organizer.discriminator}**\nMitglieder: " +
+                    (f"{', '.join(names)}" if len(names) > 0 else "Keine"))
+
+    @app_commands.command(name="id", description="Zeigt die ID für deine Lerngruppe an.")
+    async def cmd_id(self, interaction: Interaction):
+        await interaction.response.defer(ephemeral=True)
+        if self.is_group_organizer(interaction.channel, interaction.user) or utils.is_mod(interaction):
+            group_config = self.groups["groups"].get(str(interaction.channel.id))
+            if not group_config:
+                await interaction.edit_original_response(content="Das ist kein Lerngruppenkanal.")
+                return
+        await interaction.edit_original_response(
+            content=f"Die ID dieser Lerngruppe lautet: `{str(interaction.channel.id)}`.\n"
+                    f"Beitrittsanfrage mit: `!lg join {str(interaction.channel.id)}`")
+
+    @app_commands.command(name="join", description="Fragt bei der Organisatorin einer Lerngruppe um Aufnahme an.")
+    @app_commands.describe(id_or_channel="Die ID der Lerngruppe.")
+    async def cmd_join(self, interaction: Interaction, id_or_channel: discord.TextChannel = None):
+        await interaction.response.defer(ephemeral=True)
+        if id_or_channel is None:
+            id_or_channel = interaction.channel
+
+        cid = id_or_channel.id if type(id_or_channel) is discord.TextChannel else id_or_channel
+
+        group_config = self.groups["groups"].get(str(cid))
+        if not group_config:
+            await interaction.edit_original_response(content="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"<@!{interaction.author.id}> möchte gerne der Lerngruppe **#{channel.name}** beitreten.",
+            message=f"<@!{group_config['organizer_id']}>, du wirst gebraucht. Anfrage von <@!{interaction.author.id}>:",
+            custom_prefix="learninggroups:join"
+        )
+        await utils.send_dm(interaction.author, f"Deine Anfrage wurde an **#{channel.name}** gesendet. "
+                                                "Sobald die Organisatorin der Lerngruppe darüber "
+                                                "entschieden hat, bekommst du Bescheid.")
+
+    @app_commands.command(name="kick", description="Wirft @usermention aus der Gruppe.")
+    @app_commands.describe(member="Mitglied, dass du aus der Gruppe werfen möchtest")
+    async def cmd_kick(self, interaction: Interaction, member: discord.Member):
+        await interaction.response.defer(ephemeral=True)
+        if self.is_group_organizer(interaction.channel, interaction.user) or utils.is_mod(interaction):
+            group_config = self.groups["groups"].get(str(interaction.channel.id))
+            if not group_config:
+                await interaction.edit_original_response(content="Das ist keine gültiger Lerngruppenkanal.")
+                return
+
+            await self.remove_member_from_group(interaction.channel, member)
+            await self.update_permissions(interaction.channel)
+
+        await interaction.edit_original_response(content=f"{member.mention} wurde aus der Gruppe geworfen.")
+
+    @app_commands.command(name="leave", description="Du verlässt die Lerngruppe.")
+    async def cmd_leave(self, interaction: Interaction):
+        await interaction.response.defer(ephemeral=True)
+        group_config = self.groups["groups"].get(str(interaction.channel.id))
+        if not group_config:
+            await interaction.edit_original_response(content="Das ist keine gültiger Lerngruppenkanal.")
+            return
+
+        if group_config["organizer_id"] == interaction.author.id:
+            await interaction.edit_original_response(content=
+                                                     "Du kannst nicht aus deiner eigenen Lerngruppe flüchten. Gib erst die Verantwortung ab.")
+            return
+
+        await self.remove_member_from_group(interaction.channel, interaction.user)
+        await self.update_permissions(interaction.channel)
+        await interaction.edit_original_response(content="Du hast die Gruppe verlassen.")
+
+    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_organizer(request, member) or self.is_mod(member)):
+                if self.is_mod(member):
+                    user = await self.bot.fetch_user(request["organizer_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_organizer(channel, member) or self.is_mod(member):
+            if confirmed:
+                if message.mentions and len(message.mentions) == 2:
+                    await self.add_member_to_group(channel, message.mentions[1])
+                    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 setup(bot: commands.Bot) -> None:
+    await bot.add_cog(LearningGroups(bot))
diff --git a/extensions/links.py b/extensions/links.py
new file mode 100644
index 0000000000000000000000000000000000000000..47fde02cbb93f0c81b45569d645f04fcfdaf1a99
--- /dev/null
+++ b/extensions/links.py
@@ -0,0 +1,110 @@
+import discord
+from discord import app_commands, Interaction
+from discord.ext import commands
+
+import models
+from modals.link_modal import LinkModal, LinkCategoryModal
+
+
+@app_commands.guild_only()
+class Links(commands.GroupCog, name="links", description="Linkverwaltung für Kanäle."):
+    def __init__(self, bot):
+        self.bot = bot
+
+    @app_commands.command(name="show", description="Zeige Links für diesen Kanal an.")
+    @app_commands.describe(category="Zeige nur Links für diese Kategorie an.", public="Zeige die Linkliste für alle.")
+    async def cmd_show(self, interaction: Interaction, category: str = None, public: bool = False):
+        await interaction.response.defer(ephemeral=not public)
+
+        embed = discord.Embed(title=f"Links")
+        if not models.LinkCategory.has_links(interaction.channel_id):
+            embed.description = "Für diesen Channel sind noch keine Links hinterlegt."
+        if category and not models.LinkCategory.has_links(interaction.channel_id, category=category):
+            embed.description = f"Für die Kategorie `{category}` sind in diesem Channel keine Links hinterlegt. " \
+                                f"Versuch es noch mal mit einer anderen Kategorie, oder lass dir mit `/links show` " \
+                                f"alle Links in diesem Channel ausgeben."
+
+        for category in models.LinkCategory.get_categories(interaction.channel_id, category=category):
+            if category.links.count() > 0:
+                category.append_field(embed)
+            else:
+                category.delete_instance()
+
+        await interaction.edit_original_response(embed=embed)
+
+    @app_commands.command(name="add", description="Füge einen neuen Link hinzu.")
+    async def cmd_add(self, interaction: Interaction):
+        await interaction.response.send_modal(LinkModal())
+
+    @app_commands.command(name="edit-link", description="Einen bestehenden Link in der Liste bearbeiten.")
+    @app_commands.describe(category="Kategorie zu der der zu bearbeitende Link gehört.",
+                           title="Titel des zu bearbeitenden Links.")
+    async def cmd_edit_link(self, interaction: Interaction, category: str, title: str):
+        if db_category := models.LinkCategory.get_or_none(models.LinkCategory.channel == interaction.channel_id,
+                                                          models.LinkCategory.name == category):
+            if link := models.Link.get_or_none(models.Link.title == title, models.Link.category == db_category.id):
+                await interaction.response.send_modal(
+                    LinkModal(category=link.category.name, link_title=link.title, link=link.link, link_id=link.id,
+                              title="Link bearbeiten"))
+            else:
+                await interaction.response.send_message(content='Ich konnte den Link leider nicht finden.',
+                                                        ephemeral=True)
+        else:
+            await interaction.response.send_message(content='Ich konnte die Kategorie leider nicht finden.',
+                                                    ephemeral=True)
+
+    @app_commands.command(name="rename-category", description="Kategorie bearbeiten.")
+    @app_commands.describe(category="Zu bearbeitende Kategorie")
+    async def cmd_rename_category(self, interaction: Interaction, category: str):
+        if not models.LinkCategory.has_links(interaction.channel_id):
+            await interaction.response.send_message(content="Für diesen Channel sind noch keine Links hinterlegt.",
+                                                    ephemeral=True)
+            return
+
+        if db_category := models.LinkCategory.get_or_none(models.LinkCategory.channel == interaction.channel_id,
+                                                          models.LinkCategory.name == category):
+            await interaction.response.send_modal(LinkCategoryModal(db_category=db_category))
+        else:
+            await interaction.response.send_message(content='Ich konnte das Thema leider nicht finden.', ephemeral=True)
+
+    @app_commands.command(name="remove-link", description="Einen Link entfernen.")
+    @app_commands.describe(topic="Theme zu dem der zu entfernende Link gehört.",
+                           title="Titel des zu entfernenden Links.")
+    async def cmd_remove_link(self, interaction: Interaction, topic: str, title: str):
+        await interaction.response.defer(ephemeral=True)
+        topic = topic.lower()
+
+        if not models.LinkCategory.has_links(interaction.channel_id):
+            await interaction.edit_original_response(content="Für diesen Channel sind noch keine Links hinterlegt.")
+            return
+        if topic_entity := models.LinkCategory.get_or_none(models.LinkCategory.channel == interaction.channel_id,
+                                                           models.LinkCategory.name == topic):
+            if link := models.Link.get_or_none(models.Link.title == title, models.Link.topic == topic_entity.id):
+                link.delete_instance(recursive=True)
+                await interaction.edit_original_response(content=f'Link {title} entfernt')
+            else:
+                await interaction.edit_original_response(content='Ich konnte den Link leider nicht finden.')
+        else:
+            await interaction.edit_original_response(content='Ich konnte das Thema leider nicht finden.')
+            return
+
+    @app_commands.command(name="remove-topic", description="Ein Thema mit allen zugehörigen Links entfernen.")
+    @app_commands.describe(topic="Zu entfernendes Thema.")
+    async def cmd_remove_topic(self, interaction: Interaction, topic: str):
+        await interaction.response.defer(ephemeral=True)
+        topic = topic.lower()
+
+        if not models.LinkCategory.has_links(interaction.channel_id):
+            await interaction.edit_original_response(content="Für diesen Channel sind noch keine Links hinterlegt.")
+            return
+        if topic_entity := models.LinkCategory.get_or_none(models.LinkCategory.channel == interaction.channel_id,
+                                                           models.LinkCategory.name == topic):
+            topic_entity.delete_instance(recursive=True)
+            await interaction.edit_original_response(content=f'Thema {topic} mit allen zugehörigen Links entfernt')
+        else:
+            await interaction.edit_original_response(content='Ich konnte das Thema leider nicht finden.')
+            return
+
+
+async def setup(bot: commands.Bot) -> None:
+    await bot.add_cog(Links(bot))
diff --git a/cogs/support.py b/extensions/mod_mail.py
similarity index 51%
rename from cogs/support.py
rename to extensions/mod_mail.py
index 1a8f182f66b27f26eebea115816d874055fc8d60..f8896f9bee028099da11e17ad01ac512f8db53d2 100644
--- a/cogs/support.py
+++ b/extensions/mod_mail.py
@@ -1,28 +1,35 @@
-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)
+import io
+import os
+
+import discord
+from discord.ext import commands
+
+
+class ModMail(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)
+            await message.channel.send("Vielen Dank für deine Nachricht. Ich habe deine Nachricht an das Mod-Team "
+                                       "weitergeleitet. Falls dir dich mit einer Frage oder einem Problem an mich "
+                                       "gewandt hast, wird sich so schnell wie möglich jemand bei dir melden.")
+
+
+async def setup(bot: commands.Bot) -> None:
+    await bot.add_cog(ModMail(bot))
diff --git a/cogs/module_information.py b/extensions/module_information.py
similarity index 50%
rename from cogs/module_information.py
rename to extensions/module_information.py
index 0b982c4ef72cbe1b93b9c34ad235c6fd65944bd2..2be073a26972961a1e5e5c8494d5795efa45b30d 100644
--- a/cogs/module_information.py
+++ b/extensions/module_information.py
@@ -1,408 +1,339 @@
-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)
+import enum
+import json
+import os
+import re
+
+import discord
+from discord import app_commands, Interaction
+from discord.ext import commands, tasks
+
+from extensions.components.module_information.scraper import Scraper
+
+
+class ModuleInformationNotFoundError(Exception):
+    pass
+
+
+class NoCourseChannelError(Exception):
+    pass
+
+
+class NoCourseOfStudyError(Exception):
+    pass
+
+
+class Topics(enum.Enum):
+    info = 1
+    handbuch = 2
+    leseprobe = 3
+    aufwand = 4
+    mentoriate = 5
+    pruefungen = 6
+
+
+class CoursesOfStudy(enum.Enum):
+    bainf = "bainf"
+    bamath = "bamath"
+    bscmatse = "bscmatse"
+    bawiinf = "bawiinf"
+    mscma = "mscma"
+    mscinf = "mscinf"
+    mawiinf = "mawiinf"
+    mscprinf = "mscprinf"
+    mscds = "mscds"
+
+
+"""
+  Environment Variablen:
+  DISCORD_MODULE_COURSE_FILE - Datei mit Studiengangsinformationen
+  DISCORD_MODULE_DATA_FILE - In dieser Datei werden die gescrappten Daten gespeichert
+"""
+
+
+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, interaction: Interaction, arg_stg, subcommand=None):
+        try:
+            module = await self.find_module(interaction.user, interaction.channel, arg_stg)
+            embed = await subcommand(module)
+            await interaction.edit_original_response(embed=embed)
+        except NoCourseOfStudyError:
+            shorts = []
+            for course_of_studies in self.data:
+                shorts.append(f"`{course_of_studies['short']}`")
+            await interaction.edit_original_response(content=
+                                                     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 interaction.edit_original_response(content=e.args[0])
+            else:
+                await interaction.edit_original_response(
+                    content="Leider konnte ich keine Informationen zu diesem Modul/Kurs finden.")
+
+            return None
+
+    async def get_stg_short(self, user, stg):
+        if not stg:
+            stg = await self.get_stg_short_from_role(user)
+        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, user, channel, arg_stg):
+        short = await self.get_stg_short(user, arg_stg)
+        number = self.number_of_channel(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, 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
+
+        return discord.Embed(title=title,
+                             description=desc,
+                             color=19607)
+
+    async def handbook(self, module):
+        try:
+            return await self.download_for("Modulhandbuch", module)
+        except ModuleInformationNotFoundError:
+            raise ModuleInformationNotFoundError("Leider habe ich kein Modulhandbuch gefunden.")
+
+    async def reading_sample(self, module):
+        try:
+            return await self.download_for("Leseprobe", module)
+        except ModuleInformationNotFoundError:
+            raise ModuleInformationNotFoundError("Leider habe ich keine Leseprobe gefunden.")
+
+    async def info(self, 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)
+        return discord.Embed(title=f"Modul {data['title']}",
+                             description=desc,
+                             color=19607)
+
+    async def load(self, 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)
+        return discord.Embed(title=f"Arbeitsaufwand",
+                             description=desc,
+                             color=19607)
+
+    async def support(self, 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)
+        return discord.Embed(title=f"Mentoriate ",
+                             description=desc,
+                             color=19607)
+
+    async def exams(self, 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)
+
+        return discord.Embed(title=f"Prüfungsinformationen",
+                             description=desc,
+                             color=19607)
+
+    @app_commands.command(name="module",
+                          description="Erhalte Informationen zu einem Kurs/Modul, abhängig von deinem Studiengang")
+    @app_commands.describe(public="Zeige die Ausgabe des Commands öffentlich, für alle Mitglieder sichtbar.",
+                           topic="Welche speziellen Informationen interessieren dich?",
+                           stg="Der Studiengang, für den die Informationen angezeigt werden sollen.")
+    async def cmd_module(self, interaction: Interaction, public: bool, topic: Topics = None,
+                         stg: CoursesOfStudy = None):
+        await interaction.response.defer(ephemeral=not public)
+
+        if topic == Topics.handbuch:
+            await self.execute_subcommand(interaction, stg, self.handbook)
+        elif topic == Topics.leseprobe:
+            await self.execute_subcommand(interaction, stg, self.reading_sample)
+        elif topic == Topics.aufwand:
+            await self.execute_subcommand(interaction, stg, self.load)
+        elif topic == Topics.mentoriate:
+            await self.execute_subcommand(interaction, stg, self.support)
+        elif topic == Topics.pruefungen:
+            await self.execute_subcommand(interaction, stg, self.exams)
+        else:
+            await self.execute_subcommand(interaction, stg, self.info)
+
+
+async def setup(bot: commands.Bot) -> None:
+    await bot.add_cog(ModuleInformation(bot))
diff --git a/cogs/news.py b/extensions/news.py
similarity index 94%
rename from cogs/news.py
rename to extensions/news.py
index ec49112821ef3ce10474b37c72a0c5091f66b4c8..aed4424fce4532c4b094dffd60c19ca7fbd575af 100644
--- a/cogs/news.py
+++ b/extensions/news.py
@@ -1,55 +1,59 @@
-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()
+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()
+
+
+async def setup(bot: commands.Bot) -> None:
+    await bot.add_cog(News(bot))
diff --git a/extensions/polls.py b/extensions/polls.py
new file mode 100644
index 0000000000000000000000000000000000000000..d0ae1194df504cf5e23b108b345c788c3ef018f3
--- /dev/null
+++ b/extensions/polls.py
@@ -0,0 +1,112 @@
+import enum
+import json
+
+import discord
+import emoji
+from discord import app_commands, Interaction
+from discord.ext import commands
+
+from views.poll_view import PollView
+
+DEFAULT_CHOICES = ["🇦", "🇧", "🇨", "🇩", "🇪", "🇫", "🇬", "🇭", "🇮", "🇯", "🇰", "🇱", "🇲", "🇳", "🇴", "🇵", "🇶",
+                   "🇷", "🇸", "🇹"]
+
+
+class PollType(enum.Enum):
+    single_choice = "single"
+    multiple_choice = "multiple"
+
+
+@app_commands.guild_only()
+class Polls(commands.GroupCog, name="poll", description="Handle Polls in Channels"):
+    def __init__(self, bot):
+        self.bot = bot
+        self.polls = {}
+        self.load()
+
+    def load(self):
+        try:
+            with open("data/polls.json", "r") as polls_file:
+                self.polls = json.load(polls_file)
+        except FileNotFoundError:
+            pass
+
+    def save(self):
+        with open("data/polls.json", "w") as polls_file:
+            json.dump(self.polls, polls_file)
+
+    @app_commands.command(name="add", description="Erstelle eine Umfrage mit bis zu 20 Antwortmöglichkeiten.")
+    @app_commands.describe(
+        type="Umfragetyp, single_choice: nur eine Antwort kann ausgewählt werden, multiple_choice: Mehrere Antwortmöglichkeiten wählbar.",
+        anonymous="Bei einer Anonymen Umfrage kann nicht nachgeschaut werden, welcher Teilnehmer wofür abgestimmt hat.",
+        question="Welche Frage möchtest du stellen?", choice_a="1. Antwortmöglichkeit",
+        choice_b="2. Antwortmöglichkeit", choice_c="3. Antwortmöglichkeit", choice_d="4. Antwortmöglichkeit",
+        choice_e="5. Antwortmöglichkeit", choice_f="6. Antwortmöglichkeit", choice_g="7. Antwortmöglichkeit",
+        choice_h="8. Antwortmöglichkeit", choice_i="9. Antwortmöglichkeit", choice_j="10. Antwortmöglichkeit",
+        choice_k="11. Antwortmöglichkeit", choice_l="12. Antwortmöglichkeit", choice_m="13. Antwortmöglichkeit",
+        choice_n="14. Antwortmöglichkeit", choice_o="15. Antwortmöglichkeit", choice_p="16. Antwortmöglichkeit",
+        choice_q="17. Antwortmöglichkeit", choice_r="18. Antwortmöglichkeit", choice_s="19. Antwortmöglichkeit",
+        choice_t="20. Antwortmöglichkeit")
+    async def cmd_poll(self, interaction: Interaction, type: PollType, anonymous: bool, question: str, choice_a: str,
+                       choice_b: str,
+                       choice_c: str = None, choice_d: str = None, choice_e: str = None, choice_f: str = None,
+                       choice_g: str = None, choice_h: str = None, choice_i: str = None, choice_j: str = None,
+                       choice_k: str = None, choice_l: str = None, choice_m: str = None, choice_n: str = None,
+                       choice_o: str = None, choice_p: str = None, choice_q: str = None, choice_r: str = None,
+                       choice_s: str = None, choice_t: str = None):
+        """ Create a new poll """
+        choices = [self.parse_choice(index, choice) for index, choice in enumerate(
+            [choice_a, choice_b, choice_c, choice_d, choice_e, choice_f, choice_g, choice_h, choice_i, choice_j,
+             choice_k, choice_l, choice_m, choice_n, choice_o, choice_p, choice_q, choice_r, choice_s, choice_t]) if
+                   choice]
+
+        await interaction.response.defer()
+        poll = {"type": type.value, "anonymous": anonymous, "question": question, "author": interaction.user.id,
+                "choices": choices, "participants": {}}
+        await interaction.edit_original_response(embed=self.get_embed(poll), view=PollView(self))
+        message = await interaction.original_response()
+        self.polls[str(message.id)] = poll
+        self.save()
+
+    def get_embed(self, poll) -> discord.Embed:
+        embed = discord.Embed(title="Umfrage", description=poll["question"])
+        embed.add_field(name="Erstellt von", value=f'<@!{poll["author"]}>', inline=False)
+        embed.add_field(name="\u200b", value="\u200b", inline=False)
+        choices = sorted(poll["choices"], key=lambda x: x[2], reverse=True)
+
+        for choice in choices:
+            name = f'{choice[0]}  {choice[1]}'
+            value = f'{choice[2]}'
+
+            embed.add_field(name=name, value=value, inline=False)
+
+        embed.add_field(name="\u200b", value="\u200b", inline=False)
+        embed.add_field(name="Anzahl der Teilnehmer an der Umfrage", value=f"{len(poll['participants'])}", inline=False)
+
+        return embed
+
+    def parse_choice(self, idx: int, choice: str):
+        choice = choice.strip()
+        index = choice.find(" ")
+
+        if index > -1:
+            possible_option = choice[:index]
+            if emoji.is_emoji(possible_option) or possible_option in DEFAULT_CHOICES:
+                if len(choice[index:].strip()) > 0:
+                    return [possible_option, choice[index:].strip(), 0]
+            elif len(possible_option) > 1:
+                if (possible_option[0:2] == "<:" or possible_option[0:3] == "<a:") and possible_option[-1] == ">":
+                    splitted_custom_emoji = possible_option.strip("<a:>").split(":")
+                    if len(splitted_custom_emoji) == 2:
+                        id = splitted_custom_emoji[1]
+                        custom_emoji = self.bot.get_emoji(int(id))
+                        if custom_emoji and len(choice[index:].strip()) > 0:
+                            return [custom_emoji, choice[index:].strip(), 0]
+
+        return [DEFAULT_CHOICES[idx], choice, 0]
+
+
+async def setup(bot: commands.Bot) -> None:
+    polls = Polls(bot)
+    await bot.add_cog(polls)
+    bot.add_view(PollView(polls))
diff --git a/extensions/text_commands.py b/extensions/text_commands.py
new file mode 100644
index 0000000000000000000000000000000000000000..e8f073800ceea645043ef84c280800feb0c656df
--- /dev/null
+++ b/extensions/text_commands.py
@@ -0,0 +1,247 @@
+import json
+import os
+import random
+import re
+
+import discord
+from discord import Interaction, app_commands
+from discord.app_commands import Group
+from discord.ext import commands
+
+import utils
+
+
+@app_commands.guild_only()
+class TextCommands(commands.GroupCog, name="commands", description="Text Commands auflisten und verwalten"):
+    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)
+
+    def save_text_commands(self):
+        text_commands_file = open(self.cmd_file, mode='w')
+        json.dump(self.text_commands, text_commands_file)
+
+    @app_commands.command(name="list", description="Listet die Text Commands dieses Servers auf.")
+    @app_commands.describe(cmd="Command für den die Texte ausgegeben werden sollen.")
+    @app_commands.guild_only()
+    async def cmd_list(self, interaction: Interaction, cmd: str = None):
+        await self.list_commands(interaction, cmd=cmd[1:] if cmd and cmd[0] == "/" else cmd)
+
+    @app_commands.command(name="add",
+                          description="Ein neues Text Command hinzufügen, oder zu einem bestehenden einen weiteren text hinzufügen.")
+    @app_commands.describe(cmd="Command. Bsp: \"link\" für das Command \"/link\".",
+                           text="Text, der bei Benutzung des Commands ausgegeben werden soll.",
+                           description="Beschreibung des Commands, die bei Benutzung angezeigt wird. Wird nur übernommen, bei neuen Commands.")
+    async def cmd_add(self, interaction: Interaction, cmd: str, text: str, description: str):
+        await interaction.response.defer(ephemeral=True)
+        if not re.match(r"^[a-z0-9äöü]+(-[a-z0-9äöü]+)*$", cmd):
+            await interaction.edit_original_response(
+                content="Ein Command darf nur aus Kleinbuchstaben und Zahlen bestehen, die durch Bindestriche getrennt werden können.")
+            return
+
+        if utils.is_mod(interaction.user):
+            if await self.add_command(cmd, text, description, interaction.guild_id):
+                await interaction.edit_original_response(content="Dein Command wurde erfolgreich hinzugefügt!")
+            else:
+                await interaction.edit_original_response(
+                    content="Das Command, dass du hinzufügen möchtest existiert bereits.")
+        else:
+            await self.suggest_command(cmd, text, description)
+            await interaction.edit_original_response(content="Dein Vorschlag wurde den Mods zur Genehmigung vorgelegt.")
+
+    @app_commands.command(name="edit", description="Bearbeite bestehende Text Commands")
+    @app_commands.describe(cmd="Command, dass du bearbeiten möchtest", id="ID des zu bearbeitenden Texts",
+                           text="Neuer Text, der statt des alten ausgegeben werden soll.")
+    @app_commands.checks.has_role("Mod")
+    async def cmd_edit(self, interaction: Interaction, cmd: str, id: int, text: str):
+        await interaction.response.defer(ephemeral=True)
+
+        if command := self.text_commands.get(cmd):
+            texts = command.get('data')
+            if 0 <= id < len(texts):
+                texts[id] = text
+                await interaction.edit_original_response(
+                    content=f"Text {id} für Command {cmd} wurde erfolgreich geändert")
+                self.save_text_commands()
+            else:
+                await interaction.edit_original_response(content=f"Ungültiger Index")
+        else:
+            await interaction.edit_original_response(content=f"Command {cmd} nicht vorhanden!")
+
+    @app_commands.command(name="remove",
+                          description="Entferne ein gesamtes Command oder einen einzelnen Text von einem Command.")
+    @app_commands.describe(cmd="Command, dass du entfernen möchtest, oder von dem du einen Text entfernen möchtest.",
+                           id="ID des zu entfernenden Texts.")
+    @app_commands.checks.has_role("Mod")
+    async def cmd_command_remove(self, interaction: Interaction, cmd: str, id: int = None):
+        await interaction.response.defer(ephemeral=True)
+
+        if command := self.text_commands.get(cmd):
+            texts = command.get('data')
+            if id is None or (len(texts) < 2 and id == 0):
+                if cmd in self.text_commands:
+                    self.text_commands.pop(cmd)
+                    await interaction.edit_original_response(content="Text Command {cmd} wurde erfolgreich entfernt.")
+                    self.save_text_commands()
+                    self.bot.tree.remove_command(cmd)
+                    await self.bot.sync_slash_commands_for_guild(interaction.guild_id)
+                else:
+                    await interaction.edit_original_response(content="Text Command {cmd} nicht vorhanden!")
+            else:
+                if 0 <= id < len(texts):  # schließt Aufrufe von Indizen aus, die außerhalb des Felds wären
+                    del texts[id]
+                    await interaction.edit_original_response(
+                        content=f"Text {id} für Command {cmd} wurde erfolgreich entfernt")
+
+                    self.save_text_commands()
+                else:
+                    await interaction.edit_original_response(content=f"Ungültiger Index")
+
+
+
+        else:
+            await interaction.edit_original_response(content=f"Command {cmd} nicht vorhanden!")
+
+    async def list_commands(self, interaction: Interaction, cmd=None):
+        await interaction.response.defer(ephemeral=True)
+
+        if cmd and not self.text_commands.get(cmd):
+            await interaction.edit_original_response(content=f"Es tut mir leid, für `/{cmd}` habe ich keine Texte "
+                                                             f"hinterlegt, die ich dir anzeigen kann. Dies kann "
+                                                             f"entweder daran liegen, dass dies kein gültiges Command "
+                                                             f"ist, oder es handelt sich hierbei nicht um ein Command, "
+                                                             f"dass nur Texte ausgibt.")
+            return
+        commands = await self.bot.get_slash_commands_for_guild(interaction.guild_id, command=cmd)
+
+        msg = "**__Verfügbare Texte für: __**\n" if cmd else "**__Verfügbare Commands: __**\n"
+        msg += "_\* hierbei handelt es s ich um ein Text Command, also einem Command, bei dem zufällig einer der " \
+               "hinterlegten Texte ausgegeben wird. Ãœber den optionalen Parameter `cmd` kannst du dir die hinterlegten " \
+               "Texte zu diesem Command ausgeben lassen.\n\n_"
+        for command in commands:
+            text_command = self.text_commands.get(command.name)
+            command_msg = ""
+            if command.default_permissions and interaction.permissions.value & command.default_permissions.value == 0:
+                continue
+            if isinstance(command, Group):
+                command_msg += f"**{command.name}**: *{command.description}*\n"
+                for c in command.commands:
+                    command_msg += f"    `/{command.name} {c.name}`: *{c.description}*\n"
+                command_msg += "\n"
+            else:
+                if text_command:
+                    command_msg += f"`/{command.name}`\*: *{command.description}*\n"
+                    if cmd:
+                        for i, text in enumerate(text_command["data"]):
+                            command_msg += f"`{i}`: {text}\n"
+                else:
+                    command_msg += f"`/{command.name}`: *{command.description}*\n"
+                command_msg += "\n"
+
+            if len(msg + command_msg) > utils.MAX_MESSAGE_LEN:
+                await interaction.followup.send(content=msg, ephemeral=True)
+                msg = command_msg
+            else:
+                msg += command_msg
+
+        await interaction.followup.send(content=msg, ephemeral=True)
+
+    async def add_command(self, cmd: str, text: str, description: str, guild_id: int):
+        mod_channel = await self.bot.fetch_channel(self.mod_channel_id)
+        if command := self.text_commands.get(cmd):
+            command["data"].append(text)
+        else:
+            if self.exists(cmd):
+                return False
+            self.text_commands[cmd] = {"description": description, "data": [text]}
+            await self.register_command(cmd, description, guild_id=guild_id)
+
+        await mod_channel.send(f"[{cmd}] => [{text}] erfolgreich hinzugefügt.")
+        self.save_text_commands()
+        return True
+
+    async def suggest_command(self, cmd: str, text: str, description: str):
+        mod_channel = await self.bot.fetch_channel(self.mod_channel_id)
+        command = 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"👍 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 not command:
+            embed.add_field(name="Beschreibung", value=description, inline=False)
+
+        message = await mod_channel.send(embed=embed)
+        await message.add_reaction("👍")
+        await message.add_reaction("👎")
+
+    def exists(self, cmd):
+        for command in self.bot.tree.get_commands():
+            if command.name == cmd:
+                return True
+
+        return False
+
+    async def register_command(self, cmd: str, description: str, guild_id: int = 0, sync: bool = True):
+        @app_commands.command(name=cmd, description=description)
+        @app_commands.guild_only()
+        @app_commands.describe(public="Zeige die Ausgabe des Commands öffentlich, für alle Mitglieder sichtbar.")
+        async def process_command(interaction: Interaction, public: bool):
+            await interaction.response.defer(ephemeral=not public)
+            if command := self.text_commands.get(interaction.command.name):
+                texts = command["data"]
+                if len(texts) > 0:
+                    await interaction.edit_original_response(content=(random.choice(texts)))
+                    return
+
+            await interaction.edit_original_response(content="FEHLER! Command wurde nicht gefunden!")
+
+        self.bot.tree.add_command(process_command)
+        if sync:
+            await self.bot.sync_slash_commands_for_guild(guild_id)
+
+    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")
+        description = fields.get("Beschreibung")
+
+        if approved:
+            await self.add_command(cmd, text, description, message.guild.id)
+        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 init_commands(self):
+        for cmd, command in self.text_commands.items():
+            if len(command["data"]) > 0:
+                await self.register_command(cmd, command["description"], sync=False)
+
+
+async def setup(bot: commands.Bot) -> None:
+    text_commands = TextCommands(bot)
+    await bot.add_cog(text_commands)
+    await text_commands.init_commands()
diff --git a/extensions/timer.py b/extensions/timer.py
new file mode 100644
index 0000000000000000000000000000000000000000..eca98113b6116dc35545d2c005d55b154d36fde8
--- /dev/null
+++ b/extensions/timer.py
@@ -0,0 +1,191 @@
+import json
+import os
+import random
+from asyncio import sleep
+from copy import deepcopy
+from datetime import datetime, timedelta
+
+import discord
+from discord import Interaction, app_commands
+from discord.ext import commands, tasks
+
+from views.timer_view import TimerView
+
+
+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.timer_file_path = os.getenv("DISCORD_TIMER_FILE")
+        self.running_timers = self.load()
+        self.load()
+        self.run_timer.start()
+
+    def load(self):
+        with open(self.timer_file_path, mode='r') as timer_file:
+            return json.load(timer_file)
+
+    def save(self):
+        with open(self.timer_file_path, mode='w') as timer_file:
+            json.dump(self.running_timers, timer_file)
+
+    def get_view(self, disabled=False):
+        view = TimerView(self)
+
+        if disabled:
+            view.disable()
+
+        return view
+
+
+    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()
+        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,
+                              color=color)
+        embed.add_field(name="Status:", value=status, inline=False)
+        embed.add_field(name="Zeiten:", value=zeiten, inline=False)
+        embed.add_field(name="Verbleibende Zeit:", value=remaining_value + end_value, inline=False)
+        embed.add_field(name="Angemeldete User:", value=angemeldet_value if registered else "-", inline=False)
+
+        return embed
+
+    @app_commands.command(name="timer", description="Erstelle deine persönliche  Eieruhr")
+    @app_commands.guild_only()
+    async def cmd_timer(self, interaction: Interaction, working_time: int = 25, break_time: int = 5, name: str = None):
+        await interaction.response.defer()
+        message = await interaction.original_response()
+        name = name if name else random.choice(self.default_names)
+        remaining = working_time
+        status = "Arbeiten"
+        registered = [str(interaction.user.id)]
+
+        embed = self.create_embed(name, status, working_time, break_time, remaining, registered)
+        await interaction.edit_original_response(embed=embed, view=self.get_view())
+
+        self.running_timers[str(message.id)] = {'name': name,
+                                                'status': status,
+                                                'working_time': working_time,
+                                                'break_time': break_time,
+                                                'remaining': remaining,
+                                                'registered': registered,
+                                                'channel': interaction.channel_id}
+        self.save()
+        await self.make_sound(registered, 'roll_with_it-outro.mp3')
+
+    async def switch_phase(self, msg_id):
+        if timer := self.running_timers.get(msg_id):
+            if timer['status'] == "Arbeiten":
+                timer['status'] = "Pause"
+                timer['remaining'] = timer['break_time']
+            elif timer['status'] == "Pause":
+                timer['status'] = "Arbeiten"
+                timer['remaining'] = timer['working_time']
+            else:
+                self.running_timers.pop(msg_id)
+                return "Beendet"
+            self.save()
+
+            if new_msg_id := await self.edit_message(msg_id):
+                return self.running_timers[new_msg_id]['status']
+            else:
+                return "Beendet"
+
+    def get_details(self, msg_id):
+        name = self.running_timers[msg_id]['name']
+        status = self.running_timers[msg_id]['status']
+        wt = self.running_timers[msg_id]['working_time']
+        bt = self.running_timers[msg_id]['break_time']
+        remaining = self.running_timers[msg_id]['remaining']
+        registered = self.running_timers[msg_id]['registered']
+        channel = self.running_timers[msg_id]['channel']
+        return name, status, wt, bt, remaining, registered, channel
+
+    async def edit_message(self, msg_id, mentions=None, create_new=True):
+        if timer := self.running_timers.get(msg_id):
+            channel_id = timer['channel']
+            channel = await self.bot.fetch_channel(int(channel_id))
+            try:
+                msg = await channel.fetch_message(int(msg_id))
+
+                name, status, wt, bt, remaining, registered, _ = self.get_details(msg_id)
+                embed = self.create_embed(name, status, wt, bt, remaining, registered)
+
+                if create_new:
+                    await msg.delete()
+                    if not mentions:
+                        mentions = self.get_mentions(msg_id)
+                    if status == "Beendet":
+                        new_msg = await channel.send(mentions, embed=embed,
+                                                     view=self.get_view(disabled=True))
+                    else:
+                        new_msg = await channel.send(mentions, embed=embed, view=self.get_view())
+                    self.running_timers[str(new_msg.id)] = self.running_timers[msg_id]
+                    self.running_timers.pop(msg_id)
+                    self.save()
+                    msg = new_msg
+                else:
+                    await msg.edit(embed=embed, view=self.get_view())
+                return str(msg.id)
+            except discord.errors.NotFound:
+                self.running_timers.pop(msg_id)
+                self.save()
+                return None
+
+    def get_mentions(self, msg_id):
+        guild = self.bot.get_guild(self.guild_id)
+        registered = self.running_timers.get(msg_id)['registered']
+        members = [guild.get_member(int(user_id)) for user_id in registered]
+        mentions = ", ".join([member.mention for member in members])
+        return mentions
+
+    async def make_sound(self, registered_users, filename):
+        guild = self.bot.get_guild(self.guild_id)
+        for user_id in registered_users:
+            member = guild.get_member(int(user_id))
+            if member.voice:
+                channel = member.voice.channel
+                if channel:  # If user is in a channel
+                    try:
+                        voice_client = await channel.connect()
+                        voice_client.play(discord.FFmpegPCMAudio(f'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)
+
+
+async def setup(bot: commands.Bot) -> None:
+    timer = Timer(bot)
+    await bot.add_cog(timer)
+    bot.add_view(TimerView(timer))
diff --git a/extensions/voice.py b/extensions/voice.py
new file mode 100644
index 0000000000000000000000000000000000000000..e0017c93f4822896c3e9c6950fafac35d9aaa5d7
--- /dev/null
+++ b/extensions/voice.py
@@ -0,0 +1,44 @@
+import os
+
+from discord import app_commands, Interaction
+from discord.app_commands import Choice
+from discord.ext import commands
+
+
+class Voice(commands.Cog):
+    def __init__(self, bot):
+        self.lerngruppen_category = int(os.getenv("DISCORD_CATEGORY_LERNGRUPPEN"))
+        self.bot = bot
+
+    @app_commands.command(name="voice", description="Sprachkanäle öffnen oder schließen")
+    @app_commands.describe(state="Wähle, ob die Sprachkanäle geöffnet oder geschlossen werden sollen.")
+    @app_commands.choices(state=[Choice(name="open", value="open"), Choice(name="close", value="close")])
+    @app_commands.default_permissions(manage_roles=True)
+    @app_commands.guild_only()
+    async def cmd_voice(self, interaction: Interaction, state: Choice[str]):
+        await interaction.response.defer(ephemeral=True)
+        voice_channels = interaction.guild.voice_channels
+        print(voice_channels[0].user_limit)
+        if state.value == "open":
+            for voice_channel in voice_channels:
+                await voice_channel.edit(user_limit=0)
+        elif state.value == "close":
+            for voice_channel in voice_channels:
+                await voice_channel.edit(user_limit=1)
+        await interaction.edit_original_response(content="Status der Voice Channel erfolgreich geändert.")
+
+    @commands.Cog.listener()
+    async def on_voice_state_update(self, member, before, after):
+        if before.channel != after.channel and after.channel and "Lerngruppen-Voicy" in after.channel.name:
+            category = await self.bot.fetch_channel(self.lerngruppen_category)
+            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)
+
+
+async def setup(bot: commands.Bot) -> None:
+    await bot.add_cog(Voice(bot))
diff --git a/extensions/welcome.py b/extensions/welcome.py
new file mode 100644
index 0000000000000000000000000000000000000000..feab886134e51212166fb2ebb39ffded670507f9
--- /dev/null
+++ b/extensions/welcome.py
@@ -0,0 +1,28 @@
+import os
+
+from discord.ext import commands
+
+import utils
+
+
+class Welcome(commands.Cog):
+    def __init__(self, bot):
+        self.bot = bot
+        self.channel_id = int(os.getenv("DISCORD_WELCOME_CHANNEL", "0"))
+
+    @commands.Cog.listener()
+    async def on_member_join(self, member):
+        await utils.send_dm(member,
+                            f"Herzlich Willkommen auf diesem Discord-Server. Wir hoffen sehr, dass du dich hier wohl fühlst. Alle notwendigen Informationen, die du für den Einstieg brauchst, findest du in <#{self.channel_id}>\n"
+                            f"Wir würden uns sehr freuen, wenn du dich in <#{os.getenv('DISCORD_VORSTELLUNGSCHANNEL')}> allen kurz vorstellen würdest. Es gibt nicht viele Regeln zu beachten, doch die Regeln, die aufgestellt sind, findest du hier:  https://discordapp.com/channels/353315134678106113/697729059173433344/709475694157234198 .\n"
+                            f"Du darfst dir außerdem gerne im Channel <#{os.getenv('DISCORD_ROLLEN_CHANNEL')}> die passende Rolle zu den Studiengängen in denen du eingeschrieben bist zuweisen. \n\n"
+                            f"Abschließend bleibt mir nur noch, dir hier viel Spaß zu wünschen, und falls du bei etwas hilfe brauchen solltest, schreib mir doch eine private Nachricht, das Moderatoren Team wird sich dann darum kümmern.")
+
+    @commands.Cog.listener()
+    async def on_member_update(self, before, after):
+        if before.pending != after.pending and not after.pending:
+            channel = await self.bot.fetch_channel(int(os.getenv("DISCORD_GREETING_CHANNEL")))
+            await channel.send(f"Herzlich Willkommen <@!{before.id}> im Kreise der Studentinnen :wave:")
+
+async def setup(bot: commands.Bot) -> None:
+    await bot.add_cog(Welcome(bot))
diff --git a/cogs/xkcd.py b/extensions/xkcd.py
similarity index 59%
rename from cogs/xkcd.py
rename to extensions/xkcd.py
index aaab79552b547687052a12f25a7d40ef93cc5c14..81a8f02aa23da6bc0921b15b8bc5f00ee34a10bc 100644
--- a/cogs/xkcd.py
+++ b/extensions/xkcd.py
@@ -1,56 +1,51 @@
-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)
+import random
+
+import aiohttp
+import discord
+from discord import app_commands, Interaction
+from discord.ext import commands
+
+
+class Xkcd(commands.Cog):
+    def __init__(self, bot):
+        self.bot = bot
+
+    @app_commands.command(name="xkcd", description="Poste einen XKCD Comic. Zufällig oder deiner Wahl")
+    @app_commands.describe(number="Nummer des XKCD Comics, den du posten möchtest.")
+    @app_commands.guild_only()
+    async def cmd_xkcd(self, interaction: Interaction, number: int = None):
+        await interaction.response.defer()
+        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
+        embed = discord.Embed(title=f"xkcd #{num}: {title}", description=text, url=f"https://xkcd.com/{num}")
+        embed.set_image(url=img)
+
+        await interaction.edit_original_response(embed=embed)
+
+
+async def setup(bot: commands.Bot) -> None:
+    await bot.add_cog(Xkcd(bot))
diff --git a/fernuni_bot.py b/fernuni_bot.py
index 9c9e334e8ebd7cbdda0e696c39e02b673b4a4ba0..75b42a3c96ceca8ac50ddbf3bacae1ddb0742a93 100644
--- a/fernuni_bot.py
+++ b/fernuni_bot.py
@@ -1,69 +1,60 @@
 import os
+from typing import List
 
-import disnake
-from disnake.ext import commands
+import discord
+from discord import Intents, Game
+from discord.app_commands import Group
+from discord.ext import commands
 from dotenv import load_dotenv
 
-from cogs import appointments, calmdown, github, help, learninggroups, links, timer, \
-    news, polls, roles, support, text_commands, voice, welcome, xkcd, module_information
 from view_manager import ViewManager
 
 # .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'))
+GUILD_ID = 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 = int(os.getenv("DISCORD_CATEGORY_LERNGRUPPEN"))
 PIN_EMOJI = "📌"
 
+intents = Intents.all()
+extensions = ["appointments", "news", "mod_mail", "voice", "welcome", "xkcd", "timer", "polls",
+              "text_commands", "links", "module_information", "learninggroups"]
 
-class Boty(commands.Bot):
-    def __init__(self):
-        super().__init__(command_prefix='!', help_command=None, activity=disnake.Game(ACTIVITY), owner_id=OWNER,
-                         intents=disnake.Intents.all())
-        self.view_manager = ViewManager(self)
-        self.add_cogs()
-        self.persistent_views_added = False
-        # self.add_cog(elm_street.ElmStreet(self))
 
-    def is_prod(self):
-        return os.getenv("DISCORD_PROD") == "True"
+class Boty(commands.Bot):
+    def __init__(self, *args, initial_extensions: List[str], **kwargs):
+        super().__init__(*args, **kwargs)
+        self.is_prod = os.getenv("DISCORD_PROD") == "True"
+        self.initial_extensions: List[str] = initial_extensions
+        self.view_manager: ViewManager = ViewManager(self)
+
+    async def setup_hook(self) -> None:
+        for extension in self.initial_extensions:
+            await self.load_extension(f"extensions.{extension}")
+            print(f"âž• Module {extension}")
+        await self.sync_slash_commands_for_guild(GUILD_ID)
+
+    async def sync_slash_commands_for_guild(self, guild_id):
+        guild = discord.Object(id=guild_id)
+        self.tree.copy_global_to(guild=guild)
+        await self.tree.sync(guild=guild)
+
+    async def get_slash_commands_for_guild(self, guild_id, command=None):
+        guild = discord.Object(id=guild_id)
+        commands = [self.tree.get_command(command, guild=guild)] if command else self.tree.get_commands(guild=guild)
+        commands.sort(key=lambda e: f"a{e.name}" if isinstance(e, Group) else f"b{e.name}")
+        return commands
 
     async def on_ready(self):
         self.view_manager.on_ready()
-        if not self.persistent_views_added:
-            if timer_cog := self.get_cog("Timer"):
-                self.add_view(timer_cog.get_view())
-        print("Client started!")
-
-    def add_cogs(self):
-        self.add_cog(appointments.Appointments(self))
-        self.add_cog(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(support.Support(self))
-        self.add_cog(news.News(self))
-        self.add_cog(links.Links(self))
-        self.add_cog(voice.Voice(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))
-
+        print("✅ Client started!")
 
-bot = Boty()
 
-
-# bot.add_cog(ChangeLogCog(bot))
-
-# SlashClient(bot, show_warnings=True)  # Stellt den Zugriff auf die Buttons bereit
+bot = Boty(command_prefix='!', help_command=None, activity=Game(ACTIVITY), owner_id=OWNER, intents=intents,
+           initial_extensions=extensions)
 
 
 def get_reaction(reactions):
@@ -92,11 +83,6 @@ async def unpin_message(message):
             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:
@@ -116,17 +102,4 @@ async def on_raw_reaction_remove(payload):
         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/json_import.py b/json_import.py
new file mode 100644
index 0000000000000000000000000000000000000000..6e8406f5aee760f14099385f5543a9537ddca520
--- /dev/null
+++ b/json_import.py
@@ -0,0 +1,22 @@
+import json
+
+import models
+
+
+def import_links(json_file: str) -> None:
+    file = open(json_file, mode="r")
+    links = json.load(file)
+
+    for channel, categories in links.items():
+        for category, links in categories.items():
+            category = category.capitalize()
+            db_category = models.LinkCategory.get_or_create(channel=int(channel), name=category)
+            for title, link in links.items():
+                models.Link.create(link=link, title=title, category=db_category[0].id)
+
+
+if __name__ == "__main__":
+    """
+    Make sure to create a database backup before you import data from json files.
+    """
+    # import_links("data/links.json")
diff --git a/modals/link_modal.py b/modals/link_modal.py
new file mode 100644
index 0000000000000000000000000000000000000000..f5572725d4c6e9983676d445f0c8189c234cb32b
--- /dev/null
+++ b/modals/link_modal.py
@@ -0,0 +1,83 @@
+import re
+import traceback
+from typing import Optional
+
+import discord as discord
+from discord import ui
+from discord.utils import MISSING
+
+import models
+
+
+class InvalidLinkError(Exception):
+    pass
+
+
+class LinkDoesNotExistError(Exception):
+    pass
+
+
+class LinkModal(ui.Modal, title='Link hinzufügen'):
+    def __init__(self, *, category: str = None, link_title: str = None, link: str = None, link_id: int = None,
+                 title: str = MISSING, timeout: Optional[float] = None, custom_id: str = MISSING) -> None:
+        super().__init__(title=title, timeout=timeout, custom_id=custom_id)
+        self.category.default = category
+        self.link_title.default = link_title
+        self.link.default = link
+        self.link_id = link_id
+
+    category = ui.TextInput(label='Kategorie')
+    link_title = ui.TextInput(label='Titel')
+    link = ui.TextInput(label='Link')
+
+    def validate_link(self):
+        if not re.match("^https?://.+", self.link.value):
+            raise InvalidLinkError(f"`{self.link}` ist kein gültiger Link")
+
+    async def on_submit(self, interaction: discord.Interaction):
+        self.validate_link()
+        db_category = models.LinkCategory.get_or_create(channel=interaction.channel_id, name=self.category)
+
+        if self.link_id is None:
+            models.Link.create(link=self.link, title=self.link_title, category=db_category[0].id)
+            await interaction.response.send_message(content="Link erfolgreich hinzugefügt.", ephemeral=True)
+        else:
+            if link := models.Link.get_or_none(models.Link.id == self.link_id):
+                link_category = link.category
+                link.update(title=self.link_title, link=self.link, category=db_category[0].id).where(
+                    models.Link.id == link.id).execute()
+
+                if link_category.id != db_category[0].id and link.category.links.count() == 0:
+                    link_category.delete_instance()
+            else:
+                raise LinkDoesNotExistError(f"Der Link `{self.link_title}` existiert nicht.")
+
+            await interaction.response.send_message(content="Link erfolgreich bearbeitet.", ephemeral=True)
+
+    async def on_error(self, interaction: discord.Interaction, error: Exception) -> None:
+        if type(error) in [InvalidLinkError, LinkDoesNotExistError]:
+            await interaction.response.send_message(content=error, ephemeral=True)
+        else:
+            await interaction.response.send_message(content="Fehler beim Hinzufügen/Bearbeiten eines Links.",
+                                                    ephemeral=True)
+        traceback.print_exception(type(error), error, error.__traceback__)
+
+
+class LinkCategoryModal(ui.Modal, title='Kategorie umbenennen'):
+    def __init__(self, *, db_category: str = None, link_id: int = None,
+                 title: str = MISSING, timeout: Optional[float] = None, custom_id: str = MISSING) -> None:
+        super().__init__(title=title, timeout=timeout, custom_id=custom_id)
+        self.db_category = db_category
+        self.category.default = db_category.name
+        self.link_id = link_id
+
+    category = ui.TextInput(label='Kategorie')
+
+    async def on_submit(self, interaction: discord.Interaction):
+        self.db_category.update(name=self.category).where(models.LinkCategory.id == self.db_category.id).execute()
+        await interaction.response.send_message(content="Kategorie erfolgreich umbenannt.", ephemeral=True)
+
+    async def on_error(self, interaction: discord.Interaction, error: Exception) -> None:
+        await interaction.response.send_message(content=f"Fehler beim umbenennen der Kategorie `{self.category}`.",
+                                                ephemeral=True)
+        traceback.print_exception(type(error), error, error.__traceback__)
diff --git a/models.py b/models.py
new file mode 100644
index 0000000000000000000000000000000000000000..71393bcaa14fb4bc67cdcd6fa78cac2efb806a7a
--- /dev/null
+++ b/models.py
@@ -0,0 +1,45 @@
+import discord
+from peewee import *
+from peewee import ModelSelect
+
+db = SqliteDatabase("db.sqlite3")
+
+
+class BaseModel(Model):
+    class Meta:
+        database = db
+        legacy_table_names = False
+
+
+class LinkCategory(BaseModel):
+    channel = IntegerField()
+    name = CharField()
+
+    @classmethod
+    def get_categories(cls, channel: int, category: str = None) -> ModelSelect:
+        categories: ModelSelect = cls.select().where(LinkCategory.channel == channel)
+        return categories.where(LinkCategory.name == category) if category else categories
+
+    @classmethod
+    def has_links(cls, channel: int, category: str = None) -> bool:
+        for category in cls.get_categories(channel, category=category):
+            if category.links.count() > 0:
+                return True
+
+        return False
+
+    def append_field(self, embed: discord.Embed) -> None:
+        value = ""
+        for link in self.links:
+            value += f"- [{link.title}]({link.link})\n"
+
+        embed.add_field(name=self.name, value=value, inline=False)
+
+
+class Link(BaseModel):
+    link = CharField()
+    title = CharField()
+    category = ForeignKeyField(LinkCategory, backref='links')
+
+
+db.create_tables([LinkCategory, Link], safe=True)
diff --git a/requirements.txt b/requirements.txt
index 06585bd5955a3a246f7de4d1aef4cecf64fc1401..6aa7672b7d8979408aaec6b3b544956bfc702b98 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,21 +1,8 @@
-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.5.2
-emoji==1.2.0
-idna==2.10
-multidict==5.1.0
-pycparser==2.20
-PyNaCl==1.4.0
-python-dotenv==0.20.0
-requests==2.25.1
-six==1.16.0
-soupsieve==2.2.1
-tinydb==4.4.0
-typing-extensions==3.7.4.3
-urllib3==1.26.5
-yarl==1.6.3
+aiohttp==3.8.5
+beautifulsoup4==4.12.2
+discord.py==2.3.2
+emoji==2.8.0
+peewee==3.16.3
+PyNaCl==1.5.0
+python-dotenv==1.0.0
+requests==2.31.0
\ No newline at end of file
diff --git a/cogs/sounds/applause.mp3 b/sounds/applause.mp3
similarity index 100%
rename from cogs/sounds/applause.mp3
rename to sounds/applause.mp3
diff --git a/cogs/sounds/groove-intro.mp3 b/sounds/groove-intro.mp3
similarity index 100%
rename from cogs/sounds/groove-intro.mp3
rename to sounds/groove-intro.mp3
diff --git a/cogs/sounds/roll_with_it-outro.mp3 b/sounds/roll_with_it-outro.mp3
similarity index 100%
rename from cogs/sounds/roll_with_it-outro.mp3
rename to sounds/roll_with_it-outro.mp3
diff --git a/utils.py b/utils.py
index 84906098d2d61f501ac5b464681b704a975e67ad..3ff1b97ab70f64ad8b09377d9e4c410b99e6e3a9 100644
--- a/utils.py
+++ b/utils.py
@@ -2,21 +2,21 @@ import os
 import re
 from datetime import datetime
 
-import disnake
-from disnake import ButtonStyle
+from discord import ButtonStyle, Embed, User, Member
+from discord.ext.commands import Context
 from dotenv import load_dotenv
 
 from views.dialog_view import DialogView
 
 load_dotenv()
 DATE_TIME_FMT = os.getenv("DISCORD_DATE_TIME_FORMAT")
-
+MAX_MESSAGE_LEN = 2000
 
 async def send_dm(user, message, embed=None):
     """ Send DM to a user/member """
 
     try:
-        if type(user) is disnake.User or type(user) is disnake.Member:
+        if type(user) is User or type(user) is Member:
             if user.dm_channel is None:
                 await user.create_dm()
 
@@ -25,8 +25,11 @@ async def send_dm(user, message, embed=None):
         print(f"Cannot send DM to {user} with text: {message}")
 
 
-def is_mod(ctx):
-    author = ctx.author
+def is_mod(context_or_member):
+    if isinstance(context_or_member, Context):
+        author = context_or_member.author
+    else:
+        author = context_or_member
     roles = author.roles
 
     for role in roles:
@@ -55,7 +58,7 @@ def to_minutes(time):
 
 
 async def confirm(channel, title, description, message="", custom_prefix="", callback=None):
-    embed = disnake.Embed(title=title,
+    embed = Embed(title=title,
                           description=description,
                           color=19607)
     return await channel.send(message, embed=embed, view=DialogView([
diff --git a/view_manager.py b/view_manager.py
index ca22fc12063728812e16876b095d09214221525e..a274fe2aa0d3135035c30139bdec279a49eb9c3e 100644
--- a/view_manager.py
+++ b/view_manager.py
@@ -1,7 +1,7 @@
 import json
 import uuid
 
-import disnake
+import discord
 
 from views.dialog_view import DialogView
 
@@ -48,7 +48,7 @@ class ViewManager:
         )
 
     async def dialog(self, channel, title, description, message="", fields=[], buttons=None, callback_key: str = None):
-        embed = disnake.Embed(title=title,
+        embed = discord.Embed(title=title,
                               description=description,
                               color=19607)
         for field in fields:
diff --git a/views/appointment_view.py b/views/appointment_view.py
new file mode 100644
index 0000000000000000000000000000000000000000..8b6ed626d4e6e2f25a72bdc00da0c92e58b4f5e8
--- /dev/null
+++ b/views/appointment_view.py
@@ -0,0 +1,116 @@
+import io
+from datetime import datetime
+
+import discord
+from discord import File
+
+import utils
+
+
+def get_ics_file(title, date_time, reminder, recurring, description, ics_uuid):
+    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.now().strftime(fmt)}00Z\n" \
+                  f"UID:{ics_uuid}\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:{description}\n" \
+                   f"END:VALARM\n" \
+                   f"END:VEVENT\n" \
+                   f"END:VCALENDAR"
+    ics_file = io.BytesIO(appointment.encode("utf-8"))
+    return ics_file
+
+
+class AppointmentView(discord.ui.View):
+    def __init__(self, appointments):
+        super().__init__(timeout=None)
+        self.appointments = appointments
+
+    @discord.ui.button(label='Zusagen', style=discord.ButtonStyle.green, custom_id='appointment_view:accept', emoji="👍")
+    async def accept(self, interaction: discord.Interaction, button: discord.ui.Button):
+        await interaction.response.defer(ephemeral=True)
+        if channel_appointments := self.appointments.appointments.get(str(interaction.channel_id)):
+            if appointment := channel_appointments.get(str(interaction.message.id)):
+                if attendees := appointment.get("attendees"):
+                    attendees[str(interaction.user.id)] = 1
+                    self.appointments.save_appointments()
+                    await self.update_appointment(interaction.message, appointment)
+
+    @discord.ui.button(label='Absagen', style=discord.ButtonStyle.red, custom_id='appointment_view:decline', emoji="👎")
+    async def decline(self, interaction: discord.Interaction, button: discord.ui.Button):
+        await interaction.response.defer(ephemeral=True)
+        if channel_appointments := self.appointments.appointments.get(str(interaction.channel_id)):
+            if appointment := channel_appointments.get(str(interaction.message.id)):
+                if attendees := appointment.get("attendees"):
+                    if attendees.get(str(interaction.user.id)):
+                        del attendees[str(interaction.user.id)]
+                        self.appointments.save_appointments()
+                        await self.update_appointment(interaction.message, appointment)
+
+    @discord.ui.button(label='Download .ics', style=discord.ButtonStyle.blurple, custom_id='appointment_view:ics',
+                       emoji="📅")
+    async def ics(self, interaction: discord.Interaction, button: discord.ui.Button):
+        await interaction.response.defer(ephemeral=True)
+        if channel_appointments := self.appointments.appointments.get(str(interaction.channel_id)):
+            if appointment := channel_appointments.get(str(interaction.message.id)):
+                title = appointment.get("title")
+                date_time = datetime.strptime(appointment.get("date_time"), self.appointments.fmt)
+                reminder = appointment.get("reminder")
+                recurring = appointment.get("recurring")
+                description = appointment.get("description")
+                ics_uuid = appointment.get("ics_uuid")
+                file = File(get_ics_file(title, date_time, reminder, recurring, description, ics_uuid),
+                            filename=f"{appointment.get('title')}_{appointment.get('ics_uuid')}.ics")
+                await interaction.followup.send(file=file, ephemeral=True)
+
+    @discord.ui.button(label='Löschen', style=discord.ButtonStyle.gray, custom_id='appointment_view:delete', emoji="🗑")
+    async def delete(self, interaction: discord.Interaction, button: discord.ui.Button):
+        await interaction.response.defer(ephemeral=True)
+        if channel_appointments := self.appointments.appointments.get(str(interaction.channel_id)):
+            if appointment := channel_appointments.get(str(interaction.message.id)):
+                if appointment.get("author_id") == interaction.user.id or utils.is_mod(interaction.user):
+                    await interaction.followup.send(f"Termin {appointment.get('title')} gelöscht.", ephemeral=True)
+                    await interaction.message.delete()
+                    del channel_appointments[str(interaction.message.id)]
+                    self.appointments.save_appointments()
+
+    async def update_appointment(self, message, appointment):
+        channel = message.channel
+        message = message
+        author_id = appointment.get("author_id")
+        description = appointment.get("description")
+        title = appointment.get("title")
+        date_time = datetime.strptime(appointment.get("date_time"), self.appointments.fmt)
+        reminder = appointment.get("reminder")
+        recurring = appointment.get("recurring")
+        attendees = appointment.get("attendees")
+
+        await self.appointments.send_or_update_appointment(channel, author_id, description, title, date_time, reminder,
+                                                           recurring, attendees, message=message)
diff --git a/views/dialog_view.py b/views/dialog_view.py
index b625a29aa919bf977a7d6397c6497c7d94780f83..7170a131be46690599fed7935babf7c3c0f03741 100644
--- a/views/dialog_view.py
+++ b/views/dialog_view.py
@@ -1,8 +1,8 @@
-import disnake
-from disnake import ButtonStyle
+import discord
+from discord import ButtonStyle
 
 
-class DialogView(disnake.ui.View):
+class DialogView(discord.ui.View):
 
     def __init__(self, buttons=None, callback=None):
         super().__init__(timeout=None)
@@ -11,7 +11,7 @@ class DialogView(disnake.ui.View):
             self.add_button(button_config)
 
     def add_button(self, config):
-        button = disnake.ui.Button(
+        button = discord.ui.Button(
             style=config.get("style", ButtonStyle.grey),
             label=config.get("label", None),
             disabled=config.get("disabled", False),
diff --git a/views/poll_view.py b/views/poll_view.py
new file mode 100644
index 0000000000000000000000000000000000000000..390eb4e74658210a1d0438da2e8cc65d3c57ecaa
--- /dev/null
+++ b/views/poll_view.py
@@ -0,0 +1,119 @@
+import discord
+
+import utils
+
+
+async def show_participants(interaction, poll, ephemeral):
+    msg = f"Teilnehmer der Umfrage `{poll['question']}`:\n"
+    participant_choices = [[] for _ in range(len(poll["choices"]))]
+    for participant, choices in poll["participants"].items():
+        for choice in choices:
+            participant_choices[choice].append(participant)
+
+    choices = poll["choices"]
+    for idx, participants in enumerate(participant_choices):
+        choice_msg = f"{choices[idx][0]} {choices[idx][1]} ({choices[idx][2]}):"
+        choice_msg += "<@" if choices[idx][2] > 0 else ""
+        choice_msg += ">, <@".join(participants)
+        choice_msg += ">\n" if choices[idx][2] > 0 else ""
+        if len(msg) + len(choice_msg) >= utils.MAX_MESSAGE_LEN:
+            await interaction.followup.send(msg, ephemeral=ephemeral)
+            msg = choice_msg
+        else:
+            msg += choice_msg
+
+    await interaction.followup.send(msg, ephemeral=ephemeral)
+
+
+class PollView(discord.ui.View):
+    def __init__(self, polls):
+        super().__init__(timeout=None)
+        self.polls = polls
+
+    @discord.ui.button(label='Abstimmen', style=discord.ButtonStyle.green, custom_id='poll_view:vote', emoji="✅")
+    async def vote(self, interaction: discord.Interaction, button: discord.ui.Button):
+        await interaction.response.defer(ephemeral=True)
+        if poll := self.polls.polls.get(str(interaction.message.id)):
+            await interaction.followup.send(
+                f"{poll['question']}\n\n*(Nach der Abstimmung kannst du diese Nachricht verwerfen. Wenn die Abstimmung "
+                f"nicht funktioniert, bitte verwirf die Nachricht und Klicke erneut auf den Abstimmen Button der "
+                f"Abstimmung.)*", view=PollChoiceView(poll, interaction.user, interaction.message, self.polls),
+                ephemeral=True)
+
+    @discord.ui.button(label='Teilnehmer', style=discord.ButtonStyle.blurple, custom_id='poll_view:participants',
+                       emoji="👥")
+    async def participants(self, interaction: discord.Interaction, button: discord.ui.Button):
+        await interaction.response.defer(ephemeral=True)
+        if poll := self.polls.polls.get(str(interaction.message.id)):
+            if poll["anonymous"]:
+                await interaction.followup.send(
+                    "Diese Umfrage ist anonym. Daher kann ich dir nicht sagen, wer an dieser  Umfrage teilgenommen hat.")
+            else:
+                await show_participants(interaction, poll, ephemeral=True)
+
+    @discord.ui.button(label='Beenden', style=discord.ButtonStyle.gray, custom_id='poll_view:close', emoji="🛑")
+    async def close(self, interaction: discord.Interaction, button: discord.ui.Button):
+        await interaction.response.defer(ephemeral=True)
+        if poll := self.polls.polls.get(str(interaction.message.id)):
+            if poll.get("author") == interaction.user.id or utils.is_mod(interaction.user):
+                if not poll["anonymous"]:
+                    await show_participants(interaction, poll, ephemeral=False)
+
+                del self.polls.polls[str(interaction.message.id)]
+                self.polls.save()
+
+                await interaction.edit_original_response(view=None)
+
+    @discord.ui.button(label='Löschen', style=discord.ButtonStyle.gray, custom_id='poll_view:delete', emoji="🗑")
+    async def delete(self, interaction: discord.Interaction, button: discord.ui.Button):
+        await interaction.response.defer(ephemeral=True)
+        if poll := self.polls.polls.get(str(interaction.message.id)):
+            if poll.get("author") == interaction.user.id or utils.is_mod(interaction.user):
+                await interaction.followup.send(f"Umfrage {poll.get('question')} gelöscht.", ephemeral=True)
+                await interaction.message.delete()
+                del self.polls[str(interaction.message.id)]
+                self.polls.save()
+
+
+class PollChoiceView(discord.ui.View):
+    def __init__(self, poll, user, message, polls):
+        super().__init__(timeout=None)
+        self.poll = poll
+        self.user = user
+        self.add_item(PollDropdown(poll, user, message, polls))
+
+
+class PollDropdown(discord.ui.Select):
+    def __init__(self, poll, user, message, polls):
+        self.poll = poll
+        self.user = user
+        self.message = message
+        self.polls = polls
+        participant = self.poll["participants"].get(str(user.id))
+        options = [discord.SelectOption(label=choice[1], emoji=choice[0], value=str(idx),
+                                        default=self.is_default(participant, idx)) for idx, choice in
+                   enumerate(poll["choices"])]
+        max_values = 1 if poll["type"] == "single" else len(options)
+
+        super().__init__(placeholder='Gib deine Stimme(n) jetzt ab....', min_values=0, max_values=max_values,
+                         options=options)
+
+    async def callback(self, interaction: discord.Interaction):
+        await interaction.response.defer()
+        self.poll["participants"][str(interaction.user.id)] = [int(value) for value in self.values]
+
+        choices = [0] * len(self.poll["choices"])
+        for participant in self.poll["participants"].values():
+            for choice in participant:
+                choices[choice] += 1
+
+        for idx, choice in enumerate(self.poll["choices"]):
+            choice[2] = choices[idx]
+
+        await self.message.edit(embed=self.polls.get_embed(self.poll), view=PollView(self.poll))
+        self.polls.save()
+
+    def is_default(self, participant, idx):
+        if participant:
+            return idx in participant
+        return False
diff --git a/views/timer_view.py b/views/timer_view.py
index 06ce1cca482a3609e0054d859653eacf0fa484bb..35bdf31d63d8076848f3734f98c38593c6d2c65f 100644
--- a/views/timer_view.py
+++ b/views/timer_view.py
@@ -1,6 +1,6 @@
-import disnake
-from disnake import MessageInteraction, ButtonStyle
-from disnake.ui import Button, View
+import discord
+from discord import ButtonStyle, Interaction
+from discord.ui import Button, View
 
 SUBSCRIBE = "timerview:subscribe"
 UNSUBSCRIBE = "timerview:unsubscribe"
@@ -10,29 +10,108 @@ STOP = "timverview:stop"
 
 
 class TimerView(View):
-    def __init__(self, callback):
+    def __init__(self, timer):
         super().__init__(timeout=None)
-        self.callback = callback
+        self.timer = timer
 
-    @disnake.ui.button(emoji="👍", style=ButtonStyle.grey, custom_id=SUBSCRIBE)
-    async def btn_subscribe(self, button: Button, interaction: MessageInteraction):
-        await self.callback(button, interaction)
+    @discord.ui.button(label="Anmelden", emoji="👍", style=ButtonStyle.green, custom_id=SUBSCRIBE)
+    async def btn_subscribe(self, interaction: Interaction, button: Button):
+        await interaction.response.defer(ephemeral=True, thinking=False)
+        msg_id = str(interaction.message.id)
+        if timer := self.timer.running_timers.get(msg_id):
+            if str(interaction.user.id) not in timer['registered']:
+                timer['registered'].append(str(interaction.user.id))
+                self.timer.save()
+                name, status, wt, bt, remaining, registered, _ = self.timer.get_details(msg_id)
+                embed = self.timer.create_embed(name, status, wt, bt, remaining, registered)
+                await interaction.message.edit(embed=embed, view=self.timer.get_view())
+                await interaction.followup.send(content="Du hast dich erfolgreich angemeldet", ephemeral=True)
+            else:
+                await interaction.followup.send(content="Du bist bereits angemeldet.", ephemeral=True)
+        else:
+            await interaction.followup.send(content="Etwas ist schiefgelaufen...", ephemeral=True)
 
-    @disnake.ui.button(emoji="👎", style=ButtonStyle.grey, custom_id=UNSUBSCRIBE)
-    async def btn_unsubscribe(self, button: Button, interaction: MessageInteraction):
-        await self.callback(button, interaction)
+    @discord.ui.button(label="Abmelden", emoji="👎", style=ButtonStyle.red, custom_id=UNSUBSCRIBE)
+    async def btn_unsubscribe(self, interaction: Interaction, button: Button):
+        await interaction.response.defer(ephemeral=True, thinking=False)
+        msg_id = str(interaction.message.id)
+        if timer := self.timer.running_timers.get(msg_id):
+            registered = timer['registered']
+            if str(interaction.user.id) in registered:
+                if len(registered) == 1:
+                    await self.timer.on_stop(button, interaction)
+                    return
+                else:
+                    timer['registered'].remove(str(interaction.user.id))
+                    self.timer.save()
+                    name, status, wt, bt, remaining, registered, _ = self.timer.get_details(msg_id)
+                    embed = self.timer.create_embed(name, status, wt, bt, remaining, registered)
+                    await interaction.message.edit(embed=embed, view=self.timer.get_view())
+                    await interaction.followup.send(content="Du hast dich erfolgreich abgemeldet", ephemeral=True)
+            else:
+                await interaction.followup.send(content="Du warst gar nicht angemeldet.", ephemeral=True)
+        else:
+            await interaction.followup.send(content="Etwas ist schiefgelaufen...", ephemeral=True)
 
-    @disnake.ui.button(emoji="⏩", style=ButtonStyle.grey, custom_id=SKIP)
-    async def btn_skip(self, button: Button, interaction: MessageInteraction):
-        await self.callback(button, interaction)
+    @discord.ui.button(label="Phase überspringen", emoji="⏩", style=ButtonStyle.blurple, custom_id=SKIP)
+    async def btn_skip(self, interaction: Interaction, button: Button):
+        await interaction.response.defer(ephemeral=True, thinking=False)
+        msg_id = str(interaction.message.id)
+        if timer := self.timer.running_timers.get(msg_id):
+            registered = timer['registered']
+            if str(interaction.user.id) in timer['registered']:
+                new_phase = await self.timer.switch_phase(msg_id)
+                if new_phase == "Pause":
+                    await self.timer.make_sound(registered, 'groove-intro.mp3')
+                else:
+                    await self.timer.make_sound(registered, 'roll_with_it-outro.mp3')
+            else:
+                await interaction.followup.send(content="Nur angemeldete Personen können den Timer bedienen.",
+                                                ephemeral=True)
+        else:
+            await interaction.followup.send("Etwas ist schiefgelaufen...", ephemeral=True)
 
-    @disnake.ui.button(emoji="🔄", style=ButtonStyle.grey, custom_id=RESTART)
-    async def btn_restart(self, button: Button, interaction: MessageInteraction):
-        await self.callback(button, interaction)
+    @discord.ui.button(label="Neustarten", emoji="🔄", style=ButtonStyle.blurple, custom_id=RESTART)
+    async def btn_restart(self, interaction: Interaction, button: Button):
+        await interaction.response.defer(ephemeral=True, thinking=False)
+        msg_id = str(interaction.message.id)
+        if timer := self.timer.running_timers.get(msg_id):
+            registered = timer['registered']
+            if str(interaction.user.id) in timer['registered']:
+                timer['status'] = 'Arbeiten'
+                timer['remaining'] = timer['working_time']
+                self.timer.save()
 
-    @disnake.ui.button(emoji="🛑", style=ButtonStyle.grey, custom_id=STOP)
-    async def btn_stop(self, button: Button, interaction: MessageInteraction):
-        await self.callback(button, interaction)
+                await self.timer.edit_message(msg_id)
+                await self.timer.make_sound(registered, 'roll_with_it-outro.mp3')
+            else:
+                await interaction.followup.send(content="Nur angemeldete Personen können den Timer neu starten.",
+                                                ephemeral=True)
+        else:
+            await interaction.followup.send(content="Etwas ist schiefgelaufen...", ephemeral=True)
+
+    @discord.ui.button(label="Beenden", emoji="🛑", style=ButtonStyle.grey, custom_id=STOP)
+    async def btn_stop(self, interaction: Interaction, button: Button):
+        await interaction.response.defer(ephemeral=True, thinking=False)
+        msg_id = str(interaction.message.id)
+        if timer := self.timer.running_timers.get(msg_id):
+            registered = timer['registered']
+            if str(interaction.user.id) in timer['registered']:
+                mentions = self.timer.get_mentions(msg_id)
+                timer['status'] = "Beendet"
+                timer['remaining'] = 0
+                timer['registered'] = []
+
+                if new_msg_id := await self.timer.edit_message(msg_id, mentions=mentions):
+                    await self.timer.make_sound(registered, 'applause.mp3')
+                    self.timer.running_timers.pop(new_msg_id)
+                    self.timer.save()
+            else:
+                # Reply with a hidden message
+                await interaction.followup.send(content="Nur angemeldete Personen können den Timer beenden.",
+                                                ephemeral=True)
+        else:
+            await interaction.followup.send(content="Etwas ist schiefgelaufen...", ephemeral=True)
 
     def disable(self):
         for button in self.children: