Add generic webhook integration
This commit is contained in:
parent
32d33104a4
commit
48f6885f44
39 changed files with 527 additions and 324 deletions
internal
crypto
database
integration
locale/translations
de_DE.jsonel_EL.jsonen_US.jsones_ES.jsonfi_FI.jsonfr_FR.jsonhi_IN.jsonid_ID.jsonit_IT.jsonja_JP.jsonnl_NL.jsonpl_PL.jsonpt_BR.jsonru_RU.jsontr_TR.jsonuk_UA.jsonzh_CN.jsonzh_TW.json
model
reader
storage
template/templates/views
ui
|
@ -4,6 +4,7 @@
|
|||
package crypto // import "miniflux.app/v2/internal/crypto"
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
|
@ -48,3 +49,9 @@ func HashPassword(password string) (string, error) {
|
|||
bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
return string(bytes), err
|
||||
}
|
||||
|
||||
func GenerateSHA256Hmac(secret string, data []byte) string {
|
||||
h := hmac.New(sha256.New, []byte(secret))
|
||||
h.Write(data)
|
||||
return hex.EncodeToString(h.Sum(nil))
|
||||
}
|
||||
|
|
|
@ -758,4 +758,13 @@ var migrations = []func(tx *sql.Tx) error{
|
|||
`)
|
||||
return err
|
||||
},
|
||||
func(tx *sql.Tx) (err error) {
|
||||
sql := `
|
||||
ALTER TABLE integrations ADD COLUMN webhook_enabled bool default 'f';
|
||||
ALTER TABLE integrations ADD COLUMN webhook_url text default '';
|
||||
ALTER TABLE integrations ADD COLUMN webhook_secret text default '';
|
||||
`
|
||||
_, err = tx.Exec(sql)
|
||||
return err
|
||||
},
|
||||
}
|
||||
|
|
|
@ -19,6 +19,7 @@ import (
|
|||
"miniflux.app/v2/internal/integration/shiori"
|
||||
"miniflux.app/v2/internal/integration/telegrambot"
|
||||
"miniflux.app/v2/internal/integration/wallabag"
|
||||
"miniflux.app/v2/internal/integration/webhook"
|
||||
"miniflux.app/v2/internal/logger"
|
||||
"miniflux.app/v2/internal/model"
|
||||
)
|
||||
|
@ -168,45 +169,55 @@ func SendEntry(entry *model.Entry, integration *model.Integration) {
|
|||
}
|
||||
}
|
||||
|
||||
// PushEntries pushes an entry array to third-party providers during feed refreshes.
|
||||
func PushEntries(entries model.Entries, integration *model.Integration) {
|
||||
if integration.MatrixBotEnabled {
|
||||
logger.Debug("[Integration] Sending %d entries for User #%d to Matrix", len(entries), integration.UserID)
|
||||
// PushEntries pushes a list of entries to activated third-party providers during feed refreshes.
|
||||
func PushEntries(feed *model.Feed, entries model.Entries, userIntegrations *model.Integration) {
|
||||
if userIntegrations.MatrixBotEnabled {
|
||||
logger.Debug("[Integration] Sending %d entries for User #%d to Matrix", len(entries), userIntegrations.UserID)
|
||||
|
||||
err := matrixbot.PushEntries(entries, integration.MatrixBotURL, integration.MatrixBotUser, integration.MatrixBotPassword, integration.MatrixBotChatID)
|
||||
err := matrixbot.PushEntries(entries, userIntegrations.MatrixBotURL, userIntegrations.MatrixBotUser, userIntegrations.MatrixBotPassword, userIntegrations.MatrixBotChatID)
|
||||
if err != nil {
|
||||
logger.Error("[Integration] push entries to matrix bot failed: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// PushEntry pushes an entry to third-party providers during feed refreshes.
|
||||
func PushEntry(entry *model.Entry, feed *model.Feed, integration *model.Integration) {
|
||||
if integration.TelegramBotEnabled {
|
||||
logger.Debug("[Integration] Sending Entry %q for User #%d to Telegram", entry.URL, integration.UserID)
|
||||
if userIntegrations.WebhookEnabled {
|
||||
logger.Debug("[Integration] Sending %d entries for User #%d to Webhook URL: %s", len(entries), userIntegrations.UserID, userIntegrations.WebhookURL)
|
||||
|
||||
err := telegrambot.PushEntry(entry, integration.TelegramBotToken, integration.TelegramBotChatID)
|
||||
if err != nil {
|
||||
logger.Error("[Integration] push entry to telegram bot failed: %v", err)
|
||||
webhookClient := webhook.NewClient(userIntegrations.WebhookURL, userIntegrations.WebhookSecret)
|
||||
if err := webhookClient.SendWebhook(entries); err != nil {
|
||||
logger.Error("[Integration] sending entries to webhook failed: %v", err)
|
||||
}
|
||||
}
|
||||
if integration.AppriseEnabled {
|
||||
logger.Debug("[Integration] Sending Entry %q for User #%d to apprise", entry.URL, integration.UserID)
|
||||
|
||||
var appriseServiceURLs string
|
||||
if len(feed.AppriseServiceURLs) > 0 {
|
||||
appriseServiceURLs = feed.AppriseServiceURLs
|
||||
} else {
|
||||
appriseServiceURLs = integration.AppriseServicesURL
|
||||
}
|
||||
// Integrations that only support sending individual entries
|
||||
if userIntegrations.TelegramBotEnabled || userIntegrations.AppriseEnabled {
|
||||
for _, entry := range entries {
|
||||
if userIntegrations.TelegramBotEnabled {
|
||||
logger.Debug("[Integration] Sending Entry %q for User #%d to Telegram", entry.URL, userIntegrations.UserID)
|
||||
|
||||
client := apprise.NewClient(
|
||||
appriseServiceURLs,
|
||||
integration.AppriseURL,
|
||||
)
|
||||
err := telegrambot.PushEntry(entry, userIntegrations.TelegramBotToken, userIntegrations.TelegramBotChatID)
|
||||
if err != nil {
|
||||
logger.Error("[Integration] push entry to telegram bot failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := client.SendNotification(entry); err != nil {
|
||||
logger.Error("[Integration] push entry to apprise failed: %v", err)
|
||||
if userIntegrations.AppriseEnabled {
|
||||
logger.Debug("[Integration] Sending Entry %q for User #%d to apprise", entry.URL, userIntegrations.UserID)
|
||||
|
||||
appriseServiceURLs := userIntegrations.AppriseURL
|
||||
if feed.AppriseServiceURLs != "" {
|
||||
appriseServiceURLs = feed.AppriseServiceURLs
|
||||
}
|
||||
|
||||
client := apprise.NewClient(
|
||||
userIntegrations.AppriseServicesURL,
|
||||
appriseServiceURLs,
|
||||
)
|
||||
|
||||
if err := client.SendNotification(entry); err != nil {
|
||||
logger.Error("[Integration] push entry to apprise failed: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
64
internal/integration/webhook/webhook.go
Normal file
64
internal/integration/webhook/webhook.go
Normal file
|
@ -0,0 +1,64 @@
|
|||
// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package webhook // import "miniflux.app/v2/internal/integration/webhook"
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"miniflux.app/v2/internal/crypto"
|
||||
"miniflux.app/v2/internal/model"
|
||||
"miniflux.app/v2/internal/version"
|
||||
)
|
||||
|
||||
const defaultClientTimeout = 10 * time.Second
|
||||
|
||||
type Client struct {
|
||||
webhookURL string
|
||||
webhookSecret string
|
||||
}
|
||||
|
||||
func NewClient(webhookURL, webhookSecret string) *Client {
|
||||
return &Client{webhookURL, webhookSecret}
|
||||
}
|
||||
|
||||
func (c *Client) SendWebhook(entries model.Entries) error {
|
||||
if c.webhookURL == "" {
|
||||
return fmt.Errorf(`webhook: missing webhook URL`)
|
||||
}
|
||||
|
||||
if len(entries) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
requestBody, err := json.Marshal(entries)
|
||||
if err != nil {
|
||||
return fmt.Errorf("webhook: unable to encode request body: %v", err)
|
||||
}
|
||||
|
||||
request, err := http.NewRequest(http.MethodPost, c.webhookURL, bytes.NewReader(requestBody))
|
||||
if err != nil {
|
||||
return fmt.Errorf("webhook: unable to create request: %v", err)
|
||||
}
|
||||
|
||||
request.Header.Set("Content-Type", "application/json")
|
||||
request.Header.Set("User-Agent", "Miniflux/"+version.Version)
|
||||
request.Header.Set("X-Miniflux-Signature", crypto.GenerateSHA256Hmac(c.webhookSecret, requestBody))
|
||||
|
||||
httpClient := &http.Client{Timeout: defaultClientTimeout}
|
||||
response, err := httpClient.Do(request)
|
||||
if err != nil {
|
||||
return fmt.Errorf("webhook: unable to send request: %v", err)
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
if response.StatusCode >= 400 {
|
||||
return fmt.Errorf("webhook: incorrect response status code: url=%s status=%d", c.webhookURL, response.StatusCode)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -389,6 +389,9 @@
|
|||
"form.integration.shaarli_activate": "Save articles to Shaarli",
|
||||
"form.integration.shaarli_endpoint": "Shaarli URL",
|
||||
"form.integration.shaarli_api_secret": "Shaarli API Secret",
|
||||
"form.integration.webhook_activate": "Enable Webhook",
|
||||
"form.integration.webhook_url": "Webhook URL",
|
||||
"form.integration.webhook_secret": "Webhook Secret",
|
||||
"form.api_key.label.description": "API-Schlüsselbezeichnung",
|
||||
"form.submit.loading": "Lade...",
|
||||
"form.submit.saving": "Speichern...",
|
||||
|
|
|
@ -389,6 +389,9 @@
|
|||
"form.integration.shaarli_activate": "Save articles to Shaarli",
|
||||
"form.integration.shaarli_endpoint": "Shaarli URL",
|
||||
"form.integration.shaarli_api_secret": "Shaarli API Secret",
|
||||
"form.integration.webhook_activate": "Enable Webhook",
|
||||
"form.integration.webhook_url": "Webhook URL",
|
||||
"form.integration.webhook_secret": "Webhook Secret",
|
||||
"form.api_key.label.description": "Ετικέτα κλειδιού API",
|
||||
"form.submit.loading": "Φόρτωση...",
|
||||
"form.submit.saving": "Αποθήκευση...",
|
||||
|
|
|
@ -389,6 +389,9 @@
|
|||
"form.integration.shaarli_activate": "Save articles to Shaarli",
|
||||
"form.integration.shaarli_endpoint": "Shaarli URL",
|
||||
"form.integration.shaarli_api_secret": "Shaarli API Secret",
|
||||
"form.integration.webhook_activate": "Enable Webhook",
|
||||
"form.integration.webhook_url": "Webhook URL",
|
||||
"form.integration.webhook_secret": "Webhook Secret",
|
||||
"form.api_key.label.description": "API Key Label",
|
||||
"form.submit.loading": "Loading…",
|
||||
"form.submit.saving": "Saving…",
|
||||
|
|
|
@ -389,6 +389,9 @@
|
|||
"form.integration.shaarli_activate": "Save articles to Shaarli",
|
||||
"form.integration.shaarli_endpoint": "Shaarli URL",
|
||||
"form.integration.shaarli_api_secret": "Shaarli API Secret",
|
||||
"form.integration.webhook_activate": "Enable Webhook",
|
||||
"form.integration.webhook_url": "Webhook URL",
|
||||
"form.integration.webhook_secret": "Webhook Secret",
|
||||
"form.api_key.label.description": "Etiqueta de clave API",
|
||||
"form.submit.loading": "Cargando...",
|
||||
"form.submit.saving": "Guardando...",
|
||||
|
|
|
@ -389,6 +389,9 @@
|
|||
"form.integration.shaarli_activate": "Save articles to Shaarli",
|
||||
"form.integration.shaarli_endpoint": "Shaarli URL",
|
||||
"form.integration.shaarli_api_secret": "Shaarli API Secret",
|
||||
"form.integration.webhook_activate": "Enable Webhook",
|
||||
"form.integration.webhook_url": "Webhook URL",
|
||||
"form.integration.webhook_secret": "Webhook Secret",
|
||||
"form.api_key.label.description": "API Key Label",
|
||||
"form.submit.loading": "Ladataan...",
|
||||
"form.submit.saving": "Tallennetaan...",
|
||||
|
|
|
@ -389,6 +389,9 @@
|
|||
"form.integration.shaarli_activate": "Sauvegarder les articles vers Shaarli",
|
||||
"form.integration.shaarli_endpoint": "URL de l'API de Shaarli",
|
||||
"form.integration.shaarli_api_secret": "Clé d'API de Shaarli API",
|
||||
"form.integration.webhook_activate": "Enable Webhook",
|
||||
"form.integration.webhook_url": "Webhook URL",
|
||||
"form.integration.webhook_secret": "Webhook Secret",
|
||||
"form.api_key.label.description": "Libellé de la clé d'API",
|
||||
"form.submit.loading": "Chargement...",
|
||||
"form.submit.saving": "Sauvegarde en cours...",
|
||||
|
|
|
@ -389,6 +389,9 @@
|
|||
"form.integration.shaarli_activate": "Save articles to Shaarli",
|
||||
"form.integration.shaarli_endpoint": "Shaarli URL",
|
||||
"form.integration.shaarli_api_secret": "Shaarli API Secret",
|
||||
"form.integration.webhook_activate": "Enable Webhook",
|
||||
"form.integration.webhook_url": "Webhook URL",
|
||||
"form.integration.webhook_secret": "Webhook Secret",
|
||||
"form.api_key.label.description": "एपीआई कुंजी लेबल",
|
||||
"form.submit.loading": "लोड हो रहा है...",
|
||||
"form.submit.saving": "सहेजा जा रहा है...",
|
||||
|
|
|
@ -386,6 +386,9 @@
|
|||
"form.integration.shaarli_activate": "Save articles to Shaarli",
|
||||
"form.integration.shaarli_endpoint": "Shaarli URL",
|
||||
"form.integration.shaarli_api_secret": "Shaarli API Secret",
|
||||
"form.integration.webhook_activate": "Enable Webhook",
|
||||
"form.integration.webhook_url": "Webhook URL",
|
||||
"form.integration.webhook_secret": "Webhook Secret",
|
||||
"form.api_key.label.description": "Label Kunci API",
|
||||
"form.submit.loading": "Memuat...",
|
||||
"form.submit.saving": "Menyimpan...",
|
||||
|
|
|
@ -389,6 +389,9 @@
|
|||
"form.integration.shaarli_activate": "Save articles to Shaarli",
|
||||
"form.integration.shaarli_endpoint": "Shaarli URL",
|
||||
"form.integration.shaarli_api_secret": "Shaarli API Secret",
|
||||
"form.integration.webhook_activate": "Enable Webhook",
|
||||
"form.integration.webhook_url": "Webhook URL",
|
||||
"form.integration.webhook_secret": "Webhook Secret",
|
||||
"form.api_key.label.description": "Etichetta chiave API",
|
||||
"form.submit.loading": "Caricamento in corso...",
|
||||
"form.submit.saving": "Salvataggio in corso...",
|
||||
|
|
|
@ -389,6 +389,9 @@
|
|||
"form.integration.shaarli_activate": "Save articles to Shaarli",
|
||||
"form.integration.shaarli_endpoint": "Shaarli URL",
|
||||
"form.integration.shaarli_api_secret": "Shaarli API Secret",
|
||||
"form.integration.webhook_activate": "Enable Webhook",
|
||||
"form.integration.webhook_url": "Webhook URL",
|
||||
"form.integration.webhook_secret": "Webhook Secret",
|
||||
"form.api_key.label.description": "API キーラベル",
|
||||
"form.submit.loading": "読み込み中…",
|
||||
"form.submit.saving": "保存中…",
|
||||
|
|
|
@ -389,6 +389,9 @@
|
|||
"form.integration.shaarli_activate": "Save articles to Shaarli",
|
||||
"form.integration.shaarli_endpoint": "Shaarli URL",
|
||||
"form.integration.shaarli_api_secret": "Shaarli API Secret",
|
||||
"form.integration.webhook_activate": "Enable Webhook",
|
||||
"form.integration.webhook_url": "Webhook URL",
|
||||
"form.integration.webhook_secret": "Webhook Secret",
|
||||
"form.api_key.label.description": "API-sleutellabel",
|
||||
"form.submit.loading": "Laden...",
|
||||
"form.submit.saving": "Opslaag...",
|
||||
|
|
|
@ -391,6 +391,9 @@
|
|||
"form.integration.shaarli_activate": "Save articles to Shaarli",
|
||||
"form.integration.shaarli_endpoint": "Shaarli URL",
|
||||
"form.integration.shaarli_api_secret": "Shaarli API Secret",
|
||||
"form.integration.webhook_activate": "Enable Webhook",
|
||||
"form.integration.webhook_url": "Webhook URL",
|
||||
"form.integration.webhook_secret": "Webhook Secret",
|
||||
"form.api_key.label.description": "Etykieta klucza API",
|
||||
"form.submit.loading": "Ładowanie...",
|
||||
"form.submit.saving": "Zapisywanie...",
|
||||
|
|
|
@ -389,6 +389,9 @@
|
|||
"form.integration.shaarli_activate": "Save articles to Shaarli",
|
||||
"form.integration.shaarli_endpoint": "Shaarli URL",
|
||||
"form.integration.shaarli_api_secret": "Shaarli API Secret",
|
||||
"form.integration.webhook_activate": "Enable Webhook",
|
||||
"form.integration.webhook_url": "Webhook URL",
|
||||
"form.integration.webhook_secret": "Webhook Secret",
|
||||
"form.api_key.label.description": "Etiqueta da chave de API",
|
||||
"form.submit.loading": "Carregando...",
|
||||
"form.submit.saving": "Salvando...",
|
||||
|
|
|
@ -391,6 +391,9 @@
|
|||
"form.integration.shaarli_activate": "Save articles to Shaarli",
|
||||
"form.integration.shaarli_endpoint": "Shaarli URL",
|
||||
"form.integration.shaarli_api_secret": "Shaarli API Secret",
|
||||
"form.integration.webhook_activate": "Enable Webhook",
|
||||
"form.integration.webhook_url": "Webhook URL",
|
||||
"form.integration.webhook_secret": "Webhook Secret",
|
||||
"form.api_key.label.description": "Описание API-ключа",
|
||||
"form.submit.loading": "Загрузка…",
|
||||
"form.submit.saving": "Сохранение…",
|
||||
|
|
|
@ -389,6 +389,9 @@
|
|||
"form.integration.shaarli_activate": "Save articles to Shaarli",
|
||||
"form.integration.shaarli_endpoint": "Shaarli URL",
|
||||
"form.integration.shaarli_api_secret": "Shaarli API Secret",
|
||||
"form.integration.webhook_activate": "Enable Webhook",
|
||||
"form.integration.webhook_url": "Webhook URL",
|
||||
"form.integration.webhook_secret": "Webhook Secret",
|
||||
"form.api_key.label.description": "API Anahtar Etiketi",
|
||||
"form.submit.loading": "Yükleniyor...",
|
||||
"form.submit.saving": "Kaydediliyor...",
|
||||
|
|
|
@ -392,6 +392,9 @@
|
|||
"form.integration.shaarli_activate": "Save articles to Shaarli",
|
||||
"form.integration.shaarli_endpoint": "Shaarli URL",
|
||||
"form.integration.shaarli_api_secret": "Shaarli API Secret",
|
||||
"form.integration.webhook_activate": "Enable Webhook",
|
||||
"form.integration.webhook_url": "Webhook URL",
|
||||
"form.integration.webhook_secret": "Webhook Secret",
|
||||
"form.api_key.label.description": "Назва ключа API",
|
||||
"form.submit.loading": "Завантаження...",
|
||||
"form.submit.saving": "Зберігаю...",
|
||||
|
|
|
@ -387,6 +387,9 @@
|
|||
"form.integration.shaarli_activate": "Save articles to Shaarli",
|
||||
"form.integration.shaarli_endpoint": "Shaarli URL",
|
||||
"form.integration.shaarli_api_secret": "Shaarli API Secret",
|
||||
"form.integration.webhook_activate": "Enable Webhook",
|
||||
"form.integration.webhook_url": "Webhook URL",
|
||||
"form.integration.webhook_secret": "Webhook Secret",
|
||||
"form.api_key.label.description": "API密钥标签",
|
||||
"form.submit.loading": "载入中…",
|
||||
"form.submit.saving": "保存中…",
|
||||
|
|
|
@ -389,6 +389,9 @@
|
|||
"form.integration.shaarli_activate": "Save articles to Shaarli",
|
||||
"form.integration.shaarli_endpoint": "Shaarli URL",
|
||||
"form.integration.shaarli_api_secret": "Shaarli API Secret",
|
||||
"form.integration.webhook_activate": "Enable Webhook",
|
||||
"form.integration.webhook_url": "Webhook URL",
|
||||
"form.integration.webhook_secret": "Webhook Secret",
|
||||
"form.api_key.label.description": "API金鑰標籤",
|
||||
"form.submit.loading": "載入中…",
|
||||
"form.submit.saving": "儲存中…",
|
||||
|
|
|
@ -39,6 +39,13 @@ type Entry struct {
|
|||
Tags []string `json:"tags"`
|
||||
}
|
||||
|
||||
func NewEntry() *Entry {
|
||||
return &Entry{
|
||||
Enclosures: make(EnclosureList, 0),
|
||||
Tags: make([]string, 0),
|
||||
}
|
||||
}
|
||||
|
||||
// Entries represents a list of entries.
|
||||
type Entries []*Entry
|
||||
|
||||
|
|
|
@ -64,4 +64,7 @@ type Integration struct {
|
|||
ShaarliEnabled bool
|
||||
ShaarliURL string
|
||||
ShaarliAPISecret string
|
||||
WebhookEnabled bool
|
||||
WebhookURL string
|
||||
WebhookSecret string
|
||||
}
|
||||
|
|
|
@ -86,7 +86,7 @@ type atom03Entry struct {
|
|||
}
|
||||
|
||||
func (a *atom03Entry) Transform() *model.Entry {
|
||||
entry := new(model.Entry)
|
||||
entry := model.NewEntry()
|
||||
entry.URL = a.Links.originalLink()
|
||||
entry.Date = a.entryDate()
|
||||
entry.Author = a.Author.String()
|
||||
|
|
|
@ -95,7 +95,7 @@ type atom10Entry struct {
|
|||
}
|
||||
|
||||
func (a *atom10Entry) Transform() *model.Entry {
|
||||
entry := new(model.Entry)
|
||||
entry := model.NewEntry()
|
||||
entry.URL = a.Links.originalLink()
|
||||
entry.Date = a.entryDate()
|
||||
entry.Author = a.Authors.String()
|
||||
|
@ -219,7 +219,7 @@ func (a *atom10Entry) entryEnclosures() model.EnclosureList {
|
|||
}
|
||||
|
||||
func (r *atom10Entry) entryCategories() []string {
|
||||
var categoryList []string
|
||||
categoryList := make([]string, 0)
|
||||
|
||||
for _, atomCategory := range r.Categories {
|
||||
if strings.TrimSpace(atomCategory.Label) != "" {
|
||||
|
|
|
@ -10,6 +10,7 @@ import (
|
|||
"miniflux.app/v2/internal/config"
|
||||
"miniflux.app/v2/internal/errors"
|
||||
"miniflux.app/v2/internal/http/client"
|
||||
"miniflux.app/v2/internal/integration"
|
||||
"miniflux.app/v2/internal/locale"
|
||||
"miniflux.app/v2/internal/logger"
|
||||
"miniflux.app/v2/internal/model"
|
||||
|
@ -177,15 +178,24 @@ func RefreshFeed(store *storage.Storage, userID, feedID int64, forceRefresh bool
|
|||
|
||||
// We don't update existing entries when the crawler is enabled (we crawl only inexisting entries). Unless it is forced to refresh
|
||||
updateExistingEntries := forceRefresh || !originalFeed.Crawler
|
||||
if storeErr := store.RefreshFeedEntries(originalFeed.UserID, originalFeed.ID, originalFeed.Entries, updateExistingEntries); storeErr != nil {
|
||||
newEntries, storeErr := store.RefreshFeedEntries(originalFeed.UserID, originalFeed.ID, originalFeed.Entries, updateExistingEntries)
|
||||
if storeErr != nil {
|
||||
originalFeed.WithError(storeErr.Error())
|
||||
store.UpdateFeedError(originalFeed)
|
||||
return storeErr
|
||||
}
|
||||
|
||||
userIntegrations, intErr := store.Integration(userID)
|
||||
if intErr != nil {
|
||||
logger.Error("[RefreshFeed] Fetching integrations for user %d failed: %v; the refresh process will go on, but no integrations will run this time.", userID, intErr)
|
||||
} else if userIntegrations != nil && len(newEntries) > 0 {
|
||||
go integration.PushEntries(originalFeed, newEntries, userIntegrations)
|
||||
}
|
||||
|
||||
// We update caching headers only if the feed has been modified,
|
||||
// because some websites don't return the same headers when replying with a 304.
|
||||
originalFeed.WithClientResponse(response)
|
||||
|
||||
checkFeedIcon(
|
||||
store,
|
||||
originalFeed.ID,
|
||||
|
|
|
@ -181,7 +181,7 @@ func (j *jsonItem) GetEnclosures() model.EnclosureList {
|
|||
}
|
||||
|
||||
func (j *jsonItem) Transform() *model.Entry {
|
||||
entry := new(model.Entry)
|
||||
entry := model.NewEntry()
|
||||
entry.URL = j.URL
|
||||
entry.Date = j.GetDate()
|
||||
entry.Author = j.GetAuthor()
|
||||
|
@ -189,7 +189,10 @@ func (j *jsonItem) Transform() *model.Entry {
|
|||
entry.Content = j.GetContent()
|
||||
entry.Title = strings.TrimSpace(j.GetTitle())
|
||||
entry.Enclosures = j.GetEnclosures()
|
||||
entry.Tags = j.Tags
|
||||
if len(j.Tags) > 0 {
|
||||
entry.Tags = j.Tags
|
||||
}
|
||||
|
||||
return entry
|
||||
}
|
||||
|
||||
|
|
|
@ -13,8 +13,6 @@ import (
|
|||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
"miniflux.app/v2/internal/integration"
|
||||
|
||||
"miniflux.app/v2/internal/config"
|
||||
"miniflux.app/v2/internal/http/client"
|
||||
"miniflux.app/v2/internal/logger"
|
||||
|
@ -41,9 +39,6 @@ var (
|
|||
func ProcessFeedEntries(store *storage.Storage, feed *model.Feed, user *model.User, forceRefresh bool) {
|
||||
var filteredEntries model.Entries
|
||||
|
||||
// array used for bulk push
|
||||
entriesToPush := model.Entries{}
|
||||
|
||||
// Process older entries first
|
||||
for i := len(feed.Entries) - 1; i >= 0; i-- {
|
||||
entry := feed.Entries[i]
|
||||
|
@ -90,32 +85,10 @@ func ProcessFeedEntries(store *storage.Storage, feed *model.Feed, user *model.Us
|
|||
// The sanitizer should always run at the end of the process to make sure unsafe HTML is filtered.
|
||||
entry.Content = sanitizer.Sanitize(url, entry.Content)
|
||||
|
||||
if entryIsNew {
|
||||
intg, err := store.Integration(feed.UserID)
|
||||
if err != nil {
|
||||
logger.Error("[Processor] Get integrations for user %d failed: %v; the refresh process will go on, but no integrations will run this time.", feed.UserID, err)
|
||||
} else if intg != nil {
|
||||
localEntry := entry
|
||||
go func() {
|
||||
integration.PushEntry(localEntry, feed, intg)
|
||||
}()
|
||||
entriesToPush = append(entriesToPush, localEntry)
|
||||
}
|
||||
}
|
||||
|
||||
updateEntryReadingTime(store, feed, entry, entryIsNew, user)
|
||||
filteredEntries = append(filteredEntries, entry)
|
||||
}
|
||||
|
||||
intg, err := store.Integration(feed.UserID)
|
||||
if err != nil {
|
||||
logger.Error("[Processor] Get integrations for user %d failed: %v; the refresh process will go on, but no integrations will run this time.", feed.UserID, err)
|
||||
} else if intg != nil && len(entriesToPush) > 0 {
|
||||
go func() {
|
||||
integration.PushEntries(entriesToPush, intg)
|
||||
}()
|
||||
}
|
||||
|
||||
feed.Entries = filteredEntries
|
||||
}
|
||||
|
||||
|
|
|
@ -65,7 +65,7 @@ type rdfItem struct {
|
|||
}
|
||||
|
||||
func (r *rdfItem) Transform() *model.Entry {
|
||||
entry := new(model.Entry)
|
||||
entry := model.NewEntry()
|
||||
entry.Title = r.entryTitle()
|
||||
entry.Author = r.entryAuthor()
|
||||
entry.URL = r.entryURL()
|
||||
|
|
|
@ -190,7 +190,7 @@ type rssItem struct {
|
|||
}
|
||||
|
||||
func (r *rssItem) Transform() *model.Entry {
|
||||
entry := new(model.Entry)
|
||||
entry := model.NewEntry()
|
||||
entry.URL = r.entryURL()
|
||||
entry.CommentsURL = r.entryCommentsURL()
|
||||
entry.Date = r.entryDate()
|
||||
|
@ -388,7 +388,7 @@ func (r *rssItem) entryEnclosures() model.EnclosureList {
|
|||
}
|
||||
|
||||
func (r *rssItem) entryCategories() []string {
|
||||
var categoryList []string
|
||||
categoryList := make([]string, 0)
|
||||
|
||||
for _, rssCategory := range r.Categories {
|
||||
if strings.Contains(rssCategory.Inner, "<![CDATA[") {
|
||||
|
|
|
@ -107,8 +107,10 @@ func (s *Storage) createEnclosure(tx *sql.Tx, enclosure *model.Enclosure) error
|
|||
VALUES
|
||||
($1, $2, $3, $4, $5, $6)
|
||||
ON CONFLICT (user_id, entry_id, md5(url)) DO NOTHING
|
||||
RETURNING
|
||||
id
|
||||
`
|
||||
_, err := tx.Exec(
|
||||
if err := tx.QueryRow(
|
||||
query,
|
||||
enclosureURL,
|
||||
enclosure.Size,
|
||||
|
@ -116,24 +118,22 @@ func (s *Storage) createEnclosure(tx *sql.Tx, enclosure *model.Enclosure) error
|
|||
enclosure.EntryID,
|
||||
enclosure.UserID,
|
||||
enclosure.MediaProgression,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf(`store: unable to create enclosure: %v`, err)
|
||||
).Scan(&enclosure.ID); err != nil && err != sql.ErrNoRows {
|
||||
return fmt.Errorf(`store: unable to create enclosure: %w`, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Storage) updateEnclosures(tx *sql.Tx, userID, entryID int64, enclosures model.EnclosureList) error {
|
||||
if len(enclosures) == 0 {
|
||||
func (s *Storage) updateEnclosures(tx *sql.Tx, entry *model.Entry) error {
|
||||
if len(entry.Enclosures) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
sqlValues := []any{userID, entryID}
|
||||
sqlValues := []any{entry.UserID, entry.ID}
|
||||
sqlPlaceholders := []string{}
|
||||
|
||||
for _, enclosure := range enclosures {
|
||||
for _, enclosure := range entry.Enclosures {
|
||||
sqlPlaceholders = append(sqlPlaceholders, fmt.Sprintf(`$%d`, len(sqlValues)+1))
|
||||
sqlValues = append(sqlValues, strings.TrimSpace(enclosure.URL))
|
||||
|
||||
|
@ -143,11 +143,10 @@ func (s *Storage) updateEnclosures(tx *sql.Tx, userID, entryID int64, enclosures
|
|||
}
|
||||
|
||||
query := `
|
||||
DELETE FROM enclosures
|
||||
DELETE FROM
|
||||
enclosures
|
||||
WHERE
|
||||
user_id=$1 AND
|
||||
entry_id=$2 AND
|
||||
url NOT IN (%s)
|
||||
user_id=$1 AND entry_id=$2 AND url NOT IN (%s)
|
||||
`
|
||||
|
||||
query = fmt.Sprintf(query, strings.Join(sqlPlaceholders, `,`))
|
||||
|
|
|
@ -138,7 +138,7 @@ func (s *Storage) createEntry(tx *sql.Tx, entry *model.Entry) error {
|
|||
$11
|
||||
)
|
||||
RETURNING
|
||||
id, status
|
||||
id, status, created_at, changed_at
|
||||
`
|
||||
err := tx.QueryRow(
|
||||
query,
|
||||
|
@ -153,7 +153,12 @@ func (s *Storage) createEntry(tx *sql.Tx, entry *model.Entry) error {
|
|||
entry.FeedID,
|
||||
entry.ReadingTime,
|
||||
pq.Array(removeDuplicates(entry.Tags)),
|
||||
).Scan(&entry.ID, &entry.Status)
|
||||
).Scan(
|
||||
&entry.ID,
|
||||
&entry.Status,
|
||||
&entry.CreatedAt,
|
||||
&entry.ChangedAt,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf(`store: unable to create entry %q (feed #%d): %v`, entry.URL, entry.FeedID, err)
|
||||
|
@ -215,7 +220,7 @@ func (s *Storage) updateEntry(tx *sql.Tx, entry *model.Entry) error {
|
|||
enclosure.EntryID = entry.ID
|
||||
}
|
||||
|
||||
return s.updateEnclosures(tx, entry.UserID, entry.ID, entry.Enclosures)
|
||||
return s.updateEnclosures(tx, entry)
|
||||
}
|
||||
|
||||
// entryExists checks if an entry already exists based on its hash when refreshing a feed.
|
||||
|
@ -264,7 +269,7 @@ func (s *Storage) cleanupEntries(feedID int64, entryHashes []string) error {
|
|||
}
|
||||
|
||||
// RefreshFeedEntries updates feed entries while refreshing a feed.
|
||||
func (s *Storage) RefreshFeedEntries(userID, feedID int64, entries model.Entries, updateExistingEntries bool) (err error) {
|
||||
func (s *Storage) RefreshFeedEntries(userID, feedID int64, entries model.Entries, updateExistingEntries bool) (newEntries model.Entries, err error) {
|
||||
var entryHashes []string
|
||||
|
||||
for _, entry := range entries {
|
||||
|
@ -273,15 +278,15 @@ func (s *Storage) RefreshFeedEntries(userID, feedID int64, entries model.Entries
|
|||
|
||||
tx, err := s.db.Begin()
|
||||
if err != nil {
|
||||
return fmt.Errorf(`store: unable to start transaction: %v`, err)
|
||||
return nil, fmt.Errorf(`store: unable to start transaction: %v`, err)
|
||||
}
|
||||
|
||||
entryExists, err := s.entryExists(tx, entry)
|
||||
if err != nil {
|
||||
if rollbackErr := tx.Rollback(); rollbackErr != nil {
|
||||
return fmt.Errorf(`store: unable to rollback transaction: %v (rolled back due to: %v)`, rollbackErr, err)
|
||||
return nil, fmt.Errorf(`store: unable to rollback transaction: %v (rolled back due to: %v)`, rollbackErr, err)
|
||||
}
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if entryExists {
|
||||
|
@ -290,17 +295,20 @@ func (s *Storage) RefreshFeedEntries(userID, feedID int64, entries model.Entries
|
|||
}
|
||||
} else {
|
||||
err = s.createEntry(tx, entry)
|
||||
if err == nil {
|
||||
newEntries = append(newEntries, entry)
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
if rollbackErr := tx.Rollback(); rollbackErr != nil {
|
||||
return fmt.Errorf(`store: unable to rollback transaction: %v (rolled back due to: %v)`, rollbackErr, err)
|
||||
return nil, fmt.Errorf(`store: unable to rollback transaction: %v (rolled back due to: %v)`, rollbackErr, err)
|
||||
}
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
return fmt.Errorf(`store: unable to commit transaction: %v`, err)
|
||||
return nil, fmt.Errorf(`store: unable to commit transaction: %v`, err)
|
||||
}
|
||||
|
||||
entryHashes = append(entryHashes, entry.Hash)
|
||||
|
@ -312,7 +320,7 @@ func (s *Storage) RefreshFeedEntries(userID, feedID int64, entries model.Entries
|
|||
}
|
||||
}()
|
||||
|
||||
return nil
|
||||
return newEntries, nil
|
||||
}
|
||||
|
||||
// ArchiveEntries changes the status of entries to "removed" after the given number of days.
|
||||
|
|
|
@ -345,9 +345,9 @@ func (s *Storage) UpdateFeed(feed *model.Feed) (err error) {
|
|||
hide_globally=$24,
|
||||
url_rewrite_rules=$25,
|
||||
no_media_player=$26,
|
||||
apprise_service_urls=$29
|
||||
apprise_service_urls=$27
|
||||
WHERE
|
||||
id=$27 AND user_id=$28
|
||||
id=$28 AND user_id=$29
|
||||
`
|
||||
_, err = s.db.Exec(query,
|
||||
feed.FeedURL,
|
||||
|
@ -376,9 +376,9 @@ func (s *Storage) UpdateFeed(feed *model.Feed) (err error) {
|
|||
feed.HideGlobally,
|
||||
feed.UrlRewriteRules,
|
||||
feed.NoMediaPlayer,
|
||||
feed.AppriseServiceURLs,
|
||||
feed.ID,
|
||||
feed.UserID,
|
||||
feed.AppriseServiceURLs,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
|
|
|
@ -167,7 +167,10 @@ func (s *Storage) Integration(userID int64) (*model.Integration, error) {
|
|||
shiori_password,
|
||||
shaarli_enabled,
|
||||
shaarli_url,
|
||||
shaarli_api_secret
|
||||
shaarli_api_secret,
|
||||
webhook_enabled,
|
||||
webhook_url,
|
||||
webhook_secret
|
||||
FROM
|
||||
integrations
|
||||
WHERE
|
||||
|
@ -234,6 +237,9 @@ func (s *Storage) Integration(userID int64) (*model.Integration, error) {
|
|||
&integration.ShaarliEnabled,
|
||||
&integration.ShaarliURL,
|
||||
&integration.ShaarliAPISecret,
|
||||
&integration.WebhookEnabled,
|
||||
&integration.WebhookURL,
|
||||
&integration.WebhookSecret,
|
||||
)
|
||||
switch {
|
||||
case err == sql.ErrNoRows:
|
||||
|
@ -308,9 +314,12 @@ func (s *Storage) UpdateIntegration(integration *model.Integration) error {
|
|||
shiori_password=$55,
|
||||
shaarli_enabled=$56,
|
||||
shaarli_url=$57,
|
||||
shaarli_api_secret=$58
|
||||
shaarli_api_secret=$58,
|
||||
webhook_enabled=$59,
|
||||
webhook_url=$60,
|
||||
webhook_secret=$61
|
||||
WHERE
|
||||
user_id=$59
|
||||
user_id=$62
|
||||
`
|
||||
_, err := s.db.Exec(
|
||||
query,
|
||||
|
@ -372,6 +381,9 @@ func (s *Storage) UpdateIntegration(integration *model.Integration) error {
|
|||
integration.ShaarliEnabled,
|
||||
integration.ShaarliURL,
|
||||
integration.ShaarliAPISecret,
|
||||
integration.WebhookEnabled,
|
||||
integration.WebhookURL,
|
||||
integration.WebhookSecret,
|
||||
integration.UserID,
|
||||
)
|
||||
|
||||
|
|
|
@ -13,179 +13,23 @@
|
|||
<div class="alert alert-error">{{ t .errorMessage }}</div>
|
||||
{{ end }}
|
||||
|
||||
<details {{ if .form.FeverEnabled }}open{{ end }}>
|
||||
<summary>Fever</summary>
|
||||
<details {{ if .form.AppriseEnabled }}open{{ end }}>
|
||||
<summary>Apprise</summary>
|
||||
<div class="form-section">
|
||||
<label>
|
||||
<input type="checkbox" name="fever_enabled" value="1" {{ if .form.FeverEnabled }}checked{{ end }}> {{ t "form.integration.fever_activate" }}
|
||||
<input type="checkbox" name="apprise_enabled" value="1" {{ if .form.AppriseEnabled }}checked{{ end }}> {{ t "form.integration.apprise_activate" }}
|
||||
</label>
|
||||
|
||||
<label for="form-fever-username">{{ t "form.integration.fever_username" }}</label>
|
||||
<input type="text" name="fever_username" id="form-fever-username" value="{{ .form.FeverUsername }}" autocomplete="username" spellcheck="false">
|
||||
|
||||
<label for="form-fever-password">{{ t "form.integration.fever_password" }}</label>
|
||||
<input type="password" name="fever_password" id="form-fever-password" value="{{ .form.FeverPassword }}" autocomplete="new-password">
|
||||
|
||||
<p>{{ t "form.integration.fever_endpoint" }} <strong>{{ rootURL }}{{ route "feverEndpoint" }}</strong></p>
|
||||
|
||||
<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.GoogleReaderEnabled }}open{{ end }}>
|
||||
<summary>Google Reader</summary>
|
||||
<div class="form-section">
|
||||
<label>
|
||||
<input type="checkbox" name="googlereader_enabled" value="1" {{ if .form.GoogleReaderEnabled }}checked{{ end }}> {{ t "form.integration.googlereader_activate" }}
|
||||
</label>
|
||||
|
||||
<label for="form-googlereader-username">{{ t "form.integration.googlereader_username" }}</label>
|
||||
<input type="text" name="googlereader_username" id="form-googlereader-username" value="{{ .form.GoogleReaderUsername }}" autocomplete="username" spellcheck="false">
|
||||
|
||||
<label for="form-googlereader-password">{{ t "form.integration.googlereader_password" }}</label>
|
||||
<input type="password" name="googlereader_password" id="form-googlereader-password" value="{{ .form.GoogleReaderPassword }}" autocomplete="new-password">
|
||||
|
||||
<p>{{ t "form.integration.googlereader_endpoint" }} <strong>{{ rootURL }}{{ route "login" }}</strong></p>
|
||||
|
||||
<div class="buttons">
|
||||
<button type="submit" class="button button-primary" data-label-loading="{{ t "form.submit.saving" }}">{{ t "action.update" }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
<label for="form-apprise-url">{{ t "form.integration.apprise_url" }}</label>
|
||||
<input type="url" name="apprise_url" id="form-apprise-url" value="{{ .form.AppriseURL }}" placeholder="http://apprise:8080" spellcheck="false">
|
||||
|
||||
<details {{ if .form.PinboardEnabled }}open{{ end }}>
|
||||
<summary>Pinboard</summary>
|
||||
<div class="form-section">
|
||||
<label>
|
||||
<input type="checkbox" name="pinboard_enabled" value="1" {{ if .form.PinboardEnabled }}checked{{ end }}> {{ t "form.integration.pinboard_activate" }}
|
||||
<label for="form-apprise-services-url">{{ t "form.integration.apprise_services_url" }}
|
||||
<a href="https://github.com/caronc/apprise/wiki" target="_blank">
|
||||
{{ icon "external-link" }}
|
||||
</a>
|
||||
</label>
|
||||
|
||||
<label for="form-pinboard-token">{{ t "form.integration.pinboard_token" }}</label>
|
||||
<input type="password" name="pinboard_token" id="form-pinboard-token" value="{{ .form.PinboardToken }}" autocomplete="new-password">
|
||||
|
||||
<label for="form-pinboard-tags">{{ t "form.integration.pinboard_tags" }}</label>
|
||||
<input type="text" name="pinboard_tags" id="form-pinboard-tags" value="{{ .form.PinboardTags }}" spellcheck="false">
|
||||
|
||||
<label>
|
||||
<input type="checkbox" name="pinboard_mark_as_unread" value="1" {{ if .form.PinboardMarkAsUnread }}checked{{ end }}> {{ t "form.integration.pinboard_bookmark" }}
|
||||
</label>
|
||||
|
||||
<div class="buttons">
|
||||
<button type="submit" class="button button-primary" data-label-loading="{{ t "form.submit.saving" }}">{{ t "action.update" }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
<input type="text" name="apprise_services_url" id="form-apprise-services-urls" value="{{ .form.AppriseServicesURL }}" placeholder="tgram://<token>/<chat_id>/,matrix://" spellcheck="false">
|
||||
|
||||
<details {{ if .form.InstapaperEnabled }}open{{ end }}>
|
||||
<summary>Instapaper</summary>
|
||||
<div class="form-section">
|
||||
<label>
|
||||
<input type="checkbox" name="instapaper_enabled" value="1" {{ if .form.InstapaperEnabled }}checked{{ end }}> {{ t "form.integration.instapaper_activate" }}
|
||||
</label>
|
||||
|
||||
<label for="form-instapaper-username">{{ t "form.integration.instapaper_username" }}</label>
|
||||
<input type="text" name="instapaper_username" id="form-instapaper-username" value="{{ .form.InstapaperUsername }}" spellcheck="false">
|
||||
|
||||
<label for="form-instapaper-password">{{ t "form.integration.instapaper_password" }}</label>
|
||||
<input type="password" name="instapaper_password" id="form-instapaper-password" value="{{ .form.InstapaperPassword }}" autocomplete="new-password">
|
||||
|
||||
<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.PocketEnabled }}open{{ end }}>
|
||||
<summary>Pocket</summary>
|
||||
<div class="form-section">
|
||||
<label>
|
||||
<input type="checkbox" name="pocket_enabled" value="1" {{ if .form.PocketEnabled }}checked{{ end }}> {{ t "form.integration.pocket_activate" }}
|
||||
</label>
|
||||
|
||||
{{ if not .hasPocketConsumerKeyConfigured }}
|
||||
<label for="form-pocket-consumer-key">{{ t "form.integration.pocket_consumer_key" }}</label>
|
||||
<input type="text" name="pocket_consumer_key" id="form-pocket-consumer-key" value="{{ .form.PocketConsumerKey }}" spellcheck="false">
|
||||
{{ end }}
|
||||
|
||||
<label for="form-pocket-access-token">{{ t "form.integration.pocket_access_token" }}</label>
|
||||
<input type="password" name="pocket_access_token" id="form-pocket-access-token" value="{{ .form.PocketAccessToken }}" autocomplete="new-password">
|
||||
|
||||
{{ if not .form.PocketAccessToken }}
|
||||
<p><a href="{{ route "pocketAuthorize" }}">{{ t "form.integration.pocket_connect_link" }}</a></p>
|
||||
{{ end }}
|
||||
|
||||
<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.WallabagEnabled }}open{{ end }}>
|
||||
<summary>Wallabag</summary>
|
||||
<div class="form-section">
|
||||
<label>
|
||||
<input type="checkbox" name="wallabag_enabled" value="1" {{ if .form.WallabagEnabled }}checked{{ end }}> {{ t "form.integration.wallabag_activate" }}
|
||||
</label>
|
||||
|
||||
<label>
|
||||
<input type="checkbox" name="wallabag_only_url" value="1" {{ if .form.WallabagOnlyURL }}checked{{ end }}> {{ t "form.integration.wallabag_only_url" }}
|
||||
</label>
|
||||
|
||||
<label for="form-wallabag-url">{{ t "form.integration.wallabag_endpoint" }}</label>
|
||||
<input type="url" name="wallabag_url" id="form-wallabag-url" value="{{ .form.WallabagURL }}" placeholder="http://v2.wallabag.org/" spellcheck="false">
|
||||
|
||||
<label for="form-wallabag-client-id">{{ t "form.integration.wallabag_client_id" }}</label>
|
||||
<input type="text" name="wallabag_client_id" id="form-wallabag-client-id" value="{{ .form.WallabagClientID }}" spellcheck="false">
|
||||
|
||||
<label for="form-wallabag-client-secret">{{ t "form.integration.wallabag_client_secret" }}</label>
|
||||
<input type="password" name="wallabag_client_secret" id="form-wallabag-client-secret" value="{{ .form.WallabagClientSecret }}" autocomplete="new-password">
|
||||
|
||||
<label for="form-wallabag-username">{{ t "form.integration.wallabag_username" }}</label>
|
||||
<input type="text" name="wallabag_username" id="form-wallabag-username" value="{{ .form.WallabagUsername }}" spellcheck="false">
|
||||
|
||||
<label for="form-wallabag-password">{{ t "form.integration.wallabag_password" }}</label>
|
||||
<input type="password" name="wallabag_password" id="form-wallabag-password" value="{{ .form.WallabagPassword }}" autocomplete="new-password">
|
||||
|
||||
<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.NotionEnabled }}open{{ end }}>
|
||||
<summary>Notion</summary>
|
||||
<div class="form-section">
|
||||
<label>
|
||||
<input type="checkbox" name="notion_enabled" value="1" {{ if .form.NotionEnabled }}checked{{ end }}> {{ t "form.integration.notion_activate" }}
|
||||
</label>
|
||||
|
||||
<label for="form-notion-token">{{ t "form.integration.notion_token" }}</label>
|
||||
<input type="password" name="notion_token" id="form-notion-token" value="{{ .form.NotionToken }}" spellcheck="false">
|
||||
|
||||
<label for="form-notion-page-id">{{ t "form.integration.notion_page_id" }}</label>
|
||||
<input type="text" name="notion_page_id" id="form-notion-page-id" value="{{ .form.NotionPageID }}" 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.NunuxKeeperEnabled }}open{{ end }}>
|
||||
<summary>Nunux Keeper</summary>
|
||||
<div class="form-section">
|
||||
<label>
|
||||
<input type="checkbox" name="nunux_keeper_enabled" value="1" {{ if .form.NunuxKeeperEnabled }}checked{{ end }}> {{ t "form.integration.nunux_keeper_activate" }}
|
||||
</label>
|
||||
|
||||
<label for="form-nunux-keeper-url">{{ t "form.integration.nunux_keeper_endpoint" }}</label>
|
||||
<input type="url" name="nunux_keeper_url" id="form-nunux-keeper-url" value="{{ .form.NunuxKeeperURL }}" placeholder="https://api.nunux.org/keeper" spellcheck="false">
|
||||
|
||||
<label for="form-nunux-keeper-api-key">{{ t "form.integration.nunux_keeper_api_key" }}</label>
|
||||
<input type="text" name="nunux_keeper_api_key" id="form-nunux-keeper-api-key" value="{{ .form.NunuxKeeperAPIKey }}" spellcheck="false">
|
||||
|
||||
<div class="buttons">
|
||||
<button type="submit" class="button button-primary" data-label-loading="{{ t "form.submit.saving" }}">{{ t "action.update" }}</button>
|
||||
</div>
|
||||
|
@ -198,34 +42,77 @@
|
|||
<label>
|
||||
<input type="checkbox" name="espial_enabled" value="1" {{ if .form.EspialEnabled }}checked{{ end }}> {{ t "form.integration.espial_activate" }}
|
||||
</label>
|
||||
|
||||
|
||||
<label for="form-espial-url">{{ t "form.integration.espial_endpoint" }}</label>
|
||||
<input type="url" name="espial_url" id="form-espial-url" value="{{ .form.EspialURL }}" placeholder="https://esp.ae8.org" spellcheck="false">
|
||||
|
||||
|
||||
<label for="form-espial-api-key">{{ t "form.integration.espial_api_key" }}</label>
|
||||
<input type="text" name="espial_api_key" id="form-espial-api-key" value="{{ .form.EspialAPIKey }}" spellcheck="false">
|
||||
|
||||
|
||||
<label for="form-espial-tags">{{ t "form.integration.espial_tags" }}</label>
|
||||
<input type="text" name="espial_tags" id="form-espial-tags" value="{{ .form.EspialTags }}" 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.ReadwiseEnabled }}open{{ end }}>
|
||||
<summary>Readwise Reader</summary>
|
||||
<details {{ if .form.FeverEnabled }}open{{ end }}>
|
||||
<summary>Fever</summary>
|
||||
<div class="form-section">
|
||||
<label>
|
||||
<input type="checkbox" name="readwise_enabled" value="1" {{ if .form.ReadwiseEnabled }}checked{{ end }}> {{ t "form.integration.readwise_activate" }}
|
||||
<input type="checkbox" name="fever_enabled" value="1" {{ if .form.FeverEnabled }}checked{{ end }}> {{ t "form.integration.fever_activate" }}
|
||||
</label>
|
||||
|
||||
<label for="form-readwise-api-key">{{ t "form.integration.readwise_api_key" }}</label>
|
||||
<input type="text" name="readwise_api_key" id="form-readwise-api-key" value="{{ .form.ReadwiseAPIKey }}" spellcheck="false">
|
||||
|
||||
<p><a href="https://readwise.io/access_token" target="_blank">{{ t "form.integration.readwise_api_key_link" }}</a></p>
|
||||
|
||||
|
||||
<label for="form-fever-username">{{ t "form.integration.fever_username" }}</label>
|
||||
<input type="text" name="fever_username" id="form-fever-username" value="{{ .form.FeverUsername }}" autocomplete="username" spellcheck="false">
|
||||
|
||||
<label for="form-fever-password">{{ t "form.integration.fever_password" }}</label>
|
||||
<input type="password" name="fever_password" id="form-fever-password" value="{{ .form.FeverPassword }}" autocomplete="new-password">
|
||||
|
||||
<p>{{ t "form.integration.fever_endpoint" }} <strong>{{ rootURL }}{{ route "feverEndpoint" }}</strong></p>
|
||||
|
||||
<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.GoogleReaderEnabled }}open{{ end }}>
|
||||
<summary>Google Reader</summary>
|
||||
<div class="form-section">
|
||||
<label>
|
||||
<input type="checkbox" name="googlereader_enabled" value="1" {{ if .form.GoogleReaderEnabled }}checked{{ end }}> {{ t "form.integration.googlereader_activate" }}
|
||||
</label>
|
||||
|
||||
<label for="form-googlereader-username">{{ t "form.integration.googlereader_username" }}</label>
|
||||
<input type="text" name="googlereader_username" id="form-googlereader-username" value="{{ .form.GoogleReaderUsername }}" autocomplete="username" spellcheck="false">
|
||||
|
||||
<label for="form-googlereader-password">{{ t "form.integration.googlereader_password" }}</label>
|
||||
<input type="password" name="googlereader_password" id="form-googlereader-password" value="{{ .form.GoogleReaderPassword }}" autocomplete="new-password">
|
||||
|
||||
<p>{{ t "form.integration.googlereader_endpoint" }} <strong>{{ rootURL }}{{ route "login" }}</strong></p>
|
||||
|
||||
<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.InstapaperEnabled }}open{{ end }}>
|
||||
<summary>Instapaper</summary>
|
||||
<div class="form-section">
|
||||
<label>
|
||||
<input type="checkbox" name="instapaper_enabled" value="1" {{ if .form.InstapaperEnabled }}checked{{ end }}> {{ t "form.integration.instapaper_activate" }}
|
||||
</label>
|
||||
|
||||
<label for="form-instapaper-username">{{ t "form.integration.instapaper_username" }}</label>
|
||||
<input type="text" name="instapaper_username" id="form-instapaper-username" value="{{ .form.InstapaperUsername }}" spellcheck="false">
|
||||
|
||||
<label for="form-instapaper-password">{{ t "form.integration.instapaper_password" }}</label>
|
||||
<input type="password" name="instapaper_password" id="form-instapaper-password" value="{{ .form.InstapaperPassword }}" autocomplete="new-password">
|
||||
|
||||
<div class="buttons">
|
||||
<button type="submit" class="button button-primary" data-label-loading="{{ t "form.submit.saving" }}">{{ t "action.update" }}</button>
|
||||
</div>
|
||||
|
@ -238,62 +125,20 @@
|
|||
<label>
|
||||
<input type="checkbox" name="linkding_enabled" value="1" {{ if .form.LinkdingEnabled }}checked{{ end }}> {{ t "form.integration.linkding_activate" }}
|
||||
</label>
|
||||
|
||||
|
||||
<label for="form-linkding-url">{{ t "form.integration.linkding_endpoint" }}</label>
|
||||
<input type="url" name="linkding_url" id="form-linkding-url" value="{{ .form.LinkdingURL }}" placeholder="https://linkding.com" spellcheck="false">
|
||||
|
||||
|
||||
<label for="form-linkding-api-key">{{ t "form.integration.linkding_api_key" }}</label>
|
||||
<input type="text" name="linkding_api_key" id="form-linkding-api-key" value="{{ .form.LinkdingAPIKey }}" spellcheck="false">
|
||||
|
||||
|
||||
<label for="form-linkding-tags">{{ t "form.integration.linkding_tags" }}</label>
|
||||
<input type="text" name="linkding_tags" id="form-linkding-tags" value="{{ .form.LinkdingTags }}" spellcheck="false">
|
||||
|
||||
|
||||
<label>
|
||||
<input type="checkbox" name="linkding_mark_as_unread" value="1" {{ if .form.LinkdingMarkAsUnread }}checked{{ end }}> {{ t "form.integration.linkding_bookmark" }}
|
||||
</label>
|
||||
|
||||
<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.AppriseEnabled }}open{{ end }}>
|
||||
<summary>Apprise</summary>
|
||||
<div class="form-section">
|
||||
<label>
|
||||
<input type="checkbox" name="apprise_enabled" value="1" {{ if .form.AppriseEnabled }}checked{{ end }}> {{ t "form.integration.apprise_activate" }}
|
||||
</label>
|
||||
|
||||
<label for="form-apprise-url">{{ t "form.integration.apprise_url" }}</label>
|
||||
<input type="text" name="apprise_url" id="form-apprise-url" value="{{ .form.AppriseURL }}" placeholder="http://apprise:8080" spellcheck="false">
|
||||
|
||||
<label for="form-apprise-services-url">{{ t "form.integration.apprise_services_url" }}
|
||||
<a href="https://github.com/caronc/apprise/wiki" target="_blank">
|
||||
{{ icon "external-link" }}
|
||||
</a>
|
||||
</label>
|
||||
<input type="text" name="apprise_services_url" id="form-apprise-services-urls" value="{{ .form.AppriseServicesURL }}" placeholder="tgram://<token>/<chat_id>/,matrix://" 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.TelegramBotEnabled }}open{{ end }}>
|
||||
<summary>Telegram Bot</summary>
|
||||
<div class="form-section">
|
||||
<label>
|
||||
<input type="checkbox" name="telegram_bot_enabled" value="1" {{ if .form.TelegramBotEnabled }}checked{{ end }}> {{ t "form.integration.telegram_bot_activate" }}
|
||||
</label>
|
||||
|
||||
<label for="form-telegram-bot-token">{{ t "form.integration.telegram_bot_token" }}</label>
|
||||
<input type="text" name="telegram_bot_token" id="form-telegram-bot-token" value="{{ .form.TelegramBotToken }}" placeholder="bot123456:Abcdefg" spellcheck="false">
|
||||
|
||||
<label for="form-telegram-chat-id">{{ t "form.integration.telegram_chat_id" }}</label>
|
||||
<input type="text" name="telegram_bot_chat_id" id="form-telegram-chat-id" value="{{ .form.TelegramBotChatID }}" spellcheck="false">
|
||||
|
||||
<div class="buttons">
|
||||
<button type="submit" class="button button-primary" data-label-loading="{{ t "form.submit.saving" }}">{{ t "action.update" }}</button>
|
||||
</div>
|
||||
|
@ -306,19 +151,123 @@
|
|||
<label>
|
||||
<input type="checkbox" name="matrix_bot_enabled" value="1" {{ if .form.MatrixBotEnabled }}checked{{ end }}> {{ t "form.integration.matrix_bot_activate" }}
|
||||
</label>
|
||||
|
||||
|
||||
<label for="form-matrix-bot-user">{{ t "form.integration.matrix_bot_user" }}</label>
|
||||
<input type="text" name="matrix_bot_user" id="form-matrix-bot-user" value="{{ .form.MatrixBotUser }}" spellcheck="false">
|
||||
|
||||
|
||||
<label for="form-matrix-chat-password">{{ t "form.integration.matrix_bot_password" }}</label>
|
||||
<input type="password" name="matrix_bot_password" id="form-matrix-password" value="{{ .form.MatrixBotPassword }}" spellcheck="false">
|
||||
|
||||
|
||||
<label for="form-matrix-url">{{ t "form.integration.matrix_bot_url" }}</label>
|
||||
<input type="text" name="matrix_bot_url" id="form-matrix-url" value="{{ .form.MatrixBotURL }}" spellcheck="false">
|
||||
|
||||
<input type="url" name="matrix_bot_url" id="form-matrix-url" value="{{ .form.MatrixBotURL }}" spellcheck="false">
|
||||
|
||||
<label for="form-matrix-chat-id">{{ t "form.integration.matrix_bot_chat_id" }}</label>
|
||||
<input type="text" name="matrix_bot_chat_id" id="form-matrix-chat-id" value="{{ .form.MatrixBotChatID }}" 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.NotionEnabled }}open{{ end }}>
|
||||
<summary>Notion</summary>
|
||||
<div class="form-section">
|
||||
<label>
|
||||
<input type="checkbox" name="notion_enabled" value="1" {{ if .form.NotionEnabled }}checked{{ end }}> {{ t "form.integration.notion_activate" }}
|
||||
</label>
|
||||
|
||||
<label for="form-notion-token">{{ t "form.integration.notion_token" }}</label>
|
||||
<input type="password" name="notion_token" id="form-notion-token" value="{{ .form.NotionToken }}" spellcheck="false">
|
||||
|
||||
<label for="form-notion-page-id">{{ t "form.integration.notion_page_id" }}</label>
|
||||
<input type="text" name="notion_page_id" id="form-notion-page-id" value="{{ .form.NotionPageID }}" 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.NunuxKeeperEnabled }}open{{ end }}>
|
||||
<summary>Nunux Keeper</summary>
|
||||
<div class="form-section">
|
||||
<label>
|
||||
<input type="checkbox" name="nunux_keeper_enabled" value="1" {{ if .form.NunuxKeeperEnabled }}checked{{ end }}> {{ t "form.integration.nunux_keeper_activate" }}
|
||||
</label>
|
||||
|
||||
<label for="form-nunux-keeper-url">{{ t "form.integration.nunux_keeper_endpoint" }}</label>
|
||||
<input type="url" name="nunux_keeper_url" id="form-nunux-keeper-url" value="{{ .form.NunuxKeeperURL }}" placeholder="https://api.nunux.org/keeper" spellcheck="false">
|
||||
|
||||
<label for="form-nunux-keeper-api-key">{{ t "form.integration.nunux_keeper_api_key" }}</label>
|
||||
<input type="text" name="nunux_keeper_api_key" id="form-nunux-keeper-api-key" value="{{ .form.NunuxKeeperAPIKey }}" 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.PinboardEnabled }}open{{ end }}>
|
||||
<summary>Pinboard</summary>
|
||||
<div class="form-section">
|
||||
<label>
|
||||
<input type="checkbox" name="pinboard_enabled" value="1" {{ if .form.PinboardEnabled }}checked{{ end }}> {{ t "form.integration.pinboard_activate" }}
|
||||
</label>
|
||||
|
||||
<label for="form-pinboard-token">{{ t "form.integration.pinboard_token" }}</label>
|
||||
<input type="password" name="pinboard_token" id="form-pinboard-token" value="{{ .form.PinboardToken }}" autocomplete="new-password">
|
||||
|
||||
<label for="form-pinboard-tags">{{ t "form.integration.pinboard_tags" }}</label>
|
||||
<input type="text" name="pinboard_tags" id="form-pinboard-tags" value="{{ .form.PinboardTags }}" spellcheck="false">
|
||||
|
||||
<label>
|
||||
<input type="checkbox" name="pinboard_mark_as_unread" value="1" {{ if .form.PinboardMarkAsUnread }}checked{{ end }}> {{ t "form.integration.pinboard_bookmark" }}
|
||||
</label>
|
||||
|
||||
<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.PocketEnabled }}open{{ end }}>
|
||||
<summary>Pocket</summary>
|
||||
<div class="form-section">
|
||||
<label>
|
||||
<input type="checkbox" name="pocket_enabled" value="1" {{ if .form.PocketEnabled }}checked{{ end }}> {{ t "form.integration.pocket_activate" }}
|
||||
</label>
|
||||
|
||||
{{ if not .hasPocketConsumerKeyConfigured }}
|
||||
<label for="form-pocket-consumer-key">{{ t "form.integration.pocket_consumer_key" }}</label>
|
||||
<input type="text" name="pocket_consumer_key" id="form-pocket-consumer-key" value="{{ .form.PocketConsumerKey }}" spellcheck="false">
|
||||
{{ end }}
|
||||
|
||||
<label for="form-pocket-access-token">{{ t "form.integration.pocket_access_token" }}</label>
|
||||
<input type="password" name="pocket_access_token" id="form-pocket-access-token" value="{{ .form.PocketAccessToken }}" autocomplete="new-password">
|
||||
|
||||
{{ if not .form.PocketAccessToken }}
|
||||
<p><a href="{{ route "pocketAuthorize" }}">{{ t "form.integration.pocket_connect_link" }}</a></p>
|
||||
{{ end }}
|
||||
|
||||
<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.ReadwiseEnabled }}open{{ end }}>
|
||||
<summary>Readwise Reader</summary>
|
||||
<div class="form-section">
|
||||
<label>
|
||||
<input type="checkbox" name="readwise_enabled" value="1" {{ if .form.ReadwiseEnabled }}checked{{ end }}> {{ t "form.integration.readwise_activate" }}
|
||||
</label>
|
||||
|
||||
<label for="form-readwise-api-key">{{ t "form.integration.readwise_api_key" }}</label>
|
||||
<input type="text" name="readwise_api_key" id="form-readwise-api-key" value="{{ .form.ReadwiseAPIKey }}" spellcheck="false">
|
||||
|
||||
<p><a href="https://readwise.io/access_token" target="_blank">{{ t "form.integration.readwise_api_key_link" }}</a></p>
|
||||
|
||||
<div class="buttons">
|
||||
<button type="submit" class="button button-primary" data-label-loading="{{ t "form.submit.saving" }}">{{ t "action.update" }}</button>
|
||||
</div>
|
||||
|
@ -365,6 +314,78 @@
|
|||
</div>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<details {{ if .form.TelegramBotEnabled }}open{{ end }}>
|
||||
<summary>Telegram Bot</summary>
|
||||
<div class="form-section">
|
||||
<label>
|
||||
<input type="checkbox" name="telegram_bot_enabled" value="1" {{ if .form.TelegramBotEnabled }}checked{{ end }}> {{ t "form.integration.telegram_bot_activate" }}
|
||||
</label>
|
||||
|
||||
<label for="form-telegram-bot-token">{{ t "form.integration.telegram_bot_token" }}</label>
|
||||
<input type="text" name="telegram_bot_token" id="form-telegram-bot-token" value="{{ .form.TelegramBotToken }}" placeholder="bot123456:Abcdefg" spellcheck="false">
|
||||
|
||||
<label for="form-telegram-chat-id">{{ t "form.integration.telegram_chat_id" }}</label>
|
||||
<input type="text" name="telegram_bot_chat_id" id="form-telegram-chat-id" value="{{ .form.TelegramBotChatID }}" 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.WallabagEnabled }}open{{ end }}>
|
||||
<summary>Wallabag</summary>
|
||||
<div class="form-section">
|
||||
<label>
|
||||
<input type="checkbox" name="wallabag_enabled" value="1" {{ if .form.WallabagEnabled }}checked{{ end }}> {{ t "form.integration.wallabag_activate" }}
|
||||
</label>
|
||||
|
||||
<label>
|
||||
<input type="checkbox" name="wallabag_only_url" value="1" {{ if .form.WallabagOnlyURL }}checked{{ end }}> {{ t "form.integration.wallabag_only_url" }}
|
||||
</label>
|
||||
|
||||
<label for="form-wallabag-url">{{ t "form.integration.wallabag_endpoint" }}</label>
|
||||
<input type="url" name="wallabag_url" id="form-wallabag-url" value="{{ .form.WallabagURL }}" placeholder="http://v2.wallabag.org/" spellcheck="false">
|
||||
|
||||
<label for="form-wallabag-client-id">{{ t "form.integration.wallabag_client_id" }}</label>
|
||||
<input type="text" name="wallabag_client_id" id="form-wallabag-client-id" value="{{ .form.WallabagClientID }}" spellcheck="false">
|
||||
|
||||
<label for="form-wallabag-client-secret">{{ t "form.integration.wallabag_client_secret" }}</label>
|
||||
<input type="password" name="wallabag_client_secret" id="form-wallabag-client-secret" value="{{ .form.WallabagClientSecret }}" autocomplete="new-password">
|
||||
|
||||
<label for="form-wallabag-username">{{ t "form.integration.wallabag_username" }}</label>
|
||||
<input type="text" name="wallabag_username" id="form-wallabag-username" value="{{ .form.WallabagUsername }}" spellcheck="false">
|
||||
|
||||
<label for="form-wallabag-password">{{ t "form.integration.wallabag_password" }}</label>
|
||||
<input type="password" name="wallabag_password" id="form-wallabag-password" value="{{ .form.WallabagPassword }}" autocomplete="new-password">
|
||||
|
||||
<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.WebhookEnabled }}open{{ end }}>
|
||||
<summary>Webhook</summary>
|
||||
<div class="form-section">
|
||||
<label>
|
||||
<input type="checkbox" name="webhook_enabled" value="1" {{ if .form.WebhookEnabled }}checked{{ end }}> {{ t "form.integration.webhook_activate" }}
|
||||
</label>
|
||||
|
||||
<label for="form-webhook-url">{{ t "form.integration.webhook_url" }}</label>
|
||||
<input type="url" name="webhook_url" id="form-webhook-url" value="{{ .form.WebhookURL }}" placeholder="https://username:password@example.org" spellcheck="false">
|
||||
|
||||
{{ if .form.WebhookSecret }}
|
||||
<label for="form-webhook-secret">{{ t "form.integration.webhook_secret" }}</label>
|
||||
<input type="text" name="webhook_secret" id="form-webhook-secret" value="{{ .form.WebhookSecret }}" spellcheck="false" readonly>
|
||||
{{ end }}
|
||||
|
||||
<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>
|
||||
|
|
|
@ -69,6 +69,9 @@ type IntegrationForm struct {
|
|||
ShaarliEnabled bool
|
||||
ShaarliURL string
|
||||
ShaarliAPISecret string
|
||||
WebhookEnabled bool
|
||||
WebhookURL string
|
||||
WebhookSecret string
|
||||
}
|
||||
|
||||
// Merge copy form values to the model.
|
||||
|
@ -129,6 +132,8 @@ func (i IntegrationForm) Merge(integration *model.Integration) {
|
|||
integration.ShaarliEnabled = i.ShaarliEnabled
|
||||
integration.ShaarliURL = i.ShaarliURL
|
||||
integration.ShaarliAPISecret = i.ShaarliAPISecret
|
||||
integration.WebhookEnabled = i.WebhookEnabled
|
||||
integration.WebhookURL = i.WebhookURL
|
||||
}
|
||||
|
||||
// NewIntegrationForm returns a new IntegrationForm.
|
||||
|
@ -192,5 +197,7 @@ func NewIntegrationForm(r *http.Request) *IntegrationForm {
|
|||
ShaarliEnabled: r.FormValue("shaarli_enabled") == "1",
|
||||
ShaarliURL: r.FormValue("shaarli_url"),
|
||||
ShaarliAPISecret: r.FormValue("shaarli_api_secret"),
|
||||
WebhookEnabled: r.FormValue("webhook_enabled") == "1",
|
||||
WebhookURL: r.FormValue("webhook_url"),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -84,6 +84,9 @@ func (h *handler) showIntegrationPage(w http.ResponseWriter, r *http.Request) {
|
|||
ShaarliEnabled: integration.ShaarliEnabled,
|
||||
ShaarliURL: integration.ShaarliURL,
|
||||
ShaarliAPISecret: integration.ShaarliAPISecret,
|
||||
WebhookEnabled: integration.WebhookEnabled,
|
||||
WebhookURL: integration.WebhookURL,
|
||||
WebhookSecret: integration.WebhookSecret,
|
||||
}
|
||||
|
||||
sess := session.New(h.store, request.SessionID(r))
|
||||
|
|
|
@ -67,6 +67,18 @@ func (h *handler) updateIntegration(w http.ResponseWriter, r *http.Request) {
|
|||
integration.GoogleReaderPassword = ""
|
||||
}
|
||||
|
||||
if integrationForm.WebhookEnabled {
|
||||
if integrationForm.WebhookURL == "" {
|
||||
integration.WebhookEnabled = false
|
||||
integration.WebhookSecret = ""
|
||||
} else if integration.WebhookSecret == "" {
|
||||
integration.WebhookSecret = crypto.GenerateRandomStringHex(32)
|
||||
}
|
||||
} else {
|
||||
integration.WebhookURL = ""
|
||||
integration.WebhookSecret = ""
|
||||
}
|
||||
|
||||
err = h.store.UpdateIntegration(integration)
|
||||
if err != nil {
|
||||
html.ServerError(w, r, err)
|
||||
|
|
Loading…
Add table
Reference in a new issue