4
1

Discord.jsをTypeScriptで書こう

Posted at

この記事について

みすてむず いず みすきーしすてむず (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でトークンを取得しなおすこと。

config.json
{
	"token": "your-token"
}

トークンは決して漏洩させないように注意しなければならない。config.json等のファイルはGitやSubversionで管理しないこと。

.gitignore

上記注釈の通り、Git管理やnpmパッケージ化の際にいらないファイルが紛れ込まないように設定する。

.gitignore
node_modules/
.env
config.json
build/

最低限必要なのはこの辺。developers | npm Docsを読むと、.npmignoreはパッケージ化の際さらに追加で無視したいものがなければ.gitignoreだけでいいらしい。

tsconfig.json

tsconfig.json内のcompilerOptionsのコメントアウトされている部分から、以下のものを探してコメントアウトを外す。これをしないとconfig.jsonを読み込むコードにエラーが出る。また、outDir(コンパイル時の出力先)はデフォルト値が./であるかもしれないが、./buildに分けたほうがいろいろと追いやすい。

tsconfig.json(一部)
"resolveJsonModule": true,
tsconfig.json(一部)
"outDir": "./build",

package.json

package.jsonにも書き加えておこう。scriptsにコマンドを書き加える。

package.json(一部)
    "scripts": {
        "compile": "tsc -p .",
        "start": "node build/index.js"
    },

これでnpm run compileでビルドが、npm run startで起動ができるようになった。

サーバーに招待する

テスト用のサーバーを作って招待しておこう。
Developer PotalのSETTINGS > OAuth2 > URL Generatorから招待リンクが生成できる。
botapplication.commandsにチェックを入れる。下のBot Permissionsは本番リリースの場合は必要十分なものにチェックを入れる必要があるが、テストの間はサーバーでbotにロールをつけてそこで管理しても大丈夫。

  • bot
    Discord上でbotとして動作させるために必要
  • application.commands
    後述するスラッシュコマンドを使用するために必要

メインファイルを作る

ここまでの初期設定お疲れさまでした。ここからはTypeScriptを書いていく。
まずはindex.tsファイルを作成する。
内容は以下の通りだ。

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クラスを継承しているので、そのインスタンスはonceemitなどのメソッドを持っている。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!');

image.png

これで「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!');

image.png

「Hello!」と返信される。

埋め込み

const channel: BaseGuildTextChannel = client.channels.cache.get('id') as BaseGuildTextChannel;
const embedObject = new EmbedBuilder()
    .setColor(0xff0000)
    .setTitle('タイトル')
    .setDescription('埋め込み投稿の例です')
await channel.send({embeds: [embedObject]});

image.png

埋め込みの内部にはたくさん要素が入れられる。詳細はこちらを参照。
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]});

image.png

カスタムIDはどのボタンが押されたかを取得するときとかに使う。ButtonInteraction#customIdでそのインタラクションで押されたボタンのカスタムIDを取得できる。
ラベルはボタンの表示する文字。
ボタンのスタイルについてはこちらを参照。

Action rows | discord.js Guideにある通り、TypeScriptではActionRowBuilderのジェネリクス型指定が必要です。

スラッシュコマンド

正直よくわかってない。ここを参考に改良しつつ書くと大体動く。
TypeScript化のために変更する部分の参考例を以下に示す。もとはリンク先のcommands/utility/ping.jsである。

commands/utility/ping.ts
- 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を一度にモーダルに追加することもできる。
モーダルにおいて、ActionRowBuilderTextInputBuilderを1つだけ持てる。また、ModalBuilderActionRowBuilderを5つまで持てる。

その他ポイント

インタラクションを発生させた人にだけ見える返信

ボタンを押した人やモーダルを送信した人、スラッシュコマンドを使用した人にだけ見える返信を送ることができる。以下において、interactionBaseInteractionかその派生型である。

interaction.reply({content: 'あなたにしか見えない', ephemeral: true});

ephemeralオプションをtrueにする。

Collectionを利用しよう

各クラスのchache()等のメソッドで返り値として手に入るCollectionクラスはMapを継承している。そのため、Mapの持っているメソッドはそのまま使えるが、Collectionクラスのメソッドのほうが早いことが多い。特にサーバーのユーザーに対して.has()2回とかするとインタラクションの期限の3秒がすぐ終わる(体感)ので、.hasAny()または.hasAll()を使うことを推奨する。

まとめ

discord.js GuideをTypeScript向けに変えつつ軽くまとめたものなので、わからなかったら参考文献のリンクを読もう。

参考文献

4
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
1