~thrrgilag/woodstock

woodstock/client.go -rw-r--r-- 5.5 KiB
62b10b5cMorgan McMillian Update for new project location and module name 3 months ago
                                                                                
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
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
package woodstock

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"io/ioutil"
	"mime/multipart"
	"net/http"
	"net/textproto"
	"net/url"
	"strconv"
	"strings"
)

// Client object definition
type Client struct {
	clientID            string
	clientSecret        string
	passwordGrantSecret string
	queryQueue          chan query
	API                 API
}

// NewClient returns a new client based on the specified ClientID and ClientSecret
func NewClient(clientID string, clientSecret string) *Client {
	queue := make(chan query)
	client := &Client{clientID: clientID, clientSecret: clientSecret, queryQueue: queue}
	client.initialize()
	go client.throttledQuery()
	return client
}

type query struct {
	url        string
	form       url.Values
	data       interface{}
	method     string
	responseCh chan response
	json       string
	redirect   bool
	reader     io.Reader
	params     map[string]string
}

type response struct {
	data interface{}
	err  error
}

func (c *Client) initialize() {
	c.API = *&API{
		accessToken: "",
		HTTPClient:  http.DefaultClient,
	}
}

// AuthURL generates an authorization url
// https://pnut.io/docs/authentication/web-flows
func (c *Client) AuthURL(redirectURI string, scope []string, responseType string) string {
	return AuthenticateURL + "?client_id=" + c.clientID + "&redirect_uri=" + redirectURI + "&scope=" + strings.Join(scope, "%20") + "&response_type=" + responseType
}

// SetPasswordGrantSecret sets the password grant secret needed for password flow authentication
// https://pnut.io/docs/authentication/password-flow
func (c *Client) SetPasswordGrantSecret(passwordGrantSecret string) {
	c.passwordGrantSecret = passwordGrantSecret
}

// SetAccessToken sets the access token for use by the client
// https://pnut.io/docs/authentication/web-flows
// https://pnut.io/docs/authentication/password-flow
func (c *Client) SetAccessToken(accessToken string) {
	c.API.accessToken = accessToken
}

// StreamMeta meta object definition
type StreamMeta struct {
	More  bool   `json:"more"`
	MaxID string `json:"max_id"`
	MinID string `json:"min_id"`
}

// Meta object definiton
type Meta struct {
	*StreamMeta
	Code         int    `json:"code"`
	Error        string `json:"error"`
	ErrorMessage string `json:"error_message"`
}

// CommonResponse response object
type CommonResponse struct {
	Meta Meta `json:"meta"`
}

func decodeResponse(res *http.Response, data interface{}) error {
	b, err := ioutil.ReadAll(res.Body)
	if err != nil {
		return err
	}
	err = json.Unmarshal(b, data)
	if err != nil {
		return err
	}
	if res.StatusCode < 200 || res.StatusCode >= 300 {
		common := &CommonResponse{}
		err = json.Unmarshal(b, common)
		if err != nil {
			return err
		}
		return fmt.Errorf(strconv.Itoa(res.StatusCode) + ": " + common.Meta.ErrorMessage)
	}

	return nil
}

func (c *Client) execQuery(url string, form url.Values, data interface{}, params map[string]string, reader io.Reader, method string, jsonStr string, redirect bool) (err error) {
	var req *http.Request
	if reader != nil {
		body := &bytes.Buffer{}
		writer := multipart.NewWriter(body)
		if params != nil {
			h := make(textproto.MIMEHeader)
			h.Set("Content-Disposition",
				fmt.Sprintf(`form-data; name="%s"; filename="%s";`,
					params["field"], params["name"]))
			h.Set("Content-Type", params["mimetype"])
			delete(params, "field")
			delete(params, "name")
			delete(params, "mimetype")
			part, err := writer.CreatePart(h)
			if err != nil {
				return err
			}
			_, err = io.Copy(part, reader)
			for k, v := range params {
				_ = writer.WriteField(k, v)
			}
			err = writer.Close()
			req, err = http.NewRequest(
				method,
				url,
				body,
			)
			req.Header.Set("Content-Type", writer.FormDataContentType())
		} else {
			// TODO: this is for setting the file content but doesn't work
			part, err := writer.CreateFormField("content")
			if err != nil {
				return err
			}
			_, err = io.Copy(part, reader)
			err = writer.Close()
			req, err = http.NewRequest(
				method,
				url,
				body,
			)
			req.Header.Set("Content-Type", writer.FormDataContentType())
		}
	} else if jsonStr == "" {
		req, err = http.NewRequest(
			method,
			url,
			strings.NewReader(form.Encode()),
		)
		req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
	} else {
		req, err = http.NewRequest(
			method,
			url,
			bytes.NewBuffer([]byte(jsonStr)),
		)
		req.Header.Set("Content-Type", "application/json")
	}
	if err != nil {
		return err
	}
	if c.API.accessToken != "" {
		req.Header.Set("Authorization", "Bearer "+c.API.accessToken)
	}
	if redirect {
		res, err := http.DefaultTransport.RoundTrip(req)
		if err != nil {
			return err
		}
		defer res.Body.Close()
		if res.StatusCode == 301 || res.StatusCode == 302 || res.StatusCode == 303 || res.StatusCode == 307 {
			if len(res.Header["Location"]) > 0 {
				// gross
				// must fix with reflect
				err = json.Unmarshal([]byte("{\"data\":\""+res.Header["Location"][0]+"\"}"), data)
				return err
			}
			return fmt.Errorf("location is not found from header")
		}
		return fmt.Errorf(strconv.Itoa(res.StatusCode))
	}
	res, err := c.API.HTTPClient.Do(req)
	if err != nil {
		return err
	}
	defer res.Body.Close()

	return decodeResponse(res, data)
}

func (c *Client) throttledQuery() {
	for q := range c.queryQueue {
		url := q.url
		form := q.form
		data := q.data
		reader := q.reader
		params := q.params
		method := q.method
		jsonStr := q.json
		redirect := q.redirect

		responseCh := q.responseCh

		err := c.execQuery(url, form, data, params, reader, method, jsonStr, redirect)

		responseCh <- response{data, err}
	}
}

func notSupported() error {
	return fmt.Errorf("not supported")
}