Yuigo・ん・・・湿っぽい

モルック、システムエンジニア、その他趣味。大学生のころから使っているので、昔の記事は恥ずかしいし今の思想とは異なっていることが多いです。

Python3 + AWS LambdaでSlackbotをつくる(自分用メモ)

概要

会社のSlackに忍ばせました。しかしどうやって実装したのかまったく覚えていないため、改めて参考サイトを見ながら書き残すことにした。自分が不便だなと感じたことを中心に増築していく予定。

完成形

ワークスペースにチャンネルが作成されたときにお知らせする。

また、カスタムemojiが追加された際にも通知することにした。

参考

当時はいろんなところを見ていたのだけど、だいたいここに書いてあった…

AWS(API Gateway + Lambda(Python)) + Slack APIを使ったBot作成

実装メモ

だいたい以下の順番で作業した(ような気がする)

  • API GatewayとかLambdaのセッティング
  • Slack Appの作成
  • コーディング

簡単な構成図。

API GatewayとかLambdaのセッティング

Lambdaの作成

Lambdaを作成する。作成方法は「一から作成」でOK。関数の名前(任意)とランタイムを選択する。今回はPython3.7を選択した。

権限については、予めAWSLambdaBasicExecutionRoleを付与したロールを作成し、それをアタッチする。

コーディングは後で行うので一旦そのまま。

API Gatewayの作成・設定

LambdaにAPI Gatewayをトリガーとして追加する。新規APIをセキュリティはオープンで作成し、さらに任意のAPI名、任意のステージ名を記入する。

これを追加してLambda画面で保存すると、API Gatewayの設定が確定する。

作成したAPI Gatewayの設定画面から、メソッドの作成を行う。POSTメソッドにしておく。「ANY」メソッドは削除してよい。統合タイプに「Lambda関数」、Lambda関数名にさっき作ったものを入力する(候補が出てくるはず)。

作成したPOSTメソッドを選択し、「アクション」から「APIのデプロイ」を選択する。ステージはさっき入力したものと同じ名前で書く。

デプロイすると「URLの呼び出し」が表示される。このアドレスをコピーする。

Lambdaの設定

Lambdaの画面に戻り、API Gatewayを選択し、2つある設定のうち、APIエンドポイントの上のグレーの文字列が途中{/*/*}となっている方を削除して、Lambda画面右上の「保存」を押下する。

Slack Appの作成

Appはこのページ内で管理する。

https://api.slack.com/apps

Create New Appから、新しいAppの名前と動作するワークスペースを選ぶ。

Bot Userの作成

サイドバーのBot Usersを選択し、任意のDisplay nameDefault usernameを記入して、更にAlways Show My Bot as OnlineをONにして保存する。

Event Subscriptionsの設定

Slackで発生したイベントをAPI Gatewayに送るため設定する。

設定をONにし、API Gateway設定時に生成された「URLの呼び出し」のアドレスを貼り付ける。疎通確認してくれるので、失敗したときはここまでの設定を見直す。

「Subscribe to Bot Events」に、トリガーとしたいイベントを追加する。今回は、チャンネルの作成とemojiの追加なので、それぞれchannel_createdemoji_changedを追加した。Save Changesを押下。

「Install App」へ移動し、「Install App to Workspace」を押す。確認がでるのでそのまま進むとBotが作成される。

コーディング

Lambdaにコードを書いてく。必要に応じて環境変数を使用する。

一旦一部をマスクしてクソコードを全部載せる。コメントにだいたい書いたはず。

なお、コード右上の「ハンドラ」にはlambda_function.handle_slack_eventと入力する。これは{ファイル名}.{開始メソッド名}となっているので適宜変更しても大丈夫なはず。

# -*- 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):
        return "OK"

    if is_channel_created_event(slack_event):
        post_channel_created(slack_event)

    if is_emoji_added_event(slack_event):
        post_emoji_added(slack_event)

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


def post_channel_created(slack_event: dict):
    # チャンネル作成イベントの通知

    # 作成されたチャンネルの名前を取得
    channel_name = slack_event.get("event").get("channel").get("name")

    # Slackにメッセージを投稿する
    post_message_to_slack_channel(
        f"新しいチャンネルが作成されました: #{channel_name}",
        os.environ["DIST_CHANNEL_CH_CREATED"])

    return


def post_emoji_added(slack_event: dict):
    # emoji追加イベントの通知

    emoji_name = slack_event.get("event").get("name")

    post_message_to_slack_channel(
        f"emojiが追加されました! :{emoji_name}: `{emoji_name}`",
        os.environ["DIST_CHANNEL_EMOJI_ADDED"])

    return


def is_bot(slack_event: dict) -> bool:
    # botからのメッセージの場合true
    return slack_event.get("event").get("subtype") == "bot_message"


def is_channel_created_event(slack_event: dict) -> bool:
    # 発生したイベントがチャンネル作成の場合true
    return slack_event.get("event").get("type") == "channel_created"


def is_emoji_added_event(slack_event: dict) -> bool:
    # 発生したイベントがemoji追加の場合true
    return slack_event.get("event").get("type") == "emoji_changed" and slack_event.get("event").get("subtype") == "add"


def post_message_to_slack_channel(message: str, channel: str, link_names="true"):
    # 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"])
    }

    # "link_name: true"によって#や@のリンク化を有効化
    data = {
        "token": os.environ["SLACK_APP_AUTH_TOKEN"],
        "channel": channel,
        "text": message,
        "username": os.environ["BOT_NAME"],
        "link_names": link_names
    }

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

環境変数は以下のようになっている。上2つは察してほしい。下2つはSlack Appの設定画面から拾うことができるのでそれを記入する。

コード下の方の設定は今の所変更していない。