AmazonSNSのHTTPリクエストをHubotで受け取る

[AmazonSNS](https://aws.amazon.com/jp/sns/)とは、<b>Amazon</b>が提供するPUSH型通知サービスです。

多数のユーザーに対してメールやPUSH通知を送る以外に、
他のAWSサービスからの通知を受け取り、特定のエンドポイントに対してHTTPリクエストを送ったりできます。
https://aws.amazon.com/jp/sns/

今回は、他のAWSサービスからの通知をチャット上（[slack](https://slack.com/)等）に流すために、
[Hubot](https://hubot.github.com/)でAmazonSNSの通知を受け取ってみます。

なお、ここではメッセージに含まれる署名の確認を行っていません。
なりすまし攻撃を防ぐために署名の検証を行う場合は以下のヘルプをご参照ください。
http://docs.aws.amazon.com/ja_jp/sns/latest/dg/SendMessageToHttp.verify.signature.html

また、AWSへの知識が乏しいために間違った情報が含まれている可能性が大いにございます。
生暖かい目でご覧いただければと思います:bow:

# 完成品

さっそくですが、完成したスクリプトはこちらです。
次の項から解説していきます。

```coffeescript
util = require 'util'
bodyParser = require 'body-parser'
request = require 'request'

module.exports = (robot) ->
  robot.router.post '/webhook/:room', bodyParser.text(), (req, res) ->
    room = req.params.room
    type = if req.headers['x-amz-sns-message-type'] then req.headers['x-amz-sns-message-type'] else 'x-amz-sns-message-type is not found'
    body = if req.body then JSON.parse req.body else {}
    console.log type
    if type == 'SubscriptionConfirmation'
      requestSubscribe body.SubscribeURL
    else if type == 'Notification'
      sendMessage room, body
    res.send  'OK'

  requestSubscribe = (subscribeUrl) ->
    options =
      url: subscribeUrl
      method: 'GET'
    request options, subscribeCallback

  subscribeCallback = (error, response, body) ->
    console.log util.inspect body

  sendMessage = (room, body) ->
    message = body.Subject
    #message += "\r\n" + body.Message
    robot.messageRoom room, message
```

# HubotでHTTPリクエストを受信

Hubotには[express](http://expressjs.com/ja/)が内蔵されており、`robot.router.get`、または`robot.router.post`を使うことで、HTTPリクエストを受け取ることができます。
AmazonSNSのリクエストはPOSTで来るので、今回は`robot.router.post`を使います。
パスの部分は任意ですが、ここでは`:room`という形で、投稿するルームIDを指定できるようにしてみました。
（エンドポイントURLを`/webhook/%23general`にすると、`#general`に投稿されるようにします）

```coffeescript
module.exports = (robot) ->
  robot.router.post '/webhook/:room', bodyParser.text(), (req, res) ->
```

第二引数には`bodyParser.text()`を渡します。

AmazonSNSのリクエストは本文がJSON形式の文字列になっているのですが、Content-Typeが`plain/text`で送られてきます。
Hubotのexpressはデフォルトで`plain/text`で来たリクエストボディをパースできないので、ここで指定してあげる必要があります。

`body-parser`モジュールを使うので、コードの上の方でrequireしておきます。

```coffeescript
bodyParser = require 'body-parser'
```

# リクエストタイプを判定

次の行からは、パラメータからroomIDの取得、bodyのパースの他、`x-amz-sns-message-type`というヘッダーの情報を取得しています。

```coffeescript
    room = req.params.room
    type = if req.headers['x-amz-sns-message-type'] then req.headers['x-amz-sns-message-type'] else 'x-amz-sns-message-type is not found'
    body = if req.body then JSON.parse req.body else {}
```

AmazonSNSからエンドポイントに対して通知を送るには、一度エンドポイントを承認する必要があります。
承認用のリクエストも、通常の通知と同じURLに送られてきます。
リクエストヘッダーに含まれる`x-amz-sns-message-type`を取得することで、
承認用のリクエストか通常の通知かを判別できます。

承認用リクエストであれば**SubscriptionConfirmation**、通常の通知であれば**Notification**が入ってます。

# 承認用リクエストを処理

```coffeescript
    if type == 'SubscriptionConfirmation'
      requestSubscribe body.SubscribeURL
```

```coffeescript
  requestSubscribe = (subscribeUrl) ->
    options =
      url: subscribeUrl
      method: 'GET'
    request options, subscribeCallback

  subscribeCallback = (error, response, body) ->
    console.log util.inspect body
```

承認用のリクエストだった場合、リクエストボディに含まれる承認用URLにリクエストを送信する必要があります。
承認用URLは`body.subscribeUrl`に入っています。
ここでは、`request`モジュールを使ってリクエストを送信した後、受け取った内容をコンソールに出力してみました。（出力しなくても特に問題はありません）

`request`モジュールもrequireしておきます。

```coffeescript
request = require 'request'
```

# 通常の通知を処理

```coffeescript
    else if type == 'Notification'
      sendMessage room, body
```

```coffeescript
  sendMessage = (room, body) ->
    message = body.Subject
    #message += "\r\n" + body.Message
    robot.messageRoom room, message
```

通常の通知の場合は、リクエストボディに件名と本文の情報が含まれていますので、それに沿って適切に処理を行います。
`body.Subject`が件名で、`body.Message`が本文です。
今回は、指定したルームに対して件名の情報だけ投稿するようにしてみました。

# エンドポイントの登録

これでHubot側の準備ができましたので、AmazonSNSにエンドポイントを登録してみます。
（ここはあまりうまく説明できる自信が無いのでさらっと流します。詳細はAWSのヘルプページをご覧ください……:sweat_smile:）

まず、AmazonSNSのコンソール画面へ移動し、**トピック**を作成します。
http://docs.aws.amazon.com/ja_jp/sns/latest/dg/CreateTopic.html

続いて、作成したトピックに対して、エンドポイントを**サブスクライブ**します。
http://docs.aws.amazon.com/ja_jp/sns/latest/dg/SubscribeTopic.html

以下のように設定してください。
**Protocol**：`HTTP`
**Endpoint**：`http://<your_hubot_host>:<your_hubot_port>/webhook/<roomID>`

**Create subscription**をクリックすると、登録したエンドポイントURLに対して、先ほど説明した**SubscriptionConfirmation**のリクエストが実行されます。正しくスクリプトが動作すれば、エンドポイントが承認されるはずです。
（トピックの画面で、登録したエンドポイントに**Subscription ID**が付与されていれば承認成功です）

# テスト送信

AmazonSNSのコンソールにおいてトピックを表示した状態で、**Publish to topic**をクリックすると、
メッセージの作成画面が開きます。

適当な件名と本文を入力し、**Publish Message**をクリックしてみてください。
チャット上に、件名に入力した文字列が投稿されれば成功です。

# 他のAWSサービスと連携

あとは、他のAWSサービスの設定において、通知先に先ほど作成したトピックを指定すれば、
AmazonSNSを通じてHubotにリクエストが送信され、チャット上にメッセージが投稿されるようになります。
必要に応じて件名や本文を解析して見やすい形に整形すると良いと思います。

以上、AmazonSNSのHTTPリクエストをHubotで受け取る手順でした。