AWS初心者でもわかる！ ブラウザ上で完結！ AWS+Slack Event APIを使ったSlackボット超入門

## はじめに
Slackはアメリカで開発されたチャットツールで、特にスタートアップやIT業界で利用されるようになってきたコミュニケーションツールです。弊社でも、業務内外問わずSlackの利用シーンが増えてきています。Slackでは利用者の用途に応じたボットを作成することができ、定期的なタスクの実行や、利用者の発言に応じてアクションを起こすなど、自由なカスタマイズができます。

今回は「Slackでおもしろいボットを作った」「Slackボットで業務を効率化した」といった記事をいくつか読んで興味を持ったので、Slackボットの作り方を詳しく調べ実装の流れを記事としてまとめることにしました。

この記事では、前半でSlackボットの作り方を簡単にまとめ、後半で実際に簡単なボットを作成します。AWS初心者でも記事に沿って作っていけばボットが作れる、そんな記事を目指しました。

## この記事を読むことで...
- 2通りあるインタラクティブなSlackボットの作り方とその違いがどちらもわかるようになる
- 話しかけると挨拶をするSlackボットのコードが手元にできる（それをカスタマイズしていけば便利なボットが簡単に作れる）
- いくつかのAWSサービスに触れて基本的な使い方がわかるようになる

## 前提知識
- Pythonの基本文法を理解している
- 本記事で利用するPythonの標準的なライブラリ（os/json/logging/random/urllib）の使い方を知ってること
- （Optional）本記事で扱うAWSサービス（IAM・API Gateway・Lambda・CloudWatch）がそれぞれどのようなサービスか知っていること（知らなくても作れますが知っているとより理解が深まると思います）

## Slackボット作成に利用できるAPI

ユーザーと相互にやり取りするインタラクティブなSlackボットを作る場合、2通りの方法があります。

- Web APIとEvent APIを併用する方法
- Realtime Messaging APIを利用する方法

記事の前半では、

- Web API
- Event API
- Realtime Messaging API

がそれぞれどんなものなのかを簡単に説明します。

