From ccf1913d39996d6753c89276c70c97500a14cc38 Mon Sep 17 00:00:00 2001
From: dnns01 <mail@dnns01.de>
Date: Mon, 5 Oct 2020 12:18:17 +0200
Subject: [PATCH] Reorganisation to more use Cogs for a better structure of the
 code base

---
 appointments_cog.py    |  22 ++-
 fernuni-bot.py         | 403 -----------------------------------------
 fernuni_bot.py         | 119 ++++++++++++
 poll.py => poll_cog.py |  28 +++
 roles_cog.py           | 179 ++++++++++++++++++
 text_commands_cog.py   |  39 ++--
 tops_cog.py            |  86 +++++++++
 utils.py               |  24 +++
 welcome_cog.py         |  56 ++++++
 9 files changed, 528 insertions(+), 428 deletions(-)
 delete mode 100644 fernuni-bot.py
 create mode 100644 fernuni_bot.py
 rename poll.py => poll_cog.py (72%)
 create mode 100644 roles_cog.py
 create mode 100644 tops_cog.py
 create mode 100644 utils.py
 create mode 100644 welcome_cog.py

diff --git a/appointments_cog.py b/appointments_cog.py
index 7e6d51d..5e9c234 100644
--- a/appointments_cog.py
+++ b/appointments_cog.py
@@ -1,6 +1,7 @@
 import asyncio
 import datetime
 import json
+import os
 import re
 
 import discord
@@ -8,18 +9,14 @@ from discord.ext import tasks, commands
 
 
 class AppointmentsCog(commands.Cog):
-    def __init__(self, bot, fmt, APPOINTMENTS_FILE):
+    def __init__(self, bot):
         self.bot = bot
-        self.fmt = fmt
+        self.fmt = os.getenv("DISCORD_DATE_TIME_FORMAT")
         self.timer.start()
         self.appointments = {}
-        self.app_file = APPOINTMENTS_FILE
+        self.app_file = os.getenv("DISCORD_APPOINTMENTS_FILE")
         self.load_appointments()
 
-    def cog_unload(self):
-        print("unload")
-        self.timer.cancel()
-
     def load_appointments(self):
         """ Loads all appointments from APPOINTMENTS_FILE """
 
@@ -156,3 +153,14 @@ class AppointmentsCog(commands.Cog):
                     channel_appointments.pop(str(payload.message_id))
 
         self.save_appointments()
