From 4d656d27393a5118b895bad143416157b458c8c6 Mon Sep 17 00:00:00 2001 From: Peter Sanchez Date: Wed, 20 Aug 2025 21:35:33 -0600 Subject: [PATCH] feat(integration): add LinkTaco service for saving articles --- README.md | 2 +- internal/database/migrations.go | 16 + internal/integration/integration.go | 24 + internal/integration/linktaco/linktaco.go | 146 ++++++ .../integration/linktaco/linktaco_test.go | 440 ++++++++++++++++++ internal/locale/translations/de_DE.json | 11 + internal/locale/translations/el_EL.json | 11 + internal/locale/translations/en_US.json | 11 + internal/locale/translations/es_ES.json | 11 + internal/locale/translations/fi_FI.json | 11 + internal/locale/translations/fr_FR.json | 11 + internal/locale/translations/hi_IN.json | 11 + internal/locale/translations/id_ID.json | 11 + internal/locale/translations/it_IT.json | 11 + internal/locale/translations/ja_JP.json | 11 + .../locale/translations/nan_Latn_pehoeji.json | 11 + internal/locale/translations/nl_NL.json | 11 + internal/locale/translations/pl_PL.json | 11 + internal/locale/translations/pt_BR.json | 11 + internal/locale/translations/ro_RO.json | 11 + internal/locale/translations/ru_RU.json | 11 + internal/locale/translations/tr_TR.json | 11 + internal/locale/translations/uk_UA.json | 13 +- internal/locale/translations/zh_CN.json | 11 + internal/locale/translations/zh_TW.json | 11 + internal/model/integration.go | 5 + internal/storage/integration.go | 27 +- .../templates/views/integrations.html | 31 ++ internal/ui/form/integration.go | 15 + internal/ui/integration_show.go | 5 + internal/ui/integration_update.go | 11 + 31 files changed, 939 insertions(+), 5 deletions(-) create mode 100644 internal/integration/linktaco/linktaco.go create mode 100644 internal/integration/linktaco/linktaco_test.go diff --git a/README.md b/README.md index 4c6b987c..b70ee929 100644 --- a/README.md +++ b/README.md @@ -72,7 +72,7 @@ Features ### Integrations -- 25+ integrations with third-party services: [Apprise](https://github.com/caronc/apprise), [Betula](https://sr.ht/~bouncepaw/betula/), [Cubox](https://cubox.cc/), [Discord](https://discord.com/), [Espial](https://github.com/jonschoning/espial), [Instapaper](https://www.instapaper.com/), [LinkAce](https://www.linkace.org/), [Linkding](https://github.com/sissbruecker/linkding), [LinkWarden](https://linkwarden.app/), [Matrix](https://matrix.org), [Notion](https://www.notion.com/), [Ntfy](https://ntfy.sh/), [Nunux Keeper](https://keeper.nunux.org/), [Pinboard](https://pinboard.in/), [Pushover](https://pushover.net), [RainDrop](https://raindrop.io/), [Readeck](https://readeck.org/en/), [Readwise Reader](https://readwise.io/read), [RssBridge](https://rss-bridge.org/), [Shaarli](https://github.com/shaarli/Shaarli), [Shiori](https://github.com/go-shiori/shiori), [Slack](https://slack.com/), [Telegram](https://telegram.org), [Wallabag](https://www.wallabag.org/), etc. +- 25+ integrations with third-party services: [Apprise](https://github.com/caronc/apprise), [Betula](https://sr.ht/~bouncepaw/betula/), [Cubox](https://cubox.cc/), [Discord](https://discord.com/), [Espial](https://github.com/jonschoning/espial), [Instapaper](https://www.instapaper.com/), [LinkAce](https://www.linkace.org/), [Linkding](https://github.com/sissbruecker/linkding), [LinkTaco](https://linktaco.com), [LinkWarden](https://linkwarden.app/), [Matrix](https://matrix.org), [Notion](https://www.notion.com/), [Ntfy](https://ntfy.sh/), [Nunux Keeper](https://keeper.nunux.org/), [Pinboard](https://pinboard.in/), [Pushover](https://pushover.net), [RainDrop](https://raindrop.io/), [Readeck](https://readeck.org/en/), [Readwise Reader](https://readwise.io/read), [RssBridge](https://rss-bridge.org/), [Shaarli](https://github.com/shaarli/Shaarli), [Shiori](https://github.com/go-shiori/shiori), [Slack](https://slack.com/), [Telegram](https://telegram.org), [Wallabag](https://www.wallabag.org/), etc. - Bookmarklet for subscribing to websites directly from any web browser. - Webhooks for real-time notifications or custom integrations. - Compatibility with existing mobile applications using the Fever or Google Reader API. diff --git a/internal/database/migrations.go b/internal/database/migrations.go index 51e52d80..44911e28 100644 --- a/internal/database/migrations.go +++ b/internal/database/migrations.go @@ -1135,4 +1135,20 @@ var migrations = []func(tx *sql.Tx) error{ _, err = tx.Exec(sql) return err }, + func(tx *sql.Tx) (err error) { + sql := ` + CREATE TYPE linktaco_link_visibility AS ENUM ( + 'PUBLIC', + 'PRIVATE' + ); + ALTER TABLE integrations + ADD COLUMN linktaco_enabled bool default 'f', + ADD COLUMN linktaco_api_token text default '', + ADD COLUMN linktaco_org_slug text default '', + ADD COLUMN linktaco_tags text default '', + ADD COLUMN linktaco_visibility linktaco_link_visibility default 'PUBLIC'; + ` + _, err = tx.Exec(sql) + return err + }, } diff --git a/internal/integration/integration.go b/internal/integration/integration.go index 9981aeb5..2cd68ad8 100644 --- a/internal/integration/integration.go +++ b/internal/integration/integration.go @@ -15,6 +15,7 @@ import ( "miniflux.app/v2/internal/integration/karakeep" "miniflux.app/v2/internal/integration/linkace" "miniflux.app/v2/internal/integration/linkding" + "miniflux.app/v2/internal/integration/linktaco" "miniflux.app/v2/internal/integration/linkwarden" "miniflux.app/v2/internal/integration/matrixbot" "miniflux.app/v2/internal/integration/notion" @@ -242,6 +243,29 @@ func SendEntry(entry *model.Entry, userIntegrations *model.Integration) { } } + if userIntegrations.LinktacoEnabled { + slog.Debug("Sending entry to LinkTaco", + slog.Int64("user_id", userIntegrations.UserID), + slog.Int64("entry_id", entry.ID), + slog.String("entry_url", entry.URL), + ) + + client := linktaco.NewClient( + userIntegrations.LinktacoAPIToken, + userIntegrations.LinktacoOrgSlug, + userIntegrations.LinktacoTags, + userIntegrations.LinktacoVisibility, + ) + if err := client.CreateBookmark(entry.URL, entry.Title, entry.Content); err != nil { + slog.Error("Unable to send entry to LinkTaco", + slog.Int64("user_id", userIntegrations.UserID), + slog.Int64("entry_id", entry.ID), + slog.String("entry_url", entry.URL), + slog.Any("error", err), + ) + } + } + if userIntegrations.LinkwardenEnabled { slog.Debug("Sending entry to linkwarden", slog.Int64("user_id", userIntegrations.UserID), diff --git a/internal/integration/linktaco/linktaco.go b/internal/integration/linktaco/linktaco.go new file mode 100644 index 00000000..3bb9de87 --- /dev/null +++ b/internal/integration/linktaco/linktaco.go @@ -0,0 +1,146 @@ +// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package linktaco // import "miniflux.app/v2/internal/integration/linktaco" + +import ( + "bytes" + "encoding/json" + "fmt" + "net/http" + "strings" + "time" + + "miniflux.app/v2/internal/version" +) + +const ( + defaultClientTimeout = 10 * time.Second + defaultGraphQLURL = "https://api.linktaco.com/query" + maxTags = 10 + maxDescriptionLength = 500 +) + +type Client struct { + graphqlURL string + apiToken string + orgSlug string + tags string + visibility string +} + +func NewClient(apiToken, orgSlug, tags, visibility string) *Client { + if visibility == "" { + visibility = "PUBLIC" + } + return &Client{ + graphqlURL: defaultGraphQLURL, + apiToken: apiToken, + orgSlug: orgSlug, + tags: tags, + visibility: visibility, + } +} + +func (c *Client) CreateBookmark(entryURL, entryTitle, entryContent string) error { + if c.apiToken == "" || c.orgSlug == "" { + return fmt.Errorf("linktaco: missing API token or organization slug") + } + + description := entryContent + if len(description) > maxDescriptionLength { + description = description[:maxDescriptionLength] + } + + // tags (limit to 10) + tags := strings.FieldsFunc(c.tags, func(c rune) bool { + return c == ',' || c == ' ' + }) + if len(tags) > maxTags { + tags = tags[:maxTags] + } + // tagsStr is used in GraphQL query to pass comma separated tags + tagsStr := strings.Join(tags, ",") + + mutation := ` + mutation AddLink($input: LinkInput!) { + addLink(input: $input) { + id + url + title + } + } + ` + + variables := map[string]any{ + "input": map[string]any{ + "url": entryURL, + "title": entryTitle, + "description": description, + "orgSlug": c.orgSlug, + "visibility": c.visibility, + "unread": true, + "starred": false, + "archive": false, + "tags": tagsStr, + }, + } + + requestBody, err := json.Marshal(map[string]any{ + "query": mutation, + "variables": variables, + }) + if err != nil { + return fmt.Errorf("linktaco: unable to encode request body: %v", err) + } + + request, err := http.NewRequest(http.MethodPost, c.graphqlURL, bytes.NewReader(requestBody)) + if err != nil { + return fmt.Errorf("linktaco: unable to create request: %v", err) + } + + request.Header.Set("Content-Type", "application/json") + request.Header.Set("User-Agent", "Miniflux/"+version.Version) + request.Header.Set("Authorization", "Bearer "+c.apiToken) + + httpClient := &http.Client{Timeout: defaultClientTimeout} + response, err := httpClient.Do(request) + if err != nil { + return fmt.Errorf("linktaco: unable to send request: %v", err) + } + defer response.Body.Close() + + if response.StatusCode >= 400 { + return fmt.Errorf("linktaco: unable to create bookmark: status=%d", response.StatusCode) + } + + var graphqlResponse struct { + Data json.RawMessage `json:"data"` + Errors []json.RawMessage `json:"errors"` + } + + if err := json.NewDecoder(response.Body).Decode(&graphqlResponse); err != nil { + return fmt.Errorf("linktaco: unable to decode response: %v", err) + } + + if len(graphqlResponse.Errors) > 0 { + // Try to extract error message + var errorMsg string + for _, errJSON := range graphqlResponse.Errors { + var errObj struct { + Message string `json:"message"` + } + if json.Unmarshal(errJSON, &errObj) == nil && errObj.Message != "" { + errorMsg = errObj.Message + break + } + } + if errorMsg == "" { + // Fallback. Should never be reached. + errorMsg = "GraphQL error occurred (fallback message)" + } + return fmt.Errorf("linktaco: %s", errorMsg) + } + + return nil +} diff --git a/internal/integration/linktaco/linktaco_test.go b/internal/integration/linktaco/linktaco_test.go new file mode 100644 index 00000000..cf61f27a --- /dev/null +++ b/internal/integration/linktaco/linktaco_test.go @@ -0,0 +1,440 @@ +// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package linktaco + +import ( + "encoding/json" + "io" + "net/http" + "net/http/httptest" + "strings" + "testing" +) + +func TestCreateBookmark(t *testing.T) { + tests := []struct { + name string + apiToken string + orgSlug string + tags string + visibility string + entryURL string + entryTitle string + entryContent string + serverResponse func(w http.ResponseWriter, r *http.Request) + wantErr bool + errContains string + }{ + { + name: "successful bookmark creation", + apiToken: "test-token", + orgSlug: "test-org", + tags: "tag1, tag2", + visibility: "PUBLIC", + entryURL: "https://example.com", + entryTitle: "Test Article", + entryContent: "Test content", + serverResponse: func(w http.ResponseWriter, r *http.Request) { + // 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]interface{} + if err := json.Unmarshal(body, &req); err != nil { + t.Errorf("Failed to parse request body: %v", err) + } + + // Verify mutation exists + if _, ok := req["query"]; !ok { + t.Error("Missing 'query' field in request") + } + + // Return success response + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(map[string]interface{}{ + "data": map[string]interface{}{ + "addLink": map[string]interface{}{ + "id": "123", + "url": "https://example.com", + "title": "Test Article", + }, + }, + }) + }, + wantErr: false, + }, + { + name: "missing API token", + apiToken: "", + orgSlug: "test-org", + entryURL: "https://example.com", + entryTitle: "Test", + entryContent: "Content", + serverResponse: func(w http.ResponseWriter, r *http.Request) { + // Should not be called + t.Error("Server should not be called when API token is missing") + }, + wantErr: true, + errContains: "missing API token or organization slug", + }, + { + name: "missing organization slug", + apiToken: "test-token", + orgSlug: "", + entryURL: "https://example.com", + entryTitle: "Test", + entryContent: "Content", + serverResponse: func(w http.ResponseWriter, r *http.Request) { + // Should not be called + t.Error("Server should not be called when org slug is missing") + }, + wantErr: true, + errContains: "missing API token or organization slug", + }, + { + name: "GraphQL error response", + apiToken: "test-token", + orgSlug: "test-org", + entryURL: "https://example.com", + entryTitle: "Test", + entryContent: "Content", + serverResponse: func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(map[string]interface{}{ + "errors": []interface{}{ + map[string]interface{}{ + "message": "Invalid input", + }, + }, + }) + }, + wantErr: true, + errContains: "Invalid input", + }, + { + name: "HTTP error status", + apiToken: "test-token", + orgSlug: "test-org", + entryURL: "https://example.com", + entryTitle: "Test", + entryContent: "Content", + serverResponse: func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusUnauthorized) + }, + wantErr: true, + errContains: "status=401", + }, + { + name: "private visibility permission error", + apiToken: "test-token", + orgSlug: "test-org", + visibility: "PRIVATE", + entryURL: "https://example.com", + entryTitle: "Test", + entryContent: "Content", + serverResponse: func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(map[string]interface{}{ + "errors": []interface{}{ + map[string]interface{}{ + "message": "PRIVATE visibility requires a paid LinkTaco account", + }, + }, + }) + }, + wantErr: true, + errContains: "PRIVATE visibility requires a paid LinkTaco account", + }, + { + name: "content truncation", + apiToken: "test-token", + orgSlug: "test-org", + entryURL: "https://example.com", + entryTitle: "Test", + entryContent: strings.Repeat("a", 600), // Content longer than 500 chars + serverResponse: func(w http.ResponseWriter, r *http.Request) { + body, _ := io.ReadAll(r.Body) + var req map[string]interface{} + json.Unmarshal(body, &req) + + // Check that description was truncated + variables := req["variables"].(map[string]interface{}) + input := variables["input"].(map[string]interface{}) + description := input["description"].(string) + + if len(description) != maxDescriptionLength { + t.Errorf("Expected description length %d, got %d", maxDescriptionLength, len(description)) + } + + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(map[string]interface{}{ + "data": map[string]interface{}{ + "addLink": map[string]interface{}{"id": "123"}, + }, + }) + }, + wantErr: false, + }, + { + name: "tag limiting", + apiToken: "test-token", + orgSlug: "test-org", + tags: "tag1,tag2,tag3,tag4,tag5,tag6,tag7,tag8,tag9,tag10,tag11,tag12", + entryURL: "https://example.com", + entryTitle: "Test", + entryContent: "Content", + serverResponse: func(w http.ResponseWriter, r *http.Request) { + body, _ := io.ReadAll(r.Body) + var req map[string]interface{} + json.Unmarshal(body, &req) + + // Check that only 10 tags were sent + variables := req["variables"].(map[string]interface{}) + input := variables["input"].(map[string]interface{}) + tags := input["tags"].(string) + + tagCount := len(strings.Split(tags, ",")) + if tagCount != maxTags { + t.Errorf("Expected %d tags, got %d", maxTags, tagCount) + } + + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(map[string]interface{}{ + "data": map[string]interface{}{ + "addLink": map[string]interface{}{"id": "123"}, + }, + }) + }, + wantErr: false, + }, + { + name: "invalid JSON response", + apiToken: "test-token", + orgSlug: "test-org", + entryURL: "https://example.com", + entryTitle: "Test", + entryContent: "Content", + serverResponse: func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + w.Write([]byte("invalid json")) + }, + wantErr: true, + errContains: "unable to decode response", + }, + } + + 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 := &Client{ + graphqlURL: serverURL, + apiToken: tt.apiToken, + orgSlug: tt.orgSlug, + tags: tt.tags, + visibility: tt.visibility, + } + + // Call CreateBookmark + err := client.CreateBookmark(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 + apiToken string + orgSlug string + tags string + visibility string + expectedVisibility string + }{ + { + name: "with all parameters", + apiToken: "token", + orgSlug: "org", + tags: "tag1,tag2", + visibility: "PRIVATE", + expectedVisibility: "PRIVATE", + }, + { + name: "empty visibility defaults to PUBLIC", + apiToken: "token", + orgSlug: "org", + tags: "tag1", + visibility: "", + expectedVisibility: "PUBLIC", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + client := NewClient(tt.apiToken, tt.orgSlug, tt.tags, tt.visibility) + + if client.apiToken != tt.apiToken { + t.Errorf("Expected apiToken %s, got %s", tt.apiToken, client.apiToken) + } + if client.orgSlug != tt.orgSlug { + t.Errorf("Expected orgSlug %s, got %s", tt.orgSlug, client.orgSlug) + } + if client.tags != tt.tags { + t.Errorf("Expected tags %s, got %s", tt.tags, client.tags) + } + if client.visibility != tt.expectedVisibility { + t.Errorf("Expected visibility %s, got %s", tt.expectedVisibility, client.visibility) + } + if client.graphqlURL != defaultGraphQLURL { + t.Errorf("Expected graphqlURL %s, got %s", defaultGraphQLURL, client.graphqlURL) + } + }) + } +} + +func TestGraphQLMutation(t *testing.T) { + // Test that the GraphQL mutation is properly formatted + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + body, _ := io.ReadAll(r.Body) + var req map[string]interface{} + if err := json.Unmarshal(body, &req); err != nil { + t.Fatalf("Failed to parse request: %v", err) + } + + // Verify mutation structure + query, ok := req["query"].(string) + if !ok { + t.Fatal("Missing query field") + } + + // Check that mutation contains expected parts + if !strings.Contains(query, "mutation AddLink") { + t.Error("Mutation should contain 'mutation AddLink'") + } + if !strings.Contains(query, "$input: LinkInput!") { + t.Error("Mutation should contain input parameter") + } + if !strings.Contains(query, "addLink(input: $input)") { + t.Error("Mutation should contain addLink call") + } + + // Verify variables structure + variables, ok := req["variables"].(map[string]interface{}) + if !ok { + t.Fatal("Missing variables field") + } + + input, ok := variables["input"].(map[string]interface{}) + if !ok { + t.Fatal("Missing input in variables") + } + + // Check all required fields + requiredFields := []string{"url", "title", "description", "orgSlug", "visibility", "unread", "starred", "archive", "tags"} + for _, field := range requiredFields { + if _, ok := input[field]; !ok { + t.Errorf("Missing required field: %s", field) + } + } + + // Return success + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(map[string]interface{}{ + "data": map[string]interface{}{ + "addLink": map[string]interface{}{ + "id": "123", + }, + }, + }) + })) + defer server.Close() + + client := &Client{ + graphqlURL: server.URL, + apiToken: "test-token", + orgSlug: "test-org", + tags: "test", + visibility: "PUBLIC", + } + + err := client.CreateBookmark("https://example.com", "Test Title", "Test Content") + if err != nil { + t.Errorf("Unexpected error: %v", err) + } +} + +func BenchmarkCreateBookmark(b *testing.B) { + // Create a mock server that always returns success + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(map[string]interface{}{ + "data": map[string]interface{}{ + "addLink": map[string]interface{}{ + "id": "123", + }, + }, + }) + })) + defer server.Close() + + client := &Client{ + graphqlURL: server.URL, + apiToken: "test-token", + orgSlug: "test-org", + tags: "tag1,tag2,tag3", + visibility: "PUBLIC", + } + + // Run benchmark + b.ResetTimer() + for i := 0; i < b.N; i++ { + _ = client.CreateBookmark("https://example.com", "Test Title", "Test Content") + } +} + +func BenchmarkTagProcessing(b *testing.B) { + // Benchmark tag splitting and limiting + tags := "tag1,tag2,tag3,tag4,tag5,tag6,tag7,tag8,tag9,tag10,tag11,tag12,tag13,tag14,tag15" + + b.ResetTimer() + for i := 0; i < b.N; i++ { + tagsSplitFn := func(c rune) bool { + return c == ',' || c == ' ' + } + splitTags := strings.FieldsFunc(tags, tagsSplitFn) + if len(splitTags) > maxTags { + splitTags = splitTags[:maxTags] + } + _ = strings.Join(splitTags, ",") + } +} diff --git a/internal/locale/translations/de_DE.json b/internal/locale/translations/de_DE.json index 05490e64..f94d0fab 100644 --- a/internal/locale/translations/de_DE.json +++ b/internal/locale/translations/de_DE.json @@ -160,6 +160,7 @@ "error.unlink_account_without_password": "Sie müssen ein Passwort festlegen, sonst können Sie sich nicht erneut anmelden.", "error.user_already_exists": "Dieser Benutzer existiert bereits.", "error.user_mandatory_fields": "Der Benutzername ist obligatorisch.", + "error.linktaco_missing_required_fields": "LinkTaco API Token und Organization Slug sind erforderlich.", "form.api_key.label.description": "API-Schlüsselbezeichnung", "form.category.hide_globally": "Einträge in der globalen Ungelesen-Liste ausblenden", "form.category.label.title": "Titel", @@ -250,6 +251,16 @@ "form.integration.linkding_bookmark": "Lesezeichen als ungelesen markieren", "form.integration.linkding_endpoint": "Linkding-API-Endpunkt", "form.integration.linkding_tags": "Linkding-Tags", + "form.integration.linktaco_activate": "Einträge in LinkTaco speichern", + "form.integration.linktaco_api_token": "LinkTaco API Token", + "form.integration.linktaco_api_token_hint": "Holen Sie sich Ihr persönliches Zugriffstoken unter", + "form.integration.linktaco_org_slug": "Organization Slug", + "form.integration.linktaco_tags": "Tags (max. 10, kommagetrennt)", + "form.integration.linktaco_tags_hint": "Maximal 10 Tags, kommagetrennt", + "form.integration.linktaco_visibility": "Sichtbarkeit", + "form.integration.linktaco_visibility_public": "Öffentlich", + "form.integration.linktaco_visibility_private": "Privat", + "form.integration.linktaco_visibility_hint": "PRIVATE Sichtbarkeit erfordert ein kostenpflichtiges LinkTaco-Konto", "form.integration.linkwarden_activate": "Artikel in Linkwarden speichern", "form.integration.linkwarden_api_key": "Linkwarden-API-Schlüssel", "form.integration.linkwarden_endpoint": "Linkwarden-Base-URL", diff --git a/internal/locale/translations/el_EL.json b/internal/locale/translations/el_EL.json index 473d5002..459b9006 100644 --- a/internal/locale/translations/el_EL.json +++ b/internal/locale/translations/el_EL.json @@ -160,6 +160,7 @@ "error.unlink_account_without_password": "Πρέπει να ορίσετε έναν κωδικό πρόσβασης διαφορετικά δεν θα μπορείτε να συνδεθείτε ξανά.", "error.user_already_exists": "Αυτός ο χρήστης υπάρχει ήδη.", "error.user_mandatory_fields": "Το όνομα χρήστη είναι υποχρεωτικό.", + "error.linktaco_missing_required_fields": "Το LinkTaco API Token και το Organization Slug είναι απαραίτητα", "form.api_key.label.description": "Ετικέτα κλειδιού API", "form.category.hide_globally": "Απόκρυψη καταχωρήσεων σε γενική λίστα μη αναγνωσμένων", "form.category.label.title": "Τίτλος", @@ -250,6 +251,16 @@ "form.integration.linkding_bookmark": "Σημείωση του σελιδοδείκτη ως μη αναγνωσμένου", "form.integration.linkding_endpoint": "Τελικό σημείο Linkding API", "form.integration.linkding_tags": "Ετικέτες Linkding", + "form.integration.linktaco_activate": "Αποθήκευση καταχωρήσεων στο LinkTaco", + "form.integration.linktaco_api_token": "LinkTaco API Token", + "form.integration.linktaco_api_token_hint": "Λάβετε το προσωπικό σας διακριτικό πρόσβασης στο", + "form.integration.linktaco_org_slug": "Organization Slug", + "form.integration.linktaco_tags": "Ετικέτες (μέγιστο 10, διαχωρισμένες με κόμμα)", + "form.integration.linktaco_tags_hint": "Μέγιστο 10 ετικέτες, διαχωρισμένες με κόμμα", + "form.integration.linktaco_visibility": "Ορατότητα", + "form.integration.linktaco_visibility_public": "Δημόσια", + "form.integration.linktaco_visibility_private": "Ιδιωτική", + "form.integration.linktaco_visibility_hint": "Η ΙΔΙΩΤΙΚΗ ορατότητα απαιτεί επί πληρωμή λογαριασμό LinkTaco", "form.integration.linkwarden_activate": "Αποθήκευση άρθρων στο Linkwarden", "form.integration.linkwarden_api_key": "Κλειδί API Linkwarden", "form.integration.linkwarden_endpoint": "URL βάσης Linkwarden", diff --git a/internal/locale/translations/en_US.json b/internal/locale/translations/en_US.json index 65960e40..c43c8de5 100644 --- a/internal/locale/translations/en_US.json +++ b/internal/locale/translations/en_US.json @@ -89,6 +89,7 @@ "error.different_passwords": "Passwords are not the same.", "error.duplicate_fever_username": "There is already someone else with the same Fever username!", "error.duplicate_googlereader_username": "There is already someone else with the same Google Reader username!", + "error.linktaco_missing_required_fields": "LinkTaco API Token and Organization Slug are required", "error.duplicate_linked_account": "There is already someone associated with this provider!", "error.duplicated_feed": "This feed already exists.", "error.empty_file": "This file is empty.", @@ -250,6 +251,16 @@ "form.integration.linkding_bookmark": "Mark bookmark as unread", "form.integration.linkding_endpoint": "Linkding API Endpoint", "form.integration.linkding_tags": "Linkding Tags", + "form.integration.linktaco_activate": "Save entries to LinkTaco", + "form.integration.linktaco_api_token": "LinkTaco API Token", + "form.integration.linktaco_api_token_hint": "Get your personal access token at", + "form.integration.linktaco_org_slug": "Organization Slug", + "form.integration.linktaco_tags": "Tags (max 10, comma-separated)", + "form.integration.linktaco_tags_hint": "Maximum 10 tags, comma-separated", + "form.integration.linktaco_visibility": "Visibility", + "form.integration.linktaco_visibility_public": "Public", + "form.integration.linktaco_visibility_private": "Private", + "form.integration.linktaco_visibility_hint": "PRIVATE visibility requires a paid LinkTaco account", "form.integration.linkwarden_activate": "Save entries to Linkwarden", "form.integration.linkwarden_api_key": "Linkwarden API key", "form.integration.linkwarden_endpoint": "Linkwarden Base URL", diff --git a/internal/locale/translations/es_ES.json b/internal/locale/translations/es_ES.json index 8553ceeb..ccd8e3fa 100644 --- a/internal/locale/translations/es_ES.json +++ b/internal/locale/translations/es_ES.json @@ -160,6 +160,7 @@ "error.unlink_account_without_password": "Debe definir una contraseña, de lo contrario no podrá volver a iniciar sesión.", "error.user_already_exists": "Este usuario ya existe.", "error.user_mandatory_fields": "El nombre de usuario es obligatorio.", + "error.linktaco_missing_required_fields": "LinkTaco API Token y Organization Slug son obligatorios.", "form.api_key.label.description": "Etiqueta de clave API", "form.category.hide_globally": "Ocultar artículos en la lista global de no leídos", "form.category.label.title": "Título", @@ -250,6 +251,16 @@ "form.integration.linkding_bookmark": "Marcar marcador como no leído", "form.integration.linkding_endpoint": "Acceso API de Linkding", "form.integration.linkding_tags": "Etiquetas de Linkding", + "form.integration.linktaco_activate": "Guardar entradas en LinkTaco", + "form.integration.linktaco_api_token": "LinkTaco API Token", + "form.integration.linktaco_api_token_hint": "Obtenga su token de acceso personal en", + "form.integration.linktaco_org_slug": "Organization Slug", + "form.integration.linktaco_tags": "Etiquetas (máx. 10, separadas por comas)", + "form.integration.linktaco_tags_hint": "Máximo 10 etiquetas, separadas por comas", + "form.integration.linktaco_visibility": "Visibilidad", + "form.integration.linktaco_visibility_public": "Público", + "form.integration.linktaco_visibility_private": "Privado", + "form.integration.linktaco_visibility_hint": "La visibilidad PRIVADA requiere una cuenta de pago de LinkTaco", "form.integration.linkwarden_activate": "Enviar artículos a Linkwarden", "form.integration.linkwarden_api_key": "Clave de API de Linkwarden", "form.integration.linkwarden_endpoint": "URL base de Linkwarden", diff --git a/internal/locale/translations/fi_FI.json b/internal/locale/translations/fi_FI.json index 11638560..bb988aee 100644 --- a/internal/locale/translations/fi_FI.json +++ b/internal/locale/translations/fi_FI.json @@ -160,6 +160,7 @@ "error.unlink_account_without_password": "Sinun on määritettävä salasana, muuten et voi kirjautua uudelleen.", "error.user_already_exists": "Käyttäjä on jo olemassa.", "error.user_mandatory_fields": "Käyttäjätunnus on pakollinen.", + "error.linktaco_missing_required_fields": "LinkTaco API Token ja Organization Slug vaaditaan", "form.api_key.label.description": "API Key Label", "form.category.hide_globally": "Piilota artikkelit lukemattomien listassa", "form.category.label.title": "Otsikko", @@ -250,6 +251,16 @@ "form.integration.linkding_bookmark": "Merkitse kirjanmerkki lukemattomaksi", "form.integration.linkding_endpoint": "Linkding API-päätepiste", "form.integration.linkding_tags": "Linkding Tags", + "form.integration.linktaco_activate": "Tallenna kirjoituksia LinkTacoon", + "form.integration.linktaco_api_token": "LinkTaco API Token", + "form.integration.linktaco_api_token_hint": "Hanki henkilökohtainen pääsytunnistesi osoitteesta", + "form.integration.linktaco_org_slug": "Organization Slug", + "form.integration.linktaco_tags": "Tagit (enintään 10, pilkuilla erotettu)", + "form.integration.linktaco_tags_hint": "Enintään 10 tagia, pilkuilla erotettu", + "form.integration.linktaco_visibility": "Näkyvyys", + "form.integration.linktaco_visibility_public": "Julkinen", + "form.integration.linktaco_visibility_private": "Yksityinen", + "form.integration.linktaco_visibility_hint": "YKSITYINEN näkyvyys vaatii maksullisen LinkTaco-tilin", "form.integration.linkwarden_activate": "Tallenna artikkelit Linkkiin", "form.integration.linkwarden_api_key": "Linkwarden API-avain", "form.integration.linkwarden_endpoint": "Linkwarden Base URL", diff --git a/internal/locale/translations/fr_FR.json b/internal/locale/translations/fr_FR.json index 75bf4f4f..bcd2dff8 100644 --- a/internal/locale/translations/fr_FR.json +++ b/internal/locale/translations/fr_FR.json @@ -160,6 +160,7 @@ "error.unlink_account_without_password": "Vous devez définir un mot de passe sinon vous ne pourrez plus vous connecter par la suite.", "error.user_already_exists": "Cet utilisateur existe déjà.", "error.user_mandatory_fields": "Le nom d'utilisateur est obligatoire.", + "error.linktaco_missing_required_fields": "Le token API LinkTaco et le slug de l'organisation sont requis.", "form.api_key.label.description": "Libellé de la clé d'API", "form.category.hide_globally": "Masquer les entrées dans la liste globale non lue", "form.category.label.title": "Titre", @@ -250,6 +251,16 @@ "form.integration.linkding_bookmark": "Marquer le lien comme non lu", "form.integration.linkding_endpoint": "URL de l'API de Linkding", "form.integration.linkding_tags": "Libellés", + "form.integration.linktaco_activate": "Sauvegarder les entrées vers LinkTaco", + "form.integration.linktaco_api_token": "Token API LinkTaco", + "form.integration.linktaco_api_token_hint": "Obtenez votre token d'accès personnel sur", + "form.integration.linktaco_org_slug": "Slug de l'organisation", + "form.integration.linktaco_tags": "Tags (max. 10, séparés par des virgules)", + "form.integration.linktaco_tags_hint": "Maximum 10 tags, séparés par des virgules", + "form.integration.linktaco_visibility": "Visibilité", + "form.integration.linktaco_visibility_public": "Public", + "form.integration.linktaco_visibility_private": "Privé", + "form.integration.linktaco_visibility_hint": "La visibilité PRIVÉE nécessite un compte LinkTaco payant", "form.integration.linkwarden_activate": "Sauvegarder les articles vers Linkwarden", "form.integration.linkwarden_api_key": "Clé d'API de Linkwarden", "form.integration.linkwarden_endpoint": "URL de base de Linkwarden", diff --git a/internal/locale/translations/hi_IN.json b/internal/locale/translations/hi_IN.json index ff9af3d4..d831cf36 100644 --- a/internal/locale/translations/hi_IN.json +++ b/internal/locale/translations/hi_IN.json @@ -160,6 +160,7 @@ "error.unlink_account_without_password": "आपको एक पासवर्ड परिभाषित करना होगा अन्यथा आप फिर से लॉगिन नहीं कर पाएंगे।", "error.user_already_exists": "यह उपयोगकर्ता पहले से ही मौजूद है।", "error.user_mandatory_fields": "उपयोगकर्ता नाम अनिवार्य है।", + "error.linktaco_missing_required_fields": "LinkTaco API Token और Organization Slug आवश्यक हैं", "form.api_key.label.description": "एपीआई कुंजी लेबल", "form.category.hide_globally": "वैश्विक अपठित सूची में प्रविष्टियां छिपाएं", "form.category.label.title": "शीर्षक", @@ -250,6 +251,16 @@ "form.integration.linkding_bookmark": "बुकमार्क को अपठित के रूप में चिह्नित करें", "form.integration.linkding_endpoint": "लिंकिंग एपीआई समापन बिंदु", "form.integration.linkding_tags": "Linkding Tags", + "form.integration.linktaco_activate": "LinkTaco में प्रविष्टियाँ सहेजें", + "form.integration.linktaco_api_token": "LinkTaco API Token", + "form.integration.linktaco_api_token_hint": "अपना व्यक्तिगत पहुँच टोकन प्राप्त करें", + "form.integration.linktaco_org_slug": "Organization Slug", + "form.integration.linktaco_tags": "टैग (अधिकतम 10, कॉमा से अलग किए गए)", + "form.integration.linktaco_tags_hint": "अधिकतम 10 टैग, कॉमा से अलग किए गए", + "form.integration.linktaco_visibility": "दृश्यता", + "form.integration.linktaco_visibility_public": "सार्वजनिक", + "form.integration.linktaco_visibility_private": "निजी", + "form.integration.linktaco_visibility_hint": "निजी दृश्यता के लिए भुगतान LinkTaco खाता आवश्यक है", "form.integration.linkwarden_activate": "Save entries to Linkwarden", "form.integration.linkwarden_api_key": "Linkwarden API key", "form.integration.linkwarden_endpoint": "लिंकवर्डन बेस यूआरएलL", diff --git a/internal/locale/translations/id_ID.json b/internal/locale/translations/id_ID.json index 10dc2c62..9df6ad58 100644 --- a/internal/locale/translations/id_ID.json +++ b/internal/locale/translations/id_ID.json @@ -157,6 +157,7 @@ "error.unlink_account_without_password": "Anda harus mengatur kata sandi atau Anda tidak bisa masuk kembali.", "error.user_already_exists": "Pengguna ini sudah ada.", "error.user_mandatory_fields": "Harus ada nama pengguna.", + "error.linktaco_missing_required_fields": "LinkTaco API Token dan Organization Slug diperlukan", "form.api_key.label.description": "Label Kunci API", "form.category.hide_globally": "Sembunyikan entri di daftar belum dibaca global", "form.category.label.title": "Judul", @@ -247,6 +248,16 @@ "form.integration.linkding_bookmark": "Tandai markah sebagai belum dibaca", "form.integration.linkding_endpoint": "Titik URL API Linkding", "form.integration.linkding_tags": "Tanda Linkding", + "form.integration.linktaco_activate": "Simpan entri ke LinkTaco", + "form.integration.linktaco_api_token": "LinkTaco API Token", + "form.integration.linktaco_api_token_hint": "Dapatkan token akses pribadi Anda di", + "form.integration.linktaco_org_slug": "Organization Slug", + "form.integration.linktaco_tags": "Tag (maksimal 10, dipisahkan koma)", + "form.integration.linktaco_tags_hint": "Maksimal 10 tag, dipisahkan koma", + "form.integration.linktaco_visibility": "Visibilitas", + "form.integration.linktaco_visibility_public": "Publik", + "form.integration.linktaco_visibility_private": "Pribadi", + "form.integration.linktaco_visibility_hint": "Visibilitas PRIBADI membutuhkan akun LinkTaco berbayar", "form.integration.linkwarden_activate": "Simpan artikel ke Linkwarden", "form.integration.linkwarden_api_key": "Kunci API Linkwarden", "form.integration.linkwarden_endpoint": "URL Dasar Linkwarden", diff --git a/internal/locale/translations/it_IT.json b/internal/locale/translations/it_IT.json index 9bc79a78..7fc36fe6 100644 --- a/internal/locale/translations/it_IT.json +++ b/internal/locale/translations/it_IT.json @@ -160,6 +160,7 @@ "error.unlink_account_without_password": "Devi scegliere una password altrimenti la prossima volta non riuscirai ad accedere.", "error.user_already_exists": "Questo utente esiste già.", "error.user_mandatory_fields": "Il nome utente è obbligatorio.", + "error.linktaco_missing_required_fields": "LinkTaco API Token e Organization Slug sono richiesti", "form.api_key.label.description": "Etichetta chiave API", "form.category.hide_globally": "Nascondere le voci nella lista globale dei non letti", "form.category.label.title": "Titolo", @@ -250,6 +251,16 @@ "form.integration.linkding_bookmark": "Segna i preferiti come non letti", "form.integration.linkding_endpoint": "Endpoint dell'API di Linkding", "form.integration.linkding_tags": "Linkding Tags", + "form.integration.linktaco_activate": "Salva le voci in LinkTaco", + "form.integration.linktaco_api_token": "LinkTaco API Token", + "form.integration.linktaco_api_token_hint": "Ottieni il tuo token di accesso personale su", + "form.integration.linktaco_org_slug": "Organization Slug", + "form.integration.linktaco_tags": "Tag (massimo 10, separati da virgola)", + "form.integration.linktaco_tags_hint": "Massimo 10 tag, separati da virgola", + "form.integration.linktaco_visibility": "Visibilità", + "form.integration.linktaco_visibility_public": "Pubblico", + "form.integration.linktaco_visibility_private": "Privato", + "form.integration.linktaco_visibility_hint": "La visibilità PRIVATA richiede un account LinkTaco a pagamento", "form.integration.linkwarden_activate": "Salva gli articoli su Linkwarden", "form.integration.linkwarden_api_key": "API key dell'account Linkwarden", "form.integration.linkwarden_endpoint": "URL di base di Linkwarden", diff --git a/internal/locale/translations/ja_JP.json b/internal/locale/translations/ja_JP.json index 21637e74..4be7b366 100644 --- a/internal/locale/translations/ja_JP.json +++ b/internal/locale/translations/ja_JP.json @@ -157,6 +157,7 @@ "error.unlink_account_without_password": "パスワードを設定しなければ再びログインすることはできません。", "error.user_already_exists": "このユーザーは既に存在します。", "error.user_mandatory_fields": "ユーザー名が必要です。", + "error.linktaco_missing_required_fields": "LinkTaco API TokenとOrganization Slugが必要です", "form.api_key.label.description": "API キーラベル", "form.category.hide_globally": "未読一覧に記事を表示しない", "form.category.label.title": "タイトル", @@ -247,6 +248,16 @@ "form.integration.linkding_bookmark": "ブックマークを未読にする", "form.integration.linkding_endpoint": "Linkding の API Endpoint", "form.integration.linkding_tags": "Linkding Tags", + "form.integration.linktaco_activate": "LinkTacoでエントリを保存する", + "form.integration.linktaco_api_token": "LinkTaco API Token", + "form.integration.linktaco_api_token_hint": "パーソナルアクセストークンを取得する", + "form.integration.linktaco_org_slug": "Organization Slug", + "form.integration.linktaco_tags": "タグ (最大10件、カンマ区切り)", + "form.integration.linktaco_tags_hint": "最大10件のタグ、カンマ区切り", + "form.integration.linktaco_visibility": "公開設定", + "form.integration.linktaco_visibility_public": "公開", + "form.integration.linktaco_visibility_private": "非公開", + "form.integration.linktaco_visibility_hint": "非公開設定には有料のLinkTacoアカウントが必要です", "form.integration.linkwarden_activate": "Linkwarden に記事を保存する", "form.integration.linkwarden_api_key": "Linkwarden の API key", "form.integration.linkwarden_endpoint": "リンクワーデン ベース URL", diff --git a/internal/locale/translations/nan_Latn_pehoeji.json b/internal/locale/translations/nan_Latn_pehoeji.json index 5e571d28..ac75a59b 100644 --- a/internal/locale/translations/nan_Latn_pehoeji.json +++ b/internal/locale/translations/nan_Latn_pehoeji.json @@ -157,6 +157,7 @@ "error.unlink_account_without_password": "Lí it-tēng ài siat-tēng bi̍t-bé, bô lí ē bô-hoat-tō͘ koh teng-lo̍k.", "error.user_already_exists": "Chit ê sú-iōng-lâng í-keng chûn-chāi.", "error.user_mandatory_fields": "Tio̍h-ài su-li̍p kháu-chō miâ", + "error.linktaco_missing_required_fields": "LinkTaco API Token kâh Organization Slug sio̍kêi", "form.api_key.label.description": "API só-sîkhan-á", "form.category.hide_globally": "Mài hián-sī siau-sit tī choân-he̍k ah-bōe tha̍k lia̍t-pió lāi", "form.category.label.title": "Piau-tôe", @@ -247,6 +248,16 @@ "form.integration.linkding_bookmark": "Chù chòe ah-bōe tha̍k", "form.integration.linkding_endpoint": "Linkding API thâu", "form.integration.linkding_tags": "Linkding khan-á", + "form.integration.linktaco_activate": "Pó-chûn siau-sit kàu LinkTaco", + "form.integration.linktaco_api_token": "LinkTaco API Token", + "form.integration.linktaco_api_token_hint": "Chhú-tek lí ê kò-jîn chún-chhú token tī", + "form.integration.linktaco_org_slug": "Organization Slug", + "form.integration.linktaco_tags": "khan-á (siōn-koân 10, iōng tō͘-tiám keh khui)", + "form.integration.linktaco_tags_hint": "Siōn-koân 10 khan-á, iōng tō͘-tiám keh khui", + "form.integration.linktaco_visibility": "Kò-chhiah-kì sìa?", + "form.integration.linktaco_visibility_public": "Kò-chhiah-kì", + "form.integration.linktaco_visibility_private": "Su-lîn", + "form.integration.linktaco_visibility_hint": "Su-lîn sìa tík tio̍h-ài chù-hêng LinkTaco kháu-chō", "form.integration.linkwarden_activate": "Pó-chûn siau-sit kàu Linkwarden", "form.integration.linkwarden_api_key": "Linkwarden API só-sî", "form.integration.linkwarden_endpoint": "Linkwarden Base URL", diff --git a/internal/locale/translations/nl_NL.json b/internal/locale/translations/nl_NL.json index e64b62e3..70675898 100644 --- a/internal/locale/translations/nl_NL.json +++ b/internal/locale/translations/nl_NL.json @@ -160,6 +160,7 @@ "error.unlink_account_without_password": "Je moet een wachtwoord opgeven anders kun je niet meer inloggen.", "error.user_already_exists": "Deze gebruiker bestaat al.", "error.user_mandatory_fields": "Gebruikersnaam is verplicht", + "error.linktaco_missing_required_fields": "LinkTaco API Token en Organization Slug zijn verplicht", "form.api_key.label.description": "API-sleutel omschrijving", "form.category.hide_globally": "Verberg artikelen in de globale ongelezen lijst", "form.category.label.title": "Titel", @@ -250,6 +251,16 @@ "form.integration.linkding_bookmark": "Markeer favoriet als ongelezen", "form.integration.linkding_endpoint": "Linkding URL", "form.integration.linkding_tags": "Linkding tags", + "form.integration.linktaco_activate": "Artikelen opslaan in LinkTaco", + "form.integration.linktaco_api_token": "LinkTaco API Token", + "form.integration.linktaco_api_token_hint": "Verkrijg uw persoonlijke toegangstoken op", + "form.integration.linktaco_org_slug": "Organization Slug", + "form.integration.linktaco_tags": "Tags (max 10, kommagescheiden)", + "form.integration.linktaco_tags_hint": "Maximaal 10 tags, kommagescheiden", + "form.integration.linktaco_visibility": "Zichtbaarheid", + "form.integration.linktaco_visibility_public": "Openbaar", + "form.integration.linktaco_visibility_private": "Privé", + "form.integration.linktaco_visibility_hint": "PRIVÉ zichtbaarheid vereist een betaald LinkTaco account", "form.integration.linkwarden_activate": "Artikelen opslaan in Linkwarden", "form.integration.linkwarden_api_key": "Linkwarden API-sleutel", "form.integration.linkwarden_endpoint": "Linkwarden Basis URL", diff --git a/internal/locale/translations/pl_PL.json b/internal/locale/translations/pl_PL.json index 9c69e71e..79093c7a 100644 --- a/internal/locale/translations/pl_PL.json +++ b/internal/locale/translations/pl_PL.json @@ -163,6 +163,7 @@ "error.unlink_account_without_password": "Musisz zdefiniować hasło, inaczej nie będziesz mógł się ponownie zalogować.", "error.user_already_exists": "Ten użytkownik już istnieje.", "error.user_mandatory_fields": "Nazwa użytkownika jest obowiązkowa.", + "error.linktaco_missing_required_fields": "LinkTaco API Token i Organization Slug są wymagane", "form.api_key.label.description": "Etykieta klucza API", "form.category.hide_globally": "Ukryj wpisy na globalnej liście nieprzeczytanych", "form.category.label.title": "Tytuł", @@ -253,6 +254,16 @@ "form.integration.linkding_bookmark": "Oznacz zakładkę jako nieprzeczytaną", "form.integration.linkding_endpoint": "Punkt końcowy API Linkding", "form.integration.linkding_tags": "Znaczniki Linkding", + "form.integration.linktaco_activate": "Zapisuj wpisy w LinkTaco", + "form.integration.linktaco_api_token": "LinkTaco API Token", + "form.integration.linktaco_api_token_hint": "Uzyskaj swój osobisty token dostępu w", + "form.integration.linktaco_org_slug": "Organization Slug", + "form.integration.linktaco_tags": "Znaczniki (max 10, oddzielone przecinkami)", + "form.integration.linktaco_tags_hint": "Maksymalnie 10 znaczników, oddzielone przecinkami", + "form.integration.linktaco_visibility": "Widoczność", + "form.integration.linktaco_visibility_public": "Publiczne", + "form.integration.linktaco_visibility_private": "Prywatne", + "form.integration.linktaco_visibility_hint": "Widoczność PRYWATNA wymaga płatnego konta LinkTaco", "form.integration.linkwarden_activate": "Zapisuj wpisy w Linkwarden", "form.integration.linkwarden_api_key": "Klucz API do Linkwarden", "form.integration.linkwarden_endpoint": "Podstawowy adres URL Linkwardena", diff --git a/internal/locale/translations/pt_BR.json b/internal/locale/translations/pt_BR.json index 2762ac52..4b2c2a34 100644 --- a/internal/locale/translations/pt_BR.json +++ b/internal/locale/translations/pt_BR.json @@ -160,6 +160,7 @@ "error.unlink_account_without_password": "Você deve definir uma senha, senão não será possível efetuar a sessão novamente.", "error.user_already_exists": "Esse usuário já existe.", "error.user_mandatory_fields": "O nome de usuário é obrigatório.", + "error.linktaco_missing_required_fields": "LinkTaco API Token e Organization Slug são obrigatórios", "form.api_key.label.description": "Etiqueta da chave de API", "form.category.hide_globally": "Ocultar entradas na lista global não lida", "form.category.label.title": "Título", @@ -250,6 +251,16 @@ "form.integration.linkding_bookmark": "Salvar marcador como não lido", "form.integration.linkding_endpoint": "Endpoint de API do Linkding", "form.integration.linkding_tags": "Linkding Tags", + "form.integration.linktaco_activate": "Salvar itens no LinkTaco", + "form.integration.linktaco_api_token": "LinkTaco API Token", + "form.integration.linktaco_api_token_hint": "Obtenha seu token de acesso pessoal em", + "form.integration.linktaco_org_slug": "Organization Slug", + "form.integration.linktaco_tags": "Tags (máx 10, separadas por vírgula)", + "form.integration.linktaco_tags_hint": "Máximo 10 tags, separadas por vírgula", + "form.integration.linktaco_visibility": "Visibilidade", + "form.integration.linktaco_visibility_public": "Público", + "form.integration.linktaco_visibility_private": "Privado", + "form.integration.linktaco_visibility_hint": "Visibilidade PRIVADA requer uma conta LinkTaco paga", "form.integration.linkwarden_activate": "Salvar itens no Linkwarden", "form.integration.linkwarden_api_key": "Chave de API do Linkwarden", "form.integration.linkwarden_endpoint": "URL base do Linkwarden", diff --git a/internal/locale/translations/ro_RO.json b/internal/locale/translations/ro_RO.json index 60e1247c..60c00e50 100644 --- a/internal/locale/translations/ro_RO.json +++ b/internal/locale/translations/ro_RO.json @@ -163,6 +163,7 @@ "error.unlink_account_without_password": "Trebuie să definiți o parolă, altfel nu vă veți mai putea conecta.", "error.user_already_exists": "Acest utilizator există deja.", "error.user_mandatory_fields": "Numele utilizatorului este obligatoriu.", + "error.linktaco_missing_required_fields": "LinkTaco API Token și Organization Slug sunt necesare", "form.api_key.label.description": "Etichetă Cheie API", "form.category.hide_globally": "Ascunde intrările în lista globală de articole necitite", "form.category.label.title": "Titlu", @@ -253,6 +254,16 @@ "form.integration.linkding_bookmark": "Marchează semnele de carte ca necitite", "form.integration.linkding_endpoint": "Endpoint API Linkding", "form.integration.linkding_tags": "TAG-uri Linkding", + "form.integration.linktaco_activate": "Salvează înregistrările în LinkTaco", + "form.integration.linktaco_api_token": "LinkTaco API Token", + "form.integration.linktaco_api_token_hint": "Obțineți jetonul de acces personal la", + "form.integration.linktaco_org_slug": "Organization Slug", + "form.integration.linktaco_tags": "Tag-uri (maxim 10, separate prin virgule)", + "form.integration.linktaco_tags_hint": "Maxim 10 tag-uri, separate prin virgule", + "form.integration.linktaco_visibility": "Vizibilitate", + "form.integration.linktaco_visibility_public": "Public", + "form.integration.linktaco_visibility_private": "Privat", + "form.integration.linktaco_visibility_hint": "Vizibilitatea PRIVATĂ necesită un cont LinkTaco plătit", "form.integration.linkwarden_activate": "Salvează intrările în Linkwarden", "form.integration.linkwarden_api_key": "Cheie API Linkwarden", "form.integration.linkwarden_endpoint": "URL-ul de bază Linkwarden", diff --git a/internal/locale/translations/ru_RU.json b/internal/locale/translations/ru_RU.json index 5c9b0c5d..2a025556 100644 --- a/internal/locale/translations/ru_RU.json +++ b/internal/locale/translations/ru_RU.json @@ -163,6 +163,7 @@ "error.unlink_account_without_password": "Вы должны установить пароль, иначе вы не сможете войти снова.", "error.user_already_exists": "Этот пользователь уже существует.", "error.user_mandatory_fields": "Имя пользователя обязательно.", + "error.linktaco_missing_required_fields": "LinkTaco API Token и Organization Slug обязательны", "form.api_key.label.description": "Описание API-ключа", "form.category.hide_globally": "Скрыть записи в глобальном списке непрочитанных", "form.category.label.title": "Название", @@ -253,6 +254,16 @@ "form.integration.linkding_bookmark": "Помечать закладки как непрочитанное", "form.integration.linkding_endpoint": "Конечная точка Linkding API", "form.integration.linkding_tags": "Теги Linkding", + "form.integration.linktaco_activate": "Сохранять статьи в LinkTaco", + "form.integration.linktaco_api_token": "LinkTaco API Token", + "form.integration.linktaco_api_token_hint": "Получить ваш персональный токен доступа на", + "form.integration.linktaco_org_slug": "Organization Slug", + "form.integration.linktaco_tags": "Теги (макс. 10, через запятую)", + "form.integration.linktaco_tags_hint": "Максимум 10 тегов, через запятую", + "form.integration.linktaco_visibility": "Видимость", + "form.integration.linktaco_visibility_public": "Публично", + "form.integration.linktaco_visibility_private": "Приватно", + "form.integration.linktaco_visibility_hint": "ПРИВАТНАЯ видимость требует платного аккаунта LinkTaco", "form.integration.linkwarden_activate": "Сохранять статьи в Linkwarden", "form.integration.linkwarden_api_key": "API-ключ Linkwarden", "form.integration.linkwarden_endpoint": "Базовый URL-адрес Linkwarden", diff --git a/internal/locale/translations/tr_TR.json b/internal/locale/translations/tr_TR.json index fe7cc6ca..1f1b9162 100644 --- a/internal/locale/translations/tr_TR.json +++ b/internal/locale/translations/tr_TR.json @@ -160,6 +160,7 @@ "error.unlink_account_without_password": "Bir şifre belirlemelisiniz, aksi takdirde tekrar oturum açamazsınız.", "error.user_already_exists": "Bu kullanıcı zaten mevcut.", "error.user_mandatory_fields": "Kullanıcı adı zorunlu.", + "error.linktaco_missing_required_fields": "LinkTaco API Token ve Organization Slug gereklidir", "form.api_key.label.description": "API Anahtar Etiketi", "form.category.hide_globally": "Genel okunmamış listesindeki girişleri gizle", "form.category.label.title": "Başlık", @@ -250,6 +251,16 @@ "form.integration.linkding_bookmark": "Yer imini okunmadı olarak işaretle", "form.integration.linkding_endpoint": "Linkding API Uç Noktası", "form.integration.linkding_tags": "Linkding Etiketleri", + "form.integration.linktaco_activate": "Makaleleri LinkTaco'ya kaydet", + "form.integration.linktaco_api_token": "LinkTaco API Token", + "form.integration.linktaco_api_token_hint": "Kişisel erişim tokenınızı edinin", + "form.integration.linktaco_org_slug": "Organization Slug", + "form.integration.linktaco_tags": "Etiketler (maks 10, virgülle ayrılmış)", + "form.integration.linktaco_tags_hint": "Maksimum 10 etiket, virgülle ayrılmış", + "form.integration.linktaco_visibility": "Görünürlük", + "form.integration.linktaco_visibility_public": "Genel", + "form.integration.linktaco_visibility_private": "Özel", + "form.integration.linktaco_visibility_hint": "ÖZEL görünürlük ücretli bir LinkTaco hesabı gerektirir", "form.integration.linkwarden_activate": "Makaleleri Linkwarden'e kaydet", "form.integration.linkwarden_api_key": "Linkwarden API Anahtarı", "form.integration.linkwarden_endpoint": "Linkwarden Temel URL'si", diff --git a/internal/locale/translations/uk_UA.json b/internal/locale/translations/uk_UA.json index d83858f0..fae6deb6 100644 --- a/internal/locale/translations/uk_UA.json +++ b/internal/locale/translations/uk_UA.json @@ -162,7 +162,8 @@ "error.unable_to_update_user": "Не вдається оновити користувача.", "error.unlink_account_without_password": "Ви маєте встановити пароль, щоб мати можливість увійти наступного разу", "error.user_already_exists": "Такий користувач вже існує.", - "error.user_mandatory_fields": "Ім’я користувача є обов’язковим.", + "error.user_mandatory_fields": "Ім'я користувача є обов'язковим.", + "error.linktaco_missing_required_fields": "LinkTaco API Token і Organization Slug є обов'язковими", "form.api_key.label.description": "Назва ключа API", "form.category.hide_globally": "Приховати записи в глобальному списку непрочитаного", "form.category.label.title": "Назва", @@ -253,6 +254,16 @@ "form.integration.linkding_bookmark": "Відмічати закладку як непрочитану", "form.integration.linkding_endpoint": "Linkding API Endpoint", "form.integration.linkding_tags": "Linkding Tags", + "form.integration.linktaco_activate": "Зберігати статті в LinkTaco", + "form.integration.linktaco_api_token": "LinkTaco API Token", + "form.integration.linktaco_api_token_hint": "Отримайте ваш персональний токен доступу на", + "form.integration.linktaco_org_slug": "Organization Slug", + "form.integration.linktaco_tags": "Теги (макс. 10, через кому)", + "form.integration.linktaco_tags_hint": "Максимум 10 тегів, через кому", + "form.integration.linktaco_visibility": "Видимість", + "form.integration.linktaco_visibility_public": "Публічно", + "form.integration.linktaco_visibility_private": "Приватно", + "form.integration.linktaco_visibility_hint": "ПРИВАТНА видимість потребує платного акаунта LinkTaco", "form.integration.linkwarden_activate": "Зберігати статті до Linkwarden", "form.integration.linkwarden_api_key": "Ключ API Linkwarden", "form.integration.linkwarden_endpoint": "Базова URL-адреса Linkwarden", diff --git a/internal/locale/translations/zh_CN.json b/internal/locale/translations/zh_CN.json index d28e4553..01766c8e 100644 --- a/internal/locale/translations/zh_CN.json +++ b/internal/locale/translations/zh_CN.json @@ -157,6 +157,7 @@ "error.unlink_account_without_password": "您必须设置密码,否则您将无法再次登录。", "error.user_already_exists": "此用户已存在。", "error.user_mandatory_fields": "必须填写用户名。", + "error.linktaco_missing_required_fields": "LinkTaco API Token 和 Organization Slug 是必需的", "form.api_key.label.description": "API 密钥标签", "form.category.hide_globally": "在全局未读列表中隐藏条目", "form.category.label.title": "标题", @@ -247,6 +248,16 @@ "form.integration.linkding_bookmark": "将书签标记为未读", "form.integration.linkding_endpoint": "Linkding API 端点", "form.integration.linkding_tags": "Linkding 标签", + "form.integration.linktaco_activate": "保存条目到 LinkTaco", + "form.integration.linktaco_api_token": "LinkTaco API Token", + "form.integration.linktaco_api_token_hint": "在此获取您的个人访问令牌", + "form.integration.linktaco_org_slug": "Organization Slug", + "form.integration.linktaco_tags": "标签(最多10个,逗号分隔)", + "form.integration.linktaco_tags_hint": "最多10个标签,逗号分隔", + "form.integration.linktaco_visibility": "可见性", + "form.integration.linktaco_visibility_public": "公开", + "form.integration.linktaco_visibility_private": "私人", + "form.integration.linktaco_visibility_hint": "私人可见性需要付费的 LinkTaco 帐户", "form.integration.linkwarden_activate": "保存条目到 Linkwarden", "form.integration.linkwarden_api_key": "Linkwarden API 密钥", "form.integration.linkwarden_endpoint": "Linkwarden 基本 URL", diff --git a/internal/locale/translations/zh_TW.json b/internal/locale/translations/zh_TW.json index da161efe..1a9525eb 100644 --- a/internal/locale/translations/zh_TW.json +++ b/internal/locale/translations/zh_TW.json @@ -157,6 +157,7 @@ "error.unlink_account_without_password": "您必須設定密碼,否則您將無法再次登入。", "error.user_already_exists": "使用者已存在", "error.user_mandatory_fields": "必須填寫使用者名稱", + "error.linktaco_missing_required_fields": "LinkTaco API Token 和 Organization Slug 是必需的", "form.api_key.label.description": "API 金鑰標籤", "form.category.hide_globally": "在全域未讀列表中隱藏文章", "form.category.label.title": "標題", @@ -247,6 +248,16 @@ "form.integration.linkding_bookmark": "標記為未讀", "form.integration.linkding_endpoint": "Linkding API 端點", "form.integration.linkding_tags": "Linkding 標籤", + "form.integration.linktaco_activate": "儲存文章到 LinkTaco", + "form.integration.linktaco_api_token": "LinkTaco API Token", + "form.integration.linktaco_api_token_hint": "在此取得您的個人存取權杖", + "form.integration.linktaco_org_slug": "Organization Slug", + "form.integration.linktaco_tags": "標籤(最多10個,逗號分隔)", + "form.integration.linktaco_tags_hint": "最多10個標籤,逗號分隔", + "form.integration.linktaco_visibility": "可見性", + "form.integration.linktaco_visibility_public": "公開", + "form.integration.linktaco_visibility_private": "私人", + "form.integration.linktaco_visibility_hint": "私人可見性需要付費的 LinkTaco 帳戶", "form.integration.linkwarden_activate": "儲存文章到 Linkwarden", "form.integration.linkwarden_api_key": "Linkwarden API 金鑰", "form.integration.linkwarden_endpoint": "Linkwarden 基本 URL", diff --git a/internal/model/integration.go b/internal/model/integration.go index 73d64c3e..b948e25c 100644 --- a/internal/model/integration.go +++ b/internal/model/integration.go @@ -59,6 +59,11 @@ type Integration struct { LinkdingAPIKey string LinkdingTags string LinkdingMarkAsUnread bool + LinktacoEnabled bool + LinktacoAPIToken string + LinktacoOrgSlug string + LinktacoTags string + LinktacoVisibility string LinkwardenEnabled bool LinkwardenURL string LinkwardenAPIKey string diff --git a/internal/storage/integration.go b/internal/storage/integration.go index af969795..b50ee5a1 100644 --- a/internal/storage/integration.go +++ b/internal/storage/integration.go @@ -220,7 +220,12 @@ func (s *Storage) Integration(userID int64) (*model.Integration, error) { rssbridge_token, karakeep_enabled, karakeep_api_key, - karakeep_url + karakeep_url, + linktaco_enabled, + linktaco_api_token, + linktaco_org_slug, + linktaco_tags, + linktaco_visibility FROM integrations WHERE @@ -340,6 +345,11 @@ func (s *Storage) Integration(userID int64) (*model.Integration, error) { &integration.KarakeepEnabled, &integration.KarakeepAPIKey, &integration.KarakeepURL, + &integration.LinktacoEnabled, + &integration.LinktacoAPIToken, + &integration.LinktacoOrgSlug, + &integration.LinktacoTags, + &integration.LinktacoVisibility, ) switch { case err == sql.ErrNoRows: @@ -467,9 +477,14 @@ func (s *Storage) UpdateIntegration(integration *model.Integration) error { rssbridge_token=$108, karakeep_enabled=$109, karakeep_api_key=$110, - karakeep_url=$111 + karakeep_url=$111, + linktaco_enabled=$112, + linktaco_api_token=$113, + linktaco_org_slug=$114, + linktaco_tags=$115, + linktaco_visibility=$116 WHERE - user_id=$112 + user_id=$117 ` _, err := s.db.Exec( query, @@ -584,6 +599,11 @@ func (s *Storage) UpdateIntegration(integration *model.Integration) error { integration.KarakeepEnabled, integration.KarakeepAPIKey, integration.KarakeepURL, + integration.LinktacoEnabled, + integration.LinktacoAPIToken, + integration.LinktacoOrgSlug, + integration.LinktacoTags, + integration.LinktacoVisibility, integration.UserID, ) @@ -614,6 +634,7 @@ func (s *Storage) HasSaveEntry(userID int64) (result bool) { readwise_enabled='t' OR linkace_enabled='t' OR linkding_enabled='t' OR + linktaco_enabled='t' OR linkwarden_enabled='t' OR apprise_enabled='t' OR shiori_enabled='t' OR diff --git a/internal/template/templates/views/integrations.html b/internal/template/templates/views/integrations.html index 10b5c5c9..491ae12a 100644 --- a/internal/template/templates/views/integrations.html +++ b/internal/template/templates/views/integrations.html @@ -228,6 +228,37 @@ +
+ LinkTaco +
+ + + + +

