diff --git a/eventsub.go b/eventsub.go index 2571292..ac587f0 100644 --- a/eventsub.go +++ b/eventsub.go @@ -645,50 +645,59 @@ type EventSubExtensionBitsTransactionCreateEvent struct { Product EventSubProduct `json:"product"` } -// Data for a hype train begin notification +// Data for a hype train begin notification (V2) type EventSubHypeTrainBeginEvent struct { - BroadcasterUserID string `json:"broadcaster_user_id"` - BroadcasterUserLogin string `json:"broadcaster_user_login"` - BroadcasterUserName string `json:"broadcaster_user_name"` - Total int `json:"total"` - Progress int `json:"progress"` - Goal int `json:"goal"` - TopContributions []EventSubContribution `json:"top_contributions"` - LastContribution EventSubContribution `json:"last_contribution"` - StartedAt Time `json:"started_at"` - ExpiresAt Time `json:"expires_at"` - IsGoldenKappaTrain bool `json:"is_golden_kappa_train"` -} - -// Data for a hype train progress notification + ID string `json:"id"` + BroadcasterUserID string `json:"broadcaster_user_id"` + BroadcasterUserLogin string `json:"broadcaster_user_login"` + BroadcasterUserName string `json:"broadcaster_user_name"` + Total int `json:"total"` + Progress int `json:"progress"` + Goal int `json:"goal"` + Level int `json:"level"` + AllTimeHighLevel int `json:"all_time_high_level"` + AllTimeHighTotal int `json:"all_time_high_total"` + TopContributions []EventSubContribution `json:"top_contributions"` + SharedTrainParticipants []EventSubSharedTrainParticipant `json:"shared_train_participants"` + StartedAt Time `json:"started_at"` + ExpiresAt Time `json:"expires_at"` + Type string `json:"type"` + IsSharedTrain bool `json:"is_shared_train"` +} + +// Data for a hype train progress notification (V2) type EventSubHypeTrainProgressEvent struct { - BroadcasterUserID string `json:"broadcaster_user_id"` - BroadcasterUserLogin string `json:"broadcaster_user_login"` - BroadcasterUserName string `json:"broadcaster_user_name"` - Level int `json:"level"` - Total int `json:"total"` - Progress int `json:"progress"` - Goal int `json:"goal"` - TopContributions []EventSubContribution `json:"top_contributions"` - LastContribution EventSubContribution `json:"last_contribution"` - StartedAt Time `json:"started_at"` - ExpiresAt Time `json:"expires_at"` - IsGoldenKappaTrain bool `json:"is_golden_kappa_train"` -} - -// Data for a hype train end notification + ID string `json:"id"` + BroadcasterUserID string `json:"broadcaster_user_id"` + BroadcasterUserLogin string `json:"broadcaster_user_login"` + BroadcasterUserName string `json:"broadcaster_user_name"` + Total int `json:"total"` + Progress int `json:"progress"` + Goal int `json:"goal"` + Level int `json:"level"` + TopContributions []EventSubContribution `json:"top_contributions"` + SharedTrainParticipants []EventSubSharedTrainParticipant `json:"shared_train_participants"` + StartedAt Time `json:"started_at"` + ExpiresAt Time `json:"expires_at"` + Type string `json:"type"` + IsSharedTrain bool `json:"is_shared_train"` +} + +// Data for a hype train end notification (V2) type EventSubHypeTrainEndEvent struct { - ID string `json:"id"` - BroadcasterUserID string `json:"broadcaster_user_id"` - BroadcasterUserLogin string `json:"broadcaster_user_login"` - BroadcasterUserName string `json:"broadcaster_user_name"` - Level int `json:"level"` - Total int `json:"total"` - TopContributions []EventSubContribution `json:"top_contributions"` - StartedAt Time `json:"started_at"` - EndedAt Time `json:"ended_at"` - CooldownEndsAt Time `json:"cooldown_ends_at"` - IsGoldenKappaTrain bool `json:"is_golden_kappa_train"` + ID string `json:"id"` + BroadcasterUserID string `json:"broadcaster_user_id"` + BroadcasterUserLogin string `json:"broadcaster_user_login"` + BroadcasterUserName string `json:"broadcaster_user_name"` + Total int `json:"total"` + Level int `json:"level"` + TopContributions []EventSubContribution `json:"top_contributions"` + SharedTrainParticipants []EventSubSharedTrainParticipant `json:"shared_train_participants"` + StartedAt Time `json:"started_at"` + EndedAt Time `json:"ended_at"` + CooldownEndsAt Time `json:"cooldown_ends_at"` + Type string `json:"type"` + IsSharedTrain bool `json:"is_shared_train"` } // Data for a stream online notification @@ -744,7 +753,14 @@ type EventSubContribution struct { UserLogin string `json:"user_login"` UserName string `json:"user_name"` Type string `json:"type"` - Total int64 `json:"total"` + Total int `json:"total"` +} + +// This belongs to a hype train and defines a shared train participant +type EventSubSharedTrainParticipant struct { + BroadcasterUserID string `json:"broadcaster_user_id"` + BroadcasterUserLogin string `json:"broadcaster_user_login"` + BroadcasterUserName string `json:"broadcaster_user_name"` } // This belong to an outcome and defines user reward @@ -840,6 +856,7 @@ type EventSubChannelGoalEndEvent struct { type EventSubCharityAmount struct { Value int64 `json:"value"` + Amount int64 `json:"amount"` DecimalPlaces int64 `json:"decimal_places"` Currency string `json:"currency"` } diff --git a/hype_train.go b/hype_train.go index 87a7526..60d5778 100644 --- a/hype_train.go +++ b/hype_train.go @@ -6,6 +6,62 @@ type HypeTrainContribuition struct { User string `json:"user"` } +// HypeTrainStatusContribution contains information about a contribution to a Hype Train +type HypeTrainStatusContribution struct { + UserID string `json:"user_id"` + UserLogin string `json:"user_login"` + UserName string `json:"user_name"` + Type string `json:"type"` + Total int64 `json:"total"` +} + +type SharedTrainParticipant struct { + BroadcasterUserID string `json:"broadcaster_user_id"` + BroadcasterUserLogin string `json:"broadcaster_user_login"` + BroadcasterUserName string `json:"broadcaster_user_name"` +} + +type CurrentHypeTrainStatus struct { + ID string `json:"id"` + BroadcasterUserID string `json:"broadcaster_user_id"` + BroadcasterUserLogin string `json:"broadcaster_user_login"` + BroadcasterUserName string `json:"broadcaster_user_name"` + Level int64 `json:"level"` + Total int64 `json:"total"` + Progress int64 `json:"progress"` + Goal int64 `json:"goal"` + TopContributions []HypeTrainStatusContribution `json:"top_contributions"` + SharedTrainParticipants []SharedTrainParticipant `json:"shared_train_participants"` + StartedAt Time `json:"started_at"` + ExpiresAt Time `json:"expires_at"` + Type string `json:"type"` +} + +type AllTimeHighHypeTrainStatus struct { + Level int64 `json:"level"` + Total int64 `json:"total"` + AchievedAt Time `json:"achieved_at"` +} + +type HypeTrainStatus struct { + Current *CurrentHypeTrainStatus `json:"current"` + AllTimeHigh *AllTimeHighHypeTrainStatus `json:"all_time_high"` + SharedAllTimeHigh *AllTimeHighHypeTrainStatus `json:"shared_all_time_high"` +} + +type ManyHypeTrainStatuses struct { + Statuses []HypeTrainStatus `json:"data"` +} + +type HypeTrainStatusResponse struct { + ResponseCommon + Data ManyHypeTrainStatuses +} + +type HypeTrainStatusParams struct { + BroadcasterID string `query:"broadcaster_id"` +} + type HypeTrainEvent struct { ID string `json:"id"` EventType string `json:"event_type"` @@ -44,6 +100,23 @@ type HypeTrainEventsParams struct { ID string `query:"id"` } +// GetHypeTrainStatus gets the Hype Train status for the specified broadcaster. +// Required scope: channel:read:hype_train +func (c *Client) GetHypeTrainStatus(params *HypeTrainStatusParams) (*HypeTrainStatusResponse, error) { + resp, err := c.get("/hypetrain/status", &ManyHypeTrainStatuses{}, params) + if err != nil { + return nil, err + } + + status := &HypeTrainStatusResponse{} + resp.HydrateResponseCommon(&status.ResponseCommon) + status.Data.Statuses = resp.Data.(*ManyHypeTrainStatuses).Statuses + + return status, nil +} + +// GetHypeTrainEvents gets Hype Train events for a broadcaster. +// Deprecated: Use GetHypeTrainStatus instead. // Required scope: channel:read:hype_train func (c *Client) GetHypeTrainEvents(params *HypeTrainEventsParams) (*HypeTrainEventsResponse, error) { resp, err := c.get("/hypetrain/events", &ManyHypeTrainEvents{}, params) diff --git a/hype_train_test.go b/hype_train_test.go index bf76eb1..f1a271c 100644 --- a/hype_train_test.go +++ b/hype_train_test.go @@ -88,3 +88,133 @@ func TestGetHypeTrainEvents(t *testing.T) { t.Error("expected error does match return error") } } + +func TestGetHypeTrainStatus(t *testing.T) { + t.Parallel() + + testCases := []struct { + statusCode int + options *Options + params *HypeTrainStatusParams + respBody string + }{ + { + http.StatusBadRequest, + &Options{ClientID: "my-client-id"}, + &HypeTrainStatusParams{BroadcasterID: ""}, + `{"error":"Bad Request","status":400,"message":"Missing required parameter \"broadcaster_id\""}`, + }, + { + http.StatusOK, + &Options{ClientID: "my-client-id"}, + &HypeTrainStatusParams{BroadcasterID: "1337"}, + `{"data":[{"current":{"id":"1b0AsbInCHZW2SQFQkCzqN07Ib2","broadcaster_user_id":"1337","broadcaster_user_login":"cool_user","broadcaster_user_name":"Cool_User","level":2,"total":700,"progress":200,"goal":1000,"top_contributions":[{"user_id":"123","user_login":"pogchamp","user_name":"PogChamp","type":"bits","total":50},{"user_id":"456","user_login":"kappa","user_name":"Kappa","type":"subscription","total":45}],"shared_train_participants":[{"broadcaster_user_id":"456","broadcaster_user_login":"pogchamp","broadcaster_user_name":"PogChamp"},{"broadcaster_user_id":"321","broadcaster_user_login":"pogchamp","broadcaster_user_name":"PogChamp"}],"started_at":"2020-07-15T17:16:03.17106713Z","expires_at":"2020-07-15T17:16:11.17106713Z","type":"golden_kappa"},"all_time_high":{"level":6,"total":2850,"achieved_at":"2020-04-24T20:12:21.003802269Z"},"shared_all_time_high":{"level":16,"total":23850,"achieved_at":"2020-04-27T20:12:21.003802269Z"}}]}`, + }, + { + http.StatusOK, + &Options{ClientID: "my-client-id"}, + &HypeTrainStatusParams{BroadcasterID: "1338"}, + `{"data":[]}`, // No active hype train + }, + } + + for _, testCase := range testCases { + c := newMockClient(testCase.options, newMockHandler(testCase.statusCode, testCase.respBody, nil)) + + resp, err := c.GetHypeTrainStatus(testCase.params) + if err != nil { + t.Error(err) + } + + if resp.StatusCode != testCase.statusCode { + t.Errorf("expected status code to be %d, got %d", testCase.statusCode, resp.StatusCode) + } + + if resp.StatusCode == http.StatusBadRequest { + if resp.Error != "Bad Request" { + t.Errorf("expected error to be %s, got %s", "Bad Request", resp.Error) + } + + if resp.ErrorStatus != http.StatusBadRequest { + t.Errorf("expected error status to be %d, got %d", http.StatusBadRequest, resp.ErrorStatus) + } + + expectedErrMsg := "Missing required parameter \"broadcaster_id\"" + if resp.ErrorMessage != expectedErrMsg { + t.Errorf("expected error message to be %s, got %s", expectedErrMsg, resp.ErrorMessage) + } + + continue + } + + if testCase.params.BroadcasterID == "1337" { + // Test case with active hype train + if len(resp.Data.Statuses) != 1 { + t.Errorf("expected hype train statuses len to be 1, got %d", len(resp.Data.Statuses)) + } + + status := resp.Data.Statuses[0] + if status.Current == nil { + t.Error("expected current hype train status to not be nil") + } else { + if status.Current.BroadcasterUserID != "1337" { + t.Errorf("expected broadcaster_user_id to be '1337', got '%s'", status.Current.BroadcasterUserID) + } + + if status.Current.Level != 2 { + t.Errorf("expected level to be 2, got %d", status.Current.Level) + } + + if len(status.Current.TopContributions) != 2 { + t.Errorf("expected top contributions len to be 2, got %d", len(status.Current.TopContributions)) + } + + if len(status.Current.SharedTrainParticipants) != 2 { + t.Errorf("expected shared train participants len to be 2, got %d", len(status.Current.SharedTrainParticipants)) + } + } + + if status.AllTimeHigh == nil { + t.Error("expected all_time_high to not be nil") + } else { + if status.AllTimeHigh.Level != 6 { + t.Errorf("expected all_time_high level to be 6, got %d", status.AllTimeHigh.Level) + } + } + + if status.SharedAllTimeHigh == nil { + t.Error("expected shared_all_time_high to not be nil") + } else { + if status.SharedAllTimeHigh.Level != 16 { + t.Errorf("expected shared_all_time_high level to be 16, got %d", status.SharedAllTimeHigh.Level) + } + } + } else if testCase.params.BroadcasterID == "1338" { + // Test case with no active hype train + if len(resp.Data.Statuses) != 0 { + t.Errorf("expected hype train statuses len to be 0, got %d", len(resp.Data.Statuses)) + } + } + } + + // Test with HTTP Failure + options := &Options{ + ClientID: "my-client-id", + HTTPClient: &badMockHTTPClient{ + newMockHandler(0, "", nil), + }, + } + c := &Client{ + opts: options, + ctx: context.Background(), + } + + _, err := c.GetHypeTrainStatus(&HypeTrainStatusParams{}) + if err == nil { + t.Error("expected error but got nil") + } + + if err.Error() != "Failed to execute API request: Oops, that's bad :(" { + t.Error("expected error does match return error") + } +}