init llm generated microblog submodule
This commit is contained in:
parent
1c7341499a
commit
216ace7afa
5 changed files with 241 additions and 1 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -1 +1,2 @@
|
||||||
keys/
|
keys/
|
||||||
|
gemini-server
|
||||||
|
|
|
||||||
15
internal/microblog/handler.go
Normal file
15
internal/microblog/handler.go
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
package microblog
|
||||||
|
|
||||||
|
import (
|
||||||
|
gemini "github.com/kulak/gemini"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Handler interface for microblog functionality
|
||||||
|
type Handler interface {
|
||||||
|
HandleBlogRequest(w gemini.ResponseWriter, req *gemini.Request)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewHandler creates a new microblog handler
|
||||||
|
func NewHandler() Handler {
|
||||||
|
return NewMicroBlog()
|
||||||
|
}
|
||||||
208
internal/microblog/microblog.go
Normal file
208
internal/microblog/microblog.go
Normal file
|
|
@ -0,0 +1,208 @@
|
||||||
|
package microblog
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
gemini "github.com/kulak/gemini"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Post represents a single blog post
|
||||||
|
type Post struct {
|
||||||
|
ID string
|
||||||
|
Title string
|
||||||
|
Content string
|
||||||
|
Author string
|
||||||
|
Timestamp time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 == "/blog" || path == "/blog/":
|
||||||
|
mb.serveBlogIndex(w, req)
|
||||||
|
case strings.HasPrefix(path, "/blog/post/"):
|
||||||
|
postID := strings.TrimPrefix(path, "/blog/post/")
|
||||||
|
mb.servePost(w, req, postID)
|
||||||
|
case path == "/blog/new":
|
||||||
|
mb.serveNewPostForm(w, req)
|
||||||
|
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()))
|
||||||
|
}
|
||||||
|
|
||||||
|
// serveNewPostForm serves the form for creating a new post
|
||||||
|
func (mb *MicroBlog) serveNewPostForm(w gemini.ResponseWriter, req *gemini.Request) {
|
||||||
|
// Check if user is authenticated
|
||||||
|
if req.Certificate() == nil {
|
||||||
|
w.WriteStatusMsg(gemini.StatusCertRequired, "Authentication required to create posts")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteStatusMsg(gemini.StatusSuccess, "text/gemini")
|
||||||
|
|
||||||
|
var content strings.Builder
|
||||||
|
content.WriteString("# Write a New Post\n\n")
|
||||||
|
content.WriteString("To create a new post, use the Titan protocol:\n\n")
|
||||||
|
content.WriteString("titan://your-server/blog/create;mime=text/plain;token=your-auth-token\n\n")
|
||||||
|
content.WriteString("Format your post as:\n")
|
||||||
|
content.WriteString("Title: Your Post Title\n")
|
||||||
|
content.WriteString("Content: Your post content goes here...\n\n")
|
||||||
|
content.WriteString("=> /blog Back to blog\n")
|
||||||
|
|
||||||
|
w.WriteBody([]byte(content.String()))
|
||||||
|
}
|
||||||
|
|
||||||
|
// parsePostData parses the post data from Titan payload
|
||||||
|
func (mb *MicroBlog) parsePostData(data string) (title, content string) {
|
||||||
|
lines := strings.Split(data, "\n")
|
||||||
|
|
||||||
|
for _, line := range lines {
|
||||||
|
line = strings.TrimSpace(line)
|
||||||
|
if strings.HasPrefix(line, "Title:") {
|
||||||
|
title = strings.TrimSpace(strings.TrimPrefix(line, "Title:"))
|
||||||
|
} else if strings.HasPrefix(line, "Content:") {
|
||||||
|
content = strings.TrimSpace(strings.TrimPrefix(line, "Content:"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return title, content
|
||||||
|
}
|
||||||
14
main.go
14
main.go
|
|
@ -10,16 +10,26 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"gemini_site/internal/microblog"
|
||||||
|
|
||||||
gemini "github.com/kulak/gemini"
|
gemini "github.com/kulak/gemini"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ExampleHandler struct {
|
type ExampleHandler struct {
|
||||||
|
blog microblog.Handler
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h ExampleHandler) ServeGemini(w gemini.ResponseWriter, req *gemini.Request) {
|
func (h ExampleHandler) ServeGemini(w gemini.ResponseWriter, req *gemini.Request) {
|
||||||
slog.Info("gemini request",
|
slog.Info("gemini request",
|
||||||
"path", req.URL.Path,
|
"path", req.URL.Path,
|
||||||
"user", strings.Join(userName(req), " "))
|
"user", strings.Join(userName(req), " "))
|
||||||
|
|
||||||
|
// Check if this is a blog request
|
||||||
|
if strings.HasPrefix(req.URL.Path, "/blog") {
|
||||||
|
h.blog.HandleBlogRequest(w, req)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
switch req.URL.Path {
|
switch req.URL.Path {
|
||||||
case "/":
|
case "/":
|
||||||
gemini.ServeFileName("pages/home.gmi", "text/gemini")(w, req)
|
gemini.ServeFileName("pages/home.gmi", "text/gemini")(w, req)
|
||||||
|
|
@ -92,7 +102,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()
|
||||||
|
|
||||||
handler := ExampleHandler{}
|
handler := ExampleHandler{
|
||||||
|
blog: microblog.NewHandler(),
|
||||||
|
}
|
||||||
|
|
||||||
err := gemini.ListenAndServe(host, cert, key, gemini.TrapPanic(handler.ServeGemini))
|
err := gemini.ListenAndServe(host, cert, key, gemini.TrapPanic(handler.ServeGemini))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,10 @@ My personal website:
|
||||||
So far I've joined the following communities here in gemspace:
|
So far I've joined the following communities here in gemspace:
|
||||||
=> gemini://station.martinrue.com/travisshears
|
=> gemini://station.martinrue.com/travisshears
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
=> /blog Microblog - Read and write posts
|
||||||
|
|
||||||
## Site updates
|
## Site updates
|
||||||
|
|
||||||
== 26.09.2025 ==
|
== 26.09.2025 ==
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue