more configurability

This commit is contained in:
Pavel 2022-04-21 16:54:08 +03:00
parent 6825c32bcf
commit b5bd0480b1
4 changed files with 276 additions and 8 deletions

View File

@ -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)
}))
}

View File

@ -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"`

View File

@ -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,
}},
})
}

84
shooter.go Normal file
View File

@ -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
}