diff --git a/main.go b/main.go index 202dec7..238ff46 100644 --- a/main.go +++ b/main.go @@ -16,6 +16,7 @@ import ( "github.com/velour/catbase/plugins/downtime" "github.com/velour/catbase/plugins/fact" "github.com/velour/catbase/plugins/leftpad" + "github.com/velour/catbase/plugins/reminder" "github.com/velour/catbase/plugins/talker" "github.com/velour/catbase/plugins/your" "github.com/velour/catbase/slack" @@ -51,6 +52,7 @@ func main() { b.AddHandler("remember", fact.NewRemember(b)) b.AddHandler("your", your.New(b)) b.AddHandler("counter", counter.New(b)) + b.AddHandler("reminder", reminder.New(b)) // catches anything left, will always return true b.AddHandler("factoid", fact.New(b)) diff --git a/plugins/reminder/reminder.go b/plugins/reminder/reminder.go new file mode 100644 index 0000000..69a8994 --- /dev/null +++ b/plugins/reminder/reminder.go @@ -0,0 +1,148 @@ +// © 2013 the CatBase Authors under the WTFPL. See AUTHORS for the list of authors. + +package reminder + +import ( + "fmt" + "sort" + "strings" + "sync" + "time" + + "github.com/velour/catbase/bot" + "github.com/velour/catbase/bot/msg" +) + +type ReminderPlugin struct { + Bot bot.Bot + reminders []*Reminder + mutex *sync.Mutex + timer *time.Timer +} + +type Reminder struct { + from string + who string + what string + when time.Time + 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 { + dur, _ := time.ParseDuration("1h") + timer := time.NewTimer(dur) + timer.Stop() + + plugin := &ReminderPlugin{ + Bot: bot, + reminders: []*Reminder{}, + mutex: &sync.Mutex{}, + timer: timer, + } + go reminderer(plugin) + + return plugin +} + +func reminderer(p *ReminderPlugin) { + //welcome to the reminderererererererererer + for { + fmt.Println() + fmt.Println("waiting...") + fmt.Println() + + <-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() + + 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 { + channel := message.Channel + from := message.User.Name + + parts := strings.Fields(message.Body) + + if len(parts) > 5 { + if strings.ToLower(parts[0]) == "remind" { + who := parts[1] + dur, err := time.ParseDuration(parts[3]) + if err != nil { + p.Bot.SendMessage(channel, "Easy cowboy, not sure I can parse that duration.") + return true + } + when := time.Now().Add(dur) + + what := strings.Join(parts[4:], " ") + + response := fmt.Sprintf("Sure %s, I'll remind %s.", from, who) + p.Bot.SendMessage(channel, response) + + p.mutex.Lock() + + p.timer.Stop() + + p.reminders = append(p.reminders, &Reminder{ + from: from, + who: who, + what: what, + when: when, + channel: channel, + }) + + sort.Sort(reminderSlice(p.reminders)) + + p.timer.Reset(p.reminders[0].when.Sub(time.Now())) + + p.mutex.Unlock() + + return true + } + } + + return false +} + +func (p *ReminderPlugin) Help(channel string, parts []string) { + p.Bot.SendMessage(channel, "Pester someone with a reminder. Try \"remind in message\".\n\nUnsure about duration syntax? Check https://golang.org/pkg/time/#ParseDuration") +} + +func (p *ReminderPlugin) Event(kind string, message msg.Message) bool { + return false +} + +func (p *ReminderPlugin) BotMessage(message msg.Message) bool { + return false +} + +func (p *ReminderPlugin) RegisterWeb() *string { + return nil +} diff --git a/plugins/reminder/reminder_test.go b/plugins/reminder/reminder_test.go new file mode 100644 index 0000000..5337ab6 --- /dev/null +++ b/plugins/reminder/reminder_test.go @@ -0,0 +1,94 @@ +// © 2013 the CatBase Authors under the WTFPL. See AUTHORS for the list of authors. + +package reminder + +import ( + "strings" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/velour/catbase/bot" + "github.com/velour/catbase/bot/msg" + "github.com/velour/catbase/bot/user" +) + +func makeMessage(payload string) msg.Message { + isCmd := strings.HasPrefix(payload, "!") + if isCmd { + payload = payload[1:] + } + return msg.Message{ + User: &user.User{Name: "tester"}, + Channel: "test", + Body: payload, + Command: isCmd, + } +} + +func TestReminder(t *testing.T) { + mb := bot.NewMockBot() + c := New(mb) + assert.NotNil(t, c) + res := c.Message(makeMessage("!remind testuser in 1s don't fail this test")) + time.Sleep(2 * time.Second) + assert.Len(t, mb.Messages, 2) + assert.True(t, res) + assert.Contains(t, mb.Messages[0], "Sure tester, I'll remind testuser.") + assert.Contains(t, mb.Messages[1], "Hey testuser, tester wanted you to be reminded: don't fail this test") +} + +func TestReminderReorder(t *testing.T) { + mb := bot.NewMockBot() + c := New(mb) + assert.NotNil(t, c) + res := c.Message(makeMessage("!remind testuser in 2s don't fail this test 2")) + assert.True(t, res) + res = c.Message(makeMessage("!remind testuser in 1s don't fail this test 1")) + assert.True(t, res) + time.Sleep(5 * time.Second) + assert.Len(t, mb.Messages, 4) + assert.Contains(t, mb.Messages[0], "Sure tester, I'll remind testuser.") + assert.Contains(t, mb.Messages[1], "Sure tester, I'll remind testuser.") + assert.Contains(t, mb.Messages[2], "Hey testuser, tester wanted you to be reminded: don't fail this test 1") + assert.Contains(t, mb.Messages[3], "Hey testuser, tester wanted you to be reminded: don't fail this test 2") +} + +func TestReminderParse(t *testing.T) { + mb := bot.NewMockBot() + c := New(mb) + assert.NotNil(t, c) + res := c.Message(makeMessage("!remind testuser in unparseable don't fail this test")) + assert.Len(t, mb.Messages, 1) + assert.True(t, res) + assert.Contains(t, mb.Messages[0], "Easy cowboy, not sure I can parse that duration.") +} + +func TestHelp(t *testing.T) { + mb := bot.NewMockBot() + c := New(mb) + assert.NotNil(t, c) + c.Help("channel", []string{}) + assert.Len(t, mb.Messages, 1) +} + +func TestBotMessage(t *testing.T) { + mb := bot.NewMockBot() + c := New(mb) + assert.NotNil(t, c) + assert.False(t, c.BotMessage(makeMessage("test"))) +} + +func TestEvent(t *testing.T) { + mb := bot.NewMockBot() + c := New(mb) + assert.NotNil(t, c) + assert.False(t, c.Event("dummy", makeMessage("test"))) +} + +func TestRegisterWeb(t *testing.T) { + mb := bot.NewMockBot() + c := New(mb) + assert.NotNil(t, c) + assert.Nil(t, c.RegisterWeb()) +}