Cisco Spark の Bot を AWS Lambda + Amazon API Gateway (Python) で実装してみる

# 1. Cisco Spark Bot の概要

Cisco Spark には Bot 機能が用意されている。人にメンションして話かける代わりに、Bot にメンションして話しかけると、Bot はそれをコマンドとして理解して処理を行い、結果のメッセージを返す（実際には Bot は中継をするだけで実処理は別のプログラムが行う）。

「どんな処理」をして「どんな結果」を返すかはアイデア次第。ここでは最も単純な処理として、ping というメッセージに対して pong と返答する Bot を作ってみる。実処理の部分は  AWS Lambda + Amazon API gateway を使うこととする。

## 1-1. 処理の全体像

1. Bot にメンションしてメッセージを投稿
1. Bot が Target URL 宛てに次の形式に従って送信（中継）:
 * 必ず HTTP POST を使う
 * ペイロードは定型のフォーマット（Envelop と呼ぶ）が使われる
 * 非定型のデータは Envelop の中の ‘data’ へ入れられる
1. Target URL を持つのは API Gateway の役割
1. Lambda は API Gateway の受信を「トリガー」として起動
1. Bot からの POST に対するリアクションは Lambda にて自分でプログラミングする必要がある

<img width="668" alt="CiscoSparkBot.png" src="https://qiita-image-store.s3.amazonaws.com/0/104747/7e7d398a-f2e0-1d19-6522-127d67f6aca7.png">

注意点として、2. で Bot が Target URL に HTTP POST する際、ペイロードは **メッセージそのものを含まず、メッセージ ID しか含まない**。メッセージそのものを取得するには Lambda から Cisco Spark に対して HTTP GET を実施し、ID に対応するメッセージを取得する必要がある。このプログラムは自分で用意する（図中の 6）。また、結果を Cisco Spark で表示するプログラム（HTTP POST）も用意が必要（図中の 7）。

# 2. Cisco Spark 側の設定手順
## 2-1. Bot の作成

Bot を作成するには実ユーザでログインする必要がある。Bot は実ユーザに紐づいて登録されるため。

以下のサイトへアクセスして「Create a Bot」をクリックする：
https://developer.ciscospark.com/add-app.html

