Hangouts Chat のチャットボットを、Python on Google App Engineを使って実装してみた（１）

#はじめに
Hangouts Chat が一般公開されて1か月と少し経ちました。
一般公開されてからこれまで、`GAE + DialogFlow`、`Amazon API Gateway + AWS Lambda + DialogFlow`、といった組み合わせで、いくつか `Hangouts Chat` 向けのチャットボットを実装し、社内向けに公開してきました。
そういう訳で、備忘も兼ねて `Hangouts Chat BOT` の実装手順をまとめていきたいと思います。
尚、今回は入門的な内容となっていますので、これから `Hangouts Chat BOT` を開発したい方の一助となれば幸いです。

#BOT実装の流れ
GAEへの `Hangouts Chat BOT` 実装は、次の手順で進めます。

1. GCPにプロジェクトを作成
2. ローカルマシンに `Python on GAE` 開発環境を準備
3. Python Flask Skeletonのダウンロード ～ ローカルテスト
4. BOTアプリの準備 ～ ローカルテスト
5. BOTアプリをGAEにデプロイ ～ 動作テスト
6. Hangouts Chat API の有効化、ボット設定 ～ 動作テスト

#実装環境

- Python on Google App Engine スタンダード環境
- BOTのWebフレームワークには `Flask` を利用
- HTTPエンドポイントなBOTで、リクエストに対し同期応答

# GCPにプロジェクトを作成
初めてGCPにアカウントを作成した場合、新しいプロジェクトを作成します。
また既存のプロジェクトを利用するなら、この手順は省略できます。
尚、プロジェクト作成手順の詳細は割愛します。

#ローカルマシンに Python on GAE 開発環境を準備
##ローカル環境

- OS は `CentOS7`
- `pyenv`と `pyenv-virtualenv` によるPython開発環境がセットアップ済

