package main import ( "errors" "fmt" "sync" "github.com/Luzifer/go_helpers/fieldcollection" log "github.com/sirupsen/logrus" "gopkg.in/irc.v4" "github.com/Luzifer/twitch-bot/v3/internal/locker" "github.com/Luzifer/twitch-bot/v3/pkg/event" "github.com/Luzifer/twitch-bot/v3/plugins" ) var ( availableActions = make(map[string]plugins.ActorCreationFunc) availableActionsLock = new(sync.RWMutex) ) // Compile-time assertion var _ plugins.ActorRegistrationFunc = registerAction func getActorByName(name string) (plugins.Actor, error) { availableActionsLock.RLock() defer availableActionsLock.RUnlock() acf, ok := availableActions[name] if !ok { return nil, fmt.Errorf("undefined actor %q called", name) } return acf(), nil } func registerAction(name string, acf plugins.ActorCreationFunc) { availableActionsLock.Lock() defer availableActionsLock.Unlock() if _, ok := availableActions[name]; ok { log.WithField("name", name).Fatal("Duplicate registration of actor") } availableActions[name] = acf } func triggerAction(c *irc.Client, m *irc.Message, rule *plugins.Rule, ra *plugins.RuleAction, eventData *fieldcollection.FieldCollection) (preventCooldown bool, err error) { availableActionsLock.RLock() defer availableActionsLock.RUnlock() a, err := getActorByName(ra.Type) if err != nil { return false, fmt.Errorf("getting actor: %w", err) } logger := log.WithField("actor", a.Name()) if a.IsAsync() { go func() { if _, err := a.Execute(c, m, rule, eventData, ra.Attributes); err != nil { logger.WithError(err).Error("Error in async actor") } }() return preventCooldown, nil } apc, err := a.Execute(c, m, rule, eventData, ra.Attributes) if err != nil { return apc, fmt.Errorf("execute action: %w", err) } return apc, nil } func handleMessage(c *irc.Client, m *irc.Message, evt *string, eventData *fieldcollection.FieldCollection) { // Send events to registered handlers if evt != nil { go notifyEventHandlers(*evt, eventData) } matchingRules := config.GetMatchingRules(m, evt, eventData) for i := range matchingRules { go handleMessageRuleExecution(c, m, matchingRules[i], eventData) } } func handleMessageRuleExecution(c *irc.Client, m *irc.Message, r *plugins.Rule, eventData *fieldcollection.FieldCollection) { lockKey := r.CooldownLockKey(m, eventData) locker.LockByKey(lockKey) defer locker.UnlockByKey(lockKey) if !r.CanExecuteCooldowns(m, timerService, eventData) { return } var ( ruleEventData = fieldcollection.NewFieldCollection() executionError bool preventCooldown bool ) if eventData != nil { ruleEventData.SetFromData(eventData.Data()) } ActionsLoop: for _, a := range r.Actions { apc, err := triggerAction(c, m, r, a, ruleEventData) switch { case err == nil: // Rule execution did not cause an error, we store the // cooldown modifier and continue preventCooldown = preventCooldown || apc continue ActionsLoop case errors.Is(err, plugins.ErrStopRuleExecution): // Action has asked to stop executing this rule so we store // the cooldown modifier and stop executing the actions stack // Action experienced an error: We don't store the cooldown // state of this action and stop executing the actions stack // for this rule preventCooldown = preventCooldown || apc break ActionsLoop default: // Break execution for this rule when one action fails // Lock command executionError = true log.WithError(err).Error("Unable to trigger action") break ActionsLoop } } if !preventCooldown && !executionError { r.SetCooldown(timerService, m, eventData) } } func handleTypedMessage(c *irc.Client, m *irc.Message, evt event.Event) { var evtType *string if evt != nil { evtType = evt.Event() } handleMessage(c, m, evtType, event.ToFieldCollection(evt)) }