diff --git a/extensions/appointments.py b/extensions/appointments.py index 0a6860768ae7d9564c5f4f70933beaf025012e87..5ecf1475d0b12e4ad73101b38579e6fe5ced71bc 100644 --- a/extensions/appointments.py +++ b/extensions/appointments.py @@ -1,60 +1,20 @@ import asyncio -import io import json import os import uuid from datetime import datetime, timedelta -from typing import NewType, Union +from typing import NewType, Union, Dict -from discord import app_commands, errors, Embed, File, Interaction, VoiceChannel, StageChannel, TextChannel, \ +from discord import app_commands, errors, Embed, Interaction, VoiceChannel, StageChannel, TextChannel, \ ForumChannel, CategoryChannel, Thread, PartialMessageable from discord.ext import tasks, commands +from views.appointment_view import AppointmentView + Channel = NewType('Channel', Union[ VoiceChannel, StageChannel, TextChannel, ForumChannel, CategoryChannel, Thread, PartialMessageable, None]) -def get_ics_file(title, date_time, reminder, recurring): - fmt = "%Y%m%dT%H%M" - appointment = f"BEGIN:VCALENDAR\n" \ - f"PRODID:Boty McBotface\n" \ - f"VERSION:2.0\n" \ - f"BEGIN:VTIMEZONE\n" \ - f"TZID:Europe/Berlin\n" \ - f"BEGIN:DAYLIGHT\n" \ - f"TZOFFSETFROM:+0100\n" \ - f"TZOFFSETTO:+0200\n" \ - f"TZNAME:CEST\n" \ - f"DTSTART:19700329T020000\n" \ - f"RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=3\n" \ - f"END:DAYLIGHT\n" \ - f"BEGIN:STANDARD\n" \ - f"TZOFFSETFROM:+0200\n" \ - f"TZOFFSETTO:+0100\n" \ - f"TZNAME:CET\n" \ - f"DTSTART:19701025T030000\n" \ - f"RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10\n" \ - f"END:STANDARD\n" \ - f"END:VTIMEZONE\n" \ - f"BEGIN:VEVENT\n" \ - f"DTSTAMP:{datetime.now().strftime(fmt)}00Z\n" \ - f"UID:{uuid.uuid4()}\n" \ - f"SUMMARY:{title}\n" - appointment += f"RRULE:FREQ=DAILY;INTERVAL={recurring}\n" if recurring else f"" - appointment += f"DTSTART;TZID=Europe/Berlin:{date_time.strftime(fmt)}00\n" \ - f"DTEND;TZID=Europe/Berlin:{date_time.strftime(fmt)}00\n" \ - f"TRANSP:OPAQUE\n" \ - f"BEGIN:VALARM\n" \ - f"ACTION:DISPLAY\n" \ - f"TRIGGER;VALUE=DURATION:-PT{reminder}M\n" \ - f"DESCRIPTION:Halloooo, dein Termin findest bald statt!!!!\n" \ - f"END:VALARM\n" \ - f"END:VEVENT\n" \ - f"END:VCALENDAR" - ics_file = io.BytesIO(appointment.encode("utf-8")) - return ics_file - - @app_commands.guild_only() class Appointments(commands.GroupCog, name="appointments", description="Verwaltet Termine in Kanälen"): def __init__(self, bot): @@ -124,72 +84,78 @@ class Appointments(commands.GroupCog, name="appointments", description="Verwalte date_time_str = channel_appointment["date_time"] date_time = datetime.strptime(date_time_str, self.fmt) new_date_time = date_time + timedelta(days=recurring) - new_date_time_str = new_date_time.strftime(self.fmt) - splitted_new_date_time_str = new_date_time_str.split(" ") reminder = channel_appointment.get("original_reminder") reminder = reminder if reminder else 0 await self.add_appointment(channel, channel_appointment["author_id"], - splitted_new_date_time_str[0], - splitted_new_date_time_str[1], + 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() + self.save_appointments() @timer.before_loop async def before_timer(self): await asyncio.sleep(60 - datetime.now().second) + async def add_appointment(self, channel: Channel, author_id: int, date_time: datetime, reminder: int, title: str, + attendees: Dict, ics_uuid: str, description: str = "", recurring: int = None) -> None: + message = await self.send_or_update_appointment(channel, author_id, description, title, date_time, reminder, + recurring, attendees) + + if str(channel.id) not in self.appointments: + self.appointments[str(channel.id)] = {} + + channel_appointments = self.appointments.get(str(channel.id)) + channel_appointments[str(message.id)] = {"date_time": date_time.strftime(self.fmt), "reminder": reminder, + "title": title, "author_id": author_id, "recurring": recurring, + "description": description, "attendees": attendees, + "ics_uuid": ics_uuid} + + self.save_appointments() + @app_commands.command(name="add", description="Fügt dem Kanal einen neunen Termin hinzu.") @app_commands.describe(date="Tag des Termins (z. B. 21.10.2015).", time="Uhrzeit des Termins (z. B. 13:37).", reminder="Wie viele Minuten bevor der Termin startet, soll eine Erinnerung verschickt werden?", - title="Titel des Termins", + title="Titel des Termins.", description="Beschreibung des Termins.", recurring="In welchem Intervall (in Tagen) soll der Termin wiederholt werden?") async def cmd_add_appointment(self, interaction: Interaction, date: str, time: str, reminder: int, title: str, - recurring: int = None): + description: str = "", recurring: int = None): await interaction.response.defer(ephemeral=True) - await self.add_appointment(interaction.channel, interaction.user.id, date, time, reminder, title, recurring) - await interaction.edit_original_response(content="Termin erfolgreich erstellt!") - - # /appointments add date:31.08.2022 time:20:00 reminder:60 title:Test - async def add_appointment(self, channel: Channel, author_id: int, date: str, time: str, reminder: int, title: str, - recurring: int = None) -> None: try: + attendees = {str(interaction.user.id): 1} date_time = datetime.strptime(f"{date} {time}", self.fmt) + if date_time < datetime.now(): + await interaction.edit_original_response( + content="Fehler! Der Termin darf nicht in der Vergangenheit liegen.") + return + await self.add_appointment(interaction.channel, interaction.user.id, date_time, reminder, title, attendees, + str(uuid.uuid4()), description, recurring) + await interaction.edit_original_response(content="Termin erfolgreich erstellt!") except ValueError: - await channel.send("Fehler! Ungültiges Datums und/oder Zeit Format!") - return - - embed = self.get_embed(title, date_time, reminder, recurring) - message = await channel.send(embed=embed, file=File(get_ics_file(title, date_time, reminder, recurring), - filename=f"{title}.ics")) - await message.add_reaction("ðŸ‘") - await message.add_reaction("🗑ï¸") + await interaction.edit_original_response(content="Fehler! Ungültiges Datums und/oder Zeit Format!") - if str(channel.id) not in self.appointments: - self.appointments[str(channel.id)] = {} - - channel_appointments = self.appointments.get(str(channel.id)) - channel_appointments[str(message.id)] = {"date_time": date_time.strftime(self.fmt), "reminder": reminder, - "title": title, "author_id": author_id, "recurring": recurring} - - self.save_appointments() - - def get_embed(self, title: str, date_time: datetime, reminder: int, recurring: int): - embed = Embed(title="Neuer Termin hinzugefügt!", - description=f"Wenn du eine Benachrichtigung zum Beginn des Termins" - f"{f', sowie {reminder} Minuten vorher, ' if reminder > 0 else f''} " - f"erhalten möchtest, reagiere mit :thumbsup: auf diese Nachricht.", + 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="Titel", value=title, inline=False) + 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 @@ -219,30 +185,67 @@ class Appointments(commands.GroupCog, name="appointments", description="Verwalte else: await interaction.followup.send("Für diesen Kanal existieren derzeit keine Termine.", ephemeral=True) - async def handle_reactions(self, payload): - channel = await self.bot.fetch_channel(payload.channel_id) - channel_appointments = self.appointments.get(str(payload.channel_id)) - if channel_appointments: - appointment = channel_appointments.get(str(payload.message_id)) - if appointment: - if payload.user_id == appointment["author_id"]: - message = await channel.fetch_message(payload.message_id) - await message.delete() - channel_appointments.pop(str(payload.message_id)) - + async def send_or_update_appointment(self, channel, organizer, description, title, date_time, reminder, recurring, + attendees, message=None): + embed = self.get_embed(title, organizer, description, date_time, reminder, recurring, attendees) + if message: + return await message.edit(embed=embed, view=AppointmentView(self)) + else: + return await channel.send(embed=embed, view=AppointmentView(self)) + + async def update_legacy_appointments(self): + new_appointments = {} + for channel_id, appointments in self.appointments.items(): + channel_appointments = {} + try: + channel = await self.bot.fetch_channel(int(channel_id)) + + for message_id, appointment in appointments.items(): + if appointment.get("attendees") is not None: + continue + try: + message = await channel.fetch_message(int(message_id)) + title = appointment.get("title") + date_time = appointment.get("date_time") + reminder = appointment.get("reminder") + recurring = appointment.get("recurring") + author_id = appointment.get("author_id") + description = "" + attendees = {} + ics_uuid = str(uuid.uuid4()) + + for reaction in message.reactions: + if reaction.emoji == "ðŸ‘": + async for user in reaction.users(): + if user.id != self.bot.user.id: + attendees[str(user.id)] = 1 + + dt = datetime.strptime(f"{date_time}", self.fmt) + await self.send_or_update_appointment(channel, author_id, description, title, dt, reminder, + recurring, attendees, message=message) + channel_appointments[message_id] = {"date_time": date_time, + "reminder": reminder, + "title": title, + "author_id": author_id, + "recurring": recurring, + "description": description, + "attendees": attendees, + "ics_uuid": ics_uuid} + + except: + pass + except: + pass + + if len(channel_appointments) > 0: + new_appointments[channel_id] = channel_appointments + + self.appointments = new_appointments self.save_appointments() - @commands.Cog.listener() - async def on_raw_reaction_add(self, payload): - if payload.user_id == self.bot.user.id: - return - - if payload.emoji.name in ["🗑ï¸"]: - channel = await self.bot.fetch_channel(payload.channel_id) - message = await channel.fetch_message(payload.message_id) - if len(message.embeds) > 0 and message.embeds[0].title == "Neuer Termin hinzugefügt!": - await self.handle_reactions(payload) - async def setup(bot: commands.Bot) -> None: - await bot.add_cog(Appointments(bot)) + appointments = Appointments(bot) + await bot.add_cog(appointments) + bot.add_view(AppointmentView(appointments)) + await appointments.update_legacy_appointments() diff --git a/views/appointment_view.py b/views/appointment_view.py new file mode 100644 index 0000000000000000000000000000000000000000..8b6ed626d4e6e2f25a72bdc00da0c92e58b4f5e8 --- /dev/null +++ b/views/appointment_view.py @@ -0,0 +1,116 @@ +import io +from datetime import datetime + +import discord +from discord import File + +import utils + + +def get_ics_file(title, date_time, reminder, recurring, description, ics_uuid): + fmt = "%Y%m%dT%H%M" + appointment = f"BEGIN:VCALENDAR\n" \ + f"PRODID:Boty McBotface\n" \ + f"VERSION:2.0\n" \ + f"BEGIN:VTIMEZONE\n" \ + f"TZID:Europe/Berlin\n" \ + f"BEGIN:DAYLIGHT\n" \ + f"TZOFFSETFROM:+0100\n" \ + f"TZOFFSETTO:+0200\n" \ + f"TZNAME:CEST\n" \ + f"DTSTART:19700329T020000\n" \ + f"RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=3\n" \ + f"END:DAYLIGHT\n" \ + f"BEGIN:STANDARD\n" \ + f"TZOFFSETFROM:+0200\n" \ + f"TZOFFSETTO:+0100\n" \ + f"TZNAME:CET\n" \ + f"DTSTART:19701025T030000\n" \ + f"RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10\n" \ + f"END:STANDARD\n" \ + f"END:VTIMEZONE\n" \ + f"BEGIN:VEVENT\n" \ + f"DTSTAMP:{datetime.now().strftime(fmt)}00Z\n" \ + f"UID:{ics_uuid}\n" \ + f"SUMMARY:{title}\n" + appointment += f"RRULE:FREQ=DAILY;INTERVAL={recurring}\n" if recurring else f"" + appointment += f"DTSTART;TZID=Europe/Berlin:{date_time.strftime(fmt)}00\n" \ + f"DTEND;TZID=Europe/Berlin:{date_time.strftime(fmt)}00\n" \ + f"TRANSP:OPAQUE\n" \ + f"BEGIN:VALARM\n" \ + f"ACTION:DISPLAY\n" \ + f"TRIGGER;VALUE=DURATION:-PT{reminder}M\n" \ + f"DESCRIPTION:{description}\n" \ + f"END:VALARM\n" \ + f"END:VEVENT\n" \ + f"END:VCALENDAR" + ics_file = io.BytesIO(appointment.encode("utf-8")) + return ics_file + + +class AppointmentView(discord.ui.View): + def __init__(self, appointments): + super().__init__(timeout=None) + self.appointments = appointments + + @discord.ui.button(label='Zusagen', style=discord.ButtonStyle.green, custom_id='appointment_view:accept', emoji="ðŸ‘") + async def accept(self, interaction: discord.Interaction, button: discord.ui.Button): + await interaction.response.defer(ephemeral=True) + if channel_appointments := self.appointments.appointments.get(str(interaction.channel_id)): + if appointment := channel_appointments.get(str(interaction.message.id)): + if attendees := appointment.get("attendees"): + attendees[str(interaction.user.id)] = 1 + self.appointments.save_appointments() + await self.update_appointment(interaction.message, appointment) + + @discord.ui.button(label='Absagen', style=discord.ButtonStyle.red, custom_id='appointment_view:decline', emoji="👎") + async def decline(self, interaction: discord.Interaction, button: discord.ui.Button): + await interaction.response.defer(ephemeral=True) + if channel_appointments := self.appointments.appointments.get(str(interaction.channel_id)): + if appointment := channel_appointments.get(str(interaction.message.id)): + if attendees := appointment.get("attendees"): + if attendees.get(str(interaction.user.id)): + del attendees[str(interaction.user.id)] + self.appointments.save_appointments() + await self.update_appointment(interaction.message, appointment) + + @discord.ui.button(label='Download .ics', style=discord.ButtonStyle.blurple, custom_id='appointment_view:ics', + emoji="📅") + async def ics(self, interaction: discord.Interaction, button: discord.ui.Button): + await interaction.response.defer(ephemeral=True) + if channel_appointments := self.appointments.appointments.get(str(interaction.channel_id)): + if appointment := channel_appointments.get(str(interaction.message.id)): + title = appointment.get("title") + date_time = datetime.strptime(appointment.get("date_time"), self.appointments.fmt) + reminder = appointment.get("reminder") + recurring = appointment.get("recurring") + description = appointment.get("description") + ics_uuid = appointment.get("ics_uuid") + file = File(get_ics_file(title, date_time, reminder, recurring, description, ics_uuid), + filename=f"{appointment.get('title')}_{appointment.get('ics_uuid')}.ics") + await interaction.followup.send(file=file, ephemeral=True) + + @discord.ui.button(label='Löschen', style=discord.ButtonStyle.gray, custom_id='appointment_view:delete', emoji="🗑") + async def delete(self, interaction: discord.Interaction, button: discord.ui.Button): + await interaction.response.defer(ephemeral=True) + if channel_appointments := self.appointments.appointments.get(str(interaction.channel_id)): + if appointment := channel_appointments.get(str(interaction.message.id)): + if appointment.get("author_id") == interaction.user.id or utils.is_mod(interaction.user): + await interaction.followup.send(f"Termin {appointment.get('title')} gelöscht.", ephemeral=True) + await interaction.message.delete() + del channel_appointments[str(interaction.message.id)] + self.appointments.save_appointments() + + async def update_appointment(self, message, appointment): + channel = message.channel + message = message + author_id = appointment.get("author_id") + description = appointment.get("description") + title = appointment.get("title") + date_time = datetime.strptime(appointment.get("date_time"), self.appointments.fmt) + reminder = appointment.get("reminder") + recurring = appointment.get("recurring") + attendees = appointment.get("attendees") + + await self.appointments.send_or_update_appointment(channel, author_id, description, title, date_time, reminder, + recurring, attendees, message=message) diff --git a/views/joboffers_view.py b/views/joboffers_view.py deleted file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000