From eca8acddff4e07e7db0f020855601455ed102ff1 Mon Sep 17 00:00:00 2001 From: Travis Shears Date: Thu, 16 Oct 2025 11:35:33 +0200 Subject: [PATCH] add mastodon and bluesky to microblog ") ") {3,}`) ") %s ", ", p.Source, p.ID)) posted: %s ", p.Timestamp.Format("2006-01-02 15:04"))) ") --- internal/microblog/microblog.gmi | 9 +- internal/microblog/microblog.go | 150 ++++++++++++++++++------------- internal/pocketbase/pb.go | 2 +- 3 files changed, 91 insertions(+), 70 deletions(-) diff --git a/internal/microblog/microblog.gmi b/internal/microblog/microblog.gmi index 691f0d3..33b47e2 100644 --- a/internal/microblog/microblog.gmi +++ b/internal/microblog/microblog.gmi @@ -6,9 +6,8 @@ clearnet version: => https://travisshears.com/micro-blog -So for it renders posts from: -* nostr, id: nprofile1qyxhwumn8ghj7mn0wvhxcmmvqqs9udcv9uhqggjz87js9rtaph4lajlxnxsvwvm7zwdjt6etzyk52rgeg4wrz +Here post from nostr, mastodon, and bluesky are rendered. -I plan to add more platforms in the future: -* mastodon, id: dice.camp/@travisshears -* bluesky, id: @travisshears.bsky.social +My nostr id is: nprofile1qyxhwumn8ghj7mn0wvhxcmmvqqs9udcv9uhqggjz87js9rtaph4lajlxnxsvwvm7zwdjt6etzyk52rgeg4wrz I run my own relay wss://nostr.travisshears.com but you can also find me on the most popular relays. +=> https://bsky.app/profile/travisshears.bsky.social @travisshears on Bluesky +=> https://dice.camp/@travisshears @travisshears on Mastodon via dice.camp diff --git a/internal/microblog/microblog.go b/internal/microblog/microblog.go index 02a8147..b8e442e 100644 --- a/internal/microblog/microblog.go +++ b/internal/microblog/microblog.go @@ -61,59 +61,34 @@ func NewMicroBlog(pbClient *pocketbase.PocketBaseClient) *MicroBlog { return mb } -// // Add some sample posts -// mb.addSamplePosts() +func escapeHashTags(content string) string { + // TODO: maybe prettier way to do this with a code + return strings.ReplaceAll(content, "#", "\\#") +} -// return mb -// } +func removeHTMLTags(content string) string { + // Strip simple HTML tags. Paragraphs become newlines. + // Replace opening

tags with nothing, closing

with newline. + rePStart := regexp.MustCompile(`(?i)]*>`) + rePEnd := regexp.MustCompile(`(?i)

`) + content = rePStart.ReplaceAllString(content, "") + content = rePEnd.ReplaceAllString(content, "\n") -// 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), -// }, -// } + // Convert
to newline + reBr := regexp.MustCompile(`(?i)`) + content = reBr.ReplaceAllString(content, "\n") -// mb.posts = append(mb.posts, samplePosts...) -// } + // Remove any remaining tags + reTags := regexp.MustCompile(`(?i)<[^>]+>`) + content = reTags.ReplaceAllString(content, "") -// 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(), -// } + // Normalize whitespace: trim and collapse excessive blank lines + content = strings.TrimSpace(content) + reMultiNewlines := regexp.MustCompile(`\n{3,}`) + return reMultiNewlines.ReplaceAllString(content, "\n\n") -// 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 { @@ -121,8 +96,63 @@ func transformNostrPost(p pbPost) post { return post{} } content := nostrPost.Content - content = strings.ReplaceAll(content, "#", "\\#") - // content = strings.ReplaceAll(content, "\t", "\\t") + content = escapeHashTags(content) + return post{ + ID: p.ID, + RemoteID: p.RemoteID, + Content: content, + Timestamp: time.Now(), + Source: source(p.Source), + } +} + +func transformBlueSkyPost(p pbPost) post { + var fullPost struct { + Record struct { + Text string `json:"text"` + } `json:"record"` + } + if err := json.Unmarshal(p.FullPost, &fullPost); err != nil { + slog.Error("Problem unmarshalling bluesky post", "error", err) + return post{} + } + content := fullPost.Record.Text + content = escapeHashTags(content) + return post{ + ID: p.ID, + RemoteID: p.RemoteID, + Content: content, + Timestamp: time.Now(), + Source: source(p.Source), + } +} + +func transformMastodonPost(p pbPost) post { + var fullPost struct { + Content string `json:"content"` + Reblog *struct { + Content string `json:"content"` + Account *struct { + Acct string `json:"acct"` + } `json:"account"` + } `json:"reblog"` + } + var content string + if err := json.Unmarshal(p.FullPost, &fullPost); err != nil { + slog.Error("Problem unmarshalling mastodon post", "error", err) + return post{} + } + if fullPost.Reblog != nil { + content = fmt.Sprintf( + "reblogged post from %s:\n%s\n", + fullPost.Reblog.Account.Acct, + fullPost.Reblog.Content, + ) + } else { + content = fullPost.Content + } + content = escapeHashTags(content) + content = removeHTMLTags(content) return post{ ID: p.ID, RemoteID: p.RemoteID, @@ -147,21 +177,13 @@ func (mb *MicroBlog) GetRecentPosts(limit int, page int) ([]post, error) { var filteredPosts []post for _, p := range rawPosts { - if p.Source == SourceNostr { + switch p.Source { + case 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 + case SourceMastodon: + filteredPosts = append(filteredPosts, transformMastodonPost(p)) + case SourceBlueSky: + filteredPosts = append(filteredPosts, transformBlueSkyPost(p)) } } return filteredPosts, nil @@ -208,7 +230,7 @@ func (mb *MicroBlog) HandleBlogRequest(w gemini.ResponseWriter, req *gemini.Requ } func drawPost(builder *strings.Builder, p post) { - builder.WriteString(fmt.Sprintf("=> / 🖋️ nostr post: %s \n", p.ID)) + builder.WriteString(fmt.Sprintf("=> / 🖋️ %s post: %s \n", p.Source, p.ID)) builder.WriteString(formatContent(p.Content)) builder.WriteString(fmt.Sprintf("\nposted: %s\n", p.Timestamp.Format("2006-01-02 15:04"))) builder.WriteString("\n\n") diff --git a/internal/pocketbase/pb.go b/internal/pocketbase/pb.go index e7bfdf8..2967112 100644 --- a/internal/pocketbase/pb.go +++ b/internal/pocketbase/pb.go @@ -176,7 +176,7 @@ func (c *PocketBaseClient) GetList( "perPage": {strconv.Itoa(pageSize)}, "sort": {sort}, "skipTotal": {"true"}, - "filter": {"source = \"nostr\""}, + "filter": {"source = \"nostr\" || source = \"mastodon\" || source = \"blue_sky\""}, // TODO: add additional fields like image and tag? } apiURL := fmt.Sprintf("%s/api/collections/%s/records", c.host, collection)