karakeep.go 4.5 KB

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