Jelajahi Sumber

feat(integration): add ntfy integration

Frédéric Guillot 1 tahun lalu
induk
melakukan
968355f9b9
32 mengubah file dengan 567 tambahan dan 17 penghapusan
  1. 16 0
      internal/database/migrations.go
  2. 23 0
      internal/integration/integration.go
  3. 120 0
      internal/integration/ntfy/ntfy.go
  4. 14 0
      internal/locale/translations/de_DE.json
  5. 14 0
      internal/locale/translations/el_EL.json
  6. 14 0
      internal/locale/translations/en_US.json
  7. 14 0
      internal/locale/translations/es_ES.json
  8. 14 0
      internal/locale/translations/fi_FI.json
  9. 14 0
      internal/locale/translations/fr_FR.json
  10. 14 0
      internal/locale/translations/hi_IN.json
  11. 14 0
      internal/locale/translations/id_ID.json
  12. 14 0
      internal/locale/translations/it_IT.json
  13. 14 0
      internal/locale/translations/ja_JP.json
  14. 14 0
      internal/locale/translations/nl_NL.json
  15. 14 0
      internal/locale/translations/pl_PL.json
  16. 14 0
      internal/locale/translations/pt_BR.json
  17. 14 0
      internal/locale/translations/ru_RU.json
  18. 14 0
      internal/locale/translations/tr_TR.json
  19. 14 0
      internal/locale/translations/uk_UA.json
  20. 14 0
      internal/locale/translations/zh_CN.json
  21. 14 0
      internal/locale/translations/zh_TW.json
  22. 3 1
      internal/model/feed.go
  23. 7 0
      internal/model/integration.go
  24. 6 2
      internal/storage/feed.go
  25. 5 1
      internal/storage/feed_query_builder.go
  26. 31 3
      internal/storage/integration.go
  27. 33 10
      internal/template/templates/views/edit_feed.html
  28. 31 0
      internal/template/templates/views/integrations.html
  29. 2 0
      internal/ui/feed_edit.go
  30. 10 0
      internal/ui/form/feed.go
  31. 21 0
      internal/ui/form/integration.go
  32. 7 0
      internal/ui/integration_show.go

+ 16 - 0
internal/database/migrations.go

@@ -921,4 +921,20 @@ var migrations = []func(tx *sql.Tx) error{
 		_, err = tx.Exec(sql)
 		return err
 	},
+	func(tx *sql.Tx) (err error) {
+		sql := `
+			ALTER TABLE integrations ADD COLUMN ntfy_enabled bool default 'f';
+			ALTER TABLE integrations ADD COLUMN ntfy_url text default '';
+			ALTER TABLE integrations ADD COLUMN ntfy_topic text default '';
+			ALTER TABLE integrations ADD COLUMN ntfy_api_token text default '';
+			ALTER TABLE integrations ADD COLUMN ntfy_username text default '';
+			ALTER TABLE integrations ADD COLUMN ntfy_password text default '';
+			ALTER TABLE integrations ADD COLUMN ntfy_icon_url text default '';
+
+			ALTER TABLE feeds ADD COLUMN ntfy_enabled bool default 'f';
+			ALTER TABLE feeds ADD COLUMN ntfy_priority int default '3';
+		`
+		_, err = tx.Exec(sql)
+		return err
+	},
 }

+ 23 - 0
internal/integration/integration.go

@@ -16,6 +16,7 @@ import (
 	"miniflux.app/v2/internal/integration/linkwarden"
 	"miniflux.app/v2/internal/integration/matrixbot"
 	"miniflux.app/v2/internal/integration/notion"
+	"miniflux.app/v2/internal/integration/ntfy"
 	"miniflux.app/v2/internal/integration/nunuxkeeper"
 	"miniflux.app/v2/internal/integration/omnivore"
 	"miniflux.app/v2/internal/integration/pinboard"
@@ -470,6 +471,28 @@ func PushEntries(feed *model.Feed, entries model.Entries, userIntegrations *mode
 		}
 	}
 
