2
\$\begingroup\$

I have been worked on creating a chat tool similar to vk.com. Right now I have done a simple chat which requires only authentication via a PHP website and everyone can chat with each other. I used the NodeSession PHP class to access the PHP session in the node.js application.

To implement the application I used Yii1, node.js and Redis.

The following is my code base where I tried to explain and cover all aspects.

/var/www/html/whiz/protected/controllers/frontend/ChatController.php

<?php

class ChatController extends FrontendController {

    public function actionIndex() {
        $arr = array();
        $repo = new UserRepository();
        $friends = $repo->fetchFriendIds(Yii::app()->session['user_id'], "array");
        $arrOr = $arr = array();
        foreach ($friends as $friend) {
            array_push($arrOr, "id = {$friend}");
        }
        $orCondition = implode(" or ", $arrOr);
        echo $orCondition;

        $friends = User::model()->findAll(array("select" => "id, reference_id, nick_name, email, is_online, is_active", "condition" => "`is_active` = 1 and ({$orCondition})"));

        $chatLog = $this->redis->LRANGE("whizchannel", 0, 99);
        $arr = array("msg" => uniqid() . " is connecting...");
        $this->redis->publish("whizchannel", json_encode($arr));
        $this->render("index", array("chatlog" => $chatLog, "friends" => $friends));
    }

}

/var/www/html/whiz/nodejs/server.js

/*
 * -------------------
 * Express
 * -------------------
 */
var app = require('express')(),
        http = require("http"),
        url = require('url'),
        cookieParser = require('cookie-parser'), // the session is stored in a cookie, so we use this to parse it
        session = require('nodePhpSessions').SessionHandler,
        sessionHandler = new session(),
        morgan = require("morgan"),
        expressSession = require("express-session"),
        phpUnserialize = require("php-unserialize"),
        sessionStore = new expressSession.MemoryStore(),
        parseUrl = null, uId = null, uReferenceId = null, uName = null, uEmail = null,
        onlineId = [], onlineUser = [];

// Transaction logger
app.use(morgan("dev"));

// must use cookieParser before expressxSession
app.use(cookieParser());
app.use(expressSession({
    name: "Whizchat",
    secret: '47760ae7-9660-4d4c-b15d-b9986edccbf3ss',
    store: sessionStore,
    saveUninitialized: true,
    resave: true,
    cookie: {
        path: "/",
        httpOnly: true,
        secure: true,
        maxAge: null,
    }
}));

//app.set(app.session({secret: 'secret', key: 'express.sid'}));
app.use(function (req, res, next) {
    res.header("Access-Control-Allow-Origin", "*");
    res.header("Access-Control-Allow-Headers", "X-Requested-With");
    res.header("Access-Control-Allow-Headers", "Content-Type");
    res.header("Access-Control-Allow-Methods", "PUT, GET, POST, DELETE, OPTIONS");
    next();
});
app.set('host', "localhost");
//var port = normalizePort(process.env.PORT || '3000');
//app.set('port', port);
app.set('port', "3000"); //process.env.PORT

/*
 * -------------------
 * Socket Connections
 * -------------------
 */
//var server = require('http').createServer(app);
var server = http.createServer(function (req, res) {
    parseUrl = url.parse(req.url, true).query;
    //console.log(JSON.stringify(parseUrl.dataJSON));
    //console.log(typeof (parseUrl.dataJSON));
    if ("string" === typeof (parseUrl.dataJSON)) {
        var _objUser = JSON.parse(parseUrl.dataJSON);
        uId = (_objUser.user_id === undefined || _objUser.user_id === "" || _objUser.user_id === null) ? null : _objUser.user_id;
        uReferenceId = (_objUser.user_reference_id === undefined || _objUser.user_reference_id === "" || _objUser.user_reference_id === null) ? null : _objUser.user_reference_id;
        uName = (_objUser.username === undefined || _objUser.username === "" || _objUser.username === null) ? null : _objUser.username;
        uEmail = (_objUser.user_email === undefined || _objUser.user_email === "" || _objUser.user_email === null) ? null : _objUser.user_email;
    }
    res.writeHead(200, {'Content-type': "text/plain"});
    res.end(sessionHandler.run(parseUrl));
});

