diff --git a/automessage.go b/automessage.go new file mode 100644 index 0000000..ba1afca --- /dev/null +++ b/automessage.go @@ -0,0 +1,132 @@ +package main + +import ( + "crypto/sha256" + "fmt" + "strings" + "sync" + "time" + + "github.com/go-irc/irc" + "github.com/pkg/errors" + "github.com/robfig/cron/v3" + log "github.com/sirupsen/logrus" +) + +var cronParser = cron.NewParser(cron.SecondOptional | cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow) + +type autoMessage struct { + Channel string `yaml:"channel"` + Message string `yaml:"message"` + UseAction bool `yaml:"use_action"` + + Cron string `yaml:"cron"` + MessageInterval int64 `yaml:"message_interval"` + OnlyOnLive bool `yaml:"only_on_live"` + TimeInterval time.Duration `yaml:"time_interval"` + + disabled bool + lastMessageSent time.Time + linesSinceLastMessage int64 + + lock sync.RWMutex +} + +func (a *autoMessage) CanSend() bool { + if a.disabled || !a.IsValid() { + return false + } + + a.lock.RLock() + defer a.lock.RUnlock() + + switch { + case a.MessageInterval > a.linesSinceLastMessage: + // Not enough chatted lines + return false + + case a.TimeInterval > 0 && a.lastMessageSent.Add(a.TimeInterval).After(time.Now()): + // Simple timer is not yet expired + return false + + case a.Cron != "": + sched, _ := cronParser.Parse(a.Cron) + if sched.Next(a.lastMessageSent).After(time.Now()) { + // Cron timer is not yet expired + return false + } + } + + if a.OnlyOnLive { + streamLive, err := twitch.HasLiveStream(strings.TrimLeft(a.Channel, "#")) + if err != nil { + log.WithError(err).Error("Unable to determine channel live status") + return false + } + if !streamLive { + return false + } + } + + return true +} + +func (a *autoMessage) CountMessage(channel string) { + if strings.TrimLeft(channel, "#") != strings.TrimLeft(a.Channel, "#") { + return + } + + a.lock.Lock() + defer a.lock.Unlock() + + a.linesSinceLastMessage++ +} + +func (a *autoMessage) ID() string { + sum := sha256.New() + + fmt.Fprintf(sum, "channel:%q", a.Channel) + fmt.Fprintf(sum, "message:%q", a.Message) + fmt.Fprintf(sum, "action:%v", a.UseAction) + + return fmt.Sprintf("sha256:%x", sum.Sum(nil)) +} + +func (a *autoMessage) IsValid() bool { + if a.Cron != "" { + if _, err := cronParser.Parse(a.Cron); err != nil { + return false + } + } + + if a.MessageInterval == 0 && a.TimeInterval == 0 && a.Cron == "" { + return false + } + + return true +} + +func (a *autoMessage) Send(c *irc.Client) error { + a.lock.Lock() + defer a.lock.Unlock() + + msg := a.Message + if a.UseAction { + msg = fmt.Sprintf("\001ACTION %s\001", msg) + } + + if err := c.WriteMessage(&irc.Message{ + Command: "PRIVMSG", + Params: []string{ + fmt.Sprintf("#%s", strings.TrimLeft(a.Channel, "#")), + msg, + }, + }); err != nil { + return errors.Wrap(err, "sending auto-message") + } + + a.lastMessageSent = time.Now() + a.linesSinceLastMessage = 0 + + return nil +} diff --git a/config.go b/config.go index 34eb99b..48fdd71 100644 --- a/config.go +++ b/config.go @@ -17,10 +17,11 @@ import ( ) type configFile struct { - Channels []string `yaml:"channels"` - PermitAllowModerator bool `yaml:"permit_allow_moderator"` - PermitTimeout time.Duration `yaml:"permit_timeout"` - Rules []*rule `yaml:"rules"` + AutoMessages []*autoMessage `yaml:"auto_messages"` + Channels []string `yaml:"channels"` + PermitAllowModerator bool `yaml:"permit_allow_moderator"` + PermitTimeout time.Duration `yaml:"permit_timeout"` + Rules []*rule `yaml:"rules"` } func newConfigFile() configFile { @@ -236,6 +237,35 @@ func loadConfig(filename string) error { log.Warn("Loaded config with empty ruleset") } + for idx, nam := range tmpConfig.AutoMessages { + // By default assume last message to be sent now + // in order not to spam messages at startup + nam.lastMessageSent = time.Now() + + if !nam.IsValid() { + log.WithField("index", idx).Warn("Auto-Message configuration is invalid and therefore disabled") + } + + if config == nil { + // Initial config load, do not update timers + continue + } + + for _, oam := range config.AutoMessages { + if nam.ID() != oam.ID() { + continue + } + + // We disable the old message as executing it would + // mess up the constraints of the new message + oam.lock.Lock() + oam.disabled = true + + nam.lastMessageSent = oam.lastMessageSent + nam.linesSinceLastMessage = oam.linesSinceLastMessage + } + } + configLock.Lock() defer configLock.Unlock() diff --git a/go.mod b/go.mod index ca9423c..3b3b62f 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,7 @@ require ( github.com/go-irc/irc v2.1.0+incompatible github.com/gologme/log v1.2.0 github.com/pkg/errors v0.9.1 + github.com/robfig/cron/v3 v3.0.1 github.com/sirupsen/logrus v1.7.0 gopkg.in/fsnotify.v1 v1.4.7 gopkg.in/yaml.v2 v2.2.2 diff --git a/go.sum b/go.sum index b07443e..685397d 100644 --- a/go.sum +++ b/go.sum @@ -83,6 +83,8 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= +github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= +github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk= github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= diff --git a/irc.go b/irc.go index 42246a2..f43edb2 100644 --- a/irc.go +++ b/irc.go @@ -167,6 +167,15 @@ func (i ircHandler) handleTwitchPrivmsg(m *irc.Message) { "trailing": m.Trailing(), }).Trace("Received privmsg") + if m.User != i.user { + // Count messages from other users than self + configLock.RLock() + for _, am := range config.AutoMessages { + am.CountMessage(m.Params[0]) + } + configLock.RUnlock() + } + if strings.HasPrefix(m.Trailing(), "!permit") { i.handlePermit(m) return diff --git a/main.go b/main.go index 1847f1b..3daf39b 100644 --- a/main.go +++ b/main.go @@ -70,8 +70,9 @@ func main() { } var ( - irc *ircHandler - ircDisconnected = make(chan struct{}, 1) + irc *ircHandler + ircDisconnected = make(chan struct{}, 1) + autoMessageTicker = time.NewTicker(time.Second) ) ircDisconnected <- struct{}{} @@ -110,6 +111,19 @@ func main() { log.Info("Config file reloaded") + case <-autoMessageTicker.C: + configLock.RLock() + for _, am := range config.AutoMessages { + if !am.CanSend() { + continue + } + + if err := am.Send(irc.c); err != nil { + log.WithError(err).Error("Unable to send automated message") + } + } + configLock.RUnlock() + } } }