API Gateway + Lambda で slack (2) ~ slash commandとinteractive message ~

# API Gateway + Lambda で slack

前回slack appを登録した続きからです。

http://qiita.com/kanagi/items/843f898458df34f6ae90


slash commandとinteractive messageを利用して対話的にslackで処理を進めてみます。

## lambdaの登録

まずは動作テストのためにapexのhelloを置いてしまいます。
```
apex deploy hello
```
で実行されるとテキストだけ出力するサンプルが配置されます。

## API Gatewayの登録

`/` に配置してしまいます。

* [アクション] -> [メソッドの作成] -> POSTを選んでチェックをクリック

![API_Gateway.png](https://qiita-image-store.s3.amazonaws.com/0/63168/823357d9-d88e-750a-8807-859707156024.png)

* セットアップ画面になるので、先ほど作成したlambdaを選んで[保存]

![API_Gateway2.png](https://qiita-image-store.s3.amazonaws.com/0/63168/629bab13-5ecd-f74f-fd96-d438195ab66b.png)

* 権限許可確認画面がでるので[OK]

![API_Gateway3.png](https://qiita-image-store.s3.amazonaws.com/0/63168/22fefedf-6d0f-972e-2e17-3a551b5e0c81.png)

* メソッドの実行画面で[統合リクエスト]を選択

![API_Gateway4.png](https://qiita-image-store.s3.amazonaws.com/0/63168/c2c6d218-ad8d-4be3-3c9d-be9d8bb953a4.png)

* [本文マッピングテンプレート]をクリックして詳細を出してから、[マッピングテンプレーとの追加] -> application/x-www-form-urlencoded を入力してチェックをクリック

![API_Gateway5.png](https://qiita-image-store.s3.amazonaws.com/0/63168/ae7c4869-ec10-fff2-e819-e2dca50619f1.png)


* パススルー動作の変更画面がでるので [はい、この統合を保護します]

![API_Gateway6.png](https://qiita-image-store.s3.amazonaws.com/0/63168/7dd2a649-6393-1ff6-98e1-7c7c89424db4.png)

* POSTの中身をquerystringに押し込むテンプレートを記述して[保存]

前回は詳細を書きませんでしたが、これはvelocityのテンプレートです。awsで規定されている変数をvelocityのテンプレートを使ってlambdaに渡すことができるという仕組みです。

POSTのデータを展開して渡すことができるので下記を参考にしたテンプレートを配置しています。基本的に使うのは、querystring内に展開されるPOSTで渡されたパラメータです。

http://qiita.com/durosasaki/items/83af014aa85a0448770e
http://dev.classmethod.jp/cloud/aws/sugano-013-api-gateway/

![API_Gateway7.png](https://qiita-image-store.s3.amazonaws.com/0/63168/952599fa-3ac0-adfb-a1a7-8daa7351037f.png)

```
#set($rawAPIData = $input.path('$'))
## escape any quotes
#set($rawAPIData = $rawAPIData.replace('"', '\"'))

## first we get the number of "&" in the string, this tells us if there is more than one key value pair
#set($countAmpersands = $rawAPIData.length() - $rawAPIData.replace("&", "").length())

## if there are no "&" at all then we have only one key value pair.
## we append an ampersand to the string so that we can tokenise it the same way as multiple kv pairs.
## the "empty" kv pair to the right of the ampersand will be ignored anyway.
#if ($countAmpersands == 0)
 #set($rawPostData = $rawAPIData + "&")
#end

## now we tokenise using the ampersand(s)
#set($tokenisedAmpersand = $rawAPIData.split("&"))

## we set up a variable to hold the valid key value pairs
#set($tokenisedEquals = [])

## now we set up a loop to find the valid key value pairs, which must contain only one "="
#foreach( $kvPair in $tokenisedAmpersand )
 #set($countEquals = $kvPair.length() - $kvPair.replace("=", "").length())
 #if ($countEquals == 1)
  #set($kvTokenised = $kvPair.split("="))
  #if ($kvTokenised[0].length() > 0)
   ## we found a valid key value pair. add it to the list.
   #set($devNull = $tokenisedEquals.add($kvPair))
  #end
 #end
#end

{
    "headers" : {
#foreach( $key in $input.params().header.keySet() )
       "$key" : "$input.params().header.get($key)"#if( $foreach.hasNext ),#end
#end
    },
    "querystring" : {
#foreach( $kvPair in $tokenisedEquals )
  ## finally we output the JSON for this pair and append a comma if this isn't the last pair
  #set($kvTokenised = $kvPair.split("="))
  #if($kvTokenised.size() == 2 && $kvTokenised[1].length() > 0)
    #set($kvValue = $kvTokenised[1])
  #else
    #set($kvValue = "")
  #end
  #if( $foreach.hasNext )
    #set($itemDelimiter = ",")
  #else
    #set($itemDelimiter = "")
  #end
 "$kvTokenised[0]" : "$kvValue"$itemDelimiter
#end
    },
    "stage" : "$context.stage",
    "sourceIp" : "$context.identity.sourceIp",
    "userAgent" : "$context.identity.userAgent"
}
```

* デプロイしないと反映されないので[アクション] -> [APIのデプロイ] -> APIのデプロイウィンドウがでるので、ステージ名を前回作ったものを選択して[デプロイ]

![API_Gateway8.png](https://qiita-image-store.s3.amazonaws.com/0/63168/cd6f01cf-cb70-a2d7-b170-9ce2f17a045b.png)

* URLの呼び出し:で示されるURLを取っておきます

![API_Gateway9.png](https://qiita-image-store.s3.amazonaws.com/0/63168/249b7c75-e699-acbe-0667-bc6831317a92.png)


# slash commandを登録

slack appの画面から[slash commands]を選択して、[Create New Command]を選びます。

![Slack_API__Applications___wacul_Slack.png](https://qiita-image-store.s3.amazonaws.com/0/63168/0c47f769-c018-56ff-8108-bf64931add11.png)

この時点で、API gatewayに`application/x-www-form-urlencoded`でPOSTすると、helloworldが帰ってくるはずです。

```bash:
$ curl -XPOST -d "test" [API gatewayのURL]
{"hello":"world"}
```

## slack appの設定

* 作成したslack appの[Slash Commands]から[Create New Command]

![Slack_API__Applications___wacul_Slack.png](https://qiita-image-store.s3.amazonaws.com/0/63168/fc030878-8933-7514-d79a-7ea501a5b75b.png)

* コマンドを登録する

登録するコマンド名を`Command`、`Request URL`にAPI Gatewayで控えたURL、`Short Description`に適当な説明を入れて[Save]


![Slack_API__Applications___wacul_Slack3.png](https://qiita-image-store.s3.amazonaws.com/0/63168/6c5938c2-e4e7-5878-4df7-2d86ffaf50d5.png)


slackで`/hello`したら返事が返って来れば成功です。

## tokenチェックの追加

このままでは`curl`でテストした通り、どこからでもURLを叩けば結果を出してしまいます。
`hello`のように特に意味のない結果を返すだけなら問題ないですが、作ったslack appの通知だということを確認するためにtokenをチェックするようにします。

tokenは作成したslack appの`client id`や`client secret`が記載されていた、[Basic Infomation]の`Verification Token`に記載されています。

これが、slackからPOSTされた時のパラメータ

https://api.slack.com/slash-commands#triggering_a_command

のtokenとして送られてくるので、文字列が一致するかチェックするだけです。

他、使用するユーザやチャンネルを制限したいなら、`user_id`や`channel_id`をチェックしたりすればいいことになります。

# interactive message

interactive messageとは、slackのドキュメントに乗っているものを見てもらうのが一番早いかと思います。

https://api.slack.com/docs/message-buttons

ボタンを表示して、押したボタンに対してさらに反応させることができるものです。
これを利用すると、jenkinsでやっていたようなデプロイの実行などをslackで完結させることが可能になります。

## slack appの設定

interactive message用に別のlambdaを作ってもいいですが、ここでは同じものを使いまわします。

* 登録したslack appの[Interactive Messages]から[Enable Interactive Messages]

![Slack_API__Applications___wacul_Slack.png](https://qiita-image-store.s3.amazonaws.com/0/63168/49261497-9f7c-cef9-6ef2-422b653fbdf4.png)

* [Request URL]にAPI Gatewayで控えたURLを入れて[Enable Interative Messages]

![Slack_API__Applications___wacul_Slack2.png](https://qiita-image-store.s3.amazonaws.com/0/63168/c93dc90c-1db8-be6d-0cd5-c6553cb006ba.png)

## lambda functionの変更

slackのドキュメント通り、返却するメッセージにactionを付け加えます。

```
console.log('starting function')
exports.handle = function(e, ctx, cb) {
  console.log('processing event: %j', e)
  cb(null, { text: "button", attachments: [{ "callback_id": "test", actions: [{name: "test", text: "Test", type: "button", value: "test"}] }] } )
}
```

```
apex deploy hello
```

でデプロイして準備完了です。

## 実行してみる

slackで `/hello` を実行すると下記のようになるはずです。

![Slack_-_wacul.png](https://qiita-image-store.s3.amazonaws.com/0/63168/d3732fcd-f457-da06-1905-0d1c1bcb92f0.png)

この[Test]ボタンが押せるようになっています。押すと、ボタンは消えてしまうはずです。

ボタンを押した際に、interactive messagesで登録したURLにリクエストが飛んでおり、その返答が表示されているのですが、今回はボタンを押した際にも同じ`callback_id`のボタンを出しているので、ボタンが表示されない結果になっています。


あとは、ボタンを押した時に渡されるパラメータ

https://api.slack.com/docs/message-buttons#responding_to_message_actions

に従って処理を進めていけば対話的にslack上で処理を組み立てることができます。

注意点として、slash commandの場合と異なり、payloadパラメータにURLエンコードされたjsonでデータが渡されるので、slash commandと同じlambdaを使う場合は、payloadパラメータの有無で処理を分けるといいでしょう。

# TIPS

## 3秒以上かかる処理

slash commandsの最初の応答は3秒以内ではないとエラーになる仕様です。その場合、非同期で別のlambdaを呼びだしてあげたりすることで回避します。

呼び出されたlambdaでは、response_urlパラメータに記載されているURLに対して、メッセージをPOSTします。そうすればメッセージが表示されるようになります。response_urlは30分以内で５回までしか利用できません。

## メッセージのの表示非表示

コマンドを実行したときに、本人にしか見えないメッセージかどうかは `response_type`が`in_channel`か`ephemeral`かで決まります。デフォルトは`ephemeral`なので本人にしか表示されません。

`response_url`を使って複数回メッセージを送ると、デフォルトではメッセージを書き変えますが、メッセージを書き変えずに新しく途中からメッセージを表示するようにしたりといったことができます。