blob: 3095c0025f36e11c3486cf0cb9066300963064b2 [file] [log] [blame]
package main
import (
"context"
"flag"
"fmt"
"net/http"
"regexp"
"strings"
"time"
"code.hackerspace.pl/hscloud/go/mirko"
"github.com/golang/glog"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
pb "code.hackerspace.pl/hscloud/hswaw/smsgw/proto"
)
var (
flagTwilioSID string
flagTwilioToken string
flagTwilioFriendlyPhone string
flagWebhookListen string
flagWebhookPublic string
)
func init() {
flag.Set("logtostderr", "true")
}
type server struct {
dispatcher *dispatcher
}
func ourPhoneNumber(ctx context.Context, t *twilio, friendly string) (*incomingPhoneNumber, error) {
ipn, err := t.getIncomingPhoneNumbers(ctx)
if err != nil {
return nil, err
}
for _, pn := range ipn {
if pn.FriendlyName == friendly {
return &pn, nil
}
}
return nil, fmt.Errorf("requested phone number %q not in list", friendly)
}
func ensureWebhook(ctx context.Context, t *twilio) {
pn, err := ourPhoneNumber(ctx, t, flagTwilioFriendlyPhone)
if err != nil {
glog.Exitf("could not get our phone number: %v", err)
}
url := fmt.Sprintf("%ssms", flagWebhookPublic)
// first setup.
if pn.SMSMethod != "POST" || pn.SMSURL != url {
glog.Infof("Updating webhook (is %s %q, want %s %q)", pn.SMSMethod, pn.SMSURL, "POST", url)
if err := t.updateIncomingPhoneNumberSMSWebhook(ctx, pn.SID, "POST", url); err != nil {
glog.Exitf("could not set webhook: %v")
}
// try again to check that it's actually set
for {
pn, err = ourPhoneNumber(ctx, t, flagTwilioFriendlyPhone)
if err != nil {
glog.Exitf("could not get our phone number: %v", err)
}
if pn.SMSMethod == "POST" || pn.SMSURL == url {
break
}
glog.Infof("Webhook not yet ready, currently %s %q", pn.SMSMethod, pn.SMSURL)
time.Sleep(5 * time.Second)
}
glog.Infof("Webhook verified")
} else {
glog.Infof("Webhook up to date")
}
// now keep checking to make sure that nobody takes over our webhook
tick := time.NewTicker(30 * time.Second)
for {
select {
case <-ctx.Done():
return
case <-tick.C:
pn, err = ourPhoneNumber(ctx, t, flagTwilioFriendlyPhone)
if err != nil {
glog.Exitf("could not get our phone number: %v", err)
}
if pn.SMSMethod != "POST" || pn.SMSURL != url {
glog.Exitf("Webhook got deconfigured, not %s %q", pn.SMSMethod, pn.SMSURL)
}
}
}
}
func (s *server) webhookHandler(w http.ResponseWriter, r *http.Request) {
if err := r.ParseForm(); err != nil {
glog.Errorf("webhook body parse error: %v", err)
return
}
accountSID := r.PostForm.Get("AccountSid")
if accountSID != flagTwilioSID {
glog.Errorf("webhook got wrong account sid, got %q, wanted %q", accountSID, flagTwilioSID)
return
}
body := r.PostForm.Get("Body")
if body == "" {
return
}
from := r.PostForm.Get("From")
glog.Infof("Got SMS from %q, body %q", from, body)
s.dispatcher.publish(&sms{
from: from,
body: body,
timestamp: time.Now(),
})
w.WriteHeader(200)
}
func main() {
flag.StringVar(&flagTwilioSID, "twilio_sid", "", "Twilio account SID")
flag.StringVar(&flagTwilioToken, "twilio_token", "", "Twilio auth token")
flag.StringVar(&flagTwilioFriendlyPhone, "twilio_friendly_phone", "", "Twilio friendly phone number")
flag.StringVar(&flagWebhookListen, "webhook_listen", "127.0.0.1:5000", "Listen address for webhook handler")
flag.StringVar(&flagWebhookPublic, "webhook_public", "", "Public address for webhook handler (wg. http://proxy.q3k.org/smsgw/)")
flag.Parse()
if flagTwilioSID == "" || flagTwilioToken == "" {
glog.Exitf("twilio_sid and twilio_token must be set")
}
if flagTwilioFriendlyPhone == "" {
glog.Exitf("twilio_friendly_phone must be set")
}
if flagWebhookPublic == "" {
glog.Exitf("webhook_public must be set")
}
if !strings.HasSuffix(flagWebhookPublic, "/") {
flagWebhookPublic += "/"
}
s := &server{
dispatcher: newDispatcher(),
}
m := mirko.New()
if err := m.Listen(); err != nil {
glog.Exitf("Listen(): %v", err)
}
webhookMux := http.NewServeMux()
webhookMux.HandleFunc("/sms", s.webhookHandler)
webhookSrv := http.Server{
Addr: flagWebhookListen,
Handler: webhookMux,
}
go func() {
if err := webhookSrv.ListenAndServe(); err != nil {
glog.Exitf("webhook ListenAndServe: %v", err)
}
}()
t := &twilio{
accountSID: flagTwilioSID,
accountToken: flagTwilioToken,
}
go ensureWebhook(m.Context(), t)
go s.dispatcher.run(m.Context())
pb.RegisterSMSGatewayServer(m.GRPC(), s)
if err := m.Serve(); err != nil {
glog.Exitf("Serve(): %v", err)
}
<-m.Done()
}
func (s *server) Messages(req *pb.MessagesRequest, stream pb.SMSGateway_MessagesServer) error {
re := regexp.MustCompile(".*")
if req.FilterBody != "" {
var err error
re, err = regexp.Compile(req.FilterBody)
if err != nil {
return status.Errorf(codes.InvalidArgument, "filter regexp error: %v", err)
}
}
data := make(chan *sms)
cancel := make(chan struct{})
defer func() {
close(cancel)
close(data)
}()
s.dispatcher.subscribe(&subscriber{
re: re,
data: data,
cancel: cancel,
})
for d := range data {
stream.Send(&pb.MessagesResponse{
Sender: d.from,
Body: d.body,
Timestamp: d.timestamp.UnixNano(),
})
}
return nil
}