diff --git a/README.md b/README.md index b5c25ed3..e9fe8a17 100644 --- a/README.md +++ b/README.md @@ -57,7 +57,6 @@ The CLI currently supports the following products: - [api](./docs/api.md) - [configure](./docs/configure.md) -- [drops](./docs/drops.md) - [event](docs/event.md) - [token](docs/token.md) - [version](docs/version.md) diff --git a/cmd/drops.go b/cmd/drops.go deleted file mode 100644 index 5f586296..00000000 --- a/cmd/drops.go +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 -package cmd - -import ( - "github.com/spf13/cobra" - "github.com/twitchdev/twitch-cli/internal/drops" -) - -var ( - gameID string - userID string - filename string -) - -// dropsCmd represents the drops command -var dropsCmd = &cobra.Command{ - Use: "drops", - Short: "Used to interface with Drops services.", -} - -var exportDropsCmd = &cobra.Command{ - Use: "export", - Short: "Exports a CSV with a list of entitlements from the stored Client ID", - Run: runDropsCmd, -} - -func init() { - rootCmd.AddCommand(dropsCmd) - dropsCmd.AddCommand(exportDropsCmd) - - exportDropsCmd.Flags().StringVarP(&filename, "filename", "f", "", "Filename to write the output to.") - exportDropsCmd.Flags().StringVarP(&gameID, "game-id", "g", "", "ID of the game to get entitlements for.") - exportDropsCmd.Flags().StringVarP(&userID, "user-id", "u", "", "ID of the user to get entitlements for.") - exportDropsCmd.MarkFlagRequired("filename") - -} - -func runDropsCmd(cmd *cobra.Command, args []string) { - drops.ExportEntitlements(filename, gameID, userID) -} diff --git a/docs/drops.md b/docs/drops.md deleted file mode 100644 index 3ad844dc..00000000 --- a/docs/drops.md +++ /dev/null @@ -1,32 +0,0 @@ -# Drops -- [Drops](#drops) - - [Description](#description) - - [Export](#export) - -## Description - -The `drops` command contains sub-commands to interact with the Drops product. - -## Export - -Used to export Drops entitlements into a provided CSV filename. - -**Args** - -None. - - -**Flags** - -| Flag | Shorthand | Description | Example | Required? (Y/N) | -|--------------|-----------|-------------------------------------------------------------------------------------------------------|-----------------------------|-----------------| -| `--filename` | `-f` | Name of the CSV file to be generated. | `-f drops_entitlements.csv` | Y | -| `--game-id` | `-T` | ID of the game to be filtered for. If unsure, use [api get games](./api.md) described in the example. | `-g websub` | N | -| `--user-id` | `-t` | Denotes the user's TUID of the entitlement to filter for. | `-u 44635596` | N | - -**Examples** - -```sh -twitch api get games -q "name=Fortnite" -twitch drops export -f "fortnite_drops_entitlements.csv" -g 33214 -``` diff --git a/internal/api/api_test.go b/internal/api/api_test.go index b05e43af..d13fb6df 100644 --- a/internal/api/api_test.go +++ b/internal/api/api_test.go @@ -5,6 +5,7 @@ package api import ( "net/http" "net/http/httptest" + "strings" "testing" "time" @@ -29,6 +30,19 @@ func TestNewRequest(t *testing.T) { a.Equal(params.ClientID, r.Header.Get("Client-ID"), "ClientID mismatch") a.Equal("Bearer "+params.Token, r.Header.Get("Authorization"), "Token mismatch") + if r.URL.Path == "/error" { + w.WriteHeader(http.StatusInternalServerError) + } else if r.URL.Path == "/nocontent" { + w.WriteHeader(http.StatusNoContent) + return + } else if r.URL.Path == "/cursor" { + if strings.Contains(r.URL.RawQuery, "after") { + w.Write([]byte("{}")) + return + } + w.Write([]byte("{\"data\":[],\"pagination\":{\"cursor\":\"test\"}}")) + return + } w.Write([]byte("{}")) })) defer ts.Close() @@ -39,8 +53,18 @@ func TestNewRequest(t *testing.T) { viper.Set("accesstoken", "4567") viper.Set("refreshtoken", "123") - NewRequest("POST", "", []string{"test=1", "test=2"}, nil, true, false) - NewRequest("POST", "", []string{"test=1", "test=2"}, nil, false, true) + // tests for normal get requests + NewRequest("GET", "", []string{"test=1", "test=2"}, nil, true, false) + NewRequest("GET", "", []string{"test=1", "test=2"}, nil, false, true) + + // testing cursors autopagination + NewRequest("GET", "/cursor", []string{"test=1", "test=2"}, nil, false, true) + + // testing 204 no-content apis + NewRequest("POST", "/nocontent", []string{"test=1", "test=2"}, nil, false, false) + + // testing 500 errors + NewRequest("GET", "/error", []string{"test=1", "test=2"}, nil, false, true) } func TestValidOptions(t *testing.T) { diff --git a/internal/drops/client.go b/internal/drops/client.go deleted file mode 100644 index 0084232a..00000000 --- a/internal/drops/client.go +++ /dev/null @@ -1,39 +0,0 @@ -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 -package drops - -import ( - "context" - "net/http" - "time" - - "golang.org/x/time/rate" -) - -type RLClient struct { - client *http.Client - RateLimiter *rate.Limiter -} - -func (c *RLClient) Do(req *http.Request) (*http.Response, error) { - err := c.RateLimiter.Wait(context.Background()) - if err != nil { - return nil, err - } - resp, err := c.client.Do(req) - if err != nil { - return nil, err - } - return resp, nil -} - -func NewClient(l *rate.Limiter) *RLClient { - client := http.Client{ - Timeout: time.Second * 10, - } - - return &RLClient{ - client: &client, - RateLimiter: l, - } -} diff --git a/internal/drops/client_test.go b/internal/drops/client_test.go deleted file mode 100644 index 1a340f03..00000000 --- a/internal/drops/client_test.go +++ /dev/null @@ -1,43 +0,0 @@ -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 -package drops - -import ( - "io/ioutil" - "net/http" - "net/http/httptest" - "testing" - "time" - - "github.com/twitchdev/twitch-cli/internal/util" - "golang.org/x/time/rate" -) - -func TestNewClient(t *testing.T) { - a := util.SetupTestEnv(t) - - var ok = "{\"status\":\"ok\"}" - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusOK) - w.Write([]byte(ok)) - - _, err := ioutil.ReadAll(r.Body) - a.Nil(err) - - })) - - rl := rate.NewLimiter(rate.Every(10*time.Second), 50) - c := NewClient(rl) - - req, _ := http.NewRequest(http.MethodGet, ts.URL, nil) - resp, err := c.Do(req) - a.Nil(err) - - body, err := ioutil.ReadAll(resp.Body) - defer resp.Body.Close() - a.Equal(ok, string(body), "Body mismatch") - - req, _ = http.NewRequest(http.MethodGet, "potato", nil) - resp, err = c.Do(req) - a.NotNil(err) -} diff --git a/internal/drops/exporter.go b/internal/drops/exporter.go deleted file mode 100644 index ab347c1b..00000000 --- a/internal/drops/exporter.go +++ /dev/null @@ -1,167 +0,0 @@ -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 -package drops - -import ( - "encoding/csv" - "encoding/json" - "fmt" - "io/ioutil" - "net/http" - "net/url" - "os" - "time" - - "github.com/spf13/viper" - "github.com/twitchdev/twitch-cli/internal/api" - "github.com/twitchdev/twitch-cli/internal/models" - "github.com/twitchdev/twitch-cli/internal/request" - "github.com/twitchdev/twitch-cli/internal/util" - "golang.org/x/time/rate" -) - -type DropsEntitlementExportParameters struct { - GameID string - UserID string - URL string - Cursor string -} - -var ( - TOKEN string - CLIENT_ID string - ENTITLEMENTS []models.DropsEntitlementsData -) - -var BASE_URL = "https://api.twitch.tv/helix/entitlements/drops" - -func ExportEntitlements(filename string, gameID string, userID string) { - c, err := api.GetClientInformation() - if err != nil { - return - } - - CLIENT_ID = c.ClientID - TOKEN = c.Token - - if viper.GetString("BASE_URL") != "" { - BASE_URL = viper.GetString("BASE_URL") - } - - p := DropsEntitlementExportParameters{ - URL: BASE_URL, - GameID: gameID, - UserID: userID, - } - for { - var e models.DropsEntitlementsResponse - resp, err := makeAPIRequest(p) - body, err := ioutil.ReadAll(resp.Body) - defer resp.Body.Close() - if err != nil { - fmt.Printf("Error reading body: %v", err) - return - } - err = json.Unmarshal(body, &e) - if err != nil { - fmt.Printf("Error reading body: %v", err) - return - } - - ENTITLEMENTS = append(ENTITLEMENTS, e.Data...) - - if resp.StatusCode == 500 { - fmt.Println(fmt.Sprintf("[%v] Got 500 from endpoint; Make sure that your client is marked in the correct organization.", util.GetTimestamp().Format(time.RFC3339))) - break - } - - if len(e.Data) == 0 { - fmt.Println("No results, stopping.") - break - } - - if e.Pagination.Cursor == "" { - fmt.Println(fmt.Sprintf("[%v] End of records, found %v records.", util.GetTimestamp().Format(time.RFC3339), len(ENTITLEMENTS))) - break - } - - p.Cursor = e.Pagination.Cursor - - fmt.Println(fmt.Sprintf("[%v] Found %v records, hitting next page.", util.GetTimestamp().Format(time.RFC3339), len(e.Data))) - - } - - // don't make a csv if empty :) - if len(ENTITLEMENTS) == 0 { - return - } - - file, err := os.Create(filename) - if err != nil { - fmt.Printf("Error writing file %v", err) - return - } - - w := csv.NewWriter(file) - - headers := []string{ - "id", - "user_id", - "benefit_id", - "timestamp", - "game_id", - } - - w.Write(headers) - - for _, e := range ENTITLEMENTS { - v := make([]string, 0) - v = append(v, - e.ID, - e.UserID, - e.BenefitID, - e.Timestamp, - e.GameID, - ) - - w.Write(v) - } - //finish writing - w.Flush() -} - -func makeAPIRequest(p DropsEntitlementExportParameters) (*http.Response, error) { - u, err := url.Parse(p.URL) - if err != nil { - return nil, err - } - - q := u.Query() - q.Add("first", "100") - - if p.GameID != "" { - q.Add("game_id", p.GameID) - } - if p.UserID != "" { - q.Add("user_id", p.UserID) - } - if p.Cursor != "" { - q.Add("after", p.Cursor) - } - - u.RawQuery = q.Encode() - - req, _ := request.NewRequest(http.MethodGet, u.String(), nil) - req.Header.Set("Client-ID", CLIENT_ID) - req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", TOKEN)) - - rl := rate.NewLimiter(rate.Every(10*time.Second), 100) - client := NewClient(rl) - - resp, err := client.Do(req) - if err != nil { - return nil, err - } - - return resp, nil -} diff --git a/internal/drops/exporter_test.go b/internal/drops/exporter_test.go deleted file mode 100644 index 101c00a4..00000000 --- a/internal/drops/exporter_test.go +++ /dev/null @@ -1,87 +0,0 @@ -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 -package drops - -import ( - "encoding/json" - "io/ioutil" - "net/http" - "net/http/httptest" - "testing" - "time" - - "github.com/spf13/viper" - "github.com/twitchdev/twitch-cli/internal/models" - "github.com/twitchdev/twitch-cli/internal/util" -) - -func TestExportEntitlements(t *testing.T) { - a := util.SetupTestEnv(t) - viper.Set("clientid", "1111") - viper.Set("clientsecret", "2222") - viper.Set("accesstoken", "4567") - viper.Set("refreshtoken", "123") - viper.Set("tokenexpiration", "0") - - var okModel = &models.DropsEntitlementsResponse{ - Data: []models.DropsEntitlementsData{ - { - ID: "1234", - BenefitID: "234", - GameID: "34", - UserID: "4", - Timestamp: util.GetTimestamp().Format(time.RFC3339), - }, - }, - Pagination: struct { - Cursor string "json:\"cursor\"" - }{ - Cursor: "1234", - }, - } - ok, err := json.Marshal(okModel) - a.Nil(err) - - var emptyModel = &models.DropsEntitlementsResponse{ - Data: []models.DropsEntitlementsData{}, - } - - empty, err := json.Marshal(emptyModel) - a.Nil(err) - - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.URL.Path == "/data" { - w.WriteHeader(http.StatusOK) - cursor := r.URL.Query().Get("after") - if cursor != "" { - w.Write(empty) - return - } - w.Write(ok) - _, err := ioutil.ReadAll(r.Body) - a.Nil(err) - } - if r.URL.Path == "/empty" { - w.WriteHeader(http.StatusOK) - w.Write(empty) - _, err := ioutil.ReadAll(r.Body) - a.Nil(err) - } - if r.URL.Path == "/error" { - w.WriteHeader(http.StatusInternalServerError) - w.Write([]byte(ok)) - _, err := ioutil.ReadAll(r.Body) - a.Nil(err) - } - })) - filename := ".testing-csv.csv" - - viper.Set("BASE_URL", ts.URL+"/data") - ExportEntitlements(filename, "", "") - - viper.Set("BASE_URL", ts.URL+"/empty") - ExportEntitlements(filename, "1", "") - - viper.Set("BASE_URL", ts.URL+"/error") - ExportEntitlements(filename, "", "2") -} diff --git a/internal/events/trigger/forward_event.go b/internal/events/trigger/forward_event.go index c6b51485..1a104196 100644 --- a/internal/events/trigger/forward_event.go +++ b/internal/events/trigger/forward_event.go @@ -95,6 +95,9 @@ func ForwardEvent(p ForwardParamters) (*http.Response, error) { client := &http.Client{ Timeout: time.Second * 10, + CheckRedirect: func(req *http.Request, via []*http.Request) error { + return http.ErrUseLastResponse + }, } resp, err := client.Do(req) if err != nil { diff --git a/internal/events/types/authorization_revoke/authorization_revoke.go b/internal/events/types/authorization_revoke/authorization_revoke.go index 39444313..09b32f39 100644 --- a/internal/events/types/authorization_revoke/authorization_revoke.go +++ b/internal/events/types/authorization_revoke/authorization_revoke.go @@ -52,6 +52,7 @@ func (e Event) GenerateEvent(params events.MockEventParameters) (events.MockEven Method: "webhook", Callback: "null", }, + Cost: 1, CreatedAt: util.GetTimestamp().Format(time.RFC3339Nano), }, Event: models.AuthorizationRevokeEvent{ diff --git a/internal/events/types/ban/ban.go b/internal/events/types/ban/ban.go index 83db53e7..fd9816f3 100644 --- a/internal/events/types/ban/ban.go +++ b/internal/events/types/ban/ban.go @@ -50,6 +50,7 @@ func (e Event) GenerateEvent(params events.MockEventParameters) (events.MockEven Method: "webhook", Callback: "null", }, + Cost: 0, CreatedAt: util.GetTimestamp().Format(time.RFC3339Nano), }, Event: models.BanEventSubEvent{ diff --git a/internal/events/types/channel_points_redemption/redemption_event.go b/internal/events/types/channel_points_redemption/redemption_event.go index 491c2297..4a74b5cb 100644 --- a/internal/events/types/channel_points_redemption/redemption_event.go +++ b/internal/events/types/channel_points_redemption/redemption_event.go @@ -60,6 +60,7 @@ func (e Event) GenerateEvent(params events.MockEventParameters) (events.MockEven Method: "webhook", Callback: "null", }, + Cost: 0, CreatedAt: tNow, }, Event: models.RedemptionEventSubEvent{ diff --git a/internal/events/types/channel_points_reward/reward_event.go b/internal/events/types/channel_points_reward/reward_event.go index 9d6d086a..ca9ec927 100644 --- a/internal/events/types/channel_points_reward/reward_event.go +++ b/internal/events/types/channel_points_reward/reward_event.go @@ -53,6 +53,7 @@ func (e Event) GenerateEvent(params events.MockEventParameters) (events.MockEven Method: "webhook", Callback: "null", }, + Cost: 0, CreatedAt: tNow, }, Event: models.RewardEventSubEvent{ diff --git a/internal/events/types/cheer/cheer_event.go b/internal/events/types/cheer/cheer_event.go index b73dd7ca..4a3e1bd4 100644 --- a/internal/events/types/cheer/cheer_event.go +++ b/internal/events/types/cheer/cheer_event.go @@ -55,6 +55,7 @@ func (e Event) GenerateEvent(params events.MockEventParameters) (events.MockEven Method: "webhook", Callback: "null", }, + Cost: 0, CreatedAt: util.GetTimestamp().Format(time.RFC3339Nano), }, Event: models.CheerEventSubEvent{ diff --git a/internal/events/types/follow/follow_event.go b/internal/events/types/follow/follow_event.go index f255a161..f1a19af2 100644 --- a/internal/events/types/follow/follow_event.go +++ b/internal/events/types/follow/follow_event.go @@ -47,6 +47,7 @@ func (e Event) GenerateEvent(p events.MockEventParameters) (events.MockEventResp Method: "webhook", Callback: "null", }, + Cost: 0, CreatedAt: util.GetTimestamp().Format(time.RFC3339Nano), }, Event: models.FollowEventSubEvent{ diff --git a/internal/events/types/hype_train/hype_train_event.go b/internal/events/types/hype_train/hype_train_event.go index eadc6c5f..c98e639e 100644 --- a/internal/events/types/hype_train/hype_train_event.go +++ b/internal/events/types/hype_train/hype_train_event.go @@ -58,6 +58,7 @@ func (e Event) GenerateEvent(params events.MockEventParameters) (events.MockEven Method: "webhook", Callback: "null", }, + Cost: 0, CreatedAt: util.GetTimestamp().Format(time.RFC3339Nano), }, Event: models.HypeTrainEventSubEvent{ diff --git a/internal/events/types/moderator_change/moderator_change_event.go b/internal/events/types/moderator_change/moderator_change_event.go index 075e61ca..84fa4adb 100644 --- a/internal/events/types/moderator_change/moderator_change_event.go +++ b/internal/events/types/moderator_change/moderator_change_event.go @@ -43,7 +43,7 @@ func (e Event) GenerateEvent(params events.MockEventParameters) (events.MockEven ID: params.ID, Status: "enabled", Type: triggerMapping[params.Transport][params.Trigger], - Version: "beta", + Version: "1", Condition: models.EventsubCondition{ BroadcasterUserID: params.ToUserID, }, @@ -51,6 +51,7 @@ func (e Event) GenerateEvent(params events.MockEventParameters) (events.MockEven Method: "webhook", Callback: "null", }, + Cost: 0, CreatedAt: util.GetTimestamp().Format(time.RFC3339Nano), }, Event: models.ModeratorChangeEventSubEvent{ diff --git a/internal/events/types/raid/raid.go b/internal/events/types/raid/raid.go index 1a1a4be3..8bf46e77 100644 --- a/internal/events/types/raid/raid.go +++ b/internal/events/types/raid/raid.go @@ -38,7 +38,7 @@ func (e Event) GenerateEvent(params events.MockEventParameters) (events.MockEven ID: params.ID, Status: "enabled", Type: triggerMapping[params.Transport][params.Trigger], - Version: "beta", + Version: "1", Condition: models.EventsubCondition{ ToBroadcasterUserID: params.ToUserID, }, @@ -46,6 +46,7 @@ func (e Event) GenerateEvent(params events.MockEventParameters) (events.MockEven Method: "webhook", Callback: "null", }, + Cost: 0, CreatedAt: util.GetTimestamp().Format(time.RFC3339Nano), }, Event: models.RaidEvent{ diff --git a/internal/events/types/stream_change/stream_change_event.go b/internal/events/types/stream_change/stream_change_event.go index 5db8b58a..3e92e989 100644 --- a/internal/events/types/stream_change/stream_change_event.go +++ b/internal/events/types/stream_change/stream_change_event.go @@ -54,6 +54,7 @@ func (e Event) GenerateEvent(params events.MockEventParameters) (events.MockEven Method: "webhook", Callback: "null", }, + Cost: 0, CreatedAt: util.GetTimestamp().Format(time.RFC3339Nano), }, Event: models.ChannelUpdateEventSubEvent{ diff --git a/internal/events/types/streamdown/streamdown.go b/internal/events/types/streamdown/streamdown.go index 3c28c9c0..0f286fb7 100644 --- a/internal/events/types/streamdown/streamdown.go +++ b/internal/events/types/streamdown/streamdown.go @@ -48,6 +48,7 @@ func (e Event) GenerateEvent(params events.MockEventParameters) (events.MockEven Method: "webhook", Callback: "null", }, + Cost: 0, CreatedAt: util.GetTimestamp().Format(time.RFC3339Nano), }, Event: models.StreamDownEventSubEvent{ @@ -62,8 +63,7 @@ func (e Event) GenerateEvent(params events.MockEventParameters) (events.MockEven } case models.TransportWebSub: body := *&models.StreamDownWebSubResponse{ - Data: []models.StreamDownWebSubResponseData{ - }} + Data: []models.StreamDownWebSubResponseData{}} event, err = json.Marshal(body) if err != nil { diff --git a/internal/events/types/streamup/streamup.go b/internal/events/types/streamup/streamup.go index 8b69b863..f0f619c8 100644 --- a/internal/events/types/streamup/streamup.go +++ b/internal/events/types/streamup/streamup.go @@ -52,6 +52,7 @@ func (e Event) GenerateEvent(params events.MockEventParameters) (events.MockEven Method: "webhook", Callback: "null", }, + Cost: 0, CreatedAt: util.GetTimestamp().Format(time.RFC3339Nano), }, Event: models.StreamUpEventSubEvent{ diff --git a/internal/events/types/subscribe/sub_event.go b/internal/events/types/subscribe/sub_event.go index faaef126..8f783f93 100644 --- a/internal/events/types/subscribe/sub_event.go +++ b/internal/events/types/subscribe/sub_event.go @@ -68,6 +68,7 @@ func (e Event) GenerateEvent(params events.MockEventParameters) (events.MockEven Method: "webhook", Callback: "null", }, + Cost: 0, CreatedAt: util.GetTimestamp().Format(time.RFC3339Nano), }, Event: models.SubEventSubEvent{ diff --git a/internal/models/eventsub.go b/internal/models/eventsub.go index ba378760..3f17b77a 100644 --- a/internal/models/eventsub.go +++ b/internal/models/eventsub.go @@ -10,6 +10,7 @@ type EventsubSubscription struct { Condition EventsubCondition `json:"condition"` Transport EventsubTransport `json:"transport"` CreatedAt string `json:"created_at"` + Cost int64 `json:"cost"` } type EventsubTransport struct {