Skip to content
This repository was archived by the owner on Feb 7, 2024. It is now read-only.

[Question] User Online Status #117

Closed
zek opened this issue Feb 21, 2019 · 19 comments
Closed

[Question] User Online Status #117

zek opened this issue Feb 21, 2019 · 19 comments

Comments

@zek
Copy link

zek commented Feb 21, 2019

Is it possible to track user online status?

I am developing a chat app and I want to show online status of user

@stayallive
Copy link
Contributor

Yes, you can use presence channels for this.

@zek
Copy link
Author

zek commented Feb 22, 2019

@stayallive Thank you for the answer.

Presence channels have some limits associated with them: 100 members maximum

I need more than 100 members and for security purposes I want to share online status for specific users not everyone.

Is it possible to check online status of users at server-side.

If I can catch events (Online, Offline) on backend, that would be super cool.

@stayallive
Copy link
Contributor

stayallive commented Feb 22, 2019

AFAIK this library does not impose that 100 members limit.

However do keep in mind that presence channels indeed broadcast all members to each-other, you can however query the users in a presence channel from the server side using the REST API.

Possible solution would be to have each user connect to their own private channel (something like private-user-[user-id] and query if that channel exists/is occupied using the REST API.

However, this an imperfect implementation since that would require you to query the API to retrieve the status since webhooks are not implemented in this library.

@zek
Copy link
Author

zek commented Feb 22, 2019

However do keep in mind that presence channels indeed broadcast all members to each-other

So online status also will be available to all users even the user xxx is not friend with yyy. They can see each other's online status? Did I understand wrong?

I tried to do same thing in tlaverdure/laravel-echo-server. Whenever user subscribes a channel I was setting user as online in database. But the problem is if the user refreshes the page leave event comes a bit late. So the last message says user is offline. Trying to find a better implementation as whatsapp does.

So
User joins channel, set online
User joins channel, set online (after refreshing)
Leaves channel (that comes so late)

@stayallive
Copy link
Contributor

So online status also will be available to all users even the user xxx is not friend with yyy. They can see each other's online status? Did I understand wrong?

^ when using a presence channel, yes. (if every user in the system subscribes to the same presence channel, although it could be just UUID, not the full user profile needs to be there)

User joins channel, set online
User joins channel, set online (after refreshing)
Leaves channel (that comes so late)

I'm not sure what you are trying to say here sorry. Especially the "that comes so late" I don't understand 😄 Could you explain a little bit longer?


The below is a bit off-topic but might help to get where you want to go:

But without webhooks maybe the best way is to have a "heartbeat", with that I mean you have a AJAX request to your app every X seconds that sets the user status to "online" (updates last_seen_online in your db) and have the online status show offline as soon as the last_seen_online timestamp is > x seconds ago. Possibly have a CRON that dispatches events to the "friends/chats" so the online status updates in realtime for users that did not refresh.

@zek
Copy link
Author

zek commented Feb 22, 2019

I'm not sure what you are trying to say here sorry. Especially the "that comes so late" I don't understand 😄 Could you explain a little bit longer?

I am sorry for my bad english and explanation :) I hope I explained it a bit better now.

There is a PR in laravel-echo-server (tlaverdure/laravel-echo-server#322) which adds webhook feature so whenever a user joins or leaves a channel it sends a request to laravel.

I used this feature to implement online/offline status.

Whenever user joins "user.{userId}". I set user's online status as true in database. Join hook works perfectly but if user disconnects (like refreshing page or closing the application) leave hook fired a bit late. For example after 10 seconds event is fired.

If the meantime user reconnects, I set user status as online again.

10 seconds later webhook is fired I got leave event but I got no idea if this event belongs to first session or second session (which is created after page refresh or connection lose). And I marked user as offline.

My app also has voip feature so users can call their friends. I want to show online/offline status instantly.

I thought If I switch to this library, I can handle this situation better as whatsapp does.

@stayallive
Copy link
Contributor

No problem, I understand better now 👍

You really need webhooks for this indeed (or a way for the server to notify your server about events happening).

About the offline / online status there is a possible solution, don't have a is_online boolean value but count how many online/offline events you get.

Example:

[User #1] Joins chat.1
[Your app] Retrieves join webhook -> user.1 -> online_count += 1 (now 1, online!)
[User #1] Joins chat.2
[Your app] Retrieves join webhook -> user.1 -> online_count += 1 (now 2, online!)
[User #1] Leaves chat.2
[Your app] Retrieves leave webhook -> user.1 -> online_count -= 1 (now 1, still online!)
[User #1] Leaves chat.1
[Your app] Retrieves leave webhook -> user.1 -> online_count -= 1 (now 0, offline!)

^ this could also be implemented by a user.<user_id> private channel used just for online status, it's just an example.

There is no other way I think, and the leave event being delayed by a few seconds is a good thing IMO, since a user might just refresh the page or looses connection for a bit and it would be annoying to have the status flashing online/offline if a user just refreshes the page.

But since this library does not (yet, there is a PR in the works I believe) support webhooks or other ways to notify the server about join/leave events I think Laravel Echo might be your best bet currently (or Pusher ofcourse since they also support webhooks)

Comparing to WhatsApp is going to get you nowhere ;) WhatsApp invests hundreds and thousands of dollars in their infrastructure and controls every line of code so you will probably only come close, but never perfect.

@zek
Copy link
Author

zek commented Feb 22, 2019

Comparing to WhatsApp is going to get you nowhere ;) WhatsApp invests hundreds and thousands of dollars in their infrastructure and controls every line of code so you will probably only come close, but never perfect.

You're so right about it 😅

Thank you for your solution 👍 and time :) but I guess I should also lock table record before updating. I will apply this solution but adding some feature or ability to achieve this functionality could be useful for everyone.

@stayallive
Copy link
Contributor

Yeah agreed.

Ofcourse I'm sure the people possibly working on webhooks (#80) could use some help or if you have an idea for a better implementation that doesn't involve webhooks people needing this would appreciate a PR for it 👍

Good luck with the app!

@zek zek closed this as completed Feb 22, 2019
@zek
Copy link
Author

zek commented Apr 6, 2019

Hello again @stayallive :)

OK I just switched to laravel-websockets. And I inspected the codes a little bit.

I think we can just add basic laravel events when a user subscribes a channel and leaves like which is done in DashboardLogger.

So instead calling DashboardLogger::subscribed we can just fire an event and DashboardLogger can catch also my application can catch the event :) Or even we can just these events to redis.

