Discordのチャット画面からランチの候補をkintoneに登録してみよう

著者名:Mamoru Fujinoki( Fuji Business Software Inc. (External link) )

目次

はじめに

ゲーマー向けのチャットサービス Discord (External link) が多機能なことで近年脚光を浴びています。
DiscordはBotを導入することで、機能を拡張できます。
今回はDiscord botでチャット画面にモーダルのフォームを表示し、入力された値をkintoneに登録して管理するカスタマイズを紹介します。

開発の流れ

  • kintoneアプリの作成
  • Discord botの作成
  • node.jsによるkintoneとの連携プログラムの開発

kintoneアプリの作成

アプリの設定のフォームタブより以下の画像を参考にkintoneアプリを作成します。

以下のフィールドを追加します。

フィールド名 フィールドタイプ フィールドコード 備考
店名 文字列(1行) restaurant_name 必須項目
種類 ドロップダウン couisine 次の画像を参考に項目を追加
URL リンク url_link WebのURLリンクフィールド

「フォームを保存」して、アプリの「設定」画面に移動します。
「APIトークン」をクリックします。

「生成する」ボタンをクリックします。
生成されたAPIトークンの「アクセス権」に「レコードの追加」をチェックして保存します。

「アプリを更新」ボタンをクリックして、変更を反映します。
以上でアプリの作成は終了です。

Discord botの作成

Discord 開発者ポータル (External link) を開き、「New Application」ボタンをクリックします。

アプリケーション名を入力します。
チェックボックスをチェックして規約に同意します。
「Create」ボタンをクリックします。

以上でBotが作成されました。

Discord botのサーバーへの招待

次に作成したBotを以下の手順でサーバーに招待します。

「Settings」にて「Installation」メニューをクリックします。
「Guild Install」の「Scopes」に「bot」を追加します。
「Install Link」の「Copy」ボタンをクリックします。

別のブラウザーのタブ、またはウィンドウを開き、コピーしたリンクをペーストして開きます。
表示された画面で「サーバーに追加」を選択します。

ドロップダウンより追加したいサーバー名を選択し「認証」ボタンをクリックします。

以上で、Discord botの設定は終了です。

node.js による kintone との連携プログラムの開発

今回はDiscord.jsというJavaScriptライブラリを使用してローカルマシンでDiscordの会話を監視するbotプログラムを作成します。
Discord.jsを使うにはNode.jsのインストールが必要です。
Node.js (External link) のサイトより、ダウンロードしてください。

次にお使いのPCのコマンドターミナルを開き、プロジェクトファイルを格納するフォルダーを作成します。
次のコマンドを参考にフォルダーを作成し、作成されたフォルダーに移動します。
今回のフォルダー名はlunch-botにしています。

1
2
mkdir lunch-bot
cd lunch-bot

node -vでNode.jsがインストールされていることを確認します。
またnpm init -yでプロジェクトの初期化を行います。パッケージの依存関係を記したpackage.jsonファイルが生成されます。

1
2
3
node -v
> v20.15.1
npm init -y

これでNode.jsの準備ができたので、以下のコマンドを実行してDiscord.jsをインストールします。

1
npm install discord.js 

次に以下のコマンドを実行し、kintoneのJS SDKをインストールします。

1
npm install --save @kintone/rest-api-client

これでNode.jsの設定は完了です。

認証情報の管理

Discordやkintoneのトークン情報は「config.json」ファイルで管理します。
プロジェクトのディレクトリに「config.json」ファイルを作成し、以下のフォーマットで認証情報を記述します。

1
2
3
4
5
6
7
8
{
  "clientId": "{ Discord のクライアント Id}",
  "guildId": "{ Discord の サーバー Id}",
  "token": "{ Discord の API トークン}",
  "kintoneDomain": "{https://mydomain.cybozu.com}",
  "kintoneToken": "{作成した API トークン}",
  "kintoneAppId": "{日本橋ランチDBの アプリ Id}"
}

JavaScriptのコード内では以下のようにすると、認証情報を取り出せます。

1
2
const {token} = require('./config.json');
console.log(token);

クライアント ID の入手

