From 6c0b93b5274651fb0aabb552cb3d57e4d41d8612 Mon Sep 17 00:00:00 2001 From: Michael Black Date: Wed, 27 May 2026 11:18:05 -0500 Subject: [PATCH] filter hasPart recent posts on hide labels Mirrors the gating applied to comment[]: recent posts whose own labels match hideEmbedLabels or hideReplyLabels are dropped from hasPart entirely so flagged content isn't surfaced into the profile's structured data. Negated post-view labels are honored. --- bskyweb/cmd/bskyweb/jsonld.go | 9 +++++- bskyweb/cmd/bskyweb/jsonld_test.go | 49 ++++++++++++++++++++++++++++-- bskyweb/cmd/bskyweb/render_test.go | 4 +-- bskyweb/cmd/bskyweb/server.go | 2 +- 4 files changed, 58 insertions(+), 6 deletions(-) diff --git a/bskyweb/cmd/bskyweb/jsonld.go b/bskyweb/cmd/bskyweb/jsonld.go index 57cd97f4e6..466e8ffcf7 100644 --- a/bskyweb/cmd/bskyweb/jsonld.go +++ b/bskyweb/cmd/bskyweb/jsonld.go @@ -411,7 +411,9 @@ func buildPostJSONLD(pv *appbsky.FeedDefs_PostView, replies []*appbsky.FeedDefs_ } // buildProfileJSONLD marshals a ProfilePage (with hasPart recent posts). -func buildProfileJSONLD(pv *appbsky.ActorDefs_ProfileViewDetailed, recentPosts []*appbsky.FeedDefs_PostView, hideLabels map[string]bool) (string, error) { +// Recent posts whose own labels match hideLabels or hideReplyLabels are +// dropped from hasPart, mirroring the gating applied to comment[]. +func buildProfileJSONLD(pv *appbsky.ActorDefs_ProfileViewDetailed, recentPosts []*appbsky.FeedDefs_PostView, hideLabels, hideReplyLabels map[string]bool) (string, error) { if pv == nil { return "", fmt.Errorf("nil profile view") } @@ -463,6 +465,11 @@ func buildProfileJSONLD(pv *appbsky.ActorDefs_ProfileViewDetailed, recentPosts [ if len(page.HasPart) >= maxRecentPosts { break } + // Drop labeled posts entirely so flagged content isn't surfaced + // into the profile's structured data. + if postHasHideLabel(rp, hideReplyLabels) || postHasHideLabel(rp, hideLabels) { + continue + } // Recent posts go in nested form. No replies are passed, so the // reply-label set is irrelevant; pass nil. node := buildPostNode(rp, nil, hideLabels, nil) diff --git a/bskyweb/cmd/bskyweb/jsonld_test.go b/bskyweb/cmd/bskyweb/jsonld_test.go index 56cc293d0d..3d2e506cde 100644 --- a/bskyweb/cmd/bskyweb/jsonld_test.go +++ b/bskyweb/cmd/bskyweb/jsonld_test.go @@ -530,7 +530,7 @@ func TestBuildProfileJSONLD_Basic(t *testing.T) { PostsCount: intPtr(200), CreatedAt: strPtr("2023-01-01T00:00:00Z"), } - out, err := buildProfileJSONLD(pv, nil, hideEmbedLabels) + out, err := buildProfileJSONLD(pv, nil, hideEmbedLabels, hideReplyLabels) if err != nil { t.Fatal(err) } @@ -570,7 +570,7 @@ func TestBuildProfileJSONLD_HasPart(t *testing.T) { posts = append(posts, makePostView("alice.bsky.social", "did:plc:alice", "r"+string(rune('0'+i)), "post")) } - out, _ := buildProfileJSONLD(pv, posts, hideEmbedLabels) + out, _ := buildProfileJSONLD(pv, posts, hideEmbedLabels, hideReplyLabels) page := unmarshalLD(t, out) hp, ok := page["hasPart"].([]any) if !ok { @@ -588,6 +588,51 @@ func TestBuildProfileJSONLD_HasPart(t *testing.T) { } } +func TestBuildProfileJSONLD_HasPartLabelFiltered(t *testing.T) { + // Recent posts carrying hideReplyLabels or hideEmbedLabels are dropped + // from hasPart entirely. Negation is honored on post-view labels. + pv := &appbsky.ActorDefs_ProfileViewDetailed{ + Did: "did:plc:alice", Handle: "alice.bsky.social", + DisplayName: strPtr("Alice"), + CreatedAt: strPtr("2023-01-01T00:00:00Z"), + } + good := makePostView("alice.bsky.social", "did:plc:alice", "good", "ok") + hidden := makePostView("alice.bsky.social", "did:plc:alice", "hide", "hidden", + withPostLabel("!hide", false)) + spam := makePostView("alice.bsky.social", "did:plc:alice", "spam", "spam", + withSelfLabel("spam")) + embedHide := makePostView("alice.bsky.social", "did:plc:alice", "harm", "embed-only", + withPostLabel("self-harm", false)) + negated := makePostView("alice.bsky.social", "did:plc:alice", "neg", "negated", + withPostLabel("!hide", true)) + + out, _ := buildProfileJSONLD(pv, []*appbsky.FeedDefs_PostView{ + good, hidden, spam, embedHide, negated, + }, hideEmbedLabels, hideReplyLabels) + page := unmarshalLD(t, out) + hp, _ := page["hasPart"].([]any) + + got := make(map[string]bool, len(hp)) + for _, e := range hp { + got[e.(map[string]any)["identifier"].(string)] = true + } + if !got[good.Uri] { + t.Errorf("expected unlabeled post in hasPart") + } + if !got[negated.Uri] { + t.Errorf("negated hide label should not gate; expected post in hasPart") + } + if got[hidden.Uri] { + t.Errorf("post with !hide label should be dropped from hasPart") + } + if got[spam.Uri] { + t.Errorf("self-labeled spam post should be dropped from hasPart") + } + if got[embedHide.Uri] { + t.Errorf("post with hideEmbedLabels label should be dropped from hasPart") + } +} + func TestBskyPostURL(t *testing.T) { tests := []struct { name, handle, rkey, want string diff --git a/bskyweb/cmd/bskyweb/render_test.go b/bskyweb/cmd/bskyweb/render_test.go index 8a7109d836..f7c9edd677 100644 --- a/bskyweb/cmd/bskyweb/render_test.go +++ b/bskyweb/cmd/bskyweb/render_test.go @@ -114,7 +114,7 @@ func TestRenderPost_FallsBackToCanonicalizeFilter(t *testing.T) { func TestRenderProfile_EmitsJSONLD(t *testing.T) { pv := newProfileViewDetailed() - ld, err := buildProfileJSONLD(pv, nil, hideEmbedLabels) + ld, err := buildProfileJSONLD(pv, nil, hideEmbedLabels, hideReplyLabels) if err != nil { t.Fatal(err) } @@ -140,7 +140,7 @@ func TestRenderProfile_EmitsJSONLD(t *testing.T) { // before buildProfileJSONLD. func TestRenderProfile_AuthRequiredEmitsJSONLD(t *testing.T) { pv := newProfileViewDetailed() - ld, err := buildProfileJSONLD(pv, nil, hideEmbedLabels) + ld, err := buildProfileJSONLD(pv, nil, hideEmbedLabels, hideReplyLabels) if err != nil { t.Fatal(err) } diff --git a/bskyweb/cmd/bskyweb/server.go b/bskyweb/cmd/bskyweb/server.go index bb59c0543a..ec4740c205 100644 --- a/bskyweb/cmd/bskyweb/server.go +++ b/bskyweb/cmd/bskyweb/server.go @@ -713,7 +713,7 @@ func (srv *Server) WebProfile(c echo.Context) error { data["requiresAuth"] = true } - if jsonld, err := buildProfileJSONLD(pv, recentPosts, hideEmbedLabels); err == nil { + if jsonld, err := buildProfileJSONLD(pv, recentPosts, hideEmbedLabels, hideReplyLabels); err == nil { data["profileJSONLD"] = jsonld } else { log.Warnf("failed to build profile JSON-LD for %s: %v", pv.Did, err)