What do you think about it?

@zek
Copy link
Author

zek commented Apr 6, 2019

I've just extended channelmanager and channels. What do you think about implementing an event system like this?

<?php

namespace App\Services\Websockets\Channels;

use BeyondCode\LaravelWebSockets\WebSockets\Channels\PrivateChannel as BasePrivateChannel;
use Illuminate\Support\Facades\Event;
use stdClass;
use Ratchet\ConnectionInterface;

class PrivateChannel extends BasePrivateChannel
{

    public function subscribe(ConnectionInterface $connection, stdClass $payload)
    {
        dump("Subscribe: " . $this->channelName);

        Event::fire('websocket.channel.subscribe', [$this->channelName]);

        parent::subscribe($connection, $payload);
    }

    public function unsubscribe(ConnectionInterface $connection)
    {
        dump("Disconnect: " . $this->channelName);

        Event::fire('websocket.channel.unsubscribe', [$this->channelName]);

        parent::unsubscribe($connection);
    }

}

Also Websocket Server started, stopped events could be nice. So whenever websocket opens I can set all users offline or do some logic :) Because if I change my source code I need to restart websocket server.

@zek
Copy link
Author

zek commented Apr 7, 2019

<?php

namespace App\Services\Websockets\Channels;

use BeyondCode\LaravelWebSockets\WebSockets\Channels\PrivateChannel as BasePrivateChannel;
use Illuminate\Support\Facades\Redis;
use stdClass;
use Ratchet\ConnectionInterface;

class PrivateChannel extends BasePrivateChannel
{

    public function subscribe(ConnectionInterface $connection, stdClass $payload)
    {
        parent::subscribe($connection, $payload);

        Redis::publish('subscribe.' . $this->channelName, json_encode($payload));
    }

    public function unsubscribe(ConnectionInterface $connection)
    {
        Redis::publish('unsubscribe.' . $this->channelName, json_encode([]));

        parent::unsubscribe($connection);
    }

}

Here is the latest code that I use. I am encountering with a weird bug. I am not sure if I am doing something wrong or library is buggy.

I got 2 clients and I restarted application as user 21 and here is the console output.

"unsubscribe: private-user.20"
"unsubscribe: private-foreground.20"
"unsubscribe: private-user.21"
"unsubscribe: private-foreground.21"
"subscribe: private-user.21"
"subscribe: private-foreground.21"

As you can see unsubscribe action is called for private-user.20 but I didn't do any action on user 20. Also look at the console output for user 20. It looks like there is a bug in library.

"unsubscribe: private-user.20"
"unsubscribe: private-foreground.20"
"unsubscribe: private-user.21"
"unsubscribe: private-foreground.21"
"subscribe: private-user.20"
"subscribe: private-foreground.20"
Connection id 971859506.14759857 closed.
New connection opened for app key EUgNdRG3rGFE46jJqW7BQwdK.
Connection id 420796035.247941275 sending message {"event":"pusher:connection_established","data":"{\"socket_id\":\"420796035.247941275\",\"activity_timeout\":30}"}
183: connection id 420796035.247941275 received message: {"event":"pusher:subscribe","data":{"auth":"EUgNdRG3rGFE46jJqW7BQwdK:95818902f47855721b300268851bf41658d3f1a82f0c36e894e9191a88a1d1d2","channel":"private-user.20"}}.
Connection id 420796035.247941275 sending message {"event":"pusher_internal:subscription_succeeded","channel":"private-user.20"}
183: connection id 420796035.247941275 received message: {"event":"pusher:subscribe","data":{"auth":"EUgNdRG3rGFE46jJqW7BQwdK:257577455116ba2c754c4f84d853aa16be561c3bada224f547d84fea72349778","channel":"private-foreground.20"}}.
Connection id 420796035.247941275 sending message {"event":"pusher_internal:subscription_succeeded","channel":"private-foreground.20"}
183: connection id 926980622.113889915 received message: {"event":"pusher:ping","data":{}}.
Connection id 926980622.113889915 sending message {"event":"pusher:pong"}

