Skip to content

Commit 6d2d8f7

Browse files
authored
Merge pull request #130 from abhinavxd/refactor-apis
Clean up APIs and fixes
2 parents 074d147 + 98492a1 commit 6d2d8f7

Some content is hidden

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

79 files changed

+1216
-770
lines changed

cmd/config.go

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package main
2+
3+
import (
4+
"encoding/json"
5+
6+
"github.com/abhinavxd/libredesk/internal/envelope"
7+
"github.com/zerodha/fastglue"
8+
)
9+
10+
// handleGetConfig returns the public configuration needed for app initialization, this includes minimal app settings and enabled SSO providers (without secrets).
11+
func handleGetConfig(r *fastglue.Request) error {
12+
var app = r.Context.(*App)
13+
14+
// Get app settings
15+
settingsJSON, err := app.setting.GetByPrefix("app")
16+
if err != nil {
17+
return sendErrorEnvelope(r, err)
18+
}
19+
20+
// Unmarshal settings
21+
var settings map[string]any
22+
if err := json.Unmarshal(settingsJSON, &settings); err != nil {
23+
app.lo.Error("error unmarshalling settings", "err", err)
24+
return sendErrorEnvelope(r, envelope.NewError(envelope.GeneralError, app.i18n.Ts("globals.messages.errorFetching", "name", app.i18n.T("globals.terms.setting")), nil))
25+
}
26+
27+
// Filter to only include public fields needed for initial app load
28+
publicSettings := map[string]any{
29+
"app.lang": settings["app.lang"],
30+
"app.favicon_url": settings["app.favicon_url"],
31+
"app.logo_url": settings["app.logo_url"],
32+
"app.site_name": settings["app.site_name"],
33+
}
34+
35+
// Get all OIDC providers
36+
oidcProviders, err := app.oidc.GetAll()
37+
if err != nil {
38+
return sendErrorEnvelope(r, err)
39+
}
40+
41+
// Filter for enabled providers and remove client_secret
42+
enabledProviders := make([]map[string]any, 0)
43+
for _, provider := range oidcProviders {
44+
if provider.Enabled {
45+
providerMap := map[string]any{
46+
"id": provider.ID,
47+
"name": provider.Name,
48+
"provider": provider.Provider,
49+
"provider_url": provider.ProviderURL,
50+
"client_id": provider.ClientID,
51+
"logo_url": provider.ProviderLogoURL,
52+
"enabled": provider.Enabled,
53+
"redirect_uri": provider.RedirectURI,
54+
}
55+
enabledProviders = append(enabledProviders, providerMap)
56+
}
57+
}
58+
59+
// Add SSO providers to the response
60+
publicSettings["app.sso_providers"] = enabledProviders
61+
62+
return r.SendEnvelope(publicSettings)
63+
}

cmd/contacts.go

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -164,11 +164,17 @@ func handleUpdateContact(r *fastglue.Request) error {
164164
// Upload avatar?
165165
files, ok := form.File["files"]
166166
if ok && len(files) > 0 {
167-
if err := uploadUserAvatar(r, &contact, files); err != nil {
167+
if err := uploadUserAvatar(r, contact, files); err != nil {
168168
return sendErrorEnvelope(r, err)
169169
}
170170
}
171-
return r.SendEnvelope(true)
171+
172+
// Refetch contact and return it
173+
contact, err = app.user.GetContact(id, "")
174+
if err != nil {
175+
return sendErrorEnvelope(r, err)
176+
}
177+
return r.SendEnvelope(contact)
172178
}
173179

174180
// handleGetContactNotes returns all notes for a contact.
@@ -195,18 +201,21 @@ func handleCreateContactNote(r *fastglue.Request) error {
195201
auser = r.RequestCtx.UserValue("user").(amodels.User)
196202
req = createContactNoteReq{}
197203
)
198-
199204
if err := r.Decode(&req, "json"); err != nil {
200205
return sendErrorEnvelope(r, envelope.NewError(envelope.InputError, app.i18n.Ts("globals.messages.errorParsing", "name", "{globals.terms.request}"), nil))
201206
}
202-
203207
if len(req.Note) == 0 {
204208
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.empty", "name", "note"), nil, envelope.InputError)
205209
}
206-
if err := app.user.CreateNote(contactID, auser.ID, req.Note); err != nil {
210+
n, err := app.user.CreateNote(contactID, auser.ID, req.Note)
211+
if err != nil {
207212
return sendErrorEnvelope(r, err)
208213
}
209-
return r.SendEnvelope(true)
214+
n, err = app.user.GetNote(n.ID)
215+
if err != nil {
216+
return sendErrorEnvelope(r, err)
217+
}
218+
return r.SendEnvelope(n)
210219
}
211220

212221
// handleDeleteContactNote deletes a note for a contact.
@@ -240,6 +249,8 @@ func handleDeleteContactNote(r *fastglue.Request) error {
240249
}
241250
}
242251

