Align GoChat with Chatwoot frontend contracts
This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user