Bladeren bron

feat(integration): add support for Wallabag tags

Kevin Sicong Jiang 7 maanden geleden
bovenliggende
commit
8129500296

+ 7 - 0
internal/database/migrations.go

@@ -1151,4 +1151,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 wallabag_tags text default '';
+		`
+		_, err = tx.Exec(sql)
+		return err
+	},
 }

+ 3 - 0
internal/integration/integration.go

@@ -108,6 +108,7 @@ func SendEntry(entry *model.Entry, userIntegrations *model.Integration) {
 	if userIntegrations.WallabagEnabled {
 		slog.Debug("Sending entry to Wallabag",
 			slog.Int64("user_id", userIntegrations.UserID),
+			slog.String("user_tags", userIntegrations.WallabagTags),
 			slog.Int64("entry_id", entry.ID),
 			slog.String("entry_url", entry.URL),
 		)
@@ -118,12 +119,14 @@ func SendEntry(entry *model.Entry, userIntegrations *model.Integration) {
 			userIntegrations.WallabagClientSecret,
 			userIntegrations.WallabagUsername,
 			userIntegrations.WallabagPassword,
+			userIntegrations.WallabagTags,
 			userIntegrations.WallabagOnlyURL,
 		)
 
 		if err := client.CreateEntry(entry.URL, entry.Title, entry.Content); err != nil {
 			slog.Error("Unable to send entry to Wallabag",
 				slog.Int64("user_id", userIntegrations.UserID),
+				slog.String("user_tags", userIntegrations.WallabagTags),
 				slog.Int64("entry_id", entry.ID),
 				slog.String("entry_url", entry.URL),
 				slog.Any("error", err),

+ 8 - 5
internal/integration/wallabag/wallabag.go

@@ -24,11 +24,12 @@ type Client struct {
 	clientSecret string
 	username     string
 	password     string
+	tags         string
 	onlyURL      bool
 }
 
-func NewClient(baseURL, clientID, clientSecret, username, password string, onlyURL bool) *Client {
-	return &Client{baseURL, clientID, clientSecret, username, password, onlyURL}
+func NewClient(baseURL, clientID, clientSecret, username, password, tags string, onlyURL bool) *Client {
+	return &Client{baseURL, clientID, clientSecret, username, password, tags, onlyURL}
 }
 
 func (c *Client) CreateEntry(entryURL, entryTitle, entryContent string) error {
@@ -41,10 +42,10 @@ func (c *Client) CreateEntry(entryURL, entryTitle, entryContent string) error {
 		return err
 	}
 
-	return c.createEntry(accessToken, entryURL, entryTitle, entryContent)
+	return c.createEntry(accessToken, entryURL, entryTitle, entryContent, c.tags)
 }
 
-func (c *Client) createEntry(accessToken, entryURL, entryTitle, entryContent string) error {
+func (c *Client) createEntry(accessToken, entryURL, entryTitle, entryContent, tags string) error {
 	apiEndpoint, err := urllib.JoinBaseURLAndPath(c.baseURL, "/api/entries.json")
 	if err != nil {
 		return fmt.Errorf("wallbag: unable to generate entries endpoint: %v", err)
@@ -58,6 +59,7 @@ func (c *Client) createEntry(accessToken, entryURL, entryTitle, entryContent str
 		URL:     entryURL,
 		Title:   entryTitle,
 		Content: entryContent,
+		Tags:    tags,
 	})
 	if err != nil {
 		return fmt.Errorf("wallbag: unable to encode request body: %v", err)
@@ -81,7 +83,7 @@ func (c *Client) createEntry(accessToken, entryURL, entryTitle, entryContent str
 	defer response.Body.Close()
 
 	if response.StatusCode >= 400 {
-		return fmt.Errorf("wallabag: unable to get access token: url=%s status=%d", apiEndpoint, response.StatusCode)
+		return fmt.Errorf("wallabag: unable to get save entry: url=%s status=%d", apiEndpoint, response.StatusCode)
 	}
 
 	return nil
@@ -140,4 +142,5 @@ type createEntryRequest struct {
 	URL     string `json:"url"`
 	Title   string `json:"title"`
 	Content string `json:"content,omitempty"`
+	Tags    string `json:"tags,omitempty"`
 }

+ 321 - 0
internal/integration/wallabag/wallabag_test.go

@@ -0,0 +1,321 @@
+// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package wallabag
+
+import (
+	"encoding/json"
+	"io"
+	"net/http"
+	"net/http/httptest"
+	"strings"
+	"testing"
+)
+
+func TestCreateEntry(t *testing.T) {
+	entryURL := "https://example.com"
+	entryTitle := "title"
+	entryContent := "content"
+	tags := "tag1,tag2,tag3"
+
+	tests := []struct {
+		name           string
+		username       string
+		password       string
+		clientID       string
+		clientSecret   string
+		tags           string
+		onlyURL        bool
+		entryURL       string
+		entryTitle     string
+		entryContent   string
+		serverResponse func(w http.ResponseWriter, r *http.Request)
+		wantErr        bool
+		errContains    string
+	}{
+		{
+			name:         "successful entry creation with url only",
+			wantErr:      false,
+			onlyURL:      true,
+			username:     "username",
+			password:     "password",
+			clientID:     "clientId",
+			clientSecret: "clientSecret",
+			tags:         tags,
+			entryURL:     entryURL,
+			entryTitle:   entryTitle,
+			entryContent: entryContent,
+			serverResponse: func(w http.ResponseWriter, r *http.Request) {
+				if strings.Contains(r.URL.Path, "/oauth/v2/token") {
+					// Return success response
+					w.WriteHeader(http.StatusOK)
+					json.NewEncoder(w).Encode(map[string]any{
+						"access_token":  "test-token",
+						"expires_in":    3600,
+						"refresh_token": "token",
+						"scope":         "scope",
+						"token_type":    "token_type",
+					})
+					return
+				}
+				// Verify authorization header
+				auth := r.Header.Get("Authorization")
+				if auth != "Bearer test-token" {
+					t.Errorf("Expected Authorization header 'Bearer test-token', got %s", auth)
+				}
+				// Verify content type
+				contentType := r.Header.Get("Content-Type")
+				if contentType != "application/json" {
+					t.Errorf("Expected Content-Type 'application/json', got %s", contentType)
+				}
+				// Parse and verify request
+				body, _ := io.ReadAll(r.Body)
+				var req map[string]any
+				if err := json.Unmarshal(body, &req); err != nil {
+					t.Errorf("Failed to parse request body: %v", err)
+				}
+				if requstEntryURL := req["url"]; requstEntryURL != entryURL {
+					t.Errorf("Expected entryURL %s, got %s", entryURL, requstEntryURL)
+				}
+				if requestEntryTitle := req["title"]; requestEntryTitle != entryTitle {
+					t.Errorf("Expected entryTitle %s, got %s", entryTitle, requestEntryTitle)
+				}
+				if _, ok := req["content"]; ok {
+					t.Errorf("Expected entryContent to be empty, got value")
+				}
+				if requestTags := req["tags"]; requestTags != tags {
+					t.Errorf("Expected tags %s, got %s", tags, requestTags)
+				} // Return success response
+				w.WriteHeader(http.StatusOK)
+			},
+			errContains: "",
+		},
+		{
+			name:         "successful entry creation with content",
+			wantErr:      false,
+			onlyURL:      false,
+			username:     "username",
+			password:     "password",
+			clientID:     "clientId",
+			clientSecret: "clientSecret",
+			tags:         tags,
+			entryURL:     entryURL,
+			entryTitle:   entryTitle,
+			entryContent: entryContent,
+			serverResponse: func(w http.ResponseWriter, r *http.Request) {
+				if strings.Contains(r.URL.Path, "/oauth/v2/token") {
+					// Return success response
+					w.WriteHeader(http.StatusOK)
+					json.NewEncoder(w).Encode(map[string]any{
+						"access_token":  "test-token",
+						"expires_in":    3600,
+						"refresh_token": "token",
+						"scope":         "scope",
+						"token_type":    "token_type",
+					})
+					return
+				}
+				// Verify authorization header
+				auth := r.Header.Get("Authorization")
+				if auth != "Bearer test-token" {
+					t.Errorf("Expected Authorization header 'Bearer test-token', got %s", auth)
+				}
+				// Verify content type
+				contentType := r.Header.Get("Content-Type")
+				if contentType != "application/json" {
+					t.Errorf("Expected Content-Type 'application/json', got %s", contentType)
+				}
+				// Parse and verify request
+				body, _ := io.ReadAll(r.Body)
+				var req map[string]any
+				if err := json.Unmarshal(body, &req); err != nil {
+					t.Errorf("Failed to parse request body: %v", err)
+				}
+				if requstEntryURL := req["url"]; requstEntryURL != entryURL {
+					t.Errorf("Expected entryURL %s, got %s", entryURL, requstEntryURL)
+				}
+				if requestEntryTitle := req["title"]; requestEntryTitle != entryTitle {
+					t.Errorf("Expected entryTitle %s, got %s", entryTitle, requestEntryTitle)
+				}
+				if requestEntryContent := req["content"]; requestEntryContent != entryContent {
+					t.Errorf("Expected entryContent %s, got %s", entryContent, requestEntryContent)
+				}
+				if requestTags := req["tags"]; requestTags != tags {
+					t.Errorf("Expected tags %s, got %s", tags, requestTags)
+				} // Return success response
+				w.WriteHeader(http.StatusOK)
+			},
+			errContains: "",
+		},
+		{
+			name:         "failed when unable to decode accessToken response",
+			wantErr:      true,
+			onlyURL:      true,
+			username:     "username",
+			password:     "password",
+			clientID:     "clientId",
+			clientSecret: "clientSecret",
+			tags:         tags,
+			entryURL:     entryURL,
+			entryTitle:   entryTitle,
+			entryContent: entryContent,
+			serverResponse: func(w http.ResponseWriter, r *http.Request) {
+				if strings.Contains(r.URL.Path, "/oauth/v2/token") {
+					// Return success response
+					w.WriteHeader(http.StatusOK)
+					w.Write([]byte("invalid json"))
+					return
+				}
+				t.Error("Server should not be called when failed to get accessToken")
+			},
+			errContains: "unable to decode token response",
+		},
+		{
+			name:         "failed when saving entry",
+			wantErr:      true,
+			onlyURL:      true,
+			username:     "username",
+			password:     "password",
+			clientID:     "clientId",
+			clientSecret: "clientSecret",
+			tags:         tags,
+			entryURL:     entryURL,
+			entryTitle:   entryTitle,
+			entryContent: entryContent,
+			serverResponse: func(w http.ResponseWriter, r *http.Request) {
+				if strings.Contains(r.URL.Path, "/oauth/v2/token") {
+					// Return success response
+					w.WriteHeader(http.StatusOK)
+					json.NewEncoder(w).Encode(map[string]any{
+						"access_token":  "test-token",
+						"expires_in":    3600,
+						"refresh_token": "token",
+						"scope":         "scope",
+						"token_type":    "token_type",
+					})
+					return
+				}
+				w.WriteHeader(http.StatusUnauthorized)
+			},
+			errContains: "unable to get save entry",
+		},
+		{
+			name:         "failure due to no accessToken",
+			wantErr:      true,
+			onlyURL:      false,
+			username:     "username",
+			password:     "password",
+			clientID:     "clientId",
+			clientSecret: "clientSecret",
+			tags:         tags,
+			entryURL:     entryURL,
+			entryTitle:   entryTitle,
+			entryContent: entryContent,
+			serverResponse: func(w http.ResponseWriter, r *http.Request) {
+				if strings.Contains(r.URL.Path, "/oauth/v2/token") {
+					// Return error response
+					w.WriteHeader(http.StatusUnauthorized)
+					return
+				}
+				t.Error("Server should not be called when failed to get accessToken")
+			},
+			errContains: "unable to get access token",
+		},
+		{
+			name:         "failure due to missing client parameters",
+			wantErr:      true,
+			onlyURL:      false,
+			tags:         tags,
+			entryURL:     entryURL,
+			entryTitle:   entryTitle,
+			entryContent: entryContent,
+			serverResponse: func(w http.ResponseWriter, r *http.Request) {
+				t.Error("Server should not be called when failed to get accessToken")
+			},
+			errContains: "wallabag: missing base URL, client ID, client secret, username or password",
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			// Create test server if we have a server response function
+			var serverURL string
+			if tt.serverResponse != nil {
+				server := httptest.NewServer(http.HandlerFunc(tt.serverResponse))
+				defer server.Close()
+				serverURL = server.URL
+			}
+
+			// Create client with test server URL
+			client := NewClient(serverURL, tt.clientID, tt.clientSecret, tt.username, tt.password, tt.tags, tt.onlyURL)
+
+			// Call CreateBookmark
+			err := client.CreateEntry(tt.entryURL, tt.entryTitle, tt.entryContent)
+
+			// Check error expectations
+			if tt.wantErr {
+				if err == nil {
+					t.Errorf("Expected error but got none")
+				} else if tt.errContains != "" && !strings.Contains(err.Error(), tt.errContains) {
+					t.Errorf("Expected error containing '%s', got '%s'", tt.errContains, err.Error())
+				}
+			} else {
+				if err != nil {
+					t.Errorf("Unexpected error: %v", err)
+				}
+			}
+		})
+	}
+}
+
+func TestNewClient(t *testing.T) {
+	tests := []struct {
+		name         string
+		baseURL      string
+		clientID     string
+		clientSecret string
+		username     string
+		password     string
+		tags         string
+		onlyURL      bool
+	}{
+		{
+			name:         "with all parameters",
+			baseURL:      "https://wallabag.example.com",
+			clientID:     "clientID",
+			clientSecret: "clientSecret",
+			username:     "wallabag",
+			password:     "wallabag",
+			tags:         "",
+			onlyURL:      true,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			client := NewClient(tt.baseURL, tt.clientID, tt.clientSecret, tt.username, tt.password, tt.tags, tt.onlyURL)
+
+			if client.baseURL != tt.baseURL {
+				t.Errorf("Expected.baseURL %s, got %s", tt.baseURL, client.baseURL)
+			}
+			if client.username != tt.username {
+				t.Errorf("Expected username %s, got %s", tt.username, client.username)
+			}
+			if client.password != tt.password {
+				t.Errorf("Expected password %s, got %s", tt.password, client.password)
+			}
+			if client.clientID != tt.clientID {
+				t.Errorf("Expected clientID %s, got %s", tt.clientID, client.clientID)
+			}
+			if client.clientSecret != tt.clientSecret {
+				t.Errorf("Expected clientSecret %s, got %s", tt.clientSecret, client.clientSecret)
+			}
+			if client.tags != tt.tags {
+				t.Errorf("Expected tags %s, got %s", tt.tags, client.tags)
+			}
+			if client.onlyURL != tt.onlyURL {
+				t.Errorf("Expected onlyURL %v, got %v", tt.onlyURL, client.onlyURL)
+			}
+		})
+	}
+}

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

@@ -333,6 +333,7 @@
     "form.integration.wallabag_only_url": "Nur URL senden (anstelle des vollständigen Inhalts)",
     "form.integration.wallabag_password": "Wallabag-Passwort",
     "form.integration.wallabag_username": "Wallabag-Benutzername",
+    "form.integration.wallabag_tags": "Wallabag Tags",
     "form.integration.webhook_activate": "Webhooks aktivieren",
     "form.integration.webhook_secret": "Webhook-Geheimnis",
     "form.integration.webhook_url": "Standard-Webhook-URL",

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

@@ -333,6 +333,7 @@
     "form.integration.wallabag_only_url": "Αποστολή μόνο URL (αντί για πλήρες περιεχόμενο)",
     "form.integration.wallabag_password": "Wallabag Κωδικός Πρόσβασης",
     "form.integration.wallabag_username": "Όνομα Χρήστη Wallabag",
+    "form.integration.wallabag_tags": "Wallabag Tags",
     "form.integration.webhook_activate": "Ενεργοποίηση Webhooks",
     "form.integration.webhook_secret": "Μυστικό Webhooks",
     "form.integration.webhook_url": "Προεπιλεγμένη διεύθυνση URL Webhook",

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

@@ -333,6 +333,7 @@
     "form.integration.wallabag_only_url": "Send only URL (instead of full content)",
     "form.integration.wallabag_password": "Wallabag Password",
     "form.integration.wallabag_username": "Wallabag Username",
+    "form.integration.wallabag_tags": "Wallabag Tags",
     "form.integration.webhook_activate": "Enable Webhooks",
     "form.integration.webhook_secret": "Webhooks Secret",
     "form.integration.webhook_url": "Default Webhook URL",

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

@@ -333,6 +333,7 @@
     "form.integration.wallabag_only_url": "Enviar solo URL (en lugar de contenido completo)",
     "form.integration.wallabag_password": "Contraseña de Wallabag",
     "form.integration.wallabag_username": "Nombre de usuario de Wallabag",
+    "form.integration.wallabag_tags": "Wallabag Tags",
     "form.integration.webhook_activate": "Habilitar Webhooks",
     "form.integration.webhook_secret": "Secreto de Webhooks",
     "form.integration.webhook_url": "Defecto URL de Webhook",

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

@@ -333,6 +333,7 @@
     "form.integration.wallabag_only_url": "Lähetä vain URL-osoite (koko sisällön sijaan)",
     "form.integration.wallabag_password": "Wallabag-salasana",
     "form.integration.wallabag_username": "Wallabag-käyttäjätunnus",
+    "form.integration.wallabag_tags": "Wallabag Tags",
     "form.integration.webhook_activate": "Enable Webhooks",
     "form.integration.webhook_secret": "Webhooks Secret",
     "form.integration.webhook_url": "Default Webhook URL",

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

@@ -333,6 +333,7 @@
     "form.integration.wallabag_only_url": "Envoyer uniquement l'URL (au lieu du contenu complet)",
     "form.integration.wallabag_password": "Mot de passe de Wallabag",
     "form.integration.wallabag_username": "Nom d'utilisateur de Wallabag",
+    "form.integration.wallabag_tags": "Wallabag Tags",
     "form.integration.webhook_activate": "Activer le webhook",
     "form.integration.webhook_secret": "Secret du webhook",
     "form.integration.webhook_url": "URL du webhook",

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

@@ -333,6 +333,7 @@
     "form.integration.wallabag_only_url": "केवल URL भेजें (पूर्ण सामग्री के बजाय)",
     "form.integration.wallabag_password": "वालाबैग पासवर्ड",
     "form.integration.wallabag_username": "वालाबैग उपयोगकर्ता नाम",
+    "form.integration.wallabag_tags": "Wallabag Tags",
     "form.integration.webhook_activate": "Enable Webhooks",
     "form.integration.webhook_secret": "Webhooks Secret",
     "form.integration.webhook_url": "Default Webhook URL",

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

@@ -330,6 +330,7 @@
     "form.integration.wallabag_only_url": "Kirim hanya URL (alih-alih konten penuh)",
     "form.integration.wallabag_password": "Kata Sandi Wallabag",
     "form.integration.wallabag_username": "Nama Pengguna Wallabag",
+    "form.integration.wallabag_tags": "Wallabag Tags",
     "form.integration.webhook_activate": "Aktifkan Webhook",
     "form.integration.webhook_secret": "Rahasia Webhook",
     "form.integration.webhook_url": "URL Webhook baku",

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

@@ -333,6 +333,7 @@
     "form.integration.wallabag_only_url": "Invia solo URL (invece del contenuto completo)",
     "form.integration.wallabag_password": "Password dell'account Wallabag",
     "form.integration.wallabag_username": "Nome utente dell'account Wallabag",
+    "form.integration.wallabag_tags": "Wallabag Tags",
     "form.integration.webhook_activate": "Enable Webhooks",
     "form.integration.webhook_secret": "Webhooks Secret",
     "form.integration.webhook_url": "Default Webhook URL",

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

@@ -330,6 +330,7 @@
     "form.integration.wallabag_only_url": "URL のみを送信 (完全なコンテンツではなく)",
     "form.integration.wallabag_password": "Wallabag のパスワード",
     "form.integration.wallabag_username": "Wallabag のユーザー名",
+    "form.integration.wallabag_tags": "Wallabag Tags",
     "form.integration.webhook_activate": "Enable Webhooks",
     "form.integration.webhook_secret": "Webhooks Secret",
     "form.integration.webhook_url": "Default Webhook URL",

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

@@ -330,6 +330,7 @@
     "form.integration.wallabag_only_url": "Kan-na thoân bāng-chí (m̄ sī oân-chéng ê lōe-iông)",
     "form.integration.wallabag_password": "Wallabag bi̍t-bé",
     "form.integration.wallabag_username": "Wallabag kháu-chō miâ",
+    "form.integration.wallabag_tags": "Wallabag Tags",
     "form.integration.webhook_activate": "Khai-sí iōng Webhooks",
     "form.integration.webhook_secret": "Webhooks Secret",
     "form.integration.webhook_url": "Default Webhook bāng-chí",

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

@@ -333,6 +333,7 @@
     "form.integration.wallabag_only_url": "Alleen URL verzenden (in plaats van volledige inhoud)",
     "form.integration.wallabag_password": "Wallabag wachtwoord",
     "form.integration.wallabag_username": "Wallabag gebruikersnaam",
+    "form.integration.wallabag_tags": "Wallabag Tags",
     "form.integration.webhook_activate": "Webhooks activeren",
     "form.integration.webhook_secret": "Webhooks Secret",
     "form.integration.webhook_url": "Standard Webhook URL",

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

@@ -333,6 +333,7 @@
     "form.integration.wallabag_client_id": "Identyfikator klienta Wallabag",
     "form.integration.wallabag_client_secret": "Tajny klucz klienta Wallabag",
     "form.integration.wallabag_endpoint": "Podstawowy adres URL Wallabag",
+    "form.integration.wallabag_tags": "Wallabag Tags",
     "form.integration.wallabag_only_url": "Przesyłaj tylko adres URL (zamiast pełnej treści)",
     "form.integration.wallabag_password": "Hasło do Wallabag",
     "form.integration.wallabag_username": "Login do Wallabag",

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

@@ -333,6 +333,7 @@
     "form.integration.wallabag_only_url": "Enviar apenas URL (em vez de conteúdo completo)",
     "form.integration.wallabag_password": "Senha do Wallabag",
     "form.integration.wallabag_username": "Nome de usuário do Wallabag",
+    "form.integration.wallabag_tags": "Wallabag Tags",
     "form.integration.webhook_activate": "Enable Webhooks",
     "form.integration.webhook_secret": "Webhooks Secret",
     "form.integration.webhook_url": "Default Webhook URL",

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

@@ -333,6 +333,7 @@
     "form.integration.wallabag_client_id": "ID Client Wallabag",
     "form.integration.wallabag_client_secret": "Secret Client Wallabag",
     "form.integration.wallabag_endpoint": "URL Wallabag",
+    "form.integration.wallabag_tags": "Wallabag Tags",
     "form.integration.wallabag_only_url": "Trimite numai URL-ul (fără conținut complet)",
     "form.integration.wallabag_password": "Parolă Wallabag",
     "form.integration.wallabag_username": "Utilizator Wallabag",

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

@@ -333,6 +333,7 @@
     "form.integration.wallabag_client_id": "Номер клиента Wallabag",
     "form.integration.wallabag_client_secret": "Секретный код клиента Wallabag",
     "form.integration.wallabag_endpoint": "URL-адрес базы Валлабаг",
+    "form.integration.wallabag_tags": "Wallabag Tags",
     "form.integration.wallabag_only_url": "Отправлять только ссылку (без содержимого)",
     "form.integration.wallabag_password": "Пароль Wallabag",
     "form.integration.wallabag_username": "Имя пользователя Wallabag",

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

@@ -333,6 +333,7 @@
     "form.integration.wallabag_only_url": "Yalnızca URL gönder (tam makale yerine)",
     "form.integration.wallabag_password": "Wallabag Parolası",
     "form.integration.wallabag_username": "Wallabag Kullanıcı Adı",
+    "form.integration.wallabag_tags": "Wallabag Tags",
     "form.integration.webhook_activate": "Webhook'u etkinleştir",
     "form.integration.webhook_secret": "Webhooks Secret",
     "form.integration.webhook_url": "Default Webhook URL",

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

@@ -333,6 +333,7 @@
     "form.integration.wallabag_client_id": "Wallabag Client ID",
     "form.integration.wallabag_client_secret": "Wallabag Client Secret",
     "form.integration.wallabag_endpoint": "Базова URL-адреса Wallabag",
+    "form.integration.wallabag_tags": "Wallabag Tags",
     "form.integration.wallabag_only_url": "Надіслати лише URL (замість повного вмісту)",
     "form.integration.wallabag_password": "Пароль Wallabag",
     "form.integration.wallabag_username": "Ім’я користувача Wallabag",

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

@@ -330,6 +330,7 @@
     "form.integration.wallabag_only_url": "仅发送 URL(而非完整内容)",
     "form.integration.wallabag_password": "Wallabag 密码",
     "form.integration.wallabag_username": "Wallabag 用户名",
+    "form.integration.wallabag_tags": "Wallabag Tags",
     "form.integration.webhook_activate": "启用 Webhooks",
     "form.integration.webhook_secret": "Webhooks 密钥",
     "form.integration.webhook_url": "默认 Webhook URL",

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

@@ -330,6 +330,7 @@
     "form.integration.wallabag_only_url": "僅傳送網址(而不是完整內容)",
     "form.integration.wallabag_password": "Wallabag 密碼",
     "form.integration.wallabag_username": "Wallabag 使用者名稱",
+    "form.integration.wallabag_tags": "Wallabag Tags",
     "form.integration.webhook_activate": "啟用 Webhooks",
     "form.integration.webhook_secret": "Webhooks Secret",
     "form.integration.webhook_url": "Default Webhook 網址",

+ 1 - 0
internal/model/integration.go

@@ -29,6 +29,7 @@ type Integration struct {
 	WallabagClientSecret             string
 	WallabagUsername                 string
 	WallabagPassword                 string
+	WallabagTags                     string
 	NunuxKeeperEnabled               bool
 	NunuxKeeperURL                   string
 	NunuxKeeperAPIKey                string

+ 104 - 100
internal/storage/integration.go

@@ -130,6 +130,7 @@ func (s *Storage) Integration(userID int64) (*model.Integration, error) {
 			wallabag_client_secret,
 			wallabag_username,
 			wallabag_password,
+			wallabag_tags,
 			notion_enabled,
 			notion_token,
 			notion_page_id,
@@ -254,6 +255,7 @@ func (s *Storage) Integration(userID int64) (*model.Integration, error) {
 		&integration.WallabagClientSecret,
 		&integration.WallabagUsername,
 		&integration.WallabagPassword,
+		&integration.WallabagTags,
 		&integration.NotionEnabled,
 		&integration.NotionToken,
 		&integration.NotionPageID,
@@ -384,107 +386,108 @@ func (s *Storage) UpdateIntegration(integration *model.Integration) error {
 			wallabag_client_secret=$15,
 			wallabag_username=$16,
 			wallabag_password=$17,
-			nunux_keeper_enabled=$18,
-			nunux_keeper_url=$19,
-			nunux_keeper_api_key=$20,
-			googlereader_enabled=$21,
-			googlereader_username=$22,
-			googlereader_password=$23,
-			telegram_bot_enabled=$24,
-			telegram_bot_token=$25,
-			telegram_bot_chat_id=$26,
-			telegram_bot_topic_id=$27,
-			telegram_bot_disable_web_page_preview=$28,
-			telegram_bot_disable_notification=$29,
-			telegram_bot_disable_buttons=$30,
-			espial_enabled=$31,
-			espial_url=$32,
-			espial_api_key=$33,
-			espial_tags=$34,
-			linkace_enabled=$35,
-			linkace_url=$36,
-			linkace_api_key=$37,
-			linkace_tags=$38,
-			linkace_is_private=$39,
-			linkace_check_disabled=$40,
-			linkding_enabled=$41,
-			linkding_url=$42,
-			linkding_api_key=$43,
-			linkding_tags=$44,
-			linkding_mark_as_unread=$45,
-			matrix_bot_enabled=$46,
-			matrix_bot_user=$47,
-			matrix_bot_password=$48,
-			matrix_bot_url=$49,
-			matrix_bot_chat_id=$50,
-			notion_enabled=$51,
-			notion_token=$52,
-			notion_page_id=$53,
-			readwise_enabled=$54,
-			readwise_api_key=$55,
-			apprise_enabled=$56,
-			apprise_url=$57,
-			apprise_services_url=$58,
-			readeck_enabled=$59,
-			readeck_url=$60,
-			readeck_api_key=$61,
-			readeck_labels=$62,
-			readeck_only_url=$63,
-			shiori_enabled=$64,
-			shiori_url=$65,
-			shiori_username=$66,
-			shiori_password=$67,
-			shaarli_enabled=$68,
-			shaarli_url=$69,
-			shaarli_api_secret=$70,
-			webhook_enabled=$71,
-			webhook_url=$72,
-			webhook_secret=$73,
-			rssbridge_enabled=$74,
-			rssbridge_url=$75,
-			omnivore_enabled=$76,
-			omnivore_api_key=$77,
-			omnivore_url=$78,
-			linkwarden_enabled=$79,
-			linkwarden_url=$80,
-			linkwarden_api_key=$81,
-			raindrop_enabled=$82,
-			raindrop_token=$83,
-			raindrop_collection_id=$84,
-			raindrop_tags=$85,
-			betula_enabled=$86,
-			betula_url=$87,
-			betula_token=$88,
-			ntfy_enabled=$89,
-			ntfy_topic=$90,
-			ntfy_url=$91,
-			ntfy_api_token=$92,
-			ntfy_username=$93,
-			ntfy_password=$94,
-			ntfy_icon_url=$95,
-			ntfy_internal_links=$96,
-			cubox_enabled=$97,
-			cubox_api_link=$98,
-			discord_enabled=$99,
-			discord_webhook_link=$100,
-			slack_enabled=$101,
-			slack_webhook_link=$102,
-			pushover_enabled=$103,
-			pushover_user=$104,
-			pushover_token=$105,
-			pushover_device=$106,
-			pushover_prefix=$107,
-			rssbridge_token=$108,
-			karakeep_enabled=$109,
-			karakeep_api_key=$110,
-			karakeep_url=$111,
-			linktaco_enabled=$112,
-			linktaco_api_token=$113,
-			linktaco_org_slug=$114,
-			linktaco_tags=$115,
-			linktaco_visibility=$116
+			wallabag_tags=$18,
+			nunux_keeper_enabled=$19,
+			nunux_keeper_url=$20,
+			nunux_keeper_api_key=$21,
+			googlereader_enabled=$22,
+			googlereader_username=$23,
+			googlereader_password=$24,
+			telegram_bot_enabled=$25,
+			telegram_bot_token=$26,
+			telegram_bot_chat_id=$27,
+			telegram_bot_topic_id=$28,
+			telegram_bot_disable_web_page_preview=$29,
+			telegram_bot_disable_notification=$30,
+			telegram_bot_disable_buttons=$31,
+			espial_enabled=$32,
+			espial_url=$33,
+			espial_api_key=$34,
+			espial_tags=$35,
+			linkace_enabled=$36,
+			linkace_url=$37,
+			linkace_api_key=$38,
+			linkace_tags=$39,
+			linkace_is_private=$40,
+			linkace_check_disabled=$41,
+			linkding_enabled=$42,
+			linkding_url=$43,
+			linkding_api_key=$44,
+			linkding_tags=$45,
+			linkding_mark_as_unread=$46,
+			matrix_bot_enabled=$47,
+			matrix_bot_user=$48,
+			matrix_bot_password=$49,
+			matrix_bot_url=$50,
+			matrix_bot_chat_id=$51,
+			notion_enabled=$52,
+			notion_token=$53,
+			notion_page_id=$54,
+			readwise_enabled=$55,
+			readwise_api_key=$56,
+			apprise_enabled=$57,
+			apprise_url=$58,
+			apprise_services_url=$59,
+			readeck_enabled=$60,
+			readeck_url=$61,
+			readeck_api_key=$62,
+			readeck_labels=$63,
+			readeck_only_url=$64,
+			shiori_enabled=$65,
+			shiori_url=$66,
+			shiori_username=$67,
+			shiori_password=$68,
+			shaarli_enabled=$69,
+			shaarli_url=$70,
+			shaarli_api_secret=$71,
+			webhook_enabled=$72,
+			webhook_url=$73,
+			webhook_secret=$74,
+			rssbridge_enabled=$75,
+			rssbridge_url=$76,
+			omnivore_enabled=$77,
+			omnivore_api_key=$78,
+			omnivore_url=$79,
+			linkwarden_enabled=$80,
+			linkwarden_url=$81,
+			linkwarden_api_key=$82,
+			raindrop_enabled=$83,
+			raindrop_token=$84,
+			raindrop_collection_id=$85,
+			raindrop_tags=$86,
+			betula_enabled=$87,
+			betula_url=$88,
+			betula_token=$89,
+			ntfy_enabled=$90,
+			ntfy_topic=$91,
+			ntfy_url=$92,
+			ntfy_api_token=$93,
+			ntfy_username=$94,
+			ntfy_password=$95,
+			ntfy_icon_url=$96,
+			ntfy_internal_links=$97,
+			cubox_enabled=$98,
+			cubox_api_link=$99,
+			discord_enabled=$100,
+			discord_webhook_link=$101,
+			slack_enabled=$102,
+			slack_webhook_link=$103,
+			pushover_enabled=$104,
+			pushover_user=$105,
+			pushover_token=$106,
+			pushover_device=$107,
+			pushover_prefix=$108,
+			rssbridge_token=$109,
+			karakeep_enabled=$110,
+			karakeep_api_key=$111,
+			karakeep_url=$112,
+			linktaco_enabled=$113,
+			linktaco_api_token=$114,
+			linktaco_org_slug=$115,
+			linktaco_tags=$116,
+			linktaco_visibility=$117
 		WHERE
-			user_id=$117
+			user_id=$118
 	`
 	_, err := s.db.Exec(
 		query,
@@ -505,6 +508,7 @@ func (s *Storage) UpdateIntegration(integration *model.Integration) error {
 		integration.WallabagClientSecret,
 		integration.WallabagUsername,
 		integration.WallabagPassword,
+		integration.WallabagTags,
 		integration.NunuxKeeperEnabled,
 		integration.NunuxKeeperURL,
 		integration.NunuxKeeperAPIKey,

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

@@ -664,6 +664,9 @@
             <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">
 
+            <label for="form-wallabag-tags">{{ t "form.integration.wallabag_tags" }}</label>
+            <input type="text" name="wallabag_tags" id="form-wallabag-tags" value="{{ .form.WallabagTags }}" spellcheck="false">
+
             <div class="buttons">
                 <button type="submit" class="button button-primary" data-label-loading="{{ t "form.submit.saving" }}">{{ t "action.update" }}</button>
             </div>

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

@@ -32,6 +32,7 @@ type IntegrationForm struct {
 	WallabagClientSecret             string
 	WallabagUsername                 string
 	WallabagPassword                 string
+	WallabagTags                     string
 	NotionEnabled                    bool
 	NotionPageID                     string
 	NotionToken                      string
@@ -150,6 +151,7 @@ func (i IntegrationForm) Merge(integration *model.Integration) {
 	integration.WallabagClientSecret = i.WallabagClientSecret
 	integration.WallabagUsername = i.WallabagUsername
 	integration.WallabagPassword = i.WallabagPassword
+	integration.WallabagTags = i.WallabagTags
 	integration.NotionEnabled = i.NotionEnabled
 	integration.NotionPageID = i.NotionPageID
 	integration.NotionToken = i.NotionToken
@@ -270,6 +272,7 @@ func NewIntegrationForm(r *http.Request) *IntegrationForm {
 		WallabagClientSecret:             r.FormValue("wallabag_client_secret"),
 		WallabagUsername:                 r.FormValue("wallabag_username"),
 		WallabagPassword:                 r.FormValue("wallabag_password"),
+		WallabagTags:                     r.FormValue("wallabag_tags"),
 		NotionEnabled:                    r.FormValue("notion_enabled") == "1",
 		NotionPageID:                     r.FormValue("notion_page_id"),
 		NotionToken:                      r.FormValue("notion_token"),

+ 1 - 0
internal/ui/integration_show.go

@@ -45,6 +45,7 @@ func (h *handler) showIntegrationPage(w http.ResponseWriter, r *http.Request) {
 		WallabagClientSecret:             integration.WallabagClientSecret,
 		WallabagUsername:                 integration.WallabagUsername,
 		WallabagPassword:                 integration.WallabagPassword,
+		WallabagTags:                     integration.WallabagTags,
 		NotionEnabled:                    integration.NotionEnabled,
 		NotionPageID:                     integration.NotionPageID,
 		NotionToken:                      integration.NotionToken,