mirror of
https://github.com/abhinavxd/libredesk.git
synced 2025-11-13 18:36:03 +00:00
refactor.
This commit is contained in:
@@ -1,8 +1,6 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/zerodha/fastglue"
|
"github.com/zerodha/fastglue"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -12,7 +10,7 @@ func handleGetCannedResponses(r *fastglue.Request) error {
|
|||||||
)
|
)
|
||||||
c, err := app.cannedRespManager.GetAll()
|
c, err := app.cannedRespManager.GetAll()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return r.SendErrorEnvelope(http.StatusInternalServerError, "Error fetching canned responses", nil, "")
|
return sendErrorEnvelope(r, err)
|
||||||
}
|
}
|
||||||
return r.SendEnvelope(c)
|
return r.SendEnvelope(c)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,18 +15,20 @@ func initHandlers(g *fastglue.Fastglue, hub *ws.Hub) {
|
|||||||
g.POST("/api/login", handleLogin)
|
g.POST("/api/login", handleLogin)
|
||||||
g.GET("/api/logout", handleLogout)
|
g.GET("/api/logout", handleLogout)
|
||||||
|
|
||||||
|
g.GET("/api/settings", auth(handleGetSettings))
|
||||||
|
|
||||||
// Conversation.
|
// Conversation.
|
||||||
g.GET("/api/conversations/all", auth(handleGetAllConversations, "conversations:all"))
|
g.GET("/api/conversations/all", auth(handleGetAllConversations, "conversation:all"))
|
||||||
g.GET("/api/conversations/team", auth(handleGetTeamConversations, "conversations:team"))
|
g.GET("/api/conversations/team", auth(handleGetTeamConversations, "conversation:team"))
|
||||||
g.GET("/api/conversations/assigned", auth(handleGetAssignedConversations, "conversations:assigned"))
|
g.GET("/api/conversations/assigned", auth(handleGetAssignedConversations, "conversation:assigned"))
|
||||||
|
|
||||||
g.GET("/api/conversations/{uuid}", auth(handleGetConversation))
|
g.GET("/api/conversations/{uuid}", auth(handleGetConversation))
|
||||||
g.GET("/api/conversations/{uuid}/participants", auth(handleGetConversationParticipants))
|
g.GET("/api/conversations/{uuid}/participants", auth(handleGetConversationParticipants))
|
||||||
g.PUT("/api/conversations/{uuid}/last-seen", auth(handleUpdateAssigneeLastSeen))
|
g.PUT("/api/conversations/{uuid}/last-seen", auth(handleUpdateAssigneeLastSeen))
|
||||||
g.PUT("/api/conversations/{uuid}/assignee/user", auth(handleUpdateUserAssignee))
|
g.PUT("/api/conversations/{uuid}/assignee/user", auth(handleUpdateUserAssignee))
|
||||||
g.PUT("/api/conversations/{uuid}/assignee/team", auth(handleUpdateTeamAssignee))
|
g.PUT("/api/conversations/{uuid}/assignee/team", auth(handleUpdateTeamAssignee))
|
||||||
g.PUT("/api/conversations/{uuid}/priority", auth(handleUpdatePriority))
|
g.PUT("/api/conversations/{uuid}/priority", auth(handleUpdatePriority, "conversation:edit_priority"))
|
||||||
g.PUT("/api/conversations/{uuid}/status", auth(handleUpdateStatus))
|
g.PUT("/api/conversations/{uuid}/status", auth(handleUpdateStatus, "conversation:edit_status"))
|
||||||
g.POST("/api/conversations/{uuid}/tags", auth(handleAddConversationTags))
|
g.POST("/api/conversations/{uuid}/tags", auth(handleAddConversationTags))
|
||||||
|
|
||||||
// Message.
|
// Message.
|
||||||
@@ -45,23 +47,23 @@ func initHandlers(g *fastglue.Fastglue, hub *ws.Hub) {
|
|||||||
g.POST("/api/file/upload", auth(handleFileUpload))
|
g.POST("/api/file/upload", auth(handleFileUpload))
|
||||||
|
|
||||||
// User.
|
// User.
|
||||||
g.GET("/api/users/me", auth(handleGetCurrentUser))
|
g.GET("/api/users/me", auth(handleGetCurrentUser, "users:manage"))
|
||||||
g.GET("/api/users", auth(handleGetUsers))
|
g.GET("/api/users", auth(handleGetUsers, "users:manage"))
|
||||||
g.GET("/api/users/{id}", auth(handleGetUser))
|
g.GET("/api/users/{id}", auth(handleGetUser, "users:manage"))
|
||||||
g.PUT("/api/users/{id}", auth(handleUpdateUser))
|
g.PUT("/api/users/{id}", auth(handleUpdateUser, "users:manage"))
|
||||||
g.POST("/api/users", auth(handleCreateUser))
|
g.POST("/api/users", auth(handleCreateUser, "users:manage"))
|
||||||
|
|
||||||
// Team.
|
// Team.
|
||||||
g.GET("/api/teams", auth(handleGetTeams))
|
g.GET("/api/teams", auth(handleGetTeams, "teams:manage"))
|
||||||
g.GET("/api/teams/{id}", auth(handleGetTeam))
|
g.GET("/api/teams/{id}", auth(handleGetTeam, "teams:manage"))
|
||||||
g.PUT("/api/teams/{id}", auth(handleUpdateTeam))
|
g.PUT("/api/teams/{id}", auth(handleUpdateTeam, "teams:manage"))
|
||||||
g.POST("/api/teams", auth(handleCreateTeam))
|
g.POST("/api/teams", auth(handleCreateTeam, "teams:manage"))
|
||||||
|
|
||||||
// Tags.
|
// Tags.
|
||||||
g.GET("/api/tags", auth(handleGetTags))
|
g.GET("/api/tags", auth(handleGetTags))
|
||||||
|
|
||||||
// i18n.
|
// i18n.
|
||||||
g.GET("/api/lang/{lang}", handleGetI18nLang)
|
g.GET("/api/lang/{lang}", auth(handleGetI18nLang))
|
||||||
|
|
||||||
// Websocket.
|
// Websocket.
|
||||||
g.GET("/api/ws", auth(func(r *fastglue.Request) error {
|
g.GET("/api/ws", auth(func(r *fastglue.Request) error {
|
||||||
@@ -69,27 +71,27 @@ func initHandlers(g *fastglue.Fastglue, hub *ws.Hub) {
|
|||||||
}))
|
}))
|
||||||
|
|
||||||
// Automation rules.
|
// Automation rules.
|
||||||
g.GET("/api/automation/rules", handleGetAutomationRules)
|
g.GET("/api/automation/rules", auth(handleGetAutomationRules, "automations:manage"))
|
||||||
g.GET("/api/automation/rules/{id}", handleGetAutomationRule)
|
g.GET("/api/automation/rules/{id}", auth(handleGetAutomationRule, "automations:manage"))
|
||||||
g.POST("/api/automation/rules", handleCreateAutomationRule)
|
g.POST("/api/automation/rules", auth(handleCreateAutomationRule, "automations:manage"))
|
||||||
g.PUT("/api/automation/rules/{id}/toggle", handleToggleAutomationRule)
|
g.PUT("/api/automation/rules/{id}/toggle", auth(handleToggleAutomationRule, "automations:manage"))
|
||||||
g.PUT("/api/automation/rules/{id}", handleUpdateAutomationRule)
|
g.PUT("/api/automation/rules/{id}", auth(handleUpdateAutomationRule, "automations:manage"))
|
||||||
g.DELETE("/api/automation/rules/{id}", handleDeleteAutomationRule)
|
g.DELETE("/api/automation/rules/{id}", auth(handleDeleteAutomationRule, "automations:manage"))
|
||||||
|
|
||||||
// Inboxes.
|
// Inboxes.
|
||||||
g.GET("/api/inboxes", handleGetInboxes)
|
g.GET("/api/inboxes", auth(handleGetInboxes, "inboxes:manage"))
|
||||||
g.GET("/api/inboxes/{id}", handleGetInbox)
|
g.GET("/api/inboxes/{id}", auth(handleGetInbox, "inboxes:manage"))
|
||||||
g.POST("/api/inboxes", handleCreateInbox)
|
g.POST("/api/inboxes", auth(handleCreateInbox, "inboxes:manage"))
|
||||||
g.PUT("/api/inboxes/{id}/toggle", handleToggleInbox)
|
g.PUT("/api/inboxes/{id}/toggle", auth(handleToggleInbox, "inboxes:manage"))
|
||||||
g.PUT("/api/inboxes/{id}", handleUpdateInbox)
|
g.PUT("/api/inboxes/{id}", auth(handleUpdateInbox, "inboxes:manage"))
|
||||||
g.DELETE("/api/inboxes/{id}", handleDeleteInbox)
|
g.DELETE("/api/inboxes/{id}", auth(handleDeleteInbox, "inboxes:manage"))
|
||||||
|
|
||||||
// Roles.
|
// Roles.
|
||||||
g.GET("/api/roles", handleGetRoles)
|
g.GET("/api/roles", auth(handleGetRoles, "roles:manage"))
|
||||||
g.GET("/api/roles/{id}", handleGetRole)
|
g.GET("/api/roles/{id}", auth(handleGetRole, "roles:manage"))
|
||||||
g.POST("/api/roles", handleCreateRole)
|
g.POST("/api/roles", auth(handleCreateRole, "roles:manage"))
|
||||||
g.PUT("/api/roles/{id}", handleUpdateRole)
|
g.PUT("/api/roles/{id}", auth(handleUpdateRole, "roles:manage"))
|
||||||
g.DELETE("/api/roles/{id}", handleDeleteRole)
|
g.DELETE("/api/roles/{id}", auth(handleDeleteRole, "roles:manage"))
|
||||||
|
|
||||||
// Dashboard.
|
// Dashboard.
|
||||||
g.GET("/api/dashboard/me/counts", auth(handleUserDashboardCounts))
|
g.GET("/api/dashboard/me/counts", auth(handleUserDashboardCounts))
|
||||||
|
|||||||
60
cmd/init.go
60
cmd/init.go
@@ -3,6 +3,7 @@ package main
|
|||||||
import (
|
import (
|
||||||
"cmp"
|
"cmp"
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
@@ -10,7 +11,6 @@ import (
|
|||||||
|
|
||||||
"github.com/abhinavxd/artemis/internal/attachment"
|
"github.com/abhinavxd/artemis/internal/attachment"
|
||||||
"github.com/abhinavxd/artemis/internal/attachment/stores/s3"
|
"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/autoassigner"
|
||||||
"github.com/abhinavxd/artemis/internal/automation"
|
"github.com/abhinavxd/artemis/internal/automation"
|
||||||
"github.com/abhinavxd/artemis/internal/cannedresp"
|
"github.com/abhinavxd/artemis/internal/cannedresp"
|
||||||
@@ -23,6 +23,7 @@ import (
|
|||||||
notifier "github.com/abhinavxd/artemis/internal/notification"
|
notifier "github.com/abhinavxd/artemis/internal/notification"
|
||||||
emailnotifier "github.com/abhinavxd/artemis/internal/notification/providers/email"
|
emailnotifier "github.com/abhinavxd/artemis/internal/notification/providers/email"
|
||||||
"github.com/abhinavxd/artemis/internal/role"
|
"github.com/abhinavxd/artemis/internal/role"
|
||||||
|
"github.com/abhinavxd/artemis/internal/setting"
|
||||||
"github.com/abhinavxd/artemis/internal/tag"
|
"github.com/abhinavxd/artemis/internal/tag"
|
||||||
"github.com/abhinavxd/artemis/internal/team"
|
"github.com/abhinavxd/artemis/internal/team"
|
||||||
"github.com/abhinavxd/artemis/internal/template"
|
"github.com/abhinavxd/artemis/internal/template"
|
||||||
@@ -30,8 +31,9 @@ import (
|
|||||||
"github.com/abhinavxd/artemis/internal/ws"
|
"github.com/abhinavxd/artemis/internal/ws"
|
||||||
"github.com/jmoiron/sqlx"
|
"github.com/jmoiron/sqlx"
|
||||||
"github.com/knadh/go-i18n"
|
"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/parsers/toml"
|
||||||
|
"github.com/knadh/koanf/providers/confmap"
|
||||||
"github.com/knadh/koanf/providers/file"
|
"github.com/knadh/koanf/providers/file"
|
||||||
"github.com/knadh/koanf/providers/posflag"
|
"github.com/knadh/koanf/providers/posflag"
|
||||||
"github.com/knadh/koanf/providers/rawbytes"
|
"github.com/knadh/koanf/providers/rawbytes"
|
||||||
@@ -122,10 +124,38 @@ func initFS() stuffbin.FileSystem {
|
|||||||
log.Fatalf("error initializing FS: %v", err)
|
log.Fatalf("error initializing FS: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fmt.Println(fs.List())
|
|
||||||
return fs
|
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.
|
// initSessionManager initializes and returns a simplesessions.Manager instance.
|
||||||
func initSessionManager(rd *redis.Client) *simplesessions.Manager {
|
func initSessionManager(rd *redis.Client) *simplesessions.Manager {
|
||||||
maxAge := ko.Duration("app.session.cookie_max_age")
|
maxAge := ko.Duration("app.session.cookie_max_age")
|
||||||
@@ -159,8 +189,8 @@ func initUserManager(i18n *i18n.I18n, DB *sqlx.DB) *user.Manager {
|
|||||||
return mgr
|
return mgr
|
||||||
}
|
}
|
||||||
|
|
||||||
func initConversations(i18n *i18n.I18n, hub *ws.Hub, db *sqlx.DB) *conversation.Manager {
|
func initConversations(i18n *i18n.I18n, hub *ws.Hub, n notifier.Notifier, db *sqlx.DB) *conversation.Manager {
|
||||||
c, err := conversation.New(hub, i18n, conversation.Opts{
|
c, err := conversation.New(hub, i18n, n, conversation.Opts{
|
||||||
DB: db,
|
DB: db,
|
||||||
Lo: initLogger("conversation_manager"),
|
Lo: initLogger("conversation_manager"),
|
||||||
ReferenceNumPattern: ko.String("app.constants.conversation_reference_number_pattern"),
|
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
|
return engine
|
||||||
}
|
}
|
||||||
|
|
||||||
func initAutoAssignmentEngine(teamMgr *team.Manager, userMgr *user.Manager, convMgr *conversation.Manager, msgMgr *message.Manager,
|
func initAutoAssigner(teamManager *team.Manager, conversationManager *conversation.Manager) *autoassigner.Engine {
|
||||||
notifier notifier.Notifier, hub *ws.Hub) *autoassigner.Engine {
|
e, err := autoassigner.New(teamManager, conversationManager, initLogger("autoassigner"))
|
||||||
var lo = initLogger("auto_assignment_engine")
|
|
||||||
engine, err := autoassigner.New(teamMgr, userMgr, convMgr, msgMgr, notifier, hub, lo)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("error initializing auto assignment engine: %v", err)
|
log.Fatalf("error initializing auto assigner engine: %v", err)
|
||||||
}
|
}
|
||||||
return engine
|
return e
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func initNotifier(userStore notifier.UserStore, templateRenderer notifier.TemplateRenderer) notifier.Notifier {
|
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
|
var config email.Config
|
||||||
|
|
||||||
// Load JSON data into Koanf.
|
// 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)
|
log.Fatalf("error loading config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -32,7 +32,6 @@ func handleLogin(r *fastglue.Request) error {
|
|||||||
"first_name": user.FirstName,
|
"first_name": user.FirstName,
|
||||||
"last_name": user.LastName,
|
"last_name": user.LastName,
|
||||||
"team_id": user.TeamID,
|
"team_id": user.TeamID,
|
||||||
"permissions": user.Permissions,
|
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
app.lo.Error("error setting values in session", "error", err)
|
app.lo.Error("error setting values in session", "error", err)
|
||||||
return sendErrorEnvelope(r, envelope.NewError(envelope.GeneralError, app.i18n.T("user.errorAcquiringSession"), nil))
|
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"
|
"time"
|
||||||
|
|
||||||
"github.com/abhinavxd/artemis/internal/attachment"
|
"github.com/abhinavxd/artemis/internal/attachment"
|
||||||
uauth "github.com/abhinavxd/artemis/internal/auth"
|
|
||||||
"github.com/abhinavxd/artemis/internal/automation"
|
"github.com/abhinavxd/artemis/internal/automation"
|
||||||
"github.com/abhinavxd/artemis/internal/cannedresp"
|
"github.com/abhinavxd/artemis/internal/cannedresp"
|
||||||
"github.com/abhinavxd/artemis/internal/contact"
|
"github.com/abhinavxd/artemis/internal/contact"
|
||||||
@@ -17,6 +16,7 @@ import (
|
|||||||
"github.com/abhinavxd/artemis/internal/inbox"
|
"github.com/abhinavxd/artemis/internal/inbox"
|
||||||
"github.com/abhinavxd/artemis/internal/message"
|
"github.com/abhinavxd/artemis/internal/message"
|
||||||
"github.com/abhinavxd/artemis/internal/role"
|
"github.com/abhinavxd/artemis/internal/role"
|
||||||
|
"github.com/abhinavxd/artemis/internal/setting"
|
||||||
"github.com/abhinavxd/artemis/internal/tag"
|
"github.com/abhinavxd/artemis/internal/tag"
|
||||||
"github.com/abhinavxd/artemis/internal/team"
|
"github.com/abhinavxd/artemis/internal/team"
|
||||||
"github.com/abhinavxd/artemis/internal/upload"
|
"github.com/abhinavxd/artemis/internal/upload"
|
||||||
@@ -25,6 +25,7 @@ import (
|
|||||||
"github.com/knadh/go-i18n"
|
"github.com/knadh/go-i18n"
|
||||||
"github.com/knadh/koanf/v2"
|
"github.com/knadh/koanf/v2"
|
||||||
"github.com/knadh/stuffbin"
|
"github.com/knadh/stuffbin"
|
||||||
|
"github.com/redis/go-redis/v9"
|
||||||
"github.com/valyala/fasthttp"
|
"github.com/valyala/fasthttp"
|
||||||
"github.com/zerodha/fastglue"
|
"github.com/zerodha/fastglue"
|
||||||
"github.com/zerodha/logf"
|
"github.com/zerodha/logf"
|
||||||
@@ -45,8 +46,10 @@ const (
|
|||||||
type App struct {
|
type App struct {
|
||||||
constants consts
|
constants consts
|
||||||
fs stuffbin.FileSystem
|
fs stuffbin.FileSystem
|
||||||
|
rdb *redis.Client
|
||||||
i18n *i18n.I18n
|
i18n *i18n.I18n
|
||||||
lo *logf.Logger
|
lo *logf.Logger
|
||||||
|
settingsManager *setting.Manager
|
||||||
roleManager *role.Manager
|
roleManager *role.Manager
|
||||||
contactManager *contact.Manager
|
contactManager *contact.Manager
|
||||||
userManager *user.Manager
|
userManager *user.Manager
|
||||||
@@ -54,7 +57,6 @@ type App struct {
|
|||||||
sessManager *simplesessions.Manager
|
sessManager *simplesessions.Manager
|
||||||
tagManager *tag.Manager
|
tagManager *tag.Manager
|
||||||
messageManager *message.Manager
|
messageManager *message.Manager
|
||||||
auth *uauth.Manager
|
|
||||||
inboxManager *inbox.Manager
|
inboxManager *inbox.Manager
|
||||||
uploadManager *upload.Manager
|
uploadManager *upload.Manager
|
||||||
attachmentManager *attachment.Manager
|
attachmentManager *attachment.Manager
|
||||||
@@ -70,6 +72,11 @@ func main() {
|
|||||||
// Load the config files into Koanf.
|
// Load the config files into Koanf.
|
||||||
initConfig(ko)
|
initConfig(ko)
|
||||||
|
|
||||||
|
// Load app settings into Koanf.
|
||||||
|
db := initDB()
|
||||||
|
settingManager := initSettingsManager(db)
|
||||||
|
loadSettings(settingManager)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
shutdownCh = make(chan struct{})
|
shutdownCh = make(chan struct{})
|
||||||
ctx, stop = signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
|
ctx, stop = signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
|
||||||
@@ -77,8 +84,7 @@ func main() {
|
|||||||
fs = initFS()
|
fs = initFS()
|
||||||
i18n = initI18n(fs)
|
i18n = initI18n(fs)
|
||||||
lo = initLogger("artemis")
|
lo = initLogger("artemis")
|
||||||
rd = initRedis()
|
rdb = initRedis()
|
||||||
db = initDB()
|
|
||||||
templateManager = initTemplateManager(db)
|
templateManager = initTemplateManager(db)
|
||||||
attachmentManager = initAttachmentsManager(db)
|
attachmentManager = initAttachmentsManager(db)
|
||||||
contactManager = initContactManager(db)
|
contactManager = initContactManager(db)
|
||||||
@@ -86,10 +92,10 @@ func main() {
|
|||||||
teamManager = initTeamManager(db)
|
teamManager = initTeamManager(db)
|
||||||
userManager = initUserManager(i18n, db)
|
userManager = initUserManager(i18n, db)
|
||||||
notifier = initNotifier(userManager, templateManager)
|
notifier = initNotifier(userManager, templateManager)
|
||||||
conversationManager = initConversations(i18n, wsHub, db)
|
conversationManager = initConversations(i18n, wsHub, notifier, db)
|
||||||
automationEngine = initAutomationEngine(db, userManager)
|
automationEngine = initAutomationEngine(db, userManager)
|
||||||
messageManager = initMessages(db, wsHub, userManager, teamManager, contactManager, attachmentManager, conversationManager, inboxManager, automationEngine, templateManager)
|
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.
|
// Set message store for conversation manager.
|
||||||
@@ -106,8 +112,8 @@ func main() {
|
|||||||
automationEngine.SetConversationStore(conversationManager)
|
automationEngine.SetConversationStore(conversationManager)
|
||||||
go automationEngine.Serve(ctx)
|
go automationEngine.Serve(ctx)
|
||||||
|
|
||||||
// Start conversation auto assigner engine.
|
// Start conversation auto assigner.
|
||||||
go autoAssignerEngine.Serve(ctx, ko.MustDuration("autoassigner.assign_interval"))
|
go autoassigner.Serve(ctx, ko.MustDuration("autoassigner.assign_interval"))
|
||||||
|
|
||||||
// Start inserting incoming messages from all active inboxes and dispatch pending outgoing messages.
|
// Start inserting incoming messages from all active inboxes and dispatch pending outgoing messages.
|
||||||
go messageManager.StartDBInserts(ctx, ko.MustInt("message.reader_concurrency"))
|
go messageManager.StartDBInserts(ctx, ko.MustInt("message.reader_concurrency"))
|
||||||
@@ -116,8 +122,10 @@ func main() {
|
|||||||
// Init the app
|
// Init the app
|
||||||
var app = &App{
|
var app = &App{
|
||||||
lo: lo,
|
lo: lo,
|
||||||
|
rdb: rdb,
|
||||||
fs: fs,
|
fs: fs,
|
||||||
i18n: i18n,
|
i18n: i18n,
|
||||||
|
settingsManager: settingManager,
|
||||||
contactManager: contactManager,
|
contactManager: contactManager,
|
||||||
inboxManager: inboxManager,
|
inboxManager: inboxManager,
|
||||||
userManager: userManager,
|
userManager: userManager,
|
||||||
@@ -126,11 +134,10 @@ func main() {
|
|||||||
conversationManager: conversationManager,
|
conversationManager: conversationManager,
|
||||||
messageManager: messageManager,
|
messageManager: messageManager,
|
||||||
automationEngine: automationEngine,
|
automationEngine: automationEngine,
|
||||||
constants: initConstants(),
|
|
||||||
roleManager: initRoleManager(db),
|
roleManager: initRoleManager(db),
|
||||||
auth: initAuthManager(db),
|
constants: initConstants(),
|
||||||
tagManager: initTags(db),
|
tagManager: initTags(db),
|
||||||
sessManager: initSessionManager(rd),
|
sessManager: initSessionManager(rdb),
|
||||||
cannedRespManager: initCannedResponse(db),
|
cannedRespManager: initCannedResponse(db),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -159,7 +166,7 @@ func main() {
|
|||||||
|
|
||||||
log.Printf("%sShutting down the server. Please wait.\x1b[0m", colourRed)
|
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
|
// Signal to shutdown the server
|
||||||
shutdownCh <- struct{}{}
|
shutdownCh <- struct{}{}
|
||||||
|
|||||||
@@ -16,11 +16,12 @@ func handleGetMessages(r *fastglue.Request) error {
|
|||||||
app = r.Context.(*App)
|
app = r.Context.(*App)
|
||||||
uuid = r.RequestCtx.UserValue("uuid").(string)
|
uuid = r.RequestCtx.UserValue("uuid").(string)
|
||||||
)
|
)
|
||||||
|
|
||||||
msgs, err := app.messageManager.GetConversationMessages(uuid)
|
msgs, err := app.messageManager.GetConversationMessages(uuid)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return sendErrorEnvelope(r, err)
|
return sendErrorEnvelope(r, err)
|
||||||
}
|
}
|
||||||
// Generate URLs for all attachments.
|
|
||||||
for i := range msgs {
|
for i := range msgs {
|
||||||
for j := range msgs[i].Attachments {
|
for j := range msgs[i].Attachments {
|
||||||
msgs[i].Attachments[j].URL = app.attachmentManager.Store.GetURL(msgs[i].Attachments[j].UUID)
|
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)
|
app = r.Context.(*App)
|
||||||
uuid = r.RequestCtx.UserValue("uuid").(string)
|
uuid = r.RequestCtx.UserValue("uuid").(string)
|
||||||
)
|
)
|
||||||
|
|
||||||
msgs, err := app.messageManager.GetMessage(uuid)
|
msgs, err := app.messageManager.GetMessage(uuid)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return sendErrorEnvelope(r, err)
|
return sendErrorEnvelope(r, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate URLs for each of the attachments.
|
|
||||||
for i := range msgs {
|
for i := range msgs {
|
||||||
for j := range msgs[i].Attachments {
|
for j := range msgs[i].Attachments {
|
||||||
msgs[i].Attachments[j].URL = app.attachmentManager.Store.GetURL(msgs[i].Attachments[j].UUID)
|
msgs[i].Attachments[j].URL = app.attachmentManager.Store.GetURL(msgs[i].Attachments[j].UUID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return r.SendEnvelope(msgs)
|
return r.SendEnvelope(msgs)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,7 +93,6 @@ func handleSendMessage(r *fastglue.Request) error {
|
|||||||
Content: string(content),
|
Content: string(content),
|
||||||
ContentType: message.ContentTypeHTML,
|
ContentType: message.ContentTypeHTML,
|
||||||
Private: private,
|
Private: private,
|
||||||
Meta: "{}",
|
|
||||||
Attachments: attachments,
|
Attachments: attachments,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,5 +109,5 @@ func handleSendMessage(r *fastglue.Request) error {
|
|||||||
// Send WS update.
|
// Send WS update.
|
||||||
app.messageManager.BroadcastNewConversationMessage(msg, trimmedMessage)
|
app.messageManager.BroadcastNewConversationMessage(msg, trimmedMessage)
|
||||||
|
|
||||||
return r.SendEnvelope("Message sent")
|
return r.SendEnvelope(true)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/abhinavxd/artemis/internal/envelope"
|
"github.com/abhinavxd/artemis/internal/envelope"
|
||||||
@@ -11,7 +10,7 @@ import (
|
|||||||
"github.com/zerodha/simplesessions/v3"
|
"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 {
|
return func(r *fastglue.Request) error {
|
||||||
var (
|
var (
|
||||||
app = r.Context.(*App)
|
app = r.Context.(*App)
|
||||||
@@ -37,14 +36,20 @@ func auth(handler fastglue.FastRequestHandler, perms ...string) fastglue.FastReq
|
|||||||
firstName, _ = sess.String(sessVals["first_name"], nil)
|
firstName, _ = sess.String(sessVals["first_name"], nil)
|
||||||
lastName, _ = sess.String(sessVals["last_name"], nil)
|
lastName, _ = sess.String(sessVals["last_name"], nil)
|
||||||
teamID, _ = sess.Int(sessVals["team_id"], nil)
|
teamID, _ = sess.Int(sessVals["team_id"], nil)
|
||||||
// TODO: FIX.
|
|
||||||
p, _ = sess.Bytes(sessVals["permissions"], nil)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
fmt.Printf("%+v perms ", p)
|
|
||||||
|
|
||||||
if userID > 0 {
|
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{
|
r.RequestCtx.SetUserValue("user", umodels.User{
|
||||||
ID: userID,
|
ID: userID,
|
||||||
Email: email,
|
Email: email,
|
||||||
@@ -53,14 +58,6 @@ func auth(handler fastglue.FastRequestHandler, perms ...string) fastglue.FastReq
|
|||||||
TeamID: teamID,
|
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)
|
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
|
// authPage middleware makes sure user is logged in to access the page
|
||||||
// else redirects to login page.
|
// else redirects to login page.
|
||||||
func authPage(handler fastglue.FastRequestHandler) fastglue.FastRequestHandler {
|
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',
|
title: 'Automations',
|
||||||
href: '/admin/automations',
|
href: '/admin/automations',
|
||||||
description: 'Create automations and time triggers'
|
description: 'Create automations and time triggers'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Notifications',
|
||||||
|
href: '/admin/notifications',
|
||||||
|
description: 'Manage notifications for your agents'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-if="showTable">
|
<div>
|
||||||
<div class="flex justify-between mb-5">
|
<div class="flex justify-between mb-5">
|
||||||
<div>
|
<div>
|
||||||
<span class="admin-title">Inboxes</span>
|
<span class="admin-title">Inboxes</span>
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
<DataTable :columns="columns" :data="data" />
|
<DataTable :columns="columns" :data="data" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else>
|
<div>
|
||||||
<router-view></router-view>
|
<router-view></router-view>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</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>
|
<FormItem>
|
||||||
<FormLabel>Select role</FormLabel>
|
<FormLabel>Select role</FormLabel>
|
||||||
<FormControl>
|
<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">
|
<div class="flex gap-2 flex-wrap items-center px-3">
|
||||||
<TagsInputItem v-for="item in roles" :key="item" :value="item">
|
<TagsInputItem v-for="item in roles" :key="item" :value="item">
|
||||||
<TagsInputItemText />
|
<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">
|
<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 />
|
<CommandEmpty />
|
||||||
<CommandGroup>
|
<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') {
|
if (typeof ev.detail.value === 'string') {
|
||||||
searchTerm = ''
|
searchTerm = ''
|
||||||
roles.push(ev.detail.value)
|
roles.push(ev.detail.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (filteredFrameworks.length === 0) {
|
if (filteredUsers.length === 0) {
|
||||||
open = false
|
open = false
|
||||||
}
|
}
|
||||||
}">
|
}">
|
||||||
{{ framework.label }}
|
{{ user.label }}
|
||||||
</CommandItem>
|
</CommandItem>
|
||||||
</CommandGroup>
|
</CommandGroup>
|
||||||
</CommandList>
|
</CommandList>
|
||||||
@@ -146,7 +146,7 @@ const roles = ref([])
|
|||||||
const open = ref(false)
|
const open = ref(false)
|
||||||
const searchTerm = ref('')
|
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({
|
const props = defineProps({
|
||||||
initialValues: {
|
initialValues: {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import Team from '@/components/admin/team/TeamSection.vue'
|
|||||||
import Teams from '@/components/admin/team/teams/TeamsCard.vue'
|
import Teams from '@/components/admin/team/teams/TeamsCard.vue'
|
||||||
import Users from '@/components/admin/team/users/UsersCard.vue'
|
import Users from '@/components/admin/team/users/UsersCard.vue'
|
||||||
import Automation from '@/components/admin/automation/Automation.vue'
|
import Automation from '@/components/admin/automation/Automation.vue'
|
||||||
|
import NotificationTab from '@/components/admin/notification/NotificationTab.vue'
|
||||||
|
|
||||||
const routes = [
|
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.
|
// Fallback to dashboard.
|
||||||
{
|
{
|
||||||
path: '/:pathMatch(.*)*',
|
path: '/:pathMatch(.*)*',
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<!-- Resizable panel last resize value is stored in the localstorage -->
|
<!-- Resizable panel last resize value is stored in the localstorage -->
|
||||||
<ResizablePanelGroup direction="horizontal" auto-save-id="conversation.vue.resizable.panel">
|
<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>
|
<ConversationList></ConversationList>
|
||||||
</ResizablePanel>
|
</ResizablePanel>
|
||||||
<ResizableHandle />
|
<ResizableHandle />
|
||||||
|
|||||||
1
go.mod
1
go.mod
@@ -47,6 +47,7 @@ require (
|
|||||||
github.com/jaytaylor/html2text v0.0.0-20230321000545-74c2419ad056 // indirect
|
github.com/jaytaylor/html2text v0.0.0-20230321000545-74c2419ad056 // indirect
|
||||||
github.com/klauspost/compress v1.17.8 // indirect
|
github.com/klauspost/compress v1.17.8 // indirect
|
||||||
github.com/knadh/koanf/maps v0.1.1 // 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/kr/text v0.2.0 // indirect
|
||||||
github.com/mattn/go-runewidth v0.0.15 // indirect
|
github.com/mattn/go-runewidth v0.0.15 // indirect
|
||||||
github.com/mitchellh/copystructure v1.2.0 // 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/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 h1:S2hLqS4TgWZYj4/7mI5m1CQQcWurxUz6ODgOub/6LCI=
|
||||||
github.com/knadh/koanf/parsers/toml v0.1.0/go.mod h1:yUprhq6eo3GbyVXFFMdbfZSo928ksS+uo0FFqNMnO18=
|
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 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/file v0.1.0/go.mod h1:rjJ/nHQl64iYCtAW2QQnF0eSmDEX/YZ/eNFj5yR6BvA=
|
||||||
github.com/knadh/koanf/providers/posflag v0.1.0 h1:mKJlLrKPcAP7Ootf4pBZWJ6J+4wHYujwipe7Ie3qW6U=
|
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
|
package autoassigner
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
|
"strconv"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/abhinavxd/artemis/internal/conversation"
|
"github.com/abhinavxd/artemis/internal/conversation"
|
||||||
"github.com/abhinavxd/artemis/internal/conversation/models"
|
"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/team"
|
||||||
"github.com/abhinavxd/artemis/internal/user"
|
|
||||||
"github.com/abhinavxd/artemis/internal/ws"
|
|
||||||
"github.com/mr-karan/balance"
|
"github.com/mr-karan/balance"
|
||||||
"github.com/zerodha/logf"
|
"github.com/zerodha/logf"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
var (
|
||||||
roundRobinDefaultWeight = 1
|
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 {
|
type Engine struct {
|
||||||
teamRoundRobinBalancer map[int]*balance.Balance
|
roundRobinBalancer map[int]*balance.Balance
|
||||||
// Mutex to protect the balancer map
|
// Mutex to protect the balancer map
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
|
|
||||||
userIDMap map[string]int
|
conversationManager *conversation.Manager
|
||||||
convMgr *conversation.Manager
|
teamManager *team.Manager
|
||||||
teamMgr *team.Manager
|
|
||||||
userMgr *user.Manager
|
|
||||||
msgMgr *message.Manager
|
|
||||||
lo *logf.Logger
|
lo *logf.Logger
|
||||||
hub *ws.Hub
|
|
||||||
notifier notifier.Notifier
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// New creates a new instance of the Engine.
|
// New initializes a new Engine instance, set up with the provided team manager,
|
||||||
func New(teamMgr *team.Manager, userMgr *user.Manager, convMgr *conversation.Manager, msgMgr *message.Manager,
|
// conversation manager, and logger.
|
||||||
notifier notifier.Notifier, hub *ws.Hub, lo *logf.Logger) (*Engine, error) {
|
func New(teamManager *team.Manager, conversationManager *conversation.Manager, lo *logf.Logger) (*Engine, error) {
|
||||||
var e = Engine{
|
var e = Engine{
|
||||||
notifier: notifier,
|
conversationManager: conversationManager,
|
||||||
convMgr: convMgr,
|
teamManager: teamManager,
|
||||||
teamMgr: teamMgr,
|
|
||||||
msgMgr: msgMgr,
|
|
||||||
userMgr: userMgr,
|
|
||||||
lo: lo,
|
lo: lo,
|
||||||
hub: hub,
|
|
||||||
mu: sync.Mutex{},
|
mu: sync.Mutex{},
|
||||||
userIDMap: map[string]int{},
|
|
||||||
}
|
}
|
||||||
balancer, err := e.populateBalancerPool()
|
balancer, err := e.populateTeamBalancer()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
e.teamRoundRobinBalancer = balancer
|
e.roundRobinBalancer = balancer
|
||||||
return &e, nil
|
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) {
|
func (e *Engine) Serve(ctx context.Context, interval time.Duration) {
|
||||||
ticker := time.NewTicker(interval)
|
ticker := time.NewTicker(interval)
|
||||||
defer ticker.Stop()
|
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 {
|
func (e *Engine) RefreshBalancer() error {
|
||||||
e.mu.Lock()
|
e.mu.Lock()
|
||||||
defer e.mu.Unlock()
|
defer e.mu.Unlock()
|
||||||
|
|
||||||
balancer, err := e.populateBalancerPool()
|
balancer, err := e.populateTeamBalancer()
|
||||||
if err != nil {
|
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
|
return err
|
||||||
}
|
}
|
||||||
e.teamRoundRobinBalancer = balancer
|
e.roundRobinBalancer = balancer
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// populateBalancerPool populates the team balancer bool with the team members.
|
// populateTeamBalancer populates the team balancer pool with the team members.
|
||||||
func (e *Engine) populateBalancerPool() (map[int]*balance.Balance, error) {
|
func (e *Engine) populateTeamBalancer() (map[int]*balance.Balance, error) {
|
||||||
var (
|
var (
|
||||||
balancer = make(map[int]*balance.Balance)
|
balancer = make(map[int]*balance.Balance)
|
||||||
teams, err = e.teamMgr.GetAll()
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
teams, err := e.teamManager.GetAll()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, team := range teams {
|
for _, team := range teams {
|
||||||
users, err := e.teamMgr.GetTeamMembers(team.Name)
|
users, err := e.teamManager.GetTeamMembers(team.Name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -109,17 +102,16 @@ func (e *Engine) populateBalancerPool() (map[int]*balance.Balance, error) {
|
|||||||
if _, ok := balancer[team.ID]; !ok {
|
if _, ok := balancer[team.ID]; !ok {
|
||||||
balancer[team.ID] = balance.NewBalance()
|
balancer[team.ID] = balance.NewBalance()
|
||||||
}
|
}
|
||||||
// FIXME: Balancer only supports strings, using a map to store DB ids.
|
balancer[team.ID].Add(strconv.Itoa(user.ID), 1)
|
||||||
balancer[team.ID].Add(user.UUID, roundRobinDefaultWeight)
|
|
||||||
e.userIDMap[user.UUID] = user.ID
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return balancer, nil
|
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 {
|
func (e *Engine) assignConversations() error {
|
||||||
unassigned, err := e.convMgr.GetUnassigned()
|
unassigned, err := e.conversationManager.GetUnassigned()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -128,46 +120,34 @@ func (e *Engine) assignConversations() error {
|
|||||||
e.lo.Debug("found unassigned conversations", "count", len(unassigned))
|
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 {
|
for _, conversation := range unassigned {
|
||||||
// Get user uuid from the pool.
|
uid, err := e.getUserFromPool(conversation)
|
||||||
userUUID := e.getUser(conversation)
|
if err != nil {
|
||||||
if userUUID == "" {
|
e.lo.Error("error fetching user from balancer pool", "error", err)
|
||||||
e.lo.Warn("user uuid not found for round robin assignment", "team_id", conversation.AssignedTeamID.Int)
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get user ID from the map.
|
// Convert to int.
|
||||||
// FIXME: Balance only supports strings.
|
userID, err := strconv.Atoi(uid)
|
||||||
userID, ok := e.userIDMap[userUUID]
|
if err != nil {
|
||||||
if !ok {
|
e.lo.Error("error converting user id from string to int", "error", err)
|
||||||
e.lo.Warn("user id not found for user uuid", "uuid", userUUID, "team_id", conversation.AssignedTeamID.Int)
|
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update assignee and record the assigne change message.
|
// Assign conversation to this user.
|
||||||
if err := e.convMgr.UpdateUserAssignee(conversation.UUID, userID, systemUser); err != nil {
|
e.conversationManager.UpdateUserAssigneeBySystem(conversation.UUID, userID)
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send notification to the assignee.
|
|
||||||
e.notifier.SendAssignedConversationNotification([]string{userUUID}, conversation.UUID)
|
|
||||||
|
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// getUser returns user uuid from the team balancer pool.
|
// getUserFromPool returns user ID from the team balancer pool.
|
||||||
func (e *Engine) getUser(conversation models.Conversation) string {
|
func (e *Engine) getUserFromPool(conversation models.Conversation) (string, error) {
|
||||||
pool, ok := e.teamRoundRobinBalancer[conversation.AssignedTeamID.Int]
|
e.mu.Lock()
|
||||||
|
defer e.mu.Unlock()
|
||||||
|
|
||||||
|
pool, ok := e.roundRobinBalancer[conversation.AssignedTeamID.Int]
|
||||||
if !ok {
|
if !ok {
|
||||||
e.lo.Warn("team not found in balancer", "id", conversation.AssignedTeamID.Int)
|
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
|
package automation
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -23,12 +25,13 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Engine struct {
|
type Engine struct {
|
||||||
|
rules []models.Rule
|
||||||
|
rulesMu *sync.RWMutex
|
||||||
|
|
||||||
q queries
|
q queries
|
||||||
lo *logf.Logger
|
lo *logf.Logger
|
||||||
conversationStore ConversationStore
|
conversationStore ConversationStore
|
||||||
systemUser umodels.User
|
systemUser umodels.User
|
||||||
rulesMu *sync.RWMutex
|
|
||||||
rules []models.Rule
|
|
||||||
newConversationQ chan string
|
newConversationQ chan string
|
||||||
updateConversationQ chan string
|
updateConversationQ chan string
|
||||||
}
|
}
|
||||||
@@ -79,20 +82,19 @@ func New(systemUser umodels.User, opt Opts) (*Engine, error) {
|
|||||||
return e, nil
|
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) {
|
func (e *Engine) SetConversationStore(store ConversationStore) {
|
||||||
e.conversationStore = store
|
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) {
|
func (e *Engine) Serve(ctx context.Context) {
|
||||||
// TODO: Change to 1 hour.
|
ticker := time.NewTicker(1 * time.Hour)
|
||||||
ticker := time.NewTicker(30 * time.Second)
|
|
||||||
defer ticker.Stop()
|
defer ticker.Stop()
|
||||||
|
|
||||||
// Create separate semaphores for each channel
|
// Create separate semaphores for each channel
|
||||||
@@ -184,7 +186,6 @@ func (e *Engine) handleNewConversation(conversationUUID string, semaphore chan s
|
|||||||
defer func() { <-semaphore }()
|
defer func() { <-semaphore }()
|
||||||
conversation, err := e.conversationStore.Get(conversationUUID)
|
conversation, err := e.conversationStore.Get(conversationUUID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
e.lo.Error("error could not fetch conversations to evaluate new conversation rules", "conversation_uuid", conversationUUID)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
rules := e.filterRulesByType(models.RuleTypeNewConversation)
|
rules := e.filterRulesByType(models.RuleTypeNewConversation)
|
||||||
@@ -207,7 +208,6 @@ func (e *Engine) handleTimeTrigger(semaphore chan struct{}) {
|
|||||||
thirtyDaysAgo := time.Now().Add(-30 * 24 * time.Hour)
|
thirtyDaysAgo := time.Now().Add(-30 * 24 * time.Hour)
|
||||||
conversations, err := e.conversationStore.GetRecentConversations(thirtyDaysAgo)
|
conversations, err := e.conversationStore.GetRecentConversations(thirtyDaysAgo)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
e.lo.Error("error could not fetch conversations to evaluate time triggers")
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
rules := e.filterRulesByType(models.RuleTypeTimeTrigger)
|
rules := e.filterRulesByType(models.RuleTypeTimeTrigger)
|
||||||
@@ -257,6 +257,9 @@ func (e *Engine) queryRules() []models.Rule {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (e *Engine) filterRulesByType(ruleType string) []models.Rule {
|
func (e *Engine) filterRulesByType(ruleType string) []models.Rule {
|
||||||
|
e.rulesMu.RLock()
|
||||||
|
defer e.rulesMu.RUnlock()
|
||||||
|
|
||||||
var filteredRules []models.Rule
|
var filteredRules []models.Rule
|
||||||
for _, rule := range e.rules {
|
for _, rule := range e.rules {
|
||||||
if rule.Type == ruleType {
|
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)
|
e.lo.Debug("eval rule", "groups", len(rule.Groups), "rule", rule)
|
||||||
// At max there can be only 2 groups.
|
// At max there can be only 2 groups.
|
||||||
if len(rule.Groups) > 2 {
|
if len(rule.Groups) > 2 {
|
||||||
|
e.lo.Warn("more than 2 groups found for rules")
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
var results []bool
|
var results []bool
|
||||||
|
|||||||
@@ -2,9 +2,9 @@ package cannedresp
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"embed"
|
"embed"
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/abhinavxd/artemis/internal/dbutil"
|
"github.com/abhinavxd/artemis/internal/dbutil"
|
||||||
|
"github.com/abhinavxd/artemis/internal/envelope"
|
||||||
"github.com/jmoiron/sqlx"
|
"github.com/jmoiron/sqlx"
|
||||||
"github.com/zerodha/logf"
|
"github.com/zerodha/logf"
|
||||||
)
|
)
|
||||||
@@ -50,8 +50,8 @@ func New(opts Opts) (*Manager, error) {
|
|||||||
func (t *Manager) GetAll() ([]CannedResponse, error) {
|
func (t *Manager) GetAll() ([]CannedResponse, error) {
|
||||||
var c []CannedResponse
|
var c []CannedResponse
|
||||||
if err := t.q.GetAll.Select(&c); err != nil {
|
if err := t.q.GetAll.Select(&c); err != nil {
|
||||||
t.lo.Error("fetching canned responses", "error", err)
|
t.lo.Error("error fetching canned responses", "error", err)
|
||||||
return c, fmt.Errorf("error fetching canned responses")
|
return c, envelope.NewError(envelope.GeneralError, "Error fetching canned responses", nil)
|
||||||
}
|
}
|
||||||
return c, nil
|
return c, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import (
|
|||||||
"github.com/abhinavxd/artemis/internal/conversation/models"
|
"github.com/abhinavxd/artemis/internal/conversation/models"
|
||||||
"github.com/abhinavxd/artemis/internal/dbutil"
|
"github.com/abhinavxd/artemis/internal/dbutil"
|
||||||
"github.com/abhinavxd/artemis/internal/envelope"
|
"github.com/abhinavxd/artemis/internal/envelope"
|
||||||
|
notifier "github.com/abhinavxd/artemis/internal/notification"
|
||||||
"github.com/abhinavxd/artemis/internal/stringutil"
|
"github.com/abhinavxd/artemis/internal/stringutil"
|
||||||
umodels "github.com/abhinavxd/artemis/internal/user/models"
|
umodels "github.com/abhinavxd/artemis/internal/user/models"
|
||||||
"github.com/abhinavxd/artemis/internal/ws"
|
"github.com/abhinavxd/artemis/internal/ws"
|
||||||
@@ -74,6 +75,7 @@ const (
|
|||||||
|
|
||||||
type MessageStore interface {
|
type MessageStore interface {
|
||||||
RecordAssigneeUserChange(conversationUUID string, assigneeID int, actor umodels.User) error
|
RecordAssigneeUserChange(conversationUUID string, assigneeID int, actor umodels.User) error
|
||||||
|
RecordAssigneeUserChangeBySystem(conversationUUID string, assigneeID int) error
|
||||||
RecordAssigneeTeamChange(conversationUUID string, teamID int, actor umodels.User) error
|
RecordAssigneeTeamChange(conversationUUID string, teamID int, actor umodels.User) error
|
||||||
RecordPriorityChange(priority, conversationUUID string, actor umodels.User) error
|
RecordPriorityChange(priority, conversationUUID string, actor umodels.User) error
|
||||||
RecordStatusChange(status, conversationUUID string, actor umodels.User) error
|
RecordStatusChange(status, conversationUUID string, actor umodels.User) error
|
||||||
@@ -84,6 +86,7 @@ type Manager struct {
|
|||||||
db *sqlx.DB
|
db *sqlx.DB
|
||||||
hub *ws.Hub
|
hub *ws.Hub
|
||||||
i18n *i18n.I18n
|
i18n *i18n.I18n
|
||||||
|
notifier notifier.Notifier
|
||||||
messageStore MessageStore
|
messageStore MessageStore
|
||||||
q queries
|
q queries
|
||||||
ReferenceNumPattern string
|
ReferenceNumPattern string
|
||||||
@@ -121,7 +124,7 @@ type queries struct {
|
|||||||
DeleteTags *sqlx.Stmt `query:"delete-tags"`
|
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
|
var q queries
|
||||||
if err := dbutil.ScanSQLFile("queries.sql", &q, opts.DB, efs); err != nil {
|
if err := dbutil.ScanSQLFile("queries.sql", &q, opts.DB, efs); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -130,6 +133,7 @@ func New(hub *ws.Hub, i18n *i18n.I18n, opts Opts) (*Manager, error) {
|
|||||||
q: q,
|
q: q,
|
||||||
hub: hub,
|
hub: hub,
|
||||||
i18n: i18n,
|
i18n: i18n,
|
||||||
|
notifier: notfier,
|
||||||
db: opts.DB,
|
db: opts.DB,
|
||||||
lo: opts.Lo,
|
lo: opts.Lo,
|
||||||
ReferenceNumPattern: opts.ReferenceNumPattern,
|
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 {
|
if err := c.UpdateAssignee(uuid, assigneeID, assigneeTypeUser); err != nil {
|
||||||
return envelope.NewError(envelope.GeneralError, "Error updating assignee", 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 {
|
if err := c.messageStore.RecordAssigneeUserChange(uuid, assigneeID, actor); err != nil {
|
||||||
return envelope.NewError(envelope.GeneralError, "Error recording assignee change", nil)
|
return envelope.NewError(envelope.GeneralError, "Error recording assignee change", nil)
|
||||||
}
|
}
|
||||||
return 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 {
|
func (c *Manager) UpdateTeamAssignee(uuid string, teamID int, actor umodels.User) error {
|
||||||
if err := c.UpdateAssignee(uuid, teamID, assigneeTypeTeam); err != nil {
|
if err := c.UpdateAssignee(uuid, teamID, assigneeTypeTeam); err != nil {
|
||||||
return envelope.NewError(envelope.GeneralError, "Error updating assignee", 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)
|
c.lo.Error("error updating conversation assignee", "error", err)
|
||||||
return fmt.Errorf("error updating assignee")
|
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:
|
case assigneeTypeTeam:
|
||||||
if _, err := c.q.UpdateAssignedTeam.Exec(uuid, assigneeID); err != nil {
|
if _, err := c.q.UpdateAssignedTeam.Exec(uuid, assigneeID); err != nil {
|
||||||
c.lo.Error("error updating conversation assignee", "error", err)
|
c.lo.Error("error updating conversation assignee", "error", err)
|
||||||
return fmt.Errorf("error updating assignee")
|
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:
|
default:
|
||||||
return errors.New("invalid assignee type")
|
return errors.New("invalid assignee type")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,8 +17,10 @@ import (
|
|||||||
"github.com/abhinavxd/artemis/internal/contact"
|
"github.com/abhinavxd/artemis/internal/contact"
|
||||||
"github.com/abhinavxd/artemis/internal/conversation"
|
"github.com/abhinavxd/artemis/internal/conversation"
|
||||||
"github.com/abhinavxd/artemis/internal/dbutil"
|
"github.com/abhinavxd/artemis/internal/dbutil"
|
||||||
|
"github.com/abhinavxd/artemis/internal/envelope"
|
||||||
"github.com/abhinavxd/artemis/internal/inbox"
|
"github.com/abhinavxd/artemis/internal/inbox"
|
||||||
"github.com/abhinavxd/artemis/internal/message/models"
|
"github.com/abhinavxd/artemis/internal/message/models"
|
||||||
|
"github.com/abhinavxd/artemis/internal/systeminfo"
|
||||||
"github.com/abhinavxd/artemis/internal/team"
|
"github.com/abhinavxd/artemis/internal/team"
|
||||||
"github.com/abhinavxd/artemis/internal/template"
|
"github.com/abhinavxd/artemis/internal/template"
|
||||||
"github.com/abhinavxd/artemis/internal/user"
|
"github.com/abhinavxd/artemis/internal/user"
|
||||||
@@ -140,7 +142,7 @@ func (m *Manager) GetConversationMessages(uuid string) ([]models.Message, error)
|
|||||||
var messages []models.Message
|
var messages []models.Message
|
||||||
if err := m.q.GetMessages.Select(&messages, uuid); err != nil {
|
if err := m.q.GetMessages.Select(&messages, uuid); err != nil {
|
||||||
m.lo.Error("fetching messages from DB", "conversation_uuid", uuid, "error", err)
|
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
|
return messages, nil
|
||||||
}
|
}
|
||||||
@@ -149,7 +151,7 @@ func (m *Manager) GetMessage(uuid string) ([]models.Message, error) {
|
|||||||
var messages []models.Message
|
var messages []models.Message
|
||||||
if err := m.q.GetMessage.Select(&messages, uuid); err != nil {
|
if err := m.q.GetMessage.Select(&messages, uuid); err != nil {
|
||||||
m.lo.Error("fetching messages from DB", "conversation_uuid", uuid, "error", err)
|
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
|
return messages, nil
|
||||||
}
|
}
|
||||||
@@ -337,6 +339,18 @@ func (m *Manager) RecordAssigneeUserChange(conversationUUID string, assigneeID i
|
|||||||
return m.RecordActivity(ActivityAssignedUserChange, conversationUUID, assignee.FullName(), actor)
|
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 {
|
func (m *Manager) RecordAssigneeTeamChange(conversationUUID string, teamID int, actor umodels.User) error {
|
||||||
team, err := m.teamMgr.GetTeam(teamID)
|
team, err := m.teamMgr.GetTeam(teamID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ package notifier
|
|||||||
|
|
||||||
// Notifier defines the interface for sending notifications.
|
// Notifier defines the interface for sending notifications.
|
||||||
type Notifier interface {
|
type Notifier interface {
|
||||||
SendMessage(userUUIDs []string, subject, content string) error
|
SendMessage(userID []int, subject, content string) error
|
||||||
SendAssignedConversationNotification(userUUIDs []string, convUUID string) error
|
SendAssignedConversationNotification(userID []int, convUUID string) error
|
||||||
}
|
}
|
||||||
|
|
||||||
// TemplateRenderer defines the interface for rendering templates.
|
// TemplateRenderer defines the interface for rendering templates.
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ import (
|
|||||||
"github.com/zerodha/logf"
|
"github.com/zerodha/logf"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Notifier handles email notifications.
|
// Email
|
||||||
type Notifier struct {
|
type Email struct {
|
||||||
lo *logf.Logger
|
lo *logf.Logger
|
||||||
from string
|
from string
|
||||||
smtpPools []*smtppool.Pool
|
smtpPools []*smtppool.Pool
|
||||||
@@ -27,12 +27,12 @@ type Opts struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// New creates a new instance of email Notifier.
|
// 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)
|
pools, err := email.NewSmtpPool(smtpConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return &Notifier{
|
return &Email{
|
||||||
lo: opts.Lo,
|
lo: opts.Lo,
|
||||||
smtpPools: pools,
|
smtpPools: pools,
|
||||||
from: opts.FromEmail,
|
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.
|
// 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
|
var recipientEmails []string
|
||||||
for i := 0; i < len(userUUIDs); i++ {
|
for i := 0; i < len(userIDs); i++ {
|
||||||
userEmail, err := e.userStore.GetEmail(0, userUUIDs[i])
|
userEmail, err := e.userStore.GetEmail(userIDs[i], "")
|
||||||
if err != nil {
|
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
|
return err
|
||||||
}
|
}
|
||||||
recipientEmails = append(recipientEmails, userEmail)
|
recipientEmails = append(recipientEmails, userEmail)
|
||||||
@@ -80,15 +80,15 @@ func (e *Notifier) SendMessage(userUUIDs []string, subject, content string) erro
|
|||||||
return nil
|
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"
|
subject := "New conversation assigned to you"
|
||||||
link := fmt.Sprintf("http://localhost:5173/conversations/%s", convUUID)
|
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)
|
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.
|
// Send sends an email message.
|
||||||
func (e *Notifier) Send(m models.Message) error {
|
func (e *Email) Send(m models.Message) error {
|
||||||
var (
|
var (
|
||||||
ln = len(e.smtpPools)
|
ln = len(e.smtpPools)
|
||||||
srv *smtppool.Pool
|
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"`
|
TeamID int `db:"team_id" json:"team_id"`
|
||||||
Password string `db:"password" json:"-"`
|
Password string `db:"password" json:"-"`
|
||||||
TeamName null.String `db:"team_name" json:"team_name"`
|
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"`
|
SendWelcomeEmail bool `db:"-" json:"send_welcome_email"`
|
||||||
Permissions pq.StringArray `db:"permissions" json:"permissions"`
|
Permissions pq.StringArray `db:"permissions" json:"permissions"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ JOIN roles r ON r.name = ANY(u.roles)
|
|||||||
WHERE u.email = $1;
|
WHERE u.email = $1;
|
||||||
|
|
||||||
-- name: get-user
|
-- 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
|
FROM users
|
||||||
WHERE
|
WHERE
|
||||||
CASE
|
CASE
|
||||||
@@ -34,3 +34,9 @@ VALUES($1, $2, $3, $4, $5, $6, $7);
|
|||||||
UPDATE users
|
UPDATE users
|
||||||
set first_name = $2, last_name = $3, email = $4, team_id = $5, roles = $6, updated_at = now()
|
set first_name = $2, last_name = $3, email = $4, team_id = $5, roles = $6, updated_at = now()
|
||||||
where id = $1
|
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"`
|
GetUsers *sqlx.Stmt `query:"get-users"`
|
||||||
GetUser *sqlx.Stmt `query:"get-user"`
|
GetUser *sqlx.Stmt `query:"get-user"`
|
||||||
GetEmail *sqlx.Stmt `query:"get-email"`
|
GetEmail *sqlx.Stmt `query:"get-email"`
|
||||||
|
GetPermissions *sqlx.Stmt `query:"get-permissions"`
|
||||||
GetUserByEmail *sqlx.Stmt `query:"get-user-by-email"`
|
GetUserByEmail *sqlx.Stmt `query:"get-user-by-email"`
|
||||||
UpdateUser *sqlx.Stmt `query:"update-user"`
|
UpdateUser *sqlx.Stmt `query:"update-user"`
|
||||||
SetUserPassword *sqlx.Stmt `query:"set-user-password"`
|
SetUserPassword *sqlx.Stmt `query:"set-user-password"`
|
||||||
@@ -160,6 +161,15 @@ func (u *Manager) GetEmail(id int, uuid string) (string, error) {
|
|||||||
return email, nil
|
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 {
|
func (u *Manager) verifyPassword(pwd []byte, pwdHash string) error {
|
||||||
err := bcrypt.CompareHashAndPassword([]byte(pwdHash), pwd)
|
err := bcrypt.CompareHashAndPassword([]byte(pwdHash), pwd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -162,7 +162,7 @@ func (c *Hub) BroadcastConversationAssignment(userID int, conversationUUID strin
|
|||||||
c.marshalAndPush(message, []int{userID})
|
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]
|
userIDs, ok := c.ConversationSubs[conversationUUID]
|
||||||
if !ok || len(userIDs) == 0 {
|
if !ok || len(userIDs) == 0 {
|
||||||
return
|
return
|
||||||
@@ -173,7 +173,7 @@ func (c *Hub) BroadcastConversationPropertyUpdate(conversationUUID, prop string,
|
|||||||
Data: map[string]interface{}{
|
Data: map[string]interface{}{
|
||||||
"uuid": conversationUUID,
|
"uuid": conversationUUID,
|
||||||
"prop": prop,
|
"prop": prop,
|
||||||
"val": val,
|
"val": value,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user