Discord

コミュニケーションボイスチャットBot開発APISlash Commandゲーミングコミュニティ

コミュニケーションツール

Discord

概要

Discordは、ゲーム・コミュニティ向けから発展したボイス・テキストチャットプラットフォームです。優秀な音声・動画機能、コミュニティ管理機能とBot機能が充実しています。1億9千万人の月間アクティブユーザーを抱え、開発者コミュニティ、教育機関、オープンソースプロジェクトでの利用が拡大中です。

詳細

Discord(ディスコード)は、2015年にリリースされたボイス・テキストチャットプラットフォームです。当初はゲーマー向けに設計されましたが、現在では1億9千万人の月間アクティブユーザーを抱え、ゲーミング以外の用途でも急速に採用が拡大しています。特に開発者コミュニティ、教育機関、オープンソースプロジェクトでの利用が増加しています。

Discordの最大の特徴は、低遅延・高品質なボイスチャット機能と、開発者フレンドリーなAPI設計です。Discord APIとdiscord.jsライブラリを使用することで、豊富なBot機能を簡単に構築できます。2024年には、Slash Commands機能の大幅改善、discord.js v14の正式リリース、Premium Apps対応など、開発者体験が大幅に向上しました。

Slash Commandsは、従来のテキストベースコマンドと異なり、Discordクライアントに統合された一級の操作方法を提供し、タイプ入力検証、動的選択肢、エフェメラルメッセージ、ポップアップフォーム等の高度な機能を利用できます。また、サーバーレス環境でのBot実行、Webhooksによる外部連携、リッチなEmbed表示など、モダンな開発手法に対応しています。

メリット・デメリット

メリット

  • 優秀な音声品質: 低遅延・高品質なボイスチャット機能
  • Slash Commands: Discord統合のリッチなコマンドインターフェース
  • 豊富なBot API: discord.js、discord.py等の充実したライブラリ
  • コミュニティ管理: ロール、権限、モデレーション機能
  • 無料利用: 基本機能は完全無料で利用可能
  • 開発者フレンドリー: 分かりやすいAPI設計と豊富なドキュメント
  • 活発なコミュニティ: 開発者コミュニティとサポート体制
  • サーバーレス対応: CloudflareWorkers等での軽量Bot実行

デメリット

  • ゲーミング色: ビジネス利用に対するイメージ
  • 学習コスト: 高度なBot開発には知識が必要
  • API制限: レート制限とDaily limit
  • プライバシー懸念: 一部の企業・組織での利用制限
  • UI複雑性: 多機能すぎて初心者には複雑
  • 検索機能: メッセージ検索機能の制限(無料版)

主要リンク

書き方の例

Hello World Bot(discord.js v14)

const { Client, Events, GatewayIntentBits } = require('discord.js');

// Botクライアントの作成
const client = new Client({ 
  intents: [
    GatewayIntentBits.Guilds, 
    GatewayIntentBits.GuildMessages, 
    GatewayIntentBits.MessageContent
  ] 
});

// Bot起動時の処理
client.once(Events.ClientReady, readyClient => {
  console.log(`Ready! Logged in as ${readyClient.user.tag}`);
});

// メッセージ受信時の処理
client.on(Events.MessageCreate, message => {
  if (message.author.bot) return;
  
  if (message.content === 'ping') {
    message.reply('pong!');
  }
});

// Botログイン
client.login(process.env.DISCORD_TOKEN);

Slash Command実装

const { SlashCommandBuilder, REST, Routes } = require('discord.js');

// コマンド定義
const commands = [
  new SlashCommandBuilder()
    .setName('ping')
    .setDescription('Replies with Pong!'),
    
  new SlashCommandBuilder()
    .setName('user')
    .setDescription('Provides information about the user.')
    .addUserOption(option =>
      option
        .setName('target')
        .setDescription('The user to get info about')
        .setRequired(false)),
        
  new SlashCommandBuilder()
    .setName('echo')
    .setDescription('Replies with your input!')
    .addStringOption(option =>
      option
        .setName('input')
        .setDescription('The input to echo back')
        .setRequired(true))
    .addBooleanOption(option =>
      option
        .setName('ephemeral')
        .setDescription('Whether the reply should be ephemeral'))
];

