From ccc5940dd9d99ed9fadba25b1cb65d51e9ed1a78 Mon Sep 17 00:00:00 2001 From: Abhinav Raut Date: Sun, 6 Jul 2025 19:51:44 +0530 Subject: [PATCH] return created message in message fetch API --- cmd/conversation.go | 2 +- cmd/messages.go | 14 +++--- internal/conversation/conversation.go | 21 +++++++-- internal/conversation/message.go | 63 +++++++++++++++++---------- internal/conversation/queries.sql | 4 +- 5 files changed, 67 insertions(+), 37 deletions(-) diff --git a/cmd/conversation.go b/cmd/conversation.go index dbd04e1..2962efa 100644 --- a/cmd/conversation.go +++ b/cmd/conversation.go @@ -744,7 +744,7 @@ func handleCreateConversation(r *fastglue.Request) error { } // Send reply to the created conversation. - if err := app.conversation.SendReply(media, req.InboxID, auser.ID /**sender_id**/, conversationUUID, req.Content, to, nil /**cc**/, nil /**bcc**/, map[string]any{} /**meta**/); err != nil { + if _, err := app.conversation.SendReply(media, req.InboxID, auser.ID /**sender_id**/, conversationUUID, req.Content, to, nil /**cc**/, nil /**bcc**/, map[string]any{} /**meta**/); err != nil { // Delete the conversation if reply fails. if err := app.conversation.DeleteConversation(conversationUUID); err != nil { app.lo.Error("error deleting conversation", "error", err) diff --git a/cmd/messages.go b/cmd/messages.go index b9015bd..7ce1633 100644 --- a/cmd/messages.go +++ b/cmd/messages.go @@ -162,13 +162,15 @@ func handleSendMessage(r *fastglue.Request) error { } if req.Private { - if err := app.conversation.SendPrivateNote(media, user.ID, cuuid, req.Message); err != nil { - return sendErrorEnvelope(r, err) - } - } else { - if err := app.conversation.SendReply(media, conv.InboxID, user.ID, cuuid, req.Message, req.To, req.CC, req.BCC, map[string]any{} /**meta**/); err != nil { + message, err := app.conversation.SendPrivateNote(media, user.ID, cuuid, req.Message) + if err != nil { return sendErrorEnvelope(r, err) } + return r.SendEnvelope(message) } - return r.SendEnvelope(true) + message, err := app.conversation.SendReply(media, conv.InboxID, user.ID, cuuid, req.Message, req.To, req.CC, req.BCC, map[string]any{} /**meta**/) + if err != nil { + return sendErrorEnvelope(r, err) + } + return r.SendEnvelope(message) } diff --git a/internal/conversation/conversation.go b/internal/conversation/conversation.go index ee20cbb..d6dcad0 100644 --- a/internal/conversation/conversation.go +++ b/internal/conversation/conversation.go @@ -920,14 +920,17 @@ func (m *Manager) ApplyAction(action amodels.RuleAction, conv models.Conversatio } return m.UpdateConversationStatus(conv.UUID, statusID, "", "", user) case amodels.ActionSendPrivateNote: - return m.SendPrivateNote([]mmodels.Media{}, user.ID, conv.UUID, action.Value[0]) + _, err := m.SendPrivateNote([]mmodels.Media{}, user.ID, conv.UUID, action.Value[0]) + if err != nil { + return fmt.Errorf("sending private note: %w", err) + } case amodels.ActionReply: // Make recipient list. to, cc, bcc, err := m.makeRecipients(conv.ID, conv.Contact.Email.String, conv.InboxMail) if err != nil { return fmt.Errorf("making recipients for reply action: %w", err) } - return m.SendReply( + _, err = m.SendReply( []mmodels.Media{}, conv.InboxID, user.ID, @@ -938,6 +941,9 @@ func (m *Manager) ApplyAction(action amodels.RuleAction, conv models.Conversatio bcc, map[string]any{}, /**meta**/ ) + if err != nil { + return fmt.Errorf("sending reply: %w", err) + } case amodels.ActionSetSLA: slaID, err := strconv.Atoi(action.Value[0]) if err != nil { @@ -951,6 +957,7 @@ func (m *Manager) ApplyAction(action amodels.RuleAction, conv models.Conversatio default: return fmt.Errorf("unknown action: %s", action.Type) } + return nil } // RemoveConversationAssignee removes the assignee from the conversation. @@ -991,10 +998,16 @@ func (m *Manager) SendCSATReply(actorUserID int, conversation models.Conversatio // Make recipient list. to, cc, bcc, err := m.makeRecipients(conversation.ID, conversation.Contact.Email.String, conversation.InboxMail) if err != nil { - return fmt.Errorf("making recipients for CSAT reply: %w", err) + return envelope.NewError(envelope.GeneralError, m.i18n.Ts("globals.messages.errorCreating", "name", "{globals.terms.csat}"), nil) } - return m.SendReply(nil /**media**/, conversation.InboxID, actorUserID, conversation.UUID, message, to, cc, bcc, meta) + // Send CSAT reply. + _, err = m.SendReply(nil /**media**/, conversation.InboxID, actorUserID, conversation.UUID, message, to, cc, bcc, meta) + if err != nil { + m.lo.Error("error sending CSAT reply", "conversation_uuid", conversation.UUID, "error", err) + return envelope.NewError(envelope.GeneralError, m.i18n.Ts("globals.messages.errorCreating", "name", "{globals.terms.csat}"), nil) + } + return nil } // DeleteConversation deletes a conversation. diff --git a/internal/conversation/message.go b/internal/conversation/message.go index 5dfb385..afb84d8 100644 --- a/internal/conversation/message.go +++ b/internal/conversation/message.go @@ -356,8 +356,7 @@ func (m *Manager) MarkMessageAsPending(uuid string) error { } // SendPrivateNote inserts a private message in a conversation. -func (m *Manager) SendPrivateNote(media []mmodels.Media, senderID int, conversationUUID, content string) error { - // Insert Message. +func (m *Manager) SendPrivateNote(media []mmodels.Media, senderID int, conversationUUID, content string) (models.Message, error) { message := models.Message{ ConversationUUID: conversationUUID, SenderID: senderID, @@ -369,18 +368,25 @@ func (m *Manager) SendPrivateNote(media []mmodels.Media, senderID int, conversat Private: true, Media: media, } - return m.InsertMessage(&message) + if err := m.InsertMessage(&message); err != nil { + return models.Message{}, err + } + return message, nil } // SendReply inserts a reply message in a conversation. -func (m *Manager) SendReply(media []mmodels.Media, inboxID, senderID int, conversationUUID, content string, to, cc, bcc []string, meta map[string]interface{}) error { - // Save to, cc and bcc in meta. +func (m *Manager) SendReply(media []mmodels.Media, inboxID, senderID int, conversationUUID, content string, to, cc, bcc []string, meta map[string]interface{}) (models.Message, error) { + var ( + message = models.Message{} + ) + + // Clear empty fields in to, cc, bcc. to = stringutil.RemoveEmpty(to) cc = stringutil.RemoveEmpty(cc) bcc = stringutil.RemoveEmpty(bcc) if len(to) == 0 { - return envelope.NewError(envelope.GeneralError, m.i18n.Ts("globals.messages.empty", "name", "`to`"), nil) + return message, envelope.NewError(envelope.GeneralError, m.i18n.Ts("globals.messages.empty", "name", "`to`"), nil) } meta["to"] = to @@ -393,22 +399,22 @@ func (m *Manager) SendReply(media []mmodels.Media, inboxID, senderID int, conver metaJSON, err := json.Marshal(meta) if err != nil { - return envelope.NewError(envelope.GeneralError, m.i18n.Ts("globals.messages.errorMarshalling", "name", "{globals.terms.meta}"), nil) + return message, envelope.NewError(envelope.GeneralError, m.i18n.Ts("globals.messages.errorMarshalling", "name", "{globals.terms.meta}"), nil) } // Generage unique source ID i.e. message-id for email. inbox, err := m.inboxStore.GetDBRecord(inboxID) if err != nil { - return err + return message, err } sourceID, err := stringutil.GenerateEmailMessageID(conversationUUID, inbox.From) if err != nil { m.lo.Error("error generating source message id", "error", err) - return envelope.NewError(envelope.GeneralError, m.i18n.T("conversation.errorGeneratingMessageID"), nil) + return message, envelope.NewError(envelope.GeneralError, m.i18n.T("conversation.errorGeneratingMessageID"), nil) } // Insert Message. - message := models.Message{ + message = models.Message{ ConversationUUID: conversationUUID, SenderID: senderID, Type: models.MessageOutgoing, @@ -421,16 +427,17 @@ func (m *Manager) SendReply(media []mmodels.Media, inboxID, senderID int, conver Meta: metaJSON, SourceID: null.StringFrom(sourceID), } - return m.InsertMessage(&message) + if err := m.InsertMessage(&message); err != nil { + return models.Message{}, err + } + return message, nil } // InsertMessage inserts a message and attaches the media to the message. func (m *Manager) InsertMessage(message *models.Message) error { - // Private message is always sent. if message.Private { message.Status = models.MessageStatusSent } - if len(message.Meta) == 0 || string(message.Meta) == "null" { message.Meta = json.RawMessage(`{}`) } @@ -438,14 +445,16 @@ func (m *Manager) InsertMessage(message *models.Message) error { // Convert HTML content to text for search. message.TextContent = stringutil.HTML2Text(message.Content) - // Insert Message. - if err := m.q.InsertMessage.QueryRow(message.Type, message.Status, message.ConversationID, message.ConversationUUID, message.Content, message.TextContent, message.SenderID, message.SenderType, - message.Private, message.ContentType, message.SourceID, message.Meta).Scan(&message.ID, &message.UUID, &message.CreatedAt); err != nil { + // Insert and scan the message into the struct. + if err := m.q.InsertMessage.Get(message, + message.Type, message.Status, message.ConversationID, message.ConversationUUID, + message.Content, message.TextContent, message.SenderID, message.SenderType, + message.Private, message.ContentType, message.SourceID, message.Meta); err != nil { m.lo.Error("error inserting message in db", "error", err) return envelope.NewError(envelope.GeneralError, m.i18n.Ts("globals.messages.errorInserting", "name", "{globals.terms.message}"), nil) } - // Attach message to the media. + // Attach just inserted message to the media. for _, media := range message.Media { m.mediaStore.Attach(media.ID, mmodels.ModelMessages, message.ID) } @@ -465,14 +474,20 @@ func (m *Manager) InsertMessage(message *models.Message) error { // Broadcast new message. m.BroadcastNewMessage(message) - // Refetch message and send webhook event for message created. - updatedMessage, err := m.GetMessage(message.UUID) - if err != nil { - m.lo.Error("error fetching updated message for webhook event", "uuid", message.UUID, "error", err) - } else { - m.webhookStore.TriggerEvent(wmodels.EventMessageCreated, updatedMessage) + // Refetch message if this message has media attachments, as media gets linked after inserting the message. + if len(message.Media) > 0 { + refetchedMessage, err := m.GetMessage(message.UUID) + if err != nil { + m.lo.Error("error fetching message after insert", "error", err) + } else { + // Replace the message in the struct with the refetched message. + *message = refetchedMessage + } } + // Trigger webhook for new message created. + m.webhookStore.TriggerEvent(wmodels.EventMessageCreated, message) + return nil } @@ -530,7 +545,7 @@ func (m *Manager) InsertConversationActivity(activityType, conversationUUID, new content, err := m.getMessageActivityContent(activityType, newValue, actor.FullName()) if err != nil { m.lo.Error("error could not generate activity content", "error", err) - return err + return envelope.NewError(envelope.GeneralError, m.i18n.Ts("globals.messages.errorGenerating", "name", "{globals.terms.activityMessage}"), nil) } message := models.Message{ diff --git a/internal/conversation/queries.sql b/internal/conversation/queries.sql index 863e6bf..80d0617 100644 --- a/internal/conversation/queries.sql +++ b/internal/conversation/queries.sql @@ -509,9 +509,9 @@ inserted_msg AS ( $1, $2, (SELECT id FROM conversation_id), $5, $6, $7, $8, $9, $10, $11, $12 ) - RETURNING id, uuid, created_at, conversation_id + RETURNING * ) -SELECT id, uuid, created_at FROM inserted_msg; +SELECT * FROM inserted_msg; -- name: message-exists-by-source-id SELECT conversation_id