From e5729c271ed2a9e9761481a63272ddf66500f94d Mon Sep 17 00:00:00 2001 From: Quentin Mouilade Date: Thu, 2 Dec 2021 21:29:27 +0100 Subject: [PATCH 1/6] Wip reservation extension - Updated requirements - --- isartbot/ext/__init__.py | 15 +++---- isartbot/ext/reservation.py | 69 +++++++++++++++++++++++++++++++++ isartbot/languages/english.lang | 12 +++++- isartbot/languages/french.lang | 11 +++++- requirements.txt | 3 ++ settings.ini | 4 +- 6 files changed, 104 insertions(+), 10 deletions(-) create mode 100644 isartbot/ext/reservation.py diff --git a/isartbot/ext/__init__.py b/isartbot/ext/__init__.py index e462971..f656121 100644 --- a/isartbot/ext/__init__.py +++ b/isartbot/ext/__init__.py @@ -22,10 +22,11 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -from .iam import * -from .ext import * -from .test import * -from .lang import * -from .game import * -from .starboard import * -from .foodtruck import * \ No newline at end of file +from .iam import * +from .ext import * +from .test import * +from .lang import * +from .game import * +from .starboard import * +from .foodtruck import * +from .reservation import * \ No newline at end of file diff --git a/isartbot/ext/reservation.py b/isartbot/ext/reservation.py new file mode 100644 index 0000000..c20ae96 --- /dev/null +++ b/isartbot/ext/reservation.py @@ -0,0 +1,69 @@ +# -*- coding: utf-8 -*- + +# MIT License + +# Copyright (c) 2018 - 2021 Renondedju + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +import discord + +from datetime import datetime +from dataclasses import dataclass +from discord.ext import tasks, commands + +class ReservationExt(commands.Cog): + + @dataclass + class Reservation: + date: datetime + location: str + + """ Helps to create and check room reservation """ + def __init__(self, bot): + self.bot = bot + self.reservation_scan.start() + + def cog_unload(self): + self.reservation_scan.cancel() + + @tasks.loop(minutes=10.0) + async def reservation_scan(self): + return + + @reservation_scan.before_loop + async def pre_reservation_scan(self): + await self.bot.wait_until_ready() + + @commands.group(invoke_without_command=True, pass_context=True, + help="reservation_help", description="reservation_description") + async def reservation(self, ctx): + await ctx.send_help(ctx.command) + + @reservation.command(help="reservation_list_help", description="reservation_list_description") + async def list(self, ctx, page=1): + await ctx.send(embed=discord.Embed( + description = "ToDo", + title = await ctx.bot.get_translation(ctx, 'reservation_list_title'), + color = discord.Color.green() + )) + +def setup(bot): + bot.add_cog(ReservationExt(bot)) + \ No newline at end of file diff --git a/isartbot/languages/english.lang b/isartbot/languages/english.lang index 55e8a22..c8494eb 100644 --- a/isartbot/languages/english.lang +++ b/isartbot/languages/english.lang @@ -286,9 +286,19 @@ test_delay_description=Waits for 2 seconds. test_denied_help=Should never show test_denied_description=Should never execute. + ## Foodtruck foodtruck_help=Prints a list of upcoming foodtrucks foodtruck_description=Prints the list of every upcoming foodtrucks foodtruck_list_title=Upcoming foodtrucks: -foodtruck_list_empty=Such empty ¯\_(ツ)_/¯ \ No newline at end of file +foodtruck_list_empty=Such empty ¯\_(ツ)_/¯ + + +## Reservation + +reservation_help=ToDo help +reservation_description=ToDo description +reservation_list_help=Prints the list of all current reservations and their status +reservation_list_description=Prints the list of all current reservations and their status.\n[page]: Page to be displayed +reservation_list_title=Reservation list \ No newline at end of file diff --git a/isartbot/languages/french.lang b/isartbot/languages/french.lang index 0c59a18..4111184 100644 --- a/isartbot/languages/french.lang +++ b/isartbot/languages/french.lang @@ -286,9 +286,18 @@ test_delay_description=Attendez 2 secondes. test_denied_help=Ne devrait jamais montrer test_denied_description=Ne devrait jamais s'exécuter. + ## Foodtruck foodtruck_help=Imprime une liste des foodtrucks à venir foodtruck_description=Imprime la liste de tous les foodtrucks à venir foodtruck_list_title=Foodtrucks à venir : -foodtruck_list_empty=Tellement vide ¯\_(ツ)_/¯ \ No newline at end of file +foodtruck_list_empty=Tellement vide ¯\_(ツ)_/¯ + +## Reservation + +reservation_help=ToDo aide +reservation_description=ToDo description +reservation_list_help=Affiche la liste de toutes les réservations ainsi que leur status +reservation_list_description=Affiche la liste de toutes les réservations ainsi que leur status.\n[page]: Page à afficher +reservation_list_title=Liste des réservations \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 077eec2..e1eadbb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,6 @@ discord.py emoji sqlalchemy +google-api-python-client +google-auth-httplib2 +google-auth-oauthlib \ No newline at end of file diff --git a/settings.ini b/settings.ini index 7e2504f..6ca4322 100644 --- a/settings.ini +++ b/settings.ini @@ -5,7 +5,8 @@ [common] database=database.db prefix=! -super_admins=[213262036069515264] +super_admins=[213262036069515264, + 421068194095300609] # If developement_mode is set to 'yes', # every required permission can be bypassed by the following developer ids @@ -26,6 +27,7 @@ liverole=yes starboard=yes foodtruck=yes moderation=yes +reservation=yes verification=yes # Path to all the language files of the bot From 468bf5da3e7e5fbc36f3625e5dea96cce48c0878 Mon Sep 17 00:00:00 2001 From: Quentin Mouilade Date: Thu, 2 Dec 2021 23:42:49 +0100 Subject: [PATCH 2/6] Added google api services to ReservationExt --- .gitignore | 1 + isartbot/ext/reservation.py | 26 +++++++++++++++++++++++--- settings.ini | 15 +++++++++++---- 3 files changed, 35 insertions(+), 7 deletions(-) diff --git a/.gitignore b/.gitignore index ec9c3b3..658e541 100644 --- a/.gitignore +++ b/.gitignore @@ -25,6 +25,7 @@ MANIFEST token.ini database.db +google_credentials.json # PyInstaller # Usually these files are written by a python script from a template diff --git a/isartbot/ext/reservation.py b/isartbot/ext/reservation.py index c20ae96..19c4a81 100644 --- a/isartbot/ext/reservation.py +++ b/isartbot/ext/reservation.py @@ -23,10 +23,14 @@ # SOFTWARE. import discord +import json +import httplib2 -from datetime import datetime -from dataclasses import dataclass -from discord.ext import tasks, commands +from datetime import datetime +from dataclasses import dataclass +from discord.ext import tasks, commands +from googleapiclient.discovery import build +from google_auth_oauthlib.flow import InstalledAppFlow class ReservationExt(commands.Cog): @@ -38,11 +42,27 @@ class Reservation: """ Helps to create and check room reservation """ def __init__(self, bot): self.bot = bot + + self.creds = self.load_credentials() + + self.calendar_service = build('calendar', 'v3', credentials=self.creds) + self.gmail_service = build('gmail', 'v1', credentials=self.creds) + self.reservation_scan.start() def cog_unload(self): self.reservation_scan.cancel() + def load_credentials(self): + scopes = json.loads(self.bot.settings.get('reservation', 'google_api_scopes')) + credential_file_name = self.bot.settings.get('reservation', 'google_credentials') + + print(scopes) + + flow = InstalledAppFlow.from_client_secrets_file(credential_file_name, scopes) + + return flow.run_local_server(port=0) + @tasks.loop(minutes=10.0) async def reservation_scan(self): return diff --git a/settings.ini b/settings.ini index 6ca4322..f78149c 100644 --- a/settings.ini +++ b/settings.ini @@ -5,14 +5,14 @@ [common] database=database.db prefix=! -super_admins=[213262036069515264, - 421068194095300609] +super_admins=[213262036069515264] # If developement_mode is set to 'yes', # every required permission can be bypassed by the following developer ids [debug] -developement_mode=no -developer_ids=[213262036069515264] +developement_mode=yes +developer_ids=[213262036069515264, + 421068194095300609] # List of all the available extensions # if set to false, the extension won't be loaded by default @@ -58,6 +58,13 @@ role_color=0x1f8b4c [iam] list_max_lines=10 +[reservation] +calendar_id=0 +google_credentials=google_credentials.json +google_api_scopes=["https://www.googleapis.com/auth/calendar.calendarlist.readonly", + "https://www.googleapis.com/auth/calendar.events", + "https://www.googleapis.com/auth/gmail.send"] + # Logging # see https://docs.python.org/3/library/logging.config.html for more information # about the following section From d4f9877bd225b1dc9a39c9cc24ba9400aef4f9b4 Mon Sep 17 00:00:00 2001 From: Quentin Mouillade Date: Fri, 3 Dec 2021 16:07:47 +0100 Subject: [PATCH 3/6] Wip reservation list --- .gitignore | 1 + isartbot/ext/reservation.py | 94 ++++++++++++++++++++++++++++++++----- settings.ini | 14 +++++- 3 files changed, 96 insertions(+), 13 deletions(-) diff --git a/.gitignore b/.gitignore index 658e541..165f630 100644 --- a/.gitignore +++ b/.gitignore @@ -26,6 +26,7 @@ MANIFEST token.ini database.db google_credentials.json +google_token.json # PyInstaller # Usually these files are written by a python script from a template diff --git a/isartbot/ext/reservation.py b/isartbot/ext/reservation.py index 19c4a81..5ecb51f 100644 --- a/isartbot/ext/reservation.py +++ b/isartbot/ext/reservation.py @@ -22,24 +22,37 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -import discord +import re +import os import json -import httplib2 +import discord -from datetime import datetime +from datetime import datetime, timedelta from dataclasses import dataclass from discord.ext import tasks, commands +from sqlalchemy.sql.expression import true +from isartbot.helper import Helper from googleapiclient.discovery import build +from google.oauth2.credentials import Credentials from google_auth_oauthlib.flow import InstalledAppFlow +from google.auth.transport.requests import Request class ReservationExt(commands.Cog): + """ Helps to create and check room reservation """ @dataclass class Reservation: date: datetime + name: str + status: str location: str - """ Helps to create and check room reservation """ + def __str__(self) -> str: + date = self.date.strftime("%d/%m %H:%M") + + return f"{date} - {self.name} ({self.location}) - {self.status}" + + def __init__(self, bot): self.bot = bot @@ -48,22 +61,44 @@ def __init__(self, bot): self.calendar_service = build('calendar', 'v3', credentials=self.creds) self.gmail_service = build('gmail', 'v1', credentials=self.creds) + self.event_statuses = self.build_event_statuses() + self.reservation_scan.start() def cog_unload(self): self.reservation_scan.cancel() def load_credentials(self): - scopes = json.loads(self.bot.settings.get('reservation', 'google_api_scopes')) - credential_file_name = self.bot.settings.get('reservation', 'google_credentials') - - print(scopes) + creds = None + google_token_file_name = self.bot.settings.get('reservation', 'google_token') - flow = InstalledAppFlow.from_client_secrets_file(credential_file_name, scopes) + if os.path.exists(google_token_file_name): + creds = Credentials.from_authorized_user_file(google_token_file_name) + + if (not creds or not creds.valid): + if (creds and creds.expired and creds.refresh_token): + creds.refresh(Request()) + else: + scopes = json.loads(self.bot.settings.get('reservation', 'google_api_scopes')) + google_credentials_file_name = self.bot.settings.get('reservation', 'google_credentials') + + flow = InstalledAppFlow.from_client_secrets_file(google_credentials_file_name, scopes) + + creds = flow.run_local_server(port=0) + with open(google_token_file_name, 'w') as token: + token.write(creds.to_json()) + + return creds + + def build_event_statuses(self): + event_statuses = {} + + for (key, icon) in self.bot.settings.items('reservation_icons'): + event_statuses['(' + self.bot.settings.get('reservation', key) + ')'] = icon - return flow.run_local_server(port=0) + return event_statuses - @tasks.loop(minutes=10.0) + @tasks.loop(hours=4 * 24.0) async def reservation_scan(self): return @@ -78,12 +113,47 @@ async def reservation(self, ctx): @reservation.command(help="reservation_list_help", description="reservation_list_description") async def list(self, ctx, page=1): + """" Lists all current reservations on the Google Calendar with their status """ + + if (self.calendar_service == None): + await Helper.send_error(ctx, ctx.channel, 'reservation_list_error') + + now = datetime.utcnow() + + events = self.calendar_service.events().list(calendarId=self.bot.settings.get('reservation', 'calendar_id'), + timeMin=now.isoformat() + 'Z', singleEvents=True, orderBy='startTime').execute() + + event_list = [] + + regex = "([(].*[)]|\b" + self.bot.settings.get('reservation', 'unavailable') + "\b)" + + for event in events['items']: + splitted_summary = re.split(regex, event['summary']) + + name = splitted_summary[0] + date = datetime.strptime(event['start']['dateTime'], '%Y-%m-%dT%H:%M:%S+01:00') + status = splitted_summary[1] if len(splitted_summary) > 1 else "?" + location = event['location'] if 'location' in event else "?" + + event_list.append(self.Reservation( + name = name, + date = date, + status = self.event_statuses[status] if status in self.event_statuses else "?", + location = location + )) + await ctx.send(embed=discord.Embed( - description = "ToDo", + description = '\n'.join([str(event) for event in event_list]), title = await ctx.bot.get_translation(ctx, 'reservation_list_title'), color = discord.Color.green() )) + @reservation.command(help="reservation_notify_help", description="reservation_notify_description") + async def notify(self, ctx): + """" Sends a mail for validation if the last one is old enough. Remember that a mail is already sent regularly. """ + + await Helper.send_success(ctx, ctx.channel, 'reservation_notify_success') + def setup(bot): bot.add_cog(ReservationExt(bot)) \ No newline at end of file diff --git a/settings.ini b/settings.ini index f78149c..cd524ef 100644 --- a/settings.ini +++ b/settings.ini @@ -59,11 +59,23 @@ role_color=0x1f8b4c list_max_lines=10 [reservation] -calendar_id=0 +check_delay=4 +calendar_id=c539454o6gmbh1ibfto0vsa53o@group.calendar.google.com +google_token=google_token.json google_credentials=google_credentials.json google_api_scopes=["https://www.googleapis.com/auth/calendar.calendarlist.readonly", "https://www.googleapis.com/auth/calendar.events", "https://www.googleapis.com/auth/gmail.send"] +unavailable=indispo +refused=refusé +pending=à confirmer +validated=confirmé + +[reservation_icons] +unavailable=:x: +refused=:x: +pending=:hourglass: +validated=:white_check_mark: # Logging # see https://docs.python.org/3/library/logging.config.html for more information From f4d6ea69e1631a4bd7c7ddb5547234f6d1f06fab Mon Sep 17 00:00:00 2001 From: Quentin Mouilade Date: Fri, 3 Dec 2021 20:15:01 +0100 Subject: [PATCH 4/6] Finished reservation list, wip reservation notify --- isartbot/ext/reservation.py | 12 ++++++------ isartbot/languages/english.lang | 11 ++++++++--- isartbot/languages/french.lang | 11 ++++++++--- settings.ini | 4 +--- 4 files changed, 23 insertions(+), 15 deletions(-) diff --git a/isartbot/ext/reservation.py b/isartbot/ext/reservation.py index 5ecb51f..3f93413 100644 --- a/isartbot/ext/reservation.py +++ b/isartbot/ext/reservation.py @@ -112,7 +112,7 @@ async def reservation(self, ctx): await ctx.send_help(ctx.command) @reservation.command(help="reservation_list_help", description="reservation_list_description") - async def list(self, ctx, page=1): + async def list(self, ctx): """" Lists all current reservations on the Google Calendar with their status """ if (self.calendar_service == None): @@ -125,10 +125,8 @@ async def list(self, ctx, page=1): event_list = [] - regex = "([(].*[)]|\b" + self.bot.settings.get('reservation', 'unavailable') + "\b)" - for event in events['items']: - splitted_summary = re.split(regex, event['summary']) + splitted_summary = re.split("([(].*[)])", event['summary']) name = splitted_summary[0] date = datetime.strptime(event['start']['dateTime'], '%Y-%m-%dT%H:%M:%S+01:00') @@ -150,9 +148,11 @@ async def list(self, ctx, page=1): @reservation.command(help="reservation_notify_help", description="reservation_notify_description") async def notify(self, ctx): - """" Sends a mail for validation if the last one is old enough. Remember that a mail is already sent regularly. """ + """" Sends a mail for validation. Remember that a mail is already sent regularly. """ - await Helper.send_success(ctx, ctx.channel, 'reservation_notify_success') + await Helper.ask_confirmation(ctx, ctx.channel, 'reservation_notify_confirmation_title', + initial_content='reservation_notify_confirmation_description', success_content='reservation_notify_success', + failure_content='reservation_notify_aborted') def setup(bot): bot.add_cog(ReservationExt(bot)) diff --git a/isartbot/languages/english.lang b/isartbot/languages/english.lang index c8494eb..4d23398 100644 --- a/isartbot/languages/english.lang +++ b/isartbot/languages/english.lang @@ -299,6 +299,11 @@ foodtruck_list_empty=Such empty ¯\_(ツ)_/¯ reservation_help=ToDo help reservation_description=ToDo description -reservation_list_help=Prints the list of all current reservations and their status -reservation_list_description=Prints the list of all current reservations and their status.\n[page]: Page to be displayed -reservation_list_title=Reservation list \ No newline at end of file +reservation_list_help=Prints the list of all current reservations and their status. +reservation_list_description=Prints the list of all current reservations and their status. +reservation_list_title=Reservation list +reservation_list_error=Couldn't retrieve the list from the Calendar API. Please reload the extension to try fixing the issue. +reservation_notify_confirmation_title=Send a mail for validation +reservation_notify_confirmation_description=Are you sure you want to send a mail to Emilie? +reservation_notify_success=Mail sent to Emilie! +reservation_notify_aborted=Mail not sent to Emilie. \ No newline at end of file diff --git a/isartbot/languages/french.lang b/isartbot/languages/french.lang index 4111184..ebe4394 100644 --- a/isartbot/languages/french.lang +++ b/isartbot/languages/french.lang @@ -298,6 +298,11 @@ foodtruck_list_empty=Tellement vide ¯\_(ツ)_/¯ reservation_help=ToDo aide reservation_description=ToDo description -reservation_list_help=Affiche la liste de toutes les réservations ainsi que leur status -reservation_list_description=Affiche la liste de toutes les réservations ainsi que leur status.\n[page]: Page à afficher -reservation_list_title=Liste des réservations \ No newline at end of file +reservation_list_help=Affiche la liste de toutes les réservations ainsi que leur status. +reservation_list_description=Affiche la liste de toutes les réservations ainsi que leur status. +reservation_list_title=Liste des réservations +reservation_list_error=Impossible de récupérer la liste depuis l'API Calendar. Merci de relancer l'extension pour essayer de corriger le problème. +reservation_notify_confirmation_title=Envoyer un mail pour validation +reservation_notify_confirmation_description=Êtes-vous sûr(e) de vouloir envoyer un mail à Émilie ? +reservation_notify_success=Mail envoyé à Émilie ! +reservation_notify_aborted=Le mail n'a pas été envoyé à Émilie. \ No newline at end of file diff --git a/settings.ini b/settings.ini index cd524ef..6bf9d91 100644 --- a/settings.ini +++ b/settings.ini @@ -60,20 +60,18 @@ list_max_lines=10 [reservation] check_delay=4 -calendar_id=c539454o6gmbh1ibfto0vsa53o@group.calendar.google.com +calendar_id=od3nrhdihlkojgf52fmhi2mi8o@group.calendar.google.com google_token=google_token.json google_credentials=google_credentials.json google_api_scopes=["https://www.googleapis.com/auth/calendar.calendarlist.readonly", "https://www.googleapis.com/auth/calendar.events", "https://www.googleapis.com/auth/gmail.send"] unavailable=indispo -refused=refusé pending=à confirmer validated=confirmé [reservation_icons] unavailable=:x: -refused=:x: pending=:hourglass: validated=:white_check_mark: From d95076bd290a3eaf4ea1fd1823b9e40b37a4f497 Mon Sep 17 00:00:00 2001 From: Quentin Mouilade Date: Sat, 4 Dec 2021 18:24:01 +0100 Subject: [PATCH 5/6] Finished reservation notify, added is_club_manager check --- isartbot/checks/__init__.py | 19 ++--- isartbot/checks/is_club_manager.py | 41 +++++++++++ isartbot/ext/reservation.py | 112 ++++++++++++++++++++++++----- isartbot/languages/english.lang | 12 +++- isartbot/languages/french.lang | 12 +++- mail_template.txt | 6 ++ settings.ini | 18 +++-- 7 files changed, 181 insertions(+), 39 deletions(-) create mode 100644 isartbot/checks/is_club_manager.py create mode 100644 mail_template.txt diff --git a/isartbot/checks/__init__.py b/isartbot/checks/__init__.py index 10a8769..27dcfdb 100644 --- a/isartbot/checks/__init__.py +++ b/isartbot/checks/__init__.py @@ -22,12 +22,13 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -from isartbot.checks.denied import denied -from isartbot.checks.admin import is_admin -from isartbot.checks.verified import is_verified -from isartbot.checks.block_dms import block_dms -from isartbot.checks.moderator import is_moderator -from isartbot.checks.developper import is_developper, developper -from isartbot.checks.super_admin import is_super_admin, super_admin -from isartbot.checks.log_command import log_command -from isartbot.checks.trigger_typing import trigger_typing \ No newline at end of file +from isartbot.checks.denied import denied +from isartbot.checks.admin import is_admin +from isartbot.checks.verified import is_verified +from isartbot.checks.block_dms import block_dms +from isartbot.checks.moderator import is_moderator +from isartbot.checks.developper import is_developper, developper +from isartbot.checks.super_admin import is_super_admin, super_admin +from isartbot.checks.log_command import log_command +from isartbot.checks.trigger_typing import trigger_typing +from isartbot.checks.is_club_manager import is_club_manager, club_manager \ No newline at end of file diff --git a/isartbot/checks/is_club_manager.py b/isartbot/checks/is_club_manager.py new file mode 100644 index 0000000..d0979da --- /dev/null +++ b/isartbot/checks/is_club_manager.py @@ -0,0 +1,41 @@ +# -*- coding: utf-8 -*- + +# MIT License + +# Copyright (c) 2018-2021 Renondedju + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +from isartbot.checks import developper +from isartbot.exceptions import UnauthorizedCommand + +async def is_club_manager(ctx): + value = club_manager(ctx, ctx.author) or\ + ctx.author.permissions_in(ctx.channel).administrator or \ + (ctx.bot.dev_mode and developper(ctx, ctx.author)) + + if (not value): + raise UnauthorizedCommand(missing_status = await ctx.bot.get_translation(ctx, "club_manager_status", force_fetch = True)) + + return True + +def club_manager(ctx, user): + target_role = ctx.guild.get_role(ctx.bot.settings.getint('reservation', 'club_manager_role_id')) + + return target_role in user.roles diff --git a/isartbot/ext/reservation.py b/isartbot/ext/reservation.py index 3f93413..fc9ec7e 100644 --- a/isartbot/ext/reservation.py +++ b/isartbot/ext/reservation.py @@ -25,20 +25,25 @@ import re import os import json +import base64 import discord from datetime import datetime, timedelta from dataclasses import dataclass from discord.ext import tasks, commands -from sqlalchemy.sql.expression import true +from email.mime.text import MIMEText +from isartbot.checks import is_club_manager from isartbot.helper import Helper +from email.mime.multipart import MIMEMultipart from googleapiclient.discovery import build from google.oauth2.credentials import Credentials from google_auth_oauthlib.flow import InstalledAppFlow from google.auth.transport.requests import Request class ReservationExt(commands.Cog): - """ Helps to create and check room reservation """ + """ Assists club managers for room reservation """ + + __slots__ = ("bot", "creds", "calendar_service", "gmail_service", "event_statuses") @dataclass class Reservation: @@ -52,6 +57,10 @@ def __str__(self) -> str: return f"{date} - {self.name} ({self.location}) - {self.status}" + def get_mail_format(self): + date = self.date.strftime("le %d/%m à %Hh%M") + + return f"- {self.name}{date} ({self.location})" def __init__(self, bot): self.bot = bot @@ -63,12 +72,15 @@ def __init__(self, bot): self.event_statuses = self.build_event_statuses() + self.reservation_scan.change_interval(hours=self.bot.settings.getint('reservation', 'mailing_delay') * 24 ) self.reservation_scan.start() def cog_unload(self): self.reservation_scan.cancel() def load_credentials(self): + """" Loads Google credentials and writes them in a file for future loadings """ + creds = None google_token_file_name = self.bot.settings.get('reservation', 'google_token') @@ -91,16 +103,44 @@ def load_credentials(self): return creds def build_event_statuses(self): + """" Associates a reservation status with an icon """ + event_statuses = {} for (key, icon) in self.bot.settings.items('reservation_icons'): - event_statuses['(' + self.bot.settings.get('reservation', key) + ')'] = icon + event_statuses[self.bot.settings.get('reservation', key)] = icon return event_statuses @tasks.loop(hours=4 * 24.0) async def reservation_scan(self): - return + """ Sends a mail containing all pending reservations """ + + if self.gmail_service == None: + return + + event_list = await self.get_events_from_calendar() + pending_status = self.bot.settings.get('reservation_icons', 'pending') + reservation_lines = '\n'.join([event.get_mail_format() for event in event_list if event.status == pending_status]) + + mail_template_path = self.bot.settings.get('reservation', 'mail_template') + + if os.path.exists(mail_template_path): + with open(mail_template_path, encoding='utf-8') as mail_template_file: + mail_content = mail_template_file.read().format(reservation_lines) + + mime_message = MIMEMultipart() + mime_message['subject'] = self.bot.settings.get('reservation', 'mail_title') + mime_message['to'] = self.bot.settings.get('reservation', 'destination_mail') + mime_message.attach(MIMEText(mail_content, 'plain')) + + message = {'raw': base64.urlsafe_b64encode(mime_message.as_bytes()).decode(encoding='utf-8')} + + try: + self.gmail_service.users().messages().send(userId=self.bot.settings.get('reservation', 'sender_mail'), + body=message).execute() + except: + self.bot.logger.info("Failed to send the mail for validation") @reservation_scan.before_loop async def pre_reservation_scan(self): @@ -108,16 +148,66 @@ async def pre_reservation_scan(self): @commands.group(invoke_without_command=True, pass_context=True, help="reservation_help", description="reservation_description") + @commands.check(is_club_manager) async def reservation(self, ctx): await ctx.send_help(ctx.command) @reservation.command(help="reservation_list_help", description="reservation_list_description") + @commands.check(is_club_manager) async def list(self, ctx): """" Lists all current reservations on the Google Calendar with their status """ if (self.calendar_service == None): await Helper.send_error(ctx, ctx.channel, 'reservation_list_error') + event_list = await self.get_events_from_calendar() + + await ctx.send(embed=discord.Embed( + description = '\n'.join([str(event) for event in event_list]), + title = await ctx.bot.get_translation(ctx, 'reservation_list_title'), + color = discord.Color.green() + )) + + @reservation.command(help="reservation_notify_help", description="reservation_notify_description") + @commands.check(is_club_manager) + async def notify(self, ctx): + """" Sends a mail containing all pending reservations. Remember that a mail is already sent regularly. """ + + if self.gmail_service == None: + await Helper.send_error(ctx, ctx.channel, 'reservation_notify_error') + + await Helper.ask_confirmation(ctx, ctx.channel, 'reservation_notify_confirmation_title', + initial_content='reservation_notify_confirmation_description', success_content='reservation_notify_success', + failure_content='reservation_notify_aborted') + + event_list = await self.get_events_from_calendar() + pending_status = self.bot.settings.get('reservation_icons', 'pending') + reservation_lines = '\n'.join([event.get_mail_format() for event in event_list if event.status == pending_status]) + + mail_template_path = self.bot.settings.get('reservation', 'mail_template') + + if os.path.exists(mail_template_path): + with open(mail_template_path, encoding='utf-8') as mail_template_file: + mail_content = mail_template_file.read().format(reservation_lines) + + mime_message = MIMEMultipart() + mime_message['subject'] = self.bot.settings.get('reservation', 'mail_title') + mime_message['to'] = self.bot.settings.get('reservation', 'destination_mail') + mime_message.attach(MIMEText(mail_content, 'plain')) + + message = {'raw': base64.urlsafe_b64encode(mime_message.as_bytes()).decode(encoding='utf-8')} + + try: + self.gmail_service.users().messages().send(userId=self.bot.settings.get('reservation', 'sender_mail'), + body=message).execute() + except: + await Helper.send_error(ctx, ctx.channel, 'reservation_notify_send_error') + else: + await Helper.send_error(ctx, ctx.channel, 'reservation_notify_template_error') + + async def get_events_from_calendar(self): + """ Returns all current reservations """ + now = datetime.utcnow() events = self.calendar_service.events().list(calendarId=self.bot.settings.get('reservation', 'calendar_id'), @@ -140,19 +230,7 @@ async def list(self, ctx): location = location )) - await ctx.send(embed=discord.Embed( - description = '\n'.join([str(event) for event in event_list]), - title = await ctx.bot.get_translation(ctx, 'reservation_list_title'), - color = discord.Color.green() - )) - - @reservation.command(help="reservation_notify_help", description="reservation_notify_description") - async def notify(self, ctx): - """" Sends a mail for validation. Remember that a mail is already sent regularly. """ - - await Helper.ask_confirmation(ctx, ctx.channel, 'reservation_notify_confirmation_title', - initial_content='reservation_notify_confirmation_description', success_content='reservation_notify_success', - failure_content='reservation_notify_aborted') + return event_list def setup(bot): bot.add_cog(ReservationExt(bot)) diff --git a/isartbot/languages/english.lang b/isartbot/languages/english.lang index 4d23398..299fee4 100644 --- a/isartbot/languages/english.lang +++ b/isartbot/languages/english.lang @@ -22,6 +22,7 @@ super_admin_status=super administrator developper_status=developer admin_status=administrator denied_status=god +club_manager_status=club manager command_not_found=This command does not exist. no_subcommand=This command has no subcommand. @@ -297,13 +298,18 @@ foodtruck_list_empty=Such empty ¯\_(ツ)_/¯ ## Reservation -reservation_help=ToDo help -reservation_description=ToDo description +reservation_help=Assists club managers for room reservation. +reservation_description=Assists club managers for room reservation. reservation_list_help=Prints the list of all current reservations and their status. reservation_list_description=Prints the list of all current reservations and their status. reservation_list_title=Reservation list reservation_list_error=Couldn't retrieve the list from the Calendar API. Please reload the extension to try fixing the issue. +reservation_notify_help=Sends a mail containing all pending reservation. +reservation_notify_description=Sends a mail containing all pending reservation. Please use it in absolute necessity since a mail is already sent regularly. reservation_notify_confirmation_title=Send a mail for validation reservation_notify_confirmation_description=Are you sure you want to send a mail to Emilie? reservation_notify_success=Mail sent to Emilie! -reservation_notify_aborted=Mail not sent to Emilie. \ No newline at end of file +reservation_notify_aborted=Mail not sent to Emilie. +reservation_notify_error=Unable to access the Gmail API. Please reload the extension to try fixing the issue. +reservation_notify_send_error=Unable to send the mail. Please reload the extension to try fixing the issue. +reservation_notify_template_error=Unable to send the mail: the template is not found. \ No newline at end of file diff --git a/isartbot/languages/french.lang b/isartbot/languages/french.lang index ebe4394..5e23f9a 100644 --- a/isartbot/languages/french.lang +++ b/isartbot/languages/french.lang @@ -22,6 +22,7 @@ super_admin_status=super administrateur developper_status=developpeur admin_status=administrateur denied_status=dieu +club_manager_status=responsable club command_not_found=Cette commande n'existe pas. no_subcommand=Cette commande n'a pas de sous-commande. @@ -296,13 +297,18 @@ foodtruck_list_empty=Tellement vide ¯\_(ツ)_/¯ ## Reservation -reservation_help=ToDo aide -reservation_description=ToDo description +reservation_help=Assiste les responsables de club dans les réservations de salles. +reservation_description=Assiste les responsables de club dans les réservations de salles. reservation_list_help=Affiche la liste de toutes les réservations ainsi que leur status. reservation_list_description=Affiche la liste de toutes les réservations ainsi que leur status. reservation_list_title=Liste des réservations reservation_list_error=Impossible de récupérer la liste depuis l'API Calendar. Merci de relancer l'extension pour essayer de corriger le problème. +reservation_notify_help=Envoie un mail contant les réservations à confirmer. +reservation_notify_description=Envoie un mail contant les réservations à confirmer. Merci de n'utiliser cette commande qu'en cas de nécéssité puisqu'un mail est déjà envoyé régulièrement. reservation_notify_confirmation_title=Envoyer un mail pour validation reservation_notify_confirmation_description=Êtes-vous sûr(e) de vouloir envoyer un mail à Émilie ? reservation_notify_success=Mail envoyé à Émilie ! -reservation_notify_aborted=Le mail n'a pas été envoyé à Émilie. \ No newline at end of file +reservation_notify_aborted=Le mail n'a pas été envoyé à Émilie. +reservation_notify_error=Impossible d'accéder à l'API Gmail. Merci de relancer l'extension pour essayer de corriger le problème. +reservation_notify_send_error=Impossible d'envoyer le mail. Merci de relancer l'extension pour essayer de corriger le problème. +reservation_notify_template_error=Impossible d'envoyer le mail : le template est introuvable. \ No newline at end of file diff --git a/mail_template.txt b/mail_template.txt new file mode 100644 index 0000000..0516050 --- /dev/null +++ b/mail_template.txt @@ -0,0 +1,6 @@ +Hello Émilie 👋 + +Il y a des réservations non-confirmées sur le calendier: +{} + +Bonne journée ! \ No newline at end of file diff --git a/settings.ini b/settings.ini index 6bf9d91..9871062 100644 --- a/settings.ini +++ b/settings.ini @@ -10,9 +10,8 @@ super_admins=[213262036069515264] # If developement_mode is set to 'yes', # every required permission can be bypassed by the following developer ids [debug] -developement_mode=yes -developer_ids=[213262036069515264, - 421068194095300609] +developement_mode=no +developer_ids=[213262036069515264] # List of all the available extensions # if set to false, the extension won't be loaded by default @@ -59,16 +58,21 @@ role_color=0x1f8b4c list_max_lines=10 [reservation] -check_delay=4 +mailing_delay=4 +club_manager_role_id=916728222362787890 +mail_title=[Clubs] Réservation de salles/meet-up +mail_template=mail_template.txt +sender_mail=mouillade.quentin@gmail.com +destination_mail=mouillade.quentin@gmail.com +unavailable=(indispo) +pending=(à confirmer) +validated=(confirmé) calendar_id=od3nrhdihlkojgf52fmhi2mi8o@group.calendar.google.com google_token=google_token.json google_credentials=google_credentials.json google_api_scopes=["https://www.googleapis.com/auth/calendar.calendarlist.readonly", "https://www.googleapis.com/auth/calendar.events", "https://www.googleapis.com/auth/gmail.send"] -unavailable=indispo -pending=à confirmer -validated=confirmé [reservation_icons] unavailable=:x: From 4e2f59e762994f136b865b6b1d8cc04d0a6354d6 Mon Sep 17 00:00:00 2001 From: Quentin Mouilade Date: Sat, 4 Dec 2021 18:32:59 +0100 Subject: [PATCH 6/6] Cleaning --- isartbot/ext/reservation.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/isartbot/ext/reservation.py b/isartbot/ext/reservation.py index fc9ec7e..918b70b 100644 --- a/isartbot/ext/reservation.py +++ b/isartbot/ext/reservation.py @@ -140,7 +140,9 @@ async def reservation_scan(self): self.gmail_service.users().messages().send(userId=self.bot.settings.get('reservation', 'sender_mail'), body=message).execute() except: - self.bot.logger.info("Failed to send the mail for validation") + self.bot.logger.error("Failed to send the mail for validation") + else: + self.bot.logger.error("Failed to send the mail for validation : template is not found") @reservation_scan.before_loop async def pre_reservation_scan(self):