🤖

discord.js でVCの人数制限を簡単に行うBOTをつくる

2023/09/20に公開

Discordサーバ内のVCで人数制限を頻繁に変更する必要がある場合など、チャンネル設定から行うのが面倒なのでコマンドで人数制限を行う事ができるようなBOTを作成しました。
接続しているメンバーのみコマンドを使用できるので、荒らし対策にもなります。
image

コマンドの処理

人数制限を行うコマンド本体の処理について記述していく。

コマンドの登録

BOT起動時などにコマンドを登録する。

const vcTools = new SlashCommandBuilder()
    .setName(commandNames.vcTools)
    .setDescription('ボイスチャンネルで使用できるツールです。')
    .addSubcommandGroup((subcommandGroup: SlashCommandSubcommandGroupBuilder) =>
        subcommandGroup
            .setName('vc')
            .setDescription('ボイスチャンネルで使用できるツールです。')
            .addSubcommand((subcommand: SlashCommandSubcommandBuilder) =>
                subcommand
                    .setName('lock')
                    .setDescription('このボイスチャンネルに人数制限をかけます。')
                    .addIntegerOption((option: SlashCommandIntegerOption) =>
                        option.setName('人数').setDescription('制限人数を指定する場合は1~99で指定してください。').setRequired(false),
                    ),
            ),
    )
    .setDMPermission(false);

const commands = [vcTools, vclSetting];

// register slash commands
export async function registerSlashCommands() {
    const botToken = process.env.DISCORD_BOT_TOKEN;
    const botId = process.env.DISCORD_BOT_ID;

    assertExistCheck(botToken, 'DISCORD_BOT_TOKEN');
    assertExistCheck(botId, 'DISCORD_BOT_ID');

    const rest = new REST({ version: '10' }).setToken(botToken);

    await rest
        .put(Routes.applicationCommands(botId), {
            body: commands,
        })
        .then(() => logger.info('Slash commands registered.'))
        .catch((error) => {
            logger.error(error);
        });
}

インタラクション処理

コマンドのinteractionからチャンネル情報やメンバー情報などを利用する。

/**
 * Create a channel info object from an interaction.
 * @param {BaseGuildVoiceChannel} voiceChannel VoiceChannel
 * @returns channelState
 */
export async function getVoiceChannelState(voiceChannel: BaseGuildVoiceChannel) {
    const channelState: ChannelLockState = {
        channelId: voiceChannel.id,
        limit: voiceChannel.userLimit,
        isLock: voiceChannel.userLimit == 0 ? false : true,
    };

    return channelState;
}

export async function voiceLockerCommand(interaction: ChatInputCommandInteraction) {
    if (!interaction.inGuild()) return;
    
    // nullチェック
    assertExistCheck(interaction.guild, 'interaction.guild');
    assertExistCheck(interaction.channel, 'interaction.channel');

    const guild = await interaction.guild.fetch();
    const channel = interaction.channel;

    const member = await searchMemberById(guild, interaction.member.user.id);

    assertExistCheck(member, 'member');

    // VCに接続中かチェック
    if (member.voice.channel === null || member.voice.channel.id !== channel.id) {
        await interaction.reply({
            content: '接続中のボイスチャンネルでコマンドを打ってください。',
            ephemeral: true,
        });
        return;
    }

    let channelState;
    const limitNum = interaction.options.getInteger('人数');

制限人数がコマンドで設定されていれば、適応する。

    // check option
    if (limitNum !== null) {
        if (limitNum < 0 || limitNum > 99) {
            await interaction.reply({
                content: '制限人数は0~99の間で指定してください。',
                ephemeral: true,
            });
            return;
        }
	
        channelState = {
            channelId: channel.id,
            limit: limitNum,
            isLock: limitNum == 0 ? false : true,
        };

        await channel.setUserLimit(limitNum);
    } else {
        channelState = await getVoiceChannelState(channel);
    }

Embedでロック状態のダッシュボードを表示する。

/**
 * create voice state embed.
 * @param {ChannelLockState} channelState ChannelLockState Object
 * @returns created embed
 */
export function createEmbed(channelState: ChannelLockState) {
    let limit;
    if (channelState.limit === 0) {
        limit = '∞';
    } else {
        limit = channelState.limit;
    }
    const embed = new EmbedBuilder()
        .setTitle('ボイスチャンネル情報')
        .addFields([{ name: '対象のチャンネル', value: '<#' + channelState.channelId + '>' }]);
    if (channelState.isLock) {
        embed.addFields([
            {
                name: '状態',
                value: '制限中',
            },
        ]),
            embed.setColor('#d83c3e');
    } else {
        embed.addFields([
            {
                name: '状態',
                value: '制限なし',
            },
        ]),
            embed.setColor('#2d7d46');
    }
    embed.addFields([{ name: '人数制限', value: String(limit) }]);
    return embed;
}

    const embed = createEmbed(channelState);
    const button = createButtons(channelState);

    await interaction
        .reply({
            embeds: [embed],
            components: [button],
            fetchReply: true,
        })
        .catch((error) => {
            logger.error(error);
        });

    // 20秒後に削除
    await sleep(20)
    await interaction.deleteReply();
}

ボタンの処理

ダッシュボードの操作ボタンの処理を記述していく。

ボタンの作成

