From f2ac1acb17899f2e205c0a7b6933fd86cbaf42fc Mon Sep 17 00:00:00 2001 From: Knut Ahlers Date: Thu, 29 Feb 2024 19:00:44 +0100 Subject: [PATCH] [core] Add support for Hype-Train events Signed-off-by: Knut Ahlers --- docs/content/configuration/events.md | 13 +++++++- events.go | 3 ++ pkg/twitch/eventsub.go | 35 +++++++++++++++++++ pkg/twitch/scopes.go | 1 + scopes.go | 1 + twitchWatcher.go | 50 ++++++++++++++++++++++++++++ 6 files changed, 102 insertions(+), 1 deletion(-) diff --git a/docs/content/configuration/events.md b/docs/content/configuration/events.md index b163f09..4f09160 100644 --- a/docs/content/configuration/events.md +++ b/docs/content/configuration/events.md @@ -91,6 +91,17 @@ Fields: - `gifter` - The login-name of the user who gifted the subscription - `username` - The login-name of the user who upgraded their subscription +## `hypetrain_begin`, `hypetrain_end`, `hypetrain_progress` + +An Hype-Train has begun, ended or progressed in the given channel. + +Fields: + +- `channel` - The channel the event occurred in +- `level` - The current level of the Hype-Train +- `levelProgress` - Percentage of reached "points" in the current level to complete the level (not available on `hypetrain_end`) +- `event` - Raw Hype-Train event, see schema in [`pkg/twitch/eventsub.go#L92`](https://github.com/Luzifer/twitch-bot/blob/master/pkg/twitch/eventsub.go#L121) + ## `join` User joined the channel-chat. This is **NOT** an indicator they are viewing, the event is **NOT** reliably sent when the user really joined the chat. The event will be sent with some delay after they join the chat and is sometimes repeated multiple times during their stay. So **DO NOT** use this to greet users! @@ -152,7 +163,7 @@ A poll was started / was ended / had changes in the given channel. Fields: - `channel` - The channel the event occurred in -- `poll` - The poll object describing the poll, see schema in [`pkg/twitch/eventsub.go#L92`](https://github.com/Luzifer/twitch-bot/blob/master/pkg/twitch/eventsub.go#L92) +- `poll` - The poll object describing the poll, see schema in [`pkg/twitch/eventsub.go#L92`](https://github.com/Luzifer/twitch-bot/blob/master/pkg/twitch/eventsub.go#L152) - `status` - The status of the poll (one of `completed`, `terminated` or `archived`) - only available in `poll_end` - `title` - The title of the poll the event was generated for diff --git a/events.go b/events.go index 07b576b..5c02a2c 100644 --- a/events.go +++ b/events.go @@ -26,6 +26,9 @@ var ( eventTypeDelete = ptrStr("delete") eventTypeFollow = ptrStr("follow") eventTypeGiftPaidUpgrade = ptrStr("giftpaidupgrade") + eventTypeHypetrainBegin = ptrStr("hypetrain_begin") + eventTypeHypetrainEnd = ptrStr("hypetrain_end") + eventTypeHypetrainProgress = ptrStr("hypetrain_progress") eventTypeJoin = ptrStr("join") eventKoFiDonation = ptrStr("kofi_donation") eventTypeOutboundRaid = ptrStr("outbound_raid") diff --git a/pkg/twitch/eventsub.go b/pkg/twitch/eventsub.go index 7252405..fe7bd11 100644 --- a/pkg/twitch/eventsub.go +++ b/pkg/twitch/eventsub.go @@ -17,6 +17,9 @@ const ( EventSubEventTypeChannelAdBreakBegin = "channel.ad_break.begin" EventSubEventTypeChannelFollow = "channel.follow" EventSubEventTypeChannelPointCustomRewardRedemptionAdd = "channel.channel_points_custom_reward_redemption.add" + EventSubEventTypeChannelHypetrainBegin = "channel.hype_train.begin" + EventSubEventTypeChannelHypetrainProgress = "channel.hype_train.progress" + EventSubEventTypeChannelHypetrainEnd = "channel.hype_train.end" EventSubEventTypeChannelRaid = "channel.raid" EventSubEventTypeChannelShoutoutCreate = "channel.shoutout.create" EventSubEventTypeChannelShoutoutReceive = "channel.shoutout.receive" @@ -112,6 +115,38 @@ type ( FollowedAt time.Time `json:"followed_at"` } + // EventSubEventHypetrain contains the payload for all three (begin, + // progress and end) hypetrain events. Certain fields are not + // available at all event types + EventSubEventHypetrain 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"` // Only Beginn, Progress + Goal int64 `json:"goal"` // Only Beginn, Progress + TopContributions []struct { + UserID string `json:"user_id"` + UserLogin string `json:"user_login"` + UserName string `json:"user_name"` + Type string `json:"type"` + Total int64 `json:"total"` + } `json:"top_contributions"` + LastContribution *struct { // Only Begin, Progress + UserID string `json:"user_id"` + UserLogin string `json:"user_login"` + UserName string `json:"user_name"` + Type string `json:"type"` + Total int64 `json:"total"` + } `json:"last_contribution,omitempty"` + StartedAt time.Time `json:"started_at"` + ExpiresAt *time.Time `json:"expires_at,omitempty"` // Only Begin, Progress + EndedAt *time.Time `json:"ended_at,omitempty"` // Only End + CooldownEndsAt *time.Time `json:"cooldown_ends_at,omitempty"` // Only End + } + // EventSubEventPoll contains the payload for a poll change event // (not all fields are present in all poll events, see docs!) EventSubEventPoll struct { diff --git a/pkg/twitch/scopes.go b/pkg/twitch/scopes.go index e1d1197..aec58a9 100644 --- a/pkg/twitch/scopes.go +++ b/pkg/twitch/scopes.go @@ -15,6 +15,7 @@ const ( ScopeChannelManageVIPS = "channel:manage:vips" ScopeChannelManageWhispers = "user:manage:whispers" ScopeChannelReadAds = "channel:read:ads" + ScopeChannelReadHypetrain = "channel:read:hype_train" ScopeChannelReadPolls = "channel:read:polls" ScopeChannelReadRedemptions = "channel:read:redemptions" ScopeChannelReadSubscriptions = "channel:read:subscriptions" diff --git a/scopes.go b/scopes.go index a89a8d4..f0886af 100644 --- a/scopes.go +++ b/scopes.go @@ -11,6 +11,7 @@ var ( twitch.ScopeChannelManageRaids: "start raids", twitch.ScopeChannelManageVIPS: "manage VIPs", twitch.ScopeChannelReadAds: "see when an ad-break starts", + twitch.ScopeChannelReadHypetrain: "see Hype-Train events", twitch.ScopeChannelReadRedemptions: "see channel-point redemptions", twitch.ScopeChannelReadSubscriptions: "see subscribed users / sub count / points", twitch.ScopeClipsEdit: "create clips on behalf of this user", diff --git a/twitchWatcher.go b/twitchWatcher.go index 1a881fd..016f4d3 100644 --- a/twitchWatcher.go +++ b/twitchWatcher.go @@ -112,6 +112,7 @@ func (t *twitchWatcher) RemoveChannel(channel string) error { return nil } +//nolint:funlen // Just a collection of topics func (t *twitchWatcher) getTopicRegistrations(userID string) []topicRegistration { return []topicRegistration{ { @@ -130,6 +131,30 @@ func (t *twitchWatcher) getTopicRegistrations(userID string) []topicRegistration Hook: t.handleEventSubChannelFollow, Optional: true, }, + { + Topic: twitch.EventSubEventTypeChannelHypetrainBegin, + Version: twitch.EventSubTopicVersion1, + Condition: twitch.EventSubCondition{BroadcasterUserID: userID}, + RequiredScopes: []string{twitch.ScopeChannelReadHypetrain}, + Hook: t.handleEventSubHypetrainEvent(eventTypeHypetrainBegin), + Optional: true, + }, + { + Topic: twitch.EventSubEventTypeChannelHypetrainEnd, + Version: twitch.EventSubTopicVersion1, + Condition: twitch.EventSubCondition{BroadcasterUserID: userID}, + RequiredScopes: []string{twitch.ScopeChannelReadHypetrain}, + Hook: t.handleEventSubHypetrainEvent(eventTypeHypetrainEnd), + Optional: true, + }, + { + Topic: twitch.EventSubEventTypeChannelHypetrainProgress, + Version: twitch.EventSubTopicVersion1, + Condition: twitch.EventSubCondition{BroadcasterUserID: userID}, + RequiredScopes: []string{twitch.ScopeChannelReadHypetrain}, + Hook: t.handleEventSubHypetrainEvent(eventTypeHypetrainProgress), + Optional: true, + }, { Topic: twitch.EventSubEventTypeChannelPointCustomRewardRedemptionAdd, Condition: twitch.EventSubCondition{BroadcasterUserID: userID}, @@ -338,6 +363,31 @@ func (*twitchWatcher) handleEventSubChannelPollChange(event *string) func(json.R } } +func (*twitchWatcher) handleEventSubHypetrainEvent(eventType *string) func(json.RawMessage) error { + return func(m json.RawMessage) error { + var payload twitch.EventSubEventHypetrain + if err := json.Unmarshal(m, &payload); err != nil { + return errors.Wrap(err, "unmarshalling event") + } + + fields := plugins.FieldCollectionFromData(map[string]any{ + "channel": "#" + payload.BroadcasterUserLogin, + "level": payload.Level, + }) + + if payload.Goal > 0 { + fields.Set("levelProgress", float64(payload.Progress)/float64(payload.Goal)) + } + + log.WithFields(log.Fields(fields.Data())).Info("Hypetrain event") + + fields.Set("event", payload) + go handleMessage(ircHdl.Client(), nil, eventType, fields) + + return nil + } +} + func (*twitchWatcher) handleEventSubShoutoutCreated(m json.RawMessage) error { var payload twitch.EventSubEventShoutoutCreated if err := json.Unmarshal(m, &payload); err != nil {