+
+    @commands.Cog.listener()
+    async def on_raw_reaction_add(self, payload):
+        if payload.user_id == self.bot.user.id:
+            return
+
+        if payload.emoji.name in ["🗑️"]:
+            channel = await self.bot.fetch_channel(payload.channel_id)
+            message = await channel.fetch_message(payload.message_id)
+            if len(message.embeds) > 0 and message.embeds[0].title == "Neuer Termin hinzugefügt!":
+                await self.handle_reactions(payload)
diff --git a/fernuni-bot.py b/fernuni-bot.py
deleted file mode 100644
index 0694956..0000000
--- a/fernuni-bot.py
+++ /dev/null
@@ -1,403 +0,0 @@
-import json
-import os
-import re
-
-import discord
-from discord.ext import commands
-from dotenv import load_dotenv
-
-from appointments_cog import AppointmentsCog
-from poll import Poll
-from text_commands_cog import TextCommandsCog
-
-# .env file is necessary in the same directory, that contains several strings.
-load_dotenv()
-TOKEN = os.getenv('DISCORD_TOKEN')
-GUILD = int(os.getenv('DISCORD_GUILD'))
-ACTIVITY = os.getenv('DISCORD_ACTIVITY')
-OWNER = int(os.getenv('DISCORD_OWNER'))
-ROLES_FILE = os.getenv('DISCORD_ROLES_FILE')
-HELP_FILE = os.getenv('DISCORD_HELP_FILE')
-TOPS_FILE = os.getenv('DISCORD_TOPS_FILE')
-APPOINTMENTS_FILE = os.getenv("DISCORD_APPOINTMENTS_FILE")
-TEXT_COMMANDS_FILE = os.getenv("DISCORD_TEXT_COMMANDS_FILE")
-DATE_TIME_FORMAT = os.getenv("DISCORD_DATE_TIME_FORMAT")
-CATEGORY_LERNGRUPPEN = os.getenv("DISCORD_CATEGORY_LERNGRUPPEN")
-
-PIN_EMOJI = "📌"
-bot = commands.Bot(command_prefix='!', help_command=None, activity=discord.Game(ACTIVITY), owner_id=OWNER)
-appointments_cog = AppointmentsCog(bot, DATE_TIME_FORMAT, APPOINTMENTS_FILE)
-text_commands_cog = TextCommandsCog(bot, TEXT_COMMANDS_FILE)
-bot.add_cog(appointments_cog)
-bot.add_cog(text_commands_cog)
-assignable_roles = {}
-tops = {}
-
-
-def get_guild():
-    """ Returns an guild object, that matches the id specified in GUILD.
-    This guild is the FU Hagen Informatik/Mathematik guild."""
-
-    for guild in bot.guilds:
-        if guild.id == GUILD:
-            return guild
-
-    return None
-
-
-def get_key(role):
-    """ Get the key for a given role. This role is used for adding or removing a role from a user. """
-
-    for key, role_name in assignable_roles.items():
-        if role_name == role.name:
-            return key
-
-
-def get_member(user):
-    """ Get Member from passed user """
-
-    if type(user) is discord.Member:
-        return user
-    elif type(user) is discord.User:
-        guild = get_guild()
-        if guild is not None:
-            return guild.get_member(user.id)
-    return None
-
-
-def get_roles(user=None):
-    """ Get all roles assigned to a member, or all roles available on the discord server
-    (in both cases only roles are returned, that are defined in roles.json). """
-    roles_list = []
-    roles_dict = {}
-
-    if user is not None:
-        member = get_member(user)
-        if member is not None:
-            roles_list = member.roles
-    else:
-        guild = get_guild()
-        if guild is not None:
-            roles_list = guild.roles
-
-    for role in roles_list:
-        role_key = get_key(role)
-        if role_key is not None:
-            roles_dict[role_key] = role
-    return roles_dict
-
-
-def get_reaction(reactions):
-    """ Returns the reaction, that is equal to the specified PIN_EMOJI,
-    or if that reaction does not exist in list of reactions, None will be returned"""
-
-    for reaction in reactions:
-        if reaction.emoji == PIN_EMOJI:
-            return reaction
-    return None
-
-
-async def send_dm(user, message, embed=None):
-    """ Send DM to a user/member """
-
-    if type(user) is discord.User or type(user) is discord.Member:
-        if user.dm_channel is None:
-            await user.create_dm()
-
-        await user.dm_channel.send(message, embed=embed)
-
-
-@bot.command(name="help")
-async def cmd_help(ctx):
-    """ Send help message as DM """
-
-    help_file = open(HELP_FILE, mode='r')
-    help_dict = json.load(help_file)
-    embed = discord.Embed.from_dict(help_dict)
-    await send_dm(ctx.author, "", embed=embed)
-
-
-def get_role_embed(title, roles):
-    """ Returns an embed that represents all the roles that are passed to this function """
-
-    embed = discord.Embed(title=title,
-                          description="Bei jeder Rolle siehst du oben in Fett den Key der Rolle und "
-                                      "darunter den Namen der Rolle",
-                          color=19607)
-    embed.add_field(name="\u200B", value="\u200B", inline=False)
-
-    for key, role in roles.items():
-        embed.add_field(name=key, value=role.name, inline=False)
-
-    return embed
-
-
-@bot.command(name="all-roles")
-async def cmd_all_roles(message):
-    """ Send all available roles that can be assigned to a member by this bot as DM """
-
-    roles = get_roles()
-    embed = get_role_embed("Alle verfügbaren Rollen", roles)
-    await send_dm(message.author, "", embed=embed)
-
-
-@bot.command(name="my-roles")
-async def cmd_my_roles(message):
-    """ Send the roles assigned to a member as DM. """
-
-    roles = get_roles(message.author)
-    embed = get_role_embed("Dir zugewiesene Rollen", roles)
-    await send_dm(message.author, "", embed=embed)
-
-
-@bot.command(name="add-roles")
-async def cmd_add_roles(ctx, *args):
-    """ Add yourself one or more roles """
-
-    if len(args) > 0:
-        await modify_roles(ctx, True, args)
-
-
-@bot.command(name="remove-roles")
-async def cmd_remove_roles(ctx, *args):
-    """ Remove roles assigned to you """
-
-    if len(args):
-        await modify_roles(ctx, False, args)
-
-
-@bot.command(name="add-role")
-@commands.is_owner()
-async def cmd_add_role(ctx, key, role):
-    """ Add a Role to be assignable (Admin-Command only) """
-
-    assignable_roles[key] = role
-    roles_file = open(ROLES_FILE, mode='w')
-    json.dump(assignable_roles, roles_file)
-
-    if key in assignable_roles:
-        await send_dm(ctx.author, f"Rolle {role} wurde hinzugefügt")
-    else:
-        await send_dm(ctx.author, f"Fehler beim Hinzufügen der Rolle {role}")
-
-
-@bot.command(name="poll")
-async def cmd_poll(ctx, question, *answers):
-    """ Create poll """
-
-    await Poll(bot, question, answers, ctx.author.id).send_poll(ctx)
-
-
-@bot.command(name="add-top")
-async def cmd_add_top(ctx, top):
-    """ Add TOP to a channel """
-
-    channel = ctx.channel
-
-    if str(channel.id) not in tops:
-        tops[str(channel.id)] = []
-
-    channel_tops = tops.get(str(channel.id))
-    channel_tops.append(top)
-
-    tops_file = open(TOPS_FILE, mode='w')
-    json.dump(tops, tops_file)
-
-
-@bot.command(name="remove-top")
-async def cmd_remove_top(ctx, top):
-    """ Remove TOP from a channel """
-    channel = ctx.channel
-
-    if not re.match(r'^-?\d+$', top):
-        await ctx.send("Fehler! Der übergebene Parameter muss eine Zahl sein")
-    else:
-        if str(channel.id) in tops:
-            channel_tops = tops.get(str(channel.id))
-
-            if 0 < int(top) <= len(channel_tops):
-                del channel_tops[int(top) - 1]
-
-            if len(channel_tops) == 0:
-                tops.pop(str(channel.id))
-
-            tops_file = open(TOPS_FILE, mode='w')
-            json.dump(tops, tops_file)
-
-
-@bot.command(name="clear-tops")
-async def cmd_clear_tops(ctx):
-    """ Clear all TOPs from a channel """
-
-    channel = ctx.channel
-
-    if str(channel.id) in tops:
-        tops.pop(str(channel.id))
-        tops_file = open(TOPS_FILE, mode='w')
-        json.dump(tops, tops_file)
-
-
-@bot.command(name="tops")
-async def cmd_tops(ctx):
-    """ Get all TOPs from a channel """
-
-    channel = ctx.channel
-
-    embed = discord.Embed(title="Tagesordnungspunkte",
-                          color=19607)
-    embed.add_field(name="\u200B", value="\u200B", inline=False)
-
-    if str(channel.id) in tops:
-        channel_tops = tops.get(str(channel.id))
-
-        for i in range(0, len(channel_tops)):
-            embed.add_field(name=f"TOP {i + 1}", value=channel_tops[i], inline=False)
-    else:
-        embed.add_field(name="Keine Tagesordnungspunkte vorhanden", value="\u200B", inline=False)
-
-    await ctx.send(embed=embed)
-
-
-def load_roles():
-    """ Loads all assignable roles from ROLES_FILE """
-    global assignable_roles
-    roles_file = open(ROLES_FILE, mode='r')
-    assignable_roles = json.load(roles_file)
-
-
-def load_tops():
-    """ Loads all TOPs from TOPS_FILE """
-    global tops
-    tops_file = open(TOPS_FILE, mode='r')
-    tops = json.load(tops_file)
-
-
-async def modify_roles(ctx, add, args):
-    """ Add or remove roles assigned to a member. Multiple roles can be added with one command, or removed. """
-
-    guild = get_guild()
-
-    if guild is not None:
-        member = get_member(ctx.author)
-
-        roles = get_roles()
-        for key in args:
-            if key in roles:
-                role = roles[key]
-                if add:
-                    try:
-                        await member.add_roles(role)
-                        await send_dm(ctx.author, f'Dir wurde die Rolle {role.name} hinzugefügt')
-                    except Exception:
-                        await send_dm(ctx.author, f'Fehler bei der Zuweisung der Rolle {role.name}')
-                else:
-                    try:
-                        await member.remove_roles(role)
-                        await send_dm(ctx.author, f'Dir wurde die Rolle {role.name} entfernt')
-                    except Exception:
-                        await send_dm(ctx.author, f'Fehler bei der Entfernung der Rolle {role.name}')
-
-
-@bot.command(name="stats")
-async def cmd_stats(message):
-    """ Sends stats in Chat. """
-
-    guild = get_guild()
-    members = await guild.fetch_members().flatten()
-    roles = get_roles()
-    answer = f''
-    embed = discord.Embed(title="Statistiken",
-                          description=f'Wir haben aktuell {len(members)} Mitglieder auf diesem Server, verteilt auf folgende Rollen:')
-    for key, role in roles.items():
-        role_members = role.members
-        if len(role_members) > 0 and not role.name.startswith("Farbe"):
-            embed.add_field(name=role.name, value=f'{len(role_members)} Mitglieder', inline=False)
-
-    no_role = 0
-    for member in members:
-        # ToDo Search for study roles only!
-        if len(member.roles) == 1:
-            no_role += 1
-
-    embed.add_field(name="\u200B", value="\u200b", inline=False)
-    embed.add_field(name="Mitglieder ohne Rolle", value=str(no_role), inline=False)
-
-    await message.channel.send(answer, embed=embed)
-
-
-async def pin_message(message):
-    """ Pin the given message, if it is not already pinned """
-
-    if not message.pinned:
-        await message.pin()
-        await message.channel.send(f'Folgende Nachricht wurde gerade angepinnt: {message.jump_url}')
-
-
-async def unpin_message(message):
-    """ Unpin the given message, if it is pinned, and it has no pin reaction remaining. """
-
-    if message.pinned:
-        reaction = get_reaction(message.reactions)
-        if reaction is None:
-            await message.unpin()
-            await message.channel.send(f'Folgende Nachricht wurde gerade losgelöst: {message.jump_url}')
-
-
-@bot.event
-async def on_ready():
-    print("Client started!")
-    load_roles()
-    load_tops()
-
-
-@bot.event
-async def on_raw_reaction_add(payload):
-    if payload.user_id == bot.user.id:
-        return
-
-    if payload.emoji.name == PIN_EMOJI:
-        channel = await bot.fetch_channel(payload.channel_id)
-        message = await channel.fetch_message(payload.message_id)
-        await pin_message(message)
-    elif payload.emoji.name in ["🗑️", "🛑"]:
-        channel = await bot.fetch_channel(payload.channel_id)
-        message = await channel.fetch_message(payload.message_id)
-        if len(message.embeds) > 0 and message.embeds[0].title == "Umfrage":
-            poll = Poll(bot, message=message)
-            if str(payload.user_id) == poll.author:
-                if payload.emoji.name == "🗑️":
-                    await poll.delete_poll()
-                else:
-                    await poll.close_poll()
-        elif payload.emoji.name == "🗑️" and len(message.embeds) > 0 and \
-                message.embeds[0].title == "Neuer Termin hinzugefügt!":
-            await appointments_cog.handle_reactions(payload)
-    elif payload.emoji.name in ["👍"]:
-        channel = await bot.fetch_channel(payload.channel_id)
-        message = await channel.fetch_message(payload.message_id)
-        if len(message.embeds) > 0 and message.embeds[0].title == "Neuer Motivations Text":
-            await text_commands_cog.motivation_approved(message)
-
-
-@bot.event
-async def on_raw_reaction_remove(payload):
-    if payload.emoji.name == PIN_EMOJI:
-        channel = await bot.fetch_channel(payload.channel_id)
-        message = await channel.fetch_message(payload.message_id)
-        await unpin_message(message)
-
-
-@bot.event
-async def on_voice_state_update(member, before, after):
-    if before.channel != after.channel and after.channel and "Lerngruppen-Voice" in after.channel.name:
-        category = await bot.fetch_channel(CATEGORY_LERNGRUPPEN)
-        voice_channels = category.voice_channels
-
-        for voice_channel in voice_channels:
-            if len(voice_channel.members) == 0:
-                return
-
-        await category.create_voice_channel(f"Lerngruppen-Voice-{len(voice_channels) + 1}")
-
-
-bot.run(TOKEN)
diff --git a/fernuni_bot.py b/fernuni_bot.py
new file mode 100644
index 0000000..ba366f5
--- /dev/null
+++ b/fernuni_bot.py
@@ -0,0 +1,119 @@
+import json
+import os
+
+import discord
+from discord.ext import commands
+from dotenv import load_dotenv
+
+# from welcome_cog import WelcomeCog
+import utils
+from appointments_cog import AppointmentsCog
+from poll_cog import PollCog
+from roles_cog import RolesCog
+from text_commands_cog import TextCommandsCog
+from tops_cog import TopsCog
+
+# .env file is necessary in the same directory, that contains several strings.
+load_dotenv()
+TOKEN = os.getenv('DISCORD_TOKEN')
+GUILD = int(os.getenv('DISCORD_GUILD'))
+ACTIVITY = os.getenv('DISCORD_ACTIVITY')
+OWNER = int(os.getenv('DISCORD_OWNER'))
+ROLES_FILE = os.getenv('DISCORD_ROLES_FILE')
+HELP_FILE = os.getenv('DISCORD_HELP_FILE')
+CATEGORY_LERNGRUPPEN = os.getenv("DISCORD_CATEGORY_LERNGRUPPEN")
+PIN_EMOJI = "📌"
+
+bot = commands.Bot(command_prefix='!', help_command=None, activity=discord.Game(ACTIVITY), owner_id=OWNER)
+poll_cog = PollCog(bot)
+appointments_cog = AppointmentsCog(bot)
+text_commands_cog = TextCommandsCog(bot)
+tops_cog = TopsCog(bot)
+roles_cog = RolesCog(bot)
+# welcome_cog = WelcomeCog(bot)
+bot.add_cog(appointments_cog)
+bot.add_cog(text_commands_cog)
+bot.add_cog(poll_cog)
+bot.add_cog(tops_cog)
+bot.add_cog(roles_cog)
+
+
+# bot.add_cog(welcome_cog)
+
+
+def get_reaction(reactions):
+    """ Returns the reaction, that is equal to the specified PIN_EMOJI,
+    or if that reaction does not exist in list of reactions, None will be returned"""
+
+    for reaction in reactions:
+        if reaction.emoji == PIN_EMOJI:
+            return reaction
+    return None
+
+
+@bot.command(name="help")
+async def cmd_help(ctx):
+    """ Send help message as DM """
+
+    help_file = open(HELP_FILE, mode='r')
+    help_dict = json.load(help_file)
+    embed = discord.Embed.from_dict(help_dict)
+    await utils.send_dm(ctx.author, "", embed=embed)
+
+
+async def pin_message(message):
+    """ Pin the given message, if it is not already pinned """
+
+    if not message.pinned:
+        await message.pin()
+        await message.channel.send(f'Folgende Nachricht wurde gerade angepinnt: {message.jump_url}')
+
+
+async def unpin_message(message):
+    """ Unpin the given message, if it is pinned, and it has no pin reaction remaining. """
+
+    if message.pinned:
+        reaction = get_reaction(message.reactions)
+        if reaction is None:
+            await message.unpin()
+            await message.channel.send(f'Folgende Nachricht wurde gerade losgelöst: {message.jump_url}')
+
+
+@bot.event
+async def on_ready():
+    print("Client started!")
+
+
+@bot.event
+async def on_raw_reaction_add(payload):
+    if payload.user_id == bot.user.id:
+        return
+
+    if payload.emoji.name == PIN_EMOJI:
+        channel = await bot.fetch_channel(payload.channel_id)
+        message = await channel.fetch_message(payload.message_id)
+        await pin_message(message)
+
+
+@bot.event
+async def on_raw_reaction_remove(payload):
+    if payload.emoji.name == PIN_EMOJI:
+        channel = await bot.fetch_channel(payload.channel_id)
+        message = await channel.fetch_message(payload.message_id)
+        await unpin_message(message)
+
+
+@bot.event
+async def on_voice_state_update(member, before, after):
+    if before.channel != after.channel and after.channel and "Lerngruppen-Voice" in after.channel.name:
+        category = await bot.fetch_channel(CATEGORY_LERNGRUPPEN)
+        voice_channels = category.voice_channels
+
+        for voice_channel in voice_channels:
+            if len(voice_channel.members) == 0:
+                return
+
+        await category.create_voice_channel(f"Lerngruppen-Voice-{len(voice_channels) + 1}")
+
+
+bot.run(TOKEN)
diff --git a/poll.py b/poll_cog.py
similarity index 72%
rename from poll.py
rename to poll_cog.py
index 1deee10..3b0a102 100644
--- a/poll.py
+++ b/poll_cog.py
@@ -1,9 +1,37 @@
 import discord
