package microblog import ( "encoding/json" "fmt" "sort" "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" ) var supportedSources = []source{ sourceNostr, // TODO: Add support for BlueSky and Mastodon // SourceBlueSky, // SourceMastodon, } // 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 } // 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"` } // MicroBlog manages blog posts type MicroBlog struct { posts []Post } // NewMicroBlog creates a new microblog instance func NewMicroBlog() *MicroBlog { mb := &MicroBlog{ posts: make([]Post, 0), } // 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 } // GetRecentPosts returns the most recent posts func (mb *MicroBlog) GetRecentPosts(limit int) []Post { // Sort posts by timestamp (newest first) sortedPosts := make([]Post, len(mb.posts)) copy(sortedPosts, mb.posts) sort.Slice(sortedPosts, func(i, j int) bool { return sortedPosts[i].Timestamp.After(sortedPosts[j].Timestamp) }) if limit > 0 && len(sortedPosts) > limit { return sortedPosts[:limit] } return sortedPosts } // 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.serveBlogIndex(w, req) case strings.HasPrefix(path, "/microblog/post/"): postID := strings.TrimPrefix(path, "/microblog/post/") mb.servePost(w, req, postID) default: w.WriteStatusMsg(gemini.StatusNotFound, "Blog page not found") } } // serveBlogIndex serves the main blog page with recent posts func (mb *MicroBlog) serveBlogIndex(w gemini.ResponseWriter, req *gemini.Request) { w.WriteStatusMsg(gemini.StatusSuccess, "text/gemini") var content strings.Builder content.WriteString("# Gemini Microblog\n\n") posts := mb.GetRecentPosts(10) if len(posts) == 0 { content.WriteString("No posts yet. Be the first to write something!\n\n") } else { content.WriteString("## Recent Posts\n\n") for _, post := range posts { content.WriteString(fmt.Sprintf("=> /blog/post/%s %s\n", post.ID, post.Title)) 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("=> /blog/new Write a new post\n") 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())) }