more reusable funcs

This commit is contained in:
Michael Black
2026-05-26 11:34:00 -05:00
parent c51164786b
commit 6abbf3cd03
6 changed files with 154 additions and 48 deletions
+15 -12
View File
@@ -98,21 +98,24 @@ const maxRecentPosts = 10
const authorFeedFetchLimit = 3 * maxRecentPosts
// bskyPostURL returns the canonical handle-form URL for a post, given the
// post's author handle and at-uri. Returns "" if the URI cannot be parsed or
// the handle is unusable (handle.invalid).
func bskyPostURL(handle, atURI string) string {
if handle == "" || handle == "handle.invalid" {
// post's author handle and record key. Returns "" if the handle is unusable
// (empty or handle.invalid) or rkey is empty.
func bskyPostURL(handle, rkey string) string {
if handle == "" || handle == "handle.invalid" || rkey == "" {
return ""
}
return fmt.Sprintf("https://bsky.app/profile/%s/post/%s", handle, rkey)
}
// bskyPostURLFromATURI is a convenience wrapper for callers that hold an
// at-uri rather than a record key directly. Returns "" if the URI cannot be
// parsed.
func bskyPostURLFromATURI(handle, atURI string) string {
parsed, err := syntax.ParseATURI(atURI)
if err != nil {
return ""
}
rkey := parsed.RecordKey()
if rkey == "" {
return ""
}
return fmt.Sprintf("https://bsky.app/profile/%s/post/%s", handle, rkey.String())
return bskyPostURL(handle, parsed.RecordKey().String())
}
// bskyProfileURL returns the canonical handle-form URL for a profile.
@@ -185,7 +188,7 @@ func extractQuotedPostURL(pv *appbsky.FeedDefs_PostView) string {
// Skip _ViewBlocked, _ViewNotFound, _ViewDetached, and non-post records.
return ""
}
return bskyPostURL(vr.Author.Handle, vr.Uri)
return bskyPostURLFromATURI(vr.Author.Handle, vr.Uri)
}
// extractSharedContentURL returns the URL of an external link embedded in
@@ -308,7 +311,7 @@ func buildPostNode(pv *appbsky.FeedDefs_PostView, replies []*appbsky.FeedDefs_Th
node := discussionForumPosting{
Type: "DiscussionForumPosting",
URL: bskyPostURL(pv.Author.Handle, pv.Uri),
URL: bskyPostURLFromATURI(pv.Author.Handle, pv.Uri),
Identifier: pv.Uri,
Author: buildAuthor(pv.Author),
Text: postRecordText(pv),
@@ -370,7 +373,7 @@ func buildReplyNode(pv *appbsky.FeedDefs_PostView, hideLabels map[string]bool) d
node := discussionForumPosting{
Type: "DiscussionForumPosting",
URL: bskyPostURL(pv.Author.Handle, pv.Uri),
URL: bskyPostURLFromATURI(pv.Author.Handle, pv.Uri),
Identifier: pv.Uri,
Author: buildAuthor(pv.Author),
Text: postRecordText(pv),
+19 -1
View File
@@ -503,6 +503,24 @@ func TestBuildProfileJSONLD_HasPart(t *testing.T) {
}
func TestBskyPostURL(t *testing.T) {
tests := []struct {
name, handle, rkey, want string
}{
{"valid", "alice.bsky.social", "abc", "https://bsky.app/profile/alice.bsky.social/post/abc"},
{"empty handle", "", "abc", ""},
{"handle.invalid", "handle.invalid", "abc", ""},
{"empty rkey", "alice.bsky.social", "", ""},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := bskyPostURL(tt.handle, tt.rkey); got != tt.want {
t.Errorf("got %q, want %q", got, tt.want)
}
})
}
}
func TestBskyPostURLFromATURI(t *testing.T) {
tests := []struct {
name, handle, atURI, want string
}{
@@ -513,7 +531,7 @@ func TestBskyPostURL(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := bskyPostURL(tt.handle, tt.atURI); got != tt.want {
if got := bskyPostURLFromATURI(tt.handle, tt.atURI); got != tt.want {
t.Errorf("got %q, want %q", got, tt.want)
}
})
+23
View File
@@ -0,0 +1,23 @@
package main
import (
appbsky "github.com/bluesky-social/indigo/api/bsky"
)
// Helpers for inspecting profile / post labels.
// profileRequiresAuth reports whether the profile has self-applied the
// `!no-unauthenticated` label, indicating the user only wants their content
// shown to signed-in viewers. SSR responses for these profiles must omit
// post text, descriptions, and other content beyond minimal identity.
func profileRequiresAuth(pv *appbsky.ActorDefs_ProfileViewDetailed) bool {
if pv == nil {
return false
}
for _, label := range pv.Labels {
if label.Src == pv.Did && label.Val == "!no-unauthenticated" {
return true
}
}
return false
}
+81
View File
@@ -0,0 +1,81 @@
package main
import (
"testing"
comatprototypes "github.com/bluesky-social/indigo/api/atproto"
appbsky "github.com/bluesky-social/indigo/api/bsky"
)
func TestProfileRequiresAuth(t *testing.T) {
negTrue := true
tests := []struct {
name string
pv *appbsky.ActorDefs_ProfileViewDetailed
want bool
}{
{
name: "nil profile",
pv: nil,
want: false,
},
{
name: "no labels",
pv: &appbsky.ActorDefs_ProfileViewDetailed{Did: "did:plc:alice"},
want: false,
},
{
name: "self-applied !no-unauthenticated",
pv: &appbsky.ActorDefs_ProfileViewDetailed{
Did: "did:plc:alice",
Labels: []*comatprototypes.LabelDefs_Label{
{Src: "did:plc:alice", Val: "!no-unauthenticated"},
},
},
want: true,
},
{
name: "label from a different src does not gate",
pv: &appbsky.ActorDefs_ProfileViewDetailed{
Did: "did:plc:alice",
Labels: []*comatprototypes.LabelDefs_Label{
{Src: "did:plc:labeler", Val: "!no-unauthenticated"},
},
},
want: false,
},
{
name: "different label value does not gate",
pv: &appbsky.ActorDefs_ProfileViewDetailed{
Did: "did:plc:alice",
Labels: []*comatprototypes.LabelDefs_Label{
{Src: "did:plc:alice", Val: "spam"},
},
},
want: false,
},
{
// Helper does not currently honor a Neg flag — matches existing
// inline behavior in WebPost / WebProfile / WebProfileRSS. If
// negation semantics are needed for self-labels, all four call
// sites need to change together.
name: "negated label still triggers (matches prior behavior)",
pv: &appbsky.ActorDefs_ProfileViewDetailed{
Did: "did:plc:alice",
Labels: []*comatprototypes.LabelDefs_Label{
{Src: "did:plc:alice", Val: "!no-unauthenticated", Neg: &negTrue},
},
},
want: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := profileRequiresAuth(tt.pv); got != tt.want {
t.Errorf("got %v, want %v", got, tt.want)
}
})
}
}
+4 -8
View File
@@ -58,10 +58,8 @@ func (srv *Server) WebProfileRSS(c echo.Context) error {
if err != nil {
return echo.NewHTTPError(404, fmt.Sprintf("account not found: %s", handle))
}
for _, label := range pv.Labels {
if label.Src == pv.Did && label.Val == "!no-unauthenticated" {
return echo.NewHTTPError(403, fmt.Sprintf("account does not allow public views: %s", handle))
}
if profileRequiresAuth(pv) {
return echo.NewHTTPError(403, fmt.Sprintf("account does not allow public views: %s", handle))
}
return c.Redirect(http.StatusFound, fmt.Sprintf("/profile/%s/rss", pv.Did))
}
@@ -76,10 +74,8 @@ func (srv *Server) WebProfileRSS(c echo.Context) error {
if err != nil {
return echo.NewHTTPError(404, fmt.Sprintf("account not found: %s", did))
}
for _, label := range pv.Labels {
if label.Src == pv.Did && label.Val == "!no-unauthenticated" {
return echo.NewHTTPError(403, fmt.Sprintf("account does not allow public views: %s", did))
}
if profileRequiresAuth(pv) {
return echo.NewHTTPError(403, fmt.Sprintf("account does not allow public views: %s", did))
}
af, err := appbsky.FeedGetAuthorFeed(ctx, srv.xrpcc, did.String(), "", "posts_no_replies", false, 30)
+12 -27
View File
@@ -524,12 +524,7 @@ func (srv *Server) WebPost(c echo.Context) error {
log.Warnf("failed to fetch profile for: %s\t%v", identifier, err)
return c.Render(http.StatusOK, "post.html", data)
}
unauthedViewingOkay := true
for _, label := range pv.Labels {
if label.Src == pv.Did && label.Val == "!no-unauthenticated" {
unauthedViewingOkay = false
}
}
unauthedViewingOkay := !profileRequiresAuth(pv)
req := c.Request()
requestURI := fmt.Sprintf("https://%s%s", req.Host, req.URL.Path)
@@ -538,10 +533,7 @@ func (srv *Server) WebPost(c echo.Context) error {
// This both handles DID-form requests and normalizes handle-form requests against stale handles in the URL,
// guaranteeing JSON-LD `url` and <link rel="canonical"> match exactly.
// If the handle is unusable (handle.invalid or empty), fall back to the request URI with query/fragment stripped.
canonicalURL := ""
if pv.Handle != "" && pv.Handle != "handle.invalid" {
canonicalURL = fmt.Sprintf("https://bsky.app/profile/%s/post/%s", pv.Handle, rkey)
}
canonicalURL := bskyPostURL(pv.Handle, rkey.String())
if !unauthedViewingOkay {
// Provide minimal OpenGraph data for auth-required posts
@@ -661,29 +653,27 @@ func (srv *Server) WebProfile(c echo.Context) error {
return c.Render(http.StatusOK, "profile.html", data)
}
identifier := handleOrDID.Normalize().String()
isDIDInput := handleOrDID.IsDID()
pv, err := appbsky.ActorGetProfile(ctx, srv.xrpcc, identifier)
if err != nil {
log.Warnf("failed to fetch profile for: %s\t%v", identifier, err)
return c.Render(http.StatusOK, "profile.html", data)
}
unauthedViewingOkay := true
for _, label := range pv.Labels {
if label.Src == pv.Did && label.Val == "!no-unauthenticated" {
unauthedViewingOkay = false
}
}
unauthedViewingOkay := !profileRequiresAuth(pv)
req := c.Request()
data["profileView"] = pv
data["requestURI"] = fmt.Sprintf("https://%s%s", req.Host, req.URL.Path)
data["requestHost"] = req.Host
// Canonical URL: when looked up by DID and we have a usable handle,
// redirect search engines to the handle-form URL.
if isDIDInput && pv.Handle != "" && pv.Handle != "handle.invalid" {
data["canonicalURL"] = fmt.Sprintf("https://bsky.app/profile/%s", pv.Handle)
// Canonical URL: always prefer the handle-form URL when we have a usable
// handle, regardless of how the request was looked up. This handles
// DID-form requests and normalizes handle-form requests against stale
// handles, guaranteeing JSON-LD `url` and <link rel="canonical"> match.
// Falls back to requestURI (with query/fragment stripped by the
// canonicalize_url filter) when the handle is unusable.
if url := bskyProfileURL(pv.Handle); url != "" {
data["canonicalURL"] = url
}
// Fetch recent posts to embed as ProfilePage.hasPart so search engines
@@ -754,12 +744,7 @@ func (srv *Server) WebFeed(c echo.Context) error {
log.Warnf("failed to fetch profile for: %s\t%v", identifier, err)
return c.Render(http.StatusOK, "feed.html", data)
}
unauthedViewingOkay := true
for _, label := range pv.Labels {
if label.Src == pv.Did && label.Val == "!no-unauthenticated" {
unauthedViewingOkay = false
}
}
unauthedViewingOkay := !profileRequiresAuth(pv)
if !unauthedViewingOkay {
return c.Render(http.StatusOK, "feed.html", data)