Parcourir la source

Add Betula integration

Danila Gorelko il y a 1 an
Parent
commit
92db691344

+ 9 - 0
internal/database/migrations.go

@@ -912,4 +912,13 @@ 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 betula_url text default '';
+			ALTER TABLE integrations ADD COLUMN betula_token text default '';
+			ALTER TABLE integrations ADD COLUMN betula_enabled bool default 'f';
+		`
+		_, err = tx.Exec(sql)
+		return err
+	},
 }

+ 57 - 0
internal/integration/betula/betula.go

@@ -0,0 +1,57 @@
+package betula
+
+import (
+	"fmt"
+	"net/http"
+	"net/url"
+	"strings"
+	"time"
+
+	"miniflux.app/v2/internal/urllib"
+	"miniflux.app/v2/internal/version"
+)
+
+const defaultClientTimeout = 10 * time.Second
+
+type Client struct {
+	url   string
+	token string
+}
+
+func NewClient(url, token string) *Client {
+	return &Client{url: url, token: token}
+}
+
+func (c *Client) CreateBookmark(entryURL, entryTitle string, tags []string) error {
+	apiEndpoint, err := urllib.JoinBaseURLAndPath(c.url, "/save-link")
+	if err != nil {
+		return fmt.Errorf("betula: unable to generate save-link endpoint: %v", err)
+	}
+
+	values := url.Values{}
+	values.Add("url", entryURL)
+	values.Add("title", entryTitle)
+	values.Add("tags", strings.Join(tags, ","))
+
+	request, err := http.NewRequest(http.MethodPost, apiEndpoint+"?"+values.Encode(), nil)
+	if err != nil {
+		return fmt.Errorf("betula: unable to create request: %v", err)
+	}
+
+	request.Header.Set("Content-Type", "application/x-www-form-urlencoded")
+	request.Header.Set("User-Agent", "Miniflux/"+version.Version)
+	request.AddCookie(&http.Cookie{Name: "betula-token", Value: c.token})
+
+	httpClient := &http.Client{Timeout: defaultClientTimeout}
+	response, err := httpClient.Do(request)
+	if err != nil {
+		return fmt.Errorf("betula: unable to send request: %v", err)
+	}
+	defer response.Body.Close()
+
+	if response.StatusCode >= 400 {
+		return fmt.Errorf("betula: unable to create bookmark: url=%s status=%d", apiEndpoint, response.StatusCode)
+	}
+
+	return nil
+}

+ 25 - 0
internal/integration/integration.go

@@ -8,6 +8,7 @@ import (
 
 	"miniflux.app/v2/internal/config"
 	"miniflux.app/v2/internal/integration/apprise"
+	"miniflux.app/v2/internal/integration/betula"
 	"miniflux.app/v2/internal/integration/espial"
 	"miniflux.app/v2/internal/integration/instapaper"
 	"miniflux.app/v2/internal/integration/linkace"
@@ -32,6 +33,30 @@ import (
 
 // SendEntry sends the entry to third-party providers when the user click on "Save".
 func SendEntry(entry *model.Entry, userIntegrations *model.Integration) {
+	if userIntegrations.BetulaEnabled {
+		slog.Debug("Sending entry to Betula",
+			slog.Int64("user_id", userIntegrations.UserID),
+			slog.Int64("entry_id", entry.ID),
+			slog.String("entry_url", entry.URL),
+		)
+
+		client := betula.NewClient(userIntegrations.BetulaURL, userIntegrations.BetulaToken)
+		err := client.CreateBookmark(
+			entry.URL,
+			entry.Title,
+			entry.Tags,
+		)
+
+		if err != nil {
+			slog.Error("Unable to send entry to Betula",
+				slog.Int64("user_id", userIntegrations.UserID),
+				slog.Int64("entry_id", entry.ID),
+				slog.String("entry_url", entry.URL),
+				slog.Any("error", err),
+			)
+		}
+	}
+
 	if userIntegrations.PinboardEnabled {
 		slog.Debug("Sending entry to Pinboard",
 			slog.Int64("user_id", userIntegrations.UserID),

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

@@ -393,6 +393,9 @@
     "form.prefs.fieldset.global_feed_settings": "Global Feed Settings",
     "form.import.label.file": "OPML Datei",
     "form.import.label.url": "URL",
+    "form.integration.betula_activate": "Save entries to Betula",
+    "form.integration.betula_url": "Betula server URL",
+    "form.integration.betula_token": "Betula Token",
     "form.integration.fever_activate": "Fever API aktivieren",
     "form.integration.fever_username": "Fever Benutzername",
     "form.integration.fever_password": "Fever Passwort",

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

@@ -393,6 +393,9 @@
     "form.prefs.fieldset.global_feed_settings": "Global Feed Settings",
     "form.import.label.file": "Αρχείο OPML",
     "form.import.label.url": "URL",
+    "form.integration.betula_activate": "Save entries to Betula",
+    "form.integration.betula_url": "Betula server URL",
+    "form.integration.betula_token": "Betula Token",
     "form.integration.fever_activate": "Ενεργοποιήστε το Fever API",
     "form.integration.fever_username": "Όνομα Χρήστη Fever",
     "form.integration.fever_password": "Κωδικός Πρόσβασης Fever",

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

@@ -393,6 +393,9 @@
     "form.prefs.fieldset.global_feed_settings": "Global Feed Settings",
     "form.import.label.file": "OPML file",
     "form.import.label.url": "URL",
+    "form.integration.betula_activate": "Save entries to Betula",
+    "form.integration.betula_url": "Betula server URL",
+    "form.integration.betula_token": "Betula Token",
     "form.integration.fever_activate": "Activate Fever API",
     "form.integration.fever_username": "Fever Username",
     "form.integration.fever_password": "Fever Password",

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

@@ -393,6 +393,9 @@
     "form.prefs.fieldset.global_feed_settings": "Global Feed Settings",
     "form.import.label.file": "Archivo OPML",
     "form.import.label.url": "URL",
+    "form.integration.betula_activate": "Save entries to Betula",
+    "form.integration.betula_url": "Betula server URL",
+    "form.integration.betula_token": "Betula Token",
     "form.integration.fever_activate": "Activar API de Fever",
     "form.integration.fever_username": "Nombre de usuario de Fever",
     "form.integration.fever_password": "Contraseña de Fever",

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

@@ -393,6 +393,9 @@
     "form.prefs.fieldset.global_feed_settings": "Global Feed Settings",
     "form.import.label.file": "OPML-tiedosto",
     "form.import.label.url": "URL",
+    "form.integration.betula_activate": "Save entries to Betula",
+    "form.integration.betula_url": "Betula server URL",
+    "form.integration.betula_token": "Betula Token",
     "form.integration.fever_activate": "Ota Fever API käyttöön",
     "form.integration.fever_username": "Fever-käyttäjätunnus",
     "form.integration.fever_password": "Fever-salasana",

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

@@ -393,6 +393,9 @@
     "form.prefs.fieldset.global_feed_settings": "Paramètres globaux des abonnements",
     "form.import.label.file": "Fichier OPML",
     "form.import.label.url": "URL",
+    "form.integration.betula_activate": "Save entries to Betula",
+    "form.integration.betula_url": "Betula server URL",
+    "form.integration.betula_token": "Betula Token",
     "form.integration.fever_activate": "Activer l'API de Fever",
     "form.integration.fever_username": "Nom d'utilisateur pour l'API de Fever",
     "form.integration.fever_password": "Mot de passe pour l'API de Fever",

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

@@ -393,6 +393,9 @@
     "form.prefs.fieldset.global_feed_settings": "Global Feed Settings",
     "form.import.label.file": "ओपीएमएल फ़ाइल",
     "form.import.label.url": "यूआरएल",
+    "form.integration.betula_activate": "Save entries to Betula",
+    "form.integration.betula_url": "Betula server URL",
+    "form.integration.betula_token": "Betula Token",
     "form.integration.fever_activate": "फीवर एपीआई सक्रिय करें",
     "form.integration.fever_username": "फीवर उपयोगकर्ता नाम",
     "form.integration.fever_password": "फीवर पासवर्ड",

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

@@ -383,6 +383,9 @@
     "form.prefs.fieldset.global_feed_settings": "Global Feed Settings",
     "form.import.label.file": "Berkas OPML",
     "form.import.label.url": "URL",
+    "form.integration.betula_activate": "Save entries to Betula",
+    "form.integration.betula_url": "Betula server URL",
+    "form.integration.betula_token": "Betula Token",
     "form.integration.fever_activate": "Aktifkan API Fever",
     "form.integration.fever_username": "Nama Pengguna Fever",
     "form.integration.fever_password": "Kata Sandi Fever",

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

@@ -393,6 +393,9 @@
     "form.prefs.fieldset.global_feed_settings": "Global Feed Settings",
     "form.import.label.file": "File OPML",
     "form.import.label.url": "URL",
+    "form.integration.betula_activate": "Save entries to Betula",
+    "form.integration.betula_url": "Betula server URL",
+    "form.integration.betula_token": "Betula Token",
     "form.integration.fever_activate": "Abilita l'API di Fever",
     "form.integration.fever_username": "Nome utente dell'account Fever",
     "form.integration.fever_password": "Password dell'account Fever",

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

@@ -383,6 +383,9 @@
     "form.prefs.fieldset.global_feed_settings": "Global Feed Settings",
     "form.import.label.file": "OPML ファイル",
     "form.import.label.url": "URL",
+    "form.integration.betula_activate": "Save entries to Betula",
+    "form.integration.betula_url": "Betula server URL",
+    "form.integration.betula_token": "Betula Token",
     "form.integration.fever_activate": "Fever API を有効にする",
     "form.integration.fever_username": "Fever のユーザー名",
     "form.integration.fever_password": "Fever のパスワード",

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

@@ -393,6 +393,9 @@
     "form.prefs.fieldset.global_feed_settings": "Global Feed Settings",
     "form.import.label.file": "OPML-bestand",
     "form.import.label.url": "URL",
+    "form.integration.betula_activate": "Save entries to Betula",
+    "form.integration.betula_url": "Betula server URL",
+    "form.integration.betula_token": "Betula Token",
     "form.integration.fever_activate": "Activeer Fever API",
     "form.integration.fever_username": "Fever gebruikersnaam",
     "form.integration.fever_password": "Fever wachtwoord",

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

@@ -403,6 +403,9 @@
     "form.prefs.fieldset.global_feed_settings": "Global Feed Settings",
     "form.import.label.file": "Plik OPML",
     "form.import.label.url": "URL",
+    "form.integration.betula_activate": "Save entries to Betula",
+    "form.integration.betula_url": "Betula server URL",
+    "form.integration.betula_token": "Betula Token",
     "form.integration.fever_activate": "Aktywuj Fever API",
     "form.integration.fever_username": "Login do Fever",
     "form.integration.fever_password": "Hasło do Fever",

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

@@ -393,6 +393,9 @@
     "form.prefs.fieldset.global_feed_settings": "Global Feed Settings",
     "form.import.label.file": "Arquivo OPML",
     "form.import.label.url": "URL",
+    "form.integration.betula_activate": "Save entries to Betula",
+    "form.integration.betula_url": "Betula server URL",
+    "form.integration.betula_token": "Betula Token",
     "form.integration.fever_activate": "Ativar API do Fever",
     "form.integration.fever_username": "Nome de usuário do Fever",
     "form.integration.fever_password": "Senha do Fever",

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

@@ -403,6 +403,9 @@
     "form.prefs.fieldset.global_feed_settings": "Global Feed Settings",
     "form.import.label.file": "OPML файл",
     "form.import.label.url": "Ссылка",
+    "form.integration.betula_activate": "Сохранять статьи в Бетулу",
+    "form.integration.betula_url": "Адрес сервера Бетулы",
+    "form.integration.betula_token": "Токен Бетулы",
     "form.integration.fever_activate": "Активировать Fever API",
     "form.integration.fever_username": "Имя пользователя Fever",
     "form.integration.fever_password": "Пароль Fever",

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

@@ -177,6 +177,9 @@
   "form.feed.label.user_agent": "Varsayılan User Agent'i Geçersiz Kıl",
   "form.import.label.file": "OPML dosyası",
   "form.import.label.url": "URL",
+  "form.integration.betula_activate": "Save entries to Betula",
+  "form.integration.betula_url": "Betula server URL",
+  "form.integration.betula_token": "Betula Token",
   "form.integration.apprise_activate": "Push entries to Apprise",
   "form.integration.apprise_services_url": "Apprise hizmet URL'lerinin virgülle ayrılmış listesi",
   "form.integration.apprise_url": "Apprise API URL",

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

@@ -403,6 +403,9 @@
     "form.prefs.fieldset.global_feed_settings": "Global Feed Settings",
     "form.import.label.file": "Файл OPML",
     "form.import.label.url": "URL-адреса",
+    "form.integration.betula_activate": "Save entries to Betula",
+    "form.integration.betula_url": "Betula server URL",
+    "form.integration.betula_token": "Betula Token",
     "form.integration.fever_activate": "Увімкнути API Fever",
     "form.integration.fever_username": "Ім’я користувача Fever",
     "form.integration.fever_password": "Пароль Fever",

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

@@ -383,6 +383,9 @@
     "form.prefs.fieldset.global_feed_settings": "Global Feed Settings",
     "form.import.label.file": "OPML 文件",
     "form.import.label.url": "URL",
+    "form.integration.betula_activate": "Save entries to Betula",
+    "form.integration.betula_url": "Betula server URL",
+    "form.integration.betula_token": "Betula Token",
     "form.integration.fever_activate": "启用 Fever API",
     "form.integration.fever_username": "Fever 用户名",
     "form.integration.fever_password": "Fever 密码",

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

@@ -383,6 +383,9 @@
     "form.prefs.fieldset.global_feed_settings": "Global Feed Settings",
     "form.import.label.file": "OPML 檔案",
     "form.import.label.url": "URL",
+    "form.integration.betula_activate": "Save entries to Betula",
+    "form.integration.betula_url": "Betula server URL",
+    "form.integration.betula_token": "Betula Token",
     "form.integration.fever_activate": "啟用 Fever API",
     "form.integration.fever_username": "Fever 使用者名稱",
     "form.integration.fever_password": "Fever 密碼",

+ 3 - 0
internal/model/integration.go

@@ -6,6 +6,9 @@ package model // import "miniflux.app/v2/internal/model"
 // Integration represents user integration settings.
 type Integration struct {
 	UserID                           int64
+	BetulaEnabled                    bool
+	BetulaURL                        string
+	BetulaToken                      string
 	PinboardEnabled                  bool
 	PinboardToken                    string
 	PinboardTags                     string

+ 17 - 4
internal/storage/integration.go

@@ -197,7 +197,10 @@ func (s *Storage) Integration(userID int64) (*model.Integration, error) {
 			raindrop_enabled,
 			raindrop_token,
 			raindrop_collection_id,
-			raindrop_tags
+			raindrop_tags,
+			betula_enabled,
+			betula_url,
+			betula_token
 		FROM
 			integrations
 		WHERE
@@ -294,6 +297,9 @@ func (s *Storage) Integration(userID int64) (*model.Integration, error) {
 		&integration.RaindropToken,
 		&integration.RaindropCollectionID,
 		&integration.RaindropTags,
+		&integration.BetulaEnabled,
+		&integration.BetulaURL,
+		&integration.BetulaToken,
 	)
 	switch {
 	case err == sql.ErrNoRows:
@@ -398,9 +404,12 @@ func (s *Storage) UpdateIntegration(integration *model.Integration) error {
 			raindrop_enabled=$85,
 			raindrop_token=$86,
 			raindrop_collection_id=$87,
-			raindrop_tags=$88
+			raindrop_tags=$88,
+			betula_enabled=$89,
+			betula_url=$90,
+			betula_token=$91
 		WHERE
-			user_id=$89
+			user_id=$92
 	`
 	_, err := s.db.Exec(
 		query,
@@ -492,6 +501,9 @@ func (s *Storage) UpdateIntegration(integration *model.Integration) error {
 		integration.RaindropToken,
 		integration.RaindropCollectionID,
 		integration.RaindropTags,
+		integration.BetulaEnabled,
+		integration.BetulaURL,
+		integration.BetulaToken,
 		integration.UserID,
 	)
 
@@ -530,7 +542,8 @@ func (s *Storage) HasSaveEntry(userID int64) (result bool) {
 				shaarli_enabled='t' OR
 				webhook_enabled='t' OR
 				omnivore_enabled='t' OR
-				raindrop_enabled='t'
+				raindrop_enabled='t' OR
+				betula_enabled='t'
 			)
 	`
 	if err := s.db.QueryRow(query, userID).Scan(&result); err != nil {

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

@@ -38,6 +38,25 @@
         </div>
     </details>
 
+    <details {{ if .form.BetulaEnabled }}open{{ end }}>
+        <summary>Betula</summary>
+        <div class="form-section">
+            <label>
+                <input type="checkbox" name="betula_enabled" value="1" {{ if .form.BetulaEnabled }}checked{{ end }}> {{ t "form.integration.betula_activate" }}
+            </label>
+
+            <label for="form-betula-url">{{ t "form.integration.betula_url" }}</label>
+            <input type="url" name="betula_url" id="form-betula-url" value="{{ .form.BetulaURL }}" placeholder="http://links.bouncepaw.com" spellcheck="false">
+
+            <label for="form-betula-token">{{ t "form.integration.betula_token" }}</label>
+            <input type="text" name="betula_token" id="form-betula-token" value="{{ .form.BetulaToken }}" 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">

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

@@ -100,6 +100,9 @@ type IntegrationForm struct {
 	RaindropToken                    string
 	RaindropCollectionID             string
 	RaindropTags                     string
+	BetulaEnabled                    bool
+	BetulaURL                        string
+	BetulaToken                      string
 }
 
 // Merge copy form values to the model.
@@ -189,6 +192,9 @@ func (i IntegrationForm) Merge(integration *model.Integration) {
 	integration.RaindropToken = i.RaindropToken
 	integration.RaindropCollectionID = i.RaindropCollectionID
 	integration.RaindropTags = i.RaindropTags
+	integration.BetulaEnabled = i.BetulaEnabled
+	integration.BetulaURL = i.BetulaURL
+	integration.BetulaToken = i.BetulaToken
 }
 
 // NewIntegrationForm returns a new IntegrationForm.
@@ -281,6 +287,9 @@ func NewIntegrationForm(r *http.Request) *IntegrationForm {
 		RaindropToken:                    r.FormValue("raindrop_token"),
 		RaindropCollectionID:             r.FormValue("raindrop_collection_id"),
 		RaindropTags:                     r.FormValue("raindrop_tags"),
+		BetulaEnabled:                    r.FormValue("betula_enabled") == "1",
+		BetulaURL:                        r.FormValue("betula_url"),
+		BetulaToken:                      r.FormValue("betula_token"),
 	}
 }
 

+ 3 - 0
internal/ui/integration_show.go

@@ -114,6 +114,9 @@ func (h *handler) showIntegrationPage(w http.ResponseWriter, r *http.Request) {
 		RaindropToken:                    integration.RaindropToken,
 		RaindropCollectionID:             integration.RaindropCollectionID,
 		RaindropTags:                     integration.RaindropTags,
+		BetulaEnabled:                    integration.BetulaEnabled,
+		BetulaURL:                        integration.BetulaURL,
+		BetulaToken:                      integration.BetulaToken,
 	}
 
 	sess := session.New(h.store, request.SessionID(r))