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」のトグルボタンをオンにする。

ポップアップが表示されるので、任意のトークン名を入力し、「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」を押下する。
イベントの詳細は、下記の公式ドキュメントを参照
Botの作成とBot権限追加
Botの作成を行う。サイドバーの「App Home」の「Edit」を押下する。

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

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

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

インストールとBotのトークンを取得
最後に、これまで行ってきた設定をSlackにインストールする。サイドバーの「Install App」を選択し、「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
GoogleDriveにファイルをアップロードする処理
下記の記事を参考に、GoogleDriveに保存するクラスGoogleDriveFacadeを作成する。
ファイル名は 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で設定する必要があるので注意。
悩みポイント2: メッセージに投稿されたファイルのダウンロード
slack botにfile: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 botにfile:readのアクセス権をつけるのを忘れがち。
悩みポイント3: 設定完了後の「reinstall」「save changes」 押し忘れ
技術的な内容ではないが、開発者コンソールで設定を変更した後は「save changes」を押下し、アプリケーションのインストールをしなおさないと変更内容が反映されない。
何回かこの反映忘れで詰まっていたので、開発中におかしいなと思ったら再確認を。
動作確認
下記のコマンドで実行する。認証情報が間違っていたら実行後にエラーが表示される。問題がなければ「Bolt app is running!」と表示され、Botが起動する。
python main.py ⚡️ Bolt app is running!
起動が完了したら、Slackに画像を投稿してみる。その後、ファイルのアップロードが始まったことと、アップロードが完了したことがスレッドに通知されたら動作としては正常。

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

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