1
0
Fork 0
mirror of synced 2024-05-25 15:09:47 +12:00
appwrite/src/Appwrite/Messaging/Adapter/Realtime.php

332 lines
12 KiB
PHP
Raw Normal View History

2021-06-29 02:34:28 +12:00
<?php
namespace Appwrite\Messaging\Adapter;
2021-09-30 23:32:24 +13:00
use Utopia\Database\Document;
2021-06-29 02:34:28 +12:00
use Appwrite\Messaging\Adapter;
2021-06-30 23:36:58 +12:00
use Utopia\App;
2021-06-29 02:34:28 +12:00
class Realtime extends Adapter
{
/**
* Connection Tree
2022-05-24 02:54:50 +12:00
*
2022-05-11 01:32:16 +12:00
* [CONNECTION_ID] ->
2021-06-29 02:34:28 +12:00
* 'projectId' -> [PROJECT_ID]
* 'roles' -> [ROLE_x, ROLE_Y]
* 'channels' -> [CHANNEL_NAME_X, CHANNEL_NAME_Y, CHANNEL_NAME_Z]
*/
public array $connections = [];
/**
* Subscription Tree
2022-05-11 01:32:16 +12:00
*
* [PROJECT_ID] ->
* [ROLE_X] ->
2021-06-29 02:34:28 +12:00
* [CHANNEL_NAME_X] -> [CONNECTION_ID]
* [CHANNEL_NAME_Y] -> [CONNECTION_ID]
* [CHANNEL_NAME_Z] -> [CONNECTION_ID]
2022-05-11 01:32:16 +12:00
* [ROLE_Y] ->
2021-06-29 02:34:28 +12:00
* [CHANNEL_NAME_X] -> [CONNECTION_ID]
* [CHANNEL_NAME_Y] -> [CONNECTION_ID]
* [CHANNEL_NAME_Z] -> [CONNECTION_ID]
*/
public array $subscriptions = [];
/**
* Adds a subscription.
2022-05-11 01:32:16 +12:00
*
* @param string $projectId
* @param mixed $identifier
* @param array $roles
* @param array $channels
* @return void
2021-06-29 02:34:28 +12:00
*/
2021-08-19 20:24:41 +12:00
public function subscribe(string $projectId, mixed $identifier, array $roles, array $channels): void
2021-06-29 02:34:28 +12:00
{
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) {
2021-08-19 20:24:41 +12:00
$this->subscriptions[$projectId][$role][$channel][$identifier] = true;
2021-06-29 02:34:28 +12:00
}
}
2021-08-19 20:24:41 +12:00
$this->connections[$identifier] = [
2021-06-29 02:34:28 +12:00
'projectId' => $projectId,
'roles' => $roles,
'channels' => $channels
];
}
/**
2021-07-14 03:18:02 +12:00
* Removes Subscription.
2022-05-24 02:54:50 +12:00
*
2021-06-29 02:34:28 +12:00
* @param mixed $connection
2022-05-11 01:32:16 +12:00
* @return void
2021-06-29 02:34:28 +12:00
*/
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.
2022-05-11 01:32:16 +12:00
* @param string $projectId
* @param string $role
* @param string $channel
2021-07-14 03:18:02 +12:00
* @return bool
2021-06-29 02:34:28 +12:00
*/
public function hasSubscriber(string $projectId, string $role, string $channel = ''): bool
{
2021-07-14 03:18:02 +12:00
//TODO: look into moving it to an abstract class in the parent class
2021-06-29 02:34:28 +12:00
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]);
}
/**
2022-05-11 01:32:16 +12:00
* 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
2021-06-29 02:34:28 +12:00
*/
2022-04-19 04:21:45 +12:00
public static function send(string $projectId, array $payload, array $events, array $channels, array $roles, array $options = []): void
2021-06-29 02:34:28 +12:00
{
2022-05-24 02:54:50 +12:00
if (empty($channels) || empty($roles) || empty($projectId)) {
return;
}
2021-06-30 23:36:58 +12:00
$permissionsChanged = array_key_exists('permissionsChanged', $options) && $options['permissionsChanged'];
$userId = array_key_exists('userId', $options) ? $options['userId'] : null;
2021-07-14 03:18:02 +12:00
$redis = new \Redis(); //TODO: make this part of the constructor
2021-06-30 23:36:58 +12:00
$redis->connect(App::getEnv('_APP_REDIS_HOST', ''), App::getEnv('_APP_REDIS_PORT', ''));
$redis->publish('realtime', json_encode([
2021-08-19 20:24:41 +12:00
'project' => $projectId,
2021-07-14 03:18:02 +12:00
'roles' => $roles,
2021-06-30 23:36:58 +12:00
'permissionsChanged' => $permissionsChanged,
'userId' => $userId,
'data' => [
2022-04-19 04:21:45 +12:00
'events' => $events,
2021-06-30 23:36:58 +12:00
'channels' => $channels,
'timestamp' => time(),
'payload' => $payload
]
]));
2021-06-29 02:34:28 +12:00
}
/**
* Identifies the receivers of all subscriptions, based on the permissions and event.
2022-05-11 01:32:16 +12:00
*
2021-06-29 02:34:28 +12:00
* Example of performance with an event with user:XXX permissions and with X users spread across 10 different channels:
2022-05-24 02:54:50 +12:00
* - 0.014 ms (±6.88%) | 10 Connections / 100 Subscriptions
* - 0.070 ms (±3.71%) | 100 Connections / 1,000 Subscriptions
2021-06-29 02:34:28 +12:00
* - 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
2022-05-24 02:54:50 +12:00
* - 1,121.328 ms (±0.84%) | 1,000,000 Connections / 10,000,000 Subscriptions
2022-05-11 01:32:16 +12:00
*
2021-06-29 02:34:28 +12:00
* @param array $event
*/
2021-07-14 03:18:02 +12:00
public function getSubscribers(array $event)
2021-06-29 02:34:28 +12:00
{
2021-08-19 20:34:32 +12:00
2021-06-29 02:34:28 +12:00
$receivers = [];
2021-08-19 20:34:32 +12:00
/**
* Check if project has subscriber.
*/
2021-06-29 02:34:28 +12:00
if (isset($this->subscriptions[$event['project']])) {
2021-08-19 20:34:32 +12:00
/**
* Iterate through each role.
*/
2021-06-29 02:34:28 +12:00
foreach ($this->subscriptions[$event['project']] as $role => $subscription) {
2021-08-19 20:34:32 +12:00
/**
* Iterate through each channel.
*/
2021-06-29 02:34:28 +12:00
foreach ($event['data']['channels'] as $channel) {
2021-08-19 20:34:32 +12:00
/**
* Check if channel has subscriber. Also taking care of the role in the event and the wildcard role.
*/
2021-06-29 02:34:28 +12:00
if (
\array_key_exists($channel, $this->subscriptions[$event['project']][$role])
2021-10-08 04:35:17 +13:00
&& (\in_array($role, $event['roles']) || \in_array('role:all', $event['roles']))
2021-06-29 02:34:28 +12:00
) {
2021-08-19 20:34:32 +12:00
/**
* Saving all connections that are allowed to receive this event.
*/
2021-07-14 03:18:02 +12:00
foreach (array_keys($this->subscriptions[$event['project']][$role][$channel]) as $id) {
2021-08-19 20:34:32 +12:00
/**
* To prevent duplicates, we save the connections as array keys.
*/
2021-07-14 03:18:02 +12:00
$receivers[$id] = 0;
2021-06-29 02:34:28 +12:00
}
break;
}
}
}
}
return array_keys($receivers);
}
2021-06-30 23:36:58 +12:00
/**
2022-05-11 01:32:16 +12:00
* Converts the channels from the Query Params into an array.
2021-06-30 23:36:58 +12:00
* Also renames the account channel to account.USER_ID and removes all illegal account channel variations.
2022-05-11 01:32:16 +12:00
* @param array $channels
* @param string $userId
2022-05-24 02:54:50 +12:00
* @return array
2021-06-30 23:36:58 +12:00
*/
2021-07-14 03:18:02 +12:00
public static function convertChannels(array $channels, string $userId): array
2021-06-30 23:36:58 +12:00
{
$channels = array_flip($channels);
foreach ($channels as $key => $value) {
switch (true) {
case strpos($key, 'account.') === 0:
unset($channels[$key]);
break;
case $key === 'account':
2021-07-14 03:18:02 +12:00
if (!empty($userId)) {
$channels['account.' . $userId] = $value;
2021-06-30 23:36:58 +12:00
}
break;
}
}
return $channels;
}
/**
* Create channels array based on the event name and payload.
2021-12-07 01:03:12 +13:00
*
2022-05-11 01:32:16 +12:00
* @param string $event
* @param Document $payload
* @param Document|null $project
2022-05-24 02:54:50 +12:00
* @return array
2021-06-30 23:36:58 +12:00
*/
Database layer (#3338) * database response model * database collection config * new database scopes * database service update * database execption codes * remove read write permission from database model * updating tests and fixing some bugs * server side tests are now passing * databases api * tests for database endpoint * composer update * fix error * formatting * formatting fixes * get database test * more updates to events and usage * more usage updates * fix delete type * fix test * delete database * more fixes * databaseId in attributes and indexes * more fixes * fix issues * fix index subquery * fix console scope and index query * updating tests as required * fix phpcs errors and warnings * updates to review suggestions * UI progress * ui updates and cleaning up * fix type * rework database events * update tests * update types * event generation fixed * events config updated * updating context to support multiple * realtime updates * fix ids * update context * validator updates * fix naming conflict * fix tests * fix lint errors * fix wprler and realtime tests * fix webhooks test * fix event validator and other tests * formatting fixes * removing leftover var_dumps * remove leftover comment * update usage params * usage metrics updates * update database usage * fix usage * specs update * updates to usage * fix UI and usage * fix lints * internal id fixes * fixes for internal Id * renaming services and related files * rename tests * rename doc link * rename readme * fix test name * tests: fixes for 0.15.x sync Co-authored-by: Torsten Dittmann <torsten.dittmann@googlemail.com>
2022-06-22 22:51:49 +12:00
public static function fromPayload(string $event, Document $payload, Document $project = null, Document $database = null, Document $collection = null, Document $bucket = null): array
2021-06-30 23:36:58 +12:00
{
$channels = [];
2021-07-14 03:18:02 +12:00
$roles = [];
2021-06-30 23:36:58 +12:00
$permissionsChanged = false;
2021-12-07 01:03:12 +13:00
$projectId = null;
2022-05-11 01:25:49 +12:00
// TODO: add method here to remove all the magic index accesses
$parts = explode('.', $event);
2021-06-30 23:36:58 +12:00
switch ($parts[0]) {
case 'users':
$channels[] = 'account';
$channels[] = 'account.' . $parts[1];
$roles = ['user:' . $parts[1]];
2021-06-30 23:36:58 +12:00
break;
case 'teams':
if ($parts[2] === 'memberships') {
$permissionsChanged = $parts[4] ?? false;
$channels[] = 'memberships';
$channels[] = 'memberships.' . $parts[3];
$roles = ['team:' . $parts[1]];
} else {
$permissionsChanged = $parts[2] === 'create';
$channels[] = 'teams';
$channels[] = 'teams.' . $parts[1];
$roles = ['team:' . $parts[1]];
}
2021-06-30 23:36:58 +12:00
break;
Database layer (#3338) * database response model * database collection config * new database scopes * database service update * database execption codes * remove read write permission from database model * updating tests and fixing some bugs * server side tests are now passing * databases api * tests for database endpoint * composer update * fix error * formatting * formatting fixes * get database test * more updates to events and usage * more usage updates * fix delete type * fix test * delete database * more fixes * databaseId in attributes and indexes * more fixes * fix issues * fix index subquery * fix console scope and index query * updating tests as required * fix phpcs errors and warnings * updates to review suggestions * UI progress * ui updates and cleaning up * fix type * rework database events * update tests * update types * event generation fixed * events config updated * updating context to support multiple * realtime updates * fix ids * update context * validator updates * fix naming conflict * fix tests * fix lint errors * fix wprler and realtime tests * fix webhooks test * fix event validator and other tests * formatting fixes * removing leftover var_dumps * remove leftover comment * update usage params * usage metrics updates * update database usage * fix usage * specs update * updates to usage * fix UI and usage * fix lints * internal id fixes * fixes for internal Id * renaming services and related files * rename tests * rename doc link * rename readme * fix test name * tests: fixes for 0.15.x sync Co-authored-by: Torsten Dittmann <torsten.dittmann@googlemail.com>
2022-06-22 22:51:49 +12:00
case 'databases':
if (in_array($parts[4] ?? [], ['attributes', 'indexes'])) {
$channels[] = 'console';
$projectId = 'console';
$roles = ['team:' . $project->getAttribute('teamId')];
Database layer (#3338) * database response model * database collection config * new database scopes * database service update * database execption codes * remove read write permission from database model * updating tests and fixing some bugs * server side tests are now passing * databases api * tests for database endpoint * composer update * fix error * formatting * formatting fixes * get database test * more updates to events and usage * more usage updates * fix delete type * fix test * delete database * more fixes * databaseId in attributes and indexes * more fixes * fix issues * fix index subquery * fix console scope and index query * updating tests as required * fix phpcs errors and warnings * updates to review suggestions * UI progress * ui updates and cleaning up * fix type * rework database events * update tests * update types * event generation fixed * events config updated * updating context to support multiple * realtime updates * fix ids * update context * validator updates * fix naming conflict * fix tests * fix lint errors * fix wprler and realtime tests * fix webhooks test * fix event validator and other tests * formatting fixes * removing leftover var_dumps * remove leftover comment * update usage params * usage metrics updates * update database usage * fix usage * specs update * updates to usage * fix UI and usage * fix lints * internal id fixes * fixes for internal Id * renaming services and related files * rename tests * rename doc link * rename readme * fix test name * tests: fixes for 0.15.x sync Co-authored-by: Torsten Dittmann <torsten.dittmann@googlemail.com>
2022-06-22 22:51:49 +12:00
} elseif (($parts[4] ?? '') === 'documents') {
if ($database->isEmpty()) {
throw new \Exception('Database needs to be passed to Realtime for Document events in the Database.');
}
if ($collection->isEmpty()) {
throw new \Exception('Collection needs to be passed to Realtime for Document events in the Database.');
}
2021-06-30 23:36:58 +12:00
$channels[] = 'documents';
Database layer (#3338) * database response model * database collection config * new database scopes * database service update * database execption codes * remove read write permission from database model * updating tests and fixing some bugs * server side tests are now passing * databases api * tests for database endpoint * composer update * fix error * formatting * formatting fixes * get database test * more updates to events and usage * more usage updates * fix delete type * fix test * delete database * more fixes * databaseId in attributes and indexes * more fixes * fix issues * fix index subquery * fix console scope and index query * updating tests as required * fix phpcs errors and warnings * updates to review suggestions * UI progress * ui updates and cleaning up * fix type * rework database events * update tests * update types * event generation fixed * events config updated * updating context to support multiple * realtime updates * fix ids * update context * validator updates * fix naming conflict * fix tests * fix lint errors * fix wprler and realtime tests * fix webhooks test * fix event validator and other tests * formatting fixes * removing leftover var_dumps * remove leftover comment * update usage params * usage metrics updates * update database usage * fix usage * specs update * updates to usage * fix UI and usage * fix lints * internal id fixes * fixes for internal Id * renaming services and related files * rename tests * rename doc link * rename readme * fix test name * tests: fixes for 0.15.x sync Co-authored-by: Torsten Dittmann <torsten.dittmann@googlemail.com>
2022-06-22 22:51:49 +12:00
$channels[] = 'databases.' . $database->getId() . '.collections.' . $payload->getCollection() . '.documents';
$channels[] = 'databases.' . $database->getId() . '.collections.' . $payload->getCollection() . '.documents.' . $payload->getId();
2021-12-07 01:03:12 +13:00
$roles = ($collection->getAttribute('permission') === 'collection') ? $collection->getRead() : $payload->getRead();
2021-12-17 07:12:06 +13:00
}
2021-06-30 23:36:58 +12:00
break;
case 'buckets':
if ($parts[2] === 'files') {
2022-04-19 04:21:45 +12:00
if ($bucket->isEmpty()) {
throw new \Exception('Bucket needs to be pased to Realtime for File events in the Storage.');
}
$channels[] = 'files';
$channels[] = 'buckets.' . $payload->getAttribute('bucketId') . '.files';
$channels[] = 'buckets.' . $payload->getAttribute('bucketId') . '.files.' . $payload->getId();
2022-04-19 04:21:45 +12:00
$roles = ($bucket->getAttribute('permission') === 'bucket') ? $bucket->getRead() : $payload->getRead();
2022-02-13 20:02:34 +13:00
}
2021-06-30 23:36:58 +12:00
break;
case 'functions':
if ($parts[2] === 'executions') {
if (!empty($payload->getRead())) {
$channels[] = 'console';
$channels[] = 'executions';
$channels[] = 'executions.' . $payload->getId();
$channels[] = 'functions.' . $payload->getAttribute('functionId');
$roles = $payload->getRead();
}
} elseif ($parts[2] === 'deployments') {
2022-03-01 01:24:35 +13:00
$channels[] = 'console';
$roles = ['team:' . $project->getAttribute('teamId')];
2021-06-30 23:36:58 +12:00
}
2021-06-30 23:36:58 +12:00
break;
}
return [
'channels' => $channels,
2021-07-14 03:18:02 +12:00
'roles' => $roles,
2021-12-07 01:03:12 +13:00
'permissionsChanged' => $permissionsChanged,
'projectId' => $projectId
2021-06-30 23:36:58 +12:00
];
}
2021-06-29 02:34:28 +12:00
}