diff options
Diffstat (limited to 'src/Controllers')
| -rwxr-xr-x | src/Controllers/AccountController.php | 29 | ||||
| -rw-r--r-- | src/Controllers/Client/ClientController.php | 742 | ||||
| -rw-r--r-- | src/Controllers/Client/KeyController.php | 142 | ||||
| -rw-r--r-- | src/Controllers/Client/MediaController.php | 23 | ||||
| -rwxr-xr-x | src/Controllers/Client/RoomController.php (renamed from src/Controllers/RoomController.php) | 131 | ||||
| -rw-r--r-- | src/Controllers/Client/ServerInformationController.php | 57 | ||||
| -rwxr-xr-x | src/Controllers/Client/UserController.php | 184 | ||||
| -rw-r--r-- | src/Controllers/KeyController.php | 76 | ||||
| -rw-r--r-- | src/Controllers/LoginController.php | 149 | ||||
| -rw-r--r-- | src/Controllers/Server/ServerInformationController.php (renamed from src/Controllers/ServerImplementationController.php) | 23 | ||||
| -rw-r--r-- | src/Controllers/ServerDiscoveryController.php | 34 | ||||
| -rwxr-xr-x | src/Controllers/SyncController.php | 126 | ||||
| -rwxr-xr-x | src/Controllers/UserController.php | 57 |
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, - ]); - } -} |
