mirror of
				https://github.com/abhinavxd/libredesk.git
				synced 2025-11-04 05:53:30 +00:00 
			
		
		
		
	refactor.
This commit is contained in:
		@@ -1,8 +1,6 @@
 | 
			
		||||
package main
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"net/http"
 | 
			
		||||
 | 
			
		||||
	"github.com/zerodha/fastglue"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
@@ -12,7 +10,7 @@ func handleGetCannedResponses(r *fastglue.Request) error {
 | 
			
		||||
	)
 | 
			
		||||
	c, err := app.cannedRespManager.GetAll()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return r.SendErrorEnvelope(http.StatusInternalServerError, "Error fetching canned responses", nil, "")
 | 
			
		||||
		return sendErrorEnvelope(r, err)
 | 
			
		||||
	}
 | 
			
		||||
	return r.SendEnvelope(c)
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -15,18 +15,20 @@ func initHandlers(g *fastglue.Fastglue, hub *ws.Hub) {
 | 
			
		||||
	g.POST("/api/login", handleLogin)
 | 
			
		||||
	g.GET("/api/logout", handleLogout)
 | 
			
		||||
 | 
			
		||||
	g.GET("/api/settings", auth(handleGetSettings))
 | 
			
		||||
 | 
			
		||||
	// Conversation.
 | 
			
		||||
	g.GET("/api/conversations/all", auth(handleGetAllConversations, "conversations:all"))
 | 
			
		||||
	g.GET("/api/conversations/team", auth(handleGetTeamConversations, "conversations:team"))
 | 
			
		||||
	g.GET("/api/conversations/assigned", auth(handleGetAssignedConversations, "conversations:assigned"))	
 | 
			
		||||
	g.GET("/api/conversations/all", auth(handleGetAllConversations, "conversation:all"))
 | 
			
		||||
	g.GET("/api/conversations/team", auth(handleGetTeamConversations, "conversation:team"))
 | 
			
		||||
	g.GET("/api/conversations/assigned", auth(handleGetAssignedConversations, "conversation:assigned"))
 | 
			
		||||
 | 
			
		||||
	g.GET("/api/conversations/{uuid}", auth(handleGetConversation))
 | 
			
		||||
	g.GET("/api/conversations/{uuid}/participants", auth(handleGetConversationParticipants))
 | 
			
		||||
	g.PUT("/api/conversations/{uuid}/last-seen", auth(handleUpdateAssigneeLastSeen))
 | 
			
		||||
	g.PUT("/api/conversations/{uuid}/assignee/user", auth(handleUpdateUserAssignee))
 | 
			
		||||
	g.PUT("/api/conversations/{uuid}/assignee/team", auth(handleUpdateTeamAssignee))
 | 
			
		||||
	g.PUT("/api/conversations/{uuid}/priority", auth(handleUpdatePriority))
 | 
			
		||||
	g.PUT("/api/conversations/{uuid}/status", auth(handleUpdateStatus))
 | 
			
		||||
	g.PUT("/api/conversations/{uuid}/priority", auth(handleUpdatePriority, "conversation:edit_priority"))
 | 
			
		||||
	g.PUT("/api/conversations/{uuid}/status", auth(handleUpdateStatus, "conversation:edit_status"))
 | 
			
		||||
	g.POST("/api/conversations/{uuid}/tags", auth(handleAddConversationTags))
 | 
			
		||||
 | 
			
		||||
	// Message.
 | 
			
		||||
@@ -45,23 +47,23 @@ func initHandlers(g *fastglue.Fastglue, hub *ws.Hub) {
 | 
			
		||||
	g.POST("/api/file/upload", auth(handleFileUpload))
 | 
			
		||||
 | 
			
		||||
	// User.
 | 
			
		||||
	g.GET("/api/users/me", auth(handleGetCurrentUser))
 | 
			
		||||
	g.GET("/api/users", auth(handleGetUsers))
 | 
			
		||||
	g.GET("/api/users/{id}", auth(handleGetUser))
 | 
			
		||||
	g.PUT("/api/users/{id}", auth(handleUpdateUser))
 | 
			
		||||
	g.POST("/api/users", auth(handleCreateUser))
 | 
			
		||||
	g.GET("/api/users/me", auth(handleGetCurrentUser, "users:manage"))
 | 
			
		||||
	g.GET("/api/users", auth(handleGetUsers, "users:manage"))
 | 
			
		||||
	g.GET("/api/users/{id}", auth(handleGetUser, "users:manage"))
 | 
			
		||||
	g.PUT("/api/users/{id}", auth(handleUpdateUser, "users:manage"))
 | 
			
		||||
	g.POST("/api/users", auth(handleCreateUser, "users:manage"))
 | 
			
		||||
 | 
			
		||||
	// Team.
 | 
			
		||||
	g.GET("/api/teams", auth(handleGetTeams))
 | 
			
		||||
	g.GET("/api/teams/{id}", auth(handleGetTeam))
 | 
			
		||||
	g.PUT("/api/teams/{id}", auth(handleUpdateTeam))
 | 
			
		||||
	g.POST("/api/teams", auth(handleCreateTeam))
 | 
			
		||||
	g.GET("/api/teams", auth(handleGetTeams, "teams:manage"))
 | 
			
		||||
	g.GET("/api/teams/{id}", auth(handleGetTeam, "teams:manage"))
 | 
			
		||||
	g.PUT("/api/teams/{id}", auth(handleUpdateTeam, "teams:manage"))
 | 
			
		||||
	g.POST("/api/teams", auth(handleCreateTeam, "teams:manage"))
 | 
			
		||||
 | 
			
		||||
	// Tags.
 | 
			
		||||
	g.GET("/api/tags", auth(handleGetTags))
 | 
			
		||||
 | 
			
		||||
	// i18n.
 | 
			
		||||
	g.GET("/api/lang/{lang}", handleGetI18nLang)
 | 
			
		||||
	g.GET("/api/lang/{lang}", auth(handleGetI18nLang))
 | 
			
		||||
 | 
			
		||||
	// Websocket.
 | 
			
		||||
	g.GET("/api/ws", auth(func(r *fastglue.Request) error {
 | 
			
		||||
@@ -69,27 +71,27 @@ func initHandlers(g *fastglue.Fastglue, hub *ws.Hub) {
 | 
			
		||||
	}))
 | 
			
		||||
 | 
			
		||||
	// Automation rules.
 | 
			
		||||
	g.GET("/api/automation/rules", handleGetAutomationRules)
 | 
			
		||||
	g.GET("/api/automation/rules/{id}", handleGetAutomationRule)
 | 
			
		||||
	g.POST("/api/automation/rules", handleCreateAutomationRule)
 | 
			
		||||
	g.PUT("/api/automation/rules/{id}/toggle", handleToggleAutomationRule)
 | 
			
		||||
	g.PUT("/api/automation/rules/{id}", handleUpdateAutomationRule)
 | 
			
		||||
	g.DELETE("/api/automation/rules/{id}", handleDeleteAutomationRule)
 | 
			
		||||
	g.GET("/api/automation/rules", auth(handleGetAutomationRules, "automations:manage"))
 | 
			
		||||
	g.GET("/api/automation/rules/{id}", auth(handleGetAutomationRule, "automations:manage"))
 | 
			
		||||
	g.POST("/api/automation/rules", auth(handleCreateAutomationRule, "automations:manage"))
 | 
			
		||||
	g.PUT("/api/automation/rules/{id}/toggle", auth(handleToggleAutomationRule, "automations:manage"))
 | 
			
		||||
	g.PUT("/api/automation/rules/{id}", auth(handleUpdateAutomationRule, "automations:manage"))
 | 
			
		||||
	g.DELETE("/api/automation/rules/{id}", auth(handleDeleteAutomationRule, "automations:manage"))
 | 
			
		||||
 | 
			
		||||
	// Inboxes.
 | 
			
		||||
	g.GET("/api/inboxes", handleGetInboxes)
 | 
			
		||||
	g.GET("/api/inboxes/{id}", handleGetInbox)
 | 
			
		||||
	g.POST("/api/inboxes", handleCreateInbox)
 | 
			
		||||
	g.PUT("/api/inboxes/{id}/toggle", handleToggleInbox)
 | 
			
		||||
	g.PUT("/api/inboxes/{id}", handleUpdateInbox)
 | 
			
		||||
	g.DELETE("/api/inboxes/{id}", handleDeleteInbox)
 | 
			
		||||
	g.GET("/api/inboxes", auth(handleGetInboxes, "inboxes:manage"))
 | 
			
		||||
	g.GET("/api/inboxes/{id}", auth(handleGetInbox, "inboxes:manage"))
 | 
			
		||||
	g.POST("/api/inboxes", auth(handleCreateInbox, "inboxes:manage"))
 | 
			
		||||
	g.PUT("/api/inboxes/{id}/toggle", auth(handleToggleInbox, "inboxes:manage"))
 | 
			
		||||
	g.PUT("/api/inboxes/{id}", auth(handleUpdateInbox, "inboxes:manage"))
 | 
			
		||||
	g.DELETE("/api/inboxes/{id}", auth(handleDeleteInbox, "inboxes:manage"))
 | 
			
		||||
 | 
			
		||||
	// Roles.
 | 
			
		||||
	g.GET("/api/roles", handleGetRoles)
 | 
			
		||||
	g.GET("/api/roles/{id}", handleGetRole)
 | 
			
		||||
	g.POST("/api/roles", handleCreateRole)
 | 
			
		||||
	g.PUT("/api/roles/{id}", handleUpdateRole)
 | 
			
		||||
	g.DELETE("/api/roles/{id}", handleDeleteRole)
 | 
			
		||||
	g.GET("/api/roles", auth(handleGetRoles, "roles:manage"))
 | 
			
		||||
	g.GET("/api/roles/{id}", auth(handleGetRole, "roles:manage"))
 | 
			
		||||
	g.POST("/api/roles", auth(handleCreateRole, "roles:manage"))
 | 
			
		||||
	g.PUT("/api/roles/{id}", auth(handleUpdateRole, "roles:manage"))
 | 
			
		||||
	g.DELETE("/api/roles/{id}", auth(handleDeleteRole, "roles:manage"))
 | 
			
		||||
 | 
			
		||||
	// Dashboard.
 | 
			
		||||
	g.GET("/api/dashboard/me/counts", auth(handleUserDashboardCounts))
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										60
									
								
								cmd/init.go
									
									
									
									
									
								
							
							
						
						
									
										60
									
								
								cmd/init.go
									
									
									
									
									
								
							@@ -3,6 +3,7 @@ package main
 | 
			
		||||
import (
 | 
			
		||||
	"cmp"
 | 
			
		||||
	"context"
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"log"
 | 
			
		||||
	"os"
 | 
			
		||||
@@ -10,7 +11,6 @@ import (
 | 
			
		||||
 | 
			
		||||
	"github.com/abhinavxd/artemis/internal/attachment"
 | 
			
		||||
	"github.com/abhinavxd/artemis/internal/attachment/stores/s3"
 | 
			
		||||
	uauth "github.com/abhinavxd/artemis/internal/auth"
 | 
			
		||||
	"github.com/abhinavxd/artemis/internal/autoassigner"
 | 
			
		||||
	"github.com/abhinavxd/artemis/internal/automation"
 | 
			
		||||
	"github.com/abhinavxd/artemis/internal/cannedresp"
 | 
			
		||||
@@ -23,6 +23,7 @@ import (
 | 
			
		||||
	notifier "github.com/abhinavxd/artemis/internal/notification"
 | 
			
		||||
	emailnotifier "github.com/abhinavxd/artemis/internal/notification/providers/email"
 | 
			
		||||
	"github.com/abhinavxd/artemis/internal/role"
 | 
			
		||||
	"github.com/abhinavxd/artemis/internal/setting"
 | 
			
		||||
	"github.com/abhinavxd/artemis/internal/tag"
 | 
			
		||||
	"github.com/abhinavxd/artemis/internal/team"
 | 
			
		||||
	"github.com/abhinavxd/artemis/internal/template"
 | 
			
		||||
@@ -30,8 +31,9 @@ import (
 | 
			
		||||
	"github.com/abhinavxd/artemis/internal/ws"
 | 
			
		||||
	"github.com/jmoiron/sqlx"
 | 
			
		||||
	"github.com/knadh/go-i18n"
 | 
			
		||||
	"github.com/knadh/koanf/parsers/json"
 | 
			
		||||
	kjson "github.com/knadh/koanf/parsers/json"
 | 
			
		||||
	"github.com/knadh/koanf/parsers/toml"
 | 
			
		||||
	"github.com/knadh/koanf/providers/confmap"
 | 
			
		||||
	"github.com/knadh/koanf/providers/file"
 | 
			
		||||
	"github.com/knadh/koanf/providers/posflag"
 | 
			
		||||
	"github.com/knadh/koanf/providers/rawbytes"
 | 
			
		||||
@@ -122,10 +124,38 @@ func initFS() stuffbin.FileSystem {
 | 
			
		||||
			log.Fatalf("error initializing FS: %v", err)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	fmt.Println(fs.List())
 | 
			
		||||
	return fs
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// loadSettings loads settings from the DB into the given Koanf map.
 | 
			
		||||
func loadSettings(m *setting.Manager) {
 | 
			
		||||
	j, err := m.GetAllJSON()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Fatalf("error parsing settings from DB: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Setting keys are dot separated, eg: app.favicon_url. Unflatten them into
 | 
			
		||||
	// nested maps {app: {favicon_url}}.
 | 
			
		||||
	var out map[string]interface{}
 | 
			
		||||
 | 
			
		||||
	if err := json.Unmarshal(j, &out); err != nil {
 | 
			
		||||
		log.Fatalf("error unmarshalling settings from DB: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
	if err := ko.Load(confmap.Provider(out, "."), nil); err != nil {
 | 
			
		||||
		log.Fatalf("error parsing settings from DB: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func initSettingsManager(db *sqlx.DB) *setting.Manager {
 | 
			
		||||
	s, err := setting.New(setting.Opts{
 | 
			
		||||
		DB: db,
 | 
			
		||||
	})
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Fatalf("error initializing setting manager: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
	return s
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// initSessionManager initializes and returns a simplesessions.Manager instance.
 | 
			
		||||
func initSessionManager(rd *redis.Client) *simplesessions.Manager {
 | 
			
		||||
	maxAge := ko.Duration("app.session.cookie_max_age")
 | 
			
		||||
@@ -159,8 +189,8 @@ func initUserManager(i18n *i18n.I18n, DB *sqlx.DB) *user.Manager {
 | 
			
		||||
	return mgr
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func initConversations(i18n *i18n.I18n, hub *ws.Hub, db *sqlx.DB) *conversation.Manager {
 | 
			
		||||
	c, err := conversation.New(hub, i18n, conversation.Opts{
 | 
			
		||||
func initConversations(i18n *i18n.I18n, hub *ws.Hub, n notifier.Notifier, db *sqlx.DB) *conversation.Manager {
 | 
			
		||||
	c, err := conversation.New(hub, i18n, n, conversation.Opts{
 | 
			
		||||
		DB:                  db,
 | 
			
		||||
		Lo:                  initLogger("conversation_manager"),
 | 
			
		||||
		ReferenceNumPattern: ko.String("app.constants.conversation_reference_number_pattern"),
 | 
			
		||||
@@ -328,22 +358,12 @@ func initAutomationEngine(db *sqlx.DB, userManager *user.Manager) *automation.En
 | 
			
		||||
	return engine
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func initAutoAssignmentEngine(teamMgr *team.Manager, userMgr *user.Manager, convMgr *conversation.Manager, msgMgr *message.Manager,
 | 
			
		||||
	notifier notifier.Notifier, hub *ws.Hub) *autoassigner.Engine {
 | 
			
		||||
	var lo = initLogger("auto_assignment_engine")
 | 
			
		||||
	engine, err := autoassigner.New(teamMgr, userMgr, convMgr, msgMgr, notifier, hub, lo)
 | 
			
		||||
func initAutoAssigner(teamManager *team.Manager, conversationManager *conversation.Manager) *autoassigner.Engine {
 | 
			
		||||
	e, err := autoassigner.New(teamManager, conversationManager, initLogger("autoassigner"))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Fatalf("error initializing auto assignment engine: %v", err)
 | 
			
		||||
		log.Fatalf("error initializing auto assigner engine: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
	return engine
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func initAuthManager(db *sqlx.DB) *uauth.Manager {
 | 
			
		||||
	manager, err := uauth.New(db, &logf.Logger{})
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Fatalf("error initializing rbac enginer: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
	return manager
 | 
			
		||||
	return e
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func initNotifier(userStore notifier.UserStore, templateRenderer notifier.TemplateRenderer) notifier.Notifier {
 | 
			
		||||
@@ -366,7 +386,7 @@ func initEmailInbox(inboxRecord imodels.Inbox, store inbox.MessageStore) (inbox.
 | 
			
		||||
	var config email.Config
 | 
			
		||||
 | 
			
		||||
	// Load JSON data into Koanf.
 | 
			
		||||
	if err := ko.Load(rawbytes.Provider([]byte(inboxRecord.Config)), json.Parser()); err != nil {
 | 
			
		||||
	if err := ko.Load(rawbytes.Provider([]byte(inboxRecord.Config)), kjson.Parser()); err != nil {
 | 
			
		||||
		log.Fatalf("error loading config: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -32,7 +32,6 @@ func handleLogin(r *fastglue.Request) error {
 | 
			
		||||
		"first_name": user.FirstName,
 | 
			
		||||
		"last_name":  user.LastName,
 | 
			
		||||
		"team_id":    user.TeamID,
 | 
			
		||||
		"permissions": user.Permissions,
 | 
			
		||||
	}); err != nil {
 | 
			
		||||
		app.lo.Error("error setting values in session", "error", err)
 | 
			
		||||
		return sendErrorEnvelope(r, envelope.NewError(envelope.GeneralError, app.i18n.T("user.errorAcquiringSession"), nil))
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										31
									
								
								cmd/main.go
									
									
									
									
									
								
							
							
						
						
									
										31
									
								
								cmd/main.go
									
									
									
									
									
								
							@@ -9,7 +9,6 @@ import (
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/abhinavxd/artemis/internal/attachment"
 | 
			
		||||
	uauth "github.com/abhinavxd/artemis/internal/auth"
 | 
			
		||||
	"github.com/abhinavxd/artemis/internal/automation"
 | 
			
		||||
	"github.com/abhinavxd/artemis/internal/cannedresp"
 | 
			
		||||
	"github.com/abhinavxd/artemis/internal/contact"
 | 
			
		||||
@@ -17,6 +16,7 @@ import (
 | 
			
		||||
	"github.com/abhinavxd/artemis/internal/inbox"
 | 
			
		||||
	"github.com/abhinavxd/artemis/internal/message"
 | 
			
		||||
	"github.com/abhinavxd/artemis/internal/role"
 | 
			
		||||
	"github.com/abhinavxd/artemis/internal/setting"
 | 
			
		||||
	"github.com/abhinavxd/artemis/internal/tag"
 | 
			
		||||
	"github.com/abhinavxd/artemis/internal/team"
 | 
			
		||||
	"github.com/abhinavxd/artemis/internal/upload"
 | 
			
		||||
@@ -25,6 +25,7 @@ import (
 | 
			
		||||
	"github.com/knadh/go-i18n"
 | 
			
		||||
	"github.com/knadh/koanf/v2"
 | 
			
		||||
	"github.com/knadh/stuffbin"
 | 
			
		||||
	"github.com/redis/go-redis/v9"
 | 
			
		||||
	"github.com/valyala/fasthttp"
 | 
			
		||||
	"github.com/zerodha/fastglue"
 | 
			
		||||
	"github.com/zerodha/logf"
 | 
			
		||||
@@ -45,8 +46,10 @@ const (
 | 
			
		||||
type App struct {
 | 
			
		||||
	constants           consts
 | 
			
		||||
	fs                  stuffbin.FileSystem
 | 
			
		||||
	rdb                 *redis.Client
 | 
			
		||||
	i18n                *i18n.I18n
 | 
			
		||||
	lo                  *logf.Logger
 | 
			
		||||
	settingsManager     *setting.Manager
 | 
			
		||||
	roleManager         *role.Manager
 | 
			
		||||
	contactManager      *contact.Manager
 | 
			
		||||
	userManager         *user.Manager
 | 
			
		||||
@@ -54,7 +57,6 @@ type App struct {
 | 
			
		||||
	sessManager         *simplesessions.Manager
 | 
			
		||||
	tagManager          *tag.Manager
 | 
			
		||||
	messageManager      *message.Manager
 | 
			
		||||
	auth                *uauth.Manager
 | 
			
		||||
	inboxManager        *inbox.Manager
 | 
			
		||||
	uploadManager       *upload.Manager
 | 
			
		||||
	attachmentManager   *attachment.Manager
 | 
			
		||||
@@ -70,6 +72,11 @@ func main() {
 | 
			
		||||
	// Load the config files into Koanf.
 | 
			
		||||
	initConfig(ko)
 | 
			
		||||
 | 
			
		||||
	// Load app settings into Koanf.
 | 
			
		||||
	db := initDB()
 | 
			
		||||
	settingManager := initSettingsManager(db)
 | 
			
		||||
	loadSettings(settingManager)
 | 
			
		||||
 | 
			
		||||
	var (
 | 
			
		||||
		shutdownCh          = make(chan struct{})
 | 
			
		||||
		ctx, stop           = signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
 | 
			
		||||
@@ -77,8 +84,7 @@ func main() {
 | 
			
		||||
		fs                  = initFS()
 | 
			
		||||
		i18n                = initI18n(fs)
 | 
			
		||||
		lo                  = initLogger("artemis")
 | 
			
		||||
		rd                  = initRedis()
 | 
			
		||||
		db                  = initDB()
 | 
			
		||||
		rdb                 = initRedis()
 | 
			
		||||
		templateManager     = initTemplateManager(db)
 | 
			
		||||
		attachmentManager   = initAttachmentsManager(db)
 | 
			
		||||
		contactManager      = initContactManager(db)
 | 
			
		||||
@@ -86,10 +92,10 @@ func main() {
 | 
			
		||||
		teamManager         = initTeamManager(db)
 | 
			
		||||
		userManager         = initUserManager(i18n, db)
 | 
			
		||||
		notifier            = initNotifier(userManager, templateManager)
 | 
			
		||||
		conversationManager = initConversations(i18n, wsHub, db)
 | 
			
		||||
		conversationManager = initConversations(i18n, wsHub, notifier, db)
 | 
			
		||||
		automationEngine    = initAutomationEngine(db, userManager)
 | 
			
		||||
		messageManager      = initMessages(db, wsHub, userManager, teamManager, contactManager, attachmentManager, conversationManager, inboxManager, automationEngine, templateManager)
 | 
			
		||||
		autoAssignerEngine  = initAutoAssignmentEngine(teamManager, userManager, conversationManager, messageManager, notifier, wsHub)
 | 
			
		||||
		autoassigner        = initAutoAssigner(teamManager, conversationManager)
 | 
			
		||||
	)
 | 
			
		||||
 | 
			
		||||
	// Set message store for conversation manager.
 | 
			
		||||
@@ -106,8 +112,8 @@ func main() {
 | 
			
		||||
	automationEngine.SetConversationStore(conversationManager)
 | 
			
		||||
	go automationEngine.Serve(ctx)
 | 
			
		||||
 | 
			
		||||
	// Start conversation auto assigner engine.
 | 
			
		||||
	go autoAssignerEngine.Serve(ctx, ko.MustDuration("autoassigner.assign_interval"))
 | 
			
		||||
	// Start conversation auto assigner.
 | 
			
		||||
	go autoassigner.Serve(ctx, ko.MustDuration("autoassigner.assign_interval"))
 | 
			
		||||
 | 
			
		||||
	// Start inserting incoming messages from all active inboxes and dispatch pending outgoing messages.
 | 
			
		||||
	go messageManager.StartDBInserts(ctx, ko.MustInt("message.reader_concurrency"))
 | 
			
		||||
@@ -116,8 +122,10 @@ func main() {
 | 
			
		||||
	// Init the app
 | 
			
		||||
	var app = &App{
 | 
			
		||||
		lo:                  lo,
 | 
			
		||||
		rdb:                 rdb,
 | 
			
		||||
		fs:                  fs,
 | 
			
		||||
		i18n:                i18n,
 | 
			
		||||
		settingsManager:     settingManager,
 | 
			
		||||
		contactManager:      contactManager,
 | 
			
		||||
		inboxManager:        inboxManager,
 | 
			
		||||
		userManager:         userManager,
 | 
			
		||||
@@ -126,11 +134,10 @@ func main() {
 | 
			
		||||
		conversationManager: conversationManager,
 | 
			
		||||
		messageManager:      messageManager,
 | 
			
		||||
		automationEngine:    automationEngine,
 | 
			
		||||
		constants:           initConstants(),
 | 
			
		||||
		roleManager:         initRoleManager(db),
 | 
			
		||||
		auth:                initAuthManager(db),
 | 
			
		||||
		constants:           initConstants(),
 | 
			
		||||
		tagManager:          initTags(db),
 | 
			
		||||
		sessManager:         initSessionManager(rd),
 | 
			
		||||
		sessManager:         initSessionManager(rdb),
 | 
			
		||||
		cannedRespManager:   initCannedResponse(db),
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@@ -159,7 +166,7 @@ func main() {
 | 
			
		||||
 | 
			
		||||
		log.Printf("%sShutting down the server. Please wait.\x1b[0m", colourRed)
 | 
			
		||||
 | 
			
		||||
		time.Sleep(5 * time.Second)
 | 
			
		||||
		time.Sleep(1 * time.Second)
 | 
			
		||||
 | 
			
		||||
		// Signal to shutdown the server
 | 
			
		||||
		shutdownCh <- struct{}{}
 | 
			
		||||
 
 | 
			
		||||
@@ -16,11 +16,12 @@ func handleGetMessages(r *fastglue.Request) error {
 | 
			
		||||
		app  = r.Context.(*App)
 | 
			
		||||
		uuid = r.RequestCtx.UserValue("uuid").(string)
 | 
			
		||||
	)
 | 
			
		||||
 | 
			
		||||
	msgs, err := app.messageManager.GetConversationMessages(uuid)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return sendErrorEnvelope(r, err)
 | 
			
		||||
	}
 | 
			
		||||
	// Generate URLs for all attachments.
 | 
			
		||||
 | 
			
		||||
	for i := range msgs {
 | 
			
		||||
		for j := range msgs[i].Attachments {
 | 
			
		||||
			msgs[i].Attachments[j].URL = app.attachmentManager.Store.GetURL(msgs[i].Attachments[j].UUID)
 | 
			
		||||
@@ -34,18 +35,17 @@ func handleGetMessage(r *fastglue.Request) error {
 | 
			
		||||
		app  = r.Context.(*App)
 | 
			
		||||
		uuid = r.RequestCtx.UserValue("uuid").(string)
 | 
			
		||||
	)
 | 
			
		||||
 | 
			
		||||
	msgs, err := app.messageManager.GetMessage(uuid)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return sendErrorEnvelope(r, err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Generate URLs for each of the attachments.
 | 
			
		||||
	for i := range msgs {
 | 
			
		||||
		for j := range msgs[i].Attachments {
 | 
			
		||||
			msgs[i].Attachments[j].URL = app.attachmentManager.Store.GetURL(msgs[i].Attachments[j].UUID)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return r.SendEnvelope(msgs)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -93,7 +93,6 @@ func handleSendMessage(r *fastglue.Request) error {
 | 
			
		||||
		Content:          string(content),
 | 
			
		||||
		ContentType:      message.ContentTypeHTML,
 | 
			
		||||
		Private:          private,
 | 
			
		||||
		Meta:             "{}",
 | 
			
		||||
		Attachments:      attachments,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@@ -110,5 +109,5 @@ func handleSendMessage(r *fastglue.Request) error {
 | 
			
		||||
	// Send WS update.
 | 
			
		||||
	app.messageManager.BroadcastNewConversationMessage(msg, trimmedMessage)
 | 
			
		||||
 | 
			
		||||
	return r.SendEnvelope("Message sent")
 | 
			
		||||
	return r.SendEnvelope(true)
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,6 @@
 | 
			
		||||
package main
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"net/http"
 | 
			
		||||
 | 
			
		||||
	"github.com/abhinavxd/artemis/internal/envelope"
 | 
			
		||||
@@ -11,7 +10,7 @@ import (
 | 
			
		||||
	"github.com/zerodha/simplesessions/v3"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func auth(handler fastglue.FastRequestHandler, perms ...string) fastglue.FastRequestHandler {
 | 
			
		||||
func auth(handler fastglue.FastRequestHandler, requiredPerms ...string) fastglue.FastRequestHandler {
 | 
			
		||||
	return func(r *fastglue.Request) error {
 | 
			
		||||
		var (
 | 
			
		||||
			app       = r.Context.(*App)
 | 
			
		||||
@@ -37,14 +36,20 @@ func auth(handler fastglue.FastRequestHandler, perms ...string) fastglue.FastReq
 | 
			
		||||
			firstName, _ = sess.String(sessVals["first_name"], nil)
 | 
			
		||||
			lastName, _  = sess.String(sessVals["last_name"], nil)
 | 
			
		||||
			teamID, _    = sess.Int(sessVals["team_id"], nil)
 | 
			
		||||
			// TODO: FIX.
 | 
			
		||||
			p, _         = sess.Bytes(sessVals["permissions"], nil)
 | 
			
		||||
		)
 | 
			
		||||
 | 
			
		||||
		fmt.Printf("%+v perms ", p)
 | 
			
		||||
 | 
			
		||||
		if userID > 0 {
 | 
			
		||||
			// Set user in the request context.
 | 
			
		||||
			// Fetch user perms.
 | 
			
		||||
			userPerms, err := app.userManager.GetPermissions(userID)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return sendErrorEnvelope(r, err)
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if !hasPerms(userPerms, requiredPerms) {
 | 
			
		||||
				return r.SendErrorEnvelope(http.StatusUnauthorized, "You don't have permissions to access this page.", nil, envelope.PermissionError)
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			// User is loggedin, Set user in the request context.
 | 
			
		||||
			r.RequestCtx.SetUserValue("user", umodels.User{
 | 
			
		||||
				ID:        userID,
 | 
			
		||||
				Email:     email,
 | 
			
		||||
@@ -53,14 +58,6 @@ func auth(handler fastglue.FastRequestHandler, perms ...string) fastglue.FastReq
 | 
			
		||||
				TeamID:    teamID,
 | 
			
		||||
			})
 | 
			
		||||
 | 
			
		||||
			// Check permission.
 | 
			
		||||
			for _, perm := range perms {
 | 
			
		||||
				hasPerm, err := app.auth.HasPermission(userID, perm)
 | 
			
		||||
				if err != nil || !hasPerm {
 | 
			
		||||
					return r.SendErrorEnvelope(http.StatusUnauthorized, "You don't have permission to access this page.", nil, envelope.PermissionError)
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			return handler(r)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
@@ -71,6 +68,25 @@ func auth(handler fastglue.FastRequestHandler, perms ...string) fastglue.FastReq
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// hasPerms checks if all requiredPerms exist in userPerms.
 | 
			
		||||
func hasPerms(userPerms []string, requiredPerms []string) bool {
 | 
			
		||||
	userPermMap := make(map[string]bool)
 | 
			
		||||
 | 
			
		||||
	// make map for user's permissions for quick look up
 | 
			
		||||
	for _, perm := range userPerms {
 | 
			
		||||
		userPermMap[perm] = true
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// iterate through required perms and if not found in userPermMap return false
 | 
			
		||||
	for _, requiredPerm := range requiredPerms {
 | 
			
		||||
		if _, ok := userPermMap[requiredPerm]; !ok {
 | 
			
		||||
			return false
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return true
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// authPage middleware makes sure user is logged in to access the page
 | 
			
		||||
// else redirects to login page.
 | 
			
		||||
func authPage(handler fastglue.FastRequestHandler) fastglue.FastRequestHandler {
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										14
									
								
								cmd/settings.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								cmd/settings.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,14 @@
 | 
			
		||||
package main
 | 
			
		||||
 | 
			
		||||
import "github.com/zerodha/fastglue"
 | 
			
		||||
 | 
			
		||||
func handleGetSettings(r *fastglue.Request) error {
 | 
			
		||||
	var (
 | 
			
		||||
		app        = r.Context.(*App)
 | 
			
		||||
	)
 | 
			
		||||
	teams, err := app.settingsManager.GetAll()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return sendErrorEnvelope(r, err)
 | 
			
		||||
	}
 | 
			
		||||
	return r.SendEnvelope(teams)
 | 
			
		||||
}
 | 
			
		||||
@@ -17,6 +17,11 @@ const sidebarNavItems = [
 | 
			
		||||
    title: 'Automations',
 | 
			
		||||
    href: '/admin/automations',
 | 
			
		||||
    description: 'Create automations and time triggers'
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    title: 'Notifications',
 | 
			
		||||
    href: '/admin/notifications',
 | 
			
		||||
    description: 'Manage notifications for your agents'
 | 
			
		||||
  }
 | 
			
		||||
]
 | 
			
		||||
</script>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div v-if="showTable">
 | 
			
		||||
  <div>
 | 
			
		||||
    <div class="flex justify-between mb-5">
 | 
			
		||||
      <div>
 | 
			
		||||
        <span class="admin-title">Inboxes</span>
 | 
			
		||||
@@ -13,7 +13,7 @@
 | 
			
		||||
      <DataTable :columns="columns" :data="data" />
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
  <div v-else>
 | 
			
		||||
  <div>
 | 
			
		||||
    <router-view></router-view>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,23 @@
 | 
			
		||||
<template>
 | 
			
		||||
    <div>
 | 
			
		||||
        <div class="flex flex-col box border p-5 mb-5" v-for="notification in notifications" :key="notification">
 | 
			
		||||
            <div class="flex items-center space-x-2">
 | 
			
		||||
                <Switch id="airplane-mode" />
 | 
			
		||||
                <Label for="airplane-mode">{{ notification.name }}</Label>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script setup>
 | 
			
		||||
import { Label } from '@/components/ui/label'
 | 
			
		||||
import { Switch } from '@/components/ui/switch'
 | 
			
		||||
 | 
			
		||||
defineProps({
 | 
			
		||||
    notifications: {
 | 
			
		||||
        type: Array,
 | 
			
		||||
        required: true,
 | 
			
		||||
    },
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
</script>
 | 
			
		||||
@@ -0,0 +1,23 @@
 | 
			
		||||
<template>
 | 
			
		||||
    <div class="flex justify-between mb-5">
 | 
			
		||||
        <div>
 | 
			
		||||
            <span class="admin-title">Notifications</span>
 | 
			
		||||
            <p class="text-muted-foreground text-sm">Manage notifications.</p>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
    <div>
 | 
			
		||||
        <List :notifications="notifications" />
 | 
			
		||||
    </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script setup>
 | 
			
		||||
import List from './NotificationList.vue'
 | 
			
		||||
const notifications = [
 | 
			
		||||
    {
 | 
			
		||||
        name: "New conversation created"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        name: "New conversation created"
 | 
			
		||||
    }
 | 
			
		||||
]
 | 
			
		||||
</script>
 | 
			
		||||
@@ -54,7 +54,7 @@
 | 
			
		||||
      <FormItem>
 | 
			
		||||
        <FormLabel>Select role</FormLabel>
 | 
			
		||||
        <FormControl>
 | 
			
		||||
          <TagsInput v-model="roles" class="px-0 gap-0">
 | 
			
		||||
          <TagsInput v-model="roles" class="px-0 gap-0 shadow-sm">
 | 
			
		||||
            <div class="flex gap-2 flex-wrap items-center px-3">
 | 
			
		||||
              <TagsInputItem v-for="item in roles" :key="item" :value="item">
 | 
			
		||||
                <TagsInputItemText />
 | 
			
		||||
@@ -73,17 +73,17 @@
 | 
			
		||||
                <CommandList position="popper" class="w-[--radix-popper-anchor-width] rounded-md mt-2 border bg-popover text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2">
 | 
			
		||||
                  <CommandEmpty />
 | 
			
		||||
                  <CommandGroup>
 | 
			
		||||
                    <CommandItem v-for="framework in filteredFrameworks" :key="framework.value" :value="framework.label" @select.prevent="(ev) => {
 | 
			
		||||
                    <CommandItem v-for="user in filteredUsers" :key="user.value" :value="user.label" @select.prevent="(ev) => {
 | 
			
		||||
                      if (typeof ev.detail.value === 'string') {
 | 
			
		||||
                        searchTerm = ''
 | 
			
		||||
                        roles.push(ev.detail.value)
 | 
			
		||||
                      }
 | 
			
		||||
 | 
			
		||||
                      if (filteredFrameworks.length === 0) {
 | 
			
		||||
                      if (filteredUsers.length === 0) {
 | 
			
		||||
                        open = false
 | 
			
		||||
                      }
 | 
			
		||||
                    }">
 | 
			
		||||
                      {{ framework.label }}
 | 
			
		||||
                      {{ user.label }}
 | 
			
		||||
                    </CommandItem>
 | 
			
		||||
                  </CommandGroup>
 | 
			
		||||
                </CommandList>
 | 
			
		||||
@@ -146,7 +146,7 @@ const roles = ref([])
 | 
			
		||||
const open = ref(false)
 | 
			
		||||
const searchTerm = ref('')
 | 
			
		||||
 | 
			
		||||
const filteredFrameworks = computed(() => frameworks.filter(i => !roles.value.includes(i.label)))
 | 
			
		||||
const filteredUsers = computed(() => frameworks.filter(i => !roles.value.includes(i.label)))
 | 
			
		||||
 | 
			
		||||
const props = defineProps({
 | 
			
		||||
  initialValues: {
 | 
			
		||||
 
 | 
			
		||||
@@ -9,6 +9,7 @@ import Team from '@/components/admin/team/TeamSection.vue'
 | 
			
		||||
import Teams from '@/components/admin/team/teams/TeamsCard.vue'
 | 
			
		||||
import Users from '@/components/admin/team/users/UsersCard.vue'
 | 
			
		||||
import Automation from '@/components/admin/automation/Automation.vue'
 | 
			
		||||
import NotificationTab from '@/components/admin/notification/NotificationTab.vue'
 | 
			
		||||
 | 
			
		||||
const routes = [
 | 
			
		||||
  {
 | 
			
		||||
@@ -131,6 +132,27 @@ const routes = [
 | 
			
		||||
      },
 | 
			
		||||
    ]
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    path: '/admin/notifications',
 | 
			
		||||
    name: 'notifications',
 | 
			
		||||
    component: AdminView,
 | 
			
		||||
    children: [
 | 
			
		||||
      {
 | 
			
		||||
        path: '',
 | 
			
		||||
        component: NotificationTab
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        path: ':id/edit',
 | 
			
		||||
        props: true,
 | 
			
		||||
        component: () => import('@/components/admin/automation/CreateOrEditRule.vue')
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        path: 'new',
 | 
			
		||||
        props: true,
 | 
			
		||||
        component: () => import('@/components/admin/automation/CreateOrEditRule.vue')
 | 
			
		||||
      },
 | 
			
		||||
    ]
 | 
			
		||||
  },
 | 
			
		||||
  // Fallback to dashboard.
 | 
			
		||||
  {
 | 
			
		||||
    path: '/:pathMatch(.*)*',
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <!-- Resizable panel last resize value is stored in the localstorage -->
 | 
			
		||||
  <ResizablePanelGroup direction="horizontal" auto-save-id="conversation.vue.resizable.panel">
 | 
			
		||||
    <ResizablePanel :min-size="20" :default-size="23" :max-size="23">
 | 
			
		||||
    <ResizablePanel :min-size="23" :default-size="23" :max-size="40">
 | 
			
		||||
      <ConversationList></ConversationList>
 | 
			
		||||
    </ResizablePanel>
 | 
			
		||||
    <ResizableHandle />
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										1
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										1
									
								
								go.mod
									
									
									
									
									
								
							@@ -47,6 +47,7 @@ require (
 | 
			
		||||
	github.com/jaytaylor/html2text v0.0.0-20230321000545-74c2419ad056 // indirect
 | 
			
		||||
	github.com/klauspost/compress v1.17.8 // indirect
 | 
			
		||||
	github.com/knadh/koanf/maps v0.1.1 // indirect
 | 
			
		||||
	github.com/knadh/koanf/providers/confmap v0.1.0 // indirect
 | 
			
		||||
	github.com/kr/text v0.2.0 // indirect
 | 
			
		||||
	github.com/mattn/go-runewidth v0.0.15 // indirect
 | 
			
		||||
	github.com/mitchellh/copystructure v1.2.0 // indirect
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										2
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								go.sum
									
									
									
									
									
								
							@@ -75,6 +75,8 @@ github.com/knadh/koanf/parsers/json v0.1.0 h1:dzSZl5pf5bBcW0Acnu20Djleto19T0CfHc
 | 
			
		||||
github.com/knadh/koanf/parsers/json v0.1.0/go.mod h1:ll2/MlXcZ2BfXD6YJcjVFzhG9P0TdJ207aIBKQhV2hY=
 | 
			
		||||
github.com/knadh/koanf/parsers/toml v0.1.0 h1:S2hLqS4TgWZYj4/7mI5m1CQQcWurxUz6ODgOub/6LCI=
 | 
			
		||||
github.com/knadh/koanf/parsers/toml v0.1.0/go.mod h1:yUprhq6eo3GbyVXFFMdbfZSo928ksS+uo0FFqNMnO18=
 | 
			
		||||
github.com/knadh/koanf/providers/confmap v0.1.0 h1:gOkxhHkemwG4LezxxN8DMOFopOPghxRVp7JbIvdvqzU=
 | 
			
		||||
github.com/knadh/koanf/providers/confmap v0.1.0/go.mod h1:2uLhxQzJnyHKfxG927awZC7+fyHFdQkd697K4MdLnIU=
 | 
			
		||||
github.com/knadh/koanf/providers/file v0.1.0 h1:fs6U7nrV58d3CFAFh8VTde8TM262ObYf3ODrc//Lp+c=
 | 
			
		||||
github.com/knadh/koanf/providers/file v0.1.0/go.mod h1:rjJ/nHQl64iYCtAW2QQnF0eSmDEX/YZ/eNFj5yR6BvA=
 | 
			
		||||
github.com/knadh/koanf/providers/posflag v0.1.0 h1:mKJlLrKPcAP7Ootf4pBZWJ6J+4wHYujwipe7Ie3qW6U=
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										19
									
								
								i18n/fr.json
									
									
									
									
									
								
							
							
						
						
									
										19
									
								
								i18n/fr.json
									
									
									
									
									
								
							@@ -1,19 +0,0 @@
 | 
			
		||||
{
 | 
			
		||||
    "_.code": "fr",
 | 
			
		||||
    "_.name": "Français (fr)",
 | 
			
		||||
    "globals.entities.user": "utilisateur",
 | 
			
		||||
    "globals.entities.conversations": "conversations",
 | 
			
		||||
    "user.invalidEmailPassword": "Email ou mot de passe invalide.",
 | 
			
		||||
    "user.errorAcquiringSession": "Erreur lors de l'acquisition de la session",
 | 
			
		||||
    "user.errorSettingSession": "Erreur lors de la définition de la session",
 | 
			
		||||
    "conversations.emptyState": "Aucune conversation trouvée.",
 | 
			
		||||
    "conversatons.adjustFilters": "Essayez de modifier vos filtres.",
 | 
			
		||||
    "globals.messages.errorCreating": "Erreur lors de la création de {name}",
 | 
			
		||||
    "globals.messages.errorDeleting": "Erreur lors de la suppression de {name}",
 | 
			
		||||
    "globals.messages.errorFetching": "Erreur lors de la récupération de {name}",
 | 
			
		||||
    "globals.messages.notFound": "Non trouvé",
 | 
			
		||||
    "globals.messages.internalError": "Erreur interne du serveur",
 | 
			
		||||
    "globals.messages.done": "Fait",
 | 
			
		||||
    "globals.messages.emptyState": "Rien ici"
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
							
								
								
									
										21
									
								
								i18n/hi.json
									
									
									
									
									
								
							
							
						
						
									
										21
									
								
								i18n/hi.json
									
									
									
									
									
								
							@@ -1,21 +0,0 @@
 | 
			
		||||
{
 | 
			
		||||
  "_.code": "hi",
 | 
			
		||||
  "_.name": "हिन्दी (hi)",
 | 
			
		||||
  "globals.entities.user": "उपयोगकर्ता",
 | 
			
		||||
  "globals.entities.conversations": "वार्तालाप",
 | 
			
		||||
  "navbar.dashboard": "डैशबोर्ड",
 | 
			
		||||
  "navbar.conversations": "वार्तालाप",
 | 
			
		||||
  "navbar.account": "खाता",
 | 
			
		||||
  "user.invalidEmailPassword": "अमान्य ईमेल या पासवर्ड।",
 | 
			
		||||
  "user.errorAcquiringSession": "सत्र प्राप्त करने में त्रुटि",
 | 
			
		||||
  "user.errorSettingSession": "सत्र सेट करने में त्रुटि",
 | 
			
		||||
  "conversations.emptyState": "कोई वार्तालाप नहीं मिला।",
 | 
			
		||||
  "globals.messages.adjustFilters": "अपने फ़िल्टर समायोजित करने का प्रयास करें।",
 | 
			
		||||
  "globals.messages.errorCreating": "{name} बनाने में त्रुटि",
 | 
			
		||||
  "globals.messages.errorDeleting": "{name} हटाने में त्रुटि",
 | 
			
		||||
  "globals.messages.errorFetching": "{name} लाने में त्रुटि",
 | 
			
		||||
  "globals.messages.notFound": "नहीं मिला",
 | 
			
		||||
  "globals.messages.internalError": "आंतरिक सर्वर त्रुटि",
 | 
			
		||||
  "globals.messages.done": "समाप्त",
 | 
			
		||||
  "globals.messages.emptyState": "यहाँ कुछ भी नहीं है"
 | 
			
		||||
}
 | 
			
		||||
@@ -1,24 +0,0 @@
 | 
			
		||||
package auth
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"github.com/jmoiron/sqlx"
 | 
			
		||||
	"github.com/zerodha/logf"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type Manager struct {
 | 
			
		||||
	lo *logf.Logger
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type ConversationStore interface {
 | 
			
		||||
	GetAssigneedUserID(conversationID int) (int, error)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func New(db *sqlx.DB, lo *logf.Logger) (*Manager, error) {
 | 
			
		||||
	return &Manager{
 | 
			
		||||
		lo: lo,
 | 
			
		||||
	}, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (e *Manager) HasPermission(userID int, perm string) (bool, error) {
 | 
			
		||||
	return true, nil
 | 
			
		||||
}
 | 
			
		||||
@@ -1,8 +0,0 @@
 | 
			
		||||
package models
 | 
			
		||||
 | 
			
		||||
type AuthUser struct {
 | 
			
		||||
	ID        int
 | 
			
		||||
	FirstName string
 | 
			
		||||
	LastName  string
 | 
			
		||||
	Email     string
 | 
			
		||||
}
 | 
			
		||||
@@ -1,64 +1,56 @@
 | 
			
		||||
// Package autoassigner automatically assigning unassigned conversations to team agents in a round-robin fashion.
 | 
			
		||||
// Continuously assigns conversations at regular intervals.
 | 
			
		||||
package autoassigner
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"sync"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/abhinavxd/artemis/internal/conversation"
 | 
			
		||||
	"github.com/abhinavxd/artemis/internal/conversation/models"
 | 
			
		||||
	"github.com/abhinavxd/artemis/internal/message"
 | 
			
		||||
	notifier "github.com/abhinavxd/artemis/internal/notification"
 | 
			
		||||
	"github.com/abhinavxd/artemis/internal/systeminfo"
 | 
			
		||||
	"github.com/abhinavxd/artemis/internal/team"
 | 
			
		||||
	"github.com/abhinavxd/artemis/internal/user"
 | 
			
		||||
	"github.com/abhinavxd/artemis/internal/ws"
 | 
			
		||||
	"github.com/mr-karan/balance"
 | 
			
		||||
	"github.com/zerodha/logf"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
	roundRobinDefaultWeight = 1
 | 
			
		||||
var (
 | 
			
		||||
	ErrTeamNotFound = errors.New("team not found")
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// Engine handles the assignment of unassigned conversations to agents of a team using a round-robin strategy.
 | 
			
		||||
// Engine represents a manager for assigning unassigned conversations
 | 
			
		||||
// to team agents in a round-robin pattern.
 | 
			
		||||
type Engine struct {
 | 
			
		||||
	teamRoundRobinBalancer map[int]*balance.Balance
 | 
			
		||||
	roundRobinBalancer map[int]*balance.Balance
 | 
			
		||||
	// Mutex to protect the balancer map
 | 
			
		||||
	mu sync.Mutex
 | 
			
		||||
 | 
			
		||||
	userIDMap map[string]int
 | 
			
		||||
	convMgr   *conversation.Manager
 | 
			
		||||
	teamMgr   *team.Manager
 | 
			
		||||
	userMgr   *user.Manager
 | 
			
		||||
	msgMgr    *message.Manager
 | 
			
		||||
	conversationManager *conversation.Manager
 | 
			
		||||
	teamManager         *team.Manager
 | 
			
		||||
	lo                  *logf.Logger
 | 
			
		||||
	hub       *ws.Hub
 | 
			
		||||
	notifier  notifier.Notifier
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// New creates a new instance of the Engine.
 | 
			
		||||
func New(teamMgr *team.Manager, userMgr *user.Manager, convMgr *conversation.Manager, msgMgr *message.Manager,
 | 
			
		||||
	notifier notifier.Notifier, hub *ws.Hub, lo *logf.Logger) (*Engine, error) {
 | 
			
		||||
// 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, lo *logf.Logger) (*Engine, error) {
 | 
			
		||||
	var e = Engine{
 | 
			
		||||
		notifier:  notifier,
 | 
			
		||||
		convMgr:   convMgr,
 | 
			
		||||
		teamMgr:   teamMgr,
 | 
			
		||||
		msgMgr:    msgMgr,
 | 
			
		||||
		userMgr:   userMgr,
 | 
			
		||||
		conversationManager: conversationManager,
 | 
			
		||||
		teamManager:         teamManager,
 | 
			
		||||
		lo:                  lo,
 | 
			
		||||
		hub:       hub,
 | 
			
		||||
		mu:                  sync.Mutex{},
 | 
			
		||||
		userIDMap: map[string]int{},
 | 
			
		||||
	}
 | 
			
		||||
	balancer, err := e.populateBalancerPool()
 | 
			
		||||
	balancer, err := e.populateTeamBalancer()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	e.teamRoundRobinBalancer = balancer
 | 
			
		||||
	e.roundRobinBalancer = balancer
 | 
			
		||||
	return &e, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Serve 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) Serve(ctx context.Context, interval time.Duration) {
 | 
			
		||||
	ticker := time.NewTicker(interval)
 | 
			
		||||
	defer ticker.Stop()
 | 
			
		||||
@@ -74,32 +66,33 @@ func (e *Engine) Serve(ctx context.Context, interval time.Duration) {
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// RefreshBalancer updates the round-robin balancer with the latest user and team data.
 | 
			
		||||
func (e *Engine) RefreshBalancer() error {
 | 
			
		||||
	e.mu.Lock()
 | 
			
		||||
	defer e.mu.Unlock()
 | 
			
		||||
 | 
			
		||||
	balancer, err := e.populateBalancerPool()
 | 
			
		||||
	balancer, err := e.populateTeamBalancer()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		e.lo.Error("Error updating team balancer pool", "error", err)
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	e.teamRoundRobinBalancer = balancer
 | 
			
		||||
	e.roundRobinBalancer = balancer
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// populateBalancerPool populates the team balancer bool with the team members.
 | 
			
		||||
func (e *Engine) populateBalancerPool() (map[int]*balance.Balance, 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)
 | 
			
		||||
		teams, err = e.teamMgr.GetAll()
 | 
			
		||||
	)
 | 
			
		||||
 | 
			
		||||
	teams, err := e.teamManager.GetAll()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, team := range teams {
 | 
			
		||||
		users, err := e.teamMgr.GetTeamMembers(team.Name)
 | 
			
		||||
		users, err := e.teamManager.GetTeamMembers(team.Name)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return nil, err
 | 
			
		||||
		}
 | 
			
		||||
@@ -109,17 +102,16 @@ func (e *Engine) populateBalancerPool() (map[int]*balance.Balance, error) {
 | 
			
		||||
			if _, ok := balancer[team.ID]; !ok {
 | 
			
		||||
				balancer[team.ID] = balance.NewBalance()
 | 
			
		||||
			}
 | 
			
		||||
			// FIXME: Balancer only supports strings, using a map to store DB ids.
 | 
			
		||||
			balancer[team.ID].Add(user.UUID, roundRobinDefaultWeight)
 | 
			
		||||
			e.userIDMap[user.UUID] = user.ID
 | 
			
		||||
			balancer[team.ID].Add(strconv.Itoa(user.ID), 1)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return balancer, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// assignConversations fetches unassigned conversations and assigns them to users.
 | 
			
		||||
// 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.convMgr.GetUnassigned()
 | 
			
		||||
	unassigned, err := e.conversationManager.GetUnassigned()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
@@ -128,46 +120,34 @@ func (e *Engine) assignConversations() error {
 | 
			
		||||
		e.lo.Debug("found unassigned conversations", "count", len(unassigned))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Get system user, all actions here are done on behalf of the system user.
 | 
			
		||||
	systemUser, err := e.userMgr.GetUser(0, systeminfo.SystemUserUUID)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, conversation := range unassigned {
 | 
			
		||||
		// Get user uuid from the pool.
 | 
			
		||||
		userUUID := e.getUser(conversation)
 | 
			
		||||
		if userUUID == "" {
 | 
			
		||||
			e.lo.Warn("user uuid not found for round robin assignment", "team_id", conversation.AssignedTeamID.Int)
 | 
			
		||||
		uid, err := e.getUserFromPool(conversation)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			e.lo.Error("error fetching user from balancer pool", "error", err)
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// Get user ID from the map.
 | 
			
		||||
		// FIXME: Balance only supports strings.
 | 
			
		||||
		userID, ok := e.userIDMap[userUUID]
 | 
			
		||||
		if !ok {
 | 
			
		||||
			e.lo.Warn("user id not found for user uuid", "uuid", userUUID, "team_id", conversation.AssignedTeamID.Int)
 | 
			
		||||
			continue
 | 
			
		||||
		// Convert to int.
 | 
			
		||||
		userID, err := strconv.Atoi(uid)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			e.lo.Error("error converting user id from string to int", "error", err)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// Update assignee and record the assigne change message.
 | 
			
		||||
		if err := e.convMgr.UpdateUserAssignee(conversation.UUID, userID, systemUser); err != nil {
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// Send notification to the assignee.
 | 
			
		||||
		e.notifier.SendAssignedConversationNotification([]string{userUUID}, conversation.UUID)
 | 
			
		||||
 | 
			
		||||
		// Assign conversation to this user.
 | 
			
		||||
		e.conversationManager.UpdateUserAssigneeBySystem(conversation.UUID, userID)
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// getUser returns user uuid from the team balancer pool.
 | 
			
		||||
func (e *Engine) getUser(conversation models.Conversation) string {
 | 
			
		||||
	pool, ok := e.teamRoundRobinBalancer[conversation.AssignedTeamID.Int]
 | 
			
		||||
// getUserFromPool returns user ID from the team balancer pool.
 | 
			
		||||
func (e *Engine) getUserFromPool(conversation models.Conversation) (string, error) {
 | 
			
		||||
	e.mu.Lock()
 | 
			
		||||
	defer e.mu.Unlock()
 | 
			
		||||
 | 
			
		||||
	pool, ok := e.roundRobinBalancer[conversation.AssignedTeamID.Int]
 | 
			
		||||
	if !ok {
 | 
			
		||||
		e.lo.Warn("team not found in balancer", "id", conversation.AssignedTeamID.Int)
 | 
			
		||||
		return ""
 | 
			
		||||
		return "", ErrTeamNotFound
 | 
			
		||||
	}
 | 
			
		||||
	return pool.Get()
 | 
			
		||||
	return pool.Get(), nil
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,3 +1,5 @@
 | 
			
		||||
// Package automation provides a framework for automatically evaluating and applying
 | 
			
		||||
// rules to conversations based on events like new conversations, updates, and time triggers.
 | 
			
		||||
package automation
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
@@ -23,12 +25,13 @@ var (
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type Engine struct {
 | 
			
		||||
	rules   []models.Rule
 | 
			
		||||
	rulesMu *sync.RWMutex
 | 
			
		||||
 | 
			
		||||
	q                   queries
 | 
			
		||||
	lo                  *logf.Logger
 | 
			
		||||
	conversationStore   ConversationStore
 | 
			
		||||
	systemUser          umodels.User
 | 
			
		||||
	rulesMu             *sync.RWMutex
 | 
			
		||||
	rules               []models.Rule
 | 
			
		||||
	newConversationQ    chan string
 | 
			
		||||
	updateConversationQ chan string
 | 
			
		||||
}
 | 
			
		||||
@@ -79,20 +82,19 @@ func New(systemUser umodels.User, opt Opts) (*Engine, error) {
 | 
			
		||||
	return e, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (e *Engine) ReloadRules() {
 | 
			
		||||
	e.lo.Debug("reloading automation engine rules")
 | 
			
		||||
	e.rulesMu.Lock()
 | 
			
		||||
	defer e.rulesMu.Unlock()
 | 
			
		||||
	e.rules = e.queryRules()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (e *Engine) SetConversationStore(store ConversationStore) {
 | 
			
		||||
	e.conversationStore = store
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (e *Engine) ReloadRules() {
 | 
			
		||||
	e.rulesMu.Lock()
 | 
			
		||||
	defer e.rulesMu.Unlock()
 | 
			
		||||
	e.lo.Debug("reloading automation engine rules")
 | 
			
		||||
	e.rules = e.queryRules()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (e *Engine) Serve(ctx context.Context) {
 | 
			
		||||
	// TODO: Change to 1 hour.
 | 
			
		||||
	ticker := time.NewTicker(30 * time.Second)
 | 
			
		||||
	ticker := time.NewTicker(1 * time.Hour)
 | 
			
		||||
	defer ticker.Stop()
 | 
			
		||||
 | 
			
		||||
	// Create separate semaphores for each channel
 | 
			
		||||
@@ -184,7 +186,6 @@ func (e *Engine) handleNewConversation(conversationUUID string, semaphore chan s
 | 
			
		||||
	defer func() { <-semaphore }()
 | 
			
		||||
	conversation, err := e.conversationStore.Get(conversationUUID)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		e.lo.Error("error could not fetch conversations to evaluate new conversation rules", "conversation_uuid", conversationUUID)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	rules := e.filterRulesByType(models.RuleTypeNewConversation)
 | 
			
		||||
@@ -207,7 +208,6 @@ func (e *Engine) handleTimeTrigger(semaphore chan struct{}) {
 | 
			
		||||
	thirtyDaysAgo := time.Now().Add(-30 * 24 * time.Hour)
 | 
			
		||||
	conversations, err := e.conversationStore.GetRecentConversations(thirtyDaysAgo)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		e.lo.Error("error could not fetch conversations to evaluate time triggers")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	rules := e.filterRulesByType(models.RuleTypeTimeTrigger)
 | 
			
		||||
@@ -257,6 +257,9 @@ func (e *Engine) queryRules() []models.Rule {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (e *Engine) filterRulesByType(ruleType string) []models.Rule {
 | 
			
		||||
	e.rulesMu.RLock()
 | 
			
		||||
	defer e.rulesMu.RUnlock()
 | 
			
		||||
 | 
			
		||||
	var filteredRules []models.Rule
 | 
			
		||||
	for _, rule := range e.rules {
 | 
			
		||||
		if rule.Type == ruleType {
 | 
			
		||||
 
 | 
			
		||||
@@ -15,6 +15,7 @@ func (e *Engine) evalConversationRules(rules []models.Rule, conversation cmodels
 | 
			
		||||
		e.lo.Debug("eval rule", "groups", len(rule.Groups), "rule", rule)
 | 
			
		||||
		// At max there can be only 2 groups.
 | 
			
		||||
		if len(rule.Groups) > 2 {
 | 
			
		||||
			e.lo.Warn("more than 2 groups found for rules")
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
		var results []bool
 | 
			
		||||
 
 | 
			
		||||
@@ -2,9 +2,9 @@ package cannedresp
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"embed"
 | 
			
		||||
	"fmt"
 | 
			
		||||
 | 
			
		||||
	"github.com/abhinavxd/artemis/internal/dbutil"
 | 
			
		||||
	"github.com/abhinavxd/artemis/internal/envelope"
 | 
			
		||||
	"github.com/jmoiron/sqlx"
 | 
			
		||||
	"github.com/zerodha/logf"
 | 
			
		||||
)
 | 
			
		||||
@@ -50,8 +50,8 @@ func New(opts Opts) (*Manager, error) {
 | 
			
		||||
func (t *Manager) GetAll() ([]CannedResponse, error) {
 | 
			
		||||
	var c []CannedResponse
 | 
			
		||||
	if err := t.q.GetAll.Select(&c); err != nil {
 | 
			
		||||
		t.lo.Error("fetching canned responses", "error", err)
 | 
			
		||||
		return c, fmt.Errorf("error fetching canned responses")
 | 
			
		||||
		t.lo.Error("error fetching canned responses", "error", err)
 | 
			
		||||
		return c, envelope.NewError(envelope.GeneralError, "Error fetching canned responses", nil)
 | 
			
		||||
	}
 | 
			
		||||
	return c, nil
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -14,6 +14,7 @@ import (
 | 
			
		||||
	"github.com/abhinavxd/artemis/internal/conversation/models"
 | 
			
		||||
	"github.com/abhinavxd/artemis/internal/dbutil"
 | 
			
		||||
	"github.com/abhinavxd/artemis/internal/envelope"
 | 
			
		||||
	notifier "github.com/abhinavxd/artemis/internal/notification"
 | 
			
		||||
	"github.com/abhinavxd/artemis/internal/stringutil"
 | 
			
		||||
	umodels "github.com/abhinavxd/artemis/internal/user/models"
 | 
			
		||||
	"github.com/abhinavxd/artemis/internal/ws"
 | 
			
		||||
@@ -74,6 +75,7 @@ const (
 | 
			
		||||
 | 
			
		||||
type MessageStore interface {
 | 
			
		||||
	RecordAssigneeUserChange(conversationUUID string, assigneeID int, actor umodels.User) error
 | 
			
		||||
	RecordAssigneeUserChangeBySystem(conversationUUID string, assigneeID int) error
 | 
			
		||||
	RecordAssigneeTeamChange(conversationUUID string, teamID int, actor umodels.User) error
 | 
			
		||||
	RecordPriorityChange(priority, conversationUUID string, actor umodels.User) error
 | 
			
		||||
	RecordStatusChange(status, conversationUUID string, actor umodels.User) error
 | 
			
		||||
@@ -84,6 +86,7 @@ type Manager struct {
 | 
			
		||||
	db                  *sqlx.DB
 | 
			
		||||
	hub                 *ws.Hub
 | 
			
		||||
	i18n                *i18n.I18n
 | 
			
		||||
	notifier            notifier.Notifier
 | 
			
		||||
	messageStore        MessageStore
 | 
			
		||||
	q                   queries
 | 
			
		||||
	ReferenceNumPattern string
 | 
			
		||||
@@ -121,7 +124,7 @@ type queries struct {
 | 
			
		||||
	DeleteTags                   *sqlx.Stmt `query:"delete-tags"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func New(hub *ws.Hub, i18n *i18n.I18n, opts Opts) (*Manager, error) {
 | 
			
		||||
func New(hub *ws.Hub, i18n *i18n.I18n, notfier notifier.Notifier, opts Opts) (*Manager, error) {
 | 
			
		||||
	var q queries
 | 
			
		||||
	if err := dbutil.ScanSQLFile("queries.sql", &q, opts.DB, efs); err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
@@ -130,6 +133,7 @@ func New(hub *ws.Hub, i18n *i18n.I18n, opts Opts) (*Manager, error) {
 | 
			
		||||
		q:                   q,
 | 
			
		||||
		hub:                 hub,
 | 
			
		||||
		i18n:                i18n,
 | 
			
		||||
		notifier:            notfier,
 | 
			
		||||
		db:                  opts.DB,
 | 
			
		||||
		lo:                  opts.Lo,
 | 
			
		||||
		ReferenceNumPattern: opts.ReferenceNumPattern,
 | 
			
		||||
@@ -363,12 +367,30 @@ func (c *Manager) UpdateUserAssignee(uuid string, assigneeID int, actor umodels.
 | 
			
		||||
	if err := c.UpdateAssignee(uuid, assigneeID, assigneeTypeUser); err != nil {
 | 
			
		||||
		return envelope.NewError(envelope.GeneralError, "Error updating assignee", nil)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Send notification to assignee.
 | 
			
		||||
	c.notifier.SendAssignedConversationNotification([]int{assigneeID}, uuid)
 | 
			
		||||
 | 
			
		||||
	if err := c.messageStore.RecordAssigneeUserChange(uuid, assigneeID, actor); err != nil {
 | 
			
		||||
		return envelope.NewError(envelope.GeneralError, "Error recording assignee change", nil)
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (c *Manager) UpdateUserAssigneeBySystem(uuid string, assigneeID int) error {
 | 
			
		||||
	if err := c.UpdateAssignee(uuid, assigneeID, assigneeTypeUser); err != nil {
 | 
			
		||||
		return envelope.NewError(envelope.GeneralError, "Error updating assignee", nil)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Send notification to assignee.
 | 
			
		||||
	c.notifier.SendAssignedConversationNotification([]int{assigneeID}, uuid)
 | 
			
		||||
 | 
			
		||||
	if err := c.messageStore.RecordAssigneeUserChangeBySystem(uuid, assigneeID); err != nil {
 | 
			
		||||
		return envelope.NewError(envelope.GeneralError, "Error recording assignee change", nil)
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (c *Manager) UpdateTeamAssignee(uuid string, teamID int, actor umodels.User) error {
 | 
			
		||||
	if err := c.UpdateAssignee(uuid, teamID, assigneeTypeTeam); err != nil {
 | 
			
		||||
		return envelope.NewError(envelope.GeneralError, "Error updating assignee", nil)
 | 
			
		||||
@@ -386,13 +408,13 @@ func (c *Manager) UpdateAssignee(uuid string, assigneeID int, assigneeType strin
 | 
			
		||||
			c.lo.Error("error updating conversation assignee", "error", err)
 | 
			
		||||
			return fmt.Errorf("error updating assignee")
 | 
			
		||||
		}
 | 
			
		||||
		c.hub.BroadcastConversationPropertyUpdate(uuid, "assigned_user_uuid", strconv.Itoa(assigneeID))
 | 
			
		||||
		c.hub.BroadcastConversationPropertyUpdate(uuid, "assigned_user_id", strconv.Itoa(assigneeID))
 | 
			
		||||
	case assigneeTypeTeam:
 | 
			
		||||
		if _, err := c.q.UpdateAssignedTeam.Exec(uuid, assigneeID); err != nil {
 | 
			
		||||
			c.lo.Error("error updating conversation assignee", "error", err)
 | 
			
		||||
			return fmt.Errorf("error updating assignee")
 | 
			
		||||
		}
 | 
			
		||||
		c.hub.BroadcastConversationPropertyUpdate(uuid, "assigned_team_uuid", strconv.Itoa(assigneeID))
 | 
			
		||||
		c.hub.BroadcastConversationPropertyUpdate(uuid, "assigned_team_id", strconv.Itoa(assigneeID))
 | 
			
		||||
	default:
 | 
			
		||||
		return errors.New("invalid assignee type")
 | 
			
		||||
	}
 | 
			
		||||
 
 | 
			
		||||
@@ -17,8 +17,10 @@ import (
 | 
			
		||||
	"github.com/abhinavxd/artemis/internal/contact"
 | 
			
		||||
	"github.com/abhinavxd/artemis/internal/conversation"
 | 
			
		||||
	"github.com/abhinavxd/artemis/internal/dbutil"
 | 
			
		||||
	"github.com/abhinavxd/artemis/internal/envelope"
 | 
			
		||||
	"github.com/abhinavxd/artemis/internal/inbox"
 | 
			
		||||
	"github.com/abhinavxd/artemis/internal/message/models"
 | 
			
		||||
	"github.com/abhinavxd/artemis/internal/systeminfo"
 | 
			
		||||
	"github.com/abhinavxd/artemis/internal/team"
 | 
			
		||||
	"github.com/abhinavxd/artemis/internal/template"
 | 
			
		||||
	"github.com/abhinavxd/artemis/internal/user"
 | 
			
		||||
@@ -140,7 +142,7 @@ func (m *Manager) GetConversationMessages(uuid string) ([]models.Message, error)
 | 
			
		||||
	var messages []models.Message
 | 
			
		||||
	if err := m.q.GetMessages.Select(&messages, uuid); err != nil {
 | 
			
		||||
		m.lo.Error("fetching messages from DB", "conversation_uuid", uuid, "error", err)
 | 
			
		||||
		return nil, fmt.Errorf("error fetching messages")
 | 
			
		||||
		return nil, envelope.NewError(envelope.GeneralError, "Error fetching messages", nil)
 | 
			
		||||
	}
 | 
			
		||||
	return messages, nil
 | 
			
		||||
}
 | 
			
		||||
@@ -149,7 +151,7 @@ func (m *Manager) GetMessage(uuid string) ([]models.Message, error) {
 | 
			
		||||
	var messages []models.Message
 | 
			
		||||
	if err := m.q.GetMessage.Select(&messages, uuid); err != nil {
 | 
			
		||||
		m.lo.Error("fetching messages from DB", "conversation_uuid", uuid, "error", err)
 | 
			
		||||
		return nil, fmt.Errorf("error fetching messages")
 | 
			
		||||
		return nil, envelope.NewError(envelope.GeneralError, "Error fetching message", nil)
 | 
			
		||||
	}
 | 
			
		||||
	return messages, nil
 | 
			
		||||
}
 | 
			
		||||
@@ -337,6 +339,18 @@ func (m *Manager) RecordAssigneeUserChange(conversationUUID string, assigneeID i
 | 
			
		||||
	return m.RecordActivity(ActivityAssignedUserChange, conversationUUID, assignee.FullName(), actor)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (m *Manager) RecordAssigneeUserChangeBySystem(conversationUUID string, assigneeID int) error {
 | 
			
		||||
	assignee, err := m.userMgr.GetUser(assigneeID, "")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	system, err := m.userMgr.GetUser(0, systeminfo.SystemUserUUID)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	return m.RecordActivity(ActivityAssignedUserChange, conversationUUID, assignee.FullName(), system)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (m *Manager) RecordAssigneeTeamChange(conversationUUID string, teamID int, actor umodels.User) error {
 | 
			
		||||
	team, err := m.teamMgr.GetTeam(teamID)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
 
 | 
			
		||||
@@ -2,8 +2,8 @@ package notifier
 | 
			
		||||
 | 
			
		||||
// Notifier defines the interface for sending notifications.
 | 
			
		||||
type Notifier interface {
 | 
			
		||||
	SendMessage(userUUIDs []string, subject, content string) error
 | 
			
		||||
	SendAssignedConversationNotification(userUUIDs []string, convUUID string) error
 | 
			
		||||
	SendMessage(userID []int, subject, content string) error
 | 
			
		||||
	SendAssignedConversationNotification(userID []int, convUUID string) error
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// TemplateRenderer defines the interface for rendering templates.
 | 
			
		||||
 
 | 
			
		||||
@@ -12,8 +12,8 @@ import (
 | 
			
		||||
	"github.com/zerodha/logf"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// Notifier handles email notifications.
 | 
			
		||||
type Notifier struct {
 | 
			
		||||
// Email
 | 
			
		||||
type Email struct {
 | 
			
		||||
	lo               *logf.Logger
 | 
			
		||||
	from             string
 | 
			
		||||
	smtpPools        []*smtppool.Pool
 | 
			
		||||
@@ -27,12 +27,12 @@ type Opts struct {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// New creates a new instance of email Notifier.
 | 
			
		||||
func New(smtpConfig []email.SMTPConfig, userStore notifier.UserStore, TemplateRenderer notifier.TemplateRenderer, opts Opts) (*Notifier, error) {
 | 
			
		||||
func New(smtpConfig []email.SMTPConfig, userStore notifier.UserStore, TemplateRenderer notifier.TemplateRenderer, opts Opts) (*Email, error) {
 | 
			
		||||
	pools, err := email.NewSmtpPool(smtpConfig)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	return &Notifier{
 | 
			
		||||
	return &Email{
 | 
			
		||||
		lo:               opts.Lo,
 | 
			
		||||
		smtpPools:        pools,
 | 
			
		||||
		from:             opts.FromEmail,
 | 
			
		||||
@@ -42,12 +42,12 @@ func New(smtpConfig []email.SMTPConfig, userStore notifier.UserStore, TemplateRe
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SendMessage sends an email using the default template to multiple users.
 | 
			
		||||
func (e *Notifier) SendMessage(userUUIDs []string, subject, content string) error {
 | 
			
		||||
func (e *Email) SendMessage(userIDs []int, subject, content string) error {
 | 
			
		||||
	var recipientEmails []string
 | 
			
		||||
	for i := 0; i < len(userUUIDs); i++ {
 | 
			
		||||
		userEmail, err := e.userStore.GetEmail(0, userUUIDs[i])
 | 
			
		||||
	for i := 0; i < len(userIDs); i++ {
 | 
			
		||||
		userEmail, err := e.userStore.GetEmail(userIDs[i], "")
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			e.lo.Error("error fetching user email for user uuid", "error", err)
 | 
			
		||||
			e.lo.Error("error fetching user email", "error", err)
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
		recipientEmails = append(recipientEmails, userEmail)
 | 
			
		||||
@@ -80,15 +80,15 @@ func (e *Notifier) SendMessage(userUUIDs []string, subject, content string) erro
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (e *Notifier) SendAssignedConversationNotification(userUUIDs []string, convUUID string) error {
 | 
			
		||||
func (e *Email) SendAssignedConversationNotification(userIDs []int, convUUID string) error {
 | 
			
		||||
	subject := "New conversation assigned to you"
 | 
			
		||||
	link := fmt.Sprintf("http://localhost:5173/conversations/%s", convUUID)
 | 
			
		||||
	content := fmt.Sprintf("A new conversation has been assigned to you. <br>Please review the details and take necessary action by following this link: %s", link)
 | 
			
		||||
	return e.SendMessage(userUUIDs, subject, content)
 | 
			
		||||
	return e.SendMessage(userIDs, subject, content)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Send sends an email message using one of the SMTP pools.
 | 
			
		||||
func (e *Notifier) Send(m models.Message) error {
 | 
			
		||||
// Send sends an email message.
 | 
			
		||||
func (e *Email) Send(m models.Message) error {
 | 
			
		||||
	var (
 | 
			
		||||
		ln  = len(e.smtpPools)
 | 
			
		||||
		srv *smtppool.Pool
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										6
									
								
								internal/setting/models/models.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								internal/setting/models/models.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,6 @@
 | 
			
		||||
package models
 | 
			
		||||
 | 
			
		||||
type Settings struct {
 | 
			
		||||
	AppSiteName                   string   `json:"app.site_name"`
 | 
			
		||||
	AppLang                       string   `json:"app.lang"`
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										2
									
								
								internal/setting/queries.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								internal/setting/queries.sql
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,2 @@
 | 
			
		||||
-- name: get-all
 | 
			
		||||
SELECT JSON_OBJECT_AGG(key, value) AS settings FROM (SELECT * FROM settings ORDER BY key) t;
 | 
			
		||||
							
								
								
									
										69
									
								
								internal/setting/setting.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										69
									
								
								internal/setting/setting.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,69 @@
 | 
			
		||||
package setting
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"embed"
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
 | 
			
		||||
	"github.com/abhinavxd/artemis/internal/dbutil"
 | 
			
		||||
	"github.com/abhinavxd/artemis/internal/setting/models"
 | 
			
		||||
	"github.com/jmoiron/sqlx"
 | 
			
		||||
	"github.com/jmoiron/sqlx/types"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
	//go:embed queries.sql
 | 
			
		||||
	efs embed.FS
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type Manager struct {
 | 
			
		||||
	q queries
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type Opts struct {
 | 
			
		||||
	DB *sqlx.DB
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type queries struct {
 | 
			
		||||
	GetAll *sqlx.Stmt `query:"get-all"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func New(opts Opts) (*Manager, error) {
 | 
			
		||||
	var q queries
 | 
			
		||||
 | 
			
		||||
	if err := dbutil.ScanSQLFile("queries.sql", &q, opts.DB, efs); err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return &Manager{
 | 
			
		||||
		q: q,
 | 
			
		||||
	}, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (m *Manager) GetAll() (models.Settings, error) {
 | 
			
		||||
	var (
 | 
			
		||||
		b   types.JSONText
 | 
			
		||||
		out models.Settings
 | 
			
		||||
	)
 | 
			
		||||
 | 
			
		||||
	if err := m.q.GetAll.Get(&b); err != nil {
 | 
			
		||||
		return out, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err := json.Unmarshal([]byte(b), &out); err != nil {
 | 
			
		||||
		return out, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return out, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (m *Manager) GetAllJSON() (types.JSONText, error) {
 | 
			
		||||
	var (
 | 
			
		||||
		b types.JSONText
 | 
			
		||||
	)
 | 
			
		||||
 | 
			
		||||
	if err := m.q.GetAll.Get(&b); err != nil {
 | 
			
		||||
		return b, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return b, nil
 | 
			
		||||
}
 | 
			
		||||
@@ -16,7 +16,7 @@ type User struct {
 | 
			
		||||
	TeamID           int            `db:"team_id" json:"team_id"`
 | 
			
		||||
	Password         string         `db:"password" json:"-"`
 | 
			
		||||
	TeamName         null.String    `db:"team_name" json:"team_name"`
 | 
			
		||||
	Roles            []string       `db:"roles" json:"roles"`
 | 
			
		||||
	Roles            pq.StringArray `db:"roles" json:"roles"`
 | 
			
		||||
	SendWelcomeEmail bool           `db:"-" json:"send_welcome_email"`
 | 
			
		||||
	Permissions      pq.StringArray `db:"permissions" json:"permissions"`
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -14,7 +14,7 @@ JOIN roles r ON r.name = ANY(u.roles)
 | 
			
		||||
WHERE u.email = $1;
 | 
			
		||||
 | 
			
		||||
-- name: get-user
 | 
			
		||||
SELECT id, email, avatar_url, first_name, last_name, team_id 
 | 
			
		||||
SELECT id, email, avatar_url, first_name, last_name, team_id, roles
 | 
			
		||||
FROM users 
 | 
			
		||||
WHERE 
 | 
			
		||||
  CASE 
 | 
			
		||||
@@ -34,3 +34,9 @@ VALUES($1, $2, $3, $4, $5, $6, $7);
 | 
			
		||||
UPDATE users
 | 
			
		||||
set first_name = $2, last_name = $3, email = $4, team_id = $5, roles = $6, updated_at = now()
 | 
			
		||||
where id = $1
 | 
			
		||||
 | 
			
		||||
-- name: get-permissions
 | 
			
		||||
SELECT unnest(r.permissions)
 | 
			
		||||
FROM users u
 | 
			
		||||
JOIN roles r ON r.name = ANY(u.roles)
 | 
			
		||||
WHERE u.id = $1
 | 
			
		||||
@@ -51,6 +51,7 @@ type queries struct {
 | 
			
		||||
	GetUsers        *sqlx.Stmt `query:"get-users"`
 | 
			
		||||
	GetUser         *sqlx.Stmt `query:"get-user"`
 | 
			
		||||
	GetEmail        *sqlx.Stmt `query:"get-email"`
 | 
			
		||||
	GetPermissions  *sqlx.Stmt `query:"get-permissions"`
 | 
			
		||||
	GetUserByEmail  *sqlx.Stmt `query:"get-user-by-email"`
 | 
			
		||||
	UpdateUser      *sqlx.Stmt `query:"update-user"`
 | 
			
		||||
	SetUserPassword *sqlx.Stmt `query:"set-user-password"`
 | 
			
		||||
@@ -160,6 +161,15 @@ func (u *Manager) GetEmail(id int, uuid string) (string, error) {
 | 
			
		||||
	return email, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (u *Manager) GetPermissions(id int) ([]string, error) {
 | 
			
		||||
	var permissions []string
 | 
			
		||||
	if err := u.q.GetPermissions.Select(&permissions, id); err != nil {
 | 
			
		||||
		u.lo.Error("error fetching user permissions", "error", err)
 | 
			
		||||
		return permissions, envelope.NewError(envelope.GeneralError, "Error fetching user permissions", nil)
 | 
			
		||||
	}
 | 
			
		||||
	return permissions, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (u *Manager) verifyPassword(pwd []byte, pwdHash string) error {
 | 
			
		||||
	err := bcrypt.CompareHashAndPassword([]byte(pwdHash), pwd)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
 
 | 
			
		||||
@@ -162,7 +162,7 @@ func (c *Hub) BroadcastConversationAssignment(userID int, conversationUUID strin
 | 
			
		||||
	c.marshalAndPush(message, []int{userID})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (c *Hub) BroadcastConversationPropertyUpdate(conversationUUID, prop string, val string) {
 | 
			
		||||
func (c *Hub) BroadcastConversationPropertyUpdate(conversationUUID, prop string, value string) {
 | 
			
		||||
	userIDs, ok := c.ConversationSubs[conversationUUID]
 | 
			
		||||
	if !ok || len(userIDs) == 0 {
 | 
			
		||||
		return
 | 
			
		||||
@@ -173,7 +173,7 @@ func (c *Hub) BroadcastConversationPropertyUpdate(conversationUUID, prop string,
 | 
			
		||||
		Data: map[string]interface{}{
 | 
			
		||||
			"uuid": conversationUUID,
 | 
			
		||||
			"prop": prop,
 | 
			
		||||
			"val":  val,
 | 
			
		||||
			"val":  value,
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user