Repository: miniflux/v2 (language: go)

Issue:
ntfy integration: link to miniflux not to source site
The [ntfy integration of miniflux](https://miniflux.app/docs/ntfy.html) sends a notification when there are new entries. The notification includes one link to the original entry, so when you select the notification, you jump with the browser to the destination original entry. I would like that link of the notification to point to the miniflux entry in order to read the entry inside miniflux and to mark the entry as read. Or at least it could be another configuration option of the [ntfy integration](https://miniflux.app/docs/ntfy.html): the link of the notification to point to the original message or to point to the entry inside miniflux.


Discussion / hints:

Fix patch:
diff --git a/internal/database/migrations.go b/internal/database/migrations.go
index 0e8e7372e62..6791d817fcf 100644
--- a/internal/database/migrations.go
+++ b/internal/database/migrations.go
@@ -977,4 +977,9 @@ var migrations = []func(tx *sql.Tx, driver string) error{
 		_, err = tx.Exec(sql)
 		return err
 	},
+	func(tx *sql.Tx, _ string) (err error) {
+		sql := `ALTER TABLE integrations ADD COLUMN ntfy_internal_links bool default 'f';`
+		_, err = tx.Exec(sql)
+		return err
+	},
 }
diff --git a/internal/integration/integration.go b/internal/integration/integration.go
index e363af9ee90..ebca70afb10 100644
--- a/internal/integration/integration.go
+++ b/internal/integration/integration.go
@@ -506,6 +506,7 @@ func PushEntries(feed *model.Feed, entries model.Entries, userIntegrations *mode
 			userIntegrations.NtfyUsername,
 			userIntegrations.NtfyPassword,
 			userIntegrations.NtfyIconURL,
+			userIntegrations.NtfyInternalLinks,
 			feed.NtfyPriority,
 		)
 
diff --git a/internal/integration/ntfy/ntfy.go b/internal/integration/ntfy/ntfy.go
index 04fd978a88c..92f906f095d 100644
--- a/internal/integration/ntfy/ntfy.go
+++ b/internal/integration/ntfy/ntfy.go
@@ -9,8 +9,10 @@ import (
 	"fmt"
 	"log/slog"
 	"net/http"
+	"net/url"
 	"time"
 
+	"miniflux.app/v2/internal/config"
 	"miniflux.app/v2/internal/model"
 	"miniflux.app/v2/internal/version"
 )
