more reusable funcs
This commit is contained in:
@@ -98,21 +98,24 @@ const maxRecentPosts = 10
|
|||||||
const authorFeedFetchLimit = 3 * maxRecentPosts
|
const authorFeedFetchLimit = 3 * maxRecentPosts
|
||||||
|
|
||||||
// bskyPostURL returns the canonical handle-form URL for a post, given the
|
// 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
|
// post's author handle and record key. Returns "" if the handle is unusable
|
||||||
// the handle is unusable (handle.invalid).
|
// (empty or handle.invalid) or rkey is empty.
|
||||||
func bskyPostURL(handle, atURI string) string {
|
func bskyPostURL(handle, rkey string) string {
|
||||||
if handle == "" || handle == "handle.invalid" {
|
if handle == "" || handle == "handle.invalid" || rkey == "" {
|
||||||
return ""
|
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)
|
parsed, err := syntax.ParseATURI(atURI)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
rkey := parsed.RecordKey()
|
return bskyPostURL(handle, parsed.RecordKey().String())
|
||||||
if rkey == "" {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return fmt.Sprintf("https://bsky.app/profile/%s/post/%s", handle, rkey.String())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// bskyProfileURL returns the canonical handle-form URL for a profile.
|
// 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.
|
// Skip _ViewBlocked, _ViewNotFound, _ViewDetached, and non-post records.
|
||||||
return ""
|
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
|
// 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{
|
node := discussionForumPosting{
|
||||||
Type: "DiscussionForumPosting",
|
Type: "DiscussionForumPosting",
|
||||||
URL: bskyPostURL(pv.Author.Handle, pv.Uri),
|
URL: bskyPostURLFromATURI(pv.Author.Handle, pv.Uri),
|
||||||
Identifier: pv.Uri,
|
Identifier: pv.Uri,
|
||||||
Author: buildAuthor(pv.Author),
|
Author: buildAuthor(pv.Author),
|
||||||
Text: postRecordText(pv),
|
Text: postRecordText(pv),
|
||||||
@@ -370,7 +373,7 @@ func buildReplyNode(pv *appbsky.FeedDefs_PostView, hideLabels map[string]bool) d
|
|||||||
|
|
||||||
node := discussionForumPosting{
|
node := discussionForumPosting{
|
||||||
Type: "DiscussionForumPosting",
|
Type: "DiscussionForumPosting",
|
||||||
URL: bskyPostURL(pv.Author.Handle, pv.Uri),
|
URL: bskyPostURLFromATURI(pv.Author.Handle, pv.Uri),
|
||||||
Identifier: pv.Uri,
|
Identifier: pv.Uri,
|
||||||
Author: buildAuthor(pv.Author),
|
Author: buildAuthor(pv.Author),
|
||||||
Text: postRecordText(pv),
|
Text: postRecordText(pv),
|
||||||
|
|||||||
@@ -503,6 +503,24 @@ func TestBuildProfileJSONLD_HasPart(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestBskyPostURL(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 {
|
tests := []struct {
|
||||||
name, handle, atURI, want string
|
name, handle, atURI, want string
|
||||||
}{
|
}{
|
||||||
@@ -513,7 +531,7 @@ func TestBskyPostURL(t *testing.T) {
|
|||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
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)
|
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 {
|
if err != nil {
|
||||||
return echo.NewHTTPError(404, fmt.Sprintf("account not found: %s", handle))
|
return echo.NewHTTPError(404, fmt.Sprintf("account not found: %s", handle))
|
||||||
}
|
}
|
||||||
for _, label := range pv.Labels {
|
if profileRequiresAuth(pv) {
|
||||||
if label.Src == pv.Did && label.Val == "!no-unauthenticated" {
|
return echo.NewHTTPError(403, fmt.Sprintf("account does not allow public views: %s", handle))
|
||||||
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))
|
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 {
|
if err != nil {
|
||||||
return echo.NewHTTPError(404, fmt.Sprintf("account not found: %s", did))
|
return echo.NewHTTPError(404, fmt.Sprintf("account not found: %s", did))
|
||||||
}
|
}
|
||||||
for _, label := range pv.Labels {
|
if profileRequiresAuth(pv) {
|
||||||
if label.Src == pv.Did && label.Val == "!no-unauthenticated" {
|
return echo.NewHTTPError(403, fmt.Sprintf("account does not allow public views: %s", did))
|
||||||
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)
|
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)
|
log.Warnf("failed to fetch profile for: %s\t%v", identifier, err)
|
||||||
return c.Render(http.StatusOK, "post.html", data)
|
return c.Render(http.StatusOK, "post.html", data)
|
||||||
}
|
}
|
||||||
unauthedViewingOkay := true
|
unauthedViewingOkay := !profileRequiresAuth(pv)
|
||||||
for _, label := range pv.Labels {
|
|
||||||
if label.Src == pv.Did && label.Val == "!no-unauthenticated" {
|
|
||||||
unauthedViewingOkay = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
req := c.Request()
|
req := c.Request()
|
||||||
requestURI := fmt.Sprintf("https://%s%s", req.Host, req.URL.Path)
|
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,
|
// 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.
|
// 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.
|
// If the handle is unusable (handle.invalid or empty), fall back to the request URI with query/fragment stripped.
|
||||||
canonicalURL := ""
|
canonicalURL := bskyPostURL(pv.Handle, rkey.String())
|
||||||
if pv.Handle != "" && pv.Handle != "handle.invalid" {
|
|
||||||
canonicalURL = fmt.Sprintf("https://bsky.app/profile/%s/post/%s", pv.Handle, rkey)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !unauthedViewingOkay {
|
if !unauthedViewingOkay {
|
||||||
// Provide minimal OpenGraph data for auth-required posts
|
// 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)
|
return c.Render(http.StatusOK, "profile.html", data)
|
||||||
}
|
}
|
||||||
identifier := handleOrDID.Normalize().String()
|
identifier := handleOrDID.Normalize().String()
|
||||||
isDIDInput := handleOrDID.IsDID()
|
|
||||||
|
|
||||||
pv, err := appbsky.ActorGetProfile(ctx, srv.xrpcc, identifier)
|
pv, err := appbsky.ActorGetProfile(ctx, srv.xrpcc, identifier)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warnf("failed to fetch profile for: %s\t%v", identifier, err)
|
log.Warnf("failed to fetch profile for: %s\t%v", identifier, err)
|
||||||
return c.Render(http.StatusOK, "profile.html", data)
|
return c.Render(http.StatusOK, "profile.html", data)
|
||||||
}
|
}
|
||||||
unauthedViewingOkay := true
|
unauthedViewingOkay := !profileRequiresAuth(pv)
|
||||||
for _, label := range pv.Labels {
|
|
||||||
if label.Src == pv.Did && label.Val == "!no-unauthenticated" {
|
|
||||||
unauthedViewingOkay = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
req := c.Request()
|
req := c.Request()
|
||||||
data["profileView"] = pv
|
data["profileView"] = pv
|
||||||
data["requestURI"] = fmt.Sprintf("https://%s%s", req.Host, req.URL.Path)
|
data["requestURI"] = fmt.Sprintf("https://%s%s", req.Host, req.URL.Path)
|
||||||
data["requestHost"] = req.Host
|
data["requestHost"] = req.Host
|
||||||
|
|
||||||
// Canonical URL: when looked up by DID and we have a usable handle,
|
// Canonical URL: always prefer the handle-form URL when we have a usable
|
||||||
// redirect search engines to the handle-form URL.
|
// handle, regardless of how the request was looked up. This handles
|
||||||
if isDIDInput && pv.Handle != "" && pv.Handle != "handle.invalid" {
|
// DID-form requests and normalizes handle-form requests against stale
|
||||||
data["canonicalURL"] = fmt.Sprintf("https://bsky.app/profile/%s", pv.Handle)
|
// 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
|
// 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)
|
log.Warnf("failed to fetch profile for: %s\t%v", identifier, err)
|
||||||
return c.Render(http.StatusOK, "feed.html", data)
|
return c.Render(http.StatusOK, "feed.html", data)
|
||||||
}
|
}
|
||||||
unauthedViewingOkay := true
|
unauthedViewingOkay := !profileRequiresAuth(pv)
|
||||||
for _, label := range pv.Labels {
|
|
||||||
if label.Src == pv.Did && label.Val == "!no-unauthenticated" {
|
|
||||||
unauthedViewingOkay = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !unauthedViewingOkay {
|
if !unauthedViewingOkay {
|
||||||
return c.Render(http.StatusOK, "feed.html", data)
|
return c.Render(http.StatusOK, "feed.html", data)
|
||||||
|
|||||||
Reference in New Issue
Block a user