4
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

【AWS・Python】SlackのEventAPIを利用してファイルの送受信してみる

Last updated at Posted at 2020-07-25

1. はじめに

本記事では、Slack へのファイルアップロードをトリガーとして、
ファイルの受け取りと何かしらの処理をして Slack へ送り返す処理についてまとめます。

環境は AWS Lambda と Python を利用します。

2. 実装

2.1 できるもの

ファイルをアップロードすると、
アップロードしたユーザにメンションして、何かしらの処理を加えたファイルを返します。

スクリーンショット 2020-07-25 17.11.47.png

2.2 処理のイメージ図

Slack → AWS → Slack へのデータの流れです。

_qiita_slack_event_api.png

  1. Slack 上でファイルがアップロードされると、API Gateway に設定したエンドポイントが呼ばれる
  2. API Gateway が Lambda を起動
  3. 処理用の Lambda をさらに起動し、
  4. 最初の Lambda は、処理を返す
  5. 最後に呼ばれた Lambda がファイルを処理してメッセージとともに Slack へアップロード

3にて別の Lambda を起動するのは、Slack へのレスポンスを3秒以内に返す必要があるためです参考

2.3 slack上での準備

Slack の App を管理しているページのEvent Subscriptionの項目を有効にしておきましょう。

スクリーンショット 2020-07-25 17.22.16.png

加えて、Incoming Webhooksの項目も有効にしておいてください。

2.4 Chaliceでの実装

Chaliceとは、AWS Lambdaやそれに付随するサービスを簡単に構築してくれる AWS 公式のライブラリです。

2.4.1 SlackからのEventを受け取るLambda(API Gateway + Lambda)の実装

まずは、Slack からのEventを受け取る Lambda を実装します。
処理の内容は以下の通りです。

  1. Slack からのpayloadを受け取る
  2. BOT からの送信でなければ、ファイルを処理する Lambda を起動
  3. 最後に、Slack から受け取ったpayloadを返す

3をしている理由としては、Slack から送信されたpayloadに含まれるchallengeパラメータを返す必要があるためです。
これをしていない場合は、2.3 項で設定した URL がVerifiedになりません。

app.py
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()の関数へ渡せる引数としてfilecontentの2種類があります。
違いは以下の通りです。

  • file: ファイル名を指定して、アップロードする。例:file="test.csv"
  • content: bytes オブジェクトをアップロードする。例:content=json.dumps({"aaa": "bbb"}).encode('utf-8')

どちらでも行けるので便利な方を利用してください。
ちなみに、Lambda だと/tmp以下の領域は500MBくらいまで自由に読み書きができるので、そこを利用したら良いかと思います。

app.py
# =============================== #
# ここには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 SubscriptionRequest 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 SubscriptionIncoming Webhooksの項目を利用していると、
Bot の権限が足りずに関数の実行が失敗します。

その場合は、Slack App の以下のページから必要そうな権限を追加していってください。
関数の実行に必要な権限は実行時に失敗したら、エラーメッセージに含まれています。

スクリーンショット 2020-07-25 17.55.56.png

2.6.2 LambdaがLambdaを実行する権限を付与

Lambda から Lambda を呼ぶ権限も IAM に付与する必要があります。
AWS コンソールから、Lambda に付与されているロールを選び、「ポリシーをアタッチします」ボタンから
AWSLambdaFullAccessを付与してください(権限が強すぎるので本当はよくないのですが)。

スクリーンショット 2020-07-25 18.10.25.png

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にファイルをアップロードする】が参考になるかもです。

参考

4
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?