feat(widget): finish chatwoot widget stub burn-down
This commit is contained in:
@@ -3,6 +3,7 @@ package widget
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
@@ -151,6 +152,44 @@ func (h *WidgetHandler) SendMessage(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, widgetMessagePayload(resp.Message, resp.ConversationID))
|
||||
}
|
||||
|
||||
func (h *WidgetHandler) UpdateMessage(c *gin.Context) {
|
||||
widgetToken := widgetTokenFromRequest(c)
|
||||
if widgetToken == "" {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "widget_token required"})
|
||||
return
|
||||
}
|
||||
messageID, err := strconv.ParseUint(c.Param("message_id"), 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid message id"})
|
||||
return
|
||||
}
|
||||
var req struct {
|
||||
Contact struct {
|
||||
Email string `json:"email"`
|
||||
Name string `json:"name"`
|
||||
} `json:"contact"`
|
||||
Message struct {
|
||||
SubmittedValues []map[string]any `json:"submitted_values"`
|
||||
} `json:"message"`
|
||||
}
|
||||
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.UpdateMessage(c.Request.Context(), service.WidgetMessageUpdate{
|
||||
MessageID: uint(messageID),
|
||||
WidgetToken: widgetToken,
|
||||
ContactEmail: req.Contact.Email,
|
||||
ContactName: req.Contact.Name,
|
||||
SubmittedValues: req.Message.SubmittedValues,
|
||||
})
|
||||
if err != nil {
|
||||
c.JSON(widgetErrorStatus(err), gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"contact": widgetContactFullPayload(contact)})
|
||||
}
|
||||
|
||||
// GetLatestMessages implements Chatwoot's GET /api/v1/widget/messages endpoint.
|
||||
func (h *WidgetHandler) GetLatestMessages(c *gin.Context) {
|
||||
widgetToken := widgetTokenFromRequest(c)
|
||||
@@ -333,10 +372,13 @@ func (h *WidgetHandler) UpdateContact(c *gin.Context) {
|
||||
}
|
||||
|
||||
var req struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
Email string `json:"email,omitempty"`
|
||||
PhoneNumber string `json:"phone_number,omitempty"`
|
||||
CustomAttributes map[string]any `json:"custom_attributes,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Email string `json:"email,omitempty"`
|
||||
PhoneNumber string `json:"phone_number,omitempty"`
|
||||
Identifier string `json:"identifier,omitempty"`
|
||||
AvatarURL string `json:"avatar_url,omitempty"`
|
||||
CustomAttributes map[string]any `json:"custom_attributes,omitempty"`
|
||||
AdditionalAttributes map[string]any `json:"additional_attributes,omitempty"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body", "details": err.Error()})
|
||||
@@ -344,10 +386,13 @@ func (h *WidgetHandler) UpdateContact(c *gin.Context) {
|
||||
}
|
||||
|
||||
contact, err := h.widgetService.UpdateContactProfile(c.Request.Context(), widgetToken, service.WidgetContactUpdate{
|
||||
Name: req.Name,
|
||||
Email: req.Email,
|
||||
PhoneNumber: req.PhoneNumber,
|
||||
CustomAttributes: req.CustomAttributes,
|
||||
Name: req.Name,
|
||||
Email: req.Email,
|
||||
PhoneNumber: req.PhoneNumber,
|
||||
Identifier: req.Identifier,
|
||||
AvatarURL: req.AvatarURL,
|
||||
CustomAttributes: req.CustomAttributes,
|
||||
AdditionalAttributes: req.AdditionalAttributes,
|
||||
})
|
||||
if err != nil {
|
||||
status := http.StatusBadRequest
|
||||
@@ -403,6 +448,53 @@ func (h *WidgetHandler) DestroyContactCustomAttributes(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, contact)
|
||||
}
|
||||
|
||||
func (h *WidgetHandler) SetUser(c *gin.Context) {
|
||||
widgetToken := widgetTokenFromRequest(c)
|
||||
if widgetToken == "" {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "widget_token required"})
|
||||
return
|
||||
}
|
||||
var req struct {
|
||||
Identifier string `json:"identifier"`
|
||||
IdentifierHash string `json:"identifier_hash"`
|
||||
Email string `json:"email"`
|
||||
Name string `json:"name"`
|
||||
AvatarURL string `json:"avatar_url"`
|
||||
PhoneNumber string `json:"phone_number"`
|
||||
CustomAttributes map[string]any `json:"custom_attributes"`
|
||||
AdditionalAttributes map[string]any `json:"additional_attributes"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body", "details": err.Error()})
|
||||
return
|
||||
}
|
||||
identifierHash := req.IdentifierHash
|
||||
if identifierHash == "" {
|
||||
identifierHash = c.Query("identifier_hash")
|
||||
}
|
||||
resp, err := h.widgetService.SetUser(c.Request.Context(), service.WidgetSetUserRequest{
|
||||
WebsiteToken: c.Query("website_token"),
|
||||
WidgetToken: widgetToken,
|
||||
Identifier: req.Identifier,
|
||||
IdentifierHash: identifierHash,
|
||||
Email: req.Email,
|
||||
Name: req.Name,
|
||||
AvatarURL: req.AvatarURL,
|
||||
PhoneNumber: req.PhoneNumber,
|
||||
CustomAttributes: req.CustomAttributes,
|
||||
AdditionalAttributes: req.AdditionalAttributes,
|
||||
})
|
||||
if err != nil {
|
||||
c.JSON(widgetErrorStatus(err), gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
payload := widgetContactPayload(resp.Contact)
|
||||
if resp.WidgetAuthToken != "" {
|
||||
payload["widget_auth_token"] = resp.WidgetAuthToken
|
||||
}
|
||||
c.JSON(http.StatusOK, payload)
|
||||
}
|
||||
|
||||
// ToggleTyping signals that the contact is typing or stopped typing.
|
||||
// POST /widget/conversations/:id/toggle_typing
|
||||
// Reference: Chatwoot widget SDK — typing indicator
|
||||
@@ -606,6 +698,43 @@ func (h *WidgetHandler) RemoveLabel(c *gin.Context) {
|
||||
c.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func (h *WidgetHandler) SendTranscript(c *gin.Context) {
|
||||
widgetToken := widgetTokenFromRequest(c)
|
||||
if widgetToken == "" {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "widget_token required"})
|
||||
return
|
||||
}
|
||||
if err := h.widgetService.SendTranscript(c.Request.Context(), widgetToken); err != nil {
|
||||
status := widgetErrorStatus(err)
|
||||
if strings.Contains(err.Error(), "conversation not found") {
|
||||
status = http.StatusTooManyRequests
|
||||
}
|
||||
c.JSON(status, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.Status(http.StatusOK)
|
||||
}
|
||||
|
||||
func (h *WidgetHandler) AddDyteParticipantToMeeting(c *gin.Context) {
|
||||
var req struct {
|
||||
MessageID uint `json:"message_id"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body", "details": err.Error()})
|
||||
return
|
||||
}
|
||||
resp, err := h.widgetService.AddDyteParticipant(c.Request.Context(), c.Query("website_token"), req.MessageID)
|
||||
if err != nil {
|
||||
status := http.StatusUnprocessableEntity
|
||||
if strings.Contains(err.Error(), "website_token") {
|
||||
status = http.StatusBadRequest
|
||||
}
|
||||
c.JSON(status, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, resp)
|
||||
}
|
||||
|
||||
// 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,
|
||||
@@ -720,3 +849,27 @@ func widgetContactPayload(contact *model.Contact) gin.H {
|
||||
"has_phone_number": contact.PhoneNumber != "",
|
||||
}
|
||||
}
|
||||
|
||||
func widgetContactFullPayload(contact *model.Contact) gin.H {
|
||||
return gin.H{
|
||||
"id": contact.ID,
|
||||
"name": contact.Name,
|
||||
"email": contact.Email,
|
||||
"phone_number": contact.PhoneNumber,
|
||||
"avatar_url": contact.AvatarURL,
|
||||
"identifier": contact.Identifier,
|
||||
"custom_attributes": contact.CustomAttributes,
|
||||
"additional_attributes": contact.AdditionalAttributes,
|
||||
}
|
||||
}
|
||||
|
||||
func widgetErrorStatus(err error) int {
|
||||
msg := err.Error()
|
||||
if strings.Contains(msg, "invalid widget_token") || strings.Contains(msg, "HMAC failed") {
|
||||
return http.StatusUnauthorized
|
||||
}
|
||||
if strings.Contains(msg, "not found") {
|
||||
return http.StatusNotFound
|
||||
}
|
||||
return http.StatusBadRequest
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user