DjangoでDiscordログインを実装する

django-allauthを使いdjangoでdiscordアカウントでのログインを実装します。

今回のソースです。
https://github.com/sizumita/django-discord-allauth-test
(おまけまで全て含まれています。)

# django-allauth
django上でソーシャルログインを実装できる拡張ライブラリです。
https://github.com/pennersr/django-allauth
**なんとこちらがdiscordに対応していた！！**

## バージョン等
django==2.2.1
django-allauth==0.39.1

# discord
言わずもがな、ゲーマー用ボイスチャットアプリです。
https://discordapp.com/

# Installation
pipを使用してインストールします。（venv等を使用している場合は仮想環境に入っていることを確認してください。）

```terminal
pip install django-allauth
```

# Applicationの作成

https://discordapp.com/developers/applications/
へ飛び、Applicationを作成します。

右上にある `New Application`を押し、Applicationの名前を入力し、`Create`ボタンから作成します。
<img width="1831" alt="スクリーンショット 2019-05-18 20.46.55.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/164348/ee040552-9293-5131-afca-09dd060f58cb.png">

次に、表示された画面のCLIENT IDと　CLIENT SECRETをメモしておきます。
<img width="1028" alt="スクリーンショット 2019-05-18 20.48.19.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/164348/a0c0a4ef-f809-5790-be89-5bc5c4a32a55.png">

OAuth2を開き、REDIRECTSのAdd Redirectを押し、
`http://127.0.0.1:8000/accounts/discord/login/callback/`と入力します。
<img width="1448" alt="スクリーンショット 2019-05-18 23.22.18.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/164348/2ce22843-87cc-1400-bb15-6a6dab7708e9.png">
そうしたら、 Save Changesを押し保存します。


# Djangoでの設定
django-admin.pyもしくはPyCharmなどのツールを使い、djangoアプリケーションを作成します。（ここではプロジェクト名をappとします。）

```terminal
django-admin.py startproject app
```

作成したら、`app/settings.py`を開き、allauthの設定をします。

```app/settings.py
INSTALLED_APPS = [
    # ...

    'django.contrib.sites',
    'allauth',
    'allauth.account',
    'allauth.socialaccount',
    'allauth.socialaccount.providers.discord',
]

...

SITE_ID = 1
```

```app/urls.py
from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('accounts/', include('allauth.urls')),
]

```

以上を設定したら、ターミナルからmanage.pyと同じ階層に行き、マイグレート、スーパーユーザー作成をします。

```terminal
./manage.py migrate
./manage.py createsuperuser
```

# リダイレクトurlを指定する
初期設定だと`/accounts/discord/login/callback`に飛ばされてしまうので、`/`に飛ばす設定にします。
まず、`app`ディレクトリ内に`users`ディレクトリを作成し、そこに`adapter.py`を作成します。

```app/users/adapter.py
from allauth.account.adapter import DefaultAccountAdapter


class MyAccountAdapter(DefaultAccountAdapter):
    def __init__(self, request=None):
        super().__init__(request)

    def get_login_redirect_url(self, request):
        # リダイレクトするurl
        return 'http://127.0.0.1:8000/'

```

次に、`settings.py`, `urls.py`を編集します。

```app/settings.py

...

ACCOUNT_ADAPTER = 'app.users.adapter.MyAccountAdapter'
```

リダイレクトがうまくいったかの確認のため、`/`でリダイレクトできるページを作成します。

```app/urls.py
from django.contrib import admin
from django.urls import path, include
from django.http import HttpResponse


urlpatterns = [
    path('admin/', admin.site.urls),
    path('accounts/', include('allauth.urls')),
    path('', lambda request: HttpResponse('インデックス'), name='index'),
]
```

## 実はここでハマってました
discord側でエラーが起きて、どうしてなんだーと思って、djangoの方のログを追いかけてみたり、redirect urlを消したりしましたが、原因はdiscordの方のredirect urlを`http://127.0.0.1:8000/accounts/discord/login/callback`にしていなかったのが原因でした。

# Django起動

次に、djangoを起動します。

```terminal
./manage.py runserver 8000
```


## allauthの設定
http://127.0.0.1:8000/admin
へ行き、先ほど作ったスーパーユーザーでログインします。

<img width="973" alt="スクリーンショット 2019-05-18 21.26.07.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/164348/41a35801-732e-47f7-0fb7-38c2bb5caac2.png">
このような画面が表示されていると思います。
次に、`SOCIAL ACCOUNTS`の `Social applications`の `+Add`をクリックします。
<img width="1825" alt="スクリーンショット 2019-05-18 21.29.14.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/164348/aa89b7f5-0786-b035-5d83-f24b934f2e4f.png">

作成画面が表示されたら、以下の設定で保存してください。
`Provider`のDiscordを選択します。
`Name`, `Client id`, `Secret key` にはdiscordの方で作成したApplicationのものを入れてください。
Sitesのexample.comをダブルクリックもしくはクリックした後に右矢印ボタンを押し、右側に移動させてください。

SAVEボタンを押し、作成します。


# 実際にログインしてみる
adminからログアウトし、 http://127.0.0.1:8000/accounts/discord/login へ行ってみましょう。
discordに飛び、ログインと認証ができ、認証が完了し、インデックスと表示されれば完了です！

最初の1、2回はエラーが発生したり、リダイレクトされなかったりしますが、何回かやるとなくなります。

