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.

Proxy profile reads to appview

alice 584b7dba 95699b7d

+86 -67
-2
lib/ATProto/PDS/ServiceProxy.pm
··· 32 32 _blob_cid 33 33 _blob_url 34 34 _follow_index 35 - _get_local_profile 36 35 _profile_associated 37 36 _profile_record_value 38 37 _profile_view_basic ··· 74 73 'app.bsky.actor.putPreferences' => '_put_preferences', 75 74 'app.bsky.notification.getPreferences' => '_get_notification_preferences', 76 75 'app.bsky.notification.putPreferencesV2' => '_put_notification_preferences_v2', 77 - 'app.bsky.actor.getProfile' => '_get_local_profile', 78 76 'app.bsky.feed.getAuthorFeed' => '_get_author_feed', 79 77 'app.bsky.feed.getPosts' => '_get_posts', 80 78 'app.bsky.feed.getPostThread' => '_get_post_thread',
+1 -5
t/metrics.t
··· 87 87 }, 88 88 })->status_is(200); 89 89 90 - $t->get_ok("/xrpc/app.bsky.actor.getProfile?actor=$did" => { 91 - Authorization => "Bearer $access", 92 - })->status_is(200); 93 - 94 90 $t->get_ok("/xrpc/app.bsky.feed.getAuthorFeed?actor=$did&limit=10" => { 95 91 Authorization => "Bearer $access", 96 92 })->status_is(200); ··· 183 179 ); 184 180 like( 185 181 $metrics, 186 - qr/perlsky_service_proxy_requests_total\{nsid="app\.bsky\.actor\.getProfile",source="local",status="200"\} 1\b/, 182 + qr/perlsky_service_proxy_requests_total\{nsid="app\.bsky\.feed\.getAuthorFeed",source="local",status="200"\} 1\b/, 187 183 'local service-proxy request counters are exported', 188 184 ); 189 185 like(
-39
t/pds_smoke.t
··· 52 52 my $access = $created->{accessJwt}; 53 53 my $did = $created->{did}; 54 54 55 - $t->post_ok('/xrpc/com.atproto.server.createAccount' => json => { 56 - handle => 'bob.example.test', 57 - email => 'bob@example.test', 58 - password => 'hunter23', 59 - })->status_is(200); 60 - 61 - my $bob_created = $t->tx->res->json; 62 - my $bob_did = $bob_created->{did}; 63 - 64 55 $t->get_ok('/xrpc/com.atproto.server.getSession' => { 65 56 Authorization => "Bearer $access", 66 57 })->status_is(200) 67 58 ->json_is('/did', $did); 68 - 69 - $t->post_ok('/xrpc/com.atproto.repo.createRecord' => { 70 - Authorization => "Bearer $access", 71 - } => json => { 72 - repo => $did, 73 - collection => 'app.bsky.graph.follow', 74 - rkey => 'follow-bob', 75 - record => { 76 - '$type' => 'app.bsky.graph.follow', 77 - subject => $bob_did, 78 - createdAt => '2026-03-10T00:00:00Z', 79 - }, 80 - })->status_is(200) 81 - ->json_is('/uri', "at://$did/app.bsky.graph.follow/follow-bob"); 82 - 83 - $t->get_ok("/xrpc/app.bsky.actor.getProfile?actor=$did" => { 84 - Authorization => "Bearer $access", 85 - })->status_is(200) 86 - ->json_is('/did', $did) 87 - ->json_is('/followsCount', 1) 88 - ->json_is('/postsCount', 0) 89 - ->json_hasnt('/followersCount') 90 - ->json_hasnt('/viewer/knownFollowers'); 91 - 92 - $t->get_ok("/xrpc/app.bsky.actor.getProfile?actor=$bob_did" => { 93 - Authorization => "Bearer $access", 94 - })->status_is(200) 95 - ->json_is('/did', $bob_did) 96 - ->json_is('/viewer/following', "at://$did/app.bsky.graph.follow/follow-bob") 97 - ->json_hasnt('/followersCount'); 98 59 99 60 $t->post_ok('/xrpc/com.atproto.repo.createRecord' => { 100 61 Authorization => "Bearer $access",
+85 -21
t/service-proxy.t
··· 24 24 use IO::Socket::INET; 25 25 use Mojo::Server::Daemon; 26 26 use Mojo::UserAgent; 27 + use Mojo::Util qw(url_unescape); 27 28 use Mojolicious; 28 29 use Test::Mojo; 29 30 use ATProto::PDS; ··· 42 43 43 44 my $appview_app = Mojolicious->new; 44 45 my %appview_seen; 46 + my ($alice_profile, $bob_profile, $alice_did, $bob_did_value, $alice_handle, $bob_handle); 45 47 $appview_app->routes->get('/ready')->to(cb => sub { 46 48 my ($c) = @_; 47 49 $c->render(text => 'ok'); ··· 55 57 message => 'try again', 56 58 }); 57 59 } 60 + if ($nsid eq 'app.bsky.actor.getProfile') { 61 + my $actor = $c->param('actor') // q(); 62 + my $profile; 63 + if ( 64 + defined($alice_profile) 65 + && ( 66 + $actor eq ($alice_did // q()) 67 + || $actor eq url_unescape($alice_did // q()) 68 + || $actor eq ($alice_handle // q()) 69 + ) 70 + ) { 71 + $profile = $alice_profile; 72 + } 73 + elsif ( 74 + defined($bob_profile) 75 + && ( 76 + $actor eq ($bob_did_value // q()) 77 + || $actor eq url_unescape($bob_did_value // q()) 78 + || $actor eq ($bob_handle // q()) 79 + ) 80 + ) { 81 + $profile = $bob_profile; 82 + } 83 + $profile //= { 84 + did => $actor, 85 + handle => $actor, 86 + }; 87 + return $c->render(json => { 88 + %$profile, 89 + auth => $c->req->headers->authorization, 90 + }); 91 + } 58 92 my %body = ( 59 93 nsid => $nsid, 60 94 auth => $c->req->headers->authorization, ··· 122 156 my $did = $created->{did}; 123 157 my $account = $app->store->get_account_by_did($did); 124 158 my $handle = $created->{handle}; 159 + ($alice_did, $alice_handle) = ($did, $handle); 160 + my $upstream_profile = sub { 161 + my ($did, $handle, $extra) = @_; 162 + $extra ||= {}; 163 + return { 164 + did => $did, 165 + handle => $handle, 166 + associated => { 167 + activitySubscription => { allowSubscriptions => 'followers' }, 168 + chat => { allowIncoming => 'all' }, 169 + }, 170 + labels => [], 171 + createdAt => '2026-03-10T18:00:00Z', 172 + indexedAt => '2026-03-10T18:00:00Z', 173 + followersCount => 0, 174 + followsCount => 0, 175 + postsCount => 0, 176 + viewer => { 177 + muted => JSON::PP::false, 178 + blockedBy => JSON::PP::false, 179 + }, 180 + %$extra, 181 + }; 182 + }; 183 + $alice_profile = $upstream_profile->($did, $created->{handle}); 125 184 126 185 $t->post_ok('/xrpc/com.atproto.server.createAccount' => json => { 127 186 handle => 'bob', ··· 134 193 my $created_bob = $t->tx->res->json; 135 194 my $bob_access = $created_bob->{accessJwt}; 136 195 my $bob_did = $created_bob->{did}; 196 + ($bob_did_value, $bob_handle) = ($bob_did, $created_bob->{handle}); 197 + $bob_profile = $upstream_profile->($bob_did, $created_bob->{handle}); 137 198 138 199 $t->get_ok('/xrpc/app.bsky.ageassurance.getState?countryCode=GB&regionCode=ENG') 139 200 ->status_is(200) ··· 208 269 $t->get_ok("/xrpc/app.bsky.actor.getProfile?actor=$did" => { 209 270 Authorization => "Bearer $access", 210 271 })->status_is(200) 211 - ->json_is('/did' => $did) 212 - ->json_is('/handle' => $created->{handle}) 213 - ->json_is('/associated/chat/allowIncoming' => 'all') 214 - ->json_is('/associated/activitySubscription/allowSubscriptions' => 'followers') 215 - ->json_is('/labels' => []) 216 - ->json_has('/createdAt') 217 - ->json_has('/indexedAt') 218 - ->json_is('/followsCount' => 0) 219 - ->json_is('/postsCount' => 0); 220 - ok(!exists($t->tx->res->json->{followersCount}), 'local profile omits non-authoritative followersCount'); 272 + ->json_is('/did' => url_unescape($did)); 273 + ok($t->tx->res->json->{auth}, 'proxied getProfile forwards bearer auth upstream'); 221 274 222 275 $t->post_ok('/xrpc/com.atproto.repo.createRecord' => { 223 276 Authorization => "Bearer $access", ··· 232 285 }, 233 286 })->status_is(200) 234 287 ->json_is('/uri' => "at://$did/app.bsky.graph.follow/follow-bob"); 288 + $alice_profile->{followsCount} = 1; 289 + $bob_profile->{followersCount} = 1; 290 + $bob_profile->{viewer}{following} = "at://$did/app.bsky.graph.follow/follow-bob"; 235 291 236 292 $t->post_ok('/xrpc/com.atproto.repo.createRecord' => { 237 293 Authorization => "Bearer $bob_access", ··· 246 302 }, 247 303 })->status_is(200) 248 304 ->json_is('/uri' => "at://$bob_did/app.bsky.graph.follow/follow-alice"); 305 + $bob_profile->{followsCount} = 1; 306 + $alice_profile->{followersCount} = 1; 307 + $alice_profile->{viewer}{followedBy} = "at://$bob_did/app.bsky.graph.follow/follow-alice"; 308 + $alice_profile->{viewer}{knownFollowers} = { 309 + count => 1, 310 + followers => [ 311 + { 312 + did => $bob_did, 313 + handle => $created_bob->{handle}, 314 + }, 315 + ], 316 + }; 317 + $bob_profile->{viewer}{followedBy} = "at://$bob_did/app.bsky.graph.follow/follow-alice"; 249 318 250 319 $t->get_ok("/xrpc/app.bsky.actor.getProfile?actor=$did" => { 251 320 Authorization => "Bearer $access", 252 - })->status_is(200) 253 - ->json_is('/followsCount' => 1) 254 - ->json_hasnt('/viewer/knownFollowers'); 255 - ok(!exists($t->tx->res->json->{followersCount}), 'local profile still omits followersCount after remote-follow-capable activity'); 321 + })->status_is(200); 322 + ok($t->tx->res->json->{auth}, 'proxied getProfile keeps forwarding auth after follow activity'); 256 323 257 324 $t->get_ok("/xrpc/app.bsky.actor.getProfile?actor=$bob_did" => { 258 325 Authorization => "Bearer $access", 259 - })->status_is(200) 260 - ->json_is('/followsCount' => 1) 261 - ->json_is('/viewer/following' => "at://$did/app.bsky.graph.follow/follow-bob") 262 - ->json_is('/viewer/followedBy' => "at://$bob_did/app.bsky.graph.follow/follow-alice"); 263 - ok(!exists($t->tx->res->json->{followersCount}), 'remote-follower-dependent followersCount is omitted for followed local profiles'); 326 + })->status_is(200); 327 + ok($t->tx->res->json->{auth}, 'proxied getProfile works for other local actors too'); 264 328 265 329 $t->post_ok('/xrpc/com.atproto.repo.createRecord' => { 266 330 Authorization => "Bearer $bob_access", ··· 302 366 303 367 $t->get_ok("/xrpc/app.bsky.actor.getProfile?actor=$did" => { 304 368 Authorization => "Bearer $access", 305 - })->status_is(200) 306 - ->json_is('/postsCount' => 1); 369 + })->status_is(200); 370 + ok($t->tx->res->json->{auth}, 'proxied getProfile stays available after local posts exist'); 307 371 308 372 $t->get_ok("/xrpc/app.bsky.feed.getAuthorFeed?actor=$did&limit=10" => { 309 373 Authorization => "Bearer $access",