Presentation Support System with Python3

# はじめに
N高等学校プログラミングクラス1年の [@seigo2016](https://twitter.com/seigo2018)です。
[N高アドカレ8日目](https://qiita.com/advent-calendar/2019/n-highschool)になります。
きちんとした記事ということで，何を書こうか悩んだのですが，現在のメインプロジェクトの「糸かけ曼荼羅色シミュレーター」についてはU22等で沢山お話したので，今回は最近の短期プロジェクト「PSS ~Presentation Support System~」についてお話します。

# PSS ~Presentation Support System~ の目的
皆さんも人に何かを伝える際にPowerPointやGoogleSlideなどを使用してプレゼンテーションを行うことが多々あると思います。N高プログラミングクラス(以下プロクラ)でも毎月LTが開催されています。
そこでプレゼンテーションを支援するシステムを制作することにしました。
## 機能
1. リアルタイムでコメントが流れる
2. スマートフォン等からスライドを操作できる
3. ポインターが使える (未実装)

## 環境
![PSS.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/311807/781f8cfd-dc69-b15d-2cd3-cec0ee9347cb.png)

### Webサーバー
コメントの受付・記録等を行う部分になります。全てDocker上で動かしています。
メモリが1GBですが意外と動いています。
### サーバーの情報
|部分              |性能                |
|:---------------:|:------------------:|
| VPN             |Vultr               |
| CPU             |1C/1T               |
| Memory          |1024MB              |
| OS              |OS Debian 10 Buster |


### Dockerコンテナの情報
DockerとDocker-Composeの練習も兼ねて使っています。

|コンテナ名          |概要                  |
|:---------------:|:------------------:  |
| proxy           |Nginx Reverse Proxy   |
| web             |Python3.6(Pythonの実行)|
| mysql           |DB(ログイン情報を保存)    |

### サーバーの環境
パスワードのハッシュ化はbcryptを，mysqlとの接続にはmysql-connectorライブラリを使用しました。
また下記以外にはSSL，Socketを使用しました。

```text:Python3
bcrypt==3.1.7
mysql-connector==2.2.9
pycrypto==2.6.1
PyYAML==5.2
tornado==6.0.3
```

### クライアントの環境
GUIを構築するためのライブラリにはTkinterを使用しました。

```text:Python3
pdf2image==1.10.0
Pillow==6.2.1
progressbar2==3.47.0
PyYAML==5.2
```

# リアルタイムでコメントを流す
これがこのプロジェクトのメインです。　
リアルタイムでコメントが流れるシステム自体は既に他のプロクラ生によって制作されています。
しかし，クライアント同士の接続が区切られていると使用できなかったりと制約が多かったため，その部分等を改善したシステムを新たに制作することにしました。
**このシステムは実装方法・仕様が全く異なるためド○ンゴのコメントシステムの特許侵害にはあたりません**
仕組みとしてはこのような流れとなっています。

余談ですが，私の周りには脆弱性を見つけて報告することを生きがいとしている人間が複数人いますので，プレゼン本番に落とされないようセキュリティ対策がある程度必要だったりします。

## サーバー
### Webフロント
こちらがコメントを受け付ける部分になります。
流れとしては以下のようになっています。
1. 受け取ったコメントをセキュリティ上問題ないか検証する
2. 問題なればデータベースに記録
3. スレッド間で共有している変数に代入する。

```python:server.py
def send_comment(comment):
    # 現在時刻(JST)
    dt_now = dt.now(JST)
    # データベースに記録
    c = database.cursor()
    sql = "INSERT INTO comment (text, entertime) values(%s,%s)"
    c.execute(sql,
              (comment, dt_now,))
    database.commit()
    # スレッド間で共有している変数に代入
    with commentbody.get_lock():
        commentbody.value = comment.encode()


class Comment(web.RequestHandler):
    def post(self):
        comment = self.get_argument("comment")
        comment = escape(comment)
        title = "コメント"
        if re.search(r'\S', comment) and len(comment) < 30:
            send_comment(comment)
            self.render('index.html', title=title)
        else:
            message = "正しく入力してください"
            self.render('index.html', title=title, message=message)
```

### クライアントへの送信
次にこちらが受け取ったコメントをソケット通信でクライアント(発表者PC)に送り出す部分になります。
流れは以下のようになっています。
1. 10023ポートでSocketの接続を待つ
2. 接続された場合はユーザー情報を確認する
3. 受け取った情報がDB上のユーザー情報に合致すれば接続を続行する
4. その後は受け取ったコメントを適宜クライアントに送信する
送られてきたデータのidとパスワードが，予めDBに登録してあるidとパスワードのハッシュが一致するかを確認しています
~~**ネスト深くない？**~~

```python:server.py
def connect_socket():  # Socket通信
    print("SocketStart")
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
        s.bind(('0.0.0.0', 10023))  # ポート10023で開放
        while True:
            s.listen(1)
            print("Waitng ....")
            conn, addr = s.accept()
            flg = False
            while True:
                try:
                    conn = context.wrap_socket(conn, server_side=3)
                    with conn:
                        while True:
                            try:
                                print("Connecting")
                                data = conn.recv(1024).decode()
                                if not data:
                                    break
                                elif ":" in data:
                                    loginuser = data.split(":")[0]
                                    loginpass = data.split(":")[1]
                                    sql = "SELECT id, name, pass FROM users WHERE name = %s"
                                    c.execute(sql, (loginuser,))
                                    userdata = c.fetchall()
                                    if len(userdata) and bcrypt.checkpw(loginpass.encode(), userdata[0][2].encode()):
                                        print("Connected")
                                        conn.sendall("接続完了".encode("utf-8"))
                                        flg = True
                                    else:
                                        conn.sendall("認証エラー".encode("utf-8"))
                                        conn.close()
                                        flg = False
                                elif flg:
                                    comment = commentbody.value
                                    with commentbody.get_lock():
                                        commentbody.value = "".encode()
                                    if len(comment):
                                        conn.sendall(comment)
                                        comment = ""
                            except socket.error:
                                flg = False
                                break
                except Exception as e:
                    flg = False
                    print("Disconnected\n{}".format(e))
        s.close()
```

## クライアント(発表者PC)
### コメント受信
サーバーに接続し，認証情報を送信してログインしたあとクライアント(発表者PC)でコメントを受け取ります。

```python:client.py
def rcv_comment():
    # Socket通信の設定
    context = ssl.create_default_context()
    context = ssl.SSLContext(ssl.PROTOCOL_SSLv23)
    context.verify_mode = ssl.CERT_NONE
    context.check_hostname = False
    conn = context.wrap_socket(socket.socket(socket.AF_INET),
                               server_hostname=host)
    conn.connect((host, port))
    # 認証情報を用意 (書式はuser:pass)
    idpass = "{}:{}".format(user_name, user_pass).encode()
    conn.sendall(idpass)
    manager = CommentManager(canvas)
    while True:
        try:
            data = conn.recv(1024)
            if len(data):
                comment = data.decode('utf-8')
                print("recv:" + comment)
                # 認証エラーが返ってきた場合はプログラムを修了
                if comment == "認証エラー":
                    break
                # それ以外の場合はコメントを描画部分に渡す
                manager.add_text(comment)
        except KeyboardInterrupt:
            break
```

### コメント描画
受け取ったコメントをTkinterのキャンバス上に描画します。

```python:client.py
class CommentManager: # コメントを描画を管理する
    def __init__(self, canvas):
        self.canvas_text_list = []
        self.canvas = canvas
        root.after(1, self.update)

    def add_text(self, comment): # 流すコメントを追加
    # x座標は画面端に，y座標はランダムでテキストを生成
        text = self.canvas.create_text(
            w, random.uniform(2.0, 18.0) * 100, text=comment, font=comment_font) 
        self.canvas_text_list.append(text)

    def update(self): # 左に移動させていく
        new_list = []
        # リスト内のコメントを左に移動。(タグ使って一括で行ったほうが良さそう)
        for canvas_text in self.canvas_text_list: 
            self.canvas.move(canvas_text, -15, 0)
            x, y = self.canvas.coords(canvas_text)
            if x > -10:
                new_list.append(canvas_text)
            else: #　左端まで移動したら
                self.canvas.delete(canvas_text)
        self.canvas_text_list = new_list
        root.after(20, self.update)
```

# スライドの操作
スライド自体をTkinterのCanvasに読み込んでいるのでその画像を更新していくだけです

```python:client.py
def next(event): # スライドを送る
    global page
    if pagemax - 1 > page:
        canvas.itemconfig(labelimg, image=img[page])
        page += 1


def prev(event): # スライドを戻す
    global page
    if 0 <= page:
        canvas.itemconfig(labelimg, image=img[page])
        page -= 1

# ~中略~ # 
if __name__ == '__main__':
    th = Thread(target=rcv_comment)
    th.setDaemon(True)
    th.start()
    root.bind("<Key-n>", next) # ここでキーを割当
    root.bind("<Key-p>", prev)
    root.mainloop()
```

# ポインター機能の実装
ポインターデバイスとして使うものの選定から悩み中なので未実装です。
実装次第追記します。
やはりスマートフォンがいいのでしょうか？

# 課題
- Mac環境でのウィンドウの調整
- セキュリティ対策の検証
- ポインター機能の実装
- 処理速度

# 終わりに
全く詰めきれていない，怒られるコードを書いてます…。
大量にコメントが流た場合のパフォーマンスが芳しく有りません。
あとオーバーレイとして表示したほうが賢いと思います。