From 7878db5c95e5eb430b7b878fe6eb8084f15441a7 Mon Sep 17 00:00:00 2001 From: Brian Picciano Date: Sat, 21 Jan 2023 16:01:52 +0100 Subject: [PATCH] Initial implementation of post rendering over gmi --- src/cmd/mediocre-blog/main.go | 2 + src/gmi/gmi.go | 55 ++++++++++++-- src/gmi/tpl.go | 134 ++++++++++++++++++++++++++++++++++ src/gmi/tpl/index.gmi | 3 + src/gmi/tpl/posts/index.gmi | 18 +++++ src/gmi/tpl/posts/post.gmi | 9 +++ 6 files changed, 215 insertions(+), 6 deletions(-) create mode 100644 src/gmi/tpl.go create mode 100644 src/gmi/tpl/index.gmi create mode 100644 src/gmi/tpl/posts/index.gmi create mode 100644 src/gmi/tpl/posts/post.gmi diff --git a/src/cmd/mediocre-blog/main.go b/src/cmd/mediocre-blog/main.go index 8c6939f..cdd9b1b 100644 --- a/src/cmd/mediocre-blog/main.go +++ b/src/cmd/mediocre-blog/main.go @@ -124,6 +124,8 @@ func main() { }() gmiParams.Logger = logger.WithNamespace("gmi") + gmiParams.PostStore = postStore + gmiParams.PostAssetStore = postAssetStore logger.Info(ctx, "starting gmi api") gmiAPI, err := gmi.New(gmiParams) diff --git a/src/gmi/gmi.go b/src/gmi/gmi.go index 6e2d79f..7d4d186 100644 --- a/src/gmi/gmi.go +++ b/src/gmi/gmi.go @@ -12,6 +12,7 @@ import ( "git.sr.ht/~adnano/go-gemini" "git.sr.ht/~adnano/go-gemini/certificate" "github.com/mediocregopher/blog.mediocregopher.com/srv/cfg" + "github.com/mediocregopher/blog.mediocregopher.com/srv/post" "github.com/mediocregopher/mediocre-go-lib/v2/mctx" "github.com/mediocregopher/mediocre-go-lib/v2/mlog" ) @@ -19,7 +20,11 @@ import ( // Params are used to instantiate a new API instance. All fields are required // unless otherwise noted. type Params struct { - Logger *mlog.Logger + Logger *mlog.Logger + + PostStore post.Store + PostAssetStore post.AssetStore + PublicURL *url.URL ListenAddr string CertificatesPath string @@ -86,9 +91,14 @@ func New(params Params) (API, error) { params: params, } + handler, err := a.handler() + if err != nil { + return nil, fmt.Errorf("constructing handler: %w", err) + } + a.srv = &gemini.Server{ Addr: params.ListenAddr, - Handler: a.handler(), + Handler: handler, GetCertificate: certStore.Get, } @@ -109,14 +119,47 @@ func (a *api) Shutdown(ctx context.Context) error { return a.srv.Shutdown(ctx) } -func (a *api) handler() gemini.Handler { +func postsMiddleware(tplHandler gemini.Handler) gemini.Handler { + return gemini.HandlerFunc(func( ctx context.Context, rw gemini.ResponseWriter, r *gemini.Request, ) { - fmt.Fprintf(rw, "# Test\n\n") - fmt.Fprintf(rw, "HELLO WORLD\n\n") - fmt.Fprintf(rw, "=> gemini://midnight.pub Hit the pub\n\n") + + id := strings.TrimPrefix(r.URL.Path, "/posts/") + id = strings.TrimSuffix(id, ".gmi") + + if id == "index" { + tplHandler.ServeGemini(ctx, rw, r) + return + } + + query := r.URL.Query() + query.Set("id", id) + r.URL.RawQuery = query.Encode() + + r.URL.Path = "/posts/post.gmi" + + tplHandler.ServeGemini(ctx, rw, r) }) } + +func (a *api) handler() (gemini.Handler, error) { + + tplHandler, err := a.tplHandler() + if err != nil { + return nil, fmt.Errorf("generating tpl handler: %w", err) + } + + mux := new(gemini.Mux) + mux.Handle("/posts/", postsMiddleware(tplHandler)) + mux.Handle("/", tplHandler) + + h := mux + + // TODO logging + // TODO caching + + return h, nil +} diff --git a/src/gmi/tpl.go b/src/gmi/tpl.go new file mode 100644 index 0000000..7aa1a2f --- /dev/null +++ b/src/gmi/tpl.go @@ -0,0 +1,134 @@ +package gmi + +import ( + "bytes" + "context" + "embed" + "fmt" + "io" + "io/fs" + "net/url" + "strconv" + "strings" + "text/template" + + "git.sr.ht/~adnano/go-gemini" + "github.com/mediocregopher/blog.mediocregopher.com/srv/post" + "github.com/mediocregopher/mediocre-go-lib/v2/mctx" +) + +//go:embed tpl +var tplFS embed.FS + +type rendererGetPostsRes struct { + Posts []post.StoredPost + HasMore bool +} + +type renderer struct { + url *url.URL + postStore post.Store +} + +func (r renderer) GetPosts(page, count int) (rendererGetPostsRes, error) { + posts, hasMore, err := r.postStore.Get(page, count) + return rendererGetPostsRes{posts, hasMore}, err +} + +func (r renderer) GetPostByID(id string) (post.StoredPost, error) { + p, err := r.postStore.GetByID(id) + if err != nil { + return post.StoredPost{}, fmt.Errorf("fetching post %q: %w", id, err) + } + return p, nil +} + +func (r renderer) GetQueryValue(key, def string) string { + v := r.url.Query().Get(key) + if v == "" { + v = def + } + return v +} + +func (r renderer) GetQueryIntValue(key string, def int) (int, error) { + vStr := r.GetQueryValue(key, strconv.Itoa(def)) + return strconv.Atoi(vStr) +} + +func (r renderer) Add(a, b int) int { return a + b } + +func (a *api) tplHandler() (gemini.Handler, error) { + + allTpls := template.New("") + + err := fs.WalkDir(tplFS, "tpl", func(path string, d fs.DirEntry, err error) error { + + if err != nil { + return err + } + + if d.IsDir() { + return nil + } + + body, err := fs.ReadFile(tplFS, path) + if err != nil { + panic(err) + } + + name := strings.TrimPrefix(path, "tpl/") + + allTpls, err = allTpls.New(name).Parse(string(body)) + if err != nil { + return fmt.Errorf("parsing %q as template: %w", path, err) + } + + return nil + }) + + if err != nil { + return nil, fmt.Errorf("parsing templates: %w", err) + } + + return gemini.HandlerFunc(func( + ctx context.Context, + rw gemini.ResponseWriter, + r *gemini.Request, + ) { + + if strings.HasSuffix(r.URL.Path, "/") { + r.URL.Path += "index.gmi" + } + + tplPath := strings.TrimPrefix(r.URL.Path, "/") + + ctx = mctx.Annotate(ctx, + "url", r.URL, + "tplPath", tplPath, + ) + + tpl := allTpls.Lookup(tplPath) + + if tpl == nil { + a.params.Logger.WarnString(ctx, "page not found") + rw.WriteHeader(gemini.StatusNotFound, "Page not found, sorry!") + return + } + + buf := new(bytes.Buffer) + + err := tpl.Execute(buf, renderer{ + url: r.URL, + postStore: a.params.PostStore, + }) + + if err != nil { + a.params.Logger.Error(ctx, "rendering error", err) + rw.WriteHeader(gemini.StatusTemporaryFailure, err.Error()) + return + } + + io.Copy(rw, buf) + }), nil +} diff --git a/src/gmi/tpl/index.gmi b/src/gmi/tpl/index.gmi new file mode 100644 index 0000000..7c68b92 --- /dev/null +++ b/src/gmi/tpl/index.gmi @@ -0,0 +1,3 @@ +# Index + +=> /posts/index.gmi See all posts diff --git a/src/gmi/tpl/posts/index.gmi b/src/gmi/tpl/posts/index.gmi new file mode 100644 index 0000000..12eb39c --- /dev/null +++ b/src/gmi/tpl/posts/index.gmi @@ -0,0 +1,18 @@ +# mediocregopher's Posts + +{{ $page := .GetQueryIntValue "page" 0 -}} +{{ $getPostsRes := .GetPosts $page 20 -}} + +{{ if gt $page 0 -}} +=> /posts.gmi?page={{ .Add $page -1 }} Previous Page + +{{ end -}} + +{{ range $getPostsRes.Posts -}} +=> /posts/{{ .ID }}.gmi {{ .PublishedAt.Format "2006-01-02" }} - {{ .Title }} + +{{ end -}} + +{{ if $getPostsRes.HasMore -}} +=> /posts.gmi?page={{ .Add $page 1 }} Next page +{{ end -}} diff --git a/src/gmi/tpl/posts/post.gmi b/src/gmi/tpl/posts/post.gmi new file mode 100644 index 0000000..52ac5ff --- /dev/null +++ b/src/gmi/tpl/posts/post.gmi @@ -0,0 +1,9 @@ +{{ $post := .GetPostByID (.GetQueryValue "id" "") -}} + +# {{ $post.Title }} + +{{ if ne $post.Description "" -}} +> {{ $post.Description }} +{{ end -}} + +{{ $post.Body }}