Search code examples
phpwebsocketratchet

How to add user to websocket chat


I've gotten the chat to work but I have hard-coded the user's ID into Chat.php.

My login set's their email to a session ($_SESSION['email']=$email;) when they log into the site.

I can put a hidden field in the chat form with their ID and pass it into Chat.php but I would think there's a better way.

Disclaimer: I know it's not coded very well, I just want to learn it, get it working, and then code it better.

chat_box.php

<?php
echo '<div id="chatbox" class="nav chatbox">',
    '<div class="chatbox-1">',
    '</div>',
    '<div id="send_chat" class="send_chat">',
        '<input type="text" class="text" id="chatsay" name="chatsay" maxlength="200" autocomplete="off">',
        '<button class="submit-chatsend" id="chatsend">Send</button>',
    '</div>',
'</div>';

JS

window.connect = function () {
    window.ws = $.websocket("ws://domain.com:8080/", {
        open: function () {
        },
        close: function () {
        },
        events: {
            fetch: function (e) {
            },
            single: function (e) {
                var elem = e.data;

                if (elem.type == "text") {
                    var html = "<div class='msg' id='" + elem.id + "'><div class='name'>" + elem.name + "</div><div class='msgc'>" + elem.msg.linkify() + "<div class='posted'><time class='timeago' datetime='" + elem.posted + "'>" + elem.posted + "</time></div></div></div>";

                    if (typeof elem.append != "undefined") {
                        $(".msg:last").remove();
                    }

                    if (typeof elem.earlier_msg == "undefined") {
                        $(".chatbox .chatbox-1").append(html);
                        $.scrollToBottom();
                    }
                    else {
                        $(".chatbox .chatbox-1 #load_earlier_messages").remove();
                        $(".chatbox .chatbox-1 .msg:first").before(html);
                    }
                }
                else if (elem.type == "more_messages") {
                    $(".chatbox .chatbox-1 .msg:first").before("<a id='load_earlier_messages'>Load Earlier Messages...</a>");
                }
                $("time.timeago").timeago();
            }
        }
    });
};
$(document).ready(function () {
    connect();

    $(document).on("click", "#load_earlier_messages", function () {
        ws.send("fetch", {"id": $(".msg:first").attr("id")});
    });

    $('#chatsend').click(function () { //use clicks message send button
        var chatsay_val = $('#chatsay').val(); //get message text
        if (chatsay_val != "") {
            ws.send("send", {"type": "text", "msg": chatsay_val});
            $('#chatsay').val(''); //reset text
        }
    });
});

Chat.php

<?php
namespace MyApp;

use DateTime;
use DateTimeZone;
use Exception;
use PDO;
use Ratchet\MessageComponentInterface;
use Ratchet\ConnectionInterface;

class Chat implements MessageComponentInterface
{
    protected $clients=array();
    private $dbh;

    /**
     * Chat constructor.
     */
    public function __construct()
    {
        global $db_host;
        global $db_username;
        global $db_password;
        global $db;
        require_once BASE_PATH.'modules'.DS.'Database'.DS.'Database.php';
        $database=new Database($db_host, $db, $db_username, $db_password);
        $this->dbh=$database;
    }

    /**
     * @param ConnectionInterface $conn
     */
    public function onOpen(ConnectionInterface $conn)
    {
        $this->clients[$conn->resourceId]=$conn;
        echo "New connection! ({$conn->resourceId})\n";
        $this->fetchMessages($conn);
    }

