[Slack] 特定チャンネルの特定キーワードに反応して、自動リアクションするSlackアプリ(Bot)を作る

Slackで自動リアクションをする仕組みをサーバーレスで作成してみました。
2021.06.09

Slackで特定のキーワードを含むメッセージに対して、自動でリアクションを付けたいことってありますよね。というわけで、実際にやってみました。

おすすめの方

  • Slackで特定チャンネルのメッセージで動くSlackアプリ(Bot)を作成したい方
  • Slackで自動でリアクションを付与したい方
  • 上記の仕組みをサーバーレスで作成したい方

ざっくり構成

SlackのEvents APIで特定チャンネル(Slackアプリが追加されたチャンネル)のメッセージを受信し、reactions.addでリアクションを付与します。

システム構成図

自動リアクションBotを作成する(準備編)

Slackアプリ作成時にWebAPIを指定するため、最初にVerify用のAPIを作成します。

SAM Init

sam init \
    --runtime python3.8 \
    --name Slack-Team-IoT-Reaction-Bot \
    --app-template hello-world \
    --package-type Zip

SAMテンプレート

template.yaml

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: Slack-Team-IoT-Reaction-Bot

Resources:
  SlackChannelSubscribeFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: src/slack_channel_subscribe/
      Handler: app.lambda_handler
      Runtime: python3.8
      Timeout: 10
      Events:
        Message:
          Type: Api
          Properties:
            Path: /message
            Method: post

  SlackChannelSubscribeFunctionLogGroup:
      Type: AWS::Logs::LogGroup
      Properties:
        LogGroupName: !Sub /aws/lambda/${SlackChannelSubscribeFunction}

Outputs:
  SlackMessageApi:
    Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/message/"

Lambdaコード

APIのURL検証が必要なので、Event APIのドキュメントに従って、受け取ったパラメータのchallengeを返しています。

app.py

import json
import logging

logger = logging.getLogger()
logger.setLevel(logging.INFO)

def lambda_handler(event, context):
    logger.info(event['body'])

    body = json.loads(event['body'])

    return {
        'statusCode': 200,
        'body': json.dumps(
            {'challenge': body['challenge']},
        ),
    }

デプロイ

sam build  --use-container

sam package \
    --output-template-file packaged.yaml \
    --s3-bucket cm-fujii.genki-deploy

sam deploy \
    --template-file packaged.yaml \
    --stack-name Slack-Team-IoT-Reaction-Bot-Stack \
    --s3-bucket cm-fujii.genki-deploy \
    --capabilities CAPABILITY_NAMED_IAM \
    --no-fail-on-empty-changeset

APIのエンドポイントを取得

下記コマンドでAPIのエンドポイントを取得します。Slackアプリ作成時に利用するため、メモしておきます。

aws cloudformation describe-stacks \
    --stack-name Slack-Team-IoT-Reaction-Bot-Stack \
    --query 'Stacks[].Outputs'

Slackアプリの作成

新規作成

Slack Appにアクセスして、アプリを新規作成します。

Slackアプリの新規作成

Slackアプリの設定

Event Subscriptionsの設定

Basic Informationにある「Event Subscriptions」を選択します。

Event APIの作成

「Request URL」にさきほどデプロイしたURLを入力し、Verify OKになることを確認します。

Event APIの設定(Verify)

続けて、Event APIにメッセージのRead権限として、message.channelsを付与します。

Event APIの設定(権限)

忘れずにSaveします。

OAuth & Permissionsの設定

「Scopes」でreactions:writeを付与します。

Slack Appの権限設定

Slackアプリをワークスペースにインストールする

「Install to Workspace」を選択してインストールします。

Slackアプリをワークスペースにインストールする

インストール後、Bot User OAuth Tokenが表示されるので、メモしておきます。これは、リアクション付与時に使用します。

任意のチャンネルにSlackアプリを追加する

Slackで任意のチャンネルを選択し、さきほどインストールしたSlackアプリを追加します。

