mirror of
https://github.com/abhinavxd/libredesk.git
synced 2026-09-11 13:28:57 +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
311 lines
11 KiB
Go
311 lines
11 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"slices"
|
|
"strconv"
|
|
|
|
amodels "github.com/abhinavxd/libredesk/internal/auth/models"
|
|
autoModels "github.com/abhinavxd/libredesk/internal/automation/models"
|
|
"github.com/abhinavxd/libredesk/internal/envelope"
|
|
"github.com/abhinavxd/libredesk/internal/macro/models"
|
|
"github.com/valyala/fasthttp"
|
|
"github.com/zerodha/fastglue"
|
|
)
|
|
|
|
// handleGetMacros returns all macros.
|
|
func handleGetMacros(r *fastglue.Request) error {
|
|
var app = r.Context.(*App)
|
|
macros, err := app.macro.GetAll()
|
|
if err != nil {
|
|
return sendErrorEnvelope(r, err)
|
|
}
|
|
for i, m := range macros {
|
|
var actions []autoModels.RuleAction
|
|
if err := json.Unmarshal(m.Actions, &actions); err != nil {
|
|
app.lo.Error("error unmarshalling macro actions", "macro_id", m.ID, "error", err)
|
|
return r.SendErrorEnvelope(fasthttp.StatusInternalServerError, app.i18n.T("globals.messages.somethingWentWrong"), nil, envelope.GeneralError)
|
|
}
|
|
// Set display values for actions as the value field can contain DB IDs
|
|
if err := setDisplayValues(app, actions); err != nil {
|
|
app.lo.Warn("error setting display values", "error", err)
|
|
}
|
|
if macros[i].Actions, err = json.Marshal(actions); err != nil {
|
|
app.lo.Error("error marshalling macro actions", "macro_id", m.ID, "error", err)
|
|
return r.SendErrorEnvelope(fasthttp.StatusInternalServerError, app.i18n.T("globals.messages.somethingWentWrong"), nil, envelope.GeneralError)
|
|
}
|
|
}
|
|
return r.SendEnvelope(macros)
|
|
}
|
|
|
|
// handleGetMacro returns a macro.
|
|
func handleGetMacro(r *fastglue.Request) error {
|
|
var (
|
|
app = r.Context.(*App)
|
|
id, err = strconv.Atoi(r.RequestCtx.UserValue("id").(string))
|
|
)
|
|
if err != nil || id == 0 {
|
|
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.T("globals.messages.somethingWentWrong"), nil, envelope.InputError)
|
|
}
|
|
|
|
macro, err := app.macro.Get(id)
|
|
if err != nil {
|
|
return sendErrorEnvelope(r, err)
|
|
}
|
|
|
|
var actions []autoModels.RuleAction
|
|
if err := json.Unmarshal(macro.Actions, &actions); err != nil {
|
|
app.lo.Error("error unmarshalling macro actions", "macro_id", id, "error", err)
|
|
return r.SendErrorEnvelope(fasthttp.StatusInternalServerError, app.i18n.T("globals.messages.somethingWentWrong"), nil, envelope.GeneralError)
|
|
}
|
|
// Set display values for actions as the value field can contain DB IDs
|
|
if err := setDisplayValues(app, actions); err != nil {
|
|
app.lo.Warn("error setting display values", "error", err)
|
|
}
|
|
if macro.Actions, err = json.Marshal(actions); err != nil {
|
|
app.lo.Error("error marshalling macro actions", "macro_id", id, "error", err)
|
|
return r.SendErrorEnvelope(fasthttp.StatusInternalServerError, app.i18n.T("globals.messages.somethingWentWrong"), nil, envelope.GeneralError)
|
|
}
|
|
|
|
return r.SendEnvelope(macro)
|
|
}
|
|
|
|
// handleCreateMacro creates new macro.
|
|
func handleCreateMacro(r *fastglue.Request) error {
|
|
var (
|
|
app = r.Context.(*App)
|
|
macro = models.Macro{}
|
|
)
|
|
|
|
if err := r.Decode(¯o, "json"); err != nil {
|
|
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.T("errors.parsingRequest"), err.Error(), envelope.InputError)
|
|
}
|
|
|
|
if err := validateMacro(app, macro); err != nil {
|
|
return sendErrorEnvelope(r, err)
|
|
}
|
|
|
|
createdMacro, err := app.macro.Create(macro.Name, macro.MessageContent, macro.UserID, macro.TeamID, macro.Visibility, macro.VisibleWhen, macro.Actions)
|
|
if err != nil {
|
|
return sendErrorEnvelope(r, err)
|
|
}
|
|
|
|
return r.SendEnvelope(createdMacro)
|
|
}
|
|
|
|
// handleUpdateMacro updates a macro.
|
|
func handleUpdateMacro(r *fastglue.Request) error {
|
|
var (
|
|
app = r.Context.(*App)
|
|
macro = models.Macro{}
|
|
)
|
|
|
|
id, err := strconv.Atoi(r.RequestCtx.UserValue("id").(string))
|
|
if err != nil || id == 0 {
|
|
return r.SendErrorEnvelope(fasthttp.StatusBadRequest,
|
|
"Invalid macro `id`.", nil, envelope.InputError)
|
|
}
|
|
|
|
if err := r.Decode(¯o, "json"); err != nil {
|
|
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, "decode failed", err.Error(), envelope.InputError)
|
|
}
|
|
|
|
if err := validateMacro(app, macro); err != nil {
|
|
return sendErrorEnvelope(r, err)
|
|
}
|
|
|
|
updatedMacro, err := app.macro.Update(id, macro.Name, macro.MessageContent, macro.UserID, macro.TeamID, macro.Visibility, macro.VisibleWhen, macro.Actions)
|
|
if err != nil {
|
|
return sendErrorEnvelope(r, err)
|
|
}
|
|
|
|
return r.SendEnvelope(updatedMacro)
|
|
}
|
|
|
|
// handleDeleteMacro deletes macro.
|
|
func handleDeleteMacro(r *fastglue.Request) error {
|
|
var app = r.Context.(*App)
|
|
id, err := strconv.Atoi(r.RequestCtx.UserValue("id").(string))
|
|
if err != nil || id == 0 {
|
|
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.T("globals.messages.somethingWentWrong"), nil, envelope.InputError)
|
|
}
|
|
if err := app.macro.Delete(id); err != nil {
|
|
return sendErrorEnvelope(r, err)
|
|
}
|
|
return r.SendEnvelope(true)
|
|
}
|
|
|
|
// handleApplyMacro applies macro actions to a conversation.
|
|
func handleApplyMacro(r *fastglue.Request) error {
|
|
var (
|
|
app = r.Context.(*App)
|
|
auser = r.RequestCtx.UserValue("user").(amodels.User)
|
|
conversationUUID = r.RequestCtx.UserValue("uuid").(string)
|
|
id, _ = strconv.Atoi(r.RequestCtx.UserValue("id").(string))
|
|
incomingActions = []autoModels.RuleAction{}
|
|
)
|
|
user, err := app.user.GetAgentCachedOrLoad(auser.ID)
|
|
if err != nil {
|
|
return sendErrorEnvelope(r, err)
|
|
}
|
|
|
|
// Enforce conversation access.
|
|
conversation, err := app.conversation.GetConversation(0, conversationUUID, "")
|
|
if err != nil {
|
|
return sendErrorEnvelope(r, err)
|
|
}
|
|
if allowed, err := app.authz.EnforceConversationAccess(user, conversation); err != nil || !allowed {
|
|
return sendErrorEnvelope(r, envelope.NewError(envelope.PermissionError, app.i18n.T("status.deniedPermission"), nil))
|
|
}
|
|
|
|
macro, err := app.macro.Get(id)
|
|
if err != nil {
|
|
return sendErrorEnvelope(r, err)
|
|
}
|
|
|
|
// Decode incoming actions.
|
|
if err := r.Decode(&incomingActions, "json"); err != nil {
|
|
app.lo.Error("error unmashalling incoming actions", "error", err)
|
|
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.T("globals.messages.somethingWentWrong"), err.Error(), envelope.InputError)
|
|
}
|
|
|
|
// Make sure no duplicate action types are present.
|
|
actionTypes := make(map[string]bool, len(incomingActions))
|
|
for _, act := range incomingActions {
|
|
if actionTypes[act.Type] {
|
|
app.lo.Warn("duplicate action types found in macro apply apply request", "action", act.Type, "user_id", user.ID)
|
|
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.T("macro.duplicateActionsNotAllowed"), nil, envelope.InputError)
|
|
}
|
|
actionTypes[act.Type] = true
|
|
}
|
|
|
|
// Validate action permissions.
|
|
for _, act := range incomingActions {
|
|
if !isMacroActionAllowed(act.Type) {
|
|
app.lo.Warn("action not allowed in macro", "action", act.Type, "user_id", user.ID)
|
|
return r.SendErrorEnvelope(fasthttp.StatusForbidden, app.i18n.Ts("macro.actionNotAllowed", "name", act.Type), nil, envelope.PermissionError)
|
|
}
|
|
if !hasActionPermission(act.Type, user.Permissions) {
|
|
app.lo.Warn("no permission to execute macro action", "action", act.Type, "user_id", user.ID)
|
|
return r.SendErrorEnvelope(fasthttp.StatusForbidden, app.i18n.T("macro.permissionDenied"), nil, envelope.PermissionError)
|
|
}
|
|
}
|
|
|
|
// Apply actions.
|
|
successCount := 0
|
|
for _, act := range incomingActions {
|
|
if err := app.conversation.ApplyAction(act, conversation, user); err == nil {
|
|
successCount++
|
|
}
|
|
}
|
|
|
|
if successCount == 0 {
|
|
return r.SendErrorEnvelope(fasthttp.StatusInternalServerError, app.i18n.T("macro.couldNotApply"), nil, envelope.GeneralError)
|
|
}
|
|
|
|
// Increment usage count.
|
|
app.macro.IncrementUsageCount(macro.ID)
|
|
|
|
if successCount < len(incomingActions) {
|
|
return r.SendJSON(fasthttp.StatusMultiStatus, map[string]interface{}{
|
|
"message": app.i18n.T("macro.partiallyApplied"),
|
|
})
|
|
}
|
|
|
|
return r.SendJSON(fasthttp.StatusOK, map[string]interface{}{
|
|
"message": app.i18n.T("macro.applied"),
|
|
})
|
|
}
|
|
|
|
// hasActionPermission checks user permission for given action
|
|
func hasActionPermission(action string, userPerms []string) bool {
|
|
requiredPerm, exists := autoModels.ActionPermissions[action]
|
|
if !exists {
|
|
return false
|
|
}
|
|
return slices.Contains(userPerms, requiredPerm)
|
|
}
|
|
|
|
// setDisplayValues sets display values for actions.
|
|
func setDisplayValues(app *App, actions []autoModels.RuleAction) error {
|
|
getters := map[string]func(int) (string, error){
|
|
autoModels.ActionAssignTeam: func(id int) (string, error) {
|
|
t, err := app.team.Get(id)
|
|
if err != nil {
|
|
app.lo.Warn("team not found for macro action", "team_id", id)
|
|
return "", err
|
|
}
|
|
return t.Name, nil
|
|
},
|
|
autoModels.ActionAssignUser: func(id int) (string, error) {
|
|
u, err := app.user.GetAgent(id, "")
|
|
if err != nil {
|
|
app.lo.Warn("user not found for macro action", "user_id", id)
|
|
return "", err
|
|
}
|
|
return u.FullName(), nil
|
|
},
|
|
autoModels.ActionSetPriority: func(id int) (string, error) {
|
|
p, err := app.priority.Get(id)
|
|
if err != nil {
|
|
app.lo.Warn("priority not found for macro action", "priority_id", id)
|
|
return "", err
|
|
}
|
|
return p.Name, nil
|
|
},
|
|
autoModels.ActionSetStatus: func(id int) (string, error) {
|
|
s, err := app.status.Get(id)
|
|
if err != nil {
|
|
app.lo.Warn("status not found for macro action", "status_id", id)
|
|
return "", err
|
|
}
|
|
return s.Name, nil
|
|
},
|
|
}
|
|
for i := range actions {
|
|
actions[i].DisplayValue = []string{}
|
|
if getter, ok := getters[actions[i].Type]; ok {
|
|
id, _ := strconv.Atoi(actions[i].Value[0])
|
|
if name, err := getter(id); err == nil {
|
|
actions[i].DisplayValue = append(actions[i].DisplayValue, name)
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// validateMacro validates an incoming macro.
|
|
func validateMacro(app *App, macro models.Macro) error {
|
|
if macro.Name == "" {
|
|
return envelope.NewError(envelope.InputError, app.i18n.Ts("globals.messages.empty", "name", "`name`"), nil)
|
|
}
|
|
|
|
if len(macro.VisibleWhen) == 0 {
|
|
return envelope.NewError(envelope.InputError, app.i18n.Ts("globals.messages.empty", "name", "`visible_when`"), nil)
|
|
}
|
|
|
|
var act []autoModels.RuleAction
|
|
if err := json.Unmarshal(macro.Actions, &act); err != nil {
|
|
app.lo.Error("error unmarshalling macro actions", "error", err)
|
|
return envelope.NewError(envelope.InputError, app.i18n.T("globals.messages.somethingWentWrong"), nil)
|
|
}
|
|
for _, a := range act {
|
|
if len(a.Value) == 0 {
|
|
return envelope.NewError(envelope.InputError, app.i18n.Ts("globals.messages.empty", "name", a.Type), nil)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// isMacroActionAllowed returns true if the action is allowed in a macro.
|
|
func isMacroActionAllowed(action string) bool {
|
|
switch action {
|
|
case autoModels.ActionSendPrivateNote, autoModels.ActionReply:
|
|
return false
|
|
case autoModels.ActionAssignTeam, autoModels.ActionAssignUser, autoModels.ActionSetStatus, autoModels.ActionSetPriority, autoModels.ActionAddTags, autoModels.ActionSetTags, autoModels.ActionRemoveTags:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|