mirror of
https://github.com/abhinavxd/libredesk.git
synced 2025-11-02 04:53:41 +00:00
- fix: Inline images present in email quote replies previously not visible, now show up correctly, the media does not get uploaded again instead the existing media url is replaced with the cid url.
- fix: content id check for attachments, as content id is not globally unqiue. - fix: send missing websocket updates to the fronend on conversation status update. - refactor: combine get media by id and uuid into a singlequery
This commit is contained in:
@@ -156,7 +156,7 @@ func handleServeMedia(r *fastglue.Request) error {
|
||||
}
|
||||
|
||||
// Fetch media from DB.
|
||||
media, err := app.media.GetByUUID(strings.TrimPrefix(uuid, thumbPrefix))
|
||||
media, err := app.media.Get(0, strings.TrimPrefix(uuid, thumbPrefix))
|
||||
if err != nil {
|
||||
return sendErrorEnvelope(r, err)
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
<!-- Message Text -->
|
||||
<Letter
|
||||
:html="sanitizedMessageContent"
|
||||
:allowedSchemas="['cid', 'https', 'http']"
|
||||
:allowedSchemas="['cid', 'https', 'http', 'mailto']"
|
||||
class="mb-1 native-html"
|
||||
:class="{ 'mb-3': message.attachments.length > 0 }"
|
||||
/>
|
||||
@@ -72,6 +72,7 @@ import { useConversationStore } from '@/stores/conversation'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
|
||||
import { Letter } from 'vue-letter'
|
||||
import { useAppSettingsStore } from '@/stores/appSettings'
|
||||
import MessageAttachmentPreview from '@/features/conversation/message/attachment/MessageAttachmentPreview.vue'
|
||||
|
||||
const props = defineProps({
|
||||
@@ -79,18 +80,26 @@ const props = defineProps({
|
||||
})
|
||||
|
||||
const convStore = useConversationStore()
|
||||
const settingsStore = useAppSettingsStore()
|
||||
const showQuotedText = ref(false)
|
||||
|
||||
const getAvatar = computed(() => {
|
||||
return convStore.current?.contact?.avatar_url || ''
|
||||
})
|
||||
|
||||
const sanitizedMessageContent = computed(() => {
|
||||
const content = props.message.content || ''
|
||||
return props.message.attachments.reduce(
|
||||
let content = props.message.content || ''
|
||||
const baseUrl = settingsStore.settings['app.root_url']
|
||||
|
||||
// Replace CID with URL for inline attachments from the message.
|
||||
content = props.message.attachments.reduce(
|
||||
(acc, { content_id, url }) => acc.replace(new RegExp(`cid:${content_id}`, 'g'), url),
|
||||
content
|
||||
)
|
||||
|
||||
// Add base URL to all img src starting with /uploads/ as vue-letter does not allow relative URLs.
|
||||
content = content.replace(/src="\/uploads\//g, `src="${baseUrl}/uploads/`)
|
||||
|
||||
return content
|
||||
})
|
||||
|
||||
const hasQuotedContent = computed(() => sanitizedMessageContent.value.includes('<blockquote'))
|
||||
|
||||
@@ -47,14 +47,14 @@ export const isGoHourMinuteDuration = (value) => {
|
||||
}
|
||||
|
||||
const template = document.createElement('template')
|
||||
export function getTextFromHTML(htmlString) {
|
||||
try {
|
||||
template.innerHTML = htmlString
|
||||
const text = template.content.textContent || template.content.innerText || ''
|
||||
template.innerHTML = ''
|
||||
return text.trim()
|
||||
} catch (error) {
|
||||
console.error('Error converting HTML to text:', error)
|
||||
return ''
|
||||
}
|
||||
export function getTextFromHTML (htmlString) {
|
||||
try {
|
||||
template.innerHTML = htmlString
|
||||
const text = template.content.textContent || template.content.innerText || ''
|
||||
template.innerHTML = ''
|
||||
return text.trim()
|
||||
} catch (error) {
|
||||
console.error('Error converting HTML to text:', error)
|
||||
return ''
|
||||
}
|
||||
}
|
||||
@@ -106,7 +106,7 @@ type mediaStore interface {
|
||||
GetBlob(name string) ([]byte, error)
|
||||
Attach(id int, model string, modelID int) error
|
||||
GetByModel(id int, model string) ([]mmodels.Media, error)
|
||||
ContentIDExists(contentID string) (bool, error)
|
||||
ContentIDExists(contentID string) (bool, string, error)
|
||||
Upload(fileName, contentType string, content io.ReadSeeker) (string, error)
|
||||
UploadAndInsert(fileName, contentType, contentID string, modelType null.String, modelID null.Int, content io.ReadSeeker, fileSize int, disposition null.String, meta []byte) (mmodels.Media, error)
|
||||
}
|
||||
@@ -476,6 +476,12 @@ func (c *Manager) UpdateConversationTeamAssignee(uuid string, teamID int, actor
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Fetch the conversation again to get the updated assignee details.
|
||||
conversation, err := c.GetConversation(0, uuid)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
if err := c.ApplySLA(conversation, team.SLAPolicyID.Int, systemUser); err != nil {
|
||||
return nil
|
||||
}
|
||||
@@ -566,8 +572,15 @@ func (c *Manager) UpdateConversationStatus(uuid string, statusID int, status, sn
|
||||
return envelope.NewError(envelope.GeneralError, "Error recording status change", nil)
|
||||
}
|
||||
|
||||
// Broadcast update using WS
|
||||
// Broadcast updates using websocket.
|
||||
c.BroadcastConversationUpdate(uuid, "status", status)
|
||||
if status == models.StatusResolved {
|
||||
c.BroadcastConversationUpdate(uuid, "resolved_at", time.Now().Format(time.RFC3339))
|
||||
}
|
||||
if status == models.StatusClosed {
|
||||
c.BroadcastConversationUpdate(uuid, "closed_at", time.Now().Format(time.RFC3339))
|
||||
c.BroadcastConversationUpdate(uuid, "resolved_at", time.Now().Format(time.RFC3339))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -570,22 +570,25 @@ func (m *Manager) processIncomingMessage(in models.IncomingMessage) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// Evaluate automation rules for this conversation.
|
||||
// Evaluate automation rules for new conversation.
|
||||
if isNewConversation {
|
||||
m.automation.EvaluateNewConversationRules(in.Message.ConversationUUID)
|
||||
} else {
|
||||
m.automation.EvaluateConversationUpdateRules(in.Message.ConversationUUID, amodels.EventConversationMessageIncoming)
|
||||
// Reopen conversation if it's closed, snoozed, or resolved.
|
||||
systemUser, err := m.userStore.GetSystemUser()
|
||||
if err != nil {
|
||||
m.lo.Error("error fetching system user", "error", err)
|
||||
return fmt.Errorf("error fetching system user for reopening conversation: %w", err)
|
||||
}
|
||||
if err := m.ReOpenConversation(in.Message.ConversationUUID, systemUser); err != nil {
|
||||
m.lo.Error("error reopening conversation", "error", err)
|
||||
return fmt.Errorf("error reopening conversation: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Reopen conversation if it's not Open.
|
||||
systemUser, err := m.userStore.GetSystemUser()
|
||||
if err != nil {
|
||||
m.lo.Error("error fetching system user", "error", err)
|
||||
return fmt.Errorf("error fetching system user for reopening conversation: %w", err)
|
||||
}
|
||||
if err := m.ReOpenConversation(in.Message.ConversationUUID, systemUser); err != nil {
|
||||
m.lo.Error("error reopening conversation", "error", err)
|
||||
return fmt.Errorf("error reopening conversation: %w", err)
|
||||
}
|
||||
|
||||
// Trigger automations on incoming message event.
|
||||
m.automation.EvaluateConversationUpdateRules(in.Message.ConversationUUID, amodels.EventConversationMessageIncoming)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -655,7 +658,7 @@ func (c *Manager) generateMessagesQuery(baseQuery string, qArgs []interface{}, p
|
||||
return sqlQuery, pageSize, qArgs, nil
|
||||
}
|
||||
|
||||
// uploadMessageAttachments uploads attachments for a message.
|
||||
// uploadMessageAttachments uploads all attachments for a message.
|
||||
func (m *Manager) uploadMessageAttachments(message *models.Message) []error {
|
||||
if len(message.Attachments) == 0 {
|
||||
return nil
|
||||
@@ -663,28 +666,42 @@ func (m *Manager) uploadMessageAttachments(message *models.Message) []error {
|
||||
|
||||
var uploadErr []error
|
||||
for _, attachment := range message.Attachments {
|
||||
// Check if this attachment already exists by the content ID.
|
||||
if attachment.ContentID != "" {
|
||||
exists, err := m.mediaStore.ContentIDExists(attachment.ContentID)
|
||||
// Check if this attachment already exists by the content ID, as inline images can be repeated across conversations.
|
||||
contentID := attachment.ContentID
|
||||
if contentID != "" {
|
||||
// Make content ID MORE unique by prefixing it with the conversation UUID, as content id is not globally unique practically,
|
||||
// different messages can have the same content ID, I do not have the message ID at this point, so I am using sticking with the conversation UUID
|
||||
// to make it more unique.
|
||||
contentID = message.ConversationUUID + "_" + contentID
|
||||
|
||||
exists, uuid, err := m.mediaStore.ContentIDExists(contentID)
|
||||
if err != nil {
|
||||
m.lo.Error("error checking media existence by content ID", "content_id", attachment.ContentID, "error", err)
|
||||
continue
|
||||
m.lo.Error("error checking media existence by content ID", "content_id", contentID, "error", err)
|
||||
}
|
||||
|
||||
// This attachment already exists, replace the cid:content_id with the media relative url, not using absolute path as the root path can change.
|
||||
if exists {
|
||||
m.lo.Debug("attachment with content ID already exists", "content_id", attachment.ContentID)
|
||||
m.lo.Debug("attachment with content ID already exists replacing content ID with media relative URL", "content_id", contentID, "media_uuid", uuid)
|
||||
message.Content = strings.ReplaceAll(message.Content, fmt.Sprintf("cid:%s", attachment.ContentID), "/uploads/"+uuid)
|
||||
continue
|
||||
}
|
||||
|
||||
// Attachment does not exist, replace the content ID with the new more unique content ID.
|
||||
message.Content = strings.ReplaceAll(message.Content, fmt.Sprintf("cid:%s", attachment.ContentID), fmt.Sprintf("cid:%s", contentID))
|
||||
}
|
||||
|
||||
m.lo.Debug("uploading message attachment", "name", attachment.Name, "content_id", attachment.ContentID, "size", attachment.Size)
|
||||
|
||||
// Sanitize filename and upload.
|
||||
// Sanitize filename.
|
||||
attachment.Name = stringutil.SanitizeFilename(attachment.Name)
|
||||
|
||||
m.lo.Debug("uploading message attachment", "name", attachment.Name, "content_id", contentID, "size", attachment.Size, "content_type", attachment.ContentType,
|
||||
"content_id", contentID, "disposition", attachment.Disposition)
|
||||
|
||||
// Upload and insert entry in media table.
|
||||
attachReader := bytes.NewReader(attachment.Content)
|
||||
media, err := m.mediaStore.UploadAndInsert(
|
||||
attachment.Name,
|
||||
attachment.ContentType,
|
||||
attachment.ContentID,
|
||||
contentID,
|
||||
/** Linking media to message happens later **/
|
||||
null.String{}, /** modelType */
|
||||
null.Int{}, /** modelID **/
|
||||
|
||||
@@ -105,13 +105,13 @@ func (m *Manager) Insert(disposition null.String, fileName, contentType, content
|
||||
if err := m.queries.Insert.QueryRow(m.store.Name(), fileName, contentType, fileSize, meta, modelID, modelType, disposition, contentID, uuid).Scan(&id); err != nil {
|
||||
m.lo.Error("error inserting media", "error", err)
|
||||
}
|
||||
return m.Get(id)
|
||||
return m.Get(id, "")
|
||||
}
|
||||
|
||||
// Get retrieves the media record by its ID and returns the media.
|
||||
func (m *Manager) Get(id int) (models.Media, error) {
|
||||
func (m *Manager) Get(id int, uuid string) (models.Media, error) {
|
||||
var media models.Media
|
||||
if err := m.queries.Get.Get(&media, id); err != nil {
|
||||
if err := m.queries.Get.Get(&media, id, uuid); err != nil {
|
||||
m.lo.Error("error fetching media", "error", err)
|
||||
return media, envelope.NewError(envelope.GeneralError, "Error fetching media", nil)
|
||||
}
|
||||
@@ -119,28 +119,17 @@ func (m *Manager) Get(id int) (models.Media, error) {
|
||||
return media, nil
|
||||
}
|
||||
|
||||
// GetByUUID retrieves a media record by the uuid.
|
||||
func (m *Manager) GetByUUID(uuid string) (models.Media, error) {
|
||||
var media models.Media
|
||||
if err := m.queries.GetByUUID.Get(&media, uuid); err != nil {
|
||||
// ContentIDExists checks if a content_id exists in the database and returns the UUID of the media file.
|
||||
func (m *Manager) ContentIDExists(contentID string) (bool, string, error) {
|
||||
var uuid string
|
||||
if err := m.queries.ContentIDExists.Get(&uuid, contentID); err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return media, envelope.NewError(envelope.GeneralError, "File not found", nil)
|
||||
return false, "", nil
|
||||
}
|
||||
m.lo.Error("error fetching media", "error", err)
|
||||
return media, envelope.NewError(envelope.GeneralError, "Error fetching media", nil)
|
||||
m.lo.Error("error checking if content_id exists", "error", err)
|
||||
return false, "", fmt.Errorf("checking if content_id exists: %w", err)
|
||||
}
|
||||
media.URL = m.store.GetURL(uuid)
|
||||
return media, nil
|
||||
}
|
||||
|
||||
// ContentIDExists returns true if a media file with the given content ID exists.
|
||||
func (m *Manager) ContentIDExists(contentID string) (bool, error) {
|
||||
var exists bool
|
||||
if err := m.queries.ContentIDExists.Get(&exists, contentID); err != nil {
|
||||
m.lo.Error("error checking media existence", "error", err)
|
||||
return false, fmt.Errorf("checking media existence: %w", err)
|
||||
}
|
||||
return exists, nil
|
||||
return true, uuid, nil
|
||||
}
|
||||
|
||||
// GetBlob retrieves the raw binary content of a media file by its name.
|
||||
|
||||
@@ -17,7 +17,10 @@ RETURNING id;
|
||||
-- name: get-media
|
||||
SELECT id, created_at, "uuid", store, filename, content_type, model_id, model_type, "size", disposition
|
||||
FROM media
|
||||
WHERE id = $1;
|
||||
WHERE
|
||||
($1 > 0 AND id = $1)
|
||||
OR
|
||||
($2 != '' AND uuid = $2::uuid)
|
||||
|
||||
-- name: get-media-by-uuid
|
||||
SELECT id, created_at, "uuid", store, filename, content_type, model_id, model_type, "size", disposition
|
||||
@@ -48,4 +51,4 @@ WHERE model_type = 'messages'
|
||||
AND created_at < NOW() - INTERVAL '1 day';
|
||||
|
||||
-- name: content-id-exists
|
||||
SELECT EXISTS(SELECT 1 FROM media WHERE content_id = $1);
|
||||
SELECT uuid FROM media WHERE content_id = $1;
|
||||
Reference in New Issue
Block a user