【Python】AdminLTEとWebSocketでチャット機能を作ってみる その3(データ表示、登録)
おはようございます。
昨日に引き続きチャットの実装をしていきます。
今回は登録されているデータの表示と、メッセージ送信時のデータ登録など。
プログラムは前回のものを流用します。
【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">
ログアウト <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 については知識が浅く、正解が分かりませんが色々試してみます。
ではでは。
ディスカッション
コメント一覧
まだ、コメントがありません