From d712b116b65ef3ed552dbd3dd08faae009fd0bdf Mon Sep 17 00:00:00 2001 From: "Ean Milligan (Bastion)" Date: Tue, 31 Jan 2023 03:32:53 -0500 Subject: [PATCH] Add button systems, add game selection, add initial details modal --- deps.ts | 2 + src/buttons/event-creation/activities.ts | 1 + .../event-creation/step1-gameSelection.ts | 158 ++++++++++++++++++ src/buttons/event-creation/step1.ts | 25 --- src/buttons/event-creation/utils.ts | 32 ++++ src/commands/_index.ts | 3 +- src/commands/setup.ts | 3 +- src/events/interactionCreate.ts | 24 ++- src/types/commandTypes.ts | 5 + 9 files changed, 221 insertions(+), 32 deletions(-) create mode 100644 src/buttons/event-creation/step1-gameSelection.ts delete mode 100644 src/buttons/event-creation/step1.ts create mode 100644 src/buttons/event-creation/utils.ts diff --git a/deps.ts b/deps.ts index d254246..3e4d9d4 100644 --- a/deps.ts +++ b/deps.ts @@ -26,6 +26,7 @@ export { sendInteractionResponse, sendMessage, startBot, + TextStyles, } from 'https://deno.land/x/discordeno@17.0.1/mod.ts'; export type { ActionRow, @@ -43,6 +44,7 @@ export type { MakeRequired, Message, PermissionStrings, + SelectOption, } from 'https://deno.land/x/discordeno@17.0.1/mod.ts'; export { Client } from 'https://deno.land/x/mysql@v2.11.0/mod.ts'; diff --git a/src/buttons/event-creation/activities.ts b/src/buttons/event-creation/activities.ts index 1581a6b..6050a9d 100644 --- a/src/buttons/event-creation/activities.ts +++ b/src/buttons/event-creation/activities.ts @@ -5,6 +5,7 @@ export type Activity = { options?: Array; }; +// Max depth is limited to 4, 5th component row must be reserved for the custom button export const Activities: Array = [ { name: 'Destiny 2', diff --git a/src/buttons/event-creation/step1-gameSelection.ts b/src/buttons/event-creation/step1-gameSelection.ts new file mode 100644 index 0000000..474a10b --- /dev/null +++ b/src/buttons/event-creation/step1-gameSelection.ts @@ -0,0 +1,158 @@ +import { ApplicationCommandFlags, ApplicationCommandTypes, Bot, Interaction, InteractionResponseTypes, MessageComponentTypes, ActionRow, ButtonStyles, TextStyles } from '../../../deps.ts'; +import { infoColor1, somethingWentWrong } from '../../commandUtils.ts'; +import { CommandDetails } from '../../types/commandTypes.ts'; +import { Activities } from './activities.ts'; +import { generateActionRow, getNestedActivity, generateMapId, pathIdxSeparator, pathIdxEnder } from './utils.ts'; +import utils from '../../utils.ts'; + +export const customId = 'gameSel'; +const slashCommandName = 'create-event'; +// Discord Interaction Tokens last 15 minutes, we will self kill after 14.5 minutes +const tokenTimeoutS = (15 * 60) - 30; +const tokenTimeoutMS = tokenTimeoutS * 1000; +const details: CommandDetails = { + name: slashCommandName, + description: 'Creates a new event in this channel.', + type: ApplicationCommandTypes.ChatInput, +}; + +const tokenMap: Map = new Map(); + +const customEventRow: ActionRow = { + type: MessageComponentTypes.ActionRow, + components: [{ + type: MessageComponentTypes.Button, + label: 'Create Custom Event', + customId, + style: ButtonStyles.Primary, + }], +} + +const execute = async (bot: Bot, interaction: Interaction) => { + if (interaction.data && (interaction.data.name === slashCommandName || interaction.data.customId) && interaction.member && interaction.guildId && interaction.channelId) { + // Parse indexPath from the select value + const rawIdxPath: Array = interaction.data.values ? interaction.data.values[0].split(pathIdxSeparator) : ['']; + const idxPath: Array = rawIdxPath.map(rawIdx => rawIdx ? parseInt(rawIdx) : -1); + + if (interaction.data.values && interaction.data.values[0] && interaction.data.values[0].endsWith(pathIdxEnder)) { + // User selected activity, give them the details modal and delete the selectMenus + bot.helpers.deleteOriginalInteractionResponse(tokenMap.get(generateMapId(interaction.guildId, interaction.channelId, interaction.member.id))?.token || '').catch((e: Error) => utils.commonLoggers.interactionSendError('step1-gameSelection.ts:cleanup', interaction, e)); + tokenMap.delete(generateMapId(interaction.guildId, interaction.channelId, interaction.member.id)); + bot.helpers.sendInteractionResponse(interaction.id, interaction.token, { + type: InteractionResponseTypes.Modal, + data: { + title: 'Enter Event Details', + customId: 'temp', //TODO: fix + components: [{ + type: MessageComponentTypes.ActionRow, + components: [{ + type: MessageComponentTypes.InputText, + customId: 'eventTime', + label: 'Start Time:', + style: TextStyles.Short, + }] + },{ + type: MessageComponentTypes.ActionRow, + components: [{ + type: MessageComponentTypes.InputText, + customId: 'eventTimeZone', + label: 'Time Zone:', + style: TextStyles.Short, + }] + },{ + type: MessageComponentTypes.ActionRow, + components: [{ + type: MessageComponentTypes.InputText, + customId: 'eventDate', + label: 'Start Date:', + style: TextStyles.Short, + }] + },{ + type: MessageComponentTypes.ActionRow, + components: [{ + type: MessageComponentTypes.InputText, + customId: 'eventDescription', + label: 'Description:', + style: TextStyles.Paragraph, + }] + }] + }, + }) + return; + } + + const selectMenus: Array = []; + let selectMenuCustomId = `${customId}$`; + let currentBaseValue = ''; + + for (let i = 0; i < idxPath.length; i++) { + const idx = idxPath[i]; + const idxPathCopy = [...idxPath].slice(0, i); + selectMenus.push(generateActionRow(currentBaseValue, getNestedActivity(idxPathCopy, Activities), selectMenuCustomId, idx)); + + selectMenuCustomId = `${selectMenuCustomId}$`; + currentBaseValue = `${currentBaseValue}${idx}${pathIdxSeparator}`; + } + + selectMenus.push(customEventRow); + + if (interaction.data.customId && interaction.data.customId.includes('$')) { + // Let discord know we didn't ignore the user + await bot.helpers.sendInteractionResponse(interaction.id, interaction.token, { + type: InteractionResponseTypes.DeferredUpdateMessage, + }).catch((e: Error) => utils.commonLoggers.interactionSendError('step1-gameSelection.ts:ping', interaction, e)); + + // Update the original game selector + await bot.helpers.editOriginalInteractionResponse(tokenMap.get(generateMapId(interaction.guildId, interaction.channelId, interaction.member.id))?.token || '', { + components: selectMenus, + }).catch((e: Error) => utils.commonLoggers.interactionSendError('step1-gameSelection.ts:edit', interaction, e)); + } else { + // Delete old token entry if it exists + if (tokenMap.has(generateMapId(interaction.guildId, interaction.channelId, interaction.member.id))) { + bot.helpers.deleteOriginalInteractionResponse(tokenMap.get(generateMapId(interaction.guildId, interaction.channelId, interaction.member.id))?.token || '').catch((e: Error) => utils.commonLoggers.interactionSendError('step1-gameSelection.ts:cleanup', interaction, e)); + tokenMap.delete(generateMapId(interaction.guildId, interaction.channelId, interaction.member.id)); + } + + // Store token for later use + tokenMap.set(generateMapId(interaction.guildId, interaction.channelId, interaction.member.id), { + token: interaction.token, + timeoutId: setTimeout((guildId, channelId, memberId) => { + bot.helpers.deleteOriginalInteractionResponse(tokenMap.get(generateMapId(guildId, channelId, memberId))?.token || '').catch((e: Error) => utils.commonLoggers.interactionSendError('step1-gameSelection.ts:delete', interaction, e)); + tokenMap.delete(generateMapId(guildId, channelId, memberId)); + }, tokenTimeoutMS, interaction.guildId, interaction.channelId, interaction.member.id), + }); + + // Calculate destruction time + const destructTime = Math.floor((new Date().getTime() + tokenTimeoutMS) / 1000); + + // Send initial interaction + bot.helpers.sendInteractionResponse(interaction.id, interaction.token, { + type: InteractionResponseTypes.ChannelMessageWithSource, + data: { + embeds: [{ + title: 'Please select a Game and Activity, or create a Custom Event.', + description: `Please note: This message will self destruct due to limits imposed by the Discord API.`, + color: infoColor1, + }], + flags: ApplicationCommandFlags.Ephemeral, + components: selectMenus, + }, + }).catch((e: Error) => utils.commonLoggers.interactionSendError('step1-gameSelection.ts:init', interaction, e)); + } + } else { + somethingWentWrong; + } +}; + +export const createEventCommand = { + details, + execute, +}; + +export const createEventButton = { + customId, + execute, +}; diff --git a/src/buttons/event-creation/step1.ts b/src/buttons/event-creation/step1.ts deleted file mode 100644 index f6f901d..0000000 --- a/src/buttons/event-creation/step1.ts +++ /dev/null @@ -1,25 +0,0 @@ -import config from '../../../config.ts'; -import { ApplicationCommandFlags, ApplicationCommandTypes, Bot, Interaction, InteractionResponseTypes } from '../../../deps.ts'; -import { CommandDetails } from "../../types/commandTypes.ts"; - - -export const customId = 'gameSel'; -const details: CommandDetails = { - name: 'create-event', - description: 'Creates a new event in this channel.', - type: ApplicationCommandTypes.ChatInput, -}; - -const execute = (bot: Bot, interaction: Interaction) => { - -}; - -export const createEventCommand = { - details, - execute, -}; - -export const createEventButton = { - customId, - execute, -}; \ No newline at end of file diff --git a/src/buttons/event-creation/utils.ts b/src/buttons/event-creation/utils.ts new file mode 100644 index 0000000..c826c00 --- /dev/null +++ b/src/buttons/event-creation/utils.ts @@ -0,0 +1,32 @@ +import { Activity } from './activities.ts'; +import { SelectOption, ActionRow, MessageComponentTypes } from '../../../deps.ts'; + +export const pathIdxSeparator = '|'; +export const pathIdxEnder = '&'; + +export const getNestedActivity = (idxPath: Array, activities: Array): Array => { + const nextIdx = idxPath[0]; + if (idxPath.length && activities[nextIdx] && activities[nextIdx].options) { + idxPath.shift(); + return getNestedActivity(idxPath, activities[nextIdx].options || []); + } else { + return activities; + } +}; + +const getSelectOptions = (baseValue: string, activities: Array, defaultIdx?: number): Array => activities.map((act, idx) => ({ + label: act.name, + value: `${baseValue}${idx}${act.maxMembers ? pathIdxEnder : pathIdxSeparator}`, + default: idx === defaultIdx, +})); + +export const generateActionRow = (baseValue: string, activities: Array, customId: string, defaultIdx?: number): ActionRow => ({ + type: MessageComponentTypes.ActionRow, + components: [{ + type: MessageComponentTypes.SelectMenu, + customId, + options: getSelectOptions(baseValue, activities, defaultIdx), + }], +}); + +export const generateMapId = (guildId: bigint, channelId: bigint, userId: bigint) => `${guildId}-${channelId}-${userId}`; diff --git a/src/commands/_index.ts b/src/commands/_index.ts index d090a80..de5f2a8 100644 --- a/src/commands/_index.ts +++ b/src/commands/_index.ts @@ -6,8 +6,9 @@ import info from './info.ts'; import report from './report.ts'; import setup from './setup.ts'; import deleteCmd from './delete.ts'; +import { createEventCommand } from '../buttons/event-creation/step1-gameSelection.ts'; -export const commands: Array = [deleteCmd, info, report, setup]; +export const commands: Array = [deleteCmd, info, report, setup, createEventCommand]; export const createSlashCommands = async (bot: Bot) => { const globalCommands: MakeRequired[] = []; diff --git a/src/commands/setup.ts b/src/commands/setup.ts index 86d1f0e..c768be7 100644 --- a/src/commands/setup.ts +++ b/src/commands/setup.ts @@ -18,6 +18,7 @@ import { failColor, infoColor2, somethingWentWrong, successColor } from '../comm import { dbClient, lfgChannelSettings, queries } from '../db.ts'; import { CommandDetails } from '../types/commandTypes.ts'; import utils from '../utils.ts'; +import { customId as gameSelId } from '../buttons/event-creation/step1-gameSelection.ts'; const withoutMgrRole = 'without-manager-role'; const withMgrRole = 'with-manager-role'; @@ -266,7 +267,7 @@ The Discord Slash Command system will ensure you provide all the required detail components: [{ type: MessageComponentTypes.Button, label: createNewEventBtn, - customId: 'temp', // TODO: set this + customId: gameSelId, style: ButtonStyles.Success, }], }], diff --git a/src/events/interactionCreate.ts b/src/events/interactionCreate.ts index 8c802ce..1f3fde2 100644 --- a/src/events/interactionCreate.ts +++ b/src/events/interactionCreate.ts @@ -1,16 +1,30 @@ import { Bot, BotWithCache, Interaction } from '../../deps.ts'; import { commands } from '../commands/_index.ts'; +import { Button } from '../types/commandTypes.ts'; +import { createEventButton } from '../buttons/event-creation/step1-gameSelection.ts'; + +const buttons: Array