package twilioroute import ( "fmt" "log" "net/http" "strings" "sync" "time" "bitbucket.org/nemt/nemt-portal-api/application/applicationservice" "bitbucket.org/nemt/nemt-portal-api/application/notificationservice" "bitbucket.org/nemt/nemt-portal-api/application/tncservice" "bitbucket.org/nemt/nemt-portal-api/application/viewmodel" "bitbucket.org/nemt/nemt-portal-api/infra/config" "bitbucket.org/nemt/nemt-portal-api/server/router/routeutils" "github.com/gorilla/websocket" "github.com/labstack/echo" "github.com/ttacon/libphonenumber" ) var ( instance *controller once sync.Once upgrader = websocket.Upgrader{ ReadBufferSize: 1024, WriteBufferSize: 1024, CheckOrigin: func(r *http.Request) bool { return true }, } ) type controller struct { cfg *config.Config svc *applicationservice.Service tnc *tncservice.Service notification *notificationservice.Service broadcast chan viewmodel.SocketMessage clients map[*websocket.Conn]viewmodel.SocketMessage } func controllerInstance(cfg *config.Config, svc *applicationservice.Service, tnc *tncservice.Service, notification *notificationservice.Service) *controller { once.Do(func() { instance = &controller{ cfg: cfg, svc: svc, tnc: tnc, notification: notification, broadcast: make(chan viewmodel.SocketMessage), clients: make(map[*websocket.Conn]viewmodel.SocketMessage), } go instance.handleMessages() }) return instance } type Response struct { Message *Message `xml:"Message,omitempty"` } type Message struct { To string `xml:"to,attr"` Body string `xml:"Body,omitempty"` } func (c *controller) getDriverNotification(ride viewmodel.Ride, notificationType string, subject string, message string, to string, from string) viewmodel.Notification { retVal := viewmodel.Notification{ Type: notificationType, Message: message, Subject: subject, Ride: ride, User: ride.User, CreatedUser: ride.CreatedUser, To: to, From: from, } return retVal } func (c *controller) handleTwilio(ctx echo.Context) error { var twilioResponse viewmodel.TwilioWebhook err := ctx.Bind(&twilioResponse) if err != nil { fmt.Println("Error binding response: ", err.Error()) return routeutils.HandleAPIError(ctx, err) } // bResp, _ := json.MarshalIndent(twilioResponse, "", "\t") // fmt.Println("Twilio Request") // fmt.Println(string(bResp)) var resp Response requestMessage := strings.ToUpper(strings.TrimSpace(twilioResponse.Body)) if requestMessage != "CANCEL RIDE" && requestMessage != "YES" && requestMessage != "NO" && !strings.HasPrefix(requestMessage, "DRIVER") && !strings.HasPrefix(requestMessage, "MEMBER") && requestMessage != "I AM READY" { lastRide, err := c.svc.Rides.GetLastRideByPhoneNumber(twilioResponse.From, "", "") if err != nil { fmt.Println("Error to get the ride: ", err.Error()) } notifications := make([]viewmodel.Notification, 0) notifications = append(notifications, c.svc.Notification.GetNotification(lastRide, "sms", "Message to Dispatcher", twilioResponse.Body, true, "message")) notifications = append(notifications, c.svc.Notification.GetNotification(lastRide, "app", "Message to Dispatcher", twilioResponse.Body, true, "message")) notifications, err = c.svc.Notification.SendNotifications(notifications) if err != nil { fmt.Println("Error to notify: ", err.Error()) } } else { toNumber := twilioResponse.From var message string var lastRide viewmodel.Ride isDriver := false num, _ := libphonenumber.Parse(twilioResponse.From, "US") if strings.HasPrefix(requestMessage, "MEMBER") { isDriver = true lastRide, err = c.svc.Rides.GetLastRideByDriversNumber(twilioResponse.From) if err != nil { fmt.Println("Error to get last driver ride: ", err.Error()) message = fmt.Sprintf("We received a request to cancel a ride from you at %s, but cannot find a ride for this mobile number.", libphonenumber.Format(num, libphonenumber.NATIONAL)) } } else { lastRide, err = c.svc.Rides.GetLastRideByPhoneNumber(twilioResponse.From, "", "") if err != nil { fmt.Println("Error to get last ride: ", err.Error()) message = fmt.Sprintf("We received a request to cancel a ride from you at %s, but cannot find a ride for this mobile number.", libphonenumber.Format(num, libphonenumber.NATIONAL)) } } if !isDriver { if requestMessage == "I AM READY" && lastRide.UUID != "" { if (lastRide.Visit.TripType.Key == "roundtrip_call" && (lastRide.TripType.Key == "to_visit" || lastRide.TripType.Key == "from_visit_call")) || lastRide.Visit.TripType.Key == "from_visit_call" { var readyRide viewmodel.Ride authUser, err := c.svc.Users.GetByUUID(lastRide.CreatedUser.ID, "") if err != nil { fmt.Println("Error to get created user: ", err.Error()) } if lastRide.TripType.Key == "to_visit" { readyRide, err = c.svc.Rides.GetByVisitUUIDAndTripType(lastRide.Visit.UUID, "from_visit_call", authUser) if err != nil || readyRide.UUID == "" { message = fmt.Sprintf("We received a request to send the return ride from you at %s, but you do not have the a return ride prepared.", libphonenumber.Format(num, libphonenumber.NATIONAL)) } } else { readyRide = lastRide } switch readyRide.Status.Key { case "pending", "accepted", "scheduled": message = "We already request your return ride, you should receive updates soon." case "willCall": var lyftRide viewmodel.RideRequest lyftRide.RideID = "" lyftRide.Destination = readyRide.Route.Destination lyftRide.Origin = readyRide.Route.Origin name := readyRide.User.Name names := strings.Split(name, " ") lyftRide.Passenger.FirstName = names[0] lyftRide.Passenger.LastName = " " lyftRide.Passenger.PhoneNumber = *readyRide.User.PhoneNumber lyftRide.RideType = "lyft" if c.cfg.LyftProd.UserUUID != readyRide.CreatedUser.ID { lyftRide, err = c.tnc.Lyft.RequestRide(lyftRide) } else { fmt.Println("In Production") lyftRide, err = c.tnc.LyftProd.RequestRide(lyftRide) } if err != nil { fmt.Println("Error to create a lyft ride on twilio: ", err.Error()) message = "There was a problem to call your ride" } readyRide, err = c.svc.Rides.UpdateNewRide(readyRide, lyftRide, authUser) if err != nil { fmt.Println("Error to update ride: ", err.Error()) message = "There was a problem to call your ride" } go func() { err = c.svc.Notification.SendNotification(readyRide.Status.Key, readyRide, lyftRide) if err != nil { fmt.Println("Error to notify user: ", err.Error()) } }() message = fmt.Sprintf("Ride requested at %s, for pickup at %s.", lyftRide.RequestAt.Format("03:04 PM"), readyRide.Route.Destination.Name) } } } if requestMessage == "CANCEL RIDE" && lastRide.UUID != "" { var visitDate time.Time if lastRide.VisitTime != nil { visitDate = *lastRide.VisitTime } else { visitDate = *lastRide.VisitDate } var pickupDate time.Time if lastRide.PickupTime != nil { pickupDate = *lastRide.PickupTime } message = fmt.Sprintf("Are you sure you want to cancel a ride to %s visit to %s scheduled for %s?\nReply YES to confirm this cancellation", visitDate.Format("01/02/2006 03:04 PM"), lastRide.Route.Destination.Name, pickupDate.Format("03:04 PM")) } if requestMessage == "YES" && lastRide.UUID != "" { var lyftRide viewmodel.RideRequest if lastRide.CreatedUser.ID != c.cfg.LyftProd.UserUUID { lyftRide, err = c.tnc.Lyft.GetRideDetails(viewmodel.RideRequest{RideID: lastRide.InternalID}) if err != nil { fmt.Println("Error: ", err.Error()) message = "There was a problem to find your ride" } if err = c.tnc.Lyft.CancelRide(lyftRide); err != nil { fmt.Println("Error to cancel with Lyft: ", err.Error()) message = "There was a problem to cancel your ride with our transportation provider" } } else { fmt.Println("In Production") lyftRide, err = c.tnc.LyftProd.GetRideDetails(viewmodel.RideRequest{RideID: lastRide.InternalID}) if err != nil { fmt.Println("Error: ", err.Error()) message = "There was a problem to find your ride" } if err = c.tnc.LyftProd.CancelRide(lyftRide); err != nil { fmt.Println("Error to cancel with Lyft: ", err.Error()) message = "There was a problem to cancel your ride with our transportation provider" } } if err = c.svc.Rides.UpdateStatus(lastRide.UUID, "canceled"); err != nil { fmt.Println("Error to cancel the ride: ", err.Error()) message = "There was a problem to cancel your ride on our systems" } var dateToFormat time.Time if lastRide.VisitTime != nil { dateToFormat = *lastRide.VisitTime } else { dateToFormat = *lastRide.VisitDate } var pickupDate time.Time if lastRide.PickupTime != nil { pickupDate = *lastRide.PickupTime } lastRide.Status.Key = "cancelled" message = fmt.Sprintf("A ride to a %s visit to %s scheduled for %s has been cancelled.", dateToFormat.Format("01/02/2006 03:04 PM"), lastRide.Route.Destination.Name, pickupDate.Format("03:04 PM")) } // if requestMessage == "NO" && lastRide.UUID != "" { // var dateToFormat time.Time // if lastRide.VisitTime != nil { // dateToFormat = *lastRide.VisitTime // } else { // dateToFormat = *lastRide.VisitDate // } // message = fmt.Sprintf("You ride is still scheduled on %s at %s", lastRide.Route.Destination.Name, dateToFormat.Format("01/02/2006 03:04 PM")) // } if strings.HasPrefix(requestMessage, "DRIVER") && lastRide.UUID != "" { driverMessage := strings.Replace(requestMessage, "DRIVER", "", -1) driverMessage = strings.TrimSpace(driverMessage) + "\nReply MEMBER and your message to send a message to the member." notifications := make([]viewmodel.Notification, 0) notifications = append(notifications, c.getDriverNotification(lastRide, "sms", "Message to the driver", driverMessage, lastRide.Driver.PhoneNumber, twilioResponse.To)) notifications = append(notifications, c.getDriverNotification(lastRide, "sms", "Message to the driver", requestMessage, *lastRide.CreatedUser.PhoneNumber, *lastRide.User.PhoneNumber)) notifications = append(notifications, c.getDriverNotification(lastRide, "email", "Message to the driver", driverMessage, *lastRide.CreatedUser.Email, *lastRide.User.Email)) notifications, err := c.svc.Notification.SendNotifications(notifications) if err != nil { message = "There was a problem to send a message to the driver." fmt.Println("Error to send drivers notification: ", err.Error()) } message = "Your message was sent to the driver via SMS." } } else { driverMessage := strings.Replace(requestMessage, "MEMBER", "", -1) driverMessage = strings.TrimSpace(driverMessage) + "\nReply DRIVER and your message to send a message to the driver." userPhoneNumber := "" createUserPhoneNumber := "" userEmail := "" createdUserEmail := "" if lastRide.User.PhoneNumber != nil { userPhoneNumber = *lastRide.User.PhoneNumber } if lastRide.CreatedUser.PhoneNumber != nil { createUserPhoneNumber = *lastRide.CreatedUser.PhoneNumber } if lastRide.User.Email != nil { userEmail = *lastRide.User.Email } if lastRide.CreatedUser.Email != nil { createdUserEmail = *lastRide.CreatedUser.Email } notifications := make([]viewmodel.Notification, 0) notifications = append(notifications, c.getDriverNotification(lastRide, "sms", "Message to the member from the driver", driverMessage, userPhoneNumber, twilioResponse.To)) notifications = append(notifications, c.getDriverNotification(lastRide, "sms", "Message to the member from the driver", requestMessage, createUserPhoneNumber, userPhoneNumber)) notifications = append(notifications, c.getDriverNotification(lastRide, "email", "Message to the member from the driver", driverMessage, createdUserEmail, userEmail)) notifications, err := c.svc.Notification.SendNotifications(notifications) if err != nil { message = "There was a problem to send a message to the member." fmt.Println("Error to send member notification: ", err.Error()) } message = "Your message was sent to the member via SMS." } resp.Message = &Message{ To: toNumber, Body: message, } } return ctx.XML(http.StatusOK, resp) } func (c *controller) handleMessages() { for { msg := <-c.broadcast for client := range c.clients { if c.clients[client].From == msg.To { err := client.WriteJSON(msg) if err != nil { log.Printf("error: %v", err) client.Close() delete(c.clients, client) } } } } } func (c *controller) handleSocket(ctx echo.Context) error { ws, err := upgrader.Upgrade(ctx.Response(), ctx.Request(), nil) if err != nil { return err } defer ws.Close() c.clients[ws] = viewmodel.SocketMessage{Filled: false} for { var msg viewmodel.SocketMessage err := ws.ReadJSON(&msg) if err != nil { log.Printf("error: %v", err) delete(c.clients, ws) break } msg.Filled = true c.clients[ws] = msg } return nil }