-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Fixes #22 -- Implement VRC API directly in PHP for future VRC API calls.
- Loading branch information
1 parent
83dc9a2
commit ee60052
Showing
9 changed files
with
331 additions
and
103 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,67 +2,49 @@ | |
|
||
namespace App\Service; | ||
|
||
use App\Environment; | ||
use App\Service\VrcApi\AuthMiddleware; | ||
use App\Service\VrcApi\RateLimitStore; | ||
use GuzzleHttp\Client; | ||
use GuzzleHttp\HandlerStack; | ||
use GuzzleHttp\MessageFormatter; | ||
use GuzzleHttp\Middleware; | ||
use Psr\Log\LoggerInterface; | ||
use Psr\Log\LogLevel; | ||
use Spatie\GuzzleRateLimiterMiddleware\RateLimiterMiddleware; | ||
|
||
final readonly class VrcApi | ||
{ | ||
public const string VRCAPI_BASE_URL = 'https://api.vrchat.cloud/api/1/'; | ||
|
||
private Client $httpClient; | ||
|
||
public function __construct( | ||
private Client $http | ||
LoggerInterface $logger, | ||
AuthMiddleware $authMiddleware, | ||
RateLimitStore $rateLimitStore | ||
) { | ||
} | ||
|
||
/** | ||
* Send a request to the API proxy server | ||
* | ||
* @param string $method The HTTP method to perform (POST, GET, PUT, etc) | ||
* @param string $path The path to use (/api/1/visits etc) | ||
* @param string|null $body The HTTP body (if any, set to null if not wanted) | ||
* @param bool $priority If the request should use the high-priority queue | ||
* @param bool $async If the request should be async (non-failable, ideal for friend requests/invite requests) | ||
* @return string|array | ||
*/ | ||
public function sendRequest( | ||
string $method, | ||
string $path, | ||
?string $body = null, | ||
bool $priority = false, | ||
bool $async = false | ||
): string|array { | ||
/** @noinspection HttpUrlsUsage */ | ||
$uri = 'http://149.106.100.136:8124/' . $path; | ||
$stack = HandlerStack::create(); | ||
$stack->push( | ||
Middleware::log( | ||
$logger, | ||
new MessageFormatter('VRCAPI client {method} call to {uri} produced response {code}'), | ||
LogLevel::DEBUG | ||
) | ||
); | ||
$stack->push(RateLimiterMiddleware::perSecond(1, $rateLimitStore)); | ||
$stack->push($authMiddleware); | ||
|
||
$requestConfig = [ | ||
$this->httpClient = new Client([ | ||
'handler' => $stack, | ||
'base_uri' => self::VRCAPI_BASE_URL, | ||
'headers' => [ | ||
'Authorization' => $_ENV['VRCHAT_API_KEY'], | ||
'User-Agent' => 'WaterWolf/1.0 [email protected]', | ||
], | ||
]; | ||
|
||
if ($priority) { | ||
$requestConfig['headers']['X-Priority'] = 'high'; | ||
} | ||
if ($async) { | ||
$requestConfig['headers']['X-Background'] = '1'; | ||
} | ||
|
||
if ($body !== null) { | ||
$requestConfig['json'] = $body; | ||
} | ||
|
||
$response = $this->http->request( | ||
$method, | ||
$uri, | ||
$requestConfig | ||
); | ||
]); | ||
} | ||
|
||
if ($response->getHeaderLine('Content-Type') === 'application/json') { | ||
return json_decode( | ||
$response->getBody()->getContents(), | ||
true, | ||
JSON_THROW_ON_ERROR | ||
); | ||
} else { | ||
return $response->getBody()->getContents(); | ||
} | ||
public function getHttpClient(): Client | ||
{ | ||
return $this->httpClient; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,108 @@ | ||
<?php | ||
|
||
namespace App\Service\VrcApi; | ||
|
||
use App\Environment; | ||
use App\Service\VrcApi; | ||
use GuzzleHttp\Client; | ||
use GuzzleHttp\Cookie\CookieJar; | ||
use GuzzleHttp\Cookie\FileCookieJar; | ||
use GuzzleHttp\Cookie\SetCookie; | ||
use OTPHP\TOTP; | ||
use Psr\Http\Message\RequestInterface; | ||
|
||
final readonly class AuthMiddleware | ||
{ | ||
public const string VRC_AUTH_COOKIE_NAME = 'auth'; | ||
public const string VRC_TOTP_COOKIE_NAME = 'twoFactorAuth'; | ||
|
||
private string|null $username; | ||
private string|null $password; | ||
private string|null $totp; | ||
|
||
private CookieJar $cookieJar; | ||
|
||
public function __construct( | ||
private Client $httpClient | ||
) { | ||
$this->username = $_ENV['VRCHAT_USERNAME'] ?? null; | ||
$this->password = $_ENV['VRCHAT_PASSWORD'] ?? null; | ||
$this->totp = $_ENV['VRCHAT_TOTP'] ?? null; | ||
|
||
$this->cookieJar = new FileCookieJar(Environment::getTempDirectory() . '/vrcapi_cookies'); | ||
} | ||
|
||
public function __invoke(callable $next): \Closure | ||
{ | ||
return function (RequestInterface $request, array $options = []) use (&$next) { | ||
$request = $this->applyAuth($request); | ||
return $next($request, $options); | ||
}; | ||
} | ||
|
||
protected function applyAuth(RequestInterface $request): RequestInterface | ||
{ | ||
if (!$this->hasValidCookies()) { | ||
$this->reAuth(); | ||
} | ||
|
||
return $this->cookieJar->withCookieHeader($request); | ||
} | ||
|
||
private function reAuth(): void | ||
{ | ||
$options = [ | ||
'base_uri' => VrcApi::VRCAPI_BASE_URL, | ||
'cookies' => $this->cookieJar, | ||
'headers' => [ | ||
'Authorization' => 'Basic ' . base64_encode( | ||
urlencode($this->username) . ':' . urlencode($this->password) | ||
), | ||
], | ||
]; | ||
|
||
if (!empty($this->totp)) { | ||
$totp = TOTP::createFromSecret(str_replace(' ', '', strtoupper($this->totp))); | ||
$this->httpClient->post( | ||
'auth/twofactorauth/totp/verify', | ||
[ | ||
...$options, | ||
'json' => [ | ||
'code' => $totp->now(), | ||
], | ||
] | ||
); | ||
} else { | ||
$this->httpClient->get( | ||
'auth/user', | ||
$options | ||
); | ||
} | ||
} | ||
|
||
private function hasValidCookies(): bool | ||
{ | ||
if ($this->cookieJar->count() === 0) { | ||
return false; | ||
} | ||
|
||
$cookieNames = []; | ||
|
||
/** @var SetCookie $cookie */ | ||
foreach ($this->cookieJar->getIterator() as $cookie) { | ||
if (!$cookie->isExpired()) { | ||
$cookieNames[$cookie->getName()] = $cookie; | ||
} | ||
} | ||
|
||
if (!isset($cookieNames[self::VRC_AUTH_COOKIE_NAME])) { | ||
return false; | ||
} | ||
|
||
if (!empty($this->totp) && !isset($cookieNames[self::VRC_TOTP_COOKIE_NAME])) { | ||
return false; | ||
} | ||
|
||
return true; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
<?php | ||
|
||
namespace App\Service\VrcApi; | ||
|
||
use Psr\SimpleCache\CacheInterface; | ||
use Spatie\GuzzleRateLimiterMiddleware\Store; | ||
|
||
final readonly class RateLimitStore implements Store | ||
{ | ||
public const string CACHE_KEY = 'vrcapi_rate_limit'; | ||
|
||
public function __construct( | ||
private CacheInterface $psrCache | ||
) { | ||
} | ||
|
||
public function get(): array | ||
{ | ||
return $this->psrCache->get(self::CACHE_KEY, []); | ||
} | ||
|
||
public function push(int $timestamp, int $limit): void | ||
{ | ||
$entries = $this->get(); | ||
$entries[] = $timestamp; | ||
|
||
$this->psrCache->set(self::CACHE_KEY, $entries, $limit); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.