Merge pull request #52 from velour/reminder_persist

back up the reminders in the database for a better msherms pestering …
This commit is contained in:
Chris Sexton 2017-05-15 13:00:14 -04:00 committed by GitHub
commit 995b37d9d8
2 changed files with 172 additions and 107 deletions

View File

@ -3,29 +3,34 @@
package reminder package reminder
import ( import (
"errors"
"fmt" "fmt"
"sort" "log"
"strconv" "strconv"
"strings" "strings"
"sync" "sync"
"time" "time"
"github.com/jmoiron/sqlx"
"github.com/velour/catbase/bot" "github.com/velour/catbase/bot"
"github.com/velour/catbase/bot/msg" "github.com/velour/catbase/bot/msg"
"github.com/velour/catbase/config" "github.com/velour/catbase/config"
) )
const (
TIMESTAMP = "2006-01-02 15:04:05"
)
type ReminderPlugin struct { type ReminderPlugin struct {
Bot bot.Bot Bot bot.Bot
reminders []*Reminder db *sqlx.DB
mutex *sync.Mutex mutex *sync.Mutex
timer *time.Timer timer *time.Timer
config *config.Config config *config.Config
nextReminderId int
} }
type Reminder struct { type Reminder struct {
id int id int64
from string from string
who string who string
what string what string
@ -33,65 +38,40 @@ type Reminder struct {
channel string channel string
} }
type reminderSlice []*Reminder
func (s reminderSlice) Len() int {
return len(s)
}
func (s reminderSlice) Less(i, j int) bool {
return s[i].when.Before(s[j].when)
}
func (s reminderSlice) Swap(i, j int) {
s[i], s[j] = s[j], s[i]
}
func New(bot bot.Bot) *ReminderPlugin { func New(bot bot.Bot) *ReminderPlugin {
log.SetFlags(log.LstdFlags | log.Lshortfile)
if bot.DBVersion() == 1 {
if _, err := bot.DB().Exec(`create table if not exists reminders (
id integer primary key,
fromWho string,
toWho string,
what string,
remindWhen string,
channel string
);`); err != nil {
log.Fatal(err)
}
}
dur, _ := time.ParseDuration("1h") dur, _ := time.ParseDuration("1h")
timer := time.NewTimer(dur) timer := time.NewTimer(dur)
timer.Stop() timer.Stop()
plugin := &ReminderPlugin{ plugin := &ReminderPlugin{
Bot: bot, Bot: bot,
reminders: []*Reminder{}, db: bot.DB(),
mutex: &sync.Mutex{}, mutex: &sync.Mutex{},
timer: timer, timer: timer,
config: bot.Config(), config: bot.Config(),
nextReminderId: 0,
} }
plugin.queueUpNextReminder()
go reminderer(plugin) go reminderer(plugin)
return plugin return plugin
} }
func reminderer(p *ReminderPlugin) {
//welcome to the reminderererererererererer
for {
<-p.timer.C
p.mutex.Lock()
reminder := p.reminders[0]
if len(p.reminders) >= 2 {
p.reminders = p.reminders[1:]
p.timer.Reset(p.reminders[0].when.Sub(time.Now()))
} else {
p.reminders = []*Reminder{}
}
p.mutex.Unlock()
if reminder.from == reminder.who {
reminder.from = "you"
}
message := fmt.Sprintf("Hey %s, %s wanted you to be reminded: %s", reminder.who, reminder.from, reminder.what)
p.Bot.SendMessage(reminder.channel, message)
}
}
func (p *ReminderPlugin) Message(message msg.Message) bool { func (p *ReminderPlugin) Message(message msg.Message) bool {
channel := message.Channel channel := message.Channel
from := message.User.Name from := message.User.Name
@ -111,8 +91,6 @@ func (p *ReminderPlugin) Message(message msg.Message) bool {
return true return true
} }
reminders := []*Reminder{}
operator := strings.ToLower(parts[2]) operator := strings.ToLower(parts[2])
doConfirm := true doConfirm := true
@ -120,20 +98,18 @@ func (p *ReminderPlugin) Message(message msg.Message) bool {
if operator == "in" { if operator == "in" {
//one off reminder //one off reminder
//remind who in dur blah //remind who in dur blah
when := time.Now().Add(dur) when := time.Now().UTC().Add(dur)
what := strings.Join(parts[4:], " ") what := strings.Join(parts[4:], " ")
id := p.nextReminderId p.addReminder(&Reminder{
p.nextReminderId++ id: -1,
reminders = append(reminders, &Reminder{
id: id,
from: from, from: from,
who: who, who: who,
what: what, what: what,
when: when, when: when,
channel: channel, channel: channel,
}) })
} else if operator == "every" && strings.ToLower(parts[4]) == "for" { } else if operator == "every" && strings.ToLower(parts[4]) == "for" {
//batch add, especially for reminding msherms to buy a kit //batch add, especially for reminding msherms to buy a kit
//remind who every dur for dur2 blah //remind who every dur for dur2 blah
@ -143,8 +119,8 @@ func (p *ReminderPlugin) Message(message msg.Message) bool {
return true return true
} }
when := time.Now().Add(dur) when := time.Now().UTC().Add(dur)
endTime := time.Now().Add(dur2) endTime := time.Now().UTC().Add(dur2)
what := strings.Join(parts[6:], " ") what := strings.Join(parts[6:], " ")
for i := 0; when.Before(endTime); i++ { for i := 0; when.Before(endTime); i++ {
@ -154,11 +130,8 @@ func (p *ReminderPlugin) Message(message msg.Message) bool {
break break
} }
id := p.nextReminderId p.addReminder(&Reminder{
p.nextReminderId++ id: int64(-1),
reminders = append(reminders, &Reminder{
id: id,
from: from, from: from,
who: who, who: who,
what: what, what: what,
@ -178,59 +151,26 @@ func (p *ReminderPlugin) Message(message msg.Message) bool {
p.Bot.SendMessage(channel, response) p.Bot.SendMessage(channel, response)
} }
p.mutex.Lock() p.queueUpNextReminder()
p.timer.Stop()
p.reminders = append(p.reminders, reminders...)
sort.Sort(reminderSlice(p.reminders))
if len(p.reminders) > 0 {
p.timer.Reset(p.reminders[0].when.Sub(time.Now()))
}
p.mutex.Unlock()
return true return true
} }
} else if len(parts) == 2 && strings.ToLower(parts[0]) == "list" && strings.ToLower(parts[1]) == "reminders" { } else if len(parts) == 2 && strings.ToLower(parts[0]) == "list" && strings.ToLower(parts[1]) == "reminders" {
var response string response, err := p.getAllRemindersFormatted(channel)
p.mutex.Lock() if err != nil {
if len(p.reminders) == 0 { p.Bot.SendMessage(channel, "listing failed.")
response = "no pending reminders"
} else { } else {
counter := 1 p.Bot.SendMessage(channel, response)
for _, reminder := range p.reminders {
if reminder.channel == channel {
response += fmt.Sprintf("%d) %s -> %s :: %s @ %s (id=%d)\n", counter, reminder.from, reminder.who, reminder.what, reminder.when, reminder.id)
counter++
}
}
} }
p.mutex.Unlock()
p.Bot.SendMessage(channel, response)
return true return true
} else if len(parts) == 3 && strings.ToLower(parts[0]) == "cancel" && strings.ToLower(parts[1]) == "reminder" { } else if len(parts) == 3 && strings.ToLower(parts[0]) == "cancel" && strings.ToLower(parts[1]) == "reminder" {
id, err := strconv.Atoi(parts[2]) id, err := strconv.ParseInt(parts[2], 10, 64)
if err != nil { if err != nil {
p.Bot.SendMessage(channel, fmt.Sprintf("couldn't parse id: %s", parts[2])) p.Bot.SendMessage(channel, fmt.Sprintf("couldn't parse id: %s", parts[2]))
} else { } else {
p.mutex.Lock() err := p.deleteReminder(id)
deleted := false if err == nil {
for i, reminder := range p.reminders {
if reminder.id == id {
copy(p.reminders[i:], p.reminders[i+1:])
p.reminders[len(p.reminders)-1] = nil
p.reminders = p.reminders[:len(p.reminders)-1]
deleted = true
break
}
}
p.mutex.Unlock()
if deleted {
p.Bot.SendMessage(channel, fmt.Sprintf("successfully canceled reminder: %s", parts[2])) p.Bot.SendMessage(channel, fmt.Sprintf("successfully canceled reminder: %s", parts[2]))
} else { } else {
p.Bot.SendMessage(channel, fmt.Sprintf("failed to find and cancel reminder: %s", parts[2])) p.Bot.SendMessage(channel, fmt.Sprintf("failed to find and cancel reminder: %s", parts[2]))
@ -257,3 +197,128 @@ func (p *ReminderPlugin) BotMessage(message msg.Message) bool {
func (p *ReminderPlugin) RegisterWeb() *string { func (p *ReminderPlugin) RegisterWeb() *string {
return nil return nil
} }
func (p *ReminderPlugin) getNextReminder() *Reminder {
p.mutex.Lock()
defer p.mutex.Unlock()
rows, err := p.db.Query("select id, fromWho, toWho, what, remindWhen, channel from reminders order by remindWhen asc limit 1;")
if err != nil {
log.Print(err)
return nil
}
defer rows.Close()
once := false
var reminder *Reminder
for rows.Next() {
if once {
log.Print("somehow got multiple rows")
}
reminder = &Reminder{}
var when string
err := rows.Scan(&reminder.id, &reminder.from, &reminder.who, &reminder.what, &when, &reminder.channel)
if err != nil {
log.Print(err)
return nil
}
reminder.when, err = time.Parse(TIMESTAMP, when)
if err != nil {
log.Print(err)
return nil
}
once = true
}
return reminder
}
func (p *ReminderPlugin) addReminder(reminder *Reminder) error {
p.mutex.Lock()
defer p.mutex.Unlock()
_, err := p.db.Exec(`insert into reminders (fromWho, toWho, what, remindWhen, channel) values (?, ?, ?, ?, ?);`,
reminder.from, reminder.who, reminder.what, reminder.when.Format(TIMESTAMP), reminder.channel)
if err != nil {
log.Print(err)
}
return err
}
func (p *ReminderPlugin) deleteReminder(id int64) error {
p.mutex.Lock()
defer p.mutex.Unlock()
res, err := p.db.Exec(`delete from reminders where id = ?;`, id)
if err != nil {
log.Print(err)
} else {
if affected, err := res.RowsAffected(); err != nil {
return err
} else if affected != 1 {
return errors.New("didn't delete any rows")
}
}
return err
}
func (p *ReminderPlugin) getAllRemindersFormatted(channel string) (string, error) {
p.mutex.Lock()
defer p.mutex.Unlock()
rows, err := p.db.Query("select id, fromWho, toWho, what, remindWhen from reminders order by remindWhen asc;")
if err != nil {
log.Print(err)
return "", nil
}
defer rows.Close()
reminders := ""
counter := 1
reminder := &Reminder{}
for rows.Next() {
var when string
err := rows.Scan(&reminder.id, &reminder.from, &reminder.who, &reminder.what, &when)
if err != nil {
return "", err
}
reminders += fmt.Sprintf("%d) %s -> %s :: %s @ %s (%d)\n", counter, reminder.from, reminder.who, reminder.what, when, reminder.id)
counter++
}
if counter == 1 {
return "no pending reminders", nil
}
return reminders, nil
}
func (p *ReminderPlugin) queueUpNextReminder() {
nextReminder := p.getNextReminder()
if nextReminder != nil {
p.timer.Reset(nextReminder.when.Sub(time.Now().UTC()))
}
}
func reminderer(p *ReminderPlugin) {
for {
<-p.timer.C
reminder := p.getNextReminder()
if reminder != nil && time.Now().UTC().After(reminder.when) {
if reminder.from == reminder.who {
reminder.from = "you"
}
message := fmt.Sprintf("Hey %s, %s wanted you to be reminded: %s", reminder.who, reminder.from, reminder.what)
p.Bot.SendMessage(reminder.channel, message)
if err:= p.deleteReminder(reminder.id); err != nil {
log.Print(reminder.id)
log.Print(err)
log.Fatal("this will cause problems, we need to stop now.")
}
}
p.queueUpNextReminder()
}
}

View File

@ -128,13 +128,13 @@ func TestCancel(t *testing.T) {
assert.NotNil(t, c) assert.NotNil(t, c)
res := c.Message(makeMessage("!remind testuser in 1m don't fail this test")) res := c.Message(makeMessage("!remind testuser in 1m don't fail this test"))
assert.True(t, res) assert.True(t, res)
res = c.Message(makeMessage("!cancel reminder 0")) res = c.Message(makeMessage("!cancel reminder 1"))
assert.True(t, res) assert.True(t, res)
res = c.Message(makeMessage("!list reminders")) res = c.Message(makeMessage("!list reminders"))
assert.True(t, res) assert.True(t, res)
assert.Len(t, mb.Messages, 3) assert.Len(t, mb.Messages, 3)
assert.Contains(t, mb.Messages[0], "Sure tester, I'll remind testuser.") assert.Contains(t, mb.Messages[0], "Sure tester, I'll remind testuser.")
assert.Contains(t, mb.Messages[1], "successfully canceled reminder: 0") assert.Contains(t, mb.Messages[1], "successfully canceled reminder: 1")
assert.Contains(t, mb.Messages[2], "no pending reminders") assert.Contains(t, mb.Messages[2], "no pending reminders")
} }
@ -142,10 +142,10 @@ func TestCancelMiss(t *testing.T) {
mb := bot.NewMockBot() mb := bot.NewMockBot()
c := New(mb) c := New(mb)
assert.NotNil(t, c) assert.NotNil(t, c)
res := c.Message(makeMessage("!cancel reminder 0")) res := c.Message(makeMessage("!cancel reminder 1"))
assert.True(t, res) assert.True(t, res)
assert.Len(t, mb.Messages, 1) assert.Len(t, mb.Messages, 1)
assert.Contains(t, mb.Messages[0], "failed to find and cancel reminder: 0") assert.Contains(t, mb.Messages[0], "failed to find and cancel reminder: 1")
} }
func TestHelp(t *testing.T) { func TestHelp(t *testing.T) {