sey323’s blog

すーぱーえんじにあ

Slackに投稿されたファイルをGoogleDriveに保存するBotをPythonで作成

はじめに

Slackにファイルが投稿されたことを検知して、投稿されたファイルをGoogleDriveに保存するBotを作りました。

3年ほど前にSlackBotを作った経験があったのですが、その際に利用していたAPIがLegacyAPIとして非推奨になっており、新しいSlackAPIに苦戦したので備忘録として記録します。

動作としてはこんな感じ。Slackにファイルを投稿したら、GoogleDriveに保存され、保存先のURLを返してくれるBotを作成する。

完成イメージ

Slack開発コンソールでBotの作成

開発を始める前に、Slackの開発用コンソールでBotなどの設定を行う。

アプリケーションの作成

下記のURLにアクセスして新しいSlackアプリを作成する。

アクセスした後に画面赤枠の「Create NewApp」を押下する。ポップアップが表示されたら、「From scrach」を選択する

アプケーションの作成

アプリケーション名を入力するエリアが表示されるので、アプケーション名とアプリを利用するワークスペースを入力する。その後「Create App」を押下する。

これでアプリケーションの作成は完了。アプリケーションの作成後、管理画面に遷移する。

今後は、このアプリケーションの管理画面でBotの設定や認証トークンの発行をする。

Socketモードの有効化

管理画面のサイドバーより「Scket Mode」を押下する。その後「Enable Socket Mode」のトグルボタンをオンにする。

SocketModeの有効化

ポップアップが表示されるので、任意のトークン名を入力し、「Generate」を押下する。

トークン名の入力

生成が完了しトークンが表示される。このトークンはSlackAPIを利用するために必要なトークンとなるため、控えておく。

トークンの生成完了画面

これでSocketモードが有効になり、SlackへのイベントをSlackAPI経由でPythonのプログラムが検知できるようになる。

検知するイベントの設定

Socketモードの設定後、プログラムから検知するイベントを許可する設定を行う。イベントとは、Slackの「チャンネルに投稿された」「ファイルが投稿された」「チャンネルがアーカイブになった」などの動作のことを指している。

今回は「チャンネルへの投稿」または「Botに対してメンションをつけた投稿」に含まれるファイルを全て保存の対象としたいので、それらのイベントを検知できるように設定を入れる。

はじめに、サイドバーの「Event Subscriptions」を押下する。その後画面下部の「Enable Events」のトグルボタンを「On」に切り替える。

権限の追加後の画面

その後「Subscribe to bot events」の「Add Bot User Event」のボタンから、「app_mentions」「message.channels」のイベントを追加し、「Save Changes」を押下する。

イベントの詳細は、下記の公式ドキュメントを参照

api.slack.com

Botの作成とBot権限追加

Botの作成を行う。サイドバーの「App Home」の「Edit」を押下する。

Botの作成

ポップアップが表示されるので、任意の名称を入力し「Add」を押下するとBotが作成される。

Botの詳細入力

次にBotに対して権限を設定する。サイドバーから「OAuth & Permissions」を選択する。その後「Bot Token Scopes」まで移動し、「Add an OAuth Scope」を押下する。 

権限の設定画面

そこで「files:read」と「chat:write」「channels:history」を選択する。

スクリーンショット 2022-11-05 17.27.23.png

インストールとBotトークンを取得

最後に、これまで行ってきた設定をSlackにインストールする。サイドバーの「Install App」を選択し、「Install to Workspace」より、アプケーションのインストールを開始する。

「Install to Workspace」の初期画面

認証画面が表示されるので、「Allow」を押下することでインストールが完了する。

認証画面

インストールの完了後、下記のようにBot User OAuth Tokenが発行される。こちらはBotの実装でも利用するので控えておく。

インストールが完了しトークンが発行の完了

以上の作業で前準備は完了。Scoketモードを有効化した時に発行されたトークンと、ボットの作成の時に発行されたトークンは、アプリケーションの作成の際に利用するので、控えておく。

実装

以上の設定をもとにBotの開発を行う。開発に利用した環境は以下の通り。

必要ライブラリのインストール

SlackBotを作成するために必要なslack-boltと、GoogleDriveの保存に必要なPyDrive2をインストールする。

pip install PyDrive2==1.14.0 slack-bolt==1.15.2

github.com

github.com

GoogleDriveにファイルをアップロードする処理

下記の記事を参考に、GoogleDriveに保存するクラスGoogleDriveFacadeを作成する。

qiita.com

ファイル名は GoogleDriveFacade.py とする。

import os

from pydrive2.auth import GoogleAuth
from pydrive2.drive import GoogleDrive


