From 5f04332fc5748340a14b573dde6f0580a2e4d473 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Mon, 15 Jun 2026 04:32:50 -0500 Subject: [PATCH] feat: ntfy push notification support per backup profile Add ntfy (https://ntfy.sh) push notifications alongside email. Each backup profile can configure its own ntfy topic, server, and access token independently. - New profile fields: ntfy_topic, ntfy_server (default ntfy.sh), ntfy_token (optional, for private topics) - NotificationSender sends both email and ntfy in parallel - Uses priority 5 (urgent) for failures, 3 (default) for success - Includes backup status emoji, profile name, type, archive, size - 10-second timeout to prevent blocking backup completion - SQL migration 01.18.00 adds columns to profiles table --- .../com_mokosuitebackup/forms/profile.xml | 31 +++++++ .../language/en-GB/com_mokosuitebackup.ini | 7 ++ .../com_mokosuitebackup/sql/install.mysql.sql | 3 + .../sql/updates/mysql/01.18.00.sql | 5 + .../src/Engine/NotificationSender.php | 91 +++++++++++++++++++ 5 files changed, 137 insertions(+) create mode 100644 source/packages/com_mokosuitebackup/sql/updates/mysql/01.18.00.sql diff --git a/source/packages/com_mokosuitebackup/forms/profile.xml b/source/packages/com_mokosuitebackup/forms/profile.xml index 1944d47..571dbc4 100644 --- a/source/packages/com_mokosuitebackup/forms/profile.xml +++ b/source/packages/com_mokosuitebackup/forms/profile.xml @@ -215,6 +215,37 @@ + + + +
diff --git a/source/packages/com_mokosuitebackup/language/en-GB/com_mokosuitebackup.ini b/source/packages/com_mokosuitebackup/language/en-GB/com_mokosuitebackup.ini index ff4b8bd..baf09de 100644 --- a/source/packages/com_mokosuitebackup/language/en-GB/com_mokosuitebackup.ini +++ b/source/packages/com_mokosuitebackup/language/en-GB/com_mokosuitebackup.ini @@ -197,6 +197,13 @@ COM_MOKOJOOMBACKUP_FIELD_NOTIFY_SUCCESS="Notify on Success" COM_MOKOJOOMBACKUP_FIELD_NOTIFY_SUCCESS_DESC="Send an email when a backup completes successfully." COM_MOKOJOOMBACKUP_FIELD_NOTIFY_FAILURE="Notify on Failure" COM_MOKOJOOMBACKUP_FIELD_NOTIFY_FAILURE_DESC="Send an email when a backup fails. Includes log excerpt for debugging." +COM_MOKOJOOMBACKUP_FIELD_NTFY_SPACER_DESC="Push Notifications (ntfy) — Send instant push notifications to your phone or desktop via ntfy.sh or a self-hosted ntfy server." +COM_MOKOJOOMBACKUP_FIELD_NTFY_TOPIC="ntfy Topic" +COM_MOKOJOOMBACKUP_FIELD_NTFY_TOPIC_DESC="The ntfy topic to publish notifications to. Leave blank to disable push notifications." +COM_MOKOJOOMBACKUP_FIELD_NTFY_SERVER="ntfy Server" +COM_MOKOJOOMBACKUP_FIELD_NTFY_SERVER_DESC="URL of the ntfy server. Default is the public ntfy.sh service. Use your own server URL for self-hosted instances." +COM_MOKOJOOMBACKUP_FIELD_NTFY_TOKEN="Access Token" +COM_MOKOJOOMBACKUP_FIELD_NTFY_TOKEN_DESC="Optional access token for private ntfy topics. Leave blank for public topics." ; Integrity verification COM_MOKOJOOMBACKUP_TOOLBAR_VERIFY="Verify Integrity" diff --git a/source/packages/com_mokosuitebackup/sql/install.mysql.sql b/source/packages/com_mokosuitebackup/sql/install.mysql.sql index a3ae77d..8929203 100644 --- a/source/packages/com_mokosuitebackup/sql/install.mysql.sql +++ b/source/packages/com_mokosuitebackup/sql/install.mysql.sql @@ -36,6 +36,9 @@ CREATE TABLE IF NOT EXISTS `#__mokosuitebackup_profiles` ( `notify_user_groups` VARCHAR(255) NOT NULL DEFAULT '' COMMENT 'Comma-separated Joomla user group IDs', `notify_on_success` TINYINT(1) NOT NULL DEFAULT 0, `notify_on_failure` TINYINT(1) NOT NULL DEFAULT 1, + `ntfy_topic` VARCHAR(255) NOT NULL DEFAULT '' COMMENT 'ntfy topic name', + `ntfy_server` VARCHAR(512) NOT NULL DEFAULT 'https://ntfy.sh' COMMENT 'ntfy server URL', + `ntfy_token` VARCHAR(255) NOT NULL DEFAULT '' COMMENT 'ntfy access token (optional)', `published` TINYINT(1) NOT NULL DEFAULT 1, `ordering` INT(11) NOT NULL DEFAULT 0, `created` DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00', diff --git a/source/packages/com_mokosuitebackup/sql/updates/mysql/01.18.00.sql b/source/packages/com_mokosuitebackup/sql/updates/mysql/01.18.00.sql new file mode 100644 index 0000000..ef693b2 --- /dev/null +++ b/source/packages/com_mokosuitebackup/sql/updates/mysql/01.18.00.sql @@ -0,0 +1,5 @@ +-- Add ntfy push notification fields to backup profiles +ALTER TABLE `#__mokosuitebackup_profiles` + ADD COLUMN `ntfy_topic` VARCHAR(255) NOT NULL DEFAULT '' COMMENT 'ntfy topic name' AFTER `notify_on_failure`, + ADD COLUMN `ntfy_server` VARCHAR(512) NOT NULL DEFAULT 'https://ntfy.sh' COMMENT 'ntfy server URL' AFTER `ntfy_topic`, + ADD COLUMN `ntfy_token` VARCHAR(255) NOT NULL DEFAULT '' COMMENT 'ntfy access token (optional, for private topics)' AFTER `ntfy_server`; diff --git a/source/packages/com_mokosuitebackup/src/Engine/NotificationSender.php b/source/packages/com_mokosuitebackup/src/Engine/NotificationSender.php index bc48555..26467af 100644 --- a/source/packages/com_mokosuitebackup/src/Engine/NotificationSender.php +++ b/source/packages/com_mokosuitebackup/src/Engine/NotificationSender.php @@ -32,6 +32,14 @@ class NotificationSender * @return bool True if email was sent */ public static function send(object $profile, object $record, bool $success, string $logText = ''): bool + { + $emailSent = self::sendEmail($profile, $record, $success, $logText); + $ntfySent = self::sendNtfy($profile, $record, $success); + + return $emailSent || $ntfySent; + } + + private static function sendEmail(object $profile, object $record, bool $success, string $logText = ''): bool { $notifyEmail = trim($profile->notify_email ?? ''); $notifyUserGroups = $profile->notify_user_groups ?? ''; @@ -139,6 +147,89 @@ class NotificationSender } } + /** + * Send a push notification via ntfy. + */ + private static function sendNtfy(object $profile, object $record, bool $success): bool + { + $topic = trim($profile->ntfy_topic ?? ''); + $server = trim($profile->ntfy_server ?? 'https://ntfy.sh'); + $token = trim($profile->ntfy_token ?? ''); + + if ($topic === '') { + return false; + } + + // Respect the same success/failure preferences as email + if ($success && empty($profile->notify_on_success)) { + return false; + } + + if (!$success && empty($profile->notify_on_failure)) { + return false; + } + + try { + $config = Factory::getApplication()->getConfig(); + $siteName = $config->get('sitename', 'Joomla Site'); + + $statusLabel = $success ? 'SUCCESS' : 'FAILED'; + $statusEmoji = $success ? "\xE2\x9C\x85" : "\xE2\x9D\x8C"; + + $sizeHuman = $record->total_size > 0 + ? number_format($record->total_size / 1048576, 2) . ' MB' + : 'N/A'; + + $title = "{$statusEmoji} Backup {$statusLabel}: {$siteName}"; + $body = "Profile: {$profile->title}\n" + . "Type: {$record->backup_type}\n" + . "Archive: {$record->archivename}\n" + . "Size: {$sizeHuman}"; + + $url = rtrim($server, '/') . '/' . rawurlencode($topic); + + $headers = [ + 'Title: ' . $title, + 'Priority: ' . ($success ? '3' : '5'), + 'Tags: ' . ($success ? 'white_check_mark' : 'rotating_light'), + ]; + + if ($token !== '') { + $headers[] = 'Authorization: Bearer ' . $token; + } + + $ch = curl_init($url); + curl_setopt_array($ch, [ + CURLOPT_POST => true, + CURLOPT_POSTFIELDS => $body, + CURLOPT_HTTPHEADER => $headers, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_TIMEOUT => 10, + CURLOPT_CONNECTTIMEOUT => 5, + ]); + + $response = curl_exec($ch); + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + $error = curl_error($ch); + curl_close($ch); + + if ($error !== '') { + error_log('MokoSuiteBackup: ntfy error: ' . $error); + return false; + } + + if ($httpCode < 200 || $httpCode >= 300) { + error_log('MokoSuiteBackup: ntfy returned HTTP ' . $httpCode . ': ' . $response); + return false; + } + + return true; + } catch (\Throwable $e) { + error_log('MokoSuiteBackup: ntfy notification error: ' . $e->getMessage()); + return false; + } + } + /** * Resolve user group IDs to email addresses of group members. *