refactor.

This commit is contained in:
Abhinav Raut
2024-07-24 03:00:54 +05:30
parent 5e00558e9b
commit 8b763fb167
37 changed files with 457 additions and 285 deletions

View File

@@ -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)
} }

View File

@@ -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))

View File

@@ -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)
} }

View File

@@ -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))

View File

@@ -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{}{}

View File

@@ -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)
} }

View File

@@ -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
View 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)
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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: {

View File

@@ -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(.*)*',

View File

@@ -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
View File

@@ -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
View File

@@ -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=

View File

@@ -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"
}

View File

@@ -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": "यहाँ कुछ भी नहीं है"
}

View File

@@ -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
}

View File

@@ -1,8 +0,0 @@
package models
type AuthUser struct {
ID int
FirstName string
LastName string
Email string
}

View File

@@ -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
} }

View File

@@ -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 {

View File

@@ -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

View File

@@ -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
} }

View File

@@ -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")
} }

View File

@@ -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 {

View File

@@ -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.

View File

@@ -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

View File

@@ -0,0 +1,6 @@
package models
type Settings struct {
AppSiteName string `json:"app.site_name"`
AppLang string `json:"app.lang"`
}

View File

@@ -0,0 +1,2 @@
-- name: get-all
SELECT JSON_OBJECT_AGG(key, value) AS settings FROM (SELECT * FROM settings ORDER BY key) t;

View 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
}

View File

@@ -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"`
} }

View File

@@ -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

View File

@@ -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 {

View File

@@ -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,
}, },
} }