var io = require('socket.io')(server);
server.listen(app.get("port"), app.get("host"), function () {
    console.log("Server up and running...");
});

server.on('error', function (e) {
    console.log("Error occured :" + e);
});
io.set('log level', 3);

/*
 * -------------------
 * Redis 
 * -------------------
 */
var redis = require("redis");
/*
 *  Sender
 */
var publisher = redis.createClient();
/*
 * Receiver
 */
var subscriber = redis.createClient();


/*
 * -------------------
 * functions
 * -------------------
 */
function subscribeMessage(_subscriber) {
    subscriber.subscribe(_subscriber);
    subscriber.on("message", function (channel, message) {
        console.log("redis connection message:" + message);
    });
}
function pushToRedis(subscriber, data) {
    console.log("push to channel");
    subscriber.lpush("whizchatchannel", JSON.stringify(data));
    subscriber.ltrim('messages', 0, 99);
}
function getCurrentTime() {
    return Math.floor(new Date().valueOf() / 1000);
}
function accessSecureSessionInfo() {
    console.log("type:" + typeof (parseUrl.dataJSON));
    console.log("Datajosn:" + parseUrl.dataJSON);

    if ("string" === typeof (parseUrl.dataJSON)) {
        console.log("step in 1");
        var _objUser = JSON.parse(parseUrl.dataJSON);
        console.log("usrname :" + _objUser.username);
        uId = (_objUser.user_id === undefined || _objUser.user_id === "" || _objUser.user_id === null) ? null : _objUser.user_id;
        uReferenceId = (_objUser.user_reference_id === undefined || _objUser.user_reference_id === "" || _objUser.user_reference_id === null) ? null : _objUser.user_reference_id;
        uName = (_objUser.username === undefined || _objUser.username === "" || _objUser.username === null) ? null : _objUser.username;
        uEmail = (_objUser.user_email === undefined || _objUser.user_email === "" || _objUser.user_email === null) ? null : _objUser.user_email;
    }
    console.log("Uname:" + uName);
}
function setUniqueArray() {
    onlineUser.push({id: uId, name: uName});
}

/*
 * -------------------
 * Redis Subscriber
 * -------------------
 */
if (uId && uName && uEmail) {
    subscriber.subscribe("whizchatchannel");
    subscribeMessage("whizchatchannel");
}
io.sockets.on('connection', function (socket) {
    if (uId !== null && uReferenceId !== null && uName !== null && uEmail !== null) {
        if (onlineId.indexOf(uId) === -1) {
            onlineId.push(uId);
            setUniqueArray();
        }
        console.log(JSON.stringify(onlineUser));

        var _subscriber = redis.createClient();
        socket.on('connection', function (data) {
            console.log('Server running on *:' + app.get('port'));
        });
        socket.on("chat", function (data) {
            if (data.msg !== null && data.msg !== undefined && data.msg !== "") {
                var _data = {
                    uid: uId,
                    uReferenceId: uReferenceId,
                    uName: uName,
                    uMsg: data.msg,
                    uDate: getCurrentTime()
                };
                pushToRedis(_subscriber, _data);
                socket.emit("publishMessage", _data);
                socket.broadcast.emit("publishMessage", _data);
            }
        });
//    socket.onclose = function () {
//        console.info('Socket is now closed.');
//    }
        socket.on('disconnect', function () {
        });
    }
});

/var/www/html/whiz/protected/views/frontend/layouts/_js.php

