【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からリマインドが自分宛に飛んできます。

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

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

1.2 Chain Of Responsibility とは

Chain Of Responsibilityとは、

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

です。

詳しくはデザインパターン ~Chain of Responsibility~(引用元)をご覧ください。

1.3 GitPod とは

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

2. 実装

2.1 できるもの

以下のようなhogeコマンドを打つと

先頭に[hoge]を付与した文字列を返してきます。

上に見えるのが、ユーザーが打った文字列(=/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に関するキー

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

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

2.3.2 Slackに関するキー

slack apiのアプリを作成します。
そこにSlash CommandsBotsの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(参考)を叩いて対象の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に公開してあります。

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

2.5 Chain Of Responsibility の部分

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

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

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()のみ実装すればよいです。


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の処理が実行されます。

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に載せてあるプログラムでは、以下のコマンドを受けれるようにしてあります。

  • /Hoge
  • /Fuga

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

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

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

2.7 実行環境(再掲)

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

3. おわりに

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

よいSlackライフを!!

A. おまけ

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