diff options
Diffstat (limited to 'internal/integration')
-rw-r--r-- | internal/integration/integration.go | 34 | ||||
-rw-r--r-- | internal/integration/shaarli/shaarli.go | 91 |
2 files changed, 115 insertions, 10 deletions
diff --git a/internal/integration/integration.go b/internal/integration/integration.go index 1b47638e..98bda702 100644 --- a/internal/integration/integration.go +++ b/internal/integration/integration.go @@ -15,6 +15,7 @@ import ( "miniflux.app/v2/internal/integration/pinboard" "miniflux.app/v2/internal/integration/pocket" "miniflux.app/v2/internal/integration/readwise" + "miniflux.app/v2/internal/integration/shaarli" "miniflux.app/v2/internal/integration/shiori" "miniflux.app/v2/internal/integration/telegrambot" "miniflux.app/v2/internal/integration/wallabag" @@ -25,7 +26,7 @@ import ( // SendEntry sends the entry to third-party providers when the user click on "Save". func SendEntry(entry *model.Entry, integration *model.Integration) { if integration.PinboardEnabled { - logger.Debug("[Integration] Sending Entry #%d %q for User #%d to Pinboard", entry.ID, entry.URL, integration.UserID) + logger.Debug("[Integration] Sending entry #%d %q for user #%d to Pinboard", entry.ID, entry.URL, integration.UserID) client := pinboard.NewClient(integration.PinboardToken) err := client.AddBookmark( @@ -41,7 +42,7 @@ func SendEntry(entry *model.Entry, integration *model.Integration) { } if integration.InstapaperEnabled { - logger.Debug("[Integration] Sending Entry #%d %q for User #%d to Instapaper", entry.ID, entry.URL, integration.UserID) + logger.Debug("[Integration] Sending entry #%d %q for user #%d to Instapaper", entry.ID, entry.URL, integration.UserID) client := instapaper.NewClient(integration.InstapaperUsername, integration.InstapaperPassword) if err := client.AddURL(entry.URL, entry.Title); err != nil { @@ -50,7 +51,7 @@ func SendEntry(entry *model.Entry, integration *model.Integration) { } if integration.WallabagEnabled { - logger.Debug("[Integration] Sending Entry #%d %q for User #%d to Wallabag", entry.ID, entry.URL, integration.UserID) + logger.Debug("[Integration] Sending entry #%d %q for user #%d to Wallabag", entry.ID, entry.URL, integration.UserID) client := wallabag.NewClient( integration.WallabagURL, @@ -67,7 +68,7 @@ func SendEntry(entry *model.Entry, integration *model.Integration) { } if integration.NotionEnabled { - logger.Debug("[Integration] Sending Entry #%d %q for User #%d to Notion", entry.ID, entry.URL, integration.UserID) + logger.Debug("[Integration] Sending entry #%d %q for user #%d to Notion", entry.ID, entry.URL, integration.UserID) client := notion.NewClient( integration.NotionToken, @@ -79,7 +80,7 @@ func SendEntry(entry *model.Entry, integration *model.Integration) { } if integration.NunuxKeeperEnabled { - logger.Debug("[Integration] Sending Entry #%d %q for User #%d to NunuxKeeper", entry.ID, entry.URL, integration.UserID) + logger.Debug("[Integration] Sending entry #%d %q for user #%d to NunuxKeeper", entry.ID, entry.URL, integration.UserID) client := nunuxkeeper.NewClient( integration.NunuxKeeperURL, @@ -92,7 +93,7 @@ func SendEntry(entry *model.Entry, integration *model.Integration) { } if integration.EspialEnabled { - logger.Debug("[Integration] Sending Entry #%d %q for User #%d to Espial", entry.ID, entry.URL, integration.UserID) + logger.Debug("[Integration] Sending entry #%d %q for user #%d to Espial", entry.ID, entry.URL, integration.UserID) client := espial.NewClient( integration.EspialURL, @@ -105,7 +106,7 @@ func SendEntry(entry *model.Entry, integration *model.Integration) { } if integration.PocketEnabled { - logger.Debug("[Integration] Sending Entry #%d %q for User #%d to Pocket", entry.ID, entry.URL, integration.UserID) + logger.Debug("[Integration] Sending entry #%d %q for user #%d to Pocket", entry.ID, entry.URL, integration.UserID) client := pocket.NewClient(config.Opts.PocketConsumerKey(integration.PocketConsumerKey), integration.PocketAccessToken) if err := client.AddURL(entry.URL, entry.Title); err != nil { @@ -114,7 +115,7 @@ func SendEntry(entry *model.Entry, integration *model.Integration) { } if integration.LinkdingEnabled { - logger.Debug("[Integration] Sending Entry #%d %q for User #%d to Linkding", entry.ID, entry.URL, integration.UserID) + logger.Debug("[Integration] Sending entry #%d %q for user #%d to Linkding", entry.ID, entry.URL, integration.UserID) client := linkding.NewClient( integration.LinkdingURL, @@ -128,7 +129,7 @@ func SendEntry(entry *model.Entry, integration *model.Integration) { } if integration.ReadwiseEnabled { - logger.Debug("[Integration] Sending Entry #%d %q for User #%d to Readwise Reader", entry.ID, entry.URL, integration.UserID) + logger.Debug("[Integration] Sending entry #%d %q for user #%d to Readwise Reader", entry.ID, entry.URL, integration.UserID) client := readwise.NewClient( integration.ReadwiseAPIKey, @@ -140,7 +141,7 @@ func SendEntry(entry *model.Entry, integration *model.Integration) { } if integration.ShioriEnabled { - logger.Debug("[Integration] Sending Entry #%d %q for User #%d to Shiori", entry.ID, entry.URL, integration.UserID) + logger.Debug("[Integration] Sending entry #%d %q for user #%d to Shiori", entry.ID, entry.URL, integration.UserID) client := shiori.NewClient( integration.ShioriURL, @@ -152,6 +153,19 @@ func SendEntry(entry *model.Entry, integration *model.Integration) { logger.Error("[Integration] Unable to send entry #%d to Shiori for user #%d: %v", entry.ID, integration.UserID, err) } } + + if integration.ShaarliEnabled { + logger.Debug("[Integration] Sending entry #%d %q for user #%d to Shaarli", entry.ID, entry.URL, integration.UserID) + + client := shaarli.NewClient( + integration.ShaarliURL, + integration.ShaarliAPISecret, + ) + + if err := client.AddLink(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) + } + } } // PushEntries pushes an entry array to third-party providers during feed refreshes. diff --git a/internal/integration/shaarli/shaarli.go b/internal/integration/shaarli/shaarli.go new file mode 100644 index 00000000..d88e3cf4 --- /dev/null +++ b/internal/integration/shaarli/shaarli.go @@ -0,0 +1,91 @@ +// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package shaarli // import "miniflux.app/v2/internal/integration/shaarli" + +import ( + "bytes" + "crypto/hmac" + "crypto/sha512" + "encoding/base64" + "encoding/json" + "fmt" + "net/http" + "strings" + "time" + + "miniflux.app/v2/internal/url" + "miniflux.app/v2/internal/version" +) + +const defaultClientTimeout = 10 * time.Second + +type Client struct { + baseURL string + apiSecret string +} + +func NewClient(baseURL, apiSecret string) *Client { + return &Client{baseURL: baseURL, apiSecret: apiSecret} +} + +func (c *Client) AddLink(entryURL, entryTitle string) error { + if c.baseURL == "" || c.apiSecret == "" { + return fmt.Errorf("shaarli: missing base URL or API secret") + } + + apiEndpoint, err := url.JoinBaseURLAndPath(c.baseURL, "/api/v1/links") + if err != nil { + return fmt.Errorf("shaarli: invalid API endpoint: %v", err) + } + + requestBody, err := json.Marshal(&addLinkRequest{ + URL: entryURL, + Title: entryTitle, + Private: true, + }) + + if err != nil { + return fmt.Errorf("shaarli: unable to encode request body: %v", err) + } + + request, err := http.NewRequest("POST", 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 := &http.Client{Timeout: defaultClientTimeout} + response, err := httpClient.Do(request) + if err != nil { + return fmt.Errorf("shaarli: unable to send request: %v", err) + } + defer response.Body.Close() + + if response.StatusCode != http.StatusCreated { + return fmt.Errorf("shaarli: unable to add link: url=%s status=%d", apiEndpoint, response.StatusCode) + } + + return nil +} + +func (c *Client) generateBearerToken() string { + header := strings.TrimRight(base64.URLEncoding.EncodeToString([]byte(`{"typ":"JWT", "alg":"HS256"}`)), "=") + payload := strings.TrimRight(base64.URLEncoding.EncodeToString([]byte(fmt.Sprintf(`{"iat": %d}`, time.Now().Unix()))), "=") + + mac := hmac.New(sha512.New, []byte(c.apiSecret)) + mac.Write([]byte(header + "." + payload)) + signature := strings.TrimRight(base64.URLEncoding.EncodeToString(mac.Sum(nil)), "=") + + return header + "." + payload + "." + signature +} + +type addLinkRequest struct { + URL string `json:"url"` + Title string `json:"title"` + Private bool `json:"private"` +} |