はじめに
こんにちは、ニフティ インフラシステムグループ社内情報システムチームの仲上です。
先日、非エンジニアでもメール配信サービスからAPI経由で情報を取得できるように、Slack boltを使用してbotを作成しました。しかし、情報の取得・整形処理が想定より重く、エラーが頻発するようになってしまいました。その際にlazy listenerを使用してこの問題を解決したことについて紹介しようと思います。
Slack bolt と lazy listener
今回Slack botを作成するにあたって、機能の充実さと導入が容易な点から公式ライブラリのSlack boltを使用しました。
https://slack.dev/bolt-python/ja-jp/tutorial/getting-started
Slack boltを使用した場合、Slackへの応答には3秒以内返信という制約があります。処理が重かったり、ネットワークの問題などでレスポンスを返すのに3秒以上かかってしまった場合、Slack側でエラー処理されてしまします。そこでslack boltに搭載されているlazy listener を使用します。この機能を使うことにより、重い処理を非同期で実行できるので、タイムアウトによるエラーを回避することができます。
https://slack.dev/bolt-python/api-docs/slack_bolt/lazy_listener/index.html
lazy_listener を使用することで処理結果が出るより先にSlackへの応答を返すことができ、重い処理を非同期で実行することができます。
今回つくったもの
メール配信サービス(SendGrid)に登録されている情報をAPI経由で取得し、Slackに表示するbotを作りました。
SendGridとは
SendGridとは、クラウドベースのメール配信サービスです。こちらは世界的に利用されている高い到達率を誇るサービスで、APIを叩いてメールを送信することができます。また、SendGridにはテンプレート機能というものがあります。これはメールの送信元や件名、本文などを予め設定しておくことができる機能で、APIのパラメータに本文内の埋め込み情報などを渡すだけで定型文を送ることができます。
今回このテンプレートに登録されている情報を誰でも確認できるようにしたかったため、Slack botを作成しました。
構成図
Slackのグローバルショートカットをクリックするだけで使用できるようにしました。
SlackのショートカットがクリックされるとLambdaがSendGridのAPIを叩きテンプレートに関する情報を取得します。取得した情報はLambdaでSlack用のメッセージに整形され、メッセージが送信されます。
環境
- Python 3.9
- serverless framework 3.31.0
- serverless-python-requirements 6.2.3
- aws cli 2.12.4
- slack-bolt 1.18.0
- slack-sdk 3.21.3
実装
Slack appの登録やLambdaへの登録説明は省略します。
ファイル構成
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
│ ┣lib #ライブラリ保存用ディレクトリ ┣formatter.py ┣modal.py └sendgrid.py ┣slack #マニュフェスト保存用ディレクトリ ┣manifest-dev.yml └manifest-prod.yml ┣app.py ┣package-lock.json ┣package.json ┣requirements.txt └serverless.yml |
app.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 |
# Serverless Python Requirements を使用するためにimpoertします try: import unzip_requirements except ImportError: pass import os import logging from datetime import datetime # slack bot関係 from slack_bolt import App, Ack, Say, BoltContext from slack_sdk import WebClient # 独自関数 from lib.formatter import ( format_template_list, format_template_content, ) from lib.sendgrid import ( get_template_list, get_template_id, get_template_content, ) from lib.modal import get_template_detail_modal app = App( # Settings > Basic Information > App Credentials > Signing Secret で取得可能 signing_secret=os.environ.get("SLACK_SIGNING_SECRET"), # Settings > Install App で取得可能 token=os.environ.get("SLACK_BOT_TOKEN"), # AWS Lambdaで動作させるために、以下の設定を有効にします。 process_before_response=True, ) # 登録されているテンプレート一覧(テンプレート名、件名)を返す関数 def post_template_list(body: dict, client: WebClient) -> None: user_id = body["user"]["id"] template_list = get_template_list() templates_length = len(template_list) send_message = format_template_list(template_list) comment = f"<@{user_id}>\\nテンプレートは以下 {templates_length}件 が設定されています" client.files_upload( channels=os.environ["SLACK_CHANNEL"], content=send_message, filename=f"{datetime.now().strftime('%Y%m%d%H%M%S')}_RTMテンプレート一覧.tsv", filetype="tsv", initial_comment=comment, ) # 応答用 def ack(ack, body): ack() # 「realtime-mail-テンプレート一覧取得」が押されたとき、post_template_list を非同期で実行 app.shortcut("get-template-list")(ack=ack, lazy=[post_template_list]) # 「realtime-mail-テンプレート詳細取得」が押されたとき、検索項目入力用のモーダルを表示 @app.shortcut("get-template-detail") def send_modal(ack: Ack, body: dict, client: WebClient): ack() client.views_open(trigger_id=body["trigger_id"], view=get_template_detail_modal()) # モーダルのsubmitボタンが押されたとき、テンプレートの詳細を返す def post_template_details( view: dict, logger: logging.Logger, say: Say, client: WebClient, payload: dict, context: BoltContext, ) -> None: # 入力値の取得 user_id = context.get("user_id", "") inputs = view["state"]["values"] input_value = ( inputs.get("input-block", {}).get("number-input-action", {}).get("value", 0) ) # テンプレートの検索 template_list = get_template_list() template_info = get_template_id(input_value, template_list) template_id = template_info.get("id", "") # テンプレートがなかった場合 if template_id == "": client.chat_postMessage( channel=os.environ["SLACK_CHANNEL"], text=f"""<@{user_id}> 検索内容:{input_value} 検索結果 > 入力された番号で登録されたテンプレートはありませんでした。 """, ) return # テンプレートの内容を取得して変換 template_content = get_template_content(template_id) send_message = format_template_content(template_content, template_id) # テンプレート名を格納 template_name = template_info.get("template_name", "") comment = f"""<@{user_id}> 検索内容:{input_value} 検索結果 > テンプレート番号:{template_name[:4]} > 件名:{send_message.get("subject", "")} > 最終更新日:{send_message.get("updated_at", "")} """ client.files_upload( channels=os.environ["SLACK_CHANNEL"], content=send_message.get("plane_contents", ""), filename=f"{template_name[:4]}_{datetime.now().strftime('%Y%m%d%H%M%S')}.txt", filetype="text", initial_comment=comment, ) # modalのsubmitボタンが押されたとき、post_template_details を非同期で実行します app.view("modal-id")(ack=ack, lazy=[post_template_details]) # アプリを起動します(デバッグ用) if __name__ == "__main__": app.start() # --lamda用の設定 ここから-- from slack_bolt.adapter.aws_lambda import SlackRequestHandler # ロギングを AWS Lambda 向けに初期化します SlackRequestHandler.clear_all_log_handlers() logging.basicConfig(format="%(asctime)s %(message)s", level=logging.DEBUG) # AWS Lambda 用handler def handler(event, context): slack_handler = SlackRequestHandler(app=app) return slack_handler.handle(event, context) |
lasy_listener を使用する際は以下のように書きます。
1 2 3 |
# app.<アクション>("<modalIDまたはアクションID>")(ack=<ack関数名>, lazy=[<非同期で実行したい関数>]) # ↓はmodalのsubmitボタンが押されたときのアクション app.view("modal-id")(ack=ack, lazy=[post_template_details]) |
serverless.yaml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
frameworkVersion: "3" useDotenv: true service: supsys-mailrelay-slackapp provider: name: aws runtime: python3.9 region: ap-northeast-1 iam: role: statements: - Effect: Allow Action: - lambda:InvokeFunction - lambda:InvokeAsync Resource: "*" environment: SERVERLESS_STAGE: ${opt:stage, 'dev'} SLACK_SIGNING_SECRET: ${env:SLACK_SIGNING_SECRET} SLACK_BOT_TOKEN: ${env:SLACK_BOT_TOKEN} SENDGRID_API_KEY: ${env:SENDGRID_API_KEY} SLACK_CHANNEL: ${env:SLACK_CHANNEL} functions: app: handler: app.handler name: supsys-mailrelay-slackapp-${sls:stage} events: - httpApi: path: /slack/events method: post package: patterns: - "!.venv/**" - "!node_modules/**" - "!.idea/**" plugins: - serverless-python-requirements custom: pythonRequirements: zip: true slim: true useDownloadCache: false useStaticCache: false |
lambdaでは標準搭載されているパッケージ以外は自分で入れる必要があります。これらを手動で行う場合、
- パッケージをローカルにダウンロード
- zipファイルで圧縮
- lambdaに転送
と非常に手間がかかります。
そこでserverless-python-requirements
を使うことでこれらの手間を低減します。このプラグインはrequirements.txt
を宣言すると自動でパッケージの圧縮・デプロイまで行ってくれます。
デプロイ
1 2 3 4 5 6 |
$env:SLACK_SIGNING_SECRET='XXXXXXXX' $env:SLACK_BOT_TOKEN='xoxb-XXXXXXXX' $env:SLACK_CHANNEL='XXXXXXXXXXX' # 送信先SlackチャンネルのID $env:SENDGRID_API_KEY='SG.XXXXXX.XXXXXXXXX'D serverless deploy |
動作確認
テンプレート一覧
テンプレート一覧を選択します。
登録されているテンプレート一覧がDMで送られてくることが確認できました。
テンプレート詳細
テンプレート詳細を選択します。
表示されたmodalに管理番号を入力して送信します。
DMでテンプレートの内容が送られてくることが確認できました(画像はテスト用テンプレートです)
おわりに
このbotを開発したことにより、sendgridのアカウントを持っていなくてもSlackからSencGridのテンプレートを見れるようになりました!今までは問い合わせベースで対応していたので、対応の時間を大きく減らすことができました。
Slack bolt + severless framework を使ったbotの開発は非常に柔軟性・拡張性が高いので、みなさんも是非試してみてください!