PythonでDiscordの画像を発言・登録出来るBotを作った

# 最初に
Discord使ってるけど、Botを触りだしたら画像も出すように注文された。 
出すようにしたら毎回画像の追加するよう毎日頼まれるようになった。
ひどい話である。
なので、コマンドで自動追加出来るようにした。
※ 作った本人も完璧に理解してるわけじゃないから、こうしたら動いたよ程度の参考にしてね。
# 使ったもの
Python3
Discord ※pip install した。
json
urllib
# ソース
https://github.com/whalego/discord_bot
# 使い方
ざっとGITに書いてあるのをコピペ
#### /help list
Jsonに書いたdata内のlistを全部表示させる。
まじでJSONの中身全部出すだけなので、注意
####/help_add_command
コマンド追加するコマンドを表示させるよう。
#### /add_command
実行コマンド入力イメージ： /add_command img_name=test.jpg words=test,tttt img_url=httpURL
- img_name = 保存するファイル名にもなる
- words = ここに入れた文字に反応するようになる。
  - 何個でも追加できる。追加する場合は,で区切る。途中にスペース入れると壊れる。
  - 追加出来るファイル拡張子はjpg, gif, jpeg, bmpのみ。
  ※ PNG入れるの忘れた
  - URLのファイルがGIFだろうがBMPだろうが、img_nameのところで指定したファイルで保存される。
  - img_url = HTTPのURLをそのまま張る。
  - ローカルのファイルパスじゃないので注意
- 同じimg_nameは登録できない

なんか箇条書きのMDが反応しなくなった・・・

# 仕組みについて
今回は自宅に置いてあるサーバにDockerでCentos作ってその中に置いたので、ディレクトリ構造とかは適当だよ。
あとPython3が動く環境ならエントリーポイントを実行するだけでBot建てられるので、最悪Windowsで実行してもいいかもね。
DiscordのBotの登録方法とPythonの基本的なコードの書き方は説明しないよ。
参考：Botの登録方法
https://qiita.com/1ntegrale9/items/9d570ef8175cf178468f
ここ参考にしたら２秒でBOT作れたよ。

## ディレクトリ構造
```
Discord_bot
├/pict
├bot_man.py
├modules.py
├make_file.py
└filepath.json
```
## ファイルの役割
### bot_man.py
DiscordのAPI呼び出しと、メッセージのキャッチ部分実装。
基本的にclient.eventのアノテーションを付けてon_message()を呼ぶ。
on_messageの引数にしたものにDiscordのチャンネルのチャットに貼られた文字列が送られてくるイメージ。
※そう思ってたけど、ローカルから画像を直に貼り付けた場合は、下に書いたmsg.contetにURLが送られてこないらしい。
　これにちょっとだけハマったので、httpのURLだけで登録出来るようにした。
　画像を開けば中身はURLだというのに悲しいものである。

```python3
@client.event
async def on_message(msg):
    if msg.author.bot:
        # Botの発言に反応しないようにするため必須
        return

    if msg.content:
        # この方式でチャットが送られたチャンネルに対して発言が出来る。
        await msg.channel.send(f"{msg.contet}")
        # ローカルのファイルを送信する場合は以下の用に書く
        file_img = discord.File("ファイルパス")
        await msg.channel.send(File=file_img)
```

上のコードの`msg.content`の中に特定の文字が含まれてれば後で紹介する関数を実行して、`filepath.json`に追記しにいくイメージ。
特定の文字に反応のさせかたは、`in`を使って文章に登録した文字列が含まれてるかをチェックした。

簡単に中身を書いたけど詳細はソース読んでください。

### make_file.py & filepath.json
```filepath.json
{
    "data": [
        {
            "name": "test.gif",
            "file_path": "./pict/test.gif",
            "words": [
                "tttt",
                "eeee"
            ]
        }
    ]
}
```
こんな感じでdata:[]の中にどんどん追記していくことになる。もう１階上でも良かった気はする。

```python3:make_file.py:
class FilePath:
    def __init__(self):
        # Jsonのファイルパスとか
        # 画像ファイルアップロード時の情報とか

    def add_json(self):
        # ここは簡単にファイルオープンしてdataに追記してるだけ入力チェックとかはここに来る前に済ます想定
        with open(self.json_path, "r") as js:
            js = json.load(js)
            js["data"].append({"name": self.name, 
                               "file_path": self.path,
                               "words": self.words}
        # ここ最初新しくファイル開かないと保存できなくて何故か詰まってた。そりゃそうだわみたいな感じ。
        with open(self.json_path, "w") as out_js:
            json.dump(out_js, js, indent=4)
    def read_json(self):
        # ここは開いてdataの下のリスト全部吐き出すイメージ

    def __enter__(self):
        return
    def __exit__(self, ex_type, ex_value, trace):
        return True
```
ファイル操作はこんな感じです。詳しくはソース呼んでね。
今回はファイル操作する際にwith構文に書いて操作をしたかったので、`__enter__`と`__exit__`を使いました。
これはクラスオブジェクトをwith構文の中で安全に操作をするのに必須の項目らしい。
参考: https://qiita.com/Nabetani/items/0a9809ebb9f9a7f5dc49
すげえ役に立った！
ざっとenterで返す戻り値はwithを開いたときの戻り値？になるらしい。

```
with test_class() as cls:
```
この例で言うとclsの部分に当たるもの
exitは想像の通りこのステートメントが閉じるときに実行される関数。
exitの戻り値にTrueを指定するとwithを閉じるときのエラーを全て潰してくれるらしい。
閉じる時？だったかな？全てだったかな？ちょっと覚えてない。

### modules.py
ここもですが、詳しくはソース読んで・・・
ほとんど上で書いたjsonファイルの操作ですね。。。

一つだけhttpからのリクエストファイルの取得とか保存をやってるので、それを説明しておきます。

```python3:modules.py:
def download_pict(url, file_name):
    header = {
        "User-Agent": "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:47.0) Gecko/20100101 Firefox/47.0",
    }

    req = urllib.request.Request(url, headers=header)
    with urllib.request.urlopen(req) as wf:
        data = wf.read()
        with open(f"./pict/{file_name}", "wb") as lf:
            lf.write(data)

    return f"./pict/{file_name}"
```
まずそのままリクエストを送信してもブラウザからアクセスしてくれと怒られるので、
headerに上に書いてあるようなパラメータを付与してやる。スタックオーバーフローの人勝手にコピペしてすまん・・・
でまぁ、なんやかんやするとファイルが取得出来るので、バイナリデータ読み込んでファイルとして、/pict配下にしまってあげる。
最後はファイルパスを返して上げるので、JSONファイル操作する際に呼んで保存場所教えてあげてね。

# 2022/09/10追加
なんかDiscordのAPIがアップデートされた。そのせいで今までと同じようにContentからメッセージを取得することが出来なくなったのでメモ。
原因: https://support-dev.discord.com/hc/en-us/articles/4404772028055

### Pythonのアップデート
Discordのモジュールが3.8だったか忘れましたが、それ以上じゃないと起動しないので、それのアップデート。

### 下のDiscordのBotの設定ページ内で以下の設定。
https://discord.com/developers/applications

#### `Public Bot`の設定をオフにした。
これはBot作成者しかチャンネルに追加できなくする設定らしい。
#### `Message Content Intent`の設定をオンにした。
これはオフのままで実行するとEvent Loopとかいうエラーが発生した。

### コードの修正箇所
#### Clientの宣言の箇所
```
intents = discord.Intents.default()
intents.message_content = True
client = commands.Bot(command_prefix="", intents=intents)
```
`intents`の引数が必須になった。
あと`message_content`の値を有効にする必要があるらしい。

#### on_messageのメソッド
```
async def on_message(msg):

    if msg.content.startswith(""):
        print(msg.content)
```
`content.startswith("")`の下でないと`content`の中身を確認できなかった。
`content.startswith("")`はBotが受け取る値になっている。
私が作成したBotはメッセージを分割して単語ごとに画像を添付するので、
これに対してうまくメッセージを入れるのは難しかったので、空文字で対応。

※ 雑に対応したので、この設定まずいよとかあったら指摘ください。
　 気が向いたときに修正しておきます。

# 最後に
超ざっくり紹介になった。多分僕自身の備忘録的なものになるとおもう。
jsonファイル操作する際にclassをwithで開くやつは正直こんなことせんでええやろとか思ってたけど、今いる現場で似たようなことやるので、勉強がてらに使ってみた。
もうちょっと拡張とかして、エラー処理を散りばめたら便利そう。頭の中から抜け落ちないようにする。

あといつでもどこでもそうなんだけど、開発がWindowsで実行場所がLinuxの時は文字コード気をつけようね。
こいつのせいでJSON読み込んだりするのに2時間ぐらい潰れて頭おかしくなりそうだった。

実際にコード書いてた時間は4時間くらいだったので、よい勉強になったと思います。
まぁ抜けてる箇所とか実装漏れとかもあるので、時間が空いたらそこも修正しようかな。