2024-01-01 16:52:18 +00:00
// Package nuke contains a hateraid protection actor recording messages
// in all channels for a certain period of time being able to "nuke"
// their authors by regular expression based on past messages
2021-10-25 21:21:52 +00:00
package nuke
import (
"regexp"
"strings"
"sync"
"time"
"github.com/pkg/errors"
log "github.com/sirupsen/logrus"
2023-09-11 17:51:38 +00:00
"gopkg.in/irc.v4"
2021-11-25 22:48:16 +00:00
"github.com/Luzifer/go_helpers/v2/str"
2022-11-02 21:38:14 +00:00
"github.com/Luzifer/twitch-bot/v3/pkg/twitch"
"github.com/Luzifer/twitch-bot/v3/plugins"
2021-10-25 21:21:52 +00:00
)
const (
actorName = "nuke"
storeRetentionTime = 10 * time . Minute
)
var (
2022-10-25 16:47:30 +00:00
botTwitchClient * twitch . Client
formatMessage plugins . MsgFormatter
2021-10-25 21:21:52 +00:00
messageStore = map [ string ] [ ] * storedMessage { }
messageStoreLock sync . RWMutex
ptrStringDelete = func ( v string ) * string { return & v } ( "delete" )
2022-10-31 16:26:53 +00:00
ptrStringEmpty = func ( s string ) * string { return & s } ( "" )
2021-10-25 21:21:52 +00:00
ptrString10m = func ( v string ) * string { return & v } ( "10m" )
)
2024-01-01 16:52:18 +00:00
// Register provides the plugins.RegisterFunc
2021-10-25 21:21:52 +00:00
func Register ( args plugins . RegistrationArguments ) error {
2022-10-25 16:47:30 +00:00
botTwitchClient = args . GetTwitchClient ( )
2021-10-25 21:21:52 +00:00
formatMessage = args . FormatMessage
args . RegisterActor ( actorName , func ( ) plugins . Actor { return & actor { } } )
args . RegisterActorDocumentation ( plugins . ActionDocumentation {
Description : "Mass ban, delete, or timeout messages based on regex. Be sure you REALLY know what you do before using this! Used wrongly this will cause a lot of damage!" ,
Name : "Nuke Chat" ,
Type : actorName ,
Fields : [ ] plugins . ActionDocumentationField {
{
Default : "10m" ,
Description : "How long to scan into the past, template must yield a duration (max 10m)" ,
Key : "scan" ,
Name : "Scan-Duration" ,
Optional : true ,
SupportTemplate : true ,
Type : plugins . ActionDocumentationFieldTypeString ,
} ,
{
Default : "delete" ,
Description : "What action to take when message matches (delete / ban / <timeout duration>)" ,
Key : "action" ,
Name : "Match-Action" ,
Optional : true ,
SupportTemplate : true ,
Type : plugins . ActionDocumentationFieldTypeString ,
} ,
{
Default : "" ,
Description : "Regular expression (RE2) to select matching messages" ,
Key : "match" ,
Name : "Message-Match" ,
Optional : false ,
SupportTemplate : true ,
Type : plugins . ActionDocumentationFieldTypeString ,
} ,
} ,
} )
if _ , err := args . RegisterCron ( "@every 1m" , cleanupMessageStore ) ; err != nil {
return errors . Wrap ( err , "registering cleanup cron" )
}
if err := args . RegisterRawMessageHandler ( rawMessageHandler ) ; err != nil {
return errors . Wrap ( err , "registering raw message handler" )
}
return nil
}
func cleanupMessageStore ( ) {
messageStoreLock . Lock ( )
defer messageStoreLock . Unlock ( )
var storeDeletes [ ] string
for ch , msgs := range messageStore {
var idx int
for idx = 0 ; idx < len ( msgs ) ; idx ++ {
if time . Since ( msgs [ idx ] . Time ) < storeRetentionTime {
break
}
}
newMsgs := msgs [ idx : ]
if len ( newMsgs ) == 0 {
storeDeletes = append ( storeDeletes , ch )
continue
}
messageStore [ ch ] = newMsgs
log . WithFields ( log . Fields {
"channel" : ch ,
"stored_messages" : len ( newMsgs ) ,
} ) . Trace ( "[nuke] Cleared old stored messages" )
}
for _ , ch := range storeDeletes {
delete ( messageStore , ch )
log . WithFields ( log . Fields {
"channel" : ch ,
} ) . Trace ( "[nuke] Channel is no longer stored" )
}
}
func rawMessageHandler ( m * irc . Message ) error {
if m . Command != "PRIVMSG" {
// We care only about user written messages and drop the rest
return nil
}
messageStoreLock . Lock ( )
defer messageStoreLock . Unlock ( )
messageStore [ plugins . DeriveChannel ( m , nil ) ] = append (
messageStore [ plugins . DeriveChannel ( m , nil ) ] ,
& storedMessage { Time : time . Now ( ) , Msg : m } ,
)
return nil
}
type (
actor struct { }
storedMessage struct {
Time time . Time
Msg * irc . Message
}
)
2024-01-01 16:52:18 +00:00
func ( actor ) Execute ( _ * irc . Client , m * irc . Message , r * plugins . Rule , eventData * plugins . FieldCollection , attrs * plugins . FieldCollection ) ( preventCooldown bool , err error ) {
2021-10-25 21:21:52 +00:00
rawMatch , err := formatMessage ( attrs . MustString ( "match" , nil ) , m , r , eventData )
if err != nil {
return false , errors . Wrap ( err , "formatting match" )
}
match := regexp . MustCompile ( rawMatch )
rawScan , err := formatMessage ( attrs . MustString ( "scan" , ptrString10m ) , m , r , eventData )
if err != nil {
return false , errors . Wrap ( err , "formatting scan duration" )
}
scan , err := time . ParseDuration ( rawScan )
if err != nil {
return false , errors . Wrap ( err , "parsing scan duration" )
}
scanTime := time . Now ( ) . Add ( - scan )
2022-10-25 16:47:30 +00:00
var (
action actionFn
actionName string
)
2021-10-25 21:21:52 +00:00
rawAction , err := formatMessage ( attrs . MustString ( "action" , ptrStringDelete ) , m , r , eventData )
if err != nil {
return false , errors . Wrap ( err , "formatting action" )
}
switch rawAction {
case "delete" :
2022-10-25 16:47:30 +00:00
action = actionDelete
actionName = "delete $msgid"
2021-10-25 21:21:52 +00:00
case "ban" :
2022-10-25 16:47:30 +00:00
action = actionBan
actionName = "ban $user"
2021-10-25 21:21:52 +00:00
default :
to , err := time . ParseDuration ( rawAction )
if err != nil {
return false , errors . Wrap ( err , "parsing action duration" )
}
2022-10-25 16:47:30 +00:00
action = getActionTimeout ( to )
actionName = "timeout $user"
2021-10-25 21:21:52 +00:00
}
channel := plugins . DeriveChannel ( m , eventData )
messageStoreLock . RLock ( )
defer messageStoreLock . RUnlock ( )
var executedEnforcement [ ] string
for _ , stMsg := range messageStore [ channel ] {
badges := twitch . ParseBadgeLevels ( stMsg . Msg )
if stMsg . Time . Before ( scanTime ) {
continue
}
if badges . Has ( "broadcaster" ) || badges . Has ( "moderator" ) {
continue
}
if ! match . MatchString ( stMsg . Msg . Trailing ( ) ) {
continue
}
enforcement := strings . NewReplacer (
2023-09-11 17:51:38 +00:00
"$msgid" , stMsg . Msg . Tags [ "id" ] ,
2021-10-25 21:21:52 +00:00
"$user" , plugins . DeriveUser ( stMsg . Msg , nil ) ,
2022-10-25 16:47:30 +00:00
) . Replace ( actionName )
2021-10-25 21:21:52 +00:00
if str . StringInSlice ( enforcement , executedEnforcement ) {
continue
}
2023-09-11 17:51:38 +00:00
if err = action ( channel , rawMatch , stMsg . Msg . Tags [ "id" ] , plugins . DeriveUser ( stMsg . Msg , nil ) ) ; err != nil {
2022-10-25 16:47:30 +00:00
return false , errors . Wrap ( err , "executing action" )
2021-10-25 21:21:52 +00:00
}
executedEnforcement = append ( executedEnforcement , enforcement )
}
return false , nil
}
2024-01-01 16:52:18 +00:00
func ( actor ) IsAsync ( ) bool { return false }
func ( actor ) Name ( ) string { return actorName }
2021-10-25 21:21:52 +00:00
2024-01-01 16:52:18 +00:00
func ( actor ) Validate ( tplValidator plugins . TemplateValidatorFunc , attrs * plugins . FieldCollection ) ( err error ) {
2021-10-25 21:21:52 +00:00
if v , err := attrs . String ( "match" ) ; err != nil || v == "" {
return errors . New ( "match must be non-empty string" )
}
2022-10-31 16:26:53 +00:00
for _ , field := range [ ] string { "scan" , "action" , "match" } {
if err = tplValidator ( attrs . MustString ( field , ptrStringEmpty ) ) ; err != nil {
return errors . Wrapf ( err , "validating %s template" , field )
}
}
2021-10-25 21:21:52 +00:00
return nil
}