omnivore.go 3.0 KB

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