readeck.go 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152
  1. // SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
  2. // SPDX-License-Identifier: Apache-2.0
  3. package readeck // import "miniflux.app/v2/internal/integration/readeck"
  4. import (
  5. "bytes"
  6. "encoding/json"
  7. "errors"
  8. "fmt"
  9. "mime/multipart"
  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/urllib"
  16. "miniflux.app/v2/internal/version"
  17. )
  18. const defaultClientTimeout = 10 * time.Second
  19. type Client struct {
  20. baseURL string
  21. apiKey string
  22. labels string
  23. onlyURL bool
  24. }
  25. func NewClient(baseURL, apiKey, labels string, onlyURL bool) *Client {
  26. return &Client{baseURL: baseURL, apiKey: apiKey, labels: labels, onlyURL: onlyURL}
  27. }
  28. func (c *Client) CreateBookmark(entryURL, entryTitle string, entryContent string) error {
  29. if c.baseURL == "" || c.apiKey == "" {
  30. return errors.New("readeck: missing base URL or API key")
  31. }
  32. apiEndpoint, err := urllib.JoinBaseURLAndPath(c.baseURL, "/api/bookmarks/")
  33. if err != nil {
  34. return fmt.Errorf(`readeck: invalid API endpoint: %v`, err)
  35. }
  36. labelsSplitFn := func(c rune) bool {
  37. return c == ',' || c == ' '
  38. }
  39. labelsSplit := strings.FieldsFunc(c.labels, labelsSplitFn)
  40. var request *http.Request
  41. if c.onlyURL {
  42. requestBodyJson, err := json.Marshal(&readeckBookmark{
  43. URL: entryURL,
  44. Title: entryTitle,
  45. Labels: labelsSplit,
  46. })
  47. if err != nil {
  48. return fmt.Errorf("readeck: unable to encode request body: %v", err)
  49. }
  50. request, err = http.NewRequest(http.MethodPost, apiEndpoint, bytes.NewReader(requestBodyJson))
  51. if err != nil {
  52. return fmt.Errorf("readeck: unable to create request: %v", err)
  53. }
  54. request.Header.Set("Content-Type", "application/json")
  55. } else {
  56. requestBody := new(bytes.Buffer)
  57. multipartWriter := multipart.NewWriter(requestBody)
  58. urlPart, err := multipartWriter.CreateFormField("url")
  59. if err != nil {
  60. return fmt.Errorf("readeck: unable to encode request body (entry url): %v", err)
  61. }
  62. urlPart.Write([]byte(entryURL))
  63. titlePart, err := multipartWriter.CreateFormField("title")
  64. if err != nil {
  65. return fmt.Errorf("readeck: unable to encode request body (entry title): %v", err)
  66. }
  67. titlePart.Write([]byte(entryTitle))
  68. featurePart, err := multipartWriter.CreateFormField("feature_find_main")
  69. if err != nil {
  70. return fmt.Errorf("readeck: unable to encode request body (feature_find_main flag): %v", err)
  71. }
  72. featurePart.Write([]byte("false")) // false to disable readability
  73. for _, label := range labelsSplit {
  74. labelPart, err := multipartWriter.CreateFormField("labels")
  75. if err != nil {
  76. return fmt.Errorf("readeck: unable to encode request body (entry labels): %v", err)
  77. }
  78. labelPart.Write([]byte(label))
  79. }
  80. contentBodyHeader, err := json.Marshal(&partContentHeader{
  81. URL: entryURL,
  82. ContentHeader: contentHeader{ContentType: "text/html; charset=utf-8"},
  83. })
  84. if err != nil {
  85. return fmt.Errorf("readeck: unable to encode request body (entry content header): %v", err)
  86. }
  87. contentPart, err := multipartWriter.CreateFormFile("resource", "blob")
  88. if err != nil {
  89. return fmt.Errorf("readeck: unable to encode request body (entry content): %v", err)
  90. }
  91. contentPart.Write(contentBodyHeader)
  92. contentPart.Write([]byte("\n"))
  93. contentPart.Write([]byte(entryContent))
  94. err = multipartWriter.Close()
  95. if err != nil {
  96. return fmt.Errorf("readeck: unable to encode request body: %v", err)
  97. }
  98. request, err = http.NewRequest(http.MethodPost, apiEndpoint, requestBody)
  99. if err != nil {
  100. return fmt.Errorf("readeck: unable to create request: %v", err)
  101. }
  102. request.Header.Set("Content-Type", multipartWriter.FormDataContentType())
  103. }
  104. request.Header.Set("User-Agent", "Miniflux/"+version.Version)
  105. request.Header.Set("Authorization", "Bearer "+c.apiKey)
  106. httpClient := client.NewClientWithOptions(client.Options{Timeout: defaultClientTimeout, BlockPrivateNetworks: !config.Opts.IntegrationAllowPrivateNetworks()})
  107. response, err := httpClient.Do(request)
  108. if err != nil {
  109. return fmt.Errorf("readeck: unable to send request: %v", err)
  110. }
  111. defer response.Body.Close()
  112. if response.StatusCode >= 400 {
  113. return fmt.Errorf("readeck: unable to create bookmark: url=%s status=%d", apiEndpoint, response.StatusCode)
  114. }
  115. return nil
  116. }
  117. type readeckBookmark struct {
  118. URL string `json:"url"`
  119. Title string `json:"title"`
  120. Labels []string `json:"labels,omitempty"`
  121. }
  122. type contentHeader struct {
  123. ContentType string `json:"content-type"`
  124. }
  125. type partContentHeader struct {
  126. URL string `json:"url"`
  127. ContentHeader contentHeader `json:"headers"`
  128. }