beyondcode / laravel-websockets

Websockets for Laravel. Done right.
https://beyondco.de/docs/laravel-websockets
MIT License
5.08k stars 628 forks source link

Exist a way to implement webhooks? #80

Closed joaokamun closed 5 years ago

joaokamun commented 5 years ago

The only missing feature for me fully migrate from pusher is the event webhooks, I could not find anything about it. Is something planned to have? I can contribute to that, just want to know if you guys have something in mind or some direction to follow.

Regards,

enzonotario commented 5 years ago

I also want this! For now (maybe it is a wrong way, but it works) I just have created a class that extends BeyondCode\LaravelWebSockets\WebSockets\WebSocketHandler, and a custom Router, and put my own logic in certain hooks...

stayallive commented 5 years ago

This would be really cool but possibly also tricky looking at the docs it has a few "specialties" that requires some work to make this work correctly.

For example: https://pusher.com/docs/webhooks#delay

Just spit-ballin' here, but it might be a good one to write events to an Redis buffer/list and let another process/cron handle the actual sending to prevent adding to much work to the websocket server process causing it to block more than needed (which also would be an easy-ish way to get batched hook working easily).

joaokamun commented 5 years ago

@stayallive So basically the main feature to be develop here could be just an option to save events somewhere (like redis), and each individual project cares about what to do with the stored events?

stayallive commented 5 years ago

@joaokamun well, I would implement a php artisan websockets:webhooks command or something like that in this package so you could just add to a cron or daemon running that command which handles all the webhook sending.

stefandanaita commented 5 years ago

I also want this! For now (maybe it is a wrong way, but it works) I just have created a class that extends BeyondCode\LaravelWebSockets\WebSockets\WebSocketHandler, and a custom Router, and put my own logic in certain hooks...

@enzonotario could you please share an example of what you've done? Thanks!

Update: Managed to get it done by extending the websockets.router singleton in the IoC. Thanks for the idea, works nice and smooth. However, a nice webhooks implementation that allows 2 way communication between Laravel and the frontend would be amazing. CC @mpociot @freekmurze

enzonotario commented 5 years ago

@stefandanaita sorry for the delay... I just have registered a singleton in my AppServiceProvider:

    public function register()
    {
        $this->app->singleton('websockets.router', function () {
            return new Router();
        });
    }

Router.php

<?php

namespace App\WebSockets\Server;

use App\WebSockets\WebSockets\WebSocketHandler;
use BeyondCode\LaravelWebSockets\HttpApi\Controllers\FetchChannelController;
use BeyondCode\LaravelWebSockets\HttpApi\Controllers\FetchChannelsController;
use BeyondCode\LaravelWebSockets\HttpApi\Controllers\FetchUsersController;
use BeyondCode\LaravelWebSockets\HttpApi\Controllers\TriggerEventController;

class Router extends \BeyondCode\LaravelWebSockets\Server\Router
{

    public function echo()
    {
        $this->get('/app/{appKey}', WebSocketHandler::class);

        $this->post('/apps/{appId}/events', TriggerEventController::class);
        $this->get('/apps/{appId}/channels', FetchChannelsController::class);
        $this->get('/apps/{appId}/channels/{channelName}', FetchChannelController::class);
        $this->get('/apps/{appId}/channels/{channelName}/users', FetchUsersController::class);
    }

}

WebSocketHandler.php

<?php

namespace App\WebSockets\WebSockets;

use Ratchet\ConnectionInterface;
use Ratchet\RFC6455\Messaging\MessageInterface;

class WebSocketHandler extends \BeyondCode\LaravelWebSockets\WebSockets\WebSocketHandler
{

    public function onMessage(ConnectionInterface $connection, MessageInterface $message)
    {
        // My custom code...

        parent::onMessage($connection, $message);
    }

}

but sure, this is a weird way. Extending as you said in #21 I think that is a better way!

nicolasvahidzein commented 5 years ago

@stefandanaita You sir are BRILLIANT!!!!!!!!!! This was a lifesaver!!! Thank you so much. Quick question though, do you know i can know which client disconnect from the app when the connection is closed? That's the whole point of extending this package. I need to tell the chat app he is offline after a non graceful log out. Thanks.

wmfairuz commented 4 years ago

@stefandanaita You sir are BRILLIANT!!!!!!!!!! This was a lifesaver!!! Thank you so much. Quick question though, do you know i can know which client disconnect from the app when the connection is closed? That's the whole point of extending this package. I need to tell the chat app he is offline after a non graceful log out. Thanks.

@nicolasvahidzein Did you figured out how to identify user once we extend the WebSocketHandler?

ame1337 commented 4 years ago

Hi, In my custom onClose function how can I get user's id or username if I use presence channels?

rennokki commented 4 years ago

@noobshooter27 The connection can make use of the channel manager and you can get the connection from there.

rennokki commented 4 years ago

2.x will contain extendable hooks: https://github.com/beyondcode/laravel-websockets/pull/465

hengjingyoong commented 4 years ago

Hi @rennokki , may I know how can I get the private channel name from the $connection in my custom onClose function? Tried this $this->channelManager->getChannels($connection->app->id), but it seem like don't have the channel name inside.

ame1337 commented 4 years ago

Hi @rennokki , may I know how can I get the private channel name from the $connection in my custom onClose function? Tried this $this->channelManager->getChannels($connection->app->id), but it seem like don't have the channel name inside.

@hengjingyoong try dd($this->channelManager);you'll find everything there

rennokki commented 4 years ago

@hengjingyoong Have you called parent::onClose($connection, $exception) before anything else in your custom method?

hengjingyoong commented 4 years ago

@rennokki, I just used the original WebSocketHandler to test it out.

Thanks @noobshooter27 , finally I managed to find out what is the channel name from the closing connection.

What I did was defined a custom function in BeyondCode\LaravelWebSockets\WebSockets\Channels\Channel

    public function checkConnection($socketId)
    {
        if ($this->subscribedConnections[$socketId] ?? null) {
            return $this->channelName;
        }

        return false;
    }

and then in BeyondCode\LaravelWebSockets\WebSockets\WebSocketHandler, get the channel name before it closed.

    public function onClose(ConnectionInterface $connection)
    {
        $allChannels = $this->channelManager->getChannels($connection->app->id);

        foreach($allChannels as $channel){
            if ($channelName = $channel->checkConnection($connection->socketId)) {
                break;
            }
        }
        dd($channelName);

        $this->channelManager->removeFromAllChannels($connection);

        DashboardLogger::disconnection($connection);

        StatisticsLogger::disconnection($connection);
    }

But I'm not sure if that is secure to work in this way.

ps: my working scenario is attach the user_id to the private channel name, that's why I need to get back the user_id from channel name, and notify my backend which user is offline. This is to handle the non graceful log out mentioned by @nicolasvahidzein

rennokki commented 4 years ago

@hengjingyoong It's highly recommended to not change any vendor/* files.

hengjingyoong commented 4 years ago

@hengjingyoong It's highly recommended to not change any vendor/* files.

Do you have any though on getting the channel name without adding my own function in BeyondCode\LaravelWebSockets\WebSockets\Channels\Channel?

hengjingyoong commented 4 years ago

Finally I figure out how to get the closing channel name.

Here is it.

<?php

namespace App\WebSocket;

use Ratchet\ConnectionInterface;
use BeyondCode\LaravelWebSockets\WebSockets\WebSocketHandler as BaseWebSocketHandler;

class CustomWebSocketHandler extends BaseWebSocketHandler
{
    public function onClose(ConnectionInterface $connection)
    {
        $allChannels = $this->channelManager->getChannels($connection->app->id);

        foreach($allChannels as $channelName => $channel){

            if($channel->getSubscribedConnections()[$connection->socketId] ?? null) {
                $closingChannel = $channel->getChannelName();
                break;
            }
        }

        dd($closingChannel);

        parent::onClose($connection);
    }
}

ps: I was missed out the getSubscribedConnections function in BeyondCode\LaravelWebSockets\WebSockets\Channels\Channel lol

damianed commented 2 years ago

Thank you @hengjingyoong! I made just had to do some minor changes because it was throwing an error, I will leave it here in case someone else needs it

<?php

namespace App\WebSocket;

use Ratchet\ConnectionInterface;
use BeyondCode\LaravelWebSockets\WebSockets\WebSocketHandler as BaseWebSocketHandler;

class CustomWebSocketHandler extends BaseWebSocketHandler
{
    public function onClose(ConnectionInterface $connection)
    {
        $allChannels = $this->channelManager->getChannels($connection->app->id);
        $closingChannel = null;

        foreach($allChannels as $channelName => $channel){

            if($channel->getSubscribedConnections()[$connection->socketId] ?? null) {
                $closingChannel = $channelName;
                break;
            }
        }

        dd($closingChannel);

        parent::onClose($connection);
    }
}
S1ipKn0T commented 2 years ago

Thank you @hengjingyoong! I made just had to do some minor changes because it was throwing an error, I will leave it here in case someone else needs it

<?php

namespace App\WebSocket;

use Ratchet\ConnectionInterface;
use BeyondCode\LaravelWebSockets\WebSockets\WebSocketHandler as BaseWebSocketHandler;

class CustomWebSocketHandler extends BaseWebSocketHandler
{
    public function onClose(ConnectionInterface $connection)
    {
        $allChannels = $this->channelManager->getChannels($connection->app->id);
        $closingChannel = null;

        foreach($allChannels as $channelName => $channel){

            if($channel->getSubscribedConnections()[$connection->socketId] ?? null) {
                $closingChannel = $channelName;
                break;
            }
        }

        dd($closingChannel);

        parent::onClose($connection);
    }
}

Hi thank you for your code but I need users data inside of $allChannels how can i get it? (In my case I use presence channel)

garrettboone commented 1 year ago

Another approach that is simple is using the custom ArrayChannelManager as indicated in the websockets config, and then make a call like:

    public function removeFromAllChannels(ConnectionInterface $connection)
    {
        // THIS ONE LINE
        $response = Http::get(config('app.url') . '/socket-disconnected/' . $connection->socketId . '/{key-for-security}');

        if (!isset($connection->app)) {
            return;
        }

       ....

    }

In web.php:

use App\Http\Controllers\WebsocketController;
Route::get('/socket-disconnected/{socket_id}/{security_key}', [WebsocketController::class, 'socketDisconnected']);

Dispatch the desired job in WebsocketController:

class WebsocketController extends Controller
{
    public function socketDisconnected(Request $request, string $socket_id, string $security_key)
    {
        logger("Socket id received: " . $socket_id);
        if($security_key == config('websockets.security_key')) {
            ProcessWebsocketDisconnects::dispatch($socket_id);
       }
        return response('OK', 200)->header('Content-Type', 'text/plain');
    }
}
nicolasvahidzein commented 1 year ago

Thank you @garrettboone I will have to try this soon. Sounds awesome.