mirror of
https://github.com/Luzifer/twitch-bot.git
synced 2024-12-21 04:11:16 +00:00
239 lines
5.9 KiB
Go
239 lines
5.9 KiB
Go
|
package nuke
|
||
|
|
||
|
import (
|
||
|
"fmt"
|
||
|
"regexp"
|
||
|
"strings"
|
||
|
"sync"
|
||
|
"time"
|
||
|
|
||
|
"github.com/Luzifer/go_helpers/v2/str"
|
||
|
"github.com/Luzifer/twitch-bot/plugins"
|
||
|
"github.com/Luzifer/twitch-bot/twitch"
|
||
|
"github.com/go-irc/irc"
|
||
|
"github.com/pkg/errors"
|
||
|
log "github.com/sirupsen/logrus"
|
||
|
)
|
||
|
|
||
|
const (
|
||
|
actorName = "nuke"
|
||
|
storeRetentionTime = 10 * time.Minute
|
||
|
)
|
||
|
|
||
|
var (
|
||
|
formatMessage plugins.MsgFormatter
|
||
|
|
||
|
messageStore = map[string][]*storedMessage{}
|
||
|
messageStoreLock sync.RWMutex
|
||
|
|
||
|
ptrStringDelete = func(v string) *string { return &v }("delete")
|
||
|
ptrString10m = func(v string) *string { return &v }("10m")
|
||
|
)
|
||
|
|
||
|
func Register(args plugins.RegistrationArguments) error {
|
||
|
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
|
||
|
}
|
||
|
)
|
||
|
|
||
|
func (a actor) Execute(c *irc.Client, m *irc.Message, r *plugins.Rule, eventData plugins.FieldCollection, attrs plugins.FieldCollection) (preventCooldown bool, err error) {
|
||
|
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)
|
||
|
|
||
|
var action string
|
||
|
rawAction, err := formatMessage(attrs.MustString("action", ptrStringDelete), m, r, eventData)
|
||
|
if err != nil {
|
||
|
return false, errors.Wrap(err, "formatting action")
|
||
|
}
|
||
|
switch rawAction {
|
||
|
case "delete":
|
||
|
action = "/delete $msgid"
|
||
|
case "ban":
|
||
|
action = `/ban $user Nuke issued for "$match"`
|
||
|
default:
|
||
|
to, err := time.ParseDuration(rawAction)
|
||
|
if err != nil {
|
||
|
return false, errors.Wrap(err, "parsing action duration")
|
||
|
}
|
||
|
action = fmt.Sprintf(`/timeout $user %d Nuke issued for "$match"`, to/time.Second)
|
||
|
}
|
||
|
|
||
|
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(
|
||
|
"$match", rawMatch,
|
||
|
"$msgid", string(stMsg.Msg.Tags["id"]),
|
||
|
"$user", plugins.DeriveUser(stMsg.Msg, nil),
|
||
|
).Replace(action)
|
||
|
|
||
|
if str.StringInSlice(enforcement, executedEnforcement) {
|
||
|
continue
|
||
|
}
|
||
|
|
||
|
if err = c.WriteMessage(&irc.Message{
|
||
|
Command: "PRIVMSG",
|
||
|
Params: []string{
|
||
|
channel,
|
||
|
enforcement,
|
||
|
},
|
||
|
}); err != nil {
|
||
|
return false, errors.Wrap(err, "sending action")
|
||
|
}
|
||
|
|
||
|
executedEnforcement = append(executedEnforcement, enforcement)
|
||
|
}
|
||
|
|
||
|
return false, nil
|
||
|
}
|
||
|
|
||
|
func (a actor) IsAsync() bool { return false }
|
||
|
func (a actor) Name() string { return actorName }
|
||
|
|
||
|
func (a actor) Validate(attrs plugins.FieldCollection) (err error) {
|
||
|
if v, err := attrs.String("match"); err != nil || v == "" {
|
||
|
return errors.New("match must be non-empty string")
|
||
|
}
|
||
|
|
||
|
return nil
|
||
|
}
|