<script src="<?php echo Yii::app()->request->baseUrl . "/js/socket.io.js"; ?>"></script>
<script language="javascript">
    $(function () {

        var uRefId = "<?php echo Yii::app()->session['user_reference_id']; ?>"
        var socket = io.connect("http://localhost:3000/");

        /*
         * -------------------
         * Methods
         * -------------------
         */
        $.fn.clearAndFocus = function () {
            $("#txtMessage").val("").focus();
        };
        $.fn.sendMessageToServer = function () {
            socket.emit("chat", {msg: $("#txtMessage").val()});
            $.fn.clearAndFocus();
        };
        $.fn.clearAndFocus();
        /*
         * -------------------
         * Events
         * -------------------
         */
        $("#btnSend").on("click", function () {
            $.fn.sendMessageToServer();
        });
        $("#txtMessage").on("keyup", function (e) {
            if (e.keyCode === 13) {
                $.fn.sendMessageToServer();
                $.fn.clearAndFocus();
            }
        });
        /*
         * -------------------
         * Socket Events
         * -------------------
         */
        socket.on('connect', function () {
            console.log("w client is connected");
        });
        socket.on("publishMessage", function (data) {
            console.log("uRefId:" + uRefId);
            console.log("dat uRefId:" + data.uReferenceId);
            var isMe = (uRefId === data.uReferenceId) ? true : false;
            console.log("me:" + isMe);

            var cls = (true === isMe) ? "name-me" : "name-other";
            var uName = (true === isMe) ? "Me" : data.uName;
            $(".chatlog").append("\n\
                            <div class=\"chat-row\">\n\
                            <div class=\"" + cls + "\">" + uName + "</div>\n\
                            <div class=\"chat-text\">" + data.uMsg + "\n\
                            </div>");
        });
        socket.on('disconnect', function () {
        });
    });
</script>

/var/www/html/whizchat/protected/views/frontend/chat/index.php

<?php
//Check User Settings Permission.
// Pending to validate
?>
<section id="ccr-main-section">
    <div class="container">
        <?php echo $this->renderPartial("/userProfile/_featureIconsProfile", $this->data); ?>
        <div class="left_right">
            <?php echo $this->renderPartial("/userProfile/_left_userprofile", $this->data); ?>
            <!--Start Right -->
            <div class="contain_area_right">
                <div class="contain_area_right_title">
                    <div class="comment_part border_none">                        
                        <div class="comment_find">
                            <div class="comment_search">
                                <div class="comment_search_input width-94">
                                    <input type="text" id="txtFriends" name="txtFriends"  class="commentarea"/>
                                </div>
                                <div class="chatlog">
                                    <?php
                                    if (count($chatlog) > 0) {
                                        krsort($chatlog);
                                        foreach ($chatlog as $chat) {
                                            $arr = json_decode($chat);
                                            $isMe = ($arr->uReferenceId == Yii::app()->session['user_reference_id']) ? true : false;
                                            //"name-me" : "name-other";
                                            ?>
                                            <div class="chat-row">
                                                <div class = "<?php echo ($isMe) ? "name-me" : "name-other"; ?>"><?php echo ($isMe) ? "Me" : ucfirst($arr->uName); ?></div>
                                                <div class = "chat-text"><?php echo $arr->uMsg; ?></div>
                                            </div>
                                            <?php
                                        }
                                    }
                                    ?>
                                </div>
                                <div class="comment_search_input width-94">                                   
                                    <input type="text" id="txtMessage" name="txtMessage"  class="commentarea"/>
                                </div>
                                <div class="comment_search_button">
                                    <input id="btnSend" name="btnSend" class="submit-btn button " type="button" title="Search" value="Send" />
                                </div>
                            </div>
                        </div>
                        <div class="clear"></div>
                    </div>
                </div>
            </div>
            <!--End Right -->
        </div>
    </div>
    <div class="clear"></div>
</div>
</section>

/var/www/html/whizchat/protected/vendor/NodeSession.php

<?php

class NodeSession {

    const NODE_DEF_HOST = 'localhost';
    const NODE_DEF_PORT = 3000;

    static function start($host = self::NODE_DEF_HOST, $port = self::NODE_DEF_PORT) {
        $obj = new self($host, $port);
        session_set_save_handler(
                array($obj, "open"), array($obj, "close"), array($obj, "read"), array($obj, "write"), array($obj, "destroy"), array($obj, "gc"));
        @session_start();
        return $obj;
    }

