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")
}