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

2018年10月7日Python,開発

おはようございます。

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

プログラムは前回のものを流用します。
【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">
				ログアウト&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 HH:mm:ss")
    // パラメータ
    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 HH:mm:ss").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 については知識が浅く、正解が分かりませんが色々試してみます。

ではでは。

 

スポンサーリンク


関連するコンテンツ

2018年10月7日Python,開発AdminLTE,CSSフレームワーク,Python,Tornado,WebSocket,プログラミング

Posted by doradora