From aad4ecf14371b4f78a0dc839bcf8d8dff3ef82af Mon Sep 17 00:00:00 2001 From: Chris Sexton Date: Sun, 31 Jan 2021 16:48:53 -0500 Subject: [PATCH] bot: refactor callback handlers New system: * Each callback can filter for a regex * Backwards compatability using a `.*` generic regex * Handlers now accept a request object instead of bare arguments All new plugins should use this new system. --- bot/bot.go | 23 ++++++-- bot/handlers.go | 29 ++++++++-- bot/interfaces.go | 19 ++++++- bot/mock.go | 10 ++-- plugins/sms/sms.go | 130 ++++++++++++++++++++++++--------------------- 5 files changed, 136 insertions(+), 75 deletions(-) diff --git a/bot/bot.go b/bot/bot.go index 0067a91..bbc736d 100644 --- a/bot/bot.go +++ b/bot/bot.go @@ -230,16 +230,29 @@ func (b *bot) RegisterFilter(name string, f func(string) string) { b.filters[name] = f } -// Register a callback -func (b *bot) Register(p Plugin, kind Kind, cb Callback) { +// 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() if _, ok := b.callbacks[t]; !ok { - b.callbacks[t] = make(map[Kind][]Callback) + b.callbacks[t] = make(map[Kind]map[*regexp.Regexp][]ResponseHandler) } if _, ok := b.callbacks[t][kind]; !ok { - b.callbacks[t][kind] = []Callback{} + b.callbacks[t][kind] = map[*regexp.Regexp][]ResponseHandler{} } - b.callbacks[t][kind] = append(b.callbacks[t][kind], cb) + if _, ok := b.callbacks[t][kind][r]; !ok { + b.callbacks[t][kind][r] = []ResponseHandler{} + } + b.callbacks[t][kind][r] = append(b.callbacks[t][kind][r], resp) +} + +// Register a callback +// This function should be considered deprecated. +func (b *bot) Register(p Plugin, kind Kind, cb Callback) { + r := regexp.MustCompile(`.*`) + resp := func(r Request) bool { + return cb(r.Conn, r.Kind, r.Msg, r.Args...) + } + b.RegisterRegex(p, kind, r, resp) } func (b *bot) RegisterWeb(root, name string) { diff --git a/bot/handlers.go b/bot/handlers.go index 1232ec4..40664b8 100644 --- a/bot/handlers.go +++ b/bot/handlers.go @@ -41,11 +41,34 @@ RET: return true } +func parseValues(r *regexp.Regexp, body string) RegexValues { + out := RegexValues{} + subs := r.FindStringSubmatch(body) + if len(subs) == 0 { + return out + } + for i, n := range r.SubexpNames() { + out[n] = subs[i] + } + return out +} + func (b *bot) runCallback(conn Connector, plugin Plugin, evt Kind, message msg.Message, args ...interface{}) bool { t := reflect.TypeOf(plugin).String() - for _, cb := range b.callbacks[t][evt] { - if cb(conn, evt, message, args...) { - return true + for r, cbs := range b.callbacks[t][evt] { + if r.MatchString(message.Body) { + for _, cb := range cbs { + resp := Request{ + Conn: conn, + Kind: evt, + Msg: message, + Values: parseValues(r, message.Body), + Args: args, + } + if cb(resp) { + return true + } + } } } return false diff --git a/bot/interfaces.go b/bot/interfaces.go index 98a5160..f47a8b8 100644 --- a/bot/interfaces.go +++ b/bot/interfaces.go @@ -3,6 +3,8 @@ package bot import ( + "regexp" + "github.com/jmoiron/sqlx" "github.com/velour/catbase/bot/msg" @@ -46,9 +48,20 @@ type ImageAttachment struct { Height int } +type Request struct { + Conn Connector + Kind Kind + Msg msg.Message + Values RegexValues + Args []interface{} +} + type Kind int type Callback func(Connector, Kind, msg.Message, ...interface{}) bool -type CallbackMap map[string]map[Kind][]Callback +type ResponseHandler func(Request) bool +type CallbackMap map[string]map[Kind]map[*regexp.Regexp][]ResponseHandler + +type RegexValues map[string]string // b interface serves to allow mocking of the actual bot type Bot interface { @@ -77,6 +90,10 @@ type Bot interface { // The Kind arg should be one of bot.Message/Reply/Action/etc Receive(Connector, Kind, msg.Message, ...interface{}) bool + // Register a plugin callback + // Kind will be matched to the event for the callback + RegisterRegex(Plugin, Kind, *regexp.Regexp, ResponseHandler) + // Register a plugin callback // Kind will be matched to the event for the callback Register(Plugin, Kind, Callback) diff --git a/bot/mock.go b/bot/mock.go index dbb7c5e..33143ed 100644 --- a/bot/mock.go +++ b/bot/mock.go @@ -5,6 +5,7 @@ package bot import ( "fmt" "net/http" + "regexp" "strconv" "strings" @@ -51,10 +52,11 @@ func (mb *MockBot) Send(c Connector, kind Kind, args ...interface{}) (string, er } return "ERR", fmt.Errorf("Mesasge type unhandled") } -func (mb *MockBot) AddPlugin(f Plugin) {} -func (mb *MockBot) Register(p Plugin, kind Kind, cb Callback) {} -func (mb *MockBot) RegisterWeb(_, _ string) {} -func (mb *MockBot) GetWebNavigation() []EndPoint { return nil } +func (mb *MockBot) AddPlugin(f Plugin) {} +func (mb *MockBot) Register(p Plugin, kind Kind, cb Callback) {} +func (mb *MockBot) RegisterRegex(p Plugin, kind Kind, r *regexp.Regexp, h ResponseHandler) {} +func (mb *MockBot) RegisterWeb(_, _ string) {} +func (mb *MockBot) GetWebNavigation() []EndPoint { return nil } func (mb *MockBot) Receive(c Connector, kind Kind, msg msg.Message, args ...interface{}) bool { return false } diff --git a/plugins/sms/sms.go b/plugins/sms/sms.go index c56297e..fbc00b8 100644 --- a/plugins/sms/sms.go +++ b/plugins/sms/sms.go @@ -31,7 +31,12 @@ func New(b bot.Bot) *SMSPlugin { } plugin.setup() plugin.registerWeb() - b.Register(plugin, bot.Message, plugin.message) + + b.RegisterRegex(plugin, bot.Message, deleteRegex, plugin.deleteCmd) + b.RegisterRegex(plugin, bot.Message, sendRegex, plugin.sendCmd) + b.RegisterRegex(plugin, bot.Message, regSomeoneRegex, plugin.registerOtherCmd) + b.RegisterRegex(plugin, bot.Message, regSelfRegex, plugin.registerSelfCmd) + b.Register(plugin, bot.Help, plugin.help) return plugin } @@ -46,9 +51,10 @@ func (p *SMSPlugin) checkNumber(num string) (string, error) { return num, nil } -var regRegex = regexp.MustCompile(`(?i)my sms number is (\d+)`) -var reg2Regex = regexp.MustCompile(`(?i)register sms for (?P\S+) (\d+)`) +var regSelfRegex = regexp.MustCompile(`(?i)my sms number is (?P\d+)`) +var regSomeoneRegex = regexp.MustCompile(`(?i)register sms for (?P\S+) (?P\d+)`) var sendRegex = regexp.MustCompile(`(?i)send sms to (?P\S+) (?P.+)`) +var deleteRegex = regexp.MustCompile(`(?i)delete my sms$`) // Send will send a text to a registered user, who func (p *SMSPlugin) Send(who, message string) error { @@ -59,69 +65,17 @@ func (p *SMSPlugin) Send(who, message string) error { sid := p.c.Get("TWILIOSID", "") token := p.c.Get("TWILIOTOKEN", "") myNum := p.c.Get("TWILIONUMBER", "") + + if sid == "" || token == "" || myNum == "" { + return fmt.Errorf("this bot is not configured for Twilio") + } + client := twilio.NewClient(sid, token, nil) _, err = client.Messages.SendMessage(myNum, num, message, nil) return err } -func (p *SMSPlugin) message(c bot.Connector, kind bot.Kind, message msg.Message, args ...interface{}) bool { - ch, chName := message.Channel, message.ChannelName - body := strings.TrimSpace(message.Body) - who := message.User.Name - - log.Debug().Bytes("body", []byte(body)).Msgf("body: '%s', match: %v, %#v", body, sendRegex.MatchString(body), sendRegex.FindStringSubmatch(body)) - - if strings.ToLower(body) == "delete my sms" { - if err := p.delete(who); err != nil { - p.b.Send(c, bot.Message, ch, "Something went wrong.") - return true - } - p.b.Send(c, bot.Message, ch, "Okay.") - return true - } - - if sendRegex.MatchString(body) { - subs := sendRegex.FindStringSubmatch(body) - if len(subs) != 3 { - p.b.Send(c, bot.Message, ch, fmt.Sprintf("I didn't send the message. Your request makes no sense.")) - return true - } - user, body := subs[1], fmt.Sprintf("#%s: %s", chName, subs[2]) - err := p.Send(user, body) - if err != nil { - p.b.Send(c, bot.Message, ch, fmt.Sprintf("I didn't send the message, %s", err)) - return true - } - p.b.Send(c, bot.Message, ch, "I sent a message to them.") - return true - } - - if reg2Regex.MatchString(body) { - subs := reg2Regex.FindStringSubmatch(body) - if subs == nil || len(subs) != 3 { - p.b.Send(c, bot.Message, ch, fmt.Sprintf("if you're trying to register somebody, give me a "+ - "message of the format: `%s`", reg2Regex)) - return true - } - - return p.reg(c, ch, subs[1], subs[2]) - } - - if regRegex.MatchString(body) { - subs := regRegex.FindStringSubmatch(body) - if subs == nil || len(subs) != 2 { - p.b.Send(c, bot.Message, ch, fmt.Sprintf("if you're trying to register a number, give me a "+ - "message of the format: `%s`", regRegex)) - return true - } - - return p.reg(c, ch, who, subs[1]) - - } - return false -} - func (p *SMSPlugin) reg(c bot.Connector, ch, who, num string) bool { num, err := p.checkNumber(num) if err != nil { @@ -152,8 +106,8 @@ func (p *SMSPlugin) reg(c bot.Connector, ch, who, num string) bool { func (p *SMSPlugin) help(c bot.Connector, kind bot.Kind, message msg.Message, args ...interface{}) bool { ch := message.Channel - m := fmt.Sprintf("You can register your number with: `%s`", regRegex) - m += fmt.Sprintf("\nYou can register somebody else with: `%s`", reg2Regex) + m := fmt.Sprintf("You can register your number with: `%s`", regSelfRegex) + m += fmt.Sprintf("\nYou can register somebody else with: `%s`", regSomeoneRegex) m += fmt.Sprintf("\nYou can send a message to a user with: `%s`", sendRegex) m += "\nYou can deregister with: `delete my sms`" m += fmt.Sprintf("\nAll messages sent to %s will come to the channel.", @@ -244,3 +198,55 @@ func (p *SMSPlugin) setup() { log.Fatal().Err(err).Msgf("could not create sms table") } } + +func (p *SMSPlugin) deleteCmd(r bot.Request) bool { + ch := r.Msg.Channel + if err := p.delete(r.Msg.User.Name); err != nil { + p.b.Send(r.Conn, bot.Message, ch, "Something went wrong.") + return true + } + p.b.Send(r.Conn, bot.Message, ch, "Okay.") + return true +} + +func (p *SMSPlugin) sendCmd(r bot.Request) bool { + if r.Msg.User.Name == p.c.Get("nick", "") { + return false + } + ch := r.Msg.Channel + chName := r.Msg.ChannelName + c := r.Conn + if r.Values["name"] == "" || r.Values["body"] == "" { + p.b.Send(c, bot.Message, ch, fmt.Sprintf("I didn't send the message. Your request makes no sense.")) + return true + } + user, body := r.Values["name"], fmt.Sprintf("#%s: %s", chName, r.Values["body"]) + err := p.Send(user, body) + if err != nil { + p.b.Send(c, bot.Message, ch, fmt.Sprintf("I didn't send the message, %s", err)) + return true + } + p.b.Send(c, bot.Message, ch, "I sent a message to them.") + return true +} + +func (p *SMSPlugin) registerOtherCmd(r bot.Request) bool { + ch := r.Msg.Channel + if r.Values["name"] == "" || r.Values["number"] == "" { + p.b.Send(r.Conn, bot.Message, ch, fmt.Sprintf("if you're trying to register somebody, give me a "+ + "message of the format: `%s`", regSomeoneRegex)) + return true + } + + return p.reg(r.Conn, ch, r.Values["name"], r.Values["number"]) +} + +func (p *SMSPlugin) registerSelfCmd(r bot.Request) bool { + ch := r.Msg.Channel + who := r.Msg.User.Name + if r.Values["number"] == "" { + p.b.Send(r.Conn, bot.Message, ch, fmt.Sprintf("if you're trying to register a number, give me a "+ + "message of the format: `%s`", regSelfRegex)) + } + return p.reg(r.Conn, ch, who, r.Values["number"]) +}