Switched Xrpc client to Guzzle.

This commit is contained in:
flash 2025-02-26 14:57:19 +00:00
parent d3bdd8f3a2
commit 676e3fb217
Signed by: flash
GPG key ID: 2C9C2C574D47FE3E
3 changed files with 316 additions and 115 deletions

View file

@ -1,7 +1,6 @@
{
"require": {
"php": ">=8.4",
"ext-curl": "*",
"ext-mbstring": "*",
"flashwave/index": "^0.2501",
"flashii/rpcii": "~4.0",
@ -13,7 +12,8 @@
"nesbot/carbon": "~3.8",
"vlucas/phpdotenv": "~5.6",
"filp/whoops": "~2.17",
"phpseclib/phpseclib": "~3.0"
"phpseclib/phpseclib": "~3.0",
"guzzlehttp/guzzle": "~7.0"
},
"autoload": {
"classmap": [

288
composer.lock generated
View file

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "42e27bedc2ac1251eca61bf7df4f0342",
"content-hash": "1045c8f605203fae19361d659fb64c54",
"packages": [
{
"name": "carbonphp/carbon-doctrine-types",
@ -655,6 +655,215 @@
],
"time": "2024-07-20T21:45:45+00:00"
},
{
"name": "guzzlehttp/guzzle",
"version": "7.9.2",
"source": {
"type": "git",
"url": "https://github.com/guzzle/guzzle.git",
"reference": "d281ed313b989f213357e3be1a179f02196ac99b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/guzzle/guzzle/zipball/d281ed313b989f213357e3be1a179f02196ac99b",
"reference": "d281ed313b989f213357e3be1a179f02196ac99b",
"shasum": ""
},
"require": {
"ext-json": "*",
"guzzlehttp/promises": "^1.5.3 || ^2.0.3",
"guzzlehttp/psr7": "^2.7.0",
"php": "^7.2.5 || ^8.0",
"psr/http-client": "^1.0",
"symfony/deprecation-contracts": "^2.2 || ^3.0"
},
"provide": {
"psr/http-client-implementation": "1.0"
},
"require-dev": {
"bamarni/composer-bin-plugin": "^1.8.2",
"ext-curl": "*",
"guzzle/client-integration-tests": "3.0.2",
"php-http/message-factory": "^1.1",
"phpunit/phpunit": "^8.5.39 || ^9.6.20",
"psr/log": "^1.1 || ^2.0 || ^3.0"
},
"suggest": {
"ext-curl": "Required for CURL handler support",
"ext-intl": "Required for Internationalized Domain Name (IDN) support",
"psr/log": "Required for using the Log middleware"
},
"type": "library",
"extra": {
"bamarni-bin": {
"bin-links": true,
"forward-command": false
}
},
"autoload": {
"files": [
"src/functions_include.php"
],
"psr-4": {
"GuzzleHttp\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Graham Campbell",
"email": "hello@gjcampbell.co.uk",
"homepage": "https://github.com/GrahamCampbell"
},
{
"name": "Michael Dowling",
"email": "mtdowling@gmail.com",
"homepage": "https://github.com/mtdowling"
},
{
"name": "Jeremy Lindblom",
"email": "jeremeamia@gmail.com",
"homepage": "https://github.com/jeremeamia"
},
{
"name": "George Mponos",
"email": "gmponos@gmail.com",
"homepage": "https://github.com/gmponos"
},
{
"name": "Tobias Nyholm",
"email": "tobias.nyholm@gmail.com",
"homepage": "https://github.com/Nyholm"
},
{
"name": "Márk Sági-Kazár",
"email": "mark.sagikazar@gmail.com",
"homepage": "https://github.com/sagikazarmark"
},
{
"name": "Tobias Schultze",
"email": "webmaster@tubo-world.de",
"homepage": "https://github.com/Tobion"
}
],
"description": "Guzzle is a PHP HTTP client library",
"keywords": [
"client",
"curl",
"framework",
"http",
"http client",
"psr-18",
"psr-7",
"rest",
"web service"
],
"support": {
"issues": "https://github.com/guzzle/guzzle/issues",
"source": "https://github.com/guzzle/guzzle/tree/7.9.2"
},
"funding": [
{
"url": "https://github.com/GrahamCampbell",
"type": "github"
},
{
"url": "https://github.com/Nyholm",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/guzzlehttp/guzzle",
"type": "tidelift"
}
],
"time": "2024-07-24T11:22:20+00:00"
},
{
"name": "guzzlehttp/promises",
"version": "2.0.4",
"source": {
"type": "git",
"url": "https://github.com/guzzle/promises.git",
"reference": "f9c436286ab2892c7db7be8c8da4ef61ccf7b455"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/guzzle/promises/zipball/f9c436286ab2892c7db7be8c8da4ef61ccf7b455",
"reference": "f9c436286ab2892c7db7be8c8da4ef61ccf7b455",
"shasum": ""
},
"require": {
"php": "^7.2.5 || ^8.0"
},
"require-dev": {
"bamarni/composer-bin-plugin": "^1.8.2",
"phpunit/phpunit": "^8.5.39 || ^9.6.20"
},
"type": "library",
"extra": {
"bamarni-bin": {
"bin-links": true,
"forward-command": false
}
},
"autoload": {
"psr-4": {
"GuzzleHttp\\Promise\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Graham Campbell",
"email": "hello@gjcampbell.co.uk",
"homepage": "https://github.com/GrahamCampbell"
},
{
"name": "Michael Dowling",
"email": "mtdowling@gmail.com",
"homepage": "https://github.com/mtdowling"
},
{
"name": "Tobias Nyholm",
"email": "tobias.nyholm@gmail.com",
"homepage": "https://github.com/Nyholm"
},
{
"name": "Tobias Schultze",
"email": "webmaster@tubo-world.de",
"homepage": "https://github.com/Tobion"
}
],
"description": "Guzzle promises library",
"keywords": [
"promise"
],
"support": {
"issues": "https://github.com/guzzle/promises/issues",
"source": "https://github.com/guzzle/promises/tree/2.0.4"
},
"funding": [
{
"url": "https://github.com/GrahamCampbell",
"type": "github"
},
{
"url": "https://github.com/Nyholm",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/guzzlehttp/promises",
"type": "tidelift"
}
],
"time": "2024-10-17T10:06:22+00:00"
},
{
"name": "guzzlehttp/psr7",
"version": "2.7.0",
@ -1512,6 +1721,58 @@
},
"time": "2019-01-08T18:20:26+00:00"
},
{
"name": "psr/http-client",
"version": "1.0.3",
"source": {
"type": "git",
"url": "https://github.com/php-fig/http-client.git",
"reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-fig/http-client/zipball/bb5906edc1c324c9a05aa0873d40117941e5fa90",
"reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90",
"shasum": ""
},
"require": {
"php": "^7.0 || ^8.0",
"psr/http-message": "^1.0 || ^2.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.0.x-dev"
}
},
"autoload": {
"psr-4": {
"Psr\\Http\\Client\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "PHP-FIG",
"homepage": "https://www.php-fig.org/"
}
],
"description": "Common interface for HTTP clients",
"homepage": "https://github.com/php-fig/http-client",
"keywords": [
"http",
"http-client",
"psr",
"psr-18"
],
"support": {
"source": "https://github.com/php-fig/http-client"
},
"time": "2023-09-23T14:17:50+00:00"
},
{
"name": "psr/http-factory",
"version": "1.1.0",
@ -2237,16 +2498,16 @@
},
{
"name": "symfony/mime",
"version": "v7.2.3",
"version": "v7.2.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/mime.git",
"reference": "2fc3b4bd67e4747e45195bc4c98bea4628476204"
"reference": "87ca22046b78c3feaff04b337f33b38510fd686b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/mime/zipball/2fc3b4bd67e4747e45195bc4c98bea4628476204",
"reference": "2fc3b4bd67e4747e45195bc4c98bea4628476204",
"url": "https://api.github.com/repos/symfony/mime/zipball/87ca22046b78c3feaff04b337f33b38510fd686b",
"reference": "87ca22046b78c3feaff04b337f33b38510fd686b",
"shasum": ""
},
"require": {
@ -2301,7 +2562,7 @@
"mime-type"
],
"support": {
"source": "https://github.com/symfony/mime/tree/v7.2.3"
"source": "https://github.com/symfony/mime/tree/v7.2.4"
},
"funding": [
{
@ -2317,7 +2578,7 @@
"type": "tidelift"
}
],
"time": "2025-01-27T11:08:17+00:00"
"time": "2025-02-19T08:51:20+00:00"
},
{
"name": "symfony/options-resolver",
@ -2950,16 +3211,16 @@
},
{
"name": "symfony/translation",
"version": "v7.2.2",
"version": "v7.2.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/translation.git",
"reference": "e2674a30132b7cc4d74540d6c2573aa363f05923"
"reference": "283856e6981286cc0d800b53bd5703e8e363f05a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/translation/zipball/e2674a30132b7cc4d74540d6c2573aa363f05923",
"reference": "e2674a30132b7cc4d74540d6c2573aa363f05923",
"url": "https://api.github.com/repos/symfony/translation/zipball/283856e6981286cc0d800b53bd5703e8e363f05a",
"reference": "283856e6981286cc0d800b53bd5703e8e363f05a",
"shasum": ""
},
"require": {
@ -3025,7 +3286,7 @@
"description": "Provides tools to internationalize your application",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/translation/tree/v7.2.2"
"source": "https://github.com/symfony/translation/tree/v7.2.4"
},
"funding": [
{
@ -3041,7 +3302,7 @@
"type": "tidelift"
}
],
"time": "2024-12-07T08:18:10+00:00"
"time": "2025-02-13T10:27:23+00:00"
},
{
"name": "symfony/translation-contracts",
@ -3420,7 +3681,6 @@
"prefer-lowest": false,
"platform": {
"php": ">=8.4",
"ext-curl": "*",
"ext-mbstring": "*"
},
"platform-dev": {},

View file

@ -1,36 +1,28 @@
<?php
namespace Misuzu\ATProto;
use CurlHandle;
use InvalidArgumentException;
use RuntimeException;
use UnexpectedValueException;
use GuzzleHttp\Client as HttpClient;
use GuzzleHttp\Exception\RequestException;
use GuzzleHttp\Psr7\Request as HttpRequest;
use Misuzu\Misuzu;
class XrpcClient {
private readonly CurlHandle $handle;
private readonly string $userAgent;
private readonly HttpClient $httpClient;
public function __construct(
private string $service
) {
public function __construct(string $service) {
if(!filter_var($service, FILTER_VALIDATE_URL))
throw new InvalidArgumentException('$service must be a valid URL');
$handle = curl_init();
if($handle === false)
throw new RuntimeException('Failed to initialise cURL.');
$version = curl_version();
if(!is_array($version) || !array_key_exists('version', $version) || !is_string($version['version']))
throw new RuntimeException('Failed to retrieve cURL version info.');
$this->handle = $handle;
$this->userAgent = sprintf('Misuzu/%s cURL/%s', Misuzu::version(), $version['version']);
}
public function __destruct() {
curl_close($this->handle);
$this->httpClient = new HttpClient([
'allow_redirects' => true,
'base_uri' => sprintf('%s/xrpc/', rtrim($service, '/')),
'timeout' => 10,
'headers' => [
'User-Agent' => sprintf('Misuzu/%s', Misuzu::version()),
],
]);
}
/**
@ -46,98 +38,47 @@ class XrpcClient {
array $params,
array|object|string|null $data
): object {
$url = sprintf('%s/xrpc/%s', $this->service, $nsid);
if(!empty($params))
$url = sprintf('%s?%s', $url, http_build_query($params, encoding_type: PHP_QUERY_RFC3986));
$request = new HttpRequest($method, $nsid, $headers);
$options = [
'query' => $params,
];
$headers = (function($original) {
$sanitised = [];
foreach($original as $name => $value) {
$name = strtolower($name);
if(!array_key_exists($name, $sanitised))
$sanitised[$name] = $value;
if(is_object($data) || is_array($data))
$options['json'] = $data;
elseif(is_string($data))
$options['body'] = $data;
try {
$response = $this->httpClient->send($request, $options);
$statusCode = $response->getStatusCode();
} catch(RequestException $ex) {
$response = $ex->getResponse();
$statusCode = $ex->getCode();
}
return $sanitised;
})($headers);
if(is_object($data) || is_array($data)) {
$headers['content-type'] = 'application/json';
$data = json_encode($data);
}
curl_reset($this->handle);
curl_setopt_array($this->handle, [
CURLOPT_AUTOREFERER => true,
CURLOPT_CUSTOMREQUEST => $method,
CURLOPT_FAILONERROR => false,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_HEADER => true,
CURLOPT_HTTPHEADER => (function($headers) {
$lines = [];
foreach($headers as $name => $value)
$lines[] = sprintf('%s: %s', $name, $value);
return $lines;
})($headers),
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TCP_FASTOPEN => true,
CURLOPT_CONNECTTIMEOUT => 5,
CURLOPT_MAXREDIRS => 5,
CURLOPT_POSTFIELDS => $data,
CURLOPT_PROTOCOLS => CURLPROTO_HTTPS,
CURLOPT_TIMEOUT => 10,
CURLOPT_URL => $url,
CURLOPT_USERAGENT => $this->userAgent,
]);
$response = curl_exec($this->handle);
if($response === false)
throw new RuntimeException(curl_error($this->handle), curl_errno($this->handle));
if($response === true)
throw new UnexpectedValueException('Request executed successfully but cURL returned no data.');
$parts = explode("\r\n\r\n", $response, 2);
$headerLines = array_reverse(explode("\r\n", $parts[0]));
$statusLine = array_pop($headerLines);
if(!is_string($statusLine))
throw new RuntimeException('Unable to read status header.');
$statusLineScan = sscanf($statusLine, 'HTTP/%s %d %[^\t\r\n]');
if($statusLineScan === null)
throw new RuntimeException('Failed to decode HTTP status line.');
[$_, $statusCode, $statusLine] = $statusLineScan;
$statusCode = (int)$statusCode;
$headers = (function(array $raw) {
$headers = [];
while(($headerLine = array_pop($headerLines)) !== null) {
$headerLine = trim((string)$headerLine);
if($headerLine === '')
continue;
$headerParts = explode(':', $headerLine, 2);
$headers[strtolower(trim($headerParts[0]))] = count($headerParts) > 1 ? trim($headerParts[1]) : '';
}
foreach($raw as $name => $value)
$headers[strtolower($name)] = implode(', ', $value);
return $headers;
})($response->getHeaders());
$body = (string)$response->getBody();
if(array_key_exists('content-type', $headers) && str_starts_with($headers['content-type'], 'application/json'))
$data = json_decode($parts[1]);
else
$data = $parts[1];
$body = json_decode($body);
if($statusCode !== 200) {
if(is_object($data)
&& isset($data->error) && is_string($data->error)
&& isset($data->message) && is_string($data->message))
throw new RuntimeException(sprintf('%s: %s', $data->error, $data->message), $statusCode);
if(is_object($body)
&& isset($body->error) && is_string($body->error)
&& isset($body->message) && is_string($body->message))
throw new RuntimeException(sprintf('%s: %s', $body->error, $body->message), $statusCode);
throw new RuntimeException(sprintf('HTTP %03d', $statusCode), $statusCode);
}
return (object)[
'headers' => $headers,
'data' => $data,
'data' => $body,
];
}