readeck.go 4.4 KB

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