init repo from gemini capsule project
This commit is contained in:
commit
cc78733a45
10 changed files with 901 additions and 0 deletions
283
internal/microblog/microblog.go
Normal file
283
internal/microblog/microblog.go
Normal file
|
|
@ -0,0 +1,283 @@
|
|||
package microblog
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"gemini_site/internal/pocketbase"
|
||||
"log/slog"
|
||||
"os"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
gemini "github.com/kulak/gemini"
|
||||
)
|
||||
|
||||
type source string
|
||||
|
||||
const (
|
||||
sourcePleroma source = "pleroma"
|
||||
sourceBlueSky source = "blue_sky"
|
||||
sourceMastodon source = "mastodon"
|
||||
sourcePixelfed source = "pixelfed"
|
||||
sourceNostr source = "nostr"
|
||||
)
|
||||
|
||||
// Post represents a single blog post
|
||||
type post struct {
|
||||
ID string
|
||||
RemoteID string
|
||||
Content string
|
||||
// TODO: add support for images, must extend the pocketbase query
|
||||
// Images []string
|
||||
Timestamp time.Time
|
||||
Source source
|
||||
}
|
||||
|
||||
// PBPost represents a microblog post from PocketBase
|
||||
type pbPost struct {
|
||||
ID string `json:"id"`
|
||||
RemoteID string `json:"remoteId"`
|
||||
Source Source `json:"source"`
|
||||
FullPost json.RawMessage `json:"fullPost"`
|
||||
Posted string `json:"posted"`
|
||||
}
|
||||
|
||||
type nostrPost struct {
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
// MicroBlog manages blog posts
|
||||
type MicroBlog struct {
|
||||
pbClient *pocketbase.PocketBaseClient
|
||||
}
|
||||
|
||||
// NewMicroBlog creates a new microblog instance
|
||||
func NewMicroBlog(pbClient *pocketbase.PocketBaseClient) *MicroBlog {
|
||||
mb := &MicroBlog{
|
||||
pbClient: pbClient,
|
||||
}
|
||||
return mb
|
||||
}
|
||||
|
||||
// // Add some sample posts
|
||||
// mb.addSamplePosts()
|
||||
|
||||
// return mb
|
||||
// }
|
||||
|
||||
// addSamplePosts adds some initial content
|
||||
// func (mb *MicroBlog) addSamplePosts() {
|
||||
// samplePosts := []Post{
|
||||
// {
|
||||
// ID: "1",
|
||||
// Title: "Welcome to the Gemini Microblog",
|
||||
// Content: "This is the first post on our Gemini-powered microblog! It's simple, fast, and distraction-free.",
|
||||
// Author: "Admin",
|
||||
// Timestamp: time.Now().Add(-2 * time.Hour),
|
||||
// },
|
||||
// {
|
||||
// ID: "2",
|
||||
// Title: "The Beauty of Simplicity",
|
||||
// Content: "Gemini protocol encourages us to focus on content over presentation. This microblog embodies that philosophy.",
|
||||
// Author: "Admin",
|
||||
// Timestamp: time.Now().Add(-1 * time.Hour),
|
||||
// },
|
||||
// }
|
||||
|
||||
// mb.posts = append(mb.posts, samplePosts...)
|
||||
// }
|
||||
|
||||
// AddPost adds a new post to the blog
|
||||
// func (mb *MicroBlog) AddPost(title, content, author string) string {
|
||||
// id := fmt.Sprintf("%d", time.Now().Unix())
|
||||
// post := Post{
|
||||
// ID: id,
|
||||
// Title: title,
|
||||
// Content: content,
|
||||
// Author: author,
|
||||
// Timestamp: time.Now(),
|
||||
// }
|
||||
|
||||
// mb.posts = append(mb.posts, post)
|
||||
// return id
|
||||
// }
|
||||
|
||||
// GetPost retrieves a post by ID
|
||||
//
|
||||
// func (mb *MicroBlog) GetPost(id string) (*Post, bool) {
|
||||
// for _, post := range mb.posts {
|
||||
// if post.ID == id {
|
||||
// return &post, true
|
||||
// }
|
||||
// }
|
||||
// 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
|
||||
func (mb *MicroBlog) GetRecentPosts(limit int, page int) ([]post, error) {
|
||||
res, err := mb.pbClient.GetList("micro_blog_posts", page, limit, "-posted")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to fetch posts from pocketbase: %w", err)
|
||||
}
|
||||
|
||||
var rawPosts []pbPost
|
||||
if err := json.Unmarshal(res.Items, &rawPosts); err != nil {
|
||||
return nil, fmt.Errorf("failed to decode response: %w", err)
|
||||
}
|
||||
slog.Info("Posts from pocketbase", "rawPosts", rawPosts)
|
||||
|
||||
var filteredPosts []post
|
||||
for _, p := range rawPosts {
|
||||
if p.Source == SourceNostr {
|
||||
filteredPosts = append(filteredPosts, transformNostrPost(p))
|
||||
|
||||
// var nostrPost nostrPost
|
||||
// if err := json.Unmarshal(p.FullPost, &nostrPost); err != nil {
|
||||
// slog.Error("Problem unmarshalling nostr post", "error", err)
|
||||
// continue
|
||||
// }
|
||||
// filteredPosts = append(filteredPosts, post{
|
||||
// ID: p.ID,
|
||||
// RemoteID: p.RemoteID,
|
||||
// Content: nostrPost.Content,
|
||||
// Timestamp: time.Now(),
|
||||
// })
|
||||
continue
|
||||
}
|
||||
}
|
||||
return filteredPosts, nil
|
||||
}
|
||||
|
||||
func replaceLinks(content string) string {
|
||||
// Regex: ^(https?://\S+)
|
||||
// ^ : start of line
|
||||
// https? : http or https
|
||||
// :// : literal
|
||||
// \S+ : one or more non-whitespace characters
|
||||
re := regexp.MustCompile(`(?m)^(https?://\S+)`)
|
||||
// Replace each match with "=> <url>"
|
||||
return re.ReplaceAllStringFunc(content, func(match string) string {
|
||||
return "=> " + match
|
||||
})
|
||||
}
|
||||
|
||||
// HandleBlogRequest handles Gemini requests for the microblog
|
||||
func (mb *MicroBlog) HandleBlogRequest(w gemini.ResponseWriter, req *gemini.Request) {
|
||||
path := req.URL.Path
|
||||
|
||||
switch {
|
||||
case path == "/microblog" || path == "/microblog/":
|
||||
mb.serveIndex(w, req, 1)
|
||||
case strings.HasPrefix(path, "/microblog/page/"):
|
||||
pageNum := strings.TrimPrefix(path, "/microblog/page/")
|
||||
num, err := strconv.Atoi(pageNum)
|
||||
if err != nil {
|
||||
w.WriteStatusMsg(gemini.StatusBadRequest, "Invalid page number")
|
||||
return
|
||||
}
|
||||
mb.serveIndex(w, req, num)
|
||||
// case strings.HasPrefix(path, "/microblog/post/"):
|
||||
// postID := strings.TrimPrefix(path, "/microblog/post/")
|
||||
// mb.servePost(w, req, postID)
|
||||
default:
|
||||
w.WriteStatusMsg(gemini.StatusNotFound, "Page not found")
|
||||
}
|
||||
}
|
||||
|
||||
func drawPost(builder *strings.Builder, p post) {
|
||||
builder.WriteString("+------------------------------------------+\n")
|
||||
content := replaceLinks(p.Content)
|
||||
builder.WriteString(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
|
||||
func (mb *MicroBlog) serveIndex(w gemini.ResponseWriter, req *gemini.Request, pageNum int) {
|
||||
w.WriteStatusMsg(gemini.StatusSuccess, "text/gemini")
|
||||
|
||||
var content strings.Builder
|
||||
// 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
|
||||
if pageNum == 1 {
|
||||
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, err := mb.GetRecentPosts(20, pageNum)
|
||||
if err != nil {
|
||||
content.WriteString("Error fetching posts: " + err.Error() + "\n\n")
|
||||
}
|
||||
|
||||
if len(posts) == 0 {
|
||||
content.WriteString("No posts found.\n\n")
|
||||
} else {
|
||||
content.WriteString("## Posts\n\n")
|
||||
|
||||
for _, post := range posts {
|
||||
drawPost(&content, post)
|
||||
}
|
||||
}
|
||||
|
||||
content.WriteString("## Nav\n\n")
|
||||
// content.WriteString("=> /blog/new Write a new post\n")
|
||||
|
||||
content.WriteString(fmt.Sprintf("=> /microblog/page/%d Next page\n", pageNum+1))
|
||||
content.WriteString("=> / Back to home\n")
|
||||
|
||||
w.WriteBody([]byte(content.String()))
|
||||
}
|
||||
|
||||
// servePost serves a single blog post
|
||||
// func (mb *MicroBlog) servePost(w gemini.ResponseWriter, req *gemini.Request, postID string) {
|
||||
// post, found := mb.GetPost(postID)
|
||||
// if !found {
|
||||
// w.WriteStatusMsg(gemini.StatusNotFound, "Post not found")
|
||||
// return
|
||||
// }
|
||||
|
||||
// w.WriteStatusMsg(gemini.StatusSuccess, "text/gemini")
|
||||
|
||||
// var content strings.Builder
|
||||
// content.WriteString(fmt.Sprintf("# %s\n\n", post.Title))
|
||||
// content.WriteString(fmt.Sprintf("By %s on %s\n\n",
|
||||
// post.Author,
|
||||
// post.Timestamp.Format("2006-01-02 15:04")))
|
||||
// content.WriteString("---\n\n")
|
||||
// content.WriteString(post.Content)
|
||||
// content.WriteString("\n\n---\n\n")
|
||||
// content.WriteString("=> /blog Back to blog\n")
|
||||
// content.WriteString("=> / Back to home\n")
|
||||
|
||||
// w.WriteBody([]byte(content.String()))
|
||||
// }
|
||||
Loading…
Add table
Add a link
Reference in a new issue