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 }