【Python】AdminLTEとWebSocketでチャット機能を作ってみる その3(データ表示、登録)

公開日:2019-01-23
最終更新:2019-01-23
※この記事は外部サイト(https://www.doraxdora.com/blog/2018/10/07/...)からのクロス投稿です

引き続きチャットの実装をしていきます。
今回は登録されているデータの表示と、メッセージ送信時のデータ登録など。

プログラムは前回のものを流用します。
【Python】AdminLTEとWebSocketでチャット機能を作ってみる その2(ログイン機能)

下準備


新たにテーブルを追加して、データを登録しておきます。

テーブル

-- チャットメンバー  
create table TBL_CHATROOM_MEMBER (  
  ROOM_NO int(3) not null comment '部屋番号'  
  , USER_ID varchar(20) not null comment 'ユーザーID'  
  , CREATE_USER varchar(20) comment '作成者'  
  , CREATE_DATE datetime comment '作成日時'  
  , UPDATE_USER varchar(20) comment '更新者'  
  , UPDATE_DATE datetime comment '更新日時'  
  , constraint TBL_CHATROOM_MEMBER_PKC primary key (ROOM_NO,USER_ID)  
) comment 'チャットメンバー' ;  

-- ユーザー関係マスタ  
create table MST_USER_RELATION (  
  USER_ID varchar(20) not null comment 'ユーザーID'  
  , USER_ID2 varchar(20) not null comment 'ユーザーID2:ユーザーID'  
  , ROOM_NO int(3) comment '部屋番号'  
  , STATUS int(1) not null comment '状態'  
  , CREATE_USER varchar(20) comment '作成者'  
  , CREATE_DATE datetime comment '作成日時'  
  , UPDATE_USER varchar(20) comment '更新者'  
  , UPDATE_DATE datetime comment '更新日時'  
  , constraint MST_USER_RELATION_PKC primary key (USER_ID,USER_ID2)  
) comment 'ユーザー関係マスタ' ;  

-- チャットメッセージ  
create table TBL_CHATMESSAGE (  
  ROOM_NO int(3) not null comment '部屋番号'  
  , MESSAGE_NO int(10) not null comment 'メッセージ番号'  
  , USER_ID varchar(20) not null comment 'ユーザーID'  
  , MESSAGE varchar(1000) not null comment 'メッセージ内容'  
  , SEND_DATE datetime comment '送信日時'  
  , constraint TBL_CHATMESSAGE_PKC primary key (ROOM_NO,MESSAGE_NO)  
) comment 'チャットメッセージ' ;  

-- チャットルーム  
create table MST_ROOM (  
  ROOM_NO int(3) not null comment '部屋番号'  
  , ROOM_NAME varchar(30) not null comment '部屋名称'  
  , ICON varchar(20) comment 'ICON:画像ファイル名'  
  , LAST_VIEW_DATE datetime comment '最終参照日時'  
  , CREATE_USER varchar(20) comment '作成者'  
  , CREATE_DATE datetime comment '作成日時'  
  , UPDATE_USER varchar(20) comment '更新者'  
  , UPDATE_DATE datetime comment '更新日時'  
  , constraint MST_ROOM_PKC primary key (ROOM_NO)  
) comment 'チャットルーム' ;

データ

— MSTルーム
DELETE FROM MST_ROOM;
INSERT INTO MST_ROOM VALUES(‘1’,’個別’,NULL,NULL,’INIT’,NULL,NULL,NULL);
INSERT INTO MST_ROOM VALUES(‘2’,’グループ’,NULL,NULL,’INIT’,NULL,NULL,NULL);

— MSTユーザー関係
DELETE FROM MST_USER_RELATION;
INSERT INTO MST_USER_RELATION VALUES(‘001’,’002’,’0’,’1’,’INIT’,NULL,NULL,NULL);
INSERT INTO MST_USER_RELATION VALUES(‘001’,’003’,’0’,’1’,’INIT’,NULL,NULL,NULL);
INSERT INTO MST_USER_RELATION VALUES(‘001’,’004’,’0’,’1’,’INIT’,NULL,NULL,NULL);
INSERT INTO MST_USER_RELATION VALUES(‘001’,’005’,’1’,’1’,’INIT’,NULL,NULL,NULL);

— MSTチャットメンバー
DELETE FROM TBL_CHATROOM_MEMBER;
INSERT INTO TBL_CHATROOM_MEMBER VALUES(‘1’,’001’,’INIT’,NULL,NULL,NULL);
INSERT INTO TBL_CHATROOM_MEMBER VALUES(‘1’,’005’,’INIT’,NULL,NULL,NULL);
INSERT INTO TBL_CHATROOM_MEMBER VALUES(‘2’,’001’,’INIT’,NULL,NULL,NULL);
INSERT INTO TBL_CHATROOM_MEMBER VALUES(‘2’,’002’,’INIT’,NULL,NULL,NULL);
INSERT INTO TBL_CHATROOM_MEMBER VALUES(‘2’,’003’,’INIT’,NULL,NULL,NULL);
INSERT INTO TBL_CHATROOM_MEMBER VALUES(‘2’,’004’,’INIT’,NULL,NULL,NULL);

— メッセージ
DELETE FROM TBL_CHATMESSAGE;
INSERT INTO TBL_CHATMESSAGE VALUES(‘1’,’0’,’005’,’そら最近どうしてる?’,’2018-09-25 02:00:00’);
INSERT INTO TBL_CHATMESSAGE VALUES(‘1’,’1’,’001’,’相変わらずだよ。\nあいつらの面倒で手一杯でさ。’,’2018-09-25 02:05:00’);
INSERT INTO TBL_CHATMESSAGE VALUES(‘1’,’2’,’005’,’一番のお兄さんだから大変ね。\n私は一人で快適な暮らしを送っているわ(^^♪’,’2018-09-25 05:37:00’);
INSERT INTO TBL_CHATMESSAGE VALUES(‘1’,’3’,’001’,’え、なにそれ自慢ですか?’,’2018-09-25 06:10:00’);

画面の修正


コンタクトリスト、メッセージなどをDBから取得して表示するように修正。

main.html

<!DOCTYPE html>  
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="ja" lang="ja">  
<head>  
    <meta charset="utf-8">  
    <meta http-equiv="X-UA-Compatible" content="IE=edge">  
    <meta http-equiv="content-type" content="text/html; charset=UTF-8">  
    <title>チャットサンプル</title>  
    <meta content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" name="viewport">  
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css">  
    <link rel="stylesheet" href="https:////maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css">  
    <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Source+Sans+Pro:300,400,600,700,300italic,400italic,600italic">  
    <link rel="stylesheet" href="{{ static_url('css/AdminLTE.min.css') }}">  
    <link rel="stylesheet" href="{{ static_url('css/style.css') }}">  
    <link rel="stylesheet" href="{{ static_url('css/skins/skin-blue.min.css') }}">  
</head>  
<body class="hold-transition fixed">  
    <form id="logoutForm" action="/logout" method="get">  
        {% module xsrf_form_html() %}  
    </form>  
    <nav class="navbar navbar-default">  
        <div class="container-fluid">  
            <!-- ヘッダ情報 -->  
            <div class="navbar-header" style="padding:15px;">  
                <input type="hidden" id="user_id" value="{{ user_id }}" />  
                <input type="hidden" id="user_name" value="{{ user_name }}" />  
                ユーザー名:{{ user_name }}  
            </div>  
            <!-- リストの配置 -->  
            <ul class="nav navbar-nav">  
                <li class="active"><a href="#">チャット</a></li>  
                <li><a href="#">メニュー1</a></li>  
                <li><a href="#">メニュー2</a></li>  
            </ul>  
            <!-- ボタン -->  
            <button type="button" class="pull-right btn btn-default navbar-btn" onclick="logout();">  
                ログアウト&amp;nbsp;<span class="fa fa-sign-out"></span>  
            </button>  
        </div>  
    </nav>  
    <section class="content container-fluid">  
        <div class="row">  
            <div class="col-xs-8">  
                <div class="row">  
                    <div class="col-xs-8">  
                        <div id="chat-panel" class="box box-warning direct-chat direct-chat-warning box-solid" style="display:none;">  
                            <input type="hidden" id="room_no" value="{{ room_no }}" />  
                            <div class="box-header with-border">  
                                <h3 class="box-title">チャットメッセージ</h3>  
                                <div class="box-tools pull-right">  
                                    <span id="status" class="status"></span>  
                                    <span data-toggle="tooltip" title="3 New Messages" class="badge bg-yellow">3</span>  
                                    <button type="button" class="btn btn-box-tool" data-widget="collapse"><i class="fa fa-minus"></i></button>  
                                    <button type="button" class="btn btn-box-tool" data-toggle="tooltip" title="Contacts" data-widget="chat-pane-toggle">  
                                        <i class="fa fa-comments"></i>  
                                    </button>  
                                    <button type="button" class="btn btn-box-tool" data-widget="remove"><i class="fa fa-times"></i></button>  
                                </div>  
                            </div>  
                            <div class="box-body">  
                                <div class="direct-chat-messages">  
                                    {% for msg in chat_messages %}  
                                        {% if msg['user_id'] == user_id %}  
                                            <div class="direct-chat-msg right">  
                                                <div class="direct-chat-info clearfix">  
                                                    <span class="direct-chat-name pull-right">{{ msg['user_name'] }}</span>  
                                                    <span class="direct-chat-timestamp pull-left">{{ msg['send_date'] }}</span>  
                                                </div>  
                                                <img class="direct-chat-img" src="static/img/{{ msg['icon'] }}" alt="message user image">  
                                                <div class="direct-chat-text">  
                                                    {% autoescape None %}  
                                                    {{ msg['message'] }}  
                                                </div>  
                                            </div>  
                                        {% else %}  
                                            <div class="direct-chat-msg">  
                                                <div class="direct-chat-info clearfix">  
                                                    <span class="direct-chat-name pull-left">{{ msg['user_name'] }}</span>  
                                                    <span class="direct-chat-timestamp pull-right">{{ msg['send_date'] }}</span>  
                                                </div>  
                                                <img class="direct-chat-img" src="static/img/{{ msg['icon'] }}" alt="message user image">  
                                                <div class="direct-chat-text">  
                                                    {{ msg['message'] }}  
                                                </div>  
                                            </div>  
                                        {% end %}  
                                    {% end %}  
                                </div>  
                                <div class="direct-chat-contacts">  
                                    <ul class="contacts-list">  
                                    {% for user in relation_users %}  
                                        <li>  
                                            <a href="#">  
                                                <img class="contacts-list-img" src="static/img/{{ user['icon'] }}" alt="User Image">  
                                                <div class="contacts-list-info">  
                                                    <span class="contacts-list-name">  
                                                        {{ user['user_name'] }}  
                                                        <small class="contacts-list-date pull-right">{{ user['update_date'] }}</small>  
                                                    </span>  
                                                    <span class="contacts-list-msg">{{ user['message'] }}</span>  
                                                </div>  
                                            </a>  
                                        </li>  
                                    {% end %}  
                                    </ul>  
                                </div>  
                            </div>  
                            <div class="box-footer">  
                                <form action="#" method="post">  
                                    <div class="input-group">  
                                        <textarea id="message" type="text" name="message" placeholder="メッセージを入力してください" class="form-control"></textarea>  
                                        <span class="input-group-btn">  
                                            <button id="sendButton" type="button" class="btn btn-warning btn-flat btn-sm">送信</button>  
                                        </span>  
                                    </div>  
                                </form>  
                            </div>  
                        </div>  
                    </div>  
                </div>  
            </div>  
        </div>  
    </section>  
    <script src="//ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js"></script>  
    <script src="//ajax.googleapis.com/ajax/libs/jqueryui/1.8.19/jquery-ui.min.js"></script>  
    <script src="//maxcdn.bootstrapcdn.com/bootstrap/3.2.0/js/bootstrap.min.js"></script>  
    <script src="//cdnjs.cloudflare.com/ajax/libs/moment.js/2.10.6/moment.js"></script>  
    <script src="{{ static_url('js/adminlte.min.js') }}"></script>  
    <script src="{{ static_url('js/script.js') }}"></script>  
    <script>  
        $(document).ready( function () {  
            initialize();  
        } );  
    </script>  
    </body>  
</html>

プログラム

新規追加


チャットメッセージ用のクラスを追加

Dao/TblChatMessageDao.py

from Dao.SqlClient import SqlClient  



class TblChatMessageDao(SqlClient):  
    """  
    TBLチャットメッセージDAOクラス  
    """  

    def select_message_by_room_no(self, room_no):  
        """  
        チャットメッセージを取得します  
        :param room_no:  
        :return:  
        """  

        sql = "SELECT"  
        sql += "  U.USER_NAME"  
        sql += "  , U.ICON"  
        sql += "  , M.* "  
        sql += "FROM"  
        sql += "  TBL_CHATMESSAGE M"  
        sql += "  LEFT OUTER JOIN MST_USER U ON ("  
        sql += "      M.USER_ID = U.USER_ID"  
        sql += "  )"  
        sql += "WHERE"  
        sql += "  ROOM_NO = %s "  
        sql += "ORDER BY"  
        sql += "  SEND_DATE"  

        result = []  
        data = super().select(sql, [room_no])  
        for r in data:  
            result.append(self.mapping_data(r))  

        return result  

    def select_next_message_no(self, room_no):  
        """  
        部屋番号毎の次のメッセージ番号を取得します  
        :param room_no:  
        :return:  
        """  

        sql = "SELECT MAX(MESSAGE_NO) +1 AS MESSAGE_NO FROM TBL_CHATMESSAGE WHERE ROOM_NO = %s"  

        data = super().select_one(sql, [room_no])  
        return data["MESSAGE_NO"]  

    def insert_message(self, data):  
        """  
        データを登録します  
        :param data:  
        :return:  
        """  
        message_no = self.select_next_message_no(data["room_no"])  
        record = [  
            data["room_no"]  
            , message_no  
            , data["user_id"]  
            , data["message"]  
            , data["send_date"]  
        ]  
        sql = "INSERT INTO TBL_CHATMESSAGE VALUES (%s,%s,%s,%s,%s)"  
        super().execute(sql, record)  

    def mapping_data(self, record):  
        """  
        レコードをマッピングします  
        :param record:  
        :return:  
        """  
        dic = dict()  
        dic['room_no'] = record.get('ROOM_NO')  
        dic['message_no'] = record.get('MESSAGE_NO')  
        dic['user_id'] = record.get('USER_ID')  
        dic['message'] = self.escape_newline(record.get('MESSAGE'))  
        dic['send_date'] = self.parse_date(record.get('SEND_DATE'))  
        dic['user_name'] = record.get('USER_NAME')  
        dic['icon'] = record.get('ICON')  

        return dic

既存クラスの修正


コンタクトリストを取得するように修正

Dao/MstUserDao.py

from Dao.SqlClient import SqlClient  


class MstUserDao(SqlClient):  
    """  
    MSTユーザーDAOクラス  
    """  

    def select_user(self, user_id):  
        """  
        ユーザIDを指定してデータを取得します  
        :param user_id:  
        :return:  
        """  
        sql = "SELECT * FROM MST_USER WHERE USER_ID = %s"  

        data = super().select(sql, [user_id])  
        if len(data) > 0:  
            return self.mapping_data(data[0])  

        return None  

    def select_relation_user(self, user_id):  
        """  
        関係ユーザーを取得します  
        :param user_id:  
        :return:  
        """  

        sql = "SELECT"  
        sql += "  R.ROOM_NO"  
        sql += "  , M.ROOM_NAME"  
        sql += "  , U.* "  
        sql += "FROM"  
        sql += "  MST_USER_RELATION R"  
        sql += "  LEFT OUTER JOIN MST_ROOM M ON ("  
        sql += "      M.ROOM_NO = R.ROOM_NO"  
        sql += "  )"  
        sql += "  LEFT OUTER JOIN MST_USER U ON ("  
        sql += "      U.USER_ID = R.USER_ID2"  
        sql += "  ) "  
        sql += "WHERE"  
        sql += "  R.USER_ID = %s"  

        result = []  
        data = super().select(sql, [user_id])  
        for r in data:  
            result.append(self.mapping_data(r))  

        return result  

    def mapping_data(self, record):  
        """  
        レコードをマッピングします  
        :param record:  
        :return:  
        """  
        dic = dict()  
        dic['user_id'] = record.get('USER_ID')  
        dic['user_name'] = record.get('USER_NAME')  
        dic['icon'] = record.get('ICON')  
        dic['message'] = record.get('MESSAGE')  
        dic['create_user'] = record.get('CREATE_USER')  
        dic['create_date'] = self.parse_date(record.get('CREATE_DATE'))  
        dic['update_user'] = record.get('UPDATE_USER')  
        dic['update_date'] = self.parse_date(record.get('UPDATE_DATE'))  
        dic['room_no'] = record.get('ROOM_NO')  
        dic['room_name'] = record.get('ROOM_NAME')  

        return dic

 

メイン、WEBソケットクラスの修正

Main.py

class MainHandler(AuthBaseHandler):  
    """  
    初期表示処理  
    """  

    def initialize(self):  
        logging.info("[MainHandler] initialize")  

    @tornado.web.authenticated  
    def get(self):  
        logging.info("[MainHandler] get")  
        user_id = self.get_current_user()  
        user_dao = MstUserDao()  
        user = user_dao.select_user(user_id)  
        user_name = user['user_name']  

        relation_users = user_dao.select_relation_user(user_id)  

        message_dao = TblChatMessageDao()  
        chat_messages = message_dao.select_message_by_room_no(1)  

        self.render("main.html",  
                    user_id=user_id,  
                    user_name=user_name,  
                    room_no=1,  
                    relation_users=relation_users,  
                    chat_messages=chat_messages)  


class ChatHandler(WebSocketHandler):  
    """  
    チャット処理  
    """  

    def open(self):  
        logging.info("[ChatHandler] open")  

        if self not in client:  
            client.append(self)  

    def on_message(self, message):  
        logging.info("[ChatHandler] on_message : " + message)  

        # データ登録  
        data = json.loads(message)  
        message_dao = TblChatMessageDao()  
        message_dao.insert_message(data)  

        # ユーザー取得  
        user_dao = MstUserDao()  
        result = user_dao.select_user(data["user_id"])  

        # ユーザ情報にメッセージをチャット情報を追加  
        result["room_no"] = data["room_no"]  
        result["message"] = data["message"]  
        result["send_date"] = data["send_date"]  

        for cl in client:  
            cl.write_message(json.dumps(result, ensure_ascii=False))  

    def on_close(self):  
        logging.info("[ChatHandler] on_close")  

        if self in client:  
            client.remove(self)

 

クライアント側の処理も修正。

script.js

// ソケット  
var socket = new WebSocket('ws://' + location.host + '/chat');  
moment.lang('ja', {  
    weekdays: ["日曜日","月曜日","火曜日","水曜日","木曜日","金曜日","土曜日"],  
    weekdaysShort: ["日","月","火","水","木","金","土"],  
});  

/**  
 * ログアウト処理.  
 */  
function logout() {  
    $("#logoutForm").submit();  
}  

/**  
 * 初期処理.  
 */  
function initialize() {  

    // 通信ソケットオープン  
    socket.onopen = function(data) {  
        $("#status").css("color", "#FFFFFF");  
        $("#status").text(" [オンライン]");  
    }  

    // 通信ソケットクローズ  
    socket.onclose = function() {  
        $("#status").css("color", "#999999");  
        $("#status").text(" [オフライン]")  
    }  

    // メッセージ受信  
    socket.onmessage = function(e) {  
        console.log(e.data);  
        var data = JSON.parse(e.data);  

        $(".direct-chat-messages").append(createMessage(data));  
        $(".direct-chat-messages").animate({  
            scrollTop: $(".direct-chat-messages")[0].scrollHeight  
        }, 500);  
    }  

    // ボタンにイベントを追加  
    $("#sendButton").click(function () {  
        sendMessage();  
    });  

    // チャットの表示を一番下に  
    $("#chat-panel").show();  
    $(".direct-chat-messages")[0].scrollTop = $(".direct-chat-messages")[0].scrollHeight;  
}  

/**  
 * メッセージを送信.  
 */  
function sendMessage() {  

    // メッセージが未入力の場合は送信しない  
    if(!$("#message").val()) {  
        return;  
    }  

    // 送信日時  
    var now = new moment();  
    var send_date = now.format("YYYY-MM-DD HHss")  
    // パラメータ  
    var param = {  
        "user_id" : $("#user_id").val()  
        , "room_no" : $("#room_no").val()  
        , "message" : $("#message").val()  
        , "send_date" : send_date  
    }  

    socket.send(JSON.stringify(param));  
}  

/**  
 * メッセージタグを作成して返します.  
 */  
function createMessage(data) {  

    // 本人かどうかを判定  
    var isSelf = $("#user_id").val() == data.user_id;  

    var msgDiv = $("<div>", {  
        "class" : "direct-chat-text"  
        , "html" : data.message.replace(/\n/g, "<BR>")  
    });  

    var img = $("<img>", {  
        "class" : "direct-chat-img"  
        , "src" : "static/img/" + data.icon  
    });  

    var clazz = "pull-right";  
    if (isSelf) {  
        clazz = "pull-left";  
    }  
    var dt = $("<span>", {  
        "class" : "direct-chat-timestamp " + clazz  
        , "text" : moment(data.send_date, "YYYY-MM-DD HHss").format("YYYY/MM/DD(ddd) HH:mm")  
    });  

    var clazz = "pull-left";  
    if (isSelf) {  
        clazz = "pull-right";  
    }  
    var name = $("<span>", {  
        "class" : "direct-chat-name " + clazz  
        , "text" : data.user_name  
    });  

    var info = $("<div>", {  
        "class" : "direct-chat-info clearfix"  
    });  

    var clazz = "";  
    if (isSelf) {  
        clazz = "right";  
    }  
    var chatMsg = $("<div>", {  
        "class" : "direct-chat-msg " + clazz  
    });  

    // タグを作成していく  
    info.append(name);  
    info.append(dt);  

    chatMsg.append(info);  
    chatMsg.append(img);  
    chatMsg.append(msgDiv);  

    return chatMsg;  
}

起動してみる


別々のユーザで(別セッション)ログインし、それぞれでメッセージを送信してみました。

まとめ


なんとなく出来上がってきた感じがしますね。

現状は、固定のチャットルームとしていますが、
次回はそこらへんの実装を進めていきたいと思います。

メッセージの配信部分については、
ソケット通信に接続しているセッション全てに配信して
クライアント側で処理を振り分けるか、サーバー側で配信先を振り分けるか悩んでいます。

まだまだ WebSocket については知識が浅く、正解が分かりませんが色々試してみます。

ではでは。

 

記事が少しでもいいなと思ったらクラップを送ってみよう!
0
+1
@doraxdoraの技術ブログ 主に Java, C#, Python, Javascript の記事を載せていく予定。

よく一緒に読まれている記事

0件のコメント

ブログ開設 or ログイン してコメントを送ってみよう
目次をみる

技術ブログをはじめよう

Qrunch(クランチ)は、ITエンジニアリングに携わる全ての人のための技術ブログプラットフォームです。

技術ブログを開設する

Qrunchでアウトプットをはじめよう

Qrunch(クランチ)は、ITエンジニアリングに携わる全ての人のための技術ブログプラットフォームです。

Markdownで書ける

ログ機能でアウトプットを加速

デザインのカスタマイズが可能

技術ブログ開設

ここから先はアカウント(ブログ)開設が必要です

英数字4文字以上
.qrunch.io
英数字6文字以上
ログインする