mirror of https://github.com/miniflux/v2.git
Use stdlib HTTP client for third-party integrations
This commit is contained in:
parent
e5d9f2f5a0
commit
5e520ca5bf
|
@ -4,57 +4,64 @@
|
||||||
package apprise
|
package apprise
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net/http"
|
||||||
"strings"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"miniflux.app/v2/internal/http/client"
|
|
||||||
"miniflux.app/v2/internal/model"
|
"miniflux.app/v2/internal/model"
|
||||||
"miniflux.app/v2/internal/urllib"
|
"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 {
|
type Client struct {
|
||||||
servicesURL string
|
servicesURL string
|
||||||
baseURL string
|
baseURL string
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewClient returns a new Apprise client.
|
|
||||||
func NewClient(serviceURL, baseURL string) *Client {
|
func NewClient(serviceURL, baseURL string) *Client {
|
||||||
return &Client{serviceURL, baseURL}
|
return &Client{serviceURL, baseURL}
|
||||||
}
|
}
|
||||||
|
|
||||||
// PushEntry pushes entry to apprise
|
func (c *Client) SendNotification(entry *model.Entry) error {
|
||||||
func (c *Client) PushEntry(entry *model.Entry) error {
|
|
||||||
if c.baseURL == "" || c.servicesURL == "" {
|
if c.baseURL == "" || c.servicesURL == "" {
|
||||||
return fmt.Errorf("apprise: missing base URL or service URL")
|
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 {
|
if err != nil {
|
||||||
apiEndpoint, err := urllib.JoinBaseURLAndPath(c.baseURL, "/notify")
|
return fmt.Errorf(`apprise: invalid API endpoint: %v`, err)
|
||||||
if err != nil {
|
}
|
||||||
return fmt.Errorf(`apprise: invalid API endpoint: %v`, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
clt := client.New(apiEndpoint)
|
requestBody, err := json.Marshal(map[string]any{
|
||||||
message := "[" + entry.Title + "]" + "(" + entry.URL + ")" + "\n\n"
|
"urls": c.servicesURL,
|
||||||
data := &Data{
|
"body": message,
|
||||||
Urls: c.servicesURL,
|
})
|
||||||
Body: message,
|
if err != nil {
|
||||||
}
|
return fmt.Errorf("apprise: unable to encode request body: %v", err)
|
||||||
response, error := clt.PostJSON(data)
|
}
|
||||||
if error != nil {
|
|
||||||
return fmt.Errorf("apprise: ending message failed: %v", error)
|
|
||||||
}
|
|
||||||
|
|
||||||
if response.HasServerFailure() {
|
request, err := http.NewRequest(http.MethodPost, apiEndpoint, bytes.NewReader(requestBody))
|
||||||
return fmt.Errorf("apprise: request failed, status=%d", response.StatusCode)
|
if err != nil {
|
||||||
}
|
return fmt.Errorf("apprise: unable to create request: %v", err)
|
||||||
} else {
|
}
|
||||||
return fmt.Errorf("%s %s %s", c.baseURL, "responding on port:", strings.Split(c.baseURL, ":")[1])
|
|
||||||
|
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
|
return nil
|
||||||
|
|
|
@ -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"`
|
|
||||||
}
|
|
|
@ -4,59 +4,77 @@
|
||||||
package espial // import "miniflux.app/v2/internal/integration/espial"
|
package espial // import "miniflux.app/v2/internal/integration/espial"
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
"miniflux.app/v2/internal/http/client"
|
|
||||||
"miniflux.app/v2/internal/urllib"
|
"miniflux.app/v2/internal/urllib"
|
||||||
|
"miniflux.app/v2/internal/version"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Document structure of an Espial document
|
const defaultClientTimeout = 10 * time.Second
|
||||||
type Document struct {
|
|
||||||
Title string `json:"title,omitempty"`
|
|
||||||
Url string `json:"url,omitempty"`
|
|
||||||
ToRead bool `json:"toread,omitempty"`
|
|
||||||
Tags string `json:"tags,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Client represents an Espial client.
|
|
||||||
type Client struct {
|
type Client struct {
|
||||||
baseURL string
|
baseURL string
|
||||||
apiKey string
|
apiKey string
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewClient returns a new Espial client.
|
|
||||||
func NewClient(baseURL, apiKey string) *Client {
|
func NewClient(baseURL, apiKey string) *Client {
|
||||||
return &Client{baseURL: baseURL, apiKey: apiKey}
|
return &Client{baseURL: baseURL, apiKey: apiKey}
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddEntry sends an entry to Espial.
|
func (c *Client) CreateLink(entryURL, entryTitle, espialTags string) error {
|
||||||
func (c *Client) AddEntry(link, title, content, tags string) error {
|
|
||||||
if c.baseURL == "" || c.apiKey == "" {
|
if c.baseURL == "" || c.apiKey == "" {
|
||||||
return fmt.Errorf("espial: missing base URL or API key")
|
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")
|
apiEndpoint, err := urllib.JoinBaseURLAndPath(c.baseURL, "/api/add")
|
||||||
if err != nil {
|
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)
|
requestBody, err := json.Marshal(&espialDocument{
|
||||||
clt.WithAuthorization("ApiKey " + c.apiKey)
|
Title: entryTitle,
|
||||||
response, err := clt.PostJSON(doc)
|
Url: entryURL,
|
||||||
|
ToRead: true,
|
||||||
|
Tags: espialTags,
|
||||||
|
})
|
||||||
|
|
||||||
if err != nil {
|
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() {
|
request, err := http.NewRequest(http.MethodPost, apiEndpoint, bytes.NewReader(requestBody))
|
||||||
return fmt.Errorf("espial: unable to send entry, status=%d", response.StatusCode)
|
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
|
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"`
|
||||||
|
}
|
||||||
|
|
|
@ -5,42 +5,52 @@ package instapaper // import "miniflux.app/v2/internal/integration/instapaper"
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/http"
|
||||||
"net/url"
|
"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 {
|
type Client struct {
|
||||||
username string
|
username string
|
||||||
password string
|
password string
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewClient returns a new Instapaper client.
|
|
||||||
func NewClient(username, password string) *Client {
|
func NewClient(username, password string) *Client {
|
||||||
return &Client{username: username, password: password}
|
return &Client{username: username, password: password}
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddURL sends a link to Instapaper.
|
func (c *Client) AddURL(entryURL, entryTitle string) error {
|
||||||
func (c *Client) AddURL(link, title string) error {
|
|
||||||
if c.username == "" || c.password == "" {
|
if c.username == "" || c.password == "" {
|
||||||
return fmt.Errorf("instapaper: missing credentials")
|
return fmt.Errorf("instapaper: missing username or password")
|
||||||
}
|
}
|
||||||
|
|
||||||
values := url.Values{}
|
values := url.Values{}
|
||||||
values.Add("url", link)
|
values.Add("url", entryURL)
|
||||||
values.Add("title", title)
|
values.Add("title", entryTitle)
|
||||||
|
|
||||||
apiURL := "https://www.instapaper.com/api/add?" + values.Encode()
|
apiEndpoint := "https://www.instapaper.com/api/add?" + values.Encode()
|
||||||
clt := client.New(apiURL)
|
request, err := http.NewRequest(http.MethodGet, apiEndpoint, nil)
|
||||||
clt.WithCredentials(c.username, c.password)
|
|
||||||
response, err := clt.Get()
|
|
||||||
if err != 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() {
|
request.SetBasicAuth(c.username, c.password)
|
||||||
return fmt.Errorf("instapaper: unable to send url, 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("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
|
return nil
|
||||||
|
|
|
@ -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)
|
logger.Debug("[Integration] Sending entry #%d %q for user #%d to Pinboard", entry.ID, entry.URL, integration.UserID)
|
||||||
|
|
||||||
client := pinboard.NewClient(integration.PinboardToken)
|
client := pinboard.NewClient(integration.PinboardToken)
|
||||||
err := client.AddBookmark(
|
err := client.CreateBookmark(
|
||||||
entry.URL,
|
entry.URL,
|
||||||
entry.Title,
|
entry.Title,
|
||||||
integration.PinboardTags,
|
integration.PinboardTags,
|
||||||
|
@ -62,7 +62,7 @@ func SendEntry(entry *model.Entry, integration *model.Integration) {
|
||||||
integration.WallabagOnlyURL,
|
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)
|
logger.Error("[Integration] UserID #%d: %v", integration.UserID, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -74,7 +74,7 @@ func SendEntry(entry *model.Entry, integration *model.Integration) {
|
||||||
integration.NotionToken,
|
integration.NotionToken,
|
||||||
integration.NotionPageID,
|
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)
|
logger.Error("[Integration] UserID #%d: %v", integration.UserID, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -100,8 +100,8 @@ func SendEntry(entry *model.Entry, integration *model.Integration) {
|
||||||
integration.EspialAPIKey,
|
integration.EspialAPIKey,
|
||||||
)
|
)
|
||||||
|
|
||||||
if err := client.AddEntry(entry.URL, entry.Title, entry.Content, integration.EspialTags); err != nil {
|
if err := client.CreateLink(entry.URL, entry.Title, integration.EspialTags); err != nil {
|
||||||
logger.Error("[Integration] UserID #%d: %v", integration.UserID, err)
|
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.LinkdingTags,
|
||||||
integration.LinkdingMarkAsUnread,
|
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)
|
logger.Error("[Integration] UserID #%d: %v", integration.UserID, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -135,7 +135,7 @@ func SendEntry(entry *model.Entry, integration *model.Integration) {
|
||||||
integration.ReadwiseAPIKey,
|
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)
|
logger.Error("[Integration] UserID #%d: %v", integration.UserID, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -149,7 +149,7 @@ func SendEntry(entry *model.Entry, integration *model.Integration) {
|
||||||
integration.ShioriPassword,
|
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)
|
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,
|
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)
|
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.AppriseServicesURL,
|
||||||
integration.AppriseURL,
|
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)
|
logger.Error("[Integration] push entry to apprise failed: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,22 +4,19 @@
|
||||||
package linkding // import "miniflux.app/v2/internal/integration/linkding"
|
package linkding // import "miniflux.app/v2/internal/integration/linkding"
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"miniflux.app/v2/internal/http/client"
|
|
||||||
"miniflux.app/v2/internal/urllib"
|
"miniflux.app/v2/internal/urllib"
|
||||||
|
"miniflux.app/v2/internal/version"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Document structure of a Linkding document
|
const defaultClientTimeout = 10 * time.Second
|
||||||
type Document struct {
|
|
||||||
Url string `json:"url,omitempty"`
|
|
||||||
Title string `json:"title,omitempty"`
|
|
||||||
TagNames []string `json:"tag_names,omitempty"`
|
|
||||||
Unread bool `json:"unread,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Client represents an Linkding client.
|
|
||||||
type Client struct {
|
type Client struct {
|
||||||
baseURL string
|
baseURL string
|
||||||
apiKey string
|
apiKey string
|
||||||
|
@ -27,43 +24,61 @@ type Client struct {
|
||||||
unread bool
|
unread bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewClient returns a new Linkding client.
|
|
||||||
func NewClient(baseURL, apiKey, tags string, unread bool) *Client {
|
func NewClient(baseURL, apiKey, tags string, unread bool) *Client {
|
||||||
return &Client{baseURL: baseURL, apiKey: apiKey, tags: tags, unread: unread}
|
return &Client{baseURL: baseURL, apiKey: apiKey, tags: tags, unread: unread}
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddEntry sends an entry to Linkding.
|
func (c *Client) CreateBookmark(entryURL, entryTitle string) error {
|
||||||
func (c *Client) AddEntry(title, entryURL string) error {
|
|
||||||
if c.baseURL == "" || c.apiKey == "" {
|
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 {
|
tagsSplitFn := func(c rune) bool {
|
||||||
return c == ',' || c == ' '
|
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/")
|
apiEndpoint, err := urllib.JoinBaseURLAndPath(c.baseURL, "/api/bookmarks/")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf(`linkding: invalid API endpoint: %v`, err)
|
return fmt.Errorf(`linkding: invalid API endpoint: %v`, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
clt := client.New(apiEndpoint)
|
requestBody, err := json.Marshal(&linkdingBookmark{
|
||||||
clt.WithAuthorization("Token " + c.apiKey)
|
Url: entryURL,
|
||||||
response, err := clt.PostJSON(doc)
|
Title: entryTitle,
|
||||||
|
TagNames: strings.FieldsFunc(c.tags, tagsSplitFn),
|
||||||
|
Unread: c.unread,
|
||||||
|
})
|
||||||
|
|
||||||
if err != nil {
|
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() {
|
request, err := http.NewRequest(http.MethodPost, apiEndpoint, bytes.NewReader(requestBody))
|
||||||
return fmt.Errorf("linkding: unable to send entry, status=%d", response.StatusCode)
|
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
|
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"`
|
||||||
|
}
|
||||||
|
|
|
@ -4,51 +4,83 @@
|
||||||
package notion
|
package notion
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"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 {
|
type Client struct {
|
||||||
token string
|
apiToken string
|
||||||
pageID string
|
pageID string
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewClient returns a new Notion client.
|
func NewClient(apiToken, pageID string) *Client {
|
||||||
func NewClient(token, pageID string) *Client {
|
return &Client{apiToken, pageID}
|
||||||
return &Client{token, pageID}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) AddEntry(entryURL string, entryTitle string) error {
|
func (c *Client) UpdateDocument(entryURL string, entryTitle string) error {
|
||||||
if c.token == "" || c.pageID == "" {
|
if c.apiToken == "" || c.pageID == "" {
|
||||||
return fmt.Errorf("notion: missing credentials")
|
return fmt.Errorf("notion: missing API token or page ID")
|
||||||
}
|
}
|
||||||
clt := client.New("https://api.notion.com/v1/blocks/" + c.pageID + "/children")
|
|
||||||
block := &Data{
|
apiEndpoint := "https://api.notion.com/v1/blocks/" + c.pageID + "/children"
|
||||||
Children: []Block{
|
requestBody, err := json.Marshal(¬ionDocument{
|
||||||
|
Children: []block{
|
||||||
{
|
{
|
||||||
Object: "block",
|
Object: "block",
|
||||||
Type: "bookmark",
|
Type: "bookmark",
|
||||||
Bookmark: Bookmark{
|
Bookmark: bookmarkObject{
|
||||||
Caption: []interface{}{},
|
Caption: []any{},
|
||||||
URL: entryURL,
|
URL: entryURL,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
})
|
||||||
clt.WithAuthorization("Bearer " + c.token)
|
if err != nil {
|
||||||
customHeaders := map[string]string{
|
return fmt.Errorf("notion: unable to encode request body: %v", err)
|
||||||
"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 response.HasServerFailure() {
|
request, err := http.NewRequest(http.MethodPatch, apiEndpoint, bytes.NewReader(requestBody))
|
||||||
return fmt.Errorf("notion: request failed, status=%d", response.StatusCode)
|
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
|
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"`
|
||||||
|
}
|
||||||
|
|
|
@ -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"`
|
|
||||||
}
|
|
|
@ -4,42 +4,30 @@
|
||||||
package nunuxkeeper // import "miniflux.app/v2/internal/integration/nunuxkeeper"
|
package nunuxkeeper // import "miniflux.app/v2/internal/integration/nunuxkeeper"
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
"miniflux.app/v2/internal/http/client"
|
|
||||||
"miniflux.app/v2/internal/urllib"
|
"miniflux.app/v2/internal/urllib"
|
||||||
|
"miniflux.app/v2/internal/version"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Document structure of a Nununx Keeper document
|
const defaultClientTimeout = 10 * time.Second
|
||||||
type Document struct {
|
|
||||||
Title string `json:"title,omitempty"`
|
|
||||||
Origin string `json:"origin,omitempty"`
|
|
||||||
Content string `json:"content,omitempty"`
|
|
||||||
ContentType string `json:"contentType,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Client represents an Nunux Keeper client.
|
|
||||||
type Client struct {
|
type Client struct {
|
||||||
baseURL string
|
baseURL string
|
||||||
apiKey string
|
apiKey string
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewClient returns a new Nunux Keeepr client.
|
|
||||||
func NewClient(baseURL, apiKey string) *Client {
|
func NewClient(baseURL, apiKey string) *Client {
|
||||||
return &Client{baseURL: baseURL, apiKey: apiKey}
|
return &Client{baseURL: baseURL, apiKey: apiKey}
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddEntry sends an entry to Nunux Keeper.
|
func (c *Client) AddEntry(entryURL, entryTitle, entryContent string) error {
|
||||||
func (c *Client) AddEntry(link, title, content string) error {
|
|
||||||
if c.baseURL == "" || c.apiKey == "" {
|
if c.baseURL == "" || c.apiKey == "" {
|
||||||
return fmt.Errorf("nunux-keeper: missing credentials")
|
return fmt.Errorf("nunux-keeper: missing base URL or API key")
|
||||||
}
|
|
||||||
|
|
||||||
doc := &Document{
|
|
||||||
Title: title,
|
|
||||||
Origin: link,
|
|
||||||
Content: content,
|
|
||||||
ContentType: "text/html",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
apiEndpoint, err := urllib.JoinBaseURLAndPath(c.baseURL, "/v2/documents")
|
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)
|
return fmt.Errorf(`nunux-keeper: invalid API endpoint: %v`, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
clt := client.New(apiEndpoint)
|
requestBody, err := json.Marshal(&nunuxKeeperDocument{
|
||||||
clt.WithCredentials("api", c.apiKey)
|
Title: entryTitle,
|
||||||
response, err := clt.PostJSON(doc)
|
Origin: entryURL,
|
||||||
|
Content: entryContent,
|
||||||
|
ContentType: "text/html",
|
||||||
|
})
|
||||||
if err != nil {
|
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() {
|
request, err := http.NewRequest(http.MethodPost, apiEndpoint, bytes.NewReader(requestBody))
|
||||||
return fmt.Errorf("nunux-keeper: unable to send entry, status=%d", response.StatusCode)
|
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
|
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"`
|
||||||
|
}
|
||||||
|
|
|
@ -5,23 +5,24 @@ package pinboard // import "miniflux.app/v2/internal/integration/pinboard"
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/http"
|
||||||
"net/url"
|
"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 {
|
type Client struct {
|
||||||
authToken string
|
authToken string
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewClient returns a new Pinboard client.
|
|
||||||
func NewClient(authToken string) *Client {
|
func NewClient(authToken string) *Client {
|
||||||
return &Client{authToken: authToken}
|
return &Client{authToken: authToken}
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddBookmark sends a link to Pinboard.
|
func (c *Client) CreateBookmark(entryURL, entryTitle, pinboardTags string, markAsUnread bool) error {
|
||||||
func (c *Client) AddBookmark(link, title, tags string, markAsUnread bool) error {
|
|
||||||
if c.authToken == "" {
|
if c.authToken == "" {
|
||||||
return fmt.Errorf("pinboard: missing auth token")
|
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 := url.Values{}
|
||||||
values.Add("auth_token", c.authToken)
|
values.Add("auth_token", c.authToken)
|
||||||
values.Add("url", link)
|
values.Add("url", entryURL)
|
||||||
values.Add("description", title)
|
values.Add("description", entryTitle)
|
||||||
values.Add("tags", tags)
|
values.Add("tags", pinboardTags)
|
||||||
values.Add("toread", toRead)
|
values.Add("toread", toRead)
|
||||||
|
|
||||||
clt := client.New("https://api.pinboard.in/v1/posts/add?" + values.Encode())
|
apiEndpoint := "https://api.pinboard.in/v1/posts/add?" + values.Encode()
|
||||||
response, err := clt.Get()
|
request, err := http.NewRequest(http.MethodGet, apiEndpoint, nil)
|
||||||
if err != 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() {
|
request.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||||
return fmt.Errorf("pinboard: unable to send bookmark, status=%d", response.StatusCode)
|
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
|
return nil
|
||||||
|
|
|
@ -4,12 +4,13 @@
|
||||||
package pocket // import "miniflux.app/v2/internal/integration/pocket"
|
package pocket // import "miniflux.app/v2/internal/integration/pocket"
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"net/http"
|
||||||
"net/url"
|
|
||||||
|
|
||||||
"miniflux.app/v2/internal/http/client"
|
"miniflux.app/v2/internal/version"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Connector manages the authorization flow with Pocket to get a personal access token.
|
// 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.
|
// RequestToken fetches a new request token from Pocket API.
|
||||||
func (c *Connector) RequestToken(redirectURL string) (string, error) {
|
func (c *Connector) RequestToken(redirectURL string) (string, error) {
|
||||||
type req struct {
|
apiEndpoint := "https://getpocket.com/v3/oauth/request"
|
||||||
ConsumerKey string `json:"consumer_key"`
|
requestBody, err := json.Marshal(&createTokenRequest{ConsumerKey: c.consumerKey, RedirectURI: redirectURL})
|
||||||
RedirectURI string `json:"redirect_uri"`
|
|
||||||
}
|
|
||||||
|
|
||||||
clt := client.New("https://getpocket.com/v3/oauth/request")
|
|
||||||
response, err := clt.PostJSON(&req{ConsumerKey: c.consumerKey, RedirectURI: redirectURL})
|
|
||||||
if err != nil {
|
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() {
|
request, err := http.NewRequest(http.MethodPost, apiEndpoint, bytes.NewReader(requestBody))
|
||||||
return "", fmt.Errorf("pocket: unable to fetch request token, status=%d", response.StatusCode)
|
|
||||||
}
|
|
||||||
|
|
||||||
body, err := io.ReadAll(response.Body)
|
|
||||||
if err != nil {
|
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 {
|
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")
|
var result createTokenResponse
|
||||||
if code == "" {
|
if err := json.NewDecoder(response.Body).Decode(&result); err != nil {
|
||||||
return "", errors.New("pocket: code is empty")
|
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.
|
// AccessToken fetches a new access token once the end-user authorized the application.
|
||||||
func (c *Connector) AccessToken(requestToken string) (string, error) {
|
func (c *Connector) AccessToken(requestToken string) (string, error) {
|
||||||
type req struct {
|
apiEndpoint := "https://getpocket.com/v3/oauth/authorize"
|
||||||
ConsumerKey string `json:"consumer_key"`
|
requestBody, err := json.Marshal(&authorizeRequest{ConsumerKey: c.consumerKey, Code: requestToken})
|
||||||
Code string `json:"code"`
|
|
||||||
}
|
|
||||||
|
|
||||||
clt := client.New("https://getpocket.com/v3/oauth/authorize")
|
|
||||||
response, err := clt.PostJSON(&req{ConsumerKey: c.consumerKey, Code: requestToken})
|
|
||||||
if err != nil {
|
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() {
|
request, err := http.NewRequest(http.MethodPost, apiEndpoint, bytes.NewReader(requestBody))
|
||||||
return "", fmt.Errorf("pocket: unable to fetch access token, status=%d", response.StatusCode)
|
|
||||||
}
|
|
||||||
|
|
||||||
body, err := io.ReadAll(response.Body)
|
|
||||||
if err != nil {
|
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 {
|
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")
|
var result authorizeReponse
|
||||||
if token == "" {
|
if err := json.NewDecoder(response.Body).Decode(&result); err != nil {
|
||||||
return "", errors.New("pocket: access_token is empty")
|
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.
|
// AuthorizationURL returns the authorization URL for the end-user.
|
||||||
|
@ -100,3 +111,22 @@ func (c *Connector) AuthorizationURL(requestToken, redirectURL string) string {
|
||||||
redirectURL,
|
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"`
|
||||||
|
}
|
||||||
|
|
|
@ -4,51 +4,67 @@
|
||||||
package pocket // import "miniflux.app/v2/internal/integration/pocket"
|
package pocket // import "miniflux.app/v2/internal/integration/pocket"
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"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 {
|
type Client struct {
|
||||||
consumerKey string
|
consumerKey string
|
||||||
accessToken string
|
accessToken string
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewClient returns a new Pocket client.
|
|
||||||
func NewClient(consumerKey, accessToken string) *Client {
|
func NewClient(consumerKey, accessToken string) *Client {
|
||||||
return &Client{consumerKey, accessToken}
|
return &Client{consumerKey, accessToken}
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddURL sends a single link to Pocket.
|
func (c *Client) AddURL(entryURL, entryTitle string) error {
|
||||||
func (c *Client) AddURL(link, title string) error {
|
|
||||||
if c.consumerKey == "" || c.accessToken == "" {
|
if c.consumerKey == "" || c.accessToken == "" {
|
||||||
return fmt.Errorf("pocket: missing credentials")
|
return fmt.Errorf("pocket: missing consumer key or access token")
|
||||||
}
|
}
|
||||||
|
|
||||||
type body struct {
|
apiEndpoint := "https://getpocket.com/v3/add"
|
||||||
AccessToken string `json:"access_token"`
|
requestBody, err := json.Marshal(&createItemRequest{
|
||||||
ConsumerKey string `json:"consumer_key"`
|
|
||||||
Title string `json:"title,omitempty"`
|
|
||||||
URL string `json:"url"`
|
|
||||||
}
|
|
||||||
|
|
||||||
data := &body{
|
|
||||||
AccessToken: c.accessToken,
|
AccessToken: c.accessToken,
|
||||||
ConsumerKey: c.consumerKey,
|
ConsumerKey: c.consumerKey,
|
||||||
Title: title,
|
Title: entryTitle,
|
||||||
URL: link,
|
URL: entryURL,
|
||||||
}
|
})
|
||||||
|
|
||||||
clt := client.New("https://getpocket.com/v3/add")
|
|
||||||
response, err := clt.PostJSON(data)
|
|
||||||
if err != nil {
|
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() {
|
request, err := http.NewRequest(http.MethodPost, apiEndpoint, bytes.NewReader(requestBody))
|
||||||
return fmt.Errorf("pocket: unable to send url, status=%d", response.StatusCode)
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type createItemRequest struct {
|
||||||
|
AccessToken string `json:"access_token"`
|
||||||
|
ConsumerKey string `json:"consumer_key"`
|
||||||
|
Title string `json:"title,omitempty"`
|
||||||
|
URL string `json:"url"`
|
||||||
|
}
|
||||||
|
|
|
@ -6,61 +6,64 @@
|
||||||
package readwise // import "miniflux.app/v2/internal/integration/readwise"
|
package readwise // import "miniflux.app/v2/internal/integration/readwise"
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/url"
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
"miniflux.app/v2/internal/http/client"
|
"miniflux.app/v2/internal/version"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Document structure of a Readwise Reader document
|
const (
|
||||||
// This initial version accepts only the one required field, the URL
|
readwiseApiEndpoint = "https://readwise.io/api/v3/save/"
|
||||||
type Document struct {
|
defaultClientTimeout = 10 * time.Second
|
||||||
Url string `json:"url"`
|
)
|
||||||
}
|
|
||||||
|
|
||||||
// Client represents a Readwise Reader client.
|
|
||||||
type Client struct {
|
type Client struct {
|
||||||
apiKey string
|
apiKey string
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewClient returns a new Readwise Reader client.
|
|
||||||
func NewClient(apiKey string) *Client {
|
func NewClient(apiKey string) *Client {
|
||||||
return &Client{apiKey: apiKey}
|
return &Client{apiKey: apiKey}
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddEntry sends an entry to Readwise Reader.
|
func (c *Client) CreateDocument(entryURL string) error {
|
||||||
func (c *Client) AddEntry(link string) error {
|
|
||||||
if c.apiKey == "" {
|
if c.apiKey == "" {
|
||||||
return fmt.Errorf("readwise: missing API key")
|
return fmt.Errorf("readwise: missing API key")
|
||||||
}
|
}
|
||||||
|
|
||||||
doc := &Document{
|
requestBody, err := json.Marshal(&readwiseDocument{
|
||||||
Url: link,
|
URL: entryURL,
|
||||||
}
|
})
|
||||||
|
|
||||||
apiURL, err := getAPIEndpoint("https://readwise.io/api/v3/save/")
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return fmt.Errorf("readwise: unable to encode request body: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
clt := client.New(apiURL)
|
request, err := http.NewRequest(http.MethodPost, readwiseApiEndpoint, bytes.NewReader(requestBody))
|
||||||
clt.WithAuthorization("Token " + c.apiKey)
|
|
||||||
response, err := clt.PostJSON(doc)
|
|
||||||
if err != nil {
|
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() {
|
request.Header.Set("Content-Type", "application/json")
|
||||||
return fmt.Errorf("readwise: unable to send entry, status=%d", response.StatusCode)
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getAPIEndpoint(pathURL string) (string, error) {
|
type readwiseDocument struct {
|
||||||
u, err := url.Parse(pathURL)
|
URL string `json:"url"`
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("readwise: invalid API endpoint: %v", err)
|
|
||||||
}
|
|
||||||
return u.String(), nil
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,7 +29,7 @@ func NewClient(baseURL, apiSecret string) *Client {
|
||||||
return &Client{baseURL: baseURL, apiSecret: apiSecret}
|
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 == "" {
|
if c.baseURL == "" || c.apiSecret == "" {
|
||||||
return fmt.Errorf("shaarli: missing base URL or API secret")
|
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)
|
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 {
|
if err != nil {
|
||||||
return fmt.Errorf("shaarli: unable to create request: %v", err)
|
return fmt.Errorf("shaarli: unable to create request: %v", err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,7 +26,7 @@ func NewClient(baseURL, username, password string) *Client {
|
||||||
return &Client{baseURL: baseURL, username: username, password: password}
|
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 == "" {
|
if c.baseURL == "" || c.username == "" || c.password == "" {
|
||||||
return fmt.Errorf("shiori: missing base URL, username or 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)
|
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 {
|
if err != nil {
|
||||||
return fmt.Errorf("shiori: unable to create request: %v", err)
|
return fmt.Errorf("shiori: unable to create request: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
request.Header.Set("Content-Type", "application/json")
|
request.Header.Set("Content-Type", "application/json")
|
||||||
request.Header.Set("Accept", "application/json")
|
|
||||||
request.Header.Set("User-Agent", "Miniflux/"+version.Version)
|
request.Header.Set("User-Agent", "Miniflux/"+version.Version)
|
||||||
request.Header.Set("X-Session-Id", sessionID)
|
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)
|
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 {
|
if err != nil {
|
||||||
return "", fmt.Errorf("shiori: unable to create request: %v", err)
|
return "", fmt.Errorf("shiori: unable to create request: %v", err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,16 +4,20 @@
|
||||||
package wallabag // import "miniflux.app/v2/internal/integration/wallabag"
|
package wallabag // import "miniflux.app/v2/internal/integration/wallabag"
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"miniflux.app/v2/internal/http/client"
|
|
||||||
"miniflux.app/v2/internal/urllib"
|
"miniflux.app/v2/internal/urllib"
|
||||||
|
"miniflux.app/v2/internal/version"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Client represents a Wallabag client.
|
const defaultClientTimeout = 10 * time.Second
|
||||||
|
|
||||||
type Client struct {
|
type Client struct {
|
||||||
baseURL string
|
baseURL string
|
||||||
clientID string
|
clientID string
|
||||||
|
@ -23,16 +27,13 @@ type Client struct {
|
||||||
onlyURL bool
|
onlyURL bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewClient returns a new Wallabag client.
|
|
||||||
func NewClient(baseURL, clientID, clientSecret, username, password string, onlyURL bool) *Client {
|
func NewClient(baseURL, clientID, clientSecret, username, password string, onlyURL bool) *Client {
|
||||||
return &Client{baseURL, clientID, clientSecret, username, password, onlyURL}
|
return &Client{baseURL, clientID, clientSecret, username, password, onlyURL}
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddEntry sends a link to Wallabag.
|
func (c *Client) CreateEntry(entryURL, entryTitle, entryContent string) error {
|
||||||
// Pass an empty string in `content` to let Wallabag fetch the article content.
|
|
||||||
func (c *Client) AddEntry(link, title, content string) error {
|
|
||||||
if c.baseURL == "" || c.clientID == "" || c.clientSecret == "" || c.username == "" || c.password == "" {
|
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()
|
accessToken, err := c.getAccessToken()
|
||||||
|
@ -40,29 +41,47 @@ func (c *Client) AddEntry(link, title, content string) error {
|
||||||
return err
|
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 {
|
func (c *Client) createEntry(accessToken, entryURL, entryTitle, entryContent string) error {
|
||||||
endpoint, err := urllib.JoinBaseURLAndPath(c.baseURL, "/api/entries.json")
|
apiEndpoint, err := urllib.JoinBaseURLAndPath(c.baseURL, "/api/entries.json")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("wallbag: unable to generate entries endpoint: %v", err)
|
return fmt.Errorf("wallbag: unable to generate entries endpoint: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
data := map[string]string{"url": link, "title": title}
|
if c.onlyURL {
|
||||||
if !c.onlyURL {
|
entryContent = ""
|
||||||
data["content"] = content
|
|
||||||
}
|
}
|
||||||
|
|
||||||
clt := client.New(endpoint)
|
requestBody, err := json.Marshal(&createEntryRequest{
|
||||||
clt.WithAuthorization("Bearer " + accessToken)
|
URL: entryURL,
|
||||||
response, err := clt.PostJSON(data)
|
Title: entryTitle,
|
||||||
|
Content: entryContent,
|
||||||
|
})
|
||||||
if err != nil {
|
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() {
|
request, err := http.NewRequest(http.MethodPost, apiEndpoint, bytes.NewReader(requestBody))
|
||||||
return fmt.Errorf("wallabag: request failed using %q, status=%d", endpoint, response.StatusCode)
|
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
|
return nil
|
||||||
|
@ -76,27 +95,37 @@ func (c *Client) getAccessToken() (string, error) {
|
||||||
values.Add("username", c.username)
|
values.Add("username", c.username)
|
||||||
values.Add("password", c.password)
|
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 {
|
if err != nil {
|
||||||
return "", fmt.Errorf("wallbag: unable to generate token endpoint: %v", err)
|
return "", fmt.Errorf("wallbag: unable to generate token endpoint: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
clt := client.New(endpoint)
|
request, err := http.NewRequest(http.MethodPost, apiEndpoint, strings.NewReader(values.Encode()))
|
||||||
response, err := clt.PostForm(values)
|
|
||||||
if err != nil {
|
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() {
|
request.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||||
return "", fmt.Errorf("wallabag: request failed using %q, status=%d", endpoint, response.StatusCode)
|
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 {
|
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 {
|
type tokenResponse struct {
|
||||||
|
@ -107,13 +136,8 @@ type tokenResponse struct {
|
||||||
TokenType string `json:"token_type"`
|
TokenType string `json:"token_type"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func decodeTokenResponse(body io.Reader) (*tokenResponse, error) {
|
type createEntryRequest struct {
|
||||||
var token tokenResponse
|
URL string `json:"url"`
|
||||||
|
Title string `json:"title"`
|
||||||
decoder := json.NewDecoder(body)
|
Content string `json:"content,omitempty"`
|
||||||
if err := decoder.Decode(&token); err != nil {
|
|
||||||
return nil, fmt.Errorf("wallabag: unable to decode token response: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return &token, nil
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -357,7 +357,7 @@
|
||||||
"form.integration.notion_token": "Notion Secret Token",
|
"form.integration.notion_token": "Notion Secret Token",
|
||||||
"form.integration.apprise_activate": "Push entries to Apprise",
|
"form.integration.apprise_activate": "Push entries to Apprise",
|
||||||
"form.integration.apprise_url": "Apprise API URL",
|
"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_activate": "Artikel in Nunux Keeper speichern",
|
||||||
"form.integration.nunux_keeper_endpoint": "Nunux Keeper API-Endpunkt",
|
"form.integration.nunux_keeper_endpoint": "Nunux Keeper API-Endpunkt",
|
||||||
"form.integration.nunux_keeper_api_key": "Nunux Keeper API-Schlüssel",
|
"form.integration.nunux_keeper_api_key": "Nunux Keeper API-Schlüssel",
|
||||||
|
|
|
@ -357,7 +357,7 @@
|
||||||
"form.integration.notion_token": "Notion Secret Token",
|
"form.integration.notion_token": "Notion Secret Token",
|
||||||
"form.integration.apprise_activate": "Push entries to Apprise",
|
"form.integration.apprise_activate": "Push entries to Apprise",
|
||||||
"form.integration.apprise_url": "Apprise API URL",
|
"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_activate": "Αποθήκευση άρθρων στο Nunux Keeper",
|
||||||
"form.integration.nunux_keeper_endpoint": "Τελικό σημείο Nunux Keeper API",
|
"form.integration.nunux_keeper_endpoint": "Τελικό σημείο Nunux Keeper API",
|
||||||
"form.integration.nunux_keeper_api_key": "Κλειδί API Nunux Keeper",
|
"form.integration.nunux_keeper_api_key": "Κλειδί API Nunux Keeper",
|
||||||
|
|
|
@ -357,7 +357,7 @@
|
||||||
"form.integration.notion_token": "Notion Secret Token",
|
"form.integration.notion_token": "Notion Secret Token",
|
||||||
"form.integration.apprise_activate": "Push entries to Apprise",
|
"form.integration.apprise_activate": "Push entries to Apprise",
|
||||||
"form.integration.apprise_url": "Apprise API URL",
|
"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_activate": "Save entries to Nunux Keeper",
|
||||||
"form.integration.nunux_keeper_endpoint": "Nunux Keeper API Endpoint",
|
"form.integration.nunux_keeper_endpoint": "Nunux Keeper API Endpoint",
|
||||||
"form.integration.nunux_keeper_api_key": "Nunux Keeper API key",
|
"form.integration.nunux_keeper_api_key": "Nunux Keeper API key",
|
||||||
|
|
|
@ -357,7 +357,7 @@
|
||||||
"form.integration.notion_token": "Notion Secret Token",
|
"form.integration.notion_token": "Notion Secret Token",
|
||||||
"form.integration.apprise_activate": "Push entries to Apprise",
|
"form.integration.apprise_activate": "Push entries to Apprise",
|
||||||
"form.integration.apprise_url": "Apprise API URL",
|
"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_activate": "Enviar artículos a Nunux Keeper",
|
||||||
"form.integration.nunux_keeper_endpoint": "Acceso API de Nunux Keeper",
|
"form.integration.nunux_keeper_endpoint": "Acceso API de Nunux Keeper",
|
||||||
"form.integration.nunux_keeper_api_key": "Clave de API de Nunux Keeper",
|
"form.integration.nunux_keeper_api_key": "Clave de API de Nunux Keeper",
|
||||||
|
|
|
@ -357,7 +357,7 @@
|
||||||
"form.integration.notion_token": "Notion Secret Token",
|
"form.integration.notion_token": "Notion Secret Token",
|
||||||
"form.integration.apprise_activate": "Push entries to Apprise",
|
"form.integration.apprise_activate": "Push entries to Apprise",
|
||||||
"form.integration.apprise_url": "Apprise API URL",
|
"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_activate": "Tallenna artikkelit Nunux Keeperiin",
|
||||||
"form.integration.nunux_keeper_endpoint": "Nunux Keeper API-päätepiste",
|
"form.integration.nunux_keeper_endpoint": "Nunux Keeper API-päätepiste",
|
||||||
"form.integration.nunux_keeper_api_key": "Nunux Keeper API-avain",
|
"form.integration.nunux_keeper_api_key": "Nunux Keeper API-avain",
|
||||||
|
|
|
@ -353,11 +353,11 @@
|
||||||
"form.integration.wallabag_username": "Nom d'utilisateur de Wallabag",
|
"form.integration.wallabag_username": "Nom d'utilisateur de Wallabag",
|
||||||
"form.integration.wallabag_password": "Mot de passe de Wallabag",
|
"form.integration.wallabag_password": "Mot de passe de Wallabag",
|
||||||
"form.integration.notion_activate": "Sauvegarder les articles vers Notion",
|
"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.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_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_activate": "Sauvegarder les articles vers Nunux Keeper",
|
||||||
"form.integration.nunux_keeper_endpoint": "URL de l'API de 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",
|
"form.integration.nunux_keeper_api_key": "Clé d'API de Nunux Keeper",
|
||||||
|
|
|
@ -357,7 +357,7 @@
|
||||||
"form.integration.notion_token": "Notion Secret Token",
|
"form.integration.notion_token": "Notion Secret Token",
|
||||||
"form.integration.apprise_activate": "Push entries to Apprise",
|
"form.integration.apprise_activate": "Push entries to Apprise",
|
||||||
"form.integration.apprise_url": "Apprise API URL",
|
"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_activate": "विषय-वस्तु को ननक्स कीपर में सहेजें",
|
||||||
"form.integration.nunux_keeper_endpoint": "ननक्स कीपर एपीआई समापन बिंदु",
|
"form.integration.nunux_keeper_endpoint": "ननक्स कीपर एपीआई समापन बिंदु",
|
||||||
"form.integration.nunux_keeper_api_key": "ननक्स कीपर एपीआई कुंजी",
|
"form.integration.nunux_keeper_api_key": "ननक्स कीपर एपीआई कुंजी",
|
||||||
|
|
|
@ -354,7 +354,7 @@
|
||||||
"form.integration.notion_token": "Notion Secret Token",
|
"form.integration.notion_token": "Notion Secret Token",
|
||||||
"form.integration.apprise_activate": "Push entries to Apprise",
|
"form.integration.apprise_activate": "Push entries to Apprise",
|
||||||
"form.integration.apprise_url": "Apprise API URL",
|
"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_activate": "Simpan artikel ke Nunux Keeper",
|
||||||
"form.integration.nunux_keeper_endpoint": "Titik URL API Nunux Keeper",
|
"form.integration.nunux_keeper_endpoint": "Titik URL API Nunux Keeper",
|
||||||
"form.integration.nunux_keeper_api_key": "Kunci API Nunux Keeper",
|
"form.integration.nunux_keeper_api_key": "Kunci API Nunux Keeper",
|
||||||
|
|
|
@ -357,7 +357,7 @@
|
||||||
"form.integration.notion_token": "Notion Secret Token",
|
"form.integration.notion_token": "Notion Secret Token",
|
||||||
"form.integration.apprise_activate": "Push entries to Apprise",
|
"form.integration.apprise_activate": "Push entries to Apprise",
|
||||||
"form.integration.apprise_url": "Apprise API URL",
|
"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_activate": "Salva gli articoli su Nunux Keeper",
|
||||||
"form.integration.nunux_keeper_endpoint": "Endpoint dell'API di 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",
|
"form.integration.nunux_keeper_api_key": "API key dell'account Nunux Keeper",
|
||||||
|
|
|
@ -357,7 +357,7 @@
|
||||||
"form.integration.notion_token": "Notion Secret Token",
|
"form.integration.notion_token": "Notion Secret Token",
|
||||||
"form.integration.apprise_activate": "Push entries to Apprise",
|
"form.integration.apprise_activate": "Push entries to Apprise",
|
||||||
"form.integration.apprise_url": "Apprise API URL",
|
"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_activate": "Nunux Keeper に記事を保存する",
|
||||||
"form.integration.nunux_keeper_endpoint": "Nunux Keeper の API Endpoint",
|
"form.integration.nunux_keeper_endpoint": "Nunux Keeper の API Endpoint",
|
||||||
"form.integration.nunux_keeper_api_key": "Nunux Keeper の API key",
|
"form.integration.nunux_keeper_api_key": "Nunux Keeper の API key",
|
||||||
|
|
|
@ -357,7 +357,7 @@
|
||||||
"form.integration.notion_token": "Notion Secret Token",
|
"form.integration.notion_token": "Notion Secret Token",
|
||||||
"form.integration.apprise_activate": "Push entries to Apprise",
|
"form.integration.apprise_activate": "Push entries to Apprise",
|
||||||
"form.integration.apprise_url": "Apprise API URL",
|
"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_activate": "Opslaan naar Nunux Keeper",
|
||||||
"form.integration.nunux_keeper_endpoint": "Nunux Keeper URL",
|
"form.integration.nunux_keeper_endpoint": "Nunux Keeper URL",
|
||||||
"form.integration.nunux_keeper_api_key": "Nunux Keeper API-sleutel",
|
"form.integration.nunux_keeper_api_key": "Nunux Keeper API-sleutel",
|
||||||
|
|
|
@ -359,7 +359,7 @@
|
||||||
"form.integration.notion_token": "Notion Secret Token",
|
"form.integration.notion_token": "Notion Secret Token",
|
||||||
"form.integration.apprise_activate": "Push entries to Apprise",
|
"form.integration.apprise_activate": "Push entries to Apprise",
|
||||||
"form.integration.apprise_url": "Apprise API URL",
|
"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_activate": "Zapisz artykuly do Nunux Keeper",
|
||||||
"form.integration.nunux_keeper_endpoint": "Nunux Keeper URL",
|
"form.integration.nunux_keeper_endpoint": "Nunux Keeper URL",
|
||||||
"form.integration.nunux_keeper_api_key": "Nunux Keeper API key",
|
"form.integration.nunux_keeper_api_key": "Nunux Keeper API key",
|
||||||
|
|
|
@ -357,7 +357,7 @@
|
||||||
"form.integration.notion_token": "Notion Secret Token",
|
"form.integration.notion_token": "Notion Secret Token",
|
||||||
"form.integration.apprise_activate": "Push entries to Apprise",
|
"form.integration.apprise_activate": "Push entries to Apprise",
|
||||||
"form.integration.apprise_url": "Apprise API URL",
|
"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_activate": "Salvar itens no Nunux Keeper",
|
||||||
"form.integration.nunux_keeper_endpoint": "Endpoint de API do 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",
|
"form.integration.nunux_keeper_api_key": "Chave de API do Nunux Keeper",
|
||||||
|
|
|
@ -359,7 +359,7 @@
|
||||||
"form.integration.notion_token": "Notion Secret Token",
|
"form.integration.notion_token": "Notion Secret Token",
|
||||||
"form.integration.apprise_activate": "Push entries to Apprise",
|
"form.integration.apprise_activate": "Push entries to Apprise",
|
||||||
"form.integration.apprise_url": "Apprise API URL",
|
"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_activate": "Сохранять статьи в Nunux Keeper",
|
||||||
"form.integration.nunux_keeper_endpoint": "Конечная точка Nunux Keeper API",
|
"form.integration.nunux_keeper_endpoint": "Конечная точка Nunux Keeper API",
|
||||||
"form.integration.nunux_keeper_api_key": "API-ключ Nunux Keeper",
|
"form.integration.nunux_keeper_api_key": "API-ключ Nunux Keeper",
|
||||||
|
|
|
@ -357,7 +357,7 @@
|
||||||
"form.integration.notion_token": "Notion Secret Token",
|
"form.integration.notion_token": "Notion Secret Token",
|
||||||
"form.integration.apprise_activate": "Push entries to Apprise",
|
"form.integration.apprise_activate": "Push entries to Apprise",
|
||||||
"form.integration.apprise_url": "Apprise API URL",
|
"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_activate": "Makaleleri Nunux Keeper'a kaydet",
|
||||||
"form.integration.nunux_keeper_endpoint": "Nunux Keeper API Uç Noktası",
|
"form.integration.nunux_keeper_endpoint": "Nunux Keeper API Uç Noktası",
|
||||||
"form.integration.nunux_keeper_api_key": "Nunux Keeper API anahtarı",
|
"form.integration.nunux_keeper_api_key": "Nunux Keeper API anahtarı",
|
||||||
|
|
|
@ -360,7 +360,7 @@
|
||||||
"form.integration.notion_token": "Notion Secret Token",
|
"form.integration.notion_token": "Notion Secret Token",
|
||||||
"form.integration.apprise_activate": "Push entries to Apprise",
|
"form.integration.apprise_activate": "Push entries to Apprise",
|
||||||
"form.integration.apprise_url": "Apprise API URL",
|
"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_activate": "Зберігати статті до Nunux Keeper",
|
||||||
"form.integration.nunux_keeper_endpoint": "Nunux Keeper API Endpoint",
|
"form.integration.nunux_keeper_endpoint": "Nunux Keeper API Endpoint",
|
||||||
"form.integration.nunux_keeper_api_key": "Ключ API Nunux Keeper",
|
"form.integration.nunux_keeper_api_key": "Ключ API Nunux Keeper",
|
||||||
|
|
|
@ -355,7 +355,7 @@
|
||||||
"form.integration.notion_token": "Notion Secret Token",
|
"form.integration.notion_token": "Notion Secret Token",
|
||||||
"form.integration.apprise_activate": "Push entries to Apprise",
|
"form.integration.apprise_activate": "Push entries to Apprise",
|
||||||
"form.integration.apprise_url": "Apprise API URL",
|
"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_activate": "保存文章到 Nunux Keeper",
|
||||||
"form.integration.nunux_keeper_endpoint": "Nunux Keeper API 端点",
|
"form.integration.nunux_keeper_endpoint": "Nunux Keeper API 端点",
|
||||||
"form.integration.nunux_keeper_api_key": "Nunux Keeper API 密钥",
|
"form.integration.nunux_keeper_api_key": "Nunux Keeper API 密钥",
|
||||||
|
|
|
@ -357,7 +357,7 @@
|
||||||
"form.integration.notion_token": "Notion Secret Token",
|
"form.integration.notion_token": "Notion Secret Token",
|
||||||
"form.integration.apprise_activate": "Push entries to Apprise",
|
"form.integration.apprise_activate": "Push entries to Apprise",
|
||||||
"form.integration.apprise_url": "Apprise API URL",
|
"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_activate": "儲存文章到 Nunux Keeper",
|
||||||
"form.integration.nunux_keeper_endpoint": "Nunux Keeper API 端點",
|
"form.integration.nunux_keeper_endpoint": "Nunux Keeper API 端點",
|
||||||
"form.integration.nunux_keeper_api_key": "Nunux Keeper API 金鑰",
|
"form.integration.nunux_keeper_api_key": "Nunux Keeper API 金鑰",
|
||||||
|
|
Loading…
Reference in New Issue