6
1
mirror of https://git.mills.io/saltyim/saltyim.git synced 2024-06-28 09:41:02 +00:00
prologic-saltyim/internal/server.go
2022-03-24 13:39:38 -05:00

305 lines
6.3 KiB
Go

package internal
import (
"context"
"fmt"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"git.mills.io/prologic/msgbus"
"git.mills.io/prologic/observe"
"github.com/NYTimes/gziphandler"
"github.com/justinas/nosurf"
"github.com/maxence-charriere/go-app/v9/pkg/app"
"github.com/robfig/cron"
log "github.com/sirupsen/logrus"
"github.com/unrolled/logger"
"go.mills.io/saltyim/internal/pwa/routes"
"go.mills.io/saltyim"
)
var (
metrics *observe.Metrics
)
func init() {
metrics = observe.NewMetrics("twtd")
}
var (
resources app.ResourceProvider
)
// Server ...
type Server struct {
bind string
config *Config
router *Router
server *http.Server
// Message Bus
bus *msgbus.MessageBus
// Data Store
db Store
// Scheduler
cron *cron.Cron
// API
api *API
}
// AddRouter ...
func (s *Server) AddRoute(method, path string, handler http.Handler) {
s.router.Handler(method, path, handler)
}
// AddShutdownHook ...
func (s *Server) AddShutdownHook(f func()) {
s.server.RegisterOnShutdown(f)
}
// Shutdown ...
func (s *Server) Shutdown(ctx context.Context) error {
s.cron.Stop()
if err := s.server.Shutdown(ctx); err != nil {
log.WithError(err).Error("error shutting down server")
return err
}
if err := s.db.Close(); err != nil {
log.WithError(err).Error("error closing store")
return err
}
return nil
}
// Run ...
func (s *Server) Run() (err error) {
idleConnsClosed := make(chan struct{})
go func() {
if err = s.ListenAndServe(); err != http.ErrServerClosed {
// Error starting or closing listener:
log.WithError(err).Fatal("HTTP server ListenAndServe")
}
}()
sigch := make(chan os.Signal, 1)
signal.Notify(sigch, syscall.SIGINT, syscall.SIGTERM)
sig := <-sigch
log.Infof("Received signal %s", sig)
log.Info("Shutting down...")
// We received an interrupt signal, shut down.
if err = s.Shutdown(context.Background()); err != nil {
// Error from closing listeners, or context timeout:
log.WithError(err).Fatal("Error shutting down HTTP server")
}
close(idleConnsClosed)
<-idleConnsClosed
return
}
// ListenAndServe ...
func (s *Server) ListenAndServe() error {
return s.server.ListenAndServe()
}
// AddCronJob ...
func (s *Server) AddCronJob(spec string, job cron.Job) error {
return s.cron.AddJob(spec, job)
}
func (s *Server) setupMetrics() {
ctime := time.Now()
// server uptime counter
metrics.NewCounterFunc(
"server", "uptime",
"Number of nanoseconds the server has been running",
func() float64 {
return float64(time.Since(ctime).Nanoseconds())
},
)
// server info
metrics.NewGaugeVec(
"server", "info",
"Server information",
[]string{"full_version", "version", "commit"},
)
metrics.GaugeVec("server", "info").
With(map[string]string{
"full_version": saltyim.FullVersion(),
"version": saltyim.Version,
"commit": saltyim.Commit,
}).Set(1)
s.AddRoute("GET", "/metrics", metrics.Handler())
}
func (s *Server) setupCronJobs() error {
InitJobs(s.config)
for name, jobSpec := range Jobs {
if jobSpec.Schedule == "" {
continue
}
job := jobSpec.Factory(s.config, s.db)
if err := s.cron.AddJob(jobSpec.Schedule, job); err != nil {
return fmt.Errorf("invalid cron schedule for job %s: %v (see https://pkg.go.dev/github.com/robfig/cron)", name, err)
}
log.Infof("Started background job %s (%s)", name, jobSpec.Schedule)
}
return nil
}
func (s *Server) runStartupJobs() {
time.Sleep(time.Second * 5)
log.Info("running startup jobs")
for name, jobSpec := range StartupJobs {
job := jobSpec.Factory(s.config, s.db)
log.Infof("running %s now...", name)
job.Run()
}
// Merge store
if err := s.db.Merge(); err != nil {
log.WithError(err).Error("error merging store")
}
}
func (s *Server) initRoutes() {
app := &app.Handler{
Name: "Salty Chat!",
ShortName: "Salty.im",
Description: "Secure, easy, self-hosted messaging",
Icon: app.Icon{
Large: "/web/favicon-lg.png",
Default: "/web/favicon-ap.png",
},
Styles: []string{
"/web/css/wdc.min.css",
"/web/css/style.css",
"/web/css/micons.woff2",
},
Scripts: []string{
"/web/js/wdc.min.js",
},
Resources: resources,
}
s.router.Handler(http.MethodGet, "/", app)
s.router.Handler(http.MethodGet, "/config", app)
s.router.Handler(http.MethodGet, "/app.js", app)
s.router.Handler(http.MethodGet, "/app.css", app)
s.router.Handler(http.MethodGet, "/web/*static", app)
s.router.Handler(http.MethodGet, "/wasm_exec.js", app)
s.router.Handler(http.MethodGet, "/app-worker.js", app)
s.router.Handler(http.MethodGet, "/manifest.webmanifest", app)
s.router.GET("/inbox/:inbox", s.InboxHandler())
s.router.POST("/inbox/:inbox", s.InboxHandler())
}
// NewServer ...
func NewServer(bind string, options ...Option) (*Server, error) {
config := NewConfig()
for _, opt := range options {
if err := opt(config); err != nil {
return nil, err
}
}
if err := config.Validate(); err != nil {
log.WithError(err).Error("error validating config")
return nil, fmt.Errorf("error validating config: %w", err)
}
bus := msgbus.New(nil)
db, err := NewStore(config.Store)
if err != nil {
log.WithError(err).Error("error creating store")
return nil, err
}
if err := db.Merge(); err != nil {
log.WithError(err).Error("error merging store")
return nil, err
}
router := NewRouter()
api := NewAPI(router, config, db)
csrfHandler := nosurf.New(router)
csrfHandler.ExemptGlob("/api/v1/*")
csrfHandler.ExemptGlob("/inbox/*")
routes.AddRoutes()
server := &Server{
bind: bind,
config: config,
router: router,
server: &http.Server{
Addr: bind,
Handler: logger.New(logger.Options{
Prefix: "pollinator",
RemoteAddressHeaders: []string{"X-Forwarded-For"},
}).Handler(gziphandler.GzipHandler(
csrfHandler,
),
),
},
// Bus
bus: bus,
// API
api: api,
// Data Store
db: db,
// Schedular
cron: cron.New(),
}
if err := server.setupCronJobs(); err != nil {
log.WithError(err).Error("error setting up background jobs")
return nil, err
}
server.cron.Start()
log.Info("started background jobs")
server.setupMetrics()
log.Infof("serving metrics endpoint at http://%s/metrics", server.bind)
// Log interesting configuration options
log.Infof("Debug: %t", server.config.Debug)
api.initRoutes()
server.initRoutes()
go server.runStartupJobs()
return server, nil
}