tipo: modificación del cpanel y gitignore de la carpeta vendor
This commit is contained in:
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Reverb\Protocols\Pusher\Channels;
|
||||
|
||||
use Laravel\Reverb\Contracts\Connection;
|
||||
|
||||
class CacheChannel extends Channel
|
||||
{
|
||||
/**
|
||||
* Data from last event triggered.
|
||||
*/
|
||||
protected ?array $payload = null;
|
||||
|
||||
/**
|
||||
* Send a message to all connections subscribed to the channel.
|
||||
*/
|
||||
public function broadcast(array $payload, ?Connection $except = null): void
|
||||
{
|
||||
$this->payload = $payload;
|
||||
|
||||
parent::broadcast($payload, $except);
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcast a message triggered from an internal source.
|
||||
*/
|
||||
public function broadcastInternally(array $payload, ?Connection $except = null): void
|
||||
{
|
||||
parent::broadcast($payload, $except);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the channel has a cached payload.
|
||||
*/
|
||||
public function hasCachedPayload(): bool
|
||||
{
|
||||
return $this->payload !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the cached payload.
|
||||
*/
|
||||
public function cachedPayload(): ?array
|
||||
{
|
||||
return $this->payload;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Reverb\Protocols\Pusher\Channels;
|
||||
|
||||
use Laravel\Reverb\Contracts\Connection;
|
||||
use Laravel\Reverb\Loggers\Log;
|
||||
use Laravel\Reverb\Protocols\Pusher\Concerns\SerializesChannels;
|
||||
use Laravel\Reverb\Protocols\Pusher\Contracts\ChannelConnectionManager;
|
||||
use Laravel\Reverb\Protocols\Pusher\Contracts\ChannelManager;
|
||||
|
||||
class Channel
|
||||
{
|
||||
use SerializesChannels;
|
||||
|
||||
/**
|
||||
* The channel connections.
|
||||
*
|
||||
* @var \Laravel\Reverb\Contracts\ChannelConnectionManager
|
||||
*/
|
||||
protected $connections;
|
||||
|
||||
/**
|
||||
* Create a new channel instance.
|
||||
*/
|
||||
public function __construct(protected string $name)
|
||||
{
|
||||
$this->connections = app(ChannelConnectionManager::class)->for($this->name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the channel name.
|
||||
*/
|
||||
public function name(): string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all connections for the channel.
|
||||
*
|
||||
* @return array<string, \Laravel\Reverb\Protocols\Pusher\Channels\ChannelConnection>
|
||||
*/
|
||||
public function connections(): array
|
||||
{
|
||||
return $this->connections->all();
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a connection.
|
||||
*/
|
||||
public function find(Connection $connection): ?ChannelConnection
|
||||
{
|
||||
return $this->connections->find($connection);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a connection by its ID.
|
||||
*/
|
||||
public function findById(string $id): ?ChannelConnection
|
||||
{
|
||||
return $this->connections->findById($id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe to the given channel.
|
||||
*/
|
||||
public function subscribe(Connection $connection, ?string $auth = null, ?string $data = null): void
|
||||
{
|
||||
$this->connections->add($connection, $data ? json_decode($data, associative: true, flags: JSON_THROW_ON_ERROR) : []);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsubscribe from the given channel.
|
||||
*/
|
||||
public function unsubscribe(Connection $connection): void
|
||||
{
|
||||
$this->connections->remove($connection);
|
||||
|
||||
if ($this->connections->isEmpty()) {
|
||||
app(ChannelManager::class)->for($connection->app())->remove($this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the connection is subscribed to the channel.
|
||||
*/
|
||||
public function subscribed(Connection $connection): bool
|
||||
{
|
||||
return $this->connections->find($connection) !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a message to all connections subscribed to the channel.
|
||||
*/
|
||||
public function broadcast(array $payload, ?Connection $except = null): void
|
||||
{
|
||||
if ($except === null) {
|
||||
$this->broadcastToAll($payload);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$message = json_encode($payload);
|
||||
|
||||
Log::info('Broadcasting To', $this->name());
|
||||
Log::message($message);
|
||||
|
||||
foreach ($this->connections() as $connection) {
|
||||
if ($except->id() === $connection->id()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$connection->send($message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a broadcast to all connections.
|
||||
*/
|
||||
public function broadcastToAll(array $payload): void
|
||||
{
|
||||
$message = json_encode($payload);
|
||||
|
||||
Log::info('Broadcasting To', $this->name());
|
||||
Log::message($message);
|
||||
|
||||
foreach ($this->connections() as $connection) {
|
||||
$connection->send($message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcast a message triggered from an internal source.
|
||||
*/
|
||||
public function broadcastInternally(array $payload, ?Connection $except = null): void
|
||||
{
|
||||
$this->broadcast($payload, $except);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the data associated with the channel.
|
||||
*/
|
||||
public function data(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Reverb\Protocols\Pusher\Channels;
|
||||
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class ChannelBroker
|
||||
{
|
||||
/**
|
||||
* Return the relevant channel instance.
|
||||
*/
|
||||
public static function create(string $name): Channel
|
||||
{
|
||||
return match (true) {
|
||||
Str::startsWith($name, 'private-cache-') => new PrivateCacheChannel($name),
|
||||
Str::startsWith($name, 'presence-cache-') => new PresenceCacheChannel($name),
|
||||
Str::startsWith($name, 'cache') => new CacheChannel($name),
|
||||
Str::startsWith($name, 'private') => new PrivateChannel($name),
|
||||
Str::startsWith($name, 'presence') => new PresenceChannel($name),
|
||||
default => new Channel($name),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Reverb\Protocols\Pusher\Channels;
|
||||
|
||||
use Illuminate\Support\Arr;
|
||||
use Laravel\Reverb\Contracts\Connection;
|
||||
|
||||
class ChannelConnection
|
||||
{
|
||||
/**
|
||||
* Create a new channel connection instance.
|
||||
*/
|
||||
public function __construct(protected Connection $connection, protected array $data = [])
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the underlying connection.
|
||||
*/
|
||||
public function connection(): Connection
|
||||
{
|
||||
return $this->connection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the connection data.
|
||||
*/
|
||||
public function data(?string $key = null): mixed
|
||||
{
|
||||
return $key ? Arr::get($this->data, $key) : $this->data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a message to the connection.
|
||||
*/
|
||||
public function send(string $message): void
|
||||
{
|
||||
$this->connection->send($message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Proxy the given method to the underlying connection.
|
||||
*/
|
||||
public function __call(string $method, array $parameters): mixed
|
||||
{
|
||||
return $this->connection->{$method}(...$parameters);
|
||||
}
|
||||
}
|
||||
Vendored
+104
@@ -0,0 +1,104 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Reverb\Protocols\Pusher\Channels\Concerns;
|
||||
|
||||
use Laravel\Reverb\Contracts\Connection;
|
||||
|
||||
trait InteractsWithPresenceChannels
|
||||
{
|
||||
use InteractsWithPrivateChannels;
|
||||
|
||||
/**
|
||||
* Subscribe to the given channel.
|
||||
*/
|
||||
public function subscribe(Connection $connection, ?string $auth = null, ?string $data = null): void
|
||||
{
|
||||
$this->verify($connection, $auth, $data);
|
||||
|
||||
$userData = $data ? json_decode($data, associative: true, flags: JSON_THROW_ON_ERROR) : [];
|
||||
|
||||
if ($this->userIsSubscribed($userData['user_id'] ?? null)) {
|
||||
parent::subscribe($connection, $auth, $data);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
parent::subscribe($connection, $auth, $data);
|
||||
|
||||
parent::broadcastInternally(
|
||||
[
|
||||
'event' => 'pusher_internal:member_added',
|
||||
'data' => json_encode((object) $userData),
|
||||
'channel' => $this->name(),
|
||||
],
|
||||
$connection
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsubscribe from the given channel.
|
||||
*/
|
||||
public function unsubscribe(Connection $connection): void
|
||||
{
|
||||
$subscription = $this->connections->find($connection);
|
||||
|
||||
parent::unsubscribe($connection);
|
||||
|
||||
if (
|
||||
! $subscription ||
|
||||
! $subscription->data('user_id') ||
|
||||
$this->userIsSubscribed($subscription->data('user_id'))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
parent::broadcast(
|
||||
[
|
||||
'event' => 'pusher_internal:member_removed',
|
||||
'data' => json_encode(['user_id' => $subscription->data('user_id')]),
|
||||
'channel' => $this->name(),
|
||||
],
|
||||
$connection
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the data associated with the channel.
|
||||
*/
|
||||
public function data(): array
|
||||
{
|
||||
$connections = collect($this->connections->all())
|
||||
->map(fn ($connection) => $connection->data())
|
||||
->unique('user_id');
|
||||
|
||||
if ($connections->contains(fn ($connection) => ! isset($connection['user_id']))) {
|
||||
return [
|
||||
'presence' => [
|
||||
'count' => 0,
|
||||
'ids' => [],
|
||||
'hash' => [],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'presence' => [
|
||||
'count' => $connections->count() ?? 0,
|
||||
'ids' => $connections->map(fn ($connection) => $connection['user_id'])->values()->all(),
|
||||
'hash' => $connections->keyBy('user_id')->map->user_info->toArray(),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the given user is subscribed to the channel.
|
||||
*/
|
||||
protected function userIsSubscribed(?string $userId): bool
|
||||
{
|
||||
if (! $userId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return collect($this->connections->all())->map(fn ($connection) => (string) $connection->data('user_id'))->contains($userId);
|
||||
}
|
||||
}
|
||||
Vendored
+45
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Reverb\Protocols\Pusher\Channels\Concerns;
|
||||
|
||||
use Illuminate\Support\Str;
|
||||
use Laravel\Reverb\Contracts\Connection;
|
||||
use Laravel\Reverb\Protocols\Pusher\Exceptions\ConnectionUnauthorized;
|
||||
|
||||
trait InteractsWithPrivateChannels
|
||||
{
|
||||
/**
|
||||
* Subscribe to the given channel.
|
||||
*/
|
||||
public function subscribe(Connection $connection, ?string $auth = null, ?string $data = null): void
|
||||
{
|
||||
$this->verify($connection, $auth, $data);
|
||||
|
||||
parent::subscribe($connection, $auth, $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the given authentication token is valid.
|
||||
*/
|
||||
protected function verify(Connection $connection, ?string $auth = null, ?string $data = null): bool
|
||||
{
|
||||
$signature = "{$connection->id()}:{$this->name()}";
|
||||
|
||||
if ($data) {
|
||||
$signature .= ":{$data}";
|
||||
}
|
||||
|
||||
if (! hash_equals(
|
||||
hash_hmac(
|
||||
'sha256',
|
||||
$signature,
|
||||
$connection->app()->secret(),
|
||||
),
|
||||
Str::after($auth, ':')
|
||||
)) {
|
||||
throw new ConnectionUnauthorized;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Reverb\Protocols\Pusher\Channels;
|
||||
|
||||
use Laravel\Reverb\Protocols\Pusher\Channels\Concerns\InteractsWithPresenceChannels;
|
||||
|
||||
class PresenceCacheChannel extends CacheChannel
|
||||
{
|
||||
use InteractsWithPresenceChannels;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Reverb\Protocols\Pusher\Channels;
|
||||
|
||||
use Laravel\Reverb\Protocols\Pusher\Channels\Concerns\InteractsWithPresenceChannels;
|
||||
|
||||
class PresenceChannel extends PrivateChannel
|
||||
{
|
||||
use InteractsWithPresenceChannels;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Reverb\Protocols\Pusher\Channels;
|
||||
|
||||
use Laravel\Reverb\Protocols\Pusher\Channels\Concerns\InteractsWithPrivateChannels;
|
||||
|
||||
class PrivateCacheChannel extends CacheChannel
|
||||
{
|
||||
use InteractsWithPrivateChannels;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Reverb\Protocols\Pusher\Channels;
|
||||
|
||||
use Laravel\Reverb\Protocols\Pusher\Channels\Concerns\InteractsWithPrivateChannels;
|
||||
|
||||
class PrivateChannel extends Channel
|
||||
{
|
||||
use InteractsWithPrivateChannels;
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Reverb\Protocols\Pusher;
|
||||
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Support\Str;
|
||||
use Laravel\Reverb\Contracts\Connection;
|
||||
use Laravel\Reverb\Protocols\Pusher\Contracts\ChannelManager;
|
||||
|
||||
class ClientEvent
|
||||
{
|
||||
/**
|
||||
* Handle a Pusher client event.
|
||||
*/
|
||||
public static function handle(Connection $connection, array $event): void
|
||||
{
|
||||
Validator::make($event, [
|
||||
'event' => ['required', 'string'],
|
||||
'channel' => ['required', 'string'],
|
||||
'data' => ['nullable', 'array'],
|
||||
])->validate();
|
||||
|
||||
if (! Str::startsWith($event['event'], 'client-')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (! isset($event['channel'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$acceptClientEventsFrom = $connection->app()->acceptClientEventsFrom();
|
||||
|
||||
if (! in_array($acceptClientEventsFrom, ['all', 'members'])) {
|
||||
// Client events are disabled, so we should reject the event...
|
||||
$connection->send(json_encode([
|
||||
'event' => 'pusher:error',
|
||||
'data' => json_encode([
|
||||
'code' => 4301,
|
||||
'message' => 'The app does not have client messaging enabled.',
|
||||
]),
|
||||
]));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$rebroadcastEvent = $event;
|
||||
|
||||
if ($acceptClientEventsFrom == 'members') {
|
||||
$channel = app(ChannelManager::class)->find($event['channel']);
|
||||
|
||||
$channelConnection = $channel?->find($connection);
|
||||
|
||||
if (! $channelConnection) {
|
||||
$connection->send(json_encode([
|
||||
'event' => 'pusher:error',
|
||||
'data' => json_encode([
|
||||
'code' => 4009,
|
||||
'message' => 'The client is not a member of the specified channel.',
|
||||
]),
|
||||
]));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Regenerate event payload, ensuring we only include the expected fields and the authenticated user_id...
|
||||
$rebroadcastEvent = [
|
||||
'event' => $event['event'],
|
||||
'channel' => $event['channel'],
|
||||
'data' => $event['data'] ?? null,
|
||||
];
|
||||
|
||||
if ($userId = $channelConnection->data('user_id')) {
|
||||
// Because public channels allow unauthenticated users, we may not have a user ID...
|
||||
$rebroadcastEvent['user_id'] = $userId;
|
||||
}
|
||||
}
|
||||
|
||||
self::whisper(
|
||||
$connection,
|
||||
$rebroadcastEvent
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whisper a message to all connections on the channel associated with the event.
|
||||
*/
|
||||
public static function whisper(Connection $connection, array $payload): void
|
||||
{
|
||||
EventDispatcher::dispatch(
|
||||
$connection->app(),
|
||||
$payload,
|
||||
$connection
|
||||
);
|
||||
}
|
||||
}
|
||||
+93
@@ -0,0 +1,93 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Reverb\Protocols\Pusher\Concerns;
|
||||
|
||||
use Laravel\Reverb\Application;
|
||||
use Laravel\Reverb\Protocols\Pusher\Channels\CacheChannel;
|
||||
use Laravel\Reverb\Protocols\Pusher\Channels\Channel;
|
||||
use Laravel\Reverb\Protocols\Pusher\Channels\Concerns\InteractsWithPresenceChannels;
|
||||
use Laravel\Reverb\Protocols\Pusher\Contracts\ChannelManager;
|
||||
|
||||
trait InteractsWithChannelInformation
|
||||
{
|
||||
/**
|
||||
* Get meta / status information for the given channels.
|
||||
*/
|
||||
protected function infoForChannels(Application $application, array $channels, string $info): array
|
||||
{
|
||||
return collect($channels)->mapWithKeys(function ($channel) use ($application, $info) {
|
||||
$name = $channel instanceof Channel ? $channel->name() : $channel;
|
||||
|
||||
return [$name => $this->info($application, $name, $info)];
|
||||
})->all();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get meta / status information for the given channel.
|
||||
*
|
||||
* @return array<string, array<string, int>>
|
||||
*/
|
||||
protected function info(Application $application, string $channel, string $info): array
|
||||
{
|
||||
$info = explode(',', $info);
|
||||
|
||||
$channel = app(ChannelManager::class)->for($application)->find($channel);
|
||||
|
||||
return array_filter(
|
||||
$channel ? $this->occupiedInfo($channel, $info) : $this->unoccupiedInfo($info),
|
||||
fn ($item) => $item !== null
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get channel information for the given occupied channel.
|
||||
*/
|
||||
private function occupiedInfo(Channel $channel, array $info): array
|
||||
{
|
||||
$count = count($channel->connections());
|
||||
|
||||
return [
|
||||
'occupied' => in_array('occupied', $info) ? $count > 0 : null,
|
||||
'user_count' => in_array('user_count', $info) && $this->isPresenceChannel($channel) ? $this->userCount($channel) : null,
|
||||
'subscription_count' => in_array('subscription_count', $info) && ! $this->isPresenceChannel($channel) ? $count : null,
|
||||
'cache' => in_array('cache', $info) && $this->isCacheChannel($channel) ? $channel->cachedPayload() : null,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get channel information for the given unoccupied channel.
|
||||
*/
|
||||
private function unoccupiedInfo(array $info): array
|
||||
{
|
||||
return [
|
||||
'occupied' => in_array('occupied', $info) ? false : null,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the given channel is a presence channel.
|
||||
*/
|
||||
protected function isPresenceChannel(Channel $channel): bool
|
||||
{
|
||||
return in_array(InteractsWithPresenceChannels::class, class_uses($channel));
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the given channel is a cache channel.
|
||||
*/
|
||||
protected function isCacheChannel(Channel $channel): bool
|
||||
{
|
||||
return $channel instanceof CacheChannel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the number of unique users subscribed to the presence channel.
|
||||
*/
|
||||
protected function userCount(Channel $channel): int
|
||||
{
|
||||
return collect($channel->connections())
|
||||
->map(fn ($connection) => $connection->data())
|
||||
->unique('user_id')
|
||||
->count();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Reverb\Protocols\Pusher\Concerns;
|
||||
|
||||
use Laravel\Reverb\Protocols\Pusher\Contracts\ChannelConnectionManager;
|
||||
|
||||
trait SerializesChannels
|
||||
{
|
||||
/**
|
||||
* Prepare the channel instance values for serialization.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function __serialize(): array
|
||||
{
|
||||
return [
|
||||
'name' => $this->name,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore the channel after serialization.
|
||||
*/
|
||||
public function __unserialize(array $values): void
|
||||
{
|
||||
$this->name = $values['name'];
|
||||
$this->connections = app(ChannelConnectionManager::class)->for($this->name);
|
||||
}
|
||||
}
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Reverb\Protocols\Pusher\Contracts;
|
||||
|
||||
use Laravel\Reverb\Contracts\Connection;
|
||||
use Laravel\Reverb\Protocols\Pusher\Channels\ChannelConnection;
|
||||
|
||||
interface ChannelConnectionManager
|
||||
{
|
||||
/**
|
||||
* Get a channel connection manager for the given channel name.
|
||||
*/
|
||||
public function for(string $name): ChannelConnectionManager;
|
||||
|
||||
/**
|
||||
* Add a connection.
|
||||
*/
|
||||
public function add(Connection $connection, array $data): void;
|
||||
|
||||
/**
|
||||
* Remove a connection.
|
||||
*/
|
||||
public function remove(Connection $connection): void;
|
||||
|
||||
/**
|
||||
* Find a connection.
|
||||
*/
|
||||
public function find(Connection $connection): ?ChannelConnection;
|
||||
|
||||
/**
|
||||
* Find a connection by its ID.
|
||||
*/
|
||||
public function findById(string $id): ?ChannelConnection;
|
||||
|
||||
/**
|
||||
* Get all of the connections.
|
||||
*
|
||||
* @return array<string, \Laravel\Reverb\Protocols\Pusher\Channels\ChannelConnection>
|
||||
*/
|
||||
public function all(): array;
|
||||
|
||||
/**
|
||||
* Determine whether any connections remain on the channel.
|
||||
*/
|
||||
public function isEmpty(): bool;
|
||||
|
||||
/**
|
||||
* Flush the channel connection manager.
|
||||
*/
|
||||
public function flush(): void;
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Reverb\Protocols\Pusher\Contracts;
|
||||
|
||||
use Laravel\Reverb\Application;
|
||||
use Laravel\Reverb\Contracts\Connection;
|
||||
use Laravel\Reverb\Protocols\Pusher\Channels\Channel;
|
||||
|
||||
interface ChannelManager
|
||||
{
|
||||
/**
|
||||
* Get the application instance.
|
||||
*/
|
||||
public function app(): ?Application;
|
||||
|
||||
/**
|
||||
* The application the channel manager should be scoped to.
|
||||
*/
|
||||
public function for(Application $application): ChannelManager;
|
||||
|
||||
/**
|
||||
* Get all the channels.
|
||||
*
|
||||
* @return array<string, \Laravel\Reverb\Protocols\Pusher\Channels\Channel>
|
||||
*/
|
||||
public function all(): array;
|
||||
|
||||
/**
|
||||
* Determine whether the given channel exists.
|
||||
*/
|
||||
public function exists(string $channel): bool;
|
||||
|
||||
/**
|
||||
* Find the given channel.
|
||||
*/
|
||||
public function find(string $channel): ?Channel;
|
||||
|
||||
/**
|
||||
* Find the given channel or create it if it doesn't exist.
|
||||
*/
|
||||
public function findOrCreate(string $channel): Channel;
|
||||
|
||||
/**
|
||||
* Get all the connections for the given channels.
|
||||
*
|
||||
* @return array<string, \Laravel\Reverb\Protocols\Pusher\Channels\ChannelConnection>
|
||||
*/
|
||||
public function connections(?string $channel = null): array;
|
||||
|
||||
/**
|
||||
* Unsubscribe from all channels.
|
||||
*/
|
||||
public function unsubscribeFromAll(Connection $connection): void;
|
||||
|
||||
/**
|
||||
* Remove the given channel.
|
||||
*/
|
||||
public function remove(Channel $channel): void;
|
||||
|
||||
/**
|
||||
* Flush the channel manager repository.
|
||||
*/
|
||||
public function flush(): void;
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Reverb\Protocols\Pusher;
|
||||
|
||||
use Illuminate\Support\Arr;
|
||||
use Laravel\Reverb\Application;
|
||||
use Laravel\Reverb\Contracts\Connection;
|
||||
use Laravel\Reverb\Protocols\Pusher\Contracts\ChannelManager;
|
||||
use Laravel\Reverb\ServerProviderManager;
|
||||
use Laravel\Reverb\Servers\Reverb\Contracts\PubSubProvider;
|
||||
|
||||
class EventDispatcher
|
||||
{
|
||||
/**
|
||||
* Dispatch a message to a channel.
|
||||
*/
|
||||
public static function dispatch(Application $app, array $payload, ?Connection $connection = null): void
|
||||
{
|
||||
$server = app(ServerProviderManager::class);
|
||||
|
||||
if ($server->shouldNotPublishEvents()) {
|
||||
static::dispatchSynchronously($app, $payload, $connection);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$data = [
|
||||
'type' => 'message',
|
||||
'application' => serialize($app),
|
||||
'payload' => $payload,
|
||||
];
|
||||
|
||||
if ($connection?->id() !== null) {
|
||||
$data['socket_id'] = $connection?->id();
|
||||
}
|
||||
|
||||
app(PubSubProvider::class)->publish($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify all connections subscribed to the given channel.
|
||||
*/
|
||||
public static function dispatchSynchronously(Application $app, array $payload, ?Connection $connection = null): void
|
||||
{
|
||||
$channels = Arr::wrap($payload['channels'] ?? $payload['channel'] ?? []);
|
||||
|
||||
foreach ($channels as $channel) {
|
||||
unset($payload['channels']);
|
||||
|
||||
if (! $channel = app(ChannelManager::class)->for($app)->find($channel)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$payload['channel'] = $channel->name();
|
||||
|
||||
$channel->broadcast($payload, $connection);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Reverb\Protocols\Pusher;
|
||||
|
||||
use Exception;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Support\Str;
|
||||
use Laravel\Reverb\Contracts\Connection;
|
||||
use Laravel\Reverb\Protocols\Pusher\Channels\CacheChannel;
|
||||
use Laravel\Reverb\Protocols\Pusher\Channels\Channel;
|
||||
use Laravel\Reverb\Protocols\Pusher\Contracts\ChannelManager;
|
||||
|
||||
class EventHandler
|
||||
{
|
||||
/**
|
||||
* Create a new Pusher event instance.
|
||||
*/
|
||||
public function __construct(protected ChannelManager $channels)
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle an incoming Pusher event.
|
||||
*/
|
||||
public function handle(Connection $connection, string $event, array $payload = []): void
|
||||
{
|
||||
$event = Str::after($event, 'pusher:');
|
||||
|
||||
match ($event) {
|
||||
'connection_established' => $this->acknowledge($connection),
|
||||
'subscribe' => $this->subscribe(
|
||||
$connection,
|
||||
$payload['channel'],
|
||||
$payload['auth'] ?? null,
|
||||
$payload['channel_data'] ?? null
|
||||
),
|
||||
'unsubscribe' => $this->unsubscribe($connection, $payload['channel']),
|
||||
'ping' => $this->pong($connection),
|
||||
'pong' => $connection->touch(),
|
||||
default => throw new Exception('Unknown Pusher event: '.$event),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Acknowledge the connection.
|
||||
*/
|
||||
public function acknowledge(Connection $connection): void
|
||||
{
|
||||
$this->send($connection, 'connection_established', [
|
||||
'socket_id' => $connection->id(),
|
||||
'activity_timeout' => $connection->app()->activityTimeout(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe to the given channel.
|
||||
*/
|
||||
public function subscribe(Connection $connection, string $channel, ?string $auth = null, ?string $data = null): void
|
||||
{
|
||||
Validator::make([
|
||||
'channel' => $channel,
|
||||
'auth' => $auth,
|
||||
'channel_data' => $data,
|
||||
], [
|
||||
'channel' => ['nullable', 'string'],
|
||||
'auth' => ['nullable', 'string'],
|
||||
'channel_data' => ['nullable', 'json'],
|
||||
])->validate();
|
||||
|
||||
$channel = $this->channels
|
||||
->for($connection->app())
|
||||
->findOrCreate($channel);
|
||||
|
||||
$channel->subscribe($connection, $auth, $data);
|
||||
|
||||
$this->afterSubscribe($channel, $connection);
|
||||
}
|
||||
|
||||
/**
|
||||
* Carry out any actions that should be performed after a subscription.
|
||||
*/
|
||||
protected function afterSubscribe(Channel $channel, Connection $connection): void
|
||||
{
|
||||
$this->sendInternally($connection, 'subscription_succeeded', $channel->data(), $channel->name());
|
||||
|
||||
match (true) {
|
||||
$channel instanceof CacheChannel => $this->sendCachedPayload($channel, $connection),
|
||||
default => null,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsubscribe from the given channel.
|
||||
*/
|
||||
public function unsubscribe(Connection $connection, string $channel): void
|
||||
{
|
||||
$channel = $this->channels
|
||||
->for($connection->app())
|
||||
->find($channel)
|
||||
?->unsubscribe($connection);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send the cached payload for the given channel.
|
||||
*/
|
||||
protected function sendCachedPayload(CacheChannel $channel, Connection $connection): void
|
||||
{
|
||||
if ($channel->hasCachedPayload()) {
|
||||
$connection->send(
|
||||
json_encode($channel->cachedPayload())
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->send($connection, 'cache_miss', channel: $channel->name());
|
||||
}
|
||||
|
||||
/**
|
||||
* Respond to a ping on the given connection.
|
||||
*/
|
||||
public function pong(Connection $connection): void
|
||||
{
|
||||
static::send($connection, 'pong');
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a ping to the given connection.
|
||||
*/
|
||||
public function ping(Connection $connection): void
|
||||
{
|
||||
$connection->usesControlFrames()
|
||||
? $connection->control()
|
||||
: static::send($connection, 'ping');
|
||||
|
||||
$connection->ping();
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a response to the given connection.
|
||||
*/
|
||||
public function send(Connection $connection, string $event, array $data = [], ?string $channel = null): void
|
||||
{
|
||||
$connection->send(
|
||||
static::formatPayload($event, $data, $channel)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send an internal response to the given connection.
|
||||
*/
|
||||
public function sendInternally(Connection $connection, string $event, array $data = [], ?string $channel = null): void
|
||||
{
|
||||
$connection->send(
|
||||
static::formatInternalPayload($event, $data, $channel)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format the payload for the given event.
|
||||
*/
|
||||
public function formatPayload(string $event, array $data = [], ?string $channel = null, string $prefix = 'pusher:'): string|false
|
||||
{
|
||||
return json_encode(
|
||||
array_filter([
|
||||
'event' => $prefix.$event,
|
||||
'data' => empty($data) ? null : json_encode($data),
|
||||
'channel' => $channel,
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format the internal payload for the given event.
|
||||
*/
|
||||
public function formatInternalPayload(string $event, array $data = [], $channel = null): string|false
|
||||
{
|
||||
return json_encode(
|
||||
array_filter([
|
||||
'event' => 'pusher_internal:'.$event,
|
||||
'data' => json_encode((object) $data),
|
||||
'channel' => $channel,
|
||||
])
|
||||
);
|
||||
}
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Reverb\Protocols\Pusher\Exceptions;
|
||||
|
||||
class ConnectionLimitExceeded extends PusherException
|
||||
{
|
||||
/**
|
||||
* The error code associated with the exception.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $code = 4004;
|
||||
|
||||
/**
|
||||
* The error message associated with the exception.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $message = 'Application is over connection quota';
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Reverb\Protocols\Pusher\Exceptions;
|
||||
|
||||
class ConnectionUnauthorized extends PusherException
|
||||
{
|
||||
/**
|
||||
* The error code associated with the exception.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $code = 4009;
|
||||
|
||||
/**
|
||||
* The error message associated with the exception.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $message = 'Connection is unauthorized';
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Reverb\Protocols\Pusher\Exceptions;
|
||||
|
||||
class InvalidOrigin extends PusherException
|
||||
{
|
||||
/**
|
||||
* The error code associated with the exception.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $code = 4009;
|
||||
|
||||
/**
|
||||
* The error message associated with the exception.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $message = 'Origin not allowed';
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Reverb\Protocols\Pusher\Exceptions;
|
||||
|
||||
use Exception;
|
||||
|
||||
abstract class PusherException extends Exception
|
||||
{
|
||||
/**
|
||||
* The error code associated with the exception.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $code;
|
||||
|
||||
/**
|
||||
* The error message associated with the exception.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $message;
|
||||
|
||||
/**
|
||||
* Get the Pusher formatted error payload.
|
||||
*/
|
||||
public function payload(): array
|
||||
{
|
||||
return [
|
||||
'event' => 'pusher:error',
|
||||
'data' => json_encode([
|
||||
'code' => $this->code,
|
||||
'message' => $this->message,
|
||||
]),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the encoded Pusher formatted error payload.
|
||||
*/
|
||||
public function message()
|
||||
{
|
||||
return json_encode(
|
||||
$this->payload()
|
||||
);
|
||||
}
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Reverb\Protocols\Pusher\Http\Controllers;
|
||||
|
||||
use Laravel\Reverb\Protocols\Pusher\MetricsHandler;
|
||||
use Laravel\Reverb\Servers\Reverb\Http\Connection;
|
||||
use Laravel\Reverb\Servers\Reverb\Http\Response;
|
||||
use Psr\Http\Message\RequestInterface;
|
||||
use React\Promise\PromiseInterface;
|
||||
|
||||
class ChannelController extends Controller
|
||||
{
|
||||
/**
|
||||
* Handle the request.
|
||||
*/
|
||||
public function __invoke(RequestInterface $request, Connection $connection, string $appId, string $channel): PromiseInterface
|
||||
{
|
||||
$this->verify($request, $connection, $appId);
|
||||
|
||||
return app(MetricsHandler::class)->gather($this->application, 'channel', [
|
||||
'channel' => $channel,
|
||||
'info' => isset($this->query['info']) ? $this->query['info'].',occupied' : 'occupied',
|
||||
])->then(fn ($channel) => new Response((object) $channel));
|
||||
}
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Reverb\Protocols\Pusher\Http\Controllers;
|
||||
|
||||
use Laravel\Reverb\Protocols\Pusher\Concerns\InteractsWithChannelInformation;
|
||||
use Laravel\Reverb\Protocols\Pusher\MetricsHandler;
|
||||
use Laravel\Reverb\Servers\Reverb\Http\Connection;
|
||||
use Laravel\Reverb\Servers\Reverb\Http\Response;
|
||||
use Psr\Http\Message\RequestInterface;
|
||||
use React\Promise\PromiseInterface;
|
||||
|
||||
class ChannelUsersController extends Controller
|
||||
{
|
||||
use InteractsWithChannelInformation;
|
||||
|
||||
/**
|
||||
* Handle the request.
|
||||
*/
|
||||
public function __invoke(RequestInterface $request, Connection $connection, string $channel, string $appId): Response|PromiseInterface
|
||||
{
|
||||
$this->verify($request, $connection, $appId);
|
||||
|
||||
$channel = $this->channels->find($channel);
|
||||
|
||||
if (! $channel) {
|
||||
return new Response((object) [], 404);
|
||||
}
|
||||
|
||||
if (! $this->isPresenceChannel($channel)) {
|
||||
return new Response((object) [], 400);
|
||||
}
|
||||
|
||||
return app(MetricsHandler::class)
|
||||
->gather($this->application, 'channel_users', ['channel' => $channel->name()])
|
||||
->then(fn ($connections) => new Response(['users' => $connections]));
|
||||
}
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Reverb\Protocols\Pusher\Http\Controllers;
|
||||
|
||||
use Laravel\Reverb\Protocols\Pusher\MetricsHandler;
|
||||
use Laravel\Reverb\Servers\Reverb\Http\Connection;
|
||||
use Laravel\Reverb\Servers\Reverb\Http\Response;
|
||||
use Psr\Http\Message\RequestInterface;
|
||||
use React\Promise\PromiseInterface;
|
||||
|
||||
class ChannelsController extends Controller
|
||||
{
|
||||
/**
|
||||
* Handle the request.
|
||||
*/
|
||||
public function __invoke(RequestInterface $request, Connection $connection, string $appId): PromiseInterface
|
||||
{
|
||||
$this->verify($request, $connection, $appId);
|
||||
|
||||
return app(MetricsHandler::class)->gather($this->application, 'channels', [
|
||||
'filter' => $this->query['filter_by_prefix'] ?? null,
|
||||
'info' => $this->query['info'] ?? null,
|
||||
])->then(fn ($channels) => new Response(['channels' => array_map(fn ($item) => (object) $item, $channels)]));
|
||||
}
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Reverb\Protocols\Pusher\Http\Controllers;
|
||||
|
||||
use Laravel\Reverb\Protocols\Pusher\MetricsHandler;
|
||||
use Laravel\Reverb\Servers\Reverb\Http\Connection;
|
||||
use Laravel\Reverb\Servers\Reverb\Http\Response;
|
||||
use Psr\Http\Message\RequestInterface;
|
||||
use React\Promise\PromiseInterface;
|
||||
|
||||
class ConnectionsController extends Controller
|
||||
{
|
||||
/**
|
||||
* Handle the request.
|
||||
*/
|
||||
public function __invoke(RequestInterface $request, Connection $connection, string $appId): PromiseInterface
|
||||
{
|
||||
$this->verify($request, $connection, $appId);
|
||||
|
||||
return app(MetricsHandler::class)->gather($this->application, 'connections')
|
||||
->then(fn ($connections) => new Response(['connections' => count($connections)]));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Reverb\Protocols\Pusher\Http\Controllers;
|
||||
|
||||
use Illuminate\Support\Arr;
|
||||
use Laravel\Reverb\Application;
|
||||
use Laravel\Reverb\Contracts\ApplicationProvider;
|
||||
use Laravel\Reverb\Exceptions\InvalidApplication;
|
||||
use Laravel\Reverb\Protocols\Pusher\Contracts\ChannelManager;
|
||||
use Laravel\Reverb\Servers\Reverb\Concerns\ClosesConnections;
|
||||
use Laravel\Reverb\Servers\Reverb\Http\Connection;
|
||||
use Psr\Http\Message\RequestInterface;
|
||||
use Symfony\Component\HttpKernel\Exception\HttpException;
|
||||
|
||||
abstract class Controller
|
||||
{
|
||||
use ClosesConnections;
|
||||
|
||||
/**
|
||||
* Current application instance.
|
||||
*/
|
||||
protected ?Application $application = null;
|
||||
|
||||
/**
|
||||
* Active channels for the application.
|
||||
*/
|
||||
protected ?ChannelManager $channels = null;
|
||||
|
||||
/**
|
||||
* The incoming request's body.
|
||||
*/
|
||||
protected ?string $body;
|
||||
|
||||
/**
|
||||
* The incoming request's query parameters.
|
||||
*/
|
||||
protected array $query = [];
|
||||
|
||||
/**
|
||||
* Verify that the incoming request is valid.
|
||||
*/
|
||||
public function verify(RequestInterface $request, Connection $connection, $appId): void
|
||||
{
|
||||
parse_str($request->getUri()->getQuery(), $query);
|
||||
|
||||
$this->body = $request->getBody()->getContents();
|
||||
$this->query = $query;
|
||||
|
||||
$this->setApplication($appId);
|
||||
$this->setChannels();
|
||||
$this->verifySignature($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the Reverb application instance for the incoming request's application ID.
|
||||
*
|
||||
* @throws \Symfony\Component\HttpKernel\Exception\HttpException
|
||||
*/
|
||||
protected function setApplication(?string $appId): Application
|
||||
{
|
||||
if (! $appId) {
|
||||
throw new HttpException(400, 'Application ID not provided.');
|
||||
}
|
||||
|
||||
try {
|
||||
return $this->application = app(ApplicationProvider::class)->findById($appId);
|
||||
} catch (InvalidApplication $e) {
|
||||
throw new HttpException(404, 'No matching application for ID ['.$appId.'].');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the Reverb channel manager instance for the application.
|
||||
*/
|
||||
protected function setChannels(): void
|
||||
{
|
||||
$this->channels = app(ChannelManager::class)->for($this->application);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify the Pusher authentication signature.
|
||||
*
|
||||
* @throws \Symfony\Component\HttpKernel\Exception\HttpException
|
||||
*/
|
||||
protected function verifySignature(RequestInterface $request): void
|
||||
{
|
||||
$params = Arr::except($this->query, [
|
||||
'auth_signature', 'body_md5', 'appId', 'appKey', 'channelName',
|
||||
]);
|
||||
|
||||
if ($this->body !== '') {
|
||||
$params['body_md5'] = md5($this->body);
|
||||
}
|
||||
|
||||
ksort($params);
|
||||
|
||||
$signature = implode("\n", [
|
||||
$request->getMethod(),
|
||||
$request->getUri()->getPath(),
|
||||
$this->formatQueryParametersForVerification($params),
|
||||
]);
|
||||
|
||||
$signature = hash_hmac('sha256', $signature, $this->application->secret());
|
||||
$authSignature = $this->query['auth_signature'] ?? '';
|
||||
|
||||
if ($signature !== $authSignature) {
|
||||
throw new HttpException(401, 'Authentication signature invalid.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Format the given parameters into the correct format for signature verification.
|
||||
*/
|
||||
protected static function formatQueryParametersForVerification(array $params): string
|
||||
{
|
||||
if (! is_array($params)) {
|
||||
return $params;
|
||||
}
|
||||
|
||||
return collect($params)->map(function ($value, $key) {
|
||||
if (is_array($value)) {
|
||||
$value = implode(',', $value);
|
||||
}
|
||||
|
||||
return "{$key}={$value}";
|
||||
})->implode('&');
|
||||
}
|
||||
}
|
||||
+96
@@ -0,0 +1,96 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Reverb\Protocols\Pusher\Http\Controllers;
|
||||
|
||||
use Illuminate\Contracts\Validation\Validator;
|
||||
use Illuminate\Support\Facades\Validator as ValidatorFacade;
|
||||
use Laravel\Reverb\Protocols\Pusher\Concerns\InteractsWithChannelInformation;
|
||||
use Laravel\Reverb\Protocols\Pusher\EventDispatcher;
|
||||
use Laravel\Reverb\Protocols\Pusher\MetricsHandler;
|
||||
use Laravel\Reverb\Servers\Reverb\Http\Connection;
|
||||
use Laravel\Reverb\Servers\Reverb\Http\Response;
|
||||
use Psr\Http\Message\RequestInterface;
|
||||
use React\Promise\PromiseInterface;
|
||||
|
||||
use function React\Promise\all;
|
||||
|
||||
class EventsBatchController extends Controller
|
||||
{
|
||||
use InteractsWithChannelInformation;
|
||||
|
||||
/**
|
||||
* Handle the request.
|
||||
*/
|
||||
public function __invoke(RequestInterface $request, Connection $connection, string $appId): Response|PromiseInterface
|
||||
{
|
||||
$this->verify($request, $connection, $appId);
|
||||
|
||||
$payload = json_decode($this->body, associative: true, flags: JSON_THROW_ON_ERROR);
|
||||
|
||||
$validator = $this->validator($payload);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return new Response($validator->errors(), 422);
|
||||
}
|
||||
|
||||
$items = collect($payload['batch']);
|
||||
|
||||
$items = $items->map(function ($item) {
|
||||
EventDispatcher::dispatch(
|
||||
$this->application,
|
||||
[
|
||||
'event' => $item['name'],
|
||||
'channel' => $item['channel'],
|
||||
'data' => $item['data'],
|
||||
],
|
||||
isset($item['socket_id']) ? ($this->channels->connections()[$item['socket_id']] ?? null) : null
|
||||
);
|
||||
|
||||
return isset($item['info']) ? app(MetricsHandler::class)->gather(
|
||||
$this->application,
|
||||
'channel',
|
||||
['channel' => $item['channel'], 'info' => $item['info']]
|
||||
) : [];
|
||||
});
|
||||
|
||||
if ($items->contains(fn ($item) => ! empty($item))) {
|
||||
return all($items)->then(function ($items) {
|
||||
return new Response(['batch' => array_map(fn ($item) => (object) $item, $items)]);
|
||||
});
|
||||
}
|
||||
|
||||
return new Response(['batch' => (object) []]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the info for the given channels.
|
||||
*
|
||||
* @return array<string, array<string, int>>
|
||||
*/
|
||||
protected function getInfo(string $channel, string $info): array
|
||||
{
|
||||
$info = explode(',', $info);
|
||||
$count = count($this->channels->find($channel)->connections());
|
||||
$info = [
|
||||
'user_count' => in_array('user_count', $info) ? $count : null,
|
||||
'subscription_count' => in_array('subscription_count', $info) ? $count : null,
|
||||
];
|
||||
|
||||
return array_filter($info, fn ($item) => $item !== null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a validator for the incoming request payload.
|
||||
*/
|
||||
protected function validator(array $payload): Validator
|
||||
{
|
||||
return ValidatorFacade::make($payload, [
|
||||
'batch' => ['required', 'array'],
|
||||
'batch.*.name' => ['required', 'string'],
|
||||
'batch.*.data' => ['required', 'string'],
|
||||
'batch.*.channel' => ['required_without:channels', 'string'],
|
||||
'batch.*.socket_id' => ['string'],
|
||||
'batch.*.info' => ['string'],
|
||||
]);
|
||||
}
|
||||
}
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Reverb\Protocols\Pusher\Http\Controllers;
|
||||
|
||||
use Illuminate\Contracts\Validation\Validator;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Facades\Validator as ValidatorFacade;
|
||||
use Laravel\Reverb\Protocols\Pusher\Concerns\InteractsWithChannelInformation;
|
||||
use Laravel\Reverb\Protocols\Pusher\EventDispatcher;
|
||||
use Laravel\Reverb\Protocols\Pusher\MetricsHandler;
|
||||
use Laravel\Reverb\Servers\Reverb\Http\Connection;
|
||||
use Laravel\Reverb\Servers\Reverb\Http\Response;
|
||||
use Psr\Http\Message\RequestInterface;
|
||||
use React\Promise\PromiseInterface;
|
||||
|
||||
class EventsController extends Controller
|
||||
{
|
||||
use InteractsWithChannelInformation;
|
||||
|
||||
/**
|
||||
* Handle the request.
|
||||
*/
|
||||
public function __invoke(RequestInterface $request, Connection $connection, string $appId): Response|PromiseInterface
|
||||
{
|
||||
$this->verify($request, $connection, $appId);
|
||||
|
||||
$payload = json_decode($this->body, associative: true, flags: JSON_THROW_ON_ERROR);
|
||||
|
||||
$validator = $this->validator($payload);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return new Response($validator->errors(), 422);
|
||||
}
|
||||
|
||||
$channels = Arr::wrap($payload['channels'] ?? $payload['channel'] ?? []);
|
||||
if ($except = $payload['socket_id'] ?? null) {
|
||||
$except = $this->channels->connections()[$except] ?? null;
|
||||
}
|
||||
|
||||
EventDispatcher::dispatch(
|
||||
$this->application,
|
||||
[
|
||||
'event' => $payload['name'],
|
||||
'channels' => $channels,
|
||||
'data' => $payload['data'],
|
||||
],
|
||||
$except ? $except->connection() : null
|
||||
);
|
||||
|
||||
if (isset($payload['info'])) {
|
||||
return app(MetricsHandler::class)
|
||||
->gather($this->application, 'channels', ['info' => $payload['info'], 'channels' => $channels])
|
||||
->then(fn ($channels) => new Response(['channels' => array_map(fn ($channel) => (object) $channel, $channels)]));
|
||||
}
|
||||
|
||||
return new Response((object) []);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a validator for the incoming request payload.
|
||||
*/
|
||||
protected function validator(array $payload): Validator
|
||||
{
|
||||
return ValidatorFacade::make($payload, [
|
||||
'name' => ['required', 'string'],
|
||||
'data' => ['required', 'string'],
|
||||
'channels' => ['required_without:channel', 'array'],
|
||||
'channel' => ['required_without:channels', 'string'],
|
||||
'socket_id' => ['string'],
|
||||
'info' => ['string'],
|
||||
]);
|
||||
}
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Reverb\Protocols\Pusher\Http\Controllers;
|
||||
|
||||
use Laravel\Reverb\Servers\Reverb\Http\Connection;
|
||||
use Laravel\Reverb\Servers\Reverb\Http\Response;
|
||||
use Psr\Http\Message\RequestInterface;
|
||||
|
||||
class HealthCheckController extends Controller
|
||||
{
|
||||
/**
|
||||
* Handle the request.
|
||||
*/
|
||||
public function __invoke(RequestInterface $request, Connection $connection): Response
|
||||
{
|
||||
return new Response((object) ['health' => 'OK']);
|
||||
}
|
||||
}
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Reverb\Protocols\Pusher\Http\Controllers;
|
||||
|
||||
use Laravel\Reverb\Connection as ReverbConnection;
|
||||
use Laravel\Reverb\Contracts\ApplicationProvider;
|
||||
use Laravel\Reverb\Exceptions\InvalidApplication;
|
||||
use Laravel\Reverb\Protocols\Pusher\Server as PusherServer;
|
||||
use Laravel\Reverb\Servers\Reverb\Connection;
|
||||
use Psr\Http\Message\RequestInterface;
|
||||
use Ratchet\RFC6455\Messaging\FrameInterface;
|
||||
|
||||
class PusherController
|
||||
{
|
||||
/**
|
||||
* Create a new controller instance.
|
||||
*/
|
||||
public function __construct(protected PusherServer $server, protected ApplicationProvider $applications)
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoke the Reverb WebSocket server.
|
||||
*/
|
||||
public function __invoke(RequestInterface $request, Connection $connection, string $appKey): void
|
||||
{
|
||||
if (! $reverbConnection = $this->connection($request, $connection, $appKey)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$connection->withMaxMessageSize($reverbConnection->app()->maxMessageSize());
|
||||
|
||||
$connection->onMessage(
|
||||
fn ($message) => $this->server->message($reverbConnection, (string) $message)
|
||||
);
|
||||
|
||||
$connection->onControl(
|
||||
fn (FrameInterface $message) => $this->server->control($reverbConnection, $message)
|
||||
);
|
||||
|
||||
$connection->onClose(
|
||||
fn () => $this->server->close($reverbConnection)
|
||||
);
|
||||
|
||||
$connection->openBuffer();
|
||||
|
||||
$this->server->open($reverbConnection);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Reverb connection instance for the request.
|
||||
*/
|
||||
protected function connection(RequestInterface $request, Connection $connection, string $key): ?ReverbConnection
|
||||
{
|
||||
try {
|
||||
$application = $this->applications->findByKey($key);
|
||||
} catch (InvalidApplication $e) {
|
||||
$connection->send('{"event":"pusher:error","data":"{\"code\":4001,\"message\":\"Application does not exist\"}"}');
|
||||
|
||||
return $connection->close();
|
||||
}
|
||||
|
||||
return new ReverbConnection(
|
||||
$connection,
|
||||
$application,
|
||||
$request->getHeader('Origin')[0] ?? null
|
||||
);
|
||||
}
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Reverb\Protocols\Pusher\Http\Controllers;
|
||||
|
||||
use Laravel\Reverb\ServerProviderManager;
|
||||
use Laravel\Reverb\Servers\Reverb\Contracts\PubSubProvider;
|
||||
use Laravel\Reverb\Servers\Reverb\Http\Connection;
|
||||
use Laravel\Reverb\Servers\Reverb\Http\Response;
|
||||
use Psr\Http\Message\RequestInterface;
|
||||
use React\Promise\PromiseInterface;
|
||||
|
||||
class UsersTerminateController extends Controller
|
||||
{
|
||||
/**
|
||||
* Handle the request.
|
||||
*/
|
||||
public function __invoke(RequestInterface $request, Connection $connection, string $appId, string $userId): Response|PromiseInterface
|
||||
{
|
||||
$this->verify($request, $connection, $appId);
|
||||
|
||||
if (app(ServerProviderManager::class)->subscribesToEvents()) {
|
||||
return app(PubSubProvider::class)->publish([
|
||||
'type' => 'terminate',
|
||||
'application' => serialize($this->application),
|
||||
'payload' => ['user_id' => $userId],
|
||||
])->then(fn () => new Response((object) []));
|
||||
}
|
||||
|
||||
$connections = collect($this->channels->connections());
|
||||
|
||||
$connections->each(function ($connection) use ($userId) {
|
||||
if ((string) $connection->data()['user_id'] === $userId) {
|
||||
$connection->disconnect();
|
||||
}
|
||||
});
|
||||
|
||||
return new Response((object) []);
|
||||
}
|
||||
}
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Reverb\Protocols\Pusher\Managers;
|
||||
|
||||
use Laravel\Reverb\Contracts\Connection;
|
||||
use Laravel\Reverb\Protocols\Pusher\Channels\ChannelConnection;
|
||||
use Laravel\Reverb\Protocols\Pusher\Contracts\ChannelConnectionManager;
|
||||
|
||||
class ArrayChannelConnectionManager implements ChannelConnectionManager
|
||||
{
|
||||
/**
|
||||
* The channel name.
|
||||
*/
|
||||
protected string $name;
|
||||
|
||||
/**
|
||||
* The underlying connections.
|
||||
*
|
||||
* @var array<string, \Laravel\Reverb\Protocols\Pusher\Channels\ChannelConnection>
|
||||
*/
|
||||
protected $connections = [];
|
||||
|
||||
/**
|
||||
* Get a channel connection manager for the given channel name.
|
||||
*/
|
||||
public function for(string $name): ChannelConnectionManager
|
||||
{
|
||||
$this->name = $name;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a connection.
|
||||
*/
|
||||
public function add(Connection $connection, array $data): void
|
||||
{
|
||||
$this->connections[$connection->id()] = new ChannelConnection($connection, $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a connection.
|
||||
*/
|
||||
public function remove(Connection $connection): void
|
||||
{
|
||||
unset($this->connections[$connection->id()]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a connection.
|
||||
*/
|
||||
public function find(Connection $connection): ?ChannelConnection
|
||||
{
|
||||
return $this->findById($connection->id());
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a connection by its ID.
|
||||
*/
|
||||
public function findById(string $id): ?ChannelConnection
|
||||
{
|
||||
return $this->connections[$id] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all of the connections.
|
||||
*
|
||||
* @return array<string, \Laravel\Reverb\Protocols\Pusher\Channels\ChannelConnection>
|
||||
*/
|
||||
public function all(): array
|
||||
{
|
||||
return $this->connections;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether any connections remain on the channel.
|
||||
*/
|
||||
public function isEmpty(): bool
|
||||
{
|
||||
return empty($this->connections);
|
||||
}
|
||||
|
||||
/**
|
||||
* Flush the channel connection manager.
|
||||
*/
|
||||
public function flush(): void
|
||||
{
|
||||
$this->connections = [];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Reverb\Protocols\Pusher\Managers;
|
||||
|
||||
use Illuminate\Support\Arr;
|
||||
use Laravel\Reverb\Application;
|
||||
use Laravel\Reverb\Concerns\InteractsWithApplications;
|
||||
use Laravel\Reverb\Contracts\ApplicationProvider;
|
||||
use Laravel\Reverb\Contracts\Connection;
|
||||
use Laravel\Reverb\Events\ChannelCreated;
|
||||
use Laravel\Reverb\Events\ChannelRemoved;
|
||||
use Laravel\Reverb\Protocols\Pusher\Channels\Channel;
|
||||
use Laravel\Reverb\Protocols\Pusher\Channels\ChannelBroker;
|
||||
use Laravel\Reverb\Protocols\Pusher\Contracts\ChannelManager as ChannelManagerInterface;
|
||||
|
||||
class ArrayChannelManager implements ChannelManagerInterface
|
||||
{
|
||||
use InteractsWithApplications;
|
||||
|
||||
/**
|
||||
* The underlying array of applications and their channels.
|
||||
*
|
||||
* @var array<string, array<string, array<string, \Laravel\Reverb\Protocols\Pusher\Channels\Channel>>>
|
||||
*/
|
||||
protected $applications = [];
|
||||
|
||||
/**
|
||||
* The application instance.
|
||||
*
|
||||
* @var \Laravel\Reverb\Application
|
||||
*/
|
||||
protected $application;
|
||||
|
||||
/**
|
||||
* Get the application instance.
|
||||
*/
|
||||
public function app(): ?Application
|
||||
{
|
||||
return $this->application;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all the channels.
|
||||
*
|
||||
* @return array<string, \Laravel\Reverb\Protocols\Pusher\Channels\Channel>
|
||||
*/
|
||||
public function all(): array
|
||||
{
|
||||
return $this->channels();
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the given channel exists.
|
||||
*/
|
||||
public function exists(string $channel): bool
|
||||
{
|
||||
return isset($this->applications[$this->application->id()][$channel]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the given channel
|
||||
*/
|
||||
public function find(string $channel): ?Channel
|
||||
{
|
||||
return $this->channels($channel);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the given channel or create it if it doesn't exist.
|
||||
*/
|
||||
public function findOrCreate(string $channelName): Channel
|
||||
{
|
||||
if ($channel = $this->find($channelName)) {
|
||||
return $channel;
|
||||
}
|
||||
|
||||
$channel = ChannelBroker::create($channelName);
|
||||
|
||||
$this->applications[$this->application->id()][$channel->name()] = $channel;
|
||||
|
||||
ChannelCreated::dispatch($channel);
|
||||
|
||||
return $channel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all of the connections for the given channels.
|
||||
*
|
||||
* @return array<string, \Laravel\Reverb\Protocols\Pusher\Channels\ChannelConnection>
|
||||
*/
|
||||
public function connections(?string $channel = null): array
|
||||
{
|
||||
$channels = Arr::wrap($this->channels($channel));
|
||||
|
||||
return array_reduce($channels, function ($carry, $channel) {
|
||||
return $carry + $channel->connections();
|
||||
}, []);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsubscribe from all channels.
|
||||
*/
|
||||
public function unsubscribeFromAll(Connection $connection): void
|
||||
{
|
||||
foreach ($this->channels() as $channel) {
|
||||
$channel->unsubscribe($connection);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the given channel.
|
||||
*/
|
||||
public function remove(Channel $channel): void
|
||||
{
|
||||
unset($this->applications[$this->application->id()][$channel->name()]);
|
||||
|
||||
ChannelRemoved::dispatch($channel);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the given channel.
|
||||
*/
|
||||
public function channel(string $channel): ?Channel
|
||||
{
|
||||
return $this->channels($channel);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the channels for the application.
|
||||
*
|
||||
* @return \Laravel\Reverb\Protocols\Pusher\Channels\Channel|array<string, \Laravel\Reverb\Protocols\Pusher\Channels\Channel>|null
|
||||
*/
|
||||
public function channels(?string $channel = null): Channel|array|null
|
||||
{
|
||||
$channels = $this->applications[$this->application->id()] ?? [];
|
||||
|
||||
if (isset($channel)) {
|
||||
return $channels[$channel] ?? null;
|
||||
}
|
||||
|
||||
return $channels;
|
||||
}
|
||||
|
||||
/**
|
||||
* Flush the channel manager repository.
|
||||
*/
|
||||
public function flush(): void
|
||||
{
|
||||
app(ApplicationProvider::class)
|
||||
->all()
|
||||
->each(function (Application $application) {
|
||||
$this->applications[$application->id()] = [];
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Reverb\Protocols\Pusher;
|
||||
|
||||
enum MetricType: string
|
||||
{
|
||||
case CONNECTIONS = 'connections';
|
||||
case CHANNEL = 'channel';
|
||||
case CHANNELS = 'channels';
|
||||
case CHANNEL_USERS = 'channel_users';
|
||||
}
|
||||
@@ -0,0 +1,266 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Reverb\Protocols\Pusher;
|
||||
|
||||
use Illuminate\Support\Str;
|
||||
use Laravel\Reverb\Application;
|
||||
use Laravel\Reverb\Protocols\Pusher\Concerns\InteractsWithChannelInformation;
|
||||
use Laravel\Reverb\Protocols\Pusher\Contracts\ChannelManager;
|
||||
use Laravel\Reverb\ServerProviderManager;
|
||||
use Laravel\Reverb\Servers\Reverb\Contracts\PubSubProvider;
|
||||
use React\Promise\Deferred;
|
||||
use React\Promise\PromiseInterface;
|
||||
|
||||
use function React\Promise\Timer\timeout;
|
||||
|
||||
class MetricsHandler
|
||||
{
|
||||
use InteractsWithChannelInformation;
|
||||
|
||||
/**
|
||||
* The metrics being gathered.
|
||||
*
|
||||
* @var array<string, PendingMetric>
|
||||
*/
|
||||
protected array $metrics = [];
|
||||
|
||||
/**
|
||||
* Create an instance of the metrics handler.
|
||||
*/
|
||||
public function __construct(
|
||||
protected ServerProviderManager $serverProviderManager,
|
||||
protected ChannelManager $channels,
|
||||
protected PubSubProvider $pubSubProvider
|
||||
) {
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Gather the metrics for the given type.
|
||||
*/
|
||||
public function gather(Application $application, string $type, array $options = []): PromiseInterface
|
||||
{
|
||||
$metric = new PendingMetric(
|
||||
Str::random(10),
|
||||
$application,
|
||||
MetricType::from($type),
|
||||
$options
|
||||
);
|
||||
|
||||
return $this->serverProviderManager->subscribesToEvents()
|
||||
? $this->gatherMetricsFromSubscribers($metric)
|
||||
: $this->promise($this->get($metric));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the metrics for the given type.
|
||||
*/
|
||||
public function get(PendingMetric $metric): array
|
||||
{
|
||||
return match ($metric->type()) {
|
||||
MetricType::CHANNEL => $this->channel($metric),
|
||||
MetricType::CHANNELS => $this->channels($metric),
|
||||
MetricType::CHANNEL_USERS => $this->channelUsers($metric),
|
||||
MetricType::CONNECTIONS => $this->connections($metric),
|
||||
default => [],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the channel for the given application.
|
||||
*/
|
||||
protected function channel(PendingMetric $metric): array
|
||||
{
|
||||
return $this->info($metric->application(), $metric->option('channel'), $metric->option('info', ''));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the channels for the given application.
|
||||
*/
|
||||
protected function channels(PendingMetric $metric): array
|
||||
{
|
||||
if ($metric->option('channels')) {
|
||||
return $this->infoForChannels($metric->application(), $metric->option('channels'), $metric->option('info', ''));
|
||||
}
|
||||
|
||||
$channels = collect($this->channels->for($metric->application())->all());
|
||||
|
||||
if ($filter = ($metric->option('filter', false))) {
|
||||
$channels = $channels->filter(fn ($channel) => Str::startsWith($channel->name(), $filter));
|
||||
}
|
||||
|
||||
$channels = $channels->filter(fn ($channel) => count($channel->connections()) > 0);
|
||||
|
||||
return $this->infoForChannels(
|
||||
$metric->application(),
|
||||
$channels->all(),
|
||||
$metric->option('info', '')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the channel users for the given application.
|
||||
*/
|
||||
protected function channelUsers(PendingMetric $metric): array
|
||||
{
|
||||
$channel = $this->channels->for($metric->application())->find($metric->option('channel'));
|
||||
|
||||
if (! $channel) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return collect($channel->connections())
|
||||
->map(fn ($connection) => $connection->data())
|
||||
->unique('user_id')
|
||||
->map(fn ($data) => ['id' => $data['user_id']])
|
||||
->values()
|
||||
->all();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the connections for the given application.
|
||||
*/
|
||||
protected function connections(PendingMetric $metric): array
|
||||
{
|
||||
return $this->channels->for($metric->application())->connections();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gather metrics from all subscribers for the given type.
|
||||
*/
|
||||
protected function gatherMetricsFromSubscribers(PendingMetric $metric): PromiseInterface
|
||||
{
|
||||
$this->metrics[$metric->key()] = $metric;
|
||||
|
||||
$deferred = $this->listenForMetrics($metric);
|
||||
|
||||
$this->requestMetricsFromSubscribers($metric);
|
||||
|
||||
return timeout($deferred->promise(), 10)->then(
|
||||
fn ($metrics) => $metrics,
|
||||
fn () => $this->metrics[$metric->key()]?->resolve() ?? [],
|
||||
)->then(
|
||||
fn ($metrics) => $this->mergeSubscriberMetrics($metrics, $metric->type())
|
||||
)->finally(
|
||||
fn () => $this->stopListening($metric)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Request metrics from all subscribers.
|
||||
*/
|
||||
protected function requestMetricsFromSubscribers(PendingMetric $metric): void
|
||||
{
|
||||
$this->pubSubProvider->publish([
|
||||
'type' => 'metrics',
|
||||
'payload' => serialize($metric),
|
||||
])->then(function ($total) use ($metric) {
|
||||
$metric->setSubscriberCount($total);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge the given metrics into a single result set.
|
||||
*/
|
||||
protected function mergeSubscriberMetrics(array $metrics, MetricType $type): array
|
||||
{
|
||||
return match ($type) {
|
||||
MetricType::CONNECTIONS => array_reduce($metrics, fn ($carry, $item) => array_merge($carry, $item), []),
|
||||
MetricType::CHANNELS => $this->mergeChannels($metrics),
|
||||
MetricType::CHANNEL => $this->mergeChannel($metrics),
|
||||
MetricType::CHANNEL_USERS => collect($metrics)->flatten(1)->unique()->all(),
|
||||
default => [],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge multiple channel instances into a single set.
|
||||
*/
|
||||
protected function mergeChannel(array $metrics): array
|
||||
{
|
||||
return collect($metrics)
|
||||
->reduce(function ($carry, $item) {
|
||||
collect($item)->each(fn ($value, $key) => $carry->put($key, match ($key) {
|
||||
'occupied' => $carry->get($key, false) || $value,
|
||||
'user_count' => $carry->get($key, 0) + $value,
|
||||
'subscription_count' => $carry->get($key, 0) + $value,
|
||||
default => $value,
|
||||
}));
|
||||
|
||||
return $carry;
|
||||
}, collect())
|
||||
->all();
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge multiple sets of channel instances into a single result set.
|
||||
*/
|
||||
protected function mergeChannels(array $metrics): array
|
||||
{
|
||||
return collect($metrics)
|
||||
->reduce(function ($carry, $item) {
|
||||
collect($item)->each(function ($data, $channel) use ($carry) {
|
||||
$metrics = $carry->get($channel, []);
|
||||
$metrics[] = $data;
|
||||
$carry->put($channel, $metrics);
|
||||
});
|
||||
|
||||
return $carry;
|
||||
}, collect())
|
||||
->map(fn ($metrics) => $this->mergeChannel($metrics))
|
||||
->all();
|
||||
}
|
||||
|
||||
/**
|
||||
* Listen for metrics from subscribers.
|
||||
*/
|
||||
protected function listenForMetrics(PendingMetric $metric): Deferred
|
||||
{
|
||||
$deferred = new Deferred;
|
||||
|
||||
$this->pubSubProvider->on($metric->key(), function ($payload) use ($metric, $deferred) {
|
||||
$pending = $this->metrics[$metric->key()];
|
||||
$pending->append($payload['payload']);
|
||||
|
||||
if ($pending->resolvable()) {
|
||||
$deferred->resolve($pending->resolve());
|
||||
}
|
||||
});
|
||||
|
||||
return $deferred;
|
||||
}
|
||||
|
||||
/**
|
||||
* Publish the metrics for the given type.
|
||||
*/
|
||||
public function publish(PendingMetric $metric): void
|
||||
{
|
||||
$this->pubSubProvider->publish([
|
||||
'type' => $metric->key(),
|
||||
'payload' => $this->get($metric),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop listening for the given metric.
|
||||
*/
|
||||
protected function stopListening(PendingMetric $metric): void
|
||||
{
|
||||
unset($this->metrics[$metric->key()]);
|
||||
$this->pubSubProvider->stopListening($metric->key());
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a promise to resolve the given value.
|
||||
*/
|
||||
protected function promise(mixed $value): PromiseInterface
|
||||
{
|
||||
$deferred = new Deferred;
|
||||
|
||||
$promise = $deferred->promise();
|
||||
|
||||
$deferred->resolve($value);
|
||||
|
||||
return $promise;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Reverb\Protocols\Pusher;
|
||||
|
||||
use Laravel\Reverb\Application;
|
||||
|
||||
class PendingMetric
|
||||
{
|
||||
/**
|
||||
* The number of subscribers for the metric.
|
||||
*/
|
||||
protected ?int $subscribers = null;
|
||||
|
||||
/**
|
||||
* The data for the metric.
|
||||
*/
|
||||
protected array $data = [];
|
||||
|
||||
/**
|
||||
* Instantiate a new pending metric.
|
||||
*/
|
||||
public function __construct(
|
||||
protected string $key,
|
||||
protected Application $application,
|
||||
protected MetricType $type,
|
||||
protected array $options = [],
|
||||
) {
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the metric key.
|
||||
*/
|
||||
public function key(): string
|
||||
{
|
||||
return $this->key;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the metric type.
|
||||
*/
|
||||
public function type(): MetricType
|
||||
{
|
||||
return $this->type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the application for the metric.
|
||||
*/
|
||||
public function application(): Application
|
||||
{
|
||||
return $this->application;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an option for the metric.
|
||||
*/
|
||||
public function option(string $key, mixed $default = null): mixed
|
||||
{
|
||||
return $this->options[$key] ?? $default;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the options for the metric.
|
||||
*/
|
||||
public function options(): array
|
||||
{
|
||||
return $this->options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the subscriber count for the metric.
|
||||
*/
|
||||
public function setSubscriberCount(int $count): void
|
||||
{
|
||||
$this->subscribers = $count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Append data to the metric.
|
||||
*/
|
||||
public function append(array $data): void
|
||||
{
|
||||
$this->data[] = $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the metric is resolvable.
|
||||
*/
|
||||
public function resolvable(): bool
|
||||
{
|
||||
return $this->subscribers !== null && count($this->data) === $this->subscribers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the data for the metric.
|
||||
*/
|
||||
public function resolve(): array
|
||||
{
|
||||
return $this->data;
|
||||
}
|
||||
}
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Reverb\Protocols\Pusher;
|
||||
|
||||
use Laravel\Reverb\Application;
|
||||
use Laravel\Reverb\Protocols\Pusher\Contracts\ChannelManager;
|
||||
use Laravel\Reverb\Servers\Reverb\Contracts\PubSubIncomingMessageHandler;
|
||||
|
||||
class PusherPubSubIncomingMessageHandler implements PubSubIncomingMessageHandler
|
||||
{
|
||||
protected array $events = [];
|
||||
|
||||
/**
|
||||
* Handle an incoming message from the PubSub provider.
|
||||
*/
|
||||
public function handle(string $payload): void
|
||||
{
|
||||
$event = json_decode($payload, associative: true, flags: JSON_THROW_ON_ERROR);
|
||||
|
||||
$this->processEventListeners($event);
|
||||
|
||||
$application = unserialize($event['application'] ?? null, ['allowed_classes' => [Application::class]]);
|
||||
|
||||
$except = isset($event['socket_id']) ?
|
||||
app(ChannelManager::class)->for($application)->connections()[$event['socket_id']] ?? null
|
||||
: null;
|
||||
|
||||
match ($event['type'] ?? null) {
|
||||
'message' => EventDispatcher::dispatchSynchronously(
|
||||
$application,
|
||||
$event['payload'],
|
||||
$except?->connection()
|
||||
),
|
||||
'metrics' => app(MetricsHandler::class)->publish(
|
||||
unserialize($event['payload'], ['allowed_classes' => [
|
||||
Application::class, PendingMetric::class, MetricType::class,
|
||||
]])
|
||||
),
|
||||
'terminate' => collect(app(ChannelManager::class)->for($application)->connections())
|
||||
->each(function ($connection) use ($event) {
|
||||
if ((string) $connection->data()['user_id'] === $event['payload']['user_id']) {
|
||||
$connection->disconnect();
|
||||
}
|
||||
}),
|
||||
default => null,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Process the given event.
|
||||
*/
|
||||
protected function processEventListeners(array $event): void
|
||||
{
|
||||
foreach ($this->events as $eventName => $listeners) {
|
||||
if (($event['type'] ?? null) === $eventName) {
|
||||
foreach ($listeners as $listener) {
|
||||
$listener($event);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Listen for the given event.
|
||||
*/
|
||||
public function listen(string $event, callable $callback): void
|
||||
{
|
||||
$this->events[$event][] = $callback;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop listening for the given event.
|
||||
*/
|
||||
public function stopListening(string $event): void
|
||||
{
|
||||
unset($this->events[$event]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Reverb\Protocols\Pusher;
|
||||
|
||||
use Exception;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Support\Str;
|
||||
use Laravel\Reverb\Contracts\Connection;
|
||||
use Laravel\Reverb\Events\MessageReceived;
|
||||
use Laravel\Reverb\Loggers\Log;
|
||||
use Laravel\Reverb\Protocols\Pusher\Contracts\ChannelManager;
|
||||
use Laravel\Reverb\Protocols\Pusher\Exceptions\ConnectionLimitExceeded;
|
||||
use Laravel\Reverb\Protocols\Pusher\Exceptions\InvalidOrigin;
|
||||
use Laravel\Reverb\Protocols\Pusher\Exceptions\PusherException;
|
||||
use Ratchet\RFC6455\Messaging\Frame;
|
||||
use Ratchet\RFC6455\Messaging\FrameInterface;
|
||||
use Throwable;
|
||||
|
||||
class Server
|
||||
{
|
||||
/**
|
||||
* Create a new server instance.
|
||||
*/
|
||||
public function __construct(protected ChannelManager $channels, protected EventHandler $handler)
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the a client connection.
|
||||
*/
|
||||
public function open(Connection $connection): void
|
||||
{
|
||||
try {
|
||||
$this->ensureWithinConnectionLimit($connection);
|
||||
$this->verifyOrigin($connection);
|
||||
|
||||
$connection->touch();
|
||||
|
||||
$this->handler->handle($connection, 'pusher:connection_established');
|
||||
|
||||
Log::info('Connection Established', $connection->id());
|
||||
} catch (Exception $e) {
|
||||
$this->error($connection, $e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a new message received by the connected client.
|
||||
*/
|
||||
public function message(Connection $from, string $message): void
|
||||
{
|
||||
Log::info('Message Received', $from->id());
|
||||
Log::message($message);
|
||||
|
||||
$from->touch();
|
||||
|
||||
try {
|
||||
$event = json_decode($message, associative: true, flags: JSON_THROW_ON_ERROR);
|
||||
|
||||
if (Str::isJson($event['data'] ?? null)) {
|
||||
$event['data'] = json_decode($event['data'], associative: true, flags: JSON_THROW_ON_ERROR);
|
||||
}
|
||||
|
||||
Validator::make($event, ['event' => ['required', 'string']])->validate();
|
||||
|
||||
match (Str::startsWith($event['event'], 'pusher:')) {
|
||||
true => $this->handler->handle(
|
||||
$from,
|
||||
$event['event'],
|
||||
empty($event['data']) ? [] : $event['data'],
|
||||
),
|
||||
default => ClientEvent::handle($from, $event)
|
||||
};
|
||||
|
||||
Log::info('Message Handled', $from->id());
|
||||
|
||||
MessageReceived::dispatch($from, $message);
|
||||
} catch (Throwable $e) {
|
||||
$this->error($from, $e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a low-level WebSocket control frame.
|
||||
*/
|
||||
public function control(Connection $from, FrameInterface $message): void
|
||||
{
|
||||
Log::info('Control Frame Received', $from->id());
|
||||
Log::message($message);
|
||||
|
||||
$from->setUsesControlFrames();
|
||||
|
||||
if (in_array($message->getOpcode(), [Frame::OP_PING, Frame::OP_PONG], strict: true)) {
|
||||
$from->touch();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a client disconnection.
|
||||
*/
|
||||
public function close(Connection $connection): void
|
||||
{
|
||||
$this->channels
|
||||
->for($connection->app())
|
||||
->unsubscribeFromAll($connection);
|
||||
|
||||
$connection->disconnect();
|
||||
|
||||
Log::info('Connection Closed', $connection->id());
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle an error.
|
||||
*/
|
||||
public function error(Connection $connection, Throwable $exception): void
|
||||
{
|
||||
if ($exception instanceof PusherException) {
|
||||
$connection->send(json_encode($exception->payload()));
|
||||
|
||||
Log::error('Message from '.$connection->id().' resulted in a pusher error');
|
||||
Log::info($exception->getMessage());
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$connection->send(json_encode([
|
||||
'event' => 'pusher:error',
|
||||
'data' => json_encode([
|
||||
'code' => 4200,
|
||||
'message' => 'Invalid message format',
|
||||
]),
|
||||
]));
|
||||
|
||||
Log::error('Message from '.$connection->id().' resulted in an unknown error');
|
||||
Log::info($exception->getMessage());
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure the server is within the connection limit.
|
||||
*/
|
||||
protected function ensureWithinConnectionLimit(Connection $connection): void
|
||||
{
|
||||
if (! $connection->app()->hasMaxConnectionLimit()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$connections = $this->channels->for($connection->app())->connections();
|
||||
|
||||
if (count($connections) >= $connection->app()->maxConnections()) {
|
||||
throw new ConnectionLimitExceeded;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify the origin of the connection.
|
||||
*
|
||||
* @throws \Laravel\Reverb\Exceptions\InvalidOrigin
|
||||
*/
|
||||
protected function verifyOrigin(Connection $connection): void
|
||||
{
|
||||
$allowedOrigins = $connection->app()->allowedOrigins();
|
||||
|
||||
if (in_array('*', $allowedOrigins)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$origin = parse_url($connection->origin(), PHP_URL_HOST);
|
||||
|
||||
foreach ($allowedOrigins as $allowedOrigin) {
|
||||
if (Str::is($allowedOrigin, $origin)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
throw new InvalidOrigin;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user