🐶がご飯を食べるために、discord botで時報を作った話。

愛犬アレックスの独り言

🐶「お腹すいたわん、でも、ご主人はdiscordで通話中。困ったわん」
🐶「そうだ！discord経由で連絡すればいいわん！」
🐶「この間ご主人がくれたこのボタン、discordのbotになってるって言ってたわん！」
🐶「押してみよ！」

U･ω･U）ﾉ凸”ﾎﾟﾁｯ

bot「🐶<ご飯くれ！」
ワシ「あー忘れてたごめん！！」
🐶「やったー！ご飯だわん」

🐶「でも、毎日これをやるのは面倒だわん」

今日は、🐶が頑張らなくても、
- 特定の時間
- 特定のチャンネルに
- 任意の曜日に
- 任意のメッセージを送信する
botを作るわん！

# 前提条件・実行環境
- python３以上
- discord.pyのインストール
- disocrdでのアプリ作成・TOKENなどの取得
- botの権限周りの設定が完了している
- 投稿日現在は機能しているが、discordAPIのアップデートによる仕様変更の可能性あり

上記の環境設定が完了していることを前提とします。

# 階層構造
```bash
├── scheduler.py
└── .env
```

# コードの内容
```bash:.env
DISCORD_BOT_TOKEN='自分のTOKEN。公開してはいけない'
DEFAULT_CHANNEL_ID='デフォルトで設定しておくchannelのid'
```
※.envは公開しないでください！

```python:scheduler.py
import datetime
import schedule
import time
import discord
from dotenv import load_dotenv
import os
import asyncio

# .envファイルを読み込みます
load_dotenv()
TOKEN = os.getenv('DISCORD_BOT_TOKEN')
DEFAULT_CHANNEL_ID = int（os.getenv('DEFAULT_CHANNEL_ID'))

# 基本的な設定
intents = discord.Intents.default()
intents.messages = True
client = discord.Client(intents=intents)

# botの起動と同時に走る関数
@client.event
async def on_ready():
    print(f'Logged in as {client.user}')  # ログインしたユーザーの名前を出力
    await send_message("起動しました", DEFAULT_CHANNEL_ID)

# メッセージを送るための基本的な関数
async def send_message(msg, channel_id):
    try:
        channel = client.get_channel(channel_id)
        if channel:
            await channel.send(msg)
            print(f'Message sent to channel {channel.name}')
        else:
            print(f'Channel with ID {channel_id} not found.')
    except Exception as e:
        print(f'Error sending message: {e}')

# job通知を非同期で実行
async def job(msg, channel_id):
    now = datetime.datetime.now()
    print(str(now) + " 通知したよ")
    await send_message(str(now) + " : " + msg, channel_id)

# 曜日の判定
def schedule_job(msg, weekdays, channel_id):
    now = datetime.datetime.now()
    if now.weekday() in weekdays:
        client.loop.call_soon_threadsafe(asyncio.create_task, job(msg, channel_id))

# スケジュール設定 ここの部分で個別に設定していく
schedule.every().day.at("16:13").do(lambda: schedule_job("毎日", range(7), DEFAULT_CHANNEL_ID))  # 0-6: Monday to Sunday
schedule.every().day.at("16:13").do(lambda: schedule_job("火曜日", [1], DEFAULT_CHANNEL_ID))  # 1: Tuesday
schedule.every().day.at("16:13").do(lambda: schedule_job("スケジュール", range(5), DEFAULT_CHANNEL_ID))  # 0-4: Monday to Friday

# スケジュールを実行する関数
def run_schedule():
    while True:
        schedule.run_pending()
        time.sleep(60) # 60秒に一度判定を行う

# スケジュール実行を別スレッドで行う
import threading
schedule_thread = threading.Thread(target=run_schedule)
schedule_thread.start()

# ボットの起動
client.run(TOKEN)
```
# 詳細説明
このPythonスクリプトは、Discordボットを使用して定期的なメッセージ送信を管理するものです。以下では、スクリプトの各部分について詳細に説明します。

### 1. ライブラリのインポートと環境変数の読み込み

```python
import datetime
import schedule
import time
import discord
from dotenv import load_dotenv
import os
import asyncio
```

