mirror of
https://github.com/abhinavxd/libredesk.git
synced 2026-09-22 10:33:24 +00:00
43ebed1c3a
Targets broadcasts via per-tab list and open subs (validated at subscribe time) instead of broadcasting message uuid to all agents. Adds frontend list typing, throttled sound, and many perf improvements to reduce cpu memory & database queries. Other fixes: Enforce message-to-conversation binding in handleGetMessage
312 lines
10 KiB
Go
312 lines
10 KiB
Go
package main
|
|
|
|
import (
|
|
"strings"
|
|
|
|
amodels "github.com/abhinavxd/libredesk/internal/auth/models"
|
|
authzModels "github.com/abhinavxd/libredesk/internal/authz/models"
|
|
cmodels "github.com/abhinavxd/libredesk/internal/conversation/models"
|
|
"github.com/abhinavxd/libredesk/internal/envelope"
|
|
umodels "github.com/abhinavxd/libredesk/internal/user/models"
|
|
"github.com/valyala/fasthttp"
|
|
"github.com/zerodha/fastglue"
|
|
)
|
|
|
|
type messageReq struct {
|
|
Attachments []int `json:"attachments"`
|
|
Message string `json:"message"`
|
|
Private bool `json:"private"`
|
|
To []string `json:"to"`
|
|
CC []string `json:"cc"`
|
|
BCC []string `json:"bcc"`
|
|
SenderType string `json:"sender_type"`
|
|
Mentions []cmodels.MentionInput `json:"mentions"`
|
|
EchoID string `json:"echo_id"`
|
|
}
|
|
|
|
// handleGetMessages returns messages for a conversation.
|
|
func handleGetMessages(r *fastglue.Request) error {
|
|
var (
|
|
app = r.Context.(*App)
|
|
uuid = r.RequestCtx.UserValue("uuid").(string)
|
|
auser = r.RequestCtx.UserValue("user").(amodels.User)
|
|
total = 0
|
|
private *bool
|
|
)
|
|
page, pageSize := getPagination(r)
|
|
|
|
// Parse optional private filter (null = no filter)
|
|
if r.RequestCtx.QueryArgs().Has("private") {
|
|
p := r.RequestCtx.QueryArgs().GetBool("private")
|
|
private = &p
|
|
}
|
|
|
|
// Parse repeated type params: ?type=incoming&type=outgoing
|
|
var msgTypes []string
|
|
for _, v := range r.RequestCtx.QueryArgs().PeekMulti("type") {
|
|
msgTypes = append(msgTypes, string(v))
|
|
}
|
|
|
|
user, err := app.user.GetAgentCachedOrLoad(auser.ID)
|
|
if err != nil {
|
|
return sendErrorEnvelope(r, err)
|
|
}
|
|
|
|
// Check permission
|
|
_, err = enforceConversationAccess(app, uuid, user)
|
|
if err != nil {
|
|
return sendErrorEnvelope(r, err)
|
|
}
|
|
|
|
messages, pageSize, err := app.conversation.GetConversationMessages(uuid, page, pageSize, private, msgTypes)
|
|
if err != nil {
|
|
return sendErrorEnvelope(r, err)
|
|
}
|
|
|
|
rootURL, _ := app.setting.GetAppRootURL()
|
|
for i := range messages {
|
|
total = messages[i].Total
|
|
// Populate attachment URLs
|
|
for j := range messages[i].Attachments {
|
|
att := messages[i].Attachments[j]
|
|
messages[i].Attachments[j].URL = app.media.GetURL(att.UUID, att.ContentType, att.Name)
|
|
}
|
|
resolveQuotedCIDs(app, &messages[i])
|
|
resolveAttachmentCIDs(&messages[i], rootURL)
|
|
}
|
|
|
|
// Process CSAT status for all messages (will only affect CSAT messages)
|
|
app.conversation.ProcessCSATStatus(messages)
|
|
|
|
// Strip CSAT UUID from agent sessions to prevent self-rating.
|
|
if r.RequestCtx.UserValue("auth_method") != "api_key" {
|
|
for i := range messages {
|
|
if messages[i].HasCSAT() {
|
|
messages[i].StripCSATUUID()
|
|
}
|
|
}
|
|
}
|
|
|
|
return r.SendEnvelope(envelope.PageResults{
|
|
Total: total,
|
|
Results: messages,
|
|
Page: page,
|
|
PerPage: pageSize,
|
|
TotalPages: (total + pageSize - 1) / pageSize,
|
|
})
|
|
}
|
|
|
|
// handleGetMessage fetches a single from DB using the uuid.
|
|
func handleGetMessage(r *fastglue.Request) error {
|
|
var (
|
|
app = r.Context.(*App)
|
|
uuid = r.RequestCtx.UserValue("uuid").(string)
|
|
cuuid = r.RequestCtx.UserValue("cuuid").(string)
|
|
auser = r.RequestCtx.UserValue("user").(amodels.User)
|
|
)
|
|
user, err := app.user.GetAgentCachedOrLoad(auser.ID)
|
|
if err != nil {
|
|
return sendErrorEnvelope(r, err)
|
|
}
|
|
|
|
// Check permission
|
|
_, err = enforceConversationAccess(app, cuuid, user)
|
|
if err != nil {
|
|
return sendErrorEnvelope(r, err)
|
|
}
|
|
|
|
message, err := app.conversation.GetMessage(uuid)
|
|
if err != nil {
|
|
return sendErrorEnvelope(r, err)
|
|
}
|
|
|
|
if message.ConversationUUID != cuuid {
|
|
return sendErrorEnvelope(r, envelope.NewError(envelope.PermissionError, "Permission denied", nil))
|
|
}
|
|
|
|
// Process CSAT status for the message (will only affect CSAT messages)
|
|
messages := []cmodels.Message{message}
|
|
app.conversation.ProcessCSATStatus(messages)
|
|
message = messages[0]
|
|
|
|
// Strip CSAT UUID from agent sessions to prevent self-rating.
|
|
if r.RequestCtx.UserValue("auth_method") != "api_key" && message.HasCSAT() {
|
|
message.StripCSATUUID()
|
|
}
|
|
|
|
rootURL, _ := app.setting.GetAppRootURL()
|
|
for j := range message.Attachments {
|
|
att := message.Attachments[j]
|
|
message.Attachments[j].URL = app.media.GetURL(att.UUID, att.ContentType, att.Name)
|
|
}
|
|
resolveQuotedCIDs(app, &message)
|
|
resolveAttachmentCIDs(&message, rootURL)
|
|
|
|
return r.SendEnvelope(message)
|
|
}
|
|
|
|
// handleRetryMessage changes message status to `pending`, so it's enqueued for sending.
|
|
func handleRetryMessage(r *fastglue.Request) error {
|
|
var (
|
|
app = r.Context.(*App)
|
|
uuid = r.RequestCtx.UserValue("uuid").(string)
|
|
cuuid = r.RequestCtx.UserValue("cuuid").(string)
|
|
auser = r.RequestCtx.UserValue("user").(amodels.User)
|
|
)
|
|
|
|
user, err := app.user.GetAgentCachedOrLoad(auser.ID)
|
|
if err != nil {
|
|
return sendErrorEnvelope(r, err)
|
|
}
|
|
|
|
// Check permission
|
|
_, err = enforceConversationAccess(app, cuuid, user)
|
|
if err != nil {
|
|
return sendErrorEnvelope(r, err)
|
|
}
|
|
|
|
// Only outgoing agent messages that have failed can be retried.
|
|
msg, err := app.conversation.GetMessage(uuid)
|
|
if err != nil {
|
|
return sendErrorEnvelope(r, err)
|
|
}
|
|
if msg.SenderType != cmodels.SenderTypeAgent || msg.Status != cmodels.MessageStatusFailed || msg.SenderID != user.ID || msg.ConversationUUID != cuuid {
|
|
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.T("globals.messages.badRequest"), nil, envelope.InputError)
|
|
}
|
|
|
|
if err = app.conversation.MarkMessageAsPending(uuid); err != nil {
|
|
return sendErrorEnvelope(r, err)
|
|
}
|
|
return r.SendEnvelope(true)
|
|
}
|
|
|
|
// handleSendMessage sends a message in a conversation.
|
|
func handleSendMessage(r *fastglue.Request) error {
|
|
var (
|
|
app = r.Context.(*App)
|
|
auser = r.RequestCtx.UserValue("user").(amodels.User)
|
|
cuuid = r.RequestCtx.UserValue("cuuid").(string)
|
|
req = messageReq{}
|
|
)
|
|
|
|
user, err := app.user.GetAgentCachedOrLoad(auser.ID)
|
|
if err != nil {
|
|
return sendErrorEnvelope(r, err)
|
|
}
|
|
|
|
// Check access to conversation.
|
|
conv, err := enforceConversationAccess(app, cuuid, user)
|
|
if err != nil {
|
|
return sendErrorEnvelope(r, err)
|
|
}
|
|
|
|
if err := r.Decode(&req, "json"); err != nil {
|
|
app.lo.Error("error unmarshalling message request", "error", err)
|
|
return r.SendErrorEnvelope(fasthttp.StatusInternalServerError, app.i18n.T("errors.parsingRequest"), nil, envelope.InputError)
|
|
}
|
|
|
|
// Make sure the inbox is enabled.
|
|
inbox, err := app.inbox.GetDBRecord(conv.InboxID)
|
|
if err != nil {
|
|
return sendErrorEnvelope(r, err)
|
|
}
|
|
if !inbox.Enabled {
|
|
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.T("status.disabledInbox"), nil, envelope.InputError)
|
|
}
|
|
|
|
if req.SenderType != umodels.UserTypeAgent && req.SenderType != umodels.UserTypeContact {
|
|
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.T("globals.messages.somethingWentWrong"), nil, envelope.InputError)
|
|
}
|
|
|
|
// Contacts cannot send private messages
|
|
if req.SenderType == umodels.UserTypeContact && req.Private {
|
|
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.T("globals.messages.badRequest"), nil, envelope.InputError)
|
|
}
|
|
|
|
// Check if user has permission to send messages as contact
|
|
if req.SenderType == umodels.UserTypeContact {
|
|
parts := strings.Split(authzModels.PermMessagesWriteAsContact, ":")
|
|
if len(parts) != 2 {
|
|
app.lo.Error("error parsing permission string", "permission", authzModels.PermMessagesWriteAsContact)
|
|
return sendErrorEnvelope(r, envelope.NewError(envelope.InputError, app.i18n.T("globals.messages.somethingWentWrong"), nil))
|
|
}
|
|
ok, err := app.authz.Enforce(user, parts[0], parts[1])
|
|
if err != nil {
|
|
app.lo.Error("error checking permission", "error", err)
|
|
return sendErrorEnvelope(r, envelope.NewError(envelope.InputError, app.i18n.T("globals.messages.somethingWentWrong"), nil))
|
|
}
|
|
if !ok {
|
|
return r.SendErrorEnvelope(fasthttp.StatusForbidden, app.i18n.T("status.deniedPermission"), nil, envelope.PermissionError)
|
|
}
|
|
}
|
|
|
|
// Get media for all attachments, skip any already associated with a model.
|
|
media, err := getUnassociatedMedia(app, req.Attachments)
|
|
if err != nil {
|
|
return r.SendErrorEnvelope(fasthttp.StatusInternalServerError, app.i18n.T("globals.messages.somethingWentWrong"), nil, envelope.GeneralError)
|
|
}
|
|
|
|
rootURL, _ := app.setting.GetAppRootURL()
|
|
|
|
// Create contact message.
|
|
if req.SenderType == umodels.UserTypeContact {
|
|
message, err := app.conversation.CreateContactMessage(media, int(conv.ContactID), cuuid, req.Message, cmodels.ContentTypeHTML, false)
|
|
if err != nil {
|
|
return sendErrorEnvelope(r, err)
|
|
}
|
|
resolveQuotedCIDs(app, &message)
|
|
resolveAttachmentCIDs(&message, rootURL)
|
|
return r.SendEnvelope(message)
|
|
}
|
|
|
|
// Send private note.
|
|
if req.Private {
|
|
message, err := app.conversation.SendPrivateNote(media, user.ID, cuuid, req.Message, req.Mentions)
|
|
if err != nil {
|
|
return sendErrorEnvelope(r, err)
|
|
}
|
|
resolveAttachmentCIDs(&message, rootURL)
|
|
return r.SendEnvelope(message)
|
|
}
|
|
|
|
// Queue outgoing reply.
|
|
meta := map[string]any{}
|
|
if req.EchoID != "" {
|
|
meta["echo_id"] = req.EchoID
|
|
}
|
|
message, err := app.conversation.QueueReply(media, conv.InboxID, user.ID, conv.ContactID, cuuid, req.Message, req.To, req.CC, req.BCC, meta)
|
|
if err != nil {
|
|
return sendErrorEnvelope(r, err)
|
|
}
|
|
resolveQuotedCIDs(app, &message)
|
|
resolveAttachmentCIDs(&message, rootURL)
|
|
return r.SendEnvelope(message)
|
|
}
|
|
|
|
// resolveAttachmentCIDs replaces inline image cid: references in email message content
|
|
// with actual attachment URLs and resolves relative /uploads/ paths to absolute URLs.
|
|
func resolveAttachmentCIDs(msg *cmodels.Message, rootURL string) {
|
|
for _, att := range msg.Attachments {
|
|
if att.ContentID != "" && att.URL != "" {
|
|
msg.Content = strings.ReplaceAll(msg.Content, "cid:"+att.ContentID, att.URL)
|
|
}
|
|
}
|
|
if rootURL != "" {
|
|
msg.Content = strings.ReplaceAll(msg.Content, `src="/uploads/`, `src="`+rootURL+`/uploads/`)
|
|
msg.Content = strings.ReplaceAll(msg.Content, `src='/uploads/`, `src='`+rootURL+`/uploads/`)
|
|
}
|
|
}
|
|
|
|
// resolveQuotedCIDs replaces cid: refs to media on other messages with signed URLs.
|
|
func resolveQuotedCIDs(app *App, msg *cmodels.Message) {
|
|
refs, err := app.conversation.GetInlineMediaRefs(msg)
|
|
if err != nil {
|
|
app.lo.Error("error fetching inline media refs", "conversation_uuid", msg.ConversationUUID, "error", err)
|
|
return
|
|
}
|
|
for _, ref := range refs {
|
|
url := app.media.GetURL(ref.UUID, ref.ContentType, ref.Filename)
|
|
msg.Content = strings.ReplaceAll(msg.Content, "cid:"+ref.ContentID, url)
|
|
}
|
|
}
|