はじめに こんにちは、ニフティ インフラシステムグループ社内情報システムチームの仲上です。 先日、非エンジニアでもメール配信サービスから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への登録説明は省略します。 ファイル構成 │ ┣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 # 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 を使用する際は以下のように書きます。 # app.<アクション>("<modalIDまたはアクションID>")(ack=<ack関数名>, lazy=[<非同期で実行したい関数>]) # ↓はmodalのsubmitボタンが押されたときのアクション app.view("modal-id")(ack=ack, lazy=[post_template_details]) serverless.yaml 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 を宣言すると自動でパッケージの圧縮・デプロイまで行ってくれます。 デプロイ $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の開発は非常に柔軟性・拡張性が高いので、みなさんも是非試してみてください! We are hiring! ニフティでは、さまざまなプロダクトへ挑戦するエンジニアを絶賛募集中です! ご興味のある方は以下の採用サイトよりお気軽にご連絡ください! ニフティ株式会社採用情報 Tech TalkやMeetUpも開催しております こちらもお気軽にご応募ください! Event – NIFTY engineering