+from discord.ext import commands
 
 OPTIONS = ["\u0031\u20E3", "\u0032\u20E3", "\u0033\u20E3", "\u0034\u20E3", "\u0035\u20E3", "\u0036\u20E3",
            "\u0037\u20E3", "\u0038\u20E3", "\u0039\u20E3", "\u0040\u20E3"]
 
 
+class PollCog(commands.Cog):
+    def __init__(self, bot):
+        self.bot = bot
+
+    @commands.command(name="poll")
+    async def cmd_poll(self, ctx, question, *answers):
+        """ Create poll """
+
+        await Poll(self.bot, question, answers, ctx.author.id).send_poll(ctx)
+
+    @commands.Cog.listener()
+    async def on_raw_reaction_add(self, payload):
+        if payload.user_id == self.bot.user.id:
+            return
+
+        if payload.emoji.name in ["🗑️", "🛑"]:
+            channel = await self.bot.fetch_channel(payload.channel_id)
+            message = await channel.fetch_message(payload.message_id)
+            if len(message.embeds) > 0 and message.embeds[0].title == "Umfrage":
+                poll = Poll(self.bot, message=message)
+                if str(payload.user_id) == poll.author:
+                    if payload.emoji.name == "🗑️":
+                        await poll.delete_poll()
+                    else:
+                        await poll.close_poll()
+
+
 class Poll:
 
     def __init__(self, bot, question=None, answers=None, author=None, message=None):
diff --git a/roles_cog.py b/roles_cog.py
new file mode 100644
index 0000000..387f0a4
--- /dev/null
+++ b/roles_cog.py
@@ -0,0 +1,179 @@
+import json
+import os
+
+import discord
+from discord.ext import commands
+
+import utils
+
+
+class RolesCog(commands.Cog):
+    def __init__(self, bot):
+        self.bot = bot
+        self.roles_file = os.getenv("DISCORD_ROLES_FILE")
+        self.assignable_roles = {}
+        self.load_roles()
+
+    def load_roles(self):
+        """ Loads all assignable roles from ROLES_FILE """
+
+        roles_file = open(self.roles_file, mode='r')
+        self.assignable_roles = json.load(roles_file)
+
+    def get_guild(self):
+        """ Returns an guild object, that matches the id specified in GUILD.
+        This guild is the FU Hagen Informatik/Mathematik guild."""
+
+        for guild in self.bot.guilds:
+            if guild.id == int(os.getenv('DISCORD_GUILD')):
+                return guild
+
+        return None
+
+    def get_key(self, role):
+        """ Get the key for a given role. This role is used for adding or removing a role from a user. """
+
+        for key, role_name in self.assignable_roles.items():
+            if role_name == role.name:
+                return key
+
+    def get_member(self, user):
+        """ Get Member from passed user """
+
+        if type(user) is discord.Member:
+            return user
+        elif type(user) is discord.User:
+            guild = self.get_guild()
+            if guild is not None:
+                return guild.get_member(user.id)
+        return None
+
+    def get_roles(self, user=None):
+        """ Get all roles assigned to a member, or all roles available on the discord server
+        (in both cases only roles are returned, that are defined in roles.json). """
+        roles_list = []
+        roles_dict = {}
+
+        if user is not None:
+            member = self.get_member(user)
+            if member is not None:
+                roles_list = member.roles
+        else:
+            guild = self.get_guild()
+            if guild is not None:
+                roles_list = guild.roles
+
+        for role in roles_list:
+            role_key = self.get_key(role)
+            if role_key is not None:
+                roles_dict[role_key] = role
+        return roles_dict
+
+    @staticmethod
+    def get_role_embed(title, roles):
+        """ Returns an embed that represents all the roles that are passed to this function """
+
+        embed = discord.Embed(title=title,
+                              description="Bei jeder Rolle siehst du oben in Fett den Key der Rolle und "
+                                          "darunter den Namen der Rolle",
+                              color=19607)
+        embed.add_field(name="\u200B", value="\u200B", inline=False)
+
+        for key, role in roles.items():
+            embed.add_field(name=key, value=role.name, inline=False)
+
+        return embed
+
+    @commands.command(name="all-roles")
+    async def cmd_all_roles(self, message):
+        """ Send all available roles that can be assigned to a member by this bot as DM """
+
+        roles = self.get_roles()
+        embed = self.get_role_embed("Alle verfügbaren Rollen", roles)
+        await utils.send_dm(message.author, "", embed=embed)
+
+    @commands.command(name="my-roles")
+    async def cmd_my_roles(self, message):
+        """ Send the roles assigned to a member as DM. """
+
+        roles = self.get_roles(message.author)
+        embed = self.get_role_embed("Dir zugewiesene Rollen", roles)
+        await utils.send_dm(message.author, "", embed=embed)
+
+    @commands.command(name="add-roles")
+    async def cmd_add_roles(self, ctx, *args):
+        """ Add yourself one or more roles """
+
+        for arg in args:
+            await self.modify_roles(ctx.author, True, arg)
+
+    @commands.command(name="remove-roles")
+    async def cmd_remove_roles(self, ctx, *args):
+        """ Remove roles assigned to you """
+
+        for arg in args:
+            await self.modify_roles(ctx.author, False, arg)
+
+    @commands.command(name="add-role")
+    @commands.is_owner()
+    async def cmd_add_role(self, ctx, key, role):
+        """ Add a Role to be assignable (Admin-Command only) """
+
+        self.assignable_roles[key] = role
+        roles_file = open(self.roles_file, mode='w')
+        json.dump(self.assignable_roles, roles_file)
+
+        if key in self.assignable_roles:
+            await utils.send_dm(ctx.author, f"Rolle {role} wurde hinzugefügt")
+        else:
+            await utils.send_dm(ctx.author, f"Fehler beim Hinzufügen der Rolle {role}")
+
+    async def modify_roles(self, author, add, key):
+        """ Add or remove roles assigned to a member. Multiple roles can be added with one command, or removed. """
+
+        guild = self.get_guild()
+
+        if guild is not None:
+            member = self.get_member(author)
+
+            roles = self.get_roles()
+            if key in roles:
+                role = roles[key]
+                if add:
+                    try:
+                        await member.add_roles(role)
+                        await utils.send_dm(author, f'Dir wurde die Rolle {role.name} hinzugefügt')
+                    except Exception:
+                        await utils.send_dm(author, f'Fehler bei der Zuweisung der Rolle {role.name}')
+                else:
+                    try:
+                        await member.remove_roles(role)
+                        await utils.send_dm(author, f'Dir wurde die Rolle {role.name} entfernt')
+                    except Exception:
+                        await utils.send_dm(author, f'Fehler bei der Entfernung der Rolle {role.name}')
+
+    @commands.command(name="stats")
+    async def cmd_stats(self, ctx):
+        """ Sends stats in Chat. """
+
+        guild = self.get_guild()
+        members = await guild.fetch_members().flatten()
+        roles = self.get_roles()
+        answer = f''
+        embed = discord.Embed(title="Statistiken",
+                              description=f'Wir haben aktuell {len(members)} Mitglieder auf diesem Server, verteilt auf folgende Rollen:')
+        for key, role in roles.items():
+            role_members = role.members
+            if len(role_members) > 0 and not role.name.startswith("Farbe"):
+                embed.add_field(name=role.name, value=f'{len(role_members)} Mitglieder', inline=False)
+
+        no_role = 0
+        for member in members:
+            # ToDo Search for study roles only!
+            if len(member.roles) == 1:
+                no_role += 1
+
+        embed.add_field(name="\u200B", value="\u200b", inline=False)
+        embed.add_field(name="Mitglieder ohne Rolle", value=str(no_role), inline=False)
+
+        await ctx.channel.send(answer, embed=embed)
diff --git a/text_commands_cog.py b/text_commands_cog.py
index feef755..9167eb5 100644
--- a/text_commands_cog.py
+++ b/text_commands_cog.py
@@ -5,12 +5,14 @@ import random
 import discord
 from discord.ext import commands
 
