HH-469: close WebSocket fanout contract gaps (#109)

* HH-469: close websocket fanout contract gaps

* fix: unify message sender contracts

---------

Co-authored-by: Rogee <rogee@ipao.vip>
This commit is contained in:
Rogee
2026-08-23 00:22:01 +08:00
committed by GitHub
co-authored by rogee
parent f91dc6ca1a
commit ef8931099c
14 changed files with 377 additions and 114 deletions
+32 -1
View File
@@ -1,5 +1,11 @@
package model
import (
"encoding/json"
"path/filepath"
"strings"
)
// Attachment represents a file attachment on a message.
// Reference: Chatwoot Attachment model + P2B M3 spec
type Attachment struct {
@@ -21,4 +27,29 @@ type Attachment struct {
Account Account `gorm:"foreignKey:AccountID" json:"account,omitempty"`
}
func (Attachment) TableName() string { return "attachments" }
func (Attachment) TableName() string { return "attachments" }
// PushEventData matches Chatwoot's attachment payload shared by HTTP and WebSocket messages.
func (a *Attachment) PushEventData() map[string]any {
dataURL := a.FileURL
if dataURL == "" {
dataURL = a.ExternalURL
}
payload := map[string]any{
"id": a.ID,
"message_id": a.MessageID,
"file_type": a.FileType,
"account_id": a.AccountID,
"data_url": dataURL,
"thumb_url": a.ThumbURL,
"file_size": a.FileSize,
"extension": strings.TrimPrefix(filepath.Ext(a.FileName), "."),
"width": a.Width,
"height": a.Height,
}
metadata := map[string]any{}
if json.Unmarshal([]byte(a.Metadata), &metadata) == nil && len(metadata) > 0 {
payload["metadata"] = metadata
}
return payload
}
+15
View File
@@ -3,6 +3,8 @@ package model
import (
"encoding/json"
"fmt"
"os"
"strings"
"github.com/pgvector/pgvector-go"
)
@@ -89,6 +91,19 @@ type CaptainAssistant struct {
func (CaptainAssistant) TableName() string { return "captain_assistants" }
// PushEventData matches Chatwoot's public message-sender contract.
func (a *CaptainAssistant) PushEventData() map[string]any {
avatarURL := strings.TrimRight(os.Getenv("FRONTEND_URL"), "/") + "/assets/images/dashboard/captain/logo.svg"
return map[string]any{
"id": a.ID,
"name": a.Name,
"avatar_url": avatarURL,
"description": a.Description,
"created_at": a.CreatedAt,
"type": "captain_assistant",
}
}
// DefaultAssistantConfig returns the default configuration for a new Captain Assistant.
// Reference: Chatwoot Captain::Assistant default config values
func DefaultAssistantConfig() map[string]interface{} {
+16
View File
@@ -34,6 +34,22 @@ type Contact struct {
func (Contact) TableName() string { return "contacts" }
// PushEventData matches Chatwoot's public message-sender contract.
func (c *Contact) PushEventData() map[string]any {
return map[string]any{
"additional_attributes": jsonMap(c.AdditionalAttributes),
"custom_attributes": jsonMap(c.CustomAttributes),
"email": c.Email,
"id": c.ID,
"identifier": c.Identifier,
"name": c.Name,
"phone_number": c.PhoneNumber,
"thumbnail": c.AvatarURL,
"blocked": c.Blocked,
"type": "contact",
}
}
// BeforeSave syncs location/country_code from additional_attributes and upgrades contact_type.
// Reference: Chatwoot before_save :sync_contact_attributes → Contacts::SyncAttributes
func (c *Contact) BeforeSave(tx *gorm.DB) error {
+21
View File
@@ -47,6 +47,27 @@ type User struct {
func (User) TableName() string { return "users" }
// PushEventData matches Chatwoot's public message-sender contract.
func (u *User) PushEventData() map[string]any {
availableName := u.DisplayName
if availableName == "" {
availableName = u.Name
}
availabilityStatus := "offline"
if u.Available {
availabilityStatus = "online"
}
return map[string]any{
"id": u.ID,
"name": u.Name,
"available_name": availableName,
"avatar_url": u.AvatarURL,
"type": "user",
"availability_status": availabilityStatus,
"thumbnail": u.AvatarURL,
}
}
// BeforeCreate mirrors Chatwoot's Pubsubable concern by assigning every user a cable token.
func (u *User) BeforeCreate(tx *gorm.DB) error {
if u.PubsubToken == "" {