Discord Developer Portal (External link) より上記で作成したアプリケーションの設定画面から「OAuth」メニューを選択します。
「Client Information」より「CLIENT ID」の「Copy」ボタンをクリックします。
コピーした値を「config.json」に貼り付けます。

Discord API トークンの入手

次に「Bot」メニューを選択し、「Bot」の設定画面より「Reset Token」をクリックします。

「Copy」ボタンをクリックして、「config.json」にコピーした値を貼り付けます。

サーバー ID の入手

今度はDiscordのクライアント画面を開きます。
上記でBotを招待したサーバーを開き、右クリックでサブメニューを開きます。
メニューの一番下の「サーバーIDをコピー」をクリックして、「config.json」にコピーした値を貼り付けます。

information

サーバーIDをコピーが表示されない場合はユーザアカウントの開発モードを有効にする必要があります。

kintone の認証情報の設定

上記でkintoneのアプリを作成した際に生成したAPIトークン、お使いのkintoneのドメイン情報、アプリのIDを同様に「config.json」に設定します。
以上で認証情報の設定は終了です。

プログラムの作成

discord botにてスラッシュコマンドを実行するには、以下の手順に沿ってのプログラミングが必要になります。

  • コマンドプログラム:コマンドの定義や関数を記述します。
  • コマンドハンドラープログラム:コマンドファイルを読み込み、コマンドを実行するコードを記述します。
  • コマンドデプロイメントプログラム:スラッシュコマンドをDiscordに登録するコードを記述します。

コマンドプログラムの作成

以下のコマンドでプロジェクトのファルダー内に「Commands」フォルダーおよび「utility」サブフォルダーを作成します。

1
2
3
4
mkdir commands
cd commands
mkdir utility
cd utility

以下のコードを参考にプログラムを作成します。
「add-lunch.js」のファイル名で「utility」サブフォルダーの中に保存します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
/*
 * Discord bot slash commands sample scripts
 * Copyright (c) 2024 Cybozu
 *
 * Licensed under the MIT License
 * https://opensource.org/license/mit/
 */

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

module.exports = {
  data: new SlashCommandBuilder()
    .setName('add-lunch')
    .setDescription('日本橋のランチ候補の店名を追加します'),
  async execute(interaction) {
    // モーダルを作成します
    const modal = new ModalBuilder()
      .setCustomId('addLunchModal')
      .setTitle('Add Lunch Modal');

    // モーダルにコンポーネントを追加します

    // テキスト入力コンポーネントを作成します
    const nameInput = new TextInputBuilder()
      .setCustomId('nameInput')
      .setLabel('店名')
      .setStyle(TextInputStyle.Short)
      .setRequired(true);

    const typeInput = new TextInputBuilder()
      .setCustomId('typeInput')
      .setLabel('種類(カフェ、和食、イタリアン、中華、その他 から選択)')
      .setStyle(TextInputStyle.Short)
      .setRequired(false);

    const siteInput = new TextInputBuilder()
      .setCustomId('siteInput')
      .setLabel('ウェッブサイト')
      .setStyle(TextInputStyle.Short)
      .setRequired(false);

    // 1つのアクション行には1つのテキスト入力コンポーネントのみ設定可能です
    const firstActionRow = new ActionRowBuilder().addComponents(nameInput);
    const secondActionRow = new ActionRowBuilder().addComponents(typeInput);
    const thirdActionRow = new ActionRowBuilder().addComponents(siteInput);

    // 入力コンポーネントをモダルに追加します
    modal.addComponents(firstActionRow, secondActionRow, thirdActionRow);

    // モーダルを表示します
    await interaction.showModal(modal);
  },
};

以上でコマンドプログラムの作成は完了です。

コマンドプログラムの解説

スラッシュコマンドでモーダルを表示するためのdiscord.jsクラスのモジュールをロードします。

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

スラッシュコマンドを定義します。

12
13
14
data: new SlashCommandBuilder()
    .setName('add-lunch')
    .setDescription('日本橋のランチ候補の店名を追加します')

スラッシュコマンドの実行時のインタラクションにレスポンスする関数を定義します。

15
16
17
18
async execute(interaction) {
  // モーダルを表示します
    await interaction.showModal(modal);
},

