diff --git a/extensions/appointments.py b/extensions/appointments.py
index 5ecf1475d0b12e4ad73101b38579e6fe5ced71bc..fbdec4d2289587d8f5532858bb1f2ea68047ee8c 100644
--- a/extensions/appointments.py
+++ b/extensions/appointments.py
@@ -1,251 +1,119 @@
 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 import app_commands, errors, Interaction
 from discord.ext import tasks, commands
 
+from models import Appointment
 from views.appointment_view import AppointmentView
 
-Channel = NewType('Channel', Union[
-    VoiceChannel, StageChannel, TextChannel, ForumChannel, CategoryChannel, Thread, PartialMessageable, None])
+
+async def send_notification(appointment, channel):
+    message = f"Benachrichtigung!\nDer Termin \"{appointment.title}\" startet "
+
+    if appointment.reminder_sent:
+        message += f"jetzt! :loudspeaker: "
+    else:
+        message += f"<t:{int(appointment.date_time.timestamp())}:R>."
+
+    message += f"\n"
+    message += " ".join([f"<@!{str(attendee.member_id)}>" for attendee in appointment.attendees])
+
+    await channel.send(message)
 
 
 @app_commands.guild_only()
-class Appointments(commands.GroupCog, name="appointments", description="Verwaltet Termine in Kanälen"):
+class Appointments(commands.GroupCog, name="appointments", description="Handle Appointments in Channels"):
     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
+        for appointment in Appointment.select().order_by(Appointment.channel):
+            now = datetime.now()
+            date_time = appointment.date_time
+            remind_at = date_time - timedelta(
+                minutes=appointment.reminder) if not appointment.reminder_sent else date_time
+
+            if now >= remind_at:
+                try:
+                    channel = await self.bot.fetch_channel(appointment.channel)
+                    message = await channel.fetch_message(appointment.message)
+                    await send_notification(appointment, channel)
+
+                    if appointment.reminder_sent:
+                        await message.delete()
+
+                        if appointment.recurring == 0:
+                            appointment.delete_instance(recursive=True)
                         else:
-                            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()
+                            new_date_time = appointment.date_time + timedelta(days=appointment.recurring)
+                            reminder_sent = appointment.reminder == 0
+                            Appointment.update(reminder_sent=reminder_sent, date_time=new_date_time).where(
+                                Appointment.id == appointment.id).execute()
+                            updated_appointment = Appointment.get(Appointment.id == appointment.id)
+                            new_message = await channel.send(embed=updated_appointment.get_embed(),
+                                                             view=AppointmentView())
+                            Appointment.update(message=new_message.id).where(Appointment.id == appointment.id).execute()
+                    else:
+                        Appointment.update(reminder_sent=True).where(Appointment.id == appointment.id).execute()
+                except errors.NotFound:
+                    appointment.delete_instance(recursive=True)
 
     @timer.before_loop
     async def before_timer(self):
         await asyncio.sleep(60 - datetime.now().second)
 
-    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).",
+    @app_commands.command(name="add", description="Füge dem Kanal einen neuen Termin hinzu.")
+    @app_commands.describe(date="Tag des Termins im Format TT.MM.JJJJ", time="Uhrzeit des Termins im Format HH:MM",
                            reminder="Wie viele Minuten bevor der Termin startet, soll eine Erinnerung verschickt werden?",
-                           title="Titel des Termins.", description="Beschreibung des Termins.",
+                           title="Titel des Termins (so wie er dann evtl. auch im Kalender steht).",
+                           description="Detailliertere Beschreibung, was gemacht werden soll.",
                            recurring="In welchem Intervall (in Tagen) soll der Termin wiederholt werden?")
     async def cmd_add_appointment(self, interaction: Interaction, date: str, time: str, reminder: int, title: str,
-                                  description: str = "", recurring: int = None):
-
-        await interaction.response.defer(ephemeral=True)
+                                  description: str = "", recurring: int = 0):
+        """ Add an appointment to a channel """
+        channel = interaction.channel
+        author_id = interaction.user.id
         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!")
+            date_time = datetime.strptime(f"{date} {time}", self.bot.dt_format())
         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))
+            await channel.send("Fehler! Ungültiges Datums und/oder Zeit Format!")
+            return
+
+        appointment = Appointment.create(channel=channel.id, message=0, date_time=date_time, reminder=reminder,
+                                         title=title, description=description, author=author_id, recurring=recurring,
+                                         reminder_sent=reminder == 0, uuid=uuid.uuid4())
+
+        await interaction.response.send_message(embed=appointment.get_embed(), view=AppointmentView())
+        message = await interaction.original_response()
+        Appointment.update(message=message.id).where(Appointment.id == appointment.id).execute()
+
+    @app_commands.command(name="list", description="Listet alle Termine dieses Kanals auf.")
+    @app_commands.describe(show_all="Zeige die Liste für alle an.")
+    async def cmd_appointments_list(self, interaction: Interaction, show_all: bool = False):
+        """ List (and link) all Appointments in the current channel """
+        await interaction.response.defer(ephemeral=not show_all)
+
+        appointments = Appointment.select().where(Appointment.channel == interaction.channel_id)
+        if appointments:
             answer = f'Termine dieses Channels:\n'
-            delete = []
 
-            for message_id, appointment in channel_appointments.items():
+            for appointment in appointments:
                 try:
-                    message = await interaction.channel.fetch_message(int(message_id))
-                    answer += f'{appointment["date_time"]}: {appointment["title"]} => ' \
+                    message = await interaction.channel.fetch_message(appointment.message)
+                    answer += f'<t:{int(appointment.date_time.timestamp())}:F>: {appointment.title} => ' \
                               f'{message.jump_url}\n'
                 except errors.NotFound:
-                    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)
+                    appointment.delete_instance(recursive=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))
+            await interaction.edit_original_response(content=answer)
         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()
+            await interaction.edit_original_response(content="Für diesen Channel existieren derzeit keine Termine")
 
 
 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()
