1. はじめに
本記事では、Slack へのファイルアップロードをトリガーとして、
ファイルの受け取りと何かしらの処理をして Slack へ送り返す処理についてまとめます。
環境は AWS Lambda と Python を利用します。
2. 実装
2.1 できるもの
ファイルをアップロードすると、
アップロードしたユーザにメンションして、何かしらの処理を加えたファイルを返します。
2.2 処理のイメージ図
Slack → AWS → Slack へのデータの流れです。
- Slack 上でファイルがアップロードされると、API Gateway に設定したエンドポイントが呼ばれる
- API Gateway が Lambda を起動
- 処理用の Lambda をさらに起動し、
- 最初の Lambda は、処理を返す
- 最後に呼ばれた Lambda がファイルを処理してメッセージとともに Slack へアップロード
3
にて別の Lambda を起動するのは、Slack へのレスポンスを3秒以内に返す必要があるためです参考
2.3 slack上での準備
Slack の App を管理しているページのEvent Subscription
の項目を有効にしておきましょう。
加えて、Incoming Webhooks
の項目も有効にしておいてください。
2.4 Chaliceでの実装
Chalice
とは、AWS Lambda
やそれに付随するサービスを簡単に構築してくれる AWS 公式のライブラリです。
2.4.1 SlackからのEventを受け取るLambda(API Gateway + Lambda)の実装
まずは、Slack からのEvent
を受け取る Lambda を実装します。
処理の内容は以下の通りです。
- Slack からの
payload
を受け取る - BOT からの送信でなければ、ファイルを処理する Lambda を起動
- 最後に、Slack から受け取った
payload
を返す
3
をしている理由としては、Slack から送信されたpayload
に含まれるchallenge
パラメータを返す必要があるためです。
これをしていない場合は、2.3 項で設定した URL がVerified
になりません。
import io
import json
import logging
import os
import requests
from slack import WebClient
from slack.errors import SlackApiError
import boto3
app = Chalice(app_name='<your chalice-app name>')
logger = logging.getLogger()
logger.setLevel(logging.INFO)
# lambda client to invoke
lambda_client = boto3.client("lambda")
BOT_USER_ID = "<appのuser_id>"
@app.route('/your/root', methods=['POST'])
def event_subscription():
request = app.current_request
if request.raw_body is None:
# 予期しない呼び出し。400 Bad Requestを返す
return {'statusCode': 400}
payload = request.json_body
logger.info(f"payload:= {payload}")
user_id = payload['event']['user_id']
# BOTからファイルを上た場合は、Lambdaでの処理をしないようにする
# ループを避けるため
if user_id == BOT_USER_ID:
return payload
event_handler_lambda = "<この次に実装するLambdaのARN>"
lambda_client.invoke(
FunctionName=event_handler_lambda,
InvocationType='Event',
Payload=json.dumps(payload)
)
return payload
2.4.2 API ファイルを処理するLambdaの実装
次は、アップロードされたファイルに対して処理をする Lambda を実装します。
Slack へファイルがアップロードされると、file_id
が発行されます。
そのfile_id
からファイルを取得し、処理を行っていきます。
また、そのfile_id
にはファイルがアップロードされたchennel_id
が付与されているので
これを利用してファイルをアップロードするチャンネルを指定します。
ユーザへのメンションは<@{user_id}>
で行えます。
user_id
は、ファイルがアップロードされたイベントに付与されているのでそれを取得して行います。
ファイルの処理は、今は適当に行っているので、いい感じに変更してください。
client.files_upload()
の関数へ渡せる引数としてfile
とcontent
の2種類があります。
違いは以下の通りです。
- file: ファイル名を指定して、アップロードする。例:
file="test.csv"
- content: bytes オブジェクトをアップロードする。例:
content=json.dumps({"aaa": "bbb"}).encode('utf-8')
どちらでも行けるので便利な方を利用してください。
ちなみに、Lambda だと/tmp
以下の領域は500MB
くらいまで自由に読み書きができるので、そこを利用したら良いかと思います。
# =============================== #
# ここには2.4.1項で実装した内容がある想定 #
# =============================== #
@app.lambda_function()
def event_handler(event, _):
# globalで読み込むとslackへのレスポンスに3秒以上かかるため、ここでpandasを読み込む
import pandas as pd
# tokenとslack_clientの生成
slack_oauth_token = os.environ['OAuthAccessToken']
slack_bot_token = os.environ['BotUserOAuthAccessToken']
oauth_client = WebClient(token=slack_oauth_token)
bot_client = WebClient(token=slack_bot_token)
# call file info to get url
file_id = event['event']['file_id']
logger.info(f"file_id:= {file_id}")
response = oauth_client.files_info(file=file_id)
file_name = response.data["file"]["name"]
file_url = response.data["file"]["url_private"]
# slack上でファイル共有をした場合は1つのchannel_idしか入らないため
file_upload_channel_id = response.data['file']['channels'][0]
user_id = event['event']['user_id']
logger.info(f"file from {user_id}")
print("Downloaded " + file_name)
# download file
file_data = _get_slack_file_bytes(slack_token=slack_oauth_token, file_url=file_url)
converted_data = b''
if file_name.endswith(".json"):
sent_data = json.loads(file_data)
sent_data['accepted'] = "hello, world"
converted_data = json.dumps(sent_data).encode('utf-8')
elif file_name.endswith(".csv"):
df = pd.read_csv(io.BytesIO(file_data))
df['accepted'] = "world"
converted_data = df.to_csv(index=False).encode('utf-8')
# response
try:
chat_response = bot_client.chat_postMessage(
channel=file_upload_channel_id,
text=f"<@{user_id}> {file_name} is accepted :tada:"
)
logger.info(f"chat_response:= {chat_response}")
upload_response = bot_client.files_upload(
content=converted_data,
title=f"converted_{file_name}",
filename=f"converted_{file_name}",
initial_comment="here is your file",
channels=file_upload_channel_id
)
logger.info(f"upload_response:= {upload_response}")
except SlackApiError as e:
# You will get a SlackApiError if "ok" is False
assert e.response["error"]
print(e)
print(e.__str__())
return {"ok": True}
def _get_slack_file_bytes(slack_token: str, file_url) -> bytes:
r = requests.get(file_url, headers={'Authorization': f'Bearer {slack_token}'})
# get binary content
return r.content
2.5 LambdaのdeployとSlackへのURL設定
ここまできたら、chalice
のコマンドで AWS に実装して、発行される URL を取得します。
$ chalice deploy
Creating deployment package.
Updating policy for IAM role: <chalice_project_name>-dev
Updating lambda function: <chalice_project_name>-dev-event_handler
Updating lambda function: <chalice_project_name>-dev
Updating rest API
Resources deployed:
- Lambda ARN: arn:aws:lambda:<region>-<account_number>:function:<chalice_project_name>-dev-event_handler
- Lambda ARN: arn:aws:lambda:<region>-<account_number>:function:<chalice_project_name>-dev
- Rest API URL: https://<chalice_generated_chars>.execute-api.<region>.amazonaws.com/api/
API Gateway の URL を取得したら、2.3 項でのEvent Subscription
のRequest URL
に設定します。
この時、プログラムの@app.route(/your/root, ...)
に設定した/your/root
を追記します。
例で、/your/root
としましたが、/slack/event
などが良いと思います。
https://<chalice_generated_chars>.execute-api.<region>.amazonaws.com/api/your/root
2.6 実行時の注意
2.6.1 Slack Bot Userの実行権限付与
おそらく、初めてEvent Subscription
やIncoming Webhooks
の項目を利用していると、
Bot の権限が足りずに関数の実行が失敗します。
その場合は、Slack App の以下のページから必要そうな権限を追加していってください。
関数の実行に必要な権限は実行時に失敗したら、エラーメッセージに含まれています。
2.6.2 LambdaがLambdaを実行する権限を付与
Lambda から Lambda を呼ぶ権限も IAM に付与する必要があります。
AWS コンソールから、Lambda に付与されているロールを選び、「ポリシーをアタッチします」ボタンから
AWSLambdaFullAccess
を付与してください(権限が強すぎるので本当はよくないのですが)。
2.6.3 Slackでファイルを実際にルームに共有される前にLambdaが実行されてしまう。
file
に関する Slack のEvent Subscription
のうち、file_upload
を選択してしまうと、
ルームに共有しようとしているファイルが Slack 上のサーバーへアップロードが完了した時点で、Event が発火します。
処理そのものに影響はないのですが、挙動としてちょっと気持ち悪いのでfile_public
の Event が発火した場合に
Lambda などの処理を行った方が良いです。
3. おわりに
今回は、Slack を利用してファイルの送受信を行えるようにしてみました。
ファイル以外にも Slack のEvent Subscription
には、スタンプが押されたら
とかメンションがあったら
とかいろいろあるのでぜひ遊んでみてください。
Event Subscription
では、イベントのタイプを{..., 'event': {'type': 'file_public', 'file_id': '', ...}}
と言う
dict の'type'で受け取れるので、以前書いたChainOfRespontibilityでゴニョゴニョとかも利用できると思います。
Slack が提供している公式ライブラリのSlackClientが
かなり使いやすく、自分で独自の関数を組む必要がないので便利でした。
また、Event Subscription
の設定方法などは【Slackにファイルをアップロードする】が参考になるかもです。