karakeep.go 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165
  1. // SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
  2. // SPDX-License-Identifier: Apache-2.0
  3. package karakeep // import "miniflux.app/v2/internal/integration/karakeep"
  4. import (
  5. "bytes"
  6. "encoding/json"
  7. "errors"
  8. "fmt"
  9. "io"
  10. "net/http"
  11. "strings"
  12. "time"
  13. "miniflux.app/v2/internal/config"
  14. "miniflux.app/v2/internal/http/client"
  15. "miniflux.app/v2/internal/version"
  16. )
  17. const defaultClientTimeout = 10 * time.Second
  18. type Client struct {
  19. wrapped *http.Client
  20. apiEndpoint string
  21. apiToken string
  22. tags string
  23. }
  24. type tagItem struct {
  25. TagName string `json:"tagName"`
  26. }
  27. type saveURLPayload struct {
  28. Type string `json:"type"`
  29. URL string `json:"url"`
  30. }
  31. type saveURLResponse struct {
  32. ID string `json:"id"`
  33. }
  34. type attachTagsPayload struct {
  35. Tags []tagItem `json:"tags"`
  36. }
  37. type errorResponse struct {
  38. Code string `json:"code"`
  39. Error string `json:"error"`
  40. }
  41. func NewClient(apiToken string, apiEndpoint string, tags string) *Client {
  42. return &Client{wrapped: client.NewClientWithOptions(client.Options{Timeout: defaultClientTimeout, BlockPrivateNetworks: !config.Opts.IntegrationAllowPrivateNetworks()}), apiEndpoint: apiEndpoint, apiToken: apiToken, tags: tags}
  43. }
  44. func (c *Client) attachTags(entryID string) error {
  45. if c.tags == "" {
  46. return nil
  47. }
  48. tagItems := make([]tagItem, 0)
  49. for tag := range strings.SplitSeq(c.tags, ",") {
  50. if trimmedTag := strings.TrimSpace(tag); trimmedTag != "" {
  51. tagItems = append(tagItems, tagItem{TagName: trimmedTag})
  52. }
  53. }
  54. if len(tagItems) == 0 {
  55. return nil
  56. }
  57. tagRequestBody, err := json.Marshal(&attachTagsPayload{
  58. Tags: tagItems,
  59. })
  60. if err != nil {
  61. return fmt.Errorf("karakeep: unable to encode tag request body: %v", err)
  62. }
  63. tagRequest, err := http.NewRequest(http.MethodPost, fmt.Sprintf("%s/%s/tags", c.apiEndpoint, entryID), bytes.NewReader(tagRequestBody))
  64. if err != nil {
  65. return fmt.Errorf("karakeep: unable to create tag request: %v", err)
  66. }
  67. tagRequest.Header.Set("Authorization", "Bearer "+c.apiToken)
  68. tagRequest.Header.Set("Content-Type", "application/json")
  69. tagRequest.Header.Set("User-Agent", "Miniflux/"+version.Version)
  70. tagResponse, err := c.wrapped.Do(tagRequest)
  71. if err != nil {
  72. return fmt.Errorf("karakeep: unable to send tag request: %v", err)
  73. }
  74. defer tagResponse.Body.Close()
  75. if tagResponse.StatusCode != http.StatusOK && tagResponse.StatusCode != http.StatusCreated {
  76. tagResponseBody, err := io.ReadAll(tagResponse.Body)
  77. if err != nil {
  78. return fmt.Errorf("karakeep: failed to parse tag response: %s", err)
  79. }
  80. var errResponse errorResponse
  81. if err := json.Unmarshal(tagResponseBody, &errResponse); err != nil {
  82. return fmt.Errorf("karakeep: unable to parse tag error response: status=%d body=%s", tagResponse.StatusCode, string(tagResponseBody))
  83. }
  84. return fmt.Errorf("karakeep: failed to attach tags: status=%d errorcode=%s %s", tagResponse.StatusCode, errResponse.Code, errResponse.Error)
  85. }
  86. return nil
  87. }
  88. func (c *Client) SaveURL(entryURL string) error {
  89. requestBody, err := json.Marshal(&saveURLPayload{
  90. Type: "link",
  91. URL: entryURL,
  92. })
  93. if err != nil {
  94. return fmt.Errorf("karakeep: unable to encode request body: %v", err)
  95. }
  96. req, err := http.NewRequest(http.MethodPost, c.apiEndpoint, bytes.NewReader(requestBody))
  97. if err != nil {
  98. return fmt.Errorf("karakeep: unable to create request: %v", err)
  99. }
  100. req.Header.Set("Authorization", "Bearer "+c.apiToken)
  101. req.Header.Set("Content-Type", "application/json")
  102. req.Header.Set("User-Agent", "Miniflux/"+version.Version)
  103. resp, err := c.wrapped.Do(req)
  104. if err != nil {
  105. return fmt.Errorf("karakeep: unable to send request: %v", err)
  106. }
  107. defer resp.Body.Close()
  108. responseBody, err := io.ReadAll(resp.Body)
  109. if err != nil {
  110. return fmt.Errorf("karakeep: failed to parse response: %s", err)
  111. }
  112. if resp.Header.Get("Content-Type") != "application/json" {
  113. return fmt.Errorf("karakeep: unexpected content type response: %s", resp.Header.Get("Content-Type"))
  114. }
  115. if resp.StatusCode != http.StatusCreated {
  116. var errResponse errorResponse
  117. if err := json.Unmarshal(responseBody, &errResponse); err != nil {
  118. return fmt.Errorf("karakeep: unable to parse error response: status=%d body=%s", resp.StatusCode, string(responseBody))
  119. }
  120. return fmt.Errorf("karakeep: failed to save URL: status=%d errorcode=%s %s", resp.StatusCode, errResponse.Code, errResponse.Error)
  121. }
  122. var response saveURLResponse
  123. if err := json.Unmarshal(responseBody, &response); err != nil {
  124. return fmt.Errorf("karakeep: unable to parse response: %v", err)
  125. }
  126. if response.ID == "" {
  127. return errors.New("karakeep: unable to get ID from response")
  128. }
  129. if err := c.attachTags(response.ID); err != nil {
  130. return fmt.Errorf("karakeep: unable to attach tags: %v", err)
  131. }
  132. return nil
  133. }