diff --git a/main.go b/main.go index 65f0be3..ecb9c55 100644 --- a/main.go +++ b/main.go @@ -1,7 +1,9 @@ package main import ( + "net/http" "os" + "time" "github.com/gin-gonic/gin" "github.com/mkideal/cli" @@ -22,6 +24,8 @@ func main() { gin.SetMode(gin.DebugMode) } + http.DefaultClient.Timeout = time.Second * 30 + return NewServer().Run(argv.Address) })) } diff --git a/models.go b/models.go index d128894..301deae 100644 --- a/models.go +++ b/models.go @@ -110,6 +110,104 @@ type Message struct { Interactive *MessageInteractive `json:"interactive,omitempty"` } +type InboundMessage struct { + Message + Button *InboundMessageButton `json:"button,omitempty"` + Context *InboundMessageContext `json:"context,omitempty"` + From string `json:"from,omitempty"` + ID string `json:"id,omitempty"` + Identity *InboundMessageIdentity `json:"identity,omitempty"` + Timestamp string `json:"timestamp,omitempty"` + System *MessageSystem `json:"system,omitempty"` + Voice *MessageMedia `json:"voice,omitempty"` + Referral *Referral `json:"referral,omitempty"` + Errors []InboundError `json:"errors,omitempty"` +} + +type InboundWebhook struct { + Contacts []InboundContact `json:"contacts,omitempty"` + Messages []InboundMessage `json:"messages,omitempty"` + Statuses []InboundStatus `json:"statuses,omitempty"` + Errors []InboundError `json:"errors,omitempty"` +} + +type InboundContact struct { + Profile *Profile `json:"profile,omitempty"` + WaID string `json:"wa_id,omitempty"` +} + +type InboundMessageButton struct { + Payload string `json:"payload,omitempty"` + Text string `json:"text,omitempty"` +} + +type InboundMessageIdentity struct { + Acknowledged string `json:"acknowledged,omitempty"` + CreatedTimestamp string `json:"created_timestamp,omitempty"` + Hash string `json:"hash,omitempty"` +} + +type InboundMessageContext struct { + From string `json:"from,omitempty"` + ID string `json:"id,omitempty"` + GroupID string `json:"group_id,omitempty"` + Mentions []string `json:"mentions,omitempty"` + Forwarded bool `json:"forwarded,omitempty"` + FrequentlyForwarded bool `json:"frequently_forwarded,omitempty"` +} + +type InboundStatus struct { + Conversation *InboundStatusConversation `json:"conversation,omitempty"` + ID string `json:"id,omitempty"` + Pricing *InboundStatusPricing `json:"pricing,omitempty"` + RecipientID string `json:"recipient_id,omitempty"` + Status string `json:"status,omitempty"` + Timestamp json.Number `json:"timestamp,omitempty"` + Type string `json:"type,omitempty"` +} + +type InboundStatusPricing struct { + Billable bool `json:"billable,omitempty"` + PricingModel string `json:"pricing_model,omitempty"` +} + +type InboundStatusConversation struct { + ID string `json:"id,omitempty"` + Origin InboundStatusConversationOrigin `json:"origin,omitempty"` + ExpirationTimestamp json.Number `json:"expiration_timestamp,omitempty"` +} + +type InboundStatusConversationOrigin struct { + Type string `json:"type,omitempty"` +} + +type InboundError struct { + Code int `json:"code,omitempty"` + Details string `json:"details,omitempty"` + Title string `json:"title,omitempty"` +} + +type Referral struct { + Headline string `json:"headline,omitempty"` + Body string `json:"body,omitempty"` + SourceType string `json:"source_type,omitempty"` + SourceID string `json:"source_id,omitempty"` + SourceURL string `json:"source_url,omitempty"` + Image *MessageMedia `json:"image,omitempty"` + Video *MessageMedia `json:"video,omitempty"` +} + +type MessageSystem struct { + Body string `json:"body,omitempty"` + NewWaID string `json:"new_wa_id,omitempty"` + Type string `json:"type,omitempty"` + Identity string `json:"identity,omitempty"` +} + +type Profile struct { + Name string `json:"name,omitempty"` +} + type MessagesResponse struct { BaseResponse Messages []IDModel `json:"messages,omitempty"` diff --git a/server.go b/server.go index 5a65495..c6862a1 100644 --- a/server.go +++ b/server.go @@ -3,6 +3,7 @@ package main import ( "net/http" "regexp" + "time" "github.com/gin-gonic/gin" "github.com/go-playground/validator/v10" @@ -15,17 +16,31 @@ var ( ) type Mock struct { - ContactsSuccess bool `json:"contacts_success"` - MessagesSuccess bool `json:"messages_success"` + ContactsSuccess bool `json:"contacts_success"` + MessagesSuccess bool `json:"messages_success"` + MessagesStatus string `json:"messages_success_status" validate:"oneof=sent read failed"` + Webhook string `json:"webhook" validate:"url,startswith=http"` + WebhookHeaders map[string]string `json:"webhook_headers"` } type Server struct { - g *gin.Engine - mock Mock + g *gin.Engine + shooter *Shooter + mock Mock } func NewServer() (s *Server) { - s = &Server{g: gin.New()} + s = &Server{ + g: gin.New(), + mock: Mock{ + ContactsSuccess: true, + MessagesSuccess: true, + MessagesStatus: "sent", + Webhook: "", + WebhookHeaders: map[string]string{}, + }, + } + s.updateShooter() s.g.GET("/mock", s.mockData) s.g.POST("/mock", s.updateMockData) api := s.g.Group("/v1") @@ -40,6 +55,15 @@ func (s *Server) Run(addr ...string) error { return s.g.Run(addr...) } +func (s *Server) updateShooter() { + if s.shooter == nil { + s.shooter = NewShooter(s.mock.Webhook, s.mock.WebhookHeaders) + return + } + s.shooter.Webhook = s.mock.Webhook + s.shooter.Headers = s.mock.WebhookHeaders +} + func (s *Server) baseResponseOk() BaseResponse { return BaseResponse{ Meta: &Metadata{ @@ -68,11 +92,34 @@ func (s *Server) mockData(c *gin.Context) { func (s *Server) updateMockData(c *gin.Context) { var mock Mock - if err := s.bindRequest(c, &mock); err != nil { + if err := c.ShouldBindJSON(&mock); err != nil { c.AbortWithStatus(http.StatusBadRequest) return } - s.mock = mock + + current := s.mock + current.ContactsSuccess = mock.ContactsSuccess + current.MessagesSuccess = mock.MessagesSuccess + + if mock.MessagesStatus != "" { + current.MessagesStatus = mock.MessagesStatus + } + + if mock.Webhook != "" { + current.Webhook = mock.Webhook + } + + if mock.WebhookHeaders != nil { + current.WebhookHeaders = mock.WebhookHeaders + } + + if err := validate.Struct(current); err != nil { + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + s.mock = current + s.updateShooter() c.JSON(http.StatusOK, s.mock) } @@ -106,12 +153,47 @@ func (s *Server) messagesHandler(c *gin.Context) { return } + messageID := RandomString(27) + text := "" + if req.Text != nil { + text = req.Text.Body + } + log.Printf("Received new message: %#v\n", req) + if s.mock.Webhook != "" { + defer func(msgID, text string, to string) { + go func(msgID, text string, to string) { + time.Sleep(time.Millisecond * 500) + + code, err := s.shooter.SendStatus(InboundStatus{ + Type: "message", + ID: messageID, + RecipientID: to, + Status: s.mock.MessagesStatus, + }) + if err != nil { + log.Printf("error: %s\n", err) + return + } + log.Printf("status webhook code: %d\n", code) + + if text == "reply" { + code, err := s.shooter.SendText("Replying to the message", to) + if err != nil { + log.Printf("error: %s\n", err) + return + } + log.Printf("reply webhook code: %d\n", code) + } + }(msgID, text, req.To) + }(messageID, text, req.To) + } + c.JSON(http.StatusOK, MessagesResponse{ BaseResponse: s.baseResponseOk(), Messages: []IDModel{{ - ID: RandomString(27), + ID: messageID, }}, }) } diff --git a/shooter.go b/shooter.go new file mode 100644 index 0000000..d701fe5 --- /dev/null +++ b/shooter.go @@ -0,0 +1,84 @@ +package main + +import ( + "bytes" + "encoding/json" + "net/http" +) + +type Shooter struct { + Webhook string + Headers map[string]string +} + +func NewShooter(webhook string, headers map[string]string) *Shooter { + return &Shooter{ + Webhook: webhook, + Headers: headers, + } +} + +func (s *Shooter) makeRequest(v interface{}) (*http.Request, error) { + wh, err := json.Marshal(v) + if err != nil { + return nil, err + } + + req, err := http.NewRequest(http.MethodPost, s.Webhook, bytes.NewReader(wh)) + if err != nil { + return nil, err + } + + for h, v := range s.Headers { + req.Header.Set(h, v) + } + + return req, nil +} + +func (s *Shooter) SendStatus(status InboundStatus) (int, error) { + req, err := s.makeRequest(InboundWebhook{ + Statuses: []InboundStatus{status}, + }) + if err != nil { + return 0, err + } + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return 0, err + } + + return resp.StatusCode, nil +} + +func (s *Shooter) SendText(text, from string) (int, error) { + req, err := s.makeRequest(InboundWebhook{ + Contacts: []InboundContact{{ + Profile: &Profile{ + Name: from, + }, + WaID: from, + }}, + Messages: []InboundMessage{{ + Message: Message{ + Type: "text", + Text: &MessageText{ + Body: text, + }, + }, + From: from, + ID: RandomString(27), + }}, + }) + if err != nil { + return 0, err + } + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return 0, err + } + + return resp.StatusCode, nil +}