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(<< $user->getId(), ])->fetchAll(); $invitedRooms = []; $joinedRooms = []; $knockedRooms = []; $leftRooms = []; $nextBatch = (new \DateTime())->format("U"); foreach ($rooms as $room) { $events = Database::getInstance()->query(<< 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); return new JsonResponse([ "global" => new Ruleset( content: [ new PushRule( actions: [ "notify", [ "set_tweak" => "sound", "value" => "default", ], [ "set_tweak" => "highlight", ], ], default: true, enabled: true, pattern: "alice", ruleId: ".m.rule.contains_user_name", ), ], override: [ new PushRule( actions: [], conditions: [], default: true, enabled: false, ruleId: ".m.rule.master", ), ], room: [], sender: [], underride: [ new PushRule( actions: [ "notify", [ "set_tweak" => "sound", "value" => "ring", ], [ "set_tweak" => "highlight", "value" => false, ], ], conditions: [ new PushCondition( kind: PushConditionKind::EVENT_MATCH, key: "type", pattern: "m.call.invite", ) ], default: true, enabled: true, ruleId: ".m.rule.master", ), ], ), ]); } #[Route(path: "/_matrix/client/v3/capabilities", methods: ["GET"])] public function capabilities(Request $request): Response { $user = User::authenticateWithRequest($request); return new JsonResponse(new Capabilities()); } #[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" => "", ]); } }