omnivore.go 2.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125
  1. // SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
  2. // SPDX-License-Identifier: Apache-2.0
  3. package omnivore // import "miniflux.app/v2/internal/integration/omnivore"
  4. import (
  5. "bytes"
  6. "encoding/json"
  7. "fmt"
  8. "io"
  9. "net/http"
  10. "time"
  11. "miniflux.app/v2/internal/crypto"
  12. "miniflux.app/v2/internal/version"
  13. )
  14. const defaultClientTimeout = 10 * time.Second
  15. const defaultApiEndpoint = "https://api-prod.omnivore.app/api/graphql"
  16. var mutation = `
  17. mutation SaveUrl($input: SaveUrlInput!) {
  18. saveUrl(input: $input) {
  19. ... on SaveSuccess {
  20. url
  21. clientRequestId
  22. }
  23. ... on SaveError {
  24. errorCodes
  25. message
  26. }
  27. }
  28. }
  29. `
  30. type SaveUrlInput struct {
  31. ClientRequestId string `json:"clientRequestId"`
  32. Source string `json:"source"`
  33. Url string `json:"url"`
  34. }
  35. type errorResponse struct {
  36. Errors []struct {
  37. Message string `json:"message"`
  38. } `json:"errors"`
  39. }
  40. type successResponse struct {
  41. Data struct {
  42. SaveUrl struct {
  43. Url string `json:"url"`
  44. ClientRequestId string `json:"clientRequestId"`
  45. } `json:"saveUrl"`
  46. } `json:"data"`
  47. }
  48. type Client interface {
  49. SaveUrl(url string) error
  50. }
  51. type client struct {
  52. wrapped *http.Client
  53. apiEndpoint string
  54. apiToken string
  55. }
  56. func NewClient(apiToken string, apiEndpoint string) Client {
  57. if apiEndpoint == "" {
  58. apiEndpoint = defaultApiEndpoint
  59. }
  60. return &client{wrapped: &http.Client{Timeout: defaultClientTimeout}, apiEndpoint: apiEndpoint, apiToken: apiToken}
  61. }
  62. func (c *client) SaveUrl(url string) error {
  63. var payload = map[string]any{
  64. "query": mutation,
  65. "variables": map[string]any{
  66. "input": map[string]any{
  67. "clientRequestId": crypto.GenerateUUID(),
  68. "source": "api",
  69. "url": url,
  70. },
  71. },
  72. }
  73. b, err := json.Marshal(payload)
  74. if err != nil {
  75. return err
  76. }
  77. req, err := http.NewRequest(http.MethodPost, c.apiEndpoint, bytes.NewReader(b))
  78. if err != nil {
  79. return err
  80. }
  81. req.Header.Set("Authorization", c.apiToken)
  82. req.Header.Set("Content-Type", "application/json")
  83. req.Header.Set("User-Agent", "Miniflux/"+version.Version)
  84. resp, err := c.wrapped.Do(req)
  85. if err != nil {
  86. return err
  87. }
  88. defer resp.Body.Close()
  89. b, err = io.ReadAll(resp.Body)
  90. if err != nil {
  91. return fmt.Errorf("omnivore: failed to parse response: %s", err)
  92. }
  93. if resp.StatusCode >= 400 {
  94. var errResponse errorResponse
  95. if err = json.Unmarshal(b, &errResponse); err != nil {
  96. return fmt.Errorf("omnivore: failed to save URL: status=%d %s", resp.StatusCode, string(b))
  97. }
  98. return fmt.Errorf("omnivore: failed to save URL: status=%d %s", resp.StatusCode, errResponse.Errors[0].Message)
  99. }
  100. var successReponse successResponse
  101. if err = json.Unmarshal(b, &successReponse); err != nil {
  102. return fmt.Errorf("omnivore: failed to parse response, however the request appears successful, is the url correct?: status=%d %s", resp.StatusCode, string(b))
  103. }
  104. return nil
  105. }