mirror of
https://github.com/Luzifer/twitch-bot.git
synced 2024-11-09 16:50:01 +00:00
[nuke] Add new moderation module
Signed-off-by: Knut Ahlers <knut@ahlers.me>
This commit is contained in:
parent
200e131f6f
commit
c10b50bbc7
3 changed files with 261 additions and 0 deletions
|
@ -9,6 +9,7 @@ import (
|
|||
"github.com/Luzifer/twitch-bot/internal/actors/delay"
|
||||
deleteactor "github.com/Luzifer/twitch-bot/internal/actors/delete"
|
||||
"github.com/Luzifer/twitch-bot/internal/actors/modchannel"
|
||||
"github.com/Luzifer/twitch-bot/internal/actors/nuke"
|
||||
"github.com/Luzifer/twitch-bot/internal/actors/punish"
|
||||
"github.com/Luzifer/twitch-bot/internal/actors/quotedb"
|
||||
"github.com/Luzifer/twitch-bot/internal/actors/raw"
|
||||
|
@ -27,6 +28,7 @@ var (
|
|||
delay.Register,
|
||||
deleteactor.Register,
|
||||
modchannel.Register,
|
||||
nuke.Register,
|
||||
punish.Register,
|
||||
quotedb.Register,
|
||||
raw.Register,
|
||||
|
|
238
internal/actors/nuke/actor.go
Normal file
238
internal/actors/nuke/actor.go
Normal file
|
@ -0,0 +1,238 @@
|
|||
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
|
||||
}
|
|
@ -120,6 +120,27 @@ Modify variable contents
|
|||
set: ""
|
||||
```
|
||||
|
||||
## Nuke Chat
|
||||
|
||||
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!
|
||||
|
||||
```yaml
|
||||
- type: nuke
|
||||
attributes:
|
||||
# How long to scan into the past, template must yield a duration (max 10m)
|
||||
# Optional: true
|
||||
# Type: string (Supports Templating)
|
||||
scan: "10m"
|
||||
# What action to take when message matches (delete / ban / <timeout duration>)
|
||||
# Optional: true
|
||||
# Type: string (Supports Templating)
|
||||
action: "delete"
|
||||
# Regular expression (RE2) to select matching messages
|
||||
# Optional: false
|
||||
# Type: string (Supports Templating)
|
||||
match: ""
|
||||
```
|
||||
|
||||
## Punish User
|
||||
|
||||
Apply increasing punishments to user
|
||||
|
|
Loading…
Reference in a new issue