diff options
| -rw-r--r-- | matrix-specification/Data/PushRule.php | 2 | ||||
| -rw-r--r-- | matrix-specification/Enums/PushConditionKind.php | 1 | ||||
| -rw-r--r-- | migrations/20250819.php | 23 | ||||
| -rw-r--r-- | src/Controllers/Client/ClientController.php | 332 | ||||
| -rw-r--r-- | src/Controllers/Client/KeyController.php | 34 | ||||
| -rwxr-xr-x | src/Controllers/Client/UserController.php | 61 | ||||
| -rwxr-xr-x | src/Errors/NotFoundError.php | 19 | ||||
| -rwxr-xr-x | src/Errors/UnauthorizedError.php | 4 |
8 files changed, 455 insertions, 21 deletions
diff --git a/matrix-specification/Data/PushRule.php b/matrix-specification/Data/PushRule.php index 2217a78..f904b3a 100644 --- a/matrix-specification/Data/PushRule.php +++ b/matrix-specification/Data/PushRule.php @@ -5,7 +5,7 @@ namespace Matrix\Data; class PushRule implements \JsonSerializable { /** - * @param array<string|array> $actions + * @param array<string|array<string, mixed>> $actions * @param PushCondition[] $conditions */ public function __construct( diff --git a/matrix-specification/Enums/PushConditionKind.php b/matrix-specification/Enums/PushConditionKind.php index fe61c16..41223aa 100644 --- a/matrix-specification/Enums/PushConditionKind.php +++ b/matrix-specification/Enums/PushConditionKind.php @@ -4,6 +4,7 @@ namespace Matrix\Enums; enum PushConditionKind: string implements \JsonSerializable { + case CONTAINS_DISPLAY_NAME = "contains_display_name"; case EVENT_MATCH = "event_match"; case EVENT_PROPERTY_CONTAINS = "event_property_contains"; case EVENT_PROPERTY_IS = "event_property_is"; diff --git a/migrations/20250819.php b/migrations/20250819.php index 3476081..6e3c59b 100644 --- a/migrations/20250819.php +++ b/migrations/20250819.php @@ -20,6 +20,29 @@ Database::getInstance()->query(<<<SQL SQL); Database::getInstance()->query(<<<SQL + create table if not exists "account_data" ( + "key" text not null, + "value" jsonb not null, + + "user_id" text references users(id) not null, + + primary key (user_id, key) + ); +SQL); + +Database::getInstance()->query(<<<SQL + create table if not exists "room_account_data" ( + "key" text not null, + "value" jsonb not null, + + "user_id" text references users(id) not null, + "room_id" text references rooms(id) not null, + + primary key (user_id, room_id, key) + ); +SQL); + +Database::getInstance()->query(<<<SQL create table if not exists "devices" ( "id" varchar(255) not null, "name" varchar(255) not null, diff --git a/src/Controllers/Client/ClientController.php b/src/Controllers/Client/ClientController.php index cdce91e..28aea66 100644 --- a/src/Controllers/Client/ClientController.php +++ b/src/Controllers/Client/ClientController.php @@ -18,6 +18,7 @@ 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; @@ -32,6 +33,7 @@ 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; @@ -128,7 +130,7 @@ class ClientController return new JsonResponse(new ClientLoginPostResponse( accessToken: $tokens->getAccessToken(), - deviceId:$device->getId(), + deviceId: $device->getId(), userId: $user->getId(), expiresInMilliseconds: $tokens->getExpiresIn(), refreshToken: $tokens->getRefreshToken(), @@ -356,10 +358,130 @@ class ClientController { $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", [ @@ -370,20 +492,117 @@ class ClientController "set_tweak" => "highlight", ], ], + ), + + new PushRule( + ruleId: ".m.rule.is_room_mention", default: true, enabled: true, - pattern: "alice", - ruleId: ".m.rule.contains_user_name", + 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", + ], + ], ), - ], - override: [ 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: [], - conditions: [], + ), + + new PushRule( + ruleId: ".m.rule.room.server_acl", default: true, - enabled: false, - ruleId: ".m.rule.master", + 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: [], ), ], @@ -392,27 +611,95 @@ class ClientController 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", ], - [ - "set_tweak" => "highlight", - "value" => false, - ], ], + ), + + 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.call.invite", - ) + pattern: "m.room.encrypted", + ), + ], + actions: [ + ...$notificationActionSound, ], + ), + + new PushRule( + ruleId: ".m.rule.room_one_to_one", default: true, enabled: true, - ruleId: ".m.rule.master", + 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", + ], ), ], ), @@ -424,7 +711,12 @@ class ClientController { $user = User::authenticateWithRequest($request); - return new JsonResponse(new Capabilities()); + return new JsonResponse(new Capabilities( + roomVersions: new RoomVersionsCapability( + available: ["1" => "stable"], + default: "1", + ), + )); } #[Route(path: "/_matrix/client/v3/voip/turnServer", methods: ["GET"])] @@ -439,4 +731,12 @@ class ClientController "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 index 5e3245b..47f8933 100644 --- a/src/Controllers/Client/KeyController.php +++ b/src/Controllers/Client/KeyController.php @@ -2,7 +2,9 @@ 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; @@ -93,10 +95,38 @@ class KeyController $deviceKeys = $body["device_keys"]; $timeout = $body["timeout"] ?? 10000; - foreach ($deviceKeys as $keysUserId => $deviceIds) {} + $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" => [], + "device_keys" => empty($downloadedDeviceKeys) ? new \stdClass() : $downloadedDeviceKeys, ]); } diff --git a/src/Controllers/Client/UserController.php b/src/Controllers/Client/UserController.php index 277ba78..63442ec 100755 --- a/src/Controllers/Client/UserController.php +++ b/src/Controllers/Client/UserController.php @@ -4,10 +4,12 @@ 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; @@ -120,4 +122,63 @@ class UserController 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/Errors/NotFoundError.php b/src/Errors/NotFoundError.php new file mode 100755 index 0000000..a128509 --- /dev/null +++ b/src/Errors/NotFoundError.php @@ -0,0 +1,19 @@ +<?php + +namespace App\Errors; + +use Matrix\Enums\ErrorCode; +use Symfony\Component\HttpFoundation\Response; + +class NotFoundError extends Exception +{ + public function __construct(string $message = "404") + { + parent::__construct(ErrorCode::NOT_FOUND, $message, Response::HTTP_NOT_FOUND); + } + + public function getAdditionalData(): array + { + return []; + } +} diff --git a/src/Errors/UnauthorizedError.php b/src/Errors/UnauthorizedError.php index cd9981f..e164597 100755 --- a/src/Errors/UnauthorizedError.php +++ b/src/Errors/UnauthorizedError.php @@ -7,9 +7,9 @@ use Symfony\Component\HttpFoundation\Response; class UnauthorizedError extends Exception { - public function __construct() + public function __construct(string $message = "Unauthorized") { - parent::__construct(ErrorCode::FORBIDDEN, "Unauthorized", Response::HTTP_UNAUTHORIZED); + parent::__construct(ErrorCode::FORBIDDEN, $message, Response::HTTP_UNAUTHORIZED); } public function getAdditionalData(): array |
