package main import ( "context" "errors" "log/slog" "net/http" "os" "os/signal" "syscall" "time" "dana/internal/api" "dana/internal/bitrix" "dana/internal/config" "dana/internal/megafon" "dana/internal/notify" "dana/internal/onec" "dana/internal/prodoctorov" "dana/internal/sberhealth" "dana/internal/storage" ) func main() { if err := run(); err != nil { slog.Error("fatal", "err", err) os.Exit(1) } } func run() error { cfg, err := config.Load() if err != nil { return err } level := slog.LevelInfo if cfg.Debug { level = slog.LevelDebug } slog.SetDefault(slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: level}))) ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) defer stop() pd, err := prodoctorov.New(cfg.ProdoctorovCookie, cfg.ProdoctorovLPU) if err != nil { return err } sh, err := sberhealth.New(cfg.SberHealthCookie, cfg.SberHealthClinic) if err != nil { return err } store, err := storage.New(ctx, cfg.DatabaseURL) if err != nil { return err } defer store.Close() if err := store.Migrate(ctx); err != nil { return err } // Первый администратор заводится один раз, если таблица пуста. if err := store.EnsureAdmin(ctx, cfg.AdminEmail, cfg.AdminPassword); err != nil { return err } if err := api.SetTrustedProxies(cfg.TrustedProxies); err != nil { return err } handler := api.New(store, cfg.MediaDir) if err := handler.FailInterruptedReports(ctx); err != nil { slog.Warn("mark interrupted reports", "err", err) } handler.WithPBX(megafon.New(cfg.MegafonURL, cfg.MegafonKey)) if cfg.OchevidnoMegafonURL != "" && cfg.OchevidnoMegafonKey != "" { handler.WithClinicPBX("ochevidno", megafon.New(cfg.OchevidnoMegafonURL, cfg.OchevidnoMegafonKey)) } go handler.RunPBXSync(ctx) handler.WithOneC(onec.New(cfg.OnecURL, cfg.OnecKey, cfg.OnecAuthHeader)) handler.WithOneCWebhook(cfg.OnecWebhookKey) handler.WithOneCTuning(time.Duration(cfg.OnecSyncSecs)*time.Second, cfg.OnecLongPoll) go handler.RunOneCSync(ctx) handler.WithBitrix(bitrix.New(cfg.BitrixWebhookURL)) go handler.RunBookingConfirm(ctx) handler.WithSMSC(cfg.SMSCLogin, cfg.SMSCPassword) handler.WithSMSCCallbackToken(cfg.SMSCCallbackToken) handler.WithProdoctorov(pd) handler.WithSberHealth(sh) if err := handler.WithWebsite(cfg.WebDir, cfg.SiteURL); err != nil { return err } if err := handler.WithSSR(cfg.SSRRenderer, cfg.NodeBinary); err != nil { return err } if cfg.SMTPHost != "" { handler.WithNotifier(¬ify.Email{ Host: cfg.SMTPHost, Port: cfg.SMTPPort, User: cfg.SMTPUser, Password: cfg.SMTPPassword, From: cfg.NotifyFrom, To: cfg.NotifyTo, SiteURL: cfg.SiteURL, }) slog.Info("lead notifications by email", "smtp", cfg.SMTPHost, "to", cfg.NotifyTo) } else { slog.Warn("SMTP_HOST is not set: lead notifications are disabled") } srv := &http.Server{ Addr: cfg.Addr, Handler: handler.Routes(), ReadHeaderTimeout: 10 * time.Second, ReadTimeout: 30 * time.Second, WriteTimeout: 30 * time.Second, IdleTimeout: 60 * time.Second, } errCh := make(chan error, 1) go func() { slog.Info("listening", "addr", cfg.Addr) if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { errCh <- err } }() select { case err := <-errCh: return err case <-ctx.Done(): slog.Info("shutting down") } shutdownCtx, cancel := context.WithTimeout(context.Background(), cfg.ShutdownTimeout) defer cancel() return srv.Shutdown(shutdownCtx) }