## 概要
APIにリクエストしたユーザーにDMでSlack通知するAPIをつくってみる。
> [!warning]
> プロダクションセキュアな実装ではないので注意。
## プロジェクト作成
```console
uv init drf-slack-notification
cd drf-slack-notify
rm .python-version main.py README.md
mise use
[email protected]
```
### 依存パッケージの追加
```
$ uv add django==4.2 djangorestframework "psycopg[binary,pool]" django-stubs
Installed 11 packages in 93ms
+ asgiref==3.10.0
+ django==4.2
+ django-stubs==5.2.7
+ django-stubs-ext==5.2.7
+ djangorestframework==3.16.1
+ psycopg==3.2.10
+ psycopg-binary==3.2.10
+ psycopg-pool==3.2.6
+ sqlparse==0.5.3
+ types-pyyaml==6.0.12.20250915
+ typing-extensions==4.15.0
```
### プロジェクトとアプリの作成
```console
v
django-admin startproject drf_slack_notification
cd drf_slack_notification
django-admin startapp main
```
構成はこうなる。
```
drf-slack-notification
├── drf_slack_notification
│ ├── __init__.py
│ ├── asgi.py
│ ├── main
│ │ ├── __init__.py
│ │ ├── admin.py
│ │ ├── apps.py
│ │ ├── migrations
│ │ │ └── __init__.py
│ │ ├── models.py
│ │ ├── tests.py
│ │ └── views.py
│ ├── settings.py
│ ├── urls.py
│ └── wsgi.py
├── manage.py
├── mise.toml
├── pyproject.toml
└── uv.lock
```
## 不要ファイルの削除
必要な情報に集中してシンプルにしたいので不要ファイルを削除する。といっても `main` から `admin.py` と `tests.py` を削除しただけなのでほとんど変わっていない。
```
drf-slack-notification
├── drf_slack_notification
│ ├── __init__.py
│ ├── asgi.py
│ ├── main
│ │ ├── __init__.py
│ │ ├── apps.py
│ │ ├── migrations
│ │ │ └── __init__.py
│ │ ├── models.py
│ │ └── views.py
│ ├── settings.py
│ ├── urls.py
│ └── wsgi.py
├── manage.py
├── mise.toml
├── pyproject.toml
└── uv.lock
```
## [[Django REST framework|drf]]とDBの設定
### Pythonファイルの設定
`settings.py` の `INSTALLED_APPS` に `rest_framework` を追加。
```python
INSTALLED_APPS = [
"rest_framework",
]
```
`settings.py` の設定を変更。
```python
DATABASES = {
"default": {
"ENGINE": "django.db.backends.postgresql",
"NAME": "drfdb",
"USER": "postgres",
"PASSWORD": "password",
"HOST": "127.0.0.1",
"PORT": "15432",
}
}
```
### DBコンテナの設定
[[PostgreSQL]]に関するファイルを追加。
`docker-compose.yml`
```yaml
services:
db:
image: postgres:18
container_name: postgres-drf
ports:
- 15432:5432
volumes:
- ./docker-entrypoint-initdb.d:/docker-entrypoint-initdb.d
environment:
POSTGRES_PASSWORD: password
```
`docker-entrypoint-initdb.d/init.sql`
```sql
CREATE DATABASE drfdb;
\c drfdb;
```
### DBの起動
起動。
```console
docker compose up -d
```
### マイグレーション
```console
python manage.py makemigrations
python manage.py migrate
```
## ユーザー取得APIの作成
### ユーザーの追加
```console
python manage.py createsuperuser --username Minerva --email
[email protected]
```
### コードの実装
`main/serializers.py`
```python
from django.contrib.auth.models import User
from rest_framework import serializers
class UserSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = User
fields = ["url", "username", "email"]
```
`main/views.py`
```python
from django.contrib.auth.models import User
from rest_framework import permissions, viewsets
from drf_slack_notification.main.serializers import UserSerializer
class UserViewSet(viewsets.ModelViewSet):
queryset = User.objects.all()
serializer_class = UserSerializer
permission_classes = [permissions.IsAuthenticated]
```
`urls.py`
```python
from django.urls import include, path
from rest_framework import routers
from drf_slack_notification.main import views
router = routers.DefaultRouter()
router.register(r"users", views.UserViewSet)
urlpatterns = [
path("", include(router.urls)),
path("api-auth/", include("rest_framework.urls", namespace="rest_framework")),
]
```
### 動作確認
```console
python manage.py runserver
```
`GET http://localhost:8000/users/`
```json
[
{
"url": "http://localhost:8000/users/2/",
"username": "Minerva",
"email": "
[email protected]"
}
]
```
## Slackとの連携について
APIレベルでの下準備は以下で調査済。
<div class="link-card-v2">
<div class="link-card-v2-site">
<img class="link-card-v2-site-icon" src="https://publish-01.obsidian.md/access/35d05cd1bf5cc500e11cc8ba57daaf88/favicon-64.png" />
<span class="link-card-v2-site-name">Minerva</span>
</div>
<div class="link-card-v2-title">
📜2025-10-13 Slackアプリと連携してアプリからユーザーに通知できるようにする
</div>
<div class="link-card-v2-content">自作SaaSプロダクトでSlack通知機能をAPIレベルで実現するため、OAuth 2.0フローを用いてBotトークンやユーザートークンを取得し、Slackアプリ作成やリダイレクトURI設定、chat.postMessageやconversations.open APIを利用した通知方法、必要なスコープ設定、ユーザー通知情報の保存方法などを検証した。</div>
<img class="link-card-v2-image" src="https://publish-01.obsidian.md/access/35d05cd1bf5cc500e11cc8ba57daaf88/Notes/attachments/activity.webp" />
<a data-href="📜2025-10-13 Slackアプリと連携してアプリからユーザーに通知できるようにする" class="internal-link"></a>
</div>
%%[[📜2025-10-13 Slackアプリと連携してアプリからユーザーに通知できるようにする]]%%
### プロダクトSlack連携有効のシーケンス図
```mermaid
sequenceDiagram
autonumber
actor Administrator
participant Web
participant API
participant DB
participant SlackAuth
participant SlackAPI
Administrator ->> Web: 「Slack連携を有効にする」
Web ->> SlackAuth: GET https://slack.com/oauth/v2/authorize<br/>client_id, scope=chat:write:im:write, redirect_uri
SlackAuth ->> Administrator: 同意画面
Administrator ->> SlackAuth: 同意する
SlackAuth ->> Web: 302 リダイレクト<br/>code
Web ->> API: POST /oauth/bot/callback<br/>code
API ->> SlackAPI: GET /oauth.v2.access<br/>code, clientId, clientSecret
SlackAPI ->> API: app_id, user_id(Slack), access_token, bot_user_id, team_id
API ->> DB: INSERT t_slack_bot<br/>tenant_id, team_id, app_id, access_token
DB ->> API: success
API ->> Web: success
Web ->> Administrator: success
```
### ユーザーごとのSlack通知有効のシーケンス図
```mermaid
sequenceDiagram
autonumber
actor AuthedUser
participant Web
participant API
participant DB
participant SlackAuth
participant SlackAPI
AuthedUser ->> Web: 「Slack通知を有効にする」
Web ->> SlackAuth: GET https://slack.com/oauth/v2/authorize<br/>client_id, user_scope=openid, redirect_uri
SlackAuth ->> AuthedUser: 同意画面
AuthedUser ->> SlackAuth: 同意する
SlackAuth ->> Web: 302 リダイレクト<br/>code
Web ->> API: POST /oauth/user/callback<br/>code
API ->> SlackAPI: GET /oauth.v2.access<br/>code, clientId, clientSecret
SlackAPI ->> API: app_id, user_id(Slack), access_token, team_id
API ->> DB: INSERT t_slack_user<br/>tenant_id, user_id, team_id, user_id(Slack)
DB ->> API: success
API ->> Web: success
Web ->> AuthedUser: success
```
### Slack通知のシーケンス図
```mermaid
sequenceDiagram
autonumber
participant API/Lambda
participant DB
participant SlackAuth
participant SlackAPI
API/Lambda ->> DB: SELECT t_slack_user<br/>WHERE tenant_id, user_id = 通知ユーザーID
DB ->> API/Lambda: team_id, user_id(Slack), channel_id
API/Lambda ->> DB: SELECT t_slack_bot<br/>WHERE tenant_id, team_id
DB ->> API/Lambda: access_token
alt channel_idが登録されていなかった
API/Lambda ->> SlackAPI: POST /conversations.open<br/>users = 通知ユーザーID, Bearer(access_token)
SlackAPI ->> API/Lambda: channel_id
API/Lambda ->> DB: UPDATE t_slack_user(channel_id)<br/>WHERE tenant_id, user_id = 通知ユーザーID
DB ->> API/Lambda: success
end
API/Lambda ->> SlackAPI: POST /chat.postMessage<br/>channel_id, message, Bearer(access_token)
SlackAPI ->> API/Lambda: success
```
## ユーザー拡張テーブルの追加
> [!todo]
>