    /**
     * @param ConnectionInterface $conn
     * @param int /null $id
     */
    public function fetchMessages(ConnectionInterface $conn, $id=NULL)
    {
        $database=$this->dbh;

        if($id===NULL)
        {
            $database->query('SELECT * FROM `chat` ORDER BY `id` ASC', array());
            $msgs=$database->statement->fetchAll(PDO::FETCH_ASSOC);
            $msgCount=$database->count();

            if($msgCount>0)
            {
                # If more then 5 chat messages...
                if($msgCount>5)
                {
                    # Extract a slice of the array.
                    $msgs=array_slice($msgs, $msgCount-5, $msgCount);
                }

                foreach($msgs as $msg)
                {
                    $date=new DateTime($msg['posted']);
                    $date->setTimezone(new DateTimeZone(TIMEZONE));

                    $user=SearchUser($msg['user_id']);

                    $return=array(
                        "id"=>$msg['id'],
                        "name"=>$user['staffname'],
                        "type"=>$msg['type'],
                        "msg"=>$msg['msg'],
                        "posted"=>$date->format("Y-m-d H:i:sP")
                    );
                    $this->send($conn, "single", $return);
                }
                if($msgCount>5)
                {
                    $this->send($conn, "single", array(
                        "type"=>"more_messages"
                    ));
                }
            }
        }
        else
        {
            $database->query('SELECT * FROM `chat` WHERE `id` < :id ORDER BY `id` DESC LIMIT 10', array(':id'=>$id));
            $msgs=$database->statement->fetchAll(PDO::FETCH_ASSOC);
            $msgCount=$database->count();

            if($msgCount>0)
            {
                foreach($msgs as $msg)
                {
                    $date=new DateTime($msg['posted']);
                    $date->setTimezone(new DateTimeZone(TIMEZONE));

                    $user=SearchUser($msg['user_id']);

                    $return=array(
                        "id"=>$msg['id'],
                        "name"=>$user['staffname'],
                        "type"=>$msg['type'],
                        "msg"=>$msg['msg'],
                        "posted"=>$date->format("Y-m-d H:i:sP"),
                        "earlier_msg"=>TRUE
                    );
                    $this->send($conn, "single", $return);
                }

                sort($msgs);
                $firstID=$msgs[0]['id'];
                if($firstID!="1")
                {
                    $this->send($conn, "single", array(
                        "type"=>"more_messages"
                    ));
                }
            }
        }
    }

    /**
     * @param ConnectionInterface $client
     * @param $type
     * @param $data
     */
    public function send(ConnectionInterface $client, $type, $data)
    {
        $send=array(
            "type"=>$type,
            "data"=>$data
        );
        $send=json_encode($send, TRUE);
        $client->send($send);
    }

    /**
     * @param ConnectionInterface $conn
     * @param string $data
     */
    public function onMessage(ConnectionInterface $conn, $data)
    {
        $database=$this->dbh;

        $data=json_decode($data, TRUE);

        if(isset($data['data']) && count($data['data'])!=0)
        {
            $type=$data['type'];
            # How can I get the user's ID?
            $user_id=1;
            $user_name=SearchUser($user_id);

            $return=NULL;

            if($type=="send" && isset($data['data']['type']) && $user_name!=-1)
            {
                $msg=htmlspecialchars($data['data']['msg']);
                $date=new DateTime;
                $date->setTimezone(new DateTimeZone(TIMEZONE));

                if($data['data']['type']=='text')
                {
                    $database->query('SELECT `id`, `user_id`, `msg`, `type` FROM `chat` ORDER BY `id` DESC LIMIT 1', array());
                    $lastMsg=$database->statement->fetch(PDO::FETCH_OBJ);

                    if($lastMsg->user_id==$user_id && (strlen($lastMsg->msg)<=100 || strlen($lastMsg->msg)+strlen($msg)<=100))
                    {
                        # Append message.
                        $msg=$lastMsg->msg."<br/>".$msg;

                        $database->query('UPDATE `chat` SET `msg`=:msg, `posted`=NOW() WHERE `id`=:lastmsg', array(
                            ':msg'=>$msg,
                            ':lastmsg'=>$lastMsg->id
                        ));

                        $return=array(
                            "id"=>$lastMsg->id,
                            "name"=>$user_name['staffname'],
                            "type"=>"text",
                            "msg"=>$msg,
                            "posted"=>$date->format("Y-m-d H:i:sP"),
                            "append"=>TRUE
                        );
                    }
                    else
                    {
                        $database->query('INSERT INTO `chat` (`user_id`, `msg`, `type`, `posted`) VALUES (?, ?, "text", NOW())', array(
                            $user_id,
                            $msg
                        ));

                        # Get last insert ID.
                        $get_chat_id=$database->lastInsertId();
                        $return=array(
                            "id"=>$get_chat_id,
                            "name"=>$user_name['staffname'],
                            "type"=>"text",
                            "msg"=>$msg,
                            "posted"=>$date->format("Y-m-d H:i:sP")
                        );
                    }
                }

                foreach($this->clients as $client)
                {
                    $this->send($client, "single", $return);
                }
            }
            elseif($type=="fetch")
            {
                # Fetch previous messages.
                $this->fetchMessages($conn, $data['data']['id']);
            }
        }
    }

