Files
libredesk/cmd/views.go
T
Abhinav Raut 43ebed1c3a WS rewrite: per-client subscriptions and perf improvements
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
2026-05-20 23:42:07 +05:30

275 lines
9.3 KiB
Go

package main
import (
"strconv"
amodels "github.com/abhinavxd/libredesk/internal/auth/models"
"github.com/abhinavxd/libredesk/internal/envelope"
vmodels "github.com/abhinavxd/libredesk/internal/view/models"
"github.com/valyala/fasthttp"
"github.com/zerodha/fastglue"
)
// handleGetUserViews returns all personal views for a user.
func handleGetUserViews(r *fastglue.Request) error {
var (
app = r.Context.(*App)
auser = r.RequestCtx.UserValue("user").(amodels.User)
)
user, err := app.user.GetAgentCachedOrLoad(auser.ID)
if err != nil {
return sendErrorEnvelope(r, err)
}
v, err := app.view.GetUsersViews(user.ID)
if err != nil {
return sendErrorEnvelope(r, err)
}
return r.SendEnvelope(v)
}
// handleCreateUserView creates a personal view for a user.
func handleCreateUserView(r *fastglue.Request) error {
var (
app = r.Context.(*App)
view = vmodels.View{}
auser = r.RequestCtx.UserValue("user").(amodels.User)
)
if err := r.Decode(&view, "json"); err != nil {
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.T("errors.parsingRequest"), err.Error(), envelope.InputError)
}
user, err := app.user.GetAgentCachedOrLoad(auser.ID)
if err != nil {
return sendErrorEnvelope(r, err)
}
if view.Name == "" {
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.empty", "name", "`Name`"), nil, envelope.InputError)
}
if string(view.Filters) == "" {
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.empty", "name", "`Filters`"), nil, envelope.InputError)
}
createdView, err := app.view.Create(view.Name, view.Filters, user.ID)
if err != nil {
return sendErrorEnvelope(r, err)
}
return r.SendEnvelope(createdView)
}
// handleDeleteUserView deletes a personal view for a user.
func handleDeleteUserView(r *fastglue.Request) error {
var (
app = r.Context.(*App)
auser = r.RequestCtx.UserValue("user").(amodels.User)
)
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)
}
user, err := app.user.GetAgentCachedOrLoad(auser.ID)
if err != nil {
return sendErrorEnvelope(r, err)
}
view, err := app.view.Get(id)
if err != nil {
return sendErrorEnvelope(r, err)
}
// Only allow deletion of personal views owned by the user
if err := validatePersonalViewOwnership(app, view, user.ID); err != nil {
return sendErrorEnvelope(r, err)
}
if err = app.view.Delete(id); err != nil {
return sendErrorEnvelope(r, err)
}
return r.SendEnvelope(true)
}
// handleUpdateUserView updates a personal view for a user.
func handleUpdateUserView(r *fastglue.Request) error {
var (
app = r.Context.(*App)
auser = r.RequestCtx.UserValue("user").(amodels.User)
view = vmodels.View{}
)
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 := r.Decode(&view, "json"); err != nil {
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.T("errors.parsingRequest"), err.Error(), envelope.InputError)
}
user, err := app.user.GetAgentCachedOrLoad(auser.ID)
if err != nil {
return sendErrorEnvelope(r, err)
}
if view.Name == "" {
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.empty", "name", "`name`"), nil, envelope.InputError)
}
if string(view.Filters) == "" {
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.empty", "name", "`filters`"), nil, envelope.InputError)
}
v, err := app.view.Get(id)
if err != nil {
return sendErrorEnvelope(r, err)
}
// Only allow update of personal views owned by the user
if err := validatePersonalViewOwnership(app, v, user.ID); err != nil {
return sendErrorEnvelope(r, err)
}
updatedView, err := app.view.Update(id, view.Name, view.Filters, user.ID)
if err != nil {
return sendErrorEnvelope(r, err)
}
return r.SendEnvelope(updatedView)
}
// handleGetSharedViews returns shared views accessible to the current user.
func handleGetSharedViews(r *fastglue.Request) error {
var (
app = r.Context.(*App)
auser = r.RequestCtx.UserValue("user").(amodels.User)
)
user, err := app.user.GetAgentCachedOrLoad(auser.ID)
if err != nil {
return sendErrorEnvelope(r, err)
}
views, err := app.view.GetSharedViewsForUser(user.Teams.IDs())
if err != nil {
return sendErrorEnvelope(r, err)
}
return r.SendEnvelope(views)
}
// handleGetAllSharedViews returns all shared views (admin only).
func handleGetAllSharedViews(r *fastglue.Request) error {
app := r.Context.(*App)
views, err := app.view.GetAllSharedViews()
if err != nil {
return sendErrorEnvelope(r, err)
}
return r.SendEnvelope(views)
}
// handleGetSharedView returns a single shared view (admin only).
func handleGetSharedView(r *fastglue.Request) error {
var (
app = r.Context.(*App)
id, _ = strconv.Atoi(r.RequestCtx.UserValue("id").(string))
)
if id <= 0 {
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.T("globals.messages.somethingWentWrong"), nil, envelope.InputError)
}
view, err := app.view.Get(id)
if err != nil {
return sendErrorEnvelope(r, err)
}
// Ensure it's a shared view (not a personal view)
if view.Visibility == vmodels.VisibilityUser {
return r.SendErrorEnvelope(fasthttp.StatusNotFound, app.i18n.T("validation.notFoundView"), nil, envelope.NotFoundError)
}
return r.SendEnvelope(view)
}
// handleCreateSharedView creates a shared view (admin only).
func handleCreateSharedView(r *fastglue.Request) error {
var (
app = r.Context.(*App)
view = vmodels.View{}
)
if err := r.Decode(&view, "json"); err != nil {
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.T("errors.parsingRequest"), err.Error(), envelope.InputError)
}
// Validation
if err := validateSharedView(app, view); err != nil {
return sendErrorEnvelope(r, err)
}
createdView, err := app.view.CreateSharedView(view.Name, view.Filters, view.Visibility, view.TeamID)
if err != nil {
return sendErrorEnvelope(r, err)
}
return r.SendEnvelope(createdView)
}
// handleUpdateSharedView updates a shared view (admin only).
func handleUpdateSharedView(r *fastglue.Request) error {
var (
app = r.Context.(*App)
view = vmodels.View{}
id, _ = strconv.Atoi(r.RequestCtx.UserValue("id").(string))
)
if id <= 0 {
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.T("globals.messages.somethingWentWrong"), nil, envelope.InputError)
}
if err := r.Decode(&view, "json"); err != nil {
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.T("errors.parsingRequest"), err.Error(), envelope.InputError)
}
// Verify view exists and is shared
existingView, err := app.view.Get(id)
if err != nil {
return sendErrorEnvelope(r, err)
}
if existingView.Visibility == vmodels.VisibilityUser {
return r.SendErrorEnvelope(fasthttp.StatusNotFound, app.i18n.T("validation.notFoundView"), nil, envelope.NotFoundError)
}
// Validation
if err := validateSharedView(app, view); err != nil {
return sendErrorEnvelope(r, err)
}
updatedView, err := app.view.UpdateSharedView(id, view.Name, view.Filters, view.Visibility, view.TeamID)
if err != nil {
return sendErrorEnvelope(r, err)
}
return r.SendEnvelope(updatedView)
}
// handleDeleteSharedView deletes a shared view (admin only).
func handleDeleteSharedView(r *fastglue.Request) error {
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)
}
// Verify view exists and is shared
existingView, err := app.view.Get(id)
if err != nil {
return sendErrorEnvelope(r, err)
}
if existingView.Visibility == vmodels.VisibilityUser {
return r.SendErrorEnvelope(fasthttp.StatusNotFound, app.i18n.T("validation.notFoundView"), nil, envelope.NotFoundError)
}
if err = app.view.Delete(id); err != nil {
return sendErrorEnvelope(r, err)
}
return r.SendEnvelope(true)
}
// validatePersonalViewOwnership checks if the user owns the personal view.
func validatePersonalViewOwnership(app *App, view vmodels.View, userID int) error {
if view.UserID == nil || *view.UserID != userID || view.Visibility != vmodels.VisibilityUser {
return envelope.NewError(envelope.PermissionError, app.i18n.T("status.deniedPermission"), nil)
}
return nil
}
// validateSharedView validates the fields of a shared view.
func validateSharedView(app *App, view vmodels.View) error {
if view.Name == "" {
return envelope.NewError(envelope.InputError, app.i18n.Ts("globals.messages.empty", "name", "`name`"), nil)
}
if string(view.Filters) == "" {
return envelope.NewError(envelope.InputError, app.i18n.Ts("globals.messages.empty", "name", "`filters`"), nil)
}
if view.Visibility != vmodels.VisibilityAll && view.Visibility != vmodels.VisibilityTeam {
return envelope.NewError(envelope.InputError, app.i18n.T("globals.messages.somethingWentWrong"), nil)
}
if view.Visibility == vmodels.VisibilityTeam && (view.TeamID == nil || *view.TeamID <= 0) {
return envelope.NewError(envelope.InputError, app.i18n.Ts("globals.messages.required", "name", "`team_id`"), nil)
}
return nil
}