about summary refs log tree commit diff
path: root/bskyweb
diff options
context:
space:
mode:
Diffstat (limited to 'bskyweb')
-rw-r--r--bskyweb/cmd/bskyweb/formating.go57
-rw-r--r--bskyweb/cmd/bskyweb/formatting_test.go39
-rw-r--r--bskyweb/cmd/bskyweb/rss.go9
-rw-r--r--bskyweb/cmd/bskyweb/server.go26
-rw-r--r--bskyweb/cmd/bskyweb/testdata/atproto_embed_post.json60
-rw-r--r--bskyweb/templates/base.html33
-rw-r--r--bskyweb/templates/post.html8
7 files changed, 213 insertions, 19 deletions
diff --git a/bskyweb/cmd/bskyweb/formating.go b/bskyweb/cmd/bskyweb/formating.go
new file mode 100644
index 000000000..023ba3f51
--- /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 000000000..1fbf8d5ee
--- /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 64e67fd22..76689abb5 100644
--- a/bskyweb/cmd/bskyweb/rss.go
+++ b/bskyweb/cmd/bskyweb/rss.go
@@ -80,7 +80,7 @@ func (srv *Server) WebProfileRSS(c echo.Context) error {
 		}
 	}
 
-	af, err := appbsky.FeedGetAuthorFeed(ctx, srv.xrpcc, did.String(), "", "", 30)
+	af, err := appbsky.FeedGetAuthorFeed(ctx, srv.xrpcc, did.String(), "", "posts_no_replies", 30)
 	if err != nil {
 		log.Warn("failed to fetch author feed", "did", did, "err", err)
 		return err
@@ -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 94bba231a..8e7d618c2 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
+		}
+	}
+
+	if postView.Record != nil {
+		postRecord, ok := postView.Record.Val.(*appbsky.FeedPost)
+		if ok {
+			data["postText"] = ExpandPostText(postRecord)
 		}
-		data["imgThumbUrls"] = thumbUrls
 	}
+
 	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 000000000..2e54854ee
--- /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/base.html b/bskyweb/templates/base.html
index 57ad064f8..228c3d894 100644
--- a/bskyweb/templates/base.html
+++ b/bskyweb/templates/base.html
@@ -33,36 +33,55 @@
     }
 
     html {
-      scroll-behavior: smooth;
       /* Prevent text size change on orientation change https://gist.github.com/tfausak/2222823#file-ios-8-web-app-html-L138 */
       -webkit-text-size-adjust: 100%;
       height: calc(100% + env(safe-area-inset-top));
+      scrollbar-gutter: stable both-edges;
+    }
+
+    /* Remove autofill styles on Webkit */
+    input:-webkit-autofill,
+    input:-webkit-autofill:hover, 
+    input:-webkit-autofill:focus,
+    textarea:-webkit-autofill,
+    textarea:-webkit-autofill:hover,
+    textarea:-webkit-autofill:focus,
+    select:-webkit-autofill,
+    select:-webkit-autofill:hover,
+    select:-webkit-autofill:focus {
+      border: 0;
+      -webkit-text-fill-color: transparent;
+      -webkit-box-shadow: none;
+    }
+    /* Force left-align date/time inputs on iOS mobile */
+    input::-webkit-date-and-time-value {
+      text-align: left;
     }
 
     /* Color theming */
     :root {
       --text: black;
       --background: white;
-      --backgroundLight: #F3F3F8;
+      --backgroundLight: hsl(211, 20%, 95%);
     }
     html.colorMode--dark {
       --text: white;
-      --background: black;
-      --backgroundLight: #26272D;
+      --background: hsl(211, 20%, 4%);
+      --backgroundLight: hsl(211, 20%, 20%);
       color-scheme: dark;
     }
     @media (prefers-color-scheme: light) {
       html.colorMode--system {
         --text: black;
         --background: white;
-        --backgroundLight: #F3F3F8;
+        --backgroundLight: hsl(211, 20%, 95%);
       }
     }
     @media (prefers-color-scheme: dark) {
       html.colorMode--system {
         --text: white;
-        --background: black;
-        --backgroundLight: #26272D;
+        --background: hsl(211, 20%, 4%);
+        --backgroundLight: hsl(211, 20%, 20%);
         color-scheme: dark;
       }
     }
diff --git a/bskyweb/templates/post.html b/bskyweb/templates/post.html
index 307f80bbb..b6688e35b 100644
--- a/bskyweb/templates/post.html
+++ b/bskyweb/templates/post.html
@@ -21,9 +21,9 @@
   {% else %}
   <meta property="og:title" content="@{{ postView.Author.Handle }}">
   {% endif -%}
-  {%- if postView.Record.Val.Text %}
-  <meta name="description" content="{{ postView.Record.Val.Text }}">
-  <meta property="og:description" content="{{ postView.Record.Val.Text }}">
+  {%- if postText %}
+  <meta name="description" content="{{ postText }}">
+  <meta property="og:description" content="{{ postText }}">
   {% endif -%}
   {%- if imgThumbUrls %}
   {% for imgThumbUrl in imgThumbUrls %}
@@ -47,7 +47,7 @@
   <p id="bsky_display_name">{{ postView.Author.DisplayName }}</p>
   <p id="bsky_handle">{{ postView.Author.Handle }}</p>
   <p id="bsky_did">{{ postView.Author.Did }}</p>
-  <p id="bsky_post_text">{{ postView.Record.Val.Text }}</p>
+  <p id="bsky_post_text">{{ postText }}</p>
   <p id="bsky_post_indexedat">{{ postView.IndexedAt }}</p>
 </div>
 {% endif -%}