【AWS・Python】Slash Commandsを拡張性高く実装する

# 1. はじめに

本記事ではデザインパターンの一種であるChain Of Responsibilityを利用して、
SlackのSlash Commandsを拡張性高く実装する方法を紹介します。

Slash Commandsの処理を受け取るサーバーはAWSを、言語はPythonを利用します。

手軽に試せるようにコマンド3つでAWSへデプロイできるGitPod環境も用意してあります。

## 1.1 Slash Commands とは

Slack CommandsとはSlackに独自のコマンドを追加し、そのコマンドにより任意の処理を実行できるものです。
Slackが既に用意しているものもあり、有名なのは`/remind`などでしょうか。

```
/remind me tomorrow 11:00 meeting
```

と打てば、以下のような表示ととも明日の11時にslack botからリマインドが自分宛に飛んできます。
![62EBF581-B936-412F-9D23-902BAAD5E851.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/260295/33a315d7-9dfc-699a-aee6-63a8a19c7e36.jpeg)

このようなコマンドをSlash Commandsを利用することで、独自に追加実装が可能です。

Slash Commandsについての詳しい設定方法などはこちらをご覧ください。

* [公式ドキュメント](https://api.slack.com/interactivity/slash-commands)
* [Developers.ioの記事](https://dev.classmethod.jp/cloud/aws/slash-commands-train-delay/)
* [Slack Appを無料で運用する（Netlify Functions）](https://qiita.com/hide2018/items/7734dbdcfb80b1b348b0#_reference-dfdad7e4c325e8187924)

## 1.2 Chain Of Responsibility とは

Chain Of Responsibilityとは、

> * Chainという英単語は鎖、Responsibilityという英単語は責任、つまりChain of Responsibilityは、責任の連鎖という意味になります。実際にはたらい回しを行う構造と考えた方が分かりやすいです。
> * Chain of Responsibilityパターンは、複数のオブジェクトを鎖で繋いでおき、そのオブジェクトの鎖を順次渡り歩いて目的のオブジェクトを決定する方式です。
> * 人に要求がやってくる、その人がそれを処理できるなら処理する。処理できないならその要求を「次の人」にたらい回しにする。以降繰り返し・・・。これがChain of Responsibilityパターンです。
> * GoFのデザインパターンでは、振る舞いに関するデザインパターンに分類されます。

です。

詳しくは[デザインパターン ～Chain of Responsibility～（引用元）](https://qiita.com/i-tanaka730/items/073c106c58d7c74c1706)をご覧ください。


## 1.3 GitPod とは

GitHubアカウントがあれば、無料から利用できるクラウドIDEです。
PCだけではなく、iPadなどからも利用可能です。
便利です。詳しくは、[こちらの記事](https://qiita.com/Lilly008000/items/dbb64871480cf1a06acf)を参考にしてください。

# 2. 実装

## 2.1 できるもの

以下のような`hoge`コマンドを打つと
![4549AD22-F4F0-459F-8828-C9EE8561B273.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/260295/eb6514a7-bb4b-d6c6-c17c-c77062d431eb.jpeg)

先頭に[hoge]を付与した文字列を返してきます。
![51F5D529-4902-41CB-B718-DFE42573AC44.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/260295/4852ffbd-2250-2ee9-d298-008b266460b1.jpeg)

上に見えるのが、ユーザーが打った文字列（＝`/hoge あああ`）で、したに表示されているのが（＝`[hoge]あああ`）がSlash Commandsによって返される文字列です。

このような任意の処理を実行できるコマンドを、拡張性高く実装していくことができます。

## 2.2 手順

1. GitPodに環境変数を設定
2. AWS環境に処理をDeploy
3. Slash Commandsを作成し、紐付ける
4. 完成

## 2.3 準備

Slash Commandsを受けるサーバーをAWSへデプロイするために必要なキーは以下の6つです。

* AWS
    * `AccessKey`
    * `SecretAccessKey`
    * `region`
* Slack
    * `BotUserAuthToken`
    * `Slack Verification Token`
    * `Channel ID`

### 2.3.1 AWSに関するキー

`AccessKey`と`AccessSecretKey`は`IAM`のユーザーから発行することができます。
また、`region`は東京の場合は`ap-northeast-1`を設定してください。

（注：間違ってもAWSのキーをGitHubのリポジトリにpushしないでください。
漏洩した場合、マイニングするためのリソース構築が勝手に行われて課金対象になってしまうことがあります。）

### 2.3.2 Slackに関するキー

`slack api`のアプリを作成します。
そこに`Slash Commands`と`Bots`のfeatureを追加し、以下の2つのキーを取得します。

* Basis Infromation -> App Credintials -> `Verification Token`
* OAuth & Permissions -> OAuth Tokens & Redirect URLs -> `Bot User OAuth Access Token`

`Channel ID`は、`Channel Name`と異なるので注意してください。
slack apiのスラックにあるチャンネルを表示するAPI（[参考](https://api.slack.com/methods/channels.list)）を叩いて対象の`Channel ID`を取得するか、2chの内容を表示させたいチャンネルから`Slash Command`を実行し、AWSのCloudWatch Logsから`Channel ID`を取得しても良いかと思います。


### 2.3.3 GitPodの環境変数に設定

GitPodの`Environemnt Variables`に6つの環境変数を以下の名前で設定してください。また、以下の環境変数はGitPod環境で自動で読み込むために、`Name`を定めていますが、プログラムを書き換えれば任意の`Name`で実行できます。


| Name | Value |
|:--:|:--:|
| `aws_access_key` | `AccessKey` |
| `aws_secret_access_key` | `SecretAccessKey` |
| `region` | `region` |
| `slack_2ch_channel_id` | `Channel ID` |
| `slack_oauth_access_token` | `BotUserAuthToken` |
| `slack_token` | `Slack Verification Token` |

## 2.4 プログラム

プログラムは、[GitHub](https://github.com/gsy0911/ChatAppForQiita)に公開してあります。

また、GitPod環境は以下から利用可能です（GitHubアカウントは必要です。）

[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#github.com/gsy0911/ChatAppForQiita)

## 2.5 Chain Of Responsibility の部分

親クラスである`CommandExecutor`クラスの`handle_execute()`にて、処理を自クラスが担うか、担わない（＝他のクラスの任せる）かを決めています。

自クラスで処理を担う場合は`execute()`を実行し、担わない場合は次のクラスの`handle_execute()`に処理をまかすようにしています。

```python
class CommandExecutor:
    
    # ~ 中略 ~

    def handle_execute(self, params) -> dict:
        """
        入力された`params`の実行元（`command`）に拠って
        実行する処理を変更する関数

        1. 呼ばれたクラスの名前(self.name)がcommandと等しい場合、処理を行う
          * デコード処理が正しく行われた場合は、処理結果を返し、【終了】
          * 正しく行われなかった場合は、異常という処理結果を返し、【終了】

        2. 呼ばれたクラスの名前がcommandと異なる場合、次のクラスに処理を任せる : self.next.execute(params)
          * 次のクラスがある場合、処理が【続く】
          * 次のクラスがない場合、異常という処理結果を返し、【終了】

        """
        # 対象のCommandかどうか確認
        if self.check_responsibility(params):
            result_dict = self.execute(params)
            if len(result_dict) > 0:
                # メッセージの処理に成功した場合は処理結果を返す
                return result_dict
            else:
                # メッセージの処理に失敗した場合は例外を投げる
                raise ChainOfResponsibilityException

        elif self.next_executor is not None:
            # 対象のCommandでは無い場合、次のexecutorに任せる
            # [Chain of Responsibility]の部分
            return self.next_executor.handle_execute(params)
        else:
            return {"error": "Could not execute your command. Check your {/command} name"}

    def execute(self, params) -> dict:
        """
        各クラスで担うべき処理を実装する
        """
        pass
```

また、以下は`/fuga`というコマンドの処理を担うクラスです。
`handle_execute()`は親クラスで実装済みですので、各クラスは`execute()`のみ実装すればよいです。

```python

from chalicelib.CommandExecutor import CommandExecutor


class Fuga(CommandExecutor):
    def __init__(self):
        super().__init__("/fuga")

    def execute(self, params: dict) -> dict:
        # x-www-form-urlencodedではjsonのパラメータがリストでデコードされるため
        text = params['text'][0]
        self.logger.info(f"text:={text}")
        
        # slash commandから直接レスポンスを返す
        return {
            "response_type": "in_channel",
            "text": f"[fuga]{text}"
        }

```

また、処理の順序は`CommandExecutorRequestHandler`クラスにて実装してあります。呼び出す側でこのクラスを起点に実行を開始すれば、次に`Hoge`の処理が、最後に`Fuga`の処理が実行されます。

```python
class CommandExecutorRequestHandler(CommandExecutor):
    """
    最初にメッセージを受け取り、処理を開始するクラス
    このRequestHandlerではメッセージの受け取りを担うのみで
    実際の処理を行うのはset_next()内に含まれるインスタンスが処理を行う
    """
    def __init__(self):
        """
        self.set_next({instance}).set_next({instance})
        のように数珠繋ぎに関数をつなげる
        """
        super().__init__("RequestHandler")
        # commandに拠って出力する内容を切り替える
        self.set_next(Hoge()).set_next(Fuga())

```

## 2.6 拡張方法

[GitHub](https://github.com/gsy0911/ChatAppForQiita)に載せてあるプログラムでは、以下のコマンドを受けれるようにしてあります。

* `/Hoge`
* `/Fuga`

例えば、ここに`/Piyo`コマンドを実装したい場合はどうすればよいのでしょうか？
やることは以下の3つです。

1. `CommandExecutor`クラスを継承した新たに`Piyo`クラスを作成する
2. `Piyo`クラスに`execute()`を実装する
3. `CommandExecutorRequestHandler`クラスの`__init__()`に`.set_next(Piyo())`を加える

他のコマンドも同様の手順で新たなコマンドを加えることができます。

## 2.7 実行環境（再掲）

また、GitPod環境はこちらから利用可能です（GitHubアカウントは必要です。）

[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#github.com/gsy0911/ChatAppForQiita)


# 3. おわりに

Slack APIやAWSのアクセスキーの準備などがすこし手間ですが、
そこが終われば、誰でも簡単にSlash Commandsを実装することができます！！

よいSlackライフを！！

# A. おまけ

12月の初旬にバズった[こちらの記事](https://qiita.com/peisuke/items/80984db8b47cd8243019)の`/2ch`コマンドも簡単に実装することができます。
（GitHubのソースコードに既に一部が実装されており、デプロイと同時に利用可能です。これであなたのslackにも2ch環境が！！）