From c9b2a41e1ab024cd07915a8f45a7aa5d0f72908e Mon Sep 17 00:00:00 2001
From: dnns01 <git@dnns01.de>
Date: Wed, 30 Aug 2023 00:05:00 +0200
Subject: [PATCH 1/2] Different enhancements to appointments

---
 extensions/appointments.py | 309 ++++++++++++-------------------------
 json_import.py             |  25 +++
 models.py                  |  88 ++++++++++-
 views/appointment_view.py  | 147 ++++++------------
 4 files changed, 257 insertions(+), 312 deletions(-)

diff --git a/extensions/appointments.py b/extensions/appointments.py
index 5ecf147..83e3a79 100644
--- a/extensions/appointments.py
+++ b/extensions/appointments.py
@@ -1,251 +1,134 @@
 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
 
+import models
+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) -> None:
+    message = f"Aufgepasst!\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)
+
+
+def get_view(appointment: models.Appointment) -> AppointmentView:
+    view = AppointmentView()
+    if appointment.recurring == 0:
+        for child in view.children:
+            if child.custom_id == "appointment_view:skip":
+                child.disabled = True
+
+    return view
 
 
 @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=get_view(updated_appointment))
+                            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, errors.Forbidden):
+                    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}", "%d.%m.%Y %H:%M")
         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 interaction.response.send_message("Fehler! Ungültiges Datums und/oder Zeit Format!", ephemeral=True)
+            return
+
+        if date_time <= datetime.now():
+            await interaction.response.send_message("Fehler! Der Termin liegt in der Vergangenheit!", ephemeral=True)
+            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=get_view(appointment))
+        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())
diff --git a/json_import.py b/json_import.py
index 6e8406f..6a1ad1e 100644
--- a/json_import.py
+++ b/json_import.py
@@ -1,4 +1,6 @@
 import json
+import uuid
+from datetime import datetime
 
 import models
 
@@ -15,8 +17,31 @@ def import_links(json_file: str) -> None:
                 models.Link.create(link=link, title=title, category=db_category[0].id)
 
 
+def import_appointments(json_file: str) -> None:
+    file = open(json_file, mode="r")
+    appointments = json.load(file)
+
+    for channel, channel_appointments in appointments.items():
+        for message, appointment in channel_appointments.items():
+            date_time = datetime.strptime(appointment["date_time"], "%d.%m.%Y %H:%M")
+            reminder = appointment["reminder"]
+            title = appointment["title"]
+            author = appointment["author_id"]
+            recurring = appointment.get("recurring") if appointment.get("recurring") else 0
+
+            db_appointment = models.Appointment.get_or_create(channel=int(channel), message=int(message),
+                                                              date_time=date_time, reminder=reminder, title=title,
+                                                              description="", author=author, recurring=recurring,
+                                                              reminder_sent=False, uuid=uuid.uuid4())
+
+            if appointment.get("attendees"):
+                for attendee in appointment.get("attendees"):
+                    models.Attendee.create(appointment=db_appointment[0].id, member_id=attendee)
+
+
 if __name__ == "__main__":
     """
     Make sure to create a database backup before you import data from json files.
     """
     # import_links("data/links.json")
+    # import_appointments("data/appointments.json")
diff --git a/models.py b/models.py
index 71393bc..c7ff3c0 100644
--- a/models.py
+++ b/models.py
@@ -1,3 +1,7 @@
+import io
+import uuid
+from datetime import datetime
+
 import discord
 from peewee import *
 from peewee import ModelSelect
@@ -33,6 +37,9 @@ class LinkCategory(BaseModel):
         for link in self.links:
             value += f"- [{link.title}]({link.link})\n"
 
+        if len(value) > 1024:
+            value = value[:1023]
+
         embed.add_field(name=self.name, value=value, inline=False)
 
 
@@ -42,4 +49,83 @@ class Link(BaseModel):
     category = ForeignKeyField(LinkCategory, backref='links')
 
 
