diff --git a/README.md b/README.md index fcefed4..13d766f 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,7 @@ Supported sub-commands are: actor-docs Generate markdown documentation for available actors api-token Generate an api-token to be entered into the config migrate-v2 Migrate old (*.json.gz) storage file into new database + validate-config Try to load configuration file and report errors if any help Prints this help message ``` diff --git a/action_script.go b/action_script.go index ef3ba98..3cb5478 100644 --- a/action_script.go +++ b/action_script.go @@ -124,10 +124,17 @@ func (a ActorScript) Execute(c *irc.Client, m *irc.Message, r *plugins.Rule, eve func (a ActorScript) IsAsync() bool { return false } func (a ActorScript) Name() string { return "script" } -func (a ActorScript) Validate(attrs *plugins.FieldCollection) (err error) { - if cmd, err := attrs.StringSlice("command"); err != nil || len(cmd) == 0 { +func (a ActorScript) Validate(tplValidator plugins.TemplateValidatorFunc, attrs *plugins.FieldCollection) (err error) { + cmd, err := attrs.StringSlice("command") + if err != nil || len(cmd) == 0 { return errors.New("command must be slice of strings with length > 0") } + for i, el := range cmd { + if err = tplValidator(el); err != nil { + return errors.Wrapf(err, "validating cmd template (element %d)", i) + } + } + return nil } diff --git a/config.go b/config.go index 59c1ec8..9040888 100644 --- a/config.go +++ b/config.go @@ -357,21 +357,34 @@ func (c *configFile) updateAutoMessagesFromConfig(old *configFile) { } func (c configFile) validateRuleActions() error { + var hasError bool + for _, r := range c.Rules { logger := log.WithField("rule", r.MatcherID()) + + if err := r.Validate(validateTemplate); err != nil { + logger.WithError(err).Error("Rule reported invalid config") + hasError = true + } + for idx, a := range r.Actions { actor, err := getActorByName(a.Type) if err != nil { logger.WithField("index", idx).WithError(err).Error("Cannot get actor by type") - return errors.Wrap(err, "getting actor by type") + hasError = true + continue } - if err = actor.Validate(a.Attributes); err != nil { + if err = actor.Validate(validateTemplate, a.Attributes); err != nil { logger.WithField("index", idx).WithError(err).Error("Actor reported invalid config") - return errors.Wrap(err, "validating action attributes") + hasError = true } } } + if hasError { + return errors.New("config validation reported errors, see log") + } + return nil } diff --git a/internal/actors/ban/actor.go b/internal/actors/ban/actor.go index 77101d8..5c44c1a 100644 --- a/internal/actors/ban/actor.go +++ b/internal/actors/ban/actor.go @@ -103,7 +103,18 @@ func (a actor) Execute(c *irc.Client, m *irc.Message, r *plugins.Rule, eventData func (a actor) IsAsync() bool { return false } func (a actor) Name() string { return actorName } -func (a actor) Validate(attrs *plugins.FieldCollection) (err error) { return nil } +func (a actor) Validate(tplValidator plugins.TemplateValidatorFunc, attrs *plugins.FieldCollection) (err error) { + reasonTemplate, err := attrs.String("reason") + if err != nil || reasonTemplate == "" { + return errors.New("reason must be non-empty string") + } + + if err = tplValidator(reasonTemplate); err != nil { + return errors.Wrap(err, "validating reason template") + } + + return nil +} func handleAPIBan(w http.ResponseWriter, r *http.Request) { var ( diff --git a/internal/actors/counter/actor.go b/internal/actors/counter/actor.go index bbc13c7..3348d5e 100644 --- a/internal/actors/counter/actor.go +++ b/internal/actors/counter/actor.go @@ -187,11 +187,17 @@ func (a ActorCounter) Execute(c *irc.Client, m *irc.Message, r *plugins.Rule, ev func (a ActorCounter) IsAsync() bool { return false } func (a ActorCounter) Name() string { return "counter" } -func (a ActorCounter) Validate(attrs *plugins.FieldCollection) (err error) { +func (a ActorCounter) Validate(tplValidator plugins.TemplateValidatorFunc, attrs *plugins.FieldCollection) (err error) { if cn, err := attrs.String("counter"); err != nil || cn == "" { return errors.New("counter name must be non-empty string") } + for _, field := range []string{"counter", "counter_step", "counter_set"} { + if err = tplValidator(attrs.MustString(field, ptrStringEmpty)); err != nil { + return errors.Wrapf(err, "validating %s template", field) + } + } + return nil } diff --git a/internal/actors/delay/actor.go b/internal/actors/delay/actor.go index 45e6d70..7b9218d 100644 --- a/internal/actors/delay/actor.go +++ b/internal/actors/delay/actor.go @@ -69,4 +69,6 @@ func (a actor) Execute(c *irc.Client, m *irc.Message, r *plugins.Rule, eventData func (a actor) IsAsync() bool { return false } func (a actor) Name() string { return actorName } -func (a actor) Validate(attrs *plugins.FieldCollection) (err error) { return nil } +func (a actor) Validate(plugins.TemplateValidatorFunc, *plugins.FieldCollection) (err error) { + return nil +} diff --git a/internal/actors/delete/actor.go b/internal/actors/delete/actor.go index 120fb2e..147faf0 100644 --- a/internal/actors/delete/actor.go +++ b/internal/actors/delete/actor.go @@ -46,4 +46,6 @@ func (a actor) Execute(c *irc.Client, m *irc.Message, r *plugins.Rule, eventData func (a actor) IsAsync() bool { return false } func (a actor) Name() string { return actorName } -func (a actor) Validate(attrs *plugins.FieldCollection) (err error) { return nil } +func (a actor) Validate(plugins.TemplateValidatorFunc, *plugins.FieldCollection) (err error) { + return nil +} diff --git a/internal/actors/filesay/actor.go b/internal/actors/filesay/actor.go index fd470b2..7b140ca 100644 --- a/internal/actors/filesay/actor.go +++ b/internal/actors/filesay/actor.go @@ -106,10 +106,15 @@ func (a actor) Execute(c *irc.Client, m *irc.Message, r *plugins.Rule, eventData func (a actor) IsAsync() bool { return true } func (a actor) Name() string { return actorName } -func (a actor) Validate(attrs *plugins.FieldCollection) error { - if v, err := attrs.String("source"); err != nil || v == "" { +func (a actor) Validate(tplValidator plugins.TemplateValidatorFunc, attrs *plugins.FieldCollection) error { + sourceTpl, err := attrs.String("source") + if err != nil || sourceTpl == "" { return errors.New("source is expected to be non-empty string") } + if err = tplValidator(sourceTpl); err != nil { + return errors.Wrap(err, "validating source template") + } + return nil } diff --git a/internal/actors/modchannel/actor.go b/internal/actors/modchannel/actor.go index 3acb55a..5d75e30 100644 --- a/internal/actors/modchannel/actor.go +++ b/internal/actors/modchannel/actor.go @@ -16,6 +16,8 @@ const actorName = "modchannel" var ( formatMessage plugins.MsgFormatter tcGetter func(string) (*twitch.Client, error) + + ptrStringEmpty = func(s string) *string { return &s }("") ) func Register(args plugins.RegistrationArguments) error { @@ -67,9 +69,8 @@ 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) { var ( - ptrStringEmpty = func(v string) *string { return &v }("") - game = attrs.MustString("game", ptrStringEmpty) - title = attrs.MustString("title", ptrStringEmpty) + game = attrs.MustString("game", ptrStringEmpty) + title = attrs.MustString("title", ptrStringEmpty) ) if game == "" && title == "" { @@ -115,10 +116,16 @@ func (a actor) Execute(c *irc.Client, m *irc.Message, r *plugins.Rule, eventData func (a actor) IsAsync() bool { return false } func (a actor) Name() string { return actorName } -func (a actor) Validate(attrs *plugins.FieldCollection) (err error) { +func (a actor) Validate(tplValidator plugins.TemplateValidatorFunc, attrs *plugins.FieldCollection) (err error) { if v, err := attrs.String("channel"); err != nil || v == "" { return errors.New("channel must be non-empty string") } + for _, field := range []string{"channel", "game", "title"} { + if err = tplValidator(attrs.MustString(field, ptrStringEmpty)); err != nil { + return errors.Wrapf(err, "validating %s template", field) + } + } + return nil } diff --git a/internal/actors/nuke/actor.go b/internal/actors/nuke/actor.go index d92302b..c49f76c 100644 --- a/internal/actors/nuke/actor.go +++ b/internal/actors/nuke/actor.go @@ -28,6 +28,7 @@ var ( messageStoreLock sync.RWMutex ptrStringDelete = func(v string) *string { return &v }("delete") + ptrStringEmpty = func(s string) *string { return &s }("") ptrString10m = func(v string) *string { return &v }("10m") ) @@ -230,10 +231,16 @@ func (a actor) Execute(c *irc.Client, m *irc.Message, r *plugins.Rule, eventData func (a actor) IsAsync() bool { return false } func (a actor) Name() string { return actorName } -func (a actor) Validate(attrs *plugins.FieldCollection) (err error) { +func (a actor) Validate(tplValidator plugins.TemplateValidatorFunc, attrs *plugins.FieldCollection) (err error) { if v, err := attrs.String("match"); err != nil || v == "" { return errors.New("match must be non-empty string") } + for _, field := range []string{"scan", "action", "match"} { + if err = tplValidator(attrs.MustString(field, ptrStringEmpty)); err != nil { + return errors.Wrapf(err, "validating %s template", field) + } + } + return nil } diff --git a/internal/actors/punish/actor.go b/internal/actors/punish/actor.go index 31a29ef..a073c90 100644 --- a/internal/actors/punish/actor.go +++ b/internal/actors/punish/actor.go @@ -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) Name() string { return actorNamePunish } -func (a actorPunish) Validate(attrs *plugins.FieldCollection) (err error) { +func (a actorPunish) Validate(tplValidator plugins.TemplateValidatorFunc, attrs *plugins.FieldCollection) (err error) { if v, err := attrs.String("user"); err != nil || v == "" { return errors.New("user must be non-empty string") } @@ -223,6 +223,10 @@ func (a actorPunish) Validate(attrs *plugins.FieldCollection) (err error) { return errors.New("levels must be slice of strings with length > 0") } + if err = tplValidator(attrs.MustString("user", ptrStringEmpty)); err != nil { + return errors.Wrap(err, "validating user template") + } + return nil } @@ -247,10 +251,14 @@ func (a actorResetPunish) Execute(c *irc.Client, m *irc.Message, r *plugins.Rule func (a actorResetPunish) IsAsync() bool { return false } func (a actorResetPunish) Name() string { return actorNameResetPunish } -func (a actorResetPunish) Validate(attrs *plugins.FieldCollection) (err error) { +func (a actorResetPunish) Validate(tplValidator plugins.TemplateValidatorFunc, attrs *plugins.FieldCollection) (err error) { if v, err := attrs.String("user"); err != nil || v == "" { return errors.New("user must be non-empty string") } + if err = tplValidator(attrs.MustString("user", ptrStringEmpty)); err != nil { + return errors.Wrap(err, "validating user template") + } + return nil } diff --git a/internal/actors/quotedb/actor.go b/internal/actors/quotedb/actor.go index 3ec52d8..5ea65fe 100644 --- a/internal/actors/quotedb/actor.go +++ b/internal/actors/quotedb/actor.go @@ -173,7 +173,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) Name() string { return actorName } -func (a actor) Validate(attrs *plugins.FieldCollection) (err error) { +func (a actor) Validate(tplValidator plugins.TemplateValidatorFunc, attrs *plugins.FieldCollection) (err error) { action := attrs.MustString("action", ptrStringEmpty) switch action { @@ -194,5 +194,11 @@ func (a actor) Validate(attrs *plugins.FieldCollection) (err error) { return errors.New("action must be one of add, del or get") } + for _, field := range []string{"index", "quote", "format"} { + if err = tplValidator(attrs.MustString(field, ptrStringEmpty)); err != nil { + return errors.Wrapf(err, "validating %s template", field) + } + } + return nil } diff --git a/internal/actors/raw/actor.go b/internal/actors/raw/actor.go index 3f4a76c..234c6d4 100644 --- a/internal/actors/raw/actor.go +++ b/internal/actors/raw/actor.go @@ -12,6 +12,8 @@ const actorName = "raw" var ( formatMessage plugins.MsgFormatter send plugins.SendMessageFunc + + ptrStringEmpty = func(s string) *string { return &s }("") ) func Register(args plugins.RegistrationArguments) error { @@ -63,10 +65,14 @@ func (a actor) Execute(c *irc.Client, m *irc.Message, r *plugins.Rule, eventData func (a actor) IsAsync() bool { return false } func (a actor) Name() string { return actorName } -func (a actor) Validate(attrs *plugins.FieldCollection) (err error) { +func (a actor) Validate(tplValidator plugins.TemplateValidatorFunc, attrs *plugins.FieldCollection) (err error) { if v, err := attrs.String("message"); err != nil || v == "" { return errors.New("message must be non-empty string") } + if err = tplValidator(attrs.MustString("message", ptrStringEmpty)); err != nil { + return errors.Wrap(err, "validating message template") + } + return nil } diff --git a/internal/actors/respond/actor.go b/internal/actors/respond/actor.go index 7869c52..d23c3af 100644 --- a/internal/actors/respond/actor.go +++ b/internal/actors/respond/actor.go @@ -17,7 +17,8 @@ var ( formatMessage plugins.MsgFormatter send plugins.SendMessageFunc - ptrBoolFalse = func(v bool) *bool { return &v }(false) + ptrBoolFalse = func(v bool) *bool { return &v }(false) + ptrStringEmpty = func(s string) *string { return &s }("") ) func Register(args plugins.RegistrationArguments) error { @@ -121,10 +122,16 @@ func (a actor) Execute(c *irc.Client, m *irc.Message, r *plugins.Rule, eventData func (a actor) IsAsync() bool { return false } func (a actor) Name() string { return actorName } -func (a actor) Validate(attrs *plugins.FieldCollection) (err error) { +func (a actor) Validate(tplValidator plugins.TemplateValidatorFunc, attrs *plugins.FieldCollection) (err error) { if v, err := attrs.String("message"); err != nil || v == "" { return errors.New("message must be non-empty string") } + for _, field := range []string{"message", "fallback"} { + if err = tplValidator(attrs.MustString(field, ptrStringEmpty)); err != nil { + return errors.Wrapf(err, "validating %s template", field) + } + } + return nil } diff --git a/internal/actors/timeout/actor.go b/internal/actors/timeout/actor.go index f945c6a..94e93d7 100644 --- a/internal/actors/timeout/actor.go +++ b/internal/actors/timeout/actor.go @@ -82,11 +82,19 @@ func (a actor) Execute(c *irc.Client, m *irc.Message, r *plugins.Rule, eventData func (a actor) IsAsync() bool { return false } func (a actor) Name() string { return actorName } -func (a actor) Validate(attrs *plugins.FieldCollection) (err error) { +func (a actor) Validate(tplValidator plugins.TemplateValidatorFunc, attrs *plugins.FieldCollection) (err error) { 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") } + if v, err := attrs.String("reason"); err != nil || v == "" { + return errors.New("reason must be non-empty string") + } + + if err = tplValidator(attrs.MustString("reason", ptrStringEmpty)); err != nil { + return errors.Wrap(err, "validating reason template") + } + return nil } diff --git a/internal/actors/variables/actor.go b/internal/actors/variables/actor.go index 40d91d3..16dcc18 100644 --- a/internal/actors/variables/actor.go +++ b/internal/actors/variables/actor.go @@ -150,11 +150,17 @@ func (a ActorSetVariable) Execute(c *irc.Client, m *irc.Message, r *plugins.Rule func (a ActorSetVariable) IsAsync() bool { return false } func (a ActorSetVariable) Name() string { return "setvariable" } -func (a ActorSetVariable) Validate(attrs *plugins.FieldCollection) (err error) { +func (a ActorSetVariable) Validate(tplValidator plugins.TemplateValidatorFunc, attrs *plugins.FieldCollection) (err error) { if v, err := attrs.String("variable"); err != nil || v == "" { return errors.New("variable name must be non-empty string") } + for _, field := range []string{"set", "variable"} { + if err = tplValidator(attrs.MustString(field, ptrStringEmpty)); err != nil { + return errors.Wrapf(err, "validating %s template", field) + } + } + return nil } diff --git a/internal/actors/whisper/actor.go b/internal/actors/whisper/actor.go index 09b79d9..1042f82 100644 --- a/internal/actors/whisper/actor.go +++ b/internal/actors/whisper/actor.go @@ -13,6 +13,8 @@ const actorName = "whisper" var ( botTwitchClient *twitch.Client formatMessage plugins.MsgFormatter + + ptrStringEmpty = func(s string) *string { return &s }("") ) func Register(args plugins.RegistrationArguments) error { @@ -73,7 +75,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) Name() string { return actorName } -func (a actor) Validate(attrs *plugins.FieldCollection) (err error) { +func (a actor) Validate(tplValidator plugins.TemplateValidatorFunc, attrs *plugins.FieldCollection) (err error) { if v, err := attrs.String("to"); err != nil || v == "" { return errors.New("to must be non-empty string") } @@ -82,5 +84,11 @@ func (a actor) Validate(attrs *plugins.FieldCollection) (err error) { return errors.New("message must be non-empty string") } + for _, field := range []string{"message", "to"} { + if err = tplValidator(attrs.MustString(field, ptrStringEmpty)); err != nil { + return errors.Wrapf(err, "validating %s template", field) + } + } + return nil } diff --git a/internal/apimodules/customevent/actor.go b/internal/apimodules/customevent/actor.go index 423cecb..041dc63 100644 --- a/internal/apimodules/customevent/actor.go +++ b/internal/apimodules/customevent/actor.go @@ -13,8 +13,6 @@ import ( 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) { - ptrStringEmpty := func(v string) *string { return &v }("") - fd, err := formatMessage(attrs.MustString("fields", ptrStringEmpty), m, r, eventData) if err != nil { return false, errors.Wrap(err, "executing fields template") @@ -51,10 +49,16 @@ func (a actor) Execute(c *irc.Client, m *irc.Message, r *plugins.Rule, eventData func (a actor) IsAsync() bool { return false } func (a actor) Name() string { return actorName } -func (a actor) Validate(attrs *plugins.FieldCollection) (err error) { +func (a actor) Validate(tplValidator plugins.TemplateValidatorFunc, attrs *plugins.FieldCollection) (err error) { if v, err := attrs.String("fields"); err != nil || v == "" { return errors.New("fields is expected to be non-empty string") } + for _, field := range []string{"fields", "schedule_in"} { + if err = tplValidator(attrs.MustString(field, ptrStringEmpty)); err != nil { + return errors.Wrapf(err, "validating %s template", field) + } + } + return nil } diff --git a/internal/apimodules/customevent/customevent.go b/internal/apimodules/customevent/customevent.go index 68dde81..cbc4107 100644 --- a/internal/apimodules/customevent/customevent.go +++ b/internal/apimodules/customevent/customevent.go @@ -21,6 +21,8 @@ var ( eventCreatorFunc plugins.EventHandlerFunc formatMessage plugins.MsgFormatter mc *memoryCache + + ptrStringEmpty = func(s string) *string { return &s }("") ) func Register(args plugins.RegistrationArguments) error { diff --git a/main.go b/main.go index a6225f9..1ac1b24 100644 --- a/main.go +++ b/main.go @@ -179,6 +179,7 @@ func handleSubCommand(args []string) { fmt.Println(" actor-docs Generate markdown documentation for available actors") fmt.Println(" api-token Generate an api-token to be entered into the config") fmt.Println(" migrate-v2 Migrate old (*.json.gz) storage file into new database") + fmt.Println(" validate-config Try to load configuration file and report errors if any") fmt.Println(" help Prints this help message") case "migrate-v2": @@ -197,6 +198,11 @@ func handleSubCommand(args []string) { log.Info("v2 storage file was migrated") + case "validate-config": + if err := loadConfig(cfg.Config); err != nil { + log.WithError(err).Fatal("loading config") + } + default: handleSubCommand([]string{"help"}) log.Fatalf("Unknown sub-command %q", args[0]) diff --git a/msgformatter.go b/msgformatter.go index 70d1806..b9739d4 100644 --- a/msgformatter.go +++ b/msgformatter.go @@ -86,3 +86,14 @@ func formatMessageFieldUserID(compiledFields *plugins.FieldCollection, m *irc.Me func formatMessageFieldUsername(compiledFields *plugins.FieldCollection, m *irc.Message, fields *plugins.FieldCollection) { compiledFields.Set("username", plugins.DeriveUser(m, fields)) } + +func validateTemplate(tplString string) error { + // Template in frontend supports newlines, messages do not + tplString = stripNewline.ReplaceAllString(tplString, " ") + + _, err := template. + New(tplString). + Funcs(tplFuncs.GetFuncMap(nil, nil, plugins.NewFieldCollection())). + Parse(tplString) + return errors.Wrap(err, "parsing template") +} diff --git a/plugins/interface.go b/plugins/interface.go index 387b414..63930f4 100644 --- a/plugins/interface.go +++ b/plugins/interface.go @@ -24,7 +24,7 @@ type ( // Validate will be called to validate the loaded configuration. It should // return an error if required keys are missing from the AttributeStore // or if keys contain broken configs - Validate(*FieldCollection) error + Validate(TemplateValidatorFunc, *FieldCollection) error } ActorCreationFunc func() Actor @@ -105,6 +105,8 @@ type ( TemplateFuncGetter func(*irc.Message, *Rule, *FieldCollection) interface{} TemplateFuncRegister func(name string, fg TemplateFuncGetter) + TemplateValidatorFunc func(raw string) error + ValidateTokenFunc func(token string, modules ...string) error ) diff --git a/plugins/rule.go b/plugins/rule.go index 477e40c..963ae1e 100644 --- a/plugins/rule.go +++ b/plugins/rule.go @@ -193,6 +193,22 @@ func (r *Rule) UpdateFromSubscription() (bool, error) { return true, nil } +func (r Rule) Validate(tplValidate TemplateValidatorFunc) error { + if r.MatchMessage != nil { + if _, err := regexp.Compile(*r.MatchMessage); err != nil { + return errors.Wrap(err, "compiling match_message field regex") + } + } + + if r.DisableOnTemplate != nil { + if err := tplValidate(*r.DisableOnTemplate); err != nil { + return errors.Wrap(err, "parsing disable_on_template template") + } + } + + return nil +} + func (r *Rule) allowExecuteBadgeBlacklist(logger *log.Entry, m *irc.Message, event *string, badges twitch.BadgeCollection, evtData *FieldCollection) bool { for _, b := range r.DisableOn { if badges.Has(b) {