mirror of https://gitlab.com/ngerakines/tavern.git
317 lines
7.8 KiB
Go
317 lines
7.8 KiB
Go
package web
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"html/template"
|
|
"net/http"
|
|
"os"
|
|
"os/signal"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/foolin/goview"
|
|
"github.com/foolin/goview/supports/ginview"
|
|
"github.com/getsentry/sentry-go"
|
|
sentrygin "github.com/getsentry/sentry-go/gin"
|
|
"github.com/gin-contrib/sessions"
|
|
"github.com/gin-contrib/sessions/cookie"
|
|
"github.com/gin-contrib/static"
|
|
ginzap "github.com/gin-contrib/zap"
|
|
"github.com/gin-gonic/gin"
|
|
ut "github.com/go-playground/universal-translator"
|
|
"github.com/oklog/run"
|
|
"github.com/urfave/cli/v2"
|
|
"go.uber.org/zap"
|
|
|
|
"github.com/ngerakines/tavern/config"
|
|
"github.com/ngerakines/tavern/g"
|
|
"github.com/ngerakines/tavern/job"
|
|
"github.com/ngerakines/tavern/storage"
|
|
)
|
|
|
|
var Command = cli.Command{
|
|
Name: "server",
|
|
Usage: "Run the web server.",
|
|
Flags: []cli.Flag{
|
|
&config.EnvironmentFlag,
|
|
&config.ListenFlag,
|
|
&config.DomainFlag,
|
|
&config.DatabaseFlag,
|
|
&config.TranslationsFlag,
|
|
&config.SecretFlag,
|
|
&cli.StringFlag{
|
|
Name: "admin-name",
|
|
Usage: "The name of the admin user",
|
|
EnvVars: []string{"ADMIN_NAME"},
|
|
Value: "nick",
|
|
},
|
|
&config.EnableSVGerFlag,
|
|
&config.SVGerEndpointFlag,
|
|
},
|
|
Action: serverCommandAction,
|
|
}
|
|
|
|
func serverCommandAction(cliCtx *cli.Context) error {
|
|
logger, err := config.Logger(cliCtx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
domain := cliCtx.String("domain")
|
|
siteBase := fmt.Sprintf("https://%s", domain)
|
|
|
|
logger.Info("Starting",
|
|
zap.String("command", cliCtx.Command.Name),
|
|
zap.String("GOOS", runtime.GOOS),
|
|
zap.String("site", siteBase),
|
|
zap.String("env", cliCtx.String("environment")))
|
|
|
|
ohioLocal, err = time.LoadLocation("America/New_York")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
sentryConfig, err := config.NewSentryConfig(cliCtx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
svgerConfig := config.NewSVGerConfig(cliCtx)
|
|
|
|
if sentryConfig.Enabled {
|
|
err = sentry.Init(sentry.ClientOptions{
|
|
Dsn: sentryConfig.Key,
|
|
Environment: cliCtx.String("environment"),
|
|
Release: fmt.Sprintf("%s-%s", g.ReleaseCode, g.GitCommit),
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
sentry.ConfigureScope(func(scope *sentry.Scope) {
|
|
scope.SetTags(map[string]string{"container": "server"})
|
|
})
|
|
defer sentry.Recover()
|
|
}
|
|
|
|
db, dbClose, err := config.DB(cliCtx, logger)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer dbClose()
|
|
|
|
s := storage.DefaultStorage(db)
|
|
|
|
utrans, err := config.Trans(cliCtx)
|
|
if err != nil {
|
|
panic(err)
|
|
return err
|
|
}
|
|
|
|
r := gin.New()
|
|
if sentryConfig.Enabled {
|
|
r.Use(sentrygin.New(sentrygin.Options{
|
|
Repanic: true,
|
|
}))
|
|
}
|
|
|
|
r.HTMLRender = ginview.New(goview.Config{
|
|
Root: "templates",
|
|
Extension: ".html",
|
|
Master: filepath.Join("layouts", "master"),
|
|
Partials: []string{
|
|
"partials/flashes",
|
|
"partials/dashboard_menu",
|
|
"partials/activity_feed",
|
|
"partials/activity_create_note",
|
|
"partials/activity_announce_note",
|
|
},
|
|
Funcs: template.FuncMap{
|
|
"date": tmplDate,
|
|
"odate": tmplOptionalDate,
|
|
"time": tmplTime,
|
|
"otime": tmplOptionalTime,
|
|
"datetime": tmplDateTime,
|
|
"odatetime": tmplOptionalDateTime,
|
|
"shortUUID": tmplShortUUID,
|
|
"short": tmplShort,
|
|
"title": strings.Title,
|
|
"a": tmplAnchor,
|
|
"toHTML": tmplToHTML,
|
|
"strong": tmplStrong,
|
|
"wrapMaybe": tmplWrapMaybe,
|
|
"url": tmplUrlGen(siteBase),
|
|
"dict": tmplDict,
|
|
"lookupStr": tmplLookupStr,
|
|
"lookupBool": tmplLookupBool,
|
|
"urlEncode": tmplUrlEncode,
|
|
},
|
|
DisableCache: true,
|
|
})
|
|
|
|
r.Use(func(i *gin.Context) {
|
|
nonce := i.GetHeader("X-Request-ID")
|
|
if nonce != "" {
|
|
i.Set("nonce", nonce)
|
|
}
|
|
i.Next()
|
|
})
|
|
|
|
r.Use(ginzap.Ginzap(logger, time.RFC3339, true))
|
|
|
|
r.GET("/health", func(i *gin.Context) {
|
|
i.Data(200, "text/plain", []byte("OK"))
|
|
})
|
|
|
|
r.Use(static.Serve("/public", static.LocalFile("./public", false)))
|
|
|
|
secret, err := config.Secret(cliCtx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
webFingerQueue := storage.NewStringQueue()
|
|
crawlQueue := storage.NewStringQueue()
|
|
|
|
var svgConv SVGConverter
|
|
if svgerConfig.Enabled {
|
|
svgConv = DefaultSVGerClient(svgerConfig.Location)
|
|
}
|
|
|
|
h := handler{
|
|
storage: s,
|
|
logger: logger,
|
|
domain: domain,
|
|
sentryConfig: sentryConfig,
|
|
webFingerQueue: webFingerQueue,
|
|
crawlQueue: crawlQueue,
|
|
adminUser: cliCtx.String("admin-name"),
|
|
url: tmplUrlGen(siteBase),
|
|
svgConverter: svgConv,
|
|
}
|
|
|
|
configI18nMiddleware(sentryConfig, logger, utrans, domain, r)
|
|
|
|
root := r.Group("/")
|
|
{
|
|
root.GET("/.well-known/webfinger", h.webFinger)
|
|
root.GET("/.well-known/nodeinfo", h.nodeInfo)
|
|
root.GET("/nodeinfo/2.0", h.nodeInfoDetails)
|
|
}
|
|
|
|
apiV1 := r.Group("/api/v1")
|
|
{
|
|
apiV1.GET("/instance", h.apiV1Instance)
|
|
apiV1.GET("/instance/peers", h.apiV1InstancePeers)
|
|
}
|
|
|
|
root.GET("/activity/:activity", h.getActivity)
|
|
root.GET("/object/:object", h.getObject)
|
|
root.GET("/tags/:tag", h.getTaggedObjects)
|
|
|
|
root.GET("/avatar/svg/:domain/:name", h.avatarSVG)
|
|
if svgerConfig.Enabled {
|
|
root.GET("/avatar/png/:domain/:name", h.avatarPNG)
|
|
}
|
|
|
|
authenticated := r.Group("/")
|
|
{
|
|
configSessionMiddleware(secret, domain, authenticated)
|
|
authenticated.GET("/", h.home)
|
|
authenticated.POST("/signin", h.signin)
|
|
authenticated.GET("/signout", h.signout)
|
|
|
|
authenticated.GET("/feed", h.viewFeed)
|
|
authenticated.GET("/feed/mine", h.viewMyFeed)
|
|
authenticated.GET("/feed/local", h.viewFeed)
|
|
|
|
authenticated.GET("/network", h.dashboardNetwork)
|
|
authenticated.POST("/network/follow", h.networkFollow)
|
|
authenticated.POST("/network/unfollow", h.networkUnfollow)
|
|
authenticated.POST("/network/accept", h.networkAccept)
|
|
authenticated.POST("/network/reject", h.networkReject)
|
|
|
|
authenticated.GET("/compose", h.compose)
|
|
authenticated.POST("/compose/create/note", h.createNote)
|
|
authenticated.POST("/dashboard/notes/announce/note", h.announceNote)
|
|
|
|
authenticated.GET("/configure", h.configure)
|
|
|
|
authenticated.GET("/utilities", h.utilities)
|
|
authenticated.POST("/utilities/webfinger", h.utilitiesWebfinger)
|
|
}
|
|
|
|
actor := r.Group("/users")
|
|
{
|
|
actor.GET("/:name", h.actorInfo)
|
|
actor.POST("/:name/inbox", h.actorInbox)
|
|
actor.GET("/:name/outbox", h.actorOutbox)
|
|
actor.GET("/:name/following", h.actorFollowing)
|
|
actor.GET("/:name/followers", h.actorFollowers)
|
|
}
|
|
|
|
var group run.Group
|
|
|
|
srv := &http.Server{
|
|
Addr: config.ListenAddress(cliCtx),
|
|
Handler: r,
|
|
}
|
|
|
|
parentCtx, parentCtxCancel := context.WithCancel(context.Background())
|
|
defer parentCtxCancel()
|
|
|
|
group.Add(func() error {
|
|
logger.Info("starting http service", zap.String("address", srv.Addr))
|
|
return srv.ListenAndServe()
|
|
}, func(error) {
|
|
httpCancelCtx, httpCancelCtxCancel := context.WithTimeout(context.Background(), 30*time.Second)
|
|
defer httpCancelCtxCancel()
|
|
logger.Info("stopping http service")
|
|
if err := srv.Shutdown(httpCancelCtx); err != nil {
|
|
logger.Info("error stopping http service:", zap.Error(err))
|
|
}
|
|
})
|
|
|
|
webFingerJob := job.NewWebFingerWorker(logger, webFingerQueue, s)
|
|
job.RunWorker(&group, logger, webFingerJob, parentCtx)
|
|
|
|
crawlJob := job.NewCrawlWorker(logger, crawlQueue, s)
|
|
job.RunWorker(&group, logger, crawlJob, parentCtx)
|
|
|
|
quit := make(chan os.Signal)
|
|
signal.Notify(quit, os.Interrupt)
|
|
group.Add(func() error {
|
|
logger.Info("starting signal listener")
|
|
<-quit
|
|
return nil
|
|
}, func(error) {
|
|
logger.Info("stopping signal listener")
|
|
close(quit)
|
|
})
|
|
|
|
return group.Run()
|
|
}
|
|
|
|
func configI18nMiddleware(sentryCfg config.SentryConfig, logger *zap.Logger, utrans *ut.UniversalTranslator, domain string, r gin.IRouter) {
|
|
im := i18nMiddleware{
|
|
utrans: utrans,
|
|
logger: logger,
|
|
domain: domain,
|
|
enableSentry: sentryCfg.Enabled,
|
|
}
|
|
r.Use(im.findLocale)
|
|
}
|
|
|
|
func configSessionMiddleware(secret []byte, domain string, r gin.IRouter) {
|
|
store := cookie.NewStore(secret)
|
|
store.Options(sessions.Options{
|
|
Path: "/",
|
|
Domain: domain,
|
|
MaxAge: 86400 * 30,
|
|
Secure: true,
|
|
HttpOnly: true,
|
|
})
|
|
r.Use(sessions.Sessions("sess", store))
|
|
}
|