[core] BREAKING: Allow actors to set fields those after them (#11)

Signed-off-by: Knut Ahlers <knut@ahlers.me>
This commit is contained in:
Knut Ahlers 2021-11-11 14:59:08 +01:00
parent 3ba25e6db2
commit 8ba6d2ef08
Signed by: luzifer
GPG key ID: 0066F03ED215AD7D
29 changed files with 402 additions and 158 deletions

View file

@ -107,7 +107,7 @@ func init() {
type ActorCounter struct{} type ActorCounter struct{}
func (a ActorCounter) Execute(c *irc.Client, m *irc.Message, r *plugins.Rule, eventData plugins.FieldCollection, attrs plugins.FieldCollection) (preventCooldown bool, err error) { func (a ActorCounter) Execute(c *irc.Client, m *irc.Message, r *plugins.Rule, eventData *plugins.FieldCollection, attrs *plugins.FieldCollection) (preventCooldown bool, err error) {
counterName, err := formatMessage(attrs.MustString("counter", nil), m, r, eventData) counterName, err := formatMessage(attrs.MustString("counter", nil), m, r, eventData)
if err != nil { if err != nil {
return false, errors.Wrap(err, "preparing response") return false, errors.Wrap(err, "preparing response")
@ -144,7 +144,7 @@ func (a ActorCounter) Execute(c *irc.Client, m *irc.Message, r *plugins.Rule, ev
func (a ActorCounter) IsAsync() bool { return false } func (a ActorCounter) IsAsync() bool { return false }
func (a ActorCounter) Name() string { return "counter" } func (a ActorCounter) Name() string { return "counter" }
func (a ActorCounter) Validate(attrs plugins.FieldCollection) (err error) { func (a ActorCounter) Validate(attrs *plugins.FieldCollection) (err error) {
if cn, err := attrs.String("counter"); err != nil || cn == "" { if cn, err := attrs.String("counter"); err != nil || cn == "" {
return errors.New("counter name must be non-empty string") return errors.New("counter name must be non-empty string")
} }

View file

@ -46,7 +46,7 @@ func init() {
type ActorScript struct{} type ActorScript struct{}
func (a ActorScript) Execute(c *irc.Client, m *irc.Message, r *plugins.Rule, eventData plugins.FieldCollection, attrs plugins.FieldCollection) (preventCooldown bool, err error) { func (a ActorScript) Execute(c *irc.Client, m *irc.Message, r *plugins.Rule, eventData *plugins.FieldCollection, attrs *plugins.FieldCollection) (preventCooldown bool, err error) {
command, err := attrs.StringSlice("command") command, err := attrs.StringSlice("command")
if err != nil { if err != nil {
return false, errors.Wrap(err, "getting command") return false, errors.Wrap(err, "getting command")
@ -123,7 +123,7 @@ func (a ActorScript) Execute(c *irc.Client, m *irc.Message, r *plugins.Rule, eve
func (a ActorScript) IsAsync() bool { return false } func (a ActorScript) IsAsync() bool { return false }
func (a ActorScript) Name() string { return "script" } func (a ActorScript) Name() string { return "script" }
func (a ActorScript) Validate(attrs plugins.FieldCollection) (err error) { func (a ActorScript) Validate(attrs *plugins.FieldCollection) (err error) {
if cmd, err := attrs.StringSlice("command"); err != nil || len(cmd) == 0 { if cmd, err := attrs.StringSlice("command"); err != nil || len(cmd) == 0 {
return errors.New("command must be slice of strings with length > 0") return errors.New("command must be slice of strings with length > 0")
} }

View file

@ -92,7 +92,7 @@ func init() {
type ActorSetVariable struct{} type ActorSetVariable struct{}
func (a ActorSetVariable) Execute(c *irc.Client, m *irc.Message, r *plugins.Rule, eventData plugins.FieldCollection, attrs plugins.FieldCollection) (preventCooldown bool, err error) { func (a ActorSetVariable) Execute(c *irc.Client, m *irc.Message, r *plugins.Rule, eventData *plugins.FieldCollection, attrs *plugins.FieldCollection) (preventCooldown bool, err error) {
varName, err := formatMessage(attrs.MustString("variable", nil), m, r, eventData) varName, err := formatMessage(attrs.MustString("variable", nil), m, r, eventData)
if err != nil { if err != nil {
return false, errors.Wrap(err, "preparing variable name") return false, errors.Wrap(err, "preparing variable name")
@ -119,7 +119,7 @@ func (a ActorSetVariable) Execute(c *irc.Client, m *irc.Message, r *plugins.Rule
func (a ActorSetVariable) IsAsync() bool { return false } func (a ActorSetVariable) IsAsync() bool { return false }
func (a ActorSetVariable) Name() string { return "setvariable" } func (a ActorSetVariable) Name() string { return "setvariable" }
func (a ActorSetVariable) Validate(attrs plugins.FieldCollection) (err error) { func (a ActorSetVariable) Validate(attrs *plugins.FieldCollection) (err error) {
if v, err := attrs.String("variable"); err != nil || v == "" { if v, err := attrs.String("variable"); err != nil || v == "" {
return errors.New("variable name must be non-empty string") return errors.New("variable name must be non-empty string")
} }

View file

@ -40,7 +40,7 @@ func registerAction(name string, acf plugins.ActorCreationFunc) {
availableActions[name] = acf availableActions[name] = acf
} }
func triggerAction(c *irc.Client, m *irc.Message, rule *plugins.Rule, ra *plugins.RuleAction, eventData plugins.FieldCollection) (preventCooldown bool, err error) { func triggerAction(c *irc.Client, m *irc.Message, rule *plugins.Rule, ra *plugins.RuleAction, eventData *plugins.FieldCollection) (preventCooldown bool, err error) {
availableActionsLock.RLock() availableActionsLock.RLock()
defer availableActionsLock.RUnlock() defer availableActionsLock.RUnlock()
@ -64,7 +64,7 @@ func triggerAction(c *irc.Client, m *irc.Message, rule *plugins.Rule, ra *plugin
return apc, errors.Wrap(err, "execute action") return apc, errors.Wrap(err, "execute action")
} }
func handleMessage(c *irc.Client, m *irc.Message, event *string, eventData plugins.FieldCollection) { func handleMessage(c *irc.Client, m *irc.Message, event *string, eventData *plugins.FieldCollection) {
for _, r := range config.GetMatchingRules(m, event, eventData) { for _, r := range config.GetMatchingRules(m, event, eventData) {
var preventCooldown bool var preventCooldown bool

View file

@ -7,6 +7,7 @@ import (
"time" "time"
"github.com/Luzifer/go_helpers/v2/str" "github.com/Luzifer/go_helpers/v2/str"
"github.com/Luzifer/twitch-bot/plugins"
"github.com/go-irc/irc" "github.com/go-irc/irc"
"github.com/mitchellh/hashstructure/v2" "github.com/mitchellh/hashstructure/v2"
"github.com/pkg/errors" "github.com/pkg/errors"
@ -163,9 +164,10 @@ func (a *autoMessage) allowExecuteDisableOnTemplate() bool {
return true return true
} }
res, err := formatMessage(*a.DisableOnTemplate, nil, nil, map[string]interface{}{ fields := plugins.NewFieldCollection()
"channel": a.Channel, fields.Set("channel", a.Channel)
})
res, err := formatMessage(*a.DisableOnTemplate, nil, nil, fields)
if err != nil { if err != nil {
log.WithError(err).Error("Error in auto-message disable template") log.WithError(err).Error("Error in auto-message disable template")
// Caused an error, forbid execution // Caused an error, forbid execution

View file

@ -250,7 +250,7 @@ func (c *configFile) CloseRawMessageWriter() error {
return c.rawLogWriter.Close() return c.rawLogWriter.Close()
} }
func (c configFile) GetMatchingRules(m *irc.Message, event *string, eventData map[string]interface{}) []*plugins.Rule { func (c configFile) GetMatchingRules(m *irc.Message, event *string, eventData *plugins.FieldCollection) []*plugins.Rule {
configLock.RLock() configLock.RLock()
defer configLock.RUnlock() defer configLock.RUnlock()

View file

@ -29,7 +29,7 @@ func newTemplateFuncProvider() *templateFuncProvider {
return out return out
} }
func (t *templateFuncProvider) GetFuncMap(m *irc.Message, r *plugins.Rule, fields map[string]interface{}) template.FuncMap { func (t *templateFuncProvider) GetFuncMap(m *irc.Message, r *plugins.Rule, fields *plugins.FieldCollection) template.FuncMap {
t.lock.RLock() t.lock.RLock()
defer t.lock.RUnlock() defer t.lock.RUnlock()

View file

@ -9,11 +9,11 @@ import (
) )
func init() { func init() {
tplFuncs.Register("channelCounter", func(m *irc.Message, r *plugins.Rule, fields plugins.FieldCollection) interface{} { tplFuncs.Register("channelCounter", func(m *irc.Message, r *plugins.Rule, fields *plugins.FieldCollection) interface{} {
return func(name string) (string, error) { return func(name string) (string, error) {
channel, ok := fields["channel"].(string) channel, err := fields.String("channel")
if !ok { if err != nil {
return "", errors.New("channel not available") return "", errors.Wrap(err, "channel not available")
} }
return strings.Join([]string{channel, name}, ":"), nil return strings.Join([]string{channel, name}, ":"), nil

View file

@ -10,7 +10,7 @@ import (
) )
func init() { func init() {
tplFuncs.Register("arg", func(m *irc.Message, r *plugins.Rule, fields plugins.FieldCollection) interface{} { tplFuncs.Register("arg", func(m *irc.Message, r *plugins.Rule, fields *plugins.FieldCollection) interface{} {
return func(arg int) (string, error) { return func(arg int) (string, error) {
msgParts := strings.Split(m.Trailing(), " ") msgParts := strings.Split(m.Trailing(), " ")
if len(msgParts) <= arg { if len(msgParts) <= arg {
@ -21,10 +21,10 @@ func init() {
} }
}) })
tplFuncs.Register("botHasBadge", func(m *irc.Message, r *plugins.Rule, fields plugins.FieldCollection) interface{} { tplFuncs.Register("botHasBadge", func(m *irc.Message, r *plugins.Rule, fields *plugins.FieldCollection) interface{} {
return func(badge string) bool { return func(badge string) bool {
channel, ok := fields["channel"].(string) channel, err := fields.String("channel")
if !ok { if err != nil {
log.Trace("Fields for botHasBadge function had no channel") log.Trace("Fields for botHasBadge function had no channel")
return false return false
} }
@ -40,7 +40,7 @@ func init() {
tplFuncs.Register("fixUsername", plugins.GenericTemplateFunctionGetter(func(username string) string { return strings.TrimLeft(username, "@#") })) tplFuncs.Register("fixUsername", plugins.GenericTemplateFunctionGetter(func(username string) string { return strings.TrimLeft(username, "@#") }))
tplFuncs.Register("group", func(m *irc.Message, r *plugins.Rule, fields plugins.FieldCollection) interface{} { tplFuncs.Register("group", func(m *irc.Message, r *plugins.Rule, fields *plugins.FieldCollection) interface{} {
return func(idx int, fallback ...string) (string, error) { return func(idx int, fallback ...string) (string, error) {
fields := r.GetMatchMessage().FindStringSubmatch(m.Trailing()) fields := r.GetMatchMessage().FindStringSubmatch(m.Trailing())
if len(fields) <= idx { if len(fields) <= idx {
@ -55,7 +55,7 @@ func init() {
} }
}) })
tplFuncs.Register("tag", func(m *irc.Message, r *plugins.Rule, fields plugins.FieldCollection) interface{} { tplFuncs.Register("tag", func(m *irc.Message, r *plugins.Rule, fields *plugins.FieldCollection) interface{} {
return func(tag string) string { return func(tag string) string {
s, _ := m.GetTag(tag) s, _ := m.GetTag(tag)
return s return s

View file

@ -36,7 +36,7 @@ func Register(args plugins.RegistrationArguments) error {
type actor struct{} type actor struct{}
func (a actor) Execute(c *irc.Client, m *irc.Message, r *plugins.Rule, eventData plugins.FieldCollection, attrs plugins.FieldCollection) (preventCooldown bool, err error) { func (a actor) Execute(c *irc.Client, m *irc.Message, r *plugins.Rule, eventData *plugins.FieldCollection, attrs *plugins.FieldCollection) (preventCooldown bool, err error) {
ptrStringEmpty := func(v string) *string { return &v }("") ptrStringEmpty := func(v string) *string { return &v }("")
cmd := []string{ cmd := []string{
@ -63,4 +63,4 @@ func (a actor) Execute(c *irc.Client, m *irc.Message, r *plugins.Rule, eventData
func (a actor) IsAsync() bool { return false } func (a actor) IsAsync() bool { return false }
func (a actor) Name() string { return actorName } func (a actor) Name() string { return actorName }
func (a actor) Validate(attrs plugins.FieldCollection) (err error) { return nil } func (a actor) Validate(attrs *plugins.FieldCollection) (err error) { return nil }

View file

@ -45,7 +45,7 @@ func Register(args plugins.RegistrationArguments) error {
type actor struct{} type actor struct{}
func (a actor) Execute(c *irc.Client, m *irc.Message, r *plugins.Rule, eventData plugins.FieldCollection, attrs plugins.FieldCollection) (preventCooldown bool, err error) { func (a actor) Execute(c *irc.Client, m *irc.Message, r *plugins.Rule, eventData *plugins.FieldCollection, attrs *plugins.FieldCollection) (preventCooldown bool, err error) {
var ( var (
ptrZeroDuration = func(v time.Duration) *time.Duration { return &v }(0) ptrZeroDuration = func(v time.Duration) *time.Duration { return &v }(0)
delay = attrs.MustDuration("delay", ptrZeroDuration) delay = attrs.MustDuration("delay", ptrZeroDuration)
@ -68,4 +68,4 @@ func (a actor) Execute(c *irc.Client, m *irc.Message, r *plugins.Rule, eventData
func (a actor) IsAsync() bool { return false } func (a actor) IsAsync() bool { return false }
func (a actor) Name() string { return actorName } func (a actor) Name() string { return actorName }
func (a actor) Validate(attrs plugins.FieldCollection) (err error) { return nil } func (a actor) Validate(attrs *plugins.FieldCollection) (err error) { return nil }

View file

@ -24,7 +24,7 @@ func Register(args plugins.RegistrationArguments) error {
type actor struct{} type actor struct{}
func (a actor) Execute(c *irc.Client, m *irc.Message, r *plugins.Rule, eventData plugins.FieldCollection, attrs plugins.FieldCollection) (preventCooldown bool, err error) { func (a actor) Execute(c *irc.Client, m *irc.Message, r *plugins.Rule, eventData *plugins.FieldCollection, attrs *plugins.FieldCollection) (preventCooldown bool, err error) {
msgID, ok := m.Tags.GetTag("id") msgID, ok := m.Tags.GetTag("id")
if !ok || msgID == "" { if !ok || msgID == "" {
return false, nil return false, nil
@ -45,4 +45,4 @@ func (a actor) Execute(c *irc.Client, m *irc.Message, r *plugins.Rule, eventData
func (a actor) IsAsync() bool { return false } func (a actor) IsAsync() bool { return false }
func (a actor) Name() string { return actorName } func (a actor) Name() string { return actorName }
func (a actor) Validate(attrs plugins.FieldCollection) (err error) { return nil } func (a actor) Validate(attrs *plugins.FieldCollection) (err error) { return nil }

View file

@ -64,7 +64,7 @@ func Register(args plugins.RegistrationArguments) error {
type actor struct{} type actor struct{}
func (a actor) Execute(c *irc.Client, m *irc.Message, r *plugins.Rule, eventData plugins.FieldCollection, attrs plugins.FieldCollection) (preventCooldown bool, err error) { func (a actor) Execute(c *irc.Client, m *irc.Message, r *plugins.Rule, eventData *plugins.FieldCollection, attrs *plugins.FieldCollection) (preventCooldown bool, err error) {
var ( var (
ptrStringEmpty = func(v string) *string { return &v }("") ptrStringEmpty = func(v string) *string { return &v }("")
game = attrs.MustString("game", ptrStringEmpty) game = attrs.MustString("game", ptrStringEmpty)
@ -109,7 +109,7 @@ func (a actor) Execute(c *irc.Client, m *irc.Message, r *plugins.Rule, eventData
func (a actor) IsAsync() bool { return false } func (a actor) IsAsync() bool { return false }
func (a actor) Name() string { return actorName } func (a actor) Name() string { return actorName }
func (a actor) Validate(attrs plugins.FieldCollection) (err error) { func (a actor) Validate(attrs *plugins.FieldCollection) (err error) {
if v, err := attrs.String("channel"); err != nil || v == "" { if v, err := attrs.String("channel"); err != nil || v == "" {
return errors.New("channel must be non-empty string") return errors.New("channel must be non-empty string")
} }

View file

@ -143,7 +143,7 @@ type (
} }
) )
func (a actor) Execute(c *irc.Client, m *irc.Message, r *plugins.Rule, eventData plugins.FieldCollection, attrs plugins.FieldCollection) (preventCooldown bool, err error) { 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) rawMatch, err := formatMessage(attrs.MustString("match", nil), m, r, eventData)
if err != nil { if err != nil {
return false, errors.Wrap(err, "formatting match") return false, errors.Wrap(err, "formatting match")
@ -229,7 +229,7 @@ func (a actor) Execute(c *irc.Client, m *irc.Message, r *plugins.Rule, eventData
func (a actor) IsAsync() bool { return false } func (a actor) IsAsync() bool { return false }
func (a actor) Name() string { return actorName } func (a actor) Name() string { return actorName }
func (a actor) Validate(attrs plugins.FieldCollection) (err error) { func (a actor) Validate(attrs *plugins.FieldCollection) (err error) {
if v, err := attrs.String("match"); err != nil || v == "" { if v, err := attrs.String("match"); err != nil || v == "" {
return errors.New("match must be non-empty string") return errors.New("match must be non-empty string")
} }

View file

@ -142,7 +142,7 @@ type (
// Punish // Punish
func (a actorPunish) Execute(c *irc.Client, m *irc.Message, r *plugins.Rule, eventData plugins.FieldCollection, attrs plugins.FieldCollection) (preventCooldown bool, err error) { func (a actorPunish) Execute(c *irc.Client, m *irc.Message, r *plugins.Rule, eventData *plugins.FieldCollection, attrs *plugins.FieldCollection) (preventCooldown bool, err error) {
var ( var (
cooldown = attrs.MustDuration("cooldown", ptrDefaultCooldown) cooldown = attrs.MustDuration("cooldown", ptrDefaultCooldown)
reason = attrs.MustString("reason", ptrStringEmpty) reason = attrs.MustString("reason", ptrStringEmpty)
@ -214,7 +214,7 @@ func (a actorPunish) Execute(c *irc.Client, m *irc.Message, r *plugins.Rule, eve
func (a actorPunish) IsAsync() bool { return false } func (a actorPunish) IsAsync() bool { return false }
func (a actorPunish) Name() string { return actorNamePunish } func (a actorPunish) Name() string { return actorNamePunish }
func (a actorPunish) Validate(attrs plugins.FieldCollection) (err error) { func (a actorPunish) Validate(attrs *plugins.FieldCollection) (err error) {
if v, err := attrs.String("user"); err != nil || v == "" { if v, err := attrs.String("user"); err != nil || v == "" {
return errors.New("user must be non-empty string") return errors.New("user must be non-empty string")
} }
@ -228,7 +228,7 @@ func (a actorPunish) Validate(attrs plugins.FieldCollection) (err error) {
// Reset // Reset
func (a actorResetPunish) Execute(c *irc.Client, m *irc.Message, r *plugins.Rule, eventData plugins.FieldCollection, attrs plugins.FieldCollection) (preventCooldown bool, err error) { func (a actorResetPunish) Execute(c *irc.Client, m *irc.Message, r *plugins.Rule, eventData *plugins.FieldCollection, attrs *plugins.FieldCollection) (preventCooldown bool, err error) {
var ( var (
user = attrs.MustString("user", nil) user = attrs.MustString("user", nil)
uuid = attrs.MustString("uuid", ptrStringEmpty) uuid = attrs.MustString("uuid", ptrStringEmpty)
@ -249,7 +249,7 @@ func (a actorResetPunish) Execute(c *irc.Client, m *irc.Message, r *plugins.Rule
func (a actorResetPunish) IsAsync() bool { return false } func (a actorResetPunish) IsAsync() bool { return false }
func (a actorResetPunish) Name() string { return actorNameResetPunish } func (a actorResetPunish) Name() string { return actorNameResetPunish }
func (a actorResetPunish) Validate(attrs plugins.FieldCollection) (err error) { func (a actorResetPunish) Validate(attrs *plugins.FieldCollection) (err error) {
if v, err := attrs.String("user"); err != nil || v == "" { if v, err := attrs.String("user"); err != nil || v == "" {
return errors.New("user must be non-empty string") return errors.New("user must be non-empty string")
} }

View file

@ -79,7 +79,7 @@ func Register(args plugins.RegistrationArguments) error {
registerAPI(args.RegisterAPIRoute) registerAPI(args.RegisterAPIRoute)
args.RegisterTemplateFunction("lastQuoteIndex", func(m *irc.Message, r *plugins.Rule, fields plugins.FieldCollection) interface{} { args.RegisterTemplateFunction("lastQuoteIndex", func(m *irc.Message, r *plugins.Rule, fields *plugins.FieldCollection) interface{} {
return func() int { return func() int {
return storedObject.GetMaxQuoteIdx(plugins.DeriveChannel(m, nil)) return storedObject.GetMaxQuoteIdx(plugins.DeriveChannel(m, nil))
} }
@ -101,7 +101,7 @@ type (
} }
) )
func (a actor) Execute(c *irc.Client, m *irc.Message, r *plugins.Rule, eventData plugins.FieldCollection, attrs plugins.FieldCollection) (preventCooldown bool, err error) { func (a actor) Execute(c *irc.Client, m *irc.Message, r *plugins.Rule, eventData *plugins.FieldCollection, attrs *plugins.FieldCollection) (preventCooldown bool, err error) {
var ( var (
action = attrs.MustString("action", ptrStringEmpty) action = attrs.MustString("action", ptrStringEmpty)
indexStr = attrs.MustString("index", ptrStringZero) indexStr = attrs.MustString("index", ptrStringZero)
@ -149,12 +149,9 @@ func (a actor) Execute(c *irc.Client, m *irc.Message, r *plugins.Rule, eventData
return false, nil return false, nil
} }
fields := make(plugins.FieldCollection) fields := eventData.Clone()
for k, v := range eventData { fields.Set("index", idx)
fields[k] = v fields.Set("quote", quote)
}
fields["index"] = idx
fields["quote"] = quote
format := attrs.MustString("format", ptrStringOutFormat) format := attrs.MustString("format", ptrStringOutFormat)
msg, err := formatMessage(format, m, r, fields) msg, err := formatMessage(format, m, r, fields)
@ -180,7 +177,7 @@ func (a actor) Execute(c *irc.Client, m *irc.Message, r *plugins.Rule, eventData
func (a actor) IsAsync() bool { return false } func (a actor) IsAsync() bool { return false }
func (a actor) Name() string { return actorName } func (a actor) Name() string { return actorName }
func (a actor) Validate(attrs plugins.FieldCollection) (err error) { func (a actor) Validate(attrs *plugins.FieldCollection) (err error) {
action := attrs.MustString("action", ptrStringEmpty) action := attrs.MustString("action", ptrStringEmpty)
switch action { switch action {

View file

@ -38,7 +38,7 @@ func Register(args plugins.RegistrationArguments) error {
type actor struct{} type actor struct{}
func (a actor) Execute(c *irc.Client, m *irc.Message, r *plugins.Rule, eventData plugins.FieldCollection, attrs plugins.FieldCollection) (preventCooldown bool, err error) { func (a actor) Execute(c *irc.Client, m *irc.Message, r *plugins.Rule, eventData *plugins.FieldCollection, attrs *plugins.FieldCollection) (preventCooldown bool, err error) {
rawMsg, err := formatMessage(attrs.MustString("message", nil), m, r, eventData) rawMsg, err := formatMessage(attrs.MustString("message", nil), m, r, eventData)
if err != nil { if err != nil {
return false, errors.Wrap(err, "preparing raw message") return false, errors.Wrap(err, "preparing raw message")
@ -58,7 +58,7 @@ func (a actor) Execute(c *irc.Client, m *irc.Message, r *plugins.Rule, eventData
func (a actor) IsAsync() bool { return false } func (a actor) IsAsync() bool { return false }
func (a actor) Name() string { return actorName } func (a actor) Name() string { return actorName }
func (a actor) Validate(attrs plugins.FieldCollection) (err error) { func (a actor) Validate(attrs *plugins.FieldCollection) (err error) {
if v, err := attrs.String("message"); err != nil || v == "" { if v, err := attrs.String("message"); err != nil || v == "" {
return errors.New("message must be non-empty string") return errors.New("message must be non-empty string")
} }

View file

@ -74,7 +74,7 @@ func Register(args plugins.RegistrationArguments) error {
type actor struct{} type actor struct{}
func (a actor) Execute(c *irc.Client, m *irc.Message, r *plugins.Rule, eventData plugins.FieldCollection, attrs plugins.FieldCollection) (preventCooldown bool, err error) { func (a actor) Execute(c *irc.Client, m *irc.Message, r *plugins.Rule, eventData *plugins.FieldCollection, attrs *plugins.FieldCollection) (preventCooldown bool, err error) {
msg, err := formatMessage(attrs.MustString("message", nil), m, r, eventData) msg, err := formatMessage(attrs.MustString("message", nil), m, r, eventData)
if err != nil { if err != nil {
if !attrs.CanString("fallback") || attrs.MustString("fallback", nil) == "" { if !attrs.CanString("fallback") || attrs.MustString("fallback", nil) == "" {
@ -118,7 +118,7 @@ func (a actor) Execute(c *irc.Client, m *irc.Message, r *plugins.Rule, eventData
func (a actor) IsAsync() bool { return false } func (a actor) IsAsync() bool { return false }
func (a actor) Name() string { return actorName } func (a actor) Name() string { return actorName }
func (a actor) Validate(attrs plugins.FieldCollection) (err error) { func (a actor) Validate(attrs *plugins.FieldCollection) (err error) {
if v, err := attrs.String("message"); err != nil || v == "" { if v, err := attrs.String("message"); err != nil || v == "" {
return errors.New("message must be non-empty string") return errors.New("message must be non-empty string")
} }

View file

@ -37,7 +37,7 @@ func Register(args plugins.RegistrationArguments) error {
type actor struct{} type actor struct{}
func (a actor) Execute(c *irc.Client, m *irc.Message, r *plugins.Rule, eventData plugins.FieldCollection, attrs plugins.FieldCollection) (preventCooldown bool, err error) { func (a actor) Execute(c *irc.Client, m *irc.Message, r *plugins.Rule, eventData *plugins.FieldCollection, attrs *plugins.FieldCollection) (preventCooldown bool, err error) {
return false, errors.Wrap( return false, errors.Wrap(
c.WriteMessage(&irc.Message{ c.WriteMessage(&irc.Message{
Command: "PRIVMSG", Command: "PRIVMSG",
@ -53,7 +53,7 @@ func (a actor) Execute(c *irc.Client, m *irc.Message, r *plugins.Rule, eventData
func (a actor) IsAsync() bool { return false } func (a actor) IsAsync() bool { return false }
func (a actor) Name() string { return actorName } func (a actor) Name() string { return actorName }
func (a actor) Validate(attrs plugins.FieldCollection) (err error) { func (a actor) Validate(attrs *plugins.FieldCollection) (err error) {
if v, err := attrs.Duration("duration"); err != nil || v < time.Second { if v, err := attrs.Duration("duration"); err != nil || v < time.Second {
return errors.New("duration must be of type duration greater or equal one second") return errors.New("duration must be of type duration greater or equal one second")
} }

View file

@ -49,7 +49,7 @@ func Register(args plugins.RegistrationArguments) error {
type actor struct{} type actor struct{}
func (a actor) Execute(c *irc.Client, m *irc.Message, r *plugins.Rule, eventData plugins.FieldCollection, attrs plugins.FieldCollection) (preventCooldown bool, err error) { func (a actor) Execute(c *irc.Client, m *irc.Message, r *plugins.Rule, eventData *plugins.FieldCollection, attrs *plugins.FieldCollection) (preventCooldown bool, err error) {
to, err := formatMessage(attrs.MustString("to", nil), m, r, eventData) to, err := formatMessage(attrs.MustString("to", nil), m, r, eventData)
if err != nil { if err != nil {
return false, errors.Wrap(err, "preparing whisper receiver") return false, errors.Wrap(err, "preparing whisper receiver")
@ -77,7 +77,7 @@ func (a actor) Execute(c *irc.Client, m *irc.Message, r *plugins.Rule, eventData
func (a actor) IsAsync() bool { return false } func (a actor) IsAsync() bool { return false }
func (a actor) Name() string { return actorName } func (a actor) Name() string { return actorName }
func (a actor) Validate(attrs plugins.FieldCollection) (err error) { func (a actor) Validate(attrs *plugins.FieldCollection) (err error) {
if v, err := attrs.String("to"); err != nil || v == "" { if v, err := attrs.String("to"); err != nil || v == "" {
return errors.New("to must be non-empty string") return errors.New("to must be non-empty string")
} }

60
irc.go
View file

@ -198,32 +198,32 @@ func (i ircHandler) handleClearChat(m *irc.Message) {
var ( var (
evt *string evt *string
fields = plugins.FieldCollection{ fields = plugins.NewFieldCollection()
"channel": i.getChannel(m), // Compatibility to plugins.DeriveChannel
}
) )
fields.Set("channel", i.getChannel(m)) // Compatibility to plugins.DeriveChannel
switch { switch {
case secondsErr == nil && hasTargetUserID: case secondsErr == nil && hasTargetUserID:
// User & Duration = Timeout // User & Duration = Timeout
evt = eventTypeTimeout evt = eventTypeTimeout
fields["duration"] = time.Duration(seconds) * time.Second fields.Set("duration", time.Duration(seconds)*time.Second)
fields["seconds"] = seconds fields.Set("seconds", seconds)
fields["target_id"] = targetUserID fields.Set("target_id", targetUserID)
fields["target_name"] = m.Trailing() fields.Set("target_name", m.Trailing())
log.WithFields(log.Fields(fields)).Info("User was timed out") log.WithFields(log.Fields(fields.Data())).Info("User was timed out")
case hasTargetUserID: case hasTargetUserID:
// User w/o Duration = Ban // User w/o Duration = Ban
evt = eventTypeBan evt = eventTypeBan
fields["target_id"] = targetUserID fields.Set("target_id", targetUserID)
fields["target_name"] = m.Trailing() fields.Set("target_name", m.Trailing())
log.WithFields(log.Fields(fields)).Info("User was banned") log.WithFields(log.Fields(fields.Data())).Info("User was banned")
default: default:
// No User = /clear // No User = /clear
evt = eventTypeClearChat evt = eventTypeClearChat
log.WithFields(log.Fields(fields)).Info("Chat was cleared") log.WithFields(log.Fields(fields.Data())).Info("Chat was cleared")
} }
go handleMessage(i.c, m, evt, fields) go handleMessage(i.c, m, evt, fields)
@ -254,7 +254,7 @@ func (i ircHandler) handlePermit(m *irc.Message) {
log.WithField("user", username).Debug("Added permit") log.WithField("user", username).Debug("Added permit")
timerStore.AddPermit(m.Params[0], username) timerStore.AddPermit(m.Params[0], username)
go handleMessage(i.c, m, eventTypePermit, plugins.FieldCollection{"username": username}) go handleMessage(i.c, m, eventTypePermit, plugins.FieldCollectionFromData(map[string]interface{}{"username": username}))
} }
func (i ircHandler) handleTwitchNotice(m *irc.Message) { func (i ircHandler) handleTwitchNotice(m *irc.Message) {
@ -301,9 +301,9 @@ func (i ircHandler) handleTwitchPrivmsg(m *irc.Message) {
} }
if bits, err := strconv.ParseInt(string(m.Tags["bits"]), 10, 64); err == nil { if bits, err := strconv.ParseInt(string(m.Tags["bits"]), 10, 64); err == nil {
go handleMessage(i.c, m, eventTypeBits, plugins.FieldCollection{ go handleMessage(i.c, m, eventTypeBits, plugins.FieldCollectionFromData(map[string]interface{}{
"bits": bits, "bits": bits,
}) }))
} }
go handleMessage(i.c, m, nil, nil) go handleMessage(i.c, m, nil, nil)
@ -322,61 +322,61 @@ func (i ircHandler) handleTwitchUsernotice(m *irc.Message) {
log.WithField("msg", m).Warn("Received usernotice without msg-id") log.WithField("msg", m).Warn("Received usernotice without msg-id")
case "raid": case "raid":
evtData := plugins.FieldCollection{ evtData := plugins.FieldCollectionFromData(map[string]interface{}{
"channel": i.getChannel(m), // Compatibility to plugins.DeriveChannel "channel": i.getChannel(m), // Compatibility to plugins.DeriveChannel
"from": m.Tags["login"], "from": m.Tags["login"],
"user": m.Tags["login"], // Compatibility to plugins.DeriveUser "user": m.Tags["login"], // Compatibility to plugins.DeriveUser
"viewercount": m.Tags["msg-param-viewerCount"], "viewercount": m.Tags["msg-param-viewerCount"],
} })
log.WithFields(log.Fields(evtData)).Info("Incoming raid") log.WithFields(log.Fields(evtData.Data())).Info("Incoming raid")
go handleMessage(i.c, m, eventTypeRaid, evtData) go handleMessage(i.c, m, eventTypeRaid, evtData)
case "resub": case "resub":
evtData := plugins.FieldCollection{ evtData := plugins.FieldCollectionFromData(map[string]interface{}{
"channel": i.getChannel(m), // Compatibility to plugins.DeriveChannel "channel": i.getChannel(m), // Compatibility to plugins.DeriveChannel
"from": m.Tags["login"], "from": m.Tags["login"],
"subscribed_months": m.Tags["msg-param-cumulative-months"], "subscribed_months": m.Tags["msg-param-cumulative-months"],
"plan": m.Tags["msg-param-sub-plan"], "plan": m.Tags["msg-param-sub-plan"],
"user": m.Tags["login"], // Compatibility to plugins.DeriveUser "user": m.Tags["login"], // Compatibility to plugins.DeriveUser
} })
log.WithFields(log.Fields(evtData)).Info("User re-subscribed") log.WithFields(log.Fields(evtData.Data())).Info("User re-subscribed")
go handleMessage(i.c, m, eventTypeResub, evtData) go handleMessage(i.c, m, eventTypeResub, evtData)
case "sub": case "sub":
evtData := plugins.FieldCollection{ evtData := plugins.FieldCollectionFromData(map[string]interface{}{
"channel": i.getChannel(m), // Compatibility to plugins.DeriveChannel "channel": i.getChannel(m), // Compatibility to plugins.DeriveChannel
"from": m.Tags["login"], "from": m.Tags["login"],
"plan": m.Tags["msg-param-sub-plan"], "plan": m.Tags["msg-param-sub-plan"],
"user": m.Tags["login"], // Compatibility to plugins.DeriveUser "user": m.Tags["login"], // Compatibility to plugins.DeriveUser
} })
log.WithFields(log.Fields(evtData)).Info("User subscribed") log.WithFields(log.Fields(evtData.Data())).Info("User subscribed")
go handleMessage(i.c, m, eventTypeSub, evtData) go handleMessage(i.c, m, eventTypeSub, evtData)
case "subgift", "anonsubgift": case "subgift", "anonsubgift":
evtData := plugins.FieldCollection{ evtData := plugins.FieldCollectionFromData(map[string]interface{}{
"channel": i.getChannel(m), // Compatibility to plugins.DeriveChannel "channel": i.getChannel(m), // Compatibility to plugins.DeriveChannel
"from": m.Tags["login"], "from": m.Tags["login"],
"gifted_months": m.Tags["msg-param-gift-months"], "gifted_months": m.Tags["msg-param-gift-months"],
"plan": m.Tags["msg-param-sub-plan"], "plan": m.Tags["msg-param-sub-plan"],
"to": m.Tags["msg-param-recipient-user-name"], "to": m.Tags["msg-param-recipient-user-name"],
"user": m.Tags["login"], // Compatibility to plugins.DeriveUser "user": m.Tags["login"], // Compatibility to plugins.DeriveUser
} })
log.WithFields(log.Fields(evtData)).Info("User gifted a sub") log.WithFields(log.Fields(evtData.Data())).Info("User gifted a sub")
go handleMessage(i.c, m, eventTypeSubgift, evtData) go handleMessage(i.c, m, eventTypeSubgift, evtData)
case "submysterygift": case "submysterygift":
evtData := plugins.FieldCollection{ evtData := plugins.FieldCollectionFromData(map[string]interface{}{
"channel": i.getChannel(m), // Compatibility to plugins.DeriveChannel "channel": i.getChannel(m), // Compatibility to plugins.DeriveChannel
"from": m.Tags["login"], "from": m.Tags["login"],
"number": m.Tags["msg-param-mass-gift-count"], "number": m.Tags["msg-param-mass-gift-count"],
"plan": m.Tags["msg-param-sub-plan"], "plan": m.Tags["msg-param-sub-plan"],
"user": m.Tags["login"], // Compatibility to plugins.DeriveUser "user": m.Tags["login"], // Compatibility to plugins.DeriveUser
} })
log.WithFields(log.Fields(evtData)).Info("User gifted subs to the community") log.WithFields(log.Fields(evtData.Data())).Info("User gifted subs to the community")
go handleMessage(i.c, m, eventTypeSubmysterygift, evtData) go handleMessage(i.c, m, eventTypeSubmysterygift, evtData)

View file

@ -13,27 +13,23 @@ import (
// Compile-time assertion // Compile-time assertion
var _ plugins.MsgFormatter = formatMessage var _ plugins.MsgFormatter = formatMessage
func formatMessage(tplString string, m *irc.Message, r *plugins.Rule, fields plugins.FieldCollection) (string, error) { func formatMessage(tplString string, m *irc.Message, r *plugins.Rule, fields *plugins.FieldCollection) (string, error) {
compiledFields := map[string]interface{}{} compiledFields := plugins.NewFieldCollection()
if config != nil { if config != nil {
configLock.RLock() configLock.RLock()
for k, v := range config.Variables { compiledFields.SetFromData(config.Variables)
compiledFields[k] = v compiledFields.Set("permitTimeout", int64(config.PermitTimeout/time.Second))
}
compiledFields["permitTimeout"] = int64(config.PermitTimeout / time.Second)
configLock.RUnlock() configLock.RUnlock()
} }
for k, v := range fields { compiledFields.SetFromData(fields.Data())
compiledFields[k] = v
}
if m != nil { if m != nil {
compiledFields["msg"] = m compiledFields.Set("msg", m)
} }
compiledFields["username"] = plugins.DeriveUser(m, fields) compiledFields.Set("username", plugins.DeriveUser(m, fields))
compiledFields["channel"] = plugins.DeriveChannel(m, fields) compiledFields.Set("channel", plugins.DeriveChannel(m, fields))
// Parse and execute template // Parse and execute template
tpl, err := template. tpl, err := template.
@ -45,7 +41,7 @@ func formatMessage(tplString string, m *irc.Message, r *plugins.Rule, fields plu
} }
buf := new(bytes.Buffer) buf := new(bytes.Buffer)
err = tpl.Execute(buf, compiledFields) err = tpl.Execute(buf, compiledFields.Data())
return buf.String(), errors.Wrap(err, "execute template") return buf.String(), errors.Wrap(err, "execute template")
} }

View file

@ -1,9 +1,11 @@
package plugins package plugins
import ( import (
"encoding/json"
"fmt" "fmt"
"strconv" "strconv"
"strings" "strings"
"sync"
"time" "time"
"github.com/pkg/errors" "github.com/pkg/errors"
@ -14,33 +16,88 @@ var (
ErrValueMismatch = errors.New("specified value has different format") ErrValueMismatch = errors.New("specified value has different format")
) )
type FieldCollection map[string]interface{} type FieldCollection struct {
data map[string]interface{}
lock sync.RWMutex
}
func (f FieldCollection) CanBool(name string) bool { // NewFieldCollection creates a new FieldCollection with empty data store
func NewFieldCollection() *FieldCollection {
return &FieldCollection{data: make(map[string]interface{})}
}
// FieldCollectionFromData is a wrapper around NewFieldCollection and SetFromData
func FieldCollectionFromData(data map[string]interface{}) *FieldCollection {
o := NewFieldCollection()
o.SetFromData(data)
return o
}
// CanBool tries to read key name as bool and checks whether error is nil
func (f *FieldCollection) CanBool(name string) bool {
_, err := f.Bool(name) _, err := f.Bool(name)
return err == nil return err == nil
} }
func (f FieldCollection) CanDuration(name string) bool { // CanDuration tries to read key name as time.Duration and checks whether error is nil
func (f *FieldCollection) CanDuration(name string) bool {
_, err := f.Duration(name) _, err := f.Duration(name)
return err == nil return err == nil
} }
func (f FieldCollection) CanInt64(name string) bool { // CanInt64 tries to read key name as int64 and checks whether error is nil
func (f *FieldCollection) CanInt64(name string) bool {
_, err := f.Int64(name) _, err := f.Int64(name)
return err == nil return err == nil
} }
func (f FieldCollection) CanString(name string) bool { // CanString tries to read key name as string and checks whether error is nil
func (f *FieldCollection) CanString(name string) bool {
_, err := f.String(name) _, err := f.String(name)
return err == nil return err == nil
} }
func (f FieldCollection) Expect(keys ...string) error { // Clone is a wrapper around n.SetFromData(o.Data())
func (f *FieldCollection) Clone() *FieldCollection {
out := new(FieldCollection)
out.SetFromData(f.Data())
return out
}
// Data creates a map-copy of the data stored inside the FieldCollection
func (f *FieldCollection) Data() map[string]interface{} {
if f == nil {
return nil
}
f.lock.RLock()
defer f.lock.RUnlock()
out := make(map[string]interface{})
for k := range f.data {
out[k] = f.data[k]
}
return out
}
// Expect takes a list of keys and returns an error with all non-found names
func (f *FieldCollection) Expect(keys ...string) error {
if len(keys) == 0 {
return nil
}
if f == nil || f.data == nil {
return errors.New("uninitialized field collection")
}
f.lock.RLock()
defer f.lock.RUnlock()
var missing []string var missing []string
for _, k := range keys { for _, k := range keys {
if _, ok := f[k]; !ok { if _, ok := f.data[k]; !ok {
missing = append(missing, k) missing = append(missing, k)
} }
} }
@ -52,17 +109,13 @@ func (f FieldCollection) Expect(keys ...string) error {
return nil return nil
} }
func (f FieldCollection) HasAll(keys ...string) bool { // HasAll takes a list of keys and returns whether all of them exist inside the FieldCollection
for _, k := range keys { func (f *FieldCollection) HasAll(keys ...string) bool {
if _, ok := f[k]; !ok { return f.Expect(keys...) == nil
return false
}
}
return true
} }
func (f FieldCollection) MustBool(name string, defVal *bool) bool { // MustBool is a wrapper around Bool and panics if an error was returned
func (f *FieldCollection) MustBool(name string, defVal *bool) bool {
v, err := f.Bool(name) v, err := f.Bool(name)
if err != nil { if err != nil {
if defVal != nil { if defVal != nil {
@ -73,7 +126,8 @@ func (f FieldCollection) MustBool(name string, defVal *bool) bool {
return v return v
} }
func (f FieldCollection) MustDuration(name string, defVal *time.Duration) time.Duration { // MustDuration is a wrapper around Duration and panics if an error was returned
func (f *FieldCollection) MustDuration(name string, defVal *time.Duration) time.Duration {
v, err := f.Duration(name) v, err := f.Duration(name)
if err != nil { if err != nil {
if defVal != nil { if defVal != nil {
@ -84,7 +138,8 @@ func (f FieldCollection) MustDuration(name string, defVal *time.Duration) time.D
return v return v
} }
func (f FieldCollection) MustInt64(name string, defVal *int64) int64 { // MustInt64 is a wrapper around Int64 and panics if an error was returned
func (f *FieldCollection) MustInt64(name string, defVal *int64) int64 {
v, err := f.Int64(name) v, err := f.Int64(name)
if err != nil { if err != nil {
if defVal != nil { if defVal != nil {
@ -95,7 +150,8 @@ func (f FieldCollection) MustInt64(name string, defVal *int64) int64 {
return v return v
} }
func (f FieldCollection) MustString(name string, defVal *string) string { // MustString is a wrapper around String and panics if an error was returned
func (f *FieldCollection) MustString(name string, defVal *string) string {
v, err := f.String(name) v, err := f.String(name)
if err != nil { if err != nil {
if defVal != nil { if defVal != nil {
@ -106,8 +162,16 @@ func (f FieldCollection) MustString(name string, defVal *string) string {
return v return v
} }
func (f FieldCollection) Bool(name string) (bool, error) { // Bool tries to read key name as bool
v, ok := f[name] func (f *FieldCollection) Bool(name string) (bool, error) {
if f == nil || f.data == nil {
return false, errors.New("uninitialized field collection")
}
f.lock.RLock()
defer f.lock.RUnlock()
v, ok := f.data[name]
if !ok { if !ok {
return false, ErrValueNotSet return false, ErrValueNotSet
} }
@ -123,7 +187,15 @@ func (f FieldCollection) Bool(name string) (bool, error) {
return false, ErrValueMismatch return false, ErrValueMismatch
} }
func (f FieldCollection) Duration(name string) (time.Duration, error) { // Duration tries to read key name as time.Duration
func (f *FieldCollection) Duration(name string) (time.Duration, error) {
if f == nil || f.data == nil {
return 0, errors.New("uninitialized field collection")
}
f.lock.RLock()
defer f.lock.RUnlock()
v, err := f.String(name) v, err := f.String(name)
if err != nil { if err != nil {
return 0, errors.Wrap(err, "getting string value") return 0, errors.Wrap(err, "getting string value")
@ -133,8 +205,16 @@ func (f FieldCollection) Duration(name string) (time.Duration, error) {
return d, errors.Wrap(err, "parsing value") return d, errors.Wrap(err, "parsing value")
} }
func (f FieldCollection) Int64(name string) (int64, error) { // Int64 tries to read key name as int64
v, ok := f[name] func (f *FieldCollection) Int64(name string) (int64, error) {
if f == nil || f.data == nil {
return 0, errors.New("uninitialized field collection")
}
f.lock.RLock()
defer f.lock.RUnlock()
v, ok := f.data[name]
if !ok { if !ok {
return 0, ErrValueNotSet return 0, ErrValueNotSet
} }
@ -153,8 +233,50 @@ func (f FieldCollection) Int64(name string) (int64, error) {
return 0, ErrValueMismatch return 0, ErrValueMismatch
} }
func (f FieldCollection) String(name string) (string, error) { // Set sets a single key to specified value
v, ok := f[name] func (f *FieldCollection) Set(key string, value interface{}) {
if f == nil {
f = NewFieldCollection()
}
f.lock.Lock()
defer f.lock.Unlock()
if f.data == nil {
f.data = make(map[string]interface{})
}
f.data[key] = value
}
// SetFromData takes a map of data and copies all data into the FieldCollection
func (f *FieldCollection) SetFromData(data map[string]interface{}) {
if f == nil {
f = NewFieldCollection()
}
f.lock.Lock()
defer f.lock.Unlock()
if f.data == nil {
f.data = make(map[string]interface{})
}
for key, value := range data {
f.data[key] = value
}
}
// String tries to read key name as string
func (f *FieldCollection) String(name string) (string, error) {
if f == nil || f.data == nil {
return "", errors.New("uninitialized field collection")
}
f.lock.RLock()
defer f.lock.RUnlock()
v, ok := f.data[name]
if !ok { if !ok {
return "", ErrValueNotSet return "", ErrValueNotSet
} }
@ -170,8 +292,16 @@ func (f FieldCollection) String(name string) (string, error) {
return "", ErrValueMismatch return "", ErrValueMismatch
} }
func (f FieldCollection) StringSlice(name string) ([]string, error) { // StringSlice tries to read key name as []string
v, ok := f[name] func (f *FieldCollection) StringSlice(name string) ([]string, error) {
if f == nil || f.data == nil {
return nil, errors.New("uninitialized field collection")
}
f.lock.RLock()
defer f.lock.RUnlock()
v, ok := f.data[name]
if !ok { if !ok {
return nil, ErrValueNotSet return nil, ErrValueNotSet
} }
@ -196,3 +326,42 @@ func (f FieldCollection) StringSlice(name string) ([]string, error) {
return nil, ErrValueMismatch return nil, ErrValueMismatch
} }
// Implement JSON marshalling to plain underlying map[string]interface{}
func (f *FieldCollection) MarshalJSON() ([]byte, error) {
if f == nil || f.data == nil {
return []byte("{}"), nil
}
f.lock.RLock()
defer f.lock.RUnlock()
return json.Marshal(f.data)
}
func (f *FieldCollection) UnmarshalJSON(raw []byte) error {
data := make(map[string]interface{})
if err := json.Unmarshal(raw, &data); err != nil {
return errors.Wrap(err, "unmarshalling from JSON")
}
f.SetFromData(data)
return nil
}
// Implement YAML marshalling to plain underlying map[string]interface{}
func (f *FieldCollection) MarshalYAML() (interface{}, error) {
return f.Data(), nil
}
func (f *FieldCollection) UnmarshalYAML(unmarshal func(interface{}) error) error {
data := make(map[string]interface{})
if err := unmarshal(&data); err != nil {
return errors.Wrap(err, "unmarshalling from YAML")
}
f.SetFromData(data)
return nil
}

View file

@ -0,0 +1,80 @@
package plugins
import (
"bytes"
"encoding/json"
"strings"
"testing"
"gopkg.in/yaml.v2"
)
func TestFieldCollectionJSONMarshal(t *testing.T) {
var (
buf = new(bytes.Buffer)
raw = `{"key1":"test1","key2":"test2"}`
f = NewFieldCollection()
)
if err := json.NewDecoder(strings.NewReader(raw)).Decode(f); err != nil {
t.Fatalf("Unable to unmarshal: %s", err)
}
if err := json.NewEncoder(buf).Encode(f); err != nil {
t.Fatalf("Unable to marshal: %s", err)
}
if raw != strings.TrimSpace(buf.String()) {
t.Errorf("Marshalled JSON does not match expectation: res=%s exp=%s", buf.String(), raw)
}
}
func TestFieldCollectionYAMLMarshal(t *testing.T) {
var (
buf = new(bytes.Buffer)
raw = "key1: test1\nkey2: test2"
f = NewFieldCollection()
)
if err := yaml.NewDecoder(strings.NewReader(raw)).Decode(f); err != nil {
t.Fatalf("Unable to unmarshal: %s", err)
}
if err := yaml.NewEncoder(buf).Encode(f); err != nil {
t.Fatalf("Unable to marshal: %s", err)
}
if raw != strings.TrimSpace(buf.String()) {
t.Errorf("Marshalled YAML does not match expectation: res=%s exp=%s", buf.String(), raw)
}
}
func TestFieldCollectionNilModify(t *testing.T) {
var f *FieldCollection
f.Set("foo", "bar")
f = nil
f.SetFromData(map[string]interface{}{"foo": "bar"})
}
func TestFieldCollectionNilClone(t *testing.T) {
var f *FieldCollection
f.Clone()
}
func TestFieldCollectionNilDataGet(t *testing.T) {
var f *FieldCollection
for name, fn := range map[string]func(name string) bool{
"bool": f.CanBool,
"duration": f.CanDuration,
"int64": f.CanInt64,
"string": f.CanString,
} {
if fn("foo") {
t.Errorf("%s key is available", name)
}
}
}

View file

@ -7,7 +7,7 @@ import (
"github.com/go-irc/irc" "github.com/go-irc/irc"
) )
func DeriveChannel(m *irc.Message, evtData FieldCollection) string { func DeriveChannel(m *irc.Message, evtData *FieldCollection) string {
if m != nil && len(m.Params) > 0 && strings.HasPrefix(m.Params[0], "#") { if m != nil && len(m.Params) > 0 && strings.HasPrefix(m.Params[0], "#") {
return m.Params[0] return m.Params[0]
} }
@ -19,7 +19,7 @@ func DeriveChannel(m *irc.Message, evtData FieldCollection) string {
return "" return ""
} }
func DeriveUser(m *irc.Message, evtData FieldCollection) string { func DeriveUser(m *irc.Message, evtData *FieldCollection) string {
if m != nil && m.User != "" { if m != nil && m.User != "" {
return m.User return m.User
} }

View file

@ -10,7 +10,7 @@ import (
type ( type (
Actor interface { Actor interface {
// Execute will be called after the config was read into the Actor // Execute will be called after the config was read into the Actor
Execute(c *irc.Client, m *irc.Message, r *Rule, evtData FieldCollection, attrs FieldCollection) (preventCooldown bool, err error) Execute(c *irc.Client, m *irc.Message, r *Rule, evtData *FieldCollection, attrs *FieldCollection) (preventCooldown bool, err error)
// IsAsync may return true if the Execute function is to be executed // IsAsync may return true if the Execute function is to be executed
// in a Go routine as of long runtime. Normally it should return false // in a Go routine as of long runtime. Normally it should return false
// except in very specific cases // except in very specific cases
@ -21,7 +21,7 @@ type (
// Validate will be called to validate the loaded configuration. It should // Validate will be called to validate the loaded configuration. It should
// return an error if required keys are missing from the AttributeStore // return an error if required keys are missing from the AttributeStore
// or if keys contain broken configs // or if keys contain broken configs
Validate(FieldCollection) error Validate(*FieldCollection) error
} }
ActorCreationFunc func() Actor ActorCreationFunc func() Actor
@ -34,7 +34,7 @@ type (
LoggerCreationFunc func(moduleName string) *log.Entry LoggerCreationFunc func(moduleName string) *log.Entry
MsgFormatter func(tplString string, m *irc.Message, r *Rule, fields FieldCollection) (string, error) MsgFormatter func(tplString string, m *irc.Message, r *Rule, fields *FieldCollection) (string, error)
RawMessageHandlerFunc func(m *irc.Message) error RawMessageHandlerFunc func(m *irc.Message) error
RawMessageHandlerRegisterFunc func(RawMessageHandlerFunc) error RawMessageHandlerRegisterFunc func(RawMessageHandlerFunc) error
@ -83,10 +83,10 @@ type (
UnmarshalStoredObject([]byte) error UnmarshalStoredObject([]byte) error
} }
TemplateFuncGetter func(*irc.Message, *Rule, FieldCollection) interface{} TemplateFuncGetter func(*irc.Message, *Rule, *FieldCollection) interface{}
TemplateFuncRegister func(name string, fg TemplateFuncGetter) TemplateFuncRegister func(name string, fg TemplateFuncGetter)
) )
func GenericTemplateFunctionGetter(f interface{}) TemplateFuncGetter { func GenericTemplateFunctionGetter(f interface{}) TemplateFuncGetter {
return func(*irc.Message, *Rule, FieldCollection) interface{} { return f } return func(*irc.Message, *Rule, *FieldCollection) interface{} { return f }
} }

View file

@ -50,7 +50,7 @@ type (
RuleAction struct { RuleAction struct {
Type string `json:"type" yaml:"type,omitempty"` Type string `json:"type" yaml:"type,omitempty"`
Attributes FieldCollection `json:"attributes" yaml:"attributes,omitempty"` Attributes *FieldCollection `json:"attributes" yaml:"attributes,omitempty"`
} }
) )
@ -66,7 +66,7 @@ func (r Rule) MatcherID() string {
return fmt.Sprintf("hashstructure:%x", h) return fmt.Sprintf("hashstructure:%x", h)
} }
func (r *Rule) Matches(m *irc.Message, event *string, timerStore TimerStore, msgFormatter MsgFormatter, twitchClient *twitch.Client, eventData FieldCollection) bool { func (r *Rule) Matches(m *irc.Message, event *string, timerStore TimerStore, msgFormatter MsgFormatter, twitchClient *twitch.Client, eventData *FieldCollection) bool {
r.msgFormatter = msgFormatter r.msgFormatter = msgFormatter
r.timerStore = timerStore r.timerStore = timerStore
r.twitchClient = twitchClient r.twitchClient = twitchClient
@ -79,7 +79,7 @@ func (r *Rule) Matches(m *irc.Message, event *string, timerStore TimerStore, msg
}) })
) )
for _, matcher := range []func(*log.Entry, *irc.Message, *string, twitch.BadgeCollection, FieldCollection) bool{ for _, matcher := range []func(*log.Entry, *irc.Message, *string, twitch.BadgeCollection, *FieldCollection) bool{
r.allowExecuteDisable, r.allowExecuteDisable,
r.allowExecuteChannelWhitelist, r.allowExecuteChannelWhitelist,
r.allowExecuteUserWhitelist, r.allowExecuteUserWhitelist,
@ -117,7 +117,7 @@ func (r *Rule) GetMatchMessage() *regexp.Regexp {
return r.matchMessage return r.matchMessage
} }
func (r *Rule) SetCooldown(timerStore TimerStore, m *irc.Message, evtData FieldCollection) { func (r *Rule) SetCooldown(timerStore TimerStore, m *irc.Message, evtData *FieldCollection) {
if r.Cooldown != nil { if r.Cooldown != nil {
timerStore.AddCooldown(TimerTypeCooldown, "", r.MatcherID(), time.Now().Add(*r.Cooldown)) timerStore.AddCooldown(TimerTypeCooldown, "", r.MatcherID(), time.Now().Add(*r.Cooldown))
} }
@ -131,7 +131,7 @@ func (r *Rule) SetCooldown(timerStore TimerStore, m *irc.Message, evtData FieldC
} }
} }
func (r *Rule) allowExecuteBadgeBlacklist(logger *log.Entry, m *irc.Message, event *string, badges twitch.BadgeCollection, evtData FieldCollection) bool { func (r *Rule) allowExecuteBadgeBlacklist(logger *log.Entry, m *irc.Message, event *string, badges twitch.BadgeCollection, evtData *FieldCollection) bool {
for _, b := range r.DisableOn { for _, b := range r.DisableOn {
if badges.Has(b) { if badges.Has(b) {
logger.Tracef("Non-Match: Disable-Badge %s", b) logger.Tracef("Non-Match: Disable-Badge %s", b)
@ -142,7 +142,7 @@ func (r *Rule) allowExecuteBadgeBlacklist(logger *log.Entry, m *irc.Message, eve
return true return true
} }
func (r *Rule) allowExecuteBadgeWhitelist(logger *log.Entry, m *irc.Message, event *string, badges twitch.BadgeCollection, evtData FieldCollection) bool { func (r *Rule) allowExecuteBadgeWhitelist(logger *log.Entry, m *irc.Message, event *string, badges twitch.BadgeCollection, evtData *FieldCollection) bool {
if len(r.EnableOn) == 0 { if len(r.EnableOn) == 0 {
// No match criteria set, does not speak against matching // No match criteria set, does not speak against matching
return true return true
@ -157,7 +157,7 @@ func (r *Rule) allowExecuteBadgeWhitelist(logger *log.Entry, m *irc.Message, eve
return false return false
} }
func (r *Rule) allowExecuteChannelCooldown(logger *log.Entry, m *irc.Message, event *string, badges twitch.BadgeCollection, evtData FieldCollection) bool { func (r *Rule) allowExecuteChannelCooldown(logger *log.Entry, m *irc.Message, event *string, badges twitch.BadgeCollection, evtData *FieldCollection) bool {
if r.ChannelCooldown == nil || DeriveChannel(m, evtData) == "" { if r.ChannelCooldown == nil || DeriveChannel(m, evtData) == "" {
// No match criteria set, does not speak against matching // No match criteria set, does not speak against matching
return true return true
@ -176,7 +176,7 @@ func (r *Rule) allowExecuteChannelCooldown(logger *log.Entry, m *irc.Message, ev
return false return false
} }
func (r *Rule) allowExecuteChannelWhitelist(logger *log.Entry, m *irc.Message, event *string, badges twitch.BadgeCollection, evtData FieldCollection) bool { func (r *Rule) allowExecuteChannelWhitelist(logger *log.Entry, m *irc.Message, event *string, badges twitch.BadgeCollection, evtData *FieldCollection) bool {
if len(r.MatchChannels) == 0 { if len(r.MatchChannels) == 0 {
// No match criteria set, does not speak against matching // No match criteria set, does not speak against matching
return true return true
@ -190,7 +190,7 @@ func (r *Rule) allowExecuteChannelWhitelist(logger *log.Entry, m *irc.Message, e
return true return true
} }
func (r *Rule) allowExecuteDisable(logger *log.Entry, m *irc.Message, event *string, badges twitch.BadgeCollection, evtData FieldCollection) bool { func (r *Rule) allowExecuteDisable(logger *log.Entry, m *irc.Message, event *string, badges twitch.BadgeCollection, evtData *FieldCollection) bool {
if r.Disable == nil { if r.Disable == nil {
// No match criteria set, does not speak against matching // No match criteria set, does not speak against matching
return true return true
@ -204,7 +204,7 @@ func (r *Rule) allowExecuteDisable(logger *log.Entry, m *irc.Message, event *str
return true return true
} }
func (r *Rule) allowExecuteDisableOnOffline(logger *log.Entry, m *irc.Message, event *string, badges twitch.BadgeCollection, evtData FieldCollection) bool { func (r *Rule) allowExecuteDisableOnOffline(logger *log.Entry, m *irc.Message, event *string, badges twitch.BadgeCollection, evtData *FieldCollection) bool {
if r.DisableOnOffline == nil || !*r.DisableOnOffline || DeriveChannel(m, evtData) == "" { if r.DisableOnOffline == nil || !*r.DisableOnOffline || DeriveChannel(m, evtData) == "" {
// No match criteria set, does not speak against matching // No match criteria set, does not speak against matching
return true return true
@ -223,7 +223,7 @@ func (r *Rule) allowExecuteDisableOnOffline(logger *log.Entry, m *irc.Message, e
return true return true
} }
func (r *Rule) allowExecuteDisableOnPermit(logger *log.Entry, m *irc.Message, event *string, badges twitch.BadgeCollection, evtData FieldCollection) bool { func (r *Rule) allowExecuteDisableOnPermit(logger *log.Entry, m *irc.Message, event *string, badges twitch.BadgeCollection, evtData *FieldCollection) bool {
if r.DisableOnPermit != nil && *r.DisableOnPermit && DeriveChannel(m, evtData) != "" && r.timerStore.HasPermit(DeriveChannel(m, evtData), DeriveUser(m, evtData)) { if r.DisableOnPermit != nil && *r.DisableOnPermit && DeriveChannel(m, evtData) != "" && r.timerStore.HasPermit(DeriveChannel(m, evtData), DeriveUser(m, evtData)) {
logger.Trace("Non-Match: Permit") logger.Trace("Non-Match: Permit")
return false return false
@ -232,7 +232,7 @@ func (r *Rule) allowExecuteDisableOnPermit(logger *log.Entry, m *irc.Message, ev
return true return true
} }
func (r *Rule) allowExecuteDisableOnTemplate(logger *log.Entry, m *irc.Message, event *string, badges twitch.BadgeCollection, evtData FieldCollection) bool { func (r *Rule) allowExecuteDisableOnTemplate(logger *log.Entry, m *irc.Message, event *string, badges twitch.BadgeCollection, evtData *FieldCollection) bool {
if r.DisableOnTemplate == nil || *r.DisableOnTemplate == "" { if r.DisableOnTemplate == nil || *r.DisableOnTemplate == "" {
// No match criteria set, does not speak against matching // No match criteria set, does not speak against matching
return true return true
@ -253,7 +253,7 @@ func (r *Rule) allowExecuteDisableOnTemplate(logger *log.Entry, m *irc.Message,
return true return true
} }
func (r *Rule) allowExecuteEventWhitelist(logger *log.Entry, m *irc.Message, event *string, badges twitch.BadgeCollection, evtData FieldCollection) bool { func (r *Rule) allowExecuteEventWhitelist(logger *log.Entry, m *irc.Message, event *string, badges twitch.BadgeCollection, evtData *FieldCollection) bool {
if r.MatchEvent == nil || *r.MatchEvent == "" { if r.MatchEvent == nil || *r.MatchEvent == "" {
// No match criteria set, does not speak against matching // No match criteria set, does not speak against matching
return true return true
@ -267,7 +267,7 @@ func (r *Rule) allowExecuteEventWhitelist(logger *log.Entry, m *irc.Message, eve
return true return true
} }
func (r *Rule) allowExecuteMessageMatcherBlacklist(logger *log.Entry, m *irc.Message, event *string, badges twitch.BadgeCollection, evtData FieldCollection) bool { func (r *Rule) allowExecuteMessageMatcherBlacklist(logger *log.Entry, m *irc.Message, event *string, badges twitch.BadgeCollection, evtData *FieldCollection) bool {
if len(r.DisableOnMatchMessages) == 0 { if len(r.DisableOnMatchMessages) == 0 {
// No match criteria set, does not speak against matching // No match criteria set, does not speak against matching
return true return true
@ -296,7 +296,7 @@ func (r *Rule) allowExecuteMessageMatcherBlacklist(logger *log.Entry, m *irc.Mes
return true return true
} }
func (r *Rule) allowExecuteMessageMatcherWhitelist(logger *log.Entry, m *irc.Message, event *string, badges twitch.BadgeCollection, evtData FieldCollection) bool { func (r *Rule) allowExecuteMessageMatcherWhitelist(logger *log.Entry, m *irc.Message, event *string, badges twitch.BadgeCollection, evtData *FieldCollection) bool {
if r.MatchMessage == nil { if r.MatchMessage == nil {
// No match criteria set, does not speak against matching // No match criteria set, does not speak against matching
return true return true
@ -321,7 +321,7 @@ func (r *Rule) allowExecuteMessageMatcherWhitelist(logger *log.Entry, m *irc.Mes
return true return true
} }
func (r *Rule) allowExecuteRuleCooldown(logger *log.Entry, m *irc.Message, event *string, badges twitch.BadgeCollection, evtData FieldCollection) bool { func (r *Rule) allowExecuteRuleCooldown(logger *log.Entry, m *irc.Message, event *string, badges twitch.BadgeCollection, evtData *FieldCollection) bool {
if r.Cooldown == nil { if r.Cooldown == nil {
// No match criteria set, does not speak against matching // No match criteria set, does not speak against matching
return true return true
@ -340,7 +340,7 @@ func (r *Rule) allowExecuteRuleCooldown(logger *log.Entry, m *irc.Message, event
return false return false
} }
func (r *Rule) allowExecuteUserCooldown(logger *log.Entry, m *irc.Message, event *string, badges twitch.BadgeCollection, evtData FieldCollection) bool { func (r *Rule) allowExecuteUserCooldown(logger *log.Entry, m *irc.Message, event *string, badges twitch.BadgeCollection, evtData *FieldCollection) bool {
if r.UserCooldown == nil { if r.UserCooldown == nil {
// No match criteria set, does not speak against matching // No match criteria set, does not speak against matching
return true return true
@ -359,7 +359,7 @@ func (r *Rule) allowExecuteUserCooldown(logger *log.Entry, m *irc.Message, event
return false return false
} }
func (r *Rule) allowExecuteUserWhitelist(logger *log.Entry, m *irc.Message, event *string, badges twitch.BadgeCollection, evtData FieldCollection) bool { func (r *Rule) allowExecuteUserWhitelist(logger *log.Entry, m *irc.Message, event *string, badges twitch.BadgeCollection, evtData *FieldCollection) bool {
if len(r.MatchUsers) == 0 { if len(r.MatchUsers) == 0 {
// No match criteria set, does not speak against matching // No match criteria set, does not speak against matching
return true return true

View file

@ -139,7 +139,7 @@ func TestAllowExecuteDisableOnTemplate(t *testing.T) {
} { } {
// We don't test the message formatter here but only the disable functionality // We don't test the message formatter here but only the disable functionality
// so we fake the result of the evaluation // so we fake the result of the evaluation
r.msgFormatter = func(tplString string, m *irc.Message, r *Rule, fields FieldCollection) (string, error) { r.msgFormatter = func(tplString string, m *irc.Message, r *Rule, fields *FieldCollection) (string, error) {
return msg, nil return msg, nil
} }

View file

@ -196,10 +196,10 @@ func (t *twitchWatcher) triggerUpdate(channel string, title, category *string, o
"channel": channel, "channel": channel,
"category": *category, "category": *category,
}).Debug("Twitch metadata changed") }).Debug("Twitch metadata changed")
go handleMessage(ircHdl.Client(), nil, eventTypeTwitchCategoryUpdate, plugins.FieldCollection{ go handleMessage(ircHdl.Client(), nil, eventTypeTwitchCategoryUpdate, plugins.FieldCollectionFromData(map[string]interface{}{
"channel": channel, "channel": channel,
"category": *category, "category": *category,
}) }))
} }
if title != nil && t.ChannelStatus[channel].Title != *title { if title != nil && t.ChannelStatus[channel].Title != *title {
@ -208,10 +208,10 @@ func (t *twitchWatcher) triggerUpdate(channel string, title, category *string, o
"channel": channel, "channel": channel,
"title": *title, "title": *title,
}).Debug("Twitch metadata changed") }).Debug("Twitch metadata changed")
go handleMessage(ircHdl.Client(), nil, eventTypeTwitchTitleUpdate, plugins.FieldCollection{ go handleMessage(ircHdl.Client(), nil, eventTypeTwitchTitleUpdate, plugins.FieldCollectionFromData(map[string]interface{}{
"channel": channel, "channel": channel,
"title": *title, "title": *title,
}) }))
} }
if online != nil && t.ChannelStatus[channel].IsLive != *online { if online != nil && t.ChannelStatus[channel].IsLive != *online {
@ -226,8 +226,8 @@ func (t *twitchWatcher) triggerUpdate(channel string, title, category *string, o
evt = eventTypeTwitchStreamOffline evt = eventTypeTwitchStreamOffline
} }
go handleMessage(ircHdl.Client(), nil, evt, plugins.FieldCollection{ go handleMessage(ircHdl.Client(), nil, evt, plugins.FieldCollectionFromData(map[string]interface{}{
"channel": channel, "channel": channel,
}) }))
} }
} }