【Python】AdminLTEとWebSocketでチャット機能を作ってみる その2(ログイン機能)

2018年10月6日Python,開発

おはようございます。

チャットの実装ですが、
ユーザー毎の制御(トークルームみたいな)を実現するために
MySQLにマスタを持たせて、ひとまずログイン機能を実装します。

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

スポンサーリンク

下準備

今回から、MySQLを使っていきますのでその下準備を。
次のテーブルを作成してデータを登録しておきます。

テーブル

-- パスワードマスタ
create table MST_PASSWORD (
  USER_ID varchar(20) not null comment 'ユーザーID'
  , PASSWORD varchar(128) not null comment 'パスワード'
  , CREATE_USER varchar(20) comment '作成者'
  , CREATE_DATE datetime comment '作成日時'
  , UPDATE_USER varchar(20) comment '更新者'
  , UPDATE_DATE datetime comment '更新日時'
  , constraint MST_PASSWORD_PKC primary key (USER_ID)
) comment 'パスワードマスタ' ;

-- ユーザマスタ
create table MST_USER (
  USER_ID varchar(20) not null comment 'ユーザーID'
  , USER_NAME varchar(60) not null comment 'ユーザー名'
  , ICON varchar(20) comment 'アイコン:画像ファイル名'
  , MESSAGE varchar(50) comment '一言メッセージ'
  , CREATE_USER varchar(20) comment '作成者'
  , CREATE_DATE datetime comment '作成日時'
  , UPDATE_USER varchar(20) comment '更新者'
  , UPDATE_DATE datetime comment '更新日時'
  , constraint MST_USER_PKC primary key (USER_ID)
) comment 'ユーザマスタ' ;

データ

-- MSTユーザー
DELETE FROM MST_USER;
INSERT INTO MST_USER VALUES('001','そら','sora.jpg','いいことないかな','INIT',NULL,NULL,NULL);
INSERT INTO MST_USER VALUES('002','りく','riku.jpg','ちゅーるちゅーるちゃおちゅーるー','INIT',NULL,NULL,NULL);
INSERT INTO MST_USER VALUES('003','うみ','umi.jpg','誰かブラッシングしてくれないかしら','INIT',NULL,NULL,NULL);
INSERT INTO MST_USER VALUES('004','こうめ','koume.jpg','ごはんまだ?','INIT',NULL,NULL,NULL);
INSERT INTO MST_USER VALUES('005','こなつ','konatsu.jpg','早く新しい家に引っ越ししたい','INIT',NULL,NULL,NULL);

-- MSTパスワード
DELETE FROM MST_PASSWORD
INSERT INTO MST_PASSWORD VALUES('001','001','INIT',NULL,NULL,NULL);
INSERT INTO MST_PASSWORD VALUES('002','002','INIT',NULL,NULL,NULL);
INSERT INTO MST_PASSWORD VALUES('003','003','INIT',NULL,NULL,NULL);
INSERT INTO MST_PASSWORD VALUES('004','004','INIT',NULL,NULL,NULL);
INSERT INTO MST_PASSWORD VALUES('005','005','INIT',NULL,NULL,NULL);

 

ライブラリの追加

次の2つを、「メニュー」>「Default Settings」よりインストールします。

  • mysql-connector-python-rf
  • configparser

フォルダ構成

フォルダ構成は次のようになります。
前回からの変更点は、conf、Daoあたりですかね。

SampleChat
│ Main.py

├─conf
│     db.config

├─Dao
│     MstPasswordDao.py
│     MstUserDao.py
│     SqlClient.py

├─static
│ ├─css
│ │ │     AdminLTE.css
│ │ │     AdminLTE.min.css
│ │ │     login.css
│ │ │     style.css
│ │ │
│ │ └─skins
│ │             skin-blue.css
│ │             skin-blue.min.css
│ │
│ ├─img
│ │     konatsu.jpg
│ │     koume.jpg
│ │     riku.jpg
│ │     sora.jpg
│ │     umi.jpg
│ │
│ └─js
│         adminlte.min.js
│         script.js

