feat(widget): implement chatwoot widget core handlers
This commit is contained in:
@@ -32,10 +32,13 @@ func NewHandler(widgetService *service.WidgetService) *WidgetHandler {
|
||||
// contact attributes are optional (anonymous visitor if not provided).
|
||||
func (h *WidgetHandler) Init(c *gin.Context) {
|
||||
var req service.WidgetInitRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
if err := c.ShouldBindJSON(&req); err != nil && c.Request.ContentLength != 0 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body", "details": err.Error()})
|
||||
return
|
||||
}
|
||||
if req.WebsiteToken == "" {
|
||||
req.WebsiteToken = c.Query("website_token")
|
||||
}
|
||||
|
||||
// HMAC verification: if the client provides an identifier + identifier_hash,
|
||||
// verify the hash against the inbox's hmac_token. This mirrors Chatwoot's
|
||||
@@ -72,21 +75,60 @@ func (h *WidgetHandler) Init(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, resp)
|
||||
}
|
||||
|
||||
// Config implements Chatwoot's POST /api/v1/widget/config endpoint.
|
||||
func (h *WidgetHandler) Config(c *gin.Context) {
|
||||
var req service.WidgetInitRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil && c.Request.ContentLength != 0 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body", "details": err.Error()})
|
||||
return
|
||||
}
|
||||
if req.WebsiteToken == "" {
|
||||
req.WebsiteToken = c.Query("website_token")
|
||||
}
|
||||
|
||||
resp, err := h.widgetService.Init(c.Request.Context(), req)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
contact := gin.H{
|
||||
"id": resp.ContactID,
|
||||
"pubsub_token": resp.WidgetToken,
|
||||
}
|
||||
if resp.Contact != nil {
|
||||
contact["email"] = resp.Contact.Email
|
||||
contact["identifier"] = resp.Contact.Identifier
|
||||
contact["name"] = resp.Contact.Name
|
||||
contact["phone_number"] = resp.Contact.PhoneNumber
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"website_channel_config": gin.H{
|
||||
"auth_token": resp.WidgetToken,
|
||||
"website_token": resp.WidgetConfig.WebsiteToken,
|
||||
"widget_color": resp.WidgetConfig.WidgetColor,
|
||||
"welcome_title": resp.WidgetConfig.WelcomeTitle,
|
||||
"welcome_tagline": resp.WidgetConfig.WelcomeTagline,
|
||||
"website_name": resp.InboxName,
|
||||
},
|
||||
"contact": contact,
|
||||
"global_config": gin.H{},
|
||||
})
|
||||
}
|
||||
|
||||
// SendMessage sends a message from the widget contact to the conversation.
|
||||
// POST /widget/messages
|
||||
// Reference: Chatwoot widget SDK — send message endpoint
|
||||
func (h *WidgetHandler) SendMessage(c *gin.Context) {
|
||||
widgetToken := c.GetHeader("X-Widget-Token")
|
||||
if widgetToken == "" {
|
||||
widgetToken = c.Query("widget_token")
|
||||
}
|
||||
widgetToken := widgetTokenFromRequest(c)
|
||||
if widgetToken == "" {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "widget_token required"})
|
||||
return
|
||||
}
|
||||
|
||||
var req service.WidgetSendMessageRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
req, err := bindWidgetSendMessageRequest(c)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body", "details": err.Error()})
|
||||
return
|
||||
}
|
||||
@@ -102,17 +144,53 @@ func (h *WidgetHandler) SendMessage(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, resp)
|
||||
if c.FullPath() == "/widget/messages" {
|
||||
c.JSON(http.StatusOK, resp)
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, widgetMessagePayload(resp.Message, resp.ConversationID))
|
||||
}
|
||||
|
||||
// GetLatestMessages implements Chatwoot's GET /api/v1/widget/messages endpoint.
|
||||
func (h *WidgetHandler) GetLatestMessages(c *gin.Context) {
|
||||
widgetToken := widgetTokenFromRequest(c)
|
||||
if widgetToken == "" {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "widget_token required"})
|
||||
return
|
||||
}
|
||||
|
||||
offset, _ := strconv.Atoi(c.DefaultQuery("offset", "0"))
|
||||
limit, _ := strconv.Atoi(c.DefaultQuery("limit", "25"))
|
||||
if limit <= 0 || limit > 100 {
|
||||
limit = 25
|
||||
}
|
||||
|
||||
messages, total, conversation, err := h.widgetService.GetLatestConversationMessages(c.Request.Context(), widgetToken, offset, limit)
|
||||
if err != nil {
|
||||
status := http.StatusBadRequest
|
||||
if err.Error() == "invalid widget_token" {
|
||||
status = http.StatusUnauthorized
|
||||
}
|
||||
c.JSON(status, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
payload := make([]gin.H, 0, len(messages))
|
||||
for _, msg := range messages {
|
||||
payload = append(payload, widgetMessagePayload(msg, msg.ConversationID))
|
||||
}
|
||||
meta := gin.H{"total": total, "offset": offset, "limit": limit}
|
||||
if conversation != nil && conversation.ContactLastSeenAt != nil {
|
||||
meta["contact_last_seen_at"] = *conversation.ContactLastSeenAt
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"payload": payload, "meta": meta})
|
||||
}
|
||||
|
||||
// GetConversations returns the conversation list for a widget contact.
|
||||
// GET /widget/conversations
|
||||
// Reference: Chatwoot widget SDK — fetch conversation list
|
||||
func (h *WidgetHandler) GetConversations(c *gin.Context) {
|
||||
widgetToken := c.GetHeader("X-Widget-Token")
|
||||
if widgetToken == "" {
|
||||
widgetToken = c.Query("widget_token")
|
||||
}
|
||||
widgetToken := widgetTokenFromRequest(c)
|
||||
if widgetToken == "" {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "widget_token required"})
|
||||
return
|
||||
@@ -128,17 +206,60 @@ func (h *WidgetHandler) GetConversations(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
if c.FullPath() == "/api/v1/widget/conversations" {
|
||||
if len(conversations) == 0 {
|
||||
c.JSON(http.StatusOK, gin.H{})
|
||||
return
|
||||
}
|
||||
conversation := conversations[0]
|
||||
c.JSON(http.StatusOK, widgetConversationPayload(conversation))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"conversations": conversations})
|
||||
}
|
||||
|
||||
// CreateConversation implements Chatwoot's POST /api/v1/widget/conversations endpoint.
|
||||
func (h *WidgetHandler) CreateConversation(c *gin.Context) {
|
||||
widgetToken := widgetTokenFromRequest(c)
|
||||
if widgetToken == "" {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "widget_token required"})
|
||||
return
|
||||
}
|
||||
|
||||
req, err := bindWidgetSendMessageRequest(c)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body", "details": err.Error()})
|
||||
return
|
||||
}
|
||||
req.WidgetToken = widgetToken
|
||||
req.ConversationID = nil
|
||||
|
||||
resp, err := h.widgetService.SendMessage(c.Request.Context(), req)
|
||||
if err != nil {
|
||||
status := http.StatusBadRequest
|
||||
if err.Error() == "invalid widget_token" || err.Error() == "widget_token required" {
|
||||
status = http.StatusUnauthorized
|
||||
}
|
||||
c.JSON(status, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
conversation, err := h.widgetService.GetConversation(c.Request.Context(), widgetToken, resp.ConversationID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"id": resp.ConversationID, "messages": []gin.H{widgetMessagePayload(resp.Message, resp.ConversationID)}})
|
||||
return
|
||||
}
|
||||
payload := widgetConversationPayload(*conversation)
|
||||
payload["messages"] = []gin.H{widgetMessagePayload(resp.Message, resp.ConversationID)}
|
||||
c.JSON(http.StatusOK, payload)
|
||||
}
|
||||
|
||||
// GetMessages retrieves messages for a specific conversation.
|
||||
// GET /widget/conversations/:id/messages
|
||||
// Reference: Chatwoot widget SDK — fetch messages for a conversation
|
||||
func (h *WidgetHandler) GetMessages(c *gin.Context) {
|
||||
widgetToken := c.GetHeader("X-Widget-Token")
|
||||
if widgetToken == "" {
|
||||
widgetToken = c.Query("widget_token")
|
||||
}
|
||||
widgetToken := widgetTokenFromRequest(c)
|
||||
if widgetToken == "" {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "widget_token required"})
|
||||
return
|
||||
@@ -182,10 +303,7 @@ func (h *WidgetHandler) GetMessages(c *gin.Context) {
|
||||
// GET /widget/cable_token
|
||||
// Reference: Chatwoot widget SDK — fetch token for ActionCable subscription
|
||||
func (h *WidgetHandler) GetCableToken(c *gin.Context) {
|
||||
widgetToken := c.GetHeader("X-Widget-Token")
|
||||
if widgetToken == "" {
|
||||
widgetToken = c.Query("widget_token")
|
||||
}
|
||||
widgetToken := widgetTokenFromRequest(c)
|
||||
if widgetToken == "" {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "widget_token required"})
|
||||
return
|
||||
@@ -208,25 +326,29 @@ func (h *WidgetHandler) GetCableToken(c *gin.Context) {
|
||||
// PATCH /widget/contact
|
||||
// Reference: Chatwoot widget SDK — update contact name/email
|
||||
func (h *WidgetHandler) UpdateContact(c *gin.Context) {
|
||||
widgetToken := c.GetHeader("X-Widget-Token")
|
||||
if widgetToken == "" {
|
||||
widgetToken = c.Query("widget_token")
|
||||
}
|
||||
widgetToken := widgetTokenFromRequest(c)
|
||||
if widgetToken == "" {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "widget_token required"})
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
Email string `json:"email,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Email string `json:"email,omitempty"`
|
||||
PhoneNumber string `json:"phone_number,omitempty"`
|
||||
CustomAttributes map[string]any `json:"custom_attributes,omitempty"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body", "details": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
contact, err := h.widgetService.UpdateContact(c.Request.Context(), widgetToken, req.Name, req.Email)
|
||||
contact, err := h.widgetService.UpdateContactProfile(c.Request.Context(), widgetToken, service.WidgetContactUpdate{
|
||||
Name: req.Name,
|
||||
Email: req.Email,
|
||||
PhoneNumber: req.PhoneNumber,
|
||||
CustomAttributes: req.CustomAttributes,
|
||||
})
|
||||
if err != nil {
|
||||
status := http.StatusBadRequest
|
||||
if err.Error() == "invalid widget_token" {
|
||||
@@ -236,6 +358,48 @@ func (h *WidgetHandler) UpdateContact(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, widgetContactPayload(contact))
|
||||
}
|
||||
|
||||
// GetContact implements Chatwoot's GET /api/v1/widget/contact endpoint.
|
||||
func (h *WidgetHandler) GetContact(c *gin.Context) {
|
||||
widgetToken := widgetTokenFromRequest(c)
|
||||
if widgetToken == "" {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "widget_token required"})
|
||||
return
|
||||
}
|
||||
|
||||
contact, err := h.widgetService.GetContact(c.Request.Context(), widgetToken)
|
||||
if err != nil {
|
||||
status := http.StatusBadRequest
|
||||
if err.Error() == "invalid widget_token" {
|
||||
status = http.StatusUnauthorized
|
||||
}
|
||||
c.JSON(status, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, widgetContactPayload(contact))
|
||||
}
|
||||
|
||||
func (h *WidgetHandler) DestroyContactCustomAttributes(c *gin.Context) {
|
||||
widgetToken := widgetTokenFromRequest(c)
|
||||
if widgetToken == "" {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "widget_token required"})
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
CustomAttributes []string `json:"custom_attributes"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body", "details": err.Error()})
|
||||
return
|
||||
}
|
||||
contact, err := h.widgetService.DeleteContactCustomAttributes(c.Request.Context(), widgetToken, req.CustomAttributes)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, contact)
|
||||
}
|
||||
|
||||
@@ -243,29 +407,43 @@ func (h *WidgetHandler) UpdateContact(c *gin.Context) {
|
||||
// POST /widget/conversations/:id/toggle_typing
|
||||
// Reference: Chatwoot widget SDK — typing indicator
|
||||
func (h *WidgetHandler) ToggleTyping(c *gin.Context) {
|
||||
widgetToken := c.GetHeader("X-Widget-Token")
|
||||
if widgetToken == "" {
|
||||
widgetToken = c.Query("widget_token")
|
||||
}
|
||||
widgetToken := widgetTokenFromRequest(c)
|
||||
if widgetToken == "" {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "widget_token required"})
|
||||
return
|
||||
}
|
||||
|
||||
var conversationID uint64
|
||||
var err error
|
||||
conversationIDStr := c.Param("id")
|
||||
conversationID, err := strconv.ParseUint(conversationIDStr, 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid conversation id"})
|
||||
return
|
||||
if conversationIDStr != "" {
|
||||
conversationID, err = strconv.ParseUint(conversationIDStr, 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid conversation id"})
|
||||
return
|
||||
}
|
||||
} else {
|
||||
conversation, convErr := h.widgetService.GetLatestConversation(c.Request.Context(), widgetToken)
|
||||
if convErr != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": convErr.Error()})
|
||||
return
|
||||
}
|
||||
conversationID = uint64(conversation.ID)
|
||||
}
|
||||
|
||||
var req struct {
|
||||
Typing bool `json:"typing"`
|
||||
Typing bool `json:"typing"`
|
||||
TypingStatus string `json:"typing_status"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
// Default to typing=true if no body provided
|
||||
req.Typing = true
|
||||
}
|
||||
if req.TypingStatus == "on" {
|
||||
req.Typing = true
|
||||
} else if req.TypingStatus == "off" {
|
||||
req.Typing = false
|
||||
}
|
||||
|
||||
err = h.widgetService.ToggleTyping(c.Request.Context(), widgetToken, uint(conversationID), req.Typing)
|
||||
if err != nil {
|
||||
@@ -280,6 +458,76 @@ func (h *WidgetHandler) ToggleTyping(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"status": "ok"})
|
||||
}
|
||||
|
||||
func (h *WidgetHandler) UpdateLastSeen(c *gin.Context) {
|
||||
widgetToken := widgetTokenFromRequest(c)
|
||||
if widgetToken == "" {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "widget_token required"})
|
||||
return
|
||||
}
|
||||
conversation, err := h.widgetService.UpdateLastSeen(c.Request.Context(), widgetToken)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"contact_last_seen_at": conversation.ContactLastSeenAt})
|
||||
}
|
||||
|
||||
func (h *WidgetHandler) ToggleStatus(c *gin.Context) {
|
||||
widgetToken := widgetTokenFromRequest(c)
|
||||
if widgetToken == "" {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "widget_token required"})
|
||||
return
|
||||
}
|
||||
conversation, err := h.widgetService.ResolveLatestConversation(c.Request.Context(), widgetToken)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, widgetConversationPayload(*conversation))
|
||||
}
|
||||
|
||||
func (h *WidgetHandler) SetConversationCustomAttributes(c *gin.Context) {
|
||||
widgetToken := widgetTokenFromRequest(c)
|
||||
if widgetToken == "" {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "widget_token required"})
|
||||
return
|
||||
}
|
||||
var req struct {
|
||||
CustomAttributes map[string]any `json:"custom_attributes"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body", "details": err.Error()})
|
||||
return
|
||||
}
|
||||
conversation, err := h.widgetService.SetLatestConversationCustomAttributes(c.Request.Context(), widgetToken, req.CustomAttributes)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, conversation)
|
||||
}
|
||||
|
||||
func (h *WidgetHandler) DestroyConversationCustomAttributes(c *gin.Context) {
|
||||
widgetToken := widgetTokenFromRequest(c)
|
||||
if widgetToken == "" {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "widget_token required"})
|
||||
return
|
||||
}
|
||||
var req struct {
|
||||
CustomAttribute []string `json:"custom_attribute"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body", "details": err.Error()})
|
||||
return
|
||||
}
|
||||
conversation, err := h.widgetService.DeleteLatestConversationCustomAttributes(c.Request.Context(), widgetToken, req.CustomAttribute)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, conversation)
|
||||
}
|
||||
|
||||
// SubmitOfflineMessage handles a visitor submitting a message when agents are offline.
|
||||
// POST /widget/offline_message
|
||||
// Reference: Chatwoot widget SDK — when business_hours are disabled and no agents online,
|
||||
@@ -324,4 +572,73 @@ func (h *WidgetHandler) SubmitOfflineMessage(c *gin.Context) {
|
||||
"offline_message": msg,
|
||||
"message": "Your message has been recorded. An agent will respond when available.",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func widgetTokenFromRequest(c *gin.Context) string {
|
||||
if token := c.GetHeader("X-Widget-Token"); token != "" {
|
||||
return token
|
||||
}
|
||||
if token := c.GetHeader("X-Auth-Token"); token != "" {
|
||||
return token
|
||||
}
|
||||
return c.Query("widget_token")
|
||||
}
|
||||
|
||||
func bindWidgetSendMessageRequest(c *gin.Context) (service.WidgetSendMessageRequest, error) {
|
||||
var body struct {
|
||||
Content string `json:"content"`
|
||||
ContentType string `json:"content_type"`
|
||||
ConversationID *uint `json:"conversation_id"`
|
||||
Message struct {
|
||||
Content string `json:"content"`
|
||||
} `json:"message"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&body); err != nil {
|
||||
return service.WidgetSendMessageRequest{}, err
|
||||
}
|
||||
content := body.Content
|
||||
if content == "" {
|
||||
content = body.Message.Content
|
||||
}
|
||||
return service.WidgetSendMessageRequest{
|
||||
Content: content,
|
||||
ContentType: body.ContentType,
|
||||
ConversationID: body.ConversationID,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func widgetMessagePayload(message model.Message, conversationID uint) gin.H {
|
||||
return gin.H{
|
||||
"id": message.ID,
|
||||
"content": message.Content,
|
||||
"inbox_id": message.InboxID,
|
||||
"conversation_id": conversationID,
|
||||
"message_type": message.MessageType,
|
||||
"content_type": message.ContentType,
|
||||
"content_attributes": message.ContentAttributes,
|
||||
"created_at": message.CreatedAt.Unix(),
|
||||
"private": message.Private,
|
||||
"source_id": message.SourceID,
|
||||
}
|
||||
}
|
||||
|
||||
func widgetConversationPayload(conversation model.Conversation) gin.H {
|
||||
return gin.H{
|
||||
"id": conversation.ID,
|
||||
"uuid": conversation.UUID,
|
||||
"inbox_id": conversation.InboxID,
|
||||
"contact_last_seen_at": conversation.ContactLastSeenAt,
|
||||
"status": conversation.Status,
|
||||
"custom_attributes": conversation.CustomAttributes,
|
||||
}
|
||||
}
|
||||
|
||||
func widgetContactPayload(contact *model.Contact) gin.H {
|
||||
return gin.H{
|
||||
"id": contact.ID,
|
||||
"identifier": contact.Identifier,
|
||||
"has_email": contact.Email != "",
|
||||
"has_name": contact.Name != "",
|
||||
"has_phone_number": contact.PhoneNumber != "",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,6 +94,15 @@ func setupWidgetHandlerTest(t *testing.T) (*gorm.DB, *gin.Engine, *WidgetHandler
|
||||
widgetGroup.POST("/offline_message", handler.SubmitOfflineMessage)
|
||||
}
|
||||
|
||||
chatwootWidgetGroup := router.Group("/api/v1/widget")
|
||||
{
|
||||
chatwootWidgetGroup.POST("/config", handler.Config)
|
||||
chatwootWidgetGroup.GET("/messages", handler.GetLatestMessages)
|
||||
chatwootWidgetGroup.POST("/messages", handler.SendMessage)
|
||||
chatwootWidgetGroup.POST("/conversations", handler.CreateConversation)
|
||||
chatwootWidgetGroup.GET("/contact", handler.GetContact)
|
||||
}
|
||||
|
||||
return db, router, handler
|
||||
}
|
||||
|
||||
@@ -194,6 +203,81 @@ func TestWidgetHandler_Init_InvalidWebsiteToken(t *testing.T) {
|
||||
assert.Equal(t, http.StatusBadRequest, w.Code)
|
||||
}
|
||||
|
||||
func TestWidgetHandler_ChatwootConfig_Success(t *testing.T) {
|
||||
db, router, _ := setupWidgetHandlerTest(t)
|
||||
_, _ = seedWidgetHandlerData(t, db)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("POST", "/api/v1/widget/config?website_token=handler_ws_token_123", nil)
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
|
||||
var resp map[string]interface{}
|
||||
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp))
|
||||
|
||||
config := resp["website_channel_config"].(map[string]interface{})
|
||||
assert.NotEmpty(t, config["auth_token"])
|
||||
assert.Equal(t, "handler_ws_token_123", config["website_token"])
|
||||
assert.Equal(t, "Handler Widget Inbox", config["website_name"])
|
||||
|
||||
contact := resp["contact"].(map[string]interface{})
|
||||
assert.NotEmpty(t, contact["id"])
|
||||
assert.NotEmpty(t, contact["pubsub_token"])
|
||||
}
|
||||
|
||||
func TestWidgetHandler_ChatwootMessages_AuthTokenAndNestedPayload(t *testing.T) {
|
||||
db, router, _ := setupWidgetHandlerTest(t)
|
||||
_, _ = seedWidgetHandlerData(t, db)
|
||||
|
||||
wConfig := httptest.NewRecorder()
|
||||
reqConfig, _ := http.NewRequest("POST", "/api/v1/widget/config?website_token=handler_ws_token_123", nil)
|
||||
router.ServeHTTP(wConfig, reqConfig)
|
||||
require.Equal(t, http.StatusOK, wConfig.Code)
|
||||
|
||||
var configResp map[string]interface{}
|
||||
require.NoError(t, json.Unmarshal(wConfig.Body.Bytes(), &configResp))
|
||||
contact := configResp["contact"].(map[string]interface{})
|
||||
authToken := contact["pubsub_token"].(string)
|
||||
|
||||
body := map[string]interface{}{
|
||||
"message": map[string]interface{}{"content": "Hello from Chatwoot widget"},
|
||||
}
|
||||
bodyJSON, _ := json.Marshal(body)
|
||||
|
||||
wMessage := httptest.NewRecorder()
|
||||
reqMessage, _ := http.NewRequest("POST", "/api/v1/widget/messages", bytes.NewReader(bodyJSON))
|
||||
reqMessage.Header.Set("Content-Type", "application/json")
|
||||
reqMessage.Header.Set("X-Auth-Token", authToken)
|
||||
router.ServeHTTP(wMessage, reqMessage)
|
||||
require.Equal(t, http.StatusOK, wMessage.Code)
|
||||
|
||||
var messageResp map[string]interface{}
|
||||
require.NoError(t, json.Unmarshal(wMessage.Body.Bytes(), &messageResp))
|
||||
assert.Nil(t, messageResp["message"])
|
||||
assert.Equal(t, "Hello from Chatwoot widget", messageResp["content"])
|
||||
assert.NotEmpty(t, messageResp["conversation_id"])
|
||||
|
||||
wIndex := httptest.NewRecorder()
|
||||
reqIndex, _ := http.NewRequest("GET", "/api/v1/widget/messages", nil)
|
||||
reqIndex.Header.Set("X-Auth-Token", authToken)
|
||||
router.ServeHTTP(wIndex, reqIndex)
|
||||
require.Equal(t, http.StatusOK, wIndex.Code)
|
||||
|
||||
var indexResp map[string]interface{}
|
||||
require.NoError(t, json.Unmarshal(wIndex.Body.Bytes(), &indexResp))
|
||||
payload := indexResp["payload"].([]interface{})
|
||||
require.Len(t, payload, 1)
|
||||
firstMessage := payload[0].(map[string]interface{})
|
||||
assert.Equal(t, "Hello from Chatwoot widget", firstMessage["content"])
|
||||
|
||||
wContact := httptest.NewRecorder()
|
||||
reqContact, _ := http.NewRequest("GET", "/api/v1/widget/contact", nil)
|
||||
reqContact.Header.Set("X-Auth-Token", authToken)
|
||||
router.ServeHTTP(wContact, reqContact)
|
||||
require.Equal(t, http.StatusOK, wContact.Code)
|
||||
}
|
||||
|
||||
// ========== SendMessage Handler Tests ==========
|
||||
|
||||
func TestWidgetHandler_SendMessage_Success(t *testing.T) {
|
||||
@@ -679,4 +763,4 @@ func TestWidgetHandler_Init_HMACInvalid(t *testing.T) {
|
||||
var resp map[string]interface{}
|
||||
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp))
|
||||
assert.NotEmpty(t, resp["widget_token"])
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user