aboutsummaryrefslogtreecommitdiff
path: root/internal/integration/omnivore/omnivore.go
blob: b182a10a95f113799068383c88399a960ddb8053 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

package omnivore // import "miniflux.app/v2/internal/integration/omnivore"

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"time"

	"miniflux.app/v2/internal/crypto"
	"miniflux.app/v2/internal/version"
)

const defaultClientTimeout = 10 * time.Second

const defaultApiEndpoint = "https://api-prod.omnivore.app/api/graphql"

var mutation = `
mutation SaveUrl($input: SaveUrlInput!) {
  saveUrl(input: $input) {
    ... on SaveSuccess {
      url
      clientRequestId
    }
    ... on SaveError {
      errorCodes
      message
    }
  }
}
`

type SaveUrlInput struct {
	ClientRequestId string `json:"clientRequestId"`
	Source          string `json:"source"`
	Url             string `json:"url"`
}

type errorResponse struct {
	Errors []struct {
		Message string `json:"message"`
	} `json:"errors"`
}

type successResponse struct {
	Data struct {
		SaveUrl struct {
			Url             string `json:"url"`
			ClientRequestId string `json:"clientRequestId"`
		} `json:"saveUrl"`
	} `json:"data"`
}

type Client interface {
	SaveUrl(url string) error
}

type client struct {
	wrapped     *http.Client
	apiEndpoint string
	apiToken    string
}

func NewClient(apiToken string, apiEndpoint string) Client {
	if apiEndpoint == "" {
		apiEndpoint = defaultApiEndpoint
	}

	return &client{wrapped: &http.Client{Timeout: defaultClientTimeout}, apiEndpoint: apiEndpoint, apiToken: apiToken}
}

func (c *client) SaveUrl(url string) error {
	var payload = map[string]interface{}{
		"query": mutation,
		"variables": map[string]interface{}{
			"input": map[string]interface{}{
				"clientRequestId": crypto.GenerateUUID(),
				"source":          "api",
				"url":             url,
			},
		},
	}
	b, err := json.Marshal(payload)
	if err != nil {
		return err
	}
	req, err := http.NewRequest(http.MethodPost, c.apiEndpoint, bytes.NewReader(b))
	if err != nil {
		return err
	}

	req.Header.Set("Authorization", c.apiToken)
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("User-Agent", "Miniflux/"+version.Version)

	resp, err := c.wrapped.Do(req)
	if err != nil {
		return err
	}

	defer resp.Body.Close()
	b, err = io.ReadAll(resp.Body)
	if err != nil {
		return fmt.Errorf("omnivore: failed to parse response: %s", err)
	}

	if resp.StatusCode >= 400 {
		var errResponse errorResponse
		if err = json.Unmarshal(b, &errResponse); err != nil {
			return fmt.Errorf("omnivore: failed to save URL: status=%d %s", resp.StatusCode, string(b))
		}
		return fmt.Errorf("omnivore: failed to save URL: status=%d %s", resp.StatusCode, errResponse.Errors[0].Message)
	}

	var successReponse successResponse
	if err = json.Unmarshal(b, &successReponse); err != nil {
		return fmt.Errorf("omnivore: failed to parse response, however the request appears successful, is the url correct?: status=%d %s", resp.StatusCode, string(b))
	}

	return nil
}