From 10e39abeaeae11f9b3cda3377ba3e7df52ca399f Mon Sep 17 00:00:00 2001 From: Eldad Fux Date: Fri, 16 Oct 2020 10:31:09 +0300 Subject: [PATCH 001/267] Basic structure --- Dockerfile | 1 + app/preload.php | 1 + app/realtime.php | 40 +++++++++++ bin/realtime | 3 + composer.json | 2 +- composer.lock | 172 ++++++++++++++++++++++----------------------- docker-compose.yml | 50 +++++++++++-- 7 files changed, 177 insertions(+), 92 deletions(-) create mode 100644 app/realtime.php create mode 100644 bin/realtime diff --git a/Dockerfile b/Dockerfile index 75c03c3e05..5f53a82cb9 100755 --- a/Dockerfile +++ b/Dockerfile @@ -146,6 +146,7 @@ RUN mkdir -p /storage/uploads && \ RUN chmod +x /usr/local/bin/doctor && \ 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/ssl && \ chmod +x /usr/local/bin/test && \ diff --git a/app/preload.php b/app/preload.php index 63d08c3e13..8ddbffbb73 100644 --- a/app/preload.php +++ b/app/preload.php @@ -26,6 +26,7 @@ include __DIR__.'/controllers/general.php'; ->paths(realpath(__DIR__ . '/../src')) ->ignore(realpath(__DIR__ . '/../vendor/twig/twig')) ->ignore(realpath(__DIR__ . '/../vendor/guzzlehttp/guzzle')) + ->ignore(realpath(__DIR__ . '/../vendor/domnikl')) ->ignore(realpath(__DIR__ . '/../vendor/geoip2')) ->ignore(realpath(__DIR__ . '/../vendor/maxmind')) ->ignore(realpath(__DIR__ . '/../vendor/maxmind-db')) diff --git a/app/realtime.php b/app/realtime.php new file mode 100644 index 0000000000..648c6b4e9c --- /dev/null +++ b/app/realtime.php @@ -0,0 +1,40 @@ + } + * - JWT Authentication (in path / or in message) + */ + +$server = new Server("0.0.0.0", 80); + +$server->on("start", function (Server $server) { + echo "Swoole WebSocket Server has started at http://127.0.0.1:3000\n"; +}); + +$server->on('open', function(Server $server, Swoole\Http\Request $request) { + echo "connection open: {$request->fd}\n"; + // $server->tick(1000, function() use ($server, $request) { + // $server->push($request->fd, json_encode(["hello", time()])); + // }); + $server->push($request->fd, json_encode(["hello", time()])); +}); + +$server->on('message', function(Server $server, Frame $frame) { + echo "received message: {$frame->data}\n"; + $server->push($frame->fd, json_encode(["hello", time()])); +}); + +$server->on('close', function(Server $server, int $fd) { + echo "connection close: {$fd}\n"; +}); + +$server->start(); \ No newline at end of file diff --git a/bin/realtime b/bin/realtime new file mode 100644 index 0000000000..e43dc269e0 --- /dev/null +++ b/bin/realtime @@ -0,0 +1,3 @@ +#!/bin/sh + +php /usr/src/code/app/realtime.php $@ \ No newline at end of file diff --git a/composer.json b/composer.json index f0ce2624cb..33ed8a928a 100644 --- a/composer.json +++ b/composer.json @@ -53,7 +53,7 @@ "phpmailer/phpmailer": "6.1.7" }, "require-dev": { - "swoole/ide-helper": "4.5.4", + "swoole/ide-helper": "4.5.5", "appwrite/sdk-generator": "master", "phpunit/phpunit": "^9.3" }, diff --git a/composer.lock b/composer.lock index 297b144e81..e459019dcc 100644 --- a/composer.lock +++ b/composer.lock @@ -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": "07a5b2d2e742e8651d58889c3253c3b5", + "content-hash": "a59e355840c9762319629e4cc5fc2dbc", "packages": [ { "name": "appwrite/php-clamav", @@ -2300,12 +2300,12 @@ "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "ece0c3ceee73810bd95226401bbfaea9e0f64de7" + "reference": "e33667ac376b7f4dbe97ab556f8e7c8daee383d3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/ece0c3ceee73810bd95226401bbfaea9e0f64de7", - "reference": "ece0c3ceee73810bd95226401bbfaea9e0f64de7", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/e33667ac376b7f4dbe97ab556f8e7c8daee383d3", + "reference": "e33667ac376b7f4dbe97ab556f8e7c8daee383d3", "shasum": "" }, "require": { @@ -2365,7 +2365,7 @@ "type": "github" } ], - "time": "2020-10-09T14:34:55+00:00" + "time": "2020-10-15T05:14:52+00:00" }, { "name": "phpunit/php-file-iterator", @@ -2373,12 +2373,12 @@ "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-file-iterator.git", - "reference": "aa4be8575f26070b100fccb67faabb28f21f66f8" + "reference": "8a1b0bfa74eba894f241e23261febb84c7ffbd8d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/aa4be8575f26070b100fccb67faabb28f21f66f8", - "reference": "aa4be8575f26070b100fccb67faabb28f21f66f8", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/8a1b0bfa74eba894f241e23261febb84c7ffbd8d", + "reference": "8a1b0bfa74eba894f241e23261febb84c7ffbd8d", "shasum": "" }, "require": { @@ -2421,7 +2421,7 @@ "type": "github" } ], - "time": "2020-09-28T05:57:25+00:00" + "time": "2020-10-15T05:05:12+00:00" }, { "name": "phpunit/php-invoker", @@ -2429,12 +2429,12 @@ "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-invoker.git", - "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67" + "reference": "dcc4b2e39d6cb5ba5435a0177ebe947c0c0d05ff" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/5a10147d0aaf65b58940a0b72f71c9ac0423cc67", - "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67", + "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/dcc4b2e39d6cb5ba5435a0177ebe947c0c0d05ff", + "reference": "dcc4b2e39d6cb5ba5435a0177ebe947c0c0d05ff", "shasum": "" }, "require": { @@ -2480,7 +2480,7 @@ "type": "github" } ], - "time": "2020-09-28T05:58:55+00:00" + "time": "2020-10-15T05:05:21+00:00" }, { "name": "phpunit/php-text-template", @@ -2488,12 +2488,12 @@ "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-text-template.git", - "reference": "18c887016e60e52477e54534956d7b47bc52cd84" + "reference": "0b11f04dcd54d149c3904cda577ea8ef8735e377" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/18c887016e60e52477e54534956d7b47bc52cd84", - "reference": "18c887016e60e52477e54534956d7b47bc52cd84", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/0b11f04dcd54d149c3904cda577ea8ef8735e377", + "reference": "0b11f04dcd54d149c3904cda577ea8ef8735e377", "shasum": "" }, "require": { @@ -2535,7 +2535,7 @@ "type": "github" } ], - "time": "2020-09-28T06:03:05+00:00" + "time": "2020-10-15T05:06:00+00:00" }, { "name": "phpunit/php-timer", @@ -2543,12 +2543,12 @@ "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-timer.git", - "reference": "c9ff14f493699e2f6adee9fd06a0245b276643b7" + "reference": "7fe57355ba7462b1cd940d93aa003660b4e6db20" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/c9ff14f493699e2f6adee9fd06a0245b276643b7", - "reference": "c9ff14f493699e2f6adee9fd06a0245b276643b7", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/7fe57355ba7462b1cd940d93aa003660b4e6db20", + "reference": "7fe57355ba7462b1cd940d93aa003660b4e6db20", "shasum": "" }, "require": { @@ -2590,7 +2590,7 @@ "type": "github" } ], - "time": "2020-09-28T06:00:25+00:00" + "time": "2020-10-15T05:05:31+00:00" }, { "name": "phpunit/phpunit", @@ -2598,12 +2598,12 @@ "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "8b79c2a70ae855e582cef1ca63a849fe07bdb01d" + "reference": "137f98be3b928d5f9f14e66f7aa5eadcfc668bbe" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/8b79c2a70ae855e582cef1ca63a849fe07bdb01d", - "reference": "8b79c2a70ae855e582cef1ca63a849fe07bdb01d", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/137f98be3b928d5f9f14e66f7aa5eadcfc668bbe", + "reference": "137f98be3b928d5f9f14e66f7aa5eadcfc668bbe", "shasum": "" }, "require": { @@ -2689,7 +2689,7 @@ "type": "github" } ], - "time": "2020-10-11T07:43:20+00:00" + "time": "2020-10-15T05:02:48+00:00" }, { "name": "sebastian/cli-parser", @@ -2697,12 +2697,12 @@ "source": { "type": "git", "url": "https://github.com/sebastianbergmann/cli-parser.git", - "reference": "442e7c7e687e42adc03470c7b668bc4b2402c0b2" + "reference": "bb13fcea306b784ef38fc1cda21c1395c233f4bc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/442e7c7e687e42adc03470c7b668bc4b2402c0b2", - "reference": "442e7c7e687e42adc03470c7b668bc4b2402c0b2", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/bb13fcea306b784ef38fc1cda21c1395c233f4bc", + "reference": "bb13fcea306b784ef38fc1cda21c1395c233f4bc", "shasum": "" }, "require": { @@ -2741,7 +2741,7 @@ "type": "github" } ], - "time": "2020-09-28T06:08:49+00:00" + "time": "2020-10-15T05:19:54+00:00" }, { "name": "sebastian/code-unit", @@ -2749,12 +2749,12 @@ "source": { "type": "git", "url": "https://github.com/sebastianbergmann/code-unit.git", - "reference": "59236be62b1bb9919e6d7f60b0b832dc05cef9ab" + "reference": "46a6ff3fabc0449fa17ca3ec485c44ab792f65c1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/59236be62b1bb9919e6d7f60b0b832dc05cef9ab", - "reference": "59236be62b1bb9919e6d7f60b0b832dc05cef9ab", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/46a6ff3fabc0449fa17ca3ec485c44ab792f65c1", + "reference": "46a6ff3fabc0449fa17ca3ec485c44ab792f65c1", "shasum": "" }, "require": { @@ -2793,7 +2793,7 @@ "type": "github" } ], - "time": "2020-10-02T14:47:54+00:00" + "time": "2020-10-15T05:03:44+00:00" }, { "name": "sebastian/code-unit-reverse-lookup", @@ -2801,12 +2801,12 @@ "source": { "type": "git", "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", - "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5" + "reference": "a801a24d7681090e8334c631b99181df063ea457" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5", - "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/a801a24d7681090e8334c631b99181df063ea457", + "reference": "a801a24d7681090e8334c631b99181df063ea457", "shasum": "" }, "require": { @@ -2844,7 +2844,7 @@ "type": "github" } ], - "time": "2020-09-28T05:30:19+00:00" + "time": "2020-10-15T05:03:53+00:00" }, { "name": "sebastian/comparator", @@ -2852,12 +2852,12 @@ "source": { "type": "git", "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "7a8ff306445707539c1a6397372a982a1ec55120" + "reference": "d43148f588efca5b5dd0c3d98da467f5aafdac6b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/7a8ff306445707539c1a6397372a982a1ec55120", - "reference": "7a8ff306445707539c1a6397372a982a1ec55120", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/d43148f588efca5b5dd0c3d98da467f5aafdac6b", + "reference": "d43148f588efca5b5dd0c3d98da467f5aafdac6b", "shasum": "" }, "require": { @@ -2914,7 +2914,7 @@ "type": "github" } ], - "time": "2020-09-30T06:47:25+00:00" + "time": "2020-10-15T05:04:03+00:00" }, { "name": "sebastian/complexity", @@ -2922,12 +2922,12 @@ "source": { "type": "git", "url": "https://github.com/sebastianbergmann/complexity.git", - "reference": "ba8cc2da0c0bfbc813d03b56406734030c7f1eff" + "reference": "6d4cf3e8224f1e8527ab434b4ba902978af523db" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/ba8cc2da0c0bfbc813d03b56406734030c7f1eff", - "reference": "ba8cc2da0c0bfbc813d03b56406734030c7f1eff", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/6d4cf3e8224f1e8527ab434b4ba902978af523db", + "reference": "6d4cf3e8224f1e8527ab434b4ba902978af523db", "shasum": "" }, "require": { @@ -2967,7 +2967,7 @@ "type": "github" } ], - "time": "2020-09-28T06:05:03+00:00" + "time": "2020-10-15T05:06:11+00:00" }, { "name": "sebastian/diff", @@ -2975,12 +2975,12 @@ "source": { "type": "git", "url": "https://github.com/sebastianbergmann/diff.git", - "reference": "e3ec6059b3fe483d42fbaf1fe6eefa201f7b4a6d" + "reference": "c25d82b5b776a3ba4e3b232a8688e969477444e0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/e3ec6059b3fe483d42fbaf1fe6eefa201f7b4a6d", - "reference": "e3ec6059b3fe483d42fbaf1fe6eefa201f7b4a6d", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/c25d82b5b776a3ba4e3b232a8688e969477444e0", + "reference": "c25d82b5b776a3ba4e3b232a8688e969477444e0", "shasum": "" }, "require": { @@ -3029,7 +3029,7 @@ "type": "github" } ], - "time": "2020-10-13T11:48:30+00:00" + "time": "2020-10-15T05:04:12+00:00" }, { "name": "sebastian/environment", @@ -3037,12 +3037,12 @@ "source": { "type": "git", "url": "https://github.com/sebastianbergmann/environment.git", - "reference": "388b6ced16caa751030f6a69e588299fa09200ac" + "reference": "40fcf803a36737ce1d2c46c489fb5d1ec2db45d4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/388b6ced16caa751030f6a69e588299fa09200ac", - "reference": "388b6ced16caa751030f6a69e588299fa09200ac", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/40fcf803a36737ce1d2c46c489fb5d1ec2db45d4", + "reference": "40fcf803a36737ce1d2c46c489fb5d1ec2db45d4", "shasum": "" }, "require": { @@ -3088,7 +3088,7 @@ "type": "github" } ], - "time": "2020-09-28T05:52:38+00:00" + "time": "2020-10-15T05:04:22+00:00" }, { "name": "sebastian/exporter", @@ -3096,12 +3096,12 @@ "source": { "type": "git", "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "d89cc98761b8cb5a1a235a6b703ae50d34080e65" + "reference": "dbdc89af25883b35be6ddd4b88a8bc5d22bb819d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/d89cc98761b8cb5a1a235a6b703ae50d34080e65", - "reference": "d89cc98761b8cb5a1a235a6b703ae50d34080e65", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/dbdc89af25883b35be6ddd4b88a8bc5d22bb819d", + "reference": "dbdc89af25883b35be6ddd4b88a8bc5d22bb819d", "shasum": "" }, "require": { @@ -3161,7 +3161,7 @@ "type": "github" } ], - "time": "2020-09-28T05:24:23+00:00" + "time": "2020-10-15T05:04:32+00:00" }, { "name": "sebastian/global-state", @@ -3169,12 +3169,12 @@ "source": { "type": "git", "url": "https://github.com/sebastianbergmann/global-state.git", - "reference": "ea779cb749a478b22a2564ac41cd7bda79c78dc7" + "reference": "3a606041b47fb201c1d62ae03078d0e8a62569a6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/ea779cb749a478b22a2564ac41cd7bda79c78dc7", - "reference": "ea779cb749a478b22a2564ac41cd7bda79c78dc7", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/3a606041b47fb201c1d62ae03078d0e8a62569a6", + "reference": "3a606041b47fb201c1d62ae03078d0e8a62569a6", "shasum": "" }, "require": { @@ -3221,7 +3221,7 @@ "type": "github" } ], - "time": "2020-09-28T05:54:06+00:00" + "time": "2020-10-15T05:04:42+00:00" }, { "name": "sebastian/lines-of-code", @@ -3229,12 +3229,12 @@ "source": { "type": "git", "url": "https://github.com/sebastianbergmann/lines-of-code.git", - "reference": "6514b8f21906b8b46f520d1fbd17a4523fa59a54" + "reference": "5835b6f4707e022ae53a2a67420a3388b318715a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/6514b8f21906b8b46f520d1fbd17a4523fa59a54", - "reference": "6514b8f21906b8b46f520d1fbd17a4523fa59a54", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/5835b6f4707e022ae53a2a67420a3388b318715a", + "reference": "5835b6f4707e022ae53a2a67420a3388b318715a", "shasum": "" }, "require": { @@ -3274,7 +3274,7 @@ "type": "github" } ], - "time": "2020-09-28T06:07:27+00:00" + "time": "2020-10-15T05:06:21+00:00" }, { "name": "sebastian/object-enumerator", @@ -3282,12 +3282,12 @@ "source": { "type": "git", "url": "https://github.com/sebastianbergmann/object-enumerator.git", - "reference": "f6f5957013d84725427d361507e13513702888a4" + "reference": "192362c78b33b0231e1e8841678be93ce6f31830" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/f6f5957013d84725427d361507e13513702888a4", - "reference": "f6f5957013d84725427d361507e13513702888a4", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/192362c78b33b0231e1e8841678be93ce6f31830", + "reference": "192362c78b33b0231e1e8841678be93ce6f31830", "shasum": "" }, "require": { @@ -3327,7 +3327,7 @@ "type": "github" } ], - "time": "2020-09-28T05:55:06+00:00" + "time": "2020-10-15T05:04:51+00:00" }, { "name": "sebastian/object-reflector", @@ -3335,12 +3335,12 @@ "source": { "type": "git", "url": "https://github.com/sebastianbergmann/object-reflector.git", - "reference": "d9d0ab3b12acb1768bc1e0a89b23c90d2043cbe5" + "reference": "da3d1ade6fef132f2486d297c35cf61e45930a0b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/d9d0ab3b12acb1768bc1e0a89b23c90d2043cbe5", - "reference": "d9d0ab3b12acb1768bc1e0a89b23c90d2043cbe5", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/da3d1ade6fef132f2486d297c35cf61e45930a0b", + "reference": "da3d1ade6fef132f2486d297c35cf61e45930a0b", "shasum": "" }, "require": { @@ -3378,7 +3378,7 @@ "type": "github" } ], - "time": "2020-09-28T05:56:16+00:00" + "time": "2020-10-15T05:05:01+00:00" }, { "name": "sebastian/recursion-context", @@ -3386,12 +3386,12 @@ "source": { "type": "git", "url": "https://github.com/sebastianbergmann/recursion-context.git", - "reference": "7e70f3d32a3058d4ad5226c1371f2dd4677dc073" + "reference": "3767a68ada0fc1d50b22db067cd2256b1b722faa" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/7e70f3d32a3058d4ad5226c1371f2dd4677dc073", - "reference": "7e70f3d32a3058d4ad5226c1371f2dd4677dc073", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/3767a68ada0fc1d50b22db067cd2256b1b722faa", + "reference": "3767a68ada0fc1d50b22db067cd2256b1b722faa", "shasum": "" }, "require": { @@ -3437,7 +3437,7 @@ "type": "github" } ], - "time": "2020-09-28T05:27:00+00:00" + "time": "2020-10-15T05:05:41+00:00" }, { "name": "sebastian/resource-operations", @@ -3496,12 +3496,12 @@ "source": { "type": "git", "url": "https://github.com/sebastianbergmann/type.git", - "reference": "fa592377f3923946cb90bf1f6a71ba2e5f229909" + "reference": "5eb6a85e349cab8a411886cb52c070393d71bc7e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/fa592377f3923946cb90bf1f6a71ba2e5f229909", - "reference": "fa592377f3923946cb90bf1f6a71ba2e5f229909", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/5eb6a85e349cab8a411886cb52c070393d71bc7e", + "reference": "5eb6a85e349cab8a411886cb52c070393d71bc7e", "shasum": "" }, "require": { @@ -3540,7 +3540,7 @@ "type": "github" } ], - "time": "2020-10-06T08:41:03+00:00" + "time": "2020-10-15T05:05:50+00:00" }, { "name": "sebastian/version", @@ -3593,16 +3593,16 @@ }, { "name": "swoole/ide-helper", - "version": "4.5.4", + "version": "4.5.5", "source": { "type": "git", "url": "https://github.com/swoole/ide-helper.git", - "reference": "3382a1844afb206cac064252f6b8b50115bf72bb" + "reference": "aefd9d15e00cf14b89a5ed87cfa3bd79c9889028" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/swoole/ide-helper/zipball/3382a1844afb206cac064252f6b8b50115bf72bb", - "reference": "3382a1844afb206cac064252f6b8b50115bf72bb", + "url": "https://api.github.com/repos/swoole/ide-helper/zipball/aefd9d15e00cf14b89a5ed87cfa3bd79c9889028", + "reference": "aefd9d15e00cf14b89a5ed87cfa3bd79c9889028", "shasum": "" }, "require-dev": { @@ -3623,7 +3623,7 @@ } ], "description": "IDE help files for Swoole.", - "time": "2020-09-16T00:12:52+00:00" + "time": "2020-10-14T18:05:12+00:00" }, { "name": "symfony/polyfill-ctype", diff --git a/docker-compose.yml b/docker-compose.yml index 19388e4635..de9945b553 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -10,8 +10,8 @@ services: - --providers.file.directory=/storage/config - --providers.file.watch=true - --providers.docker=true - - --entrypoints.web.address=:80 - - --entrypoints.websecure.address=:443 + - --entrypoints.appwrite_web.address=:80 + - --entrypoints.appwrite_websecure.address=:443 - --accesslog=true restart: unless-stopped ports: @@ -41,9 +41,18 @@ services: networks: - appwrite labels: - - traefik.http.routers.appwrite.rule=PathPrefix(`/`) - - traefik.http.routers.appwrite-secure.rule=PathPrefix(`/`) - - traefik.http.routers.appwrite-secure.tls=true + - "traefik.enable=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: - /var/run/docker.sock:/var/run/docker.sock - appwrite-uploads:/storage/uploads:rw @@ -87,6 +96,37 @@ services: - _APP_FUNCTIONS_TIMEOUT - _APP_FUNCTIONS_CONTAINERS + appwrite-socket: + entrypoint: socket + container_name: appwrite-socket + build: + context: . + restart: unless-stopped + ports: + - 9502:80 + labels: + - "traefik.enable=true" + - "traefik.docker.network=appwrite" + - "traefik.http.services.appwrite_socket.loadbalancer.server.port=80" + #ws + - traefik.http.routers.appwrite_socket_ws.entrypoints=appwrite_web + - traefik.http.routers.appwrite_socket_ws.rule=PathPrefix(`/v1/realtime`) + - traefik.http.routers.appwrite_socket_ws.service=appwrite_socket + # wss + - traefik.http.routers.appwrite_socket_wss.entrypoints=appwrite_websecure + - traefik.http.routers.appwrite_socket_wss.rule=PathPrefix(`/v1/realtime`) + - traefik.http.routers.appwrite_socket_wss.service=appwrite_socket + - traefik.http.routers.appwrite_socket_wss.tls=true + - traefik.http.routers.appwrite_socket_wss.tls.certresolver=dns + networks: + - appwrite + depends_on: + - redis + environment: + - _APP_ENV + - _APP_REDIS_HOST + - _APP_REDIS_PORT + appwrite-worker-usage: entrypoint: worker-usage container_name: appwrite-worker-usage From 875c83a137d8f6eca4ebdca8e9cff6462840af49 Mon Sep 17 00:00:00 2001 From: Eldad Fux Date: Fri, 16 Oct 2020 10:50:46 +0300 Subject: [PATCH 002/267] Updated entypoint --- docker-compose.yml | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index de9945b553..4a8c02c642 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -96,9 +96,9 @@ services: - _APP_FUNCTIONS_TIMEOUT - _APP_FUNCTIONS_CONTAINERS - appwrite-socket: - entrypoint: socket - container_name: appwrite-socket + appwrite-realtime: + entrypoint: realtime + container_name: appwrite-realtime build: context: . restart: unless-stopped @@ -107,17 +107,17 @@ services: labels: - "traefik.enable=true" - "traefik.docker.network=appwrite" - - "traefik.http.services.appwrite_socket.loadbalancer.server.port=80" + - "traefik.http.services.appwrite_realtime.loadbalancer.server.port=80" #ws - - traefik.http.routers.appwrite_socket_ws.entrypoints=appwrite_web - - traefik.http.routers.appwrite_socket_ws.rule=PathPrefix(`/v1/realtime`) - - traefik.http.routers.appwrite_socket_ws.service=appwrite_socket + - 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_socket_wss.entrypoints=appwrite_websecure - - traefik.http.routers.appwrite_socket_wss.rule=PathPrefix(`/v1/realtime`) - - traefik.http.routers.appwrite_socket_wss.service=appwrite_socket - - traefik.http.routers.appwrite_socket_wss.tls=true - - traefik.http.routers.appwrite_socket_wss.tls.certresolver=dns + - 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: From 3cb584430b40530b39ca44ec99d446fe4b09ea41 Mon Sep 17 00:00:00 2001 From: Eldad Fux Date: Sat, 17 Oct 2020 08:48:03 +0300 Subject: [PATCH 003/267] Added docs --- app/realtime.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/realtime.php b/app/realtime.php index 648c6b4e9c..4123d741de 100644 --- a/app/realtime.php +++ b/app/realtime.php @@ -12,6 +12,10 @@ use Swoole\WebSocket\Frame; * - Limit payload size * - Message structure: { status: "ok"|"error", event: EVENT_NAME, data: } * - JWT Authentication (in path / or in message) + * + * + * - https://github.com/hhxsv5/php-sse + * - https://github.com/shuixn/socket.io-swoole-server */ $server = new Server("0.0.0.0", 80); @@ -25,6 +29,8 @@ $server->on('open', function(Server $server, Swoole\Http\Request $request) { // $server->tick(1000, function() use ($server, $request) { // $server->push($request->fd, json_encode(["hello", time()])); // }); + + var_dump($request->header); $server->push($request->fd, json_encode(["hello", time()])); }); From 5f4532ebbfbad697dd838e2959e9be98e8a6505f Mon Sep 17 00:00:00 2001 From: Eldad Fux Date: Sun, 18 Oct 2020 14:51:16 +0300 Subject: [PATCH 004/267] Added redis pub/sub --- app/realtime.php | 77 +++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 70 insertions(+), 7 deletions(-) diff --git a/app/realtime.php b/app/realtime.php index 4123d741de..b439ac5197 100644 --- a/app/realtime.php +++ b/app/realtime.php @@ -1,8 +1,8 @@ on('message', function(\Swoole\Coroutine\Redis $redis, $rs) use ($server) { +// var_dump($server); +// echo 'redis got message' . PHP_EOL; +// var_dump($rs); +// $server->send(1, $rs); +// }); +// $redis->connect('redis', 6379, function(\Swoole\Coroutine\Redis $redis, $result){ + +// echo 'connected to redis' . PHP_EOL; +// $redis->subscribe('chat'); +// }); + +$server->on("workerStart", function ($server, $workerId) { + Console::success('Worker '.++$workerId.' started succefully'); + + $redis = new Redis(); + $redis->connect('redis', 6379); + + $redis->subscribe(['realtime'], function($redis, $channel, $message) use ($server, $workerId) { + var_dump($redis, $channel, $message); + + $message = 'Message from worker #'.$workerId.'; '.$message; + + foreach($server->connections as $fd) { + if ($server->exist($fd) && $server->isEstablished($fd)) { + $server->push($fd, $message, SWOOLE_WEBSOCKET_OPCODE_TEXT, SWOOLE_WEBSOCKET_FLAG_FIN | SWOOLE_WEBSOCKET_FLAG_COMPRESS); + } + else { + $server->close($fd); + } + } + }); +}); + +$server->on('BeforeReload', function($serv, $workerId) { + Console::success('Starting reload...'); +}); + +$server->on('AfterReload', function($serv, $workerId) { + Console::success('Reload completed...'); +}); + +// $process = new Process(function($process) use ($server) { +// while (true) { +// $msg = $process->read(); + +// foreach($server->connections as $fd) { +// if ($server->exist($fd) && $server->isEstablished($fd)) { +// $server->push($fd, json_encode(['hey there']), SWOOLE_WEBSOCKET_OPCODE_TEXT, SWOOLE_WEBSOCKET_FLAG_FIN | SWOOLE_WEBSOCKET_FLAG_COMPRESS); +// } +// } + +// sleep(10); +// } +// }); + +// $server->addProcess($process); + $server->on("start", function (Server $server) { - echo "Swoole WebSocket Server has started at http://127.0.0.1:3000\n"; + Console::success('Server started succefully'); }); $server->on('open', function(Server $server, Swoole\Http\Request $request) { echo "connection open: {$request->fd}\n"; - // $server->tick(1000, function() use ($server, $request) { - // $server->push($request->fd, json_encode(["hello", time()])); - // }); - var_dump($request->header); + foreach($server->connections as $fd) { + if ($server->exist($fd) && $server->isEstablished($fd)) { + $server->push($fd, json_encode(['hey there', count($server->ports[0]->connections), ]), SWOOLE_WEBSOCKET_OPCODE_TEXT, SWOOLE_WEBSOCKET_FLAG_FIN | SWOOLE_WEBSOCKET_FLAG_COMPRESS); + } + } + $server->push($request->fd, json_encode(["hello", time()])); }); $server->on('message', function(Server $server, Frame $frame) { echo "received message: {$frame->data}\n"; - $server->push($frame->fd, json_encode(["hello", time()])); + $server->push($frame->fd, json_encode(["hello, worker_id:".$server->getWorkerId(), time()])); }); $server->on('close', function(Server $server, int $fd) { From 4dfb91f2ef089b5665f38977e0f72a5fb2644aa8 Mon Sep 17 00:00:00 2001 From: Eldad Fux Date: Mon, 19 Oct 2020 17:49:35 +0300 Subject: [PATCH 005/267] Updated phpredis client --- Dockerfile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 5f53a82cb9..519d2a01cf 100755 --- a/Dockerfile +++ b/Dockerfile @@ -15,7 +15,7 @@ RUN composer update --ignore-platform-reqs --optimize-autoloader \ FROM php:7.4-cli-alpine as step1 ENV TZ=Asia/Tel_Aviv \ - PHP_REDIS_VERSION=5.3.0 \ + PHP_REDIS_VERSION=5.3.2RC2 \ PHP_SWOOLE_VERSION=4.5.5 \ PHP_XDEBUG_VERSION=sdebug_2_9-beta @@ -170,6 +170,7 @@ RUN echo extension=redis.so >> /usr/local/etc/php/conf.d/redis.ini 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 EXPOSE 80 From b590ecc00ac67aa1fedd5b9b0b601a0da53df7d3 Mon Sep 17 00:00:00 2001 From: Eldad Fux Date: Mon, 19 Oct 2020 17:49:49 +0300 Subject: [PATCH 006/267] Removed unsupported server option --- app/http.php | 1 - 1 file changed, 1 deletion(-) diff --git a/app/http.php b/app/http.php index 9540375430..e51677f88e 100644 --- a/app/http.php +++ b/app/http.php @@ -30,7 +30,6 @@ $http 'open_http2_protocol' => true, // 'document_root' => __DIR__.'/../public', // 'enable_static_handler' => true, - 'timeout' => 7, 'http_compression' => true, 'http_compression_level' => 6, 'package_max_length' => $payloadSize, From 210fa9fd64bb61ff3528bf03100259ef46bf0fc6 Mon Sep 17 00:00:00 2001 From: Eldad Fux Date: Mon, 19 Oct 2020 17:50:12 +0300 Subject: [PATCH 007/267] Fixed redis connection init --- app/init.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/init.php b/app/init.php index 9637c4381c..c6a578eba5 100644 --- a/app/init.php +++ b/app/init.php @@ -166,8 +166,7 @@ $register->set('statsd', function () { // Register DB connection }); $register->set('cache', function () { // Register cache connection $redis = new Redis(); - $redis->pconnect(App::getEnv('_APP_REDIS_HOST', '', 2.5), - App::getEnv('_APP_REDIS_PORT', '')); + $redis->pconnect(App::getEnv('_APP_REDIS_HOST', ''), App::getEnv('_APP_REDIS_PORT', '')); return $redis; }); From 3f0595c04aa69babebb8ca0f49e0f8668918570a Mon Sep 17 00:00:00 2001 From: Eldad Fux Date: Mon, 19 Oct 2020 18:09:27 +0300 Subject: [PATCH 008/267] Test realtime server --- app/controllers/api/health.php | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/app/controllers/api/health.php b/app/controllers/api/health.php index 5ef4f2b9c8..dc14fffff0 100644 --- a/app/controllers/api/health.php +++ b/app/controllers/api/health.php @@ -30,6 +30,19 @@ App::get('/v1/health/version') $response->json(['version' => APP_VERSION_STABLE]); }, ['response']); +App::get('/v1/health/realtime') + ->desc('Get Realtime') + ->groups(['api', 'health']) + ->label('scope', 'public') + ->action(function ($response) { + /** @var Utopia\Response $response */ + $redis = new Redis(); + $redis->connect('redis', 6379); + + $redis->publish('realtime', 'I\'m a live message'); + $response->json(['status' => 'OK']); + }, ['response']); + App::get('/v1/health/db') ->desc('Get DB') ->groups(['api', 'health']) From 77e35e708196ab2d051c706da81762fe65338b79 Mon Sep 17 00:00:00 2001 From: Eldad Fux Date: Mon, 19 Oct 2020 18:09:53 +0300 Subject: [PATCH 009/267] Added redis persistent connection --- app/realtime.php | 126 ++++++++++++++++++++++++----------------------- 1 file changed, 64 insertions(+), 62 deletions(-) diff --git a/app/realtime.php b/app/realtime.php index b439ac5197..90416aa17e 100644 --- a/app/realtime.php +++ b/app/realtime.php @@ -1,6 +1,9 @@ on('message', function(\Swoole\Coroutine\Redis $redis, $rs) use ($server) { -// var_dump($server); -// echo 'redis got message' . PHP_EOL; -// var_dump($rs); -// $server->send(1, $rs); -// }); -// $redis->connect('redis', 6379, function(\Swoole\Coroutine\Redis $redis, $result){ +$connections = []; -// echo 'connected to redis' . PHP_EOL; -// $redis->subscribe('chat'); -// }); - -$server->on("workerStart", function ($server, $workerId) { +$server->on("workerStart", function ($server, $workerId) use (&$connections) { Console::success('Worker '.++$workerId.' started succefully'); - $redis = new Redis(); - $redis->connect('redis', 6379); + $attempts = 0; + $start = time(); + + while ($attempts < 3) { + try { + $redis = new Redis(); + $redis->connect('redis', 6379); + $redis->setOption(Redis::OPT_READ_TIMEOUT, -1); - $redis->subscribe(['realtime'], function($redis, $channel, $message) use ($server, $workerId) { - var_dump($redis, $channel, $message); - - $message = 'Message from worker #'.$workerId.'; '.$message; - - foreach($server->connections as $fd) { - if ($server->exist($fd) && $server->isEstablished($fd)) { - $server->push($fd, $message, SWOOLE_WEBSOCKET_OPCODE_TEXT, SWOOLE_WEBSOCKET_FLAG_FIN | SWOOLE_WEBSOCKET_FLAG_COMPRESS); + if($attempts > 0) { + Console::error('Connection lost (lasted '.(time() - $start).' seconds). Attempting restart (attempt #'.$attempts.')'); } - else { - $server->close($fd); + + if($redis->ping('')) { + $attempts = 0; } + + sleep(1); // 1 sec delay between connection attempts + + $redis->subscribe(['realtime'], function($redis, $channel, $message) use ($server, $workerId, &$connections) { + $message = 'Message from worker #'.$workerId.'; '.$message; + + Console::warning('Total connections: '.count($connections)); + + foreach($connections as $fd) { + if ($server->exist($fd) + && $server->isEstablished($fd) + ) { + Console::info('Sending message: '.$message.' (user: '.$fd.', worker: '.$workerId.')'); + + $server->push($fd, $message, SWOOLE_WEBSOCKET_OPCODE_TEXT, + SWOOLE_WEBSOCKET_FLAG_FIN | SWOOLE_WEBSOCKET_FLAG_COMPRESS); + } + else { + $server->close($fd); + } + } + }); + + $attempts++; + + } catch (\Throwable $th) { + $attempts++; + continue; } - }); + } + + Console::error('Failed to restart connection...'); }); -$server->on('BeforeReload', function($serv, $workerId) { - Console::success('Starting reload...'); -}); - -$server->on('AfterReload', function($serv, $workerId) { - Console::success('Reload completed...'); -}); - -// $process = new Process(function($process) use ($server) { -// while (true) { -// $msg = $process->read(); - -// foreach($server->connections as $fd) { -// if ($server->exist($fd) && $server->isEstablished($fd)) { -// $server->push($fd, json_encode(['hey there']), SWOOLE_WEBSOCKET_OPCODE_TEXT, SWOOLE_WEBSOCKET_FLAG_FIN | SWOOLE_WEBSOCKET_FLAG_COMPRESS); -// } -// } - -// sleep(10); -// } -// }); - -// $server->addProcess($process); - $server->on("start", function (Server $server) { Console::success('Server started succefully'); }); -$server->on('open', function(Server $server, Swoole\Http\Request $request) { - echo "connection open: {$request->fd}\n"; +$server->on('open', function(Server $server, Request $request) use (&$connections) { + $connections[] = $request->fd; + + Console::info("Connection open (user: {$request->fd}, worker: {$server->getWorkerId()})"); + Console::info('Total connections: '.count($connections)); - foreach($server->connections as $fd) { - if ($server->exist($fd) && $server->isEstablished($fd)) { - $server->push($fd, json_encode(['hey there', count($server->ports[0]->connections), ]), SWOOLE_WEBSOCKET_OPCODE_TEXT, SWOOLE_WEBSOCKET_FLAG_FIN | SWOOLE_WEBSOCKET_FLAG_COMPRESS); - } - } - - $server->push($request->fd, json_encode(["hello", time()])); + $server->push($request->fd, json_encode(["hello", count($connections)])); }); $server->on('message', function(Server $server, Frame $frame) { - echo "received message: {$frame->data}\n"; + if($frame->data === 'reload') { + $server->reload(); + } + + Console::info('Recieved message: '.$frame->data.' (user: '.$frame->fd.', worker: '.$server->getWorkerId().')'); + $server->push($frame->fd, json_encode(["hello, worker_id:".$server->getWorkerId(), time()])); }); $server->on('close', function(Server $server, int $fd) { - echo "connection close: {$fd}\n"; + Console::error('Connection close: '.$fd); }); $server->start(); \ No newline at end of file From a6cb86639b2ba71ae4a765b9dabacd03c6af58b5 Mon Sep 17 00:00:00 2001 From: Eldad Fux Date: Mon, 19 Oct 2020 21:56:02 +0300 Subject: [PATCH 010/267] Added logs --- app/realtime.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/realtime.php b/app/realtime.php index 90416aa17e..40ee8f572d 100644 --- a/app/realtime.php +++ b/app/realtime.php @@ -46,6 +46,10 @@ $server->on("workerStart", function ($server, $workerId) use (&$connections) { if($redis->ping('')) { $attempts = 0; + Console::success('Connection established'); + } + else { + Console::error('Connection failed'); } sleep(1); // 1 sec delay between connection attempts From a52de551a1070831eec037f4f245a1f66c2b2c0b Mon Sep 17 00:00:00 2001 From: Eldad Fux Date: Mon, 19 Oct 2020 23:38:49 +0300 Subject: [PATCH 011/267] Fixed connection retry --- app/realtime.php | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/app/realtime.php b/app/realtime.php index 40ee8f572d..5bfb207bd7 100644 --- a/app/realtime.php +++ b/app/realtime.php @@ -34,17 +34,18 @@ $server->on("workerStart", function ($server, $workerId) use (&$connections) { $attempts = 0; $start = time(); - while ($attempts < 3) { + while ($attempts < 300) { try { + if($attempts > 0) { + Console::error('Connection lost (lasted '.(time() - $start).' seconds). Attempting restart in 5 seconds (attempt #'.$attempts.')'); + sleep(5); // 1 sec delay between connection attempts + } + $redis = new Redis(); $redis->connect('redis', 6379); $redis->setOption(Redis::OPT_READ_TIMEOUT, -1); - if($attempts > 0) { - Console::error('Connection lost (lasted '.(time() - $start).' seconds). Attempting restart (attempt #'.$attempts.')'); - } - - if($redis->ping('')) { + if($redis->ping(true)) { $attempts = 0; Console::success('Connection established'); } @@ -52,8 +53,6 @@ $server->on("workerStart", function ($server, $workerId) use (&$connections) { Console::error('Connection failed'); } - sleep(1); // 1 sec delay between connection attempts - $redis->subscribe(['realtime'], function($redis, $channel, $message) use ($server, $workerId, &$connections) { $message = 'Message from worker #'.$workerId.'; '.$message; @@ -74,12 +73,13 @@ $server->on("workerStart", function ($server, $workerId) use (&$connections) { } }); - $attempts++; - } catch (\Throwable $th) { + Console::error('Connection error: '.$th->getMessage()); $attempts++; continue; } + + $attempts++; } Console::error('Failed to restart connection...'); From 4ccab4f9554f5a799cb41f484a801ccac57b0322 Mon Sep 17 00:00:00 2001 From: Eldad Fux Date: Tue, 20 Oct 2020 16:22:46 +0300 Subject: [PATCH 012/267] POC --- app/realtime.php | 27 +++++++++++++++++++++------ docker-compose.yml | 7 +++++++ 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/app/realtime.php b/app/realtime.php index 5bfb207bd7..cbe1da2195 100644 --- a/app/realtime.php +++ b/app/realtime.php @@ -4,8 +4,11 @@ require_once __DIR__.'/../vendor/autoload.php'; use Swoole\WebSocket\Server; use Swoole\Http\Request; +use Swoole\Process; use Swoole\WebSocket\Frame; +use Utopia\App; use Utopia\CLI\Console; +use Utopia\Route; /** * TODO List @@ -16,9 +19,11 @@ use Utopia\CLI\Console; * - Message structure: { status: "ok"|"error", event: EVENT_NAME, data: } * - JWT Authentication (in path / or in message) * - * - * - https://github.com/hhxsv5/php-sse - * - https://github.com/shuixn/socket.io-swoole-server + * Protocols Support: + * - Websocket support: https://www.swoole.co.uk/docs/modules/swoole-websocket-server + * - MQTT support: https://www.swoole.co.uk/docs/modules/swoole-mqtt-server + * - SSE support: https://github.com/hhxsv5/php-sse + * - Socket.io support: https://github.com/shuixn/socket.io-swoole-server */ ini_set('default_socket_timeout', -1); @@ -55,9 +60,7 @@ $server->on("workerStart", function ($server, $workerId) use (&$connections) { $redis->subscribe(['realtime'], function($redis, $channel, $message) use ($server, $workerId, &$connections) { $message = 'Message from worker #'.$workerId.'; '.$message; - - Console::warning('Total connections: '.count($connections)); - + foreach($connections as $fd) { if ($server->exist($fd) && $server->isEstablished($fd) @@ -87,6 +90,14 @@ $server->on("workerStart", function ($server, $workerId) use (&$connections) { $server->on("start", function (Server $server) { Console::success('Server started succefully'); + + Console::info("Master pid {$server->master_pid}, manager pid {$server->manager_pid}"); + + // listen ctrl + c + Process::signal(2, function () use ($server) { + Console::log('Stop by Ctrl+C'); + $server->shutdown(); + }); }); $server->on('open', function(Server $server, Request $request) use (&$connections) { @@ -95,6 +106,10 @@ $server->on('open', function(Server $server, Request $request) use (&$connection Console::info("Connection open (user: {$request->fd}, worker: {$server->getWorkerId()})"); Console::info('Total connections: '.count($connections)); + $app = new App('Asia/Tel_Aviv'); + + var_dump($app->getResource('user')); + $server->push($request->fd, json_encode(["hello", count($connections)])); }); diff --git a/docker-compose.yml b/docker-compose.yml index 4a8c02c642..11c66d75b3 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -124,8 +124,15 @@ services: - 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 appwrite-worker-usage: entrypoint: worker-usage From f1df0e7071fba01c7d145f62753735aeafd08ca0 Mon Sep 17 00:00:00 2001 From: Eldad Fux Date: Wed, 21 Oct 2020 13:49:56 +0300 Subject: [PATCH 013/267] Updated FW version --- app/controllers/web/home.php | 2 +- composer.json | 2 +- composer.lock | 36 ++++++++++++++++++------------------ 3 files changed, 20 insertions(+), 20 deletions(-) diff --git a/app/controllers/web/home.php b/app/controllers/web/home.php index a6c2491c0f..781abd89a5 100644 --- a/app/controllers/web/home.php +++ b/app/controllers/web/home.php @@ -437,7 +437,7 @@ App::get('/open-api-2.json') ]; foreach ($route->getParams() as $name => $param) { - $validator = (\is_callable($param['validator'])) ? call_user_func_array($param['validator'], $utopia->getResources($param['resources'])) : $param['validator']; /* @var $validator \Utopia\Validator */ + $validator = (\is_callable($param['validator'])) ? call_user_func_array($param['validator'], App::getResources($param['resources'])) : $param['validator']; /* @var $validator \Utopia\Validator */ $node = [ 'name' => $name, diff --git a/composer.json b/composer.json index c11009687f..e1df44b44e 100644 --- a/composer.json +++ b/composer.json @@ -33,7 +33,7 @@ "appwrite/php-clamav": "1.0.*", - "utopia-php/framework": "0.9.1", + "utopia-php/framework": "0.9.2", "utopia-php/abuse": "0.2.*", "utopia-php/audit": "0.3.*", "utopia-php/cache": "0.2.*", diff --git a/composer.lock b/composer.lock index 922593c99e..f13d8d1bb0 100644 --- a/composer.lock +++ b/composer.lock @@ -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": "19f7cef86ddc98623cd3ffffa2be2cae", + "content-hash": "f51a86b9521acc0e934814d05b2927d2", "packages": [ { "name": "appwrite/php-clamav", @@ -514,12 +514,12 @@ "source": { "type": "git", "url": "https://github.com/guzzle/promises.git", - "reference": "60d379c243457e073cff02bc323a2a86cb355631" + "reference": "ddfeedfff2a52661429437da0702979f708e6ac6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/promises/zipball/60d379c243457e073cff02bc323a2a86cb355631", - "reference": "60d379c243457e073cff02bc323a2a86cb355631", + "url": "https://api.github.com/repos/guzzle/promises/zipball/ddfeedfff2a52661429437da0702979f708e6ac6", + "reference": "ddfeedfff2a52661429437da0702979f708e6ac6", "shasum": "" }, "require": { @@ -557,7 +557,7 @@ "keywords": [ "promise" ], - "time": "2020-09-30T07:37:28+00:00" + "time": "2020-10-19T16:50:15+00:00" }, { "name": "guzzlehttp/psr7", @@ -1522,23 +1522,23 @@ }, { "name": "utopia-php/framework", - "version": "0.9.1", + "version": "0.9.2", "source": { "type": "git", "url": "https://github.com/utopia-php/framework.git", - "reference": "1c33b92b9188fb11b2ae70a4b7cf51844e07aa2e" + "reference": "82d1d0c913f0002e79557ae2ac9da991b2256e4d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/framework/zipball/1c33b92b9188fb11b2ae70a4b7cf51844e07aa2e", - "reference": "1c33b92b9188fb11b2ae70a4b7cf51844e07aa2e", + "url": "https://api.github.com/repos/utopia-php/framework/zipball/82d1d0c913f0002e79557ae2ac9da991b2256e4d", + "reference": "82d1d0c913f0002e79557ae2ac9da991b2256e4d", "shasum": "" }, "require": { - "php": ">=7.0.0" + "php": ">=7.3.0" }, "require-dev": { - "phpunit/phpunit": "^7.0" + "phpunit/phpunit": "^9.4" }, "type": "library", "autoload": { @@ -1562,7 +1562,7 @@ "php", "upf" ], - "time": "2020-09-09T19:50:26+00:00" + "time": "2020-10-21T04:58:48+00:00" }, { "name": "utopia-php/locale", @@ -1713,7 +1713,7 @@ "source": { "type": "git", "url": "https://github.com/appwrite/sdk-generator", - "reference": "a57b3cd56c4bfe1538276cfc77456cf95d8835cb" + "reference": "ad1ee55f61967546c0889d377b628e244182311e" }, "require": { "ext-curl": "*", @@ -1743,7 +1743,7 @@ } ], "description": "Appwrite PHP library for generating API SDKs for multiple programming languages and platforms", - "time": "2020-10-14T12:07:25+00:00" + "time": "2020-10-20T10:23:43+00:00" }, { "name": "doctrine/instantiator", @@ -2646,12 +2646,12 @@ "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "338bf27b4510498c4b0ab70c7cbc292a591dc0df" + "reference": "0534ed70e3a792e4891f32ff78055648dfac4a3d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/338bf27b4510498c4b0ab70c7cbc292a591dc0df", - "reference": "338bf27b4510498c4b0ab70c7cbc292a591dc0df", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/0534ed70e3a792e4891f32ff78055648dfac4a3d", + "reference": "0534ed70e3a792e4891f32ff78055648dfac4a3d", "shasum": "" }, "require": { @@ -2737,7 +2737,7 @@ "type": "github" } ], - "time": "2020-10-19T09:25:00+00:00" + "time": "2020-10-21T04:49:22+00:00" }, { "name": "sebastian/cli-parser", From 619781a4ad567b6def0add82e8a9fcf9cb48f83a Mon Sep 17 00:00:00 2001 From: Eldad Fux Date: Wed, 21 Oct 2020 13:50:11 +0300 Subject: [PATCH 014/267] Handle user & project channels --- app/init.php | 9 +++++---- app/realtime.php | 41 ++++++++++++++++++++++++++++++++++++++--- 2 files changed, 43 insertions(+), 7 deletions(-) diff --git a/app/init.php b/app/init.php index c6a578eba5..7f5fe1e3de 100644 --- a/app/init.php +++ b/app/init.php @@ -380,10 +380,10 @@ App::setResource('user', function($mode, $project, $console, $request, $response $request->getHeader('x-appwrite-key', '')))); // Get API Key // 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] : '')); @@ -397,7 +397,7 @@ App::setResource('user', function($mode, $project, $console, $request, $response } else { $user = $consoleDB->getDocument(Auth::$unique); - + $user ->setAttribute('$id', 'admin-'.$user->getAttribute('$id')) ; @@ -412,7 +412,8 @@ App::setResource('user', function($mode, $project, $console, $request, $response if (APP_MODE_ADMIN === $mode) { if (!empty($user->search('teamId', $project->getAttribute('teamId'), $user->getAttribute('memberships')))) { Authorization::setDefaultStatus(false); // Cancel security segmentation for admin users. - } else { + } + else { $user = new Document(['$id' => '', '$collection' => Database::SYSTEM_COLLECTION_USERS]); } } diff --git a/app/realtime.php b/app/realtime.php index cbe1da2195..497d71a8ec 100644 --- a/app/realtime.php +++ b/app/realtime.php @@ -1,7 +1,9 @@ on("workerStart", function ($server, $workerId) use (&$connections) { Console::success('Worker '.++$workerId.' started succefully'); @@ -71,7 +74,7 @@ $server->on("workerStart", function ($server, $workerId) use (&$connections) { SWOOLE_WEBSOCKET_FLAG_FIN | SWOOLE_WEBSOCKET_FLAG_COMPRESS); } else { - $server->close($fd); + $server->close($fd); } } }); @@ -106,11 +109,43 @@ $server->on('open', function(Server $server, Request $request) use (&$connection Console::info("Connection open (user: {$request->fd}, worker: {$server->getWorkerId()})"); Console::info('Total connections: '.count($connections)); + $connection = $request->fd; $app = new App('Asia/Tel_Aviv'); + $request = new SwooleRequest($request); - var_dump($app->getResource('user')); + App::setResource('request', function () use ($request) { + return $request; + }); - $server->push($request->fd, json_encode(["hello", count($connections)])); + App::setResource('response', function () { + return null; + }); + + $user = App::getResource('user'); + $project = App::getResource('project'); + + /** @var Appwrite\Database\Document $user */ + /** @var Appwrite\Database\Document $project */ + + var_dump($project->getId()); + var_dump($project->getAttribute('name')); + var_dump($user->getId()); + var_dump($user->getAttribute('name')); + + if(!isset($subscriptions[$project->getId()])) { // Init Project + $subscriptions[$project->getId()] = []; + } + + if(isset($subscriptions[$project->getId()][$user->getId()])) { // Close previous connection + $server->close($subscriptions[$project->getId()][$user->getId()]['connection']); + } + + $subscriptions[$project->getId()][$user->getId()] = [ + 'channels' => [], + 'connection' => $connection, + ]; + + $server->push($connection, json_encode(["hello", count($connections)])); }); $server->on('message', function(Server $server, Frame $frame) { From c491b781144327f5a76f70da4584c18838abf433 Mon Sep 17 00:00:00 2001 From: Eldad Fux Date: Wed, 21 Oct 2020 15:03:50 +0300 Subject: [PATCH 015/267] Added connection open/close logic --- app/realtime.php | 85 +++++++++++++++++++++++++++++------------------- 1 file changed, 52 insertions(+), 33 deletions(-) diff --git a/app/realtime.php b/app/realtime.php index 497d71a8ec..48ea450927 100644 --- a/app/realtime.php +++ b/app/realtime.php @@ -32,11 +32,10 @@ ini_set('default_socket_timeout', -1); Swoole\Runtime::enableCoroutine(SWOOLE_HOOK_ALL); $server = new Server("0.0.0.0", 80); - -$connections = []; $subscriptions = []; +$connections = []; -$server->on("workerStart", function ($server, $workerId) use (&$connections) { +$server->on("workerStart", function ($server, $workerId) use (&$subscriptions) { Console::success('Worker '.++$workerId.' started succefully'); $attempts = 0; @@ -45,7 +44,7 @@ $server->on("workerStart", function ($server, $workerId) use (&$connections) { while ($attempts < 300) { try { if($attempts > 0) { - Console::error('Connection lost (lasted '.(time() - $start).' seconds). Attempting restart in 5 seconds (attempt #'.$attempts.')'); + Console::error('Pub/sub connection lost (lasted '.(time() - $start).' seconds). Attempting restart in 5 seconds (attempt #'.$attempts.')'); sleep(5); // 1 sec delay between connection attempts } @@ -55,32 +54,32 @@ $server->on("workerStart", function ($server, $workerId) use (&$connections) { if($redis->ping(true)) { $attempts = 0; - Console::success('Connection established'); + Console::success('Pub/sub connection established'); } else { - Console::error('Connection failed'); + Console::error('Pub/sub failed'); } - $redis->subscribe(['realtime'], function($redis, $channel, $message) use ($server, $workerId, &$connections) { + $redis->subscribe(['realtime'], function($redis, $channel, $message) use ($server, $workerId) { $message = 'Message from worker #'.$workerId.'; '.$message; - foreach($connections as $fd) { - if ($server->exist($fd) - && $server->isEstablished($fd) - ) { - Console::info('Sending message: '.$message.' (user: '.$fd.', worker: '.$workerId.')'); + // foreach($connections as $fd) { + // if ($server->exist($fd) + // && $server->isEstablished($fd) + // ) { + // Console::info('Sending message: '.$message.' (user: '.$fd.', worker: '.$workerId.')'); - $server->push($fd, $message, SWOOLE_WEBSOCKET_OPCODE_TEXT, - SWOOLE_WEBSOCKET_FLAG_FIN | SWOOLE_WEBSOCKET_FLAG_COMPRESS); - } - else { - $server->close($fd); - } - } + // $server->push($fd, $message, SWOOLE_WEBSOCKET_OPCODE_TEXT, + // SWOOLE_WEBSOCKET_FLAG_FIN | SWOOLE_WEBSOCKET_FLAG_COMPRESS); + // } + // else { + // $server->close($fd); + // } + // } }); } catch (\Throwable $th) { - Console::error('Connection error: '.$th->getMessage()); + Console::error('Pub/sub error: '.$th->getMessage()); $attempts++; continue; } @@ -88,7 +87,7 @@ $server->on("workerStart", function ($server, $workerId) use (&$connections) { $attempts++; } - Console::error('Failed to restart connection...'); + Console::error('Failed to restart pub/sub...'); }); $server->on("start", function (Server $server) { @@ -103,14 +102,10 @@ $server->on("start", function (Server $server) { }); }); -$server->on('open', function(Server $server, Request $request) use (&$connections) { - $connections[] = $request->fd; - +$server->on('open', function(Server $server, Request $request) use (&$connections, &$subscriptions) { Console::info("Connection open (user: {$request->fd}, worker: {$server->getWorkerId()})"); - Console::info('Total connections: '.count($connections)); $connection = $request->fd; - $app = new App('Asia/Tel_Aviv'); $request = new SwooleRequest($request); App::setResource('request', function () use ($request) { @@ -121,6 +116,7 @@ $server->on('open', function(Server $server, Request $request) use (&$connection return null; }); + $channels = array_flip($request->getQuery('channels', [])); $user = App::getResource('user'); $project = App::getResource('project'); @@ -136,16 +132,20 @@ $server->on('open', function(Server $server, Request $request) use (&$connection $subscriptions[$project->getId()] = []; } - if(isset($subscriptions[$project->getId()][$user->getId()])) { // Close previous connection - $server->close($subscriptions[$project->getId()][$user->getId()]['connection']); + if(!isset($subscriptions[$project->getId()][$user->getId()])) { // Add user first connection + $subscriptions[$project->getId()][$user->getId()] = []; } - $subscriptions[$project->getId()][$user->getId()] = [ - 'channels' => [], - 'connection' => $connection, + foreach ($channels as $channel => $list) { + $subscriptions[$project->getId()][$user->getId()][$channel][$connection] = true; + } + + $connections[$connection] = [ + 'projectId' => $project->getId(), + 'userId' => $user->getId() ]; - $server->push($connection, json_encode(["hello", count($connections)])); + $server->push($connection, json_encode($subscriptions)); }); $server->on('message', function(Server $server, Frame $frame) { @@ -158,8 +158,27 @@ $server->on('message', function(Server $server, Frame $frame) { $server->push($frame->fd, json_encode(["hello, worker_id:".$server->getWorkerId(), time()])); }); -$server->on('close', function(Server $server, int $fd) { +$server->on('close', function(Server $server, int $fd) use (&$connections, &$subscriptions) { Console::error('Connection close: '.$fd); + + $projectId = $connections[$fd]['projectId'] ?? ''; + $userId = $connections[$fd]['userId'] ?? ''; + + foreach ($subscriptions[$projectId][$userId] as $channel => $list) { + unset($subscriptions[$projectId][$userId][$channel][$fd]); // Remove connection + + if(empty($list)) { + unset($subscriptions[$projectId][$userId][$channel]); // Remove channel + } + } + + if(empty($subscriptions[$projectId][$userId])) { + unset($subscriptions[$projectId][$userId]); // Remove user + } + + unset($connections[$fd]); + + var_dump($subscriptions); }); $server->start(); \ No newline at end of file From 437f5f1eb9cceafd98fb32d4285759fb1c92c5df Mon Sep 17 00:00:00 2001 From: Eldad Fux Date: Wed, 21 Oct 2020 18:03:18 +0300 Subject: [PATCH 016/267] Updated FW --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index e1df44b44e..9af01f8407 100644 --- a/composer.json +++ b/composer.json @@ -33,7 +33,7 @@ "appwrite/php-clamav": "1.0.*", - "utopia-php/framework": "0.9.2", + "utopia-php/framework": "0.9.4", "utopia-php/abuse": "0.2.*", "utopia-php/audit": "0.3.*", "utopia-php/cache": "0.2.*", From dabff52271deac01baac2af7cbe3ccd11469ba95 Mon Sep 17 00:00:00 2001 From: Eldad Fux Date: Wed, 21 Oct 2020 18:03:33 +0300 Subject: [PATCH 017/267] Updated spec --- docs/specs/realtime.drawio.svg | 503 ++++++++++++++++++++++++++++++++- 1 file changed, 502 insertions(+), 1 deletion(-) diff --git a/docs/specs/realtime.drawio.svg b/docs/specs/realtime.drawio.svg index 93788ddd2a..e2f07ef7f5 100644 --- a/docs/specs/realtime.drawio.svg +++ b/docs/specs/realtime.drawio.svg @@ -1 +1,502 @@ -
Appwrite API
Appwrite API
Send all events
Send all events
Messaging Worker
Messaging Worker
Redis - Message Log
Redis - Message Log
Expire Every x seconds
Expire Every x secon...
Websocket 1
Websocket 1
Connections
Connections
Events
Events
Authorization
Authorization
Websocket 2
Websocket 2
Websocket 3
Websocket 3
Pull
Pull
Connections
Connections
Events
Events
Authorization
Authorization
Connections
Connections
Events
Events
Authorization
Authorization
Clients
Clients
LoadBalancer
LoadBalancer

Real Time
Specification for adding, a sclabale, realtime stream to trasmit Appwrite system and user generated events to any platform.

Real Time...
Viewer does not support full SVG 1.1
\ No newline at end of file + + + + + + + + + + + +
+
+
+ Appwrite API +
+
+
+
+ + Appwrite API + +
+
+ + + + +
+
+
+ Send all events +
+
+
+
+ + Send all events + +
+
+ + + + + + +
+
+
+ Messaging Worker +
+
+
+
+ + Messaging Worker + +
+
+ + + + +
+
+
+ Redis - PUB/SUB +
+
+
+
+ + Redis - PUB/SUB + +
+
+ + + + + + + + +
+
+
+ Websocket 1 +
+
+
+
+ + Websocket 1 + +
+
+ + + + + + +
+
+
+ Connections +
+
+
+
+ + Connections + +
+
+ + + + + + +
+
+
+ Events +
+
+
+
+ + Events + +
+
+ + + + +
+
+
+ Authorization +
+
+
+
+ + Authorization + +
+
+ + + + + + + + +
+
+
+ Websocket 2 +
+
+
+
+ + Websocket 2 + +
+
+ + + + + + + + +
+
+
+ Websocket 3 +
+
+
+
+ + Websocket 3 + +
+
+ + + + +
+
+
+ Pull +
+
+
+
+ + Pull + +
+
+ + + + +
+
+
+ Connections +
+
+
+
+ + Connections + +
+
+ + + + +
+
+
+ Events +
+
+
+
+ + Events + +
+
+ + + + +
+
+
+ Authorization +
+
+
+
+ + Authorization + +
+
+ + + + +
+
+
+ Connections +
+
+
+
+ + Connections + +
+
+ + + + +
+
+
+ Events +
+
+
+
+ + Events + +
+
+ + + + +
+
+
+ Authorization +
+
+
+
+ + Authorization + +
+
+ + + + + + + + + + + + + + +
+
+
+ Clients +
+
+
+
+ + Clients + +
+
+ + + + + + + + + + +
+
+
+ LoadBalancer +
+
+
+
+ + LoadBalancer + +
+
+ + + + +
+
+
+

+ + Real Time +
+
+ + Specification for adding, a sclabale, realtime stream to trasmit Appwrite system and user generated events to any platform. + +
+

+
+
+
+
+ + Real Time... + +
+
+ + + + +
+
+
+ Pull +
+
+
+
+ + Pull + +
+
+ + + + +
+
+
+ Pull +
+
+
+
+ + Pull + +
+
+ + + + +
+
+
+ Websocket +
+
+
+
+ + Websocket + +
+
+ + + + +
+
+
+ MQTT +
+
+
+
+ + MQTT + +
+
+ + + + +
+
+
+ Socket.io +
+
+
+
+ + Socket.io + +
+
+ + + + +
+
+
+ SSE +
+
+
+
+ + SSE + +
+
+
+ + + + + Viewer does not support full SVG 1.1 + + + +
\ No newline at end of file From f4f66256f4d74ba9f11401855d38f81be6e2650b Mon Sep 17 00:00:00 2001 From: Eldad Fux Date: Thu, 22 Oct 2020 10:16:40 +0300 Subject: [PATCH 018/267] Now tracking all user roles --- app/realtime.php | 58 +++++++++++++++++++++++++++++++----------------- 1 file changed, 38 insertions(+), 20 deletions(-) diff --git a/app/realtime.php b/app/realtime.php index 48ea450927..9c3927cc80 100644 --- a/app/realtime.php +++ b/app/realtime.php @@ -3,6 +3,7 @@ require_once __DIR__.'/init.php'; require_once __DIR__.'/../vendor/autoload.php'; +use Appwrite\Auth\Auth; use Appwrite\Swoole\Request as SwooleRequest; use Swoole\WebSocket\Server; use Swoole\Http\Request; @@ -44,7 +45,8 @@ $server->on("workerStart", function ($server, $workerId) use (&$subscriptions) { while ($attempts < 300) { try { if($attempts > 0) { - Console::error('Pub/sub connection lost (lasted '.(time() - $start).' seconds). Attempting restart in 5 seconds (attempt #'.$attempts.')'); + Console::error('Pub/sub connection lost (lasted '.(time() - $start).' seconds, worker: '.$workerId.'). + Attempting restart in 5 seconds (attempt #'.$attempts.')'); sleep(5); // 1 sec delay between connection attempts } @@ -54,10 +56,10 @@ $server->on("workerStart", function ($server, $workerId) use (&$subscriptions) { if($redis->ping(true)) { $attempts = 0; - Console::success('Pub/sub connection established'); + Console::success('Pub/sub connection established (worker: '.$workerId.')'); } else { - Console::error('Pub/sub failed'); + Console::error('Pub/sub failed (worker: '.$workerId.')'); } $redis->subscribe(['realtime'], function($redis, $channel, $message) use ($server, $workerId) { @@ -105,6 +107,7 @@ $server->on("start", function (Server $server) { $server->on('open', function(Server $server, Request $request) use (&$connections, &$subscriptions) { Console::info("Connection open (user: {$request->fd}, worker: {$server->getWorkerId()})"); + $app = new App(''); $connection = $request->fd; $request = new SwooleRequest($request); @@ -117,8 +120,9 @@ $server->on('open', function(Server $server, Request $request) use (&$connection }); $channels = array_flip($request->getQuery('channels', [])); - $user = App::getResource('user'); - $project = App::getResource('project'); + $user = $app->getResource('user'); + $project = $app->getResource('project'); + $roles = ['user:'.$user->getId(), 'role:'.(($user->isEmpty()) ? Auth::USER_ROLE_GUEST : Auth::USER_ROLE_MEMBER)]; /** @var Appwrite\Database\Document $user */ /** @var Appwrite\Database\Document $project */ @@ -128,21 +132,33 @@ $server->on('open', function(Server $server, Request $request) use (&$connection var_dump($user->getId()); var_dump($user->getAttribute('name')); + \array_map(function ($node) use (&$roles) { + 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; + } + } + }, $user->getAttribute('memberships', [])); + if(!isset($subscriptions[$project->getId()])) { // Init Project $subscriptions[$project->getId()] = []; } - if(!isset($subscriptions[$project->getId()][$user->getId()])) { // Add user first connection - $subscriptions[$project->getId()][$user->getId()] = []; - } - - foreach ($channels as $channel => $list) { - $subscriptions[$project->getId()][$user->getId()][$channel][$connection] = true; + foreach ($roles as $key => $role) { + if(!isset($subscriptions[$project->getId()][$role])) { // Add user first connection + $subscriptions[$project->getId()][$role] = []; + } + + foreach ($channels as $channel => $list) { + $subscriptions[$project->getId()][$role][$channel][$connection] = true; + } } $connections[$connection] = [ 'projectId' => $project->getId(), - 'userId' => $user->getId() + 'roles' => $roles, ]; $server->push($connection, json_encode($subscriptions)); @@ -162,18 +178,20 @@ $server->on('close', function(Server $server, int $fd) use (&$connections, &$sub Console::error('Connection close: '.$fd); $projectId = $connections[$fd]['projectId'] ?? ''; - $userId = $connections[$fd]['userId'] ?? ''; + $roles = $connections[$fd]['roles'] ?? []; - foreach ($subscriptions[$projectId][$userId] as $channel => $list) { - unset($subscriptions[$projectId][$userId][$channel][$fd]); // Remove connection + foreach ($roles as $key => $role) { + foreach ($subscriptions[$projectId][$role] as $channel => $list) { + unset($subscriptions[$projectId][$role][$channel][$fd]); // Remove connection - if(empty($list)) { - unset($subscriptions[$projectId][$userId][$channel]); // Remove channel + if(empty($subscriptions[$projectId][$role][$channel])) { + unset($subscriptions[$projectId][$role][$channel]); // Remove channel + } } - } - if(empty($subscriptions[$projectId][$userId])) { - unset($subscriptions[$projectId][$userId]); // Remove user + if(empty($subscriptions[$projectId][$role])) { + unset($subscriptions[$projectId][$role]); // Remove role + } } unset($connections[$fd]); From c8be0a0e040a3f82c12c55c615c33ef55c80e778 Mon Sep 17 00:00:00 2001 From: Eldad Fux Date: Thu, 22 Oct 2020 15:31:41 +0300 Subject: [PATCH 019/267] Updated Redis client --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 519d2a01cf..d413f2a37c 100755 --- a/Dockerfile +++ b/Dockerfile @@ -15,7 +15,7 @@ RUN composer update --ignore-platform-reqs --optimize-autoloader \ FROM php:7.4-cli-alpine as step1 ENV TZ=Asia/Tel_Aviv \ - PHP_REDIS_VERSION=5.3.2RC2 \ + PHP_REDIS_VERSION=5.3.2 \ PHP_SWOOLE_VERSION=4.5.5 \ PHP_XDEBUG_VERSION=sdebug_2_9-beta From 081943ce0350e319c9cce5d287b1bd6f59c5574b Mon Sep 17 00:00:00 2001 From: Eldad Fux Date: Sat, 9 Jan 2021 23:58:21 +0200 Subject: [PATCH 020/267] Updated port --- docker-compose.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index 553b82c410..177dee3cd0 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -125,9 +125,10 @@ services: context: . restart: unless-stopped ports: - - 9502:80 + - 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 From 23ed7284fa7a7d3343b33ae94c7fb2a9ec576698 Mon Sep 17 00:00:00 2001 From: Eldad Fux Date: Sun, 21 Feb 2021 19:15:03 +0200 Subject: [PATCH 021/267] Updated port numbers --- docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index 3840bc42fe..6fef7a4e20 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -515,7 +515,7 @@ services: image: adminer restart: always ports: - - 9505:8080 + - 9506:8080 networks: - appwrite From 780841853e82543611af579829e74c526f0af71c Mon Sep 17 00:00:00 2001 From: Eldad Fux Date: Sun, 21 Feb 2021 23:22:32 +0200 Subject: [PATCH 022/267] Fixed code to work with 0.7 changes --- app/controllers/api/health.php | 3 +- app/realtime.php | 96 ++++++++++++++++++++++++---------- docker-compose.yml | 3 ++ 3 files changed, 74 insertions(+), 28 deletions(-) diff --git a/app/controllers/api/health.php b/app/controllers/api/health.php index 8d1b797b66..c8559278f1 100644 --- a/app/controllers/api/health.php +++ b/app/controllers/api/health.php @@ -37,6 +37,7 @@ App::get('/v1/health/realtime') ->desc('Get Realtime') ->groups(['api', 'health']) ->label('scope', 'public') + ->inject('response') ->action(function ($response) { /** @var Utopia\Response $response */ $redis = new Redis(); @@ -44,7 +45,7 @@ App::get('/v1/health/realtime') $redis->publish('realtime', 'I\'m a live message'); $response->json(['status' => 'OK']); - }, ['response']); + }); App::get('/v1/health/db') ->desc('Get DB') diff --git a/app/realtime.php b/app/realtime.php index 9c3927cc80..2779d708fd 100644 --- a/app/realtime.php +++ b/app/realtime.php @@ -4,14 +4,14 @@ require_once __DIR__.'/init.php'; require_once __DIR__.'/../vendor/autoload.php'; use Appwrite\Auth\Auth; -use Appwrite\Swoole\Request as SwooleRequest; +use Appwrite\Database\Document; use Swoole\WebSocket\Server; use Swoole\Http\Request; use Swoole\Process; use Swoole\WebSocket\Frame; use Utopia\App; use Utopia\CLI\Console; -use Utopia\Route; +use Utopia\Swoole\Request as SwooleRequest; /** * TODO List @@ -36,7 +36,7 @@ $server = new Server("0.0.0.0", 80); $subscriptions = []; $connections = []; -$server->on("workerStart", function ($server, $workerId) use (&$subscriptions) { +$server->on("workerStart", function ($server, $workerId) use (&$subscriptions, &$connections) { Console::success('Worker '.++$workerId.' started succefully'); $attempts = 0; @@ -62,22 +62,37 @@ $server->on("workerStart", function ($server, $workerId) use (&$subscriptions) { Console::error('Pub/sub failed (worker: '.$workerId.')'); } - $redis->subscribe(['realtime'], function($redis, $channel, $message) use ($server, $workerId) { + $redis->subscribe(['realtime'], function($redis, $channel, $message) use ($server, $workerId, &$connections) { $message = 'Message from worker #'.$workerId.'; '.$message; - - // foreach($connections as $fd) { - // if ($server->exist($fd) - // && $server->isEstablished($fd) - // ) { - // Console::info('Sending message: '.$message.' (user: '.$fd.', worker: '.$workerId.')'); - // $server->push($fd, $message, SWOOLE_WEBSOCKET_OPCODE_TEXT, - // SWOOLE_WEBSOCKET_FLAG_FIN | SWOOLE_WEBSOCKET_FLAG_COMPRESS); - // } - // else { - // $server->close($fd); - // } - // } + // TODO get project and resource ID and itterate over the resource read(?) permissions and send a message to all listeners + + /** + * Supported Resources: + * - Collection + * - Document + * - Bucket + * - File + * - User? / Account? (no permissions) + * - Session? (no permissions) + * - Team? (no permissions) + * - Membership? (no permissions) + * - Function + * - Execution + */ + + foreach($connections as $fd => $connection) { + if ($server->exist($fd) + && $server->isEstablished($fd) + ) { + Console::info('Sending message: '.$message.' (user: '.$fd.', worker: '.$workerId.')'); + $server->push($fd, $message, SWOOLE_WEBSOCKET_OPCODE_TEXT, + SWOOLE_WEBSOCKET_FLAG_FIN | SWOOLE_WEBSOCKET_FLAG_COMPRESS); + } + else { + $server->close($fd); + } + } }); } catch (\Throwable $th) { @@ -119,19 +134,23 @@ $server->on('open', function(Server $server, Request $request) use (&$connection return null; }); + App::setResource('project', function () { // TODO get project from query string + return new Document(); + }); + + App::setResource('user', function () { // TODO get user with JWT token + return new Document(); + }); + $channels = array_flip($request->getQuery('channels', [])); + $jwt = $request->getQuery('jwt', ''); $user = $app->getResource('user'); $project = $app->getResource('project'); - $roles = ['user:'.$user->getId(), 'role:'.(($user->isEmpty()) ? Auth::USER_ROLE_GUEST : Auth::USER_ROLE_MEMBER)]; + $roles = ['*', 'user:'.$user->getId(), 'role:'.(($user->isEmpty()) ? Auth::USER_ROLE_GUEST : Auth::USER_ROLE_MEMBER)]; /** @var Appwrite\Database\Document $user */ /** @var Appwrite\Database\Document $project */ - var_dump($project->getId()); - var_dump($project->getAttribute('name')); - var_dump($user->getId()); - var_dump($user->getAttribute('name')); - \array_map(function ($node) use (&$roles) { if (isset($node['teamId']) && isset($node['roles'])) { $roles[] = 'team:'.$node['teamId']; @@ -142,6 +161,20 @@ $server->on('open', function(Server $server, Request $request) use (&$connection } }, $user->getAttribute('memberships', [])); + /** + * Build Subscriptions 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] + */ + if(!isset($subscriptions[$project->getId()])) { // Init Project $subscriptions[$project->getId()] = []; } @@ -161,6 +194,11 @@ $server->on('open', function(Server $server, Request $request) use (&$connection 'roles' => $roles, ]; + var_dump($project->getId()); + var_dump($project->getAttribute('name')); + var_dump($user->getId()); + var_dump($user->getAttribute('name')); + $server->push($connection, json_encode($subscriptions)); }); @@ -175,8 +213,6 @@ $server->on('message', function(Server $server, Frame $frame) { }); $server->on('close', function(Server $server, int $fd) use (&$connections, &$subscriptions) { - Console::error('Connection close: '.$fd); - $projectId = $connections[$fd]['projectId'] ?? ''; $roles = $connections[$fd]['roles'] ?? []; @@ -185,17 +221,23 @@ $server->on('close', function(Server $server, int $fd) use (&$connections, &$sub unset($subscriptions[$projectId][$role][$channel][$fd]); // Remove connection if(empty($subscriptions[$projectId][$role][$channel])) { - unset($subscriptions[$projectId][$role][$channel]); // Remove channel + unset($subscriptions[$projectId][$role][$channel]); // Remove channel when no connections } } if(empty($subscriptions[$projectId][$role])) { - unset($subscriptions[$projectId][$role]); // Remove role + unset($subscriptions[$projectId][$role]); // Remove role when no channels } } + if(empty($subscriptions[$projectId])) { // Remove project when no roles + unset($subscriptions[$projectId]); + } + unset($connections[$fd]); + Console::info('Connection close: '.$fd); + var_dump($subscriptions); }); diff --git a/docker-compose.yml b/docker-compose.yml index 6fef7a4e20..9d9577a9f5 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -145,6 +145,9 @@ services: - 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: From eea1525c8bba541db75ae252bbff3178dd686a88 Mon Sep 17 00:00:00 2001 From: Eldad Fux Date: Mon, 22 Feb 2021 13:11:43 +0200 Subject: [PATCH 023/267] Updated architecture --- docs/specs/overview.drawio.svg | 75 +++++++++++++++++++++------------- 1 file changed, 47 insertions(+), 28 deletions(-) diff --git a/docs/specs/overview.drawio.svg b/docs/specs/overview.drawio.svg index db04e4c962..516749492d 100644 --- a/docs/specs/overview.drawio.svg +++ b/docs/specs/overview.drawio.svg @@ -1,4 +1,4 @@ - + @@ -117,10 +117,12 @@ - - + + + + @@ -138,8 +140,6 @@ - - @@ -157,42 +157,40 @@ - + - + - + - + - + - + - + - - - + - - - + + + - + -
+
- APIs + REST API
- - APIs + + REST API @@ -425,10 +423,12 @@ - - - - + + + + + + @@ -724,10 +724,29 @@ + + + + + + +
+
+
+ REALTIME API +
+
+
+
+ + REALTIME API + +
+
- + Viewer does not support full SVG 1.1 From 56cbc3227245a3480a35f5333f5c32e49e086a6a Mon Sep 17 00:00:00 2001 From: Eldad Fux Date: Tue, 23 Feb 2021 09:39:59 +0200 Subject: [PATCH 024/267] Cleanup --- app/realtime.php | 8 -------- 1 file changed, 8 deletions(-) diff --git a/app/realtime.php b/app/realtime.php index 2779d708fd..dd4f736e56 100644 --- a/app/realtime.php +++ b/app/realtime.php @@ -134,14 +134,6 @@ $server->on('open', function(Server $server, Request $request) use (&$connection return null; }); - App::setResource('project', function () { // TODO get project from query string - return new Document(); - }); - - App::setResource('user', function () { // TODO get user with JWT token - return new Document(); - }); - $channels = array_flip($request->getQuery('channels', [])); $jwt = $request->getQuery('jwt', ''); $user = $app->getResource('user'); From f4f0f32468340ec1b678fecfaf43712e0ef4c628 Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Wed, 24 Feb 2021 13:19:46 +0100 Subject: [PATCH 025/267] add realtime event --- app/controllers/shared/api.php | 11 ++- app/init.php | 5 ++ src/Appwrite/Event/Realtime.php | 136 ++++++++++++++++++++++++++++++++ 3 files changed, 150 insertions(+), 2 deletions(-) create mode 100644 src/Appwrite/Event/Realtime.php diff --git a/app/controllers/shared/api.php b/app/controllers/shared/api.php index 8dc0c097c7..e6ab3b40f4 100644 --- a/app/controllers/shared/api.php +++ b/app/controllers/shared/api.php @@ -110,7 +110,7 @@ App::init(function ($utopia, $request, $response, $project, $user, $register, $e }, ['utopia', 'request', 'response', 'project', 'user', 'register', 'events', 'audits', 'usage', 'deletes'], 'api'); -App::shutdown(function ($utopia, $request, $response, $project, $events, $audits, $usage, $deletes, $mode) { +App::shutdown(function ($utopia, $request, $response, $project, $events, $audits, $usage, $deletes, $realtime, $mode) { /** @var Utopia\App $utopia */ /** @var Utopia\Swoole\Request $request */ /** @var Appwrite\Utopia\Response $response */ @@ -119,6 +119,7 @@ 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\Realtime $realtime */ /** @var Appwrite\Event\Event $functions */ /** @var bool $mode */ @@ -139,6 +140,12 @@ App::shutdown(function ($utopia, $request, $response, $project, $events, $audits ->setQueue('v1-functions') ->setClass('FunctionsV1') ->trigger(); + + $realtime + ->setEvent($events->getParam('event')) + ->setPayload($response->getPayload()) + ->trigger(); + } if (!empty($audits->getParam('event'))) { @@ -162,4 +169,4 @@ App::shutdown(function ($utopia, $request, $response, $project, $events, $audits ; } -}, ['utopia', 'request', 'response', 'project', 'events', 'audits', 'usage', 'deletes', 'mode'], 'api'); +}, ['utopia', 'request', 'response', 'project', 'events', 'audits', 'usage', 'deletes', 'realtime', 'mode'], 'api'); diff --git a/app/init.php b/app/init.php index a19b9c0e05..a4d2914b09 100644 --- a/app/init.php +++ b/app/init.php @@ -20,6 +20,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\Extend\PDO; use Appwrite\OpenSSL\OpenSSL; use Utopia\App; @@ -321,6 +322,10 @@ App::setResource('events', function($register) { return new Event('', ''); }, ['register']); +App::setResource('realtime', function($register) { + return new Realtime('', []); +}, ['register']); + App::setResource('audits', function($register) { return new Event(Event::AUDITS_QUEUE_NAME, Event::AUDITS_CLASS_NAME); }, ['register']); diff --git a/src/Appwrite/Event/Realtime.php b/src/Appwrite/Event/Realtime.php new file mode 100644 index 0000000000..e1f1f887f9 --- /dev/null +++ b/src/Appwrite/Event/Realtime.php @@ -0,0 +1,136 @@ +event = $event; + $this->payload = new Document($payload); + } + + /** + * @param string $event + * return $this + */ + public function setEvent(string $event): self + { + $this->event = $event; + return $this; + } + + /** + * @return string + */ + public function getEvent(): string + { + return $this->event; + } + + /** + * @param array $payload + * return $this + */ + public function setPayload(array $payload): self + { + $this->payload = new Document($payload); + return $this; + } + + /** + * @return Document + */ + public function getPayload(): Document + { + return $this->payload; + } + + /** + * Populate channels array based on the event name and payload. + * + * @return void + */ + private function prepareChannels(): void + { + switch (true) { + case strpos($this->event, 'account.') === 0: + $this->channels[] = 'account.' . $this->payload->getId(); + + break; + case strpos($this->event, 'database.collections.') === 0: + $this->channels[] = 'collections'; + $this->channels[] = 'collections.' . $this->payload->getId(); + + break; + case strpos($this->event, 'database.documents.') === 0: + $this->channels[] = 'documents'; + $this->channels[] = 'collections.' . $this->payload->getAttribute('$collection') . '.documents'; + $this->channels[] = 'documents.' . $this->payload->getId(); + + break; + case strpos($this->event, 'storage.') === 0: + $this->channels[] = 'files'; + $this->channels[] = 'files.' . $this->payload->getId(); + + break; + } + } + + /** + * Execute Event. + * + * @return void + */ + public function trigger(): void + { + $this->prepareChannels(); + if (empty($this->channels)) return; + + $redis = new \Redis(); + $redis->connect(App::getEnv('_APP_REDIS_HOST', ''), App::getEnv('_APP_REDIS_PORT', '')); + $redis->publish('realtime', json_encode([ + 'channels' => $this->channels, + 'data' => [ + 'event' => $this->event, + 'timestamp' => time(), + 'payload' => $this->payload + ] + ])); + + $this->reset(); + } + + public function reset(): self + { + $this->event = ''; + $this->payload = $this->channels = []; + + return $this; + } +} From 46f64b4faafd08e3f81ef26fc7445ea83061816f Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Wed, 24 Feb 2021 18:12:38 +0100 Subject: [PATCH 026/267] add realtime prototype --- app/controllers/shared/api.php | 1 + app/init.php | 2 +- app/realtime.php | 225 +++++++++++++++++++++++--------- src/Appwrite/Event/Realtime.php | 31 ++++- 4 files changed, 195 insertions(+), 64 deletions(-) diff --git a/app/controllers/shared/api.php b/app/controllers/shared/api.php index e6ab3b40f4..69ffdf4f2f 100644 --- a/app/controllers/shared/api.php +++ b/app/controllers/shared/api.php @@ -143,6 +143,7 @@ App::shutdown(function ($utopia, $request, $response, $project, $events, $audits $realtime ->setEvent($events->getParam('event')) + ->setProject($project->getId()) ->setPayload($response->getPayload()) ->trigger(); diff --git a/app/init.php b/app/init.php index a4d2914b09..98ff4c9235 100644 --- a/app/init.php +++ b/app/init.php @@ -323,7 +323,7 @@ App::setResource('events', function($register) { }, ['register']); App::setResource('realtime', function($register) { - return new Realtime('', []); + return new Realtime('', '', []); }, ['register']); App::setResource('audits', function($register) { diff --git a/app/realtime.php b/app/realtime.php index 2779d708fd..aae73af2bd 100644 --- a/app/realtime.php +++ b/app/realtime.php @@ -1,18 +1,29 @@ set([ + 'worker_num' => 1 + ]); $subscriptions = []; $connections = []; -$server->on("workerStart", function ($server, $workerId) use (&$subscriptions, &$connections) { - Console::success('Worker '.++$workerId.' started succefully'); +$register = new Registry(); + +$register->set('db', function () { // Register DB connection + $dbHost = App::getEnv('_APP_DB_HOST', ''); + $dbUser = App::getEnv('_APP_DB_USER', ''); + $dbPass = App::getEnv('_APP_DB_PASS', ''); + $dbScheme = App::getEnv('_APP_DB_SCHEMA', ''); + + $pdo = new PDO("mysql:host={$dbHost};dbname={$dbScheme};charset=utf8mb4", $dbUser, $dbPass, array( + PDONative::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8mb4', + PDONative::ATTR_TIMEOUT => 3, // Seconds + PDONative::ATTR_PERSISTENT => true + )); + + // Connection settings + $pdo->setAttribute(PDONative::ATTR_DEFAULT_FETCH_MODE, PDONative::FETCH_ASSOC); // Return arrays + $pdo->setAttribute(PDONative::ATTR_ERRMODE, PDONative::ERRMODE_EXCEPTION); // Handle all errors with exceptions + + return $pdo; +}); + +$register->set('cache', function () { // Register cache connection + $redis = new Redis(); + $redis->pconnect(App::getEnv('_APP_REDIS_HOST', ''), App::getEnv('_APP_REDIS_PORT', '')); + $user = App::getEnv('_APP_REDIS_USER',''); + $pass = App::getEnv('_APP_REDIS_PASS',''); + $auth = []; + if(!empty($user)) { + $auth["user"] = $user; + } + if(!empty($pass)) { + $auth["pass"] = $pass; + } + if(!empty($auth)) { + $redis->auth($auth); + } + $redis->setOption(Redis::OPT_READ_TIMEOUT, -1); + + return $redis; +}); + +$server->on("workerStart", function ($server, $workerId) use (&$subscriptions, &$connections, &$register) { + Console::success('Worker ' . ++$workerId . ' started succefully'); $attempts = 0; $start = time(); - + 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.')'); + if ($attempts > 0) { + Console::error('Pub/sub connection lost (lasted ' . (time() - $start) . ' seconds, worker: ' . $workerId . '). + Attempting restart in 5 seconds (attempt #' . $attempts . ')'); sleep(5); // 1 sec delay between connection attempts } - $redis = new Redis(); - $redis->connect('redis', 6379); - $redis->setOption(Redis::OPT_READ_TIMEOUT, -1); + $redis = $register->get('cache'); - if($redis->ping(true)) { + if ($redis->ping(true)) { $attempts = 0; - Console::success('Pub/sub connection established (worker: '.$workerId.')'); - } - else { - Console::error('Pub/sub failed (worker: '.$workerId.')'); + Console::success('Pub/sub connection established (worker: ' . $workerId . ')'); + } else { + Console::error('Pub/sub failed (worker: ' . $workerId . ')'); } - $redis->subscribe(['realtime'], function($redis, $channel, $message) use ($server, $workerId, &$connections) { - $message = 'Message from worker #'.$workerId.'; '.$message; - + $redis->subscribe(['realtime'], function ($redis, $channel, $payload) use ($server, $workerId, &$connections, &$subscriptions) { // TODO get project and resource ID and itterate over the resource read(?) permissions and send a message to all listeners /** @@ -80,23 +130,43 @@ $server->on("workerStart", function ($server, $workerId) use (&$subscriptions, & * - Function * - Execution */ - - foreach($connections as $fd => $connection) { - if ($server->exist($fd) - && $server->isEstablished($fd) + $event = json_decode($payload); + + $receivers = []; + + foreach ($connections as $fd => $connection) { + if ($connection['projectId'] !== $event->project) { + continue; + } + + foreach ($connection['roles'] as $role) { + if (\array_key_exists($role, $subscriptions[$event->project])) { + foreach ($event->channels as $channel) { + if (\array_key_exists($channel, $subscriptions[$event->project][$role]) && \in_array($role, $event->permissions)) { + $receivers = array_merge($receivers, array_keys($subscriptions[$event->project][$role][$channel])); + break; + } + } + } + } + } + + $receivers = array_keys(array_flip($receivers)); + + foreach ($receivers as $receiver) { + if ($server->exist($receiver) + && $server->isEstablished($receiver) ) { - Console::info('Sending message: '.$message.' (user: '.$fd.', worker: '.$workerId.')'); - $server->push($fd, $message, SWOOLE_WEBSOCKET_OPCODE_TEXT, + $server->push($receiver, json_encode($event->data), SWOOLE_WEBSOCKET_OPCODE_TEXT, SWOOLE_WEBSOCKET_FLAG_FIN | SWOOLE_WEBSOCKET_FLAG_COMPRESS); } else { - $server->close($fd); + $server->close($receiver); } } }); - } catch (\Throwable $th) { - Console::error('Pub/sub error: '.$th->getMessage()); + Console::error('Pub/sub error: ' . $th->getMessage()); $attempts++; continue; } @@ -119,7 +189,7 @@ $server->on("start", function (Server $server) { }); }); -$server->on('open', function(Server $server, Request $request) use (&$connections, &$subscriptions) { +$server->on('open', function (Server $server, Request $request) use (&$connections, &$subscriptions, &$register) { Console::info("Connection open (user: {$request->fd}, worker: {$server->getWorkerId()})"); $app = new App(''); @@ -130,33 +200,74 @@ $server->on('open', function(Server $server, Request $request) use (&$connection return $request; }); - App::setResource('response', function () { - return null; - }); + App::setResource('consoleDB', function () use (&$register) { + $consoleDB = new Database(); + $consoleDB->setAdapter(new MySQLAdapter($register)); // TODO: Add Redis + $consoleDB->setNamespace('app_console'); // Should be replaced with param if we want to have parent projects + $consoleDB->setMocks(Config::getParam('collections', [])); - App::setResource('project', function () { // TODO get project from query string - return new Document(); - }); + return $consoleDB; + }, ['register']); - App::setResource('user', function () { // TODO get user with JWT token - return new Document(); - }); + App::setResource('project', function ($consoleDB, $request) { + /** @var Utopia\Swoole\Request $request */ + /** @var Appwrite\Database\Database $consoleDB */ + + Authorization::disable(); + + $project = $consoleDB->getDocument($request->getQuery('project')); + + Authorization::reset(); + + return $project; + }, ['consoleDB', 'request']); + + App::setResource('user', function ($project, $request, $projectDB) { + /** @var Utopia\Swoole\Request $request */ + /** @var Appwrite\Database\Document $project */ + /** @var Appwrite\Database\Database $projectDB */ + + Authorization::setDefaultStatus(true); + + Auth::setCookieName('a_session_' . $project->getId()); + + $session = Auth::decodeSession( + $request->getCookie( + Auth::$cookieName, // Get sessions + $request->getCookie(Auth::$cookieName . '_legacy', '') + ) + ); // Get fallback session from old clients (no SameSite support) + + 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::tokenVerify($user->getAttribute('tokens', []), Auth::TOKEN_TYPE_LOGIN, Auth::$secret) + ) { // Validate user has valid login token + $user = new Document(['$id' => '', '$collection' => Database::SYSTEM_COLLECTION_USERS]); + } + + return $user; + }, ['project', 'request', 'projectDB']); $channels = array_flip($request->getQuery('channels', [])); - $jwt = $request->getQuery('jwt', ''); $user = $app->getResource('user'); $project = $app->getResource('project'); - $roles = ['*', 'user:'.$user->getId(), 'role:'.(($user->isEmpty()) ? Auth::USER_ROLE_GUEST : Auth::USER_ROLE_MEMBER)]; + $roles = ['*', 'user:' . $user->getId(), 'role:' . (($user->isEmpty()) ? Auth::USER_ROLE_GUEST : Auth::USER_ROLE_MEMBER)]; /** @var Appwrite\Database\Document $user */ /** @var Appwrite\Database\Document $project */ \array_map(function ($node) use (&$roles) { if (isset($node['teamId']) && isset($node['roles'])) { - $roles[] = 'team:'.$node['teamId']; + $roles[] = 'team:' . $node['teamId']; foreach ($node['roles'] as $nodeRole) { // Set all team roles - $roles[] = 'team:'.$node['teamId'].'/'.$nodeRole; + $roles[] = 'team:' . $node['teamId'] . '/' . $nodeRole; } } }, $user->getAttribute('memberships', [])); @@ -175,15 +286,15 @@ $server->on('open', function(Server $server, Request $request) use (&$connection * [CHANNEL_NAME_Z] -> [CONNECTION_ID] */ - if(!isset($subscriptions[$project->getId()])) { // Init Project + if (!isset($subscriptions[$project->getId()])) { // Init Project $subscriptions[$project->getId()] = []; } foreach ($roles as $key => $role) { - if(!isset($subscriptions[$project->getId()][$role])) { // Add user first connection + if (!isset($subscriptions[$project->getId()][$role])) { // Add user first connection $subscriptions[$project->getId()][$role] = []; } - + foreach ($channels as $channel => $list) { $subscriptions[$project->getId()][$role][$channel][$connection] = true; } @@ -194,25 +305,18 @@ $server->on('open', function(Server $server, Request $request) use (&$connection 'roles' => $roles, ]; - var_dump($project->getId()); - var_dump($project->getAttribute('name')); - var_dump($user->getId()); - var_dump($user->getAttribute('name')); - $server->push($connection, json_encode($subscriptions)); }); -$server->on('message', function(Server $server, Frame $frame) { - if($frame->data === 'reload') { +$server->on('message', function (Server $server, Frame $frame) use (&$connections, &$subscriptions) { + if ($frame->data === 'reload') { $server->reload(); } - Console::info('Recieved message: '.$frame->data.' (user: '.$frame->fd.', worker: '.$server->getWorkerId().')'); - - $server->push($frame->fd, json_encode(["hello, worker_id:".$server->getWorkerId(), time()])); + Console::info('Recieved message: ' . $frame->data . ' (user: ' . $frame->fd . ', worker: ' . $server->getWorkerId() . ')'); }); -$server->on('close', function(Server $server, int $fd) use (&$connections, &$subscriptions) { +$server->on('close', function (Server $server, int $fd) use (&$connections, &$subscriptions) { $projectId = $connections[$fd]['projectId'] ?? ''; $roles = $connections[$fd]['roles'] ?? []; @@ -220,25 +324,24 @@ $server->on('close', function(Server $server, int $fd) use (&$connections, &$sub foreach ($subscriptions[$projectId][$role] as $channel => $list) { unset($subscriptions[$projectId][$role][$channel][$fd]); // Remove connection - if(empty($subscriptions[$projectId][$role][$channel])) { + if (empty($subscriptions[$projectId][$role][$channel])) { unset($subscriptions[$projectId][$role][$channel]); // Remove channel when no connections } } - if(empty($subscriptions[$projectId][$role])) { + if (empty($subscriptions[$projectId][$role])) { unset($subscriptions[$projectId][$role]); // Remove role when no channels } } - if(empty($subscriptions[$projectId])) { // Remove project when no roles + if (empty($subscriptions[$projectId])) { // Remove project when no roles unset($subscriptions[$projectId]); } unset($connections[$fd]); - Console::info('Connection close: '.$fd); + Console::info('Connection close: ' . $fd); - var_dump($subscriptions); }); $server->start(); \ No newline at end of file diff --git a/src/Appwrite/Event/Realtime.php b/src/Appwrite/Event/Realtime.php index e1f1f887f9..93f2f3050d 100644 --- a/src/Appwrite/Event/Realtime.php +++ b/src/Appwrite/Event/Realtime.php @@ -7,6 +7,11 @@ use Utopia\App; class Realtime { + /** + * @var string + */ + protected $project = ''; + /** * @var string */ @@ -26,15 +31,35 @@ class Realtime /** * Event constructor. * + * @param string $project * @param string $event * @param array $payload */ - public function __construct(string $event, array $payload) + public function __construct(string $project, string $event, array $payload) { + $this->project = $project; $this->event = $event; $this->payload = new Document($payload); } + /** + * @param string $project + * return $this + */ + public function setProject(string $project): self + { + $this->project = $project; + return $this; + } + + /** + * @return string + */ + public function getProject(): string + { + return $this->project; + } + /** * @param string $event * return $this @@ -115,11 +140,13 @@ class Realtime $redis = new \Redis(); $redis->connect(App::getEnv('_APP_REDIS_HOST', ''), App::getEnv('_APP_REDIS_PORT', '')); $redis->publish('realtime', json_encode([ + 'project' => $this->project, 'channels' => $this->channels, + 'permissions' => $this->payload->getAttribute('$permissions.read'), 'data' => [ 'event' => $this->event, 'timestamp' => time(), - 'payload' => $this->payload + 'payload' => $this->payload->getArrayCopy() ] ])); From a70cb90be103b936761af720d76297791671364f Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Thu, 25 Feb 2021 11:43:39 +0100 Subject: [PATCH 027/267] add channels to realtime response --- app/realtime.php | 76 ++++++++++++++++++--------------- src/Appwrite/Event/Realtime.php | 2 +- 2 files changed, 43 insertions(+), 35 deletions(-) diff --git a/app/realtime.php b/app/realtime.php index aae73af2bd..d5ecfa9e9d 100644 --- a/app/realtime.php +++ b/app/realtime.php @@ -19,10 +19,7 @@ use Utopia\CLI\Console; use Utopia\Config\Config; use Utopia\Registry\Registry; use Utopia\Swoole\Request as SwooleRequest; - use PDO as PDONative; -use Swoole\Database\RedisConfig; -use Swoole\Database\RedisPool; /** * TODO List @@ -46,7 +43,7 @@ Swoole\Runtime::enableCoroutine(SWOOLE_HOOK_ALL); $server = new Server("0.0.0.0", 80); $server->set([ 'worker_num' => 1 - ]); +]); $subscriptions = []; $connections = []; @@ -74,16 +71,16 @@ $register->set('db', function () { // Register DB connection $register->set('cache', function () { // Register cache connection $redis = new Redis(); $redis->pconnect(App::getEnv('_APP_REDIS_HOST', ''), App::getEnv('_APP_REDIS_PORT', '')); - $user = App::getEnv('_APP_REDIS_USER',''); - $pass = App::getEnv('_APP_REDIS_PASS',''); + $user = App::getEnv('_APP_REDIS_USER', ''); + $pass = App::getEnv('_APP_REDIS_PASS', ''); $auth = []; - if(!empty($user)) { + if (!empty($user)) { $auth["user"] = $user; } - if(!empty($pass)) { + if (!empty($pass)) { $auth["pass"] = $pass; } - if(!empty($auth)) { + if (!empty($auth)) { $redis->auth($auth); } $redis->setOption(Redis::OPT_READ_TIMEOUT, -1); @@ -114,9 +111,7 @@ $server->on("workerStart", function ($server, $workerId) use (&$subscriptions, & Console::error('Pub/sub failed (worker: ' . $workerId . ')'); } - $redis->subscribe(['realtime'], function ($redis, $channel, $payload) use ($server, $workerId, &$connections, &$subscriptions) { - // TODO get project and resource ID and itterate over the resource read(?) permissions and send a message to all listeners - + $redis->subscribe(['realtime'], function ($redis, $channel, $payload) use ($server, &$connections, &$subscriptions) { /** * Supported Resources: * - Collection @@ -130,20 +125,22 @@ $server->on("workerStart", function ($server, $workerId) use (&$subscriptions, & * - Function * - Execution */ - $event = json_decode($payload); + $event = json_decode($payload, true); $receivers = []; foreach ($connections as $fd => $connection) { - if ($connection['projectId'] !== $event->project) { + if ($connection['projectId'] !== $event['project']) { continue; } foreach ($connection['roles'] as $role) { - if (\array_key_exists($role, $subscriptions[$event->project])) { - foreach ($event->channels as $channel) { - if (\array_key_exists($channel, $subscriptions[$event->project][$role]) && \in_array($role, $event->permissions)) { - $receivers = array_merge($receivers, array_keys($subscriptions[$event->project][$role][$channel])); + if (\array_key_exists($role, $subscriptions[$event['project']])) { + foreach ($event['data']['channels'] as $channel) { + if (\array_key_exists($channel, $subscriptions[$event['project']][$role]) && \in_array($role, $event['permissions'])) { + foreach (array_keys($subscriptions[$event['project']][$role][$channel]) as $ids) { + $receivers[] = $ids; + } break; } } @@ -154,13 +151,14 @@ $server->on("workerStart", function ($server, $workerId) use (&$subscriptions, & $receivers = array_keys(array_flip($receivers)); foreach ($receivers as $receiver) { - if ($server->exist($receiver) - && $server->isEstablished($receiver) - ) { - $server->push($receiver, json_encode($event->data), SWOOLE_WEBSOCKET_OPCODE_TEXT, - SWOOLE_WEBSOCKET_FLAG_FIN | SWOOLE_WEBSOCKET_FLAG_COMPRESS); - } - else { + if ($server->exist($receiver) && $server->isEstablished($receiver)) { + $server->push( + $receiver, + json_encode($event['data']), + SWOOLE_WEBSOCKET_OPCODE_TEXT, + SWOOLE_WEBSOCKET_FLAG_FIN | SWOOLE_WEBSOCKET_FLAG_COMPRESS + ); + } else { $server->close($receiver); } } @@ -254,13 +252,26 @@ $server->on('open', function (Server $server, Request $request) use (&$connectio return $user; }, ['project', 'request', 'projectDB']); - $channels = array_flip($request->getQuery('channels', [])); - $user = $app->getResource('user'); - $project = $app->getResource('project'); - $roles = ['*', 'user:' . $user->getId(), 'role:' . (($user->isEmpty()) ? Auth::USER_ROLE_GUEST : Auth::USER_ROLE_MEMBER)]; - /** @var Appwrite\Database\Document $user */ + $user = $app->getResource('user'); + /** @var Appwrite\Database\Document $project */ + $project = $app->getResource('project'); + + $channels = $request->getQuery('channels', []); + + if (empty($project->getId())) { + $server->push($connection, 'Missing or unknown project ID'); + $server->close($connection); + } + + if (empty($request->getQuery('channels', []))) { + $server->push($connection, 'Missing or unknown channels'); + $server->close($connection); + } + + $roles = ['*', 'user:' . $user->getId(), 'role:' . (($user->isEmpty()) ? Auth::USER_ROLE_GUEST : Auth::USER_ROLE_MEMBER)]; + $channels = array_flip($channels); \array_map(function ($node) use (&$roles) { if (isset($node['teamId']) && isset($node['roles'])) { @@ -304,11 +315,9 @@ $server->on('open', function (Server $server, Request $request) use (&$connectio 'projectId' => $project->getId(), 'roles' => $roles, ]; - - $server->push($connection, json_encode($subscriptions)); }); -$server->on('message', function (Server $server, Frame $frame) use (&$connections, &$subscriptions) { +$server->on('message', function (Server $server, Frame $frame) { if ($frame->data === 'reload') { $server->reload(); } @@ -341,7 +350,6 @@ $server->on('close', function (Server $server, int $fd) use (&$connections, &$su unset($connections[$fd]); Console::info('Connection close: ' . $fd); - }); $server->start(); \ No newline at end of file diff --git a/src/Appwrite/Event/Realtime.php b/src/Appwrite/Event/Realtime.php index 93f2f3050d..61bc3c5c84 100644 --- a/src/Appwrite/Event/Realtime.php +++ b/src/Appwrite/Event/Realtime.php @@ -141,10 +141,10 @@ class Realtime $redis->connect(App::getEnv('_APP_REDIS_HOST', ''), App::getEnv('_APP_REDIS_PORT', '')); $redis->publish('realtime', json_encode([ 'project' => $this->project, - 'channels' => $this->channels, 'permissions' => $this->payload->getAttribute('$permissions.read'), 'data' => [ 'event' => $this->event, + 'channels' => $this->channels, 'timestamp' => time(), 'payload' => $this->payload->getArrayCopy() ] From 5a2d7d4aa7496617e9054f3349ee8e29b6b8afc1 Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Thu, 25 Feb 2021 18:00:41 +0100 Subject: [PATCH 028/267] add abuse checks and abstract class for logic --- app/realtime.php | 71 ++++++++++++----------- src/Appwrite/Event/Realtime.php | 11 +++- src/Appwrite/Realtime/Realtime.php | 91 ++++++++++++++++++++++++++++++ 3 files changed, 138 insertions(+), 35 deletions(-) create mode 100644 src/Appwrite/Realtime/Realtime.php diff --git a/app/realtime.php b/app/realtime.php index d5ecfa9e9d..c3eae789f8 100644 --- a/app/realtime.php +++ b/app/realtime.php @@ -10,6 +10,7 @@ use Appwrite\Database\Database; use Appwrite\Database\Document; use Appwrite\Database\Validator\Authorization; use Appwrite\Extend\PDO; +use Appwrite\Realtime\Realtime; use Swoole\WebSocket\Server; use Swoole\Http\Request; use Swoole\Process; @@ -20,6 +21,8 @@ use Utopia\Config\Config; use Utopia\Registry\Registry; use Utopia\Swoole\Request as SwooleRequest; use PDO as PDONative; +use Utopia\Abuse\Abuse; +use Utopia\Abuse\Adapters\TimeLimit; /** * TODO List @@ -127,28 +130,7 @@ $server->on("workerStart", function ($server, $workerId) use (&$subscriptions, & */ $event = json_decode($payload, true); - $receivers = []; - - foreach ($connections as $fd => $connection) { - if ($connection['projectId'] !== $event['project']) { - continue; - } - - foreach ($connection['roles'] as $role) { - if (\array_key_exists($role, $subscriptions[$event['project']])) { - foreach ($event['data']['channels'] as $channel) { - if (\array_key_exists($channel, $subscriptions[$event['project']][$role]) && \in_array($role, $event['permissions'])) { - foreach (array_keys($subscriptions[$event['project']][$role][$channel]) as $ids) { - $receivers[] = $ids; - } - break; - } - } - } - } - } - - $receivers = array_keys(array_flip($receivers)); + $receivers = Realtime::identifyReceivers($event, $connections, $subscriptions); foreach ($receivers as $receiver) { if ($server->exist($receiver) && $server->isEstablished($receiver)) { @@ -260,28 +242,47 @@ $server->on('open', function (Server $server, Request $request) use (&$connectio $channels = $request->getQuery('channels', []); + /* + * Abuse Check + */ + $timeLimit = new TimeLimit('url:{url},ip:{ip}', 60, 60, function () use ($register) { + return $register->get('db'); + }); + $timeLimit->setNamespace('app_' . $project->getId()); + $timeLimit + ->setParam('{ip}', $request->getIP()) + ->setParam('{url}', $request->getURI()); + + $abuse = new Abuse($timeLimit); + + if ($abuse->check() && App::getEnv('_APP_OPTIONS_ABUSE', 'enabled') === 'enabled') { + $server->push($connection, 'Too many requests'); + $server->close($connection); + } + + /* + * Project Check + */ if (empty($project->getId())) { $server->push($connection, 'Missing or unknown project ID'); $server->close($connection); } - if (empty($request->getQuery('channels', []))) { - $server->push($connection, 'Missing or unknown channels'); - $server->close($connection); - } + Realtime::setUser($user); $roles = ['*', 'user:' . $user->getId(), 'role:' . (($user->isEmpty()) ? Auth::USER_ROLE_GUEST : Auth::USER_ROLE_MEMBER)]; $channels = array_flip($channels); - \array_map(function ($node) use (&$roles) { - if (isset($node['teamId']) && isset($node['roles'])) { - $roles[] = 'team:' . $node['teamId']; + Realtime::parseChannels($channels); + Realtime::parseRoles($roles); - foreach ($node['roles'] as $nodeRole) { // Set all team roles - $roles[] = 'team:' . $node['teamId'] . '/' . $nodeRole; - } - } - }, $user->getAttribute('memberships', [])); + /** + * Channels Check + */ + if (empty($request->getQuery('channels', []))) { + $server->push($connection, 'Missing channels'); + $server->close($connection); + } /** * Build Subscriptions Tree @@ -315,6 +316,8 @@ $server->on('open', function (Server $server, Request $request) use (&$connectio 'projectId' => $project->getId(), 'roles' => $roles, ]; + + $server->push($connection, json_encode($channels)); }); $server->on('message', function (Server $server, Frame $frame) { diff --git a/src/Appwrite/Event/Realtime.php b/src/Appwrite/Event/Realtime.php index 61bc3c5c84..19f076a266 100644 --- a/src/Appwrite/Event/Realtime.php +++ b/src/Appwrite/Event/Realtime.php @@ -22,6 +22,11 @@ class Realtime */ protected $channels = []; + /** + * @var array + */ + protected $permissions = []; + /** * @var Document */ @@ -106,22 +111,26 @@ class Realtime switch (true) { case strpos($this->event, 'account.') === 0: $this->channels[] = 'account.' . $this->payload->getId(); + $this->permissions = ['user:' . $this->payload->getId()]; break; case strpos($this->event, 'database.collections.') === 0: $this->channels[] = 'collections'; $this->channels[] = 'collections.' . $this->payload->getId(); + $this->permissions = $this->payload->getAttribute('$permissions.read'); break; case strpos($this->event, 'database.documents.') === 0: $this->channels[] = 'documents'; $this->channels[] = 'collections.' . $this->payload->getAttribute('$collection') . '.documents'; $this->channels[] = 'documents.' . $this->payload->getId(); + $this->permissions = $this->payload->getAttribute('$permissions.read'); break; case strpos($this->event, 'storage.') === 0: $this->channels[] = 'files'; $this->channels[] = 'files.' . $this->payload->getId(); + $this->permissions = $this->payload->getAttribute('$permissions.read'); break; } @@ -141,7 +150,7 @@ class Realtime $redis->connect(App::getEnv('_APP_REDIS_HOST', ''), App::getEnv('_APP_REDIS_PORT', '')); $redis->publish('realtime', json_encode([ 'project' => $this->project, - 'permissions' => $this->payload->getAttribute('$permissions.read'), + 'permissions' => $this->permissions, 'data' => [ 'event' => $this->event, 'channels' => $this->channels, diff --git a/src/Appwrite/Realtime/Realtime.php b/src/Appwrite/Realtime/Realtime.php new file mode 100644 index 0000000000..bf812f00e2 --- /dev/null +++ b/src/Appwrite/Realtime/Realtime.php @@ -0,0 +1,91 @@ + $value) { + if (strpos($key, 'account.') === 0) { + unset($channels[$key]); + } elseif ($key === 'account') { + if (!empty(self::$user->getId())) { + $channels['account.' . self::$user->getId()] = $value; + } + unset($channels['account']); + } + } + + if (\array_key_exists('account', $channels)) { + if (self::$user->getId()) { + $channels['account.' . self::$user->getId()] = $channels['account']; + } + unset($channels['account']); + } + } + + /** + * @param array $roles + */ + static function parseRoles(array &$roles) + { + \array_map(function ($node) use (&$roles) { + 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; + } + } + }, self::$user->getAttribute('memberships', [])); + } + + /** + * @param array $event + * @param array $connections + * @param array $subscriptions + */ + static function identifyReceivers(array &$event, array &$connections, array &$subscriptions) + { + $receivers = []; + foreach ($connections as $fd => $connection) { + if ($connection['projectId'] !== $event['project']) { + continue; + } + + foreach ($connection['roles'] as $role) { + if (\array_key_exists($role, $subscriptions[$event['project']])) { + foreach ($event['data']['channels'] as $channel) { + if (\array_key_exists($channel, $subscriptions[$event['project']][$role]) && \in_array($role, $event['permissions'])) { + foreach (array_keys($subscriptions[$event['project']][$role][$channel]) as $ids) { + $receivers[] = $ids; + } + break; + } + } + } + } + } + + return array_keys(array_flip($receivers)); + } +} From 28eed022f6475941459e8a4d3a41693cf52e74b8 Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Fri, 26 Feb 2021 10:21:07 +0100 Subject: [PATCH 029/267] add redis connection pool --- app/realtime.php | 49 +++++++++++++++---------- src/Appwrite/Database/Adapter/Redis.php | 12 +++++- src/Appwrite/Realtime/Realtime.php | 2 + 3 files changed, 41 insertions(+), 22 deletions(-) diff --git a/app/realtime.php b/app/realtime.php index c3eae789f8..2f2cbc82b3 100644 --- a/app/realtime.php +++ b/app/realtime.php @@ -21,16 +21,16 @@ use Utopia\Config\Config; use Utopia\Registry\Registry; use Utopia\Swoole\Request as SwooleRequest; use PDO as PDONative; +use Swoole\Database\RedisConfig; +use Swoole\Database\RedisPool; use Utopia\Abuse\Abuse; use Utopia\Abuse\Adapters\TimeLimit; /** * TODO List * - * - Abuse Control / x mesages per connection * - CORS Validation * - Limit payload size - * - Message structure: { status: "ok"|"error", event: EVENT_NAME, data: } * - JWT Authentication (in path / or in message) * * Protocols Support: @@ -43,10 +43,13 @@ use Utopia\Abuse\Adapters\TimeLimit; ini_set('default_socket_timeout', -1); Swoole\Runtime::enableCoroutine(SWOOLE_HOOK_ALL); -$server = new Server("0.0.0.0", 80); +$server = new Server('0.0.0.0', 80); + $server->set([ - 'worker_num' => 1 + 'websocket_compression' => true, + 'package_max_length' => 81920 ]); + $subscriptions = []; $connections = []; @@ -71,27 +74,33 @@ $register->set('db', function () { // Register DB connection return $pdo; }); -$register->set('cache', function () { // Register cache connection - $redis = new Redis(); - $redis->pconnect(App::getEnv('_APP_REDIS_HOST', ''), App::getEnv('_APP_REDIS_PORT', '')); +$register->set('cache', function () use (&$pool) { // Register cache connection + $config = new RedisConfig(); + $user = App::getEnv('_APP_REDIS_USER', ''); $pass = App::getEnv('_APP_REDIS_PASS', ''); - $auth = []; + $auth = ''; + if (!empty($user)) { - $auth["user"] = $user; + $auth = $user; } if (!empty($pass)) { - $auth["pass"] = $pass; + $auth += ':' . $pass; } if (!empty($auth)) { - $redis->auth($auth); + $config->withAuth($auth); } - $redis->setOption(Redis::OPT_READ_TIMEOUT, -1); + + $config + ->withHost(App::getEnv('_APP_REDIS_HOST', '')) + ->withPort(App::getEnv('_APP_REDIS_PORT', '')); + + $pool = new RedisPool($config); - return $redis; + return $pool; }); -$server->on("workerStart", function ($server, $workerId) use (&$subscriptions, &$connections, &$register) { +$server->on('workerStart', function ($server, $workerId) use (&$subscriptions, &$connections, &$register) { Console::success('Worker ' . ++$workerId . ' started succefully'); $attempts = 0; @@ -102,10 +111,10 @@ $server->on("workerStart", function ($server, $workerId) use (&$subscriptions, & if ($attempts > 0) { Console::error('Pub/sub connection lost (lasted ' . (time() - $start) . ' seconds, worker: ' . $workerId . '). Attempting restart in 5 seconds (attempt #' . $attempts . ')'); - sleep(5); // 1 sec delay between connection attempts + sleep(5); // 5 sec delay between connection attempts } - $redis = $register->get('cache'); + $redis = $register->get('cache')->get(); if ($redis->ping(true)) { $attempts = 0; @@ -157,7 +166,7 @@ $server->on("workerStart", function ($server, $workerId) use (&$subscriptions, & Console::error('Failed to restart pub/sub...'); }); -$server->on("start", function (Server $server) { +$server->on('start', function (Server $server) { Console::success('Server started succefully'); Console::info("Master pid {$server->master_pid}, manager pid {$server->manager_pid}"); @@ -182,7 +191,7 @@ $server->on('open', function (Server $server, Request $request) use (&$connectio App::setResource('consoleDB', function () use (&$register) { $consoleDB = new Database(); - $consoleDB->setAdapter(new MySQLAdapter($register)); // TODO: Add Redis + $consoleDB->setAdapter(new RedisAdapter(new MySQLAdapter($register), $register, true)); // TODO: Add Redis $consoleDB->setNamespace('app_console'); // Should be replaced with param if we want to have parent projects $consoleDB->setMocks(Config::getParam('collections', [])); @@ -248,8 +257,8 @@ $server->on('open', function (Server $server, Request $request) use (&$connectio $timeLimit = new TimeLimit('url:{url},ip:{ip}', 60, 60, function () use ($register) { return $register->get('db'); }); - $timeLimit->setNamespace('app_' . $project->getId()); $timeLimit + ->setNamespace('app_' . $project->getId()) ->setParam('{ip}', $request->getIP()) ->setParam('{url}', $request->getURI()); @@ -355,4 +364,4 @@ $server->on('close', function (Server $server, int $fd) use (&$connections, &$su Console::info('Connection close: ' . $fd); }); -$server->start(); \ No newline at end of file +$server->start(); diff --git a/src/Appwrite/Database/Adapter/Redis.php b/src/Appwrite/Database/Adapter/Redis.php index a1e440112d..47ad309569 100644 --- a/src/Appwrite/Database/Adapter/Redis.php +++ b/src/Appwrite/Database/Adapter/Redis.php @@ -19,16 +19,22 @@ class Redis extends Adapter */ protected $adapter; + /** + * @var bool + */ + protected $isPool; + /** * Redis constructor. * * @param Adapter $adapter * @param Registry $register */ - public function __construct(Adapter $adapter, Registry $register) + public function __construct(Adapter $adapter, Registry $register, $isPool = false) { $this->register = $register; $this->adapter = $adapter; + $this->isPool = $isPool; } /** @@ -261,7 +267,9 @@ class Redis extends Adapter */ protected function getRedis(): Client { - return $this->register->get('cache'); + return $this->isPool ? + $this->register->get('cache')->get() : + $this->register->get('cache'); } /** diff --git a/src/Appwrite/Realtime/Realtime.php b/src/Appwrite/Realtime/Realtime.php index bf812f00e2..5166f4bff1 100644 --- a/src/Appwrite/Realtime/Realtime.php +++ b/src/Appwrite/Realtime/Realtime.php @@ -60,6 +60,8 @@ class Realtime } /** + * Identifies the receivers of all subscriptions, based on the permissions and event. + * * @param array $event * @param array $connections * @param array $subscriptions From 603f4ab99d209a5f29ee8caf0d62d1abc1293076 Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Fri, 26 Feb 2021 11:17:17 +0100 Subject: [PATCH 030/267] revert redis connection pool --- app/realtime.php | 30 +++++++++---------------- src/Appwrite/Database/Adapter/Redis.php | 12 ++-------- 2 files changed, 13 insertions(+), 29 deletions(-) diff --git a/app/realtime.php b/app/realtime.php index 2f2cbc82b3..2c7d9f1ae3 100644 --- a/app/realtime.php +++ b/app/realtime.php @@ -21,8 +21,6 @@ use Utopia\Config\Config; use Utopia\Registry\Registry; use Utopia\Swoole\Request as SwooleRequest; use PDO as PDONative; -use Swoole\Database\RedisConfig; -use Swoole\Database\RedisPool; use Utopia\Abuse\Abuse; use Utopia\Abuse\Adapters\TimeLimit; @@ -74,30 +72,24 @@ $register->set('db', function () { // Register DB connection return $pdo; }); -$register->set('cache', function () use (&$pool) { // Register cache connection - $config = new RedisConfig(); - +$register->set('cache', function () { // Register cache connection + $redis = new Redis(); + $redis->pconnect(App::getEnv('_APP_REDIS_HOST', ''), App::getEnv('_APP_REDIS_PORT', '')); $user = App::getEnv('_APP_REDIS_USER', ''); $pass = App::getEnv('_APP_REDIS_PASS', ''); - $auth = ''; - + $auth = []; if (!empty($user)) { - $auth = $user; + $auth["user"] = $user; } if (!empty($pass)) { - $auth += ':' . $pass; + $auth["pass"] = $pass; } if (!empty($auth)) { - $config->withAuth($auth); + $redis->auth($auth); } - - $config - ->withHost(App::getEnv('_APP_REDIS_HOST', '')) - ->withPort(App::getEnv('_APP_REDIS_PORT', '')); - - $pool = new RedisPool($config); + $redis->setOption(Redis::OPT_READ_TIMEOUT, -1); - return $pool; + return $redis; }); $server->on('workerStart', function ($server, $workerId) use (&$subscriptions, &$connections, &$register) { @@ -114,7 +106,7 @@ $server->on('workerStart', function ($server, $workerId) use (&$subscriptions, & sleep(5); // 5 sec delay between connection attempts } - $redis = $register->get('cache')->get(); + $redis = $register->get('cache'); if ($redis->ping(true)) { $attempts = 0; @@ -191,7 +183,7 @@ $server->on('open', function (Server $server, Request $request) use (&$connectio App::setResource('consoleDB', function () use (&$register) { $consoleDB = new Database(); - $consoleDB->setAdapter(new RedisAdapter(new MySQLAdapter($register), $register, true)); // TODO: Add Redis + $consoleDB->setAdapter(new MySQLAdapter($register)); // TODO: Add Redis $consoleDB->setNamespace('app_console'); // Should be replaced with param if we want to have parent projects $consoleDB->setMocks(Config::getParam('collections', [])); diff --git a/src/Appwrite/Database/Adapter/Redis.php b/src/Appwrite/Database/Adapter/Redis.php index 47ad309569..a1e440112d 100644 --- a/src/Appwrite/Database/Adapter/Redis.php +++ b/src/Appwrite/Database/Adapter/Redis.php @@ -19,22 +19,16 @@ class Redis extends Adapter */ protected $adapter; - /** - * @var bool - */ - protected $isPool; - /** * Redis constructor. * * @param Adapter $adapter * @param Registry $register */ - public function __construct(Adapter $adapter, Registry $register, $isPool = false) + public function __construct(Adapter $adapter, Registry $register) { $this->register = $register; $this->adapter = $adapter; - $this->isPool = $isPool; } /** @@ -267,9 +261,7 @@ class Redis extends Adapter */ protected function getRedis(): Client { - return $this->isPool ? - $this->register->get('cache')->get() : - $this->register->get('cache'); + return $this->register->get('cache'); } /** From 111905514d8c5370dda5f30f743aa0137f34ad92 Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Fri, 26 Feb 2021 17:01:01 +0100 Subject: [PATCH 031/267] improve realtime class --- app/realtime.php | 49 +++-------------- src/Appwrite/Realtime/Realtime.php | 87 ++++++++++++++++++++++++------ 2 files changed, 79 insertions(+), 57 deletions(-) diff --git a/app/realtime.php b/app/realtime.php index 2c7d9f1ae3..2fa34fec71 100644 --- a/app/realtime.php +++ b/app/realtime.php @@ -241,8 +241,6 @@ $server->on('open', function (Server $server, Request $request) use (&$connectio /** @var Appwrite\Database\Document $project */ $project = $app->getResource('project'); - $channels = $request->getQuery('channels', []); - /* * Abuse Check */ @@ -271,52 +269,18 @@ $server->on('open', function (Server $server, Request $request) use (&$connectio Realtime::setUser($user); - $roles = ['*', 'user:' . $user->getId(), 'role:' . (($user->isEmpty()) ? Auth::USER_ROLE_GUEST : Auth::USER_ROLE_MEMBER)]; - $channels = array_flip($channels); - - Realtime::parseChannels($channels); - Realtime::parseRoles($roles); + $roles = Realtime::getRoles(); + $channels = Realtime::parseChannels($request->getQuery('channels', [])); /** * Channels Check */ - if (empty($request->getQuery('channels', []))) { + if (empty($channels)) { $server->push($connection, 'Missing channels'); $server->close($connection); } - - /** - * Build Subscriptions 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] - */ - - if (!isset($subscriptions[$project->getId()])) { // Init Project - $subscriptions[$project->getId()] = []; - } - - foreach ($roles as $key => $role) { - if (!isset($subscriptions[$project->getId()][$role])) { // Add user first connection - $subscriptions[$project->getId()][$role] = []; - } - - foreach ($channels as $channel => $list) { - $subscriptions[$project->getId()][$role][$channel][$connection] = true; - } - } - - $connections[$connection] = [ - 'projectId' => $project->getId(), - 'roles' => $roles, - ]; + + Realtime::addSubscription($project->getId(), $connection, $subscriptions, $connections, $roles, $channels); $server->push($connection, json_encode($channels)); }); @@ -330,6 +294,9 @@ $server->on('message', function (Server $server, Frame $frame) { }); $server->on('close', function (Server $server, int $fd) use (&$connections, &$subscriptions) { + /** + * TODO: Move into Realtime Class for tests + */ $projectId = $connections[$fd]['projectId'] ?? ''; $roles = $connections[$fd]['roles'] ?? []; diff --git a/src/Appwrite/Realtime/Realtime.php b/src/Appwrite/Realtime/Realtime.php index 5166f4bff1..b6028baf4d 100644 --- a/src/Appwrite/Realtime/Realtime.php +++ b/src/Appwrite/Realtime/Realtime.php @@ -2,6 +2,7 @@ namespace Appwrite\Realtime; +use Appwrite\Auth\Auth; use Appwrite\Database\Document; class Realtime @@ -19,11 +20,34 @@ class Realtime self::$user = $user; } + /** + * @return array + */ + static function getRoles() + { + $roles = ['*', 'role:' . ((self::$user->isEmpty()) ? Auth::USER_ROLE_GUEST : Auth::USER_ROLE_MEMBER)]; + if (!(self::$user->isEmpty())) { + $roles[] = 'user:' . self::$user->getId(); + } + foreach (self::$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; + } + /** * @param array $channels */ - static function parseChannels(array &$channels) + static function parseChannels(array $channels) { + $channels = array_flip($channels); + foreach ($channels as $key => $value) { if (strpos($key, 'account.') === 0) { unset($channels[$key]); @@ -41,22 +65,8 @@ class Realtime } unset($channels['account']); } - } - /** - * @param array $roles - */ - static function parseRoles(array &$roles) - { - \array_map(function ($node) use (&$roles) { - 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; - } - } - }, self::$user->getAttribute('memberships', [])); + return $channels; } /** @@ -90,4 +100,49 @@ class Realtime return array_keys(array_flip($receivers)); } + + /** + * Adds Subscription. + * + * @param string $projectId + * @param mixed $connection + * @param array $subscriptions + * @param array $roles + * @param array $channels + */ + static function addSubscription($projectId, $connection, &$subscriptions, &$connections, &$roles, &$channels) + { + /** + * Build Subscriptions 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] + */ + + if (!isset($subscriptions[$projectId])) { // Init Project + $subscriptions[$projectId] = []; + } + + foreach ($roles as $key => $role) { + if (!isset($subscriptions[$projectId][$role])) { // Add user first connection + $subscriptions[$projectId][$role] = []; + } + + foreach ($channels as $channel => $list) { + $subscriptions[$projectId][$role][$channel][$connection] = true; + } + } + + $connections[$connection] = [ + 'projectId' => $projectId, + 'roles' => $roles, + ]; + } } From eb1c0a13d55f8bc01687c98d890dc042faba22eb Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Fri, 26 Feb 2021 17:01:37 +0100 Subject: [PATCH 032/267] add unit tests --- tests/unit/Realtime/RealtimeChannelsTest.php | 310 +++++++++++++++++++ tests/unit/Realtime/RealtimeGuestTest.php | 205 ++++++++++++ tests/unit/Realtime/RealtimeTest.php | 223 +++++++++++++ 3 files changed, 738 insertions(+) create mode 100644 tests/unit/Realtime/RealtimeChannelsTest.php create mode 100644 tests/unit/Realtime/RealtimeGuestTest.php create mode 100644 tests/unit/Realtime/RealtimeTest.php diff --git a/tests/unit/Realtime/RealtimeChannelsTest.php b/tests/unit/Realtime/RealtimeChannelsTest.php new file mode 100644 index 0000000000..c28b308a5a --- /dev/null +++ b/tests/unit/Realtime/RealtimeChannelsTest.php @@ -0,0 +1,310 @@ +connectionsAuthenticated = count($this->allChannels) * $this->connectionsPerChannel; + $this->connectionsGuest = count($this->allChannels) * $this->connectionsPerChannel; + $this->connectionsTotal = $this->connectionsAuthenticated + $this->connectionsGuest; + + /** + * Add 100 Authenticated Clients + */ + for ($i = 0; $i < $this->connectionsPerChannel; $i++) { + foreach ($this->allChannels as $index => $channel) { + Realtime::setUser(new Document([ + '$id' => 'user' . $this->connectionsCount, + 'memberships' => [ + [ + 'teamId' => 'team' . $i, + 'roles' => [ + empty($index % 2) ? 'admin' : 'member' + ] + ] + ] + ])); + + Realtime::addSubscription( + '1', + $this->connectionsCount, + $this->subscriptions, + $this->connections, + Realtime::getRoles(), + Realtime::parseChannels([0 => $channel]) + ); + + $this->connectionsCount++; + } + } + + /** + * Add 100 Guest Clients + */ + for ($i = 0; $i < $this->connectionsPerChannel; $i++) { + foreach ($this->allChannels as $index => $channel) { + Realtime::setUser(new Document([ + '$id' => '' + ])); + + Realtime::addSubscription( + '1', + $this->connectionsCount, + $this->subscriptions, + $this->connections, + Realtime::getRoles(), + Realtime::parseChannels([0 => $channel]) + ); + + $this->connectionsCount++; + } + } + } + + public function tearDown(): void + { + $this->connections = []; + $this->subscriptions = []; + $this->connectionsCount = 0; + } + + public function testSubscriptions() + { + /** + * Check for 1 project. + */ + $this->assertCount(1, $this->subscriptions); + + /** + * Check for 133 subscriptions: + * - XXX users + * - 1 * + * - 1 role:guest + * - 1 role:member + * - 10 teams + * - 20 team roles (2 roles per team) + */ + $this->assertCount(($this->connectionsAuthenticated + (3 * $this->connectionsPerChannel) + 3), $this->subscriptions['1']); + + /** + * Check for 200 connections + * - 100 Authenticated + * - 100 Guests + */ + $this->assertCount($this->connectionsTotal, $this->connections); + } + + /** + * Tests Wildcard (*) Permissions on every channel. + */ + public function testWildcardPermission() + { + foreach ($this->allChannels as $index => $channel) { + $event = [ + 'project' => '1', + 'permissions' => ['*'], + 'data' => [ + 'channels' => [ + 0 => $channel, + ] + ] + ]; + + $receivers = Realtime::identifyReceivers( + $event, + $this->connections, + $this->subscriptions + ); + + /** + * 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', + 'permissions' => $permissions, + 'data' => [ + 'channels' => [ + 0 => $channel, + ] + ] + ]; + + $receivers = Realtime::identifyReceivers( + $event, + $this->connections, + $this->subscriptions + ); + + /** + * 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', + 'permissions' => $permissions, + 'data' => [ + 'channels' => [ + 0 => $channel, + ] + ] + ]; + + $receivers = Realtime::identifyReceivers( + $event, + $this->connections, + $this->subscriptions + ); + + /** + * 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', + 'permissions' => $permissions, + 'data' => [ + 'channels' => [ + 0 => $channel, + ] + ] + ]; + + $receivers = Realtime::identifyReceivers( + $event, + $this->connections, + $this->subscriptions + ); + + /** + * 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', + 'permissions' => $permissions, + 'data' => [ + 'channels' => [ + 0 => $channel, + ] + ] + ]; + + $receivers = Realtime::identifyReceivers( + $event, + $this->connections, + $this->subscriptions + ); + + /** + * 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); + } + } + } +} diff --git a/tests/unit/Realtime/RealtimeGuestTest.php b/tests/unit/Realtime/RealtimeGuestTest.php new file mode 100644 index 0000000000..35507a0d50 --- /dev/null +++ b/tests/unit/Realtime/RealtimeGuestTest.php @@ -0,0 +1,205 @@ + '' + ])); + + $roles = Realtime::getRoles(); + $this->assertCount(2, $roles); + $this->assertContains('*', $roles); + $this->assertContains('role:guest', $roles); + + $channels = [ + 0 => 'files', + 1 => 'documents', + 2 => 'documents.789', + 3 => 'account', + 4 => 'account.456' + ]; + + $channels = Realtime::parseChannels($channels); + $this->assertCount(3, $channels); + $this->assertArrayHasKey('files', $channels); + $this->assertArrayHasKey('documents', $channels); + $this->assertArrayHasKey('documents.789', $channels); + $this->assertArrayNotHasKey('account', $channels); + $this->assertArrayNotHasKey('account.456', $channels); + + Realtime::addSubscription('1', 1, $this->subscriptions, $this->connections, $roles, $channels); + + + $event = [ + 'project' => '1', + 'permissions' => ['*'], + 'data' => [ + 'channels' => [ + 0 => 'documents', + 1 => 'documents', + ] + ] + ]; + + $receivers = Realtime::identifyReceivers( + $event, + $this->connections, + $this->subscriptions + ); + + $this->assertCount(1, $receivers); + $this->assertEquals(1, $receivers[0]); + + $event['permissions'] = ['role:guest']; + + $receivers = Realtime::identifyReceivers( + $event, + $this->connections, + $this->subscriptions + ); + + $this->assertCount(1, $receivers); + $this->assertEquals(1, $receivers[0]); + + $event['permissions'] = ['role:member']; + + $receivers = Realtime::identifyReceivers( + $event, + $this->connections, + $this->subscriptions + ); + + $this->assertEmpty($receivers); + + $event['permissions'] = ['user:123']; + + $receivers = Realtime::identifyReceivers( + $event, + $this->connections, + $this->subscriptions + ); + + $this->assertEmpty($receivers); + + $event['permissions'] = ['team:abc']; + + $receivers = Realtime::identifyReceivers( + $event, + $this->connections, + $this->subscriptions + ); + + $this->assertEmpty($receivers); + + $event['permissions'] = ['team:abc/administrator']; + + $receivers = Realtime::identifyReceivers( + $event, + $this->connections, + $this->subscriptions + ); + + $this->assertEmpty($receivers); + + $event['permissions'] = ['team:abc/god']; + + $receivers = Realtime::identifyReceivers( + $event, + $this->connections, + $this->subscriptions + ); + + $this->assertEmpty($receivers); + + $event['permissions'] = ['team:def']; + + $receivers = Realtime::identifyReceivers( + $event, + $this->connections, + $this->subscriptions + ); + + $this->assertEmpty($receivers); + + $event['permissions'] = ['team:def/guest']; + + $receivers = Realtime::identifyReceivers( + $event, + $this->connections, + $this->subscriptions + ); + + $this->assertEmpty($receivers); + + $event['permissions'] = ['user:456']; + + $receivers = Realtime::identifyReceivers( + $event, + $this->connections, + $this->subscriptions + ); + + $this->assertEmpty($receivers); + + $event['permissions'] = ['team:def/member']; + + $receivers = Realtime::identifyReceivers( + $event, + $this->connections, + $this->subscriptions + ); + + $this->assertEmpty($receivers); + + $event['permissions'] = ['*']; + $event['data']['channels'] = ['documents.123']; + + $receivers = Realtime::identifyReceivers( + $event, + $this->connections, + $this->subscriptions + ); + + $this->assertEmpty($receivers); + + $event['data']['channels'] = ['documents.789']; + + $receivers = Realtime::identifyReceivers( + $event, + $this->connections, + $this->subscriptions + ); + + $this->assertCount(1, $receivers); + $this->assertEquals(1, $receivers[0]); + + $event['project'] = '2'; + + $receivers = Realtime::identifyReceivers( + $event, + $this->connections, + $this->subscriptions + ); + + $this->assertEmpty($receivers); + } +} diff --git a/tests/unit/Realtime/RealtimeTest.php b/tests/unit/Realtime/RealtimeTest.php new file mode 100644 index 0000000000..ea97d2c5c9 --- /dev/null +++ b/tests/unit/Realtime/RealtimeTest.php @@ -0,0 +1,223 @@ + '123', + 'memberships' => [ + [ + 'teamId' => 'abc', + 'roles' => [ + 'administrator', + 'god' + ] + ], + [ + 'teamId' => 'def', + 'roles' => [ + 'guest' + ] + ] + ] + ])); + + $roles = Realtime::getRoles(); + + $this->assertCount(8, $roles); + $this->assertContains('*', $roles); + $this->assertContains('user:123', $roles); + $this->assertContains('role:member', $roles); + $this->assertContains('team:abc', $roles); + $this->assertContains('team:abc/administrator', $roles); + $this->assertContains('team:abc/god', $roles); + $this->assertContains('team:def', $roles); + $this->assertContains('team:def/guest', $roles); + + $channels = [ + 0 => 'files', + 1 => 'documents', + 2 => 'documents.789', + 3 => 'account', + 4 => 'account.456' + ]; + + $channels = Realtime::parseChannels($channels); + + $this->assertCount(4, $channels); + $this->assertArrayHasKey('files', $channels); + $this->assertArrayHasKey('documents', $channels); + $this->assertArrayHasKey('documents.789', $channels); + $this->assertArrayHasKey('account.123', $channels); + $this->assertArrayNotHasKey('account', $channels); + $this->assertArrayNotHasKey('account.456', $channels); + + Realtime::addSubscription('1', 1, $this->subscriptions, $this->connections, $roles, $channels); + + $event = [ + 'project' => '1', + 'permissions' => ['*'], + 'data' => [ + 'channels' => [ + 0 => 'account.123', + ] + ] + ]; + + $receivers = Realtime::identifyReceivers( + $event, + $this->connections, + $this->subscriptions + ); + + $this->assertCount(1, $receivers); + $this->assertEquals(1, $receivers[0]); + + $event['permissions'] = ['role:member']; + + $receivers = Realtime::identifyReceivers( + $event, + $this->connections, + $this->subscriptions + ); + + $this->assertCount(1, $receivers); + $this->assertEquals(1, $receivers[0]); + + $event['permissions'] = ['user:123']; + + $receivers = Realtime::identifyReceivers( + $event, + $this->connections, + $this->subscriptions + ); + + $this->assertCount(1, $receivers); + $this->assertEquals(1, $receivers[0]); + + $event['permissions'] = ['team:abc']; + + $receivers = Realtime::identifyReceivers( + $event, + $this->connections, + $this->subscriptions + ); + + $this->assertCount(1, $receivers); + $this->assertEquals(1, $receivers[0]); + + $event['permissions'] = ['team:abc/administrator']; + + $receivers = Realtime::identifyReceivers( + $event, + $this->connections, + $this->subscriptions + ); + + $this->assertCount(1, $receivers); + $this->assertEquals(1, $receivers[0]); + + $event['permissions'] = ['team:abc/god']; + + $receivers = Realtime::identifyReceivers( + $event, + $this->connections, + $this->subscriptions + ); + + $this->assertCount(1, $receivers); + $this->assertEquals(1, $receivers[0]); + + $event['permissions'] = ['team:def']; + + $receivers = Realtime::identifyReceivers( + $event, + $this->connections, + $this->subscriptions + ); + + $this->assertCount(1, $receivers); + $this->assertEquals(1, $receivers[0]); + + $event['permissions'] = ['team:def/guest']; + + $receivers = Realtime::identifyReceivers( + $event, + $this->connections, + $this->subscriptions + ); + + $this->assertCount(1, $receivers); + $this->assertEquals(1, $receivers[0]); + + $event['permissions'] = ['user:456']; + + $receivers = Realtime::identifyReceivers( + $event, + $this->connections, + $this->subscriptions + ); + + $this->assertEmpty($receivers); + + $event['permissions'] = ['team:def/member']; + + $receivers = Realtime::identifyReceivers( + $event, + $this->connections, + $this->subscriptions + ); + + $this->assertEmpty($receivers); + + $event['permissions'] = ['*']; + $event['data']['channels'] = ['documents.123']; + + $receivers = Realtime::identifyReceivers( + $event, + $this->connections, + $this->subscriptions + ); + + $this->assertEmpty($receivers); + + $event['data']['channels'] = ['documents.789']; + + $receivers = Realtime::identifyReceivers( + $event, + $this->connections, + $this->subscriptions + ); + + $this->assertCount(1, $receivers); + $this->assertEquals(1, $receivers[0]); + + $event['project'] = '2'; + + $receivers = Realtime::identifyReceivers( + $event, + $this->connections, + $this->subscriptions + ); + + $this->assertEmpty($receivers); + } +} From 7fb5d61b5fbf16abe64417117174453cbdbf5b7c Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Fri, 26 Feb 2021 17:07:53 +0100 Subject: [PATCH 033/267] fix comments --- tests/unit/Realtime/RealtimeChannelsTest.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/unit/Realtime/RealtimeChannelsTest.php b/tests/unit/Realtime/RealtimeChannelsTest.php index c28b308a5a..4e79078ca6 100644 --- a/tests/unit/Realtime/RealtimeChannelsTest.php +++ b/tests/unit/Realtime/RealtimeChannelsTest.php @@ -44,7 +44,7 @@ class RealtimeChannelsTest extends TestCase $this->connectionsTotal = $this->connectionsAuthenticated + $this->connectionsGuest; /** - * Add 100 Authenticated Clients + * Add Authenticated Clients */ for ($i = 0; $i < $this->connectionsPerChannel; $i++) { foreach ($this->allChannels as $index => $channel) { @@ -74,7 +74,7 @@ class RealtimeChannelsTest extends TestCase } /** - * Add 100 Guest Clients + * Add Guest Clients */ for ($i = 0; $i < $this->connectionsPerChannel; $i++) { foreach ($this->allChannels as $index => $channel) { @@ -111,7 +111,7 @@ class RealtimeChannelsTest extends TestCase $this->assertCount(1, $this->subscriptions); /** - * Check for 133 subscriptions: + * Check for correct amount of subscriptions: * - XXX users * - 1 * * - 1 role:guest @@ -122,9 +122,9 @@ class RealtimeChannelsTest extends TestCase $this->assertCount(($this->connectionsAuthenticated + (3 * $this->connectionsPerChannel) + 3), $this->subscriptions['1']); /** - * Check for 200 connections - * - 100 Authenticated - * - 100 Guests + * Check for connections + * - Authenticated + * - Guests */ $this->assertCount($this->connectionsTotal, $this->connections); } From ffb275d46d0b3d9a4bc31d52aa454c991b7b4cb8 Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Fri, 26 Feb 2021 17:09:47 +0100 Subject: [PATCH 034/267] fix comments --- tests/unit/Realtime/RealtimeChannelsTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/unit/Realtime/RealtimeChannelsTest.php b/tests/unit/Realtime/RealtimeChannelsTest.php index 4e79078ca6..cd993c6719 100644 --- a/tests/unit/Realtime/RealtimeChannelsTest.php +++ b/tests/unit/Realtime/RealtimeChannelsTest.php @@ -113,11 +113,11 @@ class RealtimeChannelsTest extends TestCase /** * Check for correct amount of subscriptions: * - XXX users + * - XXX teams + * - XXX team roles (2 roles per team) * - 1 * * - 1 role:guest * - 1 role:member - * - 10 teams - * - 20 team roles (2 roles per team) */ $this->assertCount(($this->connectionsAuthenticated + (3 * $this->connectionsPerChannel) + 3), $this->subscriptions['1']); From 8ef300c020a7e753dfd40e88f24442ece49cd15b Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Fri, 26 Feb 2021 18:26:22 +0100 Subject: [PATCH 035/267] add remove subscriptions to class --- app/realtime.php | 27 +-------------- src/Appwrite/Realtime/Realtime.php | 35 +++++++++++++++++++- tests/unit/Realtime/RealtimeChannelsTest.php | 18 ++++++++-- tests/unit/Realtime/RealtimeGuestTest.php | 13 +++++++- tests/unit/Realtime/RealtimeTest.php | 13 +++++++- 5 files changed, 75 insertions(+), 31 deletions(-) diff --git a/app/realtime.php b/app/realtime.php index 2fa34fec71..e5308a6a62 100644 --- a/app/realtime.php +++ b/app/realtime.php @@ -294,32 +294,7 @@ $server->on('message', function (Server $server, Frame $frame) { }); $server->on('close', function (Server $server, int $fd) use (&$connections, &$subscriptions) { - /** - * TODO: Move into Realtime Class for tests - */ - $projectId = $connections[$fd]['projectId'] ?? ''; - $roles = $connections[$fd]['roles'] ?? []; - - foreach ($roles as $key => $role) { - foreach ($subscriptions[$projectId][$role] as $channel => $list) { - unset($subscriptions[$projectId][$role][$channel][$fd]); // Remove connection - - if (empty($subscriptions[$projectId][$role][$channel])) { - unset($subscriptions[$projectId][$role][$channel]); // Remove channel when no connections - } - } - - if (empty($subscriptions[$projectId][$role])) { - unset($subscriptions[$projectId][$role]); // Remove role when no channels - } - } - - if (empty($subscriptions[$projectId])) { // Remove project when no roles - unset($subscriptions[$projectId]); - } - - unset($connections[$fd]); - + Realtime::removeSubscription($fd, $subscriptions, $connections); Console::info('Connection close: ' . $fd); }); diff --git a/src/Appwrite/Realtime/Realtime.php b/src/Appwrite/Realtime/Realtime.php index b6028baf4d..2dd185f933 100644 --- a/src/Appwrite/Realtime/Realtime.php +++ b/src/Appwrite/Realtime/Realtime.php @@ -110,7 +110,7 @@ class Realtime * @param array $roles * @param array $channels */ - static function addSubscription($projectId, $connection, &$subscriptions, &$connections, &$roles, &$channels) + static function addSubscription($projectId, $connection, $roles, &$subscriptions, &$connections, &$channels) { /** * Build Subscriptions Tree @@ -145,4 +145,37 @@ class Realtime 'roles' => $roles, ]; } + + /** + * Remove Subscription. + * + * @param mixed $connection + * @param array $subscriptions + * @param array $connections + */ + static function removeSubscription($connection, &$subscriptions, &$connections) + { + $projectId = $connections[$connection]['projectId'] ?? ''; + $roles = $connections[$connection]['roles'] ?? []; + + foreach ($roles as $key => $role) { + foreach ($subscriptions[$projectId][$role] as $channel => $list) { + unset($subscriptions[$projectId][$role][$channel][$connection]); // Remove connection + + if (empty($subscriptions[$projectId][$role][$channel])) { + unset($subscriptions[$projectId][$role][$channel]); // Remove channel when no connections + } + } + + if (empty($subscriptions[$projectId][$role])) { + unset($subscriptions[$projectId][$role]); // Remove role when no channels + } + } + + if (empty($subscriptions[$projectId])) { // Remove project when no roles + unset($subscriptions[$projectId]); + } + + unset($connections[$connection]); + } } diff --git a/tests/unit/Realtime/RealtimeChannelsTest.php b/tests/unit/Realtime/RealtimeChannelsTest.php index cd993c6719..d10a7aa7d2 100644 --- a/tests/unit/Realtime/RealtimeChannelsTest.php +++ b/tests/unit/Realtime/RealtimeChannelsTest.php @@ -63,9 +63,9 @@ class RealtimeChannelsTest extends TestCase Realtime::addSubscription( '1', $this->connectionsCount, + Realtime::getRoles(), $this->subscriptions, $this->connections, - Realtime::getRoles(), Realtime::parseChannels([0 => $channel]) ); @@ -85,9 +85,9 @@ class RealtimeChannelsTest extends TestCase Realtime::addSubscription( '1', $this->connectionsCount, + Realtime::getRoles(), $this->subscriptions, $this->connections, - Realtime::getRoles(), Realtime::parseChannels([0 => $channel]) ); @@ -127,6 +127,20 @@ class RealtimeChannelsTest extends TestCase * - Guests */ $this->assertCount($this->connectionsTotal, $this->connections); + + Realtime::removeSubscription(-1, $this->subscriptions, $this->connections); + + $this->assertCount($this->connectionsTotal, $this->connections); + $this->assertCount(($this->connectionsAuthenticated + (3 * $this->connectionsPerChannel) + 3), $this->subscriptions['1']); + + for ($i=0; $i < $this->connectionsCount; $i++) { + Realtime::removeSubscription($i, $this->subscriptions, $this->connections); + + $this->assertCount(($this->connectionsCount - $i - 1), $this->connections); + } + + $this->assertEmpty($this->connections); + $this->assertEmpty($this->subscriptions); } /** diff --git a/tests/unit/Realtime/RealtimeGuestTest.php b/tests/unit/Realtime/RealtimeGuestTest.php index 35507a0d50..217ea77fb1 100644 --- a/tests/unit/Realtime/RealtimeGuestTest.php +++ b/tests/unit/Realtime/RealtimeGuestTest.php @@ -46,7 +46,7 @@ class RealtimeGuestTest extends TestCase $this->assertArrayNotHasKey('account', $channels); $this->assertArrayNotHasKey('account.456', $channels); - Realtime::addSubscription('1', 1, $this->subscriptions, $this->connections, $roles, $channels); + Realtime::addSubscription('1', 1, $roles, $this->subscriptions, $this->connections, $channels); $event = [ @@ -201,5 +201,16 @@ class RealtimeGuestTest extends TestCase ); $this->assertEmpty($receivers); + + Realtime::removeSubscription(2, $this->subscriptions, $this->connections); + + $this->assertCount(1, $this->connections); + $this->assertCount(2, $this->subscriptions['1']); + + + Realtime::removeSubscription(1, $this->subscriptions, $this->connections); + + $this->assertEmpty($this->connections); + $this->assertEmpty($this->subscriptions); } } diff --git a/tests/unit/Realtime/RealtimeTest.php b/tests/unit/Realtime/RealtimeTest.php index ea97d2c5c9..19fd1e74c4 100644 --- a/tests/unit/Realtime/RealtimeTest.php +++ b/tests/unit/Realtime/RealtimeTest.php @@ -70,7 +70,7 @@ class RealtimeTest extends TestCase $this->assertArrayNotHasKey('account', $channels); $this->assertArrayNotHasKey('account.456', $channels); - Realtime::addSubscription('1', 1, $this->subscriptions, $this->connections, $roles, $channels); + Realtime::addSubscription('1', 1, $roles, $this->subscriptions, $this->connections, $channels); $event = [ 'project' => '1', @@ -219,5 +219,16 @@ class RealtimeTest extends TestCase ); $this->assertEmpty($receivers); + + Realtime::removeSubscription(2, $this->subscriptions, $this->connections); + + $this->assertCount(1, $this->connections); + $this->assertCount(8, $this->subscriptions['1']); + + + Realtime::removeSubscription(1, $this->subscriptions, $this->connections); + + $this->assertEmpty($this->connections); + $this->assertEmpty($this->subscriptions); } } From 3e92d28cc83f25295eb49803f14ac0e686b17213 Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Mon, 1 Mar 2021 12:23:45 +0100 Subject: [PATCH 036/267] remove wildcard permissions from subscriptions --- src/Appwrite/Realtime/Realtime.php | 7 +++++-- tests/unit/Realtime/RealtimeChannelsTest.php | 7 +++---- tests/unit/Realtime/RealtimeGuestTest.php | 5 ++--- tests/unit/Realtime/RealtimeTest.php | 5 ++--- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/Appwrite/Realtime/Realtime.php b/src/Appwrite/Realtime/Realtime.php index 2dd185f933..03cc7ffb6c 100644 --- a/src/Appwrite/Realtime/Realtime.php +++ b/src/Appwrite/Realtime/Realtime.php @@ -25,7 +25,7 @@ class Realtime */ static function getRoles() { - $roles = ['*', 'role:' . ((self::$user->isEmpty()) ? Auth::USER_ROLE_GUEST : Auth::USER_ROLE_MEMBER)]; + $roles = ['role:' . ((self::$user->isEmpty()) ? Auth::USER_ROLE_GUEST : Auth::USER_ROLE_MEMBER)]; if (!(self::$user->isEmpty())) { $roles[] = 'user:' . self::$user->getId(); } @@ -87,7 +87,10 @@ class Realtime foreach ($connection['roles'] as $role) { if (\array_key_exists($role, $subscriptions[$event['project']])) { foreach ($event['data']['channels'] as $channel) { - if (\array_key_exists($channel, $subscriptions[$event['project']][$role]) && \in_array($role, $event['permissions'])) { + if ( + \array_key_exists($channel, $subscriptions[$event['project']][$role]) + && (\in_array($role, $event['permissions']) || \in_array('*', $event['permissions'])) + ) { foreach (array_keys($subscriptions[$event['project']][$role][$channel]) as $ids) { $receivers[] = $ids; } diff --git a/tests/unit/Realtime/RealtimeChannelsTest.php b/tests/unit/Realtime/RealtimeChannelsTest.php index d10a7aa7d2..daa657b04c 100644 --- a/tests/unit/Realtime/RealtimeChannelsTest.php +++ b/tests/unit/Realtime/RealtimeChannelsTest.php @@ -115,11 +115,10 @@ class RealtimeChannelsTest extends TestCase * - XXX users * - XXX teams * - XXX team roles (2 roles per team) - * - 1 * * - 1 role:guest * - 1 role:member */ - $this->assertCount(($this->connectionsAuthenticated + (3 * $this->connectionsPerChannel) + 3), $this->subscriptions['1']); + $this->assertCount(($this->connectionsAuthenticated + (3 * $this->connectionsPerChannel) + 2), $this->subscriptions['1']); /** * Check for connections @@ -131,9 +130,9 @@ class RealtimeChannelsTest extends TestCase Realtime::removeSubscription(-1, $this->subscriptions, $this->connections); $this->assertCount($this->connectionsTotal, $this->connections); - $this->assertCount(($this->connectionsAuthenticated + (3 * $this->connectionsPerChannel) + 3), $this->subscriptions['1']); + $this->assertCount(($this->connectionsAuthenticated + (3 * $this->connectionsPerChannel) + 2), $this->subscriptions['1']); - for ($i=0; $i < $this->connectionsCount; $i++) { + for ($i = 0; $i < $this->connectionsCount; $i++) { Realtime::removeSubscription($i, $this->subscriptions, $this->connections); $this->assertCount(($this->connectionsCount - $i - 1), $this->connections); diff --git a/tests/unit/Realtime/RealtimeGuestTest.php b/tests/unit/Realtime/RealtimeGuestTest.php index 217ea77fb1..aba7e68454 100644 --- a/tests/unit/Realtime/RealtimeGuestTest.php +++ b/tests/unit/Realtime/RealtimeGuestTest.php @@ -26,8 +26,7 @@ class RealtimeGuestTest extends TestCase ])); $roles = Realtime::getRoles(); - $this->assertCount(2, $roles); - $this->assertContains('*', $roles); + $this->assertCount(1, $roles); $this->assertContains('role:guest', $roles); $channels = [ @@ -205,7 +204,7 @@ class RealtimeGuestTest extends TestCase Realtime::removeSubscription(2, $this->subscriptions, $this->connections); $this->assertCount(1, $this->connections); - $this->assertCount(2, $this->subscriptions['1']); + $this->assertCount(1, $this->subscriptions['1']); Realtime::removeSubscription(1, $this->subscriptions, $this->connections); diff --git a/tests/unit/Realtime/RealtimeTest.php b/tests/unit/Realtime/RealtimeTest.php index 19fd1e74c4..3ff373b9d6 100644 --- a/tests/unit/Realtime/RealtimeTest.php +++ b/tests/unit/Realtime/RealtimeTest.php @@ -42,8 +42,7 @@ class RealtimeTest extends TestCase $roles = Realtime::getRoles(); - $this->assertCount(8, $roles); - $this->assertContains('*', $roles); + $this->assertCount(7, $roles); $this->assertContains('user:123', $roles); $this->assertContains('role:member', $roles); $this->assertContains('team:abc', $roles); @@ -223,7 +222,7 @@ class RealtimeTest extends TestCase Realtime::removeSubscription(2, $this->subscriptions, $this->connections); $this->assertCount(1, $this->connections); - $this->assertCount(8, $this->subscriptions['1']); + $this->assertCount(7, $this->subscriptions['1']); Realtime::removeSubscription(1, $this->subscriptions, $this->connections); From e79c5e93d02684c72aea9c836937090e9fd52de2 Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Mon, 1 Mar 2021 12:28:13 +0100 Subject: [PATCH 037/267] fchange realtime method names --- app/realtime.php | 4 ++-- src/Appwrite/Realtime/Realtime.php | 4 ++-- tests/unit/Realtime/RealtimeChannelsTest.php | 8 ++++---- tests/unit/Realtime/RealtimeGuestTest.php | 6 +++--- tests/unit/Realtime/RealtimeTest.php | 6 +++--- 5 files changed, 14 insertions(+), 14 deletions(-) diff --git a/app/realtime.php b/app/realtime.php index e5308a6a62..2875359a03 100644 --- a/app/realtime.php +++ b/app/realtime.php @@ -280,7 +280,7 @@ $server->on('open', function (Server $server, Request $request) use (&$connectio $server->close($connection); } - Realtime::addSubscription($project->getId(), $connection, $subscriptions, $connections, $roles, $channels); + Realtime::subscribe($project->getId(), $connection, $subscriptions, $connections, $roles, $channels); $server->push($connection, json_encode($channels)); }); @@ -294,7 +294,7 @@ $server->on('message', function (Server $server, Frame $frame) { }); $server->on('close', function (Server $server, int $fd) use (&$connections, &$subscriptions) { - Realtime::removeSubscription($fd, $subscriptions, $connections); + Realtime::unsubscribe($fd, $subscriptions, $connections); Console::info('Connection close: ' . $fd); }); diff --git a/src/Appwrite/Realtime/Realtime.php b/src/Appwrite/Realtime/Realtime.php index 03cc7ffb6c..62e2be5d5f 100644 --- a/src/Appwrite/Realtime/Realtime.php +++ b/src/Appwrite/Realtime/Realtime.php @@ -113,7 +113,7 @@ class Realtime * @param array $roles * @param array $channels */ - static function addSubscription($projectId, $connection, $roles, &$subscriptions, &$connections, &$channels) + static function subscribe($projectId, $connection, $roles, &$subscriptions, &$connections, &$channels) { /** * Build Subscriptions Tree @@ -156,7 +156,7 @@ class Realtime * @param array $subscriptions * @param array $connections */ - static function removeSubscription($connection, &$subscriptions, &$connections) + static function unsubscribe($connection, &$subscriptions, &$connections) { $projectId = $connections[$connection]['projectId'] ?? ''; $roles = $connections[$connection]['roles'] ?? []; diff --git a/tests/unit/Realtime/RealtimeChannelsTest.php b/tests/unit/Realtime/RealtimeChannelsTest.php index daa657b04c..afe52373b2 100644 --- a/tests/unit/Realtime/RealtimeChannelsTest.php +++ b/tests/unit/Realtime/RealtimeChannelsTest.php @@ -60,7 +60,7 @@ class RealtimeChannelsTest extends TestCase ] ])); - Realtime::addSubscription( + Realtime::subscribe( '1', $this->connectionsCount, Realtime::getRoles(), @@ -82,7 +82,7 @@ class RealtimeChannelsTest extends TestCase '$id' => '' ])); - Realtime::addSubscription( + Realtime::subscribe( '1', $this->connectionsCount, Realtime::getRoles(), @@ -127,13 +127,13 @@ class RealtimeChannelsTest extends TestCase */ $this->assertCount($this->connectionsTotal, $this->connections); - Realtime::removeSubscription(-1, $this->subscriptions, $this->connections); + Realtime::unsubscribe(-1, $this->subscriptions, $this->connections); $this->assertCount($this->connectionsTotal, $this->connections); $this->assertCount(($this->connectionsAuthenticated + (3 * $this->connectionsPerChannel) + 2), $this->subscriptions['1']); for ($i = 0; $i < $this->connectionsCount; $i++) { - Realtime::removeSubscription($i, $this->subscriptions, $this->connections); + Realtime::unsubscribe($i, $this->subscriptions, $this->connections); $this->assertCount(($this->connectionsCount - $i - 1), $this->connections); } diff --git a/tests/unit/Realtime/RealtimeGuestTest.php b/tests/unit/Realtime/RealtimeGuestTest.php index aba7e68454..d8d19c2d01 100644 --- a/tests/unit/Realtime/RealtimeGuestTest.php +++ b/tests/unit/Realtime/RealtimeGuestTest.php @@ -45,7 +45,7 @@ class RealtimeGuestTest extends TestCase $this->assertArrayNotHasKey('account', $channels); $this->assertArrayNotHasKey('account.456', $channels); - Realtime::addSubscription('1', 1, $roles, $this->subscriptions, $this->connections, $channels); + Realtime::subscribe('1', 1, $roles, $this->subscriptions, $this->connections, $channels); $event = [ @@ -201,13 +201,13 @@ class RealtimeGuestTest extends TestCase $this->assertEmpty($receivers); - Realtime::removeSubscription(2, $this->subscriptions, $this->connections); + Realtime::unsubscribe(2, $this->subscriptions, $this->connections); $this->assertCount(1, $this->connections); $this->assertCount(1, $this->subscriptions['1']); - Realtime::removeSubscription(1, $this->subscriptions, $this->connections); + Realtime::unsubscribe(1, $this->subscriptions, $this->connections); $this->assertEmpty($this->connections); $this->assertEmpty($this->subscriptions); diff --git a/tests/unit/Realtime/RealtimeTest.php b/tests/unit/Realtime/RealtimeTest.php index 3ff373b9d6..29af833ee8 100644 --- a/tests/unit/Realtime/RealtimeTest.php +++ b/tests/unit/Realtime/RealtimeTest.php @@ -69,7 +69,7 @@ class RealtimeTest extends TestCase $this->assertArrayNotHasKey('account', $channels); $this->assertArrayNotHasKey('account.456', $channels); - Realtime::addSubscription('1', 1, $roles, $this->subscriptions, $this->connections, $channels); + Realtime::subscribe('1', 1, $roles, $this->subscriptions, $this->connections, $channels); $event = [ 'project' => '1', @@ -219,13 +219,13 @@ class RealtimeTest extends TestCase $this->assertEmpty($receivers); - Realtime::removeSubscription(2, $this->subscriptions, $this->connections); + Realtime::unsubscribe(2, $this->subscriptions, $this->connections); $this->assertCount(1, $this->connections); $this->assertCount(7, $this->subscriptions['1']); - Realtime::removeSubscription(1, $this->subscriptions, $this->connections); + Realtime::unsubscribe(1, $this->subscriptions, $this->connections); $this->assertEmpty($this->connections); $this->assertEmpty($this->subscriptions); From ba4eedb114f7b8d6220c2ea83e22fc92790536d5 Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Mon, 1 Mar 2021 15:44:01 +0100 Subject: [PATCH 038/267] use redis connection pool --- app/realtime.php | 69 ++++++++++--------------- src/Appwrite/Database/Adapter/Redis.php | 11 +++- src/Appwrite/Realtime/Realtime.php | 6 +-- 3 files changed, 39 insertions(+), 47 deletions(-) diff --git a/app/realtime.php b/app/realtime.php index 2875359a03..3e4a0abd52 100644 --- a/app/realtime.php +++ b/app/realtime.php @@ -9,8 +9,9 @@ use Appwrite\Database\Adapter\Redis as RedisAdapter; use Appwrite\Database\Database; use Appwrite\Database\Document; use Appwrite\Database\Validator\Authorization; -use Appwrite\Extend\PDO; use Appwrite\Realtime\Realtime; +use Swoole\Database\RedisConfig; +use Swoole\Database\RedisPool; use Swoole\WebSocket\Server; use Swoole\Http\Request; use Swoole\Process; @@ -18,9 +19,7 @@ use Swoole\WebSocket\Frame; use Utopia\App; use Utopia\CLI\Console; use Utopia\Config\Config; -use Utopia\Registry\Registry; use Utopia\Swoole\Request as SwooleRequest; -use PDO as PDONative; use Utopia\Abuse\Abuse; use Utopia\Abuse\Adapters\TimeLimit; @@ -51,45 +50,30 @@ $server->set([ $subscriptions = []; $connections = []; -$register = new Registry(); - -$register->set('db', function () { // Register DB connection - $dbHost = App::getEnv('_APP_DB_HOST', ''); - $dbUser = App::getEnv('_APP_DB_USER', ''); - $dbPass = App::getEnv('_APP_DB_PASS', ''); - $dbScheme = App::getEnv('_APP_DB_SCHEMA', ''); - - $pdo = new PDO("mysql:host={$dbHost};dbname={$dbScheme};charset=utf8mb4", $dbUser, $dbPass, array( - PDONative::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8mb4', - PDONative::ATTR_TIMEOUT => 3, // Seconds - PDONative::ATTR_PERSISTENT => true - )); - - // Connection settings - $pdo->setAttribute(PDONative::ATTR_DEFAULT_FETCH_MODE, PDONative::FETCH_ASSOC); // Return arrays - $pdo->setAttribute(PDONative::ATTR_ERRMODE, PDONative::ERRMODE_EXCEPTION); // Handle all errors with exceptions - - return $pdo; -}); - -$register->set('cache', function () { // Register cache connection - $redis = new Redis(); - $redis->pconnect(App::getEnv('_APP_REDIS_HOST', ''), App::getEnv('_APP_REDIS_PORT', '')); - $user = App::getEnv('_APP_REDIS_USER', ''); - $pass = App::getEnv('_APP_REDIS_PASS', ''); - $auth = []; - if (!empty($user)) { - $auth["user"] = $user; +$register->set('redis', function () { + $user = App::getEnv('_APP_REDIS_USER',''); + $pass = App::getEnv('_APP_REDIS_PASS',''); + $auth = ''; + if(!empty($user)) { + $auth += $user; } - if (!empty($pass)) { - $auth["pass"] = $pass; + if(!empty($pass)) { + $auth += ':' . $pass; } - if (!empty($auth)) { - $redis->auth($auth); - } - $redis->setOption(Redis::OPT_READ_TIMEOUT, -1); - return $redis; + $config = new RedisConfig(); + $config + ->withHost(App::getEnv('_APP_REDIS_HOST', '')) + ->withPort(App::getEnv('_APP_REDIS_PORT', '')) + ->withAuth($auth) + ->withTimeout(0) + ->withReadTimeout(0) + ->withRetryInterval(0); + + + $pool = new RedisPool($config); + + return $pool; }); $server->on('workerStart', function ($server, $workerId) use (&$subscriptions, &$connections, &$register) { @@ -106,7 +90,8 @@ $server->on('workerStart', function ($server, $workerId) use (&$subscriptions, & sleep(5); // 5 sec delay between connection attempts } - $redis = $register->get('cache'); + $redis = $register->get('redis')->get(); + $redis->setOption(Redis::OPT_READ_TIMEOUT, -1); if ($redis->ping(true)) { $attempts = 0; @@ -183,7 +168,7 @@ $server->on('open', function (Server $server, Request $request) use (&$connectio App::setResource('consoleDB', function () use (&$register) { $consoleDB = new Database(); - $consoleDB->setAdapter(new MySQLAdapter($register)); // TODO: Add Redis + $consoleDB->setAdapter(new RedisAdapter(new MySQLAdapter($register), $register, true)); $consoleDB->setNamespace('app_console'); // Should be replaced with param if we want to have parent projects $consoleDB->setMocks(Config::getParam('collections', [])); @@ -280,7 +265,7 @@ $server->on('open', function (Server $server, Request $request) use (&$connectio $server->close($connection); } - Realtime::subscribe($project->getId(), $connection, $subscriptions, $connections, $roles, $channels); + Realtime::subscribe($project->getId(), $connection, $roles, $subscriptions, $connections, $channels); $server->push($connection, json_encode($channels)); }); diff --git a/src/Appwrite/Database/Adapter/Redis.php b/src/Appwrite/Database/Adapter/Redis.php index a1e440112d..3c5cdb5030 100644 --- a/src/Appwrite/Database/Adapter/Redis.php +++ b/src/Appwrite/Database/Adapter/Redis.php @@ -19,16 +19,23 @@ class Redis extends Adapter */ protected $adapter; + /** + * @var bool + */ + protected $isPool = false; + /** * Redis constructor. * * @param Adapter $adapter * @param Registry $register + * @param bool $isPool */ - public function __construct(Adapter $adapter, Registry $register) + public function __construct(Adapter $adapter, Registry $register, bool $isPool = false) { $this->register = $register; $this->adapter = $adapter; + $this->isPool = $isPool; } /** @@ -261,7 +268,7 @@ class Redis extends Adapter */ protected function getRedis(): Client { - return $this->register->get('cache'); + return $this->isPool ? $this->register->get('redis')->get() : $this->register->get('cache'); } /** diff --git a/src/Appwrite/Realtime/Realtime.php b/src/Appwrite/Realtime/Realtime.php index 62e2be5d5f..1dc5170ed5 100644 --- a/src/Appwrite/Realtime/Realtime.php +++ b/src/Appwrite/Realtime/Realtime.php @@ -79,7 +79,7 @@ class Realtime static function identifyReceivers(array &$event, array &$connections, array &$subscriptions) { $receivers = []; - foreach ($connections as $fd => $connection) { + foreach ($connections as $connection) { if ($connection['projectId'] !== $event['project']) { continue; } @@ -133,7 +133,7 @@ class Realtime $subscriptions[$projectId] = []; } - foreach ($roles as $key => $role) { + foreach ($roles as $role) { if (!isset($subscriptions[$projectId][$role])) { // Add user first connection $subscriptions[$projectId][$role] = []; } @@ -161,7 +161,7 @@ class Realtime $projectId = $connections[$connection]['projectId'] ?? ''; $roles = $connections[$connection]['roles'] ?? []; - foreach ($roles as $key => $role) { + foreach ($roles as $role) { foreach ($subscriptions[$projectId][$role] as $channel => $list) { unset($subscriptions[$projectId][$role][$channel][$connection]); // Remove connection From 6d7e443544db3421933b1318c2fa38ceccf8c9ef Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Mon, 1 Mar 2021 16:12:52 +0100 Subject: [PATCH 039/267] convert to switch case for channel parsing --- src/Appwrite/Realtime/Realtime.php | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/src/Appwrite/Realtime/Realtime.php b/src/Appwrite/Realtime/Realtime.php index 1dc5170ed5..ba60a33323 100644 --- a/src/Appwrite/Realtime/Realtime.php +++ b/src/Appwrite/Realtime/Realtime.php @@ -49,13 +49,17 @@ class Realtime $channels = array_flip($channels); foreach ($channels as $key => $value) { - if (strpos($key, 'account.') === 0) { - unset($channels[$key]); - } elseif ($key === 'account') { - if (!empty(self::$user->getId())) { - $channels['account.' . self::$user->getId()] = $value; - } - unset($channels['account']); + switch (true) { + case strpos($key, 'account.') === 0: + unset($channels[$key]); + break; + + case $key === 'account': + if (!empty(self::$user->getId())) { + $channels['account.' . self::$user->getId()] = $value; + } + unset($channels['account']); + break; } } From 2e4224683e5daa946c787414577e2cfe27dc5774 Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Mon, 1 Mar 2021 16:13:10 +0100 Subject: [PATCH 040/267] add package_max_length description --- app/realtime.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/realtime.php b/app/realtime.php index 3e4a0abd52..eea09e065c 100644 --- a/app/realtime.php +++ b/app/realtime.php @@ -44,7 +44,7 @@ $server = new Server('0.0.0.0', 80); $server->set([ 'websocket_compression' => true, - 'package_max_length' => 81920 + 'package_max_length' => 64000 // Default maximum Package Size (64kb) ]); $subscriptions = []; From 8e912ba25248fc086f7982f48491b14904dd9e2f Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Mon, 1 Mar 2021 16:33:33 +0100 Subject: [PATCH 041/267] improve time-space complexity of identifying receivers --- app/realtime.php | 2 +- src/Appwrite/Realtime/Realtime.php | 28 ++++++++------------ tests/unit/Realtime/RealtimeChannelsTest.php | 5 ---- tests/unit/Realtime/RealtimeGuestTest.php | 14 ---------- tests/unit/Realtime/RealtimeTest.php | 13 --------- 5 files changed, 12 insertions(+), 50 deletions(-) diff --git a/app/realtime.php b/app/realtime.php index eea09e065c..7648cd62ab 100644 --- a/app/realtime.php +++ b/app/realtime.php @@ -116,7 +116,7 @@ $server->on('workerStart', function ($server, $workerId) use (&$subscriptions, & */ $event = json_decode($payload, true); - $receivers = Realtime::identifyReceivers($event, $connections, $subscriptions); + $receivers = Realtime::identifyReceivers($event, $subscriptions); foreach ($receivers as $receiver) { if ($server->exist($receiver) && $server->isEstablished($receiver)) { diff --git a/src/Appwrite/Realtime/Realtime.php b/src/Appwrite/Realtime/Realtime.php index ba60a33323..256ad64b31 100644 --- a/src/Appwrite/Realtime/Realtime.php +++ b/src/Appwrite/Realtime/Realtime.php @@ -80,26 +80,20 @@ class Realtime * @param array $connections * @param array $subscriptions */ - static function identifyReceivers(array &$event, array &$connections, array &$subscriptions) + static function identifyReceivers(array &$event, array &$subscriptions) { $receivers = []; - foreach ($connections as $connection) { - if ($connection['projectId'] !== $event['project']) { - continue; - } - - foreach ($connection['roles'] as $role) { - if (\array_key_exists($role, $subscriptions[$event['project']])) { - foreach ($event['data']['channels'] as $channel) { - if ( - \array_key_exists($channel, $subscriptions[$event['project']][$role]) - && (\in_array($role, $event['permissions']) || \in_array('*', $event['permissions'])) - ) { - foreach (array_keys($subscriptions[$event['project']][$role][$channel]) as $ids) { - $receivers[] = $ids; - } - break; + if ($subscriptions[$event['project']]) { + foreach ($subscriptions[$event['project']] as $role => $subscription) { + foreach ($event['data']['channels'] as $channel) { + if ( + \array_key_exists($channel, $subscriptions[$event['project']][$role]) + && (\in_array($role, $event['permissions']) || \in_array('*', $event['permissions'])) + ) { + foreach (array_keys($subscriptions[$event['project']][$role][$channel]) as $ids) { + $receivers[] = $ids; } + break; } } } diff --git a/tests/unit/Realtime/RealtimeChannelsTest.php b/tests/unit/Realtime/RealtimeChannelsTest.php index afe52373b2..d5b0903957 100644 --- a/tests/unit/Realtime/RealtimeChannelsTest.php +++ b/tests/unit/Realtime/RealtimeChannelsTest.php @@ -160,7 +160,6 @@ class RealtimeChannelsTest extends TestCase $receivers = Realtime::identifyReceivers( $event, - $this->connections, $this->subscriptions ); @@ -197,7 +196,6 @@ class RealtimeChannelsTest extends TestCase $receivers = Realtime::identifyReceivers( $event, - $this->connections, $this->subscriptions ); @@ -235,7 +233,6 @@ class RealtimeChannelsTest extends TestCase $receivers = Realtime::identifyReceivers( $event, - $this->connections, $this->subscriptions ); @@ -273,7 +270,6 @@ class RealtimeChannelsTest extends TestCase $receivers = Realtime::identifyReceivers( $event, - $this->connections, $this->subscriptions ); @@ -303,7 +299,6 @@ class RealtimeChannelsTest extends TestCase $receivers = Realtime::identifyReceivers( $event, - $this->connections, $this->subscriptions ); diff --git a/tests/unit/Realtime/RealtimeGuestTest.php b/tests/unit/Realtime/RealtimeGuestTest.php index d8d19c2d01..4eff23b1c6 100644 --- a/tests/unit/Realtime/RealtimeGuestTest.php +++ b/tests/unit/Realtime/RealtimeGuestTest.php @@ -61,7 +61,6 @@ class RealtimeGuestTest extends TestCase $receivers = Realtime::identifyReceivers( $event, - $this->connections, $this->subscriptions ); @@ -72,7 +71,6 @@ class RealtimeGuestTest extends TestCase $receivers = Realtime::identifyReceivers( $event, - $this->connections, $this->subscriptions ); @@ -83,7 +81,6 @@ class RealtimeGuestTest extends TestCase $receivers = Realtime::identifyReceivers( $event, - $this->connections, $this->subscriptions ); @@ -93,7 +90,6 @@ class RealtimeGuestTest extends TestCase $receivers = Realtime::identifyReceivers( $event, - $this->connections, $this->subscriptions ); @@ -103,7 +99,6 @@ class RealtimeGuestTest extends TestCase $receivers = Realtime::identifyReceivers( $event, - $this->connections, $this->subscriptions ); @@ -113,7 +108,6 @@ class RealtimeGuestTest extends TestCase $receivers = Realtime::identifyReceivers( $event, - $this->connections, $this->subscriptions ); @@ -123,7 +117,6 @@ class RealtimeGuestTest extends TestCase $receivers = Realtime::identifyReceivers( $event, - $this->connections, $this->subscriptions ); @@ -133,7 +126,6 @@ class RealtimeGuestTest extends TestCase $receivers = Realtime::identifyReceivers( $event, - $this->connections, $this->subscriptions ); @@ -143,7 +135,6 @@ class RealtimeGuestTest extends TestCase $receivers = Realtime::identifyReceivers( $event, - $this->connections, $this->subscriptions ); @@ -153,7 +144,6 @@ class RealtimeGuestTest extends TestCase $receivers = Realtime::identifyReceivers( $event, - $this->connections, $this->subscriptions ); @@ -163,7 +153,6 @@ class RealtimeGuestTest extends TestCase $receivers = Realtime::identifyReceivers( $event, - $this->connections, $this->subscriptions ); @@ -174,7 +163,6 @@ class RealtimeGuestTest extends TestCase $receivers = Realtime::identifyReceivers( $event, - $this->connections, $this->subscriptions ); @@ -184,7 +172,6 @@ class RealtimeGuestTest extends TestCase $receivers = Realtime::identifyReceivers( $event, - $this->connections, $this->subscriptions ); @@ -195,7 +182,6 @@ class RealtimeGuestTest extends TestCase $receivers = Realtime::identifyReceivers( $event, - $this->connections, $this->subscriptions ); diff --git a/tests/unit/Realtime/RealtimeTest.php b/tests/unit/Realtime/RealtimeTest.php index 29af833ee8..a819f4cb72 100644 --- a/tests/unit/Realtime/RealtimeTest.php +++ b/tests/unit/Realtime/RealtimeTest.php @@ -83,7 +83,6 @@ class RealtimeTest extends TestCase $receivers = Realtime::identifyReceivers( $event, - $this->connections, $this->subscriptions ); @@ -94,7 +93,6 @@ class RealtimeTest extends TestCase $receivers = Realtime::identifyReceivers( $event, - $this->connections, $this->subscriptions ); @@ -105,7 +103,6 @@ class RealtimeTest extends TestCase $receivers = Realtime::identifyReceivers( $event, - $this->connections, $this->subscriptions ); @@ -116,7 +113,6 @@ class RealtimeTest extends TestCase $receivers = Realtime::identifyReceivers( $event, - $this->connections, $this->subscriptions ); @@ -127,7 +123,6 @@ class RealtimeTest extends TestCase $receivers = Realtime::identifyReceivers( $event, - $this->connections, $this->subscriptions ); @@ -138,7 +133,6 @@ class RealtimeTest extends TestCase $receivers = Realtime::identifyReceivers( $event, - $this->connections, $this->subscriptions ); @@ -149,7 +143,6 @@ class RealtimeTest extends TestCase $receivers = Realtime::identifyReceivers( $event, - $this->connections, $this->subscriptions ); @@ -160,7 +153,6 @@ class RealtimeTest extends TestCase $receivers = Realtime::identifyReceivers( $event, - $this->connections, $this->subscriptions ); @@ -171,7 +163,6 @@ class RealtimeTest extends TestCase $receivers = Realtime::identifyReceivers( $event, - $this->connections, $this->subscriptions ); @@ -181,7 +172,6 @@ class RealtimeTest extends TestCase $receivers = Realtime::identifyReceivers( $event, - $this->connections, $this->subscriptions ); @@ -192,7 +182,6 @@ class RealtimeTest extends TestCase $receivers = Realtime::identifyReceivers( $event, - $this->connections, $this->subscriptions ); @@ -202,7 +191,6 @@ class RealtimeTest extends TestCase $receivers = Realtime::identifyReceivers( $event, - $this->connections, $this->subscriptions ); @@ -213,7 +201,6 @@ class RealtimeTest extends TestCase $receivers = Realtime::identifyReceivers( $event, - $this->connections, $this->subscriptions ); From a3fb2abf66c95f138294e46d14a70887a0a11f5f Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Mon, 1 Mar 2021 17:02:01 +0100 Subject: [PATCH 042/267] add cors validation --- app/realtime.php | 31 ++++++++++++++++++++++++++----- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/app/realtime.php b/app/realtime.php index 7648cd62ab..0863b5aa7e 100644 --- a/app/realtime.php +++ b/app/realtime.php @@ -9,6 +9,7 @@ use Appwrite\Database\Adapter\Redis as RedisAdapter; use Appwrite\Database\Database; use Appwrite\Database\Document; use Appwrite\Database\Validator\Authorization; +use Appwrite\Network\Validator\Origin; use Appwrite\Realtime\Realtime; use Swoole\Database\RedisConfig; use Swoole\Database\RedisPool; @@ -51,13 +52,13 @@ $subscriptions = []; $connections = []; $register->set('redis', function () { - $user = App::getEnv('_APP_REDIS_USER',''); - $pass = App::getEnv('_APP_REDIS_PASS',''); + $user = App::getEnv('_APP_REDIS_USER', ''); + $pass = App::getEnv('_APP_REDIS_PASS', ''); $auth = ''; - if(!empty($user)) { + if (!empty($user)) { $auth += $user; } - if(!empty($pass)) { + if (!empty($pass)) { $auth += ':' . $pass; } @@ -188,6 +189,10 @@ $server->on('open', function (Server $server, Request $request) use (&$connectio return $project; }, ['consoleDB', 'request']); + App::setResource('console', function ($consoleDB) { + return $consoleDB->getDocument('console'); + }, ['consoleDB']); + App::setResource('user', function ($project, $request, $projectDB) { /** @var Utopia\Swoole\Request $request */ /** @var Appwrite\Database\Document $project */ @@ -226,6 +231,9 @@ $server->on('open', function (Server $server, Request $request) use (&$connectio /** @var Appwrite\Database\Document $project */ $project = $app->getResource('project'); + /** @var Appwrite\Database\Document $console */ + $console = $app->getResource('console'); + /* * Abuse Check */ @@ -244,6 +252,19 @@ $server->on('open', function (Server $server, Request $request) use (&$connectio $server->close($connection); } + /* + * 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)) { + $server->push($connection, $originValidator->getDescription()); + $server->close($connection); + } + /* * Project Check */ @@ -264,7 +285,7 @@ $server->on('open', function (Server $server, Request $request) use (&$connectio $server->push($connection, 'Missing channels'); $server->close($connection); } - + Realtime::subscribe($project->getId(), $connection, $roles, $subscriptions, $connections, $channels); $server->push($connection, json_encode($channels)); From bbabeaf0260e44d53fc6dc60884f7dab29b5549a Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Tue, 2 Mar 2021 10:56:18 +0100 Subject: [PATCH 043/267] add ratchet websocket client to e2e test --- composer.json | 1 + composer.lock | 659 +++++++++++++++++- tests/e2e/Services/Realtime/RealtimeBase.php | 44 ++ .../Realtime/RealtimeCustomClientTest.php | 15 + 4 files changed, 695 insertions(+), 24 deletions(-) create mode 100644 tests/e2e/Services/Realtime/RealtimeBase.php create mode 100644 tests/e2e/Services/Realtime/RealtimeCustomClientTest.php diff --git a/composer.json b/composer.json index f82d5c3503..c6e41ae86a 100644 --- a/composer.json +++ b/composer.json @@ -61,6 +61,7 @@ "require-dev": { "appwrite/sdk-generator": "0.5.5", "phpunit/phpunit": "9.4.2", + "ratchet/pawl": "0.3.5", "swoole/ide-helper": "4.5.5", "vimeo/psalm": "4.1.1" }, diff --git a/composer.lock b/composer.lock index 075cf1359c..c3ae72b266 100644 --- a/composer.lock +++ b/composer.lock @@ -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": "5893b378d1dcda91aedf77059f4b0efb", + "content-hash": "ee078cbbb5ae7897f97c13e82fec567f", "packages": [ { "name": "adhocore/jwt", @@ -349,7 +349,7 @@ "issues": "https://github.com/domnikl/statsd-php/issues", "source": "https://github.com/domnikl/statsd-php/tree/master" }, - "abandoned": true, + "abandoned": "slickdeals/statsd", "time": "2020-01-03T14:24:58+00:00" }, { @@ -410,6 +410,53 @@ ], "time": "2020-08-21T02:30:13+00:00" }, + { + "name": "evenement/evenement", + "version": "v3.0.1", + "source": { + "type": "git", + "url": "https://github.com/igorw/evenement.git", + "reference": "531bfb9d15f8aa57454f5f0285b18bec903b8fb7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/igorw/evenement/zipball/531bfb9d15f8aa57454f5f0285b18bec903b8fb7", + "reference": "531bfb9d15f8aa57454f5f0285b18bec903b8fb7", + "shasum": "" + }, + "require": { + "php": ">=7.0" + }, + "require-dev": { + "phpunit/phpunit": "^6.0" + }, + "type": "library", + "autoload": { + "psr-0": { + "Evenement": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Igor Wiedler", + "email": "igor@wiedler.ch" + } + ], + "description": "Événement is a very simple event dispatching library for PHP", + "keywords": [ + "event-dispatcher", + "event-emitter" + ], + "support": { + "issues": "https://github.com/igorw/evenement/issues", + "source": "https://github.com/igorw/evenement/tree/master" + }, + "time": "2017-07-23T21:35:13+00:00" + }, { "name": "guzzlehttp/guzzle", "version": "7.2.0", @@ -1093,6 +1140,570 @@ }, "time": "2019-03-08T08:55:37+00:00" }, + { + "name": "ratchet/pawl", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/ratchetphp/Pawl.git", + "reference": "89ec703c76dc893484a2a0ed44b48a37d445abd5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ratchetphp/Pawl/zipball/89ec703c76dc893484a2a0ed44b48a37d445abd5", + "reference": "89ec703c76dc893484a2a0ed44b48a37d445abd5", + "shasum": "" + }, + "require": { + "evenement/evenement": "^3.0 || ^2.0", + "php": ">=5.4", + "ratchet/rfc6455": "^0.3", + "react/socket": "^1.0 || ^0.8 || ^0.7" + }, + "require-dev": { + "phpunit/phpunit": "~4.8" + }, + "suggest": { + "reactivex/rxphp": "~2.0" + }, + "default-branch": true, + "type": "library", + "autoload": { + "psr-4": { + "Ratchet\\Client\\": "src" + }, + "files": [ + "src/functions_include.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Asynchronous WebSocket client", + "keywords": [ + "Ratchet", + "async", + "client", + "websocket", + "websocket client" + ], + "support": { + "issues": "https://github.com/ratchetphp/Pawl/issues", + "source": "https://github.com/ratchetphp/Pawl/tree/v0.3.5" + }, + "time": "2020-07-17T15:32:47+00:00" + }, + { + "name": "ratchet/rfc6455", + "version": "v0.3", + "source": { + "type": "git", + "url": "https://github.com/ratchetphp/RFC6455.git", + "reference": "c8651c7938651c2d55f5d8c2422ac5e57a183341" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ratchetphp/RFC6455/zipball/c8651c7938651c2d55f5d8c2422ac5e57a183341", + "reference": "c8651c7938651c2d55f5d8c2422ac5e57a183341", + "shasum": "" + }, + "require": { + "guzzlehttp/psr7": "^1.0", + "php": ">=5.4.2" + }, + "require-dev": { + "phpunit/phpunit": "5.7.*", + "react/socket": "^1.3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Ratchet\\RFC6455\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "role": "Developer" + }, + { + "name": "Matt Bonneau", + "role": "Developer" + } + ], + "description": "RFC6455 WebSocket protocol handler", + "homepage": "http://socketo.me", + "keywords": [ + "WebSockets", + "rfc6455", + "websocket" + ], + "support": { + "chat": "https://gitter.im/reactphp/reactphp", + "issues": "https://github.com/ratchetphp/RFC6455/issues", + "source": "https://github.com/ratchetphp/RFC6455/tree/v0.3" + }, + "time": "2020-05-15T18:31:24+00:00" + }, + { + "name": "react/cache", + "version": "v1.1.1", + "source": { + "type": "git", + "url": "https://github.com/reactphp/cache.git", + "reference": "4bf736a2cccec7298bdf745db77585966fc2ca7e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/cache/zipball/4bf736a2cccec7298bdf745db77585966fc2ca7e", + "reference": "4bf736a2cccec7298bdf745db77585966fc2ca7e", + "shasum": "" + }, + "require": { + "php": ">=5.3.0", + "react/promise": "^3.0 || ^2.0 || ^1.1" + }, + "require-dev": { + "phpunit/phpunit": "^9.3 || ^5.7 || ^4.8.35" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\Cache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "Async, Promise-based cache interface for ReactPHP", + "keywords": [ + "cache", + "caching", + "promise", + "reactphp" + ], + "support": { + "issues": "https://github.com/reactphp/cache/issues", + "source": "https://github.com/reactphp/cache/tree/v1.1.1" + }, + "funding": [ + { + "url": "https://github.com/WyriHaximus", + "type": "github" + }, + { + "url": "https://github.com/clue", + "type": "github" + } + ], + "time": "2021-02-02T06:47:52+00:00" + }, + { + "name": "react/dns", + "version": "v1.4.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/dns.git", + "reference": "665260757171e2ab17485b44e7ffffa7acb6ca1f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/dns/zipball/665260757171e2ab17485b44e7ffffa7acb6ca1f", + "reference": "665260757171e2ab17485b44e7ffffa7acb6ca1f", + "shasum": "" + }, + "require": { + "php": ">=5.3.0", + "react/cache": "^1.0 || ^0.6 || ^0.5", + "react/event-loop": "^1.0 || ^0.5", + "react/promise": "^3.0 || ^2.7 || ^1.2.1", + "react/promise-timer": "^1.2" + }, + "require-dev": { + "clue/block-react": "^1.2", + "phpunit/phpunit": "^9.3 || ^4.8.35" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\Dns\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "Async DNS resolver for ReactPHP", + "keywords": [ + "async", + "dns", + "dns-resolver", + "reactphp" + ], + "support": { + "issues": "https://github.com/reactphp/dns/issues", + "source": "https://github.com/reactphp/dns/tree/v1.4.0" + }, + "funding": [ + { + "url": "https://github.com/WyriHaximus", + "type": "github" + }, + { + "url": "https://github.com/clue", + "type": "github" + } + ], + "time": "2020-09-18T12:12:55+00:00" + }, + { + "name": "react/event-loop", + "version": "v1.1.1", + "source": { + "type": "git", + "url": "https://github.com/reactphp/event-loop.git", + "reference": "6d24de090cd59cfc830263cfba965be77b563c13" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/event-loop/zipball/6d24de090cd59cfc830263cfba965be77b563c13", + "reference": "6d24de090cd59cfc830263cfba965be77b563c13", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "require-dev": { + "phpunit/phpunit": "^7.0 || ^6.4 || ^5.7 || ^4.8.35" + }, + "suggest": { + "ext-event": "~1.0 for ExtEventLoop", + "ext-pcntl": "For signal handling support when using the StreamSelectLoop", + "ext-uv": "* for ExtUvLoop" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\EventLoop\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "ReactPHP's core reactor event loop that libraries can use for evented I/O.", + "keywords": [ + "asynchronous", + "event-loop" + ], + "support": { + "issues": "https://github.com/reactphp/event-loop/issues", + "source": "https://github.com/reactphp/event-loop/tree/v1.1.1" + }, + "time": "2020-01-01T18:39:52+00:00" + }, + { + "name": "react/promise", + "version": "2.x-dev", + "source": { + "type": "git", + "url": "https://github.com/reactphp/promise.git", + "reference": "a9752a861e21c0fe0b380c9f9e55beddc0ed7d31" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/promise/zipball/a9752a861e21c0fe0b380c9f9e55beddc0ed7d31", + "reference": "a9752a861e21c0fe0b380c9f9e55beddc0ed7d31", + "shasum": "" + }, + "require": { + "php": ">=5.4.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.3 || ^5.7 || ^4.8.36" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\Promise\\": "src/" + }, + "files": [ + "src/functions_include.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com" + } + ], + "description": "A lightweight implementation of CommonJS Promises/A for PHP", + "keywords": [ + "promise", + "promises" + ], + "support": { + "issues": "https://github.com/reactphp/promise/issues", + "source": "https://github.com/reactphp/promise/tree/2.x" + }, + "funding": [ + { + "url": "https://github.com/WyriHaximus", + "type": "github" + }, + { + "url": "https://github.com/clue", + "type": "github" + } + ], + "time": "2021-02-09T15:06:50+00:00" + }, + { + "name": "react/promise-timer", + "version": "v1.6.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/promise-timer.git", + "reference": "daee9baf6ef30c43ea4c86399f828bb5f558f6e6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/promise-timer/zipball/daee9baf6ef30c43ea4c86399f828bb5f558f6e6", + "reference": "daee9baf6ef30c43ea4c86399f828bb5f558f6e6", + "shasum": "" + }, + "require": { + "php": ">=5.3", + "react/event-loop": "^1.0 || ^0.5 || ^0.4 || ^0.3.5", + "react/promise": "^3.0 || ^2.7.0 || ^1.2.1" + }, + "require-dev": { + "phpunit/phpunit": "^9.0 || ^5.7 || ^4.8.35" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\Promise\\Timer\\": "src/" + }, + "files": [ + "src/functions_include.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@lueck.tv" + } + ], + "description": "A trivial implementation of timeouts for Promises, built on top of ReactPHP.", + "homepage": "https://github.com/reactphp/promise-timer", + "keywords": [ + "async", + "event-loop", + "promise", + "reactphp", + "timeout", + "timer" + ], + "support": { + "issues": "https://github.com/reactphp/promise-timer/issues", + "source": "https://github.com/reactphp/promise-timer/tree/v1.6.0" + }, + "time": "2020-07-10T12:18:06+00:00" + }, + { + "name": "react/socket", + "version": "v1.6.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/socket.git", + "reference": "e2b96b23a13ca9b41ab343268dbce3f8ef4d524a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/socket/zipball/e2b96b23a13ca9b41ab343268dbce3f8ef4d524a", + "reference": "e2b96b23a13ca9b41ab343268dbce3f8ef4d524a", + "shasum": "" + }, + "require": { + "evenement/evenement": "^3.0 || ^2.0 || ^1.0", + "php": ">=5.3.0", + "react/dns": "^1.1", + "react/event-loop": "^1.0 || ^0.5", + "react/promise": "^2.6.0 || ^1.2.1", + "react/promise-timer": "^1.4.0", + "react/stream": "^1.1" + }, + "require-dev": { + "clue/block-react": "^1.2", + "phpunit/phpunit": "^9.3 || ^5.7 || ^4.8.35", + "react/promise-stream": "^1.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\Socket\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "Async, streaming plaintext TCP/IP and secure TLS socket server and client connections for ReactPHP", + "keywords": [ + "Connection", + "Socket", + "async", + "reactphp", + "stream" + ], + "support": { + "issues": "https://github.com/reactphp/socket/issues", + "source": "https://github.com/reactphp/socket/tree/v1.6.0" + }, + "funding": [ + { + "url": "https://github.com/WyriHaximus", + "type": "github" + }, + { + "url": "https://github.com/clue", + "type": "github" + } + ], + "time": "2020-08-28T12:49:05+00:00" + }, + { + "name": "react/stream", + "version": "v1.1.1", + "source": { + "type": "git", + "url": "https://github.com/reactphp/stream.git", + "reference": "7c02b510ee3f582c810aeccd3a197b9c2f52ff1a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/stream/zipball/7c02b510ee3f582c810aeccd3a197b9c2f52ff1a", + "reference": "7c02b510ee3f582c810aeccd3a197b9c2f52ff1a", + "shasum": "" + }, + "require": { + "evenement/evenement": "^3.0 || ^2.0 || ^1.0", + "php": ">=5.3.8", + "react/event-loop": "^1.0 || ^0.5 || ^0.4 || ^0.3.5" + }, + "require-dev": { + "clue/stream-filter": "~1.2", + "phpunit/phpunit": "^7.0 || ^6.4 || ^5.7 || ^4.8.35" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\Stream\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Event-driven readable and writable streams for non-blocking I/O in ReactPHP", + "keywords": [ + "event-driven", + "io", + "non-blocking", + "pipe", + "reactphp", + "readable", + "stream", + "writable" + ], + "support": { + "issues": "https://github.com/reactphp/stream/issues", + "source": "https://github.com/reactphp/stream/tree/v1.1.1" + }, + "time": "2020-05-04T10:17:57+00:00" + }, { "name": "resque/php-resque", "version": "v1.3.6", @@ -2545,9 +3156,9 @@ ], "support": { "issues": "https://github.com/felixfbecker/php-language-server-protocol/issues", - "source": "https://github.com/felixfbecker/php-language-server-protocol/tree/v1.5.0" + "source": "https://github.com/felixfbecker/php-language-server-protocol/tree/1.5.1" }, - "time": "2020-10-23T13:55:30+00:00" + "time": "2021-02-22T14:02:09+00:00" }, { "name": "matthiasmullie/minify", @@ -3375,7 +3986,7 @@ "type": "github" } ], - "time": "2021-02-14T06:52:34+00:00" + "time": "2021-02-23T15:48:43+00:00" }, { "name": "phpunit/php-invoker", @@ -3439,7 +4050,7 @@ "type": "github" } ], - "time": "2021-02-14T06:52:42+00:00" + "time": "2021-02-23T15:48:51+00:00" }, { "name": "phpunit/php-text-template", @@ -3499,7 +4110,7 @@ "type": "github" } ], - "time": "2021-02-14T06:53:15+00:00" + "time": "2021-02-23T15:49:24+00:00" }, { "name": "phpunit/php-timer", @@ -3559,7 +4170,7 @@ "type": "github" } ], - "time": "2021-02-14T06:52:50+00:00" + "time": "2021-02-23T15:48:59+00:00" }, { "name": "phpunit/phpunit", @@ -3773,7 +4384,7 @@ "type": "github" } ], - "time": "2021-02-14T06:53:40+00:00" + "time": "2021-02-23T15:49:50+00:00" }, { "name": "sebastian/code-unit", @@ -3885,7 +4496,7 @@ "type": "github" } ], - "time": "2021-02-14T06:51:27+00:00" + "time": "2021-02-23T15:47:39+00:00" }, { "name": "sebastian/comparator", @@ -3960,7 +4571,7 @@ "type": "github" } ], - "time": "2021-02-14T06:51:35+00:00" + "time": "2021-02-23T15:47:47+00:00" }, { "name": "sebastian/complexity", @@ -4084,7 +4695,7 @@ "type": "github" } ], - "time": "2021-02-14T06:51:43+00:00" + "time": "2021-02-23T15:47:55+00:00" }, { "name": "sebastian/environment", @@ -4148,7 +4759,7 @@ "type": "github" } ], - "time": "2021-02-14T06:51:52+00:00" + "time": "2021-02-23T15:48:03+00:00" }, { "name": "sebastian/exporter", @@ -4226,7 +4837,7 @@ "type": "github" } ], - "time": "2021-02-14T06:52:00+00:00" + "time": "2021-02-23T15:48:12+00:00" }, { "name": "sebastian/global-state", @@ -4291,7 +4902,7 @@ "type": "github" } ], - "time": "2021-02-14T06:52:09+00:00" + "time": "2021-02-23T15:48:19+00:00" }, { "name": "sebastian/lines-of-code", @@ -4406,7 +5017,7 @@ "type": "github" } ], - "time": "2021-02-14T06:52:17+00:00" + "time": "2021-02-23T15:48:28+00:00" }, { "name": "sebastian/object-reflector", @@ -4462,7 +5073,7 @@ "type": "github" } ], - "time": "2021-02-14T06:52:26+00:00" + "time": "2021-02-23T15:48:35+00:00" }, { "name": "sebastian/recursion-context", @@ -4526,7 +5137,7 @@ "type": "github" } ], - "time": "2021-02-14T06:52:58+00:00" + "time": "2021-02-23T15:49:08+00:00" }, { "name": "sebastian/resource-operations", @@ -4639,7 +5250,7 @@ "type": "github" } ], - "time": "2021-02-14T06:53:07+00:00" + "time": "2021-02-23T15:49:16+00:00" }, { "name": "sebastian/version", @@ -4828,7 +5439,7 @@ "type": "tidelift" } ], - "time": "2021-02-17T15:27:35+00:00" + "time": "2021-02-23T10:10:15+00:00" }, { "name": "symfony/polyfill-ctype", @@ -5347,7 +5958,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "2.3-dev" + "dev-main": "2.4-dev" }, "thanks": { "name": "symfony/contracts", @@ -5400,7 +6011,7 @@ "type": "tidelift" } ], - "time": "2021-01-27T16:27:53+00:00" + "time": "2021-02-25T16:38:04+00:00" }, { "name": "symfony/string", @@ -5613,7 +6224,7 @@ "type": "tidelift" } ], - "time": "2021-02-08T09:50:07+00:00" + "time": "2021-02-22T11:56:05+00:00" }, { "name": "vimeo/psalm", @@ -5777,7 +6388,7 @@ "issues": "https://github.com/webmozarts/assert/issues", "source": "https://github.com/webmozarts/assert/tree/master" }, - "time": "2021-01-18T12:52:36+00:00" + "time": "2021-02-28T20:01:57+00:00" }, { "name": "webmozart/path-util", diff --git a/tests/e2e/Services/Realtime/RealtimeBase.php b/tests/e2e/Services/Realtime/RealtimeBase.php new file mode 100644 index 0000000000..2c0c3b843c --- /dev/null +++ b/tests/e2e/Services/Realtime/RealtimeBase.php @@ -0,0 +1,44 @@ + $this->getProject()['$id'], + 'channels' => $channels + ]; + return 'ws://appwrite-traefik/v1/realtime?' . http_build_query($query); + } + + public function testHandshake() + { + /** + * Test for SUCCESS + */ + Ratchet\Client\connect($this->getWebsocket(['documents']), [], ['origin' => 'appwrite.test'])->then(function($conn) { + $conn->on('message', function(MessageInterface $msg) use ($conn) { + $this->assertEquals('{"documents":0}', $msg->__toString()); + $conn->close(); + }); + }, function ($e) { + echo "Could not connect: {$e->getMessage()}\n"; + }); + + /** + * Test for FAILURE + */ + Ratchet\Client\connect($this->getWebsocket(['account']), [], ['origin' => 'appwrite.test'])->then(function($conn) { + $conn->on('message', function(Message $msg) use ($conn) { + $this->assertEquals('Missing channels', $msg->__toString()); + $conn->close(); + }); + }, function ($e) { + echo "Could not connect: {$e->getMessage()}\n"; + }); + } +} \ No newline at end of file diff --git a/tests/e2e/Services/Realtime/RealtimeCustomClientTest.php b/tests/e2e/Services/Realtime/RealtimeCustomClientTest.php new file mode 100644 index 0000000000..850bb3d505 --- /dev/null +++ b/tests/e2e/Services/Realtime/RealtimeCustomClientTest.php @@ -0,0 +1,15 @@ + Date: Tue, 2 Mar 2021 10:57:22 +0100 Subject: [PATCH 044/267] move project check before abuse and cors validation --- app/realtime.php | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/app/realtime.php b/app/realtime.php index 0863b5aa7e..503f1cae00 100644 --- a/app/realtime.php +++ b/app/realtime.php @@ -234,6 +234,14 @@ $server->on('open', function (Server $server, Request $request) use (&$connectio /** @var Appwrite\Database\Document $console */ $console = $app->getResource('console'); + /* + * Project Check + */ + if (empty($project->getId())) { + $server->push($connection, 'Missing or unknown project ID'); + $server->close($connection); + } + /* * Abuse Check */ @@ -265,14 +273,6 @@ $server->on('open', function (Server $server, Request $request) use (&$connectio $server->close($connection); } - /* - * Project Check - */ - if (empty($project->getId())) { - $server->push($connection, 'Missing or unknown project ID'); - $server->close($connection); - } - Realtime::setUser($user); $roles = Realtime::getRoles(); From 61b114a999dab5826ef5815b8a4aed13434cbab2 Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Tue, 2 Mar 2021 11:23:15 +0100 Subject: [PATCH 045/267] force cancel on connection close --- app/realtime.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/realtime.php b/app/realtime.php index 503f1cae00..15b32c1bbf 100644 --- a/app/realtime.php +++ b/app/realtime.php @@ -240,6 +240,7 @@ $server->on('open', function (Server $server, Request $request) use (&$connectio if (empty($project->getId())) { $server->push($connection, 'Missing or unknown project ID'); $server->close($connection); + return; } /* @@ -258,6 +259,7 @@ $server->on('open', function (Server $server, Request $request) use (&$connectio if ($abuse->check() && App::getEnv('_APP_OPTIONS_ABUSE', 'enabled') === 'enabled') { $server->push($connection, 'Too many requests'); $server->close($connection); + return; } /* @@ -271,6 +273,7 @@ $server->on('open', function (Server $server, Request $request) use (&$connectio if (!$originValidator->isValid($origin)) { $server->push($connection, $originValidator->getDescription()); $server->close($connection); + return; } Realtime::setUser($user); @@ -284,6 +287,7 @@ $server->on('open', function (Server $server, Request $request) use (&$connectio if (empty($channels)) { $server->push($connection, 'Missing channels'); $server->close($connection); + return; } Realtime::subscribe($project->getId(), $connection, $roles, $subscriptions, $connections, $channels); From fd462d831cac0603e26fae6ad7031d6e30234544 Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Tue, 2 Mar 2021 14:06:54 +0100 Subject: [PATCH 046/267] e2e testrun --- composer.json | 1 + composer.lock | 1404 +++++++++-------- tests/e2e/Services/Realtime/RealtimeBase.php | 38 +- .../Realtime/RealtimeCustomClientTest.php | 48 + 4 files changed, 776 insertions(+), 715 deletions(-) diff --git a/composer.json b/composer.json index c6e41ae86a..df75fc237b 100644 --- a/composer.json +++ b/composer.json @@ -63,6 +63,7 @@ "phpunit/phpunit": "9.4.2", "ratchet/pawl": "0.3.5", "swoole/ide-helper": "4.5.5", + "textalk/websocket": "1.5.2", "vimeo/psalm": "4.1.1" }, "repositories": [ diff --git a/composer.lock b/composer.lock index c3ae72b266..e695d73dce 100644 --- a/composer.lock +++ b/composer.lock @@ -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": "ee078cbbb5ae7897f97c13e82fec567f", + "content-hash": "0991bf902937a89fc9e9ba5d45263f21", "packages": [ { "name": "adhocore/jwt", @@ -410,53 +410,6 @@ ], "time": "2020-08-21T02:30:13+00:00" }, - { - "name": "evenement/evenement", - "version": "v3.0.1", - "source": { - "type": "git", - "url": "https://github.com/igorw/evenement.git", - "reference": "531bfb9d15f8aa57454f5f0285b18bec903b8fb7" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/igorw/evenement/zipball/531bfb9d15f8aa57454f5f0285b18bec903b8fb7", - "reference": "531bfb9d15f8aa57454f5f0285b18bec903b8fb7", - "shasum": "" - }, - "require": { - "php": ">=7.0" - }, - "require-dev": { - "phpunit/phpunit": "^6.0" - }, - "type": "library", - "autoload": { - "psr-0": { - "Evenement": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Igor Wiedler", - "email": "igor@wiedler.ch" - } - ], - "description": "Événement is a very simple event dispatching library for PHP", - "keywords": [ - "event-dispatcher", - "event-emitter" - ], - "support": { - "issues": "https://github.com/igorw/evenement/issues", - "source": "https://github.com/igorw/evenement/tree/master" - }, - "time": "2017-07-23T21:35:13+00:00" - }, { "name": "guzzlehttp/guzzle", "version": "7.2.0", @@ -1140,570 +1093,6 @@ }, "time": "2019-03-08T08:55:37+00:00" }, - { - "name": "ratchet/pawl", - "version": "dev-master", - "source": { - "type": "git", - "url": "https://github.com/ratchetphp/Pawl.git", - "reference": "89ec703c76dc893484a2a0ed44b48a37d445abd5" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/ratchetphp/Pawl/zipball/89ec703c76dc893484a2a0ed44b48a37d445abd5", - "reference": "89ec703c76dc893484a2a0ed44b48a37d445abd5", - "shasum": "" - }, - "require": { - "evenement/evenement": "^3.0 || ^2.0", - "php": ">=5.4", - "ratchet/rfc6455": "^0.3", - "react/socket": "^1.0 || ^0.8 || ^0.7" - }, - "require-dev": { - "phpunit/phpunit": "~4.8" - }, - "suggest": { - "reactivex/rxphp": "~2.0" - }, - "default-branch": true, - "type": "library", - "autoload": { - "psr-4": { - "Ratchet\\Client\\": "src" - }, - "files": [ - "src/functions_include.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "Asynchronous WebSocket client", - "keywords": [ - "Ratchet", - "async", - "client", - "websocket", - "websocket client" - ], - "support": { - "issues": "https://github.com/ratchetphp/Pawl/issues", - "source": "https://github.com/ratchetphp/Pawl/tree/v0.3.5" - }, - "time": "2020-07-17T15:32:47+00:00" - }, - { - "name": "ratchet/rfc6455", - "version": "v0.3", - "source": { - "type": "git", - "url": "https://github.com/ratchetphp/RFC6455.git", - "reference": "c8651c7938651c2d55f5d8c2422ac5e57a183341" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/ratchetphp/RFC6455/zipball/c8651c7938651c2d55f5d8c2422ac5e57a183341", - "reference": "c8651c7938651c2d55f5d8c2422ac5e57a183341", - "shasum": "" - }, - "require": { - "guzzlehttp/psr7": "^1.0", - "php": ">=5.4.2" - }, - "require-dev": { - "phpunit/phpunit": "5.7.*", - "react/socket": "^1.3" - }, - "type": "library", - "autoload": { - "psr-4": { - "Ratchet\\RFC6455\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Chris Boden", - "email": "cboden@gmail.com", - "role": "Developer" - }, - { - "name": "Matt Bonneau", - "role": "Developer" - } - ], - "description": "RFC6455 WebSocket protocol handler", - "homepage": "http://socketo.me", - "keywords": [ - "WebSockets", - "rfc6455", - "websocket" - ], - "support": { - "chat": "https://gitter.im/reactphp/reactphp", - "issues": "https://github.com/ratchetphp/RFC6455/issues", - "source": "https://github.com/ratchetphp/RFC6455/tree/v0.3" - }, - "time": "2020-05-15T18:31:24+00:00" - }, - { - "name": "react/cache", - "version": "v1.1.1", - "source": { - "type": "git", - "url": "https://github.com/reactphp/cache.git", - "reference": "4bf736a2cccec7298bdf745db77585966fc2ca7e" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/reactphp/cache/zipball/4bf736a2cccec7298bdf745db77585966fc2ca7e", - "reference": "4bf736a2cccec7298bdf745db77585966fc2ca7e", - "shasum": "" - }, - "require": { - "php": ">=5.3.0", - "react/promise": "^3.0 || ^2.0 || ^1.1" - }, - "require-dev": { - "phpunit/phpunit": "^9.3 || ^5.7 || ^4.8.35" - }, - "type": "library", - "autoload": { - "psr-4": { - "React\\Cache\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Christian Lück", - "email": "christian@clue.engineering", - "homepage": "https://clue.engineering/" - }, - { - "name": "Cees-Jan Kiewiet", - "email": "reactphp@ceesjankiewiet.nl", - "homepage": "https://wyrihaximus.net/" - }, - { - "name": "Jan Sorgalla", - "email": "jsorgalla@gmail.com", - "homepage": "https://sorgalla.com/" - }, - { - "name": "Chris Boden", - "email": "cboden@gmail.com", - "homepage": "https://cboden.dev/" - } - ], - "description": "Async, Promise-based cache interface for ReactPHP", - "keywords": [ - "cache", - "caching", - "promise", - "reactphp" - ], - "support": { - "issues": "https://github.com/reactphp/cache/issues", - "source": "https://github.com/reactphp/cache/tree/v1.1.1" - }, - "funding": [ - { - "url": "https://github.com/WyriHaximus", - "type": "github" - }, - { - "url": "https://github.com/clue", - "type": "github" - } - ], - "time": "2021-02-02T06:47:52+00:00" - }, - { - "name": "react/dns", - "version": "v1.4.0", - "source": { - "type": "git", - "url": "https://github.com/reactphp/dns.git", - "reference": "665260757171e2ab17485b44e7ffffa7acb6ca1f" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/reactphp/dns/zipball/665260757171e2ab17485b44e7ffffa7acb6ca1f", - "reference": "665260757171e2ab17485b44e7ffffa7acb6ca1f", - "shasum": "" - }, - "require": { - "php": ">=5.3.0", - "react/cache": "^1.0 || ^0.6 || ^0.5", - "react/event-loop": "^1.0 || ^0.5", - "react/promise": "^3.0 || ^2.7 || ^1.2.1", - "react/promise-timer": "^1.2" - }, - "require-dev": { - "clue/block-react": "^1.2", - "phpunit/phpunit": "^9.3 || ^4.8.35" - }, - "type": "library", - "autoload": { - "psr-4": { - "React\\Dns\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Christian Lück", - "email": "christian@clue.engineering", - "homepage": "https://clue.engineering/" - }, - { - "name": "Cees-Jan Kiewiet", - "email": "reactphp@ceesjankiewiet.nl", - "homepage": "https://wyrihaximus.net/" - }, - { - "name": "Jan Sorgalla", - "email": "jsorgalla@gmail.com", - "homepage": "https://sorgalla.com/" - }, - { - "name": "Chris Boden", - "email": "cboden@gmail.com", - "homepage": "https://cboden.dev/" - } - ], - "description": "Async DNS resolver for ReactPHP", - "keywords": [ - "async", - "dns", - "dns-resolver", - "reactphp" - ], - "support": { - "issues": "https://github.com/reactphp/dns/issues", - "source": "https://github.com/reactphp/dns/tree/v1.4.0" - }, - "funding": [ - { - "url": "https://github.com/WyriHaximus", - "type": "github" - }, - { - "url": "https://github.com/clue", - "type": "github" - } - ], - "time": "2020-09-18T12:12:55+00:00" - }, - { - "name": "react/event-loop", - "version": "v1.1.1", - "source": { - "type": "git", - "url": "https://github.com/reactphp/event-loop.git", - "reference": "6d24de090cd59cfc830263cfba965be77b563c13" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/reactphp/event-loop/zipball/6d24de090cd59cfc830263cfba965be77b563c13", - "reference": "6d24de090cd59cfc830263cfba965be77b563c13", - "shasum": "" - }, - "require": { - "php": ">=5.3.0" - }, - "require-dev": { - "phpunit/phpunit": "^7.0 || ^6.4 || ^5.7 || ^4.8.35" - }, - "suggest": { - "ext-event": "~1.0 for ExtEventLoop", - "ext-pcntl": "For signal handling support when using the StreamSelectLoop", - "ext-uv": "* for ExtUvLoop" - }, - "type": "library", - "autoload": { - "psr-4": { - "React\\EventLoop\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "ReactPHP's core reactor event loop that libraries can use for evented I/O.", - "keywords": [ - "asynchronous", - "event-loop" - ], - "support": { - "issues": "https://github.com/reactphp/event-loop/issues", - "source": "https://github.com/reactphp/event-loop/tree/v1.1.1" - }, - "time": "2020-01-01T18:39:52+00:00" - }, - { - "name": "react/promise", - "version": "2.x-dev", - "source": { - "type": "git", - "url": "https://github.com/reactphp/promise.git", - "reference": "a9752a861e21c0fe0b380c9f9e55beddc0ed7d31" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/reactphp/promise/zipball/a9752a861e21c0fe0b380c9f9e55beddc0ed7d31", - "reference": "a9752a861e21c0fe0b380c9f9e55beddc0ed7d31", - "shasum": "" - }, - "require": { - "php": ">=5.4.0" - }, - "require-dev": { - "phpunit/phpunit": "^9.3 || ^5.7 || ^4.8.36" - }, - "type": "library", - "autoload": { - "psr-4": { - "React\\Promise\\": "src/" - }, - "files": [ - "src/functions_include.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Jan Sorgalla", - "email": "jsorgalla@gmail.com" - } - ], - "description": "A lightweight implementation of CommonJS Promises/A for PHP", - "keywords": [ - "promise", - "promises" - ], - "support": { - "issues": "https://github.com/reactphp/promise/issues", - "source": "https://github.com/reactphp/promise/tree/2.x" - }, - "funding": [ - { - "url": "https://github.com/WyriHaximus", - "type": "github" - }, - { - "url": "https://github.com/clue", - "type": "github" - } - ], - "time": "2021-02-09T15:06:50+00:00" - }, - { - "name": "react/promise-timer", - "version": "v1.6.0", - "source": { - "type": "git", - "url": "https://github.com/reactphp/promise-timer.git", - "reference": "daee9baf6ef30c43ea4c86399f828bb5f558f6e6" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/reactphp/promise-timer/zipball/daee9baf6ef30c43ea4c86399f828bb5f558f6e6", - "reference": "daee9baf6ef30c43ea4c86399f828bb5f558f6e6", - "shasum": "" - }, - "require": { - "php": ">=5.3", - "react/event-loop": "^1.0 || ^0.5 || ^0.4 || ^0.3.5", - "react/promise": "^3.0 || ^2.7.0 || ^1.2.1" - }, - "require-dev": { - "phpunit/phpunit": "^9.0 || ^5.7 || ^4.8.35" - }, - "type": "library", - "autoload": { - "psr-4": { - "React\\Promise\\Timer\\": "src/" - }, - "files": [ - "src/functions_include.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Christian Lück", - "email": "christian@lueck.tv" - } - ], - "description": "A trivial implementation of timeouts for Promises, built on top of ReactPHP.", - "homepage": "https://github.com/reactphp/promise-timer", - "keywords": [ - "async", - "event-loop", - "promise", - "reactphp", - "timeout", - "timer" - ], - "support": { - "issues": "https://github.com/reactphp/promise-timer/issues", - "source": "https://github.com/reactphp/promise-timer/tree/v1.6.0" - }, - "time": "2020-07-10T12:18:06+00:00" - }, - { - "name": "react/socket", - "version": "v1.6.0", - "source": { - "type": "git", - "url": "https://github.com/reactphp/socket.git", - "reference": "e2b96b23a13ca9b41ab343268dbce3f8ef4d524a" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/reactphp/socket/zipball/e2b96b23a13ca9b41ab343268dbce3f8ef4d524a", - "reference": "e2b96b23a13ca9b41ab343268dbce3f8ef4d524a", - "shasum": "" - }, - "require": { - "evenement/evenement": "^3.0 || ^2.0 || ^1.0", - "php": ">=5.3.0", - "react/dns": "^1.1", - "react/event-loop": "^1.0 || ^0.5", - "react/promise": "^2.6.0 || ^1.2.1", - "react/promise-timer": "^1.4.0", - "react/stream": "^1.1" - }, - "require-dev": { - "clue/block-react": "^1.2", - "phpunit/phpunit": "^9.3 || ^5.7 || ^4.8.35", - "react/promise-stream": "^1.2" - }, - "type": "library", - "autoload": { - "psr-4": { - "React\\Socket\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Christian Lück", - "email": "christian@clue.engineering", - "homepage": "https://clue.engineering/" - }, - { - "name": "Cees-Jan Kiewiet", - "email": "reactphp@ceesjankiewiet.nl", - "homepage": "https://wyrihaximus.net/" - }, - { - "name": "Jan Sorgalla", - "email": "jsorgalla@gmail.com", - "homepage": "https://sorgalla.com/" - }, - { - "name": "Chris Boden", - "email": "cboden@gmail.com", - "homepage": "https://cboden.dev/" - } - ], - "description": "Async, streaming plaintext TCP/IP and secure TLS socket server and client connections for ReactPHP", - "keywords": [ - "Connection", - "Socket", - "async", - "reactphp", - "stream" - ], - "support": { - "issues": "https://github.com/reactphp/socket/issues", - "source": "https://github.com/reactphp/socket/tree/v1.6.0" - }, - "funding": [ - { - "url": "https://github.com/WyriHaximus", - "type": "github" - }, - { - "url": "https://github.com/clue", - "type": "github" - } - ], - "time": "2020-08-28T12:49:05+00:00" - }, - { - "name": "react/stream", - "version": "v1.1.1", - "source": { - "type": "git", - "url": "https://github.com/reactphp/stream.git", - "reference": "7c02b510ee3f582c810aeccd3a197b9c2f52ff1a" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/reactphp/stream/zipball/7c02b510ee3f582c810aeccd3a197b9c2f52ff1a", - "reference": "7c02b510ee3f582c810aeccd3a197b9c2f52ff1a", - "shasum": "" - }, - "require": { - "evenement/evenement": "^3.0 || ^2.0 || ^1.0", - "php": ">=5.3.8", - "react/event-loop": "^1.0 || ^0.5 || ^0.4 || ^0.3.5" - }, - "require-dev": { - "clue/stream-filter": "~1.2", - "phpunit/phpunit": "^7.0 || ^6.4 || ^5.7 || ^4.8.35" - }, - "type": "library", - "autoload": { - "psr-4": { - "React\\Stream\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "Event-driven readable and writable streams for non-blocking I/O in ReactPHP", - "keywords": [ - "event-driven", - "io", - "non-blocking", - "pipe", - "reactphp", - "readable", - "stream", - "writable" - ], - "support": { - "issues": "https://github.com/reactphp/stream/issues", - "source": "https://github.com/reactphp/stream/tree/v1.1.1" - }, - "time": "2020-05-04T10:17:57+00:00" - }, { "name": "resque/php-resque", "version": "v1.3.6", @@ -3058,6 +2447,53 @@ ], "time": "2020-11-10T19:05:51+00:00" }, + { + "name": "evenement/evenement", + "version": "v3.0.1", + "source": { + "type": "git", + "url": "https://github.com/igorw/evenement.git", + "reference": "531bfb9d15f8aa57454f5f0285b18bec903b8fb7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/igorw/evenement/zipball/531bfb9d15f8aa57454f5f0285b18bec903b8fb7", + "reference": "531bfb9d15f8aa57454f5f0285b18bec903b8fb7", + "shasum": "" + }, + "require": { + "php": ">=7.0" + }, + "require-dev": { + "phpunit/phpunit": "^6.0" + }, + "type": "library", + "autoload": { + "psr-0": { + "Evenement": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Igor Wiedler", + "email": "igor@wiedler.ch" + } + ], + "description": "Événement is a very simple event dispatching library for PHP", + "keywords": [ + "event-dispatcher", + "event-emitter" + ], + "support": { + "issues": "https://github.com/igorw/evenement/issues", + "source": "https://github.com/igorw/evenement/tree/master" + }, + "time": "2017-07-23T21:35:13+00:00" + }, { "name": "felixfbecker/advanced-json-rpc", "version": "v3.2.0", @@ -3109,12 +2545,12 @@ "source": { "type": "git", "url": "https://github.com/felixfbecker/php-language-server-protocol.git", - "reference": "85e83cacd2ed573238678c6875f8f0d7ec699541" + "reference": "9d846d1f5cf101deee7a61c8ba7caa0a975cd730" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/felixfbecker/php-language-server-protocol/zipball/85e83cacd2ed573238678c6875f8f0d7ec699541", - "reference": "85e83cacd2ed573238678c6875f8f0d7ec699541", + "url": "https://api.github.com/repos/felixfbecker/php-language-server-protocol/zipball/9d846d1f5cf101deee7a61c8ba7caa0a975cd730", + "reference": "9d846d1f5cf101deee7a61c8ba7caa0a975cd730", "shasum": "" }, "require": { @@ -3573,16 +3009,16 @@ }, { "name": "phar-io/version", - "version": "3.0.4", + "version": "3.1.0", "source": { "type": "git", "url": "https://github.com/phar-io/version.git", - "reference": "e4782611070e50613683d2b9a57730e9a3ba5451" + "reference": "bae7c545bef187884426f042434e561ab1ddb182" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phar-io/version/zipball/e4782611070e50613683d2b9a57730e9a3ba5451", - "reference": "e4782611070e50613683d2b9a57730e9a3ba5451", + "url": "https://api.github.com/repos/phar-io/version/zipball/bae7c545bef187884426f042434e561ab1ddb182", + "reference": "bae7c545bef187884426f042434e561ab1ddb182", "shasum": "" }, "require": { @@ -3618,9 +3054,9 @@ "description": "Library for handling version information and constraints", "support": { "issues": "https://github.com/phar-io/version/issues", - "source": "https://github.com/phar-io/version/tree/3.0.4" + "source": "https://github.com/phar-io/version/tree/3.1.0" }, - "time": "2020-12-13T23:18:30+00:00" + "time": "2021-02-23T14:00:09+00:00" }, { "name": "phpdocumentor/reflection-common", @@ -3933,12 +3369,12 @@ "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-file-iterator.git", - "reference": "05fa32de35b15c94838d22482cc59d99860a706f" + "reference": "dae425925709122f7584cadeeb838edcaa491bb1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/05fa32de35b15c94838d22482cc59d99860a706f", - "reference": "05fa32de35b15c94838d22482cc59d99860a706f", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/dae425925709122f7584cadeeb838edcaa491bb1", + "reference": "dae425925709122f7584cadeeb838edcaa491bb1", "shasum": "" }, "require": { @@ -3994,12 +3430,12 @@ "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-invoker.git", - "reference": "7bba8d62fc6140730c268d5ff7fbf9c3a54996a8" + "reference": "5ad9e5f5d6ee1a837e1d50bab1017e0daf423b40" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/7bba8d62fc6140730c268d5ff7fbf9c3a54996a8", - "reference": "7bba8d62fc6140730c268d5ff7fbf9c3a54996a8", + "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/5ad9e5f5d6ee1a837e1d50bab1017e0daf423b40", + "reference": "5ad9e5f5d6ee1a837e1d50bab1017e0daf423b40", "shasum": "" }, "require": { @@ -4058,12 +3494,12 @@ "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-text-template.git", - "reference": "bca9f27936ccd6d7450f16f1ee3f125b755b7905" + "reference": "4ec5a2ac79a19b35d0cf83cce30604f77743067a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/bca9f27936ccd6d7450f16f1ee3f125b755b7905", - "reference": "bca9f27936ccd6d7450f16f1ee3f125b755b7905", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/4ec5a2ac79a19b35d0cf83cce30604f77743067a", + "reference": "4ec5a2ac79a19b35d0cf83cce30604f77743067a", "shasum": "" }, "require": { @@ -4118,12 +3554,12 @@ "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-timer.git", - "reference": "e3125d0dc516e7f7ab23d54ddefbce67627fd608" + "reference": "705821b0927b5e69e9e016c84de68dc6195c71b9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/e3125d0dc516e7f7ab23d54ddefbce67627fd608", - "reference": "e3125d0dc516e7f7ab23d54ddefbce67627fd608", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/705821b0927b5e69e9e016c84de68dc6195c71b9", + "reference": "705821b0927b5e69e9e016c84de68dc6195c71b9", "shasum": "" }, "require": { @@ -4329,18 +3765,581 @@ }, "time": "2020-10-13T07:07:53+00:00" }, + { + "name": "ratchet/pawl", + "version": "v0.3.5", + "source": { + "type": "git", + "url": "https://github.com/ratchetphp/Pawl.git", + "reference": "89ec703c76dc893484a2a0ed44b48a37d445abd5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ratchetphp/Pawl/zipball/89ec703c76dc893484a2a0ed44b48a37d445abd5", + "reference": "89ec703c76dc893484a2a0ed44b48a37d445abd5", + "shasum": "" + }, + "require": { + "evenement/evenement": "^3.0 || ^2.0", + "php": ">=5.4", + "ratchet/rfc6455": "^0.3", + "react/socket": "^1.0 || ^0.8 || ^0.7" + }, + "require-dev": { + "phpunit/phpunit": "~4.8" + }, + "suggest": { + "reactivex/rxphp": "~2.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Ratchet\\Client\\": "src" + }, + "files": [ + "src/functions_include.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Asynchronous WebSocket client", + "keywords": [ + "Ratchet", + "async", + "client", + "websocket", + "websocket client" + ], + "support": { + "issues": "https://github.com/ratchetphp/Pawl/issues", + "source": "https://github.com/ratchetphp/Pawl/tree/master" + }, + "time": "2020-07-17T15:32:47+00:00" + }, + { + "name": "ratchet/rfc6455", + "version": "v0.3", + "source": { + "type": "git", + "url": "https://github.com/ratchetphp/RFC6455.git", + "reference": "c8651c7938651c2d55f5d8c2422ac5e57a183341" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ratchetphp/RFC6455/zipball/c8651c7938651c2d55f5d8c2422ac5e57a183341", + "reference": "c8651c7938651c2d55f5d8c2422ac5e57a183341", + "shasum": "" + }, + "require": { + "guzzlehttp/psr7": "^1.0", + "php": ">=5.4.2" + }, + "require-dev": { + "phpunit/phpunit": "5.7.*", + "react/socket": "^1.3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Ratchet\\RFC6455\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "role": "Developer" + }, + { + "name": "Matt Bonneau", + "role": "Developer" + } + ], + "description": "RFC6455 WebSocket protocol handler", + "homepage": "http://socketo.me", + "keywords": [ + "WebSockets", + "rfc6455", + "websocket" + ], + "support": { + "chat": "https://gitter.im/reactphp/reactphp", + "issues": "https://github.com/ratchetphp/RFC6455/issues", + "source": "https://github.com/ratchetphp/RFC6455/tree/v0.3" + }, + "time": "2020-05-15T18:31:24+00:00" + }, + { + "name": "react/cache", + "version": "v1.1.1", + "source": { + "type": "git", + "url": "https://github.com/reactphp/cache.git", + "reference": "4bf736a2cccec7298bdf745db77585966fc2ca7e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/cache/zipball/4bf736a2cccec7298bdf745db77585966fc2ca7e", + "reference": "4bf736a2cccec7298bdf745db77585966fc2ca7e", + "shasum": "" + }, + "require": { + "php": ">=5.3.0", + "react/promise": "^3.0 || ^2.0 || ^1.1" + }, + "require-dev": { + "phpunit/phpunit": "^9.3 || ^5.7 || ^4.8.35" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\Cache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "Async, Promise-based cache interface for ReactPHP", + "keywords": [ + "cache", + "caching", + "promise", + "reactphp" + ], + "support": { + "issues": "https://github.com/reactphp/cache/issues", + "source": "https://github.com/reactphp/cache/tree/v1.1.1" + }, + "funding": [ + { + "url": "https://github.com/WyriHaximus", + "type": "github" + }, + { + "url": "https://github.com/clue", + "type": "github" + } + ], + "time": "2021-02-02T06:47:52+00:00" + }, + { + "name": "react/dns", + "version": "v1.4.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/dns.git", + "reference": "665260757171e2ab17485b44e7ffffa7acb6ca1f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/dns/zipball/665260757171e2ab17485b44e7ffffa7acb6ca1f", + "reference": "665260757171e2ab17485b44e7ffffa7acb6ca1f", + "shasum": "" + }, + "require": { + "php": ">=5.3.0", + "react/cache": "^1.0 || ^0.6 || ^0.5", + "react/event-loop": "^1.0 || ^0.5", + "react/promise": "^3.0 || ^2.7 || ^1.2.1", + "react/promise-timer": "^1.2" + }, + "require-dev": { + "clue/block-react": "^1.2", + "phpunit/phpunit": "^9.3 || ^4.8.35" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\Dns\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "Async DNS resolver for ReactPHP", + "keywords": [ + "async", + "dns", + "dns-resolver", + "reactphp" + ], + "support": { + "issues": "https://github.com/reactphp/dns/issues", + "source": "https://github.com/reactphp/dns/tree/v1.4.0" + }, + "funding": [ + { + "url": "https://github.com/WyriHaximus", + "type": "github" + }, + { + "url": "https://github.com/clue", + "type": "github" + } + ], + "time": "2020-09-18T12:12:55+00:00" + }, + { + "name": "react/event-loop", + "version": "v1.1.1", + "source": { + "type": "git", + "url": "https://github.com/reactphp/event-loop.git", + "reference": "6d24de090cd59cfc830263cfba965be77b563c13" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/event-loop/zipball/6d24de090cd59cfc830263cfba965be77b563c13", + "reference": "6d24de090cd59cfc830263cfba965be77b563c13", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "require-dev": { + "phpunit/phpunit": "^7.0 || ^6.4 || ^5.7 || ^4.8.35" + }, + "suggest": { + "ext-event": "~1.0 for ExtEventLoop", + "ext-pcntl": "For signal handling support when using the StreamSelectLoop", + "ext-uv": "* for ExtUvLoop" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\EventLoop\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "ReactPHP's core reactor event loop that libraries can use for evented I/O.", + "keywords": [ + "asynchronous", + "event-loop" + ], + "support": { + "issues": "https://github.com/reactphp/event-loop/issues", + "source": "https://github.com/reactphp/event-loop/tree/v1.1.1" + }, + "time": "2020-01-01T18:39:52+00:00" + }, + { + "name": "react/promise", + "version": "2.x-dev", + "source": { + "type": "git", + "url": "https://github.com/reactphp/promise.git", + "reference": "a9752a861e21c0fe0b380c9f9e55beddc0ed7d31" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/promise/zipball/a9752a861e21c0fe0b380c9f9e55beddc0ed7d31", + "reference": "a9752a861e21c0fe0b380c9f9e55beddc0ed7d31", + "shasum": "" + }, + "require": { + "php": ">=5.4.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.3 || ^5.7 || ^4.8.36" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\Promise\\": "src/" + }, + "files": [ + "src/functions_include.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com" + } + ], + "description": "A lightweight implementation of CommonJS Promises/A for PHP", + "keywords": [ + "promise", + "promises" + ], + "support": { + "issues": "https://github.com/reactphp/promise/issues", + "source": "https://github.com/reactphp/promise/tree/2.x" + }, + "funding": [ + { + "url": "https://github.com/WyriHaximus", + "type": "github" + }, + { + "url": "https://github.com/clue", + "type": "github" + } + ], + "time": "2021-02-09T15:06:50+00:00" + }, + { + "name": "react/promise-timer", + "version": "v1.6.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/promise-timer.git", + "reference": "daee9baf6ef30c43ea4c86399f828bb5f558f6e6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/promise-timer/zipball/daee9baf6ef30c43ea4c86399f828bb5f558f6e6", + "reference": "daee9baf6ef30c43ea4c86399f828bb5f558f6e6", + "shasum": "" + }, + "require": { + "php": ">=5.3", + "react/event-loop": "^1.0 || ^0.5 || ^0.4 || ^0.3.5", + "react/promise": "^3.0 || ^2.7.0 || ^1.2.1" + }, + "require-dev": { + "phpunit/phpunit": "^9.0 || ^5.7 || ^4.8.35" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\Promise\\Timer\\": "src/" + }, + "files": [ + "src/functions_include.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@lueck.tv" + } + ], + "description": "A trivial implementation of timeouts for Promises, built on top of ReactPHP.", + "homepage": "https://github.com/reactphp/promise-timer", + "keywords": [ + "async", + "event-loop", + "promise", + "reactphp", + "timeout", + "timer" + ], + "support": { + "issues": "https://github.com/reactphp/promise-timer/issues", + "source": "https://github.com/reactphp/promise-timer/tree/v1.6.0" + }, + "time": "2020-07-10T12:18:06+00:00" + }, + { + "name": "react/socket", + "version": "v1.6.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/socket.git", + "reference": "e2b96b23a13ca9b41ab343268dbce3f8ef4d524a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/socket/zipball/e2b96b23a13ca9b41ab343268dbce3f8ef4d524a", + "reference": "e2b96b23a13ca9b41ab343268dbce3f8ef4d524a", + "shasum": "" + }, + "require": { + "evenement/evenement": "^3.0 || ^2.0 || ^1.0", + "php": ">=5.3.0", + "react/dns": "^1.1", + "react/event-loop": "^1.0 || ^0.5", + "react/promise": "^2.6.0 || ^1.2.1", + "react/promise-timer": "^1.4.0", + "react/stream": "^1.1" + }, + "require-dev": { + "clue/block-react": "^1.2", + "phpunit/phpunit": "^9.3 || ^5.7 || ^4.8.35", + "react/promise-stream": "^1.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\Socket\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "Async, streaming plaintext TCP/IP and secure TLS socket server and client connections for ReactPHP", + "keywords": [ + "Connection", + "Socket", + "async", + "reactphp", + "stream" + ], + "support": { + "issues": "https://github.com/reactphp/socket/issues", + "source": "https://github.com/reactphp/socket/tree/v1.6.0" + }, + "funding": [ + { + "url": "https://github.com/WyriHaximus", + "type": "github" + }, + { + "url": "https://github.com/clue", + "type": "github" + } + ], + "time": "2020-08-28T12:49:05+00:00" + }, + { + "name": "react/stream", + "version": "v1.1.1", + "source": { + "type": "git", + "url": "https://github.com/reactphp/stream.git", + "reference": "7c02b510ee3f582c810aeccd3a197b9c2f52ff1a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/stream/zipball/7c02b510ee3f582c810aeccd3a197b9c2f52ff1a", + "reference": "7c02b510ee3f582c810aeccd3a197b9c2f52ff1a", + "shasum": "" + }, + "require": { + "evenement/evenement": "^3.0 || ^2.0 || ^1.0", + "php": ">=5.3.8", + "react/event-loop": "^1.0 || ^0.5 || ^0.4 || ^0.3.5" + }, + "require-dev": { + "clue/stream-filter": "~1.2", + "phpunit/phpunit": "^7.0 || ^6.4 || ^5.7 || ^4.8.35" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\Stream\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Event-driven readable and writable streams for non-blocking I/O in ReactPHP", + "keywords": [ + "event-driven", + "io", + "non-blocking", + "pipe", + "reactphp", + "readable", + "stream", + "writable" + ], + "support": { + "issues": "https://github.com/reactphp/stream/issues", + "source": "https://github.com/reactphp/stream/tree/v1.1.1" + }, + "time": "2020-05-04T10:17:57+00:00" + }, { "name": "sebastian/cli-parser", "version": "dev-master", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/cli-parser.git", - "reference": "5a6fc83d266e0fcbf890d4475bfbb713dbb4d202" + "reference": "3a42d843af4d27ca1155e1d926881af162733655" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/5a6fc83d266e0fcbf890d4475bfbb713dbb4d202", - "reference": "5a6fc83d266e0fcbf890d4475bfbb713dbb4d202", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/3a42d843af4d27ca1155e1d926881af162733655", + "reference": "3a42d843af4d27ca1155e1d926881af162733655", "shasum": "" }, "require": { @@ -4448,12 +4447,12 @@ "source": { "type": "git", "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", - "reference": "96fc758350a824cf96f9e7847ecdf9bb82c87083" + "reference": "5f5db0b35f586eb5bca0581a10bb42dd56575986" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/96fc758350a824cf96f9e7847ecdf9bb82c87083", - "reference": "96fc758350a824cf96f9e7847ecdf9bb82c87083", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/5f5db0b35f586eb5bca0581a10bb42dd56575986", + "reference": "5f5db0b35f586eb5bca0581a10bb42dd56575986", "shasum": "" }, "require": { @@ -4504,12 +4503,12 @@ "source": { "type": "git", "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "3b943ec66244e5d0a5252708d1c9073ae6d3efc9" + "reference": "dbc5fb421f242a5749845dc8dd0dc8cde2979dd9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/3b943ec66244e5d0a5252708d1c9073ae6d3efc9", - "reference": "3b943ec66244e5d0a5252708d1c9073ae6d3efc9", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/dbc5fb421f242a5749845dc8dd0dc8cde2979dd9", + "reference": "dbc5fb421f242a5749845dc8dd0dc8cde2979dd9", "shasum": "" }, "require": { @@ -4636,12 +4635,12 @@ "source": { "type": "git", "url": "https://github.com/sebastianbergmann/diff.git", - "reference": "1895a1a29e197f7d31099a320b2a3ae9e428b21d" + "reference": "93e6aa13f3dc5f8327e7fb9756e9655fc4c23e90" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/1895a1a29e197f7d31099a320b2a3ae9e428b21d", - "reference": "1895a1a29e197f7d31099a320b2a3ae9e428b21d", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/93e6aa13f3dc5f8327e7fb9756e9655fc4c23e90", + "reference": "93e6aa13f3dc5f8327e7fb9756e9655fc4c23e90", "shasum": "" }, "require": { @@ -4703,12 +4702,12 @@ "source": { "type": "git", "url": "https://github.com/sebastianbergmann/environment.git", - "reference": "7f8f2720df4d03d4368edadac24c3a7950b6cdc5" + "reference": "6e1743b808be9cfd33a716583ccb94b7d4d32e94" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/7f8f2720df4d03d4368edadac24c3a7950b6cdc5", - "reference": "7f8f2720df4d03d4368edadac24c3a7950b6cdc5", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/6e1743b808be9cfd33a716583ccb94b7d4d32e94", + "reference": "6e1743b808be9cfd33a716583ccb94b7d4d32e94", "shasum": "" }, "require": { @@ -4767,12 +4766,12 @@ "source": { "type": "git", "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "c6819d6edff3496f28c29a9ed61c564a9fdae27b" + "reference": "eca7281ab29075df68b113a37a83be616b629b12" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/c6819d6edff3496f28c29a9ed61c564a9fdae27b", - "reference": "c6819d6edff3496f28c29a9ed61c564a9fdae27b", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/eca7281ab29075df68b113a37a83be616b629b12", + "reference": "eca7281ab29075df68b113a37a83be616b629b12", "shasum": "" }, "require": { @@ -4845,12 +4844,12 @@ "source": { "type": "git", "url": "https://github.com/sebastianbergmann/global-state.git", - "reference": "a912746c9e31610f52b8e6977107e745c758cfd8" + "reference": "0ac702e6d13725242edb9b294c5d20b92fcfb8b4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/a912746c9e31610f52b8e6977107e745c758cfd8", - "reference": "a912746c9e31610f52b8e6977107e745c758cfd8", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/0ac702e6d13725242edb9b294c5d20b92fcfb8b4", + "reference": "0ac702e6d13725242edb9b294c5d20b92fcfb8b4", "shasum": "" }, "require": { @@ -4967,12 +4966,12 @@ "source": { "type": "git", "url": "https://github.com/sebastianbergmann/object-enumerator.git", - "reference": "79f258bf9b9f9f1aff7ec27fa3e0d5d7ef344088" + "reference": "8cc80b4bda00a4c5997c3fc597a34872f3a1007d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/79f258bf9b9f9f1aff7ec27fa3e0d5d7ef344088", - "reference": "79f258bf9b9f9f1aff7ec27fa3e0d5d7ef344088", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/8cc80b4bda00a4c5997c3fc597a34872f3a1007d", + "reference": "8cc80b4bda00a4c5997c3fc597a34872f3a1007d", "shasum": "" }, "require": { @@ -5025,12 +5024,12 @@ "source": { "type": "git", "url": "https://github.com/sebastianbergmann/object-reflector.git", - "reference": "232add5a51167e359e1dd03334ebffaddfb95795" + "reference": "1d33587c2c3e636936f895e103a9e82dd8102a8e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/232add5a51167e359e1dd03334ebffaddfb95795", - "reference": "232add5a51167e359e1dd03334ebffaddfb95795", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/1d33587c2c3e636936f895e103a9e82dd8102a8e", + "reference": "1d33587c2c3e636936f895e103a9e82dd8102a8e", "shasum": "" }, "require": { @@ -5081,12 +5080,12 @@ "source": { "type": "git", "url": "https://github.com/sebastianbergmann/recursion-context.git", - "reference": "d6cde15be46e8e5cc8671ceb41b63b69dfd7bd5a" + "reference": "43f58a51e8f853aadb228ba818d2be388af7237b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/d6cde15be46e8e5cc8671ceb41b63b69dfd7bd5a", - "reference": "d6cde15be46e8e5cc8671ceb41b63b69dfd7bd5a", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/43f58a51e8f853aadb228ba818d2be388af7237b", + "reference": "43f58a51e8f853aadb228ba818d2be388af7237b", "shasum": "" }, "require": { @@ -5201,12 +5200,12 @@ "source": { "type": "git", "url": "https://github.com/sebastianbergmann/type.git", - "reference": "8abc9c1947c9f928da999be28778a0ba48cdf5b4" + "reference": "557863473c1de00e165a288d5b547f1f83652e7e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/8abc9c1947c9f928da999be28778a0ba48cdf5b4", - "reference": "8abc9c1947c9f928da999be28778a0ba48cdf5b4", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/557863473c1de00e165a288d5b547f1f83652e7e", + "reference": "557863473c1de00e165a288d5b547f1f83652e7e", "shasum": "" }, "require": { @@ -5349,12 +5348,12 @@ "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "2a6f75224a537ee506e9fa1e6fc4200ad411ffd9" + "reference": "c08d7d0d458eceb62996d81d3be8d9fbf5564ec4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/2a6f75224a537ee506e9fa1e6fc4200ad411ffd9", - "reference": "2a6f75224a537ee506e9fa1e6fc4200ad411ffd9", + "url": "https://api.github.com/repos/symfony/console/zipball/c08d7d0d458eceb62996d81d3be8d9fbf5564ec4", + "reference": "c08d7d0d458eceb62996d81d3be8d9fbf5564ec4", "shasum": "" }, "require": { @@ -5939,12 +5938,12 @@ "source": { "type": "git", "url": "https://github.com/symfony/service-contracts.git", - "reference": "bf99754c6182a126968b1c2709d18548489f27eb" + "reference": "e830e6ceebd6377b019e4c9a523d6f2c27007e4a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/bf99754c6182a126968b1c2709d18548489f27eb", - "reference": "bf99754c6182a126968b1c2709d18548489f27eb", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/e830e6ceebd6377b019e4c9a523d6f2c27007e4a", + "reference": "e830e6ceebd6377b019e4c9a523d6f2c27007e4a", "shasum": "" }, "require": { @@ -6097,6 +6096,55 @@ ], "time": "2021-02-17T15:27:35+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.0", @@ -6153,12 +6201,12 @@ "source": { "type": "git", "url": "https://github.com/twigphp/Twig.git", - "reference": "429f90a02d3bd4a06787ac9bc48c56c4320b58a0" + "reference": "728c611e8643a5dd44839ffa791e21763b04a694" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/twigphp/Twig/zipball/429f90a02d3bd4a06787ac9bc48c56c4320b58a0", - "reference": "429f90a02d3bd4a06787ac9bc48c56c4320b58a0", + "url": "https://api.github.com/repos/twigphp/Twig/zipball/728c611e8643a5dd44839ffa791e21763b04a694", + "reference": "728c611e8643a5dd44839ffa791e21763b04a694", "shasum": "" }, "require": { @@ -6337,12 +6385,12 @@ "source": { "type": "git", "url": "https://github.com/webmozarts/assert.git", - "reference": "9c89b265ccc4092d58e66d72af5d343ee77a41ae" + "reference": "4631e2c7d2d7132adac9fd84d4c1a98c10a6e049" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/webmozarts/assert/zipball/9c89b265ccc4092d58e66d72af5d343ee77a41ae", - "reference": "9c89b265ccc4092d58e66d72af5d343ee77a41ae", + "url": "https://api.github.com/repos/webmozarts/assert/zipball/4631e2c7d2d7132adac9fd84d4c1a98c10a6e049", + "reference": "4631e2c7d2d7132adac9fd84d4c1a98c10a6e049", "shasum": "" }, "require": { diff --git a/tests/e2e/Services/Realtime/RealtimeBase.php b/tests/e2e/Services/Realtime/RealtimeBase.php index 2c0c3b843c..7bd6de4e1d 100644 --- a/tests/e2e/Services/Realtime/RealtimeBase.php +++ b/tests/e2e/Services/Realtime/RealtimeBase.php @@ -2,43 +2,7 @@ namespace Tests\E2E\Services\Realtime; -use Ratchet; -use Ratchet\RFC6455\Messaging\MessageInterface; - trait RealtimeBase { - private function getWebsocket($channels = []) { - $query = [ - 'project' => $this->getProject()['$id'], - 'channels' => $channels - ]; - return 'ws://appwrite-traefik/v1/realtime?' . http_build_query($query); - } - public function testHandshake() - { - /** - * Test for SUCCESS - */ - Ratchet\Client\connect($this->getWebsocket(['documents']), [], ['origin' => 'appwrite.test'])->then(function($conn) { - $conn->on('message', function(MessageInterface $msg) use ($conn) { - $this->assertEquals('{"documents":0}', $msg->__toString()); - $conn->close(); - }); - }, function ($e) { - echo "Could not connect: {$e->getMessage()}\n"; - }); - - /** - * Test for FAILURE - */ - Ratchet\Client\connect($this->getWebsocket(['account']), [], ['origin' => 'appwrite.test'])->then(function($conn) { - $conn->on('message', function(Message $msg) use ($conn) { - $this->assertEquals('Missing channels', $msg->__toString()); - $conn->close(); - }); - }, function ($e) { - echo "Could not connect: {$e->getMessage()}\n"; - }); - } -} \ No newline at end of file +} diff --git a/tests/e2e/Services/Realtime/RealtimeCustomClientTest.php b/tests/e2e/Services/Realtime/RealtimeCustomClientTest.php index 850bb3d505..e45c0b64bd 100644 --- a/tests/e2e/Services/Realtime/RealtimeCustomClientTest.php +++ b/tests/e2e/Services/Realtime/RealtimeCustomClientTest.php @@ -7,9 +7,57 @@ use Tests\E2E\Scopes\Scope; use Tests\E2E\Scopes\ProjectCustom; use Tests\E2E\Scopes\SideClient; +use WebSocket\Client as WebSocketClient; +use WebSocket\ConnectionException; + class RealtimeCustomClientTest extends Scope { use RealtimeBase; use ProjectCustom; use SideClient; + + private function getWebsocket($channels = [], $headers = []) + { + $headers = array_merge( + ['Origin' => 'http://appwrite.test'], + $headers + ); + $query = [ + 'project' => $this->getProject()['$id'], + 'channels' => $channels + ]; + return new WebSocketClient('ws://appwrite-traefik/v1/realtime?' . http_build_query($query), [ + 'headers' => $headers, + 'timeout' => 5, + ]); + } + + public function testConnection() + { + /** + * Test for SUCCESS + */ + $client = $this->getWebsocket(['documents']); + $this->assertEquals('{"documents":0}', $client->receive()); + $client->close(); + + $client = $this->getWebsocket(['documents'], ['Origin' => 'http://appwrite.unknown']); + $this->assertEquals('Invalid Origin. Register your new client (appwrite.unknown) as a new Web platform on your project console dashboard', $client->receive()); + $this->expectException(ConnectionException::class); // Check if server disconnnected client + $client->close(); + + $client = $this->getWebsocket(); + $this->assertEquals('Missing channels', $client->receive()); + $this->expectException(ConnectionException::class); // Check if server disconnnected client + $client->close(); + + $client = new WebSocketClient('ws://appwrite-traefik/v1/realtime', [ + 'headers' => [ + 'Origin' => 'appwrite.test' + ] + ]); + $this->assertEquals('Missing or unknown project ID', $client->receive()); + $this->expectException(ConnectionException::class); // Check if server disconnnected client + $client->close(); + } } \ No newline at end of file From 62857ccfaf1ad11ab4a7e743d2225fc39e45958e Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Tue, 2 Mar 2021 19:01:34 +0100 Subject: [PATCH 047/267] add account e2e tests --- tests/e2e/Services/Realtime/RealtimeBase.php | 375 ++++++++++++++++++ .../Realtime/RealtimeCustomClientTest.php | 45 --- 2 files changed, 375 insertions(+), 45 deletions(-) diff --git a/tests/e2e/Services/Realtime/RealtimeBase.php b/tests/e2e/Services/Realtime/RealtimeBase.php index 7bd6de4e1d..3449e89416 100644 --- a/tests/e2e/Services/Realtime/RealtimeBase.php +++ b/tests/e2e/Services/Realtime/RealtimeBase.php @@ -2,7 +2,382 @@ namespace Tests\E2E\Services\Realtime; +use Tests\E2E\Client; +use WebSocket\Client as WebSocketClient; +use WebSocket\ConnectionException; + trait RealtimeBase { + private function getWebsocket($channels = [], $headers = []) + { + $headers = array_merge([ + 'Origin' => 'appwrite.test' + ], $headers); + + $query = [ + 'project' => $this->getProject()['$id'], + 'channels' => $channels + ]; + return new WebSocketClient('ws://appwrite-traefik/v1/realtime?' . http_build_query($query), [ + 'headers' => $headers, + 'timeout' => 5, + ]); + } + + public function testConnection() + { + /** + * Test for SUCCESS + */ + $client = $this->getWebsocket(['documents']); + $this->assertEquals(json_encode([ + 'documents' => 0 + ]), $client->receive()); + $client->close(); + + /** + * Test for FAILURE + */ + $client = $this->getWebsocket(['documents'], ['origin' => 'http://appwrite.unknown']); + $this->assertEquals('Invalid Origin. Register your new client (appwrite.unknown) as a new Web platform on your project console dashboard', $client->receive()); + $this->expectException(ConnectionException::class); // Check if server disconnnected client + $client->close(); + + $client = $this->getWebsocket(); + $this->assertEquals('Missing channels', $client->receive()); + $this->expectException(ConnectionException::class); // Check if server disconnnected client + $client->close(); + + $client = new WebSocketClient('ws://appwrite-traefik/v1/realtime', [ + 'headers' => [ + 'Origin' => 'appwrite.test' + ] + ]); + $this->assertEquals('Missing or unknown project ID', $client->receive()); + $this->expectException(ConnectionException::class); // Check if server disconnnected client + $client->close(); + + $client = new WebSocketClient('ws://appwrite-traefik/v1/realtime?project=123', [ + 'headers' => [ + 'Origin' => 'appwrite.test' + ] + ]); + $this->assertEquals('Missing or unknown project ID', $client->receive()); + $this->expectException(ConnectionException::class); // Check if server disconnnected client + $client->close(); + } + + public function testChannelParsing() + { + $user = $this->getUser(); + $userId = $user['$id'] ?? ''; + $session = $user['session'] ?? ''; + $headers = [ + 'origin' => 'http://localhost', + 'cookie' => 'a_session_'.$this->getProject()['$id'].'=' . $session + ]; + + /** + * Test for SUCCESS + */ + $client = $this->getWebsocket(['documents']); + $response = json_decode($client->receive(), true); + $this->assertCount(1, $response); + $this->assertArrayHasKey('documents', $response); + $client->close(); + + $client = $this->getWebsocket(['account'], $headers); + $response = json_decode($client->receive(), true); + $this->assertCount(1, $response); + $this->assertArrayHasKey('account.' . $userId, $response); + $client->close(); + + $client = $this->getWebsocket(['account', 'documents', 'account.123'], $headers); + $response = json_decode($client->receive(), true); + $this->assertCount(2, $response); + $this->assertArrayHasKey('documents', $response); + $this->assertArrayHasKey('account.' . $userId, $response); + $client->close(); + + $client = $this->getWebsocket([ + 'account', + 'files', + 'files.1', + 'collections', + 'collections.1', + 'collections.1.documents', + 'collections.2', + 'collections.2.documents', + 'documents', + 'documents.1', + 'documents.2', + ], $headers); + $response = json_decode($client->receive(), true); + + $this->assertCount(11, $response); + $this->assertArrayHasKey('account.' . $userId, $response); + $this->assertArrayHasKey('files', $response); + $this->assertArrayHasKey('files.1', $response); + $this->assertArrayHasKey('collections', $response); + $this->assertArrayHasKey('collections.1', $response); + $this->assertArrayHasKey('collections.1.documents', $response); + $this->assertArrayHasKey('collections.2', $response); + $this->assertArrayHasKey('collections.2.documents', $response); + $this->assertArrayHasKey('documents', $response); + $this->assertArrayHasKey('documents.1', $response); + $this->assertArrayHasKey('documents.2', $response); + $client->close(); + } + + public function testChannelAccount() + { + $user = $this->getUser(); + $userId = $user['$id'] ?? ''; + $session = $user['session'] ?? ''; + $projectId = $this->getProject()['$id']; + + $client = $this->getWebsocket(['account'], [ + 'origin' => 'http://localhost', + 'cookie' => 'a_session_'.$projectId.'=' . $session + ]); + $response = json_decode($client->receive(), true); + $this->assertCount(1, $response); + $this->assertArrayHasKey('account.' . $userId, $response); + + /** + * Test Account Name Event + */ + $name = "Torsten Dittmann"; + + $this->client->call(Client::METHOD_PATCH, '/account/name', array_merge([ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'x-appwrite-project' => $projectId, + 'cookie' => 'a_session_' . $projectId . '=' . $session, + ]), [ + 'name' => $name + ]); + + $response = json_decode($client->receive(), true); + + $this->assertCount(1, $response['channels']); + $this->assertArrayHasKey('timestamp', $response); + $this->assertEquals('account.' . $userId, $response['channels'][0]); + $this->assertEquals('account.update.name', $response['event']); + + $this->assertArrayHasKey('$id', $response['payload']); + $this->assertArrayHasKey('name', $response['payload']); + $this->assertArrayHasKey('registration', $response['payload']); + $this->assertArrayHasKey('status', $response['payload']); + $this->assertArrayHasKey('email', $response['payload']); + $this->assertArrayHasKey('emailVerification', $response['payload']); + $this->assertArrayHasKey('prefs', $response['payload']); + + $this->assertEquals($name, $response['payload']['name']); + + + /** + * Test Account Password Event + */ + $this->client->call(Client::METHOD_PATCH, '/account/password', array_merge([ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'x-appwrite-project' => $projectId, + 'cookie' => 'a_session_'.$projectId.'=' . $session, + ]), [ + 'password' => 'new-password', + 'oldPassword' => 'password', + ]); + + $response = json_decode($client->receive(), true); + + $this->assertCount(1, $response['channels']); + $this->assertArrayHasKey('timestamp', $response); + $this->assertEquals('account.' . $userId, $response['channels'][0]); + $this->assertEquals('account.update.password', $response['event']); + + $this->assertArrayHasKey('$id', $response['payload']); + $this->assertArrayHasKey('name', $response['payload']); + $this->assertArrayHasKey('registration', $response['payload']); + $this->assertArrayHasKey('status', $response['payload']); + $this->assertArrayHasKey('email', $response['payload']); + $this->assertArrayHasKey('emailVerification', $response['payload']); + $this->assertArrayHasKey('prefs', $response['payload']); + + $this->assertEquals($name, $response['payload']['name']); + + /** + * Test Account Email Update + */ + $this->client->call(Client::METHOD_PATCH, '/account/email', array_merge([ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'x-appwrite-project' => $projectId, + 'cookie' => 'a_session_'.$projectId.'=' . $session, + ]), [ + 'email' => 'torsten@appwrite.io', + 'password' => 'new-password', + ]); + + $response = json_decode($client->receive(), true); + + $this->assertCount(1, $response['channels']); + $this->assertArrayHasKey('timestamp', $response); + $this->assertEquals('account.' . $userId, $response['channels'][0]); + $this->assertEquals('account.update.email', $response['event']); + $this->assertNotEmpty($response['payload']); + + $this->assertEquals('torsten@appwrite.io', $response['payload']['email']); + + /** + * Test Account Verification Create + */ + $this->client->call(Client::METHOD_POST, '/account/verification', array_merge([ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'x-appwrite-project' => $projectId, + 'cookie' => 'a_session_'.$projectId.'=' . $session, + ]), [ + 'url' => 'http://localhost/verification', + ]); + + $response = json_decode($client->receive(), true); + + $this->assertCount(1, $response['channels']); + $this->assertArrayHasKey('timestamp', $response); + $this->assertEquals('account.' . $userId, $response['channels'][0]); + $this->assertEquals('account.verification.create', $response['event']); + + $lastEmail = $this->getLastEmail(); + $verification = substr($lastEmail['text'], strpos($lastEmail['text'], '&secret=', 0) + 8, 256); + + /** + * Test Account Verification Complete + */ + $response = $this->client->call(Client::METHOD_PUT, '/account/verification', array_merge([ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'x-appwrite-project' => $projectId, + 'cookie' => 'a_session_'.$projectId.'=' . $session, + ]), [ + 'userId' => $userId, + 'secret' => $verification, + ]); + + $response = json_decode($client->receive(), true); + + $this->assertCount(1, $response['channels']); + $this->assertArrayHasKey('timestamp', $response); + $this->assertEquals('account.' . $userId, $response['channels'][0]); + $this->assertEquals('account.verification.update', $response['event']); + + /** + * Test Acoount Prefs Update + */ + $this->client->call(Client::METHOD_PATCH, '/account/prefs', array_merge([ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'x-appwrite-project' => $projectId, + 'cookie' => 'a_session_'.$projectId.'=' . $session, + ]), [ + 'prefs' => [ + 'prefKey1' => 'prefValue1', + 'prefKey2' => 'prefValue2', + ] + ]); + + $response = json_decode($client->receive(), true); + + $this->assertCount(1, $response['channels']); + $this->assertArrayHasKey('timestamp', $response); + $this->assertEquals('account.' . $userId, $response['channels'][0]); + $this->assertEquals('account.update.prefs', $response['event']); + $this->assertNotEmpty($response['payload']); + + /** + * Test Account Session Create + */ + $response = $this->client->call(Client::METHOD_POST, '/account/sessions', array_merge([ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'x-appwrite-project' => $projectId, + ]), [ + 'email' => 'torsten@appwrite.io', + 'password' => 'new-password', + ]); + + $sessionNew = $this->client->parseCookie((string)$response['headers']['set-cookie'])['a_session_'.$projectId]; + $sessionNewId = $response['body']['$id']; + + $response = json_decode($client->receive(), true); + + $this->assertCount(1, $response['channels']); + $this->assertArrayHasKey('timestamp', $response); + $this->assertEquals('account.' . $userId, $response['channels'][0]); + $this->assertEquals('account.sessions.create', $response['event']); + $this->assertNotEmpty($response['payload']); + + /** + * Test Account Session Delete + */ + $this->client->call(Client::METHOD_DELETE, '/account/sessions/'.$sessionNewId, array_merge([ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'x-appwrite-project' => $projectId, + 'cookie' => 'a_session_'.$projectId.'=' . $sessionNew, + ])); + + $response = json_decode($client->receive(), true); + + $this->assertCount(1, $response['channels']); + $this->assertArrayHasKey('timestamp', $response); + $this->assertEquals('account.' . $userId, $response['channels'][0]); + $this->assertEquals('account.sessions.delete', $response['event']); + $this->assertNotEmpty($response['payload']); + + /** + * Test Account Create Recovery + */ + $this->client->call(Client::METHOD_POST, '/account/recovery', array_merge([ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'x-appwrite-project' => $projectId, + ]), [ + 'email' => 'torsten@appwrite.io', + 'url' => 'http://localhost/recovery', + ]); + + $response = json_decode($client->receive(), true); + + $lastEmail = $this->getLastEmail(); + $recovery = substr($lastEmail['text'], strpos($lastEmail['text'], '&secret=', 0) + 8, 256); + + $this->assertCount(1, $response['channels']); + $this->assertArrayHasKey('timestamp', $response); + $this->assertEquals('account.' . $userId, $response['channels'][0]); + $this->assertEquals('account.recovery.create', $response['event']); + $this->assertNotEmpty($response['payload']); + + $response = $this->client->call(Client::METHOD_PUT, '/account/recovery', array_merge([ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'x-appwrite-project' => $projectId, + ]), [ + 'userId' => $userId, + 'secret' => $recovery, + 'password' => 'test-recovery', + 'passwordAgain' => 'test-recovery', + ]); + + $response = json_decode($client->receive(), true); + + $this->assertCount(1, $response['channels']); + $this->assertArrayHasKey('timestamp', $response); + $this->assertEquals('account.' . $userId, $response['channels'][0]); + $this->assertEquals('account.recovery.update', $response['event']); + $this->assertNotEmpty($response['payload']); + + $client->close(); + } } diff --git a/tests/e2e/Services/Realtime/RealtimeCustomClientTest.php b/tests/e2e/Services/Realtime/RealtimeCustomClientTest.php index e45c0b64bd..381f689691 100644 --- a/tests/e2e/Services/Realtime/RealtimeCustomClientTest.php +++ b/tests/e2e/Services/Realtime/RealtimeCustomClientTest.php @@ -15,49 +15,4 @@ class RealtimeCustomClientTest extends Scope use RealtimeBase; use ProjectCustom; use SideClient; - - private function getWebsocket($channels = [], $headers = []) - { - $headers = array_merge( - ['Origin' => 'http://appwrite.test'], - $headers - ); - $query = [ - 'project' => $this->getProject()['$id'], - 'channels' => $channels - ]; - return new WebSocketClient('ws://appwrite-traefik/v1/realtime?' . http_build_query($query), [ - 'headers' => $headers, - 'timeout' => 5, - ]); - } - - public function testConnection() - { - /** - * Test for SUCCESS - */ - $client = $this->getWebsocket(['documents']); - $this->assertEquals('{"documents":0}', $client->receive()); - $client->close(); - - $client = $this->getWebsocket(['documents'], ['Origin' => 'http://appwrite.unknown']); - $this->assertEquals('Invalid Origin. Register your new client (appwrite.unknown) as a new Web platform on your project console dashboard', $client->receive()); - $this->expectException(ConnectionException::class); // Check if server disconnnected client - $client->close(); - - $client = $this->getWebsocket(); - $this->assertEquals('Missing channels', $client->receive()); - $this->expectException(ConnectionException::class); // Check if server disconnnected client - $client->close(); - - $client = new WebSocketClient('ws://appwrite-traefik/v1/realtime', [ - 'headers' => [ - 'Origin' => 'appwrite.test' - ] - ]); - $this->assertEquals('Missing or unknown project ID', $client->receive()); - $this->expectException(ConnectionException::class); // Check if server disconnnected client - $client->close(); - } } \ No newline at end of file From 844d09d1faddaf06e2a2b26e870ddb5ee0269d41 Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Tue, 2 Mar 2021 19:06:15 +0100 Subject: [PATCH 048/267] remove unnecessary import --- tests/e2e/Services/Realtime/RealtimeCustomClientTest.php | 3 --- 1 file changed, 3 deletions(-) diff --git a/tests/e2e/Services/Realtime/RealtimeCustomClientTest.php b/tests/e2e/Services/Realtime/RealtimeCustomClientTest.php index 381f689691..8822c493b2 100644 --- a/tests/e2e/Services/Realtime/RealtimeCustomClientTest.php +++ b/tests/e2e/Services/Realtime/RealtimeCustomClientTest.php @@ -2,13 +2,10 @@ namespace Tests\E2E\Services\Realtime; -use Tests\E2E\Client; use Tests\E2E\Scopes\Scope; use Tests\E2E\Scopes\ProjectCustom; use Tests\E2E\Scopes\SideClient; -use WebSocket\Client as WebSocketClient; -use WebSocket\ConnectionException; class RealtimeCustomClientTest extends Scope { From 78c413ee30d0ae0040f7f491084bc1578f01f8b1 Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Tue, 2 Mar 2021 19:07:58 +0100 Subject: [PATCH 049/267] fix events that return non user objects --- src/Appwrite/Event/Realtime.php | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/Appwrite/Event/Realtime.php b/src/Appwrite/Event/Realtime.php index 19f076a266..cbae7d4623 100644 --- a/src/Appwrite/Event/Realtime.php +++ b/src/Appwrite/Event/Realtime.php @@ -109,6 +109,13 @@ class Realtime private function prepareChannels(): void { switch (true) { + case strpos($this->event, 'account.recovery.') === 0: + case strpos($this->event, 'account.sessions.') === 0: + case strpos($this->event, 'account.verification.') === 0: + $this->channels[] = 'account.' . $this->payload->getAttribute('userId'); + $this->permissions = ['user:' . $this->payload->getAttribute('userId')]; + + break; case strpos($this->event, 'account.') === 0: $this->channels[] = 'account.' . $this->payload->getId(); $this->permissions = ['user:' . $this->payload->getId()]; From 4253e20128c2568ef11e27e9d3575920e391bedb Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Wed, 3 Mar 2021 09:47:33 +0100 Subject: [PATCH 050/267] remove unused deps --- composer.json | 1 - composer.lock | 628 +------------------------------------------------- 2 files changed, 9 insertions(+), 620 deletions(-) diff --git a/composer.json b/composer.json index df75fc237b..f892d50fb8 100644 --- a/composer.json +++ b/composer.json @@ -61,7 +61,6 @@ "require-dev": { "appwrite/sdk-generator": "0.5.5", "phpunit/phpunit": "9.4.2", - "ratchet/pawl": "0.3.5", "swoole/ide-helper": "4.5.5", "textalk/websocket": "1.5.2", "vimeo/psalm": "4.1.1" diff --git a/composer.lock b/composer.lock index e695d73dce..5c4c506e79 100644 --- a/composer.lock +++ b/composer.lock @@ -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": "0991bf902937a89fc9e9ba5d45263f21", + "content-hash": "7f55eb794a021679abe652b85e3c0134", "packages": [ { "name": "adhocore/jwt", @@ -574,12 +574,12 @@ "source": { "type": "git", "url": "https://github.com/guzzle/psr7.git", - "reference": "f47ece9e6e8ce74e3be04bef47f46061dc18c095" + "reference": "2f3e4f6cf8fd4aad7624c90a94f0ab38fde25976" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/psr7/zipball/f47ece9e6e8ce74e3be04bef47f46061dc18c095", - "reference": "f47ece9e6e8ce74e3be04bef47f46061dc18c095", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/2f3e4f6cf8fd4aad7624c90a94f0ab38fde25976", + "reference": "2f3e4f6cf8fd4aad7624c90a94f0ab38fde25976", "shasum": "" }, "require": { @@ -641,7 +641,7 @@ "issues": "https://github.com/guzzle/psr7/issues", "source": "https://github.com/guzzle/psr7/tree/1.x" }, - "time": "2020-12-08T11:45:39+00:00" + "time": "2021-03-02T18:57:24+00:00" }, { "name": "influxdb/influxdb-php", @@ -1004,12 +1004,12 @@ "source": { "type": "git", "url": "https://github.com/php-fig/log.git", - "reference": "dd738d0b4491f32725492cf345f6b501f5922fec" + "reference": "a18c1e692e02b84abbafe4856c3cd7cc6903908c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/log/zipball/dd738d0b4491f32725492cf345f6b501f5922fec", - "reference": "dd738d0b4491f32725492cf345f6b501f5922fec", + "url": "https://api.github.com/repos/php-fig/log/zipball/a18c1e692e02b84abbafe4856c3cd7cc6903908c", + "reference": "a18c1e692e02b84abbafe4856c3cd7cc6903908c", "shasum": "" }, "require": { @@ -1047,7 +1047,7 @@ "support": { "source": "https://github.com/php-fig/log/tree/master" }, - "time": "2020-09-18T06:44:51+00:00" + "time": "2021-03-02T15:02:34+00:00" }, { "name": "ralouphie/getallheaders", @@ -2447,53 +2447,6 @@ ], "time": "2020-11-10T19:05:51+00:00" }, - { - "name": "evenement/evenement", - "version": "v3.0.1", - "source": { - "type": "git", - "url": "https://github.com/igorw/evenement.git", - "reference": "531bfb9d15f8aa57454f5f0285b18bec903b8fb7" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/igorw/evenement/zipball/531bfb9d15f8aa57454f5f0285b18bec903b8fb7", - "reference": "531bfb9d15f8aa57454f5f0285b18bec903b8fb7", - "shasum": "" - }, - "require": { - "php": ">=7.0" - }, - "require-dev": { - "phpunit/phpunit": "^6.0" - }, - "type": "library", - "autoload": { - "psr-0": { - "Evenement": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Igor Wiedler", - "email": "igor@wiedler.ch" - } - ], - "description": "Événement is a very simple event dispatching library for PHP", - "keywords": [ - "event-dispatcher", - "event-emitter" - ], - "support": { - "issues": "https://github.com/igorw/evenement/issues", - "source": "https://github.com/igorw/evenement/tree/master" - }, - "time": "2017-07-23T21:35:13+00:00" - }, { "name": "felixfbecker/advanced-json-rpc", "version": "v3.2.0", @@ -3765,569 +3718,6 @@ }, "time": "2020-10-13T07:07:53+00:00" }, - { - "name": "ratchet/pawl", - "version": "v0.3.5", - "source": { - "type": "git", - "url": "https://github.com/ratchetphp/Pawl.git", - "reference": "89ec703c76dc893484a2a0ed44b48a37d445abd5" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/ratchetphp/Pawl/zipball/89ec703c76dc893484a2a0ed44b48a37d445abd5", - "reference": "89ec703c76dc893484a2a0ed44b48a37d445abd5", - "shasum": "" - }, - "require": { - "evenement/evenement": "^3.0 || ^2.0", - "php": ">=5.4", - "ratchet/rfc6455": "^0.3", - "react/socket": "^1.0 || ^0.8 || ^0.7" - }, - "require-dev": { - "phpunit/phpunit": "~4.8" - }, - "suggest": { - "reactivex/rxphp": "~2.0" - }, - "type": "library", - "autoload": { - "psr-4": { - "Ratchet\\Client\\": "src" - }, - "files": [ - "src/functions_include.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "Asynchronous WebSocket client", - "keywords": [ - "Ratchet", - "async", - "client", - "websocket", - "websocket client" - ], - "support": { - "issues": "https://github.com/ratchetphp/Pawl/issues", - "source": "https://github.com/ratchetphp/Pawl/tree/master" - }, - "time": "2020-07-17T15:32:47+00:00" - }, - { - "name": "ratchet/rfc6455", - "version": "v0.3", - "source": { - "type": "git", - "url": "https://github.com/ratchetphp/RFC6455.git", - "reference": "c8651c7938651c2d55f5d8c2422ac5e57a183341" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/ratchetphp/RFC6455/zipball/c8651c7938651c2d55f5d8c2422ac5e57a183341", - "reference": "c8651c7938651c2d55f5d8c2422ac5e57a183341", - "shasum": "" - }, - "require": { - "guzzlehttp/psr7": "^1.0", - "php": ">=5.4.2" - }, - "require-dev": { - "phpunit/phpunit": "5.7.*", - "react/socket": "^1.3" - }, - "type": "library", - "autoload": { - "psr-4": { - "Ratchet\\RFC6455\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Chris Boden", - "email": "cboden@gmail.com", - "role": "Developer" - }, - { - "name": "Matt Bonneau", - "role": "Developer" - } - ], - "description": "RFC6455 WebSocket protocol handler", - "homepage": "http://socketo.me", - "keywords": [ - "WebSockets", - "rfc6455", - "websocket" - ], - "support": { - "chat": "https://gitter.im/reactphp/reactphp", - "issues": "https://github.com/ratchetphp/RFC6455/issues", - "source": "https://github.com/ratchetphp/RFC6455/tree/v0.3" - }, - "time": "2020-05-15T18:31:24+00:00" - }, - { - "name": "react/cache", - "version": "v1.1.1", - "source": { - "type": "git", - "url": "https://github.com/reactphp/cache.git", - "reference": "4bf736a2cccec7298bdf745db77585966fc2ca7e" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/reactphp/cache/zipball/4bf736a2cccec7298bdf745db77585966fc2ca7e", - "reference": "4bf736a2cccec7298bdf745db77585966fc2ca7e", - "shasum": "" - }, - "require": { - "php": ">=5.3.0", - "react/promise": "^3.0 || ^2.0 || ^1.1" - }, - "require-dev": { - "phpunit/phpunit": "^9.3 || ^5.7 || ^4.8.35" - }, - "type": "library", - "autoload": { - "psr-4": { - "React\\Cache\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Christian Lück", - "email": "christian@clue.engineering", - "homepage": "https://clue.engineering/" - }, - { - "name": "Cees-Jan Kiewiet", - "email": "reactphp@ceesjankiewiet.nl", - "homepage": "https://wyrihaximus.net/" - }, - { - "name": "Jan Sorgalla", - "email": "jsorgalla@gmail.com", - "homepage": "https://sorgalla.com/" - }, - { - "name": "Chris Boden", - "email": "cboden@gmail.com", - "homepage": "https://cboden.dev/" - } - ], - "description": "Async, Promise-based cache interface for ReactPHP", - "keywords": [ - "cache", - "caching", - "promise", - "reactphp" - ], - "support": { - "issues": "https://github.com/reactphp/cache/issues", - "source": "https://github.com/reactphp/cache/tree/v1.1.1" - }, - "funding": [ - { - "url": "https://github.com/WyriHaximus", - "type": "github" - }, - { - "url": "https://github.com/clue", - "type": "github" - } - ], - "time": "2021-02-02T06:47:52+00:00" - }, - { - "name": "react/dns", - "version": "v1.4.0", - "source": { - "type": "git", - "url": "https://github.com/reactphp/dns.git", - "reference": "665260757171e2ab17485b44e7ffffa7acb6ca1f" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/reactphp/dns/zipball/665260757171e2ab17485b44e7ffffa7acb6ca1f", - "reference": "665260757171e2ab17485b44e7ffffa7acb6ca1f", - "shasum": "" - }, - "require": { - "php": ">=5.3.0", - "react/cache": "^1.0 || ^0.6 || ^0.5", - "react/event-loop": "^1.0 || ^0.5", - "react/promise": "^3.0 || ^2.7 || ^1.2.1", - "react/promise-timer": "^1.2" - }, - "require-dev": { - "clue/block-react": "^1.2", - "phpunit/phpunit": "^9.3 || ^4.8.35" - }, - "type": "library", - "autoload": { - "psr-4": { - "React\\Dns\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Christian Lück", - "email": "christian@clue.engineering", - "homepage": "https://clue.engineering/" - }, - { - "name": "Cees-Jan Kiewiet", - "email": "reactphp@ceesjankiewiet.nl", - "homepage": "https://wyrihaximus.net/" - }, - { - "name": "Jan Sorgalla", - "email": "jsorgalla@gmail.com", - "homepage": "https://sorgalla.com/" - }, - { - "name": "Chris Boden", - "email": "cboden@gmail.com", - "homepage": "https://cboden.dev/" - } - ], - "description": "Async DNS resolver for ReactPHP", - "keywords": [ - "async", - "dns", - "dns-resolver", - "reactphp" - ], - "support": { - "issues": "https://github.com/reactphp/dns/issues", - "source": "https://github.com/reactphp/dns/tree/v1.4.0" - }, - "funding": [ - { - "url": "https://github.com/WyriHaximus", - "type": "github" - }, - { - "url": "https://github.com/clue", - "type": "github" - } - ], - "time": "2020-09-18T12:12:55+00:00" - }, - { - "name": "react/event-loop", - "version": "v1.1.1", - "source": { - "type": "git", - "url": "https://github.com/reactphp/event-loop.git", - "reference": "6d24de090cd59cfc830263cfba965be77b563c13" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/reactphp/event-loop/zipball/6d24de090cd59cfc830263cfba965be77b563c13", - "reference": "6d24de090cd59cfc830263cfba965be77b563c13", - "shasum": "" - }, - "require": { - "php": ">=5.3.0" - }, - "require-dev": { - "phpunit/phpunit": "^7.0 || ^6.4 || ^5.7 || ^4.8.35" - }, - "suggest": { - "ext-event": "~1.0 for ExtEventLoop", - "ext-pcntl": "For signal handling support when using the StreamSelectLoop", - "ext-uv": "* for ExtUvLoop" - }, - "type": "library", - "autoload": { - "psr-4": { - "React\\EventLoop\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "ReactPHP's core reactor event loop that libraries can use for evented I/O.", - "keywords": [ - "asynchronous", - "event-loop" - ], - "support": { - "issues": "https://github.com/reactphp/event-loop/issues", - "source": "https://github.com/reactphp/event-loop/tree/v1.1.1" - }, - "time": "2020-01-01T18:39:52+00:00" - }, - { - "name": "react/promise", - "version": "2.x-dev", - "source": { - "type": "git", - "url": "https://github.com/reactphp/promise.git", - "reference": "a9752a861e21c0fe0b380c9f9e55beddc0ed7d31" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/reactphp/promise/zipball/a9752a861e21c0fe0b380c9f9e55beddc0ed7d31", - "reference": "a9752a861e21c0fe0b380c9f9e55beddc0ed7d31", - "shasum": "" - }, - "require": { - "php": ">=5.4.0" - }, - "require-dev": { - "phpunit/phpunit": "^9.3 || ^5.7 || ^4.8.36" - }, - "type": "library", - "autoload": { - "psr-4": { - "React\\Promise\\": "src/" - }, - "files": [ - "src/functions_include.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Jan Sorgalla", - "email": "jsorgalla@gmail.com" - } - ], - "description": "A lightweight implementation of CommonJS Promises/A for PHP", - "keywords": [ - "promise", - "promises" - ], - "support": { - "issues": "https://github.com/reactphp/promise/issues", - "source": "https://github.com/reactphp/promise/tree/2.x" - }, - "funding": [ - { - "url": "https://github.com/WyriHaximus", - "type": "github" - }, - { - "url": "https://github.com/clue", - "type": "github" - } - ], - "time": "2021-02-09T15:06:50+00:00" - }, - { - "name": "react/promise-timer", - "version": "v1.6.0", - "source": { - "type": "git", - "url": "https://github.com/reactphp/promise-timer.git", - "reference": "daee9baf6ef30c43ea4c86399f828bb5f558f6e6" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/reactphp/promise-timer/zipball/daee9baf6ef30c43ea4c86399f828bb5f558f6e6", - "reference": "daee9baf6ef30c43ea4c86399f828bb5f558f6e6", - "shasum": "" - }, - "require": { - "php": ">=5.3", - "react/event-loop": "^1.0 || ^0.5 || ^0.4 || ^0.3.5", - "react/promise": "^3.0 || ^2.7.0 || ^1.2.1" - }, - "require-dev": { - "phpunit/phpunit": "^9.0 || ^5.7 || ^4.8.35" - }, - "type": "library", - "autoload": { - "psr-4": { - "React\\Promise\\Timer\\": "src/" - }, - "files": [ - "src/functions_include.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Christian Lück", - "email": "christian@lueck.tv" - } - ], - "description": "A trivial implementation of timeouts for Promises, built on top of ReactPHP.", - "homepage": "https://github.com/reactphp/promise-timer", - "keywords": [ - "async", - "event-loop", - "promise", - "reactphp", - "timeout", - "timer" - ], - "support": { - "issues": "https://github.com/reactphp/promise-timer/issues", - "source": "https://github.com/reactphp/promise-timer/tree/v1.6.0" - }, - "time": "2020-07-10T12:18:06+00:00" - }, - { - "name": "react/socket", - "version": "v1.6.0", - "source": { - "type": "git", - "url": "https://github.com/reactphp/socket.git", - "reference": "e2b96b23a13ca9b41ab343268dbce3f8ef4d524a" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/reactphp/socket/zipball/e2b96b23a13ca9b41ab343268dbce3f8ef4d524a", - "reference": "e2b96b23a13ca9b41ab343268dbce3f8ef4d524a", - "shasum": "" - }, - "require": { - "evenement/evenement": "^3.0 || ^2.0 || ^1.0", - "php": ">=5.3.0", - "react/dns": "^1.1", - "react/event-loop": "^1.0 || ^0.5", - "react/promise": "^2.6.0 || ^1.2.1", - "react/promise-timer": "^1.4.0", - "react/stream": "^1.1" - }, - "require-dev": { - "clue/block-react": "^1.2", - "phpunit/phpunit": "^9.3 || ^5.7 || ^4.8.35", - "react/promise-stream": "^1.2" - }, - "type": "library", - "autoload": { - "psr-4": { - "React\\Socket\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Christian Lück", - "email": "christian@clue.engineering", - "homepage": "https://clue.engineering/" - }, - { - "name": "Cees-Jan Kiewiet", - "email": "reactphp@ceesjankiewiet.nl", - "homepage": "https://wyrihaximus.net/" - }, - { - "name": "Jan Sorgalla", - "email": "jsorgalla@gmail.com", - "homepage": "https://sorgalla.com/" - }, - { - "name": "Chris Boden", - "email": "cboden@gmail.com", - "homepage": "https://cboden.dev/" - } - ], - "description": "Async, streaming plaintext TCP/IP and secure TLS socket server and client connections for ReactPHP", - "keywords": [ - "Connection", - "Socket", - "async", - "reactphp", - "stream" - ], - "support": { - "issues": "https://github.com/reactphp/socket/issues", - "source": "https://github.com/reactphp/socket/tree/v1.6.0" - }, - "funding": [ - { - "url": "https://github.com/WyriHaximus", - "type": "github" - }, - { - "url": "https://github.com/clue", - "type": "github" - } - ], - "time": "2020-08-28T12:49:05+00:00" - }, - { - "name": "react/stream", - "version": "v1.1.1", - "source": { - "type": "git", - "url": "https://github.com/reactphp/stream.git", - "reference": "7c02b510ee3f582c810aeccd3a197b9c2f52ff1a" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/reactphp/stream/zipball/7c02b510ee3f582c810aeccd3a197b9c2f52ff1a", - "reference": "7c02b510ee3f582c810aeccd3a197b9c2f52ff1a", - "shasum": "" - }, - "require": { - "evenement/evenement": "^3.0 || ^2.0 || ^1.0", - "php": ">=5.3.8", - "react/event-loop": "^1.0 || ^0.5 || ^0.4 || ^0.3.5" - }, - "require-dev": { - "clue/stream-filter": "~1.2", - "phpunit/phpunit": "^7.0 || ^6.4 || ^5.7 || ^4.8.35" - }, - "type": "library", - "autoload": { - "psr-4": { - "React\\Stream\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "Event-driven readable and writable streams for non-blocking I/O in ReactPHP", - "keywords": [ - "event-driven", - "io", - "non-blocking", - "pipe", - "reactphp", - "readable", - "stream", - "writable" - ], - "support": { - "issues": "https://github.com/reactphp/stream/issues", - "source": "https://github.com/reactphp/stream/tree/v1.1.1" - }, - "time": "2020-05-04T10:17:57+00:00" - }, { "name": "sebastian/cli-parser", "version": "dev-master", From eab42c85759f17ed6f862d66b81e6e50750f97f7 Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Wed, 3 Mar 2021 11:28:21 +0100 Subject: [PATCH 051/267] add collections/documents e2e tests --- tests/e2e/Services/Realtime/RealtimeBase.php | 158 +++++++++++++++++-- 1 file changed, 142 insertions(+), 16 deletions(-) diff --git a/tests/e2e/Services/Realtime/RealtimeBase.php b/tests/e2e/Services/Realtime/RealtimeBase.php index 3449e89416..e0d2b625fc 100644 --- a/tests/e2e/Services/Realtime/RealtimeBase.php +++ b/tests/e2e/Services/Realtime/RealtimeBase.php @@ -165,14 +165,7 @@ trait RealtimeBase $this->assertArrayHasKey('timestamp', $response); $this->assertEquals('account.' . $userId, $response['channels'][0]); $this->assertEquals('account.update.name', $response['event']); - - $this->assertArrayHasKey('$id', $response['payload']); - $this->assertArrayHasKey('name', $response['payload']); - $this->assertArrayHasKey('registration', $response['payload']); - $this->assertArrayHasKey('status', $response['payload']); - $this->assertArrayHasKey('email', $response['payload']); - $this->assertArrayHasKey('emailVerification', $response['payload']); - $this->assertArrayHasKey('prefs', $response['payload']); + $this->assertNotEmpty($response['payload']); $this->assertEquals($name, $response['payload']['name']); @@ -196,14 +189,7 @@ trait RealtimeBase $this->assertArrayHasKey('timestamp', $response); $this->assertEquals('account.' . $userId, $response['channels'][0]); $this->assertEquals('account.update.password', $response['event']); - - $this->assertArrayHasKey('$id', $response['payload']); - $this->assertArrayHasKey('name', $response['payload']); - $this->assertArrayHasKey('registration', $response['payload']); - $this->assertArrayHasKey('status', $response['payload']); - $this->assertArrayHasKey('email', $response['payload']); - $this->assertArrayHasKey('emailVerification', $response['payload']); - $this->assertArrayHasKey('prefs', $response['payload']); + $this->assertNotEmpty($response['payload']); $this->assertEquals($name, $response['payload']['name']); @@ -380,4 +366,144 @@ trait RealtimeBase $client->close(); } + + public function testChannelDatabase() + { + $user = $this->getUser(); + $userId = $user['$id'] ?? ''; + $session = $user['session'] ?? ''; + $projectId = $this->getProject()['$id']; + + $client = $this->getWebsocket(['documents', 'collections'], [ + 'origin' => 'http://localhost', + 'cookie' => 'a_session_'.$projectId.'=' . $session + ]); + $response = json_decode($client->receive(), true); + $this->assertCount(2, $response); + $this->assertArrayHasKey('documents', $response); + $this->assertArrayHasKey('collections', $response); + + /** + * Test for SUCCESS + */ + $actors = $this->client->call(Client::METHOD_POST, '/database/collections', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'name' => 'Actors', + 'read' => ['*'], + 'write' => ['*'], + 'rules' => [ + [ + 'label' => 'First Name', + 'key' => 'firstName', + 'type' => 'text', + 'default' => '', + 'required' => true, + 'array' => false + ], + [ + 'label' => 'Last Name', + 'key' => 'lastName', + 'type' => 'text', + 'default' => '', + 'required' => true, + 'array' => false + ], + ], + ]); + + $response = json_decode($client->receive(), true); + + $this->assertArrayHasKey('timestamp', $response); + $this->assertCount(2, $response['channels']); + $this->assertContains('collections', $response['channels']); + $this->assertContains('collections.' . $actors['body']['$id'], $response['channels']); + $this->assertEquals('database.collections.create', $response['event']); + $this->assertNotEmpty($response['payload']); + + $data = ['actorsId' => $actors['body']['$id']]; + + $document = $this->client->call(Client::METHOD_POST, '/database/collections/' . $data['actorsId'] . '/documents', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'data' => [ + 'firstName' => 'Chris', + 'lastName' => 'Evans', + ], + 'read' => ['*'], + 'write' => ['*'], + ]); + + $response = json_decode($client->receive(), true); + + $this->assertArrayHasKey('timestamp', $response); + $this->assertCount(3, $response['channels']); + $this->assertContains('documents', $response['channels']); + $this->assertContains('documents.' . $document['body']['$id'], $response['channels']); + $this->assertContains('collections.' . $actors['body']['$id'] . '.documents', $response['channels']); + $this->assertEquals('database.documents.create', $response['event']); + $this->assertNotEmpty($response['payload']); + + $data['documentId'] = $document['body']['$id']; + + $document = $this->client->call(Client::METHOD_PATCH, '/database/collections/' . $data['actorsId'] . '/documents/' . $data['documentId'], array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'data' => [ + 'firstName' => 'Chris1', + 'lastName' => 'Evans2', + ], + 'read' => ['*'], + 'write' => ['*'], + ]); + + $response = json_decode($client->receive(), true); + + $this->assertArrayHasKey('timestamp', $response); + $this->assertCount(3, $response['channels']); + $this->assertContains('documents', $response['channels']); + $this->assertContains('documents.' . $data['documentId'], $response['channels']); + $this->assertContains('collections.' . $data['actorsId'] . '.documents', $response['channels']); + $this->assertEquals('database.documents.update', $response['event']); + $this->assertNotEmpty($response['payload']); + + $this->assertEquals($response['payload']['firstName'], 'Chris1'); + $this->assertEquals($response['payload']['lastName'], 'Evans2'); + + $document = $this->client->call(Client::METHOD_POST, '/database/collections/' . $data['actorsId'] . '/documents', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'data' => [ + 'firstName' => 'Bradly', + 'lastName' => 'Cooper', + + ], + 'read' => ['*'], + 'write' => ['*'], + ]); + + $client->receive(); + + $this->client->call(Client::METHOD_DELETE, '/database/collections/' . $data['actorsId'] . '/documents/' . $document['body']['$id'], array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $response = json_decode($client->receive(), true); + + $this->assertArrayHasKey('timestamp', $response); + $this->assertCount(3, $response['channels']); + $this->assertContains('documents', $response['channels']); + $this->assertContains('documents.' . $document['body']['$id'], $response['channels']); + $this->assertContains('collections.' . $data['actorsId'] . '.documents', $response['channels']); + $this->assertEquals('database.documents.delete', $response['event']); + $this->assertNotEmpty($response['payload']); + + $client->close(); + } } From ed16232f93d63c669847d17fd4daf93024f10370 Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Wed, 3 Mar 2021 11:47:59 +0100 Subject: [PATCH 052/267] add files e2e test for realtime --- tests/e2e/Services/Realtime/RealtimeBase.php | 94 ++++++++++++++++++-- 1 file changed, 89 insertions(+), 5 deletions(-) diff --git a/tests/e2e/Services/Realtime/RealtimeBase.php b/tests/e2e/Services/Realtime/RealtimeBase.php index e0d2b625fc..379f21a022 100644 --- a/tests/e2e/Services/Realtime/RealtimeBase.php +++ b/tests/e2e/Services/Realtime/RealtimeBase.php @@ -2,6 +2,7 @@ namespace Tests\E2E\Services\Realtime; +use CURLFile; use Tests\E2E\Client; use WebSocket\Client as WebSocketClient; use WebSocket\ConnectionException; @@ -78,9 +79,6 @@ trait RealtimeBase 'cookie' => 'a_session_'.$this->getProject()['$id'].'=' . $session ]; - /** - * Test for SUCCESS - */ $client = $this->getWebsocket(['documents']); $response = json_decode($client->receive(), true); $this->assertCount(1, $response); @@ -370,7 +368,6 @@ trait RealtimeBase public function testChannelDatabase() { $user = $this->getUser(); - $userId = $user['$id'] ?? ''; $session = $user['session'] ?? ''; $projectId = $this->getProject()['$id']; @@ -384,7 +381,7 @@ trait RealtimeBase $this->assertArrayHasKey('collections', $response); /** - * Test for SUCCESS + * Test Collection Create */ $actors = $this->client->call(Client::METHOD_POST, '/database/collections', array_merge([ 'content-type' => 'application/json', @@ -425,6 +422,9 @@ trait RealtimeBase $data = ['actorsId' => $actors['body']['$id']]; + /** + * Test Document Create + */ $document = $this->client->call(Client::METHOD_POST, '/database/collections/' . $data['actorsId'] . '/documents', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], @@ -449,6 +449,9 @@ trait RealtimeBase $data['documentId'] = $document['body']['$id']; + /** + * Test Document Update + */ $document = $this->client->call(Client::METHOD_PATCH, '/database/collections/' . $data['actorsId'] . '/documents/' . $data['documentId'], array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], @@ -474,6 +477,9 @@ trait RealtimeBase $this->assertEquals($response['payload']['firstName'], 'Chris1'); $this->assertEquals($response['payload']['lastName'], 'Evans2'); + /** + * Test Document Delete + */ $document = $this->client->call(Client::METHOD_POST, '/database/collections/' . $data['actorsId'] . '/documents', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], @@ -506,4 +512,82 @@ trait RealtimeBase $client->close(); } + + public function testChannelFiles() + { + $user = $this->getUser(); + $session = $user['session'] ?? ''; + $projectId = $this->getProject()['$id']; + + $client = $this->getWebsocket(['files'], [ + 'origin' => 'http://localhost', + 'cookie' => 'a_session_'.$projectId.'=' . $session + ]); + $response = json_decode($client->receive(), true); + $this->assertCount(1, $response); + $this->assertArrayHasKey('files', $response); + + /** + * Test File Create + */ + $file = $this->client->call(Client::METHOD_POST, '/storage/files', array_merge([ + 'content-type' => 'multipart/form-data', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'file' => new CURLFile(realpath(__DIR__ . '/../../../resources/logo.png'), 'image/png', 'logo.png'), + 'read' => ['*'], + 'write' => ['*'], + 'folderId' => 'xyz', + ]); + + $response = json_decode($client->receive(), true); + + $this->assertArrayHasKey('timestamp', $response); + $this->assertCount(2, $response['channels']); + $this->assertContains('files', $response['channels']); + $this->assertContains('files.' . $file['body']['$id'], $response['channels']); + $this->assertEquals('storage.files.create', $response['event']); + $this->assertNotEmpty($response['payload']); + + $data = ['fileId' => $file['body']['$id']]; + + /** + * Test File Update + */ + $this->client->call(Client::METHOD_PUT, '/storage/files/' . $data['fileId'], array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'read' => ['*'], + 'write' => ['*'], + ]); + + $response = json_decode($client->receive(), true); + + $this->assertArrayHasKey('timestamp', $response); + $this->assertCount(2, $response['channels']); + $this->assertContains('files', $response['channels']); + $this->assertContains('files.' . $file['body']['$id'], $response['channels']); + $this->assertEquals('storage.files.update', $response['event']); + $this->assertNotEmpty($response['payload']); + + /** + * Test File Delete + */ + $this->client->call(Client::METHOD_DELETE, '/storage/files/' . $data['fileId'], array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $response = json_decode($client->receive(), true); + + $this->assertArrayHasKey('timestamp', $response); + $this->assertCount(2, $response['channels']); + $this->assertContains('files', $response['channels']); + $this->assertContains('files.' . $file['body']['$id'], $response['channels']); + $this->assertEquals('storage.files.delete', $response['event']); + $this->assertNotEmpty($response['payload']); + + $client->close(); + } } From c96f5ea75af3dd137d8077d2d130ca364f591c3f Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Thu, 4 Mar 2021 10:28:24 +0100 Subject: [PATCH 053/267] cleanup code --- app/controllers/api/health.php | 5 +-- app/realtime.php | 37 ++++++++----------- app/views/install/compose.phtml | 39 ++++++++++++++++++++ src/Appwrite/Event/Realtime.php | 7 +++- src/Appwrite/Realtime/Realtime.php | 11 ++++++ tests/unit/Realtime/RealtimeChannelsTest.php | 2 - tests/unit/Realtime/RealtimeGuestTest.php | 10 ----- 7 files changed, 72 insertions(+), 39 deletions(-) diff --git a/app/controllers/api/health.php b/app/controllers/api/health.php index c8559278f1..4658ac0794 100644 --- a/app/controllers/api/health.php +++ b/app/controllers/api/health.php @@ -40,10 +40,7 @@ App::get('/v1/health/realtime') ->inject('response') ->action(function ($response) { /** @var Utopia\Response $response */ - $redis = new Redis(); - $redis->connect('redis', 6379); - - $redis->publish('realtime', 'I\'m a live message'); + // TODO: realtime health $response->json(['status' => 'OK']); }); diff --git a/app/realtime.php b/app/realtime.php index 15b32c1bbf..6671568a18 100644 --- a/app/realtime.php +++ b/app/realtime.php @@ -27,15 +27,10 @@ use Utopia\Abuse\Adapters\TimeLimit; /** * TODO List * - * - CORS Validation - * - Limit payload size * - JWT Authentication (in path / or in message) * * Protocols Support: * - Websocket support: https://www.swoole.co.uk/docs/modules/swoole-websocket-server - * - MQTT support: https://www.swoole.co.uk/docs/modules/swoole-mqtt-server - * - SSE support: https://github.com/hhxsv5/php-sse - * - Socket.io support: https://github.com/shuixn/socket.io-swoole-server */ ini_set('default_socket_timeout', -1); @@ -44,13 +39,15 @@ Swoole\Runtime::enableCoroutine(SWOOLE_HOOK_ALL); $server = new Server('0.0.0.0', 80); $server->set([ - 'websocket_compression' => true, 'package_max_length' => 64000 // Default maximum Package Size (64kb) ]); $subscriptions = []; $connections = []; +/** + * Create Redis Connection Pool in favor of the default 'cache' register. + */ $register->set('redis', function () { $user = App::getEnv('_APP_REDIS_USER', ''); $pass = App::getEnv('_APP_REDIS_PASS', ''); @@ -106,14 +103,13 @@ $server->on('workerStart', function ($server, $workerId) use (&$subscriptions, & * Supported Resources: * - Collection * - Document - * - Bucket * - File - * - User? / Account? (no permissions) - * - Session? (no permissions) - * - Team? (no permissions) - * - Membership? (no permissions) - * - Function - * - Execution + * - Account + * - Session + * - Team? (not implemented yet) + * - Membership? (not implemented yet) + * - Function? (not available yet) + * - Execution? (not available yet) */ $event = json_decode($payload, true); @@ -157,7 +153,7 @@ $server->on('start', function (Server $server) { }); $server->on('open', function (Server $server, Request $request) use (&$connections, &$subscriptions, &$register) { - Console::info("Connection open (user: {$request->fd}, worker: {$server->getWorkerId()})"); + Console::info("Connection open (user: {$request->fd}, connections: {}, worker: {$server->getWorkerId()})"); $app = new App(''); $connection = $request->fd; @@ -263,9 +259,9 @@ $server->on('open', function (Server $server, Request $request) use (&$connectio } /* - * 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 + * 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', []))); @@ -296,11 +292,8 @@ $server->on('open', function (Server $server, Request $request) use (&$connectio }); $server->on('message', function (Server $server, Frame $frame) { - if ($frame->data === 'reload') { - $server->reload(); - } - - Console::info('Recieved message: ' . $frame->data . ' (user: ' . $frame->fd . ', worker: ' . $server->getWorkerId() . ')'); + $server->push($frame->fd, 'Sending messages is not allowed.'); + $server->close($frame->fd); }); $server->on('close', function (Server $server, int $fd) use (&$connections, &$subscriptions) { diff --git a/app/views/install/compose.phtml b/app/views/install/compose.phtml index 1c0c98108f..10d9ec86d5 100644 --- a/app/views/install/compose.phtml +++ b/app/views/install/compose.phtml @@ -95,6 +95,45 @@ services: - _APP_FUNCTIONS_MEMORY_SWAP - _APP_FUNCTIONS_ENVS + 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 + 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 + appwrite-worker-usage: image: appwrite/appwrite: entrypoint: worker-usage diff --git a/src/Appwrite/Event/Realtime.php b/src/Appwrite/Event/Realtime.php index cbae7d4623..c0f6d2ad3a 100644 --- a/src/Appwrite/Event/Realtime.php +++ b/src/Appwrite/Event/Realtime.php @@ -85,7 +85,7 @@ class Realtime /** * @param array $payload - * return $this + * @return $this */ public function setPayload(array $payload): self { @@ -169,6 +169,11 @@ class Realtime $this->reset(); } + /** + * Resets this event and unpopulates all data. + * + * @return $this + */ public function reset(): self { $this->event = ''; diff --git a/src/Appwrite/Realtime/Realtime.php b/src/Appwrite/Realtime/Realtime.php index 256ad64b31..93e5cffd97 100644 --- a/src/Appwrite/Realtime/Realtime.php +++ b/src/Appwrite/Realtime/Realtime.php @@ -13,6 +13,8 @@ class Realtime static $user; /** + * Sets the current user for the role and channel parsing. + * * @param Document $user */ static function setUser(Document $user) @@ -21,10 +23,16 @@ class Realtime } /** + * Returns array of roles that the set User has permissions to. + * * @return array */ static function getRoles() { + if (!isset(self::$user)) { + return []; + } + $roles = ['role:' . ((self::$user->isEmpty()) ? Auth::USER_ROLE_GUEST : Auth::USER_ROLE_MEMBER)]; if (!(self::$user->isEmpty())) { $roles[] = 'user:' . self::$user->getId(); @@ -42,6 +50,9 @@ class Realtime } /** + * 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 */ static function parseChannels(array $channels) diff --git a/tests/unit/Realtime/RealtimeChannelsTest.php b/tests/unit/Realtime/RealtimeChannelsTest.php index d5b0903957..1853254b43 100644 --- a/tests/unit/Realtime/RealtimeChannelsTest.php +++ b/tests/unit/Realtime/RealtimeChannelsTest.php @@ -32,8 +32,6 @@ class RealtimeChannelsTest extends TestCase 'documents.2', ]; - - public function setUp(): void { /** diff --git a/tests/unit/Realtime/RealtimeGuestTest.php b/tests/unit/Realtime/RealtimeGuestTest.php index 4eff23b1c6..b8cd68f8a9 100644 --- a/tests/unit/Realtime/RealtimeGuestTest.php +++ b/tests/unit/Realtime/RealtimeGuestTest.php @@ -11,14 +11,6 @@ class RealtimeGuestTest extends TestCase public $connections = []; public $subscriptions = []; - public function setUp(): void - { - } - - public function tearDown(): void - { - } - public function testGuest() { Realtime::setUser(new Document([ @@ -47,7 +39,6 @@ class RealtimeGuestTest extends TestCase Realtime::subscribe('1', 1, $roles, $this->subscriptions, $this->connections, $channels); - $event = [ 'project' => '1', 'permissions' => ['*'], @@ -192,7 +183,6 @@ class RealtimeGuestTest extends TestCase $this->assertCount(1, $this->connections); $this->assertCount(1, $this->subscriptions['1']); - Realtime::unsubscribe(1, $this->subscriptions, $this->connections); $this->assertEmpty($this->connections); From 927b46d9a0f56e23674b72eb30275c48b610b9da Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Thu, 4 Mar 2021 17:37:20 +0100 Subject: [PATCH 054/267] fix compose install template --- app/views/install/compose.phtml | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/app/views/install/compose.phtml b/app/views/install/compose.phtml index 10d9ec86d5..27dfbc4d00 100644 --- a/app/views/install/compose.phtml +++ b/app/views/install/compose.phtml @@ -15,8 +15,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: - :80 @@ -40,9 +40,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 From 1017a2fb6b649e0160e116adbe0bcd74c55bbf99 Mon Sep 17 00:00:00 2001 From: Eldad Fux Date: Tue, 9 Mar 2021 18:07:13 +0200 Subject: [PATCH 055/267] Refactored resources --- app/realtime.php | 106 ++---------------------- src/Appwrite/Database/Adapter/Redis.php | 11 +-- 2 files changed, 11 insertions(+), 106 deletions(-) diff --git a/app/realtime.php b/app/realtime.php index 6671568a18..b1c34266ca 100644 --- a/app/realtime.php +++ b/app/realtime.php @@ -3,23 +3,16 @@ require_once __DIR__ . '/init.php'; require_once __DIR__ . '/../vendor/autoload.php'; -use Appwrite\Auth\Auth; -use Appwrite\Database\Adapter\MySQL as MySQLAdapter; -use Appwrite\Database\Adapter\Redis as RedisAdapter; -use Appwrite\Database\Database; -use Appwrite\Database\Document; -use Appwrite\Database\Validator\Authorization; use Appwrite\Network\Validator\Origin; use Appwrite\Realtime\Realtime; -use Swoole\Database\RedisConfig; -use Swoole\Database\RedisPool; +use Appwrite\Utopia\Response; use Swoole\WebSocket\Server; use Swoole\Http\Request; +use Swoole\Http\Response as SwooleResponse; use Swoole\Process; use Swoole\WebSocket\Frame; use Utopia\App; use Utopia\CLI\Console; -use Utopia\Config\Config; use Utopia\Swoole\Request as SwooleRequest; use Utopia\Abuse\Abuse; use Utopia\Abuse\Adapters\TimeLimit; @@ -45,41 +38,14 @@ $server->set([ $subscriptions = []; $connections = []; -/** - * Create Redis Connection Pool in favor of the default 'cache' register. - */ -$register->set('redis', function () { - $user = App::getEnv('_APP_REDIS_USER', ''); - $pass = App::getEnv('_APP_REDIS_PASS', ''); - $auth = ''; - if (!empty($user)) { - $auth += $user; - } - if (!empty($pass)) { - $auth += ':' . $pass; - } - - $config = new RedisConfig(); - $config - ->withHost(App::getEnv('_APP_REDIS_HOST', '')) - ->withPort(App::getEnv('_APP_REDIS_PORT', '')) - ->withAuth($auth) - ->withTimeout(0) - ->withReadTimeout(0) - ->withRetryInterval(0); - - - $pool = new RedisPool($config); - - return $pool; -}); - $server->on('workerStart', function ($server, $workerId) use (&$subscriptions, &$connections, &$register) { Console::success('Worker ' . ++$workerId . ' started succefully'); - + $attempts = 0; $start = time(); + // $register->context('realtime-' . $workerId); + while ($attempts < 300) { try { if ($attempts > 0) { @@ -88,7 +54,7 @@ $server->on('workerStart', function ($server, $workerId) use (&$subscriptions, & sleep(5); // 5 sec delay between connection attempts } - $redis = $register->get('redis')->get(); + $redis = $register->get('cache', true); $redis->setOption(Redis::OPT_READ_TIMEOUT, -1); if ($redis->ping(true)) { @@ -163,63 +129,9 @@ $server->on('open', function (Server $server, Request $request) use (&$connectio return $request; }); - App::setResource('consoleDB', function () use (&$register) { - $consoleDB = new Database(); - $consoleDB->setAdapter(new RedisAdapter(new MySQLAdapter($register), $register, true)); - $consoleDB->setNamespace('app_console'); // Should be replaced with param if we want to have parent projects - $consoleDB->setMocks(Config::getParam('collections', [])); - - return $consoleDB; - }, ['register']); - - App::setResource('project', function ($consoleDB, $request) { - /** @var Utopia\Swoole\Request $request */ - /** @var Appwrite\Database\Database $consoleDB */ - - Authorization::disable(); - - $project = $consoleDB->getDocument($request->getQuery('project')); - - Authorization::reset(); - - return $project; - }, ['consoleDB', 'request']); - - App::setResource('console', function ($consoleDB) { - return $consoleDB->getDocument('console'); - }, ['consoleDB']); - - App::setResource('user', function ($project, $request, $projectDB) { - /** @var Utopia\Swoole\Request $request */ - /** @var Appwrite\Database\Document $project */ - /** @var Appwrite\Database\Database $projectDB */ - - Authorization::setDefaultStatus(true); - - Auth::setCookieName('a_session_' . $project->getId()); - - $session = Auth::decodeSession( - $request->getCookie( - Auth::$cookieName, // Get sessions - $request->getCookie(Auth::$cookieName . '_legacy', '') - ) - ); // Get fallback session from old clients (no SameSite support) - - 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::tokenVerify($user->getAttribute('tokens', []), Auth::TOKEN_TYPE_LOGIN, Auth::$secret) - ) { // Validate user has valid login token - $user = new Document(['$id' => '', '$collection' => Database::SYSTEM_COLLECTION_USERS]); - } - - return $user; - }, ['project', 'request', 'projectDB']); + App::setResource('response', function () { + return new Response(new SwooleResponse()); + }); /** @var Appwrite\Database\Document $user */ $user = $app->getResource('user'); diff --git a/src/Appwrite/Database/Adapter/Redis.php b/src/Appwrite/Database/Adapter/Redis.php index 3c5cdb5030..a1e440112d 100644 --- a/src/Appwrite/Database/Adapter/Redis.php +++ b/src/Appwrite/Database/Adapter/Redis.php @@ -19,23 +19,16 @@ class Redis extends Adapter */ protected $adapter; - /** - * @var bool - */ - protected $isPool = false; - /** * Redis constructor. * * @param Adapter $adapter * @param Registry $register - * @param bool $isPool */ - public function __construct(Adapter $adapter, Registry $register, bool $isPool = false) + public function __construct(Adapter $adapter, Registry $register) { $this->register = $register; $this->adapter = $adapter; - $this->isPool = $isPool; } /** @@ -268,7 +261,7 @@ class Redis extends Adapter */ protected function getRedis(): Client { - return $this->isPool ? $this->register->get('redis')->get() : $this->register->get('cache'); + return $this->register->get('cache'); } /** From a5b70e9476d289c6fe8e955b2e31451cceafcafa Mon Sep 17 00:00:00 2001 From: Eldad Fux Date: Tue, 9 Mar 2021 19:27:48 +0200 Subject: [PATCH 056/267] Fixed registry contex --- app/realtime.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/realtime.php b/app/realtime.php index b1c34266ca..3030837e73 100644 --- a/app/realtime.php +++ b/app/realtime.php @@ -39,12 +39,12 @@ $subscriptions = []; $connections = []; $server->on('workerStart', function ($server, $workerId) use (&$subscriptions, &$connections, &$register) { - Console::success('Worker ' . ++$workerId . ' started succefully'); + Console::success('Worker ' . $workerId . ' started succefully'); $attempts = 0; $start = time(); - // $register->context('realtime-' . $workerId); + $register->context('realtime-pubsub-' . $workerId); while ($attempts < 300) { try { @@ -121,6 +121,8 @@ $server->on('start', function (Server $server) { $server->on('open', function (Server $server, Request $request) use (&$connections, &$subscriptions, &$register) { Console::info("Connection open (user: {$request->fd}, connections: {}, worker: {$server->getWorkerId()})"); + $register->context('realtime-' . $server->getWorkerId()); + $app = new App(''); $connection = $request->fd; $request = new SwooleRequest($request); From af086b67c2598212652e4be10ca865b560e9d9c4 Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Tue, 9 Mar 2021 18:47:13 +0100 Subject: [PATCH 057/267] add k6 run --- tests/benchmarks/ws.js | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 tests/benchmarks/ws.js diff --git a/tests/benchmarks/ws.js b/tests/benchmarks/ws.js new file mode 100644 index 0000000000..729499471b --- /dev/null +++ b/tests/benchmarks/ws.js @@ -0,0 +1,33 @@ +// 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: '20s', target: 10 }, + { duration: '20s', target: 100 }, + { duration: '20s', target: 0 }, + ], +} + +export default function () { + const url = new URL('ws://localhost/v1/realtime'); + url.searchParams.append('project', '60479fe35d95d'); + url.searchParams.append('channels[]', 'files'); + const res = ws.connect(url.toString(), function (socket) { + socket.on('open', () => { + console.log('connected') + }); + socket.on('message', (data) => { + console.log('Message received: ', data) + }); + socket.on('close', () => console.log('disconnected')); + socket.setTimeout(function () { + console.log('2 seconds passed, closing the socket'); + socket.close(); + }, 2000); + }); + check(res, { 'status is 101': (r) => r && r.status === 101 }); +} \ No newline at end of file From 66099e06bec06575a34259c98fc8744f979cb3b5 Mon Sep 17 00:00:00 2001 From: Eldad Fux Date: Wed, 10 Mar 2021 08:52:10 +0200 Subject: [PATCH 058/267] Updated benchmarks --- tests/benchmarks/http.js | 29 +++++++++++++++++++++++++++++ tests/benchmarks/ws.js | 11 ++++++++--- 2 files changed, 37 insertions(+), 3 deletions(-) create mode 100644 tests/benchmarks/http.js diff --git a/tests/benchmarks/http.js b/tests/benchmarks/http.js new file mode 100644 index 0000000000..a73f8d5633 --- /dev/null +++ b/tests/benchmarks/http.js @@ -0,0 +1,29 @@ +import http from 'k6/http'; +import { sleep, 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 res = http.get('http://localhost:9501/v1/health/version?project=console'); + + const checkRes = check(res, { + 'status is 200': (r) => r.status === 200, + 'response body': (r) => r.body.indexOf('0.7.0') !== -1, + }); +} \ No newline at end of file diff --git a/tests/benchmarks/ws.js b/tests/benchmarks/ws.js index 729499471b..34cbfb0a3b 100644 --- a/tests/benchmarks/ws.js +++ b/tests/benchmarks/ws.js @@ -9,25 +9,30 @@ export let options = { { duration: '20s', target: 10 }, { duration: '20s', target: 100 }, { duration: '20s', target: 0 }, - ], + ], } export default function () { const url = new URL('ws://localhost/v1/realtime'); - url.searchParams.append('project', '60479fe35d95d'); + url.searchParams.append('project', '60479391b1c3f'); url.searchParams.append('channels[]', 'files'); + const res = ws.connect(url.toString(), function (socket) { socket.on('open', () => { console.log('connected') }); + socket.on('message', (data) => { console.log('Message received: ', data) }); + socket.on('close', () => console.log('disconnected')); + socket.setTimeout(function () { console.log('2 seconds passed, closing the socket'); socket.close(); - }, 2000); + }, 2000); }); + check(res, { 'status is 101': (r) => r && r.status === 101 }); } \ No newline at end of file From b26b831931b137e42948998beee5463947284281 Mon Sep 17 00:00:00 2001 From: Eldad Fux Date: Wed, 10 Mar 2021 08:53:49 +0200 Subject: [PATCH 059/267] Removed context, added pools --- app/http.php | 6 --- app/init.php | 86 ++++++++++++++++++++++---------- app/realtime.php | 7 +-- composer.json | 2 +- composer.lock | 124 +++++++++++++++++++++-------------------------- 5 files changed, 118 insertions(+), 107 deletions(-) diff --git a/app/http.php b/app/http.php index 9e45bc3b52..1992635fd2 100644 --- a/app/http.php +++ b/app/http.php @@ -15,12 +15,6 @@ use Utopia\CLI\Console; // xdebug_start_trace('/tmp/trace'); -ini_set('memory_limit','512M'); -ini_set('display_errors', 1); -ini_set('display_startup_errors', 1); -ini_set('default_socket_timeout', -1); -error_reporting(E_ALL); - $http = new Server("0.0.0.0", App::getEnv('PORT', 80)); $payloadSize = max(4000000 /* 4mb */, App::getEnv('_APP_STORAGE_LIMIT', 10000000 /* 10mb */)); diff --git a/app/init.php b/app/init.php index 98ff4c9235..39fd08494b 100644 --- a/app/init.php +++ b/app/init.php @@ -11,6 +11,12 @@ if (\file_exists(__DIR__.'/../vendor/autoload.php')) { require_once __DIR__.'/../vendor/autoload.php'; } +ini_set('memory_limit','512M'); +ini_set('display_errors', 1); +ini_set('display_startup_errors', 1); +ini_set('default_socket_timeout', -1); +error_reporting(E_ALL); + use Ahc\Jwt\JWT; use Ahc\Jwt\JWTException; use Appwrite\Auth\Auth; @@ -21,7 +27,6 @@ use Appwrite\Database\Document; use Appwrite\Database\Validator\Authorization; use Appwrite\Event\Event; use Appwrite\Event\Realtime; -use Appwrite\Extend\PDO; use Appwrite\OpenSSL\OpenSSL; use Utopia\App; use Utopia\View; @@ -31,6 +36,13 @@ use Utopia\Registry\Registry; use MaxMind\Db\Reader; use PHPMailer\PHPMailer\PHPMailer; use PDO as PDONative; +use Swoole\Runtime; +use Swoole\Database\PDOConfig; +use Swoole\Database\PDOPool; +use Swoole\Database\RedisConfig; +use Swoole\Database\RedisPool; + +Runtime::enableCoroutine(SWOOLE_HOOK_ALL); const APP_NAME = 'Appwrite'; const APP_DOMAIN = 'appwrite.io'; @@ -139,24 +151,35 @@ Database::addFilter('encrypt', /* * Registry */ -$register->set('db', function () { // Register DB connection - $dbHost = App::getEnv('_APP_DB_HOST', ''); - $dbUser = App::getEnv('_APP_DB_USER', ''); - $dbPass = App::getEnv('_APP_DB_PASS', ''); - $dbScheme = App::getEnv('_APP_DB_SCHEMA', ''); +$register->set('dbPool', function () { // Register DB connection + $config = new PDOConfig(); + $config + ->withHost(App::getEnv('_APP_DB_HOST', '')) + ->withPort(App::getEnv('_APP_DB_PORT', '')) + ->withDbName(App::getEnv('_APP_DB_SCHEMA', '')) + ->withUsername(App::getEnv('_APP_DB_USER', '')) + ->withPassword(App::getEnv('_APP_DB_PASS', '')) + ->withCharset('utf8mb4') + ->withOptions([ + PDONative::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8mb4', + PDONative::ATTR_TIMEOUT => 3, // Seconds + PDONative::ATTR_PERSISTENT => true + ]); - $pdo = new PDO("mysql:host={$dbHost};dbname={$dbScheme};charset=utf8mb4", $dbUser, $dbPass, array( - PDONative::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8mb4', - PDONative::ATTR_TIMEOUT => 3, // Seconds - PDONative::ATTR_PERSISTENT => true - )); + $pool = new PDOPool($config); + + return $pool; +}); +$register->set('db', function () use ($register) { + $pool = $register->get('dbPool'); + $pdo = $pool->get()->__getObject(); // Connection settings $pdo->setAttribute(PDONative::ATTR_DEFAULT_FETCH_MODE, PDONative::FETCH_ASSOC); // Return arrays $pdo->setAttribute(PDONative::ATTR_ERRMODE, PDONative::ERRMODE_EXCEPTION); // Handle all errors with exceptions return $pdo; -}); +}, true); $register->set('influxdb', function () { // Register DB connection $host = App::getEnv('_APP_INFLUXDB_HOST', ''); $port = App::getEnv('_APP_INFLUXDB_PORT', ''); @@ -178,25 +201,36 @@ $register->set('statsd', function () { // Register DB connection return $statsd; }); -$register->set('cache', function () { // Register cache connection - $redis = new Redis(); - $redis->pconnect(App::getEnv('_APP_REDIS_HOST', ''), App::getEnv('_APP_REDIS_PORT', '')); - $user = App::getEnv('_APP_REDIS_USER',''); - $pass = App::getEnv('_APP_REDIS_PASS',''); - $auth = []; - if(!empty($user)) { - $auth["user"] = $user; +$register->set('redisPool', function () { + $user = App::getEnv('_APP_REDIS_USER', ''); + $pass = App::getEnv('_APP_REDIS_PASS', ''); + $auth = ''; + if (!empty($user)) { + $auth += $user; } - if(!empty($pass)) { - $auth["pass"] = $pass; - } - if(!empty($auth)) { - $redis->auth($auth); + if (!empty($pass)) { + $auth += ':' . $pass; } + + $config = new RedisConfig(); + $config + ->withHost(App::getEnv('_APP_REDIS_HOST', '')) + ->withPort(App::getEnv('_APP_REDIS_PORT', '')) + ->withAuth($auth) + ->withTimeout(0) + ->withReadTimeout(0) + ->withRetryInterval(0); + + $pool = new RedisPool($config); + + return $pool; +}); +$register->set('cache', function () use ($register) { // Register cache connection + $redis = $register->get('redisPool')->get(); $redis->setOption(Redis::OPT_READ_TIMEOUT, -1); return $redis; -}); +}, true); $register->set('smtp', function () { $mail = new PHPMailer(true); diff --git a/app/realtime.php b/app/realtime.php index 3030837e73..a865213d64 100644 --- a/app/realtime.php +++ b/app/realtime.php @@ -44,8 +44,6 @@ $server->on('workerStart', function ($server, $workerId) use (&$subscriptions, & $attempts = 0; $start = time(); - $register->context('realtime-pubsub-' . $workerId); - while ($attempts < 300) { try { if ($attempts > 0) { @@ -54,8 +52,7 @@ $server->on('workerStart', function ($server, $workerId) use (&$subscriptions, & sleep(5); // 5 sec delay between connection attempts } - $redis = $register->get('cache', true); - $redis->setOption(Redis::OPT_READ_TIMEOUT, -1); + $redis = $register->get('cache'); if ($redis->ping(true)) { $attempts = 0; @@ -121,8 +118,6 @@ $server->on('start', function (Server $server) { $server->on('open', function (Server $server, Request $request) use (&$connections, &$subscriptions, &$register) { Console::info("Connection open (user: {$request->fd}, connections: {}, worker: {$server->getWorkerId()})"); - $register->context('realtime-' . $server->getWorkerId()); - $app = new App(''); $connection = $request->fd; $request = new SwooleRequest($request); diff --git a/composer.json b/composer.json index b4370e7c2f..f5630ae785 100644 --- a/composer.json +++ b/composer.json @@ -42,7 +42,7 @@ "utopia-php/cli": "0.10.0", "utopia-php/config": "0.2.*", "utopia-php/locale": "0.3.*", - "utopia-php/registry": "0.2.*", + "utopia-php/registry": "0.4.*", "utopia-php/preloader": "0.2.*", "utopia-php/domains": "0.2.*", "utopia-php/swoole": "0.2.*", diff --git a/composer.lock b/composer.lock index 85b0932c93..c29f12b4b3 100644 --- a/composer.lock +++ b/composer.lock @@ -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": "4f58de92fb64af44d915387895472881", + "content-hash": "4159791237c3f67d8a3d9df77513bcd0", "packages": [ { "name": "adhocore/jwt", @@ -349,7 +349,7 @@ "issues": "https://github.com/domnikl/statsd-php/issues", "source": "https://github.com/domnikl/statsd-php/tree/master" }, - "abandoned": true, + "abandoned": "slickdeals/statsd", "time": "2020-01-03T14:24:58+00:00" }, { @@ -521,12 +521,12 @@ "source": { "type": "git", "url": "https://github.com/guzzle/promises.git", - "reference": "ddfeedfff2a52661429437da0702979f708e6ac6" + "reference": "8e7d04f1f6450fef59366c399cfad4b9383aa30d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/promises/zipball/ddfeedfff2a52661429437da0702979f708e6ac6", - "reference": "ddfeedfff2a52661429437da0702979f708e6ac6", + "url": "https://api.github.com/repos/guzzle/promises/zipball/8e7d04f1f6450fef59366c399cfad4b9383aa30d", + "reference": "8e7d04f1f6450fef59366c399cfad4b9383aa30d", "shasum": "" }, "require": { @@ -567,9 +567,9 @@ ], "support": { "issues": "https://github.com/guzzle/promises/issues", - "source": "https://github.com/guzzle/promises/tree/master" + "source": "https://github.com/guzzle/promises/tree/1.4.1" }, - "time": "2020-10-19T16:50:15+00:00" + "time": "2021-03-07T09:25:29+00:00" }, { "name": "guzzlehttp/psr7", @@ -577,12 +577,12 @@ "source": { "type": "git", "url": "https://github.com/guzzle/psr7.git", - "reference": "f47ece9e6e8ce74e3be04bef47f46061dc18c095" + "reference": "d7fe0a0eabc266c3dcf2f20aa12121044ff196a4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/psr7/zipball/f47ece9e6e8ce74e3be04bef47f46061dc18c095", - "reference": "f47ece9e6e8ce74e3be04bef47f46061dc18c095", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/d7fe0a0eabc266c3dcf2f20aa12121044ff196a4", + "reference": "d7fe0a0eabc266c3dcf2f20aa12121044ff196a4", "shasum": "" }, "require": { @@ -644,7 +644,7 @@ "issues": "https://github.com/guzzle/psr7/issues", "source": "https://github.com/guzzle/psr7/tree/1.x" }, - "time": "2020-12-08T11:45:39+00:00" + "time": "2021-03-09T14:42:40+00:00" }, { "name": "influxdb/influxdb-php", @@ -1020,12 +1020,12 @@ "source": { "type": "git", "url": "https://github.com/php-fig/log.git", - "reference": "dd738d0b4491f32725492cf345f6b501f5922fec" + "reference": "a18c1e692e02b84abbafe4856c3cd7cc6903908c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/log/zipball/dd738d0b4491f32725492cf345f6b501f5922fec", - "reference": "dd738d0b4491f32725492cf345f6b501f5922fec", + "url": "https://api.github.com/repos/php-fig/log/zipball/a18c1e692e02b84abbafe4856c3cd7cc6903908c", + "reference": "a18c1e692e02b84abbafe4856c3cd7cc6903908c", "shasum": "" }, "require": { @@ -1063,7 +1063,7 @@ "support": { "source": "https://github.com/php-fig/log/tree/master" }, - "time": "2020-09-18T06:44:51+00:00" + "time": "2021-03-02T15:02:34+00:00" }, { "name": "ralouphie/getallheaders", @@ -1850,20 +1850,20 @@ }, { "name": "utopia-php/registry", - "version": "0.2.4", + "version": "0.3.0", "source": { "type": "git", "url": "https://github.com/utopia-php/registry.git", - "reference": "428a94f1a36147e7b7221e778c01e1be08db2893" + "reference": "8b05fe6f8af73bf77e1700212a28f36be6733d3a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/registry/zipball/428a94f1a36147e7b7221e778c01e1be08db2893", - "reference": "428a94f1a36147e7b7221e778c01e1be08db2893", + "url": "https://api.github.com/repos/utopia-php/registry/zipball/8b05fe6f8af73bf77e1700212a28f36be6733d3a", + "reference": "8b05fe6f8af73bf77e1700212a28f36be6733d3a", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=7.4" }, "require-dev": { "phpunit/phpunit": "^9.3", @@ -1896,27 +1896,27 @@ ], "support": { "issues": "https://github.com/utopia-php/registry/issues", - "source": "https://github.com/utopia-php/registry/tree/0.2.4" + "source": "https://github.com/utopia-php/registry/tree/0.3.0" }, - "time": "2020-10-24T08:51:37+00:00" + "time": "2021-03-09T17:38:40+00:00" }, { "name": "utopia-php/storage", - "version": "0.4.1", + "version": "0.4.3", "source": { "type": "git", "url": "https://github.com/utopia-php/storage.git", - "reference": "86f749f2d79268528732e560f77dde0155e162ca" + "reference": "9db3ab713a6d392c3c2c799aeea751f6c8dc2ff7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/storage/zipball/86f749f2d79268528732e560f77dde0155e162ca", - "reference": "86f749f2d79268528732e560f77dde0155e162ca", + "url": "https://api.github.com/repos/utopia-php/storage/zipball/9db3ab713a6d392c3c2c799aeea751f6c8dc2ff7", + "reference": "9db3ab713a6d392c3c2c799aeea751f6c8dc2ff7", "shasum": "" }, "require": { "php": ">=7.4", - "utopia-php/framework": "0.10.0" + "utopia-php/framework": "0.*.*" }, "require-dev": { "phpunit/phpunit": "^9.3", @@ -1948,9 +1948,9 @@ ], "support": { "issues": "https://github.com/utopia-php/storage/issues", - "source": "https://github.com/utopia-php/storage/tree/0.4.1" + "source": "https://github.com/utopia-php/storage/tree/0.4.3" }, - "time": "2021-02-19T05:04:44+00:00" + "time": "2021-03-02T20:25:02+00:00" }, { "name": "utopia-php/swoole", @@ -2065,20 +2065,20 @@ }, { "name": "webmozart/assert", - "version": "dev-master", + "version": "1.9.1", "source": { "type": "git", "url": "https://github.com/webmozarts/assert.git", - "reference": "9c89b265ccc4092d58e66d72af5d343ee77a41ae" + "reference": "bafc69caeb4d49c39fd0779086c03a3738cbb389" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/webmozarts/assert/zipball/9c89b265ccc4092d58e66d72af5d343ee77a41ae", - "reference": "9c89b265ccc4092d58e66d72af5d343ee77a41ae", + "url": "https://api.github.com/repos/webmozarts/assert/zipball/bafc69caeb4d49c39fd0779086c03a3738cbb389", + "reference": "bafc69caeb4d49c39fd0779086c03a3738cbb389", "shasum": "" }, "require": { - "php": "^7.2 || ^8.0", + "php": "^5.3.3 || ^7.0 || ^8.0", "symfony/polyfill-ctype": "^1.8" }, "conflict": { @@ -2086,15 +2086,9 @@ "vimeo/psalm": "<3.9.1" }, "require-dev": { - "phpunit/phpunit": "^8.5.13" + "phpunit/phpunit": "^4.8.36 || ^7.5.13" }, - "default-branch": true, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.10-dev" - } - }, "autoload": { "psr-4": { "Webmozart\\Assert\\": "src/" @@ -2118,9 +2112,9 @@ ], "support": { "issues": "https://github.com/webmozarts/assert/issues", - "source": "https://github.com/webmozarts/assert/tree/master" + "source": "https://github.com/webmozarts/assert/tree/1.9.1" }, - "time": "2021-01-18T12:52:36+00:00" + "time": "2020-07-08T17:02:28+00:00" } ], "packages-dev": [ @@ -3279,12 +3273,12 @@ "source": { "type": "git", "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", - "reference": "e3324ecbde7319b0bbcf0fd7ca4af19469c38da9" + "reference": "f8d350d8514ff60b5993dd0121c62299480c989c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/e3324ecbde7319b0bbcf0fd7ca4af19469c38da9", - "reference": "e3324ecbde7319b0bbcf0fd7ca4af19469c38da9", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/f8d350d8514ff60b5993dd0121c62299480c989c", + "reference": "f8d350d8514ff60b5993dd0121c62299480c989c", "shasum": "" }, "require": { @@ -3328,7 +3322,7 @@ "issues": "https://github.com/phpDocumentor/ReflectionDocBlock/issues", "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/master" }, - "time": "2020-11-18T14:27:38+00:00" + "time": "2021-03-07T11:12:25+00:00" }, { "name": "phpdocumentor/type-resolver", @@ -3874,28 +3868,22 @@ }, { "name": "psr/container", - "version": "dev-master", + "version": "1.1.x-dev", "source": { "type": "git", "url": "https://github.com/php-fig/container.git", - "reference": "381524e8568e07f31d504a945b88556548c8c42e" + "reference": "8622567409010282b7aeebe4bb841fe98b58dcaf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/container/zipball/381524e8568e07f31d504a945b88556548c8c42e", - "reference": "381524e8568e07f31d504a945b88556548c8c42e", + "url": "https://api.github.com/repos/php-fig/container/zipball/8622567409010282b7aeebe4bb841fe98b58dcaf", + "reference": "8622567409010282b7aeebe4bb841fe98b58dcaf", "shasum": "" }, "require": { "php": ">=7.2.0" }, - "default-branch": true, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.1.x-dev" - } - }, "autoload": { "psr-4": { "Psr\\Container\\": "src/" @@ -3922,9 +3910,9 @@ ], "support": { "issues": "https://github.com/php-fig/container/issues", - "source": "https://github.com/php-fig/container/tree/master" + "source": "https://github.com/php-fig/container/tree/1.1.x" }, - "time": "2020-10-13T07:07:53+00:00" + "time": "2021-03-05T17:36:06+00:00" }, { "name": "sebastian/cli-parser", @@ -4946,12 +4934,12 @@ "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "c08d7d0d458eceb62996d81d3be8d9fbf5564ec4" + "reference": "4e102e4de39852a1dcd3b2169d263b88afee7fff" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/c08d7d0d458eceb62996d81d3be8d9fbf5564ec4", - "reference": "c08d7d0d458eceb62996d81d3be8d9fbf5564ec4", + "url": "https://api.github.com/repos/symfony/console/zipball/4e102e4de39852a1dcd3b2169d263b88afee7fff", + "reference": "4e102e4de39852a1dcd3b2169d263b88afee7fff", "shasum": "" }, "require": { @@ -5036,7 +5024,7 @@ "type": "tidelift" } ], - "time": "2021-02-23T10:10:15+00:00" + "time": "2021-03-08T21:52:55+00:00" }, { "name": "symfony/polyfill-intl-grapheme", @@ -5456,17 +5444,17 @@ "source": { "type": "git", "url": "https://github.com/symfony/service-contracts.git", - "reference": "e830e6ceebd6377b019e4c9a523d6f2c27007e4a" + "reference": "96cd360b9f03a22a30cf5354e630c557bd3aac33" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/e830e6ceebd6377b019e4c9a523d6f2c27007e4a", - "reference": "e830e6ceebd6377b019e4c9a523d6f2c27007e4a", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/96cd360b9f03a22a30cf5354e630c557bd3aac33", + "reference": "96cd360b9f03a22a30cf5354e630c557bd3aac33", "shasum": "" }, "require": { "php": ">=7.2.5", - "psr/container": "^1.0" + "psr/container": "^1.1" }, "suggest": { "symfony/service-implementation": "" @@ -5528,7 +5516,7 @@ "type": "tidelift" } ], - "time": "2021-02-25T16:38:04+00:00" + "time": "2021-03-05T22:51:52+00:00" }, { "name": "symfony/string", From b75d33efc116db6bc7da3c6b20862d1dff28cb84 Mon Sep 17 00:00:00 2001 From: Eldad Fux Date: Wed, 10 Mar 2021 09:43:10 +0200 Subject: [PATCH 060/267] Cleanups --- app/init.php | 2 -- app/realtime.php | 6 ++---- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/app/init.php b/app/init.php index 39fd08494b..5f7dddba38 100644 --- a/app/init.php +++ b/app/init.php @@ -42,8 +42,6 @@ use Swoole\Database\PDOPool; use Swoole\Database\RedisConfig; use Swoole\Database\RedisPool; -Runtime::enableCoroutine(SWOOLE_HOOK_ALL); - const APP_NAME = 'Appwrite'; const APP_DOMAIN = 'appwrite.io'; const APP_EMAIL_TEAM = 'team@localhost.test'; // Default email address diff --git a/app/realtime.php b/app/realtime.php index a865213d64..f19a5cd2a8 100644 --- a/app/realtime.php +++ b/app/realtime.php @@ -1,16 +1,15 @@ Date: Wed, 10 Mar 2021 10:08:17 +0200 Subject: [PATCH 061/267] Test worker wrapers --- app/workers/audits.php | 9 ++++--- app/workers/certificates.php | 10 ++++---- app/workers/deletes.php | 11 ++++----- app/workers/functions.php | 17 +++++++------ app/workers/mails.php | 10 ++++---- app/workers/tasks.php | 16 ++++++------ app/workers/usage.php | 10 ++++---- app/workers/webhooks.php | 10 ++++---- src/Appwrite/Resque/Worker.php | 45 ++++++++++++++++++++++++++++++++++ 9 files changed, 92 insertions(+), 46 deletions(-) create mode 100644 src/Appwrite/Resque/Worker.php diff --git a/app/workers/audits.php b/app/workers/audits.php index d6027dd2e3..13af7e828e 100644 --- a/app/workers/audits.php +++ b/app/workers/audits.php @@ -1,5 +1,6 @@ log($userId, $event, $resource, $userAgent, $ip, '', $data); } - public function tearDown(): void + public function shutdown(): void { // ... Remove environment for this job } diff --git a/app/workers/certificates.php b/app/workers/certificates.php index e8693e3566..6d08d2183d 100644 --- a/app/workers/certificates.php +++ b/app/workers/certificates.php @@ -9,6 +9,7 @@ use Appwrite\Database\Adapter\MySQL as MySQLAdapter; use Appwrite\Database\Adapter\Redis as RedisAdapter; use Appwrite\Database\Validator\Authorization; use Appwrite\Network\Validator\CNAME; +use Appwrite\Resque\Worker; require_once __DIR__.'/../init.php'; @@ -16,15 +17,15 @@ Console::title('Certificates V1 Worker'); Console::success(APP_NAME.' certificates worker v1 has started'); -class CertificatesV1 +class CertificatesV1 extends Worker { public $args = []; - public function setUp(): void + public function init(): void { } - public function perform() + public function execute(): void { global $register; @@ -204,8 +205,7 @@ class CertificatesV1 Authorization::reset(); } - public function tearDown(): void + public function shutdown(): void { - // ... Remove environment for this job } } diff --git a/app/workers/deletes.php b/app/workers/deletes.php index 426dcf1ed6..0478f7661a 100644 --- a/app/workers/deletes.php +++ b/app/workers/deletes.php @@ -5,6 +5,7 @@ use Appwrite\Database\Adapter\MySQL as MySQLAdapter; use Appwrite\Database\Adapter\Redis as RedisAdapter; use Appwrite\Database\Document; use Appwrite\Database\Validator\Authorization; +use Appwrite\Resque\Worker; use Utopia\Storage\Device\Local; use Utopia\Abuse\Abuse; use Utopia\Abuse\Adapters\TimeLimit; @@ -19,18 +20,17 @@ Console::title('Deletes V1 Worker'); Console::success(APP_NAME.' deletes worker v1 has started'."\n"); -class DeletesV1 +class DeletesV1 extends Worker { - public $args = []; protected $consoleDB = null; - public function setUp(): void + public function init(): void { } - public function perform() + public function execute(): void { $projectId = $this->args['projectId']; $type = $this->args['type']; @@ -82,9 +82,8 @@ class DeletesV1 } } - public function tearDown(): void + public function shutdown(): void { - // ... Remove environment for this job } protected function deleteDocuments(Document $document, $projectId) diff --git a/app/workers/functions.php b/app/workers/functions.php index 87e3a86245..707e6960fe 100644 --- a/app/workers/functions.php +++ b/app/workers/functions.php @@ -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\Resque\Worker; use Cron\CronExpression; use Swoole\Runtime; use Utopia\App; @@ -127,17 +128,17 @@ Console::info(count($list)." functions listed in " . ($executionEnd - $execution //TODO aviod scheduled execution if delay is bigger than X offest -class FunctionsV1 +class FunctionsV1 extends Worker { public $args = []; public $allowed = []; - public function setUp(): void + public function init(): void { } - public function perform() + public function execute(): void { global $register; @@ -195,7 +196,7 @@ class FunctionsV1 Console::success('Triggered function: '.$event); - $this->execute('event', $projectId, '', $database, $function, $event, $payload); + $this->run('event', $projectId, '', $database, $function, $event, $payload); } } break; @@ -251,7 +252,7 @@ class FunctionsV1 'scheduleOriginal' => $function->getAttribute('schedule', ''), ]); // Async task rescheduale - $this->execute($trigger, $projectId, $executionId, $database, $function); + $this->run($trigger, $projectId, $executionId, $database, $function); break; @@ -264,7 +265,7 @@ class FunctionsV1 throw new Exception('Function not found ('.$functionId.')'); } - $this->execute($trigger, $projectId, $executionId, $database, $function); + $this->run($trigger, $projectId, $executionId, $database, $function); break; default: @@ -286,7 +287,7 @@ class FunctionsV1 * * @return void */ - public function execute(string $trigger, string $projectId, string $executionId, Database $database, Document $function, string $event = '', string $payload = ''): void + public function run(string $trigger, string $projectId, string $executionId, Database $database, Document $function, string $event = '', string $payload = ''): void { global $list; @@ -548,7 +549,7 @@ class FunctionsV1 return $output; } - public function tearDown(): void + public function shutdown(): void { } } diff --git a/app/workers/mails.php b/app/workers/mails.php index 6f4422b57f..50e499f901 100644 --- a/app/workers/mails.php +++ b/app/workers/mails.php @@ -1,5 +1,6 @@ getAttribute('updated') !== $updated) { // Task have already been rescheduled by owner - return false; + return; } if ($task->getAttribute('status') !== 'play') { // Skip task and don't schedule again - return false; + return; } // Reschedule @@ -202,11 +203,10 @@ class TasksV1 // Send alert if needed (use SMTP as default for now) - return true; + return; } - public function tearDown(): void + public function shutdown(): void { - // ... Remove environment for this job } } diff --git a/app/workers/usage.php b/app/workers/usage.php index c83ae7ae30..1d1a5ba5a7 100644 --- a/app/workers/usage.php +++ b/app/workers/usage.php @@ -1,5 +1,6 @@ init(); + }); + } + + public function perform() + { + run(function() { + Runtime::enableCoroutine(SWOOLE_HOOK_ALL); + + $this->execute(); + }); + } + + public function tearDown(): void + { + run(function() { + Runtime::enableCoroutine(SWOOLE_HOOK_ALL); + + $this->shutdown(); + }); + } +} From d41a8ef44b32dbf110ea53958c0117819cb0e85b Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Wed, 10 Mar 2021 10:01:24 +0100 Subject: [PATCH 062/267] add exception to realtime opener --- app/realtime.php | 117 ++++++++++++++++++++++++----------------------- 1 file changed, 59 insertions(+), 58 deletions(-) diff --git a/app/realtime.php b/app/realtime.php index b1c34266ca..2c7656c90f 100644 --- a/app/realtime.php +++ b/app/realtime.php @@ -40,7 +40,7 @@ $connections = []; $server->on('workerStart', function ($server, $workerId) use (&$subscriptions, &$connections, &$register) { Console::success('Worker ' . ++$workerId . ' started succefully'); - + $attempts = 0; $start = time(); @@ -142,65 +142,66 @@ $server->on('open', function (Server $server, Request $request) use (&$connectio /** @var Appwrite\Database\Document $console */ $console = $app->getResource('console'); - /* - * Project Check - */ - if (empty($project->getId())) { - $server->push($connection, 'Missing or unknown project ID'); + try { + /* + * Project Check + */ + if (empty($project->getId())) { + throw new Exception('Missing or unknown project ID', 1008); + } + + /* + * Abuse Check + */ + $timeLimit = new TimeLimit('url:{url},ip:{ip}', 60, 60, function () use ($register) { + return $register->get('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)) { + throw new Exception($originValidator->getDescription(), 1008); + } + + Realtime::setUser($user); + + $roles = Realtime::getRoles(); + $channels = Realtime::parseChannels($request->getQuery('channels', [])); + + /** + * Channels Check + */ + if (empty($channels)) { + throw new Exception('Missing channels', 1008); + } + + Realtime::subscribe($project->getId(), $connection, $roles, $subscriptions, $connections, $channels); + + $server->push($connection, json_encode($channels)); + } catch (\Throwable $th) { + $response = [ + 'code' => $th->getCode(), + 'message' => $th->getMessage() + ]; + $server->push($connection, json_encode($response)); $server->close($connection); - return; } - - /* - * Abuse Check - */ - $timeLimit = new TimeLimit('url:{url},ip:{ip}', 60, 60, function () use ($register) { - return $register->get('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') { - $server->push($connection, 'Too many requests'); - $server->close($connection); - return; - } - - /* - * 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)) { - $server->push($connection, $originValidator->getDescription()); - $server->close($connection); - return; - } - - Realtime::setUser($user); - - $roles = Realtime::getRoles(); - $channels = Realtime::parseChannels($request->getQuery('channels', [])); - - /** - * Channels Check - */ - if (empty($channels)) { - $server->push($connection, 'Missing channels'); - $server->close($connection); - return; - } - - Realtime::subscribe($project->getId(), $connection, $roles, $subscriptions, $connections, $channels); - - $server->push($connection, json_encode($channels)); }); $server->on('message', function (Server $server, Frame $frame) { From a3402b33f5a036bb0a08ae908873e2c37d4375a7 Mon Sep 17 00:00:00 2001 From: Eldad Fux Date: Wed, 10 Mar 2021 13:26:38 +0200 Subject: [PATCH 063/267] Overwriting registry for realtime --- app/http.php | 34 ++++---- app/init.php | 30 ++++--- app/realtime.php | 14 ++++ composer.json | 2 +- composer.lock | 143 +++++++++++++++++---------------- src/Appwrite/Resque/Worker.php | 18 +---- 6 files changed, 127 insertions(+), 114 deletions(-) diff --git a/app/http.php b/app/http.php index 1992635fd2..d00bbc09eb 100644 --- a/app/http.php +++ b/app/http.php @@ -15,6 +15,24 @@ use Utopia\CLI\Console; // xdebug_start_trace('/tmp/trace'); +Files::load(__DIR__ . '/../public'); + +include __DIR__ . '/controllers/general.php'; + +$domain = App::getEnv('_APP_DOMAIN', ''); + +Console::info('Issuing a TLS certificate for the master domain ('.$domain.') in 30 seconds. + Make sure your domain points to your server IP or restart your Appwrite server to try again.'); // TODO move this to installation script + +ResqueScheduler::enqueueAt(\time() + 30, 'v1-certificates', 'CertificatesV1', [ + 'document' => [], + 'domain' => $domain, + 'validateTarget' => false, + 'validateCNAME' => false, +]); + +Swoole\Runtime::enableCoroutine(SWOOLE_HOOK_ALL); + $http = new Server("0.0.0.0", App::getEnv('PORT', 80)); $payloadSize = max(4000000 /* 4mb */, App::getEnv('_APP_STORAGE_LIMIT', 10000000 /* 10mb */)); @@ -55,22 +73,6 @@ $http->on('start', function (Server $http) use ($payloadSize) { }); }); -Files::load(__DIR__ . '/../public'); - -include __DIR__ . '/controllers/general.php'; - -$domain = App::getEnv('_APP_DOMAIN', ''); - -Console::info('Issuing a TLS certificate for the master domain ('.$domain.') in 30 seconds. - Make sure your domain points to your server IP or restart your Appwrite server to try again.'); // TODO move this to installation script - -ResqueScheduler::enqueueAt(\time() + 30, 'v1-certificates', 'CertificatesV1', [ - 'document' => [], - 'domain' => $domain, - 'validateTarget' => false, - 'validateCNAME' => false, -]); - $http->on('request', function (SwooleRequest $swooleRequest, SwooleResponse $swooleResponse) { $request = new Request($swooleRequest); $response = new Response($swooleResponse); diff --git a/app/init.php b/app/init.php index 5f7dddba38..20a711e7af 100644 --- a/app/init.php +++ b/app/init.php @@ -36,7 +36,6 @@ use Utopia\Registry\Registry; use MaxMind\Db\Reader; use PHPMailer\PHPMailer\PHPMailer; use PDO as PDONative; -use Swoole\Runtime; use Swoole\Database\PDOConfig; use Swoole\Database\PDOPool; use Swoole\Database\RedisConfig; @@ -161,7 +160,9 @@ $register->set('dbPool', function () { // Register DB connection ->withOptions([ PDONative::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8mb4', PDONative::ATTR_TIMEOUT => 3, // Seconds - PDONative::ATTR_PERSISTENT => true + PDONative::ATTR_PERSISTENT => true, + PDONative::ATTR_DEFAULT_FETCH_MODE => PDONative::FETCH_ASSOC, + PDONative::ATTR_ERRMODE => PDONative::ERRMODE_EXCEPTION, ]); $pool = new PDOPool($config); @@ -169,15 +170,21 @@ $register->set('dbPool', function () { // Register DB connection return $pool; }); $register->set('db', function () use ($register) { - $pool = $register->get('dbPool'); - $pdo = $pool->get()->__getObject(); + $dbHost = App::getEnv('_APP_DB_HOST', ''); + $dbUser = App::getEnv('_APP_DB_USER', ''); + $dbPass = App::getEnv('_APP_DB_PASS', ''); + $dbScheme = App::getEnv('_APP_DB_SCHEMA', ''); - // Connection settings - $pdo->setAttribute(PDONative::ATTR_DEFAULT_FETCH_MODE, PDONative::FETCH_ASSOC); // Return arrays - $pdo->setAttribute(PDONative::ATTR_ERRMODE, PDONative::ERRMODE_EXCEPTION); // Handle all errors with exceptions + $pdo = new PDO("mysql:host={$dbHost};dbname={$dbScheme};charset=utf8mb4", $dbUser, $dbPass, array( + PDONative::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8mb4', + PDONative::ATTR_TIMEOUT => 3, // Seconds + PDONative::ATTR_PERSISTENT => true, + PDONative::ATTR_DEFAULT_FETCH_MODE => PDONative::FETCH_ASSOC, + PDONative::ATTR_ERRMODE => PDONative::ERRMODE_EXCEPTION, + )); return $pdo; -}, true); +}); $register->set('influxdb', function () { // Register DB connection $host = App::getEnv('_APP_INFLUXDB_HOST', ''); $port = App::getEnv('_APP_INFLUXDB_PORT', ''); @@ -223,12 +230,13 @@ $register->set('redisPool', function () { return $pool; }); -$register->set('cache', function () use ($register) { // Register cache connection - $redis = $register->get('redisPool')->get(); +$register->set('cache', function () { // Register cache connection + $redis = new Redis(); + $redis->pconnect(App::getEnv('_APP_REDIS_HOST', ''), App::getEnv('_APP_REDIS_PORT', '')); $redis->setOption(Redis::OPT_READ_TIMEOUT, -1); return $redis; -}, true); +}); $register->set('smtp', function () { $mail = new PHPMailer(true); diff --git a/app/realtime.php b/app/realtime.php index f19a5cd2a8..aa7cab8a62 100644 --- a/app/realtime.php +++ b/app/realtime.php @@ -27,6 +27,20 @@ use Utopia\Abuse\Adapters\TimeLimit; Swoole\Runtime::enableCoroutine(SWOOLE_HOOK_ALL); +$register->set('db', function () use ($register) { + $pool = $register->get('dbPool'); + $pdo = $pool->get()->__getObject(); + + return $pdo; +}, true); + +$register->set('cache', function () use ($register) { // Register cache connection + $redis = $register->get('redisPool')->get(); + $redis->setOption(Redis::OPT_READ_TIMEOUT, -1); + + return $redis; +}, true); + $server = new Server('0.0.0.0', 80); $server->set([ diff --git a/composer.json b/composer.json index f5630ae785..765a6b8599 100644 --- a/composer.json +++ b/composer.json @@ -42,7 +42,7 @@ "utopia-php/cli": "0.10.0", "utopia-php/config": "0.2.*", "utopia-php/locale": "0.3.*", - "utopia-php/registry": "0.4.*", + "utopia-php/registry": "master", "utopia-php/preloader": "0.2.*", "utopia-php/domains": "0.2.*", "utopia-php/swoole": "0.2.*", diff --git a/composer.lock b/composer.lock index c29f12b4b3..9cfd4bf6b8 100644 --- a/composer.lock +++ b/composer.lock @@ -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": "4159791237c3f67d8a3d9df77513bcd0", + "content-hash": "b579d6ac34eb119446727d63b21acca2", "packages": [ { "name": "adhocore/jwt", @@ -1850,16 +1850,16 @@ }, { "name": "utopia-php/registry", - "version": "0.3.0", + "version": "dev-master", "source": { "type": "git", "url": "https://github.com/utopia-php/registry.git", - "reference": "8b05fe6f8af73bf77e1700212a28f36be6733d3a" + "reference": "7aebbc6c5f3f04ff7a35ac3dad39fa91c9bd7c2d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/registry/zipball/8b05fe6f8af73bf77e1700212a28f36be6733d3a", - "reference": "8b05fe6f8af73bf77e1700212a28f36be6733d3a", + "url": "https://api.github.com/repos/utopia-php/registry/zipball/7aebbc6c5f3f04ff7a35ac3dad39fa91c9bd7c2d", + "reference": "7aebbc6c5f3f04ff7a35ac3dad39fa91c9bd7c2d", "shasum": "" }, "require": { @@ -1869,6 +1869,7 @@ "phpunit/phpunit": "^9.3", "vimeo/psalm": "4.0.1" }, + "default-branch": true, "type": "library", "autoload": { "psr-4": { @@ -1896,9 +1897,9 @@ ], "support": { "issues": "https://github.com/utopia-php/registry/issues", - "source": "https://github.com/utopia-php/registry/tree/0.3.0" + "source": "https://github.com/utopia-php/registry/tree/0.4.0" }, - "time": "2021-03-09T17:38:40+00:00" + "time": "2021-03-10T06:50:09+00:00" }, { "name": "utopia-php/storage", @@ -3524,12 +3525,12 @@ "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-file-iterator.git", - "reference": "dae425925709122f7584cadeeb838edcaa491bb1" + "reference": "330949c62cbc3e44120990701c949e59a4f3e141" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/dae425925709122f7584cadeeb838edcaa491bb1", - "reference": "dae425925709122f7584cadeeb838edcaa491bb1", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/330949c62cbc3e44120990701c949e59a4f3e141", + "reference": "330949c62cbc3e44120990701c949e59a4f3e141", "shasum": "" }, "require": { @@ -3577,7 +3578,7 @@ "type": "github" } ], - "time": "2021-02-23T15:48:43+00:00" + "time": "2021-03-10T06:29:10+00:00" }, { "name": "phpunit/php-invoker", @@ -3585,12 +3586,12 @@ "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-invoker.git", - "reference": "5ad9e5f5d6ee1a837e1d50bab1017e0daf423b40" + "reference": "fe3276f5cd81d19a8e8ef90a32855545f7aae7cb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/5ad9e5f5d6ee1a837e1d50bab1017e0daf423b40", - "reference": "5ad9e5f5d6ee1a837e1d50bab1017e0daf423b40", + "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/fe3276f5cd81d19a8e8ef90a32855545f7aae7cb", + "reference": "fe3276f5cd81d19a8e8ef90a32855545f7aae7cb", "shasum": "" }, "require": { @@ -3641,7 +3642,7 @@ "type": "github" } ], - "time": "2021-02-23T15:48:51+00:00" + "time": "2021-03-10T06:29:18+00:00" }, { "name": "phpunit/php-text-template", @@ -3649,12 +3650,12 @@ "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-text-template.git", - "reference": "4ec5a2ac79a19b35d0cf83cce30604f77743067a" + "reference": "11d864dc75b7f73d1e03361bff717894587f3987" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/4ec5a2ac79a19b35d0cf83cce30604f77743067a", - "reference": "4ec5a2ac79a19b35d0cf83cce30604f77743067a", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/11d864dc75b7f73d1e03361bff717894587f3987", + "reference": "11d864dc75b7f73d1e03361bff717894587f3987", "shasum": "" }, "require": { @@ -3701,7 +3702,7 @@ "type": "github" } ], - "time": "2021-02-23T15:49:24+00:00" + "time": "2021-03-10T06:29:48+00:00" }, { "name": "phpunit/php-timer", @@ -3709,12 +3710,12 @@ "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-timer.git", - "reference": "705821b0927b5e69e9e016c84de68dc6195c71b9" + "reference": "95242c4aa540e9b3655c7edbe8f76d55ac237b7b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/705821b0927b5e69e9e016c84de68dc6195c71b9", - "reference": "705821b0927b5e69e9e016c84de68dc6195c71b9", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/95242c4aa540e9b3655c7edbe8f76d55ac237b7b", + "reference": "95242c4aa540e9b3655c7edbe8f76d55ac237b7b", "shasum": "" }, "require": { @@ -3761,7 +3762,7 @@ "type": "github" } ], - "time": "2021-02-23T15:48:59+00:00" + "time": "2021-03-10T06:29:26+00:00" }, { "name": "phpunit/phpunit", @@ -3920,12 +3921,12 @@ "source": { "type": "git", "url": "https://github.com/sebastianbergmann/cli-parser.git", - "reference": "3a42d843af4d27ca1155e1d926881af162733655" + "reference": "c8472024d13a267ba49f4c1e194a01cba5b094f5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/3a42d843af4d27ca1155e1d926881af162733655", - "reference": "3a42d843af4d27ca1155e1d926881af162733655", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/c8472024d13a267ba49f4c1e194a01cba5b094f5", + "reference": "c8472024d13a267ba49f4c1e194a01cba5b094f5", "shasum": "" }, "require": { @@ -3969,7 +3970,7 @@ "type": "github" } ], - "time": "2021-02-23T15:49:50+00:00" + "time": "2021-03-10T06:30:16+00:00" }, { "name": "sebastian/code-unit", @@ -4033,12 +4034,12 @@ "source": { "type": "git", "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", - "reference": "5f5db0b35f586eb5bca0581a10bb42dd56575986" + "reference": "84710fb3a027eb62978539705a0cd00713d474c8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/5f5db0b35f586eb5bca0581a10bb42dd56575986", - "reference": "5f5db0b35f586eb5bca0581a10bb42dd56575986", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/84710fb3a027eb62978539705a0cd00713d474c8", + "reference": "84710fb3a027eb62978539705a0cd00713d474c8", "shasum": "" }, "require": { @@ -4081,7 +4082,7 @@ "type": "github" } ], - "time": "2021-02-23T15:47:39+00:00" + "time": "2021-03-10T06:28:05+00:00" }, { "name": "sebastian/comparator", @@ -4089,12 +4090,12 @@ "source": { "type": "git", "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "dbc5fb421f242a5749845dc8dd0dc8cde2979dd9" + "reference": "5dfac003e3be0ca24000cee2a2e19ba2f21aa8f8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/dbc5fb421f242a5749845dc8dd0dc8cde2979dd9", - "reference": "dbc5fb421f242a5749845dc8dd0dc8cde2979dd9", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/5dfac003e3be0ca24000cee2a2e19ba2f21aa8f8", + "reference": "5dfac003e3be0ca24000cee2a2e19ba2f21aa8f8", "shasum": "" }, "require": { @@ -4156,7 +4157,7 @@ "type": "github" } ], - "time": "2021-02-23T15:47:47+00:00" + "time": "2021-03-10T06:28:15+00:00" }, { "name": "sebastian/complexity", @@ -4221,12 +4222,12 @@ "source": { "type": "git", "url": "https://github.com/sebastianbergmann/diff.git", - "reference": "93e6aa13f3dc5f8327e7fb9756e9655fc4c23e90" + "reference": "08ab1620f0f35c41e50d847433193da76d33151e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/93e6aa13f3dc5f8327e7fb9756e9655fc4c23e90", - "reference": "93e6aa13f3dc5f8327e7fb9756e9655fc4c23e90", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/08ab1620f0f35c41e50d847433193da76d33151e", + "reference": "08ab1620f0f35c41e50d847433193da76d33151e", "shasum": "" }, "require": { @@ -4280,7 +4281,7 @@ "type": "github" } ], - "time": "2021-02-23T15:47:55+00:00" + "time": "2021-03-10T06:28:23+00:00" }, { "name": "sebastian/environment", @@ -4288,12 +4289,12 @@ "source": { "type": "git", "url": "https://github.com/sebastianbergmann/environment.git", - "reference": "6e1743b808be9cfd33a716583ccb94b7d4d32e94" + "reference": "e34aa76b02666b7f12417f2000b6d4fbb9c2016c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/6e1743b808be9cfd33a716583ccb94b7d4d32e94", - "reference": "6e1743b808be9cfd33a716583ccb94b7d4d32e94", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/e34aa76b02666b7f12417f2000b6d4fbb9c2016c", + "reference": "e34aa76b02666b7f12417f2000b6d4fbb9c2016c", "shasum": "" }, "require": { @@ -4344,7 +4345,7 @@ "type": "github" } ], - "time": "2021-02-23T15:48:03+00:00" + "time": "2021-03-10T06:28:31+00:00" }, { "name": "sebastian/exporter", @@ -4352,12 +4353,12 @@ "source": { "type": "git", "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "eca7281ab29075df68b113a37a83be616b629b12" + "reference": "889b30136f9f8a6c0c4d71954b772ac8b8d7feab" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/eca7281ab29075df68b113a37a83be616b629b12", - "reference": "eca7281ab29075df68b113a37a83be616b629b12", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/889b30136f9f8a6c0c4d71954b772ac8b8d7feab", + "reference": "889b30136f9f8a6c0c4d71954b772ac8b8d7feab", "shasum": "" }, "require": { @@ -4422,7 +4423,7 @@ "type": "github" } ], - "time": "2021-02-23T15:48:12+00:00" + "time": "2021-03-10T06:28:38+00:00" }, { "name": "sebastian/global-state", @@ -4430,12 +4431,12 @@ "source": { "type": "git", "url": "https://github.com/sebastianbergmann/global-state.git", - "reference": "0ac702e6d13725242edb9b294c5d20b92fcfb8b4" + "reference": "8a1428d5351ea5dae3aa386d3b321499ac23adea" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/0ac702e6d13725242edb9b294c5d20b92fcfb8b4", - "reference": "0ac702e6d13725242edb9b294c5d20b92fcfb8b4", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/8a1428d5351ea5dae3aa386d3b321499ac23adea", + "reference": "8a1428d5351ea5dae3aa386d3b321499ac23adea", "shasum": "" }, "require": { @@ -4487,7 +4488,7 @@ "type": "github" } ], - "time": "2021-02-23T15:48:19+00:00" + "time": "2021-03-10T06:28:46+00:00" }, { "name": "sebastian/lines-of-code", @@ -4552,12 +4553,12 @@ "source": { "type": "git", "url": "https://github.com/sebastianbergmann/object-enumerator.git", - "reference": "8cc80b4bda00a4c5997c3fc597a34872f3a1007d" + "reference": "b218fb1d63287edb7613b61122890f39e82ae8c2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/8cc80b4bda00a4c5997c3fc597a34872f3a1007d", - "reference": "8cc80b4bda00a4c5997c3fc597a34872f3a1007d", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/b218fb1d63287edb7613b61122890f39e82ae8c2", + "reference": "b218fb1d63287edb7613b61122890f39e82ae8c2", "shasum": "" }, "require": { @@ -4602,7 +4603,7 @@ "type": "github" } ], - "time": "2021-02-23T15:48:28+00:00" + "time": "2021-03-10T06:28:54+00:00" }, { "name": "sebastian/object-reflector", @@ -4610,12 +4611,12 @@ "source": { "type": "git", "url": "https://github.com/sebastianbergmann/object-reflector.git", - "reference": "1d33587c2c3e636936f895e103a9e82dd8102a8e" + "reference": "cbf30bc9ed44451f5301480f668cd4fcf6bb225a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/1d33587c2c3e636936f895e103a9e82dd8102a8e", - "reference": "1d33587c2c3e636936f895e103a9e82dd8102a8e", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/cbf30bc9ed44451f5301480f668cd4fcf6bb225a", + "reference": "cbf30bc9ed44451f5301480f668cd4fcf6bb225a", "shasum": "" }, "require": { @@ -4658,7 +4659,7 @@ "type": "github" } ], - "time": "2021-02-23T15:48:35+00:00" + "time": "2021-03-10T06:29:02+00:00" }, { "name": "sebastian/recursion-context", @@ -4666,12 +4667,12 @@ "source": { "type": "git", "url": "https://github.com/sebastianbergmann/recursion-context.git", - "reference": "43f58a51e8f853aadb228ba818d2be388af7237b" + "reference": "c3333538e25ec932d0cbdce77b6ac846757b809d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/43f58a51e8f853aadb228ba818d2be388af7237b", - "reference": "43f58a51e8f853aadb228ba818d2be388af7237b", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/c3333538e25ec932d0cbdce77b6ac846757b809d", + "reference": "c3333538e25ec932d0cbdce77b6ac846757b809d", "shasum": "" }, "require": { @@ -4722,7 +4723,7 @@ "type": "github" } ], - "time": "2021-02-23T15:49:08+00:00" + "time": "2021-03-10T06:29:33+00:00" }, { "name": "sebastian/resource-operations", @@ -4786,12 +4787,12 @@ "source": { "type": "git", "url": "https://github.com/sebastianbergmann/type.git", - "reference": "557863473c1de00e165a288d5b547f1f83652e7e" + "reference": "1bba184dccb563769fab9bd69c623c1a353dec98" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/557863473c1de00e165a288d5b547f1f83652e7e", - "reference": "557863473c1de00e165a288d5b547f1f83652e7e", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/1bba184dccb563769fab9bd69c623c1a353dec98", + "reference": "1bba184dccb563769fab9bd69c623c1a353dec98", "shasum": "" }, "require": { @@ -4835,7 +4836,7 @@ "type": "github" } ], - "time": "2021-02-23T15:49:16+00:00" + "time": "2021-03-10T06:29:41+00:00" }, { "name": "sebastian/version", @@ -5658,12 +5659,12 @@ "source": { "type": "git", "url": "https://github.com/twigphp/Twig.git", - "reference": "728c611e8643a5dd44839ffa791e21763b04a694" + "reference": "37e48403c21e06f63bc27d7ccd997fbb72b0ae2a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/twigphp/Twig/zipball/728c611e8643a5dd44839ffa791e21763b04a694", - "reference": "728c611e8643a5dd44839ffa791e21763b04a694", + "url": "https://api.github.com/repos/twigphp/Twig/zipball/37e48403c21e06f63bc27d7ccd997fbb72b0ae2a", + "reference": "37e48403c21e06f63bc27d7ccd997fbb72b0ae2a", "shasum": "" }, "require": { @@ -5729,7 +5730,7 @@ "type": "tidelift" } ], - "time": "2021-02-22T11:56:05+00:00" + "time": "2021-03-10T10:07:14+00:00" }, { "name": "vimeo/psalm", diff --git a/src/Appwrite/Resque/Worker.php b/src/Appwrite/Resque/Worker.php index 347c2b7e95..25c6ea2f52 100644 --- a/src/Appwrite/Resque/Worker.php +++ b/src/Appwrite/Resque/Worker.php @@ -18,28 +18,16 @@ abstract class Worker public function setUp(): void { - run(function() { - Runtime::enableCoroutine(SWOOLE_HOOK_ALL); - - $this->init(); - }); + $this->init(); } public function perform() { - run(function() { - Runtime::enableCoroutine(SWOOLE_HOOK_ALL); - - $this->execute(); - }); + $this->execute(); } public function tearDown(): void { - run(function() { - Runtime::enableCoroutine(SWOOLE_HOOK_ALL); - - $this->shutdown(); - }); + $this->shutdown(); } } From d462c5dce5c3296ac5c06bb98edb49952e8a0fc4 Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Wed, 10 Mar 2021 14:39:37 +0100 Subject: [PATCH 064/267] adapt to review --- app/controllers/api/health.php | 11 ----------- app/realtime.php | 14 ++++++++------ src/Appwrite/Realtime/Realtime.php | 10 ++++++++++ 3 files changed, 18 insertions(+), 17 deletions(-) diff --git a/app/controllers/api/health.php b/app/controllers/api/health.php index 4658ac0794..696247f3da 100644 --- a/app/controllers/api/health.php +++ b/app/controllers/api/health.php @@ -33,17 +33,6 @@ App::get('/v1/health/version') $response->json(['version' => APP_VERSION_STABLE]); }); -App::get('/v1/health/realtime') - ->desc('Get Realtime') - ->groups(['api', 'health']) - ->label('scope', 'public') - ->inject('response') - ->action(function ($response) { - /** @var Utopia\Response $response */ - // TODO: realtime health - $response->json(['status' => 'OK']); - }); - App::get('/v1/health/db') ->desc('Get DB') ->groups(['api', 'health']) diff --git a/app/realtime.php b/app/realtime.php index 2c7656c90f..55634c5067 100644 --- a/app/realtime.php +++ b/app/realtime.php @@ -119,12 +119,12 @@ $server->on('start', function (Server $server) { }); $server->on('open', function (Server $server, Request $request) use (&$connections, &$subscriptions, &$register) { - Console::info("Connection open (user: {$request->fd}, connections: {}, worker: {$server->getWorkerId()})"); - $app = new App(''); $connection = $request->fd; $request = new SwooleRequest($request); + Console::info("Connection open (user: {$connection}, worker: {$server->getWorkerId()})"); + App::setResource('request', function () use ($request) { return $request; }); @@ -152,8 +152,10 @@ $server->on('open', function (Server $server, Request $request) use (&$connectio /* * Abuse Check + * + * Abuse limits are connecting 128 times per minute and ip address. */ - $timeLimit = new TimeLimit('url:{url},ip:{ip}', 60, 60, function () use ($register) { + $timeLimit = new TimeLimit('url:{url},ip:{ip}', 128, 60, function () use ($register) { return $register->get('db'); }); $timeLimit @@ -209,9 +211,9 @@ $server->on('message', function (Server $server, Frame $frame) { $server->close($frame->fd); }); -$server->on('close', function (Server $server, int $fd) use (&$connections, &$subscriptions) { - Realtime::unsubscribe($fd, $subscriptions, $connections); - Console::info('Connection close: ' . $fd); +$server->on('close', function (Server $server, int $connection) use (&$connections, &$subscriptions) { + Realtime::unsubscribe($connection, $subscriptions, $connections); + Console::info('Connection close: ' . $connection); }); $server->start(); diff --git a/src/Appwrite/Realtime/Realtime.php b/src/Appwrite/Realtime/Realtime.php index 93e5cffd97..615a07d43a 100644 --- a/src/Appwrite/Realtime/Realtime.php +++ b/src/Appwrite/Realtime/Realtime.php @@ -87,6 +87,16 @@ class Realtime /** * Identifies the receivers of all subscriptions, based on the permissions and event. * + * The processing works in linear time complexity, meaning it will increase in time - the same amount it increases in space. + * + * Example with a 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 * @param array $connections * @param array $subscriptions From 390e0d64077c65e53c7739a6118dc9774ec44fcc Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Wed, 10 Mar 2021 16:18:03 +0100 Subject: [PATCH 065/267] fix comment --- src/Appwrite/Realtime/Realtime.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/Appwrite/Realtime/Realtime.php b/src/Appwrite/Realtime/Realtime.php index 615a07d43a..08c3cf3215 100644 --- a/src/Appwrite/Realtime/Realtime.php +++ b/src/Appwrite/Realtime/Realtime.php @@ -87,9 +87,7 @@ class Realtime /** * Identifies the receivers of all subscriptions, based on the permissions and event. * - * The processing works in linear time complexity, meaning it will increase in time - the same amount it increases in space. - * - * Example with a event with user:XXX permissions and with X users spread across 10 different channels: + * 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 From 40c4f0f15d3e10826497db3bd9aa871a7f8633fe Mon Sep 17 00:00:00 2001 From: Eldad Fux Date: Wed, 10 Mar 2021 22:27:50 +0200 Subject: [PATCH 066/267] Fixed tests --- Dockerfile | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index a3bb4769db..bc3c4db044 100755 --- a/Dockerfile +++ b/Dockerfile @@ -29,7 +29,8 @@ RUN \ git \ zlib-dev \ brotli-dev \ - libmaxminddb-dev + libmaxminddb-dev \ + openssl-dev RUN docker-php-ext-install sockets @@ -47,7 +48,7 @@ RUN \ cd swoole-src && \ git checkout $PHP_SWOOLE_VERSION && \ phpize && \ - ./configure --enable-sockets --enable-http2 && \ + ./configure --enable-sockets --enable-http2 --enable-openssl && \ make && make install && \ cd .. && \ ## Maxminddb extension From 799926ac537bcb2d680a26da6c4b3361dcb9f4ad Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Thu, 11 Mar 2021 10:45:54 +0100 Subject: [PATCH 067/267] fix timezone --- app/realtime.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/realtime.php b/app/realtime.php index c7674b1abb..5db5724e0f 100644 --- a/app/realtime.php +++ b/app/realtime.php @@ -128,7 +128,7 @@ $server->on('start', function (Server $server) { }); $server->on('open', function (Server $server, Request $request) use (&$connections, &$subscriptions, &$register) { - $app = new App(''); + $app = new App('America/New_York'); $connection = $request->fd; $request = new SwooleRequest($request); From e7ccc36d7b1380f89bd8a5412f06f0007895f783 Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Thu, 11 Mar 2021 10:50:19 +0100 Subject: [PATCH 068/267] fix failure tests --- tests/e2e/Services/Realtime/RealtimeBase.php | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/tests/e2e/Services/Realtime/RealtimeBase.php b/tests/e2e/Services/Realtime/RealtimeBase.php index 379f21a022..4c921b5b7a 100644 --- a/tests/e2e/Services/Realtime/RealtimeBase.php +++ b/tests/e2e/Services/Realtime/RealtimeBase.php @@ -41,12 +41,16 @@ trait RealtimeBase * Test for FAILURE */ $client = $this->getWebsocket(['documents'], ['origin' => 'http://appwrite.unknown']); - $this->assertEquals('Invalid Origin. Register your new client (appwrite.unknown) as a new Web platform on your project console dashboard', $client->receive()); + $payload = json_decode($client->receive(), true); + $this->assertEquals(1008, $payload['code']); + $this->assertEquals('Invalid Origin. Register your new client (appwrite.unknown) as a new Web platform on your project console dashboard', $payload['message']); $this->expectException(ConnectionException::class); // Check if server disconnnected client $client->close(); $client = $this->getWebsocket(); - $this->assertEquals('Missing channels', $client->receive()); + $payload = json_decode($client->receive(), true); + $this->assertEquals(1008, $payload['code']); + $this->assertEquals('Missing channels', $payload['message']); $this->expectException(ConnectionException::class); // Check if server disconnnected client $client->close(); @@ -55,7 +59,9 @@ trait RealtimeBase 'Origin' => 'appwrite.test' ] ]); - $this->assertEquals('Missing or unknown project ID', $client->receive()); + $payload = json_decode($client->receive(), true); + $this->assertEquals(1008, $payload['code']); + $this->assertEquals('Missing or unknown project ID', $payload['message']); $this->expectException(ConnectionException::class); // Check if server disconnnected client $client->close(); @@ -64,7 +70,9 @@ trait RealtimeBase 'Origin' => 'appwrite.test' ] ]); - $this->assertEquals('Missing or unknown project ID', $client->receive()); + $payload = json_decode($client->receive(), true); + $this->assertEquals(1008, $payload['code']); + $this->assertEquals('Missing or unknown project ID', $payload['message']); $this->expectException(ConnectionException::class); // Check if server disconnnected client $client->close(); } From b8531560b2b776231a3285500ccca3ce3be4e233 Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Thu, 11 Mar 2021 17:28:03 +0100 Subject: [PATCH 069/267] fix connection pool on spike loads --- app/controllers/shared/api.php | 15 ++++++++------- app/init.php | 4 ++-- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/app/controllers/shared/api.php b/app/controllers/shared/api.php index 69ffdf4f2f..f165b4b349 100644 --- a/app/controllers/shared/api.php +++ b/app/controllers/shared/api.php @@ -140,13 +140,14 @@ App::shutdown(function ($utopia, $request, $response, $project, $events, $audits ->setQueue('v1-functions') ->setClass('FunctionsV1') ->trigger(); - - $realtime - ->setEvent($events->getParam('event')) - ->setProject($project->getId()) - ->setPayload($response->getPayload()) - ->trigger(); - + + if ($project->getId() !== 'console') { + $realtime + ->setEvent($events->getParam('event')) + ->setProject($project->getId()) + ->setPayload($response->getPayload()) + ->trigger(); + } } if (!empty($audits->getParam('event'))) { diff --git a/app/init.php b/app/init.php index 20a711e7af..806bc1a718 100644 --- a/app/init.php +++ b/app/init.php @@ -165,7 +165,7 @@ $register->set('dbPool', function () { // Register DB connection PDONative::ATTR_ERRMODE => PDONative::ERRMODE_EXCEPTION, ]); - $pool = new PDOPool($config); + $pool = new PDOPool($config, 4096); // TODO: Investigate pool size return $pool; }); @@ -226,7 +226,7 @@ $register->set('redisPool', function () { ->withReadTimeout(0) ->withRetryInterval(0); - $pool = new RedisPool($config); + $pool = new RedisPool($config, 4096); // TODO: Investigate pool size return $pool; }); From 565a0bd35e3b0e6aa1444f3dd62fe2fe91ea1e31 Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Fri, 12 Mar 2021 12:00:52 +0100 Subject: [PATCH 070/267] improve k6 test --- tests/benchmarks/ws.js | 41 +++++++++++++++++++++++++++-------------- 1 file changed, 27 insertions(+), 14 deletions(-) diff --git a/tests/benchmarks/ws.js b/tests/benchmarks/ws.js index 34cbfb0a3b..4c4c658186 100644 --- a/tests/benchmarks/ws.js +++ b/tests/benchmarks/ws.js @@ -6,32 +6,45 @@ import { check } from 'k6'; export let options = { stages: [ - { duration: '20s', target: 10 }, - { duration: '20s', target: 100 }, - { duration: '20s', target: 0 }, + { + duration: '1m', + target: 250 + }, + { + duration: '1m', + target: 250 + }, ], } 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', '60479391b1c3f'); + url.searchParams.append('project', '60476312f335c'); 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', () => { - console.log('connected') - }); - - socket.on('message', (data) => { - console.log('Message received: ', data) + connection = true; + }); + + socket.on('message', (data) => { + payload = data; + checked = true; }); - socket.on('close', () => console.log('disconnected')); - socket.setTimeout(function () { - console.log('2 seconds passed, closing the socket'); + check(payload, { + 'connection opened': (r) => connection, + 'message received': (r) => checked, + 'channels are right': (r) => r === `{"files":0}` + }) socket.close(); - }, 2000); + }, 5000); }); check(res, { 'status is 101': (r) => r && r.status === 101 }); From 2efff977e0aef69f40eb2367ae8ae74dcd754f9c Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Fri, 12 Mar 2021 15:16:40 +0100 Subject: [PATCH 071/267] use redis connection pool on http --- app/init.php | 7 +++---- app/realtime.php | 11 +++-------- 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/app/init.php b/app/init.php index 806bc1a718..bad2320d7c 100644 --- a/app/init.php +++ b/app/init.php @@ -230,13 +230,12 @@ $register->set('redisPool', function () { return $pool; }); -$register->set('cache', function () { // Register cache connection - $redis = new Redis(); - $redis->pconnect(App::getEnv('_APP_REDIS_HOST', ''), App::getEnv('_APP_REDIS_PORT', '')); +$register->set('cache', function () use ($register) { // Register cache connection + $redis = $register->get('redisPool')->get(); $redis->setOption(Redis::OPT_READ_TIMEOUT, -1); return $redis; -}); +}, true); $register->set('smtp', function () { $mail = new PHPMailer(true); diff --git a/app/realtime.php b/app/realtime.php index 5db5724e0f..3217219dc5 100644 --- a/app/realtime.php +++ b/app/realtime.php @@ -5,6 +5,8 @@ require_once __DIR__ . '/init.php'; use Appwrite\Network\Validator\Origin; use Appwrite\Realtime\Realtime; use Appwrite\Utopia\Response; +use Swoole\Database\RedisConfig; +use Swoole\Database\RedisPool; use Swoole\Process; use Swoole\Http\Request; use Swoole\Http\Response as SwooleResponse; @@ -34,13 +36,6 @@ $register->set('db', function () use ($register) { return $pdo; }, true); -$register->set('cache', function () use ($register) { // Register cache connection - $redis = $register->get('redisPool')->get(); - $redis->setOption(Redis::OPT_READ_TIMEOUT, -1); - - return $redis; -}, true); - $server = new Server('0.0.0.0', 80); $server->set([ @@ -128,7 +123,7 @@ $server->on('start', function (Server $server) { }); $server->on('open', function (Server $server, Request $request) use (&$connections, &$subscriptions, &$register) { - $app = new App('America/New_York'); + $app = new App('UTC'); $connection = $request->fd; $request = new SwooleRequest($request); From 56dca6b172f34517e4941efa195991f84cded97b Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Fri, 12 Mar 2021 16:56:12 +0100 Subject: [PATCH 072/267] move resource to http worker so workers work --- app/http.php | 7 +++++++ app/init.php | 11 ++++++----- app/realtime.php | 10 ++++++++-- 3 files changed, 21 insertions(+), 7 deletions(-) diff --git a/app/http.php b/app/http.php index d00bbc09eb..60a6d9df6a 100644 --- a/app/http.php +++ b/app/http.php @@ -31,6 +31,13 @@ ResqueScheduler::enqueueAt(\time() + 30, 'v1-certificates', 'CertificatesV1', [ 'validateCNAME' => false, ]); +$register->set('cache', function () use ($register) { // Register cache connection + $redis = $register->get('redisPool')->get(); + $redis->setOption(Redis::OPT_READ_TIMEOUT, -1); + + return $redis; +}, true); + Swoole\Runtime::enableCoroutine(SWOOLE_HOOK_ALL); $http = new Server("0.0.0.0", App::getEnv('PORT', 80)); diff --git a/app/init.php b/app/init.php index bad2320d7c..94f934441b 100644 --- a/app/init.php +++ b/app/init.php @@ -165,7 +165,7 @@ $register->set('dbPool', function () { // Register DB connection PDONative::ATTR_ERRMODE => PDONative::ERRMODE_EXCEPTION, ]); - $pool = new PDOPool($config, 4096); // TODO: Investigate pool size + $pool = new PDOPool($config, 16384); // TODO: Investigate pool size return $pool; }); @@ -226,16 +226,17 @@ $register->set('redisPool', function () { ->withReadTimeout(0) ->withRetryInterval(0); - $pool = new RedisPool($config, 4096); // TODO: Investigate pool size + $pool = new RedisPool($config, 16384); // TODO: Investigate pool size return $pool; }); -$register->set('cache', function () use ($register) { // Register cache connection - $redis = $register->get('redisPool')->get(); +$register->set('cache', function () { // Register cache connection + $redis = new Redis(); + $redis->pconnect(App::getEnv('_APP_REDIS_HOST', ''), App::getEnv('_APP_REDIS_PORT', '')); $redis->setOption(Redis::OPT_READ_TIMEOUT, -1); return $redis; -}, true); +}); $register->set('smtp', function () { $mail = new PHPMailer(true); diff --git a/app/realtime.php b/app/realtime.php index 3217219dc5..120117cf39 100644 --- a/app/realtime.php +++ b/app/realtime.php @@ -5,8 +5,7 @@ require_once __DIR__ . '/init.php'; use Appwrite\Network\Validator\Origin; use Appwrite\Realtime\Realtime; use Appwrite\Utopia\Response; -use Swoole\Database\RedisConfig; -use Swoole\Database\RedisPool; +use Swoole\Database\PDOProxy; use Swoole\Process; use Swoole\Http\Request; use Swoole\Http\Response as SwooleResponse; @@ -36,6 +35,13 @@ $register->set('db', function () use ($register) { return $pdo; }, true); +$register->set('cache', function () use ($register) { // Register cache connection + $redis = $register->get('redisPool')->get(); + $redis->setOption(Redis::OPT_READ_TIMEOUT, -1); + + return $redis; +}, true); + $server = new Server('0.0.0.0', 80); $server->set([ From 520c0652367511850e54a76941d5534fc50ffde0 Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Fri, 12 Mar 2021 16:56:41 +0100 Subject: [PATCH 073/267] move connections back into pool after used --- app/realtime.php | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/app/realtime.php b/app/realtime.php index 120117cf39..0483a57fe3 100644 --- a/app/realtime.php +++ b/app/realtime.php @@ -6,6 +6,8 @@ use Appwrite\Network\Validator\Origin; use Appwrite\Realtime\Realtime; use Appwrite\Utopia\Response; use Swoole\Database\PDOProxy; +use Swoole\Database\RedisConfig; +use Swoole\Database\RedisPool; use Swoole\Process; use Swoole\Http\Request; use Swoole\Http\Response as SwooleResponse; @@ -206,6 +208,21 @@ $server->on('open', function (Server $server, Request $request) use (&$connectio Realtime::subscribe($project->getId(), $connection, $roles, $subscriptions, $connections, $channels); $server->push($connection, json_encode($channels)); + + /** + * Put used PDO and Redis Connections back into their pools. + */ + + /** @var Swoole\Database\PDOPool $dbPool */ + $dbPool = $register->get('dbPool'); + $dbPool->put(new PDOProxy(function () use ($register) { + return $register->get('db'); + } + )); + + /** @var Swoole\Database\RedisPool $redisPool */ + $redisPool = $register->get('redisPool'); + $redisPool->put($register->get('cache')); } catch (\Throwable $th) { $response = [ 'code' => $th->getCode(), From e14a49504851e0f3dfd8b14770083c6eb5e3f7f0 Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Tue, 16 Mar 2021 15:30:45 +0100 Subject: [PATCH 074/267] add custom connection pool --- app/http.php | 9 --- app/init.php | 45 ----------- app/realtime.php | 112 ++++++++++++++++----------- src/Appwrite/Database/Pool.php | 19 +++++ src/Appwrite/Database/Pool/PDO.php | 50 ++++++++++++ src/Appwrite/Database/Pool/Redis.php | 42 ++++++++++ 6 files changed, 177 insertions(+), 100 deletions(-) create mode 100644 src/Appwrite/Database/Pool.php create mode 100644 src/Appwrite/Database/Pool/PDO.php create mode 100644 src/Appwrite/Database/Pool/Redis.php diff --git a/app/http.php b/app/http.php index 60a6d9df6a..1a0def5413 100644 --- a/app/http.php +++ b/app/http.php @@ -31,15 +31,6 @@ ResqueScheduler::enqueueAt(\time() + 30, 'v1-certificates', 'CertificatesV1', [ 'validateCNAME' => false, ]); -$register->set('cache', function () use ($register) { // Register cache connection - $redis = $register->get('redisPool')->get(); - $redis->setOption(Redis::OPT_READ_TIMEOUT, -1); - - return $redis; -}, true); - -Swoole\Runtime::enableCoroutine(SWOOLE_HOOK_ALL); - $http = new Server("0.0.0.0", App::getEnv('PORT', 80)); $payloadSize = max(4000000 /* 4mb */, App::getEnv('_APP_STORAGE_LIMIT', 10000000 /* 10mb */)); diff --git a/app/init.php b/app/init.php index 94f934441b..9f7d531219 100644 --- a/app/init.php +++ b/app/init.php @@ -148,27 +148,6 @@ Database::addFilter('encrypt', /* * Registry */ -$register->set('dbPool', function () { // Register DB connection - $config = new PDOConfig(); - $config - ->withHost(App::getEnv('_APP_DB_HOST', '')) - ->withPort(App::getEnv('_APP_DB_PORT', '')) - ->withDbName(App::getEnv('_APP_DB_SCHEMA', '')) - ->withUsername(App::getEnv('_APP_DB_USER', '')) - ->withPassword(App::getEnv('_APP_DB_PASS', '')) - ->withCharset('utf8mb4') - ->withOptions([ - PDONative::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8mb4', - PDONative::ATTR_TIMEOUT => 3, // Seconds - PDONative::ATTR_PERSISTENT => true, - PDONative::ATTR_DEFAULT_FETCH_MODE => PDONative::FETCH_ASSOC, - PDONative::ATTR_ERRMODE => PDONative::ERRMODE_EXCEPTION, - ]); - - $pool = new PDOPool($config, 16384); // TODO: Investigate pool size - - return $pool; -}); $register->set('db', function () use ($register) { $dbHost = App::getEnv('_APP_DB_HOST', ''); $dbUser = App::getEnv('_APP_DB_USER', ''); @@ -206,30 +185,6 @@ $register->set('statsd', function () { // Register DB connection return $statsd; }); -$register->set('redisPool', function () { - $user = App::getEnv('_APP_REDIS_USER', ''); - $pass = App::getEnv('_APP_REDIS_PASS', ''); - $auth = ''; - if (!empty($user)) { - $auth += $user; - } - if (!empty($pass)) { - $auth += ':' . $pass; - } - - $config = new RedisConfig(); - $config - ->withHost(App::getEnv('_APP_REDIS_HOST', '')) - ->withPort(App::getEnv('_APP_REDIS_PORT', '')) - ->withAuth($auth) - ->withTimeout(0) - ->withReadTimeout(0) - ->withRetryInterval(0); - - $pool = new RedisPool($config, 16384); // TODO: Investigate pool size - - return $pool; -}); $register->set('cache', function () { // Register cache connection $redis = new Redis(); $redis->pconnect(App::getEnv('_APP_REDIS_HOST', ''), App::getEnv('_APP_REDIS_PORT', '')); diff --git a/app/realtime.php b/app/realtime.php index 0483a57fe3..cb468c35bf 100644 --- a/app/realtime.php +++ b/app/realtime.php @@ -2,12 +2,11 @@ require_once __DIR__ . '/init.php'; +use Appwrite\Database\Pool\PDOPool; +use Appwrite\Database\Pool\RedisPool; use Appwrite\Network\Validator\Origin; use Appwrite\Realtime\Realtime; use Appwrite\Utopia\Response; -use Swoole\Database\PDOProxy; -use Swoole\Database\RedisConfig; -use Swoole\Database\RedisPool; use Swoole\Process; use Swoole\Http\Request; use Swoole\Http\Response as SwooleResponse; @@ -30,21 +29,7 @@ use Utopia\Abuse\Adapters\TimeLimit; Swoole\Runtime::enableCoroutine(SWOOLE_HOOK_ALL); -$register->set('db', function () use ($register) { - $pool = $register->get('dbPool'); - $pdo = $pool->get()->__getObject(); - - return $pdo; -}, true); - -$register->set('cache', function () use ($register) { // Register cache connection - $redis = $register->get('redisPool')->get(); - $redis->setOption(Redis::OPT_READ_TIMEOUT, -1); - - return $redis; -}, true); - -$server = new Server('0.0.0.0', 80); +$server = new Server('0.0.0.0', 80, SWOOLE_PROCESS); $server->set([ 'package_max_length' => 64000 // Default maximum Package Size (64kb) @@ -53,11 +38,38 @@ $server->set([ $subscriptions = []; $connections = []; +$register->set('dbPool', function () { // Register DB connection + $dbHost = App::getEnv('_APP_DB_HOST', ''); + $dbUser = App::getEnv('_APP_DB_USER', ''); + $dbPass = App::getEnv('_APP_DB_PASS', ''); + $dbScheme = App::getEnv('_APP_DB_SCHEMA', ''); + $pool = new PDOPool(20, $dbHost, $dbScheme, $dbUser, $dbPass); + + return $pool; +}); + +$register->set('redisPool', function () { + $user = App::getEnv('_APP_REDIS_USER', ''); + $pass = App::getEnv('_APP_REDIS_PASS', ''); + $auth = []; + if ($user) { + $auth[] = $user; + } + if ($pass) { + $auth[] = $pass; + } + + $pool = new RedisPool(20, App::getEnv('_APP_REDIS_HOST', ''), App::getEnv('_APP_REDIS_PORT', ''), $auth); + + return $pool; +}); + $server->on('workerStart', function ($server, $workerId) use (&$subscriptions, &$connections, &$register) { Console::success('Worker ' . $workerId . ' started succefully'); - + $attempts = 0; $start = time(); + $redisPool = $register->get('redisPool'); while ($attempts < 300) { try { @@ -67,7 +79,8 @@ $server->on('workerStart', function ($server, $workerId) use (&$subscriptions, & sleep(5); // 5 sec delay between connection attempts } - $redis = $register->get('cache'); + /** @var Swoole\Coroutine\Redis $redis */ + $redis = $redisPool->get(); if ($redis->ping(true)) { $attempts = 0; @@ -108,6 +121,7 @@ $server->on('workerStart', function ($server, $workerId) use (&$subscriptions, & }); } catch (\Throwable $th) { Console::error('Pub/sub error: ' . $th->getMessage()); + $redisPool->put($redis); $attempts++; continue; } @@ -135,6 +149,17 @@ $server->on('open', function (Server $server, Request $request) use (&$connectio $connection = $request->fd; $request = new SwooleRequest($request); + $db = $register->get('dbPool')->get(); + $redis = $register->get('redisPool')->get(); + + $register->set('db', function () use (&$db) { + return $db; + }); + + $register->set('cache', function () use (&$redis) { // Register cache connection + return $redis; + }); + Console::info("Connection open (user: {$connection}, worker: {$server->getWorkerId()})"); App::setResource('request', function () use ($request) { @@ -145,16 +170,16 @@ $server->on('open', function (Server $server, Request $request) use (&$connectio return new Response(new SwooleResponse()); }); - /** @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'); - 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 */ @@ -167,8 +192,8 @@ $server->on('open', function (Server $server, Request $request) use (&$connectio * * Abuse limits are connecting 128 times per minute and ip address. */ - $timeLimit = new TimeLimit('url:{url},ip:{ip}', 128, 60, function () use ($register) { - return $register->get('db'); + $timeLimit = new TimeLimit('url:{url},ip:{ip}', 128, 60, function () use ($db) { + return $db; }); $timeLimit ->setNamespace('app_' . $project->getId()) @@ -208,21 +233,6 @@ $server->on('open', function (Server $server, Request $request) use (&$connectio Realtime::subscribe($project->getId(), $connection, $roles, $subscriptions, $connections, $channels); $server->push($connection, json_encode($channels)); - - /** - * Put used PDO and Redis Connections back into their pools. - */ - - /** @var Swoole\Database\PDOPool $dbPool */ - $dbPool = $register->get('dbPool'); - $dbPool->put(new PDOProxy(function () use ($register) { - return $register->get('db'); - } - )); - - /** @var Swoole\Database\RedisPool $redisPool */ - $redisPool = $register->get('redisPool'); - $redisPool->put($register->get('cache')); } catch (\Throwable $th) { $response = [ 'code' => $th->getCode(), @@ -231,6 +241,16 @@ $server->on('open', function (Server $server, Request $request) use (&$connectio $server->push($connection, json_encode($response)); $server->close($connection); } + /** + * Put used PDO and Redis Connections back into their pools. + */ + /** @var PDOPool $dbPool */ + $dbPool = $register->get('dbPool'); + $dbPool->put($db); + + /** @var RedisPool $redisPool */ + $redisPool = $register->get('redisPool'); + $redisPool->put($redis); }); $server->on('message', function (Server $server, Frame $frame) { diff --git a/src/Appwrite/Database/Pool.php b/src/Appwrite/Database/Pool.php new file mode 100644 index 0000000000..db8a7c5f6f --- /dev/null +++ b/src/Appwrite/Database/Pool.php @@ -0,0 +1,19 @@ +available = false; + while (!$this->pool->isEmpty()) { + $this->pool->pop(); + } + } +} \ No newline at end of file diff --git a/src/Appwrite/Database/Pool/PDO.php b/src/Appwrite/Database/Pool/PDO.php new file mode 100644 index 0000000000..85d263015c --- /dev/null +++ b/src/Appwrite/Database/Pool/PDO.php @@ -0,0 +1,50 @@ +pool = new SplQueue; + $this->size = $size; + for ($i=0; $i < $this->size; $i++) { + $pdo = new PDO( + "mysql:". + "host={$host};". + "dbname={$schema};" . + "charset={$charset}", + $user, + $pass, + [ + PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8mb4', + PDO::ATTR_TIMEOUT => 3, // Seconds + PDO::ATTR_PERSISTENT => true, + PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, + PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, + PDO::MYSQL_ATTR_USE_BUFFERED_QUERY => true + ] + ); + $this->pool->enqueue($pdo); + } + } + + public function put (PDO $pdo) + { + $this->pool->enqueue($pdo); + } + + public function get (): PDO + { + if ($this->available && count($this->pool) > 0) { + return $this->pool->dequeue(); + } + sleep(0.01); + return $this->get(); + } +} diff --git a/src/Appwrite/Database/Pool/Redis.php b/src/Appwrite/Database/Pool/Redis.php new file mode 100644 index 0000000000..197f20747c --- /dev/null +++ b/src/Appwrite/Database/Pool/Redis.php @@ -0,0 +1,42 @@ +pool = new SplQueue; + $this->size = $size; + for ($i=0; $i < $this->size; $i++) { + $redis = new Redis(); + $redis->pconnect($host, $port); + $redis->setOption(Redis::OPT_READ_TIMEOUT, -1); + + if ($auth) { + $redis->auth($auth); + } + + $this->pool->enqueue($redis); + } + } + + public function put (Redis $redis) + { + $this->pool->enqueue($redis); + } + + public function get (): Redis + { + if ($this->available && !$this->pool->isEmpty()) { + return $this->pool->dequeue(); + } + sleep(0.1); + return $this->get(); + } +} From 5e06cd788a62f0333cfd2bec32897a69a9b8f3f8 Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Wed, 17 Mar 2021 16:35:57 +0100 Subject: [PATCH 075/267] update k6 --- tests/benchmarks/http.js | 13 +++++++++---- tests/benchmarks/ws.js | 8 ++++---- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/tests/benchmarks/http.js b/tests/benchmarks/http.js index a73f8d5633..e46157a21f 100644 --- a/tests/benchmarks/http.js +++ b/tests/benchmarks/http.js @@ -1,5 +1,5 @@ import http from 'k6/http'; -import { sleep, check } from 'k6'; +import { check } from 'k6'; import { Counter } from 'k6/metrics'; // A simple counter for http requests @@ -20,10 +20,15 @@ export const options = { }; export default function () { - const res = http.get('http://localhost:9501/v1/health/version?project=console'); + const config = { + headers: { + 'X-Appwrite-Key': '24356eb021863f81eb7dd77c7750304d0464e141cad6e9a8befa1f7d2b066fde190df3dab1e8d2639dbb82ee848da30501424923f4cd80d887ee40ad77ded62763ee489448523f6e39667f290f9a54b2ab8fad131a0bc985e6c0f760015f7f3411e40626c75646bb19d2bb2f7bf2f63130918220a206758cbc48845fd725a695', + 'X-Appwrite-Project': '60479fe35d95d' + }} - const checkRes = check(res, { + const resDb = http.get('http://localhost:9501/v1/health/db', config); + + check(resDb, { 'status is 200': (r) => r.status === 200, - 'response body': (r) => r.body.indexOf('0.7.0') !== -1, }); } \ No newline at end of file diff --git a/tests/benchmarks/ws.js b/tests/benchmarks/ws.js index 4c4c658186..d7bef7ab5f 100644 --- a/tests/benchmarks/ws.js +++ b/tests/benchmarks/ws.js @@ -7,12 +7,12 @@ import { check } from 'k6'; export let options = { stages: [ { - duration: '1m', - target: 250 + duration: '10s', + target: 10 }, { - duration: '1m', - target: 250 + duration: '30m', + target: 10 }, ], } From 89e5841fee58d6701c0b7b796cb00ed086dd521a Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Fri, 19 Mar 2021 11:28:47 +0100 Subject: [PATCH 076/267] fix events to projects without subscriptions --- src/Appwrite/Realtime/Realtime.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Appwrite/Realtime/Realtime.php b/src/Appwrite/Realtime/Realtime.php index 08c3cf3215..f5683bfd27 100644 --- a/src/Appwrite/Realtime/Realtime.php +++ b/src/Appwrite/Realtime/Realtime.php @@ -102,7 +102,7 @@ class Realtime static function identifyReceivers(array &$event, array &$subscriptions) { $receivers = []; - if ($subscriptions[$event['project']]) { + if (isset($subscriptions[$event['project']])) { foreach ($subscriptions[$event['project']] as $role => $subscription) { foreach ($event['data']['channels'] as $channel) { if ( From 7441d95f6bbaffcf44ae215297993eee89a4f757 Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Fri, 19 Mar 2021 11:30:03 +0100 Subject: [PATCH 077/267] improve identifying receivers --- src/Appwrite/Realtime/Realtime.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Appwrite/Realtime/Realtime.php b/src/Appwrite/Realtime/Realtime.php index f5683bfd27..f0f893f267 100644 --- a/src/Appwrite/Realtime/Realtime.php +++ b/src/Appwrite/Realtime/Realtime.php @@ -110,7 +110,7 @@ class Realtime && (\in_array($role, $event['permissions']) || \in_array('*', $event['permissions'])) ) { foreach (array_keys($subscriptions[$event['project']][$role][$channel]) as $ids) { - $receivers[] = $ids; + $receivers[$ids] = 0; } break; } @@ -118,7 +118,7 @@ class Realtime } } - return array_keys(array_flip($receivers)); + return array_keys($receivers); } /** From 3c933052b798ac076bc49971aaffcbe8019e1d85 Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Mon, 22 Mar 2021 13:34:57 +0100 Subject: [PATCH 078/267] use custom PDO class --- src/Appwrite/Database/Pool/PDO.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Appwrite/Database/Pool/PDO.php b/src/Appwrite/Database/Pool/PDO.php index 85d263015c..7233f874ff 100644 --- a/src/Appwrite/Database/Pool/PDO.php +++ b/src/Appwrite/Database/Pool/PDO.php @@ -3,8 +3,7 @@ namespace Appwrite\Database\Pool; use Appwrite\Database\Pool; -use Exception; -use PDO; +use Appwrite\Extend\PDO; use SplQueue; class PDOPool extends Pool From f20bcb5d5c080eea027f61edefc4cd41d427cbe6 Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Mon, 22 Mar 2021 15:00:01 +0100 Subject: [PATCH 079/267] use appwrite pdo adapter on http --- app/init.php | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/app/init.php b/app/init.php index 0fb089484c..f7d76920c8 100644 --- a/app/init.php +++ b/app/init.php @@ -27,6 +27,7 @@ use Appwrite\Database\Document; use Appwrite\Database\Validator\Authorization; use Appwrite\Event\Event; use Appwrite\Event\Realtime; +use Appwrite\Extend\PDO; use Appwrite\OpenSSL\OpenSSL; use Utopia\App; use Utopia\View; @@ -35,11 +36,6 @@ use Utopia\Locale\Locale; use Utopia\Registry\Registry; use MaxMind\Db\Reader; use PHPMailer\PHPMailer\PHPMailer; -use PDO as PDONative; -use Swoole\Database\PDOConfig; -use Swoole\Database\PDOPool; -use Swoole\Database\RedisConfig; -use Swoole\Database\RedisPool; const APP_NAME = 'Appwrite'; const APP_DOMAIN = 'appwrite.io'; @@ -155,11 +151,11 @@ $register->set('db', function () use ($register) { $dbScheme = App::getEnv('_APP_DB_SCHEMA', ''); $pdo = new PDO("mysql:host={$dbHost};dbname={$dbScheme};charset=utf8mb4", $dbUser, $dbPass, array( - PDONative::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8mb4', - PDONative::ATTR_TIMEOUT => 3, // Seconds - PDONative::ATTR_PERSISTENT => true, - PDONative::ATTR_DEFAULT_FETCH_MODE => PDONative::FETCH_ASSOC, - PDONative::ATTR_ERRMODE => PDONative::ERRMODE_EXCEPTION, + PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8mb4', + PDO::ATTR_TIMEOUT => 3, // Seconds + PDO::ATTR_PERSISTENT => true, + PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, + PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, )); return $pdo; From 295f2c9b7e22a7d98242fd91a26c241ea84daa16 Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Wed, 24 Mar 2021 12:37:57 +0100 Subject: [PATCH 080/267] fixed connection pool for http --- app/http.php | 23 +++++++++++++++++++++-- app/init.php | 29 ++++++++++++++++++++++++++++- app/realtime.php | 26 -------------------------- 3 files changed, 49 insertions(+), 29 deletions(-) diff --git a/app/http.php b/app/http.php index f67ff2ea7c..1a08e72d76 100644 --- a/app/http.php +++ b/app/http.php @@ -3,9 +3,9 @@ require_once __DIR__.'/../vendor/autoload.php'; use Appwrite\Database\Validator\Authorization; +use Appwrite\Utopia\Response; use Utopia\Swoole\Files; use Utopia\Swoole\Request; -use Appwrite\Utopia\Response; use Swoole\Process; use Swoole\Http\Server; use Swoole\Http\Request as SwooleRequest; @@ -71,7 +71,7 @@ $http->on('start', function (Server $http) use ($payloadSize) { }); }); -$http->on('request', function (SwooleRequest $swooleRequest, SwooleResponse $swooleResponse) { +$http->on('request', function (SwooleRequest $swooleRequest, SwooleResponse $swooleResponse) use ($register) { $request = new Request($swooleRequest); $response = new Response($swooleResponse); @@ -88,6 +88,17 @@ $http->on('request', function (SwooleRequest $swooleRequest, SwooleResponse $swo return; } + $db = $register->get('dbPool')->get(); + $redis = $register->get('redisPool')->get(); + + $register->set('db', function () use (&$db) { + return $db; + }); + + $register->set('cache', function () use (&$redis) { // Register cache connection + return $redis; + }); + $app = new App('UTC'); try { @@ -107,6 +118,14 @@ $http->on('request', function (SwooleRequest $swooleRequest, SwooleResponse $swo else { $swooleResponse->end('500: Server Error'); } + } finally { + /** @var PDOPool $dbPool */ + $dbPool = $register->get('dbPool'); + $dbPool->put($db); + + /** @var RedisPool $redisPool */ + $redisPool = $register->get('redisPool'); + $redisPool->put($redis); } }); diff --git a/app/init.php b/app/init.php index f7d76920c8..a891e6f612 100644 --- a/app/init.php +++ b/app/init.php @@ -24,6 +24,8 @@ use Appwrite\Database\Database; use Appwrite\Database\Adapter\MySQL as MySQLAdapter; use Appwrite\Database\Adapter\Redis as RedisAdapter; use Appwrite\Database\Document; +use Appwrite\Database\Pool\PDOPool; +use Appwrite\Database\Pool\RedisPool; use Appwrite\Database\Validator\Authorization; use Appwrite\Event\Event; use Appwrite\Event\Realtime; @@ -144,7 +146,32 @@ Database::addFilter('encrypt', /* * Registry */ -$register->set('db', function () use ($register) { +$register->set('dbPool', function () { // Register DB connection + $dbHost = App::getEnv('_APP_DB_HOST', ''); + $dbUser = App::getEnv('_APP_DB_USER', ''); + $dbPass = App::getEnv('_APP_DB_PASS', ''); + $dbScheme = App::getEnv('_APP_DB_SCHEMA', ''); + $pool = new PDOPool(10, $dbHost, $dbScheme, $dbUser, $dbPass); + + return $pool; +}); + +$register->set('redisPool', function () { + $user = App::getEnv('_APP_REDIS_USER', ''); + $pass = App::getEnv('_APP_REDIS_PASS', ''); + $auth = []; + if ($user) { + $auth[] = $user; + } + if ($pass) { + $auth[] = $pass; + } + + $pool = new RedisPool(10, App::getEnv('_APP_REDIS_HOST', ''), App::getEnv('_APP_REDIS_PORT', ''), $auth); + + return $pool; +}); +$register->set('db', function () { $dbHost = App::getEnv('_APP_DB_HOST', ''); $dbUser = App::getEnv('_APP_DB_USER', ''); $dbPass = App::getEnv('_APP_DB_PASS', ''); diff --git a/app/realtime.php b/app/realtime.php index cb468c35bf..572b578301 100644 --- a/app/realtime.php +++ b/app/realtime.php @@ -38,32 +38,6 @@ $server->set([ $subscriptions = []; $connections = []; -$register->set('dbPool', function () { // Register DB connection - $dbHost = App::getEnv('_APP_DB_HOST', ''); - $dbUser = App::getEnv('_APP_DB_USER', ''); - $dbPass = App::getEnv('_APP_DB_PASS', ''); - $dbScheme = App::getEnv('_APP_DB_SCHEMA', ''); - $pool = new PDOPool(20, $dbHost, $dbScheme, $dbUser, $dbPass); - - return $pool; -}); - -$register->set('redisPool', function () { - $user = App::getEnv('_APP_REDIS_USER', ''); - $pass = App::getEnv('_APP_REDIS_PASS', ''); - $auth = []; - if ($user) { - $auth[] = $user; - } - if ($pass) { - $auth[] = $pass; - } - - $pool = new RedisPool(20, App::getEnv('_APP_REDIS_HOST', ''), App::getEnv('_APP_REDIS_PORT', ''), $auth); - - return $pool; -}); - $server->on('workerStart', function ($server, $workerId) use (&$subscriptions, &$connections, &$register) { Console::success('Worker ' . $workerId . ' started succefully'); From 6b5718f32ffb52ddc58bcee83dd45bb59f52be91 Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Wed, 24 Mar 2021 13:13:59 +0100 Subject: [PATCH 081/267] create seperate init for workers --- app/init.php | 29 ---------------------------- app/workers.php | 37 ++++++++++++++++++++++++++++++++++++ app/workers/audits.php | 2 +- app/workers/certificates.php | 2 +- app/workers/deletes.php | 2 +- app/workers/functions.php | 2 +- app/workers/mails.php | 2 +- app/workers/tasks.php | 2 +- app/workers/usage.php | 2 +- app/workers/webhooks.php | 2 +- 10 files changed, 45 insertions(+), 37 deletions(-) create mode 100644 app/workers.php diff --git a/app/init.php b/app/init.php index a891e6f612..f272c3905f 100644 --- a/app/init.php +++ b/app/init.php @@ -29,7 +29,6 @@ use Appwrite\Database\Pool\RedisPool; use Appwrite\Database\Validator\Authorization; use Appwrite\Event\Event; use Appwrite\Event\Realtime; -use Appwrite\Extend\PDO; use Appwrite\OpenSSL\OpenSSL; use Utopia\App; use Utopia\View; @@ -171,34 +170,6 @@ $register->set('redisPool', function () { return $pool; }); -$register->set('db', function () { - $dbHost = App::getEnv('_APP_DB_HOST', ''); - $dbUser = App::getEnv('_APP_DB_USER', ''); - $dbPass = App::getEnv('_APP_DB_PASS', ''); - $dbScheme = App::getEnv('_APP_DB_SCHEMA', ''); - - $pdo = new PDO("mysql:host={$dbHost};dbname={$dbScheme};charset=utf8mb4", $dbUser, $dbPass, array( - PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8mb4', - PDO::ATTR_TIMEOUT => 3, // Seconds - PDO::ATTR_PERSISTENT => true, - PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, - PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, - )); - - return $pdo; -}); -$register->set('influxdb', function () { // Register DB connection - $host = App::getEnv('_APP_INFLUXDB_HOST', ''); - $port = App::getEnv('_APP_INFLUXDB_PORT', ''); - - if (empty($host) || empty($port)) { - return; - } - - $client = new InfluxDB\Client($host, $port, '', '', false, false, 5); - - return $client; -}); $register->set('statsd', function () { // Register DB connection $host = App::getEnv('_APP_STATSD_HOST', 'telegraf'); $port = App::getEnv('_APP_STATSD_PORT', 8125); diff --git a/app/workers.php b/app/workers.php new file mode 100644 index 0000000000..a0df772da5 --- /dev/null +++ b/app/workers.php @@ -0,0 +1,37 @@ +set('db', function () { + $dbHost = App::getEnv('_APP_DB_HOST', ''); + $dbUser = App::getEnv('_APP_DB_USER', ''); + $dbPass = App::getEnv('_APP_DB_PASS', ''); + $dbScheme = App::getEnv('_APP_DB_SCHEMA', ''); + + $pdo = new PDO("mysql:host={$dbHost};dbname={$dbScheme};charset=utf8mb4", $dbUser, $dbPass, array( + PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8mb4', + PDO::ATTR_TIMEOUT => 3, // Seconds + PDO::ATTR_PERSISTENT => true, + PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, + PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, + )); + + return $pdo; +}); +$register->set('influxdb', function () { // Register DB connection + $host = App::getEnv('_APP_INFLUXDB_HOST', ''); + $port = App::getEnv('_APP_INFLUXDB_PORT', ''); + + if (empty($host) || empty($port)) { + return; + } + + $client = new InfluxDB\Client($host, $port, '', '', false, false, 5); + + return $client; +}); \ No newline at end of file diff --git a/app/workers/audits.php b/app/workers/audits.php index 13af7e828e..9ec03d9c81 100644 --- a/app/workers/audits.php +++ b/app/workers/audits.php @@ -5,7 +5,7 @@ use Utopia\Audit\Audit; use Utopia\Audit\Adapters\MySQL as AuditAdapter; use Utopia\CLI\Console; -require_once __DIR__.'/../init.php'; +require_once __DIR__.'/../workers.php'; Console::title('Audits V1 Worker'); diff --git a/app/workers/certificates.php b/app/workers/certificates.php index 93349ab607..8f48da6987 100644 --- a/app/workers/certificates.php +++ b/app/workers/certificates.php @@ -11,7 +11,7 @@ use Appwrite\Database\Validator\Authorization; use Appwrite\Network\Validator\CNAME; use Appwrite\Resque\Worker; -require_once __DIR__.'/../init.php'; +require_once __DIR__.'/../workers.php'; Console::title('Certificates V1 Worker'); diff --git a/app/workers/deletes.php b/app/workers/deletes.php index 0478f7661a..523d56a4f9 100644 --- a/app/workers/deletes.php +++ b/app/workers/deletes.php @@ -14,7 +14,7 @@ use Utopia\Config\Config; use Utopia\Audit\Audit; use Utopia\Audit\Adapters\MySQL as AuditAdapter; -require_once __DIR__.'/../init.php'; +require_once __DIR__.'/../workers.php'; Console::title('Deletes V1 Worker'); diff --git a/app/workers/functions.php b/app/workers/functions.php index 2cedffd240..48d3a9a6fe 100644 --- a/app/workers/functions.php +++ b/app/workers/functions.php @@ -13,7 +13,7 @@ use Utopia\App; use Utopia\CLI\Console; use Utopia\Config\Config; -require_once __DIR__.'/../init.php'; +require_once __DIR__.'/../workers.php'; Console::title('Functions V1 Worker'); diff --git a/app/workers/mails.php b/app/workers/mails.php index 50e499f901..e0f34d1d9a 100644 --- a/app/workers/mails.php +++ b/app/workers/mails.php @@ -4,7 +4,7 @@ use Appwrite\Resque\Worker; use Utopia\App; use Utopia\CLI\Console; -require_once __DIR__.'/../init.php'; +require_once __DIR__.'/../workers.php'; Console::title('Mails V1 Worker'); diff --git a/app/workers/tasks.php b/app/workers/tasks.php index 70d705431e..7fedb2c827 100644 --- a/app/workers/tasks.php +++ b/app/workers/tasks.php @@ -10,7 +10,7 @@ use Appwrite\Database\Validator\Authorization; use Appwrite\Resque\Worker; use Cron\CronExpression; -require_once __DIR__.'/../init.php'; +require_once __DIR__.'/../workers.php'; Console::title('Tasks V1 Worker'); diff --git a/app/workers/usage.php b/app/workers/usage.php index 1d1a5ba5a7..3b261500b4 100644 --- a/app/workers/usage.php +++ b/app/workers/usage.php @@ -4,7 +4,7 @@ use Appwrite\Resque\Worker; use Utopia\App; use Utopia\CLI\Console; -require_once __DIR__.'/../init.php'; +require_once __DIR__.'/../workers.php'; Console::title('Usage V1 Worker'); diff --git a/app/workers/webhooks.php b/app/workers/webhooks.php index eba7f7cf8f..0ef1038243 100644 --- a/app/workers/webhooks.php +++ b/app/workers/webhooks.php @@ -9,7 +9,7 @@ use Appwrite\Database\Adapter\Redis as RedisAdapter; use Appwrite\Database\Validator\Authorization; use Appwrite\Resque\Worker; -require_once __DIR__.'/../init.php'; +require_once __DIR__.'/../workers.php'; Console::title('Webhooks V1 Worker'); From 78ec3a7821b6868d3b3d2f3e1bec412ceef9f495 Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Wed, 24 Mar 2021 13:25:28 +0100 Subject: [PATCH 082/267] replace influx with redis cache in worker init --- app/init.php | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/app/init.php b/app/init.php index f272c3905f..c2b8d85d0f 100644 --- a/app/init.php +++ b/app/init.php @@ -170,6 +170,18 @@ $register->set('redisPool', function () { return $pool; }); +$register->set('influxdb', function () { // Register DB connection + $host = App::getEnv('_APP_INFLUXDB_HOST', ''); + $port = App::getEnv('_APP_INFLUXDB_PORT', ''); + + if (empty($host) || empty($port)) { + return; + } + + $client = new InfluxDB\Client($host, $port, '', '', false, false, 5); + + return $client; +}); $register->set('statsd', function () { // Register DB connection $host = App::getEnv('_APP_STATSD_HOST', 'telegraf'); $port = App::getEnv('_APP_STATSD_PORT', 8125); @@ -179,13 +191,6 @@ $register->set('statsd', function () { // Register DB connection return $statsd; }); -$register->set('cache', function () { // Register cache connection - $redis = new Redis(); - $redis->pconnect(App::getEnv('_APP_REDIS_HOST', ''), App::getEnv('_APP_REDIS_PORT', '')); - $redis->setOption(Redis::OPT_READ_TIMEOUT, -1); - - return $redis; -}); $register->set('smtp', function () { $mail = new PHPMailer(true); From 73794097c91f576efb17f156593af4e8c16c3808 Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Wed, 24 Mar 2021 14:36:41 +0100 Subject: [PATCH 083/267] add permissions to execution response model --- src/Appwrite/Utopia/Response/Model/Execution.php | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/Appwrite/Utopia/Response/Model/Execution.php b/src/Appwrite/Utopia/Response/Model/Execution.php index 22c7710ec6..3707a1a1f1 100644 --- a/src/Appwrite/Utopia/Response/Model/Execution.php +++ b/src/Appwrite/Utopia/Response/Model/Execution.php @@ -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.', From c39ef9c1d25648adc53469ee3d3b8f77145f10ce Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Wed, 24 Mar 2021 15:17:17 +0100 Subject: [PATCH 084/267] add functions even to realtime --- src/Appwrite/Event/Realtime.php | 10 ++- tests/e2e/Services/Realtime/RealtimeBase.php | 88 ++++++++++++++++++++ tests/unit/Realtime/RealtimeChannelsTest.php | 6 +- 3 files changed, 100 insertions(+), 4 deletions(-) diff --git a/src/Appwrite/Event/Realtime.php b/src/Appwrite/Event/Realtime.php index c0f6d2ad3a..fb722be54d 100644 --- a/src/Appwrite/Event/Realtime.php +++ b/src/Appwrite/Event/Realtime.php @@ -140,7 +140,15 @@ class Realtime $this->permissions = $this->payload->getAttribute('$permissions.read'); break; - } + case strpos($this->event, 'functions.executions.') === 0: + if (!empty($this->payload->getAttribute('$permissions.read'))) { + $this->channels[] = 'executions'; + $this->channels[] = 'executions.' . $this->payload->getId(); + $this->channels[] = 'functions.' . $this->payload->getAttribute('functionId'); + $this->permissions = $this->payload->getAttribute('$permissions.read'); + } + break; + } } /** diff --git a/tests/e2e/Services/Realtime/RealtimeBase.php b/tests/e2e/Services/Realtime/RealtimeBase.php index 4c921b5b7a..1963d9d841 100644 --- a/tests/e2e/Services/Realtime/RealtimeBase.php +++ b/tests/e2e/Services/Realtime/RealtimeBase.php @@ -598,4 +598,92 @@ trait RealtimeBase $client->close(); } + + public function testChannelExecutions() + { + $user = $this->getUser(); + $session = $user['session'] ?? ''; + $projectId = $this->getProject()['$id']; + + $client = $this->getWebsocket(['executions'], [ + 'origin' => 'http://localhost', + 'cookie' => 'a_session_'.$projectId.'=' . $session + ]); + $response = json_decode($client->receive(), true); + $this->assertCount(1, $response); + $this->assertArrayHasKey('executions', $response); + + /** + * Test File Create + */ + $function = $this->client->call(Client::METHOD_POST, '/functions', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'name' => 'Test', + 'env' => 'php-7.4', + 'execute' => ['*'], + 'timeout' => 10, + ]); + + $functionId = $function['body']['$id'] ?? ''; + + $this->assertEquals($function['headers']['status-code'], 201); + $this->assertNotEmpty($function['body']['$id']); + + $tag = $this->client->call(Client::METHOD_POST, '/functions/'.$functionId.'/tags', array_merge([ + 'content-type' => 'multipart/form-data', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'command' => 'php index.php', + 'code' => new CURLFile(realpath(__DIR__ . '/../../../resources/functions/timeout.tar.gz'), 'application/x-gzip', 'php-fx.tar.gz'), + ]); + + $tagId = $tag['body']['$id'] ?? ''; + + $this->assertEquals($tag['headers']['status-code'], 201); + $this->assertNotEmpty($tag['body']['$id']); + + $response = $this->client->call(Client::METHOD_PATCH, '/functions/'.$functionId.'/tag', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'tag' => $tagId, + ]); + + $this->assertEquals($response['headers']['status-code'], 200); + $this->assertNotEmpty($response['body']['$id']); + + $execution = $this->client->call(Client::METHOD_POST, '/functions/'.$functionId.'/executions', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), []); + + $this->assertEquals($execution['headers']['status-code'], 201); + $this->assertNotEmpty($execution['body']['$id']); + + $response = json_decode($client->receive(), true); + + $this->assertArrayHasKey('timestamp', $response); + $this->assertCount(3, $response['channels']); + $this->assertContains('executions', $response['channels']); + $this->assertContains('executions.' . $execution['body']['$id'], $response['channels']); + $this->assertContains('functions.' . $execution['body']['functionId'], $response['channels']); + $this->assertEquals('functions.executions.create', $response['event']); + $this->assertNotEmpty($response['payload']); + + sleep(6); + + $response = json_decode($client->receive(), true); + + $this->assertArrayHasKey('timestamp', $response); + $this->assertCount(3, $response['channels']); + $this->assertContains('executions', $response['channels']); + $this->assertContains('executions.' . $execution['body']['$id'], $response['channels']); + $this->assertContains('functions.' . $execution['body']['functionId'], $response['channels']); + $this->assertEquals('functions.executions.update', $response['event']); + $this->assertNotEmpty($response['payload']); + + $client->close(); + } } diff --git a/tests/unit/Realtime/RealtimeChannelsTest.php b/tests/unit/Realtime/RealtimeChannelsTest.php index 1853254b43..a84fdd4a16 100644 --- a/tests/unit/Realtime/RealtimeChannelsTest.php +++ b/tests/unit/Realtime/RealtimeChannelsTest.php @@ -25,11 +25,11 @@ class RealtimeChannelsTest extends TestCase 'collections', 'collections.1', 'collections.1.documents', - 'collections.2', - 'collections.2.documents', 'documents', 'documents.1', - 'documents.2', + 'executions', + 'executions.1', + 'functions.1', ]; public function setUp(): void From ed9c8f53cc73c7339d6d555b72837df95de9df21 Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Wed, 24 Mar 2021 15:22:29 +0100 Subject: [PATCH 085/267] fix execute method name for worker rework --- app/workers/audits.php | 2 +- app/workers/certificates.php | 2 +- app/workers/deletes.php | 2 +- app/workers/mails.php | 2 +- app/workers/tasks.php | 2 +- app/workers/usage.php | 2 +- app/workers/webhooks.php | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/app/workers/audits.php b/app/workers/audits.php index 9ec03d9c81..2fe25e0928 100644 --- a/app/workers/audits.php +++ b/app/workers/audits.php @@ -19,7 +19,7 @@ class AuditsV1 extends Worker { } - public function execute(): void + public function run(): void { global $register; diff --git a/app/workers/certificates.php b/app/workers/certificates.php index 8f48da6987..baee3f205d 100644 --- a/app/workers/certificates.php +++ b/app/workers/certificates.php @@ -25,7 +25,7 @@ class CertificatesV1 extends Worker { } - public function execute(): void + public function run(): void { global $register; diff --git a/app/workers/deletes.php b/app/workers/deletes.php index 523d56a4f9..2ec424b901 100644 --- a/app/workers/deletes.php +++ b/app/workers/deletes.php @@ -30,7 +30,7 @@ class DeletesV1 extends Worker { } - public function execute(): void + public function run(): void { $projectId = $this->args['projectId']; $type = $this->args['type']; diff --git a/app/workers/mails.php b/app/workers/mails.php index e0f34d1d9a..9bd4dcdc4f 100644 --- a/app/workers/mails.php +++ b/app/workers/mails.php @@ -21,7 +21,7 @@ class MailsV1 extends Worker { } - public function execute(): void + public function run(): void { global $register; diff --git a/app/workers/tasks.php b/app/workers/tasks.php index 7fedb2c827..68c3b9810f 100644 --- a/app/workers/tasks.php +++ b/app/workers/tasks.php @@ -27,7 +27,7 @@ class TasksV1 extends Worker { } - public function execute(): void + public function run(): void { global $register; diff --git a/app/workers/usage.php b/app/workers/usage.php index 3b261500b4..f8996fb612 100644 --- a/app/workers/usage.php +++ b/app/workers/usage.php @@ -21,7 +21,7 @@ class UsageV1 extends Worker { } - public function execute(): void + public function run(): void { global $register; diff --git a/app/workers/webhooks.php b/app/workers/webhooks.php index 0ef1038243..b0a51ad88a 100644 --- a/app/workers/webhooks.php +++ b/app/workers/webhooks.php @@ -23,7 +23,7 @@ class WebhooksV1 extends Worker { } - public function execute(): void + public function run(): void { global $register; From 764154270719956759576badcf202a23c2355140 Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Wed, 24 Mar 2021 16:51:08 +0100 Subject: [PATCH 086/267] fix execute method name for worker rework --- app/workers.php | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/app/workers.php b/app/workers.php index a0df772da5..ff5ba1dbd3 100644 --- a/app/workers.php +++ b/app/workers.php @@ -23,15 +23,10 @@ $register->set('db', function () { return $pdo; }); -$register->set('influxdb', function () { // Register DB connection - $host = App::getEnv('_APP_INFLUXDB_HOST', ''); - $port = App::getEnv('_APP_INFLUXDB_PORT', ''); +$register->set('cache', function () { // Register cache connection + $redis = new Redis(); + $redis->pconnect(App::getEnv('_APP_REDIS_HOST', ''), App::getEnv('_APP_REDIS_PORT', '')); + $redis->setOption(Redis::OPT_READ_TIMEOUT, -1); - if (empty($host) || empty($port)) { - return; - } - - $client = new InfluxDB\Client($host, $port, '', '', false, false, 5); - - return $client; + return $redis; }); \ No newline at end of file From 6d10550be8a9d82d95b0b031d64b3c16ef97fa20 Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Wed, 24 Mar 2021 16:56:42 +0100 Subject: [PATCH 087/267] add api key to admin calls on tests --- tests/e2e/Services/Realtime/RealtimeBase.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/e2e/Services/Realtime/RealtimeBase.php b/tests/e2e/Services/Realtime/RealtimeBase.php index 1963d9d841..32caea286b 100644 --- a/tests/e2e/Services/Realtime/RealtimeBase.php +++ b/tests/e2e/Services/Realtime/RealtimeBase.php @@ -619,6 +619,7 @@ trait RealtimeBase $function = $this->client->call(Client::METHOD_POST, '/functions', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] ], $this->getHeaders()), [ 'name' => 'Test', 'env' => 'php-7.4', @@ -634,6 +635,7 @@ trait RealtimeBase $tag = $this->client->call(Client::METHOD_POST, '/functions/'.$functionId.'/tags', array_merge([ 'content-type' => 'multipart/form-data', 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] ], $this->getHeaders()), [ 'command' => 'php index.php', 'code' => new CURLFile(realpath(__DIR__ . '/../../../resources/functions/timeout.tar.gz'), 'application/x-gzip', 'php-fx.tar.gz'), @@ -647,6 +649,7 @@ trait RealtimeBase $response = $this->client->call(Client::METHOD_PATCH, '/functions/'.$functionId.'/tag', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] ], $this->getHeaders()), [ 'tag' => $tagId, ]); @@ -657,6 +660,7 @@ trait RealtimeBase $execution = $this->client->call(Client::METHOD_POST, '/functions/'.$functionId.'/executions', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] ], $this->getHeaders()), []); $this->assertEquals($execution['headers']['status-code'], 201); From 2083c5bc89436e4fc3939581e257bb7f32020fce Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Wed, 24 Mar 2021 17:01:43 +0100 Subject: [PATCH 088/267] push tests --- .env | 2 +- tests/e2e/Services/Realtime/RealtimeBase.php | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.env b/.env index 581af6d978..30bacb2a83 100644 --- a/.env +++ b/.env @@ -16,7 +16,7 @@ _APP_DB_PORT=3306 _APP_DB_SCHEMA=appwrite _APP_DB_USER=user _APP_DB_PASS=password -_APP_STORAGE_ANTIVIRUS=enabled +_APP_STORAGE_ANTIVIRUS=disabled _APP_STORAGE_ANTIVIRUS_HOST=clamav _APP_STORAGE_ANTIVIRUS_PORT=3310 _APP_INFLUXDB_HOST=influxdb diff --git a/tests/e2e/Services/Realtime/RealtimeBase.php b/tests/e2e/Services/Realtime/RealtimeBase.php index 32caea286b..184440344a 100644 --- a/tests/e2e/Services/Realtime/RealtimeBase.php +++ b/tests/e2e/Services/Realtime/RealtimeBase.php @@ -620,7 +620,7 @@ trait RealtimeBase 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] - ], $this->getHeaders()), [ + ]), [ 'name' => 'Test', 'env' => 'php-7.4', 'execute' => ['*'], @@ -636,7 +636,7 @@ trait RealtimeBase 'content-type' => 'multipart/form-data', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] - ], $this->getHeaders()), [ + ]), [ 'command' => 'php index.php', 'code' => new CURLFile(realpath(__DIR__ . '/../../../resources/functions/timeout.tar.gz'), 'application/x-gzip', 'php-fx.tar.gz'), ]); @@ -650,7 +650,7 @@ trait RealtimeBase 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] - ], $this->getHeaders()), [ + ]), [ 'tag' => $tagId, ]); @@ -661,7 +661,7 @@ trait RealtimeBase 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] - ], $this->getHeaders()), []); + ]), []); $this->assertEquals($execution['headers']['status-code'], 201); $this->assertNotEmpty($execution['body']['$id']); From 52f33122302d0d31c5bb0b6491b0071cfad32f50 Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Wed, 24 Mar 2021 17:05:50 +0100 Subject: [PATCH 089/267] fix tests --- tests/e2e/Services/Realtime/RealtimeBase.php | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/tests/e2e/Services/Realtime/RealtimeBase.php b/tests/e2e/Services/Realtime/RealtimeBase.php index 184440344a..02f4a21927 100644 --- a/tests/e2e/Services/Realtime/RealtimeBase.php +++ b/tests/e2e/Services/Realtime/RealtimeBase.php @@ -666,16 +666,6 @@ trait RealtimeBase $this->assertEquals($execution['headers']['status-code'], 201); $this->assertNotEmpty($execution['body']['$id']); - $response = json_decode($client->receive(), true); - - $this->assertArrayHasKey('timestamp', $response); - $this->assertCount(3, $response['channels']); - $this->assertContains('executions', $response['channels']); - $this->assertContains('executions.' . $execution['body']['$id'], $response['channels']); - $this->assertContains('functions.' . $execution['body']['functionId'], $response['channels']); - $this->assertEquals('functions.executions.create', $response['event']); - $this->assertNotEmpty($response['payload']); - sleep(6); $response = json_decode($client->receive(), true); From 14070b32961e61e74637bc388343a2654703605e Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Wed, 24 Mar 2021 17:08:07 +0100 Subject: [PATCH 090/267] fix tests --- tests/e2e/Services/Realtime/RealtimeBase.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/e2e/Services/Realtime/RealtimeBase.php b/tests/e2e/Services/Realtime/RealtimeBase.php index 02f4a21927..3af7de1517 100644 --- a/tests/e2e/Services/Realtime/RealtimeBase.php +++ b/tests/e2e/Services/Realtime/RealtimeBase.php @@ -666,8 +666,6 @@ trait RealtimeBase $this->assertEquals($execution['headers']['status-code'], 201); $this->assertNotEmpty($execution['body']['$id']); - sleep(6); - $response = json_decode($client->receive(), true); $this->assertArrayHasKey('timestamp', $response); From 85d13bf801f0733fa30d2fdcf2ce382949aff371 Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Wed, 24 Mar 2021 17:20:06 +0100 Subject: [PATCH 091/267] fix tests --- tests/e2e/Services/Realtime/RealtimeBase.php | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/tests/e2e/Services/Realtime/RealtimeBase.php b/tests/e2e/Services/Realtime/RealtimeBase.php index 3af7de1517..b9942dd8d6 100644 --- a/tests/e2e/Services/Realtime/RealtimeBase.php +++ b/tests/e2e/Services/Realtime/RealtimeBase.php @@ -22,7 +22,7 @@ trait RealtimeBase ]; return new WebSocketClient('ws://appwrite-traefik/v1/realtime?' . http_build_query($query), [ 'headers' => $headers, - 'timeout' => 5, + 'timeout' => 10, ]); } @@ -659,9 +659,8 @@ trait RealtimeBase $execution = $this->client->call(Client::METHOD_POST, '/functions/'.$functionId.'/executions', array_merge([ 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ]), []); + 'x-appwrite-project' => $this->getProject()['$id'] + ], $this->getHeaders()), []); $this->assertEquals($execution['headers']['status-code'], 201); $this->assertNotEmpty($execution['body']['$id']); From ae8061b5ca25def065f92f74e627e28f7473e3ea Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Wed, 24 Mar 2021 17:25:07 +0100 Subject: [PATCH 092/267] fix tests --- tests/e2e/Services/Realtime/RealtimeBase.php | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/e2e/Services/Realtime/RealtimeBase.php b/tests/e2e/Services/Realtime/RealtimeBase.php index b9942dd8d6..fece44b18c 100644 --- a/tests/e2e/Services/Realtime/RealtimeBase.php +++ b/tests/e2e/Services/Realtime/RealtimeBase.php @@ -667,6 +667,16 @@ trait RealtimeBase $response = json_decode($client->receive(), true); + $this->assertArrayHasKey('timestamp', $response); + $this->assertCount(3, $response['channels']); + $this->assertContains('executions', $response['channels']); + $this->assertContains('executions.' . $execution['body']['$id'], $response['channels']); + $this->assertContains('functions.' . $execution['body']['functionId'], $response['channels']); + $this->assertEquals('functions.executions.create', $response['event']); + $this->assertNotEmpty($response['payload']); + + $response = json_decode($client->receive(), true); + $this->assertArrayHasKey('timestamp', $response); $this->assertCount(3, $response['channels']); $this->assertContains('executions', $response['channels']); From 5baca432a8c9d2523e2ed1b21f3a625b61ac8f12 Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Wed, 24 Mar 2021 17:27:59 +0100 Subject: [PATCH 093/267] fix tests --- tests/e2e/Services/Realtime/RealtimeBase.php | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/tests/e2e/Services/Realtime/RealtimeBase.php b/tests/e2e/Services/Realtime/RealtimeBase.php index fece44b18c..8b9a464324 100644 --- a/tests/e2e/Services/Realtime/RealtimeBase.php +++ b/tests/e2e/Services/Realtime/RealtimeBase.php @@ -666,6 +666,7 @@ trait RealtimeBase $this->assertNotEmpty($execution['body']['$id']); $response = json_decode($client->receive(), true); + $responseUpdate = json_decode($client->receive(), true); $this->assertArrayHasKey('timestamp', $response); $this->assertCount(3, $response['channels']); @@ -675,15 +676,13 @@ trait RealtimeBase $this->assertEquals('functions.executions.create', $response['event']); $this->assertNotEmpty($response['payload']); - $response = json_decode($client->receive(), true); - - $this->assertArrayHasKey('timestamp', $response); - $this->assertCount(3, $response['channels']); - $this->assertContains('executions', $response['channels']); - $this->assertContains('executions.' . $execution['body']['$id'], $response['channels']); - $this->assertContains('functions.' . $execution['body']['functionId'], $response['channels']); - $this->assertEquals('functions.executions.update', $response['event']); - $this->assertNotEmpty($response['payload']); + $this->assertArrayHasKey('timestamp', $responseUpdate); + $this->assertCount(3, $responseUpdate['channels']); + $this->assertContains('executions', $responseUpdate['channels']); + $this->assertContains('executions.' . $execution['body']['$id'], $responseUpdate['channels']); + $this->assertContains('functions.' . $execution['body']['functionId'], $responseUpdate['channels']); + $this->assertEquals('functions.executions.update', $responseUpdate['event']); + $this->assertNotEmpty($responseUpdate['payload']); $client->close(); } From 505877475a4ada59b4b6b6ca44a1f3bc1cd1ca2d Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Wed, 24 Mar 2021 17:30:23 +0100 Subject: [PATCH 094/267] fix tests --- tests/e2e/Services/Realtime/RealtimeBase.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/e2e/Services/Realtime/RealtimeBase.php b/tests/e2e/Services/Realtime/RealtimeBase.php index 8b9a464324..164bb025c8 100644 --- a/tests/e2e/Services/Realtime/RealtimeBase.php +++ b/tests/e2e/Services/Realtime/RealtimeBase.php @@ -22,7 +22,7 @@ trait RealtimeBase ]; return new WebSocketClient('ws://appwrite-traefik/v1/realtime?' . http_build_query($query), [ 'headers' => $headers, - 'timeout' => 10, + 'timeout' => 60, ]); } From 24a553616496b6f9760c974e2354a855fed63034 Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Wed, 24 Mar 2021 17:39:22 +0100 Subject: [PATCH 095/267] add permission to event payload --- app/workers/functions.php | 1 + 1 file changed, 1 insertion(+) diff --git a/app/workers/functions.php b/app/workers/functions.php index 00230d7c2d..f89e2ce0f4 100644 --- a/app/workers/functions.php +++ b/app/workers/functions.php @@ -485,6 +485,7 @@ class FunctionsV1 extends Worker ->setParam('event', 'functions.executions.update') ->setParam('payload', [ '$id' => $execution['$id'], + '$permissions' => $execution['$permissions'], 'functionId' => $execution['functionId'], 'dateCreated' => $execution['dateCreated'], 'trigger' => $execution['trigger'], From 8922db85e2a8ea2ebf7b655ca95ccb697c400c08 Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Wed, 24 Mar 2021 17:44:37 +0100 Subject: [PATCH 096/267] add permission to event payload --- app/workers/functions.php | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/app/workers/functions.php b/app/workers/functions.php index f89e2ce0f4..6a78e07a14 100644 --- a/app/workers/functions.php +++ b/app/workers/functions.php @@ -483,18 +483,7 @@ class FunctionsV1 extends Worker ->setParam('projectId', $projectId) ->setParam('userId', $userId) ->setParam('event', 'functions.executions.update') - ->setParam('payload', [ - '$id' => $execution['$id'], - '$permissions' => $execution['$permissions'], - 'functionId' => $execution['functionId'], - 'dateCreated' => $execution['dateCreated'], - 'trigger' => $execution['trigger'], - 'status' => $execution['status'], - 'exitCode' => $execution['exitCode'], - 'stdout' => $execution['stdout'], - 'stderr' => $execution['stderr'], - 'time' => $execution['time'] - ]); + ->setParam('payload', $execution->getArrayCopy()); $executionUpdate->trigger(); From 373b2dfc136877a2f2cfdf1c6a38345c6a74f22f Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Wed, 24 Mar 2021 17:53:44 +0100 Subject: [PATCH 097/267] add realtime event to functions worker --- app/workers/functions.php | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/app/workers/functions.php b/app/workers/functions.php index 6a78e07a14..2a151d168e 100644 --- a/app/workers/functions.php +++ b/app/workers/functions.php @@ -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\Event\Realtime; use Appwrite\Resque\Worker; use Cron\CronExpression; use Swoole\Runtime; @@ -487,6 +488,14 @@ class FunctionsV1 extends Worker $executionUpdate->trigger(); + $realtimeUpdate = new Realtime('', '', []); + + $realtimeUpdate + ->setEvent('functions.executions.update') + ->setProject($projectId) + ->setPayload($execution->getArrayCopy()) + ->trigger(); + $usage = new Event('v1-usage', 'UsageV1'); $usage From d5a3879ede795865b5ad3b282da659b45d212f61 Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Thu, 25 Mar 2021 16:41:56 +0100 Subject: [PATCH 098/267] clean up realtime even trigger in functions worker --- app/workers/functions.php | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/app/workers/functions.php b/app/workers/functions.php index 2a151d168e..4aa46647e9 100644 --- a/app/workers/functions.php +++ b/app/workers/functions.php @@ -488,13 +488,9 @@ class FunctionsV1 extends Worker $executionUpdate->trigger(); - $realtimeUpdate = new Realtime('', '', []); + $realtimeUpdate = new Realtime($projectId, 'functions.executions.update', $execution->getArrayCopy()); - $realtimeUpdate - ->setEvent('functions.executions.update') - ->setProject($projectId) - ->setPayload($execution->getArrayCopy()) - ->trigger(); + $realtimeUpdate->trigger(); $usage = new Event('v1-usage', 'UsageV1'); From ccf151c36bd6722bed7b87ed0bcebe95c8cda149 Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Thu, 25 Mar 2021 16:53:02 +0100 Subject: [PATCH 099/267] fix usage worker without functions args --- app/workers/usage.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/workers/usage.php b/app/workers/usage.php index f8996fb612..3f56af3c86 100644 --- a/app/workers/usage.php +++ b/app/workers/usage.php @@ -37,10 +37,10 @@ class UsageV1 extends Worker $httpMethod = $this->args['httpMethod']; $httpRequest = $this->args['httpRequest']; - $functionId = $this->args['functionId']; - $functionExecution = $this->args['functionExecution']; - $functionExecutionTime = $this->args['functionExecutionTime']; - $functionStatus = $this->args['functionStatus']; + $functionId = $this->args['functionId'] ?? null; + $functionExecution = $this->args['functionExecution'] ?? null; + $functionExecutionTime = $this->args['functionExecutionTime'] ?? null; + $functionStatus = $this->args['functionStatus'] ?? null; $tags = ",project={$projectId},version=".App::getEnv('_APP_VERSION', 'UNKNOWN').''; From 9cdb7f73a2964d5e18563fa5c5f57a82f73392be Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Thu, 25 Mar 2021 16:58:11 +0100 Subject: [PATCH 100/267] add null safety to usage worker --- app/workers/usage.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/workers/usage.php b/app/workers/usage.php index 3f56af3c86..b09ecce27e 100644 --- a/app/workers/usage.php +++ b/app/workers/usage.php @@ -29,13 +29,13 @@ class UsageV1 extends Worker $projectId = $this->args['projectId']; - $storage = $this->args['storage']; - $networkRequestSize = $this->args['networkRequestSize']; $networkResponseSize = $this->args['networkResponseSize']; + + $storage = $this->args['storage'] ?? null; - $httpMethod = $this->args['httpMethod']; - $httpRequest = $this->args['httpRequest']; + $httpMethod = $this->args['httpMethod'] ?? null; + $httpRequest = $this->args['httpRequest'] ?? null; $functionId = $this->args['functionId'] ?? null; $functionExecution = $this->args['functionExecution'] ?? null; From 82f12625c03937104c4f312dadae58f592a3d421 Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Mon, 29 Mar 2021 10:54:42 +0200 Subject: [PATCH 101/267] docs: adapt to realtime --- CONTRIBUTING.md | 2 + docs/specs/realtime.drawio.svg | 355 ++++++++++++++++----------------- 2 files changed, 174 insertions(+), 183 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8d25b2578a..dd22d70e99 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -163,6 +163,8 @@ Appwrite's current structure is a combination of both [Monolithic](https://en.wi │ ├── Migration │ ├── Network │ ├── OpenSSL +│ ├── Realtime +│ ├── Resque │ ├── Specification │ ├── Task │ ├── Template diff --git a/docs/specs/realtime.drawio.svg b/docs/specs/realtime.drawio.svg index e2f07ef7f5..e20e9c7ce0 100644 --- a/docs/specs/realtime.drawio.svg +++ b/docs/specs/realtime.drawio.svg @@ -1,4 +1,4 @@ - + @@ -39,30 +39,11 @@ - -
-
-
- Messaging Worker -
-
-
-
- - Messaging Worker - -
-
- - - - -
Redis - PUB/SUB @@ -70,39 +51,39 @@
- + Redis - PUB/SUB - - - - - + + + + + -
+
- Websocket 1 + Realtime Worker 1
- - Websocket 1 + + Realtime Worker 1 - - - + + + -
+
Connections @@ -110,35 +91,35 @@
- + Connections - - - + + + -
+
- Events + Subscriptions
- - Events + + Subscriptions - + -
+
Authorization @@ -146,58 +127,58 @@
- + Authorization - - - - - + + + + + -
+
- Websocket 2 + Realtime Worker 2
- - Websocket 2 + + Realtime Worker 2 - - - - - + + + + + -
+
- Websocket 3 + Realtime Worker
- - Websocket 3 + + Realtime Worker - + -
+
Pull @@ -205,50 +186,54 @@
- + Pull + + + + + + +
+
+
+ Connections +
+
+
+
+ + Connections + +
+
+ + + + + + +
+
+
+ Subscriptions +
+
+
+
+ + Subscriptions + +
+
-
-
- Connections -
-
-
-
- - Connections - -
-
- - - - -
-
-
- Events -
-
-
-
- - Events - -
-
- - - - -
Authorization @@ -256,50 +241,54 @@
- + Authorization + + + + + + +
+
+
+ Connections +
+
+
+
+ + Connections + +
+
+ + + + + + +
+
+
+ Subscriptions +
+
+
+
+ + Subscriptions + +
+
-
-
- Connections -
-
-
-
- - Connections - -
-
- - - - -
-
-
- Events -
-
-
-
- - Events - -
-
- - - - -
Authorization @@ -307,26 +296,26 @@
- + Authorization - - - - - - - - - - - + + + + + + + + + + + -
+
Clients @@ -334,22 +323,22 @@
- + Clients - - - - - - - + + + + + + + -
+
LoadBalancer @@ -357,7 +346,7 @@
- + LoadBalancer @@ -388,11 +377,11 @@ - + -
+
Pull @@ -400,16 +389,16 @@
- + Pull - + -
+
Pull @@ -417,16 +406,16 @@
- + Pull - + -
+
Websocket @@ -434,66 +423,66 @@
- + Websocket - + -
+
- MQTT + Websocket
- - MQTT + + Websocket - + -
+
- Socket.io + Websocket
- - Socket.io + + Websocket - + -
+
- SSE + Websocket
- - SSE + + Websocket - + Viewer does not support full SVG 1.1 From 0df0ad9039a951036a4c5741d7fd0d30acc81af0 Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Mon, 29 Mar 2021 10:56:06 +0200 Subject: [PATCH 102/267] docs: adapt to realtime --- docs/specs/realtime.drawio.svg | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/specs/realtime.drawio.svg b/docs/specs/realtime.drawio.svg index e20e9c7ce0..79918e1bf9 100644 --- a/docs/specs/realtime.drawio.svg +++ b/docs/specs/realtime.drawio.svg @@ -1,4 +1,4 @@ - + @@ -164,13 +164,13 @@
- Realtime Worker + Realtime Worker 3
- Realtime Worker + Realtime Worker 3
From 5228ad799023acd16bec65e7dd32e862bb80dfe8 Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Mon, 29 Mar 2021 10:58:43 +0200 Subject: [PATCH 103/267] docs: add to changes --- CHANGES.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGES.md b/CHANGES.md index bc16e9a8b7..aaca13b99b 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,7 @@ # Version 0.8.0 (Not Released Yet) ## Features +- Realtime Integration (#948) - Anonymous login (#914) - Added events for functions and executions (#971) From c8f9bbc34bf59ac9f2a8120443cad3c85aa08690 Mon Sep 17 00:00:00 2001 From: kodumbeats Date: Tue, 30 Mar 2021 16:36:07 -0400 Subject: [PATCH 104/267] Outline calls to docker socket --- app/workers/functions.php | 83 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 80 insertions(+), 3 deletions(-) diff --git a/app/workers/functions.php b/app/workers/functions.php index 87e3a86245..b18d6178a8 100644 --- a/app/workers/functions.php +++ b/app/workers/functions.php @@ -16,7 +16,7 @@ require_once __DIR__.'/../init.php'; Console::title('Functions V1 Worker'); -Runtime::setHookFlags(SWOOLE_HOOK_ALL); +// Runtime::setHookFlags(SWOOLE_HOOK_ALL); Console::success(APP_NAME.' functions worker v1 has started'); @@ -343,6 +343,14 @@ class FunctionsV1 'APPWRITE_FUNCTION_EVENT_PAYLOAD' => $payload, ]); + $tmpvars = $vars; + \array_walk($tmpvars, function (&$value, $key) { + $key = $this->filterEnvKey($key); + $value = \escapeshellarg((empty($value)) ? 'null' : $value); + // $value = "--env {$key}={$value}"; + $value = "{$key}={$value}"; + }); + \array_walk($vars, function (&$value, $key) { $key = $this->filterEnvKey($key); $value = \escapeshellarg((empty($value)) ? 'null' : $value); @@ -441,10 +449,79 @@ class FunctionsV1 $stdout = ''; $stderr = ''; + + $executionStart = \microtime(true); + $envs = \array_merge(\array_values($tmpvars), ["executionStart={$executionStart}"]); + var_dump($envs); + + /* + * Create execution via Docker API + */ + $ch = \curl_init(); + var_dump($executionStart); + + \curl_setopt($ch, CURLOPT_URL, "http://localhost/containers/{$container}/exec"); + \curl_setopt($ch, CURLOPT_UNIX_SOCKET_PATH, '/var/run/docker.sock'); + \curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); + \curl_setopt($ch, CURLOPT_POST, 1); + \curl_setopt($ch, CURLOPT_POSTFIELDS, [ + "Env" => $envs, + "Cmd" => $command, + ]); + + $headers = array(); + $headers[] = 'Content-Type: application/json'; + \curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); + + $result = \curl_exec($ch); + var_dump($result); + + if (\curl_errno($ch)) { + echo 'Error:' . \curl_error($ch); + } + + \curl_close($ch); + + /* + * Query for event 'exec_die' + */ + // $ch = \curl_init(); +// + // $URL = 'http://localhost/events'; + // $params = [ + // 'filter' => json_encode([ + // 'type' => 'container', + // 'container' => $container, + // 'event' => 'exec_die', + // ]), + // 'since' => \floor($executionStart) + // 'until' => $executionStart + +App::getEnv('_APP_FUNCTIONS_TIMEOUT', 900) + // ]; + // $URL = $URL . '?' . \http_build_query($params); + // var_dump($URL); +// + // \curl_setopt($ch, CURLOPT_URL, $URL); + // \curl_setopt($ch, CURLOPT_UNIX_SOCKET_PATH, '/var/run/docker.sock'); + // \curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); + + // $headers = array(); + // $headers[] = 'Content-Type: application/json'; + // \curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); + + // $result = \curl_exec($ch); +// + // if (\curl_errno($ch)) { + // echo 'Error:' . \curl_error($ch); + // } + // var_dump($result); + // var_dump(\microtime(true) - $executionStart); +// + // \curl_close($ch); + - $exitCode = Console::execute("docker exec ".\implode(" ", $vars)." {$container} {$command}" - , '', $stdout, $stderr, $function->getAttribute('timeout', (int) App::getEnv('_APP_FUNCTIONS_TIMEOUT', 900))); + // $exitCode = Console::execute("docker exec ".\implode(" ", $vars)." {$container} {$command}" + // , '', $stdout, $stderr, $function->getAttribute('timeout', (int) App::getEnv('_APP_FUNCTIONS_TIMEOUT', 900))); $executionEnd = \microtime(true); $executionTime = ($executionEnd - $executionStart); From 967d661a59fcac24d2b5a35d8217761a14997190 Mon Sep 17 00:00:00 2001 From: kodumbeats Date: Tue, 30 Mar 2021 18:13:44 -0400 Subject: [PATCH 105/267] Pass Content-Length header --- app/workers/functions.php | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/app/workers/functions.php b/app/workers/functions.php index b18d6178a8..016d7ded6b 100644 --- a/app/workers/functions.php +++ b/app/workers/functions.php @@ -453,26 +453,34 @@ class FunctionsV1 $executionStart = \microtime(true); $envs = \array_merge(\array_values($tmpvars), ["executionStart={$executionStart}"]); - var_dump($envs); + // var_dump($envs); /* * Create execution via Docker API */ $ch = \curl_init(); - var_dump($executionStart); \curl_setopt($ch, CURLOPT_URL, "http://localhost/containers/{$container}/exec"); \curl_setopt($ch, CURLOPT_UNIX_SOCKET_PATH, '/var/run/docker.sock'); \curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); \curl_setopt($ch, CURLOPT_POST, 1); - \curl_setopt($ch, CURLOPT_POSTFIELDS, [ - "Env" => $envs, - "Cmd" => $command, - ]); - $headers = array(); - $headers[] = 'Content-Type: application/json'; + $body = array( + "Env" => $envs, + "Cmd" => $command + ); + var_dump($body); + $body = json_encode($body); + var_dump($body); + + \curl_setopt($ch, CURLOPT_POSTFIELDS, $body); + + $headers = [ + 'Content-Type: application/json', + 'Content-Length: ' . \strlen($body) + ]; \curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); + var_dump($headers); $result = \curl_exec($ch); var_dump($result); From af6b90ba8331d49bfdc8b69a47dbc6ccf4f54528 Mon Sep 17 00:00:00 2001 From: kodumbeats Date: Tue, 30 Mar 2021 20:49:23 -0400 Subject: [PATCH 106/267] Testing if the exec must be started --- app/workers/functions.php | 78 +++++++++++++++++++++++++++++---------- 1 file changed, 58 insertions(+), 20 deletions(-) diff --git a/app/workers/functions.php b/app/workers/functions.php index 016d7ded6b..50392ce67e 100644 --- a/app/workers/functions.php +++ b/app/workers/functions.php @@ -467,7 +467,9 @@ class FunctionsV1 $body = array( "Env" => $envs, - "Cmd" => $command + "Cmd" => \explode(' ', $command), + "AttachStdout" => true, + "AttachStderr" => true ); var_dump($body); $body = json_encode($body); @@ -483,6 +485,9 @@ class FunctionsV1 var_dump($headers); $result = \curl_exec($ch); + $resultDecoded = json_decode($result, true); + $execId = $resultDecoded['Id']; + var_dump($result); if (\curl_errno($ch)) { @@ -491,12 +496,46 @@ class FunctionsV1 \curl_close($ch); + + + /* + * Maybe just creating the function doesnt start it? + * Currently throws errors + */ + + $ch = \curl_init(); + $URL = "http://localhost/exec/{$execId}/start"; + \curl_setopt($ch, CURLOPT_URL, $URL); + \curl_setopt($ch, CURLOPT_UNIX_SOCKET_PATH, '/var/run/docker.sock'); + \curl_setopt($ch, CURLOPT_POST, 1); + \curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([])); + \curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); + + $headers = array(); + $headers[] = 'Content-Type: application/json'; + \curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); + + $result = \curl_exec($ch); + + if (\curl_errno($ch)) { + echo 'Error:' . \curl_error($ch); + } + var_dump($result); + + \curl_close($ch); + + + + + + + sleep(5); /* * Query for event 'exec_die' */ - // $ch = \curl_init(); -// - // $URL = 'http://localhost/events'; + $ch = \curl_init(); + + $URL = "http://localhost/exec/{$execId}/json"; // $params = [ // 'filter' => json_encode([ // 'type' => 'container', @@ -508,24 +547,23 @@ class FunctionsV1 // ]; // $URL = $URL . '?' . \http_build_query($params); // var_dump($URL); -// - // \curl_setopt($ch, CURLOPT_URL, $URL); - // \curl_setopt($ch, CURLOPT_UNIX_SOCKET_PATH, '/var/run/docker.sock'); - // \curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); - // $headers = array(); - // $headers[] = 'Content-Type: application/json'; - // \curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); + \curl_setopt($ch, CURLOPT_URL, $URL); + \curl_setopt($ch, CURLOPT_UNIX_SOCKET_PATH, '/var/run/docker.sock'); + \curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); - // $result = \curl_exec($ch); -// - // if (\curl_errno($ch)) { - // echo 'Error:' . \curl_error($ch); - // } - // var_dump($result); - // var_dump(\microtime(true) - $executionStart); -// - // \curl_close($ch); + $headers = array(); + $headers[] = 'Content-Type: application/json'; + \curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); + + $result = \curl_exec($ch); + + if (\curl_errno($ch)) { + echo 'Error:' . \curl_error($ch); + } + var_dump($result); + + \curl_close($ch); // $exitCode = Console::execute("docker exec ".\implode(" ", $vars)." {$container} {$command}" From 30506c9b0026703f23d9909156d40378a70154a9 Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Wed, 31 Mar 2021 12:10:29 +0200 Subject: [PATCH 107/267] add debug logs --- app/realtime.php | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/app/realtime.php b/app/realtime.php index 572b578301..8bd7e2d6e2 100644 --- a/app/realtime.php +++ b/app/realtime.php @@ -63,7 +63,7 @@ $server->on('workerStart', function ($server, $workerId) use (&$subscriptions, & Console::error('Pub/sub failed (worker: ' . $workerId . ')'); } - $redis->subscribe(['realtime'], function ($redis, $channel, $payload) use ($server, &$connections, &$subscriptions) { + $redis->subscribe(['realtime'], function ($redis, $channel, $payload) use ($server, $workerId, &$subscriptions) { /** * Supported Resources: * - Collection @@ -80,6 +80,12 @@ $server->on('workerStart', function ($server, $workerId) use (&$subscriptions, & $receivers = Realtime::identifyReceivers($event, $subscriptions); + 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); + } + foreach ($receivers as $receiver) { if ($server->exist($receiver) && $server->isEstablished($receiver)) { $server->push( @@ -212,6 +218,11 @@ $server->on('open', function (Server $server, Request $request) use (&$connectio 'code' => $th->getCode(), 'message' => $th->getMessage() ]; + if (App::isDevelopment()) { + Console::error("[Error] Connection Error"); + Console::error("[Error] Code: " . $response['code']); + Console::error("[Error] Message: " . $response['message']); + } $server->push($connection, json_encode($response)); $server->close($connection); } From 1f4529d4e43ec9d6666ad2f76e45cadc8f5b59d4 Mon Sep 17 00:00:00 2001 From: kodumbeats Date: Wed, 31 Mar 2021 10:58:04 -0400 Subject: [PATCH 108/267] Start execution and get status code --- app/workers/functions.php | 70 ++++++++++++++++++--------------------- 1 file changed, 32 insertions(+), 38 deletions(-) diff --git a/app/workers/functions.php b/app/workers/functions.php index 50392ce67e..68b62c8cb1 100644 --- a/app/workers/functions.php +++ b/app/workers/functions.php @@ -450,10 +450,9 @@ class FunctionsV1 $stderr = ''; - $executionStart = \microtime(true); - $envs = \array_merge(\array_values($tmpvars), ["executionStart={$executionStart}"]); - // var_dump($envs); + $envs = array_values($tmpvars); + $envs[] = "executionStart={$executionStart}"; /* * Create execution via Docker API @@ -465,30 +464,26 @@ class FunctionsV1 \curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); \curl_setopt($ch, CURLOPT_POST, 1); - $body = array( + $body = array( "Env" => $envs, "Cmd" => \explode(' ', $command), "AttachStdout" => true, "AttachStderr" => true ); - var_dump($body); $body = json_encode($body); - var_dump($body); - \curl_setopt($ch, CURLOPT_POSTFIELDS, $body); + \curl_setopt($ch, CURLOPT_POSTFIELDS, $body); - $headers = [ + $headers = [ 'Content-Type: application/json', 'Content-Length: ' . \strlen($body) ]; \curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); - var_dump($headers); $result = \curl_exec($ch); $resultDecoded = json_decode($result, true); $execId = $resultDecoded['Id']; - var_dump($result); if (\curl_errno($ch)) { echo 'Error:' . \curl_error($ch); @@ -497,10 +492,8 @@ class FunctionsV1 \curl_close($ch); - /* - * Maybe just creating the function doesnt start it? - * Currently throws errors + * Start execution without detatching - will receive stdout/stderr as response */ $ch = \curl_init(); @@ -508,46 +501,44 @@ class FunctionsV1 \curl_setopt($ch, CURLOPT_URL, $URL); \curl_setopt($ch, CURLOPT_UNIX_SOCKET_PATH, '/var/run/docker.sock'); \curl_setopt($ch, CURLOPT_POST, 1); - \curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([])); + \curl_setopt($ch, CURLOPT_POSTFIELDS, '{}'); \curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); - $headers = array(); - $headers[] = 'Content-Type: application/json'; + $headers = [ + 'Content-Type: application/json', + 'Content-Length: 2', + ]; \curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); $result = \curl_exec($ch); + var_dump($result); if (\curl_errno($ch)) { echo 'Error:' . \curl_error($ch); } - var_dump($result); \curl_close($ch); + sleep(1); - - - - - sleep(5); + // TODO@kodumbeats: set up listener for /events for exec_start and exec_die + // We need this to get accurate + // $params = [ + // 'filter' => json_encode([ + // 'type' => 'container', + // 'container' => $container, + // 'event' => 'exec_die', + // ]), + // 'since' => \floor($executionStart) + // 'until' => $executionStart + +App::getEnv('_APP_FUNCTIONS_TIMEOUT', 900) + // ]; + // $URL = $URL . '?' . \http_build_query($params); /* - * Query for event 'exec_die' + * Get execution details */ $ch = \curl_init(); $URL = "http://localhost/exec/{$execId}/json"; - // $params = [ - // 'filter' => json_encode([ - // 'type' => 'container', - // 'container' => $container, - // 'event' => 'exec_die', - // ]), - // 'since' => \floor($executionStart) - // 'until' => $executionStart + +App::getEnv('_APP_FUNCTIONS_TIMEOUT', 900) - // ]; - // $URL = $URL . '?' . \http_build_query($params); - // var_dump($URL); - \curl_setopt($ch, CURLOPT_URL, $URL); \curl_setopt($ch, CURLOPT_UNIX_SOCKET_PATH, '/var/run/docker.sock'); \curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); @@ -557,11 +548,12 @@ class FunctionsV1 \curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); $result = \curl_exec($ch); + $execData = json_decode($result, true); // Can get the exit code from this call. + $exitCode = $execData['ExitCode']; if (\curl_errno($ch)) { echo 'Error:' . \curl_error($ch); } - var_dump($result); \curl_close($ch); @@ -581,8 +573,10 @@ class FunctionsV1 'tagId' => $tag->getId(), 'status' => $functionStatus, 'exitCode' => $exitCode, - 'stdout' => \mb_substr($stdout, -4000), // log last 4000 chars output - 'stderr' => \mb_substr($stderr, -4000), // log last 4000 chars output + // 'stdout' => \mb_substr($stdout, -4000), // log last 4000 chars output + 'stdout' => 'Not added yet', + // 'stderr' => \mb_substr($stderr, -4000), // log last 4000 chars output + 'stderr' => 'Not added yet', 'time' => $executionTime, ])); From 28dece1adfc5b43ed759c2d329eb3a2ab9c5b7f5 Mon Sep 17 00:00:00 2001 From: kodumbeats Date: Wed, 31 Mar 2021 15:41:35 -0400 Subject: [PATCH 109/267] Investigate latency of exec --- app/workers/functions.php | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/app/workers/functions.php b/app/workers/functions.php index 68b62c8cb1..c1f76b9d02 100644 --- a/app/workers/functions.php +++ b/app/workers/functions.php @@ -472,6 +472,7 @@ class FunctionsV1 ); $body = json_encode($body); + $createExecStart = microtime(true); \curl_setopt($ch, CURLOPT_POSTFIELDS, $body); $headers = [ @@ -491,11 +492,14 @@ class FunctionsV1 \curl_close($ch); + $createExecTime = microtime(true)- $createExecStart; + var_dump($createExecTime); /* * Start execution without detatching - will receive stdout/stderr as response */ + $startExecStart = microtime(true); $ch = \curl_init(); $URL = "http://localhost/exec/{$execId}/start"; \curl_setopt($ch, CURLOPT_URL, $URL); @@ -513,11 +517,14 @@ class FunctionsV1 $result = \curl_exec($ch); var_dump($result); + if (\curl_errno($ch)) { echo 'Error:' . \curl_error($ch); } \curl_close($ch); + $startExecTime = microtime(true)- $startExecStart; + var_dump($startExecTime); sleep(1); From c5ad66839d5808f8787cb2aebfc63bc8d072b0e7 Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Thu, 1 Apr 2021 12:59:11 +0200 Subject: [PATCH 110/267] add usage stats for realtime --- app/init.php | 2 +- app/realtime.php | 59 +++++++++++++++++++++++++++++---- app/views/install/compose.phtml | 1 + app/workers/usage.php | 19 ++++++++--- docker-compose.yml | 1 + 5 files changed, 71 insertions(+), 11 deletions(-) diff --git a/app/init.php b/app/init.php index b350cb9c5c..2620abe7e1 100644 --- a/app/init.php +++ b/app/init.php @@ -378,7 +378,7 @@ App::setResource('user', function($mode, $project, $console, $request, $response /** @var Appwrite\Database\Document $project */ /** @var Appwrite\Database\Database $consoleDB */ /** @var Appwrite\Database\Database $projectDB */ - /** @var bool $mode */ + /** @var string $mode */ Authorization::setDefaultStatus(true); diff --git a/app/realtime.php b/app/realtime.php index 8bd7e2d6e2..1e161a2adb 100644 --- a/app/realtime.php +++ b/app/realtime.php @@ -4,12 +4,15 @@ require_once __DIR__ . '/init.php'; use Appwrite\Database\Pool\PDOPool; use Appwrite\Database\Pool\RedisPool; +use Appwrite\Event\Event; use Appwrite\Network\Validator\Origin; use Appwrite\Realtime\Realtime; use Appwrite\Utopia\Response; -use Swoole\Process; use Swoole\Http\Request; use Swoole\Http\Response as SwooleResponse; +use Swoole\Process; +use Swoole\Table; +use Swoole\Timer; use Swoole\WebSocket\Frame; use Swoole\WebSocket\Server; use Utopia\App; @@ -38,7 +41,46 @@ $server->set([ $subscriptions = []; $connections = []; -$server->on('workerStart', function ($server, $workerId) use (&$subscriptions, &$connections, &$register) { +$stats = new Table(4096, 1); +$stats->column('projectId', Table::TYPE_STRING, 64); +$stats->column('connections', Table::TYPE_INT); +$stats->column('messages', Table::TYPE_INT); +$stats->create(); + +/** + * Sends usage stats every 10 seconds. + */ +Timer::tick(10000, function () use (&$stats) { + /** @var Table $stats */ + 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, array( + 'projectId' => $projectId, + 'messages' => 0, + 'connections' => 0 + )); + + if (App::getEnv('_APP_USAGE_STATS', 'enabled') == 'enabled') { + $usage->trigger(); + } + } +}); + +$server->on('workerStart', function ($server, $workerId) use (&$subscriptions, &$register, &$stats) { Console::success('Worker ' . $workerId . ' started succefully'); $attempts = 0; @@ -63,7 +105,7 @@ $server->on('workerStart', function ($server, $workerId) use (&$subscriptions, & Console::error('Pub/sub failed (worker: ' . $workerId . ')'); } - $redis->subscribe(['realtime'], function ($redis, $channel, $payload) use ($server, $workerId, &$subscriptions) { + $redis->subscribe(['realtime'], function ($redis, $channel, $payload) use ($server, $workerId, &$subscriptions, &$stats) { /** * Supported Resources: * - Collection @@ -98,6 +140,9 @@ $server->on('workerStart', function ($server, $workerId) use (&$subscriptions, & $server->close($receiver); } } + if (($num = count($receivers)) > 0) { + $stats->incr($event['project'], 'messages', $num); + } }); } catch (\Throwable $th) { Console::error('Pub/sub error: ' . $th->getMessage()); @@ -124,7 +169,7 @@ $server->on('start', function (Server $server) { }); }); -$server->on('open', function (Server $server, Request $request) use (&$connections, &$subscriptions, &$register) { +$server->on('open', function (Server $server, Request $request) use (&$connections, &$subscriptions, &$register, &$stats) { $app = new App('UTC'); $connection = $request->fd; $request = new SwooleRequest($request); @@ -135,11 +180,11 @@ $server->on('open', function (Server $server, Request $request) use (&$connectio $register->set('db', function () use (&$db) { return $db; }); - + $register->set('cache', function () use (&$redis) { // Register cache connection return $redis; }); - + Console::info("Connection open (user: {$connection}, worker: {$server->getWorkerId()})"); App::setResource('request', function () use ($request) { @@ -213,6 +258,8 @@ $server->on('open', function (Server $server, Request $request) use (&$connectio Realtime::subscribe($project->getId(), $connection, $roles, $subscriptions, $connections, $channels); $server->push($connection, json_encode($channels)); + + $stats->incr($project->getId(), 'connections'); } catch (\Throwable $th) { $response = [ 'code' => $th->getCode(), diff --git a/app/views/install/compose.phtml b/app/views/install/compose.phtml index 27dfbc4d00..c51a67f81c 100644 --- a/app/views/install/compose.phtml +++ b/app/views/install/compose.phtml @@ -141,6 +141,7 @@ services: - _APP_DB_SCHEMA - _APP_DB_USER - _APP_DB_PASS + - _APP_USAGE_STATS appwrite-worker-usage: image: appwrite/appwrite: diff --git a/app/workers/usage.php b/app/workers/usage.php index b09ecce27e..74363dccb5 100644 --- a/app/workers/usage.php +++ b/app/workers/usage.php @@ -25,12 +25,13 @@ class UsageV1 extends Worker { global $register; + /** @var \Domnikl\Statsd\Client $statsd */ $statsd = $register->get('statsd', true); $projectId = $this->args['projectId']; - $networkRequestSize = $this->args['networkRequestSize']; - $networkResponseSize = $this->args['networkResponseSize']; + $networkRequestSize = $this->args['networkRequestSize'] ?? 0; + $networkResponseSize = $this->args['networkResponseSize'] ?? 0; $storage = $this->args['storage'] ?? null; @@ -42,7 +43,10 @@ class UsageV1 extends Worker $functionExecutionTime = $this->args['functionExecutionTime'] ?? null; $functionStatus = $this->args['functionStatus'] ?? null; - $tags = ",project={$projectId},version=".App::getEnv('_APP_VERSION', 'UNKNOWN').''; + $realtimeConnections = $this->args['realtimeConnections'] ?? null; + $realtimeMessages = $this->args['realtimeMessages'] ?? null; + + $tags = ",project={$projectId},version=".App::getEnv('_APP_VERSION', 'UNKNOWN'); // the global namespace is prepended to every key (optional) $statsd->setNamespace('appwrite.usage'); @@ -53,10 +57,17 @@ class UsageV1 extends Worker if($functionExecution >= 1) { $statsd->increment('executions.all'.$tags.',functionId='.$functionId.',functionStatus='.$functionStatus); - var_dump($tags.',functionId='.$functionId.',functionStatus='.$functionStatus); $statsd->count('executions.time'.$tags.',functionId='.$functionId, $functionExecutionTime); } + if($realtimeConnections >= 1) { + $statsd->count('realtime.clients'.$tags, $realtimeConnections); + } + + if($realtimeMessages >= 1) { + $statsd->count('realtime.message'.$tags, $realtimeMessages); + } + $statsd->count('network.inbound'.$tags, $networkRequestSize); $statsd->count('network.outbound'.$tags, $networkResponseSize); $statsd->count('network.all'.$tags, $networkRequestSize + $networkResponseSize); diff --git a/docker-compose.yml b/docker-compose.yml index 9d9577a9f5..bbca1fcb79 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -161,6 +161,7 @@ services: - _APP_DB_SCHEMA - _APP_DB_USER - _APP_DB_PASS + - _APP_USAGE_STATS appwrite-worker-usage: entrypoint: worker-usage From de47622d03e2cbac8e191e252cf61899be1b4f67 Mon Sep 17 00:00:00 2001 From: kodumbeats Date: Thu, 1 Apr 2021 09:31:30 -0400 Subject: [PATCH 111/267] Remove sleep and pass expected vars to logs --- app/workers/functions.php | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/app/workers/functions.php b/app/workers/functions.php index c1f76b9d02..2a53720caf 100644 --- a/app/workers/functions.php +++ b/app/workers/functions.php @@ -514,9 +514,8 @@ class FunctionsV1 ]; \curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); - $result = \curl_exec($ch); - var_dump($result); - + $startData = \curl_exec($ch); + var_dump($startData); if (\curl_errno($ch)) { echo 'Error:' . \curl_error($ch); @@ -526,8 +525,6 @@ class FunctionsV1 $startExecTime = microtime(true)- $startExecStart; var_dump($startExecTime); - sleep(1); - // TODO@kodumbeats: set up listener for /events for exec_start and exec_die // We need this to get accurate // $params = [ @@ -564,7 +561,6 @@ class FunctionsV1 \curl_close($ch); - // $exitCode = Console::execute("docker exec ".\implode(" ", $vars)." {$container} {$command}" // , '', $stdout, $stderr, $function->getAttribute('timeout', (int) App::getEnv('_APP_FUNCTIONS_TIMEOUT', 900))); @@ -572,7 +568,7 @@ class FunctionsV1 $executionTime = ($executionEnd - $executionStart); $functionStatus = ($exitCode === 0) ? 'completed' : 'failed'; - Console::info("Function executed in " . ($executionEnd - $executionStart) . " seconds with exit code {$exitCode}"); + Console::info("Function executed in " . ($startExecTime) . " seconds with exit code {$exitCode}"); Authorization::disable(); @@ -581,10 +577,10 @@ class FunctionsV1 'status' => $functionStatus, 'exitCode' => $exitCode, // 'stdout' => \mb_substr($stdout, -4000), // log last 4000 chars output - 'stdout' => 'Not added yet', + 'stdout' => ($exitCode === 0) ? \mb_substr($startData, -4000) : '', // 'stderr' => \mb_substr($stderr, -4000), // log last 4000 chars output - 'stderr' => 'Not added yet', - 'time' => $executionTime, + 'stderr' => (!$exitCode === 0) ? \mb_substr($startData, -4000) : '', + 'time' => $startExecTime, ])); Authorization::reset(); From 807beb13590635af159af0618f5293478d739412 Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Tue, 6 Apr 2021 12:11:57 +0200 Subject: [PATCH 112/267] add project channel to console project to receive realtime stats --- app/realtime.php | 60 +++++++++++++++++++++++++++++++++++++----------- 1 file changed, 47 insertions(+), 13 deletions(-) diff --git a/app/realtime.php b/app/realtime.php index 1e161a2adb..9ec6c6c691 100644 --- a/app/realtime.php +++ b/app/realtime.php @@ -44,6 +44,7 @@ $connections = []; $stats = new Table(4096, 1); $stats->column('projectId', Table::TYPE_STRING, 64); $stats->column('connections', Table::TYPE_INT); +$stats->column('connectionsTotal', Table::TYPE_INT); $stats->column('messages', Table::TYPE_INT); $stats->create(); @@ -68,11 +69,11 @@ Timer::tick(10000, function () use (&$stats) { ->setParam('networkRequestSize', 0) ->setParam('networkResponseSize', 0); - $stats->set($projectId, array( + $stats->set($projectId, [ 'projectId' => $projectId, 'messages' => 0, 'connections' => 0 - )); + ]); if (App::getEnv('_APP_USAGE_STATS', 'enabled') == 'enabled') { $usage->trigger(); @@ -87,6 +88,37 @@ $server->on('workerStart', function ($server, $workerId) use (&$subscriptions, & $start = time(); $redisPool = $register->get('redisPool'); + /** + * Sending current connections to project channels on the console project every 5 seconds. + */ + $server->tick(5000, function () use (&$server, &$subscriptions, &$stats) { + if ( + array_key_exists('console', $subscriptions) + && array_key_exists('role:member', $subscriptions['console']) + && array_key_exists('project', $subscriptions['console']['role:member']) + ) { + $payload = []; + foreach ($stats as $projectId => $value) { + $payload[$projectId] = $value['connectionsTotal']; + } + foreach ($subscriptions['console']['role:member']['project'] as $connection => $value) { + foreach ($stats as $projectId => $value) { + $server->push( + $connection, + json_encode([ + 'event' => 'stats.connections', + 'channels' => ['project'], + 'timestamp' => time(), + 'payload' => $payload + ]), + SWOOLE_WEBSOCKET_OPCODE_TEXT, + SWOOLE_WEBSOCKET_FLAG_FIN | SWOOLE_WEBSOCKET_FLAG_COMPRESS + ); + } + } + } + }); + while ($attempts < 300) { try { if ($attempts > 0) { @@ -206,17 +238,17 @@ $server->on('open', function (Server $server, Request $request) use (&$connectio $console = $app->getResource('console'); /* - * Project Check - */ + * 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. - */ + * Abuse Check + * + * Abuse limits are connecting 128 times per minute and ip address. + */ $timeLimit = new TimeLimit('url:{url},ip:{ip}', 128, 60, function () use ($db) { return $db; }); @@ -232,10 +264,10 @@ $server->on('open', function (Server $server, Request $request) use (&$connectio } /* - * 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. - */ + * 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', []))); @@ -260,6 +292,7 @@ $server->on('open', function (Server $server, Request $request) use (&$connectio $server->push($connection, json_encode($channels)); $stats->incr($project->getId(), 'connections'); + $stats->incr($project->getId(), 'connectionsTotal'); } catch (\Throwable $th) { $response = [ 'code' => $th->getCode(), @@ -290,7 +323,8 @@ $server->on('message', function (Server $server, Frame $frame) { $server->close($frame->fd); }); -$server->on('close', function (Server $server, int $connection) use (&$connections, &$subscriptions) { +$server->on('close', function (Server $server, int $connection) use (&$connections, &$subscriptions, &$stats) { + $stats->decr($connections[$connection]['projectId'], 'connectionsTotal'); Realtime::unsubscribe($connection, $subscriptions, $connections); Console::info('Connection close: ' . $connection); }); From 2315cf01d3f427aab97f41a64bfb0f245782847d Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Wed, 7 Apr 2021 12:07:21 +0200 Subject: [PATCH 113/267] skip origin check for console --- app/realtime.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/realtime.php b/app/realtime.php index 9ec6c6c691..e480c385ed 100644 --- a/app/realtime.php +++ b/app/realtime.php @@ -271,7 +271,7 @@ $server->on('open', function (Server $server, Request $request) use (&$connectio $origin = $request->getOrigin(); $originValidator = new Origin(\array_merge($project->getAttribute('platforms', []), $console->getAttribute('platforms', []))); - if (!$originValidator->isValid($origin)) { + if (!$originValidator->isValid($origin) && $project->getId() !== 'console') { throw new Exception($originValidator->getDescription(), 1008); } From 2e3d4fb616e1003efb6300eede7f249baa9db613 Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Tue, 13 Apr 2021 09:10:07 +0200 Subject: [PATCH 114/267] Revert "Merge branch 'refactor-function-docker-exec' of https://github.com/appwrite/appwrite into feat-265-realtime" This reverts commit 137aa98e194fcd600a731af2416ea2c4cd5e7785, reversing changes made to c5ad66839d5808f8787cb2aebfc63bc8d072b0e7. --- app/workers/functions.php | 136 +++----------------------------------- 1 file changed, 8 insertions(+), 128 deletions(-) diff --git a/app/workers/functions.php b/app/workers/functions.php index 9a8bccc82a..cd37ee0a26 100644 --- a/app/workers/functions.php +++ b/app/workers/functions.php @@ -18,7 +18,7 @@ require_once __DIR__.'/../workers.php'; Console::title('Functions V1 Worker'); -// Runtime::setHookFlags(SWOOLE_HOOK_ALL); +Runtime::setHookFlags(SWOOLE_HOOK_ALL); Console::success(APP_NAME.' functions worker v1 has started'); @@ -352,14 +352,6 @@ class FunctionsV1 extends Worker 'APPWRITE_FUNCTION_PROJECT_ID' => $projectId, ]); - $tmpvars = $vars; - \array_walk($tmpvars, function (&$value, $key) { - $key = $this->filterEnvKey($key); - $value = \escapeshellarg((empty($value)) ? 'null' : $value); - // $value = "--env {$key}={$value}"; - $value = "{$key}={$value}"; - }); - \array_walk($vars, function (&$value, $key) { $key = $this->filterEnvKey($key); $value = \escapeshellarg((empty($value)) ? 'null' : $value); @@ -458,126 +450,16 @@ class FunctionsV1 extends Worker $stdout = ''; $stderr = ''; - $executionStart = \microtime(true); - $envs = array_values($tmpvars); - $envs[] = "executionStart={$executionStart}"; - - /* - * Create execution via Docker API - */ - $ch = \curl_init(); - - \curl_setopt($ch, CURLOPT_URL, "http://localhost/containers/{$container}/exec"); - \curl_setopt($ch, CURLOPT_UNIX_SOCKET_PATH, '/var/run/docker.sock'); - \curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); - \curl_setopt($ch, CURLOPT_POST, 1); - - $body = array( - "Env" => $envs, - "Cmd" => \explode(' ', $command), - "AttachStdout" => true, - "AttachStderr" => true - ); - $body = json_encode($body); - - $createExecStart = microtime(true); - \curl_setopt($ch, CURLOPT_POSTFIELDS, $body); - - $headers = [ - 'Content-Type: application/json', - 'Content-Length: ' . \strlen($body) - ]; - \curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); - - $result = \curl_exec($ch); - $resultDecoded = json_decode($result, true); - $execId = $resultDecoded['Id']; - - - if (\curl_errno($ch)) { - echo 'Error:' . \curl_error($ch); - } - - \curl_close($ch); - - $createExecTime = microtime(true)- $createExecStart; - var_dump($createExecTime); - - /* - * Start execution without detatching - will receive stdout/stderr as response - */ - - $startExecStart = microtime(true); - $ch = \curl_init(); - $URL = "http://localhost/exec/{$execId}/start"; - \curl_setopt($ch, CURLOPT_URL, $URL); - \curl_setopt($ch, CURLOPT_UNIX_SOCKET_PATH, '/var/run/docker.sock'); - \curl_setopt($ch, CURLOPT_POST, 1); - \curl_setopt($ch, CURLOPT_POSTFIELDS, '{}'); - \curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); - - $headers = [ - 'Content-Type: application/json', - 'Content-Length: 2', - ]; - \curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); - - $startData = \curl_exec($ch); - var_dump($startData); - - if (\curl_errno($ch)) { - echo 'Error:' . \curl_error($ch); - } - - \curl_close($ch); - $startExecTime = microtime(true)- $startExecStart; - var_dump($startExecTime); - - // TODO@kodumbeats: set up listener for /events for exec_start and exec_die - // We need this to get accurate - // $params = [ - // 'filter' => json_encode([ - // 'type' => 'container', - // 'container' => $container, - // 'event' => 'exec_die', - // ]), - // 'since' => \floor($executionStart) - // 'until' => $executionStart + +App::getEnv('_APP_FUNCTIONS_TIMEOUT', 900) - // ]; - // $URL = $URL . '?' . \http_build_query($params); - /* - * Get execution details - */ - $ch = \curl_init(); - - $URL = "http://localhost/exec/{$execId}/json"; - \curl_setopt($ch, CURLOPT_URL, $URL); - \curl_setopt($ch, CURLOPT_UNIX_SOCKET_PATH, '/var/run/docker.sock'); - \curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); - - $headers = array(); - $headers[] = 'Content-Type: application/json'; - \curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); - - $result = \curl_exec($ch); - $execData = json_decode($result, true); // Can get the exit code from this call. - $exitCode = $execData['ExitCode']; - - if (\curl_errno($ch)) { - echo 'Error:' . \curl_error($ch); - } - - \curl_close($ch); - - // $exitCode = Console::execute("docker exec ".\implode(" ", $vars)." {$container} {$command}" - // , '', $stdout, $stderr, $function->getAttribute('timeout', (int) App::getEnv('_APP_FUNCTIONS_TIMEOUT', 900))); + + $exitCode = Console::execute("docker exec ".\implode(" ", $vars)." {$container} {$command}" + , '', $stdout, $stderr, $function->getAttribute('timeout', (int) App::getEnv('_APP_FUNCTIONS_TIMEOUT', 900))); $executionEnd = \microtime(true); $executionTime = ($executionEnd - $executionStart); $functionStatus = ($exitCode === 0) ? 'completed' : 'failed'; - Console::info("Function executed in " . ($startExecTime) . " seconds with exit code {$exitCode}"); + Console::info("Function executed in " . ($executionEnd - $executionStart) . " seconds with exit code {$exitCode}"); Authorization::disable(); @@ -585,11 +467,9 @@ class FunctionsV1 extends Worker 'tagId' => $tag->getId(), 'status' => $functionStatus, 'exitCode' => $exitCode, - // 'stdout' => \mb_substr($stdout, -4000), // log last 4000 chars output - 'stdout' => ($exitCode === 0) ? \mb_substr($startData, -4000) : '', - // 'stderr' => \mb_substr($stderr, -4000), // log last 4000 chars output - 'stderr' => (!$exitCode === 0) ? \mb_substr($startData, -4000) : '', - 'time' => $startExecTime, + 'stdout' => \mb_substr($stdout, -4000), // log last 4000 chars output + 'stderr' => \mb_substr($stderr, -4000), // log last 4000 chars output + 'time' => $executionTime, ])); Authorization::reset(); From faed19dacb7bae737bff3317950b25773521b14f Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Thu, 20 May 2021 10:54:18 +0200 Subject: [PATCH 115/267] fix(tests): general fixes after php 8 --- composer.json | 2 +- composer.lock | 74 ++------------------ tests/unit/Realtime/RealtimeChannelsTest.php | 13 ++-- 3 files changed, 17 insertions(+), 72 deletions(-) diff --git a/composer.json b/composer.json index c7c2028401..74747cc18f 100644 --- a/composer.json +++ b/composer.json @@ -46,7 +46,7 @@ "utopia-php/cli": "0.11.*", "utopia-php/config": "0.2.*", "utopia-php/locale": "0.3.*", - "utopia-php/registry": "0.4.*", + "utopia-php/registry": "0.5.*", "utopia-php/preloader": "0.2.*", "utopia-php/domains": "1.1.*", "utopia-php/swoole": "0.2.*", diff --git a/composer.lock b/composer.lock index b90438cbad..835f688134 100644 --- a/composer.lock +++ b/composer.lock @@ -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": "8e60a590126f27cb20bdfbbc1b73abbb", + "content-hash": "d722be0ec2f73f6436fc99b64bd69f0a", "packages": [ { "name": "adhocore/jwt", @@ -1899,16 +1899,16 @@ }, { "name": "utopia-php/registry", - "version": "0.4.0", + "version": "0.5.0", "source": { "type": "git", "url": "https://github.com/utopia-php/registry.git", - "reference": "7aebbc6c5f3f04ff7a35ac3dad39fa91c9bd7c2d" + "reference": "bedc4ed54527b2803e6dfdccc39449f98522b70d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/registry/zipball/7aebbc6c5f3f04ff7a35ac3dad39fa91c9bd7c2d", - "reference": "7aebbc6c5f3f04ff7a35ac3dad39fa91c9bd7c2d", + "url": "https://api.github.com/repos/utopia-php/registry/zipball/bedc4ed54527b2803e6dfdccc39449f98522b70d", + "reference": "bedc4ed54527b2803e6dfdccc39449f98522b70d", "shasum": "" }, "require": { @@ -1918,7 +1918,6 @@ "phpunit/phpunit": "^9.3", "vimeo/psalm": "4.0.1" }, - "default-branch": true, "type": "library", "autoload": { "psr-4": { @@ -1946,9 +1945,9 @@ ], "support": { "issues": "https://github.com/utopia-php/registry/issues", - "source": "https://github.com/utopia-php/registry/tree/0.4.0" + "source": "https://github.com/utopia-php/registry/tree/0.5.0" }, - "time": "2021-03-10T06:50:09+00:00" + "time": "2021-03-10T10:45:22+00:00" }, { "name": "utopia-php/storage", @@ -5932,65 +5931,6 @@ }, "time": "2021-05-01T20:56:25+00:00" }, - { - "name": "webmozart/assert", - "version": "dev-master", - "source": { - "type": "git", - "url": "https://github.com/webmozarts/assert.git", - "reference": "4631e2c7d2d7132adac9fd84d4c1a98c10a6e049" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/webmozarts/assert/zipball/4631e2c7d2d7132adac9fd84d4c1a98c10a6e049", - "reference": "4631e2c7d2d7132adac9fd84d4c1a98c10a6e049", - "shasum": "" - }, - "require": { - "php": "^7.2 || ^8.0", - "symfony/polyfill-ctype": "^1.8" - }, - "conflict": { - "phpstan/phpstan": "<0.12.20", - "vimeo/psalm": "<3.9.1" - }, - "require-dev": { - "phpunit/phpunit": "^8.5.13" - }, - "default-branch": true, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.10-dev" - } - }, - "autoload": { - "psr-4": { - "Webmozart\\Assert\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Bernhard Schussek", - "email": "bschussek@gmail.com" - } - ], - "description": "Assertions to validate method input/output with nice error messages.", - "keywords": [ - "assert", - "check", - "validate" - ], - "support": { - "issues": "https://github.com/webmozarts/assert/issues", - "source": "https://github.com/webmozarts/assert/tree/master" - }, - "time": "2021-02-28T20:01:57+00:00" - }, { "name": "webmozart/path-util", "version": "2.3.0", diff --git a/tests/unit/Realtime/RealtimeChannelsTest.php b/tests/unit/Realtime/RealtimeChannelsTest.php index a84fdd4a16..5145acb2c4 100644 --- a/tests/unit/Realtime/RealtimeChannelsTest.php +++ b/tests/unit/Realtime/RealtimeChannelsTest.php @@ -57,14 +57,16 @@ class RealtimeChannelsTest extends TestCase ] ] ])); + $roles = Realtime::getRoles(); + $parsedChannels = Realtime::parseChannels([0 => $channel]); Realtime::subscribe( '1', $this->connectionsCount, - Realtime::getRoles(), + $roles, $this->subscriptions, $this->connections, - Realtime::parseChannels([0 => $channel]) + $parsedChannels ); $this->connectionsCount++; @@ -80,13 +82,16 @@ class RealtimeChannelsTest extends TestCase '$id' => '' ])); + $roles = Realtime::getRoles(); + $parsedChannels = Realtime::parseChannels([0 => $channel]); + Realtime::subscribe( '1', $this->connectionsCount, - Realtime::getRoles(), + $roles, $this->subscriptions, $this->connections, - Realtime::parseChannels([0 => $channel]) + $parsedChannels ); $this->connectionsCount++; From 288bd7fcc1d7c5f24decb57b14d6a6d278e9e5ca Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Wed, 26 May 2021 09:37:31 +0200 Subject: [PATCH 116/267] deps(composer): update composer lock --- composer.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.lock b/composer.lock index 868d9485b7..43be993f20 100644 --- a/composer.lock +++ b/composer.lock @@ -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": "e433ce62dd355a107816e8967d5c769d", + "content-hash": "c73fd7564a192ea65a1398edccb590f4", "packages": [ { "name": "adhocore/jwt", From 27fde80a09b318e0adff71dca0da4820e3877b70 Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Thu, 27 May 2021 11:08:10 +0200 Subject: [PATCH 117/267] fix(install): fix realtime service --- app/views/install/compose.phtml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/app/views/install/compose.phtml b/app/views/install/compose.phtml index c36b5ccd2e..c711b37b09 100644 --- a/app/views/install/compose.phtml +++ b/app/views/install/compose.phtml @@ -108,13 +108,10 @@ services: - _APP_FUNCTIONS_RUNTIMES appwrite-realtime: + image: /: entrypoint: realtime container_name: appwrite-realtime - build: - context: . restart: unless-stopped - ports: - - 9505:80 labels: - "traefik.enable=true" - "traefik.constraint-label-stack=appwrite" From ae8320e4fed3ed8f716a12f3c7f7b08f93aa9f53 Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Fri, 4 Jun 2021 12:53:18 +0200 Subject: [PATCH 118/267] fix(realtime): undefined projectId for shared stats --- app/controllers/api/health.php | 1 - app/realtime.php | 4 +++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/app/controllers/api/health.php b/app/controllers/api/health.php index 0885c0c40b..dd1d4a0d69 100644 --- a/app/controllers/api/health.php +++ b/app/controllers/api/health.php @@ -6,7 +6,6 @@ use Utopia\Storage\Device\Local; use Utopia\Storage\Storage; use Appwrite\ClamAV\Network; use Appwrite\Event\Event; -use RuntimeException; App::get('/v1/health') ->desc('Get HTTP') diff --git a/app/realtime.php b/app/realtime.php index e480c385ed..013b183d05 100644 --- a/app/realtime.php +++ b/app/realtime.php @@ -324,7 +324,9 @@ $server->on('message', function (Server $server, Frame $frame) { }); $server->on('close', function (Server $server, int $connection) use (&$connections, &$subscriptions, &$stats) { - $stats->decr($connections[$connection]['projectId'], 'connectionsTotal'); + if (array_key_exists($connection, $connections)) { + $stats->decr($connections[$connection]['projectId'], 'connectionsTotal'); + } Realtime::unsubscribe($connection, $subscriptions, $connections); Console::info('Connection close: ' . $connection); }); From f0d3b2f22a00be4900e378e3c701bb62a163e8d1 Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Fri, 4 Jun 2021 13:00:13 +0200 Subject: [PATCH 119/267] feat(realtime): print debug logs by default --- app/realtime.php | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/app/realtime.php b/app/realtime.php index 013b183d05..68bf4410cb 100644 --- a/app/realtime.php +++ b/app/realtime.php @@ -147,14 +147,17 @@ $server->on('workerStart', function ($server, $workerId) use (&$subscriptions, & * - Session * - Team? (not implemented yet) * - Membership? (not implemented yet) - * - Function? (not available yet) - * - Execution? (not available yet) + * - Function + * - Execution */ $event = json_decode($payload, true); $receivers = Realtime::identifyReceivers($event, $subscriptions); - if (App::isDevelopment() && !empty($receivers)) { + + // Temporarily print debug logs by default for Alpha testing. + // if (App::isDevelopment() && !empty($receivers)) { + if (!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); @@ -298,11 +301,12 @@ $server->on('open', function (Server $server, Request $request) use (&$connectio 'code' => $th->getCode(), 'message' => $th->getMessage() ]; - if (App::isDevelopment()) { + // Temporarily print debug logs by default for Alpha testing. + //if (App::isDevelopment()) { Console::error("[Error] Connection Error"); Console::error("[Error] Code: " . $response['code']); Console::error("[Error] Message: " . $response['message']); - } + //} $server->push($connection, json_encode($response)); $server->close($connection); } From f76695fb09d82e858a2fad9601db5269bc895de4 Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Tue, 8 Jun 2021 14:09:50 +0200 Subject: [PATCH 120/267] fix(realtime): project stats channel --- app/realtime.php | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/app/realtime.php b/app/realtime.php index 68bf4410cb..a125118654 100644 --- a/app/realtime.php +++ b/app/realtime.php @@ -102,19 +102,17 @@ $server->on('workerStart', function ($server, $workerId) use (&$subscriptions, & $payload[$projectId] = $value['connectionsTotal']; } foreach ($subscriptions['console']['role:member']['project'] as $connection => $value) { - foreach ($stats as $projectId => $value) { - $server->push( - $connection, - json_encode([ - 'event' => 'stats.connections', - 'channels' => ['project'], - 'timestamp' => time(), - 'payload' => $payload - ]), - SWOOLE_WEBSOCKET_OPCODE_TEXT, - SWOOLE_WEBSOCKET_FLAG_FIN | SWOOLE_WEBSOCKET_FLAG_COMPRESS - ); - } + $server->push( + $connection, + json_encode([ + 'event' => 'stats.connections', + 'channels' => ['project'], + 'timestamp' => time(), + 'payload' => $payload + ]), + SWOOLE_WEBSOCKET_OPCODE_TEXT, + SWOOLE_WEBSOCKET_FLAG_FIN | SWOOLE_WEBSOCKET_FLAG_COMPRESS + ); } } }); From 83db4d3978b38a53ed3cb6092589d06b8a3f8a28 Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Tue, 8 Jun 2021 17:27:12 +0200 Subject: [PATCH 121/267] feat(console): add realtime subscription --- .gitignore | 1 + app/views/console/home/index.phtml | 4 +- gulpfile.js | 1 + package-lock.json | 17 + public/dist/scripts/app-all.js | 935 +-- public/dist/scripts/app-dep.js | 925 +-- public/dist/scripts/app.js | 10 +- public/scripts/dependencies/appwrite.js | 9245 ++++++++++------------- public/scripts/filters.js | 3 + public/scripts/init.js | 9 +- public/scripts/services/console.js | 2 +- public/scripts/services/realtime.js | 14 + public/scripts/services/sdk.js | 2 +- public/scripts/views/service.js | 2 +- 14 files changed, 5150 insertions(+), 6020 deletions(-) create mode 100644 public/scripts/services/realtime.js diff --git a/.gitignore b/.gitignore index 224a970df4..5782415ae4 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ /vendor/ /node_modules/ /tests/resources/storage/ +/app/sdks/* /.idea/ .DS_Store .php_cs.cache diff --git a/app/views/console/home/index.phtml b/app/views/console/home/index.phtml index c065a2203d..6fffb8f570 100644 --- a/app/views/console/home/index.phtml +++ b/app/views/console/home/index.phtml @@ -104,9 +104,9 @@ $usageStatsEnabled = $this->getParam('usageStatsEnabled',true);
Bandwidth
- +
- Func. Executions + Realtime Connections
diff --git a/gulpfile.js b/gulpfile.js index 28204f0d08..f64d0285d2 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -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', diff --git a/package-lock.json b/package-lock.json index b156a38f59..a74beaedde 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1563,6 +1563,7 @@ "yallist" ], "dev": true, + "hasInstallScript": true, "optional": true, "os": [ "darwin" @@ -1806,6 +1807,16 @@ "license": "ISC", "optional": true }, + "node_modules/fsevents/node_modules/ini": { + "version": "1.3.5", + "dev": true, + "inBundle": true, + "license": "ISC", + "optional": true, + "engines": { + "node": "*" + } + }, "node_modules/fsevents/node_modules/is-fullwidth-code-point": { "version": "1.0.0", "dev": true, @@ -7028,6 +7039,12 @@ "dev": true, "optional": true }, + "ini": { + "version": "1.3.5", + "bundled": true, + "dev": true, + "optional": true + }, "is-fullwidth-code-point": { "version": "1.0.0", "bundled": true, diff --git a/public/dist/scripts/app-all.js b/public/dist/scripts/app-all.js index 604bf23816..43966130e4 100644 --- a/public/dist/scripts/app-all.js +++ b/public/dist/scripts/app-all.js @@ -1,463 +1,470 @@ -(function(window){'use strict';window.Appwrite=function(){let config={endpoint:'https://appwrite.io/v1',project:'',key:'',locale:'',mode:'',};let setEndpoint=function(endpoint){config.endpoint=endpoint;return this;};let setProject=function(value) -{http.addGlobalHeader('X-Appwrite-Project',value);config.project=value;return this;};let setKey=function(value) -{http.addGlobalHeader('X-Appwrite-Key',value);config.key=value;return this;};let setLocale=function(value) -{http.addGlobalHeader('X-Appwrite-Locale',value);config.locale=value;return this;};let setMode=function(value) -{http.addGlobalHeader('X-Appwrite-Mode',value);config.mode=value;return this;};let http=function(document){let globalParams=[],globalHeaders=[];let addParam=function(url,param,value){let a=document.createElement('a'),regex=/(?:\?|&|&)+([^=]+)(?:=([^&]*))*/g;let match,str=[];a.href=url;param=encodeURIComponent(param);while(match=regex.exec(a.search))if(param!==match[1])str.push(match[1]+(match[2]?"="+match[2]:""));str.push(param+(value?"="+encodeURIComponent(value):""));a.search=str.join("&");return a.href;};let buildQuery=function(params){let str=[];for(let p in params){if(Array.isArray(params[p])){for(let index=0;index=request.status){resolve(data);}else{reject(data);}};if(progress){request.addEventListener('progress',progress);request.upload.addEventListener('progress',progress,false);} -request.onerror=function(){reject(new Error("Network Error"));};request.send(params);})};return{'get':function(path,headers={},params={}){return call('GET',path+((Object.keys(params).length>0)?'?'+buildQuery(params):''),headers,{});},'post':function(path,headers={},params={},progress=null){return call('POST',path,headers,params,progress);},'put':function(path,headers={},params={},progress=null){return call('PUT',path,headers,params,progress);},'patch':function(path,headers={},params={},progress=null){return call('PATCH',path,headers,params,progress);},'delete':function(path,headers={},params={},progress=null){return call('DELETE',path,headers,params,progress);},'addGlobalParam':addGlobalParam,'addGlobalHeader':addGlobalHeader}}(window.document);let account={get:function(){let path='/account';let payload={};return http.get(path,{'content-type':'application/json',},payload);},create:function(email,password,name=''){if(email===undefined){throw new Error('Missing required parameter: "email"');} -if(password===undefined){throw new Error('Missing required parameter: "password"');} -let path='/account';let payload={};if(email){payload['email']=email;} -if(password){payload['password']=password;} -if(name){payload['name']=name;} -return http.post(path,{'content-type':'application/json',},payload);},delete:function(){let path='/account';let payload={};return http.delete(path,{'content-type':'application/json',},payload);},updateEmail:function(email,password){if(email===undefined){throw new Error('Missing required parameter: "email"');} -if(password===undefined){throw new Error('Missing required parameter: "password"');} -let path='/account/email';let payload={};if(email){payload['email']=email;} -if(password){payload['password']=password;} -return http.patch(path,{'content-type':'application/json',},payload);},getLogs:function(){let path='/account/logs';let payload={};return http.get(path,{'content-type':'application/json',},payload);},updateName:function(name){if(name===undefined){throw new Error('Missing required parameter: "name"');} -let path='/account/name';let payload={};if(name){payload['name']=name;} -return http.patch(path,{'content-type':'application/json',},payload);},updatePassword:function(password,oldPassword){if(password===undefined){throw new Error('Missing required parameter: "password"');} -if(oldPassword===undefined){throw new Error('Missing required parameter: "oldPassword"');} -let path='/account/password';let payload={};if(password){payload['password']=password;} -if(oldPassword){payload['oldPassword']=oldPassword;} -return http.patch(path,{'content-type':'application/json',},payload);},getPrefs:function(){let path='/account/prefs';let payload={};return http.get(path,{'content-type':'application/json',},payload);},updatePrefs:function(prefs){if(prefs===undefined){throw new Error('Missing required parameter: "prefs"');} -let path='/account/prefs';let payload={};if(prefs){payload['prefs']=prefs;} -return http.patch(path,{'content-type':'application/json',},payload);},createRecovery:function(email,url){if(email===undefined){throw new Error('Missing required parameter: "email"');} -if(url===undefined){throw new Error('Missing required parameter: "url"');} -let path='/account/recovery';let payload={};if(email){payload['email']=email;} -if(url){payload['url']=url;} -return http.post(path,{'content-type':'application/json',},payload);},updateRecovery:function(userId,secret,password,passwordAgain){if(userId===undefined){throw new Error('Missing required parameter: "userId"');} -if(secret===undefined){throw new Error('Missing required parameter: "secret"');} -if(password===undefined){throw new Error('Missing required parameter: "password"');} -if(passwordAgain===undefined){throw new Error('Missing required parameter: "passwordAgain"');} -let path='/account/recovery';let payload={};if(userId){payload['userId']=userId;} -if(secret){payload['secret']=secret;} -if(password){payload['password']=password;} -if(passwordAgain){payload['passwordAgain']=passwordAgain;} -return http.put(path,{'content-type':'application/json',},payload);},getSessions:function(){let path='/account/sessions';let payload={};return http.get(path,{'content-type':'application/json',},payload);},createSession:function(email,password){if(email===undefined){throw new Error('Missing required parameter: "email"');} -if(password===undefined){throw new Error('Missing required parameter: "password"');} -let path='/account/sessions';let payload={};if(email){payload['email']=email;} -if(password){payload['password']=password;} -return http.post(path,{'content-type':'application/json',},payload);},deleteSessions:function(){let path='/account/sessions';let payload={};return http.delete(path,{'content-type':'application/json',},payload);},createOAuth2Session:function(provider,success='https://appwrite.io/auth/oauth2/success',failure='https://appwrite.io/auth/oauth2/failure',scopes=[]){if(provider===undefined){throw new Error('Missing required parameter: "provider"');} -let path='/account/sessions/oauth2/{provider}'.replace(new RegExp('{provider}','g'),provider);let payload={};if(success){payload['success']=success;} -if(failure){payload['failure']=failure;} -if(scopes){payload['scopes']=scopes;} -payload['project']=config.project;payload['key']=config.key;let query=[];for(let p in payload){if(Array.isArray(payload[p])){for(let index=0;index{var _a;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());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;if(((_b=(_a=this.realtime)===null||_a===void 0?void 0:_a.lastMessage)===null||_b===void 0?void 0:_b.code)===1008){return;} +console.error('Realtime got disconnected. Reconnect will be attempted in 1 second.',event.reason);setTimeout(()=>{this.realtime.createSocket();},1000);});},onMessage:(channel,callback)=>(event)=>{try{const data=JSON.parse(event.data);this.realtime.lastMessage=data;if(data.channels&&data.channels.includes(channel)){callback(data);} +else if(data.code){throw 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;} +if(typeof name!=='undefined'){payload['name']=name;} +const uri=new URL(this.config.endpoint+path);return yield this.call('post',uri,{'content-type':'application/json',},payload);}),delete:()=>__awaiter(this,void 0,void 0,function*(){let path='/account';let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('delete',uri,{'content-type':'application/json',},payload);}),updateEmail:(email,password)=>__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/email';let payload={};if(typeof email!=='undefined'){payload['email']=email;} +if(typeof password!=='undefined'){payload['password']=password;} +const uri=new URL(this.config.endpoint+path);return yield this.call('patch',uri,{'content-type':'application/json',},payload);}),createJWT:()=>__awaiter(this,void 0,void 0,function*(){let path='/account/jwt';let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('post',uri,{'content-type':'application/json',},payload);}),getLogs:()=>__awaiter(this,void 0,void 0,function*(){let path='/account/logs';let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);}),updateName:(name)=>__awaiter(this,void 0,void 0,function*(){if(typeof name==='undefined'){throw new AppwriteException('Missing required parameter: "name"');} +let path='/account/name';let payload={};if(typeof name!=='undefined'){payload['name']=name;} +const uri=new URL(this.config.endpoint+path);return yield this.call('patch',uri,{'content-type':'application/json',},payload);}),updatePassword:(password,oldPassword)=>__awaiter(this,void 0,void 0,function*(){if(typeof password==='undefined'){throw new AppwriteException('Missing required parameter: "password"');} +let path='/account/password';let payload={};if(typeof password!=='undefined'){payload['password']=password;} +if(typeof oldPassword!=='undefined'){payload['oldPassword']=oldPassword;} +const uri=new URL(this.config.endpoint+path);return yield this.call('patch',uri,{'content-type':'application/json',},payload);}),getPrefs:()=>__awaiter(this,void 0,void 0,function*(){let path='/account/prefs';let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);}),updatePrefs:(prefs)=>__awaiter(this,void 0,void 0,function*(){if(typeof prefs==='undefined'){throw new AppwriteException('Missing required parameter: "prefs"');} +let path='/account/prefs';let payload={};if(typeof prefs!=='undefined'){payload['prefs']=prefs;} +const uri=new URL(this.config.endpoint+path);return yield this.call('patch',uri,{'content-type':'application/json',},payload);}),createRecovery:(email,url)=>__awaiter(this,void 0,void 0,function*(){if(typeof email==='undefined'){throw new AppwriteException('Missing required parameter: "email"');} +if(typeof url==='undefined'){throw new AppwriteException('Missing required parameter: "url"');} +let path='/account/recovery';let payload={};if(typeof email!=='undefined'){payload['email']=email;} +if(typeof url!=='undefined'){payload['url']=url;} +const uri=new URL(this.config.endpoint+path);return yield this.call('post',uri,{'content-type':'application/json',},payload);}),updateRecovery:(userId,secret,password,passwordAgain)=>__awaiter(this,void 0,void 0,function*(){if(typeof userId==='undefined'){throw new AppwriteException('Missing required parameter: "userId"');} +if(typeof secret==='undefined'){throw new AppwriteException('Missing required parameter: "secret"');} +if(typeof password==='undefined'){throw new AppwriteException('Missing required parameter: "password"');} +if(typeof passwordAgain==='undefined'){throw new AppwriteException('Missing required parameter: "passwordAgain"');} +let path='/account/recovery';let payload={};if(typeof userId!=='undefined'){payload['userId']=userId;} +if(typeof secret!=='undefined'){payload['secret']=secret;} +if(typeof password!=='undefined'){payload['password']=password;} +if(typeof passwordAgain!=='undefined'){payload['passwordAgain']=passwordAgain;} +const uri=new URL(this.config.endpoint+path);return yield this.call('put',uri,{'content-type':'application/json',},payload);}),getSessions:()=>__awaiter(this,void 0,void 0,function*(){let path='/account/sessions';let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);}),createSession:(email,password)=>__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/sessions';let payload={};if(typeof email!=='undefined'){payload['email']=email;} +if(typeof password!=='undefined'){payload['password']=password;} +const uri=new URL(this.config.endpoint+path);return yield this.call('post',uri,{'content-type':'application/json',},payload);}),deleteSessions:()=>__awaiter(this,void 0,void 0,function*(){let path='/account/sessions';let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('delete',uri,{'content-type':'application/json',},payload);}),createAnonymousSession:()=>__awaiter(this,void 0,void 0,function*(){let path='/account/sessions/anonymous';let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('post',uri,{'content-type':'application/json',},payload);}),createOAuth2Session:(provider,success,failure,scopes)=>{if(typeof provider==='undefined'){throw new AppwriteException('Missing required parameter: "provider"');} +let path='/account/sessions/oauth2/{provider}'.replace('{provider}',provider);let payload={};if(typeof success!=='undefined'){payload['success']=success;} +if(typeof failure!=='undefined'){payload['failure']=failure;} +if(typeof scopes!=='undefined'){payload['scopes']=scopes;} +const uri=new URL(this.config.endpoint+path);payload['project']=this.config.project;for(const[key,value]of Object.entries(this.flatten(payload))){uri.searchParams.append(key,value);} +if(typeof window!=='undefined'&&(window===null||window===void 0?void 0:window.location)){window.location.href=uri.toString();} +else{return uri;}},deleteSession:(sessionId)=>__awaiter(this,void 0,void 0,function*(){if(typeof sessionId==='undefined'){throw new AppwriteException('Missing required parameter: "sessionId"');} +let path='/account/sessions/{sessionId}'.replace('{sessionId}',sessionId);let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('delete',uri,{'content-type':'application/json',},payload);}),createVerification:(url)=>__awaiter(this,void 0,void 0,function*(){if(typeof url==='undefined'){throw new AppwriteException('Missing required parameter: "url"');} +let path='/account/verification';let payload={};if(typeof url!=='undefined'){payload['url']=url;} +const uri=new URL(this.config.endpoint+path);return yield this.call('post',uri,{'content-type':'application/json',},payload);}),updateVerification:(userId,secret)=>__awaiter(this,void 0,void 0,function*(){if(typeof userId==='undefined'){throw new AppwriteException('Missing required parameter: "userId"');} +if(typeof secret==='undefined'){throw new AppwriteException('Missing required parameter: "secret"');} +let path='/account/verification';let payload={};if(typeof userId!=='undefined'){payload['userId']=userId;} +if(typeof secret!=='undefined'){payload['secret']=secret;} +const uri=new URL(this.config.endpoint+path);return yield this.call('put',uri,{'content-type':'application/json',},payload);})};this.avatars={getBrowser:(code,width,height,quality)=>{if(typeof code==='undefined'){throw new AppwriteException('Missing required parameter: "code"');} +let path='/avatars/browsers/{code}'.replace('{code}',code);let payload={};if(typeof width!=='undefined'){payload['width']=width;} +if(typeof height!=='undefined'){payload['height']=height;} +if(typeof quality!=='undefined'){payload['quality']=quality;} +const uri=new URL(this.config.endpoint+path);payload['project']=this.config.project;for(const[key,value]of Object.entries(this.flatten(payload))){uri.searchParams.append(key,value);} +return uri;},getCreditCard:(code,width,height,quality)=>{if(typeof code==='undefined'){throw new AppwriteException('Missing required parameter: "code"');} +let path='/avatars/credit-cards/{code}'.replace('{code}',code);let payload={};if(typeof width!=='undefined'){payload['width']=width;} +if(typeof height!=='undefined'){payload['height']=height;} +if(typeof quality!=='undefined'){payload['quality']=quality;} +const uri=new URL(this.config.endpoint+path);payload['project']=this.config.project;for(const[key,value]of Object.entries(this.flatten(payload))){uri.searchParams.append(key,value);} +return uri;},getFavicon:(url)=>{if(typeof url==='undefined'){throw new AppwriteException('Missing required parameter: "url"');} +let path='/avatars/favicon';let payload={};if(typeof url!=='undefined'){payload['url']=url;} +const uri=new URL(this.config.endpoint+path);payload['project']=this.config.project;for(const[key,value]of Object.entries(this.flatten(payload))){uri.searchParams.append(key,value);} +return uri;},getFlag:(code,width,height,quality)=>{if(typeof code==='undefined'){throw new AppwriteException('Missing required parameter: "code"');} +let path='/avatars/flags/{code}'.replace('{code}',code);let payload={};if(typeof width!=='undefined'){payload['width']=width;} +if(typeof height!=='undefined'){payload['height']=height;} +if(typeof quality!=='undefined'){payload['quality']=quality;} +const uri=new URL(this.config.endpoint+path);payload['project']=this.config.project;for(const[key,value]of Object.entries(this.flatten(payload))){uri.searchParams.append(key,value);} +return uri;},getImage:(url,width,height)=>{if(typeof url==='undefined'){throw new AppwriteException('Missing required parameter: "url"');} +let path='/avatars/image';let payload={};if(typeof url!=='undefined'){payload['url']=url;} +if(typeof width!=='undefined'){payload['width']=width;} +if(typeof height!=='undefined'){payload['height']=height;} +const uri=new URL(this.config.endpoint+path);payload['project']=this.config.project;for(const[key,value]of Object.entries(this.flatten(payload))){uri.searchParams.append(key,value);} +return uri;},getInitials:(name,width,height,color,background)=>{let path='/avatars/initials';let payload={};if(typeof name!=='undefined'){payload['name']=name;} +if(typeof width!=='undefined'){payload['width']=width;} +if(typeof height!=='undefined'){payload['height']=height;} +if(typeof color!=='undefined'){payload['color']=color;} +if(typeof background!=='undefined'){payload['background']=background;} +const uri=new URL(this.config.endpoint+path);payload['project']=this.config.project;for(const[key,value]of Object.entries(this.flatten(payload))){uri.searchParams.append(key,value);} +return uri;},getQR:(text,size,margin,download)=>{if(typeof text==='undefined'){throw new AppwriteException('Missing required parameter: "text"');} +let path='/avatars/qr';let payload={};if(typeof text!=='undefined'){payload['text']=text;} +if(typeof size!=='undefined'){payload['size']=size;} +if(typeof margin!=='undefined'){payload['margin']=margin;} +if(typeof download!=='undefined'){payload['download']=download;} +const uri=new URL(this.config.endpoint+path);payload['project']=this.config.project;for(const[key,value]of Object.entries(this.flatten(payload))){uri.searchParams.append(key,value);} +return uri;}};this.database={listCollections:(search,limit,offset,orderType)=>__awaiter(this,void 0,void 0,function*(){let path='/database/collections';let payload={};if(typeof search!=='undefined'){payload['search']=search;} +if(typeof limit!=='undefined'){payload['limit']=limit;} +if(typeof offset!=='undefined'){payload['offset']=offset;} +if(typeof orderType!=='undefined'){payload['orderType']=orderType;} +const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);}),createCollection:(name,read,write,rules)=>__awaiter(this,void 0,void 0,function*(){if(typeof name==='undefined'){throw new AppwriteException('Missing required parameter: "name"');} +if(typeof read==='undefined'){throw new AppwriteException('Missing required parameter: "read"');} +if(typeof write==='undefined'){throw new AppwriteException('Missing required parameter: "write"');} +if(typeof rules==='undefined'){throw new AppwriteException('Missing required parameter: "rules"');} +let path='/database/collections';let payload={};if(typeof name!=='undefined'){payload['name']=name;} +if(typeof read!=='undefined'){payload['read']=read;} +if(typeof write!=='undefined'){payload['write']=write;} +if(typeof rules!=='undefined'){payload['rules']=rules;} +const uri=new URL(this.config.endpoint+path);return yield this.call('post',uri,{'content-type':'application/json',},payload);}),getCollection:(collectionId)=>__awaiter(this,void 0,void 0,function*(){if(typeof collectionId==='undefined'){throw new AppwriteException('Missing required parameter: "collectionId"');} +let path='/database/collections/{collectionId}'.replace('{collectionId}',collectionId);let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);}),updateCollection:(collectionId,name,read,write,rules)=>__awaiter(this,void 0,void 0,function*(){if(typeof collectionId==='undefined'){throw new AppwriteException('Missing required parameter: "collectionId"');} +if(typeof name==='undefined'){throw new AppwriteException('Missing required parameter: "name"');} +let path='/database/collections/{collectionId}'.replace('{collectionId}',collectionId);let payload={};if(typeof name!=='undefined'){payload['name']=name;} +if(typeof read!=='undefined'){payload['read']=read;} +if(typeof write!=='undefined'){payload['write']=write;} +if(typeof rules!=='undefined'){payload['rules']=rules;} +const uri=new URL(this.config.endpoint+path);return yield this.call('put',uri,{'content-type':'application/json',},payload);}),deleteCollection:(collectionId)=>__awaiter(this,void 0,void 0,function*(){if(typeof collectionId==='undefined'){throw new AppwriteException('Missing required parameter: "collectionId"');} +let path='/database/collections/{collectionId}'.replace('{collectionId}',collectionId);let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('delete',uri,{'content-type':'application/json',},payload);}),listDocuments:(collectionId,filters,limit,offset,orderField,orderType,orderCast,search)=>__awaiter(this,void 0,void 0,function*(){if(typeof collectionId==='undefined'){throw new AppwriteException('Missing required parameter: "collectionId"');} +let path='/database/collections/{collectionId}/documents'.replace('{collectionId}',collectionId);let payload={};if(typeof filters!=='undefined'){payload['filters']=filters;} +if(typeof limit!=='undefined'){payload['limit']=limit;} +if(typeof offset!=='undefined'){payload['offset']=offset;} +if(typeof orderField!=='undefined'){payload['orderField']=orderField;} +if(typeof orderType!=='undefined'){payload['orderType']=orderType;} +if(typeof orderCast!=='undefined'){payload['orderCast']=orderCast;} +if(typeof search!=='undefined'){payload['search']=search;} +const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);}),createDocument:(collectionId,data,read,write,parentDocument,parentProperty,parentPropertyType)=>__awaiter(this,void 0,void 0,function*(){if(typeof collectionId==='undefined'){throw new AppwriteException('Missing required parameter: "collectionId"');} +if(typeof data==='undefined'){throw new AppwriteException('Missing required parameter: "data"');} +let path='/database/collections/{collectionId}/documents'.replace('{collectionId}',collectionId);let payload={};if(typeof data!=='undefined'){payload['data']=data;} +if(typeof read!=='undefined'){payload['read']=read;} +if(typeof write!=='undefined'){payload['write']=write;} +if(typeof parentDocument!=='undefined'){payload['parentDocument']=parentDocument;} +if(typeof parentProperty!=='undefined'){payload['parentProperty']=parentProperty;} +if(typeof parentPropertyType!=='undefined'){payload['parentPropertyType']=parentPropertyType;} +const uri=new URL(this.config.endpoint+path);return yield this.call('post',uri,{'content-type':'application/json',},payload);}),getDocument:(collectionId,documentId)=>__awaiter(this,void 0,void 0,function*(){if(typeof collectionId==='undefined'){throw new AppwriteException('Missing required parameter: "collectionId"');} +if(typeof documentId==='undefined'){throw new AppwriteException('Missing required parameter: "documentId"');} +let path='/database/collections/{collectionId}/documents/{documentId}'.replace('{collectionId}',collectionId).replace('{documentId}',documentId);let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);}),updateDocument:(collectionId,documentId,data,read,write)=>__awaiter(this,void 0,void 0,function*(){if(typeof collectionId==='undefined'){throw new AppwriteException('Missing required parameter: "collectionId"');} +if(typeof documentId==='undefined'){throw new AppwriteException('Missing required parameter: "documentId"');} +if(typeof data==='undefined'){throw new AppwriteException('Missing required parameter: "data"');} +let path='/database/collections/{collectionId}/documents/{documentId}'.replace('{collectionId}',collectionId).replace('{documentId}',documentId);let payload={};if(typeof data!=='undefined'){payload['data']=data;} +if(typeof read!=='undefined'){payload['read']=read;} +if(typeof write!=='undefined'){payload['write']=write;} +const uri=new URL(this.config.endpoint+path);return yield this.call('patch',uri,{'content-type':'application/json',},payload);}),deleteDocument:(collectionId,documentId)=>__awaiter(this,void 0,void 0,function*(){if(typeof collectionId==='undefined'){throw new AppwriteException('Missing required parameter: "collectionId"');} +if(typeof documentId==='undefined'){throw new AppwriteException('Missing required parameter: "documentId"');} +let path='/database/collections/{collectionId}/documents/{documentId}'.replace('{collectionId}',collectionId).replace('{documentId}',documentId);let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('delete',uri,{'content-type':'application/json',},payload);})};this.functions={list:(search,limit,offset,orderType)=>__awaiter(this,void 0,void 0,function*(){let path='/functions';let payload={};if(typeof search!=='undefined'){payload['search']=search;} +if(typeof limit!=='undefined'){payload['limit']=limit;} +if(typeof offset!=='undefined'){payload['offset']=offset;} +if(typeof orderType!=='undefined'){payload['orderType']=orderType;} +const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);}),create:(name,execute,env,vars,events,schedule,timeout)=>__awaiter(this,void 0,void 0,function*(){if(typeof name==='undefined'){throw new AppwriteException('Missing required parameter: "name"');} +if(typeof execute==='undefined'){throw new AppwriteException('Missing required parameter: "execute"');} +if(typeof env==='undefined'){throw new AppwriteException('Missing required parameter: "env"');} +let path='/functions';let payload={};if(typeof name!=='undefined'){payload['name']=name;} +if(typeof execute!=='undefined'){payload['execute']=execute;} +if(typeof env!=='undefined'){payload['env']=env;} +if(typeof vars!=='undefined'){payload['vars']=vars;} +if(typeof events!=='undefined'){payload['events']=events;} +if(typeof schedule!=='undefined'){payload['schedule']=schedule;} +if(typeof timeout!=='undefined'){payload['timeout']=timeout;} +const uri=new URL(this.config.endpoint+path);return yield this.call('post',uri,{'content-type':'application/json',},payload);}),get:(functionId)=>__awaiter(this,void 0,void 0,function*(){if(typeof functionId==='undefined'){throw new AppwriteException('Missing required parameter: "functionId"');} +let path='/functions/{functionId}'.replace('{functionId}',functionId);let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);}),update:(functionId,name,execute,vars,events,schedule,timeout)=>__awaiter(this,void 0,void 0,function*(){if(typeof functionId==='undefined'){throw new AppwriteException('Missing required parameter: "functionId"');} +if(typeof name==='undefined'){throw new AppwriteException('Missing required parameter: "name"');} +if(typeof execute==='undefined'){throw new AppwriteException('Missing required parameter: "execute"');} +let path='/functions/{functionId}'.replace('{functionId}',functionId);let payload={};if(typeof name!=='undefined'){payload['name']=name;} +if(typeof execute!=='undefined'){payload['execute']=execute;} +if(typeof vars!=='undefined'){payload['vars']=vars;} +if(typeof events!=='undefined'){payload['events']=events;} +if(typeof schedule!=='undefined'){payload['schedule']=schedule;} +if(typeof timeout!=='undefined'){payload['timeout']=timeout;} +const uri=new URL(this.config.endpoint+path);return yield this.call('put',uri,{'content-type':'application/json',},payload);}),delete:(functionId)=>__awaiter(this,void 0,void 0,function*(){if(typeof functionId==='undefined'){throw new AppwriteException('Missing required parameter: "functionId"');} +let path='/functions/{functionId}'.replace('{functionId}',functionId);let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('delete',uri,{'content-type':'application/json',},payload);}),listExecutions:(functionId,search,limit,offset,orderType)=>__awaiter(this,void 0,void 0,function*(){if(typeof functionId==='undefined'){throw new AppwriteException('Missing required parameter: "functionId"');} +let path='/functions/{functionId}/executions'.replace('{functionId}',functionId);let payload={};if(typeof search!=='undefined'){payload['search']=search;} +if(typeof limit!=='undefined'){payload['limit']=limit;} +if(typeof offset!=='undefined'){payload['offset']=offset;} +if(typeof orderType!=='undefined'){payload['orderType']=orderType;} +const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);}),createExecution:(functionId,data)=>__awaiter(this,void 0,void 0,function*(){if(typeof functionId==='undefined'){throw new AppwriteException('Missing required parameter: "functionId"');} +let path='/functions/{functionId}/executions'.replace('{functionId}',functionId);let payload={};if(typeof data!=='undefined'){payload['data']=data;} +const uri=new URL(this.config.endpoint+path);return yield this.call('post',uri,{'content-type':'application/json',},payload);}),getExecution:(functionId,executionId)=>__awaiter(this,void 0,void 0,function*(){if(typeof functionId==='undefined'){throw new AppwriteException('Missing required parameter: "functionId"');} +if(typeof executionId==='undefined'){throw new AppwriteException('Missing required parameter: "executionId"');} +let path='/functions/{functionId}/executions/{executionId}'.replace('{functionId}',functionId).replace('{executionId}',executionId);let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);}),updateTag:(functionId,tag)=>__awaiter(this,void 0,void 0,function*(){if(typeof functionId==='undefined'){throw new AppwriteException('Missing required parameter: "functionId"');} +if(typeof tag==='undefined'){throw new AppwriteException('Missing required parameter: "tag"');} +let path='/functions/{functionId}/tag'.replace('{functionId}',functionId);let payload={};if(typeof tag!=='undefined'){payload['tag']=tag;} +const uri=new URL(this.config.endpoint+path);return yield this.call('patch',uri,{'content-type':'application/json',},payload);}),listTags:(functionId,search,limit,offset,orderType)=>__awaiter(this,void 0,void 0,function*(){if(typeof functionId==='undefined'){throw new AppwriteException('Missing required parameter: "functionId"');} +let path='/functions/{functionId}/tags'.replace('{functionId}',functionId);let payload={};if(typeof search!=='undefined'){payload['search']=search;} +if(typeof limit!=='undefined'){payload['limit']=limit;} +if(typeof offset!=='undefined'){payload['offset']=offset;} +if(typeof orderType!=='undefined'){payload['orderType']=orderType;} +const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);}),createTag:(functionId,command,code)=>__awaiter(this,void 0,void 0,function*(){if(typeof functionId==='undefined'){throw new AppwriteException('Missing required parameter: "functionId"');} +if(typeof command==='undefined'){throw new AppwriteException('Missing required parameter: "command"');} +if(typeof code==='undefined'){throw new AppwriteException('Missing required parameter: "code"');} +let path='/functions/{functionId}/tags'.replace('{functionId}',functionId);let payload={};if(typeof command!=='undefined'){payload['command']=command;} +if(typeof code!=='undefined'){payload['code']=code;} +const uri=new URL(this.config.endpoint+path);return yield this.call('post',uri,{'content-type':'multipart/form-data',},payload);}),getTag:(functionId,tagId)=>__awaiter(this,void 0,void 0,function*(){if(typeof functionId==='undefined'){throw new AppwriteException('Missing required parameter: "functionId"');} +if(typeof tagId==='undefined'){throw new AppwriteException('Missing required parameter: "tagId"');} +let path='/functions/{functionId}/tags/{tagId}'.replace('{functionId}',functionId).replace('{tagId}',tagId);let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);}),deleteTag:(functionId,tagId)=>__awaiter(this,void 0,void 0,function*(){if(typeof functionId==='undefined'){throw new AppwriteException('Missing required parameter: "functionId"');} +if(typeof tagId==='undefined'){throw new AppwriteException('Missing required parameter: "tagId"');} +let path='/functions/{functionId}/tags/{tagId}'.replace('{functionId}',functionId).replace('{tagId}',tagId);let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('delete',uri,{'content-type':'application/json',},payload);}),getUsage:(functionId,range)=>__awaiter(this,void 0,void 0,function*(){if(typeof functionId==='undefined'){throw new AppwriteException('Missing required parameter: "functionId"');} +let path='/functions/{functionId}/usage'.replace('{functionId}',functionId);let payload={};if(typeof range!=='undefined'){payload['range']=range;} +const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);})};this.health={get:()=>__awaiter(this,void 0,void 0,function*(){let path='/health';let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);}),getAntiVirus:()=>__awaiter(this,void 0,void 0,function*(){let path='/health/anti-virus';let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);}),getCache:()=>__awaiter(this,void 0,void 0,function*(){let path='/health/cache';let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);}),getDB:()=>__awaiter(this,void 0,void 0,function*(){let path='/health/db';let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);}),getQueueCertificates:()=>__awaiter(this,void 0,void 0,function*(){let path='/health/queue/certificates';let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);}),getQueueFunctions:()=>__awaiter(this,void 0,void 0,function*(){let path='/health/queue/functions';let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);}),getQueueLogs:()=>__awaiter(this,void 0,void 0,function*(){let path='/health/queue/logs';let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);}),getQueueTasks:()=>__awaiter(this,void 0,void 0,function*(){let path='/health/queue/tasks';let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);}),getQueueUsage:()=>__awaiter(this,void 0,void 0,function*(){let path='/health/queue/usage';let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);}),getQueueWebhooks:()=>__awaiter(this,void 0,void 0,function*(){let path='/health/queue/webhooks';let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);}),getStorageLocal:()=>__awaiter(this,void 0,void 0,function*(){let path='/health/storage/local';let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);}),getTime:()=>__awaiter(this,void 0,void 0,function*(){let path='/health/time';let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);})};this.locale={get:()=>__awaiter(this,void 0,void 0,function*(){let path='/locale';let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);}),getContinents:()=>__awaiter(this,void 0,void 0,function*(){let path='/locale/continents';let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);}),getCountries:()=>__awaiter(this,void 0,void 0,function*(){let path='/locale/countries';let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);}),getCountriesEU:()=>__awaiter(this,void 0,void 0,function*(){let path='/locale/countries/eu';let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);}),getCountriesPhones:()=>__awaiter(this,void 0,void 0,function*(){let path='/locale/countries/phones';let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);}),getCurrencies:()=>__awaiter(this,void 0,void 0,function*(){let path='/locale/currencies';let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);}),getLanguages:()=>__awaiter(this,void 0,void 0,function*(){let path='/locale/languages';let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);})};this.projects={list:(search,limit,offset,orderType)=>__awaiter(this,void 0,void 0,function*(){let path='/projects';let payload={};if(typeof search!=='undefined'){payload['search']=search;} +if(typeof limit!=='undefined'){payload['limit']=limit;} +if(typeof offset!=='undefined'){payload['offset']=offset;} +if(typeof orderType!=='undefined'){payload['orderType']=orderType;} +const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);}),create:(name,teamId,description,logo,url,legalName,legalCountry,legalState,legalCity,legalAddress,legalTaxId)=>__awaiter(this,void 0,void 0,function*(){if(typeof name==='undefined'){throw new AppwriteException('Missing required parameter: "name"');} +if(typeof teamId==='undefined'){throw new AppwriteException('Missing required parameter: "teamId"');} +let path='/projects';let payload={};if(typeof name!=='undefined'){payload['name']=name;} +if(typeof teamId!=='undefined'){payload['teamId']=teamId;} +if(typeof description!=='undefined'){payload['description']=description;} +if(typeof logo!=='undefined'){payload['logo']=logo;} +if(typeof url!=='undefined'){payload['url']=url;} +if(typeof legalName!=='undefined'){payload['legalName']=legalName;} +if(typeof legalCountry!=='undefined'){payload['legalCountry']=legalCountry;} +if(typeof legalState!=='undefined'){payload['legalState']=legalState;} +if(typeof legalCity!=='undefined'){payload['legalCity']=legalCity;} +if(typeof legalAddress!=='undefined'){payload['legalAddress']=legalAddress;} +if(typeof legalTaxId!=='undefined'){payload['legalTaxId']=legalTaxId;} +const uri=new URL(this.config.endpoint+path);return yield this.call('post',uri,{'content-type':'application/json',},payload);}),get:(projectId)=>__awaiter(this,void 0,void 0,function*(){if(typeof projectId==='undefined'){throw new AppwriteException('Missing required parameter: "projectId"');} +let path='/projects/{projectId}'.replace('{projectId}',projectId);let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);}),update:(projectId,name,description,logo,url,legalName,legalCountry,legalState,legalCity,legalAddress,legalTaxId)=>__awaiter(this,void 0,void 0,function*(){if(typeof projectId==='undefined'){throw new AppwriteException('Missing required parameter: "projectId"');} +if(typeof name==='undefined'){throw new AppwriteException('Missing required parameter: "name"');} +let path='/projects/{projectId}'.replace('{projectId}',projectId);let payload={};if(typeof name!=='undefined'){payload['name']=name;} +if(typeof description!=='undefined'){payload['description']=description;} +if(typeof logo!=='undefined'){payload['logo']=logo;} +if(typeof url!=='undefined'){payload['url']=url;} +if(typeof legalName!=='undefined'){payload['legalName']=legalName;} +if(typeof legalCountry!=='undefined'){payload['legalCountry']=legalCountry;} +if(typeof legalState!=='undefined'){payload['legalState']=legalState;} +if(typeof legalCity!=='undefined'){payload['legalCity']=legalCity;} +if(typeof legalAddress!=='undefined'){payload['legalAddress']=legalAddress;} +if(typeof legalTaxId!=='undefined'){payload['legalTaxId']=legalTaxId;} +const uri=new URL(this.config.endpoint+path);return yield this.call('patch',uri,{'content-type':'application/json',},payload);}),delete:(projectId,password)=>__awaiter(this,void 0,void 0,function*(){if(typeof projectId==='undefined'){throw new AppwriteException('Missing required parameter: "projectId"');} +if(typeof password==='undefined'){throw new AppwriteException('Missing required parameter: "password"');} +let path='/projects/{projectId}'.replace('{projectId}',projectId);let payload={};if(typeof password!=='undefined'){payload['password']=password;} +const uri=new URL(this.config.endpoint+path);return yield this.call('delete',uri,{'content-type':'application/json',},payload);}),updateAuthLimit:(projectId,limit)=>__awaiter(this,void 0,void 0,function*(){if(typeof projectId==='undefined'){throw new AppwriteException('Missing required parameter: "projectId"');} +if(typeof limit==='undefined'){throw new AppwriteException('Missing required parameter: "limit"');} +let path='/projects/{projectId}/auth/limit'.replace('{projectId}',projectId);let payload={};if(typeof limit!=='undefined'){payload['limit']=limit;} +const uri=new URL(this.config.endpoint+path);return yield this.call('patch',uri,{'content-type':'application/json',},payload);}),updateAuthStatus:(projectId,method,status)=>__awaiter(this,void 0,void 0,function*(){if(typeof projectId==='undefined'){throw new AppwriteException('Missing required parameter: "projectId"');} +if(typeof method==='undefined'){throw new AppwriteException('Missing required parameter: "method"');} +if(typeof status==='undefined'){throw new AppwriteException('Missing required parameter: "status"');} +let path='/projects/{projectId}/auth/{method}'.replace('{projectId}',projectId).replace('{method}',method);let payload={};if(typeof status!=='undefined'){payload['status']=status;} +const uri=new URL(this.config.endpoint+path);return yield this.call('patch',uri,{'content-type':'application/json',},payload);}),listDomains:(projectId)=>__awaiter(this,void 0,void 0,function*(){if(typeof projectId==='undefined'){throw new AppwriteException('Missing required parameter: "projectId"');} +let path='/projects/{projectId}/domains'.replace('{projectId}',projectId);let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);}),createDomain:(projectId,domain)=>__awaiter(this,void 0,void 0,function*(){if(typeof projectId==='undefined'){throw new AppwriteException('Missing required parameter: "projectId"');} +if(typeof domain==='undefined'){throw new AppwriteException('Missing required parameter: "domain"');} +let path='/projects/{projectId}/domains'.replace('{projectId}',projectId);let payload={};if(typeof domain!=='undefined'){payload['domain']=domain;} +const uri=new URL(this.config.endpoint+path);return yield this.call('post',uri,{'content-type':'application/json',},payload);}),getDomain:(projectId,domainId)=>__awaiter(this,void 0,void 0,function*(){if(typeof projectId==='undefined'){throw new AppwriteException('Missing required parameter: "projectId"');} +if(typeof domainId==='undefined'){throw new AppwriteException('Missing required parameter: "domainId"');} +let path='/projects/{projectId}/domains/{domainId}'.replace('{projectId}',projectId).replace('{domainId}',domainId);let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);}),deleteDomain:(projectId,domainId)=>__awaiter(this,void 0,void 0,function*(){if(typeof projectId==='undefined'){throw new AppwriteException('Missing required parameter: "projectId"');} +if(typeof domainId==='undefined'){throw new AppwriteException('Missing required parameter: "domainId"');} +let path='/projects/{projectId}/domains/{domainId}'.replace('{projectId}',projectId).replace('{domainId}',domainId);let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('delete',uri,{'content-type':'application/json',},payload);}),updateDomainVerification:(projectId,domainId)=>__awaiter(this,void 0,void 0,function*(){if(typeof projectId==='undefined'){throw new AppwriteException('Missing required parameter: "projectId"');} +if(typeof domainId==='undefined'){throw new AppwriteException('Missing required parameter: "domainId"');} +let path='/projects/{projectId}/domains/{domainId}/verification'.replace('{projectId}',projectId).replace('{domainId}',domainId);let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('patch',uri,{'content-type':'application/json',},payload);}),listKeys:(projectId)=>__awaiter(this,void 0,void 0,function*(){if(typeof projectId==='undefined'){throw new AppwriteException('Missing required parameter: "projectId"');} +let path='/projects/{projectId}/keys'.replace('{projectId}',projectId);let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);}),createKey:(projectId,name,scopes)=>__awaiter(this,void 0,void 0,function*(){if(typeof projectId==='undefined'){throw new AppwriteException('Missing required parameter: "projectId"');} +if(typeof name==='undefined'){throw new AppwriteException('Missing required parameter: "name"');} +if(typeof scopes==='undefined'){throw new AppwriteException('Missing required parameter: "scopes"');} +let path='/projects/{projectId}/keys'.replace('{projectId}',projectId);let payload={};if(typeof name!=='undefined'){payload['name']=name;} +if(typeof scopes!=='undefined'){payload['scopes']=scopes;} +const uri=new URL(this.config.endpoint+path);return yield this.call('post',uri,{'content-type':'application/json',},payload);}),getKey:(projectId,keyId)=>__awaiter(this,void 0,void 0,function*(){if(typeof projectId==='undefined'){throw new AppwriteException('Missing required parameter: "projectId"');} +if(typeof keyId==='undefined'){throw new AppwriteException('Missing required parameter: "keyId"');} +let path='/projects/{projectId}/keys/{keyId}'.replace('{projectId}',projectId).replace('{keyId}',keyId);let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);}),updateKey:(projectId,keyId,name,scopes)=>__awaiter(this,void 0,void 0,function*(){if(typeof projectId==='undefined'){throw new AppwriteException('Missing required parameter: "projectId"');} +if(typeof keyId==='undefined'){throw new AppwriteException('Missing required parameter: "keyId"');} +if(typeof name==='undefined'){throw new AppwriteException('Missing required parameter: "name"');} +if(typeof scopes==='undefined'){throw new AppwriteException('Missing required parameter: "scopes"');} +let path='/projects/{projectId}/keys/{keyId}'.replace('{projectId}',projectId).replace('{keyId}',keyId);let payload={};if(typeof name!=='undefined'){payload['name']=name;} +if(typeof scopes!=='undefined'){payload['scopes']=scopes;} +const uri=new URL(this.config.endpoint+path);return yield this.call('put',uri,{'content-type':'application/json',},payload);}),deleteKey:(projectId,keyId)=>__awaiter(this,void 0,void 0,function*(){if(typeof projectId==='undefined'){throw new AppwriteException('Missing required parameter: "projectId"');} +if(typeof keyId==='undefined'){throw new AppwriteException('Missing required parameter: "keyId"');} +let path='/projects/{projectId}/keys/{keyId}'.replace('{projectId}',projectId).replace('{keyId}',keyId);let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('delete',uri,{'content-type':'application/json',},payload);}),updateOAuth2:(projectId,provider,appId,secret)=>__awaiter(this,void 0,void 0,function*(){if(typeof projectId==='undefined'){throw new AppwriteException('Missing required parameter: "projectId"');} +if(typeof provider==='undefined'){throw new AppwriteException('Missing required parameter: "provider"');} +let path='/projects/{projectId}/oauth2'.replace('{projectId}',projectId);let payload={};if(typeof provider!=='undefined'){payload['provider']=provider;} +if(typeof appId!=='undefined'){payload['appId']=appId;} +if(typeof secret!=='undefined'){payload['secret']=secret;} +const uri=new URL(this.config.endpoint+path);return yield this.call('patch',uri,{'content-type':'application/json',},payload);}),listPlatforms:(projectId)=>__awaiter(this,void 0,void 0,function*(){if(typeof projectId==='undefined'){throw new AppwriteException('Missing required parameter: "projectId"');} +let path='/projects/{projectId}/platforms'.replace('{projectId}',projectId);let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);}),createPlatform:(projectId,type,name,key,store,hostname)=>__awaiter(this,void 0,void 0,function*(){if(typeof projectId==='undefined'){throw new AppwriteException('Missing required parameter: "projectId"');} +if(typeof type==='undefined'){throw new AppwriteException('Missing required parameter: "type"');} +if(typeof name==='undefined'){throw new AppwriteException('Missing required parameter: "name"');} +let path='/projects/{projectId}/platforms'.replace('{projectId}',projectId);let payload={};if(typeof type!=='undefined'){payload['type']=type;} +if(typeof name!=='undefined'){payload['name']=name;} +if(typeof key!=='undefined'){payload['key']=key;} +if(typeof store!=='undefined'){payload['store']=store;} +if(typeof hostname!=='undefined'){payload['hostname']=hostname;} +const uri=new URL(this.config.endpoint+path);return yield this.call('post',uri,{'content-type':'application/json',},payload);}),getPlatform:(projectId,platformId)=>__awaiter(this,void 0,void 0,function*(){if(typeof projectId==='undefined'){throw new AppwriteException('Missing required parameter: "projectId"');} +if(typeof platformId==='undefined'){throw new AppwriteException('Missing required parameter: "platformId"');} +let path='/projects/{projectId}/platforms/{platformId}'.replace('{projectId}',projectId).replace('{platformId}',platformId);let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);}),updatePlatform:(projectId,platformId,name,key,store,hostname)=>__awaiter(this,void 0,void 0,function*(){if(typeof projectId==='undefined'){throw new AppwriteException('Missing required parameter: "projectId"');} +if(typeof platformId==='undefined'){throw new AppwriteException('Missing required parameter: "platformId"');} +if(typeof name==='undefined'){throw new AppwriteException('Missing required parameter: "name"');} +let path='/projects/{projectId}/platforms/{platformId}'.replace('{projectId}',projectId).replace('{platformId}',platformId);let payload={};if(typeof name!=='undefined'){payload['name']=name;} +if(typeof key!=='undefined'){payload['key']=key;} +if(typeof store!=='undefined'){payload['store']=store;} +if(typeof hostname!=='undefined'){payload['hostname']=hostname;} +const uri=new URL(this.config.endpoint+path);return yield this.call('put',uri,{'content-type':'application/json',},payload);}),deletePlatform:(projectId,platformId)=>__awaiter(this,void 0,void 0,function*(){if(typeof projectId==='undefined'){throw new AppwriteException('Missing required parameter: "projectId"');} +if(typeof platformId==='undefined'){throw new AppwriteException('Missing required parameter: "platformId"');} +let path='/projects/{projectId}/platforms/{platformId}'.replace('{projectId}',projectId).replace('{platformId}',platformId);let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('delete',uri,{'content-type':'application/json',},payload);}),listTasks:(projectId)=>__awaiter(this,void 0,void 0,function*(){if(typeof projectId==='undefined'){throw new AppwriteException('Missing required parameter: "projectId"');} +let path='/projects/{projectId}/tasks'.replace('{projectId}',projectId);let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);}),createTask:(projectId,name,status,schedule,security,httpMethod,httpUrl,httpHeaders,httpUser,httpPass)=>__awaiter(this,void 0,void 0,function*(){if(typeof projectId==='undefined'){throw new AppwriteException('Missing required parameter: "projectId"');} +if(typeof name==='undefined'){throw new AppwriteException('Missing required parameter: "name"');} +if(typeof status==='undefined'){throw new AppwriteException('Missing required parameter: "status"');} +if(typeof schedule==='undefined'){throw new AppwriteException('Missing required parameter: "schedule"');} +if(typeof security==='undefined'){throw new AppwriteException('Missing required parameter: "security"');} +if(typeof httpMethod==='undefined'){throw new AppwriteException('Missing required parameter: "httpMethod"');} +if(typeof httpUrl==='undefined'){throw new AppwriteException('Missing required parameter: "httpUrl"');} +let path='/projects/{projectId}/tasks'.replace('{projectId}',projectId);let payload={};if(typeof name!=='undefined'){payload['name']=name;} +if(typeof status!=='undefined'){payload['status']=status;} +if(typeof schedule!=='undefined'){payload['schedule']=schedule;} +if(typeof security!=='undefined'){payload['security']=security;} +if(typeof httpMethod!=='undefined'){payload['httpMethod']=httpMethod;} +if(typeof httpUrl!=='undefined'){payload['httpUrl']=httpUrl;} +if(typeof httpHeaders!=='undefined'){payload['httpHeaders']=httpHeaders;} +if(typeof httpUser!=='undefined'){payload['httpUser']=httpUser;} +if(typeof httpPass!=='undefined'){payload['httpPass']=httpPass;} +const uri=new URL(this.config.endpoint+path);return yield this.call('post',uri,{'content-type':'application/json',},payload);}),getTask:(projectId,taskId)=>__awaiter(this,void 0,void 0,function*(){if(typeof projectId==='undefined'){throw new AppwriteException('Missing required parameter: "projectId"');} +if(typeof taskId==='undefined'){throw new AppwriteException('Missing required parameter: "taskId"');} +let path='/projects/{projectId}/tasks/{taskId}'.replace('{projectId}',projectId).replace('{taskId}',taskId);let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);}),updateTask:(projectId,taskId,name,status,schedule,security,httpMethod,httpUrl,httpHeaders,httpUser,httpPass)=>__awaiter(this,void 0,void 0,function*(){if(typeof projectId==='undefined'){throw new AppwriteException('Missing required parameter: "projectId"');} +if(typeof taskId==='undefined'){throw new AppwriteException('Missing required parameter: "taskId"');} +if(typeof name==='undefined'){throw new AppwriteException('Missing required parameter: "name"');} +if(typeof status==='undefined'){throw new AppwriteException('Missing required parameter: "status"');} +if(typeof schedule==='undefined'){throw new AppwriteException('Missing required parameter: "schedule"');} +if(typeof security==='undefined'){throw new AppwriteException('Missing required parameter: "security"');} +if(typeof httpMethod==='undefined'){throw new AppwriteException('Missing required parameter: "httpMethod"');} +if(typeof httpUrl==='undefined'){throw new AppwriteException('Missing required parameter: "httpUrl"');} +let path='/projects/{projectId}/tasks/{taskId}'.replace('{projectId}',projectId).replace('{taskId}',taskId);let payload={};if(typeof name!=='undefined'){payload['name']=name;} +if(typeof status!=='undefined'){payload['status']=status;} +if(typeof schedule!=='undefined'){payload['schedule']=schedule;} +if(typeof security!=='undefined'){payload['security']=security;} +if(typeof httpMethod!=='undefined'){payload['httpMethod']=httpMethod;} +if(typeof httpUrl!=='undefined'){payload['httpUrl']=httpUrl;} +if(typeof httpHeaders!=='undefined'){payload['httpHeaders']=httpHeaders;} +if(typeof httpUser!=='undefined'){payload['httpUser']=httpUser;} +if(typeof httpPass!=='undefined'){payload['httpPass']=httpPass;} +const uri=new URL(this.config.endpoint+path);return yield this.call('put',uri,{'content-type':'application/json',},payload);}),deleteTask:(projectId,taskId)=>__awaiter(this,void 0,void 0,function*(){if(typeof projectId==='undefined'){throw new AppwriteException('Missing required parameter: "projectId"');} +if(typeof taskId==='undefined'){throw new AppwriteException('Missing required parameter: "taskId"');} +let path='/projects/{projectId}/tasks/{taskId}'.replace('{projectId}',projectId).replace('{taskId}',taskId);let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('delete',uri,{'content-type':'application/json',},payload);}),getUsage:(projectId,range)=>__awaiter(this,void 0,void 0,function*(){if(typeof projectId==='undefined'){throw new AppwriteException('Missing required parameter: "projectId"');} +let path='/projects/{projectId}/usage'.replace('{projectId}',projectId);let payload={};if(typeof range!=='undefined'){payload['range']=range;} +const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);}),listWebhooks:(projectId)=>__awaiter(this,void 0,void 0,function*(){if(typeof projectId==='undefined'){throw new AppwriteException('Missing required parameter: "projectId"');} +let path='/projects/{projectId}/webhooks'.replace('{projectId}',projectId);let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);}),createWebhook:(projectId,name,events,url,security,httpUser,httpPass)=>__awaiter(this,void 0,void 0,function*(){if(typeof projectId==='undefined'){throw new AppwriteException('Missing required parameter: "projectId"');} +if(typeof name==='undefined'){throw new AppwriteException('Missing required parameter: "name"');} +if(typeof events==='undefined'){throw new AppwriteException('Missing required parameter: "events"');} +if(typeof url==='undefined'){throw new AppwriteException('Missing required parameter: "url"');} +if(typeof security==='undefined'){throw new AppwriteException('Missing required parameter: "security"');} +let path='/projects/{projectId}/webhooks'.replace('{projectId}',projectId);let payload={};if(typeof name!=='undefined'){payload['name']=name;} +if(typeof events!=='undefined'){payload['events']=events;} +if(typeof url!=='undefined'){payload['url']=url;} +if(typeof security!=='undefined'){payload['security']=security;} +if(typeof httpUser!=='undefined'){payload['httpUser']=httpUser;} +if(typeof httpPass!=='undefined'){payload['httpPass']=httpPass;} +const uri=new URL(this.config.endpoint+path);return yield this.call('post',uri,{'content-type':'application/json',},payload);}),getWebhook:(projectId,webhookId)=>__awaiter(this,void 0,void 0,function*(){if(typeof projectId==='undefined'){throw new AppwriteException('Missing required parameter: "projectId"');} +if(typeof webhookId==='undefined'){throw new AppwriteException('Missing required parameter: "webhookId"');} +let path='/projects/{projectId}/webhooks/{webhookId}'.replace('{projectId}',projectId).replace('{webhookId}',webhookId);let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);}),updateWebhook:(projectId,webhookId,name,events,url,security,httpUser,httpPass)=>__awaiter(this,void 0,void 0,function*(){if(typeof projectId==='undefined'){throw new AppwriteException('Missing required parameter: "projectId"');} +if(typeof webhookId==='undefined'){throw new AppwriteException('Missing required parameter: "webhookId"');} +if(typeof name==='undefined'){throw new AppwriteException('Missing required parameter: "name"');} +if(typeof events==='undefined'){throw new AppwriteException('Missing required parameter: "events"');} +if(typeof url==='undefined'){throw new AppwriteException('Missing required parameter: "url"');} +if(typeof security==='undefined'){throw new AppwriteException('Missing required parameter: "security"');} +let path='/projects/{projectId}/webhooks/{webhookId}'.replace('{projectId}',projectId).replace('{webhookId}',webhookId);let payload={};if(typeof name!=='undefined'){payload['name']=name;} +if(typeof events!=='undefined'){payload['events']=events;} +if(typeof url!=='undefined'){payload['url']=url;} +if(typeof security!=='undefined'){payload['security']=security;} +if(typeof httpUser!=='undefined'){payload['httpUser']=httpUser;} +if(typeof httpPass!=='undefined'){payload['httpPass']=httpPass;} +const uri=new URL(this.config.endpoint+path);return yield this.call('put',uri,{'content-type':'application/json',},payload);}),deleteWebhook:(projectId,webhookId)=>__awaiter(this,void 0,void 0,function*(){if(typeof projectId==='undefined'){throw new AppwriteException('Missing required parameter: "projectId"');} +if(typeof webhookId==='undefined'){throw new AppwriteException('Missing required parameter: "webhookId"');} +let path='/projects/{projectId}/webhooks/{webhookId}'.replace('{projectId}',projectId).replace('{webhookId}',webhookId);let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('delete',uri,{'content-type':'application/json',},payload);})};this.storage={listFiles:(search,limit,offset,orderType)=>__awaiter(this,void 0,void 0,function*(){let path='/storage/files';let payload={};if(typeof search!=='undefined'){payload['search']=search;} +if(typeof limit!=='undefined'){payload['limit']=limit;} +if(typeof offset!=='undefined'){payload['offset']=offset;} +if(typeof orderType!=='undefined'){payload['orderType']=orderType;} +const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);}),createFile:(file,read,write)=>__awaiter(this,void 0,void 0,function*(){if(typeof file==='undefined'){throw new AppwriteException('Missing required parameter: "file"');} +let path='/storage/files';let payload={};if(typeof file!=='undefined'){payload['file']=file;} +if(typeof read!=='undefined'){payload['read']=read;} +if(typeof write!=='undefined'){payload['write']=write;} +const uri=new URL(this.config.endpoint+path);return yield this.call('post',uri,{'content-type':'multipart/form-data',},payload);}),getFile:(fileId)=>__awaiter(this,void 0,void 0,function*(){if(typeof fileId==='undefined'){throw new AppwriteException('Missing required parameter: "fileId"');} +let path='/storage/files/{fileId}'.replace('{fileId}',fileId);let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);}),updateFile:(fileId,read,write)=>__awaiter(this,void 0,void 0,function*(){if(typeof fileId==='undefined'){throw new AppwriteException('Missing required parameter: "fileId"');} +if(typeof read==='undefined'){throw new AppwriteException('Missing required parameter: "read"');} +if(typeof write==='undefined'){throw new AppwriteException('Missing required parameter: "write"');} +let path='/storage/files/{fileId}'.replace('{fileId}',fileId);let payload={};if(typeof read!=='undefined'){payload['read']=read;} +if(typeof write!=='undefined'){payload['write']=write;} +const uri=new URL(this.config.endpoint+path);return yield this.call('put',uri,{'content-type':'application/json',},payload);}),deleteFile:(fileId)=>__awaiter(this,void 0,void 0,function*(){if(typeof fileId==='undefined'){throw new AppwriteException('Missing required parameter: "fileId"');} +let path='/storage/files/{fileId}'.replace('{fileId}',fileId);let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('delete',uri,{'content-type':'application/json',},payload);}),getFileDownload:(fileId)=>{if(typeof fileId==='undefined'){throw new AppwriteException('Missing required parameter: "fileId"');} +let path='/storage/files/{fileId}/download'.replace('{fileId}',fileId);let payload={};const uri=new URL(this.config.endpoint+path);payload['project']=this.config.project;for(const[key,value]of Object.entries(this.flatten(payload))){uri.searchParams.append(key,value);} +return uri;},getFilePreview:(fileId,width,height,quality,borderWidth,borderColor,borderRadius,opacity,rotation,background,output)=>{if(typeof fileId==='undefined'){throw new AppwriteException('Missing required parameter: "fileId"');} +let path='/storage/files/{fileId}/preview'.replace('{fileId}',fileId);let payload={};if(typeof width!=='undefined'){payload['width']=width;} +if(typeof height!=='undefined'){payload['height']=height;} +if(typeof quality!=='undefined'){payload['quality']=quality;} +if(typeof borderWidth!=='undefined'){payload['borderWidth']=borderWidth;} +if(typeof borderColor!=='undefined'){payload['borderColor']=borderColor;} +if(typeof borderRadius!=='undefined'){payload['borderRadius']=borderRadius;} +if(typeof opacity!=='undefined'){payload['opacity']=opacity;} +if(typeof rotation!=='undefined'){payload['rotation']=rotation;} +if(typeof background!=='undefined'){payload['background']=background;} +if(typeof output!=='undefined'){payload['output']=output;} +const uri=new URL(this.config.endpoint+path);payload['project']=this.config.project;for(const[key,value]of Object.entries(this.flatten(payload))){uri.searchParams.append(key,value);} +return uri;},getFileView:(fileId)=>{if(typeof fileId==='undefined'){throw new AppwriteException('Missing required parameter: "fileId"');} +let path='/storage/files/{fileId}/view'.replace('{fileId}',fileId);let payload={};const uri=new URL(this.config.endpoint+path);payload['project']=this.config.project;for(const[key,value]of Object.entries(this.flatten(payload))){uri.searchParams.append(key,value);} +return uri;}};this.teams={list:(search,limit,offset,orderType)=>__awaiter(this,void 0,void 0,function*(){let path='/teams';let payload={};if(typeof search!=='undefined'){payload['search']=search;} +if(typeof limit!=='undefined'){payload['limit']=limit;} +if(typeof offset!=='undefined'){payload['offset']=offset;} +if(typeof orderType!=='undefined'){payload['orderType']=orderType;} +const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);}),create:(name,roles)=>__awaiter(this,void 0,void 0,function*(){if(typeof name==='undefined'){throw new AppwriteException('Missing required parameter: "name"');} +let path='/teams';let payload={};if(typeof name!=='undefined'){payload['name']=name;} +if(typeof roles!=='undefined'){payload['roles']=roles;} +const uri=new URL(this.config.endpoint+path);return yield this.call('post',uri,{'content-type':'application/json',},payload);}),get:(teamId)=>__awaiter(this,void 0,void 0,function*(){if(typeof teamId==='undefined'){throw new AppwriteException('Missing required parameter: "teamId"');} +let path='/teams/{teamId}'.replace('{teamId}',teamId);let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);}),update:(teamId,name)=>__awaiter(this,void 0,void 0,function*(){if(typeof teamId==='undefined'){throw new AppwriteException('Missing required parameter: "teamId"');} +if(typeof name==='undefined'){throw new AppwriteException('Missing required parameter: "name"');} +let path='/teams/{teamId}'.replace('{teamId}',teamId);let payload={};if(typeof name!=='undefined'){payload['name']=name;} +const uri=new URL(this.config.endpoint+path);return yield this.call('put',uri,{'content-type':'application/json',},payload);}),delete:(teamId)=>__awaiter(this,void 0,void 0,function*(){if(typeof teamId==='undefined'){throw new AppwriteException('Missing required parameter: "teamId"');} +let path='/teams/{teamId}'.replace('{teamId}',teamId);let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('delete',uri,{'content-type':'application/json',},payload);}),getMemberships:(teamId,search,limit,offset,orderType)=>__awaiter(this,void 0,void 0,function*(){if(typeof teamId==='undefined'){throw new AppwriteException('Missing required parameter: "teamId"');} +let path='/teams/{teamId}/memberships'.replace('{teamId}',teamId);let payload={};if(typeof search!=='undefined'){payload['search']=search;} +if(typeof limit!=='undefined'){payload['limit']=limit;} +if(typeof offset!=='undefined'){payload['offset']=offset;} +if(typeof orderType!=='undefined'){payload['orderType']=orderType;} +const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);}),createMembership:(teamId,email,roles,url,name)=>__awaiter(this,void 0,void 0,function*(){if(typeof teamId==='undefined'){throw new AppwriteException('Missing required parameter: "teamId"');} +if(typeof email==='undefined'){throw new AppwriteException('Missing required parameter: "email"');} +if(typeof roles==='undefined'){throw new AppwriteException('Missing required parameter: "roles"');} +if(typeof url==='undefined'){throw new AppwriteException('Missing required parameter: "url"');} +let path='/teams/{teamId}/memberships'.replace('{teamId}',teamId);let payload={};if(typeof email!=='undefined'){payload['email']=email;} +if(typeof name!=='undefined'){payload['name']=name;} +if(typeof roles!=='undefined'){payload['roles']=roles;} +if(typeof url!=='undefined'){payload['url']=url;} +const uri=new URL(this.config.endpoint+path);return yield this.call('post',uri,{'content-type':'application/json',},payload);}),updateMembershipRoles:(teamId,membershipId,roles)=>__awaiter(this,void 0,void 0,function*(){if(typeof teamId==='undefined'){throw new AppwriteException('Missing required parameter: "teamId"');} +if(typeof membershipId==='undefined'){throw new AppwriteException('Missing required parameter: "membershipId"');} +if(typeof roles==='undefined'){throw new AppwriteException('Missing required parameter: "roles"');} +let path='/teams/{teamId}/memberships/{membershipId}'.replace('{teamId}',teamId).replace('{membershipId}',membershipId);let payload={};if(typeof roles!=='undefined'){payload['roles']=roles;} +const uri=new URL(this.config.endpoint+path);return yield this.call('patch',uri,{'content-type':'application/json',},payload);}),deleteMembership:(teamId,membershipId)=>__awaiter(this,void 0,void 0,function*(){if(typeof teamId==='undefined'){throw new AppwriteException('Missing required parameter: "teamId"');} +if(typeof membershipId==='undefined'){throw new AppwriteException('Missing required parameter: "membershipId"');} +let path='/teams/{teamId}/memberships/{membershipId}'.replace('{teamId}',teamId).replace('{membershipId}',membershipId);let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('delete',uri,{'content-type':'application/json',},payload);}),updateMembershipStatus:(teamId,membershipId,userId,secret)=>__awaiter(this,void 0,void 0,function*(){if(typeof teamId==='undefined'){throw new AppwriteException('Missing required parameter: "teamId"');} +if(typeof membershipId==='undefined'){throw new AppwriteException('Missing required parameter: "membershipId"');} +if(typeof userId==='undefined'){throw new AppwriteException('Missing required parameter: "userId"');} +if(typeof secret==='undefined'){throw new AppwriteException('Missing required parameter: "secret"');} +let path='/teams/{teamId}/memberships/{membershipId}/status'.replace('{teamId}',teamId).replace('{membershipId}',membershipId);let payload={};if(typeof userId!=='undefined'){payload['userId']=userId;} +if(typeof secret!=='undefined'){payload['secret']=secret;} +const uri=new URL(this.config.endpoint+path);return yield this.call('patch',uri,{'content-type':'application/json',},payload);})};this.users={list:(search,limit,offset,orderType)=>__awaiter(this,void 0,void 0,function*(){let path='/users';let payload={};if(typeof search!=='undefined'){payload['search']=search;} +if(typeof limit!=='undefined'){payload['limit']=limit;} +if(typeof offset!=='undefined'){payload['offset']=offset;} +if(typeof orderType!=='undefined'){payload['orderType']=orderType;} +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='/users';let payload={};if(typeof email!=='undefined'){payload['email']=email;} +if(typeof password!=='undefined'){payload['password']=password;} +if(typeof name!=='undefined'){payload['name']=name;} +const uri=new URL(this.config.endpoint+path);return yield this.call('post',uri,{'content-type':'application/json',},payload);}),get:(userId)=>__awaiter(this,void 0,void 0,function*(){if(typeof userId==='undefined'){throw new AppwriteException('Missing required parameter: "userId"');} +let path='/users/{userId}'.replace('{userId}',userId);let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);}),delete:(userId)=>__awaiter(this,void 0,void 0,function*(){if(typeof userId==='undefined'){throw new AppwriteException('Missing required parameter: "userId"');} +let path='/users/{userId}'.replace('{userId}',userId);let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('delete',uri,{'content-type':'application/json',},payload);}),getLogs:(userId)=>__awaiter(this,void 0,void 0,function*(){if(typeof userId==='undefined'){throw new AppwriteException('Missing required parameter: "userId"');} +let path='/users/{userId}/logs'.replace('{userId}',userId);let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);}),getPrefs:(userId)=>__awaiter(this,void 0,void 0,function*(){if(typeof userId==='undefined'){throw new AppwriteException('Missing required parameter: "userId"');} +let path='/users/{userId}/prefs'.replace('{userId}',userId);let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);}),updatePrefs:(userId,prefs)=>__awaiter(this,void 0,void 0,function*(){if(typeof userId==='undefined'){throw new AppwriteException('Missing required parameter: "userId"');} +if(typeof prefs==='undefined'){throw new AppwriteException('Missing required parameter: "prefs"');} +let path='/users/{userId}/prefs'.replace('{userId}',userId);let payload={};if(typeof prefs!=='undefined'){payload['prefs']=prefs;} +const uri=new URL(this.config.endpoint+path);return yield this.call('patch',uri,{'content-type':'application/json',},payload);}),getSessions:(userId)=>__awaiter(this,void 0,void 0,function*(){if(typeof userId==='undefined'){throw new AppwriteException('Missing required parameter: "userId"');} +let path='/users/{userId}/sessions'.replace('{userId}',userId);let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);}),deleteSessions:(userId)=>__awaiter(this,void 0,void 0,function*(){if(typeof userId==='undefined'){throw new AppwriteException('Missing required parameter: "userId"');} +let path='/users/{userId}/sessions'.replace('{userId}',userId);let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('delete',uri,{'content-type':'application/json',},payload);}),deleteSession:(userId,sessionId)=>__awaiter(this,void 0,void 0,function*(){if(typeof userId==='undefined'){throw new AppwriteException('Missing required parameter: "userId"');} +if(typeof sessionId==='undefined'){throw new AppwriteException('Missing required parameter: "sessionId"');} +let path='/users/{userId}/sessions/{sessionId}'.replace('{userId}',userId).replace('{sessionId}',sessionId);let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('delete',uri,{'content-type':'application/json',},payload);}),updateStatus:(userId,status)=>__awaiter(this,void 0,void 0,function*(){if(typeof userId==='undefined'){throw new AppwriteException('Missing required parameter: "userId"');} +if(typeof status==='undefined'){throw new AppwriteException('Missing required parameter: "status"');} +let path='/users/{userId}/status'.replace('{userId}',userId);let payload={};if(typeof status!=='undefined'){payload['status']=status;} +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;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(','));} +else{formData.append(key,params[key]);}} +options.body=formData;delete headers['content-type'];break;}} +try{let data=null;const response=yield crossFetch.fetch(url.toString(),options);if((_b=response.headers.get("content-type"))===null||_b===void 0?void 0:_b.includes("application/json")){data=yield response.json();} +else{data={message:yield response.text()};} +if(400<=response.status){throw new AppwriteException(data===null||data===void 0?void 0:data.message,response.status,data);} +const cookieFallback=response.headers.get('X-Fallback-Cookies');if(typeof window!=='undefined'&&window.localStorage&&cookieFallback){window.console.warn('Appwrite is using localStorage for session management. Increase your security by adding a custom domain as your API endpoint.');window.localStorage.setItem('cookieFallback',cookieFallback);} +return data;} +catch(e){throw new AppwriteException(e.message);}});} +flatten(data,prefix=''){let output={};for(const key in data){let value=data[key];let finalKey=prefix?`${prefix}[${key}]`:key;if(Array.isArray(value)){output=Object.assign(output,this.flatten(value,finalKey));} +else{output[finalKey]=value;}} +return output;}} +exports.Appwrite=Appwrite;Object.defineProperty(exports,'__esModule',{value:true});}(this.window=this.window||{},null,window));(function(global,factory){typeof exports==='object'&&typeof module!=='undefined'?module.exports=factory(function(){try{return require('moment');}catch(e){}}()):typeof define==='function'&&define.amd?define(['require'],function(require){return factory(function(){try{return require('moment');}catch(e){}}());}):(global=global||self,global.Chart=factory(global.moment));}(this,(function(moment){'use strict';moment=moment&&moment.hasOwnProperty('default')?moment['default']:moment;function createCommonjsModule(fn,module){return module={exports:{}},fn(module,module.exports),module.exports;} function getCjsExportFromNamespace(n){return n&&n['default']||n;} var colorName={"aliceblue":[240,248,255],"antiquewhite":[250,235,215],"aqua":[0,255,255],"aquamarine":[127,255,212],"azure":[240,255,255],"beige":[245,245,220],"bisque":[255,228,196],"black":[0,0,0],"blanchedalmond":[255,235,205],"blue":[0,0,255],"blueviolet":[138,43,226],"brown":[165,42,42],"burlywood":[222,184,135],"cadetblue":[95,158,160],"chartreuse":[127,255,0],"chocolate":[210,105,30],"coral":[255,127,80],"cornflowerblue":[100,149,237],"cornsilk":[255,248,220],"crimson":[220,20,60],"cyan":[0,255,255],"darkblue":[0,0,139],"darkcyan":[0,139,139],"darkgoldenrod":[184,134,11],"darkgray":[169,169,169],"darkgreen":[0,100,0],"darkgrey":[169,169,169],"darkkhaki":[189,183,107],"darkmagenta":[139,0,139],"darkolivegreen":[85,107,47],"darkorange":[255,140,0],"darkorchid":[153,50,204],"darkred":[139,0,0],"darksalmon":[233,150,122],"darkseagreen":[143,188,143],"darkslateblue":[72,61,139],"darkslategray":[47,79,79],"darkslategrey":[47,79,79],"darkturquoise":[0,206,209],"darkviolet":[148,0,211],"deeppink":[255,20,147],"deepskyblue":[0,191,255],"dimgray":[105,105,105],"dimgrey":[105,105,105],"dodgerblue":[30,144,255],"firebrick":[178,34,34],"floralwhite":[255,250,240],"forestgreen":[34,139,34],"fuchsia":[255,0,255],"gainsboro":[220,220,220],"ghostwhite":[248,248,255],"gold":[255,215,0],"goldenrod":[218,165,32],"gray":[128,128,128],"green":[0,128,0],"greenyellow":[173,255,47],"grey":[128,128,128],"honeydew":[240,255,240],"hotpink":[255,105,180],"indianred":[205,92,92],"indigo":[75,0,130],"ivory":[255,255,240],"khaki":[240,230,140],"lavender":[230,230,250],"lavenderblush":[255,240,245],"lawngreen":[124,252,0],"lemonchiffon":[255,250,205],"lightblue":[173,216,230],"lightcoral":[240,128,128],"lightcyan":[224,255,255],"lightgoldenrodyellow":[250,250,210],"lightgray":[211,211,211],"lightgreen":[144,238,144],"lightgrey":[211,211,211],"lightpink":[255,182,193],"lightsalmon":[255,160,122],"lightseagreen":[32,178,170],"lightskyblue":[135,206,250],"lightslategray":[119,136,153],"lightslategrey":[119,136,153],"lightsteelblue":[176,196,222],"lightyellow":[255,255,224],"lime":[0,255,0],"limegreen":[50,205,50],"linen":[250,240,230],"magenta":[255,0,255],"maroon":[128,0,0],"mediumaquamarine":[102,205,170],"mediumblue":[0,0,205],"mediumorchid":[186,85,211],"mediumpurple":[147,112,219],"mediumseagreen":[60,179,113],"mediumslateblue":[123,104,238],"mediumspringgreen":[0,250,154],"mediumturquoise":[72,209,204],"mediumvioletred":[199,21,133],"midnightblue":[25,25,112],"mintcream":[245,255,250],"mistyrose":[255,228,225],"moccasin":[255,228,181],"navajowhite":[255,222,173],"navy":[0,0,128],"oldlace":[253,245,230],"olive":[128,128,0],"olivedrab":[107,142,35],"orange":[255,165,0],"orangered":[255,69,0],"orchid":[218,112,214],"palegoldenrod":[238,232,170],"palegreen":[152,251,152],"paleturquoise":[175,238,238],"palevioletred":[219,112,147],"papayawhip":[255,239,213],"peachpuff":[255,218,185],"peru":[205,133,63],"pink":[255,192,203],"plum":[221,160,221],"powderblue":[176,224,230],"purple":[128,0,128],"rebeccapurple":[102,51,153],"red":[255,0,0],"rosybrown":[188,143,143],"royalblue":[65,105,225],"saddlebrown":[139,69,19],"salmon":[250,128,114],"sandybrown":[244,164,96],"seagreen":[46,139,87],"seashell":[255,245,238],"sienna":[160,82,45],"silver":[192,192,192],"skyblue":[135,206,235],"slateblue":[106,90,205],"slategray":[112,128,144],"slategrey":[112,128,144],"snow":[255,250,250],"springgreen":[0,255,127],"steelblue":[70,130,180],"tan":[210,180,140],"teal":[0,128,128],"thistle":[216,191,216],"tomato":[255,99,71],"turquoise":[64,224,208],"violet":[238,130,238],"wheat":[245,222,179],"white":[255,255,255],"whitesmoke":[245,245,245],"yellow":[255,255,0],"yellowgreen":[154,205,50]};var conversions=createCommonjsModule(function(module){var reverseKeywords={};for(var key in colorName){if(colorName.hasOwnProperty(key)){reverseKeywords[colorName[key]]=key;}} var convert=module.exports={rgb:{channels:3,labels:'rgb'},hsl:{channels:3,labels:'hsl'},hsv:{channels:3,labels:'hsv'},hwb:{channels:3,labels:'hwb'},cmyk:{channels:4,labels:'cmyk'},xyz:{channels:3,labels:'xyz'},lab:{channels:3,labels:'lab'},lch:{channels:3,labels:'lch'},hex:{channels:1,labels:['hex']},keyword:{channels:1,labels:['keyword']},ansi16:{channels:1,labels:['ansi16']},ansi256:{channels:1,labels:['ansi256']},hcg:{channels:3,labels:['h','c','g']},apple:{channels:3,labels:['r16','g16','b16']},gray:{channels:1,labels:['gray']}};for(var model in convert){if(convert.hasOwnProperty(model)){if(!('channels'in convert[model])){throw new Error('missing channels property: '+model);} @@ -2100,7 +2107,7 @@ element.dispatchEvent(new Event('looped'));};let template=(element.children.leng else{if(debug){console.error('Missing template "'+source+'"');}} if(!init){view.render(element);} return;} -http.get(source).then(function(element){return function(data){element.innerHTML=data;view.render(element);element.dispatchEvent(new CustomEvent('template-loaded',{bubbles:true,cancelable:false}));}}(element),function(){throw new Error('Failed loading template');});};check(true);for(let i=0;iscope.max){scope.list.pop();scope.counter--;} +http.get(source).then(function(element){return function(data){element.innerHTML=data;view.render(element);element.dispatchEvent(new CustomEvent('template-loaded',{bubbles:true,cancelable:false}));}}(element),function(){throw new Error('Failed loading template');});};check(true);for(let i=0;i{const realtime=window.ls.container.get('realtime');window.ls.container.get('console').subscribe('project',event=>{realtime.set(event.payload);});});(function(window){"use strict";window.ls.container.set('alerts',function(window){return{list:[],ids:0,counter:0,max:5,add:function(message,time){var scope=this;message.id=scope.ids++;message.remove=function(){scope.remove(message.id);};scope.counter++;scope.list.unshift(message);if(scope.counter>scope.max){scope.list.pop();scope.counter--;} if(time>0){window.setTimeout(function(message){return function(){scope.remove(message.id)}}(message),time);} return message.id;},remove:function(id){let scope=this;for(let index=0;index0)?'?'+buildQuery(params):''),headers,{});},'post':function(path,headers={},params={},progress=null){return call('POST',path,headers,params,progress);},'put':function(path,headers={},params={},progress=null){return call('PUT',headers,params,progress);},'patch':function(path,headers={},params={},progress=null){return call('PATCH',path,headers,params,progress);},'delete':function(path,headers={},params={},progress=null){return call('DELETE',path,headers,params,progress);},'addGlobalParam':addGlobalParam,'addGlobalHeader':addGlobalHeader}}(window.document);let analytics={create:function(id,source,activity,url){return http.post('/analytics',{'content-type':'application/json'},{id:id,source:source,activity:activity,url:url,version:env.VERSION,setup:env.SETUP});},};return{analytics:analytics,};},true);})(window);(function(window){"use strict";window.ls.container.set('console',function(window){var sdk=new window.Appwrite();sdk.setEndpoint(APP_ENV.API).setProject('console').setLocale(APP_ENV.LOCALE);return sdk;},true);})(window);(function(window){"use strict";window.ls.container.set('date',function(){function format(format,timestamp){var jsdate,f +request.onerror=function(){reject(new Error("Network Error"));};request.send(params);})};return{'get':function(path,headers={},params={}){return call('GET',path+((params.length>0)?'?'+buildQuery(params):''),headers,{});},'post':function(path,headers={},params={},progress=null){return call('POST',path,headers,params,progress);},'put':function(path,headers={},params={},progress=null){return call('PUT',headers,params,progress);},'patch':function(path,headers={},params={},progress=null){return call('PATCH',path,headers,params,progress);},'delete':function(path,headers={},params={},progress=null){return call('DELETE',path,headers,params,progress);},'addGlobalParam':addGlobalParam,'addGlobalHeader':addGlobalHeader}}(window.document);let analytics={create:function(id,source,activity,url){return http.post('/analytics',{'content-type':'application/json'},{id:id,source:source,activity:activity,url:url,version:env.VERSION,setup:env.SETUP});},};return{analytics:analytics,};},true);})(window);(function(window){"use strict";window.ls.container.set('console',function(window){var sdk=new window.Appwrite();sdk.setEndpoint(window.location.protocol+'//'+window.location.host+APP_ENV.API).setProject('console').setLocale(APP_ENV.LOCALE);return sdk;},true);})(window);(function(window){"use strict";window.ls.container.set('date',function(){function format(format,timestamp){var jsdate,f var txtWords=['Sun','Mon','Tues','Wednes','Thurs','Fri','Satur','January','February','March','April','May','June','July','August','September','October','November','December'] var formatChr=/\\?(.?)/gi var formatChrCb=function(t,s){return f[t]?f[t]():s} @@ -2244,7 +2251,7 @@ return slf.renderToken(tokens,idx,opts);} md.renderer.rules.strong_open=renderEm;md.renderer.rules.strong_close=renderEm;return md;},true);})(window);(function(window){"use strict";window.ls.container.set('rtl',function(){var rtlStock="^ا^ب^ت^ث^ج^ح^خ^د^ذ^ر^ز^س^ش^ص^ض^ط^ظ^ع^غ^ف^ق^ك^ل^م^ن^ه^و^ي^א^ב^ג^ד^ה^ו^ז^ח^ט^י^כ^ך^ל^מ^ם^נ^ן^ס^ע^פ^ף^צ^ץ^ק^ר^ש^ת^";var special=["\n"," "," ","״",'"',"_","'","!","@","#","$","^","&","%","*","(",")","+","=","-","[","]","\\","/","{","}","|",":","<",">","?",",",".","0","1","2","3","4","5","6","7","8","9"];var isRTL=function(value){for(var i=0;i{return{current:false,set:function(currentConnections){var scope=this;scope.current=currentConnections;return scope.current;}};},true,true);})(window);window.ls.router.add("/auth/signin",{template:"/auth/signin?version="+APP_ENV.CACHEBUSTER,scope:"home"}).add("/auth/signup",{template:"/auth/signup?version="+APP_ENV.CACHEBUSTER,scope:"home"}).add("/auth/recovery",{template:"/auth/recovery?version="+APP_ENV.CACHEBUSTER,scope:"home"}).add("/auth/recovery/reset",{template:"/auth/recovery/reset?version="+APP_ENV.CACHEBUSTER,scope:"home"}).add("/auth/confirm",{template:"/auth/confirm?version="+APP_ENV.CACHEBUSTER,scope:"home"}).add("/auth/join",{template:"/auth/join?version="+APP_ENV.CACHEBUSTER,scope:"home"}).add("/auth/oauth2/success",{template:"/auth/oauth2/success?version="+APP_ENV.CACHEBUSTER,scope:"home"}).add("/auth/oauth2/failure",{template:"/auth/oauth2/failure?version="+APP_ENV.CACHEBUSTER,scope:"home"}).add("/console",{template:"/console?version="+APP_ENV.CACHEBUSTER,scope:"console"}).add("/console/account",{template:"/console/account?version="+APP_ENV.CACHEBUSTER,scope:"console"}).add("/console/account/:tab",{template:"/console/account?version="+APP_ENV.CACHEBUSTER,scope:"console"}).add("/console/home",{template:"/console/home?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/home/:tab",{template:"/console/home?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/platforms/:platform",{template:function(window){return window.location.pathname+"?version="+APP_ENV.CACHEBUSTER;},scope:"console",project:true}).add("/console/notifications",{template:"/console/notifications?version="+APP_ENV.CACHEBUSTER,scope:"console"}).add("/console/settings",{template:"/console/settings?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/settings/:tab",{template:"/console/settings?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/webhooks",{template:"/console/webhooks?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/webhooks/:tab",{template:"/console/webhooks?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/keys",{template:"/console/keys?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/keys/:tab",{template:"/console/keys?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/tasks",{template:"/console/tasks?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/tasks/:tab",{template:"/console/tasks?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/database",{template:"/console/database?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/database/collection",{template:function(window){return window.location.pathname+window.location.search+'&version='+APP_ENV.CACHEBUSTER;},scope:"console",project:true}).add("/console/database/collection/:tab",{template:function(window){return window.location.pathname+window.location.search+'&version='+APP_ENV.CACHEBUSTER;},scope:"console",project:true}).add("/console/database/document",{template:function(window){return window.location.pathname+window.location.search+'&version='+APP_ENV.CACHEBUSTER;},scope:"console",project:true}).add("/console/database/document/:tab",{template:function(window){return window.location.pathname+window.location.search+'&version='+APP_ENV.CACHEBUSTER;},scope:"console",project:true}).add("/console/storage",{template:"/console/storage?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/storage/:tab",{template:"/console/storage?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/users",{template:"/console/users?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/users/user",{template:"/console/users/user?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/users/user/:tab",{template:"/console/users/user?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/users/teams/team",{template:"/console/users/teams/team?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/users/teams/team/:tab",{template:"/console/users/teams/team?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/users/:tab",{template:"/console/users?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/functions",{template:"/console/functions?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/functions/function",{template:"/console/functions/function?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/functions/function/:tab",{template:"/console/functions/function?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/functions/:tab",{template:"/console/functions?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true});window.ls.filter.add("avatar",function($value,element){if(!$value){return"";} let size=element.dataset["size"]||80;let name=$value.name||$value||"";name=(typeof name!=='string')?'--':name;return def="/v1/avatars/initials?project=console"+"&name="+ encodeURIComponent(name)+"&width="+ size+"&height="+ @@ -2272,7 +2279,7 @@ return $value;}).add("platformsLimit",function($value){return $value;}).add("lim return $value.join(", ").replace(/,\s([^,]+)$/,' and $1');}).add("envName",function($value,env){if(env&&env.RUNTIMES&&env.RUNTIMES[$value]){return env.RUNTIMES[$value].name;} return'';}).add("envLogo",function($value,env){if(env&&env.RUNTIMES&&env.RUNTIMES[$value]){return env.RUNTIMES[$value].logo;} return'';}).add("envVersion",function($value,env){if(env&&env.RUNTIMES&&env.RUNTIMES[$value]){return env.RUNTIMES[$value].version;} -return'';});function abbreviate(number,maxPlaces,forcePlaces,forceLetter){number=Number(number);forceLetter=forceLetter||false;if(forceLetter!==false){return annotate(number,maxPlaces,forcePlaces,forceLetter);} +return'';}).add("accessProject",function($value,router){return $value.hasOwnProperty(router.params.project)?$value[router.params.project]:0;});function abbreviate(number,maxPlaces,forcePlaces,forceLetter){number=Number(number);forceLetter=forceLetter||false;if(forceLetter!==false){return annotate(number,maxPlaces,forcePlaces,forceLetter);} let abbr;if(number>=1e12){abbr="T";}else if(number>=1e9){abbr="B";}else if(number>=1e6){abbr="M";}else if(number>=1e3){abbr="K";}else{abbr="";} return annotate(number,maxPlaces,forcePlaces,abbr);} function annotate(number,maxPlaces,forcePlaces,abbr){let rounded=0;switch(abbr){case"T":rounded=number/1e12;break;case"B":rounded=number/1e9;break;case"M":rounded=number/1e6;break;case"K":rounded=number/1e3;break;case"":rounded=number;break;} @@ -2294,7 +2301,7 @@ let attrKey=prefix+key.charAt(0).toUpperCase()+key.slice(1);if(element.dataset[a if(typeof data[key]!=='undefined'){result=data[key];} if(typeof result==='undefined'){result="";} return result;};let resolve=function(target,prefix="param",data={}){if(!target){return function(){};} -let args=getParams(target);return target.apply(target,args.map(function(value){let result=getValue(value,prefix,data);return result;}));};let exec=function(event){let parsedSuccess=expression.parse(success);let parsedFailure=expression.parse(failure);let parsedAction=expression.parse(action);parsedSuccess=parsedSuccess&&parsedSuccess!=""?parsedSuccess.split(",").map(element=>element.trim()):[];parsedFailure=parsedFailure&&parsedFailure!=""?parsedFailure.split(",").map(element=>element.trim()):[];element.$lsSkip=true;element.classList.add("load-service-start");if(!document.body.contains(element)){element=undefined;return false;} +let args=getParams(target);return target.apply(target,args.map(function(value){let result=getValue(value,prefix,data);return result??undefined;}));};let exec=function(event){let parsedSuccess=expression.parse(success);let parsedFailure=expression.parse(failure);let parsedAction=expression.parse(action);parsedSuccess=parsedSuccess&&parsedSuccess!=""?parsedSuccess.split(",").map(element=>element.trim()):[];parsedFailure=parsedFailure&&parsedFailure!=""?parsedFailure.split(",").map(element=>element.trim()):[];element.$lsSkip=true;element.classList.add("load-service-start");if(!document.body.contains(element)){element=undefined;return false;} if(event){event.preventDefault();} if(running){return false;} running=true;element.style.backgroud='red';if(confirm){if(window.confirm(confirm)!==true){element.classList.add("load-service-end");element.$lsSkip=false;running=false;return false;}} diff --git a/public/dist/scripts/app-dep.js b/public/dist/scripts/app-dep.js index 71a2fdaf8f..056414e9b2 100644 --- a/public/dist/scripts/app-dep.js +++ b/public/dist/scripts/app-dep.js @@ -1,463 +1,470 @@ -(function(window){'use strict';window.Appwrite=function(){let config={endpoint:'https://appwrite.io/v1',project:'',key:'',locale:'',mode:'',};let setEndpoint=function(endpoint){config.endpoint=endpoint;return this;};let setProject=function(value) -{http.addGlobalHeader('X-Appwrite-Project',value);config.project=value;return this;};let setKey=function(value) -{http.addGlobalHeader('X-Appwrite-Key',value);config.key=value;return this;};let setLocale=function(value) -{http.addGlobalHeader('X-Appwrite-Locale',value);config.locale=value;return this;};let setMode=function(value) -{http.addGlobalHeader('X-Appwrite-Mode',value);config.mode=value;return this;};let http=function(document){let globalParams=[],globalHeaders=[];let addParam=function(url,param,value){let a=document.createElement('a'),regex=/(?:\?|&|&)+([^=]+)(?:=([^&]*))*/g;let match,str=[];a.href=url;param=encodeURIComponent(param);while(match=regex.exec(a.search))if(param!==match[1])str.push(match[1]+(match[2]?"="+match[2]:""));str.push(param+(value?"="+encodeURIComponent(value):""));a.search=str.join("&");return a.href;};let buildQuery=function(params){let str=[];for(let p in params){if(Array.isArray(params[p])){for(let index=0;index=request.status){resolve(data);}else{reject(data);}};if(progress){request.addEventListener('progress',progress);request.upload.addEventListener('progress',progress,false);} -request.onerror=function(){reject(new Error("Network Error"));};request.send(params);})};return{'get':function(path,headers={},params={}){return call('GET',path+((Object.keys(params).length>0)?'?'+buildQuery(params):''),headers,{});},'post':function(path,headers={},params={},progress=null){return call('POST',path,headers,params,progress);},'put':function(path,headers={},params={},progress=null){return call('PUT',path,headers,params,progress);},'patch':function(path,headers={},params={},progress=null){return call('PATCH',path,headers,params,progress);},'delete':function(path,headers={},params={},progress=null){return call('DELETE',path,headers,params,progress);},'addGlobalParam':addGlobalParam,'addGlobalHeader':addGlobalHeader}}(window.document);let account={get:function(){let path='/account';let payload={};return http.get(path,{'content-type':'application/json',},payload);},create:function(email,password,name=''){if(email===undefined){throw new Error('Missing required parameter: "email"');} -if(password===undefined){throw new Error('Missing required parameter: "password"');} -let path='/account';let payload={};if(email){payload['email']=email;} -if(password){payload['password']=password;} -if(name){payload['name']=name;} -return http.post(path,{'content-type':'application/json',},payload);},delete:function(){let path='/account';let payload={};return http.delete(path,{'content-type':'application/json',},payload);},updateEmail:function(email,password){if(email===undefined){throw new Error('Missing required parameter: "email"');} -if(password===undefined){throw new Error('Missing required parameter: "password"');} -let path='/account/email';let payload={};if(email){payload['email']=email;} -if(password){payload['password']=password;} -return http.patch(path,{'content-type':'application/json',},payload);},getLogs:function(){let path='/account/logs';let payload={};return http.get(path,{'content-type':'application/json',},payload);},updateName:function(name){if(name===undefined){throw new Error('Missing required parameter: "name"');} -let path='/account/name';let payload={};if(name){payload['name']=name;} -return http.patch(path,{'content-type':'application/json',},payload);},updatePassword:function(password,oldPassword){if(password===undefined){throw new Error('Missing required parameter: "password"');} -if(oldPassword===undefined){throw new Error('Missing required parameter: "oldPassword"');} -let path='/account/password';let payload={};if(password){payload['password']=password;} -if(oldPassword){payload['oldPassword']=oldPassword;} -return http.patch(path,{'content-type':'application/json',},payload);},getPrefs:function(){let path='/account/prefs';let payload={};return http.get(path,{'content-type':'application/json',},payload);},updatePrefs:function(prefs){if(prefs===undefined){throw new Error('Missing required parameter: "prefs"');} -let path='/account/prefs';let payload={};if(prefs){payload['prefs']=prefs;} -return http.patch(path,{'content-type':'application/json',},payload);},createRecovery:function(email,url){if(email===undefined){throw new Error('Missing required parameter: "email"');} -if(url===undefined){throw new Error('Missing required parameter: "url"');} -let path='/account/recovery';let payload={};if(email){payload['email']=email;} -if(url){payload['url']=url;} -return http.post(path,{'content-type':'application/json',},payload);},updateRecovery:function(userId,secret,password,passwordAgain){if(userId===undefined){throw new Error('Missing required parameter: "userId"');} -if(secret===undefined){throw new Error('Missing required parameter: "secret"');} -if(password===undefined){throw new Error('Missing required parameter: "password"');} -if(passwordAgain===undefined){throw new Error('Missing required parameter: "passwordAgain"');} -let path='/account/recovery';let payload={};if(userId){payload['userId']=userId;} -if(secret){payload['secret']=secret;} -if(password){payload['password']=password;} -if(passwordAgain){payload['passwordAgain']=passwordAgain;} -return http.put(path,{'content-type':'application/json',},payload);},getSessions:function(){let path='/account/sessions';let payload={};return http.get(path,{'content-type':'application/json',},payload);},createSession:function(email,password){if(email===undefined){throw new Error('Missing required parameter: "email"');} -if(password===undefined){throw new Error('Missing required parameter: "password"');} -let path='/account/sessions';let payload={};if(email){payload['email']=email;} -if(password){payload['password']=password;} -return http.post(path,{'content-type':'application/json',},payload);},deleteSessions:function(){let path='/account/sessions';let payload={};return http.delete(path,{'content-type':'application/json',},payload);},createOAuth2Session:function(provider,success='https://appwrite.io/auth/oauth2/success',failure='https://appwrite.io/auth/oauth2/failure',scopes=[]){if(provider===undefined){throw new Error('Missing required parameter: "provider"');} -let path='/account/sessions/oauth2/{provider}'.replace(new RegExp('{provider}','g'),provider);let payload={};if(success){payload['success']=success;} -if(failure){payload['failure']=failure;} -if(scopes){payload['scopes']=scopes;} -payload['project']=config.project;payload['key']=config.key;let query=[];for(let p in payload){if(Array.isArray(payload[p])){for(let index=0;index{var _a;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());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;if(((_b=(_a=this.realtime)===null||_a===void 0?void 0:_a.lastMessage)===null||_b===void 0?void 0:_b.code)===1008){return;} +console.error('Realtime got disconnected. Reconnect will be attempted in 1 second.',event.reason);setTimeout(()=>{this.realtime.createSocket();},1000);});},onMessage:(channel,callback)=>(event)=>{try{const data=JSON.parse(event.data);this.realtime.lastMessage=data;if(data.channels&&data.channels.includes(channel)){callback(data);} +else if(data.code){throw 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;} +if(typeof name!=='undefined'){payload['name']=name;} +const uri=new URL(this.config.endpoint+path);return yield this.call('post',uri,{'content-type':'application/json',},payload);}),delete:()=>__awaiter(this,void 0,void 0,function*(){let path='/account';let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('delete',uri,{'content-type':'application/json',},payload);}),updateEmail:(email,password)=>__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/email';let payload={};if(typeof email!=='undefined'){payload['email']=email;} +if(typeof password!=='undefined'){payload['password']=password;} +const uri=new URL(this.config.endpoint+path);return yield this.call('patch',uri,{'content-type':'application/json',},payload);}),createJWT:()=>__awaiter(this,void 0,void 0,function*(){let path='/account/jwt';let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('post',uri,{'content-type':'application/json',},payload);}),getLogs:()=>__awaiter(this,void 0,void 0,function*(){let path='/account/logs';let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);}),updateName:(name)=>__awaiter(this,void 0,void 0,function*(){if(typeof name==='undefined'){throw new AppwriteException('Missing required parameter: "name"');} +let path='/account/name';let payload={};if(typeof name!=='undefined'){payload['name']=name;} +const uri=new URL(this.config.endpoint+path);return yield this.call('patch',uri,{'content-type':'application/json',},payload);}),updatePassword:(password,oldPassword)=>__awaiter(this,void 0,void 0,function*(){if(typeof password==='undefined'){throw new AppwriteException('Missing required parameter: "password"');} +let path='/account/password';let payload={};if(typeof password!=='undefined'){payload['password']=password;} +if(typeof oldPassword!=='undefined'){payload['oldPassword']=oldPassword;} +const uri=new URL(this.config.endpoint+path);return yield this.call('patch',uri,{'content-type':'application/json',},payload);}),getPrefs:()=>__awaiter(this,void 0,void 0,function*(){let path='/account/prefs';let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);}),updatePrefs:(prefs)=>__awaiter(this,void 0,void 0,function*(){if(typeof prefs==='undefined'){throw new AppwriteException('Missing required parameter: "prefs"');} +let path='/account/prefs';let payload={};if(typeof prefs!=='undefined'){payload['prefs']=prefs;} +const uri=new URL(this.config.endpoint+path);return yield this.call('patch',uri,{'content-type':'application/json',},payload);}),createRecovery:(email,url)=>__awaiter(this,void 0,void 0,function*(){if(typeof email==='undefined'){throw new AppwriteException('Missing required parameter: "email"');} +if(typeof url==='undefined'){throw new AppwriteException('Missing required parameter: "url"');} +let path='/account/recovery';let payload={};if(typeof email!=='undefined'){payload['email']=email;} +if(typeof url!=='undefined'){payload['url']=url;} +const uri=new URL(this.config.endpoint+path);return yield this.call('post',uri,{'content-type':'application/json',},payload);}),updateRecovery:(userId,secret,password,passwordAgain)=>__awaiter(this,void 0,void 0,function*(){if(typeof userId==='undefined'){throw new AppwriteException('Missing required parameter: "userId"');} +if(typeof secret==='undefined'){throw new AppwriteException('Missing required parameter: "secret"');} +if(typeof password==='undefined'){throw new AppwriteException('Missing required parameter: "password"');} +if(typeof passwordAgain==='undefined'){throw new AppwriteException('Missing required parameter: "passwordAgain"');} +let path='/account/recovery';let payload={};if(typeof userId!=='undefined'){payload['userId']=userId;} +if(typeof secret!=='undefined'){payload['secret']=secret;} +if(typeof password!=='undefined'){payload['password']=password;} +if(typeof passwordAgain!=='undefined'){payload['passwordAgain']=passwordAgain;} +const uri=new URL(this.config.endpoint+path);return yield this.call('put',uri,{'content-type':'application/json',},payload);}),getSessions:()=>__awaiter(this,void 0,void 0,function*(){let path='/account/sessions';let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);}),createSession:(email,password)=>__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/sessions';let payload={};if(typeof email!=='undefined'){payload['email']=email;} +if(typeof password!=='undefined'){payload['password']=password;} +const uri=new URL(this.config.endpoint+path);return yield this.call('post',uri,{'content-type':'application/json',},payload);}),deleteSessions:()=>__awaiter(this,void 0,void 0,function*(){let path='/account/sessions';let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('delete',uri,{'content-type':'application/json',},payload);}),createAnonymousSession:()=>__awaiter(this,void 0,void 0,function*(){let path='/account/sessions/anonymous';let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('post',uri,{'content-type':'application/json',},payload);}),createOAuth2Session:(provider,success,failure,scopes)=>{if(typeof provider==='undefined'){throw new AppwriteException('Missing required parameter: "provider"');} +let path='/account/sessions/oauth2/{provider}'.replace('{provider}',provider);let payload={};if(typeof success!=='undefined'){payload['success']=success;} +if(typeof failure!=='undefined'){payload['failure']=failure;} +if(typeof scopes!=='undefined'){payload['scopes']=scopes;} +const uri=new URL(this.config.endpoint+path);payload['project']=this.config.project;for(const[key,value]of Object.entries(this.flatten(payload))){uri.searchParams.append(key,value);} +if(typeof window!=='undefined'&&(window===null||window===void 0?void 0:window.location)){window.location.href=uri.toString();} +else{return uri;}},deleteSession:(sessionId)=>__awaiter(this,void 0,void 0,function*(){if(typeof sessionId==='undefined'){throw new AppwriteException('Missing required parameter: "sessionId"');} +let path='/account/sessions/{sessionId}'.replace('{sessionId}',sessionId);let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('delete',uri,{'content-type':'application/json',},payload);}),createVerification:(url)=>__awaiter(this,void 0,void 0,function*(){if(typeof url==='undefined'){throw new AppwriteException('Missing required parameter: "url"');} +let path='/account/verification';let payload={};if(typeof url!=='undefined'){payload['url']=url;} +const uri=new URL(this.config.endpoint+path);return yield this.call('post',uri,{'content-type':'application/json',},payload);}),updateVerification:(userId,secret)=>__awaiter(this,void 0,void 0,function*(){if(typeof userId==='undefined'){throw new AppwriteException('Missing required parameter: "userId"');} +if(typeof secret==='undefined'){throw new AppwriteException('Missing required parameter: "secret"');} +let path='/account/verification';let payload={};if(typeof userId!=='undefined'){payload['userId']=userId;} +if(typeof secret!=='undefined'){payload['secret']=secret;} +const uri=new URL(this.config.endpoint+path);return yield this.call('put',uri,{'content-type':'application/json',},payload);})};this.avatars={getBrowser:(code,width,height,quality)=>{if(typeof code==='undefined'){throw new AppwriteException('Missing required parameter: "code"');} +let path='/avatars/browsers/{code}'.replace('{code}',code);let payload={};if(typeof width!=='undefined'){payload['width']=width;} +if(typeof height!=='undefined'){payload['height']=height;} +if(typeof quality!=='undefined'){payload['quality']=quality;} +const uri=new URL(this.config.endpoint+path);payload['project']=this.config.project;for(const[key,value]of Object.entries(this.flatten(payload))){uri.searchParams.append(key,value);} +return uri;},getCreditCard:(code,width,height,quality)=>{if(typeof code==='undefined'){throw new AppwriteException('Missing required parameter: "code"');} +let path='/avatars/credit-cards/{code}'.replace('{code}',code);let payload={};if(typeof width!=='undefined'){payload['width']=width;} +if(typeof height!=='undefined'){payload['height']=height;} +if(typeof quality!=='undefined'){payload['quality']=quality;} +const uri=new URL(this.config.endpoint+path);payload['project']=this.config.project;for(const[key,value]of Object.entries(this.flatten(payload))){uri.searchParams.append(key,value);} +return uri;},getFavicon:(url)=>{if(typeof url==='undefined'){throw new AppwriteException('Missing required parameter: "url"');} +let path='/avatars/favicon';let payload={};if(typeof url!=='undefined'){payload['url']=url;} +const uri=new URL(this.config.endpoint+path);payload['project']=this.config.project;for(const[key,value]of Object.entries(this.flatten(payload))){uri.searchParams.append(key,value);} +return uri;},getFlag:(code,width,height,quality)=>{if(typeof code==='undefined'){throw new AppwriteException('Missing required parameter: "code"');} +let path='/avatars/flags/{code}'.replace('{code}',code);let payload={};if(typeof width!=='undefined'){payload['width']=width;} +if(typeof height!=='undefined'){payload['height']=height;} +if(typeof quality!=='undefined'){payload['quality']=quality;} +const uri=new URL(this.config.endpoint+path);payload['project']=this.config.project;for(const[key,value]of Object.entries(this.flatten(payload))){uri.searchParams.append(key,value);} +return uri;},getImage:(url,width,height)=>{if(typeof url==='undefined'){throw new AppwriteException('Missing required parameter: "url"');} +let path='/avatars/image';let payload={};if(typeof url!=='undefined'){payload['url']=url;} +if(typeof width!=='undefined'){payload['width']=width;} +if(typeof height!=='undefined'){payload['height']=height;} +const uri=new URL(this.config.endpoint+path);payload['project']=this.config.project;for(const[key,value]of Object.entries(this.flatten(payload))){uri.searchParams.append(key,value);} +return uri;},getInitials:(name,width,height,color,background)=>{let path='/avatars/initials';let payload={};if(typeof name!=='undefined'){payload['name']=name;} +if(typeof width!=='undefined'){payload['width']=width;} +if(typeof height!=='undefined'){payload['height']=height;} +if(typeof color!=='undefined'){payload['color']=color;} +if(typeof background!=='undefined'){payload['background']=background;} +const uri=new URL(this.config.endpoint+path);payload['project']=this.config.project;for(const[key,value]of Object.entries(this.flatten(payload))){uri.searchParams.append(key,value);} +return uri;},getQR:(text,size,margin,download)=>{if(typeof text==='undefined'){throw new AppwriteException('Missing required parameter: "text"');} +let path='/avatars/qr';let payload={};if(typeof text!=='undefined'){payload['text']=text;} +if(typeof size!=='undefined'){payload['size']=size;} +if(typeof margin!=='undefined'){payload['margin']=margin;} +if(typeof download!=='undefined'){payload['download']=download;} +const uri=new URL(this.config.endpoint+path);payload['project']=this.config.project;for(const[key,value]of Object.entries(this.flatten(payload))){uri.searchParams.append(key,value);} +return uri;}};this.database={listCollections:(search,limit,offset,orderType)=>__awaiter(this,void 0,void 0,function*(){let path='/database/collections';let payload={};if(typeof search!=='undefined'){payload['search']=search;} +if(typeof limit!=='undefined'){payload['limit']=limit;} +if(typeof offset!=='undefined'){payload['offset']=offset;} +if(typeof orderType!=='undefined'){payload['orderType']=orderType;} +const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);}),createCollection:(name,read,write,rules)=>__awaiter(this,void 0,void 0,function*(){if(typeof name==='undefined'){throw new AppwriteException('Missing required parameter: "name"');} +if(typeof read==='undefined'){throw new AppwriteException('Missing required parameter: "read"');} +if(typeof write==='undefined'){throw new AppwriteException('Missing required parameter: "write"');} +if(typeof rules==='undefined'){throw new AppwriteException('Missing required parameter: "rules"');} +let path='/database/collections';let payload={};if(typeof name!=='undefined'){payload['name']=name;} +if(typeof read!=='undefined'){payload['read']=read;} +if(typeof write!=='undefined'){payload['write']=write;} +if(typeof rules!=='undefined'){payload['rules']=rules;} +const uri=new URL(this.config.endpoint+path);return yield this.call('post',uri,{'content-type':'application/json',},payload);}),getCollection:(collectionId)=>__awaiter(this,void 0,void 0,function*(){if(typeof collectionId==='undefined'){throw new AppwriteException('Missing required parameter: "collectionId"');} +let path='/database/collections/{collectionId}'.replace('{collectionId}',collectionId);let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);}),updateCollection:(collectionId,name,read,write,rules)=>__awaiter(this,void 0,void 0,function*(){if(typeof collectionId==='undefined'){throw new AppwriteException('Missing required parameter: "collectionId"');} +if(typeof name==='undefined'){throw new AppwriteException('Missing required parameter: "name"');} +let path='/database/collections/{collectionId}'.replace('{collectionId}',collectionId);let payload={};if(typeof name!=='undefined'){payload['name']=name;} +if(typeof read!=='undefined'){payload['read']=read;} +if(typeof write!=='undefined'){payload['write']=write;} +if(typeof rules!=='undefined'){payload['rules']=rules;} +const uri=new URL(this.config.endpoint+path);return yield this.call('put',uri,{'content-type':'application/json',},payload);}),deleteCollection:(collectionId)=>__awaiter(this,void 0,void 0,function*(){if(typeof collectionId==='undefined'){throw new AppwriteException('Missing required parameter: "collectionId"');} +let path='/database/collections/{collectionId}'.replace('{collectionId}',collectionId);let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('delete',uri,{'content-type':'application/json',},payload);}),listDocuments:(collectionId,filters,limit,offset,orderField,orderType,orderCast,search)=>__awaiter(this,void 0,void 0,function*(){if(typeof collectionId==='undefined'){throw new AppwriteException('Missing required parameter: "collectionId"');} +let path='/database/collections/{collectionId}/documents'.replace('{collectionId}',collectionId);let payload={};if(typeof filters!=='undefined'){payload['filters']=filters;} +if(typeof limit!=='undefined'){payload['limit']=limit;} +if(typeof offset!=='undefined'){payload['offset']=offset;} +if(typeof orderField!=='undefined'){payload['orderField']=orderField;} +if(typeof orderType!=='undefined'){payload['orderType']=orderType;} +if(typeof orderCast!=='undefined'){payload['orderCast']=orderCast;} +if(typeof search!=='undefined'){payload['search']=search;} +const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);}),createDocument:(collectionId,data,read,write,parentDocument,parentProperty,parentPropertyType)=>__awaiter(this,void 0,void 0,function*(){if(typeof collectionId==='undefined'){throw new AppwriteException('Missing required parameter: "collectionId"');} +if(typeof data==='undefined'){throw new AppwriteException('Missing required parameter: "data"');} +let path='/database/collections/{collectionId}/documents'.replace('{collectionId}',collectionId);let payload={};if(typeof data!=='undefined'){payload['data']=data;} +if(typeof read!=='undefined'){payload['read']=read;} +if(typeof write!=='undefined'){payload['write']=write;} +if(typeof parentDocument!=='undefined'){payload['parentDocument']=parentDocument;} +if(typeof parentProperty!=='undefined'){payload['parentProperty']=parentProperty;} +if(typeof parentPropertyType!=='undefined'){payload['parentPropertyType']=parentPropertyType;} +const uri=new URL(this.config.endpoint+path);return yield this.call('post',uri,{'content-type':'application/json',},payload);}),getDocument:(collectionId,documentId)=>__awaiter(this,void 0,void 0,function*(){if(typeof collectionId==='undefined'){throw new AppwriteException('Missing required parameter: "collectionId"');} +if(typeof documentId==='undefined'){throw new AppwriteException('Missing required parameter: "documentId"');} +let path='/database/collections/{collectionId}/documents/{documentId}'.replace('{collectionId}',collectionId).replace('{documentId}',documentId);let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);}),updateDocument:(collectionId,documentId,data,read,write)=>__awaiter(this,void 0,void 0,function*(){if(typeof collectionId==='undefined'){throw new AppwriteException('Missing required parameter: "collectionId"');} +if(typeof documentId==='undefined'){throw new AppwriteException('Missing required parameter: "documentId"');} +if(typeof data==='undefined'){throw new AppwriteException('Missing required parameter: "data"');} +let path='/database/collections/{collectionId}/documents/{documentId}'.replace('{collectionId}',collectionId).replace('{documentId}',documentId);let payload={};if(typeof data!=='undefined'){payload['data']=data;} +if(typeof read!=='undefined'){payload['read']=read;} +if(typeof write!=='undefined'){payload['write']=write;} +const uri=new URL(this.config.endpoint+path);return yield this.call('patch',uri,{'content-type':'application/json',},payload);}),deleteDocument:(collectionId,documentId)=>__awaiter(this,void 0,void 0,function*(){if(typeof collectionId==='undefined'){throw new AppwriteException('Missing required parameter: "collectionId"');} +if(typeof documentId==='undefined'){throw new AppwriteException('Missing required parameter: "documentId"');} +let path='/database/collections/{collectionId}/documents/{documentId}'.replace('{collectionId}',collectionId).replace('{documentId}',documentId);let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('delete',uri,{'content-type':'application/json',},payload);})};this.functions={list:(search,limit,offset,orderType)=>__awaiter(this,void 0,void 0,function*(){let path='/functions';let payload={};if(typeof search!=='undefined'){payload['search']=search;} +if(typeof limit!=='undefined'){payload['limit']=limit;} +if(typeof offset!=='undefined'){payload['offset']=offset;} +if(typeof orderType!=='undefined'){payload['orderType']=orderType;} +const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);}),create:(name,execute,env,vars,events,schedule,timeout)=>__awaiter(this,void 0,void 0,function*(){if(typeof name==='undefined'){throw new AppwriteException('Missing required parameter: "name"');} +if(typeof execute==='undefined'){throw new AppwriteException('Missing required parameter: "execute"');} +if(typeof env==='undefined'){throw new AppwriteException('Missing required parameter: "env"');} +let path='/functions';let payload={};if(typeof name!=='undefined'){payload['name']=name;} +if(typeof execute!=='undefined'){payload['execute']=execute;} +if(typeof env!=='undefined'){payload['env']=env;} +if(typeof vars!=='undefined'){payload['vars']=vars;} +if(typeof events!=='undefined'){payload['events']=events;} +if(typeof schedule!=='undefined'){payload['schedule']=schedule;} +if(typeof timeout!=='undefined'){payload['timeout']=timeout;} +const uri=new URL(this.config.endpoint+path);return yield this.call('post',uri,{'content-type':'application/json',},payload);}),get:(functionId)=>__awaiter(this,void 0,void 0,function*(){if(typeof functionId==='undefined'){throw new AppwriteException('Missing required parameter: "functionId"');} +let path='/functions/{functionId}'.replace('{functionId}',functionId);let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);}),update:(functionId,name,execute,vars,events,schedule,timeout)=>__awaiter(this,void 0,void 0,function*(){if(typeof functionId==='undefined'){throw new AppwriteException('Missing required parameter: "functionId"');} +if(typeof name==='undefined'){throw new AppwriteException('Missing required parameter: "name"');} +if(typeof execute==='undefined'){throw new AppwriteException('Missing required parameter: "execute"');} +let path='/functions/{functionId}'.replace('{functionId}',functionId);let payload={};if(typeof name!=='undefined'){payload['name']=name;} +if(typeof execute!=='undefined'){payload['execute']=execute;} +if(typeof vars!=='undefined'){payload['vars']=vars;} +if(typeof events!=='undefined'){payload['events']=events;} +if(typeof schedule!=='undefined'){payload['schedule']=schedule;} +if(typeof timeout!=='undefined'){payload['timeout']=timeout;} +const uri=new URL(this.config.endpoint+path);return yield this.call('put',uri,{'content-type':'application/json',},payload);}),delete:(functionId)=>__awaiter(this,void 0,void 0,function*(){if(typeof functionId==='undefined'){throw new AppwriteException('Missing required parameter: "functionId"');} +let path='/functions/{functionId}'.replace('{functionId}',functionId);let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('delete',uri,{'content-type':'application/json',},payload);}),listExecutions:(functionId,search,limit,offset,orderType)=>__awaiter(this,void 0,void 0,function*(){if(typeof functionId==='undefined'){throw new AppwriteException('Missing required parameter: "functionId"');} +let path='/functions/{functionId}/executions'.replace('{functionId}',functionId);let payload={};if(typeof search!=='undefined'){payload['search']=search;} +if(typeof limit!=='undefined'){payload['limit']=limit;} +if(typeof offset!=='undefined'){payload['offset']=offset;} +if(typeof orderType!=='undefined'){payload['orderType']=orderType;} +const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);}),createExecution:(functionId,data)=>__awaiter(this,void 0,void 0,function*(){if(typeof functionId==='undefined'){throw new AppwriteException('Missing required parameter: "functionId"');} +let path='/functions/{functionId}/executions'.replace('{functionId}',functionId);let payload={};if(typeof data!=='undefined'){payload['data']=data;} +const uri=new URL(this.config.endpoint+path);return yield this.call('post',uri,{'content-type':'application/json',},payload);}),getExecution:(functionId,executionId)=>__awaiter(this,void 0,void 0,function*(){if(typeof functionId==='undefined'){throw new AppwriteException('Missing required parameter: "functionId"');} +if(typeof executionId==='undefined'){throw new AppwriteException('Missing required parameter: "executionId"');} +let path='/functions/{functionId}/executions/{executionId}'.replace('{functionId}',functionId).replace('{executionId}',executionId);let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);}),updateTag:(functionId,tag)=>__awaiter(this,void 0,void 0,function*(){if(typeof functionId==='undefined'){throw new AppwriteException('Missing required parameter: "functionId"');} +if(typeof tag==='undefined'){throw new AppwriteException('Missing required parameter: "tag"');} +let path='/functions/{functionId}/tag'.replace('{functionId}',functionId);let payload={};if(typeof tag!=='undefined'){payload['tag']=tag;} +const uri=new URL(this.config.endpoint+path);return yield this.call('patch',uri,{'content-type':'application/json',},payload);}),listTags:(functionId,search,limit,offset,orderType)=>__awaiter(this,void 0,void 0,function*(){if(typeof functionId==='undefined'){throw new AppwriteException('Missing required parameter: "functionId"');} +let path='/functions/{functionId}/tags'.replace('{functionId}',functionId);let payload={};if(typeof search!=='undefined'){payload['search']=search;} +if(typeof limit!=='undefined'){payload['limit']=limit;} +if(typeof offset!=='undefined'){payload['offset']=offset;} +if(typeof orderType!=='undefined'){payload['orderType']=orderType;} +const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);}),createTag:(functionId,command,code)=>__awaiter(this,void 0,void 0,function*(){if(typeof functionId==='undefined'){throw new AppwriteException('Missing required parameter: "functionId"');} +if(typeof command==='undefined'){throw new AppwriteException('Missing required parameter: "command"');} +if(typeof code==='undefined'){throw new AppwriteException('Missing required parameter: "code"');} +let path='/functions/{functionId}/tags'.replace('{functionId}',functionId);let payload={};if(typeof command!=='undefined'){payload['command']=command;} +if(typeof code!=='undefined'){payload['code']=code;} +const uri=new URL(this.config.endpoint+path);return yield this.call('post',uri,{'content-type':'multipart/form-data',},payload);}),getTag:(functionId,tagId)=>__awaiter(this,void 0,void 0,function*(){if(typeof functionId==='undefined'){throw new AppwriteException('Missing required parameter: "functionId"');} +if(typeof tagId==='undefined'){throw new AppwriteException('Missing required parameter: "tagId"');} +let path='/functions/{functionId}/tags/{tagId}'.replace('{functionId}',functionId).replace('{tagId}',tagId);let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);}),deleteTag:(functionId,tagId)=>__awaiter(this,void 0,void 0,function*(){if(typeof functionId==='undefined'){throw new AppwriteException('Missing required parameter: "functionId"');} +if(typeof tagId==='undefined'){throw new AppwriteException('Missing required parameter: "tagId"');} +let path='/functions/{functionId}/tags/{tagId}'.replace('{functionId}',functionId).replace('{tagId}',tagId);let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('delete',uri,{'content-type':'application/json',},payload);}),getUsage:(functionId,range)=>__awaiter(this,void 0,void 0,function*(){if(typeof functionId==='undefined'){throw new AppwriteException('Missing required parameter: "functionId"');} +let path='/functions/{functionId}/usage'.replace('{functionId}',functionId);let payload={};if(typeof range!=='undefined'){payload['range']=range;} +const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);})};this.health={get:()=>__awaiter(this,void 0,void 0,function*(){let path='/health';let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);}),getAntiVirus:()=>__awaiter(this,void 0,void 0,function*(){let path='/health/anti-virus';let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);}),getCache:()=>__awaiter(this,void 0,void 0,function*(){let path='/health/cache';let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);}),getDB:()=>__awaiter(this,void 0,void 0,function*(){let path='/health/db';let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);}),getQueueCertificates:()=>__awaiter(this,void 0,void 0,function*(){let path='/health/queue/certificates';let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);}),getQueueFunctions:()=>__awaiter(this,void 0,void 0,function*(){let path='/health/queue/functions';let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);}),getQueueLogs:()=>__awaiter(this,void 0,void 0,function*(){let path='/health/queue/logs';let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);}),getQueueTasks:()=>__awaiter(this,void 0,void 0,function*(){let path='/health/queue/tasks';let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);}),getQueueUsage:()=>__awaiter(this,void 0,void 0,function*(){let path='/health/queue/usage';let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);}),getQueueWebhooks:()=>__awaiter(this,void 0,void 0,function*(){let path='/health/queue/webhooks';let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);}),getStorageLocal:()=>__awaiter(this,void 0,void 0,function*(){let path='/health/storage/local';let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);}),getTime:()=>__awaiter(this,void 0,void 0,function*(){let path='/health/time';let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);})};this.locale={get:()=>__awaiter(this,void 0,void 0,function*(){let path='/locale';let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);}),getContinents:()=>__awaiter(this,void 0,void 0,function*(){let path='/locale/continents';let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);}),getCountries:()=>__awaiter(this,void 0,void 0,function*(){let path='/locale/countries';let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);}),getCountriesEU:()=>__awaiter(this,void 0,void 0,function*(){let path='/locale/countries/eu';let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);}),getCountriesPhones:()=>__awaiter(this,void 0,void 0,function*(){let path='/locale/countries/phones';let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);}),getCurrencies:()=>__awaiter(this,void 0,void 0,function*(){let path='/locale/currencies';let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);}),getLanguages:()=>__awaiter(this,void 0,void 0,function*(){let path='/locale/languages';let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);})};this.projects={list:(search,limit,offset,orderType)=>__awaiter(this,void 0,void 0,function*(){let path='/projects';let payload={};if(typeof search!=='undefined'){payload['search']=search;} +if(typeof limit!=='undefined'){payload['limit']=limit;} +if(typeof offset!=='undefined'){payload['offset']=offset;} +if(typeof orderType!=='undefined'){payload['orderType']=orderType;} +const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);}),create:(name,teamId,description,logo,url,legalName,legalCountry,legalState,legalCity,legalAddress,legalTaxId)=>__awaiter(this,void 0,void 0,function*(){if(typeof name==='undefined'){throw new AppwriteException('Missing required parameter: "name"');} +if(typeof teamId==='undefined'){throw new AppwriteException('Missing required parameter: "teamId"');} +let path='/projects';let payload={};if(typeof name!=='undefined'){payload['name']=name;} +if(typeof teamId!=='undefined'){payload['teamId']=teamId;} +if(typeof description!=='undefined'){payload['description']=description;} +if(typeof logo!=='undefined'){payload['logo']=logo;} +if(typeof url!=='undefined'){payload['url']=url;} +if(typeof legalName!=='undefined'){payload['legalName']=legalName;} +if(typeof legalCountry!=='undefined'){payload['legalCountry']=legalCountry;} +if(typeof legalState!=='undefined'){payload['legalState']=legalState;} +if(typeof legalCity!=='undefined'){payload['legalCity']=legalCity;} +if(typeof legalAddress!=='undefined'){payload['legalAddress']=legalAddress;} +if(typeof legalTaxId!=='undefined'){payload['legalTaxId']=legalTaxId;} +const uri=new URL(this.config.endpoint+path);return yield this.call('post',uri,{'content-type':'application/json',},payload);}),get:(projectId)=>__awaiter(this,void 0,void 0,function*(){if(typeof projectId==='undefined'){throw new AppwriteException('Missing required parameter: "projectId"');} +let path='/projects/{projectId}'.replace('{projectId}',projectId);let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);}),update:(projectId,name,description,logo,url,legalName,legalCountry,legalState,legalCity,legalAddress,legalTaxId)=>__awaiter(this,void 0,void 0,function*(){if(typeof projectId==='undefined'){throw new AppwriteException('Missing required parameter: "projectId"');} +if(typeof name==='undefined'){throw new AppwriteException('Missing required parameter: "name"');} +let path='/projects/{projectId}'.replace('{projectId}',projectId);let payload={};if(typeof name!=='undefined'){payload['name']=name;} +if(typeof description!=='undefined'){payload['description']=description;} +if(typeof logo!=='undefined'){payload['logo']=logo;} +if(typeof url!=='undefined'){payload['url']=url;} +if(typeof legalName!=='undefined'){payload['legalName']=legalName;} +if(typeof legalCountry!=='undefined'){payload['legalCountry']=legalCountry;} +if(typeof legalState!=='undefined'){payload['legalState']=legalState;} +if(typeof legalCity!=='undefined'){payload['legalCity']=legalCity;} +if(typeof legalAddress!=='undefined'){payload['legalAddress']=legalAddress;} +if(typeof legalTaxId!=='undefined'){payload['legalTaxId']=legalTaxId;} +const uri=new URL(this.config.endpoint+path);return yield this.call('patch',uri,{'content-type':'application/json',},payload);}),delete:(projectId,password)=>__awaiter(this,void 0,void 0,function*(){if(typeof projectId==='undefined'){throw new AppwriteException('Missing required parameter: "projectId"');} +if(typeof password==='undefined'){throw new AppwriteException('Missing required parameter: "password"');} +let path='/projects/{projectId}'.replace('{projectId}',projectId);let payload={};if(typeof password!=='undefined'){payload['password']=password;} +const uri=new URL(this.config.endpoint+path);return yield this.call('delete',uri,{'content-type':'application/json',},payload);}),updateAuthLimit:(projectId,limit)=>__awaiter(this,void 0,void 0,function*(){if(typeof projectId==='undefined'){throw new AppwriteException('Missing required parameter: "projectId"');} +if(typeof limit==='undefined'){throw new AppwriteException('Missing required parameter: "limit"');} +let path='/projects/{projectId}/auth/limit'.replace('{projectId}',projectId);let payload={};if(typeof limit!=='undefined'){payload['limit']=limit;} +const uri=new URL(this.config.endpoint+path);return yield this.call('patch',uri,{'content-type':'application/json',},payload);}),updateAuthStatus:(projectId,method,status)=>__awaiter(this,void 0,void 0,function*(){if(typeof projectId==='undefined'){throw new AppwriteException('Missing required parameter: "projectId"');} +if(typeof method==='undefined'){throw new AppwriteException('Missing required parameter: "method"');} +if(typeof status==='undefined'){throw new AppwriteException('Missing required parameter: "status"');} +let path='/projects/{projectId}/auth/{method}'.replace('{projectId}',projectId).replace('{method}',method);let payload={};if(typeof status!=='undefined'){payload['status']=status;} +const uri=new URL(this.config.endpoint+path);return yield this.call('patch',uri,{'content-type':'application/json',},payload);}),listDomains:(projectId)=>__awaiter(this,void 0,void 0,function*(){if(typeof projectId==='undefined'){throw new AppwriteException('Missing required parameter: "projectId"');} +let path='/projects/{projectId}/domains'.replace('{projectId}',projectId);let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);}),createDomain:(projectId,domain)=>__awaiter(this,void 0,void 0,function*(){if(typeof projectId==='undefined'){throw new AppwriteException('Missing required parameter: "projectId"');} +if(typeof domain==='undefined'){throw new AppwriteException('Missing required parameter: "domain"');} +let path='/projects/{projectId}/domains'.replace('{projectId}',projectId);let payload={};if(typeof domain!=='undefined'){payload['domain']=domain;} +const uri=new URL(this.config.endpoint+path);return yield this.call('post',uri,{'content-type':'application/json',},payload);}),getDomain:(projectId,domainId)=>__awaiter(this,void 0,void 0,function*(){if(typeof projectId==='undefined'){throw new AppwriteException('Missing required parameter: "projectId"');} +if(typeof domainId==='undefined'){throw new AppwriteException('Missing required parameter: "domainId"');} +let path='/projects/{projectId}/domains/{domainId}'.replace('{projectId}',projectId).replace('{domainId}',domainId);let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);}),deleteDomain:(projectId,domainId)=>__awaiter(this,void 0,void 0,function*(){if(typeof projectId==='undefined'){throw new AppwriteException('Missing required parameter: "projectId"');} +if(typeof domainId==='undefined'){throw new AppwriteException('Missing required parameter: "domainId"');} +let path='/projects/{projectId}/domains/{domainId}'.replace('{projectId}',projectId).replace('{domainId}',domainId);let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('delete',uri,{'content-type':'application/json',},payload);}),updateDomainVerification:(projectId,domainId)=>__awaiter(this,void 0,void 0,function*(){if(typeof projectId==='undefined'){throw new AppwriteException('Missing required parameter: "projectId"');} +if(typeof domainId==='undefined'){throw new AppwriteException('Missing required parameter: "domainId"');} +let path='/projects/{projectId}/domains/{domainId}/verification'.replace('{projectId}',projectId).replace('{domainId}',domainId);let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('patch',uri,{'content-type':'application/json',},payload);}),listKeys:(projectId)=>__awaiter(this,void 0,void 0,function*(){if(typeof projectId==='undefined'){throw new AppwriteException('Missing required parameter: "projectId"');} +let path='/projects/{projectId}/keys'.replace('{projectId}',projectId);let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);}),createKey:(projectId,name,scopes)=>__awaiter(this,void 0,void 0,function*(){if(typeof projectId==='undefined'){throw new AppwriteException('Missing required parameter: "projectId"');} +if(typeof name==='undefined'){throw new AppwriteException('Missing required parameter: "name"');} +if(typeof scopes==='undefined'){throw new AppwriteException('Missing required parameter: "scopes"');} +let path='/projects/{projectId}/keys'.replace('{projectId}',projectId);let payload={};if(typeof name!=='undefined'){payload['name']=name;} +if(typeof scopes!=='undefined'){payload['scopes']=scopes;} +const uri=new URL(this.config.endpoint+path);return yield this.call('post',uri,{'content-type':'application/json',},payload);}),getKey:(projectId,keyId)=>__awaiter(this,void 0,void 0,function*(){if(typeof projectId==='undefined'){throw new AppwriteException('Missing required parameter: "projectId"');} +if(typeof keyId==='undefined'){throw new AppwriteException('Missing required parameter: "keyId"');} +let path='/projects/{projectId}/keys/{keyId}'.replace('{projectId}',projectId).replace('{keyId}',keyId);let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);}),updateKey:(projectId,keyId,name,scopes)=>__awaiter(this,void 0,void 0,function*(){if(typeof projectId==='undefined'){throw new AppwriteException('Missing required parameter: "projectId"');} +if(typeof keyId==='undefined'){throw new AppwriteException('Missing required parameter: "keyId"');} +if(typeof name==='undefined'){throw new AppwriteException('Missing required parameter: "name"');} +if(typeof scopes==='undefined'){throw new AppwriteException('Missing required parameter: "scopes"');} +let path='/projects/{projectId}/keys/{keyId}'.replace('{projectId}',projectId).replace('{keyId}',keyId);let payload={};if(typeof name!=='undefined'){payload['name']=name;} +if(typeof scopes!=='undefined'){payload['scopes']=scopes;} +const uri=new URL(this.config.endpoint+path);return yield this.call('put',uri,{'content-type':'application/json',},payload);}),deleteKey:(projectId,keyId)=>__awaiter(this,void 0,void 0,function*(){if(typeof projectId==='undefined'){throw new AppwriteException('Missing required parameter: "projectId"');} +if(typeof keyId==='undefined'){throw new AppwriteException('Missing required parameter: "keyId"');} +let path='/projects/{projectId}/keys/{keyId}'.replace('{projectId}',projectId).replace('{keyId}',keyId);let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('delete',uri,{'content-type':'application/json',},payload);}),updateOAuth2:(projectId,provider,appId,secret)=>__awaiter(this,void 0,void 0,function*(){if(typeof projectId==='undefined'){throw new AppwriteException('Missing required parameter: "projectId"');} +if(typeof provider==='undefined'){throw new AppwriteException('Missing required parameter: "provider"');} +let path='/projects/{projectId}/oauth2'.replace('{projectId}',projectId);let payload={};if(typeof provider!=='undefined'){payload['provider']=provider;} +if(typeof appId!=='undefined'){payload['appId']=appId;} +if(typeof secret!=='undefined'){payload['secret']=secret;} +const uri=new URL(this.config.endpoint+path);return yield this.call('patch',uri,{'content-type':'application/json',},payload);}),listPlatforms:(projectId)=>__awaiter(this,void 0,void 0,function*(){if(typeof projectId==='undefined'){throw new AppwriteException('Missing required parameter: "projectId"');} +let path='/projects/{projectId}/platforms'.replace('{projectId}',projectId);let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);}),createPlatform:(projectId,type,name,key,store,hostname)=>__awaiter(this,void 0,void 0,function*(){if(typeof projectId==='undefined'){throw new AppwriteException('Missing required parameter: "projectId"');} +if(typeof type==='undefined'){throw new AppwriteException('Missing required parameter: "type"');} +if(typeof name==='undefined'){throw new AppwriteException('Missing required parameter: "name"');} +let path='/projects/{projectId}/platforms'.replace('{projectId}',projectId);let payload={};if(typeof type!=='undefined'){payload['type']=type;} +if(typeof name!=='undefined'){payload['name']=name;} +if(typeof key!=='undefined'){payload['key']=key;} +if(typeof store!=='undefined'){payload['store']=store;} +if(typeof hostname!=='undefined'){payload['hostname']=hostname;} +const uri=new URL(this.config.endpoint+path);return yield this.call('post',uri,{'content-type':'application/json',},payload);}),getPlatform:(projectId,platformId)=>__awaiter(this,void 0,void 0,function*(){if(typeof projectId==='undefined'){throw new AppwriteException('Missing required parameter: "projectId"');} +if(typeof platformId==='undefined'){throw new AppwriteException('Missing required parameter: "platformId"');} +let path='/projects/{projectId}/platforms/{platformId}'.replace('{projectId}',projectId).replace('{platformId}',platformId);let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);}),updatePlatform:(projectId,platformId,name,key,store,hostname)=>__awaiter(this,void 0,void 0,function*(){if(typeof projectId==='undefined'){throw new AppwriteException('Missing required parameter: "projectId"');} +if(typeof platformId==='undefined'){throw new AppwriteException('Missing required parameter: "platformId"');} +if(typeof name==='undefined'){throw new AppwriteException('Missing required parameter: "name"');} +let path='/projects/{projectId}/platforms/{platformId}'.replace('{projectId}',projectId).replace('{platformId}',platformId);let payload={};if(typeof name!=='undefined'){payload['name']=name;} +if(typeof key!=='undefined'){payload['key']=key;} +if(typeof store!=='undefined'){payload['store']=store;} +if(typeof hostname!=='undefined'){payload['hostname']=hostname;} +const uri=new URL(this.config.endpoint+path);return yield this.call('put',uri,{'content-type':'application/json',},payload);}),deletePlatform:(projectId,platformId)=>__awaiter(this,void 0,void 0,function*(){if(typeof projectId==='undefined'){throw new AppwriteException('Missing required parameter: "projectId"');} +if(typeof platformId==='undefined'){throw new AppwriteException('Missing required parameter: "platformId"');} +let path='/projects/{projectId}/platforms/{platformId}'.replace('{projectId}',projectId).replace('{platformId}',platformId);let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('delete',uri,{'content-type':'application/json',},payload);}),listTasks:(projectId)=>__awaiter(this,void 0,void 0,function*(){if(typeof projectId==='undefined'){throw new AppwriteException('Missing required parameter: "projectId"');} +let path='/projects/{projectId}/tasks'.replace('{projectId}',projectId);let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);}),createTask:(projectId,name,status,schedule,security,httpMethod,httpUrl,httpHeaders,httpUser,httpPass)=>__awaiter(this,void 0,void 0,function*(){if(typeof projectId==='undefined'){throw new AppwriteException('Missing required parameter: "projectId"');} +if(typeof name==='undefined'){throw new AppwriteException('Missing required parameter: "name"');} +if(typeof status==='undefined'){throw new AppwriteException('Missing required parameter: "status"');} +if(typeof schedule==='undefined'){throw new AppwriteException('Missing required parameter: "schedule"');} +if(typeof security==='undefined'){throw new AppwriteException('Missing required parameter: "security"');} +if(typeof httpMethod==='undefined'){throw new AppwriteException('Missing required parameter: "httpMethod"');} +if(typeof httpUrl==='undefined'){throw new AppwriteException('Missing required parameter: "httpUrl"');} +let path='/projects/{projectId}/tasks'.replace('{projectId}',projectId);let payload={};if(typeof name!=='undefined'){payload['name']=name;} +if(typeof status!=='undefined'){payload['status']=status;} +if(typeof schedule!=='undefined'){payload['schedule']=schedule;} +if(typeof security!=='undefined'){payload['security']=security;} +if(typeof httpMethod!=='undefined'){payload['httpMethod']=httpMethod;} +if(typeof httpUrl!=='undefined'){payload['httpUrl']=httpUrl;} +if(typeof httpHeaders!=='undefined'){payload['httpHeaders']=httpHeaders;} +if(typeof httpUser!=='undefined'){payload['httpUser']=httpUser;} +if(typeof httpPass!=='undefined'){payload['httpPass']=httpPass;} +const uri=new URL(this.config.endpoint+path);return yield this.call('post',uri,{'content-type':'application/json',},payload);}),getTask:(projectId,taskId)=>__awaiter(this,void 0,void 0,function*(){if(typeof projectId==='undefined'){throw new AppwriteException('Missing required parameter: "projectId"');} +if(typeof taskId==='undefined'){throw new AppwriteException('Missing required parameter: "taskId"');} +let path='/projects/{projectId}/tasks/{taskId}'.replace('{projectId}',projectId).replace('{taskId}',taskId);let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);}),updateTask:(projectId,taskId,name,status,schedule,security,httpMethod,httpUrl,httpHeaders,httpUser,httpPass)=>__awaiter(this,void 0,void 0,function*(){if(typeof projectId==='undefined'){throw new AppwriteException('Missing required parameter: "projectId"');} +if(typeof taskId==='undefined'){throw new AppwriteException('Missing required parameter: "taskId"');} +if(typeof name==='undefined'){throw new AppwriteException('Missing required parameter: "name"');} +if(typeof status==='undefined'){throw new AppwriteException('Missing required parameter: "status"');} +if(typeof schedule==='undefined'){throw new AppwriteException('Missing required parameter: "schedule"');} +if(typeof security==='undefined'){throw new AppwriteException('Missing required parameter: "security"');} +if(typeof httpMethod==='undefined'){throw new AppwriteException('Missing required parameter: "httpMethod"');} +if(typeof httpUrl==='undefined'){throw new AppwriteException('Missing required parameter: "httpUrl"');} +let path='/projects/{projectId}/tasks/{taskId}'.replace('{projectId}',projectId).replace('{taskId}',taskId);let payload={};if(typeof name!=='undefined'){payload['name']=name;} +if(typeof status!=='undefined'){payload['status']=status;} +if(typeof schedule!=='undefined'){payload['schedule']=schedule;} +if(typeof security!=='undefined'){payload['security']=security;} +if(typeof httpMethod!=='undefined'){payload['httpMethod']=httpMethod;} +if(typeof httpUrl!=='undefined'){payload['httpUrl']=httpUrl;} +if(typeof httpHeaders!=='undefined'){payload['httpHeaders']=httpHeaders;} +if(typeof httpUser!=='undefined'){payload['httpUser']=httpUser;} +if(typeof httpPass!=='undefined'){payload['httpPass']=httpPass;} +const uri=new URL(this.config.endpoint+path);return yield this.call('put',uri,{'content-type':'application/json',},payload);}),deleteTask:(projectId,taskId)=>__awaiter(this,void 0,void 0,function*(){if(typeof projectId==='undefined'){throw new AppwriteException('Missing required parameter: "projectId"');} +if(typeof taskId==='undefined'){throw new AppwriteException('Missing required parameter: "taskId"');} +let path='/projects/{projectId}/tasks/{taskId}'.replace('{projectId}',projectId).replace('{taskId}',taskId);let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('delete',uri,{'content-type':'application/json',},payload);}),getUsage:(projectId,range)=>__awaiter(this,void 0,void 0,function*(){if(typeof projectId==='undefined'){throw new AppwriteException('Missing required parameter: "projectId"');} +let path='/projects/{projectId}/usage'.replace('{projectId}',projectId);let payload={};if(typeof range!=='undefined'){payload['range']=range;} +const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);}),listWebhooks:(projectId)=>__awaiter(this,void 0,void 0,function*(){if(typeof projectId==='undefined'){throw new AppwriteException('Missing required parameter: "projectId"');} +let path='/projects/{projectId}/webhooks'.replace('{projectId}',projectId);let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);}),createWebhook:(projectId,name,events,url,security,httpUser,httpPass)=>__awaiter(this,void 0,void 0,function*(){if(typeof projectId==='undefined'){throw new AppwriteException('Missing required parameter: "projectId"');} +if(typeof name==='undefined'){throw new AppwriteException('Missing required parameter: "name"');} +if(typeof events==='undefined'){throw new AppwriteException('Missing required parameter: "events"');} +if(typeof url==='undefined'){throw new AppwriteException('Missing required parameter: "url"');} +if(typeof security==='undefined'){throw new AppwriteException('Missing required parameter: "security"');} +let path='/projects/{projectId}/webhooks'.replace('{projectId}',projectId);let payload={};if(typeof name!=='undefined'){payload['name']=name;} +if(typeof events!=='undefined'){payload['events']=events;} +if(typeof url!=='undefined'){payload['url']=url;} +if(typeof security!=='undefined'){payload['security']=security;} +if(typeof httpUser!=='undefined'){payload['httpUser']=httpUser;} +if(typeof httpPass!=='undefined'){payload['httpPass']=httpPass;} +const uri=new URL(this.config.endpoint+path);return yield this.call('post',uri,{'content-type':'application/json',},payload);}),getWebhook:(projectId,webhookId)=>__awaiter(this,void 0,void 0,function*(){if(typeof projectId==='undefined'){throw new AppwriteException('Missing required parameter: "projectId"');} +if(typeof webhookId==='undefined'){throw new AppwriteException('Missing required parameter: "webhookId"');} +let path='/projects/{projectId}/webhooks/{webhookId}'.replace('{projectId}',projectId).replace('{webhookId}',webhookId);let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);}),updateWebhook:(projectId,webhookId,name,events,url,security,httpUser,httpPass)=>__awaiter(this,void 0,void 0,function*(){if(typeof projectId==='undefined'){throw new AppwriteException('Missing required parameter: "projectId"');} +if(typeof webhookId==='undefined'){throw new AppwriteException('Missing required parameter: "webhookId"');} +if(typeof name==='undefined'){throw new AppwriteException('Missing required parameter: "name"');} +if(typeof events==='undefined'){throw new AppwriteException('Missing required parameter: "events"');} +if(typeof url==='undefined'){throw new AppwriteException('Missing required parameter: "url"');} +if(typeof security==='undefined'){throw new AppwriteException('Missing required parameter: "security"');} +let path='/projects/{projectId}/webhooks/{webhookId}'.replace('{projectId}',projectId).replace('{webhookId}',webhookId);let payload={};if(typeof name!=='undefined'){payload['name']=name;} +if(typeof events!=='undefined'){payload['events']=events;} +if(typeof url!=='undefined'){payload['url']=url;} +if(typeof security!=='undefined'){payload['security']=security;} +if(typeof httpUser!=='undefined'){payload['httpUser']=httpUser;} +if(typeof httpPass!=='undefined'){payload['httpPass']=httpPass;} +const uri=new URL(this.config.endpoint+path);return yield this.call('put',uri,{'content-type':'application/json',},payload);}),deleteWebhook:(projectId,webhookId)=>__awaiter(this,void 0,void 0,function*(){if(typeof projectId==='undefined'){throw new AppwriteException('Missing required parameter: "projectId"');} +if(typeof webhookId==='undefined'){throw new AppwriteException('Missing required parameter: "webhookId"');} +let path='/projects/{projectId}/webhooks/{webhookId}'.replace('{projectId}',projectId).replace('{webhookId}',webhookId);let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('delete',uri,{'content-type':'application/json',},payload);})};this.storage={listFiles:(search,limit,offset,orderType)=>__awaiter(this,void 0,void 0,function*(){let path='/storage/files';let payload={};if(typeof search!=='undefined'){payload['search']=search;} +if(typeof limit!=='undefined'){payload['limit']=limit;} +if(typeof offset!=='undefined'){payload['offset']=offset;} +if(typeof orderType!=='undefined'){payload['orderType']=orderType;} +const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);}),createFile:(file,read,write)=>__awaiter(this,void 0,void 0,function*(){if(typeof file==='undefined'){throw new AppwriteException('Missing required parameter: "file"');} +let path='/storage/files';let payload={};if(typeof file!=='undefined'){payload['file']=file;} +if(typeof read!=='undefined'){payload['read']=read;} +if(typeof write!=='undefined'){payload['write']=write;} +const uri=new URL(this.config.endpoint+path);return yield this.call('post',uri,{'content-type':'multipart/form-data',},payload);}),getFile:(fileId)=>__awaiter(this,void 0,void 0,function*(){if(typeof fileId==='undefined'){throw new AppwriteException('Missing required parameter: "fileId"');} +let path='/storage/files/{fileId}'.replace('{fileId}',fileId);let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);}),updateFile:(fileId,read,write)=>__awaiter(this,void 0,void 0,function*(){if(typeof fileId==='undefined'){throw new AppwriteException('Missing required parameter: "fileId"');} +if(typeof read==='undefined'){throw new AppwriteException('Missing required parameter: "read"');} +if(typeof write==='undefined'){throw new AppwriteException('Missing required parameter: "write"');} +let path='/storage/files/{fileId}'.replace('{fileId}',fileId);let payload={};if(typeof read!=='undefined'){payload['read']=read;} +if(typeof write!=='undefined'){payload['write']=write;} +const uri=new URL(this.config.endpoint+path);return yield this.call('put',uri,{'content-type':'application/json',},payload);}),deleteFile:(fileId)=>__awaiter(this,void 0,void 0,function*(){if(typeof fileId==='undefined'){throw new AppwriteException('Missing required parameter: "fileId"');} +let path='/storage/files/{fileId}'.replace('{fileId}',fileId);let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('delete',uri,{'content-type':'application/json',},payload);}),getFileDownload:(fileId)=>{if(typeof fileId==='undefined'){throw new AppwriteException('Missing required parameter: "fileId"');} +let path='/storage/files/{fileId}/download'.replace('{fileId}',fileId);let payload={};const uri=new URL(this.config.endpoint+path);payload['project']=this.config.project;for(const[key,value]of Object.entries(this.flatten(payload))){uri.searchParams.append(key,value);} +return uri;},getFilePreview:(fileId,width,height,quality,borderWidth,borderColor,borderRadius,opacity,rotation,background,output)=>{if(typeof fileId==='undefined'){throw new AppwriteException('Missing required parameter: "fileId"');} +let path='/storage/files/{fileId}/preview'.replace('{fileId}',fileId);let payload={};if(typeof width!=='undefined'){payload['width']=width;} +if(typeof height!=='undefined'){payload['height']=height;} +if(typeof quality!=='undefined'){payload['quality']=quality;} +if(typeof borderWidth!=='undefined'){payload['borderWidth']=borderWidth;} +if(typeof borderColor!=='undefined'){payload['borderColor']=borderColor;} +if(typeof borderRadius!=='undefined'){payload['borderRadius']=borderRadius;} +if(typeof opacity!=='undefined'){payload['opacity']=opacity;} +if(typeof rotation!=='undefined'){payload['rotation']=rotation;} +if(typeof background!=='undefined'){payload['background']=background;} +if(typeof output!=='undefined'){payload['output']=output;} +const uri=new URL(this.config.endpoint+path);payload['project']=this.config.project;for(const[key,value]of Object.entries(this.flatten(payload))){uri.searchParams.append(key,value);} +return uri;},getFileView:(fileId)=>{if(typeof fileId==='undefined'){throw new AppwriteException('Missing required parameter: "fileId"');} +let path='/storage/files/{fileId}/view'.replace('{fileId}',fileId);let payload={};const uri=new URL(this.config.endpoint+path);payload['project']=this.config.project;for(const[key,value]of Object.entries(this.flatten(payload))){uri.searchParams.append(key,value);} +return uri;}};this.teams={list:(search,limit,offset,orderType)=>__awaiter(this,void 0,void 0,function*(){let path='/teams';let payload={};if(typeof search!=='undefined'){payload['search']=search;} +if(typeof limit!=='undefined'){payload['limit']=limit;} +if(typeof offset!=='undefined'){payload['offset']=offset;} +if(typeof orderType!=='undefined'){payload['orderType']=orderType;} +const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);}),create:(name,roles)=>__awaiter(this,void 0,void 0,function*(){if(typeof name==='undefined'){throw new AppwriteException('Missing required parameter: "name"');} +let path='/teams';let payload={};if(typeof name!=='undefined'){payload['name']=name;} +if(typeof roles!=='undefined'){payload['roles']=roles;} +const uri=new URL(this.config.endpoint+path);return yield this.call('post',uri,{'content-type':'application/json',},payload);}),get:(teamId)=>__awaiter(this,void 0,void 0,function*(){if(typeof teamId==='undefined'){throw new AppwriteException('Missing required parameter: "teamId"');} +let path='/teams/{teamId}'.replace('{teamId}',teamId);let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);}),update:(teamId,name)=>__awaiter(this,void 0,void 0,function*(){if(typeof teamId==='undefined'){throw new AppwriteException('Missing required parameter: "teamId"');} +if(typeof name==='undefined'){throw new AppwriteException('Missing required parameter: "name"');} +let path='/teams/{teamId}'.replace('{teamId}',teamId);let payload={};if(typeof name!=='undefined'){payload['name']=name;} +const uri=new URL(this.config.endpoint+path);return yield this.call('put',uri,{'content-type':'application/json',},payload);}),delete:(teamId)=>__awaiter(this,void 0,void 0,function*(){if(typeof teamId==='undefined'){throw new AppwriteException('Missing required parameter: "teamId"');} +let path='/teams/{teamId}'.replace('{teamId}',teamId);let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('delete',uri,{'content-type':'application/json',},payload);}),getMemberships:(teamId,search,limit,offset,orderType)=>__awaiter(this,void 0,void 0,function*(){if(typeof teamId==='undefined'){throw new AppwriteException('Missing required parameter: "teamId"');} +let path='/teams/{teamId}/memberships'.replace('{teamId}',teamId);let payload={};if(typeof search!=='undefined'){payload['search']=search;} +if(typeof limit!=='undefined'){payload['limit']=limit;} +if(typeof offset!=='undefined'){payload['offset']=offset;} +if(typeof orderType!=='undefined'){payload['orderType']=orderType;} +const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);}),createMembership:(teamId,email,roles,url,name)=>__awaiter(this,void 0,void 0,function*(){if(typeof teamId==='undefined'){throw new AppwriteException('Missing required parameter: "teamId"');} +if(typeof email==='undefined'){throw new AppwriteException('Missing required parameter: "email"');} +if(typeof roles==='undefined'){throw new AppwriteException('Missing required parameter: "roles"');} +if(typeof url==='undefined'){throw new AppwriteException('Missing required parameter: "url"');} +let path='/teams/{teamId}/memberships'.replace('{teamId}',teamId);let payload={};if(typeof email!=='undefined'){payload['email']=email;} +if(typeof name!=='undefined'){payload['name']=name;} +if(typeof roles!=='undefined'){payload['roles']=roles;} +if(typeof url!=='undefined'){payload['url']=url;} +const uri=new URL(this.config.endpoint+path);return yield this.call('post',uri,{'content-type':'application/json',},payload);}),updateMembershipRoles:(teamId,membershipId,roles)=>__awaiter(this,void 0,void 0,function*(){if(typeof teamId==='undefined'){throw new AppwriteException('Missing required parameter: "teamId"');} +if(typeof membershipId==='undefined'){throw new AppwriteException('Missing required parameter: "membershipId"');} +if(typeof roles==='undefined'){throw new AppwriteException('Missing required parameter: "roles"');} +let path='/teams/{teamId}/memberships/{membershipId}'.replace('{teamId}',teamId).replace('{membershipId}',membershipId);let payload={};if(typeof roles!=='undefined'){payload['roles']=roles;} +const uri=new URL(this.config.endpoint+path);return yield this.call('patch',uri,{'content-type':'application/json',},payload);}),deleteMembership:(teamId,membershipId)=>__awaiter(this,void 0,void 0,function*(){if(typeof teamId==='undefined'){throw new AppwriteException('Missing required parameter: "teamId"');} +if(typeof membershipId==='undefined'){throw new AppwriteException('Missing required parameter: "membershipId"');} +let path='/teams/{teamId}/memberships/{membershipId}'.replace('{teamId}',teamId).replace('{membershipId}',membershipId);let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('delete',uri,{'content-type':'application/json',},payload);}),updateMembershipStatus:(teamId,membershipId,userId,secret)=>__awaiter(this,void 0,void 0,function*(){if(typeof teamId==='undefined'){throw new AppwriteException('Missing required parameter: "teamId"');} +if(typeof membershipId==='undefined'){throw new AppwriteException('Missing required parameter: "membershipId"');} +if(typeof userId==='undefined'){throw new AppwriteException('Missing required parameter: "userId"');} +if(typeof secret==='undefined'){throw new AppwriteException('Missing required parameter: "secret"');} +let path='/teams/{teamId}/memberships/{membershipId}/status'.replace('{teamId}',teamId).replace('{membershipId}',membershipId);let payload={};if(typeof userId!=='undefined'){payload['userId']=userId;} +if(typeof secret!=='undefined'){payload['secret']=secret;} +const uri=new URL(this.config.endpoint+path);return yield this.call('patch',uri,{'content-type':'application/json',},payload);})};this.users={list:(search,limit,offset,orderType)=>__awaiter(this,void 0,void 0,function*(){let path='/users';let payload={};if(typeof search!=='undefined'){payload['search']=search;} +if(typeof limit!=='undefined'){payload['limit']=limit;} +if(typeof offset!=='undefined'){payload['offset']=offset;} +if(typeof orderType!=='undefined'){payload['orderType']=orderType;} +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='/users';let payload={};if(typeof email!=='undefined'){payload['email']=email;} +if(typeof password!=='undefined'){payload['password']=password;} +if(typeof name!=='undefined'){payload['name']=name;} +const uri=new URL(this.config.endpoint+path);return yield this.call('post',uri,{'content-type':'application/json',},payload);}),get:(userId)=>__awaiter(this,void 0,void 0,function*(){if(typeof userId==='undefined'){throw new AppwriteException('Missing required parameter: "userId"');} +let path='/users/{userId}'.replace('{userId}',userId);let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);}),delete:(userId)=>__awaiter(this,void 0,void 0,function*(){if(typeof userId==='undefined'){throw new AppwriteException('Missing required parameter: "userId"');} +let path='/users/{userId}'.replace('{userId}',userId);let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('delete',uri,{'content-type':'application/json',},payload);}),getLogs:(userId)=>__awaiter(this,void 0,void 0,function*(){if(typeof userId==='undefined'){throw new AppwriteException('Missing required parameter: "userId"');} +let path='/users/{userId}/logs'.replace('{userId}',userId);let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);}),getPrefs:(userId)=>__awaiter(this,void 0,void 0,function*(){if(typeof userId==='undefined'){throw new AppwriteException('Missing required parameter: "userId"');} +let path='/users/{userId}/prefs'.replace('{userId}',userId);let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);}),updatePrefs:(userId,prefs)=>__awaiter(this,void 0,void 0,function*(){if(typeof userId==='undefined'){throw new AppwriteException('Missing required parameter: "userId"');} +if(typeof prefs==='undefined'){throw new AppwriteException('Missing required parameter: "prefs"');} +let path='/users/{userId}/prefs'.replace('{userId}',userId);let payload={};if(typeof prefs!=='undefined'){payload['prefs']=prefs;} +const uri=new URL(this.config.endpoint+path);return yield this.call('patch',uri,{'content-type':'application/json',},payload);}),getSessions:(userId)=>__awaiter(this,void 0,void 0,function*(){if(typeof userId==='undefined'){throw new AppwriteException('Missing required parameter: "userId"');} +let path='/users/{userId}/sessions'.replace('{userId}',userId);let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);}),deleteSessions:(userId)=>__awaiter(this,void 0,void 0,function*(){if(typeof userId==='undefined'){throw new AppwriteException('Missing required parameter: "userId"');} +let path='/users/{userId}/sessions'.replace('{userId}',userId);let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('delete',uri,{'content-type':'application/json',},payload);}),deleteSession:(userId,sessionId)=>__awaiter(this,void 0,void 0,function*(){if(typeof userId==='undefined'){throw new AppwriteException('Missing required parameter: "userId"');} +if(typeof sessionId==='undefined'){throw new AppwriteException('Missing required parameter: "sessionId"');} +let path='/users/{userId}/sessions/{sessionId}'.replace('{userId}',userId).replace('{sessionId}',sessionId);let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('delete',uri,{'content-type':'application/json',},payload);}),updateStatus:(userId,status)=>__awaiter(this,void 0,void 0,function*(){if(typeof userId==='undefined'){throw new AppwriteException('Missing required parameter: "userId"');} +if(typeof status==='undefined'){throw new AppwriteException('Missing required parameter: "status"');} +let path='/users/{userId}/status'.replace('{userId}',userId);let payload={};if(typeof status!=='undefined'){payload['status']=status;} +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;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(','));} +else{formData.append(key,params[key]);}} +options.body=formData;delete headers['content-type'];break;}} +try{let data=null;const response=yield crossFetch.fetch(url.toString(),options);if((_b=response.headers.get("content-type"))===null||_b===void 0?void 0:_b.includes("application/json")){data=yield response.json();} +else{data={message:yield response.text()};} +if(400<=response.status){throw new AppwriteException(data===null||data===void 0?void 0:data.message,response.status,data);} +const cookieFallback=response.headers.get('X-Fallback-Cookies');if(typeof window!=='undefined'&&window.localStorage&&cookieFallback){window.console.warn('Appwrite is using localStorage for session management. Increase your security by adding a custom domain as your API endpoint.');window.localStorage.setItem('cookieFallback',cookieFallback);} +return data;} +catch(e){throw new AppwriteException(e.message);}});} +flatten(data,prefix=''){let output={};for(const key in data){let value=data[key];let finalKey=prefix?`${prefix}[${key}]`:key;if(Array.isArray(value)){output=Object.assign(output,this.flatten(value,finalKey));} +else{output[finalKey]=value;}} +return output;}} +exports.Appwrite=Appwrite;Object.defineProperty(exports,'__esModule',{value:true});}(this.window=this.window||{},null,window));(function(global,factory){typeof exports==='object'&&typeof module!=='undefined'?module.exports=factory(function(){try{return require('moment');}catch(e){}}()):typeof define==='function'&&define.amd?define(['require'],function(require){return factory(function(){try{return require('moment');}catch(e){}}());}):(global=global||self,global.Chart=factory(global.moment));}(this,(function(moment){'use strict';moment=moment&&moment.hasOwnProperty('default')?moment['default']:moment;function createCommonjsModule(fn,module){return module={exports:{}},fn(module,module.exports),module.exports;} function getCjsExportFromNamespace(n){return n&&n['default']||n;} var colorName={"aliceblue":[240,248,255],"antiquewhite":[250,235,215],"aqua":[0,255,255],"aquamarine":[127,255,212],"azure":[240,255,255],"beige":[245,245,220],"bisque":[255,228,196],"black":[0,0,0],"blanchedalmond":[255,235,205],"blue":[0,0,255],"blueviolet":[138,43,226],"brown":[165,42,42],"burlywood":[222,184,135],"cadetblue":[95,158,160],"chartreuse":[127,255,0],"chocolate":[210,105,30],"coral":[255,127,80],"cornflowerblue":[100,149,237],"cornsilk":[255,248,220],"crimson":[220,20,60],"cyan":[0,255,255],"darkblue":[0,0,139],"darkcyan":[0,139,139],"darkgoldenrod":[184,134,11],"darkgray":[169,169,169],"darkgreen":[0,100,0],"darkgrey":[169,169,169],"darkkhaki":[189,183,107],"darkmagenta":[139,0,139],"darkolivegreen":[85,107,47],"darkorange":[255,140,0],"darkorchid":[153,50,204],"darkred":[139,0,0],"darksalmon":[233,150,122],"darkseagreen":[143,188,143],"darkslateblue":[72,61,139],"darkslategray":[47,79,79],"darkslategrey":[47,79,79],"darkturquoise":[0,206,209],"darkviolet":[148,0,211],"deeppink":[255,20,147],"deepskyblue":[0,191,255],"dimgray":[105,105,105],"dimgrey":[105,105,105],"dodgerblue":[30,144,255],"firebrick":[178,34,34],"floralwhite":[255,250,240],"forestgreen":[34,139,34],"fuchsia":[255,0,255],"gainsboro":[220,220,220],"ghostwhite":[248,248,255],"gold":[255,215,0],"goldenrod":[218,165,32],"gray":[128,128,128],"green":[0,128,0],"greenyellow":[173,255,47],"grey":[128,128,128],"honeydew":[240,255,240],"hotpink":[255,105,180],"indianred":[205,92,92],"indigo":[75,0,130],"ivory":[255,255,240],"khaki":[240,230,140],"lavender":[230,230,250],"lavenderblush":[255,240,245],"lawngreen":[124,252,0],"lemonchiffon":[255,250,205],"lightblue":[173,216,230],"lightcoral":[240,128,128],"lightcyan":[224,255,255],"lightgoldenrodyellow":[250,250,210],"lightgray":[211,211,211],"lightgreen":[144,238,144],"lightgrey":[211,211,211],"lightpink":[255,182,193],"lightsalmon":[255,160,122],"lightseagreen":[32,178,170],"lightskyblue":[135,206,250],"lightslategray":[119,136,153],"lightslategrey":[119,136,153],"lightsteelblue":[176,196,222],"lightyellow":[255,255,224],"lime":[0,255,0],"limegreen":[50,205,50],"linen":[250,240,230],"magenta":[255,0,255],"maroon":[128,0,0],"mediumaquamarine":[102,205,170],"mediumblue":[0,0,205],"mediumorchid":[186,85,211],"mediumpurple":[147,112,219],"mediumseagreen":[60,179,113],"mediumslateblue":[123,104,238],"mediumspringgreen":[0,250,154],"mediumturquoise":[72,209,204],"mediumvioletred":[199,21,133],"midnightblue":[25,25,112],"mintcream":[245,255,250],"mistyrose":[255,228,225],"moccasin":[255,228,181],"navajowhite":[255,222,173],"navy":[0,0,128],"oldlace":[253,245,230],"olive":[128,128,0],"olivedrab":[107,142,35],"orange":[255,165,0],"orangered":[255,69,0],"orchid":[218,112,214],"palegoldenrod":[238,232,170],"palegreen":[152,251,152],"paleturquoise":[175,238,238],"palevioletred":[219,112,147],"papayawhip":[255,239,213],"peachpuff":[255,218,185],"peru":[205,133,63],"pink":[255,192,203],"plum":[221,160,221],"powderblue":[176,224,230],"purple":[128,0,128],"rebeccapurple":[102,51,153],"red":[255,0,0],"rosybrown":[188,143,143],"royalblue":[65,105,225],"saddlebrown":[139,69,19],"salmon":[250,128,114],"sandybrown":[244,164,96],"seagreen":[46,139,87],"seashell":[255,245,238],"sienna":[160,82,45],"silver":[192,192,192],"skyblue":[135,206,235],"slateblue":[106,90,205],"slategray":[112,128,144],"slategrey":[112,128,144],"snow":[255,250,250],"springgreen":[0,255,127],"steelblue":[70,130,180],"tan":[210,180,140],"teal":[0,128,128],"thistle":[216,191,216],"tomato":[255,99,71],"turquoise":[64,224,208],"violet":[238,130,238],"wheat":[245,222,179],"white":[255,255,255],"whitesmoke":[245,245,245],"yellow":[255,255,0],"yellowgreen":[154,205,50]};var conversions=createCommonjsModule(function(module){var reverseKeywords={};for(var key in colorName){if(colorName.hasOwnProperty(key)){reverseKeywords[colorName[key]]=key;}} var convert=module.exports={rgb:{channels:3,labels:'rgb'},hsl:{channels:3,labels:'hsl'},hsv:{channels:3,labels:'hsv'},hwb:{channels:3,labels:'hwb'},cmyk:{channels:4,labels:'cmyk'},xyz:{channels:3,labels:'xyz'},lab:{channels:3,labels:'lab'},lch:{channels:3,labels:'lch'},hex:{channels:1,labels:['hex']},keyword:{channels:1,labels:['keyword']},ansi16:{channels:1,labels:['ansi16']},ansi256:{channels:1,labels:['ansi256']},hcg:{channels:3,labels:['h','c','g']},apple:{channels:3,labels:['r16','g16','b16']},gray:{channels:1,labels:['gray']}};for(var model in convert){if(convert.hasOwnProperty(model)){if(!('channels'in convert[model])){throw new Error('missing channels property: '+model);} diff --git a/public/dist/scripts/app.js b/public/dist/scripts/app.js index 079dc0868e..d1c455bd1c 100644 --- a/public/dist/scripts/app.js +++ b/public/dist/scripts/app.js @@ -137,7 +137,7 @@ element.dispatchEvent(new Event('looped'));};let template=(element.children.leng else{if(debug){console.error('Missing template "'+source+'"');}} if(!init){view.render(element);} return;} -http.get(source).then(function(element){return function(data){element.innerHTML=data;view.render(element);element.dispatchEvent(new CustomEvent('template-loaded',{bubbles:true,cancelable:false}));}}(element),function(){throw new Error('Failed loading template');});};check(true);for(let i=0;iscope.max){scope.list.pop();scope.counter--;} +http.get(source).then(function(element){return function(data){element.innerHTML=data;view.render(element);element.dispatchEvent(new CustomEvent('template-loaded',{bubbles:true,cancelable:false}));}}(element),function(){throw new Error('Failed loading template');});};check(true);for(let i=0;i{const realtime=window.ls.container.get('realtime');window.ls.container.get('console').subscribe('project',event=>{realtime.set(event.payload);});});(function(window){"use strict";window.ls.container.set('alerts',function(window){return{list:[],ids:0,counter:0,max:5,add:function(message,time){var scope=this;message.id=scope.ids++;message.remove=function(){scope.remove(message.id);};scope.counter++;scope.list.unshift(message);if(scope.counter>scope.max){scope.list.pop();scope.counter--;} if(time>0){window.setTimeout(function(message){return function(){scope.remove(message.id)}}(message),time);} return message.id;},remove:function(id){let scope=this;for(let index=0;index0)?'?'+buildQuery(params):''),headers,{});},'post':function(path,headers={},params={},progress=null){return call('POST',path,headers,params,progress);},'put':function(path,headers={},params={},progress=null){return call('PUT',headers,params,progress);},'patch':function(path,headers={},params={},progress=null){return call('PATCH',path,headers,params,progress);},'delete':function(path,headers={},params={},progress=null){return call('DELETE',path,headers,params,progress);},'addGlobalParam':addGlobalParam,'addGlobalHeader':addGlobalHeader}}(window.document);let analytics={create:function(id,source,activity,url){return http.post('/analytics',{'content-type':'application/json'},{id:id,source:source,activity:activity,url:url,version:env.VERSION,setup:env.SETUP});},};return{analytics:analytics,};},true);})(window);(function(window){"use strict";window.ls.container.set('console',function(window){var sdk=new window.Appwrite();sdk.setEndpoint(APP_ENV.API).setProject('console').setLocale(APP_ENV.LOCALE);return sdk;},true);})(window);(function(window){"use strict";window.ls.container.set('date',function(){function format(format,timestamp){var jsdate,f +request.onerror=function(){reject(new Error("Network Error"));};request.send(params);})};return{'get':function(path,headers={},params={}){return call('GET',path+((params.length>0)?'?'+buildQuery(params):''),headers,{});},'post':function(path,headers={},params={},progress=null){return call('POST',path,headers,params,progress);},'put':function(path,headers={},params={},progress=null){return call('PUT',headers,params,progress);},'patch':function(path,headers={},params={},progress=null){return call('PATCH',path,headers,params,progress);},'delete':function(path,headers={},params={},progress=null){return call('DELETE',path,headers,params,progress);},'addGlobalParam':addGlobalParam,'addGlobalHeader':addGlobalHeader}}(window.document);let analytics={create:function(id,source,activity,url){return http.post('/analytics',{'content-type':'application/json'},{id:id,source:source,activity:activity,url:url,version:env.VERSION,setup:env.SETUP});},};return{analytics:analytics,};},true);})(window);(function(window){"use strict";window.ls.container.set('console',function(window){var sdk=new window.Appwrite();sdk.setEndpoint(window.location.protocol+'//'+window.location.host+APP_ENV.API).setProject('console').setLocale(APP_ENV.LOCALE);return sdk;},true);})(window);(function(window){"use strict";window.ls.container.set('date',function(){function format(format,timestamp){var jsdate,f var txtWords=['Sun','Mon','Tues','Wednes','Thurs','Fri','Satur','January','February','March','April','May','June','July','August','September','October','November','December'] var formatChr=/\\?(.?)/gi var formatChrCb=function(t,s){return f[t]?f[t]():s} @@ -281,7 +281,7 @@ return slf.renderToken(tokens,idx,opts);} md.renderer.rules.strong_open=renderEm;md.renderer.rules.strong_close=renderEm;return md;},true);})(window);(function(window){"use strict";window.ls.container.set('rtl',function(){var rtlStock="^ا^ب^ت^ث^ج^ح^خ^د^ذ^ر^ز^س^ش^ص^ض^ط^ظ^ع^غ^ف^ق^ك^ل^م^ن^ه^و^ي^א^ב^ג^ד^ה^ו^ז^ח^ט^י^כ^ך^ל^מ^ם^נ^ן^ס^ע^פ^ף^צ^ץ^ק^ר^ש^ת^";var special=["\n"," "," ","״",'"',"_","'","!","@","#","$","^","&","%","*","(",")","+","=","-","[","]","\\","/","{","}","|",":","<",">","?",",",".","0","1","2","3","4","5","6","7","8","9"];var isRTL=function(value){for(var i=0;i{return{current:false,set:function(currentConnections){var scope=this;scope.current=currentConnections;return scope.current;}};},true,true);})(window);window.ls.router.add("/auth/signin",{template:"/auth/signin?version="+APP_ENV.CACHEBUSTER,scope:"home"}).add("/auth/signup",{template:"/auth/signup?version="+APP_ENV.CACHEBUSTER,scope:"home"}).add("/auth/recovery",{template:"/auth/recovery?version="+APP_ENV.CACHEBUSTER,scope:"home"}).add("/auth/recovery/reset",{template:"/auth/recovery/reset?version="+APP_ENV.CACHEBUSTER,scope:"home"}).add("/auth/confirm",{template:"/auth/confirm?version="+APP_ENV.CACHEBUSTER,scope:"home"}).add("/auth/join",{template:"/auth/join?version="+APP_ENV.CACHEBUSTER,scope:"home"}).add("/auth/oauth2/success",{template:"/auth/oauth2/success?version="+APP_ENV.CACHEBUSTER,scope:"home"}).add("/auth/oauth2/failure",{template:"/auth/oauth2/failure?version="+APP_ENV.CACHEBUSTER,scope:"home"}).add("/console",{template:"/console?version="+APP_ENV.CACHEBUSTER,scope:"console"}).add("/console/account",{template:"/console/account?version="+APP_ENV.CACHEBUSTER,scope:"console"}).add("/console/account/:tab",{template:"/console/account?version="+APP_ENV.CACHEBUSTER,scope:"console"}).add("/console/home",{template:"/console/home?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/home/:tab",{template:"/console/home?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/platforms/:platform",{template:function(window){return window.location.pathname+"?version="+APP_ENV.CACHEBUSTER;},scope:"console",project:true}).add("/console/notifications",{template:"/console/notifications?version="+APP_ENV.CACHEBUSTER,scope:"console"}).add("/console/settings",{template:"/console/settings?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/settings/:tab",{template:"/console/settings?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/webhooks",{template:"/console/webhooks?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/webhooks/:tab",{template:"/console/webhooks?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/keys",{template:"/console/keys?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/keys/:tab",{template:"/console/keys?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/tasks",{template:"/console/tasks?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/tasks/:tab",{template:"/console/tasks?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/database",{template:"/console/database?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/database/collection",{template:function(window){return window.location.pathname+window.location.search+'&version='+APP_ENV.CACHEBUSTER;},scope:"console",project:true}).add("/console/database/collection/:tab",{template:function(window){return window.location.pathname+window.location.search+'&version='+APP_ENV.CACHEBUSTER;},scope:"console",project:true}).add("/console/database/document",{template:function(window){return window.location.pathname+window.location.search+'&version='+APP_ENV.CACHEBUSTER;},scope:"console",project:true}).add("/console/database/document/:tab",{template:function(window){return window.location.pathname+window.location.search+'&version='+APP_ENV.CACHEBUSTER;},scope:"console",project:true}).add("/console/storage",{template:"/console/storage?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/storage/:tab",{template:"/console/storage?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/users",{template:"/console/users?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/users/user",{template:"/console/users/user?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/users/user/:tab",{template:"/console/users/user?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/users/teams/team",{template:"/console/users/teams/team?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/users/teams/team/:tab",{template:"/console/users/teams/team?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/users/:tab",{template:"/console/users?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/functions",{template:"/console/functions?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/functions/function",{template:"/console/functions/function?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/functions/function/:tab",{template:"/console/functions/function?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/functions/:tab",{template:"/console/functions?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true});window.ls.filter.add("avatar",function($value,element){if(!$value){return"";} let size=element.dataset["size"]||80;let name=$value.name||$value||"";name=(typeof name!=='string')?'--':name;return def="/v1/avatars/initials?project=console"+"&name="+ encodeURIComponent(name)+"&width="+ size+"&height="+ @@ -309,7 +309,7 @@ return $value;}).add("platformsLimit",function($value){return $value;}).add("lim return $value.join(", ").replace(/,\s([^,]+)$/,' and $1');}).add("envName",function($value,env){if(env&&env.RUNTIMES&&env.RUNTIMES[$value]){return env.RUNTIMES[$value].name;} return'';}).add("envLogo",function($value,env){if(env&&env.RUNTIMES&&env.RUNTIMES[$value]){return env.RUNTIMES[$value].logo;} return'';}).add("envVersion",function($value,env){if(env&&env.RUNTIMES&&env.RUNTIMES[$value]){return env.RUNTIMES[$value].version;} -return'';});function abbreviate(number,maxPlaces,forcePlaces,forceLetter){number=Number(number);forceLetter=forceLetter||false;if(forceLetter!==false){return annotate(number,maxPlaces,forcePlaces,forceLetter);} +return'';}).add("accessProject",function($value,router){return $value.hasOwnProperty(router.params.project)?$value[router.params.project]:0;});function abbreviate(number,maxPlaces,forcePlaces,forceLetter){number=Number(number);forceLetter=forceLetter||false;if(forceLetter!==false){return annotate(number,maxPlaces,forcePlaces,forceLetter);} let abbr;if(number>=1e12){abbr="T";}else if(number>=1e9){abbr="B";}else if(number>=1e6){abbr="M";}else if(number>=1e3){abbr="K";}else{abbr="";} return annotate(number,maxPlaces,forcePlaces,abbr);} function annotate(number,maxPlaces,forcePlaces,abbr){let rounded=0;switch(abbr){case"T":rounded=number/1e12;break;case"B":rounded=number/1e9;break;case"M":rounded=number/1e6;break;case"K":rounded=number/1e3;break;case"":rounded=number;break;} @@ -331,7 +331,7 @@ let attrKey=prefix+key.charAt(0).toUpperCase()+key.slice(1);if(element.dataset[a if(typeof data[key]!=='undefined'){result=data[key];} if(typeof result==='undefined'){result="";} return result;};let resolve=function(target,prefix="param",data={}){if(!target){return function(){};} -let args=getParams(target);return target.apply(target,args.map(function(value){let result=getValue(value,prefix,data);return result;}));};let exec=function(event){let parsedSuccess=expression.parse(success);let parsedFailure=expression.parse(failure);let parsedAction=expression.parse(action);parsedSuccess=parsedSuccess&&parsedSuccess!=""?parsedSuccess.split(",").map(element=>element.trim()):[];parsedFailure=parsedFailure&&parsedFailure!=""?parsedFailure.split(",").map(element=>element.trim()):[];element.$lsSkip=true;element.classList.add("load-service-start");if(!document.body.contains(element)){element=undefined;return false;} +let args=getParams(target);return target.apply(target,args.map(function(value){let result=getValue(value,prefix,data);return result??undefined;}));};let exec=function(event){let parsedSuccess=expression.parse(success);let parsedFailure=expression.parse(failure);let parsedAction=expression.parse(action);parsedSuccess=parsedSuccess&&parsedSuccess!=""?parsedSuccess.split(",").map(element=>element.trim()):[];parsedFailure=parsedFailure&&parsedFailure!=""?parsedFailure.split(",").map(element=>element.trim()):[];element.$lsSkip=true;element.classList.add("load-service-start");if(!document.body.contains(element)){element=undefined;return false;} if(event){event.preventDefault();} if(running){return false;} running=true;element.style.backgroud='red';if(confirm){if(window.confirm(confirm)!==true){element.classList.add("load-service-end");element.$lsSkip=false;running=false;return false;}} diff --git a/public/scripts/dependencies/appwrite.js b/public/scripts/dependencies/appwrite.js index 525e1023eb..8bd47ab36f 100644 --- a/public/scripts/dependencies/appwrite.js +++ b/public/scripts/dependencies/appwrite.js @@ -1,27 +1,4025 @@ -(function (window) { - +(function (exports, isomorphicFormData, crossFetch) { 'use strict'; - window.Appwrite = function () { - - let config = { - endpoint: 'https://appwrite.io/v1', - project: '', - key: '', - locale: '', - mode: '', - }; + function __awaiter(thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + 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', + endpointRealtime: '', + project: '', + key: '', + jwt: '', + locale: '', + mode: '', + }; + this.headers = { + 'x-sdk-version': 'appwrite:web:2.0.0', + 'X-Appwrite-Response-Format': '0.8.0', + }; + this.realtime = { + socket: undefined, + timeout: undefined, + channels: {}, + lastMessage: undefined, + createSocket: () => { + var _a; + 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()); + 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; + if (((_b = (_a = this.realtime) === null || _a === void 0 ? void 0 : _a.lastMessage) === null || _b === void 0 ? void 0 : _b.code) === 1008) { + return; + } + console.error('Realtime got disconnected. Reconnect will be attempted in 1 second.', event.reason); + setTimeout(() => { + this.realtime.createSocket(); + }, 1000); + }); + }, + onMessage: (channel, callback) => (event) => { + try { + const data = JSON.parse(event.data); + this.realtime.lastMessage = data; + if (data.channels && data.channels.includes(channel)) { + callback(data); + } + else if (data.code) { + throw data; + } + } + catch (e) { + console.error(e); + } + } + }; + this.account = { + /** + * Get Account + * + * Get currently logged in user data as JSON object. + * + * @throws {AppwriteException} + * @returns {Promise} + */ + 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 Account + * + * Use this endpoint to allow a new user to register a new account in your + * project. After the user registration completes successfully, you can use + * the [/account/verfication](/docs/client/account#accountCreateVerification) + * route to start verifying the user email address. To allow the new user to + * login to their new account, you need to create a new [account + * session](/docs/client/account#accountCreateSession). + * + * @param {string} email + * @param {string} password + * @param {string} name + * @throws {AppwriteException} + * @returns {Promise} + */ + 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; + } + if (typeof name !== 'undefined') { + payload['name'] = name; + } + const uri = new URL(this.config.endpoint + path); + return yield this.call('post', uri, { + 'content-type': 'application/json', + }, payload); + }), + /** + * Delete Account + * + * Delete a currently logged in user account. Behind the scene, the user + * record is not deleted but permanently blocked from any access. This is done + * to avoid deleted accounts being overtaken by new users with the same email + * address. Any user-related resources like documents or storage files should + * be deleted separately. + * + * @throws {AppwriteException} + * @returns {Promise} + */ + delete: () => __awaiter(this, void 0, void 0, function* () { + let path = '/account'; + let payload = {}; + const uri = new URL(this.config.endpoint + path); + return yield this.call('delete', uri, { + 'content-type': 'application/json', + }, payload); + }), + /** + * Update Account Email + * + * Update currently logged in user account email address. After changing user + * address, user confirmation status is being reset and a new confirmation + * mail is sent. For security measures, user password is required to complete + * this request. + * This endpoint can also be used to convert an anonymous account to a normal + * one, by passing an email address and a new password. + * + * @param {string} email + * @param {string} password + * @throws {AppwriteException} + * @returns {Promise} + */ + updateEmail: (email, password) => __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/email'; + let payload = {}; + if (typeof email !== 'undefined') { + payload['email'] = email; + } + if (typeof password !== 'undefined') { + payload['password'] = password; + } + const uri = new URL(this.config.endpoint + path); + return yield this.call('patch', uri, { + 'content-type': 'application/json', + }, payload); + }), + /** + * Create Account JWT + * + * Use this endpoint to create a JSON Web Token. You can use the resulting JWT + * to authenticate on behalf of the current user when working with the + * Appwrite server-side API and SDKs. The JWT secret is valid for 15 minutes + * from its creation and will be invalid if the user will logout. + * + * @throws {AppwriteException} + * @returns {Promise} + */ + createJWT: () => __awaiter(this, void 0, void 0, function* () { + let path = '/account/jwt'; + let payload = {}; + const uri = new URL(this.config.endpoint + path); + return yield this.call('post', uri, { + 'content-type': 'application/json', + }, payload); + }), + /** + * Get Account Logs + * + * Get currently logged in user list of latest security activity logs. Each + * log returns user IP address, location and date and time of log. + * + * @throws {AppwriteException} + * @returns {Promise} + */ + getLogs: () => __awaiter(this, void 0, void 0, function* () { + let path = '/account/logs'; + let payload = {}; + const uri = new URL(this.config.endpoint + path); + return yield this.call('get', uri, { + 'content-type': 'application/json', + }, payload); + }), + /** + * Update Account Name + * + * Update currently logged in user account name. + * + * @param {string} name + * @throws {AppwriteException} + * @returns {Promise} + */ + updateName: (name) => __awaiter(this, void 0, void 0, function* () { + if (typeof name === 'undefined') { + throw new AppwriteException('Missing required parameter: "name"'); + } + let path = '/account/name'; + let payload = {}; + if (typeof name !== 'undefined') { + payload['name'] = name; + } + const uri = new URL(this.config.endpoint + path); + return yield this.call('patch', uri, { + 'content-type': 'application/json', + }, payload); + }), + /** + * Update Account Password + * + * Update currently logged in user password. For validation, user is required + * to pass in the new password, and the old password. For users created with + * OAuth and Team Invites, oldPassword is optional. + * + * @param {string} password + * @param {string} oldPassword + * @throws {AppwriteException} + * @returns {Promise} + */ + updatePassword: (password, oldPassword) => __awaiter(this, void 0, void 0, function* () { + if (typeof password === 'undefined') { + throw new AppwriteException('Missing required parameter: "password"'); + } + let path = '/account/password'; + let payload = {}; + if (typeof password !== 'undefined') { + payload['password'] = password; + } + if (typeof oldPassword !== 'undefined') { + payload['oldPassword'] = oldPassword; + } + const uri = new URL(this.config.endpoint + path); + return yield this.call('patch', uri, { + 'content-type': 'application/json', + }, payload); + }), + /** + * Get Account Preferences + * + * Get currently logged in user preferences as a key-value object. + * + * @throws {AppwriteException} + * @returns {Promise} + */ + getPrefs: () => __awaiter(this, void 0, void 0, function* () { + let path = '/account/prefs'; + let payload = {}; + const uri = new URL(this.config.endpoint + path); + return yield this.call('get', uri, { + 'content-type': 'application/json', + }, payload); + }), + /** + * Update Account Preferences + * + * Update currently logged in user account preferences. You can pass only the + * specific settings you wish to update. + * + * @param {object} prefs + * @throws {AppwriteException} + * @returns {Promise} + */ + updatePrefs: (prefs) => __awaiter(this, void 0, void 0, function* () { + if (typeof prefs === 'undefined') { + throw new AppwriteException('Missing required parameter: "prefs"'); + } + let path = '/account/prefs'; + let payload = {}; + if (typeof prefs !== 'undefined') { + payload['prefs'] = prefs; + } + const uri = new URL(this.config.endpoint + path); + return yield this.call('patch', uri, { + 'content-type': 'application/json', + }, payload); + }), + /** + * Create Password Recovery + * + * Sends the user an email with a temporary secret key for password reset. + * When the user clicks the confirmation link he is redirected back to your + * app password reset URL with the secret key and email address values + * attached to the URL query string. Use the query string params to submit a + * request to the [PUT + * /account/recovery](/docs/client/account#accountUpdateRecovery) endpoint to + * complete the process. The verification link sent to the user's email + * address is valid for 1 hour. + * + * @param {string} email + * @param {string} url + * @throws {AppwriteException} + * @returns {Promise} + */ + createRecovery: (email, url) => __awaiter(this, void 0, void 0, function* () { + if (typeof email === 'undefined') { + throw new AppwriteException('Missing required parameter: "email"'); + } + if (typeof url === 'undefined') { + throw new AppwriteException('Missing required parameter: "url"'); + } + let path = '/account/recovery'; + let payload = {}; + if (typeof email !== 'undefined') { + payload['email'] = email; + } + if (typeof url !== 'undefined') { + payload['url'] = url; + } + const uri = new URL(this.config.endpoint + path); + return yield this.call('post', uri, { + 'content-type': 'application/json', + }, payload); + }), + /** + * Complete Password Recovery + * + * Use this endpoint to complete the user account password reset. Both the + * **userId** and **secret** arguments will be passed as query parameters to + * the redirect URL you have provided when sending your request to the [POST + * /account/recovery](/docs/client/account#accountCreateRecovery) endpoint. + * + * Please note that in order to avoid a [Redirect + * Attack](https://github.com/OWASP/CheatSheetSeries/blob/master/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.md) + * the only valid redirect URLs are the ones from domains you have set when + * adding your platforms in the console interface. + * + * @param {string} userId + * @param {string} secret + * @param {string} password + * @param {string} passwordAgain + * @throws {AppwriteException} + * @returns {Promise} + */ + updateRecovery: (userId, secret, password, passwordAgain) => __awaiter(this, void 0, void 0, function* () { + if (typeof userId === 'undefined') { + throw new AppwriteException('Missing required parameter: "userId"'); + } + if (typeof secret === 'undefined') { + throw new AppwriteException('Missing required parameter: "secret"'); + } + if (typeof password === 'undefined') { + throw new AppwriteException('Missing required parameter: "password"'); + } + if (typeof passwordAgain === 'undefined') { + throw new AppwriteException('Missing required parameter: "passwordAgain"'); + } + let path = '/account/recovery'; + let payload = {}; + if (typeof userId !== 'undefined') { + payload['userId'] = userId; + } + if (typeof secret !== 'undefined') { + payload['secret'] = secret; + } + if (typeof password !== 'undefined') { + payload['password'] = password; + } + if (typeof passwordAgain !== 'undefined') { + payload['passwordAgain'] = passwordAgain; + } + const uri = new URL(this.config.endpoint + path); + return yield this.call('put', uri, { + 'content-type': 'application/json', + }, payload); + }), + /** + * Get Account Sessions + * + * Get currently logged in user list of active sessions across different + * devices. + * + * @throws {AppwriteException} + * @returns {Promise} + */ + getSessions: () => __awaiter(this, void 0, void 0, function* () { + let path = '/account/sessions'; + let payload = {}; + const uri = new URL(this.config.endpoint + path); + return yield this.call('get', uri, { + 'content-type': 'application/json', + }, payload); + }), + /** + * Create Account Session + * + * Allow the user to login into their account by providing a valid email and + * password combination. This route will create a new session for the user. + * + * @param {string} email + * @param {string} password + * @throws {AppwriteException} + * @returns {Promise} + */ + createSession: (email, password) => __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/sessions'; + let payload = {}; + if (typeof email !== 'undefined') { + payload['email'] = email; + } + if (typeof password !== 'undefined') { + payload['password'] = password; + } + const uri = new URL(this.config.endpoint + path); + return yield this.call('post', uri, { + 'content-type': 'application/json', + }, payload); + }), + /** + * Delete All Account Sessions + * + * Delete all sessions from the user account and remove any sessions cookies + * from the end client. + * + * @throws {AppwriteException} + * @returns {Promise} + */ + deleteSessions: () => __awaiter(this, void 0, void 0, function* () { + let path = '/account/sessions'; + let payload = {}; + const uri = new URL(this.config.endpoint + path); + return yield this.call('delete', uri, { + 'content-type': 'application/json', + }, payload); + }), + /** + * Create Anonymous Session + * + * Use this endpoint to allow a new user to register an anonymous account in + * your project. This route will also create a new session for the user. To + * allow the new user to convert an anonymous account to a normal account + * account, you need to update its [email and + * password](/docs/client/account#accountUpdateEmail). + * + * @throws {AppwriteException} + * @returns {Promise} + */ + createAnonymousSession: () => __awaiter(this, void 0, void 0, function* () { + let path = '/account/sessions/anonymous'; + let payload = {}; + const uri = new URL(this.config.endpoint + path); + return yield this.call('post', uri, { + 'content-type': 'application/json', + }, payload); + }), + /** + * Create Account Session with OAuth2 + * + * Allow the user to login to their account using the OAuth2 provider of their + * choice. Each OAuth2 provider should be enabled from the Appwrite console + * first. Use the success and failure arguments to provide a redirect URL's + * back to your app when login is completed. + * + * @param {string} provider + * @param {string} success + * @param {string} failure + * @param {string[]} scopes + * @throws {AppwriteException} + * @returns {void|string} + */ + createOAuth2Session: (provider, success, failure, scopes) => { + if (typeof provider === 'undefined') { + throw new AppwriteException('Missing required parameter: "provider"'); + } + let path = '/account/sessions/oauth2/{provider}'.replace('{provider}', provider); + let payload = {}; + if (typeof success !== 'undefined') { + payload['success'] = success; + } + if (typeof failure !== 'undefined') { + payload['failure'] = failure; + } + if (typeof scopes !== 'undefined') { + payload['scopes'] = scopes; + } + const uri = new URL(this.config.endpoint + path); + payload['project'] = this.config.project; + for (const [key, value] of Object.entries(this.flatten(payload))) { + uri.searchParams.append(key, value); + } + if (typeof window !== 'undefined' && (window === null || window === void 0 ? void 0 : window.location)) { + window.location.href = uri.toString(); + } + else { + return uri; + } + }, + /** + * Delete Account Session + * + * Use this endpoint to log out the currently logged in user from all their + * account sessions across all of their different devices. When using the + * option id argument, only the session unique ID provider will be deleted. + * + * @param {string} sessionId + * @throws {AppwriteException} + * @returns {Promise} + */ + deleteSession: (sessionId) => __awaiter(this, void 0, void 0, function* () { + if (typeof sessionId === 'undefined') { + throw new AppwriteException('Missing required parameter: "sessionId"'); + } + let path = '/account/sessions/{sessionId}'.replace('{sessionId}', sessionId); + let payload = {}; + const uri = new URL(this.config.endpoint + path); + return yield this.call('delete', uri, { + 'content-type': 'application/json', + }, payload); + }), + /** + * Create Email Verification + * + * Use this endpoint to send a verification message to your user email address + * to confirm they are the valid owners of that address. Both the **userId** + * and **secret** arguments will be passed as query parameters to the URL you + * have provided to be attached to the verification email. The provided URL + * should redirect the user back to your app and allow you to complete the + * verification process by verifying both the **userId** and **secret** + * parameters. Learn more about how to [complete the verification + * process](/docs/client/account#accountUpdateVerification). The verification + * link sent to the user's email address is valid for 7 days. + * + * Please note that in order to avoid a [Redirect + * Attack](https://github.com/OWASP/CheatSheetSeries/blob/master/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.md), + * the only valid redirect URLs are the ones from domains you have set when + * adding your platforms in the console interface. + * + * + * @param {string} url + * @throws {AppwriteException} + * @returns {Promise} + */ + createVerification: (url) => __awaiter(this, void 0, void 0, function* () { + if (typeof url === 'undefined') { + throw new AppwriteException('Missing required parameter: "url"'); + } + let path = '/account/verification'; + let payload = {}; + if (typeof url !== 'undefined') { + payload['url'] = url; + } + const uri = new URL(this.config.endpoint + path); + return yield this.call('post', uri, { + 'content-type': 'application/json', + }, payload); + }), + /** + * Complete Email Verification + * + * Use this endpoint to complete the user email verification process. Use both + * the **userId** and **secret** parameters that were attached to your app URL + * to verify the user email ownership. If confirmed this route will return a + * 200 status code. + * + * @param {string} userId + * @param {string} secret + * @throws {AppwriteException} + * @returns {Promise} + */ + updateVerification: (userId, secret) => __awaiter(this, void 0, void 0, function* () { + if (typeof userId === 'undefined') { + throw new AppwriteException('Missing required parameter: "userId"'); + } + if (typeof secret === 'undefined') { + throw new AppwriteException('Missing required parameter: "secret"'); + } + let path = '/account/verification'; + let payload = {}; + if (typeof userId !== 'undefined') { + payload['userId'] = userId; + } + if (typeof secret !== 'undefined') { + payload['secret'] = secret; + } + const uri = new URL(this.config.endpoint + path); + return yield this.call('put', uri, { + 'content-type': 'application/json', + }, payload); + }) + }; + this.avatars = { + /** + * Get Browser Icon + * + * You can use this endpoint to show different browser icons to your users. + * The code argument receives the browser code as it appears in your user + * /account/sessions endpoint. Use width, height and quality arguments to + * change the output settings. + * + * @param {string} code + * @param {number} width + * @param {number} height + * @param {number} quality + * @throws {AppwriteException} + * @returns {URL} + */ + getBrowser: (code, width, height, quality) => { + if (typeof code === 'undefined') { + throw new AppwriteException('Missing required parameter: "code"'); + } + let path = '/avatars/browsers/{code}'.replace('{code}', code); + let payload = {}; + if (typeof width !== 'undefined') { + payload['width'] = width; + } + if (typeof height !== 'undefined') { + payload['height'] = height; + } + if (typeof quality !== 'undefined') { + payload['quality'] = quality; + } + const uri = new URL(this.config.endpoint + path); + payload['project'] = this.config.project; + for (const [key, value] of Object.entries(this.flatten(payload))) { + uri.searchParams.append(key, value); + } + return uri; + }, + /** + * Get Credit Card Icon + * + * The credit card endpoint will return you the icon of the credit card + * provider you need. Use width, height and quality arguments to change the + * output settings. + * + * @param {string} code + * @param {number} width + * @param {number} height + * @param {number} quality + * @throws {AppwriteException} + * @returns {URL} + */ + getCreditCard: (code, width, height, quality) => { + if (typeof code === 'undefined') { + throw new AppwriteException('Missing required parameter: "code"'); + } + let path = '/avatars/credit-cards/{code}'.replace('{code}', code); + let payload = {}; + if (typeof width !== 'undefined') { + payload['width'] = width; + } + if (typeof height !== 'undefined') { + payload['height'] = height; + } + if (typeof quality !== 'undefined') { + payload['quality'] = quality; + } + const uri = new URL(this.config.endpoint + path); + payload['project'] = this.config.project; + for (const [key, value] of Object.entries(this.flatten(payload))) { + uri.searchParams.append(key, value); + } + return uri; + }, + /** + * Get Favicon + * + * Use this endpoint to fetch the favorite icon (AKA favicon) of any remote + * website URL. + * + * + * @param {string} url + * @throws {AppwriteException} + * @returns {URL} + */ + getFavicon: (url) => { + if (typeof url === 'undefined') { + throw new AppwriteException('Missing required parameter: "url"'); + } + let path = '/avatars/favicon'; + let payload = {}; + if (typeof url !== 'undefined') { + payload['url'] = url; + } + const uri = new URL(this.config.endpoint + path); + payload['project'] = this.config.project; + for (const [key, value] of Object.entries(this.flatten(payload))) { + uri.searchParams.append(key, value); + } + return uri; + }, + /** + * Get Country Flag + * + * You can use this endpoint to show different country flags icons to your + * users. The code argument receives the 2 letter country code. Use width, + * height and quality arguments to change the output settings. + * + * @param {string} code + * @param {number} width + * @param {number} height + * @param {number} quality + * @throws {AppwriteException} + * @returns {URL} + */ + getFlag: (code, width, height, quality) => { + if (typeof code === 'undefined') { + throw new AppwriteException('Missing required parameter: "code"'); + } + let path = '/avatars/flags/{code}'.replace('{code}', code); + let payload = {}; + if (typeof width !== 'undefined') { + payload['width'] = width; + } + if (typeof height !== 'undefined') { + payload['height'] = height; + } + if (typeof quality !== 'undefined') { + payload['quality'] = quality; + } + const uri = new URL(this.config.endpoint + path); + payload['project'] = this.config.project; + for (const [key, value] of Object.entries(this.flatten(payload))) { + uri.searchParams.append(key, value); + } + return uri; + }, + /** + * Get Image from URL + * + * Use this endpoint to fetch a remote image URL and crop it to any image size + * you want. This endpoint is very useful if you need to crop and display + * remote images in your app or in case you want to make sure a 3rd party + * image is properly served using a TLS protocol. + * + * @param {string} url + * @param {number} width + * @param {number} height + * @throws {AppwriteException} + * @returns {URL} + */ + getImage: (url, width, height) => { + if (typeof url === 'undefined') { + throw new AppwriteException('Missing required parameter: "url"'); + } + let path = '/avatars/image'; + let payload = {}; + if (typeof url !== 'undefined') { + payload['url'] = url; + } + if (typeof width !== 'undefined') { + payload['width'] = width; + } + if (typeof height !== 'undefined') { + payload['height'] = height; + } + const uri = new URL(this.config.endpoint + path); + payload['project'] = this.config.project; + for (const [key, value] of Object.entries(this.flatten(payload))) { + uri.searchParams.append(key, value); + } + return uri; + }, + /** + * Get User Initials + * + * Use this endpoint to show your user initials avatar icon on your website or + * app. By default, this route will try to print your logged-in user name or + * email initials. You can also overwrite the user name if you pass the 'name' + * parameter. If no name is given and no user is logged, an empty avatar will + * be returned. + * + * You can use the color and background params to change the avatar colors. By + * default, a random theme will be selected. The random theme will persist for + * the user's initials when reloading the same theme will always return for + * the same initials. + * + * @param {string} name + * @param {number} width + * @param {number} height + * @param {string} color + * @param {string} background + * @throws {AppwriteException} + * @returns {URL} + */ + getInitials: (name, width, height, color, background) => { + let path = '/avatars/initials'; + let payload = {}; + if (typeof name !== 'undefined') { + payload['name'] = name; + } + if (typeof width !== 'undefined') { + payload['width'] = width; + } + if (typeof height !== 'undefined') { + payload['height'] = height; + } + if (typeof color !== 'undefined') { + payload['color'] = color; + } + if (typeof background !== 'undefined') { + payload['background'] = background; + } + const uri = new URL(this.config.endpoint + path); + payload['project'] = this.config.project; + for (const [key, value] of Object.entries(this.flatten(payload))) { + uri.searchParams.append(key, value); + } + return uri; + }, + /** + * Get QR Code + * + * Converts a given plain text to a QR code image. You can use the query + * parameters to change the size and style of the resulting image. + * + * @param {string} text + * @param {number} size + * @param {number} margin + * @param {boolean} download + * @throws {AppwriteException} + * @returns {URL} + */ + getQR: (text, size, margin, download) => { + if (typeof text === 'undefined') { + throw new AppwriteException('Missing required parameter: "text"'); + } + let path = '/avatars/qr'; + let payload = {}; + if (typeof text !== 'undefined') { + payload['text'] = text; + } + if (typeof size !== 'undefined') { + payload['size'] = size; + } + if (typeof margin !== 'undefined') { + payload['margin'] = margin; + } + if (typeof download !== 'undefined') { + payload['download'] = download; + } + const uri = new URL(this.config.endpoint + path); + payload['project'] = this.config.project; + for (const [key, value] of Object.entries(this.flatten(payload))) { + uri.searchParams.append(key, value); + } + return uri; + } + }; + this.database = { + /** + * List Collections + * + * Get a list of all the user collections. You can use the query params to + * filter your results. On admin mode, this endpoint will return a list of all + * of the project's collections. [Learn more about different API + * modes](/docs/admin). + * + * @param {string} search + * @param {number} limit + * @param {number} offset + * @param {string} orderType + * @throws {AppwriteException} + * @returns {Promise} + */ + listCollections: (search, limit, offset, orderType) => __awaiter(this, void 0, void 0, function* () { + let path = '/database/collections'; + let payload = {}; + if (typeof search !== 'undefined') { + payload['search'] = search; + } + if (typeof limit !== 'undefined') { + payload['limit'] = limit; + } + if (typeof offset !== 'undefined') { + payload['offset'] = offset; + } + if (typeof orderType !== 'undefined') { + payload['orderType'] = orderType; + } + const uri = new URL(this.config.endpoint + path); + return yield this.call('get', uri, { + 'content-type': 'application/json', + }, payload); + }), + /** + * Create Collection + * + * Create a new Collection. + * + * @param {string} name + * @param {string[]} read + * @param {string[]} write + * @param {string[]} rules + * @throws {AppwriteException} + * @returns {Promise} + */ + createCollection: (name, read, write, rules) => __awaiter(this, void 0, void 0, function* () { + if (typeof name === 'undefined') { + throw new AppwriteException('Missing required parameter: "name"'); + } + if (typeof read === 'undefined') { + throw new AppwriteException('Missing required parameter: "read"'); + } + if (typeof write === 'undefined') { + throw new AppwriteException('Missing required parameter: "write"'); + } + if (typeof rules === 'undefined') { + throw new AppwriteException('Missing required parameter: "rules"'); + } + let path = '/database/collections'; + let payload = {}; + if (typeof name !== 'undefined') { + payload['name'] = name; + } + if (typeof read !== 'undefined') { + payload['read'] = read; + } + if (typeof write !== 'undefined') { + payload['write'] = write; + } + if (typeof rules !== 'undefined') { + payload['rules'] = rules; + } + const uri = new URL(this.config.endpoint + path); + return yield this.call('post', uri, { + 'content-type': 'application/json', + }, payload); + }), + /** + * Get Collection + * + * Get a collection by its unique ID. This endpoint response returns a JSON + * object with the collection metadata. + * + * @param {string} collectionId + * @throws {AppwriteException} + * @returns {Promise} + */ + getCollection: (collectionId) => __awaiter(this, void 0, void 0, function* () { + if (typeof collectionId === 'undefined') { + throw new AppwriteException('Missing required parameter: "collectionId"'); + } + let path = '/database/collections/{collectionId}'.replace('{collectionId}', collectionId); + let payload = {}; + const uri = new URL(this.config.endpoint + path); + return yield this.call('get', uri, { + 'content-type': 'application/json', + }, payload); + }), + /** + * Update Collection + * + * Update a collection by its unique ID. + * + * @param {string} collectionId + * @param {string} name + * @param {string[]} read + * @param {string[]} write + * @param {string[]} rules + * @throws {AppwriteException} + * @returns {Promise} + */ + updateCollection: (collectionId, name, read, write, rules) => __awaiter(this, void 0, void 0, function* () { + if (typeof collectionId === 'undefined') { + throw new AppwriteException('Missing required parameter: "collectionId"'); + } + if (typeof name === 'undefined') { + throw new AppwriteException('Missing required parameter: "name"'); + } + let path = '/database/collections/{collectionId}'.replace('{collectionId}', collectionId); + let payload = {}; + if (typeof name !== 'undefined') { + payload['name'] = name; + } + if (typeof read !== 'undefined') { + payload['read'] = read; + } + if (typeof write !== 'undefined') { + payload['write'] = write; + } + if (typeof rules !== 'undefined') { + payload['rules'] = rules; + } + const uri = new URL(this.config.endpoint + path); + return yield this.call('put', uri, { + 'content-type': 'application/json', + }, payload); + }), + /** + * Delete Collection + * + * Delete a collection by its unique ID. Only users with write permissions + * have access to delete this resource. + * + * @param {string} collectionId + * @throws {AppwriteException} + * @returns {Promise} + */ + deleteCollection: (collectionId) => __awaiter(this, void 0, void 0, function* () { + if (typeof collectionId === 'undefined') { + throw new AppwriteException('Missing required parameter: "collectionId"'); + } + let path = '/database/collections/{collectionId}'.replace('{collectionId}', collectionId); + let payload = {}; + const uri = new URL(this.config.endpoint + path); + return yield this.call('delete', uri, { + 'content-type': 'application/json', + }, payload); + }), + /** + * List Documents + * + * Get a list of all the user documents. You can use the query params to + * filter your results. On admin mode, this endpoint will return a list of all + * of the project's documents. [Learn more about different API + * modes](/docs/admin). + * + * @param {string} collectionId + * @param {string[]} filters + * @param {number} limit + * @param {number} offset + * @param {string} orderField + * @param {string} orderType + * @param {string} orderCast + * @param {string} search + * @throws {AppwriteException} + * @returns {Promise} + */ + listDocuments: (collectionId, filters, limit, offset, orderField, orderType, orderCast, search) => __awaiter(this, void 0, void 0, function* () { + if (typeof collectionId === 'undefined') { + throw new AppwriteException('Missing required parameter: "collectionId"'); + } + let path = '/database/collections/{collectionId}/documents'.replace('{collectionId}', collectionId); + let payload = {}; + if (typeof filters !== 'undefined') { + payload['filters'] = filters; + } + if (typeof limit !== 'undefined') { + payload['limit'] = limit; + } + if (typeof offset !== 'undefined') { + payload['offset'] = offset; + } + if (typeof orderField !== 'undefined') { + payload['orderField'] = orderField; + } + if (typeof orderType !== 'undefined') { + payload['orderType'] = orderType; + } + if (typeof orderCast !== 'undefined') { + payload['orderCast'] = orderCast; + } + if (typeof search !== 'undefined') { + payload['search'] = search; + } + const uri = new URL(this.config.endpoint + path); + return yield this.call('get', uri, { + 'content-type': 'application/json', + }, payload); + }), + /** + * Create Document + * + * Create a new Document. Before using this route, you should create a new + * collection resource using either a [server + * integration](/docs/server/database#databaseCreateCollection) API or + * directly from your database console. + * + * @param {string} collectionId + * @param {object} data + * @param {string[]} read + * @param {string[]} write + * @param {string} parentDocument + * @param {string} parentProperty + * @param {string} parentPropertyType + * @throws {AppwriteException} + * @returns {Promise} + */ + createDocument: (collectionId, data, read, write, parentDocument, parentProperty, parentPropertyType) => __awaiter(this, void 0, void 0, function* () { + if (typeof collectionId === 'undefined') { + throw new AppwriteException('Missing required parameter: "collectionId"'); + } + if (typeof data === 'undefined') { + throw new AppwriteException('Missing required parameter: "data"'); + } + let path = '/database/collections/{collectionId}/documents'.replace('{collectionId}', collectionId); + let payload = {}; + if (typeof data !== 'undefined') { + payload['data'] = data; + } + if (typeof read !== 'undefined') { + payload['read'] = read; + } + if (typeof write !== 'undefined') { + payload['write'] = write; + } + if (typeof parentDocument !== 'undefined') { + payload['parentDocument'] = parentDocument; + } + if (typeof parentProperty !== 'undefined') { + payload['parentProperty'] = parentProperty; + } + if (typeof parentPropertyType !== 'undefined') { + payload['parentPropertyType'] = parentPropertyType; + } + const uri = new URL(this.config.endpoint + path); + return yield this.call('post', uri, { + 'content-type': 'application/json', + }, payload); + }), + /** + * Get Document + * + * Get a document by its unique ID. This endpoint response returns a JSON + * object with the document data. + * + * @param {string} collectionId + * @param {string} documentId + * @throws {AppwriteException} + * @returns {Promise} + */ + getDocument: (collectionId, documentId) => __awaiter(this, void 0, void 0, function* () { + if (typeof collectionId === 'undefined') { + throw new AppwriteException('Missing required parameter: "collectionId"'); + } + if (typeof documentId === 'undefined') { + throw new AppwriteException('Missing required parameter: "documentId"'); + } + let path = '/database/collections/{collectionId}/documents/{documentId}'.replace('{collectionId}', collectionId).replace('{documentId}', documentId); + let payload = {}; + const uri = new URL(this.config.endpoint + path); + return yield this.call('get', uri, { + 'content-type': 'application/json', + }, payload); + }), + /** + * Update Document + * + * Update a document by its unique ID. Using the patch method you can pass + * only specific fields that will get updated. + * + * @param {string} collectionId + * @param {string} documentId + * @param {object} data + * @param {string[]} read + * @param {string[]} write + * @throws {AppwriteException} + * @returns {Promise} + */ + updateDocument: (collectionId, documentId, data, read, write) => __awaiter(this, void 0, void 0, function* () { + if (typeof collectionId === 'undefined') { + throw new AppwriteException('Missing required parameter: "collectionId"'); + } + if (typeof documentId === 'undefined') { + throw new AppwriteException('Missing required parameter: "documentId"'); + } + if (typeof data === 'undefined') { + throw new AppwriteException('Missing required parameter: "data"'); + } + let path = '/database/collections/{collectionId}/documents/{documentId}'.replace('{collectionId}', collectionId).replace('{documentId}', documentId); + let payload = {}; + if (typeof data !== 'undefined') { + payload['data'] = data; + } + if (typeof read !== 'undefined') { + payload['read'] = read; + } + if (typeof write !== 'undefined') { + payload['write'] = write; + } + const uri = new URL(this.config.endpoint + path); + return yield this.call('patch', uri, { + 'content-type': 'application/json', + }, payload); + }), + /** + * Delete Document + * + * Delete a document by its unique ID. This endpoint deletes only the parent + * documents, its attributes and relations to other documents. Child documents + * **will not** be deleted. + * + * @param {string} collectionId + * @param {string} documentId + * @throws {AppwriteException} + * @returns {Promise} + */ + deleteDocument: (collectionId, documentId) => __awaiter(this, void 0, void 0, function* () { + if (typeof collectionId === 'undefined') { + throw new AppwriteException('Missing required parameter: "collectionId"'); + } + if (typeof documentId === 'undefined') { + throw new AppwriteException('Missing required parameter: "documentId"'); + } + let path = '/database/collections/{collectionId}/documents/{documentId}'.replace('{collectionId}', collectionId).replace('{documentId}', documentId); + let payload = {}; + const uri = new URL(this.config.endpoint + path); + return yield this.call('delete', uri, { + 'content-type': 'application/json', + }, payload); + }) + }; + this.functions = { + /** + * List Functions + * + * Get a list of all the project's functions. You can use the query params to + * filter your results. + * + * @param {string} search + * @param {number} limit + * @param {number} offset + * @param {string} orderType + * @throws {AppwriteException} + * @returns {Promise} + */ + list: (search, limit, offset, orderType) => __awaiter(this, void 0, void 0, function* () { + let path = '/functions'; + let payload = {}; + if (typeof search !== 'undefined') { + payload['search'] = search; + } + if (typeof limit !== 'undefined') { + payload['limit'] = limit; + } + if (typeof offset !== 'undefined') { + payload['offset'] = offset; + } + if (typeof orderType !== 'undefined') { + payload['orderType'] = orderType; + } + const uri = new URL(this.config.endpoint + path); + return yield this.call('get', uri, { + 'content-type': 'application/json', + }, payload); + }), + /** + * Create Function + * + * Create a new function. You can pass a list of + * [permissions](/docs/permissions) to allow different project users or team + * with access to execute the function using the client API. + * + * @param {string} name + * @param {string[]} execute + * @param {string} env + * @param {object} vars + * @param {string[]} events + * @param {string} schedule + * @param {number} timeout + * @throws {AppwriteException} + * @returns {Promise} + */ + create: (name, execute, env, vars, events, schedule, timeout) => __awaiter(this, void 0, void 0, function* () { + if (typeof name === 'undefined') { + throw new AppwriteException('Missing required parameter: "name"'); + } + if (typeof execute === 'undefined') { + throw new AppwriteException('Missing required parameter: "execute"'); + } + if (typeof env === 'undefined') { + throw new AppwriteException('Missing required parameter: "env"'); + } + let path = '/functions'; + let payload = {}; + if (typeof name !== 'undefined') { + payload['name'] = name; + } + if (typeof execute !== 'undefined') { + payload['execute'] = execute; + } + if (typeof env !== 'undefined') { + payload['env'] = env; + } + if (typeof vars !== 'undefined') { + payload['vars'] = vars; + } + if (typeof events !== 'undefined') { + payload['events'] = events; + } + if (typeof schedule !== 'undefined') { + payload['schedule'] = schedule; + } + if (typeof timeout !== 'undefined') { + payload['timeout'] = timeout; + } + const uri = new URL(this.config.endpoint + path); + return yield this.call('post', uri, { + 'content-type': 'application/json', + }, payload); + }), + /** + * Get Function + * + * Get a function by its unique ID. + * + * @param {string} functionId + * @throws {AppwriteException} + * @returns {Promise} + */ + get: (functionId) => __awaiter(this, void 0, void 0, function* () { + if (typeof functionId === 'undefined') { + throw new AppwriteException('Missing required parameter: "functionId"'); + } + let path = '/functions/{functionId}'.replace('{functionId}', functionId); + let payload = {}; + const uri = new URL(this.config.endpoint + path); + return yield this.call('get', uri, { + 'content-type': 'application/json', + }, payload); + }), + /** + * Update Function + * + * Update function by its unique ID. + * + * @param {string} functionId + * @param {string} name + * @param {string[]} execute + * @param {object} vars + * @param {string[]} events + * @param {string} schedule + * @param {number} timeout + * @throws {AppwriteException} + * @returns {Promise} + */ + update: (functionId, name, execute, vars, events, schedule, timeout) => __awaiter(this, void 0, void 0, function* () { + if (typeof functionId === 'undefined') { + throw new AppwriteException('Missing required parameter: "functionId"'); + } + if (typeof name === 'undefined') { + throw new AppwriteException('Missing required parameter: "name"'); + } + if (typeof execute === 'undefined') { + throw new AppwriteException('Missing required parameter: "execute"'); + } + let path = '/functions/{functionId}'.replace('{functionId}', functionId); + let payload = {}; + if (typeof name !== 'undefined') { + payload['name'] = name; + } + if (typeof execute !== 'undefined') { + payload['execute'] = execute; + } + if (typeof vars !== 'undefined') { + payload['vars'] = vars; + } + if (typeof events !== 'undefined') { + payload['events'] = events; + } + if (typeof schedule !== 'undefined') { + payload['schedule'] = schedule; + } + if (typeof timeout !== 'undefined') { + payload['timeout'] = timeout; + } + const uri = new URL(this.config.endpoint + path); + return yield this.call('put', uri, { + 'content-type': 'application/json', + }, payload); + }), + /** + * Delete Function + * + * Delete a function by its unique ID. + * + * @param {string} functionId + * @throws {AppwriteException} + * @returns {Promise} + */ + delete: (functionId) => __awaiter(this, void 0, void 0, function* () { + if (typeof functionId === 'undefined') { + throw new AppwriteException('Missing required parameter: "functionId"'); + } + let path = '/functions/{functionId}'.replace('{functionId}', functionId); + let payload = {}; + const uri = new URL(this.config.endpoint + path); + return yield this.call('delete', uri, { + 'content-type': 'application/json', + }, payload); + }), + /** + * List Executions + * + * Get a list of all the current user function execution logs. You can use the + * query params to filter your results. On admin mode, this endpoint will + * return a list of all of the project's executions. [Learn more about + * different API modes](/docs/admin). + * + * @param {string} functionId + * @param {string} search + * @param {number} limit + * @param {number} offset + * @param {string} orderType + * @throws {AppwriteException} + * @returns {Promise} + */ + listExecutions: (functionId, search, limit, offset, orderType) => __awaiter(this, void 0, void 0, function* () { + if (typeof functionId === 'undefined') { + throw new AppwriteException('Missing required parameter: "functionId"'); + } + let path = '/functions/{functionId}/executions'.replace('{functionId}', functionId); + let payload = {}; + if (typeof search !== 'undefined') { + payload['search'] = search; + } + if (typeof limit !== 'undefined') { + payload['limit'] = limit; + } + if (typeof offset !== 'undefined') { + payload['offset'] = offset; + } + if (typeof orderType !== 'undefined') { + payload['orderType'] = orderType; + } + const uri = new URL(this.config.endpoint + path); + return yield this.call('get', uri, { + 'content-type': 'application/json', + }, payload); + }), + /** + * Create Execution + * + * Trigger a function execution. The returned object will return you the + * current execution status. You can ping the `Get Execution` endpoint to get + * updates on the current execution status. Once this endpoint is called, your + * function execution process will start asynchronously. + * + * @param {string} functionId + * @param {string} data + * @throws {AppwriteException} + * @returns {Promise} + */ + createExecution: (functionId, data) => __awaiter(this, void 0, void 0, function* () { + if (typeof functionId === 'undefined') { + throw new AppwriteException('Missing required parameter: "functionId"'); + } + let path = '/functions/{functionId}/executions'.replace('{functionId}', functionId); + let payload = {}; + if (typeof data !== 'undefined') { + payload['data'] = data; + } + const uri = new URL(this.config.endpoint + path); + return yield this.call('post', uri, { + 'content-type': 'application/json', + }, payload); + }), + /** + * Get Execution + * + * Get a function execution log by its unique ID. + * + * @param {string} functionId + * @param {string} executionId + * @throws {AppwriteException} + * @returns {Promise} + */ + getExecution: (functionId, executionId) => __awaiter(this, void 0, void 0, function* () { + if (typeof functionId === 'undefined') { + throw new AppwriteException('Missing required parameter: "functionId"'); + } + if (typeof executionId === 'undefined') { + throw new AppwriteException('Missing required parameter: "executionId"'); + } + let path = '/functions/{functionId}/executions/{executionId}'.replace('{functionId}', functionId).replace('{executionId}', executionId); + let payload = {}; + const uri = new URL(this.config.endpoint + path); + return yield this.call('get', uri, { + 'content-type': 'application/json', + }, payload); + }), + /** + * Update Function Tag + * + * Update the function code tag ID using the unique function ID. Use this + * endpoint to switch the code tag that should be executed by the execution + * endpoint. + * + * @param {string} functionId + * @param {string} tag + * @throws {AppwriteException} + * @returns {Promise} + */ + updateTag: (functionId, tag) => __awaiter(this, void 0, void 0, function* () { + if (typeof functionId === 'undefined') { + throw new AppwriteException('Missing required parameter: "functionId"'); + } + if (typeof tag === 'undefined') { + throw new AppwriteException('Missing required parameter: "tag"'); + } + let path = '/functions/{functionId}/tag'.replace('{functionId}', functionId); + let payload = {}; + if (typeof tag !== 'undefined') { + payload['tag'] = tag; + } + const uri = new URL(this.config.endpoint + path); + return yield this.call('patch', uri, { + 'content-type': 'application/json', + }, payload); + }), + /** + * List Tags + * + * Get a list of all the project's code tags. You can use the query params to + * filter your results. + * + * @param {string} functionId + * @param {string} search + * @param {number} limit + * @param {number} offset + * @param {string} orderType + * @throws {AppwriteException} + * @returns {Promise} + */ + listTags: (functionId, search, limit, offset, orderType) => __awaiter(this, void 0, void 0, function* () { + if (typeof functionId === 'undefined') { + throw new AppwriteException('Missing required parameter: "functionId"'); + } + let path = '/functions/{functionId}/tags'.replace('{functionId}', functionId); + let payload = {}; + if (typeof search !== 'undefined') { + payload['search'] = search; + } + if (typeof limit !== 'undefined') { + payload['limit'] = limit; + } + if (typeof offset !== 'undefined') { + payload['offset'] = offset; + } + if (typeof orderType !== 'undefined') { + payload['orderType'] = orderType; + } + const uri = new URL(this.config.endpoint + path); + return yield this.call('get', uri, { + 'content-type': 'application/json', + }, payload); + }), + /** + * Create Tag + * + * Create a new function code tag. Use this endpoint to upload a new version + * of your code function. To execute your newly uploaded code, you'll need to + * update the function's tag to use your new tag UID. + * + * This endpoint accepts a tar.gz file compressed with your code. Make sure to + * include any dependencies your code has within the compressed file. You can + * learn more about code packaging in the [Appwrite Cloud Functions + * tutorial](/docs/functions). + * + * Use the "command" param to set the entry point used to execute your code. + * + * @param {string} functionId + * @param {string} command + * @param {File} code + * @throws {AppwriteException} + * @returns {Promise} + */ + createTag: (functionId, command, code) => __awaiter(this, void 0, void 0, function* () { + if (typeof functionId === 'undefined') { + throw new AppwriteException('Missing required parameter: "functionId"'); + } + if (typeof command === 'undefined') { + throw new AppwriteException('Missing required parameter: "command"'); + } + if (typeof code === 'undefined') { + throw new AppwriteException('Missing required parameter: "code"'); + } + let path = '/functions/{functionId}/tags'.replace('{functionId}', functionId); + let payload = {}; + if (typeof command !== 'undefined') { + payload['command'] = command; + } + if (typeof code !== 'undefined') { + payload['code'] = code; + } + const uri = new URL(this.config.endpoint + path); + return yield this.call('post', uri, { + 'content-type': 'multipart/form-data', + }, payload); + }), + /** + * Get Tag + * + * Get a code tag by its unique ID. + * + * @param {string} functionId + * @param {string} tagId + * @throws {AppwriteException} + * @returns {Promise} + */ + getTag: (functionId, tagId) => __awaiter(this, void 0, void 0, function* () { + if (typeof functionId === 'undefined') { + throw new AppwriteException('Missing required parameter: "functionId"'); + } + if (typeof tagId === 'undefined') { + throw new AppwriteException('Missing required parameter: "tagId"'); + } + let path = '/functions/{functionId}/tags/{tagId}'.replace('{functionId}', functionId).replace('{tagId}', tagId); + let payload = {}; + const uri = new URL(this.config.endpoint + path); + return yield this.call('get', uri, { + 'content-type': 'application/json', + }, payload); + }), + /** + * Delete Tag + * + * Delete a code tag by its unique ID. + * + * @param {string} functionId + * @param {string} tagId + * @throws {AppwriteException} + * @returns {Promise} + */ + deleteTag: (functionId, tagId) => __awaiter(this, void 0, void 0, function* () { + if (typeof functionId === 'undefined') { + throw new AppwriteException('Missing required parameter: "functionId"'); + } + if (typeof tagId === 'undefined') { + throw new AppwriteException('Missing required parameter: "tagId"'); + } + let path = '/functions/{functionId}/tags/{tagId}'.replace('{functionId}', functionId).replace('{tagId}', tagId); + let payload = {}; + const uri = new URL(this.config.endpoint + path); + return yield this.call('delete', uri, { + 'content-type': 'application/json', + }, payload); + }), + /** + * Get Function Usage + * + * + * @param {string} functionId + * @param {string} range + * @throws {AppwriteException} + * @returns {Promise} + */ + getUsage: (functionId, range) => __awaiter(this, void 0, void 0, function* () { + if (typeof functionId === 'undefined') { + throw new AppwriteException('Missing required parameter: "functionId"'); + } + let path = '/functions/{functionId}/usage'.replace('{functionId}', functionId); + let payload = {}; + if (typeof range !== 'undefined') { + payload['range'] = range; + } + const uri = new URL(this.config.endpoint + path); + return yield this.call('get', uri, { + 'content-type': 'application/json', + }, payload); + }) + }; + this.health = { + /** + * Get HTTP + * + * Check the Appwrite HTTP server is up and responsive. + * + * @throws {AppwriteException} + * @returns {Promise} + */ + get: () => __awaiter(this, void 0, void 0, function* () { + let path = '/health'; + let payload = {}; + const uri = new URL(this.config.endpoint + path); + return yield this.call('get', uri, { + 'content-type': 'application/json', + }, payload); + }), + /** + * Get Anti virus + * + * Check the Appwrite Anti Virus server is up and connection is successful. + * + * @throws {AppwriteException} + * @returns {Promise} + */ + getAntiVirus: () => __awaiter(this, void 0, void 0, function* () { + let path = '/health/anti-virus'; + let payload = {}; + const uri = new URL(this.config.endpoint + path); + return yield this.call('get', uri, { + 'content-type': 'application/json', + }, payload); + }), + /** + * Get Cache + * + * Check the Appwrite in-memory cache server is up and connection is + * successful. + * + * @throws {AppwriteException} + * @returns {Promise} + */ + getCache: () => __awaiter(this, void 0, void 0, function* () { + let path = '/health/cache'; + let payload = {}; + const uri = new URL(this.config.endpoint + path); + return yield this.call('get', uri, { + 'content-type': 'application/json', + }, payload); + }), + /** + * Get DB + * + * Check the Appwrite database server is up and connection is successful. + * + * @throws {AppwriteException} + * @returns {Promise} + */ + getDB: () => __awaiter(this, void 0, void 0, function* () { + let path = '/health/db'; + let payload = {}; + const uri = new URL(this.config.endpoint + path); + return yield this.call('get', uri, { + 'content-type': 'application/json', + }, payload); + }), + /** + * Get Certificate Queue + * + * Get the number of certificates that are waiting to be issued against + * [Letsencrypt](https://letsencrypt.org/) in the Appwrite internal queue + * server. + * + * @throws {AppwriteException} + * @returns {Promise} + */ + getQueueCertificates: () => __awaiter(this, void 0, void 0, function* () { + let path = '/health/queue/certificates'; + let payload = {}; + const uri = new URL(this.config.endpoint + path); + return yield this.call('get', uri, { + 'content-type': 'application/json', + }, payload); + }), + /** + * Get Functions Queue + * + * + * @throws {AppwriteException} + * @returns {Promise} + */ + getQueueFunctions: () => __awaiter(this, void 0, void 0, function* () { + let path = '/health/queue/functions'; + let payload = {}; + const uri = new URL(this.config.endpoint + path); + return yield this.call('get', uri, { + 'content-type': 'application/json', + }, payload); + }), + /** + * Get Logs Queue + * + * Get the number of logs that are waiting to be processed in the Appwrite + * internal queue server. + * + * @throws {AppwriteException} + * @returns {Promise} + */ + getQueueLogs: () => __awaiter(this, void 0, void 0, function* () { + let path = '/health/queue/logs'; + let payload = {}; + const uri = new URL(this.config.endpoint + path); + return yield this.call('get', uri, { + 'content-type': 'application/json', + }, payload); + }), + /** + * Get Tasks Queue + * + * Get the number of tasks that are waiting to be processed in the Appwrite + * internal queue server. + * + * @throws {AppwriteException} + * @returns {Promise} + */ + getQueueTasks: () => __awaiter(this, void 0, void 0, function* () { + let path = '/health/queue/tasks'; + let payload = {}; + const uri = new URL(this.config.endpoint + path); + return yield this.call('get', uri, { + 'content-type': 'application/json', + }, payload); + }), + /** + * Get Usage Queue + * + * Get the number of usage stats that are waiting to be processed in the + * Appwrite internal queue server. + * + * @throws {AppwriteException} + * @returns {Promise} + */ + getQueueUsage: () => __awaiter(this, void 0, void 0, function* () { + let path = '/health/queue/usage'; + let payload = {}; + const uri = new URL(this.config.endpoint + path); + return yield this.call('get', uri, { + 'content-type': 'application/json', + }, payload); + }), + /** + * Get Webhooks Queue + * + * Get the number of webhooks that are waiting to be processed in the Appwrite + * internal queue server. + * + * @throws {AppwriteException} + * @returns {Promise} + */ + getQueueWebhooks: () => __awaiter(this, void 0, void 0, function* () { + let path = '/health/queue/webhooks'; + let payload = {}; + const uri = new URL(this.config.endpoint + path); + return yield this.call('get', uri, { + 'content-type': 'application/json', + }, payload); + }), + /** + * Get Local Storage + * + * Check the Appwrite local storage device is up and connection is successful. + * + * @throws {AppwriteException} + * @returns {Promise} + */ + getStorageLocal: () => __awaiter(this, void 0, void 0, function* () { + let path = '/health/storage/local'; + let payload = {}; + const uri = new URL(this.config.endpoint + path); + return yield this.call('get', uri, { + 'content-type': 'application/json', + }, payload); + }), + /** + * Get Time + * + * Check the Appwrite server time is synced with Google remote NTP server. We + * use this technology to smoothly handle leap seconds with no disruptive + * events. The [Network Time + * Protocol](https://en.wikipedia.org/wiki/Network_Time_Protocol) (NTP) is + * used by hundreds of millions of computers and devices to synchronize their + * clocks over the Internet. If your computer sets its own clock, it likely + * uses NTP. + * + * @throws {AppwriteException} + * @returns {Promise} + */ + getTime: () => __awaiter(this, void 0, void 0, function* () { + let path = '/health/time'; + let payload = {}; + const uri = new URL(this.config.endpoint + path); + return yield this.call('get', uri, { + 'content-type': 'application/json', + }, payload); + }) + }; + this.locale = { + /** + * Get User Locale + * + * Get the current user location based on IP. Returns an object with user + * country code, country name, continent name, continent code, ip address and + * suggested currency. You can use the locale header to get the data in a + * supported language. + * + * ([IP Geolocation by DB-IP](https://db-ip.com)) + * + * @throws {AppwriteException} + * @returns {Promise} + */ + get: () => __awaiter(this, void 0, void 0, function* () { + let path = '/locale'; + let payload = {}; + const uri = new URL(this.config.endpoint + path); + return yield this.call('get', uri, { + 'content-type': 'application/json', + }, payload); + }), + /** + * List Continents + * + * List of all continents. You can use the locale header to get the data in a + * supported language. + * + * @throws {AppwriteException} + * @returns {Promise} + */ + getContinents: () => __awaiter(this, void 0, void 0, function* () { + let path = '/locale/continents'; + let payload = {}; + const uri = new URL(this.config.endpoint + path); + return yield this.call('get', uri, { + 'content-type': 'application/json', + }, payload); + }), + /** + * List Countries + * + * List of all countries. You can use the locale header to get the data in a + * supported language. + * + * @throws {AppwriteException} + * @returns {Promise} + */ + getCountries: () => __awaiter(this, void 0, void 0, function* () { + let path = '/locale/countries'; + let payload = {}; + const uri = new URL(this.config.endpoint + path); + return yield this.call('get', uri, { + 'content-type': 'application/json', + }, payload); + }), + /** + * List EU Countries + * + * List of all countries that are currently members of the EU. You can use the + * locale header to get the data in a supported language. + * + * @throws {AppwriteException} + * @returns {Promise} + */ + getCountriesEU: () => __awaiter(this, void 0, void 0, function* () { + let path = '/locale/countries/eu'; + let payload = {}; + const uri = new URL(this.config.endpoint + path); + return yield this.call('get', uri, { + 'content-type': 'application/json', + }, payload); + }), + /** + * List Countries Phone Codes + * + * List of all countries phone codes. You can use the locale header to get the + * data in a supported language. + * + * @throws {AppwriteException} + * @returns {Promise} + */ + getCountriesPhones: () => __awaiter(this, void 0, void 0, function* () { + let path = '/locale/countries/phones'; + let payload = {}; + const uri = new URL(this.config.endpoint + path); + return yield this.call('get', uri, { + 'content-type': 'application/json', + }, payload); + }), + /** + * List Currencies + * + * List of all currencies, including currency symbol, name, plural, and + * decimal digits for all major and minor currencies. You can use the locale + * header to get the data in a supported language. + * + * @throws {AppwriteException} + * @returns {Promise} + */ + getCurrencies: () => __awaiter(this, void 0, void 0, function* () { + let path = '/locale/currencies'; + let payload = {}; + const uri = new URL(this.config.endpoint + path); + return yield this.call('get', uri, { + 'content-type': 'application/json', + }, payload); + }), + /** + * List Languages + * + * List of all languages classified by ISO 639-1 including 2-letter code, name + * in English, and name in the respective language. + * + * @throws {AppwriteException} + * @returns {Promise} + */ + getLanguages: () => __awaiter(this, void 0, void 0, function* () { + let path = '/locale/languages'; + let payload = {}; + const uri = new URL(this.config.endpoint + path); + return yield this.call('get', uri, { + 'content-type': 'application/json', + }, payload); + }) + }; + this.projects = { + /** + * List Projects + * + * + * @param {string} search + * @param {number} limit + * @param {number} offset + * @param {string} orderType + * @throws {AppwriteException} + * @returns {Promise} + */ + list: (search, limit, offset, orderType) => __awaiter(this, void 0, void 0, function* () { + let path = '/projects'; + let payload = {}; + if (typeof search !== 'undefined') { + payload['search'] = search; + } + if (typeof limit !== 'undefined') { + payload['limit'] = limit; + } + if (typeof offset !== 'undefined') { + payload['offset'] = offset; + } + if (typeof orderType !== 'undefined') { + payload['orderType'] = orderType; + } + const uri = new URL(this.config.endpoint + path); + return yield this.call('get', uri, { + 'content-type': 'application/json', + }, payload); + }), + /** + * Create Project + * + * + * @param {string} name + * @param {string} teamId + * @param {string} description + * @param {string} logo + * @param {string} url + * @param {string} legalName + * @param {string} legalCountry + * @param {string} legalState + * @param {string} legalCity + * @param {string} legalAddress + * @param {string} legalTaxId + * @throws {AppwriteException} + * @returns {Promise} + */ + create: (name, teamId, description, logo, url, legalName, legalCountry, legalState, legalCity, legalAddress, legalTaxId) => __awaiter(this, void 0, void 0, function* () { + if (typeof name === 'undefined') { + throw new AppwriteException('Missing required parameter: "name"'); + } + if (typeof teamId === 'undefined') { + throw new AppwriteException('Missing required parameter: "teamId"'); + } + let path = '/projects'; + let payload = {}; + if (typeof name !== 'undefined') { + payload['name'] = name; + } + if (typeof teamId !== 'undefined') { + payload['teamId'] = teamId; + } + if (typeof description !== 'undefined') { + payload['description'] = description; + } + if (typeof logo !== 'undefined') { + payload['logo'] = logo; + } + if (typeof url !== 'undefined') { + payload['url'] = url; + } + if (typeof legalName !== 'undefined') { + payload['legalName'] = legalName; + } + if (typeof legalCountry !== 'undefined') { + payload['legalCountry'] = legalCountry; + } + if (typeof legalState !== 'undefined') { + payload['legalState'] = legalState; + } + if (typeof legalCity !== 'undefined') { + payload['legalCity'] = legalCity; + } + if (typeof legalAddress !== 'undefined') { + payload['legalAddress'] = legalAddress; + } + if (typeof legalTaxId !== 'undefined') { + payload['legalTaxId'] = legalTaxId; + } + const uri = new URL(this.config.endpoint + path); + return yield this.call('post', uri, { + 'content-type': 'application/json', + }, payload); + }), + /** + * Get Project + * + * + * @param {string} projectId + * @throws {AppwriteException} + * @returns {Promise} + */ + get: (projectId) => __awaiter(this, void 0, void 0, function* () { + if (typeof projectId === 'undefined') { + throw new AppwriteException('Missing required parameter: "projectId"'); + } + let path = '/projects/{projectId}'.replace('{projectId}', projectId); + let payload = {}; + const uri = new URL(this.config.endpoint + path); + return yield this.call('get', uri, { + 'content-type': 'application/json', + }, payload); + }), + /** + * Update Project + * + * + * @param {string} projectId + * @param {string} name + * @param {string} description + * @param {string} logo + * @param {string} url + * @param {string} legalName + * @param {string} legalCountry + * @param {string} legalState + * @param {string} legalCity + * @param {string} legalAddress + * @param {string} legalTaxId + * @throws {AppwriteException} + * @returns {Promise} + */ + update: (projectId, name, description, logo, url, legalName, legalCountry, legalState, legalCity, legalAddress, legalTaxId) => __awaiter(this, void 0, void 0, function* () { + if (typeof projectId === 'undefined') { + throw new AppwriteException('Missing required parameter: "projectId"'); + } + if (typeof name === 'undefined') { + throw new AppwriteException('Missing required parameter: "name"'); + } + let path = '/projects/{projectId}'.replace('{projectId}', projectId); + let payload = {}; + if (typeof name !== 'undefined') { + payload['name'] = name; + } + if (typeof description !== 'undefined') { + payload['description'] = description; + } + if (typeof logo !== 'undefined') { + payload['logo'] = logo; + } + if (typeof url !== 'undefined') { + payload['url'] = url; + } + if (typeof legalName !== 'undefined') { + payload['legalName'] = legalName; + } + if (typeof legalCountry !== 'undefined') { + payload['legalCountry'] = legalCountry; + } + if (typeof legalState !== 'undefined') { + payload['legalState'] = legalState; + } + if (typeof legalCity !== 'undefined') { + payload['legalCity'] = legalCity; + } + if (typeof legalAddress !== 'undefined') { + payload['legalAddress'] = legalAddress; + } + if (typeof legalTaxId !== 'undefined') { + payload['legalTaxId'] = legalTaxId; + } + const uri = new URL(this.config.endpoint + path); + return yield this.call('patch', uri, { + 'content-type': 'application/json', + }, payload); + }), + /** + * Delete Project + * + * + * @param {string} projectId + * @param {string} password + * @throws {AppwriteException} + * @returns {Promise} + */ + delete: (projectId, password) => __awaiter(this, void 0, void 0, function* () { + if (typeof projectId === 'undefined') { + throw new AppwriteException('Missing required parameter: "projectId"'); + } + if (typeof password === 'undefined') { + throw new AppwriteException('Missing required parameter: "password"'); + } + let path = '/projects/{projectId}'.replace('{projectId}', projectId); + let payload = {}; + if (typeof password !== 'undefined') { + payload['password'] = password; + } + const uri = new URL(this.config.endpoint + path); + return yield this.call('delete', uri, { + 'content-type': 'application/json', + }, payload); + }), + /** + * Update Project users limit + * + * + * @param {string} projectId + * @param {string} limit + * @throws {AppwriteException} + * @returns {Promise} + */ + updateAuthLimit: (projectId, limit) => __awaiter(this, void 0, void 0, function* () { + if (typeof projectId === 'undefined') { + throw new AppwriteException('Missing required parameter: "projectId"'); + } + if (typeof limit === 'undefined') { + throw new AppwriteException('Missing required parameter: "limit"'); + } + let path = '/projects/{projectId}/auth/limit'.replace('{projectId}', projectId); + let payload = {}; + if (typeof limit !== 'undefined') { + payload['limit'] = limit; + } + const uri = new URL(this.config.endpoint + path); + return yield this.call('patch', uri, { + 'content-type': 'application/json', + }, payload); + }), + /** + * Update Project auth method status. Use this endpoint to enable or disable a given auth method for this project. + * + * + * @param {string} projectId + * @param {string} method + * @param {boolean} status + * @throws {AppwriteException} + * @returns {Promise} + */ + updateAuthStatus: (projectId, method, status) => __awaiter(this, void 0, void 0, function* () { + if (typeof projectId === 'undefined') { + throw new AppwriteException('Missing required parameter: "projectId"'); + } + if (typeof method === 'undefined') { + throw new AppwriteException('Missing required parameter: "method"'); + } + if (typeof status === 'undefined') { + throw new AppwriteException('Missing required parameter: "status"'); + } + let path = '/projects/{projectId}/auth/{method}'.replace('{projectId}', projectId).replace('{method}', method); + let payload = {}; + if (typeof status !== 'undefined') { + payload['status'] = status; + } + const uri = new URL(this.config.endpoint + path); + return yield this.call('patch', uri, { + 'content-type': 'application/json', + }, payload); + }), + /** + * List Domains + * + * + * @param {string} projectId + * @throws {AppwriteException} + * @returns {Promise} + */ + listDomains: (projectId) => __awaiter(this, void 0, void 0, function* () { + if (typeof projectId === 'undefined') { + throw new AppwriteException('Missing required parameter: "projectId"'); + } + let path = '/projects/{projectId}/domains'.replace('{projectId}', projectId); + let payload = {}; + const uri = new URL(this.config.endpoint + path); + return yield this.call('get', uri, { + 'content-type': 'application/json', + }, payload); + }), + /** + * Create Domain + * + * + * @param {string} projectId + * @param {string} domain + * @throws {AppwriteException} + * @returns {Promise} + */ + createDomain: (projectId, domain) => __awaiter(this, void 0, void 0, function* () { + if (typeof projectId === 'undefined') { + throw new AppwriteException('Missing required parameter: "projectId"'); + } + if (typeof domain === 'undefined') { + throw new AppwriteException('Missing required parameter: "domain"'); + } + let path = '/projects/{projectId}/domains'.replace('{projectId}', projectId); + let payload = {}; + if (typeof domain !== 'undefined') { + payload['domain'] = domain; + } + const uri = new URL(this.config.endpoint + path); + return yield this.call('post', uri, { + 'content-type': 'application/json', + }, payload); + }), + /** + * Get Domain + * + * + * @param {string} projectId + * @param {string} domainId + * @throws {AppwriteException} + * @returns {Promise} + */ + getDomain: (projectId, domainId) => __awaiter(this, void 0, void 0, function* () { + if (typeof projectId === 'undefined') { + throw new AppwriteException('Missing required parameter: "projectId"'); + } + if (typeof domainId === 'undefined') { + throw new AppwriteException('Missing required parameter: "domainId"'); + } + let path = '/projects/{projectId}/domains/{domainId}'.replace('{projectId}', projectId).replace('{domainId}', domainId); + let payload = {}; + const uri = new URL(this.config.endpoint + path); + return yield this.call('get', uri, { + 'content-type': 'application/json', + }, payload); + }), + /** + * Delete Domain + * + * + * @param {string} projectId + * @param {string} domainId + * @throws {AppwriteException} + * @returns {Promise} + */ + deleteDomain: (projectId, domainId) => __awaiter(this, void 0, void 0, function* () { + if (typeof projectId === 'undefined') { + throw new AppwriteException('Missing required parameter: "projectId"'); + } + if (typeof domainId === 'undefined') { + throw new AppwriteException('Missing required parameter: "domainId"'); + } + let path = '/projects/{projectId}/domains/{domainId}'.replace('{projectId}', projectId).replace('{domainId}', domainId); + let payload = {}; + const uri = new URL(this.config.endpoint + path); + return yield this.call('delete', uri, { + 'content-type': 'application/json', + }, payload); + }), + /** + * Update Domain Verification Status + * + * + * @param {string} projectId + * @param {string} domainId + * @throws {AppwriteException} + * @returns {Promise} + */ + updateDomainVerification: (projectId, domainId) => __awaiter(this, void 0, void 0, function* () { + if (typeof projectId === 'undefined') { + throw new AppwriteException('Missing required parameter: "projectId"'); + } + if (typeof domainId === 'undefined') { + throw new AppwriteException('Missing required parameter: "domainId"'); + } + let path = '/projects/{projectId}/domains/{domainId}/verification'.replace('{projectId}', projectId).replace('{domainId}', domainId); + let payload = {}; + const uri = new URL(this.config.endpoint + path); + return yield this.call('patch', uri, { + 'content-type': 'application/json', + }, payload); + }), + /** + * List Keys + * + * + * @param {string} projectId + * @throws {AppwriteException} + * @returns {Promise} + */ + listKeys: (projectId) => __awaiter(this, void 0, void 0, function* () { + if (typeof projectId === 'undefined') { + throw new AppwriteException('Missing required parameter: "projectId"'); + } + let path = '/projects/{projectId}/keys'.replace('{projectId}', projectId); + let payload = {}; + const uri = new URL(this.config.endpoint + path); + return yield this.call('get', uri, { + 'content-type': 'application/json', + }, payload); + }), + /** + * Create Key + * + * + * @param {string} projectId + * @param {string} name + * @param {string[]} scopes + * @throws {AppwriteException} + * @returns {Promise} + */ + createKey: (projectId, name, scopes) => __awaiter(this, void 0, void 0, function* () { + if (typeof projectId === 'undefined') { + throw new AppwriteException('Missing required parameter: "projectId"'); + } + if (typeof name === 'undefined') { + throw new AppwriteException('Missing required parameter: "name"'); + } + if (typeof scopes === 'undefined') { + throw new AppwriteException('Missing required parameter: "scopes"'); + } + let path = '/projects/{projectId}/keys'.replace('{projectId}', projectId); + let payload = {}; + if (typeof name !== 'undefined') { + payload['name'] = name; + } + if (typeof scopes !== 'undefined') { + payload['scopes'] = scopes; + } + const uri = new URL(this.config.endpoint + path); + return yield this.call('post', uri, { + 'content-type': 'application/json', + }, payload); + }), + /** + * Get Key + * + * + * @param {string} projectId + * @param {string} keyId + * @throws {AppwriteException} + * @returns {Promise} + */ + getKey: (projectId, keyId) => __awaiter(this, void 0, void 0, function* () { + if (typeof projectId === 'undefined') { + throw new AppwriteException('Missing required parameter: "projectId"'); + } + if (typeof keyId === 'undefined') { + throw new AppwriteException('Missing required parameter: "keyId"'); + } + let path = '/projects/{projectId}/keys/{keyId}'.replace('{projectId}', projectId).replace('{keyId}', keyId); + let payload = {}; + const uri = new URL(this.config.endpoint + path); + return yield this.call('get', uri, { + 'content-type': 'application/json', + }, payload); + }), + /** + * Update Key + * + * + * @param {string} projectId + * @param {string} keyId + * @param {string} name + * @param {string[]} scopes + * @throws {AppwriteException} + * @returns {Promise} + */ + updateKey: (projectId, keyId, name, scopes) => __awaiter(this, void 0, void 0, function* () { + if (typeof projectId === 'undefined') { + throw new AppwriteException('Missing required parameter: "projectId"'); + } + if (typeof keyId === 'undefined') { + throw new AppwriteException('Missing required parameter: "keyId"'); + } + if (typeof name === 'undefined') { + throw new AppwriteException('Missing required parameter: "name"'); + } + if (typeof scopes === 'undefined') { + throw new AppwriteException('Missing required parameter: "scopes"'); + } + let path = '/projects/{projectId}/keys/{keyId}'.replace('{projectId}', projectId).replace('{keyId}', keyId); + let payload = {}; + if (typeof name !== 'undefined') { + payload['name'] = name; + } + if (typeof scopes !== 'undefined') { + payload['scopes'] = scopes; + } + const uri = new URL(this.config.endpoint + path); + return yield this.call('put', uri, { + 'content-type': 'application/json', + }, payload); + }), + /** + * Delete Key + * + * + * @param {string} projectId + * @param {string} keyId + * @throws {AppwriteException} + * @returns {Promise} + */ + deleteKey: (projectId, keyId) => __awaiter(this, void 0, void 0, function* () { + if (typeof projectId === 'undefined') { + throw new AppwriteException('Missing required parameter: "projectId"'); + } + if (typeof keyId === 'undefined') { + throw new AppwriteException('Missing required parameter: "keyId"'); + } + let path = '/projects/{projectId}/keys/{keyId}'.replace('{projectId}', projectId).replace('{keyId}', keyId); + let payload = {}; + const uri = new URL(this.config.endpoint + path); + return yield this.call('delete', uri, { + 'content-type': 'application/json', + }, payload); + }), + /** + * Update Project OAuth2 + * + * + * @param {string} projectId + * @param {string} provider + * @param {string} appId + * @param {string} secret + * @throws {AppwriteException} + * @returns {Promise} + */ + updateOAuth2: (projectId, provider, appId, secret) => __awaiter(this, void 0, void 0, function* () { + if (typeof projectId === 'undefined') { + throw new AppwriteException('Missing required parameter: "projectId"'); + } + if (typeof provider === 'undefined') { + throw new AppwriteException('Missing required parameter: "provider"'); + } + let path = '/projects/{projectId}/oauth2'.replace('{projectId}', projectId); + let payload = {}; + if (typeof provider !== 'undefined') { + payload['provider'] = provider; + } + if (typeof appId !== 'undefined') { + payload['appId'] = appId; + } + if (typeof secret !== 'undefined') { + payload['secret'] = secret; + } + const uri = new URL(this.config.endpoint + path); + return yield this.call('patch', uri, { + 'content-type': 'application/json', + }, payload); + }), + /** + * List Platforms + * + * + * @param {string} projectId + * @throws {AppwriteException} + * @returns {Promise} + */ + listPlatforms: (projectId) => __awaiter(this, void 0, void 0, function* () { + if (typeof projectId === 'undefined') { + throw new AppwriteException('Missing required parameter: "projectId"'); + } + let path = '/projects/{projectId}/platforms'.replace('{projectId}', projectId); + let payload = {}; + const uri = new URL(this.config.endpoint + path); + return yield this.call('get', uri, { + 'content-type': 'application/json', + }, payload); + }), + /** + * Create Platform + * + * + * @param {string} projectId + * @param {string} type + * @param {string} name + * @param {string} key + * @param {string} store + * @param {string} hostname + * @throws {AppwriteException} + * @returns {Promise} + */ + createPlatform: (projectId, type, name, key, store, hostname) => __awaiter(this, void 0, void 0, function* () { + if (typeof projectId === 'undefined') { + throw new AppwriteException('Missing required parameter: "projectId"'); + } + if (typeof type === 'undefined') { + throw new AppwriteException('Missing required parameter: "type"'); + } + if (typeof name === 'undefined') { + throw new AppwriteException('Missing required parameter: "name"'); + } + let path = '/projects/{projectId}/platforms'.replace('{projectId}', projectId); + let payload = {}; + if (typeof type !== 'undefined') { + payload['type'] = type; + } + if (typeof name !== 'undefined') { + payload['name'] = name; + } + if (typeof key !== 'undefined') { + payload['key'] = key; + } + if (typeof store !== 'undefined') { + payload['store'] = store; + } + if (typeof hostname !== 'undefined') { + payload['hostname'] = hostname; + } + const uri = new URL(this.config.endpoint + path); + return yield this.call('post', uri, { + 'content-type': 'application/json', + }, payload); + }), + /** + * Get Platform + * + * + * @param {string} projectId + * @param {string} platformId + * @throws {AppwriteException} + * @returns {Promise} + */ + getPlatform: (projectId, platformId) => __awaiter(this, void 0, void 0, function* () { + if (typeof projectId === 'undefined') { + throw new AppwriteException('Missing required parameter: "projectId"'); + } + if (typeof platformId === 'undefined') { + throw new AppwriteException('Missing required parameter: "platformId"'); + } + let path = '/projects/{projectId}/platforms/{platformId}'.replace('{projectId}', projectId).replace('{platformId}', platformId); + let payload = {}; + const uri = new URL(this.config.endpoint + path); + return yield this.call('get', uri, { + 'content-type': 'application/json', + }, payload); + }), + /** + * Update Platform + * + * + * @param {string} projectId + * @param {string} platformId + * @param {string} name + * @param {string} key + * @param {string} store + * @param {string} hostname + * @throws {AppwriteException} + * @returns {Promise} + */ + updatePlatform: (projectId, platformId, name, key, store, hostname) => __awaiter(this, void 0, void 0, function* () { + if (typeof projectId === 'undefined') { + throw new AppwriteException('Missing required parameter: "projectId"'); + } + if (typeof platformId === 'undefined') { + throw new AppwriteException('Missing required parameter: "platformId"'); + } + if (typeof name === 'undefined') { + throw new AppwriteException('Missing required parameter: "name"'); + } + let path = '/projects/{projectId}/platforms/{platformId}'.replace('{projectId}', projectId).replace('{platformId}', platformId); + let payload = {}; + if (typeof name !== 'undefined') { + payload['name'] = name; + } + if (typeof key !== 'undefined') { + payload['key'] = key; + } + if (typeof store !== 'undefined') { + payload['store'] = store; + } + if (typeof hostname !== 'undefined') { + payload['hostname'] = hostname; + } + const uri = new URL(this.config.endpoint + path); + return yield this.call('put', uri, { + 'content-type': 'application/json', + }, payload); + }), + /** + * Delete Platform + * + * + * @param {string} projectId + * @param {string} platformId + * @throws {AppwriteException} + * @returns {Promise} + */ + deletePlatform: (projectId, platformId) => __awaiter(this, void 0, void 0, function* () { + if (typeof projectId === 'undefined') { + throw new AppwriteException('Missing required parameter: "projectId"'); + } + if (typeof platformId === 'undefined') { + throw new AppwriteException('Missing required parameter: "platformId"'); + } + let path = '/projects/{projectId}/platforms/{platformId}'.replace('{projectId}', projectId).replace('{platformId}', platformId); + let payload = {}; + const uri = new URL(this.config.endpoint + path); + return yield this.call('delete', uri, { + 'content-type': 'application/json', + }, payload); + }), + /** + * List Tasks + * + * + * @param {string} projectId + * @throws {AppwriteException} + * @returns {Promise} + */ + listTasks: (projectId) => __awaiter(this, void 0, void 0, function* () { + if (typeof projectId === 'undefined') { + throw new AppwriteException('Missing required parameter: "projectId"'); + } + let path = '/projects/{projectId}/tasks'.replace('{projectId}', projectId); + let payload = {}; + const uri = new URL(this.config.endpoint + path); + return yield this.call('get', uri, { + 'content-type': 'application/json', + }, payload); + }), + /** + * Create Task + * + * + * @param {string} projectId + * @param {string} name + * @param {string} status + * @param {string} schedule + * @param {boolean} security + * @param {string} httpMethod + * @param {string} httpUrl + * @param {string[]} httpHeaders + * @param {string} httpUser + * @param {string} httpPass + * @throws {AppwriteException} + * @returns {Promise} + */ + createTask: (projectId, name, status, schedule, security, httpMethod, httpUrl, httpHeaders, httpUser, httpPass) => __awaiter(this, void 0, void 0, function* () { + if (typeof projectId === 'undefined') { + throw new AppwriteException('Missing required parameter: "projectId"'); + } + if (typeof name === 'undefined') { + throw new AppwriteException('Missing required parameter: "name"'); + } + if (typeof status === 'undefined') { + throw new AppwriteException('Missing required parameter: "status"'); + } + if (typeof schedule === 'undefined') { + throw new AppwriteException('Missing required parameter: "schedule"'); + } + if (typeof security === 'undefined') { + throw new AppwriteException('Missing required parameter: "security"'); + } + if (typeof httpMethod === 'undefined') { + throw new AppwriteException('Missing required parameter: "httpMethod"'); + } + if (typeof httpUrl === 'undefined') { + throw new AppwriteException('Missing required parameter: "httpUrl"'); + } + let path = '/projects/{projectId}/tasks'.replace('{projectId}', projectId); + let payload = {}; + if (typeof name !== 'undefined') { + payload['name'] = name; + } + if (typeof status !== 'undefined') { + payload['status'] = status; + } + if (typeof schedule !== 'undefined') { + payload['schedule'] = schedule; + } + if (typeof security !== 'undefined') { + payload['security'] = security; + } + if (typeof httpMethod !== 'undefined') { + payload['httpMethod'] = httpMethod; + } + if (typeof httpUrl !== 'undefined') { + payload['httpUrl'] = httpUrl; + } + if (typeof httpHeaders !== 'undefined') { + payload['httpHeaders'] = httpHeaders; + } + if (typeof httpUser !== 'undefined') { + payload['httpUser'] = httpUser; + } + if (typeof httpPass !== 'undefined') { + payload['httpPass'] = httpPass; + } + const uri = new URL(this.config.endpoint + path); + return yield this.call('post', uri, { + 'content-type': 'application/json', + }, payload); + }), + /** + * Get Task + * + * + * @param {string} projectId + * @param {string} taskId + * @throws {AppwriteException} + * @returns {Promise} + */ + getTask: (projectId, taskId) => __awaiter(this, void 0, void 0, function* () { + if (typeof projectId === 'undefined') { + throw new AppwriteException('Missing required parameter: "projectId"'); + } + if (typeof taskId === 'undefined') { + throw new AppwriteException('Missing required parameter: "taskId"'); + } + let path = '/projects/{projectId}/tasks/{taskId}'.replace('{projectId}', projectId).replace('{taskId}', taskId); + let payload = {}; + const uri = new URL(this.config.endpoint + path); + return yield this.call('get', uri, { + 'content-type': 'application/json', + }, payload); + }), + /** + * Update Task + * + * + * @param {string} projectId + * @param {string} taskId + * @param {string} name + * @param {string} status + * @param {string} schedule + * @param {boolean} security + * @param {string} httpMethod + * @param {string} httpUrl + * @param {string[]} httpHeaders + * @param {string} httpUser + * @param {string} httpPass + * @throws {AppwriteException} + * @returns {Promise} + */ + updateTask: (projectId, taskId, name, status, schedule, security, httpMethod, httpUrl, httpHeaders, httpUser, httpPass) => __awaiter(this, void 0, void 0, function* () { + if (typeof projectId === 'undefined') { + throw new AppwriteException('Missing required parameter: "projectId"'); + } + if (typeof taskId === 'undefined') { + throw new AppwriteException('Missing required parameter: "taskId"'); + } + if (typeof name === 'undefined') { + throw new AppwriteException('Missing required parameter: "name"'); + } + if (typeof status === 'undefined') { + throw new AppwriteException('Missing required parameter: "status"'); + } + if (typeof schedule === 'undefined') { + throw new AppwriteException('Missing required parameter: "schedule"'); + } + if (typeof security === 'undefined') { + throw new AppwriteException('Missing required parameter: "security"'); + } + if (typeof httpMethod === 'undefined') { + throw new AppwriteException('Missing required parameter: "httpMethod"'); + } + if (typeof httpUrl === 'undefined') { + throw new AppwriteException('Missing required parameter: "httpUrl"'); + } + let path = '/projects/{projectId}/tasks/{taskId}'.replace('{projectId}', projectId).replace('{taskId}', taskId); + let payload = {}; + if (typeof name !== 'undefined') { + payload['name'] = name; + } + if (typeof status !== 'undefined') { + payload['status'] = status; + } + if (typeof schedule !== 'undefined') { + payload['schedule'] = schedule; + } + if (typeof security !== 'undefined') { + payload['security'] = security; + } + if (typeof httpMethod !== 'undefined') { + payload['httpMethod'] = httpMethod; + } + if (typeof httpUrl !== 'undefined') { + payload['httpUrl'] = httpUrl; + } + if (typeof httpHeaders !== 'undefined') { + payload['httpHeaders'] = httpHeaders; + } + if (typeof httpUser !== 'undefined') { + payload['httpUser'] = httpUser; + } + if (typeof httpPass !== 'undefined') { + payload['httpPass'] = httpPass; + } + const uri = new URL(this.config.endpoint + path); + return yield this.call('put', uri, { + 'content-type': 'application/json', + }, payload); + }), + /** + * Delete Task + * + * + * @param {string} projectId + * @param {string} taskId + * @throws {AppwriteException} + * @returns {Promise} + */ + deleteTask: (projectId, taskId) => __awaiter(this, void 0, void 0, function* () { + if (typeof projectId === 'undefined') { + throw new AppwriteException('Missing required parameter: "projectId"'); + } + if (typeof taskId === 'undefined') { + throw new AppwriteException('Missing required parameter: "taskId"'); + } + let path = '/projects/{projectId}/tasks/{taskId}'.replace('{projectId}', projectId).replace('{taskId}', taskId); + let payload = {}; + const uri = new URL(this.config.endpoint + path); + return yield this.call('delete', uri, { + 'content-type': 'application/json', + }, payload); + }), + /** + * Get Project + * + * + * @param {string} projectId + * @param {string} range + * @throws {AppwriteException} + * @returns {Promise} + */ + getUsage: (projectId, range) => __awaiter(this, void 0, void 0, function* () { + if (typeof projectId === 'undefined') { + throw new AppwriteException('Missing required parameter: "projectId"'); + } + let path = '/projects/{projectId}/usage'.replace('{projectId}', projectId); + let payload = {}; + if (typeof range !== 'undefined') { + payload['range'] = range; + } + const uri = new URL(this.config.endpoint + path); + return yield this.call('get', uri, { + 'content-type': 'application/json', + }, payload); + }), + /** + * List Webhooks + * + * + * @param {string} projectId + * @throws {AppwriteException} + * @returns {Promise} + */ + listWebhooks: (projectId) => __awaiter(this, void 0, void 0, function* () { + if (typeof projectId === 'undefined') { + throw new AppwriteException('Missing required parameter: "projectId"'); + } + let path = '/projects/{projectId}/webhooks'.replace('{projectId}', projectId); + let payload = {}; + const uri = new URL(this.config.endpoint + path); + return yield this.call('get', uri, { + 'content-type': 'application/json', + }, payload); + }), + /** + * Create Webhook + * + * + * @param {string} projectId + * @param {string} name + * @param {string[]} events + * @param {string} url + * @param {boolean} security + * @param {string} httpUser + * @param {string} httpPass + * @throws {AppwriteException} + * @returns {Promise} + */ + createWebhook: (projectId, name, events, url, security, httpUser, httpPass) => __awaiter(this, void 0, void 0, function* () { + if (typeof projectId === 'undefined') { + throw new AppwriteException('Missing required parameter: "projectId"'); + } + if (typeof name === 'undefined') { + throw new AppwriteException('Missing required parameter: "name"'); + } + if (typeof events === 'undefined') { + throw new AppwriteException('Missing required parameter: "events"'); + } + if (typeof url === 'undefined') { + throw new AppwriteException('Missing required parameter: "url"'); + } + if (typeof security === 'undefined') { + throw new AppwriteException('Missing required parameter: "security"'); + } + let path = '/projects/{projectId}/webhooks'.replace('{projectId}', projectId); + let payload = {}; + if (typeof name !== 'undefined') { + payload['name'] = name; + } + if (typeof events !== 'undefined') { + payload['events'] = events; + } + if (typeof url !== 'undefined') { + payload['url'] = url; + } + if (typeof security !== 'undefined') { + payload['security'] = security; + } + if (typeof httpUser !== 'undefined') { + payload['httpUser'] = httpUser; + } + if (typeof httpPass !== 'undefined') { + payload['httpPass'] = httpPass; + } + const uri = new URL(this.config.endpoint + path); + return yield this.call('post', uri, { + 'content-type': 'application/json', + }, payload); + }), + /** + * Get Webhook + * + * + * @param {string} projectId + * @param {string} webhookId + * @throws {AppwriteException} + * @returns {Promise} + */ + getWebhook: (projectId, webhookId) => __awaiter(this, void 0, void 0, function* () { + if (typeof projectId === 'undefined') { + throw new AppwriteException('Missing required parameter: "projectId"'); + } + if (typeof webhookId === 'undefined') { + throw new AppwriteException('Missing required parameter: "webhookId"'); + } + let path = '/projects/{projectId}/webhooks/{webhookId}'.replace('{projectId}', projectId).replace('{webhookId}', webhookId); + let payload = {}; + const uri = new URL(this.config.endpoint + path); + return yield this.call('get', uri, { + 'content-type': 'application/json', + }, payload); + }), + /** + * Update Webhook + * + * + * @param {string} projectId + * @param {string} webhookId + * @param {string} name + * @param {string[]} events + * @param {string} url + * @param {boolean} security + * @param {string} httpUser + * @param {string} httpPass + * @throws {AppwriteException} + * @returns {Promise} + */ + updateWebhook: (projectId, webhookId, name, events, url, security, httpUser, httpPass) => __awaiter(this, void 0, void 0, function* () { + if (typeof projectId === 'undefined') { + throw new AppwriteException('Missing required parameter: "projectId"'); + } + if (typeof webhookId === 'undefined') { + throw new AppwriteException('Missing required parameter: "webhookId"'); + } + if (typeof name === 'undefined') { + throw new AppwriteException('Missing required parameter: "name"'); + } + if (typeof events === 'undefined') { + throw new AppwriteException('Missing required parameter: "events"'); + } + if (typeof url === 'undefined') { + throw new AppwriteException('Missing required parameter: "url"'); + } + if (typeof security === 'undefined') { + throw new AppwriteException('Missing required parameter: "security"'); + } + let path = '/projects/{projectId}/webhooks/{webhookId}'.replace('{projectId}', projectId).replace('{webhookId}', webhookId); + let payload = {}; + if (typeof name !== 'undefined') { + payload['name'] = name; + } + if (typeof events !== 'undefined') { + payload['events'] = events; + } + if (typeof url !== 'undefined') { + payload['url'] = url; + } + if (typeof security !== 'undefined') { + payload['security'] = security; + } + if (typeof httpUser !== 'undefined') { + payload['httpUser'] = httpUser; + } + if (typeof httpPass !== 'undefined') { + payload['httpPass'] = httpPass; + } + const uri = new URL(this.config.endpoint + path); + return yield this.call('put', uri, { + 'content-type': 'application/json', + }, payload); + }), + /** + * Delete Webhook + * + * + * @param {string} projectId + * @param {string} webhookId + * @throws {AppwriteException} + * @returns {Promise} + */ + deleteWebhook: (projectId, webhookId) => __awaiter(this, void 0, void 0, function* () { + if (typeof projectId === 'undefined') { + throw new AppwriteException('Missing required parameter: "projectId"'); + } + if (typeof webhookId === 'undefined') { + throw new AppwriteException('Missing required parameter: "webhookId"'); + } + let path = '/projects/{projectId}/webhooks/{webhookId}'.replace('{projectId}', projectId).replace('{webhookId}', webhookId); + let payload = {}; + const uri = new URL(this.config.endpoint + path); + return yield this.call('delete', uri, { + 'content-type': 'application/json', + }, payload); + }) + }; + this.storage = { + /** + * List Files + * + * Get a list of all the user files. You can use the query params to filter + * your results. On admin mode, this endpoint will return a list of all of the + * project's files. [Learn more about different API modes](/docs/admin). + * + * @param {string} search + * @param {number} limit + * @param {number} offset + * @param {string} orderType + * @throws {AppwriteException} + * @returns {Promise} + */ + listFiles: (search, limit, offset, orderType) => __awaiter(this, void 0, void 0, function* () { + let path = '/storage/files'; + let payload = {}; + if (typeof search !== 'undefined') { + payload['search'] = search; + } + if (typeof limit !== 'undefined') { + payload['limit'] = limit; + } + if (typeof offset !== 'undefined') { + payload['offset'] = offset; + } + if (typeof orderType !== 'undefined') { + payload['orderType'] = orderType; + } + const uri = new URL(this.config.endpoint + path); + return yield this.call('get', uri, { + 'content-type': 'application/json', + }, payload); + }), + /** + * Create File + * + * Create a new file. The user who creates the file will automatically be + * assigned to read and write access unless he has passed custom values for + * read and write arguments. + * + * @param {File} file + * @param {string[]} read + * @param {string[]} write + * @throws {AppwriteException} + * @returns {Promise} + */ + createFile: (file, read, write) => __awaiter(this, void 0, void 0, function* () { + if (typeof file === 'undefined') { + throw new AppwriteException('Missing required parameter: "file"'); + } + let path = '/storage/files'; + let payload = {}; + if (typeof file !== 'undefined') { + payload['file'] = file; + } + if (typeof read !== 'undefined') { + payload['read'] = read; + } + if (typeof write !== 'undefined') { + payload['write'] = write; + } + const uri = new URL(this.config.endpoint + path); + return yield this.call('post', uri, { + 'content-type': 'multipart/form-data', + }, payload); + }), + /** + * Get File + * + * Get a file by its unique ID. This endpoint response returns a JSON object + * with the file metadata. + * + * @param {string} fileId + * @throws {AppwriteException} + * @returns {Promise} + */ + getFile: (fileId) => __awaiter(this, void 0, void 0, function* () { + if (typeof fileId === 'undefined') { + throw new AppwriteException('Missing required parameter: "fileId"'); + } + let path = '/storage/files/{fileId}'.replace('{fileId}', fileId); + let payload = {}; + const uri = new URL(this.config.endpoint + path); + return yield this.call('get', uri, { + 'content-type': 'application/json', + }, payload); + }), + /** + * Update File + * + * Update a file by its unique ID. Only users with write permissions have + * access to update this resource. + * + * @param {string} fileId + * @param {string[]} read + * @param {string[]} write + * @throws {AppwriteException} + * @returns {Promise} + */ + updateFile: (fileId, read, write) => __awaiter(this, void 0, void 0, function* () { + if (typeof fileId === 'undefined') { + throw new AppwriteException('Missing required parameter: "fileId"'); + } + if (typeof read === 'undefined') { + throw new AppwriteException('Missing required parameter: "read"'); + } + if (typeof write === 'undefined') { + throw new AppwriteException('Missing required parameter: "write"'); + } + let path = '/storage/files/{fileId}'.replace('{fileId}', fileId); + let payload = {}; + if (typeof read !== 'undefined') { + payload['read'] = read; + } + if (typeof write !== 'undefined') { + payload['write'] = write; + } + const uri = new URL(this.config.endpoint + path); + return yield this.call('put', uri, { + 'content-type': 'application/json', + }, payload); + }), + /** + * Delete File + * + * Delete a file by its unique ID. Only users with write permissions have + * access to delete this resource. + * + * @param {string} fileId + * @throws {AppwriteException} + * @returns {Promise} + */ + deleteFile: (fileId) => __awaiter(this, void 0, void 0, function* () { + if (typeof fileId === 'undefined') { + throw new AppwriteException('Missing required parameter: "fileId"'); + } + let path = '/storage/files/{fileId}'.replace('{fileId}', fileId); + let payload = {}; + const uri = new URL(this.config.endpoint + path); + return yield this.call('delete', uri, { + 'content-type': 'application/json', + }, payload); + }), + /** + * Get File for Download + * + * Get a file content by its unique ID. The endpoint response return with a + * 'Content-Disposition: attachment' header that tells the browser to start + * downloading the file to user downloads directory. + * + * @param {string} fileId + * @throws {AppwriteException} + * @returns {URL} + */ + getFileDownload: (fileId) => { + if (typeof fileId === 'undefined') { + throw new AppwriteException('Missing required parameter: "fileId"'); + } + let path = '/storage/files/{fileId}/download'.replace('{fileId}', fileId); + let payload = {}; + const uri = new URL(this.config.endpoint + path); + payload['project'] = this.config.project; + for (const [key, value] of Object.entries(this.flatten(payload))) { + uri.searchParams.append(key, value); + } + return uri; + }, + /** + * Get File Preview + * + * Get a file preview image. Currently, this method supports preview for image + * files (jpg, png, and gif), other supported formats, like pdf, docs, slides, + * and spreadsheets, will return the file icon image. You can also pass query + * string arguments for cutting and resizing your preview image. + * + * @param {string} fileId + * @param {number} width + * @param {number} height + * @param {number} quality + * @param {number} borderWidth + * @param {string} borderColor + * @param {number} borderRadius + * @param {number} opacity + * @param {number} rotation + * @param {string} background + * @param {string} output + * @throws {AppwriteException} + * @returns {URL} + */ + getFilePreview: (fileId, width, height, quality, borderWidth, borderColor, borderRadius, opacity, rotation, background, output) => { + if (typeof fileId === 'undefined') { + throw new AppwriteException('Missing required parameter: "fileId"'); + } + let path = '/storage/files/{fileId}/preview'.replace('{fileId}', fileId); + let payload = {}; + if (typeof width !== 'undefined') { + payload['width'] = width; + } + if (typeof height !== 'undefined') { + payload['height'] = height; + } + if (typeof quality !== 'undefined') { + payload['quality'] = quality; + } + if (typeof borderWidth !== 'undefined') { + payload['borderWidth'] = borderWidth; + } + if (typeof borderColor !== 'undefined') { + payload['borderColor'] = borderColor; + } + if (typeof borderRadius !== 'undefined') { + payload['borderRadius'] = borderRadius; + } + if (typeof opacity !== 'undefined') { + payload['opacity'] = opacity; + } + if (typeof rotation !== 'undefined') { + payload['rotation'] = rotation; + } + if (typeof background !== 'undefined') { + payload['background'] = background; + } + if (typeof output !== 'undefined') { + payload['output'] = output; + } + const uri = new URL(this.config.endpoint + path); + payload['project'] = this.config.project; + for (const [key, value] of Object.entries(this.flatten(payload))) { + uri.searchParams.append(key, value); + } + return uri; + }, + /** + * Get File for View + * + * Get a file content by its unique ID. This endpoint is similar to the + * download method but returns with no 'Content-Disposition: attachment' + * header. + * + * @param {string} fileId + * @throws {AppwriteException} + * @returns {URL} + */ + getFileView: (fileId) => { + if (typeof fileId === 'undefined') { + throw new AppwriteException('Missing required parameter: "fileId"'); + } + let path = '/storage/files/{fileId}/view'.replace('{fileId}', fileId); + let payload = {}; + const uri = new URL(this.config.endpoint + path); + payload['project'] = this.config.project; + for (const [key, value] of Object.entries(this.flatten(payload))) { + uri.searchParams.append(key, value); + } + return uri; + } + }; + this.teams = { + /** + * List Teams + * + * Get a list of all the current user teams. You can use the query params to + * filter your results. On admin mode, this endpoint will return a list of all + * of the project's teams. [Learn more about different API + * modes](/docs/admin). + * + * @param {string} search + * @param {number} limit + * @param {number} offset + * @param {string} orderType + * @throws {AppwriteException} + * @returns {Promise} + */ + list: (search, limit, offset, orderType) => __awaiter(this, void 0, void 0, function* () { + let path = '/teams'; + let payload = {}; + if (typeof search !== 'undefined') { + payload['search'] = search; + } + if (typeof limit !== 'undefined') { + payload['limit'] = limit; + } + if (typeof offset !== 'undefined') { + payload['offset'] = offset; + } + if (typeof orderType !== 'undefined') { + payload['orderType'] = orderType; + } + const uri = new URL(this.config.endpoint + path); + return yield this.call('get', uri, { + 'content-type': 'application/json', + }, payload); + }), + /** + * Create Team + * + * Create a new team. The user who creates the team will automatically be + * assigned as the owner of the team. The team owner can invite new members, + * who will be able add new owners and update or delete the team from your + * project. + * + * @param {string} name + * @param {string[]} roles + * @throws {AppwriteException} + * @returns {Promise} + */ + create: (name, roles) => __awaiter(this, void 0, void 0, function* () { + if (typeof name === 'undefined') { + throw new AppwriteException('Missing required parameter: "name"'); + } + let path = '/teams'; + let payload = {}; + if (typeof name !== 'undefined') { + payload['name'] = name; + } + if (typeof roles !== 'undefined') { + payload['roles'] = roles; + } + const uri = new URL(this.config.endpoint + path); + return yield this.call('post', uri, { + 'content-type': 'application/json', + }, payload); + }), + /** + * Get Team + * + * Get a team by its unique ID. All team members have read access for this + * resource. + * + * @param {string} teamId + * @throws {AppwriteException} + * @returns {Promise} + */ + get: (teamId) => __awaiter(this, void 0, void 0, function* () { + if (typeof teamId === 'undefined') { + throw new AppwriteException('Missing required parameter: "teamId"'); + } + let path = '/teams/{teamId}'.replace('{teamId}', teamId); + let payload = {}; + const uri = new URL(this.config.endpoint + path); + return yield this.call('get', uri, { + 'content-type': 'application/json', + }, payload); + }), + /** + * Update Team + * + * Update a team by its unique ID. Only team owners have write access for this + * resource. + * + * @param {string} teamId + * @param {string} name + * @throws {AppwriteException} + * @returns {Promise} + */ + update: (teamId, name) => __awaiter(this, void 0, void 0, function* () { + if (typeof teamId === 'undefined') { + throw new AppwriteException('Missing required parameter: "teamId"'); + } + if (typeof name === 'undefined') { + throw new AppwriteException('Missing required parameter: "name"'); + } + let path = '/teams/{teamId}'.replace('{teamId}', teamId); + let payload = {}; + if (typeof name !== 'undefined') { + payload['name'] = name; + } + const uri = new URL(this.config.endpoint + path); + return yield this.call('put', uri, { + 'content-type': 'application/json', + }, payload); + }), + /** + * Delete Team + * + * Delete a team by its unique ID. Only team owners have write access for this + * resource. + * + * @param {string} teamId + * @throws {AppwriteException} + * @returns {Promise} + */ + delete: (teamId) => __awaiter(this, void 0, void 0, function* () { + if (typeof teamId === 'undefined') { + throw new AppwriteException('Missing required parameter: "teamId"'); + } + let path = '/teams/{teamId}'.replace('{teamId}', teamId); + let payload = {}; + const uri = new URL(this.config.endpoint + path); + return yield this.call('delete', uri, { + 'content-type': 'application/json', + }, payload); + }), + /** + * Get Team Memberships + * + * Get a team members by the team unique ID. All team members have read access + * for this list of resources. + * + * @param {string} teamId + * @param {string} search + * @param {number} limit + * @param {number} offset + * @param {string} orderType + * @throws {AppwriteException} + * @returns {Promise} + */ + getMemberships: (teamId, search, limit, offset, orderType) => __awaiter(this, void 0, void 0, function* () { + if (typeof teamId === 'undefined') { + throw new AppwriteException('Missing required parameter: "teamId"'); + } + let path = '/teams/{teamId}/memberships'.replace('{teamId}', teamId); + let payload = {}; + if (typeof search !== 'undefined') { + payload['search'] = search; + } + if (typeof limit !== 'undefined') { + payload['limit'] = limit; + } + if (typeof offset !== 'undefined') { + payload['offset'] = offset; + } + if (typeof orderType !== 'undefined') { + payload['orderType'] = orderType; + } + const uri = new URL(this.config.endpoint + path); + return yield this.call('get', uri, { + 'content-type': 'application/json', + }, payload); + }), + /** + * Create Team Membership + * + * Use this endpoint to invite a new member to join your team. An email with a + * link to join the team will be sent to the new member email address if the + * member doesn't exist in the project it will be created automatically. + * + * Use the 'URL' parameter to redirect the user from the invitation email back + * to your app. When the user is redirected, use the [Update Team Membership + * Status](/docs/client/teams#teamsUpdateMembershipStatus) endpoint to allow + * the user to accept the invitation to the team. + * + * Please note that in order to avoid a [Redirect + * Attacks](https://github.com/OWASP/CheatSheetSeries/blob/master/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.md) + * the only valid redirect URL's are the once from domains you have set when + * added your platforms in the console interface. + * + * @param {string} teamId + * @param {string} email + * @param {string[]} roles + * @param {string} url + * @param {string} name + * @throws {AppwriteException} + * @returns {Promise} + */ + createMembership: (teamId, email, roles, url, name) => __awaiter(this, void 0, void 0, function* () { + if (typeof teamId === 'undefined') { + throw new AppwriteException('Missing required parameter: "teamId"'); + } + if (typeof email === 'undefined') { + throw new AppwriteException('Missing required parameter: "email"'); + } + if (typeof roles === 'undefined') { + throw new AppwriteException('Missing required parameter: "roles"'); + } + if (typeof url === 'undefined') { + throw new AppwriteException('Missing required parameter: "url"'); + } + let path = '/teams/{teamId}/memberships'.replace('{teamId}', teamId); + let payload = {}; + if (typeof email !== 'undefined') { + payload['email'] = email; + } + if (typeof name !== 'undefined') { + payload['name'] = name; + } + if (typeof roles !== 'undefined') { + payload['roles'] = roles; + } + if (typeof url !== 'undefined') { + payload['url'] = url; + } + const uri = new URL(this.config.endpoint + path); + return yield this.call('post', uri, { + 'content-type': 'application/json', + }, payload); + }), + /** + * Update Membership Roles + * + * + * @param {string} teamId + * @param {string} membershipId + * @param {string[]} roles + * @throws {AppwriteException} + * @returns {Promise} + */ + updateMembershipRoles: (teamId, membershipId, roles) => __awaiter(this, void 0, void 0, function* () { + if (typeof teamId === 'undefined') { + throw new AppwriteException('Missing required parameter: "teamId"'); + } + if (typeof membershipId === 'undefined') { + throw new AppwriteException('Missing required parameter: "membershipId"'); + } + if (typeof roles === 'undefined') { + throw new AppwriteException('Missing required parameter: "roles"'); + } + let path = '/teams/{teamId}/memberships/{membershipId}'.replace('{teamId}', teamId).replace('{membershipId}', membershipId); + let payload = {}; + if (typeof roles !== 'undefined') { + payload['roles'] = roles; + } + const uri = new URL(this.config.endpoint + path); + return yield this.call('patch', uri, { + 'content-type': 'application/json', + }, payload); + }), + /** + * Delete Team Membership + * + * This endpoint allows a user to leave a team or for a team owner to delete + * the membership of any other team member. You can also use this endpoint to + * delete a user membership even if it is not accepted. + * + * @param {string} teamId + * @param {string} membershipId + * @throws {AppwriteException} + * @returns {Promise} + */ + deleteMembership: (teamId, membershipId) => __awaiter(this, void 0, void 0, function* () { + if (typeof teamId === 'undefined') { + throw new AppwriteException('Missing required parameter: "teamId"'); + } + if (typeof membershipId === 'undefined') { + throw new AppwriteException('Missing required parameter: "membershipId"'); + } + let path = '/teams/{teamId}/memberships/{membershipId}'.replace('{teamId}', teamId).replace('{membershipId}', membershipId); + let payload = {}; + const uri = new URL(this.config.endpoint + path); + return yield this.call('delete', uri, { + 'content-type': 'application/json', + }, payload); + }), + /** + * Update Team Membership Status + * + * Use this endpoint to allow a user to accept an invitation to join a team + * after being redirected back to your app from the invitation email recieved + * by the user. + * + * @param {string} teamId + * @param {string} membershipId + * @param {string} userId + * @param {string} secret + * @throws {AppwriteException} + * @returns {Promise} + */ + updateMembershipStatus: (teamId, membershipId, userId, secret) => __awaiter(this, void 0, void 0, function* () { + if (typeof teamId === 'undefined') { + throw new AppwriteException('Missing required parameter: "teamId"'); + } + if (typeof membershipId === 'undefined') { + throw new AppwriteException('Missing required parameter: "membershipId"'); + } + if (typeof userId === 'undefined') { + throw new AppwriteException('Missing required parameter: "userId"'); + } + if (typeof secret === 'undefined') { + throw new AppwriteException('Missing required parameter: "secret"'); + } + let path = '/teams/{teamId}/memberships/{membershipId}/status'.replace('{teamId}', teamId).replace('{membershipId}', membershipId); + let payload = {}; + if (typeof userId !== 'undefined') { + payload['userId'] = userId; + } + if (typeof secret !== 'undefined') { + payload['secret'] = secret; + } + const uri = new URL(this.config.endpoint + path); + return yield this.call('patch', uri, { + 'content-type': 'application/json', + }, payload); + }) + }; + this.users = { + /** + * List Users + * + * Get a list of all the project's users. You can use the query params to + * filter your results. + * + * @param {string} search + * @param {number} limit + * @param {number} offset + * @param {string} orderType + * @throws {AppwriteException} + * @returns {Promise} + */ + list: (search, limit, offset, orderType) => __awaiter(this, void 0, void 0, function* () { + let path = '/users'; + let payload = {}; + if (typeof search !== 'undefined') { + payload['search'] = search; + } + if (typeof limit !== 'undefined') { + payload['limit'] = limit; + } + if (typeof offset !== 'undefined') { + payload['offset'] = offset; + } + if (typeof orderType !== 'undefined') { + payload['orderType'] = orderType; + } + const uri = new URL(this.config.endpoint + path); + return yield this.call('get', uri, { + 'content-type': 'application/json', + }, payload); + }), + /** + * Create User + * + * Create a new user. + * + * @param {string} email + * @param {string} password + * @param {string} name + * @throws {AppwriteException} + * @returns {Promise} + */ + 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 = '/users'; + let payload = {}; + if (typeof email !== 'undefined') { + payload['email'] = email; + } + if (typeof password !== 'undefined') { + payload['password'] = password; + } + if (typeof name !== 'undefined') { + payload['name'] = name; + } + const uri = new URL(this.config.endpoint + path); + return yield this.call('post', uri, { + 'content-type': 'application/json', + }, payload); + }), + /** + * Get User + * + * Get a user by its unique ID. + * + * @param {string} userId + * @throws {AppwriteException} + * @returns {Promise} + */ + get: (userId) => __awaiter(this, void 0, void 0, function* () { + if (typeof userId === 'undefined') { + throw new AppwriteException('Missing required parameter: "userId"'); + } + let path = '/users/{userId}'.replace('{userId}', userId); + let payload = {}; + const uri = new URL(this.config.endpoint + path); + return yield this.call('get', uri, { + 'content-type': 'application/json', + }, payload); + }), + /** + * Delete User + * + * Delete a user by its unique ID. + * + * @param {string} userId + * @throws {AppwriteException} + * @returns {Promise} + */ + delete: (userId) => __awaiter(this, void 0, void 0, function* () { + if (typeof userId === 'undefined') { + throw new AppwriteException('Missing required parameter: "userId"'); + } + let path = '/users/{userId}'.replace('{userId}', userId); + let payload = {}; + const uri = new URL(this.config.endpoint + path); + return yield this.call('delete', uri, { + 'content-type': 'application/json', + }, payload); + }), + /** + * Get User Logs + * + * Get a user activity logs list by its unique ID. + * + * @param {string} userId + * @throws {AppwriteException} + * @returns {Promise} + */ + getLogs: (userId) => __awaiter(this, void 0, void 0, function* () { + if (typeof userId === 'undefined') { + throw new AppwriteException('Missing required parameter: "userId"'); + } + let path = '/users/{userId}/logs'.replace('{userId}', userId); + let payload = {}; + const uri = new URL(this.config.endpoint + path); + return yield this.call('get', uri, { + 'content-type': 'application/json', + }, payload); + }), + /** + * Get User Preferences + * + * Get the user preferences by its unique ID. + * + * @param {string} userId + * @throws {AppwriteException} + * @returns {Promise} + */ + getPrefs: (userId) => __awaiter(this, void 0, void 0, function* () { + if (typeof userId === 'undefined') { + throw new AppwriteException('Missing required parameter: "userId"'); + } + let path = '/users/{userId}/prefs'.replace('{userId}', userId); + let payload = {}; + const uri = new URL(this.config.endpoint + path); + return yield this.call('get', uri, { + 'content-type': 'application/json', + }, payload); + }), + /** + * Update User Preferences + * + * Update the user preferences by its unique ID. You can pass only the + * specific settings you wish to update. + * + * @param {string} userId + * @param {object} prefs + * @throws {AppwriteException} + * @returns {Promise} + */ + updatePrefs: (userId, prefs) => __awaiter(this, void 0, void 0, function* () { + if (typeof userId === 'undefined') { + throw new AppwriteException('Missing required parameter: "userId"'); + } + if (typeof prefs === 'undefined') { + throw new AppwriteException('Missing required parameter: "prefs"'); + } + let path = '/users/{userId}/prefs'.replace('{userId}', userId); + let payload = {}; + if (typeof prefs !== 'undefined') { + payload['prefs'] = prefs; + } + const uri = new URL(this.config.endpoint + path); + return yield this.call('patch', uri, { + 'content-type': 'application/json', + }, payload); + }), + /** + * Get User Sessions + * + * Get the user sessions list by its unique ID. + * + * @param {string} userId + * @throws {AppwriteException} + * @returns {Promise} + */ + getSessions: (userId) => __awaiter(this, void 0, void 0, function* () { + if (typeof userId === 'undefined') { + throw new AppwriteException('Missing required parameter: "userId"'); + } + let path = '/users/{userId}/sessions'.replace('{userId}', userId); + let payload = {}; + const uri = new URL(this.config.endpoint + path); + return yield this.call('get', uri, { + 'content-type': 'application/json', + }, payload); + }), + /** + * Delete User Sessions + * + * Delete all user's sessions by using the user's unique ID. + * + * @param {string} userId + * @throws {AppwriteException} + * @returns {Promise} + */ + deleteSessions: (userId) => __awaiter(this, void 0, void 0, function* () { + if (typeof userId === 'undefined') { + throw new AppwriteException('Missing required parameter: "userId"'); + } + let path = '/users/{userId}/sessions'.replace('{userId}', userId); + let payload = {}; + const uri = new URL(this.config.endpoint + path); + return yield this.call('delete', uri, { + 'content-type': 'application/json', + }, payload); + }), + /** + * Delete User Session + * + * Delete a user sessions by its unique ID. + * + * @param {string} userId + * @param {string} sessionId + * @throws {AppwriteException} + * @returns {Promise} + */ + deleteSession: (userId, sessionId) => __awaiter(this, void 0, void 0, function* () { + if (typeof userId === 'undefined') { + throw new AppwriteException('Missing required parameter: "userId"'); + } + if (typeof sessionId === 'undefined') { + throw new AppwriteException('Missing required parameter: "sessionId"'); + } + let path = '/users/{userId}/sessions/{sessionId}'.replace('{userId}', userId).replace('{sessionId}', sessionId); + let payload = {}; + const uri = new URL(this.config.endpoint + path); + return yield this.call('delete', uri, { + 'content-type': 'application/json', + }, payload); + }), + /** + * Update User Status + * + * Update the user status by its unique ID. + * + * @param {string} userId + * @param {number} status + * @throws {AppwriteException} + * @returns {Promise} + */ + updateStatus: (userId, status) => __awaiter(this, void 0, void 0, function* () { + if (typeof userId === 'undefined') { + throw new AppwriteException('Missing required parameter: "userId"'); + } + if (typeof status === 'undefined') { + throw new AppwriteException('Missing required parameter: "status"'); + } + let path = '/users/{userId}/status'.replace('{userId}', userId); + let payload = {}; + if (typeof status !== 'undefined') { + payload['status'] = status; + } + const uri = new URL(this.config.endpoint + path); + return yield this.call('patch', uri, { + 'content-type': 'application/json', + }, payload); + }) + }; + } /** + * Set Endpoint + * + * Your project endpoint + * * @param {string} endpoint + * * @returns {this} */ - let setEndpoint = function(endpoint) { - config.endpoint = endpoint; - + 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; + } /** * Set Project * @@ -29,17 +4027,13 @@ * * @param value string * - * @return this + * @return {this} */ - let setProject = function (value) - { - http.addGlobalHeader('X-Appwrite-Project', value); - - config.project = value; - + setProject(value) { + this.headers['X-Appwrite-Project'] = value; + this.config.project = value; return this; - }; - + } /** * Set Key * @@ -47,5096 +4041,175 @@ * * @param value string * - * @return this + * @return {this} */ - let setKey = function (value) - { - http.addGlobalHeader('X-Appwrite-Key', value); - - config.key = value; - + setKey(value) { + this.headers['X-Appwrite-Key'] = value; + this.config.key = value; return this; - }; - + } + /** + * Set JWT + * + * Your secret JSON Web Token + * + * @param value string + * + * @return {this} + */ + setJWT(value) { + this.headers['X-Appwrite-JWT'] = value; + this.config.jwt = value; + return this; + } /** * Set Locale * * @param value string * - * @return this + * @return {this} */ - let setLocale = function (value) - { - http.addGlobalHeader('X-Appwrite-Locale', value); - - config.locale = value; - + setLocale(value) { + this.headers['X-Appwrite-Locale'] = value; + this.config.locale = value; return this; - }; - + } /** * Set Mode * * @param value string * - * @return this + * @return {this} */ - let setMode = function (value) - { - http.addGlobalHeader('X-Appwrite-Mode', value); - - config.mode = value; - + setMode(value) { + this.headers['X-Appwrite-Mode'] = value; + this.config.mode = value; return this; - }; - - let http = function(document) { - let globalParams = [], - globalHeaders = []; - - let addParam = function (url, param, value) { - let a = document.createElement('a'), regex = /(?:\?|&|&)+([^=]+)(?:=([^&]*))*/g; - let match, str = []; - a.href = url; - param = encodeURIComponent(param); - - while (match = regex.exec(a.search)) if (param !== match[1]) str.push(match[1] + (match[2] ? "=" + match[2] : "")); - - str.push(param + (value ? "=" + encodeURIComponent(value) : "")); - - a.search = str.join("&"); - - return a.href; + } + /** + * 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] + * @param {(payload: unknown) => 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); + }); }; - - /** - * @param {Object} params - * @returns {string} - */ - let buildQuery = function(params) { - let str = []; - - for (let p in params) { - if(Array.isArray(params[p])) { - for (let index = 0; index < params[p].length; index++) { - let param = params[p][index]; - str.push(encodeURIComponent(p + '[]') + "=" + encodeURIComponent(param)); - } - } - else { - str.push(encodeURIComponent(p) + "=" + encodeURIComponent(params[p])); + } + 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); } } - - return str.join("&"); - }; - - let addGlobalHeader = function(key, value) { - globalHeaders[key] = {key: key.toLowerCase(), value: value.toLowerCase()}; - }; - - let addGlobalParam = function(key, value) { - globalParams.push({key: key, value: value}); - }; - - addGlobalHeader('x-sdk-version', 'appwrite:web:1.0.0'); - addGlobalHeader('content-type', ''); - - /** - * @param {string} method - * @param {string} path string - * @param {Object} headers - * @param {Object} params - * @param {function} progress - * @returns {Promise} - */ - let call = function (method, path, headers = {}, params = {}, progress = null) { - let i; - - path = config.endpoint + path; - - if (-1 === ['GET', 'POST', 'PUT', 'DELETE', 'TRACE', 'HEAD', 'OPTIONS', 'CONNECT', 'PATCH'].indexOf(method)) { - throw new Error('var method must contain a valid HTTP method name'); - } - - if (typeof path !== 'string') { - throw new Error('var path must be of type string'); - } - - if (typeof headers !== 'object') { - throw new Error('var headers must be of type object'); - } - - for (i = 0; i < globalParams.length; i++) { // Add global params to URL - path = addParam(path, globalParams[i].key, globalParams[i].value); - } - - if(window.localStorage && window.localStorage.getItem('cookieFallback')) { - headers['X-Fallback-Cookies'] = window.localStorage.getItem('cookieFallback'); - } - - for (let key in globalHeaders) { // Add Global Headers - if (globalHeaders.hasOwnProperty(key)) { - if (!headers[globalHeaders[key].key]) { - headers[globalHeaders[key].key] = globalHeaders[key].value; - } - } - } - - if(method === 'GET') { - for (let param in params) { - if (param.hasOwnProperty(key)) { - path = addParam(path, key + (Array.isArray(param) ? '[]' : ''), params[key]); - } - } - } - - switch (headers['content-type']) { // Parse request by content type - case 'application/json': - params = JSON.stringify(params); - break; - - case 'multipart/form-data': - let formData = new FormData(); - - Object.keys(params).forEach(function(key) { - let param = params[key]; - formData.append(key + (Array.isArray(param) ? '[]' : ''), param); - }); - - params = formData; - break; - } - - return new Promise(function (resolve, reject) { - - let request = new XMLHttpRequest(), key; - - request.withCredentials = true; - request.open(method, path, true); - - for (key in headers) { // Set Headers - if (headers.hasOwnProperty(key)) { - if (key === 'content-type' && headers[key] === 'multipart/form-data') { // Skip to avoid missing boundary - continue; + 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(',')); + } + else { + formData.append(key, params[key]); + } } - - request.setRequestHeader(key, headers[key]); - } + options.body = formData; + delete headers['content-type']; + break; } - - request.onload = function () { - let data = request.response; - let contentType = this.getResponseHeader('content-type') || ''; - contentType = contentType.substring(0, contentType.indexOf(';')); - - switch (contentType) { - case 'application/json': - data = JSON.parse(data); - break; - } - - let cookieFallback = this.getResponseHeader('X-Fallback-Cookies') || ''; - - if(window.localStorage && cookieFallback) { - window.console.warn('Appwrite is using localStorage for session management. Increase your security by adding a custom domain as your API endpoint.'); - window.localStorage.setItem('cookieFallback', cookieFallback); - } - - if (4 === request.readyState && 399 >= request.status) { - resolve(data); - } else { - reject(data); - } - }; - - if (progress) { - request.addEventListener('progress', progress); - request.upload.addEventListener('progress', progress, false); - } - - // Handle network errors - request.onerror = function () { - reject(new Error("Network Error")); - }; - - request.send(params); - }) - }; - - return { - 'get': function(path, headers = {}, params = {}) { - return call('GET', path + ((Object.keys(params).length > 0) ? '?' + buildQuery(params) : ''), headers, {}); - }, - 'post': function(path, headers = {}, params = {}, progress = null) { - return call('POST', path, headers, params, progress); - }, - 'put': function(path, headers = {}, params = {}, progress = null) { - return call('PUT', path, headers, params, progress); - }, - 'patch': function(path, headers = {}, params = {}, progress = null) { - return call('PATCH', path, headers, params, progress); - }, - 'delete': function(path, headers = {}, params = {}, progress = null) { - return call('DELETE', path, headers, params, progress); - }, - 'addGlobalParam': addGlobalParam, - 'addGlobalHeader': addGlobalHeader - } - }(window.document); - - let account = { - - /** - * Get Account - * - * Get currently logged in user data as JSON object. - * - * @throws {Error} - * @return {Promise} - */ - get: function() { - let path = '/account'; - - let payload = {}; - - return http - .get(path, { - 'content-type': 'application/json', - }, payload); - }, - - /** - * Create Account - * - * Use this endpoint to allow a new user to register a new account in your - * project. After the user registration completes successfully, you can use - * the [/account/verfication](/docs/client/account#accountCreateVerification) - * route to start verifying the user email address. To allow the new user to - * login to their new account, you need to create a new [account - * session](/docs/client/account#accountCreateSession). - * - * @param {string} email - * @param {string} password - * @param {string} name - * @throws {Error} - * @return {Promise} - */ - create: function(email, password, name = '') { - if(email === undefined) { - throw new Error('Missing required parameter: "email"'); } - - if(password === undefined) { - throw new Error('Missing required parameter: "password"'); - } - - let path = '/account'; - - let payload = {}; - - if(typeof email !== 'undefined') { - payload['email'] = email; - } - - if(typeof password !== 'undefined') { - payload['password'] = password; - } - - if(typeof name !== 'undefined') { - payload['name'] = name; - } - - return http - .post(path, { - 'content-type': 'application/json', - }, payload); - }, - - /** - * Delete Account - * - * Delete a currently logged in user account. Behind the scene, the user - * record is not deleted but permanently blocked from any access. This is done - * to avoid deleted accounts being overtaken by new users with the same email - * address. Any user-related resources like documents or storage files should - * be deleted separately. - * - * @throws {Error} - * @return {Promise} - */ - delete: function() { - let path = '/account'; - - let payload = {}; - - return http - .delete(path, { - 'content-type': 'application/json', - }, payload); - }, - - /** - * Update Account Email - * - * Update currently logged in user account email address. After changing user - * address, user confirmation status is being reset and a new confirmation - * mail is sent. For security measures, user password is required to complete - * this request. - * - * @param {string} email - * @param {string} password - * @throws {Error} - * @return {Promise} - */ - updateEmail: function(email, password) { - if(email === undefined) { - throw new Error('Missing required parameter: "email"'); - } - - if(password === undefined) { - throw new Error('Missing required parameter: "password"'); - } - - let path = '/account/email'; - - let payload = {}; - - if(typeof email !== 'undefined') { - payload['email'] = email; - } - - if(typeof password !== 'undefined') { - payload['password'] = password; - } - - return http - .patch(path, { - 'content-type': 'application/json', - }, payload); - }, - - /** - * Get Account Logs - * - * Get currently logged in user list of latest security activity logs. Each - * log returns user IP address, location and date and time of log. - * - * @throws {Error} - * @return {Promise} - */ - getLogs: function() { - let path = '/account/logs'; - - let payload = {}; - - return http - .get(path, { - 'content-type': 'application/json', - }, payload); - }, - - /** - * Update Account Name - * - * Update currently logged in user account name. - * - * @param {string} name - * @throws {Error} - * @return {Promise} - */ - updateName: function(name) { - if(name === undefined) { - throw new Error('Missing required parameter: "name"'); - } - - let path = '/account/name'; - - let payload = {}; - - if(typeof name !== 'undefined') { - payload['name'] = name; - } - - return http - .patch(path, { - 'content-type': 'application/json', - }, payload); - }, - - /** - * Update Account Password - * - * Update currently logged in user password. For validation, user is required - * to pass the password twice. - * - * @param {string} password - * @param {string} oldPassword - * @throws {Error} - * @return {Promise} - */ - updatePassword: function(password, oldPassword) { - if(password === undefined) { - throw new Error('Missing required parameter: "password"'); - } - - if(oldPassword === undefined) { - throw new Error('Missing required parameter: "oldPassword"'); - } - - let path = '/account/password'; - - let payload = {}; - - if(typeof password !== 'undefined') { - payload['password'] = password; - } - - if(typeof oldPassword !== 'undefined') { - payload['oldPassword'] = oldPassword; - } - - return http - .patch(path, { - 'content-type': 'application/json', - }, payload); - }, - - /** - * Get Account Preferences - * - * Get currently logged in user preferences as a key-value object. - * - * @throws {Error} - * @return {Promise} - */ - getPrefs: function() { - let path = '/account/prefs'; - - let payload = {}; - - return http - .get(path, { - 'content-type': 'application/json', - }, payload); - }, - - /** - * Update Account Preferences - * - * Update currently logged in user account preferences. You can pass only the - * specific settings you wish to update. - * - * @param {object} prefs - * @throws {Error} - * @return {Promise} - */ - updatePrefs: function(prefs) { - if(prefs === undefined) { - throw new Error('Missing required parameter: "prefs"'); - } - - let path = '/account/prefs'; - - let payload = {}; - - if(typeof prefs !== 'undefined') { - payload['prefs'] = prefs; - } - - return http - .patch(path, { - 'content-type': 'application/json', - }, payload); - }, - - /** - * Create Password Recovery - * - * Sends the user an email with a temporary secret key for password reset. - * When the user clicks the confirmation link he is redirected back to your - * app password reset URL with the secret key and email address values - * attached to the URL query string. Use the query string params to submit a - * request to the [PUT - * /account/recovery](/docs/client/account#accountUpdateRecovery) endpoint to - * complete the process. - * - * @param {string} email - * @param {string} url - * @throws {Error} - * @return {Promise} - */ - createRecovery: function(email, url) { - if(email === undefined) { - throw new Error('Missing required parameter: "email"'); - } - - if(url === undefined) { - throw new Error('Missing required parameter: "url"'); - } - - let path = '/account/recovery'; - - let payload = {}; - - if(typeof email !== 'undefined') { - payload['email'] = email; - } - - if(typeof url !== 'undefined') { - payload['url'] = url; - } - - return http - .post(path, { - 'content-type': 'application/json', - }, payload); - }, - - /** - * Complete Password Recovery - * - * Use this endpoint to complete the user account password reset. Both the - * **userId** and **secret** arguments will be passed as query parameters to - * the redirect URL you have provided when sending your request to the [POST - * /account/recovery](/docs/client/account#accountCreateRecovery) endpoint. - * - * Please note that in order to avoid a [Redirect - * Attack](https://github.com/OWASP/CheatSheetSeries/blob/master/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.md) - * the only valid redirect URLs are the ones from domains you have set when - * adding your platforms in the console interface. - * - * @param {string} userId - * @param {string} secret - * @param {string} password - * @param {string} passwordAgain - * @throws {Error} - * @return {Promise} - */ - updateRecovery: function(userId, secret, password, passwordAgain) { - if(userId === undefined) { - throw new Error('Missing required parameter: "userId"'); - } - - if(secret === undefined) { - throw new Error('Missing required parameter: "secret"'); - } - - if(password === undefined) { - throw new Error('Missing required parameter: "password"'); - } - - if(passwordAgain === undefined) { - throw new Error('Missing required parameter: "passwordAgain"'); - } - - let path = '/account/recovery'; - - let payload = {}; - - if(typeof userId !== 'undefined') { - payload['userId'] = userId; - } - - if(typeof secret !== 'undefined') { - payload['secret'] = secret; - } - - if(typeof password !== 'undefined') { - payload['password'] = password; - } - - if(typeof passwordAgain !== 'undefined') { - payload['passwordAgain'] = passwordAgain; - } - - return http - .put(path, { - 'content-type': 'application/json', - }, payload); - }, - - /** - * Get Account Sessions - * - * Get currently logged in user list of active sessions across different - * devices. - * - * @throws {Error} - * @return {Promise} - */ - getSessions: function() { - let path = '/account/sessions'; - - let payload = {}; - - return http - .get(path, { - 'content-type': 'application/json', - }, payload); - }, - - /** - * Create Account Session - * - * Allow the user to login into their account by providing a valid email and - * password combination. This route will create a new session for the user. - * - * @param {string} email - * @param {string} password - * @throws {Error} - * @return {Promise} - */ - createSession: function(email, password) { - if(email === undefined) { - throw new Error('Missing required parameter: "email"'); - } - - if(password === undefined) { - throw new Error('Missing required parameter: "password"'); - } - - let path = '/account/sessions'; - - let payload = {}; - - if(typeof email !== 'undefined') { - payload['email'] = email; - } - - if(typeof password !== 'undefined') { - payload['password'] = password; - } - - return http - .post(path, { - 'content-type': 'application/json', - }, payload); - }, - - /** - * Delete All Account Sessions - * - * Delete all sessions from the user account and remove any sessions cookies - * from the end client. - * - * @throws {Error} - * @return {Promise} - */ - deleteSessions: function() { - let path = '/account/sessions'; - - let payload = {}; - - return http - .delete(path, { - 'content-type': 'application/json', - }, payload); - }, - - /** - * Create Account Session with OAuth2 - * - * Allow the user to login to their account using the OAuth2 provider of their - * choice. Each OAuth2 provider should be enabled from the Appwrite console - * first. Use the success and failure arguments to provide a redirect URL's - * back to your app when login is completed. - * - * @param {string} provider - * @param {string} success - * @param {string} failure - * @param {string[]} scopes - * @throws {Error} - * @return {Promise} - */ - createOAuth2Session: function(provider, success = 'https://appwrite.io/auth/oauth2/success', failure = 'https://appwrite.io/auth/oauth2/failure', scopes = []) { - if(provider === undefined) { - throw new Error('Missing required parameter: "provider"'); - } - - let path = '/account/sessions/oauth2/{provider}'.replace(new RegExp('{provider}', 'g'), provider); - - let payload = {}; - - if(success) { - payload['success'] = success; - } - - if(failure) { - payload['failure'] = failure; - } - - if(scopes) { - payload['scopes'] = scopes; - } - - payload['project'] = config.project; - - payload['key'] = config.key; - - - let query = []; - - for (let p in payload) { - if(Array.isArray(payload[p])) { - for (let index = 0; index < payload[p].length; index++) { - let param = payload[p][index]; - query.push(encodeURIComponent(p + '[]') + "=" + encodeURIComponent(param)); - } + try { + let data = null; + const response = yield crossFetch.fetch(url.toString(), options); + if ((_b = response.headers.get("content-type")) === null || _b === void 0 ? void 0 : _b.includes("application/json")) { + data = yield response.json(); } else { - query.push(encodeURIComponent(p) + "=" + encodeURIComponent(payload[p])); + data = { + message: yield response.text() + }; } + if (400 <= response.status) { + throw new AppwriteException(data === null || data === void 0 ? void 0 : data.message, response.status, data); + } + const cookieFallback = response.headers.get('X-Fallback-Cookies'); + if (typeof window !== 'undefined' && window.localStorage && cookieFallback) { + window.console.warn('Appwrite is using localStorage for session management. Increase your security by adding a custom domain as your API endpoint.'); + window.localStorage.setItem('cookieFallback', cookieFallback); + } + return data; } - - query = query.join("&"); - - window.location = config.endpoint + path + ((query) ? '?' + query : ''); - }, - - /** - * Delete Account Session - * - * Use this endpoint to log out the currently logged in user from all their - * account sessions across all of their different devices. When using the - * option id argument, only the session unique ID provider will be deleted. - * - * @param {string} sessionId - * @throws {Error} - * @return {Promise} - */ - deleteSession: function(sessionId) { - if(sessionId === undefined) { - throw new Error('Missing required parameter: "sessionId"'); + catch (e) { + throw new AppwriteException(e.message); } - - let path = '/account/sessions/{sessionId}'.replace(new RegExp('{sessionId}', 'g'), sessionId); - - let payload = {}; - - return http - .delete(path, { - 'content-type': 'application/json', - }, payload); - }, - - /** - * Create Email Verification - * - * Use this endpoint to send a verification message to your user email address - * to confirm they are the valid owners of that address. Both the **userId** - * and **secret** arguments will be passed as query parameters to the URL you - * have provided to be attached to the verification email. The provided URL - * should redirect the user back to your app and allow you to complete the - * verification process by verifying both the **userId** and **secret** - * parameters. Learn more about how to [complete the verification - * process](/docs/client/account#accountUpdateVerification). - * - * Please note that in order to avoid a [Redirect - * Attack](https://github.com/OWASP/CheatSheetSeries/blob/master/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.md), - * the only valid redirect URLs are the ones from domains you have set when - * adding your platforms in the console interface. - * - * - * @param {string} url - * @throws {Error} - * @return {Promise} - */ - createVerification: function(url) { - if(url === undefined) { - throw new Error('Missing required parameter: "url"'); + }); + } + flatten(data, prefix = '') { + let output = {}; + for (const key in data) { + let value = data[key]; + let finalKey = prefix ? `${prefix}[${key}]` : key; + if (Array.isArray(value)) { + output = Object.assign(output, this.flatten(value, finalKey)); } - - let path = '/account/verification'; - - let payload = {}; - - if(typeof url !== 'undefined') { - payload['url'] = url; + else { + output[finalKey] = value; } - - return http - .post(path, { - 'content-type': 'application/json', - }, payload); - }, - - /** - * Complete Email Verification - * - * Use this endpoint to complete the user email verification process. Use both - * the **userId** and **secret** parameters that were attached to your app URL - * to verify the user email ownership. If confirmed this route will return a - * 200 status code. - * - * @param {string} userId - * @param {string} secret - * @throws {Error} - * @return {Promise} - */ - updateVerification: function(userId, secret) { - if(userId === undefined) { - throw new Error('Missing required parameter: "userId"'); - } - - if(secret === undefined) { - throw new Error('Missing required parameter: "secret"'); - } - - let path = '/account/verification'; - - let payload = {}; - - if(typeof userId !== 'undefined') { - payload['userId'] = userId; - } - - if(typeof secret !== 'undefined') { - payload['secret'] = secret; - } - - return http - .put(path, { - 'content-type': 'application/json', - }, payload); } - }; - - let avatars = { - - /** - * Get Browser Icon - * - * You can use this endpoint to show different browser icons to your users. - * The code argument receives the browser code as it appears in your user - * /account/sessions endpoint. Use width, height and quality arguments to - * change the output settings. - * - * @param {string} code - * @param {number} width - * @param {number} height - * @param {number} quality - * @throws {Error} - * @return {string} - */ - getBrowser: function(code, width = 100, height = 100, quality = 100) { - if(code === undefined) { - throw new Error('Missing required parameter: "code"'); - } - - let path = '/avatars/browsers/{code}'.replace(new RegExp('{code}', 'g'), code); - - let payload = {}; - - if(width) { - payload['width'] = width; - } - - if(height) { - payload['height'] = height; - } - - if(quality) { - payload['quality'] = quality; - } - - payload['project'] = config.project; - - payload['key'] = config.key; - - - let query = []; - - for (let p in payload) { - if(Array.isArray(payload[p])) { - for (let index = 0; index < payload[p].length; index++) { - let param = payload[p][index]; - query.push(encodeURIComponent(p + '[]') + "=" + encodeURIComponent(param)); - } - } - else { - query.push(encodeURIComponent(p) + "=" + encodeURIComponent(payload[p])); - } - } - - query = query.join("&"); - - return config.endpoint + path + ((query) ? '?' + query : ''); - }, - - /** - * Get Credit Card Icon - * - * The credit card endpoint will return you the icon of the credit card - * provider you need. Use width, height and quality arguments to change the - * output settings. - * - * @param {string} code - * @param {number} width - * @param {number} height - * @param {number} quality - * @throws {Error} - * @return {string} - */ - getCreditCard: function(code, width = 100, height = 100, quality = 100) { - if(code === undefined) { - throw new Error('Missing required parameter: "code"'); - } - - let path = '/avatars/credit-cards/{code}'.replace(new RegExp('{code}', 'g'), code); - - let payload = {}; - - if(width) { - payload['width'] = width; - } - - if(height) { - payload['height'] = height; - } - - if(quality) { - payload['quality'] = quality; - } - - payload['project'] = config.project; - - payload['key'] = config.key; - - - let query = []; - - for (let p in payload) { - if(Array.isArray(payload[p])) { - for (let index = 0; index < payload[p].length; index++) { - let param = payload[p][index]; - query.push(encodeURIComponent(p + '[]') + "=" + encodeURIComponent(param)); - } - } - else { - query.push(encodeURIComponent(p) + "=" + encodeURIComponent(payload[p])); - } - } - - query = query.join("&"); - - return config.endpoint + path + ((query) ? '?' + query : ''); - }, - - /** - * Get Favicon - * - * Use this endpoint to fetch the favorite icon (AKA favicon) of any remote - * website URL. - * - * - * @param {string} url - * @throws {Error} - * @return {string} - */ - getFavicon: function(url) { - if(url === undefined) { - throw new Error('Missing required parameter: "url"'); - } - - let path = '/avatars/favicon'; - - let payload = {}; - - if(url) { - payload['url'] = url; - } - - payload['project'] = config.project; - - payload['key'] = config.key; - - - let query = []; - - for (let p in payload) { - if(Array.isArray(payload[p])) { - for (let index = 0; index < payload[p].length; index++) { - let param = payload[p][index]; - query.push(encodeURIComponent(p + '[]') + "=" + encodeURIComponent(param)); - } - } - else { - query.push(encodeURIComponent(p) + "=" + encodeURIComponent(payload[p])); - } - } - - query = query.join("&"); - - return config.endpoint + path + ((query) ? '?' + query : ''); - }, - - /** - * Get Country Flag - * - * You can use this endpoint to show different country flags icons to your - * users. The code argument receives the 2 letter country code. Use width, - * height and quality arguments to change the output settings. - * - * @param {string} code - * @param {number} width - * @param {number} height - * @param {number} quality - * @throws {Error} - * @return {string} - */ - getFlag: function(code, width = 100, height = 100, quality = 100) { - if(code === undefined) { - throw new Error('Missing required parameter: "code"'); - } - - let path = '/avatars/flags/{code}'.replace(new RegExp('{code}', 'g'), code); - - let payload = {}; - - if(width) { - payload['width'] = width; - } - - if(height) { - payload['height'] = height; - } - - if(quality) { - payload['quality'] = quality; - } - - payload['project'] = config.project; - - payload['key'] = config.key; - - - let query = []; - - for (let p in payload) { - if(Array.isArray(payload[p])) { - for (let index = 0; index < payload[p].length; index++) { - let param = payload[p][index]; - query.push(encodeURIComponent(p + '[]') + "=" + encodeURIComponent(param)); - } - } - else { - query.push(encodeURIComponent(p) + "=" + encodeURIComponent(payload[p])); - } - } - - query = query.join("&"); - - return config.endpoint + path + ((query) ? '?' + query : ''); - }, - - /** - * Get Image from URL - * - * Use this endpoint to fetch a remote image URL and crop it to any image size - * you want. This endpoint is very useful if you need to crop and display - * remote images in your app or in case you want to make sure a 3rd party - * image is properly served using a TLS protocol. - * - * @param {string} url - * @param {number} width - * @param {number} height - * @throws {Error} - * @return {string} - */ - getImage: function(url, width = 400, height = 400) { - if(url === undefined) { - throw new Error('Missing required parameter: "url"'); - } - - let path = '/avatars/image'; - - let payload = {}; - - if(url) { - payload['url'] = url; - } - - if(width) { - payload['width'] = width; - } - - if(height) { - payload['height'] = height; - } - - payload['project'] = config.project; - - payload['key'] = config.key; - - - let query = []; - - for (let p in payload) { - if(Array.isArray(payload[p])) { - for (let index = 0; index < payload[p].length; index++) { - let param = payload[p][index]; - query.push(encodeURIComponent(p + '[]') + "=" + encodeURIComponent(param)); - } - } - else { - query.push(encodeURIComponent(p) + "=" + encodeURIComponent(payload[p])); - } - } - - query = query.join("&"); - - return config.endpoint + path + ((query) ? '?' + query : ''); - }, - - /** - * Get User Initials - * - * Use this endpoint to show your user initials avatar icon on your website or - * app. By default, this route will try to print your logged-in user name or - * email initials. You can also overwrite the user name if you pass the 'name' - * parameter. If no name is given and no user is logged, an empty avatar will - * be returned. - * - * You can use the color and background params to change the avatar colors. By - * default, a random theme will be selected. The random theme will persist for - * the user's initials when reloading the same theme will always return for - * the same initials. - * - * @param {string} name - * @param {number} width - * @param {number} height - * @param {string} color - * @param {string} background - * @throws {Error} - * @return {string} - */ - getInitials: function(name = '', width = 500, height = 500, color = '', background = '') { - let path = '/avatars/initials'; - - let payload = {}; - - if(name) { - payload['name'] = name; - } - - if(width) { - payload['width'] = width; - } - - if(height) { - payload['height'] = height; - } - - if(color) { - payload['color'] = color; - } - - if(background) { - payload['background'] = background; - } - - payload['project'] = config.project; - - payload['key'] = config.key; - - - let query = []; - - for (let p in payload) { - if(Array.isArray(payload[p])) { - for (let index = 0; index < payload[p].length; index++) { - let param = payload[p][index]; - query.push(encodeURIComponent(p + '[]') + "=" + encodeURIComponent(param)); - } - } - else { - query.push(encodeURIComponent(p) + "=" + encodeURIComponent(payload[p])); - } - } - - query = query.join("&"); - - return config.endpoint + path + ((query) ? '?' + query : ''); - }, - - /** - * Get QR Code - * - * Converts a given plain text to a QR code image. You can use the query - * parameters to change the size and style of the resulting image. - * - * @param {string} text - * @param {number} size - * @param {number} margin - * @param {boolean} download - * @throws {Error} - * @return {string} - */ - getQR: function(text, size = 400, margin = 1, download = false) { - if(text === undefined) { - throw new Error('Missing required parameter: "text"'); - } - - let path = '/avatars/qr'; - - let payload = {}; - - if(text) { - payload['text'] = text; - } - - if(size) { - payload['size'] = size; - } - - if(margin) { - payload['margin'] = margin; - } - - if(download) { - payload['download'] = download; - } - - payload['project'] = config.project; - - payload['key'] = config.key; - - - let query = []; - - for (let p in payload) { - if(Array.isArray(payload[p])) { - for (let index = 0; index < payload[p].length; index++) { - let param = payload[p][index]; - query.push(encodeURIComponent(p + '[]') + "=" + encodeURIComponent(param)); - } - } - else { - query.push(encodeURIComponent(p) + "=" + encodeURIComponent(payload[p])); - } - } - - query = query.join("&"); - - return config.endpoint + path + ((query) ? '?' + query : ''); - } - }; - - let database = { - - /** - * List Collections - * - * Get a list of all the user collections. You can use the query params to - * filter your results. On admin mode, this endpoint will return a list of all - * of the project's collections. [Learn more about different API - * modes](/docs/admin). - * - * @param {string} search - * @param {number} limit - * @param {number} offset - * @param {string} orderType - * @throws {Error} - * @return {Promise} - */ - listCollections: function(search = '', limit = 25, offset = 0, orderType = 'ASC') { - let path = '/database/collections'; - - let payload = {}; - - if(search) { - payload['search'] = search; - } - - if(limit) { - payload['limit'] = limit; - } - - if(offset) { - payload['offset'] = offset; - } - - if(orderType) { - payload['orderType'] = orderType; - } - - return http - .get(path, { - 'content-type': 'application/json', - }, payload); - }, - - /** - * Create Collection - * - * Create a new Collection. - * - * @param {string} name - * @param {string[]} read - * @param {string[]} write - * @param {string[]} rules - * @throws {Error} - * @return {Promise} - */ - createCollection: function(name, read, write, rules) { - if(name === undefined) { - throw new Error('Missing required parameter: "name"'); - } - - if(read === undefined) { - throw new Error('Missing required parameter: "read"'); - } - - if(write === undefined) { - throw new Error('Missing required parameter: "write"'); - } - - if(rules === undefined) { - throw new Error('Missing required parameter: "rules"'); - } - - let path = '/database/collections'; - - let payload = {}; - - if(typeof name !== 'undefined') { - payload['name'] = name; - } - - if(typeof read !== 'undefined') { - payload['read'] = read; - } - - if(typeof write !== 'undefined') { - payload['write'] = write; - } - - if(typeof rules !== 'undefined') { - payload['rules'] = rules; - } - - return http - .post(path, { - 'content-type': 'application/json', - }, payload); - }, - - /** - * Get Collection - * - * Get a collection by its unique ID. This endpoint response returns a JSON - * object with the collection metadata. - * - * @param {string} collectionId - * @throws {Error} - * @return {Promise} - */ - getCollection: function(collectionId) { - if(collectionId === undefined) { - throw new Error('Missing required parameter: "collectionId"'); - } - - let path = '/database/collections/{collectionId}'.replace(new RegExp('{collectionId}', 'g'), collectionId); - - let payload = {}; - - return http - .get(path, { - 'content-type': 'application/json', - }, payload); - }, - - /** - * Update Collection - * - * Update a collection by its unique ID. - * - * @param {string} collectionId - * @param {string} name - * @param {string[]} read - * @param {string[]} write - * @param {string[]} rules - * @throws {Error} - * @return {Promise} - */ - updateCollection: function(collectionId, name, read, write, rules = []) { - if(collectionId === undefined) { - throw new Error('Missing required parameter: "collectionId"'); - } - - if(name === undefined) { - throw new Error('Missing required parameter: "name"'); - } - - if(read === undefined) { - throw new Error('Missing required parameter: "read"'); - } - - if(write === undefined) { - throw new Error('Missing required parameter: "write"'); - } - - let path = '/database/collections/{collectionId}'.replace(new RegExp('{collectionId}', 'g'), collectionId); - - let payload = {}; - - if(typeof name !== 'undefined') { - payload['name'] = name; - } - - if(typeof read !== 'undefined') { - payload['read'] = read; - } - - if(typeof write !== 'undefined') { - payload['write'] = write; - } - - if(typeof rules !== 'undefined') { - payload['rules'] = rules; - } - - return http - .put(path, { - 'content-type': 'application/json', - }, payload); - }, - - /** - * Delete Collection - * - * Delete a collection by its unique ID. Only users with write permissions - * have access to delete this resource. - * - * @param {string} collectionId - * @throws {Error} - * @return {Promise} - */ - deleteCollection: function(collectionId) { - if(collectionId === undefined) { - throw new Error('Missing required parameter: "collectionId"'); - } - - let path = '/database/collections/{collectionId}'.replace(new RegExp('{collectionId}', 'g'), collectionId); - - let payload = {}; - - return http - .delete(path, { - 'content-type': 'application/json', - }, payload); - }, - - /** - * List Documents - * - * Get a list of all the user documents. You can use the query params to - * filter your results. On admin mode, this endpoint will return a list of all - * of the project's documents. [Learn more about different API - * modes](/docs/admin). - * - * @param {string} collectionId - * @param {string[]} filters - * @param {number} limit - * @param {number} offset - * @param {string} orderField - * @param {string} orderType - * @param {string} orderCast - * @param {string} search - * @throws {Error} - * @return {Promise} - */ - listDocuments: function(collectionId, filters = [], limit = 25, offset = 0, orderField = '', orderType = 'ASC', orderCast = 'string', search = '') { - if(collectionId === undefined) { - throw new Error('Missing required parameter: "collectionId"'); - } - - let path = '/database/collections/{collectionId}/documents'.replace(new RegExp('{collectionId}', 'g'), collectionId); - - let payload = {}; - - if(filters) { - payload['filters'] = filters; - } - - if(limit) { - payload['limit'] = limit; - } - - if(offset) { - payload['offset'] = offset; - } - - if(orderField) { - payload['orderField'] = orderField; - } - - if(orderType) { - payload['orderType'] = orderType; - } - - if(orderCast) { - payload['orderCast'] = orderCast; - } - - if(search) { - payload['search'] = search; - } - - return http - .get(path, { - 'content-type': 'application/json', - }, payload); - }, - - /** - * Create Document - * - * Create a new Document. Before using this route, you should create a new - * collection resource using either a [server - * integration](/docs/server/database#databaseCreateCollection) API or - * directly from your database console. - * - * @param {string} collectionId - * @param {object} data - * @param {string[]} read - * @param {string[]} write - * @param {string} parentDocument - * @param {string} parentProperty - * @param {string} parentPropertyType - * @throws {Error} - * @return {Promise} - */ - createDocument: function(collectionId, data, read, write, parentDocument = '', parentProperty = '', parentPropertyType = 'assign') { - if(collectionId === undefined) { - throw new Error('Missing required parameter: "collectionId"'); - } - - if(data === undefined) { - throw new Error('Missing required parameter: "data"'); - } - - if(read === undefined) { - throw new Error('Missing required parameter: "read"'); - } - - if(write === undefined) { - throw new Error('Missing required parameter: "write"'); - } - - let path = '/database/collections/{collectionId}/documents'.replace(new RegExp('{collectionId}', 'g'), collectionId); - - let payload = {}; - - if(typeof data !== 'undefined') { - payload['data'] = data; - } - - if(typeof read !== 'undefined') { - payload['read'] = read; - } - - if(typeof write !== 'undefined') { - payload['write'] = write; - } - - if(typeof parentDocument !== 'undefined') { - payload['parentDocument'] = parentDocument; - } - - if(typeof parentProperty !== 'undefined') { - payload['parentProperty'] = parentProperty; - } - - if(typeof parentPropertyType !== 'undefined') { - payload['parentPropertyType'] = parentPropertyType; - } - - return http - .post(path, { - 'content-type': 'application/json', - }, payload); - }, - - /** - * Get Document - * - * Get a document by its unique ID. This endpoint response returns a JSON - * object with the document data. - * - * @param {string} collectionId - * @param {string} documentId - * @throws {Error} - * @return {Promise} - */ - getDocument: function(collectionId, documentId) { - if(collectionId === undefined) { - throw new Error('Missing required parameter: "collectionId"'); - } - - if(documentId === undefined) { - throw new Error('Missing required parameter: "documentId"'); - } - - let path = '/database/collections/{collectionId}/documents/{documentId}'.replace(new RegExp('{collectionId}', 'g'), collectionId).replace(new RegExp('{documentId}', 'g'), documentId); - - let payload = {}; - - return http - .get(path, { - 'content-type': 'application/json', - }, payload); - }, - - /** - * Update Document - * - * Update a document by its unique ID. Using the patch method you can pass - * only specific fields that will get updated. - * - * @param {string} collectionId - * @param {string} documentId - * @param {object} data - * @param {string[]} read - * @param {string[]} write - * @throws {Error} - * @return {Promise} - */ - updateDocument: function(collectionId, documentId, data, read, write) { - if(collectionId === undefined) { - throw new Error('Missing required parameter: "collectionId"'); - } - - if(documentId === undefined) { - throw new Error('Missing required parameter: "documentId"'); - } - - if(data === undefined) { - throw new Error('Missing required parameter: "data"'); - } - - if(read === undefined) { - throw new Error('Missing required parameter: "read"'); - } - - if(write === undefined) { - throw new Error('Missing required parameter: "write"'); - } - - let path = '/database/collections/{collectionId}/documents/{documentId}'.replace(new RegExp('{collectionId}', 'g'), collectionId).replace(new RegExp('{documentId}', 'g'), documentId); - - let payload = {}; - - if(typeof data !== 'undefined') { - payload['data'] = data; - } - - if(typeof read !== 'undefined') { - payload['read'] = read; - } - - if(typeof write !== 'undefined') { - payload['write'] = write; - } - - return http - .patch(path, { - 'content-type': 'application/json', - }, payload); - }, - - /** - * Delete Document - * - * Delete a document by its unique ID. This endpoint deletes only the parent - * documents, its attributes and relations to other documents. Child documents - * **will not** be deleted. - * - * @param {string} collectionId - * @param {string} documentId - * @throws {Error} - * @return {Promise} - */ - deleteDocument: function(collectionId, documentId) { - if(collectionId === undefined) { - throw new Error('Missing required parameter: "collectionId"'); - } - - if(documentId === undefined) { - throw new Error('Missing required parameter: "documentId"'); - } - - let path = '/database/collections/{collectionId}/documents/{documentId}'.replace(new RegExp('{collectionId}', 'g'), collectionId).replace(new RegExp('{documentId}', 'g'), documentId); - - let payload = {}; - - return http - .delete(path, { - 'content-type': 'application/json', - }, payload); - } - }; - - let functions = { - - /** - * List Functions - * - * Get a list of all the project's functions. You can use the query params to - * filter your results. - * - * @param {string} search - * @param {number} limit - * @param {number} offset - * @param {string} orderType - * @throws {Error} - * @return {Promise} - */ - list: function(search = '', limit = 25, offset = 0, orderType = 'ASC') { - let path = '/functions'; - - let payload = {}; - - if(search) { - payload['search'] = search; - } - - if(limit) { - payload['limit'] = limit; - } - - if(offset) { - payload['offset'] = offset; - } - - if(orderType) { - payload['orderType'] = orderType; - } - - return http - .get(path, { - 'content-type': 'application/json', - }, payload); - }, - - /** - * Create Function - * - * Create a new function. You can pass a list of - * [permissions](/docs/permissions) to allow different project users or team - * with access to execute the function using the client API. - * - * @param {string} name - * @param {string[]} execute - * @param {string} env - * @param {object} vars - * @param {string[]} events - * @param {string} schedule - * @param {number} timeout - * @throws {Error} - * @return {Promise} - */ - create: function(name, execute, env, vars = {}, events = [], schedule = '', timeout = 15) { - if(name === undefined) { - throw new Error('Missing required parameter: "name"'); - } - - if(execute === undefined) { - throw new Error('Missing required parameter: "execute"'); - } - - if(env === undefined) { - throw new Error('Missing required parameter: "env"'); - } - - let path = '/functions'; - - let payload = {}; - - if(typeof name !== 'undefined') { - payload['name'] = name; - } - - if(typeof execute !== 'undefined') { - payload['execute'] = execute; - } - - if(typeof env !== 'undefined') { - payload['env'] = env; - } - - if(typeof vars !== 'undefined') { - payload['vars'] = vars; - } - - if(typeof events !== 'undefined') { - payload['events'] = events; - } - - if(typeof schedule !== 'undefined') { - payload['schedule'] = schedule; - } - - if(typeof timeout !== 'undefined') { - payload['timeout'] = timeout; - } - - return http - .post(path, { - 'content-type': 'application/json', - }, payload); - }, - - /** - * Get Function - * - * Get a function by its unique ID. - * - * @param {string} functionId - * @throws {Error} - * @return {Promise} - */ - get: function(functionId) { - if(functionId === undefined) { - throw new Error('Missing required parameter: "functionId"'); - } - - let path = '/functions/{functionId}'.replace(new RegExp('{functionId}', 'g'), functionId); - - let payload = {}; - - return http - .get(path, { - 'content-type': 'application/json', - }, payload); - }, - - /** - * Update Function - * - * Update function by its unique ID. - * - * @param {string} functionId - * @param {string} name - * @param {string[]} execute - * @param {object} vars - * @param {string[]} events - * @param {string} schedule - * @param {number} timeout - * @throws {Error} - * @return {Promise} - */ - update: function(functionId, name, execute, vars = {}, events = [], schedule = '', timeout = 15) { - if(functionId === undefined) { - throw new Error('Missing required parameter: "functionId"'); - } - - if(name === undefined) { - throw new Error('Missing required parameter: "name"'); - } - - if(execute === undefined) { - throw new Error('Missing required parameter: "execute"'); - } - - let path = '/functions/{functionId}'.replace(new RegExp('{functionId}', 'g'), functionId); - - let payload = {}; - - if(typeof name !== 'undefined') { - payload['name'] = name; - } - - if(typeof execute !== 'undefined') { - payload['execute'] = execute; - } - - if(typeof vars !== 'undefined') { - payload['vars'] = vars; - } - - if(typeof events !== 'undefined') { - payload['events'] = events; - } - - if(typeof schedule !== 'undefined') { - payload['schedule'] = schedule; - } - - if(typeof timeout !== 'undefined') { - payload['timeout'] = timeout; - } - - return http - .put(path, { - 'content-type': 'application/json', - }, payload); - }, - - /** - * Delete Function - * - * Delete a function by its unique ID. - * - * @param {string} functionId - * @throws {Error} - * @return {Promise} - */ - delete: function(functionId) { - if(functionId === undefined) { - throw new Error('Missing required parameter: "functionId"'); - } - - let path = '/functions/{functionId}'.replace(new RegExp('{functionId}', 'g'), functionId); - - let payload = {}; - - return http - .delete(path, { - 'content-type': 'application/json', - }, payload); - }, - - /** - * List Executions - * - * Get a list of all the current user function execution logs. You can use the - * query params to filter your results. On admin mode, this endpoint will - * return a list of all of the project's executions. [Learn more about - * different API modes](/docs/admin). - * - * @param {string} functionId - * @param {string} search - * @param {number} limit - * @param {number} offset - * @param {string} orderType - * @throws {Error} - * @return {Promise} - */ - listExecutions: function(functionId, search = '', limit = 25, offset = 0, orderType = 'ASC') { - if(functionId === undefined) { - throw new Error('Missing required parameter: "functionId"'); - } - - let path = '/functions/{functionId}/executions'.replace(new RegExp('{functionId}', 'g'), functionId); - - let payload = {}; - - if(search) { - payload['search'] = search; - } - - if(limit) { - payload['limit'] = limit; - } - - if(offset) { - payload['offset'] = offset; - } - - if(orderType) { - payload['orderType'] = orderType; - } - - return http - .get(path, { - 'content-type': 'application/json', - }, payload); - }, - - /** - * Create Execution - * - * Trigger a function execution. The returned object will return you the - * current execution status. You can ping the `Get Execution` endpoint to get - * updates on the current execution status. Once this endpoint is called, your - * function execution process will start asynchronously. - * - * @param {string} functionId - * @param {string} data - * @throws {Error} - * @return {Promise} - */ - createExecution: function(functionId, data) { - if(functionId === undefined) { - throw new Error('Missing required parameter: "functionId"'); - } - - let path = '/functions/{functionId}/executions'.replace(new RegExp('{functionId}', 'g'), functionId); - - let payload = {}; - - if (data) { - payload['data'] = data; - } - - return http - .post(path, { - 'content-type': 'application/json', - }, payload); - }, - - /** - * Get Execution - * - * Get a function execution log by its unique ID. - * - * @param {string} functionId - * @param {string} executionId - * @throws {Error} - * @return {Promise} - */ - getExecution: function(functionId, executionId) { - if(functionId === undefined) { - throw new Error('Missing required parameter: "functionId"'); - } - - if(executionId === undefined) { - throw new Error('Missing required parameter: "executionId"'); - } - - let path = '/functions/{functionId}/executions/{executionId}'.replace(new RegExp('{functionId}', 'g'), functionId).replace(new RegExp('{executionId}', 'g'), executionId); - - let payload = {}; - - return http - .get(path, { - 'content-type': 'application/json', - }, payload); - }, - - /** - * Update Function Tag - * - * Update the function code tag ID using the unique function ID. Use this - * endpoint to switch the code tag that should be executed by the execution - * endpoint. - * - * @param {string} functionId - * @param {string} tag - * @throws {Error} - * @return {Promise} - */ - updateTag: function(functionId, tag) { - if(functionId === undefined) { - throw new Error('Missing required parameter: "functionId"'); - } - - if(tag === undefined) { - throw new Error('Missing required parameter: "tag"'); - } - - let path = '/functions/{functionId}/tag'.replace(new RegExp('{functionId}', 'g'), functionId); - - let payload = {}; - - if(typeof tag !== 'undefined') { - payload['tag'] = tag; - } - - return http - .patch(path, { - 'content-type': 'application/json', - }, payload); - }, - - /** - * List Tags - * - * Get a list of all the project's code tags. You can use the query params to - * filter your results. - * - * @param {string} functionId - * @param {string} search - * @param {number} limit - * @param {number} offset - * @param {string} orderType - * @throws {Error} - * @return {Promise} - */ - listTags: function(functionId, search = '', limit = 25, offset = 0, orderType = 'ASC') { - if(functionId === undefined) { - throw new Error('Missing required parameter: "functionId"'); - } - - let path = '/functions/{functionId}/tags'.replace(new RegExp('{functionId}', 'g'), functionId); - - let payload = {}; - - if(search) { - payload['search'] = search; - } - - if(limit) { - payload['limit'] = limit; - } - - if(offset) { - payload['offset'] = offset; - } - - if(orderType) { - payload['orderType'] = orderType; - } - - return http - .get(path, { - 'content-type': 'application/json', - }, payload); - }, - - /** - * Create Tag - * - * Create a new function code tag. Use this endpoint to upload a new version - * of your code function. To execute your newly uploaded code, you'll need to - * update the function's tag to use your new tag UID. - * - * This endpoint accepts a tar.gz file compressed with your code. Make sure to - * include any dependencies your code has within the compressed file. You can - * learn more about code packaging in the [Appwrite Cloud Functions - * tutorial](/docs/functions). - * - * Use the "command" param to set the entry point used to execute your code. - * - * @param {string} functionId - * @param {string} command - * @param {File} code - * @throws {Error} - * @return {Promise} - */ - createTag: function(functionId, command, code) { - if(functionId === undefined) { - throw new Error('Missing required parameter: "functionId"'); - } - - if(command === undefined) { - throw new Error('Missing required parameter: "command"'); - } - - if(code === undefined) { - throw new Error('Missing required parameter: "code"'); - } - - let path = '/functions/{functionId}/tags'.replace(new RegExp('{functionId}', 'g'), functionId); - - let payload = {}; - - if(typeof command !== 'undefined') { - payload['command'] = command; - } - - if(typeof code !== 'undefined') { - payload['code'] = code; - } - - return http - .post(path, { - 'content-type': 'multipart/form-data', - }, payload); - }, - - /** - * Get Tag - * - * Get a code tag by its unique ID. - * - * @param {string} functionId - * @param {string} tagId - * @throws {Error} - * @return {Promise} - */ - getTag: function(functionId, tagId) { - if(functionId === undefined) { - throw new Error('Missing required parameter: "functionId"'); - } - - if(tagId === undefined) { - throw new Error('Missing required parameter: "tagId"'); - } - - let path = '/functions/{functionId}/tags/{tagId}'.replace(new RegExp('{functionId}', 'g'), functionId).replace(new RegExp('{tagId}', 'g'), tagId); - - let payload = {}; - - return http - .get(path, { - 'content-type': 'application/json', - }, payload); - }, - - /** - * Delete Tag - * - * Delete a code tag by its unique ID. - * - * @param {string} functionId - * @param {string} tagId - * @throws {Error} - * @return {Promise} - */ - deleteTag: function(functionId, tagId) { - if(functionId === undefined) { - throw new Error('Missing required parameter: "functionId"'); - } - - if(tagId === undefined) { - throw new Error('Missing required parameter: "tagId"'); - } - - let path = '/functions/{functionId}/tags/{tagId}'.replace(new RegExp('{functionId}', 'g'), functionId).replace(new RegExp('{tagId}', 'g'), tagId); - - let payload = {}; - - return http - .delete(path, { - 'content-type': 'application/json', - }, payload); - }, - - /** - * Get Function Usage - * - * - * @param {string} functionId - * @param {string} range - * @throws {Error} - * @return {Promise} - */ - getUsage: function(functionId, range = '30d') { - if(functionId === undefined) { - throw new Error('Missing required parameter: "functionId"'); - } - - let path = '/functions/{functionId}/usage'.replace(new RegExp('{functionId}', 'g'), functionId); - - let payload = {}; - - if(range) { - payload['range'] = range; - } - - return http - .get(path, { - 'content-type': 'application/json', - }, payload); - } - }; - - let health = { - - /** - * Get HTTP - * - * Check the Appwrite HTTP server is up and responsive. - * - * @throws {Error} - * @return {Promise} - */ - get: function() { - let path = '/health'; - - let payload = {}; - - return http - .get(path, { - 'content-type': 'application/json', - }, payload); - }, - - /** - * Get Anti virus - * - * Check the Appwrite Anti Virus server is up and connection is successful. - * - * @throws {Error} - * @return {Promise} - */ - getAntiVirus: function() { - let path = '/health/anti-virus'; - - let payload = {}; - - return http - .get(path, { - 'content-type': 'application/json', - }, payload); - }, - - /** - * Get Cache - * - * Check the Appwrite in-memory cache server is up and connection is - * successful. - * - * @throws {Error} - * @return {Promise} - */ - getCache: function() { - let path = '/health/cache'; - - let payload = {}; - - return http - .get(path, { - 'content-type': 'application/json', - }, payload); - }, - - /** - * Get DB - * - * Check the Appwrite database server is up and connection is successful. - * - * @throws {Error} - * @return {Promise} - */ - getDB: function() { - let path = '/health/db'; - - let payload = {}; - - return http - .get(path, { - 'content-type': 'application/json', - }, payload); - }, - - /** - * Get Certificate Queue - * - * Get the number of certificates that are waiting to be issued against - * [Letsencrypt](https://letsencrypt.org/) in the Appwrite internal queue - * server. - * - * @throws {Error} - * @return {Promise} - */ - getQueueCertificates: function() { - let path = '/health/queue/certificates'; - - let payload = {}; - - return http - .get(path, { - 'content-type': 'application/json', - }, payload); - }, - - /** - * Get Functions Queue - * - * - * @throws {Error} - * @return {Promise} - */ - getQueueFunctions: function() { - let path = '/health/queue/functions'; - - let payload = {}; - - return http - .get(path, { - 'content-type': 'application/json', - }, payload); - }, - - /** - * Get Logs Queue - * - * Get the number of logs that are waiting to be processed in the Appwrite - * internal queue server. - * - * @throws {Error} - * @return {Promise} - */ - getQueueLogs: function() { - let path = '/health/queue/logs'; - - let payload = {}; - - return http - .get(path, { - 'content-type': 'application/json', - }, payload); - }, - - /** - * Get Tasks Queue - * - * Get the number of tasks that are waiting to be processed in the Appwrite - * internal queue server. - * - * @throws {Error} - * @return {Promise} - */ - getQueueTasks: function() { - let path = '/health/queue/tasks'; - - let payload = {}; - - return http - .get(path, { - 'content-type': 'application/json', - }, payload); - }, - - /** - * Get Usage Queue - * - * Get the number of usage stats that are waiting to be processed in the - * Appwrite internal queue server. - * - * @throws {Error} - * @return {Promise} - */ - getQueueUsage: function() { - let path = '/health/queue/usage'; - - let payload = {}; - - return http - .get(path, { - 'content-type': 'application/json', - }, payload); - }, - - /** - * Get Webhooks Queue - * - * Get the number of webhooks that are waiting to be processed in the Appwrite - * internal queue server. - * - * @throws {Error} - * @return {Promise} - */ - getQueueWebhooks: function() { - let path = '/health/queue/webhooks'; - - let payload = {}; - - return http - .get(path, { - 'content-type': 'application/json', - }, payload); - }, - - /** - * Get Local Storage - * - * Check the Appwrite local storage device is up and connection is successful. - * - * @throws {Error} - * @return {Promise} - */ - getStorageLocal: function() { - let path = '/health/storage/local'; - - let payload = {}; - - return http - .get(path, { - 'content-type': 'application/json', - }, payload); - }, - - /** - * Get Time - * - * Check the Appwrite server time is synced with Google remote NTP server. We - * use this technology to smoothly handle leap seconds with no disruptive - * events. The [Network Time - * Protocol](https://en.wikipedia.org/wiki/Network_Time_Protocol) (NTP) is - * used by hundreds of millions of computers and devices to synchronize their - * clocks over the Internet. If your computer sets its own clock, it likely - * uses NTP. - * - * @throws {Error} - * @return {Promise} - */ - getTime: function() { - let path = '/health/time'; - - let payload = {}; - - return http - .get(path, { - 'content-type': 'application/json', - }, payload); - } - }; - - let locale = { - - /** - * Get User Locale - * - * Get the current user location based on IP. Returns an object with user - * country code, country name, continent name, continent code, ip address and - * suggested currency. You can use the locale header to get the data in a - * supported language. - * - * ([IP Geolocation by DB-IP](https://db-ip.com)) - * - * @throws {Error} - * @return {Promise} - */ - get: function() { - let path = '/locale'; - - let payload = {}; - - return http - .get(path, { - 'content-type': 'application/json', - }, payload); - }, - - /** - * List Continents - * - * List of all continents. You can use the locale header to get the data in a - * supported language. - * - * @throws {Error} - * @return {Promise} - */ - getContinents: function() { - let path = '/locale/continents'; - - let payload = {}; - - return http - .get(path, { - 'content-type': 'application/json', - }, payload); - }, - - /** - * List Countries - * - * List of all countries. You can use the locale header to get the data in a - * supported language. - * - * @throws {Error} - * @return {Promise} - */ - getCountries: function() { - let path = '/locale/countries'; - - let payload = {}; - - return http - .get(path, { - 'content-type': 'application/json', - }, payload); - }, - - /** - * List EU Countries - * - * List of all countries that are currently members of the EU. You can use the - * locale header to get the data in a supported language. - * - * @throws {Error} - * @return {Promise} - */ - getCountriesEU: function() { - let path = '/locale/countries/eu'; - - let payload = {}; - - return http - .get(path, { - 'content-type': 'application/json', - }, payload); - }, - - /** - * List Countries Phone Codes - * - * List of all countries phone codes. You can use the locale header to get the - * data in a supported language. - * - * @throws {Error} - * @return {Promise} - */ - getCountriesPhones: function() { - let path = '/locale/countries/phones'; - - let payload = {}; - - return http - .get(path, { - 'content-type': 'application/json', - }, payload); - }, - - /** - * List Currencies - * - * List of all currencies, including currency symbol, name, plural, and - * decimal digits for all major and minor currencies. You can use the locale - * header to get the data in a supported language. - * - * @throws {Error} - * @return {Promise} - */ - getCurrencies: function() { - let path = '/locale/currencies'; - - let payload = {}; - - return http - .get(path, { - 'content-type': 'application/json', - }, payload); - }, - - /** - * List Languages - * - * List of all languages classified by ISO 639-1 including 2-letter code, name - * in English, and name in the respective language. - * - * @throws {Error} - * @return {Promise} - */ - getLanguages: function() { - let path = '/locale/languages'; - - let payload = {}; - - return http - .get(path, { - 'content-type': 'application/json', - }, payload); - } - }; - - let projects = { - - /** - * List Projects - * - * - * @param {string} search - * @param {number} limit - * @param {number} offset - * @param {string} orderType - * @throws {Error} - * @return {Promise} - */ - list: function(search = '', limit = 25, offset = 0, orderType = 'ASC') { - let path = '/projects'; - - let payload = {}; - - if(search) { - payload['search'] = search; - } - - if(limit) { - payload['limit'] = limit; - } - - if(offset) { - payload['offset'] = offset; - } - - if(orderType) { - payload['orderType'] = orderType; - } - - return http - .get(path, { - 'content-type': 'application/json', - }, payload); - }, - - /** - * Create Project - * - * - * @param {string} name - * @param {string} teamId - * @param {string} description - * @param {string} logo - * @param {string} url - * @param {string} legalName - * @param {string} legalCountry - * @param {string} legalState - * @param {string} legalCity - * @param {string} legalAddress - * @param {string} legalTaxId - * @throws {Error} - * @return {Promise} - */ - create: function(name, teamId, description = '', logo = '', url = '', legalName = '', legalCountry = '', legalState = '', legalCity = '', legalAddress = '', legalTaxId = '') { - if(name === undefined) { - throw new Error('Missing required parameter: "name"'); - } - - if(teamId === undefined) { - throw new Error('Missing required parameter: "teamId"'); - } - - let path = '/projects'; - - let payload = {}; - - if(typeof name !== 'undefined') { - payload['name'] = name; - } - - if(typeof teamId !== 'undefined') { - payload['teamId'] = teamId; - } - - if(typeof description !== 'undefined') { - payload['description'] = description; - } - - if(typeof logo !== 'undefined') { - payload['logo'] = logo; - } - - if(typeof url !== 'undefined') { - payload['url'] = url; - } - - if(typeof legalName !== 'undefined') { - payload['legalName'] = legalName; - } - - if(typeof legalCountry !== 'undefined') { - payload['legalCountry'] = legalCountry; - } - - if(typeof legalState !== 'undefined') { - payload['legalState'] = legalState; - } - - if(typeof legalCity !== 'undefined') { - payload['legalCity'] = legalCity; - } - - if(typeof legalAddress !== 'undefined') { - payload['legalAddress'] = legalAddress; - } - - if(typeof legalTaxId !== 'undefined') { - payload['legalTaxId'] = legalTaxId; - } - - return http - .post(path, { - 'content-type': 'application/json', - }, payload); - }, - - /** - * Get Project - * - * - * @param {string} projectId - * @throws {Error} - * @return {Promise} - */ - get: function(projectId) { - if(projectId === undefined) { - throw new Error('Missing required parameter: "projectId"'); - } - - let path = '/projects/{projectId}'.replace(new RegExp('{projectId}', 'g'), projectId); - - let payload = {}; - - return http - .get(path, { - 'content-type': 'application/json', - }, payload); - }, - - /** - * Update Project - * - * - * @param {string} projectId - * @param {string} name - * @param {string} description - * @param {string} logo - * @param {string} url - * @param {string} legalName - * @param {string} legalCountry - * @param {string} legalState - * @param {string} legalCity - * @param {string} legalAddress - * @param {string} legalTaxId - * @throws {Error} - * @return {Promise} - */ - update: function(projectId, name, description = '', logo = '', url = '', legalName = '', legalCountry = '', legalState = '', legalCity = '', legalAddress = '', legalTaxId = '') { - if(projectId === undefined) { - throw new Error('Missing required parameter: "projectId"'); - } - - if(name === undefined) { - throw new Error('Missing required parameter: "name"'); - } - - let path = '/projects/{projectId}'.replace(new RegExp('{projectId}', 'g'), projectId); - - let payload = {}; - - if(typeof name !== 'undefined') { - payload['name'] = name; - } - - if(typeof description !== 'undefined') { - payload['description'] = description; - } - - if(typeof logo !== 'undefined') { - payload['logo'] = logo; - } - - if(typeof url !== 'undefined') { - payload['url'] = url; - } - - if(typeof legalName !== 'undefined') { - payload['legalName'] = legalName; - } - - if(typeof legalCountry !== 'undefined') { - payload['legalCountry'] = legalCountry; - } - - if(typeof legalState !== 'undefined') { - payload['legalState'] = legalState; - } - - if(typeof legalCity !== 'undefined') { - payload['legalCity'] = legalCity; - } - - if(typeof legalAddress !== 'undefined') { - payload['legalAddress'] = legalAddress; - } - - if(typeof legalTaxId !== 'undefined') { - payload['legalTaxId'] = legalTaxId; - } - - return http - .patch(path, { - 'content-type': 'application/json', - }, payload); - }, - - /** - * Delete Project - * - * - * @param {string} projectId - * @param {string} password - * @throws {Error} - * @return {Promise} - */ - delete: function(projectId, password) { - if(projectId === undefined) { - throw new Error('Missing required parameter: "projectId"'); - } - - if(password === undefined) { - throw new Error('Missing required parameter: "password"'); - } - - let path = '/projects/{projectId}'.replace(new RegExp('{projectId}', 'g'), projectId); - - let payload = {}; - - if(typeof password !== 'undefined') { - payload['password'] = password; - } - - return http - .delete(path, { - 'content-type': 'application/json', - }, payload); - }, - - /** - * Update Project users limit - * - * - * @param {string} projectId - * @param {string} limit - * @throws {Error} - * @return {Promise} - */ - updateAuthLimit: function(projectId, limit) { - if(projectId === undefined) { - throw new Error('Missing required parameter: "projectId"'); - } - - if(limit === undefined) { - throw new Error('Missing required parameter: "limit"'); - } - - let path = '/projects/{projectId}/auth/limit'.replace(new RegExp('{projectId}', 'g'), projectId); - - let payload = {}; - - if(typeof limit !== 'undefined') { - payload['limit'] = limit; - } - - return http - .patch(path, { - 'content-type': 'application/json', - }, payload); - }, - - /** - * Update Project auth method status. Use this endpoint to enable or disable a given auth method for this project. - * - * - * @param {string} projectId - * @param {string} method - * @param {boolean} status - * @throws {Error} - * @return {Promise} - */ - updateAuthStatus: function(projectId, method, status) { - if(projectId === undefined) { - throw new Error('Missing required parameter: "projectId"'); - } - - if(method === undefined) { - throw new Error('Missing required parameter: "method"'); - } - - if(status === undefined) { - throw new Error('Missing required parameter: "status"'); - } - - let path = '/projects/{projectId}/auth/{method}'.replace(new RegExp('{projectId}', 'g'), projectId).replace(new RegExp('{method}', 'g'), method); - - let payload = {}; - - if(typeof status !== 'undefined') { - payload['status'] = status; - } - - return http - .patch(path, { - 'content-type': 'application/json', - }, payload); - }, - - /** - * List Domains - * - * - * @param {string} projectId - * @throws {Error} - * @return {Promise} - */ - listDomains: function(projectId) { - if(projectId === undefined) { - throw new Error('Missing required parameter: "projectId"'); - } - - let path = '/projects/{projectId}/domains'.replace(new RegExp('{projectId}', 'g'), projectId); - - let payload = {}; - - return http - .get(path, { - 'content-type': 'application/json', - }, payload); - }, - - /** - * Create Domain - * - * - * @param {string} projectId - * @param {string} domain - * @throws {Error} - * @return {Promise} - */ - createDomain: function(projectId, domain) { - if(projectId === undefined) { - throw new Error('Missing required parameter: "projectId"'); - } - - if(domain === undefined) { - throw new Error('Missing required parameter: "domain"'); - } - - let path = '/projects/{projectId}/domains'.replace(new RegExp('{projectId}', 'g'), projectId); - - let payload = {}; - - if(typeof domain !== 'undefined') { - payload['domain'] = domain; - } - - return http - .post(path, { - 'content-type': 'application/json', - }, payload); - }, - - /** - * Get Domain - * - * - * @param {string} projectId - * @param {string} domainId - * @throws {Error} - * @return {Promise} - */ - getDomain: function(projectId, domainId) { - if(projectId === undefined) { - throw new Error('Missing required parameter: "projectId"'); - } - - if(domainId === undefined) { - throw new Error('Missing required parameter: "domainId"'); - } - - let path = '/projects/{projectId}/domains/{domainId}'.replace(new RegExp('{projectId}', 'g'), projectId).replace(new RegExp('{domainId}', 'g'), domainId); - - let payload = {}; - - return http - .get(path, { - 'content-type': 'application/json', - }, payload); - }, - - /** - * Delete Domain - * - * - * @param {string} projectId - * @param {string} domainId - * @throws {Error} - * @return {Promise} - */ - deleteDomain: function(projectId, domainId) { - if(projectId === undefined) { - throw new Error('Missing required parameter: "projectId"'); - } - - if(domainId === undefined) { - throw new Error('Missing required parameter: "domainId"'); - } - - let path = '/projects/{projectId}/domains/{domainId}'.replace(new RegExp('{projectId}', 'g'), projectId).replace(new RegExp('{domainId}', 'g'), domainId); - - let payload = {}; - - return http - .delete(path, { - 'content-type': 'application/json', - }, payload); - }, - - /** - * Update Domain Verification Status - * - * - * @param {string} projectId - * @param {string} domainId - * @throws {Error} - * @return {Promise} - */ - updateDomainVerification: function(projectId, domainId) { - if(projectId === undefined) { - throw new Error('Missing required parameter: "projectId"'); - } - - if(domainId === undefined) { - throw new Error('Missing required parameter: "domainId"'); - } - - let path = '/projects/{projectId}/domains/{domainId}/verification'.replace(new RegExp('{projectId}', 'g'), projectId).replace(new RegExp('{domainId}', 'g'), domainId); - - let payload = {}; - - return http - .patch(path, { - 'content-type': 'application/json', - }, payload); - }, - - /** - * List Keys - * - * - * @param {string} projectId - * @throws {Error} - * @return {Promise} - */ - listKeys: function(projectId) { - if(projectId === undefined) { - throw new Error('Missing required parameter: "projectId"'); - } - - let path = '/projects/{projectId}/keys'.replace(new RegExp('{projectId}', 'g'), projectId); - - let payload = {}; - - return http - .get(path, { - 'content-type': 'application/json', - }, payload); - }, - - /** - * Create Key - * - * - * @param {string} projectId - * @param {string} name - * @param {string[]} scopes - * @throws {Error} - * @return {Promise} - */ - createKey: function(projectId, name, scopes) { - if(projectId === undefined) { - throw new Error('Missing required parameter: "projectId"'); - } - - if(name === undefined) { - throw new Error('Missing required parameter: "name"'); - } - - if(scopes === undefined) { - throw new Error('Missing required parameter: "scopes"'); - } - - let path = '/projects/{projectId}/keys'.replace(new RegExp('{projectId}', 'g'), projectId); - - let payload = {}; - - if(typeof name !== 'undefined') { - payload['name'] = name; - } - - if(typeof scopes !== 'undefined') { - payload['scopes'] = scopes; - } - - return http - .post(path, { - 'content-type': 'application/json', - }, payload); - }, - - /** - * Get Key - * - * - * @param {string} projectId - * @param {string} keyId - * @throws {Error} - * @return {Promise} - */ - getKey: function(projectId, keyId) { - if(projectId === undefined) { - throw new Error('Missing required parameter: "projectId"'); - } - - if(keyId === undefined) { - throw new Error('Missing required parameter: "keyId"'); - } - - let path = '/projects/{projectId}/keys/{keyId}'.replace(new RegExp('{projectId}', 'g'), projectId).replace(new RegExp('{keyId}', 'g'), keyId); - - let payload = {}; - - return http - .get(path, { - 'content-type': 'application/json', - }, payload); - }, - - /** - * Update Key - * - * - * @param {string} projectId - * @param {string} keyId - * @param {string} name - * @param {string[]} scopes - * @throws {Error} - * @return {Promise} - */ - updateKey: function(projectId, keyId, name, scopes) { - if(projectId === undefined) { - throw new Error('Missing required parameter: "projectId"'); - } - - if(keyId === undefined) { - throw new Error('Missing required parameter: "keyId"'); - } - - if(name === undefined) { - throw new Error('Missing required parameter: "name"'); - } - - if(scopes === undefined) { - throw new Error('Missing required parameter: "scopes"'); - } - - let path = '/projects/{projectId}/keys/{keyId}'.replace(new RegExp('{projectId}', 'g'), projectId).replace(new RegExp('{keyId}', 'g'), keyId); - - let payload = {}; - - if(typeof name !== 'undefined') { - payload['name'] = name; - } - - if(typeof scopes !== 'undefined') { - payload['scopes'] = scopes; - } - - return http - .put(path, { - 'content-type': 'application/json', - }, payload); - }, - - /** - * Delete Key - * - * - * @param {string} projectId - * @param {string} keyId - * @throws {Error} - * @return {Promise} - */ - deleteKey: function(projectId, keyId) { - if(projectId === undefined) { - throw new Error('Missing required parameter: "projectId"'); - } - - if(keyId === undefined) { - throw new Error('Missing required parameter: "keyId"'); - } - - let path = '/projects/{projectId}/keys/{keyId}'.replace(new RegExp('{projectId}', 'g'), projectId).replace(new RegExp('{keyId}', 'g'), keyId); - - let payload = {}; - - return http - .delete(path, { - 'content-type': 'application/json', - }, payload); - }, - - /** - * Update Project OAuth2 - * - * - * @param {string} projectId - * @param {string} provider - * @param {string} appId - * @param {string} secret - * @throws {Error} - * @return {Promise} - */ - updateOAuth2: function(projectId, provider, appId = '', secret = '') { - if(projectId === undefined) { - throw new Error('Missing required parameter: "projectId"'); - } - - if(provider === undefined) { - throw new Error('Missing required parameter: "provider"'); - } - - let path = '/projects/{projectId}/oauth2'.replace(new RegExp('{projectId}', 'g'), projectId); - - let payload = {}; - - if(typeof provider !== 'undefined') { - payload['provider'] = provider; - } - - if(typeof appId !== 'undefined') { - payload['appId'] = appId; - } - - if(typeof secret !== 'undefined') { - payload['secret'] = secret; - } - - return http - .patch(path, { - 'content-type': 'application/json', - }, payload); - }, - - /** - * List Platforms - * - * - * @param {string} projectId - * @throws {Error} - * @return {Promise} - */ - listPlatforms: function(projectId) { - if(projectId === undefined) { - throw new Error('Missing required parameter: "projectId"'); - } - - let path = '/projects/{projectId}/platforms'.replace(new RegExp('{projectId}', 'g'), projectId); - - let payload = {}; - - return http - .get(path, { - 'content-type': 'application/json', - }, payload); - }, - - /** - * Create Platform - * - * - * @param {string} projectId - * @param {string} type - * @param {string} name - * @param {string} key - * @param {string} store - * @param {string} hostname - * @throws {Error} - * @return {Promise} - */ - createPlatform: function(projectId, type, name, key = '', store = '', hostname = '') { - if(projectId === undefined) { - throw new Error('Missing required parameter: "projectId"'); - } - - if(type === undefined) { - throw new Error('Missing required parameter: "type"'); - } - - if(name === undefined) { - throw new Error('Missing required parameter: "name"'); - } - - let path = '/projects/{projectId}/platforms'.replace(new RegExp('{projectId}', 'g'), projectId); - - let payload = {}; - - if(typeof type !== 'undefined') { - payload['type'] = type; - } - - if(typeof name !== 'undefined') { - payload['name'] = name; - } - - if(typeof key !== 'undefined') { - payload['key'] = key; - } - - if(typeof store !== 'undefined') { - payload['store'] = store; - } - - if(typeof hostname !== 'undefined') { - payload['hostname'] = hostname; - } - - return http - .post(path, { - 'content-type': 'application/json', - }, payload); - }, - - /** - * Get Platform - * - * - * @param {string} projectId - * @param {string} platformId - * @throws {Error} - * @return {Promise} - */ - getPlatform: function(projectId, platformId) { - if(projectId === undefined) { - throw new Error('Missing required parameter: "projectId"'); - } - - if(platformId === undefined) { - throw new Error('Missing required parameter: "platformId"'); - } - - let path = '/projects/{projectId}/platforms/{platformId}'.replace(new RegExp('{projectId}', 'g'), projectId).replace(new RegExp('{platformId}', 'g'), platformId); - - let payload = {}; - - return http - .get(path, { - 'content-type': 'application/json', - }, payload); - }, - - /** - * Update Platform - * - * - * @param {string} projectId - * @param {string} platformId - * @param {string} name - * @param {string} key - * @param {string} store - * @param {string} hostname - * @throws {Error} - * @return {Promise} - */ - updatePlatform: function(projectId, platformId, name, key = '', store = '', hostname = '') { - if(projectId === undefined) { - throw new Error('Missing required parameter: "projectId"'); - } - - if(platformId === undefined) { - throw new Error('Missing required parameter: "platformId"'); - } - - if(name === undefined) { - throw new Error('Missing required parameter: "name"'); - } - - let path = '/projects/{projectId}/platforms/{platformId}'.replace(new RegExp('{projectId}', 'g'), projectId).replace(new RegExp('{platformId}', 'g'), platformId); - - let payload = {}; - - if(typeof name !== 'undefined') { - payload['name'] = name; - } - - if(typeof key !== 'undefined') { - payload['key'] = key; - } - - if(typeof store !== 'undefined') { - payload['store'] = store; - } - - if(typeof hostname !== 'undefined') { - payload['hostname'] = hostname; - } - - return http - .put(path, { - 'content-type': 'application/json', - }, payload); - }, - - /** - * Delete Platform - * - * - * @param {string} projectId - * @param {string} platformId - * @throws {Error} - * @return {Promise} - */ - deletePlatform: function(projectId, platformId) { - if(projectId === undefined) { - throw new Error('Missing required parameter: "projectId"'); - } - - if(platformId === undefined) { - throw new Error('Missing required parameter: "platformId"'); - } - - let path = '/projects/{projectId}/platforms/{platformId}'.replace(new RegExp('{projectId}', 'g'), projectId).replace(new RegExp('{platformId}', 'g'), platformId); - - let payload = {}; - - return http - .delete(path, { - 'content-type': 'application/json', - }, payload); - }, - - /** - * List Tasks - * - * - * @param {string} projectId - * @throws {Error} - * @return {Promise} - */ - listTasks: function(projectId) { - if(projectId === undefined) { - throw new Error('Missing required parameter: "projectId"'); - } - - let path = '/projects/{projectId}/tasks'.replace(new RegExp('{projectId}', 'g'), projectId); - - let payload = {}; - - return http - .get(path, { - 'content-type': 'application/json', - }, payload); - }, - - /** - * Create Task - * - * - * @param {string} projectId - * @param {string} name - * @param {string} status - * @param {string} schedule - * @param {boolean} security - * @param {string} httpMethod - * @param {string} httpUrl - * @param {string[]} httpHeaders - * @param {string} httpUser - * @param {string} httpPass - * @throws {Error} - * @return {Promise} - */ - createTask: function(projectId, name, status, schedule, security, httpMethod, httpUrl, httpHeaders = [], httpUser = '', httpPass = '') { - if(projectId === undefined) { - throw new Error('Missing required parameter: "projectId"'); - } - - if(name === undefined) { - throw new Error('Missing required parameter: "name"'); - } - - if(status === undefined) { - throw new Error('Missing required parameter: "status"'); - } - - if(schedule === undefined) { - throw new Error('Missing required parameter: "schedule"'); - } - - if(security === undefined) { - throw new Error('Missing required parameter: "security"'); - } - - if(httpMethod === undefined) { - throw new Error('Missing required parameter: "httpMethod"'); - } - - if(httpUrl === undefined) { - throw new Error('Missing required parameter: "httpUrl"'); - } - - let path = '/projects/{projectId}/tasks'.replace(new RegExp('{projectId}', 'g'), projectId); - - let payload = {}; - - if(typeof name !== 'undefined') { - payload['name'] = name; - } - - if(typeof status !== 'undefined') { - payload['status'] = status; - } - - if(typeof schedule !== 'undefined') { - payload['schedule'] = schedule; - } - - if(typeof security !== 'undefined') { - payload['security'] = security; - } - - if(typeof httpMethod !== 'undefined') { - payload['httpMethod'] = httpMethod; - } - - if(typeof httpUrl !== 'undefined') { - payload['httpUrl'] = httpUrl; - } - - if(typeof httpHeaders !== 'undefined') { - payload['httpHeaders'] = httpHeaders; - } - - if(typeof httpUser !== 'undefined') { - payload['httpUser'] = httpUser; - } - - if(typeof httpPass !== 'undefined') { - payload['httpPass'] = httpPass; - } - - return http - .post(path, { - 'content-type': 'application/json', - }, payload); - }, - - /** - * Get Task - * - * - * @param {string} projectId - * @param {string} taskId - * @throws {Error} - * @return {Promise} - */ - getTask: function(projectId, taskId) { - if(projectId === undefined) { - throw new Error('Missing required parameter: "projectId"'); - } - - if(taskId === undefined) { - throw new Error('Missing required parameter: "taskId"'); - } - - let path = '/projects/{projectId}/tasks/{taskId}'.replace(new RegExp('{projectId}', 'g'), projectId).replace(new RegExp('{taskId}', 'g'), taskId); - - let payload = {}; - - return http - .get(path, { - 'content-type': 'application/json', - }, payload); - }, - - /** - * Update Task - * - * - * @param {string} projectId - * @param {string} taskId - * @param {string} name - * @param {string} status - * @param {string} schedule - * @param {boolean} security - * @param {string} httpMethod - * @param {string} httpUrl - * @param {string[]} httpHeaders - * @param {string} httpUser - * @param {string} httpPass - * @throws {Error} - * @return {Promise} - */ - updateTask: function(projectId, taskId, name, status, schedule, security, httpMethod, httpUrl, httpHeaders = [], httpUser = '', httpPass = '') { - if(projectId === undefined) { - throw new Error('Missing required parameter: "projectId"'); - } - - if(taskId === undefined) { - throw new Error('Missing required parameter: "taskId"'); - } - - if(name === undefined) { - throw new Error('Missing required parameter: "name"'); - } - - if(status === undefined) { - throw new Error('Missing required parameter: "status"'); - } - - if(schedule === undefined) { - throw new Error('Missing required parameter: "schedule"'); - } - - if(security === undefined) { - throw new Error('Missing required parameter: "security"'); - } - - if(httpMethod === undefined) { - throw new Error('Missing required parameter: "httpMethod"'); - } - - if(httpUrl === undefined) { - throw new Error('Missing required parameter: "httpUrl"'); - } - - let path = '/projects/{projectId}/tasks/{taskId}'.replace(new RegExp('{projectId}', 'g'), projectId).replace(new RegExp('{taskId}', 'g'), taskId); - - let payload = {}; - - if(typeof name !== 'undefined') { - payload['name'] = name; - } - - if(typeof status !== 'undefined') { - payload['status'] = status; - } - - if(typeof schedule !== 'undefined') { - payload['schedule'] = schedule; - } - - if(typeof security !== 'undefined') { - payload['security'] = security; - } - - if(typeof httpMethod !== 'undefined') { - payload['httpMethod'] = httpMethod; - } - - if(typeof httpUrl !== 'undefined') { - payload['httpUrl'] = httpUrl; - } - - if(typeof httpHeaders !== 'undefined') { - payload['httpHeaders'] = httpHeaders; - } - - if(typeof httpUser !== 'undefined') { - payload['httpUser'] = httpUser; - } - - if(typeof httpPass !== 'undefined') { - payload['httpPass'] = httpPass; - } - - return http - .put(path, { - 'content-type': 'application/json', - }, payload); - }, - - /** - * Delete Task - * - * - * @param {string} projectId - * @param {string} taskId - * @throws {Error} - * @return {Promise} - */ - deleteTask: function(projectId, taskId) { - if(projectId === undefined) { - throw new Error('Missing required parameter: "projectId"'); - } - - if(taskId === undefined) { - throw new Error('Missing required parameter: "taskId"'); - } - - let path = '/projects/{projectId}/tasks/{taskId}'.replace(new RegExp('{projectId}', 'g'), projectId).replace(new RegExp('{taskId}', 'g'), taskId); - - let payload = {}; - - return http - .delete(path, { - 'content-type': 'application/json', - }, payload); - }, - - /** - * Get Project - * - * - * @param {string} projectId - * @param {string} range - * @throws {Error} - * @return {Promise} - */ - getUsage: function(projectId, range = '30d') { - if(projectId === undefined) { - throw new Error('Missing required parameter: "projectId"'); - } - - let path = '/projects/{projectId}/usage'.replace(new RegExp('{projectId}', 'g'), projectId); - - let payload = {}; - - if(range) { - payload['range'] = range; - } - - return http - .get(path, { - 'content-type': 'application/json', - }, payload); - }, - - /** - * List Webhooks - * - * - * @param {string} projectId - * @throws {Error} - * @return {Promise} - */ - listWebhooks: function(projectId) { - if(projectId === undefined) { - throw new Error('Missing required parameter: "projectId"'); - } - - let path = '/projects/{projectId}/webhooks'.replace(new RegExp('{projectId}', 'g'), projectId); - - let payload = {}; - - return http - .get(path, { - 'content-type': 'application/json', - }, payload); - }, - - /** - * Create Webhook - * - * - * @param {string} projectId - * @param {string} name - * @param {string[]} events - * @param {string} url - * @param {boolean} security - * @param {string} httpUser - * @param {string} httpPass - * @throws {Error} - * @return {Promise} - */ - createWebhook: function(projectId, name, events, url, security, httpUser = '', httpPass = '') { - if(projectId === undefined) { - throw new Error('Missing required parameter: "projectId"'); - } - - if(name === undefined) { - throw new Error('Missing required parameter: "name"'); - } - - if(events === undefined) { - throw new Error('Missing required parameter: "events"'); - } - - if(url === undefined) { - throw new Error('Missing required parameter: "url"'); - } - - if(security === undefined) { - throw new Error('Missing required parameter: "security"'); - } - - let path = '/projects/{projectId}/webhooks'.replace(new RegExp('{projectId}', 'g'), projectId); - - let payload = {}; - - if(typeof name !== 'undefined') { - payload['name'] = name; - } - - if(typeof events !== 'undefined') { - payload['events'] = events; - } - - if(typeof url !== 'undefined') { - payload['url'] = url; - } - - if(typeof security !== 'undefined') { - payload['security'] = security; - } - - if(typeof httpUser !== 'undefined') { - payload['httpUser'] = httpUser; - } - - if(typeof httpPass !== 'undefined') { - payload['httpPass'] = httpPass; - } - - return http - .post(path, { - 'content-type': 'application/json', - }, payload); - }, - - /** - * Get Webhook - * - * - * @param {string} projectId - * @param {string} webhookId - * @throws {Error} - * @return {Promise} - */ - getWebhook: function(projectId, webhookId) { - if(projectId === undefined) { - throw new Error('Missing required parameter: "projectId"'); - } - - if(webhookId === undefined) { - throw new Error('Missing required parameter: "webhookId"'); - } - - let path = '/projects/{projectId}/webhooks/{webhookId}'.replace(new RegExp('{projectId}', 'g'), projectId).replace(new RegExp('{webhookId}', 'g'), webhookId); - - let payload = {}; - - return http - .get(path, { - 'content-type': 'application/json', - }, payload); - }, - - /** - * Update Webhook - * - * - * @param {string} projectId - * @param {string} webhookId - * @param {string} name - * @param {string[]} events - * @param {string} url - * @param {boolean} security - * @param {string} httpUser - * @param {string} httpPass - * @throws {Error} - * @return {Promise} - */ - updateWebhook: function(projectId, webhookId, name, events, url, security, httpUser = '', httpPass = '') { - if(projectId === undefined) { - throw new Error('Missing required parameter: "projectId"'); - } - - if(webhookId === undefined) { - throw new Error('Missing required parameter: "webhookId"'); - } - - if(name === undefined) { - throw new Error('Missing required parameter: "name"'); - } - - if(events === undefined) { - throw new Error('Missing required parameter: "events"'); - } - - if(url === undefined) { - throw new Error('Missing required parameter: "url"'); - } - - if(security === undefined) { - throw new Error('Missing required parameter: "security"'); - } - - let path = '/projects/{projectId}/webhooks/{webhookId}'.replace(new RegExp('{projectId}', 'g'), projectId).replace(new RegExp('{webhookId}', 'g'), webhookId); - - let payload = {}; - - if(typeof name !== 'undefined') { - payload['name'] = name; - } - - if(typeof events !== 'undefined') { - payload['events'] = events; - } - - if(typeof url !== 'undefined') { - payload['url'] = url; - } - - if(typeof security !== 'undefined') { - payload['security'] = security; - } - - if(typeof httpUser !== 'undefined') { - payload['httpUser'] = httpUser; - } - - if(typeof httpPass !== 'undefined') { - payload['httpPass'] = httpPass; - } - - return http - .put(path, { - 'content-type': 'application/json', - }, payload); - }, - - /** - * Delete Webhook - * - * - * @param {string} projectId - * @param {string} webhookId - * @throws {Error} - * @return {Promise} - */ - deleteWebhook: function(projectId, webhookId) { - if(projectId === undefined) { - throw new Error('Missing required parameter: "projectId"'); - } - - if(webhookId === undefined) { - throw new Error('Missing required parameter: "webhookId"'); - } - - let path = '/projects/{projectId}/webhooks/{webhookId}'.replace(new RegExp('{projectId}', 'g'), projectId).replace(new RegExp('{webhookId}', 'g'), webhookId); - - let payload = {}; - - return http - .delete(path, { - 'content-type': 'application/json', - }, payload); - } - }; - - let storage = { - - /** - * List Files - * - * Get a list of all the user files. You can use the query params to filter - * your results. On admin mode, this endpoint will return a list of all of the - * project's files. [Learn more about different API modes](/docs/admin). - * - * @param {string} search - * @param {number} limit - * @param {number} offset - * @param {string} orderType - * @throws {Error} - * @return {Promise} - */ - listFiles: function(search = '', limit = 25, offset = 0, orderType = 'ASC') { - let path = '/storage/files'; - - let payload = {}; - - if(search) { - payload['search'] = search; - } - - if(limit) { - payload['limit'] = limit; - } - - if(offset) { - payload['offset'] = offset; - } - - if(orderType) { - payload['orderType'] = orderType; - } - - return http - .get(path, { - 'content-type': 'application/json', - }, payload); - }, - - /** - * Create File - * - * Create a new file. The user who creates the file will automatically be - * assigned to read and write access unless he has passed custom values for - * read and write arguments. - * - * @param {File} file - * @param {string[]} read - * @param {string[]} write - * @throws {Error} - * @return {Promise} - */ - createFile: function(file, read, write) { - if(file === undefined) { - throw new Error('Missing required parameter: "file"'); - } - - if(read === undefined) { - throw new Error('Missing required parameter: "read"'); - } - - if(write === undefined) { - throw new Error('Missing required parameter: "write"'); - } - - let path = '/storage/files'; - - let payload = {}; - - if(typeof file !== 'undefined') { - payload['file'] = file; - } - - if(typeof read !== 'undefined') { - payload['read'] = read; - } - - if(typeof write !== 'undefined') { - payload['write'] = write; - } - - return http - .post(path, { - 'content-type': 'multipart/form-data', - }, payload); - }, - - /** - * Get File - * - * Get a file by its unique ID. This endpoint response returns a JSON object - * with the file metadata. - * - * @param {string} fileId - * @throws {Error} - * @return {Promise} - */ - getFile: function(fileId) { - if(fileId === undefined) { - throw new Error('Missing required parameter: "fileId"'); - } - - let path = '/storage/files/{fileId}'.replace(new RegExp('{fileId}', 'g'), fileId); - - let payload = {}; - - return http - .get(path, { - 'content-type': 'application/json', - }, payload); - }, - - /** - * Update File - * - * Update a file by its unique ID. Only users with write permissions have - * access to update this resource. - * - * @param {string} fileId - * @param {string[]} read - * @param {string[]} write - * @throws {Error} - * @return {Promise} - */ - updateFile: function(fileId, read, write) { - if(fileId === undefined) { - throw new Error('Missing required parameter: "fileId"'); - } - - if(read === undefined) { - throw new Error('Missing required parameter: "read"'); - } - - if(write === undefined) { - throw new Error('Missing required parameter: "write"'); - } - - let path = '/storage/files/{fileId}'.replace(new RegExp('{fileId}', 'g'), fileId); - - let payload = {}; - - if(typeof read !== 'undefined') { - payload['read'] = read; - } - - if(typeof write !== 'undefined') { - payload['write'] = write; - } - - return http - .put(path, { - 'content-type': 'application/json', - }, payload); - }, - - /** - * Delete File - * - * Delete a file by its unique ID. Only users with write permissions have - * access to delete this resource. - * - * @param {string} fileId - * @throws {Error} - * @return {Promise} - */ - deleteFile: function(fileId) { - if(fileId === undefined) { - throw new Error('Missing required parameter: "fileId"'); - } - - let path = '/storage/files/{fileId}'.replace(new RegExp('{fileId}', 'g'), fileId); - - let payload = {}; - - return http - .delete(path, { - 'content-type': 'application/json', - }, payload); - }, - - /** - * Get File for Download - * - * Get a file content by its unique ID. The endpoint response return with a - * 'Content-Disposition: attachment' header that tells the browser to start - * downloading the file to user downloads directory. - * - * @param {string} fileId - * @throws {Error} - * @return {string} - */ - getFileDownload: function(fileId) { - if(fileId === undefined) { - throw new Error('Missing required parameter: "fileId"'); - } - - let path = '/storage/files/{fileId}/download'.replace(new RegExp('{fileId}', 'g'), fileId); - - let payload = {}; - - payload['project'] = config.project; - - payload['key'] = config.key; - - - let query = []; - - for (let p in payload) { - if(Array.isArray(payload[p])) { - for (let index = 0; index < payload[p].length; index++) { - let param = payload[p][index]; - query.push(encodeURIComponent(p + '[]') + "=" + encodeURIComponent(param)); - } - } - else { - query.push(encodeURIComponent(p) + "=" + encodeURIComponent(payload[p])); - } - } - - query = query.join("&"); - - return config.endpoint + path + ((query) ? '?' + query : ''); - }, - - /** - * Get File Preview - * - * Get a file preview image. Currently, this method supports preview for image - * files (jpg, png, and gif), other supported formats, like pdf, docs, slides, - * and spreadsheets, will return the file icon image. You can also pass query - * string arguments for cutting and resizing your preview image. - * - * @param {string} fileId - * @param {number} width - * @param {number} height - * @param {number} quality - * @param {string} background - * @param {string} output - * @throws {Error} - * @return {string} - */ - getFilePreview: function(fileId, width = 0, height = 0, quality = 100, background = '', output = '') { - if(fileId === undefined) { - throw new Error('Missing required parameter: "fileId"'); - } - - let path = '/storage/files/{fileId}/preview'.replace(new RegExp('{fileId}', 'g'), fileId); - - let payload = {}; - - if(width) { - payload['width'] = width; - } - - if(height) { - payload['height'] = height; - } - - if(quality) { - payload['quality'] = quality; - } - - if(background) { - payload['background'] = background; - } - - if(output) { - payload['output'] = output; - } - - payload['project'] = config.project; - - payload['key'] = config.key; - - - let query = []; - - for (let p in payload) { - if(Array.isArray(payload[p])) { - for (let index = 0; index < payload[p].length; index++) { - let param = payload[p][index]; - query.push(encodeURIComponent(p + '[]') + "=" + encodeURIComponent(param)); - } - } - else { - query.push(encodeURIComponent(p) + "=" + encodeURIComponent(payload[p])); - } - } - - query = query.join("&"); - - return config.endpoint + path + ((query) ? '?' + query : ''); - }, - - /** - * Get File for View - * - * Get a file content by its unique ID. This endpoint is similar to the - * download method but returns with no 'Content-Disposition: attachment' - * header. - * - * @param {string} fileId - * @throws {Error} - * @return {string} - */ - getFileView: function(fileId) { - if(fileId === undefined) { - throw new Error('Missing required parameter: "fileId"'); - } - - let path = '/storage/files/{fileId}/view'.replace(new RegExp('{fileId}', 'g'), fileId); - - let payload = {}; - - payload['project'] = config.project; - - payload['key'] = config.key; - - - let query = []; - - for (let p in payload) { - if(Array.isArray(payload[p])) { - for (let index = 0; index < payload[p].length; index++) { - let param = payload[p][index]; - query.push(encodeURIComponent(p + '[]') + "=" + encodeURIComponent(param)); - } - } - else { - query.push(encodeURIComponent(p) + "=" + encodeURIComponent(payload[p])); - } - } - - query = query.join("&"); - - return config.endpoint + path + ((query) ? '?' + query : ''); - } - }; - - let teams = { - - /** - * List Teams - * - * Get a list of all the current user teams. You can use the query params to - * filter your results. On admin mode, this endpoint will return a list of all - * of the project's teams. [Learn more about different API - * modes](/docs/admin). - * - * @param {string} search - * @param {number} limit - * @param {number} offset - * @param {string} orderType - * @throws {Error} - * @return {Promise} - */ - list: function(search = '', limit = 25, offset = 0, orderType = 'ASC') { - let path = '/teams'; - - let payload = {}; - - if(search) { - payload['search'] = search; - } - - if(limit) { - payload['limit'] = limit; - } - - if(offset) { - payload['offset'] = offset; - } - - if(orderType) { - payload['orderType'] = orderType; - } - - return http - .get(path, { - 'content-type': 'application/json', - }, payload); - }, - - /** - * Create Team - * - * Create a new team. The user who creates the team will automatically be - * assigned as the owner of the team. The team owner can invite new members, - * who will be able add new owners and update or delete the team from your - * project. - * - * @param {string} name - * @param {string[]} roles - * @throws {Error} - * @return {Promise} - */ - create: function(name, roles = ["owner"]) { - if(name === undefined) { - throw new Error('Missing required parameter: "name"'); - } - - let path = '/teams'; - - let payload = {}; - - if(typeof name !== 'undefined') { - payload['name'] = name; - } - - if(typeof roles !== 'undefined') { - payload['roles'] = roles; - } - - return http - .post(path, { - 'content-type': 'application/json', - }, payload); - }, - - /** - * Get Team - * - * Get a team by its unique ID. All team members have read access for this - * resource. - * - * @param {string} teamId - * @throws {Error} - * @return {Promise} - */ - get: function(teamId) { - if(teamId === undefined) { - throw new Error('Missing required parameter: "teamId"'); - } - - let path = '/teams/{teamId}'.replace(new RegExp('{teamId}', 'g'), teamId); - - let payload = {}; - - return http - .get(path, { - 'content-type': 'application/json', - }, payload); - }, - - /** - * Update Team - * - * Update a team by its unique ID. Only team owners have write access for this - * resource. - * - * @param {string} teamId - * @param {string} name - * @throws {Error} - * @return {Promise} - */ - update: function(teamId, name) { - if(teamId === undefined) { - throw new Error('Missing required parameter: "teamId"'); - } - - if(name === undefined) { - throw new Error('Missing required parameter: "name"'); - } - - let path = '/teams/{teamId}'.replace(new RegExp('{teamId}', 'g'), teamId); - - let payload = {}; - - if(typeof name !== 'undefined') { - payload['name'] = name; - } - - return http - .put(path, { - 'content-type': 'application/json', - }, payload); - }, - - /** - * Delete Team - * - * Delete a team by its unique ID. Only team owners have write access for this - * resource. - * - * @param {string} teamId - * @throws {Error} - * @return {Promise} - */ - delete: function(teamId) { - if(teamId === undefined) { - throw new Error('Missing required parameter: "teamId"'); - } - - let path = '/teams/{teamId}'.replace(new RegExp('{teamId}', 'g'), teamId); - - let payload = {}; - - return http - .delete(path, { - 'content-type': 'application/json', - }, payload); - }, - - /** - * Get Team Memberships - * - * Get a team members by the team unique ID. All team members have read access - * for this list of resources. - * - * @param {string} teamId - * @param {string} search - * @param {number} limit - * @param {number} offset - * @param {string} orderType - * @throws {Error} - * @return {Promise} - */ - getMemberships: function(teamId, search = '', limit = 25, offset = 0, orderType = 'ASC') { - if(teamId === undefined) { - throw new Error('Missing required parameter: "teamId"'); - } - - let path = '/teams/{teamId}/memberships'.replace(new RegExp('{teamId}', 'g'), teamId); - - let payload = {}; - - if(search) { - payload['search'] = search; - } - - if(limit) { - payload['limit'] = limit; - } - - if(offset) { - payload['offset'] = offset; - } - - if(orderType) { - payload['orderType'] = orderType; - } - - return http - .get(path, { - 'content-type': 'application/json', - }, payload); - }, - - /** - * Create Team Membership - * - * Use this endpoint to invite a new member to join your team. An email with a - * link to join the team will be sent to the new member email address if the - * member doesn't exist in the project it will be created automatically. - * - * Use the 'URL' parameter to redirect the user from the invitation email back - * to your app. When the user is redirected, use the [Update Team Membership - * Status](/docs/client/teams#teamsUpdateMembershipStatus) endpoint to allow - * the user to accept the invitation to the team. - * - * Please note that in order to avoid a [Redirect - * Attacks](https://github.com/OWASP/CheatSheetSeries/blob/master/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.md) - * the only valid redirect URL's are the once from domains you have set when - * added your platforms in the console interface. - * - * @param {string} teamId - * @param {string} email - * @param {string[]} roles - * @param {string} url - * @param {string} name - * @throws {Error} - * @return {Promise} - */ - createMembership: function(teamId, email, roles, url, name = '') { - if(teamId === undefined) { - throw new Error('Missing required parameter: "teamId"'); - } - - if(email === undefined) { - throw new Error('Missing required parameter: "email"'); - } - - if(roles === undefined) { - throw new Error('Missing required parameter: "roles"'); - } - - if(url === undefined) { - throw new Error('Missing required parameter: "url"'); - } - - let path = '/teams/{teamId}/memberships'.replace(new RegExp('{teamId}', 'g'), teamId); - - let payload = {}; - - if(typeof email !== 'undefined') { - payload['email'] = email; - } - - if(typeof name !== 'undefined') { - payload['name'] = name; - } - - if(typeof roles !== 'undefined') { - payload['roles'] = roles; - } - - if(typeof url !== 'undefined') { - payload['url'] = url; - } - - return http - .post(path, { - 'content-type': 'application/json', - }, payload); - }, - - /** - * Delete Team Membership - * - * This endpoint allows a user to leave a team or for a team owner to delete - * the membership of any other team member. You can also use this endpoint to - * delete a user membership even if it is not accepted. - * - * @param {string} teamId - * @param {string} inviteId - * @throws {Error} - * @return {Promise} - */ - deleteMembership: function(teamId, inviteId) { - if(teamId === undefined) { - throw new Error('Missing required parameter: "teamId"'); - } - - if(inviteId === undefined) { - throw new Error('Missing required parameter: "inviteId"'); - } - - let path = '/teams/{teamId}/memberships/{inviteId}'.replace(new RegExp('{teamId}', 'g'), teamId).replace(new RegExp('{inviteId}', 'g'), inviteId); - - let payload = {}; - - return http - .delete(path, { - 'content-type': 'application/json', - }, payload); - }, - - /** - * Update Team Membership Status - * - * Use this endpoint to allow a user to accept an invitation to join a team - * after being redirected back to your app from the invitation email recieved - * by the user. - * - * @param {string} teamId - * @param {string} inviteId - * @param {string} userId - * @param {string} secret - * @throws {Error} - * @return {Promise} - */ - updateMembershipStatus: function(teamId, inviteId, userId, secret) { - if(teamId === undefined) { - throw new Error('Missing required parameter: "teamId"'); - } - - if(inviteId === undefined) { - throw new Error('Missing required parameter: "inviteId"'); - } - - if(userId === undefined) { - throw new Error('Missing required parameter: "userId"'); - } - - if(secret === undefined) { - throw new Error('Missing required parameter: "secret"'); - } - - let path = '/teams/{teamId}/memberships/{inviteId}/status'.replace(new RegExp('{teamId}', 'g'), teamId).replace(new RegExp('{inviteId}', 'g'), inviteId); - - let payload = {}; - - if(typeof userId !== 'undefined') { - payload['userId'] = userId; - } - - if(typeof secret !== 'undefined') { - payload['secret'] = secret; - } - - return http - .patch(path, { - 'content-type': 'application/json', - }, payload); - } - }; - - let users = { - - /** - * List Users - * - * Get a list of all the project's users. You can use the query params to - * filter your results. - * - * @param {string} search - * @param {number} limit - * @param {number} offset - * @param {string} orderType - * @throws {Error} - * @return {Promise} - */ - list: function(search = '', limit = 25, offset = 0, orderType = 'ASC') { - let path = '/users'; - - let payload = {}; - - if(search) { - payload['search'] = search; - } - - if(limit) { - payload['limit'] = limit; - } - - if(offset) { - payload['offset'] = offset; - } - - if(orderType) { - payload['orderType'] = orderType; - } - - return http - .get(path, { - 'content-type': 'application/json', - }, payload); - }, - - /** - * Create User - * - * Create a new user. - * - * @param {string} email - * @param {string} password - * @param {string} name - * @throws {Error} - * @return {Promise} - */ - create: function(email, password, name = '') { - if(email === undefined) { - throw new Error('Missing required parameter: "email"'); - } - - if(password === undefined) { - throw new Error('Missing required parameter: "password"'); - } - - let path = '/users'; - - let payload = {}; - - if(typeof email !== 'undefined') { - payload['email'] = email; - } - - if(typeof password !== 'undefined') { - payload['password'] = password; - } - - if(typeof name !== 'undefined') { - payload['name'] = name; - } - - return http - .post(path, { - 'content-type': 'application/json', - }, payload); - }, - - /** - * Get User - * - * Get a user by its unique ID. - * - * @param {string} userId - * @throws {Error} - * @return {Promise} - */ - get: function(userId) { - if(userId === undefined) { - throw new Error('Missing required parameter: "userId"'); - } - - let path = '/users/{userId}'.replace(new RegExp('{userId}', 'g'), userId); - - let payload = {}; - - return http - .get(path, { - 'content-type': 'application/json', - }, payload); - }, - - /** - * Delete User - * - * Delete a user by its unique ID. - * - * @param {string} userId - * @throws {Error} - * @return {Promise} - */ - deleteUser: function(userId) { - if(userId === undefined) { - throw new Error('Missing required parameter: "userId"'); - } - - let path = '/users/{userId}'.replace(new RegExp('{userId}', 'g'), userId); - - let payload = {}; - - return http - .delete(path, { - 'content-type': 'application/json', - }, payload); - }, - - /** - * Get User Logs - * - * Get a user activity logs list by its unique ID. - * - * @param {string} userId - * @throws {Error} - * @return {Promise} - */ - getLogs: function(userId) { - if(userId === undefined) { - throw new Error('Missing required parameter: "userId"'); - } - - let path = '/users/{userId}/logs'.replace(new RegExp('{userId}', 'g'), userId); - - let payload = {}; - - return http - .get(path, { - 'content-type': 'application/json', - }, payload); - }, - - /** - * Get User Preferences - * - * Get the user preferences by its unique ID. - * - * @param {string} userId - * @throws {Error} - * @return {Promise} - */ - getPrefs: function(userId) { - if(userId === undefined) { - throw new Error('Missing required parameter: "userId"'); - } - - let path = '/users/{userId}/prefs'.replace(new RegExp('{userId}', 'g'), userId); - - let payload = {}; - - return http - .get(path, { - 'content-type': 'application/json', - }, payload); - }, - - /** - * Update User Preferences - * - * Update the user preferences by its unique ID. You can pass only the - * specific settings you wish to update. - * - * @param {string} userId - * @param {object} prefs - * @throws {Error} - * @return {Promise} - */ - updatePrefs: function(userId, prefs) { - if(userId === undefined) { - throw new Error('Missing required parameter: "userId"'); - } - - if(prefs === undefined) { - throw new Error('Missing required parameter: "prefs"'); - } - - let path = '/users/{userId}/prefs'.replace(new RegExp('{userId}', 'g'), userId); - - let payload = {}; - - if(typeof prefs !== 'undefined') { - payload['prefs'] = prefs; - } - - return http - .patch(path, { - 'content-type': 'application/json', - }, payload); - }, - - /** - * Get User Sessions - * - * Get the user sessions list by its unique ID. - * - * @param {string} userId - * @throws {Error} - * @return {Promise} - */ - getSessions: function(userId) { - if(userId === undefined) { - throw new Error('Missing required parameter: "userId"'); - } - - let path = '/users/{userId}/sessions'.replace(new RegExp('{userId}', 'g'), userId); - - let payload = {}; - - return http - .get(path, { - 'content-type': 'application/json', - }, payload); - }, - - /** - * Delete User Sessions - * - * Delete all user's sessions by using the user's unique ID. - * - * @param {string} userId - * @throws {Error} - * @return {Promise} - */ - deleteSessions: function(userId) { - if(userId === undefined) { - throw new Error('Missing required parameter: "userId"'); - } - - let path = '/users/{userId}/sessions'.replace(new RegExp('{userId}', 'g'), userId); - - let payload = {}; - - return http - .delete(path, { - 'content-type': 'application/json', - }, payload); - }, - - /** - * Delete User Session - * - * Delete a user sessions by its unique ID. - * - * @param {string} userId - * @param {string} sessionId - * @throws {Error} - * @return {Promise} - */ - deleteSession: function(userId, sessionId) { - if(userId === undefined) { - throw new Error('Missing required parameter: "userId"'); - } - - if(sessionId === undefined) { - throw new Error('Missing required parameter: "sessionId"'); - } - - let path = '/users/{userId}/sessions/{sessionId}'.replace(new RegExp('{userId}', 'g'), userId).replace(new RegExp('{sessionId}', 'g'), sessionId); - - let payload = {}; - - return http - .delete(path, { - 'content-type': 'application/json', - }, payload); - }, - - /** - * Update User Status - * - * Update the user status by its unique ID. - * - * @param {string} userId - * @param {string} status - * @throws {Error} - * @return {Promise} - */ - updateStatus: function(userId, status) { - if(userId === undefined) { - throw new Error('Missing required parameter: "userId"'); - } - - if(status === undefined) { - throw new Error('Missing required parameter: "status"'); - } - - let path = '/users/{userId}/status'.replace(new RegExp('{userId}', 'g'), userId); - - let payload = {}; - - if(typeof status !== 'undefined') { - payload['status'] = status; - } - - return http - .patch(path, { - 'content-type': 'application/json', - }, payload); - } - }; - - return { - setEndpoint: setEndpoint, - setProject: setProject, - setKey: setKey, - setLocale: setLocale, - setMode: setMode, - account: account, - avatars: avatars, - database: database, - functions: functions, - health: health, - locale: locale, - projects: projects, - storage: storage, - teams: teams, - users: users - }; - }; - - if(typeof module !== "undefined") { - module.exports = window.Appwrite; + return output; + } } -})((typeof window !== "undefined") ? window : {}); \ No newline at end of file + exports.Appwrite = Appwrite; + + Object.defineProperty(exports, '__esModule', { value: true }); + +}(this.window = this.window || {}, null, window)); diff --git a/public/scripts/filters.js b/public/scripts/filters.js index 917a4a82ee..6036136a11 100644 --- a/public/scripts/filters.js +++ b/public/scripts/filters.js @@ -255,6 +255,9 @@ window.ls.filter return ''; }) + .add("accessProject", function($value, router) { + return $value.hasOwnProperty(router.params.project) ? $value[router.params.project] : 0; + }) ; function abbreviate(number, maxPlaces, forcePlaces, forceLetter) { diff --git a/public/scripts/init.js b/public/scripts/init.js index 3041bbd681..c5c939b0ca 100644 --- a/public/scripts/init.js +++ b/public/scripts/init.js @@ -51,4 +51,11 @@ document.addEventListener("account.create", function () { }, function (error) { window.location = '/auth/signup?failure=1'; }); -}); \ No newline at end of file +}); +window.addEventListener("load", () => { + const realtime = window.ls.container.get('realtime'); + window.ls.container.get('console').subscribe('project', event => { + realtime.set(event.payload); + }); +}); + diff --git a/public/scripts/services/console.js b/public/scripts/services/console.js index 7a08cc4e25..b1b6c7821f 100644 --- a/public/scripts/services/console.js +++ b/public/scripts/services/console.js @@ -5,7 +5,7 @@ var sdk = new window.Appwrite(); sdk - .setEndpoint(APP_ENV.API) + .setEndpoint(window.location.protocol + '//' + window.location.host + APP_ENV.API) .setProject('console') .setLocale(APP_ENV.LOCALE) ; diff --git a/public/scripts/services/realtime.js b/public/scripts/services/realtime.js new file mode 100644 index 0000000000..eb961c64f5 --- /dev/null +++ b/public/scripts/services/realtime.js @@ -0,0 +1,14 @@ +(function (window) { + "use strict"; + + window.ls.container.set('realtime', () => { + return { + current: null, + set: function (currentConnections) { + var scope = this; + scope.current = currentConnections; + return scope.current; + } + }; + }, true, true); +})(window); \ No newline at end of file diff --git a/public/scripts/services/sdk.js b/public/scripts/services/sdk.js index fa3a4fb6d3..e72d5b753e 100644 --- a/public/scripts/services/sdk.js +++ b/public/scripts/services/sdk.js @@ -5,7 +5,7 @@ var sdk = new window.Appwrite(); sdk - .setEndpoint(APP_ENV.API) + .setEndpoint(window.location.protocol + '//' + window.location.host + APP_ENV.API) .setProject(router.params.project || '') .setLocale(APP_ENV.LOCALE) .setMode('admin') diff --git a/public/scripts/views/service.js b/public/scripts/views/service.js index aaec307f5b..cd21af41b6 100644 --- a/public/scripts/views/service.js +++ b/public/scripts/views/service.js @@ -257,7 +257,7 @@ args.map(function(value) { let result = getValue(value, prefix, data); - return result; + return result ?? undefined; }) ); }; From ab6be33e6f550d203a82bf77cab70565eec78499 Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Tue, 8 Jun 2021 17:28:00 +0200 Subject: [PATCH 122/267] fix(gitignore): add sdks --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 224a970df4..5782415ae4 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ /vendor/ /node_modules/ /tests/resources/storage/ +/app/sdks/* /.idea/ .DS_Store .php_cs.cache From 03984860d59c585b45f0dd42eeb3db973837c3f2 Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Tue, 8 Jun 2021 19:02:28 +0200 Subject: [PATCH 123/267] feat(usage): realtime data to api endpoint --- app/controllers/api/projects.php | 37 ++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/app/controllers/api/projects.php b/app/controllers/api/projects.php index 0b9eb64ff0..9d2a28f04f 100644 --- a/app/controllers/api/projects.php +++ b/app/controllers/api/projects.php @@ -211,6 +211,8 @@ App::get('/v1/projects/:projectId/usage') $requests = []; $network = []; $functions = []; + $realtimeConnections = []; + $realtimeMessages = []; if ($client) { $start = $period[$range]['start']->format(DateTime::RFC3339); @@ -249,11 +251,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 = []; } @@ -318,6 +343,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, From 21ac8c11041859345650b3e8ada31b483ef81ee9 Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Thu, 10 Jun 2021 13:12:45 +0200 Subject: [PATCH 124/267] fix(usage): undefined function id --- app/workers/usage.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/workers/usage.php b/app/workers/usage.php index 0e3e6c2ebf..e858669d64 100644 --- a/app/workers/usage.php +++ b/app/workers/usage.php @@ -38,7 +38,7 @@ class UsageV1 extends Worker $httpMethod = $this->args['httpMethod'] ?? ''; $httpRequest = $this->args['httpRequest'] ?? 0; - $functionId = $this->args['functionId']; + $functionId = $this->args['functionId'] ?? ''; $functionExecution = $this->args['functionExecution'] ?? 0; $functionExecutionTime = $this->args['functionExecutionTime'] ?? 0; $functionStatus = $this->args['functionStatus'] ?? ''; @@ -65,7 +65,7 @@ class UsageV1 extends Worker } if($realtimeMessages >= 1) { - $statsd->count('realtime.message'.$tags, $realtimeMessages); + $statsd->count('realtime.messages'.$tags, $realtimeMessages); } $statsd->count('network.inbound'.$tags, $networkRequestSize); From 4d70b16801ec3ba9781b447460085d8c0a76dce1 Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Thu, 10 Jun 2021 14:42:40 +0200 Subject: [PATCH 125/267] feat(console): add dirty realtime stats --- Dockerfile | 4 +++- app/views/console/database/index.phtml | 1 + app/views/console/home/index.phtml | 26 +++++++++++++++++++++----- public/dist/scripts/app-all.js | 4 ++-- public/dist/scripts/app.js | 4 ++-- public/scripts/filters.js | 2 +- test/imagick | 1 + 7 files changed, 31 insertions(+), 11 deletions(-) create mode 160000 test/imagick diff --git a/Dockerfile b/Dockerfile index 2587ca737d..5c77c202df 100755 --- a/Dockerfile +++ b/Dockerfile @@ -54,8 +54,10 @@ RUN \ make && make install && \ cd .. && \ ## Imagick Extension - git clone --depth 1 --branch $PHP_IMAGICK_VERSION https://github.com/Imagick/imagick && \ + ## Last working commit https://github.com/Imagick/imagick/commit/35741750aa1cda2b7ac354bfa6128fa037e9cf32 + git clone --branch $PHP_IMAGICK_VERSION https://github.com/Imagick/imagick && \ cd imagick && \ + git checkout 35741750aa1cda2b7ac354bfa6128fa037e9cf32 && \ phpize && \ ./configure && \ make && make install && \ diff --git a/app/views/console/database/index.phtml b/app/views/console/database/index.phtml index 2760a1ac10..2087d1097a 100644 --- a/app/views/console/database/index.phtml +++ b/app/views/console/database/index.phtml @@ -99,6 +99,7 @@ +
diff --git a/app/views/console/home/index.phtml b/app/views/console/home/index.phtml index 6fffb8f570..cfc1667d33 100644 --- a/app/views/console/home/index.phtml +++ b/app/views/console/home/index.phtml @@ -102,12 +102,28 @@ $usageStatsEnabled = $this->getParam('usageStatsEnabled',true);
Bandwidth
- -
- -
- Realtime Connections +
+
+
+
+
+
+
+
+ +
+
N/A
+
Connections
+
N/A
+
Messages
+
+
+
+
+ 0 +
+
Live
diff --git a/public/dist/scripts/app-all.js b/public/dist/scripts/app-all.js index 43966130e4..ac223746bb 100644 --- a/public/dist/scripts/app-all.js +++ b/public/dist/scripts/app-all.js @@ -2251,7 +2251,7 @@ return slf.renderToken(tokens,idx,opts);} md.renderer.rules.strong_open=renderEm;md.renderer.rules.strong_close=renderEm;return md;},true);})(window);(function(window){"use strict";window.ls.container.set('rtl',function(){var rtlStock="^ا^ب^ت^ث^ج^ح^خ^د^ذ^ر^ز^س^ش^ص^ض^ط^ظ^ع^غ^ف^ق^ك^ل^م^ن^ه^و^ي^א^ב^ג^ד^ה^ו^ז^ח^ט^י^כ^ך^ל^מ^ם^נ^ן^ס^ע^פ^ף^צ^ץ^ק^ר^ש^ת^";var special=["\n"," "," ","״",'"',"_","'","!","@","#","$","^","&","%","*","(",")","+","=","-","[","]","\\","/","{","}","|",":","<",">","?",",",".","0","1","2","3","4","5","6","7","8","9"];var isRTL=function(value){for(var i=0;i{return{current:false,set:function(currentConnections){var scope=this;scope.current=currentConnections;return scope.current;}};},true,true);})(window);window.ls.router.add("/auth/signin",{template:"/auth/signin?version="+APP_ENV.CACHEBUSTER,scope:"home"}).add("/auth/signup",{template:"/auth/signup?version="+APP_ENV.CACHEBUSTER,scope:"home"}).add("/auth/recovery",{template:"/auth/recovery?version="+APP_ENV.CACHEBUSTER,scope:"home"}).add("/auth/recovery/reset",{template:"/auth/recovery/reset?version="+APP_ENV.CACHEBUSTER,scope:"home"}).add("/auth/confirm",{template:"/auth/confirm?version="+APP_ENV.CACHEBUSTER,scope:"home"}).add("/auth/join",{template:"/auth/join?version="+APP_ENV.CACHEBUSTER,scope:"home"}).add("/auth/oauth2/success",{template:"/auth/oauth2/success?version="+APP_ENV.CACHEBUSTER,scope:"home"}).add("/auth/oauth2/failure",{template:"/auth/oauth2/failure?version="+APP_ENV.CACHEBUSTER,scope:"home"}).add("/console",{template:"/console?version="+APP_ENV.CACHEBUSTER,scope:"console"}).add("/console/account",{template:"/console/account?version="+APP_ENV.CACHEBUSTER,scope:"console"}).add("/console/account/:tab",{template:"/console/account?version="+APP_ENV.CACHEBUSTER,scope:"console"}).add("/console/home",{template:"/console/home?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/home/:tab",{template:"/console/home?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/platforms/:platform",{template:function(window){return window.location.pathname+"?version="+APP_ENV.CACHEBUSTER;},scope:"console",project:true}).add("/console/notifications",{template:"/console/notifications?version="+APP_ENV.CACHEBUSTER,scope:"console"}).add("/console/settings",{template:"/console/settings?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/settings/:tab",{template:"/console/settings?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/webhooks",{template:"/console/webhooks?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/webhooks/:tab",{template:"/console/webhooks?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/keys",{template:"/console/keys?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/keys/:tab",{template:"/console/keys?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/tasks",{template:"/console/tasks?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/tasks/:tab",{template:"/console/tasks?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/database",{template:"/console/database?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/database/collection",{template:function(window){return window.location.pathname+window.location.search+'&version='+APP_ENV.CACHEBUSTER;},scope:"console",project:true}).add("/console/database/collection/:tab",{template:function(window){return window.location.pathname+window.location.search+'&version='+APP_ENV.CACHEBUSTER;},scope:"console",project:true}).add("/console/database/document",{template:function(window){return window.location.pathname+window.location.search+'&version='+APP_ENV.CACHEBUSTER;},scope:"console",project:true}).add("/console/database/document/:tab",{template:function(window){return window.location.pathname+window.location.search+'&version='+APP_ENV.CACHEBUSTER;},scope:"console",project:true}).add("/console/storage",{template:"/console/storage?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/storage/:tab",{template:"/console/storage?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/users",{template:"/console/users?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/users/user",{template:"/console/users/user?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/users/user/:tab",{template:"/console/users/user?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/users/teams/team",{template:"/console/users/teams/team?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/users/teams/team/:tab",{template:"/console/users/teams/team?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/users/:tab",{template:"/console/users?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/functions",{template:"/console/functions?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/functions/function",{template:"/console/functions/function?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/functions/function/:tab",{template:"/console/functions/function?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/functions/:tab",{template:"/console/functions?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true});window.ls.filter.add("avatar",function($value,element){if(!$value){return"";} +return false;};return{isRTL:isRTL,};},true);})(window);(function(window){"use strict";window.ls.container.set('sdk',function(window,router){var sdk=new window.Appwrite();sdk.setEndpoint(window.location.protocol+'//'+window.location.host+APP_ENV.API).setProject(router.params.project||'').setLocale(APP_ENV.LOCALE).setMode('admin');return sdk;},false);})(window);(function(window){"use strict";window.ls.container.set('search',function(window){return{params:{},path:'',pointer:'',selected:'',};},true,true);})(window);(function(window){"use strict";window.ls.container.set('timezone',function(){return{convert:function(unixTime){var timezoneMinutes=new Date().getTimezoneOffset();timezoneMinutes=(timezoneMinutes===0)?0:-timezoneMinutes;return parseInt(unixTime)+(timezoneMinutes*60);}};},true);})(window);(function(window){"use strict";window.ls.container.set('realtime',()=>{return{current:null,set:function(currentConnections){var scope=this;scope.current=currentConnections;return scope.current;}};},true,true);})(window);window.ls.router.add("/auth/signin",{template:"/auth/signin?version="+APP_ENV.CACHEBUSTER,scope:"home"}).add("/auth/signup",{template:"/auth/signup?version="+APP_ENV.CACHEBUSTER,scope:"home"}).add("/auth/recovery",{template:"/auth/recovery?version="+APP_ENV.CACHEBUSTER,scope:"home"}).add("/auth/recovery/reset",{template:"/auth/recovery/reset?version="+APP_ENV.CACHEBUSTER,scope:"home"}).add("/auth/confirm",{template:"/auth/confirm?version="+APP_ENV.CACHEBUSTER,scope:"home"}).add("/auth/join",{template:"/auth/join?version="+APP_ENV.CACHEBUSTER,scope:"home"}).add("/auth/oauth2/success",{template:"/auth/oauth2/success?version="+APP_ENV.CACHEBUSTER,scope:"home"}).add("/auth/oauth2/failure",{template:"/auth/oauth2/failure?version="+APP_ENV.CACHEBUSTER,scope:"home"}).add("/console",{template:"/console?version="+APP_ENV.CACHEBUSTER,scope:"console"}).add("/console/account",{template:"/console/account?version="+APP_ENV.CACHEBUSTER,scope:"console"}).add("/console/account/:tab",{template:"/console/account?version="+APP_ENV.CACHEBUSTER,scope:"console"}).add("/console/home",{template:"/console/home?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/home/:tab",{template:"/console/home?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/platforms/:platform",{template:function(window){return window.location.pathname+"?version="+APP_ENV.CACHEBUSTER;},scope:"console",project:true}).add("/console/notifications",{template:"/console/notifications?version="+APP_ENV.CACHEBUSTER,scope:"console"}).add("/console/settings",{template:"/console/settings?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/settings/:tab",{template:"/console/settings?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/webhooks",{template:"/console/webhooks?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/webhooks/:tab",{template:"/console/webhooks?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/keys",{template:"/console/keys?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/keys/:tab",{template:"/console/keys?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/tasks",{template:"/console/tasks?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/tasks/:tab",{template:"/console/tasks?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/database",{template:"/console/database?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/database/collection",{template:function(window){return window.location.pathname+window.location.search+'&version='+APP_ENV.CACHEBUSTER;},scope:"console",project:true}).add("/console/database/collection/:tab",{template:function(window){return window.location.pathname+window.location.search+'&version='+APP_ENV.CACHEBUSTER;},scope:"console",project:true}).add("/console/database/document",{template:function(window){return window.location.pathname+window.location.search+'&version='+APP_ENV.CACHEBUSTER;},scope:"console",project:true}).add("/console/database/document/:tab",{template:function(window){return window.location.pathname+window.location.search+'&version='+APP_ENV.CACHEBUSTER;},scope:"console",project:true}).add("/console/storage",{template:"/console/storage?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/storage/:tab",{template:"/console/storage?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/users",{template:"/console/users?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/users/user",{template:"/console/users/user?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/users/user/:tab",{template:"/console/users/user?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/users/teams/team",{template:"/console/users/teams/team?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/users/teams/team/:tab",{template:"/console/users/teams/team?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/users/:tab",{template:"/console/users?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/functions",{template:"/console/functions?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/functions/function",{template:"/console/functions/function?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/functions/function/:tab",{template:"/console/functions/function?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/functions/:tab",{template:"/console/functions?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true});window.ls.filter.add("avatar",function($value,element){if(!$value){return"";} let size=element.dataset["size"]||80;let name=$value.name||$value||"";name=(typeof name!=='string')?'--':name;return def="/v1/avatars/initials?project=console"+"&name="+ encodeURIComponent(name)+"&width="+ size+"&height="+ @@ -2279,7 +2279,7 @@ return $value;}).add("platformsLimit",function($value){return $value;}).add("lim return $value.join(", ").replace(/,\s([^,]+)$/,' and $1');}).add("envName",function($value,env){if(env&&env.RUNTIMES&&env.RUNTIMES[$value]){return env.RUNTIMES[$value].name;} return'';}).add("envLogo",function($value,env){if(env&&env.RUNTIMES&&env.RUNTIMES[$value]){return env.RUNTIMES[$value].logo;} return'';}).add("envVersion",function($value,env){if(env&&env.RUNTIMES&&env.RUNTIMES[$value]){return env.RUNTIMES[$value].version;} -return'';}).add("accessProject",function($value,router){return $value.hasOwnProperty(router.params.project)?$value[router.params.project]:0;});function abbreviate(number,maxPlaces,forcePlaces,forceLetter){number=Number(number);forceLetter=forceLetter||false;if(forceLetter!==false){return annotate(number,maxPlaces,forcePlaces,forceLetter);} +return'';}).add("accessProject",function($value,router){return($value&&$value.hasOwnProperty(router.params.project))?$value[router.params.project]:0;});function abbreviate(number,maxPlaces,forcePlaces,forceLetter){number=Number(number);forceLetter=forceLetter||false;if(forceLetter!==false){return annotate(number,maxPlaces,forcePlaces,forceLetter);} let abbr;if(number>=1e12){abbr="T";}else if(number>=1e9){abbr="B";}else if(number>=1e6){abbr="M";}else if(number>=1e3){abbr="K";}else{abbr="";} return annotate(number,maxPlaces,forcePlaces,abbr);} function annotate(number,maxPlaces,forcePlaces,abbr){let rounded=0;switch(abbr){case"T":rounded=number/1e12;break;case"B":rounded=number/1e9;break;case"M":rounded=number/1e6;break;case"K":rounded=number/1e3;break;case"":rounded=number;break;} diff --git a/public/dist/scripts/app.js b/public/dist/scripts/app.js index d1c455bd1c..234ff229db 100644 --- a/public/dist/scripts/app.js +++ b/public/dist/scripts/app.js @@ -281,7 +281,7 @@ return slf.renderToken(tokens,idx,opts);} md.renderer.rules.strong_open=renderEm;md.renderer.rules.strong_close=renderEm;return md;},true);})(window);(function(window){"use strict";window.ls.container.set('rtl',function(){var rtlStock="^ا^ب^ت^ث^ج^ح^خ^د^ذ^ر^ز^س^ش^ص^ض^ط^ظ^ع^غ^ف^ق^ك^ل^م^ن^ه^و^ي^א^ב^ג^ד^ה^ו^ז^ח^ט^י^כ^ך^ל^מ^ם^נ^ן^ס^ע^פ^ף^צ^ץ^ק^ר^ש^ת^";var special=["\n"," "," ","״",'"',"_","'","!","@","#","$","^","&","%","*","(",")","+","=","-","[","]","\\","/","{","}","|",":","<",">","?",",",".","0","1","2","3","4","5","6","7","8","9"];var isRTL=function(value){for(var i=0;i{return{current:false,set:function(currentConnections){var scope=this;scope.current=currentConnections;return scope.current;}};},true,true);})(window);window.ls.router.add("/auth/signin",{template:"/auth/signin?version="+APP_ENV.CACHEBUSTER,scope:"home"}).add("/auth/signup",{template:"/auth/signup?version="+APP_ENV.CACHEBUSTER,scope:"home"}).add("/auth/recovery",{template:"/auth/recovery?version="+APP_ENV.CACHEBUSTER,scope:"home"}).add("/auth/recovery/reset",{template:"/auth/recovery/reset?version="+APP_ENV.CACHEBUSTER,scope:"home"}).add("/auth/confirm",{template:"/auth/confirm?version="+APP_ENV.CACHEBUSTER,scope:"home"}).add("/auth/join",{template:"/auth/join?version="+APP_ENV.CACHEBUSTER,scope:"home"}).add("/auth/oauth2/success",{template:"/auth/oauth2/success?version="+APP_ENV.CACHEBUSTER,scope:"home"}).add("/auth/oauth2/failure",{template:"/auth/oauth2/failure?version="+APP_ENV.CACHEBUSTER,scope:"home"}).add("/console",{template:"/console?version="+APP_ENV.CACHEBUSTER,scope:"console"}).add("/console/account",{template:"/console/account?version="+APP_ENV.CACHEBUSTER,scope:"console"}).add("/console/account/:tab",{template:"/console/account?version="+APP_ENV.CACHEBUSTER,scope:"console"}).add("/console/home",{template:"/console/home?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/home/:tab",{template:"/console/home?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/platforms/:platform",{template:function(window){return window.location.pathname+"?version="+APP_ENV.CACHEBUSTER;},scope:"console",project:true}).add("/console/notifications",{template:"/console/notifications?version="+APP_ENV.CACHEBUSTER,scope:"console"}).add("/console/settings",{template:"/console/settings?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/settings/:tab",{template:"/console/settings?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/webhooks",{template:"/console/webhooks?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/webhooks/:tab",{template:"/console/webhooks?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/keys",{template:"/console/keys?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/keys/:tab",{template:"/console/keys?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/tasks",{template:"/console/tasks?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/tasks/:tab",{template:"/console/tasks?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/database",{template:"/console/database?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/database/collection",{template:function(window){return window.location.pathname+window.location.search+'&version='+APP_ENV.CACHEBUSTER;},scope:"console",project:true}).add("/console/database/collection/:tab",{template:function(window){return window.location.pathname+window.location.search+'&version='+APP_ENV.CACHEBUSTER;},scope:"console",project:true}).add("/console/database/document",{template:function(window){return window.location.pathname+window.location.search+'&version='+APP_ENV.CACHEBUSTER;},scope:"console",project:true}).add("/console/database/document/:tab",{template:function(window){return window.location.pathname+window.location.search+'&version='+APP_ENV.CACHEBUSTER;},scope:"console",project:true}).add("/console/storage",{template:"/console/storage?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/storage/:tab",{template:"/console/storage?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/users",{template:"/console/users?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/users/user",{template:"/console/users/user?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/users/user/:tab",{template:"/console/users/user?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/users/teams/team",{template:"/console/users/teams/team?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/users/teams/team/:tab",{template:"/console/users/teams/team?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/users/:tab",{template:"/console/users?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/functions",{template:"/console/functions?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/functions/function",{template:"/console/functions/function?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/functions/function/:tab",{template:"/console/functions/function?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/functions/:tab",{template:"/console/functions?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true});window.ls.filter.add("avatar",function($value,element){if(!$value){return"";} +return false;};return{isRTL:isRTL,};},true);})(window);(function(window){"use strict";window.ls.container.set('sdk',function(window,router){var sdk=new window.Appwrite();sdk.setEndpoint(window.location.protocol+'//'+window.location.host+APP_ENV.API).setProject(router.params.project||'').setLocale(APP_ENV.LOCALE).setMode('admin');return sdk;},false);})(window);(function(window){"use strict";window.ls.container.set('search',function(window){return{params:{},path:'',pointer:'',selected:'',};},true,true);})(window);(function(window){"use strict";window.ls.container.set('timezone',function(){return{convert:function(unixTime){var timezoneMinutes=new Date().getTimezoneOffset();timezoneMinutes=(timezoneMinutes===0)?0:-timezoneMinutes;return parseInt(unixTime)+(timezoneMinutes*60);}};},true);})(window);(function(window){"use strict";window.ls.container.set('realtime',()=>{return{current:null,set:function(currentConnections){var scope=this;scope.current=currentConnections;return scope.current;}};},true,true);})(window);window.ls.router.add("/auth/signin",{template:"/auth/signin?version="+APP_ENV.CACHEBUSTER,scope:"home"}).add("/auth/signup",{template:"/auth/signup?version="+APP_ENV.CACHEBUSTER,scope:"home"}).add("/auth/recovery",{template:"/auth/recovery?version="+APP_ENV.CACHEBUSTER,scope:"home"}).add("/auth/recovery/reset",{template:"/auth/recovery/reset?version="+APP_ENV.CACHEBUSTER,scope:"home"}).add("/auth/confirm",{template:"/auth/confirm?version="+APP_ENV.CACHEBUSTER,scope:"home"}).add("/auth/join",{template:"/auth/join?version="+APP_ENV.CACHEBUSTER,scope:"home"}).add("/auth/oauth2/success",{template:"/auth/oauth2/success?version="+APP_ENV.CACHEBUSTER,scope:"home"}).add("/auth/oauth2/failure",{template:"/auth/oauth2/failure?version="+APP_ENV.CACHEBUSTER,scope:"home"}).add("/console",{template:"/console?version="+APP_ENV.CACHEBUSTER,scope:"console"}).add("/console/account",{template:"/console/account?version="+APP_ENV.CACHEBUSTER,scope:"console"}).add("/console/account/:tab",{template:"/console/account?version="+APP_ENV.CACHEBUSTER,scope:"console"}).add("/console/home",{template:"/console/home?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/home/:tab",{template:"/console/home?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/platforms/:platform",{template:function(window){return window.location.pathname+"?version="+APP_ENV.CACHEBUSTER;},scope:"console",project:true}).add("/console/notifications",{template:"/console/notifications?version="+APP_ENV.CACHEBUSTER,scope:"console"}).add("/console/settings",{template:"/console/settings?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/settings/:tab",{template:"/console/settings?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/webhooks",{template:"/console/webhooks?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/webhooks/:tab",{template:"/console/webhooks?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/keys",{template:"/console/keys?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/keys/:tab",{template:"/console/keys?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/tasks",{template:"/console/tasks?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/tasks/:tab",{template:"/console/tasks?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/database",{template:"/console/database?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/database/collection",{template:function(window){return window.location.pathname+window.location.search+'&version='+APP_ENV.CACHEBUSTER;},scope:"console",project:true}).add("/console/database/collection/:tab",{template:function(window){return window.location.pathname+window.location.search+'&version='+APP_ENV.CACHEBUSTER;},scope:"console",project:true}).add("/console/database/document",{template:function(window){return window.location.pathname+window.location.search+'&version='+APP_ENV.CACHEBUSTER;},scope:"console",project:true}).add("/console/database/document/:tab",{template:function(window){return window.location.pathname+window.location.search+'&version='+APP_ENV.CACHEBUSTER;},scope:"console",project:true}).add("/console/storage",{template:"/console/storage?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/storage/:tab",{template:"/console/storage?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/users",{template:"/console/users?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/users/user",{template:"/console/users/user?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/users/user/:tab",{template:"/console/users/user?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/users/teams/team",{template:"/console/users/teams/team?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/users/teams/team/:tab",{template:"/console/users/teams/team?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/users/:tab",{template:"/console/users?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/functions",{template:"/console/functions?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/functions/function",{template:"/console/functions/function?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/functions/function/:tab",{template:"/console/functions/function?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/functions/:tab",{template:"/console/functions?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true});window.ls.filter.add("avatar",function($value,element){if(!$value){return"";} let size=element.dataset["size"]||80;let name=$value.name||$value||"";name=(typeof name!=='string')?'--':name;return def="/v1/avatars/initials?project=console"+"&name="+ encodeURIComponent(name)+"&width="+ size+"&height="+ @@ -309,7 +309,7 @@ return $value;}).add("platformsLimit",function($value){return $value;}).add("lim return $value.join(", ").replace(/,\s([^,]+)$/,' and $1');}).add("envName",function($value,env){if(env&&env.RUNTIMES&&env.RUNTIMES[$value]){return env.RUNTIMES[$value].name;} return'';}).add("envLogo",function($value,env){if(env&&env.RUNTIMES&&env.RUNTIMES[$value]){return env.RUNTIMES[$value].logo;} return'';}).add("envVersion",function($value,env){if(env&&env.RUNTIMES&&env.RUNTIMES[$value]){return env.RUNTIMES[$value].version;} -return'';}).add("accessProject",function($value,router){return $value.hasOwnProperty(router.params.project)?$value[router.params.project]:0;});function abbreviate(number,maxPlaces,forcePlaces,forceLetter){number=Number(number);forceLetter=forceLetter||false;if(forceLetter!==false){return annotate(number,maxPlaces,forcePlaces,forceLetter);} +return'';}).add("accessProject",function($value,router){return($value&&$value.hasOwnProperty(router.params.project))?$value[router.params.project]:0;});function abbreviate(number,maxPlaces,forcePlaces,forceLetter){number=Number(number);forceLetter=forceLetter||false;if(forceLetter!==false){return annotate(number,maxPlaces,forcePlaces,forceLetter);} let abbr;if(number>=1e12){abbr="T";}else if(number>=1e9){abbr="B";}else if(number>=1e6){abbr="M";}else if(number>=1e3){abbr="K";}else{abbr="";} return annotate(number,maxPlaces,forcePlaces,abbr);} function annotate(number,maxPlaces,forcePlaces,abbr){let rounded=0;switch(abbr){case"T":rounded=number/1e12;break;case"B":rounded=number/1e9;break;case"M":rounded=number/1e6;break;case"K":rounded=number/1e3;break;case"":rounded=number;break;} diff --git a/public/scripts/filters.js b/public/scripts/filters.js index 6036136a11..712a18a75b 100644 --- a/public/scripts/filters.js +++ b/public/scripts/filters.js @@ -256,7 +256,7 @@ window.ls.filter return ''; }) .add("accessProject", function($value, router) { - return $value.hasOwnProperty(router.params.project) ? $value[router.params.project] : 0; + return ($value && $value.hasOwnProperty(router.params.project)) ? $value[router.params.project] : 0; }) ; diff --git a/test/imagick b/test/imagick new file mode 160000 index 0000000000..3de6f0cf65 --- /dev/null +++ b/test/imagick @@ -0,0 +1 @@ +Subproject commit 3de6f0cf65767219f762337e46081e99ab8a334f From 2810af9311f552f54a902a6d86b27ed42a4ecddc Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Thu, 10 Jun 2021 16:17:47 +0200 Subject: [PATCH 126/267] fix(console): rename live connections --- app/views/console/home/index.phtml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/console/home/index.phtml b/app/views/console/home/index.phtml index cfc1667d33..cb6a0d846f 100644 --- a/app/views/console/home/index.phtml +++ b/app/views/console/home/index.phtml @@ -123,7 +123,7 @@ $usageStatsEnabled = $this->getParam('usageStatsEnabled',true);
0
-
Live
+
Live Connections
From c08b0a5d2bac2bf83caecc3d0f12f143271613ef Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Fri, 11 Jun 2021 15:30:33 +0200 Subject: [PATCH 127/267] feat(realtime): collection for concurrent connections --- app/config/collections.php | 33 ++++++++++++++++++++++++++++++ src/Appwrite/Database/Database.php | 3 +++ 2 files changed, 36 insertions(+) diff --git a/app/config/collections.php b/app/config/collections.php index 6789237c9c..d1d6230798 100644 --- a/app/config/collections.php +++ b/app/config/collections.php @@ -1698,6 +1698,39 @@ $collections = [ ], ], ], + Database::SYSTEM_COLLECTION_REALTIME_CONNECTIONS => [ + '$collection' => Database::SYSTEM_COLLECTION_COLLECTIONS, + '$id' => Database::SYSTEM_COLLECTION_REALTIME_CONNECTIONS, + '$permissions' => ['read' => ['*']], + 'name' => 'Realtime Connections', + 'structure' => true, + 'rules' => [ + [ + '$collection' => Database::SYSTEM_COLLECTION_REALTIME_CONNECTIONS, + 'label' => 'Container', + 'key' => 'container', + 'type' => Database::SYSTEM_VAR_TYPE_TEXT, + 'required' => true, + 'array' => false, + ], + [ + '$collection' => Database::SYSTEM_COLLECTION_REALTIME_CONNECTIONS, + 'label' => 'Timestamp', + 'key' => 'timestamp', + 'type' => Database::SYSTEM_VAR_TYPE_NUMERIC, + 'required' => true, + 'array' => false, + ], + [ + '$collection' => Database::SYSTEM_COLLECTION_REALTIME_CONNECTIONS, + 'label' => 'Data', + 'key' => 'data', + 'type' => Database::SYSTEM_VAR_TYPE_TEXT, + 'required' => true, + 'array' => false, + ], + ], + ], Database::SYSTEM_COLLECTION_RESERVED => [ '$collection' => Database::SYSTEM_COLLECTION_COLLECTIONS, '$id' => Database::SYSTEM_COLLECTION_RESERVED, diff --git a/src/Appwrite/Database/Database.php b/src/Appwrite/Database/Database.php index d0defdec03..f0fbfc2efd 100644 --- a/src/Appwrite/Database/Database.php +++ b/src/Appwrite/Database/Database.php @@ -41,6 +41,9 @@ class Database const SYSTEM_COLLECTION_FUNCTIONS = 'functions'; const SYSTEM_COLLECTION_TAGS = 'tags'; const SYSTEM_COLLECTION_EXECUTIONS = 'executions'; + + // Realtime + const SYSTEM_COLLECTION_REALTIME_CONNECTIONS = 'realtimeConnections'; // Var Types const SYSTEM_VAR_TYPE_TEXT = 'text'; From 220427916d26b1407d995d65dd67fd0bc6d79ead Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Mon, 14 Jun 2021 12:48:31 +0200 Subject: [PATCH 128/267] refactor(realtime): introduce realtime server class --- app/realtime.php | 333 +-------------- .../Realtime/{Realtime.php => Parser.php} | 2 +- src/Appwrite/Realtime/Server.php | 393 ++++++++++++++++++ tests/unit/Realtime/RealtimeChannelsTest.php | 32 +- tests/unit/Realtime/RealtimeGuestTest.php | 42 +- tests/unit/Realtime/RealtimeTest.php | 40 +- 6 files changed, 456 insertions(+), 386 deletions(-) rename src/Appwrite/Realtime/{Realtime.php => Parser.php} (99%) create mode 100644 src/Appwrite/Realtime/Server.php diff --git a/app/realtime.php b/app/realtime.php index a125118654..6d94b2cc4a 100644 --- a/app/realtime.php +++ b/app/realtime.php @@ -1,336 +1,13 @@ set([ +$config = [ 'package_max_length' => 64000 // Default maximum Package Size (64kb) -]); +]; -$subscriptions = []; -$connections = []; - -$stats = new Table(4096, 1); -$stats->column('projectId', Table::TYPE_STRING, 64); -$stats->column('connections', Table::TYPE_INT); -$stats->column('connectionsTotal', Table::TYPE_INT); -$stats->column('messages', Table::TYPE_INT); -$stats->create(); - -/** - * Sends usage stats every 10 seconds. - */ -Timer::tick(10000, function () use (&$stats) { - /** @var Table $stats */ - 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, [ - 'projectId' => $projectId, - 'messages' => 0, - 'connections' => 0 - ]); - - if (App::getEnv('_APP_USAGE_STATS', 'enabled') == 'enabled') { - $usage->trigger(); - } - } -}); - -$server->on('workerStart', function ($server, $workerId) use (&$subscriptions, &$register, &$stats) { - Console::success('Worker ' . $workerId . ' started succefully'); - - $attempts = 0; - $start = time(); - $redisPool = $register->get('redisPool'); - - /** - * Sending current connections to project channels on the console project every 5 seconds. - */ - $server->tick(5000, function () use (&$server, &$subscriptions, &$stats) { - if ( - array_key_exists('console', $subscriptions) - && array_key_exists('role:member', $subscriptions['console']) - && array_key_exists('project', $subscriptions['console']['role:member']) - ) { - $payload = []; - foreach ($stats as $projectId => $value) { - $payload[$projectId] = $value['connectionsTotal']; - } - foreach ($subscriptions['console']['role:member']['project'] as $connection => $value) { - $server->push( - $connection, - json_encode([ - 'event' => 'stats.connections', - 'channels' => ['project'], - 'timestamp' => time(), - 'payload' => $payload - ]), - SWOOLE_WEBSOCKET_OPCODE_TEXT, - SWOOLE_WEBSOCKET_FLAG_FIN | SWOOLE_WEBSOCKET_FLAG_COMPRESS - ); - } - } - }); - - 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 - } - - /** @var Swoole\Coroutine\Redis $redis */ - $redis = $redisPool->get(); - - 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, $channel, $payload) use ($server, $workerId, &$subscriptions, &$stats) { - /** - * Supported Resources: - * - Collection - * - Document - * - File - * - Account - * - Session - * - Team? (not implemented yet) - * - Membership? (not implemented yet) - * - Function - * - Execution - */ - $event = json_decode($payload, true); - - $receivers = Realtime::identifyReceivers($event, $subscriptions); - - - // Temporarily print debug logs by default for Alpha testing. - // if (App::isDevelopment() && !empty($receivers)) { - if (!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); - } - - foreach ($receivers as $receiver) { - if ($server->exist($receiver) && $server->isEstablished($receiver)) { - $server->push( - $receiver, - json_encode($event['data']), - SWOOLE_WEBSOCKET_OPCODE_TEXT, - SWOOLE_WEBSOCKET_FLAG_FIN | SWOOLE_WEBSOCKET_FLAG_COMPRESS - ); - } else { - $server->close($receiver); - } - } - if (($num = count($receivers)) > 0) { - $stats->incr($event['project'], 'messages', $num); - } - }); - } catch (\Throwable $th) { - Console::error('Pub/sub error: ' . $th->getMessage()); - $redisPool->put($redis); - $attempts++; - continue; - } - - $attempts++; - } - - Console::error('Failed to restart pub/sub...'); -}); - -$server->on('start', function (Server $server) { - Console::success('Server started succefully'); - - Console::info("Master pid {$server->master_pid}, manager pid {$server->manager_pid}"); - - // listen ctrl + c - Process::signal(2, function () use ($server) { - Console::log('Stop by Ctrl+C'); - $server->shutdown(); - }); -}); - -$server->on('open', function (Server $server, Request $request) use (&$connections, &$subscriptions, &$register, &$stats) { - $app = new App('UTC'); - $connection = $request->fd; - $request = new SwooleRequest($request); - - $db = $register->get('dbPool')->get(); - $redis = $register->get('redisPool')->get(); - - $register->set('db', function () use (&$db) { - return $db; - }); - - $register->set('cache', function () use (&$redis) { // Register cache connection - return $redis; - }); - - Console::info("Connection open (user: {$connection}, worker: {$server->getWorkerId()})"); - - App::setResource('request', function () use ($request) { - return $request; - }); - - App::setResource('response', function () { - return new Response(new SwooleResponse()); - }); - - 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, function () use ($db) { - return $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); - } - - Realtime::setUser($user); - - $roles = Realtime::getRoles(); - $channels = Realtime::parseChannels($request->getQuery('channels', [])); - - /** - * Channels Check - */ - if (empty($channels)) { - throw new Exception('Missing channels', 1008); - } - - Realtime::subscribe($project->getId(), $connection, $roles, $subscriptions, $connections, $channels); - - $server->push($connection, json_encode($channels)); - - $stats->incr($project->getId(), 'connections'); - $stats->incr($project->getId(), 'connectionsTotal'); - } catch (\Throwable $th) { - $response = [ - 'code' => $th->getCode(), - 'message' => $th->getMessage() - ]; - // Temporarily print debug logs by default for Alpha testing. - //if (App::isDevelopment()) { - Console::error("[Error] Connection Error"); - Console::error("[Error] Code: " . $response['code']); - Console::error("[Error] Message: " . $response['message']); - //} - $server->push($connection, json_encode($response)); - $server->close($connection); - } - /** - * Put used PDO and Redis Connections back into their pools. - */ - /** @var PDOPool $dbPool */ - $dbPool = $register->get('dbPool'); - $dbPool->put($db); - - /** @var RedisPool $redisPool */ - $redisPool = $register->get('redisPool'); - $redisPool->put($redis); -}); - -$server->on('message', function (Server $server, Frame $frame) { - $server->push($frame->fd, 'Sending messages is not allowed.'); - $server->close($frame->fd); -}); - -$server->on('close', function (Server $server, int $connection) use (&$connections, &$subscriptions, &$stats) { - if (array_key_exists($connection, $connections)) { - $stats->decr($connections[$connection]['projectId'], 'connectionsTotal'); - } - Realtime::unsubscribe($connection, $subscriptions, $connections); - Console::info('Connection close: ' . $connection); -}); - -$server->start(); +$realtimeServer = new Server($register, config: $config); diff --git a/src/Appwrite/Realtime/Realtime.php b/src/Appwrite/Realtime/Parser.php similarity index 99% rename from src/Appwrite/Realtime/Realtime.php rename to src/Appwrite/Realtime/Parser.php index f0f893f267..f99e7bfbe9 100644 --- a/src/Appwrite/Realtime/Realtime.php +++ b/src/Appwrite/Realtime/Parser.php @@ -5,7 +5,7 @@ namespace Appwrite\Realtime; use Appwrite\Auth\Auth; use Appwrite\Database\Document; -class Realtime +class Parser { /** * @var Document $user diff --git a/src/Appwrite/Realtime/Server.php b/src/Appwrite/Realtime/Server.php new file mode 100644 index 0000000000..0eed3f24b4 --- /dev/null +++ b/src/Appwrite/Realtime/Server.php @@ -0,0 +1,393 @@ +subscriptions = []; + $this->connections = []; + $this->register = $register; + + $this->stats = new Table(4096, 1); + $this->stats->column('projectId', Table::TYPE_STRING, 64); + $this->stats->column('connections', Table::TYPE_INT); + $this->stats->column('connectionsTotal', Table::TYPE_INT); + $this->stats->column('messages', Table::TYPE_INT); + $this->stats->create(); + + $this->server = new SwooleServer($host, $port, SWOOLE_PROCESS); + $this->server->set($config); + $this->server->on('start', [$this, 'onStart']); + $this->server->on('workerStart', [$this, 'onWorkerStart']); + $this->server->on('open', [$this, 'onOpen']); + $this->server->on('message', [$this, 'onMessage']); + $this->server->on('close', [$this, 'onClose']); + $this->server->start(); + } + + /** + * This is executed when the Realtime server starts. + * @param SwooleServer $server + * @return void + */ + public function onStart(SwooleServer $server): void + { + Console::success('Server started succefully'); + Console::info("Master pid {$server->master_pid}, manager pid {$server->manager_pid}"); + + Timer::tick(10000, function () { + /** @var Table $stats */ + foreach ($this->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); + + $this->stats->set($projectId, [ + 'projectId' => $projectId, + 'messages' => 0, + 'connections' => 0 + ]); + + if (App::getEnv('_APP_USAGE_STATS', 'enabled') == 'enabled') { + $usage->trigger(); + } + } + }); + + Process::signal(2, function () use ($server) { + Console::log('Stop by Ctrl+C'); + $server->shutdown(); + }); + } + + /** + * This is executed when a WebSocket worker process starts. + * @param SwooleServer $server + * @param int $workerId + * @return void + * @throws Exception + */ + public function onWorkerStart(SwooleServer $server, int $workerId): void + { + Console::success('Worker ' . $workerId . ' started succefully'); + + $attempts = 0; + $start = time(); + $redisPool = $this->register->get('redisPool'); + + /** + * Sending current connections to project channels on the console project every 5 seconds. + */ + $server->tick(5000, function () use (&$server) { + $this->tickSendProjectUsage($server); + }); + + 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 + } + + /** @var Swoole\Coroutine\Redis $redis */ + $redis = $redisPool->get(); + + 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, $channel, $payload) use ($server, $workerId) { + $this->onRedisPublish($payload, $server, $workerId); + }); + } catch (\Throwable $th) { + Console::error('Pub/sub error: ' . $th->getMessage()); + $redisPool->put($redis); + $attempts++; + continue; + } + + $attempts++; + } + + Console::error('Failed to restart pub/sub...'); + } + + /** + * This is executed when a new Realtime connection is established. + * @param SwooleServer $server + * @param Request $request + * @return void + * @throws Exception + * @throws UtopiaException + */ + public function onOpen(SwooleServer $server, Request $request): void + { + $app = new App('UTC'); + $connection = $request->fd; + $request = new SwooleRequest($request); + + $db = $this->register->get('dbPool')->get(); + $redis = $this->register->get('redisPool')->get(); + + $this->register->set('db', function () use (&$db) { + return $db; + }); + + $this->register->set('cache', function () use (&$redis) { // Register cache connection + return $redis; + }); + + Console::info("Connection open (user: {$connection}, worker: {$server->getWorkerId()})"); + + App::setResource('request', function () use ($request) { + return $request; + }); + + App::setResource('response', function () { + return new Response(new SwooleResponse()); + }); + + 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, function () use ($db) { + return $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); + } + + Parser::setUser($user); + + $roles = Parser::getRoles(); + $channels = Parser::parseChannels($request->getQuery('channels', [])); + + /** + * Channels Check + */ + if (empty($channels)) { + throw new Exception('Missing channels', 1008); + } + + Parser::subscribe($project->getId(), $connection, $roles, $this->subscriptions, $this->connections, $channels); + + $server->push($connection, json_encode($channels)); + + $this->stats->incr($project->getId(), 'connections'); + $this->stats->incr($project->getId(), 'connectionsTotal'); + } catch (\Throwable $th) { + $response = [ + 'code' => $th->getCode(), + 'message' => $th->getMessage() + ]; + // Temporarily print debug logs by default for Alpha testing. + //if (App::isDevelopment()) { + Console::error("[Error] Connection Error"); + Console::error("[Error] Code: " . $response['code']); + Console::error("[Error] Message: " . $response['message']); + //} + $server->push($connection, json_encode($response)); + $server->close($connection); + } + /** + * Put used PDO and Redis Connections back into their pools. + */ + /** @var PDOPool $dbPool */ + $dbPool = $this->register->get('dbPool'); + $dbPool->put($db); + + /** @var RedisPool $redisPool */ + $redisPool = $this->register->get('redisPool'); + $redisPool->put($redis); + } + + /** + * This is executed when a message is received by the Realtime server. + * @param SwooleServer $server + * @param Frame $frame + * @return void + */ + public function onMessage(SwooleServer $server, Frame $frame) + { + $server->push($frame->fd, 'Sending messages is not allowed.'); + $server->close($frame->fd); + } + + /** + * This is executed when a Realtime connection is closed. + * @param SwooleServer $server + * @param int $connection + * @return void + */ + public function onClose(SwooleServer $server, int $connection) + { + if (array_key_exists($connection, $this->connections)) { + $this->stats->decr($this->connections[$connection]['projectId'], 'connectionsTotal'); + } + Parser::unsubscribe($connection, $this->subscriptions, $this->connections); + Console::info('Connection close: ' . $connection); + } + + /** + * This is executed when an event is published on realtime channel in Redis. + * @param string $payload + * @param SwooleServer $server + * @param int $workerId + * @return void + */ + public function onRedisPublish(string $payload, SwooleServer &$server, int $workerId) + { + /** + * Supported Resources: + * - Collection + * - Document + * - File + * - Account + * - Session + * - Team? (not implemented yet) + * - Membership? (not implemented yet) + * - Function + * - Execution + */ + $event = json_decode($payload, true); + + $receivers = Parser::identifyReceivers($event, $this->subscriptions); + + // Temporarily print debug logs by default for Alpha testing. + // if (App::isDevelopment() && !empty($receivers)) { + if (!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); + } + + foreach ($receivers as $receiver) { + if ($server->exist($receiver) && $server->isEstablished($receiver)) { + $server->push( + $receiver, + json_encode($event['data']), + SWOOLE_WEBSOCKET_OPCODE_TEXT, + SWOOLE_WEBSOCKET_FLAG_FIN | SWOOLE_WEBSOCKET_FLAG_COMPRESS + ); + } else { + $server->close($receiver); + } + } + if (($num = count($receivers)) > 0) { + $this->stats->incr($event['project'], 'messages', $num); + } + } + + /** + * This sends the usage to the `console` channel. + * @param SwooleServer $server + * @return void + */ + public function tickSendProjectUsage(SwooleServer &$server) + { + if ( + array_key_exists('console', $this->subscriptions) + && array_key_exists('role:member', $this->subscriptions['console']) + && array_key_exists('project', $this->subscriptions['console']['role:member']) + ) { + $payload = []; + foreach ($this->stats as $projectId => $value) { + $payload[$projectId] = $value['connectionsTotal']; + } + foreach ($this->subscriptions['console']['role:member']['project'] as $connection => $value) { + $server->push( + $connection, + json_encode([ + 'event' => 'stats.connections', + 'channels' => ['project'], + 'timestamp' => time(), + 'payload' => $payload + ]), + SWOOLE_WEBSOCKET_OPCODE_TEXT, + SWOOLE_WEBSOCKET_FLAG_FIN | SWOOLE_WEBSOCKET_FLAG_COMPRESS + ); + } + } + } +} diff --git a/tests/unit/Realtime/RealtimeChannelsTest.php b/tests/unit/Realtime/RealtimeChannelsTest.php index 5145acb2c4..923a38325e 100644 --- a/tests/unit/Realtime/RealtimeChannelsTest.php +++ b/tests/unit/Realtime/RealtimeChannelsTest.php @@ -3,7 +3,7 @@ namespace Appwrite\Tests; use Appwrite\Database\Document; -use Appwrite\Realtime\Realtime; +use Appwrite\Realtime; use PHPUnit\Framework\TestCase; class RealtimeChannelsTest extends TestCase @@ -46,7 +46,7 @@ class RealtimeChannelsTest extends TestCase */ for ($i = 0; $i < $this->connectionsPerChannel; $i++) { foreach ($this->allChannels as $index => $channel) { - Realtime::setUser(new Document([ + Realtime\Parser::setUser(new Document([ '$id' => 'user' . $this->connectionsCount, 'memberships' => [ [ @@ -57,10 +57,10 @@ class RealtimeChannelsTest extends TestCase ] ] ])); - $roles = Realtime::getRoles(); - $parsedChannels = Realtime::parseChannels([0 => $channel]); + $roles = Realtime\Parser::getRoles(); + $parsedChannels = Realtime\Parser::parseChannels([0 => $channel]); - Realtime::subscribe( + Realtime\Parser::subscribe( '1', $this->connectionsCount, $roles, @@ -78,14 +78,14 @@ class RealtimeChannelsTest extends TestCase */ for ($i = 0; $i < $this->connectionsPerChannel; $i++) { foreach ($this->allChannels as $index => $channel) { - Realtime::setUser(new Document([ + Realtime\Parser::setUser(new Document([ '$id' => '' ])); - $roles = Realtime::getRoles(); - $parsedChannels = Realtime::parseChannels([0 => $channel]); + $roles = Realtime\Parser::getRoles(); + $parsedChannels = Realtime\Parser::parseChannels([0 => $channel]); - Realtime::subscribe( + Realtime\Parser::subscribe( '1', $this->connectionsCount, $roles, @@ -130,13 +130,13 @@ class RealtimeChannelsTest extends TestCase */ $this->assertCount($this->connectionsTotal, $this->connections); - Realtime::unsubscribe(-1, $this->subscriptions, $this->connections); + Realtime\Parser::unsubscribe(-1, $this->subscriptions, $this->connections); $this->assertCount($this->connectionsTotal, $this->connections); $this->assertCount(($this->connectionsAuthenticated + (3 * $this->connectionsPerChannel) + 2), $this->subscriptions['1']); for ($i = 0; $i < $this->connectionsCount; $i++) { - Realtime::unsubscribe($i, $this->subscriptions, $this->connections); + Realtime\Parser::unsubscribe($i, $this->subscriptions, $this->connections); $this->assertCount(($this->connectionsCount - $i - 1), $this->connections); } @@ -161,7 +161,7 @@ class RealtimeChannelsTest extends TestCase ] ]; - $receivers = Realtime::identifyReceivers( + $receivers = Realtime\Parser::identifyReceivers( $event, $this->subscriptions ); @@ -197,7 +197,7 @@ class RealtimeChannelsTest extends TestCase ] ]; - $receivers = Realtime::identifyReceivers( + $receivers = Realtime\Parser::identifyReceivers( $event, $this->subscriptions ); @@ -234,7 +234,7 @@ class RealtimeChannelsTest extends TestCase ] ]; - $receivers = Realtime::identifyReceivers( + $receivers = Realtime\Parser::identifyReceivers( $event, $this->subscriptions ); @@ -271,7 +271,7 @@ class RealtimeChannelsTest extends TestCase ] ]; - $receivers = Realtime::identifyReceivers( + $receivers = Realtime\Parser::identifyReceivers( $event, $this->subscriptions ); @@ -300,7 +300,7 @@ class RealtimeChannelsTest extends TestCase ] ]; - $receivers = Realtime::identifyReceivers( + $receivers = Realtime\Parser::identifyReceivers( $event, $this->subscriptions ); diff --git a/tests/unit/Realtime/RealtimeGuestTest.php b/tests/unit/Realtime/RealtimeGuestTest.php index b8cd68f8a9..01e43d7308 100644 --- a/tests/unit/Realtime/RealtimeGuestTest.php +++ b/tests/unit/Realtime/RealtimeGuestTest.php @@ -3,7 +3,7 @@ namespace Appwrite\Tests; use Appwrite\Database\Document; -use Appwrite\Realtime\Realtime; +use Appwrite\Realtime; use PHPUnit\Framework\TestCase; class RealtimeGuestTest extends TestCase @@ -13,11 +13,11 @@ class RealtimeGuestTest extends TestCase public function testGuest() { - Realtime::setUser(new Document([ + Realtime\Parser::setUser(new Document([ '$id' => '' ])); - $roles = Realtime::getRoles(); + $roles = Realtime\Parser::getRoles(); $this->assertCount(1, $roles); $this->assertContains('role:guest', $roles); @@ -29,7 +29,7 @@ class RealtimeGuestTest extends TestCase 4 => 'account.456' ]; - $channels = Realtime::parseChannels($channels); + $channels = Realtime\Parser::parseChannels($channels); $this->assertCount(3, $channels); $this->assertArrayHasKey('files', $channels); $this->assertArrayHasKey('documents', $channels); @@ -37,7 +37,7 @@ class RealtimeGuestTest extends TestCase $this->assertArrayNotHasKey('account', $channels); $this->assertArrayNotHasKey('account.456', $channels); - Realtime::subscribe('1', 1, $roles, $this->subscriptions, $this->connections, $channels); + Realtime\Parser::subscribe('1', 1, $roles, $this->subscriptions, $this->connections, $channels); $event = [ 'project' => '1', @@ -50,7 +50,7 @@ class RealtimeGuestTest extends TestCase ] ]; - $receivers = Realtime::identifyReceivers( + $receivers = Realtime\Parser::identifyReceivers( $event, $this->subscriptions ); @@ -60,7 +60,7 @@ class RealtimeGuestTest extends TestCase $event['permissions'] = ['role:guest']; - $receivers = Realtime::identifyReceivers( + $receivers = Realtime\Parser::identifyReceivers( $event, $this->subscriptions ); @@ -70,7 +70,7 @@ class RealtimeGuestTest extends TestCase $event['permissions'] = ['role:member']; - $receivers = Realtime::identifyReceivers( + $receivers = Realtime\Parser::identifyReceivers( $event, $this->subscriptions ); @@ -79,7 +79,7 @@ class RealtimeGuestTest extends TestCase $event['permissions'] = ['user:123']; - $receivers = Realtime::identifyReceivers( + $receivers = Realtime\Parser::identifyReceivers( $event, $this->subscriptions ); @@ -88,7 +88,7 @@ class RealtimeGuestTest extends TestCase $event['permissions'] = ['team:abc']; - $receivers = Realtime::identifyReceivers( + $receivers = Realtime\Parser::identifyReceivers( $event, $this->subscriptions ); @@ -97,7 +97,7 @@ class RealtimeGuestTest extends TestCase $event['permissions'] = ['team:abc/administrator']; - $receivers = Realtime::identifyReceivers( + $receivers = Realtime\Parser::identifyReceivers( $event, $this->subscriptions ); @@ -106,7 +106,7 @@ class RealtimeGuestTest extends TestCase $event['permissions'] = ['team:abc/god']; - $receivers = Realtime::identifyReceivers( + $receivers = Realtime\Parser::identifyReceivers( $event, $this->subscriptions ); @@ -115,7 +115,7 @@ class RealtimeGuestTest extends TestCase $event['permissions'] = ['team:def']; - $receivers = Realtime::identifyReceivers( + $receivers = Realtime\Parser::identifyReceivers( $event, $this->subscriptions ); @@ -124,7 +124,7 @@ class RealtimeGuestTest extends TestCase $event['permissions'] = ['team:def/guest']; - $receivers = Realtime::identifyReceivers( + $receivers = Realtime\Parser::identifyReceivers( $event, $this->subscriptions ); @@ -133,7 +133,7 @@ class RealtimeGuestTest extends TestCase $event['permissions'] = ['user:456']; - $receivers = Realtime::identifyReceivers( + $receivers = Realtime\Parser::identifyReceivers( $event, $this->subscriptions ); @@ -142,7 +142,7 @@ class RealtimeGuestTest extends TestCase $event['permissions'] = ['team:def/member']; - $receivers = Realtime::identifyReceivers( + $receivers = Realtime\Parser::identifyReceivers( $event, $this->subscriptions ); @@ -152,7 +152,7 @@ class RealtimeGuestTest extends TestCase $event['permissions'] = ['*']; $event['data']['channels'] = ['documents.123']; - $receivers = Realtime::identifyReceivers( + $receivers = Realtime\Parser::identifyReceivers( $event, $this->subscriptions ); @@ -161,7 +161,7 @@ class RealtimeGuestTest extends TestCase $event['data']['channels'] = ['documents.789']; - $receivers = Realtime::identifyReceivers( + $receivers = Realtime\Parser::identifyReceivers( $event, $this->subscriptions ); @@ -171,19 +171,19 @@ class RealtimeGuestTest extends TestCase $event['project'] = '2'; - $receivers = Realtime::identifyReceivers( + $receivers = Realtime\Parser::identifyReceivers( $event, $this->subscriptions ); $this->assertEmpty($receivers); - Realtime::unsubscribe(2, $this->subscriptions, $this->connections); + Realtime\Parser::unsubscribe(2, $this->subscriptions, $this->connections); $this->assertCount(1, $this->connections); $this->assertCount(1, $this->subscriptions['1']); - Realtime::unsubscribe(1, $this->subscriptions, $this->connections); + Realtime\Parser::unsubscribe(1, $this->subscriptions, $this->connections); $this->assertEmpty($this->connections); $this->assertEmpty($this->subscriptions); diff --git a/tests/unit/Realtime/RealtimeTest.php b/tests/unit/Realtime/RealtimeTest.php index a819f4cb72..a722bea10d 100644 --- a/tests/unit/Realtime/RealtimeTest.php +++ b/tests/unit/Realtime/RealtimeTest.php @@ -3,7 +3,7 @@ namespace Appwrite\Tests; use Appwrite\Database\Document; -use Appwrite\Realtime\Realtime; +use Appwrite\Realtime; use PHPUnit\Framework\TestCase; class RealtimeTest extends TestCase @@ -21,7 +21,7 @@ class RealtimeTest extends TestCase public function testUser() { - Realtime::setUser(new Document([ + Realtime\Parser::setUser(new Document([ '$id' => '123', 'memberships' => [ [ @@ -40,7 +40,7 @@ class RealtimeTest extends TestCase ] ])); - $roles = Realtime::getRoles(); + $roles = Realtime\Parser::getRoles(); $this->assertCount(7, $roles); $this->assertContains('user:123', $roles); @@ -59,7 +59,7 @@ class RealtimeTest extends TestCase 4 => 'account.456' ]; - $channels = Realtime::parseChannels($channels); + $channels = Realtime\Parser::parseChannels($channels); $this->assertCount(4, $channels); $this->assertArrayHasKey('files', $channels); @@ -69,7 +69,7 @@ class RealtimeTest extends TestCase $this->assertArrayNotHasKey('account', $channels); $this->assertArrayNotHasKey('account.456', $channels); - Realtime::subscribe('1', 1, $roles, $this->subscriptions, $this->connections, $channels); + Realtime\Parser::subscribe('1', 1, $roles, $this->subscriptions, $this->connections, $channels); $event = [ 'project' => '1', @@ -81,7 +81,7 @@ class RealtimeTest extends TestCase ] ]; - $receivers = Realtime::identifyReceivers( + $receivers = Realtime\Parser::identifyReceivers( $event, $this->subscriptions ); @@ -91,7 +91,7 @@ class RealtimeTest extends TestCase $event['permissions'] = ['role:member']; - $receivers = Realtime::identifyReceivers( + $receivers = Realtime\Parser::identifyReceivers( $event, $this->subscriptions ); @@ -101,7 +101,7 @@ class RealtimeTest extends TestCase $event['permissions'] = ['user:123']; - $receivers = Realtime::identifyReceivers( + $receivers = Realtime\Parser::identifyReceivers( $event, $this->subscriptions ); @@ -111,7 +111,7 @@ class RealtimeTest extends TestCase $event['permissions'] = ['team:abc']; - $receivers = Realtime::identifyReceivers( + $receivers = Realtime\Parser::identifyReceivers( $event, $this->subscriptions ); @@ -121,7 +121,7 @@ class RealtimeTest extends TestCase $event['permissions'] = ['team:abc/administrator']; - $receivers = Realtime::identifyReceivers( + $receivers = Realtime\Parser::identifyReceivers( $event, $this->subscriptions ); @@ -131,7 +131,7 @@ class RealtimeTest extends TestCase $event['permissions'] = ['team:abc/god']; - $receivers = Realtime::identifyReceivers( + $receivers = Realtime\Parser::identifyReceivers( $event, $this->subscriptions ); @@ -141,7 +141,7 @@ class RealtimeTest extends TestCase $event['permissions'] = ['team:def']; - $receivers = Realtime::identifyReceivers( + $receivers = Realtime\Parser::identifyReceivers( $event, $this->subscriptions ); @@ -151,7 +151,7 @@ class RealtimeTest extends TestCase $event['permissions'] = ['team:def/guest']; - $receivers = Realtime::identifyReceivers( + $receivers = Realtime\Parser::identifyReceivers( $event, $this->subscriptions ); @@ -161,7 +161,7 @@ class RealtimeTest extends TestCase $event['permissions'] = ['user:456']; - $receivers = Realtime::identifyReceivers( + $receivers = Realtime\Parser::identifyReceivers( $event, $this->subscriptions ); @@ -170,7 +170,7 @@ class RealtimeTest extends TestCase $event['permissions'] = ['team:def/member']; - $receivers = Realtime::identifyReceivers( + $receivers = Realtime\Parser::identifyReceivers( $event, $this->subscriptions ); @@ -180,7 +180,7 @@ class RealtimeTest extends TestCase $event['permissions'] = ['*']; $event['data']['channels'] = ['documents.123']; - $receivers = Realtime::identifyReceivers( + $receivers = Realtime\Parser::identifyReceivers( $event, $this->subscriptions ); @@ -189,7 +189,7 @@ class RealtimeTest extends TestCase $event['data']['channels'] = ['documents.789']; - $receivers = Realtime::identifyReceivers( + $receivers = Realtime\Parser::identifyReceivers( $event, $this->subscriptions ); @@ -199,20 +199,20 @@ class RealtimeTest extends TestCase $event['project'] = '2'; - $receivers = Realtime::identifyReceivers( + $receivers = Realtime\Parser::identifyReceivers( $event, $this->subscriptions ); $this->assertEmpty($receivers); - Realtime::unsubscribe(2, $this->subscriptions, $this->connections); + Realtime\Parser::unsubscribe(2, $this->subscriptions, $this->connections); $this->assertCount(1, $this->connections); $this->assertCount(7, $this->subscriptions['1']); - Realtime::unsubscribe(1, $this->subscriptions, $this->connections); + Realtime\Parser::unsubscribe(1, $this->subscriptions, $this->connections); $this->assertEmpty($this->connections); $this->assertEmpty($this->subscriptions); From 346b04b94b21adceb89e2f65840c6033ae6178cc Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Mon, 14 Jun 2021 16:54:16 +0200 Subject: [PATCH 129/267] sync with 0.9.x --- app/workers/functions.php | 1 + composer.lock | 34 +++++++++---------- src/Appwrite/Database/Pool/PDO.php | 49 ---------------------------- src/Appwrite/Database/Pool/Redis.php | 42 ------------------------ 4 files changed, 18 insertions(+), 108 deletions(-) delete mode 100644 src/Appwrite/Database/Pool/PDO.php delete mode 100644 src/Appwrite/Database/Pool/Redis.php diff --git a/app/workers/functions.php b/app/workers/functions.php index d360ecd9b4..99a73496da 100644 --- a/app/workers/functions.php +++ b/app/workers/functions.php @@ -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\Event\Realtime; use Appwrite\Resque\Worker; use Cron\CronExpression; use Swoole\Runtime; diff --git a/composer.lock b/composer.lock index e68554cbcb..03f829151a 100644 --- a/composer.lock +++ b/composer.lock @@ -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": "399d2426ca92e04b6d6fb84a91c316c3", + "content-hash": "ecfe641507c78e5e886eeece09c01d50", "packages": [ { "name": "adhocore/jwt", @@ -2713,20 +2713,20 @@ }, { "name": "felixfbecker/advanced-json-rpc", - "version": "v3.2.0", + "version": "v3.2.1", "source": { "type": "git", "url": "https://github.com/felixfbecker/php-advanced-json-rpc.git", - "reference": "06f0b06043c7438959dbdeed8bb3f699a19be22e" + "reference": "b5f37dbff9a8ad360ca341f3240dc1c168b45447" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/felixfbecker/php-advanced-json-rpc/zipball/06f0b06043c7438959dbdeed8bb3f699a19be22e", - "reference": "06f0b06043c7438959dbdeed8bb3f699a19be22e", + "url": "https://api.github.com/repos/felixfbecker/php-advanced-json-rpc/zipball/b5f37dbff9a8ad360ca341f3240dc1c168b45447", + "reference": "b5f37dbff9a8ad360ca341f3240dc1c168b45447", "shasum": "" }, "require": { - "netresearch/jsonmapper": "^1.0 || ^2.0", + "netresearch/jsonmapper": "^1.0 || ^2.0 || ^3.0 || ^4.0", "php": "^7.1 || ^8.0", "phpdocumentor/reflection-docblock": "^4.3.4 || ^5.0.0" }, @@ -2752,9 +2752,9 @@ "description": "A more advanced JSONRPC implementation", "support": { "issues": "https://github.com/felixfbecker/php-advanced-json-rpc/issues", - "source": "https://github.com/felixfbecker/php-advanced-json-rpc/tree/v3.2.0" + "source": "https://github.com/felixfbecker/php-advanced-json-rpc/tree/v3.2.1" }, - "time": "2021-01-10T17:48:47+00:00" + "time": "2021-06-11T22:34:44+00:00" }, { "name": "felixfbecker/language-server-protocol", @@ -3003,16 +3003,16 @@ }, { "name": "netresearch/jsonmapper", - "version": "v2.1.0", + "version": "v4.0.0", "source": { "type": "git", "url": "https://github.com/cweiske/jsonmapper.git", - "reference": "e0f1e33a71587aca81be5cffbb9746510e1fe04e" + "reference": "8bbc021a8edb2e4a7ea2f8ad4fa9ec9dce2fcb8d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/cweiske/jsonmapper/zipball/e0f1e33a71587aca81be5cffbb9746510e1fe04e", - "reference": "e0f1e33a71587aca81be5cffbb9746510e1fe04e", + "url": "https://api.github.com/repos/cweiske/jsonmapper/zipball/8bbc021a8edb2e4a7ea2f8ad4fa9ec9dce2fcb8d", + "reference": "8bbc021a8edb2e4a7ea2f8ad4fa9ec9dce2fcb8d", "shasum": "" }, "require": { @@ -3020,10 +3020,10 @@ "ext-pcre": "*", "ext-reflection": "*", "ext-spl": "*", - "php": ">=5.6" + "php": ">=7.1" }, "require-dev": { - "phpunit/phpunit": "~4.8.35 || ~5.7 || ~6.4 || ~7.0", + "phpunit/phpunit": "~7.5 || ~8.0 || ~9.0", "squizlabs/php_codesniffer": "~3.5" }, "type": "library", @@ -3048,9 +3048,9 @@ "support": { "email": "cweiske@cweiske.de", "issues": "https://github.com/cweiske/jsonmapper/issues", - "source": "https://github.com/cweiske/jsonmapper/tree/master" + "source": "https://github.com/cweiske/jsonmapper/tree/v4.0.0" }, - "time": "2020-04-16T18:48:43+00:00" + "time": "2020-12-01T19:48:11+00:00" }, { "name": "nikic/php-parser", @@ -6074,5 +6074,5 @@ "platform-overrides": { "php": "8.0" }, - "plugin-api-version": "2.1.0" + "plugin-api-version": "2.0.0" } diff --git a/src/Appwrite/Database/Pool/PDO.php b/src/Appwrite/Database/Pool/PDO.php deleted file mode 100644 index 7233f874ff..0000000000 --- a/src/Appwrite/Database/Pool/PDO.php +++ /dev/null @@ -1,49 +0,0 @@ -pool = new SplQueue; - $this->size = $size; - for ($i=0; $i < $this->size; $i++) { - $pdo = new PDO( - "mysql:". - "host={$host};". - "dbname={$schema};" . - "charset={$charset}", - $user, - $pass, - [ - PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8mb4', - PDO::ATTR_TIMEOUT => 3, // Seconds - PDO::ATTR_PERSISTENT => true, - PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, - PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, - PDO::MYSQL_ATTR_USE_BUFFERED_QUERY => true - ] - ); - $this->pool->enqueue($pdo); - } - } - - public function put (PDO $pdo) - { - $this->pool->enqueue($pdo); - } - - public function get (): PDO - { - if ($this->available && count($this->pool) > 0) { - return $this->pool->dequeue(); - } - sleep(0.01); - return $this->get(); - } -} diff --git a/src/Appwrite/Database/Pool/Redis.php b/src/Appwrite/Database/Pool/Redis.php deleted file mode 100644 index 197f20747c..0000000000 --- a/src/Appwrite/Database/Pool/Redis.php +++ /dev/null @@ -1,42 +0,0 @@ -pool = new SplQueue; - $this->size = $size; - for ($i=0; $i < $this->size; $i++) { - $redis = new Redis(); - $redis->pconnect($host, $port); - $redis->setOption(Redis::OPT_READ_TIMEOUT, -1); - - if ($auth) { - $redis->auth($auth); - } - - $this->pool->enqueue($redis); - } - } - - public function put (Redis $redis) - { - $this->pool->enqueue($redis); - } - - public function get (): Redis - { - if ($this->available && !$this->pool->isEmpty()) { - return $this->pool->dequeue(); - } - sleep(0.1); - return $this->get(); - } -} From 8638d738fa60789c24571d2449ceb586827ebeca Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Mon, 14 Jun 2021 20:22:57 +0200 Subject: [PATCH 130/267] fix dist js --- public/dist/scripts/app-all.js | 6 +++--- public/dist/scripts/app.js | 6 +++--- public/scripts/services/console.js | 2 +- public/scripts/services/sdk.js | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/public/dist/scripts/app-all.js b/public/dist/scripts/app-all.js index 6b52709e66..d287f8c5b6 100644 --- a/public/dist/scripts/app-all.js +++ b/public/dist/scripts/app-all.js @@ -2090,7 +2090,7 @@ container.set(as,container.path(context),true,watch);container.set(key,index,tru element.dispatchEvent(new Event("looped"));};let template=element.children.length===1?element.children[0]:window.document.createElement("li");echo();container.bind(element,expr+".length",echo);let path=(expr+".length").split(".");while(path.length){container.bind(element,path.join("."),echo);path.pop();}},});window.ls.container.get("view").add({selector:"data-ls-template",template:false,controller:function(element,view,http,expression,document,container){let template=element.getAttribute("data-ls-template")||"";let type=element.getAttribute("data-type")||"url";let debug=element.getAttribute("data-debug")||false;let paths=[];let check=function(init=false){let source=expression.parse(template);paths=expression.getPaths();element.innerHTML="";if("script"===type){let inlineTemplate=document.getElementById(source);if(inlineTemplate&&inlineTemplate.innerHTML){element.innerHTML=inlineTemplate.innerHTML;element.dispatchEvent(new CustomEvent("template-loaded",{bubbles:true,cancelable:false,}));}else{if(debug){console.error('Missing template "'+source+'"');}} if(!init){view.render(element);} return;} -http.get(source).then(function(element){return function(data){element.innerHTML=data;view.render(element);element.dispatchEvent(new CustomEvent('template-loaded',{bubbles:true,cancelable:false}));}}(element),function(){throw new Error('Failed loading template');});};check(true);for(let i=0;i{const realtime=window.ls.container.get('realtime');window.ls.container.get('console').subscribe('project',event=>{realtime.set(event.payload);});});(function(window){"use strict";window.ls.container.set('alerts',function(window){return{list:[],ids:0,counter:0,max:5,add:function(message,time){var scope=this;message.id=scope.ids++;message.remove=function(){scope.remove(message.id);};scope.counter++;scope.list.unshift(message);if(scope.counter>scope.max){scope.list.pop();scope.counter--;} +http.get(source).then((function(element){return function(data){element.innerHTML=data;view.render(element);element.dispatchEvent(new CustomEvent("template-loaded",{bubbles:true,cancelable:false,}));};})(element),function(){throw new Error("Failed loading template");});};check(true);for(let i=0;i{const realtime=window.ls.container.get('realtime');window.ls.container.get('console').subscribe('project',event=>{realtime.set(event.payload);});});(function(window){"use strict";window.ls.container.set('alerts',function(window){return{list:[],ids:0,counter:0,max:5,add:function(message,time){var scope=this;message.id=scope.ids++;message.remove=function(){scope.remove(message.id);};scope.counter++;scope.list.unshift(message);if(scope.counter>scope.max){scope.list.pop();scope.counter--;} if(time>0){window.setTimeout(function(message){return function(){scope.remove(message.id)}}(message),time);} return message.id;},remove:function(id){let scope=this;for(let index=0;index0)?'?'+buildQuery(params):''),headers,{});},'post':function(path,headers={},params={},progress=null){return call('POST',path,headers,params,progress);},'put':function(path,headers={},params={},progress=null){return call('PUT',headers,params,progress);},'patch':function(path,headers={},params={},progress=null){return call('PATCH',path,headers,params,progress);},'delete':function(path,headers={},params={},progress=null){return call('DELETE',path,headers,params,progress);},'addGlobalParam':addGlobalParam,'addGlobalHeader':addGlobalHeader}}(window.document);let analytics={create:function(id,source,activity,url){return http.post('/analytics',{'content-type':'application/json'},{id:id,source:source,activity:activity,url:url,version:env.VERSION,setup:env.SETUP});},};return{analytics:analytics,};},true);})(window);(function(window){"use strict";window.ls.container.set('console',function(window){var sdk=new window.Appwrite();sdk.setEndpoint(window.location.protocol+'//'+window.location.host+APP_ENV.API).setProject('console').setLocale(APP_ENV.LOCALE);return sdk;},true);})(window);(function(window){"use strict";window.ls.container.set('date',function(){function format(format,timestamp){var jsdate,f +request.onerror=function(){reject(new Error("Network Error"));};request.send(params);})};return{'get':function(path,headers={},params={}){return call('GET',path+((params.length>0)?'?'+buildQuery(params):''),headers,{});},'post':function(path,headers={},params={},progress=null){return call('POST',path,headers,params,progress);},'put':function(path,headers={},params={},progress=null){return call('PUT',headers,params,progress);},'patch':function(path,headers={},params={},progress=null){return call('PATCH',path,headers,params,progress);},'delete':function(path,headers={},params={},progress=null){return call('DELETE',path,headers,params,progress);},'addGlobalParam':addGlobalParam,'addGlobalHeader':addGlobalHeader}}(window.document);let analytics={create:function(id,source,activity,url){return http.post('/analytics',{'content-type':'application/json'},{id:id,source:source,activity:activity,url:url,version:env.VERSION,setup:env.SETUP});},};return{analytics:analytics,};},true);})(window);(function(window){"use strict";window.ls.container.set('console',function(window){var sdk=new window.Appwrite.Appwrite();sdk.setEndpoint(APP_ENV.ENDPOINT+APP_ENV.API).setProject('console').setLocale(APP_ENV.LOCALE);return sdk;},true);})(window);(function(window){"use strict";window.ls.container.set('date',function(){function format(format,timestamp){var jsdate,f var txtWords=['Sun','Mon','Tues','Wednes','Thurs','Fri','Satur','January','February','March','April','May','June','July','August','September','October','November','December'] var formatChr=/\\?(.?)/gi var formatChrCb=function(t,s){return f[t]?f[t]():s} @@ -2234,7 +2234,7 @@ return slf.renderToken(tokens,idx,opts);} md.renderer.rules.strong_open=renderEm;md.renderer.rules.strong_close=renderEm;return md;},true);})(window);(function(window){"use strict";window.ls.container.set('rtl',function(){var rtlStock="^ا^ب^ت^ث^ج^ح^خ^د^ذ^ر^ز^س^ش^ص^ض^ط^ظ^ع^غ^ف^ق^ك^ل^م^ن^ه^و^ي^א^ב^ג^ד^ה^ו^ז^ח^ט^י^כ^ך^ל^מ^ם^נ^ן^ס^ע^פ^ף^צ^ץ^ק^ר^ש^ת^";var special=["\n"," "," ","״",'"',"_","'","!","@","#","$","^","&","%","*","(",")","+","=","-","[","]","\\","/","{","}","|",":","<",">","?",",",".","0","1","2","3","4","5","6","7","8","9"];var isRTL=function(value){for(var i=0;i{return{current:null,set:function(currentConnections){var scope=this;scope.current=currentConnections;return scope.current;}};},true,true);})(window);window.ls.router.add("/auth/signin",{template:"/auth/signin?version="+APP_ENV.CACHEBUSTER,scope:"home"}).add("/auth/signup",{template:"/auth/signup?version="+APP_ENV.CACHEBUSTER,scope:"home"}).add("/auth/recovery",{template:"/auth/recovery?version="+APP_ENV.CACHEBUSTER,scope:"home"}).add("/auth/recovery/reset",{template:"/auth/recovery/reset?version="+APP_ENV.CACHEBUSTER,scope:"home"}).add("/auth/confirm",{template:"/auth/confirm?version="+APP_ENV.CACHEBUSTER,scope:"home"}).add("/auth/join",{template:"/auth/join?version="+APP_ENV.CACHEBUSTER,scope:"home"}).add("/auth/oauth2/success",{template:"/auth/oauth2/success?version="+APP_ENV.CACHEBUSTER,scope:"home"}).add("/auth/oauth2/failure",{template:"/auth/oauth2/failure?version="+APP_ENV.CACHEBUSTER,scope:"home"}).add("/console",{template:"/console?version="+APP_ENV.CACHEBUSTER,scope:"console"}).add("/console/account",{template:"/console/account?version="+APP_ENV.CACHEBUSTER,scope:"console"}).add("/console/account/:tab",{template:"/console/account?version="+APP_ENV.CACHEBUSTER,scope:"console"}).add("/console/home",{template:"/console/home?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/home/:tab",{template:"/console/home?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/platforms/:platform",{template:function(window){return window.location.pathname+"?version="+APP_ENV.CACHEBUSTER;},scope:"console",project:true}).add("/console/notifications",{template:"/console/notifications?version="+APP_ENV.CACHEBUSTER,scope:"console"}).add("/console/settings",{template:"/console/settings?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/settings/:tab",{template:"/console/settings?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/webhooks",{template:"/console/webhooks?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/webhooks/:tab",{template:"/console/webhooks?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/keys",{template:"/console/keys?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/keys/:tab",{template:"/console/keys?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/tasks",{template:"/console/tasks?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/tasks/:tab",{template:"/console/tasks?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/database",{template:"/console/database?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/database/collection",{template:function(window){return window.location.pathname+window.location.search+'&version='+APP_ENV.CACHEBUSTER;},scope:"console",project:true}).add("/console/database/collection/:tab",{template:function(window){return window.location.pathname+window.location.search+'&version='+APP_ENV.CACHEBUSTER;},scope:"console",project:true}).add("/console/database/document",{template:function(window){return window.location.pathname+window.location.search+'&version='+APP_ENV.CACHEBUSTER;},scope:"console",project:true}).add("/console/database/document/:tab",{template:function(window){return window.location.pathname+window.location.search+'&version='+APP_ENV.CACHEBUSTER;},scope:"console",project:true}).add("/console/storage",{template:"/console/storage?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/storage/:tab",{template:"/console/storage?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/users",{template:"/console/users?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/users/user",{template:"/console/users/user?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/users/user/:tab",{template:"/console/users/user?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/users/teams/team",{template:"/console/users/teams/team?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/users/teams/team/:tab",{template:"/console/users/teams/team?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/users/:tab",{template:"/console/users?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/functions",{template:"/console/functions?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/functions/function",{template:"/console/functions/function?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/functions/function/:tab",{template:"/console/functions/function?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/functions/:tab",{template:"/console/functions?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true});window.ls.filter.add("avatar",function($value,element){if(!$value){return"";} +return false;};return{isRTL:isRTL,};},true);})(window);(function(window){"use strict";window.ls.container.set('sdk',function(window,router){var sdk=new window.Appwrite.Appwrite();sdk.setEndpoint(APP_ENV.ENDPOINT+APP_ENV.API).setProject(router.params.project||'').setLocale(APP_ENV.LOCALE).setMode('admin');return sdk;},false);})(window);(function(window){"use strict";window.ls.container.set('search',function(window){return{params:{},path:'',pointer:'',selected:'',};},true,true);})(window);(function(window){"use strict";window.ls.container.set('timezone',function(){return{convert:function(unixTime){var timezoneMinutes=new Date().getTimezoneOffset();timezoneMinutes=(timezoneMinutes===0)?0:-timezoneMinutes;return parseInt(unixTime)+(timezoneMinutes*60);}};},true);})(window);(function(window){"use strict";window.ls.container.set('realtime',()=>{return{current:null,set:function(currentConnections){var scope=this;scope.current=currentConnections;return scope.current;}};},true,true);})(window);window.ls.router.add("/auth/signin",{template:"/auth/signin?version="+APP_ENV.CACHEBUSTER,scope:"home"}).add("/auth/signup",{template:"/auth/signup?version="+APP_ENV.CACHEBUSTER,scope:"home"}).add("/auth/recovery",{template:"/auth/recovery?version="+APP_ENV.CACHEBUSTER,scope:"home"}).add("/auth/recovery/reset",{template:"/auth/recovery/reset?version="+APP_ENV.CACHEBUSTER,scope:"home"}).add("/auth/confirm",{template:"/auth/confirm?version="+APP_ENV.CACHEBUSTER,scope:"home"}).add("/auth/join",{template:"/auth/join?version="+APP_ENV.CACHEBUSTER,scope:"home"}).add("/auth/oauth2/success",{template:"/auth/oauth2/success?version="+APP_ENV.CACHEBUSTER,scope:"home"}).add("/auth/oauth2/failure",{template:"/auth/oauth2/failure?version="+APP_ENV.CACHEBUSTER,scope:"home"}).add("/console",{template:"/console?version="+APP_ENV.CACHEBUSTER,scope:"console"}).add("/console/account",{template:"/console/account?version="+APP_ENV.CACHEBUSTER,scope:"console"}).add("/console/account/:tab",{template:"/console/account?version="+APP_ENV.CACHEBUSTER,scope:"console"}).add("/console/home",{template:"/console/home?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/home/:tab",{template:"/console/home?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/platforms/:platform",{template:function(window){return window.location.pathname+"?version="+APP_ENV.CACHEBUSTER;},scope:"console",project:true}).add("/console/notifications",{template:"/console/notifications?version="+APP_ENV.CACHEBUSTER,scope:"console"}).add("/console/settings",{template:"/console/settings?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/settings/:tab",{template:"/console/settings?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/webhooks",{template:"/console/webhooks?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/webhooks/:tab",{template:"/console/webhooks?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/keys",{template:"/console/keys?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/keys/:tab",{template:"/console/keys?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/tasks",{template:"/console/tasks?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/tasks/:tab",{template:"/console/tasks?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/database",{template:"/console/database?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/database/collection",{template:function(window){return window.location.pathname+window.location.search+'&version='+APP_ENV.CACHEBUSTER;},scope:"console",project:true}).add("/console/database/collection/:tab",{template:function(window){return window.location.pathname+window.location.search+'&version='+APP_ENV.CACHEBUSTER;},scope:"console",project:true}).add("/console/database/document",{template:function(window){return window.location.pathname+window.location.search+'&version='+APP_ENV.CACHEBUSTER;},scope:"console",project:true}).add("/console/database/document/:tab",{template:function(window){return window.location.pathname+window.location.search+'&version='+APP_ENV.CACHEBUSTER;},scope:"console",project:true}).add("/console/storage",{template:"/console/storage?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/storage/:tab",{template:"/console/storage?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/users",{template:"/console/users?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/users/user",{template:"/console/users/user?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/users/user/:tab",{template:"/console/users/user?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/users/teams/team",{template:"/console/users/teams/team?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/users/teams/team/:tab",{template:"/console/users/teams/team?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/users/:tab",{template:"/console/users?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/functions",{template:"/console/functions?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/functions/function",{template:"/console/functions/function?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/functions/function/:tab",{template:"/console/functions/function?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/functions/:tab",{template:"/console/functions?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true});window.ls.filter.add("avatar",function($value,element){if(!$value){return"";} let size=element.dataset["size"]||80;let name=$value.name||$value||"";name=(typeof name!=='string')?'--':name;return def="/v1/avatars/initials?project=console"+"&name="+ encodeURIComponent(name)+"&width="+ size+"&height="+ diff --git a/public/dist/scripts/app.js b/public/dist/scripts/app.js index bc74b1747e..c07e11c881 100644 --- a/public/dist/scripts/app.js +++ b/public/dist/scripts/app.js @@ -120,7 +120,7 @@ container.set(as,container.path(context),true,watch);container.set(key,index,tru element.dispatchEvent(new Event("looped"));};let template=element.children.length===1?element.children[0]:window.document.createElement("li");echo();container.bind(element,expr+".length",echo);let path=(expr+".length").split(".");while(path.length){container.bind(element,path.join("."),echo);path.pop();}},});window.ls.container.get("view").add({selector:"data-ls-template",template:false,controller:function(element,view,http,expression,document,container){let template=element.getAttribute("data-ls-template")||"";let type=element.getAttribute("data-type")||"url";let debug=element.getAttribute("data-debug")||false;let paths=[];let check=function(init=false){let source=expression.parse(template);paths=expression.getPaths();element.innerHTML="";if("script"===type){let inlineTemplate=document.getElementById(source);if(inlineTemplate&&inlineTemplate.innerHTML){element.innerHTML=inlineTemplate.innerHTML;element.dispatchEvent(new CustomEvent("template-loaded",{bubbles:true,cancelable:false,}));}else{if(debug){console.error('Missing template "'+source+'"');}} if(!init){view.render(element);} return;} -http.get(source).then(function(element){return function(data){element.innerHTML=data;view.render(element);element.dispatchEvent(new CustomEvent('template-loaded',{bubbles:true,cancelable:false}));}}(element),function(){throw new Error('Failed loading template');});};check(true);for(let i=0;i{const realtime=window.ls.container.get('realtime');window.ls.container.get('console').subscribe('project',event=>{realtime.set(event.payload);});});(function(window){"use strict";window.ls.container.set('alerts',function(window){return{list:[],ids:0,counter:0,max:5,add:function(message,time){var scope=this;message.id=scope.ids++;message.remove=function(){scope.remove(message.id);};scope.counter++;scope.list.unshift(message);if(scope.counter>scope.max){scope.list.pop();scope.counter--;} +http.get(source).then((function(element){return function(data){element.innerHTML=data;view.render(element);element.dispatchEvent(new CustomEvent("template-loaded",{bubbles:true,cancelable:false,}));};})(element),function(){throw new Error("Failed loading template");});};check(true);for(let i=0;i{const realtime=window.ls.container.get('realtime');window.ls.container.get('console').subscribe('project',event=>{realtime.set(event.payload);});});(function(window){"use strict";window.ls.container.set('alerts',function(window){return{list:[],ids:0,counter:0,max:5,add:function(message,time){var scope=this;message.id=scope.ids++;message.remove=function(){scope.remove(message.id);};scope.counter++;scope.list.unshift(message);if(scope.counter>scope.max){scope.list.pop();scope.counter--;} if(time>0){window.setTimeout(function(message){return function(){scope.remove(message.id)}}(message),time);} return message.id;},remove:function(id){let scope=this;for(let index=0;index0)?'?'+buildQuery(params):''),headers,{});},'post':function(path,headers={},params={},progress=null){return call('POST',path,headers,params,progress);},'put':function(path,headers={},params={},progress=null){return call('PUT',headers,params,progress);},'patch':function(path,headers={},params={},progress=null){return call('PATCH',path,headers,params,progress);},'delete':function(path,headers={},params={},progress=null){return call('DELETE',path,headers,params,progress);},'addGlobalParam':addGlobalParam,'addGlobalHeader':addGlobalHeader}}(window.document);let analytics={create:function(id,source,activity,url){return http.post('/analytics',{'content-type':'application/json'},{id:id,source:source,activity:activity,url:url,version:env.VERSION,setup:env.SETUP});},};return{analytics:analytics,};},true);})(window);(function(window){"use strict";window.ls.container.set('console',function(window){var sdk=new window.Appwrite();sdk.setEndpoint(window.location.protocol+'//'+window.location.host+APP_ENV.API).setProject('console').setLocale(APP_ENV.LOCALE);return sdk;},true);})(window);(function(window){"use strict";window.ls.container.set('date',function(){function format(format,timestamp){var jsdate,f +request.onerror=function(){reject(new Error("Network Error"));};request.send(params);})};return{'get':function(path,headers={},params={}){return call('GET',path+((params.length>0)?'?'+buildQuery(params):''),headers,{});},'post':function(path,headers={},params={},progress=null){return call('POST',path,headers,params,progress);},'put':function(path,headers={},params={},progress=null){return call('PUT',headers,params,progress);},'patch':function(path,headers={},params={},progress=null){return call('PATCH',path,headers,params,progress);},'delete':function(path,headers={},params={},progress=null){return call('DELETE',path,headers,params,progress);},'addGlobalParam':addGlobalParam,'addGlobalHeader':addGlobalHeader}}(window.document);let analytics={create:function(id,source,activity,url){return http.post('/analytics',{'content-type':'application/json'},{id:id,source:source,activity:activity,url:url,version:env.VERSION,setup:env.SETUP});},};return{analytics:analytics,};},true);})(window);(function(window){"use strict";window.ls.container.set('console',function(window){var sdk=new window.Appwrite.Appwrite();sdk.setEndpoint(APP_ENV.ENDPOINT+APP_ENV.API).setProject('console').setLocale(APP_ENV.LOCALE);return sdk;},true);})(window);(function(window){"use strict";window.ls.container.set('date',function(){function format(format,timestamp){var jsdate,f var txtWords=['Sun','Mon','Tues','Wednes','Thurs','Fri','Satur','January','February','March','April','May','June','July','August','September','October','November','December'] var formatChr=/\\?(.?)/gi var formatChrCb=function(t,s){return f[t]?f[t]():s} @@ -264,7 +264,7 @@ return slf.renderToken(tokens,idx,opts);} md.renderer.rules.strong_open=renderEm;md.renderer.rules.strong_close=renderEm;return md;},true);})(window);(function(window){"use strict";window.ls.container.set('rtl',function(){var rtlStock="^ا^ب^ت^ث^ج^ح^خ^د^ذ^ر^ز^س^ش^ص^ض^ط^ظ^ع^غ^ف^ق^ك^ل^م^ن^ه^و^ي^א^ב^ג^ד^ה^ו^ז^ח^ט^י^כ^ך^ל^מ^ם^נ^ן^ס^ע^פ^ף^צ^ץ^ק^ר^ש^ת^";var special=["\n"," "," ","״",'"',"_","'","!","@","#","$","^","&","%","*","(",")","+","=","-","[","]","\\","/","{","}","|",":","<",">","?",",",".","0","1","2","3","4","5","6","7","8","9"];var isRTL=function(value){for(var i=0;i{return{current:null,set:function(currentConnections){var scope=this;scope.current=currentConnections;return scope.current;}};},true,true);})(window);window.ls.router.add("/auth/signin",{template:"/auth/signin?version="+APP_ENV.CACHEBUSTER,scope:"home"}).add("/auth/signup",{template:"/auth/signup?version="+APP_ENV.CACHEBUSTER,scope:"home"}).add("/auth/recovery",{template:"/auth/recovery?version="+APP_ENV.CACHEBUSTER,scope:"home"}).add("/auth/recovery/reset",{template:"/auth/recovery/reset?version="+APP_ENV.CACHEBUSTER,scope:"home"}).add("/auth/confirm",{template:"/auth/confirm?version="+APP_ENV.CACHEBUSTER,scope:"home"}).add("/auth/join",{template:"/auth/join?version="+APP_ENV.CACHEBUSTER,scope:"home"}).add("/auth/oauth2/success",{template:"/auth/oauth2/success?version="+APP_ENV.CACHEBUSTER,scope:"home"}).add("/auth/oauth2/failure",{template:"/auth/oauth2/failure?version="+APP_ENV.CACHEBUSTER,scope:"home"}).add("/console",{template:"/console?version="+APP_ENV.CACHEBUSTER,scope:"console"}).add("/console/account",{template:"/console/account?version="+APP_ENV.CACHEBUSTER,scope:"console"}).add("/console/account/:tab",{template:"/console/account?version="+APP_ENV.CACHEBUSTER,scope:"console"}).add("/console/home",{template:"/console/home?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/home/:tab",{template:"/console/home?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/platforms/:platform",{template:function(window){return window.location.pathname+"?version="+APP_ENV.CACHEBUSTER;},scope:"console",project:true}).add("/console/notifications",{template:"/console/notifications?version="+APP_ENV.CACHEBUSTER,scope:"console"}).add("/console/settings",{template:"/console/settings?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/settings/:tab",{template:"/console/settings?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/webhooks",{template:"/console/webhooks?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/webhooks/:tab",{template:"/console/webhooks?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/keys",{template:"/console/keys?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/keys/:tab",{template:"/console/keys?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/tasks",{template:"/console/tasks?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/tasks/:tab",{template:"/console/tasks?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/database",{template:"/console/database?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/database/collection",{template:function(window){return window.location.pathname+window.location.search+'&version='+APP_ENV.CACHEBUSTER;},scope:"console",project:true}).add("/console/database/collection/:tab",{template:function(window){return window.location.pathname+window.location.search+'&version='+APP_ENV.CACHEBUSTER;},scope:"console",project:true}).add("/console/database/document",{template:function(window){return window.location.pathname+window.location.search+'&version='+APP_ENV.CACHEBUSTER;},scope:"console",project:true}).add("/console/database/document/:tab",{template:function(window){return window.location.pathname+window.location.search+'&version='+APP_ENV.CACHEBUSTER;},scope:"console",project:true}).add("/console/storage",{template:"/console/storage?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/storage/:tab",{template:"/console/storage?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/users",{template:"/console/users?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/users/user",{template:"/console/users/user?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/users/user/:tab",{template:"/console/users/user?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/users/teams/team",{template:"/console/users/teams/team?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/users/teams/team/:tab",{template:"/console/users/teams/team?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/users/:tab",{template:"/console/users?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/functions",{template:"/console/functions?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/functions/function",{template:"/console/functions/function?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/functions/function/:tab",{template:"/console/functions/function?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/functions/:tab",{template:"/console/functions?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true});window.ls.filter.add("avatar",function($value,element){if(!$value){return"";} +return false;};return{isRTL:isRTL,};},true);})(window);(function(window){"use strict";window.ls.container.set('sdk',function(window,router){var sdk=new window.Appwrite.Appwrite();sdk.setEndpoint(APP_ENV.ENDPOINT+APP_ENV.API).setProject(router.params.project||'').setLocale(APP_ENV.LOCALE).setMode('admin');return sdk;},false);})(window);(function(window){"use strict";window.ls.container.set('search',function(window){return{params:{},path:'',pointer:'',selected:'',};},true,true);})(window);(function(window){"use strict";window.ls.container.set('timezone',function(){return{convert:function(unixTime){var timezoneMinutes=new Date().getTimezoneOffset();timezoneMinutes=(timezoneMinutes===0)?0:-timezoneMinutes;return parseInt(unixTime)+(timezoneMinutes*60);}};},true);})(window);(function(window){"use strict";window.ls.container.set('realtime',()=>{return{current:null,set:function(currentConnections){var scope=this;scope.current=currentConnections;return scope.current;}};},true,true);})(window);window.ls.router.add("/auth/signin",{template:"/auth/signin?version="+APP_ENV.CACHEBUSTER,scope:"home"}).add("/auth/signup",{template:"/auth/signup?version="+APP_ENV.CACHEBUSTER,scope:"home"}).add("/auth/recovery",{template:"/auth/recovery?version="+APP_ENV.CACHEBUSTER,scope:"home"}).add("/auth/recovery/reset",{template:"/auth/recovery/reset?version="+APP_ENV.CACHEBUSTER,scope:"home"}).add("/auth/confirm",{template:"/auth/confirm?version="+APP_ENV.CACHEBUSTER,scope:"home"}).add("/auth/join",{template:"/auth/join?version="+APP_ENV.CACHEBUSTER,scope:"home"}).add("/auth/oauth2/success",{template:"/auth/oauth2/success?version="+APP_ENV.CACHEBUSTER,scope:"home"}).add("/auth/oauth2/failure",{template:"/auth/oauth2/failure?version="+APP_ENV.CACHEBUSTER,scope:"home"}).add("/console",{template:"/console?version="+APP_ENV.CACHEBUSTER,scope:"console"}).add("/console/account",{template:"/console/account?version="+APP_ENV.CACHEBUSTER,scope:"console"}).add("/console/account/:tab",{template:"/console/account?version="+APP_ENV.CACHEBUSTER,scope:"console"}).add("/console/home",{template:"/console/home?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/home/:tab",{template:"/console/home?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/platforms/:platform",{template:function(window){return window.location.pathname+"?version="+APP_ENV.CACHEBUSTER;},scope:"console",project:true}).add("/console/notifications",{template:"/console/notifications?version="+APP_ENV.CACHEBUSTER,scope:"console"}).add("/console/settings",{template:"/console/settings?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/settings/:tab",{template:"/console/settings?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/webhooks",{template:"/console/webhooks?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/webhooks/:tab",{template:"/console/webhooks?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/keys",{template:"/console/keys?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/keys/:tab",{template:"/console/keys?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/tasks",{template:"/console/tasks?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/tasks/:tab",{template:"/console/tasks?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/database",{template:"/console/database?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/database/collection",{template:function(window){return window.location.pathname+window.location.search+'&version='+APP_ENV.CACHEBUSTER;},scope:"console",project:true}).add("/console/database/collection/:tab",{template:function(window){return window.location.pathname+window.location.search+'&version='+APP_ENV.CACHEBUSTER;},scope:"console",project:true}).add("/console/database/document",{template:function(window){return window.location.pathname+window.location.search+'&version='+APP_ENV.CACHEBUSTER;},scope:"console",project:true}).add("/console/database/document/:tab",{template:function(window){return window.location.pathname+window.location.search+'&version='+APP_ENV.CACHEBUSTER;},scope:"console",project:true}).add("/console/storage",{template:"/console/storage?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/storage/:tab",{template:"/console/storage?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/users",{template:"/console/users?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/users/user",{template:"/console/users/user?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/users/user/:tab",{template:"/console/users/user?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/users/teams/team",{template:"/console/users/teams/team?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/users/teams/team/:tab",{template:"/console/users/teams/team?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/users/:tab",{template:"/console/users?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/functions",{template:"/console/functions?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/functions/function",{template:"/console/functions/function?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/functions/function/:tab",{template:"/console/functions/function?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/functions/:tab",{template:"/console/functions?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true});window.ls.filter.add("avatar",function($value,element){if(!$value){return"";} let size=element.dataset["size"]||80;let name=$value.name||$value||"";name=(typeof name!=='string')?'--':name;return def="/v1/avatars/initials?project=console"+"&name="+ encodeURIComponent(name)+"&width="+ size+"&height="+ diff --git a/public/scripts/services/console.js b/public/scripts/services/console.js index f693669c0e..1904187c2e 100644 --- a/public/scripts/services/console.js +++ b/public/scripts/services/console.js @@ -5,7 +5,7 @@ var sdk = new window.Appwrite.Appwrite(); sdk - .setEndpoint(window.location.protocol + '//' + window.location.host + APP_ENV.API) + .setEndpoint(APP_ENV.ENDPOINT + APP_ENV.API) .setProject('console') .setLocale(APP_ENV.LOCALE) ; diff --git a/public/scripts/services/sdk.js b/public/scripts/services/sdk.js index 6fe1b25b6a..117c958f80 100644 --- a/public/scripts/services/sdk.js +++ b/public/scripts/services/sdk.js @@ -5,7 +5,7 @@ var sdk = new window.Appwrite.Appwrite(); sdk - .setEndpoint(window.location.protocol + '//' + window.location.host + APP_ENV.API) + .setEndpoint(APP_ENV.ENDPOINT + APP_ENV.API) .setProject(router.params.project || '') .setLocale(APP_ENV.LOCALE) .setMode('admin') From 620980b31695810e3c7ceac2b926fd7ce77a9744 Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Tue, 15 Jun 2021 08:44:06 +0200 Subject: [PATCH 131/267] Update webhooks.php --- app/workers/webhooks.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/workers/webhooks.php b/app/workers/webhooks.php index aab605193a..0debfd39e4 100644 --- a/app/workers/webhooks.php +++ b/app/workers/webhooks.php @@ -8,7 +8,6 @@ require_once __DIR__.'/../workers.php'; Console::title('Webhooks V1 Worker'); Console::success(APP_NAME.' webhooks worker v1 has started'); -use Appwrite\Resque\Worker; class WebhooksV1 extends Worker { @@ -92,4 +91,4 @@ class WebhooksV1 extends Worker public function shutdown(): void { } -} \ No newline at end of file +} From d8bae254383f5acd4d5e0db3d4fde704eed629e8 Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Tue, 15 Jun 2021 09:15:14 +0200 Subject: [PATCH 132/267] fix(realtime): phpdoc and access modifiers --- docker-compose.yml | 38 ++++++++++++++-------------- src/Appwrite/Realtime/Server.php | 43 ++++++++++++++++++-------------- 2 files changed, 43 insertions(+), 38 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 90e6c16d55..17fac88c74 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -550,24 +550,24 @@ services: # - RESQUE_WEB_HTTP_BASIC_AUTH_USER=user # - RESQUE_WEB_HTTP_BASIC_AUTH_PASSWORD=password - # chronograf: - # image: chronograf:1.5 - # container_name: appwrite-chronograf - # restart: unless-stopped - # networks: - # - appwrite - # volumes: - # - appwrite-chronograf:/var/lib/chronograf - # ports: - # - "8888:8888" - # environment: - # - INFLUXDB_URL=http://influxdb:8086 - # - KAPACITOR_URL=http://kapacitor:9092 - # - AUTH_DURATION=48h - # - TOKEN_SECRET=duperduper5674829!jwt - # - GH_CLIENT_ID=d86f7145a41eacfc52cc - # - GH_CLIENT_SECRET=9e0081062367a2134e7f2ea95ba1a32d08b6c8ab - # - GH_ORGS=appwrite + chronograf: + image: chronograf:1.5 + container_name: appwrite-chronograf + restart: unless-stopped + networks: + - appwrite + volumes: + - appwrite-chronograf:/var/lib/chronograf + ports: + - "8888:8888" + environment: + - INFLUXDB_URL=http://influxdb:8086 + - KAPACITOR_URL=http://kapacitor:9092 + - AUTH_DURATION=48h + - TOKEN_SECRET=duperduper5674829!jwt + - GH_CLIENT_ID=d86f7145a41eacfc52cc + - GH_CLIENT_SECRET=9e0081062367a2134e7f2ea95ba1a32d08b6c8ab + - GH_ORGS=appwrite # webgrind: # image: 'jokkedk/webgrind:latest' @@ -591,4 +591,4 @@ volumes: appwrite-functions: appwrite-influxdb: appwrite-config: - # appwrite-chronograf: + appwrite-chronograf: diff --git a/src/Appwrite/Realtime/Server.php b/src/Appwrite/Realtime/Server.php index 0eed3f24b4..d8baec5857 100644 --- a/src/Appwrite/Realtime/Server.php +++ b/src/Appwrite/Realtime/Server.php @@ -55,10 +55,11 @@ class Server /** * This is executed when the Realtime server starts. + * * @param SwooleServer $server * @return void */ - public function onStart(SwooleServer $server): void + private function onStart(SwooleServer $server): void { Console::success('Server started succefully'); Console::info("Master pid {$server->master_pid}, manager pid {$server->manager_pid}"); @@ -101,12 +102,13 @@ class Server /** * This is executed when a WebSocket worker process starts. + * * @param SwooleServer $server * @param int $workerId * @return void * @throws Exception */ - public function onWorkerStart(SwooleServer $server, int $workerId): void + private function onWorkerStart(SwooleServer $server, int $workerId): void { Console::success('Worker ' . $workerId . ' started succefully'); @@ -163,7 +165,7 @@ class Server * @throws Exception * @throws UtopiaException */ - public function onOpen(SwooleServer $server, Request $request): void + private function onOpen(SwooleServer $server, Request $request): void { $app = new App('UTC'); $connection = $request->fd; @@ -284,11 +286,12 @@ class Server /** * This is executed when a message is received by the Realtime server. + * * @param SwooleServer $server * @param Frame $frame * @return void */ - public function onMessage(SwooleServer $server, Frame $frame) + private function onMessage(SwooleServer $server, Frame $frame) { $server->push($frame->fd, 'Sending messages is not allowed.'); $server->close($frame->fd); @@ -296,11 +299,12 @@ class Server /** * This is executed when a Realtime connection is closed. + * * @param SwooleServer $server * @param int $connection * @return void */ - public function onClose(SwooleServer $server, int $connection) + private function onClose(SwooleServer $server, int $connection) { if (array_key_exists($connection, $this->connections)) { $this->stats->decr($this->connections[$connection]['projectId'], 'connectionsTotal'); @@ -311,25 +315,25 @@ class Server /** * This is executed when an event is published on realtime channel in Redis. + * + * Supported Resources: + * - Collection + * - Document + * - File + * - Account + * - Session + * - Team? (not implemented yet) + * - Membership? (not implemented yet) + * - Function + * - Execution + * * @param string $payload * @param SwooleServer $server * @param int $workerId * @return void */ - public function onRedisPublish(string $payload, SwooleServer &$server, int $workerId) + private function onRedisPublish(string $payload, SwooleServer &$server, int $workerId) { - /** - * Supported Resources: - * - Collection - * - Document - * - File - * - Account - * - Session - * - Team? (not implemented yet) - * - Membership? (not implemented yet) - * - Function - * - Execution - */ $event = json_decode($payload, true); $receivers = Parser::identifyReceivers($event, $this->subscriptions); @@ -361,10 +365,11 @@ class Server /** * This sends the usage to the `console` channel. + * * @param SwooleServer $server * @return void */ - public function tickSendProjectUsage(SwooleServer &$server) + private function tickSendProjectUsage(SwooleServer &$server) { if ( array_key_exists('console', $this->subscriptions) From 82b3a56d26a2a23e0403e34b0b1d1ae57bcf2628 Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Tue, 15 Jun 2021 10:41:02 +0200 Subject: [PATCH 133/267] revert(realtime): make access modifiers public again --- src/Appwrite/Realtime/Server.php | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/Appwrite/Realtime/Server.php b/src/Appwrite/Realtime/Server.php index d8baec5857..abc849ec98 100644 --- a/src/Appwrite/Realtime/Server.php +++ b/src/Appwrite/Realtime/Server.php @@ -24,11 +24,11 @@ use Utopia\Swoole\Request as SwooleRequest; class Server { - private Registry $register; - private SwooleServer $server; - private Table $stats; - private array $subscriptions; - private array $connections; + public Registry $register; + public SwooleServer $server; + public Table $stats; + public array $subscriptions; + public array $connections; public function __construct(Registry &$register, $host = '0.0.0.0', $port = 80, $config = []) { @@ -59,7 +59,7 @@ class Server * @param SwooleServer $server * @return void */ - private function onStart(SwooleServer $server): void + public function onStart(SwooleServer $server): void { Console::success('Server started succefully'); Console::info("Master pid {$server->master_pid}, manager pid {$server->manager_pid}"); @@ -108,7 +108,7 @@ class Server * @return void * @throws Exception */ - private function onWorkerStart(SwooleServer $server, int $workerId): void + public function onWorkerStart(SwooleServer $server, int $workerId): void { Console::success('Worker ' . $workerId . ' started succefully'); @@ -165,7 +165,7 @@ class Server * @throws Exception * @throws UtopiaException */ - private function onOpen(SwooleServer $server, Request $request): void + public function onOpen(SwooleServer $server, Request $request): void { $app = new App('UTC'); $connection = $request->fd; @@ -291,7 +291,7 @@ class Server * @param Frame $frame * @return void */ - private function onMessage(SwooleServer $server, Frame $frame) + public function onMessage(SwooleServer $server, Frame $frame) { $server->push($frame->fd, 'Sending messages is not allowed.'); $server->close($frame->fd); @@ -304,7 +304,7 @@ class Server * @param int $connection * @return void */ - private function onClose(SwooleServer $server, int $connection) + public function onClose(SwooleServer $server, int $connection) { if (array_key_exists($connection, $this->connections)) { $this->stats->decr($this->connections[$connection]['projectId'], 'connectionsTotal'); @@ -332,7 +332,7 @@ class Server * @param int $workerId * @return void */ - private function onRedisPublish(string $payload, SwooleServer &$server, int $workerId) + public function onRedisPublish(string $payload, SwooleServer &$server, int $workerId) { $event = json_decode($payload, true); @@ -369,7 +369,7 @@ class Server * @param SwooleServer $server * @return void */ - private function tickSendProjectUsage(SwooleServer &$server) + public function tickSendProjectUsage(SwooleServer &$server) { if ( array_key_exists('console', $this->subscriptions) From 7adcdf916f7d799c7bd407f3d6d346b3b0475649 Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Tue, 15 Jun 2021 11:14:10 +0200 Subject: [PATCH 134/267] fix(realtime): class and phpdocs --- src/Appwrite/Realtime/Server.php | 46 ++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/src/Appwrite/Realtime/Server.php b/src/Appwrite/Realtime/Server.php index abc849ec98..e11fb43323 100644 --- a/src/Appwrite/Realtime/Server.php +++ b/src/Appwrite/Realtime/Server.php @@ -2,10 +2,14 @@ namespace Appwrite\Realtime; +use Appwrite\Database\Database; +use Appwrite\Database\Adapter\MySQL as MySQLAdapter; +use Appwrite\Database\Adapter\Redis as RedisAdapter; use Appwrite\Event\Event; use Appwrite\Network\Validator\Origin; use Appwrite\Utopia\Response; use Exception; +use Swoole\Coroutine\Redis; use Swoole\Http\Request; use Swoole\Http\Response as SwooleResponse; use Swoole\Process; @@ -17,6 +21,7 @@ use Utopia\Abuse\Abuse; use Utopia\Abuse\Adapters\TimeLimit; use Utopia\App; use Utopia\CLI\Console; +use Utopia\Config\Config; use Utopia\Exception as UtopiaException; use Utopia\Registry\Registry; use Utopia\Swoole\Request as SwooleRequest; @@ -24,10 +29,46 @@ use Utopia\Swoole\Request as SwooleRequest; class Server { + /** + * Container scoped Registry. + * @var Registry + */ public Registry $register; + + /** + * Container scoped Swoole Server. + * @var SwooleServer + */ public SwooleServer $server; + + /** + * Container scoped Table. + * @var Table + */ public Table $stats; + + /** + * Container scoped Database connection. + * @var Database + */ + public Database $db; + + /** + * Container scoped Redis connection. + * @var Redis + */ + public Redis $cache; + + /** + * Worker scoped subscription. + * @var array + */ public array $subscriptions; + + /** + * Worker scoped connections. + * @var array + */ public array $connections; public function __construct(Registry &$register, $host = '0.0.0.0', $port = 80, $config = []) @@ -43,6 +84,11 @@ class Server $this->stats->column('messages', Table::TYPE_INT); $this->stats->create(); + $this->db = new Database(); + $this->db->setAdapter(new RedisAdapter(new MySQLAdapter($this->register), $this->register)); + $this->db->setNamespace('app_console'); + $this->db->setMocks(Config::getParam('collections', [])); + $this->server = new SwooleServer($host, $port, SWOOLE_PROCESS); $this->server->set($config); $this->server->on('start', [$this, 'onStart']); From 4847a0b674429eb5c54d1d9aa41063cb1d5d7a90 Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Tue, 15 Jun 2021 17:11:07 +0200 Subject: [PATCH 135/267] feat(realtime): shared usage stats over database --- app/realtime.php | 26 ++++++++ src/Appwrite/Realtime/Server.php | 103 ++++++++++++++++++++++++++----- 2 files changed, 113 insertions(+), 16 deletions(-) diff --git a/app/realtime.php b/app/realtime.php index 6d94b2cc4a..23fb699a35 100644 --- a/app/realtime.php +++ b/app/realtime.php @@ -1,6 +1,8 @@ 64000 // Default maximum Package Size (64kb) ]; +$register->set('db', function () { + $dbHost = App::getEnv('_APP_DB_HOST', ''); + $dbUser = App::getEnv('_APP_DB_USER', ''); + $dbPass = App::getEnv('_APP_DB_PASS', ''); + $dbScheme = App::getEnv('_APP_DB_SCHEMA', ''); + + $pdo = new PDO("mysql:host={$dbHost};dbname={$dbScheme};charset=utf8mb4", $dbUser, $dbPass, array( + PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8mb4', + PDO::ATTR_TIMEOUT => 3, // Seconds + PDO::ATTR_PERSISTENT => true, + PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, + PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, + )); + + return $pdo; +}); +$register->set('cache', function () { // Register cache connection + $redis = new Redis(); + $redis->pconnect(App::getEnv('_APP_REDIS_HOST', ''), App::getEnv('_APP_REDIS_PORT', '')); + $redis->setOption(Redis::OPT_READ_TIMEOUT, -1); + + return $redis; +}); + $realtimeServer = new Server($register, config: $config); diff --git a/src/Appwrite/Realtime/Server.php b/src/Appwrite/Realtime/Server.php index e11fb43323..b397bc066c 100644 --- a/src/Appwrite/Realtime/Server.php +++ b/src/Appwrite/Realtime/Server.php @@ -5,6 +5,7 @@ namespace Appwrite\Realtime; use Appwrite\Database\Database; 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\Network\Validator\Origin; use Appwrite\Utopia\Response; @@ -42,10 +43,10 @@ class Server public SwooleServer $server; /** - * Container scoped Table. + * Container scoped Usage Table. * @var Table */ - public Table $stats; + public Table $usage; /** * Container scoped Database connection. @@ -77,12 +78,12 @@ class Server $this->connections = []; $this->register = $register; - $this->stats = new Table(4096, 1); - $this->stats->column('projectId', Table::TYPE_STRING, 64); - $this->stats->column('connections', Table::TYPE_INT); - $this->stats->column('connectionsTotal', Table::TYPE_INT); - $this->stats->column('messages', Table::TYPE_INT); - $this->stats->create(); + $this->usage = new Table(4096, 1); + $this->usage->column('projectId', Table::TYPE_STRING, 64); + $this->usage->column('connections', Table::TYPE_INT); + $this->usage->column('connectionsTotal', Table::TYPE_INT); + $this->usage->column('messages', Table::TYPE_INT); + $this->usage->create(); $this->db = new Database(); $this->db->setAdapter(new RedisAdapter(new MySQLAdapter($this->register), $this->register)); @@ -96,6 +97,7 @@ class Server $this->server->on('open', [$this, 'onOpen']); $this->server->on('message', [$this, 'onMessage']); $this->server->on('close', [$this, 'onClose']); + $this->server->container_id = uniqid(); $this->server->start(); } @@ -110,9 +112,34 @@ class Server Console::success('Server started succefully'); Console::info("Master pid {$server->master_pid}, manager pid {$server->manager_pid}"); + try { + go(function() { + $document = [ + '$collection' => Database::SYSTEM_COLLECTION_REALTIME_CONNECTIONS, + '$permissions' => [ + 'read' => ['*'], + 'write' => ['*'], + ], + 'container' => $this->server->container_id, + 'timestamp' => time(), + 'data' => '{}' + ]; + Authorization::disable(); + $document = $this->db->createDocument($document); + Authorization::enable(); + $this->server->document_id = $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()); + } + + Timer::tick(10000, function () { /** @var Table $stats */ - foreach ($this->stats as $projectId => $value) { + foreach ($this->usage as $projectId => $value) { if (empty($value['connections']) && empty($value['messages'])) { continue; } @@ -128,7 +155,7 @@ class Server ->setParam('networkRequestSize', 0) ->setParam('networkResponseSize', 0); - $this->stats->set($projectId, [ + $this->usage->set($projectId, [ 'projectId' => $projectId, 'messages' => 0, 'connections' => 0 @@ -140,6 +167,37 @@ class Server } }); + Timer::tick(10000, function () { + $payload = []; + foreach ($this->usage as $projectId => $value) { + if (!empty($value['connectionsTotal'])) { + $payload[$projectId] = $value['connectionsTotal']; + } + } + if (empty($payload)){ + return; + } + $document = [ + '$id' => $this->server->document_id, + '$collection' => Database::SYSTEM_COLLECTION_REALTIME_CONNECTIONS, + '$permissions' => [ + 'read' => ['*'], + 'write' => ['*'], + ], + 'container' => $this->server->container_id, + 'timestamp' => time(), + 'data' => json_encode($payload) + ]; + try { + $document = $this->db->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()); + } + }); + Process::signal(2, function () use ($server) { Console::log('Stop by Ctrl+C'); $server->shutdown(); @@ -302,8 +360,8 @@ class Server $server->push($connection, json_encode($channels)); - $this->stats->incr($project->getId(), 'connections'); - $this->stats->incr($project->getId(), 'connectionsTotal'); + $this->usage->incr($project->getId(), 'connections'); + $this->usage->incr($project->getId(), 'connectionsTotal'); } catch (\Throwable $th) { $response = [ 'code' => $th->getCode(), @@ -353,7 +411,7 @@ class Server public function onClose(SwooleServer $server, int $connection) { if (array_key_exists($connection, $this->connections)) { - $this->stats->decr($this->connections[$connection]['projectId'], 'connectionsTotal'); + $this->usage->decr($this->connections[$connection]['projectId'], 'connectionsTotal'); } Parser::unsubscribe($connection, $this->subscriptions, $this->connections); Console::info('Connection close: ' . $connection); @@ -405,7 +463,7 @@ class Server } } if (($num = count($receivers)) > 0) { - $this->stats->incr($event['project'], 'messages', $num); + $this->usage->incr($event['project'], 'messages', $num); } } @@ -423,8 +481,21 @@ class Server && array_key_exists('project', $this->subscriptions['console']['role:member']) ) { $payload = []; - foreach ($this->stats as $projectId => $value) { - $payload[$projectId] = $value['connectionsTotal']; + $list = $this->db->getCollection([ + 'filters' => [ + '$collection='.Database::SYSTEM_COLLECTION_REALTIME_CONNECTIONS, + 'timestamp>'.(time() - 15) + ], + ]); + + foreach ($list as $document) { + foreach (json_decode($document->getAttribute('data')) as $projectId => $value) { + if (array_key_exists($projectId, $payload)) { + $payload[$projectId] += $value; + } else { + $payload[$projectId] = $value; + } + } } foreach ($this->subscriptions['console']['role:member']['project'] as $connection => $value) { $server->push( From e281ea08c6175201ea951e6d6a8965e9a0c4f4e9 Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Tue, 15 Jun 2021 17:14:48 +0200 Subject: [PATCH 136/267] revert(docker): adding chronograf --- docker-compose.yml | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 17fac88c74..90e6c16d55 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -550,24 +550,24 @@ services: # - RESQUE_WEB_HTTP_BASIC_AUTH_USER=user # - RESQUE_WEB_HTTP_BASIC_AUTH_PASSWORD=password - chronograf: - image: chronograf:1.5 - container_name: appwrite-chronograf - restart: unless-stopped - networks: - - appwrite - volumes: - - appwrite-chronograf:/var/lib/chronograf - ports: - - "8888:8888" - environment: - - INFLUXDB_URL=http://influxdb:8086 - - KAPACITOR_URL=http://kapacitor:9092 - - AUTH_DURATION=48h - - TOKEN_SECRET=duperduper5674829!jwt - - GH_CLIENT_ID=d86f7145a41eacfc52cc - - GH_CLIENT_SECRET=9e0081062367a2134e7f2ea95ba1a32d08b6c8ab - - GH_ORGS=appwrite + # chronograf: + # image: chronograf:1.5 + # container_name: appwrite-chronograf + # restart: unless-stopped + # networks: + # - appwrite + # volumes: + # - appwrite-chronograf:/var/lib/chronograf + # ports: + # - "8888:8888" + # environment: + # - INFLUXDB_URL=http://influxdb:8086 + # - KAPACITOR_URL=http://kapacitor:9092 + # - AUTH_DURATION=48h + # - TOKEN_SECRET=duperduper5674829!jwt + # - GH_CLIENT_ID=d86f7145a41eacfc52cc + # - GH_CLIENT_SECRET=9e0081062367a2134e7f2ea95ba1a32d08b6c8ab + # - GH_ORGS=appwrite # webgrind: # image: 'jokkedk/webgrind:latest' @@ -591,4 +591,4 @@ volumes: appwrite-functions: appwrite-influxdb: appwrite-config: - appwrite-chronograf: + # appwrite-chronograf: From 0acbb6097c1033e2f0a2a3bff26814695187261f Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Wed, 16 Jun 2021 11:09:12 +0200 Subject: [PATCH 137/267] fix(realtime): add port env --- app/realtime.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/realtime.php b/app/realtime.php index 6d94b2cc4a..77c4f9fe2f 100644 --- a/app/realtime.php +++ b/app/realtime.php @@ -1,6 +1,7 @@ 64000 // Default maximum Package Size (64kb) ]; -$realtimeServer = new Server($register, config: $config); +$realtimeServer = new Server($register, port: App::getEnv('PORT', 80), config: $config); From 554d3c355749039230ebc1f15bb31f280ce6db9f Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Wed, 16 Jun 2021 11:35:37 +0200 Subject: [PATCH 138/267] feat(maintenance): add realtime usage stats --- app/init.php | 1 + app/tasks/maintenance.php | 9 +++++++++ app/workers/deletes.php | 17 +++++++++++++++++ 3 files changed, 27 insertions(+) diff --git a/app/init.php b/app/init.php index cb87697a83..ad1a9bd3a6 100644 --- a/app/init.php +++ b/app/init.php @@ -69,6 +69,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'; // Auth Types const APP_AUTH_TYPE_SESSION = 'Session'; const APP_AUTH_TYPE_JWT = 'JWT'; diff --git a/app/tasks/maintenance.php b/app/tasks/maintenance.php index eccdf61b17..a4db6ac4ec 100644 --- a/app/tasks/maintenance.php +++ b/app/tasks/maintenance.php @@ -39,6 +39,14 @@ $cli ]); } + function notifyDeleteRealtimeUsage() + { + 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); + notifyDeleteRealtimeUsage(); }, $interval); }); \ No newline at end of file diff --git a/app/workers/deletes.php b/app/workers/deletes.php index 63551e90f9..0080d35f36 100644 --- a/app/workers/deletes.php +++ b/app/workers/deletes.php @@ -69,6 +69,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); @@ -197,6 +201,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); From 6ebf6bd1556dea000cebce3e46cd93827e1a2620 Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Wed, 16 Jun 2021 19:43:06 +0200 Subject: [PATCH 139/267] feat(realtime): team events and permission validation --- app/controllers/api/teams.php | 8 ++- app/controllers/shared/api.php | 1 + src/Appwrite/Event/Realtime.php | 41 ++++++++++++ src/Appwrite/Realtime/Parser.php | 1 + src/Appwrite/Realtime/Server.php | 61 ++++++++++++++---- tests/e2e/Services/Realtime/RealtimeBase.php | 65 ++++++++++++++++++++ 6 files changed, 163 insertions(+), 14 deletions(-) diff --git a/app/controllers/api/teams.php b/app/controllers/api/teams.php index 3b3039a0fd..40e1bd4c60 100644 --- a/app/controllers/api/teams.php +++ b/app/controllers/api/teams.php @@ -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) diff --git a/app/controllers/shared/api.php b/app/controllers/shared/api.php index 5a1eeda8de..57ffe867de 100644 --- a/app/controllers/shared/api.php +++ b/app/controllers/shared/api.php @@ -201,6 +201,7 @@ App::shutdown(function ($utopia, $request, $response, $project, $events, $audits if ($project->getId() !== 'console') { $realtime ->setEvent($events->getParam('event')) + ->setUserId($events->getParam('userId')) ->setProject($project->getId()) ->setPayload($response->getPayload()) ->trigger(); diff --git a/src/Appwrite/Event/Realtime.php b/src/Appwrite/Event/Realtime.php index fb722be54d..28eb2f7c34 100644 --- a/src/Appwrite/Event/Realtime.php +++ b/src/Appwrite/Event/Realtime.php @@ -17,6 +17,11 @@ class Realtime */ protected $event = ''; + /** + * @var string + */ + protected $userId = ''; + /** * @var array */ @@ -27,6 +32,11 @@ class Realtime */ protected $permissions = []; + /** + * @var false + */ + protected $permissionsChanged = false; + /** * @var Document */ @@ -57,6 +67,16 @@ class Realtime return $this; } + /** + * @param string $userId + * return $this + */ + public function setUserId(string $userId): self + { + $this->userId = $userId; + return $this; + } + /** * @return string */ @@ -120,6 +140,25 @@ class Realtime $this->channels[] = 'account.' . $this->payload->getId(); $this->permissions = ['user:' . $this->payload->getId()]; + break; + case strpos($this->event, 'teams.memberships') === 0: + $this->channels[] = 'memberships'; + $this->channels[] = 'memberships.' . $this->payload->getId(); + $this->permissions = ['team:' . $this->payload->getAttribute('teamId')]; + + break; + case strpos($this->event, 'teams.create') === 0: + $this->permissionsChanged = true; + $this->channels[] = 'teams'; + $this->channels[] = 'teams.' . $this->payload->getId(); + $this->permissions = ['user:' . $this->userId]; + + break; + case strpos($this->event, 'teams.') === 0: + $this->channels[] = 'teams'; + $this->channels[] = 'teams.' . $this->payload->getId(); + $this->permissions = ['team:' . $this->payload->getId()]; + break; case strpos($this->event, 'database.collections.') === 0: $this->channels[] = 'collections'; @@ -166,6 +205,8 @@ class Realtime $redis->publish('realtime', json_encode([ 'project' => $this->project, 'permissions' => $this->permissions, + 'permissionsChanged' => $this->permissionsChanged, + 'userId' => $this->userId, 'data' => [ 'event' => $this->event, 'channels' => $this->channels, diff --git a/src/Appwrite/Realtime/Parser.php b/src/Appwrite/Realtime/Parser.php index f99e7bfbe9..8a5fd1bfcf 100644 --- a/src/Appwrite/Realtime/Parser.php +++ b/src/Appwrite/Realtime/Parser.php @@ -163,6 +163,7 @@ class Parser $connections[$connection] = [ 'projectId' => $projectId, 'roles' => $roles, + 'channels' => $channels ]; } diff --git a/src/Appwrite/Realtime/Server.php b/src/Appwrite/Realtime/Server.php index 0eed3f24b4..e9e79913ec 100644 --- a/src/Appwrite/Realtime/Server.php +++ b/src/Appwrite/Realtime/Server.php @@ -2,6 +2,9 @@ namespace Appwrite\Realtime; +use Appwrite\Database\Database; +use Appwrite\Database\Adapter\MySQL as MySQLAdapter; +use Appwrite\Database\Adapter\Redis as RedisAdapter; use Appwrite\Event\Event; use Appwrite\Network\Validator\Origin; use Appwrite\Utopia\Response; @@ -17,6 +20,7 @@ use Utopia\Abuse\Abuse; use Utopia\Abuse\Adapters\TimeLimit; use Utopia\App; use Utopia\CLI\Console; +use Utopia\Config\Config; use Utopia\Exception as UtopiaException; use Utopia\Registry\Registry; use Utopia\Swoole\Request as SwooleRequest; @@ -176,7 +180,7 @@ class Server return $db; }); - $this->register->set('cache', function () use (&$redis) { // Register cache connection + $this->register->set('cache', function () use (&$redis) { return $redis; }); @@ -318,20 +322,12 @@ class Server */ public function onRedisPublish(string $payload, SwooleServer &$server, int $workerId) { - /** - * Supported Resources: - * - Collection - * - Document - * - File - * - Account - * - Session - * - Team? (not implemented yet) - * - Membership? (not implemented yet) - * - Function - * - Execution - */ $event = json_decode($payload, true); + if ($event['permissionsChanged'] && $event['userId']) { + $this->addPermission($event); + } + $receivers = Parser::identifyReceivers($event, $this->subscriptions); // Temporarily print debug logs by default for Alpha testing. @@ -390,4 +386,43 @@ class Server } } } + + private function addPermission(array $event) + { + $project = $event['project']; + $userId = $event['userId']; + + if (array_key_exists($project, $this->subscriptions) && array_key_exists('user:'.$userId, $this->subscriptions[$project])) { + $connection = array_key_first(reset($this->subscriptions[$project]['user:'.$userId])); + } else { + return; + } + + /** + * This is redundant soon and will be gone with merging the usage branch. + */ + $db = $this->register->get('dbPool')->get(); + $redis = $this->register->get('redisPool')->get(); + + $this->register->set('db', function () use (&$db) { + return $db; + }); + + $this->register->set('cache', function () use (&$redis) { + return $redis; + }); + + $projectDB = new Database(); + $projectDB->setAdapter(new RedisAdapter(new MySQLAdapter($this->register), $this->register)); + $projectDB->setNamespace('app_'.$project); + $projectDB->setMocks(Config::getParam('collections', [])); + + $user = $projectDB->getDocument($userId); + + Parser::setUser($user); + + $roles = Parser::getRoles(); + + Parser::subscribe($project, $connection, $roles, $this->subscriptions, $this->connections, $this->connections[$connection]['channels']); + } } diff --git a/tests/e2e/Services/Realtime/RealtimeBase.php b/tests/e2e/Services/Realtime/RealtimeBase.php index 164bb025c8..305345c11f 100644 --- a/tests/e2e/Services/Realtime/RealtimeBase.php +++ b/tests/e2e/Services/Realtime/RealtimeBase.php @@ -686,4 +686,69 @@ trait RealtimeBase $client->close(); } + + public function testChannelTeams() + { + $user = $this->getUser(); + $session = $user['session'] ?? ''; + $projectId = $this->getProject()['$id']; + + $client = $this->getWebsocket(['teams'], [ + 'origin' => 'http://localhost', + 'cookie' => 'a_session_'.$projectId.'=' . $session + ]); + + $response = json_decode($client->receive(), true); + + $this->assertCount(1, $response); + $this->assertArrayHasKey('teams', $response); + + /** + * Test Team Create + */ + $team = $this->client->call(Client::METHOD_POST, '/teams', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $projectId, + ], $this->getHeaders()), [ + 'name' => 'Arsenal' + ]); + + $teamId = $team['body']['$id'] ?? ''; + + $this->assertEquals(201, $team['headers']['status-code']); + $this->assertNotEmpty($team['body']['$id']); + + $response = json_decode($client->receive(), true); + + $this->assertArrayHasKey('timestamp', $response); + $this->assertCount(2, $response['channels']); + $this->assertContains('teams', $response['channels']); + $this->assertContains('teams.' . $teamId, $response['channels']); + $this->assertEquals('teams.create', $response['event']); + $this->assertNotEmpty($response['payload']); + + /** + * Test Team Update + */ + $team = $this->client->call(Client::METHOD_PUT, '/teams/'.$teamId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $projectId, + ], $this->getHeaders()), [ + 'name' => 'Manchester' + ]); + + $this->assertEquals($team['headers']['status-code'], 200); + $this->assertNotEmpty($team['body']['$id']); + + $response = json_decode($client->receive(), true); + + $this->assertArrayHasKey('timestamp', $response); + $this->assertCount(2, $response['channels']); + $this->assertContains('teams', $response['channels']); + $this->assertContains('teams.' . $teamId, $response['channels']); + $this->assertEquals('teams.update', $response['event']); + $this->assertNotEmpty($response['payload']); + + $client->close(); + } } From 43036a9ba67bf1f64cb83e865d6c3e6604d9cba2 Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Thu, 17 Jun 2021 11:37:52 +0200 Subject: [PATCH 140/267] feat(realtime): add membership events --- src/Appwrite/Event/Realtime.php | 11 ++-- src/Appwrite/Realtime/Server.php | 2 +- tests/e2e/Services/Realtime/RealtimeBase.php | 55 +++++++++++++++++++- 3 files changed, 58 insertions(+), 10 deletions(-) diff --git a/src/Appwrite/Event/Realtime.php b/src/Appwrite/Event/Realtime.php index 28eb2f7c34..5942064326 100644 --- a/src/Appwrite/Event/Realtime.php +++ b/src/Appwrite/Event/Realtime.php @@ -142,19 +142,14 @@ class Realtime break; case strpos($this->event, 'teams.memberships') === 0: + $this->permissionsChanged = in_array($this->event, ['teams.memberships.update', 'teams.memberships.delete', 'teams.memberships.update.status']); $this->channels[] = 'memberships'; $this->channels[] = 'memberships.' . $this->payload->getId(); $this->permissions = ['team:' . $this->payload->getAttribute('teamId')]; - break; - case strpos($this->event, 'teams.create') === 0: - $this->permissionsChanged = true; - $this->channels[] = 'teams'; - $this->channels[] = 'teams.' . $this->payload->getId(); - $this->permissions = ['user:' . $this->userId]; - break; case strpos($this->event, 'teams.') === 0: + $this->permissionsChanged = $this->event === 'teams.create'; $this->channels[] = 'teams'; $this->channels[] = 'teams.' . $this->payload->getId(); $this->permissions = ['team:' . $this->payload->getId()]; @@ -187,7 +182,7 @@ class Realtime $this->permissions = $this->payload->getAttribute('$permissions.read'); } break; - } + } } /** diff --git a/src/Appwrite/Realtime/Server.php b/src/Appwrite/Realtime/Server.php index e9e79913ec..4351e22ce2 100644 --- a/src/Appwrite/Realtime/Server.php +++ b/src/Appwrite/Realtime/Server.php @@ -324,7 +324,7 @@ class Server { $event = json_decode($payload, true); - if ($event['permissionsChanged'] && $event['userId']) { + if ($event['permissionsChanged'] && isset($event['userId'])) { $this->addPermission($event); } diff --git a/tests/e2e/Services/Realtime/RealtimeBase.php b/tests/e2e/Services/Realtime/RealtimeBase.php index 305345c11f..c17f90b934 100644 --- a/tests/e2e/Services/Realtime/RealtimeBase.php +++ b/tests/e2e/Services/Realtime/RealtimeBase.php @@ -687,7 +687,7 @@ trait RealtimeBase $client->close(); } - public function testChannelTeams() + public function testChannelTeams(): array { $user = $this->getUser(); $session = $user['session'] ?? ''; @@ -750,5 +750,58 @@ trait RealtimeBase $this->assertNotEmpty($response['payload']); $client->close(); + + return ['teamId' => $teamId]; + } + + /** + * @depends testChannelTeams + */ + public function testChannelMemberships(array $data) + { + $teamId = $data['teamId'] ?? ''; + + $user = $this->getUser(); + $session = $user['session'] ?? ''; + $projectId = $this->getProject()['$id']; + + $client = $this->getWebsocket(['memberships'], [ + 'origin' => 'http://localhost', + 'cookie' => 'a_session_'.$projectId.'='.$session + ]); + + $response = json_decode($client->receive(), true); + + $this->assertCount(1, $response); + $this->assertArrayHasKey('memberships', $response); + + $response = $this->client->call(Client::METHOD_GET, '/teams/'.$teamId.'/memberships', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $membershipId = $response['body']['memberships'][0]['$id']; + + /** + * Test Update Membership + */ + $roles = ['admin', 'editor', 'uncle']; + $this->client->call(Client::METHOD_PATCH, '/teams/'.$teamId.'/memberships/'.$membershipId, array_merge([ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'roles' => $roles + ]); + + $response = json_decode($client->receive(), true); + $this->assertArrayHasKey('timestamp', $response); + $this->assertCount(2, $response['channels']); + $this->assertContains('memberships', $response['channels']); + $this->assertContains('memberships.' . $membershipId, $response['channels']); + $this->assertEquals('teams.memberships.update', $response['event']); + $this->assertNotEmpty($response['payload']); + + $client->close(); } } From ee0c9e5b8177afaf355c6bc837228885c09f37c3 Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Thu, 17 Jun 2021 14:11:48 +0200 Subject: [PATCH 141/267] remove unnecessary db instances --- src/Appwrite/Realtime/Server.php | 47 ++++++++++++++------------------ 1 file changed, 20 insertions(+), 27 deletions(-) diff --git a/src/Appwrite/Realtime/Server.php b/src/Appwrite/Realtime/Server.php index 22a0b27122..64460e7ad7 100644 --- a/src/Appwrite/Realtime/Server.php +++ b/src/Appwrite/Realtime/Server.php @@ -52,7 +52,13 @@ class Server * Container scoped Database connection. * @var Database */ - public Database $db; + public Database $consoleDb; + + /** + * Container scoped Database connection. + * @var Database + */ + public Database $projectDb; /** * Container scoped Redis connection. @@ -85,10 +91,14 @@ class Server $this->usage->column('messages', Table::TYPE_INT); $this->usage->create(); - $this->db = new Database(); - $this->db->setAdapter(new RedisAdapter(new MySQLAdapter($this->register), $this->register)); - $this->db->setNamespace('app_console'); - $this->db->setMocks(Config::getParam('collections', [])); + $this->consoleDb = new Database(); + $this->consoleDb->setAdapter(new RedisAdapter(new MySQLAdapter($this->register), $this->register)); + $this->consoleDb->setNamespace('app_console'); + $this->consoleDb->setMocks(Config::getParam('collections', [])); + + $this->projectDb = new Database(); + $this->projectDb->setAdapter(new RedisAdapter(new MySQLAdapter($this->register), $this->register)); + $this->projectDb->setMocks(Config::getParam('collections', [])); $this->server = new SwooleServer($host, $port, SWOOLE_PROCESS); $this->server->set($config); @@ -125,7 +135,7 @@ class Server 'data' => '{}' ]; Authorization::disable(); - $document = $this->db->createDocument($document); + $document = $this->consoleDb->createDocument($document); Authorization::enable(); $this->server->document_id = $document->getId(); }); @@ -189,7 +199,7 @@ class Server 'data' => json_encode($payload) ]; try { - $document = $this->db->updateDocument($document); + $document = $this->consoleDb->updateDocument($document); } catch (\Throwable $th) { Console::error('[Error] Type: '.get_class($th)); Console::error('[Error] Message: '.$th->getMessage()); @@ -485,7 +495,7 @@ class Server && array_key_exists('project', $this->subscriptions['console']['role:member']) ) { $payload = []; - $list = $this->db->getCollection([ + $list = $this->consoleDb->getCollection([ 'filters' => [ '$collection='.Database::SYSTEM_COLLECTION_REALTIME_CONNECTIONS, 'timestamp>'.(time() - 15) @@ -528,26 +538,9 @@ class Server return; } - /** - * This is redundant soon and will be gone with merging the usage branch. - */ - $db = $this->register->get('dbPool')->get(); - $redis = $this->register->get('redisPool')->get(); + $this->projectDb->setNamespace('app_'.$project); - $this->register->set('db', function () use (&$db) { - return $db; - }); - - $this->register->set('cache', function () use (&$redis) { - return $redis; - }); - - $projectDB = new Database(); - $projectDB->setAdapter(new RedisAdapter(new MySQLAdapter($this->register), $this->register)); - $projectDB->setNamespace('app_'.$project); - $projectDB->setMocks(Config::getParam('collections', [])); - - $user = $projectDB->getDocument($userId); + $user = $this->projectDb->getDocument($userId); Parser::setUser($user); From 05b7b3da24d1d621625e348ab8d9aa2d04ebd615 Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Fri, 18 Jun 2021 10:42:47 +0200 Subject: [PATCH 142/267] chore(composer): update lock file --- composer.lock | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/composer.lock b/composer.lock index 03f829151a..57c562ea8d 100644 --- a/composer.lock +++ b/composer.lock @@ -4823,16 +4823,16 @@ }, { "name": "sebastian/type", - "version": "2.3.2", + "version": "2.3.4", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/type.git", - "reference": "0d1c587401514d17e8f9258a27e23527cb1b06c1" + "reference": "b8cd8a1c753c90bc1a0f5372170e3e489136f914" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/0d1c587401514d17e8f9258a27e23527cb1b06c1", - "reference": "0d1c587401514d17e8f9258a27e23527cb1b06c1", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/b8cd8a1c753c90bc1a0f5372170e3e489136f914", + "reference": "b8cd8a1c753c90bc1a0f5372170e3e489136f914", "shasum": "" }, "require": { @@ -4867,7 +4867,7 @@ "homepage": "https://github.com/sebastianbergmann/type", "support": { "issues": "https://github.com/sebastianbergmann/type/issues", - "source": "https://github.com/sebastianbergmann/type/tree/2.3.2" + "source": "https://github.com/sebastianbergmann/type/tree/2.3.4" }, "funding": [ { @@ -4875,7 +4875,7 @@ "type": "github" } ], - "time": "2021-06-04T13:02:07+00:00" + "time": "2021-06-15T12:49:02+00:00" }, { "name": "sebastian/version", @@ -4984,16 +4984,16 @@ }, { "name": "symfony/console", - "version": "v5.3.0", + "version": "v5.3.2", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "058553870f7809087fa80fa734704a21b9bcaeb2" + "reference": "649730483885ff2ca99ca0560ef0e5f6b03f2ac1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/058553870f7809087fa80fa734704a21b9bcaeb2", - "reference": "058553870f7809087fa80fa734704a21b9bcaeb2", + "url": "https://api.github.com/repos/symfony/console/zipball/649730483885ff2ca99ca0560ef0e5f6b03f2ac1", + "reference": "649730483885ff2ca99ca0560ef0e5f6b03f2ac1", "shasum": "" }, "require": { @@ -5062,7 +5062,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v5.3.0" + "source": "https://github.com/symfony/console/tree/v5.3.2" }, "funding": [ { @@ -5078,7 +5078,7 @@ "type": "tidelift" } ], - "time": "2021-05-26T17:43:10+00:00" + "time": "2021-06-12T09:42:48+00:00" }, { "name": "symfony/deprecation-contracts", @@ -5635,16 +5635,16 @@ }, { "name": "symfony/string", - "version": "v5.3.0", + "version": "v5.3.2", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "a9a0f8b6aafc5d2d1c116dcccd1573a95153515b" + "reference": "0732e97e41c0a590f77e231afc16a327375d50b0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/a9a0f8b6aafc5d2d1c116dcccd1573a95153515b", - "reference": "a9a0f8b6aafc5d2d1c116dcccd1573a95153515b", + "url": "https://api.github.com/repos/symfony/string/zipball/0732e97e41c0a590f77e231afc16a327375d50b0", + "reference": "0732e97e41c0a590f77e231afc16a327375d50b0", "shasum": "" }, "require": { @@ -5698,7 +5698,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v5.3.0" + "source": "https://github.com/symfony/string/tree/v5.3.2" }, "funding": [ { @@ -5714,7 +5714,7 @@ "type": "tidelift" } ], - "time": "2021-05-26T17:43:10+00:00" + "time": "2021-06-06T09:51:56+00:00" }, { "name": "textalk/websocket", From a3cc8c9ad23110a42f32234436499d6c86e057f4 Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Fri, 18 Jun 2021 12:00:27 +0200 Subject: [PATCH 143/267] adapt to review --- app/config/collections.php | 14 +++++++------- app/tasks/maintenance.php | 4 ++-- src/Appwrite/Database/Database.php | 2 +- src/Appwrite/Realtime/Server.php | 14 ++++++++------ 4 files changed, 18 insertions(+), 16 deletions(-) diff --git a/app/config/collections.php b/app/config/collections.php index d1d6230798..62f6a41bb5 100644 --- a/app/config/collections.php +++ b/app/config/collections.php @@ -1698,15 +1698,15 @@ $collections = [ ], ], ], - Database::SYSTEM_COLLECTION_REALTIME_CONNECTIONS => [ + Database::SYSTEM_COLLECTION_CONNECTIONS => [ '$collection' => Database::SYSTEM_COLLECTION_COLLECTIONS, - '$id' => Database::SYSTEM_COLLECTION_REALTIME_CONNECTIONS, + '$id' => Database::SYSTEM_COLLECTION_CONNECTIONS, '$permissions' => ['read' => ['*']], 'name' => 'Realtime Connections', 'structure' => true, 'rules' => [ [ - '$collection' => Database::SYSTEM_COLLECTION_REALTIME_CONNECTIONS, + '$collection' => Database::SYSTEM_COLLECTION_CONNECTIONS, 'label' => 'Container', 'key' => 'container', 'type' => Database::SYSTEM_VAR_TYPE_TEXT, @@ -1714,7 +1714,7 @@ $collections = [ 'array' => false, ], [ - '$collection' => Database::SYSTEM_COLLECTION_REALTIME_CONNECTIONS, + '$collection' => Database::SYSTEM_COLLECTION_CONNECTIONS, 'label' => 'Timestamp', 'key' => 'timestamp', 'type' => Database::SYSTEM_VAR_TYPE_NUMERIC, @@ -1722,9 +1722,9 @@ $collections = [ 'array' => false, ], [ - '$collection' => Database::SYSTEM_COLLECTION_REALTIME_CONNECTIONS, - 'label' => 'Data', - 'key' => 'data', + '$collection' => Database::SYSTEM_COLLECTION_CONNECTIONS, + 'label' => 'Value', + 'key' => 'value', 'type' => Database::SYSTEM_VAR_TYPE_TEXT, 'required' => true, 'array' => false, diff --git a/app/tasks/maintenance.php b/app/tasks/maintenance.php index a4db6ac4ec..e36c980831 100644 --- a/app/tasks/maintenance.php +++ b/app/tasks/maintenance.php @@ -39,7 +39,7 @@ $cli ]); } - function notifyDeleteRealtimeUsage() + function notifyDeleteConnections() { Resque::enqueue(Event::DELETE_QUEUE_NAME, Event::DELETE_CLASS_NAME, [ 'type' => DELETE_TYPE_REALTIME, @@ -59,6 +59,6 @@ $cli notifyDeleteExecutionLogs($executionLogsRetention); notifyDeleteAbuseLogs($abuseLogsRetention); notifyDeleteAuditLogs($auditLogRetention); - notifyDeleteRealtimeUsage(); + notifyDeleteConnections(); }, $interval); }); \ No newline at end of file diff --git a/src/Appwrite/Database/Database.php b/src/Appwrite/Database/Database.php index f0fbfc2efd..09a66ee72d 100644 --- a/src/Appwrite/Database/Database.php +++ b/src/Appwrite/Database/Database.php @@ -43,7 +43,7 @@ class Database const SYSTEM_COLLECTION_EXECUTIONS = 'executions'; // Realtime - const SYSTEM_COLLECTION_REALTIME_CONNECTIONS = 'realtimeConnections'; + const SYSTEM_COLLECTION_CONNECTIONS = 'connections'; // Var Types const SYSTEM_VAR_TYPE_TEXT = 'text'; diff --git a/src/Appwrite/Realtime/Server.php b/src/Appwrite/Realtime/Server.php index 64460e7ad7..4fd7623a87 100644 --- a/src/Appwrite/Realtime/Server.php +++ b/src/Appwrite/Realtime/Server.php @@ -125,14 +125,14 @@ class Server try { go(function() { $document = [ - '$collection' => Database::SYSTEM_COLLECTION_REALTIME_CONNECTIONS, + '$collection' => Database::SYSTEM_COLLECTION_CONNECTIONS, '$permissions' => [ 'read' => ['*'], 'write' => ['*'], ], 'container' => $this->server->container_id, 'timestamp' => time(), - 'data' => '{}' + 'value' => '{}' ]; Authorization::disable(); $document = $this->consoleDb->createDocument($document); @@ -147,6 +147,7 @@ class Server } + // Run ever 10 seconds Timer::tick(10000, function () { /** @var Table $stats */ foreach ($this->usage as $projectId => $value) { @@ -177,6 +178,7 @@ class Server } }); + // Run ever 10 seconds Timer::tick(10000, function () { $payload = []; foreach ($this->usage as $projectId => $value) { @@ -189,14 +191,14 @@ class Server } $document = [ '$id' => $this->server->document_id, - '$collection' => Database::SYSTEM_COLLECTION_REALTIME_CONNECTIONS, + '$collection' => Database::SYSTEM_COLLECTION_CONNECTIONS, '$permissions' => [ 'read' => ['*'], 'write' => ['*'], ], 'container' => $this->server->container_id, 'timestamp' => time(), - 'data' => json_encode($payload) + 'value' => json_encode($payload) ]; try { $document = $this->consoleDb->updateDocument($document); @@ -497,13 +499,13 @@ class Server $payload = []; $list = $this->consoleDb->getCollection([ 'filters' => [ - '$collection='.Database::SYSTEM_COLLECTION_REALTIME_CONNECTIONS, + '$collection='.Database::SYSTEM_COLLECTION_CONNECTIONS, 'timestamp>'.(time() - 15) ], ]); foreach ($list as $document) { - foreach (json_decode($document->getAttribute('data')) as $projectId => $value) { + foreach (json_decode($document->getAttribute('value')) as $projectId => $value) { if (array_key_exists($projectId, $payload)) { $payload[$projectId] += $value; } else { From 6bb0c4adf4762b310783e106d1c5cda08f2c05a8 Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Fri, 18 Jun 2021 13:16:30 +0200 Subject: [PATCH 144/267] fix dists files for js --- public/dist/scripts/app-all.js | 4 ++-- public/dist/scripts/app.js | 4 ++-- public/scripts/services/console.js | 2 +- public/scripts/services/sdk.js | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/public/dist/scripts/app-all.js b/public/dist/scripts/app-all.js index d287f8c5b6..2767d626bf 100644 --- a/public/dist/scripts/app-all.js +++ b/public/dist/scripts/app-all.js @@ -2105,7 +2105,7 @@ params=formData;break;} return new Promise(function(resolve,reject){let request=new XMLHttpRequest(),key;request.withCredentials=true;request.open(method,path,true);for(key in headers){if(headers.hasOwnProperty(key)){request.setRequestHeader(key,headers[key]);}} request.onload=function(){if(4===request.readyState&&399>=request.status){let data=request.response;let contentType=this.getResponseHeader('content-type');contentType=contentType.substring(0,contentType.indexOf(';'));switch(contentType){case'application/json':data=JSON.parse(data);break;} resolve(data);}else{reject(new Error(request.statusText));}};if(progress){request.addEventListener('progress',progress);request.upload.addEventListener('progress',progress,false);} -request.onerror=function(){reject(new Error("Network Error"));};request.send(params);})};return{'get':function(path,headers={},params={}){return call('GET',path+((params.length>0)?'?'+buildQuery(params):''),headers,{});},'post':function(path,headers={},params={},progress=null){return call('POST',path,headers,params,progress);},'put':function(path,headers={},params={},progress=null){return call('PUT',headers,params,progress);},'patch':function(path,headers={},params={},progress=null){return call('PATCH',path,headers,params,progress);},'delete':function(path,headers={},params={},progress=null){return call('DELETE',path,headers,params,progress);},'addGlobalParam':addGlobalParam,'addGlobalHeader':addGlobalHeader}}(window.document);let analytics={create:function(id,source,activity,url){return http.post('/analytics',{'content-type':'application/json'},{id:id,source:source,activity:activity,url:url,version:env.VERSION,setup:env.SETUP});},};return{analytics:analytics,};},true);})(window);(function(window){"use strict";window.ls.container.set('console',function(window){var sdk=new window.Appwrite.Appwrite();sdk.setEndpoint(APP_ENV.ENDPOINT+APP_ENV.API).setProject('console').setLocale(APP_ENV.LOCALE);return sdk;},true);})(window);(function(window){"use strict";window.ls.container.set('date',function(){function format(format,timestamp){var jsdate,f +request.onerror=function(){reject(new Error("Network Error"));};request.send(params);})};return{'get':function(path,headers={},params={}){return call('GET',path+((params.length>0)?'?'+buildQuery(params):''),headers,{});},'post':function(path,headers={},params={},progress=null){return call('POST',path,headers,params,progress);},'put':function(path,headers={},params={},progress=null){return call('PUT',headers,params,progress);},'patch':function(path,headers={},params={},progress=null){return call('PATCH',path,headers,params,progress);},'delete':function(path,headers={},params={},progress=null){return call('DELETE',path,headers,params,progress);},'addGlobalParam':addGlobalParam,'addGlobalHeader':addGlobalHeader}}(window.document);let analytics={create:function(id,source,activity,url){return http.post('/analytics',{'content-type':'application/json'},{id:id,source:source,activity:activity,url:url,version:env.VERSION,setup:env.SETUP});},};return{analytics:analytics,};},true);})(window);(function(window){"use strict";window.ls.container.set('console',function(window){var sdk=new window.Appwrite();sdk.setEndpoint(APP_ENV.ENDPOINT+APP_ENV.API).setProject('console').setLocale(APP_ENV.LOCALE);return sdk;},true);})(window);(function(window){"use strict";window.ls.container.set('date',function(){function format(format,timestamp){var jsdate,f var txtWords=['Sun','Mon','Tues','Wednes','Thurs','Fri','Satur','January','February','March','April','May','June','July','August','September','October','November','December'] var formatChr=/\\?(.?)/gi var formatChrCb=function(t,s){return f[t]?f[t]():s} @@ -2234,7 +2234,7 @@ return slf.renderToken(tokens,idx,opts);} md.renderer.rules.strong_open=renderEm;md.renderer.rules.strong_close=renderEm;return md;},true);})(window);(function(window){"use strict";window.ls.container.set('rtl',function(){var rtlStock="^ا^ب^ت^ث^ج^ح^خ^د^ذ^ر^ز^س^ش^ص^ض^ط^ظ^ع^غ^ف^ق^ك^ل^م^ن^ه^و^ي^א^ב^ג^ד^ה^ו^ז^ח^ט^י^כ^ך^ל^מ^ם^נ^ן^ס^ע^פ^ף^צ^ץ^ק^ר^ש^ת^";var special=["\n"," "," ","״",'"',"_","'","!","@","#","$","^","&","%","*","(",")","+","=","-","[","]","\\","/","{","}","|",":","<",">","?",",",".","0","1","2","3","4","5","6","7","8","9"];var isRTL=function(value){for(var i=0;i{return{current:null,set:function(currentConnections){var scope=this;scope.current=currentConnections;return scope.current;}};},true,true);})(window);window.ls.router.add("/auth/signin",{template:"/auth/signin?version="+APP_ENV.CACHEBUSTER,scope:"home"}).add("/auth/signup",{template:"/auth/signup?version="+APP_ENV.CACHEBUSTER,scope:"home"}).add("/auth/recovery",{template:"/auth/recovery?version="+APP_ENV.CACHEBUSTER,scope:"home"}).add("/auth/recovery/reset",{template:"/auth/recovery/reset?version="+APP_ENV.CACHEBUSTER,scope:"home"}).add("/auth/confirm",{template:"/auth/confirm?version="+APP_ENV.CACHEBUSTER,scope:"home"}).add("/auth/join",{template:"/auth/join?version="+APP_ENV.CACHEBUSTER,scope:"home"}).add("/auth/oauth2/success",{template:"/auth/oauth2/success?version="+APP_ENV.CACHEBUSTER,scope:"home"}).add("/auth/oauth2/failure",{template:"/auth/oauth2/failure?version="+APP_ENV.CACHEBUSTER,scope:"home"}).add("/console",{template:"/console?version="+APP_ENV.CACHEBUSTER,scope:"console"}).add("/console/account",{template:"/console/account?version="+APP_ENV.CACHEBUSTER,scope:"console"}).add("/console/account/:tab",{template:"/console/account?version="+APP_ENV.CACHEBUSTER,scope:"console"}).add("/console/home",{template:"/console/home?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/home/:tab",{template:"/console/home?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/platforms/:platform",{template:function(window){return window.location.pathname+"?version="+APP_ENV.CACHEBUSTER;},scope:"console",project:true}).add("/console/notifications",{template:"/console/notifications?version="+APP_ENV.CACHEBUSTER,scope:"console"}).add("/console/settings",{template:"/console/settings?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/settings/:tab",{template:"/console/settings?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/webhooks",{template:"/console/webhooks?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/webhooks/:tab",{template:"/console/webhooks?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/keys",{template:"/console/keys?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/keys/:tab",{template:"/console/keys?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/tasks",{template:"/console/tasks?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/tasks/:tab",{template:"/console/tasks?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/database",{template:"/console/database?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/database/collection",{template:function(window){return window.location.pathname+window.location.search+'&version='+APP_ENV.CACHEBUSTER;},scope:"console",project:true}).add("/console/database/collection/:tab",{template:function(window){return window.location.pathname+window.location.search+'&version='+APP_ENV.CACHEBUSTER;},scope:"console",project:true}).add("/console/database/document",{template:function(window){return window.location.pathname+window.location.search+'&version='+APP_ENV.CACHEBUSTER;},scope:"console",project:true}).add("/console/database/document/:tab",{template:function(window){return window.location.pathname+window.location.search+'&version='+APP_ENV.CACHEBUSTER;},scope:"console",project:true}).add("/console/storage",{template:"/console/storage?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/storage/:tab",{template:"/console/storage?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/users",{template:"/console/users?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/users/user",{template:"/console/users/user?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/users/user/:tab",{template:"/console/users/user?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/users/teams/team",{template:"/console/users/teams/team?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/users/teams/team/:tab",{template:"/console/users/teams/team?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/users/:tab",{template:"/console/users?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/functions",{template:"/console/functions?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/functions/function",{template:"/console/functions/function?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/functions/function/:tab",{template:"/console/functions/function?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/functions/:tab",{template:"/console/functions?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true});window.ls.filter.add("avatar",function($value,element){if(!$value){return"";} +return false;};return{isRTL:isRTL,};},true);})(window);(function(window){"use strict";window.ls.container.set('sdk',function(window,router){var sdk=new window.Appwrite();sdk.setEndpoint(APP_ENV.ENDPOINT+APP_ENV.API).setProject(router.params.project||'').setLocale(APP_ENV.LOCALE).setMode('admin');return sdk;},false);})(window);(function(window){"use strict";window.ls.container.set('search',function(window){return{params:{},path:'',pointer:'',selected:'',};},true,true);})(window);(function(window){"use strict";window.ls.container.set('timezone',function(){return{convert:function(unixTime){var timezoneMinutes=new Date().getTimezoneOffset();timezoneMinutes=(timezoneMinutes===0)?0:-timezoneMinutes;return parseInt(unixTime)+(timezoneMinutes*60);}};},true);})(window);(function(window){"use strict";window.ls.container.set('realtime',()=>{return{current:null,set:function(currentConnections){var scope=this;scope.current=currentConnections;return scope.current;}};},true,true);})(window);window.ls.router.add("/auth/signin",{template:"/auth/signin?version="+APP_ENV.CACHEBUSTER,scope:"home"}).add("/auth/signup",{template:"/auth/signup?version="+APP_ENV.CACHEBUSTER,scope:"home"}).add("/auth/recovery",{template:"/auth/recovery?version="+APP_ENV.CACHEBUSTER,scope:"home"}).add("/auth/recovery/reset",{template:"/auth/recovery/reset?version="+APP_ENV.CACHEBUSTER,scope:"home"}).add("/auth/confirm",{template:"/auth/confirm?version="+APP_ENV.CACHEBUSTER,scope:"home"}).add("/auth/join",{template:"/auth/join?version="+APP_ENV.CACHEBUSTER,scope:"home"}).add("/auth/oauth2/success",{template:"/auth/oauth2/success?version="+APP_ENV.CACHEBUSTER,scope:"home"}).add("/auth/oauth2/failure",{template:"/auth/oauth2/failure?version="+APP_ENV.CACHEBUSTER,scope:"home"}).add("/console",{template:"/console?version="+APP_ENV.CACHEBUSTER,scope:"console"}).add("/console/account",{template:"/console/account?version="+APP_ENV.CACHEBUSTER,scope:"console"}).add("/console/account/:tab",{template:"/console/account?version="+APP_ENV.CACHEBUSTER,scope:"console"}).add("/console/home",{template:"/console/home?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/home/:tab",{template:"/console/home?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/platforms/:platform",{template:function(window){return window.location.pathname+"?version="+APP_ENV.CACHEBUSTER;},scope:"console",project:true}).add("/console/notifications",{template:"/console/notifications?version="+APP_ENV.CACHEBUSTER,scope:"console"}).add("/console/settings",{template:"/console/settings?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/settings/:tab",{template:"/console/settings?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/webhooks",{template:"/console/webhooks?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/webhooks/:tab",{template:"/console/webhooks?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/keys",{template:"/console/keys?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/keys/:tab",{template:"/console/keys?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/tasks",{template:"/console/tasks?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/tasks/:tab",{template:"/console/tasks?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/database",{template:"/console/database?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/database/collection",{template:function(window){return window.location.pathname+window.location.search+'&version='+APP_ENV.CACHEBUSTER;},scope:"console",project:true}).add("/console/database/collection/:tab",{template:function(window){return window.location.pathname+window.location.search+'&version='+APP_ENV.CACHEBUSTER;},scope:"console",project:true}).add("/console/database/document",{template:function(window){return window.location.pathname+window.location.search+'&version='+APP_ENV.CACHEBUSTER;},scope:"console",project:true}).add("/console/database/document/:tab",{template:function(window){return window.location.pathname+window.location.search+'&version='+APP_ENV.CACHEBUSTER;},scope:"console",project:true}).add("/console/storage",{template:"/console/storage?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/storage/:tab",{template:"/console/storage?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/users",{template:"/console/users?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/users/user",{template:"/console/users/user?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/users/user/:tab",{template:"/console/users/user?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/users/teams/team",{template:"/console/users/teams/team?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/users/teams/team/:tab",{template:"/console/users/teams/team?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/users/:tab",{template:"/console/users?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/functions",{template:"/console/functions?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/functions/function",{template:"/console/functions/function?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/functions/function/:tab",{template:"/console/functions/function?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/functions/:tab",{template:"/console/functions?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true});window.ls.filter.add("avatar",function($value,element){if(!$value){return"";} let size=element.dataset["size"]||80;let name=$value.name||$value||"";name=(typeof name!=='string')?'--':name;return def="/v1/avatars/initials?project=console"+"&name="+ encodeURIComponent(name)+"&width="+ size+"&height="+ diff --git a/public/dist/scripts/app.js b/public/dist/scripts/app.js index c07e11c881..c851e6712a 100644 --- a/public/dist/scripts/app.js +++ b/public/dist/scripts/app.js @@ -135,7 +135,7 @@ params=formData;break;} return new Promise(function(resolve,reject){let request=new XMLHttpRequest(),key;request.withCredentials=true;request.open(method,path,true);for(key in headers){if(headers.hasOwnProperty(key)){request.setRequestHeader(key,headers[key]);}} request.onload=function(){if(4===request.readyState&&399>=request.status){let data=request.response;let contentType=this.getResponseHeader('content-type');contentType=contentType.substring(0,contentType.indexOf(';'));switch(contentType){case'application/json':data=JSON.parse(data);break;} resolve(data);}else{reject(new Error(request.statusText));}};if(progress){request.addEventListener('progress',progress);request.upload.addEventListener('progress',progress,false);} -request.onerror=function(){reject(new Error("Network Error"));};request.send(params);})};return{'get':function(path,headers={},params={}){return call('GET',path+((params.length>0)?'?'+buildQuery(params):''),headers,{});},'post':function(path,headers={},params={},progress=null){return call('POST',path,headers,params,progress);},'put':function(path,headers={},params={},progress=null){return call('PUT',headers,params,progress);},'patch':function(path,headers={},params={},progress=null){return call('PATCH',path,headers,params,progress);},'delete':function(path,headers={},params={},progress=null){return call('DELETE',path,headers,params,progress);},'addGlobalParam':addGlobalParam,'addGlobalHeader':addGlobalHeader}}(window.document);let analytics={create:function(id,source,activity,url){return http.post('/analytics',{'content-type':'application/json'},{id:id,source:source,activity:activity,url:url,version:env.VERSION,setup:env.SETUP});},};return{analytics:analytics,};},true);})(window);(function(window){"use strict";window.ls.container.set('console',function(window){var sdk=new window.Appwrite.Appwrite();sdk.setEndpoint(APP_ENV.ENDPOINT+APP_ENV.API).setProject('console').setLocale(APP_ENV.LOCALE);return sdk;},true);})(window);(function(window){"use strict";window.ls.container.set('date',function(){function format(format,timestamp){var jsdate,f +request.onerror=function(){reject(new Error("Network Error"));};request.send(params);})};return{'get':function(path,headers={},params={}){return call('GET',path+((params.length>0)?'?'+buildQuery(params):''),headers,{});},'post':function(path,headers={},params={},progress=null){return call('POST',path,headers,params,progress);},'put':function(path,headers={},params={},progress=null){return call('PUT',headers,params,progress);},'patch':function(path,headers={},params={},progress=null){return call('PATCH',path,headers,params,progress);},'delete':function(path,headers={},params={},progress=null){return call('DELETE',path,headers,params,progress);},'addGlobalParam':addGlobalParam,'addGlobalHeader':addGlobalHeader}}(window.document);let analytics={create:function(id,source,activity,url){return http.post('/analytics',{'content-type':'application/json'},{id:id,source:source,activity:activity,url:url,version:env.VERSION,setup:env.SETUP});},};return{analytics:analytics,};},true);})(window);(function(window){"use strict";window.ls.container.set('console',function(window){var sdk=new window.Appwrite();sdk.setEndpoint(APP_ENV.ENDPOINT+APP_ENV.API).setProject('console').setLocale(APP_ENV.LOCALE);return sdk;},true);})(window);(function(window){"use strict";window.ls.container.set('date',function(){function format(format,timestamp){var jsdate,f var txtWords=['Sun','Mon','Tues','Wednes','Thurs','Fri','Satur','January','February','March','April','May','June','July','August','September','October','November','December'] var formatChr=/\\?(.?)/gi var formatChrCb=function(t,s){return f[t]?f[t]():s} @@ -264,7 +264,7 @@ return slf.renderToken(tokens,idx,opts);} md.renderer.rules.strong_open=renderEm;md.renderer.rules.strong_close=renderEm;return md;},true);})(window);(function(window){"use strict";window.ls.container.set('rtl',function(){var rtlStock="^ا^ب^ت^ث^ج^ح^خ^د^ذ^ر^ز^س^ش^ص^ض^ط^ظ^ع^غ^ف^ق^ك^ل^م^ن^ه^و^ي^א^ב^ג^ד^ה^ו^ז^ח^ט^י^כ^ך^ל^מ^ם^נ^ן^ס^ע^פ^ף^צ^ץ^ק^ר^ש^ת^";var special=["\n"," "," ","״",'"',"_","'","!","@","#","$","^","&","%","*","(",")","+","=","-","[","]","\\","/","{","}","|",":","<",">","?",",",".","0","1","2","3","4","5","6","7","8","9"];var isRTL=function(value){for(var i=0;i{return{current:null,set:function(currentConnections){var scope=this;scope.current=currentConnections;return scope.current;}};},true,true);})(window);window.ls.router.add("/auth/signin",{template:"/auth/signin?version="+APP_ENV.CACHEBUSTER,scope:"home"}).add("/auth/signup",{template:"/auth/signup?version="+APP_ENV.CACHEBUSTER,scope:"home"}).add("/auth/recovery",{template:"/auth/recovery?version="+APP_ENV.CACHEBUSTER,scope:"home"}).add("/auth/recovery/reset",{template:"/auth/recovery/reset?version="+APP_ENV.CACHEBUSTER,scope:"home"}).add("/auth/confirm",{template:"/auth/confirm?version="+APP_ENV.CACHEBUSTER,scope:"home"}).add("/auth/join",{template:"/auth/join?version="+APP_ENV.CACHEBUSTER,scope:"home"}).add("/auth/oauth2/success",{template:"/auth/oauth2/success?version="+APP_ENV.CACHEBUSTER,scope:"home"}).add("/auth/oauth2/failure",{template:"/auth/oauth2/failure?version="+APP_ENV.CACHEBUSTER,scope:"home"}).add("/console",{template:"/console?version="+APP_ENV.CACHEBUSTER,scope:"console"}).add("/console/account",{template:"/console/account?version="+APP_ENV.CACHEBUSTER,scope:"console"}).add("/console/account/:tab",{template:"/console/account?version="+APP_ENV.CACHEBUSTER,scope:"console"}).add("/console/home",{template:"/console/home?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/home/:tab",{template:"/console/home?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/platforms/:platform",{template:function(window){return window.location.pathname+"?version="+APP_ENV.CACHEBUSTER;},scope:"console",project:true}).add("/console/notifications",{template:"/console/notifications?version="+APP_ENV.CACHEBUSTER,scope:"console"}).add("/console/settings",{template:"/console/settings?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/settings/:tab",{template:"/console/settings?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/webhooks",{template:"/console/webhooks?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/webhooks/:tab",{template:"/console/webhooks?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/keys",{template:"/console/keys?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/keys/:tab",{template:"/console/keys?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/tasks",{template:"/console/tasks?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/tasks/:tab",{template:"/console/tasks?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/database",{template:"/console/database?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/database/collection",{template:function(window){return window.location.pathname+window.location.search+'&version='+APP_ENV.CACHEBUSTER;},scope:"console",project:true}).add("/console/database/collection/:tab",{template:function(window){return window.location.pathname+window.location.search+'&version='+APP_ENV.CACHEBUSTER;},scope:"console",project:true}).add("/console/database/document",{template:function(window){return window.location.pathname+window.location.search+'&version='+APP_ENV.CACHEBUSTER;},scope:"console",project:true}).add("/console/database/document/:tab",{template:function(window){return window.location.pathname+window.location.search+'&version='+APP_ENV.CACHEBUSTER;},scope:"console",project:true}).add("/console/storage",{template:"/console/storage?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/storage/:tab",{template:"/console/storage?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/users",{template:"/console/users?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/users/user",{template:"/console/users/user?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/users/user/:tab",{template:"/console/users/user?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/users/teams/team",{template:"/console/users/teams/team?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/users/teams/team/:tab",{template:"/console/users/teams/team?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/users/:tab",{template:"/console/users?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/functions",{template:"/console/functions?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/functions/function",{template:"/console/functions/function?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/functions/function/:tab",{template:"/console/functions/function?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/functions/:tab",{template:"/console/functions?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true});window.ls.filter.add("avatar",function($value,element){if(!$value){return"";} +return false;};return{isRTL:isRTL,};},true);})(window);(function(window){"use strict";window.ls.container.set('sdk',function(window,router){var sdk=new window.Appwrite();sdk.setEndpoint(APP_ENV.ENDPOINT+APP_ENV.API).setProject(router.params.project||'').setLocale(APP_ENV.LOCALE).setMode('admin');return sdk;},false);})(window);(function(window){"use strict";window.ls.container.set('search',function(window){return{params:{},path:'',pointer:'',selected:'',};},true,true);})(window);(function(window){"use strict";window.ls.container.set('timezone',function(){return{convert:function(unixTime){var timezoneMinutes=new Date().getTimezoneOffset();timezoneMinutes=(timezoneMinutes===0)?0:-timezoneMinutes;return parseInt(unixTime)+(timezoneMinutes*60);}};},true);})(window);(function(window){"use strict";window.ls.container.set('realtime',()=>{return{current:null,set:function(currentConnections){var scope=this;scope.current=currentConnections;return scope.current;}};},true,true);})(window);window.ls.router.add("/auth/signin",{template:"/auth/signin?version="+APP_ENV.CACHEBUSTER,scope:"home"}).add("/auth/signup",{template:"/auth/signup?version="+APP_ENV.CACHEBUSTER,scope:"home"}).add("/auth/recovery",{template:"/auth/recovery?version="+APP_ENV.CACHEBUSTER,scope:"home"}).add("/auth/recovery/reset",{template:"/auth/recovery/reset?version="+APP_ENV.CACHEBUSTER,scope:"home"}).add("/auth/confirm",{template:"/auth/confirm?version="+APP_ENV.CACHEBUSTER,scope:"home"}).add("/auth/join",{template:"/auth/join?version="+APP_ENV.CACHEBUSTER,scope:"home"}).add("/auth/oauth2/success",{template:"/auth/oauth2/success?version="+APP_ENV.CACHEBUSTER,scope:"home"}).add("/auth/oauth2/failure",{template:"/auth/oauth2/failure?version="+APP_ENV.CACHEBUSTER,scope:"home"}).add("/console",{template:"/console?version="+APP_ENV.CACHEBUSTER,scope:"console"}).add("/console/account",{template:"/console/account?version="+APP_ENV.CACHEBUSTER,scope:"console"}).add("/console/account/:tab",{template:"/console/account?version="+APP_ENV.CACHEBUSTER,scope:"console"}).add("/console/home",{template:"/console/home?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/home/:tab",{template:"/console/home?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/platforms/:platform",{template:function(window){return window.location.pathname+"?version="+APP_ENV.CACHEBUSTER;},scope:"console",project:true}).add("/console/notifications",{template:"/console/notifications?version="+APP_ENV.CACHEBUSTER,scope:"console"}).add("/console/settings",{template:"/console/settings?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/settings/:tab",{template:"/console/settings?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/webhooks",{template:"/console/webhooks?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/webhooks/:tab",{template:"/console/webhooks?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/keys",{template:"/console/keys?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/keys/:tab",{template:"/console/keys?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/tasks",{template:"/console/tasks?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/tasks/:tab",{template:"/console/tasks?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/database",{template:"/console/database?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/database/collection",{template:function(window){return window.location.pathname+window.location.search+'&version='+APP_ENV.CACHEBUSTER;},scope:"console",project:true}).add("/console/database/collection/:tab",{template:function(window){return window.location.pathname+window.location.search+'&version='+APP_ENV.CACHEBUSTER;},scope:"console",project:true}).add("/console/database/document",{template:function(window){return window.location.pathname+window.location.search+'&version='+APP_ENV.CACHEBUSTER;},scope:"console",project:true}).add("/console/database/document/:tab",{template:function(window){return window.location.pathname+window.location.search+'&version='+APP_ENV.CACHEBUSTER;},scope:"console",project:true}).add("/console/storage",{template:"/console/storage?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/storage/:tab",{template:"/console/storage?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/users",{template:"/console/users?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/users/user",{template:"/console/users/user?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/users/user/:tab",{template:"/console/users/user?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/users/teams/team",{template:"/console/users/teams/team?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/users/teams/team/:tab",{template:"/console/users/teams/team?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/users/:tab",{template:"/console/users?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/functions",{template:"/console/functions?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/functions/function",{template:"/console/functions/function?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/functions/function/:tab",{template:"/console/functions/function?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/functions/:tab",{template:"/console/functions?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true});window.ls.filter.add("avatar",function($value,element){if(!$value){return"";} let size=element.dataset["size"]||80;let name=$value.name||$value||"";name=(typeof name!=='string')?'--':name;return def="/v1/avatars/initials?project=console"+"&name="+ encodeURIComponent(name)+"&width="+ size+"&height="+ diff --git a/public/scripts/services/console.js b/public/scripts/services/console.js index 1904187c2e..fd6d983605 100644 --- a/public/scripts/services/console.js +++ b/public/scripts/services/console.js @@ -2,7 +2,7 @@ "use strict"; window.ls.container.set('console', function (window) { - var sdk = new window.Appwrite.Appwrite(); + var sdk = new window.Appwrite(); sdk .setEndpoint(APP_ENV.ENDPOINT + APP_ENV.API) diff --git a/public/scripts/services/sdk.js b/public/scripts/services/sdk.js index 117c958f80..a4720cd223 100644 --- a/public/scripts/services/sdk.js +++ b/public/scripts/services/sdk.js @@ -2,7 +2,7 @@ "use strict"; window.ls.container.set('sdk', function (window, router) { - var sdk = new window.Appwrite.Appwrite(); + var sdk = new window.Appwrite(); sdk .setEndpoint(APP_ENV.ENDPOINT + APP_ENV.API) From 9370e74dde02ebab96e1467ca4651b6905dba177 Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Fri, 18 Jun 2021 13:17:33 +0200 Subject: [PATCH 145/267] remove file leftover --- test/imagick | 1 - 1 file changed, 1 deletion(-) delete mode 160000 test/imagick diff --git a/test/imagick b/test/imagick deleted file mode 160000 index 3de6f0cf65..0000000000 --- a/test/imagick +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 3de6f0cf65767219f762337e46081e99ab8a334f From 241255dabfdf981e86a9a2427039d7edad4776f2 Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Tue, 22 Jun 2021 11:16:29 +0200 Subject: [PATCH 146/267] improve connection pools --- src/Appwrite/Database/Pool.php | 4 +++- src/Appwrite/Database/Pool/PDOPool.php | 13 ++++++------- src/Appwrite/Database/Pool/RedisPool.php | 12 +++++------- 3 files changed, 14 insertions(+), 15 deletions(-) diff --git a/src/Appwrite/Database/Pool.php b/src/Appwrite/Database/Pool.php index f914f38e77..ee8902d3d6 100644 --- a/src/Appwrite/Database/Pool.php +++ b/src/Appwrite/Database/Pool.php @@ -1,10 +1,12 @@ pool = new SplQueue; - $this->size = $size; + $this->pool = new Channel($this->size = $size); for ($i = 0; $i < $this->size; $i++) { $pdo = new PDO( "mysql:" . @@ -29,19 +28,19 @@ class PDOPool extends Pool PDO::MYSQL_ATTR_USE_BUFFERED_QUERY => true ] ); - $this->pool->enqueue($pdo); + $this->pool->push($pdo); } } public function put(PDO $pdo) { - $this->pool->enqueue($pdo); + $this->pool->push($pdo); } public function get(): PDO { - if ($this->available && count($this->pool) > 0) { - return $this->pool->dequeue(); + if ($this->available && !$this->pool->isEmpty()) { + return $this->pool->pop(); } sleep(0.01); return $this->get(); diff --git a/src/Appwrite/Database/Pool/RedisPool.php b/src/Appwrite/Database/Pool/RedisPool.php index 08e102abb0..47e740b396 100644 --- a/src/Appwrite/Database/Pool/RedisPool.php +++ b/src/Appwrite/Database/Pool/RedisPool.php @@ -3,16 +3,14 @@ namespace Appwrite\Database\Pool; use Appwrite\Database\Pool; -use SplQueue; - use Redis; +use Swoole\Coroutine\Channel; class RedisPool extends Pool { public function __construct(int $size, string $host, int $port, array $auth = []) { - $this->pool = new SplQueue; - $this->size = $size; + $this->pool = new Channel($this->size = $size); for ($i = 0; $i < $this->size; $i++) { $redis = new Redis(); $redis->pconnect($host, $port); @@ -22,19 +20,19 @@ class RedisPool extends Pool $redis->auth($auth); } - $this->pool->enqueue($redis); + $this->pool->push($redis); } } public function put(Redis $redis) { - $this->pool->enqueue($redis); + $this->pool->push($redis); } public function get(): Redis { if ($this->available && !$this->pool->isEmpty()) { - return $this->pool->dequeue(); + return $this->pool->pop(); } sleep(0.1); return $this->get(); From c1d85d17c8217fe8de5adb45f7f424c4d4745f93 Mon Sep 17 00:00:00 2001 From: Eldad Fux Date: Tue, 22 Jun 2021 22:51:14 +0300 Subject: [PATCH 147/267] POC --- Dockerfile | 2 +- app/init.php | 48 ++++++++++++++++-------- composer.lock | 1 + src/Appwrite/Database/Adapter/MySQL.php | 34 ++++++++++------- src/Appwrite/Database/Adapter/Redis.php | 13 +++---- src/Appwrite/Database/Pool/PDOPool.php | 48 ------------------------ src/Appwrite/Database/Pool/RedisPool.php | 40 -------------------- 7 files changed, 60 insertions(+), 126 deletions(-) delete mode 100644 src/Appwrite/Database/Pool/PDOPool.php delete mode 100644 src/Appwrite/Database/Pool/RedisPool.php diff --git a/Dockerfile b/Dockerfile index 5c77c202df..f8955cbe24 100755 --- a/Dockerfile +++ b/Dockerfile @@ -15,7 +15,7 @@ RUN composer update --ignore-platform-reqs --optimize-autoloader \ FROM php:8.0-cli-alpine as step1 ENV PHP_REDIS_VERSION=5.3.4 \ - PHP_SWOOLE_VERSION=v4.6.6 \ + PHP_SWOOLE_VERSION=v4.6.7 \ PHP_IMAGICK_VERSION=master \ PHP_YAML_VERSION=2.2.1 \ PHP_MAXMINDDB_VERSION=v1.10.1 diff --git a/app/init.php b/app/init.php index cb87697a83..d1b845aeb5 100644 --- a/app/init.php +++ b/app/init.php @@ -24,8 +24,6 @@ use Appwrite\Database\Database; use Appwrite\Database\Adapter\MySQL as MySQLAdapter; use Appwrite\Database\Adapter\Redis as RedisAdapter; use Appwrite\Database\Document; -use Appwrite\Database\Pool\PDOPool; -use Appwrite\Database\Pool\RedisPool; use Appwrite\Database\Validator\Authorization; use Appwrite\Event\Event; use Appwrite\Event\Realtime; @@ -37,6 +35,10 @@ use Utopia\Locale\Locale; use Utopia\Registry\Registry; use MaxMind\Db\Reader; use PHPMailer\PHPMailer\PHPMailer; +use Swoole\Database\PDOConfig; +use Swoole\Database\PDOPool; +use Swoole\Database\RedisConfig; +use Swoole\Database\RedisPool; const APP_NAME = 'Appwrite'; const APP_DOMAIN = 'appwrite.io'; @@ -154,10 +156,21 @@ Database::addFilter('encrypt', */ $register->set('dbPool', function () { // Register DB connection $dbHost = App::getEnv('_APP_DB_HOST', ''); + $dbPort = App::getEnv('_APP_DB_PORT', ''); $dbUser = App::getEnv('_APP_DB_USER', ''); $dbPass = App::getEnv('_APP_DB_PASS', ''); $dbScheme = App::getEnv('_APP_DB_SCHEMA', ''); - $pool = new PDOPool(10, $dbHost, $dbScheme, $dbUser, $dbPass); + + + $pool = new PDOPool((new PDOConfig()) + ->withHost($dbHost) + ->withPort($dbPort) + // ->withUnixSocket('/tmp/mysql.sock') + ->withDbName($dbScheme) + ->withCharset('utf8mb4') + ->withUsername($dbUser) + ->withPassword($dbPass) + ); return $pool; }); @@ -166,16 +179,19 @@ $register->set('redisPool', function () { $redisPort = App::getEnv('_APP_REDIS_PORT', ''); $redisUser = App::getEnv('_APP_REDIS_USER', ''); $redisPass = App::getEnv('_APP_REDIS_PASS', ''); - $redisAuth = []; + $redisAuth = ''; - if ($redisUser) { - $redisAuth[] = $redisUser; - } - if ($redisPass) { - $redisAuth[] = $redisPass; + if ($redisUser && $redisPass) { + $redisAuth = $redisUser.':'.$redisPass; } - $pool = new RedisPool(10, $redisHost, $redisPort, $redisAuth); + $pool = new RedisPool((new RedisConfig) + ->withHost($redisHost) + ->withPort($redisPort) + ->withAuth($redisAuth) + ->withDbIndex(0) + ->withTimeout(1) + ); return $pool; }); @@ -485,23 +501,23 @@ App::setResource('console', function($consoleDB) { return $consoleDB->getDocument('console'); }, ['consoleDB']); -App::setResource('consoleDB', function($register) { +App::setResource('consoleDB', function($db, $cache) { $consoleDB = new Database(); - $consoleDB->setAdapter(new RedisAdapter(new MySQLAdapter($register), $register)); + $consoleDB->setAdapter(new RedisAdapter(new MySQLAdapter($db, $cache), $cache)); $consoleDB->setNamespace('app_console'); // Should be replaced with param if we want to have parent projects $consoleDB->setMocks(Config::getParam('collections', [])); return $consoleDB; -}, ['register']); +}, ['db', 'cache']); -App::setResource('projectDB', function($register, $project) { +App::setResource('projectDB', function($db, $cache, $project) { $projectDB = new Database(); - $projectDB->setAdapter(new RedisAdapter(new MySQLAdapter($register), $register)); + $projectDB->setAdapter(new RedisAdapter(new MySQLAdapter($db, $cache), $cache)); $projectDB->setNamespace('app_'.$project->getId()); $projectDB->setMocks(Config::getParam('collections', [])); return $projectDB; -}, ['register', 'project']); +}, ['db', 'cache', 'project']); App::setResource('mode', function($request) { /** @var Utopia\Swoole\Request $request */ diff --git a/composer.lock b/composer.lock index 57c562ea8d..b0f8743264 100644 --- a/composer.lock +++ b/composer.lock @@ -4819,6 +4819,7 @@ "type": "github" } ], + "abandoned": true, "time": "2020-09-28T06:45:17+00:00" }, { diff --git a/src/Appwrite/Database/Adapter/MySQL.php b/src/Appwrite/Database/Adapter/MySQL.php index 37d07c3a27..5f5bc74621 100644 --- a/src/Appwrite/Database/Adapter/MySQL.php +++ b/src/Appwrite/Database/Adapter/MySQL.php @@ -2,13 +2,12 @@ namespace Appwrite\Database\Adapter; -use Utopia\Registry\Registry; use Appwrite\Database\Adapter; use Appwrite\Database\Exception\Duplicate; use Appwrite\Database\Validator\Authorization; use Exception; use PDO; -use Redis as Client; +use Redis; class MySQL extends Adapter { @@ -23,11 +22,6 @@ class MySQL extends Adapter const OPTIONS_LIMIT_ATTRIBUTES = 1000; - /** - * @var Registry - */ - protected $register; - /** * Last modified. * @@ -42,16 +36,28 @@ class MySQL extends Adapter */ protected $debug = []; + /** + * @var PDO + */ + protected $pdo; + + /** + * @var Redis + */ + protected $redis; + /** * Constructor. * * Set connection and settings * - * @param Registry $register + * @param PDO $pdo + * @param Redis $redis */ - public function __construct(Registry $register) + public function __construct(PDO $pdo, Redis $redis) { - $this->register = $register; + $this->pdo = $pdo; + $this->redis = $redis; } /** @@ -935,16 +941,16 @@ class MySQL extends Adapter */ protected function getPDO(): PDO { - return $this->register->get('db'); + return $this->pdo; } /** * @throws Exception * - * @return Client + * @return Redis */ - protected function getRedis(): Client + protected function getRedis(): Redis { - return $this->register->get('cache'); + return $this->redis; } } diff --git a/src/Appwrite/Database/Adapter/Redis.php b/src/Appwrite/Database/Adapter/Redis.php index a1e440112d..1abffe4603 100644 --- a/src/Appwrite/Database/Adapter/Redis.php +++ b/src/Appwrite/Database/Adapter/Redis.php @@ -2,7 +2,6 @@ namespace Appwrite\Database\Adapter; -use Utopia\Registry\Registry; use Appwrite\Database\Adapter; use Exception; use Redis as Client; @@ -10,9 +9,9 @@ use Redis as Client; class Redis extends Adapter { /** - * @var Registry + * @var Client */ - protected $register; + protected $redis; /** * @var Adapter @@ -23,11 +22,11 @@ class Redis extends Adapter * Redis constructor. * * @param Adapter $adapter - * @param Registry $register + * @param Client $redis */ - public function __construct(Adapter $adapter, Registry $register) + public function __construct(Adapter $adapter, Client $redis) { - $this->register = $register; + $this->redis = $redis; $this->adapter = $adapter; } @@ -261,7 +260,7 @@ class Redis extends Adapter */ protected function getRedis(): Client { - return $this->register->get('cache'); + return $this->redis; } /** diff --git a/src/Appwrite/Database/Pool/PDOPool.php b/src/Appwrite/Database/Pool/PDOPool.php deleted file mode 100644 index 3e7cc3b5f4..0000000000 --- a/src/Appwrite/Database/Pool/PDOPool.php +++ /dev/null @@ -1,48 +0,0 @@ -pool = new Channel($this->size = $size); - for ($i = 0; $i < $this->size; $i++) { - $pdo = new PDO( - "mysql:" . - "host={$host};" . - "dbname={$schema};" . - "charset={$charset}", - $user, - $pass, - [ - PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8mb4', - PDO::ATTR_TIMEOUT => 3, // Seconds - PDO::ATTR_PERSISTENT => true, - PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, - PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, - PDO::MYSQL_ATTR_USE_BUFFERED_QUERY => true - ] - ); - $this->pool->push($pdo); - } - } - - public function put(PDO $pdo) - { - $this->pool->push($pdo); - } - - public function get(): PDO - { - if ($this->available && !$this->pool->isEmpty()) { - return $this->pool->pop(); - } - sleep(0.01); - return $this->get(); - } -} diff --git a/src/Appwrite/Database/Pool/RedisPool.php b/src/Appwrite/Database/Pool/RedisPool.php deleted file mode 100644 index 47e740b396..0000000000 --- a/src/Appwrite/Database/Pool/RedisPool.php +++ /dev/null @@ -1,40 +0,0 @@ -pool = new Channel($this->size = $size); - for ($i = 0; $i < $this->size; $i++) { - $redis = new Redis(); - $redis->pconnect($host, $port); - $redis->setOption(Redis::OPT_READ_TIMEOUT, -1); - - if ($auth) { - $redis->auth($auth); - } - - $this->pool->push($redis); - } - } - - public function put(Redis $redis) - { - $this->pool->push($redis); - } - - public function get(): Redis - { - if ($this->available && !$this->pool->isEmpty()) { - return $this->pool->pop(); - } - sleep(0.1); - return $this->get(); - } -} From ca201011c52b56a846b1104dc3aee2bd54688c6a Mon Sep 17 00:00:00 2001 From: Eldad Fux Date: Tue, 22 Jun 2021 23:31:39 +0300 Subject: [PATCH 148/267] Fixes --- app/controllers/shared/api.php | 70 ++++++++++++------------- app/http.php | 4 +- src/Appwrite/Database/Adapter/MySQL.php | 4 +- src/Appwrite/Realtime/Server.php | 40 +++++++------- tests/benchmarks/ws.js | 2 +- 5 files changed, 60 insertions(+), 60 deletions(-) diff --git a/app/controllers/shared/api.php b/app/controllers/shared/api.php index 57ffe867de..9ccc914564 100644 --- a/app/controllers/shared/api.php +++ b/app/controllers/shared/api.php @@ -31,47 +31,47 @@ App::init(function ($utopia, $request, $response, $project, $user, $register, $e throw new Exception('Missing or unknown project ID', 400); } - /* - * Abuse Check - */ - $timeLimit = new TimeLimit($route->getLabel('abuse-key', 'url:{url},ip:{ip}'), $route->getLabel('abuse-limit', 0), $route->getLabel('abuse-time', 3600), function () use ($register) { - return $register->get('db'); - }); - $timeLimit->setNamespace('app_'.$project->getId()); - $timeLimit - ->setParam('{userId}', $user->getId()) - ->setParam('{userAgent}', $request->getUserAgent('')) - ->setParam('{ip}', $request->getIP()) - ->setParam('{url}', $request->getHostname().$route->getURL()) - ; + // /* + // * Abuse Check + // */ + // $timeLimit = new TimeLimit($route->getLabel('abuse-key', 'url:{url},ip:{ip}'), $route->getLabel('abuse-limit', 0), $route->getLabel('abuse-time', 3600), function () use ($register) { + // return $register->get('db'); + // }); + // $timeLimit->setNamespace('app_'.$project->getId()); + // $timeLimit + // ->setParam('{userId}', $user->getId()) + // ->setParam('{userAgent}', $request->getUserAgent('')) + // ->setParam('{ip}', $request->getIP()) + // ->setParam('{url}', $request->getHostname().$route->getURL()) + // ; - //TODO make sure we get array here + // //TODO make sure we get array here - foreach ($request->getParams() as $key => $value) { // Set request params as potential abuse keys - if(!empty($value)) { - $timeLimit->setParam('{param-'.$key.'}', (\is_array($value)) ? \json_encode($value) : $value); - } - } + // foreach ($request->getParams() as $key => $value) { // Set request params as potential abuse keys + // if(!empty($value)) { + // $timeLimit->setParam('{param-'.$key.'}', (\is_array($value)) ? \json_encode($value) : $value); + // } + // } - $abuse = new Abuse($timeLimit); + // $abuse = new Abuse($timeLimit); - if ($timeLimit->limit()) { - $response - ->addHeader('X-RateLimit-Limit', $timeLimit->limit()) - ->addHeader('X-RateLimit-Remaining', $timeLimit->remaining()) - ->addHeader('X-RateLimit-Reset', $timeLimit->time() + $route->getLabel('abuse-time', 3600)) - ; - } + // if ($timeLimit->limit()) { + // $response + // ->addHeader('X-RateLimit-Limit', $timeLimit->limit()) + // ->addHeader('X-RateLimit-Remaining', $timeLimit->remaining()) + // ->addHeader('X-RateLimit-Reset', $timeLimit->time() + $route->getLabel('abuse-time', 3600)) + // ; + // } - $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::$roles); - $isAppUser = Auth::isAppUser(Authorization::$roles); + // $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::$roles); + // $isAppUser = Auth::isAppUser(Authorization::$roles); - if (($abuse->check() // Route is rate-limited - && App::getEnv('_APP_OPTIONS_ABUSE', 'enabled') !== 'disabled') // Abuse is not diabled - && (!$isAppUser && !$isPrivilegedUser)) // User is not an admin or API key - { - throw new Exception('Too many requests', 429); - } + // if (($abuse->check() // Route is rate-limited + // && App::getEnv('_APP_OPTIONS_ABUSE', 'enabled') !== 'disabled') // Abuse is not diabled + // && (!$isAppUser && !$isPrivilegedUser)) // User is not an admin or API key + // { + // throw new Exception('Too many requests', 429); + // } /* * Background Jobs diff --git a/app/http.php b/app/http.php index 7d56a9046b..b32103a416 100644 --- a/app/http.php +++ b/app/http.php @@ -78,11 +78,11 @@ $http->on('request', function (SwooleRequest $swooleRequest, SwooleResponse $swo $db = $register->get('dbPool')->get(); $redis = $register->get('redisPool')->get(); - $register->set('db', function () use (&$db) { + App::setResource('db', function () use (&$db) { return $db; }); - $register->set('cache', function () use (&$redis) { + App::setResource('cache', function () use (&$redis) { return $redis; }); diff --git a/src/Appwrite/Database/Adapter/MySQL.php b/src/Appwrite/Database/Adapter/MySQL.php index 5f5bc74621..93dc379834 100644 --- a/src/Appwrite/Database/Adapter/MySQL.php +++ b/src/Appwrite/Database/Adapter/MySQL.php @@ -54,7 +54,7 @@ class MySQL extends Adapter * @param PDO $pdo * @param Redis $redis */ - public function __construct(PDO $pdo, Redis $redis) + public function __construct($pdo, Redis $redis) { $this->pdo = $pdo; $this->redis = $redis; @@ -939,7 +939,7 @@ class MySQL extends Adapter * * @throws Exception */ - protected function getPDO(): PDO + protected function getPDO() { return $this->pdo; } diff --git a/src/Appwrite/Realtime/Server.php b/src/Appwrite/Realtime/Server.php index 4351e22ce2..bdf5ac2973 100644 --- a/src/Appwrite/Realtime/Server.php +++ b/src/Appwrite/Realtime/Server.php @@ -176,16 +176,16 @@ class Server $db = $this->register->get('dbPool')->get(); $redis = $this->register->get('redisPool')->get(); - $this->register->set('db', function () use (&$db) { + Console::info("Connection open (user: {$connection}, worker: {$server->getWorkerId()})"); + + App::setResource('db', function () use (&$db) { return $db; }); - $this->register->set('cache', function () use (&$redis) { + App::setResource('cache', function () use (&$redis) { return $redis; }); - Console::info("Connection open (user: {$connection}, worker: {$server->getWorkerId()})"); - App::setResource('request', function () use ($request) { return $request; }); @@ -211,24 +211,24 @@ class Server 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, function () use ($db) { - return $db; - }); - $timeLimit - ->setNamespace('app_' . $project->getId()) - ->setParam('{ip}', $request->getIP()) - ->setParam('{url}', $request->getURI()); + // /* + // * Abuse Check + // * + // * Abuse limits are connecting 128 times per minute and ip address. + // */ + // $timeLimit = new TimeLimit('url:{url},ip:{ip}', 128, 60, function () use ($db) { + // return $db; + // }); + // $timeLimit + // ->setNamespace('app_' . $project->getId()) + // ->setParam('{ip}', $request->getIP()) + // ->setParam('{url}', $request->getURI()); - $abuse = new Abuse($timeLimit); + // $abuse = new Abuse($timeLimit); - if ($abuse->check() && App::getEnv('_APP_OPTIONS_ABUSE', 'enabled') === 'enabled') { - throw new Exception('Too many requests', 1013); - } + // 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. diff --git a/tests/benchmarks/ws.js b/tests/benchmarks/ws.js index d7bef7ab5f..1f69fe00be 100644 --- a/tests/benchmarks/ws.js +++ b/tests/benchmarks/ws.js @@ -21,7 +21,7 @@ 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', '60476312f335c'); + url.searchParams.append('project', 'console'); url.searchParams.append('channels[]', 'files'); const res = ws.connect(url.toString(), function (socket) { From 73489d6b7369f9f7bb98860730b2efd0ce4db512 Mon Sep 17 00:00:00 2001 From: Eldad Fux Date: Wed, 23 Jun 2021 08:59:34 +0300 Subject: [PATCH 149/267] More tiny fixes --- src/Appwrite/Database/Adapter/MySQL.php | 8 ++++---- tests/benchmarks/http.js | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Appwrite/Database/Adapter/MySQL.php b/src/Appwrite/Database/Adapter/MySQL.php index 93dc379834..52b0f9d1a7 100644 --- a/src/Appwrite/Database/Adapter/MySQL.php +++ b/src/Appwrite/Database/Adapter/MySQL.php @@ -93,8 +93,8 @@ class MySQL extends Adapter ORDER BY `order` '); - $st->bindParam(':documentUid', $document['uid'], PDO::PARAM_STR); - $st->bindParam(':documentRevision', $document['revision'], PDO::PARAM_STR); + $st->bindParam(':documentUid', $document['uid'], PDO::PARAM_STR, 32); + $st->bindParam(':documentRevision', $document['revision'], PDO::PARAM_STR, 32); $st->execute(); @@ -122,8 +122,8 @@ class MySQL extends Adapter ORDER BY `order` '); - $st->bindParam(':start', $document['uid'], PDO::PARAM_STR); - $st->bindParam(':revision', $document['revision'], PDO::PARAM_STR); + $st->bindParam(':start', $document['uid'], PDO::PARAM_STR, 32); + $st->bindParam(':revision', $document['revision'], PDO::PARAM_STR, 32); $st->execute(); diff --git a/tests/benchmarks/http.js b/tests/benchmarks/http.js index e46157a21f..c84fc18251 100644 --- a/tests/benchmarks/http.js +++ b/tests/benchmarks/http.js @@ -26,7 +26,7 @@ export default function () { 'X-Appwrite-Project': '60479fe35d95d' }} - const resDb = http.get('http://localhost:9501/v1/health/db', config); + const resDb = http.get('http://localhost:9501/', config); check(resDb, { 'status is 200': (r) => r.status === 200, From 72392ac09fc85d4186773f973631ab0e77e60b1d Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Wed, 23 Jun 2021 17:11:23 +0200 Subject: [PATCH 150/267] fix(abuse): enable abuse limits again --- app/controllers/shared/api.php | 75 ++++++++++++++++---------------- composer.lock | 25 +++++------ src/Appwrite/Realtime/Server.php | 32 +++++++------- 3 files changed, 66 insertions(+), 66 deletions(-) diff --git a/app/controllers/shared/api.php b/app/controllers/shared/api.php index 9ccc914564..07e021a4b7 100644 --- a/app/controllers/shared/api.php +++ b/app/controllers/shared/api.php @@ -9,7 +9,7 @@ use Utopia\Abuse\Adapters\TimeLimit; use Utopia\Storage\Device\Local; use Utopia\Storage\Storage; -App::init(function ($utopia, $request, $response, $project, $user, $register, $events, $audits, $usage, $deletes) { +App::init(function ($utopia, $request, $response, $project, $user, $register, $events, $audits, $usage, $deletes, $db) { /** @var Utopia\App $utopia */ /** @var Utopia\Swoole\Request $request */ /** @var Appwrite\Utopia\Response $response */ @@ -21,6 +21,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())); @@ -31,47 +32,47 @@ App::init(function ($utopia, $request, $response, $project, $user, $register, $e throw new Exception('Missing or unknown project ID', 400); } - // /* - // * Abuse Check - // */ - // $timeLimit = new TimeLimit($route->getLabel('abuse-key', 'url:{url},ip:{ip}'), $route->getLabel('abuse-limit', 0), $route->getLabel('abuse-time', 3600), function () use ($register) { - // return $register->get('db'); - // }); - // $timeLimit->setNamespace('app_'.$project->getId()); - // $timeLimit - // ->setParam('{userId}', $user->getId()) - // ->setParam('{userAgent}', $request->getUserAgent('')) - // ->setParam('{ip}', $request->getIP()) - // ->setParam('{url}', $request->getHostname().$route->getURL()) - // ; + /* + * Abuse Check + */ + $timeLimit = new TimeLimit($route->getLabel('abuse-key', 'url:{url},ip:{ip}'), $route->getLabel('abuse-limit', 0), $route->getLabel('abuse-time', 3600), function () use (&$db) { + return $db; + }); + $timeLimit->setNamespace('app_'.$project->getId()); + $timeLimit + ->setParam('{userId}', $user->getId()) + ->setParam('{userAgent}', $request->getUserAgent('')) + ->setParam('{ip}', $request->getIP()) + ->setParam('{url}', $request->getHostname().$route->getURL()) + ; - // //TODO make sure we get array here + //TODO make sure we get array here - // foreach ($request->getParams() as $key => $value) { // Set request params as potential abuse keys - // if(!empty($value)) { - // $timeLimit->setParam('{param-'.$key.'}', (\is_array($value)) ? \json_encode($value) : $value); - // } - // } + foreach ($request->getParams() as $key => $value) { // Set request params as potential abuse keys + if(!empty($value)) { + $timeLimit->setParam('{param-'.$key.'}', (\is_array($value)) ? \json_encode($value) : $value); + } + } - // $abuse = new Abuse($timeLimit); + $abuse = new Abuse($timeLimit); - // if ($timeLimit->limit()) { - // $response - // ->addHeader('X-RateLimit-Limit', $timeLimit->limit()) - // ->addHeader('X-RateLimit-Remaining', $timeLimit->remaining()) - // ->addHeader('X-RateLimit-Reset', $timeLimit->time() + $route->getLabel('abuse-time', 3600)) - // ; - // } + if ($timeLimit->limit()) { + $response + ->addHeader('X-RateLimit-Limit', $timeLimit->limit()) + ->addHeader('X-RateLimit-Remaining', $timeLimit->remaining()) + ->addHeader('X-RateLimit-Reset', $timeLimit->time() + $route->getLabel('abuse-time', 3600)) + ; + } - // $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::$roles); - // $isAppUser = Auth::isAppUser(Authorization::$roles); + $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::$roles); + $isAppUser = Auth::isAppUser(Authorization::$roles); - // if (($abuse->check() // Route is rate-limited - // && App::getEnv('_APP_OPTIONS_ABUSE', 'enabled') !== 'disabled') // Abuse is not diabled - // && (!$isAppUser && !$isPrivilegedUser)) // User is not an admin or API key - // { - // throw new Exception('Too many requests', 429); - // } + if (($abuse->check() // Route is rate-limited + && App::getEnv('_APP_OPTIONS_ABUSE', 'enabled') !== 'disabled') // Abuse is not diabled + && (!$isAppUser && !$isPrivilegedUser)) // User is not an admin or API key + { + throw new Exception('Too many requests', 429); + } /* * Background Jobs @@ -111,7 +112,7 @@ App::init(function ($utopia, $request, $response, $project, $user, $register, $e ->setParam('projectId', $project->getId()) ; -}, ['utopia', 'request', 'response', 'project', 'user', 'register', 'events', 'audits', 'usage', 'deletes'], 'api'); +}, ['utopia', 'request', 'response', 'project', 'user', 'register', 'events', 'audits', 'usage', 'deletes', 'db'], 'api'); App::init(function ($utopia, $request, $response, $project, $user) { /** @var Utopia\App $utopia */ diff --git a/composer.lock b/composer.lock index b0f8743264..22f89efb6e 100644 --- a/composer.lock +++ b/composer.lock @@ -1324,16 +1324,16 @@ }, { "name": "utopia-php/abuse", - "version": "0.4.1", + "version": "0.4.2", "source": { "type": "git", "url": "https://github.com/utopia-php/abuse.git", - "reference": "8b7973aae4b02489bd22ffea45b985608f13b6d9" + "reference": "286b52209818e5033573e6441d65adbc48a6f715" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/abuse/zipball/8b7973aae4b02489bd22ffea45b985608f13b6d9", - "reference": "8b7973aae4b02489bd22ffea45b985608f13b6d9", + "url": "https://api.github.com/repos/utopia-php/abuse/zipball/286b52209818e5033573e6441d65adbc48a6f715", + "reference": "286b52209818e5033573e6441d65adbc48a6f715", "shasum": "" }, "require": { @@ -1370,9 +1370,9 @@ ], "support": { "issues": "https://github.com/utopia-php/abuse/issues", - "source": "https://github.com/utopia-php/abuse/tree/0.4.1" + "source": "https://github.com/utopia-php/abuse/tree/0.4.2" }, - "time": "2021-06-05T14:31:33+00:00" + "time": "2021-06-23T15:04:44+00:00" }, { "name": "utopia-php/analytics", @@ -2003,16 +2003,16 @@ }, { "name": "utopia-php/swoole", - "version": "0.2.3", + "version": "0.2.4", "source": { "type": "git", "url": "https://github.com/utopia-php/swoole.git", - "reference": "45c42aae7e7d3f9f82bf194c2cfa5499b674aefe" + "reference": "37d8c64b536d6bc7da4f0f5a934a0ec44885abf4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/swoole/zipball/45c42aae7e7d3f9f82bf194c2cfa5499b674aefe", - "reference": "45c42aae7e7d3f9f82bf194c2cfa5499b674aefe", + "url": "https://api.github.com/repos/utopia-php/swoole/zipball/37d8c64b536d6bc7da4f0f5a934a0ec44885abf4", + "reference": "37d8c64b536d6bc7da4f0f5a934a0ec44885abf4", "shasum": "" }, "require": { @@ -2053,9 +2053,9 @@ ], "support": { "issues": "https://github.com/utopia-php/swoole/issues", - "source": "https://github.com/utopia-php/swoole/tree/0.2.3" + "source": "https://github.com/utopia-php/swoole/tree/0.2.4" }, - "time": "2021-03-22T22:39:24+00:00" + "time": "2021-06-22T10:49:24+00:00" }, { "name": "utopia-php/system", @@ -4819,7 +4819,6 @@ "type": "github" } ], - "abandoned": true, "time": "2020-09-28T06:45:17+00:00" }, { diff --git a/src/Appwrite/Realtime/Server.php b/src/Appwrite/Realtime/Server.php index bdf5ac2973..15c977cb40 100644 --- a/src/Appwrite/Realtime/Server.php +++ b/src/Appwrite/Realtime/Server.php @@ -211,24 +211,24 @@ class Server 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, function () use ($db) { - // return $db; - // }); - // $timeLimit - // ->setNamespace('app_' . $project->getId()) - // ->setParam('{ip}', $request->getIP()) - // ->setParam('{url}', $request->getURI()); + /* + * Abuse Check + * + * Abuse limits are connecting 128 times per minute and ip address. + */ + $timeLimit = new TimeLimit('url:{url},ip:{ip}', 128, 60, function () use (&$db) { + return $db; + }); + $timeLimit + ->setNamespace('app_' . $project->getId()) + ->setParam('{ip}', $request->getIP()) + ->setParam('{url}', $request->getURI()); - // $abuse = new Abuse($timeLimit); + $abuse = new Abuse($timeLimit); - // if ($abuse->check() && App::getEnv('_APP_OPTIONS_ABUSE', 'enabled') === 'enabled') { - // throw new Exception('Too many requests', 1013); - // } + 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. From b2aa23be114d22ebf9b65b46e2bb160459e5b3d6 Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Wed, 23 Jun 2021 17:13:01 +0200 Subject: [PATCH 151/267] update composer lock --- composer.lock | 52 +++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 50 insertions(+), 2 deletions(-) diff --git a/composer.lock b/composer.lock index fecb34fe5c..49c99de7c9 100644 --- a/composer.lock +++ b/composer.lock @@ -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": "43a04b49b8afe1a09d0e53a93c37d842", + "content-hash": "e2b6f0456d2d1a51f14fa8c5244b1024", "packages": [ { "name": "adhocore/jwt", @@ -4819,7 +4819,6 @@ "type": "github" } ], - "abandoned": true, "time": "2020-09-28T06:45:17+00:00" }, { @@ -5717,6 +5716,55 @@ ], "time": "2021-06-06T09:51:56+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.0", From e4be09ff19ad29bb05fefedf41c8a00cf1a6636a Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Wed, 23 Jun 2021 23:31:58 +0200 Subject: [PATCH 152/267] fix audit --- app/cli.php | 2 +- app/controllers/api/account.php | 7 ++++--- app/controllers/api/health.php | 16 ++++++++-------- app/controllers/api/users.php | 8 ++++---- app/http.php | 6 +++++- app/workers/certificates.php | 5 ++++- app/workers/deletes.php | 12 +++++++++--- app/workers/functions.php | 5 ++++- app/workers/tasks.php | 5 ++++- composer.lock | 14 +++++++------- src/Appwrite/Realtime/Server.php | 15 +++++---------- 11 files changed, 55 insertions(+), 40 deletions(-) diff --git a/app/cli.php b/app/cli.php index ced2fb8da4..ee6fe1a801 100644 --- a/app/cli.php +++ b/app/cli.php @@ -1,6 +1,6 @@ label('sdk.response.type', Response::CONTENT_TYPE_JSON) ->label('sdk.response.model', Response::MODEL_LOG_LIST) ->inject('response') - ->inject('register') ->inject('project') ->inject('user') ->inject('locale') ->inject('geodb') - ->action(function ($response, $register, $project, $user, $locale, $geodb) { + ->inject('app') + ->action(function ($response, $project, $user, $locale, $geodb, $app) { /** @var Appwrite\Utopia\Response $response */ /** @var Appwrite\Database\Document $project */ /** @var Appwrite\Database\Document $user */ /** @var Utopia\Locale\Locale $locale */ /** @var MaxMind\Db\Reader $geodb */ + /** @var Utopia\App $app */ - $adapter = new AuditAdapter($register->get('db')); + $adapter = new AuditAdapter($app->getResource('db')); $adapter->setNamespace('app_'.$project->getId()); $audit = new Audit($adapter); diff --git a/app/controllers/api/health.php b/app/controllers/api/health.php index dbbd59f8c3..ce8a765c8d 100644 --- a/app/controllers/api/health.php +++ b/app/controllers/api/health.php @@ -42,12 +42,12 @@ App::get('/v1/health/db') ->label('sdk.method', 'getDB') ->label('sdk.description', '/docs/references/health/get-db.md') ->inject('response') - ->inject('register') - ->action(function ($response, $register) { + ->inject('app') + ->action(function ($response, $app) { /** @var Appwrite\Utopia\Response $response */ - /** @var Utopia\Registry\Registry $register */ + /** @var Utopia\App $app */ - $register->get('db'); /* @var $db PDO */ + $app->getResource('db'); $response->json(['status' => 'OK']); }); @@ -61,11 +61,11 @@ App::get('/v1/health/cache') ->label('sdk.method', 'getCache') ->label('sdk.description', '/docs/references/health/get-cache.md') ->inject('response') - ->inject('register') - ->action(function ($response, $register) { + ->inject('app') + ->action(function ($response, $app) { /** @var Appwrite\Utopia\Response $response */ - /** @var Utopia\Registry\Registry $register */ - $register->get('cache'); /* @var $cache Predis\Client */ + /** @var Utopia\App $register */ + $app->getResource('cache'); $response->json(['status' => 'OK']); }); diff --git a/app/controllers/api/users.php b/app/controllers/api/users.php index 9dd13e34e8..a11c65edc5 100644 --- a/app/controllers/api/users.php +++ b/app/controllers/api/users.php @@ -232,18 +232,18 @@ App::get('/v1/users/:userId/logs') ->label('sdk.response.model', Response::MODEL_LOG_LIST) ->param('userId', '', new UID(), 'User unique ID.') ->inject('response') - ->inject('register') ->inject('project') ->inject('projectDB') ->inject('locale') ->inject('geodb') - ->action(function ($userId, $response, $register, $project, $projectDB, $locale, $geodb) { + ->inject('app') + ->action(function ($userId, $response, $project, $projectDB, $locale, $geodb, $app) { /** @var Appwrite\Utopia\Response $response */ - /** @var Utopia\Registry\Registry $register */ /** @var Appwrite\Database\Document $project */ /** @var Appwrite\Database\Database $projectDB */ /** @var Utopia\Locale\Locale $locale */ /** @var MaxMind\Db\Reader $geodb */ + /** @var Utopia\App $app */ $user = $projectDB->getDocument($userId); @@ -251,7 +251,7 @@ App::get('/v1/users/:userId/logs') throw new Exception('User not found', 404); } - $adapter = new AuditAdapter($register->get('db')); + $adapter = new AuditAdapter($app->getResource('db')); $adapter->setNamespace('app_'.$project->getId()); $audit = new Audit($adapter); diff --git a/app/http.php b/app/http.php index b32103a416..170e478d81 100644 --- a/app/http.php +++ b/app/http.php @@ -75,6 +75,8 @@ $http->on('request', function (SwooleRequest $swooleRequest, SwooleResponse $swo return; } + $app = new App('UTC'); + $db = $register->get('dbPool')->get(); $redis = $register->get('redisPool')->get(); @@ -86,7 +88,9 @@ $http->on('request', function (SwooleRequest $swooleRequest, SwooleResponse $swo return $redis; }); - $app = new App('UTC'); + App::setResource('app', function() use (&$app) { + return $app; + }); try { Authorization::cleanRoles(); diff --git a/app/workers/certificates.php b/app/workers/certificates.php index b44b2e44d9..bc746775c4 100644 --- a/app/workers/certificates.php +++ b/app/workers/certificates.php @@ -28,8 +28,11 @@ class CertificatesV1 extends Worker { global $register; + $db = $register->get('db'); + $cache = $register->get('cache'); + $consoleDB = new Database(); - $consoleDB->setAdapter(new RedisAdapter(new MySQLAdapter($register), $register)); + $consoleDB->setAdapter(new RedisAdapter(new MySQLAdapter($db, $cache), $cache)); $consoleDB->setNamespace('app_console'); // Main DB $consoleDB->setMocks(Config::getParam('collections', [])); diff --git a/app/workers/deletes.php b/app/workers/deletes.php index 006c38c6dc..f2931de319 100644 --- a/app/workers/deletes.php +++ b/app/workers/deletes.php @@ -358,9 +358,12 @@ class DeletesV1 extends Worker { global $register; + $db = $register->get('db'); + $cache = $register->get('cache'); + if($this->consoleDB === null) { $this->consoleDB = new Database(); - $this->consoleDB->setAdapter(new RedisAdapter(new MySQLAdapter($register), $register)); + $this->consoleDB->setAdapter(new RedisAdapter(new MySQLAdapter($db, $cache), $cache));; $this->consoleDB->setNamespace('app_console'); // Main DB $this->consoleDB->setMocks(Config::getParam('collections', [])); } @@ -374,9 +377,12 @@ class DeletesV1 extends Worker protected function getProjectDB($projectId): Database { global $register; - + + $db = $register->get('db'); + $cache = $register->get('cache'); + $projectDB = new Database(); - $projectDB->setAdapter(new RedisAdapter(new MySQLAdapter($register), $register)); + $projectDB->setAdapter(new RedisAdapter(new MySQLAdapter($db, $cache), $cache)); $projectDB->setNamespace('app_'.$projectId); // Main DB $projectDB->setMocks(Config::getParam('collections', [])); diff --git a/app/workers/functions.php b/app/workers/functions.php index 52920158e7..0c86a714c4 100644 --- a/app/workers/functions.php +++ b/app/workers/functions.php @@ -138,6 +138,9 @@ class FunctionsV1 extends Worker { global $register; + $db = $register->get('db'); + $cache = $register->get('cache'); + $projectId = $this->args['projectId'] ?? ''; $functionId = $this->args['functionId'] ?? ''; $webhooks = $this->args['webhooks'] ?? []; @@ -151,7 +154,7 @@ class FunctionsV1 extends Worker $jwt = $this->args['jwt'] ?? ''; $database = new Database(); - $database->setAdapter(new RedisAdapter(new MySQLAdapter($register), $register)); + $database->setAdapter(new RedisAdapter(new MySQLAdapter($db, $cache), $cache)); $database->setNamespace('app_'.$projectId); $database->setMocks(Config::getParam('collections', [])); diff --git a/app/workers/tasks.php b/app/workers/tasks.php index f9a9b6f5d9..0b1b33e234 100644 --- a/app/workers/tasks.php +++ b/app/workers/tasks.php @@ -30,8 +30,11 @@ class TasksV1 extends Worker { global $register; + $db = $register->get('db'); + $cache = $register->get('cache'); + $consoleDB = new Database(); - $consoleDB->setAdapter(new RedisAdapter(new MySQLAdapter($register), $register)); + $consoleDB->setAdapter(new RedisAdapter(new MySQLAdapter($db, $cache), $cache)); $consoleDB->setNamespace('app_console'); // Main DB $consoleDB->setMocks(Config::getParam('collections', [])); diff --git a/composer.lock b/composer.lock index 49c99de7c9..88953096c4 100644 --- a/composer.lock +++ b/composer.lock @@ -1431,21 +1431,21 @@ }, { "name": "utopia-php/audit", - "version": "0.5.1", + "version": "0.5.2", "source": { "type": "git", "url": "https://github.com/utopia-php/audit.git", - "reference": "154a850170a58667a15e4b65fbabb6cd0b709dd9" + "reference": "57e4f8f932164bdfd48ec32bf8d7d3f1bf7518e4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/audit/zipball/154a850170a58667a15e4b65fbabb6cd0b709dd9", - "reference": "154a850170a58667a15e4b65fbabb6cd0b709dd9", + "url": "https://api.github.com/repos/utopia-php/audit/zipball/57e4f8f932164bdfd48ec32bf8d7d3f1bf7518e4", + "reference": "57e4f8f932164bdfd48ec32bf8d7d3f1bf7518e4", "shasum": "" }, "require": { "ext-pdo": "*", - "php": ">=7.1" + "php": ">=7.4" }, "require-dev": { "phpunit/phpunit": "^9.3", @@ -1477,9 +1477,9 @@ ], "support": { "issues": "https://github.com/utopia-php/audit/issues", - "source": "https://github.com/utopia-php/audit/tree/0.5.1" + "source": "https://github.com/utopia-php/audit/tree/0.5.2" }, - "time": "2020-12-21T17:28:53+00:00" + "time": "2021-06-23T16:02:40+00:00" }, { "name": "utopia-php/cache", diff --git a/src/Appwrite/Realtime/Server.php b/src/Appwrite/Realtime/Server.php index 15c977cb40..d4ee4af420 100644 --- a/src/Appwrite/Realtime/Server.php +++ b/src/Appwrite/Realtime/Server.php @@ -402,18 +402,10 @@ class Server * This is redundant soon and will be gone with merging the usage branch. */ $db = $this->register->get('dbPool')->get(); - $redis = $this->register->get('redisPool')->get(); - - $this->register->set('db', function () use (&$db) { - return $db; - }); - - $this->register->set('cache', function () use (&$redis) { - return $redis; - }); + $cache = $this->register->get('redisPool')->get(); $projectDB = new Database(); - $projectDB->setAdapter(new RedisAdapter(new MySQLAdapter($this->register), $this->register)); + $projectDB->setAdapter(new RedisAdapter(new MySQLAdapter($db, $cache), $cache)); $projectDB->setNamespace('app_'.$project); $projectDB->setMocks(Config::getParam('collections', [])); @@ -424,5 +416,8 @@ class Server $roles = Parser::getRoles(); Parser::subscribe($project, $connection, $roles, $this->subscriptions, $this->connections, $this->connections[$connection]['channels']); + + $this->register->get('dbPool')->put($db); + $this->register->get('redisPool')->put($cache); } } From ad4ecea6731d03f8b99947aad9609846ced69146 Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Thu, 24 Jun 2021 12:13:13 +0200 Subject: [PATCH 153/267] fix(migration): database adapters --- app/tasks/migrate.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/tasks/migrate.php b/app/tasks/migrate.php index 7377de7959..57ec835dbc 100644 --- a/app/tasks/migrate.php +++ b/app/tasks/migrate.php @@ -17,13 +17,13 @@ $cli $consoleDB = new Database(); $consoleDB - ->setAdapter(new RedisAdapter(new MySQLAdapter($register), $register)) + ->setAdapter(new RedisAdapter(new MySQLAdapter($register->get('db'), $register->get('cache')), $register->get('cache'))) ->setNamespace('app_console') // Main DB ->setMocks(Config::getParam('collections', [])); $projectDB = new Database(); $projectDB - ->setAdapter(new RedisAdapter(new MySQLAdapter($register), $register)) + ->setAdapter(new RedisAdapter(new MySQLAdapter($register->get('db'), $register->get('cache')), $register->get('cache'))) ->setMocks(Config::getParam('collections', [])); $console = $consoleDB->getDocument('console'); From 923d373b6dd7159f62adc8763b1e7dc826295a62 Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Thu, 24 Jun 2021 14:22:32 +0200 Subject: [PATCH 154/267] introduce utopia-php/websocket --- app/init.php | 1 - app/preload.php | 1 + app/realtime.php | 330 ++++++++++++++- composer.json | 7 + composer.lock | 77 +++- docker-compose.yml | 18 +- src/Appwrite/Realtime/Server.php | 423 ------------------- tests/benchmarks/ws.js | 6 +- tests/e2e/Services/Realtime/RealtimeBase.php | 4 +- 9 files changed, 413 insertions(+), 454 deletions(-) delete mode 100644 src/Appwrite/Realtime/Server.php diff --git a/app/init.php b/app/init.php index d1b845aeb5..40c626a765 100644 --- a/app/init.php +++ b/app/init.php @@ -190,7 +190,6 @@ $register->set('redisPool', function () { ->withPort($redisPort) ->withAuth($redisAuth) ->withDbIndex(0) - ->withTimeout(1) ); return $pool; diff --git a/app/preload.php b/app/preload.php index 7c8ae00938..34d5f5a353 100644 --- a/app/preload.php +++ b/app/preload.php @@ -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); diff --git a/app/realtime.php b/app/realtime.php index 77c4f9fe2f..5c4ba4a12f 100644 --- a/app/realtime.php +++ b/app/realtime.php @@ -1,14 +1,332 @@ 64000 // Default maximum Package Size (64kb) -]; +$adapter = new Adapter\Swoole(port: App::getEnv('PORT', 80)); +$adapter->setPackageMaxLength(64000); // Default maximum Package Size (64kb) -$realtimeServer = new Server($register, port: App::getEnv('PORT', 80), config: $config); +$subscriptions = []; +$connections = []; + +$stats = new Table(4096, 1); +$stats->column('projectId', Table::TYPE_STRING, 64); +$stats->column('connections', Table::TYPE_INT); +$stats->column('connectionsTotal', Table::TYPE_INT); +$stats->column('messages', Table::TYPE_INT); +$stats->create(); + +$server = new Server($adapter); + +$server->onStart(function(SwooleServer $server) use ($stats) { + Console::success('Server started succefully'); + Console::info("Master pid {$server->master_pid}, manager pid {$server->manager_pid}"); + + Timer::tick(10000, function () use ($stats) { + 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, [ + 'projectId' => $projectId, + 'messages' => 0, + 'connections' => 0 + ]); + + if (App::getEnv('_APP_USAGE_STATS', 'enabled') == 'enabled') { + $usage->trigger(); + } + } + }); + + Process::signal(2, function () use ($server) { + Console::log('Stop by Ctrl+C'); + $server->shutdown(); + }); +}); + +$server->onWorkerStart(function(SwooleServer $swooleServer, int $workerId) use ($server, $register, $stats, &$subscriptions, &$connections) { + Console::success('Worker ' . $workerId . ' started succefully'); + + $attempts = 0; + $start = time(); + $redisPool = $register->get('redisPool'); + + /** + * Sending current connections to project channels on the console project every 5 seconds. + */ + Timer::tick(5000, function () use ($server, $stats, &$subscriptions) { + if ( + array_key_exists('console', $subscriptions) + && array_key_exists('role:member', $subscriptions['console']) + && array_key_exists('project', $subscriptions['console']['role:member']) + ) { + $payload = []; + foreach ($stats as $projectId => $value) { + $payload[$projectId] = $value['connectionsTotal']; + } + $server->send(array_keys($subscriptions['console']['role:member']['project']), json_encode([ + 'event' => 'stats.connections', + 'channels' => ['project'], + 'timestamp' => time(), + 'payload' => $payload + ])); + } + }); + + 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 = $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, $channel, $payload) use ($server, $workerId, $stats, $register, &$connections, &$subscriptions) { + $event = json_decode($payload, true); + + if ($event['permissionsChanged'] && isset($event['userId'])) { + $project = $event['project']; + $userId = $event['userId']; + + if (array_key_exists($project, $subscriptions) && array_key_exists('user:'.$userId, $subscriptions[$project])) { + $connection = array_key_first(reset($subscriptions[$project]['user:'.$userId])); + } else { + return; + } + + /** + * This is redundant soon and will be gone with merging the usage branch. + */ + $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_'.$project); + $projectDB->setMocks(Config::getParam('collections', [])); + + $user = $projectDB->getDocument($userId); + + Parser::setUser($user); + + $roles = Parser::getRoles(); + + Parser::subscribe($project, $connection, $roles, $subscriptions, $connections, $connections[$connection]['channels']); + + $register->get('dbPool')->put($db); + $register->get('redisPool')->put($cache); + } + + $receivers = Parser::identifyReceivers($event, $subscriptions); + + // Temporarily print debug logs by default for Alpha testing. + // if (App::isDevelopment() && !empty($receivers)) { + if (!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($event['data']) + ); + + if (($num = count($receivers)) > 0) { + $stats->incr($event['project'], 'messages', $num); + } + }); + } catch (\Throwable $th) { + Console::error('Pub/sub error: ' . $th->getMessage()); + $redisPool->put($redis); + $attempts++; + continue; + } + + $attempts++; + } + + Console::error('Failed to restart pub/sub...'); +}); + +$server->onOpen(function(SwooleServer $swooleServer, SwooleRequest $request) use ($server, $register, $stats, &$subscriptions, &$connections) { + $app = new App('UTC'); + $connection = $request->fd; + $request = new Request($request); + + /** @var PDO $db */ + $db = $register->get('dbPool')->get(); + /** @var Redis $redis */ + $redis = $register->get('redisPool')->get(); + + Console::info("Connection open (user: {$connection}, worker: {$swooleServer->getWorkerId()})"); + + 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 () { + return new Response(new SwooleResponse()); + }); + + 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, function () use (&$db) { + return $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); + } + + Parser::setUser($user); + + $roles = Parser::getRoles(); + $channels = Parser::parseChannels($request->getQuery('channels', [])); + + /** + * Channels Check + */ + if (empty($channels)) { + throw new Exception('Missing channels', 1008); + } + + Parser::subscribe($project->getId(), $connection, $roles, $subscriptions, $connections, $channels); + + $server->send([$connection], json_encode($channels)); + + $stats->incr($project->getId(), 'connections'); + $stats->incr($project->getId(), 'connectionsTotal'); + } catch (\Throwable $th) { + $response = [ + 'code' => $th->getCode(), + 'message' => $th->getMessage() + ]; + // Temporarily print debug logs by default for Alpha testing. + //if (App::isDevelopment()) { + Console::error("[Error] Connection Error"); + Console::error("[Error] Code: " . $response['code']); + Console::error("[Error] Message: " . $response['message']); + //} + $server->send([$connection], json_encode($response)); + $server->close($connection, $th->getCode()); + } finally { + /** + * Put used PDO and Redis Connections back into their pools. + */ + $register->get('dbPool')->put($db); + $register->get('redisPool')->put($redis); + } +}); + +$server->onMessage(function(SwooleServer $swooleServer, Frame $frame) use ($server) { + $connection = $frame->fd; + $server->send([$connection], 'Sending messages is not allowed.'); + $server->close($connection, 1003); +}); + +$server->onClose(function(SwooleServer $server, int $connection) use (&$connections, &$subscriptions, $stats) { + if (array_key_exists($connection, $connections)) { + $stats->decr($connections[$connection]['projectId'], 'connectionsTotal'); + } + Parser::unsubscribe($connection, $subscriptions, $connections); + Console::info('Connection close: ' . $connection); +}); + +$server->start(); \ No newline at end of file diff --git a/composer.json b/composer.json index 5d8d4a3776..5725a54069 100644 --- a/composer.json +++ b/composer.json @@ -52,6 +52,7 @@ "utopia-php/swoole": "0.2.*", "utopia-php/storage": "0.5.*", "utopia-php/image": "0.3.*", + "utopia-php/websocket": "dev-main", "resque/php-resque": "1.3.6", "matomo/device-detector": "4.2.2", "dragonmantank/cron-expression": "3.1.0", @@ -68,6 +69,12 @@ "phpunit/phpunit": "9.5.4", "vimeo/psalm": "4.7.2" }, + "repositories": [ + { + "type": "vcs", + "url": "https://github.com/utopia-php/websocket" + } + ], "provide": { "ext-phpiredis": "*" }, diff --git a/composer.lock b/composer.lock index ed4e782827..3d1d8d2ade 100644 --- a/composer.lock +++ b/composer.lock @@ -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": "ba882fb2e25e5d4304a6574986a6a7e9", + "content-hash": "3cf401f31fafb8aa517415681d53c516", "packages": [ { "name": "adhocore/jwt", @@ -115,16 +115,16 @@ }, { "name": "appwrite/php-runtimes", - "version": "dev-main", + "version": "0.3.0", "source": { "type": "git", "url": "https://github.com/appwrite/php-runtimes.git", - "reference": "cc7090a67d8824c779190b38873f0f8154f906b2" + "reference": "39be003cdff22c8447de151921001eb5d3bf2319" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/appwrite/php-runtimes/zipball/cc7090a67d8824c779190b38873f0f8154f906b2", - "reference": "cc7090a67d8824c779190b38873f0f8154f906b2", + "url": "https://api.github.com/repos/appwrite/php-runtimes/zipball/39be003cdff22c8447de151921001eb5d3bf2319", + "reference": "39be003cdff22c8447de151921001eb5d3bf2319", "shasum": "" }, "require": { @@ -136,7 +136,6 @@ "utopia-php/cli": "0.11.*", "vimeo/psalm": "4.0.1" }, - "default-branch": true, "type": "library", "autoload": { "psr-4": { @@ -145,7 +144,7 @@ }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "BSD-3" ], "authors": [ { @@ -165,9 +164,9 @@ ], "support": { "issues": "https://github.com/appwrite/php-runtimes/issues", - "source": "https://github.com/appwrite/php-runtimes/tree/main" + "source": "https://github.com/appwrite/php-runtimes/tree/0.3.0" }, - "time": "2021-06-23T07:17:12+00:00" + "time": "2021-06-15T07:52:43+00:00" }, { "name": "chillerlan/php-qrcode", @@ -2113,6 +2112,64 @@ }, "time": "2021-02-04T14:14:49+00:00" }, + { + "name": "utopia-php/websocket", + "version": "dev-main", + "source": { + "type": "git", + "url": "https://github.com/utopia-php/websocket.git", + "reference": "d08b0b1b29b7dea3c62d2ed3aab6ac872c382e76" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/utopia-php/websocket/zipball/d08b0b1b29b7dea3c62d2ed3aab6ac872c382e76", + "reference": "d08b0b1b29b7dea3c62d2ed3aab6ac872c382e76", + "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" + }, + "default-branch": true, + "type": "library", + "autoload": { + "psr-4": { + "Utopia\\WebSocket\\": "src/WebSocket" + } + }, + "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": { + "source": "https://github.com/utopia-php/websocket/tree/main", + "issues": "https://github.com/utopia-php/websocket/issues" + }, + "time": "2021-06-24T10:54:56+00:00" + }, { "name": "webmozart/assert", "version": "1.10.0", @@ -6054,7 +6111,7 @@ "aliases": [], "minimum-stability": "stable", "stability-flags": { - "appwrite/php-runtimes": 20 + "utopia-php/websocket": 20 }, "prefer-stable": false, "prefer-lowest": false, diff --git a/docker-compose.yml b/docker-compose.yml index 90e6c16d55..3240703938 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -527,15 +527,15 @@ services: networks: - appwrite - # redis-commander: - # image: rediscommander/redis-commander:latest - # restart: unless-stopped - # networks: - # - appwrite - # environment: - # - REDIS_HOSTS=redis - # ports: - # - "8081:8081" + redis-commander: + image: rediscommander/redis-commander:latest + restart: unless-stopped + networks: + - appwrite + environment: + - REDIS_HOSTS=redis + ports: + - "8081:8081" # resque: # image: appwrite/resque-web:1.1.0 diff --git a/src/Appwrite/Realtime/Server.php b/src/Appwrite/Realtime/Server.php deleted file mode 100644 index d4ee4af420..0000000000 --- a/src/Appwrite/Realtime/Server.php +++ /dev/null @@ -1,423 +0,0 @@ -subscriptions = []; - $this->connections = []; - $this->register = $register; - - $this->stats = new Table(4096, 1); - $this->stats->column('projectId', Table::TYPE_STRING, 64); - $this->stats->column('connections', Table::TYPE_INT); - $this->stats->column('connectionsTotal', Table::TYPE_INT); - $this->stats->column('messages', Table::TYPE_INT); - $this->stats->create(); - - $this->server = new SwooleServer($host, $port, SWOOLE_PROCESS); - $this->server->set($config); - $this->server->on('start', [$this, 'onStart']); - $this->server->on('workerStart', [$this, 'onWorkerStart']); - $this->server->on('open', [$this, 'onOpen']); - $this->server->on('message', [$this, 'onMessage']); - $this->server->on('close', [$this, 'onClose']); - $this->server->start(); - } - - /** - * This is executed when the Realtime server starts. - * @param SwooleServer $server - * @return void - */ - public function onStart(SwooleServer $server): void - { - Console::success('Server started succefully'); - Console::info("Master pid {$server->master_pid}, manager pid {$server->manager_pid}"); - - Timer::tick(10000, function () { - /** @var Table $stats */ - foreach ($this->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); - - $this->stats->set($projectId, [ - 'projectId' => $projectId, - 'messages' => 0, - 'connections' => 0 - ]); - - if (App::getEnv('_APP_USAGE_STATS', 'enabled') == 'enabled') { - $usage->trigger(); - } - } - }); - - Process::signal(2, function () use ($server) { - Console::log('Stop by Ctrl+C'); - $server->shutdown(); - }); - } - - /** - * This is executed when a WebSocket worker process starts. - * @param SwooleServer $server - * @param int $workerId - * @return void - * @throws Exception - */ - public function onWorkerStart(SwooleServer $server, int $workerId): void - { - Console::success('Worker ' . $workerId . ' started succefully'); - - $attempts = 0; - $start = time(); - $redisPool = $this->register->get('redisPool'); - - /** - * Sending current connections to project channels on the console project every 5 seconds. - */ - $server->tick(5000, function () use (&$server) { - $this->tickSendProjectUsage($server); - }); - - 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 - } - - /** @var Swoole\Coroutine\Redis $redis */ - $redis = $redisPool->get(); - - 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, $channel, $payload) use ($server, $workerId) { - $this->onRedisPublish($payload, $server, $workerId); - }); - } catch (\Throwable $th) { - Console::error('Pub/sub error: ' . $th->getMessage()); - $redisPool->put($redis); - $attempts++; - continue; - } - - $attempts++; - } - - Console::error('Failed to restart pub/sub...'); - } - - /** - * This is executed when a new Realtime connection is established. - * @param SwooleServer $server - * @param Request $request - * @return void - * @throws Exception - * @throws UtopiaException - */ - public function onOpen(SwooleServer $server, Request $request): void - { - $app = new App('UTC'); - $connection = $request->fd; - $request = new SwooleRequest($request); - - $db = $this->register->get('dbPool')->get(); - $redis = $this->register->get('redisPool')->get(); - - Console::info("Connection open (user: {$connection}, worker: {$server->getWorkerId()})"); - - 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 () { - return new Response(new SwooleResponse()); - }); - - 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, function () use (&$db) { - return $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); - } - - Parser::setUser($user); - - $roles = Parser::getRoles(); - $channels = Parser::parseChannels($request->getQuery('channels', [])); - - /** - * Channels Check - */ - if (empty($channels)) { - throw new Exception('Missing channels', 1008); - } - - Parser::subscribe($project->getId(), $connection, $roles, $this->subscriptions, $this->connections, $channels); - - $server->push($connection, json_encode($channels)); - - $this->stats->incr($project->getId(), 'connections'); - $this->stats->incr($project->getId(), 'connectionsTotal'); - } catch (\Throwable $th) { - $response = [ - 'code' => $th->getCode(), - 'message' => $th->getMessage() - ]; - // Temporarily print debug logs by default for Alpha testing. - //if (App::isDevelopment()) { - Console::error("[Error] Connection Error"); - Console::error("[Error] Code: " . $response['code']); - Console::error("[Error] Message: " . $response['message']); - //} - $server->push($connection, json_encode($response)); - $server->close($connection); - } - /** - * Put used PDO and Redis Connections back into their pools. - */ - /** @var PDOPool $dbPool */ - $dbPool = $this->register->get('dbPool'); - $dbPool->put($db); - - /** @var RedisPool $redisPool */ - $redisPool = $this->register->get('redisPool'); - $redisPool->put($redis); - } - - /** - * This is executed when a message is received by the Realtime server. - * @param SwooleServer $server - * @param Frame $frame - * @return void - */ - public function onMessage(SwooleServer $server, Frame $frame) - { - $server->push($frame->fd, 'Sending messages is not allowed.'); - $server->close($frame->fd); - } - - /** - * This is executed when a Realtime connection is closed. - * @param SwooleServer $server - * @param int $connection - * @return void - */ - public function onClose(SwooleServer $server, int $connection) - { - if (array_key_exists($connection, $this->connections)) { - $this->stats->decr($this->connections[$connection]['projectId'], 'connectionsTotal'); - } - Parser::unsubscribe($connection, $this->subscriptions, $this->connections); - Console::info('Connection close: ' . $connection); - } - - /** - * This is executed when an event is published on realtime channel in Redis. - * @param string $payload - * @param SwooleServer $server - * @param int $workerId - * @return void - */ - public function onRedisPublish(string $payload, SwooleServer &$server, int $workerId) - { - $event = json_decode($payload, true); - - if ($event['permissionsChanged'] && isset($event['userId'])) { - $this->addPermission($event); - } - - $receivers = Parser::identifyReceivers($event, $this->subscriptions); - - // Temporarily print debug logs by default for Alpha testing. - // if (App::isDevelopment() && !empty($receivers)) { - if (!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); - } - - foreach ($receivers as $receiver) { - if ($server->exist($receiver) && $server->isEstablished($receiver)) { - $server->push( - $receiver, - json_encode($event['data']), - SWOOLE_WEBSOCKET_OPCODE_TEXT, - SWOOLE_WEBSOCKET_FLAG_FIN | SWOOLE_WEBSOCKET_FLAG_COMPRESS - ); - } else { - $server->close($receiver); - } - } - if (($num = count($receivers)) > 0) { - $this->stats->incr($event['project'], 'messages', $num); - } - } - - /** - * This sends the usage to the `console` channel. - * @param SwooleServer $server - * @return void - */ - public function tickSendProjectUsage(SwooleServer &$server) - { - if ( - array_key_exists('console', $this->subscriptions) - && array_key_exists('role:member', $this->subscriptions['console']) - && array_key_exists('project', $this->subscriptions['console']['role:member']) - ) { - $payload = []; - foreach ($this->stats as $projectId => $value) { - $payload[$projectId] = $value['connectionsTotal']; - } - foreach ($this->subscriptions['console']['role:member']['project'] as $connection => $value) { - $server->push( - $connection, - json_encode([ - 'event' => 'stats.connections', - 'channels' => ['project'], - 'timestamp' => time(), - 'payload' => $payload - ]), - SWOOLE_WEBSOCKET_OPCODE_TEXT, - SWOOLE_WEBSOCKET_FLAG_FIN | SWOOLE_WEBSOCKET_FLAG_COMPRESS - ); - } - } - } - - private function addPermission(array $event) - { - $project = $event['project']; - $userId = $event['userId']; - - if (array_key_exists($project, $this->subscriptions) && array_key_exists('user:'.$userId, $this->subscriptions[$project])) { - $connection = array_key_first(reset($this->subscriptions[$project]['user:'.$userId])); - } else { - return; - } - - /** - * This is redundant soon and will be gone with merging the usage branch. - */ - $db = $this->register->get('dbPool')->get(); - $cache = $this->register->get('redisPool')->get(); - - $projectDB = new Database(); - $projectDB->setAdapter(new RedisAdapter(new MySQLAdapter($db, $cache), $cache)); - $projectDB->setNamespace('app_'.$project); - $projectDB->setMocks(Config::getParam('collections', [])); - - $user = $projectDB->getDocument($userId); - - Parser::setUser($user); - - $roles = Parser::getRoles(); - - Parser::subscribe($project, $connection, $roles, $this->subscriptions, $this->connections, $this->connections[$connection]['channels']); - - $this->register->get('dbPool')->put($db); - $this->register->get('redisPool')->put($cache); - } -} diff --git a/tests/benchmarks/ws.js b/tests/benchmarks/ws.js index 1f69fe00be..22754274f9 100644 --- a/tests/benchmarks/ws.js +++ b/tests/benchmarks/ws.js @@ -8,11 +8,11 @@ export let options = { stages: [ { duration: '10s', - target: 10 + target: 500 }, { - duration: '30m', - target: 10 + duration: '1m', + target: 500 }, ], } diff --git a/tests/e2e/Services/Realtime/RealtimeBase.php b/tests/e2e/Services/Realtime/RealtimeBase.php index c17f90b934..90d2fe6770 100644 --- a/tests/e2e/Services/Realtime/RealtimeBase.php +++ b/tests/e2e/Services/Realtime/RealtimeBase.php @@ -22,7 +22,7 @@ trait RealtimeBase ]; return new WebSocketClient('ws://appwrite-traefik/v1/realtime?' . http_build_query($query), [ 'headers' => $headers, - 'timeout' => 60, + 'timeout' => 30, ]); } @@ -622,7 +622,7 @@ trait RealtimeBase 'x-appwrite-key' => $this->getProject()['apiKey'] ]), [ 'name' => 'Test', - 'env' => 'php-7.4', + 'runtime' => 'php-7.4', 'execute' => ['*'], 'timeout' => 10, ]); From a77291dcb0da6b695a8bb9553285c55617d87a8a Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Thu, 24 Jun 2021 16:54:03 +0200 Subject: [PATCH 155/267] remove redis-commander --- docker-compose.yml | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 3240703938..90e6c16d55 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -527,15 +527,15 @@ services: networks: - appwrite - redis-commander: - image: rediscommander/redis-commander:latest - restart: unless-stopped - networks: - - appwrite - environment: - - REDIS_HOSTS=redis - ports: - - "8081:8081" + # redis-commander: + # image: rediscommander/redis-commander:latest + # restart: unless-stopped + # networks: + # - appwrite + # environment: + # - REDIS_HOSTS=redis + # ports: + # - "8081:8081" # resque: # image: appwrite/resque-web:1.1.0 From 06674982df6fea6bbf494e5d326659728c1dff56 Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Mon, 28 Jun 2021 12:18:00 +0200 Subject: [PATCH 156/267] refactor(realtime): move getRoles into Auth --- app/controllers/general.php | 16 +++----------- app/init.php | 6 ++---- app/realtime.php | 43 +++++++++++++++++++------------------ src/Appwrite/Auth/Auth.php | 27 +++++++++++++++++++++++ 4 files changed, 54 insertions(+), 38 deletions(-) diff --git a/app/controllers/general.php b/app/controllers/general.php index 5892aee47e..cd56666620 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -240,21 +240,11 @@ App::init(function ($utopia, $request, $response, $console, $project, $consoleDB } } - if ($user->getId()) { - Authorization::setRole('user:'.$user->getId()); - } - 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', [])); + foreach (Auth::getRoles($user) as $role) { + Authorization::setRole($role); + } // TDOO Check if user is root diff --git a/app/init.php b/app/init.php index 40c626a765..2606820347 100644 --- a/app/init.php +++ b/app/init.php @@ -432,8 +432,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 @@ -450,8 +449,7 @@ App::setResource('user', function($mode, $project, $console, $request, $response if (APP_MODE_ADMIN === $mode) { if (!empty($user->search('teamId', $project->getAttribute('teamId'), $user->getAttribute('memberships')))) { Authorization::setDefaultStatus(false); // Cancel security segmentation for admin users. - } - else { + } else { $user = new Document(['$id' => '', '$collection' => Database::SYSTEM_COLLECTION_USERS]); } } diff --git a/app/realtime.php b/app/realtime.php index 5c4ba4a12f..5b087aa96b 100644 --- a/app/realtime.php +++ b/app/realtime.php @@ -1,5 +1,6 @@ create(); $server = new Server($adapter); -$server->onStart(function(SwooleServer $server) use ($stats) { +$server->onStart(function (SwooleServer $server) use ($stats) { Console::success('Server started succefully'); Console::info("Master pid {$server->master_pid}, manager pid {$server->manager_pid}"); @@ -82,7 +83,7 @@ $server->onStart(function(SwooleServer $server) use ($stats) { }); }); -$server->onWorkerStart(function(SwooleServer $swooleServer, int $workerId) use ($server, $register, $stats, &$subscriptions, &$connections) { +$server->onWorkerStart(function (SwooleServer $swooleServer, int $workerId) use ($server, $register, $stats, &$subscriptions, &$connections) { Console::success('Worker ' . $workerId . ' started succefully'); $attempts = 0; @@ -107,7 +108,7 @@ $server->onWorkerStart(function(SwooleServer $swooleServer, int $workerId) use ( 'channels' => ['project'], 'timestamp' => time(), 'payload' => $payload - ])); + ])); } }); @@ -137,38 +138,38 @@ $server->onWorkerStart(function(SwooleServer $swooleServer, int $workerId) use ( if ($event['permissionsChanged'] && isset($event['userId'])) { $project = $event['project']; $userId = $event['userId']; - - if (array_key_exists($project, $subscriptions) && array_key_exists('user:'.$userId, $subscriptions[$project])) { - $connection = array_key_first(reset($subscriptions[$project]['user:'.$userId])); + + if (array_key_exists($project, $subscriptions) && array_key_exists('user:' . $userId, $subscriptions[$project])) { + $connection = array_key_first(reset($subscriptions[$project]['user:' . $userId])); } else { return; } - + /** * This is redundant soon and will be gone with merging the usage branch. */ $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_'.$project); + $projectDB->setNamespace('app_' . $project); $projectDB->setMocks(Config::getParam('collections', [])); - + $user = $projectDB->getDocument($userId); - + Parser::setUser($user); - - $roles = Parser::getRoles(); - + + $roles = Auth::getRoles($user); + Parser::subscribe($project, $connection, $roles, $subscriptions, $connections, $connections[$connection]['channels']); - + $register->get('dbPool')->put($db); $register->get('redisPool')->put($cache); } - + $receivers = Parser::identifyReceivers($event, $subscriptions); - + // Temporarily print debug logs by default for Alpha testing. // if (App::isDevelopment() && !empty($receivers)) { if (!empty($receivers)) { @@ -199,7 +200,7 @@ $server->onWorkerStart(function(SwooleServer $swooleServer, int $workerId) use ( Console::error('Failed to restart pub/sub...'); }); -$server->onOpen(function(SwooleServer $swooleServer, SwooleRequest $request) use ($server, $register, $stats, &$subscriptions, &$connections) { +$server->onOpen(function (SwooleServer $swooleServer, SwooleRequest $request) use ($server, $register, $stats, &$subscriptions, &$connections) { $app = new App('UTC'); $connection = $request->fd; $request = new Request($request); @@ -315,13 +316,13 @@ $server->onOpen(function(SwooleServer $swooleServer, SwooleRequest $request) use } }); -$server->onMessage(function(SwooleServer $swooleServer, Frame $frame) use ($server) { +$server->onMessage(function (SwooleServer $swooleServer, Frame $frame) use ($server) { $connection = $frame->fd; $server->send([$connection], 'Sending messages is not allowed.'); $server->close($connection, 1003); }); -$server->onClose(function(SwooleServer $server, int $connection) use (&$connections, &$subscriptions, $stats) { +$server->onClose(function (SwooleServer $server, int $connection) use (&$connections, &$subscriptions, $stats) { if (array_key_exists($connection, $connections)) { $stats->decr($connections[$connection]['projectId'], 'connectionsTotal'); } @@ -329,4 +330,4 @@ $server->onClose(function(SwooleServer $server, int $connection) use (&$connecti Console::info('Connection close: ' . $connection); }); -$server->start(); \ No newline at end of file +$server->start(); diff --git a/src/Appwrite/Auth/Auth.php b/src/Appwrite/Auth/Auth.php index 9fa181486e..ffdf1e631a 100644 --- a/src/Appwrite/Auth/Auth.php +++ b/src/Appwrite/Auth/Auth.php @@ -271,4 +271,31 @@ class Auth return false; } + + /** + * Returns all roles for a user. + * + * @param Document $user + * @return array + */ + public static function getRoles(Document $user): array + { + $roles = []; + + if ($user->getId()) { + $roles[] = 'user:'.$user->getId(); + } + + 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; + } } From 613d33321cdffe2ccc10d4ca565cde4564384a52 Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Mon, 28 Jun 2021 16:34:28 +0200 Subject: [PATCH 157/267] remove realtime Parser class --- app/realtime.php | 63 +++--- src/Appwrite/Auth/Auth.php | 4 +- src/Appwrite/Database/Pool.php | 21 -- src/Appwrite/Event/Realtime.php | 36 ++++ src/Appwrite/Messaging/Adapter.php | 10 + src/Appwrite/Messaging/Adapter/Realtime.php | 164 ++++++++++++++++ src/Appwrite/Realtime/Parser.php | 202 -------------------- 7 files changed, 244 insertions(+), 256 deletions(-) delete mode 100644 src/Appwrite/Database/Pool.php create mode 100644 src/Appwrite/Messaging/Adapter.php create mode 100644 src/Appwrite/Messaging/Adapter/Realtime.php delete mode 100644 src/Appwrite/Realtime/Parser.php diff --git a/app/realtime.php b/app/realtime.php index 5b087aa96b..5c8ed6dae1 100644 --- a/app/realtime.php +++ b/app/realtime.php @@ -5,8 +5,9 @@ use Appwrite\Database\Adapter\Redis as RedisAdapter; use Appwrite\Database\Adapter\MySQL as MySQLAdapter; use Appwrite\Database\Database; use Appwrite\Event\Event; +use Appwrite\Event\Realtime as RealtimeEvent; +use Appwrite\Messaging\Adapter\Realtime; use Appwrite\Network\Validator\Origin; -use Appwrite\Realtime\Parser; use Swoole\Http\Request as SwooleRequest; use Swoole\Http\Response as SwooleResponse; use Swoole\Process; @@ -44,6 +45,8 @@ $stats->create(); $server = new Server($adapter); +$realtime = new Realtime(); + $server->onStart(function (SwooleServer $server) use ($stats) { Console::success('Server started succefully'); Console::info("Master pid {$server->master_pid}, manager pid {$server->manager_pid}"); @@ -83,7 +86,7 @@ $server->onStart(function (SwooleServer $server) use ($stats) { }); }); -$server->onWorkerStart(function (SwooleServer $swooleServer, int $workerId) use ($server, $register, $stats, &$subscriptions, &$connections) { +$server->onWorkerStart(function (SwooleServer $swooleServer, int $workerId) use ($server, $register, $stats, $realtime) { Console::success('Worker ' . $workerId . ' started succefully'); $attempts = 0; @@ -93,22 +96,22 @@ $server->onWorkerStart(function (SwooleServer $swooleServer, int $workerId) use /** * Sending current connections to project channels on the console project every 5 seconds. */ - Timer::tick(5000, function () use ($server, $stats, &$subscriptions) { - if ( - array_key_exists('console', $subscriptions) - && array_key_exists('role:member', $subscriptions['console']) - && array_key_exists('project', $subscriptions['console']['role:member']) - ) { + Timer::tick(5000, function () use ($server, $stats, $realtime) { + if ($realtime->hasSubscriber('console', 'role:member', 'project')) { $payload = []; foreach ($stats as $projectId => $value) { $payload[$projectId] = $value['connectionsTotal']; } - $server->send(array_keys($subscriptions['console']['role:member']['project']), json_encode([ + + $event = [ 'event' => 'stats.connections', 'channels' => ['project'], + 'permissions' => ['role:member'], 'timestamp' => time(), 'payload' => $payload - ])); + ]; + + $server->send($realtime->getReceivers($event), json_encode($event)); } }); @@ -132,43 +135,38 @@ $server->onWorkerStart(function (SwooleServer $swooleServer, int $workerId) use Console::error('Pub/sub failed (worker: ' . $workerId . ')'); } - $redis->subscribe(['realtime'], function ($redis, $channel, $payload) use ($server, $workerId, $stats, $register, &$connections, &$subscriptions) { + $redis->subscribe(['realtime'], function ($redis, $channel, $payload) use ($server, $workerId, $stats, $register, $realtime) { $event = json_decode($payload, true); if ($event['permissionsChanged'] && isset($event['userId'])) { - $project = $event['project']; + $projectId = $event['project']; $userId = $event['userId']; - if (array_key_exists($project, $subscriptions) && array_key_exists('user:' . $userId, $subscriptions[$project])) { - $connection = array_key_first(reset($subscriptions[$project]['user:' . $userId])); + if ($realtime->hasSubscriber($projectId, 'user:' . $userId)) { + $connection = array_key_first(reset($realtime->subscriptions[$projectId]['user:' . $userId])); } else { return; } - /** - * This is redundant soon and will be gone with merging the usage branch. - */ $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_' . $project); + $projectDB->setNamespace('app_' . $projectId); $projectDB->setMocks(Config::getParam('collections', [])); $user = $projectDB->getDocument($userId); - Parser::setUser($user); - $roles = Auth::getRoles($user); - Parser::subscribe($project, $connection, $roles, $subscriptions, $connections, $connections[$connection]['channels']); + $realtime->subscribe($projectId, $connection, $roles, $realtime->connections[$connection]['channels']); $register->get('dbPool')->put($db); $register->get('redisPool')->put($cache); } - $receivers = Parser::identifyReceivers($event, $subscriptions); + $receivers = $realtime->getReceivers($event); // Temporarily print debug logs by default for Alpha testing. // if (App::isDevelopment() && !empty($receivers)) { @@ -200,7 +198,7 @@ $server->onWorkerStart(function (SwooleServer $swooleServer, int $workerId) use Console::error('Failed to restart pub/sub...'); }); -$server->onOpen(function (SwooleServer $swooleServer, SwooleRequest $request) use ($server, $register, $stats, &$subscriptions, &$connections) { +$server->onOpen(function (SwooleServer $swooleServer, SwooleRequest $request) use ($server, $register, $stats, &$realtime) { $app = new App('UTC'); $connection = $request->fd; $request = new Request($request); @@ -276,10 +274,12 @@ $server->onOpen(function (SwooleServer $swooleServer, SwooleRequest $request) us throw new Exception($originValidator->getDescription(), 1008); } - Parser::setUser($user); + $roles = [ + 'role:' . (($user->isEmpty()) ? Auth::USER_ROLE_GUEST : Auth::USER_ROLE_MEMBER), + ...Auth::getRoles($user) + ]; - $roles = Parser::getRoles(); - $channels = Parser::parseChannels($request->getQuery('channels', [])); + $channels = RealtimeEvent::convertChannels($request->getQuery('channels', []), $user); /** * Channels Check @@ -288,7 +288,7 @@ $server->onOpen(function (SwooleServer $swooleServer, SwooleRequest $request) us throw new Exception('Missing channels', 1008); } - Parser::subscribe($project->getId(), $connection, $roles, $subscriptions, $connections, $channels); + $realtime->subscribe($project->getId(), $connection, $roles, $channels); $server->send([$connection], json_encode($channels)); @@ -322,11 +322,12 @@ $server->onMessage(function (SwooleServer $swooleServer, Frame $frame) use ($ser $server->close($connection, 1003); }); -$server->onClose(function (SwooleServer $server, int $connection) use (&$connections, &$subscriptions, $stats) { - if (array_key_exists($connection, $connections)) { - $stats->decr($connections[$connection]['projectId'], 'connectionsTotal'); +$server->onClose(function (SwooleServer $server, int $connection) use ($realtime, $stats) { + if (array_key_exists($connection, $realtime->connections)) { + $stats->decr($realtime->connections[$connection]['projectId'], 'connectionsTotal'); } - Parser::unsubscribe($connection, $subscriptions, $connections); + $realtime->unsubscribe($connection); + Console::info('Connection close: ' . $connection); }); diff --git a/src/Appwrite/Auth/Auth.php b/src/Appwrite/Auth/Auth.php index ffdf1e631a..8dfe9cbd7f 100644 --- a/src/Appwrite/Auth/Auth.php +++ b/src/Appwrite/Auth/Auth.php @@ -280,10 +280,10 @@ class Auth */ public static function getRoles(Document $user): array { - $roles = []; - if ($user->getId()) { $roles[] = 'user:'.$user->getId(); + } else { + return []; } foreach ($user->getAttribute('memberships', []) as $node) { diff --git a/src/Appwrite/Database/Pool.php b/src/Appwrite/Database/Pool.php deleted file mode 100644 index ee8902d3d6..0000000000 --- a/src/Appwrite/Database/Pool.php +++ /dev/null @@ -1,21 +0,0 @@ -available = false; - while (!$this->pool->isEmpty()) { - $this->pool->pop(); - } - } -} diff --git a/src/Appwrite/Event/Realtime.php b/src/Appwrite/Event/Realtime.php index 5942064326..b113251500 100644 --- a/src/Appwrite/Event/Realtime.php +++ b/src/Appwrite/Event/Realtime.php @@ -121,6 +121,42 @@ class Realtime return $this->payload; } + /** + * 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 Document $user + * @return array + */ + public static function convertChannels(array $channels, Document $user): 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($user->getId())) { + $channels['account.' . $user->getId()] = $value; + } + unset($channels['account']); + break; + } + } + + if (\array_key_exists('account', $channels)) { + if ($user->getId()) { + $channels['account.' . $user->getId()] = $channels['account']; + } + unset($channels['account']); + } + + return $channels; + } + /** * Populate channels array based on the event name and payload. * diff --git a/src/Appwrite/Messaging/Adapter.php b/src/Appwrite/Messaging/Adapter.php new file mode 100644 index 0000000000..055e0e49d5 --- /dev/null +++ b/src/Appwrite/Messaging/Adapter.php @@ -0,0 +1,10 @@ + + * '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 subscribtion. + * @param string $projectId Project ID. + * @param mixed $connection Unique Identifier - Connection ID. + * @param array $roles Roles of the Subscription. + * @param array $channels Subscribed Channels. + * @return void + */ + public function subscribe(string $projectId, mixed $connection, 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][$connection] = true; + } + } + + $this->connections[$connection] = [ + '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 + { + 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 string $event + * @param array $payload + * @return void + */ + public function send(string $projectId, string $event, array $payload): void + { + $realtime = new EventRealtime($projectId, $event, $payload); + $realtime->trigger(); + } + + /** + * 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 getReceivers(array $event) + { + $receivers = []; + if (isset($this->subscriptions[$event['project']])) { + foreach ($this->subscriptions[$event['project']] as $role => $subscription) { + foreach ($event['data']['channels'] as $channel) { + if ( + \array_key_exists($channel, $this->subscriptions[$event['project']][$role]) + && (\in_array($role, $event['permissions']) || \in_array('*', $event['permissions'])) + ) { + foreach (array_keys($this->subscriptions[$event['project']][$role][$channel]) as $ids) { + $receivers[$ids] = 0; + } + break; + } + } + } + } + + return array_keys($receivers); + } +} diff --git a/src/Appwrite/Realtime/Parser.php b/src/Appwrite/Realtime/Parser.php deleted file mode 100644 index 8a5fd1bfcf..0000000000 --- a/src/Appwrite/Realtime/Parser.php +++ /dev/null @@ -1,202 +0,0 @@ -isEmpty()) ? Auth::USER_ROLE_GUEST : Auth::USER_ROLE_MEMBER)]; - if (!(self::$user->isEmpty())) { - $roles[] = 'user:' . self::$user->getId(); - } - foreach (self::$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; - } - - /** - * 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 - */ - static function parseChannels(array $channels) - { - $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(self::$user->getId())) { - $channels['account.' . self::$user->getId()] = $value; - } - unset($channels['account']); - break; - } - } - - if (\array_key_exists('account', $channels)) { - if (self::$user->getId()) { - $channels['account.' . self::$user->getId()] = $channels['account']; - } - unset($channels['account']); - } - - return $channels; - } - - /** - * 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 - * @param array $connections - * @param array $subscriptions - */ - static function identifyReceivers(array &$event, array &$subscriptions) - { - $receivers = []; - if (isset($subscriptions[$event['project']])) { - foreach ($subscriptions[$event['project']] as $role => $subscription) { - foreach ($event['data']['channels'] as $channel) { - if ( - \array_key_exists($channel, $subscriptions[$event['project']][$role]) - && (\in_array($role, $event['permissions']) || \in_array('*', $event['permissions'])) - ) { - foreach (array_keys($subscriptions[$event['project']][$role][$channel]) as $ids) { - $receivers[$ids] = 0; - } - break; - } - } - } - } - - return array_keys($receivers); - } - - /** - * Adds Subscription. - * - * @param string $projectId - * @param mixed $connection - * @param array $subscriptions - * @param array $roles - * @param array $channels - */ - static function subscribe($projectId, $connection, $roles, &$subscriptions, &$connections, &$channels) - { - /** - * Build Subscriptions 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] - */ - - if (!isset($subscriptions[$projectId])) { // Init Project - $subscriptions[$projectId] = []; - } - - foreach ($roles as $role) { - if (!isset($subscriptions[$projectId][$role])) { // Add user first connection - $subscriptions[$projectId][$role] = []; - } - - foreach ($channels as $channel => $list) { - $subscriptions[$projectId][$role][$channel][$connection] = true; - } - } - - $connections[$connection] = [ - 'projectId' => $projectId, - 'roles' => $roles, - 'channels' => $channels - ]; - } - - /** - * Remove Subscription. - * - * @param mixed $connection - * @param array $subscriptions - * @param array $connections - */ - static function unsubscribe($connection, &$subscriptions, &$connections) - { - $projectId = $connections[$connection]['projectId'] ?? ''; - $roles = $connections[$connection]['roles'] ?? []; - - foreach ($roles as $role) { - foreach ($subscriptions[$projectId][$role] as $channel => $list) { - unset($subscriptions[$projectId][$role][$channel][$connection]); // Remove connection - - if (empty($subscriptions[$projectId][$role][$channel])) { - unset($subscriptions[$projectId][$role][$channel]); // Remove channel when no connections - } - } - - if (empty($subscriptions[$projectId][$role])) { - unset($subscriptions[$projectId][$role]); // Remove role when no channels - } - } - - if (empty($subscriptions[$projectId])) { // Remove project when no roles - unset($subscriptions[$projectId]); - } - - unset($connections[$connection]); - } -} From 81066314524cc923fb7c5ed4d20688fbc27691ca Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Tue, 29 Jun 2021 13:34:48 +0200 Subject: [PATCH 158/267] update lock file --- composer.lock | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/composer.lock b/composer.lock index 9143ccbd0b..cbcec61c69 100644 --- a/composer.lock +++ b/composer.lock @@ -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": "f0045a975cc1e8215cdfef44472b702f", + "content-hash": "b1c7f49f79e05bcbc3b078242198b854", "packages": [ { "name": "adhocore/jwt", @@ -6113,7 +6113,9 @@ ], "aliases": [], "minimum-stability": "stable", - "stability-flags": [], + "stability-flags": { + "utopia-php/websocket": 20 + }, "prefer-stable": false, "prefer-lowest": false, "platform": { From e232b2a7232c57c1b2411bd43a0f6497fbe9568c Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Tue, 29 Jun 2021 15:11:14 +0200 Subject: [PATCH 159/267] fix tests --- tests/unit/Auth/AuthTest.php | 43 ++++ tests/unit/Event/RealtimeTest.php | 73 ++++++ .../MessagingChannelsTest.php} | 95 ++++---- tests/unit/Messaging/MessagingGuestTest.php | 128 ++++++++++ tests/unit/Messaging/MessagingTest.php | 135 +++++++++++ tests/unit/Realtime/RealtimeGuestTest.php | 191 --------------- tests/unit/Realtime/RealtimeTest.php | 220 ------------------ 7 files changed, 423 insertions(+), 462 deletions(-) create mode 100644 tests/unit/Event/RealtimeTest.php rename tests/unit/{Realtime/RealtimeChannelsTest.php => Messaging/MessagingChannelsTest.php} (77%) create mode 100644 tests/unit/Messaging/MessagingGuestTest.php create mode 100644 tests/unit/Messaging/MessagingTest.php delete mode 100644 tests/unit/Realtime/RealtimeGuestTest.php delete mode 100644 tests/unit/Realtime/RealtimeTest.php diff --git a/tests/unit/Auth/AuthTest.php b/tests/unit/Auth/AuthTest.php index 09d86d3eaf..0053ca3392 100644 --- a/tests/unit/Auth/AuthTest.php +++ b/tests/unit/Auth/AuthTest.php @@ -196,4 +196,47 @@ 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(0, $roles); + $this->assertEmpty($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(6, $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); + } } diff --git a/tests/unit/Event/RealtimeTest.php b/tests/unit/Event/RealtimeTest.php new file mode 100644 index 0000000000..e4aef27ed3 --- /dev/null +++ b/tests/unit/Event/RealtimeTest.php @@ -0,0 +1,73 @@ + '' + ]); + $channels = [ + 0 => 'files', + 1 => 'documents', + 2 => 'documents.789', + 3 => 'account', + 4 => 'account.456' + ]; + + $channels = Realtime::convertChannels($channels, $user); + $this->assertCount(3, $channels); + $this->assertArrayHasKey('files', $channels); + $this->assertArrayHasKey('documents', $channels); + $this->assertArrayHasKey('documents.789', $channels); + $this->assertArrayNotHasKey('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); + + $this->assertCount(4, $channels); + $this->assertArrayHasKey('files', $channels); + $this->assertArrayHasKey('documents', $channels); + $this->assertArrayHasKey('documents.789', $channels); + $this->assertArrayHasKey('account.123', $channels); + $this->assertArrayNotHasKey('account', $channels); + $this->assertArrayNotHasKey('account.456', $channels); + } +} diff --git a/tests/unit/Realtime/RealtimeChannelsTest.php b/tests/unit/Messaging/MessagingChannelsTest.php similarity index 77% rename from tests/unit/Realtime/RealtimeChannelsTest.php rename to tests/unit/Messaging/MessagingChannelsTest.php index 923a38325e..4159def554 100644 --- a/tests/unit/Realtime/RealtimeChannelsTest.php +++ b/tests/unit/Messaging/MessagingChannelsTest.php @@ -2,19 +2,21 @@ namespace Appwrite\Tests; +use Appwrite\Auth\Auth; use Appwrite\Database\Document; -use Appwrite\Realtime; +use Appwrite\Event\Realtime as EventRealtime; +use Appwrite\Messaging; +use Appwrite\Messaging\Adapter\Realtime; use PHPUnit\Framework\TestCase; -class RealtimeChannelsTest extends TestCase +class MessagingChannelsTest extends TestCase { /** * Configures how many Connections the Test should Mock. */ public $connectionsPerChannel = 10; - public $connections = []; - public $subscriptions = []; + public Realtime $realtime; public $connectionsCount = 0; public $connectionsAuthenticated = 0; public $connectionsGuest = 0; @@ -41,12 +43,14 @@ class RealtimeChannelsTest extends TestCase $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) { - Realtime\Parser::setUser(new Document([ + $user = new Document([ '$id' => 'user' . $this->connectionsCount, 'memberships' => [ [ @@ -56,16 +60,19 @@ class RealtimeChannelsTest extends TestCase ] ] ] - ])); - $roles = Realtime\Parser::getRoles(); - $parsedChannels = Realtime\Parser::parseChannels([0 => $channel]); + ]); - Realtime\Parser::subscribe( + $roles = [ + 'role:' . (($user->isEmpty()) ? Auth::USER_ROLE_GUEST : Auth::USER_ROLE_MEMBER), + ...Auth::getRoles($user) + ]; + + $parsedChannels = EventRealtime::convertChannels([0 => $channel], $user); + + $this->realtime->subscribe( '1', $this->connectionsCount, $roles, - $this->subscriptions, - $this->connections, $parsedChannels ); @@ -78,19 +85,21 @@ class RealtimeChannelsTest extends TestCase */ for ($i = 0; $i < $this->connectionsPerChannel; $i++) { foreach ($this->allChannels as $index => $channel) { - Realtime\Parser::setUser(new Document([ + $user = new Document([ '$id' => '' - ])); + ]); - $roles = Realtime\Parser::getRoles(); - $parsedChannels = Realtime\Parser::parseChannels([0 => $channel]); + $roles = [ + 'role:' . (($user->isEmpty()) ? Auth::USER_ROLE_GUEST : Auth::USER_ROLE_MEMBER), + ...Auth::getRoles($user) + ]; - Realtime\Parser::subscribe( + $parsedChannels = EventRealtime::convertChannels([0 => $channel], $user); + + $this->realtime->subscribe( '1', $this->connectionsCount, $roles, - $this->subscriptions, - $this->connections, $parsedChannels ); @@ -101,8 +110,7 @@ class RealtimeChannelsTest extends TestCase public function tearDown(): void { - $this->connections = []; - $this->subscriptions = []; + unset($this->realtime); $this->connectionsCount = 0; } @@ -111,7 +119,7 @@ class RealtimeChannelsTest extends TestCase /** * Check for 1 project. */ - $this->assertCount(1, $this->subscriptions); + $this->assertCount(1, $this->realtime->subscriptions); /** * Check for correct amount of subscriptions: @@ -121,28 +129,28 @@ class RealtimeChannelsTest extends TestCase * - 1 role:guest * - 1 role:member */ - $this->assertCount(($this->connectionsAuthenticated + (3 * $this->connectionsPerChannel) + 2), $this->subscriptions['1']); + $this->assertCount(($this->connectionsAuthenticated + (3 * $this->connectionsPerChannel) + 2), $this->realtime->subscriptions['1']); /** * Check for connections * - Authenticated * - Guests */ - $this->assertCount($this->connectionsTotal, $this->connections); + $this->assertCount($this->connectionsTotal, $this->realtime->connections); + + $this->realtime->unsubscribe(-1); - Realtime\Parser::unsubscribe(-1, $this->subscriptions, $this->connections); - - $this->assertCount($this->connectionsTotal, $this->connections); - $this->assertCount(($this->connectionsAuthenticated + (3 * $this->connectionsPerChannel) + 2), $this->subscriptions['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++) { - Realtime\Parser::unsubscribe($i, $this->subscriptions, $this->connections); + $this->realtime->unsubscribe($i); - $this->assertCount(($this->connectionsCount - $i - 1), $this->connections); + $this->assertCount(($this->connectionsCount - $i - 1), $this->realtime->connections); } - $this->assertEmpty($this->connections); - $this->assertEmpty($this->subscriptions); + $this->assertEmpty($this->realtime->connections); + $this->assertEmpty($this->realtime->subscriptions); } /** @@ -161,10 +169,7 @@ class RealtimeChannelsTest extends TestCase ] ]; - $receivers = Realtime\Parser::identifyReceivers( - $event, - $this->subscriptions - ); + $receivers = $this->realtime->getReceivers($event); /** * Every Client subscribed to the Wildcard should receive this event. @@ -197,10 +202,7 @@ class RealtimeChannelsTest extends TestCase ] ]; - $receivers = Realtime\Parser::identifyReceivers( - $event, - $this->subscriptions - ); + $receivers = $this->realtime->getReceivers($event); /** * Every Role subscribed to a Channel should receive this event. @@ -234,10 +236,7 @@ class RealtimeChannelsTest extends TestCase ] ]; - $receivers = Realtime\Parser::identifyReceivers( - $event, - $this->subscriptions - ); + $receivers = $this->realtime->getReceivers($event); /** * Every Client subscribed to a Channel should receive this event. @@ -271,10 +270,7 @@ class RealtimeChannelsTest extends TestCase ] ]; - $receivers = Realtime\Parser::identifyReceivers( - $event, - $this->subscriptions - ); + $receivers = $this->realtime->getReceivers($event); /** * Every Team Member should receive this event. @@ -300,10 +296,7 @@ class RealtimeChannelsTest extends TestCase ] ]; - $receivers = Realtime\Parser::identifyReceivers( - $event, - $this->subscriptions - ); + $receivers = $this->realtime->getReceivers($event); /** * Only 1 Team Member of a role should have access to a specific channel. diff --git a/tests/unit/Messaging/MessagingGuestTest.php b/tests/unit/Messaging/MessagingGuestTest.php new file mode 100644 index 0000000000..804116604d --- /dev/null +++ b/tests/unit/Messaging/MessagingGuestTest.php @@ -0,0 +1,128 @@ +subscribe( + '1', + 1, + ['role:guest'], + ['files' => 0, 'documents' => 0, 'documents.789' => 0, 'account.123' => 0] + ); + + $event = [ + 'project' => '1', + 'permissions' => ['*'], + 'data' => [ + 'channels' => [ + 0 => 'documents', + 1 => 'documents', + ] + ] + ]; + + $receivers = $realtime->getReceivers($event); + + $this->assertCount(1, $receivers); + $this->assertEquals(1, $receivers[0]); + + $event['permissions'] = ['role:guest']; + + $receivers = $realtime->getReceivers($event); + + $this->assertCount(1, $receivers); + $this->assertEquals(1, $receivers[0]); + + $event['permissions'] = ['role:member']; + + $receivers = $realtime->getReceivers($event); + + $this->assertEmpty($receivers); + + $event['permissions'] = ['user:123']; + + $receivers = $realtime->getReceivers($event); + + $this->assertEmpty($receivers); + + $event['permissions'] = ['team:abc']; + + $receivers = $realtime->getReceivers($event); + + $this->assertEmpty($receivers); + + $event['permissions'] = ['team:abc/administrator']; + + $receivers = $realtime->getReceivers($event); + + $this->assertEmpty($receivers); + + $event['permissions'] = ['team:abc/god']; + + $receivers = $realtime->getReceivers($event); + + $this->assertEmpty($receivers); + + $event['permissions'] = ['team:def']; + + $receivers = $realtime->getReceivers($event); + + $this->assertEmpty($receivers); + + $event['permissions'] = ['team:def/guest']; + + $receivers = $realtime->getReceivers($event); + + $this->assertEmpty($receivers); + + $event['permissions'] = ['user:456']; + + $receivers = $realtime->getReceivers($event); + + $this->assertEmpty($receivers); + + $event['permissions'] = ['team:def/member']; + + $receivers = $realtime->getReceivers($event); + + $this->assertEmpty($receivers); + + $event['permissions'] = ['*']; + $event['data']['channels'] = ['documents.123']; + + $receivers = $realtime->getReceivers($event); + + $this->assertEmpty($receivers); + + $event['data']['channels'] = ['documents.789']; + + $receivers = $realtime->getReceivers($event); + + $this->assertCount(1, $receivers); + $this->assertEquals(1, $receivers[0]); + + $event['project'] = '2'; + + $receivers = $realtime->getReceivers($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); + } +} diff --git a/tests/unit/Messaging/MessagingTest.php b/tests/unit/Messaging/MessagingTest.php new file mode 100644 index 0000000000..6cd6a69675 --- /dev/null +++ b/tests/unit/Messaging/MessagingTest.php @@ -0,0 +1,135 @@ +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', + 'permissions' => ['*'], + 'data' => [ + 'channels' => [ + 0 => 'account.123', + ] + ] + ]; + + $receivers = $realtime->getReceivers($event); + + $this->assertCount(1, $receivers); + $this->assertEquals(1, $receivers[0]); + + $event['permissions'] = ['role:member']; + + $receivers = $realtime->getReceivers($event); + + $this->assertCount(1, $receivers); + $this->assertEquals(1, $receivers[0]); + + $event['permissions'] = ['user:123']; + + $receivers = $realtime->getReceivers($event); + + $this->assertCount(1, $receivers); + $this->assertEquals(1, $receivers[0]); + + $event['permissions'] = ['team:abc']; + + $receivers = $realtime->getReceivers($event); + + $this->assertCount(1, $receivers); + $this->assertEquals(1, $receivers[0]); + + $event['permissions'] = ['team:abc/administrator']; + + $receivers = $realtime->getReceivers($event); + + $this->assertCount(1, $receivers); + $this->assertEquals(1, $receivers[0]); + + $event['permissions'] = ['team:abc/moderator']; + + $receivers = $realtime->getReceivers($event); + + $this->assertCount(1, $receivers); + $this->assertEquals(1, $receivers[0]); + + $event['permissions'] = ['team:def']; + + $receivers = $realtime->getReceivers($event); + + $this->assertCount(1, $receivers); + $this->assertEquals(1, $receivers[0]); + + $event['permissions'] = ['team:def/guest']; + + $receivers = $realtime->getReceivers($event); + + $this->assertCount(1, $receivers); + $this->assertEquals(1, $receivers[0]); + + $event['permissions'] = ['user:456']; + + $receivers = $realtime->getReceivers($event); + + $this->assertEmpty($receivers); + + $event['permissions'] = ['team:def/member']; + + $receivers = $realtime->getReceivers($event); + + $this->assertEmpty($receivers); + + $event['permissions'] = ['*']; + $event['data']['channels'] = ['documents.123']; + + $receivers = $realtime->getReceivers($event); + + $this->assertEmpty($receivers); + + $event['data']['channels'] = ['documents.789']; + + $receivers = $realtime->getReceivers($event); + + $this->assertCount(1, $receivers); + $this->assertEquals(1, $receivers[0]); + + $event['project'] = '2'; + + $receivers = $realtime->getReceivers($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); + } +} diff --git a/tests/unit/Realtime/RealtimeGuestTest.php b/tests/unit/Realtime/RealtimeGuestTest.php deleted file mode 100644 index 01e43d7308..0000000000 --- a/tests/unit/Realtime/RealtimeGuestTest.php +++ /dev/null @@ -1,191 +0,0 @@ - '' - ])); - - $roles = Realtime\Parser::getRoles(); - $this->assertCount(1, $roles); - $this->assertContains('role:guest', $roles); - - $channels = [ - 0 => 'files', - 1 => 'documents', - 2 => 'documents.789', - 3 => 'account', - 4 => 'account.456' - ]; - - $channels = Realtime\Parser::parseChannels($channels); - $this->assertCount(3, $channels); - $this->assertArrayHasKey('files', $channels); - $this->assertArrayHasKey('documents', $channels); - $this->assertArrayHasKey('documents.789', $channels); - $this->assertArrayNotHasKey('account', $channels); - $this->assertArrayNotHasKey('account.456', $channels); - - Realtime\Parser::subscribe('1', 1, $roles, $this->subscriptions, $this->connections, $channels); - - $event = [ - 'project' => '1', - 'permissions' => ['*'], - 'data' => [ - 'channels' => [ - 0 => 'documents', - 1 => 'documents', - ] - ] - ]; - - $receivers = Realtime\Parser::identifyReceivers( - $event, - $this->subscriptions - ); - - $this->assertCount(1, $receivers); - $this->assertEquals(1, $receivers[0]); - - $event['permissions'] = ['role:guest']; - - $receivers = Realtime\Parser::identifyReceivers( - $event, - $this->subscriptions - ); - - $this->assertCount(1, $receivers); - $this->assertEquals(1, $receivers[0]); - - $event['permissions'] = ['role:member']; - - $receivers = Realtime\Parser::identifyReceivers( - $event, - $this->subscriptions - ); - - $this->assertEmpty($receivers); - - $event['permissions'] = ['user:123']; - - $receivers = Realtime\Parser::identifyReceivers( - $event, - $this->subscriptions - ); - - $this->assertEmpty($receivers); - - $event['permissions'] = ['team:abc']; - - $receivers = Realtime\Parser::identifyReceivers( - $event, - $this->subscriptions - ); - - $this->assertEmpty($receivers); - - $event['permissions'] = ['team:abc/administrator']; - - $receivers = Realtime\Parser::identifyReceivers( - $event, - $this->subscriptions - ); - - $this->assertEmpty($receivers); - - $event['permissions'] = ['team:abc/god']; - - $receivers = Realtime\Parser::identifyReceivers( - $event, - $this->subscriptions - ); - - $this->assertEmpty($receivers); - - $event['permissions'] = ['team:def']; - - $receivers = Realtime\Parser::identifyReceivers( - $event, - $this->subscriptions - ); - - $this->assertEmpty($receivers); - - $event['permissions'] = ['team:def/guest']; - - $receivers = Realtime\Parser::identifyReceivers( - $event, - $this->subscriptions - ); - - $this->assertEmpty($receivers); - - $event['permissions'] = ['user:456']; - - $receivers = Realtime\Parser::identifyReceivers( - $event, - $this->subscriptions - ); - - $this->assertEmpty($receivers); - - $event['permissions'] = ['team:def/member']; - - $receivers = Realtime\Parser::identifyReceivers( - $event, - $this->subscriptions - ); - - $this->assertEmpty($receivers); - - $event['permissions'] = ['*']; - $event['data']['channels'] = ['documents.123']; - - $receivers = Realtime\Parser::identifyReceivers( - $event, - $this->subscriptions - ); - - $this->assertEmpty($receivers); - - $event['data']['channels'] = ['documents.789']; - - $receivers = Realtime\Parser::identifyReceivers( - $event, - $this->subscriptions - ); - - $this->assertCount(1, $receivers); - $this->assertEquals(1, $receivers[0]); - - $event['project'] = '2'; - - $receivers = Realtime\Parser::identifyReceivers( - $event, - $this->subscriptions - ); - - $this->assertEmpty($receivers); - - Realtime\Parser::unsubscribe(2, $this->subscriptions, $this->connections); - - $this->assertCount(1, $this->connections); - $this->assertCount(1, $this->subscriptions['1']); - - Realtime\Parser::unsubscribe(1, $this->subscriptions, $this->connections); - - $this->assertEmpty($this->connections); - $this->assertEmpty($this->subscriptions); - } -} diff --git a/tests/unit/Realtime/RealtimeTest.php b/tests/unit/Realtime/RealtimeTest.php deleted file mode 100644 index a722bea10d..0000000000 --- a/tests/unit/Realtime/RealtimeTest.php +++ /dev/null @@ -1,220 +0,0 @@ - '123', - 'memberships' => [ - [ - 'teamId' => 'abc', - 'roles' => [ - 'administrator', - 'god' - ] - ], - [ - 'teamId' => 'def', - 'roles' => [ - 'guest' - ] - ] - ] - ])); - - $roles = Realtime\Parser::getRoles(); - - $this->assertCount(7, $roles); - $this->assertContains('user:123', $roles); - $this->assertContains('role:member', $roles); - $this->assertContains('team:abc', $roles); - $this->assertContains('team:abc/administrator', $roles); - $this->assertContains('team:abc/god', $roles); - $this->assertContains('team:def', $roles); - $this->assertContains('team:def/guest', $roles); - - $channels = [ - 0 => 'files', - 1 => 'documents', - 2 => 'documents.789', - 3 => 'account', - 4 => 'account.456' - ]; - - $channels = Realtime\Parser::parseChannels($channels); - - $this->assertCount(4, $channels); - $this->assertArrayHasKey('files', $channels); - $this->assertArrayHasKey('documents', $channels); - $this->assertArrayHasKey('documents.789', $channels); - $this->assertArrayHasKey('account.123', $channels); - $this->assertArrayNotHasKey('account', $channels); - $this->assertArrayNotHasKey('account.456', $channels); - - Realtime\Parser::subscribe('1', 1, $roles, $this->subscriptions, $this->connections, $channels); - - $event = [ - 'project' => '1', - 'permissions' => ['*'], - 'data' => [ - 'channels' => [ - 0 => 'account.123', - ] - ] - ]; - - $receivers = Realtime\Parser::identifyReceivers( - $event, - $this->subscriptions - ); - - $this->assertCount(1, $receivers); - $this->assertEquals(1, $receivers[0]); - - $event['permissions'] = ['role:member']; - - $receivers = Realtime\Parser::identifyReceivers( - $event, - $this->subscriptions - ); - - $this->assertCount(1, $receivers); - $this->assertEquals(1, $receivers[0]); - - $event['permissions'] = ['user:123']; - - $receivers = Realtime\Parser::identifyReceivers( - $event, - $this->subscriptions - ); - - $this->assertCount(1, $receivers); - $this->assertEquals(1, $receivers[0]); - - $event['permissions'] = ['team:abc']; - - $receivers = Realtime\Parser::identifyReceivers( - $event, - $this->subscriptions - ); - - $this->assertCount(1, $receivers); - $this->assertEquals(1, $receivers[0]); - - $event['permissions'] = ['team:abc/administrator']; - - $receivers = Realtime\Parser::identifyReceivers( - $event, - $this->subscriptions - ); - - $this->assertCount(1, $receivers); - $this->assertEquals(1, $receivers[0]); - - $event['permissions'] = ['team:abc/god']; - - $receivers = Realtime\Parser::identifyReceivers( - $event, - $this->subscriptions - ); - - $this->assertCount(1, $receivers); - $this->assertEquals(1, $receivers[0]); - - $event['permissions'] = ['team:def']; - - $receivers = Realtime\Parser::identifyReceivers( - $event, - $this->subscriptions - ); - - $this->assertCount(1, $receivers); - $this->assertEquals(1, $receivers[0]); - - $event['permissions'] = ['team:def/guest']; - - $receivers = Realtime\Parser::identifyReceivers( - $event, - $this->subscriptions - ); - - $this->assertCount(1, $receivers); - $this->assertEquals(1, $receivers[0]); - - $event['permissions'] = ['user:456']; - - $receivers = Realtime\Parser::identifyReceivers( - $event, - $this->subscriptions - ); - - $this->assertEmpty($receivers); - - $event['permissions'] = ['team:def/member']; - - $receivers = Realtime\Parser::identifyReceivers( - $event, - $this->subscriptions - ); - - $this->assertEmpty($receivers); - - $event['permissions'] = ['*']; - $event['data']['channels'] = ['documents.123']; - - $receivers = Realtime\Parser::identifyReceivers( - $event, - $this->subscriptions - ); - - $this->assertEmpty($receivers); - - $event['data']['channels'] = ['documents.789']; - - $receivers = Realtime\Parser::identifyReceivers( - $event, - $this->subscriptions - ); - - $this->assertCount(1, $receivers); - $this->assertEquals(1, $receivers[0]); - - $event['project'] = '2'; - - $receivers = Realtime\Parser::identifyReceivers( - $event, - $this->subscriptions - ); - - $this->assertEmpty($receivers); - - Realtime\Parser::unsubscribe(2, $this->subscriptions, $this->connections); - - $this->assertCount(1, $this->connections); - $this->assertCount(7, $this->subscriptions['1']); - - - Realtime\Parser::unsubscribe(1, $this->subscriptions, $this->connections); - - $this->assertEmpty($this->connections); - $this->assertEmpty($this->subscriptions); - } -} From caddb130085920abc66b77aba6bcfa70d904ef9d Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Tue, 29 Jun 2021 18:22:06 +0200 Subject: [PATCH 160/267] add websocket branch --- composer.json | 6 +++--- composer.lock | 19 ++++--------------- 2 files changed, 7 insertions(+), 18 deletions(-) diff --git a/composer.json b/composer.json index 82c635eaf3..474f5b3589 100644 --- a/composer.json +++ b/composer.json @@ -51,7 +51,7 @@ "utopia-php/domains": "1.1.*", "utopia-php/swoole": "0.2.*", "utopia-php/storage": "0.5.*", - "utopia-php/websocket": "dev-main", + "utopia-php/websocket": "dev-fix-adapter-interface", "utopia-php/image": "0.5.*", "resque/php-resque": "1.3.6", "matomo/device-detector": "4.2.3", @@ -71,8 +71,8 @@ }, "repositories": [ { - "type": "vcs", - "url": "https://github.com/utopia-php/websocket" + "type": "git", + "url": "https://github.com/utopia-php/websocket.git" } ], "provide": { diff --git a/composer.lock b/composer.lock index cbcec61c69..d253fd6dfe 100644 --- a/composer.lock +++ b/composer.lock @@ -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": "b1c7f49f79e05bcbc3b078242198b854", + "content-hash": "9ad1f9e6ce5583eb13ea6ffa9a93cb97", "packages": [ { "name": "adhocore/jwt", @@ -2117,17 +2117,11 @@ }, { "name": "utopia-php/websocket", - "version": "dev-main", + "version": "dev-fix-adapter-interface", "source": { "type": "git", "url": "https://github.com/utopia-php/websocket.git", - "reference": "d08b0b1b29b7dea3c62d2ed3aab6ac872c382e76" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/utopia-php/websocket/zipball/d08b0b1b29b7dea3c62d2ed3aab6ac872c382e76", - "reference": "d08b0b1b29b7dea3c62d2ed3aab6ac872c382e76", - "shasum": "" + "reference": "0cff9078e8acdb99f2bb8b30a578d0137bd460de" }, "require": { "php": ">=8.0" @@ -2139,7 +2133,6 @@ "vimeo/psalm": "^4.8.1", "workerman/workerman": "^4.0" }, - "default-branch": true, "type": "library", "autoload": { "psr-4": { @@ -2167,11 +2160,7 @@ "utopia", "websocket" ], - "support": { - "source": "https://github.com/utopia-php/websocket/tree/main", - "issues": "https://github.com/utopia-php/websocket/issues" - }, - "time": "2021-06-24T10:54:56+00:00" + "time": "2021-06-29T16:04:32+00:00" }, { "name": "webmozart/assert", From ef919c0395d75b90ba49c23d99f38a9eb84a5b7e Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Tue, 29 Jun 2021 18:22:10 +0200 Subject: [PATCH 161/267] fix timelimit --- app/realtime.php | 27 +++++++-------------------- 1 file changed, 7 insertions(+), 20 deletions(-) diff --git a/app/realtime.php b/app/realtime.php index 5c8ed6dae1..3f0fed5087 100644 --- a/app/realtime.php +++ b/app/realtime.php @@ -10,12 +10,9 @@ use Appwrite\Messaging\Adapter\Realtime; use Appwrite\Network\Validator\Origin; use Swoole\Http\Request as SwooleRequest; use Swoole\Http\Response as SwooleResponse; -use Swoole\Process; use Swoole\Runtime; use Swoole\Table; use Swoole\Timer; -use Swoole\WebSocket\Frame; -use Swoole\WebSocket\Server as SwooleServer; use Utopia\Abuse\Abuse; use Utopia\Abuse\Adapters\TimeLimit; use Utopia\App; @@ -47,9 +44,8 @@ $server = new Server($adapter); $realtime = new Realtime(); -$server->onStart(function (SwooleServer $server) use ($stats) { +$server->onStart(function () use ($stats) { Console::success('Server started succefully'); - Console::info("Master pid {$server->master_pid}, manager pid {$server->manager_pid}"); Timer::tick(10000, function () use ($stats) { foreach ($stats as $projectId => $value) { @@ -79,14 +75,9 @@ $server->onStart(function (SwooleServer $server) use ($stats) { } } }); - - Process::signal(2, function () use ($server) { - Console::log('Stop by Ctrl+C'); - $server->shutdown(); - }); }); -$server->onWorkerStart(function (SwooleServer $swooleServer, int $workerId) use ($server, $register, $stats, $realtime) { +$server->onWorkerStart(function (int $workerId) use ($server, $register, $stats, $realtime) { Console::success('Worker ' . $workerId . ' started succefully'); $attempts = 0; @@ -198,9 +189,8 @@ $server->onWorkerStart(function (SwooleServer $swooleServer, int $workerId) use Console::error('Failed to restart pub/sub...'); }); -$server->onOpen(function (SwooleServer $swooleServer, SwooleRequest $request) use ($server, $register, $stats, &$realtime) { +$server->onOpen(function (int $connection, SwooleRequest $request) use ($server, $register, $stats, &$realtime) { $app = new App('UTC'); - $connection = $request->fd; $request = new Request($request); /** @var PDO $db */ @@ -208,7 +198,7 @@ $server->onOpen(function (SwooleServer $swooleServer, SwooleRequest $request) us /** @var Redis $redis */ $redis = $register->get('redisPool')->get(); - Console::info("Connection open (user: {$connection}, worker: {$swooleServer->getWorkerId()})"); + Console::info("Connection open (user: {$connection})"); App::setResource('db', function () use (&$db) { return $db; @@ -248,9 +238,7 @@ $server->onOpen(function (SwooleServer $swooleServer, SwooleRequest $request) us * * Abuse limits are connecting 128 times per minute and ip address. */ - $timeLimit = new TimeLimit('url:{url},ip:{ip}', 128, 60, function () use (&$db) { - return $db; - }); + $timeLimit = new TimeLimit('url:{url},ip:{ip}', 128, 60, $db); $timeLimit ->setNamespace('app_' . $project->getId()) ->setParam('{ip}', $request->getIP()) @@ -316,13 +304,12 @@ $server->onOpen(function (SwooleServer $swooleServer, SwooleRequest $request) us } }); -$server->onMessage(function (SwooleServer $swooleServer, Frame $frame) use ($server) { - $connection = $frame->fd; +$server->onMessage(function (int $connection, string $message) use ($server) { $server->send([$connection], 'Sending messages is not allowed.'); $server->close($connection, 1003); }); -$server->onClose(function (SwooleServer $server, int $connection) use ($realtime, $stats) { +$server->onClose(function (int $connection) use ($realtime, $stats) { if (array_key_exists($connection, $realtime->connections)) { $stats->decr($realtime->connections[$connection]['projectId'], 'connectionsTotal'); } From eaa3644f0ffe0348016d0f6a17533c1d3b8be06b Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Wed, 30 Jun 2021 13:36:58 +0200 Subject: [PATCH 162/267] remove Realtime event class --- app/controllers/shared/api.php | 28 +- app/init.php | 4 - app/realtime.php | 3 +- app/workers/functions.php | 12 +- src/Appwrite/Event/Realtime.php | 264 ------------------ src/Appwrite/Messaging/Adapter.php | 2 +- src/Appwrite/Messaging/Adapter/Realtime.php | 143 +++++++++- tests/unit/Event/RealtimeTest.php | 73 ----- .../unit/Messaging/MessagingChannelsTest.php | 6 +- tests/unit/Messaging/MessagingTest.php | 63 +++++ 10 files changed, 231 insertions(+), 367 deletions(-) delete mode 100644 src/Appwrite/Event/Realtime.php delete mode 100644 tests/unit/Event/RealtimeTest.php diff --git a/app/controllers/shared/api.php b/app/controllers/shared/api.php index 26948cef36..d275047c28 100644 --- a/app/controllers/shared/api.php +++ b/app/controllers/shared/api.php @@ -1,7 +1,9 @@ getParam('event'))) { @@ -198,12 +198,20 @@ App::shutdown(function ($utopia, $request, $response, $project, $events, $audits ->trigger(); if ($project->getId() !== 'console') { - $realtime - ->setEvent($events->getParam('event')) - ->setUserId($events->getParam('userId')) - ->setProject($project->getId()) - ->setPayload($response->getPayload()) - ->trigger(); + $payload = new Document($response->getPayload()); + $target = Realtime::fromPayload($events->getParam('event'), $payload); + + Realtime::send( + $project->getId(), + $response->getPayload(), + $events->getParam('event'), + $target['channels'], + $target['permissions'], + [ + 'permissionsChanged' => $target['permissionsChanged'], + 'userId' => $events->getParam('userId') + ] + ); } } @@ -228,4 +236,4 @@ App::shutdown(function ($utopia, $request, $response, $project, $events, $audits ; } -}, ['utopia', 'request', 'response', 'project', 'events', 'audits', 'usage', 'deletes', 'realtime', 'mode'], 'api'); +}, ['utopia', 'request', 'response', 'project', 'events', 'audits', 'usage', 'deletes', 'mode'], 'api'); diff --git a/app/init.php b/app/init.php index 2606820347..1bf4508082 100644 --- a/app/init.php +++ b/app/init.php @@ -341,10 +341,6 @@ App::setResource('events', function($register) { return new Event('', ''); }, ['register']); -App::setResource('realtime', function($register) { - return new Realtime('', '', []); -}, ['register']); - App::setResource('audits', function($register) { return new Event(Event::AUDITS_QUEUE_NAME, Event::AUDITS_CLASS_NAME); }, ['register']); diff --git a/app/realtime.php b/app/realtime.php index 3f0fed5087..31f1c28612 100644 --- a/app/realtime.php +++ b/app/realtime.php @@ -5,7 +5,6 @@ use Appwrite\Database\Adapter\Redis as RedisAdapter; use Appwrite\Database\Adapter\MySQL as MySQLAdapter; use Appwrite\Database\Database; use Appwrite\Event\Event; -use Appwrite\Event\Realtime as RealtimeEvent; use Appwrite\Messaging\Adapter\Realtime; use Appwrite\Network\Validator\Origin; use Swoole\Http\Request as SwooleRequest; @@ -267,7 +266,7 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server, ...Auth::getRoles($user) ]; - $channels = RealtimeEvent::convertChannels($request->getQuery('channels', []), $user); + $channels = Realtime::convertChannels($request->getQuery('channels', []), $user); /** * Channels Check diff --git a/app/workers/functions.php b/app/workers/functions.php index 1910fdcac0..f77c1728f4 100644 --- a/app/workers/functions.php +++ b/app/workers/functions.php @@ -6,7 +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\Event\Realtime; +use Appwrite\Messaging\Adapter\Realtime; use Appwrite\Resque\Worker; use Appwrite\Utopia\Response\Model\Execution; use Cron\CronExpression; @@ -494,9 +494,15 @@ class FunctionsV1 extends Worker $executionUpdate->trigger(); - $realtimeUpdate = new Realtime($projectId, 'functions.executions.update', $execution->getArrayCopy()); + $target = Realtime::fromPayload('functions.executions.update', $execution); - $realtimeUpdate->trigger(); + Realtime::send( + $projectId, + $execution->getArrayCopy(), + 'functions.executions.update', + $target['channels'], + $target['permissions'] + ); $usage = new Event('v1-usage', 'UsageV1'); diff --git a/src/Appwrite/Event/Realtime.php b/src/Appwrite/Event/Realtime.php deleted file mode 100644 index b113251500..0000000000 --- a/src/Appwrite/Event/Realtime.php +++ /dev/null @@ -1,264 +0,0 @@ -project = $project; - $this->event = $event; - $this->payload = new Document($payload); - } - - /** - * @param string $project - * return $this - */ - public function setProject(string $project): self - { - $this->project = $project; - return $this; - } - - /** - * @param string $userId - * return $this - */ - public function setUserId(string $userId): self - { - $this->userId = $userId; - return $this; - } - - /** - * @return string - */ - public function getProject(): string - { - return $this->project; - } - - /** - * @param string $event - * return $this - */ - public function setEvent(string $event): self - { - $this->event = $event; - return $this; - } - - /** - * @return string - */ - public function getEvent(): string - { - return $this->event; - } - - /** - * @param array $payload - * @return $this - */ - public function setPayload(array $payload): self - { - $this->payload = new Document($payload); - return $this; - } - - /** - * @return Document - */ - public function getPayload(): Document - { - return $this->payload; - } - - /** - * 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 Document $user - * @return array - */ - public static function convertChannels(array $channels, Document $user): 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($user->getId())) { - $channels['account.' . $user->getId()] = $value; - } - unset($channels['account']); - break; - } - } - - if (\array_key_exists('account', $channels)) { - if ($user->getId()) { - $channels['account.' . $user->getId()] = $channels['account']; - } - unset($channels['account']); - } - - return $channels; - } - - /** - * Populate channels array based on the event name and payload. - * - * @return void - */ - private function prepareChannels(): void - { - switch (true) { - case strpos($this->event, 'account.recovery.') === 0: - case strpos($this->event, 'account.sessions.') === 0: - case strpos($this->event, 'account.verification.') === 0: - $this->channels[] = 'account.' . $this->payload->getAttribute('userId'); - $this->permissions = ['user:' . $this->payload->getAttribute('userId')]; - - break; - case strpos($this->event, 'account.') === 0: - $this->channels[] = 'account.' . $this->payload->getId(); - $this->permissions = ['user:' . $this->payload->getId()]; - - break; - case strpos($this->event, 'teams.memberships') === 0: - $this->permissionsChanged = in_array($this->event, ['teams.memberships.update', 'teams.memberships.delete', 'teams.memberships.update.status']); - $this->channels[] = 'memberships'; - $this->channels[] = 'memberships.' . $this->payload->getId(); - $this->permissions = ['team:' . $this->payload->getAttribute('teamId')]; - - break; - case strpos($this->event, 'teams.') === 0: - $this->permissionsChanged = $this->event === 'teams.create'; - $this->channels[] = 'teams'; - $this->channels[] = 'teams.' . $this->payload->getId(); - $this->permissions = ['team:' . $this->payload->getId()]; - - break; - case strpos($this->event, 'database.collections.') === 0: - $this->channels[] = 'collections'; - $this->channels[] = 'collections.' . $this->payload->getId(); - $this->permissions = $this->payload->getAttribute('$permissions.read'); - - break; - case strpos($this->event, 'database.documents.') === 0: - $this->channels[] = 'documents'; - $this->channels[] = 'collections.' . $this->payload->getAttribute('$collection') . '.documents'; - $this->channels[] = 'documents.' . $this->payload->getId(); - $this->permissions = $this->payload->getAttribute('$permissions.read'); - - break; - case strpos($this->event, 'storage.') === 0: - $this->channels[] = 'files'; - $this->channels[] = 'files.' . $this->payload->getId(); - $this->permissions = $this->payload->getAttribute('$permissions.read'); - - break; - case strpos($this->event, 'functions.executions.') === 0: - if (!empty($this->payload->getAttribute('$permissions.read'))) { - $this->channels[] = 'executions'; - $this->channels[] = 'executions.' . $this->payload->getId(); - $this->channels[] = 'functions.' . $this->payload->getAttribute('functionId'); - $this->permissions = $this->payload->getAttribute('$permissions.read'); - } - break; - } - } - - /** - * Execute Event. - * - * @return void - */ - public function trigger(): void - { - $this->prepareChannels(); - if (empty($this->channels)) return; - - $redis = new \Redis(); - $redis->connect(App::getEnv('_APP_REDIS_HOST', ''), App::getEnv('_APP_REDIS_PORT', '')); - $redis->publish('realtime', json_encode([ - 'project' => $this->project, - 'permissions' => $this->permissions, - 'permissionsChanged' => $this->permissionsChanged, - 'userId' => $this->userId, - 'data' => [ - 'event' => $this->event, - 'channels' => $this->channels, - 'timestamp' => time(), - 'payload' => $this->payload->getArrayCopy() - ] - ])); - - $this->reset(); - } - - /** - * Resets this event and unpopulates all data. - * - * @return $this - */ - public function reset(): self - { - $this->event = ''; - $this->payload = $this->channels = []; - - return $this; - } -} diff --git a/src/Appwrite/Messaging/Adapter.php b/src/Appwrite/Messaging/Adapter.php index 055e0e49d5..d788e34d13 100644 --- a/src/Appwrite/Messaging/Adapter.php +++ b/src/Appwrite/Messaging/Adapter.php @@ -6,5 +6,5 @@ abstract class Adapter { public abstract function subscribe(string $project, mixed $identifier, array $roles, array $channels): void; public abstract function unsubscribe(mixed $identifier): void; - public abstract function send(string $projectId, string $event, array $payload): void; + public static abstract function send(string $projectId, array $payload, string $event, array $channels, array $permissions, array $options): void; } diff --git a/src/Appwrite/Messaging/Adapter/Realtime.php b/src/Appwrite/Messaging/Adapter/Realtime.php index e97710f9a6..d513cf22bc 100644 --- a/src/Appwrite/Messaging/Adapter/Realtime.php +++ b/src/Appwrite/Messaging/Adapter/Realtime.php @@ -2,8 +2,9 @@ namespace Appwrite\Messaging\Adapter; -use Appwrite\Event\Realtime as EventRealtime; +use Appwrite\Database\Document; use Appwrite\Messaging\Adapter; +use Utopia\App; class Realtime extends Adapter { @@ -116,15 +117,35 @@ class Realtime extends Adapter /** * Sends an event to the Realtime Server. - * @param string $projectId - * @param string $event + * @param string $project * @param array $payload + * @param string $event + * @param array $channels + * @param array $permissions + * @param array $options * @return void */ - public function send(string $projectId, string $event, array $payload): void + public static function send(string $project, array $payload, string $event, array $channels, array $permissions, array $options = []): void { - $realtime = new EventRealtime($projectId, $event, $payload); - $realtime->trigger(); + if (empty($channels) || empty($permissions) || empty($project)) return; + + $permissionsChanged = array_key_exists('permissionsChanged', $options) && $options['permissionsChanged']; + $userId = array_key_exists('userId', $options) ? $options['userId'] : null; + + $redis = new \Redis(); + $redis->connect(App::getEnv('_APP_REDIS_HOST', ''), App::getEnv('_APP_REDIS_PORT', '')); + $redis->publish('realtime', json_encode([ + 'project' => $project, + 'permissions' => $permissions, + 'permissionsChanged' => $permissionsChanged, + 'userId' => $userId, + 'data' => [ + 'event' => $event, + 'channels' => $channels, + 'timestamp' => time(), + 'payload' => $payload + ] + ])); } /** @@ -161,4 +182,114 @@ class Realtime extends Adapter 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 Document $user + * @return array + */ + public static function convertChannels(array $channels, Document $user): 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($user->getId())) { + $channels['account.' . $user->getId()] = $value; + } + unset($channels['account']); + break; + } + } + + if (\array_key_exists('account', $channels)) { + if ($user->getId()) { + $channels['account.' . $user->getId()] = $channels['account']; + } + unset($channels['account']); + } + + return $channels; + } + + /** + * Create channels array based on the event name and payload. + * + * @return void + */ + public static function fromPayload(string $event, Document $payload): array + { + $channels = []; + $permissions = []; + $permissionsChanged = false; + + switch (true) { + case strpos($event, 'account.recovery.') === 0: + case strpos($event, 'account.sessions.') === 0: + case strpos($event, 'account.verification.') === 0: + $channels[] = 'account.' . $payload->getAttribute('userId'); + $permissions = ['user:' . $payload->getAttribute('userId')]; + + break; + case strpos($event, 'account.') === 0: + $channels[] = 'account.' . $payload->getId(); + $permissions = ['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(); + $permissions = ['team:' . $payload->getAttribute('teamId')]; + + break; + case strpos($event, 'teams.') === 0: + $permissionsChanged = $event === 'teams.create'; + $channels[] = 'teams'; + $channels[] = 'teams.' . $payload->getId(); + $permissions = ['team:' . $payload->getId()]; + + break; + case strpos($event, 'database.collections.') === 0: + $channels[] = 'collections'; + $channels[] = 'collections.' . $payload->getId(); + $permissions = $payload->getAttribute('$permissions.read'); + + break; + case strpos($event, 'database.documents.') === 0: + $channels[] = 'documents'; + $channels[] = 'collections.' . $payload->getAttribute('$collection') . '.documents'; + $channels[] = 'documents.' . $payload->getId(); + $permissions = $payload->getAttribute('$permissions.read'); + + break; + case strpos($event, 'storage.') === 0: + $channels[] = 'files'; + $channels[] = 'files.' . $payload->getId(); + $permissions = $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'); + $permissions = $payload->getAttribute('$permissions.read'); + } + break; + } + + return [ + 'channels' => $channels, + 'permissions' => $permissions, + 'permissionsChanged' => $permissionsChanged + ]; + } } diff --git a/tests/unit/Event/RealtimeTest.php b/tests/unit/Event/RealtimeTest.php deleted file mode 100644 index e4aef27ed3..0000000000 --- a/tests/unit/Event/RealtimeTest.php +++ /dev/null @@ -1,73 +0,0 @@ - '' - ]); - $channels = [ - 0 => 'files', - 1 => 'documents', - 2 => 'documents.789', - 3 => 'account', - 4 => 'account.456' - ]; - - $channels = Realtime::convertChannels($channels, $user); - $this->assertCount(3, $channels); - $this->assertArrayHasKey('files', $channels); - $this->assertArrayHasKey('documents', $channels); - $this->assertArrayHasKey('documents.789', $channels); - $this->assertArrayNotHasKey('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); - - $this->assertCount(4, $channels); - $this->assertArrayHasKey('files', $channels); - $this->assertArrayHasKey('documents', $channels); - $this->assertArrayHasKey('documents.789', $channels); - $this->assertArrayHasKey('account.123', $channels); - $this->assertArrayNotHasKey('account', $channels); - $this->assertArrayNotHasKey('account.456', $channels); - } -} diff --git a/tests/unit/Messaging/MessagingChannelsTest.php b/tests/unit/Messaging/MessagingChannelsTest.php index 4159def554..23ad320ed9 100644 --- a/tests/unit/Messaging/MessagingChannelsTest.php +++ b/tests/unit/Messaging/MessagingChannelsTest.php @@ -4,8 +4,6 @@ namespace Appwrite\Tests; use Appwrite\Auth\Auth; use Appwrite\Database\Document; -use Appwrite\Event\Realtime as EventRealtime; -use Appwrite\Messaging; use Appwrite\Messaging\Adapter\Realtime; use PHPUnit\Framework\TestCase; @@ -67,7 +65,7 @@ class MessagingChannelsTest extends TestCase ...Auth::getRoles($user) ]; - $parsedChannels = EventRealtime::convertChannels([0 => $channel], $user); + $parsedChannels = Realtime::convertChannels([0 => $channel], $user); $this->realtime->subscribe( '1', @@ -94,7 +92,7 @@ class MessagingChannelsTest extends TestCase ...Auth::getRoles($user) ]; - $parsedChannels = EventRealtime::convertChannels([0 => $channel], $user); + $parsedChannels = Realtime::convertChannels([0 => $channel], $user); $this->realtime->subscribe( '1', diff --git a/tests/unit/Messaging/MessagingTest.php b/tests/unit/Messaging/MessagingTest.php index 6cd6a69675..d0d0aab786 100644 --- a/tests/unit/Messaging/MessagingTest.php +++ b/tests/unit/Messaging/MessagingTest.php @@ -2,6 +2,7 @@ namespace Appwrite\Tests; +use Appwrite\Database\Document; use Appwrite\Messaging\Adapter\Realtime; use PHPUnit\Framework\TestCase; @@ -132,4 +133,66 @@ class MessagingTest extends TestCase $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); + $this->assertCount(3, $channels); + $this->assertArrayHasKey('files', $channels); + $this->assertArrayHasKey('documents', $channels); + $this->assertArrayHasKey('documents.789', $channels); + $this->assertArrayNotHasKey('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); + + $this->assertCount(4, $channels); + $this->assertArrayHasKey('files', $channels); + $this->assertArrayHasKey('documents', $channels); + $this->assertArrayHasKey('documents.789', $channels); + $this->assertArrayHasKey('account.123', $channels); + $this->assertArrayNotHasKey('account', $channels); + $this->assertArrayNotHasKey('account.456', $channels); + } } From 80c4e378b30f3b7115c6bdbcfab70c1172860e66 Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Wed, 30 Jun 2021 16:04:32 +0200 Subject: [PATCH 163/267] fix(acl): getting role for user in auth class --- app/controllers/general.php | 5 ++--- app/realtime.php | 5 +---- src/Appwrite/Auth/Auth.php | 3 ++- tests/unit/Auth/AuthTest.php | 5 +++-- 4 files changed, 8 insertions(+), 10 deletions(-) diff --git a/app/controllers/general.php b/app/controllers/general.php index cd56666620..e33bbcbd51 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -234,14 +234,13 @@ App::init(function ($utopia, $request, $response, $console, $project, $consoleDB ]); $role = Auth::USER_ROLE_APP; - $scopes = \array_merge($roles[$role]['scopes'], $key->getAttribute('scopes', [])); + $scopes = \array_merge($roles[Auth::USER_ROLE_APP]['scopes'], $key->getAttribute('scopes', [])); + Authorization::setRole('role:'.Auth::USER_ROLE_APP); Authorization::setDefaultStatus(false); // Cancel security segmentation for API keys. } } - Authorization::setRole('role:'.$role); - foreach (Auth::getRoles($user) as $role) { Authorization::setRole($role); } diff --git a/app/realtime.php b/app/realtime.php index 31f1c28612..a39b8ea45a 100644 --- a/app/realtime.php +++ b/app/realtime.php @@ -261,10 +261,7 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server, throw new Exception($originValidator->getDescription(), 1008); } - $roles = [ - 'role:' . (($user->isEmpty()) ? Auth::USER_ROLE_GUEST : Auth::USER_ROLE_MEMBER), - ...Auth::getRoles($user) - ]; + $roles = Auth::getRoles($user); $channels = Realtime::convertChannels($request->getQuery('channels', []), $user); diff --git a/src/Appwrite/Auth/Auth.php b/src/Appwrite/Auth/Auth.php index 8dfe9cbd7f..d7cfda86af 100644 --- a/src/Appwrite/Auth/Auth.php +++ b/src/Appwrite/Auth/Auth.php @@ -282,8 +282,9 @@ class Auth { if ($user->getId()) { $roles[] = 'user:'.$user->getId(); + $roles[] = 'role:'.Auth::USER_ROLE_MEMBER; } else { - return []; + return ['role:'.Auth::USER_ROLE_GUEST]; } foreach ($user->getAttribute('memberships', []) as $node) { diff --git a/tests/unit/Auth/AuthTest.php b/tests/unit/Auth/AuthTest.php index 0053ca3392..a259f02c5d 100644 --- a/tests/unit/Auth/AuthTest.php +++ b/tests/unit/Auth/AuthTest.php @@ -204,8 +204,8 @@ class AuthTest extends TestCase ]); $roles = Auth::getRoles($user); - $this->assertCount(0, $roles); - $this->assertEmpty($roles); + $this->assertCount(1, $roles); + $this->assertContains('role:guest', $roles); } public function testUserRoles() @@ -232,6 +232,7 @@ class AuthTest extends TestCase $roles = Auth::getRoles($user); $this->assertCount(6, $roles); + $this->assertContains('role:member', $roles); $this->assertContains('user:123', $roles); $this->assertContains('team:abc', $roles); $this->assertContains('team:abc/administrator', $roles); From 1e6b867378203bb0c474541865dd16673c08b0e2 Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Wed, 30 Jun 2021 16:29:33 +0200 Subject: [PATCH 164/267] fix(tests): messaging unit tests --- tests/unit/Messaging/MessagingChannelsTest.php | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/tests/unit/Messaging/MessagingChannelsTest.php b/tests/unit/Messaging/MessagingChannelsTest.php index 23ad320ed9..a16197d637 100644 --- a/tests/unit/Messaging/MessagingChannelsTest.php +++ b/tests/unit/Messaging/MessagingChannelsTest.php @@ -60,10 +60,7 @@ class MessagingChannelsTest extends TestCase ] ]); - $roles = [ - 'role:' . (($user->isEmpty()) ? Auth::USER_ROLE_GUEST : Auth::USER_ROLE_MEMBER), - ...Auth::getRoles($user) - ]; + $roles = Auth::getRoles($user); $parsedChannels = Realtime::convertChannels([0 => $channel], $user); @@ -87,10 +84,7 @@ class MessagingChannelsTest extends TestCase '$id' => '' ]); - $roles = [ - 'role:' . (($user->isEmpty()) ? Auth::USER_ROLE_GUEST : Auth::USER_ROLE_MEMBER), - ...Auth::getRoles($user) - ]; + $roles = Auth::getRoles($user); $parsedChannels = Realtime::convertChannels([0 => $channel], $user); From d224fb49e69c8b2431d4db68a77360616ad90561 Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Wed, 30 Jun 2021 16:47:55 +0200 Subject: [PATCH 165/267] fix some style issues --- app/controllers/general.php | 2 +- app/init.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/controllers/general.php b/app/controllers/general.php index e33bbcbd51..860d2d7c56 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -234,7 +234,7 @@ App::init(function ($utopia, $request, $response, $console, $project, $consoleDB ]); $role = Auth::USER_ROLE_APP; - $scopes = \array_merge($roles[Auth::USER_ROLE_APP]['scopes'], $key->getAttribute('scopes', [])); + $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. diff --git a/app/init.php b/app/init.php index 1bf4508082..986b07b199 100644 --- a/app/init.php +++ b/app/init.php @@ -430,7 +430,7 @@ App::setResource('user', function($mode, $project, $console, $request, $response $user = $projectDB->getDocument(Auth::$unique); } else { $user = $consoleDB->getDocument(Auth::$unique); - + $user ->setAttribute('$id', 'admin-'.$user->getAttribute('$id')) ; From 159f75793cce6eb414dbf868a9be788e4d602349 Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Thu, 1 Jul 2021 09:19:48 +0200 Subject: [PATCH 166/267] fix tests for auth get roles --- tests/unit/Auth/AuthTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/Auth/AuthTest.php b/tests/unit/Auth/AuthTest.php index a259f02c5d..de3013f96b 100644 --- a/tests/unit/Auth/AuthTest.php +++ b/tests/unit/Auth/AuthTest.php @@ -231,7 +231,7 @@ class AuthTest extends TestCase $roles = Auth::getRoles($user); - $this->assertCount(6, $roles); + $this->assertCount(7, $roles); $this->assertContains('role:member', $roles); $this->assertContains('user:123', $roles); $this->assertContains('team:abc', $roles); From b9d95f769cb3dc1e1f73670d28457af898403fdd Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Thu, 1 Jul 2021 12:31:48 +0200 Subject: [PATCH 167/267] fix after rebase --- app/realtime.php | 140 +++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 128 insertions(+), 12 deletions(-) diff --git a/app/realtime.php b/app/realtime.php index a39b8ea45a..88abbc20d9 100644 --- a/app/realtime.php +++ b/app/realtime.php @@ -4,6 +4,7 @@ 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; @@ -39,14 +40,70 @@ $stats->column('connectionsTotal', Table::TYPE_INT); $stats->column('messages', Table::TYPE_INT); $stats->create(); +$containerId = uniqid(); +$documentId = null; + $server = new Server($adapter); $realtime = new Realtime(); -$server->onStart(function () use ($stats) { +$server->onStart(function () use ($stats, $register, $containerId, &$documentId) { Console::success('Server started succefully'); - Timer::tick(10000, function () use ($stats) { + $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 for connection 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; @@ -73,6 +130,36 @@ $server->onStart(function () use ($stats) { $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); + } }); }); @@ -81,27 +168,56 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats, $attempts = 0; $start = time(); - $redisPool = $register->get('redisPool'); /** * Sending current connections to project channels on the console project every 5 seconds. */ - Timer::tick(5000, function () use ($server, $stats, $realtime) { + Timer::tick(5000, function () use ($server, $register, $realtime) { 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', [])); + + $projectDb = new Database(); + $projectDb->setAdapter(new RedisAdapter(new MySQLAdapter($db, $cache), $cache)); + $projectDb->setMocks(Config::getParam('collections', [])); + $payload = []; - foreach ($stats as $projectId => $value) { - $payload[$projectId] = $value['connectionsTotal']; + $list = $consoleDb->getCollection([ + 'filters' => [ + '$collection=' . Database::SYSTEM_COLLECTION_CONNECTIONS, + 'timestamp>' . (time() - 15) + ], + ]); + + 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; + } + } } $event = [ - 'event' => 'stats.connections', - 'channels' => ['project'], + 'project' => 'console', 'permissions' => ['role:member'], - 'timestamp' => time(), - 'payload' => $payload + 'data' => [ + 'event' => 'stats.connections', + 'channels' => ['project'], + 'timestamp' => time(), + 'payload' => $payload + ] ]; $server->send($realtime->getReceivers($event), json_encode($event)); + $register->get('dbPool')->put($db); + $register->get('redisPool')->put($cache); } }); @@ -115,7 +231,7 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats, $start = time(); /** @var Redis $redis */ - $redis = $redisPool->get(); + $redis = $register->get('redisPool')->get(); $redis->setOption(Redis::OPT_READ_TIMEOUT, -1); if ($redis->ping(true)) { @@ -177,7 +293,7 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats, }); } catch (\Throwable $th) { Console::error('Pub/sub error: ' . $th->getMessage()); - $redisPool->put($redis); + $register->get('redisPool')->put($redis); $attempts++; continue; } From cc3f6e81ebf3d39efc905357c3febc198a1ef25c Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Thu, 1 Jul 2021 12:36:37 +0200 Subject: [PATCH 168/267] fix js dist build --- public/dist/scripts/app-all.js | 12 +++++++++--- public/dist/scripts/app-dep.js | 8 +++++++- public/dist/scripts/app.js | 4 ++-- 3 files changed, 18 insertions(+), 6 deletions(-) diff --git a/public/dist/scripts/app-all.js b/public/dist/scripts/app-all.js index 4500cfa3ce..907afb8576 100644 --- a/public/dist/scripts/app-all.js +++ b/public/dist/scripts/app-all.js @@ -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.0.0','X-Appwrite-Response-Format':'0.8.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.0.0','X-Appwrite-Response-Format':'0.8.0',};this.realtime={socket:undefined,timeout:undefined,channels:{},lastMessage:undefined,createSocket:()=>{var _a;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());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;if(((_b=(_a=this.realtime)===null||_a===void 0?void 0:_a.lastMessage)===null||_b===void 0?void 0:_b.code)===1008){return;} +console.error('Realtime got disconnected. Reconnect will be attempted in 1 second.',event.reason);setTimeout(()=>{this.realtime.createSocket();},1000);});},onMessage:(channel,callback)=>(event)=>{try{const data=JSON.parse(event.data);this.realtime.lastMessage=data;if(data.channels&&data.channels.includes(channel)){callback(data);} +else if(data.code){throw 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;} @@ -2232,7 +2238,7 @@ return slf.renderToken(tokens,idx,opts);} md.renderer.rules.strong_open=renderEm;md.renderer.rules.strong_close=renderEm;return md;},true);})(window);(function(window){"use strict";window.ls.container.set('rtl',function(){var rtlStock="^ا^ب^ت^ث^ج^ح^خ^د^ذ^ر^ز^س^ش^ص^ض^ط^ظ^ع^غ^ف^ق^ك^ل^م^ن^ه^و^ي^א^ב^ג^ד^ה^ו^ז^ח^ט^י^כ^ך^ל^מ^ם^נ^ן^ס^ע^פ^ף^צ^ץ^ק^ר^ש^ת^";var special=["\n"," "," ","״",'"',"_","'","!","@","#","$","^","&","%","*","(",")","+","=","-","[","]","\\","/","{","}","|",":","<",">","?",",",".","0","1","2","3","4","5","6","7","8","9"];var isRTL=function(value){for(var i=0;i{return{current:null,set:function(currentConnections){var scope=this;scope.current=currentConnections;return scope.current;}};},true,true);})(window);window.ls.router.add("/auth/signin",{template:"/auth/signin?version="+APP_ENV.CACHEBUSTER,scope:"home"}).add("/auth/signup",{template:"/auth/signup?version="+APP_ENV.CACHEBUSTER,scope:"home"}).add("/auth/recovery",{template:"/auth/recovery?version="+APP_ENV.CACHEBUSTER,scope:"home"}).add("/auth/recovery/reset",{template:"/auth/recovery/reset?version="+APP_ENV.CACHEBUSTER,scope:"home"}).add("/auth/confirm",{template:"/auth/confirm?version="+APP_ENV.CACHEBUSTER,scope:"home"}).add("/auth/join",{template:"/auth/join?version="+APP_ENV.CACHEBUSTER,scope:"home"}).add("/auth/oauth2/success",{template:"/auth/oauth2/success?version="+APP_ENV.CACHEBUSTER,scope:"home"}).add("/auth/oauth2/failure",{template:"/auth/oauth2/failure?version="+APP_ENV.CACHEBUSTER,scope:"home"}).add("/console",{template:"/console?version="+APP_ENV.CACHEBUSTER,scope:"console"}).add("/console/account",{template:"/console/account?version="+APP_ENV.CACHEBUSTER,scope:"console"}).add("/console/account/:tab",{template:"/console/account?version="+APP_ENV.CACHEBUSTER,scope:"console"}).add("/console/home",{template:"/console/home?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/home/:tab",{template:"/console/home?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/platforms/:platform",{template:function(window){return window.location.pathname+"?version="+APP_ENV.CACHEBUSTER;},scope:"console",project:true}).add("/console/notifications",{template:"/console/notifications?version="+APP_ENV.CACHEBUSTER,scope:"console"}).add("/console/settings",{template:"/console/settings?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/settings/:tab",{template:"/console/settings?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/webhooks",{template:"/console/webhooks?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/webhooks/:tab",{template:"/console/webhooks?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/keys",{template:"/console/keys?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/keys/:tab",{template:"/console/keys?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/tasks",{template:"/console/tasks?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/tasks/:tab",{template:"/console/tasks?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/database",{template:"/console/database?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/database/collection",{template:function(window){return window.location.pathname+window.location.search+'&version='+APP_ENV.CACHEBUSTER;},scope:"console",project:true}).add("/console/database/collection/:tab",{template:function(window){return window.location.pathname+window.location.search+'&version='+APP_ENV.CACHEBUSTER;},scope:"console",project:true}).add("/console/database/document",{template:function(window){return window.location.pathname+window.location.search+'&version='+APP_ENV.CACHEBUSTER;},scope:"console",project:true}).add("/console/database/document/:tab",{template:function(window){return window.location.pathname+window.location.search+'&version='+APP_ENV.CACHEBUSTER;},scope:"console",project:true}).add("/console/storage",{template:"/console/storage?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/storage/:tab",{template:"/console/storage?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/users",{template:"/console/users?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/users/user",{template:"/console/users/user?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/users/user/:tab",{template:"/console/users/user?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/users/teams/team",{template:"/console/users/teams/team?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/users/teams/team/:tab",{template:"/console/users/teams/team?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/users/:tab",{template:"/console/users?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/functions",{template:"/console/functions?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/functions/function",{template:"/console/functions/function?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/functions/function/:tab",{template:"/console/functions/function?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/functions/:tab",{template:"/console/functions?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true});window.ls.filter.add("avatar",function($value,element){if(!$value){return"";} let size=element.dataset["size"]||80;let name=$value.name||$value||"";name=(typeof name!=='string')?'--':name;return def="/v1/avatars/initials?project=console"+"&name="+ encodeURIComponent(name)+"&width="+ size+"&height="+ @@ -2260,7 +2266,7 @@ return $value;}).add("platformsLimit",function($value){return $value;}).add("lim return $value.join(", ").replace(/,\s([^,]+)$/,' and $1');}).add("runtimeName",function($value,env){if(env&&env.RUNTIMES&&env.RUNTIMES[$value]){return env.RUNTIMES[$value].name;} return'';}).add("runtimeLogo",function($value,env){if(env&&env.RUNTIMES&&env.RUNTIMES[$value]){return env.RUNTIMES[$value].logo;} return'';}).add("runtimeVersion",function($value,env){if(env&&env.RUNTIMES&&env.RUNTIMES[$value]){return env.RUNTIMES[$value].version;} -return'';});function abbreviate(number,maxPlaces,forcePlaces,forceLetter){number=Number(number);forceLetter=forceLetter||false;if(forceLetter!==false){return annotate(number,maxPlaces,forcePlaces,forceLetter);} +return'';}).add("accessProject",function($value,router){return($value&&$value.hasOwnProperty(router.params.project))?$value[router.params.project]:0;});function abbreviate(number,maxPlaces,forcePlaces,forceLetter){number=Number(number);forceLetter=forceLetter||false;if(forceLetter!==false){return annotate(number,maxPlaces,forcePlaces,forceLetter);} let abbr;if(number>=1e12){abbr="T";}else if(number>=1e9){abbr="B";}else if(number>=1e6){abbr="M";}else if(number>=1e3){abbr="K";}else{abbr="";} return annotate(number,maxPlaces,forcePlaces,abbr);} function annotate(number,maxPlaces,forcePlaces,abbr){let rounded=0;switch(abbr){case"T":rounded=number/1e12;break;case"B":rounded=number/1e9;break;case"M":rounded=number/1e6;break;case"K":rounded=number/1e3;break;case"":rounded=number;break;} diff --git a/public/dist/scripts/app-dep.js b/public/dist/scripts/app-dep.js index bc3b31b148..5a161a5082 100644 --- a/public/dist/scripts/app-dep.js +++ b/public/dist/scripts/app-dep.js @@ -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.0.0','X-Appwrite-Response-Format':'0.8.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.0.0','X-Appwrite-Response-Format':'0.8.0',};this.realtime={socket:undefined,timeout:undefined,channels:{},lastMessage:undefined,createSocket:()=>{var _a;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());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;if(((_b=(_a=this.realtime)===null||_a===void 0?void 0:_a.lastMessage)===null||_b===void 0?void 0:_b.code)===1008){return;} +console.error('Realtime got disconnected. Reconnect will be attempted in 1 second.',event.reason);setTimeout(()=>{this.realtime.createSocket();},1000);});},onMessage:(channel,callback)=>(event)=>{try{const data=JSON.parse(event.data);this.realtime.lastMessage=data;if(data.channels&&data.channels.includes(channel)){callback(data);} +else if(data.code){throw 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;} diff --git a/public/dist/scripts/app.js b/public/dist/scripts/app.js index 837ccf1e02..f0194333d6 100644 --- a/public/dist/scripts/app.js +++ b/public/dist/scripts/app.js @@ -264,7 +264,7 @@ return slf.renderToken(tokens,idx,opts);} md.renderer.rules.strong_open=renderEm;md.renderer.rules.strong_close=renderEm;return md;},true);})(window);(function(window){"use strict";window.ls.container.set('rtl',function(){var rtlStock="^ا^ب^ت^ث^ج^ح^خ^د^ذ^ر^ز^س^ش^ص^ض^ط^ظ^ع^غ^ف^ق^ك^ل^م^ن^ه^و^ي^א^ב^ג^ד^ה^ו^ז^ח^ט^י^כ^ך^ל^מ^ם^נ^ן^ס^ע^פ^ף^צ^ץ^ק^ר^ש^ת^";var special=["\n"," "," ","״",'"',"_","'","!","@","#","$","^","&","%","*","(",")","+","=","-","[","]","\\","/","{","}","|",":","<",">","?",",",".","0","1","2","3","4","5","6","7","8","9"];var isRTL=function(value){for(var i=0;i{return{current:null,set:function(currentConnections){var scope=this;scope.current=currentConnections;return scope.current;}};},true,true);})(window);window.ls.router.add("/auth/signin",{template:"/auth/signin?version="+APP_ENV.CACHEBUSTER,scope:"home"}).add("/auth/signup",{template:"/auth/signup?version="+APP_ENV.CACHEBUSTER,scope:"home"}).add("/auth/recovery",{template:"/auth/recovery?version="+APP_ENV.CACHEBUSTER,scope:"home"}).add("/auth/recovery/reset",{template:"/auth/recovery/reset?version="+APP_ENV.CACHEBUSTER,scope:"home"}).add("/auth/confirm",{template:"/auth/confirm?version="+APP_ENV.CACHEBUSTER,scope:"home"}).add("/auth/join",{template:"/auth/join?version="+APP_ENV.CACHEBUSTER,scope:"home"}).add("/auth/oauth2/success",{template:"/auth/oauth2/success?version="+APP_ENV.CACHEBUSTER,scope:"home"}).add("/auth/oauth2/failure",{template:"/auth/oauth2/failure?version="+APP_ENV.CACHEBUSTER,scope:"home"}).add("/console",{template:"/console?version="+APP_ENV.CACHEBUSTER,scope:"console"}).add("/console/account",{template:"/console/account?version="+APP_ENV.CACHEBUSTER,scope:"console"}).add("/console/account/:tab",{template:"/console/account?version="+APP_ENV.CACHEBUSTER,scope:"console"}).add("/console/home",{template:"/console/home?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/home/:tab",{template:"/console/home?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/platforms/:platform",{template:function(window){return window.location.pathname+"?version="+APP_ENV.CACHEBUSTER;},scope:"console",project:true}).add("/console/notifications",{template:"/console/notifications?version="+APP_ENV.CACHEBUSTER,scope:"console"}).add("/console/settings",{template:"/console/settings?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/settings/:tab",{template:"/console/settings?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/webhooks",{template:"/console/webhooks?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/webhooks/:tab",{template:"/console/webhooks?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/keys",{template:"/console/keys?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/keys/:tab",{template:"/console/keys?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/tasks",{template:"/console/tasks?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/tasks/:tab",{template:"/console/tasks?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/database",{template:"/console/database?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/database/collection",{template:function(window){return window.location.pathname+window.location.search+'&version='+APP_ENV.CACHEBUSTER;},scope:"console",project:true}).add("/console/database/collection/:tab",{template:function(window){return window.location.pathname+window.location.search+'&version='+APP_ENV.CACHEBUSTER;},scope:"console",project:true}).add("/console/database/document",{template:function(window){return window.location.pathname+window.location.search+'&version='+APP_ENV.CACHEBUSTER;},scope:"console",project:true}).add("/console/database/document/:tab",{template:function(window){return window.location.pathname+window.location.search+'&version='+APP_ENV.CACHEBUSTER;},scope:"console",project:true}).add("/console/storage",{template:"/console/storage?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/storage/:tab",{template:"/console/storage?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/users",{template:"/console/users?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/users/user",{template:"/console/users/user?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/users/user/:tab",{template:"/console/users/user?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/users/teams/team",{template:"/console/users/teams/team?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/users/teams/team/:tab",{template:"/console/users/teams/team?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/users/:tab",{template:"/console/users?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/functions",{template:"/console/functions?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/functions/function",{template:"/console/functions/function?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/functions/function/:tab",{template:"/console/functions/function?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true}).add("/console/functions/:tab",{template:"/console/functions?version="+APP_ENV.CACHEBUSTER,scope:"console",project:true});window.ls.filter.add("avatar",function($value,element){if(!$value){return"";} let size=element.dataset["size"]||80;let name=$value.name||$value||"";name=(typeof name!=='string')?'--':name;return def="/v1/avatars/initials?project=console"+"&name="+ encodeURIComponent(name)+"&width="+ size+"&height="+ @@ -292,7 +292,7 @@ return $value;}).add("platformsLimit",function($value){return $value;}).add("lim return $value.join(", ").replace(/,\s([^,]+)$/,' and $1');}).add("runtimeName",function($value,env){if(env&&env.RUNTIMES&&env.RUNTIMES[$value]){return env.RUNTIMES[$value].name;} return'';}).add("runtimeLogo",function($value,env){if(env&&env.RUNTIMES&&env.RUNTIMES[$value]){return env.RUNTIMES[$value].logo;} return'';}).add("runtimeVersion",function($value,env){if(env&&env.RUNTIMES&&env.RUNTIMES[$value]){return env.RUNTIMES[$value].version;} -return'';});function abbreviate(number,maxPlaces,forcePlaces,forceLetter){number=Number(number);forceLetter=forceLetter||false;if(forceLetter!==false){return annotate(number,maxPlaces,forcePlaces,forceLetter);} +return'';}).add("accessProject",function($value,router){return($value&&$value.hasOwnProperty(router.params.project))?$value[router.params.project]:0;});function abbreviate(number,maxPlaces,forcePlaces,forceLetter){number=Number(number);forceLetter=forceLetter||false;if(forceLetter!==false){return annotate(number,maxPlaces,forcePlaces,forceLetter);} let abbr;if(number>=1e12){abbr="T";}else if(number>=1e9){abbr="B";}else if(number>=1e6){abbr="M";}else if(number>=1e3){abbr="K";}else{abbr="";} return annotate(number,maxPlaces,forcePlaces,abbr);} function annotate(number,maxPlaces,forcePlaces,abbr){let rounded=0;switch(abbr){case"T":rounded=number/1e12;break;case"B":rounded=number/1e9;break;case"M":rounded=number/1e6;break;case"K":rounded=number/1e3;break;case"":rounded=number;break;} From 7b5b2c8af883cc28115fc73be34fca45cdf65c85 Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Thu, 1 Jul 2021 15:13:16 +0200 Subject: [PATCH 169/267] fix after rebase --- app/realtime.php | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/app/realtime.php b/app/realtime.php index a39b8ea45a..e2a1d1dcb1 100644 --- a/app/realtime.php +++ b/app/realtime.php @@ -94,14 +94,17 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats, } $event = [ - 'event' => 'stats.connections', - 'channels' => ['project'], + 'project' => 'console', 'permissions' => ['role:member'], - 'timestamp' => time(), - 'payload' => $payload + 'data' => [ + 'event' => 'stats.connections', + 'channels' => ['project'], + 'timestamp' => time(), + 'payload' => $payload + ] ]; - $server->send($realtime->getReceivers($event), json_encode($event)); + $server->send($realtime->getReceivers($event), json_encode($event['data'])); } }); From 750229f745af99785a36960ef4de0b947fce2db4 Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Wed, 7 Jul 2021 10:21:14 +0200 Subject: [PATCH 170/267] update composer lock --- composer.lock | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/composer.lock b/composer.lock index dc03063f0c..b358bfbd6b 100644 --- a/composer.lock +++ b/composer.lock @@ -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": "7983e9fe8946a99fbf818b79ff202486", + "content-hash": "29b71fcf9fd4332ac11d2beffb9a8881", "packages": [ { "name": "adhocore/jwt", @@ -2181,7 +2181,7 @@ "source": { "type": "git", "url": "https://github.com/utopia-php/websocket.git", - "reference": "0cff9078e8acdb99f2bb8b30a578d0137bd460de" + "reference": "23339d8f860c77f15e39b9772ca20b446b2a26ff" }, "require": { "php": ">=8.0" @@ -2220,7 +2220,7 @@ "utopia", "websocket" ], - "time": "2021-06-29T16:04:32+00:00" + "time": "2021-07-05T07:05:52+00:00" }, { "name": "webmozart/assert", @@ -2450,16 +2450,16 @@ }, { "name": "appwrite/sdk-generator", - "version": "dev-feat-kotlin-java-docs", + "version": "0.12.0", "source": { "type": "git", "url": "https://github.com/appwrite/sdk-generator.git", - "reference": "966d464728b41a8c449e99d7df4bd4ddca591a25" + "reference": "ca8e34f091b3a66f94a8972cb94b0b8e1161dada" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/966d464728b41a8c449e99d7df4bd4ddca591a25", - "reference": "966d464728b41a8c449e99d7df4bd4ddca591a25", + "url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/ca8e34f091b3a66f94a8972cb94b0b8e1161dada", + "reference": "ca8e34f091b3a66f94a8972cb94b0b8e1161dada", "shasum": "" }, "require": { @@ -2493,9 +2493,9 @@ "description": "Appwrite PHP library for generating API SDKs for multiple programming languages and platforms", "support": { "issues": "https://github.com/appwrite/sdk-generator/issues", - "source": "https://github.com/appwrite/sdk-generator/tree/feat-kotlin-java-docs" + "source": "https://github.com/appwrite/sdk-generator/tree/0.12.0" }, - "time": "2021-07-06T09:26:45+00:00" + "time": "2021-07-06T16:20:51+00:00" }, { "name": "composer/package-versions-deprecated", @@ -6163,7 +6163,7 @@ "aliases": [], "minimum-stability": "stable", "stability-flags": { - "appwrite/sdk-generator": 20 + "utopia-php/websocket": 20 }, "prefer-stable": false, "prefer-lowest": false, @@ -6186,5 +6186,5 @@ "platform-overrides": { "php": "8.0" }, - "plugin-api-version": "2.1.0" + "plugin-api-version": "2.0.0" } From 0f5690b2b999567c849735d18aeac9a98b39def4 Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Wed, 7 Jul 2021 10:23:31 +0200 Subject: [PATCH 171/267] update version number from websocket server --- composer.json | 2 +- composer.lock | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/composer.json b/composer.json index eaf0971a28..45fe77fc9a 100644 --- a/composer.json +++ b/composer.json @@ -51,7 +51,7 @@ "utopia-php/domains": "1.1.*", "utopia-php/swoole": "0.2.*", "utopia-php/storage": "0.5.*", - "utopia-php/websocket": "dev-fix-adapter-interface", + "utopia-php/websocket": "dev-main", "utopia-php/image": "0.5.*", "resque/php-resque": "1.3.6", "matomo/device-detector": "4.2.3", diff --git a/composer.lock b/composer.lock index b358bfbd6b..4cabaeedce 100644 --- a/composer.lock +++ b/composer.lock @@ -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": "29b71fcf9fd4332ac11d2beffb9a8881", + "content-hash": "3647b0e23acf63473fd0b34c0d6d555c", "packages": [ { "name": "adhocore/jwt", @@ -2177,11 +2177,11 @@ }, { "name": "utopia-php/websocket", - "version": "dev-fix-adapter-interface", + "version": "dev-main", "source": { "type": "git", "url": "https://github.com/utopia-php/websocket.git", - "reference": "23339d8f860c77f15e39b9772ca20b446b2a26ff" + "reference": "fdb82f0c2d61132dff5bd82111d64e4637fd63f1" }, "require": { "php": ">=8.0" @@ -2193,6 +2193,7 @@ "vimeo/psalm": "^4.8.1", "workerman/workerman": "^4.0" }, + "default-branch": true, "type": "library", "autoload": { "psr-4": { @@ -2220,7 +2221,7 @@ "utopia", "websocket" ], - "time": "2021-07-05T07:05:52+00:00" + "time": "2021-07-07T08:21:59+00:00" }, { "name": "webmozart/assert", From f62ea06f1eb590b6d410b195478c8826b3b6ed84 Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Tue, 13 Jul 2021 12:07:28 +0200 Subject: [PATCH 172/267] fix(realtime): add missing account channel --- src/Appwrite/Messaging/Adapter/Realtime.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Appwrite/Messaging/Adapter/Realtime.php b/src/Appwrite/Messaging/Adapter/Realtime.php index d513cf22bc..a7a8572ee3 100644 --- a/src/Appwrite/Messaging/Adapter/Realtime.php +++ b/src/Appwrite/Messaging/Adapter/Realtime.php @@ -234,11 +234,13 @@ class Realtime extends Adapter 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'); $permissions = ['user:' . $payload->getAttribute('userId')]; break; case strpos($event, 'account.') === 0: + $channels[] = 'account'; $channels[] = 'account.' . $payload->getId(); $permissions = ['user:' . $payload->getId()]; From 1589472606d68b8e41cd96cad4d5d9dffd8aebe3 Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Tue, 13 Jul 2021 12:09:05 +0200 Subject: [PATCH 173/267] chore(composer): udpate lock file --- composer.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/composer.lock b/composer.lock index 4cabaeedce..8dab12405a 100644 --- a/composer.lock +++ b/composer.lock @@ -2181,7 +2181,7 @@ "source": { "type": "git", "url": "https://github.com/utopia-php/websocket.git", - "reference": "fdb82f0c2d61132dff5bd82111d64e4637fd63f1" + "reference": "808317ef4ea0683c2c82dee5d543b1c8378e2e1b" }, "require": { "php": ">=8.0" @@ -2221,7 +2221,7 @@ "utopia", "websocket" ], - "time": "2021-07-07T08:21:59+00:00" + "time": "2021-07-11T13:09:44+00:00" }, { "name": "webmozart/assert", From c89f7b4c1f4cd2d7cabef88abd59b8131f0382ec Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Tue, 13 Jul 2021 12:20:26 +0200 Subject: [PATCH 174/267] fix(realtime): reset pdo connection on exception --- app/realtime.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/realtime.php b/app/realtime.php index a39b8ea45a..1d80b3f391 100644 --- a/app/realtime.php +++ b/app/realtime.php @@ -291,6 +291,10 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server, //} $server->send([$connection], json_encode($response)); $server->close($connection, $th->getCode()); + + if ($th instanceof PDOException) { + $db = null; + } } finally { /** * Put used PDO and Redis Connections back into their pools. From 882686952e85182d277f4e1e15e4ecf248d7ac56 Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Tue, 13 Jul 2021 17:18:02 +0200 Subject: [PATCH 175/267] adapt to review --- app/controllers/shared/api.php | 2 +- app/realtime.php | 8 +-- app/workers/functions.php | 12 ++-- src/Appwrite/Messaging/Adapter/Realtime.php | 55 ++++++++++--------- .../unit/Messaging/MessagingChannelsTest.php | 24 ++++---- tests/unit/Messaging/MessagingGuestTest.php | 52 +++++++++--------- tests/unit/Messaging/MessagingTest.php | 52 +++++++++--------- 7 files changed, 104 insertions(+), 101 deletions(-) diff --git a/app/controllers/shared/api.php b/app/controllers/shared/api.php index d275047c28..f41aab761d 100644 --- a/app/controllers/shared/api.php +++ b/app/controllers/shared/api.php @@ -206,7 +206,7 @@ App::shutdown(function ($utopia, $request, $response, $project, $events, $audits $response->getPayload(), $events->getParam('event'), $target['channels'], - $target['permissions'], + $target['roles'], [ 'permissionsChanged' => $target['permissionsChanged'], 'userId' => $events->getParam('userId') diff --git a/app/realtime.php b/app/realtime.php index 1d80b3f391..de39851996 100644 --- a/app/realtime.php +++ b/app/realtime.php @@ -96,12 +96,12 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats, $event = [ 'event' => 'stats.connections', 'channels' => ['project'], - 'permissions' => ['role:member'], + 'roles' => ['role:member'], 'timestamp' => time(), 'payload' => $payload ]; - $server->send($realtime->getReceivers($event), json_encode($event)); + $server->send($realtime->getSubscribers($event), json_encode($event)); } }); @@ -156,7 +156,7 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats, $register->get('redisPool')->put($cache); } - $receivers = $realtime->getReceivers($event); + $receivers = $realtime->getSubscribers($event); // Temporarily print debug logs by default for Alpha testing. // if (App::isDevelopment() && !empty($receivers)) { @@ -263,7 +263,7 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server, $roles = Auth::getRoles($user); - $channels = Realtime::convertChannels($request->getQuery('channels', []), $user); + $channels = Realtime::convertChannels($request->getQuery('channels', []), $user->getId()); /** * Channels Check diff --git a/app/workers/functions.php b/app/workers/functions.php index 1dcab7c7c6..df1c0b1db9 100644 --- a/app/workers/functions.php +++ b/app/workers/functions.php @@ -450,12 +450,12 @@ class FunctionsV1 extends Worker else { Console::info('Container is ready to run'); } - + $stdout = ''; $stderr = ''; $executionStart = \microtime(true); - + $exitCode = Console::execute("docker exec ".\implode(" ", $vars)." {$container} {$command}" , '', $stdout, $stderr, $function->getAttribute('timeout', (int) App::getEnv('_APP_FUNCTIONS_TIMEOUT', 900))); @@ -466,7 +466,7 @@ class FunctionsV1 extends Worker Console::info("Function executed in " . ($executionEnd - $executionStart) . " seconds with exit code {$exitCode}"); Authorization::disable(); - + $execution = $database->updateDocument(array_merge($execution->getArrayCopy(), [ 'tagId' => $tag->getId(), 'status' => $functionStatus, @@ -475,7 +475,7 @@ class FunctionsV1 extends Worker 'stderr' => \mb_substr($stderr, -4000), // log last 4000 chars output 'time' => $executionTime, ])); - + Authorization::reset(); if (false === $function) { @@ -501,7 +501,7 @@ class FunctionsV1 extends Worker $execution->getArrayCopy(), 'functions.executions.update', $target['channels'], - $target['permissions'] + $target['roles'] ); $usage = new Event('v1-usage', 'UsageV1'); @@ -515,7 +515,7 @@ class FunctionsV1 extends Worker ->setParam('networkRequestSize', 0) ->setParam('networkResponseSize', 0) ; - + if(App::getEnv('_APP_USAGE_STATS', 'enabled') == 'enabled') { $usage->trigger(); } diff --git a/src/Appwrite/Messaging/Adapter/Realtime.php b/src/Appwrite/Messaging/Adapter/Realtime.php index a7a8572ee3..0fd124fc97 100644 --- a/src/Appwrite/Messaging/Adapter/Realtime.php +++ b/src/Appwrite/Messaging/Adapter/Realtime.php @@ -43,6 +43,7 @@ class Realtime extends Adapter */ public function subscribe(string $projectId, mixed $connection, array $roles, array $channels): void { + //TODO: merge project & channel to a single layer if (!isset($this->subscriptions[$projectId])) { // Init Project $this->subscriptions[$projectId] = []; } @@ -65,7 +66,7 @@ class Realtime extends Adapter } /** - * Removes Subscription. + * Removes Subscription. * * @param mixed $connection * @return void @@ -101,10 +102,11 @@ class Realtime extends Adapter * @param string $projectId * @param string $role * @param string $channel - * @return bool + * @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]); @@ -121,22 +123,22 @@ class Realtime extends Adapter * @param array $payload * @param string $event * @param array $channels - * @param array $permissions + * @param array $roles * @param array $options * @return void */ - public static function send(string $project, array $payload, string $event, array $channels, array $permissions, array $options = []): void + public static function send(string $project, array $payload, string $event, array $channels, array $roles, array $options = []): void { if (empty($channels) || empty($permissions) || empty($project)) return; $permissionsChanged = array_key_exists('permissionsChanged', $options) && $options['permissionsChanged']; $userId = array_key_exists('userId', $options) ? $options['userId'] : null; - $redis = new \Redis(); + $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' => $project, - 'permissions' => $permissions, + 'roles' => $roles, 'permissionsChanged' => $permissionsChanged, 'userId' => $userId, 'data' => [ @@ -161,18 +163,19 @@ class Realtime extends Adapter * * @param array $event */ - public function getReceivers(array $event) + public function getSubscribers(array $event) { + //TODO: do comments $receivers = []; if (isset($this->subscriptions[$event['project']])) { foreach ($this->subscriptions[$event['project']] as $role => $subscription) { foreach ($event['data']['channels'] as $channel) { if ( \array_key_exists($channel, $this->subscriptions[$event['project']][$role]) - && (\in_array($role, $event['permissions']) || \in_array('*', $event['permissions'])) + && (\in_array($role, $event['roles']) || \in_array('*', $event['roles'])) ) { - foreach (array_keys($this->subscriptions[$event['project']][$role][$channel]) as $ids) { - $receivers[$ids] = 0; + foreach (array_keys($this->subscriptions[$event['project']][$role][$channel]) as $id) { + $receivers[$id] = 0; } break; } @@ -187,10 +190,10 @@ class Realtime extends Adapter * 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 Document $user + * @param string $userId * @return array */ - public static function convertChannels(array $channels, Document $user): array + public static function convertChannels(array $channels, string $userId): array { $channels = array_flip($channels); @@ -201,8 +204,8 @@ class Realtime extends Adapter break; case $key === 'account': - if (!empty($user->getId())) { - $channels['account.' . $user->getId()] = $value; + if (!empty($userId)) { + $channels['account.' . $userId] = $value; } unset($channels['account']); break; @@ -210,8 +213,8 @@ class Realtime extends Adapter } if (\array_key_exists('account', $channels)) { - if ($user->getId()) { - $channels['account.' . $user->getId()] = $channels['account']; + if ($userId) { + $channels['account.' . $userId] = $channels['account']; } unset($channels['account']); } @@ -227,7 +230,7 @@ class Realtime extends Adapter public static function fromPayload(string $event, Document $payload): array { $channels = []; - $permissions = []; + $roles = []; $permissionsChanged = false; switch (true) { @@ -236,46 +239,46 @@ class Realtime extends Adapter case strpos($event, 'account.verification.') === 0: $channels[] = 'account'; $channels[] = 'account.' . $payload->getAttribute('userId'); - $permissions = ['user:' . $payload->getAttribute('userId')]; + $roles = ['user:' . $payload->getAttribute('userId')]; break; case strpos($event, 'account.') === 0: $channels[] = 'account'; $channels[] = 'account.' . $payload->getId(); - $permissions = ['user:' . $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(); - $permissions = ['team:' . $payload->getAttribute('teamId')]; + $roles = ['team:' . $payload->getAttribute('teamId')]; break; case strpos($event, 'teams.') === 0: $permissionsChanged = $event === 'teams.create'; $channels[] = 'teams'; $channels[] = 'teams.' . $payload->getId(); - $permissions = ['team:' . $payload->getId()]; + $roles = ['team:' . $payload->getId()]; break; case strpos($event, 'database.collections.') === 0: $channels[] = 'collections'; $channels[] = 'collections.' . $payload->getId(); - $permissions = $payload->getAttribute('$permissions.read'); + $roles = $payload->getAttribute('$permissions.read'); break; case strpos($event, 'database.documents.') === 0: $channels[] = 'documents'; $channels[] = 'collections.' . $payload->getAttribute('$collection') . '.documents'; $channels[] = 'documents.' . $payload->getId(); - $permissions = $payload->getAttribute('$permissions.read'); + $roles = $payload->getAttribute('$permissions.read'); break; case strpos($event, 'storage.') === 0: $channels[] = 'files'; $channels[] = 'files.' . $payload->getId(); - $permissions = $payload->getAttribute('$permissions.read'); + $roles = $payload->getAttribute('$permissions.read'); break; case strpos($event, 'functions.executions.') === 0: @@ -283,14 +286,14 @@ class Realtime extends Adapter $channels[] = 'executions'; $channels[] = 'executions.' . $payload->getId(); $channels[] = 'functions.' . $payload->getAttribute('functionId'); - $permissions = $payload->getAttribute('$permissions.read'); + $roles = $payload->getAttribute('$permissions.read'); } break; } return [ 'channels' => $channels, - 'permissions' => $permissions, + 'roles' => $roles, 'permissionsChanged' => $permissionsChanged ]; } diff --git a/tests/unit/Messaging/MessagingChannelsTest.php b/tests/unit/Messaging/MessagingChannelsTest.php index a16197d637..d412573334 100644 --- a/tests/unit/Messaging/MessagingChannelsTest.php +++ b/tests/unit/Messaging/MessagingChannelsTest.php @@ -62,7 +62,7 @@ class MessagingChannelsTest extends TestCase $roles = Auth::getRoles($user); - $parsedChannels = Realtime::convertChannels([0 => $channel], $user); + $parsedChannels = Realtime::convertChannels([0 => $channel], $user->getId()); $this->realtime->subscribe( '1', @@ -86,7 +86,7 @@ class MessagingChannelsTest extends TestCase $roles = Auth::getRoles($user); - $parsedChannels = Realtime::convertChannels([0 => $channel], $user); + $parsedChannels = Realtime::convertChannels([0 => $channel], $user->getId()); $this->realtime->subscribe( '1', @@ -153,7 +153,7 @@ class MessagingChannelsTest extends TestCase foreach ($this->allChannels as $index => $channel) { $event = [ 'project' => '1', - 'permissions' => ['*'], + 'roles' => ['*'], 'data' => [ 'channels' => [ 0 => $channel, @@ -161,7 +161,7 @@ class MessagingChannelsTest extends TestCase ] ]; - $receivers = $this->realtime->getReceivers($event); + $receivers = $this->realtime->getSubscribers($event); /** * Every Client subscribed to the Wildcard should receive this event. @@ -186,7 +186,7 @@ class MessagingChannelsTest extends TestCase $event = [ 'project' => '1', - 'permissions' => $permissions, + 'roles' => $permissions, 'data' => [ 'channels' => [ 0 => $channel, @@ -194,7 +194,7 @@ class MessagingChannelsTest extends TestCase ] ]; - $receivers = $this->realtime->getReceivers($event); + $receivers = $this->realtime->getSubscribers($event); /** * Every Role subscribed to a Channel should receive this event. @@ -220,7 +220,7 @@ class MessagingChannelsTest extends TestCase } $event = [ 'project' => '1', - 'permissions' => $permissions, + 'roles' => $permissions, 'data' => [ 'channels' => [ 0 => $channel, @@ -228,7 +228,7 @@ class MessagingChannelsTest extends TestCase ] ]; - $receivers = $this->realtime->getReceivers($event); + $receivers = $this->realtime->getSubscribers($event); /** * Every Client subscribed to a Channel should receive this event. @@ -254,7 +254,7 @@ class MessagingChannelsTest extends TestCase } $event = [ 'project' => '1', - 'permissions' => $permissions, + 'roles' => $permissions, 'data' => [ 'channels' => [ 0 => $channel, @@ -262,7 +262,7 @@ class MessagingChannelsTest extends TestCase ] ]; - $receivers = $this->realtime->getReceivers($event); + $receivers = $this->realtime->getSubscribers($event); /** * Every Team Member should receive this event. @@ -280,7 +280,7 @@ class MessagingChannelsTest extends TestCase $event = [ 'project' => '1', - 'permissions' => $permissions, + 'roles' => $permissions, 'data' => [ 'channels' => [ 0 => $channel, @@ -288,7 +288,7 @@ class MessagingChannelsTest extends TestCase ] ]; - $receivers = $this->realtime->getReceivers($event); + $receivers = $this->realtime->getSubscribers($event); /** * Only 1 Team Member of a role should have access to a specific channel. diff --git a/tests/unit/Messaging/MessagingGuestTest.php b/tests/unit/Messaging/MessagingGuestTest.php index 804116604d..57714b1366 100644 --- a/tests/unit/Messaging/MessagingGuestTest.php +++ b/tests/unit/Messaging/MessagingGuestTest.php @@ -20,7 +20,7 @@ class MessagingGuestTest extends TestCase $event = [ 'project' => '1', - 'permissions' => ['*'], + 'roles' => ['*'], 'data' => [ 'channels' => [ 0 => 'documents', @@ -29,89 +29,89 @@ class MessagingGuestTest extends TestCase ] ]; - $receivers = $realtime->getReceivers($event); + $receivers = $realtime->getSubscribers($event); $this->assertCount(1, $receivers); $this->assertEquals(1, $receivers[0]); - $event['permissions'] = ['role:guest']; + $event['roles'] = ['role:guest']; - $receivers = $realtime->getReceivers($event); + $receivers = $realtime->getSubscribers($event); $this->assertCount(1, $receivers); $this->assertEquals(1, $receivers[0]); - $event['permissions'] = ['role:member']; + $event['roles'] = ['role:member']; - $receivers = $realtime->getReceivers($event); + $receivers = $realtime->getSubscribers($event); $this->assertEmpty($receivers); - $event['permissions'] = ['user:123']; + $event['roles'] = ['user:123']; - $receivers = $realtime->getReceivers($event); + $receivers = $realtime->getSubscribers($event); $this->assertEmpty($receivers); - $event['permissions'] = ['team:abc']; + $event['roles'] = ['team:abc']; - $receivers = $realtime->getReceivers($event); + $receivers = $realtime->getSubscribers($event); $this->assertEmpty($receivers); - $event['permissions'] = ['team:abc/administrator']; + $event['roles'] = ['team:abc/administrator']; - $receivers = $realtime->getReceivers($event); + $receivers = $realtime->getSubscribers($event); $this->assertEmpty($receivers); - $event['permissions'] = ['team:abc/god']; + $event['roles'] = ['team:abc/god']; - $receivers = $realtime->getReceivers($event); + $receivers = $realtime->getSubscribers($event); $this->assertEmpty($receivers); - $event['permissions'] = ['team:def']; + $event['roles'] = ['team:def']; - $receivers = $realtime->getReceivers($event); + $receivers = $realtime->getSubscribers($event); $this->assertEmpty($receivers); - $event['permissions'] = ['team:def/guest']; + $event['roles'] = ['team:def/guest']; - $receivers = $realtime->getReceivers($event); + $receivers = $realtime->getSubscribers($event); $this->assertEmpty($receivers); - $event['permissions'] = ['user:456']; + $event['roles'] = ['user:456']; - $receivers = $realtime->getReceivers($event); + $receivers = $realtime->getSubscribers($event); $this->assertEmpty($receivers); - $event['permissions'] = ['team:def/member']; + $event['roles'] = ['team:def/member']; - $receivers = $realtime->getReceivers($event); + $receivers = $realtime->getSubscribers($event); $this->assertEmpty($receivers); - $event['permissions'] = ['*']; + $event['roles'] = ['*']; $event['data']['channels'] = ['documents.123']; - $receivers = $realtime->getReceivers($event); + $receivers = $realtime->getSubscribers($event); $this->assertEmpty($receivers); $event['data']['channels'] = ['documents.789']; - $receivers = $realtime->getReceivers($event); + $receivers = $realtime->getSubscribers($event); $this->assertCount(1, $receivers); $this->assertEquals(1, $receivers[0]); $event['project'] = '2'; - $receivers = $realtime->getReceivers($event); + $receivers = $realtime->getSubscribers($event); $this->assertEmpty($receivers); diff --git a/tests/unit/Messaging/MessagingTest.php b/tests/unit/Messaging/MessagingTest.php index d0d0aab786..0d7aa4c3ab 100644 --- a/tests/unit/Messaging/MessagingTest.php +++ b/tests/unit/Messaging/MessagingTest.php @@ -29,7 +29,7 @@ class MessagingTest extends TestCase $event = [ 'project' => '1', - 'permissions' => ['*'], + 'roles' => ['*'], 'data' => [ 'channels' => [ 0 => 'account.123', @@ -37,89 +37,89 @@ class MessagingTest extends TestCase ] ]; - $receivers = $realtime->getReceivers($event); + $receivers = $realtime->getSubscribers($event); $this->assertCount(1, $receivers); $this->assertEquals(1, $receivers[0]); - $event['permissions'] = ['role:member']; + $event['roles'] = ['role:member']; - $receivers = $realtime->getReceivers($event); + $receivers = $realtime->getSubscribers($event); $this->assertCount(1, $receivers); $this->assertEquals(1, $receivers[0]); - $event['permissions'] = ['user:123']; + $event['roles'] = ['user:123']; - $receivers = $realtime->getReceivers($event); + $receivers = $realtime->getSubscribers($event); $this->assertCount(1, $receivers); $this->assertEquals(1, $receivers[0]); - $event['permissions'] = ['team:abc']; + $event['roles'] = ['team:abc']; - $receivers = $realtime->getReceivers($event); + $receivers = $realtime->getSubscribers($event); $this->assertCount(1, $receivers); $this->assertEquals(1, $receivers[0]); - $event['permissions'] = ['team:abc/administrator']; + $event['roles'] = ['team:abc/administrator']; - $receivers = $realtime->getReceivers($event); + $receivers = $realtime->getSubscribers($event); $this->assertCount(1, $receivers); $this->assertEquals(1, $receivers[0]); - $event['permissions'] = ['team:abc/moderator']; + $event['roles'] = ['team:abc/moderator']; - $receivers = $realtime->getReceivers($event); + $receivers = $realtime->getSubscribers($event); $this->assertCount(1, $receivers); $this->assertEquals(1, $receivers[0]); - $event['permissions'] = ['team:def']; + $event['roles'] = ['team:def']; - $receivers = $realtime->getReceivers($event); + $receivers = $realtime->getSubscribers($event); $this->assertCount(1, $receivers); $this->assertEquals(1, $receivers[0]); - $event['permissions'] = ['team:def/guest']; + $event['roles'] = ['team:def/guest']; - $receivers = $realtime->getReceivers($event); + $receivers = $realtime->getSubscribers($event); $this->assertCount(1, $receivers); $this->assertEquals(1, $receivers[0]); - $event['permissions'] = ['user:456']; + $event['roles'] = ['user:456']; - $receivers = $realtime->getReceivers($event); + $receivers = $realtime->getSubscribers($event); $this->assertEmpty($receivers); - $event['permissions'] = ['team:def/member']; + $event['roles'] = ['team:def/member']; - $receivers = $realtime->getReceivers($event); + $receivers = $realtime->getSubscribers($event); $this->assertEmpty($receivers); - $event['permissions'] = ['*']; + $event['roles'] = ['*']; $event['data']['channels'] = ['documents.123']; - $receivers = $realtime->getReceivers($event); + $receivers = $realtime->getSubscribers($event); $this->assertEmpty($receivers); $event['data']['channels'] = ['documents.789']; - $receivers = $realtime->getReceivers($event); + $receivers = $realtime->getSubscribers($event); $this->assertCount(1, $receivers); $this->assertEquals(1, $receivers[0]); $event['project'] = '2'; - $receivers = $realtime->getReceivers($event); + $receivers = $realtime->getSubscribers($event); $this->assertEmpty($receivers); @@ -148,7 +148,7 @@ class MessagingTest extends TestCase 4 => 'account.456' ]; - $channels = Realtime::convertChannels($channels, $user); + $channels = Realtime::convertChannels($channels, $user->getId()); $this->assertCount(3, $channels); $this->assertArrayHasKey('files', $channels); $this->assertArrayHasKey('documents', $channels); @@ -185,7 +185,7 @@ class MessagingTest extends TestCase 4 => 'account.456' ]; - $channels = Realtime::convertChannels($channels, $user); + $channels = Realtime::convertChannels($channels, $user->getId()); $this->assertCount(4, $channels); $this->assertArrayHasKey('files', $channels); From 5bccaeb39457bade032d97c4eda485a446ae5f10 Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Wed, 14 Jul 2021 10:52:26 +0200 Subject: [PATCH 176/267] fix(response): setStatusCode phpdoc block --- src/Appwrite/Utopia/Response.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Appwrite/Utopia/Response.php b/src/Appwrite/Utopia/Response.php index a6ec6891cc..527c529087 100644 --- a/src/Appwrite/Utopia/Response.php +++ b/src/Appwrite/Utopia/Response.php @@ -45,7 +45,7 @@ use Appwrite\Utopia\Response\Model\Mock; // Keep last use stdClass; /** - * @method Response public function setStatusCode(int $code = 200) + * @method Response setStatusCode(int $code = 200) */ class Response extends SwooleResponse { From 71d483dc7bf2dcbead62b805990405a45d73e1e4 Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Mon, 26 Jul 2021 11:05:33 +0200 Subject: [PATCH 177/267] fix(deps): update composer lock --- composer.lock | 41 +++++++++++++++++++++-------------------- 1 file changed, 21 insertions(+), 20 deletions(-) diff --git a/composer.lock b/composer.lock index 8dab12405a..db9fa5af64 100644 --- a/composer.lock +++ b/composer.lock @@ -2285,27 +2285,27 @@ "packages-dev": [ { "name": "amphp/amp", - "version": "v2.5.2", + "version": "v2.6.0", "source": { "type": "git", "url": "https://github.com/amphp/amp.git", - "reference": "efca2b32a7580087adb8aabbff6be1dc1bb924a9" + "reference": "caa95edeb1ca1bf7532e9118ede4a3c3126408cc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/amphp/amp/zipball/efca2b32a7580087adb8aabbff6be1dc1bb924a9", - "reference": "efca2b32a7580087adb8aabbff6be1dc1bb924a9", + "url": "https://api.github.com/repos/amphp/amp/zipball/caa95edeb1ca1bf7532e9118ede4a3c3126408cc", + "reference": "caa95edeb1ca1bf7532e9118ede4a3c3126408cc", "shasum": "" }, "require": { - "php": ">=7" + "php": ">=7.1" }, "require-dev": { "amphp/php-cs-fixer-config": "dev-master", "amphp/phpunit-util": "^1", "ext-json": "*", "jetbrains/phpstorm-stubs": "^2019.3", - "phpunit/phpunit": "^6.0.9 | ^7", + "phpunit/phpunit": "^7 | ^8 | ^9", "psalm/phar": "^3.11@dev", "react/promise": "^2" }, @@ -2362,7 +2362,7 @@ "support": { "irc": "irc://irc.freenode.org/amphp", "issues": "https://github.com/amphp/amp/issues", - "source": "https://github.com/amphp/amp/tree/v2.5.2" + "source": "https://github.com/amphp/amp/tree/v2.6.0" }, "funding": [ { @@ -2370,7 +2370,7 @@ "type": "github" } ], - "time": "2021-01-10T17:06:37+00:00" + "time": "2021-07-16T20:06:06+00:00" }, { "name": "amphp/byte-stream", @@ -3165,16 +3165,16 @@ }, { "name": "nikic/php-parser", - "version": "v4.11.0", + "version": "v4.12.0", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "fe14cf3672a149364fb66dfe11bf6549af899f94" + "reference": "6608f01670c3cc5079e18c1dab1104e002579143" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/fe14cf3672a149364fb66dfe11bf6549af899f94", - "reference": "fe14cf3672a149364fb66dfe11bf6549af899f94", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/6608f01670c3cc5079e18c1dab1104e002579143", + "reference": "6608f01670c3cc5079e18c1dab1104e002579143", "shasum": "" }, "require": { @@ -3215,9 +3215,9 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v4.11.0" + "source": "https://github.com/nikic/PHP-Parser/tree/v4.12.0" }, - "time": "2021-07-03T13:36:55+00:00" + "time": "2021-07-21T10:44:31+00:00" }, { "name": "openlss/lib-array2xml", @@ -3274,16 +3274,16 @@ }, { "name": "phar-io/manifest", - "version": "2.0.1", + "version": "2.0.3", "source": { "type": "git", "url": "https://github.com/phar-io/manifest.git", - "reference": "85265efd3af7ba3ca4b2a2c34dbfc5788dd29133" + "reference": "97803eca37d319dfa7826cc2437fc020857acb53" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phar-io/manifest/zipball/85265efd3af7ba3ca4b2a2c34dbfc5788dd29133", - "reference": "85265efd3af7ba3ca4b2a2c34dbfc5788dd29133", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/97803eca37d319dfa7826cc2437fc020857acb53", + "reference": "97803eca37d319dfa7826cc2437fc020857acb53", "shasum": "" }, "require": { @@ -3328,9 +3328,9 @@ "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", "support": { "issues": "https://github.com/phar-io/manifest/issues", - "source": "https://github.com/phar-io/manifest/tree/master" + "source": "https://github.com/phar-io/manifest/tree/2.0.3" }, - "time": "2020-06-27T14:33:11+00:00" + "time": "2021-07-20T11:28:43+00:00" }, { "name": "phar-io/version", @@ -4930,6 +4930,7 @@ "type": "github" } ], + "abandoned": true, "time": "2020-09-28T06:45:17+00:00" }, { From 889d6aa1c083888dddf8f431edfbd6a8c85b36cb Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Mon, 26 Jul 2021 11:05:42 +0200 Subject: [PATCH 178/267] tests(realtime): adapt account channel --- src/Appwrite/Messaging/Adapter/Realtime.php | 2 +- tests/e2e/Services/Realtime/RealtimeBase.php | 50 ++++++++++++-------- 2 files changed, 31 insertions(+), 21 deletions(-) diff --git a/src/Appwrite/Messaging/Adapter/Realtime.php b/src/Appwrite/Messaging/Adapter/Realtime.php index 0fd124fc97..1bd1245a96 100644 --- a/src/Appwrite/Messaging/Adapter/Realtime.php +++ b/src/Appwrite/Messaging/Adapter/Realtime.php @@ -129,7 +129,7 @@ class Realtime extends Adapter */ public static function send(string $project, array $payload, string $event, array $channels, array $roles, array $options = []): void { - if (empty($channels) || empty($permissions) || empty($project)) return; + if (empty($channels) || empty($roles) || empty($project)) return; $permissionsChanged = array_key_exists('permissionsChanged', $options) && $options['permissionsChanged']; $userId = array_key_exists('userId', $options) ? $options['userId'] : null; diff --git a/tests/e2e/Services/Realtime/RealtimeBase.php b/tests/e2e/Services/Realtime/RealtimeBase.php index 90d2fe6770..5407d13a50 100644 --- a/tests/e2e/Services/Realtime/RealtimeBase.php +++ b/tests/e2e/Services/Realtime/RealtimeBase.php @@ -167,9 +167,10 @@ trait RealtimeBase $response = json_decode($client->receive(), true); - $this->assertCount(1, $response['channels']); + $this->assertCount(2, $response['channels']); $this->assertArrayHasKey('timestamp', $response); - $this->assertEquals('account.' . $userId, $response['channels'][0]); + $this->assertContains('account', $response['channels']); + $this->assertContains('account.' . $userId, $response['channels']); $this->assertEquals('account.update.name', $response['event']); $this->assertNotEmpty($response['payload']); @@ -191,9 +192,10 @@ trait RealtimeBase $response = json_decode($client->receive(), true); - $this->assertCount(1, $response['channels']); + $this->assertCount(2, $response['channels']); $this->assertArrayHasKey('timestamp', $response); - $this->assertEquals('account.' . $userId, $response['channels'][0]); + $this->assertContains('account', $response['channels']); + $this->assertContains('account.' . $userId, $response['channels']); $this->assertEquals('account.update.password', $response['event']); $this->assertNotEmpty($response['payload']); @@ -214,9 +216,10 @@ trait RealtimeBase $response = json_decode($client->receive(), true); - $this->assertCount(1, $response['channels']); + $this->assertCount(2, $response['channels']); $this->assertArrayHasKey('timestamp', $response); - $this->assertEquals('account.' . $userId, $response['channels'][0]); + $this->assertContains('account', $response['channels']); + $this->assertContains('account.' . $userId, $response['channels']); $this->assertEquals('account.update.email', $response['event']); $this->assertNotEmpty($response['payload']); @@ -236,9 +239,10 @@ trait RealtimeBase $response = json_decode($client->receive(), true); - $this->assertCount(1, $response['channels']); + $this->assertCount(2, $response['channels']); $this->assertArrayHasKey('timestamp', $response); - $this->assertEquals('account.' . $userId, $response['channels'][0]); + $this->assertContains('account', $response['channels']); + $this->assertContains('account.' . $userId, $response['channels']); $this->assertEquals('account.verification.create', $response['event']); $lastEmail = $this->getLastEmail(); @@ -259,9 +263,10 @@ trait RealtimeBase $response = json_decode($client->receive(), true); - $this->assertCount(1, $response['channels']); + $this->assertCount(2, $response['channels']); $this->assertArrayHasKey('timestamp', $response); - $this->assertEquals('account.' . $userId, $response['channels'][0]); + $this->assertContains('account', $response['channels']); + $this->assertContains('account.' . $userId, $response['channels']); $this->assertEquals('account.verification.update', $response['event']); /** @@ -281,9 +286,10 @@ trait RealtimeBase $response = json_decode($client->receive(), true); - $this->assertCount(1, $response['channels']); + $this->assertCount(2, $response['channels']); $this->assertArrayHasKey('timestamp', $response); - $this->assertEquals('account.' . $userId, $response['channels'][0]); + $this->assertContains('account', $response['channels']); + $this->assertContains('account.' . $userId, $response['channels']); $this->assertEquals('account.update.prefs', $response['event']); $this->assertNotEmpty($response['payload']); @@ -304,9 +310,10 @@ trait RealtimeBase $response = json_decode($client->receive(), true); - $this->assertCount(1, $response['channels']); + $this->assertCount(2, $response['channels']); $this->assertArrayHasKey('timestamp', $response); - $this->assertEquals('account.' . $userId, $response['channels'][0]); + $this->assertContains('account', $response['channels']); + $this->assertContains('account.' . $userId, $response['channels']); $this->assertEquals('account.sessions.create', $response['event']); $this->assertNotEmpty($response['payload']); @@ -322,9 +329,10 @@ trait RealtimeBase $response = json_decode($client->receive(), true); - $this->assertCount(1, $response['channels']); + $this->assertCount(2, $response['channels']); $this->assertArrayHasKey('timestamp', $response); - $this->assertEquals('account.' . $userId, $response['channels'][0]); + $this->assertContains('account', $response['channels']); + $this->assertContains('account.' . $userId, $response['channels']); $this->assertEquals('account.sessions.delete', $response['event']); $this->assertNotEmpty($response['payload']); @@ -345,9 +353,10 @@ trait RealtimeBase $lastEmail = $this->getLastEmail(); $recovery = substr($lastEmail['text'], strpos($lastEmail['text'], '&secret=', 0) + 8, 256); - $this->assertCount(1, $response['channels']); + $this->assertCount(2, $response['channels']); $this->assertArrayHasKey('timestamp', $response); - $this->assertEquals('account.' . $userId, $response['channels'][0]); + $this->assertContains('account', $response['channels']); + $this->assertContains('account.' . $userId, $response['channels']); $this->assertEquals('account.recovery.create', $response['event']); $this->assertNotEmpty($response['payload']); @@ -364,9 +373,10 @@ trait RealtimeBase $response = json_decode($client->receive(), true); - $this->assertCount(1, $response['channels']); + $this->assertCount(2, $response['channels']); $this->assertArrayHasKey('timestamp', $response); - $this->assertEquals('account.' . $userId, $response['channels'][0]); + $this->assertContains('account', $response['channels']); + $this->assertContains('account.' . $userId, $response['channels']); $this->assertEquals('account.recovery.update', $response['event']); $this->assertNotEmpty($response['payload']); From d4c7600980cb7f3dcebd3a787eb5cceb990a11c5 Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Mon, 26 Jul 2021 11:07:09 +0200 Subject: [PATCH 179/267] tests(realtime): run the actual tests on travis --- phpunit.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/phpunit.xml b/phpunit.xml index f0a2813f5a..2fbb75e221 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -20,6 +20,7 @@ ./tests/e2e/General ./tests/e2e/Scopes ./tests/e2e/Services/Account + ./tests/e2e/Services/Realtime ./tests/e2e/Services/Avatars ./tests/e2e/Services/Database ./tests/e2e/Services/Health From 8366b6fe03511542a7cbb9a78d2a9070a63d10f7 Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Mon, 16 Aug 2021 11:11:22 +0200 Subject: [PATCH 180/267] chore(composer): update lock file --- composer.lock | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/composer.lock b/composer.lock index 3d2ddc9d52..1cba628928 100644 --- a/composer.lock +++ b/composer.lock @@ -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": "3647b0e23acf63473fd0b34c0d6d555c", + "content-hash": "b5f4227860b0b7697a79405cc8bdfba9", "packages": [ { "name": "adhocore/jwt", @@ -1756,16 +1756,16 @@ }, { "name": "utopia-php/framework", - "version": "0.17.2", + "version": "0.17.3", "source": { "type": "git", "url": "https://github.com/utopia-php/framework.git", - "reference": "3cd5fa2a9e30040277861f4254c5ccd1b1600952" + "reference": "0274f6b3e49db2af0d702edf278ec7504dc99878" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/framework/zipball/3cd5fa2a9e30040277861f4254c5ccd1b1600952", - "reference": "3cd5fa2a9e30040277861f4254c5ccd1b1600952", + "url": "https://api.github.com/repos/utopia-php/framework/zipball/0274f6b3e49db2af0d702edf278ec7504dc99878", + "reference": "0274f6b3e49db2af0d702edf278ec7504dc99878", "shasum": "" }, "require": { @@ -1799,9 +1799,9 @@ ], "support": { "issues": "https://github.com/utopia-php/framework/issues", - "source": "https://github.com/utopia-php/framework/tree/0.17.2" + "source": "https://github.com/utopia-php/framework/tree/0.17.3" }, - "time": "2021-08-02T10:18:26+00:00" + "time": "2021-08-03T13:57:01+00:00" }, { "name": "utopia-php/image", @@ -4930,7 +4930,6 @@ "type": "github" } ], - "abandoned": true, "time": "2020-09-28T06:45:17+00:00" }, { @@ -6189,5 +6188,5 @@ "platform-overrides": { "php": "8.0" }, - "plugin-api-version": "2.1.0" + "plugin-api-version": "2.0.0" } From e0f0a6e24b33ccbb0e8c00bca729980e73530ac1 Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Tue, 17 Aug 2021 10:58:33 +0200 Subject: [PATCH 181/267] fix(console): realtime --- app/init.php | 2 +- app/realtime.php | 12 +++++++----- public/scripts/dependencies/appwrite.js | 2 +- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/app/init.php b/app/init.php index 9013b397bc..50e65874bf 100644 --- a/app/init.php +++ b/app/init.php @@ -465,7 +465,7 @@ App::setResource('user', function($mode, $project, $console, $request, $response } catch (JWTException $error) { throw new Exception('Failed to verify JWT. '.$error->getMessage(), 401); } - + $jwtUserId = $payload['userId'] ?? ''; $jwtSessionId = $payload['sessionId'] ?? ''; diff --git a/app/realtime.php b/app/realtime.php index 80a859fa37..febe3679c6 100644 --- a/app/realtime.php +++ b/app/realtime.php @@ -95,14 +95,16 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats, $event = [ 'project' => 'console', - 'event' => 'stats.connections', - 'channels' => ['project'], 'roles' => ['role:member'], - 'timestamp' => time(), - 'payload' => $payload + 'data' => [ + 'event' => 'stats.connections', + 'channels' => ['project'], + 'timestamp' => time(), + 'payload' => $payload + ] ]; - $server->send($realtime->getSubscribers($event), json_encode($event)); + $server->send($realtime->getSubscribers($event), json_encode($event['data'])); } }); diff --git a/public/scripts/dependencies/appwrite.js b/public/scripts/dependencies/appwrite.js index ec69054d38..53eb94520f 100644 --- a/public/scripts/dependencies/appwrite.js +++ b/public/scripts/dependencies/appwrite.js @@ -512,7 +512,7 @@ * @param {string} failure * @param {string[]} scopes * @throws {AppwriteException} - * @returns {void|string} + * @returns {void|URL} */ createOAuth2Session: (provider, success, failure, scopes) => { if (typeof provider === 'undefined') { From fefd82680c8d1a4f0863fd18f8c76ee4fae63f6f Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Tue, 17 Aug 2021 11:08:18 +0200 Subject: [PATCH 182/267] fix(realtime): whitespace --- app/realtime.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/realtime.php b/app/realtime.php index 779eb590ef..f4a552ed3e 100644 --- a/app/realtime.php +++ b/app/realtime.php @@ -67,7 +67,7 @@ $server->onStart(function () use ($stats, $register, $containerId, &$documentId) } ]; }; - + /** * Create document for this worker for connection stats across Containers. */ From cbc282a24c17136173620d223a6c1ee7bf06d873 Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Tue, 17 Aug 2021 13:18:32 +0200 Subject: [PATCH 183/267] feat(realtime): channel for tests --- app/realtime.php | 38 ++++++++++++++++++++++++++++++-------- 1 file changed, 30 insertions(+), 8 deletions(-) diff --git a/app/realtime.php b/app/realtime.php index de39851996..7ed13f04cd 100644 --- a/app/realtime.php +++ b/app/realtime.php @@ -83,10 +83,10 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats, $start = time(); $redisPool = $register->get('redisPool'); - /** - * Sending current connections to project channels on the console project every 5 seconds. - */ Timer::tick(5000, function () use ($server, $stats, $realtime) { + /** + * Sending current connections to project channels on the console project every 5 seconds. + */ if ($realtime->hasSubscriber('console', 'role:member', 'project')) { $payload = []; foreach ($stats as $projectId => $value) { @@ -94,14 +94,36 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats, } $event = [ - 'event' => 'stats.connections', - 'channels' => ['project'], + 'project' => 'console', 'roles' => ['role:member'], - 'timestamp' => time(), - 'payload' => $payload + 'data' => [ + 'event' => 'stats.connections', + 'channels' => ['project'], + 'timestamp' => time(), + 'payload' => $payload + ] ]; - $server->send($realtime->getSubscribers($event), json_encode($event)); + $server->send($realtime->getSubscribers($event), json_encode($event['data'])); + } + /** + * 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($event['data'])); } }); From 1a6869fefc5cea0acca1267393ed010ce57a06d6 Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Wed, 18 Aug 2021 12:20:10 +0200 Subject: [PATCH 184/267] fix usage project --- app/views/console/home/index.phtml | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/app/views/console/home/index.phtml b/app/views/console/home/index.phtml index cafa098046..f0c1a3a27a 100644 --- a/app/views/console/home/index.phtml +++ b/app/views/console/home/index.phtml @@ -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"> @@ -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"> @@ -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"> @@ -106,6 +109,9 @@ $usageStatsEnabled = $this->getParam('usageStatsEnabled',true);
+

+ Realtime +

@@ -129,7 +135,9 @@ $usageStatsEnabled = $this->getParam('usageStatsEnabled',true);
- +

+ Total +

@@ -156,8 +164,8 @@ $usageStatsEnabled = $this->getParam('usageStatsEnabled',true);
-
-

Platforms

+
+

Platforms