[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 <knut@ahlers.me>
This commit is contained in:
Knut Ahlers 2024-04-06 18:40:18 +02:00
parent acf96c31ad
commit 7737d939f4
Signed by: luzifer
SSH key fingerprint: SHA256:/xtE5lCgiRDQr8SLxHMS92ZBlACmATUmF1crK16Ks4E
6 changed files with 180 additions and 43 deletions

View file

@ -273,6 +273,29 @@ Fields:
- `plan` - The sub-plan they are using (`1000` = T1, `2000` = T2, `3000` = T3, `Prime`) - `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 - `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` ## `timeout`
Moderator action caused a user to be timed out from chat. Moderator action caused a user to be timed out from chat.

View file

@ -45,6 +45,8 @@ var (
eventTypeSubgift = ptrStr("subgift") eventTypeSubgift = ptrStr("subgift")
eventTypeSubmysterygift = ptrStr("submysterygift") eventTypeSubmysterygift = ptrStr("submysterygift")
eventTypeSub = ptrStr("sub") eventTypeSub = ptrStr("sub")
eventTypeSusUserMessage = ptrStr("sus_user_message")
eventTypeSusUserUpdate = ptrStr("sus_user_update")
eventTypeTimeout = ptrStr("timeout") eventTypeTimeout = ptrStr("timeout")
eventTypeWatchStreak = ptrStr("watch_streak") eventTypeWatchStreak = ptrStr("watch_streak")
eventTypeWhisper = ptrStr("whisper") eventTypeWhisper = ptrStr("whisper")
@ -83,6 +85,8 @@ var (
eventTypeSub, eventTypeSub,
eventTypeSubgift, eventTypeSubgift,
eventTypeSubmysterygift, eventTypeSubmysterygift,
eventTypeSusUserMessage,
eventTypeSusUserUpdate,
eventTypeTimeout, eventTypeTimeout,
eventTypeWatchStreak, eventTypeWatchStreak,
eventTypeWhisper, eventTypeWhisper,

View file

@ -29,6 +29,8 @@ const (
EventSubEventTypeChannelPollBegin = "channel.poll.begin" EventSubEventTypeChannelPollBegin = "channel.poll.begin"
EventSubEventTypeChannelPollEnd = "channel.poll.end" EventSubEventTypeChannelPollEnd = "channel.poll.end"
EventSubEventTypeChannelPollProgress = "channel.poll.progress" EventSubEventTypeChannelPollProgress = "channel.poll.progress"
EventSubEventTypeChannelSuspiciousUserMessage = "channel.suspicious_user.message"
EventSubEventTypeChannelSuspiciousUserUpdate = "channel.suspicious_user.update"
EventSubEventTypeStreamOffline = "stream.offline" EventSubEventTypeStreamOffline = "stream.offline"
EventSubEventTypeStreamOnline = "stream.online" EventSubEventTypeStreamOnline = "stream.online"
EventSubEventTypeUserAuthorizationRevoke = "user.authorization.revoke" EventSubEventTypeUserAuthorizationRevoke = "user.authorization.revoke"
@ -235,6 +237,53 @@ type (
StartedAt time.Time `json:"started_at"` 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 // EventSubEventUserAuthorizationRevoke contains the payload for an
// authorization revoke event // authorization revoke event
EventSubEventUserAuthorizationRevoke struct { EventSubEventUserAuthorizationRevoke struct {

View file

@ -3,35 +3,36 @@ package twitch
// Collection of known API scopes // Collection of known API scopes
const ( const (
// API Scopes // API Scopes
ScopeChannelBot = "channel:bot" ScopeChannelBot = "channel:bot"
ScopeChannelEditCommercial = "channel:edit:commercial" ScopeChannelEditCommercial = "channel:edit:commercial"
ScopeChannelManageAds = "channel:manage:ads" ScopeChannelManageAds = "channel:manage:ads"
ScopeChannelManageBroadcast = "channel:manage:broadcast" ScopeChannelManageBroadcast = "channel:manage:broadcast"
ScopeChannelManageModerators = "channel:manage:moderators" ScopeChannelManageModerators = "channel:manage:moderators"
ScopeChannelManagePolls = "channel:manage:polls" ScopeChannelManagePolls = "channel:manage:polls"
ScopeChannelManagePredictions = "channel:manage:predictions" ScopeChannelManagePredictions = "channel:manage:predictions"
ScopeChannelManageRaids = "channel:manage:raids" ScopeChannelManageRaids = "channel:manage:raids"
ScopeChannelManageRedemptions = "channel:manage:redemptions" ScopeChannelManageRedemptions = "channel:manage:redemptions"
ScopeChannelManageVIPS = "channel:manage:vips" ScopeChannelManageVIPS = "channel:manage:vips"
ScopeChannelManageWhispers = "user:manage:whispers" ScopeChannelManageWhispers = "user:manage:whispers"
ScopeChannelReadAds = "channel:read:ads" ScopeChannelReadAds = "channel:read:ads"
ScopeChannelReadHypetrain = "channel:read:hype_train" ScopeChannelReadHypetrain = "channel:read:hype_train"
ScopeChannelReadPolls = "channel:read:polls" ScopeChannelReadPolls = "channel:read:polls"
ScopeChannelReadRedemptions = "channel:read:redemptions" ScopeChannelReadRedemptions = "channel:read:redemptions"
ScopeChannelReadSubscriptions = "channel:read:subscriptions" ScopeChannelReadSubscriptions = "channel:read:subscriptions"
ScopeClipsEdit = "clips:edit" ScopeClipsEdit = "clips:edit"
ScopeModeratorManageAnnoucements = "moderator:manage:announcements" ScopeModeratorManageAnnoucements = "moderator:manage:announcements"
ScopeModeratorManageBannedUsers = "moderator:manage:banned_users" ScopeModeratorManageBannedUsers = "moderator:manage:banned_users"
ScopeModeratorManageChatMessages = "moderator:manage:chat_messages" ScopeModeratorManageChatMessages = "moderator:manage:chat_messages"
ScopeModeratorManageChatSettings = "moderator:manage:chat_settings" ScopeModeratorManageChatSettings = "moderator:manage:chat_settings"
ScopeModeratorManageShieldMode = "moderator:manage:shield_mode" ScopeModeratorManageShieldMode = "moderator:manage:shield_mode"
ScopeModeratorManageShoutouts = "moderator:manage:shoutouts" ScopeModeratorManageShoutouts = "moderator:manage:shoutouts"
ScopeModeratorReadFollowers = "moderator:read:followers" ScopeModeratorReadFollowers = "moderator:read:followers"
ScopeModeratorReadShoutouts = "moderator:read:shoutouts" ScopeModeratorReadShoutouts = "moderator:read:shoutouts"
ScopeUserBot = "user:bot" ScopeModeratorReadSuspiciousUsers = "moderator:read:suspicious_users"
ScopeUserManageChatColor = "user:manage:chat_color" ScopeUserBot = "user:bot"
ScopeUserManageWhispers = "user:manage:whispers" ScopeUserManageChatColor = "user:manage:chat_color"
ScopeUserReadChat = "user:read:chat" ScopeUserManageWhispers = "user:manage:whispers"
ScopeUserReadChat = "user:read:chat"
// Deprecated v5 scope but used in chat // Deprecated v5 scope but used in chat
ScopeV5ChannelEditor = "channel_editor" ScopeV5ChannelEditor = "channel_editor"

View file

@ -4,20 +4,21 @@ import "github.com/Luzifer/twitch-bot/v3/pkg/twitch"
var ( var (
channelExtendedScopes = map[string]string{ channelExtendedScopes = map[string]string{
twitch.ScopeChannelEditCommercial: "run commercial", twitch.ScopeChannelEditCommercial: "run commercial",
twitch.ScopeChannelManageBroadcast: "modify category / title", twitch.ScopeChannelManageBroadcast: "modify category / title",
twitch.ScopeChannelManagePolls: "manage polls", twitch.ScopeChannelManagePolls: "manage polls",
twitch.ScopeChannelManagePredictions: "manage predictions", twitch.ScopeChannelManagePredictions: "manage predictions",
twitch.ScopeChannelManageRaids: "start raids", twitch.ScopeChannelManageRaids: "start raids",
twitch.ScopeChannelManageVIPS: "manage VIPs", twitch.ScopeChannelManageVIPS: "manage VIPs",
twitch.ScopeChannelReadAds: "see when an ad-break starts", twitch.ScopeChannelReadAds: "see when an ad-break starts",
twitch.ScopeChannelReadHypetrain: "see Hype-Train events", twitch.ScopeChannelReadHypetrain: "see Hype-Train events",
twitch.ScopeChannelReadRedemptions: "see channel-point redemptions", twitch.ScopeChannelReadRedemptions: "see channel-point redemptions",
twitch.ScopeChannelReadSubscriptions: "see subscribed users / sub count / points", twitch.ScopeChannelReadSubscriptions: "see subscribed users / sub count / points",
twitch.ScopeClipsEdit: "create clips on behalf of this user", twitch.ScopeClipsEdit: "create clips on behalf of this user",
twitch.ScopeModeratorReadFollowers: "see who follows this channel", twitch.ScopeModeratorReadFollowers: "see who follows this channel",
twitch.ScopeModeratorReadShoutouts: "see shoutouts created / received", twitch.ScopeModeratorReadShoutouts: "see shoutouts created / received",
twitch.ScopeUserManageWhispers: "send whispers on behalf of this user", twitch.ScopeModeratorReadSuspiciousUsers: "see users marked suspicious / restricted",
twitch.ScopeUserManageWhispers: "send whispers on behalf of this user",
} }
botDefaultScopes = []string{ botDefaultScopes = []string{

View file

@ -232,6 +232,22 @@ func (t *twitchWatcher) getTopicRegistrations(userID string) []topicRegistration
Hook: t.handleEventSubStreamOnOff(true), Hook: t.handleEventSubStreamOnOff(true),
Optional: 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 { func (t *twitchWatcher) updateChannelFromAPI(channel string) error {
t.lock.Lock() t.lock.Lock()
defer t.lock.Unlock() defer t.lock.Unlock()