自作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]
> ここで中断する。