@@ -22,14 +24,15 @@ const (
 
 type Client struct {
 	ntfyURL, ntfyTopic, ntfyApiToken, ntfyUsername, ntfyPassword, ntfyIconURL string
+	ntfyInternalLinks                                                         bool
 	ntfyPriority                                                              int
 }
 
-func NewClient(ntfyURL, ntfyTopic, ntfyApiToken, ntfyUsername, ntfyPassword, ntfyIconURL string, ntfyPriority int) *Client {
+func NewClient(ntfyURL, ntfyTopic, ntfyApiToken, ntfyUsername, ntfyPassword, ntfyIconURL string, ntfyInternalLinks bool, ntfyPriority int) *Client {
 	if ntfyURL == "" {
 		ntfyURL = defaultNtfyURL
 	}
-	return &Client{ntfyURL, ntfyTopic, ntfyApiToken, ntfyUsername, ntfyPassword, ntfyIconURL, ntfyPriority}
+	return &Client{ntfyURL, ntfyTopic, ntfyApiToken, ntfyUsername, ntfyPassword, ntfyIconURL, ntfyInternalLinks, ntfyPriority}
 }
 
 func (c *Client) SendMessages(feed *model.Feed, entries model.Entries) error {
@@ -46,12 +49,21 @@ func (c *Client) SendMessages(feed *model.Feed, entries model.Entries) error {
 			ntfyMessage.Icon = c.ntfyIconURL
 		}
 
+		if c.ntfyInternalLinks {
+			url, err := url.Parse(config.Opts.BaseURL())
+			if err != nil {
+				slog.Error("Unable to parse base URL", slog.Any("error", err))
+			} else {
+				ntfyMessage.Click = fmt.Sprintf("%s%s%d", url, "/unread/entry/", entry.ID)
+			}
+		}
+
 		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),
+			slog.String("entry_url", ntfyMessage.Click),
 		)
 
 		if err := c.makeRequest(ntfyMessage); err != nil {
diff --git a/internal/locale/translations/de_DE.json b/internal/locale/translations/de_DE.json
index 2bce3778544..1dac9f8a6d8 100644
--- a/internal/locale/translations/de_DE.json
+++ b/internal/locale/translations/de_DE.json
@@ -513,6 +513,7 @@
     "form.integration.ntfy_username": "Ntfy-Benutzername (optional)",
     "form.integration.ntfy_password": "Ntfy-Passwort (optional)",
     "form.integration.ntfy_icon_url": "Ntfy-Symbol-URL (optional)",
+    "form.integration.ntfy_internal_links": "Use internal links on click (optional)",
     "form.integration.discord_activate": "Einträge zu Discord pushen",
     "form.integration.discord_webhook_link": "Discord-Webhook-URL",
     "form.api_key.label.description": "API-Schlüsselbezeichnung",
diff --git a/internal/locale/translations/el_EL.json b/internal/locale/translations/el_EL.json
index 4e522e77d39..efb60e77933 100644
--- a/internal/locale/translations/el_EL.json
+++ b/internal/locale/translations/el_EL.json
@@ -513,6 +513,7 @@
     "form.integration.ntfy_username": "Ntfy Username (optional)",
     "form.integration.ntfy_password": "Ntfy Password (optional)",
     "form.integration.ntfy_icon_url": "Ntfy Icon URL (optional)",
+    "form.integration.ntfy_internal_links": "Use internal links on click (optional)",
     "form.integration.discord_activate": "Push entries to Discord",
     "form.integration.discord_webhook_link": "Discord Webhook link",
     "form.api_key.label.description": "Ετικέτα κλειδιού API",
diff --git a/internal/locale/translations/en_US.json b/internal/locale/translations/en_US.json
index 4faa30ebb10..5376cc0b40e 100644
--- a/internal/locale/translations/en_US.json
+++ b/internal/locale/translations/en_US.json
@@ -511,6 +511,7 @@
     "form.integration.ntfy_username": "Ntfy Username (optional)",
     "form.integration.ntfy_password": "Ntfy Password (optional)",
     "form.integration.ntfy_icon_url": "Ntfy Icon URL (optional)",
+    "form.integration.ntfy_internal_links": "Use internal links on click (optional)",
     "form.integration.cubox_activate": "Save entries to Cubox",
     "form.integration.cubox_api_link": "Cubox API link",
     "form.integration.discord_activate": "Push entries to Discord",
diff --git a/internal/locale/translations/es_ES.json b/internal/locale/translations/es_ES.json
index 39cfad2d7cd..f89506a3cd4 100644
--- a/internal/locale/translations/es_ES.json
+++ b/internal/locale/translations/es_ES.json
@@ -513,6 +513,7 @@
     "form.integration.ntfy_username": "Nombre de usuario de Ntfy (opcional)",
     "form.integration.ntfy_password": "Contraseña de Ntfy (opcional)",
     "form.integration.ntfy_icon_url": "URL del icono de Ntfy (opcional)",
+    "form.integration.ntfy_internal_links": "Use internal links on click (optional)",
     "form.integration.discord_activate": "Enviar artículos a Discord",
     "form.integration.discord_webhook_link": "URL de la Webhook de Discord",
     "form.api_key.label.description": "Etiqueta de clave API",
diff --git a/internal/locale/translations/fi_FI.json b/internal/locale/translations/fi_FI.json
index 157e4240f14..7626a33e6ec 100644
--- a/internal/locale/translations/fi_FI.json
+++ b/internal/locale/translations/fi_FI.json
@@ -513,6 +513,7 @@
     "form.integration.ntfy_username": "Ntfy Username (optional)",
     "form.integration.ntfy_password": "Ntfy Password (optional)",
     "form.integration.ntfy_icon_url": "Ntfy Icon URL (optional)",
+    "form.integration.ntfy_internal_links": "Use internal links on click (optional)",
     "form.integration.discord_activate": "Push entries to Discord",
     "form.integration.discord_webhook_link": "Discord Webhook link",
     "form.api_key.label.description": "API Key Label",
diff --git a/internal/locale/translations/fr_FR.json b/internal/locale/translations/fr_FR.json
index c3f50d607cf..34e0bf7eab2 100644
--- a/internal/locale/translations/fr_FR.json
+++ b/internal/locale/translations/fr_FR.json
@@ -513,6 +513,7 @@
     "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.integration.ntfy_internal_links": "Use internal links on click (optional)",
     "form.integration.discord_activate": "Envoyer les articles vers Discord",
     "form.integration.discord_webhook_link": "URL du Webhook Discord",
     "form.api_key.label.description": "Libellé de la clé d'API",
diff --git a/internal/locale/translations/hi_IN.json b/internal/locale/translations/hi_IN.json
index f4cadcd6e52..ad2c37893ba 100644
--- a/internal/locale/translations/hi_IN.json
+++ b/internal/locale/translations/hi_IN.json
@@ -513,6 +513,7 @@
     "form.integration.ntfy_username": "Ntfy Username (optional)",
     "form.integration.ntfy_password": "Ntfy Password (optional)",
     "form.integration.ntfy_icon_url": "Ntfy Icon URL (optional)",
+    "form.integration.ntfy_internal_links": "Use internal links on click (optional)",
     "form.integration.discord_activate": "Push entries to Discord",
     "form.integration.discord_webhook_link": "Discord Webhook link",
     "form.api_key.label.description": "एपीआई कुंजी लेबल",
diff --git a/internal/locale/translations/id_ID.json b/internal/locale/translations/id_ID.json
index 5434ec94831..ef4c19d4fb8 100644
--- a/internal/locale/translations/id_ID.json
+++ b/internal/locale/translations/id_ID.json
@@ -503,6 +503,7 @@
     "form.integration.ntfy_username": "Ntfy Username (optional)",
     "form.integration.ntfy_password": "Ntfy Password (optional)",
     "form.integration.ntfy_icon_url": "Ntfy Icon URL (optional)",
+    "form.integration.ntfy_internal_links": "Use internal links on click (optional)",
     "form.integration.discord_activate": "Push entries to Discord",
     "form.integration.discord_webhook_link": "Discord Webhook link",
     "form.api_key.label.description": "Label Kunci API",
diff --git a/internal/locale/translations/it_IT.json b/internal/locale/translations/it_IT.json
index 757449e1e0f..447fd4e49b8 100644
--- a/internal/locale/translations/it_IT.json
+++ b/internal/locale/translations/it_IT.json
@@ -514,6 +514,7 @@
     "form.integration.ntfy_username": "Ntfy Username (optional)",
     "form.integration.ntfy_password": "Ntfy Password (optional)",
     "form.integration.ntfy_icon_url": "Ntfy Icon URL (optional)",
+    "form.integration.ntfy_internal_links": "Use internal links on click (optional)",
     "form.integration.discord_activate": "Push entries to Discord",
     "form.integration.discord_webhook_link": "Discord Webhook link",
     "form.submit.loading": "Caricamento in corso...",
diff --git a/internal/locale/translations/ja_JP.json b/internal/locale/translations/ja_JP.json
index 8053cbf649d..30ed0a15dee 100644
--- a/internal/locale/translations/ja_JP.json
+++ b/internal/locale/translations/ja_JP.json
@@ -503,6 +503,7 @@
     "form.integration.ntfy_username": "Ntfy Username (optional)",
     "form.integration.ntfy_password": "Ntfy Password (optional)",
     "form.integration.ntfy_icon_url": "Ntfy Icon URL (optional)",
+    "form.integration.ntfy_internal_links": "Use internal links on click (optional)",
     "form.integration.discord_activate": "Push entries to Discord",
     "form.integration.discord_webhook_link": "Discord Webhook link",
     "form.api_key.label.description": "API キーラベル",
diff --git a/internal/locale/translations/nl_NL.json b/internal/locale/translations/nl_NL.json
index a97ad227418..6da0548d43a 100644
--- a/internal/locale/translations/nl_NL.json
+++ b/internal/locale/translations/nl_NL.json
@@ -513,6 +513,7 @@
     "form.integration.ntfy_username": "Ntfy gebruikersnaam (optioneel)",
     "form.integration.ntfy_password": "Ntfy wachtwoord (optioneel)",
     "form.integration.ntfy_icon_url": "Ntfy Icon URL (optioneel)",
+    "form.integration.ntfy_internal_links": "Use internal links on click (optional)",
     "form.integration.discord_activate": "Artikelen opslaan in Discord",
     "form.integration.discord_webhook_link": "Discord Webhook link",
     "form.api_key.label.description": "API-sleutel omschrijving",
diff --git a/internal/locale/translations/pl_PL.json b/internal/locale/translations/pl_PL.json
index b6aaae87d84..fc9cd95ffae 100644
--- a/internal/locale/translations/pl_PL.json
+++ b/internal/locale/translations/pl_PL.json
@@ -523,6 +523,7 @@
     "form.integration.ntfy_username": "Login do ntfy (opcjonalny)",
     "form.integration.ntfy_password": "Hasło do ntfy (opcjonalne)",
     "form.integration.ntfy_icon_url": "Adres URL ikony ntfy (opcjonalny)",
+    "form.integration.ntfy_internal_links": "Use internal links on click (optional)",
     "form.integration.discord_activate": "Przesyłaj wpisy do Discord",
     "form.integration.discord_webhook_link": "Adres URL Webhook Discord",
     "form.api_key.label.description": "Etykieta klucza API",
diff --git a/internal/locale/translations/pt_BR.json b/internal/locale/translations/pt_BR.json
index a7a3596b261..e8a71b5af62 100644
--- a/internal/locale/translations/pt_BR.json
+++ b/internal/locale/translations/pt_BR.json
@@ -513,6 +513,7 @@
     "form.integration.ntfy_username": "Ntfy Username (optional)",
     "form.integration.ntfy_password": "Ntfy Password (optional)",
     "form.integration.ntfy_icon_url": "Ntfy Icon URL (optional)",
+    "form.integration.ntfy_internal_links": "Use internal links on click (optional)",
     "form.integration.discord_activate": "Push entries to Discord",
     "form.integration.discord_webhook_link": "Discord Webhook link",
     "form.api_key.label.description": "Etiqueta da chave de API",
diff --git a/internal/locale/translations/ru_RU.json b/internal/locale/translations/ru_RU.json
index e9256862c18..2bd6d0d1b7b 100644
--- a/internal/locale/translations/ru_RU.json
+++ b/internal/locale/translations/ru_RU.json
@@ -523,6 +523,7 @@
     "form.integration.ntfy_username": "Ntfy Username (optional)",
     "form.integration.ntfy_password": "Ntfy Password (optional)",
     "form.integration.ntfy_icon_url": "Ntfy Icon URL (optional)",
+    "form.integration.ntfy_internal_links": "Use internal links on click (optional)",
     "form.integration.discord_activate": "Отправить статьи в Discord",
     "form.integration.discord_webhook_link": "Ссылка на Discord Webhook",
     "form.api_key.label.description": "Описание API-ключа",
diff --git a/internal/locale/translations/tr_TR.json b/internal/locale/translations/tr_TR.json
index eac7ee9ee41..4d31b4fb1cb 100644
--- a/internal/locale/translations/tr_TR.json
+++ b/internal/locale/translations/tr_TR.json
@@ -279,6 +279,7 @@
   "form.integration.ntfy_username": "Ntfy Username (optional)",
   "form.integration.ntfy_password": "Ntfy Password (optional)",
   "form.integration.ntfy_icon_url": "Ntfy Icon URL (optional)",
+  "form.integration.ntfy_internal_links": "Use internal links on click (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",
diff --git a/internal/locale/translations/uk_UA.json b/internal/locale/translations/uk_UA.json
index a82aa7d1fd5..ca44859134c 100644
--- a/internal/locale/translations/uk_UA.json
+++ b/internal/locale/translations/uk_UA.json
@@ -523,6 +523,7 @@
     "form.integration.ntfy_username": "Ntfy Username (optional)",
     "form.integration.ntfy_password": "Ntfy Password (optional)",
     "form.integration.ntfy_icon_url": "Ntfy Icon URL (optional)",
+    "form.integration.ntfy_internal_links": "Use internal links on click (optional)",
     "form.integration.discord_activate": "Push entries to Discord",
     "form.integration.discord_webhook_link": "Discord Webhook link",
     "form.api_key.label.description": "Назва ключа API",
diff --git a/internal/locale/translations/zh_CN.json b/internal/locale/translations/zh_CN.json
index ccf3daab822..08e4f80c0b8 100644
--- a/internal/locale/translations/zh_CN.json
+++ b/internal/locale/translations/zh_CN.json
@@ -503,6 +503,7 @@
     "form.integration.ntfy_username": "Ntfy用户名（可选）",
     "form.integration.ntfy_password": "Ntfy密码（可选）",
     "form.integration.ntfy_icon_url": "Ntfy图标URL（可选）",
+    "form.integration.ntfy_internal_links": "Use internal links on click (optional)",
     "form.integration.discord_activate": "将新文章推送到 Discord",
     "form.integration.discord_webhook_link": "Discord Webhook link",
     "form.api_key.label.description": "API密钥标签",
diff --git a/internal/locale/translations/zh_TW.json b/internal/locale/translations/zh_TW.json
index fb48f80b8a0..9d2fe7d7c25 100644
--- a/internal/locale/translations/zh_TW.json
+++ b/internal/locale/translations/zh_TW.json
@@ -503,6 +503,7 @@
     "form.integration.ntfy_username": "Ntfy Username (optional)",
     "form.integration.ntfy_password": "Ntfy Password (optional)",
     "form.integration.ntfy_icon_url": "Ntfy Icon URL (optional)",
+    "form.integration.ntfy_internal_links": "Use internal links on click (optional)",
     "form.integration.discord_activate": "推送文章到 Discord",
     "form.integration.discord_webhook_link": "Discord Webhook link",
     "form.api_key.label.description": "API金鑰標籤",
diff --git a/internal/model/integration.go b/internal/model/integration.go
index d8734b6e266..0a4e466a878 100644
--- a/internal/model/integration.go
+++ b/internal/model/integration.go
@@ -104,6 +104,7 @@ type Integration struct {
 	NtfyUsername                     string
 	NtfyPassword                     string
 	NtfyIconURL                      string
+	NtfyInternalLinks                bool
 	CuboxEnabled                     bool
 	CuboxAPILink                     string
 	DiscordEnabled                   bool
diff --git a/internal/storage/integration.go b/internal/storage/integration.go
index 8502b1caf98..32da349dcff 100644
--- a/internal/storage/integration.go
+++ b/internal/storage/integration.go
@@ -208,6 +208,7 @@ func (s *Storage) Integration(userID int64) (*model.Integration, error) {
 			ntfy_username,
 			ntfy_password,
 			ntfy_icon_url,
+			ntfy_internal_links,
 			cubox_enabled,
 			cubox_api_link,
 			discord_enabled,
@@ -318,6 +319,7 @@ func (s *Storage) Integration(userID int64) (*model.Integration, error) {
 		&integration.NtfyUsername,
 		&integration.NtfyPassword,
 		&integration.NtfyIconURL,
+		&integration.NtfyInternalLinks,
 		&integration.CuboxEnabled,
 		&integration.CuboxAPILink,
 		&integration.DiscordEnabled,
@@ -437,12 +439,13 @@ func (s *Storage) UpdateIntegration(integration *model.Integration) error {
 			ntfy_username=$96,
 			ntfy_password=$97,
 			ntfy_icon_url=$98,
-			cubox_enabled=$99,
-			cubox_api_link=$100,
-			discord_enabled=$101,
-			discord_webhook_link=$102
+			ntfy_internal_links=$99,
+			cubox_enabled=$100,
+			cubox_api_link=$101,
+			discord_enabled=$102,
+			discord_webhook_link=$103
 		WHERE
-			user_id=$103
+			user_id=$104
 	`
 	_, err := s.db.Exec(
 		query,
@@ -544,6 +547,7 @@ func (s *Storage) UpdateIntegration(integration *model.Integration) error {
 		integration.NtfyUsername,
 		integration.NtfyPassword,
 		integration.NtfyIconURL,
+		integration.NtfyInternalLinks,
 		integration.CuboxEnabled,
 		integration.CuboxAPILink,
 		integration.DiscordEnabled,
diff --git a/internal/template/templates/views/integrations.html b/internal/template/templates/views/integrations.html
index 17d6ce2d139..878e20a7c97 100644
--- a/internal/template/templates/views/integrations.html
+++ b/internal/template/templates/views/integrations.html
@@ -316,6 +316,10 @@ <h1 id="page-header-title">{{ t "page.integrations.title" }}</h1>
             <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">
 
+            <label>
+                <input type="checkbox" name="ntfy_internal_links" value="1" {{ if .form.NtfyInternalLinks }}checked{{ end }}> {{ t "form.integration.ntfy_internal_links" }}
+            </label>
+
             <div class="buttons">
                 <button type="submit" class="button button-primary" data-label-loading="{{ t "form.submit.saving" }}">{{ t "action.update" }}</button>
             </div>
diff --git a/internal/ui/form/integration.go b/internal/ui/form/integration.go
index 3049e520814..a3a4e0b2d95 100644
--- a/internal/ui/form/integration.go
+++ b/internal/ui/form/integration.go
@@ -110,6 +110,7 @@ type IntegrationForm struct {
 	NtfyUsername                     string
 	NtfyPassword                     string
 	NtfyIconURL                      string
+	NtfyInternalLinks                bool
 	CuboxEnabled                     bool
 	CuboxAPILink                     string
 	DiscordEnabled                   bool
@@ -213,6 +214,7 @@ func (i IntegrationForm) Merge(integration *model.Integration) {
 	integration.NtfyUsername = i.NtfyUsername
 	integration.NtfyPassword = i.NtfyPassword
 	integration.NtfyIconURL = i.NtfyIconURL
+	integration.NtfyInternalLinks = i.NtfyInternalLinks
 	integration.CuboxEnabled = i.CuboxEnabled
 	integration.CuboxAPILink = i.CuboxAPILink
 	integration.DiscordEnabled = i.DiscordEnabled
@@ -319,6 +321,7 @@ func NewIntegrationForm(r *http.Request) *IntegrationForm {
 		NtfyUsername:                     r.FormValue("ntfy_username"),
 		NtfyPassword:                     r.FormValue("ntfy_password"),
 		NtfyIconURL:                      r.FormValue("ntfy_icon_url"),
+		NtfyInternalLinks:                r.FormValue("ntfy_internal_links") == "1",
 		CuboxEnabled:                     r.FormValue("cubox_enabled") == "1",
 		CuboxAPILink:                     r.FormValue("cubox_api_link"),
 		DiscordEnabled:                   r.FormValue("discord_enabled") == "1",
diff --git a/internal/ui/integration_show.go b/internal/ui/integration_show.go
index a6e0ece37d5..2ad5053991c 100644
--- a/internal/ui/integration_show.go
+++ b/internal/ui/integration_show.go
@@ -124,6 +124,7 @@ func (h *handler) showIntegrationPage(w http.ResponseWriter, r *http.Request) {
 		NtfyUsername:                     integration.NtfyUsername,
 		NtfyPassword:                     integration.NtfyPassword,
 		NtfyIconURL:                      integration.NtfyIconURL,
+		NtfyInternalLinks:                integration.NtfyInternalLinks,
 		CuboxEnabled:                     integration.CuboxEnabled,
 		CuboxAPILink:                     integration.CuboxAPILink,
 		DiscordEnabled:                   integration.DiscordEnabled,


Test patch: