2020-05-14 20:48:41 +00:00
|
|
|
package sms
|
|
|
|
|
|
|
|
import (
|
2020-05-15 14:22:23 +00:00
|
|
|
"fmt"
|
2021-12-20 17:40:10 +00:00
|
|
|
bh "github.com/timshannon/bolthold"
|
2020-05-15 14:22:23 +00:00
|
|
|
"net/http"
|
|
|
|
"regexp"
|
|
|
|
"strings"
|
|
|
|
|
2021-07-28 15:32:59 +00:00
|
|
|
"github.com/go-chi/chi/v5"
|
2020-05-15 14:22:23 +00:00
|
|
|
twilio "github.com/kevinburke/twilio-go"
|
|
|
|
"github.com/rs/zerolog/log"
|
|
|
|
|
2020-05-14 20:48:41 +00:00
|
|
|
"github.com/velour/catbase/bot"
|
|
|
|
"github.com/velour/catbase/bot/msg"
|
|
|
|
"github.com/velour/catbase/config"
|
|
|
|
)
|
|
|
|
|
2020-05-15 14:22:23 +00:00
|
|
|
var plugin *SMSPlugin
|
|
|
|
|
2020-05-14 20:48:41 +00:00
|
|
|
type SMSPlugin struct {
|
2021-12-20 17:40:10 +00:00
|
|
|
b bot.Bot
|
|
|
|
c *config.Config
|
|
|
|
store *bh.Store
|
|
|
|
}
|
|
|
|
|
|
|
|
type Person struct {
|
|
|
|
Who string
|
|
|
|
Num string
|
2020-05-14 20:48:41 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func New(b bot.Bot) *SMSPlugin {
|
2020-05-15 14:22:23 +00:00
|
|
|
if plugin != nil {
|
|
|
|
return plugin
|
|
|
|
}
|
|
|
|
plugin = &SMSPlugin{
|
2021-12-20 17:40:10 +00:00
|
|
|
b: b,
|
|
|
|
c: b.Config(),
|
|
|
|
store: b.Store(),
|
2020-05-15 14:22:23 +00:00
|
|
|
}
|
2020-05-17 14:49:38 +00:00
|
|
|
plugin.registerWeb()
|
2021-01-31 21:48:53 +00:00
|
|
|
|
|
|
|
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)
|
|
|
|
|
2020-05-15 14:22:23 +00:00
|
|
|
b.Register(plugin, bot.Help, plugin.help)
|
|
|
|
return plugin
|
|
|
|
}
|
|
|
|
|
|
|
|
func (p *SMSPlugin) checkNumber(num string) (string, error) {
|
|
|
|
expectedLen := 10
|
|
|
|
|
|
|
|
if len(num) != expectedLen {
|
|
|
|
return num, fmt.Errorf("invalid number length: %d, expected %d", len(num), expectedLen)
|
2020-05-14 20:48:41 +00:00
|
|
|
}
|
2020-05-15 14:22:23 +00:00
|
|
|
|
|
|
|
return num, nil
|
|
|
|
}
|
|
|
|
|
2021-01-31 21:48:53 +00:00
|
|
|
var regSelfRegex = regexp.MustCompile(`(?i)my sms number is (?P<number>\d+)`)
|
|
|
|
var regSomeoneRegex = regexp.MustCompile(`(?i)register sms for (?P<name>\S+) (?P<number>\d+)`)
|
2020-05-17 15:13:40 +00:00
|
|
|
var sendRegex = regexp.MustCompile(`(?i)send sms to (?P<name>\S+) (?P<body>.+)`)
|
2021-01-31 21:48:53 +00:00
|
|
|
var deleteRegex = regexp.MustCompile(`(?i)delete my sms$`)
|
2020-05-15 14:22:23 +00:00
|
|
|
|
|
|
|
// Send will send a text to a registered user, who
|
|
|
|
func (p *SMSPlugin) Send(who, message string) error {
|
2020-05-17 14:49:38 +00:00
|
|
|
num, err := p.get(who)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("unknown user: %s: %w", who, err)
|
2020-05-15 14:22:23 +00:00
|
|
|
}
|
|
|
|
sid := p.c.Get("TWILIOSID", "")
|
|
|
|
token := p.c.Get("TWILIOTOKEN", "")
|
|
|
|
myNum := p.c.Get("TWILIONUMBER", "")
|
|
|
|
|
2021-01-31 21:48:53 +00:00
|
|
|
if sid == "" || token == "" || myNum == "" {
|
|
|
|
return fmt.Errorf("this bot is not configured for Twilio")
|
2020-05-15 14:22:23 +00:00
|
|
|
}
|
|
|
|
|
2021-01-31 21:48:53 +00:00
|
|
|
client := twilio.NewClient(sid, token, nil)
|
2020-05-17 15:13:40 +00:00
|
|
|
|
2021-01-31 21:48:53 +00:00
|
|
|
_, err = client.Messages.SendMessage(myNum, num, message, nil)
|
|
|
|
return err
|
2020-05-17 15:13:40 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (p *SMSPlugin) reg(c bot.Connector, ch, who, num string) bool {
|
|
|
|
num, err := p.checkNumber(num)
|
|
|
|
if err != nil {
|
|
|
|
p.b.Send(c, bot.Message, ch, fmt.Sprintf("That number didn't make sense to me: %s", err))
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
me := p.b.WhoAmI()
|
|
|
|
m := fmt.Sprintf("Hello from %s. You are registered for reminders and you can chat with the channel "+
|
|
|
|
"by talking to this number", me)
|
|
|
|
err = p.add(who, num)
|
|
|
|
if err != nil {
|
|
|
|
log.Error().Err(err).Msgf("error adding user")
|
|
|
|
p.b.Send(c, bot.Message, ch, fmt.Sprintf("I didn't send the message, %s", err))
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
err = p.Send(who, m)
|
|
|
|
if err != nil {
|
|
|
|
log.Error().Err(err).Msgf("error sending to user")
|
|
|
|
if err := p.delete(who); err != nil {
|
|
|
|
log.Error().Err(err).Msgf("error deleting user")
|
2020-05-15 14:22:23 +00:00
|
|
|
}
|
2020-05-17 15:13:40 +00:00
|
|
|
p.b.Send(c, bot.Message, ch, fmt.Sprintf("I didn't send the message, %s", err))
|
2020-05-15 14:22:23 +00:00
|
|
|
return true
|
|
|
|
}
|
2020-05-17 15:13:40 +00:00
|
|
|
p.b.Send(c, bot.Message, ch, "I sent a message to them.")
|
|
|
|
return true
|
2020-05-14 20:48:41 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (p *SMSPlugin) help(c bot.Connector, kind bot.Kind, message msg.Message, args ...interface{}) bool {
|
|
|
|
ch := message.Channel
|
2021-01-31 21:48:53 +00:00
|
|
|
m := fmt.Sprintf("You can register your number with: `%s`", regSelfRegex)
|
|
|
|
m += fmt.Sprintf("\nYou can register somebody else with: `%s`", regSomeoneRegex)
|
2020-05-17 14:57:45 +00:00
|
|
|
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.",
|
|
|
|
p.c.Get("TWILIONUMBER", "unknown"))
|
|
|
|
m += "\nAll reminders with registered users will be sent to their SMS number."
|
|
|
|
p.b.Send(c, bot.Message, ch, m)
|
2020-05-14 20:48:41 +00:00
|
|
|
return true
|
|
|
|
}
|
2020-05-15 14:22:23 +00:00
|
|
|
|
|
|
|
func (p *SMSPlugin) registerWeb() {
|
2021-07-28 15:32:59 +00:00
|
|
|
r := chi.NewRouter()
|
|
|
|
r.HandleFunc("/new", p.receive)
|
|
|
|
p.b.RegisterWeb(r, "/sms")
|
2020-05-15 14:22:23 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (p *SMSPlugin) receive(w http.ResponseWriter, r *http.Request) {
|
2020-05-17 14:49:38 +00:00
|
|
|
if r.Method != http.MethodPost {
|
|
|
|
w.WriteHeader(405)
|
|
|
|
fmt.Fprintf(w, "Incorrect HTTP method")
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := r.ParseForm(); err != nil {
|
|
|
|
log.Error().Err(err).Msgf("could not parse incoming SMS")
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
body := r.PostFormValue("Body")
|
|
|
|
number := strings.TrimPrefix(r.PostFormValue("From"), "+1")
|
|
|
|
from, err := p.getByNumber(number)
|
|
|
|
if err != nil {
|
|
|
|
log.Debug().Err(err).Msgf("could not find user")
|
|
|
|
from = number
|
|
|
|
}
|
|
|
|
|
|
|
|
m := fmt.Sprintf("SMS From %s: %s", from, body)
|
|
|
|
chs := p.c.GetArray("channels", []string{})
|
2020-05-15 14:22:23 +00:00
|
|
|
|
2020-05-17 14:49:38 +00:00
|
|
|
log.Debug().Msgf("%s to %v", m, chs)
|
|
|
|
|
|
|
|
for _, ch := range chs {
|
|
|
|
p.b.Send(p.b.DefaultConnector(), bot.Message, ch, m)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (p *SMSPlugin) add(who, num string) error {
|
2021-12-20 17:40:10 +00:00
|
|
|
person := Person{who, num}
|
|
|
|
return p.store.Upsert(who, person)
|
2020-05-17 14:49:38 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (p *SMSPlugin) delete(who string) error {
|
2021-12-20 17:40:10 +00:00
|
|
|
return p.store.Delete(who, Person{})
|
2020-05-17 14:49:38 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (p *SMSPlugin) get(who string) (string, error) {
|
2021-12-20 17:40:10 +00:00
|
|
|
person := Person{}
|
|
|
|
err := p.store.Get(who, &person)
|
|
|
|
return person.Num, err
|
2020-05-17 14:49:38 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (p *SMSPlugin) getByNumber(num string) (string, error) {
|
2021-12-20 17:40:10 +00:00
|
|
|
person := Person{}
|
|
|
|
err := p.store.Find(&person, bh.Where("num").Eq(num))
|
|
|
|
return person.Who, err
|
2020-05-15 14:22:23 +00:00
|
|
|
}
|
2021-01-31 21:48:53 +00:00
|
|
|
|
|
|
|
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"])
|
|
|
|
}
|