Merge pull request #948 from appwrite/feat-265-realtime
Feat 265 - Realtime Implementation
This commit is contained in:
commit
afc1622fc2
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -2,6 +2,7 @@
|
|||
/vendor/
|
||||
/node_modules/
|
||||
/tests/resources/storage/
|
||||
/app/sdks/*
|
||||
/.idea/
|
||||
.DS_Store
|
||||
.php_cs.cache
|
||||
|
|
|
@ -167,6 +167,8 @@ Appwrite's current structure is a combination of both [Monolithic](https://en.wi
|
|||
│ ├── Migration
|
||||
│ ├── Network
|
||||
│ ├── OpenSSL
|
||||
│ ├── Realtime
|
||||
│ ├── Resque
|
||||
│ ├── Specification
|
||||
│ ├── Task
|
||||
│ ├── Template
|
||||
|
|
|
@ -33,6 +33,7 @@ RUN \
|
|||
git \
|
||||
zlib-dev \
|
||||
brotli-dev \
|
||||
openssl-dev \
|
||||
yaml-dev \
|
||||
imagemagick \
|
||||
imagemagick-dev \
|
||||
|
@ -52,7 +53,7 @@ RUN \
|
|||
git clone --depth 1 --branch $PHP_SWOOLE_VERSION https://github.com/swoole/swoole-src.git && \
|
||||
cd swoole-src && \
|
||||
phpize && \
|
||||
./configure --enable-http2 && \
|
||||
./configure --enable-sockets --enable-http2 --enable-openssl && \
|
||||
make && make install && \
|
||||
cd .. && \
|
||||
## Imagick Extension
|
||||
|
@ -217,6 +218,7 @@ RUN chmod +x /usr/local/bin/doctor && \
|
|||
chmod +x /usr/local/bin/maintenance && \
|
||||
chmod +x /usr/local/bin/install && \
|
||||
chmod +x /usr/local/bin/migrate && \
|
||||
chmod +x /usr/local/bin/realtime && \
|
||||
chmod +x /usr/local/bin/schedule && \
|
||||
chmod +x /usr/local/bin/sdks && \
|
||||
chmod +x /usr/local/bin/ssl && \
|
||||
|
@ -246,6 +248,7 @@ RUN if [ "$DEBUG" == "true" ]; then echo "opcache.enable=0" >> /usr/local/etc/ph
|
|||
RUN echo "opcache.preload_user=www-data" >> /usr/local/etc/php/conf.d/appwrite.ini
|
||||
RUN echo "opcache.preload=/usr/src/code/app/preload.php" >> /usr/local/etc/php/conf.d/appwrite.ini
|
||||
RUN echo "opcache.enable_cli=1" >> /usr/local/etc/php/conf.d/appwrite.ini
|
||||
RUN echo "default_socket_timeout=-1" >> /usr/local/etc/php/conf.d/appwrite.ini
|
||||
RUN echo "opcache.jit_buffer_size=100M" >> /usr/local/etc/php/conf.d/appwrite.ini
|
||||
RUN echo "opcache.jit=1235" >> /usr/local/etc/php/conf.d/appwrite.ini
|
||||
|
||||
|
|
|
@ -1706,6 +1706,39 @@ $collections = [
|
|||
],
|
||||
],
|
||||
],
|
||||
Database::SYSTEM_COLLECTION_CONNECTIONS => [
|
||||
'$collection' => Database::SYSTEM_COLLECTION_COLLECTIONS,
|
||||
'$id' => Database::SYSTEM_COLLECTION_CONNECTIONS,
|
||||
'$permissions' => ['read' => ['*']],
|
||||
'name' => 'Realtime Connections',
|
||||
'structure' => true,
|
||||
'rules' => [
|
||||
[
|
||||
'$collection' => Database::SYSTEM_COLLECTION_CONNECTIONS,
|
||||
'label' => 'Container',
|
||||
'key' => 'container',
|
||||
'type' => Database::SYSTEM_VAR_TYPE_TEXT,
|
||||
'required' => true,
|
||||
'array' => false,
|
||||
],
|
||||
[
|
||||
'$collection' => Database::SYSTEM_COLLECTION_CONNECTIONS,
|
||||
'label' => 'Timestamp',
|
||||
'key' => 'timestamp',
|
||||
'type' => Database::SYSTEM_VAR_TYPE_NUMERIC,
|
||||
'required' => true,
|
||||
'array' => false,
|
||||
],
|
||||
[
|
||||
'$collection' => Database::SYSTEM_COLLECTION_CONNECTIONS,
|
||||
'label' => 'Value',
|
||||
'key' => 'value',
|
||||
'type' => Database::SYSTEM_VAR_TYPE_TEXT,
|
||||
'required' => true,
|
||||
'array' => false,
|
||||
],
|
||||
],
|
||||
],
|
||||
Database::SYSTEM_COLLECTION_RESERVED => [
|
||||
'$collection' => Database::SYSTEM_COLLECTION_COLLECTIONS,
|
||||
'$id' => Database::SYSTEM_COLLECTION_RESERVED,
|
||||
|
|
|
@ -222,6 +222,8 @@ App::get('/v1/projects/:projectId/usage')
|
|||
$requests = [];
|
||||
$network = [];
|
||||
$functions = [];
|
||||
$realtimeConnections = [];
|
||||
$realtimeMessages = [];
|
||||
|
||||
if ($client) {
|
||||
$start = $period[$range]['start']->format(DateTime::RFC3339);
|
||||
|
@ -260,11 +262,34 @@ App::get('/v1/projects/:projectId/usage')
|
|||
'date' => \strtotime($point['time']),
|
||||
];
|
||||
}
|
||||
|
||||
// Realtime Connections
|
||||
$result = $database->query('SELECT sum(value) AS "value" FROM "appwrite_usage_realtime_clients" WHERE time > \''.$start.'\' AND time < \''.$end.'\' AND "metric_type"=\'counter\' AND "project"=\''.$project->getId().'\' GROUP BY time('.$period[$range]['group'].') FILL(null)');
|
||||
$points = $result->getPoints();
|
||||
|
||||
foreach ($points as $point) {
|
||||
$realtimeConnections[] = [
|
||||
'value' => (!empty($point['value'])) ? $point['value'] : 0,
|
||||
'date' => \strtotime($point['time']),
|
||||
];
|
||||
}
|
||||
// Realtime Messages
|
||||
$result = $database->query('SELECT sum(value) AS "value" FROM "appwrite_usage_realtime_messages" WHERE time > \''.$start.'\' AND time < \''.$end.'\' AND "metric_type"=\'counter\' AND "project"=\''.$project->getId().'\' GROUP BY time('.$period[$range]['group'].') FILL(null)');
|
||||
$points = $result->getPoints();
|
||||
|
||||
foreach ($points as $point) {
|
||||
$realtimeMessages[] = [
|
||||
'value' => (!empty($point['value'])) ? $point['value'] : 0,
|
||||
'date' => \strtotime($point['time']),
|
||||
];
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$requests = [];
|
||||
$network = [];
|
||||
$functions = [];
|
||||
$realtimeConnections = [];
|
||||
$realtimeMessages = [];
|
||||
}
|
||||
|
||||
|
||||
|
@ -327,6 +352,18 @@ App::get('/v1/projects/:projectId/usage')
|
|||
return $item['value'];
|
||||
}, $functions)),
|
||||
],
|
||||
'realtimeConnections' => [
|
||||
'data' => $realtimeConnections,
|
||||
'total' => \array_sum(\array_map(function ($item) {
|
||||
return $item['value'];
|
||||
}, $realtimeConnections)),
|
||||
],
|
||||
'realtimeMessages' => [
|
||||
'data' => $realtimeMessages,
|
||||
'total' => \array_sum(\array_map(function ($item) {
|
||||
return $item['value'];
|
||||
}, $realtimeMessages)),
|
||||
],
|
||||
'collections' => [
|
||||
'data' => $collections,
|
||||
'total' => $collectionsTotal,
|
||||
|
|
|
@ -37,10 +37,12 @@ App::post('/v1/teams')
|
|||
->inject('response')
|
||||
->inject('user')
|
||||
->inject('projectDB')
|
||||
->action(function ($name, $roles, $response, $user, $projectDB) {
|
||||
->inject('events')
|
||||
->action(function ($name, $roles, $response, $user, $projectDB, $events) {
|
||||
/** @var Appwrite\Utopia\Response $response */
|
||||
/** @var Appwrite\Database\Document $user */
|
||||
/** @var Appwrite\Database\Database $projectDB */
|
||||
/** @var Appwrite\Event\Event $events */
|
||||
|
||||
Authorization::disable();
|
||||
|
||||
|
@ -90,6 +92,10 @@ App::post('/v1/teams')
|
|||
}
|
||||
}
|
||||
|
||||
if (!empty($user->getId())) {
|
||||
$events->setParam('userId', $user->getId());
|
||||
}
|
||||
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_CREATED)
|
||||
->dynamic($team, Response::MODEL_TEAM)
|
||||
|
|
|
@ -240,26 +240,15 @@ App::init(function ($utopia, $request, $response, $console, $project, $consoleDB
|
|||
$role = Auth::USER_ROLE_APP;
|
||||
$scopes = \array_merge($roles[$role]['scopes'], $key->getAttribute('scopes', []));
|
||||
|
||||
Authorization::setRole('role:'.Auth::USER_ROLE_APP);
|
||||
Authorization::setDefaultStatus(false); // Cancel security segmentation for API keys.
|
||||
}
|
||||
}
|
||||
|
||||
if ($user->getId()) {
|
||||
Authorization::setRole('user:'.$user->getId());
|
||||
foreach (Auth::getRoles($user) as $authRole) {
|
||||
Authorization::setRole($authRole);
|
||||
}
|
||||
|
||||
Authorization::setRole('role:'.$role);
|
||||
|
||||
\array_map(function ($node) {
|
||||
if (isset($node['teamId']) && isset($node['roles'])) {
|
||||
Authorization::setRole('team:'.$node['teamId']);
|
||||
|
||||
foreach ($node['roles'] as $nodeRole) { // Set all team roles
|
||||
Authorization::setRole('team:'.$node['teamId'].'/'.$nodeRole);
|
||||
}
|
||||
}
|
||||
}, $user->getAttribute('memberships', []));
|
||||
|
||||
// TDOO Check if user is root
|
||||
|
||||
if (!\in_array($scope, $scopes)) {
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
<?php
|
||||
|
||||
use Appwrite\Auth\Auth;
|
||||
use Appwrite\Database\Document;
|
||||
use Appwrite\Database\Validator\Authorization;
|
||||
use Appwrite\Messaging\Adapter\Realtime;
|
||||
use Utopia\App;
|
||||
use Utopia\Exception;
|
||||
use Utopia\Abuse\Abuse;
|
||||
|
@ -21,6 +23,7 @@ App::init(function ($utopia, $request, $response, $project, $user, $register, $e
|
|||
/** @var Appwrite\Event\Event $usage */
|
||||
/** @var Appwrite\Event\Event $deletes */
|
||||
/** @var Appwrite\Event\Event $functions */
|
||||
/** @var PDO $db */
|
||||
|
||||
Storage::setDevice('files', new Local(APP_STORAGE_UPLOADS.'/app-'.$project->getId()));
|
||||
Storage::setDevice('functions', new Local(APP_STORAGE_FUNCTIONS.'/app-'.$project->getId()));
|
||||
|
@ -111,7 +114,6 @@ App::init(function ($utopia, $request, $response, $project, $user, $register, $e
|
|||
|
||||
}, ['utopia', 'request', 'response', 'project', 'user', 'register', 'events', 'audits', 'usage', 'deletes', 'db'], 'api');
|
||||
|
||||
|
||||
App::init(function ($utopia, $request, $response, $project, $user) {
|
||||
/** @var Utopia\App $utopia */
|
||||
/** @var Utopia\Swoole\Request $request */
|
||||
|
@ -181,7 +183,6 @@ App::shutdown(function ($utopia, $request, $response, $project, $events, $audits
|
|||
/** @var Appwrite\Event\Event $audits */
|
||||
/** @var Appwrite\Event\Event $usage */
|
||||
/** @var Appwrite\Event\Event $deletes */
|
||||
/** @var Appwrite\Event\Event $functions */
|
||||
/** @var bool $mode */
|
||||
|
||||
if (!empty($events->getParam('event'))) {
|
||||
|
@ -201,6 +202,23 @@ App::shutdown(function ($utopia, $request, $response, $project, $events, $audits
|
|||
->setQueue('v1-functions')
|
||||
->setClass('FunctionsV1')
|
||||
->trigger();
|
||||
|
||||
if ($project->getId() !== 'console') {
|
||||
$payload = new Document($response->getPayload());
|
||||
$target = Realtime::fromPayload($events->getParam('event'), $payload);
|
||||
|
||||
Realtime::send(
|
||||
$project->getId(),
|
||||
$response->getPayload(),
|
||||
$events->getParam('event'),
|
||||
$target['channels'],
|
||||
$target['roles'],
|
||||
[
|
||||
'permissionsChanged' => $target['permissionsChanged'],
|
||||
'userId' => $events->getParam('userId')
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($audits->getParam('event'))) {
|
||||
|
|
|
@ -26,6 +26,7 @@ use Appwrite\Database\Adapter\Redis as RedisAdapter;
|
|||
use Appwrite\Database\Document;
|
||||
use Appwrite\Database\Validator\Authorization;
|
||||
use Appwrite\Event\Event;
|
||||
use Appwrite\Event\Realtime;
|
||||
use Appwrite\OpenSSL\OpenSSL;
|
||||
use Utopia\App;
|
||||
use Utopia\View;
|
||||
|
@ -70,6 +71,7 @@ const DELETE_TYPE_EXECUTIONS = 'executions';
|
|||
const DELETE_TYPE_AUDIT = 'audit';
|
||||
const DELETE_TYPE_ABUSE = 'abuse';
|
||||
const DELETE_TYPE_CERTIFICATES = 'certificates';
|
||||
const DELETE_TYPE_REALTIME = 'realtime';
|
||||
// Mail Types
|
||||
const MAIL_TYPE_VERIFICATION = 'verification';
|
||||
const MAIL_TYPE_MAGIC_SESSION = 'magicSession';
|
||||
|
@ -421,10 +423,10 @@ App::setResource('user', function($mode, $project, $console, $request, $response
|
|||
$request->getCookie(Auth::$cookieName.'_legacy', '')));// Get fallback session from old clients (no SameSite support)
|
||||
|
||||
// Get fallback session from clients who block 3rd-party cookies
|
||||
$response->addHeader('X-Debug-Fallback', 'false');
|
||||
if($response) $response->addHeader('X-Debug-Fallback', 'false');
|
||||
|
||||
if(empty($session['id']) && empty($session['secret'])) {
|
||||
$response->addHeader('X-Debug-Fallback', 'true');
|
||||
if($response) $response->addHeader('X-Debug-Fallback', 'true');
|
||||
$fallback = $request->getHeader('x-fallback-cookies', '');
|
||||
$fallback = \json_decode($fallback, true);
|
||||
$session = Auth::decodeSession(((isset($fallback[Auth::$cookieName])) ? $fallback[Auth::$cookieName] : ''));
|
||||
|
@ -435,8 +437,7 @@ App::setResource('user', function($mode, $project, $console, $request, $response
|
|||
|
||||
if (APP_MODE_ADMIN !== $mode) {
|
||||
$user = $projectDB->getDocument(Auth::$unique);
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
$user = $consoleDB->getDocument(Auth::$unique);
|
||||
|
||||
$user
|
||||
|
|
|
@ -32,6 +32,7 @@ foreach ([
|
|||
realpath(__DIR__ . '/../vendor/psr/log'),
|
||||
realpath(__DIR__ . '/../vendor/matomo'),
|
||||
realpath(__DIR__ . '/../vendor/symfony'),
|
||||
realpath(__DIR__ . '/../vendor/utopia-php/websocket'), // TODO: remove workerman autoload
|
||||
] as $key => $value) {
|
||||
if($value !== false) {
|
||||
$preloader->ignore($value);
|
||||
|
|
581
app/realtime.php
Normal file
581
app/realtime.php
Normal file
|
@ -0,0 +1,581 @@
|
|||
<?php
|
||||
|
||||
use Appwrite\Auth\Auth;
|
||||
use Appwrite\Database\Adapter\Redis as RedisAdapter;
|
||||
use Appwrite\Database\Adapter\MySQL as MySQLAdapter;
|
||||
use Appwrite\Database\Database;
|
||||
use Appwrite\Database\Validator\Authorization;
|
||||
use Appwrite\Event\Event;
|
||||
use Appwrite\Messaging\Adapter\Realtime;
|
||||
use Appwrite\Network\Validator\Origin;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Swoole\Http\Request as SwooleRequest;
|
||||
use Swoole\Http\Response as SwooleResponse;
|
||||
use Swoole\Runtime;
|
||||
use Swoole\Table;
|
||||
use Swoole\Timer;
|
||||
use Utopia\Abuse\Abuse;
|
||||
use Utopia\Abuse\Adapters\TimeLimit;
|
||||
use Utopia\App;
|
||||
use Utopia\CLI\Console;
|
||||
use Utopia\Config\Config;
|
||||
use Utopia\Swoole\Request;
|
||||
use Utopia\WebSocket\Server;
|
||||
use Utopia\WebSocket\Adapter;
|
||||
|
||||
require_once __DIR__ . '/init.php';
|
||||
|
||||
Runtime::enableCoroutine(SWOOLE_HOOK_ALL);
|
||||
|
||||
$realtime = new Realtime();
|
||||
|
||||
/**
|
||||
* Table for statistics across all workers.
|
||||
*/
|
||||
$stats = new Table(4096, 1);
|
||||
$stats->column('projectId', Table::TYPE_STRING, 64);
|
||||
$stats->column('teamId', Table::TYPE_STRING, 64);
|
||||
$stats->column('connections', Table::TYPE_INT);
|
||||
$stats->column('connectionsTotal', Table::TYPE_INT);
|
||||
$stats->column('messages', Table::TYPE_INT);
|
||||
$stats->create();
|
||||
|
||||
$containerId = uniqid();
|
||||
$documentId = null;
|
||||
|
||||
$adapter = new Adapter\Swoole(port: App::getEnv('PORT', 80));
|
||||
$adapter->setPackageMaxLength(64000); // Default maximum Package Size (64kb)
|
||||
|
||||
$server = new Server($adapter);
|
||||
|
||||
$server->onStart(function () use ($stats, $register, $containerId, &$documentId) {
|
||||
Console::success('Server started succefully');
|
||||
|
||||
$getConsoleDb = function () use ($register) {
|
||||
$db = $register->get('dbPool')->get();
|
||||
$cache = $register->get('redisPool')->get();
|
||||
|
||||
$consoleDb = new Database();
|
||||
$consoleDb->setAdapter(new RedisAdapter(new MySQLAdapter($db, $cache), $cache));
|
||||
$consoleDb->setNamespace('app_console');
|
||||
$consoleDb->setMocks(Config::getParam('collections', []));
|
||||
|
||||
return [
|
||||
$consoleDb,
|
||||
function () use ($register, $db, $cache) {
|
||||
$register->get('dbPool')->put($db);
|
||||
$register->get('redisPool')->put($cache);
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
/**
|
||||
* Create document for this worker to share stats across Containers.
|
||||
*/
|
||||
go(function () use ($getConsoleDb, $containerId, &$documentId) {
|
||||
try {
|
||||
[$consoleDb, $returnConsoleDb] = call_user_func($getConsoleDb);
|
||||
$document = [
|
||||
'$collection' => Database::SYSTEM_COLLECTION_CONNECTIONS,
|
||||
'$permissions' => [
|
||||
'read' => ['*'],
|
||||
'write' => ['*'],
|
||||
],
|
||||
'container' => $containerId,
|
||||
'timestamp' => time(),
|
||||
'value' => '{}'
|
||||
];
|
||||
Authorization::disable();
|
||||
$document = $consoleDb->createDocument($document);
|
||||
Authorization::enable();
|
||||
$documentId = $document->getId();
|
||||
} catch (\Throwable $th) {
|
||||
Console::error('[Error] Type: ' . get_class($th));
|
||||
Console::error('[Error] Message: ' . $th->getMessage());
|
||||
Console::error('[Error] File: ' . $th->getFile());
|
||||
Console::error('[Error] Line: ' . $th->getLine());
|
||||
} finally {
|
||||
call_user_func($returnConsoleDb);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Save current connections to the Database every 5 seconds.
|
||||
*/
|
||||
Timer::tick(5000, function () use ($stats, $getConsoleDb, $containerId, &$documentId) {
|
||||
[$consoleDb, $returnConsoleDb] = call_user_func($getConsoleDb);
|
||||
|
||||
foreach ($stats as $projectId => $value) {
|
||||
if (empty($value['connections']) && empty($value['messages'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$connections = $value['connections'];
|
||||
$messages = $value['messages'];
|
||||
|
||||
$usage = new Event('v1-usage', 'UsageV1');
|
||||
$usage
|
||||
->setParam('projectId', $projectId)
|
||||
->setParam('realtimeConnections', $connections)
|
||||
->setParam('realtimeMessages', $messages)
|
||||
->setParam('networkRequestSize', 0)
|
||||
->setParam('networkResponseSize', 0);
|
||||
|
||||
$stats->set($projectId, [
|
||||
'messages' => 0,
|
||||
'connections' => 0
|
||||
]);
|
||||
|
||||
if (App::getEnv('_APP_USAGE_STATS', 'enabled') == 'enabled') {
|
||||
$usage->trigger();
|
||||
}
|
||||
}
|
||||
$payload = [];
|
||||
foreach ($stats as $projectId => $value) {
|
||||
if (!empty($value['connectionsTotal'])) {
|
||||
$payload[$projectId] = $value['connectionsTotal'];
|
||||
}
|
||||
}
|
||||
if (empty($payload)) {
|
||||
return;
|
||||
}
|
||||
$document = [
|
||||
'$id' => $documentId,
|
||||
'$collection' => Database::SYSTEM_COLLECTION_CONNECTIONS,
|
||||
'$permissions' => [
|
||||
'read' => ['*'],
|
||||
'write' => ['*'],
|
||||
],
|
||||
'container' => $containerId,
|
||||
'timestamp' => time(),
|
||||
'value' => json_encode($payload)
|
||||
];
|
||||
try {
|
||||
$document = $consoleDb->updateDocument($document);
|
||||
} catch (\Throwable $th) {
|
||||
Console::error('[Error] Type: ' . get_class($th));
|
||||
Console::error('[Error] Message: ' . $th->getMessage());
|
||||
Console::error('[Error] File: ' . $th->getFile());
|
||||
Console::error('[Error] Line: ' . $th->getLine());
|
||||
} finally {
|
||||
call_user_func($returnConsoleDb);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$server->onWorkerStart(function (int $workerId) use ($server, $register, $stats, $realtime) {
|
||||
Console::success('Worker ' . $workerId . ' started succefully');
|
||||
|
||||
$attempts = 0;
|
||||
$start = time();
|
||||
|
||||
Timer::tick(5000, function () use ($server, $register, $realtime, $stats) {
|
||||
/**
|
||||
* Sending current connections to project channels on the console project every 5 seconds.
|
||||
*/
|
||||
if ($realtime->hasSubscriber('console', 'role:member', 'project')) {
|
||||
$db = $register->get('dbPool')->get();
|
||||
$cache = $register->get('redisPool')->get();
|
||||
|
||||
$consoleDb = new Database();
|
||||
$consoleDb->setAdapter(new RedisAdapter(new MySQLAdapter($db, $cache), $cache));
|
||||
$consoleDb->setNamespace('app_console');
|
||||
$consoleDb->setMocks(Config::getParam('collections', []));
|
||||
|
||||
$payload = [];
|
||||
$list = $consoleDb->getCollection([
|
||||
'filters' => [
|
||||
'$collection=' . Database::SYSTEM_COLLECTION_CONNECTIONS,
|
||||
'timestamp>' . (time() - 15)
|
||||
],
|
||||
]);
|
||||
|
||||
/**
|
||||
* Aggregate stats across containers.
|
||||
*/
|
||||
foreach ($list as $document) {
|
||||
foreach (json_decode($document->getAttribute('value')) as $projectId => $value) {
|
||||
if (array_key_exists($projectId, $payload)) {
|
||||
$payload[$projectId] += $value;
|
||||
} else {
|
||||
$payload[$projectId] = $value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($stats as $projectId => $value) {
|
||||
if (!array_key_exists($projectId, $payload)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$event = [
|
||||
'project' => 'console',
|
||||
'roles' => ['team:' . $value['teamId']],
|
||||
'data' => [
|
||||
'event' => 'stats.connections',
|
||||
'channels' => ['project'],
|
||||
'timestamp' => time(),
|
||||
'payload' => [
|
||||
$projectId => $payload[$projectId]
|
||||
]
|
||||
]
|
||||
];
|
||||
|
||||
$server->send($realtime->getSubscribers($event), json_encode([
|
||||
'type' => 'event',
|
||||
'data' => $event['data']
|
||||
]));
|
||||
}
|
||||
|
||||
$register->get('dbPool')->put($db);
|
||||
$register->get('redisPool')->put($cache);
|
||||
}
|
||||
/**
|
||||
* Sending test message for SDK E2E tests every 5 seconds.
|
||||
*/
|
||||
if ($realtime->hasSubscriber('console', 'role:guest', 'tests')) {
|
||||
$payload = ['response' => 'WS:/v1/realtime:passed'];
|
||||
|
||||
$event = [
|
||||
'project' => 'console',
|
||||
'roles' => ['role:guest'],
|
||||
'data' => [
|
||||
'event' => 'test.event',
|
||||
'channels' => ['tests'],
|
||||
'timestamp' => time(),
|
||||
'payload' => $payload
|
||||
]
|
||||
];
|
||||
|
||||
$server->send($realtime->getSubscribers($event), json_encode([
|
||||
'type' => 'event',
|
||||
'data' => $event['data']
|
||||
]));
|
||||
}
|
||||
});
|
||||
|
||||
while ($attempts < 300) {
|
||||
try {
|
||||
if ($attempts > 0) {
|
||||
Console::error('Pub/sub connection lost (lasted ' . (time() - $start) . ' seconds, worker: ' . $workerId . ').
|
||||
Attempting restart in 5 seconds (attempt #' . $attempts . ')');
|
||||
sleep(5); // 5 sec delay between connection attempts
|
||||
}
|
||||
$start = time();
|
||||
|
||||
/** @var Redis $redis */
|
||||
$redis = $register->get('redisPool')->get();
|
||||
$redis->setOption(Redis::OPT_READ_TIMEOUT, -1);
|
||||
|
||||
if ($redis->ping(true)) {
|
||||
$attempts = 0;
|
||||
Console::success('Pub/sub connection established (worker: ' . $workerId . ')');
|
||||
} else {
|
||||
Console::error('Pub/sub failed (worker: ' . $workerId . ')');
|
||||
}
|
||||
|
||||
$redis->subscribe(['realtime'], function (Redis $redis, string $channel, string $payload) use ($server, $workerId, $stats, $register, $realtime) {
|
||||
$event = json_decode($payload, true);
|
||||
|
||||
if ($event['permissionsChanged'] && isset($event['userId'])) {
|
||||
$projectId = $event['project'];
|
||||
$userId = $event['userId'];
|
||||
|
||||
if ($realtime->hasSubscriber($projectId, 'user:' . $userId)) {
|
||||
$connection = array_key_first(reset($realtime->subscriptions[$projectId]['user:' . $userId]));
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
$db = $register->get('dbPool')->get();
|
||||
$cache = $register->get('redisPool')->get();
|
||||
|
||||
$projectDB = new Database();
|
||||
$projectDB->setAdapter(new RedisAdapter(new MySQLAdapter($db, $cache), $cache));
|
||||
$projectDB->setNamespace('app_' . $projectId);
|
||||
$projectDB->setMocks(Config::getParam('collections', []));
|
||||
|
||||
$user = $projectDB->getDocument($userId);
|
||||
|
||||
$roles = Auth::getRoles($user);
|
||||
|
||||
$realtime->subscribe($projectId, $connection, $roles, $realtime->connections[$connection]['channels']);
|
||||
|
||||
$register->get('dbPool')->put($db);
|
||||
$register->get('redisPool')->put($cache);
|
||||
}
|
||||
|
||||
$receivers = $realtime->getSubscribers($event);
|
||||
|
||||
if (App::isDevelopment() && !empty($receivers)) {
|
||||
Console::log("[Debug][Worker {$workerId}] Receivers: " . count($receivers));
|
||||
Console::log("[Debug][Worker {$workerId}] Receivers Connection IDs: " . json_encode($receivers));
|
||||
Console::log("[Debug][Worker {$workerId}] Event: " . $payload);
|
||||
}
|
||||
|
||||
$server->send(
|
||||
$receivers,
|
||||
json_encode([
|
||||
'type' => 'event',
|
||||
'data' => $event['data']
|
||||
])
|
||||
);
|
||||
|
||||
if (($num = count($receivers)) > 0) {
|
||||
$stats->incr($event['project'], 'messages', $num);
|
||||
}
|
||||
});
|
||||
} catch (\Throwable $th) {
|
||||
Console::error('Pub/sub error: ' . $th->getMessage());
|
||||
$register->get('redisPool')->put($redis);
|
||||
$attempts++;
|
||||
continue;
|
||||
}
|
||||
|
||||
$attempts++;
|
||||
}
|
||||
|
||||
Console::error('Failed to restart pub/sub...');
|
||||
});
|
||||
|
||||
$server->onOpen(function (int $connection, SwooleRequest $request) use ($server, $register, $stats, &$realtime) {
|
||||
$app = new App('UTC');
|
||||
$request = new Request($request);
|
||||
$response = new Response(new SwooleResponse());
|
||||
|
||||
/** @var PDO $db */
|
||||
$db = $register->get('dbPool')->get();
|
||||
/** @var Redis $redis */
|
||||
$redis = $register->get('redisPool')->get();
|
||||
|
||||
Console::info("Connection open (user: {$connection})");
|
||||
|
||||
App::setResource('db', function () use (&$db) {
|
||||
return $db;
|
||||
});
|
||||
|
||||
App::setResource('cache', function () use (&$redis) {
|
||||
return $redis;
|
||||
});
|
||||
|
||||
App::setResource('request', function () use ($request) {
|
||||
return $request;
|
||||
});
|
||||
|
||||
App::setResource('response', function () use ($response) {
|
||||
return $response;
|
||||
});
|
||||
|
||||
try {
|
||||
/** @var \Appwrite\Database\Document $user */
|
||||
$user = $app->getResource('user');
|
||||
|
||||
/** @var \Appwrite\Database\Document $project */
|
||||
$project = $app->getResource('project');
|
||||
|
||||
/** @var \Appwrite\Database\Document $console */
|
||||
$console = $app->getResource('console');
|
||||
|
||||
/*
|
||||
* Project Check
|
||||
*/
|
||||
if (empty($project->getId())) {
|
||||
throw new Exception('Missing or unknown project ID', 1008);
|
||||
}
|
||||
|
||||
/*
|
||||
* Abuse Check
|
||||
*
|
||||
* Abuse limits are connecting 128 times per minute and ip address.
|
||||
*/
|
||||
$timeLimit = new TimeLimit('url:{url},ip:{ip}', 128, 60, $db);
|
||||
$timeLimit
|
||||
->setNamespace('app_' . $project->getId())
|
||||
->setParam('{ip}', $request->getIP())
|
||||
->setParam('{url}', $request->getURI());
|
||||
|
||||
$abuse = new Abuse($timeLimit);
|
||||
|
||||
if ($abuse->check() && App::getEnv('_APP_OPTIONS_ABUSE', 'enabled') === 'enabled') {
|
||||
throw new Exception('Too many requests', 1013);
|
||||
}
|
||||
|
||||
/*
|
||||
* Validate Client Domain - Check to avoid CSRF attack.
|
||||
* Adding Appwrite API domains to allow XDOMAIN communication.
|
||||
* Skip this check for non-web platforms which are not required to send an origin header.
|
||||
*/
|
||||
$origin = $request->getOrigin();
|
||||
$originValidator = new Origin(\array_merge($project->getAttribute('platforms', []), $console->getAttribute('platforms', [])));
|
||||
|
||||
if (!$originValidator->isValid($origin) && $project->getId() !== 'console') {
|
||||
throw new Exception($originValidator->getDescription(), 1008);
|
||||
}
|
||||
|
||||
$roles = Auth::getRoles($user);
|
||||
|
||||
$channels = Realtime::convertChannels($request->getQuery('channels', []), $user->getId());
|
||||
|
||||
/**
|
||||
* Channels Check
|
||||
*/
|
||||
if (empty($channels)) {
|
||||
throw new Exception('Missing channels', 1008);
|
||||
}
|
||||
|
||||
$realtime->subscribe($project->getId(), $connection, $roles, $channels);
|
||||
|
||||
$user = empty($user->getId()) ? null : $response->output($user, Response::MODEL_USER);
|
||||
|
||||
$server->send([$connection], json_encode([
|
||||
'type' => 'connected',
|
||||
'data' => [
|
||||
'channels' => array_keys($channels),
|
||||
'user' => $user
|
||||
]
|
||||
]));
|
||||
|
||||
$stats->set($project->getId(), [
|
||||
'projectId' => $project->getId(),
|
||||
'teamId' => $project->getAttribute('teamId')
|
||||
]);
|
||||
$stats->incr($project->getId(), 'connections');
|
||||
$stats->incr($project->getId(), 'connectionsTotal');
|
||||
} catch (\Throwable $th) {
|
||||
$response = [
|
||||
'type' => 'error',
|
||||
'data' => [
|
||||
'code' => $th->getCode(),
|
||||
'message' => $th->getMessage()
|
||||
]
|
||||
];
|
||||
|
||||
$server->send([$connection], json_encode($response));
|
||||
$server->close($connection, $th->getCode());
|
||||
|
||||
if (App::isDevelopment()) {
|
||||
Console::error('[Error] Connection Error');
|
||||
Console::error('[Error] Code: ' . $response['data']['code']);
|
||||
Console::error('[Error] Message: ' . $response['data']['message']);
|
||||
}
|
||||
|
||||
if ($th instanceof PDOException) {
|
||||
$db = null;
|
||||
}
|
||||
} finally {
|
||||
/**
|
||||
* Put used PDO and Redis Connections back into their pools.
|
||||
*/
|
||||
$register->get('dbPool')->put($db);
|
||||
$register->get('redisPool')->put($redis);
|
||||
}
|
||||
});
|
||||
|
||||
$server->onMessage(function (int $connection, string $message) use ($server, $register, $realtime, $containerId) {
|
||||
try {
|
||||
$response = new Response(new SwooleResponse());
|
||||
$db = $register->get('dbPool')->get();
|
||||
$cache = $register->get('redisPool')->get();
|
||||
|
||||
$projectDB = new Database();
|
||||
$projectDB->setAdapter(new RedisAdapter(new MySQLAdapter($db, $cache), $cache));
|
||||
$projectDB->setNamespace('app_' . $realtime->connections[$connection]['projectId']);
|
||||
$projectDB->setMocks(Config::getParam('collections', []));
|
||||
|
||||
/*
|
||||
* Abuse Check
|
||||
*
|
||||
* Abuse limits are sending 32 times per minute and connection.
|
||||
*/
|
||||
$timeLimit = new TimeLimit('url:{url},conection:{connection}', 32, 60, $db);
|
||||
$timeLimit
|
||||
->setNamespace('app_' . $realtime->connections[$connection]['projectId'])
|
||||
->setParam('{connection}', $connection)
|
||||
->setParam('{container}', $containerId);
|
||||
|
||||
$abuse = new Abuse($timeLimit);
|
||||
|
||||
if ($abuse->check() && App::getEnv('_APP_OPTIONS_ABUSE', 'enabled') === 'enabled') {
|
||||
throw new Exception('Too many messages', 1013);
|
||||
}
|
||||
|
||||
$message = json_decode($message, true);
|
||||
|
||||
if (is_null($message) || (!array_key_exists('type', $message) && !array_key_exists('data', $message))) {
|
||||
throw new Exception('Message format is not valid.', 1003);
|
||||
}
|
||||
|
||||
switch ($message['type']) {
|
||||
/**
|
||||
* This type is used to authenticate.
|
||||
*/
|
||||
case 'authentication':
|
||||
if (!array_key_exists('session', $message['data'])) {
|
||||
throw new Exception('Payload is not valid.', 1003);
|
||||
}
|
||||
|
||||
$session = Auth::decodeSession($message['data']['session']);
|
||||
Auth::$unique = $session['id'];
|
||||
Auth::$secret = $session['secret'];
|
||||
|
||||
$user = $projectDB->getDocument(Auth::$unique);
|
||||
|
||||
if (
|
||||
empty($user->getId()) // Check a document has been found in the DB
|
||||
|| Database::SYSTEM_COLLECTION_USERS !== $user->getCollection() // Validate returned document is really a user document
|
||||
|| !Auth::sessionVerify($user->getAttribute('sessions', []), Auth::$secret) // Validate user has valid login token
|
||||
) {
|
||||
// cookie not valid
|
||||
throw new Exception('Session is not valid.', 1003);
|
||||
}
|
||||
|
||||
$roles = Auth::getRoles($user);
|
||||
$channels = Realtime::convertChannels(array_flip($realtime->connections[$connection]['channels']), $user->getId());
|
||||
$realtime->subscribe($realtime->connections[$connection]['projectId'], $connection, $roles, $channels);
|
||||
|
||||
$user = $response->output($user, Response::MODEL_USER);
|
||||
$server->send([$connection], json_encode([
|
||||
'type' => 'response',
|
||||
'data' => [
|
||||
'to' => 'authentication',
|
||||
'success' => true,
|
||||
'user' => $user
|
||||
]
|
||||
]));
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Exception('Message type is not valid.', 1003);
|
||||
break;
|
||||
}
|
||||
} catch (\Throwable $th) {
|
||||
$response = [
|
||||
'type' => 'error',
|
||||
'data' => [
|
||||
'code' => $th->getCode(),
|
||||
'message' => $th->getMessage()
|
||||
]
|
||||
];
|
||||
|
||||
$server->send([$connection], json_encode($response));
|
||||
|
||||
if ($th->getCode() === 1008) {
|
||||
$server->close($connection, $th->getCode());
|
||||
}
|
||||
} finally {
|
||||
$register->get('dbPool')->put($db);
|
||||
$register->get('redisPool')->put($cache);
|
||||
}
|
||||
});
|
||||
|
||||
$server->onClose(function (int $connection) use ($realtime, $stats) {
|
||||
if (array_key_exists($connection, $realtime->connections)) {
|
||||
$stats->decr($realtime->connections[$connection]['projectId'], 'connectionsTotal');
|
||||
}
|
||||
$realtime->unsubscribe($connection);
|
||||
|
||||
Console::info('Connection close: ' . $connection);
|
||||
});
|
||||
|
||||
$server->start();
|
|
@ -39,6 +39,14 @@ $cli
|
|||
]);
|
||||
}
|
||||
|
||||
function notifyDeleteConnections()
|
||||
{
|
||||
Resque::enqueue(Event::DELETE_QUEUE_NAME, Event::DELETE_CLASS_NAME, [
|
||||
'type' => DELETE_TYPE_REALTIME,
|
||||
'timestamp' => time() - 60
|
||||
]);
|
||||
}
|
||||
|
||||
// # of days in seconds (1 day = 86400s)
|
||||
$interval = (int) App::getEnv('_APP_MAINTENANCE_INTERVAL', '86400');
|
||||
$executionLogsRetention = (int) App::getEnv('_APP_MAINTENANCE_RETENTION_EXECUTION', '1209600');
|
||||
|
@ -51,5 +59,6 @@ $cli
|
|||
notifyDeleteExecutionLogs($executionLogsRetention);
|
||||
notifyDeleteAbuseLogs($abuseLogsRetention);
|
||||
notifyDeleteAuditLogs($auditLogRetention);
|
||||
notifyDeleteConnections();
|
||||
}, $interval);
|
||||
});
|
|
@ -4,7 +4,7 @@ $usageStatsEnabled = $this->getParam('usageStatsEnabled',true);
|
|||
?>
|
||||
|
||||
<div class="cover margin-bottom-small">
|
||||
<div class="zone xl margin-bottom-xl margin-top-small">
|
||||
<div class="zone xxl margin-bottom-xl margin-top-small">
|
||||
<h1 class="margin-bottom-small"
|
||||
data-service="projects.get"
|
||||
data-event="load,project.update,projects.createPlatform,projects.updatePlatform,projects.deletePlatform"
|
||||
|
@ -39,7 +39,8 @@ $usageStatsEnabled = $this->getParam('usageStatsEnabled',true);
|
|||
data-event="submit"
|
||||
data-name="usage"
|
||||
data-param-project-id="{{router.params.project}}"
|
||||
data-param-range="24h">
|
||||
data-param-range="24h"
|
||||
data-scope="console">
|
||||
<button class="tick">24h</button>
|
||||
</form>
|
||||
|
||||
|
@ -53,7 +54,8 @@ $usageStatsEnabled = $this->getParam('usageStatsEnabled',true);
|
|||
data-service="projects.getUsage"
|
||||
data-event="submit"
|
||||
data-name="usage"
|
||||
data-param-project-id="{{router.params.project}}">
|
||||
data-param-project-id="{{router.params.project}}"
|
||||
data-scope="console">
|
||||
<button class="tick">30d</button>
|
||||
</form>
|
||||
|
||||
|
@ -68,7 +70,8 @@ $usageStatsEnabled = $this->getParam('usageStatsEnabled',true);
|
|||
data-event="submit"
|
||||
data-name="usage"
|
||||
data-param-project-id="{{router.params.project}}"
|
||||
data-param-range="90d">
|
||||
data-param-range="90d"
|
||||
data-scope="console">
|
||||
<button class="tick">90d</button>
|
||||
</form>
|
||||
|
||||
|
@ -83,7 +86,6 @@ $usageStatsEnabled = $this->getParam('usageStatsEnabled',true);
|
|||
data-name="usage"
|
||||
data-param-project-id="{{router.params.project}}"
|
||||
data-param-range="30d">
|
||||
|
||||
<?php if (!$graph && $usageStatsEnabled): ?>
|
||||
<div class="box dashboard">
|
||||
<div class="row responsive">
|
||||
|
@ -94,26 +96,20 @@ $usageStatsEnabled = $this->getParam('usageStatsEnabled',true);
|
|||
|
||||
<div class="chart-metric">
|
||||
<div class="value margin-bottom-small"><span class="sum" data-ls-bind="{{usage.requests.total|statsTotal}}">N/A</span></div>
|
||||
<div class="metric margin-bottom-small">Requests <span class="tooltip" data-tooltip="Total number of API requests"><i class="icon-info-circled"></i></span></div>
|
||||
<div class="unit margin-start-no margin-bottom-small">Requests</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col span-3">
|
||||
<div class="value margin-bottom-small">
|
||||
<span class="sum" data-ls-bind="{{usage.network.total|humanFileSize}}" data-default="0">0</span>
|
||||
<span data-ls-bind="{{usage.network.total|humanFileUnit}}" class="text-size-small unit"></span>
|
||||
</div>
|
||||
<div class="metric margin-bottom-small">Bandwidth</div>
|
||||
|
||||
<div class="margin-top-large value small">
|
||||
<b class="text-size-small sum small" data-ls-bind="{{usage.functions.total|statsTotal}}" data-default="0"></b>
|
||||
<br />
|
||||
<b>Func. Executions</b>
|
||||
<span class="sum" data-ls-bind="{{realtime.current|accessProject}}" data-default="0">0</span>
|
||||
</div>
|
||||
<div class="unit margin-start-no margin-bottom-small">Connections</div>
|
||||
<div class="chart-bar margin-top-small margin-bottom-small" data-ls-attrs="data-history={{realtime.history|accessProject}}" data-forms-chart-bars="{{realtime.history|accessProject}}"></div>
|
||||
<div class="text-fade-dark text-size-small">Activity last 60 seconds</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="box dashboard">
|
||||
<div class="row responsive">
|
||||
<div class="col span-3">
|
||||
|
@ -132,8 +128,10 @@ $usageStatsEnabled = $this->getParam('usageStatsEnabled',true);
|
|||
<div class="margin-top-small"><b class="text-size-small unit">Users</b></div>
|
||||
</div>
|
||||
<div class="col span-3">
|
||||
<div class="value"><span class="sum" data-ls-bind="{{usage.tasks.total}}" data-default="0">0</span></div>
|
||||
<div class="margin-top-small"><b class="text-size-small unit">Tasks</b></div>
|
||||
<div class="value"><span class="sum" data-ls-bind="{{usage.functions.total|statsTotal}}" data-default="0">0</span></div>
|
||||
<div class="margin-top-small"><b class="text-size-small unit">Executions</b></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -17,8 +17,8 @@ services:
|
|||
- --providers.docker=true
|
||||
- --providers.docker.exposedByDefault=false
|
||||
- --providers.docker.constraints=Label(`traefik.constraint-label-stack`,`appwrite`)
|
||||
- --entrypoints.web.address=:80
|
||||
- --entrypoints.websecure.address=:443
|
||||
- --entrypoints.appwrite_web.address=:80
|
||||
- --entrypoints.appwrite_websecure.address=:443
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- <?php echo $httpPort; ?>:80
|
||||
|
@ -42,9 +42,17 @@ services:
|
|||
labels:
|
||||
- traefik.enable=true
|
||||
- traefik.constraint-label-stack=appwrite
|
||||
- traefik.http.routers.appwrite.rule=PathPrefix(`/`)
|
||||
- traefik.http.routers.appwrite-secure.rule=PathPrefix(`/`)
|
||||
- traefik.http.routers.appwrite-secure.tls=true
|
||||
- traefik.docker.network=appwrite
|
||||
- traefik.http.services.appwrite_api.loadbalancer.server.port=80
|
||||
#http
|
||||
- traefik.http.routers.appwrite_api_http.entrypoints=appwrite_web
|
||||
- traefik.http.routers.appwrite_api_http.rule=PathPrefix(`/`)
|
||||
- traefik.http.routers.appwrite_api_http.service=appwrite_api
|
||||
# https
|
||||
- traefik.http.routers.appwrite_api_https.entrypoints=appwrite_websecure
|
||||
- traefik.http.routers.appwrite_api_https.rule=PathPrefix(`/`)
|
||||
- traefik.http.routers.appwrite_api_https.service=appwrite_api
|
||||
- traefik.http.routers.appwrite_api_https.tls=true
|
||||
volumes:
|
||||
- appwrite-uploads:/storage/uploads:rw
|
||||
- appwrite-cache:/storage/cache:rw
|
||||
|
@ -99,6 +107,43 @@ services:
|
|||
- _APP_FUNCTIONS_MEMORY_SWAP
|
||||
- _APP_FUNCTIONS_RUNTIMES
|
||||
|
||||
appwrite-realtime:
|
||||
image: <?php echo $organization; ?>/<?php echo $image; ?>:<?php echo $version."\n"; ?>
|
||||
entrypoint: realtime
|
||||
container_name: appwrite-realtime
|
||||
restart: unless-stopped
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.constraint-label-stack=appwrite"
|
||||
- "traefik.docker.network=appwrite"
|
||||
- "traefik.http.services.appwrite_realtime.loadbalancer.server.port=80"
|
||||
#ws
|
||||
- traefik.http.routers.appwrite_realtime_ws.entrypoints=appwrite_web
|
||||
- traefik.http.routers.appwrite_realtime_ws.rule=PathPrefix(`/v1/realtime`)
|
||||
- traefik.http.routers.appwrite_realtime_ws.service=appwrite_realtime
|
||||
# wss
|
||||
- traefik.http.routers.appwrite_realtime_wss.entrypoints=appwrite_websecure
|
||||
- traefik.http.routers.appwrite_realtime_wss.rule=PathPrefix(`/v1/realtime`)
|
||||
- traefik.http.routers.appwrite_realtime_wss.service=appwrite_realtime
|
||||
- traefik.http.routers.appwrite_realtime_wss.tls=true
|
||||
- traefik.http.routers.appwrite_realtime_wss.tls.certresolver=dns
|
||||
networks:
|
||||
- appwrite
|
||||
depends_on:
|
||||
- redis
|
||||
environment:
|
||||
- _APP_ENV
|
||||
- _APP_OPTIONS_ABUSE
|
||||
- _APP_OPENSSL_KEY_V1
|
||||
- _APP_REDIS_HOST
|
||||
- _APP_REDIS_PORT
|
||||
- _APP_DB_HOST
|
||||
- _APP_DB_PORT
|
||||
- _APP_DB_SCHEMA
|
||||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
- _APP_USAGE_STATS
|
||||
|
||||
appwrite-worker-usage:
|
||||
image: <?php echo $organization; ?>/<?php echo $image; ?>:<?php echo $version."\n"; ?>
|
||||
entrypoint: worker-usage
|
||||
|
|
|
@ -70,6 +70,10 @@ class DeletesV1 extends Worker
|
|||
$this->deleteAbuseLogs($this->args['timestamp']);
|
||||
break;
|
||||
|
||||
case DELETE_TYPE_REALTIME:
|
||||
$this->deleteRealtimeUsage($this->args['timestamp']);
|
||||
break;
|
||||
|
||||
case DELETE_TYPE_CERTIFICATES:
|
||||
$document = new Document($this->args['document']);
|
||||
$this->deleteCertificates($document);
|
||||
|
@ -204,6 +208,19 @@ class DeletesV1 extends Worker
|
|||
});
|
||||
}
|
||||
|
||||
protected function deleteRealtimeUsage($timestamp)
|
||||
{
|
||||
if (!($consoleDB = $this->getConsoleDB())) {
|
||||
throw new Exception('Failed to get consoleDb.');
|
||||
}
|
||||
// Delete Dead Realtime Logs
|
||||
$this->deleteByGroup([
|
||||
'$collection='.Database::SYSTEM_COLLECTION_REALTIME_CONNECTIONS,
|
||||
'timestamp<'.$timestamp
|
||||
], $consoleDB);
|
||||
|
||||
}
|
||||
|
||||
protected function deleteFunction(Document $document, $projectId)
|
||||
{
|
||||
$projectDB = $this->getProjectDB($projectId);
|
||||
|
|
|
@ -6,6 +6,7 @@ use Appwrite\Database\Adapter\MySQL as MySQLAdapter;
|
|||
use Appwrite\Database\Adapter\Redis as RedisAdapter;
|
||||
use Appwrite\Database\Validator\Authorization;
|
||||
use Appwrite\Event\Event;
|
||||
use Appwrite\Messaging\Adapter\Realtime;
|
||||
use Appwrite\Resque\Worker;
|
||||
use Appwrite\Utopia\Response\Model\Execution;
|
||||
use Cron\CronExpression;
|
||||
|
@ -531,6 +532,16 @@ class FunctionsV1 extends Worker
|
|||
|
||||
$executionUpdate->trigger();
|
||||
|
||||
$target = Realtime::fromPayload('functions.executions.update', $execution);
|
||||
|
||||
Realtime::send(
|
||||
$projectId,
|
||||
$execution->getArrayCopy(),
|
||||
'functions.executions.update',
|
||||
$target['channels'],
|
||||
$target['roles']
|
||||
);
|
||||
|
||||
$usage = new Event('v1-usage', 'UsageV1');
|
||||
|
||||
$usage
|
||||
|
@ -540,7 +551,8 @@ class FunctionsV1 extends Worker
|
|||
->setParam('functionStatus', $functionStatus)
|
||||
->setParam('functionExecutionTime', $executionTime * 1000) // ms
|
||||
->setParam('networkRequestSize', 0)
|
||||
->setParam('networkResponseSize', 0);
|
||||
->setParam('networkResponseSize', 0)
|
||||
;
|
||||
|
||||
if (App::getEnv('_APP_USAGE_STATS', 'enabled') == 'enabled') {
|
||||
$usage->trigger();
|
||||
|
|
|
@ -37,6 +37,9 @@ class UsageV1 extends Worker
|
|||
$functionExecutionTime = $this->args['functionExecutionTime'] ?? 0;
|
||||
$functionStatus = $this->args['functionStatus'] ?? '';
|
||||
|
||||
$realtimeConnections = $this->args['realtimeConnections'] ?? 0;
|
||||
$realtimeMessages = $this->args['realtimeMessages'] ?? 0;
|
||||
|
||||
$tags = ",project={$projectId},version=".App::getEnv('_APP_VERSION', 'UNKNOWN');
|
||||
|
||||
// the global namespace is prepended to every key (optional)
|
||||
|
@ -51,6 +54,14 @@ class UsageV1 extends Worker
|
|||
$statsd->count('executions.time' . $tags . ',functionId=' . $functionId, $functionExecutionTime);
|
||||
}
|
||||
|
||||
if($realtimeConnections >= 1) {
|
||||
$statsd->count('realtime.clients'.$tags, $realtimeConnections);
|
||||
}
|
||||
|
||||
if($realtimeMessages >= 1) {
|
||||
$statsd->count('realtime.messages'.$tags, $realtimeMessages);
|
||||
}
|
||||
|
||||
$statsd->count('network.inbound'.$tags, $networkRequestSize);
|
||||
$statsd->count('network.outbound'.$tags, $networkResponseSize);
|
||||
$statsd->count('network.all'.$tags, $networkRequestSize + $networkResponseSize);
|
||||
|
|
3
bin/realtime
Normal file
3
bin/realtime
Normal file
|
@ -0,0 +1,3 @@
|
|||
#!/bin/sh
|
||||
|
||||
php /usr/src/code/app/realtime.php $@
|
|
@ -51,6 +51,7 @@
|
|||
"utopia-php/domains": "1.1.*",
|
||||
"utopia-php/swoole": "0.2.*",
|
||||
"utopia-php/storage": "0.5.*",
|
||||
"utopia-php/websocket": "0.0.*",
|
||||
"utopia-php/image": "0.5.*",
|
||||
"resque/php-resque": "1.3.6",
|
||||
"matomo/device-detector": "4.2.3",
|
||||
|
@ -64,8 +65,9 @@
|
|||
},
|
||||
"require-dev": {
|
||||
"appwrite/sdk-generator": "0.12.1",
|
||||
"swoole/ide-helper": "4.6.7",
|
||||
"phpunit/phpunit": "9.5.6",
|
||||
"swoole/ide-helper": "4.6.7",
|
||||
"textalk/websocket": "1.5.2",
|
||||
"vimeo/psalm": "4.7.2"
|
||||
},
|
||||
"provide": {
|
||||
|
|
109
composer.lock
generated
109
composer.lock
generated
|
@ -4,7 +4,7 @@
|
|||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "0a782184d016e458ff18f20a2c49ccfb",
|
||||
"content-hash": "cbfa98da20b9061eeec78a86ef2a8bed",
|
||||
"packages": [
|
||||
{
|
||||
"name": "adhocore/jwt",
|
||||
|
@ -2230,6 +2230,64 @@
|
|||
},
|
||||
"time": "2021-02-04T14:14:49+00:00"
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/websocket",
|
||||
"version": "0.0.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/utopia-php/websocket.git",
|
||||
"reference": "808317ef4ea0683c2c82dee5d543b1c8378e2e1b"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/utopia-php/websocket/zipball/808317ef4ea0683c2c82dee5d543b1c8378e2e1b",
|
||||
"reference": "808317ef4ea0683c2c82dee5d543b1c8378e2e1b",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^9.5.5",
|
||||
"swoole/ide-helper": "4.6.6",
|
||||
"textalk/websocket": "1.5.2",
|
||||
"vimeo/psalm": "^4.8.1",
|
||||
"workerman/workerman": "^4.0"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Utopia\\WebSocket\\": "src/WebSocket"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Eldad Fux",
|
||||
"email": "eldad@appwrite.io"
|
||||
},
|
||||
{
|
||||
"name": "Torsten Dittmann",
|
||||
"email": "torsten@appwrite.io"
|
||||
}
|
||||
],
|
||||
"description": "A simple abstraction for WebSocket servers.",
|
||||
"keywords": [
|
||||
"framework",
|
||||
"php",
|
||||
"upf",
|
||||
"utopia",
|
||||
"websocket"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/utopia-php/websocket/issues",
|
||||
"source": "https://github.com/utopia-php/websocket/tree/0.0.1"
|
||||
},
|
||||
"time": "2021-07-11T13:09:44+00:00"
|
||||
},
|
||||
{
|
||||
"name": "webmozart/assert",
|
||||
"version": "1.10.0",
|
||||
|
@ -5835,6 +5893,55 @@
|
|||
],
|
||||
"time": "2021-08-26T08:00:08+00:00"
|
||||
},
|
||||
{
|
||||
"name": "textalk/websocket",
|
||||
"version": "1.5.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/Textalk/websocket-php.git",
|
||||
"reference": "b93249453806a2dd46495de46d76fcbcb0d8dee8"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/Textalk/websocket-php/zipball/b93249453806a2dd46495de46d76fcbcb0d8dee8",
|
||||
"reference": "b93249453806a2dd46495de46d76fcbcb0d8dee8",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": "^7.2 | ^8.0",
|
||||
"psr/log": "^1.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"php-coveralls/php-coveralls": "^2.0",
|
||||
"phpunit/phpunit": "^8.0|^9.0",
|
||||
"squizlabs/php_codesniffer": "^3.5"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"WebSocket\\": "lib"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"ISC"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Fredrik Liljegren"
|
||||
},
|
||||
{
|
||||
"name": "Sören Jensen",
|
||||
"email": "soren@abicart.se"
|
||||
}
|
||||
],
|
||||
"description": "WebSocket client and server",
|
||||
"support": {
|
||||
"issues": "https://github.com/Textalk/websocket-php/issues",
|
||||
"source": "https://github.com/Textalk/websocket-php/tree/1.5.2"
|
||||
},
|
||||
"time": "2021-02-12T15:39:23+00:00"
|
||||
},
|
||||
{
|
||||
"name": "theseer/tokenizer",
|
||||
"version": "1.2.1",
|
||||
|
|
|
@ -17,8 +17,8 @@ services:
|
|||
- --providers.docker=true
|
||||
- --providers.docker.exposedByDefault=false
|
||||
- --providers.docker.constraints=Label(`traefik.constraint-label-stack`,`appwrite`)
|
||||
- --entrypoints.web.address=:80
|
||||
- --entrypoints.websecure.address=:443
|
||||
- --entrypoints.appwrite_web.address=:80
|
||||
- --entrypoints.appwrite_websecure.address=:443
|
||||
- --accesslog=true
|
||||
ports:
|
||||
- 80:80
|
||||
|
@ -47,11 +47,19 @@ services:
|
|||
networks:
|
||||
- appwrite
|
||||
labels:
|
||||
- traefik.enable=true
|
||||
- traefik.constraint-label-stack=appwrite
|
||||
- traefik.http.routers.appwrite.rule=PathPrefix(`/`)
|
||||
- traefik.http.routers.appwrite-secure.rule=PathPrefix(`/`)
|
||||
- traefik.http.routers.appwrite-secure.tls=true
|
||||
- "traefik.enable=true"
|
||||
- "traefik.constraint-label-stack=appwrite"
|
||||
- "traefik.docker.network=appwrite"
|
||||
- "traefik.http.services.appwrite_api.loadbalancer.server.port=80"
|
||||
#http
|
||||
- traefik.http.routers.appwrite_api_http.entrypoints=appwrite_web
|
||||
- traefik.http.routers.appwrite_api_http.rule=PathPrefix(`/`)
|
||||
- traefik.http.routers.appwrite_api_http.service=appwrite_api
|
||||
# https
|
||||
- traefik.http.routers.appwrite_api_https.entrypoints=appwrite_websecure
|
||||
- traefik.http.routers.appwrite_api_https.rule=PathPrefix(`/`)
|
||||
- traefik.http.routers.appwrite_api_https.service=appwrite_api
|
||||
- traefik.http.routers.appwrite_api_https.tls=true
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
- appwrite-uploads:/storage/uploads:rw
|
||||
|
@ -122,6 +130,49 @@ services:
|
|||
- _APP_FUNCTIONS_MEMORY_SWAP
|
||||
- _APP_FUNCTIONS_RUNTIMES
|
||||
|
||||
appwrite-realtime:
|
||||
entrypoint: realtime
|
||||
container_name: appwrite-realtime
|
||||
build:
|
||||
context: .
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- 9505:80
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.constraint-label-stack=appwrite"
|
||||
- "traefik.docker.network=appwrite"
|
||||
- "traefik.http.services.appwrite_realtime.loadbalancer.server.port=80"
|
||||
#ws
|
||||
- traefik.http.routers.appwrite_realtime_ws.entrypoints=appwrite_web
|
||||
- traefik.http.routers.appwrite_realtime_ws.rule=PathPrefix(`/v1/realtime`)
|
||||
- traefik.http.routers.appwrite_realtime_ws.service=appwrite_realtime
|
||||
# wss
|
||||
- traefik.http.routers.appwrite_realtime_wss.entrypoints=appwrite_websecure
|
||||
- traefik.http.routers.appwrite_realtime_wss.rule=PathPrefix(`/v1/realtime`)
|
||||
- traefik.http.routers.appwrite_realtime_wss.service=appwrite_realtime
|
||||
- traefik.http.routers.appwrite_realtime_wss.tls=true
|
||||
- traefik.http.routers.appwrite_realtime_wss.tls.certresolver=dns
|
||||
networks:
|
||||
- appwrite
|
||||
volumes:
|
||||
- ./app:/usr/src/code/app
|
||||
- ./src:/usr/src/code/src
|
||||
depends_on:
|
||||
- redis
|
||||
environment:
|
||||
- _APP_ENV
|
||||
- _APP_OPTIONS_ABUSE
|
||||
- _APP_OPENSSL_KEY_V1
|
||||
- _APP_REDIS_HOST
|
||||
- _APP_REDIS_PORT
|
||||
- _APP_DB_HOST
|
||||
- _APP_DB_PORT
|
||||
- _APP_DB_SCHEMA
|
||||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
- _APP_USAGE_STATS
|
||||
|
||||
appwrite-worker-usage:
|
||||
entrypoint: worker-usage
|
||||
container_name: appwrite-worker-usage
|
||||
|
@ -479,7 +530,7 @@ services:
|
|||
container_name: appwrite-adminer
|
||||
restart: always
|
||||
ports:
|
||||
- 9505:8080
|
||||
- 9506:8080
|
||||
networks:
|
||||
- appwrite
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
<svg host="65bd71144e" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="1108px" height="839px" viewBox="-0.5 -0.5 1108 839" content="<mxfile host="f62fd6c3-483a-4a80-b2dc-315a36eb41a5" modified="2021-01-16T10:33:38.685Z" agent="5.0 (Macintosh; Intel Mac OS X 11_0_0) AppleWebKit/537.36 (KHTML, like Gecko) Code/1.52.1 Chrome/83.0.4103.122 Electron/9.3.5 Safari/537.36" etag="OZW_7vXQqOQ9aLjWoWdU" version="13.10.0" type="embed"><diagram id="WOshqXSVd2VkRfcggtcB" name="Page-1">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</diagram></mxfile>">
|
||||
<svg host="65bd71144e" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="1108px" height="839px" viewBox="-0.5 -0.5 1108 839" content="<mxfile host="612c770d-d23c-4897-ac0d-72478d1c5aef" modified="2021-02-22T07:17:54.650Z" agent="5.0 (Macintosh; Intel Mac OS X 11_2_1) AppleWebKit/537.36 (KHTML, like Gecko) Code/1.53.2 Chrome/87.0.4280.141 Electron/11.2.1 Safari/537.36" etag="sAOb0L0gpVpMeEycjwCe" version="14.2.4" type="embed"><diagram id="WOshqXSVd2VkRfcggtcB" name="Page-1">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</diagram></mxfile>">
|
||||
<defs/>
|
||||
<g>
|
||||
<path d="M 77 40 L 77 80 L 397 80 L 397 113.63" fill="none" stroke="#23445d" stroke-miterlimit="10" pointer-events="stroke"/>
|
||||
|
@ -117,10 +117,12 @@
|
|||
</g>
|
||||
<path d="M 457 220 L 500.63 220" fill="none" stroke="#000000" stroke-miterlimit="10" pointer-events="stroke"/>
|
||||
<path d="M 505.88 220 L 498.88 223.5 L 500.63 220 L 498.88 216.5 Z" fill="#000000" stroke="#000000" stroke-miterlimit="10" pointer-events="all"/>
|
||||
<path d="M 397 240 L 397 275 L 597 275 L 597 303.63" fill="none" stroke="#10739e" stroke-miterlimit="10" pointer-events="stroke"/>
|
||||
<path d="M 597 308.88 L 593.5 301.88 L 597 303.63 L 600.5 301.88 Z" fill="#10739e" stroke="#10739e" stroke-miterlimit="10" pointer-events="all"/>
|
||||
<path d="M 397 240 L 397 275 L 437 275 L 437 303.63" fill="none" stroke="#10739e" stroke-miterlimit="10" pointer-events="stroke"/>
|
||||
<path d="M 437 308.88 L 433.5 301.88 L 437 303.63 L 440.5 301.88 Z" fill="#10739e" stroke="#10739e" stroke-miterlimit="10" pointer-events="all"/>
|
||||
<path d="M 397 240 L 397 275 L 97 275 L 97 303.63" fill="none" stroke="#10739e" stroke-miterlimit="10" pointer-events="stroke"/>
|
||||
<path d="M 97 308.88 L 93.5 301.88 L 97 303.63 L 100.5 301.88 Z" fill="#10739e" stroke="#10739e" stroke-miterlimit="10" pointer-events="all"/>
|
||||
<path d="M 397 240 L 397 275 L 597 275 L 597 303.63" fill="none" stroke="#006eaf" stroke-miterlimit="10" pointer-events="stroke"/>
|
||||
<path d="M 597 308.88 L 593.5 301.88 L 597 303.63 L 600.5 301.88 Z" fill="#006eaf" stroke="#006eaf" stroke-miterlimit="10" pointer-events="all"/>
|
||||
<rect x="337" y="200" width="120" height="40" fill="#dae8fc" stroke="#6c8ebf" pointer-events="all"/>
|
||||
<g transform="translate(-0.5 -0.5)">
|
||||
<switch>
|
||||
|
@ -138,8 +140,6 @@
|
|||
</text>
|
||||
</switch>
|
||||
</g>
|
||||
<path d="M 97 350 L 97 380 L 597 380 L 597 356.37" fill="none" stroke="#6c8ebf" stroke-miterlimit="10" pointer-events="stroke"/>
|
||||
<path d="M 597 351.12 L 600.5 358.12 L 597 356.37 L 593.5 358.12 Z" fill="#6c8ebf" stroke="#6c8ebf" stroke-miterlimit="10" pointer-events="all"/>
|
||||
<rect x="37" y="310" width="120" height="40" fill="#ffffff" stroke="none" pointer-events="all"/>
|
||||
<g transform="translate(-0.5 -0.5)">
|
||||
<switch>
|
||||
|
@ -157,42 +157,40 @@
|
|||
</text>
|
||||
</switch>
|
||||
</g>
|
||||
<path d="M 657 330 L 897 330 L 897 250 L 960.63 250" fill="none" stroke="#d4d4d4" stroke-miterlimit="10" pointer-events="stroke"/>
|
||||
<path d="M 497 330 L 897 330 L 897 250 L 960.63 250" fill="none" stroke="#d4d4d4" stroke-miterlimit="10" pointer-events="stroke"/>
|
||||
<path d="M 965.88 250 L 958.88 253.5 L 960.63 250 L 958.88 246.5 Z" fill="#d4d4d4" stroke="#d4d4d4" stroke-miterlimit="10" pointer-events="all"/>
|
||||
<path d="M 657 330 L 897 330 L 897 300 L 960.63 300" fill="none" stroke="#d4d4d4" stroke-miterlimit="10" pointer-events="stroke"/>
|
||||
<path d="M 497 330 L 897 330 L 897 300 L 960.63 300" fill="none" stroke="#d4d4d4" stroke-miterlimit="10" pointer-events="stroke"/>
|
||||
<path d="M 965.88 300 L 958.88 303.5 L 960.63 300 L 958.88 296.5 Z" fill="#d4d4d4" stroke="#d4d4d4" stroke-miterlimit="10" pointer-events="all"/>
|
||||
<path d="M 657 330 L 897 330 L 897 400 L 960.63 400" fill="none" stroke="#d4d4d4" stroke-miterlimit="10" pointer-events="stroke"/>
|
||||
<path d="M 497 330 L 897 330 L 897 400 L 960.63 400" fill="none" stroke="#d4d4d4" stroke-miterlimit="10" pointer-events="stroke"/>
|
||||
<path d="M 965.88 400 L 958.88 403.5 L 960.63 400 L 958.88 396.5 Z" fill="#d4d4d4" stroke="#d4d4d4" stroke-miterlimit="10" pointer-events="all"/>
|
||||
<path d="M 657 330 L 897 330 L 897 450 L 960.63 450" fill="none" stroke="#d4d4d4" stroke-miterlimit="10" pointer-events="stroke"/>
|
||||
<path d="M 497 330 L 897 330 L 897 450 L 960.63 450" fill="none" stroke="#d4d4d4" stroke-miterlimit="10" pointer-events="stroke"/>
|
||||
<path d="M 965.88 450 L 958.88 453.5 L 960.63 450 L 958.88 446.5 Z" fill="#d4d4d4" stroke="#d4d4d4" stroke-miterlimit="10" pointer-events="all"/>
|
||||
<path d="M 657 330 L 897 330 L 897 500 L 960.63 500" fill="none" stroke="#d4d4d4" stroke-miterlimit="10" pointer-events="stroke"/>
|
||||
<path d="M 497 330 L 897 330 L 897 500 L 960.63 500" fill="none" stroke="#d4d4d4" stroke-miterlimit="10" pointer-events="stroke"/>
|
||||
<path d="M 965.88 500 L 958.88 503.5 L 960.63 500 L 958.88 496.5 Z" fill="#d4d4d4" stroke="#d4d4d4" stroke-miterlimit="10" pointer-events="all"/>
|
||||
<path d="M 657 330 L 897 330 L 897 550 L 960.63 550" fill="none" stroke="#d4d4d4" stroke-miterlimit="10" pointer-events="stroke"/>
|
||||
<path d="M 497 330 L 897 330 L 897 550 L 960.63 550" fill="none" stroke="#d4d4d4" stroke-miterlimit="10" pointer-events="stroke"/>
|
||||
<path d="M 965.88 550 L 958.88 553.5 L 960.63 550 L 958.88 546.5 Z" fill="#d4d4d4" stroke="#d4d4d4" stroke-miterlimit="10" pointer-events="all"/>
|
||||
<path d="M 657 330 L 897 330 L 897 600 L 960.63 600" fill="none" stroke="#d4d4d4" stroke-miterlimit="10" pointer-events="stroke"/>
|
||||
<path d="M 497 330 L 897 330 L 897 600 L 960.63 600" fill="none" stroke="#d4d4d4" stroke-miterlimit="10" pointer-events="stroke"/>
|
||||
<path d="M 965.88 600 L 958.88 603.5 L 960.63 600 L 958.88 596.5 Z" fill="#d4d4d4" stroke="#d4d4d4" stroke-miterlimit="10" pointer-events="all"/>
|
||||
<path d="M 537 330 L 347 330 L 347 493.63" fill="none" stroke="#56517e" stroke-miterlimit="10" pointer-events="stroke"/>
|
||||
<path d="M 347 498.88 L 343.5 491.88 L 347 493.63 L 350.5 491.88 Z" fill="#56517e" stroke="#56517e" stroke-miterlimit="10" pointer-events="all"/>
|
||||
<path d="M 537 330 L 207 330 L 207 493.63" fill="none" stroke="#56517e" stroke-miterlimit="10" pointer-events="stroke"/>
|
||||
<path d="M 377 330 L 207 330 L 207 493.63" fill="none" stroke="#56517e" stroke-miterlimit="10" pointer-events="stroke"/>
|
||||
<path d="M 207 498.88 L 203.5 491.88 L 207 493.63 L 210.5 491.88 Z" fill="#56517e" stroke="#56517e" stroke-miterlimit="10" pointer-events="all"/>
|
||||
<path d="M 597 350 L 597 403.63" fill="none" stroke="#82b366" stroke-miterlimit="10" pointer-events="stroke"/>
|
||||
<path d="M 597 408.88 L 593.5 401.88 L 597 403.63 L 600.5 401.88 Z" fill="#82b366" stroke="#82b366" stroke-miterlimit="10" pointer-events="all"/>
|
||||
<path d="M 657 330 L 897 330 L 897 350 L 960.63 350" fill="none" stroke="#d4d4d4" stroke-miterlimit="10" pointer-events="stroke"/>
|
||||
<path d="M 437 350 L 437 380 L 597 380 L 597 403.63" fill="none" stroke="#2d7600" stroke-miterlimit="10" pointer-events="stroke"/>
|
||||
<path d="M 597 408.88 L 593.5 401.88 L 597 403.63 L 600.5 401.88 Z" fill="#2d7600" stroke="#2d7600" stroke-miterlimit="10" pointer-events="all"/>
|
||||
<path d="M 497 330 L 897 330 L 897 350 L 960.63 350" fill="none" stroke="#d4d4d4" stroke-miterlimit="10" pointer-events="stroke"/>
|
||||
<path d="M 965.88 350 L 958.88 353.5 L 960.63 350 L 958.88 346.5 Z" fill="#d4d4d4" stroke="#d4d4d4" stroke-miterlimit="10" pointer-events="all"/>
|
||||
<rect x="537" y="310" width="120" height="40" fill="#ffffff" stroke="none" pointer-events="all"/>
|
||||
<rect x="377" y="310" width="120" height="40" fill="#ffffff" stroke="none" pointer-events="all"/>
|
||||
<g transform="translate(-0.5 -0.5)">
|
||||
<switch>
|
||||
<foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility">
|
||||
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 118px; height: 1px; padding-top: 330px; margin-left: 538px;">
|
||||
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 118px; height: 1px; padding-top: 330px; margin-left: 378px;">
|
||||
<div style="box-sizing: border-box; font-size: 0; text-align: center; ">
|
||||
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; ">
|
||||
APIs
|
||||
REST API
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</foreignObject>
|
||||
<text x="597" y="334" fill="#000000" font-family="Helvetica" font-size="12px" text-anchor="middle">
|
||||
APIs
|
||||
<text x="437" y="334" fill="#000000" font-family="Helvetica" font-size="12px" text-anchor="middle">
|
||||
REST API
|
||||
</text>
|
||||
</switch>
|
||||
</g>
|
||||
|
@ -425,10 +423,12 @@
|
|||
</text>
|
||||
</switch>
|
||||
</g>
|
||||
<path d="M 597 430 L 597 465 L 527 465 L 527 493.63" fill="none" stroke="#82b366" stroke-miterlimit="10" pointer-events="stroke"/>
|
||||
<path d="M 527 498.88 L 523.5 491.88 L 527 493.63 L 530.5 491.88 Z" fill="#82b366" stroke="#82b366" stroke-miterlimit="10" pointer-events="all"/>
|
||||
<path d="M 597 430 L 597 465 L 667 465 L 667 493.63" fill="none" stroke="#82b366" stroke-miterlimit="10" pointer-events="stroke"/>
|
||||
<path d="M 667 498.88 L 663.5 491.88 L 667 493.63 L 670.5 491.88 Z" fill="#82b366" stroke="#82b366" stroke-miterlimit="10" pointer-events="all"/>
|
||||
<path d="M 597 430 L 597 465 L 527 465 L 527 493.63" fill="none" stroke="#2d7600" stroke-miterlimit="10" pointer-events="stroke"/>
|
||||
<path d="M 527 498.88 L 523.5 491.88 L 527 493.63 L 530.5 491.88 Z" fill="#2d7600" stroke="#2d7600" stroke-miterlimit="10" pointer-events="all"/>
|
||||
<path d="M 597 430 L 597 465 L 667 465 L 667 493.63" fill="none" stroke="#2d7600" stroke-miterlimit="10" pointer-events="stroke"/>
|
||||
<path d="M 667 498.88 L 663.5 491.88 L 667 493.63 L 670.5 491.88 Z" fill="#2d7600" stroke="#2d7600" stroke-miterlimit="10" pointer-events="all"/>
|
||||
<path d="M 597 430 L 597 465 L 807 465 L 807 493.63" fill="none" stroke="#2d7600" stroke-miterlimit="10" pointer-events="stroke"/>
|
||||
<path d="M 807 498.88 L 803.5 491.88 L 807 493.63 L 810.5 491.88 Z" fill="#2d7600" stroke="#2d7600" stroke-miterlimit="10" pointer-events="all"/>
|
||||
<rect x="437" y="410" width="320" height="20" fill="#d5e8d4" stroke="#82b366" pointer-events="all"/>
|
||||
<g transform="translate(-0.5 -0.5)">
|
||||
<switch>
|
||||
|
@ -724,10 +724,29 @@
|
|||
</text>
|
||||
</switch>
|
||||
</g>
|
||||
<path d="M 597 350 L 597 403.63" fill="none" stroke="#2d7600" stroke-miterlimit="10" pointer-events="stroke"/>
|
||||
<path d="M 597 408.88 L 593.5 401.88 L 597 403.63 L 600.5 401.88 Z" fill="#2d7600" stroke="#2d7600" stroke-miterlimit="10" pointer-events="all"/>
|
||||
<rect x="537" y="310" width="120" height="40" fill="#ffffff" stroke="none" pointer-events="all"/>
|
||||
<g transform="translate(-0.5 -0.5)">
|
||||
<switch>
|
||||
<foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility">
|
||||
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 118px; height: 1px; padding-top: 330px; margin-left: 538px;">
|
||||
<div style="box-sizing: border-box; font-size: 0; text-align: center; ">
|
||||
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; ">
|
||||
REALTIME API
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</foreignObject>
|
||||
<text x="597" y="334" fill="#000000" font-family="Helvetica" font-size="12px" text-anchor="middle">
|
||||
REALTIME API
|
||||
</text>
|
||||
</switch>
|
||||
</g>
|
||||
</g>
|
||||
<switch>
|
||||
<g requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"/>
|
||||
<a transform="translate(0,-5)" xlink:href="https://desk.draw.io/support/solutions/articles/16000042487" target="_blank">
|
||||
<a transform="translate(0,-5)" xlink:href="https://www.diagrams.net/doc/faq/svg-export-text-problems" target="_blank">
|
||||
<text text-anchor="middle" font-size="10px" x="50%" y="100%">
|
||||
Viewer does not support full SVG 1.1
|
||||
</text>
|
||||
|
|
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 64 KiB |
File diff suppressed because one or more lines are too long
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 40 KiB |
|
@ -27,6 +27,7 @@ const configApp = {
|
|||
'public/scripts/services/sdk.js',
|
||||
'public/scripts/services/search.js',
|
||||
'public/scripts/services/timezone.js',
|
||||
'public/scripts/services/realtime.js',
|
||||
|
||||
'public/scripts/routes.js',
|
||||
'public/scripts/filters.js',
|
||||
|
@ -41,6 +42,7 @@ const configApp = {
|
|||
'public/scripts/views/forms/clone.js',
|
||||
'public/scripts/views/forms/add.js',
|
||||
'public/scripts/views/forms/chart.js',
|
||||
'public/scripts/views/forms/chart-bar.js',
|
||||
'public/scripts/views/forms/code.js',
|
||||
'public/scripts/views/forms/color.js',
|
||||
'public/scripts/views/forms/copy.js',
|
||||
|
|
|
@ -20,6 +20,7 @@
|
|||
<directory>./tests/e2e/General</directory>
|
||||
<directory>./tests/e2e/Scopes</directory>
|
||||
<directory>./tests/e2e/Services/Account</directory>
|
||||
<directory>./tests/e2e/Services/Realtime</directory>
|
||||
<directory>./tests/e2e/Services/Avatars</directory>
|
||||
<directory>./tests/e2e/Services/Database</directory>
|
||||
<directory>./tests/e2e/Services/Health</directory>
|
||||
|
|
30
public/dist/scripts/app-all.js
vendored
30
public/dist/scripts/app-all.js
vendored
File diff suppressed because one or more lines are too long
13
public/dist/scripts/app-dep.js
vendored
13
public/dist/scripts/app-dep.js
vendored
|
@ -5,7 +5,13 @@ function rejected(value){try{step(generator["throw"](value));}catch(e){reject(e)
|
|||
function step(result){result.done?resolve(result.value):adopt(result.value).then(fulfilled,rejected);}
|
||||
step((generator=generator.apply(thisArg,_arguments||[])).next());});}
|
||||
class AppwriteException extends Error{constructor(message,code=0,response=''){super(message);this.name='AppwriteException';this.message=message;this.code=code;this.response=response;}}
|
||||
class Appwrite{constructor(){this.config={endpoint:'https://appwrite.io/v1',project:'',key:'',jwt:'',locale:'',mode:'',};this.headers={'x-sdk-version':'appwrite:web:2.1.0','X-Appwrite-Response-Format':'0.9.0',};this.account={get:()=>__awaiter(this,void 0,void 0,function*(){let path='/account';let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);}),create:(email,password,name)=>__awaiter(this,void 0,void 0,function*(){if(typeof email==='undefined'){throw new AppwriteException('Missing required parameter: "email"');}
|
||||
class Appwrite{constructor(){this.config={endpoint:'https://appwrite.io/v1',endpointRealtime:'',project:'',key:'',jwt:'',locale:'',mode:'',};this.headers={'x-sdk-version':'appwrite:web:2.1.0','X-Appwrite-Response-Format':'0.9.0',};this.realtime={socket:undefined,timeout:undefined,channels:{},lastMessage:undefined,createSocket:()=>{var _a,_b;const channels=new URLSearchParams();channels.set('project',this.config.project);for(const property in this.realtime.channels){channels.append('channels[]',property);}
|
||||
if(((_a=this.realtime.socket)===null||_a===void 0?void 0:_a.readyState)===WebSocket.OPEN){this.realtime.socket.close();}
|
||||
this.realtime.socket=new WebSocket(this.config.endpointRealtime+'/realtime?'+channels.toString());(_b=this.realtime.socket)===null||_b===void 0?void 0:_b.addEventListener('message',this.realtime.authenticate);for(const channel in this.realtime.channels){this.realtime.channels[channel].forEach(callback=>{var _a;(_a=this.realtime.socket)===null||_a===void 0?void 0:_a.addEventListener('message',callback);});}
|
||||
this.realtime.socket.addEventListener('close',event=>{var _a,_b,_c;if(((_b=(_a=this.realtime)===null||_a===void 0?void 0:_a.lastMessage)===null||_b===void 0?void 0:_b.type)==='error'&&((_c=this.realtime)===null||_c===void 0?void 0:_c.lastMessage.data).code===1008){return;}
|
||||
console.error('Realtime got disconnected. Reconnect will be attempted in 1 second.',event.reason);setTimeout(()=>{this.realtime.createSocket();},1000);});},authenticate:(event)=>{var _a,_b;const message=JSON.parse(event.data);if(message.type==='connected'){const cookie=JSON.parse((_a=window.localStorage.getItem('cookieFallback'))!==null&&_a!==void 0?_a:"{}");const session=cookie===null||cookie===void 0?void 0:cookie[`a_session_${this.config.project}`];const data=message.data;if(session&&!data.user){(_b=this.realtime.socket)===null||_b===void 0?void 0:_b.send(JSON.stringify({type:"authentication",data:{session}}));}}},onMessage:(channel,callback)=>(event)=>{try{const message=JSON.parse(event.data);this.realtime.lastMessage=message;if(message.type==='event'){let data=message.data;if(data.channels&&data.channels.includes(channel)){callback(data);}}
|
||||
else if(message.type==='error'){throw message.data;}}
|
||||
catch(e){console.error(e);}}};this.account={get:()=>__awaiter(this,void 0,void 0,function*(){let path='/account';let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);}),create:(email,password,name)=>__awaiter(this,void 0,void 0,function*(){if(typeof email==='undefined'){throw new AppwriteException('Missing required parameter: "email"');}
|
||||
if(typeof password==='undefined'){throw new AppwriteException('Missing required parameter: "password"');}
|
||||
let path='/account';let payload={};if(typeof email!=='undefined'){payload['email']=email;}
|
||||
if(typeof password!=='undefined'){payload['password']=password;}
|
||||
|
@ -449,12 +455,15 @@ const uri=new URL(this.config.endpoint+path);return yield this.call('patch',uri,
|
|||
if(typeof emailVerification==='undefined'){throw new AppwriteException('Missing required parameter: "emailVerification"');}
|
||||
let path='/users/{userId}/verification'.replace('{userId}',userId);let payload={};if(typeof emailVerification!=='undefined'){payload['emailVerification']=emailVerification;}
|
||||
const uri=new URL(this.config.endpoint+path);return yield this.call('patch',uri,{'content-type':'application/json',},payload);})};}
|
||||
setEndpoint(endpoint){this.config.endpoint=endpoint;return this;}
|
||||
setEndpoint(endpoint){this.config.endpoint=endpoint;this.config.endpointRealtime=this.config.endpointRealtime||this.config.endpoint.replace("https://","wss://").replace("http://","ws://");return this;}
|
||||
setEndpointRealtime(endpointRealtime){this.config.endpointRealtime=endpointRealtime;return this;}
|
||||
setProject(value){this.headers['X-Appwrite-Project']=value;this.config.project=value;return this;}
|
||||
setKey(value){this.headers['X-Appwrite-Key']=value;this.config.key=value;return this;}
|
||||
setJWT(value){this.headers['X-Appwrite-JWT']=value;this.config.jwt=value;return this;}
|
||||
setLocale(value){this.headers['X-Appwrite-Locale']=value;this.config.locale=value;return this;}
|
||||
setMode(value){this.headers['X-Appwrite-Mode']=value;this.config.mode=value;return this;}
|
||||
subscribe(channels,callback){let channelArray=typeof channels==='string'?[channels]:channels;let savedChannels=[];channelArray.forEach((channel,index)=>{if(!(channel in this.realtime.channels)){this.realtime.channels[channel]=[];}
|
||||
savedChannels[index]={name:channel,index:(this.realtime.channels[channel].push(this.realtime.onMessage(channel,callback))-1)};clearTimeout(this.realtime.timeout);this.realtime.timeout=window===null||window===void 0?void 0:window.setTimeout(()=>{this.realtime.createSocket();},1);});return()=>{savedChannels.forEach(channel=>{var _a;(_a=this.realtime.socket)===null||_a===void 0?void 0:_a.removeEventListener('message',this.realtime.channels[channel.name][channel.index]);this.realtime.channels[channel.name].splice(channel.index,1);});};}
|
||||
call(method,url,headers={},params={}){var _a,_b;return __awaiter(this,void 0,void 0,function*(){method=method.toUpperCase();headers=Object.assign(Object.assign({},headers),this.headers);let options={method,headers,credentials:'include'};if(typeof window!=='undefined'&&window.localStorage){headers['X-Fallback-Cookies']=(_a=window.localStorage.getItem('cookieFallback'))!==null&&_a!==void 0?_a:"";}
|
||||
if(method==='GET'){for(const[key,value]of Object.entries(this.flatten(params))){url.searchParams.append(key,value);}}
|
||||
else{switch(headers['content-type']){case'application/json':options.body=JSON.stringify(params);break;case'multipart/form-data':let formData=new FormData();for(const key in params){if(Array.isArray(params[key])){formData.append(key+'[]',params[key].join(','));}
|
||||
|
|
17
public/dist/scripts/app.js
vendored
17
public/dist/scripts/app.js
vendored
File diff suppressed because one or more lines are too long
2
public/dist/styles/default-ltr.css
vendored
2
public/dist/styles/default-ltr.css
vendored
File diff suppressed because one or more lines are too long
2
public/dist/styles/default-rtl.css
vendored
2
public/dist/styles/default-rtl.css
vendored
File diff suppressed because one or more lines are too long
|
@ -39,6 +39,7 @@
|
|||
constructor() {
|
||||
this.config = {
|
||||
endpoint: 'https://appwrite.io/v1',
|
||||
endpointRealtime: '',
|
||||
project: '',
|
||||
key: '',
|
||||
jwt: '',
|
||||
|
@ -49,6 +50,76 @@
|
|||
'x-sdk-version': 'appwrite:web:2.1.0',
|
||||
'X-Appwrite-Response-Format': '0.9.0',
|
||||
};
|
||||
this.realtime = {
|
||||
socket: undefined,
|
||||
timeout: undefined,
|
||||
channels: {},
|
||||
lastMessage: undefined,
|
||||
createSocket: () => {
|
||||
var _a, _b;
|
||||
const channels = new URLSearchParams();
|
||||
channels.set('project', this.config.project);
|
||||
for (const property in this.realtime.channels) {
|
||||
channels.append('channels[]', property);
|
||||
}
|
||||
if (((_a = this.realtime.socket) === null || _a === void 0 ? void 0 : _a.readyState) === WebSocket.OPEN) {
|
||||
this.realtime.socket.close();
|
||||
}
|
||||
this.realtime.socket = new WebSocket(this.config.endpointRealtime + '/realtime?' + channels.toString());
|
||||
(_b = this.realtime.socket) === null || _b === void 0 ? void 0 : _b.addEventListener('message', this.realtime.authenticate);
|
||||
for (const channel in this.realtime.channels) {
|
||||
this.realtime.channels[channel].forEach(callback => {
|
||||
var _a;
|
||||
(_a = this.realtime.socket) === null || _a === void 0 ? void 0 : _a.addEventListener('message', callback);
|
||||
});
|
||||
}
|
||||
this.realtime.socket.addEventListener('close', event => {
|
||||
var _a, _b, _c;
|
||||
if (((_b = (_a = this.realtime) === null || _a === void 0 ? void 0 : _a.lastMessage) === null || _b === void 0 ? void 0 : _b.type) === 'error' && ((_c = this.realtime) === null || _c === void 0 ? void 0 : _c.lastMessage.data).code === 1008) {
|
||||
return;
|
||||
}
|
||||
console.error('Realtime got disconnected. Reconnect will be attempted in 1 second.', event.reason);
|
||||
setTimeout(() => {
|
||||
this.realtime.createSocket();
|
||||
}, 1000);
|
||||
});
|
||||
},
|
||||
authenticate: (event) => {
|
||||
var _a, _b;
|
||||
const message = JSON.parse(event.data);
|
||||
if (message.type === 'connected') {
|
||||
const cookie = JSON.parse((_a = window.localStorage.getItem('cookieFallback')) !== null && _a !== void 0 ? _a : "{}");
|
||||
const session = cookie === null || cookie === void 0 ? void 0 : cookie[`a_session_${this.config.project}`];
|
||||
const data = message.data;
|
||||
if (session && !data.user) {
|
||||
(_b = this.realtime.socket) === null || _b === void 0 ? void 0 : _b.send(JSON.stringify({
|
||||
type: "authentication",
|
||||
data: {
|
||||
session
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
},
|
||||
onMessage: (channel, callback) => (event) => {
|
||||
try {
|
||||
const message = JSON.parse(event.data);
|
||||
this.realtime.lastMessage = message;
|
||||
if (message.type === 'event') {
|
||||
let data = message.data;
|
||||
if (data.channels && data.channels.includes(channel)) {
|
||||
callback(data);
|
||||
}
|
||||
}
|
||||
else if (message.type === 'error') {
|
||||
throw message.data;
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
};
|
||||
this.account = {
|
||||
/**
|
||||
* Get Account
|
||||
|
@ -4117,6 +4188,18 @@
|
|||
*/
|
||||
setEndpoint(endpoint) {
|
||||
this.config.endpoint = endpoint;
|
||||
this.config.endpointRealtime = this.config.endpointRealtime || this.config.endpoint.replace("https://", "wss://").replace("http://", "ws://");
|
||||
return this;
|
||||
}
|
||||
/**
|
||||
* Set Realtime Endpoint
|
||||
*
|
||||
* @param {string} endpointRealtime
|
||||
*
|
||||
* @returns {this}
|
||||
*/
|
||||
setEndpointRealtime(endpointRealtime) {
|
||||
this.config.endpointRealtime = endpointRealtime;
|
||||
return this;
|
||||
}
|
||||
/**
|
||||
|
@ -4185,6 +4268,55 @@
|
|||
this.config.mode = value;
|
||||
return this;
|
||||
}
|
||||
/**
|
||||
* Subscribes to Appwrite events and passes you the payload in realtime.
|
||||
*
|
||||
* @param {string|string[]} channels
|
||||
* Channel to subscribe - pass a single channel as a string or multiple with an array of strings.
|
||||
*
|
||||
* Possible channels are:
|
||||
* - account
|
||||
* - collections
|
||||
* - collections.[ID]
|
||||
* - collections.[ID].documents
|
||||
* - documents
|
||||
* - documents.[ID]
|
||||
* - files
|
||||
* - files.[ID]
|
||||
* - executions
|
||||
* - executions.[ID]
|
||||
* - functions.[ID]
|
||||
* - teams
|
||||
* - teams.[ID]
|
||||
* - memberships
|
||||
* - memberships.[ID]
|
||||
* @param {(payload: RealtimeMessage) => void} callback Is called on every realtime update.
|
||||
* @returns {() => void} Unsubscribes from events.
|
||||
*/
|
||||
subscribe(channels, callback) {
|
||||
let channelArray = typeof channels === 'string' ? [channels] : channels;
|
||||
let savedChannels = [];
|
||||
channelArray.forEach((channel, index) => {
|
||||
if (!(channel in this.realtime.channels)) {
|
||||
this.realtime.channels[channel] = [];
|
||||
}
|
||||
savedChannels[index] = {
|
||||
name: channel,
|
||||
index: (this.realtime.channels[channel].push(this.realtime.onMessage(channel, callback)) - 1)
|
||||
};
|
||||
clearTimeout(this.realtime.timeout);
|
||||
this.realtime.timeout = window === null || window === void 0 ? void 0 : window.setTimeout(() => {
|
||||
this.realtime.createSocket();
|
||||
}, 1);
|
||||
});
|
||||
return () => {
|
||||
savedChannels.forEach(channel => {
|
||||
var _a;
|
||||
(_a = this.realtime.socket) === null || _a === void 0 ? void 0 : _a.removeEventListener('message', this.realtime.channels[channel.name][channel.index]);
|
||||
this.realtime.channels[channel.name].splice(channel.index, 1);
|
||||
});
|
||||
};
|
||||
}
|
||||
call(method, url, headers = {}, params = {}) {
|
||||
var _a, _b;
|
||||
return __awaiter(this, void 0, void 0, function* () {
|
||||
|
|
|
@ -255,6 +255,9 @@ window.ls.filter
|
|||
|
||||
return '';
|
||||
})
|
||||
.add("accessProject", function($value, router) {
|
||||
return ($value && $value.hasOwnProperty(router.params.project)) ? $value[router.params.project] : 0;
|
||||
})
|
||||
;
|
||||
|
||||
function abbreviate(number, maxPlaces, forcePlaces, forceLetter) {
|
||||
|
|
|
@ -52,3 +52,74 @@ document.addEventListener("account.create", function () {
|
|||
window.location = '/auth/signup?failure=1';
|
||||
});
|
||||
});
|
||||
window.addEventListener("load", async () => {
|
||||
const bars = 12;
|
||||
const realtime = window.ls.container.get('realtime');
|
||||
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
|
||||
let current = {};
|
||||
window.ls.container.get('console').subscribe('project', event => {
|
||||
for (let project in event.payload) {
|
||||
current[project] = event.payload[project] ?? 0;
|
||||
}
|
||||
});
|
||||
|
||||
while (true) {
|
||||
let newHistory = {};
|
||||
let createdHistory = false;
|
||||
for (const project in current) {
|
||||
let history = realtime?.history ?? {};
|
||||
|
||||
if (!(project in history)) {
|
||||
history[project] = new Array(bars).fill({
|
||||
percentage: 0,
|
||||
value: 0
|
||||
});
|
||||
}
|
||||
|
||||
history = history[project];
|
||||
history.push({
|
||||
percentage: 0,
|
||||
value: current[project]
|
||||
});
|
||||
if (history.length >= bars) {
|
||||
history.shift();
|
||||
}
|
||||
|
||||
const highest = history.reduce((prev, curr) => {
|
||||
return (curr.value > prev) ? curr.value : prev;
|
||||
}, 0);
|
||||
|
||||
history = history.map(({ percentage, value }) => {
|
||||
createdHistory = true;
|
||||
percentage = value === 0 ? 0 : ((Math.round((value / highest) * 10) / 10) * 100);
|
||||
if (percentage > 100) percentage = 100;
|
||||
else if (percentage == 0 && value != 0) percentage = 5;
|
||||
|
||||
return {
|
||||
percentage: percentage,
|
||||
value: value
|
||||
};
|
||||
})
|
||||
newHistory[project] = history;
|
||||
}
|
||||
|
||||
let currentSnapshot = { ...current };
|
||||
for (let index = .1; index <= 1; index += .05) {
|
||||
let currentTransition = { ...currentSnapshot };
|
||||
for (const project in current) {
|
||||
if (project in newHistory) {
|
||||
let base = newHistory[project][bars - 2].value;
|
||||
let cur = currentSnapshot[project];
|
||||
let offset = (cur - base) * index;
|
||||
currentTransition[project] = base + Math.floor(offset);
|
||||
}
|
||||
}
|
||||
|
||||
realtime.setCurrent(currentTransition);
|
||||
await sleep(250);
|
||||
}
|
||||
|
||||
realtime.setHistory(newHistory);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
20
public/scripts/services/realtime.js
Normal file
20
public/scripts/services/realtime.js
Normal file
|
@ -0,0 +1,20 @@
|
|||
(function (window) {
|
||||
"use strict";
|
||||
|
||||
window.ls.container.set('realtime', () => {
|
||||
return {
|
||||
current: null,
|
||||
history: null,
|
||||
setCurrent: function(currentConnections) {
|
||||
var scope = this;
|
||||
scope.current = currentConnections;
|
||||
return scope.current;
|
||||
},
|
||||
setHistory: function(history) {
|
||||
var scope = this;
|
||||
scope.history = history;
|
||||
return scope.history;
|
||||
}
|
||||
};
|
||||
}, true, true);
|
||||
})(window);
|
42
public/scripts/views/forms/chart-bar.js
Normal file
42
public/scripts/views/forms/chart-bar.js
Normal file
|
@ -0,0 +1,42 @@
|
|||
(function (window) {
|
||||
"use strict";
|
||||
|
||||
window.ls.container.get("view").add({
|
||||
selector: "data-forms-chart-bars",
|
||||
controller: (element) => {
|
||||
let observer = null;
|
||||
let populateChart = () => {
|
||||
let history = element.dataset?.history;
|
||||
if (history == 0) {
|
||||
history = new Array(12).fill({
|
||||
percentage: 0,
|
||||
value: 0
|
||||
});
|
||||
} else {
|
||||
history = JSON.parse(history);
|
||||
}
|
||||
element.innerHTML = '';
|
||||
history.forEach(({ percentage, value }, index) => {
|
||||
const seconds = 60 - (index * 5);
|
||||
const bar = document.createElement('span');
|
||||
bar.classList.add('bar');
|
||||
bar.classList.add(`bar-${percentage}`);
|
||||
bar.classList.add('tooltip');
|
||||
bar.classList.add('down');
|
||||
bar.setAttribute('data-tooltip', `${value} (${seconds} seconds ago)`);
|
||||
element.appendChild(bar);
|
||||
})
|
||||
}
|
||||
if (observer) {
|
||||
observer.disconnect();
|
||||
} else {
|
||||
observer = new MutationObserver(populateChart);
|
||||
observer.observe(element, {
|
||||
attributes: true,
|
||||
attributeFilter: ['data-history']
|
||||
});
|
||||
}
|
||||
populateChart();
|
||||
}
|
||||
});
|
||||
})(window);
|
|
@ -2,10 +2,10 @@
|
|||
position: relative;
|
||||
background: var(--config-color-background-fade);
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 0 3px rgba(0, 0, 0, 0.05);
|
||||
padding: 30px;
|
||||
display: block;
|
||||
border-bottom: none;
|
||||
box-shadow: 0 0 3px rgba(0, 0, 0, 0.05);
|
||||
display: block;
|
||||
padding: 30px;
|
||||
|
||||
&.padding-tiny {
|
||||
padding: 5px;
|
||||
|
|
|
@ -466,7 +466,7 @@ fieldset {
|
|||
white-space: nowrap;
|
||||
background: var(--config-color-tooltip-background);
|
||||
border-radius: 5px;
|
||||
bottom: 26px;
|
||||
bottom: calc(100% + 6px);
|
||||
color: var(--config-color-tooltip-text);
|
||||
content: attr(data-tooltip);
|
||||
padding: 5px 15px;
|
||||
|
@ -483,24 +483,25 @@ fieldset {
|
|||
border: solid;
|
||||
border-color: var(--config-color-tooltip-background) transparent;
|
||||
border-width: 6px 6px 0 6px;
|
||||
bottom: 20px;
|
||||
bottom: 100%;
|
||||
content: "";
|
||||
position: absolute;
|
||||
z-index: 99;
|
||||
.func-start(5px);
|
||||
.func-start(3px);
|
||||
}
|
||||
|
||||
&.down:hover:after {
|
||||
top: 26px;
|
||||
&.down:hover {
|
||||
&:after {
|
||||
top: calc(100% + 6px);
|
||||
bottom: inherit;
|
||||
}
|
||||
|
||||
&.down:hover:before {
|
||||
top: 20px;
|
||||
&:before {
|
||||
top: 100%;
|
||||
border-width: 0 6px 6px 6px;
|
||||
bottom: inherit;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tag {
|
||||
display: inline-block;
|
||||
|
|
|
@ -408,7 +408,7 @@
|
|||
|
||||
.dashboard {
|
||||
padding: 20px;
|
||||
overflow: hidden;
|
||||
overflow: visible;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
margin-bottom: 2px;
|
||||
|
@ -522,6 +522,44 @@
|
|||
padding: 0;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.chart-bar {
|
||||
height: 4rem;
|
||||
width: auto;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
|
||||
@media @desktops {
|
||||
padding-right: 15px;
|
||||
}
|
||||
|
||||
.bar {
|
||||
width: 12.5%;
|
||||
background-color: var(--config-color-chart-fade);
|
||||
margin: 0 2px;
|
||||
border-top: 2px solid var(--config-color-chart);
|
||||
|
||||
&:hover {
|
||||
background-color: var(--config-color-chart);
|
||||
}
|
||||
|
||||
.bar-loop (@i) when (@i > -1) {
|
||||
&.bar-@{i} {
|
||||
height: ~"@{i}%";
|
||||
}
|
||||
&.bar-@{i} when(@i = 0) {
|
||||
border-top: 1px solid var(--config-color-chart);
|
||||
}
|
||||
.bar-loop(@i - 10);
|
||||
}
|
||||
|
||||
.bar-loop (100);
|
||||
|
||||
&.bar-5 {
|
||||
height: 5%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.chart-metric {
|
||||
|
@ -587,10 +625,10 @@
|
|||
}
|
||||
|
||||
&:nth-child(1), &.blue {
|
||||
color: #29b5d9;
|
||||
color: var(--config-color-chart);
|
||||
|
||||
&::before {
|
||||
background: #29b5d9;
|
||||
background: var(--config-color-chart);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -23,12 +23,15 @@
|
|||
--config-color-normal: #40404c;
|
||||
--config-color-dark: #313131;
|
||||
--config-color-fade: #8f8f8f;
|
||||
--config-color-fade-dark: #6e6e6e;
|
||||
--config-color-fade-light: #e2e2e2;
|
||||
--config-color-fade-super: #f1f3f5;
|
||||
--config-color-danger: #f53d3d;
|
||||
--config-color-success: #1bbf61;
|
||||
--config-color-warning: #fffbdd;
|
||||
--config-color-info: #386fd2;
|
||||
--config-color-chart: #29b5d9;
|
||||
--config-color-chart-fade: #d4f0f7;
|
||||
--config-border-color: #f3f3f3;
|
||||
--config-border-fade: #e0e3e4;
|
||||
--config-border-radius: 10px;
|
||||
|
@ -105,12 +108,15 @@
|
|||
--config-color-normal: #c7d8eb;
|
||||
--config-color-dark: #c7d8eb;
|
||||
--config-color-fade: #bec3e0;
|
||||
--config-color-fade-dark: #81859b;
|
||||
--config-color-fade-light: #181818;
|
||||
--config-color-fade-super: #262D50;
|
||||
--config-color-danger: #d84a4a;
|
||||
--config-color-success: #34b86d;
|
||||
--config-color-warning: #e0d56d;
|
||||
--config-color-info: #386fd2;
|
||||
--config-color-chart: #29b5d9;
|
||||
--config-color-chart-fade: #c7d8eb;
|
||||
--config-border-color: #262D50;
|
||||
--config-border-fade: #19203a;
|
||||
--config-prism-background: #1F253F;
|
||||
|
|
|
@ -95,6 +95,10 @@ small {
|
|||
font-size: 13px;
|
||||
}
|
||||
|
||||
.text-size-smaller {
|
||||
font-size: 11.5px;
|
||||
}
|
||||
|
||||
.text-size-xs {
|
||||
font-size: 10px;
|
||||
}
|
||||
|
@ -154,6 +158,10 @@ small {
|
|||
color: var(--config-color-fade);
|
||||
}
|
||||
|
||||
.text-fade-dark {
|
||||
color: var(--config-color-fade-dark);
|
||||
}
|
||||
|
||||
&.text-green {
|
||||
color: var(--config-color-success);
|
||||
}
|
||||
|
|
|
@ -273,4 +273,32 @@ class Auth
|
|||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all roles for a user.
|
||||
*
|
||||
* @param Document $user
|
||||
* @return array
|
||||
*/
|
||||
public static function getRoles(Document $user): array
|
||||
{
|
||||
if ($user->getId()) {
|
||||
$roles[] = 'user:'.$user->getId();
|
||||
$roles[] = 'role:'.Auth::USER_ROLE_MEMBER;
|
||||
} else {
|
||||
return ['role:'.Auth::USER_ROLE_GUEST];
|
||||
}
|
||||
|
||||
foreach ($user->getAttribute('memberships', []) as $node) {
|
||||
if (isset($node['teamId']) && isset($node['roles'])) {
|
||||
$roles[] = 'team:' . $node['teamId'];
|
||||
|
||||
foreach ($node['roles'] as $nodeRole) { // Set all team roles
|
||||
$roles[] = 'team:' . $node['teamId'] . '/' . $nodeRole;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $roles;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -42,6 +42,9 @@ class Database
|
|||
const SYSTEM_COLLECTION_TAGS = 'tags';
|
||||
const SYSTEM_COLLECTION_EXECUTIONS = 'executions';
|
||||
|
||||
// Realtime
|
||||
const SYSTEM_COLLECTION_CONNECTIONS = 'connections';
|
||||
|
||||
// Var Types
|
||||
const SYSTEM_VAR_TYPE_TEXT = 'text';
|
||||
const SYSTEM_VAR_TYPE_NUMERIC = 'numeric';
|
||||
|
|
10
src/Appwrite/Messaging/Adapter.php
Normal file
10
src/Appwrite/Messaging/Adapter.php
Normal file
|
@ -0,0 +1,10 @@
|
|||
<?php
|
||||
|
||||
namespace Appwrite\Messaging;
|
||||
|
||||
abstract class Adapter
|
||||
{
|
||||
public abstract function subscribe(string $projectId, mixed $identifier, array $roles, array $channels): void;
|
||||
public abstract function unsubscribe(mixed $identifier): void;
|
||||
public static abstract function send(string $projectId, array $payload, string $event, array $channels, array $roles, array $options): void;
|
||||
}
|
313
src/Appwrite/Messaging/Adapter/Realtime.php
Normal file
313
src/Appwrite/Messaging/Adapter/Realtime.php
Normal file
|
@ -0,0 +1,313 @@
|
|||
<?php
|
||||
|
||||
namespace Appwrite\Messaging\Adapter;
|
||||
|
||||
use Appwrite\Database\Document;
|
||||
use Appwrite\Messaging\Adapter;
|
||||
use Redis;
|
||||
use Utopia\App;
|
||||
|
||||
class Realtime extends Adapter
|
||||
{
|
||||
/**
|
||||
* Connection Tree
|
||||
*
|
||||
* [CONNECTION_ID] ->
|
||||
* 'projectId' -> [PROJECT_ID]
|
||||
* 'roles' -> [ROLE_x, ROLE_Y]
|
||||
* 'channels' -> [CHANNEL_NAME_X, CHANNEL_NAME_Y, CHANNEL_NAME_Z]
|
||||
*/
|
||||
public array $connections = [];
|
||||
|
||||
/**
|
||||
* Subscription Tree
|
||||
*
|
||||
* [PROJECT_ID] ->
|
||||
* [ROLE_X] ->
|
||||
* [CHANNEL_NAME_X] -> [CONNECTION_ID]
|
||||
* [CHANNEL_NAME_Y] -> [CONNECTION_ID]
|
||||
* [CHANNEL_NAME_Z] -> [CONNECTION_ID]
|
||||
* [ROLE_Y] ->
|
||||
* [CHANNEL_NAME_X] -> [CONNECTION_ID]
|
||||
* [CHANNEL_NAME_Y] -> [CONNECTION_ID]
|
||||
* [CHANNEL_NAME_Z] -> [CONNECTION_ID]
|
||||
*/
|
||||
public array $subscriptions = [];
|
||||
|
||||
/**
|
||||
* Adds a subscription.
|
||||
*
|
||||
* @param string $projectId
|
||||
* @param mixed $identifier
|
||||
* @param array $roles
|
||||
* @param array $channels
|
||||
* @return void
|
||||
*/
|
||||
public function subscribe(string $projectId, mixed $identifier, array $roles, array $channels): void
|
||||
{
|
||||
if (!isset($this->subscriptions[$projectId])) { // Init Project
|
||||
$this->subscriptions[$projectId] = [];
|
||||
}
|
||||
|
||||
foreach ($roles as $role) {
|
||||
if (!isset($this->subscriptions[$projectId][$role])) { // Add user first connection
|
||||
$this->subscriptions[$projectId][$role] = [];
|
||||
}
|
||||
|
||||
foreach ($channels as $channel => $list) {
|
||||
$this->subscriptions[$projectId][$role][$channel][$identifier] = true;
|
||||
}
|
||||
}
|
||||
|
||||
$this->connections[$identifier] = [
|
||||
'projectId' => $projectId,
|
||||
'roles' => $roles,
|
||||
'channels' => $channels
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes Subscription.
|
||||
*
|
||||
* @param mixed $connection
|
||||
* @return void
|
||||
*/
|
||||
public function unsubscribe(mixed $connection): void
|
||||
{
|
||||
$projectId = $this->connections[$connection]['projectId'] ?? '';
|
||||
$roles = $this->connections[$connection]['roles'] ?? [];
|
||||
|
||||
foreach ($roles as $role) {
|
||||
foreach ($this->subscriptions[$projectId][$role] as $channel => $list) {
|
||||
unset($this->subscriptions[$projectId][$role][$channel][$connection]); // Remove connection
|
||||
|
||||
if (empty($this->subscriptions[$projectId][$role][$channel])) {
|
||||
unset($this->subscriptions[$projectId][$role][$channel]); // Remove channel when no connections
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($this->subscriptions[$projectId][$role])) {
|
||||
unset($this->subscriptions[$projectId][$role]); // Remove role when no channels
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($this->subscriptions[$projectId])) { // Remove project when no roles
|
||||
unset($this->subscriptions[$projectId]);
|
||||
}
|
||||
|
||||
unset($this->connections[$connection]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if Channel has a subscriber.
|
||||
* @param string $projectId
|
||||
* @param string $role
|
||||
* @param string $channel
|
||||
* @return bool
|
||||
*/
|
||||
public function hasSubscriber(string $projectId, string $role, string $channel = ''): bool
|
||||
{
|
||||
//TODO: look into moving it to an abstract class in the parent class
|
||||
if (empty($channel)) {
|
||||
return array_key_exists($projectId, $this->subscriptions)
|
||||
&& array_key_exists($role, $this->subscriptions[$projectId]);
|
||||
}
|
||||
|
||||
return array_key_exists($projectId, $this->subscriptions)
|
||||
&& array_key_exists($role, $this->subscriptions[$projectId])
|
||||
&& array_key_exists($channel, $this->subscriptions[$projectId][$role]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends an event to the Realtime Server.
|
||||
* @param string $projectId
|
||||
* @param array $payload
|
||||
* @param string $event
|
||||
* @param array $channels
|
||||
* @param array $roles
|
||||
* @param array $options
|
||||
* @return void
|
||||
*/
|
||||
public static function send(string $projectId, array $payload, string $event, array $channels, array $roles, array $options = []): void
|
||||
{
|
||||
if (empty($channels) || empty($roles) || empty($projectId)) return;
|
||||
|
||||
$permissionsChanged = array_key_exists('permissionsChanged', $options) && $options['permissionsChanged'];
|
||||
$userId = array_key_exists('userId', $options) ? $options['userId'] : null;
|
||||
|
||||
$redis = new \Redis(); //TODO: make this part of the constructor
|
||||
$redis->connect(App::getEnv('_APP_REDIS_HOST', ''), App::getEnv('_APP_REDIS_PORT', ''));
|
||||
$redis->publish('realtime', json_encode([
|
||||
'project' => $projectId,
|
||||
'roles' => $roles,
|
||||
'permissionsChanged' => $permissionsChanged,
|
||||
'userId' => $userId,
|
||||
'data' => [
|
||||
'event' => $event,
|
||||
'channels' => $channels,
|
||||
'timestamp' => time(),
|
||||
'payload' => $payload
|
||||
]
|
||||
]));
|
||||
}
|
||||
|
||||
/**
|
||||
* Identifies the receivers of all subscriptions, based on the permissions and event.
|
||||
*
|
||||
* Example of performance with an event with user:XXX permissions and with X users spread across 10 different channels:
|
||||
* - 0.014 ms (±6.88%) | 10 Connections / 100 Subscriptions
|
||||
* - 0.070 ms (±3.71%) | 100 Connections / 1,000 Subscriptions
|
||||
* - 0.846 ms (±2.74%) | 1,000 Connections / 10,000 Subscriptions
|
||||
* - 10.866 ms (±1.01%) | 10,000 Connections / 100,000 Subscriptions
|
||||
* - 110.201 ms (±2.32%) | 100,000 Connections / 1,000,000 Subscriptions
|
||||
* - 1,121.328 ms (±0.84%) | 1,000,000 Connections / 10,000,000 Subscriptions
|
||||
*
|
||||
* @param array $event
|
||||
*/
|
||||
public function getSubscribers(array $event)
|
||||
{
|
||||
|
||||
$receivers = [];
|
||||
/**
|
||||
* Check if project has subscriber.
|
||||
*/
|
||||
if (isset($this->subscriptions[$event['project']])) {
|
||||
/**
|
||||
* Iterate through each role.
|
||||
*/
|
||||
foreach ($this->subscriptions[$event['project']] as $role => $subscription) {
|
||||
/**
|
||||
* Iterate through each channel.
|
||||
*/
|
||||
foreach ($event['data']['channels'] as $channel) {
|
||||
/**
|
||||
* Check if channel has subscriber. Also taking care of the role in the event and the wildcard role.
|
||||
*/
|
||||
if (
|
||||
\array_key_exists($channel, $this->subscriptions[$event['project']][$role])
|
||||
&& (\in_array($role, $event['roles']) || \in_array('*', $event['roles']))
|
||||
) {
|
||||
/**
|
||||
* Saving all connections that are allowed to receive this event.
|
||||
*/
|
||||
foreach (array_keys($this->subscriptions[$event['project']][$role][$channel]) as $id) {
|
||||
/**
|
||||
* To prevent duplicates, we save the connections as array keys.
|
||||
*/
|
||||
$receivers[$id] = 0;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return array_keys($receivers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the channels from the Query Params into an array.
|
||||
* Also renames the account channel to account.USER_ID and removes all illegal account channel variations.
|
||||
* @param array $channels
|
||||
* @param string $userId
|
||||
* @return array
|
||||
*/
|
||||
public static function convertChannels(array $channels, string $userId): array
|
||||
{
|
||||
$channels = array_flip($channels);
|
||||
|
||||
foreach ($channels as $key => $value) {
|
||||
switch (true) {
|
||||
case strpos($key, 'account.') === 0:
|
||||
unset($channels[$key]);
|
||||
break;
|
||||
|
||||
case $key === 'account':
|
||||
if (!empty($userId)) {
|
||||
$channels['account.' . $userId] = $value;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $channels;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create channels array based on the event name and payload.
|
||||
*
|
||||
* @param string $event
|
||||
* @param Document $payload
|
||||
* @return array
|
||||
*/
|
||||
public static function fromPayload(string $event, Document $payload): array
|
||||
{
|
||||
$channels = [];
|
||||
$roles = [];
|
||||
$permissionsChanged = false;
|
||||
|
||||
switch (true) {
|
||||
case strpos($event, 'account.recovery.') === 0:
|
||||
case strpos($event, 'account.sessions.') === 0:
|
||||
case strpos($event, 'account.verification.') === 0:
|
||||
$channels[] = 'account';
|
||||
$channels[] = 'account.' . $payload->getAttribute('userId');
|
||||
$roles = ['user:' . $payload->getAttribute('userId')];
|
||||
|
||||
break;
|
||||
case strpos($event, 'account.') === 0:
|
||||
$channels[] = 'account';
|
||||
$channels[] = 'account.' . $payload->getId();
|
||||
$roles = ['user:' . $payload->getId()];
|
||||
|
||||
break;
|
||||
case strpos($event, 'teams.memberships') === 0:
|
||||
$permissionsChanged = in_array($event, ['teams.memberships.update', 'teams.memberships.delete', 'teams.memberships.update.status']);
|
||||
$channels[] = 'memberships';
|
||||
$channels[] = 'memberships.' . $payload->getId();
|
||||
$roles = ['team:' . $payload->getAttribute('teamId')];
|
||||
|
||||
break;
|
||||
case strpos($event, 'teams.') === 0:
|
||||
$permissionsChanged = $event === 'teams.create';
|
||||
$channels[] = 'teams';
|
||||
$channels[] = 'teams.' . $payload->getId();
|
||||
$roles = ['team:' . $payload->getId()];
|
||||
|
||||
break;
|
||||
case strpos($event, 'database.collections.') === 0:
|
||||
$channels[] = 'collections';
|
||||
$channels[] = 'collections.' . $payload->getId();
|
||||
$roles = $payload->getAttribute('$permissions.read');
|
||||
|
||||
break;
|
||||
case strpos($event, 'database.documents.') === 0:
|
||||
$channels[] = 'documents';
|
||||
$channels[] = 'collections.' . $payload->getAttribute('$collection') . '.documents';
|
||||
$channels[] = 'documents.' . $payload->getId();
|
||||
$roles = $payload->getAttribute('$permissions.read');
|
||||
|
||||
break;
|
||||
case strpos($event, 'storage.') === 0:
|
||||
$channels[] = 'files';
|
||||
$channels[] = 'files.' . $payload->getId();
|
||||
$roles = $payload->getAttribute('$permissions.read');
|
||||
|
||||
break;
|
||||
case strpos($event, 'functions.executions.') === 0:
|
||||
if (!empty($payload->getAttribute('$permissions.read'))) {
|
||||
$channels[] = 'executions';
|
||||
$channels[] = 'executions.' . $payload->getId();
|
||||
$channels[] = 'functions.' . $payload->getAttribute('functionId');
|
||||
$roles = $payload->getAttribute('$permissions.read');
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return [
|
||||
'channels' => $channels,
|
||||
'roles' => $roles,
|
||||
'permissionsChanged' => $permissionsChanged
|
||||
];
|
||||
}
|
||||
}
|
|
@ -16,6 +16,13 @@ class Execution extends Model
|
|||
'default' => '',
|
||||
'example' => '5e5ea5c16897e',
|
||||
])
|
||||
->addRule('$permissions', [
|
||||
'type' => Response::MODEL_PERMISSIONS,
|
||||
'description' => 'Execution permissions.',
|
||||
'default' => new \stdClass,
|
||||
'example' => new \stdClass,
|
||||
'array' => false,
|
||||
])
|
||||
->addRule('functionId', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'Function ID.',
|
||||
|
|
34
tests/benchmarks/http.js
Normal file
34
tests/benchmarks/http.js
Normal file
|
@ -0,0 +1,34 @@
|
|||
import http from 'k6/http';
|
||||
import { check } from 'k6';
|
||||
import { Counter } from 'k6/metrics';
|
||||
|
||||
// A simple counter for http requests
|
||||
export const requests = new Counter('http_reqs');
|
||||
|
||||
// you can specify stages of your test (ramp up/down patterns) through the options object
|
||||
// target is the number of VUs you are aiming for
|
||||
|
||||
export const options = {
|
||||
stages: [
|
||||
{ target: 50, duration: '1m' },
|
||||
// { target: 15, duration: '1m' },
|
||||
// { target: 0, duration: '1m' },
|
||||
],
|
||||
thresholds: {
|
||||
requests: ['count < 100'],
|
||||
},
|
||||
};
|
||||
|
||||
export default function () {
|
||||
const config = {
|
||||
headers: {
|
||||
'X-Appwrite-Key': '24356eb021863f81eb7dd77c7750304d0464e141cad6e9a8befa1f7d2b066fde190df3dab1e8d2639dbb82ee848da30501424923f4cd80d887ee40ad77ded62763ee489448523f6e39667f290f9a54b2ab8fad131a0bc985e6c0f760015f7f3411e40626c75646bb19d2bb2f7bf2f63130918220a206758cbc48845fd725a695',
|
||||
'X-Appwrite-Project': '60479fe35d95d'
|
||||
}}
|
||||
|
||||
const resDb = http.get('http://localhost:9501/', config);
|
||||
|
||||
check(resDb, {
|
||||
'status is 200': (r) => r.status === 200,
|
||||
});
|
||||
}
|
59
tests/benchmarks/ws.js
Normal file
59
tests/benchmarks/ws.js
Normal file
|
@ -0,0 +1,59 @@
|
|||
// k6 run tests/benchmarks/ws.js
|
||||
|
||||
import { URL } from 'https://jslib.k6.io/url/1.0.0/index.js';
|
||||
import ws from 'k6/ws';
|
||||
import { check } from 'k6';
|
||||
|
||||
export let options = {
|
||||
stages: [
|
||||
{
|
||||
duration: '10s',
|
||||
target: 500
|
||||
},
|
||||
{
|
||||
duration: '1m',
|
||||
target: 500
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
export default function () {
|
||||
// const url = new URL('wss://appwrite-realtime.monitor-api.com/v1/realtime');
|
||||
// url.searchParams.append('project', '604249e6b1a9f');
|
||||
const url = new URL('ws://localhost/v1/realtime');
|
||||
url.searchParams.append('project', '612625394933c');
|
||||
url.searchParams.append('channels[]', 'files');
|
||||
|
||||
const res = ws.connect(url.toString(), function (socket) {
|
||||
let connection = false;
|
||||
let checked = false;
|
||||
let payload = null;
|
||||
socket.on('open', () => {
|
||||
connection = true;
|
||||
});
|
||||
|
||||
socket.on('message', (data) => {
|
||||
payload = data;
|
||||
checked = true;
|
||||
});
|
||||
|
||||
socket.setTimeout(function () {
|
||||
check(payload, {
|
||||
'connection opened': (r) => connection,
|
||||
'message received': (r) => checked,
|
||||
'channels are right': (r) => r === JSON.stringify({
|
||||
"type": "connected",
|
||||
"data": {
|
||||
"channels": [
|
||||
"files"
|
||||
],
|
||||
"user": null
|
||||
}
|
||||
})
|
||||
})
|
||||
socket.close();
|
||||
}, 5000);
|
||||
});
|
||||
|
||||
check(res, { 'status is 101': (r) => r && r.status === 101 });
|
||||
}
|
1104
tests/e2e/Services/Realtime/RealtimeBase.php
Normal file
1104
tests/e2e/Services/Realtime/RealtimeBase.php
Normal file
File diff suppressed because it is too large
Load diff
15
tests/e2e/Services/Realtime/RealtimeCustomClientTest.php
Normal file
15
tests/e2e/Services/Realtime/RealtimeCustomClientTest.php
Normal file
|
@ -0,0 +1,15 @@
|
|||
<?php
|
||||
|
||||
namespace Tests\E2E\Services\Realtime;
|
||||
|
||||
use Tests\E2E\Scopes\Scope;
|
||||
use Tests\E2E\Scopes\ProjectCustom;
|
||||
use Tests\E2E\Scopes\SideClient;
|
||||
|
||||
|
||||
class RealtimeCustomClientTest extends Scope
|
||||
{
|
||||
use RealtimeBase;
|
||||
use ProjectCustom;
|
||||
use SideClient;
|
||||
}
|
|
@ -196,4 +196,48 @@ class AuthTest extends TestCase
|
|||
$this->assertEquals(false, Auth::isAppUser(['role:'.Auth::USER_ROLE_OWNER => true, 'role:'.Auth::USER_ROLE_GUEST => true]));
|
||||
$this->assertEquals(false, Auth::isAppUser(['role:'.Auth::USER_ROLE_OWNER => true, 'role:'.Auth::USER_ROLE_ADMIN => true, 'role:'.Auth::USER_ROLE_DEVELOPER => true]));
|
||||
}
|
||||
|
||||
public function testGuestRoles()
|
||||
{
|
||||
$user = new Document([
|
||||
'$id' => ''
|
||||
]);
|
||||
|
||||
$roles = Auth::getRoles($user);
|
||||
$this->assertCount(1, $roles);
|
||||
$this->assertContains('role:guest', $roles);
|
||||
}
|
||||
|
||||
public function testUserRoles()
|
||||
{
|
||||
$user = new Document([
|
||||
'$id' => '123',
|
||||
'memberships' => [
|
||||
[
|
||||
'teamId' => 'abc',
|
||||
'roles' => [
|
||||
'administrator',
|
||||
'moderator'
|
||||
]
|
||||
],
|
||||
[
|
||||
'teamId' => 'def',
|
||||
'roles' => [
|
||||
'guest'
|
||||
]
|
||||
]
|
||||
]
|
||||
]);
|
||||
|
||||
$roles = Auth::getRoles($user);
|
||||
|
||||
$this->assertCount(7, $roles);
|
||||
$this->assertContains('role:member', $roles);
|
||||
$this->assertContains('user:123', $roles);
|
||||
$this->assertContains('team:abc', $roles);
|
||||
$this->assertContains('team:abc/administrator', $roles);
|
||||
$this->assertContains('team:abc/moderator', $roles);
|
||||
$this->assertContains('team:def', $roles);
|
||||
$this->assertContains('team:def/guest', $roles);
|
||||
}
|
||||
}
|
||||
|
|
306
tests/unit/Messaging/MessagingChannelsTest.php
Normal file
306
tests/unit/Messaging/MessagingChannelsTest.php
Normal file
|
@ -0,0 +1,306 @@
|
|||
<?php
|
||||
|
||||
namespace Appwrite\Tests;
|
||||
|
||||
use Appwrite\Auth\Auth;
|
||||
use Appwrite\Database\Document;
|
||||
use Appwrite\Messaging\Adapter\Realtime;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class MessagingChannelsTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* Configures how many Connections the Test should Mock.
|
||||
*/
|
||||
public $connectionsPerChannel = 10;
|
||||
|
||||
public Realtime $realtime;
|
||||
public $connectionsCount = 0;
|
||||
public $connectionsAuthenticated = 0;
|
||||
public $connectionsGuest = 0;
|
||||
public $connectionsTotal = 0;
|
||||
public $allChannels = [
|
||||
'files',
|
||||
'files.1',
|
||||
'collections',
|
||||
'collections.1',
|
||||
'collections.1.documents',
|
||||
'documents',
|
||||
'documents.1',
|
||||
'executions',
|
||||
'executions.1',
|
||||
'functions.1',
|
||||
];
|
||||
|
||||
public function setUp(): void
|
||||
{
|
||||
/**
|
||||
* Setup global Counts
|
||||
*/
|
||||
$this->connectionsAuthenticated = count($this->allChannels) * $this->connectionsPerChannel;
|
||||
$this->connectionsGuest = count($this->allChannels) * $this->connectionsPerChannel;
|
||||
$this->connectionsTotal = $this->connectionsAuthenticated + $this->connectionsGuest;
|
||||
|
||||
$this->realtime = new Realtime();
|
||||
|
||||
/**
|
||||
* Add Authenticated Clients
|
||||
*/
|
||||
for ($i = 0; $i < $this->connectionsPerChannel; $i++) {
|
||||
foreach ($this->allChannels as $index => $channel) {
|
||||
$user = new Document([
|
||||
'$id' => 'user' . $this->connectionsCount,
|
||||
'memberships' => [
|
||||
[
|
||||
'teamId' => 'team' . $i,
|
||||
'roles' => [
|
||||
empty($index % 2) ? 'admin' : 'member'
|
||||
]
|
||||
]
|
||||
]
|
||||
]);
|
||||
|
||||
$roles = Auth::getRoles($user);
|
||||
|
||||
$parsedChannels = Realtime::convertChannels([0 => $channel], $user->getId());
|
||||
|
||||
$this->realtime->subscribe(
|
||||
'1',
|
||||
$this->connectionsCount,
|
||||
$roles,
|
||||
$parsedChannels
|
||||
);
|
||||
|
||||
$this->connectionsCount++;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add Guest Clients
|
||||
*/
|
||||
for ($i = 0; $i < $this->connectionsPerChannel; $i++) {
|
||||
foreach ($this->allChannels as $index => $channel) {
|
||||
$user = new Document([
|
||||
'$id' => ''
|
||||
]);
|
||||
|
||||
$roles = Auth::getRoles($user);
|
||||
|
||||
$parsedChannels = Realtime::convertChannels([0 => $channel], $user->getId());
|
||||
|
||||
$this->realtime->subscribe(
|
||||
'1',
|
||||
$this->connectionsCount,
|
||||
$roles,
|
||||
$parsedChannels
|
||||
);
|
||||
|
||||
$this->connectionsCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function tearDown(): void
|
||||
{
|
||||
unset($this->realtime);
|
||||
$this->connectionsCount = 0;
|
||||
}
|
||||
|
||||
public function testSubscriptions()
|
||||
{
|
||||
/**
|
||||
* Check for 1 project.
|
||||
*/
|
||||
$this->assertCount(1, $this->realtime->subscriptions);
|
||||
|
||||
/**
|
||||
* Check for correct amount of subscriptions:
|
||||
* - XXX users
|
||||
* - XXX teams
|
||||
* - XXX team roles (2 roles per team)
|
||||
* - 1 role:guest
|
||||
* - 1 role:member
|
||||
*/
|
||||
$this->assertCount(($this->connectionsAuthenticated + (3 * $this->connectionsPerChannel) + 2), $this->realtime->subscriptions['1']);
|
||||
|
||||
/**
|
||||
* Check for connections
|
||||
* - Authenticated
|
||||
* - Guests
|
||||
*/
|
||||
$this->assertCount($this->connectionsTotal, $this->realtime->connections);
|
||||
|
||||
$this->realtime->unsubscribe(-1);
|
||||
|
||||
$this->assertCount($this->connectionsTotal, $this->realtime->connections);
|
||||
$this->assertCount(($this->connectionsAuthenticated + (3 * $this->connectionsPerChannel) + 2), $this->realtime->subscriptions['1']);
|
||||
|
||||
for ($i = 0; $i < $this->connectionsCount; $i++) {
|
||||
$this->realtime->unsubscribe($i);
|
||||
|
||||
$this->assertCount(($this->connectionsCount - $i - 1), $this->realtime->connections);
|
||||
}
|
||||
|
||||
$this->assertEmpty($this->realtime->connections);
|
||||
$this->assertEmpty($this->realtime->subscriptions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests Wildcard (*) Permissions on every channel.
|
||||
*/
|
||||
public function testWildcardPermission()
|
||||
{
|
||||
foreach ($this->allChannels as $index => $channel) {
|
||||
$event = [
|
||||
'project' => '1',
|
||||
'roles' => ['*'],
|
||||
'data' => [
|
||||
'channels' => [
|
||||
0 => $channel,
|
||||
]
|
||||
]
|
||||
];
|
||||
|
||||
$receivers = $this->realtime->getSubscribers($event);
|
||||
|
||||
/**
|
||||
* Every Client subscribed to the Wildcard should receive this event.
|
||||
*/
|
||||
$this->assertCount($this->connectionsTotal / count($this->allChannels), $receivers, $channel);
|
||||
|
||||
foreach ($receivers as $receiver) {
|
||||
/**
|
||||
* Making sure the right clients receive the event.
|
||||
*/
|
||||
$this->assertStringEndsWith($index, $receiver);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function testRolePermissions()
|
||||
{
|
||||
$roles = ['role:guest', 'role:member'];
|
||||
foreach ($this->allChannels as $index => $channel) {
|
||||
foreach ($roles as $role) {
|
||||
$permissions = [$role];
|
||||
|
||||
$event = [
|
||||
'project' => '1',
|
||||
'roles' => $permissions,
|
||||
'data' => [
|
||||
'channels' => [
|
||||
0 => $channel,
|
||||
]
|
||||
]
|
||||
];
|
||||
|
||||
$receivers = $this->realtime->getSubscribers($event);
|
||||
|
||||
/**
|
||||
* Every Role subscribed to a Channel should receive this event.
|
||||
*/
|
||||
$this->assertCount($this->connectionsPerChannel, $receivers, $channel);
|
||||
|
||||
foreach ($receivers as $receiver) {
|
||||
/**
|
||||
* Making sure the right clients receive the event.
|
||||
*/
|
||||
$this->assertStringEndsWith($index, $receiver);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function testUserPermissions()
|
||||
{
|
||||
foreach ($this->allChannels as $index => $channel) {
|
||||
$permissions = [];
|
||||
for ($i = 0; $i < $this->connectionsPerChannel; $i++) {
|
||||
$permissions[] = 'user:user' . (!empty($i) ? $i : '') . $index;
|
||||
}
|
||||
$event = [
|
||||
'project' => '1',
|
||||
'roles' => $permissions,
|
||||
'data' => [
|
||||
'channels' => [
|
||||
0 => $channel,
|
||||
]
|
||||
]
|
||||
];
|
||||
|
||||
$receivers = $this->realtime->getSubscribers($event);
|
||||
|
||||
/**
|
||||
* Every Client subscribed to a Channel should receive this event.
|
||||
*/
|
||||
$this->assertCount($this->connectionsAuthenticated / count($this->allChannels), $receivers, $channel);
|
||||
|
||||
foreach ($receivers as $receiver) {
|
||||
/**
|
||||
* Making sure the right clients receive the event.
|
||||
*/
|
||||
$this->assertStringEndsWith($index, $receiver);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function testTeamPermissions()
|
||||
{
|
||||
foreach ($this->allChannels as $index => $channel) {
|
||||
$permissions = [];
|
||||
|
||||
for ($i = 0; $i < $this->connectionsPerChannel; $i++) {
|
||||
$permissions[] = 'team:team' . $i;
|
||||
}
|
||||
$event = [
|
||||
'project' => '1',
|
||||
'roles' => $permissions,
|
||||
'data' => [
|
||||
'channels' => [
|
||||
0 => $channel,
|
||||
]
|
||||
]
|
||||
];
|
||||
|
||||
$receivers = $this->realtime->getSubscribers($event);
|
||||
|
||||
/**
|
||||
* Every Team Member should receive this event.
|
||||
*/
|
||||
$this->assertCount($this->connectionsAuthenticated / count($this->allChannels), $receivers, $channel);
|
||||
|
||||
foreach ($receivers as $receiver) {
|
||||
/**
|
||||
* Making sure the right clients receive the event.
|
||||
*/
|
||||
$this->assertStringEndsWith($index, $receiver);
|
||||
}
|
||||
|
||||
$permissions = ['team:team' . $index . '/' . (empty($index % 2) ? 'admin' : 'member')];
|
||||
|
||||
$event = [
|
||||
'project' => '1',
|
||||
'roles' => $permissions,
|
||||
'data' => [
|
||||
'channels' => [
|
||||
0 => $channel,
|
||||
]
|
||||
]
|
||||
];
|
||||
|
||||
$receivers = $this->realtime->getSubscribers($event);
|
||||
|
||||
/**
|
||||
* Only 1 Team Member of a role should have access to a specific channel.
|
||||
*/
|
||||
$this->assertCount(1, $receivers, $channel);
|
||||
|
||||
foreach ($receivers as $receiver) {
|
||||
/**
|
||||
* Making sure the right clients receive the event.
|
||||
*/
|
||||
$this->assertStringEndsWith($index, $receiver);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
128
tests/unit/Messaging/MessagingGuestTest.php
Normal file
128
tests/unit/Messaging/MessagingGuestTest.php
Normal file
|
@ -0,0 +1,128 @@
|
|||
<?php
|
||||
|
||||
namespace Appwrite\Tests;
|
||||
|
||||
use Appwrite\Messaging\Adapter\Realtime;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class MessagingGuestTest extends TestCase
|
||||
{
|
||||
public function testGuest()
|
||||
{
|
||||
$realtime = new Realtime();
|
||||
|
||||
$realtime->subscribe(
|
||||
'1',
|
||||
1,
|
||||
['role:guest'],
|
||||
['files' => 0, 'documents' => 0, 'documents.789' => 0, 'account.123' => 0]
|
||||
);
|
||||
|
||||
$event = [
|
||||
'project' => '1',
|
||||
'roles' => ['*'],
|
||||
'data' => [
|
||||
'channels' => [
|
||||
0 => 'documents',
|
||||
1 => 'documents',
|
||||
]
|
||||
]
|
||||
];
|
||||
|
||||
$receivers = $realtime->getSubscribers($event);
|
||||
|
||||
$this->assertCount(1, $receivers);
|
||||
$this->assertEquals(1, $receivers[0]);
|
||||
|
||||
$event['roles'] = ['role:guest'];
|
||||
|
||||
$receivers = $realtime->getSubscribers($event);
|
||||
|
||||
$this->assertCount(1, $receivers);
|
||||
$this->assertEquals(1, $receivers[0]);
|
||||
|
||||
$event['roles'] = ['role:member'];
|
||||
|
||||
$receivers = $realtime->getSubscribers($event);
|
||||
|
||||
$this->assertEmpty($receivers);
|
||||
|
||||
$event['roles'] = ['user:123'];
|
||||
|
||||
$receivers = $realtime->getSubscribers($event);
|
||||
|
||||
$this->assertEmpty($receivers);
|
||||
|
||||
$event['roles'] = ['team:abc'];
|
||||
|
||||
$receivers = $realtime->getSubscribers($event);
|
||||
|
||||
$this->assertEmpty($receivers);
|
||||
|
||||
$event['roles'] = ['team:abc/administrator'];
|
||||
|
||||
$receivers = $realtime->getSubscribers($event);
|
||||
|
||||
$this->assertEmpty($receivers);
|
||||
|
||||
$event['roles'] = ['team:abc/god'];
|
||||
|
||||
$receivers = $realtime->getSubscribers($event);
|
||||
|
||||
$this->assertEmpty($receivers);
|
||||
|
||||
$event['roles'] = ['team:def'];
|
||||
|
||||
$receivers = $realtime->getSubscribers($event);
|
||||
|
||||
$this->assertEmpty($receivers);
|
||||
|
||||
$event['roles'] = ['team:def/guest'];
|
||||
|
||||
$receivers = $realtime->getSubscribers($event);
|
||||
|
||||
$this->assertEmpty($receivers);
|
||||
|
||||
$event['roles'] = ['user:456'];
|
||||
|
||||
$receivers = $realtime->getSubscribers($event);
|
||||
|
||||
$this->assertEmpty($receivers);
|
||||
|
||||
$event['roles'] = ['team:def/member'];
|
||||
|
||||
$receivers = $realtime->getSubscribers($event);
|
||||
|
||||
$this->assertEmpty($receivers);
|
||||
|
||||
$event['roles'] = ['*'];
|
||||
$event['data']['channels'] = ['documents.123'];
|
||||
|
||||
$receivers = $realtime->getSubscribers($event);
|
||||
|
||||
$this->assertEmpty($receivers);
|
||||
|
||||
$event['data']['channels'] = ['documents.789'];
|
||||
|
||||
$receivers = $realtime->getSubscribers($event);
|
||||
|
||||
$this->assertCount(1, $receivers);
|
||||
$this->assertEquals(1, $receivers[0]);
|
||||
|
||||
$event['project'] = '2';
|
||||
|
||||
$receivers = $realtime->getSubscribers($event);
|
||||
|
||||
$this->assertEmpty($receivers);
|
||||
|
||||
$realtime->unsubscribe(2);
|
||||
|
||||
$this->assertCount(1, $realtime->connections);
|
||||
$this->assertCount(1, $realtime->subscriptions['1']);
|
||||
|
||||
$realtime->unsubscribe(1);
|
||||
|
||||
$this->assertEmpty($realtime->connections);
|
||||
$this->assertEmpty($realtime->subscriptions);
|
||||
}
|
||||
}
|
198
tests/unit/Messaging/MessagingTest.php
Normal file
198
tests/unit/Messaging/MessagingTest.php
Normal file
|
@ -0,0 +1,198 @@
|
|||
<?php
|
||||
|
||||
namespace Appwrite\Tests;
|
||||
|
||||
use Appwrite\Database\Document;
|
||||
use Appwrite\Messaging\Adapter\Realtime;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class MessagingTest extends TestCase
|
||||
{
|
||||
public function setUp(): void
|
||||
{
|
||||
}
|
||||
|
||||
public function tearDown(): void
|
||||
{
|
||||
}
|
||||
|
||||
public function testUser()
|
||||
{
|
||||
$realtime = new Realtime();
|
||||
|
||||
$realtime->subscribe(
|
||||
'1',
|
||||
1,
|
||||
['user:123', 'role:member', 'team:abc', 'team:abc/administrator', 'team:abc/moderator', 'team:def', 'team:def/guest'],
|
||||
['files' => 0, 'documents' => 0, 'documents.789' => 0, 'account.123' => 0]
|
||||
);
|
||||
|
||||
$event = [
|
||||
'project' => '1',
|
||||
'roles' => ['*'],
|
||||
'data' => [
|
||||
'channels' => [
|
||||
0 => 'account.123',
|
||||
]
|
||||
]
|
||||
];
|
||||
|
||||
$receivers = $realtime->getSubscribers($event);
|
||||
|
||||
$this->assertCount(1, $receivers);
|
||||
$this->assertEquals(1, $receivers[0]);
|
||||
|
||||
$event['roles'] = ['role:member'];
|
||||
|
||||
$receivers = $realtime->getSubscribers($event);
|
||||
|
||||
$this->assertCount(1, $receivers);
|
||||
$this->assertEquals(1, $receivers[0]);
|
||||
|
||||
$event['roles'] = ['user:123'];
|
||||
|
||||
$receivers = $realtime->getSubscribers($event);
|
||||
|
||||
$this->assertCount(1, $receivers);
|
||||
$this->assertEquals(1, $receivers[0]);
|
||||
|
||||
$event['roles'] = ['team:abc'];
|
||||
|
||||
$receivers = $realtime->getSubscribers($event);
|
||||
|
||||
$this->assertCount(1, $receivers);
|
||||
$this->assertEquals(1, $receivers[0]);
|
||||
|
||||
$event['roles'] = ['team:abc/administrator'];
|
||||
|
||||
$receivers = $realtime->getSubscribers($event);
|
||||
|
||||
$this->assertCount(1, $receivers);
|
||||
$this->assertEquals(1, $receivers[0]);
|
||||
|
||||
$event['roles'] = ['team:abc/moderator'];
|
||||
|
||||
$receivers = $realtime->getSubscribers($event);
|
||||
|
||||
$this->assertCount(1, $receivers);
|
||||
$this->assertEquals(1, $receivers[0]);
|
||||
|
||||
$event['roles'] = ['team:def'];
|
||||
|
||||
$receivers = $realtime->getSubscribers($event);
|
||||
|
||||
$this->assertCount(1, $receivers);
|
||||
$this->assertEquals(1, $receivers[0]);
|
||||
|
||||
$event['roles'] = ['team:def/guest'];
|
||||
|
||||
$receivers = $realtime->getSubscribers($event);
|
||||
|
||||
$this->assertCount(1, $receivers);
|
||||
$this->assertEquals(1, $receivers[0]);
|
||||
|
||||
$event['roles'] = ['user:456'];
|
||||
|
||||
$receivers = $realtime->getSubscribers($event);
|
||||
|
||||
$this->assertEmpty($receivers);
|
||||
|
||||
$event['roles'] = ['team:def/member'];
|
||||
|
||||
$receivers = $realtime->getSubscribers($event);
|
||||
|
||||
$this->assertEmpty($receivers);
|
||||
|
||||
$event['roles'] = ['*'];
|
||||
$event['data']['channels'] = ['documents.123'];
|
||||
|
||||
$receivers = $realtime->getSubscribers($event);
|
||||
|
||||
$this->assertEmpty($receivers);
|
||||
|
||||
$event['data']['channels'] = ['documents.789'];
|
||||
|
||||
$receivers = $realtime->getSubscribers($event);
|
||||
|
||||
$this->assertCount(1, $receivers);
|
||||
$this->assertEquals(1, $receivers[0]);
|
||||
|
||||
$event['project'] = '2';
|
||||
|
||||
$receivers = $realtime->getSubscribers($event);
|
||||
|
||||
$this->assertEmpty($receivers);
|
||||
|
||||
$realtime->unsubscribe(2);
|
||||
|
||||
$this->assertCount(1, $realtime->connections);
|
||||
$this->assertCount(7, $realtime->subscriptions['1']);
|
||||
|
||||
$realtime->unsubscribe(1);
|
||||
|
||||
$this->assertEmpty($realtime->connections);
|
||||
$this->assertEmpty($realtime->subscriptions);
|
||||
}
|
||||
|
||||
public function testConvertChannelsGuest()
|
||||
{
|
||||
$user = new Document([
|
||||
'$id' => ''
|
||||
]);
|
||||
|
||||
$channels = [
|
||||
0 => 'files',
|
||||
1 => 'documents',
|
||||
2 => 'documents.789',
|
||||
3 => 'account',
|
||||
4 => 'account.456'
|
||||
];
|
||||
|
||||
$channels = Realtime::convertChannels($channels, $user->getId());
|
||||
$this->assertCount(4, $channels);
|
||||
$this->assertArrayHasKey('files', $channels);
|
||||
$this->assertArrayHasKey('documents', $channels);
|
||||
$this->assertArrayHasKey('documents.789', $channels);
|
||||
$this->assertArrayHasKey('account', $channels);
|
||||
$this->assertArrayNotHasKey('account.456', $channels);
|
||||
}
|
||||
|
||||
public function testConvertChannelsUser()
|
||||
{
|
||||
$user = new Document([
|
||||
'$id' => '123',
|
||||
'memberships' => [
|
||||
[
|
||||
'teamId' => 'abc',
|
||||
'roles' => [
|
||||
'administrator',
|
||||
'moderator'
|
||||
]
|
||||
],
|
||||
[
|
||||
'teamId' => 'def',
|
||||
'roles' => [
|
||||
'guest'
|
||||
]
|
||||
]
|
||||
]
|
||||
]);
|
||||
$channels = [
|
||||
0 => 'files',
|
||||
1 => 'documents',
|
||||
2 => 'documents.789',
|
||||
3 => 'account',
|
||||
4 => 'account.456'
|
||||
];
|
||||
|
||||
$channels = Realtime::convertChannels($channels, $user->getId());
|
||||
|
||||
$this->assertCount(5, $channels);
|
||||
$this->assertArrayHasKey('files', $channels);
|
||||
$this->assertArrayHasKey('documents', $channels);
|
||||
$this->assertArrayHasKey('documents.789', $channels);
|
||||
$this->assertArrayHasKey('account.123', $channels);
|
||||
$this->assertArrayHasKey('account', $channels);
|
||||
$this->assertArrayNotHasKey('account.456', $channels);
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue