discord.py入門(1) 

# はじめに
discord.pyの非公式のサーバーを運用しています。
ここでは質問も受け付けています。
https://discord.gg/KPp2Wsu **この記事についての質問もこちらにお願いします**

# この記事について
この記事はdiscord.pyについて段階を踏んで勉強していくための記事になります。
まず始めに基本の書き方を学び、次に発展的な内容を学びます。
**【シリーズ】**

[discord.py入門(1)](https://qiita.com/sizumita/items/9d44ae7d1ce007391699)
[discord.py入門(2)](https://qiita.com/sizumita/items/b37e74fca41d98077a18)
[discord.py入門(3)](https://qiita.com/sizumita/items/cafd00fe3e114d834ce3)




# 注意
**この記事は少しでもPythonができる人向けになります。**
ただし、コピペするだけでも使えますが、その後書けなくなるので、もっと個別に機能を作りたい場合は、
本やpaizaラーニング、ドットインストール、Progate、[京大のテキスト](https://repository.kulib.kyoto-u.ac.jp/dspace/handle/2433/245698) を使って勉強することをお勧めします。
私がお勧めな本は、辻真吾さんの[Pythonスタートブック](https://www.amazon.co.jp/Pythonスタートブック-増補改訂版-辻-真吾/dp/4774196436/)です。


# 筆者の動作環境
Python 3.8.0
Mac OS Catalina 10.15.2
discord.py-1.3.2


# 初期設定・Botを動かす

Pythonが入っていない方は[pythonjpのページ](https://www.python.jp/install/install.html) から導入してください。

次に、 [こちらの記事](https://qiita.com/1ntegrale9/items/cb285053f2fa5d0cccdf) から、アカウントを初期設定してください。


できたら、適当なエディタから新しくファイルを作り、

```python
import discord
client = discord.Client()

@client.event
async def on_ready():
    print("on_ready")
    print(discord.__version__)

client.run("あなたのbotのトークン")
```

と書き、`discordbot.py`などとして保存してください。

`python3 discordbot.py`などとして実行し、コンソールに`on_ready`などと表示されれば成功です。


また、Herokuを使用する場合は [こちらの記事](https://qiita.com/1ntegrale9/items/9d570ef8175cf178468f) を見て設定を行ってください。


# イベントを使ってみよう
ここまでの内容ができた読者の皆さんは、まずeventの概念を覚えましょう。
上で書いた

```python
@client.event
async def on_ready():
    print("on_ready")
    print(discord.__version__)
```

は覚えていますか？ここでは、普通の関数とは違い、**デコレーター**と**コルーチン関数**が使われています。

## デコレーターとは
デコレーターとは、関数やclassの前に書き、その関数を修飾するための仕組みです。詳しくは[こちら](https://qiita.com/mtb_beta/items/d257519b018b8cd0cc2e) をご覧ください。

## コルーチン関数とは
次に、コルーチン関数とは、async/awaitに対応した関数です。
pythonのasync/await用のライブラリはasyncioというものがあります。これがdiscord.pyを支えています。
asyncioについては初心者の域を超えてしまうので、ここでは詳しく説明できません（私もわかっていません）。 ([参考](https://qiita.com/icoxfog417/items/07cbf5110ca82629aca0))

ここでは、関数定義の時に`async def`とついているコルーチン関数は`await 関数名()`として呼び出す、
`await`はコルーチン関数内でしか呼び出せない、ということを覚えておけば大丈夫だと思います。

## Eventとは
discord.pyのeventとは、discord apiから送られてきた、例えば`メッセージが送られた`などのイベントが発生した時に
その関数を実行してくれる仕組みです。

イベントを設定するには`on_ + イベント名`を関数名につけ、また関数定義の一行前に`@client.event`と書く必要があります（clientの部分は自分の定義名に変更してください）。
上の例の`on_ready`は、`ready` = Botが起動した というイベントに対応しています。`on_message` = メッセージが送られた、
`on_reaction_add` = リアクションが送られたなどといったイベントがあります。一覧は [こちら](https://discordpy.readthedocs.io/ja/latest/api.html#event-reference)。

**注意**
たまに、このように書いている人を見かけます:

```python
@client.event
async def on_message(message):
    処理1

@client.event
async def on_message(message):
    処理2

```

**この様に書くと、一番最後に定義したon_messageのみが実行されます!!!**
よって、関数定義は一つにする必要があります。この様に書く必要があります:

```python
@client.event
async def on_message(message):
    処理1
    処理2

```

# Clientについて
discord.Clientは、apiとの通信を受け持っているclassです。Clientには、様々な取得系の関数があります。
主なものを紹介します。

## get_channel, get_guild, get_user, get_emoji
**これらは非同期関数ではありません**
channel, guild, user, emojiをそれぞれidから取得するための関数です。

## get_all_members, get_all_channels
**これらは非同期関数ではありません**
全てのメンバー、全てのチャンネルを返すイテレーターです。ユーザーは重複する可能性があります。



# 簡単な応答botを作る
discord.pyについて学んだところで、簡単な応答botを作ってみましょう。

### この項で勉強すること

- メッセージへの反応のさせ方
- よく使う変数


今回の仕様は、

- `鳴いて` と言われたら`にゃーん`,`わん！`,`コケコッコー`のどれかを返す
です。`on_message`というeventを使いますが、このeventはめちゃくちゃ多用することになるので、書き方を覚えておくといいと思います。


```python
import discord
import random
client = discord.Client()
# ランダムで送るメッセージの一覧 ※ここに書き足すことでランダムに選ぶ内容を増やせる
random_contents = [
    "にゃーん",
    "わん！",
    "コケッコッコー",
]

@client.event
async def on_ready():
    print("on_ready")
    print(discord.__version__)


@client.event
async def on_message(message):
    # 送信者がbotである場合は弾く
    if message.author.bot:
        return 
    # メッセージの本文が 鳴いて だった場合
    if message.content == "鳴いて":
        # 送信するメッセージをランダムで決める
        content = random.choice(random_contents)
        # メッセージが送られてきたチャンネルに送る
        await message.channel.send(content)
    elif message.content == "おはよう":
        await message.channel.send("おはよう！！")


client.run("あなたのbotのトークン")
```

`if message.content == "鳴いて":`で、メッセージの本文(content)が`鳴いて`であるかを判定しています。
コマンドを作成する場合はもっぱらこの手法や正規表現などを使います（後で紹介するcommands.bot frameworkを使用しない場合）。
messageのクラスは`discord.Message`です。

次に、`await message.channel.send(content)`で、メッセージが送られたチャンネルにメッセージを送信しています。
メッセージを送信する時は、TextChannelなどのオブジェクトにあるsendという関数を使用します。
ドキュメントを見るときも、チャンネルに対して行う動作（名前を変えるなど）はTextChannelなど、メッセージに対して行う動作（消去する、ピン留めする）はMessageにあると考えると探しやすいです。

## よく使う変数一覧
以下に、MessageとGuild、TextChannel、Memberにあるよく使われる変数の一覧を示します。**関数についてはそれぞれ触れたところで解説します。**

### 書き方について
List\[discord.Reaction]のような書き方は、discord.Reactionが入っているリストという意味です。

**[discord.Message](https://discordpy.readthedocs.io/en/latest/api.html#discord.Message)**

| 変数名        | 内容         | クラス         |
|:-------------|:------------------------|:--------------------------------|
| id           | メッセージのid                | int
| author       | メッセージの送信者             | [discord.Member](https://discordpy.readthedocs.io/en/latest/api.html#member) |
| guild        | メッセージを送信されたサーバー   | [discord.Guild](https://discordpy.readthedocs.io/en/latest/api.html#guild) (DMだった場合はNone)  |
| channel      | メッセージが送信されたチャンネル | [discord.TextChannel](https://discordpy.readthedocs.io/en/latest/api.html#textchannel) or [discord.DMChannel](https://discordpy.readthedocs.io/en/latest/api.html#discord.DMChannel) or [discord.GroupChannel](https://discordpy.readthedocs.io/en/latest/api.html#discord.GroupChannel) |
| content      | メッセージの本文              | str |
| created_at   | メッセージが送られた時間        | datetime.datetime |
| edited_at    | 最後にメッセージが編集された時間  | datetime.datetime(編集されてない場合はない) |
| jump_url     | メッセージリンク                | str |
| embeds       | 埋め込みのlist                | List\[[discord.Embed](https://discordpy.readthedocs.io/en/latest/api.html#discord.Embed)] |
| attachments  | 一緒に送られた画像やファイルのlist | List\[[discord.Attachment](https://discordpy.readthedocs.io/en/latest/api.html#discord.Attachment)] |
| reactions    | つけられたリアクションのlist | List\[[discord.Reaction](https://discordpy.readthedocs.io/en/latest/api.html#discord.Reaction)]

**[discord.Guild](https://discordpy.readthedocs.io/en/latest/api.html#guild)**

| 変数名        | 内容         | クラス         |
|:-------------|:--------------------------------|:--------------------------------|
| name         | サーバー名 | str |
| id           | サーバーのid | int | 
| owner        | サーバー所有者 | [discord.Member](https://discordpy.readthedocs.io/en/latest/api.html#member) |
| channels     | サーバーの全てのチャンネル | List\[[discord.TextChannel](https://discordpy.readthedocs.io/en/latest/api.html#textchannel), [discord.VoiceChannel](https://discordpy.readthedocs.io/en/latest/api.html#voicechannel), [discord.CategoryChannel](https://discordpy.readthedocs.io/en/latest/api.html#categorychannel)] |
| text_channels| サーバーの全てのテキストチャンネル | List\[[discord.TextChannel](https://discordpy.readthedocs.io/en/latest/api.html#textchannel)] |
| voice_channels| サーバーの全てのボイスチャンネル | List\[[discord.VoiceChannel](https://discordpy.readthedocs.io/en/latest/api.html#voicechannel)] |
| categories   | サーバーのカテゴリーのlist | List\[[discord.CategoryChannel](https://discordpy.readthedocs.io/en/latest/api.html#categorychannel)] |
| me           | サーバー内でのbot | [discord.Member](https://discordpy.readthedocs.io/en/latest/api.html#member) |
| voice_client | サーバー内でのボイスクライアント | [discord.VoiceClient](https://discordpy.readthedocs.io/en/latest/api.html#discord.VoiceClient) or None |
| members      | サーバーの全てのメンバー | List\[[discord.Member](https://discordpy.readthedocs.io/en/latest/api.html#member)] |
| roles        | サーバーの全ての役職 | List\[[discord.Role](https://discordpy.readthedocs.io/en/latest/api.html#discord.Role)] |
| icon_url     | サーバーアイコンのurl | [discord.Asset](https://discordpy.readthedocs.io/en/latest/api.html#discord.Asset) |

**[discord.TextChannel](https://discordpy.readthedocs.io/en/latest/api.html#textchannel)**

| 変数名        | 内容         | クラス         |
|:-------------|:--------------------------------|:--------------------------------|
| name         | チャンネル名 | str |
| id           | チャンネルid | int |
| guild        | チャンネルがあるギルド | [discord.Guild](https://discordpy.readthedocs.io/en/latest/api.html#guild) |
| category_id  | チャンネルがあるカテゴリーのid | int もしくは なし |
| topic        | チャンネルトピック | str |
| members      | そのチャンネルを見ることができる全てのユーザー | List\[[discord.Member](https://discordpy.readthedocs.io/en/latest/api.html#member)] |
| mention      | チャンネルのメンション(<#チャンネルid>) | str |

**[discord.Member](https://discordpy.readthedocs.io/en/latest/api.html#member)**

| 変数名        | 内容         | クラス         |
|:-------------|:--------------------------------|:--------------------------------|
| id           | ユーザーid | int |
| name         | ユーザーネーム | str |
| discriminator| 俗にいうタグ | str |
| bot          | ユーザーがbotか | bool |
| joined_at    | サーバーに入った時間 | datetime.datetime |
| created_at   | ユーザーを作った時間 | datetime.datetime |
| guild        | メンバーがいるサーバー | [discord.Guild](https://discordpy.readthedocs.io/en/latest/api.html#guild) |
| nick         | メンバーのニックネーム | str もしくは なし |
| roles        | メンバーが持っている全ての役職 | List\[[discord.Role](https://discordpy.readthedocs.io/en/latest/api.html#discord.Role)] |
| mention      | メンバーへのメンション(<@メンバーid>) | str |
| display_name | ニックネームがあればニックネーム、なければ名前 | str |

これらを見ずに使えるようになれば、ぐんとリファレンスを読むことが少なくなると思います。

# 画像を扱う
次に、画像を扱ってみましょう。

### この項で勉強すること

- 画像が送信されたことを判定する
- Attachmentについて学ぶ
- 画像を送信する

## Attachmentについて
Attachmentは、メッセージと一緒に送られてきたファイルに当たります。Attachmentのurlは`Attachment.url`で取得できます。
また、ファイル名は`Attachment.filename`です。

### Attachmentをローカルに保存する
送られてきた画像を保存したい時などには、`Attachment.save`**関数**を使うことができます。例を示します:

```python
attachment = message.attachments[0]
# 送られてきたファイルをattachment.pngという名前で保存する
await attachment.save("attachment.png")
```

### Attachmentの中身を取り出す
ファイルに保存しなくても、中身が欲しい時があります。その時は`Attachment.read`関数を使用します。この関数の返り値はbytesです。

```python
attachment = message.attachments[0]
# 送られてきたファイルをdataに格納
data = await attachment.read()
```

## 画像が送信されたか判定する
画像があればMessage.attachmentsに格納されています。よって、画像が送信されたかはMessage.attachmentsが空ではないか、またそれの拡張子がpng,jpgなどであるかを判定すればいいことになります。

```python
@client.event
async def on_message(message):
    # 送信者がbotである場合は弾く
    if message.author.bot:
        return 
    # ファイルがある場合
    if message.attachments:
        for attachment in message.attachments:
            # Attachmentの拡張子がpng, jpg, jpegのどれかだった場合
            if attachment.url.endswith(("png", "jpg", "jpeg")):
                await message.channel.send(attachment.url)
```

## 画像を送信する
ローカルの画像を送るためには、discord.Fileを使用します。
また、TextChannel.sendのfileという引数を使用します。

```python
@client.event
async def on_message(message):
    # 送信者がbotである場合は弾く
    if message.author.bot:
        return 
    if message.content == "猫の画像":
        # ローカルにあるcat.pngという名前のファイルを送信する
        await message.channel.send(file=discord.File("cat.png"))
```

# リアクションを扱う
### この項で勉強すること

- リアクションをつける
- リアクションを削除する
- ついているリアクションを一覧で表示する
- 引数つきコマンドを作る

## リアクションをつける
リアクションをつけるのは非常に簡単です。

```python
# 😙というリアクションをつける
await message.add_reaction("😙")
```

複数リアクションをつける場合はこうします:

```python
for reaction in ["☺️", "😙", "🚗"]:
    await message.add_reaction(reaction)
```

## リアクションを削除する

一人がつけたリアクションを削除するにはMessage.remove_reactionを使用します。
第1引数に削除したいユーザー、第2引数に絵文字を入れます。

```python
# 自分の😙というリアクションを消す
await message.remove_reaction(message.guild.me, "😙")
```

特定のリアクションを全て消したい時には、Message.clear_reactionを使用します。

```python
# 全員の😙というリアクションを消す
await message.clear_reaction("😙")
```

~~言論統制のために~~リアクションを全て消してまっさらにするためには、Message.clear_reactionsを使用します。

```python
# 言論統制し、全てのリアクションを消す
await message.clear_reactions()
```

## メッセージについているリアクションの一覧を表示する
まず、メッセージを選択する必要があります。選択の仕方は様々ですが、ここでは正規表現とメッセージリンクを使用してみましょう。

メッセージを取得し、リアクション一覧を手に入れます。

```python
import re
link_regex = re.compile(
            r'^https?://(?:(ptb|canary)\.)?discordapp\.com/channels/'
            r'(?:([0-9]{15,21})|(@me))'
            r'/(?P<channel_id>[0-9]{15,21})/(?P<message_id>[0-9]{15,21})/?$'
        )

link = ""
match = link_regex.match(link)
channel = client.get_channel(int(match.group("channel_id")))
# channelからメッセージを取得する、get_messageは無いので注意!
message = await channel.fetch_message(int(match.group("message_id")))
# リアクションのリスト
reactions = message.reactions
```

次に、リアクションのリストから、絵文字と個数をリスト形式で表示させてみましょう。
Reaction.emojiが絵文字、Reaction.countがリアクションの個数です。

```python
text = "絵文字 : 個数\n"
for reaction in reactions:
    text += f"{reaction.emoji} : {reaction.count}\n"

await message.channel.send(text)
```

これを、on_messageに当てはめるとこうなります。

```python
import discord
import re
link_regex = re.compile(
            r'^https?://(?:(ptb|canary)\.)?discordapp\.com/channels/'
            r'(?:([0-9]{15,21})|(@me))'
            r'/(?P<channel_id>[0-9]{15,21})/(?P<message_id>[0-9]{15,21})/?$'
        )
client = discord.Client()


@client.event
async def on_message(message):
    # 送信者がbotである場合は弾く
    if message.author.bot:
        return 
    if message.content.startswith("/get_reactions "):
        link = message.content.replace("/get_reactions ", "")
        match = link_regex.match(link)
        channel = client.get_channel(int(match.group("channel_id")))
        target_message = await channel.fetch_message(int(match.group("message_id")))
        reactions = target_message.reactions
        text = "絵文字 : 個数\n"
        for reaction in reactions:
            text += f"{reaction.emoji} : {reaction.count}\n"

        await message.channel.send(text)

client.run("あなたのbotのtoken")
```

引数付きコマンドは、このようにreplaceを使用したり、正規表現を使ったり、空白がなければsplitを使うことが多いです。
しかし、Clientのみでコマンドを作る時は引数は一つぐらいの関数にしておくほうがいいのかなとは思っています。

# Embedを送信する
URLのプレビューなどでお馴染みのEmbedを送信してみましょう。

### この項で勉強すること

- Embedの作成方法
- フィールドの使い方・画像や時間を入れる
- Embedの送信方法

## Embedを作成する
discord.pyでのEmbedはdiscord.Embed型を使い表現します。
作成時、タイトルと説明を入れることができます（両方省略可）。

```python
embed = discord.Embed(title="タイトル", description="説明")
```

## 情報を追加する
Embedには、フィールド、タイムスタンプ、サムネイル、画像、上側のユーザー情報、下側のアイコンとテキストを追加することができます。
フィールドは**20**個まで、その他は一つ追加できます。
また、フィールドはnameとvalueからなりますが、**必ず両方とも空文字列(**`""`**)やNone以外の、普通の文字列を入れてください**。
送信する際にエラーになります。
これらは設定しなくても送信は可能です。

```python
embed = discord.Embed(title="タイトル", description="説明")

# フィールドを追加する
embed.add_field(name="名前", value="値")

# タイムスタンプを設定する
import datetime
embed.timestamp = datetime.datetime.now()

# サムネイルを追加する(URL指定なので注意！)
embed.set_thumbnail(url="https://upload.wikimedia.org/wikipedia/commons/0/07/Viscontisforzatarot.jpg")

# 画像を追加する(こちらもURL)
embed.set_image("https://upload.wikimedia.org/wikipedia/commons/0/07/Viscontisforzatarot.jpg")

# 上側のユーザー情報を追加
embed.set_author(name="タロットマン", url="https://ja.wikipedia.org/wiki/タロット", icon_url="https://upload.wikimedia.org/wikipedia/commons/6/6f/Taroky_trul.JPG")

# 下側のアイコンとテキストを追加
embed.set_footer(text="アルカナあるかな？", icon_url="https://upload.wikimedia.org/wikipedia/commons/6/6f/Taroky_trul.JPG")
```

## 送信する
discord.TextChannel.send関数の、embed引数に指定してやることで送ることができます。

```python
# 上の続き

await channel.send(embed=embed)
```

# 終わりに

ここまでいかがだったでしょうか。
今回は書き方をメッセージ・画像・リアクション・Embedに分けて説明しました。
次の記事では、発展的なeventの使用方法、設定の変更方法、チャンネルの作り方について扱います。
それでは。 [次の記事へ](https://qiita.com/sizumita/items/b37e74fca41d98077a18)