### Web API
Slackが用意しているWeb APIを利用して、外部からSlackにアクションを起こすことができます。メッセージの投稿はもちろんのこと、チャンネルの管理やプライベートな会話の管理までSlackでできることは一通りこのAPIを使えば実現できます。利用できるAPIの一覧は「[Methods](https://api.slack.com/web)」にまとめられているので利用する際は参考にしてください。

Web API単体では、Slackに対して一方的にアクションを起こすことしかできないため、チャンネルに投稿があったら○○するボットのようにSlack上で発生したイベントに反応するボットを作るためには、後述のEvent APIとともに利用する必要があります。

### Event API
Event APIはSlack上でイベントが発生した際、指定したURLにそのイベントの内容をHTTPリクエストしてもらえる仕組みであり、このような仕組みはWebhookと呼ばれます。例えば、Slackにメッセージが投稿されると、指定したURLにメッセージの内容や投稿者の情報を含んだリクエストが飛びます。この仕組みを利用するには、SlackからのHTTPリクエストを処理するためのエンドポイントを作成しておく必要があります。

以下はWeb APIとEvent APIを併用してSlackボットを作成した場合のイメージ図です。Slack上で起きたイベントを、あらかじめ用意しておいたボット用サーバのエンドポイントにリクエストしてもらい、ボット用サーバ側で前述のWeb APIを利用してSlackにアクションを起こすことで、Slackと相互にやり取りするインタラクティブなボットを作ることができます。

<img width="1199" alt="event_api.png" src="https://qiita-image-store.s3.amazonaws.com/0/216033/ccdf5bc8-ba36-ad2f-afea-6d6a9aa5ca12.png">

### Realtime Messaging API

一方、Realtime Messaging APIは双方向のデータ通信を可能とするプロトコルであるWebSocketを利用しています。そのため、HTTPではEvent APIとWeb APIの2つの仕組みを使わなければならなかった相互の通信を、これ単体で実現できます。以下の図はRealtime Messaging APIを利用してSlackボットを作成した場合のイメージ図です。

<img width="1195" alt="websocket.png" src="https://qiita-image-store.s3.amazonaws.com/0/216033/c4f5fb19-9b9e-618d-fb01-75529ac59ed1.png">

注意する点としては、Event APIとRealtime Messaging APIではSlackから受け取ることのできるイベントが異なります。[こちら](https://api.slack.com/rtm)にそれぞれができること、できないことがまとめてあるので作成するときに自分のやりたいことはどちらで実現できるのかを確認しておきましょう。

記事の後半で実際にSlackボットを作成しますが、この記事ではWeb APIとEvent APIを使った例のみ説明します。Realtime Messaging APIを使った方法でボットを作りたい、という方のためにRealtime Messaging APIを使ったSlackボット用ライブラリをいくつか紹介しておきます。（全部Python用ですが...）

- https://github.com/lins05/slackbot
- https://github.com/slackapi/python-rtmbot
- https://github.com/slackapi/python-slackclient

## Slackボットを作ってみよう

ここからは前半で紹介した3つの仕組みのうちWeb API・Event APIを使って、自分が参加しているチャンネルにメッセージが投稿されたら、「Hello, Slack Bot!」と固定文言を返答するボットを作成します。イベントに反応して何かする、という流れさえ作ることができればあとはいくらでもカスタマイズできるようになるので、あくまでシンプルなボットを作ります。

ちなみに、Web API・Event APIを使った方法を選んだのは、AWS Lambdaを使ったサーバレスな構成で、しかもWebブラウザ上で全ての開発が完結できて簡単だったからです。Realtime Messaging APIを使った方法だとWebSocketのコネクションを管理しなければならず状態を持つことになるので、サーバレスな構成にはできません。

### 開発環境
今回はAWSのAPI Gateway+LambdaでSlackからのイベントを受け取るエンドポイントを作成し、エンドポイントの処理はPython 3.6を使って実装します。今回紹介するボットの作成はコーディングを含めてすべてブラウザ上で完結しますので、AWSのアカウントさえあれば前もって環境を用意する必要はありません。アカウントを持っていない方は[こちら](https://portal.aws.amazon.com/billing/signup#/start)から取得しておいてください。

### アーキテクチャ全体像
以下のような構成でボットを作成します。
<img width="558" alt="スクリーンショット 2017-12-21 8.35.14.png" src="https://qiita-image-store.s3.amazonaws.com/0/216033/0b833cae-0458-dfa1-b75a-100faa8597f1.png">

### SlackからのイベントをLambdaで受け取るまで
何はともあれ、SlackからEvent API経由でイベント情報を受け取ることから全てが始まります。まずはそこまでを目指します。

#### Slack APIのサイト上でボット作成用のアプリを作成する
Web APIやEvent APIを利用するには、Slack APIのサイト上でアプリを作成する必要があります。

初めて作成する方は[Slack API](https://api.slack.com)にアクセスし、画面中央の「Start Building」から自分のアプリを作成します。既にSlack APIのサイトを利用したことがある方は右上の「Your Apps」から自分の作成したアプリの一覧を開き、「Create New App」でアプリを作成できます。

今回アプリ名は「bot-sample」としました。「Development Slack Workspace」にはボットを参加させたいワークスペースを指定します。ワークスペースにはあらかじめログインしておく必要があります。

<img width="562" alt="スクリーンショット 2017-12-21 5.37.13.png" src="https://qiita-image-store.s3.amazonaws.com/0/216033/1ada385d-c253-1b54-6699-e2b422bf2d31.png">

アプリ作成完了後、以下のようなアプリの基本情報ページに遷移します。

<img width="1430" alt="スクリーンショット 2017-12-20 6.57.03.png" src="https://qiita-image-store.s3.amazonaws.com/0/216033/54b4180c-42b8-b7b6-1f1d-8c73f320d684.png">

#### Slack APIのアプリ上にボットユーザを作成する
次に、基本情報ページから主役となるボットユーザを作成しましょう。

Slack APIのサイト上でサイドメニューの「ボットユーザー」をクリックしボットユーザページに遷移した後、「ボットユーザーを追加する」ボタンを押してアプリにボットユーザーを追加します。

追加したボットユーザーには以下の画面のように名前をつけることができます。画像では「botsample」と愛情の感じられない名前になっていますが、皆さんは愛嬌のある名前をつけてあげてください。

<img width="1438" alt="スクリーンショット 2017-12-20 7.00.09.png" src="https://qiita-image-store.s3.amazonaws.com/0/216033/f7ce684e-495d-626e-93d2-80f92a4e2927.png">

#### Slackイベントを受け取るエンドポイントを作成する
アプリが作成できたら、イベントを受け取る先を用意します。AWS上にSlackからのイベントを受け取るLambda関数を作成し、API Gateway経由でアクセスできるようにします。

#### IAMのロールを作成
まず、Lambda関数に付与するロールを作成します。後のステップでAWSコンソール上でコーディングをするのですが、その時にログを出力できるとデバッグしやすいです。なので今回はLambda関数からCloudWatchにデバッグ用のログを出力できるようにロールを設定します。

AWSコンソールにてIAMの画面を開き、サイドバーからロールのページを開きます。「ロールの作成」ボタンを押してロール作成のページに遷移します。
![2017-12-20_12h13_52.png](https://qiita-image-store.s3.amazonaws.com/0/216033/075858f9-6a50-948c-7a26-b31314a29d0a.png)

このロールはLambda関数に付与するので、信頼関係の画面で「AWSサービス>Lambda」を選択して次のステップへ。
![2017-12-20_12h13_06.png](https://qiita-image-store.s3.amazonaws.com/0/216033/1ac1ebb8-9cf5-6d30-f136-0c3e45bd6338.png)

アクセス権限の画面ではCloudWatchへのログの書き込み権限を持つ「AWSLambdaBasicExecutionRole」ポリシーを選択して次へ。（作成したいボットで他のAWS上のサービスを利用したい場合はここで権限を追加しちゃいましょう。）
![2017-12-20_12h18_27.png](https://qiita-image-store.s3.amazonaws.com/0/216033/eeeba621-659b-3908-0b8c-742634ce313f.png)

確認画面にてロール名をつけ、設定内容を確認してロールを作成します。今回はロール名を「botSampleRole」としました。

#### Lambda関数の作成
ロールを作成したら、次にAWSのコンソールからLambda関数を作成しましょう。AWSコンソールにてLambdaの画面を開き「関数の作成」ボタンから関数の作成画面へ遷移します。
![2017-12-20_15h09_19.png](https://qiita-image-store.s3.amazonaws.com/0/216033/cae3a8d1-9600-b839-a3e5-b97378fc29e5.png)

関数の作成画面では以下のように「一から作成」を選択して各項目を埋めます。今回はランタイムにPython 3.6を、ロールには先ほど作成したロールを指定します。
![2017-12-20_12h25_00.png](https://qiita-image-store.s3.amazonaws.com/0/216033/3da1dee5-ec62-cf5c-e0d6-9cc8f437b78b.png)

関数の作成が完了すると以下のような画面に遷移します。何やらエディタのようなものが見えるかと思いますが、この画面でLambda関数のの処理内容をコーディングできます。
![2017-12-20_15h31_59.png](https://qiita-image-store.s3.amazonaws.com/0/216033/1a793d2a-e8eb-24dc-f62d-577deca6a0a0.png)

#### Event APIの認証に対応できるようにコーディング
Event APIにイベント情報の通知先エンドポイントを登録する際、エンドポイントに以下の形式で認証のためのリクエストが飛びます。（値は仮です。）

``` json
{
    "token": "Jhj5dZrVaK7ZwHHjRyZWjbDl",
    "challenge": "3eZbrw1aBm2rZgRNFdxV2595E9CY3gmdALWMmHkvFXO7tYXAYM8P",
    "type": "url_verification"
}
```

この中のchallengeの項目の内容（トークン）をSlackに送り返してやることで認証を通過できます。要は、登録したエンドポイントは本当にEvent API用のものですか？という確認をしているんですね。ソースコードを以下に示します。いろいろインポートしているのは、この後のステップで使うものも含めているためです。

``` python
# -*- coding: utf-8 -*-
import os
import json
import logging
import urllib.request

# ログ設定
logger = logging.getLogger()
logger.setLevel(logging.INFO)

def handle_slack_event(slack_event: dict, context) -> str:

    # 受け取ったイベント情報をCloud Watchログに出力
    logging.info(json.dumps(slack_event))

    # Event APIの認証
    if "challenge" in slack_event:
        return slack_event.get("challenge")
```

Lambdaでは2つの引数を持つ関数をプログラムのエントリポイントとして指定できます。1つ目の引数がリクエストの内容、2つ目がLambdaの設定や実行の状態を（あと何秒でタイムアウトするかなど）取得できるコンテキストになります。

コードを書き終えたら、同じ画面内のハンドラの設定を`lambda_function.handle_slack_event`とします。ハンドラは`{ファイル名}.{メソッド名}`の形でプログラムのエントリポイントを指定する設定箇所です。最後に保存するのをお忘れなく。

<img width="1311" alt="スクリーンショット 2017-12-21 6.32.33.png" src="https://qiita-image-store.s3.amazonaws.com/0/216033/e2195715-ec2a-8476-64a2-27e54c72bfa7.png">

#### API Gatewayの作成
次に、作成したLambda関数を叩くためのエンドポイントを作るためにAPI Gatewayの設定をします。API Gatewayの画面を開き、新しいAPIの作成をします。適当な名前をつけてAPIを作成しましょう。今回は「botSampleAPI」としました。
<img width="1080" alt="スクリーンショット 2017-12-21 6.37.24.png" src="https://qiita-image-store.s3.amazonaws.com/0/216033/f4dabf85-97cd-0492-5efc-8d22bc702bb0.png">

作成完了後の画面から、「アクション>メソッドの作成」を選択し、POSTメソッドを追加します。
<img width="1080" alt="スクリーンショット 2017-12-21 6.40.04.png" src="https://qiita-image-store.s3.amazonaws.com/0/216033/a744a689-5f47-5694-214a-c174911fdf2f.png">

POSTメソッドのセットアップ画面にて、先ほど作成したLambda関数と統合するよう設定します。
<img width="1081" alt="スクリーンショット 2017-12-21 6.42.12.png" src="https://qiita-image-store.s3.amazonaws.com/0/216033/451b8fec-02d0-c36d-85fd-9aaf1f97dd2d.png">

POSTメソッドを作成したら、「アクション>APIのデプロイ」からAPIをデプロイするとエンドポイントの完成です！デプロイボタンを押してエンドポイントが生成されると、画面上部にURLが出ているはずですので、それをコピーしておいてください。
<img width="588" alt="スクリーンショット 2017-12-21 6.44.31.png" src="https://qiita-image-store.s3.amazonaws.com/0/216033/a5f1391b-1d76-3af7-1a3c-711ced5955c2.png">

#### Event APIにエンドポイントを登録
長い道のりでしたが、これでやっとSlackのイベントを受け取ることができます。

[Slack API](https://api.slack.com)にアクセスし、最初のステップで作成したアプリを開きましょう。サイドメニューの「イベントの購読」を開いて、イベントを有効にします。

すると、URLを入力する欄が出てきますので、コピーしておいたエンドポイントのURLを貼り付けます。貼り付けた瞬間に先ほど説明したchallenge項目を含むリクエストが行われ、実装がうまくいっていれば認証をパスするはずです。

<img width="1060" alt="スクリーンショット 2017-12-21 6.52.24.png" src="https://qiita-image-store.s3.amazonaws.com/0/216033/6ce378c1-857d-9368-d6ac-34c30a02cc3c.png">

同じページの下部にある「ボットのイベントに参加する」に画像のように設定します。これで、ボットユーザが参加したチャンネルにメッセージが投稿されると、Lambda関数が呼ばれるようになりました！
<img width="705" alt="スクリーンショット 2017-12-21 6.58.51.png" src="https://qiita-image-store.s3.amazonaws.com/0/216033/c88582c8-9abc-bf17-6478-aff5d03aa0ea.png">

#### ボットユーザをワークスペースに追加
最後にボットユーザをワークスペースに追加します。

Slack APIの自分のアプリのページのサイドメニューから「アプリをインストール」を選択。「ワークスペースへのアプリのインストール」ボタンを押下して画面の指示に従って進めていくとトークンが発行され、ワークスペースにアプリとボットユーザが追加されます。

あとはチャンネルにボットを招いて、メッセージを投稿するとLambdaにメッセージが送られます。Lambdaが呼ばれたことを確認するにはAWSコンソールからCloudWatchを開き、ログを確認しましょう。
<img width="1074" alt="スクリーンショット 2017-12-21 7.09.06.png" src="https://qiita-image-store.s3.amazonaws.com/0/216033/1b1cc3a8-ba6d-f4e1-b138-62360b5b081d.png">

このように、pythonのコード中で`logging.info`を呼ぶことでCloudWatchにログが出力できます。もちろんwarningやerror用のメソッドもあります。

### Web APIを使ってボットにメッセージを投稿させる
ここまでで必要な環境は整ったので、あとはコーディングに集中できます。次は誰かがチャンネルに投稿した時に、ボットに「Hello, Slack Bot!」と言わせてみましょう。

#### メッセージ投稿用の関数を作成する
投稿するメッセージと投稿先のチャンネル名またはチャンネルIDを受け取ってメッセージを投稿する関数を作成します。

``` python
def post_message_to_slack_channel(message: str, channel: str):
    # Slackのchat.postMessage APIを利用して投稿する
    # ヘッダーにはコンテンツタイプとボット認証トークンを付与する
    url = "https://slack.com/api/chat.postMessage"
    headers = {
        "Content-Type": "application/json; charset=UTF-8",
        "Authorization": "Bearer {0}".format(os.environ["SLACK_BOT_USER_ACCESS_TOKEN"])
    }
    data = {
        "token": os.environ["SLACK_APP_AUTH_TOKEN"],
        "channel": channel,
        "text": message,
        "username": "Bot-Sample"
    }

    req = urllib.request.Request(url, data=json.dumps(data).encode("utf-8"), method="POST", headers=headers)
    urllib.request.urlopen(req)
    return
```

この関数では[chat.postMessage](https://api.slack.com/methods/chat.postMessage) APIを使ってSlackにメッセージを投稿しています。POSTメソッドを利用したWeb APIには、URLエンコード形式でしかリクエストボディを受け付けないものと、JSON形式のリクエストボディを利用できるものが存在します。chat.postMessage APIはJSON形式をサポートしているAPIです。JSON形式をサポートしているAPIは「[Methods supporting JSON POSTs](https://api.slack.com/web#methods_supporting_json)」にまとまっています。

ソースコード中ではヘッダに2つの情報を指定しています。

``` python
    headers = {
        "Content-Type": "application/json; charset=UTF-8",
        "Authorization": "Bearer {0}".format(os.environ["SLACK_BOT_USER_ACCESS_TOKEN"])
    }
```

`Content-type`はURLエンコードの形式ならば`application/x-www-form-urlencoded`に、JSONでリクエストする際には`application/json`にします。

また、JSON形式でのリクエストの場合のみ、ボットユーザの認証のためのトークンをBearer Tokenとしてヘッダーに追加する必要があります。認証トークンが`xoxa-xxxxxxxxx-xxxx`だとすれば、リクストヘッダの`Authorization`に`Bearer xoxa-xxxxxxxxx-xxxx`と指定します。（私は`Bearer`という文字列が必要であることに気づかず少々ハマってしまいました。）上記の実装では環境変数から認証トークンを取得する実装になっていますが文字列で直接指定しても構いません。ボットユーザの認証トークンは、Slack APIのサイトのサイドバーから「OAuth & 権限」ページを開き、「ボットユーザーの OAuth アクセストークン」から取得できます。

また、リクエストボディについてボットユーザの認証トークンとは別にアプリ自体の認証トークンを`token`項目に渡す必要があります。こちらのトークンはリクエストの形式に関わらず必須です。

``` python
    data = {
        "token": os.environ["SLACK_APP_AUTH_TOKEN"],
        "channel": channel,
        "text": message,
        "username": "Bot-Sample"
    }
```

上記の実装では環境変数から取得していますが文字列で直接指定しても構いません。アプリの認証トークンはSlack APIのサイトのサイドバーから「基本情報」ページを開き、「アプリの認証情報」の中の「認証トークン」から取得できます。

#### Lambda関数の環境変数にトークンをセットする
環境変数からトークンを取得する実装の場合、Lambda関数の環境変数の設定が必要になります。以下の画像の画面の環境変数欄に`SLACK_APP_AUTH_TOKEN`と`SLACK_BOT_USER_ACCESS_TOKEN`を設定しておきます。

<img width="1199" alt="スクリーンショット 2017-12-21 8.10.16.png" src="https://qiita-image-store.s3.amazonaws.com/0/216033/beb4fdb0-e1fb-54fd-649d-8288ec3d79c9.png">

#### 関数を呼び出す
作成した関数を呼び出してSlackにメッセージを投稿させましょう。コードの全体は以下のようになります。

``` python
# -*- coding: utf-8 -*-
import os
import json
import logging
import urllib.request

# ログ設定
logger = logging.getLogger()
logger.setLevel(logging.INFO)

def handle_slack_event(slack_event: dict, context) -> str:

    # 受け取ったイベント情報をCloud Watchログに出力
    logging.info(json.dumps(slack_event))

    # Event APIの認証
    if "challenge" in slack_event:
        return slack_event.get("challenge")
    
    # ボットによるイベントまたはメッセージ投稿イベント以外の場合
    # 反応させないためにそのままリターンする
    # Slackには何かしらのレスポンスを返す必要があるのでOKと返す
    # （返さない場合、失敗とみなされて同じリクエストが何度か送られてくる）
    if is_bot(slack_event) or not is_message_event(slack_event):
        return "OK"
    
    # Slackにメッセージを投稿する
    post_message_to_slack_channel("Hello, Slack Bot!", slack_event.get("event").get("channel"))

    # メッセージの投稿とは別に、Event APIによるリクエストの結果として
    # Slackに何かしらのレスポンスを返す必要があるのでOKと返す
    # （返さない場合、失敗とみなされて同じリクエストが何度か送られてくる）
    return "OK"

def is_bot(slack_event: dict) -> bool:
    return slack_event.get("event").get("subtype") == "bot_message"


def is_message_event(slack_event: dict) -> bool:
    return slack_event.get("event").get("type") == "message"
    
def post_message_to_slack_channel(message: str, channel: str):
    # Slackのchat.postMessage APIを利用して投稿する
    # ヘッダーにはコンテンツタイプとボット認証トークンを付与する
    url = "https://slack.com/api/chat.postMessage"
    headers = {
        "Content-Type": "application/json; charset=UTF-8",
        "Authorization": "Bearer {0}".format(os.environ["SLACK_BOT_USER_ACCESS_TOKEN"])
    }
    data = {
        "token": os.environ["SLACK_APP_AUTH_TOKEN"],
        "channel": channel,
        "text": message,
        "username": "Bot-Sample"
    }
    req = urllib.request.Request(url, data=json.dumps(data).encode("utf-8"), method="POST", headers=headers)
    urllib.request.urlopen(req)
    return
```

しれっと関数を2つ追加していますが、これらはメッセージの投稿イベントかどうかを判定する関数と、ボットユーザによるイベントかどうかを判定する関数になります。ボットユーザ自身のイベントもEvent APIによって送られてくるので、ボットユーザによるイベントの場合は反応しないようにしないと自分のイベントに自分で反応してメッセージを無限に投稿し続けてしまいます。

## 動作確認
こんな感じになりました！

![2017-12-21 08_28_30.gif](https://qiita-image-store.s3.amazonaws.com/0/216033/a6771ce7-8561-40f2-b634-d48f24403e63.gif)

# 終わりに
この記事ではSlackボットの作成に使えるAPIの解説と、Web APIとEvent APIを用いた簡単なSlackボットの作成の流れを紹介しました。今回作成したボットのように簡単なものならちょっとした空き時間で作れますし、開発環境の用意すら不要です。皆さんも隙間時間で面倒な業務を自動化するボットを作ってみたり、面白いボットを作ってチームの雰囲気を和やかにしてみてください。

最後に、今回時間がなくて記事に取り入れられませんでしたが、リクルートの[Proofreading API](https://a3rt.recruit-tech.co.jp/product/proofreadingAPI/)を利用してチャンネルに投稿したメッセージの日本語チェック（誤字・脱字・不自然な表現のチェック）をするボットを作成してみました。ソースコードにはできる限り丁寧に説明コメントをつけていますのでそちらも参考にしてみてください。

[誤字・脱字指摘ボット（GitHub）](https://github.com/mido-app/slack-event-api-bot-example)