はじめに
お疲れ様です! @Keichan_15 です!
半年ほど前に以下の記事でChatGPT APIを使用したSlackBotの作成方法について公開しました。
で、最近そのSlackBotで事件が起きたんですよね~~ 僕のせいなんですが
とあるSlackのチャンネルにBotを導入して半年くらいずーっと運用していたのですが、諸事情あってプライベートチャンネルに変更した瞬間に返信がピタリと止んでしまったのです。
なんで~って思って調べてみたら、普通にSlack公式が以下のページで 「パブリックチャンネルだけしか使えんで!」 って書いてたみたいで、完全に見落としてました。嘘やん…。
Outgoing Webhooks are a legacy method of sending notifications to an app about two specific activities:
1. A message was posted in a particular public Slack channel.
ChatGPT APIのクレジットも持て余しちゃってる & 最近遂にGPT-4が使用できるようになったので、何とか活かせないかと考慮した結果、Discord Botとして第2の人生を歩み始めるようにしてあげるのが良いんじゃないかという結論に…!
そこで今回は
- 特定のチャンネルにChatGPTのBotが自動返信
- 複数メンバーのメッセージ送信に対応するスレッド返信
- 継続運用のためのデプロイ
の豪華三本立てでお送りしていきます!
スレッド返信の日本語資料が少なすぎて本当に困難を極めました…マジツラカッタ~~
それではやっていきましょう!!
環境
- WSL2(Ubuntu 20.04.5)
- Node.js(v16.18.0)
- npm(v8.19.2)
- Visual Studio Code(v1.83.1)
当初はおもちゃを動かすくらいの想定だったので、昔にRails環境を構築した時に一緒に入れたNode.jsが入ってたWSL2を選びました。
ぶっちゃけNode.jsが動かせる環境なら何でも良いと思います。
環境準備
まず任意のディレクトリを作成します。本記事ではchat_gpt
にしました。何でも良いです。
作成したディレクトリに移動後、以下のコマンドを入力してpackage.json
を生成します。
$ npm init -y
以下のようなpackage.json
が生成されます。
{
"name": "chat_gpt",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC"
}
今回はindex.js
をbot
というフォルダ内で管理するようにするため、package.json
を以下のように書き換えましょう。(ついでにモジュール環境でimportステートメントを使えるようにもしておきます)
{
"name": "chat_gpt",
"version": "1.0.0",
"description": "",
- "main": "index.js",
+ "main": "bot/index.js",
+ "type": "module",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC"
}
同時にbot
ディレクトリを作成し、index.js
も作成します。
この時点でディレクトリ構成が以下のようになっていれば問題ありません。
次に以下の3つをインストールするため、以下のコマンドを入力します。
discord.js
-
dotenv
(.env 環境変数読み込み用のモジュール) openai
$ npm install discord.js dotenv openai
次に環境変数設定用の.env
ファイルを作成します。
$ touch .env
これで準備は一旦完了です。現時点のディレクトリ構成が以下のようになっていれば問題ありません!
package.json
も以下のように更新されていると思います。
{
"name": "chat_gpt",
"version": "1.0.0",
"description": "",
"main": "bot/index.js",
"type": "module",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"discord.js": "^14.13.0",
"dotenv": "^16.3.1",
"openai": "^4.14.1"
}
}
次にBot側の設定をしていきましょう!
Bot準備
Discord Developer Portalにアクセスします。(ログインしてない場合はログインしてくださいね!)
画面右上のNew Application
をクリックします。
NAME
の欄にBot名を入力し、チェックボックスにチェックを入れてCreate
をクリックします。
サイドバーのsettings
からbot
をクリックし、各設定を以下のように変更しましょう。
変更後はSave Changes
をお忘れなく!
次にBuild-A-Bot
のTOKEN
欄にあるReset Token
をクリックしましょう。
Yes, do it!
をクリック。
表示されたTOKENをコピーしてください。後ほど使用しますのでどこかにメモしておいてくださいね!
次にサイドバーのOAuth2
> URL Generator
をクリックします。
SCOPES
とBOT PERMISSIONS
の設定項目があるので、以下のようにチェックを入れてください。
チェックが終わったら画面の一番下に表示されているURLをコピーしてブラウザに貼り付けましょう。
以下のような画面が表示されるので、Botを導入したいサーバーを選択してはい
をクリックします。
認証
をクリック。
Botがサーバーに入ったことを確認しましょう!
OpenAI APIキー取得
Open AIのAPIキー取得方法については、自身が過去に執筆した以下の記事にて紹介しています。
本記事では取得方法の詳細については割愛させて頂きます。
こちらもDiscordBotと同様、取得したAPIキーは忘れないようメモしておいてくださいね。
.envの編集
環境変数を設定します。.env
に以下の記述を追加してください。
TOKEN=DiscordBotのTOKEN
OPENAI_API_KEY=OpenAIのAPIキー
CHANNEL_ID=Botが返信を送信するチャンネルのID
CHANNEL_ID
の確認方法ですが、導入したいテキストチャンネルを右クリックするとチャンネルIDをコピー
のボタンが出てきます。
index.jsの編集
まず、特定のチャンネルにメッセージが送信された場合に、そのチャンネルにBotがそのまま返信するようにします。
index.js
に以下のコードを設定してください。
import { Client, GatewayIntentBits } from 'discord.js';
import dotenv from 'dotenv';
import OpenAI from 'openai';
// dotenv適用(これで.envに記載の環境変数を使用できる)
dotenv.config()
const openai = new OpenAI({
apiKey: process.env.OPENAI_API_KEY,
});
const client = new Client({ intents: [
GatewayIntentBits.Guilds,
GatewayIntentBits.MessageContent,
GatewayIntentBits.GuildMessages,
GatewayIntentBits.GuildPresences,
GatewayIntentBits.GuildMembers,
] });
client.once('ready', async () => {
// 開始ログ
console.log('ready!');
});
client.on('messageCreate', async (message) => {
// メッセージの送信者がBotの場合 or 特定のチャンネル以外からのメッセージ送信の場合はreturn
if (message.author.bot || message.channel.id != process.env.CHANNEL_ID) return;
try {
// 送信されたメッセージをpromptに設定
const completion = await openai.chat.completions.create({
messages: [{ role: 'user', content: `${message.content}`}],
model: 'gpt-4',
});
if (completion.choices[0].message.content === undefined) throw new Error();
// メッセージが送信されたチャンネルに、GPT-4の返信を送信
await message.channel.send(completion.choices[0].message.content);
} catch (err) {
console.log(err);
};
});
client.login(process.env.TOKEN);
コードの内容は基本的にコメント通りの内容になっています。
何せ日本語資料が少なすぎるので、ドキュメントやGitHubと睨めっこする時間が多かったな~という感じです。
注意点としては今回の記事で使用しているdiscord.jsのバージョンは14になっています。
13との大きな変更点にIntentsの書き方が変わっている所が挙げられるみたいです。 ぶっちゃけ追えてない
13まではFLAGS.GUILD_MESSAGES
のような書き方だったのが、14でGatewayIntentBits.MessageContent
のような書き方に変わっているようです。
ただこれから導入するのであれば基本的にv14
になると思うので、あまり気にしなくても良い所かなと思います。
13→14へのバージョンアップが伴う場合は要注意ポイントになりますね。
動作確認
index.js
の編集が終わったらローカルで一度Botを動作させてみましょう。
ターミナルに以下のコマンドを入力します。
$ node bot/index.js
Botの返信先に設定したチャンネル内でメッセージを入力すると、Botから返信が返ってくることが確認できました!
スレッド返信に対応する
現段階でもChatGPTのBotとして十分な働きをしてくれているのですが、当初の想定ではこのチャンネルに参加しているメンバーなら誰でも使用できるようにしたいと考えていました。
案の定、Botを解禁した瞬間、治安がドエライことに…
そこで各メッセージに対してスレッドを作成することで、どのメッセージに対する返信なのかを分かりやすくするようにしてみたいと考えたわけです。
これが魔境・地獄の始まりなのですが 完成版から見ていきましょう。
index.js
を以下のコードに書き換えてください。
import { Client, GatewayIntentBits } from 'discord.js';
import dotenv from 'dotenv';
import OpenAI from 'openai';
dotenv.config()
const openai = new OpenAI({
apiKey: process.env.OPENAI_API_KEY,
});
const client = new Client({ intents: [
GatewayIntentBits.Guilds,
GatewayIntentBits.MessageContent,
GatewayIntentBits.GuildMessages,
GatewayIntentBits.GuildPresences,
GatewayIntentBits.GuildMembers,
] });
client.once('ready', async () => {
console.log('ready!');
});
client.on('messageCreate', async (message) => {
// メッセージの送信者がBotの場合 or 特定のチャンネル以外からのメッセージ送信の場合はreturn
if (message.author.bot || message.channel.id != process.env.CHANNEL_ID) return;
try {
// 送信されたメッセージをpromptに設定
const completion = await openai.chat.completions.create({
messages: [{ role: 'user', content: `${message.content}`}],
model: 'gpt-4',
});
if (completion.choices[0].message.content === undefined) throw new Error();
+ const thread = await message.startThread({
+ name: message.content,
+ autoArchiveDuration: 60,
+ })
+ await thread.send(completion.choices[0].message.content);
- await message.channel.send(completion.choices[0].message.content);
} catch (err) {
console.log(err);
};
});
client.login(process.env.TOKEN);
該当箇所にコードを追加・削除して再度Botを起動すると、以下のように送信したメッセージに対してスレッドを作成し、スレッド内でBotが返信していることを確認できると思います!
解説
追加を行った箇所について補足します。
const thread = await message.startThread({
...
})
Threads
のMessage#startThread()
は既存メッセージに対してスレッドを作成することができます。
discordのスレッド作成方法は主に以下の2点です。
- チャンネルにスレッドを新規作成する
- メッセージに対してスレッドを新規作成する
今回は送信されたメッセージに対してスレッドを作成したかったため、Message#startThread()
を使用しました。
name: message.content,
autoArchiveDuration: 60,
name:
はスレッドのタイトルを設定することができます。今回は送信されたメッセージをタイトルに設定するようにしました。
autoArchiveDuration:
は自動アーカイブを行う時間を設定します。
その他詳しい内容についてはdiscord.js Guideに細かい記載があるので、ご興味ある方はご覧になられることをおススメします。
このスレッド返信ですが、本当に魔境で日本語資料が少なすぎてしんどかった…。ツライ。
公式ドキュメント(英語)って正義なんやなって。
fly.ioにデプロイして継続運用する
ちなみにここまで作成したBotですが、現状段階ではローカルで起動している間のみBotが動作するため、サーバーを停止してしまうとBotも一緒に停止してしまいます。
そのためBotを継続運用するためにはデプロイが必要になるんですね。
Slack Botの際はGASでデプロイするだけで動作したのですが、DiscordBotはGASでは自動返信を行うことができないんですね…。面倒だな…。
なるべくお金を掛けたくない乞食マンなので、今herokuの代替先としてアツいfly.io
を今回採用しました。
fly.io
の個人的に良いポイントは、CLIで基本的にデプロイまで完結してしまうという点と東京リージョンが標準搭載されている点です。いいねえ。
まずflyctl
をインストールします。
$ curl -L https://fly.io/install.sh | sh
OSによってインストールコマンドが変わるため、以下のページでご自身のOSに沿ったコマンドを入力してください。
次にサインアップ・サインインを行います。自動で画面が開くので、案内に沿って進めていってください。
$ flyctl auth signup
$ flyctl auth login
ログインが完了したら以下のコマンドを入力し、質問内容に沿ってコマンドを使用して回答していきます。
fly.launch
は対話形式でデプロイを自動化するためのコマンドです。
$ fly launch
...
# アプリケーション名を入力(お好きな名前でOK!)
? Choose an app name (leave blank to generate one): keichan-gpt-bot
...
# リージョンを選択(東京リージョンでOK)
? Choose a region for deployment: Tokyo, Japan (nrt)
App will use 'nrt' region as primary
...
Created app 'keichan-gpt-bot' in organization 'personal'
...
If you need custom packages installed, or have problems with your deployment
build, you may need to edit the Dockerfile for app-specific changes. If you
need help, please post on https://community.fly.io.
Now: run 'fly deploy' to deploy your Node.js app.
Now: run 'fly deploy' to deploy your Node.js app.
の表示が出ていればOKです。
ちなみに実行後のディレクトリを確認すると、以下の3つのファイルが追加されていると思います。
.dockerignore
Dockerfile
fly.toml
次にfly.toml
を開き、以下のように編集します。
デフォルトではhttp service
を使用するようになっているため、今回のBot用に変更を加える必要があります。
# fly.toml app configuration file generated for keichan-gpt-bot on 2023-10-28T18:20:20+09:00
#
# See https://fly.io/docs/reference/configuration/ for information about how to use this file.
#
app = "keichan-gpt-bot"
+ primary_region = "nrt"
+ kill_signal = "SIGINT"
+ kill_timeout = 5
+ processes = []
+ [env]
- primary_region = "nrt"
- [build]
- [http_service]
- internal_port = 3000
- force_https = true
- auto_stop_machines = true
- auto_start_machines = true
- min_machines_running = 0
- processes = ["app"]
最後に環境変数を設定します。fly.io
は環境変数の登録がCLIのみでしかできません。
$ flyctl secrets set TOKEN=DiscordBotのTOKEN OPENAI_API_KEY=OpenAIのAPIキー CHANNEL_ID=Botが返信を送信するチャンネルのID
環境変数が登録できているかは以下のコマンドで確認できます。
$ flyctl secrets list
NAME DIGEST CREATED AT
CHANNEL_ID 7e98152c44c44309 21s ago
OPENAI_API_KEY 6080ebcf755537aa 21s ago
TOKEN 5eee98f021cbafb8 21s ago
最後にデプロイをしましょう。以下コマンドを入力します。
$ fly deploy
...
No machines in group app, launching a new machine
Creating a standby machine for 32874ddec54d78
Finished launching new machines
-------
✔ Machine 91857559c53178 [app] was created
-------
上記のような表記が出ていればデプロイは成功です!
ダッシュボードにアクセスするとデプロイしたbotの名前が表示されていることが確認できます。
デプロイが問題無く成功しているかはMonitoring
の画面で確認しましょう。
デプロイ後にアプリケーションが上手く動作しない場合は、このMonitoring
を確認することでエラーの特定につながります。
参考
おわりに
いかがでしたでしょうか。
今回はdiscord.jsとChatGPT APIを使用したDiscordBotを作成し、fly.ioを使用してデプロイまで行いました。
プログラミングを学ぶ上で個人的におススメなのは、こういうしょうもないおもちゃを興味本位で作り始めるのが一番良いのかなと思います。何事も興味が無いとやる気って出ないですし。
そういえばそろそろQiita Advent Calendar 2023の季節がやってきますね。楽しみ~~。
ではまた!