diff --git a/cmd/main.go b/cmd/main.go index 16adb89..8c204fa 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -46,7 +46,7 @@ var ( ko = koanf.New(".") frontendDir = "frontend/dist" ctx = context.Background() - buildString string + buildString = "" ) // App is the global app context which is passed and injected in the http handlers. @@ -103,7 +103,7 @@ func main() { os.Exit(0) } - log.Printf("Build: %s", buildString) + colorlog.Green("Build: %s", buildString) // Installer. if ko.Bool("install") { @@ -132,9 +132,12 @@ func main() { loadSettings(settings) var ( + autoAssignInterval = ko.MustDuration("autoassigner.interval") + unsnoozeInterval = ko.MustDuration("conversation.unsnooze_interval") automationWrk = ko.MustInt("automation.worker_count") - messageDispatchWrk = ko.MustInt("message.dispatch_workers") - messageDispatchScanInterval = ko.MustDuration("message.dispatch_scan_interval") + messageOutgoingQWorkers = ko.MustDuration("message.outgoing_queue_workers") + messageIncomingQWorkers = ko.MustDuration("message.incoming_queue_workers") + messageOutgoingScanInterval = ko.MustDuration("message.message_outoing_scan_interval") lo = initLogger("libredesk") wsHub = ws.NewHub() rdb = initRedis() @@ -157,34 +160,17 @@ func main() { autoassigner = initAutoAssigner(team, user, conversation) ) - // Set store. automation.SetConversationStore(conversation) - - // Start inbox receivers. startInboxes(ctx, inbox, conversation) - // Start evaluating automation rules. go automation.Run(ctx, automationWrk) - - // Start conversation auto assigner. - go autoassigner.Run(ctx) - - // Start processing incoming and outgoing messages. - go conversation.Run(ctx, messageDispatchWrk, messageDispatchScanInterval) - - // Run the unsnoozer. - go conversation.RunUnsnoozer(ctx) - - // Start notifier. + go autoassigner.Run(ctx, autoAssignInterval) + go conversation.Run(ctx, messageIncomingQWorkers, messageOutgoingQWorkers, messageOutgoingScanInterval) + go conversation.RunUnsnoozer(ctx, unsnoozeInterval) + go media.DeleteUnlinkedMedia(ctx) go notifier.Run(ctx) - - // Start SLA monitor. go sla.Run(ctx) - // Purge unlinked message media. - go media.DeleteUnlinkedMessageMedia(ctx) - - // Init the app var app = &App{ lo: lo, fs: fs, @@ -215,17 +201,12 @@ func main() { ai: initAI(db), } - // Init fastglue and set app in ctx. g := fastglue.NewGlue() - - // Set the app in context. g.SetContext(app) - - // Init HTTP handlers. initHandlers(g, wsHub) s := &fasthttp.Server{ - Name: "libredesk", + Name: "LibreDesk", ReadTimeout: ko.MustDuration("app.server.read_timeout"), WriteTimeout: ko.MustDuration("app.server.write_timeout"), MaxRequestBodySize: ko.MustInt("app.server.max_body_size"), @@ -239,16 +220,14 @@ func main() { } }() - colorlog.Green("🚀 server listening on %s %s", ko.String("app.server.address"), ko.String("app.server.socket")) + colorlog.Green("🚀 listening on %s %s", ko.String("app.server.address"), ko.String("app.server.socket")) // Wait for shutdown signal. <-ctx.Done() colorlog.Red("Shutting down the server. Please wait....") - // Shutdown HTTP server. s.Shutdown() colorlog.Red("Server shutdown complete.") colorlog.Red("Shutting down services. Please wait....") - // Shutdown services. inbox.Close() colorlog.Red("Inbox shutdown complete.") automation.Close() diff --git a/frontend/src/components/sidebar/Sidebar.vue b/frontend/src/components/sidebar/Sidebar.vue index 0c352e2..fbb219e 100644 --- a/frontend/src/components/sidebar/Sidebar.vue +++ b/frontend/src/components/sidebar/Sidebar.vue @@ -33,6 +33,12 @@ import { Search, MessageCircle } from 'lucide-vue-next' +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu' import { computed } from 'vue' import { useUserStore } from '@/stores/user' import { useConversationStore } from '@/stores/conversation' diff --git a/frontend/src/views/UserLoginView.vue b/frontend/src/views/UserLoginView.vue index 137be0a..01415ca 100644 --- a/frontend/src/views/UserLoginView.vue +++ b/frontend/src/views/UserLoginView.vue @@ -63,12 +63,12 @@
- - + -->
Forgot password? diff --git a/internal/autoassigner/autoassigner.go b/internal/autoassigner/autoassigner.go index a934f73..44913f9 100644 --- a/internal/autoassigner/autoassigner.go +++ b/internal/autoassigner/autoassigner.go @@ -4,13 +4,13 @@ package autoassigner import ( "context" "errors" + "fmt" "strconv" "sync" "time" - "github.com/abhinavxd/libredesk/internal/conversation" "github.com/abhinavxd/libredesk/internal/conversation/models" - "github.com/abhinavxd/libredesk/internal/team" + tmodels "github.com/abhinavxd/libredesk/internal/team/models" umodels "github.com/abhinavxd/libredesk/internal/user/models" "github.com/mr-karan/balance" "github.com/zerodha/logf" @@ -24,6 +24,16 @@ const ( AssignmentTypeRoundRobin = "Round robin" ) +type conversationStore interface { + GetUnassignedConversations() ([]models.Conversation, error) + UpdateConversationUserAssignee(conversationUUID string, userID int, user umodels.User) error +} + +type teamStore interface { + GetAll() ([]tmodels.Team, error) + GetMembers(teamID int) ([]umodels.User, error) +} + // Engine represents a manager for assigning unassigned conversations // to team agents in a round-robin pattern. type Engine struct { @@ -32,23 +42,23 @@ type Engine struct { // Mutex to protect the balancer map balanceMu sync.Mutex - systemUser umodels.User - conversationManager *conversation.Manager - teamManager *team.Manager - lo *logf.Logger - closed bool - closedMu sync.Mutex - wg sync.WaitGroup + systemUser umodels.User + conversationStore conversationStore + teamStore teamStore + lo *logf.Logger + closed bool + closedMu sync.Mutex + wg sync.WaitGroup } // New initializes a new Engine instance, set up with the provided team manager, // conversation manager, and logger. -func New(teamManager *team.Manager, conversationManager *conversation.Manager, systemUser umodels.User, lo *logf.Logger) (*Engine, error) { +func New(teamStore teamStore, conversationStore conversationStore, systemUser umodels.User, lo *logf.Logger) (*Engine, error) { var e = Engine{ - conversationManager: conversationManager, - teamManager: teamManager, - systemUser: systemUser, - lo: lo, + conversationStore: conversationStore, + teamStore: teamStore, + systemUser: systemUser, + lo: lo, } balancer, err := e.populateTeamBalancer() if err != nil { @@ -60,8 +70,8 @@ func New(teamManager *team.Manager, conversationManager *conversation.Manager, s // Run initiates the conversation assignment process and is to be invoked as a goroutine. // This function continuously assigns unassigned conversations to agents at regular intervals. -func (e *Engine) Run(ctx context.Context) { - ticker := time.NewTicker(60 * time.Second) +func (e *Engine) Run(ctx context.Context, autoAssignInterval time.Duration) { + ticker := time.NewTicker(autoAssignInterval) defer ticker.Stop() e.wg.Add(1) @@ -78,6 +88,9 @@ func (e *Engine) Run(ctx context.Context) { if closed { return } + if err := e.reloadBalancer(); err != nil { + e.lo.Error("error reloading balancer", "error", err) + } if err := e.assignConversations(); err != nil { e.lo.Error("error assigning conversations", "error", err) } @@ -97,14 +110,14 @@ func (e *Engine) Close() { e.wg.Wait() } -// RefreshBalancer updates the round-robin balancer with the latest user and team data. -func (e *Engine) RefreshBalancer() error { +// reloadBalancer updates the round-robin balancer with the latest user and team data. +func (e *Engine) reloadBalancer() error { e.balanceMu.Lock() defer e.balanceMu.Unlock() balancer, err := e.populateTeamBalancer() if err != nil { - e.lo.Error("Error updating team balancer pool", "error", err) + e.lo.Error("error updating team balancer pool", "error", err) return err } e.roundRobinBalancer = balancer @@ -114,10 +127,9 @@ func (e *Engine) RefreshBalancer() error { // populateTeamBalancer populates the team balancer pool with the team members. func (e *Engine) populateTeamBalancer() (map[int]*balance.Balance, error) { var ( - balancer = make(map[int]*balance.Balance) + balancer = make(map[int]*balance.Balance) + teams, err = e.teamStore.GetAll() ) - - teams, err := e.teamManager.GetAll() if err != nil { return nil, err } @@ -127,12 +139,12 @@ func (e *Engine) populateTeamBalancer() (map[int]*balance.Balance, error) { continue } - users, err := e.teamManager.GetMembers(team.ID) + users, err := e.teamStore.GetMembers(team.ID) if err != nil { return nil, err } - // Add the users to team balancer pool. + // Add users to team's balancer pool. for _, user := range users { if _, ok := balancer[team.ID]; !ok { balancer[team.ID] = balance.NewBalance() @@ -146,30 +158,35 @@ func (e *Engine) populateTeamBalancer() (map[int]*balance.Balance, error) { // assignConversations function fetches conversations that have been assigned to teams but not to any individual user, // and then proceeds to assign them to team members based on a round-robin strategy. func (e *Engine) assignConversations() error { - unassigned, err := e.conversationManager.GetUnassignedConversations() + unassignedConversations, err := e.conversationStore.GetUnassignedConversations() if err != nil { - return err + return fmt.Errorf("fetching unassigned conversations: %w", err) } - e.lo.Debug("found unassigned conversations", "count", len(unassigned)) + if len(unassignedConversations) > 0 { + e.lo.Debug("found unassigned conversations", "count", len(unassignedConversations)) + } - for _, conversation := range unassigned { + for _, conversation := range unassignedConversations { // Get user from the pool. userIDStr, err := e.getUserFromPool(conversation) if err != nil { - e.lo.Error("error fetching user from balancer pool", "error", err) + e.lo.Error("error fetching user from balancer pool", "conversation_uuid", conversation.UUID, "error", err) continue } // Convert to int. userID, err := strconv.Atoi(userIDStr) if err != nil { - e.lo.Error("error converting user id from string to int", "error", err) + e.lo.Error("error converting user id from string to int", "user_id", userIDStr, "error", err) continue } // Assign conversation. - e.conversationManager.UpdateConversationUserAssignee(conversation.UUID, userID, e.systemUser) + if err := e.conversationStore.UpdateConversationUserAssignee(conversation.UUID, userID, e.systemUser); err != nil { + e.lo.Error("error assigning conversation", "conversation_uuid", conversation.UUID, "error", err) + continue + } } return nil } diff --git a/internal/conversation/conversation.go b/internal/conversation/conversation.go index 16238c8..81e1d69 100644 --- a/internal/conversation/conversation.go +++ b/internal/conversation/conversation.go @@ -1,4 +1,4 @@ -// Package conversation provides functionality to manage conversations in the system. +// Package conversation manages conversations and messages. package conversation import ( @@ -163,7 +163,6 @@ type queries struct { // Conversation queries. GetLatestReceivedMessageSourceID *sqlx.Stmt `query:"get-latest-received-message-source-id"` GetToAddress *sqlx.Stmt `query:"get-to-address"` - GetConversationID *sqlx.Stmt `query:"get-conversation-id"` GetConversationUUID *sqlx.Stmt `query:"get-conversation-uuid"` GetConversation *sqlx.Stmt `query:"get-conversation"` GetConversationsCreatedAfter *sqlx.Stmt `query:"get-conversations-created-after"` @@ -177,8 +176,7 @@ type queries struct { UpdateConversationPriority *sqlx.Stmt `query:"update-conversation-priority"` UpdateConversationStatus *sqlx.Stmt `query:"update-conversation-status"` UpdateConversationLastMessage *sqlx.Stmt `query:"update-conversation-last-message"` - UpdateConversationMeta *sqlx.Stmt `query:"update-conversation-meta"` - InsertConverstionParticipant *sqlx.Stmt `query:"insert-conversation-participant"` + InsertConversationParticipant *sqlx.Stmt `query:"insert-conversation-participant"` InsertConversation *sqlx.Stmt `query:"insert-conversation"` UpsertConversationTags *sqlx.Stmt `query:"upsert-conversation-tags"` UnassignOpenConversations *sqlx.Stmt `query:"unassign-open-conversations"` @@ -269,19 +267,6 @@ func (c *Manager) GetUnassignedConversations() ([]models.Conversation, error) { return conv, nil } -// GetConversationID retrieves the ID of a conversation by its UUID. -func (c *Manager) GetConversationID(uuid string) (int, error) { - var id int - if err := c.q.GetConversationID.QueryRow(uuid).Scan(&id); err != nil { - if err == sql.ErrNoRows { - return id, err - } - c.lo.Error("fetching conversation from DB", "error", err) - return id, err - } - return id, nil -} - // GetConversationUUID retrieves the UUID of a conversation by its ID. func (c *Manager) GetConversationUUID(id int) (string, error) { var uuid string @@ -353,20 +338,6 @@ func (c *Manager) GetConversations(userID int, listType, order, orderBy, filters return conversations, nil } -// UpdateConversationMeta updates the metadata of a conversation. -func (c *Manager) UpdateConversationMeta(conversationID int, conversationUUID string, meta map[string]string) error { - metaJSON, err := json.Marshal(meta) - if err != nil { - c.lo.Error("error marshalling conversation meta", "meta", meta, "error", err) - return err - } - if _, err := c.q.UpdateConversationMeta.Exec(conversationID, conversationUUID, metaJSON); err != nil { - c.lo.Error("error updating conversation meta", "error", "error") - return err - } - return nil -} - // UpdateConversationLastMessage updates the last message details for a conversation. func (c *Manager) UpdateConversationLastMessage(convesationID int, conversationUUID, lastMessage string, lastMessageAt time.Time) error { if _, err := c.q.UpdateConversationLastMessage.Exec(convesationID, conversationUUID, lastMessage, lastMessageAt); err != nil { @@ -403,7 +374,7 @@ func (c *Manager) UpdateConversationUserAssignee(uuid string, assigneeID int, ac } // Send email to assignee. - if err := c.SendAssignedConversationEmail([]int{assigneeID}, conversation.Subject.String, uuid); err != nil { + if err := c.SendAssignedConversationEmail([]int{assigneeID}, conversation); err != nil { c.lo.Error("error sending assigned conversation email", "error", err) } @@ -638,14 +609,13 @@ func (c *Manager) makeConversationsListQuery(userID int, baseQuery, listType, or // Build the paginated query. query, qArgs, err := dbutil.BuildPaginatedQuery(baseQuery, qArgs, dbutil.PaginationOptions{ Order: order, - OrderBy: "", + OrderBy: orderBy, Page: page, PageSize: pageSize, }, filtersJSON, dbutil.AllowedFields{ "conversations": ConversationsListAllowedFilterFields, "conversation_statuses": ConversationStatusesFilterFields, }) - fmt.Println("Query: ", query) if err != nil { c.lo.Error("error preparing query", "error", err) return "", nil, err @@ -674,16 +644,27 @@ func (m *Manager) GetLatestReceivedMessageSourceID(conversationID int) (string, } // SendAssignedConversationEmail sends a email for an assigned conversation to the passed user ids. -func (m *Manager) SendAssignedConversationEmail(userIDs []int, subject, conversationUUID string) error { +func (m *Manager) SendAssignedConversationEmail(userIDs []int, conversation models.Conversation) error { + agent, err := m.userStore.Get(userIDs[0]) + if err != nil { + m.lo.Error("error fetching agent", "error", err) + return fmt.Errorf("fetching agent: %w", err) + } + content, subject, err := m.template.RenderNamedTemplate(template.TmplConversationAssigned, map[string]interface{}{ "conversation": map[string]string{ - "subject": subject, - "uuid": conversationUUID, + "subject": conversation.Subject.String, + "uuid": conversation.UUID, + "reference_number": conversation.ReferenceNumber, + "priority": conversation.Priority.String, + }, + "agent": map[string]string{ + "full_name": agent.FullName(), }, }) if err != nil { - m.lo.Error("error rendering template", "template", template.TmplConversationAssigned, "conversation_uuid", conversationUUID, "error", err) + m.lo.Error("error rendering template", "template", template.TmplConversationAssigned, "conversation_uuid", conversation.UUID, "error", err) return fmt.Errorf("rendering template: %w", err) } nm := notifier.Message{ @@ -784,7 +765,7 @@ func (m *Manager) ApplyAction(action amodels.RuleAction, conversation models.Con // addConversationParticipant adds a user as participant to a conversation. func (c *Manager) addConversationParticipant(userID int, conversationUUID string) error { - if _, err := c.q.InsertConverstionParticipant.Exec(userID, conversationUUID); err != nil { + if _, err := c.q.InsertConversationParticipant.Exec(userID, conversationUUID); err != nil { if !dbutil.IsUniqueViolationError(err) { c.lo.Error("error adding conversation participant", "user_id", userID, "conversation_uuid", conversationUUID, "error", err) return fmt.Errorf("adding conversation participant: %w", err) diff --git a/internal/conversation/message.go b/internal/conversation/message.go index fcbc4b8..fcfbc3c 100644 --- a/internal/conversation/message.go +++ b/internal/conversation/message.go @@ -49,25 +49,24 @@ const ( // Run starts a pool of worker goroutines to handle message dispatching via inbox's channel and processes incoming messages. It scans for // pending outgoing messages at the specified read interval and pushes them to the outgoing queue. -func (m *Manager) Run(ctx context.Context, dispatchConcurrency int, scanInterval time.Duration) { +func (m *Manager) Run(ctx context.Context, incomingQWorkers, outgoingQWorkers, scanInterval time.Duration) { dbScanner := time.NewTicker(scanInterval) defer dbScanner.Stop() - // Spawn a worker goroutine pool to dispatch messages. - for range dispatchConcurrency { + for range outgoingQWorkers { m.wg.Add(1) go func() { defer m.wg.Done() m.MessageDispatchWorker(ctx) }() } - - // Spawn a goroutine to process incoming messages. - m.wg.Add(1) - go func() { - defer m.wg.Done() - m.IncomingMessageWorker(ctx) - }() + for range incomingQWorkers { + m.wg.Add(1) + go func() { + defer m.wg.Done() + m.IncomingMessageWorker(ctx) + }() + } // Scan pending outgoing messages and send them. for { diff --git a/internal/conversation/queries.sql b/internal/conversation/queries.sql index 363d716..00bf086 100644 --- a/internal/conversation/queries.sql +++ b/internal/conversation/queries.sql @@ -11,36 +11,40 @@ RETURNING id, uuid; -- name: get-conversations SELECT - COUNT(*) OVER() AS total, - conversations.created_at, - conversations.updated_at, - conversations.uuid, - conversations.assignee_last_seen_at, - users.first_name as "contact.first_name", - users.last_name as "contact.last_name", - users.avatar_url as "contact.avatar_url", - inboxes.channel as inbox_channel, - inboxes.name as inbox_name, - conversations.sla_policy_id, - conversations.first_reply_at, - conversations.resolved_at, - conversations.subject, - conversations.last_message, - conversations.last_message_at, - conversations.next_sla_deadline_at, - conversations.priority_id, - ( - SELECT COUNT(*) - FROM conversation_messages m - WHERE m.conversation_id = conversations.id AND m.created_at > conversations.assignee_last_seen_at - ) AS unread_message_count, - conversation_statuses.name as status, - conversation_priorities.name as priority +COUNT(*) OVER() as total, +conversations.created_at, +conversations.updated_at, +conversations.uuid, +conversations.assignee_last_seen_at, +users.first_name as "contact.first_name", +users.last_name as "contact.last_name", +users.avatar_url as "contact.avatar_url", +inboxes.channel as inbox_channel, +inboxes.name as inbox_name, +conversations.sla_policy_id, +conversations.first_reply_at, +conversations.resolved_at, +conversations.subject, +conversations.last_message, +conversations.last_message_at, +conversations.next_sla_deadline_at, +conversations.priority_id, +( + SELECT CASE WHEN COUNT(*) > 9 THEN 10 ELSE COUNT(*) END + FROM ( + SELECT 1 FROM conversation_messages + WHERE conversation_id = conversations.id + AND created_at > conversations.assignee_last_seen_at + LIMIT 10 + ) t +) as unread_message_count, +conversation_statuses.name as status, +conversation_priorities.name as priority FROM conversations - JOIN users ON conversations.contact_id = users.id - JOIN inboxes ON conversations.inbox_id = inboxes.id - LEFT JOIN conversation_statuses ON conversations.status_id = conversation_statuses.id - LEFT JOIN conversation_priorities ON conversations.priority_id = conversation_priorities.id +JOIN users ON contact_id = users.id +JOIN inboxes ON inbox_id = inboxes.id +LEFT JOIN conversation_statuses ON status_id = conversation_statuses.id +LEFT JOIN conversation_priorities ON priority_id = conversation_priorities.id WHERE 1=1 %s -- name: get-conversation @@ -115,9 +119,6 @@ LEFT JOIN conversation_statuses s ON c.status_id = s.id LEFT JOIN conversation_priorities p ON c.priority_id = p.id WHERE c.created_at > $1; --- name: get-conversation-id -SELECT id from conversations where uuid = $1; - -- name: get-conversation-uuid SELECT uuid from conversations where id = $1; @@ -169,10 +170,11 @@ SET assignee_last_seen_at = now(), updated_at = now() WHERE uuid = $1; --- name: update-conversation-meta -UPDATE conversations -SET meta = meta || $3, updated_at = now() -WHERE CASE WHEN $1 > 0 THEN id = $1 ELSE uuid = $2 END; +-- name: update-conversation-last-message +UPDATE conversations SET last_message = $3, last_message_at = $4 WHERE CASE + WHEN $1 > 0 THEN id = $1 + ELSE uuid = $2 +END -- name: get-conversation-participants SELECT users.id as id, first_name, last_name, avatar_url @@ -183,12 +185,6 @@ WHERE conversation_id = SELECT id FROM conversations WHERE uuid = $1 ); --- name: update-conversation-last-message -UPDATE conversations SET last_message = $3, last_message_at = $4 WHERE CASE - WHEN $1 > 0 THEN id = $1 - ELSE uuid = $2 -END - -- name: insert-conversation-participant INSERT INTO conversation_participants (user_id, conversation_id) @@ -196,30 +192,14 @@ VALUES($1, (SELECT id FROM conversations WHERE uuid = $2)); -- name: get-unassigned-conversations SELECT + c.created_at, c.updated_at, c.uuid, - c.assignee_last_seen_at, c.assigned_team_id, inb.channel as inbox_channel, - inb.name as inbox_name, - ct.first_name, - ct.last_name, - ct.avatar_url, - c.subject, - c.last_message, - c.last_message_at, - ( - SELECT COUNT(*) - FROM conversation_messages m - WHERE m.conversation_id = c.id AND m.created_at > c.assignee_last_seen_at - ) AS unread_message_count, - s.name as status, - p.name as priority + inb.name as inbox_name FROM conversations c - JOIN users ct ON c.contact_id = ct.id JOIN inboxes inb ON c.inbox_id = inb.id - LEFT JOIN conversation_statuses s ON c.status_id = s.id - LEFT JOIN conversation_priorities p ON c.priority_id = p.id WHERE assigned_user_id IS NULL AND assigned_team_id IS NOT NULL; -- name: get-dashboard-counts @@ -231,7 +211,7 @@ SELECT json_build_object( ) FROM conversations c INNER JOIN conversation_statuses s ON c.status_id = s.id -WHERE s.name = 'Open' AND 1=1 %s; +WHERE s.name not in ('Resolved', 'Closed') AND 1=1 %s; -- name: get-dashboard-charts WITH new_conversations AS ( @@ -405,46 +385,36 @@ GROUP BY ORDER BY m.created_at; -- name: get-messages -WITH conversation_id AS ( - SELECT id - FROM conversations - WHERE uuid = $1 - LIMIT 1 -), -attachments AS ( - SELECT - model_id as message_id, - json_agg( - json_build_object( - 'name', filename, - 'content_type', content_type, - 'uuid', uuid, - 'size', size, - 'content_id', content_id, - 'disposition', disposition - ) ORDER BY filename - ) AS attachment_details - FROM media - WHERE model_type = 'messages' - GROUP BY message_id -) SELECT - COUNT(*) OVER() AS total, - m.created_at, - m.updated_at, - m.status, - m.type, - m.content, - m.uuid, - m.private, - m.sender_id, - m.sender_type, - m.meta, - COALESCE(a.attachment_details, '[]'::json) AS attachments + COUNT(*) OVER() AS total, + m.created_at, + m.updated_at, + m.status, + m.type, + m.content, + m.uuid, + m.private, + m.sender_id, + m.sender_type, + m.meta, + COALESCE( + (SELECT json_agg( + json_build_object( + 'name', filename, + 'content_type', content_type, + 'uuid', uuid, + 'size', size, + 'content_id', content_id, + 'disposition', disposition + ) ORDER BY filename + ) FROM media + WHERE model_type = 'messages' AND model_id = m.id), + '[]'::json) AS attachments FROM conversation_messages m -LEFT JOIN attachments a ON a.message_id = m.id -WHERE m.conversation_id = (SELECT id FROM conversation_id) ORDER BY m.created_at DESC -%s +WHERE m.conversation_id = ( + SELECT id FROM conversations WHERE uuid = $1 LIMIT 1 +) +ORDER BY m.created_at DESC %s -- name: insert-message WITH conversation_id AS ( diff --git a/internal/conversation/unsnoozer.go b/internal/conversation/unsnoozer.go index 90246e4..97d2b2e 100644 --- a/internal/conversation/unsnoozer.go +++ b/internal/conversation/unsnoozer.go @@ -7,8 +7,8 @@ import ( ) // RunUnsnoozer runs the conversation unsnoozer. -func (c *Manager) RunUnsnoozer(ctx context.Context) { - ticker := time.NewTicker(5 * time.Minute) +func (c *Manager) RunUnsnoozer(ctx context.Context, unsnoozeInterval time.Duration) { + ticker := time.NewTicker(unsnoozeInterval) defer ticker.Stop() for { select { diff --git a/internal/inbox/inbox.go b/internal/inbox/inbox.go index 3000f2b..0af0b57 100644 --- a/internal/inbox/inbox.go +++ b/internal/inbox/inbox.go @@ -162,7 +162,7 @@ func (m *Manager) GetAll() ([]imodels.Inbox, error) { // Create creates an inbox in the DB. func (m *Manager) Create(inbox imodels.Inbox) error { - if _, err := m.queries.InsertInbox.Exec(inbox.Channel, inbox.Config, inbox.Name, inbox.From); err != nil { + if _, err := m.queries.InsertInbox.Exec(inbox.Channel, inbox.Config, inbox.Name, inbox.From, inbox.CSATEnabled); err != nil { m.lo.Error("error creating inbox", "error", err) return envelope.NewError(envelope.GeneralError, "Error creating inbox", nil) } @@ -300,7 +300,7 @@ func (m *Manager) Update(id int, inbox imodels.Inbox) error { inbox.Config = updatedConfig } - if _, err := m.queries.Update.Exec(id, inbox.Channel, inbox.Config, inbox.Name, inbox.From); err != nil { + if _, err := m.queries.Update.Exec(id, inbox.Channel, inbox.Config, inbox.Name, inbox.From, inbox.CSATEnabled); err != nil { m.lo.Error("error updating inbox", "error", err) return envelope.NewError(envelope.GeneralError, "Error updating inbox", nil) } diff --git a/internal/inbox/queries.sql b/internal/inbox/queries.sql index a11f635..d541f6e 100644 --- a/internal/inbox/queries.sql +++ b/internal/inbox/queries.sql @@ -6,15 +6,15 @@ SELECT id, name, channel, disabled, updated_at from inboxes where deleted_at is -- name: insert-inbox INSERT INTO inboxes -(channel, config, "name", "from") -VALUES($1, $2, $3, $4); +(channel, config, "name", "from", csat_enabled) +VALUES($1, $2, $3, $4, $5) -- name: get-inbox SELECT * from inboxes where id = $1 and deleted_at is NULL; -- name: update UPDATE inboxes -set channel = $2, config = $3, "name" = $4, "from" = $5, updated_at = now() +set channel = $2, config = $3, "name" = $4, "from" = $5, csat_enabled = $6, updated_at = now() where id = $1 and deleted_at is NULL; -- name: soft-delete diff --git a/internal/media/media.go b/internal/media/media.go index 1b15119..3cd6a01 100644 --- a/internal/media/media.go +++ b/internal/media/media.go @@ -197,14 +197,14 @@ func (m *Manager) DeleteByUUID(uuid string) error { return nil } -// DeleteUnlinkedMessageMedia is a blocking function that periodically deletes media files that are not linked to any conversation message. -func (m *Manager) DeleteUnlinkedMessageMedia(ctx context.Context) { +// DeleteUnlinkedMedia is a blocking function that periodically deletes media files that are not linked to any conversation message. +func (m *Manager) DeleteUnlinkedMedia(ctx context.Context) { m.deleteUnlinkedMessageMedia() for { select { case <-ctx.Done(): return - case <-time.After(24 * time.Hour): + case <-time.After(2 * time.Hour): m.lo.Info("deleting unlinked message media") if err := m.deleteUnlinkedMessageMedia(); err != nil { m.lo.Error("error deleting unlinked media", "error", err)