From 4c669e520f43d592f880046ab0630a9f4ab6a9aa Mon Sep 17 00:00:00 2001 From: Chris Sexton Date: Tue, 27 Apr 2021 12:36:34 -0400 Subject: [PATCH] last: create plugin --- bot/bot.go | 9 ++- bot/handlers.go | 2 +- bot/interfaces.go | 3 + bot/mock.go | 1 + main.go | 2 + plugins/first/first.go | 12 +-- plugins/last/last.go | 157 ++++++++++++++++++++++++++++++++++++++ plugins/last/last_test.go | 27 +++++++ 8 files changed, 204 insertions(+), 9 deletions(-) create mode 100644 plugins/last/last.go create mode 100644 plugins/last/last_test.go diff --git a/bot/bot.go b/bot/bot.go index ac5037c..5d82a97 100644 --- a/bot/bot.go +++ b/bot/bot.go @@ -243,7 +243,7 @@ func (b *bot) RegisterTable(p Plugin, handlers HandlerTable) { // RegisterRegex does what register does, but with a matcher func (b *bot) RegisterRegex(p Plugin, kind Kind, r *regexp.Regexp, resp ResponseHandler) { - t := reflect.TypeOf(p).String() + t := PluginName(p) if _, ok := b.callbacks[t]; !ok { b.callbacks[t] = make(map[Kind][]HandlerSpec) } @@ -354,7 +354,7 @@ func (b *bot) GetWhitelist() []string { return list } -func (b *bot) onBlacklist(channel, plugin string) bool { +func (b *bot) OnBlacklist(channel, plugin string) bool { return b.pluginBlacklist[channel+plugin] } @@ -365,3 +365,8 @@ func (b *bot) onWhitelist(plugin string) bool { func pluginNameStem(name string) string { return strings.Split(strings.TrimPrefix(name, "*"), ".")[0] } + +func PluginName(p Plugin) string { + t := reflect.TypeOf(p).String() + return t +} diff --git a/bot/handlers.go b/bot/handlers.go index c939fd0..bec470b 100644 --- a/bot/handlers.go +++ b/bot/handlers.go @@ -28,7 +28,7 @@ func (b *bot) Receive(conn Connector, kind Kind, msg msg.Message, args ...interf } for _, name := range b.pluginOrdering { - if b.onBlacklist(msg.Channel, pluginNameStem(name)) || !b.onWhitelist(pluginNameStem(name)) { + if b.OnBlacklist(msg.Channel, pluginNameStem(name)) || !b.onWhitelist(pluginNameStem(name)) { continue } if b.runCallback(conn, b.plugins[name], kind, msg, args...) { diff --git a/bot/interfaces.go b/bot/interfaces.go index 47e389a..cfb2450 100644 --- a/bot/interfaces.go +++ b/bot/interfaces.go @@ -159,6 +159,9 @@ type Bot interface { // Get the contents of the white list GetWhitelist() []string + + // Check if a particular plugin is blacklisted + OnBlacklist(string, string) bool } // Connector represents a server connection to a chat service diff --git a/bot/mock.go b/bot/mock.go index bbf9252..55b1e61 100644 --- a/bot/mock.go +++ b/bot/mock.go @@ -121,4 +121,5 @@ func (mb *MockBot) GetPluginNames() []string { return nil } func (mb *MockBot) RefreshPluginBlacklist() error { return nil } func (mb *MockBot) RefreshPluginWhitelist() error { return nil } func (mb *MockBot) GetWhitelist() []string { return []string{} } +func (mb *MockBot) OnBlacklist(ch, p string) bool { return false } func (mb *MockBot) URLFormat(title, url string) string { return title + url } diff --git a/main.go b/main.go index 8561c79..b22f361 100644 --- a/main.go +++ b/main.go @@ -13,6 +13,7 @@ import ( "github.com/velour/catbase/connectors/discord" "github.com/velour/catbase/plugins/giphy" "github.com/velour/catbase/plugins/gpt2" + "github.com/velour/catbase/plugins/last" "github.com/velour/catbase/plugins/achievements" "github.com/velour/catbase/plugins/aoc" @@ -120,6 +121,7 @@ func main() { b.AddPlugin(giphy.New(b)) b.AddPlugin(gpt2.New(b)) b.AddPlugin(emojifyme.New(b)) + b.AddPlugin(last.New(b)) b.AddPlugin(first.New(b)) b.AddPlugin(leftpad.New(b)) b.AddPlugin(talker.New(b)) diff --git a/plugins/first/first.go b/plugins/first/first.go index 72a9eb0..48b7720 100644 --- a/plugins/first/first.go +++ b/plugins/first/first.go @@ -83,7 +83,7 @@ func New(b bot.Bot) *FirstPlugin { } log.Info().Msgf("First plugin initialized with day: %s", - midnight(time.Now())) + Midnight(time.Now())) fp := &FirstPlugin{ bot: b, @@ -135,9 +135,9 @@ func getLastFirst(db *sqlx.DB, channel string) (*FirstEntry, error) { }, nil } -func midnight(t time.Time) time.Time { +func Midnight(t time.Time) time.Time { y, m, d := t.Date() - return time.Date(y, m, d, 0, 0, 0, 0, time.UTC) + return time.Date(y, m, d, 0, 0, 0, 0, time.Local) } func isNotToday(f *FirstEntry) bool { @@ -145,8 +145,8 @@ func isNotToday(f *FirstEntry) bool { return true } t := f.time - t0 := midnight(t) - return t0.Before(midnight(time.Now())) + t0 := Midnight(t) + return t0.Before(Midnight(time.Now())) } func (p *FirstPlugin) register() { @@ -281,7 +281,7 @@ func (p *FirstPlugin) recordFirst(c bot.Connector, message msg.Message) { Str("body", message.Body). Msg("Recording first") first := &FirstEntry{ - day: midnight(time.Now()), + day: Midnight(time.Now()), time: message.Time, channel: message.Channel, body: message.Body, diff --git a/plugins/last/last.go b/plugins/last/last.go new file mode 100644 index 0000000..ce10bef --- /dev/null +++ b/plugins/last/last.go @@ -0,0 +1,157 @@ +package last + +import ( + "fmt" + "regexp" + "time" + + "github.com/jmoiron/sqlx" + "github.com/rs/zerolog/log" + + "github.com/velour/catbase/bot" + "github.com/velour/catbase/plugins/first" +) + +type LastPlugin struct { + b bot.Bot + db *sqlx.DB + + handlers bot.HandlerTable + channels map[string]bool +} + +func New(b bot.Bot) *LastPlugin { + p := &LastPlugin{ + b: b, + db: b.DB(), + channels: map[string]bool{}, + } + if err := p.migrate(); err != nil { + panic(err) + } + p.register() + return p +} + +func (p *LastPlugin) migrate() error { + tx, err := p.db.Beginx() + if err != nil { + return err + } + _, err = tx.Exec(`create table if not exists last ( + day integer primary key, + ts int not null, + channel string not null, + who string not null, + message string not null + )`) + if err != nil { + tx.Rollback() + return err + } + return tx.Commit() +} + +func (p *LastPlugin) register() { + p.handlers = bot.HandlerTable{ + { + Kind: bot.Message, IsCmd: false, + Regex: regexp.MustCompile(`.*`), + HelpText: "Last does secret stuff you don't need to know about.", + Handler: p.recordLast, + }, + { + Kind: bot.Message, IsCmd: true, + Regex: regexp.MustCompile(`(?i)^who killed the channel\??$`), + HelpText: "Find out who had last yesterday", + Handler: p.whoKilled, + }, + } + p.b.RegisterTable(p, p.handlers) +} + +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 { + 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)) + } + } + + _, err := p.db.Exec( + `insert into last values (?, ?, ?, ?, ?) + on conflict(day) do update set + ts=excluded.ts, channel=excluded.channel, who=excluded.who, message=excluded.message + where day=excluded.day`, + day.Unix(), time.Now().Unix(), ch, who, r.Msg.Body) + if err != nil { + log.Error().Err(err).Msgf("Could not record last.") + } + return false +} + +type last struct { + Day int64 + TS int64 + Channel string + Who string + Message string +} + +func (p *LastPlugin) yesterdaysLast() (last, error) { + l := last{} + midnight := first.Midnight(time.Now()) + q := `select * from last where day < ? order by day limit 1` + err := p.db.Get(&l, q, midnight) + if err != nil { + return l, err + } + return l, nil +} + +func (p *LastPlugin) reportLast(ch string) func() { + return func() { + p.sayLast(p.b.DefaultConnector(), ch) + time.AfterFunc(24*time.Hour, p.reportLast(ch)) + } +} + +func (p *LastPlugin) whoKilled(r bot.Request) bool { + p.sayLast(r.Conn, r.Msg.Channel) + return true +} + +func (p *LastPlugin) sayLast(c bot.Connector, ch string) { + l, err := p.yesterdaysLast() + if err != nil { + log.Error().Err(err).Msgf("Couldn't find last") + p.b.Send(c, bot.Message, ch, "I couldn't find a last.") + return + } + if l.Day == 0 { + log.Error().Interface("l", l).Msgf("Couldn't find last") + p.b.Send(c, bot.Message, ch, "I couldn't find a last.") + } + msg := fmt.Sprintf(`%s killed the channel last night by saying "%s"`, l.Who, l.Message) + p.b.Send(c, bot.Message, ch, msg) +} diff --git a/plugins/last/last_test.go b/plugins/last/last_test.go new file mode 100644 index 0000000..7ebf5c3 --- /dev/null +++ b/plugins/last/last_test.go @@ -0,0 +1,27 @@ +package last + +import ( + "os" + "testing" + "time" + + "github.com/rs/zerolog" + "github.com/rs/zerolog/log" + "github.com/stretchr/testify/assert" +) + +func TestNextNoonBeforeNoon(t *testing.T) { + log.Logger = log.Logger.Output(zerolog.ConsoleWriter{Out: os.Stderr}) + t0 := time.Date(2021, 04, 27, 10, 10, 0, 0, time.Local) + t1 := nextNoon(t0) + expected := 1*time.Hour + 50*time.Minute + assert.Equal(t, expected, t1) +} + +func TestNextNoonAfterNoon(t *testing.T) { + log.Logger = log.Logger.Output(zerolog.ConsoleWriter{Out: os.Stderr}) + t0 := time.Date(2021, 04, 27, 14, 15, 0, 0, time.Local) + t1 := nextNoon(t0) + expected := 21*time.Hour + 45*time.Minute + assert.Equal(t, expected, t1) +}