diff --git a/src/Appwrite/Messaging/Adapter/Realtime.php b/src/Appwrite/Messaging/Adapter/Realtime.php index d5ac905fd1..202f8246ff 100644 --- a/src/Appwrite/Messaging/Adapter/Realtime.php +++ b/src/Appwrite/Messaging/Adapter/Realtime.php @@ -589,6 +589,11 @@ class Realtime extends MessagingAdapter $roles = [Role::team($project->getAttribute('teamId'))->toString()]; } break; + case 'presences': + $channels[] = 'presences'; + $channels[] = 'presences.' . $parts[1]; + $roles = $payload->getRead(); + break; } return [ diff --git a/src/Appwrite/Platform/Modules/Presences/HTTP/Delete.php b/src/Appwrite/Platform/Modules/Presences/HTTP/Delete.php index f6af01de58..7853abe905 100644 --- a/src/Appwrite/Platform/Modules/Presences/HTTP/Delete.php +++ b/src/Appwrite/Platform/Modules/Presences/HTTP/Delete.php @@ -2,6 +2,7 @@ namespace Appwrite\Platform\Modules\Presences\HTTP; +use Appwrite\Event\Event; use Appwrite\Extend\Exception; use Appwrite\Platform\Modules\Compute\Base; use Appwrite\SDK\AuthType; @@ -31,6 +32,9 @@ class Delete extends Base ->desc('Delete presence') ->groups(['api', 'presences']) ->label('scope', 'users.write') + ->label('event', 'presences.[presenceId].delete') + ->label('audits.event', 'presence.delete') + ->label('audits.resource', 'presence/{request.presenceId}') ->label('sdk', new Method( namespace: 'presences', group: 'presences', @@ -48,10 +52,11 @@ class Delete extends Base ->param('presenceId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Presence unique ID.', false, ['dbForProject']) ->inject('response') ->inject('dbForProject') + ->inject('queueForEvents') ->callback($this->action(...)); } - public function action(string $presenceId, Response $response, Database $dbForProject): void + public function action(string $presenceId, Response $response, Database $dbForProject, Event $queueForEvents): void { $presence = $dbForProject->getDocument('presenceLogs', $presenceId); @@ -60,6 +65,7 @@ class Delete extends Base } $dbForProject->deleteDocument('presenceLogs', $presenceId); + $queueForEvents->setParam('presenceId', $presence->getId()); $response->noContent(); } diff --git a/src/Appwrite/Platform/Modules/Presences/HTTP/Update.php b/src/Appwrite/Platform/Modules/Presences/HTTP/Update.php index 71cd654068..3298108b04 100644 --- a/src/Appwrite/Platform/Modules/Presences/HTTP/Update.php +++ b/src/Appwrite/Platform/Modules/Presences/HTTP/Update.php @@ -2,6 +2,7 @@ namespace Appwrite\Platform\Modules\Presences\HTTP; +use Appwrite\Event\Event; use Appwrite\Extend\Exception; use Appwrite\Platform\Modules\Presences\HTTP\Action as PresenceAction; use Appwrite\SDK\AuthType; @@ -35,6 +36,9 @@ class Update extends PresenceAction ->desc('Update presence') ->groups(['api', 'presences']) ->label('scope', 'users.write') + ->label('event', 'presences.[presenceId].update') + ->label('audits.event', 'presence.update') + ->label('audits.resource', 'presence/{response.$id}') ->label('sdk', new Method( namespace: 'presences', group: 'presences', @@ -58,6 +62,7 @@ class Update extends PresenceAction ->inject('dbForProject') ->inject('user') ->inject('authorization') + ->inject('queueForEvents') ->callback($this->action(...)); } @@ -71,7 +76,8 @@ class Update extends PresenceAction Response $response, Database $dbForProject, User $user, - Authorization $authorization + Authorization $authorization, + Event $queueForEvents ): void { $isAPIKey = $user->isApp($authorization->getRoles()); $isPrivilegedUser = $user->isPrivileged($authorization->getRoles()); @@ -118,6 +124,7 @@ class Update extends PresenceAction } $presence = $dbForProject->updateDocument('presenceLogs', $presenceId, $updates); + $queueForEvents->setParam('presenceId', $presence->getId()); $response->dynamic($presence, Response::MODEL_PRESENCE); } diff --git a/src/Appwrite/Platform/Modules/Presences/HTTP/Upsert.php b/src/Appwrite/Platform/Modules/Presences/HTTP/Upsert.php index feb17b10f9..8b7b3c8a47 100644 --- a/src/Appwrite/Platform/Modules/Presences/HTTP/Upsert.php +++ b/src/Appwrite/Platform/Modules/Presences/HTTP/Upsert.php @@ -2,6 +2,7 @@ namespace Appwrite\Platform\Modules\Presences\HTTP; +use Appwrite\Event\Event; use Appwrite\Extend\Exception; use Appwrite\Platform\Modules\Presences\HTTP\Action as PresenceAction; use Appwrite\SDK\AuthType; @@ -39,6 +40,9 @@ class Upsert extends PresenceAction ->desc('Upsert presence') ->groups(['api', 'presences']) ->label('scope', 'users.write') + ->label('event', 'presences.[presenceId].upsert') + ->label('audits.event', 'presence.upsert') + ->label('audits.resource', 'presence/{response.$id}') ->label('sdk', new Method( namespace: 'presences', group: 'presences', @@ -63,6 +67,7 @@ class Upsert extends PresenceAction ->inject('dbForProject') ->inject('user') ->inject('authorization') + ->inject('queueForEvents') ->callback($this->action(...)); } @@ -76,7 +81,8 @@ class Upsert extends PresenceAction Response $response, Database $dbForProject, User $user, - Authorization $authorization + Authorization $authorization, + Event $queueForEvents ): void { $isAPIKey = $user->isApp($authorization->getRoles()); $isPrivilegedUser = $user->isPrivileged($authorization->getRoles()); @@ -121,6 +127,7 @@ class Upsert extends PresenceAction } $presence = $dbForProject->upsertDocument('presenceLogs', $presenceDocument); + $queueForEvents->setParam('presenceId', $presence->getId()); $response->dynamic($presence, Response::MODEL_PRESENCE); } diff --git a/tests/e2e/Services/Presence/PresenceRealtimeClientTest.php b/tests/e2e/Services/Presence/PresenceRealtimeClientTest.php index e99350e7fb..fcd54c6abb 100644 --- a/tests/e2e/Services/Presence/PresenceRealtimeClientTest.php +++ b/tests/e2e/Services/Presence/PresenceRealtimeClientTest.php @@ -208,4 +208,73 @@ class PresenceRealtimeClientTest extends Scope $client->close(); } + + public function testChannelParsing(): void + { + $presenceId = ID::unique(); + $userId = $this->getUser()['$id']; + $headers = [ + 'origin' => 'http://localhost', + 'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $this->getUser()['session'], + ]; + + $client = $this->getWebsocket(['presences', 'presences.' . $presenceId], $headers, timeout: 5); + $connected = \json_decode($client->receive(), true); + + $this->assertSame('connected', $connected['type'] ?? null); + $this->assertCount(2, $connected['data']['channels'] ?? []); + $this->assertContains('presences', $connected['data']['channels']); + $this->assertContains('presences.' . $presenceId, $connected['data']['channels']); + + $create = $this->client->call( + Client::METHOD_PUT, + '/presences/' . $presenceId, + $this->getServerHeaders(), + [ + 'userId' => $userId, + 'status' => 'online', + 'metadata' => ['source' => 'channel-parsing-create'], + 'permissions' => $this->getPresencePermissions($userId), + ] + ); + $this->assertSame(200, $create['headers']['status-code']); + + $createEvent = \json_decode($client->receive(), true); + $this->assertSame('event', $createEvent['type'] ?? null); + $this->assertContains('presences', $createEvent['data']['channels'] ?? []); + $this->assertContains('presences.' . $presenceId, $createEvent['data']['channels'] ?? []); + $this->assertContains('presences.' . $presenceId . '.upsert', $createEvent['data']['events'] ?? []); + + $update = $this->client->call( + Client::METHOD_PATCH, + '/presences/' . $presenceId, + $this->getServerHeaders(), + [ + 'status' => 'away', + 'metadata' => ['source' => 'channel-parsing-update'], + ] + ); + $this->assertSame(200, $update['headers']['status-code']); + + $updateEvent = \json_decode($client->receive(), true); + $this->assertSame('event', $updateEvent['type'] ?? null); + $this->assertContains('presences', $updateEvent['data']['channels'] ?? []); + $this->assertContains('presences.' . $presenceId, $updateEvent['data']['channels'] ?? []); + $this->assertContains('presences.' . $presenceId . '.update', $updateEvent['data']['events'] ?? []); + + $delete = $this->client->call( + Client::METHOD_DELETE, + '/presences/' . $presenceId, + $this->getServerHeaders() + ); + $this->assertSame(204, $delete['headers']['status-code']); + + $deleteEvent = \json_decode($client->receive(), true); + $this->assertSame('event', $deleteEvent['type'] ?? null); + $this->assertContains('presences', $deleteEvent['data']['channels'] ?? []); + $this->assertContains('presences.' . $presenceId, $deleteEvent['data']['channels'] ?? []); + $this->assertContains('presences.' . $presenceId . '.delete', $deleteEvent['data']['events'] ?? []); + + $client->close(); + } } diff --git a/tests/unit/Messaging/MessagingTest.php b/tests/unit/Messaging/MessagingTest.php index 4b2474c760..ba2ad3ac75 100644 --- a/tests/unit/Messaging/MessagingTest.php +++ b/tests/unit/Messaging/MessagingTest.php @@ -330,4 +330,25 @@ class MessagingTest extends TestCase $this->assertContains(Role::any()->toString(), $result['roles']); $this->assertContains(Role::team('123abc')->toString(), $result['roles']); } + + public function testFromPayloadPresenceChannels(): void + { + $presenceId = ID::custom('presence123'); + + $result = Realtime::fromPayload( + event: 'presences.' . $presenceId . '.upsert', + payload: new Document([ + '$id' => $presenceId, + '$permissions' => [ + Permission::read(Role::any()), + Permission::update(Role::users()), + Permission::delete(Role::users()), + ], + ]), + ); + + $this->assertContains('presences', $result['channels']); + $this->assertContains('presences.' . $presenceId, $result['channels']); + $this->assertContains(Role::any()->toString(), $result['roles']); + } }