class GoogleDriveFacade:
    def __init__(self, setting_path: str = "settings.yaml") -> None:
        gauth = GoogleAuth(setting_path)
        gauth.LocalWebserverAuth()

        self.drive = GoogleDrive(gauth)

    def create_folder(self, folder_name):
        ret = self.check_files(folder_name)
        if ret:
            folder = ret
            print(f"{folder['title']}: exists")
        else:
            folder = self.drive.CreateFile(
                {"title": folder_name, "mimeType": "application/vnd.google-apps.folder"}
            )
            folder.Upload()

        return folder

    def check_files(
        self,
        folder_name,
    ):
        query = f'title = "{os.path.basename(folder_name)}"'

        list = self.drive.ListFile({"q": query}).GetList()
        if len(list) > 0:
            return list[0]
        return False

    def upload(
        self,
        local_file_path: str,
        save_folder_name: str = "sample",
        is_convert: bool = False,
    ):

        if save_folder_name:
            folder = self.create_folder(save_folder_name)

        file = self.drive.CreateFile(
            {
                "title": os.path.basename(local_file_path),
                "parents": [{"id": folder["id"]}],
            }
        )
        file.SetContentFile(local_file_path)
        file.Upload({"convert": is_convert})

        drive_url = f"https://drive.google.com/uc?id={str( file['id'] )}"
        return drive_url

SlackBotの実装

最後にSlackBotの処理をmain.pyとして作成する。

from datetime import datetime

import requests
from slack_bolt import App
from slack_bolt.adapter.socket_mode import SocketModeHandler

from GoogleDriveFacade import GoogleDriveFacade

SLACK_API_TOKEN = ${API Token(Socketモードを有効化した時に発行されたトークン)}
SLACK_BOT_USER_TOKEN = ${Bot User OAuth Token(Botを作成した時に発行されたトークン)}


app = App(token=SLACK_BOT_USER_TOKEN)
google_drive = GoogleDriveFacade()


@app.event({"type": "message", "subtype": "file_share"})
def upload_image(event, say):
    # 投稿元スレッドに返信
    thread = say(thread_ts=event["event_ts"], text=f"ファイルをアップロードします")
    
    # スレッドからファイルをダウンロード
    filename = download_from_slack(
        event["files"][0]["url_private_download"], SLACK_BOT_USER_TOKEN
    )
    
    # Google Driveにファイルをアップロード
    drive_url = google_drive.upload(filename, "slack_upload") 
    
    # アップロードURLを返す。
    thread = say(
        thread_ts=thread["ts"],
        text=f"アップロードが完了しました。 URL: {drive_url}",
    )


def download_from_slack(download_url: str, auth: str) -> str:
    """Slackから画像をダウンロードして保存し、保存したパスを返す。

    Args:
        download_url (str): 画像のURL
        auth (str): 画像の閲覧に必要なSlackの認証キー

    Returns:
        str: 画像が保存されているパス
    """
    img_data = requests.get(
        download_url,
        allow_redirects=True,
        headers={"Authorization": f"Bearer {auth}"},
        stream=True,
    ).content

    filename = f"sample_{datetime.now().strftime('%Y%m%d%H%M%S')}.png"
    with open(filename, "wb") as f:
        f.write(img_data)

    return filename


if __name__ == "__main__":
    handler = SocketModeHandler(app, SLACK_API_TOKEN)
    handler.start()

悩みポイント1: ファイルが添付されているメッセージを検知

下記のアノテーションを入れる。

@app.event({"type": "message", "subtype": "file_share"})

@app.event()アノテーションはSlackのほとんどのEventを検知できるので便利。検知したいイベントはSlackの開発者コンソールのEvent Subscriptionで設定する必要があるので注意。

api.slack.com

悩みポイント2: メッセージに投稿されたファイルのダウンロード

slack botfile:readのアクセス権をつけ、slack bot user tokenを認証ヘッダーに含めてリクエストする。

def download_from_slack(download_url: str, auth: str) -> str:
    """Slackから画像をダウンロードして保存し、保存したパスを返す。

    Args:
        download_url (str): 画像のURL
        auth (str): 画像の閲覧に必要なSlackの認証キー

    Returns:
        str: 画像が保存されているパス
    """
    img_data = requests.get(
        download_url,
        allow_redirects=True,
        headers={"Authorization": f"Bearer {auth}"},
        stream=True,
    ).content

ググってみると過去のLegacyAPIでファイルをダウンロードするパターンしかなく、新しいSlackAPIで実装している例が少なく苦戦した。bot user tokenを認証キーに入れるんだな、とはわかっても、slack botfile:readのアクセス権をつけるのを忘れがち。

悩みポイント3: 設定完了後の「reinstall」「save changes」 押し忘れ

技術的な内容ではないが、開発者コンソールで設定を変更した後は「save changes」を押下し、アプリケーションのインストールをしなおさないと変更内容が反映されない。

何回かこの反映忘れで詰まっていたので、開発中におかしいなと思ったら再確認を。

動作確認

下記のコマンドで実行する。認証情報が間違っていたら実行後にエラーが表示される。問題がなければ「Bolt app is running!」と表示され、Botが起動する。

python main.py

⚡️ Bolt app is running!

起動が完了したら、Slackに画像を投稿してみる。その後、ファイルのアップロードが始まったことと、アップロードが完了したことがスレッドに通知されたら動作としては正常。

実行結果

スレッドにはアップロードしたGoogleDriveへのリンクが表示されるので、そちらからも保存されていることを確認する。

GoogleDriveに保存されていることを確認

おわりに

LegacyAPIと比較して、学習コストは上がったが、使いこなせば痒いところにも手が届きそう、といった印象。セールスフォースに買収されたことを受けて、エンタープライズ向けに調整したという感じだろうか。

参考

api.slack.com

qiita.com