From 00a98db405a09b673d069b2e41c846f55a47cb54 Mon Sep 17 00:00:00 2001 From: "Ean Milligan (Bastion)" Date: Fri, 21 Apr 2023 02:00:36 -0400 Subject: [PATCH] Add manager Join/Leave/Alternate commands additionally update deleteConfirmed such that the group up manager name syntax matches name syntax elsewhere in the bot --- db/populateDefaults.ts | 3 + src/buttons/live-event/deleteConfirmed.ts | 5 +- src/buttons/live-event/joinRequest.ts | 2 +- src/buttons/live-event/utils.ts | 47 +++++-- src/commands/_index.ts | 3 +- src/commands/managerJLA.ts | 164 ++++++++++++++++++++++ src/commands/setup.ts | 3 +- src/utils.ts | 3 + 8 files changed, 215 insertions(+), 15 deletions(-) create mode 100644 src/commands/managerJLA.ts diff --git a/db/populateDefaults.ts b/db/populateDefaults.ts index 732003f..2588e65 100644 --- a/db/populateDefaults.ts +++ b/db/populateDefaults.ts @@ -8,6 +8,9 @@ const actions = [ 'cmd-report', 'cmd-setup', 'cmd-gameSel', + 'cmd-join', + 'cmd-leave', + 'cmd-alternate', 'btn-gameSel', 'btn-customAct', 'btn-createEvt', diff --git a/src/buttons/live-event/deleteConfirmed.ts b/src/buttons/live-event/deleteConfirmed.ts index b4557bc..4d11ac4 100644 --- a/src/buttons/live-event/deleteConfirmed.ts +++ b/src/buttons/live-event/deleteConfirmed.ts @@ -10,7 +10,7 @@ export const confirmedCustomId = 'confirmedCustomId'; export const confirmStr = 'yes'; const execute = async (bot: Bot, interaction: Interaction) => { - if (interaction?.data?.customId && interaction?.data?.components?.length && interaction.channelId && interaction.guildId && interaction.member) { + if (interaction?.data?.customId && interaction?.data?.components?.length && interaction.channelId && interaction.guildId && interaction.member && interaction.member.user) { // Light Telemetry dbClient.execute(queries.callIncCnt('btn-confirmDelEvent')).catch((e) => utils.commonLoggers.dbError('deleteConfirmed.ts@incCnt', 'call sproc INC_CNT on', e)); @@ -33,6 +33,7 @@ const execute = async (bot: Bot, interaction: Interaction) => { if (tempDataMap.get(confirmedCustomId)?.toLowerCase() === confirmStr) { const eventMessage = await bot.helpers.getMessage(evtChannelId, evtMessageId).catch((e: Error) => utils.commonLoggers.messageGetError('deleteConfirmed.ts', 'get eventMessage', e)); const userId = interaction.member.id; + const userName = interaction.member.user.username; // Delete event bot.helpers.deleteMessage(evtChannelId, evtMessageId, 'User deleted event').then(() => { dbClient.execute(queries.deleteEvent, [evtChannelId, evtMessageId]).catch((e) => utils.commonLoggers.dbError('deleteConfirmed.ts@deleteEvent', 'delete event from', e)); @@ -56,7 +57,7 @@ const execute = async (bot: Bot, interaction: Interaction) => { embeds: [{ color: infoColor2, title: `Event deleted by a ${config.name} Manager`, - description: `The following event was deleted by <@${userId}>.`, + description: `The following event was deleted by ${userName} - <@${userId}>.`, timestamp: new Date().getTime(), }, eventEmbed], }).catch((e: Error) => utils.commonLoggers.messageSendError('deleteConfirmed.ts', 'send log message', e)); diff --git a/src/buttons/live-event/joinRequest.ts b/src/buttons/live-event/joinRequest.ts index 52ad55e..08d0932 100644 --- a/src/buttons/live-event/joinRequest.ts +++ b/src/buttons/live-event/joinRequest.ts @@ -18,7 +18,7 @@ const execute = async (bot: Bot, interaction: Interaction) => { const memberRequesting = getLfgMembers(interaction.message.embeds[0].fields[0].value || '')[0]; const approved = interaction.data.customId.includes(approveStr); const responseStr = interaction.data.customId.split(idSeparator)[1] || ''; - const capResponseStr = `${responseStr.charAt(0).toUpperCase()}${responseStr.slice(1)}`; + const capResponseStr = utils.capitalizeFirstChar(responseStr); const eventIds = utils.messageUrlToIds(interaction.message.embeds[0].description.split(')')[0] || ''); const eventUrl = utils.idsToMessageUrl(eventIds); const joinRequestMapId = generateMapId(eventIds.messageId, eventIds.channelId, memberRequesting.id); diff --git a/src/buttons/live-event/utils.ts b/src/buttons/live-event/utils.ts index aba608c..2b27655 100644 --- a/src/buttons/live-event/utils.ts +++ b/src/buttons/live-event/utils.ts @@ -81,7 +81,7 @@ const editEvent = async ( memberList: Array, maxMemberCount: number, alternateList: Array, - loudAcknowledge = false, + loudAcknowledge: boolean, ) => { if (evtMessageEmbed.fields) { // Update the fields @@ -122,7 +122,7 @@ ${safelyDismissMsg}`, }; // Generic no response response -const noEdit = async (bot: Bot, interaction: Interaction, loudAcknowledge = false) => { +const noEdit = async (bot: Bot, interaction: Interaction, loudAcknowledge: boolean) => { if (loudAcknowledge) { bot.helpers.sendInteractionResponse(interaction.id, interaction.token, { type: InteractionResponseTypes.ChannelMessageWithSource, @@ -149,7 +149,16 @@ export const getGuildName = async (bot: Bot, guildId: bigint): Promise = (await bot.helpers.getGuild(guildId).catch((e: Error) => utils.commonLoggers.messageGetError('utils.ts', 'get guild', e)) || { name: 'failed to get guild name' }).name; // Remove member from the event -export const removeMemberFromEvent = async (bot: Bot, interaction: Interaction, evtMessageEmbed: Embed, evtMessageId: bigint, evtChannelId: bigint, userId: bigint, evtGuildId: bigint) => { +export const removeMemberFromEvent = async ( + bot: Bot, + interaction: Interaction, + evtMessageEmbed: Embed, + evtMessageId: bigint, + evtChannelId: bigint, + userId: bigint, + evtGuildId: bigint, + loudAcknowledge = false, +): Promise => { if (evtMessageEmbed.fields) { // Get old counts const [oldMemberCount, maxMemberCount] = getEventMemberCount(evtMessageEmbed.fields[LfgEmbedIndexes.JoinedMembers].name); @@ -202,13 +211,16 @@ export const removeMemberFromEvent = async (bot: Bot, interaction: Interaction, } // Update the event - await editEvent(bot, interaction, evtMessageEmbed, evtMessageId, evtChannelId, memberList, maxMemberCount, alternateList); + await editEvent(bot, interaction, evtMessageEmbed, evtMessageId, evtChannelId, memberList, maxMemberCount, alternateList, loudAcknowledge); + return true; } else { // Send noEdit response because user did not actually leave - await noEdit(bot, interaction); + await noEdit(bot, interaction, loudAcknowledge); + return false; } } else { await somethingWentWrong(bot, interaction, 'noFieldsInRemoveMember'); + return false; } }; @@ -222,7 +234,7 @@ export const alternateMemberToEvent = async ( member: LFGMember, userJoinOnFull = false, loudAcknowledge = false, -) => { +): Promise => { if (evtMessageEmbed.fields) { member.joined = userJoinOnFull; // Get current alternates @@ -241,18 +253,30 @@ export const alternateMemberToEvent = async ( // Update the event evtMessageEmbed.fields[LfgEmbedIndexes.AlternateMembers].value = generateAlternateList(alternateList); await editEvent(bot, interaction, evtMessageEmbed, evtMessageId, evtChannelId, memberList, maxMemberCount, alternateList, loudAcknowledge); + return true; } else { // Send noEdit response because user was already an alternate and joined status did not change await noEdit(bot, interaction, loudAcknowledge); + return false; } } else { // No fields, can't alternate await somethingWentWrong(bot, interaction, 'noFieldsInAlternateMember'); + return false; } }; // Join member to the event -export const joinMemberToEvent = async (bot: Bot, interaction: Interaction, evtMessageEmbed: Embed, evtMessageId: bigint, evtChannelId: bigint, member: LFGMember, evtGuildId: bigint) => { +export const joinMemberToEvent = async ( + bot: Bot, + interaction: Interaction, + evtMessageEmbed: Embed, + evtMessageId: bigint, + evtChannelId: bigint, + member: LFGMember, + evtGuildId: bigint, + loudAcknowledge = false, +): Promise => { if (evtMessageEmbed.fields) { // Get current member list and count const [oldMemberCount, maxMemberCount] = getEventMemberCount(evtMessageEmbed.fields[LfgEmbedIndexes.JoinedMembers].name); @@ -260,10 +284,11 @@ export const joinMemberToEvent = async (bot: Bot, interaction: Interaction, evtM // Verify user is not already on the joined list if (memberList.find((joinedMember) => joinedMember.id === member.id)) { // Send noEdit response because user was already joined - await noEdit(bot, interaction); + await noEdit(bot, interaction, loudAcknowledge); + return false; } else if (oldMemberCount === maxMemberCount) { // Event full, add member to alternate list - await alternateMemberToEvent(bot, interaction, evtMessageEmbed, evtMessageId, evtChannelId, member, true); + return await alternateMemberToEvent(bot, interaction, evtMessageEmbed, evtMessageId, evtChannelId, member, true, loudAcknowledge); } else { // Join member to event memberList.push(member); @@ -272,7 +297,7 @@ export const joinMemberToEvent = async (bot: Bot, interaction: Interaction, evtM const alternateList = removeLfgMember(getLfgMembers(evtMessageEmbed.fields[LfgEmbedIndexes.AlternateMembers].value), member.id); // Update the event - await editEvent(bot, interaction, evtMessageEmbed, evtMessageId, evtChannelId, memberList, maxMemberCount, alternateList); + await editEvent(bot, interaction, evtMessageEmbed, evtMessageId, evtChannelId, memberList, maxMemberCount, alternateList, loudAcknowledge); // Check if we need to notify the owner that their event has filled if (memberList.length === maxMemberCount) { @@ -292,10 +317,12 @@ export const joinMemberToEvent = async (bot: Bot, interaction: Interaction, evtM }], }).catch((e: Error) => utils.commonLoggers.messageSendError('utils.ts', 'event filled dm', e)); } + return true; } } else { // No fields, can't join await somethingWentWrong(bot, interaction, 'noFieldsInJoinMember'); + return false; } }; diff --git a/src/commands/_index.ts b/src/commands/_index.ts index 7650661..3db04fd 100644 --- a/src/commands/_index.ts +++ b/src/commands/_index.ts @@ -6,9 +6,10 @@ import info from './info.ts'; import report from './report.ts'; import setup from './setup.ts'; import deleteCmd from './delete.ts'; +import managerJLA from './managerJLA.ts'; import { gameSelectionCommand } from '../buttons/event-creation/step1-gameSelection.ts'; -export const commands: Array = [deleteCmd, info, report, setup, gameSelectionCommand]; +export const commands: Array = [deleteCmd, info, report, setup, gameSelectionCommand, managerJLA]; export const createSlashCommands = async (bot: Bot) => { const globalCommands: MakeRequired[] = []; diff --git a/src/commands/managerJLA.ts b/src/commands/managerJLA.ts new file mode 100644 index 0000000..b69fb2a --- /dev/null +++ b/src/commands/managerJLA.ts @@ -0,0 +1,164 @@ +import { ApplicationCommandOptionTypes, ApplicationCommandTypes, Bot, Interaction } from '../../deps.ts'; +import { alternateMemberToEvent, getGuildName, joinMemberToEvent, removeMemberFromEvent } from '../buttons/live-event/utils.ts'; +import { generateMemberList } from '../buttons/eventUtils.ts'; +import { dbClient, generateGuildSettingKey, lfgChannelSettings, queries } from '../db.ts'; +import { infoColor2, sendDirectMessage, somethingWentWrong, stopThat } from '../commandUtils.ts'; +import { CommandDetails, LFGMember } from '../types/commandTypes.ts'; +import config from '../../config.ts'; +import utils from '../utils.ts'; + +export const eventName = 'event'; +export const joinName = 'join'; +export const leaveName = 'leave'; +export const alternateName = 'alternate'; +export const eventLinkName = 'event-link'; +export const userName = 'user'; + +// Create command with three nearly identical subcommands +const generateOptions = (commandName: string) => ({ + name: commandName, + description: `${config.name} Manager Command: ${utils.capitalizeFirstChar(commandName)}s a user to an event in this channel.`, + type: ApplicationCommandOptionTypes.SubCommand, + options: [ + { + name: eventLinkName, + type: ApplicationCommandOptionTypes.String, + description: 'Please copy the message link for the desired event.', + required: true, + minLength: 31, + maxLength: 100, + }, + { + name: userName, + type: ApplicationCommandOptionTypes.User, + description: `The user you wish to ${commandName}.`, + required: true, + }, + ], +}); +const details: CommandDetails = { + name: eventName, + description: `${config.name} Manager Command`, + type: ApplicationCommandTypes.ChatInput, + options: [generateOptions(joinName), generateOptions(leaveName), generateOptions(alternateName)], +}; + +const execute = async (bot: Bot, interaction: Interaction) => { + if (interaction.data?.options?.[0].options && interaction.channelId && interaction.guildId && interaction.member && interaction.member.user) { + // Get action and log to db + const actionName = interaction.data.options[0].name; + dbClient.execute(queries.callIncCnt(`cmd-${actionName}`)).catch((e) => utils.commonLoggers.dbError('managerJLA.ts', 'call sproc INC_CNT on', e)); + const lfgChannelSetting = lfgChannelSettings.get(generateGuildSettingKey(interaction.guildId, interaction.channelId)) || { + managed: false, + managerRoleId: 0n, + logChannelId: 0n, + }; + + // Check if guild is managed and if user is a manager + if (lfgChannelSetting.managed && interaction.member.roles.includes(lfgChannelSetting.managerRoleId)) { + // User is a manager, parse out our data + const tempDataMap: Map = new Map(); + for (const option of interaction.data.options[0].options) { + tempDataMap.set(option.name || 'missingCustomId', option.value as string || ''); + } + const eventLink = tempDataMap.get(eventLinkName) || ''; + const userToAdd = BigInt(tempDataMap.get(userName) || '0'); + const eventIds = utils.messageUrlToIds(eventLink); + + // Verify fields exist + if (!eventLink || !userToAdd || !eventIds.guildId || !eventIds.channelId || !eventIds.messageId) { + somethingWentWrong(bot, interaction, 'missingLinkOrUserInManagerJLA'); + return; + } + + // Get event from link + const eventMessage = await bot.helpers.getMessage(eventIds.channelId, eventIds.messageId).catch((e: Error) => utils.commonLoggers.messageGetError('managerJLA.ts', 'get eventMessage', e)); + const userDetails = await bot.helpers.getUser(userToAdd).catch((e: Error) => utils.commonLoggers.messageGetError('managerJLA.ts', 'get userDetails', e)); + if (eventMessage && userDetails) { + // Perform the action + const userInfo: LFGMember = { + id: userToAdd, + name: userDetails.username, + }; + let changeMade = false; + switch (actionName) { + case joinName: + changeMade = await joinMemberToEvent(bot, interaction, eventMessage.embeds[0], eventIds.messageId, eventIds.channelId, userInfo, eventIds.guildId, true); + break; + case leaveName: + changeMade = await removeMemberFromEvent(bot, interaction, eventMessage.embeds[0], eventIds.messageId, eventIds.channelId, userToAdd, eventIds.guildId, true); + break; + case alternateName: + changeMade = await alternateMemberToEvent(bot, interaction, eventMessage.embeds[0], eventIds.messageId, eventIds.channelId, userInfo, false, true); + break; + default: + somethingWentWrong(bot, interaction, 'actionNameWrongManagerJLA'); + break; + } + + if (changeMade) { + // userToAdd was had JLA done to them, DM them with details\ + const guildName = await getGuildName(bot, interaction.guildId); + const commonFields = [{ + name: 'Event Link:', + value: `[Click Here](${eventLink}) to view the event.`, + inline: true, + }, { + name: 'Action Performed:', + value: utils.capitalizeFirstChar(actionName), + inline: true, + }]; + sendDirectMessage(bot, userToAdd, { + embeds: [{ + color: infoColor2, + title: `Notice: A ${config.name} Manager has performed an action for you in ${guildName}`, + fields: [ + { + name: `${config.name} Manager:`, + value: generateMemberList([{ + id: interaction.member.id, + name: interaction.member.user.username, + }]), + inline: true, + }, + ...commonFields, + { + name: 'Are you unhappy with this action?', + value: `Please reach out to the ${config.name} Manager that performed this action, or the moderators/administrators of ${guildName}.`, + }, + ], + }], + }).catch((e: Error) => utils.commonLoggers.messageSendError('managerJLA.ts', 'send DM fail', e)); + + // Log this action + bot.helpers.sendMessage(lfgChannelSetting.logChannelId, { + embeds: [{ + color: infoColor2, + title: `A ${config.name} Manager has performed an action on behalf of a user.`, + description: `The following user had an action by ${interaction.member.user.username} - <@${interaction.member.id}>.`, + fields: [...commonFields, { + name: 'User:', + value: generateMemberList([userInfo]), + inline: true, + }], + timestamp: new Date().getTime(), + }], + }).catch((e: Error) => utils.commonLoggers.messageSendError('deleteConfirmed.ts', 'send log message', e)); + } + } else { + somethingWentWrong(bot, interaction, 'eventOrUserMissingFromManagerJLA'); + } + } else { + // User not a manager + stopThat(bot, interaction, `${actionName} users to`); + } + } else { + // All data missing + somethingWentWrong(bot, interaction, 'missingDataInManagerJLA'); + } +}; + +export default { + details, + execute, +}; diff --git a/src/commands/setup.ts b/src/commands/setup.ts index 41f8d71..8164638 100644 --- a/src/commands/setup.ts +++ b/src/commands/setup.ts @@ -19,6 +19,7 @@ import { CommandDetails } from '../types/commandTypes.ts'; import utils from '../utils.ts'; import { customId as gameSelId } from '../buttons/event-creation/step1-gameSelection.ts'; import { alternateEventBtnStr, joinEventBtnStr, leaveEventBtnStr, requestToJoinEventBtnStr } from '../buttons/eventUtils.ts'; +import { alternateName, eventLinkName, eventName, joinName, leaveName, userName } from './managerJLA.ts'; const withoutMgrRole = 'without-manager-role'; const withMgrRole = 'with-manager-role'; @@ -150,7 +151,7 @@ const execute = async (bot: Bot, interaction: Interaction) => { name: `${config.name} Manager Details:`, value: `${config.name} Managers with the <@&${managerRoleId}> role may edit or delete events in this guild, along with using the following commands to update the activity members: -\`/join\` \`/leave\` \`/alternate\` +\`/${eventName} [${joinName} | ${leaveName} | ${alternateName}] [${eventLinkName}] [${userName}]\` The Discord Slash Command system will ensure you provide all the required details.`, }); diff --git a/src/utils.ts b/src/utils.ts index 3982562..713ee43 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -18,6 +18,8 @@ const messageUrlToIds = (url: string): UrlIds => { }; }; +const capitalizeFirstChar = (input: string) => `${input.charAt(0).toUpperCase()}${input.slice(1)}`; + const genericLogger = (level: LT, message: string) => log(level, message); const interactionSendError = (location: string, interaction: Interaction | string, err: Error) => genericLogger(LT.ERROR, `${location} | Failed to respond to interaction: ${jsonStringifyBig(interaction)} | Error: ${err.name} - ${err.message}`); @@ -38,6 +40,7 @@ const channelUpdateError = (location: string, message: string, err: Error) => ge const dbError = (location: string, type: string, err: Error) => genericLogger(LT.ERROR, `${location} | Failed to ${type} database | Error: ${err.name} - ${err.message}`); export default { + capitalizeFirstChar, commonLoggers: { channelUpdateError, dbError,