perlsky is a Perl 5 implementation of an AT Protocol Personal Data Server.
13
fork

Configure Feed

Select the types of activity you want to include in your feed.

Refine local appview feed fallback semantics

alice dd087123 75a51b94

+90 -16
+20 -11
lib/ATProto/PDS/ServiceProxy/Threads.pm
··· 40 40 my $limit = $c->param('limit') // 50; 41 41 $limit = 1 if $limit < 1; 42 42 $limit = 100 if $limit > 100; 43 + return undef if $c->store->count_records_by_collection($account->{did}, 'app.bsky.feed.repost'); 43 44 44 45 my $page = $c->store->list_records( 45 46 $account->{did}, ··· 48 49 cursor => $c->param('cursor'), 49 50 reverse => 1, 50 51 ); 51 - return undef if grep { _post_requires_upstream($self, $c, $_) } @{ $page->{items} || [] }; 52 52 my $profile_value = $self->_profile_record_value($c, $account); 53 53 my @feed = map { 54 54 +{ ··· 66 66 xrpc_error(405, 'MethodNotAllowed', 'app.bsky.feed.getPosts expects GET') 67 67 unless $c->req->method eq 'GET'; 68 68 69 - my @uris = grep { defined($_) && length($_) } $c->every_param('uris'); 69 + my @uris = grep { defined($_) && length($_) } 70 + map { ref($_) eq 'ARRAY' ? @$_ : $_ } 71 + $c->every_param('uris'); 70 72 xrpc_error(400, 'InvalidRequest', 'uris is required') unless @uris; 71 73 72 74 my @resolved; ··· 81 83 } 82 84 return undef unless defined $resolved; 83 85 my ($account, $row) = @$resolved; 84 - return undef if _post_requires_upstream($self, $c, $row); 85 86 my $canonical_uri = $self->_post_uri($account, $row); 86 87 next if $seen_uri{$canonical_uri}++; 87 88 push @resolved, $resolved; ··· 155 156 } 156 157 157 158 sub _thread_requires_upstream ($self, $c, $row) { 158 - for my $uri ($self->_reply_parent_uri($row), $self->_quoted_uri($row->{value})) { 159 + for my $uri ($self->_reply_parent_uri($row)) { 159 160 next unless defined $uri && length $uri; 160 161 my $resolved = eval { $self->_resolve_local_post_uri($c, $uri) }; 161 162 return 1 if !$resolved || $@; ··· 413 414 } 414 415 415 416 if ($type eq 'app.bsky.embed.record' && ref($embed->{record}) eq 'HASH') { 417 + my $record = $self->_record_embed_view($c, $embed->{record}, $viewer, $depth); 418 + return undef unless defined $record; 416 419 return { 417 420 '$type' => 'app.bsky.embed.record#view', 418 - record => $self->_record_embed_view($c, $embed->{record}, $viewer, $depth), 421 + record => $record, 419 422 }; 420 423 } 421 424 ··· 446 449 447 450 sub _record_embed_view ($self, $c, $record_ref, $viewer = undef, $depth = 0) { 448 451 my $uri = $record_ref->{uri} // q(); 449 - my $resolved = $self->_resolve_local_post_uri($c, $uri); 450 - return { 451 - '$type' => 'app.bsky.embed.record#viewNotFound', 452 - uri => $uri, 453 - notFound => JSON::PP::true, 454 - } unless $resolved; 452 + my $resolved = eval { $self->_resolve_local_post_uri($c, $uri) }; 453 + if (my $err = $@) { 454 + return { 455 + '$type' => 'app.bsky.embed.record#viewNotFound', 456 + uri => $uri, 457 + notFound => JSON::PP::true, 458 + } if ref($err) eq 'HASH' 459 + && ($err->{status} // 0) == 404 460 + && ($err->{error} // q()) eq 'RecordNotFound'; 461 + die $err; 462 + } 463 + return undef unless $resolved; 455 464 456 465 my ($account, $row) = @$resolved; 457 466 my $profile_value = $self->_profile_record_value($c, $account);
+40
t/service-proxy-local.t
··· 71 71 ]; 72 72 } 73 73 74 + sub count_records_by_collection { 75 + my ($self, $did, $collection) = @_; 76 + $self->{count_records_by_collection_calls}{"$did|$collection"}++; 77 + return $self->{count_records_by_collection}{"$did|$collection"} // 0; 78 + } 79 + 74 80 sub latest_event_seq { 75 81 my ($self) = @_; 76 82 $self->{latest_event_seq_calls}++; ··· 329 335 $viewer->{followedBy}, 330 336 "at://did:plc:bob/app.bsky.graph.follow/follow-alice", 331 337 'profile viewer includes the reciprocal follow URI', 338 + ); 339 + 340 + my $remote_quote_embed = $proxy->_post_embed_view( 341 + $c, 342 + $store->{accounts_by_did}{$did}, 343 + { 344 + embed => { 345 + '$type' => 'app.bsky.embed.record', 346 + record => { 347 + uri => 'at://did:plc:bob/app.bsky.feed.post/remote-post', 348 + cid => 'bafyremote', 349 + }, 350 + }, 351 + }, 352 + ); 353 + ok(!defined($remote_quote_embed), 'remote quoted records omit non-authoritative derived embeds'); 354 + 355 + my $missing_local_quote_embed = $proxy->_post_embed_view( 356 + $c, 357 + $store->{accounts_by_did}{$did}, 358 + { 359 + embed => { 360 + '$type' => 'app.bsky.embed.record', 361 + record => { 362 + uri => "at://$did/app.bsky.feed.post/missing-post", 363 + cid => 'bafymissing', 364 + }, 365 + }, 366 + }, 367 + ); 368 + is( 369 + $missing_local_quote_embed->{record}{'$type'}, 370 + 'app.bsky.embed.record#viewNotFound', 371 + 'missing local quoted records still render viewNotFound', 332 372 ); 333 373 334 374 done_testing;
+30 -5
t/service-proxy.t
··· 493 493 494 494 $t->get_ok("/xrpc/app.bsky.feed.getAuthorFeed?actor=$did&limit=10" => { 495 495 Authorization => "Bearer $access", 496 + })->status_is(200); 497 + ok(!$t->tx->res->json->{auth}, 'author feed keeps local read-after-write behavior for quoted remote records'); 498 + is($t->tx->res->json->{feed}[0]{post}{uri}, $quoted_remote_uri, 'quoted remote post stays visible in the local author feed'); 499 + ok(!exists($t->tx->res->json->{feed}[0]{post}{embed}), 'quoted remote post omits a non-authoritative derived embed'); 500 + 501 + $t->get_ok('/xrpc/app.bsky.feed.getPosts?uris=' . _uri_escape($quoted_remote_uri) => { 502 + Authorization => "Bearer $access", 496 503 })->status_is(200) 497 - ->json_is('/nsid' => 'app.bsky.feed.getAuthorFeed'); 498 - ok($t->tx->res->json->{auth}, 'author feed proxies upstream when quoted remote records need non-local context'); 504 + ->json_is('/posts/0/uri' => $quoted_remote_uri); 505 + ok(!$t->tx->res->json->{auth}, 'getPosts keeps local read-after-write behavior for quoted remote records'); 506 + ok(!exists($t->tx->res->json->{posts}[0]{embed}), 'getPosts omits non-authoritative derived embeds for remote quotes'); 507 + 508 + $t->post_ok('/xrpc/com.atproto.repo.createRecord' => { 509 + Authorization => "Bearer $access", 510 + } => json => { 511 + repo => $did, 512 + collection => 'app.bsky.feed.repost', 513 + rkey => 'repost-remote', 514 + record => { 515 + '$type' => 'app.bsky.feed.repost', 516 + subject => { 517 + uri => 'at://did:plc:remote/app.bsky.feed.post/reposted-post', 518 + cid => 'bafyremote-repost', 519 + }, 520 + createdAt => '2026-03-10T18:00:30Z', 521 + }, 522 + })->status_is(200) 523 + ->json_has('/uri'); 499 524 500 - $t->get_ok('/xrpc/app.bsky.feed.getPosts?uris=' . _uri_escape($quoted_remote_uri) => { 525 + $t->get_ok("/xrpc/app.bsky.feed.getAuthorFeed?actor=$did&limit=10" => { 501 526 Authorization => "Bearer $access", 502 527 })->status_is(200) 503 - ->json_is('/nsid' => 'app.bsky.feed.getPosts'); 504 - ok($t->tx->res->json->{auth}, 'getPosts proxies upstream when quoted remote records need non-local context'); 528 + ->json_is('/nsid' => 'app.bsky.feed.getAuthorFeed'); 529 + ok($t->tx->res->json->{auth}, 'author feed proxies upstream when repost records make the local feed incomplete'); 505 530 506 531 $t->get_ok('/xrpc/app.bsky.feed.getPostThread?uri=' . _uri_escape($post_uri) => { 507 532 Authorization => "Bearer $access",