Add Betula integration

This commit is contained in:
Danila Gorelko 2024-07-04 22:59:47 +03:00 committed by GitHub
parent a334c8e691
commit 92db691344
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
26 changed files with 196 additions and 4 deletions

View file

@ -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
},
}

View file

@ -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
}

View file

@ -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),

View file

@ -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",

View file

@ -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",

View file

@ -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",

View file

@ -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",

View file

@ -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",

View file

@ -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",

View file

@ -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": "फीवर पासवर्ड",

View file

@ -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",

View file

@ -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",

View file

@ -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 のパスワード",

View file

@ -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",

View file

@ -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",

View file

@ -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",

View file

@ -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",

View file

@ -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",

View file

@ -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",

View file

@ -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 密码",

View file

@ -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 密碼",

View file

@ -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

View file

@ -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 {

View file

@ -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">

View file

@ -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"),
}
}

View file

@ -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))