Python(+ Flask)でMattermostのBot作った

# 背景
社内のコミュニケーションツールをMattermostに移行することになった。
既存のBotをMattermostに移行できたらいいなーと思ったので、
自分自身の勉強も兼ねてPythonでMattermost用にBotを再実装[^1]してみた。
Botを実装するに当たって色々と調べまわったので、自分のためにも情報をまとめておく。

# 環境
- CentOS 7.7 (Mattermostサーバ)
- Ubuntu 18.04 LTS (Botサーバ)
- Mattermost 5.14.0
- Python 3.6.8

なんとなくCentOSとUbuntuの両方が触りたい気分だったのと、
Ubuntu 18.04はデフォルトでPython3が入っているので使ってみた。
要するに特に意味はないです。

# 環境構築
#### Mattermostの導入
MattermostはDockerで簡単に導入できるので、まずDockerを導入する。

```:CentOS
# Dockerをインストール
$ yum install -y yum-utils device-mapper-persistent-data lvm2
$ yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
$ yum install docker-ce docker-ce-cli containerd.io

# インストール確認
$ docker --version
Docker version 19.03.5, build 633a0ea
$

# Docker起動
$ systemctl status docker
$ systemctl enable docker
$ systemctl start docker
$ systemctl status docker

# 動作確認
$ docker run hello-world
Unable to find image 'hello-world:latest' locally
latest: Pulling from library/hello-world
1b930d010525: Pull complete
Digest: sha256:4df8ca8a7e309c256d60d7971ea14c27672fc0d10c5f303856d7bc48f8cc17ff
Status: Downloaded newer image for hello-world:latest

Hello from Docker!
This message shows that your installation appears to be working correctly.

To generate this message, Docker took the following steps:
 1. The Docker client contacted the Docker daemon.
 2. The Docker daemon pulled the "hello-world" image from the Docker Hub.
    (amd64)
 3. The Docker daemon created a new container from that image which runs the
    executable that produces the output you are currently reading.
 4. The Docker daemon streamed that output to the Docker client, which sent it
    to your terminal.

To try something more ambitious, you can run an Ubuntu container with:
 $ docker run -it ubuntu bash

Share images, automate workflows, and more with a free Docker ID:
 https://hub.docker.com/

For more examples and ideas, visit:
 https://docs.docker.com/get-started/

$
```

Dockerが導入できたら、以下でMattermostを導入、起動できる。

```
$ docker run --name mattermost-preview -d --publish 8065:8065 --add-host dockerhost:127.0.0.1 mattermost/mattermost-preview
```

一旦導入した後は以下で起動/停止が可能。

```
# 起動
$ docker start mattermost-preview

# 停止
$ docker stop mattermost-preview

# プロセス確認
$ docker ps
```

