Use stdlib HTTP client for third-party integrations
This commit is contained in:
parent
e5d9f2f5a0
commit
5e520ca5bf
34 changed files with 509 additions and 358 deletions
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
||||
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"`
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"`
|
||||
}
|
||||
|
|
|
@ -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"`
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
||||
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"`
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"`
|
||||
}
|
||||
|
|
|
@ -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"`
|
||||
}
|
||||
|
|
|
@ -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"`
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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"`
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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": "ननक्स कीपर एपीआई कुंजी",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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ı",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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 密钥",
|
||||
|
|
|
@ -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 金鑰",
|
||||
|
|
Loading…
Reference in a new issue