Files
libredesk/cmd/main.go
Abhinav Raut 30902310dc feat: Add Markdown to HTML conversion and clean JSON response utility
- Implemented MarkdownToHTML function using goldmark for converting markdown content to HTML.
- Added CleanJSONResponse function to remove markdown code blocks from LLM responses.
- Updated stringutil tests to remove unnecessary test cases for empty strings and special characters.

refactor: Update SQL schema for knowledge base and help center

- Introduced ai_knowledge_type enum for knowledge base categorization.
- Added help_center_id reference in inboxes table.
- Enhanced help_centers table with default_locale column.
- Changed data types from INTEGER to INT for consistency across tables.
- Renamed ai_custom_answers table to ai_knowledge_base and adjusted its schema.

fix: Remove unnecessary CSS filter from default icon in widget

- Cleaned up widget.js by removing the brightness filter from the default icon styling.
2025-08-22 01:14:08 +05:30

327 lines
11 KiB
Go

package main
import (
"context"
"fmt"
"log"
"os"
"os/signal"
"sync"
"sync/atomic"
"syscall"
"time"
_ "time/tzdata"
_ "github.com/pgvector/pgvector-go"
activitylog "github.com/abhinavxd/libredesk/internal/activity_log"
"github.com/abhinavxd/libredesk/internal/ai"
auth_ "github.com/abhinavxd/libredesk/internal/auth"
"github.com/abhinavxd/libredesk/internal/authz"
businesshours "github.com/abhinavxd/libredesk/internal/business_hours"
"github.com/abhinavxd/libredesk/internal/colorlog"
"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/macro"
notifier "github.com/abhinavxd/libredesk/internal/notification"
"github.com/abhinavxd/libredesk/internal/report"
"github.com/abhinavxd/libredesk/internal/search"
"github.com/abhinavxd/libredesk/internal/sla"
"github.com/abhinavxd/libredesk/internal/view"
"github.com/abhinavxd/libredesk/internal/automation"
"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/inbox"
"github.com/abhinavxd/libredesk/internal/media"
"github.com/abhinavxd/libredesk/internal/oidc"
"github.com/abhinavxd/libredesk/internal/ratelimit"
"github.com/abhinavxd/libredesk/internal/role"
"github.com/abhinavxd/libredesk/internal/setting"
"github.com/abhinavxd/libredesk/internal/tag"
"github.com/abhinavxd/libredesk/internal/team"
"github.com/abhinavxd/libredesk/internal/template"
"github.com/abhinavxd/libredesk/internal/user"
"github.com/abhinavxd/libredesk/internal/webhook"
"github.com/knadh/go-i18n"
"github.com/knadh/koanf/v2"
"github.com/knadh/stuffbin"
"github.com/valyala/fasthttp"
"github.com/zerodha/fastglue"
"github.com/zerodha/logf"
)
var (
ko = koanf.New(".")
ctx = context.Background()
appName = "libredesk"
frontendDir = "frontend/dist/main"
widgetDir = "frontend/dist/widget"
// Injected at build time.
buildString string
versionString string
)
// App is the global app context which is passed and injected in the http handlers.
type App struct {
fs stuffbin.FileSystem
consts atomic.Value
auth *auth_.Auth
authz *authz.Enforcer
i18n *i18n.I18n
lo *logf.Logger
oidc *oidc.Manager
media *media.Manager
setting *setting.Manager
role *role.Manager
user *user.Manager
team *team.Manager
status *status.Manager
priority *priority.Manager
tag *tag.Manager
inbox *inbox.Manager
tmpl *template.Manager
macro *macro.Manager
conversation *conversation.Manager
automation *automation.Engine
businessHours *businesshours.Manager
sla *sla.Manager
csat *csat.Manager
view *view.Manager
ai *ai.Manager
search *search.Manager
activityLog *activitylog.Manager
notifier *notifier.Service
customAttribute *customAttribute.Manager
report *report.Manager
webhook *webhook.Manager
rateLimit *ratelimit.Limiter
helpcenter *helpcenter.Manager
// Global state that stores data on an available app update.
update *AppUpdate
sync.Mutex
}
func main() {
// Set up signal handler.
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
defer stop()
// Load command line flags into Koanf.
initFlags()
// Version flag.
if ko.Bool("version") {
fmt.Println(buildString)
os.Exit(0)
}
// Build string injected at build time.
colorlog.Green("Build: %s", buildString)
// Load the config files into Koanf.
initConfig(ko)
// Init stuffbin fs.
fs := initFS()
// Init DB.
db := initDB()
// Installer.
if ko.Bool("install") {
install(ctx, db, fs, ko.Bool("idempotent-install"), !ko.Bool("yes"))
os.Exit(0)
}
// Set system user password.
if ko.Bool("set-system-user-password") {
setSystemUserPass(ctx, db)
os.Exit(0)
}
// Check if schema is installed.
installed, err := checkSchema(db)
if err != nil {
log.Fatalf("error checking db schema: %v", err)
}
if !installed {
log.Println("database tables are missing. Use the `--install` flag to set up the database schema.")
os.Exit(0)
}
// Upgrade.
if ko.Bool("upgrade") {
upgrade(db, fs, !ko.Bool("yes"))
os.Exit(0)
}
// Check for pending upgrade.
checkPendingUpgrade(db)
// Load app settings from DB into the Koanf instance.
settings := initSettings(db)
loadSettings(settings)
// Fallback for config typo. Logs a warning but continues to work with the incorrect key.
// Uses 'message.message_outgoing_scan_interval' (correct key) as default key, falls back to the common typo.
msgOutgoingScanIntervalKey := "message.message_outgoing_scan_interval"
if ko.String(msgOutgoingScanIntervalKey) == "" {
if ko.String("message.message_outoing_scan_interval") != "" {
colorlog.Red("WARNING: typo in config key 'message.message_outoing_scan_interval' detected. Use 'message.message_outgoing_scan_interval' instead in your config.toml file. Support for this incorrect key will be removed in a future release.")
msgOutgoingScanIntervalKey = "message.message_outoing_scan_interval"
}
}
var (
autoAssignInterval = ko.MustDuration("autoassigner.autoassign_interval")
unsnoozeInterval = ko.MustDuration("conversation.unsnooze_interval")
automationWorkers = ko.MustInt("automation.worker_count")
messageOutgoingQWorkers = ko.MustDuration("message.outgoing_queue_workers")
messageIncomingQWorkers = ko.MustDuration("message.incoming_queue_workers")
messageOutgoingScanInterval = ko.MustDuration(msgOutgoingScanIntervalKey)
slaEvaluationInterval = ko.MustDuration("sla.evaluation_interval")
lo = initLogger(appName)
rdb = initRedis()
constants = initConstants()
i18n = initI18n(fs)
csat = initCSAT(db, i18n)
oidc = initOIDC(db, settings, i18n)
status = initStatus(db, i18n)
priority = initPriority(db, i18n)
auth = initAuth(oidc, rdb, i18n)
template = initTemplate(db, fs, constants, i18n)
media = initMedia(db, i18n)
inbox = initInbox(db, i18n)
team = initTeam(db, i18n)
businessHours = initBusinessHours(db, i18n)
webhook = initWebhook(db, i18n)
user = initUser(i18n, db)
wsHub = initWS(user)
notifier = initNotifier()
automation = initAutomationEngine(db, i18n)
sla = initSLA(db, team, settings, businessHours, notifier, template, user, i18n)
conversation = initConversations(i18n, sla, status, priority, wsHub, notifier, db, inbox, user, team, media, settings, csat, automation, template, webhook)
autoassigner = initAutoAssigner(team, user, conversation)
rateLimiter = initRateLimit(rdb)
helpcenter = initHelpCenter(db, i18n)
ai = initAI(db, i18n, conversation, helpcenter)
)
wsHub.SetConversationStore(conversation)
automation.SetConversationStore(conversation)
conversation.SetAIStore(ai)
helpcenter.SetAIStore(ai)
// Start inboxes.
startInboxes(ctx, inbox, conversation, user)
go automation.Run(ctx, automationWorkers)
go autoassigner.Run(ctx, autoAssignInterval)
go conversation.Run(ctx, messageIncomingQWorkers, messageOutgoingQWorkers, messageOutgoingScanInterval)
go conversation.RunUnsnoozer(ctx, unsnoozeInterval)
go webhook.Run(ctx)
go notifier.Run(ctx)
go sla.Run(ctx, slaEvaluationInterval)
go sla.SendNotifications(ctx)
go media.DeleteUnlinkedMedia(ctx)
go user.MonitorAgentAvailability(ctx)
go ai.StartConversationCompletions()
var app = &App{
lo: lo,
fs: fs,
sla: sla,
oidc: oidc,
i18n: i18n,
auth: auth,
media: media,
setting: settings,
inbox: inbox,
user: user,
team: team,
status: status,
priority: priority,
tmpl: template,
notifier: notifier,
consts: atomic.Value{},
conversation: conversation,
automation: automation,
businessHours: businessHours,
activityLog: initActivityLog(db, i18n),
customAttribute: initCustomAttribute(db, i18n),
authz: initAuthz(i18n),
view: initView(db),
report: initReport(db, i18n),
csat: initCSAT(db, i18n),
search: initSearch(db, i18n),
role: initRole(db, i18n),
tag: initTag(db, i18n),
macro: initMacro(db, i18n),
ai: ai,
webhook: webhook,
rateLimit: rateLimiter,
helpcenter: helpcenter,
}
app.consts.Store(constants)
g := fastglue.NewGlue()
g.SetContext(app)
initHandlers(g, wsHub)
s := &fasthttp.Server{
Name: appName,
ReadTimeout: ko.MustDuration("app.server.read_timeout"),
WriteTimeout: ko.MustDuration("app.server.write_timeout"),
MaxRequestBodySize: ko.MustInt("app.server.max_body_size"),
MaxKeepaliveDuration: ko.MustDuration("app.server.keepalive_timeout"),
ReadBufferSize: ko.Int("app.server.read_buffer_size"),
}
go func() {
colorlog.Green("Server started at %s", ko.String("app.server.address"))
if ko.String("server.socket") != "" {
colorlog.Green("Unix socket created at %s", ko.String("server.socket"))
}
if err := g.ListenAndServe(ko.String("app.server.address"), ko.String("server.socket"), s); err != nil {
log.Fatalf("error starting server: %v", err)
}
}()
// Start the app update checker.
if ko.Bool("app.check_updates") {
go checkUpdates(versionString, time.Hour*1, app)
}
// Wait for shutdown signal.
<-ctx.Done()
colorlog.Red("Shutting down HTTP server...")
s.Shutdown()
colorlog.Red("Shutting down inboxes...")
inbox.Close()
colorlog.Red("Shutting down automation...")
automation.Close()
colorlog.Red("Shutting down autoassigner...")
autoassigner.Close()
colorlog.Red("Shutting down notifier...")
notifier.Close()
colorlog.Red("Shutting down webhook...")
webhook.Close()
colorlog.Red("Shutting down conversation...")
conversation.Close()
colorlog.Red("Shutting down AI...")
app.ai.StopConversationCompletions()
colorlog.Red("Shutting down SLA...")
sla.Close()
colorlog.Red("Shutting down database...")
db.Close()
colorlog.Red("Shutting down redis...")
rdb.Close()
colorlog.Green("Shutdown complete.")
}