Browse Source

Add RSS-Bridge integration

Ryan Stafford 2 years ago
parent
commit
120aabfbce

+ 8 - 0
internal/api/subscription.go

@@ -7,6 +7,7 @@ import (
 	json_parser "encoding/json"
 	"net/http"
 
+	"miniflux.app/v2/internal/http/request"
 	"miniflux.app/v2/internal/http/response/json"
 	"miniflux.app/v2/internal/model"
 	"miniflux.app/v2/internal/reader/subscription"
@@ -25,6 +26,12 @@ func (h *handler) discoverSubscriptions(w http.ResponseWriter, r *http.Request)
 		return
 	}
 
+	var rssbridgeURL string
+	intg, err := h.store.Integration(request.UserID(r))
+	if err == nil && intg != nil && intg.RSSBridgeEnabled {
+		rssbridgeURL = intg.RSSBridgeURL
+	}
+
 	subscriptions, finderErr := subscription.FindSubscriptions(
 		subscriptionDiscoveryRequest.URL,
 		subscriptionDiscoveryRequest.UserAgent,
@@ -33,6 +40,7 @@ func (h *handler) discoverSubscriptions(w http.ResponseWriter, r *http.Request)
 		subscriptionDiscoveryRequest.Password,
 		subscriptionDiscoveryRequest.FetchViaProxy,
 		subscriptionDiscoveryRequest.AllowSelfSignedCertificates,
+		rssbridgeURL,
 	)
 	if finderErr != nil {
 		json.ServerError(w, r, finderErr)

+ 8 - 0
internal/database/migrations.go

@@ -799,4 +799,12 @@ var migrations = []func(tx *sql.Tx) error{
 		_, err = tx.Exec(sql)
 		return err
 	},
+	func(tx *sql.Tx) (err error) {
+		sql := `
+			ALTER TABLE integrations ADD COLUMN rssbridge_enabled bool default 'f';
+			ALTER TABLE integrations ADD COLUMN rssbridge_url text default '';
+		`
+		_, err = tx.Exec(sql)
+		return
+	},
 }

+ 1 - 1
internal/googlereader/handler.go

