【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 については知識が浅く、正解が分かりませんが色々試してみます。
ではでは。
ディスカッション
コメント一覧
まだ、コメントがありません