discord.js を使用して気軽に日程調整の Discord Bot を作成する

### はじめに
最近、友人とゲームをすることが増えたのですが
社会人がほとんどのため、みんなの都合を合わせるのが大変だったりします。

今回は簡易的に日程調整を行えるような Bot を作成したので共有します。

https://github.com/Toya-Onodera/DiscordScheduleManagementBot

一応 GitHub でコードは管理していますが、簡単に作成できるのでみなさん気軽にやってみてください！
よろしくお願いします 🙇🏻‍♂️

### 流れ
<img width=640 src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276546/a087235e-33cd-c104-5de3-3edf0b8db75b.png" alt="Discord Bot 導入の流れ" />

- Discord 上で動く Bot を作成する
- メンションされたら Bot が日程を提示するように実装する
- Bot を永続化する

### Discord 上で動く Bot を作成する
[誰でも作れる！Discord Bot（基礎編）- https://note.com/exteoi/n/nf1c37cb26c41](https://note.com/exteoi/n/nf1c37cb26c41) を参考に進めました。
とてもわかりやすく説明されており、こちらの説明を読めば Bot の作成はできると思うので __コード部分以外は割愛します。__
また、普段から JavaScript に触れているのでそのまま [Discord.js](https://discord.js.org/#/) を使用します。

https://note.com/exteoi/n/nf1c37cb26c41

### メンションされたら Bot が日程を提示するように実装する

<details><summary>コード全文</summary>
<div>

```javascript
import http from "http";
import {Client} from "discord.js";
import dotenv from "dotenv";
import dayjs from "dayjs";
import "dayjs/locale/ja";

/******************** Discord Bot ********************/
const client = new Client({disableEveryone: true});
dotenv.config();
dayjs.locale("ja");

const connectDiscord = () => client.login(process.env.DISCORD_BOT_TOKEN);

const toNextWeekends = () => {
    const japanNowDate = dayjs();
    const weekends = [5, 6, 7];

    return weekends.map((d, n) => {
        // 日曜日だったら次の週末を提示する
        const diffDay = d - japanNowDate.day();
        const targetDate = japanNowDate.add(diffDay, "day");

        return `${n + 1}: ${targetDate.format("YYYY/MM/DD(ddd)")}`;
    }).join("\n");
};

if (process.env.DISCORD_BOT_TOKEN === undefined) {
    console.log("DISCORD_BOT_TOKEN does not exist.");
    process.exit(0);
}

else {
    connectDiscord();
}

client.once("ready", () => {
    console.log("This bot is running...");

    // Bot オンラインであれば、「Bot をプレイ中と表示される」
    client.user.setPresence({activity: {name: "Bot"}});
});

client.on("message", async (message) => {
    // 自分のメッセージには反応しないようにする
    if (message.author.bot) {
        return;
    }

    // メンションされる & @everyone や @here ではない & 単独メンションのときに動作する
    if ((message.mentions.users.find((e) => e.username === client.user.username) && message.mentions.users.array().length === 1)
        || (message.mentions.roles.find((e) => e.name === client.user.username) && message.mentions.roles.array().length === 1)) {
        const sentMessage = await message.channel.send(`@everyone \n${toNextWeekends()}`);
        try {
            await sentMessage.react("1️⃣");
            await sentMessage.react("2️⃣");
            await sentMessage.react("3️⃣");
            await sentMessage.react("❌");
        } catch (err) {
            console.error(err);
        }
    }
});

/******************** Node Server ********************/
const httpServer = http.createServer();

httpServer.on("request", (request, response) => {
    connectDiscord();
    response.writeHead(200, { "Content-Type": "text/plain" });
    response.end("Discord bot is active now \n");
});

httpServer.listen(3000);
```

</div>
</details>

まず、メンションされたら動作するように実装します。

```javascript
client.on("message", async (message) => {
    ...

    // メンションされる & @everyone や @here ではない & 単独メンションのときに動作する
    if ((message.mentions.users.find((e) => e.username === client.user.username) && message.mentions.users.array().length === 1)
        || (message.mentions.roles.find((e) => e.name === client.user.username) && message.mentions.roles.array().length === 1)) {
        const sentMessage = await message.channel.send(`@everyone \n${toNextWeekends()}`);
        try {
            await sentMessage.react("1️⃣");
            await sentMessage.react("2️⃣");
            await sentMessage.react("3️⃣");
            await sentMessage.react("❌");
        } catch (err) {
            console.error(err);
        }
    }
});
```

`client.on("message", ...)` でメッセージが送信されたイベントを監視できます。（返り値として `MessageMentions` が返ってきます）

[MessageMentions - discord.js](https://discord.js.org/#/docs/main/stable/class/MessageMentions) を見ると、`message.mentions.users` と `message.mentions.roles` でユーザが誰にメンションをしたかを確認できそうです。
通常のメンションとロールでのメンションを想定して両方で動作できるように実装します。

もう少し綺麗にチェックする方法はありそうですが、ひとまず動作することを最優先にしていたためこのような実装にしました。

その後、全体メンションで日付を表示してリアクションが行えるように発言を行うことで Bot の役目を全うします。
`toNextWeekends()` では以下のように実装を行って週末を逆算し、表示するように実装しています。

```javascript
const toNextWeekends = () => {
    const japanNowDate = dayjs();
    const weekends = [5, 6, 7];

    return weekends.map((d, n) => {
        // 日曜日だったら次の週末を提示する
        const diffDay = d - japanNowDate.day();
        const targetDate = japanNowDate.add(diffDay, "day");

        return `${n + 1}: ${targetDate.format("YYYY/MM/DD(ddd)")}`;
    }).join("\n");
};
```
[dayjs](https://github.com/iamkun/dayjs) といった node.js で日付を簡単に扱える npm モジュールが存在しているのでそれを使用します。
日付は `5(金)`、`6(土)`、`7(日)` で扱われているので、差分を計算して週末を出します。
`dayjs().add(指定した日付との差分, "day")` で指定の曜日を出してくれるのでとても便利です。
最終的に `.map()` で生成した配列を `.join("\n")` で改行を行いつつ文字列に変換することでテンプレート文字列の中で使用できるようにしています。

### Bot を永続化する
今回、Bot は Glitch というサービスを使用しています。
Glitch は無料枠だと 5分に1回サービスが落ちるので Google Apps Script を使用して定期的に起動させる必要があります。

```javascript
const wakeGlitch = () => {
    const GLITCH_URL = "YOUR GLITCH URL";
    const json = { "type": "wake" };
    const params = {
        "contentType": "application/json; charset=utf-8",
        "method": "post",
        "payload": json,
        "headers": json,
        "muteHttpExceptions": true
    };
  
    response = UrlFetchApp.fetch(GLITCH_URL, params);
};
```

これを定期的に実行するように Google Apps Script で設定します。
詳しくは前述した [誰でも作れる！Discord Bot（基礎編）- https://note.com/exteoi/n/nf1c37cb26c41](https://note.com/exteoi/n/nf1c37cb26c41) を参考にお願いします。

### 結果
<img alt="Bot のイメージ図" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276546/905e0043-2e5f-f8b7-fa42-68ab67f4c210.gif">

まだまだ改良の余地はありますが、とりあえず Bot を作成して簡易的なスケジュール管理を行うといった目標は達成できました。

### おわりに
Bot を無料で作成して運用まで簡単に作成できますね。皆さんも何か作ってみてはいかがでしょうか。
機械学習を組み合わせて画像で遊べる Bot とかを作っても良いかもしれないですね。
この記事では [Discord.js](https://discord.js.org/#/) を使用していますが、他の言語でも実装可能です。
最後まで読んでいただき、ありがとうございました 🙇🏻‍♂️