252+
app.lo.Info("deleting contact note", "note_id", noteID, "contact_id", contactID, "actor_id", auser.ID)
253+
243254
if err := app.user.DeleteNote(noteID, contactID); err != nil {
244255
return sendErrorEnvelope(r, err)
245256
}
@@ -251,6 +262,7 @@ func handleBlockContact(r *fastglue.Request) error {
251262
var (
252263
app = r.Context.(*App)
253264
contactID, _ = strconv.Atoi(r.RequestCtx.UserValue("id").(string))
265+
auser = r.RequestCtx.UserValue("user").(amodels.User)
254266
req = blockContactReq{}
255267
)
256268

@@ -262,8 +274,15 @@ func handleBlockContact(r *fastglue.Request) error {
262274
return sendErrorEnvelope(r, envelope.NewError(envelope.InputError, app.i18n.Ts("globals.messages.errorParsing", "name", "{globals.terms.request}"), nil))
263275
}
264276

277+
app.lo.Info("setting contact block status", "contact_id", contactID, "enabled", req.Enabled, "actor_id", auser.ID)
278+
265279
if err := app.user.ToggleEnabled(contactID, models.UserTypeContact, req.Enabled); err != nil {
266280
return sendErrorEnvelope(r, err)
267281
}
268-
return r.SendEnvelope(true)
282+
283+
contact, err := app.user.GetContact(contactID, "")
284+
if err != nil {
285+
return sendErrorEnvelope(r, err)
286+
}
287+
return r.SendEnvelope(contact)
269288
}

cmd/conversation.go

Lines changed: 68 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ type createConversationRequest struct {
4949
Subject string `json:"subject"`
5050
Content string `json:"content"`
5151
Attachments []int `json:"attachments"`
52+
Initiator string `json:"initiator"` // "contact" | "agent"
5253
}
5354

5455
// handleGetAllConversations retrieves all conversations.
@@ -273,8 +274,8 @@ func handleGetConversation(r *fastglue.Request) error {
273274
return sendErrorEnvelope(r, err)
274275
}
275276

276-
prev, _ := app.conversation.GetContactConversations(conv.ContactID)
277-
conv.PreviousConversations = filterCurrentConv(prev, conv.UUID)
277+
prev, _ := app.conversation.GetContactPreviousConversations(conv.ContactID, 10)
278+
conv.PreviousConversations = filterCurrentPreviousConv(prev, conv.UUID)
278279
return r.SendEnvelope(conv)
279280
}
280281

@@ -649,14 +650,14 @@ func handleRemoveTeamAssignee(r *fastglue.Request) error {
649650
return r.SendEnvelope(true)
650651
}
651652