Slackアプリをチャンネルに追加する

「Slackアプリを追加したチャンネルのメッセージ」がデプロイしたAPIに渡されます。

自動リアクションBotを作成する(リアクション編)

SSMパラメータストアにトークンを保存する

SecureStringとして保存しています。

aws ssm put-parameter \
    --type 'SecureString' \
    --name '/Slack/Toekn/Team-IoT-Reaction-Bot' \
    --value 'xoxb-xxx-yyy-zzz'

SAMテンプレート

SSMパラメータストアのRead権限やSSMパラメータストアに保存したトークンのKey名を環境変に設定しています。

template.yaml

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: Slack-Team-IoT-Reaction-Bot

Parameters:
  SlackAppTokenKey:
    Type: String

Resources:
  SlackChannelSubscribeFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: src/slack_channel_subscribe/
      Handler: app.lambda_handler
      Runtime: python3.8
      Policies:
        - arn:aws:iam::aws:policy/AmazonSSMReadOnlyAccess
      Environment:
        Variables:
          SLACK_APP_TOKEN_KEY: !Ref SlackAppTokenKey
      Timeout: 10
      Events:
        Message:
          Type: Api
          Properties:
            Path: /message
            Method: post

  SlackChannelSubscribeFunctionLogGroup:
      Type: AWS::Logs::LogGroup
      Properties:
        LogGroupName: !Sub /aws/lambda/${SlackChannelSubscribeFunction}

Outputs:
  SlackMessageApi:
    Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/message/"

Lambdaコード

Lambdaコードを下記に変更します。メッセージの内容を確認し、いいねが含まれている場合に、:good:のリアクションを付与します。

app.py

import json
import logging
import os

import boto3
import requests

logger = logging.getLogger()
logger.setLevel(logging.INFO)

SLACK_APP_TOKEN_KEY = os.environ['SLACK_APP_TOKEN_KEY']

ssm = boto3.client('ssm')

def lambda_handler(event: dict, context: dict):
    logger.info(event['body'])

    body = json.loads(event['body'])

    if is_reaction_message(body) is False:
        return

    token = get_token()
    reaction_slack(body, token, 'good')

    return {
        'statusCode': 200,
    }

def is_reaction_message(body: dict) -> bool:
    return 'いいね' in body['event']['text']

def get_token() -> str:
    res = ssm.get_parameter(
        Name=SLACK_APP_TOKEN_KEY,
        WithDecryption=True
    )
    return res['Parameter']['Value']

def reaction_slack(body: dict, token: str, name: str) -> None:
    channel = body['event']['channel']
    timestamp = body['event']['event_ts']

    headers = {
        'Content-Type': 'application/json',
        'Authorization': f'Bearer {token}'
    }

    url = 'https://slack.com/api/reactions.add'
    
    data = {
        'channel': channel,
        'name': name,
        'timestamp': timestamp
    }

    try:
        response = requests.post(url, headers=headers, data=json.dumps(data))
    except requests.exceptions.RequestException as e:
        logger.error(e)
    else:
        logger.info(response.status_code)
        logger.info(response.text)

デプロイ

--parameter-overridesを追加して、SSMパラメータストアのKey名を渡しています。

sam build  --use-container

sam package \
    --output-template-file packaged.yaml \
    --s3-bucket cm-fujii.genki-deploy

sam deploy \
    --template-file packaged.yaml \
    --stack-name Slack-Team-IoT-Reaction-Bot-Stack \
    --s3-bucket cm-fujii.genki-deploy \
    --capabilities CAPABILITY_NAMED_IAM \
    --parameter-overrides SlackAppTokenKey=/Slack/Toekn/Team-IoT-Reaction-Bot \
    --no-fail-on-empty-changeset

動作確認

Slackで「いいね」を含むメッセージを投稿すると、:good:リアクションが付与されました!!

リアクションが付いた様子

リアクションが付いた様子

さいごに

この内容を応用すれば、NGワードゲーム(日替わり)が作れそうですね。

参考