Skip to content

Commit f8d810c

Browse files
authored
Merge branch 'main' into bugfix/fix-channel-points-id
2 parents 8491a9f + 89cc2bd commit f8d810c

File tree

13 files changed

+268
-42
lines changed

13 files changed

+268
-42
lines changed

Makefile

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
version = "0.4.0"
1+
version = "0.5.0"
22

33
release:
44
docker build . -t twitch-cli:latest

cmd/api.go

+6-2
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616
var queryParameters []string
1717
var body string
1818
var prettyPrint bool
19+
var autoPaginate bool
1920

2021
var apiCmd = &cobra.Command{
2122
Use: "api",
@@ -71,20 +72,23 @@ func init() {
7172
apiCmd.PersistentFlags().MarkHidden("pretty-print")
7273

7374
apiCmd.PersistentFlags().BoolVarP(&prettyPrint, "unformatted", "u", false, "Whether to have API requests come back unformatted/non-prettyprinted. Default is false.")
75+
76+
getCmd.PersistentFlags().BoolVarP(&autoPaginate, "autopaginate", "P", false, "Whether to have API requests automatically paginate. Default is false.")
77+
7478
}
7579

7680
func cmdRun(cmd *cobra.Command, args []string) {
7781
if len(args) == 0 {
7882
cmd.Help()
7983
return
8084
} else if len(args) == 1 && args[0][:1] == "/" {
81-
api.NewRequest(cmd.Name(), args[0], queryParameters, []byte(body), !prettyPrint)
85+
api.NewRequest(cmd.Name(), args[0], queryParameters, []byte(body), !prettyPrint, autoPaginate)
8286
return
8387
}
8488
if body != "" && body[:1] == "@" {
8589
body = getBodyFromFile(body[1:])
8690
}
87-
api.NewRequest(cmd.Name(), "/"+strings.Join(args[:], "/"), queryParameters, []byte(body), !prettyPrint)
91+
api.NewRequest(cmd.Name(), "/"+strings.Join(args[:], "/"), queryParameters, []byte(body), !prettyPrint, autoPaginate)
8892
}
8993

9094
func getBodyFromFile(filename string) string {

docs/api.md

+5-4
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,11 @@ Allows the user to make GET calls to endpoints on Helix. Requires a logged in to
2929

3030
**Flags**
3131

32-
| Flag | Shorthand | Description | Example | Required? (Y/N) |
33-
|------------------|-----------|---------------------------------------------------------------------------------------------------------------|----------------------|-----------------|
34-
| `--query-param` | `-q` | Query parameters for the endpoint in `key=value` format. Multiple can be entered to give multiple parameters. | `get -q login=ninja` | N |
35-
| `--unformatted` | `-u` | Whether to return unformatted responses. Default is `false`. | `get -u` | N |
32+
| Flag | Shorthand | Description | Example | Required? (Y/N) |
33+
|------------------|-----------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------|-----------------|
34+
| `--query-param` | `-q` | Query parameters for the endpoint in `key=value` format. Multiple can be entered to give multiple parameters. | `get -q login=ninja` | N |
35+
| `--unformatted` | `-u` | Whether to return unformatted responses. Default is `false`. | `get -u` | N |
36+
| `--autopaginate` | `-P` | Whether to autopaginate the response from Twitch **WARNING** This flag can cause extremely large payloads and cause issues with some terminals. Default is `false`. | `get -P` | N |
3637

3738
**Examples**
3839

internal/api/api.go

+89-23
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
"time"
1515

1616
"github.com/twitchdev/twitch-cli/internal/login"
17+
"github.com/twitchdev/twitch-cli/internal/models"
1718
"github.com/twitchdev/twitch-cli/internal/util"
1819

1920
"github.com/TylerBrock/colorjson"
@@ -29,7 +30,11 @@ type clientInformation struct {
2930
}
3031

3132
// NewRequest is used to request data from the Twitch API using a HTTP GET request- this function is a wrapper for the apiRequest function that handles the network call
32-
func NewRequest(method string, path string, queryParameters []string, body []byte, prettyPrint bool) {
33+
func NewRequest(method string, path string, queryParameters []string, body []byte, prettyPrint bool, autopaginate bool) {
34+
var data models.APIResponse
35+
var err error
36+
var cursor string
37+
3338
client, err := GetClientInformation()
3439

3540
if viper.GetString("BASE_URL") != "" {
@@ -40,37 +45,94 @@ func NewRequest(method string, path string, queryParameters []string, body []byt
4045
fmt.Println("Error fetching client information", err.Error())
4146
}
4247

43-
Parameters := url.Values{}
48+
for {
49+
var apiResponse models.APIResponse
50+
51+
u, err := url.Parse(baseURL + path)
52+
if err != nil {
53+
fmt.Printf("Error getting url: %v", err)
54+
return
55+
}
4456

45-
if queryParameters != nil {
46-
path += "?"
57+
q := u.Query()
4758
for _, param := range queryParameters {
4859
value := strings.Split(param, "=")
49-
Parameters.Add(value[0], value[1])
60+
q.Add(value[0], value[1])
61+
}
62+
63+
if cursor != "" {
64+
q.Set("after", cursor)
65+
}
66+
67+
if autopaginate == true {
68+
first := "100"
69+
// since channel points custom rewards endpoints only support 50, capping that here
70+
if strings.Contains(u.String(), "custom_rewards") {
71+
first = "50"
72+
}
73+
74+
q.Set("first", first)
75+
}
76+
77+
u.RawQuery = q.Encode()
78+
79+
resp, err := apiRequest(strings.ToUpper(method), u.String(), body, apiRequestParameters{
80+
ClientID: client.ClientID,
81+
Token: client.Token,
82+
})
83+
if err != nil {
84+
fmt.Printf("Error reading body: %v", err)
85+
return
86+
}
87+
88+
if resp.StatusCode == http.StatusNoContent {
89+
fmt.Println("Endpoint responded with status 204")
90+
return
91+
}
92+
93+
err = json.Unmarshal(resp.Body, &apiResponse)
94+
if err != nil {
95+
fmt.Printf("Error unmarshalling body: %v", err)
96+
return
5097
}
51-
path += Parameters.Encode()
98+
99+
if resp.StatusCode > 299 || resp.StatusCode < 200 {
100+
data = apiResponse
101+
break
102+
}
103+
104+
data.Data = append(data.Data, apiResponse.Data...)
105+
106+
if autopaginate == false {
107+
data.Pagination.Cursor = apiResponse.Pagination.Cursor
108+
break
109+
}
110+
111+
if apiResponse.Pagination.Cursor == "" {
112+
break
113+
}
114+
115+
if apiResponse.Pagination.Cursor == cursor {
116+
break
117+
}
118+
cursor = apiResponse.Pagination.Cursor
119+
52120
}
53-
resp, err := apiRequest(strings.ToUpper(method), baseURL+path, body, apiRequestParameters{
54-
ClientID: client.ClientID,
55-
Token: client.Token,
56-
})
57-
if err != nil {
58-
fmt.Printf("Error reading body: %v", err)
59-
return
121+
122+
// handle json marshalling better; returns empty slice vs. null
123+
if len(data.Data) == 0 && data.Error == "" {
124+
data.Data = make([]interface{}, 0)
60125
}
61126

62-
if resp.StatusCode == http.StatusNoContent {
63-
fmt.Println("Endpoint responded with status 204")
127+
d, err := json.Marshal(data)
128+
if err != nil {
129+
log.Printf("Error marshalling json: %v", err)
64130
return
65131
}
66132

67133
if prettyPrint == true {
68134
var obj map[string]interface{}
69-
if err := json.Unmarshal(resp.Body, &obj); err != nil {
70-
fmt.Printf("Error pretty-printing body: %v", err)
71-
return
72-
}
73-
135+
json.Unmarshal(d, &obj)
74136
// since Command Prompt/Powershell don't support coloring, will pretty print without colors
75137
if runtime.GOOS == "windows" {
76138
s, _ := json.MarshalIndent(obj, "", " ")
@@ -81,12 +143,16 @@ func NewRequest(method string, path string, queryParameters []string, body []byt
81143
f := colorjson.NewFormatter()
82144
f.Indent = 2
83145
f.KeyColor = color.New(color.FgBlue).Add(color.Bold)
84-
s, _ := f.Marshal(obj)
85-
146+
s, err := f.Marshal(obj)
147+
if err != nil {
148+
fmt.Println(err)
149+
return
150+
}
86151
fmt.Println(string(s))
87152
return
88153
}
89-
fmt.Println(string(resp.Body))
154+
155+
fmt.Println(string(d))
90156
return
91157
}
92158

internal/api/api_request.go

+4-4
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,10 @@ import (
66
"bytes"
77
"fmt"
88
"io/ioutil"
9-
"net/http"
109
"time"
1110

1211
"github.com/twitchdev/twitch-cli/internal/request"
12+
"golang.org/x/time/rate"
1313
)
1414

1515
type apiRequestParameters struct {
@@ -26,14 +26,14 @@ func apiRequest(method string, url string, payload []byte, p apiRequestParameter
2626

2727
req.Header.Set("Client-ID", p.ClientID)
2828
req.Header.Set("Content-Type", "application/json")
29+
rl := rate.NewLimiter(rate.Every(time.Minute), 800)
30+
31+
client := NewClient(rl)
2932

3033
if p.Token != "" {
3134
req.Header.Set("Authorization", "Bearer "+p.Token)
3235
}
3336

34-
client := &http.Client{
35-
Timeout: time.Second * 10,
36-
}
3737
resp, err := client.Do(req)
3838
if err != nil {
3939
fmt.Printf("Error reading body: %v", err)

internal/api/api_test.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,8 @@ func TestNewRequest(t *testing.T) {
3939
viper.Set("accesstoken", "4567")
4040
viper.Set("refreshtoken", "123")
4141

42-
NewRequest("POST", "", []string{"test=1", "test=2"}, nil, true)
43-
NewRequest("POST", "", []string{"test=1", "test=2"}, nil, false)
42+
NewRequest("POST", "", []string{"test=1", "test=2"}, nil, true, false)
43+
NewRequest("POST", "", []string{"test=1", "test=2"}, nil, false, true)
4444
}
4545

4646
func TestValidOptions(t *testing.T) {

internal/api/client.go

+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
package api
4+
5+
import (
6+
"context"
7+
"net/http"
8+
"time"
9+
10+
"golang.org/x/time/rate"
11+
)
12+
13+
type RLClient struct {
14+
client *http.Client
15+
RateLimiter *rate.Limiter
16+
}
17+
18+
func (c *RLClient) Do(req *http.Request) (*http.Response, error) {
19+
err := c.RateLimiter.Wait(context.Background())
20+
if err != nil {
21+
return nil, err
22+
}
23+
resp, err := c.client.Do(req)
24+
if err != nil {
25+
return nil, err
26+
}
27+
return resp, nil
28+
}
29+
30+
func NewClient(l *rate.Limiter) *RLClient {
31+
client := http.Client{
32+
Timeout: time.Second * 10,
33+
}
34+
35+
return &RLClient{
36+
client: &client,
37+
RateLimiter: l,
38+
}
39+
}

internal/api/client_test.go

+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
package api
4+
5+
import (
6+
"io/ioutil"
7+
"net/http"
8+
"net/http/httptest"
9+
"testing"
10+
"time"
11+
12+
"github.com/twitchdev/twitch-cli/internal/util"
13+
"golang.org/x/time/rate"
14+
)
15+
16+
func TestNewClient(t *testing.T) {
17+
a := util.SetupTestEnv(t)
18+
19+
var ok = "{\"status\":\"ok\"}"
20+
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
21+
w.WriteHeader(http.StatusOK)
22+
w.Write([]byte(ok))
23+
24+
_, err := ioutil.ReadAll(r.Body)
25+
a.Nil(err)
26+
27+
}))
28+
29+
rl := rate.NewLimiter(rate.Every(time.Minute), 800)
30+
c := NewClient(rl)
31+
32+
req, _ := http.NewRequest(http.MethodGet, ts.URL, nil)
33+
resp, err := c.Do(req)
34+
a.Nil(err)
35+
36+
body, err := ioutil.ReadAll(resp.Body)
37+
defer resp.Body.Close()
38+
a.Equal(ok, string(body), "Body mismatch")
39+
40+
req, _ = http.NewRequest(http.MethodGet, "potato", nil)
41+
resp, err = c.Do(req)
42+
a.NotNil(err)
43+
}

0 commit comments

Comments
 (0)