+	if userIntegrations.NtfyEnabled && feed.NtfyEnabled {
+		slog.Debug("Sending new entries to Ntfy",
+			slog.Int64("user_id", userIntegrations.UserID),
+			slog.Int("nb_entries", len(entries)),
+			slog.Int64("feed_id", feed.ID),
+		)
+
+		client := ntfy.NewClient(
+			userIntegrations.NtfyURL,
+			userIntegrations.NtfyTopic,
+			userIntegrations.NtfyAPIToken,
+			userIntegrations.NtfyUsername,
+			userIntegrations.NtfyPassword,
+			userIntegrations.NtfyIconURL,
+			feed.NtfyPriority,
+		)
+
+		if err := client.SendMessages(feed, entries); err != nil {
+			slog.Warn("Unable to send new entries to Ntfy", slog.Any("error", err))
+		}
+	}
+
 	// Integrations that only support sending individual entries
 	if userIntegrations.TelegramBotEnabled || userIntegrations.AppriseEnabled {
 		for _, entry := range entries {

+ 120 - 0
internal/integration/ntfy/ntfy.go

@@ -0,0 +1,120 @@
+// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package ntfy // import "miniflux.app/v2/internal/integration/ntfy"
+
+import (
+	"bytes"
+	"encoding/json"
+	"fmt"
+	"log/slog"
+	"net/http"
+	"time"
+
+	"miniflux.app/v2/internal/model"
+	"miniflux.app/v2/internal/version"
+)
+
+const (
+	defaultClientTimeout = 10 * time.Second
+	defaultNtfyURL       = "https://ntfy.sh"
+)
+
+type Client struct {
+	ntfyURL, ntfyTopic, ntfyApiToken, ntfyUsername, ntfyPassword, ntfyIconURL string
+	ntfyPriority                                                              int
+}
+
+func NewClient(ntfyURL, ntfyTopic, ntfyApiToken, ntfyUsername, ntfyPassword, ntfyIconURL string, ntfyPriority int) *Client {
+	if ntfyURL == "" {
+		ntfyURL = defaultNtfyURL
+	}
+	return &Client{ntfyURL, ntfyTopic, ntfyApiToken, ntfyUsername, ntfyPassword, ntfyIconURL, ntfyPriority}
+}
+
+func (c *Client) SendMessages(feed *model.Feed, entries model.Entries) error {
+	for _, entry := range entries {
+		ntfyMessage := &ntfyMessage{
+			Topic:    c.ntfyTopic,
+			Message:  entry.Title,
+			Title:    feed.Title,
+			Priority: c.ntfyPriority,
+			Click:    entry.URL,
+		}
+
+		if c.ntfyIconURL != "" {
+			ntfyMessage.Icon = c.ntfyIconURL
+		}
+
+		slog.Debug("Sending Ntfy message",
+			slog.String("url", c.ntfyURL),
+			slog.String("topic", c.ntfyTopic),
+			slog.Int("priority", ntfyMessage.Priority),
+			slog.String("message", ntfyMessage.Message),
+			slog.String("entry_url", entry.URL),
+		)
+
+		if err := c.makeRequest(ntfyMessage); err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
+
+func (c *Client) makeRequest(payload any) error {
+	requestBody, err := json.Marshal(payload)
+	if err != nil {
+		return fmt.Errorf("ntfy: unable to encode request body: %v", err)
+	}
+
+	request, err := http.NewRequest(http.MethodPost, c.ntfyURL, bytes.NewReader(requestBody))
+	if err != nil {
+		return fmt.Errorf("ntfy: unable to create request: %v", err)
+	}
+
+	request.Header.Set("Content-Type", "application/json")
+	request.Header.Set("User-Agent", "Miniflux/"+version.Version)
+
+	// See https://docs.ntfy.sh/publish/#access-tokens
+	if c.ntfyApiToken != "" {
+		request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", c.ntfyApiToken))
+	}
+
+	// See https://docs.ntfy.sh/publish/#username-password
+	if c.ntfyUsername != "" && c.ntfyPassword != "" {
+		request.SetBasicAuth(c.ntfyUsername, c.ntfyPassword)
+	}
+
+	httpClient := &http.Client{Timeout: defaultClientTimeout}
+	response, err := httpClient.Do(request)
+	if err != nil {
+		return fmt.Errorf("ntfy: unable to send request: %v", err)
+	}
+	defer response.Body.Close()
+
+	if response.StatusCode >= 400 {
+		return fmt.Errorf("ntfy: incorrect response status code %d for url %s", response.StatusCode, c.ntfyURL)
+	}
+
+	return nil
+}
+
+// See https://docs.ntfy.sh/publish/#publish-as-json
+type ntfyMessage struct {
+	Topic    string       `json:"topic"`
+	Message  string       `json:"message"`
+	Title    string       `json:"title"`
+	Tags     []string     `json:"tags,omitempty"`
+	Priority int          `json:"priority,omitempty"`
+	Icon     string       `json:"icon,omitempty"` // https://docs.ntfy.sh/publish/#icons
+	Click    string       `json:"click,omitempty"`
+	Actions  []ntfyAction `json:"actions,omitempty"`
+}
+
+// See https://docs.ntfy.sh/publish/#action-buttons
+type ntfyAction struct {
+	Action string `json:"action"`
+	Label  string `json:"label"`
+	URL    string `json:"url"`
+}

+ 14 - 0
internal/locale/translations/de_DE.json

@@ -347,6 +347,13 @@
     "form.feed.label.disabled": "Dieses Abonnement nicht aktualisieren",
     "form.feed.label.no_media_player": "Kein Media-Player (Audio/Video)",
     "form.feed.label.hide_globally": "Einträge in der globalen Ungelesen-Liste ausblenden",
+    "form.feed.label.ntfy_activate": "Push entries to ntfy",
+    "form.feed.label.ntfy_priority": "Ntfy priority",
+    "form.feed.label.ntfy_max_priority": "Ntfy max priority",
+    "form.feed.label.ntfy_high_priority": "Ntfy high priority",
+    "form.feed.label.ntfy_default_priority": "Ntfy default priority",
+    "form.feed.label.ntfy_low_priority": "Ntfy low priority",
+    "form.feed.label.ntfy_min_priority": "Ntfy min priority",
     "form.feed.fieldset.general": "Allgemein",
     "form.feed.fieldset.rules": "Regeln",
     "form.feed.fieldset.network_settings": "Netzwerkeinstellungen",
@@ -488,6 +495,13 @@
     "form.integration.webhook_secret": "Webhook Geheimnis",
     "form.integration.rssbridge_activate": "Beim Hinzufügen von Abonnements RSS-Bridge prüfen.",
     "form.integration.rssbridge_url": "RSS-Bridge server URL",
+    "form.integration.ntfy_activate": "Push entries to ntfy",
+    "form.integration.ntfy_topic": "Ntfy topic",
+    "form.integration.ntfy_url": "Ntfy URL (optional, default is ntfy.sh)",
+    "form.integration.ntfy_api_token": "Ntfy API Token (optional)",
+    "form.integration.ntfy_username": "Ntfy Username (optional)",
+    "form.integration.ntfy_password": "Ntfy Password (optional)",
+    "form.integration.ntfy_icon_url": "Ntfy Icon URL (optional)",
     "form.api_key.label.description": "API-Schlüsselbezeichnung",
     "form.submit.loading": "Lade...",
     "form.submit.saving": "Speichern...",

+ 14 - 0
internal/locale/translations/el_EL.json

@@ -351,6 +351,13 @@
     "form.feed.fieldset.rules": "Rules",
     "form.feed.fieldset.network_settings": "Network Settings",
     "form.feed.fieldset.integration": "Third-Party Services",
+    "form.feed.label.ntfy_activate": "Push entries to ntfy",
+    "form.feed.label.ntfy_priority": "Ntfy priority",
+    "form.feed.label.ntfy_max_priority": "Ntfy max priority",
+    "form.feed.label.ntfy_high_priority": "Ntfy high priority",
+    "form.feed.label.ntfy_default_priority": "Ntfy default priority",
+    "form.feed.label.ntfy_low_priority": "Ntfy low priority",
+    "form.feed.label.ntfy_min_priority": "Ntfy min priority",
     "form.category.label.title": "Τίτλος",
     "form.category.hide_globally": "Απόκρυψη καταχωρήσεων σε γενική λίστα μη αναγνωσμένων",
     "form.user.label.username": "Χρήστης",
@@ -488,6 +495,13 @@
     "form.integration.webhook_secret": "Webhook Secret",
     "form.integration.rssbridge_activate": "Check RSS-Bridge when adding subscriptions",
     "form.integration.rssbridge_url": "RSS-Bridge server URL",
+    "form.integration.ntfy_activate": "Push entries to ntfy",
+    "form.integration.ntfy_topic": "Ntfy topic",
+    "form.integration.ntfy_url": "Ntfy URL (optional, default is ntfy.sh)",
+    "form.integration.ntfy_api_token": "Ntfy API Token (optional)",
+    "form.integration.ntfy_username": "Ntfy Username (optional)",
+    "form.integration.ntfy_password": "Ntfy Password (optional)",
+    "form.integration.ntfy_icon_url": "Ntfy Icon URL (optional)",
     "form.api_key.label.description": "Ετικέτα κλειδιού API",
     "form.submit.loading": "Φόρτωση...",
     "form.submit.saving": "Αποθήκευση...",

+ 14 - 0
internal/locale/translations/en_US.json

@@ -347,6 +347,13 @@
     "form.feed.label.disabled": "Do not refresh this feed",
     "form.feed.label.no_media_player": "No media player (audio/video)",
     "form.feed.label.hide_globally": "Hide entries in global unread list",
+    "form.feed.label.ntfy_activate": "Push entries to ntfy",
+    "form.feed.label.ntfy_priority": "Ntfy priority",
+    "form.feed.label.ntfy_max_priority": "Ntfy max priority",
+    "form.feed.label.ntfy_high_priority": "Ntfy high priority",
+    "form.feed.label.ntfy_default_priority": "Ntfy default priority",
+    "form.feed.label.ntfy_low_priority": "Ntfy low priority",
+    "form.feed.label.ntfy_min_priority": "Ntfy min priority",
     "form.feed.fieldset.general": "General",
     "form.feed.fieldset.rules": "Rules",
     "form.feed.fieldset.network_settings": "Network Settings",
@@ -488,6 +495,13 @@
     "form.integration.webhook_secret": "Webhook Secret",
     "form.integration.rssbridge_activate": "Check RSS-Bridge when adding subscriptions",
     "form.integration.rssbridge_url": "RSS-Bridge server URL",
+    "form.integration.ntfy_activate": "Push entries to ntfy",
+    "form.integration.ntfy_topic": "Ntfy topic",
+    "form.integration.ntfy_url": "Ntfy URL (optional, default is ntfy.sh)",
+    "form.integration.ntfy_api_token": "Ntfy API Token (optional)",
+    "form.integration.ntfy_username": "Ntfy Username (optional)",
+    "form.integration.ntfy_password": "Ntfy Password (optional)",
+    "form.integration.ntfy_icon_url": "Ntfy Icon URL (optional)",
     "form.api_key.label.description": "API Key Label",
     "form.submit.loading": "Loading…",
     "form.submit.saving": "Saving…",

+ 14 - 0
internal/locale/translations/es_ES.json

@@ -347,6 +347,13 @@
     "form.feed.label.disabled": "No actualice este feed",
     "form.feed.label.no_media_player": "No media player (audio/video)",
     "form.feed.label.hide_globally": "Ocultar artículos en la lista global de no leídos",
+    "form.feed.label.ntfy_activate": "Push entries to ntfy",
+    "form.feed.label.ntfy_priority": "Ntfy priority",
+    "form.feed.label.ntfy_max_priority": "Ntfy max priority",
+    "form.feed.label.ntfy_high_priority": "Ntfy high priority",
+    "form.feed.label.ntfy_default_priority": "Ntfy default priority",
+    "form.feed.label.ntfy_low_priority": "Ntfy low priority",
+    "form.feed.label.ntfy_min_priority": "Ntfy min priority",
     "form.feed.fieldset.general": "General",
     "form.feed.fieldset.rules": "Rules",
     "form.feed.fieldset.network_settings": "Network Settings",
@@ -488,6 +495,13 @@
     "form.integration.webhook_secret": "Webhook Secret",
     "form.integration.rssbridge_activate": "Check RSS-Bridge when adding subscriptions",
     "form.integration.rssbridge_url": "RSS-Bridge server URL",
+    "form.integration.ntfy_activate": "Push entries to ntfy",
+    "form.integration.ntfy_topic": "Ntfy topic",
+    "form.integration.ntfy_url": "Ntfy URL (optional, default is ntfy.sh)",
+    "form.integration.ntfy_api_token": "Ntfy API Token (optional)",
+    "form.integration.ntfy_username": "Ntfy Username (optional)",
+    "form.integration.ntfy_password": "Ntfy Password (optional)",
+    "form.integration.ntfy_icon_url": "Ntfy Icon URL (optional)",
     "form.api_key.label.description": "Etiqueta de clave API",
     "form.submit.loading": "Cargando...",
     "form.submit.saving": "Guardando...",

+ 14 - 0
internal/locale/translations/fi_FI.json

@@ -347,6 +347,13 @@
     "form.feed.label.disabled": "Älä päivitä tätä syötettä",
     "form.feed.label.no_media_player": "No media player (audio/video)",
     "form.feed.label.hide_globally": "Piilota artikkelit lukemattomien listassa",
+    "form.feed.label.ntfy_activate": "Push entries to ntfy",
+    "form.feed.label.ntfy_priority": "Ntfy priority",
+    "form.feed.label.ntfy_max_priority": "Ntfy max priority",
+    "form.feed.label.ntfy_high_priority": "Ntfy high priority",
+    "form.feed.label.ntfy_default_priority": "Ntfy default priority",
+    "form.feed.label.ntfy_low_priority": "Ntfy low priority",
+    "form.feed.label.ntfy_min_priority": "Ntfy min priority",
     "form.feed.fieldset.general": "General",
     "form.feed.fieldset.rules": "Rules",
     "form.feed.fieldset.network_settings": "Network Settings",
@@ -488,6 +495,13 @@
     "form.integration.webhook_secret": "Webhook Secret",
     "form.integration.rssbridge_activate": "Check RSS-Bridge when adding subscriptions",
     "form.integration.rssbridge_url": "RSS-Bridge server URL",
+    "form.integration.ntfy_activate": "Push entries to ntfy",
+    "form.integration.ntfy_topic": "Ntfy topic",
+    "form.integration.ntfy_url": "Ntfy URL (optional, default is ntfy.sh)",
+    "form.integration.ntfy_api_token": "Ntfy API Token (optional)",
+    "form.integration.ntfy_username": "Ntfy Username (optional)",
+    "form.integration.ntfy_password": "Ntfy Password (optional)",
+    "form.integration.ntfy_icon_url": "Ntfy Icon URL (optional)",
     "form.api_key.label.description": "API Key Label",
     "form.submit.loading": "Ladataan...",
     "form.submit.saving": "Tallennetaan...",

+ 14 - 0
internal/locale/translations/fr_FR.json

@@ -347,6 +347,13 @@
     "form.feed.label.disabled": "Ne pas actualiser ce flux",
     "form.feed.label.no_media_player": "Pas de lecteur multimedia (audio/vidéo)",
     "form.feed.label.hide_globally": "Masquer les entrées dans la liste globale non lue",
+    "form.feed.label.ntfy_activate": "Activer les notifications",
+    "form.feed.label.ntfy_priority": "Priorité de notification",
+    "form.feed.label.ntfy_max_priority": "Priorité maximale de notification",
+    "form.feed.label.ntfy_high_priority": "Priorité élevée de notification",
+    "form.feed.label.ntfy_default_priority": "Priorité par défaut de notification",
+    "form.feed.label.ntfy_low_priority": "Priorité basse de notification",
+    "form.feed.label.ntfy_min_priority": "Priorité minimale de notification",
     "form.feed.fieldset.general": "Général",
     "form.feed.fieldset.rules": "Règles",
     "form.feed.fieldset.network_settings": "Paramètres réseau",
@@ -488,6 +495,13 @@
     "form.integration.webhook_secret": "Secret du webhook",
     "form.integration.rssbridge_activate": "Check RSS-Bridge when adding subscriptions",
     "form.integration.rssbridge_url": "RSS-Bridge server URL",
+    "form.integration.ntfy_activate": "Envoyer les entrées vers ntfy",
+    "form.integration.ntfy_topic": "Sujet Ntfy",
+    "form.integration.ntfy_url": "URL de Ntfy (optionnel, ntfy.sh par défaut)",
+    "form.integration.ntfy_api_token": "Jeton d'API Ntfy (optionnel)",
+    "form.integration.ntfy_username": "Nom d'utilisateur Ntfy (optionnel)",
+    "form.integration.ntfy_password": "Mot de passe Ntfy (facultatif)",
+    "form.integration.ntfy_icon_url": "URL de l'icône Ntfy (facultatif)",
     "form.api_key.label.description": "Libellé de la clé d'API",
     "form.submit.loading": "Chargement...",
     "form.submit.saving": "Sauvegarde en cours...",

+ 14 - 0
internal/locale/translations/hi_IN.json

@@ -347,6 +347,13 @@
     "form.feed.label.disabled": "इस फ़ीड को रीफ़्रेश न करें",
     "form.feed.label.no_media_player": "No media player (audio/video)",
     "form.feed.label.hide_globally": "वैश्विक अपठित सूची में प्रविष्टियां छिपाएं",
+    "form.feed.label.ntfy_activate": "Push entries to ntfy",
+    "form.feed.label.ntfy_priority": "Ntfy priority",
+    "form.feed.label.ntfy_max_priority": "Ntfy max priority",
+    "form.feed.label.ntfy_high_priority": "Ntfy high priority",
+    "form.feed.label.ntfy_default_priority": "Ntfy default priority",
+    "form.feed.label.ntfy_low_priority": "Ntfy low priority",
+    "form.feed.label.ntfy_min_priority": "Ntfy min priority",
     "form.feed.fieldset.general": "General",
     "form.feed.fieldset.rules": "Rules",
     "form.feed.fieldset.network_settings": "Network Settings",
@@ -488,6 +495,13 @@
     "form.integration.webhook_secret": "Webhook Secret",
     "form.integration.rssbridge_activate": "Check RSS-Bridge when adding subscriptions",
     "form.integration.rssbridge_url": "RSS-Bridge server URL",
+    "form.integration.ntfy_activate": "Push entries to ntfy",
+    "form.integration.ntfy_topic": "Ntfy topic",
+    "form.integration.ntfy_url": "Ntfy URL (optional, default is ntfy.sh)",
+    "form.integration.ntfy_api_token": "Ntfy API Token (optional)",
+    "form.integration.ntfy_username": "Ntfy Username (optional)",
+    "form.integration.ntfy_password": "Ntfy Password (optional)",
+    "form.integration.ntfy_icon_url": "Ntfy Icon URL (optional)",
     "form.api_key.label.description": "एपीआई कुंजी लेबल",
     "form.submit.loading": "लोड हो रहा है...",
     "form.submit.saving": "सहेजा जा रहा है...",

+ 14 - 0
internal/locale/translations/id_ID.json

@@ -337,6 +337,13 @@
     "form.feed.label.disabled": "Jangan perbarui umpan ini",
     "form.feed.label.no_media_player": "No media player (audio/video)",
     "form.feed.label.hide_globally": "Sembunyikan entri di daftar belum dibaca global",
+    "form.feed.label.ntfy_activate": "Push entries to ntfy",
+    "form.feed.label.ntfy_priority": "Ntfy priority",
+    "form.feed.label.ntfy_max_priority": "Ntfy max priority",
+    "form.feed.label.ntfy_high_priority": "Ntfy high priority",
+    "form.feed.label.ntfy_default_priority": "Ntfy default priority",
+    "form.feed.label.ntfy_low_priority": "Ntfy low priority",
+    "form.feed.label.ntfy_min_priority": "Ntfy min priority",
     "form.feed.fieldset.general": "General",
     "form.feed.fieldset.rules": "Rules",
     "form.feed.fieldset.network_settings": "Network Settings",
@@ -478,6 +485,13 @@
     "form.integration.webhook_secret": "Webhook Secret",
     "form.integration.rssbridge_activate": "Check RSS-Bridge when adding subscriptions",
     "form.integration.rssbridge_url": "RSS-Bridge server URL",
+    "form.integration.ntfy_activate": "Push entries to ntfy",
+    "form.integration.ntfy_topic": "Ntfy topic",
+    "form.integration.ntfy_url": "Ntfy URL (optional, default is ntfy.sh)",
+    "form.integration.ntfy_api_token": "Ntfy API Token (optional)",
+    "form.integration.ntfy_username": "Ntfy Username (optional)",
+    "form.integration.ntfy_password": "Ntfy Password (optional)",
+    "form.integration.ntfy_icon_url": "Ntfy Icon URL (optional)",
     "form.api_key.label.description": "Label Kunci API",
     "form.submit.loading": "Memuat...",
     "form.submit.saving": "Menyimpan...",

+ 14 - 0
internal/locale/translations/it_IT.json

@@ -347,6 +347,13 @@
     "form.feed.label.disabled": "Non aggiornare questo feed",
     "form.feed.label.no_media_player": "No media player (audio/video)",
     "form.feed.label.hide_globally": "Nascondere le voci nella lista globale dei non letti",
+    "form.feed.label.ntfy_activate": "Push entries to ntfy",
+    "form.feed.label.ntfy_priority": "Ntfy priority",
+    "form.feed.label.ntfy_max_priority": "Ntfy max priority",
+    "form.feed.label.ntfy_high_priority": "Ntfy high priority",
+    "form.feed.label.ntfy_default_priority": "Ntfy default priority",
+    "form.feed.label.ntfy_low_priority": "Ntfy low priority",
+    "form.feed.label.ntfy_min_priority": "Ntfy min priority",
     "form.feed.fieldset.general": "General",
     "form.feed.fieldset.rules": "Rules",
     "form.feed.fieldset.network_settings": "Network Settings",
@@ -489,6 +496,13 @@
     "form.integration.rssbridge_activate": "Check RSS-Bridge when adding subscriptions",
     "form.integration.rssbridge_url": "RSS-Bridge server URL",
     "form.api_key.label.description": "Etichetta chiave API",
+    "form.integration.ntfy_activate": "Push entries to ntfy",
+    "form.integration.ntfy_topic": "Ntfy topic",
+    "form.integration.ntfy_url": "Ntfy URL (optional, default is ntfy.sh)",
+    "form.integration.ntfy_api_token": "Ntfy API Token (optional)",
+    "form.integration.ntfy_username": "Ntfy Username (optional)",
+    "form.integration.ntfy_password": "Ntfy Password (optional)",
+    "form.integration.ntfy_icon_url": "Ntfy Icon URL (optional)",
     "form.submit.loading": "Caricamento in corso...",
     "form.submit.saving": "Salvataggio in corso...",
     "time_elapsed.not_yet": "non ancora",

+ 14 - 0
internal/locale/translations/ja_JP.json

@@ -337,6 +337,13 @@
     "form.feed.label.disabled": "このフィードを更新しない",
     "form.feed.label.no_media_player": "No media player (audio/video)",
     "form.feed.label.hide_globally": "未読一覧に記事を表示しない",
+    "form.feed.label.ntfy_activate": "Push entries to ntfy",
+    "form.feed.label.ntfy_priority": "Ntfy priority",
+    "form.feed.label.ntfy_max_priority": "Ntfy max priority",
+    "form.feed.label.ntfy_high_priority": "Ntfy high priority",
+    "form.feed.label.ntfy_default_priority": "Ntfy default priority",
+    "form.feed.label.ntfy_low_priority": "Ntfy low priority",
+    "form.feed.label.ntfy_min_priority": "Ntfy min priority",
     "form.feed.fieldset.general": "General",
     "form.feed.fieldset.rules": "Rules",
     "form.feed.fieldset.network_settings": "Network Settings",
@@ -478,6 +485,13 @@
     "form.integration.webhook_secret": "Webhook Secret",
     "form.integration.rssbridge_activate": "Check RSS-Bridge when adding subscriptions",
     "form.integration.rssbridge_url": "RSS-Bridge server URL",
+    "form.integration.ntfy_activate": "Push entries to ntfy",
+    "form.integration.ntfy_topic": "Ntfy topic",
+    "form.integration.ntfy_url": "Ntfy URL (optional, default is ntfy.sh)",
+    "form.integration.ntfy_api_token": "Ntfy API Token (optional)",
+    "form.integration.ntfy_username": "Ntfy Username (optional)",
+    "form.integration.ntfy_password": "Ntfy Password (optional)",
+    "form.integration.ntfy_icon_url": "Ntfy Icon URL (optional)",
     "form.api_key.label.description": "API キーラベル",
     "form.submit.loading": "読み込み中…",
     "form.submit.saving": "保存中…",

+ 14 - 0
internal/locale/translations/nl_NL.json

@@ -347,6 +347,13 @@
     "form.feed.label.disabled": "Vernieuw deze feed niet",
     "form.feed.label.no_media_player": "No media player (audio/video)",
     "form.feed.label.hide_globally": "Verberg items in de globale ongelezen lijst",
+    "form.feed.label.ntfy_activate": "Push entries to ntfy",
+    "form.feed.label.ntfy_priority": "Ntfy priority",
+    "form.feed.label.ntfy_max_priority": "Ntfy max priority",
+    "form.feed.label.ntfy_high_priority": "Ntfy high priority",
+    "form.feed.label.ntfy_default_priority": "Ntfy default priority",
+    "form.feed.label.ntfy_low_priority": "Ntfy low priority",
+    "form.feed.label.ntfy_min_priority": "Ntfy min priority",
     "form.feed.fieldset.general": "General",
     "form.feed.fieldset.rules": "Rules",
     "form.feed.fieldset.network_settings": "Network Settings",
@@ -488,6 +495,13 @@
     "form.integration.webhook_secret": "Webhook Secret",
     "form.integration.rssbridge_activate": "Check RSS-Bridge when adding subscriptions",
     "form.integration.rssbridge_url": "RSS-Bridge server URL",
+    "form.integration.ntfy_activate": "Push entries to ntfy",
+    "form.integration.ntfy_topic": "Ntfy topic",
+    "form.integration.ntfy_url": "Ntfy URL (optional, default is ntfy.sh)",
+    "form.integration.ntfy_api_token": "Ntfy API Token (optional)",
+    "form.integration.ntfy_username": "Ntfy Username (optional)",
+    "form.integration.ntfy_password": "Ntfy Password (optional)",
+    "form.integration.ntfy_icon_url": "Ntfy Icon URL (optional)",
     "form.api_key.label.description": "API-sleutellabel",
     "form.submit.loading": "Laden...",
     "form.submit.saving": "Opslaag...",

+ 14 - 0
internal/locale/translations/pl_PL.json

@@ -357,6 +357,13 @@
     "form.feed.label.disabled": "Nie odświeżaj tego kanału",
     "form.feed.label.no_media_player": "No media player (audio/video)",
     "form.feed.label.hide_globally": "Ukryj wpisy na globalnej liście nieprzeczytanych",
+    "form.feed.label.ntfy_activate": "Push entries to ntfy",
+    "form.feed.label.ntfy_priority": "Ntfy priority",
+    "form.feed.label.ntfy_max_priority": "Ntfy max priority",
+    "form.feed.label.ntfy_high_priority": "Ntfy high priority",
+    "form.feed.label.ntfy_default_priority": "Ntfy default priority",
+    "form.feed.label.ntfy_low_priority": "Ntfy low priority",
+    "form.feed.label.ntfy_min_priority": "Ntfy min priority",
     "form.feed.fieldset.general": "General",
     "form.feed.fieldset.rules": "Rules",
     "form.feed.fieldset.network_settings": "Network Settings",
@@ -498,6 +505,13 @@
     "form.integration.webhook_secret": "Webhook Secret",
     "form.integration.rssbridge_activate": "Check RSS-Bridge when adding subscriptions",
     "form.integration.rssbridge_url": "RSS-Bridge server URL",
+    "form.integration.ntfy_activate": "Push entries to ntfy",
+    "form.integration.ntfy_topic": "Ntfy topic",
+    "form.integration.ntfy_url": "Ntfy URL (optional, default is ntfy.sh)",
+    "form.integration.ntfy_api_token": "Ntfy API Token (optional)",
+    "form.integration.ntfy_username": "Ntfy Username (optional)",
+    "form.integration.ntfy_password": "Ntfy Password (optional)",
+    "form.integration.ntfy_icon_url": "Ntfy Icon URL (optional)",
     "form.api_key.label.description": "Etykieta klucza API",
     "form.submit.loading": "Ładowanie...",
     "form.submit.saving": "Zapisywanie...",

+ 14 - 0
internal/locale/translations/pt_BR.json

@@ -347,6 +347,13 @@
     "form.feed.label.no_media_player": "No media player (audio/video)",
     "form.feed.label.fetch_via_proxy": "Buscar via proxy",
     "form.feed.label.hide_globally": "Ocultar entradas na lista global não lida",
+    "form.feed.label.ntfy_activate": "Push entries to ntfy",
+    "form.feed.label.ntfy_priority": "Ntfy priority",
+    "form.feed.label.ntfy_max_priority": "Ntfy max priority",
+    "form.feed.label.ntfy_high_priority": "Ntfy high priority",
+    "form.feed.label.ntfy_default_priority": "Ntfy default priority",
+    "form.feed.label.ntfy_low_priority": "Ntfy low priority",
+    "form.feed.label.ntfy_min_priority": "Ntfy min priority",
     "form.feed.fieldset.general": "General",
     "form.feed.fieldset.rules": "Rules",
     "form.feed.fieldset.network_settings": "Network Settings",
@@ -488,6 +495,13 @@
     "form.integration.webhook_secret": "Webhook Secret",
     "form.integration.rssbridge_activate": "Check RSS-Bridge when adding subscriptions",
     "form.integration.rssbridge_url": "RSS-Bridge server URL",
+    "form.integration.ntfy_activate": "Push entries to ntfy",
+    "form.integration.ntfy_topic": "Ntfy topic",
+    "form.integration.ntfy_url": "Ntfy URL (optional, default is ntfy.sh)",
+    "form.integration.ntfy_api_token": "Ntfy API Token (optional)",
+    "form.integration.ntfy_username": "Ntfy Username (optional)",
+    "form.integration.ntfy_password": "Ntfy Password (optional)",
+    "form.integration.ntfy_icon_url": "Ntfy Icon URL (optional)",
     "form.api_key.label.description": "Etiqueta da chave de API",
     "form.submit.loading": "Carregando...",
     "form.submit.saving": "Salvando...",

+ 14 - 0
internal/locale/translations/ru_RU.json

@@ -357,6 +357,13 @@
     "form.feed.label.disabled": "Не обновлять эту подписку",
     "form.feed.label.no_media_player": "Отключить медиаплеер (аудио и видео)",
     "form.feed.label.hide_globally": "Скрыть записи в глобальном списке непрочитанных",
+    "form.feed.label.ntfy_activate": "Push entries to ntfy",
+    "form.feed.label.ntfy_priority": "Ntfy priority",
+    "form.feed.label.ntfy_max_priority": "Ntfy max priority",
+    "form.feed.label.ntfy_high_priority": "Ntfy high priority",
+    "form.feed.label.ntfy_default_priority": "Ntfy default priority",
+    "form.feed.label.ntfy_low_priority": "Ntfy low priority",
+    "form.feed.label.ntfy_min_priority": "Ntfy min priority",
     "form.feed.fieldset.general": "General",
     "form.feed.fieldset.rules": "Rules",
     "form.feed.fieldset.network_settings": "Network Settings",
@@ -498,6 +505,13 @@
     "form.integration.webhook_secret": "Секретный ключ для вебхуков",
     "form.integration.rssbridge_activate": "Check RSS-Bridge when adding subscriptions",
     "form.integration.rssbridge_url": "RSS-Bridge server URL",
+    "form.integration.ntfy_activate": "Push entries to ntfy",
+    "form.integration.ntfy_topic": "Ntfy topic",
+    "form.integration.ntfy_url": "Ntfy URL (optional, default is ntfy.sh)",
+    "form.integration.ntfy_api_token": "Ntfy API Token (optional)",
+    "form.integration.ntfy_username": "Ntfy Username (optional)",
+    "form.integration.ntfy_password": "Ntfy Password (optional)",
+    "form.integration.ntfy_icon_url": "Ntfy Icon URL (optional)",
     "form.api_key.label.description": "Описание API-ключа",
     "form.submit.loading": "Загрузка…",
     "form.submit.saving": "Сохранение…",

+ 14 - 0
internal/locale/translations/tr_TR.json

@@ -272,6 +272,20 @@
   "form.integration.webhook_activate": "Webhook'u etkinleştir",
   "form.integration.webhook_secret": "Webhook Secret",
   "form.integration.webhook_url": "Webhook URL",
+  "form.integration.ntfy_activate": "Push entries to ntfy",
+  "form.integration.ntfy_topic": "Ntfy topic",
+  "form.integration.ntfy_url": "Ntfy URL (optional, default is ntfy.sh)",
+  "form.integration.ntfy_api_token": "Ntfy API Token (optional)",
+  "form.integration.ntfy_username": "Ntfy Username (optional)",
+  "form.integration.ntfy_password": "Ntfy Password (optional)",
+  "form.integration.ntfy_icon_url": "Ntfy Icon URL (optional)",
+  "form.feed.label.ntfy_activate": "Push entries to ntfy",
+  "form.feed.label.ntfy_priority": "Ntfy priority",
+  "form.feed.label.ntfy_max_priority": "Ntfy max priority",
+  "form.feed.label.ntfy_high_priority": "Ntfy high priority",
+  "form.feed.label.ntfy_default_priority": "Ntfy default priority",
+  "form.feed.label.ntfy_low_priority": "Ntfy low priority",
+  "form.feed.label.ntfy_min_priority": "Ntfy min priority",
   "form.prefs.fieldset.application_settings": "Uygulama Ayarları",
   "form.prefs.fieldset.authentication_settings": "Kimlik Doğrulama Ayarları",
   "form.prefs.fieldset.reader_settings": "Okuyucu Ayarları",

+ 14 - 0
internal/locale/translations/uk_UA.json

@@ -357,6 +357,13 @@
     "form.feed.label.disabled": "Не оновлювати цю стрічку",
     "form.feed.label.no_media_player": "No media player (audio/video)",
     "form.feed.label.hide_globally": "Приховати записи в глобальному списку непрочитаного",
+    "form.feed.label.ntfy_activate": "Push entries to ntfy",
+    "form.feed.label.ntfy_priority": "Ntfy priority",
+    "form.feed.label.ntfy_max_priority": "Ntfy max priority",
+    "form.feed.label.ntfy_high_priority": "Ntfy high priority",
+    "form.feed.label.ntfy_default_priority": "Ntfy default priority",
+    "form.feed.label.ntfy_low_priority": "Ntfy low priority",
+    "form.feed.label.ntfy_min_priority": "Ntfy min priority",
     "form.category.label.title": "Назва",
     "form.category.hide_globally": "Приховати записи в глобальному списку непрочитаного",
     "form.feed.fieldset.general": "General",
@@ -498,6 +505,13 @@
     "form.integration.webhook_secret": "Webhook Secret",
     "form.integration.rssbridge_activate": "Check RSS-Bridge when adding subscriptions",
     "form.integration.rssbridge_url": "RSS-Bridge server URL",
+    "form.integration.ntfy_activate": "Push entries to ntfy",
+    "form.integration.ntfy_topic": "Ntfy topic",
+    "form.integration.ntfy_url": "Ntfy URL (optional, default is ntfy.sh)",
+    "form.integration.ntfy_api_token": "Ntfy API Token (optional)",
+    "form.integration.ntfy_username": "Ntfy Username (optional)",
+    "form.integration.ntfy_password": "Ntfy Password (optional)",
+    "form.integration.ntfy_icon_url": "Ntfy Icon URL (optional)",
     "form.api_key.label.description": "Назва ключа API",
     "form.submit.loading": "Завантаження...",
     "form.submit.saving": "Зберігаю...",

+ 14 - 0
internal/locale/translations/zh_CN.json

@@ -337,6 +337,13 @@
     "form.feed.label.disabled": "请勿刷新此源",
     "form.feed.label.no_media_player": "没有媒体播放器(音频/视频)",
     "form.feed.label.hide_globally": "隐藏全局未读列表中的文章",
+    "form.feed.label.ntfy_activate": "Push entries to ntfy",
+    "form.feed.label.ntfy_priority": "Ntfy priority",
+    "form.feed.label.ntfy_max_priority": "Ntfy max priority",
+    "form.feed.label.ntfy_high_priority": "Ntfy high priority",
+    "form.feed.label.ntfy_default_priority": "Ntfy default priority",
+    "form.feed.label.ntfy_low_priority": "Ntfy low priority",
+    "form.feed.label.ntfy_min_priority": "Ntfy min priority",
     "form.feed.fieldset.general": "通用",
     "form.feed.fieldset.rules": "规则",
     "form.feed.fieldset.network_settings": "网络设置",
@@ -478,6 +485,13 @@
     "form.integration.webhook_secret": "Webhook 密钥",
     "form.integration.rssbridge_activate": "添加订阅时检查 RSS-Bridge",
     "form.integration.rssbridge_url": "RSS-Bridge 服务器 URL",
+    "form.integration.ntfy_activate": "Push entries to ntfy",
+    "form.integration.ntfy_topic": "Ntfy topic",
+    "form.integration.ntfy_url": "Ntfy URL (optional, default is ntfy.sh)",
+    "form.integration.ntfy_api_token": "Ntfy API Token (optional)",
+    "form.integration.ntfy_username": "Ntfy Username (optional)",
+    "form.integration.ntfy_password": "Ntfy Password (optional)",
+    "form.integration.ntfy_icon_url": "Ntfy Icon URL (optional)",
     "form.api_key.label.description": "API密钥标签",
     "form.submit.loading": "载入中…",
     "form.submit.saving": "保存中…",

+ 14 - 0
internal/locale/translations/zh_TW.json

@@ -337,6 +337,13 @@
     "form.feed.label.disabled": "請勿更新此 Feed",
     "form.feed.label.no_media_player": "沒有媒體播放器(音訊/視訊)",
     "form.feed.label.hide_globally": "隱藏全域性未讀列表中的文章",
+    "form.feed.label.ntfy_activate": "Push entries to ntfy",
+    "form.feed.label.ntfy_priority": "Ntfy priority",
+    "form.feed.label.ntfy_max_priority": "Ntfy max priority",
+    "form.feed.label.ntfy_high_priority": "Ntfy high priority",
+    "form.feed.label.ntfy_default_priority": "Ntfy default priority",
+    "form.feed.label.ntfy_low_priority": "Ntfy low priority",
+    "form.feed.label.ntfy_min_priority": "Ntfy min priority",
     "form.feed.fieldset.general": "通用",
     "form.feed.fieldset.rules": "規則",
     "form.feed.fieldset.network_settings": "網路設定",
@@ -478,6 +485,13 @@
     "form.integration.webhook_secret": "Webhook Secret",
     "form.integration.rssbridge_activate": "新增訂閱時檢查 RSS-Bridge",
     "form.integration.rssbridge_url": "RSS-Bridge 伺服器的 URL",
+    "form.integration.ntfy_activate": "Push entries to ntfy",
+    "form.integration.ntfy_topic": "Ntfy topic",
+    "form.integration.ntfy_url": "Ntfy URL (optional, default is ntfy.sh)",
+    "form.integration.ntfy_api_token": "Ntfy API Token (optional)",
+    "form.integration.ntfy_username": "Ntfy Username (optional)",
+    "form.integration.ntfy_password": "Ntfy Password (optional)",
+    "form.integration.ntfy_icon_url": "Ntfy Icon URL (optional)",
     "form.api_key.label.description": "API金鑰標籤",
     "form.submit.loading": "載入中…",
     "form.submit.saving": "儲存中…",

+ 3 - 1
internal/model/feed.go

@@ -51,8 +51,10 @@ type Feed struct {
 	AllowSelfSignedCertificates bool      `json:"allow_self_signed_certificates"`
 	FetchViaProxy               bool      `json:"fetch_via_proxy"`
 	HideGlobally                bool      `json:"hide_globally"`
-	AppriseServiceURLs          string    `json:"apprise_service_urls"`
 	DisableHTTP2                bool      `json:"disable_http2"`
+	AppriseServiceURLs          string    `json:"apprise_service_urls"`
+	NtfyEnabled                 bool      `json:"ntfy_enabled"`
+	NtfyPriority                int       `json:"ntfy_priority"`
 
 	// Non persisted attributes
 	Category *Category `json:"category,omitempty"`

+ 7 - 0
internal/model/integration.go

@@ -97,4 +97,11 @@ type Integration struct {
 	RaindropToken                    string
 	RaindropCollectionID             string
 	RaindropTags                     string
+	NtfyEnabled                      bool
+	NtfyTopic                        string
+	NtfyURL                          string
+	NtfyAPIToken                     string
+	NtfyUsername                     string
+	NtfyPassword                     string
+	NtfyIconURL                      string
 }

+ 6 - 2
internal/storage/feed.go

@@ -347,9 +347,11 @@ func (s *Storage) UpdateFeed(feed *model.Feed) (err error) {
 			no_media_player=$26,
 			apprise_service_urls=$27,
 			disable_http2=$28,
-			description=$29
+			description=$29,
+			ntfy_enabled=$30,
+			ntfy_priority=$31
 		WHERE
-			id=$30 AND user_id=$31
+			id=$32 AND user_id=$33
 	`
 	_, err = s.db.Exec(query,
 		feed.FeedURL,
@@ -381,6 +383,8 @@ func (s *Storage) UpdateFeed(feed *model.Feed) (err error) {
 		feed.AppriseServiceURLs,
 		feed.DisableHTTP2,
 		feed.Description,
+		feed.NtfyEnabled,
+		feed.NtfyPriority,
 		feed.ID,
 		feed.UserID,
 	)

+ 5 - 1
internal/storage/feed_query_builder.go

@@ -165,7 +165,9 @@ func (f *FeedQueryBuilder) GetFeeds() (model.Feeds, error) {
 			fi.icon_id,
 			u.timezone,
 			f.apprise_service_urls,
-			f.disable_http2
+			f.disable_http2,
+			f.ntfy_enabled,
+			f.ntfy_priority
 		FROM
 			feeds f
 		LEFT JOIN
@@ -234,6 +236,8 @@ func (f *FeedQueryBuilder) GetFeeds() (model.Feeds, error) {
 			&tz,
 			&feed.AppriseServiceURLs,
 			&feed.DisableHTTP2,
+			&feed.NtfyEnabled,
+			&feed.NtfyPriority,
 		)
 
 		if err != nil {

+ 31 - 3
internal/storage/integration.go

@@ -200,7 +200,14 @@ func (s *Storage) Integration(userID int64) (*model.Integration, error) {
 			raindrop_tags,
 			betula_enabled,
 			betula_url,
-			betula_token
+			betula_token,
+			ntfy_enabled,
+			ntfy_topic,
+			ntfy_url,
+			ntfy_api_token,
+			ntfy_username,
+			ntfy_password,
+			ntfy_icon_url
 		FROM
 			integrations
 		WHERE
@@ -300,6 +307,13 @@ func (s *Storage) Integration(userID int64) (*model.Integration, error) {
 		&integration.BetulaEnabled,
 		&integration.BetulaURL,
 		&integration.BetulaToken,
+		&integration.NtfyEnabled,
+		&integration.NtfyTopic,
+		&integration.NtfyURL,
+		&integration.NtfyAPIToken,
+		&integration.NtfyUsername,
+		&integration.NtfyPassword,
+		&integration.NtfyIconURL,
 	)
 	switch {
 	case err == sql.ErrNoRows:
@@ -407,9 +421,16 @@ func (s *Storage) UpdateIntegration(integration *model.Integration) error {
 			raindrop_tags=$88,
 			betula_enabled=$89,
 			betula_url=$90,
-			betula_token=$91
+			betula_token=$91,
+			ntfy_enabled=$92,
+			ntfy_topic=$93,
+			ntfy_url=$94,
+			ntfy_api_token=$95,
+			ntfy_username=$96,
+			ntfy_password=$97,
+			ntfy_icon_url=$98
 		WHERE
-			user_id=$92
+			user_id=$99
 	`
 	_, err := s.db.Exec(
 		query,
@@ -504,6 +525,13 @@ func (s *Storage) UpdateIntegration(integration *model.Integration) error {
 		integration.BetulaEnabled,
 		integration.BetulaURL,
 		integration.BetulaToken,
+		integration.NtfyEnabled,
+		integration.NtfyTopic,
+		integration.NtfyURL,
+		integration.NtfyAPIToken,
+		integration.NtfyUsername,
+		integration.NtfyPassword,
+		integration.NtfyIconURL,
 		integration.UserID,
 	)
 

+ 33 - 10
internal/template/templates/views/edit_feed.html

@@ -142,7 +142,7 @@
                     {{ t "form.feed.label.blocklist_rules" }}
                 </label>
                 &nbsp;
-                <a href=" https://miniflux.app/docs/rules.html#filtering-rules" target="_blank">
+                <a href="https://miniflux.app/docs/rules.html#filtering-rules" target="_blank">
                     {{ icon "external-link" }}
                 </a>
             </div>
@@ -153,7 +153,7 @@
                     {{ t "form.feed.label.keeplist_rules" }}
                 </label>
                 &nbsp;
-                <a href=" https://miniflux.app/docs/rules.html#filtering-rules" target="_blank">
+                <a href="https://miniflux.app/docs/rules.html#filtering-rules" target="_blank">
                     {{ icon "external-link" }}
                 </a>
             </div>
@@ -164,7 +164,7 @@
                     {{ t "form.feed.label.urlrewrite_rules" }}
                 </label>
                 &nbsp;
-                <a href=" https://miniflux.app/docs/rules.html#rewriteurl-rules" target="_blank">
+                <a href="https://miniflux.app/docs/rules.html#rewriteurl-rules" target="_blank">
                     {{ icon "external-link" }}
                 </a>
             </div>
@@ -178,13 +178,36 @@
         <fieldset>
             <legend>{{ t "form.feed.fieldset.integration" }}</legend>
 
-            <div class="form-label-row">
-                <label for="form-apprise-service-urls">
-                    {{ t "form.feed.label.apprise_service_urls" }}
-                </label>
-                &nbsp;
-            </div>
-            <input type="text" name="apprise_service_urls" id="form-apprise-service-urls" value="{{ .form.AppriseServiceURLs }}" spellcheck="false">
+            <details>
+                <summary>Apprise</summary>
+                <div class="form-label-row">
+                    <label for="form-apprise-service-urls">
+                        {{ t "form.feed.label.apprise_service_urls" }}
+                    </label>
+                </div>
+                <input type="text" name="apprise_service_urls" id="form-apprise-service-urls" value="{{ .form.AppriseServiceURLs }}" spellcheck="false">
+            </details>
+
+            <details>
+                <summary>Ntfy</summary>
+                <label><input type="checkbox" name="ntfy_enabled" value="1" {{ if .form.NtfyEnabled }}checked{{ end }}> {{ t "form.feed.label.ntfy_activate" }}</label>
+                <div class="form-label-row">
+                    <label for="form-ntfy-priority">
+                        {{ t "form.feed.label.ntfy_priority" }}
+                    </label>
+                    &nbsp;
+                    <a href="https://docs.ntfy.sh/publish/#message-priority" target="_blank">
+                        {{ icon "external-link" }}
+                    </a>
+                </div>
+                <select id="form-ntfy-priority" name="ntfy_priority">
+                    <option value="5" {{ if eq .form.NtfyPriority 5 }}selected{{ end }}>5 - {{ t "form.feed.label.ntfy_max_priority" }}</option>
+                    <option value="4" {{ if eq .form.NtfyPriority 4 }}selected{{ end }}>4 - {{ t "form.feed.label.ntfy_high_priority" }}</option>
+                    <option value="3" {{ if eq .form.NtfyPriority 3 }}selected{{ end }}>3 - {{ t "form.feed.label.ntfy_default_priority" }}</option>
+                    <option value="2" {{ if eq .form.NtfyPriority 2 }}selected{{ end }}>2 - {{ t "form.feed.label.ntfy_low_priority" }}</option>
+                    <option value="1" {{ if eq .form.NtfyPriority 1 }}selected{{ end }}>1 - {{ t "form.feed.label.ntfy_min_priority" }}</option>
+                </select>
+            </details>
 
             <div class="buttons">
                 <button type="submit" class="button button-primary" data-label-loading="{{ t "form.submit.saving" }}">{{ t "action.update" }}</button>

+ 31 - 0
internal/template/templates/views/integrations.html

@@ -259,6 +259,37 @@
         </div>
     </details>
 
+    <details {{ if .form.NtfyEnabled }}open{{ end }}>
+        <summary>Ntfy</summary>
+        <div class="form-section">
+            <label>
+                <input type="checkbox" name="ntfy_enabled" value="1" {{ if .form.NtfyEnabled }}checked{{ end }}> {{ t "form.integration.ntfy_activate" }}
+            </label>
+
+            <label for="form-ntfy-topic">{{ t "form.integration.ntfy_topic" }}</label>
+            <input type="text" name="ntfy_topic" id="form-ntfy-topic" value="{{ .form.NtfyTopic }}" spellcheck="false">
+
+            <label for="form-ntfy-url">{{ t "form.integration.ntfy_url" }}</label>
+            <input type="url" name="ntfy_url" id="form-ntfy-url" value="{{ .form.NtfyURL }}" placeholder="https://ntfy.sh" spellcheck="false">
+
+            <label for="form-ntfy-api-token">{{ t "form.integration.ntfy_api_token" }}</label>
+            <input type="text" name="ntfy_api_token" id="form-ntfy-api-token" value="{{ .form.NtfyAPIToken }}" spellcheck="false">
+
+            <label for="form-ntfy-username">{{ t "form.integration.ntfy_username" }}</label>
+            <input type="text" name="ntfy_username" id="form-ntfy-username" value="{{ .form.NtfyUsername }}" spellcheck="false">
+
+            <label for="form-ntfy-password">{{ t "form.integration.ntfy_password" }}</label>
+            <input type="text" name="ntfy_password" id="form-ntfy-password" value="{{ .form.NtfyPassword }}" spellcheck="false">
+
+            <label for="form-ntfy-icon-url">{{ t "form.integration.ntfy_icon_url" }}</label>
+            <input type="url" name="ntfy_icon_url" id="form-ntfy-icon-url" value="{{ .form.NtfyIconURL }}" spellcheck="false">
+
+            <div class="buttons">
+                <button type="submit" class="button button-primary" data-label-loading="{{ t "form.submit.saving" }}">{{ t "action.update" }}</button>
+            </div>
+        </div>
+    </details>
+
     <details {{ if .form.NunuxKeeperEnabled }}open{{ end }}>
         <summary>Nunux Keeper</summary>
         <div class="form-section">

+ 2 - 0
internal/ui/feed_edit.go

@@ -64,6 +64,8 @@ func (h *handler) showEditFeedPage(w http.ResponseWriter, r *http.Request) {
 		CategoryHidden:              feed.Category.HideGlobally,
 		AppriseServiceURLs:          feed.AppriseServiceURLs,
 		DisableHTTP2:                feed.DisableHTTP2,
+		NtfyEnabled:                 feed.NtfyEnabled,
+		NtfyPriority:                feed.NtfyPriority,
 	}
 
 	sess := session.New(h.store, request.SessionID(r))

+ 10 - 0
internal/ui/form/feed.go

@@ -36,6 +36,8 @@ type FeedForm struct {
 	CategoryHidden              bool // Category has "hide_globally"
 	AppriseServiceURLs          string
 	DisableHTTP2                bool
+	NtfyEnabled                 bool
+	NtfyPriority                int
 }
 
 // Merge updates the fields of the given feed.
@@ -65,6 +67,8 @@ func (f FeedForm) Merge(feed *model.Feed) *model.Feed {
 	feed.HideGlobally = f.HideGlobally
 	feed.AppriseServiceURLs = f.AppriseServiceURLs
 	feed.DisableHTTP2 = f.DisableHTTP2
+	feed.NtfyEnabled = f.NtfyEnabled
+	feed.NtfyPriority = f.NtfyPriority
 	return feed
 }
 
@@ -74,6 +78,10 @@ func NewFeedForm(r *http.Request) *FeedForm {
 	if err != nil {
 		categoryID = 0
 	}
+	ntfyPriority, err := strconv.Atoi(r.FormValue("ntfy_priority"))
+	if err != nil {
+		ntfyPriority = 0
+	}
 	return &FeedForm{
 		FeedURL:                     r.FormValue("feed_url"),
 		SiteURL:                     r.FormValue("site_url"),
@@ -98,5 +106,7 @@ func NewFeedForm(r *http.Request) *FeedForm {
 		HideGlobally:                r.FormValue("hide_globally") == "1",
 		AppriseServiceURLs:          r.FormValue("apprise_service_urls"),
 		DisableHTTP2:                r.FormValue("disable_http2") == "1",
+		NtfyEnabled:                 r.FormValue("ntfy_enabled") == "1",
+		NtfyPriority:                ntfyPriority,
 	}
 }

+ 21 - 0
internal/ui/form/integration.go

@@ -103,6 +103,13 @@ type IntegrationForm struct {
 	BetulaEnabled                    bool
 	BetulaURL                        string
 	BetulaToken                      string
+	NtfyEnabled                      bool
+	NtfyTopic                        string
+	NtfyURL                          string
+	NtfyAPIToken                     string
+	NtfyUsername                     string
+	NtfyPassword                     string
+	NtfyIconURL                      string
 }
 
 // Merge copy form values to the model.
@@ -195,6 +202,13 @@ func (i IntegrationForm) Merge(integration *model.Integration) {
 	integration.BetulaEnabled = i.BetulaEnabled
 	integration.BetulaURL = i.BetulaURL
 	integration.BetulaToken = i.BetulaToken
+	integration.NtfyEnabled = i.NtfyEnabled
+	integration.NtfyTopic = i.NtfyTopic
+	integration.NtfyURL = i.NtfyURL
+	integration.NtfyAPIToken = i.NtfyAPIToken
+	integration.NtfyUsername = i.NtfyUsername
+	integration.NtfyPassword = i.NtfyPassword
+	integration.NtfyIconURL = i.NtfyIconURL
 }
 
 // NewIntegrationForm returns a new IntegrationForm.
@@ -290,6 +304,13 @@ func NewIntegrationForm(r *http.Request) *IntegrationForm {
 		BetulaEnabled:                    r.FormValue("betula_enabled") == "1",
 		BetulaURL:                        r.FormValue("betula_url"),
 		BetulaToken:                      r.FormValue("betula_token"),
+		NtfyEnabled:                      r.FormValue("ntfy_enabled") == "1",
+		NtfyTopic:                        r.FormValue("ntfy_topic"),
+		NtfyURL:                          r.FormValue("ntfy_url"),
+		NtfyAPIToken:                     r.FormValue("ntfy_api_token"),
+		NtfyUsername:                     r.FormValue("ntfy_username"),
+		NtfyPassword:                     r.FormValue("ntfy_password"),
+		NtfyIconURL:                      r.FormValue("ntfy_icon_url"),
 	}
 }
 

+ 7 - 0
internal/ui/integration_show.go

@@ -117,6 +117,13 @@ func (h *handler) showIntegrationPage(w http.ResponseWriter, r *http.Request) {
 		BetulaEnabled:                    integration.BetulaEnabled,
 		BetulaURL:                        integration.BetulaURL,
 		BetulaToken:                      integration.BetulaToken,
+		NtfyEnabled:                      integration.NtfyEnabled,
+		NtfyTopic:                        integration.NtfyTopic,
+		NtfyURL:                          integration.NtfyURL,
+		NtfyAPIToken:                     integration.NtfyAPIToken,
+		NtfyUsername:                     integration.NtfyUsername,
+		NtfyPassword:                     integration.NtfyPassword,
+		NtfyIconURL:                      integration.NtfyIconURL,
 	}
 
 	sess := session.New(h.store, request.SessionID(r))