モーダルを定義します。
モーダルに追加するコンポーネントを定義し、アクション行としてモーダルに追加します。
モーダルに追加できるコンポーネントは、1行テキスト(Short)か複数行のテキスト(Paragraph)のみです。

また、アクション行にはひとつのテキストコンポーネントのみ指定でき、最大5行までです。
スラッシュコマンドのレスポンスとしてモーダルを表示します。

モーダルの設定の詳細については discord.js guide のモーダルの項 (External link) を参考にしてください。
スラッシュコマンドの設定に関する詳細は、discord.js guideの Creating slash commands (External link) の項を参照してください。
モーダルの設定に関する詳細は、discord.js guideの Modals (External link) の項を参照してください。

16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
// モーダルを作成します
const modal = new ModalBuilder()
  .setCustomId('addLunchModal')
  .setTitle('Add Lunch Modal');

// モーダルにコンポーネントを追加します

// テキスト入力コンポーネントを作成します
const nameInput = new TextInputBuilder()
  .setCustomId('nameInput')
  .setLabel('店名')
  .setStyle(TextInputStyle.Short)
  .setRequired(true);

const typeInput = new TextInputBuilder()
  .setCustomId('typeInput')
  .setLabel('種類(カフェ、和食、イタリアン、中華、その他 から選択)')
  .setStyle(TextInputStyle.Short)
  .setRequired(false);

const siteInput = new TextInputBuilder()
  .setCustomId('siteInput')
  .setLabel('ウェッブサイト')
  .setStyle(TextInputStyle.Short)
  .setRequired(false);

// 1つのアクション行には1つのテキスト入力コンポーネントのみ設定可能です
const firstActionRow = new ActionRowBuilder().addComponents(nameInput);
const secondActionRow = new ActionRowBuilder().addComponents(typeInput);
const thirdActionRow = new ActionRowBuilder().addComponents(siteInput);

// 入力コンポーネントをモダルに追加します
modal.addComponents(firstActionRow, secondActionRow, thirdActionRow);

// モーダルを表示します
await interaction.showModal(modal);

コマンドハンドラープログラムの作成

以下のコードを参考にプログラムを作成し、プロジェクトフォルダー直下に「index.js」のファイル名で保存します。
フォルダーツリーのディレクトリを「lunch-bot」に戻します。

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
/*
 * Discord bot command handler sample scripts
 * Copyright (c) 2024 Cybozu
 *
 * Licensed under the MIT License
 * https://opensource.org/license/mit/
 */

const fs = require('node:fs');
const path = require('node:path');
const {Client, Collection, Events, GatewayIntentBits} = require('discord.js');
const {token, kintoneDomain, kintoneToken, kintoneAppId} = require('./config.json');

const {KintoneRestAPIClient} = require('@kintone/rest-api-client');

const kintoneClient = new KintoneRestAPIClient({
  baseUrl: kintoneDomain,
  // Use API Token authentication
  auth: {apiToken: kintoneToken}
});

// クライアントインスタンスを新規作成します
const client = new Client({intents: [GatewayIntentBits.Guilds]});

client.commands = new Collection();

const foldersPath = path.join(__dirname, 'commands');
const commandFolders = fs.readdirSync(foldersPath);

for (const folder of commandFolders) {
  const commandsPath = path.join(foldersPath, folder);
  const commandFiles = fs.readdirSync(commandsPath).filter(file => file.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);
    } else {
      console.log(`[警告] ${filePath} のコマンドに必要な "data" あるいは "execute" のプロパティーが抜けています`);
    }
  }
}

