add page for microblog header content

This commit is contained in:
Travis Shears 2025-09-28 12:42:59 +02:00
parent ef5f5e33ab
commit ee4dfdad18
5 changed files with 110 additions and 57 deletions

4
dev.sh
View file

@ -2,4 +2,6 @@
set -e set -e
go run main.go -cert=./keys/localhost.crt.pem -key=./keys/localhost_key.pem -host=localhost:8080 source .env
fd | entr -r go run main.go -cert=./keys/localhost.crt.pem -key=./keys/localhost_key.pem -host=localhost:8080

View file

@ -5,6 +5,8 @@ import (
"fmt" "fmt"
"gemini_site/internal/pocketbase" "gemini_site/internal/pocketbase"
"log/slog" "log/slog"
"os"
"strings"
"time" "time"
gemini "github.com/kulak/gemini" gemini "github.com/kulak/gemini"
@ -20,14 +22,6 @@ const (
sourceNostr source = "nostr" sourceNostr source = "nostr"
) )
var supportedSources = map[source]bool{
sourceNostr: true,
sourcePleroma: false,
sourceBlueSky: false,
sourceMastodon: false,
sourcePixelfed: false,
}
// Post represents a single blog post // Post represents a single blog post
type post struct { type post struct {
ID string ID string
@ -36,6 +30,7 @@ type post struct {
// TODO: add support for images, must extend the pocketbase query // TODO: add support for images, must extend the pocketbase query
// Images []string // Images []string
Timestamp time.Time Timestamp time.Time
Source source
} }
// PBPost represents a microblog post from PocketBase // PBPost represents a microblog post from PocketBase
@ -108,14 +103,32 @@ func NewMicroBlog(pbClient *pocketbase.PocketBaseClient) *MicroBlog {
// } // }
// GetPost retrieves a post by ID // GetPost retrieves a post by ID
// func (mb *MicroBlog) GetPost(id string) (*Post, bool) { //
// for _, post := range mb.posts { // func (mb *MicroBlog) GetPost(id string) (*Post, bool) {
// if post.ID == id { // for _, post := range mb.posts {
// return &post, true // if post.ID == id {
// } // return &post, true
// } // }
// return nil, false // }
// } // return nil, false
// }
func transformNostrPost(p pbPost) post {
var nostrPost nostrPost
if err := json.Unmarshal(p.FullPost, &nostrPost); err != nil {
slog.Error("Problem unmarshalling nostr post", "error", err)
return post{}
}
content := nostrPost.Content
content = strings.ReplaceAll(content, "#", "\\#")
// content = strings.ReplaceAll(content, "\t", "\\t")
return post{
ID: p.ID,
RemoteID: p.RemoteID,
Content: content,
Timestamp: time.Now(),
Source: source(p.Source),
}
}
// GetRecentPosts returns the most recent posts // GetRecentPosts returns the most recent posts
func (mb *MicroBlog) GetRecentPosts(limit int) ([]post, error) { func (mb *MicroBlog) GetRecentPosts(limit int) ([]post, error) {
@ -133,17 +146,19 @@ func (mb *MicroBlog) GetRecentPosts(limit int) ([]post, error) {
var filteredPosts []post var filteredPosts []post
for _, p := range rawPosts { for _, p := range rawPosts {
if p.Source == SourceNostr { if p.Source == SourceNostr {
var nostrPost nostrPost filteredPosts = append(filteredPosts, transformNostrPost(p))
if err := json.Unmarshal(p.FullPost, &nostrPost); err != nil {
slog.Error("Problem unmarshalling nostr post", "error", err) // var nostrPost nostrPost
continue // if err := json.Unmarshal(p.FullPost, &nostrPost); err != nil {
} // slog.Error("Problem unmarshalling nostr post", "error", err)
filteredPosts = append(filteredPosts, post{ // continue
ID: p.ID, // }
RemoteID: p.RemoteID, // filteredPosts = append(filteredPosts, post{
Content: nostrPost.Content, // ID: p.ID,
Timestamp: time.Now(), // RemoteID: p.RemoteID,
}) // Content: nostrPost.Content,
// Timestamp: time.Now(),
// })
continue continue
} }
} }
@ -152,47 +167,68 @@ func (mb *MicroBlog) GetRecentPosts(limit int) ([]post, error) {
// HandleBlogRequest handles Gemini requests for the microblog // HandleBlogRequest handles Gemini requests for the microblog
func (mb *MicroBlog) HandleBlogRequest(w gemini.ResponseWriter, req *gemini.Request) { func (mb *MicroBlog) HandleBlogRequest(w gemini.ResponseWriter, req *gemini.Request) {
// path := req.URL.Path path := req.URL.Path
// switch { switch {
// case path == "/microblog" || path == "/microblog/": case path == "/microblog" || path == "/microblog/":
// mb.serveBlogIndex(w, req) mb.serveIndex(w, req)
// case strings.HasPrefix(path, "/microblog/post/"): // case strings.HasPrefix(path, "/microblog/post/"):
// postID := strings.TrimPrefix(path, "/microblog/post/") // postID := strings.TrimPrefix(path, "/microblog/post/")
// mb.servePost(w, req, postID) // mb.servePost(w, req, postID)
// default: default:
// w.WriteStatusMsg(gemini.StatusNotFound, "Blog page not found") w.WriteStatusMsg(gemini.StatusNotFound, "Page not found")
// } }
}
func drawPost(builder *strings.Builder, p post) {
builder.WriteString("+------------------------------------------+\n")
builder.WriteString(p.Content)
builder.WriteString("\n")
builder.WriteString(fmt.Sprintf("source: %s, id: %s...\n", p.Source, p.RemoteID[:10]))
builder.WriteString("+------------------------------------------+\n\n\n")
// builder.WriteString(fmt.Sprintf("=> /blog/post/%s %s\n", p.ID, p.Title))
// builder.WriteString(fmt.Sprintf(" By %s on %s\n\n",
// p.Timestamp.Format("2006-01-02 15:04")))
} }
// serveBlogIndex serves the main blog page with recent posts // serveBlogIndex serves the main blog page with recent posts
// func (mb *MicroBlog) serveBlogIndex(w gemini.ResponseWriter, req *gemini.Request) { func (mb *MicroBlog) serveIndex(w gemini.ResponseWriter, req *gemini.Request) {
// w.WriteStatusMsg(gemini.StatusSuccess, "text/gemini") w.WriteStatusMsg(gemini.StatusSuccess, "text/gemini")
// var content strings.Builder var content strings.Builder
// content.WriteString("# Gemini Microblog\n\n") // content.WriteString("# Gemini Microblog\n\n")
// content.WriteString("Here are my microblog posts from various plantforms\n")
// Read and include the contents of ../../pages/microblog.gmi
page, err := os.ReadFile("./pages/microblog.gmi")
if err != nil {
slog.Error("Problem reading microblog page", "error", err)
return
}
content.Write(page)
content.WriteString("\n")
// posts := mb.GetRecentPosts(10) posts, err := mb.GetRecentPosts(20)
if err != nil {
content.WriteString("Error fetching posts: " + err.Error() + "\n\n")
}
// if len(posts) == 0 { if len(posts) == 0 {
// content.WriteString("No posts yet. Be the first to write something!\n\n") content.WriteString("No posts found.\n\n")
// } else { } else {
// content.WriteString("## Recent Posts\n\n") content.WriteString("## Recent Posts\n\n")
// for _, post := range posts { for _, post := range posts {
// content.WriteString(fmt.Sprintf("=> /blog/post/%s %s\n", post.ID, post.Title)) drawPost(&content, post)
// content.WriteString(fmt.Sprintf(" By %s on %s\n\n", }
// post.Author, }
// post.Timestamp.Format("2006-01-02 15:04")))
// }
// }
// content.WriteString("## Actions\n\n") content.WriteString("## Nav\n\n")
// content.WriteString("=> /blog/new Write a new post\n") // content.WriteString("=> /blog/new Write a new post\n")
// content.WriteString("=> / Back to home\n") content.WriteString("=> / Back to home\n")
// w.WriteBody([]byte(content.String())) w.WriteBody([]byte(content.String()))
// } }
// servePost serves a single blog post // servePost serves a single blog post
// func (mb *MicroBlog) servePost(w gemini.ResponseWriter, req *gemini.Request, postID string) { // func (mb *MicroBlog) servePost(w gemini.ResponseWriter, req *gemini.Request, postID string) {

View file

@ -176,6 +176,7 @@ func (c *PocketBaseClient) GetList(
"perPage": {strconv.Itoa(pageSize)}, "perPage": {strconv.Itoa(pageSize)},
"sort": {sort}, "sort": {sort},
"skipTotal": {"true"}, "skipTotal": {"true"},
"filter": {"source = \"nostr\""},
// TODO: add additional fields like image and tag? // TODO: add additional fields like image and tag?
} }
apiURL := fmt.Sprintf("%s/api/collections/%s/records", c.host, collection) apiURL := fmt.Sprintf("%s/api/collections/%s/records", c.host, collection)

View file

@ -11,6 +11,7 @@ import (
"time" "time"
"gemini_site/internal/microblog" "gemini_site/internal/microblog"
"gemini_site/internal/pocketbase"
gemini "github.com/kulak/gemini" gemini "github.com/kulak/gemini"
) )
@ -102,8 +103,9 @@ func main() {
flag.StringVar(&key, "key", "server.key.pem", "private key associated with certificate file") flag.StringVar(&key, "key", "server.key.pem", "private key associated with certificate file")
flag.Parse() flag.Parse()
pbClient := pocketbase.NewPocketBaseClient()
handler := MainHandler{ handler := MainHandler{
blog: microblog.NewHandler(), blog: microblog.NewHandler(pbClient),
} }
err := gemini.ListenAndServe(host, cert, key, gemini.TrapPanic(handler.ServeGemini)) err := gemini.ListenAndServe(host, cert, key, gemini.TrapPanic(handler.ServeGemini))

12
pages/microblog.gmi Normal file
View file

@ -0,0 +1,12 @@
# Microblog
An aggregation of tweet like posts from my various social media platforms.
clearnet version:
=> https://travisshears.com/micro-blog
So for it renders posts from:
* nostr, id: nprofile1qyxhwumn8ghj7mn0wvhxcmmvqqs9udcv9uhqggjz87js9rtaph4lajlxnxsvwvm7zwdjt6etzyk52rgeg4wrz
* mastodon, id: dice.camp/@travisshears
* bluesky, id: @travisshears.bsky.social