mirror of
https://github.com/abhinavxd/libredesk.git
synced 2026-09-10 14:15:42 +00:00
1268 lines
38 KiB
Go
1268 lines
38 KiB
Go
package main
|
|
|
|
import (
|
|
"cmp"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"log"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"html/template"
|
|
|
|
activitylog "github.com/abhinavxd/libredesk/internal/activity_log"
|
|
"github.com/abhinavxd/libredesk/internal/ai"
|
|
"github.com/abhinavxd/libredesk/internal/aiagent"
|
|
auth_ "github.com/abhinavxd/libredesk/internal/auth"
|
|
"github.com/abhinavxd/libredesk/internal/authz"
|
|
"github.com/abhinavxd/libredesk/internal/autoassigner"
|
|
"github.com/abhinavxd/libredesk/internal/automation"
|
|
businesshours "github.com/abhinavxd/libredesk/internal/business_hours"
|
|
"github.com/abhinavxd/libredesk/internal/colorlog"
|
|
contextlink "github.com/abhinavxd/libredesk/internal/context_link"
|
|
"github.com/abhinavxd/libredesk/internal/conversation"
|
|
"github.com/abhinavxd/libredesk/internal/conversation/priority"
|
|
"github.com/abhinavxd/libredesk/internal/conversation/status"
|
|
"github.com/abhinavxd/libredesk/internal/csat"
|
|
customAttribute "github.com/abhinavxd/libredesk/internal/custom_attribute"
|
|
"github.com/abhinavxd/libredesk/internal/helpcenter"
|
|
"github.com/abhinavxd/libredesk/internal/importer"
|
|
"github.com/abhinavxd/libredesk/internal/inbox"
|
|
"github.com/abhinavxd/libredesk/internal/inbox/channel/email"
|
|
"github.com/abhinavxd/libredesk/internal/inbox/channel/livechat"
|
|
imodels "github.com/abhinavxd/libredesk/internal/inbox/models"
|
|
"github.com/abhinavxd/libredesk/internal/macro"
|
|
"github.com/abhinavxd/libredesk/internal/media"
|
|
fs "github.com/abhinavxd/libredesk/internal/media/stores/localfs"
|
|
"github.com/abhinavxd/libredesk/internal/media/stores/s3"
|
|
notifier "github.com/abhinavxd/libredesk/internal/notification"
|
|
emailnotifier "github.com/abhinavxd/libredesk/internal/notification/providers/email"
|
|
"github.com/abhinavxd/libredesk/internal/oidc"
|
|
"github.com/abhinavxd/libredesk/internal/ratelimit"
|
|
"github.com/abhinavxd/libredesk/internal/report"
|
|
"github.com/abhinavxd/libredesk/internal/role"
|
|
"github.com/abhinavxd/libredesk/internal/search"
|
|
"github.com/abhinavxd/libredesk/internal/setting"
|
|
"github.com/abhinavxd/libredesk/internal/sla"
|
|
"github.com/abhinavxd/libredesk/internal/ssrf"
|
|
"github.com/abhinavxd/libredesk/internal/tag"
|
|
"github.com/abhinavxd/libredesk/internal/team"
|
|
tmpl "github.com/abhinavxd/libredesk/internal/template"
|
|
"github.com/abhinavxd/libredesk/internal/user"
|
|
"github.com/abhinavxd/libredesk/internal/view"
|
|
"github.com/abhinavxd/libredesk/internal/webhook"
|
|
"github.com/abhinavxd/libredesk/internal/ws"
|
|
"github.com/jmoiron/sqlx"
|
|
"github.com/knadh/go-i18n"
|
|
kjson "github.com/knadh/koanf/parsers/json"
|
|
"github.com/knadh/koanf/parsers/toml"
|
|
"github.com/knadh/koanf/providers/confmap"
|
|
"github.com/knadh/koanf/providers/env/v2"
|
|
"github.com/knadh/koanf/providers/file"
|
|
"github.com/knadh/koanf/providers/posflag"
|
|
"github.com/knadh/koanf/providers/rawbytes"
|
|
"github.com/knadh/koanf/v2"
|
|
"github.com/knadh/stuffbin"
|
|
_ "github.com/lib/pq"
|
|
"github.com/redis/go-redis/v9"
|
|
flag "github.com/spf13/pflag"
|
|
"github.com/volatiletech/null/v9"
|
|
goredis "github.com/zerodha/fastcache/stores/goredis/v9"
|
|
"github.com/zerodha/fastcache/v4"
|
|
"github.com/zerodha/logf"
|
|
)
|
|
|
|
// constants holds the app constants.
|
|
type constants struct {
|
|
AppBaseURL string
|
|
FaviconURL string
|
|
LogoURL string
|
|
SiteName string
|
|
UploadProvider string
|
|
AllowedUploadFileExtensions []string
|
|
MaxFileUploadSizeMB int
|
|
}
|
|
|
|
// Config loads config from files and environment variables into koanf.
|
|
func initConfig(ko *koanf.Koanf) {
|
|
for _, f := range ko.Strings("config") {
|
|
log.Println("reading config file:", f)
|
|
if err := ko.Load(file.Provider(f), toml.Parser()); err != nil {
|
|
if os.IsNotExist(err) {
|
|
log.Printf("WARNING: Config file not found. Continuing with defaults and environment variables.")
|
|
continue
|
|
}
|
|
log.Fatalf("error loading config from file: %v.", err)
|
|
}
|
|
}
|
|
// Load environment variables with `LIBREDESK_` prefix.
|
|
ko.Load(env.Provider(".", env.Opt{
|
|
Prefix: "LIBREDESK_",
|
|
TransformFunc: func(key, val string) (string, any) {
|
|
// Transform the key.
|
|
key = strings.ReplaceAll(strings.ToLower(strings.TrimPrefix(key, "LIBREDESK_")), "__", ".")
|
|
return key, val
|
|
},
|
|
}), nil)
|
|
}
|
|
|
|
// validateConfig logs warnings/fatals for invalid config values.
|
|
func validateConfig(ko *koanf.Koanf) {
|
|
encKey := ko.MustString("app.encryption_key")
|
|
|
|
if len(encKey) != 32 {
|
|
log.Fatalf("encryption_key must be exactly 32 characters, got %d", len(encKey))
|
|
}
|
|
|
|
// Warn if using sample config value.
|
|
if encKey == sampleEncKey {
|
|
colorlog.Red("WARNING: You are using the sample encryption_key from config.sample.toml. Change it immediately. Generate a secure key with `openssl rand -hex 16`")
|
|
}
|
|
}
|
|
|
|
// initFlags initializes the commandline flags.
|
|
func initFlags() {
|
|
f := flag.NewFlagSet("config", flag.ContinueOnError)
|
|
|
|
// Registering `--help` handler.
|
|
f.Usage = func() {
|
|
fmt.Fprintf(os.Stderr, "Usage of %s:\n", os.Args[0])
|
|
f.PrintDefaults()
|
|
}
|
|
|
|
// Register the commandline flags and parse them.
|
|
f.StringSlice("config", []string{"config.toml"},
|
|
"path to one or more config files (will be merged in order)")
|
|
f.Bool("version", false, "show current version of the build")
|
|
f.Bool("install", false, "setup database")
|
|
f.Bool("idempotent-install", false, "run idempotent installation, i.e., skip installion if schema is already installed useful for the first time setup")
|
|
f.Bool("yes", false, "skip confirmation prompt")
|
|
f.Bool("upgrade", false, "upgrade the database schema")
|
|
f.Bool("set-system-user-password", false, "set password for the system user")
|
|
f.String("static-dir", "", "path to a directory with custom static files and templates to override the defaults")
|
|
|
|
if err := f.Parse(os.Args[1:]); err != nil {
|
|
log.Fatalf("loading flags: %v", err)
|
|
}
|
|
|
|
if err := ko.Load(posflag.Provider(f, ".", ko), nil); err != nil {
|
|
log.Fatalf("loading config: %v", err)
|
|
}
|
|
}
|
|
|
|
// initConstants initializes the app constants.
|
|
func initConstants() *constants {
|
|
return &constants{
|
|
AppBaseURL: ko.String("app.root_url"),
|
|
FaviconURL: ko.String("app.favicon_url"),
|
|
LogoURL: ko.String("app.logo_url"),
|
|
SiteName: ko.String("app.site_name"),
|
|
UploadProvider: ko.MustString("upload.provider"),
|
|
AllowedUploadFileExtensions: ko.Strings("app.allowed_file_upload_extensions"),
|
|
MaxFileUploadSizeMB: ko.Int("app.max_file_upload_size"),
|
|
}
|
|
}
|
|
|
|
// initFS initializes the stuffbin FileSystem. If staticDir is set, files from
|
|
// that directory are merged into the FS, overriding embedded defaults.
|
|
func initFS(staticDir string) stuffbin.FileSystem {
|
|
// Custom static dir mirrors the structure of the built-in static/ directory.
|
|
staticFiles := []string{
|
|
"./:static",
|
|
}
|
|
|
|
// Get self executable path.
|
|
path, err := os.Executable()
|
|
if err != nil {
|
|
log.Fatalf("error initializing FS: %v", err)
|
|
}
|
|
|
|
// Load embedded files in the executable.
|
|
fs, err := stuffbin.UnStuff(path)
|
|
|
|
if err != nil {
|
|
if err == stuffbin.ErrNoID {
|
|
// Running in local/dev mode, use the local filesystem.
|
|
// Only include frontend dirs if they exist (frontend build is optional in dev).
|
|
colorlog.Red("binary unstuff failed, using local filesystem for static files")
|
|
files := []string{"i18n", "static"}
|
|
for _, d := range []string{"frontend/dist/main", "frontend/dist/widget"} {
|
|
if _, err := os.Stat(d); err == nil {
|
|
files = append(files, d)
|
|
}
|
|
}
|
|
fs, err = stuffbin.NewLocalFS("/", files...)
|
|
if err != nil {
|
|
log.Fatalf("error initializing local FS: %v", err)
|
|
}
|
|
} else {
|
|
log.Fatalf("error initializing FS: %v", err)
|
|
}
|
|
}
|
|
|
|
// Merge custom static files if a custom static dir is provided.
|
|
if staticDir != "" {
|
|
// Only include paths that exist in the custom dir.
|
|
var sf []string
|
|
for _, def := range staticFiles {
|
|
src, _, _ := strings.Cut(def, ":")
|
|
if _, err := os.Stat(filepath.Join(staticDir, src)); err == nil {
|
|
sf = append(sf, def)
|
|
}
|
|
}
|
|
|
|
if len(sf) > 0 {
|
|
files := joinFSPaths(staticDir, sf)
|
|
fLocal, err := stuffbin.NewLocalFS("/", files...)
|
|
if err != nil {
|
|
log.Fatalf("error loading custom static files from '%s': %v", staticDir, err)
|
|
}
|
|
if err := fs.Merge(fLocal); err != nil {
|
|
log.Fatalf("error merging custom static files from '%s': %v", staticDir, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
return fs
|
|
}
|
|
|
|
// joinFSPaths joins a root directory with stuffbin path specs (local:virtual).
|
|
func joinFSPaths(root string, paths []string) []string {
|
|
out := make([]string, 0, len(paths))
|
|
for _, p := range paths {
|
|
f := strings.Split(p, ":")
|
|
out = append(out, filepath.Join(root, f[0])+":"+f[1])
|
|
}
|
|
return out
|
|
}
|
|
|
|
// loadSettings loads settings from the DB into 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]any
|
|
|
|
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)
|
|
}
|
|
}
|
|
|
|
// initSettings inits setting manager.
|
|
func initSettings(db *sqlx.DB) *setting.Manager {
|
|
s, err := setting.New(setting.Opts{
|
|
DB: db,
|
|
Lo: initLogger("settings"),
|
|
EncryptionKey: ko.MustString("app.encryption_key"),
|
|
})
|
|
if err != nil {
|
|
log.Fatalf("error initializing setting manager: %v", err)
|
|
}
|
|
return s
|
|
}
|
|
|
|
// initUser inits user manager.
|
|
func initUser(i18n *i18n.I18n, DB *sqlx.DB) *user.Manager {
|
|
mgr, err := user.New(i18n, user.Opts{
|
|
DB: DB,
|
|
Lo: initLogger("user_manager"),
|
|
})
|
|
if err != nil {
|
|
log.Fatalf("error initializing user manager: %v", err)
|
|
}
|
|
return mgr
|
|
}
|
|
|
|
// initConversations inits conversation manager.
|
|
func initConversations(
|
|
i18n *i18n.I18n,
|
|
sla *sla.Manager,
|
|
status *status.Manager,
|
|
priority *priority.Manager,
|
|
hub *ws.Hub,
|
|
db *sqlx.DB,
|
|
inboxStore *inbox.Manager,
|
|
userStore *user.Manager,
|
|
teamStore *team.Manager,
|
|
mediaStore *media.Manager,
|
|
settings *setting.Manager,
|
|
csat *csat.Manager,
|
|
automationEngine *automation.Engine,
|
|
template *tmpl.Manager,
|
|
webhook *webhook.Manager,
|
|
dispatcher *notifier.Dispatcher,
|
|
) *conversation.Manager {
|
|
continuityConfig := &conversation.ContinuityConfig{}
|
|
if ko.Exists("conversation.continuity_scan_interval") {
|
|
continuityConfig.BatchCheckInterval = ko.MustDuration("conversation.continuity_scan_interval")
|
|
}
|
|
|
|
c, err := conversation.New(hub, i18n, sla, status, priority, inboxStore, userStore, teamStore, mediaStore, settings, csat, automationEngine, template, webhook, dispatcher, conversation.Opts{
|
|
DB: db,
|
|
Lo: initLogger("conversation_manager"),
|
|
OutgoingMessageQueueSize: ko.MustInt("message.outgoing_queue_size"),
|
|
IncomingMessageQueueSize: ko.MustInt("message.incoming_queue_size"),
|
|
ContinuityConfig: continuityConfig,
|
|
SubjectRefFormat: ko.String("conversation.subject_ref_format"),
|
|
})
|
|
if err != nil {
|
|
log.Fatalf("error initializing conversation manager: %v", err)
|
|
}
|
|
return c
|
|
}
|
|
|
|
// initTag inits tag manager.
|
|
func initTag(db *sqlx.DB, i18n *i18n.I18n) *tag.Manager {
|
|
var lo = initLogger("tag_manager")
|
|
mgr, err := tag.New(tag.Opts{
|
|
DB: db,
|
|
Lo: lo,
|
|
I18n: i18n,
|
|
})
|
|
if err != nil {
|
|
log.Fatalf("error initializing tags: %v", err)
|
|
}
|
|
return mgr
|
|
}
|
|
|
|
// initViews inits view manager.
|
|
func initView(db *sqlx.DB, i18n *i18n.I18n) *view.Manager {
|
|
var lo = initLogger("view_manager")
|
|
m, err := view.New(view.Opts{
|
|
DB: db,
|
|
Lo: lo,
|
|
I18n: i18n,
|
|
})
|
|
if err != nil {
|
|
log.Fatalf("error initializing view manager: %v", err)
|
|
}
|
|
return m
|
|
}
|
|
|
|
// initMacro inits macro manager.
|
|
func initMacro(db *sqlx.DB, i18n *i18n.I18n) *macro.Manager {
|
|
var lo = initLogger("macro")
|
|
m, err := macro.New(macro.Opts{
|
|
DB: db,
|
|
Lo: lo,
|
|
I18n: i18n,
|
|
})
|
|
if err != nil {
|
|
log.Fatalf("error initializing macro manager: %v", err)
|
|
}
|
|
return m
|
|
}
|
|
|
|
// initBusinessHours inits business hours manager.
|
|
func initBusinessHours(db *sqlx.DB, i18n *i18n.I18n) *businesshours.Manager {
|
|
var lo = initLogger("business-hours")
|
|
m, err := businesshours.New(businesshours.Opts{
|
|
DB: db,
|
|
Lo: lo,
|
|
I18n: i18n,
|
|
})
|
|
if err != nil {
|
|
log.Fatalf("error initializing business hours manager: %v", err)
|
|
}
|
|
return m
|
|
}
|
|
|
|
// initSLA inits SLA manager.
|
|
func initSLA(db *sqlx.DB, teamManager *team.Manager, settings *setting.Manager, businessHours *businesshours.Manager, template *tmpl.Manager, userManager *user.Manager, i18n *i18n.I18n, dispatcher *notifier.Dispatcher) *sla.Manager {
|
|
var lo = initLogger("sla")
|
|
m, err := sla.New(sla.Opts{
|
|
DB: db,
|
|
Lo: lo,
|
|
I18n: i18n,
|
|
}, teamManager, settings, businessHours, template, userManager, dispatcher)
|
|
if err != nil {
|
|
log.Fatalf("error initializing SLA manager: %v", err)
|
|
}
|
|
return m
|
|
}
|
|
|
|
// initCSAT inits CSAT manager.
|
|
func initCSAT(db *sqlx.DB, i18n *i18n.I18n) *csat.Manager {
|
|
var lo = initLogger("csat")
|
|
m, err := csat.New(csat.Opts{
|
|
DB: db,
|
|
Lo: lo,
|
|
I18n: i18n,
|
|
})
|
|
if err != nil {
|
|
log.Fatalf("error initializing CSAT manager: %v", err)
|
|
}
|
|
return m
|
|
}
|
|
|
|
// initWS inits websocket hub.
|
|
func initWS(user *user.Manager) *ws.Hub {
|
|
return ws.NewHub(initLogger("ws"), user)
|
|
}
|
|
|
|
// getCustomStaticDir returns the custom static directory path from CLI flag or config.
|
|
func getCustomStaticDir() string {
|
|
dir := ko.String("static-dir")
|
|
if dir == "" {
|
|
dir = ko.String("app.static_dir")
|
|
}
|
|
if dir == "" {
|
|
return ""
|
|
}
|
|
abs, err := filepath.Abs(dir)
|
|
if err != nil {
|
|
return dir
|
|
}
|
|
return abs
|
|
}
|
|
|
|
// initTemplates inits template manager.
|
|
func initTemplate(db *sqlx.DB, fs stuffbin.FileSystem, consts *constants, i18n *i18n.I18n) *tmpl.Manager {
|
|
var (
|
|
lo = initLogger("template")
|
|
funcMap = getTmplFuncs(consts, i18n, fs)
|
|
)
|
|
tpls, err := stuffbin.ParseTemplatesGlob(funcMap, fs, "/static/email-templates/*.html")
|
|
if err != nil {
|
|
log.Fatalf("error parsing e-mail templates: %v", err)
|
|
}
|
|
webTpls, err := parseWebTemplates(funcMap, fs)
|
|
if err != nil {
|
|
log.Fatalf("error parsing web templates: %v", err)
|
|
}
|
|
|
|
m, err := tmpl.New(lo, db, webTpls, tpls, funcMap, i18n)
|
|
if err != nil {
|
|
log.Fatalf("error initializing template manager: %v", err)
|
|
}
|
|
return m
|
|
}
|
|
|
|
// getTmplFuncs returns the template functions.
|
|
func getTmplFuncs(consts *constants, i18n *i18n.I18n, fs stuffbin.FileSystem) template.FuncMap {
|
|
lucideIcons := loadLucideIcons(fs)
|
|
return template.FuncMap{
|
|
"LucideIcon": func(name string) template.HTML {
|
|
return lucideIcons[name]
|
|
},
|
|
"RootURL": func() string {
|
|
return consts.AppBaseURL
|
|
},
|
|
"FaviconURL": func() string {
|
|
return consts.FaviconURL
|
|
},
|
|
"AssetVer": func() string {
|
|
return assetVersion
|
|
},
|
|
"Date": func(layout string) string {
|
|
if layout == "" {
|
|
layout = time.ANSIC
|
|
}
|
|
return time.Now().Format(layout)
|
|
},
|
|
"LogoURL": func() string {
|
|
return consts.LogoURL
|
|
},
|
|
"SiteName": func() string {
|
|
return consts.SiteName
|
|
},
|
|
"L": func() any {
|
|
return i18n
|
|
},
|
|
"map": func(pairs ...any) (map[string]any, error) {
|
|
if len(pairs)%2 != 0 {
|
|
return nil, fmt.Errorf("map: odd number of arguments")
|
|
}
|
|
out := make(map[string]any, len(pairs)/2)
|
|
for i := 0; i < len(pairs); i += 2 {
|
|
key, ok := pairs[i].(string)
|
|
if !ok {
|
|
return nil, fmt.Errorf("map: key %v is not a string", pairs[i])
|
|
}
|
|
out[key] = pairs[i+1]
|
|
}
|
|
return out, nil
|
|
},
|
|
}
|
|
}
|
|
|
|
// reloadSettings reloads the settings from the database into the Koanf instance.
|
|
func reloadSettings(app *App) error {
|
|
app.lo.Info("reloading settings")
|
|
j, err := app.setting.GetAllJSON()
|
|
if err != nil {
|
|
app.lo.Error("error parsing settings from DB", "error", err)
|
|
return err
|
|
}
|
|
var out map[string]any
|
|
if err := json.Unmarshal(j, &out); err != nil {
|
|
app.lo.Error("error unmarshalling settings from DB", "error", err)
|
|
return err
|
|
}
|
|
app.Lock()
|
|
err = ko.Load(confmap.Provider(out, "."), nil)
|
|
app.Unlock()
|
|
if err != nil {
|
|
app.lo.Error("error loading settings into koanf", "error", err)
|
|
return err
|
|
}
|
|
newConsts := initConstants()
|
|
app.consts.Store(newConsts)
|
|
return nil
|
|
}
|
|
|
|
// reloadTemplates reloads the templates from the filesystem.
|
|
func reloadTemplates(app *App) error {
|
|
app.lo.Info("reloading templates")
|
|
funcMap := getTmplFuncs(app.consts.Load().(*constants), app.i18n, app.fs)
|
|
tpls, err := stuffbin.ParseTemplatesGlob(funcMap, app.fs, "/static/email-templates/*.html")
|
|
if err != nil {
|
|
app.lo.Error("error parsing email templates", "error", err)
|
|
return err
|
|
}
|
|
webTpls, err := parseWebTemplates(funcMap, app.fs)
|
|
if err != nil {
|
|
app.lo.Error("error parsing web templates", "error", err)
|
|
return err
|
|
}
|
|
|
|
return app.tmpl.Reload(webTpls, tpls, funcMap)
|
|
}
|
|
|
|
// parseWebTemplates parses the top-level web templates and the per-template help center pages.
|
|
func parseWebTemplates(funcMap template.FuncMap, fs stuffbin.FileSystem) (*template.Template, error) {
|
|
var paths []string
|
|
for _, pattern := range []string{
|
|
"/static/public/web-templates/*.html",
|
|
"/static/public/web-templates/help/*/*.html",
|
|
} {
|
|
p, err := fs.Glob(pattern)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
paths = append(paths, p...)
|
|
}
|
|
return stuffbin.ParseTemplates(funcMap, fs, paths...)
|
|
}
|
|
|
|
// initTeam inits team manager.
|
|
func initTeam(db *sqlx.DB, i18n *i18n.I18n) *team.Manager {
|
|
var lo = initLogger("team-manager")
|
|
mgr, err := team.New(team.Opts{
|
|
DB: db,
|
|
Lo: lo,
|
|
I18n: i18n,
|
|
})
|
|
if err != nil {
|
|
log.Fatalf("error initializing team manager: %v", err)
|
|
}
|
|
return mgr
|
|
}
|
|
|
|
// initMedia inits media manager.
|
|
func initMedia(db *sqlx.DB, i18n *i18n.I18n, settings *setting.Manager) *media.Manager {
|
|
var (
|
|
store media.Store
|
|
err error
|
|
lo = initLogger("media")
|
|
)
|
|
rootURL := func() string {
|
|
u, err := settings.GetAppRootURL()
|
|
if err != nil {
|
|
// Fallback to config if settings fetch fails
|
|
return ko.String("app.root_url")
|
|
}
|
|
return u
|
|
}
|
|
switch s := ko.MustString("upload.provider"); s {
|
|
case "s3":
|
|
store, err = s3.New(s3.Opt{
|
|
URL: ko.String("upload.s3.url"),
|
|
PublicURL: ko.String("upload.s3.public_url"),
|
|
AccessKey: ko.String("upload.s3.access_key"),
|
|
SecretKey: ko.String("upload.s3.secret_key"),
|
|
Region: ko.String("upload.s3.region"),
|
|
Bucket: ko.String("upload.s3.bucket"),
|
|
BucketPath: ko.String("upload.s3.bucket_path"),
|
|
// All files are private by default.
|
|
BucketType: "private",
|
|
Expiry: ko.Duration("upload.s3.expiry"),
|
|
})
|
|
if err != nil {
|
|
log.Fatalf("error initializing s3 media store: %v", err)
|
|
}
|
|
case "fs":
|
|
// Default expiry to 1h if not set.
|
|
fsExpiry := ko.Duration("upload.fs.expiry")
|
|
if fsExpiry == 0 {
|
|
fsExpiry = 1 * time.Hour
|
|
}
|
|
store, err = fs.New(fs.Opts{
|
|
UploadURI: media.PublicURI,
|
|
UploadPath: filepath.Clean(ko.String("upload.fs.upload_path")),
|
|
RootURL: rootURL,
|
|
SigningKey: ko.MustString("app.encryption_key"),
|
|
Expiry: fsExpiry,
|
|
})
|
|
if err != nil {
|
|
log.Fatalf("error initializing fs media store: %v", err)
|
|
}
|
|
default:
|
|
log.Fatalf("unknown media store: %s", s)
|
|
}
|
|
|
|
media, err := media.New(media.Opts{
|
|
Store: store,
|
|
Lo: lo,
|
|
DB: db,
|
|
I18n: i18n,
|
|
RootURL: rootURL,
|
|
})
|
|
if err != nil {
|
|
log.Fatalf("error initializing media: %v", err)
|
|
}
|
|
return media
|
|
}
|
|
|
|
// initInbox initializes the inbox manager without registering inboxes.
|
|
func initInbox(db *sqlx.DB, i18n *i18n.I18n) *inbox.Manager {
|
|
var lo = initLogger("inbox-manager")
|
|
mgr, err := inbox.New(lo, db, i18n, ko.MustString("app.encryption_key"))
|
|
if err != nil {
|
|
log.Fatalf("error initializing inbox manager: %v", err)
|
|
}
|
|
return mgr
|
|
}
|
|
|
|
// initAutomationEngine initializes the automation engine.
|
|
func initAutomationEngine(db *sqlx.DB, i18n *i18n.I18n) *automation.Engine {
|
|
var lo = initLogger("automation_engine")
|
|
engine, err := automation.New(automation.Opts{
|
|
DB: db,
|
|
Lo: lo,
|
|
I18n: i18n,
|
|
})
|
|
if err != nil {
|
|
log.Fatalf("error initializing automation engine: %v", err)
|
|
}
|
|
return engine
|
|
}
|
|
|
|
// initAutoAssigner initializes the auto assigner.
|
|
func initAutoAssigner(teamManager *team.Manager, userManager *user.Manager, conversationManager *conversation.Manager) *autoassigner.Engine {
|
|
systemUser, err := userManager.GetSystemUser()
|
|
if err != nil {
|
|
log.Fatalf("error fetching system user: %v", err)
|
|
}
|
|
e, err := autoassigner.New(teamManager, conversationManager, systemUser, initLogger("autoassigner"))
|
|
if err != nil {
|
|
log.Fatalf("error initializing auto assigner: %v", err)
|
|
}
|
|
return e
|
|
}
|
|
|
|
// initNotifier initializes the notifier service with available providers.
|
|
func initNotifier() *notifier.Service {
|
|
smtpCfg := imodels.SMTPConfig{}
|
|
if err := ko.UnmarshalWithConf("notification.email", &smtpCfg, koanf.UnmarshalConf{Tag: "json"}); err != nil {
|
|
log.Fatalf("error unmarshalling email notification provider config: %v", err)
|
|
}
|
|
|
|
emailNotifier, err := emailnotifier.New([]imodels.SMTPConfig{smtpCfg}, emailnotifier.Opts{
|
|
Lo: initLogger("email-notifier"),
|
|
FromEmail: ko.String("notification.email.email_address"),
|
|
})
|
|
if err != nil {
|
|
log.Fatalf("error initializing email notifier: %v", err)
|
|
}
|
|
|
|
notifierProviders := map[string]notifier.Notifier{
|
|
emailNotifier.Name(): emailNotifier,
|
|
}
|
|
|
|
return notifier.NewService(notifierProviders, ko.MustInt("notification.concurrency"), ko.MustInt("notification.queue_size"), initLogger("notifier"))
|
|
}
|
|
|
|
// initEmailInbox loads inbox config from DB and initializes the email inbox.
|
|
func initEmailInbox(inboxRecord imodels.Inbox, msgStore inbox.MessageStore, usrStore inbox.UserStore, mgr *inbox.Manager) (inbox.Inbox, error) {
|
|
var config imodels.Config
|
|
|
|
// Load JSON data into Koanf.
|
|
if err := ko.Load(rawbytes.Provider([]byte(inboxRecord.Config)), kjson.Parser()); err != nil {
|
|
return nil, fmt.Errorf("loading config: %w", err)
|
|
}
|
|
|
|
if err := ko.UnmarshalWithConf("", &config, koanf.UnmarshalConf{Tag: "json"}); err != nil {
|
|
return nil, fmt.Errorf("unmarshalling `%s` %s config: %w", inboxRecord.Channel, inboxRecord.Name, err)
|
|
}
|
|
|
|
if len(config.SMTP) == 0 {
|
|
log.Printf("WARNING: Zero SMTP servers configured for `%s` inbox: Name: `%s`", inboxRecord.Channel, inboxRecord.Name)
|
|
}
|
|
|
|
if len(config.IMAP) == 0 {
|
|
log.Printf("WARNING: Zero IMAP clients configured for `%s` inbox: Name: `%s`", inboxRecord.Channel, inboxRecord.Name)
|
|
}
|
|
|
|
config.From = inboxRecord.From
|
|
config.FromNameTemplate = inboxRecord.FromNameTemplate
|
|
|
|
if len(config.From) == 0 {
|
|
log.Printf("WARNING: No `from` email address set for `%s` inbox: Name: `%s`", inboxRecord.Channel, inboxRecord.Name)
|
|
}
|
|
|
|
// Callback to persist refreshed tokens in DB.
|
|
tokenRefreshCallback := func(inboxID int, updatedConfig imodels.Config) error {
|
|
// Marshal updated config to JSON
|
|
updatedConfigJSON, err := json.Marshal(updatedConfig)
|
|
if err != nil {
|
|
log.Printf("ERROR: Failed to marshal updated config during token refresh for inbox %d: %v", inboxID, err)
|
|
return err
|
|
}
|
|
|
|
// Persist updated config to DB
|
|
if err := mgr.UpdateConfig(inboxID, updatedConfigJSON); err != nil {
|
|
log.Printf("ERROR: Failed to persist refreshed tokens during operation for inbox %d: %v", inboxID, err)
|
|
return err
|
|
}
|
|
|
|
log.Printf("INFO: Successfully persisted refreshed tokens during operation for inbox: %d", inboxID)
|
|
return nil
|
|
}
|
|
|
|
inbox, err := email.New(msgStore, usrStore, email.Opts{
|
|
ID: inboxRecord.ID,
|
|
Name: inboxRecord.Name,
|
|
Config: config,
|
|
Lo: initLogger("email_inbox"),
|
|
TokenRefreshCallback: tokenRefreshCallback,
|
|
})
|
|
|
|
if err != nil {
|
|
return nil, fmt.Errorf("initializing `%s` inbox: `%s` error : %w", inboxRecord.Channel, inboxRecord.Name, err)
|
|
}
|
|
|
|
log.Printf("`%s` inbox successfully initialized", inboxRecord.Name)
|
|
|
|
return inbox, nil
|
|
}
|
|
|
|
// initLiveChatInbox initializes the live chat inbox.
|
|
func initLiveChatInbox(inboxRecord imodels.Inbox, msgStore inbox.MessageStore, usrStore inbox.UserStore, signAvatarURL func(*null.String)) (inbox.Inbox, error) {
|
|
var config livechat.Config
|
|
|
|
// Load JSON data into Koanf.
|
|
if err := ko.Load(rawbytes.Provider([]byte(inboxRecord.Config)), kjson.Parser()); err != nil {
|
|
return nil, fmt.Errorf("loading config: %w", err)
|
|
}
|
|
|
|
if err := ko.UnmarshalWithConf("", &config, koanf.UnmarshalConf{Tag: "json"}); err != nil {
|
|
return nil, fmt.Errorf("unmarshalling `%s` %s config: %w", inboxRecord.Channel, inboxRecord.Name, err)
|
|
}
|
|
|
|
inbox, err := livechat.New(msgStore, usrStore, livechat.Opts{
|
|
ID: inboxRecord.ID,
|
|
Name: inboxRecord.Name,
|
|
Config: config,
|
|
Lo: initLogger("livechat_inbox"),
|
|
SignAvatarURL: signAvatarURL,
|
|
})
|
|
|
|
if err != nil {
|
|
return nil, fmt.Errorf("initializing `%s` inbox: `%s` error : %w", inboxRecord.Channel, inboxRecord.Name, err)
|
|
}
|
|
|
|
log.Printf("`%s` inbox successfully initialized", inboxRecord.Name)
|
|
|
|
return inbox, nil
|
|
}
|
|
|
|
// makeInboxInitializer creates an inbox initializer function.
|
|
func makeInboxInitializer(mgr *inbox.Manager, signAvatarURL func(*null.String)) func(imodels.Inbox, inbox.MessageStore, inbox.UserStore) (inbox.Inbox, error) {
|
|
return func(inboxR imodels.Inbox, msgStore inbox.MessageStore, usrStore inbox.UserStore) (inbox.Inbox, error) {
|
|
switch inboxR.Channel {
|
|
case inbox.ChannelEmail:
|
|
return initEmailInbox(inboxR, msgStore, usrStore, mgr)
|
|
case inbox.ChannelLiveChat:
|
|
return initLiveChatInbox(inboxR, msgStore, usrStore, signAvatarURL)
|
|
default:
|
|
return nil, fmt.Errorf("unknown inbox channel: %s", inboxR.Channel)
|
|
}
|
|
}
|
|
}
|
|
|
|
// reloadInbox reloads a single inbox by ID using the signal-aware context.
|
|
func reloadInbox(app *App, id int) error {
|
|
app.lo.Info("reloading inbox", "id", id)
|
|
return app.inbox.ReloadInbox(app.ctx, id, makeInboxInitializer(app.inbox, app.conversation.SignAvatarURL))
|
|
}
|
|
|
|
// startInboxes registers the active inboxes and starts receiver for each.
|
|
func startInboxes(ctx context.Context, mgr *inbox.Manager, msgStore inbox.MessageStore, usrStore inbox.UserStore, signAvatarURL func(*null.String)) {
|
|
mgr.SetMessageStore(msgStore)
|
|
mgr.SetUserStore(usrStore)
|
|
|
|
if err := mgr.InitInboxes(makeInboxInitializer(mgr, signAvatarURL)); err != nil {
|
|
log.Fatalf("error initializing inboxes: %v", err)
|
|
}
|
|
|
|
if err := mgr.Start(ctx); err != nil {
|
|
log.Fatalf("error starting inboxes: %v", err)
|
|
}
|
|
}
|
|
|
|
// initAuthz initializes authorization enforcer.
|
|
func initAuthz(i18n *i18n.I18n) *authz.Enforcer {
|
|
enforcer, err := authz.NewEnforcer(initLogger("authz"), i18n)
|
|
if err != nil {
|
|
log.Fatalf("error initializing authz: %v", err)
|
|
}
|
|
return enforcer
|
|
}
|
|
|
|
// initSSRFControl builds the shared outbound-request guard from config.
|
|
func initSSRFControl() ssrf.Control {
|
|
lo := initLogger("ssrf")
|
|
return ssrf.NewControl(ko.Bool("ssrf.enabled"), ko.Strings("ssrf.allowed_cidrs"), lo)
|
|
}
|
|
|
|
// initAuth initializes the authentication manager.
|
|
func initAuth(o *oidc.Manager, rd *redis.Client, i18n *i18n.I18n, dialControl ssrf.Control) *auth_.Auth {
|
|
lo := initLogger("auth")
|
|
|
|
providers, err := buildProviders(o)
|
|
if err != nil {
|
|
log.Fatalf("error initializing auth: %v", err)
|
|
}
|
|
|
|
secure := !ko.Bool("app.server.disable_secure_cookies")
|
|
sessionLifetime := ko.Duration("app.server.session_lifetime")
|
|
auth, err := auth_.New(auth_.Config{Providers: providers, SecureCookies: secure, SessionLifetime: sessionLifetime}, i18n, rd, lo, dialControl)
|
|
if err != nil {
|
|
log.Fatalf("error initializing auth: %v", err)
|
|
}
|
|
|
|
return auth
|
|
}
|
|
|
|
// reloadAuth reloads the auth providers.
|
|
func reloadAuth(app *App) error {
|
|
app.lo.Info("reloading auth manager")
|
|
providers, err := buildProviders(app.oidc)
|
|
if err != nil {
|
|
app.lo.Error("error reloading auth", "error", err)
|
|
return err
|
|
}
|
|
if err := app.auth.Reload(auth_.Config{Providers: providers}); err != nil {
|
|
app.lo.Error("error reloading auth", "error", err)
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// buildProviders creates a list of auth providers from the OIDC manager.
|
|
func buildProviders(o *oidc.Manager) ([]auth_.Provider, error) {
|
|
oidcConfigs, err := o.GetAll()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
providers := make([]auth_.Provider, 0, len(oidcConfigs))
|
|
for _, config := range oidcConfigs {
|
|
if !config.Enabled {
|
|
continue
|
|
}
|
|
providers = append(providers, auth_.Provider{
|
|
ID: config.ID,
|
|
Provider: config.Provider,
|
|
ProviderURL: config.ProviderURL,
|
|
RedirectURL: func() (string, error) { return o.RedirectURL(config.ID) },
|
|
ClientID: config.ClientID,
|
|
ClientSecret: config.ClientSecret,
|
|
})
|
|
}
|
|
return providers, nil
|
|
}
|
|
|
|
// initOIDC initializes open id connect config manager.
|
|
func initOIDC(db *sqlx.DB, settings *setting.Manager, i18n *i18n.I18n) *oidc.Manager {
|
|
lo := initLogger("oidc")
|
|
o, err := oidc.New(oidc.Opts{
|
|
DB: db,
|
|
Lo: lo,
|
|
I18n: i18n,
|
|
EncryptionKey: ko.MustString("app.encryption_key"),
|
|
}, settings)
|
|
if err != nil {
|
|
log.Fatalf("error initializing oidc: %v", err)
|
|
}
|
|
return o
|
|
}
|
|
|
|
// initI18n inits i18n.
|
|
func initI18n(fs stuffbin.FileSystem) *i18n.I18n {
|
|
lang := cmp.Or(ko.String("app.lang"), defLang)
|
|
log.Printf("loading i18n language file: %s", lang)
|
|
if _, err := fs.Read("/i18n/" + lang + ".json"); err != nil {
|
|
log.Fatalf("error reading i18n language file `%s` : %v", lang, err)
|
|
}
|
|
i18n, err := loadI18nLang(lang, fs)
|
|
if err != nil {
|
|
log.Fatalf("error initializing i18n: %v", err)
|
|
}
|
|
return i18n
|
|
}
|
|
|
|
// initRedis inits redis DB.
|
|
func initRedis() *redis.Client {
|
|
// Load options from redis URL if set.
|
|
redisURL := ko.String("redis.url")
|
|
if redisURL != "" {
|
|
options, err := redis.ParseURL(redisURL)
|
|
if err != nil {
|
|
log.Fatalf("error parsing redis url: %v", err)
|
|
}
|
|
return redis.NewClient(options)
|
|
}
|
|
// Load from individual config options.
|
|
return redis.NewClient(&redis.Options{
|
|
Addr: ko.MustString("redis.address"),
|
|
Username: ko.String("redis.user"),
|
|
Password: ko.String("redis.password"),
|
|
DB: ko.Int("redis.db"),
|
|
})
|
|
}
|
|
|
|
func initFastCache(rdb *redis.Client) *fastcache.FastCache {
|
|
return fastcache.New(goredis.New(goredis.Config{Prefix: fastCachePrefix}, rdb))
|
|
}
|
|
|
|
// initRedis inits postgres DB.
|
|
func initDB() *sqlx.DB {
|
|
dsn := fmt.Sprintf(
|
|
"host=%s port=%d user=%s password=%s dbname=%s sslmode=%s %s",
|
|
ko.MustString("db.host"),
|
|
ko.MustInt("db.port"),
|
|
ko.MustString("db.user"),
|
|
ko.MustString("db.password"),
|
|
ko.MustString("db.database"),
|
|
ko.String("db.ssl_mode"),
|
|
ko.String("db.params"),
|
|
)
|
|
|
|
db, err := sqlx.Connect("postgres", dsn)
|
|
if err != nil {
|
|
log.Fatalf("error connecting to DB: %v", err)
|
|
}
|
|
|
|
db.SetMaxOpenConns(ko.MustInt("db.max_open"))
|
|
db.SetMaxIdleConns(ko.MustInt("db.max_idle"))
|
|
db.SetConnMaxLifetime(ko.MustDuration("db.max_lifetime"))
|
|
|
|
return db
|
|
}
|
|
|
|
// initRedis inits role manager.
|
|
func initRole(db *sqlx.DB, i18n *i18n.I18n) *role.Manager {
|
|
var lo = initLogger("role_manager")
|
|
r, err := role.New(role.Opts{
|
|
DB: db,
|
|
Lo: lo,
|
|
I18n: i18n,
|
|
})
|
|
if err != nil {
|
|
log.Fatalf("error initializing role manager: %v", err)
|
|
}
|
|
return r
|
|
}
|
|
|
|
// initStatus inits conversation status manager.
|
|
func initStatus(db *sqlx.DB, i18n *i18n.I18n) *status.Manager {
|
|
manager, err := status.New(status.Opts{
|
|
DB: db,
|
|
Lo: initLogger("status-manager"),
|
|
I18n: i18n,
|
|
})
|
|
if err != nil {
|
|
log.Fatalf("error initializing status manager: %v", err)
|
|
}
|
|
return manager
|
|
}
|
|
|
|
// initPriority inits conversation priority manager.
|
|
func initPriority(db *sqlx.DB, i18n *i18n.I18n) *priority.Manager {
|
|
manager, err := priority.New(priority.Opts{
|
|
DB: db,
|
|
Lo: initLogger("priority-manager"),
|
|
I18n: i18n,
|
|
})
|
|
if err != nil {
|
|
log.Fatalf("error initializing priority manager: %v", err)
|
|
}
|
|
return manager
|
|
}
|
|
|
|
// initAI inits AI manager.
|
|
func initAI(ctx context.Context, db *sqlx.DB, i18n *i18n.I18n, dialControl ssrf.Control) *ai.Manager {
|
|
lo := initLogger("ai")
|
|
m, err := ai.New(ai.Opts{
|
|
Ctx: ctx,
|
|
DB: db,
|
|
Lo: lo,
|
|
I18n: i18n,
|
|
EncryptionKey: ko.MustString("app.encryption_key"),
|
|
DialControl: dialControl,
|
|
})
|
|
if err != nil {
|
|
log.Fatalf("error initializing AI manager: %v", err)
|
|
}
|
|
return m
|
|
}
|
|
|
|
// initHelpCenter inits the help center manager.
|
|
func initHelpCenter(db *sqlx.DB, i18n *i18n.I18n, indexer helpcenter.ArticleIndexer) *helpcenter.Manager {
|
|
m, err := helpcenter.New(helpcenter.Opts{
|
|
DB: db,
|
|
Lo: initLogger("helpcenter"),
|
|
I18n: i18n,
|
|
Indexer: indexer,
|
|
})
|
|
if err != nil {
|
|
log.Fatalf("error initializing help center manager: %v", err)
|
|
}
|
|
return m
|
|
}
|
|
|
|
// initAIAgent inits the autonomous AI agent manager.
|
|
func initAIAgent(db *sqlx.DB, i18n *i18n.I18n, aiManager *ai.Manager, convo *conversation.Manager, mediaManager *media.Manager, settingManager *setting.Manager, userManager *user.Manager, notifierService *notifier.Service, rdb *redis.Client) *aiagent.Manager {
|
|
m, err := aiagent.New(aiagent.Opts{
|
|
DB: db,
|
|
Lo: initLogger("ai_agent"),
|
|
I18n: i18n,
|
|
QueueSize: cmp.Or(ko.Int("ai_agent.queue_size"), 1000),
|
|
MaxSteps: min(max(cmp.Or(ko.Int("ai_agent.max_steps"), 6), 1), 20),
|
|
MaxHistoryMessages: min(max(cmp.Or(ko.Int("ai_agent.max_history_messages"), 30), 5), 100),
|
|
}, aiManager, convo, mediaManager, settingManager, userManager, notifierService, rdb)
|
|
if err != nil {
|
|
log.Fatalf("error initializing AI agent manager: %v", err)
|
|
}
|
|
return m
|
|
}
|
|
|
|
// initSearch inits search manager.
|
|
func initSearch(db *sqlx.DB, i18n *i18n.I18n) *search.Manager {
|
|
lo := initLogger("search")
|
|
m, err := search.New(search.Opts{
|
|
DB: db,
|
|
Lo: lo,
|
|
I18n: i18n,
|
|
})
|
|
if err != nil {
|
|
log.Fatalf("error initializing search manager: %v", err)
|
|
}
|
|
return m
|
|
}
|
|
|
|
// initCustomAttribute inits custom attribute manager.
|
|
func initCustomAttribute(db *sqlx.DB, i18n *i18n.I18n) *customAttribute.Manager {
|
|
lo := initLogger("custom-attribute")
|
|
m, err := customAttribute.New(customAttribute.Opts{
|
|
DB: db,
|
|
Lo: lo,
|
|
I18n: i18n,
|
|
})
|
|
if err != nil {
|
|
log.Fatalf("error initializing custom attribute manager: %v", err)
|
|
}
|
|
return m
|
|
}
|
|
|
|
// initActivityLog inits activity log manager.
|
|
func initActivityLog(db *sqlx.DB, i18n *i18n.I18n) *activitylog.Manager {
|
|
lo := initLogger("activity-log")
|
|
m, err := activitylog.New(activitylog.Opts{
|
|
DB: db,
|
|
Lo: lo,
|
|
I18n: i18n,
|
|
})
|
|
if err != nil {
|
|
log.Fatalf("error initializing activity log manager: %v", err)
|
|
}
|
|
return m
|
|
}
|
|
|
|
// initReport inits report manager.
|
|
func initReport(db *sqlx.DB, i18n *i18n.I18n) *report.Manager {
|
|
lo := initLogger("report")
|
|
m, err := report.New(report.Opts{
|
|
DB: db,
|
|
Lo: lo,
|
|
I18n: i18n,
|
|
})
|
|
if err != nil {
|
|
log.Fatalf("error initializing report manager: %v", err)
|
|
}
|
|
return m
|
|
}
|
|
|
|
// initContextLink inits context link manager.
|
|
func initContextLink(db *sqlx.DB, i18n *i18n.I18n) *contextlink.Manager {
|
|
var lo = initLogger("context-link")
|
|
m, err := contextlink.New(contextlink.Opts{
|
|
DB: db,
|
|
Lo: lo,
|
|
I18n: i18n,
|
|
EncryptionKey: ko.MustString("app.encryption_key"),
|
|
})
|
|
if err != nil {
|
|
log.Fatalf("error initializing context link manager: %v", err)
|
|
}
|
|
return m
|
|
}
|
|
|
|
// initWebhook inits webhook manager.
|
|
func initWebhook(db *sqlx.DB, i18n *i18n.I18n, dialControl ssrf.Control) *webhook.Manager {
|
|
var lo = initLogger("webhook")
|
|
m, err := webhook.New(webhook.Opts{
|
|
DB: db,
|
|
Lo: lo,
|
|
I18n: i18n,
|
|
Workers: ko.MustInt("webhook.workers"),
|
|
QueueSize: ko.MustInt("webhook.queue_size"),
|
|
Timeout: ko.MustDuration("webhook.timeout"),
|
|
EncryptionKey: ko.MustString("app.encryption_key"),
|
|
DialControl: dialControl,
|
|
})
|
|
if err != nil {
|
|
log.Fatalf("error initializing webhook manager: %v", err)
|
|
}
|
|
return m
|
|
}
|
|
|
|
// initUserNotification inits user notification manager.
|
|
func initUserNotification(db *sqlx.DB, i18n *i18n.I18n) *notifier.UserNotificationManager {
|
|
var lo = initLogger("user-notification")
|
|
m, err := notifier.NewUserNotificationManager(notifier.UserNotificationOpts{
|
|
DB: db,
|
|
Lo: lo,
|
|
I18n: i18n,
|
|
})
|
|
if err != nil {
|
|
log.Fatalf("error initializing user notification manager: %v", err)
|
|
}
|
|
return m
|
|
}
|
|
|
|
// initImporter inits the importer manager.
|
|
func initImporter(i18n *i18n.I18n) *importer.Importer {
|
|
return importer.New(importer.Opts{
|
|
Lo: initLogger("importer"),
|
|
I18n: i18n,
|
|
})
|
|
}
|
|
|
|
// initNotifDispatcher initializes the notification dispatcher.
|
|
func initNotifDispatcher(userNotification *notifier.UserNotificationManager, outbound *notifier.Service, wsHub *ws.Hub, emailEnabled bool) *notifier.Dispatcher {
|
|
return notifier.NewDispatcher(notifier.DispatcherOpts{
|
|
InApp: userNotification,
|
|
Outbound: outbound,
|
|
WSHub: wsHub,
|
|
EmailEnabled: emailEnabled,
|
|
Lo: initLogger("notification-dispatcher"),
|
|
})
|
|
}
|
|
|
|
// initLogger initializes a logf logger.
|
|
func initLogger(src string) *logf.Logger {
|
|
lvl, env := ko.MustString("app.log_level"), ko.MustString("app.env")
|
|
lo := logf.New(logf.Opts{
|
|
Level: getLogLevel(lvl),
|
|
EnableColor: getColor(env),
|
|
EnableCaller: true,
|
|
CallerSkipFrameCount: 3,
|
|
DefaultFields: []any{"sc", src},
|
|
})
|
|
return &lo
|
|
}
|
|
|
|
func getColor(env string) bool {
|
|
color := false
|
|
if env == "dev" {
|
|
color = true
|
|
}
|
|
return color
|
|
}
|
|
|
|
func getLogLevel(lvl string) logf.Level {
|
|
switch lvl {
|
|
case "info":
|
|
return logf.InfoLevel
|
|
case "debug":
|
|
return logf.DebugLevel
|
|
case "warn":
|
|
return logf.WarnLevel
|
|
case "error":
|
|
return logf.ErrorLevel
|
|
case "fatal":
|
|
return logf.FatalLevel
|
|
default:
|
|
return logf.InfoLevel
|
|
}
|
|
}
|
|
|
|
// initRateLimit initializes the rate limiter with default rules.
|
|
// Defaults are used unless overridden in config.toml under [rate_limit.<name>].
|
|
func initRateLimit(redisClient *redis.Client) *ratelimit.Limiter {
|
|
limiter := ratelimit.New(redisClient)
|
|
|
|
defaults := []struct {
|
|
Name string
|
|
RPM int
|
|
}{
|
|
{"widget", 100},
|
|
{"auth", 30},
|
|
{"public", 100},
|
|
{"media", 300},
|
|
}
|
|
|
|
for _, d := range defaults {
|
|
enabled := true
|
|
rpm := d.RPM
|
|
|
|
cfgKey := "rate_limit." + d.Name
|
|
if ko.Exists(cfgKey) {
|
|
var cfg struct {
|
|
Enabled bool `toml:"enabled"`
|
|
RequestsPerMinute int `toml:"requests_per_minute"`
|
|
}
|
|
ko.UnmarshalWithConf(cfgKey, &cfg, koanf.UnmarshalConf{Tag: "toml"})
|
|
enabled = cfg.Enabled
|
|
if cfg.RequestsPerMinute > 0 {
|
|
rpm = cfg.RequestsPerMinute
|
|
}
|
|
}
|
|
|
|
if !enabled {
|
|
log.Printf("WARNING: rate limit rule '%s' is disabled", d.Name)
|
|
}
|
|
|
|
limiter.AddRule(ratelimit.Rule{
|
|
Name: d.Name,
|
|
Enabled: enabled,
|
|
RequestsPerMinute: rpm,
|
|
})
|
|
}
|
|
|
|
return limiter
|
|
}
|