aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGravatar Frédéric Guillot <f@miniflux.net> 2023-09-08 22:45:17 -0700
committerGravatar Frédéric Guillot <f@miniflux.net> 2023-09-09 13:11:42 -0700
commit48f6885f4472efbe0e23f990ae8d4545f9a6a73d (patch)
treea05b35013e65f95013f90006b07870ddaeaf4065
parent32d33104a4934771ca99b1bcfe55bd0e4e88809b (diff)
downloadv2-48f6885f4472efbe0e23f990ae8d4545f9a6a73d.tar.gz
v2-48f6885f4472efbe0e23f990ae8d4545f9a6a73d.tar.zst
v2-48f6885f4472efbe0e23f990ae8d4545f9a6a73d.zip
Add generic webhook integration
-rw-r--r--internal/crypto/crypto.go7
-rw-r--r--internal/database/migrations.go9
-rw-r--r--internal/integration/integration.go69
-rw-r--r--internal/integration/webhook/webhook.go64
-rw-r--r--internal/locale/translations/de_DE.json3
-rw-r--r--internal/locale/translations/el_EL.json3
-rw-r--r--internal/locale/translations/en_US.json3
-rw-r--r--internal/locale/translations/es_ES.json3
-rw-r--r--internal/locale/translations/fi_FI.json3
-rw-r--r--internal/locale/translations/fr_FR.json3
-rw-r--r--internal/locale/translations/hi_IN.json3
-rw-r--r--internal/locale/translations/id_ID.json3
-rw-r--r--internal/locale/translations/it_IT.json3
-rw-r--r--internal/locale/translations/ja_JP.json3
-rw-r--r--internal/locale/translations/nl_NL.json3
-rw-r--r--internal/locale/translations/pl_PL.json3
-rw-r--r--internal/locale/translations/pt_BR.json3
-rw-r--r--internal/locale/translations/ru_RU.json3
-rw-r--r--internal/locale/translations/tr_TR.json3
-rw-r--r--internal/locale/translations/uk_UA.json3
-rw-r--r--internal/locale/translations/zh_CN.json3
-rw-r--r--internal/locale/translations/zh_TW.json3
-rw-r--r--internal/model/entry.go7
-rw-r--r--internal/model/integration.go3
-rw-r--r--internal/reader/atom/atom_03.go2
-rw-r--r--internal/reader/atom/atom_10.go4
-rw-r--r--internal/reader/handler/handler.go12
-rw-r--r--internal/reader/json/json.go7
-rw-r--r--internal/reader/processor/processor.go27
-rw-r--r--internal/reader/rdf/rdf.go2
-rw-r--r--internal/reader/rss/rss.go4
-rw-r--r--internal/storage/enclosure.go25
-rw-r--r--internal/storage/entry.go30
-rw-r--r--internal/storage/feed.go6
-rw-r--r--internal/storage/integration.go18
-rw-r--r--internal/template/templates/views/integrations.html355
-rw-r--r--internal/ui/form/integration.go7
-rw-r--r--internal/ui/integration_show.go3
-rw-r--r--internal/ui/integration_update.go12
39 files changed, 465 insertions, 262 deletions
diff --git a/internal/crypto/crypto.go b/internal/crypto/crypto.go
index 44854910..fc974a1f 100644
--- a/internal/crypto/crypto.go
+++ b/internal/crypto/crypto.go
@@ -4,6 +4,7 @@
package crypto // import "miniflux.app/v2/internal/crypto"
import (
+ "crypto/hmac"
"crypto/rand"
"crypto/sha256"
"encoding/base64"
@@ -48,3 +49,9 @@ func HashPassword(password string) (string, error) {
bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
return string(bytes), err
}
+
+func GenerateSHA256Hmac(secret string, data []byte) string {
+ h := hmac.New(sha256.New, []byte(secret))
+ h.Write(data)
+ return hex.EncodeToString(h.Sum(nil))
+}
diff --git a/internal/database/migrations.go b/internal/database/migrations.go
index 4830acd1..e485243b 100644
--- a/internal/database/migrations.go
+++ b/internal/database/migrations.go
@@ -758,4 +758,13 @@ var migrations = []func(tx *sql.Tx) error{
`)
return err
},
+ func(tx *sql.Tx) (err error) {
+ sql := `
+ ALTER TABLE integrations ADD COLUMN webhook_enabled bool default 'f';
+ ALTER TABLE integrations ADD COLUMN webhook_url text default '';
+ ALTER TABLE integrations ADD COLUMN webhook_secret text default '';
+ `
+ _, err = tx.Exec(sql)
+ return err
+ },
}
diff --git a/internal/integration/integration.go b/internal/integration/integration.go
index 65d005d8..07b70308 100644
--- a/internal/integration/integration.go
+++ b/internal/integration/integration.go
@@ -19,6 +19,7 @@ import (
"miniflux.app/v2/internal/integration/shiori"
"miniflux.app/v2/internal/integration/telegrambot"
"miniflux.app/v2/internal/integration/wallabag"
+ "miniflux.app/v2/internal/integration/webhook"
"miniflux.app/v2/internal/logger"
"miniflux.app/v2/internal/model"
)
@@ -168,45 +169,55 @@ func SendEntry(entry *model.Entry, integration *model.Integration) {
}
}
-// PushEntries pushes an entry array to third-party providers during feed refreshes.
-func PushEntries(entries model.Entries, integration *model.Integration) {
- if integration.MatrixBotEnabled {
- logger.Debug("[Integration] Sending %d entries for User #%d to Matrix", len(entries), integration.UserID)
+// PushEntries pushes a list of entries to activated third-party providers during feed refreshes.
+func PushEntries(feed *model.Feed, entries model.Entries, userIntegrations *model.Integration) {
+ if userIntegrations.MatrixBotEnabled {
+ logger.Debug("[Integration] Sending %d entries for User #%d to Matrix", len(entries), userIntegrations.UserID)
- err := matrixbot.PushEntries(entries, integration.MatrixBotURL, integration.MatrixBotUser, integration.MatrixBotPassword, integration.MatrixBotChatID)
+ err := matrixbot.PushEntries(entries, userIntegrations.MatrixBotURL, userIntegrations.MatrixBotUser, userIntegrations.MatrixBotPassword, userIntegrations.MatrixBotChatID)
if err != nil {
logger.Error("[Integration] push entries to matrix bot failed: %v", err)
}
}
-}
-// PushEntry pushes an entry to third-party providers during feed refreshes.
-func PushEntry(entry *model.Entry, feed *model.Feed, integration *model.Integration) {
- if integration.TelegramBotEnabled {
- logger.Debug("[Integration] Sending Entry %q for User #%d to Telegram", entry.URL, integration.UserID)
+ if userIntegrations.WebhookEnabled {
+ logger.Debug("[Integration] Sending %d entries for User #%d to Webhook URL: %s", len(entries), userIntegrations.UserID, userIntegrations.WebhookURL)
- err := telegrambot.PushEntry(entry, integration.TelegramBotToken, integration.TelegramBotChatID)
- if err != nil {
- logger.Error("[Integration] push entry to telegram bot failed: %v", err)
+ webhookClient := webhook.NewClient(userIntegrations.WebhookURL, userIntegrations.WebhookSecret)
+ if err := webhookClient.SendWebhook(entries); err != nil {
+ logger.Error("[Integration] sending entries to webhook failed: %v", err)
}
}
- if integration.AppriseEnabled {
- logger.Debug("[Integration] Sending Entry %q for User #%d to apprise", entry.URL, integration.UserID)
-
- var appriseServiceURLs string
- if len(feed.AppriseServiceURLs) > 0 {
- appriseServiceURLs = feed.AppriseServiceURLs
- } else {
- appriseServiceURLs = integration.AppriseServicesURL
- }
-
- client := apprise.NewClient(
- appriseServiceURLs,
- integration.AppriseURL,
- )
- if err := client.SendNotification(entry); err != nil {
- logger.Error("[Integration] push entry to apprise failed: %v", err)
+ // Integrations that only support sending individual entries
+ if userIntegrations.TelegramBotEnabled || userIntegrations.AppriseEnabled {
+ for _, entry := range entries {
+ if userIntegrations.TelegramBotEnabled {
+ logger.Debug("[Integration] Sending Entry %q for User #%d to Telegram", entry.URL, userIntegrations.UserID)
+
+ err := telegrambot.PushEntry(entry, userIntegrations.TelegramBotToken, userIntegrations.TelegramBotChatID)
+ if err != nil {
+ logger.Error("[Integration] push entry to telegram bot failed: %v", err)
+ }
+ }
+
+ if userIntegrations.AppriseEnabled {
+ logger.Debug("[Integration] Sending Entry %q for User #%d to apprise", entry.URL, userIntegrations.UserID)
+
+ appriseServiceURLs := userIntegrations.AppriseURL
+ if feed.AppriseServiceURLs != "" {
+ appriseServiceURLs = feed.AppriseServiceURLs
+ }
+
+ client := apprise.NewClient(
+ userIntegrations.AppriseServicesURL,
+ appriseServiceURLs,
+ )
+
+ if err := client.SendNotification(entry); err != nil {
+ logger.Error("[Integration] push entry to apprise failed: %v", err)
+ }
+ }
}
}
}
diff --git a/internal/integration/webhook/webhook.go b/internal/integration/webhook/webhook.go
new file mode 100644
index 00000000..65f5fa8c
--- /dev/null
+++ b/internal/integration/webhook/webhook.go
@@ -0,0 +1,64 @@
+// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package webhook // import "miniflux.app/v2/internal/integration/webhook"
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "time"
+
+ "miniflux.app/v2/internal/crypto"
+ "miniflux.app/v2/internal/model"
+ "miniflux.app/v2/internal/version"
+)
+
+const defaultClientTimeout = 10 * time.Second
+
+type Client struct {
+ webhookURL string
+ webhookSecret string
+}
+
+func NewClient(webhookURL, webhookSecret string) *Client {
+ return &Client{webhookURL, webhookSecret}
+}
+
+func (c *Client) SendWebhook(entries model.Entries) error {
+ if c.webhookURL == "" {
+ return fmt.Errorf(`webhook: missing webhook URL`)
+ }
+
+ if len(entries) == 0 {
+ return nil
+ }
+
+ requestBody, err := json.Marshal(entries)
+ if err != nil {
+ return fmt.Errorf("webhook: unable to encode request body: %v", err)
+ }
+
+ request, err := http.NewRequest(http.MethodPost, c.webhookURL, bytes.NewReader(requestBody))
+ if err != nil {
+ return fmt.Errorf("webhook: unable to create request: %v", err)
+ }
+
+ request.Header.Set("Content-Type", "application/json")
+ request.Header.Set("User-Agent", "Miniflux/"+version.Version)
+ request.Header.Set("X-Miniflux-Signature", crypto.GenerateSHA256Hmac(c.webhookSecret, requestBody))
+
+ httpClient := &http.Client{Timeout: defaultClientTimeout}
+ response, err := httpClient.Do(request)
+ if err != nil {
+ return fmt.Errorf("webhook: unable to send request: %v", err)
+ }
+ defer response.Body.Close()
+
+ if response.StatusCode >= 400 {
+ return fmt.Errorf("webhook: incorrect response status code: url=%s status=%d", c.webhookURL, response.StatusCode)
+ }
+
+ return nil
+}
diff --git a/internal/locale/translations/de_DE.json b/internal/locale/translations/de_DE.json
index 98774a9b..dd91e211 100644
--- a/internal/locale/translations/de_DE.json
+++ b/internal/locale/translations/de_DE.json
@@ -389,6 +389,9 @@
"form.integration.shaarli_activate": "Save articles to Shaarli",
"form.integration.shaarli_endpoint": "Shaarli URL",
"form.integration.shaarli_api_secret": "Shaarli API Secret",
+ "form.integration.webhook_activate": "Enable Webhook",
+ "form.integration.webhook_url": "Webhook URL",
+ "form.integration.webhook_secret": "Webhook Secret",
"form.api_key.label.description": "API-Schlüsselbezeichnung",
"form.submit.loading": "Lade...",
"form.submit.saving": "Speichern...",
diff --git a/internal/locale/translations/el_EL.json b/internal/locale/translations/el_EL.json
index 6dc9cd59..53ba2abb 100644
--- a/internal/locale/translations/el_EL.json
+++ b/internal/locale/translations/el_EL.json
@@ -389,6 +389,9 @@
"form.integration.shaarli_activate": "Save articles to Shaarli",
"form.integration.shaarli_endpoint": "Shaarli URL",
"form.integration.shaarli_api_secret": "Shaarli API Secret",
+ "form.integration.webhook_activate": "Enable Webhook",
+ "form.integration.webhook_url": "Webhook URL",
+ "form.integration.webhook_secret": "Webhook Secret",
"form.api_key.label.description": "Ετικέτα κλειδιού API",
"form.submit.loading": "Φόρτωση...",
"form.submit.saving": "Αποθήκευση...",
diff --git a/internal/locale/translations/en_US.json b/internal/locale/translations/en_US.json
index bcec58ca..58e7281e 100644
--- a/internal/locale/translations/en_US.json
+++ b/internal/locale/translations/en_US.json
@@ -389,6 +389,9 @@
"form.integration.shaarli_activate": "Save articles to Shaarli",
"form.integration.shaarli_endpoint": "Shaarli URL",
"form.integration.shaarli_api_secret": "Shaarli API Secret",
+ "form.integration.webhook_activate": "Enable Webhook",
+ "form.integration.webhook_url": "Webhook URL",
+ "form.integration.webhook_secret": "Webhook Secret",
"form.api_key.label.description": "API Key Label",
"form.submit.loading": "Loading…",
"form.submit.saving": "Saving…",
diff --git a/internal/locale/translations/es_ES.json b/internal/locale/translations/es_ES.json
index 5e002ec0..baeee1ca 100644
--- a/internal/locale/translations/es_ES.json
+++ b/internal/locale/translations/es_ES.json
@@ -389,6 +389,9 @@
"form.integration.shaarli_activate": "Save articles to Shaarli",
"form.integration.shaarli_endpoint": "Shaarli URL",
"form.integration.shaarli_api_secret": "Shaarli API Secret",
+ "form.integration.webhook_activate": "Enable Webhook",
+ "form.integration.webhook_url": "Webhook URL",
+ "form.integration.webhook_secret": "Webhook Secret",
"form.api_key.label.description": "Etiqueta de clave API",
"form.submit.loading": "Cargando...",
"form.submit.saving": "Guardando...",
diff --git a/internal/locale/translations/fi_FI.json b/internal/locale/translations/fi_FI.json
index 41fec82d..79f92838 100644
--- a/internal/locale/translations/fi_FI.json
+++ b/internal/locale/translations/fi_FI.json
@@ -389,6 +389,9 @@
"form.integration.shaarli_activate": "Save articles to Shaarli",
"form.integration.shaarli_endpoint": "Shaarli URL",
"form.integration.shaarli_api_secret": "Shaarli API Secret",
+ "form.integration.webhook_activate": "Enable Webhook",
+ "form.integration.webhook_url": "Webhook URL",
+ "form.integration.webhook_secret": "Webhook Secret",
"form.api_key.label.description": "API Key Label",
"form.submit.loading": "Ladataan...",
"form.submit.saving": "Tallennetaan...",
diff --git a/internal/locale/translations/fr_FR.json b/internal/locale/translations/fr_FR.json
index 2069ad3c..30cc939e 100644
--- a/internal/locale/translations/fr_FR.json
+++ b/internal/locale/translations/fr_FR.json
@@ -389,6 +389,9 @@
"form.integration.shaarli_activate": "Sauvegarder les articles vers Shaarli",
"form.integration.shaarli_endpoint": "URL de l'API de Shaarli",
"form.integration.shaarli_api_secret": "Clé d'API de Shaarli API",
+ "form.integration.webhook_activate": "Enable Webhook",
+ "form.integration.webhook_url": "Webhook URL",
+ "form.integration.webhook_secret": "Webhook Secret",
"form.api_key.label.description": "Libellé de la clé d'API",
"form.submit.loading": "Chargement...",
"form.submit.saving": "Sauvegarde en cours...",
diff --git a/internal/locale/translations/hi_IN.json b/internal/locale/translations/hi_IN.json
index 89ce57a9..253b8bbb 100644
--- a/internal/locale/translations/hi_IN.json
+++ b/internal/locale/translations/hi_IN.json
@@ -389,6 +389,9 @@
"form.integration.shaarli_activate": "Save articles to Shaarli",
"form.integration.shaarli_endpoint": "Shaarli URL",
"form.integration.shaarli_api_secret": "Shaarli API Secret",
+ "form.integration.webhook_activate": "Enable Webhook",
+ "form.integration.webhook_url": "Webhook URL",
+ "form.integration.webhook_secret": "Webhook Secret",
"form.api_key.label.description": "एपीआई कुंजी लेबल",
"form.submit.loading": "लोड हो रहा है...",
"form.submit.saving": "सहेजा जा रहा है...",
diff --git a/internal/locale/translations/id_ID.json b/internal/locale/translations/id_ID.json
index efc07073..fb46b6e2 100644
--- a/internal/locale/translations/id_ID.json
+++ b/internal/locale/translations/id_ID.json
@@ -386,6 +386,9 @@
"form.integration.shaarli_activate": "Save articles to Shaarli",
"form.integration.shaarli_endpoint": "Shaarli URL",
"form.integration.shaarli_api_secret": "Shaarli API Secret",
+ "form.integration.webhook_activate": "Enable Webhook",
+ "form.integration.webhook_url": "Webhook URL",
+ "form.integration.webhook_secret": "Webhook Secret",
"form.api_key.label.description": "Label Kunci API",
"form.submit.loading": "Memuat...",
"form.submit.saving": "Menyimpan...",
diff --git a/internal/locale/translations/it_IT.json b/internal/locale/translations/it_IT.json
index 78363f1b..17582cc0 100644
--- a/internal/locale/translations/it_IT.json
+++ b/internal/locale/translations/it_IT.json
@@ -389,6 +389,9 @@
"form.integration.shaarli_activate": "Save articles to Shaarli",
"form.integration.shaarli_endpoint": "Shaarli URL",
"form.integration.shaarli_api_secret": "Shaarli API Secret",
+ "form.integration.webhook_activate": "Enable Webhook",
+ "form.integration.webhook_url": "Webhook URL",
+ "form.integration.webhook_secret": "Webhook Secret",
"form.api_key.label.description": "Etichetta chiave API",
"form.submit.loading": "Caricamento in corso...",
"form.submit.saving": "Salvataggio in corso...",
diff --git a/internal/locale/translations/ja_JP.json b/internal/locale/translations/ja_JP.json
index 71e9abe5..fb8ba253 100644
--- a/internal/locale/translations/ja_JP.json
+++ b/internal/locale/translations/ja_JP.json
@@ -389,6 +389,9 @@
"form.integration.shaarli_activate": "Save articles to Shaarli",
"form.integration.shaarli_endpoint": "Shaarli URL",
"form.integration.shaarli_api_secret": "Shaarli API Secret",
+ "form.integration.webhook_activate": "Enable Webhook",
+ "form.integration.webhook_url": "Webhook URL",
+ "form.integration.webhook_secret": "Webhook Secret",
"form.api_key.label.description": "API キーラベル",
"form.submit.loading": "読み込み中…",
"form.submit.saving": "保存中…",
diff --git a/internal/locale/translations/nl_NL.json b/internal/locale/translations/nl_NL.json
index 8013bba8..804f118e 100644
--- a/internal/locale/translations/nl_NL.json
+++ b/internal/locale/translations/nl_NL.json
@@ -389,6 +389,9 @@
"form.integration.shaarli_activate": "Save articles to Shaarli",
"form.integration.shaarli_endpoint": "Shaarli URL",
"form.integration.shaarli_api_secret": "Shaarli API Secret",
+ "form.integration.webhook_activate": "Enable Webhook",
+ "form.integration.webhook_url": "Webhook URL",
+ "form.integration.webhook_secret": "Webhook Secret",
"form.api_key.label.description": "API-sleutellabel",
"form.submit.loading": "Laden...",
"form.submit.saving": "Opslaag...",
diff --git a/internal/locale/translations/pl_PL.json b/internal/locale/translations/pl_PL.json
index 41b80ad4..555d6b11 100644
--- a/internal/locale/translations/pl_PL.json
+++ b/internal/locale/translations/pl_PL.json
@@ -391,6 +391,9 @@
"form.integration.shaarli_activate": "Save articles to Shaarli",
"form.integration.shaarli_endpoint": "Shaarli URL",
"form.integration.shaarli_api_secret": "Shaarli API Secret",
+ "form.integration.webhook_activate": "Enable Webhook",
+ "form.integration.webhook_url": "Webhook URL",
+ "form.integration.webhook_secret": "Webhook Secret",
"form.api_key.label.description": "Etykieta klucza API",
"form.submit.loading": "Ładowanie...",
"form.submit.saving": "Zapisywanie...",
diff --git a/internal/locale/translations/pt_BR.json b/internal/locale/translations/pt_BR.json
index c85b0d32..6eb4df12 100644
--- a/internal/locale/translations/pt_BR.json
+++ b/internal/locale/translations/pt_BR.json
@@ -389,6 +389,9 @@
"form.integration.shaarli_activate": "Save articles to Shaarli",
"form.integration.shaarli_endpoint": "Shaarli URL",
"form.integration.shaarli_api_secret": "Shaarli API Secret",
+ "form.integration.webhook_activate": "Enable Webhook",
+ "form.integration.webhook_url": "Webhook URL",
+ "form.integration.webhook_secret": "Webhook Secret",
"form.api_key.label.description": "Etiqueta da chave de API",
"form.submit.loading": "Carregando...",
"form.submit.saving": "Salvando...",
diff --git a/internal/locale/translations/ru_RU.json b/internal/locale/translations/ru_RU.json
index 7eb0a2e9..25ef32be 100644
--- a/internal/locale/translations/ru_RU.json
+++ b/internal/locale/translations/ru_RU.json
@@ -391,6 +391,9 @@
"form.integration.shaarli_activate": "Save articles to Shaarli",
"form.integration.shaarli_endpoint": "Shaarli URL",
"form.integration.shaarli_api_secret": "Shaarli API Secret",
+ "form.integration.webhook_activate": "Enable Webhook",
+ "form.integration.webhook_url": "Webhook URL",
+ "form.integration.webhook_secret": "Webhook Secret",
"form.api_key.label.description": "Описание API-ключа",
"form.submit.loading": "Загрузка…",
"form.submit.saving": "Сохранение…",
diff --git a/internal/locale/translations/tr_TR.json b/internal/locale/translations/tr_TR.json
index fb74da25..5c688b3c 100644
--- a/internal/locale/translations/tr_TR.json
+++ b/internal/locale/translations/tr_TR.json
@@ -389,6 +389,9 @@
"form.integration.shaarli_activate": "Save articles to Shaarli",
"form.integration.shaarli_endpoint": "Shaarli URL",
"form.integration.shaarli_api_secret": "Shaarli API Secret",
+ "form.integration.webhook_activate": "Enable Webhook",
+ "form.integration.webhook_url": "Webhook URL",
+ "form.integration.webhook_secret": "Webhook Secret",
"form.api_key.label.description": "API Anahtar Etiketi",
"form.submit.loading": "Yükleniyor...",
"form.submit.saving": "Kaydediliyor...",
diff --git a/internal/locale/translations/uk_UA.json b/internal/locale/translations/uk_UA.json
index 2896d692..6a2b570c 100644
--- a/internal/locale/translations/uk_UA.json
+++ b/internal/locale/translations/uk_UA.json
@@ -392,6 +392,9 @@
"form.integration.shaarli_activate": "Save articles to Shaarli",
"form.integration.shaarli_endpoint": "Shaarli URL",
"form.integration.shaarli_api_secret": "Shaarli API Secret",
+ "form.integration.webhook_activate": "Enable Webhook",
+ "form.integration.webhook_url": "Webhook URL",
+ "form.integration.webhook_secret": "Webhook Secret",
"form.api_key.label.description": "Назва ключа API",
"form.submit.loading": "Завантаження...",
"form.submit.saving": "Зберігаю...",
diff --git a/internal/locale/translations/zh_CN.json b/internal/locale/translations/zh_CN.json
index a3b430a3..6d39521d 100644
--- a/internal/locale/translations/zh_CN.json
+++ b/internal/locale/translations/zh_CN.json
@@ -387,6 +387,9 @@
"form.integration.shaarli_activate": "Save articles to Shaarli",
"form.integration.shaarli_endpoint": "Shaarli URL",
"form.integration.shaarli_api_secret": "Shaarli API Secret",
+ "form.integration.webhook_activate": "Enable Webhook",
+ "form.integration.webhook_url": "Webhook URL",
+ "form.integration.webhook_secret": "Webhook Secret",
"form.api_key.label.description": "API密钥标签",
"form.submit.loading": "载入中…",
"form.submit.saving": "保存中…",
diff --git a/internal/locale/translations/zh_TW.json b/internal/locale/translations/zh_TW.json
index 45c44165..23f62428 100644
--- a/internal/locale/translations/zh_TW.json
+++ b/internal/locale/translations/zh_TW.json
@@ -389,6 +389,9 @@
"form.integration.shaarli_activate": "Save articles to Shaarli",
"form.integration.shaarli_endpoint": "Shaarli URL",
"form.integration.shaarli_api_secret": "Shaarli API Secret",
+ "form.integration.webhook_activate": "Enable Webhook",
+ "form.integration.webhook_url": "Webhook URL",
+ "form.integration.webhook_secret": "Webhook Secret",
"form.api_key.label.description": "API金鑰標籤",
"form.submit.loading": "載入中…",
"form.submit.saving": "儲存中…",
diff --git a/internal/model/entry.go b/internal/model/entry.go
index d09bd3ee..277d4269 100644
--- a/internal/model/entry.go
+++ b/internal/model/entry.go
@@ -39,6 +39,13 @@ type Entry struct {
Tags []string `json:"tags"`
}
+func NewEntry() *Entry {
+ return &Entry{
+ Enclosures: make(EnclosureList, 0),
+ Tags: make([]string, 0),
+ }
+}
+
// Entries represents a list of entries.
type Entries []*Entry
diff --git a/internal/model/integration.go b/internal/model/integration.go
index c00be86f..027369ee 100644
--- a/internal/model/integration.go
+++ b/internal/model/integration.go
@@ -64,4 +64,7 @@ type Integration struct {
ShaarliEnabled bool
ShaarliURL string
ShaarliAPISecret string
+ WebhookEnabled bool
+ WebhookURL string
+ WebhookSecret string
}
diff --git a/internal/reader/atom/atom_03.go b/internal/reader/atom/atom_03.go
index a760ce26..d7e99ae6 100644
--- a/internal/reader/atom/atom_03.go
+++ b/internal/reader/atom/atom_03.go
@@ -86,7 +86,7 @@ type atom03Entry struct {
}
func (a *atom03Entry) Transform() *model.Entry {
- entry := new(model.Entry)
+ entry := model.NewEntry()
entry.URL = a.Links.originalLink()
entry.Date = a.entryDate()
entry.Author = a.Author.String()
diff --git a/internal/reader/atom/atom_10.go b/internal/reader/atom/atom_10.go
index 2c6edf17..8eee69bf 100644
--- a/internal/reader/atom/atom_10.go
+++ b/internal/reader/atom/atom_10.go
@@ -95,7 +95,7 @@ type atom10Entry struct {
}
func (a *atom10Entry) Transform() *model.Entry {
- entry := new(model.Entry)
+ entry := model.NewEntry()
entry.URL = a.Links.originalLink()
entry.Date = a.entryDate()
entry.Author = a.Authors.String()
@@ -219,7 +219,7 @@ func (a *atom10Entry) entryEnclosures() model.EnclosureList {
}
func (r *atom10Entry) entryCategories() []string {
- var categoryList []string
+ categoryList := make([]string, 0)
for _, atomCategory := range r.Categories {
if strings.TrimSpace(atomCategory.Label) != "" {
diff --git a/internal/reader/handler/handler.go b/internal/reader/handler/handler.go
index 06bd78fc..320643b1 100644
--- a/internal/reader/handler/handler.go
+++ b/internal/reader/handler/handler.go
@@ -10,6 +10,7 @@ import (
"miniflux.app/v2/internal/config"
"miniflux.app/v2/internal/errors"
"miniflux.app/v2/internal/http/client"
+ "miniflux.app/v2/internal/integration"
"miniflux.app/v2/internal/locale"
"miniflux.app/v2/internal/logger"
"miniflux.app/v2/internal/model"
@@ -177,15 +178,24 @@ func RefreshFeed(store *storage.Storage, userID, feedID int64, forceRefresh bool
// We don't update existing entries when the crawler is enabled (we crawl only inexisting entries). Unless it is forced to refresh
updateExistingEntries := forceRefresh || !originalFeed.Crawler
- if storeErr := store.RefreshFeedEntries(originalFeed.UserID, originalFeed.ID, originalFeed.Entries, updateExistingEntries); storeErr != nil {
+ newEntries, storeErr := store.RefreshFeedEntries(originalFeed.UserID, originalFeed.ID, originalFeed.Entries, updateExistingEntries)
+ if storeErr != nil {
originalFeed.WithError(storeErr.Error())
store.UpdateFeedError(originalFeed)
return storeErr
}
+ userIntegrations, intErr := store.Integration(userID)
+ if intErr != nil {
+ logger.Error("[RefreshFeed] Fetching integrations for user %d failed: %v; the refresh process will go on, but no integrations will run this time.", userID, intErr)
+ } else if userIntegrations != nil && len(newEntries) > 0 {
+ go integration.PushEntries(originalFeed, newEntries, userIntegrations)
+ }
+
// We update caching headers only if the feed has been modified,
// because some websites don't return the same headers when replying with a 304.
originalFeed.WithClientResponse(response)
+
checkFeedIcon(
store,
originalFeed.ID,
diff --git a/internal/reader/json/json.go b/internal/reader/json/json.go
index 48f64c23..68f4c0f8 100644
--- a/internal/reader/json/json.go
+++ b/internal/reader/json/json.go
@@ -181,7 +181,7 @@ func (j *jsonItem) GetEnclosures() model.EnclosureList {
}
func (j *jsonItem) Transform() *model.Entry {
- entry := new(model.Entry)
+ entry := model.NewEntry()
entry.URL = j.URL
entry.Date = j.GetDate()
entry.Author = j.GetAuthor()
@@ -189,7 +189,10 @@ func (j *jsonItem) Transform() *model.Entry {
entry.Content = j.GetContent()
entry.Title = strings.TrimSpace(j.GetTitle())
entry.Enclosures = j.GetEnclosures()
- entry.Tags = j.Tags
+ if len(j.Tags) > 0 {
+ entry.Tags = j.Tags
+ }
+
return entry
}
diff --git a/internal/reader/processor/processor.go b/internal/reader/processor/processor.go
index 4dec59f0..d56d4289 100644
--- a/internal/reader/processor/processor.go
+++ b/internal/reader/processor/processor.go
@@ -13,8 +13,6 @@ import (
"time"
"unicode/utf8"
- "miniflux.app/v2/internal/integration"
-
"miniflux.app/v2/internal/config"
"miniflux.app/v2/internal/http/client"
"miniflux.app/v2/internal/logger"
@@ -41,9 +39,6 @@ var (
func ProcessFeedEntries(store *storage.Storage, feed *model.Feed, user *model.User, forceRefresh bool) {
var filteredEntries model.Entries
- // array used for bulk push
- entriesToPush := model.Entries{}
-
// Process older entries first
for i := len(feed.Entries) - 1; i >= 0; i-- {
entry := feed.Entries[i]
@@ -90,32 +85,10 @@ func ProcessFeedEntries(store *storage.Storage, feed *model.Feed, user *model.Us
// The sanitizer should always run at the end of the process to make sure unsafe HTML is filtered.
entry.Content = sanitizer.Sanitize(url, entry.Content)
- if entryIsNew {
- intg, err := store.Integration(feed.UserID)
- if err != nil {
- logger.Error("[Processor] Get integrations for user %d failed: %v; the refresh process will go on, but no integrations will run this time.", feed.UserID, err)
- } else if intg != nil {
- localEntry := entry
- go func() {
- integration.PushEntry(localEntry, feed, intg)
- }()
- entriesToPush = append(entriesToPush, localEntry)
- }
- }
-
updateEntryReadingTime(store, feed, entry, entryIsNew, user)
filteredEntries = append(filteredEntries, entry)
}
- intg, err := store.Integration(feed.UserID)
- if err != nil {
- logger.Error("[Processor] Get integrations for user %d failed: %v; the refresh process will go on, but no integrations will run this time.", feed.UserID, err)
- } else if intg != nil && len(entriesToPush) > 0 {
- go func() {
- integration.PushEntries(entriesToPush, intg)
- }()
- }
-
feed.Entries = filteredEntries
}
diff --git a/internal/reader/rdf/rdf.go b/internal/reader/rdf/rdf.go
index 935d0c0c..ca74cb2a 100644
--- a/internal/reader/rdf/rdf.go
+++ b/internal/reader/rdf/rdf.go
@@ -65,7 +65,7 @@ type rdfItem struct {
}
func (r *rdfItem) Transform() *model.Entry {
- entry := new(model.Entry)
+ entry := model.NewEntry()
entry.Title = r.entryTitle()
entry.Author = r.entryAuthor()
entry.URL = r.entryURL()
diff --git a/internal/reader/rss/rss.go b/internal/reader/rss/rss.go
index 323c6041..f2ecdaec 100644
--- a/internal/reader/rss/rss.go
+++ b/internal/reader/rss/rss.go
@@ -190,7 +190,7 @@ type rssItem struct {
}
func (r *rssItem) Transform() *model.Entry {
- entry := new(model.Entry)
+ entry := model.NewEntry()
entry.URL = r.entryURL()
entry.CommentsURL = r.entryCommentsURL()
entry.Date = r.entryDate()
@@ -388,7 +388,7 @@ func (r *rssItem) entryEnclosures() model.EnclosureList {
}
func (r *rssItem) entryCategories() []string {
- var categoryList []string
+ categoryList := make([]string, 0)
for _, rssCategory := range r.Categories {
if strings.Contains(rssCategory.Inner, "<![CDATA[") {
diff --git a/internal/storage/enclosure.go b/internal/storage/enclosure.go
index 8f8ef8e1..8eb1cea4 100644
--- a/internal/storage/enclosure.go
+++ b/internal/storage/enclosure.go
@@ -107,8 +107,10 @@ func (s *Storage) createEnclosure(tx *sql.Tx, enclosure *model.Enclosure) error
VALUES
($1, $2, $3, $4, $5, $6)
ON CONFLICT (user_id, entry_id, md5(url)) DO NOTHING
+ RETURNING
+ id
`
- _, err := tx.Exec(
+ if err := tx.QueryRow(
query,
enclosureURL,
enclosure.Size,
@@ -116,24 +118,22 @@ func (s *Storage) createEnclosure(tx *sql.Tx, enclosure *model.Enclosure) error
enclosure.EntryID,
enclosure.UserID,
enclosure.MediaProgression,
- )
-
- if err != nil {
- return fmt.Errorf(`store: unable to create enclosure: %v`, err)
+ ).Scan(&enclosure.ID); err != nil && err != sql.ErrNoRows {
+ return fmt.Errorf(`store: unable to create enclosure: %w`, err)
}
return nil
}
-func (s *Storage) updateEnclosures(tx *sql.Tx, userID, entryID int64, enclosures model.EnclosureList) error {
- if len(enclosures) == 0 {
+func (s *Storage) updateEnclosures(tx *sql.Tx, entry *model.Entry) error {
+ if len(entry.Enclosures) == 0 {
return nil
}
- sqlValues := []any{userID, entryID}
+ sqlValues := []any{entry.UserID, entry.ID}
sqlPlaceholders := []string{}
- for _, enclosure := range enclosures {
+ for _, enclosure := range entry.Enclosures {
sqlPlaceholders = append(sqlPlaceholders, fmt.Sprintf(`$%d`, len(sqlValues)+1))
sqlValues = append(sqlValues, strings.TrimSpace(enclosure.URL))
@@ -143,11 +143,10 @@ func (s *Storage) updateEnclosures(tx *sql.Tx, userID, entryID int64, enclosures
}
query := `
- DELETE FROM enclosures
+ DELETE FROM
+ enclosures
WHERE
- user_id=$1 AND
- entry_id=$2 AND
- url NOT IN (%s)
+ user_id=$1 AND entry_id=$2 AND url NOT IN (%s)
`
query = fmt.Sprintf(query, strings.Join(sqlPlaceholders, `,`))
diff --git a/internal/storage/entry.go b/internal/storage/entry.go
index fd8a0110..718f50d1 100644
--- a/internal/storage/entry.go
+++ b/internal/storage/entry.go
@@ -138,7 +138,7 @@ func (s *Storage) createEntry(tx *sql.Tx, entry *model.Entry) error {
$11
)
RETURNING
- id, status
+ id, status, created_at, changed_at
`
err := tx.QueryRow(
query,
@@ -153,7 +153,12 @@ func (s *Storage) createEntry(tx *sql.Tx, entry *model.Entry) error {
entry.FeedID,
entry.ReadingTime,
pq.Array(removeDuplicates(entry.Tags)),
- ).Scan(&entry.ID, &entry.Status)
+ ).Scan(
+ &entry.ID,
+ &entry.Status,
+ &entry.CreatedAt,
+ &entry.ChangedAt,
+ )
if err != nil {
return fmt.Errorf(`store: unable to create entry %q (feed #%d): %v`, entry.URL, entry.FeedID, err)
@@ -215,7 +220,7 @@ func (s *Storage) updateEntry(tx *sql.Tx, entry *model.Entry) error {
enclosure.EntryID = entry.ID
}
- return s.updateEnclosures(tx, entry.UserID, entry.ID, entry.Enclosures)
+ return s.updateEnclosures(tx, entry)
}
// entryExists checks if an entry already exists based on its hash when refreshing a feed.
@@ -264,7 +269,7 @@ func (s *Storage) cleanupEntries(feedID int64, entryHashes []string) error {
}
// RefreshFeedEntries updates feed entries while refreshing a feed.
-func (s *Storage) RefreshFeedEntries(userID, feedID int64, entries model.Entries, updateExistingEntries bool) (err error) {
+func (s *Storage) RefreshFeedEntries(userID, feedID int64, entries model.Entries, updateExistingEntries bool) (newEntries model.Entries, err error) {
var entryHashes []string
for _, entry := range entries {
@@ -273,15 +278,15 @@ func (s *Storage) RefreshFeedEntries(userID, feedID int64, entries model.Entries
tx, err := s.db.Begin()
if err != nil {
- return fmt.Errorf(`store: unable to start transaction: %v`, err)
+ return nil, fmt.Errorf(`store: unable to start transaction: %v`, err)
}
entryExists, err := s.entryExists(tx, entry)
if err != nil {
if rollbackErr := tx.Rollback(); rollbackErr != nil {
- return fmt.Errorf(`store: unable to rollback transaction: %v (rolled back due to: %v)`, rollbackErr, err)
+ return nil, fmt.Errorf(`store: unable to rollback transaction: %v (rolled back due to: %v)`, rollbackErr, err)
}
- return err
+ return nil, err
}
if entryExists {
@@ -290,17 +295,20 @@ func (s *Storage) RefreshFeedEntries(userID, feedID int64, entries model.Entries
}
} else {
err = s.createEntry(tx, entry)
+ if err == nil {
+ newEntries = append(newEntries, entry)
+ }
}
if err != nil {
if rollbackErr := tx.Rollback(); rollbackErr != nil {
- return fmt.Errorf(`store: unable to rollback transaction: %v (rolled back due to: %v)`, rollbackErr, err)
+ return nil, fmt.Errorf(`store: unable to rollback transaction: %v (rolled back due to: %v)`, rollbackErr, err)
}
- return err
+ return nil, err
}
if err := tx.Commit(); err != nil {
- return fmt.Errorf(`store: unable to commit transaction: %v`, err)
+ return nil, fmt.Errorf(`store: unable to commit transaction: %v`, err)
}
entryHashes = append(entryHashes, entry.Hash)
@@ -312,7 +320,7 @@ func (s *Storage) RefreshFeedEntries(userID, feedID int64, entries model.Entries
}
}()
- return nil
+ return newEntries, nil
}
// ArchiveEntries changes the status of entries to "removed" after the given number of days.
diff --git a/internal/storage/feed.go b/internal/storage/feed.go
index 01fba798..5cf32181 100644
--- a/internal/storage/feed.go
+++ b/internal/storage/feed.go
@@ -345,9 +345,9 @@ func (s *Storage) UpdateFeed(feed *model.Feed) (err error) {
hide_globally=$24,
url_rewrite_rules=$25,
no_media_player=$26,
- apprise_service_urls=$29
+ apprise_service_urls=$27
WHERE
- id=$27 AND user_id=$28
+ id=$28 AND user_id=$29
`
_, err = s.db.Exec(query,
feed.FeedURL,
@@ -376,9 +376,9 @@ func (s *Storage) UpdateFeed(feed *model.Feed) (err error) {
feed.HideGlobally,
feed.UrlRewriteRules,
feed.NoMediaPlayer,
+ feed.AppriseServiceURLs,
feed.ID,
feed.UserID,
- feed.AppriseServiceURLs,
)
if err != nil {
diff --git a/internal/storage/integration.go b/internal/storage/integration.go
index c68168d0..2c1e85a2 100644
--- a/internal/storage/integration.go
+++ b/internal/storage/integration.go
@@ -167,7 +167,10 @@ func (s *Storage) Integration(userID int64) (*model.Integration, error) {
shiori_password,
shaarli_enabled,
shaarli_url,
- shaarli_api_secret
+ shaarli_api_secret,
+ webhook_enabled,
+ webhook_url,
+ webhook_secret
FROM
integrations
WHERE
@@ -234,6 +237,9 @@ func (s *Storage) Integration(userID int64) (*model.Integration, error) {
&integration.ShaarliEnabled,
&integration.ShaarliURL,
&integration.ShaarliAPISecret,
+ &integration.WebhookEnabled,
+ &integration.WebhookURL,
+ &integration.WebhookSecret,
)
switch {
case err == sql.ErrNoRows:
@@ -308,9 +314,12 @@ func (s *Storage) UpdateIntegration(integration *model.Integration) error {
shiori_password=$55,
shaarli_enabled=$56,
shaarli_url=$57,
- shaarli_api_secret=$58
+ shaarli_api_secret=$58,
+ webhook_enabled=$59,
+ webhook_url=$60,
+ webhook_secret=$61
WHERE
- user_id=$59
+ user_id=$62
`
_, err := s.db.Exec(
query,
@@ -372,6 +381,9 @@ func (s *Storage) UpdateIntegration(integration *model.Integration) error {
integration.ShaarliEnabled,
integration.ShaarliURL,
integration.ShaarliAPISecret,
+ integration.WebhookEnabled,
+ integration.WebhookURL,
+ integration.WebhookSecret,
integration.UserID,
)
diff --git a/internal/template/templates/views/integrations.html b/internal/template/templates/views/integrations.html
index 255bc825..14b7728e 100644
--- a/internal/template/templates/views/integrations.html
+++ b/internal/template/templates/views/integrations.html
@@ -13,21 +13,66 @@
<div class="alert alert-error">{{ t .errorMessage }}</div>
{{ end }}
+ <details {{ if .form.AppriseEnabled }}open{{ end }}>
+ <summary>Apprise</summary>
+ <div class="form-section">
+ <label>
+ <input type="checkbox" name="apprise_enabled" value="1" {{ if .form.AppriseEnabled }}checked{{ end }}> {{ t "form.integration.apprise_activate" }}
+ </label>
+
+ <label for="form-apprise-url">{{ t "form.integration.apprise_url" }}</label>
+ <input type="url" name="apprise_url" id="form-apprise-url" value="{{ .form.AppriseURL }}" placeholder="http://apprise:8080" spellcheck="false">
+
+ <label for="form-apprise-services-url">{{ t "form.integration.apprise_services_url" }}
+ <a href="https://github.com/caronc/apprise/wiki" target="_blank">
+ {{ icon "external-link" }}
+ </a>
+ </label>
+ <input type="text" name="apprise_services_url" id="form-apprise-services-urls" value="{{ .form.AppriseServicesURL }}" placeholder="tgram://<token>/<chat_id>/,matrix://" 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.EspialEnabled }}open{{ end }}>
+ <summary>Espial</summary>
+ <div class="form-section">
+ <label>
+ <input type="checkbox" name="espial_enabled" value="1" {{ if .form.EspialEnabled }}checked{{ end }}> {{ t "form.integration.espial_activate" }}
+ </label>
+
+ <label for="form-espial-url">{{ t "form.integration.espial_endpoint" }}</label>
+ <input type="url" name="espial_url" id="form-espial-url" value="{{ .form.EspialURL }}" placeholder="https://esp.ae8.org" spellcheck="false">
+
+ <label for="form-espial-api-key">{{ t "form.integration.espial_api_key" }}</label>
+ <input type="text" name="espial_api_key" id="form-espial-api-key" value="{{ .form.EspialAPIKey }}" spellcheck="false">
+
+ <label for="form-espial-tags">{{ t "form.integration.espial_tags" }}</label>
+ <input type="text" name="espial_tags" id="form-espial-tags" value="{{ .form.EspialTags }}" 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.FeverEnabled }}open{{ end }}>
<summary>Fever</summary>
<div class="form-section">
<label>
<input type="checkbox" name="fever_enabled" value="1" {{ if .form.FeverEnabled }}checked{{ end }}> {{ t "form.integration.fever_activate" }}
</label>
-
+
<label for="form-fever-username">{{ t "form.integration.fever_username" }}</label>
<input type="text" name="fever_username" id="form-fever-username" value="{{ .form.FeverUsername }}" autocomplete="username" spellcheck="false">
-
+
<label for="form-fever-password">{{ t "form.integration.fever_password" }}</label>
<input type="password" name="fever_password" id="form-fever-password" value="{{ .form.FeverPassword }}" autocomplete="new-password">
-
+
<p>{{ t "form.integration.fever_endpoint" }} <strong>{{ rootURL }}{{ route "feverEndpoint" }}</strong></p>
-
+
<div class="buttons">
<button type="submit" class="button button-primary" data-label-loading="{{ t "form.submit.saving" }}">{{ t "action.update" }}</button>
</div>
@@ -40,38 +85,15 @@
<label>
<input type="checkbox" name="googlereader_enabled" value="1" {{ if .form.GoogleReaderEnabled }}checked{{ end }}> {{ t "form.integration.googlereader_activate" }}
</label>
-
+
<label for="form-googlereader-username">{{ t "form.integration.googlereader_username" }}</label>
<input type="text" name="googlereader_username" id="form-googlereader-username" value="{{ .form.GoogleReaderUsername }}" autocomplete="username" spellcheck="false">
-
+
<label for="form-googlereader-password">{{ t "form.integration.googlereader_password" }}</label>
<input type="password" name="googlereader_password" id="form-googlereader-password" value="{{ .form.GoogleReaderPassword }}" autocomplete="new-password">
-
+
<p>{{ t "form.integration.googlereader_endpoint" }} <strong>{{ rootURL }}{{ route "login" }}</strong></p>
-
- <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.PinboardEnabled }}open{{ end }}>
- <summary>Pinboard</summary>
- <div class="form-section">
- <label>
- <input type="checkbox" name="pinboard_enabled" value="1" {{ if .form.PinboardEnabled }}checked{{ end }}> {{ t "form.integration.pinboard_activate" }}
- </label>
-
- <label for="form-pinboard-token">{{ t "form.integration.pinboard_token" }}</label>
- <input type="password" name="pinboard_token" id="form-pinboard-token" value="{{ .form.PinboardToken }}" autocomplete="new-password">
-
- <label for="form-pinboard-tags">{{ t "form.integration.pinboard_tags" }}</label>
- <input type="text" name="pinboard_tags" id="form-pinboard-tags" value="{{ .form.PinboardTags }}" spellcheck="false">
-
- <label>
- <input type="checkbox" name="pinboard_mark_as_unread" value="1" {{ if .form.PinboardMarkAsUnread }}checked{{ end }}> {{ t "form.integration.pinboard_bookmark" }}
- </label>
-
<div class="buttons">
<button type="submit" class="button button-primary" data-label-loading="{{ t "form.submit.saving" }}">{{ t "action.update" }}</button>
</div>
@@ -84,70 +106,64 @@
<label>
<input type="checkbox" name="instapaper_enabled" value="1" {{ if .form.InstapaperEnabled }}checked{{ end }}> {{ t "form.integration.instapaper_activate" }}
</label>
-
+
<label for="form-instapaper-username">{{ t "form.integration.instapaper_username" }}</label>
<input type="text" name="instapaper_username" id="form-instapaper-username" value="{{ .form.InstapaperUsername }}" spellcheck="false">
-
+
<label for="form-instapaper-password">{{ t "form.integration.instapaper_password" }}</label>
<input type="password" name="instapaper_password" id="form-instapaper-password" value="{{ .form.InstapaperPassword }}" autocomplete="new-password">
-
+
<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.PocketEnabled }}open{{ end }}>
- <summary>Pocket</summary>
+ <details {{ if .form.LinkdingEnabled }}open{{ end }}>
+ <summary>Linkding</summary>
<div class="form-section">
<label>
- <input type="checkbox" name="pocket_enabled" value="1" {{ if .form.PocketEnabled }}checked{{ end }}> {{ t "form.integration.pocket_activate" }}
+ <input type="checkbox" name="linkding_enabled" value="1" {{ if .form.LinkdingEnabled }}checked{{ end }}> {{ t "form.integration.linkding_activate" }}
</label>
-
- {{ if not .hasPocketConsumerKeyConfigured }}
- <label for="form-pocket-consumer-key">{{ t "form.integration.pocket_consumer_key" }}</label>
- <input type="text" name="pocket_consumer_key" id="form-pocket-consumer-key" value="{{ .form.PocketConsumerKey }}" spellcheck="false">
- {{ end }}
-
- <label for="form-pocket-access-token">{{ t "form.integration.pocket_access_token" }}</label>
- <input type="password" name="pocket_access_token" id="form-pocket-access-token" value="{{ .form.PocketAccessToken }}" autocomplete="new-password">
-
- {{ if not .form.PocketAccessToken }}
- <p><a href="{{ route "pocketAuthorize" }}">{{ t "form.integration.pocket_connect_link" }}</a></p>
- {{ end }}
-
+
+ <label for="form-linkding-url">{{ t "form.integration.linkding_endpoint" }}</label>
+ <input type="url" name="linkding_url" id="form-linkding-url" value="{{ .form.LinkdingURL }}" placeholder="https://linkding.com" spellcheck="false">
+
+ <label for="form-linkding-api-key">{{ t "form.integration.linkding_api_key" }}</label>
+ <input type="text" name="linkding_api_key" id="form-linkding-api-key" value="{{ .form.LinkdingAPIKey }}" spellcheck="false">
+
+ <label for="form-linkding-tags">{{ t "form.integration.linkding_tags" }}</label>
+ <input type="text" name="linkding_tags" id="form-linkding-tags" value="{{ .form.LinkdingTags }}" spellcheck="false">
+
+ <label>
+ <input type="checkbox" name="linkding_mark_as_unread" value="1" {{ if .form.LinkdingMarkAsUnread }}checked{{ end }}> {{ t "form.integration.linkding_bookmark" }}
+ </label>
+
<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.WallabagEnabled }}open{{ end }}>
- <summary>Wallabag</summary>
+ <details {{ if .form.MatrixBotEnabled }}open{{ end }}>
+ <summary>Matrix Bot</summary>
<div class="form-section">
<label>
- <input type="checkbox" name="wallabag_enabled" value="1" {{ if .form.WallabagEnabled }}checked{{ end }}> {{ t "form.integration.wallabag_activate" }}
- </label>
-
- <label>
- <input type="checkbox" name="wallabag_only_url" value="1" {{ if .form.WallabagOnlyURL }}checked{{ end }}> {{ t "form.integration.wallabag_only_url" }}
+ <input type="checkbox" name="matrix_bot_enabled" value="1" {{ if .form.MatrixBotEnabled }}checked{{ end }}> {{ t "form.integration.matrix_bot_activate" }}
</label>
-
- <label for="form-wallabag-url">{{ t "form.integration.wallabag_endpoint" }}</label>
- <input type="url" name="wallabag_url" id="form-wallabag-url" value="{{ .form.WallabagURL }}" placeholder="http://v2.wallabag.org/" spellcheck="false">
-
- <label for="form-wallabag-client-id">{{ t "form.integration.wallabag_client_id" }}</label>
- <input type="text" name="wallabag_client_id" id="form-wallabag-client-id" value="{{ .form.WallabagClientID }}" spellcheck="false">
-
- <label for="form-wallabag-client-secret">{{ t "form.integration.wallabag_client_secret" }}</label>
- <input type="password" name="wallabag_client_secret" id="form-wallabag-client-secret" value="{{ .form.WallabagClientSecret }}" autocomplete="new-password">
-
- <label for="form-wallabag-username">{{ t "form.integration.wallabag_username" }}</label>
- <input type="text" name="wallabag_username" id="form-wallabag-username" value="{{ .form.WallabagUsername }}" spellcheck="false">
-
- <label for="form-wallabag-password">{{ t "form.integration.wallabag_password" }}</label>
- <input type="password" name="wallabag_password" id="form-wallabag-password" value="{{ .form.WallabagPassword }}" autocomplete="new-password">
-
+
+ <label for="form-matrix-bot-user">{{ t "form.integration.matrix_bot_user" }}</label>
+ <input type="text" name="matrix_bot_user" id="form-matrix-bot-user" value="{{ .form.MatrixBotUser }}" spellcheck="false">
+
+ <label for="form-matrix-chat-password">{{ t "form.integration.matrix_bot_password" }}</label>
+ <input type="password" name="matrix_bot_password" id="form-matrix-password" value="{{ .form.MatrixBotPassword }}" spellcheck="false">
+
+ <label for="form-matrix-url">{{ t "form.integration.matrix_bot_url" }}</label>
+ <input type="url" name="matrix_bot_url" id="form-matrix-url" value="{{ .form.MatrixBotURL }}" spellcheck="false">
+
+ <label for="form-matrix-chat-id">{{ t "form.integration.matrix_bot_chat_id" }}</label>
+ <input type="text" name="matrix_bot_chat_id" id="form-matrix-chat-id" value="{{ .form.MatrixBotChatID }}" spellcheck="false">
+
<div class="buttons">
<button type="submit" class="button button-primary" data-label-loading="{{ t "form.submit.saving" }}">{{ t "action.update" }}</button>
</div>
@@ -160,13 +176,13 @@
<label>
<input type="checkbox" name="notion_enabled" value="1" {{ if .form.NotionEnabled }}checked{{ end }}> {{ t "form.integration.notion_activate" }}
</label>
-
+
<label for="form-notion-token">{{ t "form.integration.notion_token" }}</label>
<input type="password" name="notion_token" id="form-notion-token" value="{{ .form.NotionToken }}" spellcheck="false">
-
+
<label for="form-notion-page-id">{{ t "form.integration.notion_page_id" }}</label>
<input type="text" name="notion_page_id" id="form-notion-page-id" value="{{ .form.NotionPageID }}" spellcheck="false">
-
+
<div class="buttons">
<button type="submit" class="button button-primary" data-label-loading="{{ t "form.submit.saving" }}">{{ t "action.update" }}</button>
</div>
@@ -179,35 +195,61 @@
<label>
<input type="checkbox" name="nunux_keeper_enabled" value="1" {{ if .form.NunuxKeeperEnabled }}checked{{ end }}> {{ t "form.integration.nunux_keeper_activate" }}
</label>
-
+
<label for="form-nunux-keeper-url">{{ t "form.integration.nunux_keeper_endpoint" }}</label>
<input type="url" name="nunux_keeper_url" id="form-nunux-keeper-url" value="{{ .form.NunuxKeeperURL }}" placeholder="https://api.nunux.org/keeper" spellcheck="false">
-
+
<label for="form-nunux-keeper-api-key">{{ t "form.integration.nunux_keeper_api_key" }}</label>
<input type="text" name="nunux_keeper_api_key" id="form-nunux-keeper-api-key" value="{{ .form.NunuxKeeperAPIKey }}" 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.EspialEnabled }}open{{ end }}>
- <summary>Espial</summary>
+ <details {{ if .form.PinboardEnabled }}open{{ end }}>
+ <summary>Pinboard</summary>
<div class="form-section">
<label>
- <input type="checkbox" name="espial_enabled" value="1" {{ if .form.EspialEnabled }}checked{{ end }}> {{ t "form.integration.espial_activate" }}
+ <input type="checkbox" name="pinboard_enabled" value="1" {{ if .form.PinboardEnabled }}checked{{ end }}> {{ t "form.integration.pinboard_activate" }}
</label>
-
- <label for="form-espial-url">{{ t "form.integration.espial_endpoint" }}</label>
- <input type="url" name="espial_url" id="form-espial-url" value="{{ .form.EspialURL }}" placeholder="https://esp.ae8.org" spellcheck="false">
-
- <label for="form-espial-api-key">{{ t "form.integration.espial_api_key" }}</label>
- <input type="text" name="espial_api_key" id="form-espial-api-key" value="{{ .form.EspialAPIKey }}" spellcheck="false">
-
- <label for="form-espial-tags">{{ t "form.integration.espial_tags" }}</label>
- <input type="text" name="espial_tags" id="form-espial-tags" value="{{ .form.EspialTags }}" spellcheck="false">
-
+
+ <label for="form-pinboard-token">{{ t "form.integration.pinboard_token" }}</label>
+ <input type="password" name="pinboard_token" id="form-pinboard-token" value="{{ .form.PinboardToken }}" autocomplete="new-password">
+
+ <label for="form-pinboard-tags">{{ t "form.integration.pinboard_tags" }}</label>
+ <input type="text" name="pinboard_tags" id="form-pinboard-tags" value="{{ .form.PinboardTags }}" spellcheck="false">
+
+ <label>
+ <input type="checkbox" name="pinboard_mark_as_unread" value="1" {{ if .form.PinboardMarkAsUnread }}checked{{ end }}> {{ t "form.integration.pinboard_bookmark" }}
+ </label>
+
+ <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.PocketEnabled }}open{{ end }}>
+ <summary>Pocket</summary>
+ <div class="form-section">
+ <label>
+ <input type="checkbox" name="pocket_enabled" value="1" {{ if .form.PocketEnabled }}checked{{ end }}> {{ t "form.integration.pocket_activate" }}
+ </label>
+
+ {{ if not .hasPocketConsumerKeyConfigured }}
+ <label for="form-pocket-consumer-key">{{ t "form.integration.pocket_consumer_key" }}</label>
+ <input type="text" name="pocket_consumer_key" id="form-pocket-consumer-key" value="{{ .form.PocketConsumerKey }}" spellcheck="false">
+ {{ end }}
+
+ <label for="form-pocket-access-token">{{ t "form.integration.pocket_access_token" }}</label>
+ <input type="password" name="pocket_access_token" id="form-pocket-access-token" value="{{ .form.PocketAccessToken }}" autocomplete="new-password">
+
+ {{ if not .form.PocketAccessToken }}
+ <p><a href="{{ route "pocketAuthorize" }}">{{ t "form.integration.pocket_connect_link" }}</a></p>
+ {{ end }}
+
<div class="buttons">
<button type="submit" class="button button-primary" data-label-loading="{{ t "form.submit.saving" }}">{{ t "action.update" }}</button>
</div>
@@ -220,61 +262,53 @@
<label>
<input type="checkbox" name="readwise_enabled" value="1" {{ if .form.ReadwiseEnabled }}checked{{ end }}> {{ t "form.integration.readwise_activate" }}
</label>
-
+
<label for="form-readwise-api-key">{{ t "form.integration.readwise_api_key" }}</label>
<input type="text" name="readwise_api_key" id="form-readwise-api-key" value="{{ .form.ReadwiseAPIKey }}" spellcheck="false">
-
+
<p><a href="https://readwise.io/access_token" target="_blank">{{ t "form.integration.readwise_api_key_link" }}</a></p>
-
+
<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.LinkdingEnabled }}open{{ end }}>
- <summary>Linkding</summary>
+ <details {{ if .form.ShaarliEnabled }}open{{ end }}>
+ <summary>Shaarli</summary>
<div class="form-section">
<label>
- <input type="checkbox" name="linkding_enabled" value="1" {{ if .form.LinkdingEnabled }}checked{{ end }}> {{ t "form.integration.linkding_activate" }}
- </label>
-
- <label for="form-linkding-url">{{ t "form.integration.linkding_endpoint" }}</label>
- <input type="url" name="linkding_url" id="form-linkding-url" value="{{ .form.LinkdingURL }}" placeholder="https://linkding.com" spellcheck="false">
-
- <label for="form-linkding-api-key">{{ t "form.integration.linkding_api_key" }}</label>
- <input type="text" name="linkding_api_key" id="form-linkding-api-key" value="{{ .form.LinkdingAPIKey }}" spellcheck="false">
-
- <label for="form-linkding-tags">{{ t "form.integration.linkding_tags" }}</label>
- <input type="text" name="linkding_tags" id="form-linkding-tags" value="{{ .form.LinkdingTags }}" spellcheck="false">
-
- <label>
- <input type="checkbox" name="linkding_mark_as_unread" value="1" {{ if .form.LinkdingMarkAsUnread }}checked{{ end }}> {{ t "form.integration.linkding_bookmark" }}
+ <input type="checkbox" name="shaarli_enabled" value="1" {{ if .form.ShaarliEnabled }}checked{{ end }}> {{ t "form.integration.shaarli_activate" }}
</label>
-
+
+ <label for="form-shaarli-url">{{ t "form.integration.shaarli_endpoint" }}</label>
+ <input type="url" name="shaarli_url" id="form-shaarli-url" value="{{ .form.ShaarliURL }}" placeholder="https://shaarli.example.org" spellcheck="false">
+
+ <label for="form-shaarli-api-secret">{{ t "form.integration.shaarli_api_secret" }}</label>
+ <input type="password" name="shaarli_api_secret" id="form-shaarli-api-secret" value="{{ .form.ShaarliAPISecret }}" autocomplete="new-password">
+
<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.AppriseEnabled }}open{{ end }}>
- <summary>Apprise</summary>
+ <details {{ if .form.ShioriEnabled }}open{{ end }}>
+ <summary>Shiori</summary>
<div class="form-section">
<label>
- <input type="checkbox" name="apprise_enabled" value="1" {{ if .form.AppriseEnabled }}checked{{ end }}> {{ t "form.integration.apprise_activate" }}
- </label>
-
- <label for="form-apprise-url">{{ t "form.integration.apprise_url" }}</label>
- <input type="text" name="apprise_url" id="form-apprise-url" value="{{ .form.AppriseURL }}" placeholder="http://apprise:8080" spellcheck="false">
-
- <label for="form-apprise-services-url">{{ t "form.integration.apprise_services_url" }}
- <a href="https://github.com/caronc/apprise/wiki" target="_blank">
- {{ icon "external-link" }}
- </a>
+ <input type="checkbox" name="shiori_enabled" value="1" {{ if .form.ShioriEnabled }}checked{{ end }}> {{ t "form.integration.shiori_activate" }}
</label>
- <input type="text" name="apprise_services_url" id="form-apprise-services-urls" value="{{ .form.AppriseServicesURL }}" placeholder="tgram://<token>/<chat_id>/,matrix://" spellcheck="false">
-
+
+ <label for="form-shiori-url">{{ t "form.integration.shiori_endpoint" }}</label>
+ <input type="url" name="shiori_url" id="form-shiori-url" value="{{ .form.ShioriURL }}" placeholder="https://shiori.example.org" spellcheck="false">
+
+ <label for="form-shiori-username">{{ t "form.integration.shiori_username" }}</label>
+ <input type="text" name="shiori_username" id="form-shiori-username" value="{{ .form.ShioriUsername }}" spellcheck="false">
+
+ <label for="form-shiori-password">{{ t "form.integration.shiori_password" }}</label>
+ <input type="password" name="shiori_password" id="form-shiori-password" value="{{ .form.ShioriPassword }}" autocomplete="new-password">
+
<div class="buttons">
<button type="submit" class="button button-primary" data-label-loading="{{ t "form.submit.saving" }}">{{ t "action.update" }}</button>
</div>
@@ -287,56 +321,44 @@
<label>
<input type="checkbox" name="telegram_bot_enabled" value="1" {{ if .form.TelegramBotEnabled }}checked{{ end }}> {{ t "form.integration.telegram_bot_activate" }}
</label>
-
+
<label for="form-telegram-bot-token">{{ t "form.integration.telegram_bot_token" }}</label>
<input type="text" name="telegram_bot_token" id="form-telegram-bot-token" value="{{ .form.TelegramBotToken }}" placeholder="bot123456:Abcdefg" spellcheck="false">
-
+
<label for="form-telegram-chat-id">{{ t "form.integration.telegram_chat_id" }}</label>
<input type="text" name="telegram_bot_chat_id" id="form-telegram-chat-id" value="{{ .form.TelegramBotChatID }}" 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.MatrixBotEnabled }}open{{ end }}>
- <summary>Matrix Bot</summary>
+ <details {{ if .form.WallabagEnabled }}open{{ end }}>
+ <summary>Wallabag</summary>
<div class="form-section">
<label>
- <input type="checkbox" name="matrix_bot_enabled" value="1" {{ if .form.MatrixBotEnabled }}checked{{ end }}> {{ t "form.integration.matrix_bot_activate" }}
+ <input type="checkbox" name="wallabag_enabled" value="1" {{ if .form.WallabagEnabled }}checked{{ end }}> {{ t "form.integration.wallabag_activate" }}
</label>
-
- <label for="form-matrix-bot-user">{{ t "form.integration.matrix_bot_user" }}</label>
- <input type="text" name="matrix_bot_user" id="form-matrix-bot-user" value="{{ .form.MatrixBotUser }}" spellcheck="false">
-
- <label for="form-matrix-chat-password">{{ t "form.integration.matrix_bot_password" }}</label>
- <input type="password" name="matrix_bot_password" id="form-matrix-password" value="{{ .form.MatrixBotPassword }}" spellcheck="false">
-
- <label for="form-matrix-url">{{ t "form.integration.matrix_bot_url" }}</label>
- <input type="text" name="matrix_bot_url" id="form-matrix-url" value="{{ .form.MatrixBotURL }}" spellcheck="false">
-
- <label for="form-matrix-chat-id">{{ t "form.integration.matrix_bot_chat_id" }}</label>
- <input type="text" name="matrix_bot_chat_id" id="form-matrix-chat-id" value="{{ .form.MatrixBotChatID }}" 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.ShaarliEnabled }}open{{ end }}>
- <summary>Shaarli</summary>
- <div class="form-section">
<label>
- <input type="checkbox" name="shaarli_enabled" value="1" {{ if .form.ShaarliEnabled }}checked{{ end }}> {{ t "form.integration.shaarli_activate" }}
+ <input type="checkbox" name="wallabag_only_url" value="1" {{ if .form.WallabagOnlyURL }}checked{{ end }}> {{ t "form.integration.wallabag_only_url" }}
</label>
- <label for="form-shaarli-url">{{ t "form.integration.shaarli_endpoint" }}</label>
- <input type="url" name="shaarli_url" id="form-shaarli-url" value="{{ .form.ShaarliURL }}" placeholder="https://shaarli.example.org" spellcheck="false">
+ <label for="form-wallabag-url">{{ t "form.integration.wallabag_endpoint" }}</label>
+ <input type="url" name="wallabag_url" id="form-wallabag-url" value="{{ .form.WallabagURL }}" placeholder="http://v2.wallabag.org/" spellcheck="false">
- <label for="form-shaarli-api-secret">{{ t "form.integration.shaarli_api_secret" }}</label>
- <input type="password" name="shaarli_api_secret" id="form-shaarli-api-secret" value="{{ .form.ShaarliAPISecret }}" autocomplete="new-password">
+ <label for="form-wallabag-client-id">{{ t "form.integration.wallabag_client_id" }}</label>
+ <input type="text" name="wallabag_client_id" id="form-wallabag-client-id" value="{{ .form.WallabagClientID }}" spellcheck="false">
+
+ <label for="form-wallabag-client-secret">{{ t "form.integration.wallabag_client_secret" }}</label>
+ <input type="password" name="wallabag_client_secret" id="form-wallabag-client-secret" value="{{ .form.WallabagClientSecret }}" autocomplete="new-password">
+
+ <label for="form-wallabag-username">{{ t "form.integration.wallabag_username" }}</label>
+ <input type="text" name="wallabag_username" id="form-wallabag-username" value="{{ .form.WallabagUsername }}" spellcheck="false">
+
+ <label for="form-wallabag-password">{{ t "form.integration.wallabag_password" }}</label>
+ <input type="password" name="wallabag_password" id="form-wallabag-password" value="{{ .form.WallabagPassword }}" autocomplete="new-password">
<div class="buttons">
<button type="submit" class="button button-primary" data-label-loading="{{ t "form.submit.saving" }}">{{ t "action.update" }}</button>
@@ -344,21 +366,20 @@
</div>
</details>
- <details {{ if .form.ShioriEnabled }}open{{ end }}>
- <summary>Shiori</summary>
+ <details {{ if .form.WebhookEnabled }}open{{ end }}>
+ <summary>Webhook</summary>
<div class="form-section">
<label>
- <input type="checkbox" name="shiori_enabled" value="1" {{ if .form.ShioriEnabled }}checked{{ end }}> {{ t "form.integration.shiori_activate" }}
+ <input type="checkbox" name="webhook_enabled" value="1" {{ if .form.WebhookEnabled }}checked{{ end }}> {{ t "form.integration.webhook_activate" }}
</label>
- <label for="form-shiori-url">{{ t "form.integration.shiori_endpoint" }}</label>
- <input type="url" name="shiori_url" id="form-shiori-url" value="{{ .form.ShioriURL }}" placeholder="https://shiori.example.org" spellcheck="false">
+ <label for="form-webhook-url">{{ t "form.integration.webhook_url" }}</label>
+ <input type="url" name="webhook_url" id="form-webhook-url" value="{{ .form.WebhookURL }}" placeholder="https://username:password@example.org" spellcheck="false">
- <label for="form-shiori-username">{{ t "form.integration.shiori_username" }}</label>
- <input type="text" name="shiori_username" id="form-shiori-username" value="{{ .form.ShioriUsername }}" spellcheck="false">
-
- <label for="form-shiori-password">{{ t "form.integration.shiori_password" }}</label>
- <input type="password" name="shiori_password" id="form-shiori-password" value="{{ .form.ShioriPassword }}" autocomplete="new-password">
+ {{ if .form.WebhookSecret }}
+ <label for="form-webhook-secret">{{ t "form.integration.webhook_secret" }}</label>
+ <input type="text" name="webhook_secret" id="form-webhook-secret" value="{{ .form.WebhookSecret }}" spellcheck="false" readonly>
+ {{ end }}
<div class="buttons">
<button type="submit" class="button button-primary" data-label-loading="{{ t "form.submit.saving" }}">{{ t "action.update" }}</button>
diff --git a/internal/ui/form/integration.go b/internal/ui/form/integration.go
index 53ff9202..de7b1630 100644
--- a/internal/ui/form/integration.go
+++ b/internal/ui/form/integration.go
@@ -69,6 +69,9 @@ type IntegrationForm struct {
ShaarliEnabled bool
ShaarliURL string
ShaarliAPISecret string
+ WebhookEnabled bool
+ WebhookURL string
+ WebhookSecret string
}
// Merge copy form values to the model.
@@ -129,6 +132,8 @@ func (i IntegrationForm) Merge(integration *model.Integration) {
integration.ShaarliEnabled = i.ShaarliEnabled
integration.ShaarliURL = i.ShaarliURL
integration.ShaarliAPISecret = i.ShaarliAPISecret
+ integration.WebhookEnabled = i.WebhookEnabled
+ integration.WebhookURL = i.WebhookURL
}
// NewIntegrationForm returns a new IntegrationForm.
@@ -192,5 +197,7 @@ func NewIntegrationForm(r *http.Request) *IntegrationForm {
ShaarliEnabled: r.FormValue("shaarli_enabled") == "1",
ShaarliURL: r.FormValue("shaarli_url"),
ShaarliAPISecret: r.FormValue("shaarli_api_secret"),
+ WebhookEnabled: r.FormValue("webhook_enabled") == "1",
+ WebhookURL: r.FormValue("webhook_url"),
}
}
diff --git a/internal/ui/integration_show.go b/internal/ui/integration_show.go
index 7a30a043..abde5348 100644
--- a/internal/ui/integration_show.go
+++ b/internal/ui/integration_show.go
@@ -84,6 +84,9 @@ func (h *handler) showIntegrationPage(w http.ResponseWriter, r *http.Request) {
ShaarliEnabled: integration.ShaarliEnabled,
ShaarliURL: integration.ShaarliURL,
ShaarliAPISecret: integration.ShaarliAPISecret,
+ WebhookEnabled: integration.WebhookEnabled,
+ WebhookURL: integration.WebhookURL,
+ WebhookSecret: integration.WebhookSecret,
}
sess := session.New(h.store, request.SessionID(r))
diff --git a/internal/ui/integration_update.go b/internal/ui/integration_update.go
index 4a68691c..b8bfed44 100644
--- a/internal/ui/integration_update.go
+++ b/internal/ui/integration_update.go
@@ -67,6 +67,18 @@ func (h *handler) updateIntegration(w http.ResponseWriter, r *http.Request) {
integration.GoogleReaderPassword = ""
}
+ if integrationForm.WebhookEnabled {
+ if integrationForm.WebhookURL == "" {
+ integration.WebhookEnabled = false
+ integration.WebhookSecret = ""
+ } else if integration.WebhookSecret == "" {
+ integration.WebhookSecret = crypto.GenerateRandomStringHex(32)
+ }
+ } else {
+ integration.WebhookURL = ""
+ integration.WebhookSecret = ""
+ }
+
err = h.store.UpdateIntegration(integration)
if err != nil {
html.ServerError(w, r, err)