+import utils
+
 
 class TextCommandsCog(commands.Cog):
-    def __init__(self, bot, TEXT_COMMANDS_FILE):
+    def __init__(self, bot):
         self.bot = bot
         self.text_commands = {}
-        self.cmd_file = TEXT_COMMANDS_FILE
+        self.cmd_file = os.getenv("DISCORD_TEXT_COMMANDS_FILE")
         self.load_text_commands()
 
     def load_text_commands(self):
@@ -23,16 +25,6 @@ class TextCommandsCog(commands.Cog):
         text_commands_file = open(self.cmd_file, mode='w')
         json.dump(self.text_commands, text_commands_file)
 
-    def is_mod(ctx):
-        author = ctx.author
-        roles = author.roles
-
-        for role in roles:
-            if role.id == int(os.getenv("DISCORD_MOD_ROLE")):
-                return True
-
-        return False
-
     @commands.Cog.listener(name="on_message")
     async def process_text_commands(self, message):
         if message.author == self.bot.user:
@@ -45,7 +37,7 @@ class TextCommandsCog(commands.Cog):
             await message.channel.send(random.choice(texts))
 
     @commands.command(name="add-text-command")
-    @commands.check(is_mod)
+    @commands.check(utils.is_mod)
     async def cmd_add_text_command(self, ctx, cmd, text):
         texts = self.text_commands.get(cmd)
 
@@ -59,7 +51,7 @@ class TextCommandsCog(commands.Cog):
         await ctx.send(f"[{cmd}] => [{text}] erfolgreich hinzugefügt.")
 
     @commands.command(name="text-commands")
-    @commands.check(is_mod)
+    @commands.check(utils.is_mod)
     async def cmd_text_commands(self, ctx):
         answer = f"Text Commands:\n"
 
@@ -71,7 +63,7 @@ class TextCommandsCog(commands.Cog):
         await ctx.send(answer)
 
     @commands.command(name="texts")
-    @commands.check(is_mod)
+    @commands.check(utils.is_mod)
     async def cmd_texts(self, ctx, cmd):
         texts = self.text_commands.get(cmd)
         answer = f"Für {cmd} hinterlegte Texte: \n"
@@ -87,7 +79,7 @@ class TextCommandsCog(commands.Cog):
         await ctx.send(answer)
 
     @commands.command(name="edit-text")
-    @commands.check(is_mod)
+    @commands.check(utils.is_mod)
     async def cmd_edit_text(self, ctx, cmd, id, text):
         texts = self.text_commands.get(cmd)
 
@@ -103,7 +95,7 @@ class TextCommandsCog(commands.Cog):
             await ctx.send("Command {cmd} nicht vorhanden!")
 
     @commands.command(name="remove-text")
