diff --git a/bskyweb/cmd/bskyweb/formating.go b/bskyweb/cmd/bskyweb/formating.go new file mode 100644 index 00000000..023ba3f5 --- /dev/null +++ b/bskyweb/cmd/bskyweb/formating.go @@ -0,0 +1,57 @@ +package main + +import ( + "fmt" + "slices" + "strings" + + appbsky "github.com/bluesky-social/indigo/api/bsky" +) + +// Function to expand shortened links in rich text back to full urls, replacing shortened urls in social card meta tags and the noscript output. +// +// This essentially reverses the effect of the typescript function `shortenLinks()` in `src/lib/strings/rich-text-manip.ts` +func ExpandPostText(post *appbsky.FeedPost) string { + postText := post.Text + var charsAdded int = 0 + // iterate over facets, check if they're link facets, and if found, grab the uri + for _, facet := range post.Facets { + linkUri := "" + if slices.ContainsFunc(facet.Features, func(feat *appbsky.RichtextFacet_Features_Elem) bool { + if feat.RichtextFacet_Link == nil || feat.RichtextFacet_Link.LexiconTypeID != "app.bsky.richtext.facet#link" { + return false + } + + // bail out if bounds checks fail + if int(facet.Index.ByteStart)+charsAdded > len(postText) || int(facet.Index.ByteEnd)+charsAdded > len(postText) { + return false + } + linkText := postText[int(facet.Index.ByteStart)+charsAdded : int(facet.Index.ByteEnd)+charsAdded] + linkUri = feat.RichtextFacet_Link.Uri + + // only expand uris that have been shortened (as opposed to those with non-uri anchor text) + if strings.HasSuffix(linkText, "...") && strings.Contains(linkUri, linkText[0:len(linkText)-3]) { + return true + } + return false + }) { + // replace the shortened uri with the full length one from the facet using utf8 byte offsets + // NOTE: we already did bounds check above + postText = postText[0:int(facet.Index.ByteStart)+charsAdded] + linkUri + postText[int(facet.Index.ByteEnd)+charsAdded:] + charsAdded += len(linkUri) - int(facet.Index.ByteEnd-facet.Index.ByteStart) + } + } + // if the post has an embeded link and its url doesn't already appear in postText, append it to + // the end to avoid social cards with missing links + if post.Embed != nil && post.Embed.EmbedExternal != nil && post.Embed.EmbedExternal.External != nil { + externalURI := post.Embed.EmbedExternal.External.Uri + if !strings.Contains(postText, externalURI) { + postText = fmt.Sprintf("%s\n%s", postText, externalURI) + } + } + // TODO: could embed the actual post text? + if post.Embed != nil && (post.Embed.EmbedRecord != nil || post.Embed.EmbedRecordWithMedia != nil) { + postText = fmt.Sprintf("%s\n\n[contains quote post or other embeded content]", postText) + } + return postText +} diff --git a/bskyweb/cmd/bskyweb/formatting_test.go b/bskyweb/cmd/bskyweb/formatting_test.go new file mode 100644 index 00000000..1fbf8d5e --- /dev/null +++ b/bskyweb/cmd/bskyweb/formatting_test.go @@ -0,0 +1,39 @@ +package main + +import ( + "encoding/json" + "io" + "os" + "strings" + "testing" + + appbsky "github.com/bluesky-social/indigo/api/bsky" +) + +func loadPost(t *testing.T, p string) appbsky.FeedPost { + + f, err := os.Open(p) + if err != nil { + t.Fatal(err) + } + defer func() { _ = f.Close() }() + + postBytes, err := io.ReadAll(f) + if err != nil { + t.Fatal(err) + } + var post appbsky.FeedPost + if err := json.Unmarshal(postBytes, &post); err != nil { + t.Fatal(err) + } + return post +} + +func TestExpandPostText(t *testing.T) { + post := loadPost(t, "testdata/atproto_embed_post.json") + + text := ExpandPostText(&post) + if !strings.Contains(text, "https://github.com/snarfed/bridgy-fed") { + t.Fail() + } +} diff --git a/bskyweb/cmd/bskyweb/rss.go b/bskyweb/cmd/bskyweb/rss.go index b756b90e..76689abb 100644 --- a/bskyweb/cmd/bskyweb/rss.go +++ b/bskyweb/cmd/bskyweb/rss.go @@ -96,7 +96,10 @@ func (srv *Server) WebProfileRSS(c echo.Context) error { if err != nil { return err } - rec := p.Post.Record.Val.(*appbsky.FeedPost) + rec, ok := p.Post.Record.Val.(*appbsky.FeedPost) + if !ok { + continue + } // only top-level posts in RSS (no replies) if rec.Reply != nil { continue @@ -108,7 +111,7 @@ func (srv *Server) WebProfileRSS(c echo.Context) error { } posts = append(posts, Item{ Link: fmt.Sprintf("https://%s/profile/%s/post/%s", req.Host, pv.Handle, aturi.RecordKey().String()), - Description: rec.Text, + Description: ExpandPostText(rec), PubDate: pubDate, GUID: ItemGUID{ Value: aturi.String(), diff --git a/bskyweb/cmd/bskyweb/server.go b/bskyweb/cmd/bskyweb/server.go index 94bba231..8e7d618c 100644 --- a/bskyweb/cmd/bskyweb/server.go +++ b/bskyweb/cmd/bskyweb/server.go @@ -336,13 +336,29 @@ func (srv *Server) WebPost(c echo.Context) error { postView := tpv.Thread.FeedDefs_ThreadViewPost.Post data["postView"] = postView data["requestURI"] = fmt.Sprintf("https://%s%s", req.Host, req.URL.Path) - if postView.Embed != nil && postView.Embed.EmbedImages_View != nil { - var thumbUrls []string - for i := range postView.Embed.EmbedImages_View.Images { - thumbUrls = append(thumbUrls, postView.Embed.EmbedImages_View.Images[i].Thumb) + if postView.Embed != nil { + if postView.Embed.EmbedImages_View != nil { + var thumbUrls []string + for i := range postView.Embed.EmbedImages_View.Images { + thumbUrls = append(thumbUrls, postView.Embed.EmbedImages_View.Images[i].Thumb) + } + data["imgThumbUrls"] = thumbUrls + } else if postView.Embed.EmbedRecordWithMedia_View != nil && postView.Embed.EmbedRecordWithMedia_View.Media != nil && postView.Embed.EmbedRecordWithMedia_View.Media.EmbedImages_View != nil { + var thumbUrls []string + for i := range postView.Embed.EmbedRecordWithMedia_View.Media.EmbedImages_View.Images { + thumbUrls = append(thumbUrls, postView.Embed.EmbedRecordWithMedia_View.Media.EmbedImages_View.Images[i].Thumb) + } + data["imgThumbUrls"] = thumbUrls } - data["imgThumbUrls"] = thumbUrls } + + if postView.Record != nil { + postRecord, ok := postView.Record.Val.(*appbsky.FeedPost) + if ok { + data["postText"] = ExpandPostText(postRecord) + } + } + return c.Render(http.StatusOK, "post.html", data) } diff --git a/bskyweb/cmd/bskyweb/testdata/atproto_embed_post.json b/bskyweb/cmd/bskyweb/testdata/atproto_embed_post.json new file mode 100644 index 00000000..2e54854e --- /dev/null +++ b/bskyweb/cmd/bskyweb/testdata/atproto_embed_post.json @@ -0,0 +1,60 @@ +{ + "$type": "app.bsky.feed.post", + "createdAt": "2023-12-04T19:30:03.024Z", + "embed": { + "$type": "app.bsky.embed.external", + "external": { + "description": "🕸 Bridges the IndieWeb to Mastodon and the fediverse via ActivityPub. - GitHub - snarfed/bridgy-fed: 🕸 Bridges the IndieWeb to Mastodon and the fediverse via ActivityPub.", + "thumb": { + "$type": "blob", + "ref": { + "$link": "bafkreidplhjcnrl2c74r3xs7nh7k7q3ny6ul7cgxr2fophblvdeky6t64e" + }, + "mimeType": "image/jpeg", + "size": 347998 + }, + "title": "GitHub - snarfed/bridgy-fed: 🕸 Bridges the IndieWeb to Mastodon and the fediverse via ActivityPub...", + "uri": "https://github.com/snarfed/bridgy-fed" + } + }, + "facets": [ + { + "features": [ + { + "$type": "app.bsky.richtext.facet#link", + "uri": "https://github.com/snarfed/bridgy-fed" + } + ], + "index": { + "byteEnd": 92, + "byteStart": 66 + } + }, + { + "features": [ + { + "$type": "app.bsky.richtext.facet#mention", + "did": "did:plc:fdme4gb7mu7zrie7peay7tst" + } + ], + "index": { + "byteEnd": 149, + "byteStart": 137 + } + } + ], + "langs": [ + "en" + ], + "reply": { + "parent": { + "cid": "bafyreifaidyl62p4snkdwsygviemsxyidi3cd7dxvjomh5644sovxhsppa", + "uri": "at://did:plc:ewvi7nxzyoun6zhxrhs64oiz/app.bsky.feed.post/3kfqklhpalh2c" + }, + "root": { + "cid": "bafyreibiimdwmsp5mqpm7utqcdmvo6fdqmofblp5obs3h7ub6652zyooci", + "uri": "at://did:plc:ewvi7nxzyoun6zhxrhs64oiz/app.bsky.feed.post/3kfqkkjdkic2e" + } + }, + "text": "Bridgy Fed is an open-source project — check out the code here: github.com/snarfed/brid...\n\nStay updated with the project by following @snarfed.org!" +} diff --git a/bskyweb/templates/post.html b/bskyweb/templates/post.html index 307f80bb..b6688e35 100644 --- a/bskyweb/templates/post.html +++ b/bskyweb/templates/post.html @@ -21,9 +21,9 @@ {% else %} {% endif -%} - {%- if postView.Record.Val.Text %} - - + {%- if postText %} + + {% endif -%} {%- if imgThumbUrls %} {% for imgThumbUrl in imgThumbUrls %} @@ -47,7 +47,7 @@

{{ postView.Author.DisplayName }}

{{ postView.Author.Handle }}

{{ postView.Author.Did }}

-

{{ postView.Record.Val.Text }}

+

{{ postText }}

{{ postView.IndexedAt }}

{% endif -%}