discord-bot-architect
Discord bot の本番環境向け開発に特化したスキルです。Discord.js(JavaScript)および Pycord(Python)を使用し、gateway intents、スラッシュコマンド、インタラクティブコンポーネント、レート制限、シャーディングまで幅広くカバーします。スケーラブルで実運用に耐える Discord bot を構築したい場合に活用できます。
description の原文を見る
Specialized skill for building production-ready Discord bots. Covers Discord.js (JavaScript) and Pycord (Python), gateway intents, slash commands, interactive components, rate limiting, and sharding.
SKILL.md 本文
Discord Bot Architect
本番環境対応の Discord ボット構築に特化したスキルです。 Discord.js (JavaScript) と Pycord (Python)、ゲートウェイインテント、 スラッシュコマンド、インタラクティブコンポーネント、レート制限、シャーディングに対応しています。
原則
- メッセージ解析より スラッシュコマンド (Message Content Intent は廃止予定)
- インタラクションに対して常に 3 秒以内に応答する
- 必要なインテントのみをリクエスト (特権インテントを最小化)
- レート制限に対して指数バックオフで対応
- 最初からシャーディングを計画 (2500+ ギルドで必須)
- リッチな UX のためにコンポーネント (ボタン、セレクト、モーダル) を使用
- ギルドコマンドでテストしてから、準備完了時にグローバルにデプロイ
パターン
Discord.js v14 基盤
Discord.js v14 とスラッシュコマンドを使用した最新のボット設定
使用場面: JavaScript/TypeScript で Discord ボットを構築、完全なゲートウェイ接続が必要、複雑なインタラクションを構築
// src/index.js
const { Client, Collection, GatewayIntentBits, Events } = require('discord.js');
const fs = require('node:fs');
const path = require('node:path');
require('dotenv').config();
// 最小限の必要インテントでクライアント作成
const client = new Client({
intents: [
GatewayIntentBits.Guilds,
// 必要に応じて追加:
// GatewayIntentBits.GuildMessages,
// GatewayIntentBits.MessageContent, // 特権 - 可能なら避ける
]
});
// コマンド読み込み
client.commands = new Collection();
const commandsPath = path.join(__dirname, 'commands');
const commandFiles = fs.readdirSync(commandsPath).filter(f => f.endsWith('.js'));
for (const file of commandFiles) {
const filePath = path.join(commandsPath, file);
const command = require(filePath);
if ('data' in command && 'execute' in command) {
client.commands.set(command.data.name, command);
}
}
// イベント読み込み
const eventsPath = path.join(__dirname, 'events');
const eventFiles = fs.readdirSync(eventsPath).filter(f => f.endsWith('.js'));
for (const file of eventFiles) {
const filePath = path.join(eventsPath, file);
const event = require(filePath);
if (event.once) {
client.once(event.name, (...args) => event.execute(...args));
} else {
client.on(event.name, (...args) => event.execute(...args));
}
}
client.login(process.env.DISCORD_TOKEN);
// src/commands/ping.js
const { SlashCommandBuilder } = require('discord.js');
module.exports = {
data: new SlashCommandBuilder()
.setName('ping')
.setDescription('Pong で応答します'),
async execute(interaction) {
const sent = await interaction.reply({
content: 'Pinging...',
fetchReply: true
});
const latency = sent.createdTimestamp - interaction.createdTimestamp;
await interaction.editReply(`Pong! Latency: ${latency}ms`);
}
};
// src/events/interactionCreate.js
const { Events } = require('discord.js');
module.exports = {
name: Events.InteractionCreate,
async execute(interaction) {
if (!interaction.isChatInputCommand()) return;
const command = interaction.client.commands.get(interaction.commandName);
if (!command) {
console.error(`No command matching ${interaction.commandName}`);
return;
}
try {
await command.execute(interaction);
} catch (error) {
console.error(error);
const reply = {
content: 'このコマンド実行中にエラーが発生しました',
ephemeral: true
};
if (interaction.replied || interaction.deferred) {
await interaction.followUp(reply);
} else {
await interaction.reply(reply);
}
}
}
};
// src/deploy-commands.js
const { REST, Routes } = require('discord.js');
const fs = require('node:fs');
const path = require('node:path');
require('dotenv').config();
const commands = [];
const commandsPath = path.join(__dirname, 'commands');
const commandFiles = fs.readdirSync(commandsPath).filter(f => f.endsWith('.js'));
for (const file of commandFiles) {
const command = require(path.join(commandsPath, file));
commands.push(command.data.toJSON());
}
const rest = new REST().setToken(process.env.DISCORD_TOKEN);
(async () => {
try {
console.log(`${commands.length} 個のコマンドをリフレッシュしています...`);
// ギルドコマンド (即座、テスト用)
// const data = await rest.put(
// Routes.applicationGuildCommands(CLIENT_ID, GUILD_ID),
// { body: commands }
// );
// グローバルコマンド (反映に最大 1 時間)
const data = await rest.put(
Routes.applicationCommands(process.env.CLIENT_ID),
{ body: commands }
);
console.log(`${data.length} 個のコマンドを正常に登録しました`);
} catch (error) {
console.error(error);
}
})();
構造
discord-bot/ ├── src/ │ ├── index.js # メインエントリーポイント │ ├── deploy-commands.js # コマンド登録スクリプト │ ├── commands/ # スラッシュコマンドハンドラ │ │ └── ping.js │ └── events/ # イベントハンドラ │ ├── ready.js │ └── interactionCreate.js ├── .env └── package.json
Pycord ボット基盤
Pycord (Python) を使用した Discord ボットと application コマンド
使用場面: Python で Discord ボットを構築、async/await パターンを優先、優れたスラッシュコマンド対応が必要
# main.py
import os
import discord
from discord.ext import commands
from dotenv import load_dotenv
load_dotenv()
# インテント設定 - 必要なもののみ有効化
intents = discord.Intents.default()
# intents.message_content = True # 特権 - 可能なら避ける
# intents.members = True # 特権
bot = commands.Bot(
command_prefix="!", # 従来型、スラッシュコマンドを優先
intents=intents
)
@bot.event
async def on_ready():
print(f"{bot.user} でログインしました")
# コマンド同期 (注意深く - シャープエッジを参照)
# await bot.sync_commands()
# スラッシュコマンド
@bot.slash_command(name="ping", description="ボットのレイテンシを確認")
async def ping(ctx: discord.ApplicationContext):
latency = round(bot.latency * 1000)
await ctx.respond(f"Pong! Latency: {latency}ms")
# オプション付きスラッシュコマンド
@bot.slash_command(name="greet", description="ユーザーに挨拶")
async def greet(
ctx: discord.ApplicationContext,
user: discord.Option(discord.Member, "挨拶するユーザー"),
message: discord.Option(str, "カスタムメッセージ", required=False)
):
msg = message or "Hello!"
await ctx.respond(f"{user.mention}, {msg}")
# Cog 読み込み
for filename in os.listdir("./cogs"):
if filename.endswith(".py"):
bot.load_extension(f"cogs.{filename[:-3]}")
bot.run(os.environ["DISCORD_TOKEN"])
# cogs/general.py
import discord
from discord.ext import commands
class General(commands.Cog):
def __init__(self, bot):
self.bot = bot
@commands.slash_command(name="info", description="ボット情報")
async def info(self, ctx: discord.ApplicationContext):
embed = discord.Embed(
title="Bot Info",
description="便利な Discord ボット",
color=discord.Color.blue()
)
embed.add_field(name="Servers", value=len(self.bot.guilds))
embed.add_field(name="Latency", value=f"{round(self.bot.latency * 1000)}ms")
await ctx.respond(embed=embed)
@commands.Cog.listener()
async def on_member_join(self, member: discord.Member):
# Members インテント必須 (特権)
channel = member.guild.system_channel
if channel:
await channel.send(f"ようこそ {member.mention}!")
def setup(bot):
bot.add_cog(General(bot))
構造
discord-bot/ ├── main.py # メインボットファイル ├── cogs/ # コマンドグループ │ └── general.py ├── .env └── requirements.txt
インタラクティブコンポーネントパターン
ボタン、セレクトメニュー、モーダルを使用したリッチな UX
使用場面: インタラクティブユーザーインターフェースが必要、スラッシュコマンドオプション以上のユーザー入力を収集、メニュー、確認、フォーム構築
// Discord.js - ボタンとセレクトメニュー
const {
SlashCommandBuilder,
ActionRowBuilder,
ButtonBuilder,
ButtonStyle,
StringSelectMenuBuilder,
ModalBuilder,
TextInputBuilder,
TextInputStyle
} = require('discord.js');
module.exports = {
data: new SlashCommandBuilder()
.setName('menu')
.setDescription('インタラクティブメニューを表示'),
async execute(interaction) {
// ボタン行
const buttonRow = new ActionRowBuilder()
.addComponents(
new ButtonBuilder()
.setCustomId('confirm')
.setLabel('確認')
.setStyle(ButtonStyle.Primary),
new ButtonBuilder()
.setCustomId('cancel')
.setLabel('キャンセル')
.setStyle(ButtonStyle.Danger),
new ButtonBuilder()
.setLabel('ドキュメンテーション')
.setURL('https://discord.js.org')
.setStyle(ButtonStyle.Link) // リンクボタンはイベント発火しない
);
// セレクトメニュー行 (1 行に 1 つ、5 スロット全て使用)
const selectRow = new ActionRowBuilder()
.addComponents(
new StringSelectMenuBuilder()
.setCustomId('select-role')
.setPlaceholder('ロールを選択')
.setMinValues(1)
.setMaxValues(3)
.addOptions([
{ label: 'Developer', value: 'dev', emoji: '💻' },
{ label: 'Designer', value: 'design', emoji: '🎨' },
{ label: 'Community', value: 'community', emoji: '🎉' }
])
);
await interaction.reply({
content: 'オプションを選択してください:',
components: [buttonRow, selectRow]
});
// 応答を収集
const collector = interaction.channel.createMessageComponentCollector({
filter: i => i.user.id === interaction.user.id,
time: 60_000 // 60 秒タイムアウト
});
collector.on('collect', async i => {
if (i.customId === 'confirm') {
await i.update({ content: '確認しました!', components: [] });
collector.stop();
} else if (i.customId === 'cancel') {
await i.update({ content: 'キャンセルしました', components: [] });
collector.stop();
} else if (i.customId === 'select-role') {
await i.update({ content: `選択しました: ${i.values.join(', ')}` });
}
});
}
};
// モーダル (フォーム)
module.exports = {
data: new SlashCommandBuilder()
.setName('feedback')
.setDescription('フィードバックを送信'),
async execute(interaction) {
const modal = new ModalBuilder()
.setCustomId('feedback-modal')
.setTitle('フィードバック送信');
const titleInput = new TextInputBuilder()
.setCustomId('feedback-title')
.setLabel('タイトル')
.setStyle(TextInputStyle.Short)
.setRequired(true)
.setMaxLength(100);
const bodyInput = new TextInputBuilder()
.setCustomId('feedback-body')
.setLabel('フィードバック')
.setStyle(TextInputStyle.Paragraph)
.setRequired(true)
.setMaxLength(1000)
.setPlaceholder('フィードバックを説明してください...');
modal.addComponents(
new ActionRowBuilder().addComponents(titleInput),
new ActionRowBuilder().addComponents(bodyInput)
);
// モーダル表示 - 最初の応答でなければならない
await interaction.showModal(modal);
}
};
// interactionCreate でモーダル送信を処理
if (interaction.isModalSubmit()) {
if (interaction.customId === 'feedback-modal') {
const title = interaction.fields.getTextInputValue('feedback-title');
const body = interaction.fields.getTextInputValue('feedback-body');
await interaction.reply({
content: `フィードバックをありがとうございます!\n**${title}**\n${body}`,
ephemeral: true
});
}
}
# Pycord - ボタンとビュー
import discord
class ConfirmView(discord.ui.View):
def __init__(self):
super().__init__(timeout=60)
self.value = None
@discord.ui.button(label="確認", style=discord.ButtonStyle.green)
async def confirm(self, button, interaction):
self.value = True
await interaction.response.edit_message(content="確認しました!", view=None)
self.stop()
@discord.ui.button(label="キャンセル", style=discord.ButtonStyle.red)
async def cancel(self, button, interaction):
self.value = False
await interaction.response.edit_message(content="キャンセルしました", view=None)
self.stop()
@bot.slash_command(name="confirm")
async def confirm_cmd(ctx: discord.ApplicationContext):
view = ConfirmView()
await ctx.respond("よろしいですか?", view=view)
await view.wait() # ユーザーインタラクション待機
if view.value is None:
await ctx.followup.send("タイムアウトしました")
# セレクトメニュー
class RoleSelect(discord.ui.Select):
def __init__(self):
options = [
discord.SelectOption(label="Developer", value="dev", emoji="💻"),
discord.SelectOption(label="Designer", value="design", emoji="🎨"),
]
super().__init__(
placeholder="ロールを選択...",
min_values=1,
max_values=2,
options=options
)
async def callback(self, interaction):
await interaction.response.send_message(
f"選択しました: {', '.join(self.values)}",
ephemeral=True
)
class RoleView(discord.ui.View):
def __init__(self):
super().__init__()
self.add_item(RoleSelect())
# モーダル
class FeedbackModal(discord.ui.Modal):
def __init__(self):
super().__init__(title="フィードバック送信")
self.add_item(discord.ui.InputText(
label="タイトル",
style=discord.InputTextStyle.short,
required=True,
max_length=100
))
self.add_item(discord.ui.InputText(
label="フィードバック",
style=discord.InputTextStyle.long,
required=True,
max_length=1000
))
async def callback(self, interaction):
title = self.children[0].value
body = self.children[1].value
await interaction.response.send_message(
f"ありがとうございます!\n**{title}**\n{body}",
ephemeral=True
)
@bot.slash_command(name="feedback")
async def feedback(ctx: discord.ApplicationContext):
await ctx.send_modal(FeedbackModal())
制限
- メッセージ/モーダルあたり 5 ActionRows
- ActionRow あたり 5 ボタン
- ActionRow あたり 1 セレクトメニュー (5 スロット全て使用)
- メッセージあたり最大 5 セレクトメニュー
- セレクトメニューあたり 25 オプション
- モーダルは最初の応答でなければならない (最初にディファーできない)
遅延応答パターン
3 秒以上かかる操作をタイムアウトなしで処理
使用場面: 操作に 3 秒以上かかる、データベースクエリ、API 呼び出し、LLM 応答、ファイル処理または生成
// Discord.js - 遅延応答
module.exports = {
data: new SlashCommandBuilder()
.setName('slow-task')
.setDescription('遅い操作を実行'),
async execute(interaction) {
// 即座にディファー - 3 秒以内!
await interaction.deferReply();
// エフェメラルの場合: await interaction.deferReply({ ephemeral: true });
try {
// これで 15 分かけて完了できます
const result = await slowDatabaseQuery();
const aiResponse = await callOpenAI(result);
// ディファーされた応答を編集
await interaction.editReply({
content: `結果: ${aiResponse}`,
embeds: [resultEmbed]
});
} catch (error) {
await interaction.editReply({
content: 'リクエスト処理中にエラーが発生しました。'
});
}
}
};
// コンポーネント (ボタン、セレクトメニュー) の場合
collector.on('collect', async i => {
await i.deferUpdate(); // 視覚的な変更なしで確認
// または: await i.deferReply({ ephemeral: true });
const result = await slowOperation();
await i.editReply({ content: result });
});
# Pycord - 遅延応答
@bot.slash_command(name="slow-task")
async def slow_task(ctx: discord.ApplicationContext):
# 即座にディファー
await ctx.defer()
# エフェメラルの場合: await ctx.defer(ephemeral=True)
try:
result = await slow_database_query()
ai_response = await call_openai(result)
await ctx.followup.send(f"結果: {ai_response}")
except Exception as e:
await ctx.followup.send("エラーが発生しました")
タイミング
- Initial_response: 3 秒
- Deferred_followup: 15 分
- Ephemeral_note: 最初の応答でのみ設定可能、後で変更できない
Embed ビルダーパターン
プロフェッショナルな見た目のコンテンツ用リッチ埋め込みメッセージ
使用場面: フォーマットされた情報を表示、ステータス更新、ヘルプメニュー、ログ、構造化データ (フィールド、画像)
const { EmbedBuilder, Colors } = require('discord.js');
// 基本的な Embed
const embed = new EmbedBuilder()
.setColor(Colors.Blue)
.setTitle('ボットステータス')
.setURL('https://example.com')
.setAuthor({
name: 'Bot Name',
iconURL: client.user.displayAvatarURL()
})
.setDescription('現在のステータスと統計')
.addFields(
{ name: 'Servers', value: `${client.guilds.cache.size}`, inline: true },
{ name: 'Users', value: `${client.users.cache.size}`, inline: true },
{ name: 'Uptime', value: formatUptime(), inline: true }
)
.setThumbnail(client.user.displayAvatarURL())
.setImage('https://example.com/banner.png')
.setTimestamp()
.setFooter({
text: 'User がリクエスト',
iconURL: interaction.user.displayAvatarURL()
});
await interaction.reply({ embeds: [embed] });
// 複数の Embed (最大 10 個)
await interaction.reply({ embeds: [embed1, embed2, embed3] });
# Pycord
embed = discord.Embed(
title="ボットステータス",
description="現在のステータスと統計",
color=discord.Color.blue(),
url="https://example.com"
)
embed.set_author(
name="Bot Name",
icon_url=bot.user.display_avatar.url
)
embed.add_field(name="Servers", value=len(bot.guilds), inline=True)
embed.add_field(name="Users", value=len(bot.users), inline=True)
embed.set_thumbnail(url=bot.user.display_avatar.url)
embed.set_image(url="https://example.com/banner.png")
embed.set_footer(text="User がリクエスト", icon_url=ctx.author.display_avatar.url)
embed.timestamp = discord.utils.utcnow()
await ctx.respond(embed=embed)
制限
- メッセージあたり 10 Embed
- すべての Embed で総計 6000 文字
- タイトル 256 文字
- 説明 4096 文字
- Embed あたり 25 フィールド
- フィールド名 256 文字
- フィールド値 1024 文字
レート制限処理パターン
Discord API レート制限を適切に処理
使用場面: 大量操作、一括メッセージ送信またはロール割り当て、繰り返される API 呼び出し
// Discord.js は自動的にレート制限を処理しますが、カスタム処理の場合:
const { REST } = require('discord.js');
const rest = new REST({ version: '10' })
.setToken(process.env.DISCORD_TOKEN);
rest.on('rateLimited', (info) => {
console.log(`レート制限! ${info.retryAfter}ms 後に再試行`);
console.log(`ルート: ${info.route}`);
console.log(`グローバル: ${info.global}`);
});
// 一括操作用キューパターン
class RateLimitQueue {
constructor() {
this.queue = [];
this.processing = false;
this.requestsPerSecond = 40; // 50 以下の安全なマージン
}
async add(operation) {
return new Promise((resolve, reject) => {
this.queue.push({ operation, resolve, reject });
this.process();
});
}
async process() {
if (this.processing || this.queue.length === 0) return;
this.processing = true;
while (this.queue.length > 0) {
const { operation, resolve, reject } = this.queue.shift();
try {
const result = await operation();
resolve(result);
} catch (error) {
reject(error);
}
// スロットル: 約 40 リクエスト/秒
await new Promise(r => setTimeout(r, 1000 / this.requestsPerSecond));
}
this.processing = false;
}
}
const queue = new RateLimitQueue();
// 使用法: 200 メッセージをレート制限に遭わずに送信
for (const user of users) {
await queue.add(() => user.send('ようこそ!'));
}
# Pycord/discord.py は自動的にレート制限を処理
# カスタム処理の場合:
import asyncio
from collections import deque
class RateLimitQueue:
def __init__(self, requests_per_second=40):
self.queue = deque()
self.processing = False
self.delay = 1 / requests_per_second
async def add(self, coro):
future = asyncio.Future()
self.queue.append((coro, future))
if not self.processing:
asyncio.create_task(self._process())
return await future
async def _process(self):
self.processing = True
while self.queue:
coro, future = self.queue.popleft()
try:
result = await coro
future.set_result(result)
except Exception as e:
future.set_exception(e)
await asyncio.sleep(self.delay)
self.processing = False
queue = RateLimitQueue()
# 使用法
for member in guild.members:
await queue.add(member.send("ようこそ!"))
Rate_limits
- グローバル: 50 リクエスト/秒
- ゲートウェイ: 120 リクエスト/60 秒
- 特定: 同じチャネルへのメッセージ: 5/5s、一括削除: 1/1s、ギルドメンバーリクエスト: ギルドサイズによる
シャーディングパターン
2500+ サーバーでボットをシャーディングでスケーリング
使用場面: ボットが 2500 ギルドに近づいている (必須)、水平スケーリングが必要、大規模ボット向けメモリ最適化
// Discord.js シャーディングマネージャ
// shard.js (メインエントリ)
const { ShardingManager } = require('discord.js');
const manager = new ShardingManager('./bot.js', {
token: process.env.DISCORD_TOKEN,
totalShards: 'auto', // Discord が最適な数を決定
// または指定: totalShards: 4
});
manager.on('shardCreate', shard => {
console.log(`シャード ${shard.id} を起動しました`);
shard.on('ready', () => {
console.log(`シャード ${shard.id} 準備完了`);
});
shard.on('disconnect', () => {
console.log(`シャード ${shard.id} 接続切断`);
});
});
manager.spawn();
// bot.js - シャーディング用に修正
const { Client } = require('discord.js');
const client = new Client({ intents: [...] });
// シャード情報を取得
client.on('ready', () => {
console.log(`シャード ${client.shard.ids[0]} 準備完了、${client.guilds.cache.size} ギルド`);
});
// クロスシャードデータ
async function getTotalGuilds() {
const results = await client.shard.fetchClientValues('guilds.cache.size');
return results.reduce((acc, count) => acc + count, 0);
}
// すべてのシャードにブロードキャスト
async function broadcastMessage(channelId, message) {
await client.shard.broadcastEval(
(c, { channelId, message }) => {
const channel = c.channels.cache.get(channelId);
if (channel) channel.send(message);
},
{ context: { channelId, message } }
);
}
# Pycord - AutoShardedBot
import discord
from discord.ext import commands
# シャーディングを自動的に処理
bot = commands.AutoShardedBot(
command_prefix="!",
intents=discord.Intents.default(),
shard_count=None # 自動決定
)
@bot.event
async def on_ready():
print(f"{len(bot.shards)} シャードでログインしました")
for shard_id, shard in bot.shards.items():
print(f"シャード {shard_id}: {shard.latency * 1000:.2f}ms")
@bot.event
async def on_shard_ready(shard_id):
print(f"シャード {shard_id} 準備完了")
# シャードあたりのギルドを取得
for shard_id, guilds in bot.guilds_by_shard().items():
print(f"シャード {shard_id}: {len(guilds)} ギルド")
Scaling_guide
- 1-2500 ギルド: シャーディング不要
- 2500+ ギルド: Discord が必須
- 推奨: シャードあたり ~1000 ギルド
- メモリ: 各シャードは別プロセスで実行
シャープエッジ
インタラクションタイムアウト (3 秒ルール)
重要度: 致命的
状況: スラッシュコマンド、ボタン、セレクトメニュー、またはモーダルの処理
症状: ユーザーに「このインタラクションは失敗しました」または「アプリケーションが応答しませんでした」が表示される。 ローカルではコマンドが動作するが、本番では失敗する。 遅い操作が完了しない。
なぜこれが破損: Discord はすべてのインタラクションに 3 秒以内の応答を要求:
- スラッシュコマンド
- ボタンクリック
- セレクトメニュー選択
- コンテキストメニューコマンド
応答前に遅い操作 (データベース、API、ファイル I/O) をすると、 ウィンドウを逃します。ボットが後で正しく要求を処理しても、 Discord はエラーを表示します。
応答後、フォローアップ応答に 15 分かけることができます。
推奨される修正:
即座に応答、後で処理
// Discord.js - 遅い操作用にディファー
module.exports = {
async execute(interaction) {
// ディファーを即座に - 遅い操作の前
await interaction.deferReply();
// エフェメラル: await interaction.deferReply({ ephemeral: true });
// これで 15 分かけることができます
const result = await slowDatabaseQuery();
const aiResponse = await callLLM(result);
// ディファーされた応答を編集
await interaction.editReply(`結果: ${aiResponse}`);
}
};
# Pycord
@bot.slash_command()
async def slow_command(ctx):
await ctx.defer() # 即座に応答
# await ctx.defer(ephemeral=True) # プライベート応答
result = await slow_operation()
await ctx.followup.send(f"結果: {result}")
コンポーネント (ボタン、メニュー) の場合
// メッセージを更新する場合
await interaction.deferUpdate();
// 新しい応答を送信する場合
await interaction.deferReply({ ephemeral: true });
特権インテント設定の不足
重要度: 致命的
状況: ボットがメンバーデータ、プレゼンス、またはメッセージコンテンツが必要
症状: Members インテント: メンバーリストが空、on_member_join が発火しない Presences インテント: ステータスが常に unknown/offline Message content インテント: message.content が空文字列
なぜこれが破損: Discord には 3 つの特権インテントがあり、手動で有効化が必要:
- GUILD_MEMBERS - メンバー参加/退出、メンバーリスト
- GUILD_PRESENCES - オンラインステータス、アクティビティ
- MESSAGE_CONTENT - メッセージテキスト読み込み (コマンドは廃止予定)
これらは以下が必要:
- Discord Developer Portal > Bot > 特権ゲートウェイインテントで有効化
- ボットコードでリクエスト
100+ サーバーでは、使用継続に Discord 認証が必要。
推奨される修正:
ステップ 1: Developer Portal で有効化
1. https://discord.com/developers/applications に移動
2. アプリケーションを選択
3. Bot セクションに移動
4. 特権ゲートウェイインテントまでスクロール
5. 必要なインテントをオンに
ステップ 2: コードでリクエスト
// Discord.js
const { Client, GatewayIntentBits } = require('discord.js');
const client = new Client({
intents: [
GatewayIntentBits.Guilds,
GatewayIntentBits.GuildMembers, // 特権
// GatewayIntentBits.GuildPresences, // 特権
// GatewayIntentBits.MessageContent, // 特権 - 避ける!
]
});
# Pycord
intents = discord.Intents.default()
intents.members = True # 特権
# intents.presences = True # 特権
# intents.message_content = True # 特権 - 避ける!
bot = commands.Bot(intents=intents)
可能ならメッセージコンテンツインテントを避ける
メッセージ解析の代わりにスラッシュコマンド、ボタン、モーダルを使用します。 これらはメッセージコンテンツインテント不要。
コマンド登録レート制限
重要度: 高
状況: スラッシュコマンド登録
症状: コマンドが表示されない。デプロイ時に 429 エラー。 「レート制限中です」メッセージ。 一部ギルドには表示されるがその他にはされない。
なぜこれが破損: コマンド登録はレート制限:
- グローバルコマンド: 1 日 200 作成/更新、反映に最大 1 時間
- ギルドコマンド: ギルドあたり 1 日 200 作成/更新、即座に更新
一般的なミス:
- ボット起動のたびにコマンド登録
- すべてのギルドに個別登録
- ループなしで変更
推奨される修正:
別のデプロイスクリプトを使用 (起動時ではなく)
// deploy-commands.js - 手動で実行、ボット起動時ではなく
const { REST, Routes } = require('discord.js');
const rest = new REST().setToken(process.env.DISCORD_TOKEN);
async function deploy() {
// 開発: ギルドコマンド (即座)
if (process.env.GUILD_ID) {
await rest.put(
Routes.applicationGuildCommands(
process.env.CLIENT_ID,
process.env.GUILD_ID
),
{ body: commands }
);
console.log('ギルドコマンドを即座にデプロイ');
}
// 本番: グローバルコマンド (最大 1 時間)
else {
await rest.put(
Routes.applicationCommands(process.env.CLIENT_ID),
{ body: commands }
);
console.log('グローバルコマンドをデプロイ (最大 1 時間かかる場合あり)');
}
}
deploy();
# Pycord - 起動のたびに同期しない
@bot.event
async def on_ready():
# これをしないでください:
# await bot.sync_commands()
print(f"準備完了! コマンドは既に登録されています。")
# 代わりに、手動で同期またはフラグを使用
if __name__ == "__main__":
if "--sync" in sys.argv:
# 明示的にリクエストされた場合のみ同期
bot.sync_commands_on_start = True
bot.run(token)
テストワークフロー
- 開発中はギルドコマンドを使用 (即座に更新)
- 本番準備ができたときのみグローバルコマンドをデプロイ
- デプロイスクリプトを手動で実行、再起動のたびではなく
ボットトークン公開
重要度: 致命的
状況: ボットトークンの保存または共有
症状: ボットからの認可されていないアクション。 ボットがランダムなサーバーに参加。 ボットがスパムまたは悪意のあるコンテンツを送信。 Discord が無効化後に「無効なトークン」。
なぜこれが破損: ボットトークンはボットの完全な制御を提供。攻撃者は:
- ボットとしてメッセージを送信
- サーバーに参加、招待を作成
- ボットがアクセスできるすべてのデータにアクセス
- ボットが管理者を持つサーバーを乗っ取る可能性
Discord は GitHub に公開されたトークンを積極的にスキャンし、無効化。 一般的な公開ポイント:
- Git にコミット
- Discord 自体で共有
- クライアント側コード
- 公開スクリーンショット
推奨される修正:
トークンをハードコードしない
// 悪い - これをしないでください
const token = 'MTIzNDU2Nzg5MDEyMzQ1Njc4.ABCDEF.xyz...';
// よい - 環境変数
require('dotenv').config();
client.login(process.env.DISCORD_TOKEN);
.gitignore を使用
# .gitignore
.env
.env.local
config.json
トークンが公開された場合
- すぐに Developer Portal に移動
- トークンを再生成
- すべてのデプロイを更新
- ボットアクティビティをレビュー、認可されていないアクションを確認
- 必要に場合は git 履歴をチェック、強制プッシュで削除
環境変数を適切に使用
# .env (コミットしない)
DISCORD_TOKEN=your_token_here
CLIENT_ID=your_client_id
// dotenv で読み込み
require('dotenv').config();
const token = process.env.DISCORD_TOKEN;
ボットに applications.commands スコープがない
重要度: 高
状況: スラッシュコマンドがユーザーに表示されない
症状: ボットはサーバーにいるが、スラッシュコマンドが表示されない。 / を入力してもボットのコマンドが出ない。 コマンドは開発サーバーで動作したがその他では動作しない。
なぜこれが破損: Discord には 2 つの重要な OAuth スコープ:
bot- 従来のボット権限 (メッセージ、リアクションなど)applications.commands- スラッシュコマンド権限
スラッシュコマンドが存在する前に bot スコープのみで招待された多くのボット。
両方のスコープで再招待が必要。
推奨される修正:
正しい招待 URL を生成
https://discord.com/api/oauth2/authorize
?client_id=YOUR_CLIENT_ID
&permissions=0
&scope=bot%20applications.commands
Discord Developer Portal で
- OAuth2 > URL Generator に移動
- 両方を選択:
botapplications.commands
- 必要なボット権限を選択
- 生成された URL を使用
キックなしで再招待
ボットがサーバーに既にいても、新しい招待 URL を使用できます。 これはボットを削除せずに新しいスコープを追加します。
// コードで招待 URL を生成
const inviteUrl = client.generateInvite({
scopes: ['bot', 'applications.commands'],
permissions: [
'SendMessages',
'EmbedLinks',
// 必要なその他の権限を追加
]
});
グローバルコマンドがすぐに表示されない
重要度: 中
状況: グローバルスラッシュコマンドをデプロイ
症状: デプロイ後、コマンドが表示されない。 ギルドコマンドは動作するがグローバルコマンドは動作しない。 1 時間後にコマンドが表示される。
なぜこれが破損: グローバルコマンドは Discord の CDN とキャッシュ用に最大 1 時間かけて すべての Discord サーバーに反映されることができます。 これは設計によるもの。
ギルドコマンドは即座ですが、その特定ギルドのみで動作。
推奨される修正:
開発: ギルドコマンドを使用
// テスト用の即座の更新
await rest.put(
Routes.applicationGuildCommands(CLIENT_ID, GUILD_ID),
{ body: commands }
);
本番: オフピーク時にグローバルコマンドをデプロイ
// 最大 1 時間かけて反映
await rest.put(
Routes.applicationCommands(CLIENT_ID),
{ body: commands }
);
ワークフロー
- ギルドコマンドで開発とテスト (即座)
- 準備完了時にグローバルコマンドをデプロイ
- 最大 1 時間の反映を待つ
- グローバルコマンドを頻繁にデプロイしない
頻繁なゲートウェイ切断
重要度: 中
状況: ボットがランダムにオフラインまたはイベント喪失
症状: ボットが間欠的にオフラインとして表示。 イベントが喪失 (メンバー参加、メッセージ)。 ログに再接続メッセージ。
なぜこれが破損: Discord ゲートウェイは定期的なハートビート必須。問題:
- ブロッキング操作がハートビートを防ぐ
- ネットワーク不安定性
- GC 一時停止による メモリ圧力
- シャーディングなしで多くのギルド (2500+ 必須)
推奨される修正:
イベントループをブロックしない
// 悪い - イベントループをブロック
const data = fs.readFileSync('file.json');
// よい - async
const data = await fs.promises.readFile('file.json');
再接続を適切に処理
client.on('shardResume', (id, replayedEvents) => {
console.log(`シャード ${id} 再開、${replayedEvents} イベント再生`);
});
client.on('shardDisconnect', (event, id) => {
console.log(`シャード ${id} 接続切断`);
});
client.on('shardReconnecting', (id) => {
console.log(`シャード ${id} 再接続中...`);
});
スケール時にシャーディングを実装
// 2500+ ギルド必須
const manager = new ShardingManager('./bot.js', {
token: process.env.DISCORD_TOKEN,
totalShards: 'auto'
});
manager.spawn();
モーダルが最初の応答である必要
重要度: 中
状況: スラッシュコマンドまたはボタンからモーダルを表示
症状: 「インタラクションは既に応答しています」エラー。 モーダルが表示されない。 時々動作するが、その他は動作しない。
なぜこれが破損: モーダルは特別な要件を持つ: モーダルを表示することは、 インタラクションへの最初の応答でなければならない。 以下ができません:
- defer() その後 showModal()
- reply() その後 showModal()
- 3 秒以上経つ その後 showModal()
推奨される修正:
即座にモーダルを表示
// 正しい - モーダルが最初の応答
async execute(interaction) {
const modal = new ModalBuilder()
.setCustomId('my-modal')
.setTitle('入力フォーム');
// 即座に表示 - defer、reply なし
await interaction.showModal(modal);
}
// 悪い - 最初にディファー
async execute(interaction) {
await interaction.deferReply(); // これはできません
await interaction.showModal(modal); // 失敗します
}
最初に何かをチェックする必要な場合
async execute(interaction) {
// クイック同期チェック OK (3 秒以下)
if (!hasPermission(interaction.user.id)) {
return interaction.reply({
content: '権限がありません',
ephemeral: true
});
}
// モーダルを表示 (このパスで最初のインタラクション応答)
await interaction.showModal(modal);
}
検証チェック
ハードコードされた Discord トークン
重要度: エラー
Discord トークンはハードコードされてはいけません
メッセージ: ハードコードされた Discord トークンが検出。環境変数を使用してください。
トークン変数割り当て
重要度: エラー
トークンは環境から来るべき、文字列ではない
メッセージ: トークンが文字列リテラルから割り当て。環境変数を使用してください。
クライアント側コードのトークン
重要度: エラー
Discord 認証情報をブラウザに公開しない
メッセージ: Discord 認証情報がクライアント側に公開。サーバー側のみ。
ディファーなしの遅い操作
重要度: 警告
遅い操作はタイムアウト回避用にディファーすべき
メッセージ: ディファーなしの遅い操作。インタラクションがタイムアウトする可能性。
エラーハンドリングなしのインタラクション
重要度: 警告
インタラクションは適切なエラー用に try/catch を持つべき
メッセージ: エラーハンドリングなしのインタラクション。try/catch を追加。
メッセージコンテンツインテントを使用
重要度: 警告
メッセージコンテンツは特権、スラッシュコマンドを優先
メッセージ: メッセージコンテンツインテント使用。スラッシュコマンド検討。
すべてのインテントをリクエスト
重要度: 警告
必要なインテントのみをリクエスト
メッセージ: すべてのインテントをリクエスト。必要なもののみ有効化。
Ready イベントでコマンド同期
重要度: 警告
ボット起動のたびにコマンドを同期しない
メッセージ: 起動時にコマンドを同期。別のデプロイスクリプトを使用。
ループでコマンド登録
重要度: 警告
個別呼び出しではなく一括登録を使用
メッセージ: ループでコマンド登録。一括登録を使用。
レート制限処理なし
重要度: 情報
一括操作用レート制限処理を検討
メッセージ: レート制限処理なしの一括操作。
コラボレーション
委譲トリガー
- ユーザーが AI 駆動型 Discord ボット が必要 -> llm-architect (LLM を会話型 Discord ボットに統合)
- ユーザーが Slack 統合も必要 -> slack-bot-builder (クロスプラットフォームボットアーキテクチャ)
- ユーザーが音声機能が必要 -> voice-agents (Discord 音声チャネル統合)
- ユーザーがボットデータ用データベースが必要 -> postgres-wizard (ユーザーデータ、サーバー設定、モデレーションログ保存)
- ユーザーがワークフロー自動化が必要 -> workflow-automation (Discord イベントがワークフロートリガー)
- ユーザーが高可用性が必要 -> devops (大規模ボット向けシャーディング、スケーリング、モニタリング)
- ユーザーが支払い統合が必要 -> stripe-specialist (プレミアムボット機能、サブスクリプション管理)
使用時期
リクエストが上記で説明した機能とパターンと明確に一致するときにこのスキルを使用。
制限
- このスキルは上記で説明したスコープと明確に一致するタスクのみで使用。
- 出力を環境特有の検証、テスト、または専門家レビューの代替としない。
- 必要な入力、権限、安全性の境界線、またはプロセス基準が不足している場合は立ち止まり、明確化をリクエスト。
ライセンス: MIT(寛容ライセンスのため全文を引用しています) · 原本リポジトリ
詳細情報
- 作者
- sickn33
- ライセンス
- MIT
- 最終更新
- 不明
Source: https://github.com/sickn33/antigravity-awesome-skills / ライセンス: MIT
関連スキル
doubt-driven-development
重要な判断はすべて、本番環境への展開前に新しい視点から対抗的レビューを実施します。速度より正確性が重要な場合、不慣れなコードを扱う場合、本番環境・セキュリティに関わるロジック・取り消し不可の操作など影響度が高い場合、または後でバグを修正するよりも今検証する方が効率的な場合に活用してください。
apprun-skills
TypeScriptを使用したAppRunアプリケーションのMVU設計に関する総合的なガイダンスが得られます。コンポーネントパターン、イベントハンドリング、状態管理(非同期ジェネレータを含む)、パラメータと保護機能を備えたルーティング・ナビゲーション、vistestを使用したテストに対応しています。AppRunコンポーネントの設計・レビュー、ルートの配線、状態フローの管理、AppRunテストの作成時に活用してください。
desloppify
コードベースのヘルスチェックと技術負債の追跡ツールです。コード品質、技術負債、デッドコード、大規模ファイル、ゴッドクラス、重複関数、コードスメル、命名規則の問題、インポートサイクル、結合度の問題についてユーザーが質問した場合に使用してください。また、ヘルススコアの確認、次の改善項目の提案、クリーンアップ計画の作成をリクエストされた際にも対応します。29言語に対応しています。
debugging-and-error-recovery
テストが失敗したり、ビルドが壊れたり、動作が期待と異なったり、予期しないエラーが発生したりした場合に、体系的な根本原因デバッグをガイドします。推測ではなく、根本原因を見つけて修正するための体系的なアプローチが必要な場合に使用してください。
test-driven-development
テスト駆動開発により実装を進めます。ロジックの実装、バグの修正、動作の変更など、あらゆる場面で活用できます。コードが正常に動作することを証明する必要がある場合、バグ報告を受けた場合、既存機能を修正する予定がある場合に使用してください。
incremental-implementation
変更を段階的に実施します。複数のファイルに影響する機能や変更を実装する場合に使用してください。大量のコードを一度に書こうとしている場合や、タスクが一度では完結できないほど大きい場合に活用します。