-db.create_tables([LinkCategory, Link], safe=True)
+class Appointment(BaseModel):
+    channel = IntegerField()
+    message = IntegerField()
+    date_time = DateTimeField()
+    reminder = IntegerField()
+    title = CharField()
+    description = CharField()
+    author = IntegerField()
+    recurring = IntegerField()
+    reminder_sent = BooleanField()
+    uuid = UUIDField(unique=True)
+
+    def get_embed(self) -> discord.Embed:
+        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) -> io.BytesIO:
+        fmt: str = "%Y%m%dT%H%M"
+        appointment: str = 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:{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 = io.BytesIO(appointment.encode("utf-8"))
+        return ics_file
+
+
+class Attendee(BaseModel):
+    appointment = ForeignKeyField(Appointment, backref='attendees')
+    member_id = IntegerField()
+
+
+db.create_tables([LinkCategory, Link, Appointment, Attendee], safe=True)
diff --git a/views/appointment_view.py b/views/appointment_view.py
index 8b6ed62..285b775 100644
--- a/views/appointment_view.py
+++ b/views/appointment_view.py
@@ -1,116 +1,67 @@
-import io
-from datetime import datetime
+from datetime import timedelta
 
 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)
+    async def on_accept(self, interaction: discord.Interaction, button: discord.ui.Button):
+        if appointment := Appointment.get_or_none(Appointment.message == interaction.message.id):
+            if attendee := appointment.attendees.filter(member_id=interaction.user.id):
+                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)
+    async def on_decline(self, interaction: discord.Interaction, button: discord.ui.Button):
+        if appointment := Appointment.get_or_none(Appointment.message == interaction.message.id):
+            if attendee := appointment.attendees.filter(member_id=interaction.user.id):
+                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='Übersringen', style=discord.ButtonStyle.blurple, custom_id='appointment_view:skip',
+                       emoji="⏭️")
+    async def on_skip(self, interaction: discord.Interaction, button: discord.ui.Button):
+        await interaction.response.defer(thinking=False)
+        if appointment := Appointment.get_or_none(Appointment.message == interaction.message.id):
+            if interaction.user.id == appointment.author or utils.is_mod(interaction.user):
+                new_date_time = appointment.date_time + timedelta(days=appointment.recurring)
+                Appointment.update(date_time=new_date_time, reminder_sent=False).where(
+                    Appointment.id == appointment.id).execute()
+                updated_appointment = Appointment.get(Appointment.id == appointment.id)
+                await interaction.message.edit(embed=updated_appointment.get_embed())
+
 
     @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)
+    async def on_ics(self, interaction: discord.Interaction, button: discord.ui.Button):
+        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}.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)
+    async def on_delete(self, interaction: discord.Interaction, button: discord.ui.Button):
+        await interaction.response.defer(thinking=False)
+        if appointment := Appointment.get_or_none(Appointment.message == interaction.message.id):
+            if interaction.user.id == appointment.author or utils.is_mod(interaction.user):
+                appointment.delete_instance(recursive=True)
+                await interaction.message.delete()
-- 
GitLab


From 073b86ef54d9c4f824320e1ceb4314f116cdc01f Mon Sep 17 00:00:00 2001
From: dnns01 <git@dnns01.de>
Date: Tue, 17 Sep 2024 23:49:01 +0200
Subject: [PATCH 2/2] Fix skip functionality

---
 extensions/appointments.py |  8 ++++----
 views/appointment_view.py  | 10 +++++++---
 2 files changed, 11 insertions(+), 7 deletions(-)

diff --git a/extensions/appointments.py b/extensions/appointments.py
index 1b6bcae..4eee611 100644
--- a/extensions/appointments.py
+++ b/extensions/appointments.py
@@ -18,7 +18,7 @@ async def send_notification(appointment, channel):
     if appointment.reminder_sent:
         return await channel.send(message, embed=appointment.get_embed(2))
 
