Add Betula integration
This commit is contained in:
parent
a334c8e691
commit
92db691344
26 changed files with 196 additions and 4 deletions
|
@ -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
internal/integration/betula/betula.go
Normal file
57
internal/integration/betula/betula.go
Normal 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
|
||||
}
|
|
@ -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),
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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": "फीवर पासवर्ड",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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 のパスワード",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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 密码",
|
||||
|
|
|
@ -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 密碼",
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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"),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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))
|
||||
|
|
Loading…
Reference in a new issue