// © 2013 the CatBase Authors under the WTFPL. See AUTHORS for the list of authors. package reminder import ( "github.com/olebedev/when" "github.com/olebedev/when/rules/common" "github.com/olebedev/when/rules/en" bh "github.com/timshannon/bolthold" "github.com/velour/catbase/bot" "github.com/velour/catbase/bot/msg" "github.com/velour/catbase/config" "sync" "time" ) import ( "fmt" "github.com/rs/zerolog/log" "strconv" "strings" "github.com/velour/catbase/plugins/sms" ) const ( TIMESTAMP = "2006-01-02 15:04:05" ) type ReminderPlugin struct { bot bot.Bot store *bh.Store mutex *sync.Mutex timer *time.Timer config *config.Config when *when.Parser } type Reminder struct { ID uint64 `boltholdKey:"ID"` From string Who string What string When time.Time Channel string } func New(b bot.Bot) *ReminderPlugin { dur, _ := time.ParseDuration("1h") timer := time.NewTimer(dur) timer.Stop() w := when.New(nil) w.Add(en.All...) w.Add(common.All...) plugin := &ReminderPlugin{ bot: b, store: b.Store(), mutex: &sync.Mutex{}, timer: timer, config: b.Config(), when: w, } plugin.queueUpNextReminder() go reminderer(b.DefaultConnector(), plugin) b.Register(plugin, bot.Message, plugin.message) b.Register(plugin, bot.Help, plugin.help) return plugin } func (p *ReminderPlugin) message(c bot.Connector, kind bot.Kind, message msg.Message, args ...interface{}) bool { channel := message.Channel from := message.User.Name var dur, dur2 time.Duration t, err := p.when.Parse(message.Body, time.Now()) // Allowing err to fallthrough for other parsing if t != nil && err == nil { t2 := t.Time.Sub(time.Now()).String() message.Body = string(message.Body[0:t.Index]) + t2 + string(message.Body[t.Index+len(t.Text):]) log.Debug(). Str("body", message.Body). Str("text", t.Text). Msg("Got time request") } parts := strings.Fields(message.Body) if len(parts) >= 5 { if strings.ToLower(parts[0]) == "remind" { who := parts[1] if who == "me" { who = from } dur, err = time.ParseDuration(parts[3]) if err != nil { p.bot.Send(c, bot.Message, channel, "Easy cowboy, not sure I can parse that duration. Try something like '1.5h' or '2h45m'.") return true } operator := strings.ToLower(parts[2]) doConfirm := true if operator == "in" || operator == "at" || operator == "on" { //one off reminder //remind Who in dur blah when := time.Now().UTC().Add(dur) what := strings.Join(parts[4:], " ") p.addReminder(&Reminder{ From: from, Who: who, What: what, When: when, Channel: channel, }) } else if operator == "every" && strings.ToLower(parts[4]) == "for" { //batch add, especially for reminding msherms to buy a kit //remind Who every dur for dur2 blah dur2, err = time.ParseDuration(parts[5]) if err != nil { log.Error().Err(err) p.bot.Send(c, bot.Message, channel, "Easy cowboy, not sure I can parse that duration. Try something like '1.5h' or '2h45m'.") return true } when := time.Now().UTC().Add(dur) endTime := time.Now().UTC().Add(dur2) what := strings.Join(parts[6:], " ") max := p.config.GetInt("Reminder.MaxBatchAdd", 10) for i := 0; when.Before(endTime); i++ { if i >= max { p.bot.Send(c, bot.Message, channel, "Easy cowboy, that's a lot of reminders. I'll add some of them.") doConfirm = false break } p.addReminder(&Reminder{ From: from, Who: who, What: what, When: when, Channel: channel, }) when = when.Add(dur) } } else { p.bot.Send(c, bot.Message, channel, "Easy cowboy, not sure I comprehend What you're asking.") return true } if doConfirm && from == who { p.bot.Send(c, bot.Message, channel, fmt.Sprintf("Okay. I'll remind you.")) } else if doConfirm { p.bot.Send(c, bot.Message, channel, fmt.Sprintf("Sure %s, I'll remind %s.", from, who)) } p.queueUpNextReminder() return true } } else if len(parts) >= 2 && strings.ToLower(parts[0]) == "list" && strings.ToLower(parts[1]) == "reminders" { var response string var err error if len(parts) == 2 { response, err = p.getAllRemindersFormatted(channel) } else if len(parts) == 4 { if strings.ToLower(parts[2]) == "to" { response, err = p.getAllRemindersToMeFormatted(channel, strings.ToLower(parts[3])) } else if strings.ToLower(parts[2]) == "From" { response, err = p.getAllRemindersFromMeFormatted(channel, strings.ToLower(parts[3])) } } if err != nil { p.bot.Send(c, bot.Message, channel, "listing failed.") } else { p.bot.Send(c, bot.Message, channel, response) } return true } else if len(parts) == 3 && strings.ToLower(parts[0]) == "cancel" && strings.ToLower(parts[1]) == "reminder" { id, err := strconv.ParseUint(parts[2], 10, 64) if err != nil { p.bot.Send(c, bot.Message, channel, fmt.Sprintf("couldn't parse ID: %s", parts[2])) } else { err := p.deleteReminder(id) if err == nil { p.bot.Send(c, bot.Message, channel, fmt.Sprintf("successfully canceled reminder: %s", parts[2])) } else { p.bot.Send(c, bot.Message, channel, fmt.Sprintf("failed to find and cancel reminder: %s", parts[2])) } } return true } return false } func (p *ReminderPlugin) help(c bot.Connector, kind bot.Kind, message msg.Message, args ...interface{}) bool { p.bot.Send(c, bot.Message, message.Channel, "Pester someone with a reminder. Try \"remind in message\".\n\nUnsure about duration syntax? Check https://golang.org/pkg/time/#ParseDuration") return true } func (p *ReminderPlugin) getNextReminder() *Reminder { p.mutex.Lock() defer p.mutex.Unlock() reminder := Reminder{} res, err := p.store.FindAggregate(Reminder{}, &bh.Query{}, "") if err != nil { log.Error().Err(err) return nil } if len(res) == 0 { log.Error().Msg("No next reminder in system.") return nil } res[0].Max("RemindWhen", &reminder) return &reminder } func (p *ReminderPlugin) addReminder(reminder *Reminder) error { p.mutex.Lock() defer p.mutex.Unlock() err := p.store.Insert(bh.NextSequence(), reminder) if err != nil { log.Error().Err(err).Msgf("error creating reminder") return err } return nil } func (p *ReminderPlugin) deleteReminder(id uint64) error { p.mutex.Lock() defer p.mutex.Unlock() err := p.store.Delete(id, Reminder{}) return err } func (p *ReminderPlugin) getRemindersFormatted(filter, who string) (string, error) { max := p.config.GetInt("Reminder.MaxList", 25) p.mutex.Lock() defer p.mutex.Unlock() q := bh.Where(filter).Eq(who) if filter == "" || who == "" { q = &bh.Query{} } total, err := p.store.Count(Reminder{}, q) if err != nil { log.Error().Err(err) return "", nil } if total == 0 { return "no pending reminders", nil } reminders := []Reminder{} err = p.store.Find(&reminders, q.SortBy("ID").Limit(max)) if err != nil { log.Error().Err(err) return "", nil } txt := "" for counter, reminder := range reminders { txt += fmt.Sprintf("%d) %s -> %s :: %s @ %s (%d)\n", counter, reminder.From, reminder.Who, reminder.What, reminder.When, reminder.ID) counter++ } remaining := total - max if remaining > 0 { txt += fmt.Sprintf("...%d more...\n", remaining) } return txt, nil } func (p *ReminderPlugin) getAllRemindersFormatted(channel string) (string, error) { return p.getRemindersFormatted("", "") } func (p *ReminderPlugin) getAllRemindersFromMeFormatted(channel, me string) (string, error) { return p.getRemindersFormatted("FromWho", me) } func (p *ReminderPlugin) getAllRemindersToMeFormatted(channel, me string) (string, error) { return p.getRemindersFormatted("ToWho", me) } func (p *ReminderPlugin) queueUpNextReminder() { nextReminder := p.getNextReminder() if nextReminder != nil { p.timer.Reset(nextReminder.When.Sub(time.Now().UTC())) } } func reminderer(c bot.Connector, p *ReminderPlugin) { for { <-p.timer.C reminder := p.getNextReminder() if reminder != nil && time.Now().UTC().After(reminder.When) { var message string if reminder.From == reminder.Who { reminder.From = "you" message = fmt.Sprintf("Hey %s, you wanted to be reminded: %s", reminder.Who, reminder.What) } else { message = fmt.Sprintf("Hey %s, %s wanted you to be reminded: %s", reminder.Who, reminder.From, reminder.What) } p.bot.Send(c, bot.Message, reminder.Channel, message) smsPlugin := sms.New(p.bot) if err := smsPlugin.Send(reminder.Who, message); err != nil { log.Error().Err(err).Msgf("could not send reminder") } if err := p.deleteReminder(reminder.ID); err != nil { log.Fatal(). Uint64("ID", reminder.ID). Err(err). Msg("this will cause problems, we need to stop now.") } } p.queueUpNextReminder() } }