Parcourir la source

feat(integration): add tags option for karakeep integration

Christian Frommert il y a 6 mois
Parent
commit
b1fda599ac

+ 7 - 0
internal/database/migrations.go

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

+ 7 - 1
internal/integration/integration.go

@@ -457,14 +457,20 @@ func SendEntry(entry *model.Entry, userIntegrations *model.Integration) {
 	if userIntegrations.KarakeepEnabled {
 		slog.Debug("Sending entry to Karakeep",
 			slog.Int64("user_id", userIntegrations.UserID),
+			slog.String("user_tags", userIntegrations.KarakeepTags),
 			slog.Int64("entry_id", entry.ID),
 			slog.String("entry_url", entry.URL),
 		)
 
-		client := karakeep.NewClient(userIntegrations.KarakeepAPIKey, userIntegrations.KarakeepURL)
+		client := karakeep.NewClient(
+			userIntegrations.KarakeepAPIKey,
+			userIntegrations.KarakeepURL,
+			userIntegrations.KarakeepTags,
+		)
 		if err := client.SaveURL(entry.URL); err != nil {
 			slog.Error("Unable to send entry to Karakeep",
 				slog.Int64("user_id", userIntegrations.UserID),
+				slog.String("user_tags", userIntegrations.KarakeepTags),
 				slog.Int64("entry_id", entry.ID),
 				slog.String("entry_url", entry.URL),
 				slog.Any("error", err),

+ 91 - 9
internal/integration/karakeep/karakeep.go

@@ -6,9 +6,11 @@ package karakeep // import "miniflux.app/v2/internal/integration/karakeep"
 import (
 	"bytes"
 	"encoding/json"
+	"errors"
 	"fmt"
 	"io"
 	"net/http"
+	"strings"
 	"time"
 
 	"miniflux.app/v2/internal/version"
@@ -16,9 +18,15 @@ import (
 
 const defaultClientTimeout = 10 * time.Second
 
-type errorResponse struct {
-	Code  string `json:"code"`
-	Error string `json:"error"`
+type Client struct {
+	wrapped     *http.Client
+	apiEndpoint string
+	apiToken    string
+	tags        string
+}
+
+type tagItem struct {
+	TagName string `json:"tagName"`
 }
 
 type saveURLPayload struct {
@@ -26,14 +34,75 @@ type saveURLPayload struct {
 	URL  string `json:"url"`
 }
 
-type Client struct {
-	wrapped     *http.Client
-	apiEndpoint string
-	apiToken    string
+type saveURLResponse struct {
+	ID string `json:"id"`
 }
 
-func NewClient(apiToken string, apiEndpoint string) *Client {
-	return &Client{wrapped: &http.Client{Timeout: defaultClientTimeout}, apiEndpoint: apiEndpoint, apiToken: apiToken}
+type attachTagsPayload struct {
+	Tags []tagItem `json:"tags"`
+}
+
+type errorResponse struct {
+	Code  string `json:"code"`
+	Error string `json:"error"`
+}
+
+func NewClient(apiToken string, apiEndpoint string, tags string) *Client {
+	return &Client{wrapped: &http.Client{Timeout: defaultClientTimeout}, apiEndpoint: apiEndpoint, apiToken: apiToken, tags: tags}
+}
+
+func (c *Client) attachTags(entryID string) error {
+	if c.tags == "" {
+		return nil
+	}
+
+	tagItems := make([]tagItem, 0)
+	for tag := range strings.SplitSeq(c.tags, ",") {
+		if trimmedTag := strings.TrimSpace(tag); trimmedTag != "" {
+			tagItems = append(tagItems, tagItem{TagName: trimmedTag})
+		}
+	}
+
+	if len(tagItems) == 0 {
+		return nil
+	}
+
+	tagRequestBody, err := json.Marshal(&attachTagsPayload{
+		Tags: tagItems,
+	})
+	if err != nil {
+		return fmt.Errorf("karakeep: unable to encode tag request body: %v", err)
+	}
+
+	tagRequest, err := http.NewRequest(http.MethodPost, fmt.Sprintf("%s/%s/tags", c.apiEndpoint, entryID), bytes.NewReader(tagRequestBody))
+	if err != nil {
+		return fmt.Errorf("karakeep: unable to create tag request: %v", err)
+	}
+
+	tagRequest.Header.Set("Authorization", "Bearer "+c.apiToken)
+	tagRequest.Header.Set("Content-Type", "application/json")
+	tagRequest.Header.Set("User-Agent", "Miniflux/"+version.Version)
+
+	tagResponse, err := c.wrapped.Do(tagRequest)
+	if err != nil {
+		return fmt.Errorf("karakeep: unable to send tag request: %v", err)
+	}
+	defer tagResponse.Body.Close()
+
+	if tagResponse.StatusCode != http.StatusOK && tagResponse.StatusCode != http.StatusCreated {
+		tagResponseBody, err := io.ReadAll(tagResponse.Body)
+		if err != nil {
+			return fmt.Errorf("karakeep: failed to parse tag response: %s", err)
+		}
+
+		var errResponse errorResponse
+		if err := json.Unmarshal(tagResponseBody, &errResponse); err != nil {
+			return fmt.Errorf("karakeep: unable to parse tag error response: status=%d body=%s", tagResponse.StatusCode, string(tagResponseBody))
+		}
+		return fmt.Errorf("karakeep: failed to attach tags: status=%d errorcode=%s %s", tagResponse.StatusCode, errResponse.Code, errResponse.Error)
+	}
+
+	return nil
 }
 
 func (c *Client) SaveURL(entryURL string) error {
@@ -77,5 +146,18 @@ func (c *Client) SaveURL(entryURL string) error {
 		return fmt.Errorf("karakeep: failed to save URL: status=%d errorcode=%s %s", resp.StatusCode, errResponse.Code, errResponse.Error)
 	}
 
+	var response saveURLResponse
+	if err := json.Unmarshal(responseBody, &response); err != nil {
+		return fmt.Errorf("karakeep: unable to parse response: %v", err)
+	}
+
+	if response.ID == "" {
+		return errors.New("karakeep: unable to get ID from response")
+	}
+
+	if err := c.attachTags(response.ID); err != nil {
+		return fmt.Errorf("karakeep: unable to attach tags: %v", err)
+	}
+
 	return nil
 }

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

@@ -241,6 +241,7 @@
     "form.integration.karakeep_activate": "Artikel in Karakeep speichern",
     "form.integration.karakeep_api_key": "Karakeep-API-Schlüssel",
     "form.integration.karakeep_url": "Karakeep-API-Endpunkt",
+    "form.integration.karakeep_tags": "Karakeep-Tags",
     "form.integration.linkace_activate": "Artikel in LinkAce speichern",
     "form.integration.linkace_api_key": "LinkAce-API-Schlüssel",
     "form.integration.linkace_check_disabled": "Linkprüfung deaktivieren",

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

@@ -241,6 +241,7 @@
     "form.integration.karakeep_activate": "Αποθήκευση άρθρων στο Karakeep",
     "form.integration.karakeep_api_key": "Κλειδί API Karakeep",
     "form.integration.karakeep_url": "Τελικό σημείο Karakeep API",
+    "form.integration.karakeep_tags": "Ετικέτες Karakeep",
     "form.integration.linkace_activate": "Αποθήκευση καταχωρήσεων στο LinkAce",
     "form.integration.linkace_api_key": "Κλειδί API LinkAce",
     "form.integration.linkace_check_disabled": "Απενεργοποίηση ελέγχου συνδέσμου",

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

@@ -241,6 +241,7 @@
     "form.integration.karakeep_activate": "Save entries to Karakeep",
     "form.integration.karakeep_api_key": "Karakeep API key",
     "form.integration.karakeep_url": "Karakeep API Endpoint",
+    "form.integration.karakeep_tags": "Karakeep Tags",
     "form.integration.linkace_activate": "Save entries to LinkAce",
     "form.integration.linkace_api_key": "LinkAce API key",
     "form.integration.linkace_check_disabled": "Disable link check",

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

@@ -241,6 +241,7 @@
     "form.integration.karakeep_activate": "Enviar artículos a Karakeep",
     "form.integration.karakeep_api_key": "Clave de API de Karakeep",
     "form.integration.karakeep_url": "Acceso API de Karakeep",
+    "form.integration.karakeep_tags": "Etiquetas de Karakeep",
     "form.integration.linkace_activate": "Guardar artículos en LinkAce",
     "form.integration.linkace_api_key": "Clave API de LinkAce",
     "form.integration.linkace_check_disabled": "Deshabilitar la comprobación de enlace",

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

@@ -241,6 +241,7 @@
     "form.integration.karakeep_activate": "Tallenna artikkelit Karakeepiin",
     "form.integration.karakeep_api_key": "Karakeep API-avain",
     "form.integration.karakeep_url": "Karakeep API-päätepiste",
+    "form.integration.karakeep_tags": "Karakeep Tags",
     "form.integration.linkace_activate": "Save entries to LinkAce",
     "form.integration.linkace_api_key": "LinkAce API key",
     "form.integration.linkace_check_disabled": "Disable link check",

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

@@ -241,6 +241,7 @@
     "form.integration.karakeep_activate": "Sauvegarder les articles vers Karakeep",
     "form.integration.karakeep_api_key": "Clé d'API de Karakeep",
     "form.integration.karakeep_url": "URL de l'API de Karakeep",
+    "form.integration.karakeep_tags": "Libellés Karakeep",
     "form.integration.linkace_activate": "Enregistrer les entrées vers LinkAce",
     "form.integration.linkace_api_key": "Clé d'API LinkAce",
     "form.integration.linkace_check_disabled": "Désactiver la vérification des liens",

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

@@ -241,6 +241,7 @@
     "form.integration.karakeep_activate": "Save entries to Karakeep",
     "form.integration.karakeep_api_key": "Karakeep API key",
     "form.integration.karakeep_url": "Karakeep API Endpoint",
+    "form.integration.karakeep_tags": "Karakeep Labels",
     "form.integration.linkace_activate": "Save entries to LinkAce",
     "form.integration.linkace_api_key": "LinkAce API key",
     "form.integration.linkace_check_disabled": "Disable link check",

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

@@ -238,6 +238,7 @@
     "form.integration.karakeep_activate": "Simpan artikel ke Karakeep",
     "form.integration.karakeep_api_key": "Kunci API Karakeep",
     "form.integration.karakeep_url": "Titik URL API Karakeep",
+    "form.integration.karakeep_tags": "Tanda di Karakeep",
     "form.integration.linkace_activate": "Simpan artikel ke LinkAce",
     "form.integration.linkace_api_key": "Kunci API LinkAce",
     "form.integration.linkace_check_disabled": "Matikan pemeriksaan tautan",

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

@@ -241,6 +241,7 @@
     "form.integration.karakeep_activate": "Salva gli articoli su Karakeep",
     "form.integration.karakeep_api_key": "API key dell'account Karakeep",
     "form.integration.karakeep_url": "Endpoint dell'API di Karakeep",
+    "form.integration.karakeep_tags": "Karakeep Tags",
     "form.integration.linkace_activate": "Salva gli articoli su LinkAce",
     "form.integration.linkace_api_key": "API key dell'account LinkAce",
     "form.integration.linkace_check_disabled": "Disabilita i controlli",

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

@@ -238,6 +238,7 @@
     "form.integration.karakeep_activate": "Karakeep に記事を保存する",
     "form.integration.karakeep_api_key": "Karakeep の API key",
     "form.integration.karakeep_url": "Karakeep の API Endpoint",
+    "form.integration.karakeep_tags": "Karakeep の Tags",
     "form.integration.linkace_activate": "Save entries to LinkAce",
     "form.integration.linkace_api_key": "LinkAce API key",
     "form.integration.linkace_check_disabled": "Disable link check",

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

@@ -238,6 +238,7 @@
     "form.integration.karakeep_activate": "Pó-chûn siau-sit kàu Karakeep",
     "form.integration.karakeep_api_key": "Karakeep API só-sî",
     "form.integration.karakeep_url": "Karakeep API thâu",
+    "form.integration.karakeep_tags": "Karakeep khan-á",
     "form.integration.linkace_activate": "Pó-chûn siau-sit kàu LinkAce",
     "form.integration.linkace_api_key": "LinkAce API só-sî",
     "form.integration.linkace_check_disabled": "Thêng iōng liân-kiat kiám-cha",

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

@@ -241,6 +241,7 @@
     "form.integration.karakeep_activate": "Artikelen opslaan in Karakeep",
     "form.integration.karakeep_api_key": "Karakeep API-sleutel",
     "form.integration.karakeep_url": "Karakeep URL",
+    "form.integration.karakeep_tags": "Karakeep tags",
     "form.integration.linkace_activate": "Artikelen opslaan in LinkAce",
     "form.integration.linkace_api_key": "LinkAce API-sleutel",
     "form.integration.linkace_check_disabled": "Koppelingcontrole uitschakelen",

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

@@ -244,6 +244,7 @@
     "form.integration.karakeep_activate": "Zapisuj wpisy w Karakeep",
     "form.integration.karakeep_api_key": "Klucz API do Karakeep",
     "form.integration.karakeep_url": "Punkt końcowy API Karakeep",
+    "form.integration.karakeep_tags": "Znaczniki Karakeep",
     "form.integration.linkace_activate": "Zapisuj wpisy w LinkAce",
     "form.integration.linkace_api_key": "Klucz API do LinkAce",
     "form.integration.linkace_check_disabled": "Wyłącz sprawdzanie łączy",

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

@@ -241,6 +241,7 @@
     "form.integration.karakeep_activate": "Salvar itens no Karakeep",
     "form.integration.karakeep_api_key": "Chave de API do Karakeep",
     "form.integration.karakeep_url": "Endpoint de API do Karakeep",
+    "form.integration.karakeep_tags": "Karakeep Tags",
     "form.integration.linkace_activate": "Salvar itens no LinkAce",
     "form.integration.linkace_api_key": "Chave de API do LinkAce",
     "form.integration.linkace_check_disabled": "Desativar verificação de link",

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

@@ -244,6 +244,7 @@
     "form.integration.karakeep_activate": "Salvare înregistrări în Karakeep",
     "form.integration.karakeep_api_key": "Cheie API Karakeep",
     "form.integration.karakeep_url": "Punct acces API Karakeep",
+    "form.integration.karakeep_tags": "Karakeep Tags",
     "form.integration.linkace_activate": "Salvează intrările în LinkAce",
     "form.integration.linkace_api_key": "Cheie API LinkAce",
     "form.integration.linkace_check_disabled": "Dezactivează verificarea link-urilor",

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

@@ -244,6 +244,7 @@
     "form.integration.karakeep_activate": "Сохранять статьи в Karakeep",
     "form.integration.karakeep_api_key": "API-ключ Karakeep",
     "form.integration.karakeep_url": "Конечная точка Karakeep API",
+    "form.integration.karakeep_tags": "Karakeep Tags",
     "form.integration.linkace_activate": "Сохранять статьи в LinkAce",
     "form.integration.linkace_api_key": "API-ключ LinkAce",
     "form.integration.linkace_check_disabled": "Отключить проверку ссылок",

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

@@ -241,6 +241,7 @@
     "form.integration.karakeep_activate": "Makaleleri Karakeep'a kaydet",
     "form.integration.karakeep_api_key": "Karakeep API anahtarı",
     "form.integration.karakeep_url": "Karakeep API Uç Noktası",
+    "form.integration.karakeep_tags": "Karakeep Tags",
     "form.integration.linkace_activate": "Makaleleri LinkAce'e kaydet",
     "form.integration.linkace_api_key": "LinkAce API anahtarı",
     "form.integration.linkace_check_disabled": "Link kontrolünü devre dışı bırak",

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

@@ -244,6 +244,7 @@
     "form.integration.karakeep_activate": "Зберігати статті до Karakeep",
     "form.integration.karakeep_api_key": "Ключ API Karakeep",
     "form.integration.karakeep_url": "Karakeep API Endpoint",
+    "form.integration.karakeep_tags": "Karakeep Tags",
     "form.integration.linkace_activate": "Save entries to LinkAce",
     "form.integration.linkace_api_key": "LinkAce API key",
     "form.integration.linkace_check_disabled": "Disable link check",

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

@@ -238,6 +238,7 @@
     "form.integration.karakeep_activate": "保存条目到 Karakeep",
     "form.integration.karakeep_api_key": "Karakeep API 密钥",
     "form.integration.karakeep_url": "Karakeep API 端点",
+    "form.integration.karakeep_tags": "Karakeep 标签",
     "form.integration.linkace_activate": "保存条目到 LinkAce",
     "form.integration.linkace_api_key": "LinkAce API 密钥",
     "form.integration.linkace_check_disabled": "禁用链接检查",

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

@@ -238,6 +238,7 @@
     "form.integration.karakeep_activate": "儲存文章到 Karakeep",
     "form.integration.karakeep_api_key": "Karakeep API 金鑰",
     "form.integration.karakeep_url": "Karakeep API 端點",
+    "form.integration.karakeep_tags": "Karakeep 標籤",
     "form.integration.linkace_activate": "儲存文章到 LinkAce",
     "form.integration.linkace_api_key": "LinkAce API 金鑰",
     "form.integration.linkace_check_disabled": "停用連結檢查",

+ 1 - 0
internal/model/integration.go

@@ -100,6 +100,7 @@ type Integration struct {
 	KarakeepEnabled                  bool
 	KarakeepAPIKey                   string
 	KarakeepURL                      string
+	KarakeepTags                     string
 	RaindropEnabled                  bool
 	RaindropToken                    string
 	RaindropCollectionID             string

+ 11 - 7
internal/storage/integration.go

@@ -222,6 +222,7 @@ func (s *Storage) Integration(userID int64) (*model.Integration, error) {
 			karakeep_enabled,
 			karakeep_api_key,
 			karakeep_url,
+			karakeep_tags,
 			linktaco_enabled,
 			linktaco_api_token,
 			linktaco_org_slug,
@@ -348,6 +349,7 @@ func (s *Storage) Integration(userID int64) (*model.Integration, error) {
 		&integration.KarakeepEnabled,
 		&integration.KarakeepAPIKey,
 		&integration.KarakeepURL,
+		&integration.KarakeepTags,
 		&integration.LinktacoEnabled,
 		&integration.LinktacoAPIToken,
 		&integration.LinktacoOrgSlug,
@@ -483,14 +485,15 @@ func (s *Storage) UpdateIntegration(integration *model.Integration) error {
 			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,
-			archiveorg_enabled=$118
+			karakeep_tags=$113,
+			linktaco_enabled=$114,
+			linktaco_api_token=$115,
+			linktaco_org_slug=$116,
+			linktaco_tags=$117,
+			linktaco_visibility=$118,
+			archiveorg_enabled=$119
 		WHERE
-			user_id=$119
+			user_id=$120
 	`
 	_, err := s.db.Exec(
 		query,
@@ -606,6 +609,7 @@ func (s *Storage) UpdateIntegration(integration *model.Integration) error {
 		integration.KarakeepEnabled,
 		integration.KarakeepAPIKey,
 		integration.KarakeepURL,
+		integration.KarakeepTags,
 		integration.LinktacoEnabled,
 		integration.LinktacoAPIToken,
 		integration.LinktacoOrgSlug,

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

@@ -420,6 +420,9 @@
             <label for="form-karakeep-url">{{ t "form.integration.karakeep_url" }}</label>
             <input type="url" name="karakeep_url" id="form-karakeep-url" value="{{ .form.KarakeepURL }}" placeholder="https://try.karakeep.app/api/v1/bookmarks" spellcheck="false">
 
+            <label for="form-karakeep-tags">{{ t "form.integration.karakeep_tags" }}</label>
+            <input type="text" name="karakeep_tags" id="form-karakeep-tags" value="{{ .form.KarakeepTags }}" placeholder="miniflux, new" 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

@@ -103,6 +103,7 @@ type IntegrationForm struct {
 	KarakeepEnabled                  bool
 	KarakeepAPIKey                   string
 	KarakeepURL                      string
+	KarakeepTags                     string
 	RaindropEnabled                  bool
 	RaindropToken                    string
 	RaindropCollectionID             string
@@ -222,6 +223,7 @@ func (i IntegrationForm) Merge(integration *model.Integration) {
 	integration.KarakeepEnabled = i.KarakeepEnabled
 	integration.KarakeepAPIKey = i.KarakeepAPIKey
 	integration.KarakeepURL = i.KarakeepURL
+	integration.KarakeepTags = i.KarakeepTags
 	integration.RaindropEnabled = i.RaindropEnabled
 	integration.RaindropToken = i.RaindropToken
 	integration.RaindropCollectionID = i.RaindropCollectionID
@@ -344,6 +346,7 @@ func NewIntegrationForm(r *http.Request) *IntegrationForm {
 		KarakeepEnabled:                  r.FormValue("karakeep_enabled") == "1",
 		KarakeepAPIKey:                   r.FormValue("karakeep_api_key"),
 		KarakeepURL:                      r.FormValue("karakeep_url"),
+		KarakeepTags:                     r.FormValue("karakeep_tags"),
 		RaindropEnabled:                  r.FormValue("raindrop_enabled") == "1",
 		RaindropToken:                    r.FormValue("raindrop_token"),
 		RaindropCollectionID:             r.FormValue("raindrop_collection_id"),

+ 1 - 0
internal/ui/integration_show.go

@@ -116,6 +116,7 @@ func (h *handler) showIntegrationPage(w http.ResponseWriter, r *http.Request) {
 		KarakeepEnabled:                  integration.KarakeepEnabled,
 		KarakeepAPIKey:                   integration.KarakeepAPIKey,
 		KarakeepURL:                      integration.KarakeepURL,
+		KarakeepTags:                     integration.KarakeepTags,
 		RaindropEnabled:                  integration.RaindropEnabled,
 		RaindropToken:                    integration.RaindropToken,
 		RaindropCollectionID:             integration.RaindropCollectionID,