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.

Honor admin invite controls and email defaults

alice 353b8348 1f1d7c30

+173 -3
+10 -2
lib/ATProto/PDS/API/Admin.pm
··· 110 110 require_admin($c); 111 111 my $body = $c->req->json || {}; 112 112 my $account = $c->store->get_account_by_did($body->{recipientDid} // q()); 113 + xrpc_error(404, 'AccountNotFound', 'Recipient was not found') unless $account; 114 + xrpc_error(400, 'InvalidRequest', 'account does not have an email address') 115 + unless defined($account->{email}) && length($account->{email}); 116 + my $subject = defined($body->{subject}) && length($body->{subject}) 117 + ? $body->{subject} 118 + : 'Message via your PDS'; 113 119 $c->store->log_outbound_email( 114 120 recipient_did => $body->{recipientDid}, 115 - recipient_email => $account ? $account->{email} : undef, 121 + recipient_email => $account->{email}, 116 122 sender_did => $body->{senderDid}, 117 - subject => $body->{subject}, 123 + subject => $subject, 118 124 content => $body->{content}, 119 125 comment => $body->{comment}, 120 126 sent => 1, ··· 203 209 $registry->register('com.atproto.admin.disableInviteCodes', sub ($c, $endpoint) { 204 210 require_admin($c); 205 211 my $body = $c->req->json || {}; 212 + xrpc_error(400, 'InvalidRequest', 'cannot disable admin invite codes') 213 + if grep { defined($_) && $_ eq 'admin' } @{ $body->{accounts} || [] }; 206 214 $c->store->disable_invite_codes( 207 215 codes => $body->{codes}, 208 216 accounts => $body->{accounts},
+2
lib/ATProto/PDS/API/Server.pm
··· 871 871 } 872 872 873 873 my (undef, $account) = require_auth($c, audience => TOKEN_AUD_ACCESS, required_scope => 'full'); 874 + xrpc_error(400, 'InvalidRequest', 'Invite creation is disabled for this account') 875 + if $account->{invites_disabled}; 874 876 if ($opts{multiple}) { 875 877 my @targets = @{ $body->{forAccounts} || [ $account->{did} ] }; 876 878 @targets = ($account->{did}) unless @targets;
+161 -1
t/uncovered-endpoints.t
··· 125 125 is($handle_event->{type}, 'identity', 'admin.updateAccountHandle appends an identity event'); 126 126 is($handle_event->{payload}{handle}, 'alice.external.test', 'identity event carries the updated handle'); 127 127 128 + $t->get_ok(Mojo::URL->new('/xrpc/com.atproto.admin.getAccountInfos')->query( 129 + dids => [ $did, 'did:web:example.test:users:missing' ], 130 + ) => { 131 + Authorization => $admin_auth, 132 + })->status_is(200) 133 + ->json_is('/infos/0/did' => $did) 134 + ->json_is('/infos/0/handle' => 'alice.external.test'); 135 + 136 + is(scalar @{ $t->tx->res->json->{infos} || [] }, 1, 'getAccountInfos returns only existing accounts'); 137 + 138 + $t->get_ok(Mojo::URL->new('/xrpc/com.atproto.admin.searchAccounts')->query( 139 + email => 'ALICE@EXAMPLE.TEST', 140 + ) => { 141 + Authorization => $admin_auth, 142 + })->status_is(200) 143 + ->json_is('/accounts/0/did' => $did); 144 + 145 + $t->post_ok('/xrpc/com.atproto.admin.updateAccountEmail' => { 146 + Authorization => $admin_auth, 147 + } => json => { 148 + account => $did, 149 + email => 'Alice+Admin@Example.Test', 150 + })->status_is(200) 151 + ->json_is({}); 152 + 153 + $account = $app->store->get_account_by_did($did); 154 + is($account->{email}, 'alice+admin@example.test', 'admin.updateAccountEmail normalizes email'); 155 + ok(!defined($account->{email_confirmed_at}), 'admin.updateAccountEmail clears email confirmation state'); 156 + 157 + $t->get_ok(Mojo::URL->new('/xrpc/com.atproto.admin.getAccountInfo')->query( 158 + did => $did, 159 + ) => { 160 + Authorization => $admin_auth, 161 + })->status_is(200) 162 + ->json_is('/email' => 'alice+admin@example.test'); 163 + 164 + $t->post_ok('/xrpc/com.atproto.admin.sendEmail' => { 165 + Authorization => $admin_auth, 166 + } => json => { 167 + recipientDid => $did, 168 + content => 'hello from perlsky', 169 + })->status_is(200) 170 + ->json_is('/sent' => JSON::PP::true); 171 + 172 + my $outbound = $app->store->dbh->selectrow_hashref( 173 + q{SELECT * FROM outbound_emails WHERE recipient_did = ? ORDER BY id DESC LIMIT 1}, 174 + undef, 175 + $did, 176 + ); 177 + ok($outbound, 'admin.sendEmail logs an outbound email'); 178 + is($outbound->{subject}, 'Message via your PDS', 'admin.sendEmail uses the reference default subject'); 179 + is($outbound->{recipient_email}, 'alice+admin@example.test', 'admin.sendEmail uses the updated normalized email'); 180 + 128 181 $t->post_ok('/xrpc/com.atproto.admin.updateAccountSigningKey' => { 129 182 Authorization => $admin_auth, 130 183 } => json => { ··· 154 207 })->status_is(200) 155 208 ->json_is('/codes/0/account' => 'admin'); 156 209 157 - is(scalar @{ $t->tx->res->json->{codes}[0]{codes} || [] }, 2, 'admin createInviteCodes returns the requested number of codes'); 210 + my @admin_codes = @{ $t->tx->res->json->{codes}[0]{codes} || [] }; 211 + is(scalar @admin_codes, 2, 'admin createInviteCodes returns the requested number of codes'); 212 + 213 + $t->post_ok('/xrpc/com.atproto.admin.disableInviteCodes' => { 214 + Authorization => $admin_auth, 215 + } => json => { 216 + accounts => ['admin'], 217 + })->status_is(400) 218 + ->json_is('/error' => 'InvalidRequest'); 219 + 220 + $t->post_ok('/xrpc/com.atproto.admin.disableInviteCodes' => { 221 + Authorization => $admin_auth, 222 + } => json => { 223 + codes => [$admin_codes[0]], 224 + })->status_is(200) 225 + ->json_is({}); 226 + 227 + ok($app->store->get_invite_code($admin_codes[0])->{disabled}, 'disableInviteCodes marks the requested code disabled'); 158 228 159 229 $t->post_ok('/xrpc/com.atproto.server.createInviteCodes' => { 160 230 Authorization => "Bearer $access", ··· 174 244 })->status_is(400) 175 245 ->json_is('/error' => 'InvalidRequest'); 176 246 247 + $t->post_ok('/xrpc/com.atproto.admin.disableAccountInvites' => { 248 + Authorization => $admin_auth, 249 + } => json => { 250 + account => $did, 251 + note => 'paused for audit', 252 + })->status_is(200) 253 + ->json_is({}); 254 + 255 + $t->get_ok(Mojo::URL->new('/xrpc/com.atproto.admin.getAccountInfo')->query( 256 + did => $did, 257 + ) => { 258 + Authorization => $admin_auth, 259 + })->status_is(200) 260 + ->json_is('/invitesDisabled' => JSON::PP::true) 261 + ->json_is('/inviteNote' => 'paused for audit'); 262 + 263 + $t->post_ok('/xrpc/com.atproto.server.createInviteCode' => { 264 + Authorization => "Bearer $access", 265 + } => json => { 266 + useCount => 1, 267 + })->status_is(400) 268 + ->json_is('/error' => 'InvalidRequest'); 269 + 270 + $t->post_ok('/xrpc/com.atproto.admin.enableAccountInvites' => { 271 + Authorization => $admin_auth, 272 + } => json => { 273 + account => $did, 274 + })->status_is(200) 275 + ->json_is({}); 276 + 277 + $t->post_ok('/xrpc/com.atproto.server.createInviteCode' => { 278 + Authorization => "Bearer $access", 279 + } => json => { 280 + useCount => 1, 281 + })->status_is(200) 282 + ->json_like('/code' => qr/\Aperlsky-/); 283 + 284 + my $user_code = $t->tx->res->json->{code}; 285 + 286 + $t->post_ok('/xrpc/com.atproto.admin.disableInviteCodes' => { 287 + Authorization => $admin_auth, 288 + } => json => { 289 + codes => [$user_code], 290 + })->status_is(200) 291 + ->json_is({}); 292 + 293 + my ($disabled_row) = grep { $_->{code} eq $user_code } @{ $app->store->list_invite_codes_for_account($did) || [] }; 294 + ok($disabled_row && $disabled_row->{disabled}, 'disableInviteCodes disables the requested invite code'); 295 + 296 + $t->post_ok('/xrpc/com.atproto.admin.disableInviteCodes' => { 297 + Authorization => $admin_auth, 298 + } => json => { 299 + accounts => ['admin'], 300 + })->status_is(400) 301 + ->json_is('/error' => 'InvalidRequest'); 302 + 177 303 $t->post_ok('/xrpc/com.atproto.sync.notifyOfUpdate' => json => { 178 304 hostname => 'crawler.example.test', 179 305 })->status_is(200) ··· 184 310 })->status_is(200) 185 311 ->json_is('/hostname' => 'crawler.example.test') 186 312 ->json_is('/status' => 'active'); 313 + 314 + $t->post_ok('/xrpc/com.atproto.admin.sendEmail' => { 315 + Authorization => $admin_auth, 316 + } => json => { 317 + recipientDid => $did, 318 + subject => 'Hello', 319 + content => 'Testing', 320 + })->status_is(200) 321 + ->json_is('/sent' => JSON::PP::true); 322 + 323 + $t->post_ok('/xrpc/com.atproto.server.createAccount' => json => { 324 + handle => 'noemail.example.test', 325 + password => 'hunter22', 326 + })->status_is(200); 327 + 328 + my $noemail_did = $t->tx->res->json->{did}; 329 + 330 + $t->post_ok('/xrpc/com.atproto.admin.sendEmail' => { 331 + Authorization => $admin_auth, 332 + } => json => { 333 + recipientDid => $noemail_did, 334 + subject => 'Hello', 335 + content => 'Testing', 336 + })->status_is(400) 337 + ->json_is('/error' => 'InvalidRequest'); 338 + 339 + $t->post_ok('/xrpc/com.atproto.admin.sendEmail' => { 340 + Authorization => $admin_auth, 341 + } => json => { 342 + recipientDid => 'did:web:example.test:users:missing', 343 + subject => 'Hello', 344 + content => 'Testing', 345 + })->status_is(404) 346 + ->json_is('/error' => 'AccountNotFound'); 187 347 188 348 $t->post_ok('/xrpc/com.atproto.admin.updateAccountPassword' => { 189 349 Authorization => $admin_auth,