// コマンド登録
const rest = new REST().setToken(process.env.DISCORD_TOKEN);

(async () => {
  try {
    console.log('Started refreshing application (/) commands.');
    
    await rest.put(
      Routes.applicationGuildCommands(process.env.CLIENT_ID, process.env.GUILD_ID),
      { body: commands }
    );
    
    console.log('Successfully reloaded application (/) commands.');
  } catch (error) {
    console.error(error);
  }
})();

Slash Command ハンドラー

const { Events, EmbedBuilder } = require('discord.js');

client.on(Events.InteractionCreate, async interaction => {
  if (!interaction.isChatInputCommand()) return;

  const { commandName } = interaction;

  switch (commandName) {
    case 'ping':
      await interaction.reply('Pong!');
      break;
      
    case 'user':
      const user = interaction.options.getUser('target') || interaction.user;
      const member = interaction.guild.members.cache.get(user.id);
      
      const userEmbed = new EmbedBuilder()
        .setTitle(`User Info: ${user.username}`)
        .setThumbnail(user.displayAvatarURL())
        .addFields(
          { name: 'Username', value: user.username, inline: true },
          { name: 'User ID', value: user.id, inline: true },
          { name: 'Account Created', value: user.createdAt.toDateString(), inline: true }
        )
        .setColor('#0099ff');
        
      if (member) {
        userEmbed.addFields(
          { name: 'Joined Server', value: member.joinedAt.toDateString(), inline: true }
        );
      }
      
      await interaction.reply({ embeds: [userEmbed] });
      break;
      
    case 'echo':
      const input = interaction.options.getString('input');
      const isEphemeral = interaction.options.getBoolean('ephemeral') || false;
      
      await interaction.reply({ 
        content: `Echo: ${input}`,
        ephemeral: isEphemeral
      });
      break;
      
    default:
      await interaction.reply({ 
        content: 'Unknown command!', 
        ephemeral: true 
      });
  }
});

高度なSlash Command(選択肢付き)

const { SlashCommandBuilder } = require('discord.js');

const data = new SlashCommandBuilder()
  .setName('weather')
  .setDescription('Get weather information!')
  .addStringOption(option =>
    option
      .setName('city')
      .setDescription('Select a city')
      .setRequired(true)
      .addChoices(
        { name: 'Tokyo', value: 'tokyo' },
        { name: 'New York', value: 'newyork' },
        { name: 'London', value: 'london' },
        { name: 'Paris', value: 'paris' }
      ))
  .addStringOption(option =>
    option
      .setName('unit')
      .setDescription('Temperature unit')
      .addChoices(
        { name: 'Celsius', value: 'celsius' },
        { name: 'Fahrenheit', value: 'fahrenheit' }
      ));

// コマンド実行
client.on(Events.InteractionCreate, async interaction => {
  if (!interaction.isChatInputCommand()) return;
  
  if (interaction.commandName === 'weather') {
    const city = interaction.options.getString('city');
    const unit = interaction.options.getString('unit') || 'celsius';
    
    // 外部API呼び出し例
    try {
      const weatherData = await getWeatherData(city, unit);
      
      const weatherEmbed = new EmbedBuilder()
        .setTitle(`Weather in ${weatherData.cityName}`)
        .setDescription(weatherData.description)
        .addFields(
          { name: 'Temperature', value: `${weatherData.temp}°${unit === 'celsius' ? 'C' : 'F'}`, inline: true },
          { name: 'Humidity', value: `${weatherData.humidity}%`, inline: true },
          { name: 'Wind Speed', value: `${weatherData.windSpeed} km/h`, inline: true }
        )
        .setColor('#87CEEB')
        .setTimestamp();
        
      await interaction.reply({ embeds: [weatherEmbed] });
    } catch (error) {
      await interaction.reply({ 
        content: 'Failed to fetch weather data!', 
        ephemeral: true 
      });
    }
  }
});

オートコンプリート機能

const { SlashCommandBuilder } = require('discord.js');

