2016-05-09 17:09:17 +00:00
// © 2013 the CatBase Authors under the WTFPL. See AUTHORS for the list of authors.
package reminder
import (
2017-05-11 16:40:59 +00:00
"errors"
2016-05-09 17:09:17 +00:00
"fmt"
2022-08-31 17:20:16 +00:00
"regexp"
2017-05-09 14:12:24 +00:00
"strconv"
2016-05-09 17:09:17 +00:00
"strings"
"sync"
"time"
2019-03-10 02:26:11 +00:00
"github.com/jmoiron/sqlx"
2019-03-07 16:35:42 +00:00
"github.com/rs/zerolog/log"
2019-03-10 02:26:11 +00:00
"github.com/olebedev/when"
"github.com/olebedev/when/rules/common"
"github.com/olebedev/when/rules/en"
2016-05-09 17:09:17 +00:00
"github.com/velour/catbase/bot"
"github.com/velour/catbase/bot/msg"
2017-05-01 15:54:44 +00:00
"github.com/velour/catbase/config"
2020-05-17 14:49:38 +00:00
"github.com/velour/catbase/plugins/sms"
2016-05-09 17:09:17 +00:00
)
2017-05-11 16:40:59 +00:00
const (
TIMESTAMP = "2006-01-02 15:04:05"
)
2016-05-09 17:09:17 +00:00
type ReminderPlugin struct {
2022-08-31 17:20:16 +00:00
bot bot . Bot
db * sqlx . DB
mutex * sync . Mutex
timer * time . Timer
config * config . Config
when * when . Parser
lastReminder map [ string ] * Reminder
2016-05-09 17:09:17 +00:00
}
type Reminder struct {
2017-05-11 16:40:59 +00:00
id int64
2016-05-09 17:09:17 +00:00
from string
who string
what string
when time . Time
channel string
}
2019-02-05 19:41:38 +00:00
func New ( b bot . Bot ) * ReminderPlugin {
if _ , err := b . DB ( ) . Exec ( ` create table if not exists reminders (
2017-05-11 16:40:59 +00:00
id integer primary key ,
fromWho string ,
toWho string ,
what string ,
remindWhen string ,
channel string
) ; ` ) ; err != nil {
2019-03-07 16:35:42 +00:00
log . Fatal ( ) . Err ( err )
2017-05-11 16:40:59 +00:00
}
2016-05-09 17:09:17 +00:00
dur , _ := time . ParseDuration ( "1h" )
timer := time . NewTimer ( dur )
timer . Stop ( )
2019-03-10 02:26:11 +00:00
w := when . New ( nil )
w . Add ( en . All ... )
w . Add ( common . All ... )
2016-05-09 17:09:17 +00:00
plugin := & ReminderPlugin {
2022-08-31 17:20:16 +00:00
bot : b ,
db : b . DB ( ) ,
mutex : & sync . Mutex { } ,
timer : timer ,
config : b . Config ( ) ,
when : w ,
lastReminder : map [ string ] * Reminder { } ,
2016-05-09 17:09:17 +00:00
}
2017-01-05 14:49:27 +00:00
2017-05-11 16:40:59 +00:00
plugin . queueUpNextReminder ( )
2016-05-09 17:09:17 +00:00
2022-08-31 17:20:16 +00:00
go plugin . reminderer ( b . DefaultConnector ( ) )
2016-05-09 17:09:17 +00:00
2022-08-31 17:20:16 +00:00
b . RegisterRegexCmd ( plugin , bot . Message , regexp . MustCompile ( ` (?i)^snooze (?P<duration>.+)$ ` ) , plugin . snooze )
2019-02-05 19:41:38 +00:00
b . Register ( plugin , bot . Message , plugin . message )
b . Register ( plugin , bot . Help , plugin . help )
2017-05-11 16:40:59 +00:00
return plugin
2016-05-09 17:09:17 +00:00
}
2022-08-31 17:20:16 +00:00
func ( p * ReminderPlugin ) snooze ( r bot . Request ) bool {
lastReminder := p . lastReminder [ r . Msg . Channel ]
if lastReminder == nil {
p . bot . Send ( r . Conn , bot . Message , r . Msg . Channel , "My memory is too small to contain a snoozed reminder." )
return true
}
durationTxt := replaceDuration ( p . when , r . Values [ "duration" ] )
dur , err := time . ParseDuration ( durationTxt )
if err != nil {
p . bot . Send ( r . Conn , bot . Message , r . Msg . Channel , "Whoa, cowboy. I can't parse that time duration." )
return true
}
lastReminder . when = time . Now ( ) . UTC ( ) . Add ( dur )
p . addReminder ( lastReminder )
delete ( p . lastReminder , r . Msg . Channel )
p . bot . Send ( r . Conn , bot . Message , r . Msg . Channel , fmt . Sprintf ( "Okay, I'll let you know in %s" , dur ) )
p . queueUpNextReminder ( )
return true
}
func replaceDuration ( when * when . Parser , txt string ) string {
t , err := when . Parse ( txt , time . Now ( ) )
if t != nil && err == nil {
return txt [ 0 : t . Index ] + t . Time . Sub ( time . Now ( ) ) . String ( ) + txt [ t . Index + len ( t . Text ) : ]
}
return txt
}
2022-03-22 01:32:44 +00:00
func ( p * ReminderPlugin ) message ( c bot . Connector , kind bot . Kind , message msg . Message , args ... any ) bool {
2016-05-09 17:09:17 +00:00
channel := message . Channel
from := message . User . Name
parts := strings . Fields ( message . Body )
2016-05-09 17:27:28 +00:00
if len ( parts ) >= 5 {
2016-05-09 17:09:17 +00:00
if strings . ToLower ( parts [ 0 ] ) == "remind" {
who := parts [ 1 ]
2016-08-07 01:22:03 +00:00
if who == "me" {
who = from
}
2017-05-01 15:54:44 +00:00
2022-08-31 17:20:16 +00:00
dur , err := time . ParseDuration ( parts [ 3 ] )
2016-05-09 17:09:17 +00:00
if err != nil {
2019-05-27 23:21:53 +00:00
p . bot . Send ( c , bot . Message , channel , "Easy cowboy, not sure I can parse that duration. Try something like '1.5h' or '2h45m'." )
2016-05-09 17:09:17 +00:00
return true
}
2017-05-01 15:54:44 +00:00
operator := strings . ToLower ( parts [ 2 ] )
doConfirm := true
2019-03-10 02:26:11 +00:00
if operator == "in" || operator == "at" || operator == "on" {
2017-05-01 15:54:44 +00:00
//one off reminder
//remind who in dur blah
2017-05-11 16:40:59 +00:00
when := time . Now ( ) . UTC ( ) . Add ( dur )
2017-05-01 15:54:44 +00:00
what := strings . Join ( parts [ 4 : ] , " " )
2017-05-11 16:40:59 +00:00
p . addReminder ( & Reminder {
id : - 1 ,
2017-05-01 15:54:44 +00:00
from : from ,
who : who ,
what : what ,
when : when ,
channel : channel ,
} )
2017-05-11 16:40:59 +00:00
2017-05-01 15:54:44 +00:00
} 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
2022-08-31 17:20:16 +00:00
dur2 , err := time . ParseDuration ( parts [ 5 ] )
2017-05-01 15:54:44 +00:00
if err != nil {
2019-03-10 02:26:11 +00:00
log . Error ( ) . Err ( err )
2019-05-27 23:21:53 +00:00
p . bot . Send ( c , bot . Message , channel , "Easy cowboy, not sure I can parse that duration. Try something like '1.5h' or '2h45m'." )
2017-05-01 15:54:44 +00:00
return true
}
2017-05-11 16:40:59 +00:00
when := time . Now ( ) . UTC ( ) . Add ( dur )
endTime := time . Now ( ) . UTC ( ) . Add ( dur2 )
2017-05-01 15:54:44 +00:00
what := strings . Join ( parts [ 6 : ] , " " )
2019-01-27 21:18:16 +00:00
max := p . config . GetInt ( "Reminder.MaxBatchAdd" , 10 )
2017-05-01 15:54:44 +00:00
for i := 0 ; when . Before ( endTime ) ; i ++ {
2019-01-21 19:24:03 +00:00
if i >= max {
2019-05-27 23:21:53 +00:00
p . bot . Send ( c , bot . Message , channel , "Easy cowboy, that's a lot of reminders. I'll add some of them." )
2017-05-01 15:54:44 +00:00
doConfirm = false
break
}
2017-05-11 16:40:59 +00:00
p . addReminder ( & Reminder {
id : int64 ( - 1 ) ,
2017-05-01 15:54:44 +00:00
from : from ,
who : who ,
what : what ,
when : when ,
channel : channel ,
} )
when = when . Add ( dur )
}
} else {
2019-05-27 23:21:53 +00:00
p . bot . Send ( c , bot . Message , channel , "Easy cowboy, not sure I comprehend what you're asking." )
2017-05-01 15:54:44 +00:00
return true
}
2016-05-09 17:09:17 +00:00
2018-10-26 15:01:01 +00:00
if doConfirm && from == who {
2019-05-27 23:21:53 +00:00
p . bot . Send ( c , bot . Message , channel , fmt . Sprintf ( "Okay. I'll remind you." ) )
2018-10-26 15:01:01 +00:00
} else if doConfirm {
2019-05-27 23:21:53 +00:00
p . bot . Send ( c , bot . Message , channel , fmt . Sprintf ( "Sure %s, I'll remind %s." , from , who ) )
2017-05-01 15:54:44 +00:00
}
2016-05-09 17:09:17 +00:00
2017-05-11 16:40:59 +00:00
p . queueUpNextReminder ( )
2016-05-09 17:09:17 +00:00
return true
}
2018-02-05 21:04:40 +00:00
} 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 ] ) )
}
}
2017-05-11 16:40:59 +00:00
if err != nil {
2019-05-27 23:21:53 +00:00
p . bot . Send ( c , bot . Message , channel , "listing failed." )
2017-04-27 16:47:18 +00:00
} else {
2019-05-27 23:21:53 +00:00
p . bot . Send ( c , bot . Message , channel , response )
2017-04-27 16:47:18 +00:00
}
return true
2017-05-09 14:12:24 +00:00
} else if len ( parts ) == 3 && strings . ToLower ( parts [ 0 ] ) == "cancel" && strings . ToLower ( parts [ 1 ] ) == "reminder" {
2017-05-11 16:40:59 +00:00
id , err := strconv . ParseInt ( parts [ 2 ] , 10 , 64 )
2017-05-09 14:12:24 +00:00
if err != nil {
2019-05-27 23:21:53 +00:00
p . bot . Send ( c , bot . Message , channel , fmt . Sprintf ( "couldn't parse id: %s" , parts [ 2 ] ) )
2017-05-09 14:12:24 +00:00
} else {
2017-05-11 16:40:59 +00:00
err := p . deleteReminder ( id )
if err == nil {
2019-05-27 23:21:53 +00:00
p . bot . Send ( c , bot . Message , channel , fmt . Sprintf ( "successfully canceled reminder: %s" , parts [ 2 ] ) )
2017-05-09 14:12:24 +00:00
} else {
2019-05-27 23:21:53 +00:00
p . bot . Send ( c , bot . Message , channel , fmt . Sprintf ( "failed to find and cancel reminder: %s" , parts [ 2 ] ) )
2017-05-09 14:12:24 +00:00
}
}
return true
2016-05-09 17:09:17 +00:00
}
return false
}
2022-03-22 01:32:44 +00:00
func ( p * ReminderPlugin ) help ( c bot . Connector , kind bot . Kind , message msg . Message , args ... any ) bool {
2019-05-27 23:21:53 +00:00
p . bot . Send ( c , bot . Message , message . Channel , "Pester someone with a reminder. Try \"remind <user> in <duration> message\".\n\nUnsure about duration syntax? Check https://golang.org/pkg/time/#ParseDuration" )
2019-02-05 19:41:38 +00:00
return true
2016-05-09 17:09:17 +00:00
}
2017-05-11 16:40:59 +00:00
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 {
2019-03-07 16:35:42 +00:00
log . Error ( ) . Err ( err )
2017-05-11 16:40:59 +00:00
return nil
}
defer rows . Close ( )
once := false
var reminder * Reminder
for rows . Next ( ) {
if once {
2019-03-07 16:35:42 +00:00
log . Debug ( ) . Msg ( "somehow got multiple rows" )
2017-05-11 16:40:59 +00:00
}
reminder = & Reminder { }
var when string
err := rows . Scan ( & reminder . id , & reminder . from , & reminder . who , & reminder . what , & when , & reminder . channel )
if err != nil {
2019-03-07 16:35:42 +00:00
log . Error ( ) . Err ( err )
2017-05-11 16:40:59 +00:00
return nil
}
reminder . when , err = time . Parse ( TIMESTAMP , when )
if err != nil {
2019-03-07 16:35:42 +00:00
log . Error ( ) . Err ( err )
2017-05-11 16:40:59 +00:00
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 (?, ?, ?, ?, ?); ` ,
2018-02-05 21:04:40 +00:00
reminder . from , reminder . who , reminder . what , reminder . when . Format ( TIMESTAMP ) , reminder . channel )
2017-05-11 16:40:59 +00:00
if err != nil {
2019-03-07 16:35:42 +00:00
log . Error ( ) . Err ( err )
2017-05-11 16:40:59 +00:00
}
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 {
2019-03-07 16:35:42 +00:00
log . Error ( ) . Err ( err )
2017-05-11 16:40:59 +00:00
} else {
if affected , err := res . RowsAffected ( ) ; err != nil {
return err
} else if affected != 1 {
return errors . New ( "didn't delete any rows" )
}
}
return err
}
2019-01-27 21:18:16 +00:00
func ( p * ReminderPlugin ) getRemindersFormatted ( filter string ) ( string , error ) {
max := p . config . GetInt ( "Reminder.MaxList" , 25 )
queryString := fmt . Sprintf ( "select id, fromWho, toWho, what, remindWhen from reminders %s order by remindWhen asc limit %d;" , filter , max )
countString := fmt . Sprintf ( "select COUNT(*) from reminders %s;" , filter )
2017-05-11 16:40:59 +00:00
p . mutex . Lock ( )
defer p . mutex . Unlock ( )
2019-01-27 21:18:16 +00:00
var total int
err := p . db . Get ( & total , countString )
if err != nil {
2019-03-07 16:35:42 +00:00
log . Error ( ) . Err ( err )
2019-01-27 21:18:16 +00:00
return "" , nil
}
if total == 0 {
return "no pending reminders" , nil
}
2018-02-05 21:04:40 +00:00
rows , err := p . db . Query ( queryString )
2017-05-11 16:40:59 +00:00
if err != nil {
2019-03-07 16:35:42 +00:00
log . Error ( ) . Err ( err )
2017-05-11 16:40:59 +00:00
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 ++
}
2019-01-27 21:18:16 +00:00
remaining := total - max
if remaining > 0 {
reminders += fmt . Sprintf ( "...%d more...\n" , remaining )
2017-05-11 16:40:59 +00:00
}
return reminders , nil
}
2018-02-05 21:04:40 +00:00
func ( p * ReminderPlugin ) getAllRemindersFormatted ( channel string ) ( string , error ) {
2019-01-27 21:18:16 +00:00
return p . getRemindersFormatted ( "" )
2018-02-05 21:04:40 +00:00
}
func ( p * ReminderPlugin ) getAllRemindersFromMeFormatted ( channel , me string ) ( string , error ) {
2019-01-27 21:18:16 +00:00
return p . getRemindersFormatted ( fmt . Sprintf ( "where fromWho = '%s'" , me ) )
2018-02-05 21:04:40 +00:00
}
func ( p * ReminderPlugin ) getAllRemindersToMeFormatted ( channel , me string ) ( string , error ) {
2019-01-27 21:18:16 +00:00
return p . getRemindersFormatted ( fmt . Sprintf ( "where toWho = '%s'" , me ) )
2018-02-05 21:04:40 +00:00
}
2017-05-11 16:40:59 +00:00
func ( p * ReminderPlugin ) queueUpNextReminder ( ) {
nextReminder := p . getNextReminder ( )
if nextReminder != nil {
p . timer . Reset ( nextReminder . when . Sub ( time . Now ( ) . UTC ( ) ) )
}
}
2022-08-31 17:20:16 +00:00
func ( p * ReminderPlugin ) reminderer ( c bot . Connector ) {
2017-05-11 16:40:59 +00:00
for {
<- p . timer . C
reminder := p . getNextReminder ( )
if reminder != nil && time . Now ( ) . UTC ( ) . After ( reminder . when ) {
2022-08-31 18:27:10 +00:00
p . lastReminder [ reminder . channel ] = reminder
2018-10-26 17:38:12 +00:00
var message string
2017-05-11 16:40:59 +00:00
if reminder . from == reminder . who {
reminder . from = "you"
2018-10-26 17:38:12 +00:00
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 )
2017-05-11 16:40:59 +00:00
}
2019-05-27 23:21:53 +00:00
p . bot . Send ( c , bot . Message , reminder . channel , message )
2020-05-17 14:49:38 +00:00
smsPlugin := sms . New ( p . bot )
if err := smsPlugin . Send ( reminder . who , message ) ; err != nil {
log . Error ( ) . Err ( err ) . Msgf ( "could not send reminder" )
}
2017-05-11 16:40:59 +00:00
2018-02-05 21:04:40 +00:00
if err := p . deleteReminder ( reminder . id ) ; err != nil {
2019-03-07 16:35:42 +00:00
log . Error ( ) .
Int64 ( "id" , reminder . id ) .
Err ( err ) .
Msg ( "this will cause problems, we need to stop now." )
2017-05-11 16:40:59 +00:00
}
}
p . queueUpNextReminder ( )
}
}