652-
// filterCurrentConv removes the current conversation from the list of conversations.
653-
func filterCurrentConv(convs []cmodels.Conversation, uuid string) []cmodels.Conversation {
653+
// filterCurrentPreviousConv removes the current conversation from the list of previous conversations.
654+
func filterCurrentPreviousConv(convs []cmodels.PreviousConversation, uuid string) []cmodels.PreviousConversation {
654655
for i, c := range convs {
655656
if c.UUID == uuid {
656657
return append(convs[:i], convs[i+1:]...)
657658
}
658659
}
659-
return []cmodels.Conversation{}
660+
return []cmodels.PreviousConversation{}
660661
}
661662

662663
// handleCreateConversation creates a new conversation and sends a message to it.
@@ -672,38 +673,16 @@ func handleCreateConversation(r *fastglue.Request) error {
672673
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.errorParsing", "name", "{globals.terms.request}"), nil, envelope.InputError)
673674
}
674675

675-
to := []string{req.Email}
676-
677-
// Validate required fields
678-
if req.InboxID <= 0 {
679-
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.required", "name", "`inbox_id`"), nil, envelope.InputError)
680-
}
681-
if req.Content == "" {
682-
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.required", "name", "`content`"), nil, envelope.InputError)
683-
}
684-
if req.Email == "" {
685-
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.required", "name", "`contact_email`"), nil, envelope.InputError)
686-
}
687-
if req.FirstName == "" {
688-
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.required", "name", "`first_name`"), nil, envelope.InputError)
689-
}
690-
if !stringutil.ValidEmail(req.Email) {
691-
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.invalid", "name", "`contact_email`"), nil, envelope.InputError)
692-
}
693-
694-
user, err := app.user.GetAgent(auser.ID, "")
695-
if err != nil {
676+
// Validate the request
677+
if err := validateCreateConversationRequest(req, app); err != nil {
696678
return sendErrorEnvelope(r, err)
697679
}
698680

699-
// Check if inbox exists and is enabled.
700-
inbox, err := app.inbox.GetDBRecord(req.InboxID)
681+
to := []string{req.Email}
682+
user, err := app.user.GetAgent(auser.ID, "")
701683
if err != nil {
702684
return sendErrorEnvelope(r, err)
703685
}
704-
if !inbox.Enabled {
705-
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.disabled", "name", "inbox"), nil, envelope.InputError)
706-
}
707686

708687
// Find or create contact.
709688
contact := umodels.User{
@@ -717,22 +696,22 @@ func handleCreateConversation(r *fastglue.Request) error {
717696
return sendErrorEnvelope(r, envelope.NewError(envelope.GeneralError, app.i18n.Ts("globals.messages.errorCreating", "name", "{globals.terms.contact}"), nil))
718697
}
719698

720-
// Create conversation
699+
// Create conversation first.
721700
conversationID, conversationUUID, err := app.conversation.CreateConversation(
722701
contact.ID,
723702
contact.ContactChannelID,
724703
req.InboxID,
725704
"", /** last_message **/
726705
time.Now(), /** last_message_at **/
727706
req.Subject,
728-
true, /** append reference number to subject **/
707+
true, /** append reference number to subject? **/
729708
)
730709
if err != nil {
731710
app.lo.Error("error creating conversation", "error", err)
732711
return sendErrorEnvelope(r, envelope.NewError(envelope.GeneralError, app.i18n.Ts("globals.messages.errorCreating", "name", "{globals.terms.conversation}"), nil))
733712
}
734713

735-
// Prepare attachments.
714+
// Get media for the attachment ids.
736715
var media = make([]medModels.Media, 0, len(req.Attachments))
737716
for _, id := range req.Attachments {
738717
m, err := app.media.Get(id, "")
@@ -743,13 +722,29 @@ func handleCreateConversation(r *fastglue.Request) error {
743722
media = append(media, m)
744723
}
745724

746-
// Send reply to the created conversation.
747-
if _, err := app.conversation.SendReply(media, req.InboxID, auser.ID /**sender_id**/, conversationUUID, req.Content, to, nil /**cc**/, nil /**bcc**/, map[string]any{} /**meta**/); err != nil {
748-
// Delete the conversation if reply fails.
749-
if err := app.conversation.DeleteConversation(conversationUUID); err != nil {
750-
app.lo.Error("error deleting conversation", "error", err)
725+
// Send initial message based on the initiator of conversation.
726+
switch req.Initiator {
727+
case umodels.UserTypeAgent:
728+
// Queue reply.
729+
if _, err := app.conversation.QueueReply(media, req.InboxID, auser.ID /**sender_id**/, conversationUUID, req.Content, to, nil /**cc**/, nil /**bcc**/, map[string]any{} /**meta**/); err != nil {
730+
// Delete the conversation if msg queue fails.
731+
if err := app.conversation.DeleteConversation(conversationUUID); err != nil {
732+
app.lo.Error("error deleting conversation", "error", err)
733+
}
734+
return sendErrorEnvelope(r, envelope.NewError(envelope.GeneralError, app.i18n.Ts("globals.messages.errorSending", "name", "{globals.terms.message}"), nil))
751735
}
752-
return sendErrorEnvelope(r, envelope.NewError(envelope.GeneralError, app.i18n.Ts("globals.messages.errorSending", "name", "{globals.terms.message}"), nil))
736+
case umodels.UserTypeContact:
737+
// Create message on behalf of contact.
738+
if _, err := app.conversation.CreateContactMessage(media, contact.ID, conversationUUID, req.Content, cmodels.ContentTypeHTML); err != nil {
739+
// Delete the conversation if message creation fails.
740+
if err := app.conversation.DeleteConversation(conversationUUID); err != nil {
741+
app.lo.Error("error deleting conversation", "error", err)
742+
}
743+
return sendErrorEnvelope(r, envelope.NewError(envelope.GeneralError, app.i18n.Ts("globals.messages.errorSending", "name", "{globals.terms.message}"), nil))
744+
}
745+
default:
746+
// Guard anyway.
747+
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.invalid", "name", "`initiator`"), nil, envelope.InputError)
753748
}
754749

755750
// Assign the conversation to the agent or team.
@@ -768,3 +763,36 @@ func handleCreateConversation(r *fastglue.Request) error {
768763

769764
return r.SendEnvelope(conversation)
770765
}
766+
767+
// validateCreateConversationRequest validates the create conversation request fields.
768+
func validateCreateConversationRequest(req createConversationRequest, app *App) error {
769+
if req.InboxID <= 0 {
770+
return envelope.NewError(envelope.InputError, app.i18n.Ts("globals.messages.required", "name", "`inbox_id`"), nil)
771+
}
772+
if req.Content == "" {
773+
return envelope.NewError(envelope.InputError, app.i18n.Ts("globals.messages.required", "name", "`content`"), nil)
774+
}
775+
if req.Email == "" {
776+
return envelope.NewError(envelope.InputError, app.i18n.Ts("globals.messages.required", "name", "`contact_email`"), nil)
777+
}
778+
if req.FirstName == "" {
779+
return envelope.NewError(envelope.InputError, app.i18n.Ts("globals.messages.required", "name", "`first_name`"), nil)
780+
}
781+
if !stringutil.ValidEmail(req.Email) {
782+
return envelope.NewError(envelope.InputError, app.i18n.Ts("globals.messages.invalid", "name", "`contact_email`"), nil)
783+
}
784+
if req.Initiator != umodels.UserTypeContact && req.Initiator != umodels.UserTypeAgent {
785+
return envelope.NewError(envelope.InputError, app.i18n.Ts("globals.messages.invalid", "name", "`initiator`"), nil)
786+
}
787+
788+
// Check if inbox exists and is enabled.
789+
inbox, err := app.inbox.GetDBRecord(req.InboxID)
790+
if err != nil {
791+
return err
792+
}
793+
if !inbox.Enabled {
794+
return envelope.NewError(envelope.InputError, app.i18n.Ts("globals.messages.disabled", "name", "inbox"), nil)
795+
}
796+
797+
return nil
798+
}

cmd/csat.go

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,16 +17,16 @@ func handleShowCSAT(r *fastglue.Request) error {
1717
if err != nil {
1818
return app.tmpl.RenderWebPage(r.RequestCtx, "error", map[string]interface{}{
1919
"Data": map[string]interface{}{
20-
"ErrorMessage": "Page not found",
20+
"ErrorMessage": app.i18n.T("globals.messages.pageNotFound"),
2121
},
2222
})
2323
}
2424

2525
if csat.ResponseTimestamp.Valid {
2626
return app.tmpl.RenderWebPage(r.RequestCtx, "info", map[string]interface{}{
2727
"Data": map[string]interface{}{
28-
"Title": "Thank you!",
29-
"Message": "We appreciate you taking the time to submit your feedback.",
28+
"Title": app.i18n.T("globals.messages.thankYou"),
29+
"Message": app.i18n.T("csat.thankYouMessage"),
3030
},
3131
})
3232
}
@@ -35,14 +35,14 @@ func handleShowCSAT(r *fastglue.Request) error {
3535
if err != nil {
3636
return app.tmpl.RenderWebPage(r.RequestCtx, "error", map[string]interface{}{
3737
"Data": map[string]interface{}{
38-
"ErrorMessage": "Page not found",
38+
"ErrorMessage": app.i18n.T("globals.messages.pageNotFound"),
3939
},
4040
})
4141
}
4242

4343
return app.tmpl.RenderWebPage(r.RequestCtx, "csat", map[string]interface{}{
4444
"Data": map[string]interface{}{
45-
"Title": "Rate your interaction with us",
45+
"Title": app.i18n.T("csat.pageTitle"),
4646
"CSAT": map[string]interface{}{
4747
"UUID": csat.UUID,
4848
},
@@ -67,23 +67,23 @@ func handleUpdateCSATResponse(r *fastglue.Request) error {
6767
if err != nil {
6868
return app.tmpl.RenderWebPage(r.RequestCtx, "error", map[string]interface{}{
6969
"Data": map[string]interface{}{
70-
"ErrorMessage": "Invalid `rating`",
70+
"ErrorMessage": app.i18n.T("globals.messages.somethingWentWrong"),
7171
},
7272
})
7373
}
7474

7575
if ratingI < 1 || ratingI > 5 {
7676
return app.tmpl.RenderWebPage(r.RequestCtx, "error", map[string]interface{}{
7777
"Data": map[string]interface{}{
78-
"ErrorMessage": "Invalid `rating`",
78+
"ErrorMessage": app.i18n.T("globals.messages.somethingWentWrong"),
7979
},
8080
})
8181
}
8282

8383
if uuid == "" {
8484
return app.tmpl.RenderWebPage(r.RequestCtx, "error", map[string]interface{}{
8585
"Data": map[string]interface{}{
86-
"ErrorMessage": "Invalid `uuid`",
86+
"ErrorMessage": app.i18n.T("globals.messages.somethingWentWrong"),
8787
},
8888
})
8989
}
@@ -98,8 +98,8 @@ func handleUpdateCSATResponse(r *fastglue.Request) error {
9898

9999
return app.tmpl.RenderWebPage(r.RequestCtx, "info", map[string]interface{}{
100100
"Data": map[string]interface{}{
101-
"Title": "Thank you!",
102-
"Message": "We appreciate you taking the time to submit your feedback.",
101+
"Title": app.i18n.T("globals.messages.thankYou"),
102+
"Message": app.i18n.T("csat.thankYouMessage"),
103103
},
104104
})
105105
}

0 commit comments

Comments
 (0)