more reusable funcs
This commit is contained in:
@@ -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),
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user