Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use stdlib HTTP client for third-party integrations #2023

Merged
merged 1 commit into from
Aug 15, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 37 additions & 30 deletions internal/integration/apprise/apprise.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
return fmt.Errorf(`apprise: invalid API endpoint: %v`, err)
}

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)
}

request, err := http.NewRequest(http.MethodPost, apiEndpoint, bytes.NewReader(requestBody))
if err != nil {
apiEndpoint, err := urllib.JoinBaseURLAndPath(c.baseURL, "/notify")
if err != nil {
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)
}

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])
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
Expand Down
9 changes: 0 additions & 9 deletions internal/integration/apprise/wrapper.go

This file was deleted.

66 changes: 42 additions & 24 deletions internal/integration/espial/espial.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
apiEndpoint, err := urllib.JoinBaseURLAndPath(c.baseURL, "/api/add")
if err != nil {
return fmt.Errorf("espial: invalid API endpoint: %v", err)
}

requestBody, err := json.Marshal(&espialDocument{
Title: entryTitle,
Url: entryURL,
ToRead: true,
Tags: tags,
Tags: espialTags,
})

if err != nil {
return fmt.Errorf("espial: unable to encode request body: %v", err)
}

apiEndpoint, err := urllib.JoinBaseURLAndPath(c.baseURL, "/api/add")
request, err := http.NewRequest(http.MethodPost, apiEndpoint, bytes.NewReader(requestBody))
if err != nil {
return fmt.Errorf(`espial: invalid API endpoint: %v`, err)
return fmt.Errorf("espial: unable to create request: %v", err)
}

clt := client.New(apiEndpoint)
clt.WithAuthorization("ApiKey " + c.apiKey)
response, err := clt.PostJSON(doc)
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 entry: %v", err)
return fmt.Errorf("espial: unable to send request: %v", err)
}
defer response.Body.Close()

if response.HasServerFailure() {
return fmt.Errorf("espial: unable to send entry, status=%d", response.StatusCode)
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"`
}
40 changes: 25 additions & 15 deletions internal/integration/instapaper/instapaper.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)

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 create request: %v", err)
}

request.SetBasicAuth(c.username, c.password)
request.Header.Set("Content-Type", "application/x-www-form-urlencoded")
request.Header.Set("User-Agent", "Miniflux/"+version.Version)

apiURL := "https://www.instapaper.com/api/add?" + values.Encode()
clt := client.New(apiURL)
clt.WithCredentials(c.username, c.password)
response, err := clt.Get()
httpClient := &http.Client{Timeout: defaultClientTimeout}
response, err := httpClient.Do(request)
if err != nil {
return fmt.Errorf("instapaper: unable to send url: %v", err)
return fmt.Errorf("instapaper: unable to send request: %v", err)
}
defer response.Body.Close()

if response.HasServerFailure() {
return fmt.Errorf("instapaper: unable to send url, status=%d", response.StatusCode)
if response.StatusCode != http.StatusCreated {
return fmt.Errorf("instapaper: unable to add URL: url=%s status=%d", apiEndpoint, response.StatusCode)
}

return nil
Expand Down
22 changes: 11 additions & 11 deletions internal/integration/integration.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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)
}
}
Expand All @@ -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)
}
}
Expand All @@ -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)
}
}

Expand All @@ -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)
}
}
Expand All @@ -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)
}
}
Expand All @@ -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)
}
}
Expand All @@ -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)
}
}
Expand Down Expand Up @@ -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)
}
}
Expand Down
Loading
Loading