Browse Source

fix(integration): fix bugs, naming, and inconsistencies across sub-packages

Bugs fixed:
- pushover: inverted error-check logic (err != nil → err == nil)
- nunuxkeeper: wrong error prefix ("notion:" → "nunux-keeper:")
- wallabag: typo "wallbag" in five error messages
- omnivore: potential panic on empty errors array
- rssbridge: off-by-one status check (> 400 → >= 400)
- archiveorg: slog called with printf-style %v format verb
- integration: redundant TelegramBotEnabled check (dead code)
- integration: webhook failure logged at Debug instead of Warn

Non-idiomatic Go fixed:
- archiveorg: replace http.Get() with proper http.Client, add
  User-Agent header, remove unnecessary goroutine, return error
- apprise/discord/slack: use defer for response.Body.Close()
- omnivore: return concrete struct instead of interface from NewClient
- pushover: rename New() to NewClient() for consistency
- wallabag/ntfy: use named fields in struct literal initialization
- shiori: remove unnecessary named return values
- instapaper/pinboard/betula: remove Content-Type on bodyless requests
- rssbridge: add missing User-Agent header

Naming and style:
- Rename Url→URL, ClientRequestId→ClientRequestID in struct fields
  (espial, linkace, linkding, readeck, pinboard, omnivore)
- Rename SaveUrl→SaveURL method (omnivore)
- Unexport internal-only types (pushover, omnivore)
- Fix variable typo successReponse→successResponse (omnivore)
- Normalize error prefixes to lowercase "pkg:" style (pushover, rssbridge)
- Fix grammar "Rewrited"→"Rewrote" (rssbridge)
Frédéric Guillot 1 month ago
parent
commit
df7fc1e853

+ 2 - 2
internal/integration/apprise/apprise.go

