H-337: fix Web Channel availability and realtime delivery (#60)

* H-337: fix Web Channel availability and realtime delivery

* fix(widget): preserve realtime sender and activity contracts

* fix(widget): keep realtime sender payloads consistent

* fix(widget): make public message persistence atomic

* fix(inbox): keep availability projection out of schema

---------

Co-authored-by: Rogee <rogee@ipao.vip>
This commit is contained in:
Rogee
2026-08-20 14:30:05 +08:00
committed by GitHub
co-authored by rogee
parent 55b56ee172
commit 3d9817c9f5
17 changed files with 769 additions and 247 deletions
@@ -12,20 +12,14 @@ import (
"github.com/gochat/gochat/internal/model"
"github.com/gochat/gochat/internal/service"
"github.com/gochat/gochat/internal/webhookutil"
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
eventPublisher widgetEventPublisher
widgetService *service.WidgetService
}
// NewHandler creates a new WidgetHandler with the WidgetService dependency.
@@ -35,11 +29,6 @@ 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
@@ -212,7 +201,6 @@ 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, resp)
return
}
@@ -220,7 +208,6 @@ 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)
}
@@ -262,40 +249,6 @@ 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)