diff options
28 files changed, 599 insertions, 5 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..14c1b94 100644 --- a/composer.json +++ b/composer.json @@ -8,10 +8,10 @@ } ], "require": { + "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,7 +20,8 @@ "autoload": { "psr-4": { "App\\": "src/", - "Tests\\": "tests/" + "Tests\\": "tests/", + "Matrix\\": "matrix-specification" } }, "scripts": { diff --git a/composer.lock b/composer.lock index bb1e9cb..c80924c 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "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": "b8c9f3de980f8dc287642ead631cec2e", "packages": [ { "name": "psr/log", diff --git a/matrix-specification/AccountData.php b/matrix-specification/AccountData.php new file mode 100644 index 0000000..b29e7fe --- /dev/null +++ b/matrix-specification/AccountData.php @@ -0,0 +1,16 @@ +<?php + +namespace Matrix; + +use Matrix\Events\Event; + +class AccountData +{ + /** + * @param Event[] $events + */ + public function __construct( + private array $events + ) + {} +} 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..bf805b2 --- /dev/null +++ b/matrix-specification/Enums/EventType.php @@ -0,0 +1,17 @@ +<?php + +namespace Matrix\Enums; + +enum EventType: string implements \JsonSerializable +{ + case PRESENCE = "m.presence"; + + case ROOM_MEMBER = "m.room.member"; + case ROOM_MESSAGE = "m.room.message"; + case ROOM_NAME = "m.room.name"; + + 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/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/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..f71d4bd --- /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 = "", + bool $currentlyActive = false, + string $displayName = "", + int $lastActiveAgo = 0, + string $statusMessage = "", + ) + { + parent::__construct( + [ + "avatar_url" => $avatarUrl, + "currently_active" => $currentlyActive, + "display_name" => $displayName, + "last_active_ago" => $lastActiveAgo, + "presence" => $presence, + "status_msg" => $statusMessage, + ], + $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/UnsignedData.php b/matrix-specification/Events/UnsignedData.php new file mode 100644 index 0000000..924af29 --- /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, + ], "is_null"); + } +} diff --git a/matrix-specification/Requests/ClientLoginGetRequest.php b/matrix-specification/Requests/ClientLoginGetRequest.php new file mode 100644 index 0000000..73cc24c --- /dev/null +++ b/matrix-specification/Requests/ClientLoginGetRequest.php @@ -0,0 +1,6 @@ +<?php + +namespace Matrix\Requests; + +class ClientLoginGetRequest implements RateLimited +{} diff --git a/matrix-specification/Requests/ClientLoginPostRequest.php b/matrix-specification/Requests/ClientLoginPostRequest.php new file mode 100644 index 0000000..d17c558 --- /dev/null +++ b/matrix-specification/Requests/ClientLoginPostRequest.php @@ -0,0 +1,53 @@ +<?php + +namespace Matrix\Requests; + +use Matrix\Enums\LoginType; +use Matrix\UserIdentifier; + +class ClientLoginPostRequest implements RateLimited, \JsonSerializable +{ + 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 jsonSerialize(): 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, "is_null"); + } +} diff --git a/matrix-specification/Requests/ClientSyncGetRequest.php b/matrix-specification/Requests/ClientSyncGetRequest.php new file mode 100644 index 0000000..2921fcf --- /dev/null +++ b/matrix-specification/Requests/ClientSyncGetRequest.php @@ -0,0 +1,8 @@ +<?php + +namespace Matrix\Requests; + +class ClientSyncGetRequest +{ + # TODO +} diff --git a/matrix-specification/Requests/RateLimited.php b/matrix-specification/Requests/RateLimited.php new file mode 100644 index 0000000..9f917a4 --- /dev/null +++ b/matrix-specification/Requests/RateLimited.php @@ -0,0 +1,8 @@ +<?php + +namespace Matrix\Requests; + +interface RateLimited +{ + # TODO +} diff --git a/matrix-specification/Requests/RequiresAuthentication.php b/matrix-specification/Requests/RequiresAuthentication.php new file mode 100644 index 0000000..c494f13 --- /dev/null +++ b/matrix-specification/Requests/RequiresAuthentication.php @@ -0,0 +1,8 @@ +<?php + +namespace Matrix\Requests; + +interface RequiresAuthentication +{ + public function authenticateUser(): bool; +} diff --git a/matrix-specification/Responses/ClientLoginGetResponse.php b/matrix-specification/Responses/ClientLoginGetResponse.php new file mode 100644 index 0000000..e0ccc26 --- /dev/null +++ b/matrix-specification/Responses/ClientLoginGetResponse.php @@ -0,0 +1,21 @@ +<?php + +namespace Matrix\Responses; + +class ClientLoginGetResponse implements \JsonSerializable +{ + /** + * @param LoginFlow[] $loginFlows + */ + public function __construct( + private array $loginFlows, + ) + {} + + public function jsonSerialize(): array + { + return [ + "flows" => $this->loginFlows, + ]; + } +} diff --git a/matrix-specification/Responses/ClientSyncGetResponse.php b/matrix-specification/Responses/ClientSyncGetResponse.php new file mode 100644 index 0000000..27464c1 --- /dev/null +++ b/matrix-specification/Responses/ClientSyncGetResponse.php @@ -0,0 +1,8 @@ +<?php + +namespace Matrix\Responses; + +class ClientSyncGetResponse +{ + # TODO +} diff --git a/matrix-specification/Responses/LoginFlow.php b/matrix-specification/Responses/LoginFlow.php new file mode 100644 index 0000000..6ddb703 --- /dev/null +++ b/matrix-specification/Responses/LoginFlow.php @@ -0,0 +1,31 @@ +<?php + +namespace Matrix\Responses; + +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/UserIdentifier.php b/matrix-specification/UserIdentifier.php new file mode 100644 index 0000000..63f939a --- /dev/null +++ b/matrix-specification/UserIdentifier.php @@ -0,0 +1,45 @@ +<?php + +namespace Matrix; + +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; + } +} |