└─templates
login.html
main.html

画面

ログイン画面

以前記事に書いた【Python】FullCalendarにログイン機能をつけてみる を流用しました。

画面

login.html

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>チャットサンプル- ログイン</title>
		<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css">
        <link rel="stylesheet" href="{{ static_url('css/login.css') }}"/>
       	<link rel="stylesheet" href="https:////maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css">
		<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js"></script>
		<script type="text/javascript" src="https://stackpath.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"></script>
        <script type="text/javascript">
            function submitLogin() {
                $("#loginForm").submit();
            }
        </script>
    </head>
    <body>
        <form id="loginForm" method="post" action="/login">
            <div class="jumbotron">
                <div class="container">
                    <span class="fa fa-comments"></span>
                    <h2>チャットサンプル</h2>
                    <div class="box">
                        {% module xsrf_form_html() %}
                        <input id="inputUserId" name="user_id" type="text" placeholder="ユーザーID">
                        <input id="inputPassword" name="password" type="password" placeholder="パスワード">
                        <span class="errMsg">{{ error_msg }}</span>
                        <button class="btn btn-default full-width">
                            <span class="glyphicon glyphicon-ok"></span>
                        </button>
                    </div>
                </div>
            </div>
        </form>
    </body>
</html>

スタイル

login.css

body {
    background: #ffbb55 none repeat scroll 0 0;
}

.jumbotron {
	text-align: center;
	width: 35rem;
	border-radius: 0.5rem;
	top: 0;
	bottom: 0;
	left: 0;
	right: 0;
	position: absolute;
	margin: 4rem auto;
	background-color: #fff;
	padding: 2rem;
	height:45rem;
}

.container .fa {
	font-size: 10rem;
	margin-top: 3rem;
	color: #f96145;
}

input {
	width: 100%;
	margin-bottom: 1.4rem;
	padding: 1rem;
	background-color: #ecf2f4;
	border-radius: 0.2rem;
	border: none;
}
h2 {
	margin-bottom: 3rem;
	font-weight: bold;
	color: #ababab;
}
.btn {
	border-radius: 0.2rem;
}
.btn .glyphicon {
	font-size: 3rem;
	color: #fff;
}
.full-width {
	background-color: #8eb5e2;
	width: 100%;
	-webkit-border-top-right-radius: 0;
	-webkit-border-bottom-right-radius: 0;
	-moz-border-radius-topright: 0;
	-moz-border-radius-bottomright: 0;
	border-top-right-radius: 0;
	border-bottom-right-radius: 0;
}

.box {
	position: absolute;
	bottom: 0;
	left: 0;
	margin-bottom: 3rem;
	margin-left: 3rem;
	margin-right: 3rem;
}

span.errMsg {
    color: #f96145;
    font-size: 11px;
}

メイン画面

