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
This commit is contained in:
Ean Milligan (Bastion) 2023-04-09 04:12:53 -04:00
parent 487079713f
commit a6848eb33a
8 changed files with 194 additions and 33 deletions

View File

@ -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');

View File

@ -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) {

View File

@ -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;
// 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: interaction.member.id,
id: memberId,
name: interaction.member.user.username,
}, interaction.guildId);
}
} else {
somethingWentWrong(bot, interaction, 'noDataFromJoinEventButton');
}

View File

@ -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<string, {
status: JoinRequestStatus;
timestamp: number;
}> = 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<LFGMember> =>
export const getLfgMembers = (rawMemberList: string): Array<LFGMember> =>
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<string> =>
export const getGuildName = async (bot: Bot, guildId: bigint): Promise<string> =>
(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,
}],
}];

View File

@ -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) => {
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
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));
return bot.helpers.sendMessage(userDmChannel?.id || 0n, message);
};

View File

@ -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, {

View File

@ -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,

View File

@ -17,10 +17,11 @@ export const queries = {
};
export const lfgChannelSettings: Map<string, LfgChannelSetting> = 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,
});