- `datetime`, `schedule`, `time`, `discord`: 時間処理、スケジューリング、Discord API、環境変数の操作に必要なライブラリをインポートします。
- `load_dotenv()`, `os.getenv()`: `.env` ファイルから環境変数を読み込みます。`DISCORD_BOT_TOKEN`と`DEFAULT_CHANNEL_ID`がここで使用されています。

### 2. Discordクライアントのセットアップ

```python
# 基本的な設定
intents = discord.Intents.default()
intents.messages = True
client = discord.Client(intents=intents)
```

- `discord.Client` インスタンスを作成し、イベントを処理するために必要な設定を行います。メッセージの送受信を有効にしています。

### 3. イベントハンドラーの定義

```python
# botの起動と同時に走る関数
@client.event
async def on_ready():
    print(f'Logged in as {client.user}')  # ログインしたユーザーの名前を出力
    await send_message("起動しました", DEFAULT_CHANNEL_ID)
```

- `on_ready()` 関数は、Discordにボットがログインし準備が完了したときに実行されます。`send_message()` を使用して指定されたチャンネルにメッセージを送信します。

### 4. メッセージ送信のための関数

```python
# メッセージを送るための基本的な関数
async def send_message(msg, channel_id):
    try:
        channel = client.get_channel(channel_id)
        if channel:
            await channel.send(msg)
            print(f'Message sent to channel {channel.name}')
        else:
            print(f'Channel with ID {channel_id} not found.')
    except Exception as e:
        print(f'Error sending message: {e}')
```

- `send_message()` 関数は、指定されたチャンネルIDにメッセージを非同期で送信します。チャンネルが見つからない場合やエラーが発生した場合に例外を処理します。

### 5. スケジュール関数と非同期ジョブ

```python
# job通知を非同期で実行
async def job(msg, channel_id):
    now = datetime.datetime.now()
    print(str(now) + " 通知したよ")
    await send_message(str(now) + " : " + msg, channel_id)

# 曜日の判定
def schedule_job(msg, weekdays, channel_id):
    now = datetime.datetime.now()
    if now.weekday() in weekdays:
        client.loop.call_soon_threadsafe(asyncio.create_task, job(msg, channel_id))
```

- `job()` 関数は非同期で実行され、指定されたメッセージとチャンネルIDでメッセージを送信します。
- `schedule_job()` 関数は、特定の曜日にジョブをスケジュールするために使用されます。`schedule.every().day.at()` で指定された時間に、`lambda` を使用して非同期のジョブが実行されます。

### 6. スケジュールの設定と実行

```python
# スケジュール設定 ここの部分で個別に設定していく
schedule.every().day.at("16:13").do(lambda: schedule_job("毎日", range(7), DEFAULT_CHANNEL_ID))  # 0-6: Monday to Sunday
schedule.every().day.at("16:13").do(lambda: schedule_job("火曜日", [1], DEFAULT_CHANNEL_ID))  # 1: Tuesday
schedule.every().day.at("16:13").do(lambda: schedule_job("スケジュール", range(5), DEFAULT_CHANNEL_ID))  # 0-4: Monday to Friday
```

- `schedule.every().day.at()` を使用して、特定の時間にスケジュールを設定します。`lambda` を使用して、非同期の `schedule_job()` 関数を実行し、ジョブをスケジュールします。

### 7. スケジュール実行の管理

```python
# スケジュールを実行する関数
def run_schedule():
    while True:
        schedule.run_pending()
        time.sleep(60) # 60秒に一度判定を行う

# スケジュール実行を別スレッドで行う
import threading
schedule_thread = threading.Thread(target=run_schedule)
schedule_thread.start()
```

- `run_schedule()` 関数は、スケジュールされたジョブが実行されるように定期的に `schedule.run_pending()` を呼び出します。
- `threading.Thread` を使用して、スケジュール実行をメインスレッドとは別のスレッドで行います。

### 8. ボットの起動

```python
# ボットの起動
client.run(TOKEN)
```

- `client.run()` を使用して、Discordボットを起動します。これにより、Discord APIに接続し、イベントの受信や処理が開始されます。

これらの要素が組み合わさって、このスクリプトは指定されたチャンネルに定期的にメッセージを送信する Discord ボットを実現しています。

# 終わりに
🐶「やっった！わざわざご主人に連絡する必要なくなった！」
🐶「明日からは空腹に耐えずに済むぜ〜」