ヘッダーメニューを追加、
OSSのCSS、JSはCDNから取得するように変更しました。

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">
			<!-- 2.ヘッダ情報 -->
			<div class="navbar-header" style="padding:15px;">
				ユーザー名:{{ user_name }}
			</div>
			<!-- 3.リストの配置 -->
			<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>
			<!-- 4.ボタン -->
			<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">
			<!-- Left col -->
			<div class="col-xs-8">
				<!-- /.box -->
				<div class="row">
					<div class="col-xs-8">
						<!-- DIRECT CHAT -->
						<div id="chat-panel" class="box box-warning direct-chat direct-chat-warning box-solid" style="display:none;">
							<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>
							<!-- /.box-header -->
							<div class="box-body">
								<!-- Conversations are loaded here -->
								<div class="direct-chat-messages">
									<!-- Message. Default to the left -->
									<div class="direct-chat-msg">
										<div class="direct-chat-info clearfix">
											<span class="direct-chat-name pull-left">こなつ</span>
											<span class="direct-chat-timestamp pull-right">2018/09/25(月) 02:00</span>
										</div>
										<!-- /.direct-chat-info -->
										<img class="direct-chat-img" src="static/img/konatsu.jpg" alt="message user image">
										<!-- /.direct-chat-img -->
										<div class="direct-chat-text">
											そら最近どうしてる?
										</div>
										<!-- /.direct-chat-text -->
									</div>
									<!-- /.direct-chat-msg -->
									<!-- Message to the right -->
									<div class="direct-chat-msg right">
										<div class="direct-chat-info clearfix">
											<span class="direct-chat-name pull-right">そら</span>
											<span class="direct-chat-timestamp pull-left">2018/09/25(月) 02:05</span>
										</div>
										<!-- /.direct-chat-info -->
										<img class="direct-chat-img" src="static/img/sora.jpg" alt="message user image">
										<!-- /.direct-chat-img -->
										<div class="direct-chat-text">
											相変わらずだよ。<BR>
											あいつらの面倒で手一杯でさ。
										</div>
										<!-- /.direct-chat-text -->
									</div>
									<!-- /.direct-chat-msg -->
									<!-- Message. Default to the left -->
									<div class="direct-chat-msg">
										<div class="direct-chat-info clearfix">
											<span class="direct-chat-name pull-left">こなつ</span>
											<span class="direct-chat-timestamp pull-right">2018/09/25(月) 05:37</span>
										</div>
										<!-- /.direct-chat-info -->
										<img class="direct-chat-img" src="static/img/konatsu.jpg" alt="message user image">
										<!-- /.direct-chat-img -->
										<div class="direct-chat-text">
											一番のお兄さんだから大変ね。<BR>
											私は一人で快適な暮らしを送っているわ(^^♪
										</div>
										<!-- /.direct-chat-text -->
									</div>
									<!-- /.direct-chat-msg -->
									<!-- Message to the right -->
									<div class="direct-chat-msg right">
										<div class="direct-chat-info clearfix">
											<span class="direct-chat-name pull-right">そら</span>
											<span class="direct-chat-timestamp pull-left">2018/09/25(月) 06:10</span>
										</div>
										<!-- /.direct-chat-info -->
										<img class="direct-chat-img" src="static/img/sora.jpg" alt="message user image">
										<!-- /.direct-chat-img -->
										<div class="direct-chat-text">
											え、なにそれ自慢ですか?
										</div>
										<!-- /.direct-chat-text -->
									</div>
									<!-- /.direct-chat-msg -->
								</div>
								<!--/.direct-chat-messages-->
								<!-- Contacts are loaded here -->
								<div class="direct-chat-contacts">
									<ul class="contacts-list">
										<li>
											<a href="#">
												<img class="contacts-list-img" src="static/img/konatsu.jpg" alt="User Image">
												<div class="contacts-list-info">
													<span class="contacts-list-name">
														こなつ
														<small class="contacts-list-date pull-right">2018/09/25(月)</small>
													</span>
													<span class="contacts-list-msg">早く新しい家に引っ越ししたい。</span>
												</div>
												<!-- /.contacts-list-info -->
											</a>
										</li>
										<li>
											<a href="#">
												<img class="contacts-list-img" src="static/img/umi.jpg" alt="User Image">
												<div class="contacts-list-info">
													<span class="contacts-list-name">
														うみ
														<small class="contacts-list-date pull-right">2018/09/25(月)</small>
													</span>
													<span class="contacts-list-msg">誰かブラッシングしてくれないかしら。</span>
												</div>
												<!-- /.contacts-list-info -->
											</a>
										</li>
										<li>
											<a href="#">
												<img class="contacts-list-img" src="static/img/koume.jpg" alt="User Image">
												<div class="contacts-list-info">
													<span class="contacts-list-name">
														こうめ
														<small class="contacts-list-date pull-right">2018/09/24(日)</small>
													</span>
													<span class="contacts-list-msg">ちゅーるちゅーるちゃおちゅーるー</span>
												</div>
												<!-- /.contacts-list-info -->
											</a>
										</li>
										<li>
											<a href="#">
												<img class="contacts-list-img" src="static/img/riku.jpg" alt="User Image">
												<div class="contacts-list-info">
													<span class="contacts-list-name">
														りく
														<small class="contacts-list-date pull-right">2018/09/12(水)</small>
													</span>
													<span class="contacts-list-msg">ごはんまだ?</span>
												</div>
												<!-- /.contacts-list-info -->
											</a>
										</li>
										<!-- End Contact Item -->
									</ul>
									<!-- /.contatcts-list -->
								</div>
								<!-- /.direct-chat-pane -->
							</div>
							<!-- /.box-body -->
							<div class="box-footer">
								<form action="#" method="post">
									<div class="input-group">
										<input id="message" type="text" name="message" placeholder="Type Message ..." class="form-control">
										<span class="input-group-btn">
											<button id="sendButton" type="button" class="btn btn-warning btn-flat">Send</button>
										</span>
									</div>
								</form>
							</div>
							<!-- /.box-footer-->
						</div>
						<!--/.direct-chat -->
					</div>
					<!-- /.col -->
				</div>
				<!-- /.col -->
			</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>

プログラム

新規追加

MySQLとやり取りするクラスを新規で作成します。

Dao/SqlClient.py

import mysql.connector
from contextlib import closing
import configparser


class SqlClient:
    """
    SQLクライアントクラス.
    """

    def __init__(self, host=None, port=None, user=None, password=None, database=None):
        """
        イニシャライザ
        :param host:
        :param port:
        :param user:
        :param password:
        :param database:
        """

        conf = configparser.ConfigParser()
        conf.read("conf/db.config")

        # 指定されていないパラメータは設定ファイルから読み込み
        host = host if host is not None else conf.get("MySQL", "host")
        port = port if port is not None else conf.get("MySQL", "port")
        user = user if user is not None else conf.get("MySQL", "user")
        password = password if password is not None else conf.get("MySQL", "password")
        database = database if database is not None else conf.get("MySQL", "database")

        self.config = {
            "host": host,
            "port": port,
            "user": user,
            "password": password,
            "database": database
        }

    def select(self, sql, data=None):
        """
        データを検索します
        :param sql:
        :param data:
        :return:
        """

        with closing(mysql.connector.connect(**self.config)) as conn:
            c = conn.cursor(dictionary=True)
            c.execute(sql, data)

            return c.fetchall()

    def execute(self, sql, data):
        """
        クエリを実行します.
        :param sql:
        :param data:
        :return:
        """

        with closing(mysql.connector.connect(**self.config)) as conn:
            c = conn.cursor()
            c.execute(sql, data)
            c.close()
            conn.commit()

さらに、それぞれのテーブルよりデータを取得するためのクラスを追加。

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

    @staticmethod
    def mapping_data(record):
        """
        レコードをマッピングします
        :param record:
        :return:
        """
        dic = {
            "user_id": record['USER_ID'],
            "user_name": record['USER_NAME'],
            "icon": record['ICON'],
            "message": record['MESSAGE'],
            "createUser": record['CREATE_USER'],
            "createDate": record['CREATE_DATE'],
            "updateUser": record['UPDATE_USER'],
            "updateDate": record['UPDATE_DATE']}

        return dic

 

MstPasswordDao.py

from Dao.SqlClient import SqlClient


class MstPasswordDao(SqlClient):
    """
    MSTパスワードDAOクラス
    """

    def select_password(self, user_id):
        """
        ユーザIDを指定してパスワードを取得します
        :param user_id:
        :return:
        """
        sql = "SELECT * FROM MST_PASSWORD WHERE USER_ID = %s"

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

        return ""

    @staticmethod
    def mapping_data(record):
        """
        レコードをマッピングします
        :param record:
        :return:
        """
        dic = {
            "user_id": record['USER_ID'],
            "password": record['PASSWORD'],
            "createUser": record['CREATE_USER'],
            "createDate": record['CREATE_DATE'],
            "updateUser": record['UPDATE_USER'],
            "updateDate": record['UPDATE_DATE']}

        return dic

既存クラスの修正

メイン処理にログイン機能を追加します。

# --- coding: utf-8 ---
"""
チャットサンプル
"""

import os
import logging

import tornado.web
import tornado.ioloop
import tornado.websocket

from tornado.web import RequestHandler
from tornado.options import options
from tornado.websocket import WebSocketHandler
from Dao.MstPasswordDao import MstPasswordDao
from Dao.MstUserDao import MstUserDao


client = []

class AuthBaseHandler(RequestHandler):
    """
    認証ハンドラー基底クラス
    """
    cookie_user_id = "user_id"

    def get_current_user(self):
        logging.info("AuthBaseHandler [get_current_user]")

        user_id = self.get_secure_cookie(self.cookie_user_id)
        if not user_id:
            return ""
        return user_id.decode("UTF-8")

    def set_current_user(self, user_id):
        logging.info("AuthBaseHandler [set_current_user]")

        self.set_secure_cookie(self.cookie_user_id, user_id)

    def clear_current_user(self):
        logging.info("AuthBaseHandler [clear_current_user]")

        self.clear_cookie(self.cookie_user_id)


class AuthLoginHandler(AuthBaseHandler):
    """
    ログインハンドラー
    """

    def get(self):
        logging.info("AuthLoginHandler [get]")

        self.render("Login.html", error_msg="")

    def post(self):
        logging.info("AuthLoginHandler [post]")

        self.check_xsrf_cookie()

        # 認証処理
        input_user_id = self.get_argument("user_id")
        input_password = self.get_argument("password")

        dao = MstPasswordDao()
        password = dao.select_password(input_user_id)

        # 入力されたパスワードと保存されているパスワードをチェック
        is_auth = False if input_password != password else True

        if is_auth:
            self.set_current_user(input_user_id)
            self.redirect("/main")
        else:
            self.render("login.html", error_msg="ユーザーコードまたはパスワードが間違っています。")


class AuthLogoutHandler(AuthBaseHandler):
    """
    ログアウトハンドラー
    """

    def get(self):
        self.clear_current_user()
        self.redirect('/login')


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

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

    @tornado.web.authenticated
    def get(self):
        logging.info("[MainHandler] get")

        dao = MstUserDao()
        user = dao.select_user(self.get_current_user())
        user_name = user['user_name']

        self.render("main.html", user_name=user_name)


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)

        for cl in client:
            cl.write_message(message)

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

        if self in client:
            client.remove(self)


application = tornado.web.Application([
    (r"/login", AuthLoginHandler),
    (r"/logout", AuthLogoutHandler),
    (r"/main", MainHandler),
    (r"/chat", ChatHandler),
],
    template_path=os.path.join(os.getcwd(),  "templates"),
    static_path=os.path.join(os.getcwd(),  "static"),
    login_url="/login",
    cookie_secret="adfaskljfwepmaldskf:as;k",
    xsrf_cookies=True
)

if __name__ == "__main__":
    tornado.options.parse_command_line()
    application.listen(8888)
    logging.info("server started")
    tornado.ioloop.IOLoop.instance().start()

起動してみる

ログイン画面

ユーザID、パスワードを入力してボタンをクリック。

ログイン後トップ

チャット画面が表示されました。

まとめ

ログイン自体は以前もやったので大したことはなかったのですが、トークルームの実装について悩んでいます。
(複数人でのチャットや入室、退室をどうしよう)

とりあえず次回はデータを取得して表示するのと、
メッセージ送信時にDBに登録するところをやってみようかと思います。

ではでは。

 

スポンサーリンク


関連するコンテンツ

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

Posted by doradora