From 48f6885f4472efbe0e23f990ae8d4545f9a6a73d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Guillot?= Date: Fri, 8 Sep 2023 22:45:17 -0700 Subject: [PATCH] Add generic webhook integration --- internal/crypto/crypto.go | 7 + internal/database/migrations.go | 9 + internal/integration/integration.go | 65 ++- internal/integration/webhook/webhook.go | 64 +++ internal/locale/translations/de_DE.json | 3 + internal/locale/translations/el_EL.json | 3 + internal/locale/translations/en_US.json | 3 + internal/locale/translations/es_ES.json | 3 + internal/locale/translations/fi_FI.json | 3 + internal/locale/translations/fr_FR.json | 3 + internal/locale/translations/hi_IN.json | 3 + internal/locale/translations/id_ID.json | 3 + internal/locale/translations/it_IT.json | 3 + internal/locale/translations/ja_JP.json | 3 + internal/locale/translations/nl_NL.json | 3 + internal/locale/translations/pl_PL.json | 3 + internal/locale/translations/pt_BR.json | 3 + internal/locale/translations/ru_RU.json | 3 + internal/locale/translations/tr_TR.json | 3 + internal/locale/translations/uk_UA.json | 3 + internal/locale/translations/zh_CN.json | 3 + internal/locale/translations/zh_TW.json | 3 + internal/model/entry.go | 7 + internal/model/integration.go | 3 + internal/reader/atom/atom_03.go | 2 +- internal/reader/atom/atom_10.go | 4 +- internal/reader/handler/handler.go | 12 +- internal/reader/json/json.go | 7 +- internal/reader/processor/processor.go | 27 - internal/reader/rdf/rdf.go | 2 +- internal/reader/rss/rss.go | 4 +- internal/storage/enclosure.go | 25 +- internal/storage/entry.go | 30 +- internal/storage/feed.go | 6 +- internal/storage/integration.go | 18 +- .../templates/views/integrations.html | 483 +++++++++--------- internal/ui/form/integration.go | 7 + internal/ui/integration_show.go | 3 + internal/ui/integration_update.go | 12 + 39 files changed, 527 insertions(+), 324 deletions(-) create mode 100644 internal/integration/webhook/webhook.go 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 - } + // 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) - client := apprise.NewClient( - appriseServiceURLs, - integration.AppriseURL, - ) + err := telegrambot.PushEntry(entry, userIntegrations.TelegramBotToken, userIntegrations.TelegramBotChatID) + if err != nil { + logger.Error("[Integration] push entry to telegram bot failed: %v", err) + } + } - if err := client.SendNotification(entry); err != nil { - logger.Error("[Integration] push entry to apprise 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, "{{ t .errorMessage }} {{ end }} -
- Fever +
+ Apprise
- - - - - - - -

{{ t "form.integration.fever_endpoint" }} {{ rootURL }}{{ route "feverEndpoint" }}

- -
- -
-
-
-
- Google Reader -
- - - - - - - - -

{{ t "form.integration.googlereader_endpoint" }} {{ rootURL }}{{ route "login" }}

- -
- -
-
-
+ + -
- Pinboard -
-
-
+ -
- Instapaper -
- - - - - - - - -
- -
-
-
- -
- Pocket -
- - - {{ if not .hasPocketConsumerKeyConfigured }} - - - {{ end }} - - - - - {{ if not .form.PocketAccessToken }} -

{{ t "form.integration.pocket_connect_link" }}

- {{ end }} - -
- -
-
-
- -
- Wallabag -
- - - - - - - - - - - - - - - - - - - -
- -
-
-
- -
- Notion -
- - - - - - - - -
- -
-
-
- -
- Nunux Keeper -
- - - - - - - -
@@ -198,34 +42,77 @@ - + - + - + - +
-
- Readwise Reader +
+ Fever
- - - - -

{{ t "form.integration.readwise_api_key_link" }}

- + + + + + + + +

{{ t "form.integration.fever_endpoint" }} {{ rootURL }}{{ route "feverEndpoint" }}

+ +
+ +
+
+
+ +
+ Google Reader +
+ + + + + + + + +

{{ t "form.integration.googlereader_endpoint" }} {{ rootURL }}{{ route "login" }}

+ +
+ +
+
+
+ +
+ Instapaper +
+ + + + + + + +
@@ -238,62 +125,20 @@ - + - + - + - + - -
- -
-
-
-
- Apprise -
- - - - - - - - -
- -
-
-
- -
- Telegram Bot -
- - - - - - - -
@@ -306,19 +151,123 @@ - + - + - + - - + + - + +
+ +
+
+
+ +
+ Notion +
+ + + + + + + + +
+ +
+
+
+ +
+ Nunux Keeper +
+ + + + + + + + +
+ +
+
+
+ +
+ Pinboard +
+ + + + + + + + + + +
+ +
+
+
+ +
+ Pocket +
+ + + {{ if not .hasPocketConsumerKeyConfigured }} + + + {{ end }} + + + + + {{ if not .form.PocketAccessToken }} +

{{ t "form.integration.pocket_connect_link" }}

+ {{ end }} + +
+ +
+
+
+ +
+ Readwise Reader +
+ + + + + +

{{ t "form.integration.readwise_api_key_link" }}

+
@@ -365,6 +314,78 @@
+ +
+ Telegram Bot +
+ + + + + + + + +
+ +
+
+
+ +
+ Wallabag +
+ + + + + + + + + + + + + + + + + + + +
+ +
+
+
+ +
+ Webhook +
+ + + + + + {{ if .form.WebhookSecret }} + + + {{ end }} + +
+ +
+
+

{{ t "page.integration.bookmarklet" }}

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)