-    @commands.check(is_mod)
+    @commands.check(utils.is_mod)
     async def cmd_remove_text(self, ctx, cmd, id):
         texts = self.text_commands.get(cmd)
 
@@ -123,7 +115,7 @@ class TextCommandsCog(commands.Cog):
             await ctx.send("Command {cmd} nicht vorhanden!")
 
     @commands.command(name="remove-text-command")
-    @commands.check(is_mod)
+    @commands.check(utils.is_mod)
     async def cmd_remove_text_command(self, ctx, cmd):
         if cmd in self.text_commands:
             self.text_commands.pop(cmd)
@@ -150,3 +142,14 @@ class TextCommandsCog(commands.Cog):
 
         await self.cmd_add_text_command(ctx, "!motivation", text)
         await message.delete()
+
+    @commands.Cog.listener()
+    async def on_raw_reaction_add(self, payload):
+        if payload.user_id == self.bot.user.id:
+            return
+
+        if payload.emoji.name in ["👍"]:
+            channel = await self.bot.fetch_channel(payload.channel_id)
+            message = await channel.fetch_message(payload.message_id)
+            if len(message.embeds) > 0 and message.embeds[0].title == "Neuer Motivations Text":
+                await self.motivation_approved(message)
diff --git a/tops_cog.py b/tops_cog.py
new file mode 100644
index 0000000..1c5f54a
--- /dev/null
+++ b/tops_cog.py
@@ -0,0 +1,86 @@
+import json
+import os
+import re
+
+import discord
+from discord.ext import commands
+
+
+class TopsCog(commands.Cog):
+    def __init__(self, bot):
+        self.bot = bot
+        self.tops_file = os.getenv('DISCORD_TOPS_FILE')
+        self.tops = {}
+        self.load_tops()
+
+    def load_tops(self):
+        """ Loads all TOPs from TOPS_FILE """
+
+        tops_file = open(self.tops_file, mode='r')
+        self.tops = json.load(tops_file)
+
+    @commands.command(name="add-top")
+    async def cmd_add_top(self, ctx, top):
+        """ Add TOP to a channel """
+
+        channel = ctx.channel
+
+        if str(channel.id) not in self.tops:
+            self.tops[str(channel.id)] = []
+
+        channel_tops = self.tops.get(str(channel.id))
+        channel_tops.append(top)
+
+        tops_file = open(self.tops_file, mode='w')
+        json.dump(self.tops, tops_file)
+
+    @commands.command(name="remove-top")
+    async def cmd_remove_top(self, ctx, top):
+        """ Remove TOP from a channel """
+        channel = ctx.channel
+
+        if not re.match(r'^-?\d+$', top):
+            await ctx.send("Fehler! Der übergebene Parameter muss eine Zahl sein")
+        else:
+            if str(channel.id) in self.tops:
+                channel_tops = self.tops.get(str(channel.id))
+
+                if 0 < int(top) <= len(channel_tops):
+                    del channel_tops[int(top) - 1]
+
+                if len(channel_tops) == 0:
+                    self.tops.pop(str(channel.id))
+
+                tops_file = open(self.tops_file, mode='w')
+                json.dump(self.tops, tops_file)
+
+    @commands.command(name="clear-tops")
+    async def cmd_clear_tops(self, ctx):
+        """ Clear all TOPs from a channel """
+
+        channel = ctx.channel
+
+        if str(channel.id) in self.tops:
+            self.tops.pop(str(channel.id))
+            tops_file = open(self.tops_file, mode='w')
+            json.dump(self.tops, tops_file)
+
+    @commands.command(name="tops")
+    async def cmd_tops(self, ctx):
+        """ Get all TOPs from a channel """
+
+        channel = ctx.channel
+
+        embed = discord.Embed(title="Tagesordnungspunkte",
+                              color=19607)
+        embed.add_field(name="\u200B", value="\u200B", inline=False)
+
+        if str(channel.id) in self.tops:
+            channel_tops = self.tops.get(str(channel.id))
+
+            for i in range(0, len(channel_tops)):
+                embed.add_field(name=f"TOP {i + 1}", value=channel_tops[i], inline=False)
+        else:
+            embed.add_field(name="Keine Tagesordnungspunkte vorhanden", value="\u200B", inline=False)
+
+        await ctx.send(embed=embed)
diff --git a/utils.py b/utils.py
new file mode 100644
index 0000000..74c7dc5
--- /dev/null
+++ b/utils.py
@@ -0,0 +1,24 @@
+import os
+
+import discord
+
+
+async def send_dm(user, message, embed=None):
+    """ Send DM to a user/member """
+
+    if type(user) is discord.User or type(user) is discord.Member:
+        if user.dm_channel is None:
+            await user.create_dm()
+
+        await user.dm_channel.send(message, embed=embed)
+
+
+def is_mod(ctx):
+    author = ctx.author
+    roles = author.roles
+
+    for role in roles:
+        if role.id == int(os.getenv("DISCORD_MOD_ROLE")):
+            return True
+
+    return False
diff --git a/welcome_cog.py b/welcome_cog.py
new file mode 100644
index 0000000..5ffc3ab
--- /dev/null
+++ b/welcome_cog.py
@@ -0,0 +1,56 @@
+import os
+
+import discord
+from discord.ext import commands
+
+import utils
+
+
+class WelcomeCog(commands.Cog):
+    def __init__(self, bot):
+        self.bot = bot
+        self.channel_id = 731078162334875689
+        self.message_id = 761317936262414378
+
+    @commands.command("update-welcome")
+    @commands.check(utils.is_mod)
+    async def cmd_update_welcome(self, ctx):
+        channel = await self.bot.fetch_channel(self.channel_id)
+        message = await channel.fetch_message(self.message_id)
+
+        embed = discord.Embed(title="Herzlich Willkommen auf dem Discord von Studierenden für Studierende.",
+                              description="Disclaimer: Das hier ist kein offizieller Kanal der Fernuni. Hier findet auch keine offizielle Betreuung durch die Fernuni statt. Dieser Discord dient zum Austausch unter Studierenden über einzelne Kurse, um sich gegenseitig helfen zu können, aber auch um über andere Themen in einen Austausch zu treten. Es soll KEIN Ersatz für die Kanäle der Lehrgebiete sein, wie die Newsgroups, Moodle-Foren und was es noch so gibt. Der Discord soll die Möglichkeit bieten, feste Lerngruppen zu finden und sich in diesen gegenseitig zu helfen und zu treffen. Zudem soll er durch den Austausch in den Kanälen auch eine Art flexible Lerngruppe zu einzelnen Kursen ermöglichen. Daher ist unser Apell an euch: Nutzt bitte auch die Betreuungsangebote der entsprechenden Kurse, in die ihr eingeschrieben seid. ")
+        embed.set_thumbnail(
+            url="https://cdn.discordapp.com/avatars/697842294279241749/c7d3063f39d33862e9b950f72ab71165.webp?size=1024")
+        embed.add_field(name="Lerngruppen",
+                        value="Wenn ihr eine feste Lerngruppe gründen möchtet, dann könnt ihr dafür gerne einen eigenen Textchannel bekommen. Sagt einfach bescheid, dann kann dieser erstellt werden. Ihr könnt dann auch entscheiden, ob nur ihr Zugang zu diesem Channel haben möchtet, oder ob dieser für alle zugänglich sein soll.",
+                        inline=False)
+
+        embed.add_field(name="Vorstellung",
+                        value="Es gibt einen <#731078162334875693>. Wir würden uns freuen, wenn ihr euch kurz vorstellen würdet. So ist es möglich, Gemeinsamkeiten zu entdecken und man weiß ungefähr, mit wem man es zu tun hat. Hier soll auch gar nicht der komplette Lebenslauf stehen, schreibt einfach das, was ihr so über euch mitteilen möchtet.",
+                        inline=False)
+
+        embed.add_field(name="Regeln",
+                        value="Es gibt hier auch ein paar, wenige Regeln, an die wir uns alle halten wollen. Diese findet ihr hier https://discordapp.com/channels/353315134678106113/697729059173433344/709475694157234198",
+                        inline=False)
+
+        embed.add_field(name="Nachrichten anpinnen",
+                        value="Wenn ihr Nachrichten in einem Channel anpinnen möchtet, könnt ihr dafür unseren Bot verwenden. Setzt einfach eine :pushpin: Reaktion auf die entsprechende Nachricht und der pin-bot erledigt den Rest.",
+                        inline=False)
+
+        embed.add_field(name="Rollen",
+                        value="Außerdem haben wir Rollen für die einzelnen Studiengänge. Das soll es in bestimmten Situationen vereinfachen, zu identifizieren, in welchem Studiengang man eingeschrieben ist. Dadurch lassen sich bestimmte Nachrichten und Fragen besser im Kontext zuordnen und die Fragen können passend zum Studiengang präziser beantwortet werden. Die Rollen, die hierfür derzeit zur Verfügung stehen sind:\nB.Sc. Informatik, B.Sc. Mathematik, B.Sc. Wirtschaftsinformatik, B.Sc. Mathematisch-Technische Softwareentwicklung,\nM.Sc. Informatik, M.Sc. Praktische Informatik, M.Sc. Mathematik, M.Sc. Wirtschaftsinformatik. ",
+                        inline=False)
+
+        await message.edit(content="", embed=embed)
+
+        guild = await self.bot.fetch_guild(int(os.getenv('DISCORD_GUILD')))
+        roles_cog = self.bot.get_cog("RolesCog")
+
+        print(roles_cog.assignable_roles)
+
+        for emoji in guild.emojis:
+            if emoji.name in roles_cog.assignable_roles.keys():
+                await message.add_reaction(emoji)
+
+            print(emoji)
-- 
GitLab