diff options
83 files changed, 2166 insertions, 118 deletions
@@ -1,4 +1,7 @@ -Matrix Specification: https://spec.matrix.org/v1.15/ +Matrix Specification +- https://spec.matrix.org/v1.15/ +- https://spec.matrix.org/v1.16/ +- https://spec.matrix.org/legacy/client_server/r0.6.1 # TODO: check if access_token is expired BEFORE using it to fetch the user diff --git a/composer.json b/composer.json index f187ddf..5a1178e 100644 --- a/composer.json +++ b/composer.json @@ -8,10 +8,11 @@ } ], "require": { + "psr/http-message": "^2.0", + "psr/log": "^3.0", "symfony/dotenv": "^7.3", "symfony/http-foundation": "^7.3", - "symfony/routing": "^7.3", - "psr/log": "^3.0" + "symfony/routing": "^7.3" }, "require-dev": { "guzzlehttp/guzzle": "^7.9", @@ -20,6 +21,7 @@ "autoload": { "psr-4": { "App\\": "src/", + "Matrix\\": "matrix-specification", "Tests\\": "tests/" } }, diff --git a/composer.lock b/composer.lock index bb1e9cb..d720eb4 100644 --- a/composer.lock +++ b/composer.lock @@ -4,9 +4,62 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "4ddf0e945485466529ae4e7507dfb9fa", + "content-hash": "4964ee19b991107643bde192528a6119", "packages": [ { + "name": "psr/http-message", + "version": "2.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-message.git", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP messages", + "homepage": "https://github.com/php-fig/http-message", + "keywords": [ + "http", + "http-message", + "psr", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-message/tree/2.0" + }, + "time": "2023-04-04T09:54:51+00:00" + }, + { "name": "psr/log", "version": "3.0.2", "source": { @@ -203,16 +256,16 @@ }, { "name": "symfony/http-foundation", - "version": "v7.3.2", + "version": "v7.3.3", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "6877c122b3a6cc3695849622720054f6e6fa5fa6" + "reference": "7475561ec27020196c49bb7c4f178d33d7d3dc00" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/6877c122b3a6cc3695849622720054f6e6fa5fa6", - "reference": "6877c122b3a6cc3695849622720054f6e6fa5fa6", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/7475561ec27020196c49bb7c4f178d33d7d3dc00", + "reference": "7475561ec27020196c49bb7c4f178d33d7d3dc00", "shasum": "" }, "require": { @@ -262,7 +315,7 @@ "description": "Defines an object-oriented layer for the HTTP specification", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-foundation/tree/v7.3.2" + "source": "https://github.com/symfony/http-foundation/tree/v7.3.3" }, "funding": [ { @@ -282,7 +335,7 @@ "type": "tidelift" } ], - "time": "2025-07-10T08:47:49+00:00" + "time": "2025-08-20T08:04:18+00:00" }, { "name": "symfony/polyfill-mbstring", @@ -1636,59 +1689,6 @@ "time": "2024-04-15T12:06:14+00:00" }, { - "name": "psr/http-message", - "version": "2.0", - "source": { - "type": "git", - "url": "https://github.com/php-fig/http-message.git", - "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/php-fig/http-message/zipball/402d35bcb92c70c026d1a6a9883f06b2ead23d71", - "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71", - "shasum": "" - }, - "require": { - "php": "^7.2 || ^8.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "Psr\\Http\\Message\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "PHP-FIG", - "homepage": "https://www.php-fig.org/" - } - ], - "description": "Common interface for HTTP messages", - "homepage": "https://github.com/php-fig/http-message", - "keywords": [ - "http", - "http-message", - "psr", - "psr-7", - "request", - "response" - ], - "support": { - "source": "https://github.com/php-fig/http-message/tree/2.0" - }, - "time": "2023-04-04T09:54:51+00:00" - }, - { "name": "ralouphie/getallheaders", "version": "3.0.3", "source": { diff --git a/docker-compose.yml b/docker-compose.yml index ea5668f..519873f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,6 +1,6 @@ services: db: - image: postgres + image: postgres:17 environment: - "POSTGRES_DB=${DB_NAME}" - "POSTGRES_USER=${DB_USER}" diff --git a/matrix-specification/Data/AccountData.php b/matrix-specification/Data/AccountData.php new file mode 100644 index 0000000..fa3e4a3 --- /dev/null +++ b/matrix-specification/Data/AccountData.php @@ -0,0 +1,23 @@ +<?php + +namespace Matrix\Data; + +use Matrix\Events\Event; + +class AccountData implements \JsonSerializable +{ + /** + * @param Event[] $events + */ + public function __construct( + private array $events + ) + {} + + public function jsonSerialize(): array + { + return [ + "events" => $this->events, + ]; + } +} diff --git a/matrix-specification/Data/AuthenticationData.php b/matrix-specification/Data/AuthenticationData.php new file mode 100644 index 0000000..64fdd95 --- /dev/null +++ b/matrix-specification/Data/AuthenticationData.php @@ -0,0 +1,24 @@ +<?php + +namespace Matrix\Data; + +class AuthenticationData implements \JsonSerializable +{ + public function __construct( + private ?string $session = null, + private ?string $type = null, + ) + { + # TODO: throw for session and type + # TODO: throw for keys dependent on login type + # throw new \InvalidArgumentException("at least one is required"); + } + + public function jsonSerialize(): array + { + return [ + "session" => $this->session, + "type" => $this->type, + ]; + } +} diff --git a/matrix-specification/Data/Contact.php b/matrix-specification/Data/Contact.php new file mode 100644 index 0000000..54a48e4 --- /dev/null +++ b/matrix-specification/Data/Contact.php @@ -0,0 +1,28 @@ +<?php + +namespace Matrix\Data; + +use Matrix\Enums\Role; + +class Contact implements \JsonSerializable +{ + public function __construct( + private Role|string $role, + private ?string $emailAddress = null, + private ?string $matrixId = null, + ) + { + if (is_null($emailAddress) && is_null($matrixId)) { + throw new \InvalidArgumentException("at least one of emailAddress or matrixId is required"); + } + } + + public function jsonSerialize(): array + { + return array_filter([ + "email_address" => $this->emailAddress, + "matrix_id" => $this->matrixId, + "role" => $this->role, + ], fn ($value) => ! is_null($value)); + } +} diff --git a/matrix-specification/Data/DeviceKeys.php b/matrix-specification/Data/DeviceKeys.php new file mode 100644 index 0000000..034263e --- /dev/null +++ b/matrix-specification/Data/DeviceKeys.php @@ -0,0 +1,31 @@ +<?php + +namespace Matrix\Data; + +class DeviceKeys implements \JsonSerializable +{ + /** + * @param string[] $algorithms + * @param array<string, string> $keys + * @param array<string, array<string, string>> $signatures + */ + public function __construct( + private array $algorithms, + private string $deviceId, + private array $keys, + private array $signatures, + private string $userId, + ) + {} + + public function jsonSerialize(): array + { + return [ + "algorithms" => $this->algorithms, + "device_id" => $this->deviceId, + "keys" => $this->keys, + "signatures" => $this->signatures, + "user_id" => $this->userId, + ]; + } +} diff --git a/matrix-specification/Data/DeviceLists.php b/matrix-specification/Data/DeviceLists.php new file mode 100644 index 0000000..33d9459 --- /dev/null +++ b/matrix-specification/Data/DeviceLists.php @@ -0,0 +1,24 @@ +<?php + +namespace Matrix\Data; + +class DeviceLists implements \JsonSerializable +{ + /** + * @param string[] $changed + * @param string[] $left + */ + public function __construct( + private array $changed = [], + private array $left = [], + ) + {} + + public function jsonSerialize(): array + { + return [ + "changed" => $this->events, + "left" => $this->events, + ]; + } +} diff --git a/matrix-specification/Data/DiscoveryInformation.php b/matrix-specification/Data/DiscoveryInformation.php new file mode 100644 index 0000000..f4eda64 --- /dev/null +++ b/matrix-specification/Data/DiscoveryInformation.php @@ -0,0 +1,30 @@ +<?php + +namespace Matrix\Data; + +class DiscoveryInformation implements \JsonSerializable +{ + /** + * @param array<string, array<mixed, mixed>> $otherProperties + */ + public function __construct( + private HomeServerInformation $homeServerInformation, + private IdentityServerInformation $identityServerInformation, + private ?array $otherProperties = null, + ) + {} + + public function jsonSerialize(): array + { + return array_filter( + array_merge( + [ + "m.homeserver" => $this->homeServerInformation, + "m.identity_server" => $this->identityServerInformation, + ], + $this->otherProperties ?? [], + ), + fn ($value) => ! is_null($value) + ); + } +} diff --git a/matrix-specification/Data/Filters/EventFilter.php b/matrix-specification/Data/Filters/EventFilter.php new file mode 100644 index 0000000..cd8fdf2 --- /dev/null +++ b/matrix-specification/Data/Filters/EventFilter.php @@ -0,0 +1,36 @@ +<?php + +namespace Matrix\Data\Filters; + +class EventFilter implements \JsonSerializable +{ + /** + * @param string[] $notSenders + * @param string[] $notTypes + * @param string[] $senders + * @param string[] $types + */ + public function __construct( + private ?int $limit = null, + private ?array $notSenders = null, + private ?array $notTypes = null, + private ?array $senders = null, + private ?array $types = null, + ) + { + if (! is_null($limit) && $limit <= 0) { + throw new \InvalidArgumentException("limit must be an integer greater than 0"); + } + } + + public function jsonSerialize(): array + { + return array_filter([ + "limit" => $this->limit, + "not_senders" => $this->notSenders, + "not_types" => $this->notTypes, + "senders" => $this->senders, + "types" => $this->types, + ], fn ($value) => ! is_null($value)); + } +} diff --git a/matrix-specification/Data/Filters/RoomEventFilter.php b/matrix-specification/Data/Filters/RoomEventFilter.php new file mode 100644 index 0000000..b7ce87b --- /dev/null +++ b/matrix-specification/Data/Filters/RoomEventFilter.php @@ -0,0 +1,50 @@ +<?php + +namespace Matrix\Data\Filters; + +class RoomEventFilter extends EventFilter +{ + /** + * @param string[] $notRooms + * @param string[] $notSenders + * @param string[] $notTypes + * @param string[] $rooms + * @param string[] $senders + * @param string[] $types + */ + public function __construct( + private ?bool $containsUrl = null, + private ?bool $includeRedundantMembers = null, + private ?bool $lazyLoadMembers = null, + ?int $limit = null, + private ?array $notRooms = null, + ?array $notSenders = null, + ?array $notTypes = null, + private ?array $rooms = null, + ?array $senders = null, + ?array $types = null, + private ?bool $unreadThreadNotifications = null, + ) + { + parent::__construct($limit, $notSenders, $notTypes, $senders, $types); + } + + public function setDefaults(): void + { + $this->includeRedundantMembers ??= false; + $this->lazyLoadMembers ??= false; + $this->unreadThreadNotifications ??= false; + } + + public function jsonSerialize(): array + { + return parent::jsonSerialize() + array_filter([ + "contains_url" => $this->containsUrl, + "include_redundant_members" => $this->includeRedundantMembers, + "lazy_load_members" => $this->lazyLoadMembers, + "not_rooms" => $this->notRooms, + "rooms" => $this->rooms, + "unread_thread_notifications" => $this->unreadThreadNotifications, + ], fn ($value) => ! is_null($value)); + } +} diff --git a/matrix-specification/Data/Filters/RoomFilter.php b/matrix-specification/Data/Filters/RoomFilter.php new file mode 100644 index 0000000..fe741c5 --- /dev/null +++ b/matrix-specification/Data/Filters/RoomFilter.php @@ -0,0 +1,39 @@ +<?php + +namespace Matrix\Data\Filters; + +class RoomFilter +{ + /** + * @param string[] $notRooms + * @param string[] $rooms + */ + public function __construct( + private ?RoomEventFilter $accountData = null, + private ?RoomEventFilter $ephemeral = null, + private ?bool $includeLeave = null, + private ?array $notRooms = null, + private ?array $rooms = null, + private ?RoomEventFilter $state = null, + private ?RoomEventFilter $timeline = null, + ) + {} + + public function setDefaults(): void + { + $this->includeLeave ??= false; + } + + public function jsonSerialize(): array + { + return array_filter([ + "account_data" => $this->accountData, + "ephemeral" => $this->ephemeral, + "include_leave" => $this->includeLeave, + "not_rooms" => $this->notRooms, + "rooms" => $this->rooms, + "state" => $this->state, + "timeline" => $this->timeline, + ], fn ($value) => ! is_null($value)); + } +} diff --git a/matrix-specification/Data/HomeServerInformation.php b/matrix-specification/Data/HomeServerInformation.php new file mode 100644 index 0000000..15c9f10 --- /dev/null +++ b/matrix-specification/Data/HomeServerInformation.php @@ -0,0 +1,18 @@ +<?php + +namespace Matrix\Data; + +class HomeServerInformation implements \JsonSerializable +{ + public function __construct( + private string $baseUrl, + ) + {} + + public function jsonSerialize(): array + { + return [ + "base_url" => $this->baseUrl, + ]; + } +} diff --git a/matrix-specification/Data/IdentityServerInformation.php b/matrix-specification/Data/IdentityServerInformation.php new file mode 100644 index 0000000..bed8323 --- /dev/null +++ b/matrix-specification/Data/IdentityServerInformation.php @@ -0,0 +1,18 @@ +<?php + +namespace Matrix\Data; + +class IdentityServerInformation implements \JsonSerializable +{ + public function __construct( + private string $baseUrl, + ) + {} + + public function jsonSerialize(): array + { + return [ + "base_url" => $this->baseUrl, + ]; + } +} diff --git a/matrix-specification/Data/KeyObject.php b/matrix-specification/Data/KeyObject.php new file mode 100644 index 0000000..120350c --- /dev/null +++ b/matrix-specification/Data/KeyObject.php @@ -0,0 +1,32 @@ +<?php + +namespace Matrix\Data; + +class KeyObject implements \JsonSerializable +{ + /** + * @param array<string, array> $signatures + */ + public function __construct( + private string $key, + private array $signatures, + private bool $isFallback = false, + ) + {} + + public function jsonSerialize(): array + { + $keyObject = [ + "key" => $this->key, + "signatures" => $this->signatures, + ]; + + if ($this->isFallback) { + $keyObject += [ + "fallback" => true, + ]; + } + + return $keyObject; + } +} diff --git a/matrix-specification/Data/LoginFlow.php b/matrix-specification/Data/LoginFlow.php new file mode 100644 index 0000000..6874aad --- /dev/null +++ b/matrix-specification/Data/LoginFlow.php @@ -0,0 +1,31 @@ +<?php + +namespace Matrix\Data; + +use Matrix\Enums\LoginType; + +class LoginFlow implements \JsonSerializable +{ + public function __construct( + private LoginType $type, + private ?bool $getLoginToken = null, + ) + {} + + public function jsonSerialize(): array + { + $loginFlow = [ + "type" => $this->type, + ]; + + $loginFlow += match ($this->type) { + LoginType::TOKEN => [ + "get_login_token" => $this->getLoginToken, + ], + + default => [], + }; + + return $loginFlow; + } +} diff --git a/matrix-specification/Data/Presence.php b/matrix-specification/Data/Presence.php new file mode 100644 index 0000000..971343f --- /dev/null +++ b/matrix-specification/Data/Presence.php @@ -0,0 +1,23 @@ +<?php + +namespace Matrix\Data; + +use Matrix\Events\PresenceEvent; + +class Presence implements \JsonSerializable +{ + /** + * @param PresenceEvent[] $events + */ + public function __construct( + private array $events + ) + {} + + public function jsonSerialize(): array + { + return [ + "events" => $this->events, + ]; + } +} diff --git a/matrix-specification/Data/Room/Ephemeral.php b/matrix-specification/Data/Room/Ephemeral.php new file mode 100644 index 0000000..bcaf22f --- /dev/null +++ b/matrix-specification/Data/Room/Ephemeral.php @@ -0,0 +1,21 @@ +<?php + +namespace Matrix\Data\Room; + +class Ephemeral implements \JsonSerializable +{ + /** + * @param Event[] $events + */ + public function __construct( + private array $events, + ) + {} + + public function jsonSerialize(): array + { + return [ + "events" => $this->events, + ]; + } +} diff --git a/matrix-specification/Data/Room/InviteState.php b/matrix-specification/Data/Room/InviteState.php new file mode 100644 index 0000000..ca468ee --- /dev/null +++ b/matrix-specification/Data/Room/InviteState.php @@ -0,0 +1,23 @@ +<?php + +namespace Matrix\Data\Room; + +use Matrix\Events\StrippedStateEvent; + +class InviteState implements \JsonSerializable +{ + /** + * @param StrippedStateEvent[] $events + */ + public function __construct( + private array $events, + ) + {} + + public function jsonSerialize(): array + { + return [ + "events" => $this->events, + ]; + } +} diff --git a/matrix-specification/Data/Room/InvitedRoom.php b/matrix-specification/Data/Room/InvitedRoom.php new file mode 100644 index 0000000..f081861 --- /dev/null +++ b/matrix-specification/Data/Room/InvitedRoom.php @@ -0,0 +1,18 @@ +<?php + +namespace Matrix\Data\Room; + +class InvitedRoom implements \JsonSerializable +{ + public function __construct( + private InviteState $inviteState, + ) + {} + + public function jsonSerialize(): array + { + return [ + "invite_state" => $this->inviteState, + ]; + } +} diff --git a/matrix-specification/Data/Room/JoinedRoom.php b/matrix-specification/Data/Room/JoinedRoom.php new file mode 100644 index 0000000..0057071 --- /dev/null +++ b/matrix-specification/Data/Room/JoinedRoom.php @@ -0,0 +1,37 @@ +<?php + +namespace Matrix\Data\Room; + +use Matrix\Data\AccountData; + +class JoinedRoom implements \JsonSerializable +{ + /** + * @param array<string, ThreadNotificationCounts> + */ + public function __construct( + private ?AccountData $accountData = null, + private ?Ephemeral $ephemeral = null, + private ?State $state = null, + private ?State $stateAfter = null, + private ?RoomSummary $summary = null, + private ?Timeline $timeline = null, + private ?UnreadNotificationCounts $unreadNotifications = null, + private ?array $unreadThreadNotifications = null, + ) + {} + + public function jsonSerialize(): array + { + return array_filter([ + "account_data" => $this->inviteState, + "ephemeral" => $this->ephemeral, + "state" => $this->state, + "state_after" => $this->stateAfter, + "summary" => $this->summary, + "timeline" => $this->timeline, + "unread_notifications" => $this->unreadNotifications, + "unreadThreadNotifications" => $this->unreadThreadNotifications, + ], fn ($value) => ! is_null($value)); + } +} diff --git a/matrix-specification/Data/Room/KnockState.php b/matrix-specification/Data/Room/KnockState.php new file mode 100644 index 0000000..e294da8 --- /dev/null +++ b/matrix-specification/Data/Room/KnockState.php @@ -0,0 +1,23 @@ +<?php + +namespace Matrix\Data\Room; + +use Matrix\Events\StrippedStateEvent; + +class KnockState implements \JsonSerializable +{ + /** + * @param StrippedStateEvent[] $events + */ + public function __construct( + private array $events, + ) + {} + + public function jsonSerialize(): array + { + return [ + "events" => $this->events, + ]; + } +} diff --git a/matrix-specification/Data/Room/KnockedRoom.php b/matrix-specification/Data/Room/KnockedRoom.php new file mode 100644 index 0000000..cbea19f --- /dev/null +++ b/matrix-specification/Data/Room/KnockedRoom.php @@ -0,0 +1,18 @@ +<?php + +namespace Matrix\Data\Room; + +class KnockedRoom implements \JsonSerializable +{ + public function __construct( + private KnockState $knockState, + ) + {} + + public function jsonSerialize(): array + { + return [ + "knock_state" => $this->knockState, + ]; + } +} diff --git a/matrix-specification/Data/Room/LeftRoom.php b/matrix-specification/Data/Room/LeftRoom.php new file mode 100644 index 0000000..64b9462 --- /dev/null +++ b/matrix-specification/Data/Room/LeftRoom.php @@ -0,0 +1,30 @@ +<?php + +namespace Matrix\Data\Room; + +use Matrix\Data\AccountData; + +/** + * @see https://spec.matrix.org/v1.16/client-server-api/#get_matrixclientv3sync_response-200_left-room + * TODO: validate against request. add ValidatesAgainstRequest interface? and MatrixRequest base class? + */ +class LeftRoom implements \JsonSerializable +{ + public function __construct( + private ?AccountData $accountData = null, + private ?State $state = null, + private ?State $stateAfter = null, + private ?Timeline $timeline = null, + ) + {} + + public function jsonSerialize(): array + { + return array_filter([ + "account_data" => $this->inviteState, + "state" => $this->state, + "state_after" => $this->stateAfter, + "timeline" => $this->timeline, + ], fn ($value) => ! is_null($value)); + } +} diff --git a/matrix-specification/Data/Room/RoomSummary.php b/matrix-specification/Data/Room/RoomSummary.php new file mode 100644 index 0000000..5ac86a8 --- /dev/null +++ b/matrix-specification/Data/Room/RoomSummary.php @@ -0,0 +1,25 @@ +<?php + +namespace Matrix\Data\Room; + +class RoomSummary implements \JsonSerializable +{ + /** + * @param string[] $heroes + */ + public function __construct( + private ?array $heroes = null, + private ?int $invitedMemberCount = null, + private ?int $joinedMemberCount = null, + ) + {} + + public function jsonSerialize(): array + { + return array_filter([ + "m.heroes" => $this->heroes, + "m.invited_member_count" => $this->invitedMemberCount, + "m.joined_member_count" => $this->joinedMemberCount, + ], fn ($value) => ! is_null($value)); + } +} diff --git a/matrix-specification/Data/Room/Rooms.php b/matrix-specification/Data/Room/Rooms.php new file mode 100644 index 0000000..1a84608 --- /dev/null +++ b/matrix-specification/Data/Room/Rooms.php @@ -0,0 +1,62 @@ +<?php + +namespace Matrix\Data\Room; + +class Rooms implements \JsonSerializable +{ + /** + * @param array<string, InvitedRoom> $invite + * @param array<string, JoinedRoom> $join + * @param array<string, KnockedRoom> $knock + * @param array<string, LeftRoom> $leave + */ + public function __construct( + private ?array $invite = null, + private ?array $join = null, + private ?array $knock = null, + private ?array $leave = null, + ) + {} + + public function jsonSerialize(): array + { + return [ + "invite" => $this->invite ?? new \stdClass(), + "join" => $this->join ?? new \stdClass(), + "knock" => $this->knock ?? new \stdClass(), + "leave" => $this->leave ?? new \stdClass(), + ]; + } + + /** + * @return InvitedRoom[] + */ + public function getInvite(): ?array + { + return $this->invite; + } + + /** + * @return JoinedRoom[] + */ + public function getJoined(): ?array + { + return $this->join; + } + + /** + * @return KnockedRoom[] + */ + public function getKnocked(): ?array + { + return $this->knock; + } + + /** + * @return LeftRoom[] + */ + public function getLeft(): ?array + { + return $this->leave; + } +} diff --git a/matrix-specification/Data/Room/State.php b/matrix-specification/Data/Room/State.php new file mode 100644 index 0000000..be9f0ef --- /dev/null +++ b/matrix-specification/Data/Room/State.php @@ -0,0 +1,23 @@ +<?php + +namespace Matrix\Data\Room; + +use Matrix\Events\ClientEventWithoutRoomId; + +class State implements \JsonSerializable +{ + /** + * @param ClientEventWithoutRoomId[] $events + */ + public function __construct( + private array $events, + ) + {} + + public function jsonSerialize(): array + { + return [ + "events" => $this->events, + ]; + } +} diff --git a/matrix-specification/Data/Room/ThreadNotificationCounts.php b/matrix-specification/Data/Room/ThreadNotificationCounts.php new file mode 100644 index 0000000..602de61 --- /dev/null +++ b/matrix-specification/Data/Room/ThreadNotificationCounts.php @@ -0,0 +1,20 @@ +<?php + +namespace Matrix\Data\Room; + +class ThreadNotificationCounts implements \JsonSerializable +{ + public function __construct( + private ?int $highlightCount = null, + private ?int $notificationCount = null, + ) + {} + + public function jsonSerialize(): array + { + return array_filter([ + "highlight_count" => $this->highlightCount, + "notification_count" => $this->notificationCount, + ], fn ($value) => ! is_null($value)); + } +} diff --git a/matrix-specification/Data/Room/Timeline.php b/matrix-specification/Data/Room/Timeline.php new file mode 100644 index 0000000..8d70ddd --- /dev/null +++ b/matrix-specification/Data/Room/Timeline.php @@ -0,0 +1,27 @@ +<?php + +namespace Matrix\Data\Room; + +use Matrix\Events\ClientEventWithoutRoomId; + +class Timeline implements \JsonSerializable +{ + /** + * @param ClientEventWithoutRoomId[] $events + */ + public function __construct( + private array $events, + private ?bool $limited = null, + private ?string $previousBatch = null, + ) + {} + + public function jsonSerialize(): array + { + return array_filter([ + "events" => $this->events, + "limited" => $this->limited, + "prev_batch" => $this->previousBatch, + ], fn ($value) => ! is_null($value)); + } +} diff --git a/matrix-specification/Data/Room/UnreadNotificationCounts.php b/matrix-specification/Data/Room/UnreadNotificationCounts.php new file mode 100644 index 0000000..f00a6a5 --- /dev/null +++ b/matrix-specification/Data/Room/UnreadNotificationCounts.php @@ -0,0 +1,20 @@ +<?php + +namespace Matrix\Data\Room; + +class UnreadNotificationCounts implements \JsonSerializable +{ + public function __construct( + private ?int $highlightCount = null, + private ?int $notificationCount = null, + ) + {} + + public function jsonSerialize(): array + { + return array_filter([ + "highlight_count" => $this->highlightCount, + "notification_count" => $this->notificationCount, + ], fn ($value) => ! is_null($value)); + } +} diff --git a/matrix-specification/Data/ToDevice.php b/matrix-specification/Data/ToDevice.php new file mode 100644 index 0000000..362ce06 --- /dev/null +++ b/matrix-specification/Data/ToDevice.php @@ -0,0 +1,23 @@ +<?php + +namespace Matrix\Data; + +use Matrix\Events\SenderEvent; + +class ToDevice implements \JsonSerializable +{ + /** + * @param SenderEvent[] $events + */ + public function __construct( + private array $events + ) + {} + + public function jsonSerialize(): array + { + return [ + "events" => $this->events, + ]; + } +} diff --git a/matrix-specification/Data/UserId.php b/matrix-specification/Data/UserId.php new file mode 100644 index 0000000..01a5e9e --- /dev/null +++ b/matrix-specification/Data/UserId.php @@ -0,0 +1,17 @@ +<?php + +namespace Matrix\Data; + +class UserId +{ + public static function validate(): bool + {} + + public static function parse(): array + {} + + public static function build(string $username, string $serverName): string + { + return "@$username:$serverName"; + } +} diff --git a/matrix-specification/Data/UserIdentifier.php b/matrix-specification/Data/UserIdentifier.php new file mode 100644 index 0000000..8fcdae0 --- /dev/null +++ b/matrix-specification/Data/UserIdentifier.php @@ -0,0 +1,45 @@ +<?php + +namespace Matrix\Data; + +use Matrix\Enums\UserIdentifierType; + +class UserIdentifier implements \JsonSerializable +{ + public function __construct( + private UserIdentifierType $type, + private ?string $user = null, + private ?string $thirdPartyMedium = null, + private ?string $thirdPartyAddress = null, + private ?string $phoneCountry = null, + private ?string $phoneNumber = null, + ) + {} + + public function jsonSerialize(): array + { + $userIdentifier = [ + "type" => $this->type, + ]; + + $userIdentifier += match ($this->type) { + UserIdentifierType::USER => [ + "user" => $this->user, + ], + + UserIdentifierType::THIRDPARTY => [ + "medium" => $this->thirdPartyMedium, + "address" => $this->thirdPartyAddress, + ], + + UserIdentifierType::PHONE => [ + "country" => $this->phoneCountry, + "phone" => $this->phoneNumber, + ], + + default => [], + }; + + return $userIdentifier; + } +} diff --git a/matrix-specification/Enums/ApiPathVersion.php b/matrix-specification/Enums/ApiPathVersion.php new file mode 100644 index 0000000..5664688 --- /dev/null +++ b/matrix-specification/Enums/ApiPathVersion.php @@ -0,0 +1,11 @@ +<?php + +namespace Matrix\Enums; + +enum ApiPathVersion: string +{ + case R0 = "r0"; + case V1 = "v1"; + case V2 = "v2"; + case V3 = "v3"; +} diff --git a/matrix-specification/Enums/AuthenticationType.php b/matrix-specification/Enums/AuthenticationType.php new file mode 100644 index 0000000..e335eed --- /dev/null +++ b/matrix-specification/Enums/AuthenticationType.php @@ -0,0 +1,19 @@ +<?php + +namespace Matrix\Enums; + +enum AuthenticationType: string implements \JsonSerializable +{ + case DUMMY = "m.login.dummy"; + case EMAIL_IDENTITY = "m.login.email.identity"; + case MSISDN = "m.login.msisdn"; + case PASSWORD = "m.login.password"; + case RECAPTCHA = "m.login.recaptcha"; + case REGISTRATION_TOKEN = "m.login.registration_token"; + case SSO = "m.login.sso"; + + public function jsonSerialize(): string + { + return $this->value; + } +} diff --git a/matrix-specification/Enums/ErrorCode.php b/matrix-specification/Enums/ErrorCode.php new file mode 100644 index 0000000..1a22f64 --- /dev/null +++ b/matrix-specification/Enums/ErrorCode.php @@ -0,0 +1,52 @@ +<?php + +namespace Matrix\Enums; + +enum ErrorCode: string implements \JsonSerializable +{ + case FORBIDDEN = "M_FORBIDDEN"; + case UNKNOWN_TOKEN = "M_UNKNOWN_TOKEN"; + case MISSING_TOKEN = "M_MISSING_TOKEN"; + case USER_LOCKED = "M_USER_LOCKED"; + case USER_SUSPENDED = "M_USER_SUSPENDED"; + case BAD_JSON = "M_BAD_JSON"; + case NOT_JSON = "M_NOT_JSON"; + case NOT_FOUND = "M_NOT_FOUND"; + case LIMIT_EXCEEDED = "M_LIMIT_EXCEEDED"; + case UNRECOGNIZED = "M_UNRECOGNIZED"; + case UNKNOWN = "M_UNKNOWN"; + + case UNAUTHORIZED = "M_UNAUTHORIZED"; + case USER_DEACTIVATED = "M_USER_DEACTIVATED"; + case USER_IN_USE = "M_USER_IN_USE"; + case INVALID_USERNAME = "M_INVALID_USERNAME"; + case ROOM_IN_USE = "M_ROOM_IN_USE"; + case INVALID_ROOM_STATE = "M_INVALID_ROOM_STATE"; + + case THREEPID_IN_USE = "M_THREEPID_IN_USE"; + case THREEPID_NOT_FOUND = "M_THREEPID_NOT_FOUND"; + case THREEPID_AUTH_FAILED = "M_THREEPID_AUTH_FAILED"; + case THREEPID_DENIED = "M_THREEPID_DENIED"; + case THREEPID_MEDIUM_NOT_SUPPORTED = "M_THREEPID_MEDIUM_NOT_SUPPORTED"; + + case SERVER_NOT_TRUSTED = "M_SERVER_NOT_TRUSTED"; + case UNSUPPORTED_ROOM_VERSION = "M_UNSUPPORTED_ROOM_VERSION"; + case INCOMPATIBLE_ROOM_VERSION = "M_INCOMPATIBLE_ROOM_VERSION"; + case BAD_STATE = "M_BAD_STATE"; + case GUEST_ACCESS_FORBIDDEN = "M_GUEST_ACCESS_FORBIDDEN"; + + case CAPTCHA_NEEDED = "M_CAPTCHA_NEEDED"; + case CAPTCHA_INVALID = "M_CAPTCHA_INVALID"; + + case MISSING_PARAM = "M_MISSING_PARAM"; + case INVALID_PARAM = "M_INVALID_PARAM"; + case TOO_LARGE = "M_TOO_LARGE"; + case EXCLUSIVE = "M_EXCLUSIVE"; + case RESOURCE_LIMIT_EXCEEDED = "M_RESOURCE_LIMIT_EXCEEDED"; + case CANNOT_LEAVE_SERVER_NOTICE_ROOM = "M_CANNOT_LEAVE_SERVER_NOTICE_ROOM"; + + public function jsonSerialize(): string + { + return $this->value; + } +} diff --git a/matrix-specification/Enums/EventType.php b/matrix-specification/Enums/EventType.php new file mode 100644 index 0000000..da199dd --- /dev/null +++ b/matrix-specification/Enums/EventType.php @@ -0,0 +1,21 @@ +<?php + +namespace Matrix\Enums; + +enum EventType: string implements \JsonSerializable +{ + case PRESENCE = "m.presence"; + case RECEIPT = "m.receipt"; + + case ROOM_MEMBER = "m.room.member"; + case ROOM_MESSAGE = "m.room.message"; + case ROOM_NAME = "m.room.name"; + + case TAG = "m.tag"; + case TYPING = "m.typing"; + + public function jsonSerialize(): string + { + return $this->value; + } +} diff --git a/matrix-specification/Enums/LoginType.php b/matrix-specification/Enums/LoginType.php new file mode 100644 index 0000000..b268cf5 --- /dev/null +++ b/matrix-specification/Enums/LoginType.php @@ -0,0 +1,14 @@ +<?php + +namespace Matrix\Enums; + +enum LoginType: string implements \JsonSerializable +{ + case PASSWORD = "m.login.password"; + case TOKEN = "m.login.token"; + + public function jsonSerialize(): string + { + return $this->value; + } +} diff --git a/matrix-specification/Enums/MembershipState.php b/matrix-specification/Enums/MembershipState.php new file mode 100644 index 0000000..750f4c0 --- /dev/null +++ b/matrix-specification/Enums/MembershipState.php @@ -0,0 +1,17 @@ +<?php + +namespace Matrix\Enums; + +enum MembershipState: string implements \JsonSerializable +{ + case BAN = "ban"; + case INVITE = "invite"; + case JOIN = "join"; + case KNOCK = "knock"; + case LEAVE = "leave"; + + public function jsonSerialize(): string + { + return $this->value; + } +} diff --git a/matrix-specification/Enums/PresenceState.php b/matrix-specification/Enums/PresenceState.php new file mode 100644 index 0000000..bc97ae9 --- /dev/null +++ b/matrix-specification/Enums/PresenceState.php @@ -0,0 +1,15 @@ +<?php + +namespace Matrix\Enums; + +enum PresenceState: string implements \JsonSerializable +{ + case OFFLINE = "offline"; + case ONLINE = "online"; + case UNAVAILABLE = "unavailable"; + + public function jsonSerialize(): string + { + return $this->value; + } +} diff --git a/matrix-specification/Enums/Role.php b/matrix-specification/Enums/Role.php new file mode 100644 index 0000000..f2abf9b --- /dev/null +++ b/matrix-specification/Enums/Role.php @@ -0,0 +1,14 @@ +<?php + +namespace Matrix\Enums; + +enum Role: string implements \JsonSerializable +{ + case ADMIN = "m.role.admin"; + case SECURITY = "m.role.security"; + + public function jsonSerialize(): string + { + return $this->value; + } +} diff --git a/matrix-specification/Enums/UserIdentifierType.php b/matrix-specification/Enums/UserIdentifierType.php new file mode 100644 index 0000000..d4be36e --- /dev/null +++ b/matrix-specification/Enums/UserIdentifierType.php @@ -0,0 +1,15 @@ +<?php + +namespace Matrix\Enums; + +enum UserIdentifierType: string implements \JsonSerializable +{ + case USER = "m.id.user"; + case THIRDPARTY = "m.id.thirdparty"; + case PHONE = "m.id.phone"; + + public function jsonSerialize(): string + { + return $this->value; + } +} diff --git a/matrix-specification/Enums/UserRegistrationKind.php b/matrix-specification/Enums/UserRegistrationKind.php new file mode 100644 index 0000000..28ddd40 --- /dev/null +++ b/matrix-specification/Enums/UserRegistrationKind.php @@ -0,0 +1,14 @@ +<?php + +namespace Matrix\Enums; + +enum UserRegistrationKind: string implements \JsonSerializable +{ + case GUEST = "guest"; + case USER = "user"; + + public function jsonSerialize(): string + { + return $this->value; + } +} diff --git a/matrix-specification/Errors/Error.php b/matrix-specification/Errors/Error.php new file mode 100644 index 0000000..2adc642 --- /dev/null +++ b/matrix-specification/Errors/Error.php @@ -0,0 +1,41 @@ +<?php + +namespace Matrix\Errors; + +use Matrix\Enums\ErrorCode; + +abstract class Error extends \RuntimeException implements \JsonSerializable +{ + public function __construct( + private ErrorCode $errorCode, + string $message, + int $httpCode + ) + { + parent::__construct($message, $httpCode); + } + + public function getErrorCode(): ErrorCode + { + return $this->errorCode; + } + + public function getHttpCode(): int + { + return $this->getCode(); + } + + /** + * @return array<string, mixed> + */ + abstract public function getAdditionalData(): array; + + public function jsonSerialize(): array + { + return [ + "errcode" => $this->getErrorCode(), + "error" => $this->getMessage(), + ...$this->getAdditionalData(), + ]; + } +} diff --git a/matrix-specification/Errors/RateLimitError.php b/matrix-specification/Errors/RateLimitError.php new file mode 100644 index 0000000..2f4193c --- /dev/null +++ b/matrix-specification/Errors/RateLimitError.php @@ -0,0 +1,20 @@ +<?php + +namespace Matrix\Errors; + +use Matrix\Enums\ErrorCode; + +class RateLimitError extends Error +{ + public function __construct(private int $retryAfterMilliseconds) + { + parent::__construct(ErrorCode::LIMIT_EXCEEDED, "Too many requests", 429); + } + + public function getAdditionalData(): array + { + return [ + "retry_after_ms" => $this->retryAfterMilliseconds, + ]; + } +} diff --git a/matrix-specification/Events/ClientEvent.php b/matrix-specification/Events/ClientEvent.php new file mode 100644 index 0000000..be1e354 --- /dev/null +++ b/matrix-specification/Events/ClientEvent.php @@ -0,0 +1,32 @@ +<?php + +namespace Matrix\Events; + +use Matrix\Enums\EventType; + +class ClientEvent extends ClientEventWithoutRoomId +{ + public function __construct( + array $content, + string $eventId, + int $originServerTimestamp, + protected string $roomId, + string $sender, + string $stateKey, + EventType $type, + ?UnsignedData $unsigned = null, + ) + { + parent::__construct($content, $eventId, $originServerTimestamp, $sender, $stateKey, $type); + } + + public function jsonSerialize(): array + { + $clientEvent = parent::jsonSerialize(); + $clientEvent += [ + "room_id" => $this->roomId, + ]; + + return $clientEvent; + } +} diff --git a/matrix-specification/Events/ClientEventWithoutRoomId.php b/matrix-specification/Events/ClientEventWithoutRoomId.php new file mode 100644 index 0000000..0410d5b --- /dev/null +++ b/matrix-specification/Events/ClientEventWithoutRoomId.php @@ -0,0 +1,34 @@ +<?php + +namespace Matrix\Events; + +use Matrix\Enums\EventType; + +class ClientEventWithoutRoomId extends SenderEvent +{ + public function __construct( + array $content, + protected string $eventId, + protected int $originServerTimestamp, + string $sender, + protected string $stateKey, + EventType $type, + protected ?UnsignedData $unsigned = null, + ) + { + parent::__construct($content, $sender, $type); + } + + public function jsonSerialize(): array + { + return [ + "content" => $this->content ?: new \stdClass, + "event_id" => $this->eventId, + "origin_server_ts" => $this->originServerTimestamp, + "sender" => $this->sender, + "state_key" => $this->stateKey, + "type" => $this->type, + "unsigned" => $this->unsigned ?? new \stdClass, + ]; + } +} diff --git a/matrix-specification/Events/Event.php b/matrix-specification/Events/Event.php new file mode 100644 index 0000000..fe85f48 --- /dev/null +++ b/matrix-specification/Events/Event.php @@ -0,0 +1,17 @@ +<?php + +namespace Matrix\Events; + +use Matrix\Enums\EventType; + +abstract class Event implements \JsonSerializable +{ + /** + * @param array<string, mixed> $content + */ + public function __construct( + protected array $content, + protected EventType $type, + ) + {} +} diff --git a/matrix-specification/Events/PresenceEvent.php b/matrix-specification/Events/PresenceEvent.php new file mode 100644 index 0000000..7854444 --- /dev/null +++ b/matrix-specification/Events/PresenceEvent.php @@ -0,0 +1,42 @@ +<?php + +namespace Matrix\Events; + +use Matrix\Enums\EventType; +use Matrix\Enums\PresenceState; + +class PresenceEvent extends SenderEvent +{ + public function __construct( + string $sender, + PresenceState $presence, + ?string $avatarUrl = null, + ?bool $currentlyActive = null, + ?string $displayName = null, + ?int $lastActiveAgo = null, + ?string $statusMessage = null, + ) + { + parent::__construct( + array_filter([ + "avatar_url" => $avatarUrl, + "currently_active" => $currentlyActive, + "display_name" => $displayName, + "last_active_ago" => $lastActiveAgo, + "presence" => $presence, + "status_msg" => $statusMessage, + ], fn ($value) => ! is_null($value)), + $sender, + EventType::PRESENCE + ); + } + + public function jsonSerialize(): array + { + return [ + "content" => $this->content, + "sender" => $this->sender, + "type" => $this->type, + ]; + } +} diff --git a/matrix-specification/Events/SenderEvent.php b/matrix-specification/Events/SenderEvent.php new file mode 100644 index 0000000..f3d4ceb --- /dev/null +++ b/matrix-specification/Events/SenderEvent.php @@ -0,0 +1,20 @@ +<?php + +namespace Matrix\Events; + +use Matrix\Enums\EventType; + +abstract class SenderEvent extends Event +{ + /** + * @param array<string, mixed> $content + */ + public function __construct( + array $content, + protected string $sender, + EventType $type, + ) + { + parent::__construct($content, $type); + } +} diff --git a/matrix-specification/Events/StrippedStateEvent.php b/matrix-specification/Events/StrippedStateEvent.php new file mode 100644 index 0000000..6e8aea6 --- /dev/null +++ b/matrix-specification/Events/StrippedStateEvent.php @@ -0,0 +1,31 @@ +<?php + +namespace Matrix\Events; + +use Matrix\Enums\EventType; + +class StrippedStateEvent extends SenderEvent +{ + /** + * @param array<string, mixed> $content + */ + public function __construct( + array $content, + string $sender, + private string $stateKey, + EventType $type, + ) + { + parent::__construct($content, $sender, $type); + } + + public function jsonSerialize(): array + { + return [ + "content" => $this->content, + "sender" => $this->sender, + "state_key" => $this->stateKey, + "type" => $this->type, + ]; + } +} diff --git a/matrix-specification/Events/UnsignedData.php b/matrix-specification/Events/UnsignedData.php new file mode 100644 index 0000000..3c5cd46 --- /dev/null +++ b/matrix-specification/Events/UnsignedData.php @@ -0,0 +1,31 @@ +<?php + +namespace Matrix\Events; + +use Matrix\Enums\MembershipState; + +/** + * @see https://spec.matrix.org/v1.16/client-server-api/#definition-clientevent_unsigneddata + */ +class UnsignedData implements \JsonSerializable +{ + public function __construct( + private ?int $age = null, + private ?MembershipState $membership = null, + private ?array $previousContent = null, + private ?ClientEvent $redactedBecause = null, + private ?string $transactionId = null, + ) + {} + + public function jsonSerialize(): array + { + return array_filter([ + "age" => $this->age, + "membership" => $this->membership, + "prev_content" => $this->previousContent, + "redacted_because" => $this->redactedBecause, + "transaction_id" => $this->transactionId, + ], fn ($value) => ! is_null($value)); + } +} diff --git a/matrix-specification/Message.php b/matrix-specification/Message.php new file mode 100644 index 0000000..015348a --- /dev/null +++ b/matrix-specification/Message.php @@ -0,0 +1,18 @@ +<?php + +namespace Matrix; + +abstract class Message implements \JsonSerializable +{ + public function setDefaults(): void {} + + /** + * @return array<string, string> + */ + abstract public function getBody(): array; + + public function jsonSerialize(): array + { + return $this->getBody(); + } +} diff --git a/matrix-specification/Request.php b/matrix-specification/Request.php new file mode 100644 index 0000000..c8a13d6 --- /dev/null +++ b/matrix-specification/Request.php @@ -0,0 +1,15 @@ +<?php + +namespace Matrix; + +use Matrix\Enums\ApiPathVersion; + +abstract class Request extends Message +{ + abstract public function getUri(string $scheme, string $serverName, ApiPathVersion $version): string; + + /** + * @return array<string, string> + */ + abstract public function getQueryParameters(): array; +} diff --git a/matrix-specification/Requests/ClientAccountWhoamiGetRequest.php b/matrix-specification/Requests/ClientAccountWhoamiGetRequest.php new file mode 100644 index 0000000..50313bb --- /dev/null +++ b/matrix-specification/Requests/ClientAccountWhoamiGetRequest.php @@ -0,0 +1,27 @@ +<?php + +namespace Matrix\Requests; + +use Matrix\Enums\ApiPathVersion; +use Matrix\Request; + +class ClientAccountWhoamiGetRequest extends Request implements RateLimited, RequiresAuthentication +{ + public function __construct() + {} + + public function getUri(string $scheme, string $serverName, ApiPathVersion $version): string + { + return "{$scheme}://{$serverName}/_matrix/client/{$version}/account/whoami"; + } + + public function getQueryParameters(): array + { + return []; + } + + public function getBody(): array + { + return []; + } +} diff --git a/matrix-specification/Requests/ClientDirectoryRoomAliasGetRequest.php b/matrix-specification/Requests/ClientDirectoryRoomAliasGetRequest.php new file mode 100644 index 0000000..ea104a2 --- /dev/null +++ b/matrix-specification/Requests/ClientDirectoryRoomAliasGetRequest.php @@ -0,0 +1,29 @@ +<?php + +namespace Matrix\Requests; + +use Matrix\Enums\ApiPathVersion; +use Matrix\Request; + +class ClientDirectoryRoomAliasGetRequest extends Request +{ + public function __construct( + private string $roomAlias, + ) + {} + + public function getUri(string $scheme, string $serverName, ApiPathVersion $version): string + { + return "{$scheme}://{$serverName}/_matrix/client/{$version}/directory/room/{$this->roomAlias}"; + } + + public function getQueryParameters(): array + { + return []; + } + + public function getBody(): array + { + return []; + } +} diff --git a/matrix-specification/Requests/ClientKeysUploadPostRequest.php b/matrix-specification/Requests/ClientKeysUploadPostRequest.php new file mode 100644 index 0000000..05b2fde --- /dev/null +++ b/matrix-specification/Requests/ClientKeysUploadPostRequest.php @@ -0,0 +1,41 @@ +<?php + +namespace Matrix\Requests; + +use Matrix\Data\DeviceKeys; +use Matrix\Data\KeyObject; +use Matrix\Enums\ApiPathVersion; +use Matrix\Request; + +class ClientKeysUploadPostRequest extends Request implements RateLimited, RequiresAuthentication +{ + /** + * @param array<string, string|KeyObject> $fallbackKeys + * @param array<string, string|KeyObject> $oneTimeKeys + */ + public function __construct( + private ?DeviceKeys $deviceKeys = null, + private ?array $fallbackKeys = null, + private ?array $oneTimeKeys = null, + ) + {} + + public function getUri(string $scheme, string $serverName, ApiPathVersion $version): string + { + return "{$scheme}://{$serverName}/_matrix/client/{$version}/keys/upload"; + } + + public function getQueryParameters(): array + { + return []; + } + + public function getBody(): array + { + return array_filter([ + "device_keys" => $this->deviceKeys, + "fallback_keys" => $this->fallbackKeys, + "one_time_keys" => $this->oneTimeKeys, + ], fn ($value) => ! is_null($value)); + } +} diff --git a/matrix-specification/Requests/ClientLoginPostRequest.php b/matrix-specification/Requests/ClientLoginPostRequest.php new file mode 100644 index 0000000..161c6de --- /dev/null +++ b/matrix-specification/Requests/ClientLoginPostRequest.php @@ -0,0 +1,65 @@ +<?php + +namespace Matrix\Requests; + +use Matrix\Data\UserIdentifier; +use Matrix\Enums\ApiPathVersion; +use Matrix\Enums\LoginType; +use Matrix\Request; + +class ClientLoginPostRequest extends Request implements RateLimited +{ + public function __construct( + private LoginType $type, + private ?string $deviceId = null, + private ?UserIdentifier $identifier = null, + private ?string $initialDeviceDisplayName = null, + private ?string $password = null, + private ?bool $refreshToken = null, + private ?string $token = null, + ) + { + if ($type == LoginType::PASSWORD && is_null($password)) { + throw new \InvalidArgumentException("password is required when using LoginType password"); + } + + if ($type == LoginType::TOKEN && is_null($token)) { + throw new \InvalidArgumentException("token is required when using LoginType token"); + } + } + + public function getUri(string $scheme, string $serverName, ApiPathVersion $version): string + { + return "{$scheme}://{$serverName}/_matrix/client/{$version}/login"; + } + + public function getQueryParameters(): array + { + return []; + } + + public function getBody(): array + { + $request = [ + "device_id" => $this->deviceId, + "identifier" => $this->identifier, + "initial_device_display_name" => $this->initialDeviceDisplayName, + "refresh_token" => $this->refreshToken, + "type" => $this->type, + ]; + + $request += match ($this->type) { + LoginType::PASSWORD => [ + "password" => $this->password, + ], + + LoginType::TOKEN => [ + "token" => $this->token, + ], + + default => [], + }; + + return array_filter($request, fn ($value) => ! is_null($value)); + } +} diff --git a/matrix-specification/Requests/ClientRefreshPostRequest.php b/matrix-specification/Requests/ClientRefreshPostRequest.php new file mode 100644 index 0000000..3733945 --- /dev/null +++ b/matrix-specification/Requests/ClientRefreshPostRequest.php @@ -0,0 +1,31 @@ +<?php + +namespace Matrix\Requests; + +use Matrix\Enums\ApiPathVersion; +use Matrix\Request; + +class ClientRefreshPostRequest extends Request implements RateLimited +{ + public function __construct( + private string $refreshToken, + ) + {} + + public function getUri(string $scheme, string $serverName, ApiPathVersion $version): string + { + return "{$scheme}://{$serverName}/_matrix/client/{$version}/refresh"; + } + + public function getQueryParameters(): array + { + return []; + } + + public function getBody(): array + { + return [ + "refresh_token" => $this->refreshToken, + ]; + } +} diff --git a/matrix-specification/Requests/ClientRegisterPostRequest.php b/matrix-specification/Requests/ClientRegisterPostRequest.php new file mode 100644 index 0000000..74c0c1d --- /dev/null +++ b/matrix-specification/Requests/ClientRegisterPostRequest.php @@ -0,0 +1,57 @@ +<?php + +namespace Matrix\Requests; + +use Matrix\Data\AuthenticationData; +use Matrix\Enums\ApiPathVersion; +use Matrix\Enums\UserRegistrationKind; +use Matrix\Request; + +/** + * @see https://spec.matrix.org/v1.16/client-server-api/#post_matrixclientv3register + */ +class ClientRegisterPostRequest extends Request implements RateLimited +{ + public function __construct( + private AuthenticationData $authenticationData, + private string $password, + private ?UserRegistrationKind $kind = null, + private ?string $deviceId = null, + private ?bool $inhibitLogin = null, + private ?string $initialDeviceDisplayName = null, + private ?string $username = null, + private ?bool $refreshToken = null, + ) + {} + + public function setDefaults(): void + { + $this->kind ??= UserRegistrationKind::USER; + $this->inhibitLogin ??= false; + } + + public function getUri(string $scheme, string $serverName, ApiPathVersion $version): string + { + return "{$scheme}://{$serverName}/_matrix/client/{$version}/register"; + } + + public function getQueryParameters(): array + { + return array_filter([ + "kind" => $this->kind, + ], fn ($value) => ! is_null($value)); + } + + public function getBody(): array + { + return array_filter([ + "auth" => $this->authenticationData, + "device_id" => $this->deviceId, + "inhibit_login" => $this->inhibitLogin, + "initial_device_display_name" => $this->initialDeviceDisplayName, + "password" => $this->password, + "refresh_token" => $this->refreshToken, + "username" => $this->username, + ], fn ($value) => ! is_null($value)); + } +} diff --git a/matrix-specification/Requests/ClientSyncGetRequest.php b/matrix-specification/Requests/ClientSyncGetRequest.php new file mode 100644 index 0000000..f19e820 --- /dev/null +++ b/matrix-specification/Requests/ClientSyncGetRequest.php @@ -0,0 +1,50 @@ +<?php + +namespace Matrix\Requests; + +use Matrix\Enums\ApiPathVersion; +use Matrix\Enums\PresenceState; +use Matrix\Request; + +class ClientSyncGetRequest extends Request implements RequiresAuthentication +{ + public function __construct( + private ?string $filter = null, + private ?bool $fullState = null, + private ?PresenceState $setPresence = null, + private ?string $since = null, + private ?int $timeout = null, + private ?bool $useStateAfter = null, + ) + {} + + public function setDefaults(): void + { + $this->fullState ??= false; + $this->setPresence ??= PresenceState::ONLINE; + $this->timeout ??= 0; + $this->useStateAfter ??= false; + } + + public function getUri(string $scheme, string $serverName, ApiPathVersion $version): string + { + return "{$scheme}://{$serverName}/_matrix/client/{$version}/sync"; + } + + public function getQueryParameters(): array + { + return array_filter([ + "filter" => $this->filter, + "full_state" => $this->fullState, + "set_presence" => $this->setPresence, + "since" => $this->since, + "timeout" => $this->timeout, + "use_state_after" => $this->useStateAfter, + ], fn ($value) => ! is_null($value)); + } + + public function getBody(): array + { + return []; + } +} diff --git a/matrix-specification/Requests/ClientUserIdFilterPostRequest.php b/matrix-specification/Requests/ClientUserIdFilterPostRequest.php new file mode 100644 index 0000000..cd01532 --- /dev/null +++ b/matrix-specification/Requests/ClientUserIdFilterPostRequest.php @@ -0,0 +1,45 @@ +<?php + +namespace Matrix\Requests; + +use Matrix\Data\Filters\EventFilter; +use Matrix\Data\Filters\RoomFilter; +use Matrix\Enums\ApiPathVersion; +use Matrix\Request; + +class ClientUserIdFilterPostRequest extends Request implements RequiresAuthentication +{ + /** + * @param string[] $eventFields + */ + public function __construct( + private string $userId, + private ?EventFilter $accountData = null, + private ?array $eventFields = null, + private ?string $eventFormat = null, + private ?EventFilter $presence = null, + private ?RoomFilter $room = null, + ) + {} + + public function getUri(string $scheme, string $serverName, ApiPathVersion $version): string + { + return "{$scheme}://{$serverName}/_matrix/client/{$version}/user/{$this->userId}/filter"; + } + + public function getQueryParameters(): array + { + return []; + } + + public function getBody(): array + { + return [ + "account_data" => $this->accountData, + "event_fields" => $this->eventFields, + "event_format" => $this->eventFormat, + "presence" => $this->presence, + "room" => $this->room, + ]; + } +} diff --git a/matrix-specification/Requests/RateLimited.php b/matrix-specification/Requests/RateLimited.php new file mode 100644 index 0000000..a75054f --- /dev/null +++ b/matrix-specification/Requests/RateLimited.php @@ -0,0 +1,6 @@ +<?php + +namespace Matrix\Requests; + +interface RateLimited +{} diff --git a/matrix-specification/Requests/RequiresAuthentication.php b/matrix-specification/Requests/RequiresAuthentication.php new file mode 100644 index 0000000..cc4b3e6 --- /dev/null +++ b/matrix-specification/Requests/RequiresAuthentication.php @@ -0,0 +1,6 @@ +<?php + +namespace Matrix\Requests; + +interface RequiresAuthentication +{} diff --git a/matrix-specification/Requests/RequiresAuthenticationOptional.php b/matrix-specification/Requests/RequiresAuthenticationOptional.php new file mode 100644 index 0000000..2ff7980 --- /dev/null +++ b/matrix-specification/Requests/RequiresAuthenticationOptional.php @@ -0,0 +1,6 @@ +<?php + +namespace Matrix\Requests; + +interface RequiresAuthenticationOptional +{} diff --git a/matrix-specification/Response.php b/matrix-specification/Response.php new file mode 100644 index 0000000..d44acb6 --- /dev/null +++ b/matrix-specification/Response.php @@ -0,0 +1,8 @@ +<?php + +namespace Matrix; + +abstract class Response extends Message +{ + public function validateRequired(): void {} +} diff --git a/matrix-specification/Responses/ClientAccountWhoamiGetResponse.php b/matrix-specification/Responses/ClientAccountWhoamiGetResponse.php new file mode 100644 index 0000000..2f7ff81 --- /dev/null +++ b/matrix-specification/Responses/ClientAccountWhoamiGetResponse.php @@ -0,0 +1,24 @@ +<?php + +namespace Matrix\Responses; + +use Matrix\Response; + +class ClientAccountWhoamiGetResponse extends Response +{ + public function __construct( + private string $userId, + private ?string $deviceId = null, + private ?bool $isGuest = null, + ) + {} + + public function getBody(): array + { + return array_filter([ + "device_id" => $this->deviceId, + "is_guest" => $this->isGuest, + "user_id" => $this->userId, + ], fn ($value) => ! is_null($value)); + } +} diff --git a/matrix-specification/Responses/ClientDirectoryRoomAliasGetResponse.php b/matrix-specification/Responses/ClientDirectoryRoomAliasGetResponse.php new file mode 100644 index 0000000..4a5977a --- /dev/null +++ b/matrix-specification/Responses/ClientDirectoryRoomAliasGetResponse.php @@ -0,0 +1,25 @@ +<?php + +namespace Matrix\Responses; + +use Matrix\Response; + +class ClientDirectoryRoomAliasGetResponse extends Response +{ + /** + * @param string[] $servers + */ + public function __construct( + private string $roomId, + private array $servers, + ) + {} + + public function getBody(): array + { + return [ + "room_id" => $this->roomId, + "servers" => $this->servers, + ]; + } +} diff --git a/matrix-specification/Responses/ClientKeysUploadPostResponse.php b/matrix-specification/Responses/ClientKeysUploadPostResponse.php new file mode 100644 index 0000000..3d335e4 --- /dev/null +++ b/matrix-specification/Responses/ClientKeysUploadPostResponse.php @@ -0,0 +1,26 @@ +<?php + +namespace Matrix\Responses; + +use Matrix\Response; + +/** + * @see https://spec.matrix.org/v1.16/client-server-api/#post_matrixclientv3keysupload + */ +class ClientKeysUploadPostResponse extends Response +{ + /** + * @param array<string, integer> $oneTimeKeyCounts + */ + public function __construct( + private array $oneTimeKeyCounts, + ) + {} + + public function getBody(): array + { + return [ + "one_time_keys_counts" => $this->oneTimeKeyCounts, + ]; + } +} diff --git a/matrix-specification/Responses/ClientLoginGetResponse.php b/matrix-specification/Responses/ClientLoginGetResponse.php new file mode 100644 index 0000000..b8badbd --- /dev/null +++ b/matrix-specification/Responses/ClientLoginGetResponse.php @@ -0,0 +1,24 @@ +<?php + +namespace Matrix\Responses; + +use Matrix\Data\LoginFlow; +use Matrix\Response; + +class ClientLoginGetResponse extends Response +{ + /** + * @param LoginFlow[] $loginFlows + */ + public function __construct( + private array $loginFlows, + ) + {} + + public function getBody(): array + { + return [ + "flows" => $this->loginFlows, + ]; + } +} diff --git a/matrix-specification/Responses/ClientLoginPostResponse.php b/matrix-specification/Responses/ClientLoginPostResponse.php new file mode 100644 index 0000000..4a0fa7d --- /dev/null +++ b/matrix-specification/Responses/ClientLoginPostResponse.php @@ -0,0 +1,31 @@ +<?php + +namespace Matrix\Responses; + +use Matrix\Data\DiscoveryInformation; +use Matrix\Response; + +class ClientLoginPostResponse extends Response +{ + public function __construct( + private string $accessToken, + private string $deviceId, + private string $userId, + private ?int $expiresInMilliseconds = null, + private ?string $refreshToken = null, + private ?DiscoveryInformation $wellKnown = null, + ) + {} + + public function getBody(): array + { + return array_filter([ + "access_token" => $this->accessToken, + "device_id" => $this->deviceId, + "expires_in_ms" => $this->expiresInMilliseconds, + "refresh_token" => $this->refreshToken, + "user_id" => $this->userId, + "well_known" => $this->wellKnown, + ], fn ($value) => ! is_null($value)); + } +} diff --git a/matrix-specification/Responses/ClientRefreshPostResponse.php b/matrix-specification/Responses/ClientRefreshPostResponse.php new file mode 100644 index 0000000..38519a3 --- /dev/null +++ b/matrix-specification/Responses/ClientRefreshPostResponse.php @@ -0,0 +1,24 @@ +<?php + +namespace Matrix\Responses; + +use Matrix\Response; + +class ClientRefreshPostResponse extends Response +{ + public function __construct( + private string $accessToken, + private ?int $expiresInMilliseconds = null, + private ?string $refreshToken = null, + ) + {} + + public function getBody(): array + { + return array_filter([ + "access_token" => $this->accessToken, + "expires_in_ms" => $this->expiresInMilliseconds, + "refresh_token" => $this->refreshToken, + ], fn ($value) => ! is_null($value)); + } +} diff --git a/matrix-specification/Responses/ClientRegisterPostResponse.php b/matrix-specification/Responses/ClientRegisterPostResponse.php new file mode 100644 index 0000000..6ed65ce --- /dev/null +++ b/matrix-specification/Responses/ClientRegisterPostResponse.php @@ -0,0 +1,39 @@ +<?php + +namespace Matrix\Responses; + +use Matrix\Requests\ClientRegisterPostRequest; +use Matrix\Response; + +class ClientRegisterPostResponse extends Response +{ + public function __construct( + private string $userId, + private ?string $accessToken = null, + private ?string $deviceId = null, + private ?int $expiresInMilliseconds = null, + private ?string $homeServer = null, + private ?string $refreshToken = null, + ) + {} + + public function validateRequired(ClientRegisterPostRequest $request): void + { + $requestBody = $request->getBody(); + if ($requestBody["inhibit_login"] === false) { + # TODO: validate + } + } + + public function getBody(): array + { + return array_filter([ + "access_token" => $this->accessToken, + "device_id" => $this->deviceId, + "expires_in_ms" => $this->expiresInMilliseconds, + "home_server" => $this->homeServer, + "refresh_token" => $this->refreshToken, + "user_id" => $this->userId, + ], fn ($value) => ! is_null($value)); + } +} diff --git a/matrix-specification/Responses/ClientSyncGetResponse.php b/matrix-specification/Responses/ClientSyncGetResponse.php new file mode 100644 index 0000000..dbe2a29 --- /dev/null +++ b/matrix-specification/Responses/ClientSyncGetResponse.php @@ -0,0 +1,40 @@ +<?php + +namespace Matrix\Responses; + +use Matrix\Data\AccountData; +use Matrix\Data\DeviceLists; +use Matrix\Data\Presence; +use Matrix\Data\Room\Rooms; +use Matrix\Data\ToDevice; +use Matrix\Response; + +class ClientSyncGetResponse extends Response +{ + /** + * @param array<string, int> $deviceOneTimeKeysCount + */ + public function __construct( + private string $nextBatch, + private ?AccountData $accountData = null, + private ?DeviceLists $deviceLists = null, + private ?array $deviceOneTimeKeysCount = null, + private ?Presence $presence = null, + private ?Rooms $rooms = null, + private ?ToDevice $toDevice = null, + ) + {} + + public function getBody(): array + { + return array_filter([ + "account_data" => $this->accountData, + "device_lists" => $this->deviceLists, + "device_one_time_keys_count" => $this->deviceOneTimeKeysCount, + "next_batch" => $this->nextBatch, + "presence" => $this->presence, + "rooms" => $this->rooms, + "to_device" => $this->toDevice, + ], fn ($value) => ! is_null($value)); + } +} diff --git a/matrix-specification/Responses/ClientUserIdFilterPostResponse.php b/matrix-specification/Responses/ClientUserIdFilterPostResponse.php new file mode 100644 index 0000000..0a5d062 --- /dev/null +++ b/matrix-specification/Responses/ClientUserIdFilterPostResponse.php @@ -0,0 +1,27 @@ +<?php + +namespace Matrix\Responses; + +use Matrix\Response; + +class ClientUserIdFilterPostResponse extends Response +{ + public function __construct( + private string $filterId, + ) + { + if (str_starts_with($filterId, "{")) { + throw new \InvalidArgumentException( + "filterId cannot start with a { as this character is used to determine if the filter provided is inline JSON " . + "or a previously declared filter by homeservers on some APIs" + ); + } + } + + public function getBody(): array + { + return [ + "filter_id" => $this->filterId, + ]; + } +} diff --git a/matrix-specification/Responses/ClientVersionsGetResponse.php b/matrix-specification/Responses/ClientVersionsGetResponse.php new file mode 100644 index 0000000..44d2cd0 --- /dev/null +++ b/matrix-specification/Responses/ClientVersionsGetResponse.php @@ -0,0 +1,26 @@ +<?php + +namespace Matrix\Responses; + +use Matrix\Response; + +class ClientVersionsGetResponse extends Response +{ + /** + * @param string[] $versions + * @param array<string, bool> $unstableFeatures + */ + public function __construct( + private array $versions, + private ?array $unstableFeatures = null, + ) + {} + + public function getBody(): array + { + return array_filter([ + "unstable_features" => $this->unstableFeatures, + "versions" => $this->versions, + ], fn ($value) => ! is_null($value)); + } +} diff --git a/matrix-specification/Responses/WellKnownMatrixClientGetResponse.php b/matrix-specification/Responses/WellKnownMatrixClientGetResponse.php new file mode 100644 index 0000000..2a0c6d1 --- /dev/null +++ b/matrix-specification/Responses/WellKnownMatrixClientGetResponse.php @@ -0,0 +1,19 @@ +<?php + +namespace Matrix\Responses; + +use Matrix\Data\DiscoveryInformation; +use Matrix\Response; + +class WellKnownMatrixClientGetResponse extends Response +{ + public function __construct( + private DiscoveryInformation $discoveryInformation, + ) + {} + + public function getBody(): array + { + return $this->discoveryInformation->jsonSerialize(); + } +} diff --git a/matrix-specification/Responses/WellKnownMatrixSupportGetResponse.php b/matrix-specification/Responses/WellKnownMatrixSupportGetResponse.php new file mode 100644 index 0000000..bdd971a --- /dev/null +++ b/matrix-specification/Responses/WellKnownMatrixSupportGetResponse.php @@ -0,0 +1,34 @@ +<?php + +namespace Matrix\Responses; + +use Matrix\Data\Contact; +use Matrix\Response; + +class WellKnownMatrixSupportGetResponse extends Response +{ + /** + * @param Contact[] $contacts + */ + public function __construct( + private ?array $contacts = null, + private ?string $supportPage = null, + ) + { + if (is_null($contacts) && is_null($supportPage)) { + throw new \InvalidArgumentException("at least one of contacts or supportPage is required"); + } + + if (! is_null($contacts) && is_null($supportPage) && empty($contacts)) { + throw new \InvalidArgumentException("if only contacts is set, it must contain at least one item"); + } + } + + public function getBody(): array + { + return array_filter([ + "contacts" => $this->contacts, + "support_page" => $this->supportPage, + ], fn ($value) => ! is_null($value)); + } +} diff --git a/src/Controllers/AccountController.php b/src/Controllers/AccountController.php index 858a6b5..8e20880 100755 --- a/src/Controllers/AccountController.php +++ b/src/Controllers/AccountController.php @@ -2,9 +2,9 @@ namespace App\Controllers; -use App\Errors\UnauthorizedError; use App\Models\Device; use App\Models\User; +use Matrix\Responses\ClientAccountWhoamiGetResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\JsonResponse; @@ -18,18 +18,12 @@ class AccountController */ public function whoami(Request $request): Response { - $accessToken = str_replace("Bearer ", "", $request->headers->get("authorization") ?: ""); - $user = User::fetchWithAccessToken($accessToken); - - if (empty($user)) { - throw new UnauthorizedError(); - } - + $user = User::authenticateWithRequest($request); $device = Device::fetch(userId: $user->getId()); - return new JsonResponse([ - "device_id" => $device->getId(), - "user_id" => $user->getId(), - ]); + return new JsonResponse(new ClientAccountWhoamiGetResponse( + userId: $user->getId(), + deviceId: $device->getId(), + )); } } diff --git a/src/Controllers/KeyController.php b/src/Controllers/KeyController.php index a8b4fb1..7777229 100644 --- a/src/Controllers/KeyController.php +++ b/src/Controllers/KeyController.php @@ -4,10 +4,11 @@ namespace App\Controllers; use App\Errors\AppException; use App\Errors\ErrorCode; -use App\Errors\UnauthorizedError; use App\Models\Tokens; use App\Models\User; use App\Support\RequestValidator; +use Matrix\Responses\ClientKeysUploadPostResponse; +use Matrix\Responses\ClientRefreshPostResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\JsonResponse; @@ -29,22 +30,14 @@ class KeyController */ public function upload(Request $request): Response { - $accessToken = str_replace("Bearer ", "", $request->headers->get("authorization") ?: ""); - $user = User::fetchWithAccessToken($accessToken); - - if (empty($user)) { - throw new UnauthorizedError(); - } - + $user = User::authenticateWithRequest($request); $body = json_decode($request->getContent(), true); RequestValidator::validateJson(); - return new JsonResponse([ - "one_time_key_counts" => [ - "curve25519" => 0, - "signed_curve25519" => count($body["one_time_keys"]) - ], - ]); + return new JsonResponse(new ClientKeysUploadPostResponse([ + "curve25519" => 0, + "signed_curve25519" => count($body["one_time_keys"]), + ])); } public function query(Request $request): Response @@ -74,10 +67,10 @@ class KeyController $newTokens = Tokens::new($tokens->getUserId(), $tokens->getDeviceId()); $newTokens->insert(); - return new JsonResponse([ - "access_token" => $newTokens->getAccessToken(), - "expires_in" => $newTokens->getExpiresIn(), - "refresh_token" => $newTokens->getRefreshToken(), - ]); + return new JsonResponse(new ClientRefreshPostResponse( + accessToken: $newTokens->getAccessToken(), + expiresInMilliseconds: $newTokens->getExpiresIn(), + refreshToken: $newTokens->getRefreshToken(), + )); } } diff --git a/src/Controllers/LoginController.php b/src/Controllers/LoginController.php index 15f1583..c520e25 100644 --- a/src/Controllers/LoginController.php +++ b/src/Controllers/LoginController.php @@ -10,9 +10,12 @@ use App\Models\Device; use App\Models\Tokens; use App\Models\User; use App\Support\RequestValidator; -use App\Types\LoginFlow; -use App\Types\LoginType; use App\Types\UserRegistrationKind; +use Matrix\Data\LoginFlow; +use Matrix\Enums\LoginType; +use Matrix\Responses\ClientLoginGetResponse; +use Matrix\Responses\ClientLoginPostResponse; +use Matrix\Responses\ClientRegisterPostResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\JsonResponse; @@ -24,11 +27,9 @@ class LoginController */ public function supportedLoginTypes(Request $request): Response { - return new JsonResponse([ - "flows" => [ - (new LoginFlow(LoginType::PASSWORD))->toArray(), - ], - ]); + return new JsonResponse(new ClientLoginGetResponse([ + (new LoginFlow(LoginType::PASSWORD)), + ])); } /** @@ -87,14 +88,13 @@ class LoginController } } - return new JsonResponse([ - "access_token" => $tokens->getAccessToken(), - "device_id" => $device->getId(), - "expires_in_ms" => $tokens->getExpiresIn(), - "refresh_token" => $tokens->getRefreshToken(), - "user_id" => $user->getId(), - #"well_known" => [], - ]); + return new JsonResponse(new ClientLoginPostResponse( + accessToken: $tokens->getAccessToken(), + deviceId:$device->getId(), + userId: $user->getId(), + expiresInMilliseconds: $tokens->getExpiresIn(), + refreshToken: $tokens->getRefreshToken(), + )); } /** @@ -130,12 +130,12 @@ class LoginController $tokens = Tokens::new($userId, $device->getId()); $tokens->insert(); - return new JsonResponse([ - "access_token" => $tokens->getAccessToken(), - "device_id" => $device->getId(), - "expires_in_ms" => $tokens->getExpiresIn(), - "refresh_token" => $tokens->getRefreshToken(), - "user_id" => $userId, - ]); + return new JsonResponse(new ClientRegisterPostResponse( + accessToken: $tokens->getAccessToken(), + deviceId: $device->getId(), + expiresInMilliseconds: $tokens->getExpiresIn(), + refreshToken: $tokens->getRefreshToken(), + userId: $userId, + )); } } diff --git a/src/Models/User.php b/src/Models/User.php index 423394a..c0c73f8 100644 --- a/src/Models/User.php +++ b/src/Models/User.php @@ -3,7 +3,9 @@ namespace App\Models; use App\Database; +use App\Errors\UnauthorizedError; use App\Support\ConnectsToDatabase; +use Symfony\Component\HttpFoundation\Request; class User implements ConnectsToDatabase { @@ -50,7 +52,7 @@ class User implements ConnectsToDatabase return self::fromDatabase($row); } - public static function fetchWithAccessToken(string $accessToken): ?static + public static function fetchWithAccessToken(string $accessToken): ?self { $row = Database::getInstance()->query(<<<SQL select users.* from users left join tokens on tokens.user_id = users.id where tokens.access_token=:access_token @@ -70,6 +72,18 @@ class User implements ConnectsToDatabase return new self($id); } + public static function authenticateWithRequest(Request $request): self + { + $accessToken = str_replace("Bearer ", "", $request->headers->get("authorization") ?: ""); + $user = self::fetchWithAccessToken($accessToken); + + if (empty($user)) { + throw new UnauthorizedError(); + } + + return $user; + } + public function insert(): bool { return !! Database::getInstance()->query( |