As I understand, it is related with removeFromAllChannels function.

@zek zek reopened this Apr 7, 2019
@zek
Copy link
Author

zek commented Apr 7, 2019

OK I figured out I should check if user subscribed to channel as below :)

if (isset($this->subscribedConnections[$connection->socketId])) {
}

@ronmrcdo
Copy link

ronmrcdo commented Sep 10, 2019

HI @zek I'm quite into this scenarion how you manage to get the users

Here's my extended Manager

image

Am I into right path?

@zek
Copy link
Author

zek commented Sep 10, 2019

@ronmrcdo I stopped using this library :/

@zek
Copy link
Author

zek commented Sep 11, 2019

<?php

namespace App\Services\Websockets;

use App\Services\Websockets\Channels\Channel;
use App\Services\Websockets\Channels\PresenceChannel;
use App\Services\Websockets\Channels\PrivateChannel;
use BeyondCode\LaravelWebSockets\WebSockets\Channels\ChannelManagers\ArrayChannelManager as BaseArrayChannelManager;
use Illuminate\Support\Str;

class ArrayChannelManager extends BaseArrayChannelManager
{

    protected function determineChannelClass(string $channelName): string
    {
        if (Str::startsWith($channelName, 'private-')) {
            return PrivateChannel::class;
        }

        if (Str::startsWith($channelName, 'presence-')) {
            return PresenceChannel::class;
        }

        return Channel::class;
    }

}
<?php

namespace App\Services\Websockets\Channels;

use BeyondCode\LaravelWebSockets\WebSockets\Channels\Channel as BaseChannel;
use Illuminate\Support\Facades\Redis;
use stdClass;
use Ratchet\ConnectionInterface;

class Channel extends BaseChannel
{

    public function subscribe(ConnectionInterface $connection, stdClass $payload)
    {
        parent::subscribe($connection, $payload);

        Redis::publish('subscribe.' . $this->channelName, json_encode($payload));
    }

    public function unsubscribe(ConnectionInterface $connection)
    {
        if (isset($this->subscribedConnections[$connection->socketId])) {
            Redis::publish('unsubscribe.' . $this->channelName, json_encode([]));
        }

        parent::unsubscribe($connection);
    }

}
<?php

namespace App\Services\Websockets\Channels;

use BeyondCode\LaravelWebSockets\WebSockets\Channels\PresenceChannel as BasePresenceChannel;
use Illuminate\Support\Facades\Redis;
use stdClass;
use Ratchet\ConnectionInterface;

class PresenceChannel extends BasePresenceChannel
{

    public function subscribe(ConnectionInterface $connection, stdClass $payload)
    {
        parent::subscribe($connection, $payload);

        Redis::publish('subscribe.' . $this->channelName, json_encode($payload));
    }

    public function unsubscribe(ConnectionInterface $connection)
    {
        if (isset($this->subscribedConnections[$connection->socketId])) {
            Redis::publish('unsubscribe.' . $this->channelName, json_encode([]));
        }

        parent::unsubscribe($connection);
    }

}
<?php

namespace App\Services\Websockets\Channels;

use BeyondCode\LaravelWebSockets\WebSockets\Channels\PrivateChannel as BasePrivateChannel;
use Illuminate\Support\Facades\Redis;
use stdClass;
use Ratchet\ConnectionInterface;

class PrivateChannel extends BasePrivateChannel
{

    public function subscribe(ConnectionInterface $connection, stdClass $payload)
    {
        parent::subscribe($connection, $payload);

        Redis::publish('subscribe.' . $this->channelName, json_encode($payload));
    }

    public function unsubscribe(ConnectionInterface $connection)
    {
        if (isset($this->subscribedConnections[$connection->socketId])) {
            Redis::publish('unsubscribe.' . $this->channelName, json_encode([]));
        }

        parent::unsubscribe($connection);
    }

}

Lastly in config/websockets.php

    'channel_manager' => \App\Services\Websockets\ArrayChannelManager::class,

@ronmrcdo
Copy link

HI zek. Thanks for pointing it out. and I really appreciate it.

@tibinvpaul
Copy link

Hey @zek - I read your comment saying that you stopped using this library! I am making my system production-ready and we heavily use realtime messages. Did you stop using this after identifying some issues like performance, scalability, etc? I don't want to change the technology after going live so it would be great if you can share your experience. Thanks in advance!

@mjjabarullah
Copy link

Is there any solution to store online offline status of user using private channel??

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

6 participants