barkeep: initial commit
13 files changed, 276 insertions(+), 5 deletions(-)
A cmd/barkeep/.envrc
@@ -0,0 +1,7 @@ +source_env ../../.envrc + +export KO_DATA_PATH=$(expand_path ../../kodata) + +watch_file .env +dotenv_if_exists +env_vars_required PUBLISHER_BASE_URL
A cmd/barkeep/justfile
@@ -0,0 +1,8 @@ +listen_address := env_var_or_default("SERVER_LISTEN_ADDRESS", "::1") +port := env_var_or_default("SERVER_PORT", "8081") + +run: + systemfd -s http::{{ listen_address }}:{{ port }} -- wgo run --root ../.. . --dev + +build: + go build .
A cmd/barkeep/main.go
@@ -0,0 +1,72 @@ +package main + +import ( + "fmt" + "os" + + "github.com/ardanlabs/conf/v3" + "gitlab.com/tozd/go/errors" + + "go.alanpearce.eu/homestead/internal/publisher" + "go.alanpearce.eu/homestead/internal/server" + "go.alanpearce.eu/homestead/internal/tssrv" + "go.alanpearce.eu/x/log" +) + +type Options struct { + Development bool `conf:"flag:dev"` + Publisher publisher.Options + Tailscale tssrv.Options + Server server.Options +} + +func main() { + options := &Options{} + help, err := conf.Parse("", options) + if err != nil { + if errors.Is(err, conf.ErrHelpWanted) { + fmt.Println(help) + os.Exit(1) + } + panic("parsing runtime configuration: " + err.Error()) + } + + log := log.Configure(!options.Development) + + if options.Development { + options.Publisher.Development = true + options.Tailscale.Development = true + } + + pub, err := publisher.New(&options.Publisher, log.Named("publisher")) + if err != nil { + log.Fatal("error running publisher", "error", err) + } + + if options.Development { + srv, err := server.New(&options.Server, log.Named("server")) + if err != nil { + log.Fatal("error creating server", "error", err) + } + + if err := srv.HostApp(pub.App); err != nil { + log.Fatal("error hosting app", "error", err) + } + + if err := srv.Start(); err != nil { + log.Fatal("error starting server", "error", err) + } + + return + } + + ts, err := tssrv.New(&options.Tailscale) + if err != nil { + log.Fatal("error creating tailscale server", "error", err) + } + + err = ts.Serve(pub.Handler) + if err != nil { + log.Fatal("error serving", "error", err) + } +}
M go.mod → go.mod
@@ -27,8 +27,9 @@ github.com/snabb/sitemap v1.0.4 github.com/stefanfritsch/goldmark-fences v1.0.0 github.com/yuin/goldmark v1.7.8 gitlab.com/tozd/go/errors v0.10.0 - go.alanpearce.eu/gomponents v1.2.0 + go.alanpearce.eu/gomponents v1.4.0 go.alanpearce.eu/x v0.0.0-20250213214218-1bdfdc914d6c + go.hacdias.com/indielib v0.4.3 go.uber.org/zap v1.27.0 modernc.org/sqlite v1.36.0 tailscale.com v1.82.4@@ -141,6 +142,7 @@ golang.org/x/crypto v0.36.0 // indirect golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect golang.org/x/mod v0.24.0 // indirect golang.org/x/net v0.37.0 // indirect + golang.org/x/oauth2 v0.26.0 // indirect golang.org/x/sync v0.12.0 // indirect golang.org/x/sys v0.31.0 // indirect golang.org/x/term v0.30.0 // indirect@@ -156,4 +158,6 @@ modernc.org/libc v1.61.13 // indirect modernc.org/mathutil v1.7.1 // indirect modernc.org/memory v1.8.2 // indirect moul.io/zapfilter v1.7.0 // indirect + willnorris.com/go/microformats v1.2.1-0.20240301064101-b5d1b9d2120e // indirect + willnorris.com/go/webmention v0.0.0-20220108183051-4a23794272f0 // indirect )
M go.sum → go.sum
@@ -24,6 +24,7 @@ github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa h1:LHTHcTQiSGT7VVbI0o4wBRNQIgn917usHWOd6VAffYI= github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4= github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA= github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA= +github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA= github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM= github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=@@ -161,6 +162,7 @@ github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU= github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=@@ -332,6 +334,7 @@ github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701/go.mod h1:P3a5rG4X7tI17Nn3aOIAYr5HbIMukwXG0urG0WuL8OA= github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0= github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8= github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM= +github.com/wsxiaoys/terminal v0.0.0-20160513160801-0940f3fc43a0/go.mod h1:IXCdmsXIht47RaVFLEdVnh1t+pgYtTAhQGj73kz+2DM= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=@@ -345,10 +348,12 @@ github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic= github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= gitlab.com/tozd/go/errors v0.10.0 h1:A98kL+gaDvWnY6ZB/u8zP+sYaWsWUGBHeFMtamvW/74= gitlab.com/tozd/go/errors v0.10.0/go.mod h1:q3Ugr0C8dCzMEkrzjjlV2qNsm9e0KvqBjwcbcjCpBe4= -go.alanpearce.eu/gomponents v1.2.0 h1:5SoLlMMc04xvLcmHVgnScjX1DzBM4mbwyTDa0cOPiD8= -go.alanpearce.eu/gomponents v1.2.0/go.mod h1:uX96UAsHCut1cKMAYVWWxQ9ADt1CAPI8LpyAu0LRQPs= +go.alanpearce.eu/gomponents v1.4.0 h1:Ibvoce+U0rnPKlDOE+wXDbNniNQL8mYO667+qS5J1Go= +go.alanpearce.eu/gomponents v1.4.0/go.mod h1:WxD+6FRSvwThQOzV0r6zPAA9CRb41lutUZMSC7r6BRc= go.alanpearce.eu/x v0.0.0-20250213214218-1bdfdc914d6c h1:4bmqdTMUSf5N5PX/8PbujwKfVu8bactVkRxoK1BDSg8= go.alanpearce.eu/x v0.0.0-20250213214218-1bdfdc914d6c/go.mod h1:tY0oD1bc264ETaBoFQ/tY8vl1T8tDmhIkU56xcqBYH8= +go.hacdias.com/indielib v0.4.3 h1:1QT0ZzMk+vMkoe4uZ31DLnWlLklGPgBYry0I+lCl0qM= +go.hacdias.com/indielib v0.4.3/go.mod h1:W7tSM6pCiM2JLdZ8xzSMpPf3GBB2hz+ONvGfvdp6S9o= go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/atomic v1.8.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=@@ -405,6 +410,8 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211020060615-d418f374d309/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=@@ -415,6 +422,8 @@ golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c= golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= +golang.org/x/oauth2 v0.26.0 h1:afQXWNNaeC4nvZ0Ed9XvCCzXM6UHJG7iCg0W4fPqSBE= +golang.org/x/oauth2 v0.26.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=@@ -496,6 +505,7 @@ golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU= golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg= golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI=@@ -557,3 +567,7 @@ software.sslmate.com/src/go-pkcs12 v0.4.0 h1:H2g08FrTvSFKUj+D309j1DPfk5APnIdAQAB8aEykJ5k= software.sslmate.com/src/go-pkcs12 v0.4.0/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI= tailscale.com v1.82.4 h1:J8QEVQ2DOM0Oy0Ac2UkdBpi8bDvTaGEgd+bBz3g2o7w= tailscale.com v1.82.4/go.mod h1:iU6kohVzG+bP0/5XjqBAnW8/6nSG/Du++bO+x7VJZD0= +willnorris.com/go/microformats v1.2.1-0.20240301064101-b5d1b9d2120e h1:TRIOwo0NxN4KVSgYlYmiQktd9I96YgZ3942/JVzhwTM= +willnorris.com/go/microformats v1.2.1-0.20240301064101-b5d1b9d2120e/go.mod h1:zzo0hFA/E/nl1ZAjXiXA7KCKwCTdgBU+7HXltGgHeGA= +willnorris.com/go/webmention v0.0.0-20220108183051-4a23794272f0 h1:V5+O+YZHchEwu6ZmPcqT1dQ+mHgE356Q+w9SVOQ+QZg= +willnorris.com/go/webmention v0.0.0-20220108183051-4a23794272f0/go.mod h1:DgeruqKIsZtcDXVXNbBHa0YYEm88oAnK7PahkDtuCvw=
M gomod2nix.toml → gomod2nix.toml
@@ -365,11 +365,14 @@ [mod."gitlab.com/tozd/go/errors"] version = "v0.10.0" hash = "sha256-oW37KsieVKJOWk9ZXbGuQvuU4nyJCZzgYrTZHFkoCs4=" [mod."go.alanpearce.eu/gomponents"] - version = "v1.2.0" - hash = "sha256-pF+3We63loSMwhTUafgIdmBYc4cj5yVIVQRyaX1sWB4=" + version = "v1.4.0" + hash = "sha256-Q8YN8eNouMnW/JaBJpjUmaOZ5cfBj5gFURgVlbnaiDM=" [mod."go.alanpearce.eu/x"] version = "v0.0.0-20250213214218-1bdfdc914d6c" hash = "sha256-6XdXMqWwY8rk3i+HUb9wd0idoqh4GXqC6x1NZZC1O6M=" + [mod."go.hacdias.com/indielib"] + version = "v0.4.3" + hash = "sha256-6b4Pb0qUrqHbi1Zvr03bvs81jLQe0rwhkeL41HJJtVg=" [mod."go.uber.org/multierr"] version = "v1.11.0" hash = "sha256-Lb6rHHfR62Ozg2j2JZy3MKOMKdsfzd1IYTR57r3Mhp0="@@ -394,6 +397,9 @@ hash = "sha256-Lrl4RzOO0J/S5E+AQ7Y6yh8B9yygvZ2W0fsKEtBSYNU=" [mod."golang.org/x/net"] version = "v0.37.0" hash = "sha256-sZKbJISVdBwyuYRQgrraTKxeIORWlzK5hScceQ2dE58=" + [mod."golang.org/x/oauth2"] + version = "v0.26.0" + hash = "sha256-WnKyoJ2hxEMMyZLIFqATxGzPWfngCYML3nQJWx/SdXk=" [mod."golang.org/x/sync"] version = "v0.12.0" hash = "sha256-lPIbI6aXx+iKtFjPaXKNLEnuNfhHCVl7EQiE7alUvlM="@@ -445,3 +451,9 @@ hash = "sha256-H6j5h8w123Y7d0zvKGkL5jiRYICtjmgzd2P/eeNaLrs=" [mod."tailscale.com"] version = "v1.82.4" hash = "sha256-19yfGXKlpMUy7F4PgtWKk7cVpHYOtxpOojWspeNiMhA=" + [mod."willnorris.com/go/microformats"] + version = "v1.2.1-0.20240301064101-b5d1b9d2120e" + hash = "sha256-7pXSTRKqzgN1CaeB1bep+V24O6yNIPNDNDL6AZPiI8A=" + [mod."willnorris.com/go/webmention"] + version = "v0.0.0-20220108183051-4a23794272f0" + hash = "sha256-X9n88Wm3Uw4wLmRgkF/bjTnELOXhyGgmpndzfGAQ2R8="
M internal/config/config.go → internal/config/config.go
@@ -25,6 +25,15 @@ type URL struct { *url.URL } +func NewURL(rawURL string) URL { + u, err := url.Parse(rawURL) + if err != nil { + panic(err) + } + + return URL{u} +} + func (u *URL) UnmarshalText(text []byte) (err error) { u.URL, err = url.Parse(string(text))
A internal/publisher/app.go
@@ -0,0 +1,58 @@ +package publisher + +import ( + "net/http" + + "gitlab.com/tozd/go/errors" + "go.hacdias.com/indielib/indieauth" + + "go.alanpearce.eu/homestead/internal/config" + ihttp "go.alanpearce.eu/homestead/internal/http" + "go.alanpearce.eu/homestead/internal/server" + "go.alanpearce.eu/homestead/templates" + "go.alanpearce.eu/x/log" +) + +type Options struct { + Development bool `conf:"-"` + BaseURL config.URL + VCSRemoteURL config.URL `conf:"default:https://git.alanpearce.eu/website"` +} + +type App struct { + log *log.Logger + indieauthServer *indieauth.Server + siteSettings templates.SiteSettings + *server.App +} + +func New(opts *Options, log *log.Logger) (*App, errors.E) { + var err error + app := &App{ + log: log, + indieauthServer: indieauth.NewServer(true, &http.Client{}), + siteSettings: templates.SiteSettings{ + Title: "Barkeep", + Language: "en-GB", + Menu: []config.MenuItem{}, + InjectLiveReload: opts.Development, + }, + App: &server.App{ + Domain: opts.BaseURL.Hostname(), + Shutdown: func() {}, + }, + } + + err = indieauth.IsValidProfileURL(opts.BaseURL.String()) + if err != nil { + return nil, errors.WithMessage(err, "invalid base URL") + } + + mux := ihttp.NewServeMux() + mux.HandleFunc("/", app.Index) + mux.HandleFunc("/style.css", app.Style) + + app.Handler = mux + + return app, nil +}
A internal/publisher/mux.go
@@ -0,0 +1,34 @@ +package publisher + +import ( + "net/http" + + ihttp "go.alanpearce.eu/homestead/internal/http" + pubtpl "go.alanpearce.eu/homestead/internal/publisher/templates" + "go.alanpearce.eu/homestead/templates" +) + +func (app *App) Index(w http.ResponseWriter, _ *http.Request) *ihttp.Error { + err := pubtpl.IndexPage(app.siteSettings, templates.PageSettings{ + Title: "Home", + }).Render(w) + + if err != nil { + return &ihttp.Error{ + Code: http.StatusInternalServerError, + Message: "Failed to render index page", + } + } + + return nil +} + +func (app *App) Style(w http.ResponseWriter, _ *http.Request) *ihttp.Error { + w.Header().Set("Content-Type", "text/css") + _, err := w.Write([]byte(templates.CSS)) + if err != nil { + return &ihttp.Error{Code: http.StatusInternalServerError, Message: err.Error()} + } + + return nil +}
A internal/publisher/templates/index.go
@@ -0,0 +1,10 @@ +package templates + +import ( + g "go.alanpearce.eu/gomponents" + base "go.alanpearce.eu/homestead/templates" +) + +func IndexPage(site base.SiteSettings, page base.PageSettings) g.Node { + return Layout(site, page, g.Text("Index Page")) +}
A internal/publisher/templates/layout.go
@@ -0,0 +1,40 @@ +package templates + +import ( + base "go.alanpearce.eu/homestead/templates" + + g "go.alanpearce.eu/gomponents" + c "go.alanpearce.eu/gomponents/components" + . "go.alanpearce.eu/gomponents/html" +) + +func Layout(site base.SiteSettings, page base.PageSettings, children ...g.Node) g.Node { + return c.HTML5(c.HTML5Props{ + Title: site.Title, + Description: "", + Language: site.Language, + Head: []g.Node{ + Link( + Rel("stylesheet"), + Href("/style.css"), + ), + }, + Body: []g.Node{ + Header( + H1(A( + base.ExtendAttrs(base.Attrs{ + "class": "title", + "href": "/", + }, page.TitleAttrs), + g.Text(site.Title), + )), + ), + Main(ID("main"), g.Group(children)), + Footer( + A(Href("https://git.alanpearce.eu/homestead"), g.Text("Source Code")), + ), + g.If(site.InjectLiveReload, base.LiveReload), + }, + HTMLAttrs: []g.Node{}, + }) +}