@@ -673,7 +673,7 @@ func (h *handler) quickAddHandler(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	subscriptions, s_err := mfs.FindSubscriptions(url, "", "", "", "", false, false)
+	subscriptions, s_err := mfs.FindSubscriptions(url, "", "", "", "", false, false, "")
 	if s_err != nil {
 		json.ServerError(w, r, s_err)
 		return

+ 45 - 0
internal/integration/rssbridge/rssbridge.go

@@ -0,0 +1,45 @@
+package rssbridge // import "miniflux.app/integration/rssbridge"
+
+import (
+	"encoding/json"
+	"fmt"
+	"net/http"
+	"net/url"
+)
+
+type Bridge struct {
+	URL        string     `json:"url"`
+	BridgeMeta BridgeMeta `json:"bridgeMeta"`
+}
+
+type BridgeMeta struct {
+	Name string `json:"name"`
+}
+
+func DetectBridges(rssbridgeURL, websiteURL string) (bridgeResponse []Bridge, err error) {
+	u, err := url.Parse(rssbridgeURL)
+	if err != nil {
+		return nil, err
+	}
+	values := u.Query()
+	values.Add("action", "findfeed")
+	values.Add("format", "atom")
+	values.Add("url", websiteURL)
+	u.RawQuery = values.Encode()
+
+	response, err := http.Get(u.String())
+	if err != nil {
+		return nil, err
+	}
+	defer response.Body.Close()
+	if response.StatusCode == http.StatusNotFound {
+		return
+	}
+	if response.StatusCode > 400 {
+		return nil, fmt.Errorf("RSS-Bridge: server failure (%d)", response.StatusCode)
+	}
+	if err := json.NewDecoder(response.Body).Decode(&bridgeResponse); err != nil {
+		return nil, fmt.Errorf("RSS-Bridge: unable to decode bridge response: %w", err)
+	}
+	return
+}

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

@@ -404,6 +404,8 @@
     "form.integration.webhook_activate": "Enable Webhook",
     "form.integration.webhook_url": "Webhook URL",
     "form.integration.webhook_secret": "Webhook Secret",
+    "form.integration.rssbridge_activate": "Check RSS-Bridge when adding subscriptions",
+    "form.integration.rssbridge_url": "RSS-Bridge server URL",
     "form.api_key.label.description": "API-Schlüsselbezeichnung",
     "form.submit.loading": "Lade...",
     "form.submit.saving": "Speichern...",

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

@@ -404,6 +404,8 @@
     "form.integration.webhook_activate": "Enable Webhook",
     "form.integration.webhook_url": "Webhook URL",
     "form.integration.webhook_secret": "Webhook Secret",
+    "form.integration.rssbridge_activate": "Check RSS-Bridge when adding subscriptions",
+    "form.integration.rssbridge_url": "RSS-Bridge server URL",
     "form.api_key.label.description": "Ετικέτα κλειδιού API",
     "form.submit.loading": "Φόρτωση...",
     "form.submit.saving": "Αποθήκευση...",

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

@@ -404,6 +404,8 @@
     "form.integration.webhook_activate": "Enable Webhook",
     "form.integration.webhook_url": "Webhook URL",
     "form.integration.webhook_secret": "Webhook Secret",
+    "form.integration.rssbridge_activate": "Check RSS-Bridge when adding subscriptions",
+    "form.integration.rssbridge_url": "RSS-Bridge server URL",
     "form.api_key.label.description": "API Key Label",
     "form.submit.loading": "Loading…",
     "form.submit.saving": "Saving…",

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

@@ -404,6 +404,8 @@
     "form.integration.webhook_activate": "Enable Webhook",
     "form.integration.webhook_url": "Webhook URL",
     "form.integration.webhook_secret": "Webhook Secret",
+    "form.integration.rssbridge_activate": "Check RSS-Bridge when adding subscriptions",
+    "form.integration.rssbridge_url": "RSS-Bridge server URL",
     "form.api_key.label.description": "Etiqueta de clave API",
     "form.submit.loading": "Cargando...",
     "form.submit.saving": "Guardando...",

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

@@ -404,6 +404,8 @@
     "form.integration.webhook_activate": "Enable Webhook",
     "form.integration.webhook_url": "Webhook URL",
     "form.integration.webhook_secret": "Webhook Secret",
+    "form.integration.rssbridge_activate": "Check RSS-Bridge when adding subscriptions",
+    "form.integration.rssbridge_url": "RSS-Bridge server URL",
     "form.api_key.label.description": "API Key Label",
     "form.submit.loading": "Ladataan...",
     "form.submit.saving": "Tallennetaan...",

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

@@ -404,6 +404,8 @@
     "form.integration.webhook_activate": "Activer le webhook",
     "form.integration.webhook_url": "URL du webhook",
     "form.integration.webhook_secret": "Secret du webhook",
+    "form.integration.rssbridge_activate": "Check RSS-Bridge when adding subscriptions",
+    "form.integration.rssbridge_url": "RSS-Bridge server URL",
     "form.api_key.label.description": "Libellé de la clé d'API",
     "form.submit.loading": "Chargement...",
     "form.submit.saving": "Sauvegarde en cours...",

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

@@ -404,6 +404,8 @@
     "form.integration.webhook_activate": "Enable Webhook",
     "form.integration.webhook_url": "Webhook URL",
     "form.integration.webhook_secret": "Webhook Secret",
+    "form.integration.rssbridge_activate": "Check RSS-Bridge when adding subscriptions",
+    "form.integration.rssbridge_url": "RSS-Bridge server URL",
     "form.api_key.label.description": "एपीआई कुंजी लेबल",
     "form.submit.loading": "लोड हो रहा है...",
     "form.submit.saving": "सहेजा जा रहा है...",

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

@@ -401,6 +401,8 @@
     "form.integration.webhook_activate": "Enable Webhook",
     "form.integration.webhook_url": "Webhook URL",
     "form.integration.webhook_secret": "Webhook Secret",
+    "form.integration.rssbridge_activate": "Check RSS-Bridge when adding subscriptions",
+    "form.integration.rssbridge_url": "RSS-Bridge server URL",
     "form.api_key.label.description": "Label Kunci API",
     "form.submit.loading": "Memuat...",
     "form.submit.saving": "Menyimpan...",

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

@@ -404,6 +404,8 @@
     "form.integration.webhook_activate": "Enable Webhook",
     "form.integration.webhook_url": "Webhook URL",
     "form.integration.webhook_secret": "Webhook Secret",
+    "form.integration.rssbridge_activate": "Check RSS-Bridge when adding subscriptions",
+    "form.integration.rssbridge_url": "RSS-Bridge server URL",
     "form.api_key.label.description": "Etichetta chiave API",
     "form.submit.loading": "Caricamento in corso...",
     "form.submit.saving": "Salvataggio in corso...",

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

@@ -404,6 +404,8 @@
     "form.integration.webhook_activate": "Enable Webhook",
     "form.integration.webhook_url": "Webhook URL",
     "form.integration.webhook_secret": "Webhook Secret",
+    "form.integration.rssbridge_activate": "Check RSS-Bridge when adding subscriptions",
+    "form.integration.rssbridge_url": "RSS-Bridge server URL",
     "form.api_key.label.description": "API キーラベル",
     "form.submit.loading": "読み込み中…",
     "form.submit.saving": "保存中…",

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

@@ -404,6 +404,8 @@
     "form.integration.webhook_activate": "Enable Webhook",
     "form.integration.webhook_url": "Webhook URL",
     "form.integration.webhook_secret": "Webhook Secret",
+    "form.integration.rssbridge_activate": "Check RSS-Bridge when adding subscriptions",
+    "form.integration.rssbridge_url": "RSS-Bridge server URL",
     "form.api_key.label.description": "API-sleutellabel",
     "form.submit.loading": "Laden...",
     "form.submit.saving": "Opslaag...",

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

@@ -406,6 +406,8 @@
     "form.integration.webhook_activate": "Enable Webhook",
     "form.integration.webhook_url": "Webhook URL",
     "form.integration.webhook_secret": "Webhook Secret",
+    "form.integration.rssbridge_activate": "Check RSS-Bridge when adding subscriptions",
+    "form.integration.rssbridge_url": "RSS-Bridge server URL",
     "form.api_key.label.description": "Etykieta klucza API",
     "form.submit.loading": "Ładowanie...",
     "form.submit.saving": "Zapisywanie...",

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

@@ -404,6 +404,8 @@
     "form.integration.webhook_activate": "Enable Webhook",
     "form.integration.webhook_url": "Webhook URL",
     "form.integration.webhook_secret": "Webhook Secret",
+    "form.integration.rssbridge_activate": "Check RSS-Bridge when adding subscriptions",
+    "form.integration.rssbridge_url": "RSS-Bridge server URL",
     "form.api_key.label.description": "Etiqueta da chave de API",
     "form.submit.loading": "Carregando...",
     "form.submit.saving": "Salvando...",

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

@@ -406,6 +406,8 @@
     "form.integration.webhook_activate": "Включить вебхуки",
     "form.integration.webhook_url": "Адрес вебхуков",
     "form.integration.webhook_secret": "Секретный ключ для вебхуков",
+    "form.integration.rssbridge_activate": "Check RSS-Bridge when adding subscriptions",
+    "form.integration.rssbridge_url": "RSS-Bridge server URL",
     "form.api_key.label.description": "Описание API-ключа",
     "form.submit.loading": "Загрузка…",
     "form.submit.saving": "Сохранение…",

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

@@ -404,6 +404,8 @@
     "form.integration.webhook_activate": "Enable Webhook",
     "form.integration.webhook_url": "Webhook URL",
     "form.integration.webhook_secret": "Webhook Secret",
+    "form.integration.rssbridge_activate": "Check RSS-Bridge when adding subscriptions",
+    "form.integration.rssbridge_url": "RSS-Bridge server URL",
     "form.api_key.label.description": "API Anahtar Etiketi",
     "form.submit.loading": "Yükleniyor...",
     "form.submit.saving": "Kaydediliyor...",

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

@@ -407,6 +407,8 @@
     "form.integration.webhook_activate": "Enable Webhook",
     "form.integration.webhook_url": "Webhook URL",
     "form.integration.webhook_secret": "Webhook Secret",
+    "form.integration.rssbridge_activate": "Check RSS-Bridge when adding subscriptions",
+    "form.integration.rssbridge_url": "RSS-Bridge server URL",
     "form.api_key.label.description": "Назва ключа API",
     "form.submit.loading": "Завантаження...",
     "form.submit.saving": "Зберігаю...",

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

@@ -402,6 +402,8 @@
     "form.integration.webhook_activate": "Enable Webhook",
     "form.integration.webhook_url": "Webhook URL",
     "form.integration.webhook_secret": "Webhook Secret",
+    "form.integration.rssbridge_activate": "Check RSS-Bridge when adding subscriptions",
+    "form.integration.rssbridge_url": "RSS-Bridge server URL",
     "form.api_key.label.description": "API密钥标签",
     "form.submit.loading": "载入中…",
     "form.submit.saving": "保存中…",

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

@@ -404,6 +404,8 @@
     "form.integration.webhook_activate": "Enable Webhook",
     "form.integration.webhook_url": "Webhook URL",
     "form.integration.webhook_secret": "Webhook Secret",
+    "form.integration.rssbridge_activate": "Check RSS-Bridge when adding subscriptions",
+    "form.integration.rssbridge_url": "RSS-Bridge server URL",
     "form.api_key.label.description": "API金鑰標籤",
     "form.submit.loading": "載入中…",
     "form.submit.saving": "儲存中…",

+ 2 - 0
internal/model/integration.go

@@ -71,4 +71,6 @@ type Integration struct {
 	WebhookEnabled                   bool
 	WebhookURL                       string
 	WebhookSecret                    string
+	RSSBridgeEnabled                 bool
+	RSSBridgeURL                     string
 }

+ 20 - 1
internal/reader/subscription/finder.go

@@ -12,6 +12,7 @@ import (
 	"miniflux.app/v2/internal/config"
 	"miniflux.app/v2/internal/errors"
 	"miniflux.app/v2/internal/http/client"
+	"miniflux.app/v2/internal/integration/rssbridge"
 	"miniflux.app/v2/internal/reader/browser"
 	"miniflux.app/v2/internal/reader/parser"
 	"miniflux.app/v2/internal/urllib"
@@ -26,7 +27,25 @@ var (
 )
 
 // FindSubscriptions downloads and try to find one or more subscriptions from an URL.
-func FindSubscriptions(websiteURL, userAgent, cookie, username, password string, fetchViaProxy, allowSelfSignedCertificates bool) (Subscriptions, *errors.LocalizedError) {
+func FindSubscriptions(websiteURL, userAgent, cookie, username, password string, fetchViaProxy, allowSelfSignedCertificates bool, rssbridgeURL string) (Subscriptions, *errors.LocalizedError) {
+	if rssbridgeURL != "" {
+		bridges, err := rssbridge.DetectBridges(rssbridgeURL, websiteURL)
+		if err != nil {
+			return nil, errors.NewLocalizedError("RSS-Bridge: %v", err)
+		}
+		if len(bridges) > 0 {
+			var subscriptions Subscriptions
+			for _, bridge := range bridges {
+				subscriptions = append(subscriptions, &Subscription{
+					Title: bridge.BridgeMeta.Name,
+					URL:   bridge.URL,
+					Type:  "atom",
+				})
+			}
+			return subscriptions, nil
+		}
+	}
+
 	websiteURL = findYoutubeChannelFeed(websiteURL)
 	websiteURL = parseYoutubeVideoPage(websiteURL)
 

+ 11 - 3
internal/storage/integration.go

@@ -174,7 +174,9 @@ func (s *Storage) Integration(userID int64) (*model.Integration, error) {
 			shaarli_api_secret,
 			webhook_enabled,
 			webhook_url,
-			webhook_secret
+			webhook_secret,
+			rssbridge_enabled,
+			rssbridge_url
 		FROM
 			integrations
 		WHERE
@@ -248,6 +250,8 @@ func (s *Storage) Integration(userID int64) (*model.Integration, error) {
 		&integration.WebhookEnabled,
 		&integration.WebhookURL,
 		&integration.WebhookSecret,
+		&integration.RSSBridgeEnabled,
+		&integration.RSSBridgeURL,
 	)
 	switch {
 	case err == sql.ErrNoRows:
@@ -329,9 +333,11 @@ func (s *Storage) UpdateIntegration(integration *model.Integration) error {
 			shaarli_api_secret=$62,
 			webhook_enabled=$63,
 			webhook_url=$64,
-			webhook_secret=$65
+			webhook_secret=$65,
+			rssbridge_enabled=$66,
+			rssbridge_url=$67
 		WHERE
-			user_id=$66
+			user_id=$68
 	`
 	_, err := s.db.Exec(
 		query,
@@ -400,6 +406,8 @@ func (s *Storage) UpdateIntegration(integration *model.Integration) error {
 		integration.WebhookEnabled,
 		integration.WebhookURL,
 		integration.WebhookSecret,
+		integration.RSSBridgeEnabled,
+		integration.RSSBridgeURL,
 		integration.UserID,
 	)
 

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

@@ -401,6 +401,22 @@
             </div>
         </div>
     </details>
+
+    <details {{ if .form.RSSBridgeEnabled }}open{{ end }}>
+        <summary>RSS-Bridge</summary>
+        <div class="form-section">
+            <label>
+                <input type="checkbox" name="rssbridge_enabled" value="1" {{ if .form.RSSBridgeEnabled }}checked{{ end }}> {{ t "form.integration.rssbridge_activate" }}
+            </label>
+
+            <label for="form-rssbridge-url">{{ t "form.integration.rssbridge_url" }}</label>
+            <input type="url" name="rssbridge_url" id="form-rssbridge-url" value="{{ .form.RSSBridgeURL }}" 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>
 </form>
 
 <h3>{{ t "page.integration.bookmarklet" }}</h3>

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

@@ -77,6 +77,8 @@ type IntegrationForm struct {
 	WebhookEnabled                   bool
 	WebhookURL                       string
 	WebhookSecret                    string
+	RSSBridgeEnabled                 bool
+	RSSBridgeURL                     string
 }
 
 // Merge copy form values to the model.
@@ -143,6 +145,8 @@ func (i IntegrationForm) Merge(integration *model.Integration) {
 	integration.ShaarliAPISecret = i.ShaarliAPISecret
 	integration.WebhookEnabled = i.WebhookEnabled
 	integration.WebhookURL = i.WebhookURL
+	integration.RSSBridgeEnabled = i.RSSBridgeEnabled
+	integration.RSSBridgeURL = i.RSSBridgeURL
 }
 
 // NewIntegrationForm returns a new IntegrationForm.
@@ -212,6 +216,8 @@ func NewIntegrationForm(r *http.Request) *IntegrationForm {
 		ShaarliAPISecret:                 r.FormValue("shaarli_api_secret"),
 		WebhookEnabled:                   r.FormValue("webhook_enabled") == "1",
 		WebhookURL:                       r.FormValue("webhook_url"),
+		RSSBridgeEnabled:                 r.FormValue("rssbridge_enabled") == "1",
+		RSSBridgeURL:                     r.FormValue("rssbridge_url"),
 	}
 }
 

+ 2 - 0
internal/ui/integration_show.go

@@ -91,6 +91,8 @@ func (h *handler) showIntegrationPage(w http.ResponseWriter, r *http.Request) {
 		WebhookEnabled:                   integration.WebhookEnabled,
 		WebhookURL:                       integration.WebhookURL,
 		WebhookSecret:                    integration.WebhookSecret,
+		RSSBridgeEnabled:                 integration.RSSBridgeEnabled,
+		RSSBridgeURL:                     integration.RSSBridgeURL,
 	}
 
 	sess := session.New(h.store, request.SessionID(r))

+ 6 - 0
internal/ui/subscription_submit.go

@@ -50,6 +50,11 @@ func (h *handler) submitSubscription(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
+	var rssbridgeURL string
+	if intg, err := h.store.Integration(user.ID); err == nil && intg != nil && intg.RSSBridgeEnabled {
+		rssbridgeURL = intg.RSSBridgeURL
+	}
+
 	subscriptions, findErr := subscription.FindSubscriptions(
 		subscriptionForm.URL,
 		subscriptionForm.UserAgent,
@@ -58,6 +63,7 @@ func (h *handler) submitSubscription(w http.ResponseWriter, r *http.Request) {
 		subscriptionForm.Password,
 		subscriptionForm.FetchViaProxy,
 		subscriptionForm.AllowSelfSignedCertificates,
+		rssbridgeURL,
 	)
 	if findErr != nil {
 		v.Set("form", subscriptionForm)