    private function unserializeSession($data) {
        if (strlen($data) == 0) {
            return array();
        }

// match all the session keys and offsets
        preg_match_all('/(^|;|\})([a-zA-Z0-9_]+)\|/i', $data, $matchesarray, PREG_OFFSET_CAPTURE);
        $returnArray = array();

        $lastOffset = null;
        $currentKey = '';
        foreach ($matchesarray[2] as $value) {
            $offset = $value[1];
            if (!is_null($lastOffset)) {
                $valueText = substr($data, $lastOffset, $offset - $lastOffset);
                $returnArray[$currentKey] = unserialize($valueText);
            }
            $currentKey = $value[0];

            $lastOffset = $offset + strlen($currentKey) + 1;
        }

        $valueText = substr($data, $lastOffset);
        $returnArray[$currentKey] = unserialize($valueText);

        return $returnArray;
    }

    function __construct($host = self::NODE_DEF_HOST, $port = self::NODE_DEF_PORT) {
        $this->_host = $host;
        $this->_port = $port;
    }

    function open($save_path, $session_name) {
        return true;
    }

    function close() {
        return true;
    }

    public function read($id) {
        return (string) $this->send(__FUNCTION__, array('id' => $id));
    }

    public function write($id, $data) {
        try {
            $this->send(__FUNCTION__, array(
                'id' => $id,
                'data' => $data,
                'time' => time(),
                'dataJSON' => json_encode($this->unserializeSession($data))));
            return true;
        } catch (Exception $e) {
            return false;
        }
    }

    public function destroy($id) {
        try {
            $this->send(__FUNCTION__, array('id' => $id));
        } catch (Exception $e) {
            return false;
        }
        return true;
    }

    function gc($maxlifetime) {
        try {
            $this->send(__FUNCTION__, array('maxlifetime' => $maxlifetime, 'time' => time()));
        } catch (Exception $e) {
            return false;
        }
        return true;
    }

    private function send($action, $params) {
        $params = array('action' => $action) + $params;       
        return file_get_contents("http://{$this->_host}:{$this->_port}?" . http_build_query($params));
    }

}
?>

/var/www/html/whizchat/protected/components/FrontendController.php

<?php

/**
 * Controller is the customized base controller class.
 * All controller classes for this application should extend from this base class.
 */
class FrontendController extends CController {

    /**
     * @var string the default layout for the controller view. Defaults to '//layouts/column1',
     * meaning using a single column layout. See 'protected/views/layouts/column1.php'.
     */
    public $layout = '//layouts/main';

    /**
     * @var array context menu items. This property will be assigned to {@link CMenu::items}.
     */
    public $menu = array();
    public $data = array();

    /**
     * @var array the breadcrumbs of the current page. The value of this property will
     * be assigned to {@link CBreadcrumbs::links}. Please refer to {@link CBreadcrumbs::links}
     * for more details on how to specify this property.
     */
    public $breadcrumbs = array();
    public $app;
    public $redis;

    public function init() {


        /*
         * Node session
         */
        require_once(__DIR__ . "/../vendor/NodeSession.php");
        NodeSession::start();

        /*
         * Initialize redis server
         */
        require_once(__DIR__ . "/../vendor/predis/autoload.php");
        Predis\Autoloader::register();
        $this->redis = new Predis\Client(array(
            "scheme" => "tcp",
            "host" => "localhost",
            "port" => 6379));
        $this->redis->publish("init_channel", "Connected to redis...");
    }
}
?>

I know that there is lots of room for improvements. I would like to get suggestions and fixes for some aspects.

  • I am able to access the PHP session in node.js but I don't know if this is really the proper way to do it. So please provide me with an alternate suggestion if any. If not, then is there scope of improvement in code base of server.js about to PHP session access?

  • I am right now creating a simple list in Redis, but can anyone suggest a better way to manage the chat log? Right now I am simply loading the complete list but I would like to validate that chat made possible among friends only. Anonymous user can't be allowed to chat with other like all other social media can do.

  • Provide me with suggestions of improvements in the code base.

I had referred Redis very well but am still confused about creating a chat-log database.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.