diff --git a/VERSION b/VERSION index 17e51c3..0ea3a94 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.1.1 +0.2.0 diff --git a/src/AuthorizedHttpClient.php b/src/AuthorizedHttpClient.php index dfd14f8..567b29c 100644 --- a/src/AuthorizedHttpClient.php +++ b/src/AuthorizedHttpClient.php @@ -6,19 +6,21 @@ namespace Flashii; use Flashii\Credentials\Credentials; -use Flashii\Http\{HttpClient,HttpRequest,HttpResponse}; +use Flashii\Http\{HttpClient,HttpRequest,HttpResponse,RequestSpeedrun}; /** * Provides a proxy HttpClient that ensures the HTTP Authorization header is set on requests. */ class AuthorizedHttpClient implements HttpClient { + use RequestSpeedrun; + /** * @param HttpClient $httpClient Underlying HttpClient implementation. * @param Credentials $credentials Credentials to use for the Authorization header. */ public function __construct( - private HttpClient $httpClient, - private Credentials $credentials + private readonly HttpClient $httpClient, + private readonly Credentials $credentials ) {} /** diff --git a/src/Credentials/BasicCredentials.php b/src/Credentials/BasicCredentials.php index 33b238a..aa29af9 100644 --- a/src/Credentials/BasicCredentials.php +++ b/src/Credentials/BasicCredentials.php @@ -9,14 +9,14 @@ namespace Flashii\Credentials; * Implements basic HTTP credentials for OAuth2 apps. */ class BasicCredentials implements Credentials { - private string $authz; + private readonly string $authz; /** * @param string $userName User name/Client ID. * @param string $password Password/Client secret. */ public function __construct( - private string $userName, + private readonly string $userName, #[\SensitiveParameter] string $password = '' ) { diff --git a/src/Credentials/BearerCredentials.php b/src/Credentials/BearerCredentials.php index 8dd3c60..c05215e 100644 --- a/src/Credentials/BearerCredentials.php +++ b/src/Credentials/BearerCredentials.php @@ -9,7 +9,7 @@ namespace Flashii\Credentials; * Implements bearer HTTP credentials for OAuth2 access tokens. */ class BearerCredentials implements Credentials { - private string $authz; + private readonly string $authz; /** * @param string $accessToken Bearer access token. diff --git a/src/Credentials/MisuzuCredentials.php b/src/Credentials/MisuzuCredentials.php index 1eb96aa..8c20575 100644 --- a/src/Credentials/MisuzuCredentials.php +++ b/src/Credentials/MisuzuCredentials.php @@ -11,7 +11,7 @@ namespace Flashii\Credentials; * Do not use this unless you're flashwave!!!!!!! bad!!!!!!!! */ class MisuzuCredentials implements Credentials { - private string $authz; + private readonly string $authz; /** * @param string $sessionToken Misuzu session token. diff --git a/src/FlashiiClient.php b/src/FlashiiClient.php index 51ec461..a00d8d8 100644 --- a/src/FlashiiClient.php +++ b/src/FlashiiClient.php @@ -11,6 +11,7 @@ use Flashii\Http\HttpClient; use Flashii\Http\Curl\CurlHttpClient; use Flashii\Http\Stream\StreamHttpClient; use Flashii\OAuth2\OAuth2Client; +use Flashii\V1\V1Client; /** * Flashii API client. @@ -37,12 +38,13 @@ class FlashiiClient { */ public const LIB_PATH_VERSION = self::LIB_PATH_ROOT . DIRECTORY_SEPARATOR . 'VERSION'; - private AuthorizedHttpClient $httpClient; - private Credentials $credentials; - private FlashiiUrls $urls; + private readonly AuthorizedHttpClient $httpClient; + private readonly Credentials $credentials; + private readonly FlashiiUrls $urls; // Use newLazyGhost to initialise these once PHP 8.4 can be targeted proper private ?OAuth2Client $oauth2 = null; + private ?V1Client $v1 = null; /** * @param string $userAgentOrHttpClient User agent string for your application or a HttpClient implementation, providing a string will autodetect whether to use cURL or PHP streams. @@ -88,6 +90,20 @@ class FlashiiClient { return $this->oauth2; } + /** + * Retreives the APIv1 client portion. + * + * @return V1Client + */ + public function v1(): V1Client { + $this->v1 ??= new V1Client( + $this->httpClient, + $this->urls + ); + + return $this->v1; + } + /** * Fork this API client with different credentials. * diff --git a/src/FlashiiUrls.php b/src/FlashiiUrls.php index a8b6990..50f220d 100644 --- a/src/FlashiiUrls.php +++ b/src/FlashiiUrls.php @@ -28,8 +28,8 @@ final class FlashiiUrls { * @param string $idUrl ID service base URL, without trailing /. */ public function __construct( - private string $apiUrl, - private string $idUrl + private readonly string $apiUrl, + private readonly string $idUrl ) {} /** diff --git a/src/Http/Curl/CurlHttpClient.php b/src/Http/Curl/CurlHttpClient.php index 7e80921..adaaf1e 100644 --- a/src/Http/Curl/CurlHttpClient.php +++ b/src/Http/Curl/CurlHttpClient.php @@ -8,20 +8,23 @@ namespace Flashii\Http\Curl; use CurlHandle; use InvalidArgumentException; use RuntimeException; -use Flashii\Http\{HttpClient,HttpRequest,HttpResponse}; +use Flashii\Http\{HttpClient,HttpRequest,HttpResponse,RequestSpeedrun}; /** * cURL extension backed HTTP client implementation. */ class CurlHttpClient implements HttpClient { - private CurlHandle $handle; + use RequestSpeedrun; + + private readonly string $userAgent; + private readonly CurlHandle $handle; /** * @param string $userAgent User-Agent string. * @throws RuntimeException If cURL initialisation failed. */ public function __construct( - private string $userAgent + string $userAgent ) { $handle = curl_init(); if($handle === false) diff --git a/src/Http/Curl/CurlHttpResponse.php b/src/Http/Curl/CurlHttpResponse.php index 04bbe5c..3d82858 100644 --- a/src/Http/Curl/CurlHttpResponse.php +++ b/src/Http/Curl/CurlHttpResponse.php @@ -19,10 +19,10 @@ class CurlHttpResponse implements HttpResponse { * @param string $body HTTP body. */ public function __construct( - private int $statusCode, - private string $statusText, - private array $headers, - private string $body + private readonly int $statusCode, + private readonly string $statusText, + private readonly array $headers, + private readonly string $body ) {} public function getStatusCode(): int { diff --git a/src/Http/HttpClient.php b/src/Http/HttpClient.php index dc47763..9367ed0 100644 --- a/src/Http/HttpClient.php +++ b/src/Http/HttpClient.php @@ -27,8 +27,22 @@ interface HttpClient { * * @param HttpRequest $request Request to send. * @throws InvalidArgumentException If $request is not understood by the HttpClient implementation. - * @throws RuntimeException If the HTTP request failed in an unexpected way. + * @throws RuntimeException If the request failed in an unexpected way. * @return HttpResponse */ function sendRequest(HttpRequest $request): HttpResponse; + + /** + * Combines createRequest and sendRequest into one method call. + * + * Less noise for when we don't need to touch HttpRequest :) + * + * @param string $method Desired request method. + * @param string $url Target URL. + * @throws InvalidArgumentException If $request is not understood by the HttpClient implementation. + * @throws RuntimeException If the request could not be initialised. + * @throws RuntimeException If the request failed in an unexpected way. + * @return HttpResponse + */ + function request(string $method, string $url): HttpResponse; } diff --git a/src/Http/JsonBody.php b/src/Http/JsonBody.php index 5945fe1..0c4da66 100644 --- a/src/Http/JsonBody.php +++ b/src/Http/JsonBody.php @@ -5,7 +5,6 @@ namespace Flashii\Http; -use RuntimeException; /** * Common implementation for the getJsonBody method of HttpResponse. */ diff --git a/src/Http/RequestSpeedrun.php b/src/Http/RequestSpeedrun.php new file mode 100644 index 0000000..5608487 --- /dev/null +++ b/src/Http/RequestSpeedrun.php @@ -0,0 +1,30 @@ +sendRequest($this->createRequest($method, $url)); + } +} diff --git a/src/Http/Stream/StreamHttpClient.php b/src/Http/Stream/StreamHttpClient.php index 1680eee..ed98c1c 100644 --- a/src/Http/Stream/StreamHttpClient.php +++ b/src/Http/Stream/StreamHttpClient.php @@ -7,17 +7,19 @@ namespace Flashii\Http\Stream; use InvalidArgumentException; use RuntimeException; -use Flashii\Http\{HttpClient,HttpRequest,HttpResponse}; +use Flashii\Http\{HttpClient,HttpRequest,HttpResponse,RequestSpeedrun}; /** * PHP stream backed HTTP client implementation. */ class StreamHttpClient implements HttpClient { + use RequestSpeedrun; + /** * @param string $userAgent User-Agent string. */ public function __construct( - private string $userAgent + private readonly string $userAgent ) {} public function createRequest(string $method, string $url): HttpRequest { diff --git a/src/Http/Stream/StreamHttpResponse.php b/src/Http/Stream/StreamHttpResponse.php index 681f2c0..fbc06b1 100644 --- a/src/Http/Stream/StreamHttpResponse.php +++ b/src/Http/Stream/StreamHttpResponse.php @@ -19,10 +19,10 @@ class StreamHttpResponse implements HttpResponse { * @param string $body HTTP body. */ public function __construct( - private int $statusCode, - private string $statusText, - private array $headers, - private string $body + private readonly int $statusCode, + private readonly string $statusText, + private readonly array $headers, + private readonly string $body ) {} public function getStatusCode(): int { diff --git a/src/Http/SuccessStatusCode.php b/src/Http/SuccessStatusCode.php index a7d2181..f9e059a 100644 --- a/src/Http/SuccessStatusCode.php +++ b/src/Http/SuccessStatusCode.php @@ -6,6 +6,7 @@ namespace Flashii\Http; use RuntimeException; + /** * Common implementation for the SuccessStatusCode methods of HttpResponse. */ diff --git a/src/OAuth2/OAuth2AuthorizationRequest.php b/src/OAuth2/OAuth2AuthorizationRequest.php index f4a571f..6153fdd 100644 --- a/src/OAuth2/OAuth2AuthorizationRequest.php +++ b/src/OAuth2/OAuth2AuthorizationRequest.php @@ -18,12 +18,12 @@ final class OAuth2AuthorizationRequest { * @param int $interval Interval of seconds to check at. */ public function __construct( - private string $deviceCode, - private string $userCode, - private string $verificationUri, - private string $verificationUriComplete, - private int $expiresIn, - private int $interval + private readonly string $deviceCode, + private readonly string $userCode, + private readonly string $verificationUri, + private readonly string $verificationUriComplete, + private readonly int $expiresIn, + private readonly int $interval ) {} public function getDeviceCode(): string { diff --git a/src/OAuth2/OAuth2Client.php b/src/OAuth2/OAuth2Client.php index 37581ad..4e290f4 100644 --- a/src/OAuth2/OAuth2Client.php +++ b/src/OAuth2/OAuth2Client.php @@ -21,10 +21,10 @@ class OAuth2Client { * @param FlashiiUrls $urls API URLs. */ public function __construct( - private FlashiiClient $flashiiClient, - private AuthorizedHttpClient $httpClient, - private Credentials $credentials, - private FlashiiUrls $urls + private readonly FlashiiClient $flashiiClient, + private readonly AuthorizedHttpClient $httpClient, + private readonly Credentials $credentials, + private readonly FlashiiUrls $urls ) {} /** diff --git a/src/OAuth2/OAuth2Error.php b/src/OAuth2/OAuth2Error.php index 5b3d3b3..f804419 100644 --- a/src/OAuth2/OAuth2Error.php +++ b/src/OAuth2/OAuth2Error.php @@ -15,9 +15,9 @@ final class OAuth2Error { * @param string $uri Error URI. */ public function __construct( - private string $error, - private string $description, - private string $uri + private readonly string $error, + private readonly string $description, + private readonly string $uri ) {} /** diff --git a/src/OAuth2/OAuth2Token.php b/src/OAuth2/OAuth2Token.php index 71c157c..40a81ee 100644 --- a/src/OAuth2/OAuth2Token.php +++ b/src/OAuth2/OAuth2Token.php @@ -5,7 +5,6 @@ namespace Flashii\OAuth2; -use Flashii\Credentials\BearerCredentials; /** * OAuth2 token response. */ @@ -18,11 +17,11 @@ final class OAuth2Token { * @param string $refreshToken Refresh token. */ public function __construct( - private string $accessToken, - private string $tokenType, - private int $expiresIn, - private string $scope, - private string $refreshToken + private readonly string $accessToken, + private readonly string $tokenType, + private readonly int $expiresIn, + private readonly string $scope, + private readonly string $refreshToken ) {} public function getAccessToken(): string { diff --git a/src/V1/Emotes/V1Emote.php b/src/V1/Emotes/V1Emote.php new file mode 100644 index 0000000..bc1cc57 --- /dev/null +++ b/src/V1/Emotes/V1Emote.php @@ -0,0 +1,84 @@ +url; + } + + /** + * Strings that the emote should replace, surrounding colon characters omitted. + * + * @return string[] + */ + public function getStrings(): array { + return $this->strings; + } + + /** + * Internal unique ID of the emote. May be blank on some requests. + * + * @return string + */ + public function getId(): string { + return $this->id; + } + + /** + * Ordering of the emote. May be blank on some requests. + * + * @return int + */ + public function getOrder(): int { + return $this->order; + } + + /** + * Decodes an object into this proper object. + * + * @param object $info + * @return V1Emote + */ + public static function decode(object $info): self { + return new static( + isset($info->url) && is_string($info->url) ? $info->url : '', + isset($info->strings) && is_array($info->strings) ? $info->strings : [], // @phpstan-ignore-line: complains because the constructor expects array but its just getting array, not sure how to solve this + isset($info->id) && is_string($info->id) ? $info->id : '', + isset($info->order) && is_int($info->order) ? $info->order : 0 + ); + } + + /** + * Creates a blank emote instance. + * + * @return V1Emote + */ + public static function empty(): self { + return new static('', [], '', 0); + } +} diff --git a/src/V1/Emotes/V1EmotesClient.php b/src/V1/Emotes/V1EmotesClient.php new file mode 100644 index 0000000..cb831a1 --- /dev/null +++ b/src/V1/Emotes/V1EmotesClient.php @@ -0,0 +1,53 @@ +httpClient->request('GET', $this->urls->getApiUrl('/v1/emotes', $args)); + $response->ensureSuccessStatusCode(); + + $emotes = $response->getJsonBody(); + if(!is_array($emotes)) + return []; + + return array_map( + fn(mixed $item) => (is_object($item) ? V1Emote::decode($item) : V1Emote::empty()), + $emotes + ); + } +} diff --git a/src/V1/Users/V1User.php b/src/V1/Users/V1User.php new file mode 100644 index 0000000..f6a06e3 --- /dev/null +++ b/src/V1/Users/V1User.php @@ -0,0 +1,277 @@ +id; + } + + /** + * Gets user name. + * + * @return string + */ + public function getName(): string { + return $this->name; + } + + /** + * Whether a raw 24-bit RGB colour is present. + * + * @return bool + */ + public function hasColourRaw(): bool { + return $this->colourRaw !== null; + } + + /** + * Gets raw 24-bit RGB colour value. + * + * @return int + */ + public function getColourRaw(): int { + return $this->colourRaw ?? 0; + } + + /** + * Gets CSS colour. + * + * @return string + */ + public function getColourCss(): string { + return $this->colourCss; + } + + /** + * Gets user rank. + * + * @return int + */ + public function getRank(): int { + return $this->rank; + } + + /** + * Gets user ISO 3166-1 alpha-2 (adjacent) country code. + * + * @return string + */ + public function getCountryCode(): string { + return $this->countryCode; + } + + /** + * Gets all avatar URIs. + * + * @return V1UserAvatar[] + */ + public function getAvatarUrls(): array { + return $this->avatarUrls; + } + + /** + * Gets sorted avatar list URIs without the original URI. + * + * @return V1UserAvatar[] + */ + private function getAvatarSorted(): array { + if($this->avatarsSorted === null) { + $avatars = []; + foreach($this->avatarUrls as $avatarUrl) + if(!$avatarUrl->isOriginalImage()) + $avatars[] = $avatarUrl; + + usort($avatars, fn($a, $b) => $a->getResolution() - $b->getResolution()); + $this->avatarsSorted = $avatars; + } + + return $this->avatarsSorted; + } + + /** + * Selected the most appropriate avatar resolution URI. + * + * @param int $res Target resolution + * @return string Avatar URI, empty if none found. + */ + public function selectAvatarUrl(int $res): string { + $selected = null; + $avatars = $this->getAvatarSorted(); + foreach($avatars as $avatar) + if($selected === null || abs($res - $selected->getResolution()) >= abs($avatar->getResolution() - $res)) + $selected = $avatar; + + return $selected?->getUrl() ?? ''; + } + + /** + * Gets the URI of the original avatar image, empty if none found. + * + * @return string + */ + public function getOriginalAvatarUrl(): string { + foreach($this->avatarUrls as $avatarUrl) + if($avatarUrl->isOriginalImage()) + return $avatarUrl->getUrl(); + + return ''; + } + + /** + * Gets user profile URI. + * + * @return string + */ + public function getProfileUrl(): string { + return $this->profileUrl; + } + + /** + * Gets user registration date/time. + * + * @return DateTimeInterface + */ + public function getCreatedAt(): DateTimeInterface { + return $this->createdAt; + } + + /** + * Whether the user has a last active date. + * + * @return bool + */ + public function hasLastActiveAt(): bool { + return $this->lastActiveAt !== null; + } + + /** + * Gets user last active date. + * + * @return DateTimeInterface + */ + public function getLastActiveAt(): DateTimeInterface { + return $this->lastActiveAt ?? new DateTimeImmutable('@0'); + } + + /** + * Gets user role strings. + * + * @return string[] + */ + public function getRoles(): array { + return $this->roles; + } + + /** + * Checks whether the user has a given role string. + * + * @param string $role Case-insensitive role string. + * @return bool + */ + public function hasRole(string $role): bool { + return in_array(strtolower($role), $this->roles); + } + + /** + * Gets user title. + * + * @return string + */ + public function getTitle(): string { + return $this->title; + } + + /** + * Whether this is a super user. + * + * @return bool + */ + public function isSuper(): bool { + return $this->super; + } + + /** + * Whether this user is deleted. + * + * @return bool + */ + public function isDeleted(): bool { + return $this->deleted; + } + + /** + * Decodes an object into this proper object. + * + * @param object $info + * @return V1User + */ + public static function decode(object $info): self { + return new static( + isset($info->id) && is_string($info->id) ? $info->id : '', + isset($info->name) && is_string($info->name) ? $info->name : '', + isset($info->colour_raw) && is_int($info->colour_raw) ? $info->colour_raw : null, + isset($info->colour_css) && is_string($info->colour_css) ? $info->colour_css : '', + isset($info->rank) && is_int($info->rank) ? $info->rank : 0, + isset($info->country_code) && is_string($info->country_code) ? $info->country_code : '', + isset($info->avatar_urls) && is_array($info->avatar_urls) ? array_map(fn(mixed $item) => (is_object($item) ? V1UserAvatar::decode($item) : V1UserAvatar::empty()), $info->avatar_urls) : [], + isset($info->profile_url) && is_string($info->profile_url) ? $info->profile_url : '', + new DateTimeImmutable(isset($info->created_at) && is_string($info->created_at) ? $info->created_at : '@0'), + isset($info->last_active_at) && is_string($info->last_active_at) ? new DateTimeImmutable($info->last_active_at) : null, + isset($info->roles) && is_array($info->roles) ? $info->roles : [], // @phpstan-ignore-line + isset($info->title) && is_string($info->title) ? $info->title : '', + isset($info->super) && is_bool($info->super) ? $info->super : false, + isset($info->deleted) && is_bool($info->deleted) ? $info->deleted : false + ); + } +} diff --git a/src/V1/Users/V1UserAvatar.php b/src/V1/Users/V1UserAvatar.php new file mode 100644 index 0000000..50d3f44 --- /dev/null +++ b/src/V1/Users/V1UserAvatar.php @@ -0,0 +1,70 @@ +res === 0; + } + + /** + * Gets the width and height value of the avatar image. + * 0 for the original image, possibly not a square. + * + * @return int + */ + public function getResolution(): int { + return $this->res; + } + + /** + * Gets URI of the avatar image. + * + * @return string + */ + public function getUrl(): string { + return $this->url; + } + + /** + * Decodes an object into this proper object. + * + * @param object $info + * @return V1UserAvatar + */ + public static function decode(object $info): self { + return new static( + isset($info->res) && is_int($info->res) ? $info->res : 0, + isset($info->url) && is_string($info->url) ? $info->url : '' + ); + } + + /** + * Creates a blank emote instance. + * + * @return V1UserAvatar + */ + public static function empty(): self { + return new static(0, ''); + } +} diff --git a/src/V1/V1Client.php b/src/V1/V1Client.php new file mode 100644 index 0000000..08e2609 --- /dev/null +++ b/src/V1/V1Client.php @@ -0,0 +1,56 @@ +httpClient->request('GET', $this->urls->getApiUrl('/v1/me')); + $response->ensureSuccessStatusCode(); + + $me = $response->getJsonBody(); + if(!is_object($me)) + throw new RuntimeException('API response was not a JSON object.'); + + return V1User::decode($me); + } + + /** + * Retrieves the emotes portion of APIv1. + * + * @return V1EmotesClient + */ + public function emotes(): V1EmotesClient { + $this->emotes ??= new V1EmotesClient($this->httpClient, $this->urls); + return $this->emotes; + } +}