discord.js 基礎

Client を作って Gateway に接続、Intents で受け取りたいイベントを宣言、 Events でハンドラを書く。これが基本ループ。

Client

import { Client, GatewayIntentBits, Partials, Events } from "discord.js"

const client = new Client({
  intents: [
    GatewayIntentBits.Guilds,            // サーバー基本情報
    GatewayIntentBits.GuildMessages,     // メッセージイベント
    GatewayIntentBits.MessageContent,    // 本文(Privileged)
    GatewayIntentBits.GuildMembers,      // メンバー情報(Privileged)
    GatewayIntentBits.GuildVoiceStates,  // 音声状態
    GatewayIntentBits.GuildPresences,    // オンライン状態(Privileged)
    GatewayIntentBits.MessageReactions,  // リアクション
    GatewayIntentBits.DirectMessages,    // DM
  ],
  partials: [
    Partials.Message,
    Partials.Channel,
    Partials.Reaction,
  ],
})

client.login(process.env.DISCORD_TOKEN)

Intents の選び方

必要な分だけ宣言。多すぎると無駄な帯域、少なすぎるとイベントが来ない。

Partials

Discord はキャッシュにない古いメッセージなどを「partial」として返す。 partial を扱うなら明示的に Partials を宣言する。古いメッセージのリアクション取得などで必要。

主なイベント

イベント発火
ClientReady接続完了
Errorエラー
Warn警告
Debugデバッグログ
InteractionCreateスラッシュコマンド・ボタン等
MessageCreateメッセージ投稿
MessageUpdate編集
MessageDelete削除
MessageReactionAddリアクション追加
GuildMemberAddメンバー参加
GuildMemberRemove退出
VoiceStateUpdateVC 状態変化
ThreadCreateスレッド作成

イベントハンドラ

client.once(Events.ClientReady, (c) => {
  console.log(`Logged in as ${c.user.tag}`)
})

client.on(Events.MessageCreate, async (msg) => {
  if (msg.author.bot) return                         // bot 同士のループ防止
  if (!msg.content.startsWith("!ping")) return
  await msg.reply("pong")
})

client.on(Events.Error, (err) => console.error(err))

Channel / Guild / User / Member

Discord の登場人物:

取得

const guild = client.guilds.cache.get(GUILD_ID)
const channel = guild?.channels.cache.get(CHANNEL_ID)
const member = await guild?.members.fetch(USER_ID)
const role = guild?.roles.cache.find((r) => r.name === "Admin")

// fetch(API リクエスト・最新値)
const user = await client.users.fetch(USER_ID)

// キャッシュにあれば優先
const cached = guild?.members.cache.get(USER_ID)

cache はメモリ上、fetch は API リクエストで最新。大規模 bot ではキャッシュ多用、特殊な状況でだけ fetch

メッセージを送る

// チャンネルから
const channel = await client.channels.fetch(CHANNEL_ID)
if (channel?.isTextBased()) {
  await channel.send("こんにちは")
}

// メッセージへの返信
await msg.reply("内容")
await msg.reply({ content: "内容", ephemeral: false })

// メンションせずに返信
await msg.reply({ content: "...", allowedMentions: { repliedUser: false } })

ロールの操作

const member = await guild.members.fetch(userId)
const role = guild.roles.cache.find((r) => r.name === "Verified")

await member.roles.add(role)
await member.roles.remove(role)

if (member.roles.cache.has(role.id)) {
  // 持っている
}

権限チェック

import { PermissionFlagsBits } from "discord.js"

if (member.permissions.has(PermissionFlagsBits.ManageMessages)) {
  // 管理者向けの処理
}

// チャンネル単位
const perms = channel.permissionsFor(member)
if (perms?.has(PermissionFlagsBits.SendMessages)) { ... }

キャッシュとレート制限

Discord API にはレート制限がある(route ごとに違う)。 discord.js は内部でキューイング・自動再試行するので、普通の使い方では意識しなくて良い。

Sharding(大規模 bot)

Discord は2,500 サーバー超になると 1 接続では扱えない。Shardに分割する必要がある。

import { ShardingManager } from "discord.js"

const manager = new ShardingManager("./bot.js", {
  totalShards: "auto",
  token: process.env.DISCORD_TOKEN,
})

manager.on("shardCreate", (shard) => console.log(`Launched shard ${shard.id}`))
manager.spawn()

TypeScript セットアップ

npm install -D typescript @types/node tsx
npx tsc --init

tsconfig.json の最低限:

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "Bundler",
    "esModuleInterop": true,
    "strict": true,
    "outDir": "./dist"
  },
  "include": ["src"]
}

ファイル構成(実用パターン)

my-bot/
├── src/
│   ├── index.ts              ← Client 起動
│   ├── events/
│   │   ├── ready.ts
│   │   ├── interactionCreate.ts
│   │   └── messageCreate.ts
│   ├── commands/
│   │   ├── ping.ts
│   │   └── info.ts
│   └── lib/
│       └── deploy-commands.ts
├── .env
├── package.json
└── tsconfig.json

コマンドをファイル単位で動的読み込みするパターンが定石(次ページで詳述)。

注意点