From 9b69d147a9d7b162b2d1b34d24942466f34c2b2c Mon Sep 17 00:00:00 2001 From: khanxmetu Date: Fri, 20 Sep 2024 21:15:39 +0300 Subject: [PATCH] Added ntfy notification service --- moodle_dl/cli/notifications_wizard.py | 42 +++++++++++ moodle_dl/main.py | 11 +++ moodle_dl/notifications/__init__.py | 3 +- moodle_dl/notifications/ntfy/__init__.py | 0 .../notifications/ntfy/ntfy_formatter.py | 61 ++++++++++++++++ moodle_dl/notifications/ntfy/ntfy_service.py | 69 +++++++++++++++++++ moodle_dl/notifications/ntfy/ntfy_shooter.py | 20 ++++++ moodle_dl/types.py | 1 + 8 files changed, 206 insertions(+), 1 deletion(-) create mode 100644 moodle_dl/notifications/ntfy/__init__.py create mode 100644 moodle_dl/notifications/ntfy/ntfy_formatter.py create mode 100644 moodle_dl/notifications/ntfy/ntfy_service.py create mode 100644 moodle_dl/notifications/ntfy/ntfy_shooter.py diff --git a/moodle_dl/cli/notifications_wizard.py b/moodle_dl/cli/notifications_wizard.py index 35236533..6c39c90e 100644 --- a/moodle_dl/cli/notifications_wizard.py +++ b/moodle_dl/cli/notifications_wizard.py @@ -4,6 +4,7 @@ from moodle_dl.notifications.discord.discord_shooter import DiscordShooter from moodle_dl.notifications.mail.mail_formater import create_full_welcome_mail from moodle_dl.notifications.mail.mail_shooter import MailShooter +from moodle_dl.notifications.ntfy.ntfy_shooter import NtfyShooter from moodle_dl.notifications.telegram.telegram_shooter import ( RequestRejectedError, TelegramShooter, @@ -170,6 +171,47 @@ def interactively_configure_discord(self) -> None: self.config.set_property('discord', discord_cfg) + def interactively_configure_ntfy(self) -> None: + "Guides the user through the configuration of the ntfy notification." + + do_ntfy = Cutie.prompt_yes_or_no('Do you want to activate Notifications via ntfy?') + + if not do_ntfy: + self.config.remove_property('ntfy') + else: + print('[The following Inputs are not validated!]') + config_valid = False + while not config_valid: + topic = input('ntfy topic: ') + do_ntfy_server = Cutie.prompt_yes_or_no('Do you want to set a custom ntfy server?') + server = None + if do_ntfy_server: + server = input('ntfy server: ') + + print('Testing server-Config...') + + try: + ntfy_shooter = NtfyShooter(topic=topic, server=server) + ntfy_shooter.send(title='', message='This is a test message from moodle-dl!') + + except (ConnectionError, RuntimeError) as e: + print(f'Error while sending the test message: {str(e)}') + continue + + else: + input( + 'Please check if you received the Testmessage.' + + ' If yes, confirm with Return.\nIf not, exit' + + ' this program ([CTRL]+[C]) and try again later.' + ) + config_valid = True + + ntfy_cfg = {'topic': topic} + if server: + ntfy_cfg['server'] = server + + self.config.set_property('ntfy', ntfy_cfg) + def interactively_configure_xmpp(self) -> None: "Guides the user through the configuration of the xmpp notification." diff --git a/moodle_dl/main.py b/moodle_dl/main.py index 77d3ba13..b87823ef 100755 --- a/moodle_dl/main.py +++ b/moodle_dl/main.py @@ -56,6 +56,8 @@ def choose_task(config: ConfigHelper, opts: MoodleDlOpts): NotificationsWizard(config, opts).interactively_configure_telegram() elif opts.change_notification_discord: NotificationsWizard(config, opts).interactively_configure_discord() + elif opts.change_notification_ntfy: + NotificationsWizard(config, opts).interactively_configure_ntfy() elif opts.change_notification_xmpp: NotificationsWizard(config, opts).interactively_configure_xmpp() elif opts.config: @@ -266,6 +268,15 @@ def _dir_path(path): help=('Activate / deactivate / change the settings for receiving notifications via Discord.'), ) + group.add_argument( + '-cn', + '--change-notification-ntfy', + dest='change_notification_ntfy', + default=False, + action='store_true', + help=('Activate / deactivate / change the settings for receiving notifications via ntfy.'), + ) + group.add_argument( '-cx', '--change-notification-xmpp', diff --git a/moodle_dl/notifications/__init__.py b/moodle_dl/notifications/__init__.py index ae8874c1..83aba9c0 100644 --- a/moodle_dl/notifications/__init__.py +++ b/moodle_dl/notifications/__init__.py @@ -5,10 +5,11 @@ from moodle_dl.notifications.discord.discord_service import DiscordService from moodle_dl.notifications.mail.mail_service import MailService from moodle_dl.notifications.notification_service import NotificationService +from moodle_dl.notifications.ntfy.ntfy_service import NtfyService from moodle_dl.notifications.telegram.telegram_service import TelegramService from moodle_dl.notifications.xmpp.xmpp_service import XmppService -__all__ = ['ConsoleService', 'MailService', 'TelegramService', 'DiscordService', 'XmppService'] +__all__ = ['ConsoleService', 'MailService', 'TelegramService', 'DiscordService', 'NtfyService', 'XmppService'] REMOTE_SERVICES = [ Class diff --git a/moodle_dl/notifications/ntfy/__init__.py b/moodle_dl/notifications/ntfy/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/moodle_dl/notifications/ntfy/ntfy_formatter.py b/moodle_dl/notifications/ntfy/ntfy_formatter.py new file mode 100644 index 00000000..a4dbe5b9 --- /dev/null +++ b/moodle_dl/notifications/ntfy/ntfy_formatter.py @@ -0,0 +1,61 @@ +from typing import Optional, TypedDict + +from moodle_dl.types import Course, File + + +class NtfyMessage(TypedDict): + title: str + message: str + source_url: Optional[str] + + +def _get_change_type(file: File) -> str: + if file.modified: + return "modified" + elif file.deleted: + return "deleted" + elif file.moved: + return "moved" + else: + return "new" + + +def create_full_moodle_diff_messages(changes: list[Course]) -> list[NtfyMessage]: + messages = [] + for course in changes: + files_generic_msg = NtfyMessage() + misc_generic_msg = NtfyMessage() + files_generic_msg["message"] = f"{course.fullname}\n" + misc_generic_msg["message"] = f"{course.fullname}\n" + files_generic_cnt = 0 + misc_generic_cnt = 0 + for file in course.files: + change_type = _get_change_type(file) + if file.content_type == "description": + msg = NtfyMessage( + title=" ".join(file.content_filepath.split()[1:]), + message=f"{course.fullname}\n{file.content_filename}", + source_url=file.content_fileurl, + ) + if change_type != "new": + msg["message"] += f"\nMessage {change_type}" + messages.append(msg) + elif file.content_type in ("file", "assignfile"): + msg_str = f"* {file.content_filename}" + if file.content_type == "assignfile": + msg_str += " | Assignment File" + if change_type != "new": + msg_str += f" | File {change_type}" + msg_str += "\n" + files_generic_msg["message"] += msg_str + files_generic_cnt += 1 + else: + misc_generic_msg["message"] += f"* {file.content_filename}\n" + misc_generic_cnt += 1 + files_generic_msg["title"] = f"{files_generic_cnt} File Changes" + misc_generic_msg["title"] = f"{misc_generic_cnt} Misc Changes" + if files_generic_cnt: + messages.append(files_generic_msg) + if misc_generic_cnt: + messages.append(misc_generic_msg) + return messages diff --git a/moodle_dl/notifications/ntfy/ntfy_service.py b/moodle_dl/notifications/ntfy/ntfy_service.py new file mode 100644 index 00000000..1db0d199 --- /dev/null +++ b/moodle_dl/notifications/ntfy/ntfy_service.py @@ -0,0 +1,69 @@ +import logging +import traceback +from typing import List + +import moodle_dl.notifications.ntfy.ntfy_formatter as NF +from moodle_dl.downloader.task import Task +from moodle_dl.notifications.notification_service import NotificationService +from moodle_dl.notifications.ntfy.ntfy_shooter import NtfyShooter +from moodle_dl.types import Course + + +class NtfyService(NotificationService): + def _is_configured(self) -> bool: + # Checks if the sending of ntfy messages has been configured. + try: + self.config.get_property("ntfy") + return True + except ValueError: + logging.debug("ntfy-Notifications not configured, skipping.") + return False + + def _send_messages(self, messages: List[str]): + """ + Sends an message + """ + if not self._is_configured() or messages is None or len(messages) == 0: + return + + ntfy_cfg = self.config.get_property("ntfy") + + logging.info("Sending Notification via ntfy...") + ntfy_shooter = NtfyShooter(ntfy_cfg["topic"], ntfy_cfg.get("server")) + + for message in messages: + try: + ntfy_shooter.send(**message) + except BaseException as e: + logging.error( + "While sending notification:\n%s", + traceback.format_exc(), + extra={"exception": e}, + ) + raise e # to be properly notified via Sentry + + def notify_about_changes_in_moodle(self, changes: List[Course]) -> None: + """ + Sends out a notification about the downloaded changes. + @param changes: A list of changed courses with changed files. + """ + if not self._is_configured(): + return + + messages = NF.create_full_moodle_diff_messages(changes) + + self._send_messages(messages) + + def notify_about_error(self, error_description: str): + """ + Sends out an error message if configured to do so. + @param error_description: The error object. + """ + pass + + def notify_about_failed_downloads(self, failed_downloads: List[Task]): + """ + Sends out an message about failed download if configured to send out error messages. + @param failed_downloads: A list of failed Tasks. + """ + pass diff --git a/moodle_dl/notifications/ntfy/ntfy_shooter.py b/moodle_dl/notifications/ntfy/ntfy_shooter.py new file mode 100644 index 00000000..da6227d3 --- /dev/null +++ b/moodle_dl/notifications/ntfy/ntfy_shooter.py @@ -0,0 +1,20 @@ +import json +from typing import Optional + +import requests + + +class NtfyShooter: + def __init__(self, topic: str, server: Optional[str] = None): + self.topic = topic + self.server = server or "https://ntfy.sh/" + + def send(self, title: str, message: str, source_url: Optional[str] = None): + data = {"topic": self.topic, "title": title, "message": message} + if source_url: + data["click"] = source_url + view_action = {"action": "view", "label": "View", "url": source_url} + data.setdefault("actions", []).append(view_action) + + resp = requests.post(self.server, data=json.dumps(data)) + resp.raise_for_status() diff --git a/moodle_dl/types.py b/moodle_dl/types.py index 0f44d4dc..7b88620d 100644 --- a/moodle_dl/types.py +++ b/moodle_dl/types.py @@ -254,6 +254,7 @@ class MoodleDlOpts: change_notification_mail: bool change_notification_telegram: bool change_notification_discord: bool + change_notification_ntfy: bool change_notification_xmpp: bool manage_database: bool delete_old_files: bool