// © 2013 the CatBase Authors under the WTFPL. See AUTHORS for the list of authors. package first import ( "fmt" bh "github.com/timshannon/bolthold" "regexp" "strings" "time" "github.com/rs/zerolog/log" "github.com/velour/catbase/config" "github.com/velour/catbase/bot" "github.com/velour/catbase/bot/msg" ) // This is a first plugin to serve as an example and quick copy/paste for new plugins. type FirstPlugin struct { bot bot.Bot config *config.Config store *bh.Store handlers bot.HandlerTable enabled bool } type FirstEntry struct { ID uint64 `boltholdKey:"ID"` Day time.Time Time time.Time Channel string Body string Nick string Saved bool } // Insert or update the first entry func (fe *FirstEntry) save(store *bh.Store) error { return store.Insert(bh.NextSequence(), fe) } func (fe *FirstEntry) delete(store *bh.Store) error { return store.Delete(fe.ID, FirstEntry{}) } // NewFirstPlugin creates a new FirstPlugin with the Plugin interface func New(b bot.Bot) *FirstPlugin { log.Info().Msgf("First plugin initialized with Day: %s", Midnight(time.Now())) fp := &FirstPlugin{ bot: b, config: b.Config(), store: b.Store(), enabled: true, } fp.register() b.Register(fp, bot.Help, fp.help) return fp } func getLastFirst(store *bh.Store, channel string) (*FirstEntry, error) { fe := &FirstEntry{} err := store.FindOne(fe, bh.Where("Channel").Eq(channel)) if err != nil { return nil, err } log.Debug().Msgf("ID: %v Day %v Time %v Body %v Nick %v", fe) return fe, nil } func Midnight(t time.Time) time.Time { y, m, d := t.Date() return time.Date(y, m, d, 0, 0, 0, 0, time.Local) } func (p *FirstPlugin) isNotToday(f *FirstEntry) bool { if f == nil { return true } t := f.Time t0 := Midnight(t) jitter := time.Duration(p.config.GetInt("first.jitter", 0)) t0 = t0.Add(jitter * time.Millisecond) return t0.Before(Midnight(time.Now())) } func (p *FirstPlugin) register() { p.handlers = []bot.HandlerSpec{ {Kind: bot.Message, IsCmd: false, Regex: regexp.MustCompile(`(?i)^who'?s on first the most.?$`), Handler: func(r bot.Request) bool { first, err := getLastFirst(p.store, r.Msg.Channel) if first != nil && err == nil { p.leaderboard(r.Conn, r.Msg.Channel) return true } return false }}, {Kind: bot.Message, IsCmd: false, Regex: regexp.MustCompile(`(?i)^who'?s on first.?$`), Handler: func(r bot.Request) bool { first, err := getLastFirst(p.store, r.Msg.Channel) if first != nil && err == nil { p.announceFirst(r.Conn, first) return true } return false }}, {Kind: bot.Message, IsCmd: true, Regex: regexp.MustCompile(`(?i)^clear first$`), Handler: func(r bot.Request) bool { if !p.bot.CheckAdmin(r.Msg.User.Name) { p.bot.Send(r.Conn, bot.Message, r.Msg.Channel, "You are not authorized to do that.") return true } fe, err := getLastFirst(p.store, r.Msg.Channel) if err != nil { p.bot.Send(r.Conn, bot.Message, r.Msg.Channel, "Could not find a first entry.") return true } p.enabled = false err = fe.delete(p.store) if err != nil { p.bot.Send(r.Conn, bot.Message, r.Msg.Channel, fmt.Sprintf("Could not delete first entry: %s", err)) p.enabled = true return true } d := p.bot.Config().GetInt("first.maxregen", 300) log.Debug().Msgf("Setting first timer for %d seconds", d) timer := time.NewTimer(time.Duration(d) * time.Second) go func() { <-timer.C p.enabled = true log.Debug().Msgf("Re-enabled first") }() p.bot.Send(r.Conn, bot.Message, r.Msg.Channel, fmt.Sprintf("Deleted first entry: '%s' and set a random timer for when first will happen next.", fe.Body)) return true }}, {Kind: bot.Message, IsCmd: false, Regex: regexp.MustCompile(`.*`), Handler: func(r bot.Request) bool { if r.Msg.IsIM || !p.enabled || !p.enabled_channel(r) { return false } first, err := getLastFirst(p.store, r.Msg.Channel) if err != nil { log.Error(). Err(err). Msg("Error getting last first") } log.Debug().Bool("first == nil", first == nil).Msg("Is first nil?") log.Debug().Bool("first == nil || isNotToday()", p.isNotToday(first)).Msg("Is it today?") log.Debug().Bool("p.allowed", p.allowed(r.Msg)).Msg("Allowed?") if (first == nil || p.isNotToday(first)) && p.allowed(r.Msg) { log.Debug(). Str("Body", r.Msg.Body). Interface("t0", first). Time("t1", time.Now()). Msg("Recording first") p.recordFirst(r.Conn, r.Msg) return false } return false }}, } p.bot.RegisterTable(p, p.handlers) } func (p *FirstPlugin) enabled_channel(r bot.Request) bool { chs := p.config.GetArray("first.channels", []string{}) for _, ch := range chs { if r.Msg.Channel == ch { return true } } return false } func (p *FirstPlugin) allowed(message msg.Message) bool { if message.Body == "" { return false } for _, m := range p.bot.Config().GetArray("Bad.Msgs", []string{}) { match, err := regexp.MatchString(m, strings.ToLower(message.Body)) if err != nil { log.Error().Err(err).Msg("Bad regexp") } if match { log.Info(). Str("user", message.User.Name). Str("Body", message.Body). Msg("Disallowing first") return false } } for _, host := range p.bot.Config().GetArray("Bad.Hosts", []string{}) { if host == message.Host { log.Info(). Str("user", message.User.Name). Str("Body", message.Body). Msg("Disallowing first") return false } } for _, nick := range p.bot.Config().GetArray("Bad.Nicks", []string{}) { if nick == message.User.Name { log.Info(). Str("user", message.User.Name). Str("Body", message.Body). Msg("Disallowing first") return false } } return true } func (p *FirstPlugin) recordFirst(c bot.Connector, message msg.Message) { log.Info(). Str("Channel", message.Channel). Str("user", message.User.Name). Str("Body", message.Body). Msg("Recording first") first := &FirstEntry{ Day: Midnight(time.Now()), Time: time.Now(), Channel: message.Channel, Body: message.Body, Nick: message.User.Name, } log.Info().Msgf("recordFirst: %+v", first.Day) err := first.save(p.store) if err != nil { log.Error().Err(err).Msg("Error saving first entry") return } p.announceFirst(c, first) } func (p *FirstPlugin) leaderboard(c bot.Connector, ch string) error { // todo: remove this once we verify stuff //q := `select max(Channel) Channel, max(Nick) Nick, count(ID) count // from first // group by Channel, Nick // having Channel = ? // order by count desc // limit 3` groups, err := p.store.FindAggregate(FirstEntry{}, bh.Where("Channel").Eq(ch), "Channel", "Nick") if err != nil { return err } if len(groups) != 1 { return fmt.Errorf("found %d groups but expected 1", len(groups)) } //res := groups[0] //talismans := []string{":gold-trophy:", ":silver-trophy:", ":bronze-trophy:"} //msg := "First leaderboard:\n" //n := res.Count() //fe := FirstEntry{} // //for i, e := range res { // msg += fmt.Sprintf("%s %d %s\n", talismans[i], e.Count, e.Nick) //} //p.bot.Send(c, bot.Message, ch, msg) // todo: care about this return nil } func (p *FirstPlugin) announceFirst(c bot.Connector, first *FirstEntry) { ch := first.Channel p.bot.Send(c, bot.Message, ch, fmt.Sprintf("%s had first at %s with the message: \"%s\"", first.Nick, first.Time.Format("15:04"), first.Body)) } // Help responds to help requests. Every plugin must implement a help function. func (p *FirstPlugin) help(c bot.Connector, kind bot.Kind, message msg.Message, args ...interface{}) bool { p.bot.Send(c, bot.Message, message.Channel, "You can ask 'who's on first?' to find out.") return true }