@@ -25,7 +25,7 @@ type Client struct {
 }
 
 func NewClient(serviceURL, baseURL string) *Client {
-	return &Client{serviceURL, baseURL}
+	return &Client{servicesURL: serviceURL, baseURL: baseURL}
 }
 
 func (c *Client) SendNotification(feed *model.Feed, entries model.Entries) error {
@@ -70,7 +70,7 @@ func (c *Client) SendNotification(feed *model.Feed, entries model.Entries) error
 		if err != nil {
 			return fmt.Errorf("apprise: unable to send request: %v", err)
 		}
-		response.Body.Close()
+		defer response.Body.Close()
 
 		if response.StatusCode >= 400 {
 			return fmt.Errorf("apprise: unable to send a notification: url=%s status=%d", apiEndpoint, response.StatusCode)

+ 27 - 23
internal/integration/archiveorg/archiveorg.go

@@ -4,11 +4,16 @@
 package archiveorg
 
 import (
-	"log/slog"
+	"fmt"
 	"net/http"
 	"net/url"
+	"time"
+
+	"miniflux.app/v2/internal/version"
 )
 
+const defaultClientTimeout = 30 * time.Second
+
 // See https://docs.google.com/document/d/1Nsv52MvSjbLb2PCpHlat0gkzw0EvtSgpKHu4mk0MnrA/edit?tab=t.0
 const options = "delay_wb_availability=1&if_not_archived_within=15d"
 
@@ -18,26 +23,25 @@ func NewClient() *Client {
 	return &Client{}
 }
 
-func (c *Client) SendURL(entryURL, title string) {
-	// We're using a goroutine here as submissions to archive.org might take a long time
-	// and trigger a timeout on miniflux' side.
-	go func(entryURL string) {
-		res, err := http.Get("https://web.archive.org/save/" + url.QueryEscape(entryURL) + "?" + options)
-		if err != nil {
-			slog.Error("archiveorg: unable to send request: %v",
-				slog.Any("err", err),
-				slog.String("title", title),
-				slog.String("url", entryURL),
-			)
-			return
-		}
-		if res.StatusCode > 299 {
-			slog.Error("archiveorg: failed with status code",
-				slog.String("title", title),
-				slog.String("url", entryURL),
-				slog.Int("code", res.StatusCode),
-			)
-		}
-		res.Body.Close()
-	}(entryURL)
+func (c *Client) SendURL(entryURL string) error {
+	requestURL := "https://web.archive.org/save/" + url.QueryEscape(entryURL) + "?" + options
+	request, err := http.NewRequest(http.MethodGet, requestURL, nil)
+	if err != nil {
+		return fmt.Errorf("archiveorg: unable to create request: %v", err)
+	}
+
+	request.Header.Set("User-Agent", "Miniflux/"+version.Version)
+
+	httpClient := &http.Client{Timeout: defaultClientTimeout}
+	response, err := httpClient.Do(request)
+	if err != nil {
+		return fmt.Errorf("archiveorg: unable to send request: %v", err)
+	}
+	defer response.Body.Close()
+
+	if response.StatusCode >= 400 {
+		return fmt.Errorf("archiveorg: unexpected status code: url=%s status=%d", requestURL, response.StatusCode)
+	}
+
+	return nil
 }

+ 0 - 1
internal/integration/betula/betula.go

@@ -41,7 +41,6 @@ func (c *Client) CreateBookmark(entryURL, entryTitle string, tags []string) erro
 		return fmt.Errorf("betula: unable to create request: %v", err)
 	}
 
-	request.Header.Set("Content-Type", "application/x-www-form-urlencoded")
 	request.Header.Set("User-Agent", "Miniflux/"+version.Version)
 	request.AddCookie(&http.Cookie{Name: "betula-token", Value: c.token})
 

+ 1 - 1
internal/integration/discord/discord.go

@@ -82,7 +82,7 @@ func (c *Client) SendDiscordMsg(feed *model.Feed, entries model.Entries) error {
 		if err != nil {
 			return fmt.Errorf("discord: unable to send request: %v", err)
 		}
-		response.Body.Close()
+		defer response.Body.Close()
 
 		if response.StatusCode >= 400 {
 			return fmt.Errorf("discord: unable to send a notification: url=%s status=%d", c.webhookURL, response.StatusCode)

+ 2 - 2
internal/integration/espial/espial.go

@@ -38,7 +38,7 @@ func (c *Client) CreateLink(entryURL, entryTitle, espialTags string) error {
 
 	requestBody, err := json.Marshal(&espialDocument{
 		Title:  entryTitle,
-		Url:    entryURL,
+		URL:    entryURL,
 		ToRead: true,
 		Tags:   espialTags,
 	})
@@ -75,7 +75,7 @@ func (c *Client) CreateLink(entryURL, entryTitle, espialTags string) error {
 
 type espialDocument struct {
 	Title  string `json:"title,omitempty"`
-	Url    string `json:"url,omitempty"`
+	URL    string `json:"url,omitempty"`
 	ToRead bool   `json:"toread,omitempty"`
 	Tags   string `json:"tags,omitempty"`
 }

+ 0 - 1
internal/integration/instapaper/instapaper.go

@@ -40,7 +40,6 @@ func (c *Client) AddURL(entryURL, entryTitle string) error {
 	}
 
 	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}

+ 29 - 24
internal/integration/integration.go

@@ -409,7 +409,14 @@ func SendEntry(entry *model.Entry, userIntegrations *model.Integration) {
 			slog.String("entry_url", entry.URL),
 		)
 
-		archiveorg.NewClient().SendURL(entry.URL, entry.Title)
+		if err := archiveorg.NewClient().SendURL(entry.URL); err != nil {
+			slog.Error("Unable to send entry to Archive.org",
+				slog.Int64("user_id", userIntegrations.UserID),
+				slog.Int64("entry_id", entry.ID),
+				slog.String("entry_url", entry.URL),
+				slog.Any("error", err),
+			)
+		}
 	}
 
 	if userIntegrations.WebhookEnabled {
@@ -447,7 +454,7 @@ func SendEntry(entry *model.Entry, userIntegrations *model.Integration) {
 		)
 
 		client := omnivore.NewClient(userIntegrations.OmnivoreAPIKey, userIntegrations.OmnivoreURL)
-		if err := client.SaveUrl(entry.URL); err != nil {
+		if err := client.SaveURL(entry.URL); err != nil {
 			slog.Error("Unable to send entry to Omnivore",
 				slog.Int64("user_id", userIntegrations.UserID),
 				slog.Int64("entry_id", entry.ID),
@@ -543,7 +550,7 @@ func PushEntries(feed *model.Feed, entries model.Entries, userIntegrations *mode
 
 		webhookClient := webhook.NewClient(webhookURL, userIntegrations.WebhookSecret)
 		if err := webhookClient.SendNewEntriesWebhookEvent(feed, entries); err != nil {
-			slog.Debug("Unable to send new entries to Webhook",
+			slog.Warn("Unable to send new entries to Webhook",
 				slog.Int64("user_id", userIntegrations.UserID),
 				slog.Int("nb_entries", len(entries)),
 				slog.Int64("feed_id", feed.ID),
@@ -642,7 +649,7 @@ func PushEntries(feed *model.Feed, entries model.Entries, userIntegrations *mode
 			slog.Int64("feed_id", feed.ID),
 		)
 
-		client := pushover.New(
+		client := pushover.NewClient(
 			userIntegrations.PushoverUser,
 			userIntegrations.PushoverToken,
 			feed.PushoverPriority,
@@ -658,30 +665,28 @@ func PushEntries(feed *model.Feed, entries model.Entries, userIntegrations *mode
 	// Integrations that only support sending individual entries
 	if userIntegrations.TelegramBotEnabled {
 		for _, entry := range entries {
-			if userIntegrations.TelegramBotEnabled {
-				slog.Debug("Sending a new entry to Telegram",
+			slog.Debug("Sending a new entry to Telegram",
+				slog.Int64("user_id", userIntegrations.UserID),
+				slog.Int64("entry_id", entry.ID),
+				slog.String("entry_url", entry.URL),
+			)
+
+			if err := telegrambot.PushEntry(
+				feed,
+				entry,
+				userIntegrations.TelegramBotToken,
+				userIntegrations.TelegramBotChatID,
+				userIntegrations.TelegramBotTopicID,
+				userIntegrations.TelegramBotDisableWebPagePreview,
+				userIntegrations.TelegramBotDisableNotification,
+				userIntegrations.TelegramBotDisableButtons,
+			); err != nil {
+				slog.Error("Unable to send entry to Telegram",
 					slog.Int64("user_id", userIntegrations.UserID),
 					slog.Int64("entry_id", entry.ID),
 					slog.String("entry_url", entry.URL),
+					slog.Any("error", err),
 				)
-
-				if err := telegrambot.PushEntry(
-					feed,
-					entry,
-					userIntegrations.TelegramBotToken,
-					userIntegrations.TelegramBotChatID,
-					userIntegrations.TelegramBotTopicID,
-					userIntegrations.TelegramBotDisableWebPagePreview,
-					userIntegrations.TelegramBotDisableNotification,
-					userIntegrations.TelegramBotDisableButtons,
-				); err != nil {
-					slog.Error("Unable to send entry to Telegram",
-						slog.Int64("user_id", userIntegrations.UserID),
-						slog.Int64("entry_id", entry.ID),
-						slog.String("entry_url", entry.URL),
-						slog.Any("error", err),
-					)
-				}
 			}
 		}
 	}

+ 2 - 2
internal/integration/linkace/linkace.go

@@ -44,7 +44,7 @@ func (c *Client) AddURL(entryURL, entryTitle string) error {
 		return fmt.Errorf("linkace: invalid API endpoint: %v", err)
 	}
 	requestBody, err := json.Marshal(&createItemRequest{
-		Url:           entryURL,
+		URL:           entryURL,
 		Title:         entryTitle,
 		Tags:          strings.FieldsFunc(c.tags, tagsSplitFn),
 		Private:       c.private,
@@ -80,7 +80,7 @@ func (c *Client) AddURL(entryURL, entryTitle string) error {
 
 type createItemRequest struct {
 	Title         string   `json:"title,omitempty"`
-	Url           string   `json:"url"`
+	URL           string   `json:"url"`
 	Tags          []string `json:"tags,omitempty"`
 	Private       bool     `json:"is_private,omitempty"`
 	CheckDisabled bool     `json:"check_disabled,omitempty"`

+ 2 - 2
internal/integration/linkding/linkding.go

@@ -44,7 +44,7 @@ func (c *Client) CreateBookmark(entryURL, entryTitle string) error {
 	}
 
 	requestBody, err := json.Marshal(&linkdingBookmark{
-		Url:      entryURL,
+		URL:      entryURL,
 		Title:    entryTitle,
 		TagNames: strings.FieldsFunc(c.tags, tagsSplitFn),
 		Unread:   c.unread,
@@ -78,7 +78,7 @@ func (c *Client) CreateBookmark(entryURL, entryTitle string) error {
 }
 
 type linkdingBookmark struct {
-	Url      string   `json:"url,omitempty"`
+	URL      string   `json:"url,omitempty"`
 	Title    string   `json:"title,omitempty"`
 	TagNames []string `json:"tag_names,omitempty"`
 	Unread   bool     `json:"unread,omitempty"`

+ 10 - 1
internal/integration/ntfy/ntfy.go

@@ -32,7 +32,16 @@ func NewClient(ntfyURL, ntfyTopic, ntfyApiToken, ntfyUsername, ntfyPassword, ntf
 	if ntfyURL == "" {
 		ntfyURL = defaultNtfyURL
 	}
-	return &Client{ntfyURL, ntfyTopic, ntfyApiToken, ntfyUsername, ntfyPassword, ntfyIconURL, ntfyInternalLinks, ntfyPriority}
+	return &Client{
+		ntfyURL:           ntfyURL,
+		ntfyTopic:         ntfyTopic,
+		ntfyApiToken:      ntfyApiToken,
+		ntfyUsername:      ntfyUsername,
+		ntfyPassword:      ntfyPassword,
+		ntfyIconURL:       ntfyIconURL,
+		ntfyInternalLinks: ntfyInternalLinks,
+		ntfyPriority:      ntfyPriority,
+	}
 }
 
 func (c *Client) SendMessages(feed *model.Feed, entries model.Entries) error {

+ 1 - 1
internal/integration/nunuxkeeper/nunuxkeeper.go

@@ -43,7 +43,7 @@ func (c *Client) AddEntry(entryURL, entryTitle, entryContent string) error {
 		ContentType: "text/html",
 	})
 	if err != nil {
-		return fmt.Errorf("notion: unable to encode request body: %v", err)
+		return fmt.Errorf("nunux-keeper: unable to encode request body: %v", err)
 	}
 
 	request, err := http.NewRequest(http.MethodPost, apiEndpoint, bytes.NewReader(requestBody))

+ 13 - 20
internal/integration/omnivore/omnivore.go

@@ -34,12 +34,6 @@ mutation SaveUrl($input: SaveUrlInput!) {
 }
 `
 
-type SaveUrlInput struct {
-	ClientRequestId string `json:"clientRequestId"`
-	Source          string `json:"source"`
-	Url             string `json:"url"`
-}
-
 type errorResponse struct {
 	Errors []struct {
 		Message string `json:"message"`
@@ -49,31 +43,27 @@ type errorResponse struct {
 type successResponse struct {
 	Data struct {
 		SaveUrl struct {
-			Url             string `json:"url"`
-			ClientRequestId string `json:"clientRequestId"`
+			URL             string `json:"url"`
+			ClientRequestID string `json:"clientRequestId"`
 		} `json:"saveUrl"`
 	} `json:"data"`
 }
 
-type Client interface {
-	SaveUrl(url string) error
-}
-
-type client struct {
+type Client struct {
 	wrapped     *http.Client
 	apiEndpoint string
 	apiToken    string
 }
 
-func NewClient(apiToken string, apiEndpoint string) Client {
+func NewClient(apiToken string, apiEndpoint string) *Client {
 	if apiEndpoint == "" {
 		apiEndpoint = defaultApiEndpoint
 	}
 
-	return &client{wrapped: &http.Client{Timeout: defaultClientTimeout}, apiEndpoint: apiEndpoint, apiToken: apiToken}
+	return &Client{wrapped: &http.Client{Timeout: defaultClientTimeout}, apiEndpoint: apiEndpoint, apiToken: apiToken}
 }
 
-func (c *client) SaveUrl(url string) error {
+func (c *Client) SaveURL(url string) error {
 	var payload = map[string]any{
 		"query": mutation,
 		"variables": map[string]any{
@@ -105,7 +95,7 @@ func (c *client) SaveUrl(url string) error {
 	defer resp.Body.Close()
 	b, err = io.ReadAll(resp.Body)
 	if err != nil {
-		return fmt.Errorf("omnivore: failed to parse response: %s", err)
+		return fmt.Errorf("omnivore: failed to parse response: %v", err)
 	}
 
 	if resp.StatusCode >= 400 {
@@ -113,11 +103,14 @@ func (c *client) SaveUrl(url string) error {
 		if err = json.Unmarshal(b, &errResponse); err != nil {
 			return fmt.Errorf("omnivore: failed to save URL: status=%d %s", resp.StatusCode, string(b))
 		}
-		return fmt.Errorf("omnivore: failed to save URL: status=%d %s", resp.StatusCode, errResponse.Errors[0].Message)
+		if len(errResponse.Errors) > 0 {
+			return fmt.Errorf("omnivore: failed to save URL: status=%d %s", resp.StatusCode, errResponse.Errors[0].Message)
+		}
+		return fmt.Errorf("omnivore: failed to save URL: status=%d %s", resp.StatusCode, string(b))
 	}
 
-	var successReponse successResponse
-	if err = json.Unmarshal(b, &successReponse); err != nil {
+	var successResp successResponse
+	if err = json.Unmarshal(b, &successResp); err != nil {
 		return fmt.Errorf("omnivore: failed to parse response, however the request appears successful, is the url correct?: status=%d %s", resp.StatusCode, string(b))
 	}
 

+ 0 - 2
internal/integration/pinboard/pinboard.go

@@ -57,7 +57,6 @@ func (c *Client) CreateBookmark(entryURL, entryTitle, pinboardTags string, markA
 		return fmt.Errorf("pinboard: unable to create request: %v", err)
 	}
 
-	request.Header.Set("Content-Type", "application/x-www-form-urlencoded")
 	request.Header.Set("User-Agent", "Miniflux/"+version.Version)
 
 	httpClient := &http.Client{Timeout: defaultClientTimeout}
@@ -90,7 +89,6 @@ func (c *Client) getBookmark(entryURL string) (*Post, error) {
 		return nil, fmt.Errorf("pinboard: unable to create request: %v", err)
 	}
 
-	request.Header.Set("Content-Type", "application/x-www-form-urlencoded")
 	request.Header.Set("User-Agent", "Miniflux/"+version.Version)
 
 	httpClient := &http.Client{Timeout: defaultClientTimeout}

+ 3 - 3
internal/integration/pinboard/post.go

@@ -13,7 +13,7 @@ import (
 // Post a Pinboard bookmark.  "inspiration" from https://github.com/drags/pinboard/blob/master/posts.go#L32-L42
 type Post struct {
 	XMLName     xml.Name  `xml:"post"`
-	Url         string    `xml:"href,attr"`
+	URL         string    `xml:"href,attr"`
 	Description string    `xml:"description,attr"`
 	Tags        string    `xml:"tag,attr"`
 	Extended    string    `xml:"extended,attr"`
@@ -30,7 +30,7 @@ type posts struct {
 
 func NewPost(url string, description string) *Post {
 	return &Post{
-		Url:         url,
+		URL:         url,
 		Description: description,
 		Date:        time.Now(),
 		Toread:      "no",
@@ -48,7 +48,7 @@ func (p *Post) SetToread() {
 }
 
 func (p *Post) AddValues(values url.Values) {
-	values.Add("url", p.Url)
+	values.Add("url", p.URL)
 	values.Add("description", p.Description)
 	values.Add("tags", p.Tags)
 	if p.Toread != "" {

+ 12 - 12
internal/integration/pushover/pushover.go

@@ -31,7 +31,7 @@ type Client struct {
 	priority int
 }
 
-type Message struct {
+type message struct {
 	Token string `json:"token"`
 	User  string `json:"user"`
 
@@ -44,14 +44,14 @@ type Message struct {
 	Device   string `json:"device,omitempty"`
 }
 
-type ErrorResponse struct {
+type errorResponse struct {
 	User    string   `json:"user"`
 	Errors  []string `json:"errors"`
 	Status  int      `json:"status"`
 	Request string   `json:"request"`
 }
 
-func New(user, token string, priority int, device, urlPrefix string) *Client {
+func NewClient(user, token string, priority int, device, urlPrefix string) *Client {
 	if urlPrefix == "" {
 		urlPrefix = defaultPushoverURL
 	}
@@ -76,7 +76,7 @@ func (c *Client) SendMessages(feed *model.Feed, entries model.Entries) error {
 		return errors.New("pushover token and user are required")
 	}
 	for _, entry := range entries {
-		msg := &Message{
+		msg := &message{
 			User:   c.user,
 			Token:  c.token,
 			Device: c.device,
@@ -94,22 +94,22 @@ func (c *Client) SendMessages(feed *model.Feed, entries model.Entries) error {
 		)
 
 		if err := c.makeRequest(msg); err != nil {
-			return fmt.Errorf("c.makeRequest: %w", err)
+			return fmt.Errorf("pushover: unable to send message: %w", err)
 		}
 	}
 
 	return nil
 }
 
-func (c *Client) makeRequest(payload *Message) error {
+func (c *Client) makeRequest(payload *message) error {
 	jsonData, err := json.Marshal(payload)
 	if err != nil {
-		return fmt.Errorf("json.Marshal: %w", err)
+		return fmt.Errorf("pushover: unable to encode request body: %w", err)
 	}
 	url := c.prefix + "/1/messages.json"
 	req, err := http.NewRequest(http.MethodPost, url, bytes.NewBuffer(jsonData))
 	if err != nil {
-		return fmt.Errorf("http.NewRequest: %w", err)
+		return fmt.Errorf("pushover: unable to create request: %w", err)
 	}
 
 	req.Header.Add("Content-Type", "application/json")
@@ -118,21 +118,21 @@ func (c *Client) makeRequest(payload *Message) error {
 	httpClient := &http.Client{Timeout: defaultClientTimeout}
 	resp, err := httpClient.Do(req)
 	if err != nil {
-		return fmt.Errorf("httpClient.Do: %w", err)
+		return fmt.Errorf("pushover: unable to send request: %w", err)
 	}
 	defer resp.Body.Close()
 
 	if resp.StatusCode >= http.StatusBadRequest {
 		errorMessage := resp.Status
 
-		var errResp ErrorResponse
-		if err := json.NewDecoder(resp.Body).Decode(&errResp); err != nil {
+		var errResp errorResponse
+		if err := json.NewDecoder(resp.Body).Decode(&errResp); err == nil {
 			if len(errResp.Errors) > 0 {
 				errorMessage = strings.Join(errResp.Errors, ",")
 			}
 		}
 
-		return fmt.Errorf("pushover API error (%d): %s", resp.StatusCode, errorMessage)
+		return fmt.Errorf("pushover: API error: status=%d %s", resp.StatusCode, errorMessage)
 	}
 
 	return nil

+ 4 - 4
internal/integration/readeck/readeck.go

@@ -48,7 +48,7 @@ func (c *Client) CreateBookmark(entryURL, entryTitle string, entryContent string
 	var request *http.Request
 	if c.onlyURL {
 		requestBodyJson, err := json.Marshal(&readeckBookmark{
-			Url:    entryURL,
+			URL:    entryURL,
 			Title:  entryTitle,
 			Labels: labelsSplit,
 		})
@@ -91,7 +91,7 @@ func (c *Client) CreateBookmark(entryURL, entryTitle string, entryContent string
 		}
 
 		contentBodyHeader, err := json.Marshal(&partContentHeader{
-			Url:           entryURL,
+			URL:           entryURL,
 			ContentHeader: contentHeader{ContentType: "text/html; charset=utf-8"},
 		})
 		if err != nil {
@@ -135,7 +135,7 @@ func (c *Client) CreateBookmark(entryURL, entryTitle string, entryContent string
 }
 
 type readeckBookmark struct {
-	Url    string   `json:"url"`
+	URL    string   `json:"url"`
 	Title  string   `json:"title"`
 	Labels []string `json:"labels,omitempty"`
 }
@@ -145,6 +145,6 @@ type contentHeader struct {
 }
 
 type partContentHeader struct {
-	Url           string        `json:"url"`
+	URL           string        `json:"url"`
 	ContentHeader contentHeader `json:"headers"`
 }

+ 11 - 7
internal/integration/rssbridge/rssbridge.go

@@ -11,6 +11,8 @@ import (
 	"net/url"
 	"strings"
 	"time"
+
+	"miniflux.app/v2/internal/version"
 )
 
 const defaultClientTimeout = 30 * time.Second
@@ -27,7 +29,7 @@ type BridgeMeta struct {
 func DetectBridges(rssBridgeURL, rssBridgeToken, websiteURL string) ([]*Bridge, error) {
 	endpointURL, err := url.Parse(rssBridgeURL)
 	if err != nil {
-		return nil, fmt.Errorf("RSS-Bridge: unable to parse bridge URL: %w", err)
+		return nil, fmt.Errorf("rssbridge: unable to parse bridge URL: %w", err)
 	}
 
 	values := endpointURL.Query()
@@ -43,14 +45,16 @@ func DetectBridges(rssBridgeURL, rssBridgeToken, websiteURL string) ([]*Bridge,
 
 	request, err := http.NewRequest(http.MethodGet, endpointURL.String(), nil)
 	if err != nil {
-		return nil, fmt.Errorf("RSS-Bridge: unable to create request: %w", err)
+		return nil, fmt.Errorf("rssbridge: unable to create request: %w", err)
 	}
 
+	request.Header.Set("User-Agent", "Miniflux/"+version.Version)
+
 	httpClient := &http.Client{Timeout: defaultClientTimeout}
 
 	response, err := httpClient.Do(request)
 	if err != nil {
-		return nil, fmt.Errorf("RSS-Bridge: unable to execute request: %w", err)
+		return nil, fmt.Errorf("rssbridge: unable to execute request: %w", err)
 	}
 	defer response.Body.Close()
 
@@ -58,13 +62,13 @@ func DetectBridges(rssBridgeURL, rssBridgeToken, websiteURL string) ([]*Bridge,
 		return nil, nil
 	}
 
-	if response.StatusCode > 400 {
-		return nil, fmt.Errorf("RSS-Bridge: unexpected status code %d", response.StatusCode)
+	if response.StatusCode >= 400 {
+		return nil, fmt.Errorf("rssbridge: unexpected status code %d", response.StatusCode)
 	}
 
 	var bridgeResponse []*Bridge
 	if err := json.NewDecoder(response.Body).Decode(&bridgeResponse); err != nil {
-		return nil, fmt.Errorf("RSS-Bridge: unable to decode bridge response: %w", err)
+		return nil, fmt.Errorf("rssbridge: unable to decode bridge response: %w", err)
 	}
 
 	for _, bridge := range bridgeResponse {
@@ -76,7 +80,7 @@ func DetectBridges(rssBridgeURL, rssBridgeToken, websiteURL string) ([]*Bridge,
 		if strings.HasPrefix(bridge.URL, "./") {
 			bridge.URL = rssBridgeURL + bridge.URL[2:]
 
-			slog.Debug("Rewrited relative RSS bridge URL",
+			slog.Debug("Rewrote relative RSS bridge URL",
 				slog.String("name", bridge.BridgeMeta.Name),
 				slog.String("url", bridge.URL),
 			)

+ 1 - 1
internal/integration/shiori/shiori.go

@@ -80,7 +80,7 @@ func (c *Client) CreateBookmark(entryURL, entryTitle string) error {
 	return nil
 }
 
-func (c *Client) authenticate() (token string, err error) {
+func (c *Client) authenticate() (string, error) {
 	apiEndpoint, err := urllib.JoinBaseURLAndPath(c.baseURL, "/api/v1/auth/login")
 	if err != nil {
 		return "", fmt.Errorf("shiori: invalid API endpoint: %v", err)

+ 1 - 1
internal/integration/slack/slack.go

@@ -86,7 +86,7 @@ func (c *Client) SendSlackMsg(feed *model.Feed, entries model.Entries) error {
 		if err != nil {
 			return fmt.Errorf("slack: unable to send request: %v", err)
 		}
-		response.Body.Close()
+		defer response.Body.Close()
 
 		if response.StatusCode >= 400 {
 			return fmt.Errorf("slack: unable to send a notification: url=%s status=%d", c.webhookURL, response.StatusCode)

+ 14 - 6
internal/integration/wallabag/wallabag.go

@@ -30,7 +30,15 @@ type Client struct {
 }
 
 func NewClient(baseURL, clientID, clientSecret, username, password, tags string, onlyURL bool) *Client {
-	return &Client{baseURL, clientID, clientSecret, username, password, tags, onlyURL}
+	return &Client{
+		baseURL:      baseURL,
+		clientID:     clientID,
+		clientSecret: clientSecret,
+		username:     username,
+		password:     password,
+		tags:         tags,
+		onlyURL:      onlyURL,
+	}
 }
 
 func (c *Client) CreateEntry(entryURL, entryTitle, entryContent string) error {
@@ -49,7 +57,7 @@ func (c *Client) CreateEntry(entryURL, entryTitle, entryContent string) error {
 func (c *Client) createEntry(accessToken, entryURL, entryTitle, entryContent, tags 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)
+		return fmt.Errorf("wallabag: unable to generate entries endpoint: %v", err)
 	}
 
 	if c.onlyURL {
@@ -63,12 +71,12 @@ func (c *Client) createEntry(accessToken, entryURL, entryTitle, entryContent, ta
 		Tags:    tags,
 	})
 	if err != nil {
-		return fmt.Errorf("wallbag: unable to encode request body: %v", err)
+		return fmt.Errorf("wallabag: unable to encode request body: %v", err)
 	}
 
 	request, err := http.NewRequest(http.MethodPost, apiEndpoint, bytes.NewReader(requestBody))
 	if err != nil {
-		return fmt.Errorf("wallbag: unable to create request: %v", err)
+		return fmt.Errorf("wallabag: unable to create request: %v", err)
 	}
 
 	request.Header.Set("Content-Type", "application/json")
@@ -100,12 +108,12 @@ func (c *Client) getAccessToken() (string, error) {
 
 	apiEndpoint, err := urllib.JoinBaseURLAndPath(c.baseURL, "/oauth/v2/token")
 	if err != nil {
-		return "", fmt.Errorf("wallbag: unable to generate token endpoint: %v", err)
+		return "", fmt.Errorf("wallabag: unable to generate token endpoint: %v", err)
 	}
 
 	request, err := http.NewRequest(http.MethodPost, apiEndpoint, strings.NewReader(values.Encode()))
 	if err != nil {
-		return "", fmt.Errorf("wallbag: unable to create request: %v", err)
+		return "", fmt.Errorf("wallabag: unable to create request: %v", err)
 	}
 
 	request.Header.Set("Content-Type", "application/x-www-form-urlencoded")