Files
gochat/internal/handler/api/v1/conversation_serializer.go
T

622 lines
23 KiB
Go

package v1
import (
"context"
"encoding/json"
"path/filepath"
"strings"
"time"
"github.com/gochat/gochat/internal/model"
"github.com/gochat/gochat/internal/service"
"gorm.io/datatypes"
"gorm.io/gorm"
)
type chatwootConversationListResponse struct {
Data chatwootConversationListData `json:"data"`
}
type chatwootConversationListData struct {
Meta chatwootConversationCounts `json:"meta"`
Payload []chatwootConversationPayload `json:"payload"`
}
type chatwootConversationCounts struct {
MineCount int64 `json:"mine_count"`
AssignedCount int64 `json:"assigned_count"`
UnassignedCount int64 `json:"unassigned_count"`
AllCount int64 `json:"all_count"`
}
type chatwootConversationPayload struct {
Meta chatwootConversationMeta `json:"meta"`
ID uint `json:"id"`
Messages []chatwootMessagePayload `json:"messages"`
AccountID uint `json:"account_id"`
UUID string `json:"uuid"`
AdditionalAttributes map[string]any `json:"additional_attributes"`
AgentLastSeenAt int64 `json:"agent_last_seen_at"`
AssigneeLastSeenAt int64 `json:"assignee_last_seen_at"`
CanReply bool `json:"can_reply"`
ContactLastSeenAt int64 `json:"contact_last_seen_at"`
CustomAttributes map[string]any `json:"custom_attributes"`
InboxID uint `json:"inbox_id"`
Labels []string `json:"labels"`
Muted bool `json:"muted"`
SnoozedUntil *int64 `json:"snoozed_until"`
Status string `json:"status"`
CreatedAt int64 `json:"created_at"`
UpdatedAt float64 `json:"updated_at"`
Timestamp int64 `json:"timestamp"`
FirstReplyCreatedAt int64 `json:"first_reply_created_at"`
UnreadCount int64 `json:"unread_count"`
LastNonActivityMessage *chatwootMessagePayload `json:"last_non_activity_message"`
LastActivityAt int64 `json:"last_activity_at"`
Priority string `json:"priority"`
WaitingSince int64 `json:"waiting_since"`
SlaPolicyID *uint `json:"sla_policy_id"`
AppliedSLA map[string]any `json:"applied_sla,omitempty"`
}
type chatwootConversationMeta struct {
Sender map[string]any `json:"sender"`
Channel string `json:"channel"`
Assignee map[string]any `json:"assignee,omitempty"`
AssigneeType string `json:"assignee_type,omitempty"`
Team map[string]any `json:"team,omitempty"`
HMACVerified *bool `json:"hmac_verified,omitempty"`
}
type chatwootConversationSearchResponse struct {
Meta chatwootConversationSearchMeta `json:"meta"`
Payload []chatwootConversationSearchPayload `json:"payload"`
}
type chatwootConversationSearchMeta struct {
MineCount int64 `json:"mine_count"`
UnassignedCount int64 `json:"unassigned_count"`
AllCount int64 `json:"all_count"`
}
type chatwootConversationSearchPayload struct {
ID uint `json:"id"`
UUID string `json:"uuid"`
CreatedAt int64 `json:"created_at"`
Contact map[string]any `json:"contact"`
Inbox map[string]any `json:"inbox"`
Messages []chatwootConversationSearchMessage `json:"messages"`
AccountID uint `json:"account_id"`
Meta chatwootConversationMeta `json:"meta"`
}
type chatwootConversationSearchMessage struct {
Content string `json:"content"`
ID uint `json:"id"`
SenderName string `json:"sender_name,omitempty"`
MessageType int `json:"message_type"`
CreatedAt int64 `json:"created_at"`
}
type chatwootMessageIndexResponse struct {
Meta chatwootMessageIndexMeta `json:"meta"`
Payload []chatwootMessagePayload `json:"payload"`
}
type chatwootMessageIndexMeta struct {
Labels []string `json:"labels"`
AdditionalAttrs map[string]any `json:"additional_attributes"`
Contact map[string]any `json:"contact"`
Assignee map[string]any `json:"assignee,omitempty"`
AgentLastSeenAt int64 `json:"agent_last_seen_at"`
AssigneeLastSeenAt int64 `json:"assignee_last_seen_at"`
}
type chatwootMessagePayload struct {
ID uint `json:"id"`
Content string `json:"content"`
InboxID uint `json:"inbox_id"`
EchoID string `json:"echo_id,omitempty"`
ConversationID uint `json:"conversation_id"`
MessageType int `json:"message_type"`
ContentType string `json:"content_type"`
Status string `json:"status"`
ContentAttributes map[string]any `json:"content_attributes"`
CreatedAt int64 `json:"created_at"`
Private bool `json:"private"`
SourceID string `json:"source_id"`
Sender map[string]any `json:"sender,omitempty"`
Attachments []any `json:"attachments,omitempty"`
}
func serializeConversationList(ctx context.Context, db *gorm.DB, conversations []model.Conversation, total int64) chatwootConversationListResponse {
payload := make([]chatwootConversationPayload, 0, len(conversations))
for i := range conversations {
payload = append(payload, serializeConversation(ctx, db, &conversations[i]))
}
return chatwootConversationListResponse{Data: chatwootConversationListData{
Meta: chatwootConversationCounts{AllCount: total},
Payload: payload,
}}
}
func serializeConversationSearchList(ctx context.Context, db *gorm.DB, conversations []model.Conversation, counts service.FilterCountMeta) chatwootConversationSearchResponse {
payload := make([]chatwootConversationSearchPayload, 0, len(conversations))
for i := range conversations {
payload = append(payload, serializeConversationSearch(ctx, db, &conversations[i]))
}
return chatwootConversationSearchResponse{
Meta: chatwootConversationSearchMeta{
MineCount: counts.MineCount,
UnassignedCount: counts.UnassignedCount,
AllCount: counts.AllCount,
},
Payload: payload,
}
}
func serializeConversationSearch(ctx context.Context, db *gorm.DB, conversation *model.Conversation) chatwootConversationSearchPayload {
payload := chatwootConversationSearchPayload{
ID: conversationDisplayID(conversation),
UUID: conversation.UUID,
CreatedAt: conversation.CreatedAt.Unix(),
Contact: map[string]any{"id": conversation.ContactID},
Inbox: map[string]any{"id": conversation.InboxID, "channel_type": conversation.ChannelType},
Messages: []chatwootConversationSearchMessage{},
AccountID: conversation.AccountID,
Meta: serializeConversationMeta(ctx, db, conversation),
}
if db == nil {
return payload
}
var contact model.Contact
if err := db.WithContext(ctx).Where("id = ?", conversation.ContactID).First(&contact).Error; err == nil {
payload.Contact = map[string]any{"id": contact.ID, "name": contact.Name}
}
var inbox model.Inbox
if err := db.WithContext(ctx).Where("id = ?", conversation.InboxID).First(&inbox).Error; err == nil {
payload.Inbox = map[string]any{"id": inbox.ID, "name": inbox.Name, "channel_type": inbox.ChannelType}
}
var messages []model.Message
if err := db.WithContext(ctx).
Where("account_id = ? AND conversation_id = ?", conversation.AccountID, conversation.ID).
Order("id ASC").
Find(&messages).Error; err == nil {
payload.Messages = make([]chatwootConversationSearchMessage, 0, len(messages))
for i := range messages {
payload.Messages = append(payload.Messages, chatwootConversationSearchMessage{
Content: messages[i].Content,
ID: messages[i].ID,
MessageType: messageTypeValue(messages[i].MessageType),
CreatedAt: messages[i].CreatedAt.Unix(),
})
}
}
return payload
}
func serializeConversationPayloads(ctx context.Context, db *gorm.DB, conversations []model.Conversation) []chatwootConversationPayload {
payload := make([]chatwootConversationPayload, 0, len(conversations))
for i := range conversations {
payload = append(payload, serializeConversation(ctx, db, &conversations[i]))
}
return payload
}
func serializeConversation(ctx context.Context, db *gorm.DB, conversation *model.Conversation) chatwootConversationPayload {
var lastMessage *model.Message
if db != nil {
var msg model.Message
if err := db.WithContext(ctx).Where("account_id = ? AND conversation_id = ?", conversation.AccountID, conversation.ID).Order("id DESC").First(&msg).Error; err == nil {
lastMessage = &msg
}
}
messages := []chatwootMessagePayload{}
var lastNonActivity *chatwootMessagePayload
if lastMessage != nil {
serialized := serializeMessage(ctx, db, lastMessage, conversation)
messages = append(messages, serialized)
if lastMessage.MessageType != "activity" {
lastNonActivity = &serialized
}
}
payload := chatwootConversationPayload{
Meta: serializeConversationMeta(ctx, db, conversation),
ID: conversationDisplayID(conversation),
Messages: messages,
AccountID: conversation.AccountID,
UUID: conversation.UUID,
AdditionalAttributes: jsonObject(conversation.AdditionalAttributes),
AgentLastSeenAt: int64Value(conversation.AgentLastSeenAt),
AssigneeLastSeenAt: int64Value(conversation.AssigneeLastSeenAt),
CanReply: true,
ContactLastSeenAt: int64Value(conversation.ContactLastSeenAt),
CustomAttributes: jsonObject(conversation.CustomAttributes),
InboxID: conversation.InboxID,
Labels: labelList(conversation.Labels),
Muted: conversation.Muted,
SnoozedUntil: conversation.SnoozedUntil,
Status: conversation.Status,
CreatedAt: conversation.CreatedAt.Unix(),
UpdatedAt: float64(conversation.UpdatedAt.UnixNano()) / float64(time.Second),
Timestamp: int64Value(conversation.LastActivityAt),
FirstReplyCreatedAt: int64Value(conversation.FirstReplyCreatedAt),
UnreadCount: unreadCount(ctx, db, conversation),
LastNonActivityMessage: lastNonActivity,
LastActivityAt: int64Value(conversation.LastActivityAt),
Priority: conversation.Priority,
WaitingSince: int64Value(conversation.WaitingSince),
SlaPolicyID: conversation.SlaPolicyID,
}
if appliedSLA := serializeAppliedSlaForConversation(ctx, db, conversation.ID); appliedSLA != nil {
payload.AppliedSLA = appliedSLA
}
return payload
}
func serializeAppliedSlaForConversation(ctx context.Context, db *gorm.DB, conversationID uint) map[string]any {
if db == nil || conversationID == 0 {
return nil
}
var applied model.AppliedSLA
if err := db.WithContext(ctx).
Preload("SlaPolicy").
Where("conversation_id = ?", conversationID).
First(&applied).Error; err != nil {
return nil
}
return map[string]any{
"id": applied.ID,
"sla_id": applied.SlaPolicyID,
"sla_status": applied.SLAStatus,
"created_at": applied.CreatedAt.Unix(),
"updated_at": applied.UpdatedAt.Unix(),
"sla_description": applied.SlaPolicy.Description,
"sla_name": applied.SlaPolicy.Name,
"sla_first_response_time_threshold": applied.SlaPolicy.FirstResponseTimeThreshold,
"sla_next_response_time_threshold": applied.SlaPolicy.NextResponseTimeThreshold,
"sla_only_during_business_hours": applied.SlaPolicy.OnlyDuringBusinessHours,
"sla_resolution_time_threshold": applied.SlaPolicy.ResolutionTimeThreshold,
}
}
func serializeConversationMeta(ctx context.Context, db *gorm.DB, conversation *model.Conversation) chatwootConversationMeta {
meta := chatwootConversationMeta{Channel: conversation.ChannelType}
if db == nil {
meta.Sender = map[string]any{"id": conversation.ContactID}
return meta
}
var contact model.Contact
if err := db.WithContext(ctx).First(&contact, conversation.ContactID).Error; err == nil {
meta.Sender = serializeContact(&contact)
} else {
meta.Sender = map[string]any{"id": conversation.ContactID}
}
if conversation.AssigneeID != nil && *conversation.AssigneeID != 0 {
var user model.User
if err := db.WithContext(ctx).First(&user, *conversation.AssigneeID).Error; err == nil {
meta.Assignee = serializeUser(&user, conversation.AccountID)
meta.AssigneeType = "User"
}
}
if conversation.TeamID != nil && *conversation.TeamID != 0 {
var team model.Team
if err := db.WithContext(ctx).First(&team, *conversation.TeamID).Error; err == nil {
meta.Team = serializeTeam(&team)
}
}
if conversation.ContactInboxID != nil && *conversation.ContactInboxID != 0 {
var contactInbox model.ContactInbox
if err := db.WithContext(ctx).First(&contactInbox, *conversation.ContactInboxID).Error; err == nil {
meta.HMACVerified = &contactInbox.HMACVerified
}
}
return meta
}
func serializeMessageIndex(ctx context.Context, db *gorm.DB, conversation *model.Conversation, messages []model.Message) chatwootMessageIndexResponse {
payload := make([]chatwootMessagePayload, 0, len(messages))
for i := range messages {
payload = append(payload, serializeMessage(ctx, db, &messages[i], conversation))
}
meta := chatwootMessageIndexMeta{
Labels: labelList(conversation.Labels),
AdditionalAttrs: jsonObject(conversation.AdditionalAttributes),
Contact: map[string]any{"id": conversation.ContactID},
AgentLastSeenAt: int64Value(conversation.AgentLastSeenAt),
AssigneeLastSeenAt: int64Value(conversation.AssigneeLastSeenAt),
}
if db != nil {
var contact model.Contact
if err := db.WithContext(ctx).First(&contact, conversation.ContactID).Error; err == nil {
meta.Contact = serializeContact(&contact)
}
if conversation.AssigneeID != nil && *conversation.AssigneeID != 0 {
var user model.User
if err := db.WithContext(ctx).First(&user, *conversation.AssigneeID).Error; err == nil {
meta.Assignee = serializeUser(&user, conversation.AccountID)
}
}
}
return chatwootMessageIndexResponse{Meta: meta, Payload: payload}
}
func serializeMessage(ctx context.Context, db *gorm.DB, message *model.Message, conversation *model.Conversation) chatwootMessagePayload {
conversationID := message.ConversationID
if conversation != nil {
conversationID = conversationDisplayID(conversation)
}
payload := chatwootMessagePayload{
ID: message.ID,
Content: message.Content,
InboxID: message.InboxID,
EchoID: message.EchoID,
ConversationID: conversationID,
MessageType: messageTypeValue(message.MessageType),
ContentType: nonEmpty(message.ContentType, "text"),
Status: nonEmpty(message.Status, "sent"),
ContentAttributes: jsonObject(message.ContentAttributes),
CreatedAt: message.CreatedAt.Unix(),
Private: message.Private,
SourceID: message.SourceID,
}
if db != nil && message.SenderID != nil && *message.SenderID != 0 {
senderType := strings.ToLower(message.SenderType)
if senderType == "contact" {
var contact model.Contact
if err := db.WithContext(ctx).First(&contact, *message.SenderID).Error; err == nil {
payload.Sender = serializeContact(&contact)
}
} else {
var user model.User
if err := db.WithContext(ctx).First(&user, *message.SenderID).Error; err == nil {
payload.Sender = serializeUser(&user, message.AccountID)
}
}
}
if db != nil {
var attachments []model.Attachment
if err := db.WithContext(ctx).Where("message_id = ?", message.ID).Order("id ASC").Find(&attachments).Error; err == nil && len(attachments) > 0 {
payload.Attachments = make([]any, 0, len(attachments))
for i := range attachments {
payload.Attachments = append(payload.Attachments, serializeAttachment(ctx, db, &attachments[i]))
}
}
}
return payload
}
func serializeAttachment(ctx context.Context, db *gorm.DB, attachment *model.Attachment) map[string]any {
extension := strings.TrimPrefix(filepath.Ext(attachment.FileName), ".")
dataURL := nonEmpty(attachment.FileURL, attachment.ExternalURL)
payload := map[string]any{
"id": attachment.ID,
"message_id": attachment.MessageID,
"file_type": attachment.FileType,
"account_id": attachment.AccountID,
"data_url": dataURL,
"thumb_url": attachment.ThumbURL,
"file_size": attachment.FileSize,
"extension": extension,
"width": attachment.Width,
"height": attachment.Height,
}
message := attachment.Message
if message.ID == 0 && db != nil && attachment.MessageID != 0 {
_ = db.WithContext(ctx).First(&message, attachment.MessageID).Error
}
if !message.CreatedAt.IsZero() {
payload["created_at"] = message.CreatedAt.Unix()
}
if sender := serializeMessageSender(ctx, db, &message); sender != nil {
payload["sender"] = sender
}
return payload
}
func serializeAttachmentWithConversation(ctx context.Context, db *gorm.DB, attachment *model.Attachment) map[string]any {
payload := serializeAttachment(ctx, db, attachment)
message := attachment.Message
if message.ID == 0 && db != nil && attachment.MessageID != 0 {
_ = db.WithContext(ctx).First(&message, attachment.MessageID).Error
}
if db != nil && message.ConversationID != 0 {
var conversation model.Conversation
if err := db.WithContext(ctx).First(&conversation, message.ConversationID).Error; err == nil {
payload["conversation_id"] = conversationDisplayID(&conversation)
}
}
return payload
}
func serializeMessageSender(ctx context.Context, db *gorm.DB, message *model.Message) map[string]any {
if db == nil || message == nil || message.SenderID == nil || *message.SenderID == 0 {
return nil
}
senderType := strings.ToLower(message.SenderType)
if senderType == "contact" {
var contact model.Contact
if err := db.WithContext(ctx).First(&contact, *message.SenderID).Error; err == nil {
return serializeContact(&contact)
}
return nil
}
var user model.User
if err := db.WithContext(ctx).First(&user, *message.SenderID).Error; err == nil {
return serializeUser(&user, message.AccountID)
}
return nil
}
func serializeContact(contact *model.Contact) map[string]any {
return map[string]any{
"additional_attributes": jsonObject(contact.AdditionalAttributes),
"availability_status": "offline",
"email": contact.Email,
"id": contact.ID,
"name": contact.Name,
"phone_number": contact.PhoneNumber,
"blocked": contact.Blocked,
"identifier": contact.Identifier,
"thumbnail": contact.AvatarURL,
"custom_attributes": jsonObject(contact.CustomAttributes),
"last_activity_at": int64Value(contact.LastActivityAt),
"created_at": contact.CreatedAt.Unix(),
}
}
func serializeUser(user *model.User, accountID uint) map[string]any {
return map[string]any{
"id": user.ID,
"account_id": accountID,
"availability_status": availabilityStatus(user.Available),
"auto_offline": false,
"confirmed": user.ConfirmedAt != nil,
"email": user.Email,
"provider": nonEmpty(user.Provider, "email"),
"available_name": nonEmpty(user.DisplayName, user.Name),
"name": user.Name,
"role": nonEmpty(user.Role, "agent"),
"thumbnail": user.AvatarURL,
}
}
func serializeUserFromDB(ctx context.Context, db *gorm.DB, userID uint, accountID uint) any {
if userID == 0 || db == nil {
return nil
}
var user model.User
if err := db.WithContext(ctx).First(&user, userID).Error; err != nil {
return nil
}
return serializeUser(&user, accountID)
}
func serializeTeam(team *model.Team) map[string]any {
return map[string]any{
"id": team.ID,
"account_id": team.AccountID,
"name": team.Name,
"description": team.Description,
"allow_auto_assignment": team.AllowAutoAssignment,
"created_at": team.CreatedAt.Unix(),
"updated_at": team.UpdatedAt.Unix(),
}
}
func serializeTeamFromDB(ctx context.Context, db *gorm.DB, teamID uint, accountID uint) any {
if teamID == 0 || db == nil {
return nil
}
var team model.Team
if err := db.WithContext(ctx).Where("id = ? AND account_id = ?", teamID, accountID).First(&team).Error; err != nil {
return nil
}
return serializeTeam(&team)
}
func conversationDisplayID(conversation *model.Conversation) uint {
if conversation.DisplayID != nil && *conversation.DisplayID != 0 {
return *conversation.DisplayID
}
return conversation.ID
}
func messageTypeValue(value string) int {
switch strings.ToLower(value) {
case "incoming":
return 0
case "outgoing", "private_note":
return 1
case "activity":
return 2
case "template":
return 3
default:
return 1
}
}
func labelList(labels string) []string {
labels = strings.TrimSpace(labels)
if labels == "" {
return []string{}
}
if strings.HasPrefix(labels, "[") {
var list []string
if err := json.Unmarshal([]byte(labels), &list); err == nil {
return list
}
}
parts := strings.Split(labels, ",")
result := make([]string, 0, len(parts))
for _, part := range parts {
label := strings.TrimSpace(part)
if label != "" {
result = append(result, label)
}
}
return result
}
func jsonObject(raw datatypes.JSON) map[string]any {
if len(raw) == 0 || string(raw) == "null" {
return map[string]any{}
}
var value map[string]any
if err := json.Unmarshal(raw, &value); err != nil || value == nil {
return map[string]any{}
}
return value
}
func int64Value(value *int64) int64 {
if value == nil {
return 0
}
return *value
}
func nonEmpty(value, fallback string) string {
if value == "" {
return fallback
}
return value
}
func availabilityStatus(available bool) string {
if available {
return "online"
}
return "offline"
}
func unreadCount(ctx context.Context, db *gorm.DB, conversation *model.Conversation) int64 {
if db == nil {
return 0
}
query := db.WithContext(ctx).Model(&model.Message{}).
Where("account_id = ? AND conversation_id = ? AND message_type = ?", conversation.AccountID, conversation.ID, "incoming")
if conversation.AgentLastSeenAt != nil {
query = query.Where("created_at > ?", time.Unix(*conversation.AgentLastSeenAt, 0))
}
var count int64
_ = query.Count(&count).Error
return count
}