##Google Cloud SDKのインストール
[このページ](https://cloud.google.com/sdk/docs/quickstart-redhat-centos?hl=ja)を参考に、次の手順で `Google Cloud SDK` と `Python 関連の追加コンポーネント` をインストールします。

###YUMリポジトリの追加
```bash
$ sudo tee -a /etc/yum.repos.d/google-cloud-sdk.repo << EOM
[google-cloud-sdk]
name=Google Cloud SDK
baseurl=https://packages.cloud.google.com/yum/repos/cloud-sdk-el7-x86_64
enabled=1
gpgcheck=1
repo_gpgcheck=1
gpgkey=https://packages.cloud.google.com/yum/doc/yum-key.gpg
       https://packages.cloud.google.com/yum/doc/rpm-package-key.gpg
EOM
```

###Google Cloud SDK、追加コンポーネントのインストール
```bash
$ sudo yum install google-cloud-sdk google-cloud-sdk-app-engine-python google-cloud-sdk-app-engine-python-extras
```

###Google Cloud SDKの初期設定
前述のページを参考に、Google Cloud SDK の初期設定を行います。

```bash
$ gcloud init
```

##Python Flask Skeleton for Google App Engine
[GitHub リポジトリ](https://github.com/googlearchive/appengine-flask-skeleton) で公開されている `Google App Engine 向け Python Flask スケルトン` をローカル環境の任意のディレクトリ（今回は `~/projects/chatbot`）にダウンロードします。

```bash
$ git clone https://github.com/GoogleCloudPlatform/appengine-flask-skeleton.git ~/projects/chatbot
```

##pyenv+virtualenvで仮想環境準備
チャットボットを開発するディレクトリ下で Python2.7 最新版が利用出来るよう、Python 仮想環境を準備します。

```bash
$ pyenv virtualenv 2.7.14 chatbot
$ cd ~/projects/chatbot
$ pyenv local chatbot
$ python -V
Python 2.7.14
```

##Flaskと依存モジュールのアップデート
前述でダウンロードした `Google App Engine 向け Python Flask スケルトン` には、予め Flask と依存モジュールが同梱（`./lib` ディレクトリ内）されていますが、次のコマンドで最新バージョンにアップデートします。

```bash
$ pip install -U -r requirements.txt -t lib
```

##ローカル動作テスト
次のコマンドでローカル開発サーバ（dev_appserver.py）を起動します。

```bash
$ dev_appserver.py .
```

別ターミナルを起動し、localhost の 8080 ポートにアクセスします。
問題が無ければ、レスポンスで `Hello World!` 文字列が返されます。

```bash
$ curl http://localhost:8080
Hello World!
```

#チャットボット準備
`Google App Engine 向け Python Flask スケルトン` の環境を利用し、簡単なチャットボットを実装します。
尚、今回はボットに話しかけた内容が、そのままボットから返される `やまびこ` ボットとなります。

##ボットのPythonコード
`Hangouts Chat | Google Developers`ページ内の [Creating new bots](https://developers.google.com/hangouts/chat/how-tos/bots-develop) にあるPythonサンプルコードを、`bot.py`というファイル名で保存します。
尚、コード2行目には、念のため `coding: utf-8` を追記しています。

```bash
$ vi bot.py
```

```python:bot.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""Example bot that returns a synchronous response."""

from flask import Flask, request, json

app = Flask(__name__)

@app.route('/', methods=['POST'])
def on_event():
  """Handles an event from Hangouts Chat."""
  event = request.get_json()
  if event['type'] == 'ADDED_TO_SPACE' and event['space']['type'] == 'ROOM':
    text = 'Thanks for adding me to "%s"!' % event['space']['displayName']
  elif event['type'] == 'MESSAGE':
    text = 'You said: `%s`' % event['message']['text']
  else:
    return
  return json.jsonify({'text': text})

if __name__ == '__main__':
  app.run(port=8080, debug=True)
```

##デプロイ設定ファイル（app.yaml）の変更
追加した `bot.py` が全てのHTTPリクエストのルートとなるよう、app.yaml内のハンドラ設定を変更します。

```bash
$ vi app.yaml
```

```yaml:app.yaml
- url: .*  # This regex directs all routes to main.app
  script: main.app
```

↓↓↓変更

```yaml:app.yaml
- url: .*  # This regex directs all routes to main.app
  script: bot.app
```

#チャットボットのローカル動作テスト

##テストデータの準備
[このページ](https://developers.google.com/hangouts/chat/reference/message-formats/events)にあるイベントフォーマットの例を元に、テスト用のリクエストデータ `event.json` を準備します。

```bash
$ vi event.json
```

###ADDED_TO_SPACEイベントのテストデータ
イベントタイプが `ADDED_TO_SPACE`のイベントは、チャットルームにBOTを追加した際、ボットに送信されます。

```json:event.json
{
  "type": "ADDED_TO_SPACE",
  "eventTime": "2017-03-02T19:02:59.910959Z",
  "space": {
    "name": "spaces/AAAAAAAAAAA",
    "displayName": "Chuck Norris Discussion Room",
    "type": "ROOM"
  },
  "user": {
    "name": "users/12345678901234567890",
    "displayName": "Chuck Norris",
    "avatarUrl": "https://lh3.googleusercontent.com/.../photo.jpg",
    "email": "chuck@example.com"
  }
}
```

###MESSAGEイベントのテストデータ
イベントタイプが `MESSAGE`のイベントは、ボット宛にテキストメッセージが送信されます。

```json:event.json
{
  "type": "MESSAGE",
  "eventTime": "2017-03-02T19:02:59.910959Z",
  "space": {
    "name": "spaces/AAAAAAAAAAA",
    "displayName": "Chuck Norris Discussion Room",
    "type": "ROOM"
  },
  "message": {
    "name": "spaces/AAAAAAAAAAA/messages/CCCCCCCCCCC",
    "sender": {
      "name": "users/12345678901234567890",
      "displayName": "Chuck Norris",
      "avatarUrl": "https://lh3.googleusercontent.com/.../photo.jpg",
      "email": "chuck@example.com"
    },
    "createTime": "2017-03-02T19:02:59.910959Z",
    "text": "@TestBot Violence is my last option.",
    "thread": {
      "name": "spaces/AAAAAAAAAAA/threads/BBBBBBBBBBB"
    },
    "annotations": [
      {
        "length": 8,
        "startIndex": 0,
        "userMention": {
          "type": "MENTION",
          "user": {
            "avatarUrl": "https://.../avatar.png",
            "displayName": "TestBot",
            "name": "users/1234567890987654321",
            "type": "BOT"
          }
        },
        "type": "USER_MENTION"
      }
    ]
  },
  "user": {
    "name": "users/12345678901234567890",
    "displayName": "Chuck Norris",
    "avatarUrl": "https://lh3.googleusercontent.com/.../photo.jpg",
    "email": "chuck@example.com"
  }
}
```

##ローカル動作テスト
ローカル開発サーバを起動します。

```bash
$ dev_appserver.py . 
```

別コンソールを開き、テストデータ `event.json`をボットにPOSTします。

```bash
$ curl http://localhost:8080 -X POST -H "Content-Type: application/json" -d @event.json
```

`ADDED_TO_SPACE`イベントをPOSTした場合、ボットから次のレスポンスが返されます。

```
{
  "text": "Thanks for adding me to \"Chuck Norris Discussion Room\"!"
}
```

`MESSAGE`イベントをPOSTした場合、ボットから次のレスポンスが返されます。

```
{
  "text": "You said: `@TestBot Violence is my last option.`"
}
```

#ボットアプリをGAEにデプロイ

##サービスIDの変更（必要に応じて）
プロジェクト上で、既にGAEをサービスID `default` として利用している場合、誤って `default` サービスにデプロイしないよう、別のサービスIDを設定します。今回、新しく作成したプロジェクトにデプロイする場合は、この手順は省略できます。

ちなみにサービスIDを設定しない場合、サービスIDは`default`が暗黙的に設定され、ターゲットURLは `https://<プロジェクトID>.appspot.com` となります。
またサービスIDを設定した場合は、ターゲットURLは `https://<サービスID>-dot-<プロジェクトID>.appspot.com`となります。

```bash
$ vi app.yaml
```

`service` を追記

```yaml:app.yaml
service: chatbot
```

##GAEへデプロイ

`gcloud`コマンドでボットをGAEにデプロイします。
途中デプロイ先のリージョンを問われるので、任意のリージョンを指定する必要があります。

```bash
$ gcloud app deploy

･･･

 [1] europe-west2  (supports standard and flexible)
 [2] us-east1      (supports standard and flexible)
 [3] us-east4      (supports standard and flexible)
 [4] asia-northeast1 (supports standard and flexible)
 [5] asia-south1   (supports standard and flexible)
 [6] australia-southeast1 (supports standard and flexible)
 [7] southamerica-east1 (supports standard and flexible)
 [8] northamerica-northeast1 (supports standard and flexible)
 [9] us-central    (supports standard and flexible)
 [10] europe-west3  (supports standard and flexible)
 [11] europe-west   (supports standard and flexible)
 [12] cancel
Please enter your numeric choice:  4

･･･

descriptor:      [/path/to/projects/chatbot/app.yaml]
source:          [/path/to/projects/chatbot]
target project:  [<Project-ID>]
target service:  [default]
target version:  [20180410t123456]
target url:      [https://<Project-ID>.appspot.com]

Do you want to continue (Y/n)?  y

･･･
```

##操作テスト
ローカル動作テストと同様に、テスト用リクエストデータをGAEのターゲットURLにPOSTして動作を確認します。

```bash
$ curl https://<Project-ID>.appspot.com -X POST -H "Content-Type: application/json" -d @event.json
{
  "text": "You said: `@TestBot Violence is my last option.`"
}
```

以上の手順で、GAEにボットを実装できました。

#Hangouts Chat API の設定

##Hangouts Chat API の有効化
GCPコンソールから、`API とサービス` > `ダッシュボード` を開き、`API とサービスの有効化` をクリックします。
![screencapture-console-cloud-google-apis-dashboard-2018-04-10-16_21_30.png](https://qiita-image-store.s3.amazonaws.com/0/94476/e40a83d5-e397-d7f2-e0d4-2bcb2345d795.png)

検索欄に `Hangouts` を入力すると `Hangouts Chat API` が表示されるので、それをクリックします。 
![screencapture-console-cloud-google-apis-library-2018-04-10-16_22_32.png](https://qiita-image-store.s3.amazonaws.com/0/94476/9893cfb5-94c1-780d-5328-db2a4e27e842.png)

`有効にする` をクリックします。
![screencapture-console-cloud-google-apis-library-chat-googleapis-com-2018-04-10-16_22_52.png](https://qiita-image-store.s3.amazonaws.com/0/94476/1623b2c5-0d6f-e6ab-6d0c-3cf348ddc7c6.png)

##BOTの設定
API 有効化後、Hangouts Chat APIの概要ページに遷移するので、設定タブをクリックし、ボットに関する設定を入力します。

![screencapture-console-cloud-google-apis-api-chat-googleapis-com-hangouts-chat-2018-04-10-16_32_37.png](https://qiita-image-store.s3.amazonaws.com/0/94476/fef9d239-8409-eba1-3a45-2af9edc691ce.png)

|項目|説明|
|---|---|
|ボット名|Hangout Chat 上でのボットの表示名|
|アバターのURL|Hangout Chat 上で、ボットのアイコンとなる画像URL|
|説明|ボットの役割、機能等の説明文|
|機能|ボット宛のダイレクトメッセージ送信、ボットのチャットルーム内利用を有効、無効化|
|接続設定|`Bot URL`を選択し、GAEのターゲットURLを指定|
|確認トークン|Hangouts Chatからボット宛のリクエストイベントデータに付加されるトークン。ボット側のアプリでこのトークンをチェックすることで、セキュリティを向上できる。|
|権限|Hangouts Chatでのボットの利用権限。ボットを利用できるドメインユーザーの範囲を指定


#Hangouts Chat 上で動作テスト
##ボットにダイレクトメッセージを送信
Hangouts Chat の左上にある `ユーザー、チャットルーム、bot を検索`からボット名を入力すると、今回作成したボットが表示されるので、そのボットをクリックします。
![2018-04-10_16h34_19.png](https://qiita-image-store.s3.amazonaws.com/0/94476/87b32fea-ce8e-837e-2e87-b655d2bdcf01.png)

何かメッセージを送信すると、BOTから `You said: (送信したメッセージ)`が返ってきます。
![screencapture-chat-google-dm-jNN4pAAAAAE-2018-04-10-16_36_02.png](https://qiita-image-store.s3.amazonaws.com/0/94476/b00851d8-bbe7-2d36-bb27-c795d4a15e17.png)

##チャットルームからボット宛にメッセージ送信
ボットを追加したいチャットルームを開き、上部にある `<チャットルーム名>` > `ユーザーと bot を追加` をクリックします。
![2018-04-10_16h37_20.png](https://qiita-image-store.s3.amazonaws.com/0/94476/82c86cab-b1b6-80e8-b762-50468034cd23.png)

検索欄にボット名を入力し、表示されたボットをクリックします。
![screencapture-chat-google-room-AAAAgZEsEWA-2018-04-10-16_37_57.png](https://qiita-image-store.s3.amazonaws.com/0/94476/849f655c-d615-059e-1bfb-fa57a4e27b9d.png)

チャットルームに追加された旨のメッセージがボットから返されます。
あとはボット宛とわかるメンション（@ボット名）を付けてメッセージを送信します。
![screencapture-chat-google-room-AAAAgZEsEWA-2018-04-10-16_39_08.png](https://qiita-image-store.s3.amazonaws.com/0/94476/c8d4925a-ca6f-2742-15df-a109fcbb1509.png)

#次回
今回の内容により、GAE への Hangouts Chat BOT 実装の雰囲気を掴んでもらえたと思いますので、次回は Google DialogFlow を利用し、もう少し「ボットらしい」モノの実装方法を投稿したいと思います。