From 7737d939f43dec34045ebf18b486106aa73da9de Mon Sep 17 00:00:00 2001 From: Knut Ahlers Date: Sat, 6 Apr 2024 18:40:18 +0200 Subject: [PATCH] [eventsub] Add support for suspicious user events - User status is updated - User sent a message while having sus-user status Signed-off-by: Knut Ahlers --- docs/content/configuration/events.md | 23 +++++++++++ events.go | 4 ++ pkg/twitch/eventsub.go | 49 +++++++++++++++++++++++ pkg/twitch/scopes.go | 59 ++++++++++++++-------------- scopes.go | 29 +++++++------- twitchWatcher.go | 59 ++++++++++++++++++++++++++++ 6 files changed, 180 insertions(+), 43 deletions(-) diff --git a/docs/content/configuration/events.md b/docs/content/configuration/events.md index d8f5da7..d4b021a 100644 --- a/docs/content/configuration/events.md +++ b/docs/content/configuration/events.md @@ -273,6 +273,29 @@ Fields: - `plan` - The sub-plan they are using (`1000` = T1, `2000` = T2, `3000` = T3, `Prime`) - `username` - The login-name of the user who gifted the subscription +## `sus_user_message` + +A suspicious (monitored / restricted) user sent a message in the given channel + +- `ban_evasion` _string_ - Status of the ban-evasion detection: `unknown`, `possible`, `likely` +- `channel` _string_ - The channel in which the event occurred +- `message` _string_ - The message the user sent in plain text +- `shared_ban_channels` _[]string_ - IDs of channels with shared ban-info in which the user is also banned +- `status` _string_ - Restriction status: `active_monitoring`, `restricted` +- `user_id` _string_ - ID of the user sending the message +- `user_type` _[]string_ - How the user ended being on the naughty-list: `manually_added`, `ban_evader_detector`, or `shared_channel_ban` +- `username` _string_ - The login-name of the user sending the message + +## `sus_user_update` + +The status of suspicious user was changed by a moderator + +- `channel` _string_ - The channel in which the event occurred +- `moderator` _string_ - The login-name of the moderator changing the status +- `status` _string_ - Restriction status: `no_treatment`, `active_monitoring`, `restricted` +- `user_id` _string_ - ID of the suspicious user +- `username` _string_ - Login-name of the suspicious user + ## `timeout` Moderator action caused a user to be timed out from chat. diff --git a/events.go b/events.go index f876baa..975a5cf 100644 --- a/events.go +++ b/events.go @@ -45,6 +45,8 @@ var ( eventTypeSubgift = ptrStr("subgift") eventTypeSubmysterygift = ptrStr("submysterygift") eventTypeSub = ptrStr("sub") + eventTypeSusUserMessage = ptrStr("sus_user_message") + eventTypeSusUserUpdate = ptrStr("sus_user_update") eventTypeTimeout = ptrStr("timeout") eventTypeWatchStreak = ptrStr("watch_streak") eventTypeWhisper = ptrStr("whisper") @@ -83,6 +85,8 @@ var ( eventTypeSub, eventTypeSubgift, eventTypeSubmysterygift, + eventTypeSusUserMessage, + eventTypeSusUserUpdate, eventTypeTimeout, eventTypeWatchStreak, eventTypeWhisper, diff --git a/pkg/twitch/eventsub.go b/pkg/twitch/eventsub.go index 0f1b8b7..de2684b 100644 --- a/pkg/twitch/eventsub.go +++ b/pkg/twitch/eventsub.go @@ -29,6 +29,8 @@ const ( EventSubEventTypeChannelPollBegin = "channel.poll.begin" EventSubEventTypeChannelPollEnd = "channel.poll.end" EventSubEventTypeChannelPollProgress = "channel.poll.progress" + EventSubEventTypeChannelSuspiciousUserMessage = "channel.suspicious_user.message" + EventSubEventTypeChannelSuspiciousUserUpdate = "channel.suspicious_user.update" EventSubEventTypeStreamOffline = "stream.offline" EventSubEventTypeStreamOnline = "stream.online" EventSubEventTypeUserAuthorizationRevoke = "user.authorization.revoke" @@ -235,6 +237,53 @@ type ( StartedAt time.Time `json:"started_at"` } + // EventSubEventSuspiciousUserMessage contains the payload for a + // channel.suspicious_user.message + EventSubEventSuspiciousUserMessage struct { + BroadcasterUserID string `json:"broadcaster_user_id"` + BroadcasterUserName string `json:"broadcaster_user_name"` + BroadcasterUserLogin string `json:"broadcaster_user_login"` + UserID string `json:"user_id"` + UserName string `json:"user_name"` + UserLogin string `json:"user_login"` + LowTrustStatus string `json:"low_trust_status"` // Can be the following: "none", "active_monitoring", or "restricted" + SharedBanChannelIDs []string `json:"shared_ban_channel_ids"` + Types []string `json:"types"` // can be "manual", "ban_evader_detector", or "shared_channel_ban" + BanEvasionEvaluation string `json:"ban_evasion_evaluation"` // can be "unknown", "possible", or "likely" + Message struct { + MessageID string `json:"message_id"` + Text string `json:"text"` + Fragments []struct { + Type string `json:"type"` + Text string `json:"text"` + Cheermote struct { + Prefix string `json:"prefix"` + Bits int `json:"bits"` + Tier int `json:"tier"` + } `json:"Cheermote"` + Emote struct { + ID string `json:"id"` + EmoteSetID string `json:"emote_set_id"` + } `json:"emote"` + } `json:"fragments"` + } `json:"message"` + } + + // EventSubEventSuspiciousUserUpdated contains the payload for a + // channel.suspicious_user.update + EventSubEventSuspiciousUserUpdated struct { + BroadcasterUserID string `json:"broadcaster_user_id"` + BroadcasterUserName string `json:"broadcaster_user_name"` + BroadcasterUserLogin string `json:"broadcaster_user_login"` + ModeratorUserID string `json:"moderator_user_id"` + ModeratorUserName string `json:"moderator_user_name"` + ModeratorUserLogin string `json:"moderator_user_login"` + UserID string `json:"user_id"` + UserName string `json:"user_name"` + UserLogin string `json:"user_login"` + LowTrustStatus string `json:"low_trust_status"` // Can be the following: "none", "active_monitoring", or "restricted" + } + // EventSubEventUserAuthorizationRevoke contains the payload for an // authorization revoke event EventSubEventUserAuthorizationRevoke struct { diff --git a/pkg/twitch/scopes.go b/pkg/twitch/scopes.go index aec58a9..7cf3a2f 100644 --- a/pkg/twitch/scopes.go +++ b/pkg/twitch/scopes.go @@ -3,35 +3,36 @@ package twitch // Collection of known API scopes const ( // API Scopes - ScopeChannelBot = "channel:bot" - ScopeChannelEditCommercial = "channel:edit:commercial" - ScopeChannelManageAds = "channel:manage:ads" - ScopeChannelManageBroadcast = "channel:manage:broadcast" - ScopeChannelManageModerators = "channel:manage:moderators" - ScopeChannelManagePolls = "channel:manage:polls" - ScopeChannelManagePredictions = "channel:manage:predictions" - ScopeChannelManageRaids = "channel:manage:raids" - ScopeChannelManageRedemptions = "channel:manage:redemptions" - 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" - ScopeClipsEdit = "clips:edit" - ScopeModeratorManageAnnoucements = "moderator:manage:announcements" - ScopeModeratorManageBannedUsers = "moderator:manage:banned_users" - ScopeModeratorManageChatMessages = "moderator:manage:chat_messages" - ScopeModeratorManageChatSettings = "moderator:manage:chat_settings" - ScopeModeratorManageShieldMode = "moderator:manage:shield_mode" - ScopeModeratorManageShoutouts = "moderator:manage:shoutouts" - ScopeModeratorReadFollowers = "moderator:read:followers" - ScopeModeratorReadShoutouts = "moderator:read:shoutouts" - ScopeUserBot = "user:bot" - ScopeUserManageChatColor = "user:manage:chat_color" - ScopeUserManageWhispers = "user:manage:whispers" - ScopeUserReadChat = "user:read:chat" + ScopeChannelBot = "channel:bot" + ScopeChannelEditCommercial = "channel:edit:commercial" + ScopeChannelManageAds = "channel:manage:ads" + ScopeChannelManageBroadcast = "channel:manage:broadcast" + ScopeChannelManageModerators = "channel:manage:moderators" + ScopeChannelManagePolls = "channel:manage:polls" + ScopeChannelManagePredictions = "channel:manage:predictions" + ScopeChannelManageRaids = "channel:manage:raids" + ScopeChannelManageRedemptions = "channel:manage:redemptions" + 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" + ScopeClipsEdit = "clips:edit" + ScopeModeratorManageAnnoucements = "moderator:manage:announcements" + ScopeModeratorManageBannedUsers = "moderator:manage:banned_users" + ScopeModeratorManageChatMessages = "moderator:manage:chat_messages" + ScopeModeratorManageChatSettings = "moderator:manage:chat_settings" + ScopeModeratorManageShieldMode = "moderator:manage:shield_mode" + ScopeModeratorManageShoutouts = "moderator:manage:shoutouts" + ScopeModeratorReadFollowers = "moderator:read:followers" + ScopeModeratorReadShoutouts = "moderator:read:shoutouts" + ScopeModeratorReadSuspiciousUsers = "moderator:read:suspicious_users" + ScopeUserBot = "user:bot" + ScopeUserManageChatColor = "user:manage:chat_color" + ScopeUserManageWhispers = "user:manage:whispers" + ScopeUserReadChat = "user:read:chat" // Deprecated v5 scope but used in chat ScopeV5ChannelEditor = "channel_editor" diff --git a/scopes.go b/scopes.go index f0886af..fba2740 100644 --- a/scopes.go +++ b/scopes.go @@ -4,20 +4,21 @@ import "github.com/Luzifer/twitch-bot/v3/pkg/twitch" var ( channelExtendedScopes = map[string]string{ - twitch.ScopeChannelEditCommercial: "run commercial", - twitch.ScopeChannelManageBroadcast: "modify category / title", - twitch.ScopeChannelManagePolls: "manage polls", - twitch.ScopeChannelManagePredictions: "manage predictions", - 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", - twitch.ScopeModeratorReadFollowers: "see who follows this channel", - twitch.ScopeModeratorReadShoutouts: "see shoutouts created / received", - twitch.ScopeUserManageWhispers: "send whispers on behalf of this user", + twitch.ScopeChannelEditCommercial: "run commercial", + twitch.ScopeChannelManageBroadcast: "modify category / title", + twitch.ScopeChannelManagePolls: "manage polls", + twitch.ScopeChannelManagePredictions: "manage predictions", + 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", + twitch.ScopeModeratorReadFollowers: "see who follows this channel", + twitch.ScopeModeratorReadShoutouts: "see shoutouts created / received", + twitch.ScopeModeratorReadSuspiciousUsers: "see users marked suspicious / restricted", + twitch.ScopeUserManageWhispers: "send whispers on behalf of this user", } botDefaultScopes = []string{ diff --git a/twitchWatcher.go b/twitchWatcher.go index fb88c63..6dc6a56 100644 --- a/twitchWatcher.go +++ b/twitchWatcher.go @@ -232,6 +232,22 @@ func (t *twitchWatcher) getTopicRegistrations(userID string) []topicRegistration Hook: t.handleEventSubStreamOnOff(true), Optional: true, }, + { + Topic: twitch.EventSubEventTypeChannelSuspiciousUserMessage, + Version: twitch.EventSubTopicVersionBeta, + Condition: twitch.EventSubCondition{BroadcasterUserID: userID, ModeratorUserID: userID}, + RequiredScopes: []string{twitch.ScopeModeratorReadSuspiciousUsers}, + Hook: t.handleEventSubSusUserMessage, + Optional: true, + }, + { + Topic: twitch.EventSubEventTypeChannelSuspiciousUserUpdate, + Version: twitch.EventSubTopicVersionBeta, + Condition: twitch.EventSubCondition{BroadcasterUserID: userID, ModeratorUserID: userID}, + RequiredScopes: []string{twitch.ScopeModeratorReadSuspiciousUsers}, + Hook: t.handleEventSubSusUserUpdate, + Optional: true, + }, } } @@ -438,6 +454,49 @@ func (t *twitchWatcher) handleEventSubStreamOnOff(isOnline bool) func(json.RawMe } } +func (*twitchWatcher) handleEventSubSusUserMessage(m json.RawMessage) (err error) { + var payload twitch.EventSubEventSuspiciousUserMessage + if err := json.Unmarshal(m, &payload); err != nil { + return errors.Wrap(err, "unmarshalling event") + } + + fields := fieldcollection.FieldCollectionFromData(map[string]any{ + "ban_evasion": payload.BanEvasionEvaluation, + "channel": "#" + payload.BroadcasterUserLogin, + "message": payload.Message.Text, + "shared_ban_channels": payload.SharedBanChannelIDs, + "status": payload.LowTrustStatus, + "user_id": payload.UserID, + "user_type": payload.Types, + "username": payload.UserLogin, + }) + + log.WithFields(log.Fields(fields.Data())).Info("restricted user message") + go handleMessage(ircHdl.Client(), nil, eventTypeSusUserMessage, fields) + + return nil +} + +func (*twitchWatcher) handleEventSubSusUserUpdate(m json.RawMessage) (err error) { + var payload twitch.EventSubEventSuspiciousUserUpdated + if err := json.Unmarshal(m, &payload); err != nil { + return errors.Wrap(err, "unmarshalling event") + } + + fields := fieldcollection.FieldCollectionFromData(map[string]any{ + "channel": "#" + payload.BroadcasterUserLogin, + "moderator": payload.ModeratorUserLogin, + "status": payload.LowTrustStatus, + "user_id": payload.UserID, + "username": payload.UserLogin, + }) + + log.WithFields(log.Fields(fields.Data())).Info("user restriction updated") + go handleMessage(ircHdl.Client(), nil, eventTypeSusUserUpdate, fields) + + return nil +} + func (t *twitchWatcher) updateChannelFromAPI(channel string) error { t.lock.Lock() defer t.lock.Unlock()