discord.pyでランニングコストゼロのBOTを作ってみた コーディング編

# 経緯とご挨拶
初投稿です。

この記事では、レンタルサーバーサービスのKoyebでdiscordのbotを作成した過程を記してみます。

Koyebを用いる以前はrailwayにて複数のbotを作成していましたが、railway有料化＆円安の影響で毎月5ドル(700円ほど)の支払いになってしまっていたため、完全無料のサーバーに移転させました。

# 完成図
名前がすごいことになってるのは気にしないでください。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3778018/41f4ad6c-7b79-4429-b7a5-8c8fa0cc8ad6.png)

# やりたいこと
* 誰かが通話チャンネルに参加したとき、チャットへ通知を出す機能を持ったbotの作成
* 完全無料サーバーを利用してランニングコストをゼロにする
* 管理を容易にするため、Githubにpushすればbotが更新されるようにする

# 環境
必須なものがどれだけかわかりませんが私の環境です。
* GitHub Education(学生なので無料)
* python 3.13.2
* discord.py==2.5.2


# discord.pyを用いたbotの中身の作成
基本となるテンプレートを配布します。
### ⚠️注意点
* botのtokenは[discord developers portal](https://discord.com/developers/applications/)から各自で取得して環境変数に追加して下さい。
* オペランドの「message」などに送信者などのあらゆる情報が入っているので、[discord.py APIリファレンス](https://discordpy.readthedocs.io/ja/latest/api.html)を参照してください。
* 設定項目などを普通の変数に保存すると他サーバーと共通になってしまうので、サーバーごとに保存してください。
```python
import discord
import os
import json

intent = discord.Intents.all()
client = discord.Client(intents=intent)


@client.event
async def on_ready():
    print(f"起動時に発火します")


@client.event
async def on_message(message):
    if message.author == client.user:
        return
    print("メッセージを受け取ったときに発火します")


@client.event
async def on_voice_state_update(member, before, after):
    print("ボイスチャンネルに入ったり抜けたりした時に発火します")


client.run(os.environ["TOKEN"])
```

### 実際に作成した主な部分
```python
@client.event
async def on_voice_state_update(member, before, after):
    config = load_server_config(member.guild.id)
    if after.channel == None:
        return
    if before.channel == None and after.channel.id != None:
        channel_id = config["voice_channel"]
        channel = client.get_channel(int(channel_id))
        message = config["voice_message"]
        if member.guild.id == テストサーバーID or member.guild.id == 実際のサーバーID:
            message_embed = discord.Embed(
                title=f"{member.display_name} が {"https://discord.com/channels/" + str(member.guild.id) + "/" + str(after.channel.id)} に参加しました。", 
                description="")
            message_embed.set_author(name=member.display_name, icon_url=member.display_avatar.url)
            message_embed.set_thumbnail(url=member.display_avatar.url)
            await channel.send(content=message,embed=message_embed)
        else:
            await channel.send(message)
```
### 概要
* サーバーごとの設定を```member.guild.id```から検索して使用する
* 主なサーバーでは独自のUIを使用したいため、特定サーバーを名指しで指定

## 詰まったところ
* 製作中に送信先のチャンネルIDを変数に格納してしまっていたため、テストサーバーに入室した場合に本番サーバーに通知が出てしまった
⇒jsonに格納して解決

* ユーザーのアイコンを取得する際に```User.avater```を使用した場合に、アイコンを設定していないユーザーに対してエラーを起こして作動しなかった。
⇒```User.display_avater```を用いることで解決
* トークンをtoken.txtのように保存しようとしたが、GitHubにpushするとtokenの流出扱いになるため、無効化された。
⇒サーバーの環境変数として指定することで解決

# 新たに発生した問題点
設定をjsonに保存するように変更したが、GitHubから再実行しようとするとサーバーのローカルにあるjsonが削除されてしまう。
## jsonをGitHubにアップロード
### ⚠️注意点
* 今回使用するサーバーでは、GitHubのpushを検知して自動でデプロイする機能があるため、プロジェクトのあるリポジトリにアップロードすると頻繁に再起動してしまう。（起動がうまくいかないこともあったので再起動の回数は最小限にしたい）
⇒よってバックアップ用のリポジトリを作成しよう。
* アクセストークンには１時間ごと(？)にアクセス回数の上限が設定されている。動作ログを記録したい場合はログを蓄積しておいて10分に一度アップロードなどにするとよい。
```python
from github import Github
from github.GithubException import UnknownObjectException
import json
import datetime
import csv
import io
import os
class mygithub:
    def __init__(self):
        # with open("github_token", "r") as f:
        #     token = f.read().strip()
        # アクセストークンで認証
        self.g = Github(os.environ["github_token"])
        self.repo = self.g.get_user().get_repo("n")
        pass
    def save_json(self, json_data):
        datetime_now = datetime.datetime.now()
        file_path = f"log_{str(datetime_now.month)}_{str(datetime_now.day)}_{str(datetime_now.hour)}_{str(datetime_now.minute)}.json"
        self.repo.create_file(
            path=file_path,
            message="Update README from PyGithub",
            content=json.dumps(json_data, indent=4),
            branch="main"
        )
        pass
```


# まとめ
本記事ではbotのコード部分を作成した。以降は別ページに書く。
↓↓↓続き↓↓↓

https://qiita.com/ak-2302/items/1c20269eafce9fd29038