feat: add support for sending messages as contact

- introduce new permission `messages:write_as_contact` that needs to be set to allow this.
This commit is contained in:
Abhinav Raut
2025-09-13 20:55:04 +05:30
parent a0e1ccf117
commit 210b8bb53b
7 changed files with 50 additions and 2 deletions

View File

@@ -734,7 +734,7 @@ func handleCreateConversation(r *fastglue.Request) error {
return sendErrorEnvelope(r, envelope.NewError(envelope.GeneralError, app.i18n.Ts("globals.messages.errorSending", "name", "{globals.terms.message}"), nil)) return sendErrorEnvelope(r, envelope.NewError(envelope.GeneralError, app.i18n.Ts("globals.messages.errorSending", "name", "{globals.terms.message}"), nil))
} }
case umodels.UserTypeContact: case umodels.UserTypeContact:
// Create message on behalf of contact. // Create contact message.
if _, err := app.conversation.CreateContactMessage(media, contact.ID, conversationUUID, req.Content, cmodels.ContentTypeHTML); err != nil { if _, err := app.conversation.CreateContactMessage(media, contact.ID, conversationUUID, req.Content, cmodels.ContentTypeHTML); err != nil {
// Delete the conversation if message creation fails. // Delete the conversation if message creation fails.
if err := app.conversation.DeleteConversation(conversationUUID); err != nil { if err := app.conversation.DeleteConversation(conversationUUID); err != nil {

View File

@@ -2,10 +2,14 @@ package main
import ( import (
"strconv" "strconv"
"strings"
amodels "github.com/abhinavxd/libredesk/internal/auth/models" 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" "github.com/abhinavxd/libredesk/internal/envelope"
medModels "github.com/abhinavxd/libredesk/internal/media/models" medModels "github.com/abhinavxd/libredesk/internal/media/models"
umodels "github.com/abhinavxd/libredesk/internal/user/models"
"github.com/valyala/fasthttp" "github.com/valyala/fasthttp"
"github.com/zerodha/fastglue" "github.com/zerodha/fastglue"
) )
@@ -17,6 +21,7 @@ type messageReq struct {
To []string `json:"to"` To []string `json:"to"`
CC []string `json:"cc"` CC []string `json:"cc"`
BCC []string `json:"bcc"` BCC []string `json:"bcc"`
SenderType string `json:"sender_type"`
} }
// handleGetMessages returns messages for a conversation. // handleGetMessages returns messages for a conversation.
@@ -150,7 +155,31 @@ func handleSendMessage(r *fastglue.Request) error {
return r.SendErrorEnvelope(fasthttp.StatusInternalServerError, app.i18n.Ts("globals.messages.errorParsing", "name", "{globals.terms.request}"), nil, envelope.InputError) return r.SendErrorEnvelope(fasthttp.StatusInternalServerError, app.i18n.Ts("globals.messages.errorParsing", "name", "{globals.terms.request}"), nil, envelope.InputError)
} }
// Prepare attachments. if req.SenderType != umodels.UserTypeAgent && req.SenderType != umodels.UserTypeContact {
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.invalid", "name", "`sender_type`"), 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 {
return sendErrorEnvelope(r, envelope.NewError(envelope.InputError, app.i18n.Ts("globals.messages.errorChecking", "name", "{globals.terms.permission}"), nil))
}
ok, err := app.authz.Enforce(user, parts[0], parts[1])
if err != nil {
return sendErrorEnvelope(r, envelope.NewError(envelope.InputError, app.i18n.Ts("globals.messages.errorChecking", "name", "{globals.terms.permission}"), nil))
}
if !ok {
return r.SendErrorEnvelope(fasthttp.StatusForbidden, app.i18n.Ts("globals.messages.denied", "name", "{globals.terms.permission}"), nil, envelope.PermissionError)
}
}
// Get media for all attachments.
var media = make([]medModels.Media, 0, len(req.Attachments)) var media = make([]medModels.Media, 0, len(req.Attachments))
for _, id := range req.Attachments { for _, id := range req.Attachments {
m, err := app.media.Get(id, "") m, err := app.media.Get(id, "")
@@ -161,6 +190,16 @@ func handleSendMessage(r *fastglue.Request) error {
media = append(media, m) media = append(media, m)
} }
// Create contact message.
if req.SenderType == umodels.UserTypeContact {
message, err := app.conversation.CreateContactMessage(media, int(conv.ContactID), cuuid, req.Message, cmodels.ContentTypeHTML)
if err != nil {
return sendErrorEnvelope(r, err)
}
return r.SendEnvelope(message)
}
// Send private note.
if req.Private { if req.Private {
message, err := app.conversation.SendPrivateNote(media, user.ID, cuuid, req.Message) message, err := app.conversation.SendPrivateNote(media, user.ID, cuuid, req.Message)
if err != nil { if err != nil {
@@ -168,6 +207,8 @@ func handleSendMessage(r *fastglue.Request) error {
} }
return r.SendEnvelope(message) return r.SendEnvelope(message)
} }
// Queue reply.
message, err := app.conversation.QueueReply(media, conv.InboxID, user.ID, cuuid, req.Message, req.To, req.CC, req.BCC, map[string]any{} /**meta**/) message, err := app.conversation.QueueReply(media, conv.InboxID, user.ID, cuuid, req.Message, req.To, req.CC, req.BCC, map[string]any{} /**meta**/)
if err != nil { if err != nil {
return sendErrorEnvelope(r, err) return sendErrorEnvelope(r, err)

View File

@@ -12,6 +12,7 @@ export const permissions = {
CONVERSATIONS_UPDATE_TAGS: 'conversations:update_tags', CONVERSATIONS_UPDATE_TAGS: 'conversations:update_tags',
MESSAGES_READ: 'messages:read', MESSAGES_READ: 'messages:read',
MESSAGES_WRITE: 'messages:write', MESSAGES_WRITE: 'messages:write',
MESSAGES_WRITE_AS_CONTACT: 'messages:write_as_contact',
VIEW_MANAGE: 'view:manage', VIEW_MANAGE: 'view:manage',
GENERAL_SETTINGS_MANAGE: 'general_settings:manage', GENERAL_SETTINGS_MANAGE: 'general_settings:manage',
NOTIFICATION_SETTINGS_MANAGE: 'notification_settings:manage', NOTIFICATION_SETTINGS_MANAGE: 'notification_settings:manage',

View File

@@ -140,6 +140,7 @@ const permissions = ref([
{ name: perms.CONVERSATIONS_UPDATE_TAGS, label: t('admin.role.conversations.updateTags') }, { name: perms.CONVERSATIONS_UPDATE_TAGS, label: t('admin.role.conversations.updateTags') },
{ name: perms.MESSAGES_READ, label: t('admin.role.messages.read') }, { name: perms.MESSAGES_READ, label: t('admin.role.messages.read') },
{ name: perms.MESSAGES_WRITE, label: t('admin.role.messages.write') }, { name: perms.MESSAGES_WRITE, label: t('admin.role.messages.write') },
{ name: perms.MESSAGES_WRITE_AS_CONTACT, label: t('admin.role.messages.writeAsContact') },
{ name: perms.VIEW_MANAGE, label: t('admin.role.view.manage') } { name: perms.VIEW_MANAGE, label: t('admin.role.view.manage') }
] ]
}, },

View File

@@ -122,6 +122,7 @@ import { Input } from '@/components/ui/input'
import { useEmitter } from '@/composables/useEmitter' import { useEmitter } from '@/composables/useEmitter'
import { useFileUpload } from '@/composables/useFileUpload' import { useFileUpload } from '@/composables/useFileUpload'
import ReplyBoxContent from '@/features/conversation/ReplyBoxContent.vue' import ReplyBoxContent from '@/features/conversation/ReplyBoxContent.vue'
import { UserTypeAgent } from '@/constants/user'
import { import {
Form, Form,
FormField, FormField,
@@ -252,6 +253,7 @@ const processSend = async () => {
if (hasTextContent.value > 0 || mediaFiles.value.length > 0) { if (hasTextContent.value > 0 || mediaFiles.value.length > 0) {
const message = htmlContent.value const message = htmlContent.value
await api.sendMessage(conversationStore.current.uuid, { await api.sendMessage(conversationStore.current.uuid, {
sender_type: UserTypeAgent,
private: messageType.value === 'private_note', private: messageType.value === 'private_note',
message: message, message: message,
attachments: mediaFiles.value.map((file) => file.id), attachments: mediaFiles.value.map((file) => file.id),

View File

@@ -500,6 +500,7 @@
"admin.role.conversations.updateTags": "Add or remove conversation tags", "admin.role.conversations.updateTags": "Add or remove conversation tags",
"admin.role.messages.read": "View conversation messages", "admin.role.messages.read": "View conversation messages",
"admin.role.messages.write": "Send messages in conversations", "admin.role.messages.write": "Send messages in conversations",
"admin.role.messages.writeAsContact": "Send messages as contact",
"admin.role.view.manage": "Create and manage conversation views", "admin.role.view.manage": "Create and manage conversation views",
"admin.role.generalSettings.manage": "Manage General Settings", "admin.role.generalSettings.manage": "Manage General Settings",
"admin.role.notificationSettings.manage": "Manage Notification Settings", "admin.role.notificationSettings.manage": "Manage Notification Settings",

View File

@@ -15,6 +15,7 @@ const (
PermConversationWrite = "conversations:write" PermConversationWrite = "conversations:write"
PermMessagesRead = "messages:read" PermMessagesRead = "messages:read"
PermMessagesWrite = "messages:write" PermMessagesWrite = "messages:write"
PermMessagesWriteAsContact = "messages:write_as_contact"
// View // View
PermViewManage = "view:manage" PermViewManage = "view:manage"
@@ -102,6 +103,7 @@ var validPermissions = map[string]struct{}{
PermConversationWrite: {}, PermConversationWrite: {},
PermMessagesRead: {}, PermMessagesRead: {},
PermMessagesWrite: {}, PermMessagesWrite: {},
PermMessagesWriteAsContact: {},
PermViewManage: {}, PermViewManage: {},
PermStatusManage: {}, PermStatusManage: {},
PermTagsManage: {}, PermTagsManage: {},