From 5e520ca5bf06db39592728955d8e31773d5b7c4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Guillot?= Date: Sun, 13 Aug 2023 21:58:45 -0700 Subject: [PATCH] Use stdlib HTTP client for third-party integrations --- internal/integration/apprise/apprise.go | 63 +++++----- internal/integration/apprise/wrapper.go | 9 -- internal/integration/espial/espial.go | 70 +++++++---- internal/integration/instapaper/instapaper.go | 40 +++--- internal/integration/integration.go | 22 ++-- internal/integration/linkding/linkding.go | 67 ++++++---- internal/integration/notion/notion.go | 84 +++++++++---- internal/integration/notion/wrapper.go | 19 --- .../integration/nunuxkeeper/nunuxkeeper.go | 66 ++++++---- internal/integration/pinboard/pinboard.go | 37 ++++-- internal/integration/pocket/connector.go | 116 +++++++++++------- internal/integration/pocket/pocket.go | 62 ++++++---- internal/integration/readwise/readwise.go | 59 ++++----- internal/integration/shaarli/shaarli.go | 4 +- internal/integration/shiori/shiori.go | 7 +- internal/integration/wallabag/wallabag.go | 102 +++++++++------ internal/locale/translations/de_DE.json | 2 +- internal/locale/translations/el_EL.json | 2 +- internal/locale/translations/en_US.json | 2 +- internal/locale/translations/es_ES.json | 2 +- internal/locale/translations/fi_FI.json | 2 +- internal/locale/translations/fr_FR.json | 6 +- internal/locale/translations/hi_IN.json | 2 +- internal/locale/translations/id_ID.json | 2 +- internal/locale/translations/it_IT.json | 2 +- internal/locale/translations/ja_JP.json | 2 +- internal/locale/translations/nl_NL.json | 2 +- internal/locale/translations/pl_PL.json | 2 +- internal/locale/translations/pt_BR.json | 2 +- internal/locale/translations/ru_RU.json | 2 +- internal/locale/translations/tr_TR.json | 2 +- internal/locale/translations/uk_UA.json | 2 +- internal/locale/translations/zh_CN.json | 2 +- internal/locale/translations/zh_TW.json | 2 +- 34 files changed, 509 insertions(+), 358 deletions(-) delete mode 100644 internal/integration/apprise/wrapper.go delete mode 100644 internal/integration/notion/wrapper.go diff --git a/internal/integration/apprise/apprise.go b/internal/integration/apprise/apprise.go index 3d1c40a2..a74c8f1f 100644 --- a/internal/integration/apprise/apprise.go +++ b/internal/integration/apprise/apprise.go @@ -4,57 +4,64 @@ package apprise import ( + "bytes" + "encoding/json" "fmt" - "net" - "strings" + "net/http" "time" - "miniflux.app/v2/internal/http/client" "miniflux.app/v2/internal/model" "miniflux.app/v2/internal/urllib" + "miniflux.app/v2/internal/version" ) -const defaultClientTimeout = 1 * time.Second +const defaultClientTimeout = 10 * time.Second -// Client represents a Apprise client. type Client struct { servicesURL string baseURL string } -// NewClient returns a new Apprise client. func NewClient(serviceURL, baseURL string) *Client { return &Client{serviceURL, baseURL} } -// PushEntry pushes entry to apprise -func (c *Client) PushEntry(entry *model.Entry) error { +func (c *Client) SendNotification(entry *model.Entry) error { if c.baseURL == "" || c.servicesURL == "" { return fmt.Errorf("apprise: missing base URL or service URL") } - _, err := net.DialTimeout("tcp", c.baseURL, defaultClientTimeout) + + message := "[" + entry.Title + "]" + "(" + entry.URL + ")" + "\n\n" + apiEndpoint, err := urllib.JoinBaseURLAndPath(c.baseURL, "/notify") if err != nil { - apiEndpoint, err := urllib.JoinBaseURLAndPath(c.baseURL, "/notify") - if err != nil { - return fmt.Errorf(`apprise: invalid API endpoint: %v`, err) - } + return fmt.Errorf(`apprise: invalid API endpoint: %v`, err) + } - clt := client.New(apiEndpoint) - message := "[" + entry.Title + "]" + "(" + entry.URL + ")" + "\n\n" - data := &Data{ - Urls: c.servicesURL, - Body: message, - } - response, error := clt.PostJSON(data) - if error != nil { - return fmt.Errorf("apprise: ending message failed: %v", error) - } + requestBody, err := json.Marshal(map[string]any{ + "urls": c.servicesURL, + "body": message, + }) + if err != nil { + return fmt.Errorf("apprise: unable to encode request body: %v", err) + } - if response.HasServerFailure() { - return fmt.Errorf("apprise: request failed, status=%d", response.StatusCode) - } - } else { - return fmt.Errorf("%s %s %s", c.baseURL, "responding on port:", strings.Split(c.baseURL, ":")[1]) + request, err := http.NewRequest(http.MethodPost, apiEndpoint, bytes.NewReader(requestBody)) + if err != nil { + return fmt.Errorf("apprise: unable to create request: %v", err) + } + + request.Header.Set("Content-Type", "application/json") + request.Header.Set("User-Agent", "Miniflux/"+version.Version) + + httpClient := &http.Client{Timeout: defaultClientTimeout} + response, err := httpClient.Do(request) + if err != nil { + return fmt.Errorf("apprise: unable to send request: %v", err) + } + defer response.Body.Close() + + if response.StatusCode >= 400 { + return fmt.Errorf("apprise: unable to send a notification: url=%s status=%d", apiEndpoint, response.StatusCode) } return nil diff --git a/internal/integration/apprise/wrapper.go b/internal/integration/apprise/wrapper.go deleted file mode 100644 index 8b8bac7d..00000000 --- a/internal/integration/apprise/wrapper.go +++ /dev/null @@ -1,9 +0,0 @@ -// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package apprise - -type Data struct { - Urls string `json:"urls"` - Body string `json:"body"` -} diff --git a/internal/integration/espial/espial.go b/internal/integration/espial/espial.go index b7a9cc70..06fff116 100644 --- a/internal/integration/espial/espial.go +++ b/internal/integration/espial/espial.go @@ -4,59 +4,77 @@ package espial // import "miniflux.app/v2/internal/integration/espial" import ( + "bytes" + "encoding/json" "fmt" + "net/http" + "time" - "miniflux.app/v2/internal/http/client" "miniflux.app/v2/internal/urllib" + "miniflux.app/v2/internal/version" ) -// Document structure of an Espial document -type Document struct { - Title string `json:"title,omitempty"` - Url string `json:"url,omitempty"` - ToRead bool `json:"toread,omitempty"` - Tags string `json:"tags,omitempty"` -} +const defaultClientTimeout = 10 * time.Second -// Client represents an Espial client. type Client struct { baseURL string apiKey string } -// NewClient returns a new Espial client. func NewClient(baseURL, apiKey string) *Client { return &Client{baseURL: baseURL, apiKey: apiKey} } -// AddEntry sends an entry to Espial. -func (c *Client) AddEntry(link, title, content, tags string) error { +func (c *Client) CreateLink(entryURL, entryTitle, espialTags string) error { if c.baseURL == "" || c.apiKey == "" { return fmt.Errorf("espial: missing base URL or API key") } - doc := &Document{ - Title: title, - Url: link, - ToRead: true, - Tags: tags, - } - apiEndpoint, err := urllib.JoinBaseURLAndPath(c.baseURL, "/api/add") if err != nil { - return fmt.Errorf(`espial: invalid API endpoint: %v`, err) + return fmt.Errorf("espial: invalid API endpoint: %v", err) } - clt := client.New(apiEndpoint) - clt.WithAuthorization("ApiKey " + c.apiKey) - response, err := clt.PostJSON(doc) + requestBody, err := json.Marshal(&espialDocument{ + Title: entryTitle, + Url: entryURL, + ToRead: true, + Tags: espialTags, + }) + if err != nil { - return fmt.Errorf("espial: unable to send entry: %v", err) + return fmt.Errorf("espial: unable to encode request body: %v", err) } - if response.HasServerFailure() { - return fmt.Errorf("espial: unable to send entry, status=%d", response.StatusCode) + request, err := http.NewRequest(http.MethodPost, apiEndpoint, bytes.NewReader(requestBody)) + if err != nil { + return fmt.Errorf("espial: 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", "ApiKey "+c.apiKey) + + httpClient := &http.Client{Timeout: defaultClientTimeout} + response, err := httpClient.Do(request) + if err != nil { + return fmt.Errorf("espial: unable to send request: %v", err) + } + defer response.Body.Close() + + if response.StatusCode != http.StatusCreated { + responseBody := new(bytes.Buffer) + responseBody.ReadFrom(response.Body) + + return fmt.Errorf("espial: unable to create link: url=%s status=%d body=%s", apiEndpoint, response.StatusCode, responseBody.String()) } return nil } + +type espialDocument struct { + Title string `json:"title,omitempty"` + Url string `json:"url,omitempty"` + ToRead bool `json:"toread,omitempty"` + Tags string `json:"tags,omitempty"` +} diff --git a/internal/integration/instapaper/instapaper.go b/internal/integration/instapaper/instapaper.go index e593e24e..7114302d 100644 --- a/internal/integration/instapaper/instapaper.go +++ b/internal/integration/instapaper/instapaper.go @@ -5,42 +5,52 @@ package instapaper // import "miniflux.app/v2/internal/integration/instapaper" import ( "fmt" + "net/http" "net/url" + "time" - "miniflux.app/v2/internal/http/client" + "miniflux.app/v2/internal/version" ) -// Client represents an Instapaper client. +const defaultClientTimeout = 10 * time.Second + type Client struct { username string password string } -// NewClient returns a new Instapaper client. func NewClient(username, password string) *Client { return &Client{username: username, password: password} } -// AddURL sends a link to Instapaper. -func (c *Client) AddURL(link, title string) error { +func (c *Client) AddURL(entryURL, entryTitle string) error { if c.username == "" || c.password == "" { - return fmt.Errorf("instapaper: missing credentials") + return fmt.Errorf("instapaper: missing username or password") } values := url.Values{} - values.Add("url", link) - values.Add("title", title) + values.Add("url", entryURL) + values.Add("title", entryTitle) - apiURL := "https://www.instapaper.com/api/add?" + values.Encode() - clt := client.New(apiURL) - clt.WithCredentials(c.username, c.password) - response, err := clt.Get() + apiEndpoint := "https://www.instapaper.com/api/add?" + values.Encode() + request, err := http.NewRequest(http.MethodGet, apiEndpoint, nil) if err != nil { - return fmt.Errorf("instapaper: unable to send url: %v", err) + return fmt.Errorf("instapaper: unable to create request: %v", err) } - if response.HasServerFailure() { - return fmt.Errorf("instapaper: unable to send url, status=%d", response.StatusCode) + request.SetBasicAuth(c.username, c.password) + request.Header.Set("Content-Type", "application/x-www-form-urlencoded") + request.Header.Set("User-Agent", "Miniflux/"+version.Version) + + httpClient := &http.Client{Timeout: defaultClientTimeout} + response, err := httpClient.Do(request) + if err != nil { + return fmt.Errorf("instapaper: unable to send request: %v", err) + } + defer response.Body.Close() + + if response.StatusCode != http.StatusCreated { + return fmt.Errorf("instapaper: unable to add URL: url=%s status=%d", apiEndpoint, response.StatusCode) } return nil diff --git a/internal/integration/integration.go b/internal/integration/integration.go index 98bda702..7385950c 100644 --- a/internal/integration/integration.go +++ b/internal/integration/integration.go @@ -29,7 +29,7 @@ func SendEntry(entry *model.Entry, integration *model.Integration) { logger.Debug("[Integration] Sending entry #%d %q for user #%d to Pinboard", entry.ID, entry.URL, integration.UserID) client := pinboard.NewClient(integration.PinboardToken) - err := client.AddBookmark( + err := client.CreateBookmark( entry.URL, entry.Title, integration.PinboardTags, @@ -62,7 +62,7 @@ func SendEntry(entry *model.Entry, integration *model.Integration) { integration.WallabagOnlyURL, ) - if err := client.AddEntry(entry.URL, entry.Title, entry.Content); err != nil { + if err := client.CreateEntry(entry.URL, entry.Title, entry.Content); err != nil { logger.Error("[Integration] UserID #%d: %v", integration.UserID, err) } } @@ -74,7 +74,7 @@ func SendEntry(entry *model.Entry, integration *model.Integration) { integration.NotionToken, integration.NotionPageID, ) - if err := client.AddEntry(entry.URL, entry.Title); err != nil { + if err := client.UpdateDocument(entry.URL, entry.Title); err != nil { logger.Error("[Integration] UserID #%d: %v", integration.UserID, err) } } @@ -100,8 +100,8 @@ func SendEntry(entry *model.Entry, integration *model.Integration) { integration.EspialAPIKey, ) - if err := client.AddEntry(entry.URL, entry.Title, entry.Content, integration.EspialTags); err != nil { - logger.Error("[Integration] UserID #%d: %v", integration.UserID, err) + if err := client.CreateLink(entry.URL, entry.Title, integration.EspialTags); err != nil { + logger.Error("[Integration] Unable to send entry #%d to Espial for user #%d: %v", entry.ID, integration.UserID, err) } } @@ -123,7 +123,7 @@ func SendEntry(entry *model.Entry, integration *model.Integration) { integration.LinkdingTags, integration.LinkdingMarkAsUnread, ) - if err := client.AddEntry(entry.Title, entry.URL); err != nil { + if err := client.CreateBookmark(entry.URL, entry.Title); err != nil { logger.Error("[Integration] UserID #%d: %v", integration.UserID, err) } } @@ -135,7 +135,7 @@ func SendEntry(entry *model.Entry, integration *model.Integration) { integration.ReadwiseAPIKey, ) - if err := client.AddEntry(entry.URL); err != nil { + if err := client.CreateDocument(entry.URL); err != nil { logger.Error("[Integration] UserID #%d: %v", integration.UserID, err) } } @@ -149,7 +149,7 @@ func SendEntry(entry *model.Entry, integration *model.Integration) { integration.ShioriPassword, ) - if err := client.AddBookmark(entry.URL, entry.Title); err != nil { + if err := client.CreateBookmark(entry.URL, entry.Title); err != nil { logger.Error("[Integration] Unable to send entry #%d to Shiori for user #%d: %v", entry.ID, integration.UserID, err) } } @@ -162,7 +162,7 @@ func SendEntry(entry *model.Entry, integration *model.Integration) { integration.ShaarliAPISecret, ) - if err := client.AddLink(entry.URL, entry.Title); err != nil { + if err := client.CreateLink(entry.URL, entry.Title); err != nil { logger.Error("[Integration] Unable to send entry #%d to Shaarli for user #%d: %v", entry.ID, integration.UserID, err) } } @@ -197,8 +197,8 @@ func PushEntry(entry *model.Entry, integration *model.Integration) { integration.AppriseServicesURL, integration.AppriseURL, ) - err := client.PushEntry(entry) - if err != nil { + + if err := client.SendNotification(entry); err != nil { logger.Error("[Integration] push entry to apprise failed: %v", err) } } diff --git a/internal/integration/linkding/linkding.go b/internal/integration/linkding/linkding.go index 9cbd3cc5..20c68557 100644 --- a/internal/integration/linkding/linkding.go +++ b/internal/integration/linkding/linkding.go @@ -4,22 +4,19 @@ package linkding // import "miniflux.app/v2/internal/integration/linkding" import ( + "bytes" + "encoding/json" "fmt" + "net/http" "strings" + "time" - "miniflux.app/v2/internal/http/client" "miniflux.app/v2/internal/urllib" + "miniflux.app/v2/internal/version" ) -// Document structure of a Linkding document -type Document struct { - Url string `json:"url,omitempty"` - Title string `json:"title,omitempty"` - TagNames []string `json:"tag_names,omitempty"` - Unread bool `json:"unread,omitempty"` -} +const defaultClientTimeout = 10 * time.Second -// Client represents an Linkding client. type Client struct { baseURL string apiKey string @@ -27,43 +24,61 @@ type Client struct { unread bool } -// NewClient returns a new Linkding client. func NewClient(baseURL, apiKey, tags string, unread bool) *Client { return &Client{baseURL: baseURL, apiKey: apiKey, tags: tags, unread: unread} } -// AddEntry sends an entry to Linkding. -func (c *Client) AddEntry(title, entryURL string) error { +func (c *Client) CreateBookmark(entryURL, entryTitle string) error { if c.baseURL == "" || c.apiKey == "" { - return fmt.Errorf("linkding: missing credentials") + return fmt.Errorf("linkding: missing base URL or API key") } tagsSplitFn := func(c rune) bool { return c == ',' || c == ' ' } - doc := &Document{ - Url: entryURL, - Title: title, - TagNames: strings.FieldsFunc(c.tags, tagsSplitFn), - Unread: c.unread, - } - apiEndpoint, err := urllib.JoinBaseURLAndPath(c.baseURL, "/api/bookmarks/") if err != nil { return fmt.Errorf(`linkding: invalid API endpoint: %v`, err) } - clt := client.New(apiEndpoint) - clt.WithAuthorization("Token " + c.apiKey) - response, err := clt.PostJSON(doc) + requestBody, err := json.Marshal(&linkdingBookmark{ + Url: entryURL, + Title: entryTitle, + TagNames: strings.FieldsFunc(c.tags, tagsSplitFn), + Unread: c.unread, + }) + if err != nil { - return fmt.Errorf("linkding: unable to send entry: %v", err) + return fmt.Errorf("linkding: unable to encode request body: %v", err) } - if response.HasServerFailure() { - return fmt.Errorf("linkding: unable to send entry, status=%d", response.StatusCode) + request, err := http.NewRequest(http.MethodPost, apiEndpoint, bytes.NewReader(requestBody)) + if err != nil { + return fmt.Errorf("linkding: 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", "Token "+c.apiKey) + + httpClient := &http.Client{Timeout: defaultClientTimeout} + response, err := httpClient.Do(request) + if err != nil { + return fmt.Errorf("linkding: unable to send request: %v", err) + } + defer response.Body.Close() + + if response.StatusCode >= 400 { + return fmt.Errorf("linkding: unable to create bookmark: url=%s status=%d", apiEndpoint, response.StatusCode) } return nil } + +type linkdingBookmark struct { + Url string `json:"url,omitempty"` + Title string `json:"title,omitempty"` + TagNames []string `json:"tag_names,omitempty"` + Unread bool `json:"unread,omitempty"` +} diff --git a/internal/integration/notion/notion.go b/internal/integration/notion/notion.go index fe579928..7026877e 100644 --- a/internal/integration/notion/notion.go +++ b/internal/integration/notion/notion.go @@ -4,51 +4,83 @@ package notion import ( + "bytes" + "encoding/json" "fmt" + "net/http" + "time" - "miniflux.app/v2/internal/http/client" + "miniflux.app/v2/internal/version" ) -// Client represents a Notion client. +const defaultClientTimeout = 10 * time.Second + type Client struct { - token string - pageID string + apiToken string + pageID string } -// NewClient returns a new Notion client. -func NewClient(token, pageID string) *Client { - return &Client{token, pageID} +func NewClient(apiToken, pageID string) *Client { + return &Client{apiToken, pageID} } -func (c *Client) AddEntry(entryURL string, entryTitle string) error { - if c.token == "" || c.pageID == "" { - return fmt.Errorf("notion: missing credentials") +func (c *Client) UpdateDocument(entryURL string, entryTitle string) error { + if c.apiToken == "" || c.pageID == "" { + return fmt.Errorf("notion: missing API token or page ID") } - clt := client.New("https://api.notion.com/v1/blocks/" + c.pageID + "/children") - block := &Data{ - Children: []Block{ + + apiEndpoint := "https://api.notion.com/v1/blocks/" + c.pageID + "/children" + requestBody, err := json.Marshal(¬ionDocument{ + Children: []block{ { Object: "block", Type: "bookmark", - Bookmark: Bookmark{ - Caption: []interface{}{}, + Bookmark: bookmarkObject{ + Caption: []any{}, URL: entryURL, }, }, }, - } - clt.WithAuthorization("Bearer " + c.token) - customHeaders := map[string]string{ - "Notion-Version": "2022-06-28", - } - clt.WithCustomHeaders(customHeaders) - response, error := clt.PatchJSON(block) - if error != nil { - return fmt.Errorf("notion: unable to patch entry: %v", error) + }) + if err != nil { + return fmt.Errorf("notion: unable to encode request body: %v", err) } - if response.HasServerFailure() { - return fmt.Errorf("notion: request failed, status=%d", response.StatusCode) + request, err := http.NewRequest(http.MethodPatch, apiEndpoint, bytes.NewReader(requestBody)) + if err != nil { + return fmt.Errorf("notion: unable to create request: %v", err) } + + request.Header.Set("Content-Type", "application/json") + request.Header.Set("User-Agent", "Miniflux/"+version.Version) + request.Header.Set("Notion-Version", "2022-06-28") + request.Header.Set("Authorization", "Bearer "+c.apiToken) + + httpClient := &http.Client{Timeout: defaultClientTimeout} + response, err := httpClient.Do(request) + if err != nil { + return fmt.Errorf("notion: unable to send request: %v", err) + } + defer response.Body.Close() + + if response.StatusCode != http.StatusOK { + return fmt.Errorf("notion: unable to update document: url=%s status=%d", apiEndpoint, response.StatusCode) + } + return nil } + +type notionDocument struct { + Children []block `json:"children"` +} + +type block struct { + Object string `json:"object"` + Type string `json:"type"` + Bookmark bookmarkObject `json:"bookmark"` +} + +type bookmarkObject struct { + Caption []any `json:"caption"` + URL string `json:"url"` +} diff --git a/internal/integration/notion/wrapper.go b/internal/integration/notion/wrapper.go deleted file mode 100644 index d37633c0..00000000 --- a/internal/integration/notion/wrapper.go +++ /dev/null @@ -1,19 +0,0 @@ -// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package notion - -type Data struct { - Children []Block `json:"children"` -} - -type Block struct { - Object string `json:"object"` - Type string `json:"type"` - Bookmark Bookmark `json:"bookmark"` -} - -type Bookmark struct { - Caption []interface{} `json:"caption"` // Assuming the "caption" field can have different types - URL string `json:"url"` -} diff --git a/internal/integration/nunuxkeeper/nunuxkeeper.go b/internal/integration/nunuxkeeper/nunuxkeeper.go index 7c8d4d78..0352887f 100644 --- a/internal/integration/nunuxkeeper/nunuxkeeper.go +++ b/internal/integration/nunuxkeeper/nunuxkeeper.go @@ -4,42 +4,30 @@ package nunuxkeeper // import "miniflux.app/v2/internal/integration/nunuxkeeper" import ( + "bytes" + "encoding/json" "fmt" + "net/http" + "time" - "miniflux.app/v2/internal/http/client" "miniflux.app/v2/internal/urllib" + "miniflux.app/v2/internal/version" ) -// Document structure of a Nununx Keeper document -type Document struct { - Title string `json:"title,omitempty"` - Origin string `json:"origin,omitempty"` - Content string `json:"content,omitempty"` - ContentType string `json:"contentType,omitempty"` -} +const defaultClientTimeout = 10 * time.Second -// Client represents an Nunux Keeper client. type Client struct { baseURL string apiKey string } -// NewClient returns a new Nunux Keeepr client. func NewClient(baseURL, apiKey string) *Client { return &Client{baseURL: baseURL, apiKey: apiKey} } -// AddEntry sends an entry to Nunux Keeper. -func (c *Client) AddEntry(link, title, content string) error { +func (c *Client) AddEntry(entryURL, entryTitle, entryContent string) error { if c.baseURL == "" || c.apiKey == "" { - return fmt.Errorf("nunux-keeper: missing credentials") - } - - doc := &Document{ - Title: title, - Origin: link, - Content: content, - ContentType: "text/html", + return fmt.Errorf("nunux-keeper: missing base URL or API key") } apiEndpoint, err := urllib.JoinBaseURLAndPath(c.baseURL, "/v2/documents") @@ -47,16 +35,42 @@ func (c *Client) AddEntry(link, title, content string) error { return fmt.Errorf(`nunux-keeper: invalid API endpoint: %v`, err) } - clt := client.New(apiEndpoint) - clt.WithCredentials("api", c.apiKey) - response, err := clt.PostJSON(doc) + requestBody, err := json.Marshal(&nunuxKeeperDocument{ + Title: entryTitle, + Origin: entryURL, + Content: entryContent, + ContentType: "text/html", + }) if err != nil { - return fmt.Errorf("nunux-keeper: unable to send entry: %v", err) + return fmt.Errorf("notion: unable to encode request body: %v", err) } - if response.HasServerFailure() { - return fmt.Errorf("nunux-keeper: unable to send entry, status=%d", response.StatusCode) + request, err := http.NewRequest(http.MethodPost, apiEndpoint, bytes.NewReader(requestBody)) + if err != nil { + return fmt.Errorf("nunux-keeper: unable to create request: %v", err) + } + + request.SetBasicAuth("api", c.apiKey) + request.Header.Set("Content-Type", "application/json") + request.Header.Set("User-Agent", "Miniflux/"+version.Version) + + httpClient := &http.Client{Timeout: defaultClientTimeout} + response, err := httpClient.Do(request) + if err != nil { + return fmt.Errorf("nunux-keeper: unable to send request: %v", err) + } + defer response.Body.Close() + + if response.StatusCode != http.StatusOK { + return fmt.Errorf("nunux-keeper: unable to create document: url=%s status=%d", apiEndpoint, response.StatusCode) } return nil } + +type nunuxKeeperDocument struct { + Title string `json:"title,omitempty"` + Origin string `json:"origin,omitempty"` + Content string `json:"content,omitempty"` + ContentType string `json:"contentType,omitempty"` +} diff --git a/internal/integration/pinboard/pinboard.go b/internal/integration/pinboard/pinboard.go index 0977c703..a0cb6a51 100644 --- a/internal/integration/pinboard/pinboard.go +++ b/internal/integration/pinboard/pinboard.go @@ -5,23 +5,24 @@ package pinboard // import "miniflux.app/v2/internal/integration/pinboard" import ( "fmt" + "net/http" "net/url" + "time" - "miniflux.app/v2/internal/http/client" + "miniflux.app/v2/internal/version" ) -// Client represents a Pinboard client. +const defaultClientTimeout = 10 * time.Second + type Client struct { authToken string } -// NewClient returns a new Pinboard client. func NewClient(authToken string) *Client { return &Client{authToken: authToken} } -// AddBookmark sends a link to Pinboard. -func (c *Client) AddBookmark(link, title, tags string, markAsUnread bool) error { +func (c *Client) CreateBookmark(entryURL, entryTitle, pinboardTags string, markAsUnread bool) error { if c.authToken == "" { return fmt.Errorf("pinboard: missing auth token") } @@ -33,19 +34,29 @@ func (c *Client) AddBookmark(link, title, tags string, markAsUnread bool) error values := url.Values{} values.Add("auth_token", c.authToken) - values.Add("url", link) - values.Add("description", title) - values.Add("tags", tags) + values.Add("url", entryURL) + values.Add("description", entryTitle) + values.Add("tags", pinboardTags) values.Add("toread", toRead) - clt := client.New("https://api.pinboard.in/v1/posts/add?" + values.Encode()) - response, err := clt.Get() + apiEndpoint := "https://api.pinboard.in/v1/posts/add?" + values.Encode() + request, err := http.NewRequest(http.MethodGet, apiEndpoint, nil) if err != nil { - return fmt.Errorf("pinboard: unable to send bookmark: %v", err) + return fmt.Errorf("pinboard: unable to create request: %v", err) } - if response.HasServerFailure() { - return fmt.Errorf("pinboard: unable to send bookmark, status=%d", response.StatusCode) + request.Header.Set("Content-Type", "application/x-www-form-urlencoded") + request.Header.Set("User-Agent", "Miniflux/"+version.Version) + + httpClient := &http.Client{Timeout: defaultClientTimeout} + response, err := httpClient.Do(request) + if err != nil { + return fmt.Errorf("pinboard: unable to send request: %v", err) + } + defer response.Body.Close() + + if response.StatusCode >= 400 { + return fmt.Errorf("pinboard: unable to create a bookmark: url=%s status=%d", apiEndpoint, response.StatusCode) } return nil diff --git a/internal/integration/pocket/connector.go b/internal/integration/pocket/connector.go index 5878e646..a5584750 100644 --- a/internal/integration/pocket/connector.go +++ b/internal/integration/pocket/connector.go @@ -4,12 +4,13 @@ package pocket // import "miniflux.app/v2/internal/integration/pocket" import ( + "bytes" + "encoding/json" "errors" "fmt" - "io" - "net/url" + "net/http" - "miniflux.app/v2/internal/http/client" + "miniflux.app/v2/internal/version" ) // Connector manages the authorization flow with Pocket to get a personal access token. @@ -24,72 +25,82 @@ func NewConnector(consumerKey string) *Connector { // RequestToken fetches a new request token from Pocket API. func (c *Connector) RequestToken(redirectURL string) (string, error) { - type req struct { - ConsumerKey string `json:"consumer_key"` - RedirectURI string `json:"redirect_uri"` - } - - clt := client.New("https://getpocket.com/v3/oauth/request") - response, err := clt.PostJSON(&req{ConsumerKey: c.consumerKey, RedirectURI: redirectURL}) + apiEndpoint := "https://getpocket.com/v3/oauth/request" + requestBody, err := json.Marshal(&createTokenRequest{ConsumerKey: c.consumerKey, RedirectURI: redirectURL}) if err != nil { - return "", fmt.Errorf("pocket: unable to fetch request token: %v", err) + return "", fmt.Errorf("pocket: unable to encode request body: %v", err) } - if response.HasServerFailure() { - return "", fmt.Errorf("pocket: unable to fetch request token, status=%d", response.StatusCode) - } - - body, err := io.ReadAll(response.Body) + request, err := http.NewRequest(http.MethodPost, apiEndpoint, bytes.NewReader(requestBody)) if err != nil { - return "", fmt.Errorf("pocket: unable to read response body: %v", err) + return "", fmt.Errorf("pocket: unable to create request: %v", err) } - values, err := url.ParseQuery(string(body)) + request.Header.Set("Content-Type", "application/json") + request.Header.Set("X-Accept", "application/json") + request.Header.Set("User-Agent", "Miniflux/"+version.Version) + + httpClient := &http.Client{Timeout: defaultClientTimeout} + response, err := httpClient.Do(request) if err != nil { - return "", fmt.Errorf("pocket: unable to parse response: %v", err) + return "", fmt.Errorf("pocket: unable to send request: %v", err) + } + defer response.Body.Close() + + if response.StatusCode >= 400 { + return "", fmt.Errorf("pocket: unable get request token: url=%s status=%d", apiEndpoint, response.StatusCode) } - code := values.Get("code") - if code == "" { - return "", errors.New("pocket: code is empty") + var result createTokenResponse + if err := json.NewDecoder(response.Body).Decode(&result); err != nil { + return "", fmt.Errorf("pocket: unable to decode response: %v", err) } - return code, nil + if result.Code == "" { + return "", errors.New("pocket: request token is empty") + } + + return result.Code, nil } // AccessToken fetches a new access token once the end-user authorized the application. func (c *Connector) AccessToken(requestToken string) (string, error) { - type req struct { - ConsumerKey string `json:"consumer_key"` - Code string `json:"code"` - } - - clt := client.New("https://getpocket.com/v3/oauth/authorize") - response, err := clt.PostJSON(&req{ConsumerKey: c.consumerKey, Code: requestToken}) + apiEndpoint := "https://getpocket.com/v3/oauth/authorize" + requestBody, err := json.Marshal(&authorizeRequest{ConsumerKey: c.consumerKey, Code: requestToken}) if err != nil { - return "", fmt.Errorf("pocket: unable to fetch access token: %v", err) + return "", fmt.Errorf("pocket: unable to encode request body: %v", err) } - if response.HasServerFailure() { - return "", fmt.Errorf("pocket: unable to fetch access token, status=%d", response.StatusCode) - } - - body, err := io.ReadAll(response.Body) + request, err := http.NewRequest(http.MethodPost, apiEndpoint, bytes.NewReader(requestBody)) if err != nil { - return "", fmt.Errorf("pocket: unable to read response body: %v", err) + return "", fmt.Errorf("pocket: unable to create request: %v", err) } - values, err := url.ParseQuery(string(body)) + request.Header.Set("Content-Type", "application/json") + request.Header.Set("X-Accept", "application/json") + request.Header.Set("User-Agent", "Miniflux/"+version.Version) + + httpClient := &http.Client{Timeout: defaultClientTimeout} + response, err := httpClient.Do(request) if err != nil { - return "", fmt.Errorf("pocket: unable to parse response: %v", err) + return "", fmt.Errorf("pocket: unable to send request: %v", err) + } + defer response.Body.Close() + + if response.StatusCode >= 400 { + return "", fmt.Errorf("pocket: unable get access token: url=%s status=%d", apiEndpoint, response.StatusCode) } - token := values.Get("access_token") - if token == "" { - return "", errors.New("pocket: access_token is empty") + var result authorizeReponse + if err := json.NewDecoder(response.Body).Decode(&result); err != nil { + return "", fmt.Errorf("pocket: unable to decode response: %v", err) } - return token, nil + if result.AccessToken == "" { + return "", errors.New("pocket: access token is empty") + } + + return result.AccessToken, nil } // AuthorizationURL returns the authorization URL for the end-user. @@ -100,3 +111,22 @@ func (c *Connector) AuthorizationURL(requestToken, redirectURL string) string { redirectURL, ) } + +type createTokenRequest struct { + ConsumerKey string `json:"consumer_key"` + RedirectURI string `json:"redirect_uri"` +} + +type createTokenResponse struct { + Code string `json:"code"` +} + +type authorizeRequest struct { + ConsumerKey string `json:"consumer_key"` + Code string `json:"code"` +} + +type authorizeReponse struct { + AccessToken string `json:"access_token"` + Username string `json:"username"` +} diff --git a/internal/integration/pocket/pocket.go b/internal/integration/pocket/pocket.go index e940ce55..c45006e4 100644 --- a/internal/integration/pocket/pocket.go +++ b/internal/integration/pocket/pocket.go @@ -4,51 +4,67 @@ package pocket // import "miniflux.app/v2/internal/integration/pocket" import ( + "bytes" + "encoding/json" "fmt" + "net/http" + "time" - "miniflux.app/v2/internal/http/client" + "miniflux.app/v2/internal/version" ) -// Client represents a Pocket client. +const defaultClientTimeout = 10 * time.Second + type Client struct { consumerKey string accessToken string } -// NewClient returns a new Pocket client. func NewClient(consumerKey, accessToken string) *Client { return &Client{consumerKey, accessToken} } -// AddURL sends a single link to Pocket. -func (c *Client) AddURL(link, title string) error { +func (c *Client) AddURL(entryURL, entryTitle string) error { if c.consumerKey == "" || c.accessToken == "" { - return fmt.Errorf("pocket: missing credentials") + return fmt.Errorf("pocket: missing consumer key or access token") } - type body struct { - AccessToken string `json:"access_token"` - ConsumerKey string `json:"consumer_key"` - Title string `json:"title,omitempty"` - URL string `json:"url"` - } - - data := &body{ + apiEndpoint := "https://getpocket.com/v3/add" + requestBody, err := json.Marshal(&createItemRequest{ AccessToken: c.accessToken, ConsumerKey: c.consumerKey, - Title: title, - URL: link, - } - - clt := client.New("https://getpocket.com/v3/add") - response, err := clt.PostJSON(data) + Title: entryTitle, + URL: entryURL, + }) if err != nil { - return fmt.Errorf("pocket: unable to send url: %v", err) + return fmt.Errorf("pocket: unable to encode request body: %v", err) } - if response.HasServerFailure() { - return fmt.Errorf("pocket: unable to send url, status=%d", response.StatusCode) + request, err := http.NewRequest(http.MethodPost, apiEndpoint, bytes.NewReader(requestBody)) + if err != nil { + return fmt.Errorf("pocket: unable to create request: %v", err) + } + + request.Header.Set("Content-Type", "application/json") + request.Header.Set("User-Agent", "Miniflux/"+version.Version) + + httpClient := &http.Client{Timeout: defaultClientTimeout} + response, err := httpClient.Do(request) + if err != nil { + return fmt.Errorf("pocket: unable to send request: %v", err) + } + defer response.Body.Close() + + if response.StatusCode >= 400 { + return fmt.Errorf("pocket: unable to create item: url=%s status=%d", apiEndpoint, response.StatusCode) } return nil } + +type createItemRequest struct { + AccessToken string `json:"access_token"` + ConsumerKey string `json:"consumer_key"` + Title string `json:"title,omitempty"` + URL string `json:"url"` +} diff --git a/internal/integration/readwise/readwise.go b/internal/integration/readwise/readwise.go index 16032b30..40d40d0c 100644 --- a/internal/integration/readwise/readwise.go +++ b/internal/integration/readwise/readwise.go @@ -6,61 +6,64 @@ package readwise // import "miniflux.app/v2/internal/integration/readwise" import ( + "bytes" + "encoding/json" "fmt" - "net/url" + "net/http" + "time" - "miniflux.app/v2/internal/http/client" + "miniflux.app/v2/internal/version" ) -// Document structure of a Readwise Reader document -// This initial version accepts only the one required field, the URL -type Document struct { - Url string `json:"url"` -} +const ( + readwiseApiEndpoint = "https://readwise.io/api/v3/save/" + defaultClientTimeout = 10 * time.Second +) -// Client represents a Readwise Reader client. type Client struct { apiKey string } -// NewClient returns a new Readwise Reader client. func NewClient(apiKey string) *Client { return &Client{apiKey: apiKey} } -// AddEntry sends an entry to Readwise Reader. -func (c *Client) AddEntry(link string) error { +func (c *Client) CreateDocument(entryURL string) error { if c.apiKey == "" { return fmt.Errorf("readwise: missing API key") } - doc := &Document{ - Url: link, - } + requestBody, err := json.Marshal(&readwiseDocument{ + URL: entryURL, + }) - apiURL, err := getAPIEndpoint("https://readwise.io/api/v3/save/") if err != nil { - return err + return fmt.Errorf("readwise: unable to encode request body: %v", err) } - clt := client.New(apiURL) - clt.WithAuthorization("Token " + c.apiKey) - response, err := clt.PostJSON(doc) + request, err := http.NewRequest(http.MethodPost, readwiseApiEndpoint, bytes.NewReader(requestBody)) if err != nil { - return fmt.Errorf("readwise: unable to send entry: %v", err) + return fmt.Errorf("readwise: unable to create request: %v", err) } - if response.HasServerFailure() { - return fmt.Errorf("readwise: unable to send entry, status=%d", response.StatusCode) + request.Header.Set("Content-Type", "application/json") + request.Header.Set("User-Agent", "Miniflux/"+version.Version) + request.Header.Set("Authorization", "Token "+c.apiKey) + + httpClient := &http.Client{Timeout: defaultClientTimeout} + response, err := httpClient.Do(request) + if err != nil { + return fmt.Errorf("readwise: unable to send request: %v", err) + } + defer response.Body.Close() + + if response.StatusCode >= 400 { + return fmt.Errorf("readwise: unable to create document: url=%s status=%d", readwiseApiEndpoint, response.StatusCode) } return nil } -func getAPIEndpoint(pathURL string) (string, error) { - u, err := url.Parse(pathURL) - if err != nil { - return "", fmt.Errorf("readwise: invalid API endpoint: %v", err) - } - return u.String(), nil +type readwiseDocument struct { + URL string `json:"url"` } diff --git a/internal/integration/shaarli/shaarli.go b/internal/integration/shaarli/shaarli.go index e5d49fd0..a69da227 100644 --- a/internal/integration/shaarli/shaarli.go +++ b/internal/integration/shaarli/shaarli.go @@ -29,7 +29,7 @@ func NewClient(baseURL, apiSecret string) *Client { return &Client{baseURL: baseURL, apiSecret: apiSecret} } -func (c *Client) AddLink(entryURL, entryTitle string) error { +func (c *Client) CreateLink(entryURL, entryTitle string) error { if c.baseURL == "" || c.apiSecret == "" { return fmt.Errorf("shaarli: missing base URL or API secret") } @@ -49,7 +49,7 @@ func (c *Client) AddLink(entryURL, entryTitle string) error { return fmt.Errorf("shaarli: unable to encode request body: %v", err) } - request, err := http.NewRequest("POST", apiEndpoint, bytes.NewReader(requestBody)) + request, err := http.NewRequest(http.MethodPost, apiEndpoint, bytes.NewReader(requestBody)) if err != nil { return fmt.Errorf("shaarli: unable to create request: %v", err) } diff --git a/internal/integration/shiori/shiori.go b/internal/integration/shiori/shiori.go index 455aaebe..22ec9f31 100644 --- a/internal/integration/shiori/shiori.go +++ b/internal/integration/shiori/shiori.go @@ -26,7 +26,7 @@ func NewClient(baseURL, username, password string) *Client { return &Client{baseURL: baseURL, username: username, password: password} } -func (c *Client) AddBookmark(entryURL, entryTitle string) error { +func (c *Client) CreateBookmark(entryURL, entryTitle string) error { if c.baseURL == "" || c.username == "" || c.password == "" { return fmt.Errorf("shiori: missing base URL, username or password") } @@ -51,13 +51,12 @@ func (c *Client) AddBookmark(entryURL, entryTitle string) error { return fmt.Errorf("shiori: unable to encode request body: %v", err) } - request, err := http.NewRequest("POST", apiEndpoint, bytes.NewReader(requestBody)) + request, err := http.NewRequest(http.MethodPost, apiEndpoint, bytes.NewReader(requestBody)) if err != nil { return fmt.Errorf("shiori: unable to create request: %v", err) } request.Header.Set("Content-Type", "application/json") - request.Header.Set("Accept", "application/json") request.Header.Set("User-Agent", "Miniflux/"+version.Version) request.Header.Set("X-Session-Id", sessionID) @@ -87,7 +86,7 @@ func (c *Client) authenticate() (sessionID string, err error) { return "", fmt.Errorf("shiori: unable to encode request body: %v", err) } - request, err := http.NewRequest("POST", apiEndpoint, bytes.NewReader(requestBody)) + request, err := http.NewRequest(http.MethodPost, apiEndpoint, bytes.NewReader(requestBody)) if err != nil { return "", fmt.Errorf("shiori: unable to create request: %v", err) } diff --git a/internal/integration/wallabag/wallabag.go b/internal/integration/wallabag/wallabag.go index e5788185..110e6561 100644 --- a/internal/integration/wallabag/wallabag.go +++ b/internal/integration/wallabag/wallabag.go @@ -4,16 +4,20 @@ package wallabag // import "miniflux.app/v2/internal/integration/wallabag" import ( + "bytes" "encoding/json" "fmt" - "io" + "net/http" "net/url" + "strings" + "time" - "miniflux.app/v2/internal/http/client" "miniflux.app/v2/internal/urllib" + "miniflux.app/v2/internal/version" ) -// Client represents a Wallabag client. +const defaultClientTimeout = 10 * time.Second + type Client struct { baseURL string clientID string @@ -23,16 +27,13 @@ type Client struct { onlyURL bool } -// NewClient returns a new Wallabag client. func NewClient(baseURL, clientID, clientSecret, username, password string, onlyURL bool) *Client { return &Client{baseURL, clientID, clientSecret, username, password, onlyURL} } -// AddEntry sends a link to Wallabag. -// Pass an empty string in `content` to let Wallabag fetch the article content. -func (c *Client) AddEntry(link, title, content string) error { +func (c *Client) CreateEntry(entryURL, entryTitle, entryContent string) error { if c.baseURL == "" || c.clientID == "" || c.clientSecret == "" || c.username == "" || c.password == "" { - return fmt.Errorf("wallabag: missing credentials") + return fmt.Errorf("wallabag: missing base URL, client ID, client secret, username or password") } accessToken, err := c.getAccessToken() @@ -40,29 +41,47 @@ func (c *Client) AddEntry(link, title, content string) error { return err } - return c.createEntry(accessToken, link, title, content) + return c.createEntry(accessToken, entryURL, entryTitle, entryContent) } -func (c *Client) createEntry(accessToken, link, title, content string) error { - endpoint, err := urllib.JoinBaseURLAndPath(c.baseURL, "/api/entries.json") +func (c *Client) createEntry(accessToken, entryURL, entryTitle, entryContent 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) } - data := map[string]string{"url": link, "title": title} - if !c.onlyURL { - data["content"] = content + if c.onlyURL { + entryContent = "" } - clt := client.New(endpoint) - clt.WithAuthorization("Bearer " + accessToken) - response, err := clt.PostJSON(data) + requestBody, err := json.Marshal(&createEntryRequest{ + URL: entryURL, + Title: entryTitle, + Content: entryContent, + }) if err != nil { - return fmt.Errorf("wallabag: unable to post entry using %q endpoint: %v", endpoint, err) + return fmt.Errorf("wallbag: unable to encode request body: %v", err) } - if response.HasServerFailure() { - return fmt.Errorf("wallabag: request failed using %q, status=%d", endpoint, response.StatusCode) + request, err := http.NewRequest(http.MethodPost, apiEndpoint, bytes.NewReader(requestBody)) + if err != nil { + return fmt.Errorf("wallbag: unable to create request: %v", err) + } + + request.Header.Set("Content-Type", "application/json") + request.Header.Set("Accept", "application/json") + request.Header.Set("User-Agent", "Miniflux/"+version.Version) + request.Header.Set("Authorization", "Bearer "+accessToken) + + httpClient := &http.Client{Timeout: defaultClientTimeout} + response, err := httpClient.Do(request) + if err != nil { + return fmt.Errorf("wallabag: unable to send request: %v", err) + } + 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 nil @@ -76,27 +95,37 @@ func (c *Client) getAccessToken() (string, error) { values.Add("username", c.username) values.Add("password", c.password) - endpoint, err := urllib.JoinBaseURLAndPath(c.baseURL, "/oauth/v2/token") + apiEndpoint, err := urllib.JoinBaseURLAndPath(c.baseURL, "/oauth/v2/token") if err != nil { return "", fmt.Errorf("wallbag: unable to generate token endpoint: %v", err) } - clt := client.New(endpoint) - response, err := clt.PostForm(values) + request, err := http.NewRequest(http.MethodPost, apiEndpoint, strings.NewReader(values.Encode())) if err != nil { - return "", fmt.Errorf("wallabag: unable to get access token using %q endpoint: %v", endpoint, err) + return "", fmt.Errorf("wallbag: unable to create request: %v", err) } - if response.HasServerFailure() { - return "", fmt.Errorf("wallabag: request failed using %q, status=%d", endpoint, response.StatusCode) - } + request.Header.Set("Content-Type", "application/x-www-form-urlencoded") + request.Header.Set("Accept", "application/json") + request.Header.Set("User-Agent", "Miniflux/"+version.Version) - token, err := decodeTokenResponse(response.Body) + httpClient := &http.Client{Timeout: defaultClientTimeout} + response, err := httpClient.Do(request) if err != nil { - return "", err + return "", fmt.Errorf("wallabag: unable to send request: %v", err) + } + 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 token.AccessToken, nil + var responseBody tokenResponse + if err := json.NewDecoder(response.Body).Decode(&responseBody); err != nil { + return "", fmt.Errorf("wallabag: unable to decode token response: %v", err) + } + + return responseBody.AccessToken, nil } type tokenResponse struct { @@ -107,13 +136,8 @@ type tokenResponse struct { TokenType string `json:"token_type"` } -func decodeTokenResponse(body io.Reader) (*tokenResponse, error) { - var token tokenResponse - - decoder := json.NewDecoder(body) - if err := decoder.Decode(&token); err != nil { - return nil, fmt.Errorf("wallabag: unable to decode token response: %v", err) - } - - return &token, nil +type createEntryRequest struct { + URL string `json:"url"` + Title string `json:"title"` + Content string `json:"content,omitempty"` } diff --git a/internal/locale/translations/de_DE.json b/internal/locale/translations/de_DE.json index 46d8b727..bfa26329 100644 --- a/internal/locale/translations/de_DE.json +++ b/internal/locale/translations/de_DE.json @@ -357,7 +357,7 @@ "form.integration.notion_token": "Notion Secret Token", "form.integration.apprise_activate": "Push entries to Apprise", "form.integration.apprise_url": "Apprise API URL", - "form.integration.apprise_services_url": "Apprise services urls seperated by comma", + "form.integration.apprise_services_url": "Comma separated list of Apprise service URLs", "form.integration.nunux_keeper_activate": "Artikel in Nunux Keeper speichern", "form.integration.nunux_keeper_endpoint": "Nunux Keeper API-Endpunkt", "form.integration.nunux_keeper_api_key": "Nunux Keeper API-Schlüssel", diff --git a/internal/locale/translations/el_EL.json b/internal/locale/translations/el_EL.json index 1e8509fb..83def66d 100644 --- a/internal/locale/translations/el_EL.json +++ b/internal/locale/translations/el_EL.json @@ -357,7 +357,7 @@ "form.integration.notion_token": "Notion Secret Token", "form.integration.apprise_activate": "Push entries to Apprise", "form.integration.apprise_url": "Apprise API URL", - "form.integration.apprise_services_url": "Apprise services urls seperated by comma", + "form.integration.apprise_services_url": "Comma separated list of Apprise service URLs", "form.integration.nunux_keeper_activate": "Αποθήκευση άρθρων στο Nunux Keeper", "form.integration.nunux_keeper_endpoint": "Τελικό σημείο Nunux Keeper API", "form.integration.nunux_keeper_api_key": "Κλειδί API Nunux Keeper", diff --git a/internal/locale/translations/en_US.json b/internal/locale/translations/en_US.json index 85ac803d..cef0a263 100644 --- a/internal/locale/translations/en_US.json +++ b/internal/locale/translations/en_US.json @@ -357,7 +357,7 @@ "form.integration.notion_token": "Notion Secret Token", "form.integration.apprise_activate": "Push entries to Apprise", "form.integration.apprise_url": "Apprise API URL", - "form.integration.apprise_services_url": "Apprise Services URLs (seperated by comma)", + "form.integration.apprise_services_url": "Comma separated list of Apprise service URLs", "form.integration.nunux_keeper_activate": "Save entries to Nunux Keeper", "form.integration.nunux_keeper_endpoint": "Nunux Keeper API Endpoint", "form.integration.nunux_keeper_api_key": "Nunux Keeper API key", diff --git a/internal/locale/translations/es_ES.json b/internal/locale/translations/es_ES.json index 22017cf4..f78f0c91 100644 --- a/internal/locale/translations/es_ES.json +++ b/internal/locale/translations/es_ES.json @@ -357,7 +357,7 @@ "form.integration.notion_token": "Notion Secret Token", "form.integration.apprise_activate": "Push entries to Apprise", "form.integration.apprise_url": "Apprise API URL", - "form.integration.apprise_services_url": "Apprise services urls seperated by comma", + "form.integration.apprise_services_url": "Comma separated list of Apprise service URLs", "form.integration.nunux_keeper_activate": "Enviar artículos a Nunux Keeper", "form.integration.nunux_keeper_endpoint": "Acceso API de Nunux Keeper", "form.integration.nunux_keeper_api_key": "Clave de API de Nunux Keeper", diff --git a/internal/locale/translations/fi_FI.json b/internal/locale/translations/fi_FI.json index 37fa74f5..4163dedc 100644 --- a/internal/locale/translations/fi_FI.json +++ b/internal/locale/translations/fi_FI.json @@ -357,7 +357,7 @@ "form.integration.notion_token": "Notion Secret Token", "form.integration.apprise_activate": "Push entries to Apprise", "form.integration.apprise_url": "Apprise API URL", - "form.integration.apprise_services_url": "Apprise services urls seperated by comma", + "form.integration.apprise_services_url": "Comma separated list of Apprise service URLs", "form.integration.nunux_keeper_activate": "Tallenna artikkelit Nunux Keeperiin", "form.integration.nunux_keeper_endpoint": "Nunux Keeper API-päätepiste", "form.integration.nunux_keeper_api_key": "Nunux Keeper API-avain", diff --git a/internal/locale/translations/fr_FR.json b/internal/locale/translations/fr_FR.json index 13668ae3..fabd3260 100644 --- a/internal/locale/translations/fr_FR.json +++ b/internal/locale/translations/fr_FR.json @@ -353,11 +353,11 @@ "form.integration.wallabag_username": "Nom d'utilisateur de Wallabag", "form.integration.wallabag_password": "Mot de passe de Wallabag", "form.integration.notion_activate": "Sauvegarder les articles vers Notion", - "form.integration.notion_page_id": "l'identifiant de la page Notion", + "form.integration.notion_page_id": "Identifiant de la page Notion", "form.integration.notion_token": "Jeton d'accès de l'API de Notion", - "form.integration.apprise_activate": "Push entries to Apprise", + "form.integration.apprise_activate": "Emvoyer les articles vers Apprise", "form.integration.apprise_url": "Apprise API URL", - "form.integration.apprise_services_url": "Apprise services urls seperated by comma", + "form.integration.apprise_services_url": "Comma separated list of Apprise services", "form.integration.nunux_keeper_activate": "Sauvegarder les articles vers Nunux Keeper", "form.integration.nunux_keeper_endpoint": "URL de l'API de Nunux Keeper", "form.integration.nunux_keeper_api_key": "Clé d'API de Nunux Keeper", diff --git a/internal/locale/translations/hi_IN.json b/internal/locale/translations/hi_IN.json index f2c124c3..cdd3e5f9 100644 --- a/internal/locale/translations/hi_IN.json +++ b/internal/locale/translations/hi_IN.json @@ -357,7 +357,7 @@ "form.integration.notion_token": "Notion Secret Token", "form.integration.apprise_activate": "Push entries to Apprise", "form.integration.apprise_url": "Apprise API URL", - "form.integration.apprise_services_url": "Apprise services urls seperated by comma", + "form.integration.apprise_services_url": "Comma separated list of Apprise service URLs", "form.integration.nunux_keeper_activate": "विषय-वस्तु को ननक्स कीपर में सहेजें", "form.integration.nunux_keeper_endpoint": "ननक्स कीपर एपीआई समापन बिंदु", "form.integration.nunux_keeper_api_key": "ननक्स कीपर एपीआई कुंजी", diff --git a/internal/locale/translations/id_ID.json b/internal/locale/translations/id_ID.json index 05108450..d6688e0c 100644 --- a/internal/locale/translations/id_ID.json +++ b/internal/locale/translations/id_ID.json @@ -354,7 +354,7 @@ "form.integration.notion_token": "Notion Secret Token", "form.integration.apprise_activate": "Push entries to Apprise", "form.integration.apprise_url": "Apprise API URL", - "form.integration.apprise_services_url": "Apprise services urls seperated by comma", + "form.integration.apprise_services_url": "Comma separated list of Apprise service URLs", "form.integration.nunux_keeper_activate": "Simpan artikel ke Nunux Keeper", "form.integration.nunux_keeper_endpoint": "Titik URL API Nunux Keeper", "form.integration.nunux_keeper_api_key": "Kunci API Nunux Keeper", diff --git a/internal/locale/translations/it_IT.json b/internal/locale/translations/it_IT.json index 68903209..8bffcd14 100644 --- a/internal/locale/translations/it_IT.json +++ b/internal/locale/translations/it_IT.json @@ -357,7 +357,7 @@ "form.integration.notion_token": "Notion Secret Token", "form.integration.apprise_activate": "Push entries to Apprise", "form.integration.apprise_url": "Apprise API URL", - "form.integration.apprise_services_url": "Apprise services urls seperated by comma", + "form.integration.apprise_services_url": "Comma separated list of Apprise service URLs", "form.integration.nunux_keeper_activate": "Salva gli articoli su Nunux Keeper", "form.integration.nunux_keeper_endpoint": "Endpoint dell'API di Nunux Keeper", "form.integration.nunux_keeper_api_key": "API key dell'account Nunux Keeper", diff --git a/internal/locale/translations/ja_JP.json b/internal/locale/translations/ja_JP.json index 5cc3a362..e88f26c8 100644 --- a/internal/locale/translations/ja_JP.json +++ b/internal/locale/translations/ja_JP.json @@ -357,7 +357,7 @@ "form.integration.notion_token": "Notion Secret Token", "form.integration.apprise_activate": "Push entries to Apprise", "form.integration.apprise_url": "Apprise API URL", - "form.integration.apprise_services_url": "Apprise services urls seperated by comma", + "form.integration.apprise_services_url": "Comma separated list of Apprise service URLs", "form.integration.nunux_keeper_activate": "Nunux Keeper に記事を保存する", "form.integration.nunux_keeper_endpoint": "Nunux Keeper の API Endpoint", "form.integration.nunux_keeper_api_key": "Nunux Keeper の API key", diff --git a/internal/locale/translations/nl_NL.json b/internal/locale/translations/nl_NL.json index d8bb7d21..1403208b 100644 --- a/internal/locale/translations/nl_NL.json +++ b/internal/locale/translations/nl_NL.json @@ -357,7 +357,7 @@ "form.integration.notion_token": "Notion Secret Token", "form.integration.apprise_activate": "Push entries to Apprise", "form.integration.apprise_url": "Apprise API URL", - "form.integration.apprise_services_url": "Apprise services urls seperated by comma", + "form.integration.apprise_services_url": "Comma separated list of Apprise service URLs", "form.integration.nunux_keeper_activate": "Opslaan naar Nunux Keeper", "form.integration.nunux_keeper_endpoint": "Nunux Keeper URL", "form.integration.nunux_keeper_api_key": "Nunux Keeper API-sleutel", diff --git a/internal/locale/translations/pl_PL.json b/internal/locale/translations/pl_PL.json index fe40c6c0..cea71451 100644 --- a/internal/locale/translations/pl_PL.json +++ b/internal/locale/translations/pl_PL.json @@ -359,7 +359,7 @@ "form.integration.notion_token": "Notion Secret Token", "form.integration.apprise_activate": "Push entries to Apprise", "form.integration.apprise_url": "Apprise API URL", - "form.integration.apprise_services_url": "Apprise services urls seperated by comma", + "form.integration.apprise_services_url": "Comma separated list of Apprise service URLs", "form.integration.nunux_keeper_activate": "Zapisz artykuly do Nunux Keeper", "form.integration.nunux_keeper_endpoint": "Nunux Keeper URL", "form.integration.nunux_keeper_api_key": "Nunux Keeper API key", diff --git a/internal/locale/translations/pt_BR.json b/internal/locale/translations/pt_BR.json index db9fea56..d1dcce60 100644 --- a/internal/locale/translations/pt_BR.json +++ b/internal/locale/translations/pt_BR.json @@ -357,7 +357,7 @@ "form.integration.notion_token": "Notion Secret Token", "form.integration.apprise_activate": "Push entries to Apprise", "form.integration.apprise_url": "Apprise API URL", - "form.integration.apprise_services_url": "Apprise services urls seperated by comma", + "form.integration.apprise_services_url": "Comma separated list of Apprise service URLs", "form.integration.nunux_keeper_activate": "Salvar itens no Nunux Keeper", "form.integration.nunux_keeper_endpoint": "Endpoint de API do Nunux Keeper", "form.integration.nunux_keeper_api_key": "Chave de API do Nunux Keeper", diff --git a/internal/locale/translations/ru_RU.json b/internal/locale/translations/ru_RU.json index 1580322c..d7b473d1 100644 --- a/internal/locale/translations/ru_RU.json +++ b/internal/locale/translations/ru_RU.json @@ -359,7 +359,7 @@ "form.integration.notion_token": "Notion Secret Token", "form.integration.apprise_activate": "Push entries to Apprise", "form.integration.apprise_url": "Apprise API URL", - "form.integration.apprise_services_url": "Apprise services urls seperated by comma", + "form.integration.apprise_services_url": "Comma separated list of Apprise service URLs", "form.integration.nunux_keeper_activate": "Сохранять статьи в Nunux Keeper", "form.integration.nunux_keeper_endpoint": "Конечная точка Nunux Keeper API", "form.integration.nunux_keeper_api_key": "API-ключ Nunux Keeper", diff --git a/internal/locale/translations/tr_TR.json b/internal/locale/translations/tr_TR.json index 6d70f842..572aad1b 100644 --- a/internal/locale/translations/tr_TR.json +++ b/internal/locale/translations/tr_TR.json @@ -357,7 +357,7 @@ "form.integration.notion_token": "Notion Secret Token", "form.integration.apprise_activate": "Push entries to Apprise", "form.integration.apprise_url": "Apprise API URL", - "form.integration.apprise_services_url": "Apprise services urls seperated by comma", + "form.integration.apprise_services_url": "Comma separated list of Apprise service URLs", "form.integration.nunux_keeper_activate": "Makaleleri Nunux Keeper'a kaydet", "form.integration.nunux_keeper_endpoint": "Nunux Keeper API Uç Noktası", "form.integration.nunux_keeper_api_key": "Nunux Keeper API anahtarı", diff --git a/internal/locale/translations/uk_UA.json b/internal/locale/translations/uk_UA.json index 6008884f..a92e285a 100644 --- a/internal/locale/translations/uk_UA.json +++ b/internal/locale/translations/uk_UA.json @@ -360,7 +360,7 @@ "form.integration.notion_token": "Notion Secret Token", "form.integration.apprise_activate": "Push entries to Apprise", "form.integration.apprise_url": "Apprise API URL", - "form.integration.apprise_services_url": "Apprise services urls seperated by comma", + "form.integration.apprise_services_url": "Comma separated list of Apprise service URLs", "form.integration.nunux_keeper_activate": "Зберігати статті до Nunux Keeper", "form.integration.nunux_keeper_endpoint": "Nunux Keeper API Endpoint", "form.integration.nunux_keeper_api_key": "Ключ API Nunux Keeper", diff --git a/internal/locale/translations/zh_CN.json b/internal/locale/translations/zh_CN.json index e985b2c3..0acf016a 100644 --- a/internal/locale/translations/zh_CN.json +++ b/internal/locale/translations/zh_CN.json @@ -355,7 +355,7 @@ "form.integration.notion_token": "Notion Secret Token", "form.integration.apprise_activate": "Push entries to Apprise", "form.integration.apprise_url": "Apprise API URL", - "form.integration.apprise_services_url": "Apprise services urls seperated by comma", + "form.integration.apprise_services_url": "Comma separated list of Apprise service URLs", "form.integration.nunux_keeper_activate": "保存文章到 Nunux Keeper", "form.integration.nunux_keeper_endpoint": "Nunux Keeper API 端点", "form.integration.nunux_keeper_api_key": "Nunux Keeper API 密钥", diff --git a/internal/locale/translations/zh_TW.json b/internal/locale/translations/zh_TW.json index a762bb98..817884e2 100644 --- a/internal/locale/translations/zh_TW.json +++ b/internal/locale/translations/zh_TW.json @@ -357,7 +357,7 @@ "form.integration.notion_token": "Notion Secret Token", "form.integration.apprise_activate": "Push entries to Apprise", "form.integration.apprise_url": "Apprise API URL", - "form.integration.apprise_services_url": "Apprise services urls seperated by comma", + "form.integration.apprise_services_url": "Comma separated list of Apprise service URLs", "form.integration.nunux_keeper_activate": "儲存文章到 Nunux Keeper", "form.integration.nunux_keeper_endpoint": "Nunux Keeper API 端點", "form.integration.nunux_keeper_api_key": "Nunux Keeper API 金鑰",