Align GoChat with Chatwoot frontend contracts

This commit is contained in:
2026-06-13 22:13:32 +08:00
parent 71abf58636
commit d884fdda0a
162 changed files with 9825 additions and 528 deletions
+101 -10
View File
@@ -11,14 +11,20 @@ import (
"github.com/gochat/gochat/internal/model"
"github.com/gochat/gochat/internal/service"
wspkg "github.com/gochat/gochat/internal/ws"
)
type widgetEventPublisher interface {
PublishWidgetEvent(accountID uint, pubsubToken string, eventType string, payload interface{})
}
// WidgetHandler handles the public-facing widget API endpoints.
// These endpoints are accessed by the embedded JS widget on customer websites
// and do not require agent JWT authentication — they use a widget_token instead.
// Reference: Chatwoot app/controllers/api/v1/widget_messages_controller.rb
type WidgetHandler struct {
widgetService *service.WidgetService
widgetService *service.WidgetService
eventPublisher widgetEventPublisher
}
// NewHandler creates a new WidgetHandler with the WidgetService dependency.
@@ -28,6 +34,11 @@ func NewHandler(widgetService *service.WidgetService) *WidgetHandler {
}
}
func (h *WidgetHandler) WithEventPublisher(publisher widgetEventPublisher) *WidgetHandler {
h.eventPublisher = publisher
return h
}
// Init handles widget initialization — authenticates/creates a contact
// and returns a widget_token (pubsub_token) for subsequent requests.
// POST /widget/init
@@ -42,6 +53,9 @@ func (h *WidgetHandler) Init(c *gin.Context) {
if req.WebsiteToken == "" {
req.WebsiteToken = c.Query("website_token")
}
if req.WidgetToken == "" {
req.WidgetToken = widgetTokenFromRequest(c)
}
// HMAC verification: if the client provides an identifier + identifier_hash,
// verify the hash against the inbox's hmac_token. This mirrors Chatwoot's
@@ -88,6 +102,9 @@ func (h *WidgetHandler) Config(c *gin.Context) {
if req.WebsiteToken == "" {
req.WebsiteToken = c.Query("website_token")
}
if req.WidgetToken == "" {
req.WidgetToken = widgetTokenFromRequest(c)
}
resp, err := h.widgetService.Init(c.Request.Context(), req)
if err != nil {
@@ -114,16 +131,29 @@ func (h *WidgetHandler) Config(c *gin.Context) {
contact["phone_number"] = resp.Contact.PhoneNumber
}
channelConfig := 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,
"enabledFeatures": widgetEnabledFeatures(resp.WidgetConfig),
}
if resp.WidgetConfig.PreChatFieldsEnabled {
channelConfig["preChatFormEnabled"] = true
channelConfig["preChatFormOptions"] = gin.H{
"pre_chat_message": resp.WidgetConfig.PreChatMessage,
"pre_chat_fields": defaultWidgetPreChatFields(),
}
} else {
channelConfig["preChatFormEnabled"] = false
channelConfig["preChatFormOptions"] = gin.H{"pre_chat_message": "", "pre_chat_fields": []gin.H{}}
}
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,
"website_channel_config": channelConfig,
"contact": contact,
"global_config": gin.H{
"directUploadsEnabled": true,
"maximumFileUploadSize": 40,
@@ -131,6 +161,20 @@ func (h *WidgetHandler) Config(c *gin.Context) {
})
}
func defaultWidgetPreChatFields() []gin.H {
return []gin.H{
{"label": "Email Id", "name": "emailAddress", "type": "email", "field_type": "standard", "required": false, "enabled": true},
{"label": "Full name", "name": "fullName", "type": "text", "field_type": "standard", "required": true, "enabled": true},
}
}
func widgetEnabledFeatures(config service.WebWidgetConfig) []string {
if len(config.SelectedFeatureFlags) > 0 {
return config.SelectedFeatureFlags
}
return []string{"attachments", "emoji_picker", "end_conversation"}
}
// SendMessage sends a message from the widget contact to the conversation.
// POST /widget/messages
// Reference: Chatwoot widget SDK — send message endpoint
@@ -163,6 +207,11 @@ func (h *WidgetHandler) SendMessage(c *gin.Context) {
}
if c.FullPath() == "/widget/messages" {
payload := widgetMessagePayload(resp.Message, resp.ConversationID)
if len(resp.Attachments) > 0 {
payload["attachments"] = widgetAttachmentPayloads(resp.Attachments)
}
h.publishWidgetMessageEvent(req.WidgetToken, payload)
c.JSON(http.StatusOK, resp)
return
}
@@ -170,6 +219,7 @@ func (h *WidgetHandler) SendMessage(c *gin.Context) {
if len(resp.Attachments) > 0 {
payload["attachments"] = widgetAttachmentPayloads(resp.Attachments)
}
h.publishWidgetMessageEvent(req.WidgetToken, payload)
c.JSON(http.StatusOK, payload)
}
@@ -211,6 +261,40 @@ func (h *WidgetHandler) UpdateMessage(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"contact": widgetContactFullPayload(contact)})
}
func (h *WidgetHandler) publishWidgetMessageEvent(pubsubToken string, payload gin.H) {
if h.eventPublisher == nil || payload == nil {
return
}
accountID := uintFromPayload(payload["account_id"])
if accountID == 0 {
accountID = uintFromPayload(payload["accountId"])
}
if accountID == 0 {
return
}
h.eventPublisher.PublishWidgetEvent(accountID, pubsubToken, wspkg.EventMessageCreated, payload)
}
func uintFromPayload(value any) uint {
switch typed := value.(type) {
case uint:
return typed
case int:
if typed > 0 {
return uint(typed)
}
case int64:
if typed > 0 {
return uint(typed)
}
case float64:
if typed > 0 {
return uint(typed)
}
}
return 0
}
// GetLatestMessages implements Chatwoot's GET /api/v1/widget/messages endpoint.
func (h *WidgetHandler) GetLatestMessages(c *gin.Context) {
widgetToken := widgetTokenFromRequest(c)
@@ -1128,6 +1212,12 @@ func widgetTokenFromRequest(c *gin.Context) string {
if token := c.GetHeader("X-Auth-Token"); token != "" {
return token
}
if token := c.Query("cw_conversation"); token != "" {
return token
}
if cookie, err := c.Cookie("cw_conversation"); err == nil && cookie != "" {
return cookie
}
return c.Query("widget_token")
}
@@ -1227,6 +1317,7 @@ func firstFormValue(values map[string][]string, keys ...string) string {
func widgetMessagePayload(message model.Message, conversationID uint) gin.H {
return gin.H{
"id": message.ID,
"account_id": message.AccountID,
"content": message.Content,
"inbox_id": message.InboxID,
"conversation_id": conversationID,