mirror of
https://github.com/abhinavxd/libredesk.git
synced 2026-09-22 02:23:32 +00:00
524 lines
22 KiB
Go
524 lines
22 KiB
Go
package models
|
|
|
|
import (
|
|
"encoding/json"
|
|
"time"
|
|
|
|
"github.com/abhinavxd/libredesk/internal/attachment"
|
|
"github.com/abhinavxd/libredesk/internal/stringutil"
|
|
mmodels "github.com/abhinavxd/libredesk/internal/media/models"
|
|
umodels "github.com/abhinavxd/libredesk/internal/user/models"
|
|
"github.com/lib/pq"
|
|
"github.com/volatiletech/null/v9"
|
|
)
|
|
|
|
var (
|
|
StatusOpen = "Open"
|
|
StatusReplied = "Replied"
|
|
StatusResolved = "Resolved"
|
|
StatusClosed = "Closed"
|
|
StatusSnoozed = "Snoozed"
|
|
|
|
AssigneeTypeTeam = "team"
|
|
AssigneeTypeUser = "user"
|
|
|
|
AllConversations = "all"
|
|
AssignedConversations = "assigned"
|
|
UnassignedConversations = "unassigned"
|
|
TeamUnassignedConversations = "team_unassigned"
|
|
TeamAllConversations = "team_all"
|
|
MentionedConversations = "mentioned"
|
|
|
|
MessageIncoming = "incoming"
|
|
MessageOutgoing = "outgoing"
|
|
MessageActivity = "activity"
|
|
|
|
SenderTypeAgent = "agent"
|
|
SenderTypeContact = "contact"
|
|
|
|
MentionTypeAgent = "agent"
|
|
MentionTypeTeam = "team"
|
|
|
|
MessageStatusPending = "pending"
|
|
MessageStatusSent = "sent"
|
|
MessageStatusFailed = "failed"
|
|
MessageStatusReceived = "received"
|
|
|
|
ActivityStatusChange = "status_change"
|
|
ActivityPriorityChange = "priority_change"
|
|
ActivityAssignedUserChange = "assigned_user_change"
|
|
ActivityAssignedTeamChange = "assigned_team_change"
|
|
ActivitySelfAssign = "self_assign"
|
|
ActivityTagAdded = "tag_added"
|
|
ActivityTagRemoved = "tag_removed"
|
|
ActivitySLASet = "sla_set"
|
|
ActivityParticipantAdded = "participant_added"
|
|
|
|
ContentTypeText = "text"
|
|
ContentTypeHTML = "html"
|
|
)
|
|
|
|
type ContinuityConversation struct {
|
|
ID int `db:"id"`
|
|
UUID string `db:"uuid"`
|
|
ContactID int `db:"contact_id"`
|
|
InboxID int `db:"inbox_id"`
|
|
ContactLastSeenAt time.Time `db:"contact_last_seen_at"`
|
|
LastContinuityEmailSentAt null.Time `db:"last_continuity_email_sent_at"`
|
|
ContactEmail null.String `db:"contact_email"`
|
|
ContactFirstName null.String `db:"contact_first_name"`
|
|
ContactLastName null.String `db:"contact_last_name"`
|
|
LinkedEmailInboxID null.Int `db:"linked_email_inbox_id"`
|
|
ReferenceNumber string `db:"reference_number"`
|
|
ContinuityEmailSubject null.String `db:"continuity_email_subject"`
|
|
}
|
|
|
|
type ContinuityUnreadMessage struct {
|
|
Message
|
|
SenderFirstName null.String `db:"sender.first_name"`
|
|
SenderLastName null.String `db:"sender.last_name"`
|
|
SenderType string `db:"sender.type"`
|
|
AttachmentNames string `db:"attachment_names"`
|
|
}
|
|
|
|
type LastChatMessage struct {
|
|
Content string `db:"content" json:"content"`
|
|
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
|
Author umodels.ChatUser `db:"author" json:"author"`
|
|
}
|
|
|
|
type ChatConversation struct {
|
|
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
|
UUID string `db:"uuid" json:"uuid"`
|
|
Status string `db:"status" json:"status"`
|
|
LastChatMessage LastChatMessage `db:"last_message" json:"last_message"`
|
|
UnreadMessageCount int `db:"unread_message_count" json:"unread_message_count"`
|
|
Assignee *umodels.ChatUser `db:"assignee" json:"assignee"`
|
|
}
|
|
|
|
type ChatMessage struct {
|
|
UUID string `json:"uuid"`
|
|
Status string `json:"status"`
|
|
ConversationUUID string `json:"conversation_uuid"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
Content string `json:"content"`
|
|
TextContent string `json:"text_content"`
|
|
Author MessageAuthor `json:"author"`
|
|
Attachments attachment.Attachments `json:"attachments"`
|
|
Meta json.RawMessage `json:"meta"`
|
|
}
|
|
|
|
// ConversationListItem represents a conversation in list views
|
|
type ConversationListItem struct {
|
|
Total int `db:"total" json:"-"`
|
|
ID int `db:"id" json:"id"`
|
|
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
|
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
|
|
UUID string `db:"uuid" json:"uuid"`
|
|
ReferenceNumber string `db:"reference_number" json:"reference_number"`
|
|
WaitingSince null.Time `db:"waiting_since" json:"waiting_since"`
|
|
Contact ConversationListContact `db:"contact" json:"contact"`
|
|
InboxChannel string `db:"inbox_channel" json:"inbox_channel"`
|
|
InboxName string `db:"inbox_name" json:"inbox_name"`
|
|
SLAPolicyID null.Int `db:"sla_policy_id" json:"sla_policy_id"`
|
|
FirstReplyAt null.Time `db:"first_reply_at" json:"first_reply_at"`
|
|
LastReplyAt null.Time `db:"last_reply_at" json:"last_reply_at"`
|
|
ResolvedAt null.Time `db:"resolved_at" json:"resolved_at"`
|
|
Subject null.String `db:"subject" json:"subject"`
|
|
LastMessage null.String `db:"last_message" json:"last_message"`
|
|
LastMessageAt null.Time `db:"last_message_at" json:"last_message_at"`
|
|
LastMessageSender null.String `db:"last_message_sender" json:"last_message_sender"`
|
|
LastInteraction null.String `db:"last_interaction" json:"last_interaction"`
|
|
LastInteractionAt null.Time `db:"last_interaction_at" json:"last_interaction_at"`
|
|
LastInteractionSender null.String `db:"last_interaction_sender" json:"last_interaction_sender"`
|
|
NextSLADeadlineAt null.Time `db:"next_sla_deadline_at" json:"next_sla_deadline_at"`
|
|
PriorityID null.Int `db:"priority_id" json:"priority_id"`
|
|
UnreadMessageCount int `db:"unread_message_count" json:"unread_message_count"`
|
|
Status null.String `db:"status" json:"status"`
|
|
Priority null.String `db:"priority" json:"priority"`
|
|
FirstResponseDueAt null.Time `db:"first_response_deadline_at" json:"first_response_deadline_at"`
|
|
ResolutionDueAt null.Time `db:"resolution_deadline_at" json:"resolution_deadline_at"`
|
|
AppliedSLAID null.Int `db:"applied_sla_id" json:"applied_sla_id"`
|
|
NextResponseDueAt null.Time `db:"next_response_deadline_at" json:"next_response_deadline_at"`
|
|
NextResponseMetAt null.Time `db:"next_response_met_at" json:"next_response_met_at"`
|
|
MentionedMessageUUID null.String `db:"mentioned_message_uuid" json:"mentioned_message_uuid"`
|
|
}
|
|
|
|
// ConversationListContact represents contact info in conversation list views
|
|
type ConversationListContact struct {
|
|
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
|
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
|
|
FirstName string `db:"first_name" json:"first_name"`
|
|
LastName string `db:"last_name" json:"last_name"`
|
|
Email null.String `db:"email" json:"email"`
|
|
AvatarURL null.String `db:"avatar_url" json:"avatar_url"`
|
|
}
|
|
|
|
type Conversation struct {
|
|
ID int `db:"id" json:"id"`
|
|
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
|
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
|
|
UUID string `db:"uuid" json:"uuid"`
|
|
ContactID int `db:"contact_id" json:"contact_id"`
|
|
InboxID int `db:"inbox_id" json:"inbox_id"`
|
|
ClosedAt null.Time `db:"closed_at" json:"closed_at"`
|
|
ResolvedAt null.Time `db:"resolved_at" json:"resolved_at"`
|
|
ReferenceNumber string `db:"reference_number" json:"reference_number"`
|
|
Priority null.String `db:"priority" json:"priority"`
|
|
PriorityID null.Int `db:"priority_id" json:"priority_id"`
|
|
Status null.String `db:"status" json:"status"`
|
|
StatusID null.Int `db:"status_id" json:"status_id"`
|
|
FirstReplyAt null.Time `db:"first_reply_at" json:"first_reply_at"`
|
|
LastReplyAt null.Time `db:"last_reply_at" json:"last_reply_at"`
|
|
AssignedUserID null.Int `db:"assigned_user_id" json:"assigned_user_id"`
|
|
AssignedTeamID null.Int `db:"assigned_team_id" json:"assigned_team_id"`
|
|
WaitingSince null.Time `db:"waiting_since" json:"waiting_since"`
|
|
Subject null.String `db:"subject" json:"subject"`
|
|
InboxMail string `db:"inbox_mail" json:"inbox_mail"`
|
|
InboxName string `db:"inbox_name" json:"inbox_name"`
|
|
InboxChannel string `db:"inbox_channel" json:"inbox_channel"`
|
|
Tags null.JSON `db:"tags" json:"tags"`
|
|
Meta json.RawMessage `db:"meta" json:"meta"`
|
|
CustomAttributes json.RawMessage `db:"custom_attributes" json:"custom_attributes"`
|
|
LastMessageAt null.Time `db:"last_message_at" json:"last_message_at"`
|
|
LastMessage null.String `db:"last_message" json:"last_message"`
|
|
LastMessageSender null.String `db:"last_message_sender" json:"last_message_sender"`
|
|
LastInteraction null.String `db:"last_interaction" json:"last_interaction"`
|
|
LastInteractionAt null.Time `db:"last_interaction_at" json:"last_interaction_at"`
|
|
LastInteractionSender null.String `db:"last_interaction_sender" json:"last_interaction_sender"`
|
|
Contact ConversationContact `db:"contact" json:"contact"`
|
|
SLAPolicyID null.Int `db:"sla_policy_id" json:"sla_policy_id"`
|
|
SlaPolicyName null.String `db:"sla_policy_name" json:"sla_policy_name"`
|
|
AppliedSLAID null.Int `db:"applied_sla_id" json:"applied_sla_id"`
|
|
FirstResponseDueAt null.Time `db:"first_response_deadline_at" json:"first_response_deadline_at"`
|
|
ResolutionDueAt null.Time `db:"resolution_deadline_at" json:"resolution_deadline_at"`
|
|
NextResponseDueAt null.Time `db:"next_response_deadline_at" json:"next_response_deadline_at"`
|
|
NextResponseMetAt null.Time `db:"next_response_met_at" json:"next_response_met_at"`
|
|
PreviousConversations []PreviousConversation `db:"-" json:"previous_conversations"`
|
|
}
|
|
|
|
type ConversationContact struct {
|
|
ID int `db:"id" json:"id"`
|
|
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
|
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
|
|
FirstName string `db:"first_name" json:"first_name"`
|
|
LastName string `db:"last_name" json:"last_name"`
|
|
Email null.String `db:"email" json:"email"`
|
|
Type string `db:"type" json:"type"`
|
|
AvailabilityStatus string `db:"availability_status" json:"availability_status"`
|
|
AvatarURL null.String `db:"avatar_url" json:"avatar_url"`
|
|
PhoneNumber null.String `db:"phone_number" json:"phone_number"`
|
|
PhoneNumberCountryCode null.String `db:"phone_number_country_code" json:"phone_number_country_code"`
|
|
Country null.String `db:"country" json:"country"`
|
|
CustomAttributes json.RawMessage `db:"custom_attributes" json:"custom_attributes"`
|
|
Enabled bool `db:"enabled" json:"enabled"`
|
|
LastActiveAt null.Time `db:"last_active_at" json:"last_active_at"`
|
|
LastLoginAt null.Time `db:"last_login_at" json:"last_login_at"`
|
|
ExternalUserID null.String `db:"external_user_id" json:"external_user_id"`
|
|
}
|
|
|
|
func (c *ConversationContact) FullName() string {
|
|
if c.LastName == "" {
|
|
return c.FirstName
|
|
}
|
|
return c.FirstName + " " + c.LastName
|
|
}
|
|
|
|
type PreviousConversation struct {
|
|
ID int `db:"id" json:"id"`
|
|
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
|
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
|
|
UUID string `db:"uuid" json:"uuid"`
|
|
Subject string `db:"subject" json:"subject"`
|
|
Contact PreviousConversationContact `db:"contact" json:"contact"`
|
|
LastMessage null.String `db:"last_message" json:"last_message"`
|
|
LastMessageAt null.Time `db:"last_message_at" json:"last_message_at"`
|
|
}
|
|
|
|
type PreviousConversationContact struct {
|
|
FirstName string `db:"first_name" json:"first_name"`
|
|
LastName string `db:"last_name" json:"last_name"`
|
|
AvatarURL null.String `db:"avatar_url" json:"avatar_url"`
|
|
}
|
|
|
|
type ConversationParticipant struct {
|
|
ID int `db:"id" json:"id"`
|
|
FirstName string `db:"first_name" json:"first_name"`
|
|
LastName string `db:"last_name" json:"last_name"`
|
|
AvatarURL null.String `db:"avatar_url" json:"avatar_url"`
|
|
}
|
|
|
|
type MessageAuthor struct {
|
|
ID int `db:"id" json:"id"`
|
|
FirstName string `db:"first_name" json:"first_name"`
|
|
LastName string `db:"last_name" json:"last_name"`
|
|
Email null.String `db:"email" json:"email"`
|
|
AvatarURL null.String `db:"avatar_url" json:"avatar_url"`
|
|
AvailabilityStatus string `db:"availability_status" json:"availability_status"`
|
|
Type string `db:"type" json:"type"`
|
|
LastActiveAt null.Time `db:"last_active_at" json:"last_active_at"`
|
|
}
|
|
|
|
type ConversationCounts struct {
|
|
TotalAssigned int `db:"total_assigned" json:"total_assigned"`
|
|
UnresolvedCount int `db:"unresolved_count" json:"unresolved_count"`
|
|
AwaitingResponseCount int `db:"awaiting_response_count" json:"awaiting_response_count"`
|
|
CreatedTodayCount int `db:"created_today_count" json:"created_today_count"`
|
|
}
|
|
|
|
type NewConversationsStats struct {
|
|
Date string `db:"date" json:"date"`
|
|
NewConversations int `db:"new_conversations" json:"new_conversations"`
|
|
}
|
|
|
|
// Message represents a message in a conversation
|
|
type Message struct {
|
|
Total int `db:"total" json:"-"`
|
|
ID int `db:"id" json:"id"`
|
|
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
|
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
|
|
UUID string `db:"uuid" json:"uuid"`
|
|
Type string `db:"type" json:"type"`
|
|
Status string `db:"status" json:"status"`
|
|
ConversationID int `db:"conversation_id" json:"conversation_id"`
|
|
ConversationUUID string `db:"conversation_uuid" json:"conversation_uuid"`
|
|
Content string `db:"content" json:"content"`
|
|
TextContent string `db:"text_content" json:"text_content"`
|
|
ContentType string `db:"content_type" json:"content_type"`
|
|
Private bool `db:"private" json:"private"`
|
|
SourceID null.String `db:"source_id" json:"-"`
|
|
SenderID int `db:"sender_id" json:"sender_id"`
|
|
SenderType string `db:"sender_type" json:"sender_type"`
|
|
InboxID int `db:"inbox_id" json:"-"`
|
|
Meta json.RawMessage `db:"meta" json:"meta"`
|
|
Attachments attachment.Attachments `db:"attachments" json:"attachments"`
|
|
From string `db:"from" json:"-"`
|
|
Subject string `db:"subject" json:"-"`
|
|
Channel string `db:"channel" json:"-"`
|
|
To pq.StringArray `db:"to" json:"-"`
|
|
CC pq.StringArray `db:"cc" json:"-"`
|
|
BCC pq.StringArray `db:"bcc" json:"-"`
|
|
MessageReceiverID int `db:"message_receiver_id" json:"-"`
|
|
Media []mmodels.Media `json:"-"`
|
|
Author MessageAuthor `db:"author" json:"author"`
|
|
}
|
|
|
|
// IsContinuityMessage returns true if the message is a continuity email.
|
|
func (m *Message) IsContinuityMessage() bool {
|
|
var meta map[string]any
|
|
if err := json.Unmarshal([]byte(m.Meta), &meta); err != nil {
|
|
return false
|
|
}
|
|
isContinuity, _ := meta["continuity_email"].(bool)
|
|
return isContinuity
|
|
}
|
|
|
|
// csatMeta unmarshals the message meta and returns the map and whether is_csat is true.
|
|
func (m *Message) csatMeta() (map[string]any, bool) {
|
|
var meta map[string]any
|
|
if err := json.Unmarshal([]byte(m.Meta), &meta); err != nil {
|
|
return nil, false
|
|
}
|
|
isCsat, _ := meta["is_csat"].(bool)
|
|
return meta, isCsat
|
|
}
|
|
|
|
// HasCSAT returns true if the message is a CSAT message.
|
|
func (m *Message) HasCSAT() bool {
|
|
_, isCsat := m.csatMeta()
|
|
return isCsat
|
|
}
|
|
|
|
// ExtractCSATUUID extracts the CSAT UUID from the message meta, falling back to URL parsing.
|
|
func (m *Message) ExtractCSATUUID() string {
|
|
meta, isCsat := m.csatMeta()
|
|
if !isCsat {
|
|
return ""
|
|
}
|
|
|
|
// Read from meta first.
|
|
if uuid, ok := meta["csat_uuid"].(string); ok && uuid != "" {
|
|
return uuid
|
|
}
|
|
|
|
// Fallback: extract UUID from the CSAT URL in the message content.
|
|
return stringutil.ExtractUUID(m.Content)
|
|
}
|
|
|
|
// CensorCSATContentWithStatus redacts the content and adds submission status for CSAT messages.
|
|
func (m *Message) CensorCSATContentWithStatus(csatSubmitted bool, csatUUID string, rating int, feedback string) {
|
|
meta, isCsat := m.csatMeta()
|
|
if !isCsat {
|
|
return
|
|
}
|
|
|
|
m.Content = "Please rate this conversation"
|
|
m.TextContent = m.Content
|
|
|
|
meta["csat_submitted"] = csatSubmitted
|
|
meta["csat_uuid"] = csatUUID
|
|
|
|
if csatSubmitted {
|
|
if rating > 0 {
|
|
meta["submitted_rating"] = rating
|
|
}
|
|
meta["submitted_feedback"] = feedback
|
|
}
|
|
|
|
if updatedMeta, err := json.Marshal(meta); err == nil {
|
|
m.Meta = json.RawMessage(updatedMeta)
|
|
}
|
|
}
|
|
|
|
// StripCSATUUID removes the csat_uuid from the message meta.
|
|
// Used to hide CSAT links from agent sessions while keeping them for API key callers.
|
|
func (m *Message) StripCSATUUID() {
|
|
var meta map[string]any
|
|
if err := json.Unmarshal([]byte(m.Meta), &meta); err != nil {
|
|
return
|
|
}
|
|
delete(meta, "csat_uuid")
|
|
if updatedMeta, err := json.Marshal(meta); err == nil {
|
|
m.Meta = json.RawMessage(updatedMeta)
|
|
}
|
|
}
|
|
|
|
// OutboundMessage contains fields needed for sending messages via inboxes.
|
|
type OutboundMessage struct {
|
|
// Core message identifiers
|
|
UUID string
|
|
ConversationUUID string
|
|
|
|
// Sender info
|
|
SenderID int
|
|
MessageReceiverID int
|
|
|
|
// Content
|
|
Content string
|
|
TextContent string
|
|
ContentType string
|
|
AltContent string // Plain text alternative for HTML emails
|
|
|
|
// Email-specific fields
|
|
From string
|
|
To []string
|
|
CC []string
|
|
BCC []string
|
|
Subject string
|
|
SourceID string
|
|
|
|
// Threading (email)
|
|
References []string
|
|
InReplyTo string
|
|
ReplyTo string
|
|
|
|
// Attachments
|
|
Attachments attachment.Attachments
|
|
|
|
// Metadata
|
|
Meta json.RawMessage
|
|
CreatedAt time.Time
|
|
}
|
|
|
|
// ToOutbound converts a Message to an OutboundMessage for transport.
|
|
// Transport-only fields (References, InReplyTo, Headers, AltContent) must be set by caller.
|
|
func (m *Message) ToOutbound() OutboundMessage {
|
|
return OutboundMessage{
|
|
UUID: m.UUID,
|
|
ConversationUUID: m.ConversationUUID,
|
|
SenderID: m.SenderID,
|
|
MessageReceiverID: m.MessageReceiverID,
|
|
Content: m.Content,
|
|
TextContent: m.TextContent,
|
|
ContentType: m.ContentType,
|
|
From: m.From,
|
|
To: m.To,
|
|
CC: m.CC,
|
|
BCC: m.BCC,
|
|
Subject: m.Subject,
|
|
SourceID: m.SourceID.String,
|
|
Attachments: m.Attachments,
|
|
Meta: m.Meta,
|
|
CreatedAt: m.CreatedAt,
|
|
}
|
|
}
|
|
|
|
type IncomingContact struct {
|
|
ID int
|
|
FirstName string
|
|
LastName string
|
|
Email null.String
|
|
}
|
|
|
|
type IncomingMessage struct {
|
|
// Channel context
|
|
Channel string
|
|
InboxID int
|
|
|
|
// Contact
|
|
Contact IncomingContact
|
|
|
|
// Message fields
|
|
Subject string
|
|
SourceID null.String
|
|
Content string
|
|
ContentType string
|
|
Meta json.RawMessage
|
|
Attachments attachment.Attachments
|
|
|
|
// Email threading
|
|
ConversationUUIDFromReplyTo string // UUID extracted from plus-addressed recipient (inbox+conv-{uuid}@domain)
|
|
InReplyTo string
|
|
References []string
|
|
}
|
|
|
|
// ToMessage converts IncomingMessage to a Message for DB insertion.
|
|
func (in *IncomingMessage) ToMessage(senderID, conversationID int, conversationUUID string) Message {
|
|
return Message{
|
|
Channel: in.Channel,
|
|
SenderType: SenderTypeContact,
|
|
Type: MessageIncoming,
|
|
Status: MessageStatusReceived,
|
|
InboxID: in.InboxID,
|
|
Subject: in.Subject,
|
|
SourceID: in.SourceID,
|
|
Content: in.Content,
|
|
ContentType: in.ContentType,
|
|
Meta: in.Meta,
|
|
Attachments: in.Attachments,
|
|
SenderID: senderID,
|
|
ConversationID: conversationID,
|
|
ConversationUUID: conversationUUID,
|
|
}
|
|
}
|
|
|
|
type Status struct {
|
|
ID int `db:"id" json:"id"`
|
|
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
|
Name string `db:"name" json:"name"`
|
|
}
|
|
|
|
type Priority struct {
|
|
ID int `db:"id" json:"id"`
|
|
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
|
Name string `db:"name" json:"name"`
|
|
}
|
|
|
|
// ConversationDraft represents a draft reply for a conversation.
|
|
type ConversationDraft struct {
|
|
ID int64 `db:"id" json:"id"`
|
|
ConversationID int64 `db:"conversation_id" json:"conversation_id"`
|
|
ConversationUUID string `db:"conversation_uuid" json:"conversation_uuid"`
|
|
UserID int64 `db:"user_id" json:"user_id"`
|
|
Content string `db:"content" json:"content"`
|
|
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
|
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
|
|
Meta json.RawMessage `db:"meta" json:"meta"`
|
|
}
|
|
|
|
// MentionInput represents a mention in a private note from frontend.
|
|
type MentionInput struct {
|
|
Type string `json:"type"` // "agent" or "team"
|
|
ID int `json:"id"`
|
|
}
|