+    await bot.add_cog(Appointments(bot))
+    bot.add_view(AppointmentView())
\ No newline at end of file
diff --git a/extensions/components/module_information/scraper.py b/extensions/components/module_information/scraper.py
deleted file mode 100644
index 135f62a052d440b96f48ef4827ee1107b214a56d..0000000000000000000000000000000000000000
--- a/extensions/components/module_information/scraper.py
+++ /dev/null
@@ -1,198 +0,0 @@
-import json
-import re
-
-import aiohttp
-from bs4 import BeautifulSoup
-
-
-class Scraper:
-    def __init__(self, filename):
-        self.base_url = 'https://www.fernuni-hagen.de'
-        self.courses_file = filename
-
-    async def scrape(self):
-        courses_of_studies = self.load_courses_of_studies()
-        for course in courses_of_studies:
-            await self.fetch_module_infos_for_course_of_studies(course)
-        return courses_of_studies
-
-    async def fetch_module_infos_for_course_of_studies(self, course):
-        url = course['url']
-        html = await self.fetch(url)
-        modules = self.parse_index_page(html)
-        for module in modules:
-            html = await self.fetch(module['url'])
-            module['page'] = self.parse_course_page(html, course)
-        course['modules'] = modules
-
-    def load_courses_of_studies(self):
-        group_file = open(self.courses_file, mode='r')
-        return json.load(group_file)
-
-    async def fetch(self, url):
-        #print (f"Fetching {url}")
-        sess = aiohttp.ClientSession()
-        req = await sess.get(url)
-        text = await req.read()
-        await sess.close()
-        return text
-
-    def prepare_url(self, url):
-        if re.search(r"^http(s)*://", url):
-            return url
-        elif re.search(r"^/", url):
-            return self.base_url + url
-        return self.base_url + "/" + url
-
-    def parse_index_page(self, html):
-        soup = BeautifulSoup(html, "html.parser")
-        modules_source = soup.findAll('a', text=re.compile(r'^[0-9]{5} '))
-        modules = []
-        for item in modules_source:
-            module = {
-                "title": item.get_text(),
-                "number": re.sub('^0+', '', re.search('^([0-9]+) ', item.get_text())[1]),
-                "url": self.prepare_url(item['href'])
-            }
-            modules.append(module)
-        return modules
-
-    def parse_course_page(self, html, stg):
-        soup = BeautifulSoup(html, "html.parser")
-        module = {
-            "title": self.parse_title(soup),
-            "infos": self.parse_infos(soup),
-            "courses": self.parse_courses(soup),
-            "support": self.parse_support(soup),
-            "exams": self.parse_exams(soup),
-            "downloads": self.parse_downloads(soup, stg),
-            "persons": self.parse_persons(soup)
-        }
-        return module
-
-    def parse_title(self, soup):
-        title = re.sub(
-            r" -.*FernUniversität in Hagen",
-            "",
-            soup.title.string,
-            flags=re.S
-        ).strip()
-        return title
-
-    def parse_infos(self, soup):
-        try:
-            info_source = soup.find(summary='Modulinformationen')
-        except:
-            return None
-        if info_source is None:
-            return None
-
-        infos = {
-            "ects": self.get_info(info_source, 'ECTS'),
-            "time": self.get_info(info_source, 'Arbeitsaufwand'),
-            "duration": self.get_info(info_source, 'Dauer des Moduls'),
-            "interval": self.get_info(info_source, 'Häufigkeit des Moduls'),
-            "notes": self.get_info(info_source, 'Anmerkung'),
-            "requirements": self.get_info(info_source, 'Inhaltliche Voraussetzung')
-        }
-
-        return infos
-
-    def get_info(self, info_source, title):
-        th = info_source.find('th', text=title)
-        if th is not None:
-            td = th.findNext('td')
-            if td is not None:
-                return td.get_text()
-        return None
-
-    def parse_courses(self, soup):
-        try:
-            course_source = soup.find('h2', text=re.compile(r'Aktuelles Angebot')) \
-                .findNext('div') \
-                .findAll('a')
-        except:
-            return None
-        courses = []
-        for link in course_source:
-            course = {
-                "name": re.sub('^Kurs [0-9]+ ', '', link.get_text()),
-                "number": re.sub('^0+', '', re.search('([^/]+)$', link['href'])[1]),
-                "url": self.prepare_url(link['href'])
-            }
-            courses.append(course)
-        return courses
-
-    def parse_support(self, soup):
-        try:
-            support_source = soup.find('h2', text=re.compile(
-                r'Mentorielle Betreuung an den Campusstandorten')).findNext('ul').findAll('li')
-        except:
-            return None
-
-        supports = None
-        if support_source:
-            supports = []
-            for item in support_source:
-                support = {
-                    "title": item.get_text(),
-                    "city": item.find('a').get_text(),
-                    "url": self.prepare_url(item.find('a')['href'])
-                }
-                supports.append(support)
-        return supports
-
-    def parse_exams(self, soup):
-        try:
-            exam_source = soup.find(summary='Prüfungsinformationen')
-        except:
-            return None
-        stg = exam_source.findNext('th', colspan='2')
-        exams = []
-        while stg != None:
-            exam = {
-                "name": stg.get_text(),
-                "type": stg.findNext('th', text='Art der Prüfungsleistung').findNext('td').get_text(),
-                "requirements": stg.findNext('th', text='Voraussetzung').findNext('td').get_text(),
-                "weight": stg.findNext('th', text='Stellenwert der Note').findNext('td').get_text(),
-                "hard_requirements": stg.findNext('th', text='Formale Voraussetzungen').findNext('td').get_text()
-            }
-            exams.append(exam)
-            stg = stg.findNext('th', colspan='2')
-        return exams
-
-    def parse_downloads(self, soup, stg):
-        try:
-            source1 = soup.find('h2', text=re.compile(r'Download')) \
-                .findNext('ul', attrs={'class': 'pdfliste'}) \
-                .findAll('li', attrs={'class': None})
-            source2 = soup.find('h2', text=re.compile(r'Download')) \
-                .findNext('ul', attrs={'class': 'pdfliste'}) \
-                .findAll('li', attrs={'class': re.compile(stg['short'])})
-
-            download_source = [*source1, *source2]
-
-        except:
-            return None
-
-        downloads = None
-        if download_source:
-            downloads = []
-            for item in download_source:
-                download = {
-                    "title": item.find('a').get_text(),
-                    "url": self.prepare_url(item.find('a')['href'])
-                }
-                downloads.append(download)
-        return downloads
-
-    def parse_persons(self, soup):
-        try:
-            person_source = soup.find('h2', text=re.compile(
-                r'Ansprechpersonen')).findNext('ul').findAll('h4')
-        except:
-            return None
-        persons = []
-        for item in person_source:
-            persons.append(item.get_text())
-        return persons
diff --git a/extensions/components/poll/poll.py b/extensions/components/poll/poll.py
deleted file mode 100644
index 52ee24f47536fe44d5f85853e2ae0d0a63ba78c8..0000000000000000000000000000000000000000
--- a/extensions/components/poll/poll.py
+++ /dev/null
@@ -1,151 +0,0 @@
-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/extensions/links.py b/extensions/links.py
index 47fde02cbb93f0c81b45569d645f04fcfdaf1a99..575a869bcc0a0a078ef093c1370f7a2388faaf99 100644
--- a/extensions/links.py
+++ b/extensions/links.py
@@ -16,21 +16,28 @@ class Links(commands.GroupCog, name="links", description="Linkverwaltung für Ka
     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")
+        message = "### __Folgende Links sind in diesem Channel hinterlegt__\n"
         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()
+            message = "Für diesen Channel sind noch keine Links hinterlegt."
+        elif category and not models.LinkCategory.has_links(interaction.channel_id, category=category):
+            message = (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` alle Links "
+                       f"in diesem Channel ausgeben.")
+        else:
+            for category in models.LinkCategory.get_categories(interaction.channel_id, category=category):
+                message += f"**{category.name}**\n"
+                if category.links.count() > 0:
+                    for link in category.links:
+                        link_text = f"- [{link.title}](<{link.url}>)\n"
+
+                        if len(message) + len(link_text) > 1900:
+                            await interaction.followup.send(message, ephemeral=not public)
+                            message = ""
+                        message += link_text
+                else:
+                    category.delete_instance()
 
-        await interaction.edit_original_response(embed=embed)
+        await interaction.followup.send(message, ephemeral=not public)
 
     @app_commands.command(name="add", description="Füge einen neuen Link hinzu.")
     async def cmd_add(self, interaction: Interaction):
@@ -44,7 +51,7 @@ class Links(commands.GroupCog, name="links", description="Linkverwaltung für Ka
                                                           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,
+                    LinkModal(category=link.category.name, link_title=link.title, link=link.url, link_id=link.id,
                               title="Link bearbeiten"))
             else:
                 await interaction.response.send_message(content='Ich konnte den Link leider nicht finden.',
diff --git a/extensions/mod_mail.py b/extensions/mod_mail.py
index f8896f9bee028099da11e17ad01ac512f8db53d2..a0bc52591aef5b12390cbabdb05e3aa2c439830f 100644
--- a/extensions/mod_mail.py
+++ b/extensions/mod_mail.py
@@ -1,34 +1,62 @@
 import io
-import os
+from typing import List
 
 import discord
+from discord import Message, Guild
 from discord.ext import commands
 
+from views.mod_mail_view import ModMailView
+
 
 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):
+    async def on_message(self, message: Message) -> None:
         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.")
+            guilds = await self.find_guilds(message.author)
+
+            if len(guilds) == 1:
+                await self.send_modmail(guilds[0], message)
+            else:
+                await message.channel.send(
+                    "Um deine Nachricht an die Moderation des richtigen Server weiterleiten zu können musst du hier bitte den gewünschten Server auswählen.",
+                    view=ModMailView(guilds, message, self.send_modmail))
+
+    async def send_modmail(self, guild: Guild, orig_message: Message) -> None:
+        channel_id = self.bot.get_settings(guild.id).modmail_channel_id
+        channel = await self.bot.fetch_channel(channel_id)
+        files = []
+
+        for attachment in orig_message.attachments:
+            fp = io.BytesIO()
+            await attachment.save(fp)
+            files.append(discord.File(fp, filename=attachment.filename))
+
+        await channel.send(f"Support Nachricht von <@!{orig_message.author.id}>:")
+        try:
+            await channel.send(orig_message.content, files=files, stickers=orig_message.stickers)
+        except discord.Forbidden:
+            await channel.send(f"{orig_message.content}\n+ Sticker:\n{orig_message.stickers[0].url}", files=files)
+        await orig_message.channel.send(f"Vielen Dank für deine Nachricht. Ich habe deine Nachricht an die Moderation "
+                                        f"des Servers {guild.name} weitergeleitet. Es wird sich so schnell wie "
+                                        f"möglich jemand bei dir melden.")
+
+    async def find_guilds(self, user: discord.User) -> List[discord.Guild]:
+        guilds = []
+
+        for guild in self.bot.guilds:
+            try:
+                member = await guild.fetch_member(user.id)
+                guilds.append(guild)
+            except discord.errors.NotFound:
+                pass
+
+        return guilds
 
 
 async def setup(bot: commands.Bot) -> None:
diff --git a/extensions/module_information.py b/extensions/module_information.py
index 2be073a26972961a1e5e5c8494d5795efa45b30d..2d346b4da3a977644a730c1716dfdacff2baf146 100644
--- a/extensions/module_information.py
+++ b/extensions/module_information.py
@@ -1,13 +1,11 @@
 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
+from models import Module, Download
 
 
 class ModuleInformationNotFoundError(Exception):
@@ -18,10 +16,6 @@ class NoCourseChannelError(Exception):
     pass
 
 
-class NoCourseOfStudyError(Exception):
-    pass
-
-
 class Topics(enum.Enum):
     info = 1
     handbuch = 2
@@ -31,182 +25,51 @@ class Topics(enum.Enum):
     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()
+        # self.update_loop.start()
 
     @tasks.loop(hours=24)
+    # Replace with loop that checks if updates happened or not and send a notification in case it did not.
     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
+        pass
+        # await self.refresh_data()
 
     @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
+    @staticmethod
+    async def find_module(channel, number):
+        if not number:
+            try:
+                number = re.search(r"^([0-9]*)-", channel.name)[1]
+            except TypeError:
+                raise NoCourseChannelError
+
+        # At this point we can be sure to have a number. Either passed in from the user as argument or from the channel name
+        if module := Module.get_or_none(Module.number == number):
+            return module
+        else:
+            raise ModuleInformationNotFoundError(f"Zum Modul mit der Nummer {number} konnte ich keine Informationen "
+                                                 f"finden. Bitte geh sicher, dass dies ein gültiges Modul ist. "
+                                                 f"Ansonsten schreibe mir eine Direktnachricht und ich leite sie "
+                                                 f"weiter an das Mod-Team.")
 
+    @staticmethod
+    async def download_for(title, module):
         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)
+        for download in module.downloads.where(Download.title.contains(title)):
+            found = True
+            desc += f"[{download.title}]({download.url})\n"
         if not found:
             raise ModuleInformationNotFoundError
 
         return discord.Embed(title=title,
                              description=desc,
-                             color=19607)
+                             color=19607, url=module.url)
 
     async def handbook(self, module):
         try:
@@ -220,119 +83,115 @@ class ModuleInformation(commands.Cog):
         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"
+    @staticmethod
+    async def info(module):
+        desc = (f"Wie viele Credits bekomme ich? **{module.ects} ECTS**\n"
+                f"Wie lange geht das Modul? **{module.duration}**\n"
+                f"Wie oft wird das Modul angeboten? **{module.interval}**\n"
                 )
 
-        if (requirements := info.get('requirements')) and len(requirements) > 0 and requirements != 'keine':
+        if (requirements := module.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 != '-':
+        if (notes := module.notes) and len(notes) > 0 and notes != '-':
             desc += f"\nAnmerkungen: \n\n{notes}\n"
 
-        if (persons := data['page'].get('persons')) and len(persons) > 0:
+        if (contacts := module.contacts) and len(contacts) > 0:
             desc += f"\nAnsprechparnter: \n"
-            desc += ', '.join(persons) + "\n"
+            desc += ', '.join([contact.name for contact in contacts]) + "\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"
+        if (events := module.events) and len(events) > 0:
+            desc += f"\nAktuelles Angebot: \n"
+            for event in events:
+                desc += f"[{event.name}]({event.url})\n"
 
-        desc += self.stg_string_for_desc(module)
-        return discord.Embed(title=f"Modul {data['title']}",
+        return discord.Embed(title=f"Modul {module.title}",
                              description=desc,
-                             color=19607)
+                             color=19607, url=module.url)
 
-    async def load(self, module):
-        try:
-            data = module['data']['page']['infos']['time']
-            if not data:
-                raise KeyError
-        except KeyError:
-            raise ModuleInformationNotFoundError
+    @staticmethod
+    async def effort(module):
+        if not module.effort or len(module.effort) == 0:
+            raise ModuleInformationNotFoundError(
+                f"Ich kann leider derzeit nichts über den Aufwand des Moduls {module.number}-{module.title} sagen.")
 
-        time = re.sub(r': *(\r*\n*)*', ':\n', data)
-        desc = f"{time}"
-        desc += self.stg_string_for_desc(module)
+        effort = re.sub(r': *(\r*\n*)*', ':\n', module.effort)
         return discord.Embed(title=f"Arbeitsaufwand",
-                             description=desc,
-                             color=19607)
+                             description=f"{effort}",
+                             color=19607, url=module.url)
 
-    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.")
+    @staticmethod
+    async def support(module):
+        if len(module.support) == 0:
+            raise ModuleInformationNotFoundError(
+                f"Ich kann leider derzeit keine Mentoriate für das Modul {module.number}-{module.title} finden.")
 
         desc = ""
-        for support in data:
-            desc += f"[{support['title']}]({support['url']})\n"
-        desc += self.stg_string_for_desc(module)
+        for support in module.support:
+            desc += f"[{support.title}]({support.url})\n"
         return discord.Embed(title=f"Mentoriate ",
                              description=desc,
-                             color=19607)
+                             color=19607, url=module.url)
 
-    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.")
+    @staticmethod
+    async def exams(module):
+        if len(module.exams) == 0:
+            raise ModuleInformationNotFoundError(
+                f"Ich kann leider derzeit keine Prüfungsinformationen für das Modul {module.number}-{module.title} finden.")
 
         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"
+        for exam in module.exams:
+            desc += f"**{exam.name}**\n{exam.type}\n"
+            if exam.weight and len(exam.weight) > 0 and exam.weight != '-':
+                desc += f"Gewichtung: **{exam.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 exam.requirements and len(exam.requirements) > 0 and exam.requirements != 'keine':
+                desc += f"Inhaltliche Voraussetzungen: \n{exam.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)
+            if exam.hard_requirements and len(exam.hard_requirements) > 0 \
+                    and exam.hard_requirements != 'keine':
+                desc += f"Formale Voraussetzungen: \n{exam.hard_requirements}\n\n"
 
         return discord.Embed(title=f"Prüfungsinformationen",
                              description=desc,
-                             color=19607)
+                             color=19607, url=module.url)
+
+    async def get_embed(self, module: Module, topic: Topics):
+        if topic == Topics.handbuch:
+            return await self.handbook(module)
+        elif topic == Topics.leseprobe:
+            return await self.reading_sample(module)
+        elif topic == Topics.aufwand:
+            return await self.effort(module)
+        elif topic == Topics.mentoriate:
+            return await self.support(module)
+        elif topic == Topics.pruefungen:
+            return await self.exams(module)
+        return await self.info(module)
 
     @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):
+                           module_nr="Nummer des Moduls, für das du die Informationen angezeigt bekommen möchtest.")
+    async def cmd_module(self, interaction: Interaction, public: bool, topic: Topics = None, module_nr: int = 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)
+        try:
+            module = await self.find_module(interaction.channel, module_nr)
+            embed = await self.get_embed(module, topic)
+            await interaction.edit_original_response(embed=embed)
+        except NoCourseChannelError:
+            await interaction.edit_original_response(
+                content="Ich konnte keine Modulnummer finden. Bitte gib entweder die Modulnummer direkt an, "
+                        "oder verwende dieses Kommando in einem Modulkanal.")
+        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.")
 
 
 async def setup(bot: commands.Bot) -> None:
diff --git a/extensions/news.py b/extensions/news.py
index aed4424fce4532c4b094dffd60c19ca7fbd575af..ad43ea280c1da902416cef79cb646bf2dd56ed9a 100644
--- a/extensions/news.py
+++ b/extensions/news.py
@@ -1,37 +1,28 @@
-import json
-import os
-
 from aiohttp import ClientSession
 from bs4 import BeautifulSoup
 from discord.ext import commands, tasks
 
+import models
+
 
 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):
+        # ToDo: Add better handling for guild
+        guild = models.Settings.select()[0].guild_id
+        url = self.bot.get_settings(guild).news_url
+        channel_id = self.bot.get_settings(guild).news_channel_id
+
         async with ClientSession() as session:
-            async with session.get(self.url) as r:
+            async with session.get(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)
+                    channel = await self.bot.fetch_channel(channel_id)
 
                     for news in soup.find("ul", attrs={"class": "fu-link-list"}).find_all("li"):
                         date = news.span.text
@@ -41,18 +32,19 @@ class News(commands.Cog):
                         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
+                        if news := models.News.get_or_none(link=link):
+                            if news.date != date:
+                                await self.announce_news(channel, date, title, link)
+                                news.update(date=date).where(models.News.link == link).execute()
                         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()
+                            await self.announce_news(channel, date, title, link)
+                            models.News.create(link=link, date=date)
+
+    async def announce_news(self, channel, date, title, link):
+        guild = models.Settings.select()[0].guild_id
+        news_role = self.bot.get_settings(guild).news_role_id
+        await channel.send(
+            f":loudspeaker: <@&{news_role}> Neues aus der Fakultät vom {date} :loudspeaker: \n{title} \n{link}")
 
 
 async def setup(bot: commands.Bot) -> None:
diff --git a/extensions/polls.py b/extensions/polls.py
index d0ae1194df504cf5e23b108b345c788c3ef018f3..49300a4365a99a4bfaed0eb4b47b1ece95626b16 100644
--- a/extensions/polls.py
+++ b/extensions/polls.py
@@ -1,54 +1,32 @@
-import enum
-import json
-
-import discord
 import emoji
 from discord import app_commands, Interaction
 from discord.ext import commands
 
+from models import *
 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,
+    @app_commands.command(name="add", description="Erstelle eine anonyme Umfrage mit bis zu 20 Antwortmöglichkeiten.")
+    @app_commands.describe(question="Welche Frage möchtest du stellen?", choice_a="1. Antwortmöglichkeit",
+                           choice_b="2. Antwortmöglichkeit", choice_c="3. Antwortmöglichkeit",
+                           choice_d="4. Antwortmöglichkeit", choice_e="5. Antwortmöglichkeit",
+                           choice_f="6. Antwortmöglichkeit", choice_g="7. Antwortmöglichkeit",
+                           choice_h="8. Antwortmöglichkeit", choice_i="9. Antwortmöglichkeit",
+                           choice_j="10. Antwortmöglichkeit", choice_k="11. Antwortmöglichkeit",
+                           choice_l="12. Antwortmöglichkeit", choice_m="13. Antwortmöglichkeit",
+                           choice_n="14. Antwortmöglichkeit", choice_o="15. Antwortmöglichkeit",
+                           choice_p="16. Antwortmöglichkeit", choice_q="17. Antwortmöglichkeit",
+                           choice_r="18. Antwortmöglichkeit", choice_s="19. Antwortmöglichkeit",
+                           choice_t="20. Antwortmöglichkeit")
+    async def cmd_poll(self, interaction: Interaction, question: str, choice_a: str, choice_b: str,
                        choice_c: str = None, choice_d: str = None, choice_e: str = None, choice_f: str = None,
                        choice_g: str = None, choice_h: str = None, choice_i: str = None, choice_j: str = None,
                        choice_k: str = None, choice_l: str = None, choice_m: str = None, choice_n: str = None,
@@ -60,30 +38,14 @@ class Polls(commands.GroupCog, name="poll", description="Handle Polls in Channel
              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))
+        await interaction.response.send_message("Bereite Umfrage vor, bitte warten...", view=PollView())
         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)
-
+        poll = Poll.create(question=question, author=interaction.user.id, channel=interaction.channel_id,
+                           message=message.id)
         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)
+            PollChoice.create(poll_id=poll.id, emoji=choice[0], text=choice[1])
 
-        return embed
+        await interaction.edit_original_response(content="", embed=poll.get_embed(), view=PollView())
 
     def parse_choice(self, idx: int, choice: str):
         choice = choice.strip()
@@ -93,7 +55,7 @@ class Polls(commands.GroupCog, name="poll", description="Handle Polls in Channel
             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]
+                    return [possible_option, choice[index:].strip()]
             elif len(possible_option) > 1:
                 if (possible_option[0:2] == "<:" or possible_option[0:3] == "<a:") and possible_option[-1] == ">":
                     splitted_custom_emoji = possible_option.strip("<a:>").split(":")
@@ -101,12 +63,11 @@ class Polls(commands.GroupCog, name="poll", description="Handle Polls in Channel
                         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 [custom_emoji, choice[index:].strip()]
 
-        return [DEFAULT_CHOICES[idx], choice, 0]
+        return [DEFAULT_CHOICES[idx], choice]
 
 
 async def setup(bot: commands.Bot) -> None:
-    polls = Polls(bot)
-    await bot.add_cog(polls)
-    bot.add_view(PollView(polls))
+    await bot.add_cog(Polls(bot))
+    bot.add_view(PollView())
\ No newline at end of file
diff --git a/extensions/text_commands.py b/extensions/text_commands.py
index e8f073800ceea645043ef84c280800feb0c656df..380a940858f06034a1db4c76ed6adc18ed781468 100644
--- a/extensions/text_commands.py
+++ b/extensions/text_commands.py
@@ -1,191 +1,163 @@
-import json
-import os
 import random
 import re
 
 import discord
-from discord import Interaction, app_commands
-from discord.app_commands import Group
+from discord import app_commands, Interaction
 from discord.ext import commands
 
 import utils
+from models import Command, CommandText
+from views.text_command_view import TextCommandView
 
 
 @app_commands.guild_only()
 class TextCommands(commands.GroupCog, name="commands", description="Text Commands auflisten und verwalten"):
     def __init__(self, bot):
         self.bot = bot
-        self.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)
+        await interaction.response.defer(ephemeral=True)
+        items = []
+        if cmd:
+            if command := Command.get_or_none(Command.command == cmd):
+                items = [command_text.text for command_text in command.texts]
+
+            if len(items) == 0:
+                await interaction.edit_original_response(content=f"{cmd} ist kein verfügbares Text-Command")
+                return
+        else:
+            for command in Command.select():
+                if command.texts.count() > 0:
+                    items.append(command.command)
+
+        answer = f"Text Commands:\n" if cmd is None else f"Für {cmd} hinterlegte Texte:\n"
+        first = True
+        for i, item in enumerate(items):
+            if len(answer) + len(item) > 2000:
+                if first:
+                    await interaction.edit_original_response(content=answer)
+                    first = False
+                else:
+                    await interaction.followup.send(answer, ephemeral=True)
+                answer = f""
+
+            answer += f"{i}: {item}\n"
+
+        if first:
+            await interaction.edit_original_response(content=answer)
+        else:
+            await interaction.followup.send(answer, ephemeral=True)
 
     @app_commands.command(name="add",
-                          description="Ein neues Text Command hinzufügen, oder zu einem bestehenden einen weiteren text hinzufügen.")
+                          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):
+        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 utils.is_mod(interaction.user, self.bot):
             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 self.suggest_command(cmd, text, description, interaction.guild_id)
             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
+        if not utils.is_mod(interaction.user, self.bot):
+            await interaction.edit_original_response(content="Du hast nicht die notwendigen Berechtigungen, "
+                                                             "um dieses Command zu benutzen!")
+            return
+
+        if command := Command.get_or_none(Command.command == cmd):
+            command_texts = list(command.texts)
+            if 0 <= id < len(command_texts):
+                CommandText.update(text=text).where(CommandText.id == command_texts[id].id).execute()
                 await interaction.edit_original_response(
                     content=f"Text {id} für Command {cmd} wurde erfolgreich geändert")
-                self.save_text_commands()
             else:
-                await interaction.edit_original_response(content=f"Ungültiger Index")
+                await interaction.edit_original_response(content="Ungültiger Index")
         else:
-            await interaction.edit_original_response(content=f"Command {cmd} nicht vorhanden!")
+            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!")
+        if not utils.is_mod(interaction.user, self.bot):
+            await interaction.edit_original_response(content="Du hast nicht die notwendigen Berechtigungen, "
+                                                             "um dieses Command zu benutzen!")
+            return
+
+        if command := Command.get_or_none(Command.command == cmd):
+            if id is None:
+                await self.remove_command(command)
+                await interaction.edit_original_response(content=f"Text Command `{cmd}` wurde erfolgreich entfernt.")
             else:
-                if 0 <= id < len(texts):  # schließt Aufrufe von Indizen aus, die außerhalb des Felds wären
-                    del texts[id]
+                command_texts = list(command.texts)
+                if 0 <= id < len(command_texts):
+                    await self.remove_text(command, command_texts, id)
                     await interaction.edit_original_response(
-                        content=f"Text {id} für Command {cmd} wurde erfolgreich entfernt")
-
-                    self.save_text_commands()
+                        content=f"Text {id} für Command `{cmd}` wurde erfolgreich entfernt")
                 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)
+            await interaction.edit_original_response(content=f"Command `{cmd}` nicht vorhanden!")
 
     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)
+        mod_channel_id = self.bot.get_settings(guild_id).modmail_channel_id
+        mod_channel = await self.bot.fetch_channel(mod_channel_id)
+        if command := Command.get_or_none(Command.command == cmd):
+            CommandText.create(text=text, command=command.id)
         else:
             if self.exists(cmd):
                 return False
-            self.text_commands[cmd] = {"description": description, "data": [text]}
-            await self.register_command(cmd, description, guild_id=guild_id)
+            command = Command.create(command=cmd, description=description)
+            CommandText.create(text=text, command=command.id)
+            await self.register_command(command)
 
         await mod_channel.send(f"[{cmd}] => [{text}] erfolgreich hinzugefügt.")
-        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)
+    async def suggest_command(self, cmd: str, text: str, description: str, guild_id: int):
+        mod_channel_id = self.bot.get_settings(guild_id).modmail_channel_id
+        mod_channel = await self.bot.fetch_channel(mod_channel_id)
+        command = Command.get_or_none(Command.command == cmd)
         title = "Vorschlag für neuen Command Text" if command else "Vorschlag für neues Command"
 
-        embed = discord.Embed(title=title,
-                              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)
+        embed = discord.Embed(title=title)
+        embed.add_field(name="Command", value=cmd, inline=False)
+        embed.add_field(name="Text", value=text, inline=False)
         if not command:
             embed.add_field(name="Beschreibung", value=description, inline=False)
 
-        message = await mod_channel.send(embed=embed)
-        await message.add_reaction("👍")
-        await message.add_reaction("👎")
+        await mod_channel.send(embed=embed, view=TextCommandView(self))
+
+    async def remove_text(self, command, command_texts, id):
+        command_text = list(command_texts)[id]
+        command_text.delete_instance(recursive=True)
+        if command.texts.count() == 0:
+            await self.remove_command(command)
+
+    async def remove_command(self, command):
+        await self.unregister_command(command)
+        command.delete_instance(recursive=True)
 
     def exists(self, cmd):
         for command in self.bot.tree.get_commands():
@@ -194,54 +166,36 @@ class TextCommands(commands.GroupCog, name="commands", description="Text Command
 
         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)
+    async def init_commands(self):
+        for command in Command.select():
+            if command.texts.count() > 0:
+                await self.register_command(command, sync=False)
+
+    async def register_command(self, command: Command, sync: bool = True):
+        @app_commands.command(name=command.command, description=command.description)
         @app_commands.guild_only()
         @app_commands.describe(public="Zeige die Ausgabe des Commands öffentlich, für alle Mitglieder sichtbar.")
         async def process_command(interaction: Interaction, public: bool):
             await interaction.response.defer(ephemeral=not public)
-            if command := self.text_commands.get(interaction.command.name):
-                texts = command["data"]
+            if cmd := Command.get_or_none(Command.command == interaction.command.name):
+                texts = list(cmd.texts)
                 if len(texts) > 0:
-                    await interaction.edit_original_response(content=(random.choice(texts)))
+                    await interaction.edit_original_response(content=(random.choice(texts)).text)
                     return
 
             await interaction.edit_original_response(content="FEHLER! Command wurde nicht gefunden!")
 
         self.bot.tree.add_command(process_command)
         if sync:
-            await self.bot.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
+            await self.bot.tree.sync()
 
-        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 unregister_command(self, command: Command):
+        self.bot.tree.remove_command(command.command)
+        await self.bot.tree.sync()
 
 
 async def setup(bot: commands.Bot) -> None:
     text_commands = TextCommands(bot)
     await bot.add_cog(text_commands)
     await text_commands.init_commands()
+    bot.add_view(TextCommandView(text_commands))
diff --git a/extensions/voice.py b/extensions/voice.py
index e0017c93f4822896c3e9c6950fafac35d9aaa5d7..f428ac4fe2c82490aecb277b638f2a604f55188c 100644
--- a/extensions/voice.py
+++ b/extensions/voice.py
@@ -1,5 +1,3 @@
-import os
-
 from discord import app_commands, Interaction
 from discord.app_commands import Choice
 from discord.ext import commands
@@ -7,7 +5,6 @@ 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")
@@ -30,14 +27,16 @@ class Voice(commands.Cog):
     @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)
+            category_id = self.bot.get_settings(member.guild.id).learninggroup_voice_category_id
+            bitrate = self.bot.get_settings(member.guild.id).voice_bitrate
+            category = await self.bot.fetch_channel(category_id)
             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)
+            await category.create_voice_channel(f"Lerngruppen-Voicy-{len(voice_channels) + 1}", bitrate=bitrate)
 
 
 async def setup(bot: commands.Bot) -> None:
diff --git a/extensions/welcome.py b/extensions/welcome.py
index feab886134e51212166fb2ebb39ffded670507f9..dfae9b07192ba649639c33131ca8c86e6e43807e 100644
--- a/extensions/welcome.py
+++ b/extensions/welcome.py
@@ -1,28 +1,18 @@
-import os
-
+from discord import Member
 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):
+    async def on_member_update(self, before: Member, after: Member) -> None:
         if before.pending != after.pending and not after.pending:
-            channel = await self.bot.fetch_channel(int(os.getenv("DISCORD_GREETING_CHANNEL")))
+            channel_id = self.bot.get_settings(before.guild.id).greeting_channel_id
+            channel = await self.bot.fetch_channel(channel_id)
             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/fernuni_bot.py b/fernuni_bot.py
index 75b42a3c96ceca8ac50ddbf3bacae1ddb0742a93..238b33b5cfddde8ebb8873ddce3cef5dd49f274d 100644
--- a/fernuni_bot.py
+++ b/fernuni_bot.py
@@ -7,6 +7,7 @@ from discord.app_commands import Group
 from discord.ext import commands
 from dotenv import load_dotenv
 
+from models import Settings
 from view_manager import ViewManager
 
 # .env file is necessary in the same directory, that contains several strings.
@@ -27,7 +28,6 @@ extensions = ["appointments", "news", "mod_mail", "voice", "welcome", "xkcd", "t
 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)
 
@@ -52,6 +52,14 @@ class Boty(commands.Bot):
         self.view_manager.on_ready()
         print("✅ Client started!")
 
+    @staticmethod
+    def get_settings(guild_id: int) -> Settings:
+        return Settings.get(Settings.guild_id == guild_id)
+
+    @staticmethod
+    def dt_format():
+        return "%d.%m.%Y %H:%M"
+
 
 bot = Boty(command_prefix='!', help_command=None, activity=Game(ACTIVITY), owner_id=OWNER, intents=intents,
            initial_extensions=extensions)
diff --git a/json_import.py b/json_import.py
index 6e8406f5aee760f14099385f5543a9537ddca520..7f02c5cd3bb7709aac874fb7f8ce352991c384d5 100644
--- a/json_import.py
+++ b/json_import.py
@@ -12,7 +12,35 @@ def import_links(json_file: str) -> None:
             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)
+                link = link[1:-1] if link[0] == "<" and link[-1] == ">" else link
+                models.Link.create(url=link, title=title, category=db_category[0].id)
+
+
+def import_news(json_file: str) -> None:
+    file = open(json_file, mode="r")
+    news = json.load(file)
+
+    for link, date in news.items():
+        models.News.create(link=link, date=date)
+
+
+def import_commands(json_file: str) -> None:
+    file = open(json_file, mode="r")
+    commands = json.load(file)
+
+    for command, data in commands.items():
+        db_command = models.Command.get_or_create(command=command, description=data["description"])
+        for text in data["data"]:
+            models.CommandText.create(text=text, command=db_command[0].id)
+
+
+def import_courses(json_file: str) -> None:
+    file = open(json_file, mode="r")
+    courses = json.load(file)
+
+    for course in courses:
+        models.Course.get_or_create(name=course["name"], short=course["short"], url=course["url"],
+                                    role_id=int(course["role"]))
 
 
 if __name__ == "__main__":
@@ -20,3 +48,6 @@ if __name__ == "__main__":
     Make sure to create a database backup before you import data from json files.
     """
     # import_links("data/links.json")
+    # import_news("data/news.json")
+    # import_commands("data/text_commands.json")
+    # import_courses("data/courses_of_studies.json")
diff --git a/modals/link_modal.py b/modals/link_modal.py
index f5572725d4c6e9983676d445f0c8189c234cb32b..8d06c4da99235e33a9f224df989cc3221f50a9eb 100644
--- a/modals/link_modal.py
+++ b/modals/link_modal.py
@@ -39,12 +39,12 @@ class LinkModal(ui.Modal, title='Link hinzufügen'):
         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)
+            models.Link.create(url=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(
+                link.update(title=self.link_title, url=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:
diff --git a/models.py b/models.py
index 71393bcaa14fb4bc67cdcd6fa78cac2efb806a7a..a2ae97d60a359326054a7c9d6f1fc38d026f3856 100644
--- a/models.py
+++ b/models.py
@@ -1,3 +1,7 @@
+import datetime
+import io
+import uuid
+
 import discord
 from peewee import *
 from peewee import ModelSelect
@@ -11,6 +15,18 @@ class BaseModel(Model):
         legacy_table_names = False
 
 
+class Settings(BaseModel):
+    guild_id = IntegerField(default=0)
+    greeting_channel_id = IntegerField(default=0)
+    modmail_channel_id = IntegerField(default=0)
+    news_url = CharField()
+    news_channel_id = IntegerField(default=0)
+    news_role_id = IntegerField(default=0)
+    command_approval_channel_id = IntegerField(default=0)
+    learninggroup_voice_category_id = IntegerField(default=0)
+    voice_bitrate = IntegerField(default=0)
+
+
 class LinkCategory(BaseModel):
     channel = IntegerField()
     name = CharField()
@@ -28,18 +44,198 @@ class LinkCategory(BaseModel):
 
         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):
+    url = CharField()
+    title = CharField()
+    category = ForeignKeyField(LinkCategory, backref='links')
 
 
-class Link(BaseModel):
+class News(BaseModel):
     link = CharField()
+    date = CharField()
+
+
+class Poll(BaseModel):
+    question = CharField()
+    author = IntegerField()
+    channel = IntegerField()
+    message = IntegerField()
+
+    def get_embed(self) -> discord.Embed:
+        embed = discord.Embed(title="Umfrage", description=self.question)
+        embed.add_field(name="Erstellt von", value=f'<@!{self.author}>', inline=False)
+        embed.add_field(name="\u200b", value="\u200b", inline=False)
+
+        for choice in self.choices:
+            name = f'{choice.emoji}  {choice.text}'
+            value = f'{len(choice.participants)}'
+
+            embed.add_field(name=name, value=value, inline=False)
+
+        participants = {str(participant.member_id): 1 for participant in
+                        PollParticipant.select().join(PollChoice, on=PollParticipant.poll_choice).where(
+                            PollChoice.poll == self)}
+
+        embed.add_field(name="\u200b", value="\u200b", inline=False)
+        embed.add_field(name="Anzahl der Teilnehmer an der Umfrage", value=f"{len(participants)}", inline=False)
+
+        return embed
+
+
+class PollChoice(BaseModel):
+    poll = ForeignKeyField(Poll, backref='choices')
+    text = CharField()
+    emoji = CharField()
+
+
+class PollParticipant(BaseModel):
+    poll_choice = ForeignKeyField(PollChoice, backref='participants')
+    member_id = IntegerField()
+
+
+class Command(BaseModel):
+    command = CharField(unique=True)
+    description = CharField()
+
+
+class CommandText(BaseModel):
+    text = CharField()
+    command = ForeignKeyField(Command, backref="texts")
+
+
+class Appointment(BaseModel):
+    channel = IntegerField()
+    message = IntegerField()
+    date_time = DateTimeField()
+    reminder = IntegerField()
     title = CharField()
-    category = ForeignKeyField(LinkCategory, backref='links')
+    description = CharField()
+    author = IntegerField()
+    recurring = IntegerField()
+    reminder_sent = BooleanField()
+    uuid = UUIDField(default=uuid.uuid4())
+
+    def get_embed(self):
+        attendees = self.attendees
+        embed = discord.Embed(title=self.title,
+                              description=f"Wenn du eine Benachrichtigung zum Beginn des Termins"
+                                          f"{f', sowie {self.reminder} Minuten vorher, ' if self.reminder > 0 else f''}"
+                                          f" erhalten möchtest, reagiere mit :thumbsup: auf diese Nachricht.",
+                              color=19607)
+
+        if len(self.description) > 0:
+            embed.add_field(name="Beschreibung", value=self.description, inline=False)
+        embed.add_field(name="Startzeitpunkt", value=f"<t:{int(self.date_time.timestamp())}:F>", inline=False)
+        if self.reminder > 0:
+            embed.add_field(name="Benachrichtigung", value=f"{self.reminder} Minuten vor dem Start", inline=False)
+        if self.recurring > 0:
+            embed.add_field(name="Wiederholung", value=f"Alle {self.recurring} Tage", inline=False)
+        if len(attendees) > 0:
+            embed.add_field(name=f"Teilnehmerinnen ({len(attendees)})",
+                            value=",".join([f"<@{attendee.member_id}>" for attendee in attendees]))
+
+        return embed
+
+    def get_ics_file(self):
+        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:{self.uuid}\n" \
+                      f"SUMMARY:{self.title}\n"
+        appointment += f"RRULE:FREQ=DAILY;INTERVAL={self.recurring}\n" if self.recurring else f""
+        appointment += f"DTSTART;TZID=Europe/Berlin:{self.date_time.strftime(fmt)}00\n" \
+                       f"DTEND;TZID=Europe/Berlin:{self.date_time.strftime(fmt)}00\n" \
+                       f"TRANSP:OPAQUE\n" \
+                       f"BEGIN:VALARM\n" \
+                       f"ACTION:DISPLAY\n" \
+                       f"TRIGGER;VALUE=DURATION:-PT{self.reminder}M\n" \
+                       f"DESCRIPTION:{self.description}\n" \
+                       f"END:VALARM\n" \
+                       f"END:VEVENT\n" \
+                       f"END:VCALENDAR"
+        ics_file = io.BytesIO(appointment.encode("utf-8"))
+        return ics_file
+
+
+class Attendee(BaseModel):
+    appointment = ForeignKeyField(Appointment, backref='attendees')
+    member_id = IntegerField()
+
+
+class Course(BaseModel):
+    name = CharField()
+    short = CharField()
+    url = CharField()
+    role_id = IntegerField()
+
+
+class Module(BaseModel):
+    number = IntegerField(primary_key=True)
+    title = CharField()
+    url = CharField()
+    ects = CharField(null=True)
+    effort = CharField(null=True)
+    duration = CharField(null=True)
+    interval = CharField(null=True)
+    notes = CharField(null=True)
+    requirements = CharField(null=True)
+
+
+class Event(BaseModel):
+    name = CharField()
+    number = CharField()
+    url = CharField()
+    module = ForeignKeyField(Module, backref='events')
+
+
+class Support(BaseModel):
+    title = CharField()
+    city = CharField()
+    url = CharField()
+    module = ForeignKeyField(Module, backref='support')
+
+
+class Exam(BaseModel):
+    name = CharField()
+    type = CharField(null=True)
+    requirements = CharField(null=True)
+    weight = CharField(null=True)
+    hard_requirements = CharField(null=True)
+    module = ForeignKeyField(Module, backref='exams')
+
+
+class Download(BaseModel):
+    title = CharField()
+    url = CharField()
+    module = ForeignKeyField(Module, backref='downloads')
+
+
+class Contact(BaseModel):
+    name = CharField()
+    module = ForeignKeyField(Module, backref='contacts')
 
 
-db.create_tables([LinkCategory, Link], safe=True)
+db.create_tables(
+    [Settings, LinkCategory, Link, News, Poll, PollChoice, PollParticipant, Command, CommandText, Appointment,
+     Attendee, Course, Module, Event, Support, Exam, Download, Contact], safe=True)
diff --git a/module_scraper.py b/module_scraper.py
new file mode 100644
index 0000000000000000000000000000000000000000..51ada03bd390fecc128d4cb42a09f2ee15f74d58
--- /dev/null
+++ b/module_scraper.py
@@ -0,0 +1,179 @@
+import asyncio
+import re
+from typing import List
+
+import aiohttp
+from bs4 import BeautifulSoup
+
+import models
+from models import Course, Module, Event, Support, Exam, Download, Contact
+
+
+class Scraper:
+    def __init__(self):
+        self.base_url = 'https://www.fernuni-hagen.de'
+        self.modules_scraped = {}
+
+    async def scrape(self) -> None:
+        with models.db.transaction() as txn:
+            Module.delete().execute()
+            Event.delete().execute()
+            Support.delete().execute()
+            Exam.delete().execute()
+            Download.delete().execute()
+            Contact.delete().execute()
+            for course in Course.select():
+                print(f"Get modules for {course.name}")
+                await self.fetch_modules(course)
+            modules = Module.select()
+            for idx, module in enumerate(modules):
+                print(f"{idx + 1}/{len(modules)} Get infos for {module.title}")
+                await self.fetch_modules_infos(module)
+            txn.commit()
+
+    async def fetch_modules(self, course: Course) -> None:
+        module_links = self.parse_index_page(await self.fetch(course.url))
+        for module_link in module_links:
+            Module.get_or_create(number=module_link["number"], title=module_link["title"], url=module_link["url"])
+
+    async def fetch_modules_infos(self, module: Module) -> None:
+        html = await self.fetch(module.url)
+        self.parse_module_page(html, module)
+
+    @staticmethod
+    async def fetch(url: str) -> str:
+        async with aiohttp.ClientSession() as session:
+            req = await session.get(url, ssl=False)
+            text = await req.read()
+            return text
+
+    def prepare_url(self, url: str) -> str:
+        if re.search(r"^http(s)*://", url):
+            return url
+        elif re.search(r"^/", url):
+            return self.base_url + url
+        return self.base_url + "/" + url
+
+    def parse_index_page(self, html: str) -> List:
+        soup = BeautifulSoup(html, "html.parser")
+        module_links = soup.findAll('a', string=re.compile(r'^[0-9]{5} '))
+        return [{"title": module_link.get_text()[6:],
+                 "number": int(re.search('^([0-9]+) ', module_link.get_text())[1]),
+                 "url": self.prepare_url(module_link['href']).split("?")[0]}
+                for module_link in module_links]
+
+    def parse_module_page(self, html: str, module: Module) -> None:
+        soup = BeautifulSoup(html, "html.parser")
+        info = self.parse_info(soup)
+        Module.update(ects=info["ects"], effort=info["effort"], duration=info["duration"], interval=info["interval"],
+                      notes=info["notes"], requirements=info["requirements"]).where(
+            Module.number == module.number).execute()
+
+        for event in self.parse_events(soup):
+            Event.create(name=event["name"], number=event["number"], url=event["url"], module=module)
+
+        for support_item in self.parse_support(soup):
+            Support.create(title=support_item["title"], city=support_item["city"], url=support_item["url"],
+                           module=module)
+
+        for exam in self.parse_exams(soup):
+            Exam.create(name=exam["name"], type=exam["type"], requirements=exam["requirements"],
+                        hard_requirements=exam["hard_requirements"], module=module)
+
+        for download in self.parse_downloads(soup):
+            models.Download.create(title=download["title"], url=download["url"], module=module)
+
+        for contact in self.parse_contacts(soup):
+            models.Contact.create(name=contact, module=module)
+
+    def parse_info(self, soup):
+        try:
+            info_source = soup.find(summary='Modulinformationen')
+        except:
+            return None
+        if info_source is None:
+            return None
+
+        return {
+            "ects": self.get_info(info_source, 'ECTS'),
+            "effort": self.get_info(info_source, 'Arbeitsaufwand'),
+            "duration": self.get_info(info_source, 'Dauer des Moduls'),
+            "interval": self.get_info(info_source, 'Häufigkeit des Moduls'),
+            "notes": self.get_info(info_source, 'Anmerkung'),
+            "requirements": self.get_info(info_source, 'Inhaltliche Voraussetzung')
+        }
+
+    def get_info(self, info_source, title):
+        th = info_source.find('th', string=title)
+        if th is not None:
+            td = th.findNext('td')
+            if td is not None:
+                return td.get_text()
+        return None
+
+    def parse_events(self, soup):
+        try:
+            course_source = soup.find('h2', string=re.compile(r'Aktuelles Angebot')) \
+                .findNext('div') \
+                .findAll('a')
+            return [{"name": re.sub('^Kurs [0-9]+ ', '', link.get_text()),
+                     "number": re.sub('^0+', '', re.search('([^/]+)$', link['href'])[1]),
+                     "url": self.prepare_url(link['href'])} for link in course_source]
+        except:
+            return []
+
+    def parse_support(self, soup):
+        try:
+            support_source = soup.find('h2', string=re.compile(
+                r'Mentorielle Betreuung an den Campusstandorten')).findNext('ul').findAll('li')
+        except:
+            support_source = []
+
+        return [{"title": item.get_text(), "city": item.find('a').get_text(),
+                 "url": self.prepare_url(item.find('a')['href'])} for item in support_source]
+
+    @staticmethod
+    def parse_exams(soup):
+        try:
+            exam_source = soup.find(summary='Prüfungsinformationen')
+        except:
+            return []
+        stg = exam_source.findNext('th', colspan='2')
+        exams = []
+        while stg != None:
+            exam = {
+                "name": stg.get_text(),
+                "type": stg.findNext('th', string='Art der Prüfungsleistung').findNext('td').get_text(),
+                "requirements": stg.findNext('th', string='Voraussetzung').findNext('td').get_text(),
+                # "weight": stg.findNext('th', string='Stellenwert der Note').findNext('td').get_text(),
+                "hard_requirements": stg.findNext('th', string='Formale Voraussetzungen').findNext('td').get_text()
+            }
+            exams.append(exam)
+            stg = stg.findNext('th', colspan='2')
+        return exams
+
+    def parse_downloads(self, soup):
+        try:
+            downloads = soup.find('h2', string=re.compile(r'Download')) \
+                .findNext('ul', attrs={'class': 'pdfliste'}) \
+                .findAll('li')
+        except:
+            downloads = []
+
+        return [{"title": download.find('a').get_text(),
+                 "url": self.prepare_url(download.find('a')['href'])}
+                for download in downloads]
+
+    @staticmethod
+    def parse_contacts(soup):
+        try:
+            contacts = soup.find('h2', string=re.compile(
+                r'Ansprechpersonen')).findNext('ul').findAll('h4')
+        except:
+            return []
+        return [contact.get_text() for contact in contacts]
+
+
+if __name__ == "__main__":
+    scraper = Scraper()
+    asyncio.run(scraper.scrape())
diff --git a/requirements.txt b/requirements.txt
index 0f7d9f68c8ecb412a02e9737278ac126503e5121..a00ecc8954bc3598351d30c5513616224a42963d 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,8 +1,21 @@
-aiohttp==3.9.2
-beautifulsoup4==4.12.2
+aiohttp==3.9.3
+beautifulsoup4==4.12.3
 discord.py==2.3.2
-emoji==2.8.0
-peewee==3.16.3
+emoji==2.10.1
+peewee==3.17.1
 PyNaCl==1.5.0
-python-dotenv==1.0.0
-requests==2.31.0
\ No newline at end of file
+python-dotenv==1.0.1
+requests==2.31.0
+
+aiosignal==1.3.1
+attrs==23.2.0
+certifi==2024.2.2
+cffi==1.16.0
+charset-normalizer==3.3.2
+frozenlist==1.4.1
+idna==3.6
+multidict==6.0.5
+pycparser==2.21
+soupsieve==2.5
+urllib3==2.2.1
+yarl==1.9.4
diff --git a/views/appointment_view.py b/views/appointment_view.py
index 8b6ed626d4e6e2f25a72bdc00da0c92e58b4f5e8..e0d3b2003f1fef732c049244453b33a0a572c8fe 100644
--- a/views/appointment_view.py
+++ b/views/appointment_view.py
@@ -1,116 +1,53 @@
-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
+from models import Appointment, Attendee
 
 
 class AppointmentView(discord.ui.View):
-    def __init__(self, appointments):
+    def __init__(self):
         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)
+        if appointment := Appointment.get_or_none(Appointment.message == interaction.message.id):
+            attendee = appointment.attendees.filter(member_id=interaction.user.id)
+            if attendee:
+                await interaction.response.send_message("Du bist bereits Teilnehmerin dieses Termins.",
+                                                        ephemeral=True)
+                return
+            else:
+                Attendee.create(appointment=appointment.id, member_id=interaction.user.id)
+                await interaction.message.edit(embed=appointment.get_embed())
+
+        await interaction.response.defer(thinking=False)
 
     @discord.ui.button(label='Absagen', style=discord.ButtonStyle.red, custom_id='appointment_view:decline', emoji="👎")
     async def decline(self, interaction: discord.Interaction, button: discord.ui.Button):
-        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)
+        if appointment := Appointment.get_or_none(Appointment.message == interaction.message.id):
+            attendee = appointment.attendees.filter(member_id=interaction.user.id)
+            if attendee:
+                attendee = attendee[0]
+                attendee.delete_instance()
+                await interaction.message.edit(embed=appointment.get_embed())
+            else:
+                await interaction.response.send_message("Du kannst nur absagen, wenn du vorher zugesagt hast.",
+                                                        ephemeral=True)
+                return
+
+        await interaction.response.defer(thinking=False)
 
     @discord.ui.button(label='Download .ics', style=discord.ButtonStyle.blurple, custom_id='appointment_view:ics',
                        emoji="📅")
     async def ics(self, interaction: discord.Interaction, button: discord.ui.Button):
-        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)
+        if appointment := Appointment.get_or_none(Appointment.message == interaction.message.id):
+            await interaction.response.send_message("", file=File(appointment.get_ics_file(),
+                                                                  filename=f"{appointment.title}_{appointment.uuid}.ics"), ephemeral=True)
 
     @discord.ui.button(label='Löschen', style=discord.ButtonStyle.gray, custom_id='appointment_view:delete', emoji="🗑")
     async def delete(self, interaction: discord.Interaction, button: discord.ui.Button):
-        await interaction.response.defer(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)
+        await interaction.response.defer(thinking=False)
+        if appointment := Appointment.get_or_none(Appointment.message == interaction.message.id):
+            if interaction.user.id == appointment.author:
+                appointment.delete_instance(recursive=True)
+                await interaction.message.delete()
\ No newline at end of file
diff --git a/views/mod_mail_view.py b/views/mod_mail_view.py
new file mode 100644
index 0000000000000000000000000000000000000000..e59cbfdd709d212a933eb5bcf46f75d62e5310af
--- /dev/null
+++ b/views/mod_mail_view.py
@@ -0,0 +1,25 @@
+from typing import List
+
+import discord
+
+
+class ModMailView(discord.ui.View):
+    def __init__(self, guilds: List[discord.Guild], orig_message, send_modmail):
+        super().__init__()
+        self.add_item(ServerDropdown(guilds, orig_message, send_modmail))
+
+
+class ServerDropdown(discord.ui.Select):
+    def __init__(self, guilds: List[discord.Guild], orig_message, send_modmail):
+        self.guilds = {str(guild.id): guild for guild in guilds}
+        self.orig_message = orig_message
+        self.send_modmail = send_modmail
+        options = [discord.SelectOption(label=guild.name, value=guild_id) for guild_id, guild in self.guilds.items()]
+
+        super().__init__(placeholder='Bitte wähle einen Server aus: ', min_values=0, max_values=1,
+                         options=options)
+
+    async def callback(self, interaction: discord.Interaction):
+        await interaction.response.defer()
+        await self.send_modmail(self.guilds[self.values[0]], self.orig_message)
+        await interaction.delete_original_response()
diff --git a/views/poll_view.py b/views/poll_view.py
index 390eb4e74658210a1d0438da2e8cc65d3c57ecaa..41b9a39602c1d86e8a1ca35659c3617a8d6848dd 100644
--- a/views/poll_view.py
+++ b/views/poll_view.py
@@ -1,119 +1,65 @@
 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)
+from models import Poll, PollParticipant
 
 
 class PollView(discord.ui.View):
-    def __init__(self, polls):
+    def __init__(self):
         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 "
+        if poll := Poll.get_or_none(Poll.message == interaction.message.id):
+            await interaction.response.send_message(
+                f"{poll.question}\n\n*(Nach der Abstimmung kannst du diese Nachricht verwerfen. Wenn die Abstimmung "
                 f"nicht funktioniert, bitte verwirf die Nachricht und Klicke erneut auf den Abstimmen Button der "
-                f"Abstimmung.)*", view=PollChoiceView(poll, interaction.user, interaction.message, self.polls),
+                f"Abstimmung.)*", view=PollChoiceView(poll, interaction.user),
                 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.response.defer(thinking=False)
+        if poll := Poll.get_or_none(Poll.message == interaction.message.id):
+            if interaction.user.id == poll.author:
+                poll.delete_instance(recursive=True)
                 await interaction.edit_original_response(view=None)
 
     @discord.ui.button(label='Löschen', style=discord.ButtonStyle.gray, custom_id='poll_view:delete', emoji="🗑")
     async def delete(self, interaction: discord.Interaction, button: discord.ui.Button):
-        await interaction.response.defer(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.response.defer(thinking=False)
+        if poll := Poll.get_or_none(Poll.message == interaction.message.id):
+            if interaction.user.id == poll.author:
+                poll.delete_instance(recursive=True)
                 await interaction.message.delete()
-                del self.polls[str(interaction.message.id)]
-                self.polls.save()
 
 
 class PollChoiceView(discord.ui.View):
-    def __init__(self, poll, user, message, polls):
+    def __init__(self, poll, user):
         super().__init__(timeout=None)
         self.poll = poll
         self.user = user
-        self.add_item(PollDropdown(poll, user, message, polls))
+        self.add_item(PollDropdown(poll, user))
 
 
 class PollDropdown(discord.ui.Select):
-    def __init__(self, poll, user, message, polls):
+    def __init__(self, poll, user):
         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)
+        options = [discord.SelectOption(label=choice.text, emoji=choice.emoji,
+                                        default=len(choice.participants.filter(member_id=user.id)) > 0) for choice in
+                   poll.choices]
 
-        super().__init__(placeholder='Gib deine Stimme(n) jetzt ab....', min_values=0, max_values=max_values,
+        super().__init__(placeholder='Gib deine Stimme(n) jetzt ab....', min_values=0, max_values=len(options),
                          options=options)
 
     async def callback(self, interaction: discord.Interaction):
-        await interaction.response.defer()
-        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
+        await interaction.response.defer(thinking=False)
+        for choice in self.poll.choices:
+            participants = choice.participants.filter(member_id=self.user.id)
+            if participants and choice.text not in self.values:
+                participants[0].delete_instance()
+            elif not participants and choice.text in self.values:
+                PollParticipant.create(poll_choice_id=choice.id, member_id=self.user.id)
+
+        message = await interaction.channel.fetch_message(self.poll.message)
+        await message.edit(embed=self.poll.get_embed(), view=PollView())
\ No newline at end of file
diff --git a/views/text_command_view.py b/views/text_command_view.py
new file mode 100644
index 0000000000000000000000000000000000000000..61e6816c56b594768f8258ea8af7f655bb854354
--- /dev/null
+++ b/views/text_command_view.py
@@ -0,0 +1,42 @@
+import discord
+from discord import Message
+
+from models import CommandText, Command
+
+
+def get_from_embed(message: Message) -> tuple[str, str, str] | tuple[str, str, None]:
+    embed = message.embeds[0]
+    fields = {field.name: field.value for field in embed.fields}
+
+    return fields.get("Command"), fields.get("Text"), fields.get("Beschreibung")
+
+
+class TextCommandView(discord.ui.View):
+    def __init__(self, text_commands):
+        super().__init__(timeout=None)
+        self.text_commands = text_commands
+
+    @discord.ui.button(emoji="👍", style=discord.ButtonStyle.green, custom_id='text_command_view:approve')
+    async def approve(self, interaction: discord.Interaction, button: discord.ui.Button):
+        await interaction.response.defer(thinking=True)
+        cmd, text, description = get_from_embed(interaction.message)
+
+        if command := Command.get_or_none(Command.command == cmd):
+            CommandText.create(text=text, command=command.id)
+        else:
+            command = Command.create(command=cmd, description=description)
+            CommandText.create(text=text, command=command.id)
+            await self.text_commands.register_command(command)
+        await interaction.followup.send(content=f"Command `{cmd}` mit Text "
+                                                f"`{text}` wurde akzeptiert.")
+
+        await interaction.message.delete()
+
+    @discord.ui.button(emoji="👎", style=discord.ButtonStyle.red, custom_id='text_command_view:decline')
+    async def decline(self, interaction: discord.Interaction, button: discord.ui.Button):
+        await interaction.response.defer(thinking=True)
+        cmd, text, _ = get_from_embed(interaction.message)
+
+        await interaction.followup.send(content=f"Command `{cmd}` mit Text "
+                                                f"`{text}` wurde abgelehnt.")
+        await interaction.message.delete()
\ No newline at end of file