feat: ntfy push notification support per backup profile
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Joomla: Extension CI / Release Readiness Check (pull_request) Failing after 6s
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 10s
Universal: Auto Version Bump / Version Bump (push) Successful in 15s
Generic: Repo Health / Access control (pull_request) Successful in 2s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 10s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 14s
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
Joomla: Extension CI / Tests (PHP 8.2) (pull_request) Has been cancelled
Joomla: Extension CI / Tests (PHP 8.3) (pull_request) Has been cancelled
Joomla: Extension CI / PHPStan Analysis (pull_request) Has been cancelled
Generic: Repo Health / Scripts governance (pull_request) Has been cancelled
Generic: Repo Health / Repository health (pull_request) Has been cancelled
Generic: Repo Health / Report Issues (pull_request) Has been cancelled

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
This commit is contained in:
Jonathan Miller
2026-06-15 04:32:50 -05:00
parent 77667d436a
commit 5f04332fc5
5 changed files with 137 additions and 0 deletions
@@ -215,6 +215,37 @@
<option value="1">JYES</option>
<option value="0">JNO</option>
</field>
<field
name="ntfy_spacer"
type="note"
label=""
description="COM_MOKOJOOMBACKUP_FIELD_NTFY_SPACER_DESC"
class="alert alert-light border"
/>
<field
name="ntfy_topic"
type="text"
label="COM_MOKOJOOMBACKUP_FIELD_NTFY_TOPIC"
description="COM_MOKOJOOMBACKUP_FIELD_NTFY_TOPIC_DESC"
maxlength="255"
hint="my-backups"
/>
<field
name="ntfy_server"
type="url"
label="COM_MOKOJOOMBACKUP_FIELD_NTFY_SERVER"
description="COM_MOKOJOOMBACKUP_FIELD_NTFY_SERVER_DESC"
maxlength="512"
default="https://ntfy.sh"
hint="https://ntfy.sh"
/>
<field
name="ntfy_token"
type="password"
label="COM_MOKOJOOMBACKUP_FIELD_NTFY_TOKEN"
description="COM_MOKOJOOMBACKUP_FIELD_NTFY_TOKEN_DESC"
maxlength="255"
/>
</fieldset>
<fieldset name="ftp" label="COM_MOKOJOOMBACKUP_FIELDSET_FTP">
@@ -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="<strong>Push Notifications (ntfy)</strong> — Send instant push notifications to your phone or desktop via <a href='https://ntfy.sh' target='_blank'>ntfy.sh</a> 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"
@@ -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',
@@ -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`;
@@ -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.
*