From 824532b90c4cd1b6ec4fe4d34db400ae912d2591 Mon Sep 17 00:00:00 2001
From: dnns01 <mail@dnns01.de>
Date: Fri, 25 Sep 2020 18:11:40 +0200
Subject: [PATCH] Added functionality to add Appointments to a channel

---
 .gitignore          |   2 +
 appointments_cog.py | 158 ++++++++++++++++++++++++++++++++++++++++++++
 fernuni-bot.py      |  11 ++-
 tops.json           |   6 --
 4 files changed, 170 insertions(+), 7 deletions(-)
 create mode 100644 appointments_cog.py
 delete mode 100644 tops.json

diff --git a/.gitignore b/.gitignore
index 4479ec5..34e507a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -116,3 +116,5 @@ GitHub.sublime-settings
 .history
 /roles.json
 /help.json
+/appointments.json
+/tops.json
diff --git a/appointments_cog.py b/appointments_cog.py
new file mode 100644
index 0000000..7e6d51d
--- /dev/null
+++ b/appointments_cog.py
@@ -0,0 +1,158 @@
+import asyncio
+import datetime
+import json
+import re
+
+import discord
+from discord.ext import tasks, commands
+
+
+class AppointmentsCog(commands.Cog):
+    def __init__(self, bot, fmt, APPOINTMENTS_FILE):
+        self.bot = bot
+        self.fmt = fmt
+        self.timer.start()
+        self.appointments = {}
+        self.app_file = APPOINTMENTS_FILE
+        self.load_appointments()
+
+    def cog_unload(self):
+        print("unload")
+        self.timer.cancel()
+
+    def load_appointments(self):
+        """ Loads all appointments from APPOINTMENTS_FILE """
+
+        appointments_file = open(self.app_file, mode='r')
+        self.appointments = json.load(appointments_file)
+
+    @tasks.loop(minutes=1)
+    async def timer(self):
+        delete = []
+
+        for channel_id, channel_appointments in self.appointments.items():
+            for message_id, appointment in channel_appointments.items():
+                now = datetime.datetime.now()
+                date_time = datetime.datetime.strptime(appointment[0], self.fmt)
+                remind_at = date_time - datetime.timedelta(minutes=appointment[1])
+
+                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[2]}\" ist "
+
+                        if appointment[1] > 0 and diff > 0:
+                            answer += f"in {diff} Minuten fällig."
+                            appointment[1] = 0
+                        else:
+                            answer += f"jetzt fällig. :loudspeaker: "
+                            delete.append(message_id)
+
+                        answer += f"\n"
+                        for reaction in reactions:
+                            if reaction.emoji == "👍":
+                                async for user in reaction.users():
+                                    if user != self.bot.user:
+                                        answer += f"<@!{str(user.id)}>"
+
+                        await channel.send(answer)
+
+                        if str(message.id) in delete:
+                            await message.delete()
+                    except discord.errors.NotFound:
+                        delete.append(message_id)
+
+            if len(delete) > 0:
+                for key in delete:
+                    channel_appointments.pop(key)
+                self.save_appointments()
+
+    @timer.before_loop
+    async def before_timer(self):
+        await asyncio.sleep(60 - datetime.datetime.now().second)
+
+    @commands.command(name="add-appointment")
+    async def cmd_add_appointment(self, ctx, date, time, reminder, title):
+        """ Add appointment to a channel """
+
+        channel = ctx.channel
+        try:
+            date_time = datetime.datetime.strptime(f"{date} {time}", self.fmt)
+        except ValueError:
+            await ctx.send("Fehler! Ungültiges Datums und/oder Zeit Format!")
+            return
+
+        if not re.match(r"^\d+$", reminder):
+            await ctx.send("Fehler! Benachrichtigung muss eine positive ganze Zahl (in Minuten) sein!")
+            return
+
+        embed = discord.Embed(title="Neuer Termin hinzugefügt!",
+                              description=f"Wenn du eine Benachrichtigung zum Beginn des Termins, sowie {reminder} "
+                                          f"Minuten vorher erhalten möchtest, reagiere mit :thumbsup: auf diese Nachricht.",
+                              color=19607)
+
+        embed.add_field(name="Titel", value=title, inline=False)
+        embed.add_field(name="Startzeitpunkt", value=f"{date} {time}", inline=False)
+        embed.add_field(name="Benachrichtigung", value=f"{reminder} Minuten vor dem Start", inline=False)
+
+        message = await ctx.send(embed=embed)
+        await message.add_reaction("👍")
+        await message.add_reaction("🗑️")
+
+        if str(channel.id) not in self.appointments:
+            self.appointments[str(channel.id)] = {}
+
+        channel_appointments = self.appointments.get(str(channel.id))
+        channel_appointments[str(message.id)] = [date_time.strftime(self.fmt), int(reminder), title,
+                                                 ctx.author.id]
+
+        self.save_appointments()
+
+    @commands.command(name="appointments")
+    async def cmd_appointments(self, ctx):
+        """ List (and link) all Appointments in the current channel """
+
+        if str(ctx.channel.id) in self.appointments:
+            channel_appointments = self.appointments.get(str(ctx.channel.id))
+            answer = f'Termine dieses Channels:\n'
+            delete = []
+
+            for message_id, appointment in channel_appointments.items():
+                try:
+                    message = await ctx.channel.fetch_message(int(message_id))
+                    answer += f'{appointment[0]}: {appointment[2]} => ' \
+                              f'{message.jump_url}\n'
+                except discord.errors.NotFound:
+                    delete.append(message_id)
+
+            if len(delete) > 0:
+                for key in delete:
+                    channel_appointments.pop(key)
+                self.save_appointments()
+
+            await ctx.channel.send(answer)
+        else:
+            await ctx.send("Für diesen Channel existieren derzeit keine Termine")
+
+    def add_appointment(self, channel):
+        pass
+
+    def save_appointments(self):
+        appointments_file = open(self.app_file, mode='w')
+        json.dump(self.appointments, appointments_file)
+
+    async def handle_reactions(self, payload):
+        channel = await self.bot.fetch_channel(payload.channel_id)
+        channel_appointments = self.appointments.get(str(payload.channel_id))
+        if channel_appointments:
+            appointment = channel_appointments.get(str(payload.message_id))
+            if appointment:
+                if payload.user_id == appointment[3]:
+                    message = await channel.fetch_message(payload.message_id)
+                    await message.delete()
+                    channel_appointments.pop(str(payload.message_id))
+
+        self.save_appointments()
diff --git a/fernuni-bot.py b/fernuni-bot.py
index aeddb5e..06f1904 100644
--- a/fernuni-bot.py
+++ b/fernuni-bot.py
@@ -1,10 +1,12 @@
 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
 
 # .env file is necessary in the same directory, that contains several strings.
@@ -16,9 +18,13 @@ 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")
+DATE_TIME_FORMAT = os.getenv("DISCORD_DATE_TIME_FORMAT")
 
 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)
+bot.add_cog(appointments_cog)
 assignable_roles = {}
 tops = {}
 
@@ -198,7 +204,7 @@ async def cmd_remove_top(ctx, top):
     """ Remove TOP from a channel """
     channel = ctx.channel
 
-    if not top.isnumeric():
+    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:
@@ -366,6 +372,9 @@ async def on_raw_reaction_add(payload):
                     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)
 
 
 @bot.event
diff --git a/tops.json b/tops.json
deleted file mode 100644
index 73dd0a2..0000000
--- a/tops.json
+++ /dev/null
@@ -1,6 +0,0 @@
-{
-  "731078162334875688": [
-    "Richtig nicer TOP",
-    "Letzter TOP!"
-  ]
-}
\ No newline at end of file
-- 
GitLab