diff --git a/app/Console/Commands/UserAccountDelete.php b/app/Console/Commands/UserAccountDelete.php index c12d88e4e..1787a35f7 100644 --- a/app/Console/Commands/UserAccountDelete.php +++ b/app/Console/Commands/UserAccountDelete.php @@ -22,6 +22,7 @@ class UserAccountDelete extends Command {--concurrency=500 : Number of concurrent deliveries} {--chunk=500 : Number of inbox rows to process per DB chunk} {--attempts=2 : Max attempts for retryable failures} + {--target= : Send to a single inbox URL for debugging} {--dry-run : Build payload and audience, but do not send}'; protected $description = 'Federate Account Deletion'; @@ -112,6 +113,10 @@ class UserAccountDelete extends Command return self::SUCCESS; } + if ($target = $this->option('target')) { + return $this->sendDebug($target, $payload, $digest); + } + if ($totalTargets === 0) { $this->warn('No candidate shared inboxes found.'); @@ -279,19 +284,15 @@ class UserAccountDelete extends Command string $payload, int $concurrency ): array { - $version = config('pixelfed.version'); - $appUrl = config('app.url'); - $userAgent = "(Pixelfed/{$version}; +{$appUrl})"; $delivered = []; $httpFailed = []; $retryable = collect(); - $requests = function () use ($client, $urls, $digest, $payload, $userAgent) { + $requests = function () use ($client, $urls, $digest, $payload) { foreach ($urls as $url) { $headers = HttpSignature::instanceActorSignWithDigest($url, $digest, [ 'Content-Type' => 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"', - 'User-Agent' => $userAgent, ]); yield function () use ($client, $url, $headers, $payload) { @@ -346,6 +347,65 @@ class UserAccountDelete extends Command ]; } + protected function sendDebug(string $url, string $payload, string $digest): int + { + $version = config('pixelfed.version'); + $appUrl = config('app.url'); + $userAgent = "(Pixelfed/{$version}; +{$appUrl})"; + + $headers = HttpSignature::instanceActorSignWithDigest($url, $digest, [ + 'Content-Type' => 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"', + ]); + + $this->info('Target: '.$url); + $this->newLine(); + + $this->info('Request headers:'); + foreach ($headers as $key => $value) { + $this->line(" {$key}: {$value}"); + } + $this->newLine(); + + $this->info('Payload:'); + $this->line($payload); + $this->newLine(); + + $client = new Client([ + 'timeout' => 15.0, + 'connect_timeout' => 5.0, + 'http_errors' => false, + 'allow_redirects' => false, + ]); + + try { + $response = $client->post($url, [ + 'headers' => $headers, + 'body' => $payload, + ]); + + $status = $response->getStatusCode(); + $body = (string) $response->getBody(); + + $this->info("Response status: {$status}"); + $this->newLine(); + + $this->info('Response headers:'); + foreach ($response->getHeaders() as $name => $values) { + $this->line(" {$name}: ".implode(', ', $values)); + } + $this->newLine(); + + $this->info('Response body:'); + $this->line($body ?: '(empty)'); + + return $status >= 200 && $status < 300 ? self::SUCCESS : self::FAILURE; + } catch (\Throwable $e) { + $this->error("Transport error: {$e->getMessage()}"); + + return self::FAILURE; + } + } + protected function isRetryableStatus(int $status): bool { return in_array($status, [408, 425, 429, 500, 502, 503, 504], true); diff --git a/app/Util/ActivityPub/HttpSignature.php b/app/Util/ActivityPub/HttpSignature.php index 054e5aa88..adb6dbd01 100644 --- a/app/Util/ActivityPub/HttpSignature.php +++ b/app/Util/ActivityPub/HttpSignature.php @@ -130,7 +130,7 @@ class HttpSignature unset($headers['(request-target)']); $headers['Signature'] = $signatureHeader; - return $headers; + return self::_headersToCurlArray($headers); } public static function parseSignatureHeader($signature)