■参考
・[Qiita : CentOS7にDockerをインストールする](https://qiita.com/inakadegaebal/items/be9fecce813cebec5986)
・[docker docs : Get Docker Engine - Community for CentOS](https://docs.docker.com/install/linux/docker-ce/centos/)

#### Mattermostの設定
Mattermostが起動できたら、ブラウザからアクセスできるようになる。

http://xxx.xxx.xxx.xxx:8065

アクセスできたら管理者アカウントを作成。
メールアドレスは適当なものでOK。
![192.168.56.101_8065_signup_email_02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/507855/781b0449-995a-30ba-10f1-65fa496485c2.png)

適当にチームを作成。
![192.168.56.101_8065_create_team_display_name.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/507855/55c00c71-3384-6561-e800-8e235dfa3305.png)
![192.168.56.101_8065_create_team_team_url_MASK.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/507855/e8e1076b-1a26-5d85-57a3-ac4b4c02743a.png)


日本語化する。

Main Menu -> Account Settings -> Display -> Language
![192.168.56.101_8065_bot-test_channels_town-square.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/507855/682711b5-cea9-e0c2-5f31-144ed074eadc.png)

デフォルトだとプライベートネットワークに対して接続制限が掛かっている。
Webhookの通信先のサーバが同一セグメントの場合は通信許可の設定をしておく必要がある。
(自分はここで結構ハマった。。)

メインメニュー -> システムコンソール -> 開発者 -> 信頼されていない内部接続を許可する
![192.168.56.101_8065_admin_console_environment_developer.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/507855/59702ad2-1504-d83c-ea86-709ef9892f6d.png)

デフォルトではBotアカウントが作成できないようになっているので、有効化する。

メインメニュー -> システムコンソール -> ボットアカウント -> ボットアカウントの作成を有効にする
![192.168.56.101_8065_admin_console_integrations_bot_accounts.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/507855/ee65e70b-3a6c-1e3d-b97f-d718b0d57e34.png)

外向きのWebhookを作成する。

メインメニュー -> 統合機能 -> 外向きのウェブフック

コンテントタイプ　：　application/json (今回はJSONを選択してみた)
チャンネル　　　　：　トリガーを監視するチャンネル
トリガーワード　　：　Webhookの起動トリガーとなるワード
コールバックURL　 ：　メッセージ送信先(Botサーバ)のURL(今回はFlaskを使うので、デフォルトのポート5000を指定している)
![192.168.56.201_8065_bot-test_integrations_outgoing_webhooks_add_01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/507855/417c1ea2-af4d-8cf9-ef1e-7691a56a0622.png)
![192.168.56.201_8065_bot-test_integrations_outgoing_webhooks_add_02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/507855/5a7e2d8c-79f3-73fb-3a35-dc4466935811.png)
![192.168.56.201_8065_bot-test_integrations_outgoing_webhooks.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/507855/cff2f045-e41a-7524-e557-7c7b3fcac483.png)

Botアカウントを作成する。

メインメニュー -> 統合機能 -> ボットアカウント
![192.168.56.101_8065_bot-test_integrations_bots_add.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/507855/6f067465-7d7d-2f51-b9e0-8028065ded94.png)
Botの投稿権限はここで設定する。

![192.168.56.101_8065_bot-test_integrations_confirm_type=bots&id=5o4eejp977fqzqdprd7ji13gac&token=3tpdq6cc1tfi3cshkrazr7a6be_MASK.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/507855/91aaba14-c596-f5e5-f444-849f4e3c03af.png)
Botアカウントのトークンは後からメニュー画面で確認する術がないので、必ず控えておく。
ただ、万が一控え忘れたとしても以下コマンドで確認することは可能。

```
$ docker exec -t -e MYSQL_PWD=mostest mattermost-preview mysql -u mmuser mattermost_test -e "select * from UserAccessTokens;"
+----------------------------+----------------------------+----------------------------+-------------+----------+
| Id                         | Token                      | UserId                     | Description | IsActive |
+----------------------------+----------------------------+----------------------------+-------------+----------+
| xxxxxxxxxxxxxxxxxxxxxxxxxx | xxxxxxxxxxxxxxxxxxxxxxxxxx | xxxxxxxxxxxxxxxxxxxxxxxxxx | ?????????   |        1 |
+----------------------------+----------------------------+----------------------------+-------------+----------+
$
```

curlコマンドでBotアカウントから投稿できるか確認する。
まずは投稿先のチャンネルIDを確認する。

チャンネル -> 情報を表示する
![192.168.56.101_8065_bot-test_channels_town-square_03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/507855/47127a79-590e-27d0-c69b-908f045369f3.png)
![192.168.56.101_8065_bot-test_channels_town-square_02_MASK.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/507855/ef267baa-d147-cd2a-de51-6d855e2959bc.png)


以下コマンドでも確認可能。

```
$ docker exec -t -e MYSQL_PWD=mostest mattermost-preview mysql -u mmuser mattermost_test -e "select * from Channels;"
+----------------------------+---------------+---------------+----------+----------------------------+------+-------------+--------------------------------------------------------+--------+---------+---------------+---------------+---------------+-----------+----------+------------------+
| Id                         | CreateAt      | UpdateAt      | DeleteAt | TeamId                     | Type | DisplayName | Name                                                   | Header | Purpose | LastPostAt    | TotalMsgCount | ExtraUpdateAt | CreatorId | SchemeId | GroupConstrained |
+----------------------------+---------------+---------------+----------+----------------------------+------+-------------+--------------------------------------------------------+--------+---------+---------------+---------------+---------------+-----------+----------+------------------+
| xxxxxxxxxxxxxxxxxxxxxxxxxx | 1574487544576 | 1574487544576 |        0 | xxxxxxxxxxxxxxxxxxxxxxxxxx | O    | Off-Topic   | off-topic                                              |        |         | 1574487544652 |             0 |             0 |           | NULL     |             NULL |
| xxxxxxxxxxxxxxxxxxxxxxxxxx | 1574489265292 | 1574489265292 |        0 |                            | D    |             | xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx |        |         | 1574489265315 |             1 |             0 |           | NULL     |             NULL |
| xxxxxxxxxxxxxxxxxxxxxxxxxx | 1574487544570 | 1574487544570 |        0 | xxxxxxxxxxxxxxxxxxxxxxxxxx | O    | Town Square | town-square                                            |        |         | 1574487544611 |             0 |             0 |           | NULL     |             NULL |
+----------------------------+---------------+---------------+----------+----------------------------+------+-------------+--------------------------------------------------------+--------+---------+---------------+---------------+---------------+-----------+----------+------------------+
$
```

チャンネルID、Botトークンを指定して、APIに対してcurlコマンドでPOSTリクエスト。

```
$ curl -i -X POST -H 'Content-Type: application/json' -d '{"channel_id":"[Channel ID]", "message":"This is a message from a bot"}' -H 'Authorization: Bearer [Bot Token]' http://xxx.xxx.xxx.xxx:8065/api/v4/posts
```
![192.168.56.101_8065_bot-test_channels_town-square_04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/507855/9e8b478e-2868-bbe1-82c9-06315a043322.png)
Botアカウントで投稿できているのが確認できた。

■参考
・[Usual Software Engineer : Mattermost で Bot Account を作る方法](http://innossh.hatenablog.com/entry/2019/07/29/161130)

# Webhookへの応答
PythonでやるにはFlaskでやるのが簡単そうだったので、Flaskで実装してみた。
curlでやっているようにJSONをAPIに対してPOSTしているだけ。

```:Ubuntu
$ sudo apt install python3-pip
$ pip3 -V
pip 9.0.1 from /usr/lib/python3/dist-packages (python 3.6)
$
$ pip3 install flask
$ pip3 install requests
```
```python:bottest.py
import json

import requests
from flask import Flask, request

BOT_TOKEN = '[Bot Token]'
CHANNEL_ID = '[Channel ID]'
MM_API_ADDRESS = 'http://xxx.xxx.xxx.xxx:8065/api/v4/posts'

app = Flask(__name__)

@app.route('/bot-test', methods=['POST'])  # 外向きのWebhookのコールバックURL
def bot_reply():
    posted_user = request.json['user_name']
    posted_msg  = request.json['text']

    reply_headers = {
        'Content-Type': 'application/json',
        'Authorization': 'Bearer ' + BOT_TOKEN,
    }

    reply_data = {
        "channel_id": CHANNEL_ID,
        "message": f"@{posted_user} Bot reply message.",
        "props": {
            "attachments": [
                    {
                "author_name": posted_user,
                "text": posted_msg,
                }
            ]
        },
    }

    reply_request = requests.post(
        MM_API_ADDRESS,
        headers = reply_headers,
        data = json.dumps(reply_data)
    )

    return reply_request

if __name__ == '__main__':
    app.debug = True
    app.run(host='0.0.0.0')
```

応答が返せるかテストしてみる。
Flaskのテストサーバを起動。

```:Ubuntu
$ python3 bottest.py
```

Mattermostでトリガーワードを含むメッセージを投稿。
![192.168.56.201_8065_bot-test_channels_town-square.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/507855/246c04ab-8117-597c-59b2-cd4bc27e10b7.png)

Webhookをトリガーにして、Botアカウントから投稿できているのが確認できた。
あとはBotのロジックを実装していくだけ。

ちなみにMattermostのWebhookから飛んでくるJSONはこんな感じ。

```
{
    'token': 'xxxxxxxxxxxxxxxxxxxxxxxxxx', 
    'team_id': 'xxxxxxxxxxxxxxxxxxxxxxxxxx', 
    'team_domain': 'bot-test', 
    'channel_id': 'xxxxxxxxxxxxxxxxxxxxxxxxxx', 
    'channel_name': 'town-square', 
    'timestamp': 1234567890123, 
    'user_id': 'xxxxxxxxxxxxxxxxxxxxxxxxxx', 
    'user_name': 'mmadmin', 
    'post_id': 'xxxxxxxxxxxxxxxxxxxxxxxxxx', 
    'text': '#bot test', 
    'trigger_word': '#bot', 
    'file_ids': ''
}
```

■参考
・[NOBWAK'S LAIR : SLACK/MATTERMOSTのBOTをPYTHONのFLASKで作ろう](http://april.fool.jp/blogs/2016/06/02/slackmattermost%E3%81%AEbot%E3%82%92python%E3%81%AEflask%E3%81%A7%E4%BD%9C%E3%82%8D%E3%81%86/)
・[Qiita : MattermostとPython+Flaskでシンプルなbotを作る](https://qiita.com/slowsingle/items/d07dd9d1f3bc9429c433)

# おまけ
Botが完成したら、Webアプリケーションとして動かすためにWebサーバとWSGIサーバが必要になる。
色々調べた結果、自分はNginx + Gunicornを選択した。(一番シンプルで簡単そうな気がしたから)
この辺はまだまだ理解もあやふやなので、適当に駆け足でメモ程度に記載しておく。。

Gunicornはpipでインストール。

```:Ubuntu
$ sudo pip3 install gunicorn
```

gunicorn [Pythonスクリプト名]:[Flaskインスタンス名] で起動。(デフォルトのポートは8000)
(以下は bottest.py 内の app インスタンスを起動)

```
# Pythonスクリプトを配置しているディレクトリで実行
$ cd /path/bot
$ sudo gunicorn bottest:app
[YYYY-MM-DD hh:mm:ss +0900] [1699] [INFO] Starting gunicorn 20.0.2
[YYYY-MM-DD hh:mm:ss +0900] [1699] [INFO] Listening at: http://127.0.0.1:8000 (1699)
[YYYY-MM-DD hh:mm:ss +0900] [1699] [INFO] Using worker: sync
[YYYY-MM-DD hh:mm:ss +0900] [1703] [INFO] Booting worker with pid: 1703
```

Flaskのテストサーバと同じポートを使用したい場合は以下でOK
(Mattermost側のWebhook設定を変えるのが面倒なときとか)

```
$ sudo gunicorn bottest:app -b 0.0.0.0:5000
```

Pythonスクリプトのある場所まで移動しなくても chdir オプションでいける。

```
$ sudo gunicorn --chdir /path/bot bottest:app -b 0.0.0.0:5000
```

停止時は Ctrl + C でプロセスをkillする。

Gunicorn単体で動かすならこれでいいが、このままではNginxと連携できない。
まずはNginxをインストール。

```:Ubuntu
$ sudo apt install nginx
$ nginx -v
nginx version: nginx/1.14.0 (Ubuntu)
$
```

Nginxの設定をしていく。
Gunicornとの通信のためにSocketが必要になる。(以下では /tmp に作る)

```nginx:/etc/nginx/sites-available/default
upstream bot-test {
    server unix:/tmp/bot-test.sock;
}

server {
    listen 5000;

    root path/bot;
    server_name localhost;

    access_log /var/log/nginx/access.log;
    error_log /var/log/nginx/error.log;

    location /bot-test {
        try_files $uri @flask;
    }

    location @flask {
        proxy_pass http://bot-test;
    }
}
```

Nginxの稼働確認。

```
$ sudo nginx -t
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
$
$ sudo systemctl status nginx
$ sudo systemctl restart nginx
$ sudo systemctl status nginx
```

Nginxの設定が問題なければ、Gunicornの設定ファイルを作成する。
設定ファイルの場所、名前は任意のものでOK
先ほどNginxの設定ファイルに記載したSocketを記載する。

```
$ vim gunicorn_conf.py
```
```:gunicorn_conf.py
bind = 'unix:/tmp/bot-test.sock'
daemon = True
reload = True
```

-c オプションで設定ファイルを指定し、起動できればOK

```
$ sudo gunicorn bottest:app -c gunicorn_conf.py
```

停止時はプロセスをkillする。

```
$ sudo pkill gunicorn
```

正直なところ、おまけの部分はとりあえず動けばいいやの精神でやっているので、
絶対もっといいやり方があるはず。。

■参考
・[Qiita : gunicorn + Flask + nginx + Systemdで動かしてみた
](https://qiita.com/arata-honda/items/09e1c350b1bd8a186e9c)・[『入る学科間違えた高専生』の日記 : Ubuntu + Nginx + Gunicornを手動デプロイする手順](https://nnsnodnb.hatenablog.jp/entry/ubuntu-nginx-gunicorn-deploy)
・[Qiita : pythonをデーモン化するメモ](https://qiita.com/croquisdukke/items/9c5d8933496ba6729c78)

[^1]: 車輪の再実装
 https://www.slideshare.net/RansuiIso/python-115121978
 https://www.youtube.com/watch?v=kO4FNg648qE