package last import ( "fmt" bh "github.com/timshannon/bolthold" "regexp" "time" "github.com/velour/catbase/config" "github.com/rs/zerolog/log" "github.com/velour/catbase/bot" "github.com/velour/catbase/plugins/first" ) type LastPlugin struct { b bot.Bot store *bh.Store c *config.Config handlers bot.HandlerTable channels map[string]bool } func New(b bot.Bot) *LastPlugin { p := &LastPlugin{ b: b, store: b.Store(), c: b.Config(), channels: map[string]bool{}, } p.register() return p } func (p *LastPlugin) register() { p.handlers = bot.HandlerTable{ { Kind: bot.Message, IsCmd: true, Regex: regexp.MustCompile(`(?i)^who killed the channel\??$`), HelpText: "Find out who had Last yesterday", Handler: p.whoKilled, }, { Kind: bot.Message, IsCmd: true, Regex: regexp.MustCompile(`(?i)^who killed #?(?P\S+)\??$`), HelpText: "Find out who had Last yesterday in a channel", Handler: p.whoKilledChannel, }, { Kind: bot.Message, IsCmd: false, Regex: regexp.MustCompile(`.*`), HelpText: "Last does secret stuff you don't need to know about.", Handler: p.recordLast, }, } p.b.RegisterTable(p, p.handlers) } func (p *LastPlugin) enabled_channel(r bot.Request) bool { chs := p.c.GetArray("Last.channels", []string{}) for _, ch := range chs { if r.Msg.Channel == ch { return true } } return false } func nextNoon(t time.Time) time.Duration { day := first.Midnight(t) nextNoon := day.Add(12 * time.Hour) log.Debug(). Time("t", t). Time("nextNoon", nextNoon). Bool("before(t)", nextNoon.Before(t)). Msgf("nextNoon") if nextNoon.Before(t) { nextNoon = nextNoon.Add(24 * time.Hour) } log.Debug().Msgf("nextNoon.Sub(t): %v", nextNoon.Sub(t)) return nextNoon.Sub(t) } func (p *LastPlugin) recordLast(r bot.Request) bool { if !p.enabled_channel(r) { return false } ch := r.Msg.Channel who := r.Msg.User.Name day := first.Midnight(time.Now()) if _, ok := p.channels[ch]; !ok { if !p.b.OnBlacklist(ch, bot.PluginName(p)) { p.channels[ch] = true log.Debug().Msgf("Next Noon: %v", nextNoon(time.Now().UTC())) time.AfterFunc(nextNoon(time.Now().Local()), p.reportLast(ch)) } } if r.Msg.Body == "" { return false } invalidUsers := p.c.GetArray("Last.invalidUsers", []string{"unknown"}) for _, u := range invalidUsers { if who == u { return false } } l := &Last{ Day: day.Unix(), TS: time.Now().Unix(), Channel: ch, Who: who, Message: r.Msg.Body, } // todo: I think Last might depend on a key being something more real return p.store.Insert(bh.NextSequence(), l) == nil } type Last struct { ID uint64 `boltholdKey:"ID"` Day int64 TS int64 `db:"ts"` Channel string `db:"channel"` Who string `db:"who"` Message string `db:"message"` } func (p *LastPlugin) yesterdaysLast(ch string) (Last, error) { l := Last{} midnight := first.Midnight(time.Now()) q := `select * from Last where channel = ? and day < ? and day >= ? order by day limit 1` log.Debug().Str("q", q).Msgf("yesterdaysLast: %d to %d", midnight.Unix(), midnight.Add(-24*time.Hour).Unix()) err := p.store.FindOne(&l, bh.Where("Channel").Eq(ch).And("Day").Lt(midnight.Unix()).And("Day").Ge(midnight.Add(-24*time.Hour).Unix())) if err != nil { return Last{}, err } return l, nil } func (p *LastPlugin) reportLast(ch string) func() { return func() { p.sayLast(p.b.DefaultConnector(), ch, ch, false) time.AfterFunc(24*time.Hour, p.reportLast(ch)) } } func (p *LastPlugin) whoKilled(r bot.Request) bool { p.sayLast(r.Conn, r.Msg.Channel, r.Msg.Channel, true) return true } func (p *LastPlugin) whoKilledChannel(r bot.Request) bool { ch := r.Values["channel"] p.sayLast(r.Conn, r.Conn.GetChannelID(ch), r.Msg.Channel, true) return true } func (p *LastPlugin) sayLast(c bot.Connector, chFrom, chTo string, force bool) { l, err := p.yesterdaysLast(chFrom) if err != nil || l.Day == 0 { log.Error().Err(err).Interface("Last", l).Msgf("Couldn't find Last") if force { p.b.Send(c, bot.Message, chTo, "I couldn't find a Last.") } return } msg := fmt.Sprintf(`%s killed the channel Last night by saying "%s"`, l.Who, l.Message) p.b.Send(c, bot.Message, chTo, msg) }