diff options
| author | Daniel Weipert <git@mail.dweipert.de> | 2025-09-19 17:03:47 +0200 | 
|---|---|---|
| committer | Daniel Weipert <git@mail.dweipert.de> | 2025-09-19 17:03:47 +0200 | 
| commit | b08047ba485f86038f33175162943c0a9878ab1a (patch) | |
| tree | 49980e78eb3e641b04ddcc0817f74e89dc07f2b7 | |
| parent | 2ae0c2fa2a0bb5a7cd1fd9da1c6d2a6090126e67 (diff) | |
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; +  } +} | 
