DiscordでmentionしたらChatGPT APIを通して返信してくれるBotを作成する

とりあえずAPIを投げて返信してくれるまでを作成しました。
本当はGCPのCompute EngineのVMなどで実行し続けるのがいいと思いますが、そこまで行くと規模が大きくなってしまうので、一旦ローカルでサーバ上げてる間に返信してくれるのがゴールとします。
ローカルで立ち上げてる間に返信するだけなら無料の範囲ですし、自分がMacでBotを起動中にスリープモードにしてもBotが返信してくれることは確認できました。

![ラウバーン-黒魔道士.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/264910/7a7d88e7-aa12-104c-4a57-cfd6e2a436c3.gif)

https://github.com/Ancient-Scapes/discord-chatgpt-bot

## 環境準備編

作業を始める前に、以下の準備を済ませておきます。

- Discord Botの登録、およびTokenの発行

## Discord Botの登録編

### Botの登録をする

https://discord.com/developers/applications

- ここでNew Applicationから登録します。適切な名前を入力します。
- ↓Botの設定を有効化してください。二段階認証が求められると思うのでそこは対応してください。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/264910/fd10a8c1-7d06-3050-f1b9-64ab5c6d80ce.png)


- PUBLIC BOTは誰でもBotが使えるようになっちゃうので今回の場合トグルをオフにしておきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/264910/192523ec-a5c1-57ad-09e3-192dec65f06f.png)

- メッセージ送信に関わる情報を取得する必要があるため、MESSAGE CONTENT INTENTをオンに
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/264910/c4fee924-7e98-e36c-05b6-1515288294f2.png)

### サーバに適切な権限で招待を行う

- とりあえず、チャンネルに返信してくれるだけだったらこれでいいと思います。
- 返信をするためには他のユーザの入力を見る必要があるため、チャンネルの閲覧権限を付与しておきます。
- チェックを入れたら、下のCopyからURLをコピーし、URLに飛んで適切なサーバに招待してください。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/264910/281faab8-ac0c-ee84-875a-8c60f2b3e252.png)


## OpenAIのAPIキー発行編

https://platform.openai.com/account/api-keys
ここに関しては検索すれば分かると思いますし、恐らく難しいことは無いと思うので割愛します。
ただ、APIキーは一度発行するともう一度見れないようになっているのでご注意ください。
必ずどこかにコピペして控えることをオススメします。
もし忘れてしまった場合は新規にAPIキーを発行し、必要であれば以前のAPIキーを削除してください。

## 返信プログラム動かす編

とりあえず動かすのに、まずは秘匿情報を入力します。
`~/.env` を作成し、以下の情報を入力してください。
BOT_TOKENには登録したBotのTOKEN部分を入力します。(こちらも1度しか表示されないものなので控えておいてください)
API_KEYにはOPENAIのAPIキーを入力してください

```.env
BOT_USER_NAME=BOTユーザ名
BOT_TOKEN=xxx
API_KEY=xxx
```

## 起動編

いつもどおり `npm i` でインストールして `npm run dev` で実行です。ログインできたらなんかログ出ます

```
npm i
npm run dev
```

ローカルで起動しログインが完了したらDiscordでBotに聞きたいことをメンションしましょう。
このときロールではなくユーザ名でメンションする必要があることに注意してください。(1敗)
同名のため間違うことがあります。

![ラウバーン.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/264910/aca1f9cf-aa7f-ed5a-5f53-547ed59600c5.gif)


### 補足

また、このリポジトリを２つ立ち上げて２種類のBotを実行しようとすると環境変数が衝突？するようでうまくいかなくなります。
なので、BOT_USER_NAMEおよびBOT_TOKENは末尾に2をつけるなどしてコードも変える形で対応してください。
Botを1種類しか使用しない人は気にしなくて大丈夫です。


## おまけ

これだけだと面白くないので、プロンプトを弄ります

```typescript
  const body = JSON.stringify({
    model: "gpt-3.5-turbo",
    messages: [
      {
        role: "user",
        content: message + "また、必ず一人称には吾輩を使用し、二人称には貴様を使用してください。なお、口調は厳格な30代男性軍人のような喋り方をするものとします。",
      },
    ],
  });
```


![ラウバーン-黒魔道士.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/264910/7a7d88e7-aa12-104c-4a57-cfd6e2a436c3.gif)


おしまい

みなさんも自分だけの最強のBotを作ってください

## 番外編 使用しているコードの解説

Discord Botを操作する部分はDiscord.jsという外部ライブラリに依存しています。
ChatGPT APIを叩く部分は特に複雑な処理も必要なさそうだったため、cross-fetchを使用し結果を取得しています。

https://github.com/Ancient-Scapes/discord-chatgpt-bot/blob/main/index.ts

### Client作成

dicordjsのclientを作成します。このとき、適切な権限を設定します。
自分はとりあえず必要そうなものを指定しました。

```typescript

const options :ClientOptions = {
  intents: [
    GatewayIntentBits.Guilds,
    GatewayIntentBits.GuildMessages,
    GatewayIntentBits.MessageContent,
  ],
};
const client = new Client(options);
```

### メッセージを確認し、メンションが含まれていたら返信

- Events.MessageCreateでチャンネルにメッセージが送信されたら関数が発火するようになります。
- 早期returnがないとチャンネルにメッセージが送信されるたびに喋りかけてくるウザいBotになるため、ちゃんと自分へのmentionがあるか確認してからAPIを叩きます。
- `message.channel.sendTyping();` を実行するとBotが入力中表示をしてUXが向上します。
- ChatGPT APIのレスポンスが返ってくるまでにはかなり時間がかかるので普通に入力中が途切れますが気にしなくていいです。

うまくmentionされていることを確認したら、あとはAPIに投げて結果をチャンネルに投稿して終了です。

ChatGPT APIに投げる前に、mentionされている部分を切り取らないとそのままリクエストされるため、なんかいい感じに切り取ってください。　自分は<@xxxxxxxxxxxxxx> の >部分までを消すようにしました。

```typescript
client.on(Events.MessageCreate, async (message) => {
  if (message.mentions.users.first()?.username !== process.env.BOT_USER_NAME) return;

  try {
    message.channel.sendTyping();
    const text = await chatCompletion(requestStr(message.content));
    if (!text) throw Error(text);

    await message.channel.send(text);
  } catch (error) {
    console.log(error);
  }
});

// mention部分のtextを削除し、本文のみでAPIにリクエストする
const requestStr = (str: string) => str.substring(str.indexOf(">"), str.length);
```

## 参考記事

https://zenn.dev/erukiti/articles/ts-chatgpt-api