summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Readme.md5
-rw-r--r--composer.json6
-rw-r--r--composer.lock120
-rw-r--r--docker-compose.yml2
-rw-r--r--matrix-specification/Data/AccountData.php23
-rw-r--r--matrix-specification/Data/AuthenticationData.php24
-rw-r--r--matrix-specification/Data/Contact.php28
-rw-r--r--matrix-specification/Data/DeviceKeys.php31
-rw-r--r--matrix-specification/Data/DeviceLists.php24
-rw-r--r--matrix-specification/Data/DiscoveryInformation.php30
-rw-r--r--matrix-specification/Data/Filters/EventFilter.php36
-rw-r--r--matrix-specification/Data/Filters/RoomEventFilter.php50
-rw-r--r--matrix-specification/Data/Filters/RoomFilter.php39
-rw-r--r--matrix-specification/Data/HomeServerInformation.php18
-rw-r--r--matrix-specification/Data/IdentityServerInformation.php18
-rw-r--r--matrix-specification/Data/KeyObject.php32
-rw-r--r--matrix-specification/Data/LoginFlow.php31
-rw-r--r--matrix-specification/Data/Presence.php23
-rw-r--r--matrix-specification/Data/Room/Ephemeral.php21
-rw-r--r--matrix-specification/Data/Room/InviteState.php23
-rw-r--r--matrix-specification/Data/Room/InvitedRoom.php18
-rw-r--r--matrix-specification/Data/Room/JoinedRoom.php37
-rw-r--r--matrix-specification/Data/Room/KnockState.php23
-rw-r--r--matrix-specification/Data/Room/KnockedRoom.php18
-rw-r--r--matrix-specification/Data/Room/LeftRoom.php30
-rw-r--r--matrix-specification/Data/Room/RoomSummary.php25
-rw-r--r--matrix-specification/Data/Room/Rooms.php62
-rw-r--r--matrix-specification/Data/Room/State.php23
-rw-r--r--matrix-specification/Data/Room/ThreadNotificationCounts.php20
-rw-r--r--matrix-specification/Data/Room/Timeline.php27
-rw-r--r--matrix-specification/Data/Room/UnreadNotificationCounts.php20
-rw-r--r--matrix-specification/Data/ToDevice.php23
-rw-r--r--matrix-specification/Data/UserId.php17
-rw-r--r--matrix-specification/Data/UserIdentifier.php45
-rw-r--r--matrix-specification/Enums/ApiPathVersion.php11
-rw-r--r--matrix-specification/Enums/AuthenticationType.php19
-rw-r--r--matrix-specification/Enums/ErrorCode.php52
-rw-r--r--matrix-specification/Enums/EventType.php21
-rw-r--r--matrix-specification/Enums/LoginType.php14
-rw-r--r--matrix-specification/Enums/MembershipState.php17
-rw-r--r--matrix-specification/Enums/PresenceState.php15
-rw-r--r--matrix-specification/Enums/Role.php14
-rw-r--r--matrix-specification/Enums/UserIdentifierType.php15
-rw-r--r--matrix-specification/Enums/UserRegistrationKind.php14
-rw-r--r--matrix-specification/Errors/Error.php41
-rw-r--r--matrix-specification/Errors/RateLimitError.php20
-rw-r--r--matrix-specification/Events/ClientEvent.php32
-rw-r--r--matrix-specification/Events/ClientEventWithoutRoomId.php34
-rw-r--r--matrix-specification/Events/Event.php17
-rw-r--r--matrix-specification/Events/PresenceEvent.php42
-rw-r--r--matrix-specification/Events/SenderEvent.php20
-rw-r--r--matrix-specification/Events/StrippedStateEvent.php31
-rw-r--r--matrix-specification/Events/UnsignedData.php31
-rw-r--r--matrix-specification/Message.php18
-rw-r--r--matrix-specification/Request.php15
-rw-r--r--matrix-specification/Requests/ClientAccountWhoamiGetRequest.php27
-rw-r--r--matrix-specification/Requests/ClientDirectoryRoomAliasGetRequest.php29
-rw-r--r--matrix-specification/Requests/ClientKeysUploadPostRequest.php41
-rw-r--r--matrix-specification/Requests/ClientLoginPostRequest.php65
-rw-r--r--matrix-specification/Requests/ClientRefreshPostRequest.php31
-rw-r--r--matrix-specification/Requests/ClientRegisterPostRequest.php57
-rw-r--r--matrix-specification/Requests/ClientSyncGetRequest.php50
-rw-r--r--matrix-specification/Requests/ClientUserIdFilterPostRequest.php45
-rw-r--r--matrix-specification/Requests/RateLimited.php6
-rw-r--r--matrix-specification/Requests/RequiresAuthentication.php6
-rw-r--r--matrix-specification/Requests/RequiresAuthenticationOptional.php6
-rw-r--r--matrix-specification/Response.php8
-rw-r--r--matrix-specification/Responses/ClientAccountWhoamiGetResponse.php24
-rw-r--r--matrix-specification/Responses/ClientDirectoryRoomAliasGetResponse.php25
-rw-r--r--matrix-specification/Responses/ClientKeysUploadPostResponse.php26
-rw-r--r--matrix-specification/Responses/ClientLoginGetResponse.php24
-rw-r--r--matrix-specification/Responses/ClientLoginPostResponse.php31
-rw-r--r--matrix-specification/Responses/ClientRefreshPostResponse.php24
-rw-r--r--matrix-specification/Responses/ClientRegisterPostResponse.php39
-rw-r--r--matrix-specification/Responses/ClientSyncGetResponse.php40
-rw-r--r--matrix-specification/Responses/ClientUserIdFilterPostResponse.php27
-rw-r--r--matrix-specification/Responses/ClientVersionsGetResponse.php26
-rw-r--r--matrix-specification/Responses/WellKnownMatrixClientGetResponse.php19
-rw-r--r--matrix-specification/Responses/WellKnownMatrixSupportGetResponse.php34
-rwxr-xr-xsrc/Controllers/AccountController.php18
-rw-r--r--src/Controllers/KeyController.php31
-rw-r--r--src/Controllers/LoginController.php44
-rw-r--r--src/Models/User.php16
83 files changed, 2166 insertions, 118 deletions
diff --git a/Readme.md b/Readme.md
index 243eacb..d0c9e00 100644
--- a/Readme.md
+++ b/Readme.md
@@ -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(