diff --git a/cogs/learninggroups.py b/cogs/learninggroups.py index 0db282752fb2f2af24f9d32627e1b0e7203ed592..940d79269823d166bd51a77920a86f38b580505a 100644 --- a/cogs/learninggroups.py +++ b/cogs/learninggroups.py @@ -2,6 +2,7 @@ import json import os import re import time +from typing import Union import disnake from disnake.ext import commands @@ -12,7 +13,7 @@ from cogs.help import help, handle_error, help_category """ Environment Variablen: DISCORD_LEARNINGGROUPS_OPEN - ID der Kategorie für offene Lerngruppen - DISCORD_LEARNINGGROUPS_CLOSE - ID der Kategorie für geschlossene Lerngruppen + DISCORD_LEARNINGGROUPS_CLOSE - ID der Kategorie für private Lerngruppen DISCORD_LEARNINGGROUPS_ARCHIVE - ID der Kategorie für archivierte Lerngruppen DISCORD_LEARNINGGROUPS_REQUEST - ID des Channels in welchem Requests vom Bot eingestellt werden DISCORD_LEARNINGGROUPS_INFO - ID des Channels in welchem die Lerngruppen-Informationen gepostet/aktualisert werden @@ -21,9 +22,12 @@ from cogs.help import help, handle_error, help_category DISCORD_MOD_ROLE - ID der Moderator Rolle von der erweiterte Lerngruppen-Actionen ausgeführt werden dürfen """ +LG_OPEN_SYMBOL = f'🌲' +LG_CLOSE_SYMBOL = f'🛑' +LG_LISTED_SYMBOL = f'📖' @help_category("learninggroups", "Lerngruppen", - "Mit dem Lerngruppen-Feature kannst du Lerngruppen-Kanäle beantragen und/oder diese rudimentär verwalten.", + "Mit dem Lerngruppen-Feature kannst du Lerngruppen-Kanäle beantragen und verwalten.", "Hier kannst du Lerngruppen-Kanäle anlegen, beantragen und verwalten.") class LearningGroups(commands.Cog): def __init__(self, bot): @@ -38,12 +42,19 @@ class LearningGroups(commands.Cog): self.channel_info = os.getenv('DISCORD_LEARNINGGROUPS_INFO') self.group_file = os.getenv('DISCORD_LEARNINGGROUPS_FILE') self.header_file = os.getenv('DISCORD_LEARNINGGROUPS_COURSE_FILE') + self.support_channel = os.getenv('DISCORD_SUPPORT_CHANNEL') self.mod_role = os.getenv("DISCORD_MOD_ROLE") - self.groups = {} - self.header = {} + self.guild_id = os.getenv("DISCORD_GUILD") + self.groups = {} # owner and learninggroup-member ids + self.channels = {} # complete channel configs + self.header = {} # headlines for statusmessage self.load_groups() self.load_header() + @commands.Cog.listener(name="on_ready") + async def on_ready(self): + await self.update_channels() + def load_header(self): file = open(self.header_file, mode='r') self.header = json.load(file) @@ -55,15 +66,22 @@ class LearningGroups(commands.Cog): def load_groups(self): group_file = open(self.group_file, mode='r') self.groups = json.load(group_file) - - def save_groups(self): + if not self.groups.get("groups"): + self.groups['groups'] = {} + if not self.groups.get("requested"): + self.groups['requested'] = {} + if not self.groups.get("messageids"): + self.groups['messageids'] = [] + + async def save_groups(self): + await self.update_channels() group_file = open(self.group_file, mode='w') json.dump(self.groups, group_file) def arg_open_to_bool(self, arg_open): if arg_open in ["offen", "open"]: return True - if arg_open in ["geschlossen", "closed", "close"]: + if arg_open in ["geschlossen", "closed", "close", "private", "privat"]: return False return None @@ -91,7 +109,7 @@ class LearningGroups(commands.Cog): if channel_config['is_open'] is None: if command: await ctx.channel.send( - f"Fehler! Bitte gib an ob die Gruppe **offen** (**open**) oder **geschlossen** (**closed**) ist. Gib `!help {command}` für Details ein.") + f"Fehler! Bitte gib an ob die Gruppe **offen** (**open**) oder **privat** (**private**) ist. Gib `!help {command}` für Details ein.") return False if not re.match(r"^[0-9]+$", channel_config['course']): if command: @@ -121,10 +139,11 @@ class LearningGroups(commands.Cog): return category def full_channel_name(self, channel_config): - return (f"{f'🌲' if channel_config['is_open'] else f'🛑'}" - f"{channel_config['course']}-{channel_config['name']}-{channel_config['semester']}") + return (f"{LG_OPEN_SYMBOL if channel_config['is_open'] else LG_CLOSE_SYMBOL}" + f"{channel_config['course']}-{channel_config['name']}-{channel_config['semester']}" + f"{LG_LISTED_SYMBOL if channel_config['is_listed'] else ''}") - async def update_groupinfo(self): + async def update_statusmessage(self): info_message_ids = self.groups.get("messageids") channel = await self.bot.fetch_channel(int(self.channel_info)) @@ -136,13 +155,14 @@ class LearningGroups(commands.Cog): msg = f"**Lerngruppen**\n\n" course_msg = "" - sorted_groups = sorted(self.groups["groups"].values( - ), key=lambda group: f"{group['course']}-{group['name']}") - open_groups = [group for group in sorted_groups if group['is_open']] + sorted_channels = sorted(self.channels.values( + ), key=lambda channel: f"{channel['course']}-{channel['name']}") + open_channels = [channel for channel in sorted_channels if channel['is_open'] or channel['is_listed']] courseheader = None - for group in open_groups: + no_headers = [] + for lg_channel in open_channels: - if group['course'] != courseheader: + if lg_channel['course'] != courseheader: if len(msg) + len(course_msg) > self.msg_max_len: message = await channel.send(msg) info_message_ids.append(message.id) @@ -151,157 +171,270 @@ class LearningGroups(commands.Cog): else: msg += course_msg course_msg = "" - header = self.header.get(group['course']) + header = self.header.get(lg_channel['course']) if header: course_msg += f"**{header}**\n" else: - course_msg += f"**{group['course']} - -------------------------------------**\n" - courseheader = group['course'] - - groupchannel = await self.bot.fetch_channel(int(group['channel_id'])) - course_msg += f" {groupchannel.mention}\n" + course_msg += f"**{lg_channel['course']} - -------------------------------------**\n" + no_headers.append(lg_channel['course']) + courseheader = lg_channel['course'] + + groupchannel = await self.bot.fetch_channel(int(lg_channel['channel_id'])) + course_msg += f" {groupchannel.mention}" + + if lg_channel['is_listed']: + group_config = self.groups["groups"].get(lg_channel['channel_id']) + if group_config: + user = await self.bot.fetch_user(group_config['owner_id']) + if user: + course_msg += f" **@{user.name}**" + course_msg += f"\n **↳** `!lg join {groupchannel.id}`" + course_msg += "\n" msg += course_msg message = await channel.send(msg) + if len(no_headers) > 0: + support_channel = await self.bot.fetch_channel(int(self.support_channel)) + if support_channel: + await support_channel.send(f"Es fehlen noch Ãœberschriften für folgende Kurse in der Lerngruppenübersicht: **{', '.join(no_headers)}**") info_message_ids.append(message.id) self.groups["messageids"] = info_message_ids - self.save_groups() + await self.save_groups() async def archive(self, channel): + group_config = self.groups["groups"].get(str(channel.id)) + if not group_config: + await channel.send("Das ist kein Lerngruppenkanal.") + return category = await self.bot.fetch_channel(self.category_archive) await self.move_channel(channel, category) await channel.edit(name=f"archiv-${channel.name[1:]}") - self.remove_group(channel) + await self.remove_group(channel) + await self.update_permissions(channel) - async def set_channel_state(self, channel, is_open): - channel_config = self.groups["groups"][str(channel.id)] + async def set_channel_state(self, channel, is_open=None, is_listed=None): + channel_config = self.channels[str(channel.id)] if await self.check_rename_rate_limit(channel_config): - return # prevent api requests when ratelimited + return False # prevent api requests when ratelimited - was_open = channel_config["is_open"] - if (was_open == is_open): - return # prevent api requests when nothing changed + if is_open is not None: + was_open = channel_config["is_open"] + if was_open == is_open: + return False # prevent api requests when nothing changed + channel_config["is_open"] = is_open - channel_config["is_open"] = is_open - channel_config["last_rename"] = int(time.time()) + if is_listed is not None and not channel_config["is_open"]: + was_listed = channel_config["is_listed"] + if was_listed == is_listed: + return False # prevent api requests when nothing changed + channel_config["is_listed"] = is_listed + self.groups["groups"][str(channel.id)]["last_rename"] = int(time.time()) await channel.edit(name=self.full_channel_name(channel_config)) category = await self.category_of_channel(is_open) - await self.move_channel(channel, category) - await self.update_groupinfo() - self.save_groups() + await self.move_channel(channel, category, sync=is_open) + await self.save_groups() + await self.update_statusmessage() + return True async def set_channel_name(self, channel, name): - channel_config = self.groups["groups"][str(channel.id)] + channel_config = self.channels[str(channel.id)] if await self.check_rename_rate_limit(channel_config): return # prevent api requests when ratelimited + self.groups["groups"][str(channel.id)]["last_rename"] = int(time.time()) channel_config["name"] = name - channel_config["last_rename"] = int(time.time()) await channel.edit(name=self.full_channel_name(channel_config)) - await self.update_groupinfo() - self.save_groups() + await self.save_groups() + await self.update_statusmessage() - async def move_channel(self, channel, category): + async def move_channel(self, channel, category, sync=True): for sortchannel in category.text_channels: if sortchannel.name[1:] > channel.name[1:]: - await channel.move(category=category, before=sortchannel, sync_permissions=True) + await channel.move(category=category, before=sortchannel, sync_permissions=sync) return - await channel.move(category=category, sync_permissions=True, end=True) + await channel.move(category=category, sync_permissions=sync, end=True) async def add_requested_group_channel(self, message, direct=False): - channel_config = self.groups["requested"].get(str(message.id)) - - category = await self.category_of_channel(channel_config["is_open"]) - channel_name = self.full_channel_name(channel_config) - channel = await category.create_text_channel(channel_name) - channel_config["channel_id"] = str(channel.id) - - user = await self.bot.fetch_user(channel_config["owner_id"]) - await utils.send_dm(user, - f"Deine Lerngruppe <#{channel.id}> wurde eingerichtet. Du kannst mit **!open** und **!close** den Status dieser Gruppe setzen. Bedenke aber bitte, dass die Discord API die möglichen Namensänderungen stark limitiert. Daher ist nur ein Statuswechsel alle **5 Minuten** möglich.") - - self.groups["groups"][str(channel.id)] = channel_config - - self.remove_group_request(message) + requested_channel_config = self.groups["requested"].get(str(message.id)) + + category = await self.category_of_channel(requested_channel_config["is_open"]) + full_channel_name = self.full_channel_name(requested_channel_config) + channel = await category.create_text_channel(full_channel_name) + await self.move_channel(channel, category, False) + user = await self.bot.fetch_user(requested_channel_config["owner_id"]) + + await channel.send(f":wave: <@!{user.id}>, hier ist deine neue Lerngruppe.\n" + "Es gibt offene und private Lerngruppen. Eine offene Lerngruppe ist für jeden sichtbar " + "und jeder kann darin schreiben. Eine private Lerngruppe ist unsichtbar und auf eine " + "Gruppe an Kommilitoninnen beschränkt." + "```" + "Besitzerfunktionen:\n" + "!lg addmember @: Fügt ein Mitglied zur Lerngruppe hinzu.\n" + "!lg owner <@newowner>: Ändert die Besitzerin der Lerngruppe auf @newowner.\n" + "!lg open: Öffnet eine private Lerngruppe.\n" + "!lg close: Stellt die Lerngruppe auf privat.\n" + "!lg show: Zeigt eine private Lerngruppe in der Lerngruppenliste an.\n" + "!lg hide: Entfernt eine private Lerngruppe aus der Lerngruppenliste.\n" + "!lg kick @user: Schließt einen Benutzer von der Lerngruppe aus.\n" + "\nKommandos für alle:\n" + "!lg id: Zeigt die ID der Lerngruppe an mit der andere Kommilitoninnen beitreten können.\n" + "!lg members: Zeigt die Mitglieder der Lerngruppe an.\n" + "!lg owner: Zeigt die Besitzerin der Lerngruppe.\n" + "!lg leave: Du verlässt die Lerngruppe.\n" + "!lg join: Anfrage stellen in die Lerngruppe aufgenommen zu werden.\n" + "\nMit dem nachfolgenden Kommando kann eine Kommilitonin darum" + "bitten in die Lerngruppe aufgenommen zu werden wenn diese bereits privat ist.\n" + f"!lg join {channel.id}" + "\n(manche Kommandos sind von Discord limitiert und können nur einmal alle 5 Minuten ausgeführt werden)" + "```" + ) + + self.groups["groups"][str(channel.id)] = {"owner_id": requested_channel_config["owner_id"]} + + await self.remove_group_request(message) if not direct: await message.delete() - await self.update_groupinfo() - self.save_groups() + await self.save_groups() + await self.update_statusmessage() + if not requested_channel_config["is_open"]: + await self.update_permissions(channel) - def remove_group_request(self, message): + async def remove_group_request(self, message): del self.groups["requested"][str(message.id)] - self.save_groups() + await self.save_groups() - def remove_group(self, channel): + async def remove_group(self, channel): del self.groups["groups"][str(channel.id)] - self.save_groups() - - @help( - category="learninggroups", - brief="Erstellt aus den Lerngruppen-Kanälen eine Datendatei. ", - description=( - "Initialisiert alle Gruppen in den Kategorien für offene und geschlossene Lerngruppen und baut die Verwaltungsdaten dazu auf. " - "Die Lerngruppen-Kanal-Namen müssen hierfür zuvor ins Format #{symbol}{kursnummer}-{name}-{semester} gebracht werden. " - "Als Owner wird der ausführende Account für alle Lerngruppen gesetzt. " - "Wenn die Verwaltungsdatenbank nicht leer ist, wird das Kommando nicht ausgeführt. " - ), - mod=True - ) - @commands.command(name="init-groups") - @commands.check(utils.is_mod) - async def cmd_init_groups(self, ctx): - if len(self.groups["groups"]) > 0: - await ctx.channel.send("Nope. Das sollte ich lieber nicht tun.") - return - - msg = "Initialisierung abgeschlossen:\n" + await self.save_groups() + + def channel_to_channel_config(self, channel): + cid = str(channel.id) + is_listed = channel.name[-1] == LG_LISTED_SYMBOL + result = re.match(r"([0-9]{4,6})-(.*)-([a-z0-9]+)$", channel.name[1:] if not is_listed else channel.name[1:-1]) + is_open = channel.name[0] == LG_OPEN_SYMBOL + course, name, semester = result.group(1, 2, 3) + + channel_config = {"course": course, "name": name, "category": channel.category_id, "semester": semester, + "is_open": is_open, "is_listed": is_listed, "channel_id": cid} + if self.groups["groups"].get(cid): + channel_config.update(self.groups["groups"].get(cid)) + return channel_config + + async def update_channels(self): + self.channels = {} for is_open in [True, False]: category = await self.category_of_channel(is_open) - msg += f"**{category.name}**\n" for channel in category.text_channels: - result = re.match( - r"([0-9]{4,6})-(.*)-([a-z0-9]+)$", channel.name[1:]) - if result is None: - await utils.send_dm(ctx.author, f"Abbruch! Channelname hat falsches Format: {channel.name}") - self.groups["groups"] = {} - return + channel_config = self.channel_to_channel_config(channel) + + self.channels[str(channel.id)] = channel_config + + async def add_member_to_group(self, channel: disnake.TextChannel, arg_member: disnake.Member): + group_config = self.groups["groups"].get(str(channel.id)) + if not group_config: + await channel.send("Das ist kein Lerngruppenkanal.") + return + + users = group_config.get("users") + if not users: + users = {} + mid = str(arg_member.id) + if not users.get(mid): + users[mid] = True + group_config["users"] = users + + await self.save_groups() + + async def remove_member_from_group(self, channel: disnake.TextChannel, arg_member: disnake.Member): + group_config = self.groups["groups"].get(str(channel.id)) + if not group_config: + await channel.send("Das ist kein Lerngruppenkanal.") + return + + users = group_config.get("users") + if not users: + return + mid = str(arg_member.id) + users.pop(mid, None) - course, name, semester = result.group(1, 2, 3) + await self.save_groups() - channel_config = {"owner_id": ctx.author.id, "course": course, "name": name, "semester": semester, - "is_open": is_open, "channel_id": str(channel.id)} - if not await self.is_channel_config_valid(ctx, channel_config): - await utils.send_dm(ctx.author, f"Abbruch! Channelname hat falsches Format: {channel.name}") - self.groups["groups"] = {} - return + async def update_permissions(self, channel): + overwrites = await self.overwrites(channel) + await channel.edit(overwrites=overwrites) - self.groups["groups"][str(channel.id)] = channel_config - msg += f" #{course}-{name}-{semester}\n" + async def overwrites(self, channel): + channel = await self.bot.fetch_channel(str(channel.id)) + group_config = self.groups["groups"].get(str(channel.id)) + guild = await self.bot.fetch_guild(int(self.guild_id)) - await utils.send_dm(ctx.author, msg) - await self.update_groupinfo() - self.save_groups() + overwrites = {guild.default_role: disnake.PermissionOverwrite(read_messages=False)} + + if not group_config: + return overwrites + + owner = self.bot.get_user(group_config["owner_id"]) + if not owner: + return overwrites + + overwrites[owner] = disnake.PermissionOverwrite(read_messages=True) + users = group_config.get("users") + if not users: + return overwrites + + for userid in users.keys(): + user = await self.bot.fetch_user(userid) + overwrites[user] = disnake.PermissionOverwrite(read_messages=True) + + return overwrites + + @help( + category="learninggroups", + syntax="!lg <command>", + brief="Lerngruppenverwaltung" + ) + @commands.group(name="lg", aliases=["learninggroup", "lerngruppe"], pass_context=True) + async def cmd_lg(self, ctx): + return + #pass + # if not ctx.invoked_subcommand: + # await self.cmd_module_info(ctx) + + @help( + command_group="lg", + category="learninggroups", + brief="Updated die Lerngruppenliste", + mod=True + ) + @cmd_lg.command(name="update") + @commands.check(utils.is_mod) + async def cmd_update(self, ctx): + await self.update_channels() + await self.update_statusmessage() @help( + command_group="lg", category="learninggroups", - syntax="!add-course <coursenumber> <name...>", - brief="Fügt einen Kurs als neue Ãœberschrift in Botys Lerngruppen-Liste (Kanal #lerngruppen) hinzu. Darf Leerzeichen enthalten, Anführungszeichen sind nicht erforderlich.", - example="!add-course 1141 Mathematische Grundlagen", + syntax="!lg header <coursenumber> <name...>", + brief="Fügt einen Kurs als neue Ãœberschrift in Botys Lerngruppen-Liste (Kanal #lerngruppen) hinzu. " + "Darf Leerzeichen enthalten, Anführungszeichen sind nicht erforderlich.", + example="!lg header 1141 Mathematische Grundlagen", parameters={ "coursenumber": "Nummer des Kurses wie von der Fernuni angegeben (ohne führende Nullen z. B. 1142).", "name...": "Ein frei wählbarer Text (darf Leerzeichen enthalten).", }, - description="Kann auch zum Bearbeiten einer Ãœberschrift genutzt werden. Bei bereits existierender Kursnummer wird die Ãœberschrift abgeändert", + description="Kann auch zum Bearbeiten einer Ãœberschrift genutzt werden. Bei bereits existierender " + "Kursnummer wird die Ãœberschrift abgeändert", mod=True ) - @commands.command(name="add-course") + @cmd_lg.command(name="header") @commands.check(utils.is_mod) - async def cmd_add_course(self, ctx, arg_course, *arg_name): + async def cmd_add_header(self, ctx, arg_course, *arg_name): if not re.match(r"[0-9]+", arg_course): await ctx.channel.send( f"Fehler! Die Kursnummer muss numerisch sein. Gib `!help add-course` für Details ein.") @@ -309,173 +442,383 @@ class LearningGroups(commands.Cog): self.header[arg_course] = f"{arg_course} - {' '.join(arg_name)}" self.save_header() - await self.update_groupinfo() + await self.update_statusmessage() @help( + command_group="lg", category="learninggroups", - syntax="!add-group <coursenumber> <name> <semester> <status> <@usermention>", - example="!add-group 1142 mathegenies sose22 clsoed @someuser", + syntax="!lg add <coursenumber> <name> <semester> <status> <@usermention>", + example="!lg add 1142 mathegenies sose22 closed @someuser", brief="Fügt einen Lerngruppen-Kanal hinzu. Der Name darf keine Leerzeichen enthalten.", parameters={ "coursenumber": "Nummer des Kurses wie von der Fernuni angegeben (ohne führende Nullen z. B. 1142).", "name": "Ein frei wählbarer Text ohne Leerzeichen. Bindestriche sind zulässig.", - "semester": "Das Semester, für welches diese Lerngruppe erstellt werden soll. sose oder wise gefolgt von der zweistelligen Jahreszahl (z. B. sose22).", + "semester": ("Das Semester, für welches diese Lerngruppe erstellt werden soll." + "sose oder wise gefolgt von der zweistelligen Jahreszahl (z. B. sose22)."), "status": "Gibt an ob die Lerngruppe für weitere Lernwillige geöffnet ist (open) oder nicht (closed).", "@usermention": "Der so erwähnte Benutzer wird als Besitzer für die Lerngruppe gesetzt." }, mod=True ) - @commands.command(name="add-group") + @cmd_lg.command(name="add") @commands.check(utils.is_mod) async def cmd_add_group(self, ctx, arg_course, arg_name, arg_semester, arg_open, arg_owner: disnake.Member): is_open = self.arg_open_to_bool(arg_open) channel_config = {"owner_id": arg_owner.id, "course": arg_course, "name": arg_name, "semester": arg_semester, - "is_open": is_open} + "is_open": is_open, "is_listed": False} if not await self.is_channel_config_valid(ctx, channel_config, ctx.command.name): return self.groups["requested"][str(ctx.message.id)] = channel_config - self.save_groups() + await self.save_groups() await self.add_requested_group_channel(ctx.message, direct=True) @help( + command_group="lg", category="learninggroups", - syntax="!request-group <coursenumber> <name> <semester> <status>", + syntax="!lg request <coursenumber> <name> <semester> <status>", brief="Stellt eine Anfrage für einen neuen Lerngruppen-Kanal.", - example="!request-group 1142 mathegenies sose22 closed", + example="!lg request 1142 mathegenies sose22 closed", description=("Moderatorinnen können diese Anfrage bestätigen, dann wird die Gruppe eingerichtet. " "Der Besitzer der Gruppe ist der Benutzer der die Anfrage eingestellt hat."), parameters={ "coursenumber": "Nummer des Kurses, wie von der FernUni angegeben (ohne führende Nullen z. B. 1142).", "name": "Ein frei wählbarer Text ohne Leerzeichen.", - "semester": "Das Semester, für welches diese Lerngruppe erstellt werden soll. sose oder wise gefolgt von der zweistelligen Jahrenszahl (z. B. sose22).", + "semester": "Das Semester, für welches diese Lerngruppe erstellt werden soll. sose oder wise gefolgt " + "von der zweistelligen Jahreszahl (z. B. sose22).", "status": "Gibt an ob die Lerngruppe für weitere Lernwillige geöffnet ist (open) oder nicht (closed)." } ) - @commands.command(name="request-group") + @cmd_lg.command(name="request", aliases=["r", "req"]) async def cmd_request_group(self, ctx, arg_course, arg_name, arg_semester, arg_open): is_open = self.arg_open_to_bool(arg_open) + arg_name = re.sub( + r"[^A-Za-zäöüß0-9-]", + "", + arg_name.lower().replace(" ", "-") + ) + arg_semester = arg_semester.lower() + if len(arg_semester) == 8: + arg_semester = f"{arg_semester[0:4]}{arg_semester[-2:]}" channel_config = {"owner_id": ctx.author.id, "course": arg_course, "name": arg_name, "semester": arg_semester, - "is_open": is_open} + "is_open": is_open, "is_listed": False} if not await self.is_channel_config_valid(ctx, channel_config, ctx.command.name): return + channel = await self.bot.fetch_channel(int(self.channel_request)) channel_name = self.full_channel_name(channel_config) - embed = disnake.Embed(title="Lerngruppenanfrage!", - description=f"<@!{ctx.author.id}> möchte gerne die Lerngruppe **#{channel_name}** eröffnen", - color=19607) - - channel_request = await self.bot.fetch_channel(int(self.channel_request)) - message = await channel_request.send(embed=embed) - await message.add_reaction("ðŸ‘") - await message.add_reaction("🗑ï¸") + message = await utils.confirm( + channel=channel, + title="Lerngruppenanfrage", + description=f"<@!{ctx.author.id}> möchte gerne die Lerngruppe **#{channel_name}** eröffnen.", + callback=self.on_group_request + ) self.groups["requested"][str(message.id)] = channel_config - self.save_groups() + await self.save_groups() + + @help( + command_group="lg", + category="learninggroups", + syntax="!lg show", + brief="Zeigt einen privaten Lerngruppenkanal trotzdem in der Liste an.", + description=("Muss im betreffenden Lerngruppen-Kanal ausgeführt werden. " + "Die Lerngruppe wird in der Ãœbersicht der Lerngruppen gelistet, so können Kommilitoninnen noch " + "Anfragen stellen, um in die Lerngruppe aufgenommen zu werden." + "Diese Aktion kann nur von der Besitzerin der Lerngruppe ausgeführt werden. ") + ) + @cmd_lg.command(name="show") + async def cmd_show(self, ctx): + if self.is_group_owner(ctx.channel, ctx.author) or utils.is_mod(ctx): + await self.set_channel_state(ctx.channel, is_listed=True) @help( + command_group="lg", category="learninggroups", + syntax="!lg hide", + brief="Versteckt einen privaten Lerngruppenkanal. ", + description=("Muss im betreffenden Lerngruppen-Kanal ausgeführt werden. " + "Die Lerngruppe wird nicht mehr in der Liste der Lerngruppen aufgeführt." + "Diese Aktion kann nur von der Besitzerin der Lerngruppe ausgeführt werden. ") + ) + @cmd_lg.command(name="hide") + async def cmd_hide(self, ctx): + if self.is_group_owner(ctx.channel, ctx.author) or utils.is_mod(ctx): + await self.set_channel_state(ctx.channel, is_listed=False) + + @help( + command_group="lg", + category="learninggroups", + syntax="!lg open", brief="Öffnet den Lerngruppen-Kanal wenn du die Besitzerin bist. ", description=("Muss im betreffenden Lerngruppen-Kanal ausgeführt werden. " "Verschiebt den Lerngruppen-Kanal in die Kategorie für offene Kanäle und ändert das Icon. " - "Diese Aktion kann nur vom Besitzer der Lerngruppe ausgeführt werden. ") + "Diese Aktion kann nur von der Besitzerin der Lerngruppe ausgeführt werden. ") ) - @commands.command(name="open") + @cmd_lg.command(name="open") async def cmd_open(self, ctx): if self.is_group_owner(ctx.channel, ctx.author) or utils.is_mod(ctx): await self.set_channel_state(ctx.channel, is_open=True) @help( + command_group="lg", category="learninggroups", + syntax="!lg close", brief="Schließt den Lerngruppen-Kanal wenn du die Besitzerin bist. ", description=("Muss im betreffenden Lerngruppen-Kanal ausgeführt werden. " - "Verschiebt den Lerngruppen-Kanal in die Kategorie für geschlossene Kanäle und ändert das Icon. " - "Diese Aktion kann nur vom Besitzer der Lerngruppe ausgeführt werden. ") + "Stellt die Lerngruppe auf privat. Es haben nur noch Mitglieder " + "der Lerngruppe zugriff auf den Kanal. (siehe `!lg members`)" + "Diese Aktion kann nur von der Besitzerin der Lerngruppe ausgeführt werden. ") ) - @commands.command(name="close") + @cmd_lg.command(name="close", aliases=["privat", "private"]) async def cmd_close(self, ctx): if self.is_group_owner(ctx.channel, ctx.author) or utils.is_mod(ctx): - await self.set_channel_state(ctx.channel, is_open=False) + if await self.set_channel_state(ctx.channel, is_open=False): + await self.update_permissions(ctx.channel) @help( + command_group="lg", category="learninggroups", - syntax="!rename <name>", + syntax="!lg rename <name>", brief="Ändert den Namen des Lerngruppen-Kanals, in dem das Komando ausgeführt wird.", - example="!rename matheluschen", + example="!lg rename matheluschen", description="Aus #1142-matheprofis-sose22 wird nach dem Aufruf des Beispiels #1142-matheluschen-sose22.", parameters={ "name": "Der neue Name der Lerngruppe ohne Leerzeichen." }, mod=True ) - @commands.command(name="rename") + @cmd_lg.command(name="rename") @commands.check(utils.is_mod) async def cmd_rename(self, ctx, arg_name): await self.set_channel_name(ctx.channel, arg_name) @help( + command_group="lg", + syntax="!lg archive", category="learninggroups", brief="Archiviert den Lerngruppen-Kanal", description="Verschiebt den Lerngruppen-Kanal, in welchem dieses Kommando ausgeführt wird, ins Archiv.", mod=True ) - @commands.command(name="archive") + @cmd_lg.command(name="archive", aliases=["archiv"]) @commands.check(utils.is_mod) async def cmd_archive(self, ctx): await self.archive(ctx.channel) @help( + command_group="lg", category="learninggroups", - syntax="!owner <@usermention>", + syntax="!lg owner <@usermention>", example="!owner @someuser", brief="Setzt die Besitzerin eines Lerngruppen-Kanals", description="Muss im betreffenden Lerngruppen-Kanal ausgeführt werden. ", parameters={ "@usermention": "Der neue Besitzer der Lerngruppe." + } + ) + @cmd_lg.command(name="owner") + async def cmd_owner(self, ctx, arg_owner: disnake.Member = None): + group_config = self.groups["groups"].get(str(ctx.channel.id)) + + if not group_config: + self.groups["groups"][str(ctx.channel.id)] = {} + group_config = self.groups["groups"][str(ctx.channel.id)] + + if not arg_owner: + owner_id = group_config.get("owner_id") + if owner_id: + user = await self.bot.fetch_user(owner_id) + await ctx.channel.send(f"Besitzer: @{user.name}") + + elif isinstance(group_config, dict): + if self.is_group_owner(ctx.channel, ctx.author) or utils.is_mod(ctx): + group_config["owner_id"] = arg_owner.id + await self.remove_member_from_group(ctx.channel, arg_owner) + await self.add_member_to_group(ctx.channel, ctx.author) + await self.save_groups() + await self.update_permissions(ctx.channel) + await ctx.channel.send( + f"Glückwunsch {arg_owner.mention}! Du bist jetzt die Besitzerin dieser Lerngruppe.") + + @help( + command_group="lg", + category="learninggroups", + syntax="!lg addmember <@usermention> <#channel>", + example="!lg addmember @someuser #1141-mathegl-lerngruppe-sose21", + brief="Fügt einen Benutzer zu einer Lerngruppe hinzu.", + parameters={ + "@usermention": "Der so erwähnte Benutzer wird zur Lerngruppe hinzugefügt.", + "#channel": "Der Kanal dem der Benutzer hinzugefügt werden soll." + } + ) + @cmd_lg.command(name="addmember", aliases=["addm", "am"]) + async def cmd_add_member(self, ctx, arg_member: disnake.Member, arg_channel: disnake.TextChannel): + if self.is_group_owner(arg_channel, ctx.author) or utils.is_mod(ctx): + await self.add_member_to_group(arg_channel, arg_member) + await self.update_permissions(arg_channel) + + @help( + command_group="lg", + category="learninggroups", + syntax="!lg removemember <@usermention> <#channel>", + example="!lg removemember @someuser #1141-mathegl-lerngruppe-sose21", + brief="Entfernt einen Benutzer aus einer Lerngruppe.", + parameters={ + "#channel": "Der Kanal aus dem der Benutzer gelöscht werden soll.", + "@usermention": "Der so erwähnte Benutzer wird aus der Lerngruppe entfernt." }, mod=True ) - @commands.command(name="owner") + @cmd_lg.command(name="removemember", aliases=["remm", "rm"]) @commands.check(utils.is_mod) - async def cmd_owner(self, ctx, arg_owner: disnake.Member): - channel_config = self.groups["groups"].get(str(ctx.channel.id)) - if channel_config: - channel_config["owner_id"] = arg_owner.id - self.save_groups() - await ctx.channel.send(f"Glückwunsch {arg_owner.mention}! Du bist jetzt die Besitzerin dieser Lerngruppe.") + async def cmd_remove_member(self, ctx, arg_member: disnake.Member, arg_channel: disnake.TextChannel): + await self.remove_member_from_group(arg_channel, arg_member) + await self.update_permissions(arg_channel) @help( + command_group="lg", category="learninggroups", - brief="Zeigt die Besitzerin eines Lerngruppen-Kanals an.", - description="Muss im betreffenden Lerngruppen-Kanal ausgeführt werden.", - mod=True + syntax="!lg members", + brief="Listet die Mitglieder der Lerngruppe auf.", ) - @commands.command(name="show-owner") - @commands.check(utils.is_mod) - async def cmd_show_owner(self, ctx): - channel_config = self.groups["groups"].get(str(ctx.channel.id)) - owner_id = channel_config.get("owner_id") - if owner_id: - user = await self.bot.fetch_user(owner_id) - await ctx.channel.send(f"Besitzer: @{user.name}") - - @commands.Cog.listener() - async def on_raw_reaction_add(self, payload): - if payload.user_id == self.bot.user.id: + @cmd_lg.command(name="members") + async def cmd_members(self, ctx): + group_config = self.groups["groups"].get(str(ctx.channel.id)) + if not group_config: + await ctx.channel.send("Das ist kein Lerngruppenkanal.") + return + + users = group_config.get("users") + if not users: + await ctx.channel.send("Keine Benutzer vorhanden.") + return + + names = [] + for user_id in users: + user = await self.bot.fetch_user(user_id) + names.append("@" + user.name) + await ctx.channel.send(f"Members: {', '.join(names)}") + + @help( + command_group="lg", + category="learninggroups", + syntax="!lg id", + brief="Zeigt die ID für deine Lerngruppe an.", + ) + @cmd_lg.command(name="id") + async def cmd_id(self, ctx): + if self.is_group_owner(ctx.channel, ctx.author) or utils.is_mod(ctx): + group_config = self.groups["groups"].get(str(ctx.channel.id)) + if not group_config: + await ctx.channel.send("Das ist kein Lerngruppenkanal.") + return + await ctx.channel.send(f"Die ID dieser Lerngruppe lautet: `{str(ctx.channel.id)}`.\n" + f"Beitrittsanfrage mit: `!lg join {str(ctx.channel.id)}`") + + @help( + command_group="lg", + category="learninggroups", + syntax="!lg join <lg-id>", + brief="Fragt bei der Besitzerin einer Lerngruppe um Aufnahme.", + parameters={ + "id": "Die ID zur Lerngruppe." + } + ) + @cmd_lg.command(name="join") + async def cmd_join(self, ctx, arg_id_or_channel: Union[int, disnake.TextChannel] = None): + + if arg_id_or_channel is None: + arg_id_or_channel = ctx.channel + + cid = arg_id_or_channel.id if type(arg_id_or_channel) is disnake.TextChannel else arg_id_or_channel + + group_config = self.groups["groups"].get(str(cid)) + if not group_config: + await ctx.channel.send("Das ist keine gültiger Lerngruppenkanal.") return - channel = await self.bot.fetch_channel(payload.channel_id) - message = await channel.fetch_message(payload.message_id) - request = self.groups["requested"].get(str(message.id)) + channel = await self.bot.fetch_channel(int(cid)) + + await utils.confirm( + channel=channel, + title="Jemand möchte deiner Lerngruppe beitreten!", + description=f"<@!{ctx.author.id}> möchte gerne der Lerngruppe **#{channel.name}** beitreten.", + message=f"Anfrage von <@!{ctx.author.id}>", + callback=self.on_join_request + ) - if payload.emoji.name in ["ðŸ‘"] and self.is_group_request_message(message) and self.is_mod(payload.member): - await self.add_requested_group_channel(message, direct=False) + @help( + command_group="lg", + category="learninggroups", + syntax="!lg kick <@usermention>", + brief="Wirft @usermention aus der Gruppe." + ) + @cmd_lg.command(name="kick") + async def cmd_kick(self, ctx, arg_member: disnake.Member): + if self.is_group_owner(ctx.channel, ctx.author) or utils.is_mod(ctx): + group_config = self.groups["groups"].get(str(ctx.channel.id)) + if not group_config: + await ctx.channel.send("Das ist keine gültiger Lerngruppenkanal.") + return + + await self.remove_member_from_group(ctx.channel, arg_member) + await self.update_permissions(ctx.channel) + + @help( + command_group="lg", + category="learninggroups", + syntax="!lg leave", + brief="Du verlässt die Lerngruppe." + ) + @cmd_lg.command(name="leave") + async def cmd_leave(self, ctx): + group_config = self.groups["groups"].get(str(ctx.channel.id)) + if not group_config: + await ctx.channel.send("Das ist keine gültiger Lerngruppenkanal.") + return + + if group_config["owner_id"] == ctx.author.id: + await ctx.channel.send("Du kannst nicht aus deiner eigenen Lerngruppe flüchten. Ãœbertrage erst den Besitz.") + return + + await self.remove_member_from_group(ctx.channel, ctx.author) + await self.update_permissions(ctx.channel) + + async def on_group_request(self, confirmed, button, interaction: disnake.InteractionMessage): + channel = interaction.channel + member = interaction.author + message = interaction.message + + if str(channel.id) == str(self.channel_request): + request = self.groups["requested"].get(str(message.id)) + if confirmed and self.is_mod(member): + await self.add_requested_group_channel(message, direct=False) + + elif not confirmed and (self.is_request_owner(request, member) or self.is_mod(member)): + await self.remove_group_request(message) + await message.delete() + + async def on_join_request(self, confirmed, button, interaction: disnake.InteractionMessage): + channel = interaction.channel + member = interaction.author + message = interaction.message + group_config = self.groups["groups"].get(str(channel.id)) + + if not group_config: + return + + if self.is_group_owner(channel, member) or self.is_mod(member): + if confirmed: + if message.mentions and len(message.mentions) == 1: + await self.add_member_to_group(channel, message.mentions[0]) + await self.update_permissions(channel) + + else: + await channel.send(f"Leider ist ein Fehler aufgetreten.") - if payload.emoji.name in ["🗑ï¸"] and self.is_group_request_message(message) and ( - self.is_request_owner(request, payload.member) or self.is_mod(payload.member)): - self.remove_group_request(message) await message.delete() async def cog_command_error(self, ctx, error): diff --git a/utils.py b/utils.py index b5b8e57e70ef7b03e0be6a746a82c2788a6e38d2..b41e8bbc9cebb1465421c5b5804a9036a2e7db95 100644 --- a/utils.py +++ b/utils.py @@ -2,6 +2,8 @@ import os import disnake import re +from views.confirm_view import ConfirmView + async def send_dm(user, message, embed=None): """ Send DM to a user/member """ @@ -10,7 +12,7 @@ async def send_dm(user, message, embed=None): if user.dm_channel is None: await user.create_dm() - await user.dm_channel.send(message, embed=embed) + return await user.dm_channel.send(message, embed=embed) def is_mod(ctx): @@ -41,3 +43,9 @@ def to_minutes(time): return int(time) + +async def confirm(channel, title, description, message="", callback=None): + embed = disnake.Embed(title=title, + description=description, + color=19607) + return await channel.send(message, embed=embed, view=ConfirmView(callback)) diff --git a/views/confirm_view.py b/views/confirm_view.py new file mode 100644 index 0000000000000000000000000000000000000000..0f2517cf3271cde80bdc19cfc2b6fe346ae4ae72 --- /dev/null +++ b/views/confirm_view.py @@ -0,0 +1,19 @@ +import disnake +from disnake import MessageInteraction, ButtonStyle +from disnake.ui import Button + + +class ConfirmView(disnake.ui.View): + def __init__(self, callback=None): + super().__init__(timeout=None) + self.callback = callback + + @disnake.ui.button(emoji="ðŸ‘", style=ButtonStyle.grey) + async def btn_subscribe(self, button: Button, interaction: MessageInteraction): + if self.callback: + await self.callback(True, button, interaction) + + @disnake.ui.button(emoji="👎", style=ButtonStyle.grey) + async def btn_unsubscribe(self, button: Button, interaction: MessageInteraction): + if self.callback: + await self.callback(False, button, interaction) \ No newline at end of file