From f5539e6a8d6a3351b8ee9b08720b00246aef782e Mon Sep 17 00:00:00 2001 From: Joe Dixon Date: Mon, 4 Dec 2023 14:47:34 +0000 Subject: [PATCH 1/7] increase payload size --- src/Http/Request.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Http/Request.php b/src/Http/Request.php index a462e1ce..ceea1a49 100644 --- a/src/Http/Request.php +++ b/src/Http/Request.php @@ -20,7 +20,7 @@ class Request * * @var int */ - const MAX_SIZE = 4096; + const MAX_SIZE = 10000; /** * Turn the raw message into a Psr7 request. From e1aac88351711436421149778ea6303498a6f7b2 Mon Sep 17 00:00:00 2001 From: Joe Dixon Date: Mon, 4 Dec 2023 15:29:43 +0000 Subject: [PATCH 2/7] validate events --- composer.json | 1 + .../Http/Controllers/EventsController.php | 50 ++++++++++++++-- tests/Feature/Reverb/EventsControllerTest.php | 60 +++++++++++++------ 3 files changed, 86 insertions(+), 25 deletions(-) diff --git a/composer.json b/composer.json index 2b825fbe..ec1510e8 100644 --- a/composer.json +++ b/composer.json @@ -24,6 +24,7 @@ "illuminate/http": "^10.0", "illuminate/redis": "^10.0", "illuminate/support": "^10.0", + "illuminate/validation": "*", "ratchet/rfc6455": "^0.3.1", "react/socket": "^1.14", "symfony/http-foundation": "^6.3" diff --git a/src/Pusher/Http/Controllers/EventsController.php b/src/Pusher/Http/Controllers/EventsController.php index ce014b62..a5dc2ca5 100644 --- a/src/Pusher/Http/Controllers/EventsController.php +++ b/src/Pusher/Http/Controllers/EventsController.php @@ -2,7 +2,11 @@ namespace Laravel\Reverb\Pusher\Http\Controllers; +use Illuminate\Contracts\Validation\Validator; use Illuminate\Support\Arr; +use Illuminate\Support\Facades\Validator as ValidatorFacade; +use Laravel\Reverb\Channels\Channel; +use Laravel\Reverb\Channels\Concerns\InteractsWithPresenceChannels; use Laravel\Reverb\Event; use Laravel\Reverb\Http\Connection; use Psr\Http\Message\RequestInterface; @@ -17,9 +21,15 @@ class EventsController extends Controller public function __invoke(RequestInterface $request, Connection $connection, string $appId): Response { $this->verify($request, $connection, $appId); - // @TODO Validate the request body as a JSON object in the correct format. $payload = json_decode($this->body, true); + + $validator = $this->validate($payload); + + if ($validator->fails()) { + return new JsonResponse($validator->errors(), 422); + } + $channels = Arr::wrap($payload['channels'] ?? $payload['channel'] ?? []); Event::dispatch( @@ -50,15 +60,43 @@ protected function getInfo(array $channels, string $info): array $info = explode(',', $info); $channels = collect($channels)->mapWithKeys(function ($channel) use ($info) { - $count = count($this->channels->find($channel)->connections()); + if (! $channel = $this->channels->find($channel)) { + return []; + } + + $count = count($channel->connections()); + $info = [ - 'user_count' => in_array('user_count', $info) ? $count : null, - 'subscription_count' => in_array('subscription_count', $info) ? $count : null, + 'user_count' => in_array('user_count', $info) && $this->isPresenceChannel($channel) ? $count : null, + 'subscription_count' => in_array('subscription_count', $info) && ! $this->isPresenceChannel($channel) ? $count : null, ]; - return [$channel => array_filter($info, fn ($item) => $item !== null)]; - })->all(); + return [$channel->name() => (object) array_filter($info, fn ($item) => $item !== null)]; + })->filter()->all(); return ['channels' => $channels]; } + + /** + * Determine if the channel is a presence channel. + */ + protected function isPresenceChannel(Channel $channel): bool + { + return in_array(InteractsWithPresenceChannels::class, class_uses($channel)); + } + + /** + * Validate the incoming request. + */ + protected function validate(array $payload): Validator + { + return ValidatorFacade::make($payload, [ + 'name' => ['required', 'string'], + 'data' => ['required', 'array'], + 'channels' => ['required_without:channel', 'array'], + 'channel' => ['required_without:channels', 'string'], + 'socket_id' => ['string'], + 'info' => ['string'], + ]); + } } diff --git a/tests/Feature/Reverb/EventsControllerTest.php b/tests/Feature/Reverb/EventsControllerTest.php index 5d12db4c..96e1f28e 100644 --- a/tests/Feature/Reverb/EventsControllerTest.php +++ b/tests/Feature/Reverb/EventsControllerTest.php @@ -1,6 +1,7 @@ subscribe('test-channel-one'); + $this->subscribe('presence-test-channel-one'); $response = await($this->signedPostRequest('events', [ 'name' => 'NewEvent', - 'channels' => ['test-channel-one', 'test-channel-two'], + 'channels' => ['presence-test-channel-one', 'test-channel-two'], 'data' => ['some' => 'data'], 'info' => 'user_count', ])); $this->assertSame(200, $response->getStatusCode()); - $this->assertSame('{"channels":{"test-channel-one":{"user_count":1},"test-channel-two":{"user_count":0}}}', $response->getBody()->getContents()); + $this->assertSame('{"channels":{"presence-test-channel-one":{"user_count":1},"test-channel-two":{}}}', $response->getBody()->getContents()); }); it('can return subscription counts when requested', function () { @@ -47,27 +48,13 @@ $response = await($this->signedPostRequest('events', [ 'name' => 'NewEvent', - 'channels' => ['test-channel-one', 'test-channel-two'], + 'channels' => ['presence-test-channel-one', 'test-channel-two'], 'data' => ['some' => 'data'], 'info' => 'subscription_count', ])); $this->assertSame(200, $response->getStatusCode()); - $this->assertSame('{"channels":{"test-channel-one":{"subscription_count":0},"test-channel-two":{"subscription_count":1}}}', $response->getBody()->getContents()); -}); - -it('can return user and subscription counts when requested', function () { - $this->subscribe('test-channel-two'); - - $response = await($this->signedPostRequest('events', [ - 'name' => 'NewEvent', - 'channels' => ['test-channel-one', 'test-channel-two'], - 'data' => ['some' => 'data'], - 'info' => 'subscription_count,user_count', - ])); - - $this->assertSame(200, $response->getStatusCode()); - $this->assertSame('{"channels":{"test-channel-one":{"user_count":0,"subscription_count":0},"test-channel-two":{"user_count":1,"subscription_count":1}}}', $response->getBody()->getContents()); + $this->assertSame('{"channels":{"presence-test-channel-one":{},"test-channel-two":{"subscription_count":1}}}', $response->getBody()->getContents()); }); it('can ignore a subscriber', function () { @@ -93,3 +80,38 @@ $this->assertSame('{}', $response->getBody()->getContents()); expect(await($promiseTwo))->toBeFalse(); }); + +it('validates invalid data', function ($payload) { + await($this->signedPostRequest('events', $payload)); +}) +->throws(ResponseException::class, exceptionCode: 422) +->with([ + [ + [ + 'name' => 'NewEvent', + 'channel' => 'test-channel', + ], + ], + [ + [ + 'name' => 'NewEvent', + 'channels' => ['test-channel-one', 'test-channel-two'], + ], + ], + [ + [ + 'name' => 'NewEvent', + 'channel' => 'test-channel', + 'data' => ['some' => 'data'], + 'socket_id' => 1234, + ], + ], + [ + [ + 'name' => 'NewEvent', + 'channel' => 'test-channel', + 'data' => ['some' => 'data'], + 'info' => 1234, + ], + ], +]); From c1eec615e5ff940ce4ce599aeef853cac592a9f2 Mon Sep 17 00:00:00 2001 From: Joe Dixon Date: Mon, 4 Dec 2023 20:38:47 +0000 Subject: [PATCH 3/7] wip --- composer.json | 7 +-- .../InteractsWithChannelInformation.php | 54 +++++++++++++++++++ .../Http/Controllers/ChannelsController.php | 17 ++++-- .../Controllers/EventsBatchController.php | 41 ++++++++++++-- .../Http/Controllers/EventsController.php | 45 +++------------- .../Feature/Reverb/ChannelsControllerTest.php | 27 +++++++--- .../Reverb/EventsBatchControllerTest.php | 42 ++++++++------- tests/ReverbTestCase.php | 1 - 8 files changed, 153 insertions(+), 81 deletions(-) create mode 100644 src/Pusher/Concerns/InteractsWithChannelInformation.php diff --git a/composer.json b/composer.json index ec1510e8..7d09423c 100644 --- a/composer.json +++ b/composer.json @@ -19,12 +19,7 @@ "aws/aws-sdk-php": "^3.241", "clue/redis-react": "^2.6", "guzzlehttp/psr7": "^2.6", - "illuminate/cache": "^10.0", - "illuminate/console": "^10.0", - "illuminate/http": "^10.0", - "illuminate/redis": "^10.0", - "illuminate/support": "^10.0", - "illuminate/validation": "*", + "illuminate/contracts": "^10.0", "ratchet/rfc6455": "^0.3.1", "react/socket": "^1.14", "symfony/http-foundation": "^6.3" diff --git a/src/Pusher/Concerns/InteractsWithChannelInformation.php b/src/Pusher/Concerns/InteractsWithChannelInformation.php new file mode 100644 index 00000000..2c4fb6ec --- /dev/null +++ b/src/Pusher/Concerns/InteractsWithChannelInformation.php @@ -0,0 +1,54 @@ +mapWithKeys(function ($channel) use ($info) { + $name = $channel instanceof Channel ? $channel->name() : $channel; + + return [$name => $this->info($name, $info)]; + })->all(); + } + + /** + * Get the info for the given channels. + * + * @param array $channels + * @return array> + */ + protected function info(string $channel, string $info): array + { + $info = explode(',', $info); + + if (! $channel = app(ChannelManager::class)->find($channel)) { + return []; + } + + $count = count($channel->connections()); + + $info = [ + 'user_count' => in_array('user_count', $info) && $this->isPresenceChannel($channel) ? $count : null, + 'subscription_count' => in_array('subscription_count', $info) && ! $this->isPresenceChannel($channel) ? $count : null, + ]; + + return array_filter($info, fn ($item) => $item !== null); + } + + /** + * Determine if the channel is a presence channel. + */ + protected function isPresenceChannel(Channel $channel): bool + { + return in_array(InteractsWithPresenceChannels::class, class_uses($channel)); + } +} diff --git a/src/Pusher/Http/Controllers/ChannelsController.php b/src/Pusher/Http/Controllers/ChannelsController.php index 0acdb5c1..422db776 100644 --- a/src/Pusher/Http/Controllers/ChannelsController.php +++ b/src/Pusher/Http/Controllers/ChannelsController.php @@ -4,12 +4,15 @@ use Illuminate\Support\Str; use Laravel\Reverb\Http\Connection; +use Laravel\Reverb\Pusher\Concerns\InteractsWithChannelInformation; use Psr\Http\Message\RequestInterface; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Response; class ChannelsController extends Controller { + use InteractsWithChannelInformation; + /** * Handle the request. */ @@ -18,16 +21,20 @@ public function __invoke(RequestInterface $request, Connection $connection, stri $this->verify($request, $connection, $appId); $channels = collect($this->channels->all()); - $info = explode(',', $this->query['info'] ?? ''); if (isset($this->query['filter_by_prefix'])) { $channels = $channels->filter(fn ($channel) => Str::startsWith($channel->name(), $this->query['filter_by_prefix'])); } - $channels = $channels->mapWithKeys(function ($channel) use ($info) { - return [$channel->name() => array_filter(['user_count' => in_array('user_count', $info) ? count($channel->connections()) : null])]; - }); + $channels = $channels->filter(fn ($channel) => count($channel->connections()) > 0); + + $channels = $this->infoForChannels( + $channels->all(), + $this->query['info'] ?? '' + ); - return new JsonResponse((object) ['channels' => $channels]); + return new JsonResponse([ + 'channels' => array_map(fn ($item) => (object) $item, $channels) + ]); } } diff --git a/src/Pusher/Http/Controllers/EventsBatchController.php b/src/Pusher/Http/Controllers/EventsBatchController.php index 9834baca..3d746611 100644 --- a/src/Pusher/Http/Controllers/EventsBatchController.php +++ b/src/Pusher/Http/Controllers/EventsBatchController.php @@ -2,23 +2,35 @@ namespace Laravel\Reverb\Pusher\Http\Controllers; +use Illuminate\Contracts\Validation\Validator; +use Illuminate\Support\Facades\Validator as ValidatorFacade; use Laravel\Reverb\Event; use Laravel\Reverb\Http\Connection; +use Laravel\Reverb\Pusher\Concerns\InteractsWithChannelInformation; use Psr\Http\Message\RequestInterface; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Response; class EventsBatchController extends Controller { + use InteractsWithChannelInformation; + /** * Handle the request. */ public function __invoke(RequestInterface $request, Connection $connection, string $appId): Response { $this->verify($request, $connection, $appId); - // @TODO Validate the request body as a JSON array of events in the correct format and a max of 10 items. - $items = collect(json_decode($this->body, true)); + $payload = json_decode($this->body, true); + + $validator = $this->validate($payload); + + if ($validator->fails()) { + return new JsonResponse($validator->errors(), 422); + } + + $items = collect($payload['batch']); $info = $items->map(function ($item) { Event::dispatch( @@ -31,10 +43,16 @@ public function __invoke(RequestInterface $request, Connection $connection, stri isset($item['socket_id']) ? ($this->channels->connections()[$item['socket_id']] ?? null) : null ); - return isset($item['info']) ? $this->getInfo($item['channel'], $item['info']) : []; + return isset($item['info']) ? $this->info($item['channel'], $item['info']) : []; }); - return $info->some(fn ($item) => count($item) > 0) ? new JsonResponse((object) ['batch' => $info->all()]) : new JsonResponse((object) []); + if ($info->some(fn ($item) => count($item) > 0)) { + return new JsonResponse( + ['batch' => $info->each(fn ($item) => (object) $item)->all()] + ); + } + + return new JsonResponse(['batch' => (object) []]); } /** @@ -54,4 +72,19 @@ protected function getInfo(string $channel, string $info): array return array_filter($info, fn ($item) => $item !== null); } + + /** + * Validate the incoming request. + */ + protected function validate(array $payload): Validator + { + return ValidatorFacade::make($payload, [ + 'batch' => ['required', 'array'], + 'batch.*.name' => ['required', 'string'], + 'batch.*.data' => ['required', 'array'], + 'batch.*.channel' => ['required_without:channels', 'string'], + 'batch.*.socket_id' => ['string'], + 'batch.*.info' => ['string'], + ]); + } } diff --git a/src/Pusher/Http/Controllers/EventsController.php b/src/Pusher/Http/Controllers/EventsController.php index a5dc2ca5..90e87fa3 100644 --- a/src/Pusher/Http/Controllers/EventsController.php +++ b/src/Pusher/Http/Controllers/EventsController.php @@ -5,16 +5,17 @@ use Illuminate\Contracts\Validation\Validator; use Illuminate\Support\Arr; use Illuminate\Support\Facades\Validator as ValidatorFacade; -use Laravel\Reverb\Channels\Channel; -use Laravel\Reverb\Channels\Concerns\InteractsWithPresenceChannels; use Laravel\Reverb\Event; use Laravel\Reverb\Http\Connection; +use Laravel\Reverb\Pusher\Concerns\InteractsWithChannelInformation; use Psr\Http\Message\RequestInterface; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Response; class EventsController extends Controller { + use InteractsWithChannelInformation; + /** * Handle the request. */ @@ -43,48 +44,14 @@ public function __invoke(RequestInterface $request, Connection $connection, stri ); if (isset($payload['info'])) { - return new JsonResponse((object) $this->getInfo($channels, $payload['info'])); + return new JsonResponse([ + 'channels' => array_map(fn ($item) => (object) $item, $this->infoForChannels($channels, $payload['info'])), + ]); } return new JsonResponse((object) []); } - /** - * Get the info for the given channels. - * - * @param array $channels - * @return array> - */ - protected function getInfo(array $channels, string $info): array - { - $info = explode(',', $info); - - $channels = collect($channels)->mapWithKeys(function ($channel) use ($info) { - if (! $channel = $this->channels->find($channel)) { - return []; - } - - $count = count($channel->connections()); - - $info = [ - 'user_count' => in_array('user_count', $info) && $this->isPresenceChannel($channel) ? $count : null, - 'subscription_count' => in_array('subscription_count', $info) && ! $this->isPresenceChannel($channel) ? $count : null, - ]; - - return [$channel->name() => (object) array_filter($info, fn ($item) => $item !== null)]; - })->filter()->all(); - - return ['channels' => $channels]; - } - - /** - * Determine if the channel is a presence channel. - */ - protected function isPresenceChannel(Channel $channel): bool - { - return in_array(InteractsWithPresenceChannels::class, class_uses($channel)); - } - /** * Validate the incoming request. */ diff --git a/tests/Feature/Reverb/ChannelsControllerTest.php b/tests/Feature/Reverb/ChannelsControllerTest.php index 5e75e1af..70ba72d9 100644 --- a/tests/Feature/Reverb/ChannelsControllerTest.php +++ b/tests/Feature/Reverb/ChannelsControllerTest.php @@ -1,5 +1,6 @@ subscribe('test-channel-one'); - $this->subscribe('test-channel-two'); + $this->subscribe('presence-test-channel-two'); $response = await($this->signedRequest('channels?info=user_count')); $this->assertSame(200, $response->getStatusCode()); - $this->assertSame('{"channels":{"test-channel-one":{"user_count":1},"test-channel-two":{"user_count":1}}}', $response->getBody()->getContents()); + $this->assertSame('{"channels":{"test-channel-one":{},"presence-test-channel-two":{"user_count":1}}}', $response->getBody()->getContents()); }); it('can return filtered channels', function () { $this->subscribe('test-channel-one'); - $this->subscribe('test-channel-two'); + $this->subscribe('presence-test-channel-two'); - $response = await($this->signedRequest('channels?filter_by_prefix=test-channel-t&info=user_count')); + $response = await($this->signedRequest('channels?filter_by_prefix=presence-test-channel-t&info=user_count')); $this->assertSame(200, $response->getStatusCode()); - $this->assertSame('{"channels":{"test-channel-two":{"user_count":1}}}', $response->getBody()->getContents()); + $this->assertSame('{"channels":{"presence-test-channel-two":{"user_count":1}}}', $response->getBody()->getContents()); }); it('returns empty results if no metrics requested', function () { @@ -33,5 +34,19 @@ $response = await($this->signedRequest('channels')); $this->assertSame(200, $response->getStatusCode()); - $this->assertSame('{"channels":{"test-channel-one":[],"test-channel-two":[]}}', $response->getBody()->getContents()); + $this->assertSame('{"channels":{"test-channel-one":{},"test-channel-two":{}}}', $response->getBody()->getContents()); +}); + +it('only returns occupied channels', function () { + $this->subscribe('test-channel-one'); + $this->subscribe('test-channel-two'); + + $channels = channelManager(); + $connection = Arr::first($channels->connections()); + $channels->unsubscribeFromAll($connection->connection()); + + $response = await($this->signedRequest('channels')); + + $this->assertSame(200, $response->getStatusCode()); + $this->assertSame('{"channels":{"test-channel-two":{}}}', $response->getBody()->getContents()); }); diff --git a/tests/Feature/Reverb/EventsBatchControllerTest.php b/tests/Feature/Reverb/EventsBatchControllerTest.php index 1bd1bb58..9b6ebcee 100644 --- a/tests/Feature/Reverb/EventsBatchControllerTest.php +++ b/tests/Feature/Reverb/EventsBatchControllerTest.php @@ -7,18 +7,20 @@ uses(ReverbTestCase::class); it('can receive an event batch trigger', function () { - $response = await($this->signedPostRequest('batch_events', [[ - 'name' => 'NewEvent', - 'channel' => 'test-channel', - 'data' => ['some' => 'data'], + $response = await($this->signedPostRequest('batch_events', ['batch' => [ + [ + 'name' => 'NewEvent', + 'channel' => 'test-channel', + 'data' => ['some' => 'data'], + ], ]])); - $this->assertSame(200, $response->getStatusCode()); - $this->assertSame('{}', $response->getBody()->getContents()); + expect($response->getStatusCode())->toBe(200); + expect($response->getBody()->getContents())->toBe('{"batch":{}}'); }); it('can receive an event batch trigger with multiple events', function () { - $response = await($this->signedPostRequest('batch_events', [ + $response = await($this->signedPostRequest('batch_events', ['batch' => [ [ 'name' => 'NewEvent', 'channel' => 'test-channel', @@ -29,17 +31,17 @@ 'channel' => 'test-channel-two', 'data' => ['some' => ['more' => 'data']], ], - ])); + ]])); - $this->assertSame(200, $response->getStatusCode()); - $this->assertSame('{}', $response->getBody()->getContents()); + expect($response->getStatusCode())->toBe(200); + expect($response->getBody()->getContents())->toBe('{"batch":{}}'); }); it('can receive an event batch trigger with multiple events and return info for each', function () { - $response = await($this->signedPostRequest('batch_events', [ + $response = await($this->signedPostRequest('batch_events', ['batch' => [ [ 'name' => 'NewEvent', - 'channel' => 'test-channel', + 'channel' => 'presence-test-channel', 'data' => ['some' => 'data'], 'info' => 'user_count', ], @@ -55,17 +57,17 @@ 'data' => ['some' => ['more' => 'data']], 'info' => 'subscription_count,user_count', ], - ])); + ]])); - $this->assertSame(200, $response->getStatusCode()); - $this->assertSame('{"batch":[{"user_count":0},{"subscription_count":0},{"user_count":0,"subscription_count":0}]}', $response->getBody()->getContents()); + expect($response->getStatusCode())->toBe(200); + expect($response->getBody()->getContents())->toBe('{"batch":[{"user_count":0},{"subscription_count":0},{"subscription_count":0}]}'); }); it('can receive an event batch trigger with multiple events and return info for some', function () { - $response = await($this->signedPostRequest('batch_events', [ + $response = await($this->signedPostRequest('batch_events', ['batch' => [ [ 'name' => 'NewEvent', - 'channel' => 'test-channel', + 'channel' => 'presence-test-channel', 'data' => ['some' => 'data'], 'info' => 'user_count', ], @@ -74,8 +76,8 @@ 'channel' => 'test-channel-two', 'data' => ['some' => ['more' => 'data']], ], - ])); + ]])); - $this->assertSame(200, $response->getStatusCode()); - $this->assertSame('{"batch":[{"user_count":0},[]]}', $response->getBody()->getContents()); + expect($response->getStatusCode())->toBe(200); + expect($response->getBody()->getContents())->toBe('{"batch":[{"user_count":0},[]]}'); }); diff --git a/tests/ReverbTestCase.php b/tests/ReverbTestCase.php index 1efe9b38..0e42493e 100644 --- a/tests/ReverbTestCase.php +++ b/tests/ReverbTestCase.php @@ -38,7 +38,6 @@ protected function setUp(): void { parent::setUp(); - $this->app->instance(Logger::class, new NullLogger); $this->loop = Loop::get(); $this->startServer(); } From ddebe52824aae90e7afba2bbf303df22b41ca9b9 Mon Sep 17 00:00:00 2001 From: Joe Dixon Date: Mon, 4 Dec 2023 20:54:35 +0000 Subject: [PATCH 4/7] format cache channels --- .../InteractsWithChannelInformation.php | 11 ++++++++++ .../Http/Controllers/ChannelController.php | 14 ++++--------- .../Feature/Reverb/ChannelControllerTest.php | 21 +++++++------------ 3 files changed, 22 insertions(+), 24 deletions(-) diff --git a/src/Pusher/Concerns/InteractsWithChannelInformation.php b/src/Pusher/Concerns/InteractsWithChannelInformation.php index 2c4fb6ec..e056197f 100644 --- a/src/Pusher/Concerns/InteractsWithChannelInformation.php +++ b/src/Pusher/Concerns/InteractsWithChannelInformation.php @@ -2,6 +2,7 @@ namespace Laravel\Reverb\Pusher\Concerns; +use Laravel\Reverb\Channels\CacheChannel; use Laravel\Reverb\Channels\Channel; use Laravel\Reverb\Channels\Concerns\InteractsWithPresenceChannels; use Laravel\Reverb\Contracts\ChannelManager; @@ -37,8 +38,10 @@ protected function info(string $channel, string $info): array $count = count($channel->connections()); $info = [ + 'occupied' => in_array('occupied', $info) ? $count > 0 : null, 'user_count' => in_array('user_count', $info) && $this->isPresenceChannel($channel) ? $count : null, 'subscription_count' => in_array('subscription_count', $info) && ! $this->isPresenceChannel($channel) ? $count : null, + 'cache' => in_array('cache', $info) && $this->isCacheChannel($channel) ? $channel->cachedPayload() : null, ]; return array_filter($info, fn ($item) => $item !== null); @@ -51,4 +54,12 @@ protected function isPresenceChannel(Channel $channel): bool { return in_array(InteractsWithPresenceChannels::class, class_uses($channel)); } + + /** + * Determine if the channel is a cache channel. + */ + protected function isCacheChannel(Channel $channel): bool + { + return $channel instanceof CacheChannel; + } } diff --git a/src/Pusher/Http/Controllers/ChannelController.php b/src/Pusher/Http/Controllers/ChannelController.php index 6bf0baaa..87af542a 100644 --- a/src/Pusher/Http/Controllers/ChannelController.php +++ b/src/Pusher/Http/Controllers/ChannelController.php @@ -3,12 +3,15 @@ namespace Laravel\Reverb\Pusher\Http\Controllers; use Laravel\Reverb\Http\Connection; +use Laravel\Reverb\Pusher\Concerns\InteractsWithChannelInformation; use Psr\Http\Message\RequestInterface; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Response; class ChannelController extends Controller { + use InteractsWithChannelInformation; + /** * Handle the request. */ @@ -16,15 +19,6 @@ public function __invoke(RequestInterface $request, Connection $connection, stri { $this->verify($request, $connection, $appId); - $info = explode(',', $this->query['info'] ?? ''); - $connections = $this->channels->find($channel)->connections(); - $totalConnections = count($connections); - - return new JsonResponse((object) array_filter([ - 'occupied' => $totalConnections > 0, - 'user_count' => in_array('user_count', $info) ? $totalConnections : null, - 'subscription_count' => in_array('subscription_count', $info) ? $totalConnections : null, - 'cache' => in_array('cache', $info) ? '{}' : null, - ], fn ($item) => $item !== null)); + return new JsonResponse((object) $this->info($channel, ($this->query['info'] ?? '').',occupied')); } } diff --git a/tests/Feature/Reverb/ChannelControllerTest.php b/tests/Feature/Reverb/ChannelControllerTest.php index 993ecd6e..7f230bb1 100644 --- a/tests/Feature/Reverb/ChannelControllerTest.php +++ b/tests/Feature/Reverb/ChannelControllerTest.php @@ -13,28 +13,21 @@ $response = await($this->signedRequest('channels/test-channel-one?info=user_count,subscription_count,cache')); $this->assertSame(200, $response->getStatusCode()); - $this->assertSame('{"occupied":true,"user_count":2,"subscription_count":2,"cache":"{}"}', $response->getBody()->getContents()); + $this->assertSame('{"occupied":true,"subscription_count":2}', $response->getBody()->getContents()); }); it('returns unoccupied when no connections', function () { $response = await($this->signedRequest('channels/test-channel-one?info=user_count,subscription_count,cache')); $this->assertSame(200, $response->getStatusCode()); - $this->assertSame('{"occupied":false,"user_count":0,"subscription_count":0,"cache":"{}"}', $response->getBody()->getContents()); + $this->assertSame('{"occupied":false,"subscription_count":0}', $response->getBody()->getContents()); }); -it('can return only the requested attributes', function () { - $this->subscribe('test-channel-one'); - - $response = await($this->signedRequest('channels/test-channel-one?info=user_count,subscription_count,cache')); - $this->assertSame(200, $response->getStatusCode()); - $this->assertSame('{"occupied":true,"user_count":1,"subscription_count":1,"cache":"{}"}', $response->getBody()->getContents()); - - $response = await($this->signedRequest('channels/test-channel-one?info=cache')); - $this->assertSame(200, $response->getStatusCode()); - $this->assertSame('{"occupied":true,"cache":"{}"}', $response->getBody()->getContents()); +it('can return cache channel attributes', function () { + $this->subscribe('cache-test-channel-one'); + channelManager()->find('cache-test-channel-one')->broadcast(['some' => 'data']); - $response = await($this->signedRequest('channels/test-channel-one?info=subscription_count,user_count')); + $response = await($this->signedRequest('channels/cache-test-channel-one?info=subscription_count,cache')); $this->assertSame(200, $response->getStatusCode()); - $this->assertSame('{"occupied":true,"user_count":1,"subscription_count":1}', $response->getBody()->getContents()); + $this->assertSame('{"occupied":true,"subscription_count":1,"cache":{"some":"data"}}', $response->getBody()->getContents()); }); From a0d1f64baccd2576291b9a707386feebd889c0a4 Mon Sep 17 00:00:00 2001 From: Joe Dixon Date: Mon, 4 Dec 2023 20:56:43 +0000 Subject: [PATCH 5/7] corrrectly get channel users --- src/Pusher/Http/Controllers/ChannelUsersController.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Pusher/Http/Controllers/ChannelUsersController.php b/src/Pusher/Http/Controllers/ChannelUsersController.php index 0ac0b045..99fd62f1 100644 --- a/src/Pusher/Http/Controllers/ChannelUsersController.php +++ b/src/Pusher/Http/Controllers/ChannelUsersController.php @@ -4,12 +4,15 @@ use Laravel\Reverb\Channels\PresenceChannel; use Laravel\Reverb\Http\Connection; +use Laravel\Reverb\Pusher\Concerns\InteractsWithChannelInformation; use Psr\Http\Message\RequestInterface; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Response; class ChannelUsersController extends Controller { + use InteractsWithChannelInformation; + /** * Handle the request. */ @@ -19,7 +22,7 @@ public function __invoke(RequestInterface $request, Connection $connection, stri $channel = $this->channels->find($channel); - if (! $channel instanceof PresenceChannel) { + if (! $this->isPresenceChannel($channel)) { return new JsonResponse((object) [], 400); } From c6c444837f61377ff7bfd134581b99fe3ba00ff3 Mon Sep 17 00:00:00 2001 From: Joe Dixon Date: Mon, 4 Dec 2023 21:05:24 +0000 Subject: [PATCH 6/7] terminate with user id --- .../Http/Controllers/UsersTerminateController.php | 10 ++++++---- .../Reverb/UsersTerminateControllerTest.php | 14 +++++++------- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/src/Pusher/Http/Controllers/UsersTerminateController.php b/src/Pusher/Http/Controllers/UsersTerminateController.php index 6bd2a934..50bd0da0 100644 --- a/src/Pusher/Http/Controllers/UsersTerminateController.php +++ b/src/Pusher/Http/Controllers/UsersTerminateController.php @@ -16,11 +16,13 @@ public function __invoke(RequestInterface $request, Connection $connection, stri { $this->verify($request, $connection, $appId); - if (! $connection = $this->channels->connections()[$userId]) { - return new JsonResponse((object) [], 400); - } + $connections = collect($this->channels->connections()); - $connection->connection()->disconnect(); + $connections->each(function ($connection) use ($userId) { + if ((string) $connection->data()['user_id'] === $userId) { + $connection->disconnect(); + } + }); return new JsonResponse((object) []); } diff --git a/tests/Feature/Reverb/UsersTerminateControllerTest.php b/tests/Feature/Reverb/UsersTerminateControllerTest.php index 58e4b068..06e9a21f 100644 --- a/tests/Feature/Reverb/UsersTerminateControllerTest.php +++ b/tests/Feature/Reverb/UsersTerminateControllerTest.php @@ -13,20 +13,20 @@ it('unsubscribes from all channels and terminates a user', function () { $connection = $this->connect(); - $this->subscribe('test-channel-one', connection: $connection); + $this->subscribe('presence-test-channel-one', ['user_id' => '123'], connection: $connection); $this->subscribe('test-channel-two', connection: $connection); $connection = $this->connect(); - $this->subscribe('test-channel-one', connection: $connection); + $this->subscribe('presence-test-channel-one', ['user_id' => '456'], connection: $connection); $this->subscribe('test-channel-two', connection: $connection); - expect(collect(channelManager()->all())->get('test-channel-one')->connections())->toHaveCount(2); - expect(collect(channelManager()->all())->get('test-channel-two')->connections())->toHaveCount(2); + expect(collect(channelManager()->find('presence-test-channel-one')->connections()))->toHaveCount(2); + expect(collect(channelManager()->find('test-channel-two')->connections()))->toHaveCount(2); - $response = await($this->signedPostRequest("users/{$this->connectionId}/terminate_connections")); + $response = await($this->signedPostRequest("users/456/terminate_connections")); $this->assertSame(200, $response->getStatusCode()); $this->assertSame('{}', $response->getBody()->getContents()); - expect(collect(channelManager()->all())->get('test-channel-one')->connections())->toHaveCount(1); - expect(collect(channelManager()->all())->get('test-channel-two')->connections())->toHaveCount(1); + expect(collect(channelManager()->find('presence-test-channel-one')->connections()))->toHaveCount(1); + expect(collect(channelManager()->find('test-channel-two')->connections()))->toHaveCount(1); }); From 1a263c6ecab4690cf68e3c4fd9af1b60fc9880b7 Mon Sep 17 00:00:00 2001 From: Joe Dixon Date: Mon, 4 Dec 2023 21:05:38 +0000 Subject: [PATCH 7/7] formatting --- .../Controllers/ChannelUsersController.php | 1 - .../Http/Controllers/ChannelsController.php | 4 +- .../Feature/Reverb/ChannelsControllerTest.php | 2 +- tests/Feature/Reverb/EventsControllerTest.php | 46 +++++++++---------- .../Reverb/UsersTerminateControllerTest.php | 2 +- tests/ReverbTestCase.php | 2 - 6 files changed, 27 insertions(+), 30 deletions(-) diff --git a/src/Pusher/Http/Controllers/ChannelUsersController.php b/src/Pusher/Http/Controllers/ChannelUsersController.php index 99fd62f1..075872cc 100644 --- a/src/Pusher/Http/Controllers/ChannelUsersController.php +++ b/src/Pusher/Http/Controllers/ChannelUsersController.php @@ -2,7 +2,6 @@ namespace Laravel\Reverb\Pusher\Http\Controllers; -use Laravel\Reverb\Channels\PresenceChannel; use Laravel\Reverb\Http\Connection; use Laravel\Reverb\Pusher\Concerns\InteractsWithChannelInformation; use Psr\Http\Message\RequestInterface; diff --git a/src/Pusher/Http/Controllers/ChannelsController.php b/src/Pusher/Http/Controllers/ChannelsController.php index 422db776..a78383cf 100644 --- a/src/Pusher/Http/Controllers/ChannelsController.php +++ b/src/Pusher/Http/Controllers/ChannelsController.php @@ -27,14 +27,14 @@ public function __invoke(RequestInterface $request, Connection $connection, stri } $channels = $channels->filter(fn ($channel) => count($channel->connections()) > 0); - + $channels = $this->infoForChannels( $channels->all(), $this->query['info'] ?? '' ); return new JsonResponse([ - 'channels' => array_map(fn ($item) => (object) $item, $channels) + 'channels' => array_map(fn ($item) => (object) $item, $channels), ]); } } diff --git a/tests/Feature/Reverb/ChannelsControllerTest.php b/tests/Feature/Reverb/ChannelsControllerTest.php index 70ba72d9..c11ee27c 100644 --- a/tests/Feature/Reverb/ChannelsControllerTest.php +++ b/tests/Feature/Reverb/ChannelsControllerTest.php @@ -42,7 +42,7 @@ $this->subscribe('test-channel-two'); $channels = channelManager(); - $connection = Arr::first($channels->connections()); + $connection = Arr::first($channels->connections()); $channels->unsubscribeFromAll($connection->connection()); $response = await($this->signedRequest('channels')); diff --git a/tests/Feature/Reverb/EventsControllerTest.php b/tests/Feature/Reverb/EventsControllerTest.php index 96e1f28e..f59fed45 100644 --- a/tests/Feature/Reverb/EventsControllerTest.php +++ b/tests/Feature/Reverb/EventsControllerTest.php @@ -84,34 +84,34 @@ it('validates invalid data', function ($payload) { await($this->signedPostRequest('events', $payload)); }) -->throws(ResponseException::class, exceptionCode: 422) -->with([ - [ + ->throws(ResponseException::class, exceptionCode: 422) + ->with([ [ - 'name' => 'NewEvent', - 'channel' => 'test-channel', + [ + 'name' => 'NewEvent', + 'channel' => 'test-channel', + ], ], - ], - [ [ - 'name' => 'NewEvent', - 'channels' => ['test-channel-one', 'test-channel-two'], + [ + 'name' => 'NewEvent', + 'channels' => ['test-channel-one', 'test-channel-two'], + ], ], - ], - [ [ - 'name' => 'NewEvent', - 'channel' => 'test-channel', - 'data' => ['some' => 'data'], - 'socket_id' => 1234, + [ + 'name' => 'NewEvent', + 'channel' => 'test-channel', + 'data' => ['some' => 'data'], + 'socket_id' => 1234, + ], ], - ], - [ [ - 'name' => 'NewEvent', - 'channel' => 'test-channel', - 'data' => ['some' => 'data'], - 'info' => 1234, + [ + 'name' => 'NewEvent', + 'channel' => 'test-channel', + 'data' => ['some' => 'data'], + 'info' => 1234, + ], ], - ], -]); + ]); diff --git a/tests/Feature/Reverb/UsersTerminateControllerTest.php b/tests/Feature/Reverb/UsersTerminateControllerTest.php index 06e9a21f..57165b0b 100644 --- a/tests/Feature/Reverb/UsersTerminateControllerTest.php +++ b/tests/Feature/Reverb/UsersTerminateControllerTest.php @@ -23,7 +23,7 @@ expect(collect(channelManager()->find('presence-test-channel-one')->connections()))->toHaveCount(2); expect(collect(channelManager()->find('test-channel-two')->connections()))->toHaveCount(2); - $response = await($this->signedPostRequest("users/456/terminate_connections")); + $response = await($this->signedPostRequest('users/456/terminate_connections')); $this->assertSame(200, $response->getStatusCode()); $this->assertSame('{}', $response->getBody()->getContents()); diff --git a/tests/ReverbTestCase.php b/tests/ReverbTestCase.php index 0e42493e..14b86cd4 100644 --- a/tests/ReverbTestCase.php +++ b/tests/ReverbTestCase.php @@ -6,9 +6,7 @@ use Illuminate\Support\Str; use Laravel\Reverb\Concerns\InteractsWithAsyncRedis; use Laravel\Reverb\Contracts\Connection; -use Laravel\Reverb\Contracts\Logger; use Laravel\Reverb\Event; -use Laravel\Reverb\Loggers\NullLogger; use Laravel\Reverb\ServerManager; use Laravel\Reverb\Servers\Reverb\Factory; use Ratchet\Client\WebSocket;