{{ t "form.integration.linktaco_api_token_hint" }} https://linktaco.com/oauth2/personal

+ + + + + + +

{{ t "form.integration.linktaco_tags_hint" }}

+ + + +

{{ t "form.integration.linktaco_visibility_hint" }}

+ +
+ +
+
+
+
Linkwarden
diff --git a/internal/ui/form/integration.go b/internal/ui/form/integration.go index 170043e4..4d4e29c3 100644 --- a/internal/ui/form/integration.go +++ b/internal/ui/form/integration.go @@ -62,6 +62,11 @@ type IntegrationForm struct { LinkdingAPIKey string LinkdingTags string LinkdingMarkAsUnread bool + LinktacoEnabled bool + LinktacoAPIToken string + LinktacoOrgSlug string + LinktacoTags string + LinktacoVisibility string LinkwardenEnabled bool LinkwardenURL string LinkwardenAPIKey string @@ -175,6 +180,11 @@ func (i IntegrationForm) Merge(integration *model.Integration) { integration.LinkdingAPIKey = i.LinkdingAPIKey integration.LinkdingTags = i.LinkdingTags integration.LinkdingMarkAsUnread = i.LinkdingMarkAsUnread + integration.LinktacoEnabled = i.LinktacoEnabled + integration.LinktacoAPIToken = i.LinktacoAPIToken + integration.LinktacoOrgSlug = i.LinktacoOrgSlug + integration.LinktacoTags = i.LinktacoTags + integration.LinktacoVisibility = i.LinktacoVisibility integration.LinkwardenEnabled = i.LinkwardenEnabled integration.LinkwardenURL = i.LinkwardenURL integration.LinkwardenAPIKey = i.LinkwardenAPIKey @@ -290,6 +300,11 @@ func NewIntegrationForm(r *http.Request) *IntegrationForm { LinkdingAPIKey: r.FormValue("linkding_api_key"), LinkdingTags: r.FormValue("linkding_tags"), LinkdingMarkAsUnread: r.FormValue("linkding_mark_as_unread") == "1", + LinktacoEnabled: r.FormValue("linktaco_enabled") == "1", + LinktacoAPIToken: r.FormValue("linktaco_api_token"), + LinktacoOrgSlug: r.FormValue("linktaco_org_slug"), + LinktacoTags: r.FormValue("linktaco_tags"), + LinktacoVisibility: r.FormValue("linktaco_visibility"), LinkwardenEnabled: r.FormValue("linkwarden_enabled") == "1", LinkwardenURL: r.FormValue("linkwarden_url"), LinkwardenAPIKey: r.FormValue("linkwarden_api_key"), diff --git a/internal/ui/integration_show.go b/internal/ui/integration_show.go index afd2571e..d26c02cc 100644 --- a/internal/ui/integration_show.go +++ b/internal/ui/integration_show.go @@ -75,6 +75,11 @@ func (h *handler) showIntegrationPage(w http.ResponseWriter, r *http.Request) { LinkdingAPIKey: integration.LinkdingAPIKey, LinkdingTags: integration.LinkdingTags, LinkdingMarkAsUnread: integration.LinkdingMarkAsUnread, + LinktacoEnabled: integration.LinktacoEnabled, + LinktacoAPIToken: integration.LinktacoAPIToken, + LinktacoOrgSlug: integration.LinktacoOrgSlug, + LinktacoTags: integration.LinktacoTags, + LinktacoVisibility: integration.LinktacoVisibility, LinkwardenEnabled: integration.LinkwardenEnabled, LinkwardenURL: integration.LinkwardenURL, LinkwardenAPIKey: integration.LinkwardenAPIKey, diff --git a/internal/ui/integration_update.go b/internal/ui/integration_update.go index a06568cc..518ed7a1 100644 --- a/internal/ui/integration_update.go +++ b/internal/ui/integration_update.go @@ -75,6 +75,17 @@ func (h *handler) updateIntegration(w http.ResponseWriter, r *http.Request) { integration.WebhookSecret = "" } + if integrationForm.LinktacoEnabled { + if integrationForm.LinktacoAPIToken == "" || integrationForm.LinktacoOrgSlug == "" { + sess.NewFlashErrorMessage(printer.Print("error.linktaco_missing_required_fields")) + html.Redirect(w, r, route.Path(h.router, "integrations")) + return + } + if integration.LinktacoVisibility == "" { + integration.LinktacoVisibility = "PUBLIC" + } + } + err = h.store.UpdateIntegration(integration) if err != nil { html.ServerError(w, r, err)