From 707a55f3c74bce7ccdfc0d3a97506b04c7d31ba7 Mon Sep 17 00:00:00 2001 From: dnns01 <github@dnns01.de> Date: Fri, 9 Sep 2022 11:46:49 +0200 Subject: [PATCH] Release v1.1 (#175) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Stellenangebote der Uni posten (#148) * job-offer feature hinzugefügt * updated disnake, switched joboffers to slashcommand * move JOBS_URL and STD:FAK to .env * cahnge all requested changes * add 'all' as STD_FAK possibility * update .env.template and info * fix problem with `all` when STD_FAK!=all * fix missing Sonderzeichen * create turnable pages if more than 5 offers * fix Beständigkeit of outdated joboffers * create new embed for every five new joboffers Co-authored-by: dnns01 <git@dnns01.de> * use next "li" instead of "div" to include more support information for some modules (#166) * Fix für Probleme im Lerngruppenmanagement (#162) * fix for #130 and #155 * fix for #153 * add filter to learngroup request to reduce support time and fix for #159 * add detection of semester and status in learngroup requests to switch if arguments if needed, should fix # 63 * add ping for lg-owner on join request. fix #145 * Rebuild role assignment (#182) * Rebuild role assignment to work with slash commands and buttons. Also roles assignment is easier to configure without further code changes * Changed from DM confirmation to ephemeral message in channel as interaction response Co-authored-by: dnns01 <git@dnns01.de> * Cleanup .env.template (#185) * Rename Owner in Lg-module #178 (#181) * Renamed 'Owner' in Lg-module, resolved typos * Renamed 'Owner' in Lg-module, resolved typos * Anglizismen entfernt * Ein Tippfehler noch korrigiert * All instances of "owner" changed to "organizer". * Änderungen von Lou-M's Review übernommen * Beschreibungen der Umgebungsvariablen von LEARNINGGROUPS modifiziert bzw. hinzugefügt, kleinere Typos Co-authored-by: dnns01 <github@dnns01.de> * Added confirmation, when roles are updated (#188) Co-authored-by: dnns01 <git@dnns01.de> Co-authored-by: samari-k <74598487+samari-k@users.noreply.github.com> Co-authored-by: dnns01 <git@dnns01.de> Co-authored-by: Fabian <75286597+ffaadd@users.noreply.github.com> Co-authored-by: J.M. Baldwin <oddbook@posteo.net> --- .env.template | 28 +- .idea/fernuni-bot.iml | 2 +- .idea/misc.xml | 2 +- README.md | 2 +- cogs/components/module_information/scraper.py | 2 +- cogs/job_offers.py | 196 ++++++++++++ cogs/learninggroups.py | 276 +++++++++-------- cogs/roles.py | 284 ++++++------------ fernuni_bot.py | 1 + requirements.txt | 42 +-- utils.py | 13 +- views/joboffers_view.py | 35 +++ 12 files changed, 518 insertions(+), 365 deletions(-) create mode 100644 cogs/job_offers.py create mode 100644 views/joboffers_view.py diff --git a/.env.template b/.env.template index 3ecd2f4..549da08 100644 --- a/.env.template +++ b/.env.template @@ -10,28 +10,23 @@ DISCORD_PROD=<True, if running in an productive environment, otherwise False> # IDs DISCORD_OWNER=<ID of Server owner> DISCORD_MOD_ROLE=<ID of Mod Role> -DISCORD_MOD_CHANNEL=<ID of Mod only channel> DISCORD_CATEGORY_LERNGRUPPEN=<Channel category for learning group voice channels> DISCORD_WELCOME_CHANNEL=<ID of Welcome channel, where Welcome Message should be posted> DISCORD_WELCOME_MSG=<ID of Welcome message> DISCORD_VORSTELLUNGSCHANNEL=<ID of Channel used for introductions of new users> DISCORD_ROLLEN_CHANNEL=<ID of role assignment channel> -DISCORD_DEGREE_PROGRAM_MSG=<ID of Message that can be used for assignement of degree program roles> -DISCORD_COLOR_MSG=<ID of Message that can be used for assignment of color roles> -DISCORD_SPECIAL_MSG=<ID of Message that can be used for assignment of special roles> -DISCORD_STUDENTIN_ROLE=<ID of "Studentin" role> -DISCORD_ADVENT_CALENDAR_CHANNEL=<ID of Advent calendar channel> DISCORD_SUPPORT_CHANNEL=<ID of support channel, where the Bot posts all DMs sent by users> DISCORD_GREETING_CHANNEL=<ID of greeting channel, where new users are welcomed> -DISCORD_ADVENT_CALENDAR_MESSAGE=<ID of Advent calendar message> DISCORD_NEWS_CHANNEL=<ID of News Channel, where news from faculty are posted> DISCORD_POLL_SUGG_CHANNEL=<ID of Channel, where poll suggestions are posted> DISCORD_NEWS_ROLE=<ID of news role> DISCORD_CHANGE_LOG_CHANNEL=<ID of channel used by change log feature> -DISCORD_LEARNINGGROUPS_OPEN=<ID of Channel category for open learning groups> -DISCORD_LEARNINGGROUPS_CLOSE=<ID of Channel category for closed learning groups> -DISCORD_LEARNINGGROUPS_REQUEST=<ID of Channel category for learning group requests> -DISCORD_LEARNINGGROUPS_INFO=<ID of Channel category for open learning groups> +DISCORD_LEARNINGGROUPS_OPEN=<ID of the channel category for open learning groups> +DISCORD_LEARNINGGROUPS_CLOSE=<ID of the channel category for closed learning groups> +DISCORD_LEARNINGGROUPS_PRIVATE=<ID of the channel category for private learning groups> +DISCORD_LEARNINGGROUPS_ARCHIVE=<ID of the channel category for archived learning groups> +DISCORD_LEARNINGGROUPS_REQUEST=<ID of the channel category for learning group requests made via the bot> +DISCORD_LEARNINGGROUPS_INFO=<ID of the channel category for posting or updating information about the learning group> DISCORD_IDEE_CHANNEL=<ID of Channel, where bot ideas can be submitted> DISCORD_IDEE_EMOJI=<ID of Idee Emoji, used for reactions> DISCORD_CALMDOWN_ROLE=<ID of "Calmdown" role> @@ -40,22 +35,25 @@ DISCORD_ELM_STREET_CHANNEL=<ID of elm street channel> DISCORD_HALLOWEEN_CATEGORY=<ID of Halloween category> DISCORD_SEASONAL_EVENTS_CATEGORY=<ID of Seasonal Events Category> DISCORD_ADVENT_CALENDAR_CHANNEL_2021=<ID of advent calendar chanel for 2021> +DISCORD_JOBOFFERS_CHANNEL=<ID of "Stellenangebote" Channel> # JSON Files DISCORD_ROLES_FILE=<File name for roles JSON file> DISCORD_HELP_FILE=<File name for help JSON file> -DISCORD_TOPS_FILE=<File name for TOPS JSON file> DISCORD_APPOINTMENTS_FILE=<File name for appointments JSON file> DISCORD_TEXT_COMMANDS_FILE=<File name for text commands JSON file> -DISCORD_LEARNINGGROUPS_FILE=<File name for learning groups JSON file> -DISCORD_LEARNINGGROUPS_COURSE_FILE=<File name for leaarning groups courses JSON file> +DISCORD_LEARNINGGROUPS_FILE=<Name of the JSON file for administering learning groups> +DISCORD_LEARNINGGROUPS_COURSE_FILE=<Name of the JSON file that contains the course names used in the learning group information> DISCORD_CALMDOWN_FILE=<File name for calmdowns JSON file> DISCORD_MODULE_COURSE_FILE=<File name for module course JSON file> DISCORD_MODULE_DATA_FILE=<File name for module data JSON file> DISCORD_TIMER_FILE=<File name for running timers JSON file> DISCORD_ADVENT_CALENDAR_FILE=<File name for advent calendar JSON file> +DISCORD_JOBOFFERS_FILE=<File name for job offers JSON file> # Misc DISCORD_DATE_TIME_FORMAT=<Date and time format used for commands like %d.%m.%Y %H:%M> -DISCORD_IDEE_REACT_QTY=<Amount of reactions to a submitted idea, neccessary to create a github issue (amount is including botys own reaction)> +DISCORD_IDEE_REACT_QTY=<Amount of reactions to a submitted idea, neccessary to create a github issue (amount is including Boty's own reaction)> DISCORD_ADVENT_CALENDAR_START=<Start date and time for advent calendar. Something like "01.12.2021 00:00"> +DISCORD_JOBOFFERS_URL=<url from which job offers are fetched, atm "https://www.fernuni-hagen.de/uniintern/arbeitsthemen/karriere/stellen/include/hk.shtml"> +DISCORD_JOBOFFERS_STD_FAK=<faculty for which job offers should be posted, one of [mi|rewi|wiwi|ksw|psy|other|all]> diff --git a/.idea/fernuni-bot.iml b/.idea/fernuni-bot.iml index 7ad0d73..6b3214e 100644 --- a/.idea/fernuni-bot.iml +++ b/.idea/fernuni-bot.iml @@ -4,7 +4,7 @@ <content url="file://$MODULE_DIR$"> <excludeFolder url="file://$MODULE_DIR$/venv" /> </content> - <orderEntry type="jdk" jdkName="Python 3.9 (fernuni-bot)" jdkType="Python SDK" /> + <orderEntry type="jdk" jdkName="Python 3.10 (fernuni-bot)" jdkType="Python SDK" /> <orderEntry type="sourceFolder" forTests="false" /> </component> </module> \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index 266752f..0fb7607 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -3,5 +3,5 @@ <component name="JavaScriptSettings"> <option name="languageLevel" value="ES6" /> </component> - <component name="ProjectRootManager" version="2" project-jdk-name="Python 3.9 (fernuni-bot)" project-jdk-type="Python SDK" /> + <component name="ProjectRootManager" version="2" project-jdk-name="Python 3.10 (fernuni-bot)" project-jdk-type="Python SDK" /> </project> \ No newline at end of file diff --git a/README.md b/README.md index 14e9a9b..05babe4 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ Zu dem Zeitpunkt kann Boty: * [Lerngruppenverwaltung](https://github.com/FU-Hagen-Discord/fernuni-bot/tree/master/cogs/learninggroups.py) * Anfragen/ Erstellen/ Umbenennen/ Löschen - * Ownership -> Toggle: 🛑vollzählig/ 🌲offen für neue Mitglieder + * Organisatorenrolle -> Lerngruppenstatus als öffentlich sichtbar und 🌲offen für neue Mitglieder oder 🛑vollzählig, oder als unsichtbare 🚪private Gruppe festlegen. * [Kalenderfunktion](https://github.com/FU-Hagen-Discord/fernuni-bot/tree/master/cogs/appointments.py) diff --git a/cogs/components/module_information/scraper.py b/cogs/components/module_information/scraper.py index 2a795a2..0665e53 100644 --- a/cogs/components/module_information/scraper.py +++ b/cogs/components/module_information/scraper.py @@ -124,7 +124,7 @@ class Scraper: def parse_support(self, soup): try: support_source = soup.find('h2', text=re.compile( - r'Mentorielle Betreuung an den Campusstandorten')).findNext('div').findAll('li') + r'Mentorielle Betreuung an den Campusstandorten')).findNext('ul').findAll('li') except: return None diff --git a/cogs/job_offers.py b/cogs/job_offers.py new file mode 100644 index 0000000..d477aed --- /dev/null +++ b/cogs/job_offers.py @@ -0,0 +1,196 @@ +import json +import os +from copy import deepcopy + +import aiohttp +from bs4 import BeautifulSoup +import disnake +from disnake import ApplicationCommandInteraction, MessageInteraction +from disnake.ext import commands, tasks +from disnake.ui import View, Button + +from cogs.help import help +from views import joboffers_view + +""" + Environment Variablen: + DISCORD_JOBOFFERS_FILE - json file mit allen aktuellen + DISCORD_JOBOFFERS_CHANNEL - Channel-ID für Stellenangebote + DISCORD_JOBOFFERS_URL - URL von der die Stellenangebote geholt werde + DISCORD_JOBOFFERS_STD_FAK - Fakultät deren Stellenangebote standardmäßig gepostet werden + + Struktur der json: + {fak:{id:{title:..., info:..., link:..., deadline:...}} + mit fak = [mi|rewi|wiwi|ksw|psy|other] +""" + +JOBS_URL = os.getenv("DISCORD_JOBOFFERS_URL") +STD_FAK = os.getenv("DISCORD_JOBOFFERS_STD_FAK") + +class Joboffers(commands.Cog): + def __init__(self, bot): + self.bot = bot + self.joboffers = {} + self.joboffers_channel_id = int(os.getenv("DISCORD_JOBOFFERS_CHANNEL")) + self.joboffers_file = os.getenv("DISCORD_JOBOFFERS_FILE") + self.load_joboffers() + self.update_loop.start() + + @tasks.loop(hours=24) + async def update_loop(self): + await self.fetch_joboffers() + + @update_loop.before_loop + async def before_update_loop(self): + await self.bot.wait_until_ready() + + def save_joboffers(self): + with open(self.joboffers_file, mode='w') as joboffers_file: + json.dump(self.joboffers, joboffers_file) + + def load_joboffers(self): + try: + with open(self.joboffers_file, mode='r') as joboffers_file: + self.joboffers = json.load(joboffers_file) + except FileNotFoundError: + self.joboffers = {} + + @help( + syntax="/jobs <fak?>", + parameters={ + "fak": "Fakultät für die die studentische Hilfskraft Jobs ausgegeben werden sollen " + "(mi, rewi, wiwi, ksw, psy)" + }, + brief="Ruft Jobangebote für Studiernde der Fernuni Hagen auf." + ) + @commands.slash_command(name="jobs", aliases=["offers","stellen","joboffers"], + description="Liste Jobangebote der Uni auf") + async def cmd_jobs(self, interaction: ApplicationCommandInteraction, + chosen_faculty: str = commands.Param(default=STD_FAK, + name='faculty', + choices=['mi','rewi','wiwi','ksw','psy','other','all'])): + await self.fetch_joboffers() + + fak_text = "aller Fakultäten" if chosen_faculty == 'all' else f"der Fakultät {chosen_faculty}" + description = f"Ich habe folgende Stellenangebote {fak_text} gefunden:" + + pages = [] + page = [] + for fak, fak_offers in self.joboffers.items(): + if chosen_faculty != 'all' and fak != chosen_faculty: + continue + + for offer_id, offer_data in fak_offers.items(): + descr = f"{offer_data['info']}\nDeadline: {offer_data['deadline']}\n{offer_data['link']}" + field = {'name': offer_data['title'], 'value': descr, 'inline': False} + if len(page) < 5: + page.append(field) + else: + pages.append(deepcopy(page)) + page = [] + if len(page) > 0: + pages.append(deepcopy(page)) + + page_nr = 1 + embed = self.get_embed(description, pages[page_nr-1], page_nr, len(pages)) + view = joboffers_view.JobOffersView(self.on_page_skip, pages, page_nr, description) + + await interaction.response.send_message(embed=embed, view=view, ephemeral=True) + + def get_embed(self, description, page_content, page_nr, all_pages_nr): + embed = disnake.Embed(title="Stellenangebote der Uni", + description=f"Ich habe folgende Stellenangebote {description} gefunden:") + for field in page_content: + embed.add_field(**field) + embed.set_footer(text=f"Seite {page_nr}/{all_pages_nr}") + return embed + + async def on_page_skip(self, button: Button, interaction: MessageInteraction, pages, page_nr, embed_description): + if button.custom_id == "jobs:next": + page_nr += 1 + if button.custom_id == "jobs:prev": + page_nr -= 1 + embed = self.get_embed(embed_description, pages[page_nr-1], page_nr, len(pages)) + view = joboffers_view.JobOffersView(self.on_page_skip, pages, page_nr, embed_description) + await interaction.response.edit_message(embed=embed, view=view) + + async def post_new_jobs(self, jobs): + fak_text = "aller Fakultäten" if STD_FAK == 'all' else f"der Fakultät {STD_FAK}" + joboffers_channel = await self.bot.fetch_channel(self.joboffers_channel_id) + + embed = disnake.Embed(title="Neue Stellenangebote der Uni", + description=f"Ich habe folgende neue Stellenangebote {fak_text} gefunden:") + i = 0 + for job in jobs: + i += 1 + descr = f"{job['info']}\nDeadline: {job['deadline']}\n{job['link']}" + embed.add_field(name=job['title'], value=descr, inline=False) + if i % 5 == 0: + await joboffers_channel.send(embed=embed) + embed = disnake.Embed(title="Neue Stellenangebote der Uni ... Fortsetzung") + if i % 5 != 0: + await joboffers_channel.send(embed=embed) + + async def fetch_joboffers(self): + sess = aiohttp.ClientSession() + req = await sess.get(JOBS_URL) + text = await req.read() + await sess.close() + + soup = BeautifulSoup(text, "html.parser") + list = soup.findAll("li") + + # alte Liste sichern zum Abgleich + old_joboffers = deepcopy(self.joboffers) + # Liste leeren um outdated joboffers auszusortieren + self.joboffers = {} + + for job in list: + detail_string = job.text.strip() + if "Studentische Hilfskraft" in detail_string: + id = detail_string[detail_string.index('(')+12:detail_string.index(')')] + title = detail_string[:detail_string.index(')')+1] + info = detail_string[detail_string.index(')')+1:detail_string.index('[')] + deadline = detail_string[detail_string.index('[')+1:detail_string.index(']')] + link = job.find('a')['href'] + + # Sonderzeichen aufräumen + to_replace = ["ä", "ü", "²", "„", "“"] + replace_with = ["ä", "ü", "²", "\"", "\""] + for i in range(len(to_replace)): + info = info.replace(to_replace[i], replace_with[i]) + + faks = ["other", "wiwi", "mi", "ksw", "psy", "rewi"] + + fak_id = int(id[0]) # Kennziffer 1=wiwi, 2=mi, 3=ksw, 4=psy, 5=rewi, alle anderen=other + if fak_id in range(1,6): + fak = faks[fak_id] + else: + fak = faks[0] + + if not self.joboffers.get(fak): + self.joboffers[fak] = {} + self.joboffers[fak][id] = {'title': title, 'info': info, 'deadline': deadline, 'link': link} + self.save_joboffers() + await self.check_for_new_jobs(old_joboffers) + + async def check_for_new_jobs(self, old_joboffers): + new_jobs = [] + + for fak, fak_offers in self.joboffers.items(): + if STD_FAK != 'all' and fak != STD_FAK: + continue + + if fak_old := old_joboffers.get(fak): + for offer_id, offer_data in fak_offers.items(): + if old_offer := fak_old.get(offer_id): + if offer_data != old_offer: + new_jobs.append(offer_data) + else: + new_jobs.append(offer_data) + else: + for offer_id, offer_data in self.joboffers.get(fak).items(): + new_jobs.append(offer_data) + + if new_jobs: + await self.post_new_jobs(new_jobs) diff --git a/cogs/learninggroups.py b/cogs/learninggroups.py index 675db08..e28e386 100644 --- a/cogs/learninggroups.py +++ b/cogs/learninggroups.py @@ -15,15 +15,16 @@ import utils 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 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 + Umgebungsvariablen: + DISCORD_LEARNINGGROUPS_OPEN - Kategorie-ID der offenen Lerngruppen + DISCORD_LEARNINGGROUPS_CLOSE - Kategorie-ID der geschlossenen Lerngruppen + DISCORD_LEARNINGGROUPS_PRIVATE - Kategorie-ID der privaten Lerngruppen + DISCORD_LEARNINGGROUPS_ARCHIVE - Kategorie-ID der archivierten Lerngruppen + DISCORD_LEARNINGGROUPS_REQUEST - ID des Kanals, in dem Anfragen, die über den Bot gestellt wurden, eingetragen werden + DISCORD_LEARNINGGROUPS_INFO - ID des Kanals, in dem die Lerngruppen-Informationen gepostet/aktualisert werden DISCORD_LEARNINGGROUPS_FILE - Name der Datei mit Verwaltungsdaten der Lerngruppen (minimaler Inhalt: {"requested": {},"groups": {}}) - DISCORD_LEARNINGGROUPS_COURSE_FILE - Name der Datei welche die Kursnamen für die Lerngruppen-Informationen enthält (minimalter Inhalt: {}) - DISCORD_MOD_ROLE - ID der Moderator Rolle von der erweiterte Lerngruppen-Actionen ausgeführt werden dürfen + DISCORD_LEARNINGGROUPS_COURSE_FILE - Name der Datei welche die Kursnamen für die Lerngruppen-Informationen enthält (minimaler Inhalt: {}) + DISCORD_MOD_ROLE - ID der Moderations-Rolle, die erweiterte Lerngruppen-Aktionen ausführen darf """ LG_OPEN_SYMBOL = f'🌲' @@ -41,8 +42,8 @@ class GroupState(Enum): @help_category("learninggroups", "Lerngruppen", - "Mit dem Lerngruppen-Feature kannst du Lerngruppen-Kanäle beantragen und verwalten.", - "Hier kannst du Lerngruppen-Kanäle anlegen, beantragen und verwalten.") + "Mit dem Lerngruppen-Feature kannst du Lerngruppenkanäle beantragen und verwalten.", + "Hier kannst du Lerngruppenkanäle anlegen, beantragen und verwalten.") class LearningGroups(commands.Cog): def __init__(self, bot): self.bot = bot @@ -68,9 +69,9 @@ class LearningGroups(commands.Cog): self.support_channel = os.getenv('DISCORD_SUPPORT_CHANNEL') self.mod_role = os.getenv("DISCORD_MOD_ROLE") self.guild_id = os.getenv("DISCORD_GUILD") - self.groups = {} # owner and learninggroup-member ids + self.groups = {} # organizer and learninggroup-member ids self.channels = {} # complete channel configs - self.header = {} # headlines for statusmessage + self.header = {} # headlines for status message self.load_groups() self.load_header() @@ -132,13 +133,13 @@ class LearningGroups(commands.Cog): return GroupState.PRIVATE return None - def is_request_owner(self, request, member): - return request["owner_id"] == member.id + def is_request_organizer(self, request, member): + return request["organizer_id"] == member.id - def is_group_owner(self, channel, member): + def is_group_organizer(self, channel, member): channel_config = self.groups["groups"].get(str(channel.id)) if channel_config: - return channel_config["owner_id"] == member.id + return channel_config["organizer_id"] == member.id return False def is_mod(self, member): @@ -177,7 +178,7 @@ class LearningGroups(commands.Cog): seconds = channel_config["last_rename"] + self.rename_ratelimit - now if seconds > 0: channel = await self.bot.fetch_channel(int(channel_config["channel_id"])) - await channel.send(f"Discord limitiert die Aufrufe für manche Funktionen, daher kannst du diese Aktion erst wieder in {seconds} Sekunden ausführen.") + await channel.send(f"Discord schränkt die Anzahl der Aufrufe für manche Funktionen ein, daher kannst du diese Aktion erst wieder in {seconds} Sekunden ausführen.") return seconds > 0 async def category_of_channel(self, state: GroupState): @@ -233,7 +234,7 @@ class LearningGroups(commands.Cog): if lg_channel['is_listed'] and lg_channel['state'] == GroupState.PRIVATE: group_config = self.groups["groups"].get(lg_channel['channel_id']) if group_config: - user = await self.bot.fetch_user(group_config['owner_id']) + user = await self.bot.fetch_user(group_config['organizer_id']) if user: course_msg += f" **@{user.name}#{user.discriminator}**" course_msg += f"\n **↳** `!lg join {groupchannel.id}`" @@ -244,7 +245,7 @@ class LearningGroups(commands.Cog): 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)}**") + await support_channel.send(f"In der Lerngruppenübersicht fehlen noch Überschriften für die folgenden Kurse: **{', '.join(no_headers)}**") info_message_ids.append(message.id) self.groups["messageids"] = info_message_ids await self.save_groups() @@ -257,8 +258,10 @@ class LearningGroups(commands.Cog): category = await self.bot.fetch_channel(self.categories[GroupState.ARCHIVED]) await self.move_channel(channel, category) await channel.edit(name=f"archiv-${channel.name[1:]}") - await self.remove_group(channel) await self.update_permissions(channel) + await self.remove_group(channel) + await self.update_statusmessage() + async def set_channel_state(self, channel, state: GroupState = None): channel_config = self.channels[str(channel.id)] @@ -268,7 +271,7 @@ class LearningGroups(commands.Cog): if state is not None: old_state = channel_config["state"] if old_state == state: - return False # prevent api requests when nothing changed + return False # prevent api requests when nothing has changed channel_config["state"] = state await self.alter_channel(channel, channel_config) return True @@ -280,7 +283,7 @@ class LearningGroups(commands.Cog): if channel_config["state"] in [GroupState.CLOSED, GroupState.PRIVATE]: was_listed = channel_config["is_listed"] if was_listed == is_listed: - return False # prevent api requests when nothing changed + return False # prevent api requests when nothing has changed channel_config["is_listed"] = is_listed await self.alter_channel(channel, channel_config) return True @@ -322,16 +325,16 @@ class LearningGroups(commands.Cog): 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"]) + user = await self.bot.fetch_user(requested_channel_config["organizer_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." "```" - "Besitzerinfunktionen:\n" + "Funktionen für Lerngruppenorganisatorinnen:\n" "!lg addmember <@newmember>: Fügt ein Mitglied zur Lerngruppe hinzu.\n" - "!lg owner <@newowner>: Ändert die Besitzerin der Lerngruppe auf @newowner.\n" + "!lg organizer <@neworganizer>: Ändert die Organisatorin der Lerngruppe auf @neworganizer.\n" "!lg open: Öffnet eine Lerngruppe.\n" "!lg close: Schließt eine Lerngruppe.\n" "!lg private: Stellt die Lerngruppe auf privat.\n" @@ -341,17 +344,17 @@ class LearningGroups(commands.Cog): "\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 organizer: Zeigt die Organisatorin der Lerngruppe an.\n" "!lg leave: Du verlässt die Lerngruppe.\n" - "!lg join: Anfrage stellen in die Lerngruppe aufgenommen zu werden.\n" + "!lg join: Anfrage, um der Lerngruppe beizutreten.\n" "\nMit dem nachfolgenden Kommando kann eine Kommilitonin darum " - "bitten in die Lerngruppe aufgenommen zu werden wenn diese bereits privat ist.\n" + "bitten in die Lerngruppe aufgenommen zu werden wenn die Gruppe 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)" + "\n(Manche Kommandos werden von Discord eingeschränkt und können nur einmal alle 5 Minuten ausgeführt werden.)" "```" ) self.groups["groups"][str(channel.id)] = { - "owner_id": requested_channel_config["owner_id"], + "organizer_id": requested_channel_config["organizer_id"], "last_rename": int(time.time()) } @@ -417,11 +420,14 @@ class LearningGroups(commands.Cog): users[mid] = True user = await self.bot.fetch_user(mid) if user and send_message: - await utils.send_dm(user, f"Du wurdest in die Lerngruppe <#{channel.id}> aufgenommen. " - "Viel Spass beim gemeinsamen Lernen!\n" - "Dieser Link führt dich direkt zum Lerngruppen-Channel. " - "Diese Nachricht kannst du bei Bedarf in unserer Unterhaltung " - "über Rechtsklick anpinnen.") + try: + await utils.send_dm(user, f"Du wurdest in die Lerngruppe <#{channel.id}> aufgenommen. " + "Viel Spass beim gemeinsamen Lernen!\n" + "Dieser Link führt dich direkt zum Lerngruppenkanal. " + "Diese Nachricht kannst du in unserer Unterhaltung mit Rechtsklick anpinnen, " + "wenn du möchtest.") + except: + pass group_config["users"] = users @@ -440,7 +446,7 @@ class LearningGroups(commands.Cog): if users.pop(mid, None): user = await self.bot.fetch_user(mid) if user and send_message: - await utils.send_dm(user, f"Du wurdest aus der Lerngruppe {channel.name} entfernt") + await utils.send_dm(user, f"Du wurdest aus der Lerngruppe {channel.name} entfernt.") await self.save_groups() @@ -466,11 +472,11 @@ class LearningGroups(commands.Cog): if not group_config: return overwrites - owner = self.bot.get_user(group_config["owner_id"]) - if not owner: + organizer = self.bot.get_user(group_config["organizer_id"]) + if not organizer: return overwrites - overwrites[owner] = disnake.PermissionOverwrite(read_messages=True) + overwrites[organizer] = disnake.PermissionOverwrite(read_messages=True) users = group_config.get("users") if not users: return overwrites @@ -489,12 +495,12 @@ class LearningGroups(commands.Cog): @commands.group(name="lg", aliases=["learninggroup", "lerngruppe"], pass_context=True) async def cmd_lg(self, ctx): if not ctx.invoked_subcommand: - await ctx.channel.send("Gib `!help lg` ein um eine Übersicht über die Lerngruppen-Kommandos zu erhalten.") + await ctx.channel.send("Gib `!help lg` ein, um eine Übersicht über die Lerngruppen-Kommandos zu erhalten.") @help( command_group="lg", category="learninggroups", - brief="Updated die Lerngruppenliste", + brief="Aktualisiert die Lerngruppenliste", mod=True ) @cmd_lg.command(name="update") @@ -508,14 +514,14 @@ class LearningGroups(commands.Cog): category="learninggroups", 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.", + "Der Name 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. Existiert die Kursnummer bereits, " + "wird die Überschrift geändert.", mod=True ) @cmd_lg.command(name="header") @@ -535,22 +541,22 @@ class LearningGroups(commands.Cog): category="learninggroups", 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.", + brief="Fügt einen Lerngruppenkanal 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)."), "status": "Gibt an ob die Lerngruppe für weitere Lernwillige geöffnet ist (open) oder nicht (closed).", - "@usermention": "Die so erwähnte Benutzerin wird als Besitzerin für die Lerngruppe gesetzt." + "@usermention": "Die so erwähnte Benutzerin wird als Organisatorin der Lerngruppe eingesetzt." }, mod=True ) @cmd_lg.command(name="add") @commands.check(utils.is_mod) - async def cmd_add_group(self, ctx, arg_course, arg_name, arg_semester, arg_state, arg_owner: disnake.Member): + async def cmd_add_group(self, ctx, arg_course, arg_name, arg_semester, arg_state, arg_organizer: disnake.Member): state = self.arg_state_to_group_state(arg_state) - channel_config = {"owner_id": arg_owner.id, "course": arg_course, "name": arg_name, "semester": arg_semester, + channel_config = {"organizer_id": arg_organizer.id, "course": arg_course, "name": arg_name, "semester": arg_semester, "state": state, "is_listed": False} if not await self.is_channel_config_valid(ctx, channel_config, ctx.command.name): @@ -564,30 +570,50 @@ class LearningGroups(commands.Cog): command_group="lg", category="learninggroups", syntax="!lg request <coursenumber> <name> <semester> <status>", - brief="Stellt eine Anfrage für einen neuen Lerngruppen-Kanal.", + brief="Stellt eine Anfrage für einen neuen Lerngruppenkanal.", example="!lg request 1142 mathegenies sose22 closed", description=("Moderatorinnen können diese Anfrage bestätigen, dann wird die Gruppe eingerichtet. " - "Die Besitzerin der Gruppe ist die Benutzerin die die Anfrage eingestellt hat."), + "Die Organisatorin der Gruppe ist die Benutzerin, die die Anfrage gestellt 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 Jahreszahl (z. B. sose22).", - "status": "Gibt an ob die Lerngruppe für weitere Lernwillige geöffnet ist (open) oder nicht (closed)." + "status": "Gibt an ob die Lerngruppe für weitere Lernwillige geöffnet ist (open) oder nicht (closed) oder ob es sich um eine private Lerngruppe handelt (private)." } ) @cmd_lg.command(name="request", aliases=["r", "req"]) async def cmd_request_group(self, ctx, arg_course, arg_name, arg_semester, arg_state): + + arg_state = re.sub(r"[^a-z0-9]", "", arg_state.lower()) + arg_semester = re.sub(r"[^a-z0-9]", "", arg_semester.lower()) + + if re.match(r"(wise)|(sose)[0-9]+", arg_state) and re.match(r"(open)|(closed*)|(private)", arg_semester): + tmp = arg_state + arg_state = arg_semester + arg_semester = tmp + + arg_semester = re.sub(r"[^wiseo0-9]", "", arg_semester) + + arg_state = re.sub(r"[^a-z]", "", arg_state) + state = self.arg_state_to_group_state(arg_state) + + arg_course = re.sub(r"[^0-9]", "", arg_course) + arg_course = re.sub(r"^0+", "", arg_course) + 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, + channel_config = {"organizer_id": ctx.author.id, "course": arg_course, "name": arg_name, "semester": arg_semester, "state": state, "is_listed": False} if not await self.is_channel_config_valid(ctx, channel_config, ctx.command.name): @@ -610,14 +636,14 @@ class LearningGroups(commands.Cog): 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. ") + description=("Muss im betreffenden Lerngruppenkanal ausgeführt werden. " + "Die Lerngruppe wird in der Übersicht der Lerngruppen aufgeführt, so dass Kommilitoninnen noch " + "anfragen können, in die Lerngruppe aufgenommen zu werden." + "Diese Aktion kann nur von der Organisatorin 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): + if self.is_group_organizer(ctx.channel, ctx.author) or utils.is_mod(ctx): channel_config = self.channels[str(ctx.channel.id)] if channel_config: if channel_config.get("state") == GroupState.PRIVATE: @@ -626,7 +652,7 @@ class LearningGroups(commands.Cog): elif channel_config.get("state") == GroupState.OPEN: await ctx.channel.send("Nichts zu tun. Offene Lerngruppen werden sowieso in der Liste angezeigt.") elif channel_config.get("state") == GroupState.CLOSED: - await ctx.channel.send("Möchtest du die Gruppen öffnen? Versuch‘s mit `!lg open`") + await ctx.channel.send("Möchtest du die Gruppen öffnen? Versuch's mit `!lg open`") @help( @@ -634,13 +660,13 @@ class LearningGroups(commands.Cog): 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. ") + description=("Muss im betreffenden Lerngruppenkanal ausgeführt werden. " + "Die Lerngruppe wird nicht mehr in der Liste der Lerngruppen angezeigt. " + "Diese Aktion kann nur von der Organisatorin 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): + if self.is_group_organizer(ctx.channel, ctx.author) or utils.is_mod(ctx): channel_config = self.channels[str(ctx.channel.id)] if channel_config: if channel_config.get("state") == GroupState.PRIVATE: @@ -650,8 +676,8 @@ class LearningGroups(commands.Cog): elif channel_config.get("state") == GroupState.OPEN: await ctx.channel.send("Offene Lerngruppen können nicht aus der Lerngruppenliste entfernt werden. " - "Führe `!lg close` aus um die Lerngruppe zu schließen, " - "oder `!lg private` um diese auf " + "Führe `!lg close` aus, um die Lerngruppe zu schließen, " + "oder `!lg private`, um diese auf " "privat zu schalten.") elif channel_config.get("state") == GroupState.CLOSED: await ctx.channel.send("Wenn diese Gruppe privat werden soll, ist das Kommando das du brauchst: `!lg private`") @@ -670,44 +696,44 @@ class LearningGroups(commands.Cog): 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 von der Besitzerin der Lerngruppe ausgeführt werden. ") + brief="Öffnet den Lerngruppenkanal, wenn du die Organisatorin bist. ", + description=("Muss im betreffenden Lerngruppenkanal ausgeführt werden. " + "Verschiebt den Lerngruppenkanal in die Kategorie für offene Kanäle und ändert das Icon. " + "Diese Aktion kann nur von der Organisatorin der Lerngruppe ausgeführt werden. ") ) @cmd_lg.command(name="open", aliases=["opened", "offen"]) async def cmd_open(self, ctx): - if self.is_group_owner(ctx.channel, ctx.author) or utils.is_mod(ctx): + if self.is_group_organizer(ctx.channel, ctx.author) or utils.is_mod(ctx): await self.set_channel_state(ctx.channel, state=GroupState.OPEN) @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. " + brief="Schließt den Lerngruppenkanal, wenn du die Organisatorin bist. ", + description=("Muss im betreffenden Lerngruppenkanal ausgeführt werden. " "Stellt die Lerngruppe auf geschlossen. Dies ist rein symbolisch und zeigt an, " "dass keine neuen Mitglieder mehr aufgenommen werden. " - "Diese Aktion kann nur von der Besitzerin der Lerngruppe ausgeführt werden. ") + "Diese Aktion kann nur von der Organisatorin der Lerngruppe ausgeführt werden. ") ) @cmd_lg.command(name="close", aliases=["closed", "geschlossen"]) async def cmd_close(self, ctx): - if self.is_group_owner(ctx.channel, ctx.author) or utils.is_mod(ctx): + if self.is_group_organizer(ctx.channel, ctx.author) or utils.is_mod(ctx): await self.set_channel_state(ctx.channel, state=GroupState.CLOSED) @help( command_group="lg", category="learninggroups", syntax="!lg private", - brief="Macht aus deiner Lerngruppe eine private Lerngruppe wenn du die Besitzerin bist. ", - description=("Muss im betreffenden Lerngruppen-Kanal ausgeführt werden. " + brief="Macht aus deiner Lerngruppe eine private Lerngruppe, wenn du die Organisatorin bist. ", + description=("Muss im betreffenden Lerngruppenkanal 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. ") + "der Lerngruppe Zugriff auf den Kanal. (siehe `!lg members`)" + "Diese Aktion kann nur von der Organisatorin der Lerngruppe ausgeführt werden. ") ) @cmd_lg.command(name="private", aliases=["privat"]) async def cmd_private(self, ctx): - if self.is_group_owner(ctx.channel, ctx.author) or utils.is_mod(ctx): + if self.is_group_organizer(ctx.channel, ctx.author) or utils.is_mod(ctx): if await self.set_channel_state(ctx.channel, state=GroupState.PRIVATE): await self.update_permissions(ctx.channel) @@ -717,7 +743,7 @@ class LearningGroups(commands.Cog): command_group="lg", category="learninggroups", syntax="!lg rename <name>", - brief="Ändert den Namen des Lerngruppen-Kanals, in dem das Komando ausgeführt wird.", + brief="Ändert den Namen des Lerngruppenkanals, in dem das Kommando ausgeführt wird.", example="!lg rename matheluschen", description="Aus #1142-matheprofis-sose22 wird nach dem Aufruf des Beispiels #1142-matheluschen-sose22.", parameters={ @@ -734,8 +760,8 @@ class LearningGroups(commands.Cog): 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.", + brief="Archiviert den Lerngruppenkanal.", + description="Verschiebt den Lerngruppenkanal, in welchem dieses Kommando ausgeführt wird, ins Archiv.", mod=True ) @cmd_lg.command(name="archive", aliases=["archiv"]) @@ -746,42 +772,42 @@ class LearningGroups(commands.Cog): @help( command_group="lg", category="learninggroups", - syntax="!lg owner <@usermention>", - example="!owner @someuser", - brief="Setzt die Besitzerin eines Lerngruppen-Kanals", - description="Muss im betreffenden Lerngruppen-Kanal ausgeführt werden. ", + syntax="!lg organizer <@usermention>", + example="!lg organizer @someuser", + brief="Bestimmt die Organisatorin eines Lerngruppenkanals.", + description="Muss im betreffenden Lerngruppenkanal ausgeführt werden. ", parameters={ - "@usermention": "Die neue Besitzerin der Lerngruppe." + "@usermention": "Die neue Organisatorin der Lerngruppe." } ) - @cmd_lg.command(name="owner") - async def cmd_owner(self, ctx, new_owner: disnake.Member = None): + @cmd_lg.command(name="organizer") + async def cmd_organizer(self, ctx, new_organizer: 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)] - owner_id = group_config.get("owner_id") + organizer_id = group_config.get("organizer_id") - if not owner_id: + if not organizer_id: return - if not new_owner: - user = await self.bot.fetch_user(owner_id) - await ctx.channel.send(f"Besitzerin: @{user.name}#{user.discriminator}") + if not new_organizer: + user = await self.bot.fetch_user(organizer_id) + await ctx.channel.send(f"Organisatorin: @{user.name}#{user.discriminator}") elif isinstance(group_config, dict): - owner = await self.bot.fetch_user(owner_id) - if self.is_group_owner(ctx.channel, ctx.author) or utils.is_mod(ctx): - group_config["owner_id"] = new_owner.id - await self.remove_member_from_group(ctx.channel, new_owner, False) - if new_owner != owner: - await self.add_member_to_group(ctx.channel, owner, False) + organizer = await self.bot.fetch_user(organizer_id) + if self.is_group_organizer(ctx.channel, ctx.author) or utils.is_mod(ctx): + group_config["organizer_id"] = new_organizer.id + await self.remove_member_from_group(ctx.channel, new_organizer, False) + if new_organizer != organizer: + await self.add_member_to_group(ctx.channel, organizer, False) await self.save_groups() await self.update_permissions(ctx.channel) await ctx.channel.send( - f"Glückwunsch {new_owner.mention}! Du bist jetzt die Besitzerin dieser Lerngruppe.") + f"Glückwunsch {new_organizer.mention}! Du bist jetzt die Organisatorin dieser Lerngruppe.") @help( command_group="lg", @@ -791,7 +817,7 @@ class LearningGroups(commands.Cog): brief="Fügt eine Benutzerin zu einer Lerngruppe hinzu.", parameters={ "@usermention": "Die so erwähnte Benutzerin wird zur Lerngruppe hinzugefügt.", - "#channel": "(optional) Der Kanal dem die Benutzerin hinzugefügt werden soll." + "#channel": "(optional) Der Kanal, zu dem die Benutzerin hinzugefügt werden soll." } ) @cmd_lg.command(name="addmember", aliases=["addm", "am"]) @@ -799,10 +825,10 @@ class LearningGroups(commands.Cog): if not arg_channel: if not self.channels.get(str(ctx.channel.id)): await ctx.channel.send("Wenn das Kommando außerhalb eines Lerngruppenkanals aufgerufen wird, muss der" - "Lerngruppenkanal angehängt werden. `!lg addmember <@usermention> <#channel>`") + "Lerngruppenkanal angefügt werden. `!lg addmember <@usermention> <#channel>`") return arg_channel = ctx.channel - if self.is_group_owner(arg_channel, ctx.author) or utils.is_mod(ctx): + if self.is_group_organizer(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) @@ -813,7 +839,7 @@ class LearningGroups(commands.Cog): example="!lg removemember @someuser #1141-mathegl-lerngruppe-sose21", brief="Entfernt eine Benutzerin aus einer Lerngruppe.", parameters={ - "#channel": "Der Kanal aus dem die Benutzerin gelöscht werden soll.", + "#channel": "Der Kanal, aus dem die Benutzerin gelöscht werden soll.", "@usermention": "Die so erwähnte Benutzerin wird aus der Lerngruppe entfernt." }, mod=True @@ -828,7 +854,7 @@ class LearningGroups(commands.Cog): command_group="lg", category="learninggroups", syntax="!lg members", - brief="Listet die Mitglieder der Lerngruppe auf.", + brief="Zählt die Mitglieder der Lerngruppe auf.", ) @cmd_lg.command(name="members") async def cmd_members(self, ctx): @@ -836,14 +862,14 @@ class LearningGroups(commands.Cog): if not group_config: await ctx.channel.send("Das ist kein Lerngruppenkanal.") return - owner_id = group_config.get("owner_id") + organizer_id = group_config.get("organizer_id") - if not owner_id: + if not organizer_id: return - owner = await self.bot.fetch_user(owner_id) + organizer = await self.bot.fetch_user(organizer_id) users = group_config.get("users", {}) - if not users and not owner: + if not users and not organizer: await ctx.channel.send("Keine Lerngruppenmitglieder vorhanden.") return @@ -853,7 +879,7 @@ class LearningGroups(commands.Cog): user = await self.bot.fetch_user(user_id) names.append("@" + user.name + "#" + user.discriminator) - await ctx.channel.send(f"Besitzerin: **@{owner.name}#{owner.discriminator}**\nMitglieder: " + + await ctx.channel.send(f"Organisatorin: **@{organizer.name}#{organizer.discriminator}**\nMitglieder: " + (f"{', '.join(names)}" if len(names) > 0 else "Keine")) @help( @@ -864,7 +890,7 @@ class LearningGroups(commands.Cog): ) @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): + if self.is_group_organizer(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.") @@ -876,9 +902,9 @@ class LearningGroups(commands.Cog): command_group="lg", category="learninggroups", syntax="!lg join <lg-id>", - brief="Fragt bei der Besitzerin einer Lerngruppe um Aufnahme.", + brief="Fragt bei der Organisatorin einer Lerngruppe um Aufnahme an.", parameters={ - "id": "Die ID zur Lerngruppe." + "id": "Die ID der Lerngruppe." } ) @cmd_lg.command(name="join") @@ -900,12 +926,12 @@ class LearningGroups(commands.Cog): 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}>", + message=f"<@!{group_config['organizer_id']}>, du wirst gebraucht. Anfrage von <@!{ctx.author.id}>:", custom_prefix="learninggroups:join" ) await utils.send_dm(ctx.author, f"Deine Anfrage wurde an **#{channel.name}** gesendet. " - "Sobald die Besitzerin der Lerngruppe darüber " - "entschieden hat bekommst du Bescheid.") + "Sobald die Organisatorin der Lerngruppe darüber " + "entschieden hat, bekommst du Bescheid.") @help( command_group="lg", @@ -915,7 +941,7 @@ class LearningGroups(commands.Cog): ) @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): + if self.is_group_organizer(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.") @@ -937,8 +963,8 @@ class LearningGroups(commands.Cog): 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.") + if group_config["organizer_id"] == ctx.author.id: + await ctx.channel.send("Du kannst nicht aus deiner eigenen Lerngruppe flüchten. Gib erst die Verantwortung ab.") return await self.remove_member_from_group(ctx.channel, ctx.author) @@ -954,9 +980,9 @@ class LearningGroups(commands.Cog): 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)): + elif not confirmed and (self.is_request_organizer(request, member) or self.is_mod(member)): if self.is_mod(member): - user = await self.bot.fetch_user(request["owner_id"] ) + user = await self.bot.fetch_user(request["organizer_id"] ) if user: await utils.send_dm(user, f"Deine Lerngruppenanfrage für #{self.full_channel_name(request)} wurde abgelehnt.") await self.remove_group_request(message) @@ -972,10 +998,10 @@ class LearningGroups(commands.Cog): if not group_config: return - if self.is_group_owner(channel, member) or self.is_mod(member): + if self.is_group_organizer(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]) + if message.mentions and len(message.mentions) == 2: + await self.add_member_to_group(channel, message.mentions[1]) await self.update_permissions(channel) else: diff --git a/cogs/roles.py b/cogs/roles.py index fa3a4d4..f3be0d5 100644 --- a/cogs/roles.py +++ b/cogs/roles.py @@ -2,24 +2,18 @@ import json import os import disnake +import emoji from disnake.ext import commands -import utils -from cogs.help import help, handle_error, help_category - -@help_category("updater", "Updater", "Diese Kommandos werden zum Updaten von Nachrichten benutzt, die Boty automatisch erzeugt.") -@help_category("info", "Informationen", "Kleine Helferlein, um schnell an Informationen zu kommen.") class Roles(commands.Cog): def __init__(self, bot): self.bot = bot self.roles_file = os.getenv("DISCORD_ROLES_FILE") self.channel_id = int(os.getenv("DISCORD_ROLLEN_CHANNEL", "0")) - self.degree_program_message_id = int(os.getenv("DISCORD_DEGREE_PROGRAM_MSG", "0")) - self.color_message_id = int(os.getenv("DISCORD_COLOR_MSG", "0")) - self.special_message_id = int(os.getenv("DISCORD_SPECIAL_MSG", "0")) self.assignable_roles = {} self.load_roles() + self.register_views() def load_roles(self): """ Loads all assignable roles from ROLES_FILE """ @@ -27,202 +21,102 @@ class Roles(commands.Cog): roles_file = open(self.roles_file, mode='r') self.assignable_roles = json.load(roles_file) - def get_degree_program_emojis(self): - """ Creates a dict for degree program role emojis """ - - tmp_emojis = {} - emojis = {} - degree_program_assignable = self.assignable_roles[0] - - # start with getting all emojis that are used in those roles as a dict - for emoji in self.bot.emojis: - if emoji.name in degree_program_assignable: - tmp_emojis[emoji.name] = emoji - - # bring them in desired order - for key in degree_program_assignable.keys(): - emojis[key] = tmp_emojis.get(key) - - return emojis - - def get_color_emojis(self): - """ Creates a dict for color role emojis """ - - emojis = {} - color_assignable = self.assignable_roles[1] + def register_views(self): + """ Register view for each category at view manager """ - # start with getting all emojis that are used in those roles as a dict - for emoji in self.bot.emojis: - if emoji.name in color_assignable: - emojis[emoji.name] = emoji + for role_category, roles in self.assignable_roles.items(): + prefix = f"assign_{role_category}" + self.bot.view_manager.register(prefix, self.on_button_clicked) - return emojis + def get_stat_roles(self): + """ Get all roles that should be part of the stats Command """ - def get_special_emojis(self): - """ Creates a dict for special role emojis """ + stat_roles = [] + for category in self.assignable_roles.values(): + if category["in_stats"]: + for role in category["roles"].values(): + stat_roles.append(role["name"]) - return self.assignable_roles[2] + return stat_roles - def get_key(self, role): - """ Get the key for a given role. This role is used for adding or removing a role from a user. """ + async def on_button_clicked(self, button: disnake.ui.Button, interaction: disnake.MessageInteraction, value=None): + """ + Add or Remove Roles, when Button is clicked. Role gets added, if the user clicking the button doesn't have + the role already assigned, and removed, if the role is already assigned + """ - for key, role_name in self.assignable_roles[0].items(): - if role_name == role.name: - return key + guild_roles = {str(role.id): role for role in interaction.guild.roles} + role = guild_roles.get(value) - @help( - category="info", - brief="Gibt die Mitgliederstatistik aus." - ) - @commands.command(name="stats") - async def cmd_stats(self, ctx): - """ Sends stats in Chat. """ - - guild = ctx.guild + if role in interaction.author.roles: + await interaction.author.remove_roles(role) + await interaction.send(f"Rolle \"{role.name}\" erfolgreich entfernt", ephemeral=True) + else: + await interaction.author.add_roles(role) + await interaction.send(f"Rolle \"{role.name}\" erfolgreich hinzugefügt", ephemeral=True) + + @commands.slash_command(name="update-roles", description="Update Self-Assignable Roles") + @commands.default_member_permissions(moderate_members=True) + async def cmd_update_roles(self, interaction: disnake.ApplicationCommandInteraction): + """ Update all role assignment messages in role assignment channel """ + await interaction.response.defer(ephemeral=True) + + channel = await interaction.guild.fetch_channel(self.channel_id) + await channel.purge() + for role_category, roles in self.assignable_roles.items(): + prefix = f"assign_{role_category}" + fields = [] + buttons = [] + value = f"" + guild_roles = {role.name: role for role in interaction.guild.roles} + + for key, role in roles.get("roles").items(): + role_emoji = role.get('emoji') if role.get( + 'emoji') in emoji.UNICODE_EMOJI_ALIAS_ENGLISH else f"<{role.get('emoji')}>" + value += f"{role_emoji} : {role.get('name')}\n" + buttons.append({"emoji": role_emoji, "custom_id": f"{prefix}_{key}", + "value": f"{str(guild_roles.get(role.get('name')).id)}"}) + + if roles.get("list_roles"): + fields.append({"name": "Rollen", "value": value, "inline": False}) + + await self.bot.view_manager.dialog( + channel=channel, + title=f"Vergabe von {roles.get('name')}", + description="Durch klicken auf den entsprechenden Button kannst du dir die damit " + "assoziierte Rolle zuweisen, bzw. entfernen.", + message="", + fields=fields, + callback_key=prefix, + buttons=buttons + ) + + await interaction.edit_original_message("Rollen erfolgreich aktualisiert.") + + @commands.slash_command(name="stats", description="Rollen Statistik abrufen") + async def cmd_stats(self, interaction: disnake.ApplicationCommandInteraction, show: bool = False): + """ + Send role statistics into chat, by default as ephemeral + + Parameters + ---------- + show: Sichtbar für alle? + """ + + guild = interaction.guild members = await guild.fetch_members().flatten() - answer = f'' + guild_roles = {role.name: role for role in interaction.guild.roles} + stat_roles = self.get_stat_roles() embed = disnake.Embed(title="Statistiken", - description=f'Wir haben aktuell {len(members)} Mitglieder auf diesem Server, verteilt auf folgende Rollen:') + description=f'Wir haben aktuell {len(members)} Mitglieder auf diesem Server, ' + f'verteilt auf folgende Rollen:') - for role in guild.roles: - if not self.get_key(role): - continue + for role_name in stat_roles: + role = guild_roles[role_name] role_members = role.members - if len(role_members) > 0 and not role.name.startswith("Farbe"): - embed.add_field(name=role.name, value=f'{len(role_members)} Mitglieder', inline=False) - - no_role = 0 - for member in members: - # ToDo Search for study roles only! - if len(member.roles) == 1: - no_role += 1 - - embed.add_field(name="\u200B", value="\u200b", inline=False) - embed.add_field(name="Mitglieder ohne Rolle", value=str(no_role), inline=False) - - await ctx.channel.send(answer, embed=embed) - - @help( - category="updater", - brief="Aktualisiert die Vergabe-Nachricht von Studiengangs-Rollen.", - mod=True - ) - @commands.command("update-degree-program") - @commands.check(utils.is_mod) - async def cmd_update_degree_program(self, ctx): - channel = await self.bot.fetch_channel(self.channel_id) - message = await channel.fetch_message(self.degree_program_message_id) - degree_program_emojis = self.get_degree_program_emojis() - - embed = disnake.Embed(title="Vergabe von Studiengangs-Rollen", - description="Durch klicken auf die entsprechende Reaktion kannst du dir die damit assoziierte Rolle zuweisen, oder entfernen. Dies funktioniert so, dass ein Klick auf die Reaktion die aktuelle Zuordnung dieser Rolle ändert. Das bedeutet, wenn du die Rolle, die mit <:St:763126549327118366> assoziiert ist, schon hast, aber die Reaktion noch nicht ausgewählt hast, dann wird dir bei einem Klick auf die Reaktion diese Rolle wieder weggenommen. ") - - value = f"" - for key, emoji in degree_program_emojis.items(): - if emoji: - value += f"<:{key}:{emoji.id}> : {self.assignable_roles[0].get(key)}\n" - - embed.add_field(name="Rollen", - value=value, - inline=False) - - await message.edit(content="", embed=embed) - await message.clear_reactions() - - for emoji in degree_program_emojis.values(): - if emoji: - await message.add_reaction(emoji) - - @help( - category="updater", - brief="Aktualisiert die Vergabe-Nachricht von Farb-Rollen.", - mod=True - ) - @commands.command("update-color") - @commands.check(utils.is_mod) - async def cmd_update_color(self, ctx): - channel = await self.bot.fetch_channel(self.channel_id) - message = await channel.fetch_message(self.color_message_id) - color_emojis = self.get_color_emojis() - - embed = disnake.Embed(title="Vergabe von Farb-Rollen", - description="Durch klicken auf die entsprechende Reaktion kannst du dir die damit assoziierte Rolle zuweisen, oder entfernen. Dies funktioniert so, dass ein Klick auf die Reaktion die aktuelle Zuordnung dieser Rolle ändert. Das bedeutet, wenn du die Rolle, die mit <:FarbeGruen:771451407916204052> assoziiert ist, schon hast, aber die Reaktion noch nicht ausgewählt hast, dann wird dir bei einem Klick auf die Reaktion diese Rolle wieder weggenommen. ") - - await message.edit(content="", embed=embed) - await message.clear_reactions() - - for emoji in color_emojis.values(): - if emoji: - await message.add_reaction(emoji) - - @help( - category="updater", - brief="Aktualisiert die Vergabe-Nachricht von Spezial-Rollen.", - mod=True - ) - @commands.command("update-special") - @commands.check(utils.is_mod) - async def cmd_update_special(self, ctx): - channel = await self.bot.fetch_channel(self.channel_id) - message = await channel.fetch_message(self.special_message_id) - special_emojis = self.get_special_emojis() - - embed = disnake.Embed(title="Vergabe von Spezial-Rollen", - description="Durch klicken auf die entsprechende Reaktion kannst du dir die damit assoziierte Rolle zuweisen, oder entfernen. Dies funktioniert so, dass ein Klick auf die Reaktion die aktuelle Zuordnung dieser Rolle ändert. Das bedeutet, wenn du die Rolle, die mit :exclamation: assoziiert ist, schon hast, aber die Reaktion noch nicht ausgewählt hast, dann wird dir bei einem Klick auf die Reaktion diese Rolle wieder weggenommen. ") - - value = f"" - for emoji, role in special_emojis.items(): - value += f"{emoji} : {role}\n" - - embed.add_field(name="Rollen", - value=value, - inline=False) - - await message.edit(content="", embed=embed) - await message.clear_reactions() - - for emoji in special_emojis.keys(): - await message.add_reaction(emoji) - - @commands.Cog.listener() - async def on_raw_reaction_add(self, payload): - if payload.user_id == self.bot.user.id or payload.message_id not in [self.degree_program_message_id, - self.color_message_id, - self.special_message_id]: - return - - if payload.emoji.name not in self.assignable_roles[0] and payload.emoji.name not in self.assignable_roles[ - 1] and payload.emoji.name not in self.assignable_roles[2]: - return - - role_name = "" - guild = await self.bot.fetch_guild(payload.guild_id) - member = await guild.fetch_member(payload.user_id) - channel = await self.bot.fetch_channel(payload.channel_id) - message = await channel.fetch_message(payload.message_id) - roles = member.roles - - await message.remove_reaction(payload.emoji, member) - - if payload.emoji.name in self.assignable_roles[0]: - role_name = self.assignable_roles[0].get(payload.emoji.name) - elif payload.emoji.name in self.assignable_roles[1]: - role_name = self.assignable_roles[1].get(payload.emoji.name) - else: - role_name = self.assignable_roles[2].get(payload.emoji.name) - - for role in roles: - if role.name == role_name: - await member.remove_roles(role) - await utils.send_dm(member, f"Rolle \"{role.name}\" erfolgreich entfernt") - break - else: - guild_roles = guild.roles - - for role in guild_roles: - if role.name == role_name: - await member.add_roles(role) - await utils.send_dm(member, f"Rolle \"{role.name}\" erfolgreich hinzugefügt") + num_members = len(role_members) + if num_members > 0: + embed.add_field(name=role.name, + value=f'{num_members} {"Mitglieder" if num_members > 1 else "Mitglied"}', inline=False) - async def cog_command_error(self, ctx, error): - await handle_error(ctx, error) + await interaction.send(embed=embed, ephemeral=not show) diff --git a/fernuni_bot.py b/fernuni_bot.py index 9c9e334..6386018 100644 --- a/fernuni_bot.py +++ b/fernuni_bot.py @@ -56,6 +56,7 @@ class Boty(commands.Bot): self.add_cog(calmdown.Calmdown(self)) self.add_cog(github.Github(self)) self.add_cog(timer.Timer(self)) + # self.add_cog(job_offers.Joboffers(self)) bot = Boty() diff --git a/requirements.txt b/requirements.txt index 7213ab1..06585bd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,21 +1,21 @@ -aiohttp==3.7.4 -async-timeout==3.0.1 -attrs==20.3.0 -beautifulsoup4==4.9.3 -certifi==2020.12.5 -cffi==1.14.5 -chardet==3.0.4 -disnake==2.2.2 -emoji==1.2.0 -idna==2.10 -multidict==5.1.0 -pycparser==2.20 -PyNaCl==1.4.0 -python-dotenv==0.17.0 -requests==2.25.1 -six==1.16.0 -soupsieve==2.2.1 -tinydb==4.4.0 -typing-extensions==3.7.4.3 -urllib3==1.26.5 -yarl==1.6.3 +aiohttp==3.7.4 +async-timeout==3.0.1 +attrs==20.3.0 +beautifulsoup4==4.9.3 +certifi==2020.12.5 +cffi==1.14.5 +chardet==3.0.4 +disnake==2.5.2 +emoji==1.2.0 +idna==2.10 +multidict==5.1.0 +pycparser==2.20 +PyNaCl==1.4.0 +python-dotenv==0.20.0 +requests==2.25.1 +six==1.16.0 +soupsieve==2.2.1 +tinydb==4.4.0 +typing-extensions==3.7.4.3 +urllib3==1.26.5 +yarl==1.6.3 diff --git a/utils.py b/utils.py index 0959473..8490609 100644 --- a/utils.py +++ b/utils.py @@ -15,11 +15,14 @@ DATE_TIME_FMT = os.getenv("DISCORD_DATE_TIME_FORMAT") async def send_dm(user, message, embed=None): """ Send DM to a user/member """ - if type(user) is disnake.User or type(user) is disnake.Member: - if user.dm_channel is None: - await user.create_dm() - - return await user.dm_channel.send(message, embed=embed) + try: + if type(user) is disnake.User or type(user) is disnake.Member: + if user.dm_channel is None: + await user.create_dm() + + return await user.dm_channel.send(message, embed=embed) + except: + print(f"Cannot send DM to {user} with text: {message}") def is_mod(ctx): diff --git a/views/joboffers_view.py b/views/joboffers_view.py new file mode 100644 index 0000000..588abba --- /dev/null +++ b/views/joboffers_view.py @@ -0,0 +1,35 @@ +import disnake +from disnake import MessageInteraction, ButtonStyle +from disnake.ui import Button, View + +NEXT = "jobs:next" +PREV = "jobs:prev" + + +class JobOffersView(View): + def __init__(self, callback, list_of_pages, actual_page_nr, embed_description): + super().__init__(timeout=None) + self.callback = callback + self.list_of_pages = list_of_pages + self.actual_page_nr = actual_page_nr + self.embed_description = embed_description + if actual_page_nr == 1: + self.disable_prev() + if actual_page_nr == len(self.list_of_pages): + self.disable_next() + + @disnake.ui.button(emoji="⬅", custom_id=PREV) + async def btn_prev(self, button: Button, interaction: MessageInteraction): + await self.callback(button, interaction, self.list_of_pages, self.actual_page_nr, self.embed_description) + + @disnake.ui.button(emoji="➡", custom_id=NEXT) + async def btn_next(self, button: Button, interaction: MessageInteraction): + await self.callback(button, interaction, self.list_of_pages, self.actual_page_nr, self.embed_description) + + def disable_prev(self): + prev_button = self.children[0] + prev_button.disabled = True + + def disable_next(self): + next_button = self.children[1] + next_button.disabled = True \ No newline at end of file -- GitLab