![image.png](https://qiita-image-store.s3.amazonaws.com/0/104747/e8e62bcd-81b7-47fd-302f-35958176ada6.png)




続く画面で以下の三項目を全て埋める：

* Display Name：Bot の名前
* Bot Username：識別子。Bot の名前と同じでも良いが他の人と重複してはならない
* Icon：画像の URL を入力

![image.png](https://qiita-image-store.s3.amazonaws.com/0/104747/6a1107ca-ff8a-0be8-b259-8e61a4742b2a.png)

正しく入力すると Bot 用の Access Token が表示されるのでメモする。失効はしない。取り扱いに注意。

## 2-2. Room へ Bot を追加

Cisco Spark を開いて任意の Room へ行き、人を Room へ加えるのと同じ要領で Bot を Room へ追加する（Bot の名前で検索した後、追加）。

## 2-3. Webhook について

Cisco Spark へメッセージを投稿すると、Bot はそれを中継し、HTTP POST で別のプログラムへ渡す。プログラムの処理方法は様々だが通常結果は Bot へ戻され（いわゆるコールバック）Cisco Spark 上に表示する。HTTP によるコールバックの仕組みを Webhook と呼ぶ。

Webhook を設定する上で特に重要なのが Target URL である。インターネット上に存在する指定の URL で Bot が HTTP POST を投げる宛先となる。

Target URL 及びプログラムは Bot を作る人が用意しなければならない。そのために、例えば・・・サーバを準備して、OS をインストールして、Web サーバを稼働させ、IPアドレスを割り当てた後、URL を割り当てる・・・等と従来の方法を使っても良いが、相当な手間がかかるので、ここでは API Gateway を使って Target URL を用意し、実プログラムを Lambda で動作させる。

Target URL を決定するには Lambda + API Gateway の設定を先に行う必要がある。Cisco Spark の Webhook の設定は 4-1. で行う。


# 3. AWS Lambda + Amazon API Gateway 側の設定
※ この項は [Amazon API Gateway での AWS Lambda の使用 (オンデマンド HTTPS 経由)](http://docs.aws.amazon.com/ja_jp/lambda/latest/dg/with-on-demand-https-example.html) のチュートリアルを基本情報として、Cisco Spark Bot へ対応するよう必要に応じて手順をカスタマイズしています。

※ AWS マネジメントコンソール、及び、AWS CLI の設定が完了しアクセス可能であることが前提です。完了していない場合は[こちらの記事](http://docs.aws.amazon.com/ja_jp/lambda/latest/dg/with-on-demand-https-example-prepare.html)の「ステップ 1: 準備」等を参考に設定を完了して下さい。

## 3-1. Python 用デプロイパッケージを作成する
デプロイパッケージとは以下の二つを zip ファイルとして圧縮したもの：

* Lambda のプログラム（今回は Python3.6）
* 上記が使用するライブラリ

作成は以下の手順で行う：
1.AWS CLI へログインする。
2.Lambda 用の Python プログラムを作成する。ファイル名は Lambda 関数名と同じにする（LF4CiscoSpark）。ここでは以下のサンプルプログラムを作成する：

```
$ vi LF4CiscoSpark.py
---
from __future__ import print_function

import boto3
import json
import requests
import logging

logger = logging.getLogger()
logger.setLevel(logging.INFO)

access_code = ''        # BotのAccess Codeを入力
botDisplayName = ''     # Bot名を入力

def sendSparkGET(event):
    url = 'https://api.ciscospark.com/v1/messages/{0}'.format(event.get('data')['id'])
    headers = {
        'Authorization' : 'Bearer ' + access_code,
        'Content-Type' : 'application/json'
    }
    r1 = requests.get(url, headers = headers)
    return json.loads(r1.text)

def sendSparkPOST(event, message_detail):
    url = 'https://api.ciscospark.com/v1/messages/'
    headers = {
        'Authorization' : 'Bearer ' + access_code,
        'Content-Type' : 'application/json'
    }
    payload = {
      "roomId" : event.get('data')['roomId'],
      "text" : 'pong'
    }
    r1 = requests.post(url, headers = headers, data = json.dumps(payload))
    return True

def handler(event, context):
    print("Received event: " + json.dumps(event, indent=2))

    message_detail = sendSparkGET(event)
    bot_command = message_detail['text']
    
    bot_commands = {
        botDisplayName + 'ping' : lambda x, y : sendSparkPOST(x, y)
    }

    if bot_command in bot_commands:
        return bot_commands[bot_command](event, message_detail)
    else:
        raise ValueError('Unrecognized operation')
---
```
3.上記プログラム中で requests を import しているが、Lambda の実行環境には標準で含まれていない。標準外ライブラリを使いたい場合、デプロイパッケージへ該当ライブラリ（ requests）のファイルを含める必要がある。対応ファイルは通常 site-packages の中にあるはずなので、まず site-packages/requests ディレクトリを zip する：

```
$ zip LF4CiscoSpark.zip -r /(path-to-site-packages)/site-packages/requests
```

4.次に、今作成した LF4CiscoSpark.py を zip ファイルへ追加する：

```
$ zip -g LF4CiscoSpark.zip LF4CiscoSpark.py
```

### プログラムについて
Bot にメンションして "ping" とメッセージを送ると、Bot が "pong" と返信してくれる。 "ping" 以外のコマンドを使いたい場合は bot_commands に処理を記述すればよい。

* sendSparkGET() … 「1-1. 処理の全体像」の図中 6 に対応
* sendSparkPOST() … 「1-1. 処理の全体像」の図中 7 に対応

## 3-2. 実行ロール (IAM role) を作成する

IAM はユーザーがどの AWS リソースへアクセスできるか、そのリソースをどのような方法で使用できるかを制御する仕組み。一般的な設定手順としては、１）ロールの作成 ２）ポリシーの作成 ３）ロールへポリシーをアタッチ、の３ステップを行う。今回ポリシーは標準ポリシーを使うので２）は行わなくてよい。手順が完了すると ロール ARN と呼ばれる文字列が表示される。これは次の手順で Lambda 関数を作成するのに必要。

1. IAM コンソール (https://console.aws.amazon.com/iam/) にサインインする。
2. 「ロール」をクリック。
3. 「新しいロールの作成」をクリック。
4. 「AWS サービスロール」が選択されていることを確認。
5. 「AWS Lambda」の横の「選択」をクリック。
6. 「フィルター」と記載されたフィールドに lambda と入力
7. 表示された標準ポリシーから適切なポリシーにチェックを入れる。ここでは AWSLambdaBasicExecutionRole を選択する。注）セキュリティ上適切なポリシーを選択することは重要。[こちら](https://gist.github.com/gene1wood/55b358748be3c314f956)に AWS 標準ポリシーの一覧があるので "AWSLambda" でポリシー名を検索して AWSLambdaBasicExecutionRole より適切なものがあればそちらを選択する。"Action" に注目して比較するとよい。
7. 「ロール名」は lambda-gateway-execution-role とする。
8. 「ロールの作成」をクリック。
9. 今作成した lambda-gateway-execution-role （チェックを入れるのではなくロール名）をクリック
10. ロール ARN をメモする(arn:で始まる文字列)。これは次のステップで Lambda 関数を作るのに必要


## 3-3. Lambda 関数を作成する

1.AWS CLI へログインする。
2.以下のコマンドで lambda 関数を作成する。arn: には前の手順で取得したものを、region と profile には適切なものを指定する。zip-file 名と handler 名は function-name と同じにする必要がある模様。

```
$ aws lambda create-function --region us-west-2 \
--function-name LF4CiscoSpark \
--zip-file fileb://LF4CiscoSpark.zip \
--role arn:xxxxxxxxxxxxxxxxxx \
--handler LF4CiscoSpark.handler \
--runtime python3.6 \
--profile adminuser
```

※ なお、コードを update するときは以下：

```
$ aws lambda update-function-code \
--function-name LF4CiscoSpark \
--zip-file fileb://LF4CiscoSpark.zip
```

## 3-4. API Gateway を作成する
今回 API Gateway の作成には AWS CLI を使う。少々ステップが多いが、CLI での作業結果を逐一 AWS コンソールで確認しながら作業を行うとわかりやすい。
### 3-4-1. API の作成
1.AWS CLI へログインする。
2.以下のコマンドで API4CiscoSpark という名の API を作成する。作成に成功すると API を識別するための API ID が生成される。
※ API が生成されるとルートリソースも同時に生成される。次の手順でルートリソースの子リソースを作成するが、そのためのはルートリソースの ID も必要。

```
$ aws apigateway create-rest-api \
--name API4CiscoSpark \
--region us-west-2 \
--profile adminuser
（以下実行結果）
{
    "name": "API4CiscoSpark",
    "createdDate": 1501839827,
    "id": ""				           # API ID
}
```

AWS コンソールの表示結果（API Gateway）：

<img width="897" alt="API_Gateway_step1.png" src="https://qiita-image-store.s3.amazonaws.com/0/104747/77506e1d-62c4-b232-127d-3fdcee43ca80.png">


3.API のルートリソースの ID を調べる：

```
$ aws apigateway get-resources \
--rest-api-id (API ID)
（以下実行結果）
{
    "items": [
        {
            "path": "/",
            "id": ""			       # ROOT RESOURCE ID
        }
    ]
}
```

### 3-4-2. API 子リソースの作成

1.以下のコマンドで API4CiscoSpark API の子リソース Resource4CiscoSpark を作成する：

```
$ aws apigateway create-resource \
--rest-api-id (API ID) \
--parent-id (ROOT RESOURCE ID) \
--path-part Resource4CiscoSpark
（以下実行結果）
{
    "pathPart": "Resource4CiscoSpark",
    "parentId": "",
    "path": "/Resource4CiscoSpark",
    "id": ""			                # RESOURCE ID
}
```

AWS コンソールの表示結果（API Gateway）：

<img width="938" alt="API_Gateway_step2.png" src="https://qiita-image-store.s3.amazonaws.com/0/104747/5964da72-a104-e919-bf17-3257c054c8de.png">

### 3-4-3. メソッドの作成

1.以下のコマンドで Resource4CiscoSpark リソースに POST メソッドを追加する：

```
$ aws apigateway put-method \
--rest-api-id (API ID) \
--resource-id (RESOURCE ID) \
--http-method POST \
--authorization-type NONE
```

AWS コンソールの表示結果：

<img width="730" alt="API_Gateway_step3.png" src="https://qiita-image-store.s3.amazonaws.com/0/104747/6d09b76f-98ce-a1b0-c51b-f4e949f6b027.png">

### 3-4-4. Lambda 関数を POST メソッドの送信先に設定

1.以下のコマンドで Lambda 関数を POST メソッドの送信先に設定する。(アカウント番号) には AWS のアカウント番号12桁の数字を入れる：

```
$ aws apigateway put-integration \
--rest-api-id (API ID) \
--resource-id (RESOURCE ID) \
--http-method POST \
--type AWS \
--integration-http-method POST \
--uri arn:aws:apigateway:(region):lambda:path/2015-03-31/functions/arn:aws:lambda:(region):(アカウント番号):function:LF4CiscoSpark/invocations
```

AWS コンソールの表示結果（API Gateway）：

<img width="984" alt="API_Gateway_step4.png" src="https://qiita-image-store.s3.amazonaws.com/0/104747/d0ae6d03-cb77-d7b5-0988-04bb26319ae8.png">

### 3-4-5. POST メソッドのレスポンスを JSON に設定

1.以下のコマンドで POST メソッドのレスポンスを JSON に設定する。この設定により API メソッドが返すレスポンスのタイプが指定される。

```
$ aws apigateway put-method-response \
--rest-api-id (API ID) \
--resource-id (RESOURCE ID) \
--http-method POST \
--status-code 200 \
--response-models "{\"application/json\": \"Empty\"}"
```

### 3-4-6. POST メソッドの統合レスポンスを JSON に設定

1.以下のコマンドで POST メソッドの統合レスポンスを JSON に設定する。この設定により
 Lambda 関数が返すレスポンスのタイプを指定している。

```
$ aws apigateway put-integration-response \
--rest-api-id (API ID) \
--resource-id (RESOURCE ID) \
--http-method POST \
--status-code 200 \
--response-templates "{\"application/json\": \"\"}"
```

AWS コンソールの表示結果（API Gateway）：

<img width="984" alt="API_Gateway_step5.png" src="https://qiita-image-store.s3.amazonaws.com/0/104747/5d5f90c9-98f0-d3c6-cee6-1f99c0936deb.png">

### 3-4-7. API をデプロイする

1.次のコマンドで、作成した API を prod （プロダクション＝本番環境）というステージにデプロイする：

```
$ aws apigateway create-deployment \
--rest-api-id (API ID) \
--stage-name prod
```

AWS コンソールの表示結果（API Gateway）：

<img width="1014" alt="API_Gateway_step6.png" src="https://qiita-image-store.s3.amazonaws.com/0/104747/a9b4fcdd-4178-5337-521a-cee3e63b4786.png">

「URL の呼び出し」の右側に記載されている URL が Cisco Spark の Webhook 用 Target URL となる。

### 3-4-8. Lambda のトリガーとして API Gateway を指定する

```
$ aws lambda add-permission \
--function-name LF4CiscoSpark \
--statement-id apigateway \
--action lambda:InvokeFunction \
--principal apigateway.amazonaws.com \
--source-arn "arn:aws:execute-api:us-west-2:(アカウント番号):(API ID)/prod/POST/Resource4CiscoSpark"
```

AWS コンソールの表示結果（*AWS Lambda*）：

<img width="943" alt="API_Gateway_step7.png" src="https://qiita-image-store.s3.amazonaws.com/0/104747/e8db64da-56eb-c649-d3b5-f80c6e2904b5.png">

# 4. 設定の完了と動作確認
## 4-1. Cisco Spark で Webhook を設定

Webhook を設定するには、HTTP PUT を以下の条件で実行すればよい:

* URL: 
    * https://api.ciscospark.com/v1/webhooks を指定する
* ヘッダ
    * 'Authorization' : 'Bearer ' + Bot のアクセスコード
    * 'Content-Type' : 'application/json' を指定
* ペイロード
    * 'name' : webhook の名前を任意でつける
    * 'targetUrl' : Amazon API Gateway で設定した Target URL
    * 'resource' : 'message' を指定する
    * 'event' : 今回は webhook を作るので 'created' を指定する
    * 'filter' : 'roomId=' に続けて Bot の所属する roomId を指定する

roomId は[こちら](https://developer.ciscospark.com/endpoint-rooms-roomId-get.html)から調査可能。

<img width="448" alt="API_Gateway_step11.png" src="https://qiita-image-store.s3.amazonaws.com/0/104747/4b754e00-0c69-ed44-d419-fde99c180c22.png">


```
import requests

access_code = ''                       # ここに Access Code を入力

url = 'https://api.ciscospark.com/v1/webhooks'
headers = {
    'Authorization' : 'Bearer ' + access_code,
    'Content-Type' : 'application/json'
}

r = requests.put(url, headers = headers)

{
  'name' : 'My Awesome Webhook',
  'targetUrl' : 'https://example.com/mywebhook',
  'resource' : 'messages',
  'event' : 'created',
  'filter' : 'roomId=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
}
```

## 4-2. 動作確認
Cisco Spark にて Bot にメンションして "ping" と入力すると、Bot が "pong" と返してくれる。

<img width="422" alt="API_Gateway_step10.png" src="https://qiita-image-store.s3.amazonaws.com/0/104747/5a3ae299-7ad0-9261-0285-a050333cbe0c.png">

#（補足）API Gateway のログを生成するには

1.AWS コンソールで API Gateway サービスを開く。
2.「API名（API4CiscoSpark）」>「ステージ」>「prod」とクリックして「設定」タブを表示する。
3.「CloudWatch ログを有効化」にチェックを入れる。「ログレベル」を設定する。
4.「変更を保存」
  
<img width="816" alt="API_Gateway_step8.png" src="https://qiita-image-store.s3.amazonaws.com/0/104747/be02b4c2-71f8-f073-07db-329acdb97eae.png">

5.CloudWatch にてログを確認可能