discord.pyでログオフ機能を実装する(追加でシステム自体も落とす)

# 0.この記事で解説すること
* discord.pyのAPIリファレンスの読み方が分かる
* いきなりrun(token)が動かなくなった時の対応方法
* Botのログオフ機能を実装する

# 1.今回、解説に至った経緯
1. デバッグしたいのでbotをログオフさせたい
1. 解説記事を色々と見たが実装してる方がいなかった
1. システムの強制終了を頻繁にしているとrun(token)で動かなくなった
1. 対策ができたので記事を書くことに

**※対策の追記内容があります！**　[対策の追記](https://qiita.com/DoNotPrayDebug/items/8ab5dd9d245f35561f4a)

3.についてですが、Replitで動かしてると発生するようです（他環境でも起きるかも？）
内容としては以下のエラーログが出ます（一部抜粋）
```
discord.errors.HTTPException: 429 Too Many Requests (error code: 0):
```

# 2.ログオフ機能を実装する

## 2.1 開発環境
* 統合開発環境: [Replit](https://replit.com/)
* discord.py: 2.0.1
* 言語: python3.8

## 2.2 処理の詳細内容
1. Discordのユーザー入力を検知
1. Botをログオフさせる
1. コード上でログオフを検知してシステム自体を落とす

### 2.2.1 Discordのユーザー入力検知
[on_message(message)](https://discordpy.readthedocs.io/ja/latest/api.html#messages)で取得できます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2870946/4e985e81-a84a-9b96-2137-fdd7a10723bb.png)
以下の内容を踏まえて元となるコードを書きます。

```.py
import discord
import os, sys, datetime, json

class MyClient(discord.Client):

  #メッセージが書き込まれた時
  async def on_message(self, message):
  #送信者がbot自身の場合はコマンドを無効にする
  if message.author.bot:
    print("bot送信\n")
    return

  if ".logoff" in message.content:
    #ログオフ
    await self.Logoff(message)
    return

def main():
  #環境変数からtokenを取ってくる
  TOKEN = os.getenv("TOKEN")
  #すべての機能を使えるようにする
  intents = discord.Intents.all()
  #intentsは必須パラメータ
  client = MyClient(intents=intents)
  #Discord接続
  client.run(TOKEN)

if __name__ == "__main__":
    main()
```
on_message(message)で使われる引数はクラスです。
メンバ変数やメソッドはどう使うかはAPIリファレンスを読めばわかります。
[discord.Message](https://discordpy.readthedocs.io/ja/latest/api.html#message)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2870946/5bc9d634-a232-220b-d636-b32bd42c2f7a.png)
今回の場合だと
A.送信者がBotかを判定
B.メッセージ内容に.logoffとあればログオフさせる関数を実行

それぞれの処理詳細について説明していきます。

#### 2.2.1(A) Bot判定
```.py
import discord
import os, sys, datetime, json

class MyClient(discord.Client):
  #メッセージが書き込まれた時
  async def on_message(self, message):

  #送信者がbot自身の場合はコマンドを無効にする
  if message.author.bot:
    print("bot送信\n")
    return
```
APIリファレンスが読めないと、この記述にたどり着けないと思います。
（他の解説記事を読むと別の記述でBot判定していました）

まずは[message.author](https://discordpy.readthedocs.io/ja/latest/api.html#discord.Message.author)から見ていきましょう。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2870946/2a15d4c9-f9d3-67d6-99d9-ee0184172559.png)
こちらを見ると今回の場合はMemberが取得できるようです。
リンクをクリックして[Member](https://discordpy.readthedocs.io/ja/latest/api.html#discord.Member)の中身をみていきましょう。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2870946/c5b83c9a-66f8-385b-2f26-5e5ca99f60b7.png)
discord.Memberクラスを取得できるようです。
この中にあるメンバ変数BotはBotの場合Trueを返します。

長かったので今までの流れをまとめると以下の記述となります。

**discord.Messageクラスはdiscord.Memberクラスを持っている。
 → discord.Memberクラスのメンバ変数botを使ってBot判定をする。**

図解すると、こんな感じです。

```
on_message(message)

message = discord.Messageクラス
 L message.author = discord.Memberクラス
     L bot(Botの場合はTrue)
```

#### 2.2.1(B) メッセージ内容判定
```.py
import discord
import os, sys, datetime, json

class MyClient(discord.Client):
  #メッセージが書き込まれた時
  async def on_message(self, message):
  if ".logoff" in message.content:
    #ログオフ
    await self.Logoff(message)
    return
```
[message.content](https://discordpy.readthedocs.io/ja/latest/api.html#discord.Message.content)はメンバ変数でメッセージ内容を取得できます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2870946/cc4260aa-07b8-ab39-889e-597290c0b73a.png)

### 2.2.2 Botをログオフさせる
まずはコードから
```.py
import discord
import os, sys, datetime, json

class MyClient(discord.Client):

  #ログオフ
  async def Logoff(self, message):
    print("実行:Logoff\n")
    await message.channel.send("ログオフします")
    await self.close()
```

ココでは2つの処理をしています。
A.チャンネルにメッセージを送信
B.ログオフする

では1つずつ見ていきましょう。

#### 2.2.2(A) チャンネルにメッセージを送信
messageはdiscord.Messageクラスです。
[message.channel](https://discordpy.readthedocs.io/ja/latest/api.html#discord.Message.channel)を見てみましょう。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2870946/e9c60bbb-6314-baa3-dd6d-8b484fe64c2b.png)
今回の場合だとdiscord.TextChannelクラスが取れます。
（メッセージを送信したテキストチャンネルの情報）
discord.TextChannelのsend()メソッドでメッセージを送信しています。

#### 2.2.2(B) ログオフ 
self.close()となってますが、これはdiscord.Clientクラスの[close()メソッド](https://discordpy.readthedocs.io/ja/latest/api.html#discord.Client.close)です。
(自作クラスMyClientはdiscord.Clientを継承している)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2870946/a704d543-5426-c82b-0ed3-a59344cd9a12.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2870946/06ce41df-f22a-3569-dfcd-b9c6a3749889.png)

### 2.2.3 コード上でログオフを検知してシステム自体を落とす
まずはコードから
```.py
import discord
import os, sys, datetime, json

class MyClient(discord.Client):
  #先ほど説明したので省略

def main():
  #環境変数からtokenを取ってくる
  TOKEN = os.getenv("TOKEN")
  #すべての機能を使えるようにする
  intents = discord.Intents.all()
  #intentsは必須パラメータ
  client = MyClient(intents=intents)
  #Discord接続
  client.run(TOKEN)

  #pythonを落とす
  if (client.is_closed()):
    #discord.errors.HTTPException: 429 Too Many Requests対策
    print("osを切ります")
    os.system("kill 1")

if __name__ == "__main__":
  main()
```

discord.Clientクラスのis_closed()メソッド
これはcloseされた場合にTrueを返します。
この判定が通ったときに呼び出されるos.system("kill 1")

コレはに引数内容のコマンドを実行させます。
今回の記述内容は「今動いてるシステムを終了させる」です。

**※対策の追記内容があります！**　[対策の追記](https://qiita.com/DoNotPrayDebug/items/8ab5dd9d245f35561f4a)

## 2.3 実際に動かしてみた
起動時
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2870946/a4399cc1-6b5e-00f2-5218-fbeff27a0e92.png)
Discordの画面
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2870946/ba85d93e-f2d0-5c4e-24d2-2d8b7aaf44f6.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2870946/06dde4fe-20dc-3cf0-e136-87f686e357f7.png)
StopからRunに変わったので実装できてます。
ココまでお付き合いいただき、ありがとうございました。

※何か連絡や依頼がありましたらDMをお願いします。
[Twitter: Don't Pray Debug](https://twitter.com/DebugDon)