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.

Persist label negations across restore

alice 133049c2 ffd539e4

+111 -35
+9 -2
lib/ATProto/PDS/API/Admin.pm
··· 291 291 292 292 my $src = service_did($c->app->settings); 293 293 my ($uri, $cid) = _label_uri_and_cid($subject); 294 + my $label_time = time; 294 295 my $label = { 295 296 ver => 1, 296 297 src => $src, 297 298 uri => $uri, 298 299 (defined $cid ? (cid => $cid) : ()), 299 300 val => '!hide', 300 - cts => ATProto::PDS::API::Util::iso8601(time), 301 + cts => ATProto::PDS::API::Util::iso8601($label_time), 301 302 ($now ? () : (neg => JSON::PP::true)), 302 303 }; 303 304 ··· 308 309 uri => $uri, 309 310 cid => $cid, 310 311 val => '!hide', 312 + created_at => $label_time, 313 + neg => 0, 311 314 ); 312 315 } else { 313 - $c->store->delete_label( 316 + $c->store->put_label( 314 317 subject_key => subject_key($subject), 315 318 src => $src, 319 + uri => $uri, 320 + cid => $cid, 316 321 val => '!hide', 322 + created_at => $label_time, 323 + neg => 1, 317 324 ); 318 325 } 319 326
+1
lib/ATProto/PDS/API/Misc.pm
··· 285 285 uri => $row->{uri}, 286 286 (defined($row->{cid}) ? (cid => $row->{cid}) : ()), 287 287 val => $row->{val}, 288 + ($row->{neg} ? (neg => JSON::PP::true) : ()), 288 289 cts => iso8601($row->{created_at}), 289 290 (defined($row->{exp}) ? (exp => iso8601($row->{exp})) : ()), 290 291 (defined($row->{sig}) ? (sig => $row->{sig}) : ()),
+13 -17
lib/ATProto/PDS/Store/SQLite.pm
··· 1333 1333 $self->dbh, 1334 1334 q{ 1335 1335 INSERT INTO labels ( 1336 - subject_key, src, uri, cid, val, exp, sig, created_at, updated_at 1337 - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) 1336 + subject_key, src, uri, cid, val, neg, exp, sig, created_at, updated_at 1337 + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) 1338 1338 ON CONFLICT(subject_key, src, val) DO UPDATE SET 1339 + id = excluded.id, 1339 1340 uri = excluded.uri, 1340 1341 cid = excluded.cid, 1342 + neg = excluded.neg, 1341 1343 exp = excluded.exp, 1342 1344 sig = excluded.sig, 1345 + created_at = excluded.created_at, 1343 1346 updated_at = excluded.updated_at 1344 1347 }, 1345 1348 [ ··· 1348 1351 $uri, 1349 1352 $args{cid}, 1350 1353 $val, 1354 + ($args{neg} ? 1 : 0), 1351 1355 $args{exp}, 1352 1356 $args{sig}, 1353 1357 $now, 1354 1358 $args{updated_at} // $now, 1355 1359 ], 1356 1360 _blob_bind_positions_for_names( 1357 - [qw(subject_key src uri cid val exp sig created_at updated_at)], 1361 + [qw(subject_key src uri cid val neg exp sig created_at updated_at)], 1358 1362 qw(sig), 1359 1363 ), 1360 1364 ); ··· 1377 1381 $args{src}, 1378 1382 $args{val}, 1379 1383 ), qw(sig)); 1380 - } 1381 - 1382 - sub delete_label ($self, %args) { 1383 - $self->dbh->do( 1384 - q{ 1385 - DELETE FROM labels 1386 - WHERE subject_key = ? AND src = ? AND val = ? 1387 - }, 1388 - undef, 1389 - $args{subject_key}, 1390 - $args{src}, 1391 - $args{val}, 1392 - ); 1393 - return 1; 1394 1384 } 1395 1385 1396 1386 sub list_labels ($self, %args) { ··· 2093 2083 statements => [ 2094 2084 q{ALTER TABLE sessions ADD COLUMN next_id TEXT}, 2095 2085 q{ALTER TABLE app_passwords ADD COLUMN privileged INTEGER NOT NULL DEFAULT 0}, 2086 + ], 2087 + }, 2088 + { 2089 + version => 9, 2090 + statements => [ 2091 + q{ALTER TABLE labels ADD COLUMN neg INTEGER NOT NULL DEFAULT 0}, 2096 2092 ], 2097 2093 }, 2098 2094 );
+18
t/extended-api.t
··· 214 214 ->status_is(200) 215 215 ->json_is('/labels/0/val', '!hide'); 216 216 217 + $t->post_ok('/xrpc/com.atproto.admin.updateSubjectStatus' => { 218 + Authorization => $admin_auth, 219 + } => json => { 220 + subject => { did => $did }, 221 + takedown => { applied => JSON::PP::false, ref => 'unit-test' }, 222 + })->status_is(200); 223 + 224 + $t->get_ok(Mojo::URL->new('/xrpc/com.atproto.label.queryLabels')->query( 225 + uriPatterns => "at://$did*", 226 + ))->status_is(200) 227 + ->json_is('/labels/0/val', '!hide') 228 + ->json_is('/labels/0/neg', JSON::PP::true); 229 + 230 + $t->get_ok('/xrpc/com.atproto.temp.fetchLabels?limit=10') 231 + ->status_is(200) 232 + ->json_is('/labels/0/val', '!hide') 233 + ->json_is('/labels/0/neg', JSON::PP::true); 234 + 217 235 $t->post_ok('/xrpc/com.atproto.sync.requestCrawl' => json => { 218 236 hostname => 'relay.example.test', 219 237 })->status_is(200);
+44 -16
t/labels.t
··· 138 138 uriPatterns => $record_uri, 139 139 sources => $service_did, 140 140 ))->status_is(200) 141 - ->json_is('/labels', []); 141 + ->json_is('/labels/0/uri', $record_uri) 142 + ->json_is('/labels/0/cid', $record_cid) 143 + ->json_is('/labels/0/val', '!hide') 144 + ->json_is('/labels/0/neg', JSON::PP::true); 142 145 143 146 my $blob_tx = $t->ua->build_tx( 144 147 POST => '/xrpc/com.atproto.repo.uploadBlob' => { ··· 193 196 uriPatterns => "at://$did", 194 197 sources => $service_did, 195 198 ))->status_is(200) 196 - ->json_is('/labels', []); 199 + ->json_is('/labels/0/uri', "at://$did") 200 + ->json_is('/labels/0/cid', $blob_cid) 201 + ->json_is('/labels/0/val', '!hide') 202 + ->json_is('/labels/0/neg', JSON::PP::true); 197 203 198 204 $t->post_ok('/xrpc/com.atproto.admin.updateSubjectStatus' => { 199 205 Authorization => $admin_auth, ··· 215 221 ok(!$frame->{body}{labels}[0]{neg}, 'takedown frame is a positive label'); 216 222 217 223 $t->get_ok(Mojo::URL->new('/xrpc/com.atproto.label.queryLabels')->query( 218 - uriPatterns => "at://$did*", 224 + uriPatterns => "at://$did", 219 225 sources => $service_did, 220 226 ))->status_is(200) 221 - ->json_is('/labels/0/src', $service_did) 222 - ->json_is('/labels/0/uri', "at://$did") 223 - ->json_is('/labels/0/val', '!hide'); 227 + ->json_has('/labels/0'); 228 + 229 + my ($repo_label) = grep { 230 + ($_->{uri} // q()) eq "at://$did" 231 + && !defined $_->{cid} 232 + && ($_->{val} // q()) eq '!hide' 233 + && !$_->{neg} 234 + } @{ $t->tx->res->json->{labels} }; 235 + ok($repo_label, 'repo query includes the positive repo label itself'); 236 + is($repo_label->{src}, $service_did, 'repo label query preserves the local label source'); 224 237 225 238 $t->get_ok(Mojo::URL->new('/xrpc/com.atproto.label.queryLabels')->query( 226 239 uriPatterns => "at://$did*", ··· 249 262 my $bob_frame = decode_frame($ws->message->[1]); 250 263 is($bob_frame->{body}{labels}[0]{uri}, "at://$bob_did", 'second repo takedown streams immediately'); 251 264 252 - $t->get_ok(Mojo::URL->new('/xrpc/com.atproto.label.queryLabels')->query( 253 - uriPatterns => 'at://*', 265 + $t->get_ok(Mojo::URL->new('/xrpc/com.atproto.label.queryLabels')->query([ 266 + uriPatterns => $record_uri, 267 + uriPatterns => "at://$bob_did", 254 268 limit => 1, 255 - ))->status_is(200) 269 + ]))->status_is(200) 256 270 ->json_has('/cursor') 257 271 ->json_is('/labels/0/src', $service_did); 258 272 259 273 my $first_page = $t->tx->res->json; 260 274 my $cursor = $t->tx->res->json->{cursor}; 261 275 262 - $t->get_ok(Mojo::URL->new('/xrpc/com.atproto.label.queryLabels')->query( 263 - uriPatterns => 'at://*', 276 + $t->get_ok(Mojo::URL->new('/xrpc/com.atproto.label.queryLabels')->query([ 277 + uriPatterns => $record_uri, 278 + uriPatterns => "at://$bob_did", 264 279 cursor => $cursor, 265 280 limit => 1, 266 - ))->status_is(200) 281 + ]))->status_is(200) 267 282 ->json_has('/labels/0'); 268 283 269 284 my $second_page = $t->tx->res->json; 270 - isnt($second_page->{labels}[0]{uri}, $first_page->{labels}[0]{uri}, 'cursor pagination does not repeat the same label'); 285 + isnt( 286 + join("\0", map { defined $_ ? $_ : q() } @{$second_page->{labels}[0]}{qw(uri cid neg)}), 287 + join("\0", map { defined $_ ? $_ : q() } @{$first_page->{labels}[0]}{qw(uri cid neg)}), 288 + 'cursor pagination does not repeat the same label', 289 + ); 271 290 is_deeply( 272 291 [ sort map { $_->{uri} } @{ $first_page->{labels} }, @{ $second_page->{labels} } ], 273 - [ sort "at://$did", "at://$bob_did" ], 292 + [ sort $record_uri, "at://$bob_did" ], 274 293 'cursor pagination covers the expected label subjects without overlap', 275 294 ); 276 295 ··· 326 345 $skip_non_label->finish_ok; 327 346 328 347 $t->get_ok(Mojo::URL->new('/xrpc/com.atproto.label.queryLabels')->query( 329 - uriPatterns => "at://$did*", 348 + uriPatterns => "at://$did", 330 349 sources => $service_did, 331 350 ))->status_is(200) 332 - ->json_is('/labels', []); 351 + ->json_has('/labels/0'); 352 + 353 + my ($repo_neg_label) = grep { 354 + ($_->{uri} // q()) eq "at://$did" 355 + && !defined $_->{cid} 356 + && ($_->{val} // q()) eq '!hide' 357 + && $_->{neg} 358 + } @{ $t->tx->res->json->{labels} }; 359 + ok($repo_neg_label, 'repo query includes the negated repo label itself'); 360 + is($repo_neg_label->{src}, $service_did, 'negated repo label keeps the local source'); 333 361 334 362 $app->store->dbh->do(q{DELETE FROM events WHERE seq <= ?}, undef, $app->store->latest_event_seq); 335 363
+26
t/store-sqlite.t
··· 125 125 $store->revoke_app_password('app-1', revoked_at => 456); 126 126 is($store->get_app_password('app-1')->{revoked_at}, 456, 'app passwords can be revoked'); 127 127 128 + my $positive_label = $store->put_label( 129 + subject_key => 'repo:did:web:pds.example.com:users:alice', 130 + src => 'did:web:pds.example.com', 131 + uri => 'at://did:web:pds.example.com:users:alice', 132 + val => '!hide', 133 + created_at => 100, 134 + ); 135 + ok(!$positive_label->{neg}, 'new labels default to positive state'); 136 + 137 + my $negated_label = $store->put_label( 138 + subject_key => 'repo:did:web:pds.example.com:users:alice', 139 + src => 'did:web:pds.example.com', 140 + uri => 'at://did:web:pds.example.com:users:alice', 141 + val => '!hide', 142 + neg => 1, 143 + created_at => 200, 144 + ); 145 + is($negated_label->{neg}, 1, 'label negation state is stored'); 146 + cmp_ok($negated_label->{id}, '>', $positive_label->{id}, 'negating a label refreshes its pagination id'); 147 + is($negated_label->{created_at}, 200, 'negating a label refreshes its label timestamp'); 148 + is( 149 + $store->list_labels(uri_patterns => ['at://did:web:pds.example.com:users:alice'])->{items}[0]{neg}, 150 + 1, 151 + 'label listings preserve negation rows', 152 + ); 153 + 128 154 $store->close; 129 155 130 156 done_testing;