Added currently existent V1 routes.

This commit is contained in:
flash 2024-11-16 15:47:12 +00:00
parent 58cbf592fe
commit 094fb8e5b4
24 changed files with 653 additions and 47 deletions

View file

@ -1 +1 @@
0.1.1
0.2.0

View file

@ -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
) {}
/**

View file

@ -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 = ''
) {

View file

@ -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.

View file

@ -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.

View file

@ -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.
*

View file

@ -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
) {}
/**

View file

@ -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)

View file

@ -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 {

View file

@ -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;
}

View file

@ -5,7 +5,6 @@
namespace Flashii\Http;
use RuntimeException;
/**
* Common implementation for the getJsonBody method of HttpResponse.
*/

View file

@ -0,0 +1,30 @@
<?php declare(strict_types=1);
// RequestSpeedrun.php
// Created: 2024-11-16
// Updated: 2024-11-16
namespace Flashii\Http;
use InvalidArgumentException;
use RuntimeException;
/**
* Common implementation for the request method of HttpClient.
*/
trait RequestSpeedrun {
/**
* 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
*/
public function request(string $method, string $url): HttpResponse {
return $this->sendRequest($this->createRequest($method, $url));
}
}

View file

@ -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 {

View file

@ -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 {

View file

@ -6,6 +6,7 @@
namespace Flashii\Http;
use RuntimeException;
/**
* Common implementation for the SuccessStatusCode methods of HttpResponse.
*/

View file

@ -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 {

View file

@ -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
) {}
/**

View file

@ -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
) {}
/**

View file

@ -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 {

84
src/V1/Emotes/V1Emote.php Normal file
View file

@ -0,0 +1,84 @@
<?php declare(strict_types=1);
// V1Emote.php
// Created: 2024-11-16
// Updated: 2024-11-16
namespace Flashii\V1\Emotes;
/**
* Represents an APIv1 emote.
*/
final class V1Emote {
/**
* @param string $url URI to the emote image.
* @param string[] $strings Emote strings.
* @param string $id Internal system ID of the emote.
* @param int $order Internal ordering of the emote.
*/
public function __construct(
private readonly string $url,
private readonly array $strings,
private readonly string $id,
private readonly int $order
) {}
/**
* URI of the emote image.
*
* @return string
*/
public function getUrl(): string {
return $this->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<string> 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);
}
}

View file

@ -0,0 +1,53 @@
<?php declare(strict_types=1);
// V1EmotesClient.php
// Created: 2024-11-16
// Updated: 2024-11-16
namespace Flashii\V1\Emotes;
use Flashii\FlashiiUrls;
use Flashii\Http\HttpClient;
/**
* Implements the APIv1 emotes portion.
*/
class V1EmotesClient {
/**
* @param HttpClient $httpClient HTTP client.
* @param FlashiiUrls $urls API URLs.
*/
public function __construct(
private readonly HttpClient $httpClient,
private readonly FlashiiUrls $urls
) {}
/**
* Retrieves a list of all emotes.
*
* @param bool $includeOrder Whether to include the ordering values.
* @param bool $includeIds Whether to include internal Ids.
* @return V1Emote[]
*/
public function all(
bool $includeOrder = false,
bool $includeIds = false
): array {
$args = [];
if($includeIds)
$args['include_id'] = '1';
if($includeOrder)
$args['include_order'] = '1';
$response = $this->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
);
}
}

277
src/V1/Users/V1User.php Normal file
View file

@ -0,0 +1,277 @@
<?php declare(strict_types=1);
// V1User.php
// Created: 2024-11-16
// Updated: 2024-11-16
namespace Flashii\V1\Users;
use DateTimeImmutable;
use DateTimeInterface;
/**
* Represents an APIv1 user.
*/
final class V1User {
/** @var ?V1UserAvatar[] */
private ?array $avatarsSorted = null;
/**
* @param string $id User ID.
* @param string $name User name.
* @param ?int $colourRaw Raw 24-bit RGB value, null for inherit.
* @param string $colourCss CSS colour.
* @param int $rank User rank.
* @param string $countryCode ISO 3166-1 alpha-2 (adjacent) country code.
* @param V1UserAvatar[] $avatarUrls Avatar URIs.
* @param string $profileUrl Profile URI.
* @param DateTimeInterface $createdAt User registration date.
* @param ?DateTimeInterface $lastActiveAt Last user activity, null for none or hidden.
* @param string[] $roles User role strings.
* @param string $title User title.
* @param bool $super Super user status.
* @param bool $deleted Deleted status.
*/
public function __construct(
private readonly string $id,
private readonly string $name,
private readonly ?int $colourRaw,
private readonly string $colourCss,
private readonly int $rank,
private readonly string $countryCode,
private readonly array $avatarUrls,
private readonly string $profileUrl,
private readonly DateTimeInterface $createdAt,
private readonly ?DateTimeInterface $lastActiveAt,
private readonly array $roles,
private readonly string $title,
private readonly bool $super,
private readonly bool $deleted
) {}
/**
* Gets user ID.
*
* @return string
*/
public function getId(): string {
return $this->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
);
}
}

View file

@ -0,0 +1,70 @@
<?php declare(strict_types=1);
// V1UserAvatar.php
// Created: 2024-11-16
// Updated: 2024-11-16
namespace Flashii\V1\Users;
/**
* Represents an APIv1 user avatar.
*/
final class V1UserAvatar {
/**
* @param int $res Width and height value of the avatar image.
* @param string $url URI of the avatar image.
*/
public function __construct(
private readonly int $res,
private readonly string $url
) {}
/**
* Checks whether this avatar image is the original upload (not guaranteed to be square).
*
* @return bool
*/
public function isOriginalImage(): bool {
return $this->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, '');
}
}

56
src/V1/V1Client.php Normal file
View file

@ -0,0 +1,56 @@
<?php declare(strict_types=1);
// V1Client.php
// Created: 2024-11-16
// Updated: 2024-11-16
namespace Flashii\V1;
use RuntimeException;
use Flashii\FlashiiUrls;
use Flashii\Http\HttpClient;
use Flashii\V1\Emotes\V1EmotesClient;
use Flashii\V1\Users\V1User;
/**
* Implements the APIv1 client.
*/
class V1Client {
private ?V1EmotesClient $emotes = null;
/**
* @param HttpClient $httpClient HTTP client.
* @param FlashiiUrls $urls API URLs.
*/
public function __construct(
private readonly HttpClient $httpClient,
private readonly FlashiiUrls $urls
) {}
/**
* Returns information about the currently authenticated entity (user or app).
*
* @todo Currently there's no format for apps on the server side.
* @throws RuntimeException If there is no authenticated entity.
* @return V1User
*/
public function me(): V1User {
$response = $this->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;
}
}