Skip to content

Commit d69a8c5

Browse files
committed
feat: custom attributes for contacts and conversations
1 parent 4e893ef commit d69a8c5

40 files changed

+1932
-277
lines changed

cmd/conversation.go

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -478,6 +478,64 @@ func handleUpdateConversationtags(r *fastglue.Request) error {
478478
return r.SendEnvelope(true)
479479
}
480480

481+
// handleUpdateConversationCustomAttributes updates custom attributes of a conversation.
482+
func handleUpdateConversationCustomAttributes(r *fastglue.Request) error {
483+
var (
484+
app = r.Context.(*App)
485+
attributes = map[string]any{}
486+
auser = r.RequestCtx.UserValue("user").(amodels.User)
487+
uuid = r.RequestCtx.UserValue("uuid").(string)
488+
)
489+
if err := r.Decode(&attributes, ""); err != nil {
490+
app.lo.Error("error unmarshalling custom attributes JSON", "error", err)
491+
return r.SendErrorEnvelope(fasthttp.StatusInternalServerError, app.i18n.Ts("globals.messages.errorParsing", "name", "{globals.terms.request}"), nil, envelope.InputError)
492+
}
493+
494+
// Enforce conversation access.
495+
user, err := app.user.GetAgent(auser.ID, "")
496+
if err != nil {
497+
return sendErrorEnvelope(r, err)
498+
}
499+
_, err = enforceConversationAccess(app, uuid, user)
500+
if err != nil {
501+
return sendErrorEnvelope(r, err)
502+
}
503+
504+
// Update custom attributes.
505+
if err := app.conversation.UpdateConversationCustomAttributes(uuid, attributes); err != nil {
506+
return sendErrorEnvelope(r, err)
507+
}
508+
return r.SendEnvelope(true)
509+
}
510+
511+
// handleUpdateContactCustomAttributes updates custom attributes of a contact.
512+
func handleUpdateContactCustomAttributes(r *fastglue.Request) error {
513+
var (
514+
app = r.Context.(*App)
515+
attributes = map[string]any{}
516+
auser = r.RequestCtx.UserValue("user").(amodels.User)
517+
uuid = r.RequestCtx.UserValue("uuid").(string)
518+
)
519+
if err := r.Decode(&attributes, ""); err != nil {
520+
app.lo.Error("error unmarshalling custom attributes JSON", "error", err)
521+
return r.SendErrorEnvelope(fasthttp.StatusInternalServerError, app.i18n.Ts("globals.messages.errorParsing", "name", "{globals.terms.request}"), nil, envelope.InputError)
522+
}
523+
524+
// Enforce conversation access.
525+
user, err := app.user.GetAgent(auser.ID, "")
526+
if err != nil {
527+
return sendErrorEnvelope(r, err)
528+
}
529+
conversation, err := enforceConversationAccess(app, uuid, user)
530+
if err != nil {
531+
return sendErrorEnvelope(r, err)
532+
}
533+
if err := app.user.UpdateCustomAttributes(conversation.ContactID, attributes); err != nil {
534+
return sendErrorEnvelope(r, err)
535+
}
536+
return r.SendEnvelope(true)
537+
}
538+
481539
// handleDashboardCounts retrieves general dashboard counts for all users.
482540
func handleDashboardCounts(r *fastglue.Request) error {
483541
var (

cmd/custom_attributes.go

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
package main
2+
3+
import (
4+
"strconv"
5+
6+
cmodels "github.com/abhinavxd/libredesk/internal/custom_attribute/models"
7+
"github.com/abhinavxd/libredesk/internal/envelope"
8+
"github.com/valyala/fasthttp"
9+
"github.com/zerodha/fastglue"
10+
)
11+
12+
// handleGetCustomAttribute retrieves a custom attribute by its ID.
13+
func handleGetCustomAttribute(r *fastglue.Request) error {
14+
var (
15+
app = r.Context.(*App)
16+
)
17+
id, err := strconv.Atoi(r.RequestCtx.UserValue("id").(string))
18+
if err != nil || id <= 0 {
19+
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.invalid", "name", "`id`"), nil, envelope.InputError)
20+
}
21+
22+
attribute, err := app.customAttribute.Get(id)
23+
if err != nil {
24+
return sendErrorEnvelope(r, err)
25+
}
26+
return r.SendEnvelope(attribute)
27+
}
28+
29+
// handleGetCustomAttributes retrieves all custom attributes from the database.
30+
func handleGetCustomAttributes(r *fastglue.Request) error {
31+
var (
32+
app = r.Context.(*App)
33+
appliesTo = string(r.RequestCtx.QueryArgs().Peek("applies_to"))
34+
)
35+
attributes, err := app.customAttribute.GetAll(appliesTo)
36+
if err != nil {
37+
return sendErrorEnvelope(r, err)
38+
}
39+
return r.SendEnvelope(attributes)
40+
}
41+
42+
// handleCreateCustomAttribute creates a new custom attribute in the database.
43+
func handleCreateCustomAttribute(r *fastglue.Request) error {
44+
var (
45+
app = r.Context.(*App)
46+
attribute = cmodels.CustomAttribute{}
47+
)
48+
if err := r.Decode(&attribute, "json"); err != nil {
49+
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.errorParsing", "name", "{globals.terms.request}"), err.Error(), envelope.InputError)
50+
}
51+
if err := validateCustomAttribute(app, attribute); err != nil {
52+
return sendErrorEnvelope(r, err)
53+
}
54+
if err := app.customAttribute.Create(attribute); err != nil {
55+
return sendErrorEnvelope(r, err)
56+
}
57+
return r.SendEnvelope(true)
58+
}
59+
60+
// handleUpdateCustomAttribute updates an existing custom attribute in the database.
61+
func handleUpdateCustomAttribute(r *fastglue.Request) error {
62+
var (
63+
app = r.Context.(*App)
64+
attribute = cmodels.CustomAttribute{}
65+
)
66+
id, err := strconv.Atoi(r.RequestCtx.UserValue("id").(string))
67+
if err != nil || id <= 0 {
68+
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.invalid", "name", "`id`"), nil, envelope.InputError)
69+
}
70+
if err := r.Decode(&attribute, "json"); err != nil {
71+
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.errorParsing", "name", "{globals.terms.request}"), err.Error(), envelope.InputError)
72+
}
73+
if err := validateCustomAttribute(app, attribute); err != nil {
74+
return sendErrorEnvelope(r, err)
75+
}
76+
if err = app.customAttribute.Update(id, attribute); err != nil {
77+
return sendErrorEnvelope(r, err)
78+
}
79+
80+
return r.SendEnvelope(true)
81+
}
82+
83+
// handleDeleteCustomAttribute deletes a custom attribute from the database.
84+
func handleDeleteCustomAttribute(r *fastglue.Request) error {
85+
var (
86+
app = r.Context.(*App)
87+
)
88+
id, err := strconv.Atoi(r.RequestCtx.UserValue("id").(string))
89+
if err != nil || id <= 0 {
90+
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.invalid", "name", "`id`"), nil, envelope.InputError)
91+
}
92+
93+
if err = app.customAttribute.Delete(id); err != nil {
94+
return sendErrorEnvelope(r, err)
95+
}
96+
97+
return r.SendEnvelope(true)
98+
}
99+
100+
// validateCustomAttribute validates a custom attribute.
101+
func validateCustomAttribute(app *App, attribute cmodels.CustomAttribute) error {
102+
if attribute.Name == "" {
103+
return envelope.NewError(envelope.InputError, app.i18n.Ts("globals.messages.empty", "name", "`name`"), nil)
104+
}
105+
if attribute.AppliesTo == "" {
106+
return envelope.NewError(envelope.InputError, app.i18n.Ts("globals.messages.empty", "name", "`applies_to`"), nil)
107+
}
108+
if attribute.DataType == "" {
109+
return envelope.NewError(envelope.InputError, app.i18n.Ts("globals.messages.empty", "name", "`type`"), nil)
110+
}
111+
if attribute.Description == "" {
112+
return envelope.NewError(envelope.InputError, app.i18n.Ts("globals.messages.empty", "name", "`description`"), nil)
113+
}
114+
if attribute.Key == "" {
115+
return envelope.NewError(envelope.InputError, app.i18n.Ts("globals.messages.empty", "name", "`key`"), nil)
116+
}
117+
return nil
118+
}

cmd/handlers.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,8 @@ func initHandlers(g *fastglue.Fastglue, hub *ws.Hub) {
6060
g.POST("/api/v1/conversations/{cuuid}/messages", perm(handleSendMessage, "messages:write"))
6161
g.PUT("/api/v1/conversations/{cuuid}/messages/{uuid}/retry", perm(handleRetryMessage, "messages:write"))
6262
g.POST("/api/v1/conversations", perm(handleCreateConversation, "conversations:write"))
63+
g.PUT("/api/v1/conversations/{uuid}/custom-attributes", auth(handleUpdateConversationCustomAttributes))
64+
g.PUT("/api/v1/conversations/{uuid}/contacts/custom-attributes", auth(handleUpdateContactCustomAttributes))
6365

6466
// Search.
6567
g.GET("/api/v1/conversations/search", perm(handleSearchConversations, "conversations:read"))
@@ -180,6 +182,13 @@ func initHandlers(g *fastglue.Fastglue, hub *ws.Hub) {
180182
g.POST("/api/v1/ai/completion", auth(handleAICompletion))
181183
g.PUT("/api/v1/ai/provider", perm(handleUpdateAIProvider, "ai:manage"))
182184

185+
// Custom attributes.
186+
g.GET("/api/v1/custom-attributes", auth(handleGetCustomAttributes))
187+
g.POST("/api/v1/custom-attributes", perm(handleCreateCustomAttribute, "custom_attributes:manage"))
188+
g.GET("/api/v1/custom-attributes/{id}", perm(handleGetCustomAttribute, "custom_attributes:manage"))
189+
g.PUT("/api/v1/custom-attributes/{id}", perm(handleUpdateCustomAttribute, "custom_attributes:manage"))
190+
g.DELETE("/api/v1/custom-attributes/{id}", perm(handleDeleteCustomAttribute, "custom_attributes:manage"))
191+
183192
// WebSocket.
184193
g.GET("/ws", auth(func(r *fastglue.Request) error {
185194
return handleWS(r, hub)

cmd/init.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import (
2323
"github.com/abhinavxd/libredesk/internal/conversation/priority"
2424
"github.com/abhinavxd/libredesk/internal/conversation/status"
2525
"github.com/abhinavxd/libredesk/internal/csat"
26+
customAttribute "github.com/abhinavxd/libredesk/internal/custom_attribute"
2627
"github.com/abhinavxd/libredesk/internal/inbox"
2728
"github.com/abhinavxd/libredesk/internal/inbox/channel/email"
2829
imodels "github.com/abhinavxd/libredesk/internal/inbox/models"
@@ -793,6 +794,20 @@ func initSearch(db *sqlx.DB, i18n *i18n.I18n) *search.Manager {
793794
return m
794795
}
795796

797+
// initCustomAttribute inits custom attribute manager.
798+
func initCustomAttribute(db *sqlx.DB, i18n *i18n.I18n) *customAttribute.Manager {
799+
lo := initLogger("custom-attribute")
800+
m, err := customAttribute.New(customAttribute.Opts{
801+
DB: db,
802+
Lo: lo,
803+
I18n: i18n,
804+
})
805+
if err != nil {
806+
log.Fatalf("error initializing custom attribute manager: %v", err)
807+
}
808+
return m
809+
}
810+
796811
// initLogger initializes a logf logger.
797812
func initLogger(src string) *logf.Logger {
798813
lvl, env := ko.MustString("app.log_level"), ko.MustString("app.env")

cmd/main.go

Lines changed: 57 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919
businesshours "github.com/abhinavxd/libredesk/internal/business_hours"
2020
"github.com/abhinavxd/libredesk/internal/colorlog"
2121
"github.com/abhinavxd/libredesk/internal/csat"
22+
customAttribute "github.com/abhinavxd/libredesk/internal/custom_attribute"
2223
"github.com/abhinavxd/libredesk/internal/macro"
2324
notifier "github.com/abhinavxd/libredesk/internal/notification"
2425
"github.com/abhinavxd/libredesk/internal/search"
@@ -59,33 +60,34 @@ var (
5960

6061
// App is the global app context which is passed and injected in the http handlers.
6162
type App struct {
62-
fs stuffbin.FileSystem
63-
consts atomic.Value
64-
auth *auth_.Auth
65-
authz *authz.Enforcer
66-
i18n *i18n.I18n
67-
lo *logf.Logger
68-
oidc *oidc.Manager
69-
media *media.Manager
70-
setting *setting.Manager
71-
role *role.Manager
72-
user *user.Manager
73-
team *team.Manager
74-
status *status.Manager
75-
priority *priority.Manager
76-
tag *tag.Manager
77-
inbox *inbox.Manager
78-
tmpl *template.Manager
79-
macro *macro.Manager
80-
conversation *conversation.Manager
81-
automation *automation.Engine
82-
businessHours *businesshours.Manager
83-
sla *sla.Manager
84-
csat *csat.Manager
85-
view *view.Manager
86-
ai *ai.Manager
87-
search *search.Manager
88-
notifier *notifier.Service
63+
fs stuffbin.FileSystem
64+
consts atomic.Value
65+
auth *auth_.Auth
66+
authz *authz.Enforcer
67+
i18n *i18n.I18n
68+
lo *logf.Logger
69+
oidc *oidc.Manager
70+
media *media.Manager
71+
setting *setting.Manager
72+
role *role.Manager
73+
user *user.Manager
74+
team *team.Manager
75+
status *status.Manager
76+
priority *priority.Manager
77+
tag *tag.Manager
78+
inbox *inbox.Manager
79+
tmpl *template.Manager
80+
macro *macro.Manager
81+
conversation *conversation.Manager
82+
automation *automation.Engine
83+
businessHours *businesshours.Manager
84+
sla *sla.Manager
85+
csat *csat.Manager
86+
view *view.Manager
87+
ai *ai.Manager
88+
search *search.Manager
89+
notifier *notifier.Service
90+
customAttribute *customAttribute.Manager
8991

9092
// Global state that stores data on an available app update.
9193
update *AppUpdate
@@ -197,33 +199,34 @@ func main() {
197199
go user.MonitorAgentAvailability(ctx)
198200

199201
var app = &App{
200-
lo: lo,
201-
fs: fs,
202-
sla: sla,
203-
oidc: oidc,
204-
i18n: i18n,
205-
auth: auth,
206-
media: media,
207-
setting: settings,
208-
inbox: inbox,
209-
user: user,
210-
team: team,
211-
status: status,
212-
priority: priority,
213-
tmpl: template,
214-
notifier: notifier,
215-
consts: atomic.Value{},
216-
conversation: conversation,
217-
automation: automation,
218-
businessHours: businessHours,
219-
authz: initAuthz(i18n),
220-
view: initView(db),
221-
csat: initCSAT(db, i18n),
222-
search: initSearch(db, i18n),
223-
role: initRole(db, i18n),
224-
tag: initTag(db, i18n),
225-
macro: initMacro(db, i18n),
226-
ai: initAI(db, i18n),
202+
lo: lo,
203+
fs: fs,
204+
sla: sla,
205+
oidc: oidc,
206+
i18n: i18n,
207+
auth: auth,
208+
media: media,
209+
setting: settings,
210+
inbox: inbox,
211+
user: user,
212+
team: team,
213+
status: status,
214+
priority: priority,
215+
tmpl: template,
216+
notifier: notifier,
217+
consts: atomic.Value{},
218+
conversation: conversation,
219+
automation: automation,
220+
businessHours: businessHours,
221+
customAttribute: initCustomAttribute(db, i18n),
222+
authz: initAuthz(i18n),
223+
view: initView(db),
224+
csat: initCSAT(db, i18n),
225+
search: initSearch(db, i18n),
226+
role: initRole(db, i18n),
227+
tag: initTag(db, i18n),
228+
macro: initMacro(db, i18n),
229+
ai: initAI(db, i18n),
227230
}
228231
app.consts.Store(constants)
229232

cmd/middlewares.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ func perm(handler fastglue.FastRequestHandler, perm string) fastglue.FastRequest
7979
hdrToken = string(r.RequestCtx.Request.Header.Peek("X-CSRFTOKEN"))
8080
)
8181

82+
// Match CSRF token from cookie and header.
8283
if cookieToken == "" || hdrToken == "" || cookieToken != hdrToken {
8384
app.lo.Error("csrf token mismatch", "cookie_token", cookieToken, "header_token", hdrToken)
8485
return r.SendErrorEnvelope(http.StatusForbidden, app.i18n.T("auth.csrfTokenMismatch"), nil, envelope.PermissionError)

0 commit comments

Comments
 (0)