Skip to content

Commit 220321b

Browse files
authored
Merge pull request #64 from abhinavxd/feat/custom-attributes-and-notes
feat: custom attributes and contact notes
2 parents 4e893ef + d5ba706 commit 220321b

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

58 files changed

+2954
-502
lines changed

cmd/contacts.go

Lines changed: 87 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"strconv"
66
"strings"
77

8+
amodels "github.com/abhinavxd/libredesk/internal/auth/models"
89
"github.com/abhinavxd/libredesk/internal/envelope"
910
"github.com/abhinavxd/libredesk/internal/stringutil"
1011
"github.com/abhinavxd/libredesk/internal/user/models"
@@ -98,10 +99,6 @@ func handleUpdateContact(r *fastglue.Request) error {
9899
if v, ok := form.Value["phone_number_calling_code"]; ok && len(v) > 0 {
99100
phoneNumberCallingCode = string(v[0])
100101
}
101-
enabled := false
102-
if v, ok := form.Value["enabled"]; ok && len(v) > 0 {
103-
enabled = string(v[0]) == "true"
104-
}
105102
avatarURL := ""
106103
if v, ok := form.Value["avatar_url"]; ok && len(v) > 0 {
107104
avatarURL = string(v[0])
@@ -131,7 +128,6 @@ func handleUpdateContact(r *fastglue.Request) error {
131128
AvatarURL: null.NewString(avatarURL, avatarURL != ""),
132129
PhoneNumber: null.NewString(phoneNumber, phoneNumber != ""),
133130
PhoneNumberCallingCode: null.NewString(phoneNumberCallingCode, phoneNumberCallingCode != ""),
134-
Enabled: enabled,
135131
}
136132

137133
if err := app.user.UpdateContact(id, contactToUpdate); err != nil {
@@ -155,3 +151,89 @@ func handleUpdateContact(r *fastglue.Request) error {
155151
}
156152
return r.SendEnvelope(true)
157153
}
154+
155+
// handleGetContactNotes returns all notes for a contact.
156+
func handleGetContactNotes(r *fastglue.Request) error {
157+
var (
158+
app = r.Context.(*App)
159+
contactID, _ = strconv.Atoi(r.RequestCtx.UserValue("id").(string))
160+
)
161+
if contactID <= 0 {
162+
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.invalid", "name", "`id`"), nil, envelope.InputError)
163+
}
164+
notes, err := app.user.GetNotes(contactID)
165+
if err != nil {
166+
return sendErrorEnvelope(r, err)
167+
}
168+
return r.SendEnvelope(notes)
169+
}
170+
171+
// handleCreateContactNote creates a note for a contact.
172+
func handleCreateContactNote(r *fastglue.Request) error {
173+
var (
174+
app = r.Context.(*App)
175+
contactID, _ = strconv.Atoi(r.RequestCtx.UserValue("id").(string))
176+
auser = r.RequestCtx.UserValue("user").(amodels.User)
177+
note = string(r.RequestCtx.PostArgs().Peek("note"))
178+
)
179+
if len(note) == 0 {
180+
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.empty", "name", "note"), nil, envelope.InputError)
181+
}
182+
if err := app.user.CreateNote(contactID, auser.ID, note); err != nil {
183+
return sendErrorEnvelope(r, err)
184+
}
185+
return r.SendEnvelope(true)
186+
}
187+
188+
// handleDeleteContactNote deletes a note for a contact.
189+
func handleDeleteContactNote(r *fastglue.Request) error {
190+
var (
191+
app = r.Context.(*App)
192+
contactID, _ = strconv.Atoi(r.RequestCtx.UserValue("id").(string))
193+
noteID, _ = strconv.Atoi(r.RequestCtx.UserValue("note_id").(string))
194+
auser = r.RequestCtx.UserValue("user").(amodels.User)
195+
)
196+
if contactID <= 0 {
197+
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.invalid", "name", "`id`"), nil, envelope.InputError)
198+
}
199+
if noteID <= 0 {
200+
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.invalid", "name", "`note_id`"), nil, envelope.InputError)
201+
}
202+
203+
agent, err := app.user.GetAgent(auser.ID, "")
204+
if err != nil {
205+
return sendErrorEnvelope(r, err)
206+
}
207+
208+
// Allow deletion of only own notes and not those created by others, but also allow `Admin` to delete any note.
209+
if !agent.HasAdminRole() {
210+
note, err := app.user.GetNote(noteID)
211+
if err != nil {
212+
return sendErrorEnvelope(r, err)
213+
}
214+
if note.UserID != auser.ID {
215+
return r.SendErrorEnvelope(fasthttp.StatusForbidden, app.i18n.Ts("globals.messages.canOnlyDeleteOwn", "name", "{globals.terms.note}"), nil, envelope.InputError)
216+
}
217+
}
218+
219+
if err := app.user.DeleteNote(noteID, contactID); err != nil {
220+
return sendErrorEnvelope(r, err)
221+
}
222+
return r.SendEnvelope(true)
223+
}
224+
225+
// handleBlockContact blocks a contact.
226+
func handleBlockContact(r *fastglue.Request) error {
227+
var (
228+
app = r.Context.(*App)
229+
contactID, _ = strconv.Atoi(r.RequestCtx.UserValue("id").(string))
230+
enabled = r.RequestCtx.PostArgs().GetBool("enabled")
231+
)
232+
if contactID <= 0 {
233+
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.invalid", "name", "`id`"), nil, envelope.InputError)
234+
}
235+
if err := app.user.ToggleEnabled(contactID, models.UserTypeContact, enabled); err != nil {
236+
return sendErrorEnvelope(r, err)
237+
}
238+
return r.SendEnvelope(true)
239+
}

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

cmd/handlers.go

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ func initHandlers(g *fastglue.Fastglue, hub *ws.Hub) {
2020
g.GET("/api/v1/oidc/{id}/login", handleOIDCLogin)
2121
g.GET("/api/v1/oidc/{id}/finish", handleOIDCCallback)
2222

23+
// i18n.
24+
g.GET("/api/v1/lang/{lang}", handleGetI18nLang)
25+
2326
// Media.
2427
g.GET("/uploads/{uuid}", auth(handleServeMedia))
2528
g.POST("/api/v1/media", auth(handleMediaUpload))
@@ -60,11 +63,13 @@ func initHandlers(g *fastglue.Fastglue, hub *ws.Hub) {
6063
g.POST("/api/v1/conversations/{cuuid}/messages", perm(handleSendMessage, "messages:write"))
6164
g.PUT("/api/v1/conversations/{cuuid}/messages/{uuid}/retry", perm(handleRetryMessage, "messages:write"))
6265
g.POST("/api/v1/conversations", perm(handleCreateConversation, "conversations:write"))
66+
g.PUT("/api/v1/conversations/{uuid}/custom-attributes", auth(handleUpdateConversationCustomAttributes))
67+
g.PUT("/api/v1/conversations/{uuid}/contacts/custom-attributes", auth(handleUpdateContactCustomAttributes))
6368

6469
// Search.
6570
g.GET("/api/v1/conversations/search", perm(handleSearchConversations, "conversations:read"))
6671
g.GET("/api/v1/messages/search", perm(handleSearchMessages, "messages:read"))
67-
g.GET("/api/v1/contacts/search", perm(handleSearchContacts, "conversations:write"))
72+
g.GET("/api/v1/contacts/search", perm(handleSearchContacts, "contacts:read"))
6873

6974
// Views.
7075
g.GET("/api/v1/views/me", perm(handleGetUserViews, "view:manage"))
@@ -110,9 +115,15 @@ func initHandlers(g *fastglue.Fastglue, hub *ws.Hub) {
110115
g.POST("/api/v1/agents/set-password", tryAuth(handleSetPassword))
111116

112117
// Contacts.
113-
g.GET("/api/v1/contacts", perm(handleGetContacts, "contacts:manage"))
114-
g.GET("/api/v1/contacts/{id}", perm(handleGetContact, "contacts:manage"))
115-
g.PUT("/api/v1/contacts/{id}", perm(handleUpdateContact, "contacts:manage"))
118+
g.GET("/api/v1/contacts", perm(handleGetContacts, "contacts:read_all"))
119+
g.GET("/api/v1/contacts/{id}", perm(handleGetContact, "contacts:read"))
120+
g.PUT("/api/v1/contacts/{id}", perm(handleUpdateContact, "contacts:write"))
121+
g.PUT("/api/v1/contacts/{id}/block", perm(handleBlockContact, "contacts:block"))
122+
123+
// Contact notes.
124+
g.GET("/api/v1/contacts/{id}/notes", perm(handleGetContactNotes, "contact_notes:read"))
125+
g.POST("/api/v1/contacts/{id}/notes", perm(handleCreateContactNote, "contact_notes:write"))
126+
g.DELETE("/api/v1/contacts/{id}/notes/{note_id}", perm(handleDeleteContactNote, "contact_notes:delete"))
116127

117128
// Teams.
118129
g.GET("/api/v1/teams/compact", auth(handleGetTeamsCompact))
@@ -122,9 +133,6 @@ func initHandlers(g *fastglue.Fastglue, hub *ws.Hub) {
122133
g.PUT("/api/v1/teams/{id}", perm(handleUpdateTeam, "teams:manage"))
123134
g.DELETE("/api/v1/teams/{id}", perm(handleDeleteTeam, "teams:manage"))
124135

125-
// i18n.
126-
g.GET("/api/v1/lang/{lang}", handleGetI18nLang)
127-
128136
// Automations.
129137
g.GET("/api/v1/automations/rules", perm(handleGetAutomationRules, "automations:manage"))
130138
g.GET("/api/v1/automations/rules/{id}", perm(handleGetAutomationRule, "automations:manage"))
@@ -180,6 +188,13 @@ func initHandlers(g *fastglue.Fastglue, hub *ws.Hub) {
180188
g.POST("/api/v1/ai/completion", auth(handleAICompletion))
181189
g.PUT("/api/v1/ai/provider", perm(handleUpdateAIProvider, "ai:manage"))
182190

191+
// Custom attributes.
192+
g.GET("/api/v1/custom-attributes", auth(handleGetCustomAttributes))
193+
g.POST("/api/v1/custom-attributes", perm(handleCreateCustomAttribute, "custom_attributes:manage"))
194+
g.GET("/api/v1/custom-attributes/{id}", perm(handleGetCustomAttribute, "custom_attributes:manage"))
195+
g.PUT("/api/v1/custom-attributes/{id}", perm(handleUpdateCustomAttribute, "custom_attributes:manage"))
196+
g.DELETE("/api/v1/custom-attributes/{id}", perm(handleDeleteCustomAttribute, "custom_attributes:manage"))
197+
183198
// WebSocket.
184199
g.GET("/ws", auth(func(r *fastglue.Request) error {
185200
return handleWS(r, hub)

0 commit comments

Comments
 (0)