diff --git a/config.example.ts b/config.example.ts index 4972098..bd5086c 100644 --- a/config.example.ts +++ b/config.example.ts @@ -15,6 +15,7 @@ export const config = { 'link': { // Links to various sites 'sourceCode': 'https://github.com/Burn-E99/GroupUp', // Link to the repository 'supportServer': '', // Invite link to the Discord support server + 'addToCalendar': '', // Link to where the icsGenerator is hosted }, 'logChannel': 'the_log_channel', // Discord channel ID where the bot should put startup messages and other error messages needed 'reportChannel': 'the_report_channel', // Discord channel ID where reports will be sent when using the built-in report command diff --git a/deps.ts b/deps.ts index 3e4d9d4..067c1da 100644 --- a/deps.ts +++ b/deps.ts @@ -41,6 +41,7 @@ export type { EventHandlers, Guild, Interaction, + InteractionResponse, MakeRequired, Message, PermissionStrings, diff --git a/src/buttons/event-creation/dateTimeUtils.ts b/src/buttons/event-creation/dateTimeUtils.ts index 9578395..7a7eba9 100644 --- a/src/buttons/event-creation/dateTimeUtils.ts +++ b/src/buttons/event-creation/dateTimeUtils.ts @@ -1,5 +1,5 @@ const monthsLong: Array = ['JANUARY', 'FEBRUARY', 'MARCH', 'APRIL', 'MAY', 'JUNE', 'JULY', 'AUGUST', 'SEPTEMBER', 'OCTOBER', 'NOVEMBER', 'DECEMBER']; -const monthsShort: Array = monthsLong.map((month) => month.slice(0, 3)); +export const monthsShort: Array = monthsLong.map((month) => month.slice(0, 3)); const tzMap: Map = new Map([ ['CDT', '-05:00'], ['CST', '-06:00'], @@ -77,11 +77,15 @@ const parseEventTime = (preParsedEventTime: string): [string, string, string] => parsedEventTimePeriod = ''; } + if (!parsedEventTimePeriod && parsedEventTimeHours.length < 2) { + parsedEventTimeHours = `0${parsedEventTimeHours}`; + } + return [parsedEventTimeHours, parsedEventTimeMinutes, parsedEventTimePeriod]; }; // Takes user input Time Zone and makes it actually usable -const parseEventTimeZone = (preParsedEventTimeZone: string): string => { +const parseEventTimeZone = (preParsedEventTimeZone: string): [string, string] => { if (shorthandUSTZ.includes(preParsedEventTimeZone)) { // Handle shorthand US timezones, adding S for standard time and D for Daylight Savings const today = new Date(); @@ -95,20 +99,20 @@ const parseEventTimeZone = (preParsedEventTimeZone: string): string => { } if (tzMap.has(preParsedEventTimeZone)) { // TZ is proper abbreviation, use our map to convert - return tzMap.get(preParsedEventTimeZone) || 'how did we get here?'; + return [`UTC${tzMap.get(preParsedEventTimeZone)}`, preParsedEventTimeZone]; } else { // Determine if user put in UTC4, which needs to be UTC+4 let addPlusSign = false; - if (!preParsedEventTimeZone.includes('+') || !preParsedEventTimeZone.includes('-')) { + if (!preParsedEventTimeZone.includes('+') && !preParsedEventTimeZone.includes('-')) { addPlusSign = true; } // Determine if we need to prepend UTC/GMT, handle adding the + into the string - if (!preParsedEventTimeZone.startsWith('UTC') || preParsedEventTimeZone.startsWith('GMT')) { + if (!preParsedEventTimeZone.startsWith('UTC') && preParsedEventTimeZone.startsWith('GMT')) { preParsedEventTimeZone = `UTC${addPlusSign && '+'}${preParsedEventTimeZone}`; } else if (addPlusSign) { preParsedEventTimeZone = `${preParsedEventTimeZone.slice(0, 3)}+${preParsedEventTimeZone.slice(3)}`; } - return preParsedEventTimeZone; + return [preParsedEventTimeZone, preParsedEventTimeZone]; } }; @@ -135,8 +139,8 @@ const parseEventDate = (preParsedEventDate: string): [string, string, string] => // Month and Day exist, so determine year and parse month/day parsedEventYear = (isNaN(parseInt(parsedEventYear)) ? today.getFullYear() : parseInt(parsedEventYear)).toString(); parsedEventDay = parseInt(parsedEventDay).toString(); - if (!monthsLong.includes(parsedEventMonth) || !monthsShort.includes(parsedEventMonth)) { - parsedEventMonth = parseInt(parsedEventMonth).toString(); + if (!monthsLong.includes(parsedEventMonth) && !monthsShort.includes(parsedEventMonth)) { + parsedEventMonth = monthsShort[parseInt(parsedEventMonth) - 1]; } } @@ -144,15 +148,20 @@ const parseEventDate = (preParsedEventDate: string): [string, string, string] => }; // Take full raw Date/Time input and convert it to a proper Date -export const getDateFromRawInput = (rawEventTime: string, rawEventTimeZone: string, rawEventDate: string): Date => { +export const getDateFromRawInput = (rawEventTime: string, rawEventTimeZone: string, rawEventDate: string): [Date, string] => { // Verify/Set Time const [parsedEventTimeHours, parsedEventTimeMinutes, parsedEventTimePeriod] = parseEventTime(rawEventTime.replaceAll(':', '').toUpperCase()); // Verify/Set Time Zone - const parsedEventTimeZone = parseEventTimeZone(rawEventTimeZone.replaceAll(' ', '').trim().toUpperCase()); + const [parsedEventTimeZone, userInputTimeZone] = parseEventTimeZone(rawEventTimeZone.replaceAll(' ', '').trim().toUpperCase()); // Verify/Set Date const [parsedEventYear, parsedEventMonth, parsedEventDay] = parseEventDate(rawEventDate.trim().toUpperCase()); - return new Date(`${parsedEventMonth} ${parsedEventDay}, ${parsedEventYear} ${parsedEventTimeHours}:${parsedEventTimeMinutes} ${parsedEventTimePeriod} ${parsedEventTimeZone}`); + return [ + new Date(`${parsedEventMonth} ${parsedEventDay}, ${parsedEventYear} ${parsedEventTimeHours}:${parsedEventTimeMinutes} ${parsedEventTimePeriod} ${parsedEventTimeZone}`), + `${parsedEventTimeHours}${parsedEventTimePeriod ? ':' : ''}${parsedEventTimeMinutes} ${parsedEventTimePeriod} ${userInputTimeZone} ${parsedEventMonth.slice(0, 1)}${ + parsedEventMonth.slice(1, 3).toLowerCase() + } ${parsedEventDay}, ${parsedEventYear}`, + ]; }; diff --git a/src/buttons/event-creation/step1-gameSelection.ts b/src/buttons/event-creation/step1-gameSelection.ts index 32bb1d1..279d426 100644 --- a/src/buttons/event-creation/step1-gameSelection.ts +++ b/src/buttons/event-creation/step1-gameSelection.ts @@ -2,10 +2,23 @@ import { ActionRow, ApplicationCommandFlags, ApplicationCommandTypes, Bot, Butto import { infoColor1, somethingWentWrong } from '../../commandUtils.ts'; import { CommandDetails } from '../../types/commandTypes.ts'; import { Activities } from './activities.ts'; -import { addTokenToMap, deleteTokenEarly, generateActionRow, generateMapId, getNestedActivity, idSeparator, pathIdxEnder, pathIdxSeparator, selfDestructMessage, tokenMap } from './utils.ts'; +import { + addTokenToMap, + deleteTokenEarly, + generateActionRow, + generateMapId, + getNestedActivity, + idSeparator, + LfgEmbedIndexes, + pathIdxEnder, + pathIdxSeparator, + selfDestructMessage, + tokenMap, +} from './utils.ts'; import utils from '../../utils.ts'; import { customId as createCustomActivityBtnId } from './step1a-openCustomModal.ts'; import { customId as finalizeEventBtnId } from './step2-finalize.ts'; +import { monthsShort } from './dateTimeUtils.ts'; export const customId = 'gameSel'; export const eventTimeId = 'eventTime'; @@ -39,6 +52,20 @@ const execute = async (bot: Bot, interaction: Interaction) => { if ((interaction.data.customId?.includes(idSeparator) && interaction.data.customId.endsWith(pathIdxEnder)) || interaction.data?.values?.[0].endsWith(pathIdxEnder)) { // User selected activity, give them the details modal and delete the selectMenus await deleteTokenEarly(bot, interaction, interaction.guildId, interaction.channelId, interaction.member.id); + + let prefillTime = ''; + let prefillTimeZone = ''; + let prefillDate = ''; + let prefillDescription = ''; + if (interaction.message && interaction.message.embeds[0].fields) { + let rawEventDateTime = interaction.message.embeds[0].fields[LfgEmbedIndexes.StartTime].value.split('\n')[0].split(' '); + const monthIdx = rawEventDateTime.findIndex((item) => monthsShort.includes(item.toUpperCase())); + prefillTime = rawEventDateTime.slice(0, monthIdx - 2).join(' ').trim(); + prefillTimeZone = rawEventDateTime[monthIdx - 1].trim(); + prefillDate = rawEventDateTime.slice(monthIdx).join(' ').trim(); + prefillDescription = interaction.message.embeds[0].fields[LfgEmbedIndexes.Description].value.trim(); + } + bot.helpers.sendInteractionResponse(interaction.id, interaction.token, { type: InteractionResponseTypes.Modal, data: { @@ -54,6 +81,7 @@ const execute = async (bot: Bot, interaction: Interaction) => { style: TextStyles.Short, minLength: 1, maxLength: 8, + value: prefillTime || undefined, }], }, { type: MessageComponentTypes.ActionRow, @@ -65,6 +93,7 @@ const execute = async (bot: Bot, interaction: Interaction) => { style: TextStyles.Short, minLength: 2, maxLength: 8, + value: prefillTimeZone || undefined, }], }, { type: MessageComponentTypes.ActionRow, @@ -76,6 +105,7 @@ const execute = async (bot: Bot, interaction: Interaction) => { style: TextStyles.Short, minLength: 1, maxLength: 20, + value: prefillDate || undefined, }], }, { type: MessageComponentTypes.ActionRow, @@ -88,6 +118,7 @@ const execute = async (bot: Bot, interaction: Interaction) => { required: false, minLength: 0, maxLength: 1000, + value: prefillDescription || undefined, }], }], }, diff --git a/src/buttons/event-creation/step2-finalize.ts b/src/buttons/event-creation/step2-finalize.ts index 8c3a2f0..15eea90 100644 --- a/src/buttons/event-creation/step2-finalize.ts +++ b/src/buttons/event-creation/step2-finalize.ts @@ -1,7 +1,7 @@ import { Bot, Interaction, InteractionResponseTypes, MessageComponentTypes, TextStyles } from '../../../deps.ts'; import { somethingWentWrong } from '../../commandUtils.ts'; import { eventDateId, eventDescriptionId, eventTimeId, eventTimeZoneId } from './step1-gameSelection.ts'; -import { getFinalActivity, idSeparator, pathIdxSeparator } from './utils.ts'; +import { createLFGPost, getFinalActivity, idSeparator, pathIdxSeparator } from './utils.ts'; import { Activities, Activity } from './activities.ts'; import { getDateFromRawInput } from './dateTimeUtils.ts'; @@ -51,9 +51,15 @@ const execute = async (bot: Bot, interaction: Interaction) => { } // Get Date Object from user input - const eventDateTime = getDateFromRawInput(rawEventTime, rawEventTimeZone, rawEventDate); + const [eventDateTime, eventDateTimeStr] = getDateFromRawInput(rawEventTime, rawEventTimeZone, rawEventDate); - somethingWentWrong(bot, interaction, `TESTING@${rawEventTime}_${rawEventTimeZone}_${rawEventDate}`); + bot.helpers.sendInteractionResponse( + interaction.id, + interaction.token, + createLFGPost(category, activity, eventDateTime, eventDateTimeStr, eventDescription, interaction.member.nick || 'test', [], [], customIdIdxPath, true), + ); + + // somethingWentWrong(bot, interaction, `TESTING@${rawEventTime}_${rawEventTimeZone}_${rawEventDate}`); } else { somethingWentWrong(bot, interaction, 'noDataFromEventDescriptionModal'); } diff --git a/src/buttons/event-creation/utils.ts b/src/buttons/event-creation/utils.ts index dae1c09..6cbae16 100644 --- a/src/buttons/event-creation/utils.ts +++ b/src/buttons/event-creation/utils.ts @@ -1,6 +1,21 @@ +import config from '../../../config.ts'; import { Activity } from './activities.ts'; -import { ActionRow, Bot, Interaction, MessageComponentTypes, SelectOption } from '../../../deps.ts'; +import { + ActionRow, + ApplicationCommandFlags, + Bot, + ButtonComponent, + ButtonStyles, + Interaction, + InteractionResponse, + InteractionResponseTypes, + MessageComponentTypes, + SelectOption, +} from '../../../deps.ts'; import utils from '../../utils.ts'; +import { successColor } from '../../commandUtils.ts'; +import { LFGMember } from '../../types/commandTypes.ts'; +import { customId as gameSelCustomId } from './step1-gameSelection.ts'; // Discord Interaction Tokens last 15 minutes, we will self kill after 14.5 minutes const tokenTimeoutS = (15 * 60) - 30; @@ -70,3 +85,119 @@ export const deleteTokenEarly = async (bot: Bot, interaction: Interaction, guild tokenMap.delete(generateMapId(guildId, channelId, userId)); } }; + +const finalizeButtons = (idxPath: string): [ButtonComponent, ButtonComponent, ButtonComponent] => [{ + type: MessageComponentTypes.Button, + label: 'Create Event', + style: ButtonStyles.Success, + customId: 'createEvent', // TODO: replace with proper id +}, { + type: MessageComponentTypes.Button, + label: 'Create Whitelisted Event', + style: ButtonStyles.Primary, + customId: `createEvent${idSeparator}`, // TODO: replace with proper id +}, { + type: MessageComponentTypes.Button, + label: 'Edit Event Details', + style: ButtonStyles.Secondary, + customId: `${gameSelCustomId}${idSeparator}${idxPath}${pathIdxEnder}`, +}]; + +export const generateLFGButtons = (whitelist: boolean): [ButtonComponent, ButtonComponent, ButtonComponent, ButtonComponent, ButtonComponent] => [{ + type: MessageComponentTypes.Button, + label: `${whitelist ? 'Request to ' : ''}Join`, + style: ButtonStyles.Success, + customId: `joinEvent${whitelist ? idSeparator : ''}`, // TODO: replace with proper id +}, { + type: MessageComponentTypes.Button, + label: `Join as Alternate`, + style: ButtonStyles.Primary, + customId: 'alternateEvent', // TODO: replace with proper id +}, { + type: MessageComponentTypes.Button, + label: 'Leave', + style: ButtonStyles.Danger, + customId: 'leaveEvent', // TODO: replace with proper id +}, { + type: MessageComponentTypes.Button, + label: '', + style: ButtonStyles.Secondary, + customId: 'editEvent', // TODO: replace with proper id + emoji: { + name: '✏️', + }, +}, { + type: MessageComponentTypes.Button, + label: '', + style: ButtonStyles.Secondary, + customId: 'deleteEvent', // TODO: replace with proper id + emoji: { + name: '🗑️', + }, +}]; + +export enum LfgEmbedIndexes { + Activity, + StartTime, + ICSLink, + Description, + JoinedMembers, + AlternateMembers, +} +export const createLFGPost = ( + category: string, + activity: Activity, + eventDateTime: Date, + eventDateTimeStr: String, + eventDescription: string, + author: string, + memberList: Array, + alternateList: Array, + idxPath: string, + editing: boolean, + whitelist = false, +): InteractionResponse => { + const icsDetails = `${category}: ${activity.name}`; + return { + type: InteractionResponseTypes.ChannelMessageWithSource, + data: { + flags: ApplicationCommandFlags.Ephemeral, + content: editing ? 'Please verify the information below, then click on the $name button below' : 'test', + embeds: [{ + color: successColor, + fields: [{ + name: `${category}:`, + value: activity.name, + inline: true, + }, { + name: 'Start Time:', + value: `${eventDateTimeStr}\n`, + inline: true, + }, { + name: 'Add to Calendar:', + value: `[Download ICS File](${config.links.addToCalendar}?t=${eventDateTime.getTime()}&n=${icsDetails.replaceAll(' ', '+')})`, + inline: true, + }, { + name: 'Description:', + value: eventDescription, + }, { + name: `Members Joined: ${memberList.length}/${activity.maxMembers}`, + value: memberList.length ? memberList.map((member) => `${member.name} - <@${member.id}>`).join('\n') : 'None', + inline: true, + }, { + name: 'Alternates:', + value: alternateList.length ? alternateList.map((member) => `${member.name} - <@${member.id}>${member.joined ? ' *' : ''}`).join('\n') : 'None', + inline: true, + }], + footer: { + text: `Created by: ${author}`, + }, + timestamp: eventDateTime.getTime(), + }], + components: [{ + type: MessageComponentTypes.ActionRow, + components: editing ? finalizeButtons(idxPath) : generateLFGButtons(whitelist), + }], + }, + }; +}; diff --git a/src/types/commandTypes.ts b/src/types/commandTypes.ts index 2a67f25..4d9452e 100644 --- a/src/types/commandTypes.ts +++ b/src/types/commandTypes.ts @@ -31,3 +31,9 @@ export type DBGuildSettings = { managerRoleId: bigint; logChannelId: bigint; }; + +export type LFGMember = { + id: bigint; + name: string; + joined?: boolean; +};