// クライアントインスタンスの準備が整った時、一度だけこのコードを実行します
client.once(Events.ClientReady, readyClient => {
  console.log(`準備完了! ${readyClient.user.tag} としてログインしました`);
});

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

  const command = interaction.client.commands.get(interaction.commandName);

  if (!command) {
    console.error(`${interaction.commandName} とマッチするコマンドが見つかりませんでした`);
    return;
  }

  try {
    await command.execute(interaction);
  } catch (error) {
    console.error(error);
    if (interaction.replied || interaction.deferred) {
      await interaction.followUp({content: 'コマンド実行中にエラーが発生しました!', ephemeral: true});
    } else {
      await interaction.reply({content: 'コマンド実行中にエラーが発生しました!', ephemeral: true});
    }
  }
});

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

  const name = interaction.fields.getTextInputValue('nameInput');
  const type = interaction.fields.getTextInputValue('typeInput');
  const web = interaction.fields.getTextInputValue('siteInput');

  const recordData = {
    restaurant_name: {
      value: name
    },
    couisine: {
      value: type
    },
    url_link: {
      value: web
    }
  };
  kintoneClient.record.addRecord({app: kintoneAppId, record: recordData})
    .then(async resp => {
      console.log(resp);
      await interaction.reply({content: `${name} がkintoneに登録されました。${web}`});
    })
    .catch(err => {
      console.log(err);
    });
});

// トークンにて Discord にログインします
client.login(token);

以上でコマンドハンドラープログラムの作成は完了です。

コマンドハンドラープログラムの解説

node.js標準のファイルシステムおよびパスユティリティのモジュールを読み込み、必要なdiscord.jsクラスを読み込みます。
「config.json」ファイルに設定した認証情報を読み込みます。
discord.jsのクライアントインスタンスを生成し、上記で作成したコマンドファイルからコマンドを読み込みます。
client.commandsコレクションに読み込んだコマンドを設定します。
詳細は、discord.js guideの Loading commands file (External link) の項を参照してください。

 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
const fs = require('node:fs');
const path = require('node:path');
const {Client, Collection, Events, GatewayIntentBits} = require('discord.js');
const {token, kintoneDomain, kintoneToken, kintoneAppId} = require('./config.json');

// クライアントインスタンスを新規作成します
const client = new Client({intents: [GatewayIntentBits.Guilds]});

client.commands = new Collection();

const foldersPath = path.join(__dirname, 'commands');
const commandFolders = fs.readdirSync(foldersPath);

for (const folder of commandFolders) {
  const commandsPath = path.join(foldersPath, folder);
  const commandFiles = fs.readdirSync(commandsPath).filter(file => file.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);
    } else {
      console.log(`[警告] ${filePath} のコマンドに必要な "data" あるいは "execute" のプロパティーが抜けています`);
    }
  }
}

スラッシュコマンドの実行時のリスナーを生成します。
受け取ったインタラクションがスラッシュコマンド以外の場合には、ハンドラーから抜け出します。
詳細は、discord.js guideの Receiving command interaction (External link) の項を参照してください。

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

});

受け取ったインタラクションにマッチするスラッシュコマンドを実行します。
詳細は、discord.js guideの Executing commands (External link) の項を参照してください。

53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
const command = interaction.client.commands.get(interaction.commandName);

if (!command) {
  console.error(`${interaction.commandName} とマッチするコマンドが見つかりませんでした`);
  return;
}

try {
  await command.execute(interaction);
} catch (error) {
  console.error(error);
  if (interaction.replied || interaction.deferred) {
    await interaction.followUp({content: 'コマンド実行中にエラーが発生しました!', ephemeral: true});
  } else {
    await interaction.reply({content: 'コマンド実行中にエラーが発生しました!', ephemeral: true});
  }
}

モーダルフォーム送信時のリスナーを生成します。
受け取ったインタラクションがモーダルフォーム以外の場合には、ハンドラーから抜け出します。
詳細は、discord.js guideの Receiving modal submissions (External link) の項を参照してください。

72
73
74
75
client.on(Events.InteractionCreate, async interaction => {
  if (!interaction.isModalSubmit()) return;

});

モーダルに入力されたデータを取得します。
詳細は、discord.js guideの Extracting data from modal submissions (External link) の項を参照してください。

75
76
77
const name = interaction.fields.getTextInputValue('nameInput');
const type = interaction.fields.getTextInputValue('typeInput');
const web = interaction.fields.getTextInputValue('siteInput');

モーダルフォームから取得したデータをkintoneに登録します。
登録が成功した場合、discordのモーダル送信のレスポンスとしてレストラン名とWebサイトのURLを表示します。
モーダル送信のレスポンスの詳細は、discord.js guideの Responding to modal submissions (External link) の項を参照してください。