-    return await channel.send(message, embed=appointment.get_embed(1), view=AppointmentView())
+    return await channel.send(message, embed=appointment.get_embed(1), view=AppointmentView(can_skip=appointment.recurring > 0))
 
 
 @app_commands.guild_only()
@@ -50,7 +50,7 @@ class Appointments(commands.GroupCog, name="appointments", description="Handle A
                                 Appointment.id == appointment.id).execute()
                             updated_appointment = Appointment.get(Appointment.id == appointment.id)
                             new_message = await channel.send(embed=updated_appointment.get_embed(0),
-                                                             view=AppointmentView())
+                                                             view=AppointmentView(can_skip=appointment.recurring > 0))
                             Appointment.update(message=new_message.id).where(Appointment.id == appointment.id).execute()
                     else:
                         Appointment.update(reminder_sent=True).where(Appointment.id == appointment.id).execute()
@@ -98,7 +98,7 @@ class Appointments(commands.GroupCog, name="appointments", description="Handle A
                                          reminder_sent=reminder == 0, uuid=uuid.uuid4())
         Attendee.create(appointment=appointment, member_id=author_id)
 
-        await interaction.response.send_message(embed=appointment.get_embed(0), view=AppointmentView())
+        await interaction.response.send_message(embed=appointment.get_embed(0), view=AppointmentView(can_skip=appointment.recurring > 0))
         message = await interaction.original_response()
         Appointment.update(message=message.id).where(Appointment.id == appointment.id).execute()
 
@@ -127,4 +127,4 @@ class Appointments(commands.GroupCog, name="appointments", description="Handle A
 
 async def setup(bot: commands.Bot) -> None:
     await bot.add_cog(Appointments(bot))
-    bot.add_view(AppointmentView())
+    bot.add_view(AppointmentView(can_skip=True))
diff --git a/views/appointment_view.py b/views/appointment_view.py
index e3faa4a..724e066 100644
--- a/views/appointment_view.py
+++ b/views/appointment_view.py
@@ -1,12 +1,16 @@
+from datetime import timedelta
+
 import discord
 from discord import File
 
+import utils
 from models import Appointment, Attendee
 
 
 class AppointmentView(discord.ui.View):
-    def __init__(self):
+    def __init__(self, can_skip: bool):
         super().__init__(timeout=None)
+        self.on_skip.disabled = not can_skip
 
     @discord.ui.button(label='Anmelden', style=discord.ButtonStyle.green, custom_id='appointment_view:accept', emoji="👍")
     async def accept(self, interaction: discord.Interaction, button: discord.ui.Button):
@@ -37,7 +41,7 @@ class AppointmentView(discord.ui.View):
 
         await interaction.response.defer(thinking=False)
 
-    @discord.ui.button(label='Übersringen', style=discord.ButtonStyle.blurple, custom_id='appointment_view:skip',
+    @discord.ui.button(label='Überspringen', style=discord.ButtonStyle.blurple, custom_id='appointment_view:skip',
                        emoji="⏭️")
     async def on_skip(self, interaction: discord.Interaction, button: discord.ui.Button):
         await interaction.response.defer(thinking=False)
@@ -47,7 +51,7 @@ class AppointmentView(discord.ui.View):
                 Appointment.update(date_time=new_date_time, reminder_sent=False).where(
                     Appointment.id == appointment.id).execute()
                 updated_appointment = Appointment.get(Appointment.id == appointment.id)
-                await interaction.message.edit(embed=updated_appointment.get_embed())
+                await interaction.message.edit(embed=updated_appointment.get_embed(1 if updated_appointment.reminder_sent and updated_appointment.reminder > 0 else 0))
 
 
     @discord.ui.button(label='Download .ics', style=discord.ButtonStyle.blurple, custom_id='appointment_view:ics',
-- 
GitLab