summaryrefslogtreecommitdiff
path: root/src/Controllers
diff options
context:
space:
mode:
Diffstat (limited to 'src/Controllers')
-rwxr-xr-xsrc/Controllers/AccountController.php29
-rw-r--r--src/Controllers/Client/ClientController.php742
-rw-r--r--src/Controllers/Client/KeyController.php142
-rw-r--r--src/Controllers/Client/MediaController.php23
-rwxr-xr-xsrc/Controllers/Client/RoomController.php (renamed from src/Controllers/RoomController.php)131
-rw-r--r--src/Controllers/Client/ServerInformationController.php57
-rwxr-xr-xsrc/Controllers/Client/UserController.php184
-rw-r--r--src/Controllers/KeyController.php76
-rw-r--r--src/Controllers/LoginController.php149
-rw-r--r--src/Controllers/Server/ServerInformationController.php (renamed from src/Controllers/ServerImplementationController.php)23
-rw-r--r--src/Controllers/ServerDiscoveryController.php34
-rwxr-xr-xsrc/Controllers/SyncController.php126
-rwxr-xr-xsrc/Controllers/UserController.php57
13 files changed, 1265 insertions, 508 deletions
diff --git a/src/Controllers/AccountController.php b/src/Controllers/AccountController.php
deleted file mode 100755
index 8e20880..0000000
--- a/src/Controllers/AccountController.php
+++ /dev/null
@@ -1,29 +0,0 @@
-<?php
-
-namespace App\Controllers;
-
-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;
-
-class AccountController
-{
- /**
- * GET /_matrix/client/v3/account/whoami
- *
- * @see https://spec.matrix.org/v1.15/client-server-api/#get_matrixclientv3accountwhoami
- */
- public function whoami(Request $request): Response
- {
- $user = User::authenticateWithRequest($request);
- $device = Device::fetch(userId: $user->getId());
-
- return new JsonResponse(new ClientAccountWhoamiGetResponse(
- userId: $user->getId(),
- deviceId: $device->getId(),
- ));
- }
-}
diff --git a/src/Controllers/Client/ClientController.php b/src/Controllers/Client/ClientController.php
new file mode 100644
index 0000000..28aea66
--- /dev/null
+++ b/src/Controllers/Client/ClientController.php
@@ -0,0 +1,742 @@
+<?php
+
+namespace App\Controllers\Client;
+
+use App\App;
+use App\Database;
+use App\Errors\AppException;
+use App\Errors\ErrorResponse;
+use App\Errors\UnauthorizedError;
+use App\Errors\UnknownError;
+use App\Models\Device;
+use App\Models\RoomEvent;
+use App\Models\Tokens;
+use App\Models\User;
+use App\Support\Id;
+use App\Support\Logger;
+use App\Support\Parser;
+use App\Support\RequestValidator;
+use Matrix\Data\AccountData;
+use Matrix\Data\Capabilities;
+use Matrix\Data\Capability\RoomVersionsCapability;
+use Matrix\Data\DeviceLists;
+use Matrix\Data\LoginFlow;
+use Matrix\Data\Presence;
+use Matrix\Data\PushCondition;
+use Matrix\Data\PushRule;
+use Matrix\Data\Room\Ephemeral;
+use Matrix\Data\Room\JoinedRoom;
+use Matrix\Data\Room\RoomSummary;
+use Matrix\Data\Room\Rooms;
+use Matrix\Data\Room\State;
+use Matrix\Data\Room\Timeline;
+use Matrix\Data\Room\UnreadNotificationCounts;
+use Matrix\Data\Ruleset;
+use Matrix\Data\ToDevice;
+use Matrix\Data\UserId;
+use Matrix\Enums\AuthenticationType;
+use Matrix\Enums\ErrorCode;
+use Matrix\Enums\LoginType;
+use Matrix\Enums\MembershipState;
+use Matrix\Enums\PresenceState;
+use Matrix\Enums\PushConditionKind;
+use Matrix\Enums\UserRegistrationKind;
+use Matrix\Events\PresenceEvent;
+use Matrix\Responses\ClientLoginGetResponse;
+use Matrix\Responses\ClientLoginPostResponse;
+use Matrix\Responses\ClientRefreshPostResponse;
+use Matrix\Responses\ClientRegisterPostResponse;
+use Matrix\Responses\ClientSyncGetResponse;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\Response;
+use Symfony\Component\HttpFoundation\JsonResponse;
+use Symfony\Component\Routing\Attribute\Route;
+
+class ClientController
+{
+ #[Route(path: "_matrix/client/r0/login", methods: ["GET"])]
+ #[Route(path: "_matrix/client/v3/login", methods: ["GET"])]
+ public function supportedLoginTypes(Request $request): Response
+ {
+ return new JsonResponse(new ClientLoginGetResponse([
+ (new LoginFlow(LoginType::PASSWORD)),
+ ]));
+ }
+
+ #[Route(path: "_matrix/client/r0/login", methods: ["POST"])]
+ #[Route(path: "_matrix/client/v3/login", methods: ["POST"])]
+ public function login(Request $request): Response
+ {
+ Logger::logRequestToFile($request);
+
+ $body = json_decode($request->getContent(), true);
+ RequestValidator::validateJson();
+
+ // validate login type
+ $loginType = null;
+ try {
+ $loginType = LoginType::from($body["type"]);
+ } catch (\ValueError $error) {
+ throw new UnknownError("Bad login type.", Response::HTTP_BAD_REQUEST);
+ }
+
+ // get user id
+ $userId = Parser::parseUser($body["identifier"]["user"]);
+ if (empty($userId["server"])) {
+ $userId = "@$userId[username]:$_ENV[DOMAIN]";
+ #$userId = "@$userId[username]:localhost";
+ } else {
+ $userId = "@$userId[username]:$userId[server]";
+ }
+
+ if ($loginType !== LoginType::PASSWORD) {
+ throw new AppException(ErrorCode::UNRECOGNIZED, "only password login supported for now", Response::HTTP_SERVICE_UNAVAILABLE);
+ }
+
+ $user = User::fetchWithPassword($userId, $body["password"]);
+
+ if (! $user) {
+ throw new AppException(ErrorCode::FORBIDDEN, "Invalid credentials", Response::HTTP_FORBIDDEN);
+ }
+
+ $deviceId = $body["device_id"] ?? "";
+
+ $device = null;
+ $tokens = null;
+
+ // create new device with tokens
+ if (empty($deviceId)) {
+ $device = Device::new(
+ $user->getId(),
+ initialDisplayName: $body["initial_device_display_name"] ?? "",
+ );
+ $device->insert();
+
+ $tokens = Tokens::new($userId, $device->getId());
+ $tokens->insert();
+ } else { // fetch existing device and tokens
+ $device = $user->fetchDevice($deviceId);
+ $tokens = Tokens::fetch($userId, $device->getId());
+
+ if (empty($tokens)) {
+ throw new AppException(
+ ErrorCode::UNKNOWN_TOKEN,
+ "Soft logged out",
+ Response::HTTP_UNAUTHORIZED,
+ ["soft_logout" => true],
+ );
+ }
+ }
+
+ return new JsonResponse(new ClientLoginPostResponse(
+ accessToken: $tokens->getAccessToken(),
+ deviceId: $device->getId(),
+ userId: $user->getId(),
+ expiresInMilliseconds: $tokens->getExpiresIn(),
+ refreshToken: $tokens->getRefreshToken(),
+ ));
+ }
+
+ #[Route(path: "_matrix/client/v3/register", methods: ["POST"])]
+ public function register(Request $request): Response
+ {
+ $body = json_decode($request->getContent(), true);
+ RequestValidator::validateJson();
+
+ if (empty($body)) {
+ return new JsonResponse([
+ "completed" => [],
+ "flows" => [
+ [
+ "stages" => [
+ AuthenticationType::DUMMY,
+ ],
+ ],
+ ],
+ "params" => [],
+ "session" => Id::generate(),
+ ], Response::HTTP_UNAUTHORIZED);
+ }
+
+ // validate kind
+ $kind = null;
+ try {
+ $kind = UserRegistrationKind::from($request->query->get("kind") ?? "user");
+ } catch (\ValueError $error) {
+ throw new UnknownError("Bad registration kind.", Response::HTTP_BAD_REQUEST);
+ }
+
+ $username = $body["username"] ?? "";
+ if (empty($username)) {
+ $username = crc32(time() . random_bytes(64));
+ }
+
+ $userId = "@$username:$_ENV[DOMAIN]";
+
+ if (empty($body["password"])) {
+ throw new AppException(ErrorCode::MISSING_PARAM, "missing password", Response::HTTP_BAD_REQUEST);
+ }
+
+ if (Database::getInstance()->query("select id from users where id=:id", ["id" => $userId])->fetchColumn()) {
+ throw new AppException(ErrorCode::USER_IN_USE, "The desired user ID is already taken.", Response::HTTP_BAD_REQUEST);
+ }
+
+ Database::getInstance()->query("insert into users (id, name, password) values (:id, :name, :password)", [
+ "id" => $userId,
+ "name" => $username,
+ "password" => $body["password"],
+ ]);
+
+ $device_id = $body["device_id"] ?? "";
+ $initialDeviceDisplayName = $body["initial_device_display_name"] ?? "";
+
+ $device = Device::new($userId, $device_id, $initialDeviceDisplayName);
+ $device->insert();
+
+ $tokens = Tokens::new($userId, $device->getId());
+ $tokens->insert();
+
+ return new JsonResponse(new ClientRegisterPostResponse(
+ accessToken: $tokens->getAccessToken(),
+ deviceId: $device->getId(),
+ expiresInMilliseconds: $tokens->getExpiresIn(),
+ refreshToken: $tokens->getRefreshToken(),
+ userId: $userId,
+ ));
+ }
+
+ /**
+ * @see https://spec.matrix.org/v1.15/client-server-api/#get_matrixclientv3sync
+ * @see https://spec.matrix.org/v1.15/client-server-api/#extensions-to-sync
+ */
+ #[Route(path: "_matrix/client/r0/sync", methods: ["GET"])]
+ #[Route(path: "_matrix/client/v3/sync", methods: ["GET"])]
+ public function sync(Request $request): Response
+ {
+ $user = User::authenticateWithRequest($request);
+
+ $filter = $request->query->get("filter", "");
+ $syncFullState = $request->query->get("full_state", false);
+ $setPresence = PresenceState::tryFrom($request->query->get("set_presence") ?? "") ?? PresenceState::ONLINE;
+ $since = $request->query->get("since", "");
+ $timeout = $request->query->get("timeout", 0);
+ $useStateAfter = $request->query->get("use_state_after", false) ?: $request->query->get("org.matrix.msc4222.use_state_after", false);
+
+ if (! empty($filter)) {
+ if (str_starts_with($filter, "{")) {
+ $filter = json_decode($filter, true);
+ } else {
+ $filter = Database::getInstance()->query("select * from filters where id=:id", ["id" => $filter])->fetch();
+ }
+ }
+
+ // device one time keys count
+ $deviceOneTimeKeysCount = Database::getInstance()
+ ->query("select count(*) from one_time_keys where user_id=:user_id and device_id=:device_id", [
+ "user_id" => $user->getId(),
+ "device_id" => $user->getDeviceId(),
+ ])
+ ->fetchColumn();
+ $deviceOneTimeKeysCount = [
+ "signed_curve25519" => $deviceOneTimeKeysCount,
+ ];
+
+ // rooms
+ $rooms = Database::getInstance()->query(<<<SQL
+ select * from rooms
+ left join room_memberships
+ on rooms.id = room_memberships.room_id
+ where room_memberships.user_id = :user_id
+ SQL, [
+ "user_id" => $user->getId(),
+ ])->fetchAll();
+
+ $invitedRooms = [];
+ $joinedRooms = [];
+ $knockedRooms = [];
+ $leftRooms = [];
+
+ $nextBatch = (new \DateTime())->format("U");
+
+ foreach ($rooms as $room) {
+ $events = Database::getInstance()->query(<<<SQL
+ select * from room_events
+ where room_id = :room_id
+ and origin_server_timestamp > to_timestamp(:since)
+ SQL, [
+ "room_id" => $room["room_id"],
+ #"limit" => ($filter["room"]["timeline"]["limit"] ?? false) ? "limit " . $filter["room"]["timeline"]["limit"] : "",
+ "since" => (empty($since) ? \DateTime::createFromTimestamp(0) : \DateTime::createFromTimestamp($since))->format("U"),
+ ])->fetchAll();
+
+ if (! empty($events) && MembershipState::tryFrom($room["state"]) === MembershipState::JOIN) {
+ $joinedRooms[$room["room_id"]] = new JoinedRoom(
+ accountData: new AccountData([]),
+ ephemeral: new Ephemeral([]),
+ state: new State(array_map([RoomEvent::class, "transformEvent"], $events)),
+ summary: new RoomSummary(
+ heroes: [],
+ invitedMemberCount: 0,
+ joinedMemberCount: 1,
+ ),
+ timeline: new Timeline(
+ events: array_map([RoomEvent::class, "transformEvent"], $events),
+ limited: false,# $filter["room"]["timeline"]["limit"] ?? false,
+ previousBatch: null,
+ ),
+ unreadNotifications: new UnreadNotificationCounts(0, 0),
+ unreadThreadNotifications: [],
+ );
+ }
+
+ if (! empty($events)) {
+ $newestEvent = RoomEvent::transformEvent($events[array_key_last($events)]);
+ $nextBatch = \DateTime::createFromTimestamp($newestEvent->getOriginServerTimestamp())->format("U");
+ }
+ }
+
+ if (($timeout / 1000) > App::getExectionTime()) {
+ sleep(intval(($timeout / 1000) - App::getExectionTime()));
+ }
+
+ return new JsonResponse(new ClientSyncGetResponse(
+ nextBatch: $nextBatch,
+
+ accountData: new AccountData([]),
+
+ deviceLists: new DeviceLists([], []),
+
+ deviceOneTimeKeysCount: $deviceOneTimeKeysCount,
+
+ presence: new Presence([
+ new PresenceEvent(
+ sender: $user->getId(),
+ presence: $setPresence,
+ ),
+ ]),
+
+ rooms: new Rooms(
+ $invitedRooms,
+ $joinedRooms,
+ $knockedRooms,
+ $leftRooms,
+ ),
+
+ toDevice: new ToDevice([]),
+ ));
+ }
+
+ #[Route(path: "/_matrix/client/v3/refresh", methods: ["POST"])]
+ public function refresh(Request $request): Response
+ {
+ $body = json_decode($request->getContent(), true);
+ RequestValidator::validateJson();
+
+ $tokens = Tokens::fetchWithRefreshToken($body["refresh_token"]);
+
+ if (empty($tokens)) {
+ throw new AppException(
+ ErrorCode::UNKNOWN_TOKEN,
+ "Soft logged out",
+ Response::HTTP_UNAUTHORIZED,
+ ["soft_logout" => true],
+ );
+ }
+
+ $newTokens = Tokens::new($tokens->getUserId(), $tokens->getDeviceId());
+ $newTokens->insert();
+
+ return new JsonResponse(new ClientRefreshPostResponse(
+ accessToken: $newTokens->getAccessToken(),
+ expiresInMilliseconds: $newTokens->getExpiresIn(),
+ refreshToken: $newTokens->getRefreshToken(),
+ ));
+ }
+
+ #[Route(path: "/_matrix/client/v3/pushrules", methods: ["GET"])]
+ public function pushRules(Request $request): Response
+ {
+ $user = User::authenticateWithRequest($request);
+
+ $notificationActionBase = [
+ "notify",
+ ];
+ $notificationActionSound = $notificationActionBase + [
+ [
+ "set_tweak" => "sound",
+ "value" => "default",
+ ],
+ ];
+ $notificationActionHighlight = $notificationActionBase + [
+ [
+ "set_tweak" => "highlight",
+ ],
+ ];
+ $notifactionActionSoundHighlight = $notificationActionSound + [
+ [
+ "set_tweak" => "highlight",
+ ],
+ ];
+
+ // @see https://spec.matrix.org/v1.16/client-server-api/#predefined-rules
+ return new JsonResponse([
+ "global" => new Ruleset(
+ content: [
+ new PushRule(
+ ruleId: ".m.rule.contains_user_name",
+ default: true,
+ enabled: false,
+ pattern: Parser::parseUser($user->getId())["username"],
+ actions: [
+ ...$notifactionActionSoundHighlight,
+ ],
+ ),
+ ],
+
+ override: [
+ new PushRule(
+ ruleId: ".m.rule.master",
+ default: true,
+ enabled: false,
+ conditions: [],
+ actions: [],
+ ),
+
+ new PushRule(
+ ruleId: ".m.rule.suppress_notices",
+ default: true,
+ enabled: true,
+ conditions: [
+ new PushCondition(
+ kind: PushConditionKind::EVENT_MATCH,
+ key: "content.msgtype",
+ pattern: "m.notice",
+ ),
+ ],
+ actions: [],
+ ),
+
+ new PushRule(
+ ruleId: ".m.rule.invite_for_me",
+ default: true,
+ enabled: true,
+ conditions: [
+ new PushCondition(
+ kind: PushConditionKind::EVENT_MATCH,
+ key: "type",
+ pattern: "m.room.member",
+ ),
+ new PushCondition(
+ kind: PushConditionKind::EVENT_MATCH,
+ key: "content.membership",
+ pattern: "invite",
+ ),
+ new PushCondition(
+ kind: PushConditionKind::EVENT_MATCH,
+ key: "state_key",
+ pattern: $user->getId(),
+ ),
+ ],
+ actions: [
+ ...$notificationActionSound,
+ ],
+ ),
+
+
+ new PushRule(
+ ruleId: ".m.rule.member_event",
+ default: true,
+ enabled: true,
+ conditions: [
+ new PushCondition(
+ kind: PushConditionKind::EVENT_MATCH,
+ key: "type",
+ pattern: "m.room.member",
+ ),
+ ],
+ actions: [],
+ ),
+
+ new PushRule(
+ ruleId: ".m.rule.is_user_mention",
+ default: true,
+ enabled: true,
+ conditions: [
+ new PushCondition(
+ kind: PushConditionKind::EVENT_PROPERTY_CONTAINS,
+ key: "content.m\\.mentions.user_ids",
+ value: $user->getId(),
+ ),
+ ],
+ actions: [
+ ...$notifactionActionSoundHighlight,
+ ],
+ ),
+
+ new PushRule(
+ ruleId: ".m.rule.contains_display_name",
+ default: true,
+ enabled: false,
+ conditions: [
+ new PushCondition(
+ kind: PushConditionKind::CONTAINS_DISPLAY_NAME,
+ ),
+ ],
+ actions: [
+ "notify",
+ [
+ "set_tweak" => "sound",
+ "value" => "default",
+ ],
+ [
+ "set_tweak" => "highlight",
+ ],
+ ],
+ ),
+
+ new PushRule(
+ ruleId: ".m.rule.is_room_mention",
+ default: true,
+ enabled: true,
+ conditions: [
+ new PushCondition(
+ kind: PushConditionKind::EVENT_PROPERTY_IS,
+ key: "content.m\\.mentions.room",
+ value: true,
+ ),
+ new PushCondition(
+ kind: PushConditionKind::SENDER_NOTIFICATION_PERMISSION,
+ key: "room",
+ ),
+ ],
+ actions: [
+ ...$notificationActionHighlight,
+ ],
+ ),
+
+ new PushRule(
+ ruleId: ".m.rule.roomnotif",
+ default: true,
+ enabled: false,
+ conditions: [
+ new PushCondition(
+ kind: PushConditionKind::EVENT_MATCH,
+ key: "content.body",
+ pattern: "@room",
+ ),
+ new PushCondition(
+ kind: PushConditionKind::SENDER_NOTIFICATION_PERMISSION,
+ key: "room",
+ ),
+ ],
+ actions: [
+ "notify",
+ [
+ "set_tweak" => "highlight",
+ ],
+ ],
+ ),
+
+ new PushRule(
+ ruleId: ".m.rule.tombstone",
+ default: true,
+ enabled: true,
+ conditions: [
+ new PushCondition(
+ kind: PushConditionKind::EVENT_MATCH,
+ key: "type",
+ pattern: "m.room.tombstone",
+ ),
+ new PushCondition(
+ kind: PushConditionKind::EVENT_MATCH,
+ key: "state_key",
+ pattern: "",
+ ),
+ ],
+ actions: [
+ ...$notificationActionHighlight,
+ ],
+ ),
+
+ new PushRule(
+ ruleId: ".m.rule.reaction",
+ default: true,
+ enabled: true,
+ conditions: [
+ new PushCondition(
+ kind: PushConditionKind::EVENT_MATCH,
+ key: "type",
+ pattern: "m.reaction",
+ ),
+ ],
+ actions: [],
+ ),
+
+ new PushRule(
+ ruleId: ".m.rule.room.server_acl",
+ default: true,
+ enabled: true,
+ conditions: [
+ new PushCondition(
+ kind: PushConditionKind::EVENT_MATCH,
+ key: "type",
+ pattern: "m.room.server_acl",
+ ),
+ new PushCondition(
+ kind: PushConditionKind::EVENT_MATCH,
+ key: "state_key",
+ pattern: "",
+ ),
+ ],
+ actions: [],
+ ),
+
+ new PushRule(
+ ruleId: ".m.rule.suppress_edits",
+ default: true,
+ enabled: true,
+ conditions: [
+ new PushCondition(
+ kind: PushConditionKind::EVENT_PROPERTY_IS,
+ key: "content.m\\.relates_to.rel_type",
+ value: "m.replace",
+ ),
+ ],
+ actions: [],
+ ),
+ ],
+
+ room: [],
+ sender: [],
+
+ underride: [
+ new PushRule(
+ ruleId: ".m.rule.call",
+ default: true,
+ enabled: true,
+ conditions: [
+ new PushCondition(
+ kind: PushConditionKind::EVENT_MATCH,
+ key: "type",
+ pattern: "m.call.invite",
+ )
+ ],
+ actions: [
+ "notify",
+ [
+ "set_tweak" => "sound",
+ "value" => "ring",
+ ],
+ ],
+ ),
+
+ new PushRule(
+ ruleId: ".m.rule.encrypted_room_one_to_one",
+ default: true,
+ enabled: true,
+ conditions: [
+ new PushCondition(
+ kind: PushConditionKind::ROOM_MEMBER_COUNT,
+ is: "2",
+ ),
+ new PushCondition(
+ kind: PushConditionKind::EVENT_MATCH,
+ key: "type",
+ pattern: "m.room.encrypted",
+ ),
+ ],
+ actions: [
+ ...$notificationActionSound,
+ ],
+ ),
+
+ new PushRule(
+ ruleId: ".m.rule.room_one_to_one",
+ default: true,
+ enabled: true,
+ conditions: [
+ new PushCondition(
+ kind: PushConditionKind::ROOM_MEMBER_COUNT,
+ is: "2",
+ ),
+ new PushCondition(
+ kind: PushConditionKind::EVENT_MATCH,
+ key: "type",
+ pattern: "m.room.message",
+ ),
+ ],
+ actions: [
+ ...$notificationActionSound,
+ ],
+ ),
+
+ new PushRule(
+ ruleId: ".m.rule.message",
+ default: true,
+ enabled: true,
+ conditions: [
+ new PushCondition(
+ kind: PushConditionKind::EVENT_MATCH,
+ key: "type",
+ pattern: "m.room.message",
+ ),
+ ],
+ actions: [
+ "notify",
+ ],
+ ),
+
+ new PushRule(
+ ruleId: ".m.rule.encrypted",
+ default: true,
+ enabled: true,
+ conditions: [
+ new PushCondition(
+ kind: PushConditionKind::EVENT_MATCH,
+ key: "type",
+ pattern: "m.room.encrypted",
+ ),
+ ],
+ actions: [
+ "notify",
+ ],
+ ),
+ ],
+ ),
+ ]);
+ }
+
+ #[Route(path: "/_matrix/client/v3/capabilities", methods: ["GET"])]
+ public function capabilities(Request $request): Response
+ {
+ $user = User::authenticateWithRequest($request);
+
+ return new JsonResponse(new Capabilities(
+ roomVersions: new RoomVersionsCapability(
+ available: ["1" => "stable"],
+ default: "1",
+ ),
+ ));
+ }
+
+ #[Route(path: "/_matrix/client/v3/voip/turnServer", methods: ["GET"])]
+ public function voipTurnServer(Request $request): Response
+ {
+ $user = User::authenticateWithRequest($request);
+
+ return new JsonResponse([
+ "password" => "",
+ "ttl" => 86400,
+ "uris" => [],
+ "username" => "",
+ ]);
+ }
+
+ #[Route(path: "/_matrix/client/v3/thirdparty/protocols", methods: ["GET"])]
+ public function thirdPartyProtocols(Request $request): Response
+ {
+ $user = User::authenticateWithRequest($request);
+
+ return new JsonResponse(new \stdClass());
+ }
+}
diff --git a/src/Controllers/Client/KeyController.php b/src/Controllers/Client/KeyController.php
new file mode 100644
index 0000000..47f8933
--- /dev/null
+++ b/src/Controllers/Client/KeyController.php
@@ -0,0 +1,142 @@
+<?php
+
+namespace App\Controllers\Client;
+
+use App\App;
+use App\Database;
+use App\Models\Device;
+use App\Models\User;
+use App\Support\RequestValidator;
+use Matrix\Responses\ClientKeysUploadPostResponse;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\Response;
+use Symfony\Component\HttpFoundation\JsonResponse;
+use Symfony\Component\Routing\Attribute\Route;
+
+class KeyController
+{
+ #[Route(path: "/_matrix/client/r0/keys/upload", methods: ["POST"])]
+ #[Route(path: "/_matrix/client/v3/keys/upload", methods: ["POST"])]
+ public function upload(Request $request): Response
+ {
+ $user = User::authenticateWithRequest($request);
+ $body = json_decode($request->getContent(), true);
+ RequestValidator::validateJson();
+
+ if (! empty($body["device_keys"])) {
+ if ($body["device_keys"]["user_id"] !== $user->getId()) {}
+ if ($body["device_keys"]["user_id"] !== $user->getDeviceId()) {}
+
+ Database::getInstance()->query(<<<SQL
+ insert into device_keys (supported_algorithms, keys, signatures, user_id, device_id)
+ values (:supported_algorithms, :keys, :signatures, :user_id, :device_id)
+ SQL, [
+ "supported_algorithms" => json_encode($body["device_keys"]["algorithms"]),
+ "keys" => json_encode($body["device_keys"]["keys"]),
+ "signatures" => json_encode($body["device_keys"]["signatures"]),
+ "user_id" => $user->getId(),
+ "device_id" => $user->getDeviceId(),
+ ]);
+ }
+
+ $oneTimeKeys = $body["one_time_keys"];
+ if (! empty($body["fallback_keys"])) {
+ $oneTimeKeys += $body["fallback_keys"];
+ }
+
+ foreach ($oneTimeKeys as $identifier => $object) {
+ $identifierParts = explode(":", $identifier);
+
+ $algorithm = $identifierParts[0];
+ $id = $identifierParts[1];
+
+ $signatures = array_values($object["signatures"])[0];
+ $signatureIdentifier = array_keys($signatures)[0];
+ $signatureAlgorithm = explode(":", $signatureIdentifier)[0];
+ $signatureKey = array_values($signatures)[0];
+
+ $deviceId = explode(":", $signatureIdentifier)[1];
+
+ Database::getInstance()->query(<<<SQL
+ insert into one_time_keys (id, key, algorithm, signature_key, signature_algorithm, is_fallback, user_id, device_id)
+ values (:id, :key, :algorithm, :signature_key, :signature_algorithm, :is_fallback, :user_id, :device_id)
+ SQL, [
+ "id" => $id,
+ "key" => $object["key"],
+ "algorithm" => $algorithm,
+ "signature_key" => $signatureKey,
+ "signature_algorithm" => $signatureAlgorithm,
+ "is_fallback" => ($object["fallback"] ?? false) ?: 0,
+ "user_id" => $user->getId(),
+ "device_id" => $deviceId,
+ ]);
+ }
+
+ # TODO: do that per algorithm
+ $currentCountOneTimeKeys = Database::getInstance()
+ ->query("select count(id) from one_time_keys where user_id=:userId and is_fallback=false", [
+ "userId" => $user->getId(),
+ ])
+ ->fetchColumn();
+
+ return new JsonResponse(new ClientKeysUploadPostResponse([
+ #"curve25519" => 0,
+ "signed_curve25519" => $currentCountOneTimeKeys,
+ ]));
+ }
+
+ #[Route(path: "/_matrix/client/v3/keys/query", methods: ["POST"])]
+ public function query(Request $request): Response
+ {
+ $user = User::authenticateWithRequest($request);
+ $body = json_decode($request->getContent(), true);
+ RequestValidator::validateJson();
+
+ $deviceKeys = $body["device_keys"];
+ $timeout = $body["timeout"] ?? 10000;
+
+ $downloadedDeviceKeys = [];
+ foreach ($deviceKeys as $keysUserId => $deviceIds) {
+ foreach ($deviceIds as $deviceId) {
+ $result = Database::getInstance()
+ ->query("select * from device_keys where user_id=:user_id and device_id=:device_id", [
+ "user_id" => $keysUserId,
+ "device_id" => $deviceId,
+ ])
+ ->fetch();
+
+ $device = Device::fetch($deviceId, $keysUserId);
+
+ $downloadedDeviceKeys[$keysUserId][$deviceId] = [
+ "algorithms" => $result["supported_algorithms"],
+ "keys" => $result["keys"],
+ "signatures" => $result["signatures"],
+ "device_id" => $result["device_id"],
+ "user_id" => $result["user_id"],
+ "unsigned" => [
+ "device_display_name" => $device->getName(),
+ ],
+ ];
+ }
+ }
+
+ // apply timeout
+ if ($timeout > 0) {
+ sleep(intval(($timeout / 1000) - App::getExectionTime()));
+ }
+
+ return new JsonResponse([
+ "device_keys" => empty($downloadedDeviceKeys) ? new \stdClass() : $downloadedDeviceKeys,
+ ]);
+ }
+
+ #[Route(path: "/_matrix/client/v3/keys/claim", methods: ["POST"])]
+ public function claim(Request $request): Response
+ {
+ $user = User::authenticateWithRequest($request);
+ $body = json_decode($request->getContent(), true);
+ RequestValidator::validateJson();
+
+ return new JsonResponse();
+ }
+}
diff --git a/src/Controllers/Client/MediaController.php b/src/Controllers/Client/MediaController.php
new file mode 100644
index 0000000..73f4aa0
--- /dev/null
+++ b/src/Controllers/Client/MediaController.php
@@ -0,0 +1,23 @@
+<?php
+
+namespace App\Controllers\Client;
+
+use App\Models\User;
+use Symfony\Component\HttpFoundation\JsonResponse;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\Response;
+use Symfony\Component\Routing\Attribute\Route;
+
+class MediaController
+{
+ #[Route(path: "/_matrix/media/v3/config", methods: ["GET"])]
+ #[Route(path: "/_matrix/client/v1/media/config", methods: ["GET"])]
+ public function mediaConfig(Request $request): Response
+ {
+ $user = User::authenticateWithRequest($request);
+
+ return new JsonResponse([
+ "m.upload.size" => 50000000,
+ ]);
+ }
+}
diff --git a/src/Controllers/RoomController.php b/src/Controllers/Client/RoomController.php
index dcc0415..07bad27 100755
--- a/src/Controllers/RoomController.php
+++ b/src/Controllers/Client/RoomController.php
@@ -1,17 +1,18 @@
<?php
-namespace App\Controllers;
+namespace App\Controllers\Client;
use App\Database;
use App\Errors\AppException;
-use App\Errors\ErrorCode;
+use App\Errors\Exception;
use App\Errors\UnauthorizedError;
-use App\Events\RoomMessageEvent;
use App\Models\RoomEvent;
use App\Models\User;
use App\Support\Id;
use App\Support\Parser;
use App\Support\RequestValidator;
+use Matrix\Data\UnsignedData;
+use Matrix\Enums\ErrorCode;
use Matrix\Enums\EventType;
use Matrix\Enums\MembershipState;
use Matrix\Enums\MessageType;
@@ -19,6 +20,7 @@ use Matrix\Enums\RoomGuestAccess;
use Matrix\Enums\RoomHistoryVisibility;
use Matrix\Enums\RoomJoinRule;
use Matrix\Enums\RoomVisibility;
+use Matrix\Events\ClientEvent;
use Matrix\Events\Room\CreateEvent;
use Matrix\Events\Room\GuestAccessEvent;
use Matrix\Events\Room\HistoryVisibilityEvent;
@@ -29,9 +31,11 @@ use Matrix\Events\Room\PowerLevelsEvent;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\JsonResponse;
+use Symfony\Component\Routing\Attribute\Route;
class RoomController
{
+ #[Route(path: "/_matrix/client/v3/createRoom", methods: ["POST"])]
public function createRoom(Request $request): Response
{
$user = User::authenticateWithRequest($request);
@@ -71,10 +75,11 @@ class RoomController
// create room
$roomId = Id::generateRoomId();
Database::getInstance()->query(<<<SQL
- insert into rooms (id, name) values (:id, :name)
+ insert into rooms (id, name, version) values (:id, :name, :version)
SQL, [
"id" => $roomId,
"name" => $roomAliasName, # "#$roomAliasName:$_ENV[DOMAIN]",
+ "version" => $roomVersion,
]);
$roomCreateEvent = new RoomEvent(new CreateEvent(
@@ -157,10 +162,9 @@ class RoomController
}
/**
- * GET /_matrix/client/v3/directory/room/{roomAlias}
- *
* @see https://spec.matrix.org/v1.15/client-server-api/#get_matrixclientv3directoryroomroomalias
*/
+ #[Route(path: "/_matrix/client/v3/directory/room/{roomAlias}", methods: ["GET"])]
public function resolveAlias(Request $request): Response
{
$alias = $request->attributes->get("roomAlias");
@@ -195,7 +199,87 @@ class RoomController
"servers" => [],
]);
}
+
+ #[Route(path: "/_matrix/client/v3/rooms/{roomId}/join", methods: ["POST"])]
+ public function joinRoomId(Request $request): Response
+ {
+ return new JsonResponse();
+ }
+
+ #[Route(path: "/_matrix/client/v3/join/{roomIdOrAlias}", methods: ["POST"])]
+ public function joinRoomIdOrAlias(Request $request): Response
+ {
+ $user = User::authenticateWithRequest($request);
+ $body = json_decode($request->getContent(), true);
+ RequestValidator::validateJson();
+
+ $roomIdOrAlias = $request->attributes->get("roomIdOrAlias");
+
+ $via = $request->query->get("via", "");
+
+ $reason = $body["reason"] ?? "";
+ #$thirdPartySigned
+
+ /*
+ if (isRoomId()) {
+ $request->attributes->set("roomId", $roomIdOrAlias);
+ return $this->joinRoomId($request);
+ }
+ */
+
+ # TODO: db query id or name = value
+ $roomId = Database::getInstance()
+ ->query("select id from rooms where id=:value or name=:value", [
+ "value" => $roomIdOrAlias,
+ ])
+ ->fetchColumn();
+
+ return new JsonResponse([
+ "room_id" => $roomId,
+ ]);
+ }
+
+ #[Route(path: "/_matrix/client/v3/rooms/{roomId}/messages", methods: ["GET"])]
+ public function getMessages(Request $request): Response
+ {
+ $user = User::authenticateWithRequest($request);
+
+ $roomId = $request->attributes->get("roomId");
+
+ $membership = Database::getInstance()
+ ->query("select state from room_memberships where user_id=:user_id and room_id=:room_id", [
+ "user_id" => $user->getId(),
+ "room_id" => $roomId,
+ ])
+ ->fetchColumn();
+
+ if (MembershipState::from($membership) !== MembershipState::JOIN) {
+ throw new Exception(ErrorCode::FORBIDDEN, "You aren't a member of the room.", Response::HTTP_FORBIDDEN);
+ }
+
+ $direction = $request->query->get("dir");
+ $filter = $request->query->get("filter");
+ $from = $request->query->get("from");
+ $limit = $request->query->get("limit", 10);
+ $to = $request->query->get("to");
+ $events = Database::getInstance()->query(<<<SQL
+ select * from room_events
+ where room_id = :room_id
+ SQL, [
+ "room_id" => $roomId,
+ #"limit" => ($filter["room"]["timeline"]["limit"] ?? false) ? "limit " . $filter["room"]["timeline"]["limit"] : "",
+ ])->fetchAll();
+
+ return new JsonResponse([
+ "chunk" => array_map([RoomEvent::class, "transformEvent"], $events),
+ "end" => "",
+ "start" => "",
+ "state" => [],
+ ]);
+ }
+
+ #[Route(path: "/_matrix/client/v3/rooms/{roomId}/read_markers", methods: ["POST"])]
public function readMarkers(Request $request): Response
{
$user = User::authenticateWithRequest($request);
@@ -208,14 +292,12 @@ class RoomController
}
/**
- * PUT /_matrix/client/v3/rooms/{roomId}/send/{eventType}/{txnId}
- *
* @see https://spec.matrix.org/v1.15/client-server-api/#put_matrixclientv3roomsroomidsendeventtypetxnid
*/
+ #[Route(path: "/_matrix/client/v3/rooms/{roomId}/send/{eventType}/{txnId}", methods: ["PUT"])]
public function send(Request $request): Response
{
- $accessToken = str_replace("Bearer ", "", $request->headers->get("authorization") ?: "");
- $user = User::fetchWithAccessToken($accessToken);
+ $user = User::authenticateWithRequest($request);
if (empty($user)) {
throw new UnauthorizedError();
@@ -228,24 +310,21 @@ class RoomController
$body = json_decode($request->getContent(), true);
RequestValidator::validateJson();
- $message = $body["body"];
- $messageType = MessageType::from($body["msgtype"]);
+ // validate msgtype
+ MessageType::from($body["msgtype"]);
- $eventId = "\$" . md5(random_bytes(512)) . ":" . $_ENV["DOMAIN"];
- $event = new RoomMessageEvent(
- id: $eventId,
- sender: $user->getId(),
- originServerTimestamp: new \DateTime("now"),
- content: [
- "body" => $message,
- "msgtype" => $messageType->value,
- ],
- unsigned: [
- "age" => 1234,
- "membership" => MembershipState::JOIN->value,
- ],
+ $eventId = Id::generateEventId();
+ $event = new RoomEvent(new ClientEvent(
+ content: $body,
+ eventId: $eventId,
+ originServerTimestamp: time(),
roomId: $roomId,
- );
+ sender: $user->getId(),
+ type: $eventType,
+ unsigned: new UnsignedData(
+ membership: MembershipState::JOIN,
+ ),
+ ));
$event->insert();
return new JsonResponse([
diff --git a/src/Controllers/Client/ServerInformationController.php b/src/Controllers/Client/ServerInformationController.php
new file mode 100644
index 0000000..51e68e6
--- /dev/null
+++ b/src/Controllers/Client/ServerInformationController.php
@@ -0,0 +1,57 @@
+<?php
+
+namespace App\Controllers\Client;
+
+use Matrix\Responses\ClientVersionsGetResponse;
+use Symfony\Component\HttpFoundation\JsonResponse;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\Response;
+use Symfony\Component\Routing\Attribute\Route;
+
+class ServerInformationController
+{
+ #[Route(path: "/.well-known/matrix/client", methods: ["GET"])]
+ public function client(Request $request): Response
+ {
+ return new JsonResponse([
+ "m.homeserver" => [
+ "base_url" => "https://$_ENV[DOMAIN]",
+ ],
+ ]);
+ }
+
+ #[Route(path: "/.well-known/matrix/support", methods: ["GET"])]
+ public function support(Request $request): Response
+ {
+ return new JsonResponse([
+ "contacts" => [],
+ "support_page" => "",
+ ]);
+ }
+
+ #[Route(path: "/_matrix/client/versions", methods: ["GET"])]
+ public function versions(Request $request): Response
+ {
+ return new JsonResponse(new ClientVersionsGetResponse([
+ "r0.0.1",
+ "v1.1",
+ ]));
+ }
+
+ public function server(Request $request): Response
+ {
+ return new JsonResponse([
+ "m.server" => "$_ENV[DOMAIN]:443",
+ ]);
+ }
+
+ public function version(Request $request): Response
+ {
+ return new JsonResponse([
+ "server" => [
+ "name" => "Matrix PHP",
+ "version" => "0.1.0",
+ ],
+ ]);
+ }
+}
diff --git a/src/Controllers/Client/UserController.php b/src/Controllers/Client/UserController.php
new file mode 100755
index 0000000..63442ec
--- /dev/null
+++ b/src/Controllers/Client/UserController.php
@@ -0,0 +1,184 @@
+<?php
+
+namespace App\Controllers\Client;
+
+use App\Database;
+use App\Errors\AppException;
+use App\Errors\NotFoundError;
+use App\Errors\UnauthorizedError;
+use App\Models\Device;
+use App\Models\User;
+use App\Support\RequestValidator;
+use Cassandra\Exception\UnauthorizedException;
+use Matrix\Enums\ErrorCode;
+use Matrix\Responses\ClientAccountWhoamiGetResponse;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\Response;
+use Symfony\Component\HttpFoundation\JsonResponse;
+use Symfony\Component\Routing\Attribute\Route;
+
+class UserController
+{
+ #[Route(path: "/_matrix/client/v3/account/whoami", methods: ["GET"])]
+ public function whoami(Request $request): Response
+ {
+ $user = User::authenticateWithRequest($request);
+ $device = Device::fetch(userId: $user->getId());
+
+ return new JsonResponse(new ClientAccountWhoamiGetResponse(
+ userId: $user->getId(),
+ deviceId: $device->getId(),
+ ));
+ }
+
+ /**
+ * @see https://spec.matrix.org/v1.16/client-server-api/#post_matrixclientv3useruseridfilter
+ */
+ #[Route(path: "/_matrix/client/r0/user/{userId}/filter", methods: ["POST"])]
+ #[Route(path: "/_matrix/client/v3/user/{userId}/filter", methods: ["POST"])]
+ public function uploadFilter(Request $request): Response
+ {
+ $accessToken = str_replace("Bearer ", "", $request->headers->get("authorization") ?: "");
+ $user = User::fetchWithAccessToken($accessToken);
+
+ if (empty($user)) {
+ throw new UnauthorizedError();
+ }
+
+ $userId = $request->get("userId");
+ if ($user->getId() !== $userId) {
+ throw new UnauthorizedError();
+ }
+
+ $body = json_decode($request->getContent(), true);
+ RequestValidator::validateJson();
+
+ $filterId = md5($userId . random_bytes(512));
+
+ Database::getInstance()->query(<<<SQL
+ insert into filters (id, account_data, event_fields, event_format, presence, room, user_id)
+ values (:id, :account_data, :event_fields, :event_format, :presence, :room, :user_id)
+ SQL, [
+ "id" => $filterId,
+ "account_data" => isset($body["account_data"]) ? json_encode($body["account_data"]) : null,
+ "event_fields" => isset($body["event_fields"]) ? json_encode($body["event_fields"]) : null,
+ "event_format" => isset($body["event_format"]) ? json_encode($body["event_format"]) : null,
+ "presence" => isset($body["presence"]) ? json_encode($body["presence"]) : null,
+ "room" => isset($body["room"]) ? json_encode($body["room"]) : null,
+ "user_id" => $userId,
+ ]);
+
+ return new JsonResponse([
+ "filter_id" => $filterId,
+ ]);
+ }
+
+ #[Route(path: "/_matrix/client/v3/user/{userId}/{filter}/{filterId}", methods: ["GET"])]
+ public function getFilter(Request $request): Response
+ {
+ $user = User::authenticateWithRequest($request);
+
+ $userId = $request->attributes->get("userId");
+ $filterId = $request->attributes->get("filterId");
+
+ $filter = Database::getInstance()
+ ->query("select * from filters where id=:id and user_id=:user_id", [
+ "id" => $filterId,
+ "user_id" => $userId,
+ ])
+ ->fetch();
+
+ if (empty($filter)) {
+ throw new AppException(
+ ErrorCode::NOT_FOUND,
+ "Unknown filter.",
+ Response::HTTP_NOT_FOUND
+ );
+ }
+
+ return new JsonResponse([
+ "account_data" => json_decode($filter["account_data"] ?? ""),
+ "event_fields" => json_decode($filter["event_fields"] ?? ""),
+ "event_format" => $filter["event_format"] ?? "",
+ "presence" => json_decode($filter["presence"] ?? ""),
+ "room" => json_decode($filter["room"] ?? ""),
+ ]);
+ }
+
+ #[Route(path: "/_matrix/client/v3/profile/{userId}", methods: ["GET"])]
+ public function getProfile(Request $request): Response
+ {
+ $userId = $request->attributes->get("userId");
+
+ $user = Database::getInstance()->query("select * from users where id=:user_id", ["user_id" => $userId])->fetch();
+
+ if (empty($user)) {
+ throw new AppException(
+ ErrorCode::NOT_FOUND,
+ "There is no profile information for this user or this user does not exist.",
+ Response::HTTP_NOT_FOUND
+ );
+ }
+
+ return new JsonResponse();
+ }
+
+ #[Route(path: "/_matrix/client/v3/user/{userId}/account_data/{type}", methods: ["GET"])]
+ public function getAccountData(Request $request): Response
+ {
+ $user = User::authenticateWithRequest($request);
+
+ $userId = $request->attributes->get("userId");
+ $type = $request->attributes->get("type");
+
+ if ($user->getId() !== $userId) {
+ throw new UnauthorizedError("Cannot get account data for other users.");
+ }
+
+ $value = Database::getInstance()
+ ->query("select value from account_data where user_id=:user_id and key=:key", [
+ "user_id" => $userId,
+ "key" => $type,
+ ])
+ ->fetchColumn();
+
+ if (empty($value)) {
+ throw new NotFoundError("Account data not found.");
+ }
+
+ return new JsonResponse([
+ $type => $value,
+ ]);
+ }
+
+ #[Route(path: "/_matrix/client/v3/user/{userId}/account_data/{type}", methods: ["PUT"])]
+ public function setAccountData(Request $request): Response
+ {
+ $user = User::authenticateWithRequest($request);
+ $body = json_decode($request->getContent(), true);
+ RequestValidator::validateJson();
+
+ $userId = $request->attributes->get("userId");
+ $type = $request->attributes->get("type");
+
+ if ($user->getId() !== $userId) {
+ throw new UnauthorizedError("Cannot add account data for other users.");
+ }
+
+ Database::getInstance()
+ ->query(
+ <<<SQL
+ insert into account_data (key, value, user_id) values (:key, :value, :user_id)
+ on conflict (user_id, key) do update set
+ value = excluded.value
+ SQL,
+ [
+ "key" => $type,
+ "value" => $request->getContent(),
+ "user_id" => $userId,
+ ]
+ );
+
+ return new JsonResponse();
+ }
+}
diff --git a/src/Controllers/KeyController.php b/src/Controllers/KeyController.php
deleted file mode 100644
index 7777229..0000000
--- a/src/Controllers/KeyController.php
+++ /dev/null
@@ -1,76 +0,0 @@
-<?php
-
-namespace App\Controllers;
-
-use App\Errors\AppException;
-use App\Errors\ErrorCode;
-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;
-
-class KeyController
-{
- public function server(Request $request): Response
- {
- return new JsonResponse([
- "server" => [
- "name" => "Matrix PHP",
- "version" => "0.1.0",
- ],
- ]);
- }
-
- /**
- * POST /_matrix/client/v3/keys/upload
- */
- public function upload(Request $request): Response
- {
- $user = User::authenticateWithRequest($request);
- $body = json_decode($request->getContent(), true);
- RequestValidator::validateJson();
-
- return new JsonResponse(new ClientKeysUploadPostResponse([
- "curve25519" => 0,
- "signed_curve25519" => count($body["one_time_keys"]),
- ]));
- }
-
- public function query(Request $request): Response
- {
- $serverName = $request->attributes->get("serverName");
- }
-
- /**
- * POST /_matrix/client/v3/refresh
- */
- public function refresh(Request $request): Response
- {
- $body = json_decode($request->getContent(), true);
- RequestValidator::validateJson();
-
- $tokens = Tokens::fetchWithRefreshToken($body["refresh_token"]);
-
- if (empty($tokens)) {
- throw new AppException(
- ErrorCode::UNKNOWN_TOKEN,
- "Soft logged out",
- Response::HTTP_UNAUTHORIZED,
- ["soft_logout" => true],
- );
- }
-
- $newTokens = Tokens::new($tokens->getUserId(), $tokens->getDeviceId());
- $newTokens->insert();
-
- 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
deleted file mode 100644
index 9ae3a48..0000000
--- a/src/Controllers/LoginController.php
+++ /dev/null
@@ -1,149 +0,0 @@
-<?php
-
-namespace App\Controllers;
-
-use App\Database;
-use App\Errors\AppException;
-use App\Errors\ErrorCode;
-use App\Errors\UnknownError;
-use App\Models\Device;
-use App\Models\Tokens;
-use App\Models\User;
-use App\Support\Logger;
-use App\Support\Parser;
-use App\Support\RequestValidator;
-use Matrix\Data\LoginFlow;
-use Matrix\Enums\LoginType;
-use Matrix\Enums\UserRegistrationKind;
-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;
-
-class LoginController
-{
- /**
- * GET /_matrix/client/r0/login
- */
- public function supportedLoginTypes(Request $request): Response
- {
- return new JsonResponse(new ClientLoginGetResponse([
- (new LoginFlow(LoginType::PASSWORD)),
- ]));
- }
-
- /**
- * POST /_matrix/client/v3/login
- */
- public function login(Request $request): Response
- {
- Logger::logRequestToFile($request);
-
- $body = json_decode($request->getContent(), true);
- RequestValidator::validateJson();
-
- // validate login type
- $loginType = null;
- try {
- $loginType = LoginType::from($body["type"]);
- } catch (\ValueError $error) {
- throw new UnknownError("Bad login type.", Response::HTTP_BAD_REQUEST);
- }
-
- // get user id
- $userId = Parser::parseUser($body["identifier"]["user"]);
- if (empty($userId["server"])) {
- #$userId = "@$userId[username]:$_ENV[DOMAIN]";
- $userId = "@$userId[username]:localhost";
- }
-
- #if ($loginType == LoginType::PASSWORD) {}
-
- $user = User::fetchWithPassword($userId, $body["password"]);
-
- if (! $user) {
- throw new AppException(ErrorCode::FORBIDDEN, "Invalid credentials", Response::HTTP_FORBIDDEN);
- }
-
- $deviceId = $body["device_id"] ?? "";
-
- $device = null;
- $tokens = null;
-
- // create new device with tokens
- if (empty($deviceId)) {
- $device = Device::new(
- $user->getId(),
- initialDisplayName: $body["initial_device_display_name"] ?? "",
- );
- $device->insert();
-
- $tokens = Tokens::new($userId, $device->getId());
- $tokens->insert();
- } else { // fetch existing device and tokens
- $device = $user->fetchDevice($deviceId);
- $tokens = Tokens::fetch($userId, $device->getId());
-
- if (empty($tokens)) {
- throw new AppException(
- ErrorCode::UNKNOWN_TOKEN,
- "Soft logged out",
- Response::HTTP_UNAUTHORIZED,
- ["soft_logout" => true],
- );
- }
- }
-
- return new JsonResponse(new ClientLoginPostResponse(
- accessToken: $tokens->getAccessToken(),
- deviceId:$device->getId(),
- userId: $user->getId(),
- expiresInMilliseconds: $tokens->getExpiresIn(),
- refreshToken: $tokens->getRefreshToken(),
- ));
- }
-
- /**
- * POST /_matrix/client/v3/register
- */
- public function register(Request $request): Response
- {
- $body = json_decode($request->getContent(), true);
- RequestValidator::validateJson();
-
- // validate kind
- $kind = null;
- try {
- $kind = UserRegistrationKind::from($request->query->get("kind") ?? "user");
- } catch (\ValueError $error) {
- throw new UnknownError("Bad registration kind.", Response::HTTP_BAD_REQUEST);
- }
-
- $username = $body["username"];
- $userId = "@$username:$_ENV[DOMAIN]";
-
- Database::getInstance()->query("insert into users (id, password) values (:id, :password)", [
- "id" => $userId,
- "password" => $body["password"],
- ]);
-
- $device_id = $body["device_id"] ?? "";
- $initialDeviceDisplayName = $body["initial_device_display_name"] ?? "";
-
- $device = Device::new($userId, $device_id, $initialDeviceDisplayName);
- $device->insert();
-
- $tokens = Tokens::new($userId, $device->getId());
- $tokens->insert();
-
- return new JsonResponse(new ClientRegisterPostResponse(
- accessToken: $tokens->getAccessToken(),
- deviceId: $device->getId(),
- expiresInMilliseconds: $tokens->getExpiresIn(),
- refreshToken: $tokens->getRefreshToken(),
- userId: $userId,
- ));
- }
-}
diff --git a/src/Controllers/ServerImplementationController.php b/src/Controllers/Server/ServerInformationController.php
index 93b9a3f..a77bcca 100644
--- a/src/Controllers/ServerImplementationController.php
+++ b/src/Controllers/Server/ServerInformationController.php
@@ -1,28 +1,29 @@
<?php
-namespace App\Controllers;
+namespace App\Controllers\Server;
+use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
-use Symfony\Component\HttpFoundation\JsonResponse;
+use Symfony\Component\Routing\Attribute\Route;
-class ServerImplementationController
+class ServerInformationController
{
- public function version(Request $request): Response
+ #[Route(path: "/.well-known/matrix/server", methods: ["GET"])]
+ public function server(Request $request): Response
{
return new JsonResponse([
- "server" => [
- "name" => "Matrix PHP",
- "version" => "0.1.0",
- ],
+ "m.server" => "$_ENV[DOMAIN]:443",
]);
}
- public function versions(Request $request): Response
+ #[Route(path: "/_matrix/federation/v1/version", methods: ["GET"])]
+ public function version(Request $request): Response
{
return new JsonResponse([
- "versions" => [
- "v1.1",
+ "server" => [
+ "name" => "Matrix PHP",
+ "version" => "0.1.0",
],
]);
}
diff --git a/src/Controllers/ServerDiscoveryController.php b/src/Controllers/ServerDiscoveryController.php
deleted file mode 100644
index 917df14..0000000
--- a/src/Controllers/ServerDiscoveryController.php
+++ /dev/null
@@ -1,34 +0,0 @@
-<?php
-
-namespace App\Controllers;
-
-use Symfony\Component\HttpFoundation\Request;
-use Symfony\Component\HttpFoundation\Response;
-use Symfony\Component\HttpFoundation\JsonResponse;
-
-class ServerDiscoveryController
-{
- public function server(Request $request): Response
- {
- return new JsonResponse([
- "m.server" => "$_ENV[DOMAIN]:443",
- ]);
- }
-
- public function client(Request $request): Response
- {
- return new JsonResponse([
- "m.homeserver" => [
- "base_url" => "http://$_ENV[DOMAIN]",
- ],
- ]);
- }
-
- public function support(Request $request): Response
- {
- return new JsonResponse([
- "contacts" => [],
- "support_page" => "",
- ]);
- }
-}
diff --git a/src/Controllers/SyncController.php b/src/Controllers/SyncController.php
deleted file mode 100755
index acebb11..0000000
--- a/src/Controllers/SyncController.php
+++ /dev/null
@@ -1,126 +0,0 @@
-<?php
-
-namespace App\Controllers;
-
-use App\Database;
-use App\Models\RoomEvent;
-use App\Models\User;
-use Matrix\Data\AccountData;
-use Matrix\Data\DeviceLists;
-use Matrix\Data\Presence;
-use Matrix\Data\Room\Ephemeral;
-use Matrix\Data\Room\JoinedRoom;
-use Matrix\Data\Room\RoomSummary;
-use Matrix\Data\Room\Rooms;
-use Matrix\Data\Room\State;
-use Matrix\Data\Room\Timeline;
-use Matrix\Data\Room\UnreadNotificationCounts;
-use Matrix\Data\ToDevice;
-use Matrix\Enums\MembershipState;
-use Matrix\Enums\PresenceState;
-use Matrix\Events\PresenceEvent;
-use Matrix\Responses\ClientSyncGetResponse;
-use Symfony\Component\HttpFoundation\Request;
-use Symfony\Component\HttpFoundation\Response;
-use Symfony\Component\HttpFoundation\JsonResponse;
-
-class SyncController
-{
- /**
- * GET /_matrix/client/v3/sync
- *
- * @see https://spec.matrix.org/v1.15/client-server-api/#get_matrixclientv3sync
- * @see https://spec.matrix.org/v1.15/client-server-api/#extensions-to-sync
- */
- public function sync(Request $request): Response
- {
- $user = User::authenticateWithRequest($request);
-
- $filter = $request->query->get("filter", "");
- $syncFullState = $request->query->get("full_state", false);
- $setPresence = PresenceState::tryFrom($request->query->get("set_presence") ?? "") ?? PresenceState::ONLINE;
- $since = $request->query->get("since", "");
- $timeout = $request->query->get("timeout", 0);
- $useStateAfter = $request->query->get("use_state_after", false);
-
- if (! empty($filter)) {
- if (str_starts_with($filter, "{")) {
- $filter = json_decode($filter, true);
- } else {
- $filter = Database::getInstance()->query("select * from filters where id=:id", ["id" => $filter])->fetch();
- }
- }
-
- $rooms = Database::getInstance()->query(<<<SQL
- select * from rooms
- left join room_memberships
- on rooms.id = room_memberships.room_id
- where room_memberships.user_id = :user_id
- SQL, [
- "user_id" => $user->getId(),
- ])->fetchAll();
-
- $invitedRooms = [];
- $joinedRooms = [];
- $knockedRooms = [];
- $leftRooms = [];
-
- foreach ($rooms as $room) {
- $events = Database::getInstance()->query(<<<SQL
- select * from room_events
- where room_id = :room_id
- SQL, [
- "room_id" => $room["room_id"],
- #"limit" => ($filter["room"]["timeline"]["limit"] ?? false) ? "limit " . $filter["room"]["timeline"]["limit"] : "",
- ])->fetchAll();
-
- if ($since === "" && MembershipState::tryFrom($room["state"]) === MembershipState::JOIN) {
- $joinedRooms[$room["room_id"]] = new JoinedRoom(
- accountData: new AccountData([]),
- ephemeral: new Ephemeral([]),
- state: new State([]),
- summary: new RoomSummary(
- heroes: [],
- invitedMemberCount: 0,
- joinedMemberCount: 1,
- ),
- timeline: new Timeline(
- events: array_map([RoomEvent::class, "transformEvent"], $events),
- limited: false,# $filter["room"]["timeline"]["limit"] ?? false,
- previousBatch: null,
- ),
- unreadNotifications: new UnreadNotificationCounts(0, 0),
- unreadThreadNotifications: [],
- );
- }
- }
-
- return new JsonResponse(new ClientSyncGetResponse(
- nextBatch: "1",
-
- accountData: new AccountData([]),
-
- deviceLists: new DeviceLists([], []),
-
- deviceOneTimeKeysCount: [
- "signed_curve25519" => 10,
- ],
-
- presence: new Presence([
- new PresenceEvent(
- sender: $user->getId(),
- presence: $setPresence,
- ),
- ]),
-
- rooms: new Rooms(
- $invitedRooms,
- $joinedRooms,
- $knockedRooms,
- $leftRooms,
- ),
-
- toDevice: new ToDevice([]),
- ));
- }
-}
diff --git a/src/Controllers/UserController.php b/src/Controllers/UserController.php
deleted file mode 100755
index d102160..0000000
--- a/src/Controllers/UserController.php
+++ /dev/null
@@ -1,57 +0,0 @@
-<?php
-
-namespace App\Controllers;
-
-use App\Database;
-use App\Errors\UnauthorizedError;
-use App\Models\User;
-use App\Support\RequestValidator;
-use Symfony\Component\HttpFoundation\Request;
-use Symfony\Component\HttpFoundation\Response;
-use Symfony\Component\HttpFoundation\JsonResponse;
-
-class UserController
-{
- /**
- * POST /_matrix/client/r0/user/{userId}/filter
- * POST /_matrix/client/v3/user/{userId}/filter
- *
- * @see https://spec.matrix.org/v1.16/client-server-api/#post_matrixclientv3useruseridfilter
- */
- public function uploadFilter(Request $request): Response
- {
- $accessToken = str_replace("Bearer ", "", $request->headers->get("authorization") ?: "");
- $user = User::fetchWithAccessToken($accessToken);
-
- if (empty($user)) {
- throw new UnauthorizedError();
- }
-
- $userId = $request->get("userId");
- if ($user->getId() !== $userId) {
- throw new UnauthorizedError();
- }
-
- $body = json_decode($request->getContent(), true);
- RequestValidator::validateJson();
-
- $filterId = md5($userId . random_bytes(512));
-
- Database::getInstance()->query(<<<SQL
- insert into filters (id, account_data, event_fields, event_format, presence, room, user_id)
- values (:id, :account_data, :event_fields, :event_format, :presence, :room, :user_id)
- SQL, [
- "id" => $filterId,
- "account_data" => isset($body["account_data"]) ? json_encode($body["account_data"]) : null,
- "event_fields" => isset($body["event_fields"]) ? json_encode($body["event_fields"]) : null,
- "event_format" => isset($body["event_format"]) ? json_encode($body["event_format"]) : null,
- "presence" => isset($body["presence"]) ? json_encode($body["presence"]) : null,
- "room" => isset($body["room"]) ? json_encode($body["room"]) : null,
- "user_id" => $userId,
- ]);
-
- return new JsonResponse([
- "filter_id" => $filterId,
- ]);
- }
-}