自作SaaSプロダクトでイベント発生時に[[Slack]]へ通知を行う仕組みを実現するため、APIレベルでの実現可能性を考える。 - SaaSプロダクトと[[Slack]]の管理者が、[[Slack]]通知機能を開放できる - SaaSプロダクト管理者画面から、[[Botトークン (Slack)|Botトークン]]を登録させる[[OAuth 2.0]]フロー - 通知先情報があれば通知できる状態にする - SaaSプロダクトの各ユーザーが、自分への通知を[[Slack]]に変更できる - SaaSプロダクトの設定画面から、[[ユーザートークン (Slack)|ユーザートークン]]の[[OAuth 2.0]]フローを通して、通知先情報をSaaSプロダクトのDBに登録 - [[Botトークン (Slack)|Botトークン]]が参照して通知する ## 通知する 通知先(ユーザーID)は分かっている前提で通知するところまでのフロー。 ### Slackアプリの作成 まずは、[[Slackアプリを作成]]する。 - `App Name:` drf-slack-notification ### [[リダイレクトURI]]の設定 [[Slack]]の[[OAuth]]で[[リダイレクトURI]]として指定するエンドポイントを作成する。実際はWebのエンドポイントになるが、今回の作業でWebpを介する予定はないので適当で良い。 ``` https://localhost:8000 ``` ![[2025-10-13-00-17-40.avif]] ### リクエストしてcodeを取得する [[認可リクエスト (OAuth 2.0)|認可リクエスト]]をしてみる。 ``` https://slack.com/oauth/v2/authorize?client_id=${clientId}&user_scope=chat:write&redirect_uri=https%3A//localhost%3A8000 ``` ![[2025-10-13-00-26-21.avif]] `Allow` を押す。エラーっぽい表示になるが、URLにはちゃんと `code` が渡ってきている。 ``` https://localhost:8000/?code=${code}&state= ``` ### codeから[[アクセストークン (OAuth 2.0)|アクセストークン]]を取得する `POST https://slack.com/api/oauth.v2.access` にリクエストする。 - [[Basic認証]] - `Username` -> `<Client ID>` - `Password` -> `<Client Secret>` - Body ([[x-www-form-urlencoded]]) - `code` -> `<code>` 以下のような感じになればOK。 ```json { "ok": true, "app_id": "...", "authed_user": { "id": "...", "scope": "chat:write", "access_token": "xoxp-...", "token_type": "user" }, "team": { "id": "...", "name": "..." }, "enterprise": null, "is_enterprise_install": false } ``` ### 通知してみる `POST https://slack.com/api/chat.postMessage` - [[Bearer認証]] - `Token` -> `<authed_user.access_token>` - Body (json) - 以下 ```json { "channel": "<authed_user.id>", "blocks": [ { "type": "section", "text": { "type": "mrkdwn", "text": "てすと" } } ] } ``` 通知はされるが、DMに自分で投稿したみたいになってしまう。 ## ユーザーのDMではなくアプリのDMとして ### [[認可リクエスト (OAuth 2.0)|認可リクエスト]]のやりなおし 先程は `user_scope` に指定していたため[[ユーザートークン (Slack)|ユーザートークン]]を取得してしまっていたためと思われる。 ``` https://slack.com/oauth/v2/authorize?client_id=${clientId}&user_scope=chat:write&redirect_uri=https%3A//localhost%3A8000 ``` `scope` を指定して[[Botトークン (Slack)|Botトークン]]を取得する。 ``` https://slack.com/oauth/v2/authorize?client_id=${clientId}&scope=chat:write&redirect_uri=https%3A//localhost%3A8000 ``` [[Botトークン (Slack)|Botトークン]]のパーミッションがなさそう。 ![[2025-10-13-08-40-44.avif]] ### Bot Token Scopesを設定する botとしてはDMに送信できればいいので、最小限の権限として `chat:write` をつける。 ![[2025-10-13-11-02-41.avif]] 再び[[認可リクエスト (OAuth 2.0)|認可リクエスト]]を行う。 ``` https://slack.com/oauth/v2/authorize?client_id=${clientId}&scope=chat:write&redirect_uri=https%3A//localhost%3A8000 ``` 今度は平気そう。 ![[2025-10-13-11-06-42.avif]] ### codeからアクセストークンを取得する 先程と同じ手順。 `POST https://slack.com/api/oauth.v2.access` にリクエストする。 - [[Basic認証]] - `Username` -> `<Client ID>` - `Password` -> `<Client Secret>` - Body ([[x-www-form-urlencoded]]) - `code` -> `<code>` 先ほどとはレスポンス形式が少し異なる。 ```json { "ok": true, "app_id": "A...", "authed_user": { "id": "U...", }, "scope": "chat:write", "token_type": "bot", "access_token": "xoxb-...", "bot_user_id": "U...", "team": { "id": "T...", "name": "..." }, "enterprise": null, "is_enterprise_install": false } ``` ### 通知してみる `POST https://slack.com/api/chat.postMessage` - [[Bearer認証]] - `Token` -> `<access_token>` - Body (json) - 以下 ```json { "channel": "<authed_user.id>", "blocks": [ { "type": "section", "text": { "type": "mrkdwn", "text": "てすと2" } } ] } ``` `U` 開始のユーザーIDでも、`D`開始のDM channel IDでも通知できる。返却結果の `channel` に `D` 開始のDM channel IDが返却されるので、今後はこれを使ったほうがよさそう。 ```json { "ok": true, "channel": "D...", "ts": "...", "message": { ... } } ``` > [!caution] > botがjoinしていないchannelだと **200** で以下が返却されるので注意。 > > ```json > { > "ok": false, > "error": "not_in_channel" > } > ``` ## ユーザーへの通知情報を保存する 認可リクエストに [openid scope](https://docs.slack.dev/reference/scopes/openid) を指定する。 ``` https://slack.com/oauth/v2/authorize?client_id=${clientId}&user_scope=openid&redirect_uri=https%3A//localhost%3A8000 ``` ![[2025-10-13-18-59-00.avif]] [[リダイレクトURI]]の `code` を取得して、[[アクセストークン (OAuth 2.0)|アクセストークン]]を取得する。 ```json { "ok": true, "app_id": "A...", "authed_user": { "id": "U...", "scope": "openid", "access_token": "xoxp-...", "token_type": "user" }, "team": { "id": "T..." }, "enterprise": null, "is_enterprise_install": false } ``` この時点で `authed_user.id` を保存すれば必要最低限の処理は終わり。`channel_id` の取得タイミングによって処理が分岐する。どちらがいいかは微妙。 - **`channel_id` を登録時に確定させる** - このあとに `POST /conversations.open` を実行する - トークンは `authed_user.access_token` ではなく [[Botトークン (Slack)|Botトークン]] - ユーザーIDをDBに登録しないならここでやる方が合理的 - 通知時に比べて処理が分散されるので [[Rate Limit]] にかかりにくい - **`channel_id` を送信時に確定させる** - 登録処理はここで終了できる - 通知後に `POST /conversations.open` へのアクセスや **DB更新が必要** - ただし、`channel_id` が登録済の場合は不要 (初回だけ) - `POST /conversations.open` がエラーになったとき `user.id` でフォールバックができる - 意味があるのかは謎 - 複雑性は上がってしまう - [[Botトークン (Slack)|Botトークン]]の利用箇所が送信時のみになるからシンプルな気はする ### トークンの種類を判別する方法 - `authed_user.token_type` が `"user"` なら[[ユーザートークン (Slack)|ユーザートークン]] - `token_type` が `"bot"` なら[[Botトークン (Slack)|Botトークン]] プロパティのパスがバラバラなので判別はクリーンではない。 ## conversations.open を使ってみる `POST /conversations.open` にリクエストするとパーミッションが足りていないため以下のような結果が返却される。 **ステータスコードは200なので注意**。 ```json { "ok": false, "error": "missing_scope", "needed": "channels:write,groups:write,mpim:write,im:write", "provided": "chat:write", "warning": "missing_charset", "response_metadata": { "warnings": [ "missing_charset" ] } } ``` `OAuth & Permissions` で `im:write` scopeを追加する。 ![[2025-10-13-18-18-54.avif]] `scope` に `im:write` を追加して[[認可リクエスト (OAuth 2.0)|認可リクエスト]]を行う。 ``` https://slack.com/oauth/v2/authorize?client_id=<client_id>&scope=chat:write,im:write&redirect_uri=https%3A//localhost%3A8000 ``` codeからアクセストークンを取得(詳細は省略)し、以下のbodyで `POST /conversations.open` にもう一度リクエストする。 ```json { "users": "<authed_user.id>" } ``` 以下のように結果が返るので `channel.id` を保存してメッセージ送信に利用する。 ```json { "ok": true, "no_op": true, "already_open": true, "channel": { "id": "D..." }, "warning": "missing_charset", "response_metadata": { "warnings": [ "missing_charset" ] } } ``` > [!attention] > ここで中断する。