79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
const recordData = {
  restaurant_name: {
    value: name
  },
  couisine: {
    value: type
  },
  url_link: {
    value: web
  }
};
kintoneClient.record.addRecord({app: kintoneAppId, record: recordData})
  .then(async resp => {
    console.log(resp);
    await interaction.reply({content: `${name} がkintoneに登録されました。${web}`});
  })
  .catch(err => {
    console.log(err);
  });

コマンドデプロイメントプログラムの作成

以下のコードを参考にプログラムを作成し、プロジェクトフォルダー直下に「deploy-commands.js」のファイル名で保存します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
/*
 * Discord bot commands deployment sample scripts
 * Copyright (c) 2024 Cybozu
 *
 * Licensed under the MIT License
 * https://opensource.org/license/mit/
 */

const {REST, Routes} = require('discord.js');
const {clientId, guildId, token} = require('./config.json');
const fs = require('node:fs');
const path = require('node:path');

const commands = [];
// commands ディレクトリーから全ての command フォルダーを取得します
const foldersPath = path.join(__dirname, 'commands');
const commandFolders = fs.readdirSync(foldersPath);

for (const folder of commandFolders) {
  // commands ディレクトリーから全ての command ファイルを取得します
  const commandsPath = path.join(foldersPath, folder);
  const commandFiles = fs.readdirSync(commandsPath).filter(file => file.endsWith('.js'));
  // SlashCommandBuilder#toJSON() により、デプロイ用の各コマンドの出力データを取得します
  for (const file of commandFiles) {
    const filePath = path.join(commandsPath, file);
    const command = require(filePath);
    if ('data' in command && 'execute' in command) {
      commands.push(command.data.toJSON());
    } else {
      console.log(`[警告] ${filePath} のコマンドに必要な "data" あるいは "execute" のプロパティーが抜けています`);
    }
  }
}

// REST モジュールのインスタンスのコンストラクトおよび準備
const rest = new REST().setToken(token);

// コマンドのデプロイ!
(async () => {
  try {
    console.log(`${commands.length} つのアプリケーションの (/) コマンドのリフレシュッシュを開始しました`);

    // guild 内の全てのコマンドを put メソッドでリフレッシュします
    const data = await rest.put(
      Routes.applicationGuildCommands(clientId, guildId),
      {body: commands},
    );

    console.log(`${data.length} つのアプリケーションの (/) コマンドのリロードに成功しました`);
  } catch (error) {
    // エラーをキャッチおよびログ出力します!
    console.error(error);
  }
})();

以上でコマンドデプロイメントプログラムの作成は完了です。

コマンドデプロイメントプログラムの解説

コマンドデプロイメントプログラムの詳細は、discord.js guideの Registering slash commands (External link) の項を参照してください。
コマンドデプロイメントプログラムの作成が完了したら、以下のコマンドを実行してDiscordにコマンドを登録します。

1
node deploy-commands.js

動作確認

コマンドターミナルで次のコマンドを実行し、Discord Botプログラムを実行します。

1
node index.js

起動したLunch BotがDiscordアプリ上でオンライン表示されているのを確認します。

「/add-lunch」コマンドをDiscordアプリで実行します。

モーダルフォームが表示されるので、レストランの情報を入力し、送信ボタンをクリックします。

kintoneへの登録が成功すると「レストラン名」「Webサイト」がDiscordアプリに表示されます。

kintoneのアプリに入力したレストラン情報が登録されていれば成功です。

今回は、サンプルアプリのため、ローカルサーバーを起動して動作確認を行いましたが、「Google Cloud Run」や「Digital Ocean」等のクラウドサービスプロバイダーにデプロイして実運用してください。

まとめ

Discordは、本来ゲーマー向けのコミュニケーションツールですが、Discord Botをカスタマイズすることで機能を拡張できます。
今回のようにkintoneと連携し、モーダルフォームを表示することでデータの管理が容易になり、ビジネスツールとしても大いに活用できます。

information

このTipsは、2024年9月版kintoneとdiscord.js v14.15.3で動作を確認しています。