# さいごに
djangoでdiscordのログインをする方法を紹介しました。
allauthは初めて使いましたが、なかなか使いやすくてよかったです。
djangoでも簡単にdiscordログインが出来ました。

# おまけ: 権限を変えてみる
初期設定だと権限が'email', 'identify'の二つしか許可されていないため、増やしてみたいと思います。
settingに書く方法がわからなかったため、同じものをもう一つ用意して、そちらを改造していきます。

## discordのプロバイダーを自作する
まず、以下のファイル構成で`discord`ディレクトリを作成します。

```
app
|
+-app/
|
+-discord/
|
+-manage.py
```

discord ディレクトリの中に、`__init__.py`, `provider.py`, `urls.py`, `views.py`を作成します。
`__init__.py`は何も書かないでください。

```discord/provider.py
from allauth.socialaccount.providers.base import ProviderAccount
from allauth.socialaccount.providers.oauth2.provider import OAuth2Provider
from django.conf import settings


class DiscordAccount(ProviderAccount):
    def to_str(self):
        dflt = super(DiscordAccount, self).to_str()
        return self.account.extra_data.get('username', dflt)


class DiscordProvider(OAuth2Provider):
    id = 'discord'
    name = 'Discord'
    account_class = DiscordAccount

    def extract_uid(self, data):
        return str(data['id'])

    def extract_common_fields(self, data):
        return dict(
            email=data.get('email'),
            username=data.get('username'),
            name=data.get('username'),
        )

    def get_default_scope(self):
        # ここを変更することでsettingから読み込めるようにする
        return settings.DISCORD_SCOPES


provider_classes = [DiscordProvider]
```

```discord/urls.py
from .provider import DiscordProvider
from allauth.socialaccount.providers.oauth2.urls import default_urlpatterns


urlpatterns = default_urlpatterns(DiscordProvider)
```

```discord/views.py
import requests

from .provider import DiscordProvider
from allauth.socialaccount.providers.oauth2.views import (
    OAuth2Adapter,
    OAuth2CallbackView,
    OAuth2LoginView,
)


class DiscordOAuth2Adapter(OAuth2Adapter):
    provider_id = DiscordProvider.id
    access_token_url = 'https://discordapp.com/api/oauth2/token'
    authorize_url = 'https://discordapp.com/api/oauth2/authorize'
    profile_url = 'https://discordapp.com/api/users/@me'

    def complete_login(self, request, app, token, **kwargs):
        headers = {
            'Authorization': 'Bearer {0}'.format(token.token),
            'Content-Type': 'application/json',
        }
        extra_data = requests.get(self.profile_url, headers=headers)

        return self.get_provider().sociallogin_from_response(
            request,
            extra_data.json()
        )


oauth2_login = OAuth2LoginView.adapter_view(DiscordOAuth2Adapter)
oauth2_callback = OAuth2CallbackView.adapter_view(DiscordOAuth2Adapter)
```

## settingの追記
`app/settings.py`に、許可したいスコープの一覧のlistを書きます。また、自分で作成したdiscordのアダプターを読み込みます。

```app/settings.py

INSTALLED_APPS = [
    # 'allauth.socialaccount.providers.discord',
    'discord',
]

...

DISCORD_SCOPES = ['email', 'identify', 'connections', 'guilds', 'messages.read']
```

これで完成です。自分の好きなスコープを追加してください。

### 設定できるスコープの一覧
<details><summary>ここをクリックして表示</summary><div>

#### email
メールアドレスにアクセスします。

#### identify
ユーザー名とアバター画像にアクセスします。

#### connections
サードパーティのアプリケーションに接続します。

#### guilds
あなたがどのサーバーに入っているか確認します。

#### guilds.join
サーバーに参加します
- 招待を代わりに行います

#### gum.join
あなたの代わりにグループDMに参加します。
- これによりアプリはあなたに代わって作成されたグループDMに参加します。

#### rpc 
Discordクライアントとインターフェイスで接続
- ローカルのdiscordクライアントとの接続を許可するアプリケーションです。

#### rpc.api
Discordクライアント上でアクションを実行してください。
- これにより、アプリがメッセージを送信すること、設定を変更することコマンドを実行することなどが可能になります。

#### rpc.notifications.read
Discordクライアントへの通知を受信する
- これにより、アプリはあなた宛の通知を見ることができます。

#### webhook.incoming
チャンネルにWebhookを追加します。
- アプリがチャンネルにメッセージを送信することを許可します。

#### messages.read
全てのメッセージを読み込む
- これによりアプリはあなたのアカウントがアクセスできる全てのメッセージを読むことができます。

#### applications.builds.upload
Upload and manage builds

#### applications.builds.read
Read build information

#### applications.store.update
Manage store SKUs, listings, and assets

#### applications.entitlements
Manage entitlements
- This allows the app to read and consume entitlements for applications as you on Discord's store
<img width="983" alt="スクリーンショット 2019-05-19 0.44.41.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/164348/08e6a1a1-d785-09ce-ba0c-1d4b44fd3677.png">
</div></details>

## 例
`DISCORD_SCOPES = ['email', 'identify', 'connections', 'guilds', 'messages.read']`
<img width="631" alt="スクリーンショット 2019-05-19 1.31.21.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/164348/5d0b9822-512e-7164-9e72-55d07427ae8a.png">