// オートコンプリート対応コマンド
const autocompleteCommand = new SlashCommandBuilder()
  .setName('search')
  .setDescription('Search for something!')
  .addStringOption(option =>
    option
      .setName('query')
      .setDescription('Search query')
      .setAutocomplete(true)
      .setRequired(true));

// オートコンプリート応答
client.on(Events.InteractionCreate, async interaction => {
  if (interaction.isAutocomplete()) {
    const command = interaction.commandName;
    
    if (command === 'search') {
      const focusedValue = interaction.options.getFocused();
      
      // 検索候補の生成
      const choices = [
        'JavaScript Tutorial',
        'Discord Bot Guide',
        'API Documentation',
        'Programming Tips',
        'Web Development'
      ];
      
      const filtered = choices.filter(choice => 
        choice.toLowerCase().includes(focusedValue.toLowerCase())
      ).slice(0, 25); // Discord制限: 最大25個
      
      await interaction.respond(
        filtered.map(choice => ({ name: choice, value: choice }))
      );
    }
  }
});

モーダル(ポップアップフォーム)

const { 
  SlashCommandBuilder, 
  ModalBuilder, 
  TextInputBuilder, 
  TextInputStyle, 
  ActionRowBuilder 
} = require('discord.js');

// モーダル表示コマンド
client.on(Events.InteractionCreate, async interaction => {
  if (!interaction.isChatInputCommand()) return;
  
  if (interaction.commandName === 'feedback') {
    const modal = new ModalBuilder()
      .setCustomId('feedbackModal')
      .setTitle('Feedback Form');
      
    const titleInput = new TextInputBuilder()
      .setCustomId('feedbackTitle')
      .setLabel('Title')
      .setStyle(TextInputStyle.Short)
      .setMaxLength(100)
      .setRequired(true);
      
    const bodyInput = new TextInputBuilder()
      .setCustomId('feedbackBody')
      .setLabel('Feedback')
      .setStyle(TextInputStyle.Paragraph)
      .setMaxLength(1000)
      .setRequired(true);
      
    const firstActionRow = new ActionRowBuilder().addComponents(titleInput);
    const secondActionRow = new ActionRowBuilder().addComponents(bodyInput);
    
    modal.addComponents(firstActionRow, secondActionRow);
    
    await interaction.showModal(modal);
  }
});

// モーダル送信処理
client.on(Events.InteractionCreate, async interaction => {
  if (!interaction.isModalSubmit()) return;
  
  if (interaction.customId === 'feedbackModal') {
    const title = interaction.fields.getTextInputValue('feedbackTitle');
    const body = interaction.fields.getTextInputValue('feedbackBody');
    
    const feedbackEmbed = new EmbedBuilder()
      .setTitle('New Feedback Received')
      .addFields(
        { name: 'Title', value: title },
        { name: 'Feedback', value: body },
        { name: 'User', value: interaction.user.username }
      )
      .setColor('#00ff00')
      .setTimestamp();
      
    // フィードバックチャンネルに送信
    const feedbackChannel = interaction.guild.channels.cache.get('FEEDBACK_CHANNEL_ID');
    if (feedbackChannel) {
      await feedbackChannel.send({ embeds: [feedbackEmbed] });
    }
    
    await interaction.reply({ 
      content: 'Thank you for your feedback!', 
      ephemeral: true 
    });
  }
});

Button と Select Menu

const { 
  ButtonBuilder, 
  ButtonStyle, 
  StringSelectMenuBuilder, 
  StringSelectMenuOptionBuilder,
  ActionRowBuilder 
} = require('discord.js');

// ボタン付きメッセージ
client.on(Events.InteractionCreate, async interaction => {
  if (!interaction.isChatInputCommand()) return;
  
  if (interaction.commandName === 'menu') {
    const confirm = new ButtonBuilder()
      .setCustomId('confirm')
      .setLabel('Confirm')
      .setStyle(ButtonStyle.Success);
      
    const cancel = new ButtonBuilder()
      .setCustomId('cancel')
      .setLabel('Cancel')
      .setStyle(ButtonStyle.Danger);
      
    const info = new ButtonBuilder()
      .setCustomId('info')
      .setLabel('More Info')
      .setStyle(ButtonStyle.Secondary);
      
    const row = new ActionRowBuilder()
      .addComponents(confirm, cancel, info);
      
    await interaction.reply({
      content: 'Choose an action:',
      components: [row]
    });
  }
});

