この記事について
みすてむず いず みすきーしすてむず (4) Advent Calendar 2023の12月12日の記事になります。
discord.js Guideを読みながら書いていてつまづいたところを解説しつつ、TypeScriptならではのポイントとかを書き留めておくものです。
前提
- JavaScriptが多少わかる
- Node.jsがインストール済みである
- パッケージマネージャーはnpmを使用するので、それ以外を使用する場合は適宜読み替えてください
- 開発者モードをオンにしてDiscord上のユニークIDが取れる
- Discord BotをDeveloper Potalで作成・設定済みである
- 環境:Ubuntu 22.04 LTS、typescript v5.2.2、discord.js v14.13.0
プロジェクトを作成する
プロジェクトを作成するディレクトリで以下を実行する。
$ npm init
イニシャライズが終わったら以下を実行する。
$ npm install discord.js
さらにTypeScript環境をインストールする。
$ npm install typescript
開発環境のみにインストールする場合は代わりに以下を実行する。
$ npm install typescript --save-dev
npmの型情報をインストールする。
$ npm install @types/node
ts-nodeをインストールする。これは開発環境のみでいいと思う。
$ npm install ts-node --save-dev
tsconfig.jsonを生成する。
% npx tsc --init
初期設定
トークン
Developers Potalのbotを選んで、SETTINGS > Botから取得できる。
トークンをコード中に書くのはセキュリティ的に良くないので、別の形で保存する。
今回はconfig.json
を使用する。ほかのやり方についてはこちらを参照のこと。
なお、トークンはおおむねアルファベットの大文字・小文字・数字・記号が混ざった、長いものである。そうでもない場合はトークンじゃなくてクライアントIDか何かと間違っているかもしれないので、Developer Potalでトークンを取得しなおすこと。
{
"token": "your-token"
}
トークンは決して漏洩させないように注意しなければならない。config.json
等のファイルはGitやSubversionで管理しないこと。
.gitignore
上記注釈の通り、Git管理やnpmパッケージ化の際にいらないファイルが紛れ込まないように設定する。
node_modules/
.env
config.json
build/
最低限必要なのはこの辺。developers | npm Docsを読むと、.npmignore
はパッケージ化の際さらに追加で無視したいものがなければ.gitignore
だけでいいらしい。
tsconfig.json
tsconfig.json内のcompilerOptions
のコメントアウトされている部分から、以下のものを探してコメントアウトを外す。これをしないとconfig.jsonを読み込むコードにエラーが出る。また、outDir
(コンパイル時の出力先)はデフォルト値が./
であるかもしれないが、./build
に分けたほうがいろいろと追いやすい。
"resolveJsonModule": true,
"outDir": "./build",
package.json
package.json
にも書き加えておこう。scripts
にコマンドを書き加える。
"scripts": {
"compile": "tsc -p .",
"start": "node build/index.js"
},
これでnpm run compile
でビルドが、npm run start
で起動ができるようになった。
サーバーに招待する
テスト用のサーバーを作って招待しておこう。
Developer PotalのSETTINGS > OAuth2 > URL Generatorから招待リンクが生成できる。
bot
とapplication.commands
にチェックを入れる。下のBot Permissionsは本番リリースの場合は必要十分なものにチェックを入れる必要があるが、テストの間はサーバーでbotにロールをつけてそこで管理しても大丈夫。
- bot
Discord上でbotとして動作させるために必要 - application.commands
後述するスラッシュコマンドを使用するために必要
メインファイルを作る
ここまでの初期設定お疲れさまでした。ここからはTypeScriptを書いていく。
まずはindex.ts
ファイルを作成する。
内容は以下の通りだ。
import { Client, Events, GatewayIntentBits } from 'discord.js';
// config.jsonの内容が増えたときのことも考えて全部インポートしている
import * as config from './config.json';
const client: Client = new Client({ intents: [GatewayIntentBits.Guilds] });
client.once(Events.ClientReady, c => {
console.log(`Ready! Logged in as ${c.user.tag}`);
});
client.login(config.token);
ClientクラスはBaseClientクラスを継承している。さらにBaseClientクラスはEventEmitterクラスを継承しているので、そのインスタンスはonce
やemit
などのメソッドを持っている。EventEmitterクラスに関して詳しくはこちらを参照。ざっくり説明すると、client.once()
の第一引数にイベントを入れると第二引数の関数がそのイベントが発火したときに1回だけ実行される。第一引数の定義済みイベントはこちらを参照。
実行
うまくできているかの確認を兼ねてさっそく実行してみよう。
$ npm run compile
$ npm run start
うまく行っていればReady! Logged in as (Botのタグ)
と表示されるはずだ。タグとはbot#0000
のような文字列だ。新しい環境だと後ろの#0000
のような部分がないかもしれない。
止めたいときはCtrl + C
などで止めよう。
いろいろなメッセージを送る
チャンネルの取得
まずチャンネルを取得する。今回はサーバーのテキストチャンネル。サーバーのテキストチャンネルでない場合は型の変更が必要。'id'の欄にはstring型でユニークID(数字)を指定。
// client.channels.cache.get(<id>)はChannel | undefinedなので
// 派生型で受ける場合はasで型指定が必要
const channel: BaseGuildTextChannel = client.channels.cache.get('id') as BaseGuildTextChannel;
これでchannel
変数にチャンネルが入る。
文字列の送信
const channel: BaseGuildTextChannel = client.channels.cache.get('id') as BaseGuildTextChannel;
// メッセージの送信自体は1行だけ
await channel.send('Hello!');
これで「Hello!」というメッセージがIDで指定したチャンネルに送られる。
BaseGuildTextChannel#send()
は引数にstringまたはMessagePayload、MessageReplyOptionsが取れる。
返信
// 返信する投稿のあるチャンネルを取得する
const channel: BaseGuildTextChannel = client.channels.cache.get('channelId') as BaseGuildTextChannel;
// チャンネル内から返信する投稿を取得する
// channel.message.fetch()はMessage|Collection<Snowflake, Message>なので
// as Messageをつけないと型エラーが出る
const message: Message = await channel.messages.fetch('messageId') as Message;
// 返信自体は1行だけ
await message.reply('Hello!');
「Hello!」と返信される。
埋め込み
const channel: BaseGuildTextChannel = client.channels.cache.get('id') as BaseGuildTextChannel;
const embedObject = new EmbedBuilder()
.setColor(0xff0000)
.setTitle('タイトル')
.setDescription('埋め込み投稿の例です')
await channel.send({embeds: [embedObject]});
埋め込みの内部にはたくさん要素が入れられる。詳細はこちらを参照。
channel.send()
に渡すembeds
パラメーターは配列を取ることに注意。
ボタン
const channel: BaseGuildTextChannel = client.channels.cache.get('id') as BaseGuildTextChannel;
const button = new ButtonBuilder()
.setCustomId('button1')
.setLabel('Click it!')
.setStyle(ButtonStyle.Primary);
const actionRow = new ActionRowBuilder<ButtonBuilder>()
.addComponents(button);
await channel.send({components: [actionRow]});
カスタムIDはどのボタンが押されたかを取得するときとかに使う。ButtonInteraction#customId
でそのインタラクションで押されたボタンのカスタムIDを取得できる。
ラベルはボタンの表示する文字。
ボタンのスタイルについてはこちらを参照。
Action rows | discord.js Guideにある通り、TypeScriptではActionRowBuilderのジェネリクス型指定が必要です。
スラッシュコマンド
正直よくわかってない。ここを参考に改良しつつ書くと大体動く。
TypeScript化のために変更する部分の参考例を以下に示す。もとはリンク先のcommands/utility/ping.js
である。
- const { SlashCommandBuilder } = require('discord.js');
+ import { CommandInteraction, SlashCommandBuilder } from "discord.js";
module.exports = {
data: new SlashCommandBuilder()
.setName('ping')
.setDescription('Replies with Pong!'),
- async execute(interaction) {
+ async execute(interaction: CommandInteraction) {
await interaction.reply('Pong!');
},
};
interaction
の型を明示することで、execute
内でinteraction
のメソッドやプロパティを使うときにTypeScriptの恩恵を得られる。型のインポートを忘れずに。
インタラクション
スラッシュコマンドやボタンの押下、モーダル(後述)の送信などのイベントをインタラクションと呼ぶ。基本的にインタラクションへの応答は3秒以内に行わないといけない。それ以上かかる場合はinteraction.deferReply()
を使用してDiscordに応答に時間がかかることを通知する必要がある。
なお、interaction.deferReply({ephemeral: true})
とすることで、この応答を一時的なものとすることもできる。
モーダル
モーダルはインタラクションに対する返答のひとつである。初期応答としてしか使用できないため、deferReply()
を使用した場合は使用できない。
const question = new TextInputBuilder()
.setCustomId(`question`)
.setLabel(`好きな色を教えてね`)
.setStyle(TextInputStyle.Short);
const actionRow = new ActionRow<ModalActionRowComponentBuilder>()
.addComponents(question);
const modal = new ModalBuilder()
.setCustomId('modal1')
.setTitle('モーダルの例です');
modal.addComponents(actionRow);
await interaction.showModal(modal);
ModalBuilder#addComponents()
は残余引数を取るので、複数の引数を渡すことで複数のActionRowを一度にモーダルに追加することもできる。
モーダルにおいて、ActionRowBuilder
はTextInputBuilder
を1つだけ持てる。また、ModalBuilder
はActionRowBuilder
を5つまで持てる。
その他ポイント
インタラクションを発生させた人にだけ見える返信
ボタンを押した人やモーダルを送信した人、スラッシュコマンドを使用した人にだけ見える返信を送ることができる。以下において、interaction
はBaseInteraction
かその派生型である。
interaction.reply({content: 'あなたにしか見えない', ephemeral: true});
ephemeral
オプションをtrue
にする。
Collectionを利用しよう
各クラスのchache()
等のメソッドで返り値として手に入るCollection
クラスはMap
を継承している。そのため、Map
の持っているメソッドはそのまま使えるが、Collection
クラスのメソッドのほうが早いことが多い。特にサーバーのユーザーに対して.has()
2回とかするとインタラクションの期限の3秒がすぐ終わる(体感)ので、.hasAny()
または.hasAll()
を使うことを推奨する。
まとめ
discord.js GuideをTypeScript向けに変えつつ軽くまとめたものなので、わからなかったら参考文献のリンクを読もう。
参考文献
- 一番強いけど英語 Introduction | discord.js Guide
- ちょっと古いけど日本語 discord.js Japan User Group