From a6848eb33a292a8dd6bbdeea6a3f99b40854ac28 Mon Sep 17 00:00:00 2001 From: "Ean Milligan (Bastion)" Date: Sun, 9 Apr 2023 04:12:53 -0400 Subject: [PATCH] Started work on WL Event system, minor update to other files for consistency db.ts, setup.ts, delete.ts: change lfgSettings to use generator function for map key joinEvent.ts: add first half of WL system, add system to reduce spam/abuse of this system. commandUtils.ts: fix sendDirectMessage to return right promise --- db/populateDefaults.ts | 1 - .../event-creation/step3-createEvent.ts | 2 +- src/buttons/live-event/joinEvent.ts | 120 ++++++++++++++++-- src/buttons/live-event/utils.ts | 71 ++++++++++- src/commandUtils.ts | 14 +- src/commands/delete.ts | 7 +- src/commands/setup.ts | 7 +- src/db.ts | 5 +- 8 files changed, 194 insertions(+), 33 deletions(-) diff --git a/db/populateDefaults.ts b/db/populateDefaults.ts index e5bc476..6f107b5 100644 --- a/db/populateDefaults.ts +++ b/db/populateDefaults.ts @@ -1,5 +1,4 @@ // This file will populate the tables with default values - import { dbClient } from '../src/db.ts'; console.log('Attempting to insert default actions into command_cnt'); diff --git a/src/buttons/event-creation/step3-createEvent.ts b/src/buttons/event-creation/step3-createEvent.ts index 89691ee..8e8c73a 100644 --- a/src/buttons/event-creation/step3-createEvent.ts +++ b/src/buttons/event-creation/step3-createEvent.ts @@ -27,7 +27,7 @@ const execute = async (bot: Bot, interaction: Interaction) => { embeds: [interaction.message.embeds[0]], components: [{ type: MessageComponentTypes.ActionRow, - components: generateLFGButtons(interaction.data.customId.includes(idSeparator)), + components: generateLFGButtons(interaction.data.customId.includes(idSeparator)), // TODO: verify we can DM the user if they set this to WL mode }], }).catch((e: Error) => utils.commonLoggers.messageSendError('step3-createEvent.ts', 'createEvent', e)); if (!eventMessage) { diff --git a/src/buttons/live-event/joinEvent.ts b/src/buttons/live-event/joinEvent.ts index bc42a5a..cb6feb9 100644 --- a/src/buttons/live-event/joinEvent.ts +++ b/src/buttons/live-event/joinEvent.ts @@ -1,24 +1,122 @@ -import { Bot, Interaction } from '../../../deps.ts'; -import { dbClient, queries } from '../../db.ts'; -import { somethingWentWrong } from '../../commandUtils.ts'; -import { idSeparator } from '../eventUtils.ts'; +import { ApplicationCommandFlags, Bot, Interaction, InteractionResponseTypes } from '../../../deps.ts'; +import { dbClient, generateGuildSettingKey, lfgChannelSettings, queries } from '../../db.ts'; +import { infoColor1, safelyDismissMsg, sendDirectMessage, somethingWentWrong, successColor, warnColor } from '../../commandUtils.ts'; +import { generateMemberList, idSeparator, LfgEmbedIndexes } from '../eventUtils.ts'; import utils from '../../utils.ts'; -import { joinMemberToEvent } from './utils.ts'; +import config from '../../../config.ts'; +import { generateMapId, getGuildName, getLfgMembers, joinMemberToEvent, joinRequestMap, joinRequestResponseButtons, JoinRequestStatus } from './utils.ts'; export const customId = 'joinEvent'; export const execute = async (bot: Bot, interaction: Interaction) => { - if (interaction.data?.customId && interaction.member && interaction.member.user && interaction.channelId && interaction.guildId && interaction.message && interaction.message.embeds[0]) { + if ( + interaction.data?.customId && interaction.member && interaction.member.user && interaction.channelId && interaction.guildId && interaction.message && interaction.message.embeds[0] && + interaction.message.embeds[0].fields + ) { // Light Telemetry dbClient.execute(queries.callIncCnt(interaction.data.customId.includes(idSeparator) ? 'btn-joinWLEvent' : 'btn-joinEvent')).catch((e) => utils.commonLoggers.dbError('joinEvent.ts', 'call sproc INC_CNT on', e) ); + const ownerId = BigInt(interaction.message.embeds[0].footer?.iconUrl?.split('#')[1] || '0'); + const memberId = interaction.member.id; - // Join user to event - joinMemberToEvent(bot, interaction, interaction.message.embeds[0], interaction.message.id, interaction.channelId, { - id: interaction.member.id, - name: interaction.member.user.username, - }, interaction.guildId); + // Check if event is whitelisted + if (interaction.data.customId.includes(idSeparator) && memberId !== ownerId) { + // Initialize WL vars + const joinRequestKey = generateMapId(interaction.message.id, interaction.channelId, memberId); + const messageUrl = utils.idsToMessageUrl({ + guildId: interaction.guildId, + channelId: interaction.channelId, + messageId: interaction.message.id, + }); + const lfgChannelSetting = lfgChannelSettings.get(generateGuildSettingKey(interaction.guildId, interaction.channelId)) || { managed: false }; + const urgentManagerStr = lfgChannelSetting.managed ? ` a ${config.name} Manager (members with the <@&${lfgChannelSetting.managerRoleId}> role in this guild) or ` : ' '; + const eventMembers = getLfgMembers(interaction.message.embeds[0].fields[LfgEmbedIndexes.JoinedMembers].value); + + if (eventMembers.find((lfgMember) => lfgMember.id === memberId)) { + // User is already joined to event, block request + bot.helpers.sendInteractionResponse(interaction.id, interaction.token, { + type: InteractionResponseTypes.ChannelMessageWithSource, + data: { + flags: ApplicationCommandFlags.Ephemeral, + embeds: [{ + color: warnColor, + title: 'Notice: Request Blocked', + description: `To reduce spam, ${config.name} has blocked this request to join as you have already joined this event. + +${safelyDismissMsg}`, + }], + }, + }).catch((e: Error) => utils.commonLoggers.interactionSendError('joinEvent.ts@userAlreadyJoined', interaction, e)); + } else if (joinRequestMap.has(joinRequestKey)) { + // User has already sent request, block new one + bot.helpers.sendInteractionResponse(interaction.id, interaction.token, { + type: InteractionResponseTypes.ChannelMessageWithSource, + data: { + flags: ApplicationCommandFlags.Ephemeral, + embeds: [{ + color: warnColor, + title: 'Notice: Request Blocked', + description: `To reduce spam, ${config.name} has blocked this request to join as you have recently sent a request for this event. + +If this request is urgent, please speak with${urgentManagerStr}the owner of [this event](${messageUrl}), <@${ownerId}>, to resolve the issue. + +The status of your recent Join Request for [this event](${messageUrl}) is: \`${joinRequestMap.get(joinRequestKey)?.status || 'Failed to retrieve status'}\` + +${safelyDismissMsg}`, + }], + }, + }).catch((e: Error) => utils.commonLoggers.interactionSendError('joinEvent.ts@requestBlocked', interaction, e)); + } else { + const guildName = await getGuildName(bot, interaction.guildId); + // User is not joined and this is first request, send the Join Request + sendDirectMessage(bot, ownerId, { + embeds: [{ + color: infoColor1, + title: 'New Join Request!', + description: `A member has requested to join [your event](${messageUrl}) in \`${guildName}\`. Please use the buttons below this message to Approve or Deny the request.`, + fields: [{ + name: 'Member Details:', + value: generateMemberList([{ + id: memberId, + name: interaction.member.user.username, + }]), + }], + }], + components: joinRequestResponseButtons(false), + }).then(() => { + // Alert requester that join request has been sent + bot.helpers.sendInteractionResponse(interaction.id, interaction.token, { + type: InteractionResponseTypes.ChannelMessageWithSource, + data: { + flags: ApplicationCommandFlags.Ephemeral, + embeds: [{ + color: successColor, + title: 'Notice: Request Received', + description: `The owner of [this event](${messageUrl}), <@${ownerId}>, has been notified of your request. You will receive a Direct Message when <@${ownerId}> responds to the request. + +${safelyDismissMsg}`, + }], + }, + }).catch((e: Error) => utils.commonLoggers.interactionSendError('joinEvent.ts@requestReceived', interaction, e)); + + // Track the request to prevent spam + joinRequestMap.set(joinRequestKey, { + status: JoinRequestStatus.Pending, + timestamp: new Date().getTime(), + }); + }).catch((e: Error) => { + somethingWentWrong(bot, interaction, 'failedToDMOwnerInRequestToJoinEventButton'); + utils.commonLoggers.messageSendError('joinEvent.ts@dmOwner', 'failed to DM owner for join request', e); + }); + } + } else { + // Join user to event + joinMemberToEvent(bot, interaction, interaction.message.embeds[0], interaction.message.id, interaction.channelId, { + id: memberId, + name: interaction.member.user.username, + }, interaction.guildId); + } } else { somethingWentWrong(bot, interaction, 'noDataFromJoinEventButton'); } diff --git a/src/buttons/live-event/utils.ts b/src/buttons/live-event/utils.ts index fae7ed2..a58dbcf 100644 --- a/src/buttons/live-event/utils.ts +++ b/src/buttons/live-event/utils.ts @@ -1,9 +1,52 @@ -import { Bot, ButtonStyles, Embed, Interaction, InteractionResponseTypes, MessageComponentTypes } from '../../../deps.ts'; +import { ActionRow, Bot, ButtonStyles, Embed, Interaction, InteractionResponseTypes, MessageComponentTypes } from '../../../deps.ts'; import { LFGMember, UrlIds } from '../../types/commandTypes.ts'; import { sendDirectMessage, somethingWentWrong, successColor } from '../../commandUtils.ts'; import { generateAlternateList, generateMemberList, generateMemberTitle, leaveEventBtnStr, LfgEmbedIndexes, noMembersStr } from '../eventUtils.ts'; import utils from '../../utils.ts'; +// Join status map to prevent spamming the system +export enum JoinRequestStatus { + Pending = 'Pending', + Approved = 'Approved', + Denied = 'Denied', +} +export const generateMapId = (messageId: bigint, channelId: bigint, userId: bigint) => `${messageId}-${channelId}-${userId}`; +export const joinRequestMap: Map = new Map(); + +// Join request map cleaner +const oneHour = 1000 * 60 * 60; +const oneDay = oneHour * 24; +const oneWeek = oneDay * 7; +setInterval(() => { + const now = new Date().getTime(); + joinRequestMap.forEach((joinRequest, key) => { + switch (joinRequest.status) { + case JoinRequestStatus.Approved: + // Delete Approved when over 1 hour old + if (joinRequest.timestamp > now - oneHour) { + joinRequestMap.delete(key); + } + break; + case JoinRequestStatus.Pending: + // Delete Pending when over 1 day old + if (joinRequest.timestamp > now - oneDay) { + joinRequestMap.delete(key); + } + break; + case JoinRequestStatus.Denied: + // Delete Rejected when over 1 week old + if (joinRequest.timestamp > now - oneWeek) { + joinRequestMap.delete(key); + } + break; + } + }); + // Run cleaner every hour +}, oneHour); + // Get Member Counts from the title const getEventMemberCount = (rawMemberTitle: string): [number, number] => { const [rawCurrentCount, rawMaxCount] = rawMemberTitle.split('/'); @@ -13,7 +56,7 @@ const getEventMemberCount = (rawMemberTitle: string): [number, number] => { }; // Get LFGMember objects from string list -const getLfgMembers = (rawMemberList: string): Array => +export const getLfgMembers = (rawMemberList: string): Array => rawMemberList.trim() === noMembersStr ? [] : rawMemberList.split('\n').map((rawMember) => { const [memberName, memberMention] = rawMember.split('-'); const lfgMember: LFGMember = { @@ -67,7 +110,7 @@ const noEdit = async (bot: Bot, interaction: Interaction) => }).catch((e: Error) => utils.commonLoggers.interactionSendError('utils.ts', interaction, e)); // Get Guild Name -const getGuildName = async (bot: Bot, guildId: bigint): Promise => +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 @@ -120,7 +163,7 @@ export const removeMemberFromEvent = async (bot: Bot, interaction: Interaction, customId: 'leaveEventCustomId', // TODO: fix }], }], - }).catch((e: Error) => utils.commonLoggers.messageSendError('utils.ts', 'user promotion', e)); + }).catch((e: Error) => utils.commonLoggers.messageSendError('utils.ts', 'user promotion dm', e)); } // Update the event @@ -203,7 +246,7 @@ export const joinMemberToEvent = async (bot: Bot, interaction: Interaction, evtM description: `[Click here to view the event in ${guildName}.](${utils.idsToMessageUrl(urlIds)})`, fields: evtMessageEmbed.fields, }], - }).catch((e: Error) => utils.commonLoggers.messageSendError('utils.ts', 'event filled', e)); + }).catch((e: Error) => utils.commonLoggers.messageSendError('utils.ts', 'event filled dm', e)); } } } else { @@ -211,3 +254,21 @@ export const joinMemberToEvent = async (bot: Bot, interaction: Interaction, evtM await somethingWentWrong(bot, interaction, 'noFieldsInJoinMember'); } }; + +// Join Request Approve/Deny Buttons +export const joinRequestResponseButtons = (disabled: boolean): ActionRow[] => [{ + type: MessageComponentTypes.ActionRow, + components: [{ + type: MessageComponentTypes.Button, + label: 'Approve Request', + style: ButtonStyles.Success, + customId: 'approveJoinRequestCustomId', // TODO: fix + disabled, + }, { + type: MessageComponentTypes.Button, + label: 'Deny Request', + style: ButtonStyles.Danger, + customId: 'denyJoinRequestCustomId', // TODO: fix + disabled, + }], +}]; diff --git a/src/commandUtils.ts b/src/commandUtils.ts index 537d336..1bd5bb7 100644 --- a/src/commandUtils.ts +++ b/src/commandUtils.ts @@ -1,6 +1,6 @@ import { ApplicationCommandFlags, Bot, CreateMessage, Interaction, InteractionResponseTypes } from '../deps.ts'; import config from '../config.ts'; -import { lfgChannelSettings } from './db.ts'; +import { generateGuildSettingKey, lfgChannelSettings } from './db.ts'; import utils from './utils.ts'; export const failColor = 0xe71212; @@ -21,7 +21,7 @@ export const getRandomStatus = (guildCount: number): string => { }; export const isLFGChannel = (guildId: bigint, channelId: bigint) => { - return (lfgChannelSettings.has(`${guildId}-${channelId}`) || channelId === 0n || guildId === 0n) ? ApplicationCommandFlags.Ephemeral : undefined; + return (lfgChannelSettings.has(generateGuildSettingKey(guildId, channelId)) || channelId === 0n || guildId === 0n) ? ApplicationCommandFlags.Ephemeral : undefined; }; export const somethingWentWrong = async (bot: Bot, interaction: Interaction, errorCode: string) => @@ -42,8 +42,8 @@ export const somethingWentWrong = async (bot: Bot, interaction: Interaction, err }).catch((e: Error) => utils.commonLoggers.interactionSendError('commandUtils.ts', interaction, e)); // Send DM to User -export const sendDirectMessage = async (bot: Bot, userId: bigint, message: CreateMessage) => - bot.helpers.getDmChannel(userId).then((userDmChannel) => { - // Actually send the DM - bot.helpers.sendMessage(userDmChannel.id, message).catch((e: Error) => utils.commonLoggers.messageSendError('commandUtils.ts', message, e)); - }).catch((e: Error) => utils.commonLoggers.messageGetError('commandUtils.ts', 'get userDmChannel', e)); +export const sendDirectMessage = async (bot: Bot, userId: bigint, message: CreateMessage) => { + const userDmChannel = await bot.helpers.getDmChannel(userId).catch((e: Error) => utils.commonLoggers.messageGetError('commandUtils.ts', 'get userDmChannel', e)); + // Actually send the DM + return bot.helpers.sendMessage(userDmChannel?.id || 0n, message); +}; diff --git a/src/commands/delete.ts b/src/commands/delete.ts index d81a64c..7ac7717 100644 --- a/src/commands/delete.ts +++ b/src/commands/delete.ts @@ -1,7 +1,7 @@ import config from '../../config.ts'; import { ApplicationCommandFlags, ApplicationCommandTypes, Bot, Interaction, InteractionResponseTypes } from '../../deps.ts'; import { failColor, safelyDismissMsg, somethingWentWrong, successColor } from '../commandUtils.ts'; -import { dbClient, lfgChannelSettings, queries } from '../db.ts'; +import { dbClient, generateGuildSettingKey, lfgChannelSettings, queries } from '../db.ts'; import { CommandDetails } from '../types/commandTypes.ts'; import utils from '../utils.ts'; @@ -16,7 +16,8 @@ const execute = async (bot: Bot, interaction: Interaction) => { dbClient.execute(queries.callIncCnt('cmd-delete')).catch((e) => utils.commonLoggers.dbError('delete.ts', 'call sproc INC_CNT on', e)); if (interaction.guildId && interaction.channelId) { - if (!lfgChannelSettings.has(`${interaction.guildId}-${interaction.channelId}`)) { + const lfgChannelSettingKey = generateGuildSettingKey(interaction.guildId, interaction.channelId); + if (!lfgChannelSettings.has(lfgChannelSettingKey)) { // Cannot delete a lfg channel that has not been set up bot.helpers.sendInteractionResponse(interaction.id, interaction.token, { type: InteractionResponseTypes.ChannelMessageWithSource, @@ -43,7 +44,7 @@ const execute = async (bot: Bot, interaction: Interaction) => { somethingWentWrong(bot, interaction, 'deleteDBDeleteFail'); return; } - lfgChannelSettings.delete(`${interaction.guildId}-${interaction.channelId}`); + lfgChannelSettings.delete(lfgChannelSettingKey); // Complete the interaction bot.helpers.sendInteractionResponse(interaction.id, interaction.token, { diff --git a/src/commands/setup.ts b/src/commands/setup.ts index b49bee6..977dfca 100644 --- a/src/commands/setup.ts +++ b/src/commands/setup.ts @@ -14,7 +14,7 @@ import { OverwriteTypes, } from '../../deps.ts'; import { failColor, infoColor2, safelyDismissMsg, somethingWentWrong, successColor } from '../commandUtils.ts'; -import { dbClient, lfgChannelSettings, queries } from '../db.ts'; +import { dbClient, generateGuildSettingKey, 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'; @@ -64,7 +64,8 @@ const execute = async (bot: Bot, interaction: Interaction) => { const setupOpts = interaction.data?.options?.[0]; if (setupOpts?.name && interaction.channelId && interaction.guildId) { - if (lfgChannelSettings.has(`${interaction.guildId}-${interaction.channelId}`)) { + const lfgChannelSettingKey = generateGuildSettingKey(interaction.guildId, interaction.channelId); + if (lfgChannelSettings.has(lfgChannelSettingKey)) { // Cannot setup a lfg channel that is already set up bot.helpers.sendInteractionResponse(interaction.id, interaction.token, { type: InteractionResponseTypes.ChannelMessageWithSource, @@ -260,7 +261,7 @@ The Discord Slash Command system will ensure you provide all the required detail return; } // Store the ids to the active map - lfgChannelSettings.set(`${interaction.guildId}-${interaction.channelId}`, { + lfgChannelSettings.set(lfgChannelSettingKey, { managed: setupOpts.name === withMgrRole, managerRoleId, logChannelId, diff --git a/src/db.ts b/src/db.ts index 6016ba8..e75d3ea 100644 --- a/src/db.ts +++ b/src/db.ts @@ -17,10 +17,11 @@ export const queries = { }; export const lfgChannelSettings: Map = new Map(); +export const generateGuildSettingKey = (guildId: bigint, channelId: bigint) => `${guildId}-${channelId}`; const getGuildSettings = await dbClient.query('SELECT * FROM guild_settings'); getGuildSettings.forEach((g: DBGuildSettings) => { - lfgChannelSettings.set(`${g.guildId}-${g.lfgChannelId}`, { - managed: g.managerRoleId === 0n && g.logChannelId === 0n, + lfgChannelSettings.set(generateGuildSettingKey(g.guildId, g.lfgChannelId), { + managed: g.managerRoleId !== 0n && g.logChannelId !== 0n, managerRoleId: g.managerRoleId, logChannelId: g.logChannelId, });