// ボタンクリック処理
client.on(Events.InteractionCreate, async interaction => {
  if (!interaction.isButton()) return;
  
  switch (interaction.customId) {
    case 'confirm':
      await interaction.update({ 
        content: '✅ Confirmed!', 
        components: [] 
      });
      break;
      
    case 'cancel':
      await interaction.update({ 
        content: '❌ Cancelled!', 
        components: [] 
      });
      break;
      
    case 'info':
      const select = new StringSelectMenuBuilder()
        .setCustomId('infoSelect')
        .setPlaceholder('Select information type')
        .addOptions(
          new StringSelectMenuOptionBuilder()
            .setLabel('General Info')
            .setDescription('Basic information')
            .setValue('general'),
          new StringSelectMenuOptionBuilder()
            .setLabel('Technical Details')
            .setDescription('Technical specifications')
            .setValue('technical'),
          new StringSelectMenuOptionBuilder()
            .setLabel('Help & Support')
            .setDescription('Get help and support')
            .setValue('support')
        );
        
      const selectRow = new ActionRowBuilder()
        .addComponents(select);
        
      await interaction.update({
        content: 'Select information type:',
        components: [selectRow]
      });
      break;
  }
});

Webhook連携

const { WebhookClient, EmbedBuilder } = require('discord.js');

// Webhook設定
const webhook = new WebhookClient({ 
  url: 'WEBHOOK_URL_HERE' 
});

// GitHub Webhook連携例
app.post('/github-webhook', async (req, res) => {
  const payload = req.body;
  
  if (payload.action === 'opened' && payload.pull_request) {
    const pr = payload.pull_request;
    
    const embed = new EmbedBuilder()
      .setTitle('New Pull Request')
      .setURL(pr.html_url)
      .setDescription(pr.title)
      .addFields(
        { name: 'Author', value: pr.user.login, inline: true },
        { name: 'Repository', value: payload.repository.full_name, inline: true },
        { name: 'Branch', value: `${pr.head.ref}${pr.base.ref}`, inline: true }
      )
      .setColor('#28a745')
      .setTimestamp();
      
    await webhook.send({
      content: '📝 New Pull Request opened!',
      embeds: [embed]
    });
  }
  
  res.status(200).send('OK');
});

// CI/CD通知例
async function sendBuildNotification(status, buildUrl, commit) {
  const color = status === 'success' ? '#28a745' : '#dc3545';
  const emoji = status === 'success' ? '✅' : '❌';
  
  const embed = new EmbedBuilder()
    .setTitle(`${emoji} Build ${status.toUpperCase()}`)
    .setURL(buildUrl)
    .setDescription(`Commit: ${commit.message}`)
    .addFields(
      { name: 'Author', value: commit.author, inline: true },
      { name: 'Branch', value: commit.branch, inline: true },
      { name: 'Duration', value: commit.duration, inline: true }
    )
    .setColor(color)
    .setTimestamp();
    
  await webhook.send({ embeds: [embed] });
}

環境変数設定

# .env ファイル
DISCORD_TOKEN=your-bot-token
CLIENT_ID=your-client-id
GUILD_ID=your-guild-id

# オプション設定
WEBHOOK_URL=your-webhook-url
DATABASE_URL=your-database-url
REDIS_URL=your-redis-url

# API Key(外部連携用)
WEATHER_API_KEY=your-weather-api-key
GITHUB_TOKEN=your-github-token

package.json例

{
  "name": "discord-bot",
  "version": "1.0.0",
  "description": "Advanced Discord bot with slash commands",
  "main": "index.js",
  "scripts": {
    "start": "node index.js",
    "dev": "nodemon index.js",
    "deploy-commands": "node deploy-commands.js"
  },
  "dependencies": {
    "discord.js": "^14.20.0",
    "dotenv": "^16.0.3"
  },
  "devDependencies": {
    "nodemon": "^3.0.0"
  },
  "engines": {
    "node": ">=16.9.0"
  }
}