refactor(integration): factorize JSON request construction
Every integration talking to a JSON API hand-rolled the same dance: json.Marshal the payload, build an *http.Request, set the Content-Type and User-Agent headers, then run it through NewClientWithOptions. This is near-identical copy-paste, so any change done to how requests are done had to be repeated in each one. This commit introduces a small request builder in internal/http/client and route every integration through it: ``` client.NewRequestBuilder(endpoint). WithMethod(http.MethodPost). WithJSON(payload). WithHeaders(extraHeaders). Do() ``` Centralizing the logic also evens out disparities that had crept in between integrations: - private networks are now blocked everywhere, honoring INTEGRATION_ALLOW_PRIVATE_NETWORKS; notion previously did not block them; - request errors are wrapped consistently with %w instead of a mix of %v/%w; - the per-integration defaultClientTimeout constant is replaced by a single shared default in the client package; - the Content-Type and Miniflux User-Agent headers are always set. Reviewed-By: gudvinr
This commit is contained in:
@@ -4,16 +4,23 @@
|
||||
package client // import "miniflux.app/v2/internal/http/client"
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"miniflux.app/v2/internal/config"
|
||||
"miniflux.app/v2/internal/urllib"
|
||||
"miniflux.app/v2/internal/version"
|
||||
)
|
||||
|
||||
const defaultRequestTimeout = 10 * time.Second
|
||||
|
||||
// ErrPrivateNetwork is returned when a connection to a private network is blocked.
|
||||
var ErrPrivateNetwork = errors.New("client: connection to private network is blocked")
|
||||
|
||||
@@ -68,3 +75,93 @@ func NewClientWithOptions(opts Options) *http.Client {
|
||||
Transport: transport,
|
||||
}
|
||||
}
|
||||
|
||||
// requestBuilder builds and executes HTTP requests with the builder pattern.
|
||||
type requestBuilder struct {
|
||||
err error
|
||||
endpoint string
|
||||
method string
|
||||
body io.Reader
|
||||
headers http.Header
|
||||
}
|
||||
|
||||
// NewRequestBuilder creates a new request builder for the given endpoint.
|
||||
func NewRequestBuilder(endpoint string) *requestBuilder {
|
||||
return &requestBuilder{
|
||||
endpoint: endpoint,
|
||||
method: http.MethodGet,
|
||||
headers: make(http.Header),
|
||||
}
|
||||
}
|
||||
|
||||
// WithMethod sets the HTTP method.
|
||||
func (r *requestBuilder) WithMethod(method string) *requestBuilder {
|
||||
r.method = method
|
||||
return r
|
||||
}
|
||||
|
||||
// WithHeader sets a header value.
|
||||
func (r *requestBuilder) WithHeader(key, value string) *requestBuilder {
|
||||
r.headers.Set(key, value)
|
||||
return r
|
||||
}
|
||||
|
||||
// WithJSON marshals payload as JSON, sets the body and Content-Type.
|
||||
func (r *requestBuilder) WithJSON(payload any) *requestBuilder {
|
||||
requestBody, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
r.err = fmt.Errorf("unable to encode request body: %w", err)
|
||||
return r
|
||||
}
|
||||
|
||||
return r.WithJSONBody(requestBody)
|
||||
}
|
||||
|
||||
// WithJSONBody sets an already-marshaled JSON body and the Content-Type.
|
||||
// It is useful when the caller needs the encoded payload for another
|
||||
// purpose (e.g. computing a signature) to avoid marshaling it twice.
|
||||
func (r *requestBuilder) WithJSONBody(body []byte) *requestBuilder {
|
||||
r.body = bytes.NewReader(body)
|
||||
r.headers.Set("Content-Type", "application/json")
|
||||
return r
|
||||
}
|
||||
|
||||
// Do builds and executes the request.
|
||||
//
|
||||
// Private networks are blocked unless explicitly allowed through the
|
||||
// INTEGRATION_ALLOW_PRIVATE_NETWORKS option.
|
||||
func (r *requestBuilder) Do() (*http.Response, error) {
|
||||
if r.err != nil {
|
||||
return nil, r.err
|
||||
}
|
||||
|
||||
// The request is assembled lazily here rather than being stored as a
|
||||
// prebuilt *http.Request in the builder: http.NewRequest inspects the
|
||||
// body's concrete type (e.g. *bytes.Reader) to populate ContentLength and
|
||||
// GetBody. Constructing it only once the body is known yields a correct
|
||||
// Content-Length header and lets the client replay the body on redirects.
|
||||
req, err := http.NewRequest(r.method, r.endpoint, r.body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to create request: %w", err)
|
||||
}
|
||||
|
||||
for key, values := range r.headers {
|
||||
for _, value := range values {
|
||||
req.Header.Add(key, value)
|
||||
}
|
||||
}
|
||||
|
||||
req.Header.Set("User-Agent", "Miniflux/"+version.Version)
|
||||
|
||||
clientOptions := Options{
|
||||
Timeout: defaultRequestTimeout,
|
||||
BlockPrivateNetworks: !config.Opts.IntegrationAllowPrivateNetworks(),
|
||||
}
|
||||
|
||||
response, err := NewClientWithOptions(clientOptions).Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to send request: %w", err)
|
||||
}
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
@@ -5,11 +5,15 @@ package client
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"miniflux.app/v2/internal/config"
|
||||
"miniflux.app/v2/internal/version"
|
||||
)
|
||||
|
||||
func TestNewClientWithoutBlockingPrivateNetworks(t *testing.T) {
|
||||
@@ -111,3 +115,73 @@ func TestBlockPrivateNetworksAllowsLoopbackWhenDisabled(t *testing.T) {
|
||||
t.Fatalf("Expected status 200, got %d", resp.StatusCode)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRequestBuilderWithJSON(t *testing.T) {
|
||||
configureIntegrationAllowPrivateNetworksOption(t)
|
||||
|
||||
var gotMethod, gotContentType, gotUserAgent, gotAuth, gotBody string
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
gotMethod = r.Method
|
||||
gotContentType = r.Header.Get("Content-Type")
|
||||
gotUserAgent = r.Header.Get("User-Agent")
|
||||
gotAuth = r.Header.Get("Authorization")
|
||||
body, _ := io.ReadAll(r.Body)
|
||||
gotBody = string(body)
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
response, err := NewRequestBuilder(server.URL).
|
||||
WithMethod(http.MethodPost).
|
||||
WithHeader("Authorization", "Bearer secret").
|
||||
WithJSON(map[string]string{"hello": "world"}).
|
||||
Do()
|
||||
if err != nil {
|
||||
t.Fatalf("request execution failed: %v", err)
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
if response.StatusCode != http.StatusCreated {
|
||||
t.Errorf("expected status %d, got %d", http.StatusCreated, response.StatusCode)
|
||||
}
|
||||
if gotMethod != http.MethodPost {
|
||||
t.Errorf("expected method POST, got %s", gotMethod)
|
||||
}
|
||||
if gotContentType != "application/json" {
|
||||
t.Errorf("expected Content-Type application/json, got %q", gotContentType)
|
||||
}
|
||||
if want := "Miniflux/" + version.Version; gotUserAgent != want {
|
||||
t.Errorf("expected User-Agent %q, got %q", want, gotUserAgent)
|
||||
}
|
||||
if gotAuth != "Bearer secret" {
|
||||
t.Errorf("expected Authorization %q, got %q", "Bearer secret", gotAuth)
|
||||
}
|
||||
if gotBody != `{"hello":"world"}` {
|
||||
t.Errorf("expected body %q, got %q", `{"hello":"world"}`, gotBody)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRequestBuilderWithInvalidEndpoint(t *testing.T) {
|
||||
_, err := NewRequestBuilder("://invalid").WithMethod(http.MethodPost).WithJSON(nil).Do()
|
||||
if err == nil {
|
||||
t.Fatal("expected an error for an invalid endpoint, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
func configureIntegrationAllowPrivateNetworksOption(t *testing.T) {
|
||||
t.Helper()
|
||||
|
||||
t.Setenv("INTEGRATION_ALLOW_PRIVATE_NETWORKS", "1")
|
||||
|
||||
configParser := config.NewConfigParser()
|
||||
parsedOptions, err := configParser.ParseEnvironmentVariables()
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to configure test options: %v", err)
|
||||
}
|
||||
|
||||
previousOptions := config.Opts
|
||||
config.Opts = parsedOptions
|
||||
t.Cleanup(func() {
|
||||
config.Opts = previousOptions
|
||||
})
|
||||
}
|
||||
|
||||
@@ -6,21 +6,13 @@
|
||||
package cubox // import "miniflux.app/v2/internal/integration/cubox"
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"miniflux.app/v2/internal/config"
|
||||
"miniflux.app/v2/internal/http/client"
|
||||
"miniflux.app/v2/internal/version"
|
||||
)
|
||||
|
||||
const defaultClientTimeout = 10 * time.Second
|
||||
|
||||
type Client struct {
|
||||
apiLink string
|
||||
}
|
||||
@@ -34,28 +26,15 @@ func (c *Client) SaveLink(entryURL string) error {
|
||||
return errors.New("cubox: missing API link")
|
||||
}
|
||||
|
||||
requestBody, err := json.Marshal(&card{
|
||||
Type: "url",
|
||||
Content: entryURL,
|
||||
})
|
||||
response, err := client.NewRequestBuilder(c.apiLink).
|
||||
WithMethod(http.MethodPost).
|
||||
WithJSON(&card{
|
||||
Type: "url",
|
||||
Content: entryURL,
|
||||
}).
|
||||
Do()
|
||||
if err != nil {
|
||||
return fmt.Errorf("cubox: unable to encode request body: %w", err)
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultClientTimeout)
|
||||
defer cancel()
|
||||
|
||||
request, err := http.NewRequestWithContext(ctx, http.MethodPost, c.apiLink, bytes.NewReader(requestBody))
|
||||
if err != nil {
|
||||
return fmt.Errorf("cubox: unable to create request: %w", err)
|
||||
}
|
||||
|
||||
request.Header.Set("Content-Type", "application/json")
|
||||
request.Header.Set("User-Agent", "Miniflux/"+version.Version)
|
||||
|
||||
response, err := client.NewClientWithOptions(client.Options{Timeout: defaultClientTimeout, BlockPrivateNetworks: !config.Opts.IntegrationAllowPrivateNetworks()}).Do(request)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cubox: unable to send request: %w", err)
|
||||
return fmt.Errorf("cubox: %w", err)
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
|
||||
@@ -6,21 +6,15 @@
|
||||
package discord // import "miniflux.app/v2/internal/integration/discord"
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"miniflux.app/v2/internal/config"
|
||||
"miniflux.app/v2/internal/http/client"
|
||||
"miniflux.app/v2/internal/model"
|
||||
"miniflux.app/v2/internal/urllib"
|
||||
"miniflux.app/v2/internal/version"
|
||||
)
|
||||
|
||||
const defaultClientTimeout = 10 * time.Second
|
||||
const discordMsgColor = 5793266
|
||||
|
||||
type Client struct {
|
||||
@@ -33,56 +27,45 @@ func NewClient(webhookURL string) *Client {
|
||||
|
||||
func (c *Client) SendDiscordMsg(feed *model.Feed, entries model.Entries) error {
|
||||
for _, entry := range entries {
|
||||
requestBody, err := json.Marshal(&discordMessage{
|
||||
Embeds: []discordEmbed{
|
||||
{
|
||||
Title: "RSS feed update from Miniflux",
|
||||
Color: discordMsgColor,
|
||||
Fields: []discordFields{
|
||||
{
|
||||
Name: "Updated feed",
|
||||
Value: feed.Title,
|
||||
},
|
||||
{
|
||||
Name: "Article link",
|
||||
Value: "[" + entry.Title + "]" + "(" + entry.URL + ")",
|
||||
},
|
||||
{
|
||||
Name: "Author",
|
||||
Value: entry.Author,
|
||||
Inline: true,
|
||||
},
|
||||
{
|
||||
Name: "Source website",
|
||||
Value: urllib.RootURL(feed.SiteURL),
|
||||
Inline: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("discord: unable to encode request body: %v", err)
|
||||
}
|
||||
|
||||
request, err := http.NewRequest(http.MethodPost, c.webhookURL, bytes.NewReader(requestBody))
|
||||
if err != nil {
|
||||
return fmt.Errorf("discord: unable to create request: %v", err)
|
||||
}
|
||||
|
||||
request.Header.Set("Content-Type", "application/json")
|
||||
request.Header.Set("User-Agent", "Miniflux/"+version.Version)
|
||||
|
||||
slog.Debug("Sending Discord notification",
|
||||
slog.String("webhookURL", c.webhookURL),
|
||||
slog.String("title", feed.Title),
|
||||
slog.String("entry_url", entry.URL),
|
||||
)
|
||||
|
||||
httpClient := client.NewClientWithOptions(client.Options{Timeout: defaultClientTimeout, BlockPrivateNetworks: !config.Opts.IntegrationAllowPrivateNetworks()})
|
||||
response, err := httpClient.Do(request)
|
||||
response, err := client.NewRequestBuilder(c.webhookURL).
|
||||
WithMethod(http.MethodPost).
|
||||
WithJSON(&discordMessage{
|
||||
Embeds: []discordEmbed{
|
||||
{
|
||||
Title: "RSS feed update from Miniflux",
|
||||
Color: discordMsgColor,
|
||||
Fields: []discordFields{
|
||||
{
|
||||
Name: "Updated feed",
|
||||
Value: feed.Title,
|
||||
},
|
||||
{
|
||||
Name: "Article link",
|
||||
Value: "[" + entry.Title + "]" + "(" + entry.URL + ")",
|
||||
},
|
||||
{
|
||||
Name: "Author",
|
||||
Value: entry.Author,
|
||||
Inline: true,
|
||||
},
|
||||
{
|
||||
Name: "Source website",
|
||||
Value: urllib.RootURL(feed.SiteURL),
|
||||
Inline: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}).
|
||||
Do()
|
||||
if err != nil {
|
||||
return fmt.Errorf("discord: unable to send request: %v", err)
|
||||
return fmt.Errorf("discord: %w", err)
|
||||
}
|
||||
response.Body.Close()
|
||||
|
||||
|
||||
@@ -5,20 +5,14 @@ package espial // import "miniflux.app/v2/internal/integration/espial"
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"miniflux.app/v2/internal/config"
|
||||
"miniflux.app/v2/internal/http/client"
|
||||
"miniflux.app/v2/internal/urllib"
|
||||
"miniflux.app/v2/internal/version"
|
||||
)
|
||||
|
||||
const defaultClientTimeout = 10 * time.Second
|
||||
|
||||
type Client struct {
|
||||
baseURL string
|
||||
apiKey string
|
||||
@@ -38,30 +32,18 @@ func (c *Client) CreateLink(entryURL, entryTitle, espialTags string) error {
|
||||
return fmt.Errorf("espial: invalid API endpoint: %v", err)
|
||||
}
|
||||
|
||||
requestBody, err := json.Marshal(&espialDocument{
|
||||
Title: entryTitle,
|
||||
URL: entryURL,
|
||||
ToRead: true,
|
||||
Tags: espialTags,
|
||||
})
|
||||
|
||||
response, err := client.NewRequestBuilder(apiEndpoint).
|
||||
WithMethod(http.MethodPost).
|
||||
WithJSON(&espialDocument{
|
||||
Title: entryTitle,
|
||||
URL: entryURL,
|
||||
ToRead: true,
|
||||
Tags: espialTags,
|
||||
}).
|
||||
WithHeader("Authorization", "ApiKey "+c.apiKey).
|
||||
Do()
|
||||
if err != nil {
|
||||
return fmt.Errorf("espial: unable to encode request body: %v", err)
|
||||
}
|
||||
|
||||
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 := client.NewClientWithOptions(client.Options{Timeout: defaultClientTimeout, BlockPrivateNetworks: !config.Opts.IntegrationAllowPrivateNetworks()})
|
||||
response, err := httpClient.Do(request)
|
||||
if err != nil {
|
||||
return fmt.Errorf("espial: unable to send request: %v", err)
|
||||
return fmt.Errorf("espial: %w", err)
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
|
||||
@@ -4,22 +4,15 @@
|
||||
package linkace // import "miniflux.app/v2/internal/integration/linkace"
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"miniflux.app/v2/internal/config"
|
||||
"miniflux.app/v2/internal/http/client"
|
||||
"miniflux.app/v2/internal/urllib"
|
||||
"miniflux.app/v2/internal/version"
|
||||
)
|
||||
|
||||
const defaultClientTimeout = 10 * time.Second
|
||||
|
||||
type Client struct {
|
||||
baseURL string
|
||||
apiKey string
|
||||
@@ -45,31 +38,20 @@ func (c *Client) AddURL(entryURL, entryTitle string) error {
|
||||
if err != nil {
|
||||
return fmt.Errorf("linkace: invalid API endpoint: %v", err)
|
||||
}
|
||||
requestBody, err := json.Marshal(&createItemRequest{
|
||||
URL: entryURL,
|
||||
Title: entryTitle,
|
||||
Tags: strings.FieldsFunc(c.tags, tagsSplitFn),
|
||||
Private: c.private,
|
||||
CheckDisabled: c.checkDisabled,
|
||||
})
|
||||
response, err := client.NewRequestBuilder(apiEndpoint).
|
||||
WithMethod(http.MethodPost).
|
||||
WithJSON(&createItemRequest{
|
||||
URL: entryURL,
|
||||
Title: entryTitle,
|
||||
Tags: strings.FieldsFunc(c.tags, tagsSplitFn),
|
||||
Private: c.private,
|
||||
CheckDisabled: c.checkDisabled,
|
||||
}).
|
||||
WithHeader("Accept", "application/json").
|
||||
WithHeader("Authorization", "Bearer "+c.apiKey).
|
||||
Do()
|
||||
if err != nil {
|
||||
return fmt.Errorf("linkace: unable to encode request body: %v", err)
|
||||
}
|
||||
|
||||
request, err := http.NewRequest(http.MethodPost, apiEndpoint, bytes.NewReader(requestBody))
|
||||
if err != nil {
|
||||
return fmt.Errorf("linkace: 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 "+c.apiKey)
|
||||
|
||||
httpClient := client.NewClientWithOptions(client.Options{Timeout: defaultClientTimeout, BlockPrivateNetworks: !config.Opts.IntegrationAllowPrivateNetworks()})
|
||||
response, err := httpClient.Do(request)
|
||||
if err != nil {
|
||||
return fmt.Errorf("linkace: unable to send request: %v", err)
|
||||
return fmt.Errorf("linkace: %w", err)
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
|
||||
@@ -4,22 +4,15 @@
|
||||
package linkding // import "miniflux.app/v2/internal/integration/linkding"
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"miniflux.app/v2/internal/config"
|
||||
"miniflux.app/v2/internal/http/client"
|
||||
"miniflux.app/v2/internal/urllib"
|
||||
"miniflux.app/v2/internal/version"
|
||||
)
|
||||
|
||||
const defaultClientTimeout = 10 * time.Second
|
||||
|
||||
type Client struct {
|
||||
baseURL string
|
||||
apiKey string
|
||||
@@ -45,30 +38,18 @@ func (c *Client) CreateBookmark(entryURL, entryTitle string) error {
|
||||
return fmt.Errorf(`linkding: invalid API endpoint: %v`, err)
|
||||
}
|
||||
|
||||
requestBody, err := json.Marshal(&linkdingBookmark{
|
||||
URL: entryURL,
|
||||
Title: entryTitle,
|
||||
TagNames: strings.FieldsFunc(c.tags, tagsSplitFn),
|
||||
Unread: c.unread,
|
||||
})
|
||||
|
||||
response, err := client.NewRequestBuilder(apiEndpoint).
|
||||
WithMethod(http.MethodPost).
|
||||
WithJSON(&linkdingBookmark{
|
||||
URL: entryURL,
|
||||
Title: entryTitle,
|
||||
TagNames: strings.FieldsFunc(c.tags, tagsSplitFn),
|
||||
Unread: c.unread,
|
||||
}).
|
||||
WithHeader("Authorization", "Token "+c.apiKey).
|
||||
Do()
|
||||
if err != nil {
|
||||
return fmt.Errorf("linkding: unable to encode request body: %v", err)
|
||||
}
|
||||
|
||||
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 := client.NewClientWithOptions(client.Options{Timeout: defaultClientTimeout, BlockPrivateNetworks: !config.Opts.IntegrationAllowPrivateNetworks()})
|
||||
response, err := httpClient.Do(request)
|
||||
if err != nil {
|
||||
return fmt.Errorf("linkding: unable to send request: %v", err)
|
||||
return fmt.Errorf("linkding: %w", err)
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
|
||||
@@ -4,22 +4,15 @@
|
||||
package linkwarden // import "miniflux.app/v2/internal/integration/linkwarden"
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"miniflux.app/v2/internal/config"
|
||||
"miniflux.app/v2/internal/http/client"
|
||||
"miniflux.app/v2/internal/urllib"
|
||||
"miniflux.app/v2/internal/version"
|
||||
)
|
||||
|
||||
const defaultClientTimeout = 10 * time.Second
|
||||
|
||||
type Client struct {
|
||||
baseURL string
|
||||
apiKey string
|
||||
@@ -59,25 +52,13 @@ func (c *Client) CreateBookmark(entryURL, entryTitle string) error {
|
||||
payload.Collection = &linkwardenCollection{ID: c.collectionID}
|
||||
}
|
||||
|
||||
requestBody, err := json.Marshal(payload)
|
||||
|
||||
response, err := client.NewRequestBuilder(apiEndpoint).
|
||||
WithMethod(http.MethodPost).
|
||||
WithJSON(payload).
|
||||
WithHeader("Authorization", "Bearer "+c.apiKey).
|
||||
Do()
|
||||
if err != nil {
|
||||
return fmt.Errorf("linkwarden: unable to encode request body: %v", err)
|
||||
}
|
||||
|
||||
request, err := http.NewRequest(http.MethodPost, apiEndpoint, bytes.NewReader(requestBody))
|
||||
if err != nil {
|
||||
return fmt.Errorf("linkwarden: 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", "Bearer "+c.apiKey)
|
||||
|
||||
httpClient := client.NewClientWithOptions(client.Options{Timeout: defaultClientTimeout, BlockPrivateNetworks: !config.Opts.IntegrationAllowPrivateNetworks()})
|
||||
response, err := httpClient.Do(request)
|
||||
if err != nil {
|
||||
return fmt.Errorf("linkwarden: unable to send request: %v", err)
|
||||
return fmt.Errorf("linkwarden: %w", err)
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
|
||||
@@ -4,19 +4,13 @@
|
||||
package notion
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"miniflux.app/v2/internal/http/client"
|
||||
"miniflux.app/v2/internal/version"
|
||||
)
|
||||
|
||||
const defaultClientTimeout = 10 * time.Second
|
||||
|
||||
type Client struct {
|
||||
apiToken string
|
||||
pageID string
|
||||
@@ -32,36 +26,25 @@ func (c *Client) UpdateDocument(entryURL string, entryTitle string) error {
|
||||
}
|
||||
|
||||
apiEndpoint := "https://api.notion.com/v1/blocks/" + c.pageID + "/children"
|
||||
requestBody, err := json.Marshal(¬ionDocument{
|
||||
Children: []block{
|
||||
{
|
||||
Object: "block",
|
||||
Type: "bookmark",
|
||||
Bookmark: bookmarkObject{
|
||||
Caption: []any{},
|
||||
URL: entryURL,
|
||||
response, err := client.NewRequestBuilder(apiEndpoint).
|
||||
WithMethod(http.MethodPatch).
|
||||
WithJSON(¬ionDocument{
|
||||
Children: []block{
|
||||
{
|
||||
Object: "block",
|
||||
Type: "bookmark",
|
||||
Bookmark: bookmarkObject{
|
||||
Caption: []any{},
|
||||
URL: entryURL,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
}).
|
||||
WithHeader("Notion-Version", "2022-06-28").
|
||||
WithHeader("Authorization", "Bearer "+c.apiToken).
|
||||
Do()
|
||||
if err != nil {
|
||||
return fmt.Errorf("notion: unable to encode request body: %v", err)
|
||||
}
|
||||
|
||||
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 := client.NewClientWithOptions(client.Options{Timeout: defaultClientTimeout})
|
||||
response, err := httpClient.Do(request)
|
||||
if err != nil {
|
||||
return fmt.Errorf("notion: unable to send request: %v", err)
|
||||
return fmt.Errorf("notion: %w", err)
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
|
||||
@@ -4,21 +4,14 @@
|
||||
package raindrop // import "miniflux.app/v2/internal/integration/raindrop"
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"miniflux.app/v2/internal/config"
|
||||
"miniflux.app/v2/internal/http/client"
|
||||
"miniflux.app/v2/internal/version"
|
||||
)
|
||||
|
||||
const defaultClientTimeout = 10 * time.Second
|
||||
|
||||
type Client struct {
|
||||
token string
|
||||
collectionID string
|
||||
@@ -35,30 +28,18 @@ func (c *Client) CreateRaindrop(entryURL, entryTitle string) error {
|
||||
return errors.New("raindrop: missing token")
|
||||
}
|
||||
|
||||
var request *http.Request
|
||||
requestBodyJson, err := json.Marshal(&raindrop{
|
||||
Link: entryURL,
|
||||
Title: entryTitle,
|
||||
Collection: collection{Id: c.collectionID},
|
||||
Tags: c.tags,
|
||||
})
|
||||
response, err := client.NewRequestBuilder("https://api.raindrop.io/rest/v1/raindrop").
|
||||
WithMethod(http.MethodPost).
|
||||
WithJSON(&raindrop{
|
||||
Link: entryURL,
|
||||
Title: entryTitle,
|
||||
Collection: collection{Id: c.collectionID},
|
||||
Tags: c.tags,
|
||||
}).
|
||||
WithHeader("Authorization", "Bearer "+c.token).
|
||||
Do()
|
||||
if err != nil {
|
||||
return fmt.Errorf("raindrop: unable to encode request body: %v", err)
|
||||
}
|
||||
|
||||
request, err = http.NewRequest(http.MethodPost, "https://api.raindrop.io/rest/v1/raindrop", bytes.NewReader(requestBodyJson))
|
||||
if err != nil {
|
||||
return fmt.Errorf("raindrop: 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", "Bearer "+c.token)
|
||||
|
||||
httpClient := client.NewClientWithOptions(client.Options{Timeout: defaultClientTimeout, BlockPrivateNetworks: !config.Opts.IntegrationAllowPrivateNetworks()})
|
||||
response, err := httpClient.Do(request)
|
||||
if err != nil {
|
||||
return fmt.Errorf("raindrop: unable to send request: %v", err)
|
||||
return fmt.Errorf("raindrop: %w", err)
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
|
||||
@@ -6,22 +6,14 @@
|
||||
package readwise // import "miniflux.app/v2/internal/integration/readwise"
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"miniflux.app/v2/internal/config"
|
||||
"miniflux.app/v2/internal/http/client"
|
||||
"miniflux.app/v2/internal/version"
|
||||
)
|
||||
|
||||
const (
|
||||
readwiseApiEndpoint = "https://readwise.io/api/v3/save/"
|
||||
defaultClientTimeout = 10 * time.Second
|
||||
)
|
||||
const readwiseApiEndpoint = "https://readwise.io/api/v3/save/"
|
||||
|
||||
type Client struct {
|
||||
apiKey string
|
||||
@@ -36,27 +28,15 @@ func (c *Client) CreateDocument(entryURL string) error {
|
||||
return errors.New("readwise: missing API key")
|
||||
}
|
||||
|
||||
requestBody, err := json.Marshal(&readwiseDocument{
|
||||
URL: entryURL,
|
||||
})
|
||||
|
||||
response, err := client.NewRequestBuilder(readwiseApiEndpoint).
|
||||
WithMethod(http.MethodPost).
|
||||
WithJSON(&readwiseDocument{
|
||||
URL: entryURL,
|
||||
}).
|
||||
WithHeader("Authorization", "Token "+c.apiKey).
|
||||
Do()
|
||||
if err != nil {
|
||||
return fmt.Errorf("readwise: unable to encode request body: %v", err)
|
||||
}
|
||||
|
||||
request, err := http.NewRequest(http.MethodPost, readwiseApiEndpoint, bytes.NewReader(requestBody))
|
||||
if err != nil {
|
||||
return fmt.Errorf("readwise: 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 := client.NewClientWithOptions(client.Options{Timeout: defaultClientTimeout, BlockPrivateNetworks: !config.Opts.IntegrationAllowPrivateNetworks()})
|
||||
response, err := httpClient.Do(request)
|
||||
if err != nil {
|
||||
return fmt.Errorf("readwise: unable to send request: %v", err)
|
||||
return fmt.Errorf("readwise: %w", err)
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
|
||||
@@ -4,24 +4,18 @@
|
||||
package shaarli // import "miniflux.app/v2/internal/integration/shaarli"
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/hmac"
|
||||
"crypto/sha512"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"miniflux.app/v2/internal/config"
|
||||
"miniflux.app/v2/internal/http/client"
|
||||
"miniflux.app/v2/internal/urllib"
|
||||
"miniflux.app/v2/internal/version"
|
||||
)
|
||||
|
||||
const defaultClientTimeout = 10 * time.Second
|
||||
|
||||
type Client struct {
|
||||
baseURL string
|
||||
apiSecret string
|
||||
@@ -41,30 +35,18 @@ func (c *Client) CreateLink(entryURL, entryTitle string) error {
|
||||
return fmt.Errorf("shaarli: invalid API endpoint: %v", err)
|
||||
}
|
||||
|
||||
requestBody, err := json.Marshal(&addLinkRequest{
|
||||
URL: entryURL,
|
||||
Title: entryTitle,
|
||||
Private: true,
|
||||
})
|
||||
|
||||
response, err := client.NewRequestBuilder(apiEndpoint).
|
||||
WithMethod(http.MethodPost).
|
||||
WithJSON(&addLinkRequest{
|
||||
URL: entryURL,
|
||||
Title: entryTitle,
|
||||
Private: true,
|
||||
}).
|
||||
WithHeader("Accept", "application/json").
|
||||
WithHeader("Authorization", "Bearer "+c.generateBearerToken()).
|
||||
Do()
|
||||
if err != nil {
|
||||
return fmt.Errorf("shaarli: unable to encode request body: %v", err)
|
||||
}
|
||||
|
||||
request, err := http.NewRequest(http.MethodPost, apiEndpoint, bytes.NewReader(requestBody))
|
||||
if err != nil {
|
||||
return fmt.Errorf("shaarli: 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 "+c.generateBearerToken())
|
||||
|
||||
httpClient := client.NewClientWithOptions(client.Options{Timeout: defaultClientTimeout, BlockPrivateNetworks: !config.Opts.IntegrationAllowPrivateNetworks()})
|
||||
response, err := httpClient.Do(request)
|
||||
if err != nil {
|
||||
return fmt.Errorf("shaarli: unable to send request: %v", err)
|
||||
return fmt.Errorf("shaarli: %w", err)
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
|
||||
@@ -4,21 +4,15 @@
|
||||
package shiori // import "miniflux.app/v2/internal/integration/shiori"
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"miniflux.app/v2/internal/config"
|
||||
"miniflux.app/v2/internal/http/client"
|
||||
"miniflux.app/v2/internal/urllib"
|
||||
"miniflux.app/v2/internal/version"
|
||||
)
|
||||
|
||||
const defaultClientTimeout = 10 * time.Second
|
||||
|
||||
type Client struct {
|
||||
baseURL string
|
||||
username string
|
||||
@@ -44,34 +38,21 @@ func (c *Client) CreateBookmark(entryURL, entryTitle string) error {
|
||||
return fmt.Errorf("shiori: invalid API endpoint: %v", err)
|
||||
}
|
||||
|
||||
requestBody, err := json.Marshal(&addBookmarkRequest{
|
||||
URL: entryURL,
|
||||
Title: entryTitle,
|
||||
Excerpt: "",
|
||||
CreateArchive: true,
|
||||
CreateEbook: false,
|
||||
Public: 0,
|
||||
Tags: make([]string, 0),
|
||||
})
|
||||
|
||||
response, err := client.NewRequestBuilder(apiEndpoint).
|
||||
WithMethod(http.MethodPost).
|
||||
WithJSON(&addBookmarkRequest{
|
||||
URL: entryURL,
|
||||
Title: entryTitle,
|
||||
Excerpt: "",
|
||||
CreateArchive: true,
|
||||
CreateEbook: false,
|
||||
Public: 0,
|
||||
Tags: make([]string, 0),
|
||||
}).
|
||||
WithHeader("Authorization", "Bearer "+token).
|
||||
Do()
|
||||
if err != nil {
|
||||
return fmt.Errorf("shiori: unable to encode request body: %v", err)
|
||||
}
|
||||
|
||||
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("User-Agent", "Miniflux/"+version.Version)
|
||||
request.Header.Set("Authorization", "Bearer "+token)
|
||||
|
||||
httpClient := client.NewClientWithOptions(client.Options{Timeout: defaultClientTimeout, BlockPrivateNetworks: !config.Opts.IntegrationAllowPrivateNetworks()})
|
||||
|
||||
response, err := httpClient.Do(request)
|
||||
if err != nil {
|
||||
return fmt.Errorf("shiori: unable to send request: %v", err)
|
||||
return fmt.Errorf("shiori: %w", err)
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
@@ -88,25 +69,13 @@ func (c *Client) authenticate() (string, error) {
|
||||
return "", fmt.Errorf("shiori: invalid API endpoint: %v", err)
|
||||
}
|
||||
|
||||
requestBody, err := json.Marshal(&authRequest{Username: c.username, Password: c.password, RememberMe: false})
|
||||
response, err := client.NewRequestBuilder(apiEndpoint).
|
||||
WithMethod(http.MethodPost).
|
||||
WithJSON(&authRequest{Username: c.username, Password: c.password, RememberMe: false}).
|
||||
WithHeader("Accept", "application/json").
|
||||
Do()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("shiori: unable to encode request body: %v", err)
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
httpClient := client.NewClientWithOptions(client.Options{Timeout: defaultClientTimeout, BlockPrivateNetworks: !config.Opts.IntegrationAllowPrivateNetworks()})
|
||||
|
||||
response, err := httpClient.Do(request)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("shiori: unable to send request: %v", err)
|
||||
return "", fmt.Errorf("shiori: %w", err)
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
|
||||
@@ -6,21 +6,15 @@
|
||||
package slack // import "miniflux.app/v2/internal/integration/slack"
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"miniflux.app/v2/internal/config"
|
||||
"miniflux.app/v2/internal/http/client"
|
||||
"miniflux.app/v2/internal/model"
|
||||
"miniflux.app/v2/internal/urllib"
|
||||
"miniflux.app/v2/internal/version"
|
||||
)
|
||||
|
||||
const defaultClientTimeout = 10 * time.Second
|
||||
const slackMsgColor = "#5865F2"
|
||||
|
||||
type Client struct {
|
||||
@@ -33,60 +27,49 @@ func NewClient(webhookURL string) *Client {
|
||||
|
||||
func (c *Client) SendSlackMsg(feed *model.Feed, entries model.Entries) error {
|
||||
for _, entry := range entries {
|
||||
requestBody, err := json.Marshal(&slackMessage{
|
||||
Attachments: []slackAttachments{
|
||||
{
|
||||
Title: "RSS feed update from Miniflux",
|
||||
Color: slackMsgColor,
|
||||
Fields: []slackFields{
|
||||
{
|
||||
Title: "Updated feed",
|
||||
Value: feed.Title,
|
||||
},
|
||||
{
|
||||
Title: "Article title",
|
||||
Value: entry.Title,
|
||||
},
|
||||
{
|
||||
Title: "Article link",
|
||||
Value: entry.URL,
|
||||
},
|
||||
{
|
||||
Title: "Author",
|
||||
Value: entry.Author,
|
||||
Short: true,
|
||||
},
|
||||
{
|
||||
Title: "Source website",
|
||||
Value: urllib.RootURL(feed.SiteURL),
|
||||
Short: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("slack: unable to encode request body: %v", err)
|
||||
}
|
||||
|
||||
request, err := http.NewRequest(http.MethodPost, c.webhookURL, bytes.NewReader(requestBody))
|
||||
if err != nil {
|
||||
return fmt.Errorf("slack: unable to create request: %v", err)
|
||||
}
|
||||
|
||||
request.Header.Set("Content-Type", "application/json")
|
||||
request.Header.Set("User-Agent", "Miniflux/"+version.Version)
|
||||
|
||||
slog.Debug("Sending Slack notification",
|
||||
slog.String("webhookURL", c.webhookURL),
|
||||
slog.String("title", feed.Title),
|
||||
slog.String("entry_url", entry.URL),
|
||||
)
|
||||
|
||||
httpClient := client.NewClientWithOptions(client.Options{Timeout: defaultClientTimeout, BlockPrivateNetworks: !config.Opts.IntegrationAllowPrivateNetworks()})
|
||||
response, err := httpClient.Do(request)
|
||||
response, err := client.NewRequestBuilder(c.webhookURL).
|
||||
WithMethod(http.MethodPost).
|
||||
WithJSON(&slackMessage{
|
||||
Attachments: []slackAttachments{
|
||||
{
|
||||
Title: "RSS feed update from Miniflux",
|
||||
Color: slackMsgColor,
|
||||
Fields: []slackFields{
|
||||
{
|
||||
Title: "Updated feed",
|
||||
Value: feed.Title,
|
||||
},
|
||||
{
|
||||
Title: "Article title",
|
||||
Value: entry.Title,
|
||||
},
|
||||
{
|
||||
Title: "Article link",
|
||||
Value: entry.URL,
|
||||
},
|
||||
{
|
||||
Title: "Author",
|
||||
Value: entry.Author,
|
||||
Short: true,
|
||||
},
|
||||
{
|
||||
Title: "Source website",
|
||||
Value: urllib.RootURL(feed.SiteURL),
|
||||
Short: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}).
|
||||
Do()
|
||||
if err != nil {
|
||||
return fmt.Errorf("slack: unable to send request: %v", err)
|
||||
return fmt.Errorf("slack: %w", err)
|
||||
}
|
||||
response.Body.Close()
|
||||
|
||||
|
||||
@@ -4,23 +4,18 @@
|
||||
package webhook // import "miniflux.app/v2/internal/integration/webhook"
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"miniflux.app/v2/internal/config"
|
||||
"miniflux.app/v2/internal/crypto"
|
||||
"miniflux.app/v2/internal/http/client"
|
||||
"miniflux.app/v2/internal/model"
|
||||
"miniflux.app/v2/internal/version"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultClientTimeout = 10 * time.Second
|
||||
|
||||
NewEntriesEventType = "new_entries"
|
||||
SaveEntryEventType = "save_entry"
|
||||
)
|
||||
@@ -124,20 +119,14 @@ func (c *Client) makeRequest(eventType string, payload any) error {
|
||||
return fmt.Errorf("webhook: unable to encode request body: %v", err)
|
||||
}
|
||||
|
||||
request, err := http.NewRequest(http.MethodPost, c.webhookURL, bytes.NewReader(requestBody))
|
||||
response, err := client.NewRequestBuilder(c.webhookURL).
|
||||
WithMethod(http.MethodPost).
|
||||
WithJSONBody(requestBody).
|
||||
WithHeader("X-Miniflux-Signature", crypto.GenerateSHA256Hmac(c.webhookSecret, requestBody)).
|
||||
WithHeader("X-Miniflux-Event-Type", eventType).
|
||||
Do()
|
||||
if err != nil {
|
||||
return fmt.Errorf("webhook: unable to create request: %v", err)
|
||||
}
|
||||
|
||||
request.Header.Set("Content-Type", "application/json")
|
||||
request.Header.Set("User-Agent", "Miniflux/"+version.Version)
|
||||
request.Header.Set("X-Miniflux-Signature", crypto.GenerateSHA256Hmac(c.webhookSecret, requestBody))
|
||||
request.Header.Set("X-Miniflux-Event-Type", eventType)
|
||||
|
||||
httpClient := client.NewClientWithOptions(client.Options{Timeout: defaultClientTimeout, BlockPrivateNetworks: !config.Opts.IntegrationAllowPrivateNetworks()})
|
||||
response, err := httpClient.Do(request)
|
||||
if err != nil {
|
||||
return fmt.Errorf("webhook: unable to send request: %v", err)
|
||||
return fmt.Errorf("webhook: %w", err)
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user