diff --git a/docs/event.md b/docs/event.md index 5c13e5f..46f712b 100644 --- a/docs/event.md +++ b/docs/event.md @@ -52,9 +52,9 @@ This command can take either the Event or Alias listed as an argument. It is pre | `channel.goal.begin` | `goal-begin` | Channel creator goal start event. | | `channel.goal.end` | `goal-end` | Channel creator goal end event. | | `channel.goal.progress` | `goal-progress` | Channel creator goal progress event. | -| `channel.hype_train.begin` | `hype-train-begin` | Channel hype train start event. | -| `channel.hype_train.end` | `hype-train-end` | Channel hype train start event. | -| `channel.hype_train.progress` | `hype-train-progress` | Channel hype train start event. | +| `channel.hype_train.begin` | `hype-train-begin` | Channel hype train begin event. Supports V1 and V2; use `--version 2` for V2 with shared train and type fields. | +| `channel.hype_train.end` | `hype-train-end` | Channel hype train end event. Supports V1 and V2; use `--version 2` for V2 with shared train and type fields. | +| `channel.hype_train.progress` | `hype-train-progress` | Channel hype train progress event. Supports V1 and V2; use `--version 2` for V2 with shared train and type fields. | | `channel.moderator.add` | `add-moderator` | Channel moderator add event. | | `channel.moderator.remove` | `remove-moderator` | Channel moderator removal event. | | `channel.poll.begin` | `poll-begin` | Channel poll begin event. | diff --git a/internal/events/types/hype_train_v2/hype_train_event.go b/internal/events/types/hype_train_v2/hype_train_event.go new file mode 100644 index 0000000..2818727 --- /dev/null +++ b/internal/events/types/hype_train_v2/hype_train_event.go @@ -0,0 +1,172 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +package hype_train_v2 + +import ( + "encoding/json" + "strings" + "time" + + "github.com/twitchdev/twitch-cli/internal/events" + "github.com/twitchdev/twitch-cli/internal/models" + "github.com/twitchdev/twitch-cli/internal/util" +) + +var transportsSupported = map[string]bool{ + models.TransportWebhook: true, + models.TransportWebSocket: true, +} +var triggerSupported = []string{"hype-train-begin", "hype-train-progress", "hype-train-end"} +var triggerMapping = map[string]map[string]string{ + models.TransportWebhook: { + "hype-train-progress": "channel.hype_train.progress", + "hype-train-begin": "channel.hype_train.begin", + "hype-train-end": "channel.hype_train.end", + }, + models.TransportWebSocket: { + "hype-train-progress": "channel.hype_train.progress", + "hype-train-begin": "channel.hype_train.begin", + "hype-train-end": "channel.hype_train.end", + }, +} + +type Event struct{} + +func (e Event) GenerateEvent(params events.MockEventParameters) (events.MockEventResponse, error) { + var event []byte + var err error + + localLevel := util.RandomInt(4) + 1 + localTotal := util.RandomInt(10 * 100) + localGoal := util.RandomInt(10*100*100) + localTotal + localProgress := localTotal - util.RandomInt(100) + + tNow, _ := time.Parse(time.RFC3339Nano, params.Timestamp) + + switch params.Transport { + case models.TransportWebhook, models.TransportWebSocket: + body := models.HypeTrainEventSubResponseV2{ + Subscription: models.EventsubSubscription{ + ID: params.SubscriptionID, + Status: params.SubscriptionStatus, + Type: triggerMapping[params.Transport][params.Trigger], + Version: e.SubscriptionVersion(), + Condition: models.EventsubCondition{ + BroadcasterUserID: params.ToUserID, + }, + Transport: models.EventsubTransport{ + Method: "webhook", + Callback: "null", + }, + Cost: 0, + CreatedAt: params.Timestamp, + }, + Event: models.HypeTrainEventSubEventV2{ + ID: util.RandomGUID(), + BroadcasterUserID: params.ToUserID, + BroadcasterUserLogin: params.ToUserName, + BroadcasterUserName: params.ToUserName, + Total: localTotal, + Progress: &localProgress, + Goal: localGoal, + TopContributions: []models.ContributionData{ + { + TotalContribution: util.RandomInt(10 * 100), + TypeOfContribution: util.RandomType(), + UserWhoMadeContribution: util.RandomUserID(), + UserNameWhoMadeContribution: "cli_user1", + UserLoginWhoMadeContribution: "cli_user1", + }, + { + TotalContribution: util.RandomInt(10 * 100), + TypeOfContribution: util.RandomType(), + UserWhoMadeContribution: util.RandomUserID(), + UserNameWhoMadeContribution: "cli_user2", + UserLoginWhoMadeContribution: "cli_user2", + }, + }, + SharedTrainParticipants: nil, + Level: localLevel, + StartedAtTimestamp: params.Timestamp, + ExpiresAtTimestamp: tNow.Add(5 * time.Minute).Format(time.RFC3339Nano), + IsSharedTrain: false, + Type: "regular", + }, + } + if params.Trigger == "hype-train-begin" { + body.Event.Progress = &localTotal + body.Event.AllTimeHighLevel = localLevel + util.RandomInt(3) + body.Event.AllTimeHighTotal = localTotal + util.RandomInt(10*100) + } + if params.Trigger == "hype-train-end" { + body.Event.CooldownEndsAtTimestamp = tNow.Add(1 * time.Hour).Format(time.RFC3339Nano) + body.Event.EndedAtTimestamp = tNow.Format(time.RFC3339Nano) + body.Event.ExpiresAtTimestamp = "" + body.Event.Goal = 0 + body.Event.Progress = nil + body.Event.StartedAtTimestamp = tNow.Add(5 * -time.Minute).Format(time.RFC3339Nano) + } + event, err = json.Marshal(body) + if err != nil { + return events.MockEventResponse{}, err + } + + // Delete event info if Subscription.Status is not set to "enabled" + if !strings.EqualFold(params.SubscriptionStatus, "enabled") { + var i interface{} + if err := json.Unmarshal([]byte(event), &i); err != nil { + return events.MockEventResponse{}, err + } + if m, ok := i.(map[string]interface{}); ok { + delete(m, "event") // Matches JSON key defined in body variable above + } + + event, err = json.Marshal(i) + if err != nil { + return events.MockEventResponse{}, err + } + } + default: + return events.MockEventResponse{}, nil + } + return events.MockEventResponse{ + ID: params.EventMessageID, + JSON: event, + FromUser: params.FromUserID, + ToUser: params.ToUserID, + }, nil +} +func (e Event) ValidTransport(t string) bool { + return transportsSupported[t] +} +func (e Event) ValidTrigger(t string) bool { + for _, ts := range triggerSupported { + if ts == t { + return true + } + } + return false +} +func (e Event) GetTopic(transport string, trigger string) string { + return triggerMapping[transport][trigger] +} +func (e Event) GetAllTopicsByTransport(transport string) []string { + allTopics := []string{} + for _, topic := range triggerMapping[transport] { + allTopics = append(allTopics, topic) + } + return allTopics +} +func (e Event) GetEventSubAlias(t string) string { + // check for aliases + for trigger, topic := range triggerMapping[models.TransportWebhook] { + if topic == t { + return trigger + } + } + return "" +} + +func (e Event) SubscriptionVersion() string { + return "2" +} diff --git a/internal/events/types/hype_train_v2/hype_train_event_test.go b/internal/events/types/hype_train_v2/hype_train_event_test.go new file mode 100644 index 0000000..5642a98 --- /dev/null +++ b/internal/events/types/hype_train_v2/hype_train_event_test.go @@ -0,0 +1,147 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +package hype_train_v2 + +import ( + "encoding/json" + "testing" + + "github.com/twitchdev/twitch-cli/internal/events" + "github.com/twitchdev/twitch-cli/internal/models" + "github.com/twitchdev/twitch-cli/test_setup" +) + +var toUser = "4567" + +func TestEventSub(t *testing.T) { + a := test_setup.SetupTestEnv(t) + + params := events.MockEventParameters{ + ToUserID: toUser, + Transport: models.TransportWebhook, + Trigger: "hype-train-begin", + SubscriptionStatus: "enabled", + } + + r, err := Event{}.GenerateEvent(params) + a.Nil(err) + + var body models.HypeTrainEventSubResponseV2 + err = json.Unmarshal(r.JSON, &body) + a.Nil(err) + + a.Equal("channel.hype_train.begin", body.Subscription.Type, "Expected event type %v, got %v", "channel.hype_train.begin", body.Subscription.Type) + a.Equal(toUser, body.Event.BroadcasterUserID, "Expected to user %v, got %v", toUser, body.Event.BroadcasterUserID) + a.Equal("2", body.Subscription.Version, "Expected version 2, got %v", body.Subscription.Version) + a.Equal("regular", body.Event.Type, "Expected type regular, got %v", body.Event.Type) + a.Equal(false, body.Event.IsSharedTrain, "Expected is_shared_train false") + a.Nil(body.Event.SharedTrainParticipants, "Expected shared_train_participants nil") + a.NotZero(body.Event.AllTimeHighLevel, "Expected all_time_high_level to be set for begin event") + a.NotZero(body.Event.AllTimeHighTotal, "Expected all_time_high_total to be set for begin event") + a.NotNil(body.Event.Progress, "Expected progress to be set for begin event") + + params = events.MockEventParameters{ + ToUserID: toUser, + Transport: models.TransportWebhook, + Trigger: "hype-train-progress", + SubscriptionStatus: "enabled", + } + + r, err = Event{}.GenerateEvent(params) + a.Nil(err) + + body = models.HypeTrainEventSubResponseV2{} + err = json.Unmarshal(r.JSON, &body) + a.Nil(err) + + a.Equal("channel.hype_train.progress", body.Subscription.Type, "Expected event type %v, got %v", "channel.hype_train.progress", body.Subscription.Type) + a.Equal(toUser, body.Event.BroadcasterUserID, "Expected to user %v, got %v", toUser, body.Event.BroadcasterUserID) + a.Equal("2", body.Subscription.Version) + a.NotNil(body.Event.Progress, "Expected progress to be set for progress event") + + params = events.MockEventParameters{ + ToUserID: toUser, + Transport: models.TransportWebhook, + Trigger: "hype-train-end", + SubscriptionStatus: "enabled", + } + + r, err = Event{}.GenerateEvent(params) + a.Nil(err) + + body = models.HypeTrainEventSubResponseV2{} + err = json.Unmarshal(r.JSON, &body) + a.Nil(err) + + a.Equal("channel.hype_train.end", body.Subscription.Type, "Expected event type %v, got %v", "channel.hype_train.end", body.Subscription.Type) + a.Equal(toUser, body.Event.BroadcasterUserID, "Expected to user %v, got %v", toUser, body.Event.BroadcasterUserID) + a.Equal("2", body.Subscription.Version) + a.Nil(body.Event.Progress, "Expected progress to be nil for end event") + a.NotEmpty(body.Event.EndedAtTimestamp, "Expected ended_at to be set for end event") + a.NotEmpty(body.Event.CooldownEndsAtTimestamp, "Expected cooldown_ends_at to be set for end event") +} + +func TestWebSocketTransport(t *testing.T) { + a := test_setup.SetupTestEnv(t) + + params := events.MockEventParameters{ + ToUserID: toUser, + Transport: models.TransportWebSocket, + Trigger: "hype-train-begin", + SubscriptionStatus: "enabled", + } + + r, err := Event{}.GenerateEvent(params) + a.Nil(err) + + var body models.HypeTrainEventSubResponseV2 + err = json.Unmarshal(r.JSON, &body) + a.Nil(err) + + a.Equal("channel.hype_train.begin", body.Subscription.Type) + a.Equal("2", body.Subscription.Version) +} + +func TestFakeTransport(t *testing.T) { + a := test_setup.SetupTestEnv(t) + + params := events.MockEventParameters{ + ToUserID: toUser, + Transport: "fake_transport", + Trigger: "hype-train-progress", + } + + r, err := Event{}.GenerateEvent(params) + a.Nil(err) + a.Empty(r) +} +func TestValidTrigger(t *testing.T) { + a := test_setup.SetupTestEnv(t) + + r := Event{}.ValidTrigger("hype-train-begin") + a.Equal(true, r) + + r = Event{}.ValidTrigger("hype-train-progress") + a.Equal(true, r) + + r = Event{}.ValidTrigger("hype-train-end") + a.Equal(true, r) + +} + +func TestValidTransport(t *testing.T) { + a := test_setup.SetupTestEnv(t) + + r := Event{}.ValidTransport(models.TransportWebhook) + a.Equal(true, r) + + r = Event{}.ValidTransport(models.TransportWebSocket) + a.Equal(true, r) +} + +func TestGetTopic(t *testing.T) { + a := test_setup.SetupTestEnv(t) + + r := Event{}.GetTopic(models.TransportWebhook, "hype-train-progress") + a.Equal("channel.hype_train.progress", r, "Expected %v, got %v", "channel.hype_train.progress", r) +} diff --git a/internal/events/types/types.go b/internal/events/types/types.go index 77222aa..e6658fa 100644 --- a/internal/events/types/types.go +++ b/internal/events/types/types.go @@ -25,6 +25,7 @@ import ( "github.com/twitchdev/twitch-cli/internal/events/types/gift" "github.com/twitchdev/twitch-cli/internal/events/types/goal" "github.com/twitchdev/twitch-cli/internal/events/types/hype_train" + "github.com/twitchdev/twitch-cli/internal/events/types/hype_train_v2" "github.com/twitchdev/twitch-cli/internal/events/types/moderator_change" "github.com/twitchdev/twitch-cli/internal/events/types/poll" "github.com/twitchdev/twitch-cli/internal/events/types/prediction" @@ -57,6 +58,7 @@ func AllEvents() []events.MockEvent { gift.Event{}, goal.Event{}, hype_train.Event{}, + hype_train_v2.Event{}, moderator_change.Event{}, poll.Event{}, prediction.Event{}, @@ -160,6 +162,9 @@ func GetByTriggerAndTransportAndVersion(trigger string, transport string, versio // This does not include any "beta" events, just old production versions func RemovedEvents() map[string]string { return map[string]string{ - "channel.follow": "1", + "channel.follow": "1", + "channel.hype_train.begin": "1", + "channel.hype_train.end": "1", + "channel.hype_train.progress": "1", } } diff --git a/internal/models/hype_train.go b/internal/models/hype_train.go index c1fcfe1..8f228e9 100644 --- a/internal/models/hype_train.go +++ b/internal/models/hype_train.go @@ -31,3 +31,35 @@ type HypeTrainEventSubEvent struct { EndedAtTimestamp string `json:"ended_at,omitempty"` CooldownEndsAtTimestamp string `json:"cooldown_ends_at,omitempty"` } + +type SharedTrainParticipant struct { + BroadcasterUserID string `json:"broadcaster_user_id"` + BroadcasterUserLogin string `json:"broadcaster_user_login"` + BroadcasterUserName string `json:"broadcaster_user_name"` +} + +type HypeTrainEventSubEventV2 struct { + ID string `json:"id"` + BroadcasterUserID string `json:"broadcaster_user_id"` + BroadcasterUserLogin string `json:"broadcaster_user_login"` + BroadcasterUserName string `json:"broadcaster_user_name"` + Total int64 `json:"total"` + Progress *int64 `json:"progress,omitempty"` + Goal int64 `json:"goal,omitempty"` + TopContributions []ContributionData `json:"top_contributions"` + SharedTrainParticipants *[]SharedTrainParticipant `json:"shared_train_participants"` + Level int64 `json:"level,omitempty"` + StartedAtTimestamp string `json:"started_at,omitempty"` + ExpiresAtTimestamp string `json:"expires_at,omitempty"` + EndedAtTimestamp string `json:"ended_at,omitempty"` + CooldownEndsAtTimestamp string `json:"cooldown_ends_at,omitempty"` + IsSharedTrain bool `json:"is_shared_train"` + Type string `json:"type"` + AllTimeHighLevel int64 `json:"all_time_high_level,omitempty"` + AllTimeHighTotal int64 `json:"all_time_high_total,omitempty"` +} + +type HypeTrainEventSubResponseV2 struct { + Subscription EventsubSubscription `json:"subscription"` + Event HypeTrainEventSubEventV2 `json:"event"` +}