mirror of
https://github.com/appwrite/appwrite.git
synced 2026-05-26 13:51:13 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8b0cc07cc2 | ||
|
|
102988ce85 | ||
|
|
fd2b85a3af | ||
|
|
3b2c8a4d36 |
@@ -40,9 +40,9 @@ _APP_REDIS_PORT=6379
|
||||
_APP_REDIS_PASS=
|
||||
_APP_REDIS_USER=
|
||||
COMPOSE_PROFILES=mariadb,mongodb,postgresql
|
||||
_APP_DB_ADAPTER=mongodb
|
||||
_APP_DB_HOST=mongodb
|
||||
_APP_DB_PORT=27017
|
||||
_APP_DB_ADAPTER=mariadb
|
||||
_APP_DB_HOST=mariadb
|
||||
_APP_DB_PORT=3306
|
||||
_APP_DB_SCHEMA=appwrite
|
||||
_APP_DB_USER=user
|
||||
_APP_DB_PASS=password
|
||||
|
||||
@@ -2754,4 +2754,151 @@ return [
|
||||
],
|
||||
],
|
||||
],
|
||||
// to track connected users via realtime
|
||||
// TODO: check we need expiry or not so that we ttl the user
|
||||
'presence' => [
|
||||
'$collection' => ID::custom(Database::METADATA),
|
||||
'$id' => ID::custom('presence'),
|
||||
'name' => 'Presence',
|
||||
'attributes' => [
|
||||
[
|
||||
'$id' => ID::custom('userInternalId'),
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => Database::LENGTH_KEY,
|
||||
'signed' => true,
|
||||
'required' => true,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('userId'),
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => Database::LENGTH_KEY,
|
||||
'signed' => true,
|
||||
'required' => false,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
]
|
||||
],
|
||||
'indexes' => [
|
||||
[
|
||||
'$id' => ID::custom('_unique_userId'),
|
||||
'type' => Database::INDEX_UNIQUE,
|
||||
'attributes' => ['userId'],
|
||||
'lengths' => [Database::LENGTH_KEY],
|
||||
'orders' => [Database::ORDER_ASC],
|
||||
]
|
||||
]
|
||||
],
|
||||
|
||||
// event logs
|
||||
'presenceLogs' => [
|
||||
'$collection' => ID::custom(Database::METADATA),
|
||||
'$id' => ID::custom('presenceLogs'),
|
||||
'name' => 'Presence Logs',
|
||||
'attributes' => [
|
||||
[
|
||||
'$id' => ID::custom('userInternalId'),
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => Database::LENGTH_KEY,
|
||||
'signed' => true,
|
||||
'required' => true,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('userId'),
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => Database::LENGTH_KEY,
|
||||
'signed' => true,
|
||||
'required' => false,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
// permissions must be sorted before md5 conversion to have deterministic hashes
|
||||
[
|
||||
'$id' => ID::custom('perms_md5'),
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => Database::LENGTH_KEY,
|
||||
'signed' => true,
|
||||
'required' => false,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('expiry'),
|
||||
'type' => Database::VAR_DATETIME,
|
||||
'format' => '',
|
||||
'size' => 0,
|
||||
'signed' => false,
|
||||
'required' => false,
|
||||
'default' => null, // TODO: needed a default value
|
||||
'array' => false,
|
||||
'filters' => ['datetime'],
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('status'),
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => Database::LENGTH_KEY,
|
||||
'signed' => true,
|
||||
'required' => false,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('source'),
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => Database::LENGTH_KEY,
|
||||
'signed' => true,
|
||||
'required' => true,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
// TODO: check the usecase and apply it
|
||||
[
|
||||
'$id' => ID::custom('hostname'),
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => Database::LENGTH_KEY,
|
||||
'signed' => true,
|
||||
'required' => false,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('metadata'),
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => 65535,
|
||||
'signed' => true,
|
||||
'required' => false,
|
||||
'default' => new \stdClass(),
|
||||
'array' => false,
|
||||
'filters' => ['json'],
|
||||
],
|
||||
],
|
||||
'indexes' => [
|
||||
[
|
||||
'$id' => ID::custom('_unique_userId_perms'),
|
||||
'type' => Database::INDEX_UNIQUE,
|
||||
'attributes' => ['userId','perms_md5'],
|
||||
'lengths' => [Database::LENGTH_KEY],
|
||||
'orders' => [Database::ORDER_ASC],
|
||||
]
|
||||
]
|
||||
]
|
||||
];
|
||||
|
||||
@@ -108,6 +108,7 @@ use Appwrite\Utopia\Response\Model\None;
|
||||
use Appwrite\Utopia\Response\Model\Phone;
|
||||
use Appwrite\Utopia\Response\Model\Platform;
|
||||
use Appwrite\Utopia\Response\Model\Preferences;
|
||||
use Appwrite\Utopia\Response\Model\Presence;
|
||||
use Appwrite\Utopia\Response\Model\Project;
|
||||
use Appwrite\Utopia\Response\Model\Provider;
|
||||
use Appwrite\Utopia\Response\Model\ProviderRepository;
|
||||
@@ -172,6 +173,7 @@ Response::setModel(new BaseList('Databases List', Response::MODEL_DATABASE_LIST,
|
||||
Response::setModel(new BaseList('Indexes List', Response::MODEL_INDEX_LIST, 'indexes', Response::MODEL_INDEX));
|
||||
Response::setModel(new BaseList('Column Indexes List', Response::MODEL_COLUMN_INDEX_LIST, 'indexes', Response::MODEL_COLUMN_INDEX));
|
||||
Response::setModel(new BaseList('Users List', Response::MODEL_USER_LIST, 'users', Response::MODEL_USER));
|
||||
Response::setModel(new BaseList('Presence List', Response::MODEL_PRESENCE_LIST, 'presences', Response::MODEL_PRESENCE));
|
||||
Response::setModel(new BaseList('Sessions List', Response::MODEL_SESSION_LIST, 'sessions', Response::MODEL_SESSION));
|
||||
Response::setModel(new BaseList('Identities List', Response::MODEL_IDENTITY_LIST, 'identities', Response::MODEL_IDENTITY));
|
||||
Response::setModel(new BaseList('Logs List', Response::MODEL_LOG_LIST, 'logs', Response::MODEL_LOG));
|
||||
@@ -334,6 +336,7 @@ Response::setModel(new DevKey());
|
||||
Response::setModel(new MockNumber());
|
||||
Response::setModel(new AuthProvider());
|
||||
Response::setModel(new Platform());
|
||||
Response::setModel(new Presence());
|
||||
Response::setModel(new Variable());
|
||||
Response::setModel(new Country());
|
||||
Response::setModel(new Continent());
|
||||
|
||||
@@ -241,6 +241,32 @@ if (!function_exists('triggerStats')) {
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('createPresence')) {
|
||||
function createPresence(Document $project, User $user, string $status, array $permissions, ?DateTime $expiry = null): Document
|
||||
{
|
||||
$dbForProject = getProjectDB($project);
|
||||
sort($permissions);
|
||||
return $dbForProject->withTransaction(function () use ($dbForProject, $user, $status, $permissions, $expiry) {
|
||||
$document = $dbForProject->upsertDocument('presenceLogs', new Document([
|
||||
'userInternalId' => $user->getSequence(),
|
||||
'userId' => $user->getId(),
|
||||
'$permissions' => $permissions,
|
||||
'perms_md5' => md5(json_encode($permissions)),
|
||||
'expiry' => $expiry,
|
||||
'status' => $status,
|
||||
'source' => 'realtime'
|
||||
]));
|
||||
$dbForProject->upsertDocument('presence', new Document([
|
||||
'$id' => $user->getId(),
|
||||
'userInternalId' => $user->getSequence(),
|
||||
'userId' => $user->getId(),
|
||||
]));
|
||||
|
||||
return $document;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
$realtime = getRealtime();
|
||||
|
||||
/**
|
||||
@@ -960,7 +986,70 @@ $server->onMessage(function (int $connection, string $message) use ($server, $re
|
||||
}
|
||||
|
||||
break;
|
||||
case 'presence':
|
||||
if (!array_key_exists('session', $message['data'])) {
|
||||
throw new Exception(Exception::REALTIME_MESSAGE_FORMAT_INVALID, 'Payload is not valid.');
|
||||
}
|
||||
|
||||
if ($project === null || $project->isEmpty()) {
|
||||
throw new Exception(Exception::REALTIME_POLICY_VIOLATION, 'Missing project context. Reconnect to the project first.');
|
||||
}
|
||||
|
||||
$store = new Store();
|
||||
$store->decode($message['data']['session']);
|
||||
|
||||
/** @var User $user */
|
||||
$user = $database->getDocument('users', $store->getProperty('id', ''));
|
||||
|
||||
$proofForToken = new Token();
|
||||
$proofForToken->setHash(new Sha());
|
||||
|
||||
if (
|
||||
empty($user->getId())
|
||||
|| !$user->sessionVerify($store->getProperty('secret', ''), $proofForToken)
|
||||
) {
|
||||
throw new Exception(Exception::REALTIME_MESSAGE_FORMAT_INVALID, 'Session is not valid.');
|
||||
}
|
||||
|
||||
if (
|
||||
!array_key_exists('status', $message['data'])
|
||||
|| !array_key_exists('permissions', $message['data'])
|
||||
) {
|
||||
throw new Exception(Exception::REALTIME_POLICY_VIOLATION, 'Status and permissions are required.');
|
||||
}
|
||||
|
||||
$status = $message['data']['status'];
|
||||
if (!is_string($status) || $status === '') {
|
||||
throw new Exception(Exception::REALTIME_POLICY_VIOLATION, 'Status must be a non-empty string.');
|
||||
}
|
||||
|
||||
$permissions = $message['data']['permissions'];
|
||||
if (!is_array($permissions)) {
|
||||
throw new Exception(Exception::REALTIME_POLICY_VIOLATION, 'Permissions must be an array.');
|
||||
}
|
||||
|
||||
$expiry = null;
|
||||
// $expiryValue = $message['data']['expiry'] ?? null;
|
||||
// if (is_string($expiryValue) && $expiryValue !== '') {
|
||||
// try {
|
||||
// $expiry = new DateTime($expiryValue);
|
||||
// } catch (Throwable) {
|
||||
// throw new Exception(Exception::REALTIME_MESSAGE_FORMAT_INVALID, 'Payload is not valid.');
|
||||
// }
|
||||
// }
|
||||
|
||||
$document = createPresence($project, $user, $status, $permissions, $expiry);
|
||||
|
||||
$server->send([$connection], json_encode([
|
||||
'type' => 'presence',
|
||||
'data' => [
|
||||
'$id' => $document->getId(),
|
||||
'status' => $status,
|
||||
'userId' => $user->getId(),
|
||||
]
|
||||
]));
|
||||
|
||||
break;
|
||||
default:
|
||||
throw new Exception(Exception::REALTIME_MESSAGE_FORMAT_INVALID, 'Message type is not valid.');
|
||||
}
|
||||
|
||||
@@ -0,0 +1,375 @@
|
||||
diff --git a/src/Appwrite/Platform/Appwrite.php b/src/Appwrite/Platform/Appwrite.php
|
||||
index 06312d9cb2..ec23a9a112 100644
|
||||
--- a/src/Appwrite/Platform/Appwrite.php
|
||||
+++ b/src/Appwrite/Platform/Appwrite.php
|
||||
@@ -9,6 +9,7 @@ use Appwrite\Platform\Modules\Core;
|
||||
use Appwrite\Platform\Modules\Databases;
|
||||
use Appwrite\Platform\Modules\Functions;
|
||||
use Appwrite\Platform\Modules\Health;
|
||||
+use Appwrite\Platform\Modules\Presence;
|
||||
use Appwrite\Platform\Modules\Project;
|
||||
use Appwrite\Platform\Modules\Projects;
|
||||
use Appwrite\Platform\Modules\Proxy;
|
||||
@@ -31,6 +32,7 @@ class Appwrite extends Platform
|
||||
$this->addModule(new Projects\Module());
|
||||
$this->addModule(new Functions\Module());
|
||||
$this->addModule(new Health\Module());
|
||||
+ $this->addModule(new Presence\Module());
|
||||
$this->addModule(new Sites\Module());
|
||||
$this->addModule(new Console\Module());
|
||||
$this->addModule(new Proxy\Module());
|
||||
diff --git a/src/Appwrite/Platform/Modules/Presence/HTTP/Iterative/XList.php b/src/Appwrite/Platform/Modules/Presence/HTTP/Iterative/XList.php
|
||||
index 165c4b8f88..f86b9feda5 100644
|
||||
--- a/src/Appwrite/Platform/Modules/Presence/HTTP/Iterative/XList.php
|
||||
+++ b/src/Appwrite/Platform/Modules/Presence/HTTP/Iterative/XList.php
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
-namespace Appwrite\Platform\Modules\Presence\Http\Iterative;
|
||||
+namespace Appwrite\Platform\Modules\Presence\HTTP\Iterative;
|
||||
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Utopia\Response as UtopiaResponse;
|
||||
@@ -9,6 +9,7 @@ use Utopia\Database\Document;
|
||||
use Utopia\Database\Exception\Order as OrderException;
|
||||
use Utopia\Database\Exception\Query as QueryException;
|
||||
use Utopia\Database\Query;
|
||||
+use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\Platform\Action;
|
||||
|
||||
class XList extends Action
|
||||
@@ -22,21 +23,24 @@ class XList extends Action
|
||||
{
|
||||
$this
|
||||
->setHttpMethod(self::HTTP_REQUEST_METHOD_GET)
|
||||
+ // TODO: create a separate scope
|
||||
+ ->label('scope', 'documents.read')
|
||||
->setHttpPath('/v1/iterative/presence')
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
+ ->inject('authorization')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
// Since POC so not adding queries or advanced filtering now
|
||||
// just for getting group based presence list based on permissions
|
||||
- public function action(UtopiaResponse $response, Database $dbForProject): void
|
||||
+ public function action(UtopiaResponse $response, Database $dbForProject, Authorization $authorization): void
|
||||
{
|
||||
try {
|
||||
$presenceLogs = [];
|
||||
- $users = $dbForProject->find('presence',[]);
|
||||
+ $users = $authorization->skip(fn () => $dbForProject->find('presence', []));
|
||||
foreach ($users as $user) {
|
||||
- $presenceLogs[] = $dbForProject->findOne('presenceLogs',[
|
||||
- Query::equal('userId', $user['userId']),
|
||||
+ $presenceLogs[] = $dbForProject->findOne('presenceLogs', [
|
||||
+ Query::equal('userId', [$user['userId']]),
|
||||
Query::orderDesc('$updatedAt'),
|
||||
Query::limit(1),
|
||||
]);
|
||||
diff --git a/src/Appwrite/Platform/Modules/Presence/Services/Http.php b/src/Appwrite/Platform/Modules/Presence/Services/Http.php
|
||||
index 1afeed6164..fa3dc42197 100644
|
||||
--- a/src/Appwrite/Platform/Modules/Presence/Services/Http.php
|
||||
+++ b/src/Appwrite/Platform/Modules/Presence/Services/Http.php
|
||||
@@ -2,7 +2,9 @@
|
||||
|
||||
namespace Appwrite\Platform\Modules\Presence\Services;
|
||||
|
||||
-use Appwrite\Platform\Modules\Presence\Http\Iterative\XList as ListPresence;
|
||||
+use Appwrite\Platform\Modules\Presence\HTTP\Create as CreatePresence;
|
||||
+use Appwrite\Platform\Modules\Presence\HTTP\Iterative\XList as ListPresence;
|
||||
+use Appwrite\Platform\Modules\Presence\HTTP\Iterative\XListUnique as ListPresenceUnique;
|
||||
use Utopia\Platform\Service;
|
||||
|
||||
class Http extends Service
|
||||
@@ -10,6 +12,8 @@ class Http extends Service
|
||||
public function __construct()
|
||||
{
|
||||
$this->type = Service::TYPE_HTTP;
|
||||
+ $this->addAction(CreatePresence::getName(), new CreatePresence());
|
||||
$this->addAction(ListPresence::getName(), new ListPresence());
|
||||
+ $this->addAction(ListPresenceUnique::getName(), new ListPresenceUnique());
|
||||
}
|
||||
}
|
||||
diff --git a/tests/e2e/Services/Realtime/PresenceBase.php b/tests/e2e/Services/Realtime/PresenceBase.php
|
||||
index e69de29bb2..5124708aa2 100644
|
||||
--- a/tests/e2e/Services/Realtime/PresenceBase.php
|
||||
+++ b/tests/e2e/Services/Realtime/PresenceBase.php
|
||||
@@ -0,0 +1,251 @@
|
||||
+<?php
|
||||
+
|
||||
+namespace Tests\E2E\Services\Realtime;
|
||||
+
|
||||
+use Tests\E2E\Client;
|
||||
+use Tests\E2E\Scopes\ProjectCustom;
|
||||
+use Tests\E2E\Scopes\Scope;
|
||||
+use Tests\E2E\Scopes\SideClient;
|
||||
+use Utopia\Database\Helpers\Permission;
|
||||
+use Utopia\Database\Helpers\Role;
|
||||
+
|
||||
+abstract class PresenceBase extends Scope
|
||||
+{
|
||||
+ use RealtimeBase;
|
||||
+ use ProjectCustom;
|
||||
+ use SideClient;
|
||||
+
|
||||
+ /**
|
||||
+ * @var array<string, array<float>>
|
||||
+ */
|
||||
+ private array $timings = [
|
||||
+ 'createPresenceApi' => [],
|
||||
+ 'createPresenceRealtime' => [],
|
||||
+ 'listPresence' => [],
|
||||
+ ];
|
||||
+
|
||||
+ abstract protected function getNumberOfUsersToCreate(): int;
|
||||
+
|
||||
+ abstract protected function getListPresenceURL(): string;
|
||||
+
|
||||
+ protected function getSeedPresenceDocumentsCount(): int
|
||||
+ {
|
||||
+ return 0;
|
||||
+ }
|
||||
+
|
||||
+ public function testListPresenceBenchmarkWithPermissionGroups(): void
|
||||
+ {
|
||||
+ $this->seedPresenceLoad();
|
||||
+
|
||||
+ $usersToCreate = \max(2, $this->getNumberOfUsersToCreate());
|
||||
+ $users = [];
|
||||
+ for ($i = 0; $i < $usersToCreate; $i++) {
|
||||
+ $users[] = $this->getUser(true);
|
||||
+ }
|
||||
+
|
||||
+ $expectedByViewer = [];
|
||||
+ foreach ($users as $viewer) {
|
||||
+ $expectedByViewer[$viewer['$id']] = [];
|
||||
+ }
|
||||
+
|
||||
+ // Uniform visibility: each user's presence is readable by a rotating half-set of users.
|
||||
+ $window = \max(1, (int) \floor($usersToCreate / 2));
|
||||
+ foreach ($users as $ownerIndex => $owner) {
|
||||
+ $status = 'uniform-presence-' . $ownerIndex;
|
||||
+ $permissions = [];
|
||||
+
|
||||
+ for ($offset = 0; $offset < $window; $offset++) {
|
||||
+ $viewerIndex = ($ownerIndex + $offset) % $usersToCreate;
|
||||
+ $viewer = $users[$viewerIndex];
|
||||
+ $permissions[] = Permission::read(Role::user($viewer['$id']));
|
||||
+ $expectedByViewer[$viewer['$id']][$owner['$id']] = $status;
|
||||
+ }
|
||||
+
|
||||
+ $this->reportPresence($owner, $status, $permissions);
|
||||
+ \usleep(50000);
|
||||
+ }
|
||||
+
|
||||
+ foreach ($users as $viewer) {
|
||||
+ $benchmark = $this->fetchPresenceListAs($viewer);
|
||||
+ $this->assertEquals(200, $benchmark['response']['headers']['status-code']);
|
||||
+ $this->assertGreaterThan(0.0, $benchmark['elapsedMs']);
|
||||
+
|
||||
+ $rows = $this->extractPresenceRows($benchmark['response']['body']);
|
||||
+ $visibleMap = $this->indexPresenceRowsByUserId($rows);
|
||||
+ $expectedVisible = $expectedByViewer[$viewer['$id']];
|
||||
+
|
||||
+ foreach ($expectedVisible as $ownerUserId => $status) {
|
||||
+ $this->assertArrayHasKey($ownerUserId, $visibleMap, 'Viewer cannot see expected user: ' . $ownerUserId);
|
||||
+ $this->assertEquals($status, $visibleMap[$ownerUserId]['status'] ?? null);
|
||||
+ }
|
||||
+
|
||||
+ foreach ($users as $owner) {
|
||||
+ $ownerUserId = $owner['$id'];
|
||||
+ if (!isset($expectedVisible[$ownerUserId])) {
|
||||
+ $this->assertArrayNotHasKey($ownerUserId, $visibleMap, 'Viewer should not see unauthorized user: ' . $ownerUserId);
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ $this->printTimingSummary();
|
||||
+ }
|
||||
+
|
||||
+ private function seedPresenceLoad(): void
|
||||
+ {
|
||||
+ $seedCount = \max(0, $this->getSeedPresenceDocumentsCount());
|
||||
+
|
||||
+ if ($seedCount === 0) {
|
||||
+ return;
|
||||
+ }
|
||||
+
|
||||
+ for ($i = 0; $i < $seedCount; $i++) {
|
||||
+ $seedUser = $this->getUser(true);
|
||||
+
|
||||
+ $this->createPresenceViaApi(
|
||||
+ $seedUser,
|
||||
+ 'seed-load-' . $i,
|
||||
+ [Permission::read(Role::user($seedUser['$id']))]
|
||||
+ );
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ private function reportPresence(array $user, string $status, array $permissions): void
|
||||
+ {
|
||||
+ $start = \microtime(true);
|
||||
+ $projectId = $this->getProject()['$id'];
|
||||
+ $client = $this->getWebsocket(['account'], [
|
||||
+ 'origin' => 'http://localhost',
|
||||
+ 'cookie' => 'a_session_' . $projectId . '=' . $user['session'],
|
||||
+ ]);
|
||||
+
|
||||
+ // connected payload
|
||||
+ $client->receive();
|
||||
+
|
||||
+ $expiry = \gmdate('Y-m-d\TH:i:s\Z', \time() + 120);
|
||||
+
|
||||
+ $client->send(\json_encode([
|
||||
+ 'type' => 'presence',
|
||||
+ 'data' => [
|
||||
+ 'session' => $user['session'],
|
||||
+ 'status' => $status,
|
||||
+ 'permissions' => $permissions,
|
||||
+ 'expiry' => $expiry,
|
||||
+ ],
|
||||
+ ]));
|
||||
+
|
||||
+ \usleep(50000);
|
||||
+ $client->close();
|
||||
+
|
||||
+ $elapsedMs = (\microtime(true) - $start) * 1000;
|
||||
+ $this->recordTiming('createPresenceRealtime', $elapsedMs);
|
||||
+ }
|
||||
+
|
||||
+ private function createPresenceViaApi(array $user, string $status, array $permissions): void
|
||||
+ {
|
||||
+ $projectId = $this->getProject()['$id'];
|
||||
+ $headers = [
|
||||
+ 'content-type' => 'application/json',
|
||||
+ 'origin' => 'http://localhost',
|
||||
+ 'x-appwrite-project' => $projectId,
|
||||
+ 'cookie' => 'a_session_' . $projectId . '=' . $user['session'],
|
||||
+ ];
|
||||
+
|
||||
+ $payload = [
|
||||
+ 'status' => $status,
|
||||
+ 'permissions' => $permissions,
|
||||
+ ];
|
||||
+
|
||||
+ $start = \microtime(true);
|
||||
+
|
||||
+ $response = $this->client->call(Client::METHOD_POST, '/iterative/presence', $headers, $payload);
|
||||
+
|
||||
+ $elapsedMs = (\microtime(true) - $start) * 1000;
|
||||
+ $this->recordTiming('createPresenceApi', $elapsedMs);
|
||||
+ $this->assertEquals(
|
||||
+ 201,
|
||||
+ $response['headers']['status-code'],
|
||||
+ 'Create presence failed: ' . json_encode($response['body'] ?? [])
|
||||
+ );
|
||||
+ }
|
||||
+
|
||||
+ private function fetchPresenceListAs(array $user): array
|
||||
+ {
|
||||
+ $projectId = $this->getProject()['$id'];
|
||||
+
|
||||
+ $start = \microtime(true);
|
||||
+ $response = $this->client->call(Client::METHOD_GET, $this->getListPresenceURL(), [
|
||||
+ 'content-type' => 'application/json',
|
||||
+ 'origin' => 'http://localhost',
|
||||
+ 'x-appwrite-project' => $projectId,
|
||||
+ 'cookie' => 'a_session_' . $projectId . '=' . $user['session'],
|
||||
+ ]);
|
||||
+ $elapsedMs = (\microtime(true) - $start) * 1000;
|
||||
+ $this->recordTiming('listPresence', $elapsedMs);
|
||||
+
|
||||
+ return [
|
||||
+ 'elapsedMs' => $elapsedMs,
|
||||
+ 'response' => $response,
|
||||
+ ];
|
||||
+ }
|
||||
+
|
||||
+ private function extractPresenceRows(array $body): array
|
||||
+ {
|
||||
+ foreach (['presences', 'presence', 'documents', 'rows'] as $key) {
|
||||
+ $value = $body[$key] ?? null;
|
||||
+ if (\is_array($value)) {
|
||||
+ return $value;
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ return [];
|
||||
+ }
|
||||
+
|
||||
+ private function indexPresenceRowsByUserId(array $rows): array
|
||||
+ {
|
||||
+ $indexed = [];
|
||||
+ foreach ($rows as $row) {
|
||||
+ if (!\is_array($row)) {
|
||||
+ continue;
|
||||
+ }
|
||||
+
|
||||
+ $userId = $row['userId'] ?? null;
|
||||
+ if (!\is_string($userId) || $userId === '') {
|
||||
+ continue;
|
||||
+ }
|
||||
+
|
||||
+ $indexed[$userId] = $row;
|
||||
+ }
|
||||
+
|
||||
+ return $indexed;
|
||||
+ }
|
||||
+
|
||||
+ private function recordTiming(string $operation, float $elapsedMs): void
|
||||
+ {
|
||||
+ $this->timings[$operation][] = $elapsedMs;
|
||||
+ }
|
||||
+
|
||||
+ private function printTimingSummary(): void
|
||||
+ {
|
||||
+ foreach ($this->timings as $operation => $values) {
|
||||
+ if (empty($values)) {
|
||||
+ continue;
|
||||
+ }
|
||||
+
|
||||
+ $avg = \array_sum($values) / \count($values);
|
||||
+ $min = \min($values);
|
||||
+ $max = \max($values);
|
||||
+
|
||||
+ \fwrite(
|
||||
+ \STDOUT,
|
||||
+ \sprintf(
|
||||
+ "\n[Presence Benchmark] %s count=%d avg=%.2fms min=%.2fms max=%.2fms\n",
|
||||
+ $operation,
|
||||
+ \count($values),
|
||||
+ $avg,
|
||||
+ $min,
|
||||
+ $max
|
||||
+ )
|
||||
+ );
|
||||
+ }
|
||||
+ }
|
||||
+}
|
||||
diff --git a/tests/e2e/Services/Realtime/PresenceIterativeTest.php b/tests/e2e/Services/Realtime/PresenceIterativeTest.php
|
||||
index e69de29bb2..cb0ec8baa2 100644
|
||||
--- a/tests/e2e/Services/Realtime/PresenceIterativeTest.php
|
||||
+++ b/tests/e2e/Services/Realtime/PresenceIterativeTest.php
|
||||
@@ -0,0 +1,21 @@
|
||||
+<?php
|
||||
+
|
||||
+namespace Tests\E2E\Services\Realtime;
|
||||
+
|
||||
+class PresenceIterativeTest extends PresenceBase
|
||||
+{
|
||||
+ protected function getNumberOfUsersToCreate(): int
|
||||
+ {
|
||||
+ return 8;
|
||||
+ }
|
||||
+
|
||||
+ protected function getListPresenceURL(): string
|
||||
+ {
|
||||
+ return '/iterative/presence/unique';
|
||||
+ }
|
||||
+
|
||||
+ protected function getSeedPresenceDocumentsCount(): int
|
||||
+ {
|
||||
+ return 100;
|
||||
+ }
|
||||
+}
|
||||
@@ -0,0 +1,91 @@
|
||||
# Scope Flow (Unauth / Guest)
|
||||
|
||||
This document summarizes how Appwrite computes and validates the **API “scope”** for each HTTP request (and what it does *not* do for database permissions).
|
||||
|
||||
## 1. Endpoint “scope” gate (HTTP)
|
||||
|
||||
The per-request scope evaluation happens in `app/controllers/shared/api.php`.
|
||||
|
||||
### 1.1 Role selection
|
||||
When the request has no authenticated user/session/API key, `$user->isEmpty()` is `true`, so Appwrite selects the **guests** role:
|
||||
|
||||
```php
|
||||
$role = $user->isEmpty()
|
||||
? Role::guests()->toString()
|
||||
: Role::users()->toString();
|
||||
```
|
||||
|
||||
### 1.2 Default scopes for the role
|
||||
Appwrite loads the default scope list from `app/config/roles.php`:
|
||||
|
||||
```php
|
||||
$scopes = $roles[$role]['scopes'];
|
||||
```
|
||||
|
||||
For `guests`, `documents.write` is included by default.
|
||||
|
||||
### 1.3 Validate the route’s declared scope
|
||||
Each endpoint declares the required scope using `->label('scope', ...)`.
|
||||
The request is allowed to proceed only if the endpoint scope is present in the role’s scope list:
|
||||
|
||||
```php
|
||||
$allowed = (array) $route->getLabel('scope', 'none');
|
||||
if (empty(array_intersect($allowed, $scopes))) {
|
||||
throw new Exception(Exception::GENERAL_UNAUTHORIZED_SCOPE, ...);
|
||||
}
|
||||
```
|
||||
|
||||
## 2. “Guest” userId: endpoint scope does not imply a DB user identity
|
||||
|
||||
The “guests” role is used for the HTTP endpoint scope gate.
|
||||
Database permission decisions later depend on the *authorization roles* and the presence of a `userId`.
|
||||
|
||||
When there is no authenticated user, Appwrite may still create a “guest” `Document` for auditing purposes, but with an **empty** `'$id'`:
|
||||
|
||||
```php
|
||||
$user = new User([
|
||||
'$id' => '',
|
||||
'type' => ACTIVITY_TYPE_GUEST,
|
||||
...
|
||||
]);
|
||||
```
|
||||
|
||||
In the DocumentsDB `createDocument` handler, default per-user `$permissions` are only added when `!empty($user->getId())`:
|
||||
|
||||
```php
|
||||
if (!empty($user->getId()) && !$isPrivilegedUser) {
|
||||
// add default 'read/update/delete' permissions for the current user
|
||||
}
|
||||
```
|
||||
|
||||
So even though the endpoint scope gate may succeed for guests, the database layer still requires that the **collection/database permissions** permit the create operation for the guest/authorization context.
|
||||
|
||||
## 3. After the scope gate: database create authorization
|
||||
|
||||
For `documentsdb` document creation, `src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Create.php` checks:
|
||||
|
||||
1. The target database/collection exists and is enabled (unless API key/privileged).
|
||||
2. For each document, the authorization validator must accept the operation:
|
||||
- `create` permission is validated against `$collection->getPermissionsByType(Database::PERMISSION_CREATE)`
|
||||
- `update` is validated against collection/document rules (and may depend on `documentSecurity`)
|
||||
|
||||
If the collection does not grant `create` to a role present in the request’s authorization context (e.g. `guests` / `any` / other configured roles), the request fails with `USER_UNAUTHORIZED`.
|
||||
|
||||
## 4. Bulk create extra restriction (unauth)
|
||||
|
||||
For bulk creates, there is an additional guard in the same DocumentsDB create handler:
|
||||
|
||||
```php
|
||||
if ($isBulk && !$isAPIKey && !$isPrivilegedUser) {
|
||||
throw new Exception(Exception::GENERAL_UNAUTHORIZED_SCOPE);
|
||||
}
|
||||
```
|
||||
|
||||
So bulk create is further restricted beyond the endpoint scope gate.
|
||||
|
||||
## Where to look (quick pointers)
|
||||
|
||||
- HTTP scope gate: `app/controllers/shared/api.php`
|
||||
- Default role->scope mapping: `app/config/roles.php`
|
||||
- DocumentsDB create + DB permission checks: `src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Create.php`
|
||||
|
||||
@@ -0,0 +1,279 @@
|
||||
[34m └── Creating database: logs...[0m
|
||||
[1m[32mdatabase.setup[0m[2m · [0m[32m3ms[0m[2m · [0m[2m0315c704[0m
|
||||
|
||||
[36mdatabase.name [0m [37mlogs[0m
|
||||
[36mdatabase.collections_created[0m [37m0[0m
|
||||
[36mlevel [0m [37minfo[0m
|
||||
|
||||
[2m────────────────────────────────────────────────────────────[0m
|
||||
[34m └── Creating database: appwrite...[0m
|
||||
[1m[32mdatabase.setup[0m[2m · [0m[32m5.1ms[0m[2m · [0m[2me2f2aedf[0m
|
||||
|
||||
[36mdatabase.name [0m [37mappwrite[0m
|
||||
[36mdatabase.collections_created[0m [37m0[0m
|
||||
[36mlevel [0m [37minfo[0m
|
||||
|
||||
[2m────────────────────────────────────────────────────────────[0m
|
||||
[1m[32mhttp.server.start[0m[2m · [0m[32m0ms[0m[2m · [0m[2m59b604f0[0m
|
||||
|
||||
[36mserver.workers [0m [37m60[0m
|
||||
[36mserver.payload_size[0m [37m12582912[0m
|
||||
[36mserver.master_pid [0m [37m1[0m
|
||||
[36mserver.manager_pid [0m [37m7[0m
|
||||
[36mlevel [0m [37minfo[0m
|
||||
|
||||
[2m────────────────────────────────────────────────────────────[0m
|
||||
[1m[32mhttp.request[0m[2m · [0m[32m85ms[0m[2m · [0m[2m691453e9[0m
|
||||
|
||||
[36mhttp.method [0m [37mPOST[0m
|
||||
[36mhttp.path [0m [37m/v1/account[0m
|
||||
[36mhttp.response.code[0m [37m201[0m
|
||||
[36mlevel [0m [37minfo[0m
|
||||
|
||||
[2m────────────────────────────────────────────────────────────[0m
|
||||
[1m[32mhttp.request[0m[2m · [0m[32m82.4ms[0m[2m · [0m[2mc84b3c1b[0m
|
||||
|
||||
[36mhttp.method [0m [37mPOST[0m
|
||||
[36mhttp.path [0m [37m/v1/account/sessions/email[0m
|
||||
[36mhttp.response.code[0m [37m201[0m
|
||||
[36mlevel [0m [37minfo[0m
|
||||
|
||||
[2m────────────────────────────────────────────────────────────[0m
|
||||
[1m[32mhttp.request[0m[2m · [0m[32m3.4ms[0m[2m · [0m[2m7bfbeb70[0m
|
||||
|
||||
[36mhttp.method [0m [37mGET[0m
|
||||
[36mhttp.path [0m [37m/v1/account[0m
|
||||
[36mhttp.response.code[0m [37m200[0m
|
||||
[36mlevel [0m [37minfo[0m
|
||||
|
||||
[2m────────────────────────────────────────────────────────────[0m
|
||||
[1m[32mhttp.request[0m[2m · [0m[32m8ms[0m[2m · [0m[2mb2fc006d[0m
|
||||
|
||||
[36mhttp.method [0m [37mPOST[0m
|
||||
[36mhttp.path [0m [37m/v1/teams[0m
|
||||
[36mhttp.response.code[0m [37m201[0m
|
||||
[36mlevel [0m [37minfo[0m
|
||||
|
||||
[2m────────────────────────────────────────────────────────────[0m
|
||||
[1m[32mhttp.request[0m[2m · [0m[33m658.5ms[0m[2m · [0m[2m9085360f[0m
|
||||
|
||||
[36mhttp.method [0m [37mPOST[0m
|
||||
[36mhttp.path [0m [37m/v1/projects[0m
|
||||
[36mhttp.response.code[0m [37m201[0m
|
||||
[36mlevel [0m [37minfo[0m
|
||||
|
||||
[2m────────────────────────────────────────────────────────────[0m
|
||||
[1m[32mhttp.request[0m[2m · [0m[32m4.3ms[0m[2m · [0m[2m38681d3b[0m
|
||||
|
||||
[36mhttp.method [0m [37mPOST[0m
|
||||
[36mhttp.path [0m [37m/v1/projects/:projectId/keys[0m
|
||||
[36mhttp.response.code[0m [37m201[0m
|
||||
[36mlevel [0m [37minfo[0m
|
||||
|
||||
[2m────────────────────────────────────────────────────────────[0m
|
||||
[1m[32mhttp.request[0m[2m · [0m[32m3.9ms[0m[2m · [0m[2m15566177[0m
|
||||
|
||||
[36mhttp.method [0m [37mPOST[0m
|
||||
[36mhttp.path [0m [37m/v1/projects/:projectId/dev-keys[0m
|
||||
[36mhttp.response.code[0m [37m201[0m
|
||||
[36mlevel [0m [37minfo[0m
|
||||
|
||||
[2m────────────────────────────────────────────────────────────[0m
|
||||
[1m[32mhttp.request[0m[2m · [0m[32m4.8ms[0m[2m · [0m[2ma8ecb3f4[0m
|
||||
|
||||
[36mhttp.method [0m [37mPOST[0m
|
||||
[36mhttp.path [0m [37m/v1/webhooks[0m
|
||||
[36mhttp.response.code[0m [37m201[0m
|
||||
[36mlevel [0m [37minfo[0m
|
||||
|
||||
[2m────────────────────────────────────────────────────────────[0m
|
||||
[1m[32mhttp.request[0m[2m · [0m[33m123.9ms[0m[2m · [0m[2mab97717c[0m
|
||||
|
||||
[36mhttp.method [0m [37mPATCH[0m
|
||||
[36mhttp.path [0m [37m/v1/projects/:projectId/smtp[0m
|
||||
[36mhttp.response.code[0m [37m200[0m
|
||||
[36mlevel [0m [37minfo[0m
|
||||
|
||||
[2m────────────────────────────────────────────────────────────[0m
|
||||
[34m[Presence][Create] presenceId=69c3c4c56ace451371a1 creatorUserId=69c3c4c54841d17f980b creatorUserInternalId=1 status=seed-load-0 permissions=["read(\"user:69c3c4c54841d17f980b\")"] allowedReadRoles=["user:69c3c4c54841d17f980b"] expiry=null[0m
|
||||
[34m[Presence][Create] presenceId=69c3c4c588d8b30491a4 creatorUserId=69c3c4c56bb8f17caf7a creatorUserInternalId=2 status=seed-load-1 permissions=["read(\"user:69c3c4c56bb8f17caf7a\")"] allowedReadRoles=["user:69c3c4c56bb8f17caf7a"] expiry=null[0m
|
||||
[34m[Presence][Create] presenceId=69c3c4c59eb137bbfafb creatorUserId=69c3c4c589c2990fa68b creatorUserInternalId=3 status=seed-load-2 permissions=["read(\"user:69c3c4c589c2990fa68b\")"] allowedReadRoles=["user:69c3c4c589c2990fa68b"] expiry=null[0m
|
||||
[34m[Presence][Create] presenceId=69c3c4c5b57384645c88 creatorUserId=69c3c4c59f9c34c4d5f7 creatorUserInternalId=4 status=seed-load-3 permissions=["read(\"user:69c3c4c59f9c34c4d5f7\")"] allowedReadRoles=["user:69c3c4c59f9c34c4d5f7"] expiry=null[0m
|
||||
[34m[Presence][Create] presenceId=69c3c4c5cab9908e0156 creatorUserId=69c3c4c5b61873b720cb creatorUserInternalId=5 status=seed-load-4 permissions=["read(\"user:69c3c4c5b61873b720cb\")"] allowedReadRoles=["user:69c3c4c5b61873b720cb"] expiry=null[0m
|
||||
[34m[Presence][Create] presenceId=69c3c4c5e1192f7986fd creatorUserId=69c3c4c5cb7be0a29bcd creatorUserInternalId=6 status=seed-load-5 permissions=["read(\"user:69c3c4c5cb7be0a29bcd\")"] allowedReadRoles=["user:69c3c4c5cb7be0a29bcd"] expiry=null[0m
|
||||
[34m[Presence][Create] presenceId=69c3c4c60334c89b7103 creatorUserId=69c3c4c5e2057801da30 creatorUserInternalId=7 status=seed-load-6 permissions=["read(\"user:69c3c4c5e2057801da30\")"] allowedReadRoles=["user:69c3c4c5e2057801da30"] expiry=null[0m
|
||||
[34m[Presence][Create] presenceId=69c3c4c6195a3a1d88e8 creatorUserId=69c3c4c6044b3ee83478 creatorUserInternalId=8 status=seed-load-7 permissions=["read(\"user:69c3c4c6044b3ee83478\")"] allowedReadRoles=["user:69c3c4c6044b3ee83478"] expiry=null[0m
|
||||
[34m[Presence][Create] presenceId=69c3c4c62f7ae5c297e6 creatorUserId=69c3c4c61a3bc31f5004 creatorUserInternalId=9 status=seed-load-8 permissions=["read(\"user:69c3c4c61a3bc31f5004\")"] allowedReadRoles=["user:69c3c4c61a3bc31f5004"] expiry=null[0m
|
||||
[34m[Presence][Create] presenceId=69c3c4c645a5783d2247 creatorUserId=69c3c4c63095cd7eb79b creatorUserInternalId=10 status=seed-load-9 permissions=["read(\"user:69c3c4c63095cd7eb79b\")"] allowedReadRoles=["user:69c3c4c63095cd7eb79b"] expiry=null[0m
|
||||
[34m[Presence][Create] presenceId=69c3c4c65c9eb21b4da4 creatorUserId=69c3c4c646d615986db9 creatorUserInternalId=11 status=seed-load-10 permissions=["read(\"user:69c3c4c646d615986db9\")"] allowedReadRoles=["user:69c3c4c646d615986db9"] expiry=null[0m
|
||||
[34m[Presence][Create] presenceId=69c3c4c672ad6e939983 creatorUserId=69c3c4c65da521f6ded7 creatorUserInternalId=12 status=seed-load-11 permissions=["read(\"user:69c3c4c65da521f6ded7\")"] allowedReadRoles=["user:69c3c4c65da521f6ded7"] expiry=null[0m
|
||||
[34m[Presence][Create] presenceId=69c3c4c6885529a9ee0c creatorUserId=69c3c4c673867d685ef5 creatorUserInternalId=13 status=seed-load-12 permissions=["read(\"user:69c3c4c673867d685ef5\")"] allowedReadRoles=["user:69c3c4c673867d685ef5"] expiry=null[0m
|
||||
[34m[Presence][Create] presenceId=69c3c4c6a475ce934b73 creatorUserId=69c3c4c6894169cd73d6 creatorUserInternalId=14 status=seed-load-13 permissions=["read(\"user:69c3c4c6894169cd73d6\")"] allowedReadRoles=["user:69c3c4c6894169cd73d6"] expiry=null[0m
|
||||
[34m[Presence][Create] presenceId=69c3c4c6be613d23f503 creatorUserId=69c3c4c6a63983e2609d creatorUserInternalId=15 status=seed-load-14 permissions=["read(\"user:69c3c4c6a63983e2609d\")"] allowedReadRoles=["user:69c3c4c6a63983e2609d"] expiry=null[0m
|
||||
[34m[Presence][Create] presenceId=69c3c4c6d6c7e4efff52 creatorUserId=69c3c4c6bf2ba633e9b0 creatorUserInternalId=16 status=seed-load-15 permissions=["read(\"user:69c3c4c6bf2ba633e9b0\")"] allowedReadRoles=["user:69c3c4c6bf2ba633e9b0"] expiry=null[0m
|
||||
[34m[Presence][Create] presenceId=69c3c4c6eef7edd3454a creatorUserId=69c3c4c6d7f8bb919b94 creatorUserInternalId=17 status=seed-load-16 permissions=["read(\"user:69c3c4c6d7f8bb919b94\")"] allowedReadRoles=["user:69c3c4c6d7f8bb919b94"] expiry=null[0m
|
||||
[34m[Presence][Create] presenceId=69c3c4c712aa150e02ff creatorUserId=69c3c4c6efce3ea5c64f creatorUserInternalId=18 status=seed-load-17 permissions=["read(\"user:69c3c4c6efce3ea5c64f\")"] allowedReadRoles=["user:69c3c4c6efce3ea5c64f"] expiry=null[0m
|
||||
[34m[Presence][Create] presenceId=69c3c4c72947176267d4 creatorUserId=69c3c4c713b02d43af5d creatorUserInternalId=19 status=seed-load-18 permissions=["read(\"user:69c3c4c713b02d43af5d\")"] allowedReadRoles=["user:69c3c4c713b02d43af5d"] expiry=null[0m
|
||||
[34m[Presence][Create] presenceId=69c3c4c745ee2bc05dd7 creatorUserId=69c3c4c72a2f5a455615 creatorUserInternalId=20 status=seed-load-19 permissions=["read(\"user:69c3c4c72a2f5a455615\")"] allowedReadRoles=["user:69c3c4c72a2f5a455615"] expiry=null[0m
|
||||
[34m[Presence][Create] presenceId=69c3c4c76cc5619d7952 creatorUserId=69c3c4c746ef8ccf63d2 creatorUserInternalId=21 status=seed-load-20 permissions=["read(\"user:69c3c4c746ef8ccf63d2\")"] allowedReadRoles=["user:69c3c4c746ef8ccf63d2"] expiry=null[0m
|
||||
[34m[Presence][Create] presenceId=69c3c4c78b0f72e6f308 creatorUserId=69c3c4c76e4696874cd2 creatorUserInternalId=22 status=seed-load-21 permissions=["read(\"user:69c3c4c76e4696874cd2\")"] allowedReadRoles=["user:69c3c4c76e4696874cd2"] expiry=null[0m
|
||||
[34m[Presence][Create] presenceId=69c3c4c7a5861027bbb2 creatorUserId=69c3c4c78c95a0ad7f3b creatorUserInternalId=23 status=seed-load-22 permissions=["read(\"user:69c3c4c78c95a0ad7f3b\")"] allowedReadRoles=["user:69c3c4c78c95a0ad7f3b"] expiry=null[0m
|
||||
[34m[Presence][Create] presenceId=69c3c4c7bdcc9346f6cb creatorUserId=69c3c4c7a6525f92b5ea creatorUserInternalId=24 status=seed-load-23 permissions=["read(\"user:69c3c4c7a6525f92b5ea\")"] allowedReadRoles=["user:69c3c4c7a6525f92b5ea"] expiry=null[0m
|
||||
[34m[Presence][Create] presenceId=69c3c4c7d4748916ca1b creatorUserId=69c3c4c7be86cb172ba1 creatorUserInternalId=25 status=seed-load-24 permissions=["read(\"user:69c3c4c7be86cb172ba1\")"] allowedReadRoles=["user:69c3c4c7be86cb172ba1"] expiry=null[0m
|
||||
[34m[Presence][Create] presenceId=69c3c4c7eb109ba5aab3 creatorUserId=69c3c4c7d53b8709c3f6 creatorUserInternalId=26 status=seed-load-25 permissions=["read(\"user:69c3c4c7d53b8709c3f6\")"] allowedReadRoles=["user:69c3c4c7d53b8709c3f6"] expiry=null[0m
|
||||
[34m[Presence][Create] presenceId=69c3c4c80d3eee93bec5 creatorUserId=69c3c4c7ebc6cd745fba creatorUserInternalId=27 status=seed-load-26 permissions=["read(\"user:69c3c4c7ebc6cd745fba\")"] allowedReadRoles=["user:69c3c4c7ebc6cd745fba"] expiry=null[0m
|
||||
[34m[Presence][Create] presenceId=69c3c4c826222fc63b29 creatorUserId=69c3c4c80dd361298b2d creatorUserInternalId=28 status=seed-load-27 permissions=["read(\"user:69c3c4c80dd361298b2d\")"] allowedReadRoles=["user:69c3c4c80dd361298b2d"] expiry=null[0m
|
||||
[34m[Presence][Create] presenceId=69c3c4c83ff385f56e03 creatorUserId=69c3c4c82702aaef7601 creatorUserInternalId=29 status=seed-load-28 permissions=["read(\"user:69c3c4c82702aaef7601\")"] allowedReadRoles=["user:69c3c4c82702aaef7601"] expiry=null[0m
|
||||
[34m[Presence][Create] presenceId=69c3c4c8588d838b15c4 creatorUserId=69c3c4c840c38a2cb2a1 creatorUserInternalId=30 status=seed-load-29 permissions=["read(\"user:69c3c4c840c38a2cb2a1\")"] allowedReadRoles=["user:69c3c4c840c38a2cb2a1"] expiry=null[0m
|
||||
[34m[Presence][Create] presenceId=69c3c4c872553a3bd691 creatorUserId=69c3c4c8593f7a759d90 creatorUserInternalId=31 status=seed-load-30 permissions=["read(\"user:69c3c4c8593f7a759d90\")"] allowedReadRoles=["user:69c3c4c8593f7a759d90"] expiry=null[0m
|
||||
[34m[Presence][Create] presenceId=69c3c4c88a3ed8171be7 creatorUserId=69c3c4c87364129b8b10 creatorUserInternalId=32 status=seed-load-31 permissions=["read(\"user:69c3c4c87364129b8b10\")"] allowedReadRoles=["user:69c3c4c87364129b8b10"] expiry=null[0m
|
||||
[34m[Presence][Create] presenceId=69c3c4c8a166e5f6918f creatorUserId=69c3c4c88b50b7da379c creatorUserInternalId=33 status=seed-load-32 permissions=["read(\"user:69c3c4c88b50b7da379c\")"] allowedReadRoles=["user:69c3c4c88b50b7da379c"] expiry=null[0m
|
||||
[34m[Presence][Create] presenceId=69c3c4c8b833b4c4c039 creatorUserId=69c3c4c8a29626880e8a creatorUserInternalId=34 status=seed-load-33 permissions=["read(\"user:69c3c4c8a29626880e8a\")"] allowedReadRoles=["user:69c3c4c8a29626880e8a"] expiry=null[0m
|
||||
[34m[Presence][Create] presenceId=69c3c4c8cdce4601b40e creatorUserId=69c3c4c8b8d555460ce4 creatorUserInternalId=35 status=seed-load-34 permissions=["read(\"user:69c3c4c8b8d555460ce4\")"] allowedReadRoles=["user:69c3c4c8b8d555460ce4"] expiry=null[0m
|
||||
[34m[Presence][Create] presenceId=69c3c4c8e354c645edbd creatorUserId=69c3c4c8ce681f6b14b5 creatorUserInternalId=36 status=seed-load-35 permissions=["read(\"user:69c3c4c8ce681f6b14b5\")"] allowedReadRoles=["user:69c3c4c8ce681f6b14b5"] expiry=null[0m
|
||||
[34m[Presence][Create] presenceId=69c3c4c91855e8fa04bc creatorUserId=69c3c4c8e42809a8447f creatorUserInternalId=37 status=seed-load-36 permissions=["read(\"user:69c3c4c8e42809a8447f\")"] allowedReadRoles=["user:69c3c4c8e42809a8447f"] expiry=null[0m
|
||||
[34m[Presence][Create] presenceId=69c3c4c9314ab1324ec3 creatorUserId=69c3c4c919560c20b571 creatorUserInternalId=38 status=seed-load-37 permissions=["read(\"user:69c3c4c919560c20b571\")"] allowedReadRoles=["user:69c3c4c919560c20b571"] expiry=null[0m
|
||||
[34m[Presence][Create] presenceId=69c3c4c9484eef2ff740 creatorUserId=69c3c4c932129f7ca7e4 creatorUserInternalId=39 status=seed-load-38 permissions=["read(\"user:69c3c4c932129f7ca7e4\")"] allowedReadRoles=["user:69c3c4c932129f7ca7e4"] expiry=null[0m
|
||||
[34m[Presence][Create] presenceId=69c3c4c95e8ec4aca79a creatorUserId=69c3c4c94910abcc8aec creatorUserInternalId=40 status=seed-load-39 permissions=["read(\"user:69c3c4c94910abcc8aec\")"] allowedReadRoles=["user:69c3c4c94910abcc8aec"] expiry=null[0m
|
||||
[34m[Presence][Create] presenceId=69c3c4c975518c7aa1bd creatorUserId=69c3c4c95f24f9af0831 creatorUserInternalId=41 status=seed-load-40 permissions=["read(\"user:69c3c4c95f24f9af0831\")"] allowedReadRoles=["user:69c3c4c95f24f9af0831"] expiry=null[0m
|
||||
[34m[Presence][Create] presenceId=69c3c4c98c02f23b36f3 creatorUserId=69c3c4c9760f86b1abbb creatorUserInternalId=42 status=seed-load-41 permissions=["read(\"user:69c3c4c9760f86b1abbb\")"] allowedReadRoles=["user:69c3c4c9760f86b1abbb"] expiry=null[0m
|
||||
[34m[Presence][Create] presenceId=69c3c4c9a5806bf0e1ae creatorUserId=69c3c4c98c98e5d1af05 creatorUserInternalId=43 status=seed-load-42 permissions=["read(\"user:69c3c4c98c98e5d1af05\")"] allowedReadRoles=["user:69c3c4c98c98e5d1af05"] expiry=null[0m
|
||||
[34m[Presence][Create] presenceId=69c3c4c9bf94a1fadde5 creatorUserId=69c3c4c9a62794290964 creatorUserInternalId=44 status=seed-load-43 permissions=["read(\"user:69c3c4c9a62794290964\")"] allowedReadRoles=["user:69c3c4c9a62794290964"] expiry=null[0m
|
||||
[34m[Presence][Create] presenceId=69c3c4c9d8ec34edf8dd creatorUserId=69c3c4c9c096487b20d6 creatorUserInternalId=45 status=seed-load-44 permissions=["read(\"user:69c3c4c9c096487b20d6\")"] allowedReadRoles=["user:69c3c4c9c096487b20d6"] expiry=null[0m
|
||||
[34m[Presence][Create] presenceId=69c3c4ca0125bc4b8a1f creatorUserId=69c3c4c9d9b2f95e2274 creatorUserInternalId=46 status=seed-load-45 permissions=["read(\"user:69c3c4c9d9b2f95e2274\")"] allowedReadRoles=["user:69c3c4c9d9b2f95e2274"] expiry=null[0m
|
||||
[34m[Presence][Create] presenceId=69c3c4ca1b3d271733bf creatorUserId=69c3c4ca020b86e230fa creatorUserInternalId=47 status=seed-load-46 permissions=["read(\"user:69c3c4ca020b86e230fa\")"] allowedReadRoles=["user:69c3c4ca020b86e230fa"] expiry=null[0m
|
||||
[34m[Presence][Create] presenceId=69c3c4ca35506c30928c creatorUserId=69c3c4ca1c19fdab77fe creatorUserInternalId=48 status=seed-load-47 permissions=["read(\"user:69c3c4ca1c19fdab77fe\")"] allowedReadRoles=["user:69c3c4ca1c19fdab77fe"] expiry=null[0m
|
||||
[34m[Presence][Create] presenceId=69c3c4ca4e2e607ea1b4 creatorUserId=69c3c4ca368b01d28694 creatorUserInternalId=49 status=seed-load-48 permissions=["read(\"user:69c3c4ca368b01d28694\")"] allowedReadRoles=["user:69c3c4ca368b01d28694"] expiry=null[0m
|
||||
[34m[Presence][Create] presenceId=69c3c4ca6bc2cda50bf7 creatorUserId=69c3c4ca4f12d1833b1b creatorUserInternalId=50 status=seed-load-49 permissions=["read(\"user:69c3c4ca4f12d1833b1b\")"] allowedReadRoles=["user:69c3c4ca4f12d1833b1b"] expiry=null[0m
|
||||
[34m[Presence][Create] presenceId=69c3c4ca82e2a56f06be creatorUserId=69c3c4ca6c88186bfb08 creatorUserInternalId=51 status=seed-load-50 permissions=["read(\"user:69c3c4ca6c88186bfb08\")"] allowedReadRoles=["user:69c3c4ca6c88186bfb08"] expiry=null[0m
|
||||
[34m[Presence][Create] presenceId=69c3c4ca993f6ee33f80 creatorUserId=69c3c4ca83803d70d020 creatorUserInternalId=52 status=seed-load-51 permissions=["read(\"user:69c3c4ca83803d70d020\")"] allowedReadRoles=["user:69c3c4ca83803d70d020"] expiry=null[0m
|
||||
[34m[Presence][Create] presenceId=69c3c4caafaa4b1ab97d creatorUserId=69c3c4ca9a41e097a1f4 creatorUserInternalId=53 status=seed-load-52 permissions=["read(\"user:69c3c4ca9a41e097a1f4\")"] allowedReadRoles=["user:69c3c4ca9a41e097a1f4"] expiry=null[0m
|
||||
[34m[Presence][Create] presenceId=69c3c4cac5afd9a17d7a creatorUserId=69c3c4cab04b5d8d847b creatorUserInternalId=54 status=seed-load-53 permissions=["read(\"user:69c3c4cab04b5d8d847b\")"] allowedReadRoles=["user:69c3c4cab04b5d8d847b"] expiry=null[0m
|
||||
[34m[Presence][Create] presenceId=69c3c4cadcceb3ef2f18 creatorUserId=69c3c4cac6da2c7334bc creatorUserInternalId=55 status=seed-load-54 permissions=["read(\"user:69c3c4cac6da2c7334bc\")"] allowedReadRoles=["user:69c3c4cac6da2c7334bc"] expiry=null[0m
|
||||
[34m[Presence][Create] presenceId=69c3c4cb012d0ac72269 creatorUserId=69c3c4cadd9b588677f6 creatorUserInternalId=56 status=seed-load-55 permissions=["read(\"user:69c3c4cadd9b588677f6\")"] allowedReadRoles=["user:69c3c4cadd9b588677f6"] expiry=null[0m
|
||||
[34m[Presence][Create] presenceId=69c3c4cb171b6055fb23 creatorUserId=69c3c4cb01dba80b770f creatorUserInternalId=57 status=seed-load-56 permissions=["read(\"user:69c3c4cb01dba80b770f\")"] allowedReadRoles=["user:69c3c4cb01dba80b770f"] expiry=null[0m
|
||||
[34m[Presence][Create] presenceId=69c3c4cb2d4888836fc7 creatorUserId=69c3c4cb17ff0602b51f creatorUserInternalId=58 status=seed-load-57 permissions=["read(\"user:69c3c4cb17ff0602b51f\")"] allowedReadRoles=["user:69c3c4cb17ff0602b51f"] expiry=null[0m
|
||||
[34m[Presence][Create] presenceId=69c3c4cb434f9b80288f creatorUserId=69c3c4cb2df430669fc9 creatorUserInternalId=59 status=seed-load-58 permissions=["read(\"user:69c3c4cb2df430669fc9\")"] allowedReadRoles=["user:69c3c4cb2df430669fc9"] expiry=null[0m
|
||||
[34m[Presence][Create] presenceId=69c3c4cb593ab1108adb creatorUserId=69c3c4cb4429dacfa37b creatorUserInternalId=60 status=seed-load-59 permissions=["read(\"user:69c3c4cb4429dacfa37b\")"] allowedReadRoles=["user:69c3c4cb4429dacfa37b"] expiry=null[0m
|
||||
[34m[Presence][Create] presenceId=69c3c4cb705ac2108e84 creatorUserId=69c3c4cb59fc972454ca creatorUserInternalId=61 status=seed-load-60 permissions=["read(\"user:69c3c4cb59fc972454ca\")"] allowedReadRoles=["user:69c3c4cb59fc972454ca"] expiry=null[0m
|
||||
[34m[Presence][Create] presenceId=69c3c4cb86f28aa29eb6 creatorUserId=69c3c4cb7117244e97a9 creatorUserInternalId=62 status=seed-load-61 permissions=["read(\"user:69c3c4cb7117244e97a9\")"] allowedReadRoles=["user:69c3c4cb7117244e97a9"] expiry=null[0m
|
||||
[34m[Presence][Create] presenceId=69c3c4cb9de034bffd1b creatorUserId=69c3c4cb87dcaffd33af creatorUserInternalId=63 status=seed-load-62 permissions=["read(\"user:69c3c4cb87dcaffd33af\")"] allowedReadRoles=["user:69c3c4cb87dcaffd33af"] expiry=null[0m
|
||||
[34m[Presence][Create] presenceId=69c3c4cbb4f00f24c7a4 creatorUserId=69c3c4cb9f026ad79750 creatorUserInternalId=64 status=seed-load-63 permissions=["read(\"user:69c3c4cb9f026ad79750\")"] allowedReadRoles=["user:69c3c4cb9f026ad79750"] expiry=null[0m
|
||||
[34m[Presence][Create] presenceId=69c3c4cbcab6da5b5ab0 creatorUserId=69c3c4cbb59aa1049c7d creatorUserInternalId=65 status=seed-load-64 permissions=["read(\"user:69c3c4cbb59aa1049c7d\")"] allowedReadRoles=["user:69c3c4cbb59aa1049c7d"] expiry=null[0m
|
||||
[34m[Presence][Create] presenceId=69c3c4cbe369257073e6 creatorUserId=69c3c4cbcbafae64b2d5 creatorUserInternalId=66 status=seed-load-65 permissions=["read(\"user:69c3c4cbcbafae64b2d5\")"] allowedReadRoles=["user:69c3c4cbcbafae64b2d5"] expiry=null[0m
|
||||
[34m[Presence][Create] presenceId=69c3c4cc0528e1889ffa creatorUserId=69c3c4cbe4234c8a7436 creatorUserInternalId=67 status=seed-load-66 permissions=["read(\"user:69c3c4cbe4234c8a7436\")"] allowedReadRoles=["user:69c3c4cbe4234c8a7436"] expiry=null[0m
|
||||
[34m[Presence][Create] presenceId=69c3c4cc1be86868f379 creatorUserId=69c3c4cc05da299e391c creatorUserInternalId=68 status=seed-load-67 permissions=["read(\"user:69c3c4cc05da299e391c\")"] allowedReadRoles=["user:69c3c4cc05da299e391c"] expiry=null[0m
|
||||
[34m[Presence][Create] presenceId=69c3c4cc32ab71359be8 creatorUserId=69c3c4cc1d34921f5d3d creatorUserInternalId=69 status=seed-load-68 permissions=["read(\"user:69c3c4cc1d34921f5d3d\")"] allowedReadRoles=["user:69c3c4cc1d34921f5d3d"] expiry=null[0m
|
||||
[34m[Presence][Create] presenceId=69c3c4cc48ec1ee35390 creatorUserId=69c3c4cc3395c6dbf6b4 creatorUserInternalId=70 status=seed-load-69 permissions=["read(\"user:69c3c4cc3395c6dbf6b4\")"] allowedReadRoles=["user:69c3c4cc3395c6dbf6b4"] expiry=null[0m
|
||||
[34m[Presence][Create] presenceId=69c3c4cc5f200fbd5f4c creatorUserId=69c3c4cc49bba8351229 creatorUserInternalId=71 status=seed-load-70 permissions=["read(\"user:69c3c4cc49bba8351229\")"] allowedReadRoles=["user:69c3c4cc49bba8351229"] expiry=null[0m
|
||||
[34m[Presence][Create] presenceId=69c3c4cc770048b9c583 creatorUserId=69c3c4cc600e94c356fb creatorUserInternalId=72 status=seed-load-71 permissions=["read(\"user:69c3c4cc600e94c356fb\")"] allowedReadRoles=["user:69c3c4cc600e94c356fb"] expiry=null[0m
|
||||
[34m[Presence][Create] presenceId=69c3c4cc8e1b4aacfe66 creatorUserId=69c3c4cc77c94de533a9 creatorUserInternalId=73 status=seed-load-72 permissions=["read(\"user:69c3c4cc77c94de533a9\")"] allowedReadRoles=["user:69c3c4cc77c94de533a9"] expiry=null[0m
|
||||
[34m[Presence][Create] presenceId=69c3c4cca54115b2073a creatorUserId=69c3c4cc8ec226e16aa5 creatorUserInternalId=74 status=seed-load-73 permissions=["read(\"user:69c3c4cc8ec226e16aa5\")"] allowedReadRoles=["user:69c3c4cc8ec226e16aa5"] expiry=null[0m
|
||||
[34m[Presence][Create] presenceId=69c3c4ccbc03600bda1c creatorUserId=69c3c4cca657ec7b7eae creatorUserInternalId=75 status=seed-load-74 permissions=["read(\"user:69c3c4cca657ec7b7eae\")"] allowedReadRoles=["user:69c3c4cca657ec7b7eae"] expiry=null[0m
|
||||
[34m[Presence][Create] presenceId=69c3c4ccd24f44be3a9b creatorUserId=69c3c4ccbcb98856f5f1 creatorUserInternalId=76 status=seed-load-75 permissions=["read(\"user:69c3c4ccbcb98856f5f1\")"] allowedReadRoles=["user:69c3c4ccbcb98856f5f1"] expiry=null[0m
|
||||
[34m[Presence][Create] presenceId=69c3c4cce9253a1cc7ef creatorUserId=69c3c4ccd2fdda2721b3 creatorUserInternalId=77 status=seed-load-76 permissions=["read(\"user:69c3c4ccd2fdda2721b3\")"] allowedReadRoles=["user:69c3c4ccd2fdda2721b3"] expiry=null[0m
|
||||
[34m[Presence][Create] presenceId=69c3c4cd0c5b8c4f36da creatorUserId=69c3c4cce9fe57f1aa39 creatorUserInternalId=78 status=seed-load-77 permissions=["read(\"user:69c3c4cce9fe57f1aa39\")"] allowedReadRoles=["user:69c3c4cce9fe57f1aa39"] expiry=null[0m
|
||||
[34m[Presence][Create] presenceId=69c3c4cd22d2340473bf creatorUserId=69c3c4cd0d72b78e9721 creatorUserInternalId=79 status=seed-load-78 permissions=["read(\"user:69c3c4cd0d72b78e9721\")"] allowedReadRoles=["user:69c3c4cd0d72b78e9721"] expiry=null[0m
|
||||
[34m[Presence][Create] presenceId=69c3c4cd390bf0aa2f24 creatorUserId=69c3c4cd23c36931906d creatorUserInternalId=80 status=seed-load-79 permissions=["read(\"user:69c3c4cd23c36931906d\")"] allowedReadRoles=["user:69c3c4cd23c36931906d"] expiry=null[0m
|
||||
[34m[Presence][Create] presenceId=69c3c4cd4f3fa151fece creatorUserId=69c3c4cd39faa29575ad creatorUserInternalId=81 status=seed-load-80 permissions=["read(\"user:69c3c4cd39faa29575ad\")"] allowedReadRoles=["user:69c3c4cd39faa29575ad"] expiry=null[0m
|
||||
[34m[Presence][Create] presenceId=69c3c4cd66700b2ebeb1 creatorUserId=69c3c4cd504c8d82de8c creatorUserInternalId=82 status=seed-load-81 permissions=["read(\"user:69c3c4cd504c8d82de8c\")"] allowedReadRoles=["user:69c3c4cd504c8d82de8c"] expiry=null[0m
|
||||
[34m[Presence][Create] presenceId=69c3c4cd7d7e4c3e39b7 creatorUserId=69c3c4cd6722c11f5d3c creatorUserInternalId=83 status=seed-load-82 permissions=["read(\"user:69c3c4cd6722c11f5d3c\")"] allowedReadRoles=["user:69c3c4cd6722c11f5d3c"] expiry=null[0m
|
||||
[34m[Presence][Create] presenceId=69c3c4cd9500ad23f2d1 creatorUserId=69c3c4cd7f5b5d824f19 creatorUserInternalId=84 status=seed-load-83 permissions=["read(\"user:69c3c4cd7f5b5d824f19\")"] allowedReadRoles=["user:69c3c4cd7f5b5d824f19"] expiry=null[0m
|
||||
[34m[Presence][Create] presenceId=69c3c4cdab6430527db1 creatorUserId=69c3c4cd95ed047abaf8 creatorUserInternalId=85 status=seed-load-84 permissions=["read(\"user:69c3c4cd95ed047abaf8\")"] allowedReadRoles=["user:69c3c4cd95ed047abaf8"] expiry=null[0m
|
||||
[34m[Presence][Create] presenceId=69c3c4cdce5d90e08839 creatorUserId=69c3c4cdaeea5ad72cee creatorUserInternalId=86 status=seed-load-85 permissions=["read(\"user:69c3c4cdaeea5ad72cee\")"] allowedReadRoles=["user:69c3c4cdaeea5ad72cee"] expiry=null[0m
|
||||
[34m[Presence][Create] presenceId=69c3c4cdec1a72eb157f creatorUserId=69c3c4cdcf21d0f4fa0f creatorUserInternalId=87 status=seed-load-86 permissions=["read(\"user:69c3c4cdcf21d0f4fa0f\")"] allowedReadRoles=["user:69c3c4cdcf21d0f4fa0f"] expiry=null[0m
|
||||
[34m[Presence][Create] presenceId=69c3c4ce0f34c32e62f1 creatorUserId=69c3c4cded1340d16f05 creatorUserInternalId=88 status=seed-load-87 permissions=["read(\"user:69c3c4cded1340d16f05\")"] allowedReadRoles=["user:69c3c4cded1340d16f05"] expiry=null[0m
|
||||
[34m[Presence][Create] presenceId=69c3c4ce2805daf2a243 creatorUserId=69c3c4ce0fe17c4125c5 creatorUserInternalId=89 status=seed-load-88 permissions=["read(\"user:69c3c4ce0fe17c4125c5\")"] allowedReadRoles=["user:69c3c4ce0fe17c4125c5"] expiry=null[0m
|
||||
[34m[Presence][Create] presenceId=69c3c4ce3fbeb7fa150d creatorUserId=69c3c4ce28b4931bca5a creatorUserInternalId=90 status=seed-load-89 permissions=["read(\"user:69c3c4ce28b4931bca5a\")"] allowedReadRoles=["user:69c3c4ce28b4931bca5a"] expiry=null[0m
|
||||
[34m[Presence][Create] presenceId=69c3c4ce57ab854ca9db creatorUserId=69c3c4ce409823582a3a creatorUserInternalId=91 status=seed-load-90 permissions=["read(\"user:69c3c4ce409823582a3a\")"] allowedReadRoles=["user:69c3c4ce409823582a3a"] expiry=null[0m
|
||||
[34m[Presence][Create] presenceId=69c3c4ce6e1975b7549f creatorUserId=69c3c4ce5888c8058712 creatorUserInternalId=92 status=seed-load-91 permissions=["read(\"user:69c3c4ce5888c8058712\")"] allowedReadRoles=["user:69c3c4ce5888c8058712"] expiry=null[0m
|
||||
[34m[Presence][Create] presenceId=69c3c4ce858176eef53e creatorUserId=69c3c4ce6ef972bf230f creatorUserInternalId=93 status=seed-load-92 permissions=["read(\"user:69c3c4ce6ef972bf230f\")"] allowedReadRoles=["user:69c3c4ce6ef972bf230f"] expiry=null[0m
|
||||
[34m[Presence][Create] presenceId=69c3c4ce9b542d1a21cd creatorUserId=69c3c4ce865fbfd4d52c creatorUserInternalId=94 status=seed-load-93 permissions=["read(\"user:69c3c4ce865fbfd4d52c\")"] allowedReadRoles=["user:69c3c4ce865fbfd4d52c"] expiry=null[0m
|
||||
[34m[Presence][Create] presenceId=69c3c4ceb1255ea5eb53 creatorUserId=69c3c4ce9befaa675543 creatorUserInternalId=95 status=seed-load-94 permissions=["read(\"user:69c3c4ce9befaa675543\")"] allowedReadRoles=["user:69c3c4ce9befaa675543"] expiry=null[0m
|
||||
[34m[Presence][Create] presenceId=69c3c4cec7e3c02d8c69 creatorUserId=69c3c4ceb1fed3ad5846 creatorUserInternalId=96 status=seed-load-95 permissions=["read(\"user:69c3c4ceb1fed3ad5846\")"] allowedReadRoles=["user:69c3c4ceb1fed3ad5846"] expiry=null[0m
|
||||
[34m[Presence][Create] presenceId=69c3c4cedde388757f65 creatorUserId=69c3c4cec8d5f2a62f19 creatorUserInternalId=97 status=seed-load-96 permissions=["read(\"user:69c3c4cec8d5f2a62f19\")"] allowedReadRoles=["user:69c3c4cec8d5f2a62f19"] expiry=null[0m
|
||||
[34m[Presence][Create] presenceId=69c3c4cf0301ecb8f64d creatorUserId=69c3c4cedeb597b92294 creatorUserInternalId=98 status=seed-load-97 permissions=["read(\"user:69c3c4cedeb597b92294\")"] allowedReadRoles=["user:69c3c4cedeb597b92294"] expiry=null[0m
|
||||
[34m[Presence][Create] presenceId=69c3c4cf18ec2a87aab1 creatorUserId=69c3c4cf03c05c68c5ff creatorUserInternalId=99 status=seed-load-98 permissions=["read(\"user:69c3c4cf03c05c68c5ff\")"] allowedReadRoles=["user:69c3c4cf03c05c68c5ff"] expiry=null[0m
|
||||
[34m[Presence][Create] presenceId=69c3c4cf2fd9eb2fd41e creatorUserId=69c3c4cf19b8e319f413 creatorUserInternalId=100 status=seed-load-99 permissions=["read(\"user:69c3c4cf19b8e319f413\")"] allowedReadRoles=["user:69c3c4cf19b8e319f413"] expiry=null[0m
|
||||
[34m[Presence][XList] requesterUserId=69c3c4cf30b59e4a5549 requesterUserInternalId=101 requesterRoles=["any","users","user:69c3c4cf30b59e4a5549","user:69c3c4cf30b59e4a5549/unverified","users/unverified"][0m
|
||||
[34m[Presence][XList][DENY] requesterUserId=69c3c4cf30b59e4a5549 presenceOwnerUserId=69c3c4c54841d17f980b presenceId=69c3c4c56ace451371a1 allowedReadRoles=["user:69c3c4c54841d17f980b"] requesterRoles=["any","users","user:69c3c4cf30b59e4a5549","user:69c3c4cf30b59e4a5549/unverified","users/unverified"] validatorDescription=Missing "read" permission for role "user:69c3c4c54841d17f980b". Only "["any","users","user:69c3c4cf30b59e4a5549","user:69c3c4cf30b59e4a5549\/unverified","users\/unverified"]" scopes are allowed and "["user:69c3c4c54841d17f980b"]" was given. status=seed-load-0[0m
|
||||
[34m[Presence][XList][DENY] requesterUserId=69c3c4cf30b59e4a5549 presenceOwnerUserId=69c3c4c56bb8f17caf7a presenceId=69c3c4c588d8b30491a4 allowedReadRoles=["user:69c3c4c56bb8f17caf7a"] requesterRoles=["any","users","user:69c3c4cf30b59e4a5549","user:69c3c4cf30b59e4a5549/unverified","users/unverified"] validatorDescription=Missing "read" permission for role "user:69c3c4c56bb8f17caf7a". Only "["any","users","user:69c3c4cf30b59e4a5549","user:69c3c4cf30b59e4a5549\/unverified","users\/unverified"]" scopes are allowed and "["user:69c3c4c56bb8f17caf7a"]" was given. status=seed-load-1[0m
|
||||
[34m[Presence][XList][DENY] requesterUserId=69c3c4cf30b59e4a5549 presenceOwnerUserId=69c3c4c589c2990fa68b presenceId=69c3c4c59eb137bbfafb allowedReadRoles=["user:69c3c4c589c2990fa68b"] requesterRoles=["any","users","user:69c3c4cf30b59e4a5549","user:69c3c4cf30b59e4a5549/unverified","users/unverified"] validatorDescription=Missing "read" permission for role "user:69c3c4c589c2990fa68b". Only "["any","users","user:69c3c4cf30b59e4a5549","user:69c3c4cf30b59e4a5549\/unverified","users\/unverified"]" scopes are allowed and "["user:69c3c4c589c2990fa68b"]" was given. status=seed-load-2[0m
|
||||
[34m[Presence][XList][DENY] requesterUserId=69c3c4cf30b59e4a5549 presenceOwnerUserId=69c3c4c59f9c34c4d5f7 presenceId=69c3c4c5b57384645c88 allowedReadRoles=["user:69c3c4c59f9c34c4d5f7"] requesterRoles=["any","users","user:69c3c4cf30b59e4a5549","user:69c3c4cf30b59e4a5549/unverified","users/unverified"] validatorDescription=Missing "read" permission for role "user:69c3c4c59f9c34c4d5f7". Only "["any","users","user:69c3c4cf30b59e4a5549","user:69c3c4cf30b59e4a5549\/unverified","users\/unverified"]" scopes are allowed and "["user:69c3c4c59f9c34c4d5f7"]" was given. status=seed-load-3[0m
|
||||
[34m[Presence][XList][DENY] requesterUserId=69c3c4cf30b59e4a5549 presenceOwnerUserId=69c3c4c5b61873b720cb presenceId=69c3c4c5cab9908e0156 allowedReadRoles=["user:69c3c4c5b61873b720cb"] requesterRoles=["any","users","user:69c3c4cf30b59e4a5549","user:69c3c4cf30b59e4a5549/unverified","users/unverified"] validatorDescription=Missing "read" permission for role "user:69c3c4c5b61873b720cb". Only "["any","users","user:69c3c4cf30b59e4a5549","user:69c3c4cf30b59e4a5549\/unverified","users\/unverified"]" scopes are allowed and "["user:69c3c4c5b61873b720cb"]" was given. status=seed-load-4[0m
|
||||
[34m[Presence][XList][DENY] requesterUserId=69c3c4cf30b59e4a5549 presenceOwnerUserId=69c3c4c5cb7be0a29bcd presenceId=69c3c4c5e1192f7986fd allowedReadRoles=["user:69c3c4c5cb7be0a29bcd"] requesterRoles=["any","users","user:69c3c4cf30b59e4a5549","user:69c3c4cf30b59e4a5549/unverified","users/unverified"] validatorDescription=Missing "read" permission for role "user:69c3c4c5cb7be0a29bcd". Only "["any","users","user:69c3c4cf30b59e4a5549","user:69c3c4cf30b59e4a5549\/unverified","users\/unverified"]" scopes are allowed and "["user:69c3c4c5cb7be0a29bcd"]" was given. status=seed-load-5[0m
|
||||
[34m[Presence][XList][DENY] requesterUserId=69c3c4cf30b59e4a5549 presenceOwnerUserId=69c3c4c5e2057801da30 presenceId=69c3c4c60334c89b7103 allowedReadRoles=["user:69c3c4c5e2057801da30"] requesterRoles=["any","users","user:69c3c4cf30b59e4a5549","user:69c3c4cf30b59e4a5549/unverified","users/unverified"] validatorDescription=Missing "read" permission for role "user:69c3c4c5e2057801da30". Only "["any","users","user:69c3c4cf30b59e4a5549","user:69c3c4cf30b59e4a5549\/unverified","users\/unverified"]" scopes are allowed and "["user:69c3c4c5e2057801da30"]" was given. status=seed-load-6[0m
|
||||
[34m[Presence][XList][DENY] requesterUserId=69c3c4cf30b59e4a5549 presenceOwnerUserId=69c3c4c6044b3ee83478 presenceId=69c3c4c6195a3a1d88e8 allowedReadRoles=["user:69c3c4c6044b3ee83478"] requesterRoles=["any","users","user:69c3c4cf30b59e4a5549","user:69c3c4cf30b59e4a5549/unverified","users/unverified"] validatorDescription=Missing "read" permission for role "user:69c3c4c6044b3ee83478". Only "["any","users","user:69c3c4cf30b59e4a5549","user:69c3c4cf30b59e4a5549\/unverified","users\/unverified"]" scopes are allowed and "["user:69c3c4c6044b3ee83478"]" was given. status=seed-load-7[0m
|
||||
[34m[Presence][XList][DENY] requesterUserId=69c3c4cf30b59e4a5549 presenceOwnerUserId=69c3c4c61a3bc31f5004 presenceId=69c3c4c62f7ae5c297e6 allowedReadRoles=["user:69c3c4c61a3bc31f5004"] requesterRoles=["any","users","user:69c3c4cf30b59e4a5549","user:69c3c4cf30b59e4a5549/unverified","users/unverified"] validatorDescription=Missing "read" permission for role "user:69c3c4c61a3bc31f5004". Only "["any","users","user:69c3c4cf30b59e4a5549","user:69c3c4cf30b59e4a5549\/unverified","users\/unverified"]" scopes are allowed and "["user:69c3c4c61a3bc31f5004"]" was given. status=seed-load-8[0m
|
||||
[34m[Presence][XList][DENY] requesterUserId=69c3c4cf30b59e4a5549 presenceOwnerUserId=69c3c4c63095cd7eb79b presenceId=69c3c4c645a5783d2247 allowedReadRoles=["user:69c3c4c63095cd7eb79b"] requesterRoles=["any","users","user:69c3c4cf30b59e4a5549","user:69c3c4cf30b59e4a5549/unverified","users/unverified"] validatorDescription=Missing "read" permission for role "user:69c3c4c63095cd7eb79b". Only "["any","users","user:69c3c4cf30b59e4a5549","user:69c3c4cf30b59e4a5549\/unverified","users\/unverified"]" scopes are allowed and "["user:69c3c4c63095cd7eb79b"]" was given. status=seed-load-9[0m
|
||||
[34m[Presence][XList][DENY] requesterUserId=69c3c4cf30b59e4a5549 presenceOwnerUserId=69c3c4c646d615986db9 presenceId=69c3c4c65c9eb21b4da4 allowedReadRoles=["user:69c3c4c646d615986db9"] requesterRoles=["any","users","user:69c3c4cf30b59e4a5549","user:69c3c4cf30b59e4a5549/unverified","users/unverified"] validatorDescription=Missing "read" permission for role "user:69c3c4c646d615986db9". Only "["any","users","user:69c3c4cf30b59e4a5549","user:69c3c4cf30b59e4a5549\/unverified","users\/unverified"]" scopes are allowed and "["user:69c3c4c646d615986db9"]" was given. status=seed-load-10[0m
|
||||
[34m[Presence][XList][DENY] requesterUserId=69c3c4cf30b59e4a5549 presenceOwnerUserId=69c3c4c65da521f6ded7 presenceId=69c3c4c672ad6e939983 allowedReadRoles=["user:69c3c4c65da521f6ded7"] requesterRoles=["any","users","user:69c3c4cf30b59e4a5549","user:69c3c4cf30b59e4a5549/unverified","users/unverified"] validatorDescription=Missing "read" permission for role "user:69c3c4c65da521f6ded7". Only "["any","users","user:69c3c4cf30b59e4a5549","user:69c3c4cf30b59e4a5549\/unverified","users\/unverified"]" scopes are allowed and "["user:69c3c4c65da521f6ded7"]" was given. status=seed-load-11[0m
|
||||
[34m[Presence][XList][DENY] requesterUserId=69c3c4cf30b59e4a5549 presenceOwnerUserId=69c3c4c673867d685ef5 presenceId=69c3c4c6885529a9ee0c allowedReadRoles=["user:69c3c4c673867d685ef5"] requesterRoles=["any","users","user:69c3c4cf30b59e4a5549","user:69c3c4cf30b59e4a5549/unverified","users/unverified"] validatorDescription=Missing "read" permission for role "user:69c3c4c673867d685ef5". Only "["any","users","user:69c3c4cf30b59e4a5549","user:69c3c4cf30b59e4a5549\/unverified","users\/unverified"]" scopes are allowed and "["user:69c3c4c673867d685ef5"]" was given. status=seed-load-12[0m
|
||||
[34m[Presence][XList][DENY] requesterUserId=69c3c4cf30b59e4a5549 presenceOwnerUserId=69c3c4c6894169cd73d6 presenceId=69c3c4c6a475ce934b73 allowedReadRoles=["user:69c3c4c6894169cd73d6"] requesterRoles=["any","users","user:69c3c4cf30b59e4a5549","user:69c3c4cf30b59e4a5549/unverified","users/unverified"] validatorDescription=Missing "read" permission for role "user:69c3c4c6894169cd73d6". Only "["any","users","user:69c3c4cf30b59e4a5549","user:69c3c4cf30b59e4a5549\/unverified","users\/unverified"]" scopes are allowed and "["user:69c3c4c6894169cd73d6"]" was given. status=seed-load-13[0m
|
||||
[34m[Presence][XList][DENY] requesterUserId=69c3c4cf30b59e4a5549 presenceOwnerUserId=69c3c4c6a63983e2609d presenceId=69c3c4c6be613d23f503 allowedReadRoles=["user:69c3c4c6a63983e2609d"] requesterRoles=["any","users","user:69c3c4cf30b59e4a5549","user:69c3c4cf30b59e4a5549/unverified","users/unverified"] validatorDescription=Missing "read" permission for role "user:69c3c4c6a63983e2609d". Only "["any","users","user:69c3c4cf30b59e4a5549","user:69c3c4cf30b59e4a5549\/unverified","users\/unverified"]" scopes are allowed and "["user:69c3c4c6a63983e2609d"]" was given. status=seed-load-14[0m
|
||||
[34m[Presence][XList][DENY] requesterUserId=69c3c4cf30b59e4a5549 presenceOwnerUserId=69c3c4c6bf2ba633e9b0 presenceId=69c3c4c6d6c7e4efff52 allowedReadRoles=["user:69c3c4c6bf2ba633e9b0"] requesterRoles=["any","users","user:69c3c4cf30b59e4a5549","user:69c3c4cf30b59e4a5549/unverified","users/unverified"] validatorDescription=Missing "read" permission for role "user:69c3c4c6bf2ba633e9b0". Only "["any","users","user:69c3c4cf30b59e4a5549","user:69c3c4cf30b59e4a5549\/unverified","users\/unverified"]" scopes are allowed and "["user:69c3c4c6bf2ba633e9b0"]" was given. status=seed-load-15[0m
|
||||
[34m[Presence][XList][DENY] requesterUserId=69c3c4cf30b59e4a5549 presenceOwnerUserId=69c3c4c6d7f8bb919b94 presenceId=69c3c4c6eef7edd3454a allowedReadRoles=["user:69c3c4c6d7f8bb919b94"] requesterRoles=["any","users","user:69c3c4cf30b59e4a5549","user:69c3c4cf30b59e4a5549/unverified","users/unverified"] validatorDescription=Missing "read" permission for role "user:69c3c4c6d7f8bb919b94". Only "["any","users","user:69c3c4cf30b59e4a5549","user:69c3c4cf30b59e4a5549\/unverified","users\/unverified"]" scopes are allowed and "["user:69c3c4c6d7f8bb919b94"]" was given. status=seed-load-16[0m
|
||||
[34m[Presence][XList][DENY] requesterUserId=69c3c4cf30b59e4a5549 presenceOwnerUserId=69c3c4c6efce3ea5c64f presenceId=69c3c4c712aa150e02ff allowedReadRoles=["user:69c3c4c6efce3ea5c64f"] requesterRoles=["any","users","user:69c3c4cf30b59e4a5549","user:69c3c4cf30b59e4a5549/unverified","users/unverified"] validatorDescription=Missing "read" permission for role "user:69c3c4c6efce3ea5c64f". Only "["any","users","user:69c3c4cf30b59e4a5549","user:69c3c4cf30b59e4a5549\/unverified","users\/unverified"]" scopes are allowed and "["user:69c3c4c6efce3ea5c64f"]" was given. status=seed-load-17[0m
|
||||
[34m[Presence][XList][DENY] requesterUserId=69c3c4cf30b59e4a5549 presenceOwnerUserId=69c3c4c713b02d43af5d presenceId=69c3c4c72947176267d4 allowedReadRoles=["user:69c3c4c713b02d43af5d"] requesterRoles=["any","users","user:69c3c4cf30b59e4a5549","user:69c3c4cf30b59e4a5549/unverified","users/unverified"] validatorDescription=Missing "read" permission for role "user:69c3c4c713b02d43af5d". Only "["any","users","user:69c3c4cf30b59e4a5549","user:69c3c4cf30b59e4a5549\/unverified","users\/unverified"]" scopes are allowed and "["user:69c3c4c713b02d43af5d"]" was given. status=seed-load-18[0m
|
||||
[34m[Presence][XList][DENY] requesterUserId=69c3c4cf30b59e4a5549 presenceOwnerUserId=69c3c4c72a2f5a455615 presenceId=69c3c4c745ee2bc05dd7 allowedReadRoles=["user:69c3c4c72a2f5a455615"] requesterRoles=["any","users","user:69c3c4cf30b59e4a5549","user:69c3c4cf30b59e4a5549/unverified","users/unverified"] validatorDescription=Missing "read" permission for role "user:69c3c4c72a2f5a455615". Only "["any","users","user:69c3c4cf30b59e4a5549","user:69c3c4cf30b59e4a5549\/unverified","users\/unverified"]" scopes are allowed and "["user:69c3c4c72a2f5a455615"]" was given. status=seed-load-19[0m
|
||||
[32mWorker 1 stopped successfully[0m
|
||||
[32mWorker 2 stopped successfully[0m
|
||||
[32mWorker 3 stopped successfully[0m
|
||||
[32mWorker 4 stopped successfully[0m
|
||||
[32mWorker 8 stopped successfully[0m
|
||||
[32mWorker 6 stopped successfully[0m
|
||||
[32mWorker 5 stopped successfully[0m
|
||||
[32mWorker 36 stopped successfully[0m
|
||||
[32mWorker 7 stopped successfully[0m
|
||||
[32mWorker 23 stopped successfully[0m
|
||||
[32mWorker 9 stopped successfully[0m
|
||||
[32mWorker 49 stopped successfully[0m
|
||||
[32mWorker 24 stopped successfully[0m
|
||||
[32mWorker 50 stopped successfully[0m
|
||||
[32mWorker 31 stopped successfully[0m
|
||||
[32mWorker 22 stopped successfully[0m
|
||||
[32mWorker 11 stopped successfully[0m
|
||||
[32mWorker 21 stopped successfully[0m
|
||||
[32mWorker 25 stopped successfully[0m
|
||||
[32mWorker 52 stopped successfully[0m
|
||||
[32mWorker 12 stopped successfully[0m
|
||||
[32mWorker 32 stopped successfully[0m
|
||||
[32mWorker 20 stopped successfully[0m
|
||||
[32mWorker 19 stopped successfully[0m
|
||||
[32mWorker 53 stopped successfully[0m
|
||||
[32mWorker 39 stopped successfully[0m
|
||||
[32mWorker 26 stopped successfully[0m
|
||||
[32mWorker 54 stopped successfully[0m
|
||||
[32mWorker 13 stopped successfully[0m
|
||||
[32mWorker 10 stopped successfully[0m
|
||||
[32mWorker 55 stopped successfully[0m
|
||||
[32mWorker 28 stopped successfully[0m
|
||||
[32mWorker 34 stopped successfully[0m
|
||||
[32mWorker 40 stopped successfully[0m
|
||||
[32mWorker 27 stopped successfully[0m
|
||||
[32mWorker 29 stopped successfully[0m
|
||||
[32mWorker 18 stopped successfully[0m
|
||||
[32mWorker 35 stopped successfully[0m
|
||||
[32mWorker 14 stopped successfully[0m
|
||||
[32mWorker 30 stopped successfully[0m
|
||||
[32mWorker 37 stopped successfully[0m
|
||||
[32mWorker 33 stopped successfully[0m
|
||||
[32mWorker 56 stopped successfully[0m
|
||||
[32mWorker 15 stopped successfully[0m
|
||||
[32mWorker 41 stopped successfully[0m
|
||||
[32mWorker 57 stopped successfully[0m
|
||||
[32mWorker 58 stopped successfully[0m
|
||||
[32mWorker 51 stopped successfully[0m
|
||||
[32mWorker 42 stopped successfully[0m
|
||||
[32mWorker 16 stopped successfully[0m
|
||||
[32mWorker 59 stopped successfully[0m
|
||||
[32mWorker 60 stopped successfully[0m
|
||||
[32mWorker 17 stopped successfully[0m
|
||||
[32mWorker 48 stopped successfully[0m
|
||||
[32mWorker 47 stopped successfully[0m
|
||||
[32mWorker 46 stopped successfully[0m
|
||||
[32mWorker 45 stopped successfully[0m
|
||||
[32mWorker 44 stopped successfully[0m
|
||||
[32mWorker 38 stopped successfully[0m
|
||||
[32mWorker 43 stopped successfully[0m
|
||||
[32mWorker 61 stopped successfully[0m
|
||||
@@ -9,6 +9,7 @@ use Appwrite\Platform\Modules\Core;
|
||||
use Appwrite\Platform\Modules\Databases;
|
||||
use Appwrite\Platform\Modules\Functions;
|
||||
use Appwrite\Platform\Modules\Health;
|
||||
use Appwrite\Platform\Modules\Presence;
|
||||
use Appwrite\Platform\Modules\Project;
|
||||
use Appwrite\Platform\Modules\Projects;
|
||||
use Appwrite\Platform\Modules\Proxy;
|
||||
@@ -31,6 +32,7 @@ class Appwrite extends Platform
|
||||
$this->addModule(new Projects\Module());
|
||||
$this->addModule(new Functions\Module());
|
||||
$this->addModule(new Health\Module());
|
||||
$this->addModule(new Presence\Module());
|
||||
$this->addModule(new Sites\Module());
|
||||
$this->addModule(new Console\Module());
|
||||
$this->addModule(new Proxy\Module());
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Platform\Modules\Presence\HTTP;
|
||||
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Utopia\Console;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\DateTime;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Platform\Action;
|
||||
use Utopia\Validator\ArrayList;
|
||||
use Utopia\Validator\Text;
|
||||
|
||||
class Create extends Action
|
||||
{
|
||||
public static function getName(): string
|
||||
{
|
||||
return 'createPresence';
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
->setHttpMethod(self::HTTP_REQUEST_METHOD_POST)
|
||||
->setHttpPath('/v1/iterative/presence')
|
||||
->groups(['api', 'presence'])
|
||||
->label('scope', 'documents.write')
|
||||
->param('status', '', new Text(255), 'Presence status.')
|
||||
->param('permissions', [], new ArrayList(new Text(255), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Permissions list.')
|
||||
->param('expiry', null, new Text(64, 0), 'Presence expiry in ISO 8601 format.', true)
|
||||
->inject('response')
|
||||
->inject('user')
|
||||
->inject('dbForProject')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
public function action(
|
||||
string $status,
|
||||
array $permissions,
|
||||
?string $expiry,
|
||||
Response $response,
|
||||
Document $user,
|
||||
Database $dbForProject
|
||||
): void {
|
||||
if (empty($user->getId())) {
|
||||
throw new Exception(Exception::USER_UNAUTHORIZED, 'User is unauthorized.');
|
||||
}
|
||||
|
||||
sort($permissions);
|
||||
|
||||
$parsedExpiry = null;
|
||||
if (!empty($expiry)) {
|
||||
try {
|
||||
$parsedExpiry = new DateTime($expiry);
|
||||
} catch (\Throwable) {
|
||||
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Expiry is not a valid datetime.');
|
||||
}
|
||||
}
|
||||
$presenceLog = $dbForProject->upsertDocument('presenceLogs', new Document([
|
||||
// Stable identity so repeated createPresence calls overwrite the latest state
|
||||
// instead of creating multiple documents per user.
|
||||
'userInternalId' => $user->getSequence(),
|
||||
'userId' => $user->getId(),
|
||||
'$permissions' => $permissions,
|
||||
'perms_md5' => md5((string) json_encode($permissions)),
|
||||
'status' => $status,
|
||||
'expiry' => $parsedExpiry,
|
||||
'source' => 'realtime'
|
||||
]));
|
||||
|
||||
$dbForProject->upsertDocument('presence', new Document([
|
||||
// Stable identity so the "presence" table doesn't accumulate duplicates
|
||||
'$id' => $user->getId(),
|
||||
'userInternalId' => $user->getSequence(),
|
||||
'userId' => $user->getId(),
|
||||
]));
|
||||
|
||||
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_CREATED)
|
||||
->dynamic(new Document([
|
||||
'$id' => $presenceLog->getId(),
|
||||
'userId' => $presenceLog->getAttribute('userId'),
|
||||
'status' => $presenceLog->getAttribute('status'),
|
||||
'expiry' => $presenceLog->getAttribute('expiry'),
|
||||
'$createdAt' => $presenceLog->getCreatedAt(),
|
||||
'$updatedAt' => $presenceLog->getUpdatedAt(),
|
||||
]), Response::MODEL_PRESENCE);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Platform\Modules\Presence\HTTP;
|
||||
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Utopia\Response as UtopiaResponse;
|
||||
use Utopia\Console;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Exception\Order as OrderException;
|
||||
use Utopia\Database\Exception\Query as QueryException;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\Database\Validator\Authorization\Input;
|
||||
use Utopia\Database\Validator\UID;
|
||||
use Utopia\Platform\Action;
|
||||
|
||||
class Get extends Action
|
||||
{
|
||||
public static function getName(): string
|
||||
{
|
||||
return 'getPresence';
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
->setHttpMethod(self::HTTP_REQUEST_METHOD_GET)
|
||||
// TODO: create a separate scope
|
||||
->groups(['api', 'presence'])
|
||||
->label('scope', 'documents.read')
|
||||
->setHttpPath('/v1/presence:/:presenceId')
|
||||
->param('presenceId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Presence ID.', false, ['dbForProject'])
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('authorization')
|
||||
->inject('user')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
// Since POC so not adding queries or advanced filtering now
|
||||
// just for getting group based presence list based on permissions
|
||||
public function action(string $presenceId, UtopiaResponse $response, Database $dbForProject, Authorization $authorization, Document $user): void
|
||||
{
|
||||
try {
|
||||
// `presenceLogs` are permissioned per-document (via `$permissions`),
|
||||
// but DB-level auth for this metadata collection may be missing.
|
||||
// So we fetch without auth checks, then enforce read permission using the document itself.
|
||||
$document = $authorization->skip(fn () => $dbForProject->getDocument('presenceLogs', $presenceId));
|
||||
} catch (OrderException $e) {
|
||||
throw new Exception(Exception::DATABASE_QUERY_ORDER_NULL, "The order column '{$e->getAttribute()}' had a null value. Cursor pagination requires all rows order column values are non-null.");
|
||||
} catch (QueryException) {
|
||||
throw new Exception(Exception::GENERAL_QUERY_INVALID);
|
||||
}
|
||||
|
||||
Console::info(
|
||||
\sprintf(
|
||||
'[Presence][Get] presenceId=%s requesterUserId=%s requesterUserInternalId=%s requesterRoles=%s',
|
||||
$presenceId,
|
||||
$user->getId(),
|
||||
(string) $user->getSequence(),
|
||||
\json_encode($authorization->getRoles(), JSON_UNESCAPED_SLASHES)
|
||||
)
|
||||
);
|
||||
|
||||
if ($document->isEmpty()) {
|
||||
throw new Exception(Exception::DOCUMENT_NOT_FOUND, 'Presence not found.');
|
||||
}
|
||||
|
||||
// Document permission context
|
||||
$documentUserId = $document->getAttribute('userId');
|
||||
$documentReadRoles = $document->getRead();
|
||||
Console::info(
|
||||
\sprintf(
|
||||
'[Presence][Get] presenceId=%s documentUserId=%s documentAllowedReadRoles=%s documentAllPermissions=%s',
|
||||
$presenceId,
|
||||
(string) $documentUserId,
|
||||
\json_encode($documentReadRoles, JSON_UNESCAPED_SLASHES),
|
||||
\json_encode($document->getPermissions(), JSON_UNESCAPED_SLASHES)
|
||||
)
|
||||
);
|
||||
|
||||
if (
|
||||
!($authorization->isValid(
|
||||
new Input(Database::PERMISSION_READ, $document->getRead())
|
||||
))
|
||||
) {
|
||||
Console::info(
|
||||
\sprintf(
|
||||
'[Presence][Get][DENY] presenceId=%s requesterUserId=%s allowedReadRoles=%s requestedReadRoles=%s validatorDescription=%s',
|
||||
$presenceId,
|
||||
$user->getId(),
|
||||
\json_encode($documentReadRoles, JSON_UNESCAPED_SLASHES),
|
||||
\json_encode($authorization->getRoles(), JSON_UNESCAPED_SLASHES),
|
||||
$authorization->getDescription()
|
||||
)
|
||||
);
|
||||
// Don't leak existence for unauthorized callers.
|
||||
throw new Exception(Exception::DOCUMENT_NOT_FOUND, 'Presence not found.');
|
||||
}
|
||||
|
||||
$response->dynamic($document, UtopiaResponse::MODEL_PRESENCE);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Platform\Modules\Presence\HTTP\Iterative;
|
||||
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Utopia\Response as UtopiaResponse;
|
||||
use Utopia\Console;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Exception\Order as OrderException;
|
||||
use Utopia\Database\Exception\Query as QueryException;
|
||||
use Utopia\Database\Query;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\Database\Validator\Authorization\Input;
|
||||
use Utopia\Platform\Action;
|
||||
|
||||
class XList extends Action
|
||||
{
|
||||
public static function getName(): string
|
||||
{
|
||||
return 'listPresence';
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
->setHttpMethod(self::HTTP_REQUEST_METHOD_GET)
|
||||
// TODO: create a separate scope
|
||||
->groups(['api', 'presence'])
|
||||
->label('scope', 'documents.read')
|
||||
->setHttpPath('/v1/iterative/presence')
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('authorization')
|
||||
->inject('user')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
// Since POC so not adding queries or advanced filtering now
|
||||
// just for getting group based presence list based on permissions
|
||||
public function action(UtopiaResponse $response, Database $dbForProject, Authorization $authorization, Document $user): void
|
||||
{
|
||||
$requestingUserId = $user->getId();
|
||||
$hostname = $dbForProject->getAdapter()->getHostname();
|
||||
$usersCacheKey = \sprintf(
|
||||
'%s-cache-%s:%s:%s:collection:presence',
|
||||
$dbForProject->getCacheName(),
|
||||
$hostname ?? '',
|
||||
$dbForProject->getNamespace(),
|
||||
$dbForProject->getTenant()
|
||||
);
|
||||
$presenceLogsCacheKey = \sprintf(
|
||||
'%s-cache-%s:%s:%s:collection:presenceLogs:user:%s',
|
||||
$dbForProject->getCacheName(),
|
||||
$hostname ?? '',
|
||||
$dbForProject->getNamespace(),
|
||||
$dbForProject->getTenant(),
|
||||
$requestingUserId
|
||||
);
|
||||
$ttl = 60;
|
||||
try {
|
||||
$presenceLogs = [];
|
||||
$totalStart = microtime(true);
|
||||
$userStart = microtime(true);
|
||||
|
||||
$presenceCacheStart = microtime(true);
|
||||
$cachedPresenceLogs = $dbForProject->getCache()->load($presenceLogsCacheKey, $ttl);
|
||||
if($cachedPresenceLogs !== null && $cachedPresenceLogs !== false && \is_array($cachedPresenceLogs)){
|
||||
$presenceLogs = array_map(fn($doc) => new Document($doc), $cachedPresenceLogs);
|
||||
$presenceLogEnd = microtime(true) - $presenceCacheStart;
|
||||
$totalEnd = microtime(true) - $totalStart;
|
||||
Console::info(sprintf(
|
||||
"Cache | Requesting User [%s] | [Total][time] %.2f ms | [Users][time] %.2f ms | [PresenceCache][time] %.2f ms\n",
|
||||
$requestingUserId,
|
||||
$totalEnd * 1000,
|
||||
0,
|
||||
$presenceLogEnd * 1000
|
||||
));
|
||||
|
||||
$response->dynamic(new Document([
|
||||
'presences' => $presenceLogs,
|
||||
'total' => \count($presenceLogs),
|
||||
]), UtopiaResponse::MODEL_PRESENCE_LIST);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$cachedUsers = $dbForProject->getCache()->load($usersCacheKey, $ttl);
|
||||
if($cachedUsers !== null && $cachedUsers !== false && \is_array($cachedUsers)){
|
||||
$users = array_map(fn($doc) => new Document($doc), $cachedUsers);
|
||||
}
|
||||
else{
|
||||
$users = $authorization->skip(fn () => $dbForProject->find('presence', [
|
||||
Query::limit(10000),
|
||||
]));
|
||||
|
||||
// saving to cache
|
||||
$documentsArray = \array_map(function ($doc) {
|
||||
return $doc->getArrayCopy();
|
||||
}, $users);
|
||||
$dbForProject->getCache()->save($usersCacheKey, $documentsArray);
|
||||
}
|
||||
|
||||
$userEnd = microtime(true) - $userStart;
|
||||
$presenceLogStart = microtime(true);
|
||||
foreach ($users as $presenceUser) {
|
||||
// $presenceCacheKey = \sprintf(
|
||||
// '%s-cache-%s:%s:%s:collection:presenceLogs:user:%s',
|
||||
// $dbForProject->getCacheName(),
|
||||
// $hostname ?? '',
|
||||
// $dbForProject->getNamespace(),
|
||||
// $dbForProject->getTenant(),
|
||||
// $presenceUser['userId'] ?? ''
|
||||
// );
|
||||
|
||||
$presenceLog = $dbForProject->findOne('presenceLogs', [
|
||||
Query::equal('userId', [$presenceUser['userId']]),
|
||||
Query::orderDesc('$updatedAt'),
|
||||
// Tie-breaker: `$updatedAt` has only millisecond precision.
|
||||
Query::orderDesc('$id'),
|
||||
Query::limit(1),
|
||||
]);
|
||||
|
||||
$presenceLogs[] = $presenceLog;
|
||||
}
|
||||
$dbForProject->getCache()->save($presenceLogsCacheKey, $presenceLogs);
|
||||
$presenceLogEnd = microtime(true) - $presenceLogStart;
|
||||
$totalEnd = microtime(true) - $totalStart;
|
||||
} catch (OrderException $e) {
|
||||
throw new Exception(Exception::DATABASE_QUERY_ORDER_NULL, "The order column '{$e->getAttribute()}' had a null value. Cursor pagination requires all rows order column values are non-null.");
|
||||
} catch (QueryException) {
|
||||
throw new Exception(Exception::GENERAL_QUERY_INVALID);
|
||||
}
|
||||
Console::info(sprintf(
|
||||
"Requesting User [%s] | [Total][time] %.2f ms | [Users][time] %.2f ms | [PresenceLogs][time] %.2f ms\n",
|
||||
$requestingUserId,
|
||||
$totalEnd * 1000,
|
||||
$userEnd * 1000,
|
||||
$presenceLogEnd * 1000
|
||||
));
|
||||
$response->dynamic(new Document([
|
||||
'presences' => $presenceLogs,
|
||||
'total' => \count($presenceLogs),
|
||||
]), UtopiaResponse::MODEL_PRESENCE_LIST);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Platform\Modules\Presence;
|
||||
|
||||
use Appwrite\Platform\Modules\Presence\Services\Http;
|
||||
use Utopia\Platform;
|
||||
|
||||
class Module extends Platform\Module
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->addService('http', new Http());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Platform\Modules\Presence\Services;
|
||||
|
||||
use Appwrite\Platform\Modules\Presence\HTTP\Create as CreatePresence;
|
||||
use Appwrite\Platform\Modules\Presence\HTTP\Get as GetPresence;
|
||||
use Appwrite\Platform\Modules\Presence\HTTP\Iterative\XList as ListPresence;
|
||||
use Utopia\Platform\Service;
|
||||
|
||||
class Http extends Service
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->type = Service::TYPE_HTTP;
|
||||
$this->addAction(CreatePresence::getName(), new CreatePresence());
|
||||
$this->addAction(ListPresence::getName(), new ListPresence());
|
||||
$this->addAction(GetPresence::getName(), new GetPresence());
|
||||
}
|
||||
}
|
||||
@@ -127,6 +127,10 @@ class Response extends SwooleResponse
|
||||
public const MODEL_JWT = 'jwt';
|
||||
public const MODEL_PREFERENCES = 'preferences';
|
||||
|
||||
// Presence
|
||||
public const MODEL_PRESENCE = 'presence';
|
||||
public const MODEL_PRESENCE_LIST = 'presenceList';
|
||||
|
||||
// MFA
|
||||
public const MODEL_MFA_TYPE = 'mfaType';
|
||||
public const MODEL_MFA_FACTORS = 'mfaFactors';
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Utopia\Response\Model;
|
||||
|
||||
use Appwrite\Utopia\Response;
|
||||
use Appwrite\Utopia\Response\Model;
|
||||
|
||||
class Presence extends Model
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
->addRule('$id', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'Presence ID.',
|
||||
'default' => '',
|
||||
'example' => '67e0c52f001f6d12a345',
|
||||
])
|
||||
->addRule('userId', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'User ID.',
|
||||
'default' => '',
|
||||
'example' => '67e0c3fe002a4b98d210',
|
||||
])
|
||||
->addRule('status', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'Presence status.',
|
||||
'default' => '',
|
||||
'example' => 'online',
|
||||
])
|
||||
->addRule('expiry', [
|
||||
'type' => self::TYPE_DATETIME,
|
||||
'description' => 'Presence expiration date in ISO 8601 format.',
|
||||
'required' => false,
|
||||
'default' => null,
|
||||
'example' => self::TYPE_DATETIME_EXAMPLE,
|
||||
])
|
||||
->addRule('$createdAt', [
|
||||
'type' => self::TYPE_DATETIME,
|
||||
'description' => 'Project creation date in ISO 8601 format.',
|
||||
'default' => '',
|
||||
'example' => self::TYPE_DATETIME_EXAMPLE,
|
||||
])
|
||||
->addRule('$updatedAt', [
|
||||
'type' => self::TYPE_DATETIME,
|
||||
'description' => 'Project update date in ISO 8601 format.',
|
||||
'default' => '',
|
||||
'example' => self::TYPE_DATETIME_EXAMPLE,
|
||||
])
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Name
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getName(): string
|
||||
{
|
||||
return 'Presence';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Type
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getType(): string
|
||||
{
|
||||
return Response::MODEL_PRESENCE;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,232 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\E2E\Services\Realtime;
|
||||
|
||||
use Tests\E2E\Client;
|
||||
use Utopia\Database\Helpers\Permission;
|
||||
use Utopia\Database\Helpers\Role;
|
||||
use WebSocket\Client as WebSocketClient;
|
||||
|
||||
class PresenceIterativeTest extends PresenceBase
|
||||
{
|
||||
protected function getNumberOfUsersToCreate(): int
|
||||
{
|
||||
return 100;
|
||||
}
|
||||
|
||||
protected function getListPresenceURL(): string
|
||||
{
|
||||
return '/iterative/presence';
|
||||
}
|
||||
|
||||
protected function getSeedPresenceDocumentsCount(): int
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
private function extractPresenceRows(array $body): array
|
||||
{
|
||||
foreach (['presences', 'presence', 'documents', 'rows'] as $key) {
|
||||
$value = $body[$key] ?? null;
|
||||
if (\is_array($value)) {
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
private function getPresenceCookieHeaders(string $projectId, array $user): array
|
||||
{
|
||||
return [
|
||||
'content-type' => 'application/json',
|
||||
'origin' => 'http://localhost',
|
||||
'x-appwrite-project' => $projectId,
|
||||
'cookie' => 'a_session_' . $projectId . '=' . $user['session'],
|
||||
];
|
||||
}
|
||||
|
||||
public function testPresenceApiCreateGetAndList(): void
|
||||
{
|
||||
$projectId = $this->getProject()['$id'];
|
||||
|
||||
$owner = $this->getUser(true);
|
||||
$viewer = $this->getUser(true);
|
||||
$other = $this->getUser(true);
|
||||
|
||||
$status = 'api-presence-test-' . \uniqid();
|
||||
$permissions = [Permission::read(Role::user($viewer['$id']))];
|
||||
|
||||
// Create via HTTP
|
||||
$createHeaders = $this->getPresenceCookieHeaders($projectId, $owner);
|
||||
$payload = [
|
||||
'status' => $status,
|
||||
'permissions' => $permissions,
|
||||
];
|
||||
|
||||
$createResponse = $this->client->call(
|
||||
Client::METHOD_POST,
|
||||
'/iterative/presence',
|
||||
$createHeaders,
|
||||
$payload
|
||||
);
|
||||
|
||||
$this->assertEquals(201, $createResponse['headers']['status-code'], 'Create presence failed.');
|
||||
|
||||
$presenceId = $createResponse['body']['$id'] ?? null;
|
||||
$this->assertIsString($presenceId, 'Missing $id in create response body.');
|
||||
$this->assertEquals($status, $createResponse['body']['status'] ?? null);
|
||||
|
||||
// GET presence as the same viewer
|
||||
$getResponse = $this->client->call(
|
||||
Client::METHOD_GET,
|
||||
'/presence:/' . $presenceId,
|
||||
$this->getPresenceCookieHeaders($projectId, $viewer)
|
||||
);
|
||||
|
||||
$this->assertEquals(200, $getResponse['headers']['status-code']);
|
||||
$this->assertEquals($status, $getResponse['body']['status'] ?? null);
|
||||
|
||||
// GET presence as an unrelated user should not succeed
|
||||
$otherGetResponse = $this->client->call(
|
||||
Client::METHOD_GET,
|
||||
'/presence:/' . $presenceId,
|
||||
$this->getPresenceCookieHeaders($projectId, $other)
|
||||
);
|
||||
|
||||
$this->assertNotEquals(200, $otherGetResponse['headers']['status-code']);
|
||||
|
||||
// List presence as viewer should include owner
|
||||
$listResponse = $this->client->call(
|
||||
Client::METHOD_GET,
|
||||
$this->getListPresenceURL(),
|
||||
$this->getPresenceCookieHeaders($projectId, $viewer)
|
||||
);
|
||||
$this->assertEquals(200, $listResponse['headers']['status-code']);
|
||||
|
||||
$rows = $this->extractPresenceRows($listResponse['body']);
|
||||
$found = false;
|
||||
foreach ($rows as $row) {
|
||||
if (($row['userId'] ?? null) === $owner['$id']) {
|
||||
$found = true;
|
||||
$this->assertEquals($status, $row['status'] ?? null);
|
||||
}
|
||||
}
|
||||
|
||||
$this->assertTrue($found, 'Viewer should see owner presence in list.');
|
||||
|
||||
// List presence as other should not include owner
|
||||
$otherListResponse = $this->client->call(
|
||||
Client::METHOD_GET,
|
||||
$this->getListPresenceURL(),
|
||||
$this->getPresenceCookieHeaders($projectId, $other)
|
||||
);
|
||||
$this->assertEquals(200, $otherListResponse['headers']['status-code']);
|
||||
|
||||
$otherRows = $this->extractPresenceRows($otherListResponse['body']);
|
||||
$otherFound = false;
|
||||
foreach ($otherRows as $row) {
|
||||
if (($row['userId'] ?? null) === $owner['$id']) {
|
||||
$otherFound = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$this->assertFalse($otherFound, 'Other user should not see owner presence in list.');
|
||||
}
|
||||
|
||||
public function testPresenceRealtimeCreateGetAndList(): void
|
||||
{
|
||||
$projectId = $this->getProject()['$id'];
|
||||
|
||||
$owner = $this->getUser(true);
|
||||
$viewer = $this->getUser(true);
|
||||
$other = $this->getUser(true);
|
||||
|
||||
$status = 'realtime-presence-test-' . \uniqid();
|
||||
$permissions = [Permission::read(Role::user($viewer['$id']))];
|
||||
|
||||
$channels = ['account'];
|
||||
$queryString = http_build_query(['project' => $projectId, 'channels' => $channels]);
|
||||
|
||||
$wsClient = new WebSocketClient(
|
||||
'ws://appwrite.test/v1/realtime?' . $queryString,
|
||||
[
|
||||
'headers' => [
|
||||
'origin' => 'http://localhost',
|
||||
'cookie' => 'a_session_' . $projectId . '=' . $owner['session'],
|
||||
],
|
||||
'timeout' => 2,
|
||||
]
|
||||
);
|
||||
|
||||
// connected payload
|
||||
$connectedPayloadRaw = $wsClient->receive();
|
||||
$connectedPayload = \json_decode((string) $connectedPayloadRaw, true);
|
||||
$this->assertIsArray($connectedPayload, 'Expected websocket connected payload JSON.');
|
||||
$this->assertEquals('connected', $connectedPayload['type'] ?? null);
|
||||
|
||||
// Send presence
|
||||
$wsClient->send(\json_encode([
|
||||
'type' => 'presence',
|
||||
'data' => [
|
||||
'session' => $owner['session'],
|
||||
'status' => $status,
|
||||
'permissions' => $permissions,
|
||||
],
|
||||
]));
|
||||
|
||||
// presence ack payload
|
||||
$presenceAckRaw = $wsClient->receive();
|
||||
$presenceAck = \json_decode((string) $presenceAckRaw, true);
|
||||
$this->assertIsArray($presenceAck, 'Expected websocket presence ack JSON.');
|
||||
$this->assertEquals('presence', $presenceAck['type'] ?? null);
|
||||
|
||||
$wsClient->close();
|
||||
|
||||
// Allow realtime write to settle
|
||||
\usleep(100000);
|
||||
|
||||
// List presence as viewer should include owner
|
||||
$listResponse = $this->client->call(
|
||||
Client::METHOD_GET,
|
||||
$this->getListPresenceURL(),
|
||||
$this->getPresenceCookieHeaders($projectId, $viewer)
|
||||
);
|
||||
$this->assertEquals(200, $listResponse['headers']['status-code']);
|
||||
|
||||
$rows = $this->extractPresenceRows($listResponse['body']);
|
||||
$ownerRow = null;
|
||||
foreach ($rows as $row) {
|
||||
if (($row['userId'] ?? null) === $owner['$id']) {
|
||||
$ownerRow = $row;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$this->assertIsArray($ownerRow, 'Expected viewer to see owner presence in list.');
|
||||
$this->assertEquals($status, $ownerRow['status'] ?? null);
|
||||
|
||||
// GET presence as the same viewer
|
||||
$presenceId = $ownerRow['$id'] ?? $ownerRow['id'] ?? null;
|
||||
$this->assertIsString($presenceId, 'Missing presence id in list row.');
|
||||
|
||||
$getResponse = $this->client->call(
|
||||
Client::METHOD_GET,
|
||||
'/presence:/' . $presenceId,
|
||||
$this->getPresenceCookieHeaders($projectId, $viewer)
|
||||
);
|
||||
|
||||
$this->assertEquals(200, $getResponse['headers']['status-code']);
|
||||
$this->assertEquals($status, $getResponse['body']['status'] ?? null);
|
||||
|
||||
// GET presence as an unrelated user should not succeed
|
||||
$otherGetResponse = $this->client->call(
|
||||
Client::METHOD_GET,
|
||||
'/presence:/' . $presenceId,
|
||||
$this->getPresenceCookieHeaders($projectId, $other)
|
||||
);
|
||||
|
||||
$this->assertNotEquals(200, $otherGetResponse['headers']['status-code']);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user