diff --git a/endpoints/cronjobs/sendnotifications.php b/endpoints/cronjobs/sendnotifications.php index 44c21186b..ad6d09d19 100644 --- a/endpoints/cronjobs/sendnotifications.php +++ b/endpoints/cronjobs/sendnotifications.php @@ -25,6 +25,7 @@ $webhookNotificationsEnabled = false; $pushoverNotificationsEnabled = false; $discordNotificationsEnabled = false; + $ntfyNotificationsEnabled = false; // Get notification settings (how many days before the subscription ends should the notification be sent) $query = "SELECT days FROM notification_settings WHERE user_id = :userId"; @@ -102,6 +103,19 @@ $pushover['token'] = $row["token"]; } + // Check if Nrfy notifications are enabled and get the settings + $query = "SELECT * FROM ntfy_notifications WHERE user_id = :userId"; + $stmt = $db->prepare($query); + $stmt->bindValue(':userId', $userId, SQLITE3_INTEGER); + $result = $stmt->execute(); + + if ($row = $result->fetchArray(SQLITE3_ASSOC)) { + $ntfyNotificationsEnabled = $row['enabled']; + $ntfy['host'] = $row["host"]; + $ntfy['topic'] = $row["topic"]; + $ntfy['headers'] = $row["headers"]; + } + // Check if Webhook notifications are enabled and get the settings $query = "SELECT * FROM webhook_notifications WHERE user_id = :userId"; $stmt = $db->prepare($query); @@ -120,7 +134,9 @@ } } - $notificationsEnabled = $emailNotificationsEnabled || $gotifyNotificationsEnabled || $telegramNotificationsEnabled || $webhookNotificationsEnabled || $pushoverNotificationsEnabled || $discordNotificationsEnabled; + $notificationsEnabled = $emailNotificationsEnabled || $gotifyNotificationsEnabled || $telegramNotificationsEnabled || + $webhookNotificationsEnabled || $pushoverNotificationsEnabled || $discordNotificationsEnabled || + $ntfyNotificationsEnabled; // If no notifications are enabled, no need to run if (!$notificationsEnabled) { @@ -429,6 +445,50 @@ } } + // Ntfy notifications if enabled + if ($ntfyNotificationsEnabled) { + foreach ($notify as $userId => $perUser) { + // Get name of user from household table + $stmt = $db->prepare('SELECT * FROM household WHERE id = :userId'); + $stmt->bindValue(':userId', $userId, SQLITE3_INTEGER); + $result = $stmt->execute(); + $user = $result->fetchArray(SQLITE3_ASSOC); + + if ($user['name']) { + $message = $user['name'] . ", the following subscriptions are up for renewal:\n"; + } else { + $message = "The following subscriptions are up for renewal:\n"; + } + + foreach ($perUser as $subscription) { + $dayText = $subscription['days'] == 1 ? "Tomorrow" : "In " . $subscription['days'] . " days"; + $message .= $subscription['name'] . " for " . $subscription['price'] . " (" . $dayText . ")\n"; + } + + $headers = json_decode($ntfy["headers"], true); + $customheaders = array_map(function($key, $value) { + return "$key: $value"; + }, array_keys($headers), $headers); + + $ch = curl_init(); + + curl_setopt($ch, CURLOPT_URL, $ntfy['host'] . '/' . $ntfy['topic']); + curl_setopt($ch, CURLOPT_POST, 1); + curl_setopt($ch, CURLOPT_POSTFIELDS, $message); + curl_setopt($ch, CURLOPT_HTTPHEADER, $customheaders); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + + $response = curl_exec($ch); + curl_close($ch); + + if ($response === false) { + echo "Error sending notifications: " . curl_error($ch) . ""; + } else { + echo "Ntfy Notifications sent"; + } + } + } + // Webhook notifications if enabled if ($webhookNotificationsEnabled) { // Get webhook payload and turn it into a json object diff --git a/endpoints/db/migrate.php b/endpoints/db/migrate.php index 99069b31d..e78947709 100644 --- a/endpoints/db/migrate.php +++ b/endpoints/db/migrate.php @@ -49,6 +49,9 @@ function errorHandler($severity, $message, $file, $line) { } foreach ($requiredMigrations as $migration) { + if (!file_exists($migration)) { + $migration = '../../' . $migration; + } require_once $migration; $stmtInsert = $db->prepare('INSERT INTO migrations (migration) VALUES (:migration)'); diff --git a/endpoints/notifications/saventfynotifications.php b/endpoints/notifications/saventfynotifications.php new file mode 100644 index 000000000..ec663bc99 --- /dev/null +++ b/endpoints/notifications/saventfynotifications.php @@ -0,0 +1,84 @@ + false, + "message" => translate('session_expired', $i18n) + ])); + } + + if ($_SERVER["REQUEST_METHOD"] === "POST") { + $postData = file_get_contents("php://input"); + $data = json_decode($postData, true); + + if ( + !isset($data["topic"]) || $data["topic"] == "" || + !isset($data["host"]) || $data["host"] == "" + ) { + $response = [ + "success" => false, + "message" => translate('fill_mandatory_fields', $i18n) + ]; + echo json_encode($response); + } else { + $enabled = $data["enabled"]; + $host = $data["host"]; + $topic = $data["topic"]; + $headers = $data["headers"]; + + $query = "SELECT COUNT(*) FROM ntfy_notifications WHERE user_id = :userId"; + $stmt = $db->prepare($query); + $stmt->bindParam(":userId", $userId, SQLITE3_INTEGER); + $result = $stmt->execute(); + + if ($result === false) { + $response = [ + "success" => false, + "message" => translate('error_saving_notifications', $i18n) + ]; + echo json_encode($response); + } else { + $row = $result->fetchArray(); + $count = $row[0]; + if ($count == 0) { + $query = "INSERT INTO ntfy_notifications (enabled, host, topic, headers, user_id) + VALUES (:enabled, :host, :topic, :headers, :userId)"; + } else { + $query = "UPDATE ntfy_notifications + SET enabled = :enabled, host = :host, topic = :topic, headers = :headers WHERE user_id = :userId"; + } + + $stmt = $db->prepare($query); + $stmt->bindValue(':enabled', $enabled, SQLITE3_INTEGER); + $stmt->bindValue(':host', $host, SQLITE3_TEXT); + $stmt->bindValue(':topic', $topic, SQLITE3_TEXT); + $stmt->bindValue(':headers', $headers, SQLITE3_TEXT); + $stmt->bindValue(':userId', $userId, SQLITE3_INTEGER); + + if ($stmt->execute()) { + $response = [ + "success" => true, + "message" => translate('notifications_settings_saved', $i18n) + ]; + echo json_encode($response); + } else { + $response = [ + "success" => false, + "message" => translate('error_saving_notifications', $i18n) + ]; + echo json_encode($response); + } + } + } + + } else { + $response = [ + "success" => false, + "message" => translate('invalid_request_method', $i18n) + ]; + echo json_encode($response); + } + +?> diff --git a/endpoints/notifications/savepushovernotifications.php b/endpoints/notifications/savepushovernotifications.php index f2295ce23..0c171de12 100644 --- a/endpoints/notifications/savepushovernotifications.php +++ b/endpoints/notifications/savepushovernotifications.php @@ -70,6 +70,12 @@ } } } + } else { + $response = [ + "success" => false, + "message" => translate('invalid_request_method', $i18n) + ]; + echo json_encode($response); } ?> \ No newline at end of file diff --git a/endpoints/notifications/testntfynotifications.php b/endpoints/notifications/testntfynotifications.php new file mode 100644 index 000000000..612d06186 --- /dev/null +++ b/endpoints/notifications/testntfynotifications.php @@ -0,0 +1,70 @@ + false, + "message" => translate('session_expired', $i18n) + ])); +} + +if ($_SERVER["REQUEST_METHOD"] === "POST") { + $postData = file_get_contents("php://input"); + $data = json_decode($postData, true); + + if ( + !isset($data["host"]) || $data["host"] == "" || + !isset($data["topic"]) || $data["topic"] == "" + ) { + $response = [ + "success" => false, + "message" => translate('fill_mandatory_fields', $i18n) + ]; + echo json_encode($response); + } else { + $host = $data["host"]; + $topic = $data["topic"]; + $headers = json_decode($data["headers"], true); + $customheaders = array_map(function($key, $value) { + return "$key: $value"; + }, array_keys($headers), $headers); + + $url = "$host/$topic"; + + // Set the message parameters + $message = translate('test_notification', $i18n); + + $ch = curl_init(); + + // Set the URL and other options + curl_setopt($ch, CURLOPT_URL, $url); + curl_setopt($ch, CURLOPT_POST, 1); + curl_setopt($ch, CURLOPT_POSTFIELDS, $message); + curl_setopt($ch, CURLOPT_HTTPHEADER, $customheaders); + + // Execute the request + $response = curl_exec($ch); + + // Close the cURL session + curl_close($ch); + + // Check if the message was sent successfully + if ($response === false) { + die(json_encode([ + "success" => false, + "message" => translate('notification_failed', $i18n) + ])); + } else { + print_r($response); + } + + die(json_encode([ + "success" => true, + "message" => translate('notification_sent_successfuly', $i18n) + ])); + } + +} + +?> \ No newline at end of file diff --git a/includes/i18n/de.php b/includes/i18n/de.php index 6909fa998..77f5f6caa 100644 --- a/includes/i18n/de.php +++ b/includes/i18n/de.php @@ -154,6 +154,8 @@ "discord_bot_avatar_url" => "Bot Avatar URL", "pushover" => "Pushover", "pushover_user_key" => "Pushover User Key", + 'host' => "Host", + 'topic' => "Topic", "categories" => "Kategorien", "save_category" => "Kategorie speichern", "delete_category" => "Kategorie löschen", diff --git a/includes/i18n/el.php b/includes/i18n/el.php index 4893487ce..af6dfb82a 100644 --- a/includes/i18n/el.php +++ b/includes/i18n/el.php @@ -154,6 +154,8 @@ "discord_bot_avatar_url" => "Discord Bot Avatar URL", "pushover" => "Pushover", "pushover_user_key" => "Pushover User Key", + 'host' => "Host", + 'topic' => "Θέμα", "categories" => "Κατηγορίες", "save_category" => "Αποθήκευση κατηγορίας", "delete_category" => "Διαγραφή κατηγορίας", diff --git a/includes/i18n/en.php b/includes/i18n/en.php index 74982d965..7a2d975be 100644 --- a/includes/i18n/en.php +++ b/includes/i18n/en.php @@ -154,6 +154,8 @@ "discord_bot_avatar_url" => "Discord Bot Avatar URL", "pushover" => "Pushover", "pushover_user_key" => "Pushover User Key", + 'host' => "Host", + 'topic' => "Topic", "categories" => "Categories", "save_category" => "Save Category", "delete_category" => "Delete Category", diff --git a/includes/i18n/es.php b/includes/i18n/es.php index 42ad2b613..0180c4c66 100644 --- a/includes/i18n/es.php +++ b/includes/i18n/es.php @@ -154,6 +154,8 @@ "discord_bot_avatar_url" => "URL del avatar del bot", "pushover" => "Pushover", "pushover_user_key" => "Clave de usuario", + 'host' => "Host", + 'topic' => "Topico", "categories" => "Categorías", "save_category" => "Guardar Categoría", "delete_category" => "Eliminar Categoría", diff --git a/includes/i18n/fr.php b/includes/i18n/fr.php index ab9e08792..418a0a028 100644 --- a/includes/i18n/fr.php +++ b/includes/i18n/fr.php @@ -154,6 +154,8 @@ "discord_bot_avatar_url" => "URL de l'avatar du bot Discord", "pushover" => "Pushover", "pushover_user_key" => "Clé utilisateur Pushover", + 'host' => "Hôte", + 'topic' => "Sujet", "categories" => "Catégories", "save_category" => "Enregistrer la catégorie", "delete_category" => "Supprimer la catégorie", diff --git a/includes/i18n/it.php b/includes/i18n/it.php index b1ce8bb2a..08c879c78 100644 --- a/includes/i18n/it.php +++ b/includes/i18n/it.php @@ -160,6 +160,8 @@ "discord_bot_avatar_url" => "URL dell'avatar del bot", "pushover" => "Pushover", "pushover_user_key" => "Chiave utente", + 'host' => "Host", + 'topic' => "Topic", 'categories' => 'Categorie', 'save_category' => 'Salva categoria', 'delete_category' => 'Elimina categoria', diff --git a/includes/i18n/jp.php b/includes/i18n/jp.php index 2e3b8a1dc..7921c5e40 100644 --- a/includes/i18n/jp.php +++ b/includes/i18n/jp.php @@ -154,6 +154,8 @@ "discord_bot_avatar_url" => "DiscordボットアバターURL", "pushover" => "Pushover", "pushover_user_key" => "Pushoverユーザーキー", + 'host' => "ホスト", + 'topic' => "トピック", "categories" => "カテゴリ", "save_category" => "カテゴリを保存", "delete_category" => "カテゴリを削除", diff --git a/includes/i18n/ko.php b/includes/i18n/ko.php index e7817de0b..96efe1ef7 100644 --- a/includes/i18n/ko.php +++ b/includes/i18n/ko.php @@ -153,7 +153,9 @@ "discord_bot_username" => "디스코드 봇 유저명", "discord_bot_avatar_url" => "디스코드 봇 아바타 URL", "pushover" => "Pushover", - "pushover_user_key" => "Pushover User Key", + "pushover_user_key" => "Pushover User Key",~ + 'host' => "호스트", + 'topic' => "토픽", "categories" => "카테고리", "save_category" => "카테고리 저장", "delete_category" => "카테고리 삭제", diff --git a/includes/i18n/pl.php b/includes/i18n/pl.php index b4140efb5..8a15ff296 100644 --- a/includes/i18n/pl.php +++ b/includes/i18n/pl.php @@ -154,6 +154,8 @@ "discord_bot_avatar_url" => "URL awatara bota", "pushover" => "Pushover", "pushover_user_key" => "Klucz użytkownika", + 'host' => "Host", + 'topic' => "Temat", "categories" => "Kategorie", "save_category" => "Zapisz kategorię", "delete_category" => "Usuń kategorię", diff --git a/includes/i18n/pt.php b/includes/i18n/pt.php index fde510eb0..a82ff8ae0 100644 --- a/includes/i18n/pt.php +++ b/includes/i18n/pt.php @@ -154,6 +154,8 @@ "discord_bot_avatar_url" => "URL do Avatar do Bot", "pushover" => "Pushover", "pushover_user_key" => "Chave de Utilizador Pushover", + 'host' => "Host", + 'topic' => "Tópico", "categories" => "Categorias", "save_category" => "Guardar Categoria", "delete_category" => "Apagar Categoria", diff --git a/includes/i18n/pt_br.php b/includes/i18n/pt_br.php index 416258f41..512e2319b 100644 --- a/includes/i18n/pt_br.php +++ b/includes/i18n/pt_br.php @@ -152,6 +152,8 @@ "discord_bot_avatar_url" => "URL do Avatar", "pushover" => "Pushover", "pushover_user_key" => "Chave do Usuário", + 'host' => "Host", + 'topic' => "Tópico", "categories" => "Categorias", "save_category" => "Salvar categoria", "delete_category" => "Excluir categoria", diff --git a/includes/i18n/ru.php b/includes/i18n/ru.php index 5731a7bdb..eb1fc0cb2 100644 --- a/includes/i18n/ru.php +++ b/includes/i18n/ru.php @@ -154,6 +154,8 @@ "discord_bot_avatar_url" => "URL-адрес аватара бота Discord", "pushover" => "Pushover", "pushover_user_key" => "Ключ пользователя Pushover", + 'host' => "Хост", + 'topic' => "Тема", "categories" => "Категории", "save_category" => "Сохранить категорию", "delete_category" => "Удалить категорию", diff --git a/includes/i18n/sl.php b/includes/i18n/sl.php index 6f2f55319..48e27df29 100644 --- a/includes/i18n/sl.php +++ b/includes/i18n/sl.php @@ -154,6 +154,8 @@ "discord_bot_avatar_url" => "URL avatarja Discordovega bota", "pushover" => "Pushover", "pushover_user_key" => "Uporabniški ključ Pushover", + 'host' => "Gostitelj", + 'topic' => "Tema", "categories" => "Kategorije", "save_category" => "Shrani kategorijo", "delete_category" => "Izbriši kategorijo", diff --git a/includes/i18n/sr.php b/includes/i18n/sr.php index 900ae4e57..aa14614c9 100644 --- a/includes/i18n/sr.php +++ b/includes/i18n/sr.php @@ -153,6 +153,8 @@ "discord_bot_avatar_url" => "Дискорд бот URL аватара", "pushover" => "Пушовер", "pushover_user_key" => "Пушовер кориснички кључ", + 'host' => "Домаћин", + 'topic' => "Тема", "categories" => "Категорије", "save_category" => "Сачувај категорију", "delete_category" => "Избриши категорију", diff --git a/includes/i18n/sr_lat.php b/includes/i18n/sr_lat.php index 44cf784f8..a51830bbe 100644 --- a/includes/i18n/sr_lat.php +++ b/includes/i18n/sr_lat.php @@ -154,6 +154,8 @@ "discord_bot_avatar_url" => "Discord bot URL avatara", "pushover" => "Pushover", "pushover_user_key" => "Pushover korisnički ključ", + 'host' => "Host", + 'topic' => "Tema", "categories" => "Kategorije", "save_category" => "Sačuvaj kategoriju", "delete_category" => "Izbriši kategoriju", diff --git a/includes/i18n/tr.php b/includes/i18n/tr.php index 7bf0a3667..31ea59d70 100644 --- a/includes/i18n/tr.php +++ b/includes/i18n/tr.php @@ -154,6 +154,8 @@ "discord_bot_avatar_url" => "Discord Bot Avatar URL", "pushover" => "Pushover", "pushover_user_key" => "Pushover Kullanıcı Anahtarı", + 'host' => "Host", + 'topic' => "Konu", "categories" => "Kategoriler", "save_category" => "Kategoriyi Kaydet", "delete_category" => "Kategoriyi Sil", diff --git a/includes/i18n/zh_cn.php b/includes/i18n/zh_cn.php index 306a82eda..b933855a7 100644 --- a/includes/i18n/zh_cn.php +++ b/includes/i18n/zh_cn.php @@ -162,6 +162,8 @@ "discord_bot_avatar_url" => "Discord 机器人头像 URL", "pushover" => "Pushover", "pushover_user_key" => "Pushover 用户密钥", + 'host' => "主机", + 'topic' => "主题", "categories" => "分类", "save_category" => "保存分类", "delete_category" => "删除分类", diff --git a/includes/i18n/zh_tw.php b/includes/i18n/zh_tw.php index 439cfd2dc..1401b12e3 100644 --- a/includes/i18n/zh_tw.php +++ b/includes/i18n/zh_tw.php @@ -154,6 +154,8 @@ "discord_bot_avatar_url" => "Discord 機器人頭像 URL", "pushover" => "Pushover", "pushover_user_key" => "Pushover 使用者金鑰", + 'host' => "主機", + 'topic' => "主題", "categories" => "分類", "save_category" => "儲存分類", "delete_category" => "刪除分類", diff --git a/includes/version.php b/includes/version.php index 1dc342cf0..13b60f72b 100644 --- a/includes/version.php +++ b/includes/version.php @@ -1,3 +1,3 @@ diff --git a/migrations/000021.php b/migrations/000021.php new file mode 100644 index 000000000..a849e80c2 --- /dev/null +++ b/migrations/000021.php @@ -0,0 +1,15 @@ +exec('CREATE TABLE IF NOT EXISTS ntfy_notifications ( + enabled BOOLEAN DEFAULT 0, + host TEXT DEFAULT "", + topic TEXT DEFAULT "", + headers TEXT DEFAULT "", + user_id INTEGER, + FOREIGN KEY (user_id) REFERENCES user(id) +)'); \ No newline at end of file diff --git a/scripts/notifications.js b/scripts/notifications.js index 3a816b5f1..bb67040dd 100644 --- a/scripts/notifications.js +++ b/scripts/notifications.js @@ -280,4 +280,41 @@ function testNotificationsDiscordButton() { }; makeFetchCall('endpoints/notifications/testdiscordnotifications.php', data, button); +} + +function testNotificationsNtfyButton() { + const button = document.getElementById("testNotificationsNtfy"); + button.disabled = true; + + const host = document.getElementById("ntfyhost").value; + const topic = document.getElementById("ntfytopic").value; + const headers = document.getElementById("ntfyheaders").value; + + + const data = { + host: host, + topic: topic, + headers: headers + }; + + makeFetchCall('endpoints/notifications/testntfynotifications.php', data, button); +} + +function saveNotificationsNtfyButton() { + const button = document.getElementById("saveNotificationsNtfy"); + button.disabled = true; + + const enabled = document.getElementById("ntfyenabled").checked ? 1 : 0; + const host = document.getElementById("ntfyhost").value; + const topic = document.getElementById("ntfytopic").value; + const headers = document.getElementById("ntfyheaders").value; + + const data = { + enabled: enabled, + host: host, + topic: topic, + headers: headers + }; + + makeFetchCall('endpoints/notifications/saventfynotifications.php', data, button); } \ No newline at end of file diff --git a/settings.php b/settings.php index 710641b48..9249ff196 100644 --- a/settings.php +++ b/settings.php @@ -309,6 +309,28 @@ $notificationsTelegram['chat_id'] = ""; } + // Ntfy notifications + $sql = "SELECT * FROM ntfy_notifications WHERE user_id = :userId LIMIT 1"; + $stmt = $db->prepare($sql); + $stmt->bindValue(':userId', $userId, SQLITE3_INTEGER); + $result = $stmt->execute(); + + $rowCount = 0; + while ($row = $result->fetchArray(SQLITE3_ASSOC)) { + $notificationsNtfy['enabled'] = $row['enabled']; + $notificationsNtfy['host'] = $row['host']; + $notificationsNtfy['topic'] = $row['topic']; + $notificationsNtfy['headers'] = $row['headers']; + $rowCount++; + } + + if ($rowCount == 0) { + $notificationsNtfy['enabled'] = 0; + $notificationsNtfy['host'] = ""; + $notificationsNtfy['topic'] = ""; + $notificationsNtfy['headers'] = ""; + } + // Webhook notifications $sql = "SELECT * FROM webhook_notifications WHERE user_id = :userId LIMIT 1"; $stmt = $db->prepare($sql); @@ -537,6 +559,31 @@ + + + + Ntfy + + + + + > + = translate('enabled', $i18n) ?> + + + + + + + + + = $notificationsNtfy['headers'] ?> + + + + + + diff --git a/styles/dark-theme.css b/styles/dark-theme.css index e4f49b35e..26808020c 100644 --- a/styles/dark-theme.css +++ b/styles/dark-theme.css @@ -88,10 +88,14 @@ select:disabled { cursor: not-allowed; } +button.secondary-button, +button.button.secondary-button, input[type="button"].secondary-button { background-color: #222; } +button.button.secondary-button:hover, +button.secondary-button:hover, input[type="button"].secondary-button:hover { background-color: #111; } diff --git a/styles/styles.css b/styles/styles.css index c8caa2bdb..882576bcd 100644 --- a/styles/styles.css +++ b/styles/styles.css @@ -669,7 +669,7 @@ header #avatar { } .account-notifications-section { - border: 1px solid #ccc; + border: 1px solid #aaa; border-radius: 8px; margin-bottom: 10px; overflow: hidden;