クラスDiscordに課題管理Botを導入した話

こんばんは。[りーぜんと](http://twitter.com/50m_regent)です。

題名の通りです。経緯↓

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">Teams絶対課題見逃すから課題管理用discordBOTつくる</p>&mdash; りーぜんと (@50m_regent) <a href="https://twitter.com/50m_regent/status/1260525008624836610?ref_src=twsrc%5Etfw">May 13, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

流行りのオンライン授業が弊学でも始まりました。草ですね。

もはやオンライン授業の代名詞となっているZoomではなくMicrosoftのTeamsというものを使って授業が行われています。このTeams、学生側は課題が絶望的に管理しづらいです。(個人の意見)

**じゃあクラスのDiscordに管理用Bot作って入れれば良いじゃん。**(名案)

他の高校生たちがクラスDiscordを運用してるかは知りませんが、うちはあるので活用するまでです。

要は、**この記事ではPythonを使ってdiscordのbotを作る方法を説明します。**
作成したコードは[GitHub](http://github.com/50m-regent/AssignmentBot)に載っているので参考にしてみてください。

### 目次
1. [Discord上でBotアプリを作る](#Discord上でBotアプリを作る)
2. [とりあえず接続してみる](#とりあえず接続してみる)
3. [コマンドを追加してみる](#コマンドを追加してみる)
4. [まとめ](#まとめ)

<a id="Discord上でBotアプリを作る"></a>
# Discord上でBotアプリを作る
新しいBotをDiscordに登録します。[Discord Developer Portal](https://discord.com/developers/applications)にアクセスします。
右上の**New Application**をクリックして好きな名前(Botの名前になります)で新しいアプリケーションを作成しましょう。
<img width="1085" alt="Applicatonis.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/634947/f2d5ed64-f151-f742-76b3-f787b81bcf98.png">

次に左のタブのBotから**Add Bot**します。
Usernameの下にTokenというところがあるのでCopyしてどこかに保存しておきましょう。
<img width="1088" alt="Bot.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/634947/c1534c2b-7a45-48bc-1551-e91e3e12e470.png">

今度はOAuth2タブに移動します。Scopesの中のbotにチェックマークを入れて、Bot PermissionのSend messagesとManage messagesにチェックを入れます。
<img width="1098" alt="OAuth2.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/634947/b3144fe6-0027-3633-795b-b4a8ab02d2c0.png">

出てきたリンクを開いてBotを自分が参加しているDiscordサーバーに追加することができます。
Discordを開いて確認してみましょう。

<a id="とりあえず接続してみる"></a>
# とりあえず接続してみる
今回はPythonを使ってコードを書きます。[discord.py](https://discordpy.readthedocs.io/ja/latest/)を使うので各自使えるようにしておいてください。

とりあえずメッセージに反応するようにします。

```python:main.py
import discord

TOKEN = '***'

client = discord.Client()

@client.event
async def on_message(message):
    if message.content == 'hello':
        await message.channel.send('hi')

client.run(TOKEN)
```

**TOKEN**は[Discord上でBotアプリを作る](#Discord上でBotアプリを作る)でコピーした文字列に置き換えてください。

実行してみましょう。
<img width="232" alt="スクリーンショット 2020-05-18 23.02.21.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/634947/801841a3-07a3-3122-5db5-55cbcac11202.png">

かわいいですね。

client.run()でBotを実行してます。
on_message()は何かメッセージが送信されたときに実行されます。送られてきた内容が**hello**だったときに**hi**と返すだけのBotです。
次はよくある**!p**のようなコマンドを実装してみます。

<a id="コマンドを追加してみる"></a>
# コマンドを追加してみる
コマンドの数だけ**if文**を書くのはセンスがないのでコマンドリストを作っておいてそこからコマンドを読むようにしましょう。

```python:main.py
import discord

assignment_list = []

client = discord.Client()

async def kadaihelp(message):
    string = 'コマンドリスト\n'
    for command in COMMANDS:
        string += '------------------------\n'
        string += '{}: {}\n'.format('!' + command, COMMANDS[command]['description'])
        string += '    使い方: {}\n'.format(COMMANDS[command]['use'])
        string += '    省略形: {}\n'.format(COMMANDS[command]['alias'])
        string += '------------------------'

    await message.channel.send(string)

async def newkadai(message):
    msg = message.content.split(' ')
    try:
        title, deadline, memo = msg[1:]

        assignment_list.append({
            'title': title,
            'deadline': deadline,
            'memo': memo
        })

        await message.channel.send('課題を追加しました！')
    except:
        await message.channel.send('入力形式が間違っています。')

async def deletekadai(message):
    msg = message.content.split(' ')
    for i in range(len(assignment_list)):
        if assignment_list[i]['title'] == msg[1]:
            assignment_list.pop(i)
            await message.channel.send('課題を削除しました')

async def kadailist(message):
    string = '課題一覧\n'
    for i, assignment in enumerate(assignment_list):
        string += '------------------------\n'
        string += '{}. {}\n'.format(i + 1, assignment['title'])
        string += '締切: {}\n'.format(assignment['deadline'])
        string += '備考: {}\n'.format(assignment['memo'])
        string += '------------------------\n'
        
    string += '現在、{}個の課題が出されています。'.format(len(assignment_list))
    await message.channel.send(string)

async def close(message):
    await client.close()

TOKEN = '***'
COMMANDS = {
    'kadaihelp': {
        'description': 'このリストを表示します。',
        'use': '!kadaihelp',
        'alias': '!kh',
        'func': kadaihelp
    },
    'newkadai': {
        'description': '新しい課題を追加します。',
        'use': '!newkadai \{タイトル\} \{締切\} \{備考\}',
        'alias': '!nk',
        'func': newkadai
    },
    'deletekadai': {
        'description': '課題削除',
        'use': '!deletekadai \{課題名\}',
        'alias': '!dk',
        'func': deletekadai
    },
    'kadailist': {
        'description': '登録されている課題一覧を表示します。',
        'use': '!kadailist',
        'alias': '!kl',
        'func': kadailist
    },
    'exit': {
        'description': 'Botを終了します。',
        'use': '!exit',
        'alias': '!ex',
        'func': close
    }
}

@client.event
async def on_ready():
    print('KadaiShosu起動')

@client.event
async def on_message(message):
    msg = message.content.split(' ')

    if message.author.bot:
        return

    for command in COMMANDS:
        if msg[0] in ['!' + command, COMMANDS[command]['alias']]:
            await COMMANDS[command]['func'](message)

client.run(TOKEN)
```
とりあえず5つコマンドを追加してみました。
**COMMANDS**の中に辞書形式でコマンドリストを作成しました。on_message()の中で全てのコマンドに対して呼び出されたかどうかを判別して実際にその動作を行う関数を実行します。

それぞれのコマンドに省略形として**alias**を用意しました。もっと良い書き方があるかもしれません。是非いろいろ試してみてください。

ここまででとりあえず課題管理Botとしては機能します。しかし、このままではプログラムを閉じたときに課題がリセットされてしまいます。
最後に変数を保存、読み込む部分だけ書いてみます。

```main.py
import pickle

async def close(message):
    pickle.dump(assignment_list, open('assignments.pkl', 'wb'))

    await message.channel.send('Bye^^')

    await client.close()

def load_assignments():
    global assignment_list

    assignment_list = pickle.load(open('assignments.pkl', 'rb'))

load_assignments()
```

新しくpickleというモジュールを使います。このモジュールは変数の保存を実現してくれます。

close()は書き換えて、他は書き足してください。
!exitでBotを終了したときにassignments_listが保存されて、起動時に読み込まれます。

<a id="まとめ"></a>
## まとめ
今回はここまでとします。他にもいろいろな機能があると嬉しいと思うので、自分で実装してみてください。

よければ[Twitter](http://twitter.com/50m_regent)フォローしてください。じゃあね。