Replitでユーザー登録してダッシュボードにアクセスすると、+ Create Appが見えるので、選択すると新しいアプリケーションを作るウィザードが表示されます。Choose a TemplateからNode.jsを選択、名前を入力して新しいアプリケーションを作成します。

最初のコードを書く

プロジェクトを作成すると、いくつかの設定ファイルと空のindex.jsからなるプロジェクトが作成されます。

まずはこれから動かす環境内で正しくコードが実行できることを確認するために、簡単なコードを書いて実行してみます。

console.log('Hello, world!')

たったこれだけのコードですが、動作確認には十分です。画面上部の Run ボタンかCtrl + Enterでプログラムを実行すると、

Hello, world!

と表示されます。これで無事に実行環境の確認ができました。

プロジェクトファイルの修正

次にDiscord用のライブラリを導入するために、package.jsonの記述は次のように書き換えます。以下の内容はこの記事を書いたときの最新バージョンなので、Node.js環境やdiscord.jsのバージョンアップに伴い適宜修正をしてください。

"dependencies": {
  "discord.js": "14",
  "@discordjs/builders": "1.11.2",
  "@discordjs/rest": "2.5.0",
  "discord-api-types": "0.38.8",
  "@types/node": "^22.13.11"
}

ライブラリはとにかく最新にすればいいというものではありません。バージョンによって正しく動作するNode.jsのバージョンとライブラリ自体がさらに依存するライブラリのバージョンがあるので、全体として動作する組合せで利用する必要があります。

環境変数を設定する

ボットを登録したときに取得したトークンとApplication IDはボットを動かすために必要となりますが、プログラムコード上に含めるのはセキュリティ上の懸念があります。万が一コードが流出したときに、ボットの乗っ取りが発生してしまうからです。

これを防ぐために環境変数を使います。環境変数はプログラムの実行開始時に外部からコードの実行上重要な値を注入し、コード上で参照できる変数の集まりのことです。

Replitでは同等の機能がSecretという名前で提供されています。IDEの左側にツールの項目があるので、その中からSecretを選択します(分かりにくいですがスクロールできます)。ここで名前と値を設定できるので、それぞれTOKENAPPLICATION_IDという名前で登録します。

環境変数を使わなくてもいいのでは?

もちろんボットトークンを直接コード中に記述することもできますが、そうすると書いたコードをどこかにアップロードしたり共有したりといったことがしにくくなるので、重要な情報はなるべく環境変数で与えたほうが無難です。

自分はそんなことはしないと思うかもしれませんが、人間は往々にして過ちを犯す生き物なので、面倒くさがらず環境変数を使いましょう。

セットアップコードを書く

次に最新バージョンのdiscord.jsのガイドを参照して最初のコードを書きます。

const { Client, Events, GatewayIntentBits } = require('discord.js')
 
const client = new Client({ intents: [GatewayIntentBits.Guilds] })
 
// ボットが起動したときに1回だけ実行されるコード
// この部分はログインしたあとで中身が実行される
client.once(Events.ClientReady, readyClient => {
    console.log(`Ready! Logged in as ${readyClient.user.tag}`)
})
 
// ボットトークンを使ってログインをしてボットを起動する
client.login(process.env.TOKEN)

うまく動けば「Ready! Logged in as XXX」と表示されるでしょう。

コマンドに反応させる

ボットにスラッシュコマンドを送ると返事を返すようにしてみましょう。まずは簡単な例としてAdvanced command creationを参考してコマンドに対して単純に反応するだけの /ping コマンドと /echo を追加します。

const { Client, Events, GatewayIntentBits } = require('discord.js')
const { REST } = require('@discordjs/rest')
const { Routes } = require('discord-api-types/v10')
const { SlashCommandBuilder } = require('@discordjs/builders')
 
const client = new Client({ intents: [GatewayIntentBits.Guilds] })
 
const commands = [
    // /pingコマンドを定義する
    new SlashCommandBuilder()
        .setName('ping')
        .setDescription('ボットが応答します'),
    // /echoコマンドを定義する。入力した文字列をそのまま返す
    new SlashCommandBuilder()
        .setName('echo')
        .setDescription('ボットがオウム返しします')
        .addStringOption(option => option
            .setName('text')
            .setDescription('内容')
            .setRequired(true))
].map(command => command.toJSON())
 
const rest = new REST({ version: '10' }).setToken(process.env.TOKEN);
 
// コマンドを登録する
rest.put(Routes.applicationCommands(process.env.APPLICATION_ID), { body: commands })
  .then(() => console.log('Successfully registered application commands.'))
  .catch(console.error);
 
// ボットが起動したときに1回だけ実行されるコード
// この部分はログインしたあとで中身が実行される
client.once(Events.ClientReady, readyClient => {
    console.log(`Ready! Logged in as ${readyClient.user.tag}`)
})
 
client.on(Events.InteractionCreate, async interaction => {
    if (interaction.isCommand()) {
        if (interaction.commandName == 'ping') {
            interaction.reply('pong')
        } else if (interaction.commandName == 'echo') {
            interaction.reply(interaction.options.getString('text'))
        } else {
            throw new Error('未定義のコマンドです')
        }
    }
})
 
// ボットトークンを使ってログインをしてボットを起動する
client.login(process.env.TOKEN)

ボット起動後にしばらくしてからDiscord上で /ping コマンドと /echo [text] コマンドが使えるようになっているはずです。

コマンドのパラメータは必須・省略可能どちらにもできますし、特定の値に制限もできますので、自分にあったコマンドを作ってみましょう。

Replitの注意点

初期のStarterプランでは開発環境で月間1GB、本番環境で月間10GBのインターネット側への転送上限があります。また、常時起動させ続けることはできず、ユーザーからの報告によれば、数分から30分程度で停止するようです。

無料プランでも動かし続ける裏技とかありませんか?

Replitはアクティビティがないことを検知して稼働を停止するので、定期的になんらかの通信を発生させることによって、無料プランでも稼働させ続けることは可能と考えられますが、プラットフォーム側が意図しているサービスの利用方法であるとは考えにくいので、ここではあえて紹介しません。

おまけ:Pythonでのコード

discord.pyを使ったPythonのコードは次のようになります。discord.jsと違うのは、デコレータによるコマンドごとの関数定義になっているところです。

import os
import discord
from discord.ext import commands
 
intents = discord.Intents.default()
bot = commands.Bot(
    application_id=int(os.environ['APPLICATION_ID']),
    command_prefix=None,
    intents=intents
)
 
# ボット起動後に一度だけ実行される
@bot.event
async def on_ready():
    # スラッシュコマンドを登録する
    await bot.tree.sync()
 
# /ping コマンドに対する応答
@bot.tree.command(name='ping', description='ボットが応答します')
async def ping(interaction: discord.Interaction):
    await interaction.response.send_message('pong')
 
bot.run(os.environ['TOKEN'])

それで次は?

いやいや、次はありません。これで全部です。Discordを介してユーザーとボットがやりとりする方法はこれまで説明してきた通りですし、ユーザーからの入力を受け付けて返答をすることができるようになったので、入力に応じて何かをするだけです。

とは言っても、いきなりオープンワールドに放り出されても困るとは思いますし、せめて最初の草原フィールドくらいは渡り合えるくらいを目標に、次回からは設計やエラー処理、その他の雑多な事項について取り上げて、Discordボットが長期的な運用に耐えうるような仕組み作りや方法について学んでいきます。

第3回 ドメインモデル