ソースを参照

feat: add auto-push option to readeck integration

Lennard Schwarz 3 ヶ月 前
コミット
fbc4d700d5

+ 7 - 0
internal/database/migrations.go

@@ -1380,4 +1380,11 @@ 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 readeck_push_enabled bool default 'f';
+		`
+		_, err = tx.Exec(sql)
+		return err
+	},
 }

+ 26 - 0
internal/integration/integration.go

@@ -685,4 +685,30 @@ func PushEntries(feed *model.Feed, entries model.Entries, userIntegrations *mode
 			}
 		}
 	}
+
+	// Push each new entry to Readeck when push is enabled
+	if userIntegrations.ReadeckPushEnabled {
+		client := readeck.NewClient(
+			userIntegrations.ReadeckURL,
+			userIntegrations.ReadeckAPIKey,
+			userIntegrations.ReadeckLabels,
+			userIntegrations.ReadeckOnlyURL,
+		)
+		for _, entry := range entries {
+			slog.Debug("Sending a new entry to Readeck",
+				slog.Int64("user_id", userIntegrations.UserID),
+				slog.Int64("entry_id", entry.ID),
+				slog.String("entry_url", entry.URL),
+			)
+
+			if err := client.CreateBookmark(entry.URL, entry.Title, entry.Content); err != nil {
+				slog.Error("Unable to send entry to Readeck",
+					slog.Int64("user_id", userIntegrations.UserID),
+					slog.Int64("entry_id", entry.ID),
+					slog.String("entry_url", entry.URL),
+					slog.Any("error", err),
+				)
+			}
+		}
+	}
 }

+ 260 - 0
internal/integration/readeck/readeck_test.go

@@ -0,0 +1,260 @@
+// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package readeck
+
+import (
+    "encoding/json"
+    "io"
+    "mime/multipart"
+    "net/http"
+    "net/http/httptest"
+    "strings"
+    "testing"
+)
+
+func TestCreateBookmark(t *testing.T) {
+    entryURL := "https://example.com/article"
+    entryTitle := "Example Title"
+    entryContent := "<p>Some HTML content</p>"
+    labels := "tag1,tag2"
+
+    tests := []struct {
+        name           string
+        onlyURL        bool
+        baseURL        string
+        apiKey         string
+        labels         string
+        entryURL       string
+        entryTitle     string
+        entryContent   string
+        serverResponse func(w http.ResponseWriter, r *http.Request)
+        wantErr        bool
+        errContains    string
+    }{
+        {
+            name:     "successful bookmark creation with only URL",
+            onlyURL:  true,
+            labels:   labels,
+            entryURL: entryURL,
+            entryTitle: entryTitle,
+            entryContent: entryContent,
+            serverResponse: func(w http.ResponseWriter, r *http.Request) {
+                if r.Method != http.MethodPost {
+                    t.Errorf("expected POST, got %s", r.Method)
+                }
+                if r.URL.Path != "/api/bookmarks/" {
+                    t.Errorf("expected path /api/bookmarks/, got %s", r.URL.Path)
+                }
+                if got := r.Header.Get("Authorization"); !strings.HasPrefix(got, "Bearer ") {
+                    t.Errorf("expected Authorization Bearer header, got %q", got)
+                }
+                if ct := r.Header.Get("Content-Type"); ct != "application/json" {
+                    t.Errorf("expected Content-Type application/json, got %s", ct)
+                }
+
+                body, _ := io.ReadAll(r.Body)
+                var payload map[string]any
+                if err := json.Unmarshal(body, &payload); err != nil {
+                    t.Fatalf("failed to parse JSON body: %v", err)
+                }
+                if u := payload["url"]; u != entryURL {
+                    t.Errorf("expected url %s, got %v", entryURL, u)
+                }
+                if title := payload["title"]; title != entryTitle {
+                    t.Errorf("expected title %s, got %v", entryTitle, title)
+                }
+                // Labels should be split into an array
+                if raw := payload["labels"]; raw == nil {
+                    t.Errorf("expected labels to be set")
+                } else if arr, ok := raw.([]any); ok {
+                    if len(arr) != 2 || arr[0] != "tag1" || arr[1] != "tag2" {
+                        t.Errorf("unexpected labels: %#v", arr)
+                    }
+                } else {
+                    t.Errorf("labels should be an array, got %T", raw)
+                }
+                w.WriteHeader(http.StatusOK)
+            },
+        },
+        {
+            name:     "successful bookmark creation with content (multipart)",
+            onlyURL:  false,
+            labels:   labels,
+            entryURL: entryURL,
+            entryTitle: entryTitle,
+            entryContent: entryContent,
+            serverResponse: func(w http.ResponseWriter, r *http.Request) {
+                if r.Method != http.MethodPost {
+                    t.Errorf("expected POST, got %s", r.Method)
+                }
+                if r.URL.Path != "/api/bookmarks/" {
+                    t.Errorf("expected path /api/bookmarks/, got %s", r.URL.Path)
+                }
+                if got := r.Header.Get("Authorization"); !strings.HasPrefix(got, "Bearer ") {
+                    t.Errorf("expected Authorization Bearer header, got %q", got)
+                }
+                ct := r.Header.Get("Content-Type")
+                if !strings.HasPrefix(ct, "multipart/form-data;") {
+                    t.Errorf("expected multipart/form-data, got %s", ct)
+                }
+                boundaryIdx := strings.Index(ct, "boundary=")
+                if boundaryIdx == -1 {
+                    t.Fatalf("missing multipart boundary in Content-Type: %s", ct)
+                }
+                boundary := ct[boundaryIdx+len("boundary="):]
+                mr := multipart.NewReader(r.Body, boundary)
+
+                seenLabels := []string{}
+                var seenURL, seenTitle, seenFeature string
+                var resourceHeader map[string]any
+                var resourceBody string
+
+                for {
+                    part, err := mr.NextPart()
+                    if err == io.EOF {
+                        break
+                    }
+                    if err != nil {
+                        t.Fatalf("reading multipart: %v", err)
+                    }
+                    name := part.FormName()
+                    data, _ := io.ReadAll(part)
+                    switch name {
+                    case "url":
+                        seenURL = string(data)
+                    case "title":
+                        seenTitle = string(data)
+                    case "feature_find_main":
+                        seenFeature = string(data)
+                    case "labels":
+                        seenLabels = append(seenLabels, string(data))
+                    case "resource":
+                        // First line is JSON header, then newline, then content
+                        all := string(data)
+                        idx := strings.IndexByte(all, '\n')
+                        if idx == -1 {
+                            t.Fatalf("resource content missing header separator")
+                        }
+                        headerJSON := all[:idx]
+                        resourceBody = all[idx+1:]
+                        if err := json.Unmarshal([]byte(headerJSON), &resourceHeader); err != nil {
+                            t.Fatalf("invalid resource header JSON: %v", err)
+                        }
+                    }
+                }
+
+                if seenURL != entryURL {
+                    t.Errorf("expected url %s, got %s", entryURL, seenURL)
+                }
+                if seenTitle != entryTitle {
+                    t.Errorf("expected title %s, got %s", entryTitle, seenTitle)
+                }
+                if seenFeature != "false" {
+                    t.Errorf("expected feature_find_main to be 'false', got %s", seenFeature)
+                }
+                if len(seenLabels) != 2 || seenLabels[0] != "tag1" || seenLabels[1] != "tag2" {
+                    t.Errorf("unexpected labels: %#v", seenLabels)
+                }
+                if resourceHeader == nil {
+                    t.Fatalf("missing resource header")
+                }
+                if hURL, _ := resourceHeader["url"].(string); hURL != entryURL {
+                    t.Errorf("expected resource header url %s, got %v", entryURL, hURL)
+                }
+                if headers, ok := resourceHeader["headers"].(map[string]any); ok {
+                    if ct, _ := headers["content-type"].(string); ct != "text/html; charset=utf-8" {
+                        t.Errorf("expected resource header content-type text/html; charset=utf-8, got %v", ct)
+                    }
+                } else {
+                    t.Errorf("missing resource header 'headers' field")
+                }
+                if resourceBody != entryContent {
+                    t.Errorf("expected resource body %q, got %q", entryContent, resourceBody)
+                }
+
+                w.WriteHeader(http.StatusOK)
+            },
+        },
+        {
+            name:        "error when server returns 400",
+            onlyURL:     true,
+            labels:      labels,
+            entryURL:    entryURL,
+            entryTitle:  entryTitle,
+            entryContent: entryContent,
+            serverResponse: func(w http.ResponseWriter, r *http.Request) {
+                w.WriteHeader(http.StatusBadRequest)
+            },
+            wantErr:     true,
+            errContains: "unable to create bookmark",
+        },
+        {
+            name:        "error when missing baseURL or apiKey",
+            onlyURL:     true,
+            baseURL:     "",
+            apiKey:      "",
+            labels:      labels,
+            entryURL:    entryURL,
+            entryTitle:  entryTitle,
+            entryContent: entryContent,
+            serverResponse: nil,
+            wantErr:     true,
+            errContains: "missing base URL or API key",
+        },
+    }
+
+    for _, tt := range tests {
+        t.Run(tt.name, func(t *testing.T) {
+            var serverURL string
+            if tt.serverResponse != nil {
+                srv := httptest.NewServer(http.HandlerFunc(tt.serverResponse))
+                defer srv.Close()
+                serverURL = srv.URL
+            }
+            baseURL := tt.baseURL
+            if baseURL == "" {
+                baseURL = serverURL
+            }
+            apiKey := tt.apiKey
+            if apiKey == "" {
+                apiKey = "test-api-key"
+            }
+
+            client := NewClient(baseURL, apiKey, tt.labels, tt.onlyURL)
+            err := client.CreateBookmark(tt.entryURL, tt.entryTitle, tt.entryContent)
+
+            if tt.wantErr {
+                if err == nil {
+                    t.Fatalf("expected error, got none")
+                }
+                if tt.errContains != "" && !strings.Contains(err.Error(), tt.errContains) {
+                    t.Fatalf("expected error containing %q, got %q", tt.errContains, err.Error())
+                }
+            } else if err != nil {
+                t.Fatalf("unexpected error: %v", err)
+            }
+        })
+    }
+}
+
+func TestNewClient(t *testing.T) {
+    baseURL := "https://readeck.example.com"
+    apiKey := "key"
+    labels := "tag1,tag2"
+    onlyURL := true
+
+    c := NewClient(baseURL, apiKey, labels, onlyURL)
+    if c.baseURL != baseURL {
+        t.Errorf("expected baseURL %s, got %s", baseURL, c.baseURL)
+    }
+    if c.apiKey != apiKey {
+        t.Errorf("expected apiKey %s, got %s", apiKey, c.apiKey)
+    }
+    if c.labels != labels {
+        t.Errorf("expected labels %s, got %s", labels, c.labels)
+    }
+    if c.onlyURL != onlyURL {
+        t.Errorf("expected onlyURL %v, got %v", onlyURL, c.onlyURL)
+    }
+}

+ 2 - 1
internal/locale/translations/de_DE.json

@@ -307,6 +307,7 @@
     "form.integration.readeck_endpoint": "Readeck-URL",
     "form.integration.readeck_labels": "Readeck-Labels",
     "form.integration.readeck_only_url": "Nur URL senden (anstelle des vollständigen Inhalts)",
+    "form.integration.readeck_push_activate": "Neue Artikel automatisch in Readeck speichern",
     "form.integration.readwise_activate": "Artikel in Readwise Reader speichern",
     "form.integration.readwise_api_key": "Readwise-Reader-Zugangstoken",
     "form.integration.readwise_api_key_link": "Erhalten Sie Ihren Readwise-Zugangstoken",
@@ -630,4 +631,4 @@
     "time_elapsed.yesterday": "gestern",
     "tooltip.keyboard_shortcuts": "Tastenkürzel: %s",
     "tooltip.logged_user": "Angemeldet als %s"
-}
+}

+ 2 - 1
internal/locale/translations/el_EL.json

@@ -307,6 +307,7 @@
     "form.integration.readeck_endpoint": "Τελικό σημείο Readeck API",
     "form.integration.readeck_labels": "Ετικέτες Readeck",
     "form.integration.readeck_only_url": "Αποστολή μόνο URL (αντί για πλήρες περιεχόμενο)",
+    "form.integration.readeck_push_activate": "Automatically push new entries to Readeck",
     "form.integration.readwise_activate": "Αποθήκευση καταχωρήσεων στο Readwise Reader",
     "form.integration.readwise_api_key": "Διακριτικό πρόσβασης Readwise Reader",
     "form.integration.readwise_api_key_link": "Λήψη του διακριτικού πρόσβασης Readwise",
@@ -630,4 +631,4 @@
     "time_elapsed.yesterday": "χθες",
     "tooltip.keyboard_shortcuts": "Συντόμευση πληκτρολογίου: % s",
     "tooltip.logged_user": "Συνδεδεμένος/η ως %s"
-}
+}

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

@@ -307,6 +307,7 @@
     "form.integration.readeck_endpoint": "Readeck URL",
     "form.integration.readeck_labels": "Readeck Labels",
     "form.integration.readeck_only_url": "Send only URL (instead of full content)",
+    "form.integration.readeck_push_activate": "Automatically push new entries to Readeck",
     "form.integration.readwise_activate": "Save entries to Readwise Reader",
     "form.integration.readwise_api_key": "Readwise Reader Access Token",
     "form.integration.readwise_api_key_link": "Get your Readwise Access Token",

+ 2 - 1
internal/locale/translations/es_ES.json

@@ -307,6 +307,7 @@
     "form.integration.readeck_endpoint": "Acceso API de Readeck",
     "form.integration.readeck_labels": "Etiquetas de Readeck",
     "form.integration.readeck_only_url": "Enviar solo URL (en lugar de contenido completo)",
+    "form.integration.readeck_push_activate": "Automatically push new entries to Readeck",
     "form.integration.readwise_activate": "Guardar artículos en Readwise Reader",
     "form.integration.readwise_api_key": "Token de acceso a Readwise Reader",
     "form.integration.readwise_api_key_link": "Obtener tu token de acceso a Readwise",
@@ -630,4 +631,4 @@
     "time_elapsed.yesterday": "ayer",
     "tooltip.keyboard_shortcuts": "Atajo de teclado: %s",
     "tooltip.logged_user": "Registrado como %s"
-}
+}

+ 2 - 1
internal/locale/translations/fi_FI.json

@@ -307,6 +307,7 @@
     "form.integration.readeck_endpoint": "Readeck API-päätepiste",
     "form.integration.readeck_labels": "Readeck Labels",
     "form.integration.readeck_only_url": "Lähetä vain URL-osoite (koko sisällön sijaan)",
+    "form.integration.readeck_push_activate": "Automatically push new entries to Readeck",
     "form.integration.readwise_activate": "Save entries to Readwise Reader",
     "form.integration.readwise_api_key": "Readwise Reader Access Token",
     "form.integration.readwise_api_key_link": "Get your Readwise Access Token",
@@ -630,4 +631,4 @@
     "time_elapsed.yesterday": "eilen",
     "tooltip.keyboard_shortcuts": "Pikanäppäin: %s",
     "tooltip.logged_user": "Kirjautunut %s-käyttäjänä"
-}
+}

+ 2 - 1
internal/locale/translations/fr_FR.json

@@ -307,6 +307,7 @@
     "form.integration.readeck_endpoint": "URL de l'API de Readeck",
     "form.integration.readeck_labels": "Libellés Readeck",
     "form.integration.readeck_only_url": "Envoyer uniquement l'URL (au lieu du contenu complet)",
+    "form.integration.readeck_push_activate": "Automatically push new entries to Readeck",
     "form.integration.readwise_activate": "Enregistrer les entrées vers Readwise Reader",
     "form.integration.readwise_api_key": "Jeton d'accès au lecteur Readwise",
     "form.integration.readwise_api_key_link": "Obtenez votre jeton d'accès Readwise",
@@ -630,4 +631,4 @@
     "time_elapsed.yesterday": "hier",
     "tooltip.keyboard_shortcuts": "Raccourci clavier : %s",
     "tooltip.logged_user": "Connecté en tant que %s"
-}
+}

+ 2 - 1
internal/locale/translations/hi_IN.json

@@ -307,6 +307,7 @@
     "form.integration.readeck_endpoint": "Readeck यूआरएल",
     "form.integration.readeck_labels": "Readeck Labels",
     "form.integration.readeck_only_url": "केवल URL भेजें (पूर्ण सामग्री के बजाय)",
+    "form.integration.readeck_push_activate": "Automatically push new entries to Readeck",
     "form.integration.readwise_activate": "Save entries to Readwise Reader",
     "form.integration.readwise_api_key": "Readwise Reader Access Token",
     "form.integration.readwise_api_key_link": "Get your Readwise Access Token",
@@ -630,4 +631,4 @@
     "time_elapsed.yesterday": "कल",
     "tooltip.keyboard_shortcuts": "कुंजीपटल शॉर्टकट: %s",
     "tooltip.logged_user": "%s के रूप में लॉग इन किया"
-}
+}

+ 2 - 1
internal/locale/translations/id_ID.json

@@ -304,6 +304,7 @@
     "form.integration.readeck_endpoint": "Titik URL API Readeck",
     "form.integration.readeck_labels": "Tagar Readeck",
     "form.integration.readeck_only_url": "Kirim hanya URL (alih-alih konten penuh)",
+    "form.integration.readeck_push_activate": "Automatically push new entries to Readeck",
     "form.integration.readwise_activate": "Simpan artikel ke Readwise",
     "form.integration.readwise_api_key": "Token Akses Readwise",
     "form.integration.readwise_api_key_link": "Dapatkan Token Akses Readwise Anda",
@@ -612,4 +613,4 @@
     "time_elapsed.yesterday": "kemarin",
     "tooltip.keyboard_shortcuts": "Pintasan Papan Tik: %s",
     "tooltip.logged_user": "Masuk sebagai %s"
-}
+}

+ 2 - 1
internal/locale/translations/it_IT.json

@@ -307,6 +307,7 @@
     "form.integration.readeck_endpoint": "Endpoint dell'API di Readeck",
     "form.integration.readeck_labels": "Readeck Labels",
     "form.integration.readeck_only_url": "Invia solo URL (invece del contenuto completo)",
+    "form.integration.readeck_push_activate": "Automatically push new entries to Readeck",
     "form.integration.readwise_activate": "Save entries to Readwise Reader",
     "form.integration.readwise_api_key": "Readwise Reader Access Token",
     "form.integration.readwise_api_key_link": "Get your Readwise Access Token",
@@ -630,4 +631,4 @@
     "time_elapsed.yesterday": "ieri",
     "tooltip.keyboard_shortcuts": "Scorciatoia da tastiera: %s",
     "tooltip.logged_user": "Autenticato come %s"
-}
+}

+ 2 - 1
internal/locale/translations/ja_JP.json

@@ -304,6 +304,7 @@
     "form.integration.readeck_endpoint": "Readeck の API Endpoint",
     "form.integration.readeck_labels": "Readeck Labels",
     "form.integration.readeck_only_url": "URL のみを送信 (完全なコンテンツではなく)",
+    "form.integration.readeck_push_activate": "Automatically push new entries to Readeck",
     "form.integration.readwise_activate": "Save entries to Readwise Reader",
     "form.integration.readwise_api_key": "Readwise Reader Access Token",
     "form.integration.readwise_api_key_link": "Get your Readwise Access Token",
@@ -612,4 +613,4 @@
     "time_elapsed.yesterday": "昨日",
     "tooltip.keyboard_shortcuts": "キーボードショートカット: %s",
     "tooltip.logged_user": "%s としてログイン中"
-}
+}

+ 2 - 1
internal/locale/translations/nan_Latn_pehoeji.json

@@ -304,6 +304,7 @@
     "form.integration.readeck_endpoint": "Readeck API thâu",
     "form.integration.readeck_labels": "Readeck Labels",
     "form.integration.readeck_only_url": "Kan-na thoân bāng-chí (m̄ sī oân-chéng ê lōe-iông)",
+    "form.integration.readeck_push_activate": "Automatically push new entries to Readeck",
     "form.integration.readwise_activate": "Pó-chûn siau-sit kàu Readwise Reader",
     "form.integration.readwise_api_key": "Readwise Reader Acess Token",
     "form.integration.readwise_api_key_link": "Chhú-tek lí ê Readwise Acess Token",
@@ -612,4 +613,4 @@
     "time_elapsed.yesterday": "cha-hng",
     "tooltip.keyboard_shortcuts": "Khoài-sok khí:%s",
     "tooltip.logged_user": "Chit-má teng-lo̍k--ê:  %s"
-}
+}

+ 2 - 1
internal/locale/translations/nl_NL.json

@@ -307,6 +307,7 @@
     "form.integration.readeck_endpoint": "Readeck URL",
     "form.integration.readeck_labels": "Readeck Labels",
     "form.integration.readeck_only_url": "Alleen URL verzenden (in plaats van volledige inhoud)",
+    "form.integration.readeck_push_activate": "Automatically push new entries to Readeck",
     "form.integration.readwise_activate": "Artikelen opslaan in Readwise Reader",
     "form.integration.readwise_api_key": "Readwise Reader Access Token",
     "form.integration.readwise_api_key_link": "Readwise Access Token ophalen",
@@ -630,4 +631,4 @@
     "time_elapsed.yesterday": "gisteren",
     "tooltip.keyboard_shortcuts": "Sneltoets: %s",
     "tooltip.logged_user": "Ingelogd als %s"
-}
+}

+ 2 - 1
internal/locale/translations/pl_PL.json

@@ -310,6 +310,7 @@
     "form.integration.readeck_endpoint": "Adres URL Readeck",
     "form.integration.readeck_labels": "Etykiety Readeck",
     "form.integration.readeck_only_url": "Wysyłaj tylko adres URL (zamiast pełnej treści)",
+    "form.integration.readeck_push_activate": "Automatically push new entries to Readeck",
     "form.integration.readwise_activate": "Zapisuj wpisy w czytniku Readwise",
     "form.integration.readwise_api_key": "Token dostępu do czytnika Readwise",
     "form.integration.readwise_api_key_link": "Zdobądź token dostępu Readwise",
@@ -648,4 +649,4 @@
     "time_elapsed.yesterday": "wczoraj",
     "tooltip.keyboard_shortcuts": "Skróty klawiszowe: %s",
     "tooltip.logged_user": "Zalogowany jako %s"
-}
+}

+ 2 - 1
internal/locale/translations/pt_BR.json

@@ -307,6 +307,7 @@
     "form.integration.readeck_endpoint": "Endpoint de API do Readeck",
     "form.integration.readeck_labels": "Readeck Labels",
     "form.integration.readeck_only_url": "Enviar apenas URL (em vez de conteúdo completo)",
+    "form.integration.readeck_push_activate": "Automatically push new entries to Readeck",
     "form.integration.readwise_activate": "Salvar itens no Readwise Reader",
     "form.integration.readwise_api_key": "Token de acesso do Readwise Reader",
     "form.integration.readwise_api_key_link": "Obtenha seu token de acesso do Readwise",
@@ -630,4 +631,4 @@
     "time_elapsed.yesterday": "ontem",
     "tooltip.keyboard_shortcuts": "Atalho do teclado: %s",
     "tooltip.logged_user": "Autenticado como %s"
-}
+}

+ 2 - 1
internal/locale/translations/ro_RO.json

@@ -310,6 +310,7 @@
     "form.integration.readeck_endpoint": "URL Readeck",
     "form.integration.readeck_labels": "Etichete Readeck",
     "form.integration.readeck_only_url": "Trimite numai URL (în loc de tot conținutul)",
+    "form.integration.readeck_push_activate": "Automatically push new entries to Readeck",
     "form.integration.readwise_activate": "Salvare înregistrări în Readwise Reader",
     "form.integration.readwise_api_key": "Token Acces Readwise Reader",
     "form.integration.readwise_api_key_link": "Obțineți Token-ul de Acess pe Readwise",
@@ -648,4 +649,4 @@
     "time_elapsed.yesterday": "ieri",
     "tooltip.keyboard_shortcuts": "Scurtături Tastatură: %s",
     "tooltip.logged_user": "Atentificat ca %s"
-}
+}

+ 2 - 1
internal/locale/translations/ru_RU.json

@@ -310,6 +310,7 @@
     "form.integration.readeck_endpoint": "Конечная точка Readeck API",
     "form.integration.readeck_labels": "Теги Readeck",
     "form.integration.readeck_only_url": "Отправлять только ссылку (без содержимого)",
+    "form.integration.readeck_push_activate": "Automatically push new entries to Readeck",
     "form.integration.readwise_activate": "Сохранить статьи в Readwise",
     "form.integration.readwise_api_key": "Токен доступа в Readwise",
     "form.integration.readwise_api_key_link": "Получить токен доступа Readwise",
@@ -648,4 +649,4 @@
     "time_elapsed.yesterday": "вчера",
     "tooltip.keyboard_shortcuts": "Сочетания клавиш: %s",
     "tooltip.logged_user": "Авторизован как %s"
-}
+}

+ 2 - 1
internal/locale/translations/tr_TR.json

@@ -307,6 +307,7 @@
     "form.integration.readeck_endpoint": "Readeck API Uç Noktası",
     "form.integration.readeck_labels": "Readeck Etiketleri",
     "form.integration.readeck_only_url": "Yalnızca URL gönder (tam makale yerine)",
+    "form.integration.readeck_push_activate": "Automatically push new entries to Readeck",
     "form.integration.readwise_activate": "Makaleleri Readwise Reader'a kaydet",
     "form.integration.readwise_api_key": "Readwise Reader Access Token",
     "form.integration.readwise_api_key_link": "Readwise Access Token'ınızı alın",
@@ -630,4 +631,4 @@
     "time_elapsed.yesterday": "dün",
     "tooltip.keyboard_shortcuts": "Klavye Kısayolu: %s",
     "tooltip.logged_user": "%s olarak giriş yapıldı"
-}
+}

+ 2 - 1
internal/locale/translations/uk_UA.json

@@ -310,6 +310,7 @@
     "form.integration.readeck_endpoint": "Readeck URL",
     "form.integration.readeck_labels": "Readeck Labels",
     "form.integration.readeck_only_url": "Надіслати лише URL (замість повного вмісту)",
+    "form.integration.readeck_push_activate": "Automatically push new entries to Readeck",
     "form.integration.readwise_activate": "Save entries to Readwise Reader",
     "form.integration.readwise_api_key": "Readwise Reader Access Token",
     "form.integration.readwise_api_key_link": "Get your Readwise Access Token",
@@ -648,4 +649,4 @@
     "time_elapsed.yesterday": "вчора",
     "tooltip.keyboard_shortcuts": "Комбінація клавіш: %s",
     "tooltip.logged_user": "Здійснено вхід як %s"
-}
+}

+ 2 - 1
internal/locale/translations/zh_CN.json

@@ -304,6 +304,7 @@
     "form.integration.readeck_endpoint": "Readeck API 端点",
     "form.integration.readeck_labels": "Readeck 标签",
     "form.integration.readeck_only_url": "仅发送 URL(而非完整内容)",
+    "form.integration.readeck_push_activate": "Automatically push new entries to Readeck",
     "form.integration.readwise_activate": "保存条目到 Readwise Reader",
     "form.integration.readwise_api_key": "Readwise Reader 访问令牌",
     "form.integration.readwise_api_key_link": "获取你的 Readwise 访问令牌",
@@ -612,4 +613,4 @@
     "time_elapsed.yesterday": "昨天",
     "tooltip.keyboard_shortcuts": "键盘快捷键:%s",
     "tooltip.logged_user": "登录用户:%s"
-}
+}

+ 2 - 1
internal/locale/translations/zh_TW.json

@@ -304,6 +304,7 @@
     "form.integration.readeck_endpoint": "Readeck API 端點",
     "form.integration.readeck_labels": "Readeck Labels",
     "form.integration.readeck_only_url": "僅傳送網址(而不是完整內容)",
+    "form.integration.readeck_push_activate": "Automatically push new entries to Readeck",
     "form.integration.readwise_activate": "儲存文章到 Readwise Reader",
     "form.integration.readwise_api_key": "Readwise Reader 存取金鑰",
     "form.integration.readwise_api_key_link": "取得您的 Readwise 存取金鑰",
@@ -612,4 +613,4 @@
     "time_elapsed.yesterday": "昨天",
     "tooltip.keyboard_shortcuts": "快捷鍵:%s",
     "tooltip.logged_user": "目前登入 %s"
-}
+}

+ 1 - 0
internal/model/integration.go

@@ -78,6 +78,7 @@ type Integration struct {
 	AppriseURL                       string
 	AppriseServicesURL               string
 	ReadeckEnabled                   bool
+	ReadeckPushEnabled               bool
 	ReadeckURL                       string
 	ReadeckAPIKey                    string
 	ReadeckLabels                    string

+ 6 - 2
internal/storage/integration.go

@@ -178,6 +178,7 @@ func (s *Storage) Integration(userID int64) (*model.Integration, error) {
 			readeck_api_key,
 			readeck_labels,
 			readeck_only_url,
+			readeck_push_enabled,
 			shiori_enabled,
 			shiori_url,
 			shiori_username,
@@ -306,6 +307,7 @@ func (s *Storage) Integration(userID int64) (*model.Integration, error) {
 		&integration.ReadeckAPIKey,
 		&integration.ReadeckLabels,
 		&integration.ReadeckOnlyURL,
+		&integration.ReadeckPushEnabled,
 		&integration.ShioriEnabled,
 		&integration.ShioriURL,
 		&integration.ShioriUsername,
@@ -494,9 +496,10 @@ func (s *Storage) UpdateIntegration(integration *model.Integration) error {
 			linktaco_tags=$117,
 			linktaco_visibility=$118,
 			archiveorg_enabled=$119,
-			linkwarden_collection_id=$120
+			linkwarden_collection_id=$120,
+			readeck_push_enabled=$121
 		WHERE
-			user_id=$121
+			user_id=$122
 	`
 	_, err := s.db.Exec(
 		query,
@@ -620,6 +623,7 @@ func (s *Storage) UpdateIntegration(integration *model.Integration) error {
 		integration.LinktacoVisibility,
 		integration.ArchiveorgEnabled,
 		integration.LinkwardenCollectionID,
+		integration.ReadeckPushEnabled,
 		integration.UserID,
 	)
 

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

@@ -509,6 +509,10 @@
                 <input type="checkbox" name="readeck_enabled" value="1" {{ if .form.ReadeckEnabled }}checked{{ end }}> {{ t "form.integration.readeck_activate" }}
             </label>
 
+            <label>
+                <input type="checkbox" name="readeck_push_enabled" value="1" {{ if .form.ReadeckPushEnabled }}checked{{ end }}> {{ t "form.integration.readeck_push_activate" }}
+            </label>
+
             <label>
                 <input type="checkbox" name="readeck_only_url" value="1" {{ if .form.ReadeckOnlyURL }}checked{{ end }}> {{ t "form.integration.readeck_only_url" }}
             </label>

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

@@ -81,6 +81,7 @@ type IntegrationForm struct {
 	AppriseURL                       string
 	AppriseServicesURL               string
 	ReadeckEnabled                   bool
+	ReadeckPushEnabled               bool
 	ReadeckURL                       string
 	ReadeckAPIKey                    string
 	ReadeckLabels                    string
@@ -203,6 +204,7 @@ func (i IntegrationForm) Merge(integration *model.Integration) {
 	integration.AppriseServicesURL = i.AppriseServicesURL
 	integration.AppriseURL = i.AppriseURL
 	integration.ReadeckEnabled = i.ReadeckEnabled
+	integration.ReadeckPushEnabled = i.ReadeckPushEnabled
 	integration.ReadeckURL = i.ReadeckURL
 	integration.ReadeckAPIKey = i.ReadeckAPIKey
 	integration.ReadeckLabels = i.ReadeckLabels
@@ -327,6 +329,7 @@ func NewIntegrationForm(r *http.Request) *IntegrationForm {
 		AppriseURL:                       r.FormValue("apprise_url"),
 		AppriseServicesURL:               r.FormValue("apprise_services_url"),
 		ReadeckEnabled:                   r.FormValue("readeck_enabled") == "1",
+		ReadeckPushEnabled:               r.FormValue("readeck_push_enabled") == "1",
 		ReadeckURL:                       r.FormValue("readeck_url"),
 		ReadeckAPIKey:                    r.FormValue("readeck_api_key"),
 		ReadeckLabels:                    r.FormValue("readeck_labels"),

+ 1 - 0
internal/ui/integration_show.go

@@ -94,6 +94,7 @@ func (h *handler) showIntegrationPage(w http.ResponseWriter, r *http.Request) {
 		AppriseURL:                       integration.AppriseURL,
 		AppriseServicesURL:               integration.AppriseServicesURL,
 		ReadeckEnabled:                   integration.ReadeckEnabled,
+		ReadeckPushEnabled:               integration.ReadeckPushEnabled,
 		ReadeckURL:                       integration.ReadeckURL,
 		ReadeckAPIKey:                    integration.ReadeckAPIKey,
 		ReadeckLabels:                    integration.ReadeckLabels,