// © 2013 the CatBase Authors under the WTFPL. See AUTHORS for the list of authors. package first import ( "database/sql" "fmt" "regexp" "strings" "time" "github.com/jmoiron/sqlx" "github.com/rs/zerolog/log" "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 db *sqlx.DB } type FirstEntry struct { id int64 day time.Time time time.Time channel string body string nick string saved bool } // Insert or update the first entry func (fe *FirstEntry) save(db *sqlx.DB) error { if _, err := db.Exec(`insert into first (day, time, channel, body, nick) values (?, ?, ?, ?, ?)`, fe.day.Unix(), fe.time.Unix(), fe.channel, fe.body, fe.nick, ); err != nil { return err } return nil } // NewFirstPlugin creates a new FirstPlugin with the Plugin interface func New(b bot.Bot) *FirstPlugin { _, err := b.DB().Exec(`create table if not exists first ( id integer primary key, day integer, time integer, channel string, body string, nick string );`) if err != nil { log.Fatal(). Err(err). Msg("Could not create first table") } log.Info().Msgf("First plugin initialized with day: %s", midnight(time.Now())) fp := &FirstPlugin{ Bot: b, db: b.DB(), } b.Register(fp, bot.Message, fp.message) b.Register(fp, bot.Help, fp.help) return fp } func getLastFirst(db *sqlx.DB, channel string) (*FirstEntry, error) { // Get last first entry var id sql.NullInt64 var day sql.NullInt64 var timeEntered sql.NullInt64 var body sql.NullString var nick sql.NullString err := db.QueryRow(`select id, max(day), time, body, nick from first where channel = ? limit 1; `, channel).Scan( &id, &day, &timeEntered, &body, &nick, ) switch { case err == sql.ErrNoRows || !id.Valid: log.Info().Msg("No previous first entries") return nil, nil case err != nil: log.Warn().Err(err).Msg("Error on first query row") return nil, err } log.Debug().Msgf("id: %v day %v time %v body %v nick %v", id, day, timeEntered, body, nick) return &FirstEntry{ id: id.Int64, day: time.Unix(day.Int64, 0), time: time.Unix(timeEntered.Int64, 0), channel: channel, body: body.String, nick: nick.String, saved: true, }, nil } func midnight(t time.Time) time.Time { y, m, d := t.Date() return time.Date(y, m, d, 0, 0, 0, 0, time.UTC) } func isNotToday(f *FirstEntry) bool { if f == nil { return true } t := f.time t0 := midnight(t) return t0.Before(midnight(time.Now())) } // Message responds to the bot hook on recieving messages. // This function returns true if the plugin responds in a meaningful way to the users message. // Otherwise, the function returns false and the bot continues execution of other plugins. func (p *FirstPlugin) message(c bot.Connector, kind bot.Kind, message msg.Message, args ...interface{}) bool { if message.IsIM { log.Debug().Msg("Skipping IM") return false } first, err := getLastFirst(p.db, message.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()", isNotToday(first)).Msg("Is it today?") log.Debug().Bool("p.allowed", p.allowed(message)).Msg("Allowed?") if (first == nil || isNotToday(first)) && p.allowed(message) { log.Debug(). Str("body", message.Body). Interface("t0", first). Time("t1", time.Now()). Msg("Recording first") p.recordFirst(c, message) return false } r := strings.NewReplacer("’", "", "'", "", "\"", "", ",", "", ".", "", ":", "", "?", "", "!", "") m := strings.ToLower(message.Body) if r.Replace(m) == "whos on first the most" && first != nil { p.leaderboard(c, message.Channel) return true } if r.Replace(m) == "whos on first" && first != nil { p.announceFirst(c, first) return true } return false } func (p *FirstPlugin) allowed(message msg.Message) bool { 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: message.Time, channel: message.Channel, body: message.Body, nick: message.User.Name, } log.Info().Msgf("recordFirst: %+v", first.day) err := first.save(p.db) 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 { q := `select max(channel) channel, max(nick) nick, count(id) count from first group by channel, nick having channel = ? limit 3` res := []struct { Channel string Nick string Count int }{} err := p.db.Select(&res, q, ch) if err != nil { return err } talismans := []string{":gold-trophy:", ":silver-trophy:", ":bronze-trophy:"} msg := "First leaderboard:\n" 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) 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 }