diff --git a/src/deploy-commands.ts b/src/deploy-commands.ts index 317d990..ba4e119 100644 --- a/src/deploy-commands.ts +++ b/src/deploy-commands.ts @@ -66,6 +66,11 @@ export const listenChannelCommand = new SlashCommandBuilder() sub .setName('list') .setDescription(`List the channels the bot is currently actively listening to.`) + ) + .addSubcommand((sub) => + sub + .setName('modify') + .setDescription(`Add or remove channels via select menu UI (first 25 text channels only)`) ); export const trainCommand = new SlashCommandBuilder() diff --git a/src/index.ts b/src/index.ts index 95eaf0b..d50ae6c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -11,8 +11,11 @@ import Markov, { import { createConnection } from 'typeorm'; import { MarkovInputData } from 'markov-strings-db/dist/src/entity/MarkovInputData'; -import { APIInteractionGuildMember } from 'discord-api-types'; import type { PackageJsonPerson } from 'types-package-json'; +import { + APISelectMenuComponent, + APIInteractionGuildMember, +} from 'discord.js/node_modules/discord-api-types'; import L from './logger'; import { Channel } from './entity/Channel'; import { Guild } from './entity/Guild'; @@ -32,6 +35,12 @@ interface MarkovDataCustom { attachments: string[]; } +interface SelectMenuChannel { + id: string; + listen?: boolean; + name?: string; +} + const INVALID_PERMISSIONS_MESSAGE = 'You do not have the permissions for this action.'; const INVALID_GUILD_MESSAGE = 'This action must be performed within a server.'; @@ -77,6 +86,25 @@ async function getValidChannels(guild: Discord.Guild): Promise { + const MAX_SELECT_OPTIONS = 25; + const textChannels = guild.channels.cache.filter( + (c): c is Discord.TextChannel => c !== null && c instanceof Discord.TextChannel + ); + const foundDbChannels = await Channel.findByIds(Array.from(textChannels.keys())); + const foundDbChannelsWithName: SelectMenuChannel[] = foundDbChannels.map((c) => ({ + ...c, + name: textChannels.find((t) => t.id === c.id)?.name, + })); + const notFoundDbChannels: SelectMenuChannel[] = textChannels + .filter((c) => !foundDbChannels.find((d) => d.id === c.id)) + .map((c) => ({ id: c.id, listen: false, name: textChannels.find((t) => t.id === c.id)?.name })); + const limitedDbChannels = foundDbChannelsWithName + .concat(notFoundDbChannels) + .slice(0, MAX_SELECT_OPTIONS); + return limitedDbChannels; +} + async function addValidChannels(channels: Discord.TextChannel[], guildId: string): Promise { const dbChannels = channels.map((c) => { return Channel.create({ id: c.id, guild: Guild.create({ id: guildId }), listen: true }); @@ -165,7 +193,7 @@ function messageToData(message: Discord.Message): AddDataProps { async function saveGuildMessageHistory( interaction: Discord.Message | Discord.CommandInteraction ): Promise { - if (!isModerator(interaction.member as any)) return INVALID_PERMISSIONS_MESSAGE; + if (!isModerator(interaction.member)) return INVALID_PERMISSIONS_MESSAGE; if (!interaction.guildId || !interaction.guild) return INVALID_GUILD_MESSAGE; const markov = await getMarkovByGuildId(interaction.guildId); const channels = await getValidChannels(interaction.guild); @@ -456,58 +484,122 @@ client.on('messageUpdate', async (oldMessage, newMessage) => { }); client.on('interactionCreate', async (interaction) => { - if (!interaction.isCommand()) return; + if (interaction.isCommand()) { + L.info({ command: interaction.commandName }, 'Recieved slash command'); - L.info({ command: interaction.commandName }, 'Recieved slash command'); + if (interaction.commandName === helpCommand.name) { + await interaction.reply(helpMessage()); + } else if (interaction.commandName === inviteCommand.name) { + await interaction.reply(inviteMessage()); + } else if (interaction.commandName === messageCommand.name) { + await interaction.deferReply(); + const tts = interaction.options.getBoolean('tts') || false; + const debug = interaction.options.getBoolean('debug') || false; + const generatedResponse = await generateResponse(interaction, debug, tts); + if (generatedResponse.message) await interaction.editReply(generatedResponse.message); + else await interaction.deleteReply(); + if (generatedResponse.debug) await interaction.followUp(generatedResponse.debug); + if (generatedResponse.error) { + await interaction.followUp({ ...generatedResponse.error, ephemeral: true }); + } + } else if (interaction.commandName === listenChannelCommand.name) { + await interaction.deferReply(); + const subCommand = interaction.options.getSubcommand(true) as 'add' | 'remove' | 'list'; + if (subCommand === 'list') { + const reply = await listValidChannels(interaction); + await interaction.editReply(reply); + } else if (subCommand === 'add') { + if (!isModerator(interaction.member)) { + await interaction.deleteReply(); + await interaction.followUp({ content: INVALID_PERMISSIONS_MESSAGE, ephemeral: true }); + return; + } + const channels = getChannelsFromInteraction(interaction); + await addValidChannels(channels, interaction.guildId); + await interaction.editReply( + `Added ${channels.length} text channels to the list. Use \`/train\` to update the past known messages.` + ); + } else if (subCommand === 'remove') { + if (!isModerator(interaction.member)) { + await interaction.deleteReply(); + await interaction.followUp({ content: INVALID_PERMISSIONS_MESSAGE, ephemeral: true }); + return; + } + const channels = getChannelsFromInteraction(interaction); + await removeValidChannels(channels, interaction.guildId); + await interaction.editReply( + `Removed ${channels.length} text channels from the list. Use \`/train\` to remove these channels from the past known messages.` + ); + } else if (subCommand === 'modify') { + await interaction.deleteReply(); + if (!interaction.guild) { + await interaction.followUp({ content: INVALID_GUILD_MESSAGE, ephemeral: true }); + return; + } + if (!isModerator(interaction.member)) { + await interaction.followUp({ content: INVALID_PERMISSIONS_MESSAGE, ephemeral: true }); + return; + } + const dbTextChannels = await getTextChannels(interaction.guild); + const row = new Discord.MessageActionRow().addComponents( + new Discord.MessageSelectMenu() + .setCustomId('listen-modify-select') + .setPlaceholder('Nothing selected') + .setMinValues(0) + .setMaxValues(dbTextChannels.length) + .addOptions( + dbTextChannels.map((c) => ({ + label: `#${c.name}` || c.id, + value: c.id, + default: c.listen || false, + })) + ) + ); - if (interaction.commandName === helpCommand.name) { - await interaction.reply(helpMessage()); - } else if (interaction.commandName === inviteCommand.name) { - await interaction.reply(inviteMessage()); - } else if (interaction.commandName === messageCommand.name) { - await interaction.deferReply(); - const tts = interaction.options.getBoolean('tts') || false; - const debug = interaction.options.getBoolean('debug') || false; - const generatedResponse = await generateResponse(interaction, debug, tts); - if (generatedResponse.message) await interaction.editReply(generatedResponse.message); - else await interaction.deleteReply(); - if (generatedResponse.debug) await interaction.followUp(generatedResponse.debug); - if (generatedResponse.error) { - await interaction.followUp({ ...generatedResponse.error, ephemeral: true }); - } - } else if (interaction.commandName === listenChannelCommand.name) { - await interaction.deferReply(); - const subCommand = interaction.options.getSubcommand(true) as 'add' | 'remove' | 'list'; - if (subCommand === 'list') { - const reply = await listValidChannels(interaction); - await interaction.editReply(reply); - } else if (subCommand === 'add') { - if (!isModerator(interaction.member as any)) { - await interaction.deleteReply(); - await interaction.followUp({ content: INVALID_PERMISSIONS_MESSAGE, ephemeral: true }); - return; + await interaction.followUp({ + content: 'Select which channels you would like to the bot to actively listen to', + components: [row], + ephemeral: true, + }); } - const channels = getChannelsFromInteraction(interaction); - await addValidChannels(channels, interaction.guildId); - await interaction.editReply( - `Added ${channels.length} text channels to the list. Use \`/train\` to update the past known messages.` - ); - } else if (subCommand === 'remove') { - if (!isModerator(interaction.member as any)) { - await interaction.deleteReply(); - await interaction.followUp({ content: INVALID_PERMISSIONS_MESSAGE, ephemeral: true }); - return; - } - const channels = getChannelsFromInteraction(interaction); - await removeValidChannels(channels, interaction.guildId); - await interaction.editReply( - `Removed ${channels.length} text channels from the list. Use \`/train\` to remove these channels from the past known messages.` - ); + } else if (interaction.commandName === trainCommand.name) { + await interaction.deferReply(); + const responseMessage = await saveGuildMessageHistory(interaction); + await interaction.editReply(responseMessage); } - } else if (interaction.commandName === trainCommand.name) { - await interaction.deferReply(); - const responseMessage = await saveGuildMessageHistory(interaction); - await interaction.editReply(responseMessage); + } else if (interaction.isSelectMenu()) { + await interaction.deferUpdate(); + const { guild } = interaction; + if (!isModerator(interaction.member)) { + await interaction.followUp({ content: INVALID_PERMISSIONS_MESSAGE, ephemeral: true }); + return; + } + if (!guild) { + await interaction.deleteReply(); + await interaction.followUp({ content: INVALID_GUILD_MESSAGE, ephemeral: true }); + return; + } + + const allChannels = + (interaction.component as APISelectMenuComponent).options?.map((o) => o.value) || []; + const selectedChannelIds = interaction.values; + + const textChannels = ( + await Promise.all( + allChannels.map(async (c) => { + return guild.channels.fetch(c); + }) + ) + ).filter((c): c is Discord.TextChannel => c !== null && c instanceof Discord.TextChannel); + const unselectedChannels = textChannels.filter((t) => !selectedChannelIds.includes(t.id)); + const selectedChannels = textChannels.filter((t) => selectedChannelIds.includes(t.id)); + await addValidChannels(selectedChannels, guild.id); + await removeValidChannels(unselectedChannels, guild.id); + + await interaction.followUp({ + content: 'Updated actively listened to channels list.', + ephemeral: true, + }); } });