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.
This commit is contained in:
Michael Black
2026-05-27 11:18:05 -05:00
parent b170cd45ba
commit 6c0b93b527
4 changed files with 58 additions and 6 deletions
+8 -1
View File
@@ -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)
+47 -2
View File
@@ -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
+2 -2
View File
@@ -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)
}
+1 -1
View File
@@ -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)