Discordのチャット画面からkintoneアプリに登録されているランチ候補を問い合わせてリスト表示してみよう

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

目次

はじめに

本記事は Discordのチャット画面からランチの候補をkintoneに登録してみよう の続きとして、Discordのモーダルフォームを使ってkintoneに登録したランチ候補を絞り込み、Discordにリスト表示する方法を紹介します。

そのため Discordのチャット画面からランチの候補をkintoneに登録してみよう のサンプルアプリが作成されていることを前提とします。

kintone APIトークンのアクセス権追加

前回作成したkintoneアプリの「APIトークン」の設定で、「アクセス権」に「レコード閲覧」を追加し、「保存」します。

コマンドプログラムの追加

前回作成したサンプルアプリと同じプロジェクトを使用し、コマンドプログラムファイルを追加します。
作成方法の詳細は、前回の コマンドプログラムの作成 を参照してください。

次のコードを参考にプログラムを作成します。
「list-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
/*
 * 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('list-lunch')
    .setDescription('条件に合った日本橋のランチ候補の店名リストを表示します'),
  async execute(interaction) {
    // モーダルを作成します
    const modal = new ModalBuilder()
      .setCustomId('listLunchModal')
      .setTitle('List Lunch Modal');

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

    // テキスト入力コンポーネントを作成します

    const couisineInput = new TextInputBuilder()
      .setCustomId('couisineInput')
      .setLabel('どんなランチの気分ですか?(カフェ、和食、イタリアン、中華、その他 から選択)')
      .setStyle(TextInputStyle.Short)
      .setRequired(true);

    // アクション行にテキスト入力コンポーネントを設定します
    const actionRow = new ActionRowBuilder().addComponents(couisineInput);

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

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

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

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

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

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

1
2
3
data: new SlashCommandBuilder()
  .setName('list-lunch')
  .setDescription('条件に合った日本橋のランチ候補の店名リストを表示します')

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

1
2
3
4
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) の項を参照してください。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// モーダルを作成します
const modal = new ModalBuilder()
  .setCustomId('listLunchModal')
  .setTitle('List Lunch Modal');

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

// テキスト入力コンポーネントを作成します

const couisineInput = new TextInputBuilder()
  .setCustomId('couisineInput')
  .setLabel('どんなランチの気分ですか?(カフェ、和食、イタリアン、中華、その他 から選択)')
  .setStyle(TextInputStyle.Short)
  .setRequired(true);

// アクション行にテキスト入力コンポーネントを設定します
const actionRow = new ActionRowBuilder().addComponents(couisineInput);

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

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

コマンドハンドラープログラムの修正

前回作成したサンプルアプリと同じプロジェクトを使用し、コマンドハンドラープログラムファイルを修正します。
作成方法の詳細は、前回の コマンドハンドラープログラムの作成 を参照してください。

以下のコードを参考に「index.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
 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
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
/*
 * Discord bot command handler sample scripts
 * Copyright (c) 2024 Cybozu
 *
 * Licensed under the MIT License
 * https://opensource.org/license/mit/
 */

// 必要なdiscord.jsクラスを読み込みます
const fs = require('node:fs');
const path = require('node:path');
const {Client, Collection, Events, GatewayIntentBits, EmbedBuilder} = 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;

  if (interaction.customId === 'addLunchModal') {
    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);
      });
  } else if (interaction.customId === 'listLunchModal') {
    const couisine = interaction.fields.getTextInputValue('couisineInput');
    const query = 'couisine in ("' + couisine + '") order by restaurant_name asc limit 100';

    kintoneClient.record.getRecords({app: kintoneAppId, query: query})
      .then(async resp => {
        console.log(resp);
        if (resp.records.length > 0) {
          const embed = new EmbedBuilder()
            .setTitle('ランチ候補リスト')
            .addFields(
              {name: '種類:', value: '\u200B', inline: true},
              {name: couisine, value: '\u200B', inline: true},
              {name: '\u200B', value: '\u200B', inline: true}
            );

          resp.records.forEach(element => {
            embed
              .addFields(
                {name: '店名', value: element.restaurant_name.value, inline: true},
                {name: 'URL', value: element.url_link.value, inline: true},
                {name: '\u200B', value: '\u200B', inline: true}
              );
          });

          await interaction.reply({embeds: [embed]});
        } else {
          await interaction.reply({content: '該当するランチ情報はありませんでした'});
        }
      })
      .catch(err => {
        console.log(err);
      });
  }
});

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

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

必要なdiscord.jsクラスを読み込みます。
今回は「EmbedBuilder」を追加しています。

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

受け取ったインタラクションのモーダルフォームのIDが「listLunchModal」の場合、モーダルに入力されたデータを取得します。
詳細は、discord.js guideの Extracting data from modal submissions (External link) の項を参照してください。

1
2
3
4
5
6
7
if (interaction.customId === 'addLunchModal') {
  // Add Lunch モーダルの処理
} else if (interaction.customId === 'listLunchModal') {
  const couisine = interaction.fields.getTextInputValue('couisineInput');
  const query = 'couisine in ("' + couisine + '") order by restaurant_name asc limit 100';

}

入力された「ランチの種類」からkintoneにレストランのリストを問い合わせます。
取得したレストランの「店名」、「URL」をリスト表示します。
モーダル送信のレスポンスの詳細は、discord.js guideの Responding to modal submissions (External link) の項を参照してください。

今回はランチのリストを表示するのにEmbedsを使用しています。
Embedsの詳細は、discord.js guideの Embeds (External link) の項を参照してください。

 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
kintoneClient.record.getRecords({app: kintoneAppId, query: query})
  .then(async resp => {
    if (resp.records.length > 0) {
      const embed = new EmbedBuilder()
        .setTitle('ランチ候補リスト')
        .addFields(
          {name: '種類:', value: '\u200B', inline: true},
          {name: couisine, value: '\u200B', inline: true},
          {name: '\u200B', value: '\u200B', inline: true}
        );

      resp.records.forEach(element => {
        embed
          .addFields(
            {name: '店名', value: element.restaurant_name.value, inline: true},
            {name: 'URL', value: element.url_link.value, inline: true},
            {name: '\u200B', value: '\u200B', inline: true}
          );
      });

      await interaction.reply({embeds: [embed]});
    } else {
      await interaction.reply({content: '該当するランチ情報はありませんでした'});
    }
  })
  .catch(err => {
    console.log(err);
  });

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

コマンドデプロイメントプログラムは前回の記事で使用したプログラムと同様です。
作成方法の詳細は、前回の コマンドデプロイメントプログラムの作成 を参照してください。

コマンドデプロイメントプログラムの実行

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

1
node deploy-commands.js

動作確認

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

1
node index.js

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

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

モーダルフォームが表示されるのでランチの種類を入力し(カフェ、和食、イタリアン、中華、その他から選択)、送信ボタンをクリックします。

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

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

まとめ

本記事では、前回紹介した Discordのチャット画面からランチの候補をkintoneに登録してみよう を活用して、kintoneアプリに登録されているランチ候補のレストランを表示するプログラムを作成しました。
プログラム内でコマンドを定義すれば、他にもいろいろなレコード情報を取得できます。
業務アプリに応用すれば、直接kintoneアプリにログインしなくても、Discordのチャット上で、必要な情報を獲得できます。

information

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