    /**
     * @param ConnectionInterface $conn
     */
    public function onClose(ConnectionInterface $conn)
    {
        # The connection is closed, remove it, as we can no longer send it messages.
        unset($this->clients[$conn->resourceId]);
        echo "Connection {$conn->resourceId} has disconnected\n";
    }

    /**
     * @param ConnectionInterface $conn
     * @param Exception $e
     */
    public function onError(ConnectionInterface $conn, Exception $e)
    {
        echo "An error has occurred: {$e->getMessage()}\n";
        $conn->close();
    }
}

start_server.php (start server via command line: php start_server.php)

<?php
# Need this for the database insert.
if(!defined('DOMAIN_NAME'))
{
    define('DOMAIN_NAME', 'domain.com');
}
require_once 'includes/config.php';
include_once BASE_PATH.'modules'.DS.'WS'.DS.'server.php';

server.php

<?php

use Ratchet\Server\IoServer;
use Ratchet\Http\HttpServer;
use Ratchet\WebSocket\WsServer;
use MyApp\Chat;

$ip="domain.com";
$port="8080";

# Need this for the database insert.
if(!defined('DOMAIN_NAME'))
{
    define('DOMAIN_NAME', $ip);
}

require_once '../../vendor/autoload.php';

$server = IoServer::factory(
    new HttpServer(
        new WsServer(
            new Chat()
        )
    ),
    $port,
    $ip
);

$server->run();

Here's my methodToGetSessionData() method as @Sherif suggested

/**
 * @param $sessionId
 * @return mixed
 */
private function methodToGetSessionData($sessionId)
{
    if(file_exists(session_save_path().'/sess_'.$sessionId))
    {
        $file=session_save_path().'/sess_'.$sessionId;
    }
    else
    {
        $file=sys_get_temp_dir().'/sess_'.$sessionId;
    }
    $contents=file_get_contents($file);
    session_decode($contents);

    return $_SESSION;
}

Solution

  • Just use the session like you're already doing. In your onOpen method the $conn has the initial HTTP request as a GuzzleHttp object. You can extract the session cookie from that and read in the session to your websocket server.

    public function onOpen(ConnectionInterface $conn)
    {
        // get the cookies
        $cookies = (string) $conn->WebSocket->request->getHeader('Cookie');
    
        // look at each cookie to find the one you expect
        $cookies = array_map('trim', explode(';', $cookies));
        $sessionId = null;
    
        foreach($cookies as $cookie) {
            // If the string is empty keep going
            if (!strlen($cookie)) {
                continue;
            }
            // Otherwise, let's get the cookie name and value
            list($cookieName, $cookieValue) = explode('=', $cookie, 2) + [null, null];
            // If either are empty, something went wrong, we'll fail silently here
            if (!strlen($cookieName) || !strlen($cookieValue)) {
                continue;
            }
            // If it's not the cookie we're looking for keep going
            if ($cookieName !== "PHPSESSID") {
                continue;
            }
            // If we've gotten this far we have the session id
            $sessionId = urldecode($cookieValue);
            break;
        }
    
        // If we got here and $sessionId is still null, then the user isn't logged in
        if (!$sessionId) {
            return $conn->close(); // close the connection - no session!
        }
    
        // Extract the session data using the session id from the cookie
        $conn->session = $this->methodToGetSessionData($sessionId);
    
        // now you have access to things in the session
        $this->clinets[] = $conn;
    }
    

    Depending on how you're storing session throughout your site with PHP you'll need to implement the methodToGetSessionData method to be able to read from that session store and deserialize the data. From there you'll have access to anything stored in the session through $conn->session or $client->session in your websocket server.

    Typically if you're using the default file based storage in PHP it's easy enough to just read the session file from your session.storage_path and unserialize it. I think Ratchet offers some session components for things like Redis/Memcached that you can easily inject into your websocket app as well.