diff --git a/internal/database/migrations.go b/internal/database/migrations.go index e38c657e..0952a011 100644 --- a/internal/database/migrations.go +++ b/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 + }, } diff --git a/internal/integration/betula/betula.go b/internal/integration/betula/betula.go new file mode 100644 index 00000000..30dea95c --- /dev/null +++ b/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 +} diff --git a/internal/integration/integration.go b/internal/integration/integration.go index 64447bc9..ad596ec0 100644 --- a/internal/integration/integration.go +++ b/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), diff --git a/internal/locale/translations/de_DE.json b/internal/locale/translations/de_DE.json index 607dda09..71ff2034 100644 --- a/internal/locale/translations/de_DE.json +++ b/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", diff --git a/internal/locale/translations/el_EL.json b/internal/locale/translations/el_EL.json index 53e04d16..17ea225d 100644 --- a/internal/locale/translations/el_EL.json +++ b/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", diff --git a/internal/locale/translations/en_US.json b/internal/locale/translations/en_US.json index 0bfcc4bb..e20aab23 100644 --- a/internal/locale/translations/en_US.json +++ b/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", diff --git a/internal/locale/translations/es_ES.json b/internal/locale/translations/es_ES.json index 570b6885..4a0c3c67 100644 --- a/internal/locale/translations/es_ES.json +++ b/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", diff --git a/internal/locale/translations/fi_FI.json b/internal/locale/translations/fi_FI.json index ebd620a2..96f86d43 100644 --- a/internal/locale/translations/fi_FI.json +++ b/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", diff --git a/internal/locale/translations/fr_FR.json b/internal/locale/translations/fr_FR.json index 5299479b..af9bff78 100644 --- a/internal/locale/translations/fr_FR.json +++ b/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", diff --git a/internal/locale/translations/hi_IN.json b/internal/locale/translations/hi_IN.json index 1e806c83..c03679f5 100644 --- a/internal/locale/translations/hi_IN.json +++ b/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": "फीवर पासवर्ड", diff --git a/internal/locale/translations/id_ID.json b/internal/locale/translations/id_ID.json index 9114af8d..4795cd53 100644 --- a/internal/locale/translations/id_ID.json +++ b/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", diff --git a/internal/locale/translations/it_IT.json b/internal/locale/translations/it_IT.json index 630f688b..685b35ea 100644 --- a/internal/locale/translations/it_IT.json +++ b/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", diff --git a/internal/locale/translations/ja_JP.json b/internal/locale/translations/ja_JP.json index f68b2242..d2d32b99 100644 --- a/internal/locale/translations/ja_JP.json +++ b/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 のパスワード", diff --git a/internal/locale/translations/nl_NL.json b/internal/locale/translations/nl_NL.json index 4843a5c9..a07cf97d 100644 --- a/internal/locale/translations/nl_NL.json +++ b/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", diff --git a/internal/locale/translations/pl_PL.json b/internal/locale/translations/pl_PL.json index b61fbced..9ab385b9 100644 --- a/internal/locale/translations/pl_PL.json +++ b/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", diff --git a/internal/locale/translations/pt_BR.json b/internal/locale/translations/pt_BR.json index 6a4486d5..2c779e96 100644 --- a/internal/locale/translations/pt_BR.json +++ b/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", diff --git a/internal/locale/translations/ru_RU.json b/internal/locale/translations/ru_RU.json index 6d91f121..e8035c56 100644 --- a/internal/locale/translations/ru_RU.json +++ b/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", diff --git a/internal/locale/translations/tr_TR.json b/internal/locale/translations/tr_TR.json index 53064e09..e3c992a8 100644 --- a/internal/locale/translations/tr_TR.json +++ b/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", diff --git a/internal/locale/translations/uk_UA.json b/internal/locale/translations/uk_UA.json index b9c6c7f7..89d705a3 100644 --- a/internal/locale/translations/uk_UA.json +++ b/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", diff --git a/internal/locale/translations/zh_CN.json b/internal/locale/translations/zh_CN.json index 0ddcc24a..7230971c 100644 --- a/internal/locale/translations/zh_CN.json +++ b/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 密码", diff --git a/internal/locale/translations/zh_TW.json b/internal/locale/translations/zh_TW.json index 2c84bd7b..d67af7a2 100644 --- a/internal/locale/translations/zh_TW.json +++ b/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 密碼", diff --git a/internal/model/integration.go b/internal/model/integration.go index 05e4cec7..ff01538c 100644 --- a/internal/model/integration.go +++ b/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 diff --git a/internal/storage/integration.go b/internal/storage/integration.go index 2b848ac3..5bc20110 100644 --- a/internal/storage/integration.go +++ b/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 { diff --git a/internal/template/templates/views/integrations.html b/internal/template/templates/views/integrations.html index 66d03154..90586cbe 100644 --- a/internal/template/templates/views/integrations.html +++ b/internal/template/templates/views/integrations.html @@ -38,6 +38,25 @@ +
+ Betula +
+ + + + + + + + +
+ +
+
+
+
Espial
diff --git a/internal/ui/form/integration.go b/internal/ui/form/integration.go index 7809a3ff..5a6ac271 100644 --- a/internal/ui/form/integration.go +++ b/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"), } } diff --git a/internal/ui/integration_show.go b/internal/ui/integration_show.go index 8b3299a4..0e93093f 100644 --- a/internal/ui/integration_show.go +++ b/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))