それぞれのボタンにカスタムIDを設定して、ボタンを作成する。
作成するボタンの種類は、channelLockStateに合わせて変える。

/**
 * create buttons
 * @param {ChannelLockState} channelLockState ChannelLockState Object
 * @returns created buttons
 */
export function createButtons(channelLockState: ChannelLockState) {
    const button = new ActionRowBuilder<ButtonBuilder>();
    const limit = channelLockState.limit;
    if (channelLockState.isLock) {
        // if limit number is 1, disable '-' button.
        if (limit == 1) {
            button.addComponents([
                new ButtonBuilder().setCustomId('voiceLock_dec').setLabel('-').setStyle(ButtonStyle.Primary).setDisabled(true),
            ]);
        } else {
            button.addComponents([
                new ButtonBuilder().setCustomId('voiceLock_dec').setLabel('-').setStyle(ButtonStyle.Primary).setDisabled(false),
            ]);
        }

        button.addComponents([
            new ButtonBuilder().setCustomId('voiceLock_change').setLabel('UNLOCK').setStyle(ButtonStyle.Success).setEmoji('🔓'),
        ]);

        // if limit number is 99, disable '+' button.
        if (limit == 99) {
            button.addComponents([
                new ButtonBuilder().setCustomId('voiceLock_inc').setLabel('+').setStyle(ButtonStyle.Primary).setDisabled(true),
            ]);
        } else {
            button.addComponents([
                new ButtonBuilder().setCustomId('voiceLock_inc').setLabel('+').setStyle(ButtonStyle.Primary).setDisabled(false),
            ]);
        }
    } else {
        button.addComponents([
            new ButtonBuilder().setCustomId('voiceLock_dec').setLabel('-').setStyle(ButtonStyle.Primary).setDisabled(true),
            new ButtonBuilder().setCustomId('voiceLock_change').setLabel('LOCK').setStyle(ButtonStyle.Danger).setEmoji('🔒'),
            new ButtonBuilder().setCustomId('voiceLock_inc').setLabel('+').setStyle(ButtonStyle.Primary).setDisabled(true),
        ]);
    }
    return button;
}

インタラクション処理

ボタンのinteractionからチャンネル情報やメンバー情報などを利用する。

export async function voiceLockerUpdate(interaction: ButtonInteraction) {
    if (!interaction.inGuild()) return;

    // nullチェック
    assertExistCheck(interaction.guild, 'interaction.guild');
    assertExistCheck(interaction.channel, 'interaction.channel');

    const guild = await interaction.guild.fetch();

    const member = await searchMemberById(guild, interaction.member.user.id);
    const channel = interaction.channel;

    assertExistCheck(member, 'member');

    // VC以外の場合
    if (!channel.isVoiceBased()) {
        await interaction.reply({
            content: 'このチャンネルでは利用できません。',
            ephemeral: true,
        });
        return;
    }

    // ボイスチャンネル内のメンバー数を取得
    const voiceMemberNum = channel.members.size;

    // ボタンを押した人が該当VCにいない場合
    if (!channel.isVoiceBased() || member.voice.channel == null || member.voice.channel.id != channel.id) {
        await interaction.reply({
            content: '対象のボイスチャンネルに接続する必要があります。\n接続してから再度お試しください。',
            ephemeral: true,
        });
        return;
    }

channelLockStateとボタンのカスタムIDに応じて人数制限の処理を行う。

    const channelLockState = await getVoiceChannelState(channel);

    let limit = Number(channelLockState.limit);

    // When you press the 'LOCK' button or 'UNLOCK' button
    if (interaction.customId == 'voiceLock_change') {
        const label = interaction.component.label; // Get the state to set from the button label
        if (label === 'LOCK') {
            await channel.setUserLimit(voiceMemberNum);
            channelLockState.isLock = true;
            channelLockState.limit = voiceMemberNum;
        } else if (label === 'UNLOCK') {
            await channel.setUserLimit(0);
            channelLockState.isLock = false;
            channelLockState.limit = 0;
        }
    }

    // Check for Embed operation
    if (channelLockState.isLock) {
        if (interaction.customId === 'voiceLock_inc') {
            // do nothing when pressed with '99'
            if (limit != 99) {
                limit += 1;
                channelLockState.limit = limit;
                await channel.setUserLimit(limit);
            }
        } else if (interaction.customId === 'voiceLock_dec') {
            // do nothing when pressed with '1'
            if (limit != 1) {
                limit -= 1;
                channelLockState.limit = limit;
                await channel.setUserLimit(limit);
            }
        }
    } else {
        // Behavior when '+'or'-' is pressed even though it is not locked
        if (interaction.customId === 'voiceLock_inc' || interaction.customId === 'voiceLock_dec') {
            await interaction
                .reply({
                    content: '現在ボイスチャンネルはロックされていません。',
                    ephemeral: true,
                    fetchReply: true,
                })
                .catch((error) => {
                    logger.error(error);
                });
            return;
        }
    }

Embedを更新する。

    await interaction
        .update({
            embeds: [createEmbed(channelLockState)],
            components: [createButtons(channelLockState)],
            fetchReply: true,
        })
        .catch((error) => {
            logger.error(error);
        });

GitHub Repository

https://github.com/root-lump/Discord-VC-Tools

Discussion