From f38ae161f637432a7e13eaaac239a9a05ab4251b Mon Sep 17 00:00:00 2001 From: "Ean Milligan (Bastion)" Date: Wed, 21 Dec 2022 14:49:52 -0500 Subject: [PATCH 001/110] ver bump for rewrite branch --- README.md | 2 +- config.example.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 14d6175..0f296e7 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Group Up - An Event Scheduling Discord Bot | V0.5.8 - 2022/12/09 +# Group Up - An Event Scheduling Discord Bot | V1.0.0 - 2022/12/21 [![SonarCloud](https://sonarcloud.io/images/project_badges/sonarcloud-orange.svg)](https://sonarcloud.io/summary/new_code?id=GroupUp) [![Maintainability Rating](https://sonarcloud.io/api/project_badges/measure?project=GroupUp&metric=sqale_rating)](https://sonarcloud.io/summary/new_code?id=GroupUp) [![Security Rating](https://sonarcloud.io/api/project_badges/measure?project=GroupUp&metric=security_rating)](https://sonarcloud.io/summary/new_code?id=GroupUp) [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=GroupUp&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=GroupUp) [![Bugs](https://sonarcloud.io/api/project_badges/measure?project=GroupUp&metric=bugs)](https://sonarcloud.io/summary/new_code?id=GroupUp) [![Duplicated Lines (%)](https://sonarcloud.io/api/project_badges/measure?project=GroupUp&metric=duplicated_lines_density)](https://sonarcloud.io/summary/new_code?id=GroupUp) [![Lines of Code](https://sonarcloud.io/api/project_badges/measure?project=GroupUp&metric=ncloc)](https://sonarcloud.io/summary/new_code?id=GroupUp) diff --git a/config.example.ts b/config.example.ts index 797c4b9..04c30f8 100644 --- a/config.example.ts +++ b/config.example.ts @@ -1,6 +1,6 @@ export const config = { 'name': 'Group Up', // Name of the bot - 'version': '0.5.8', // Version of the bot + 'version': '1.0.0', // Version of the bot 'token': 'the_bot_token', // Discord API Token for this bot 'localtoken': 'local_testing_token', // Discord API Token for a secondary OPTIONAL testing bot, THIS MUST BE DIFFERENT FROM "token" 'prefix': 'gu!', // Prefix for all commands From 460a7ed557e836acc6f5773368494897e4abf63a Mon Sep 17 00:00:00 2001 From: "Ean Milligan (Bastion)" Date: Mon, 9 Jan 2023 21:26:41 -0500 Subject: [PATCH 002/110] Begin rewrite --- _OLD/deps.ts | 41 + _OLD/mod.ts | 1246 ++++++++++++++++++++++++++++ {src => _OLD/src}/constantCmds.ts | 0 {src => _OLD/src}/games.ts | 0 {src => _OLD/src}/intervals.ts | 0 {src => _OLD/src}/lfgHandlers.d.ts | 0 {src => _OLD/src}/lfgHandlers.ts | 0 {src => _OLD/src}/mod.d.ts | 0 {src => _OLD/src}/timeUtils.ts | 0 {src => _OLD/src}/utils.ts | 0 deps.ts | 6 +- flags.ts | 4 +- mod.ts | 1246 ---------------------------- 13 files changed, 1292 insertions(+), 1251 deletions(-) create mode 100644 _OLD/deps.ts create mode 100644 _OLD/mod.ts rename {src => _OLD/src}/constantCmds.ts (100%) rename {src => _OLD/src}/games.ts (100%) rename {src => _OLD/src}/intervals.ts (100%) rename {src => _OLD/src}/lfgHandlers.d.ts (100%) rename {src => _OLD/src}/lfgHandlers.ts (100%) rename {src => _OLD/src}/mod.d.ts (100%) rename {src => _OLD/src}/timeUtils.ts (100%) rename {src => _OLD/src}/utils.ts (100%) diff --git a/_OLD/deps.ts b/_OLD/deps.ts new file mode 100644 index 0000000..65f3887 --- /dev/null +++ b/_OLD/deps.ts @@ -0,0 +1,41 @@ +// All external dependancies are to be loaded here to make updating dependancy versions much easier +export { + botId, + cache, + cacheHandlers, + deleteMessage, + DiscordActivityTypes, + DiscordButtonStyles, + DiscordInteractionResponseTypes, + DiscordInteractionTypes, + editBotNickname, + editBotStatus, + getGuild, + getMessage, + getUser, + hasGuildPermissions, + Intents, + sendDirectMessage, + sendInteractionResponse, + sendMessage, + startBot, + structures, +} from 'https://deno.land/x/discordeno@12.0.1/mod.ts'; + +export type { + ActionRow, + ButtonComponent, + ButtonData, + CreateMessage, + DebugArg, + DiscordenoGuild, + DiscordenoMember, + DiscordenoMessage, + Embed, + EmbedField, + Interaction, +} from 'https://deno.land/x/discordeno@12.0.1/mod.ts'; + +export { Client } from 'https://deno.land/x/mysql@v2.10.1/mod.ts'; + +export { initLog, log, LogTypes as LT } from 'https://raw.githubusercontent.com/Burn-E99/Log4Deno/V1.1.0/mod.ts'; diff --git a/_OLD/mod.ts b/_OLD/mod.ts new file mode 100644 index 0000000..ed6fa54 --- /dev/null +++ b/_OLD/mod.ts @@ -0,0 +1,1246 @@ +import { + ActionRow, + botId, + ButtonComponent, + ButtonData, + cache, + cacheHandlers, + // MySQL Driver deps + Client, + DebugArg, + deleteMessage, + DiscordActivityTypes, + DiscordButtonStyles, + DiscordenoGuild, + DiscordenoMessage, + DiscordInteractionResponseTypes, + DiscordInteractionTypes, + editBotNickname, + editBotStatus, + Embed, + getGuild, + getMessage, + getUser, + hasGuildPermissions, + initLog, + Intents, + log, + // Log4Deno deps + LT, + sendDirectMessage, + sendInteractionResponse, + sendMessage, + // Discordeno deps + startBot, +} from './deps.ts'; + +import { ActiveLFG, BuildingLFG, GuildCleanChannels, GuildModRoles, GuildPrefixes } from './src/mod.d.ts'; +import intervals from './src/intervals.ts'; +import { LFGActivities } from './src/games.ts'; +import { JoinLeaveType } from './src/lfgHandlers.d.ts'; +import { handleLFGStep, handleMemberJoin, handleMemberLeave, urlToIds } from './src/lfgHandlers.ts'; +import { constantCmds, editBtns, lfgStepQuestions } from './src/constantCmds.ts'; +import { jsonParseBig, jsonStringifyBig } from './src/utils.ts'; + +import { DEBUG, LOCALMODE } from './flags.ts'; +import config from './config.ts'; + +// Initialize DB client +const dbClient = await new Client().connect({ + hostname: LOCALMODE ? config.db.localhost : config.db.host, + port: config.db.port, + db: config.db.name, + username: config.db.username, + password: config.db.password, +}); + +// Initialize logging client with folder to use for logs, needs --allow-write set on Deno startup +initLog('logs', DEBUG); +log(LT.INFO, `${config.name} Starting up . . .`); + +// Handle idling out the active builders +const activeBuilders: Array = []; +setInterval(() => { + intervals.buildingTimeout(activeBuilders); +}, 1000); + +const activeLFGPosts: Array = jsonParseBig(localStorage.getItem('activeLFGPosts') || '[]'); +log(LT.INFO, `Loaded ${activeLFGPosts.length} activeLFGPosts`); +setInterval(() => { + intervals.lfgNotifier(activeLFGPosts); +}, 60000); + +const guildPrefixes: Map = new Map(); +const getGuildPrefixes = await dbClient.query('SELECT * FROM guild_prefix'); +getGuildPrefixes.forEach((g: GuildPrefixes) => { + guildPrefixes.set(g.guildId, g.prefix); +}); + +const guildModRoles: Map = new Map(); +const getGuildModRoles = await dbClient.query('SELECT * FROM guild_mod_role'); +getGuildModRoles.forEach((g: GuildModRoles) => { + guildModRoles.set(g.guildId, g.roleId); +}); + +const cleanChannels: Map> = new Map(); +const getCleanChannels = await dbClient.query('SELECT * FROM guild_clean_channel'); +getCleanChannels.forEach((g: GuildCleanChannels) => { + const tempArr = cleanChannels.get(g.guildId) || []; + tempArr.push(g.channelId); + cleanChannels.set(g.guildId, tempArr); +}); + +const ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; + +// Start up the Discord Bot +startBot({ + token: LOCALMODE ? config.localtoken : config.token, + intents: [Intents.GuildMessages, Intents.DirectMessages, Intents.Guilds], + eventHandlers: { + ready: () => { + log(LT.INFO, `${config.name} Logged in!`); + editBotStatus({ + activities: [{ + name: 'Booting up . . .', + type: DiscordActivityTypes.Game, + createdAt: new Date().getTime(), + }], + status: 'online', + }); + + // Interval to rotate the status text every 30 seconds to show off more commands + setInterval(async () => { + log(LT.LOG, 'Changing bot status'); + try { + const cachedCount = await cacheHandlers.size('guilds'); + // Wrapped in try-catch due to hard crash possible + editBotStatus({ + activities: [{ + name: intervals.getRandomStatus(cachedCount), + type: DiscordActivityTypes.Game, + createdAt: new Date().getTime(), + }], + status: 'online', + }); + } catch (e) { + log(LT.ERROR, `Failed to update status: ${jsonStringifyBig(e)}`); + } + }, 30000); + + // Interval to update bot list stats every 24 hours + LOCALMODE ? log(LT.INFO, 'updateListStatistics not running') : setInterval(() => { + log(LT.LOG, 'Updating all bot lists statistics'); + intervals.updateListStatistics(botId, cache.guilds.size); + }, 86400000); + + // setTimeout added to make sure the startup message does not error out + setTimeout(() => { + LOCALMODE && editBotNickname(config.devServer, `LOCAL - ${config.name}`); + LOCALMODE ? log(LT.INFO, 'updateListStatistics not running') : intervals.updateListStatistics(botId, cache.guilds.size); + editBotStatus({ + activities: [{ + name: 'Booting Complete', + type: DiscordActivityTypes.Game, + createdAt: new Date().getTime(), + }], + status: 'online', + }); + sendMessage(config.logChannel, `${config.name} has started, running version ${config.version}.`).catch((e) => { + log(LT.ERROR, `Failed to send message: ${jsonStringifyBig(e)}`); + }); + }, 1000); + }, + guildCreate: (guild: DiscordenoGuild) => { + log(LT.LOG, `Handling joining guild ${jsonStringifyBig(guild)}`); + sendMessage(config.logChannel, `New guild joined: ${guild.name} (id: ${guild.id}). This guild has ${guild.memberCount} members!`).catch((e) => { + log(LT.ERROR, `Failed to send message: ${jsonStringifyBig(e)}`); + }); + }, + guildDelete: async (guild: DiscordenoGuild) => { + log(LT.LOG, `Handling leaving guild ${jsonStringifyBig(guild)}`); + sendMessage(config.logChannel, `I have been removed from: ${guild.name} (id: ${guild.id}).`).catch((e) => { + log(LT.ERROR, `Failed to send message: ${jsonStringifyBig(e)}`); + }); + + try { + await dbClient.execute('DELETE FROM guild_prefix WHERE guildId = ?', [guild.id]); + await dbClient.execute('DELETE FROM guild_mod_role WHERE guildId = ?', [guild.id]); + await dbClient.execute('DELETE FROM guild_clean_channel WHERE guildId = ?', [guild.id]); + } catch (e) { + log(LT.WARN, `Failed to remove guild from DB: ${jsonStringifyBig(e)}`); + } + }, + debug: (dmsg: string | DebugArg, data?: string) => log(LT.LOG, `Debug Message | ${jsonStringifyBig(dmsg)} | ${jsonStringifyBig(data)}`, false), + messageCreate: async (message: DiscordenoMessage) => { + // Ignore all other bots + if (message.isBot) return; + + const prefix = guildPrefixes.get(message.guildId) || config.prefix; + + // Handle messages not starting with the prefix + if (message.content.indexOf(prefix) !== 0) { + // Mentions + if (message.mentionedUserIds[0] === botId && (message.content.trim().startsWith(`<@${botId}>`) || message.content.trim().startsWith(`<@!${botId}>`))) { + // Light telemetry to see how many times a command is being run + await dbClient.execute(`CALL INC_CNT("prefix");`).catch((e) => { + log(LT.ERROR, `Failed to call stored procedure INC_CNT: ${jsonStringifyBig(e)}`); + }); + + if (message.content.trim() === `<@${botId}>` || message.content.trim() === `<@!${botId}>`) { + message.send({ + embeds: [{ + title: `Hello ${message.member?.username}, and thanks for using Group Up!`, + fields: [ + { + name: `My prefix in this guild is: \`${prefix}\``, + value: 'Mention me with a new prefix to change it.', + }, + ], + }], + }).catch((e) => { + log(LT.WARN, `Failed to send message | ${jsonStringifyBig(e)}`); + }); + } else if (await hasGuildPermissions(message.guildId, message.authorId, ['ADMINISTRATOR'])) { + const newPrefix = message.content.replace(`<@!${botId}>`, '').replace(`<@${botId}>`, '').trim(); + + if (newPrefix.length <= 10) { + let success = true; + if (guildPrefixes.has(message.guildId)) { + // Execute the DB update + await dbClient.execute('UPDATE guild_prefix SET prefix = ? WHERE guildId = ?', [newPrefix, message.guildId]).catch((e) => { + log(LT.ERROR, `Failed to insert into database: ${jsonStringifyBig(e)}`); + success = false; + }); + } else { + // Execute the DB insertion + await dbClient.execute('INSERT INTO guild_prefix(guildId,prefix) values(?,?)', [message.guildId, newPrefix]).catch((e) => { + log(LT.ERROR, `Failed to insert into database: ${jsonStringifyBig(e)}`); + success = false; + }); + } + + if (success) { + guildPrefixes.set(message.guildId, newPrefix); + message.send({ + embeds: [{ + fields: [ + { + name: `My prefix in this guild is now: \`${newPrefix}\``, + value: 'Mention me with a new prefix to change it.', + }, + ], + }], + }).catch((e) => { + log(LT.WARN, `Failed to send message | ${jsonStringifyBig(e)}`); + }); + } else { + message.send({ + embeds: [{ + fields: [ + { + name: 'Something went wrong!', + value: `My prefix is still \`${prefix}\`. Please try again, and if the problem persists, please report this to the developers using \`${prefix}report\`.`, + }, + ], + }], + }).catch((e) => { + log(LT.WARN, `Failed to send message | ${jsonStringifyBig(e)}`); + }); + } + } else { + message.send({ + embeds: [{ + fields: [ + { + name: 'Prefix too long, please set a prefix less than 10 characters long.', + value: 'Mention me with a new prefix to change it.', + }, + ], + }], + }).catch((e) => { + log(LT.WARN, `Failed to send message | ${jsonStringifyBig(e)}`); + }); + } + } + return; + } + + // Other + const activeIdx = activeBuilders.findIndex((x) => (message.channelId === x.channelId && message.authorId === x.userId)); + if (activeIdx > -1) { + activeBuilders[activeIdx].lastTouch = new Date(); + activeBuilders[activeIdx] = await handleLFGStep(activeBuilders[activeIdx], message.content); + + if (activeBuilders[activeIdx].step === 'done') { + if (message.member) { + const memberJoined = handleMemberJoin(activeBuilders[activeIdx].lfgMsg.embeds[0].fields || [], message.member, false); + + const newTimestamp = new Date(parseInt(memberJoined.embed[1].value.split('#')[1])); + const newLfgUid = ALPHABET[Math.floor(Math.random() * 26)] + ALPHABET[Math.floor(Math.random() * 26)]; + + const tempMembers = memberJoined.embed[4].name.split(':')[1].split('/'); + const currentMembers = parseInt(tempMembers[0]); + const maxMembers = parseInt(tempMembers[1]); + + if (activeBuilders[activeIdx].editing) { + if (currentMembers > maxMembers) { + const currentPeople = memberJoined.embed[4].value.split('\n'); + const newAlts = currentPeople.splice(maxMembers); + memberJoined.embed[4].value = currentPeople.join('\n') || 'None'; + memberJoined.embed[5].value = `${newAlts.join('\n')}\n${memberJoined.embed[5].value === 'None' ? '' : memberJoined.embed[5].value}`; + memberJoined.embed[4].name = `Members Joined: ${maxMembers}/${maxMembers}`; + } + } + + await activeBuilders[activeIdx].lfgMsg.edit({ + content: '', + embeds: [{ + fields: memberJoined.embed, + footer: { + text: `Created by: ${message.member.username} | ${newLfgUid}`, + }, + timestamp: newTimestamp.toISOString(), + }], + components: [ + { + type: 1, + components: [ + { + type: 2, + label: 'Join', + customId: 'active@join_group', + style: DiscordButtonStyles.Success, + }, + { + type: 2, + label: 'Leave', + customId: 'active@leave_group', + style: DiscordButtonStyles.Danger, + }, + { + type: 2, + label: 'Join as Alternate', + customId: 'active@alternate_group', + style: DiscordButtonStyles.Primary, + }, + ], + }, + ], + }).catch((e) => { + log(LT.WARN, `Failed to edit message | ${jsonStringifyBig(e)}`); + }); + + if (activeBuilders[activeIdx]) { + const activeLFGIdx = activeLFGPosts.findIndex( + (lfg) => (lfg.channelId === activeBuilders[activeIdx].channelId && lfg.messageId === activeBuilders[activeIdx].lfgMsg.id && lfg.ownerId === activeBuilders[activeIdx].userId), + ); + if (activeLFGIdx >= 0) { + activeLFGPosts[activeLFGIdx].lfgUid = newLfgUid; + activeLFGPosts[activeLFGIdx].lfgTime = newTimestamp.getTime(); + activeLFGPosts[activeLFGIdx].notified = false; + activeLFGPosts[activeLFGIdx].locked = false; + } else { + activeLFGPosts.push({ + messageId: activeBuilders[activeIdx].lfgMsg.id, + channelId: activeBuilders[activeIdx].lfgMsg.channelId, + ownerId: message.authorId, + lfgUid: newLfgUid, + lfgTime: newTimestamp.getTime(), + notified: false, + locked: false, + }); + } + localStorage.setItem('activeLFGPosts', jsonStringifyBig(activeLFGPosts)); + } + } + + await activeBuilders[activeIdx].questionMsg.delete().catch((e) => { + log(LT.WARN, `Failed to delete message | ${jsonStringifyBig(e)}`); + }); + activeBuilders.splice(activeIdx, 1); + } + await message.delete().catch((e) => { + log(LT.WARN, `Failed to delete message | ${jsonStringifyBig(e)}`); + }); + return; + } + + // Should this get cleaned up? + const enabledCleanChannels = cleanChannels.get(message.guildId); + if (enabledCleanChannels && enabledCleanChannels.length && enabledCleanChannels.indexOf(message.channelId) > -1) { + message.delete('Cleaning Channel').catch((e) => { + log(LT.WARN, `Failed to delete message | ${jsonStringifyBig(e)}`); + }); + return; + } + return; + } else { + // User is sending a command, make sure its a lfg command if its being sent in a clean channel + const enabledCleanChannels = cleanChannels.get(message.guildId); + if (enabledCleanChannels && enabledCleanChannels.length && enabledCleanChannels.indexOf(message.channelId) > -1 && message.content.indexOf(`${prefix}lfg`) !== 0) { + message.delete('Cleaning Channel').catch((e) => { + log(LT.WARN, `Failed to delete message | ${jsonStringifyBig(e)}`); + }); + return; + } + } + + log(LT.LOG, `Handling message ${jsonStringifyBig(message)}`); + + // Split into standard command + args format + const args = message.content.slice(prefix.length).trim().split(/[ \n]+/g); + const command = args.shift()?.toLowerCase(); + + // All commands below here + + // ping + // Its a ping test, what else do you want. + if (command === 'ping') { + // Light telemetry to see how many times a command is being run + dbClient.execute(`CALL INC_CNT("ping");`).catch((e) => { + log(LT.ERROR, `Failed to call stored procedure INC_CNT: ${jsonStringifyBig(e)}`); + }); + + // Calculates ping between sending a message and editing it, giving a nice round-trip latency. + try { + const m = await message.send({ + embeds: [{ + title: 'Ping?', + }], + }); + m.edit({ + embeds: [{ + title: `Pong! Latency is ${m.timestamp - message.timestamp}ms.`, + }], + }); + } catch (e) { + log(LT.ERROR, `Failed to send message: ${jsonStringifyBig(message)} | ${jsonStringifyBig(e)}`); + } + } // lfg + // Handles all LFG commands, creating, editing, deleting + else if (command === 'lfg') { + // Light telemetry to see how many times a command is being run + dbClient.execute(`CALL INC_CNT("lfg");`).catch((e) => { + log(LT.ERROR, `Failed to call stored procedure INC_CNT: ${jsonStringifyBig(e)}`); + }); + + const subcmd = (args[0] || 'help').toLowerCase(); + const lfgUid = (args[1] || '').toUpperCase(); + + // Learn how the LFG command works + if (subcmd === 'help' || subcmd === 'h' || subcmd === '?') { + message.send(constantCmds.lfgHelp).catch((e) => { + log(LT.ERROR, `Failed to send message: ${jsonStringifyBig(message)} | ${jsonStringifyBig(e)}`); + }); + } // Create a new LFG + else if (subcmd === 'create' || subcmd === 'c') { + try { + const lfgMsg = await message.send(`Creating new LFG post for <@${message.authorId}>. Please reply with the requested information and watch as your LFG post gets created!`); + + const gameButtons: Array = Object.keys(LFGActivities).map((game) => { + return { + type: 2, + label: game, + customId: `building@set_game#${game}`, + style: DiscordButtonStyles.Primary, + }; + }); + + const buttonComps: Array = []; + + const temp: Array = []; + + gameButtons.forEach((btn, idx) => { + if (!temp[Math.floor(idx / 5)]) { + temp[Math.floor(idx / 5)] = [btn]; + } else { + temp[Math.floor(idx / 5)].push(btn); + } + }); + + temp.forEach((btns) => { + if (btns.length && btns.length <= 5) { + buttonComps.push({ + type: 1, + components: btns, + }); + } + }); + + const question = await message.send({ + content: lfgStepQuestions.set_game, + components: buttonComps, + }); + + activeBuilders.push({ + userId: message.authorId, + channelId: message.channelId, + step: 'set_game', + lfgMsg: lfgMsg, + questionMsg: question, + lastTouch: new Date(), + maxIdle: 60, + editing: false, + }); + + message.delete().catch((e) => { + log(LT.WARN, `Failed to delete message | ${jsonStringifyBig(e)}`); + }); + } catch (e) { + log(LT.WARN, `LFG failed at step | create | ${jsonStringifyBig(e)}`); + } + } // Delete an existing LFG + else if (subcmd === 'delete' || subcmd === 'd') { + try { + // User provided a Uid, use it + if (lfgUid) { + const matches = activeLFGPosts.filter((lfg) => (message.authorId === lfg.ownerId && message.channelId === lfg.channelId && lfgUid === lfg.lfgUid)); + + // Found one, delete + if (matches.length) { + await deleteMessage(matches[0].channelId, matches[0].messageId, 'User requested LFG to be deleted.').catch((e) => { + log(LT.WARN, `Failed to find message to delete | ${jsonStringifyBig(e)}`); + }); + const lfgIdx = activeLFGPosts.findIndex((lfg) => (message.authorId === lfg.ownerId && message.channelId === lfg.channelId && lfgUid === lfg.lfgUid)); + + activeLFGPosts.splice(lfgIdx, 1); + + localStorage.setItem('activeLFGPosts', jsonStringifyBig(activeLFGPosts)); + + const m = await message.send(constantCmds.lfgDelete3); + + m.delete('Channel Cleanup', 5000).catch((e) => { + log(LT.WARN, `Failed to delete message | ${jsonStringifyBig(e)}`); + }); + message.delete('Channel Cleanup', 5000).catch((e) => { + log(LT.WARN, `Failed to delete message | ${jsonStringifyBig(e)}`); + }); + } // Did not find one + else { + const m = await message.send(constantCmds.lfgDelete1); + + m.delete('Channel Cleanup', 5000).catch((e) => { + log(LT.WARN, `Failed to delete message | ${jsonStringifyBig(e)}`); + }); + message.delete('Channel Cleanup', 5000).catch((e) => { + log(LT.WARN, `Failed to delete message | ${jsonStringifyBig(e)}`); + }); + } + } // User did not provide a Uid, find it automatically + else { + const matches = activeLFGPosts.filter((lfg) => (message.authorId === lfg.ownerId && message.channelId === lfg.channelId)); + + // Found one, delete + if (matches.length === 1) { + await deleteMessage(matches[0].channelId, matches[0].messageId, 'User requested LFG to be deleted.').catch((e) => { + log(LT.WARN, `Failed to find message to delete | ${jsonStringifyBig(e)}`); + }); + const lfgIdx = activeLFGPosts.findIndex((lfg) => (message.authorId === lfg.ownerId && message.channelId === lfg.channelId)); + + activeLFGPosts.splice(lfgIdx, 1); + + localStorage.setItem('activeLFGPosts', jsonStringifyBig(activeLFGPosts)); + + const m = await message.send(constantCmds.lfgDelete3); + + m.delete('Channel Cleanup', 5000).catch((e) => { + log(LT.WARN, `Failed to delete message | ${jsonStringifyBig(e)}`); + }); + message.delete('Channel Cleanup', 5000).catch((e) => { + log(LT.WARN, `Failed to delete message | ${jsonStringifyBig(e)}`); + }); + } // Found multiple, notify user + else if (matches.length) { + const deleteMsg = constantCmds.lfgDelete2; + const deepCloningFailedSoThisIsTheSolution = constantCmds.lfgDelete2.embeds[0].fields[0].value; + matches.forEach((mt) => { + deleteMsg.embeds[0].fields[0].value += `[${mt.lfgUid}](https://discord.com/channels/${message.guildId}/${mt.channelId}/${mt.messageId})\n`; + }); + + deleteMsg.embeds[0].fields[0].value += '\nThis message will self descruct in 30 seconds.'; + + const m = await message.send(deleteMsg); + constantCmds.lfgDelete2.embeds[0].fields[0].value = deepCloningFailedSoThisIsTheSolution; + + m.delete('Channel Cleanup', 30000).catch((e) => { + log(LT.WARN, `Failed to delete message | ${jsonStringifyBig(e)}`); + }); + message.delete('Channel Cleanup', 30000).catch((e) => { + log(LT.WARN, `Failed to delete message | ${jsonStringifyBig(e)}`); + }); + } // Found none, notify user you cannot delete other's lfgs + else { + const m = await message.send(constantCmds.lfgDelete1); + + m.delete('Channel Cleanup', 5000).catch((e) => { + log(LT.WARN, `Failed to delete message | ${jsonStringifyBig(e)}`); + }); + message.delete('Channel Cleanup', 5000).catch((e) => { + log(LT.WARN, `Failed to delete message | ${jsonStringifyBig(e)}`); + }); + } + } + } catch (e) { + log(LT.WARN, `LFG failed at step | delete | ${jsonStringifyBig(e)}`); + } + } // Edit an existing LFG + else if (subcmd === 'edit' || subcmd === 'e') { + try { + // User provided a Uid, use it + if (lfgUid) { + const matches = activeLFGPosts.filter((lfg) => (message.authorId === lfg.ownerId && message.channelId === lfg.channelId && lfgUid === lfg.lfgUid)); + + // Found one, edit + if (matches.length) { + const lfgMessage = await (await getMessage(matches[0].channelId, matches[0].messageId)).edit({ + content: `Editing new LFG post for <@${matches[0].ownerId}>. Please reply with the requested information and watch as your LFG post gets edited!`, + }); + const question = await message.send({ + content: 'Please select an item to edit from the buttons below:', + components: [{ + type: 1, + components: editBtns, + }], + }); + + activeBuilders.push({ + userId: matches[0].ownerId, + channelId: matches[0].channelId, + step: 'edit_btn', + lfgMsg: lfgMessage, + questionMsg: question, + lastTouch: new Date(), + maxIdle: 60, + editing: true, + }); + + message.delete().catch((e) => { + log(LT.WARN, `Failed to delete message | ${jsonStringifyBig(e)}`); + }); + } // Did not find one + else { + const m = await message.send(constantCmds.lfgEdit1); + + m.delete('Channel Cleanup', 5000).catch((e) => { + log(LT.WARN, `Failed to delete message | ${jsonStringifyBig(e)}`); + }); + message.delete('Channel Cleanup', 5000).catch((e) => { + log(LT.WARN, `Failed to delete message | ${jsonStringifyBig(e)}`); + }); + } + } // User did not provide a Uid, find it automatically + else { + const matches = activeLFGPosts.filter((lfg) => (message.authorId === lfg.ownerId && message.channelId === lfg.channelId)); + + // Found one, edit + if (matches.length === 1) { + const lfgMessage = await (await getMessage(matches[0].channelId, matches[0].messageId)).edit({ + content: `Editing new LFG post for <@${matches[0].ownerId}>. Please reply with the requested information and watch as your LFG post gets edited!`, + }); + const question = await message.send({ + content: 'Please select an item to edit from the buttons below:', + components: [{ + type: 1, + components: editBtns, + }], + }); + + activeBuilders.push({ + userId: matches[0].ownerId, + channelId: matches[0].channelId, + step: 'edit_btn', + lfgMsg: lfgMessage, + questionMsg: question, + lastTouch: new Date(), + maxIdle: 60, + editing: true, + }); + + message.delete().catch((e) => { + log(LT.WARN, `Failed to delete message | ${jsonStringifyBig(e)}`); + }); + } // Found multiple, notify user + else if (matches.length) { + const deleteMsg = constantCmds.lfgEdit2; + const deepCloningFailedSoThisIsTheSolution = constantCmds.lfgEdit2.embeds[0].fields[0].value; + matches.forEach((mt) => { + deleteMsg.embeds[0].fields[0].value += `[${mt.lfgUid}](https://discord.com/channels/${message.guildId}/${mt.channelId}/${mt.messageId})\n`; + }); + + deleteMsg.embeds[0].fields[0].value += '\nThis message will self descruct in 30 seconds.'; + + const m = await message.send(deleteMsg); + constantCmds.lfgEdit2.embeds[0].fields[0].value = deepCloningFailedSoThisIsTheSolution; + + m.delete('Channel Cleanup', 30000).catch((e) => { + log(LT.WARN, `Failed to delete message | ${jsonStringifyBig(e)}`); + }); + message.delete('Channel Cleanup', 30000).catch((e) => { + log(LT.WARN, `Failed to delete message | ${jsonStringifyBig(e)}`); + }); + } // Found none, notify user you cannot edit other's lfgs + else { + const m = await message.send(constantCmds.lfgEdit1); + + m.delete('Channel Cleanup', 5000).catch((e) => { + log(LT.WARN, `Failed to delete message | ${jsonStringifyBig(e)}`); + }); + message.delete('Channel Cleanup', 5000).catch((e) => { + log(LT.WARN, `Failed to delete message | ${jsonStringifyBig(e)}`); + }); + } + } + } catch (e) { + log(LT.WARN, `LFG failed at step | edit | ${jsonStringifyBig(e)}`); + } + } // Join a LFG on behalf of a user + // gu!lfg join [url] [join/leave/alternate] [member?] + else if (subcmd === 'join' || subcmd === 'leave' || subcmd === 'alternate') { + try { + const action = subcmd; + const lfgIds = urlToIds(args[1] || ''); + const memberStr = args[2] || `<@${message.authorId}>`; + const member = await message.guild?.members.get(BigInt(memberStr.substr(3, memberStr.length - 4))); + + const modRole = guildModRoles.get(message.guildId) || 0n; + + // Join yourself (or others if you are a guild mod) to an LFG + if (lfgIds.guildId === message.guildId && member && (member.id === message.authorId || message.guildMember?.roles.includes(modRole))) { + const lfgMessage = await getMessage(lfgIds.channelId, lfgIds.messageId); + + const embeds = lfgMessage.embeds[0].fields || []; + let results: JoinLeaveType = { + embed: [], + success: false, + full: true, + justFilled: false, + }; + let actionResp: string; + switch (action) { + case 'join': + results = handleMemberJoin(embeds, member, false); + actionResp = 'joined'; + break; + case 'leave': + results = handleMemberLeave(embeds, member); + actionResp = 'left'; + break; + case 'alternate': + results = handleMemberJoin(embeds, member, true); + actionResp = 'joined as alternate'; + break; + } + + let resp: string; + if (results.success && lfgMessage.components) { + const buttonRow: ActionRow = lfgMessage.components[0] as ActionRow; + + await lfgMessage.edit({ + embeds: [{ + fields: results.embed, + footer: lfgMessage.embeds[0].footer, + timestamp: lfgMessage.embeds[0].timestamp, + }], + components: [buttonRow], + }); + + if (results.justFilled) { + const thisLFGPost = activeLFGPosts.filter((lfg) => (lfgMessage.id === lfg.messageId && lfgMessage.channelId === lfg.channelId))[0]; + const thisLFG = (await getMessage(thisLFGPost.channelId, thisLFGPost.messageId)).embeds[0].fields || []; + sendDirectMessage(thisLFGPost.ownerId, { + embeds: [{ + title: `Hello ${(await getUser(thisLFGPost.ownerId)).username}! Your event in ${ + lfgMessage.guild?.name || (await getGuild(message.guildId, { counts: false, addToCache: false })).name + } has filled up!`, + fields: [ + thisLFG[0], + { + name: 'Your members are:', + value: thisLFG[4].value, + }, + ], + }], + }); + } + + resp = `Successfully ${actionResp} LFG.`; + } else { + resp = `Failed to ${action} LFG.`; + } + + const m = await message.send({ + embeds: [{ + title: resp, + }], + }); + + m.delete('Channel Cleanup', 5000).catch((e) => { + log(LT.WARN, `Failed to delete message | ${jsonStringifyBig(e)}`); + }); + message.delete('Channel Cleanup', 5000).catch((e) => { + log(LT.WARN, `Failed to delete message | ${jsonStringifyBig(e)}`); + }); + } + } catch (e) { + log(LT.WARN, `Member Join/Leave/Alt command failed: ${jsonStringifyBig(message)} | ${jsonStringifyBig(e)}`); + + const m = await message.send({ + embeds: [{ + title: 'Failed to find LFG.', + }], + }); + + m.delete('Channel Cleanup').catch((e) => { + log(LT.WARN, `Failed to clean up joiner | joining on behalf | ${jsonStringifyBig(e)}`); + }); + message.delete('Channel Cleanup', 5000).catch((e) => { + log(LT.WARN, `Failed to clean up joiner | joining on behalf | ${jsonStringifyBig(e)}`); + }); + } + } // Sets the mod role + else if (subcmd === 'set_mod_role' && (await hasGuildPermissions(message.guildId, message.authorId, ['ADMINISTRATOR']))) { + const mentionedRole = args[1] || ''; + const roleId = BigInt(mentionedRole.substr(3, mentionedRole.length - 4)); + if (message.guild?.roles.has(roleId)) { + let success = true; + if (guildModRoles.has(message.guildId)) { + // Execute the DB update + await dbClient.execute('UPDATE guild_mod_role SET roleId = ? WHERE guildId = ?', [roleId, message.guildId]).catch((e) => { + log(LT.ERROR, `Failed to insert into database: ${jsonStringifyBig(e)}`); + success = false; + }); + } else { + // Execute the DB insertion + await dbClient.execute('INSERT INTO guild_mod_role(guildId,roleId) values(?,?)', [message.guildId, roleId]).catch((e) => { + log(LT.ERROR, `Failed to insert into database: ${jsonStringifyBig(e)}`); + success = false; + }); + } + + if (success) { + guildModRoles.set(message.guildId, roleId); + message.send({ + embeds: [{ + fields: [ + { + name: 'LFG Mod Role set successfully', + value: `LFG Mod Role set to ${args[1]}.`, + }, + ], + }], + }).catch((e) => { + log(LT.WARN, `Failed to send message | ${jsonStringifyBig(e)}`); + }); + } else { + message.send({ + embeds: [{ + fields: [ + { + name: 'Something went wrong!', + value: 'LFG Mod Role has been left unchanged.', + }, + ], + }], + }).catch((e) => { + log(LT.WARN, `Failed to send message | ${jsonStringifyBig(e)}`); + }); + } + } else { + if (guildModRoles.has(message.guildId)) { + message.send({ + embeds: [{ + fields: [ + { + name: 'LFG Mod Role is currently set to:', + value: `<@&${guildModRoles.get(message.guildId)}>`, + }, + ], + }], + }).catch((e) => { + log(LT.WARN, `Failed to send message | ${jsonStringifyBig(e)}`); + }); + } else { + message.send({ + embeds: [{ + fields: [ + { + name: 'There is no LFG Mod Role set for this guild.', + value: `To set one, run this command again with the role mentioned.\n\nExample: \`${prefix}lfg set_mod_role @newModRole\``, + }, + ], + }], + }).catch((e) => { + log(LT.WARN, `Failed to send message | ${jsonStringifyBig(e)}`); + }); + } + } + } // Sets the channel cleaning up for LFG channels to keep LFG events visible and prevent conversations + else if (subcmd === 'set_clean_channel' && (await hasGuildPermissions(message.guildId, message.authorId, ['ADMINISTRATOR']))) { + const cleanSetting = (args[1] || 'list').toLowerCase(); + let success = true; + if (cleanSetting === 'on') { + // Execute the DB insertion + await dbClient.execute('INSERT INTO guild_clean_channel(guildId,channelId) values(?,?)', [message.guildId, message.channelId]).catch((e) => { + log(LT.ERROR, `Failed to insert into database: ${jsonStringifyBig(e)}`); + success = false; + }); + + if (success) { + const tempArr = cleanChannels.get(message.guildId) || []; + tempArr.push(message.channelId); + cleanChannels.set(message.guildId, tempArr); + + const m = await message.send({ + embeds: [{ + fields: [ + { + name: 'Channel Cleaning turned ON.', + value: 'This message will self destruct in 5 seconds.', + }, + ], + }], + }).catch((e) => { + log(LT.WARN, `Failed to send message | ${jsonStringifyBig(e)}`); + }); + + m && m.delete('Channel Cleanup', 5000).catch((e) => { + log(LT.WARN, `Failed to clean up | set_clean_channel | ${jsonStringifyBig(e)}`); + }); + message.delete('Channel Cleanup', 5000).catch((e) => { + log(LT.WARN, `Failed to clean up | set_clean_channel | ${jsonStringifyBig(e)}`); + }); + } else { + message.send({ + embeds: [{ + fields: [ + { + name: 'Something went wrong!', + value: 'Channel Clean status left off.', + }, + ], + }], + }).catch((e) => { + log(LT.WARN, `Failed to send message | ${jsonStringifyBig(e)}`); + }); + } + } else if (cleanSetting === 'off') { + // turns clean off for channel + // Execute the DB insertion + await dbClient.execute('DELETE FROM guild_clean_channel WHERE guildId = ? AND channelId = ?', [message.guildId, message.channelId]).catch((e) => { + log(LT.ERROR, `Failed to delete from database: ${jsonStringifyBig(e)}`); + success = false; + }); + + if (success) { + let tempArr = cleanChannels.get(message.guildId) || []; + tempArr = tempArr.filter((channelId) => channelId !== message.channelId); + cleanChannels.set(message.guildId, tempArr); + + const m = await message.send({ + embeds: [{ + fields: [ + { + name: 'Channel Cleaning turned OFF.', + value: 'This message will self destruct in 5 seconds.', + }, + ], + }], + }).catch((e) => { + log(LT.WARN, `Failed to send message | ${jsonStringifyBig(e)}`); + }); + + m && m.delete('Channel Cleanup', 5000).catch((e) => { + log(LT.WARN, `Failed to clean up | set_clean_channel | ${jsonStringifyBig(e)}`); + }); + message.delete('Channel Cleanup', 5000).catch((e) => { + log(LT.WARN, `Failed to clean up | set_clean_channel | ${jsonStringifyBig(e)}`); + }); + } else { + message.send({ + embeds: [{ + fields: [ + { + name: 'Something went wrong!', + value: 'Channel Clean status left on.', + }, + ], + }], + }).catch((e) => { + log(LT.WARN, `Failed to send message | ${jsonStringifyBig(e)}`); + }); + } + } else if (cleanSetting === 'list') { + // send list of channels with clean on + let cleanChannelStr = ''; + + for (const channelId of cleanChannels.get(message.guildId) || []) { + cleanChannelStr += `<#${channelId}>\n`; + } + cleanChannelStr = cleanChannelStr.substr(0, cleanChannelStr.length - 1); + + const tmpEmbed: Embed = {}; + + if (cleanChannelStr) { + tmpEmbed.fields = [ + { + name: 'Clean Channels enabled for this guild:', + value: cleanChannelStr, + }, + ]; + } else { + tmpEmbed.title = 'No Clean Channels are enabled for this guild.'; + } + + await message.send({ + embeds: [tmpEmbed], + }).catch((e) => { + log(LT.WARN, `Failed to send message | ${jsonStringifyBig(e)}`); + }); + } + } + } // report or r (command that failed) + // Manually report something that screwed up + else if (command === 'report' || command === 'r') { + // Light telemetry to see how many times a command is being run + dbClient.execute(`CALL INC_CNT("report");`).catch((e) => { + log(LT.ERROR, `Failed to call stored procedure INC_CNT: ${jsonStringifyBig(e)}`); + }); + + sendMessage(config.reportChannel, 'USER REPORT:\n' + args.join(' ')).catch((e) => { + log(LT.ERROR, `Failed to send message: ${jsonStringifyBig(message)} | ${jsonStringifyBig(e)}`); + }); + message.send(constantCmds.report).catch((e) => { + log(LT.ERROR, `Failed to send message: ${jsonStringifyBig(message)} | ${jsonStringifyBig(e)}`); + }); + } // version or v + // Returns version of the bot + else if (command === 'version' || command === 'v') { + // Light telemetry to see how many times a command is being run + dbClient.execute(`CALL INC_CNT("version");`).catch((e) => { + log(LT.ERROR, `Failed to call stored procedure INC_CNT: ${jsonStringifyBig(e)}`); + }); + + message.send(constantCmds.version).catch((e) => { + log(LT.ERROR, `Failed to send message: ${jsonStringifyBig(message)} | ${jsonStringifyBig(e)}`); + }); + } // info or i + // Info command, prints short desc on bot and some links + else if (command === 'info' || command === 'i') { + // Light telemetry to see how many times a command is being run + dbClient.execute(`CALL INC_CNT("info");`).catch((e) => { + log(LT.ERROR, `Failed to call stored procedure INC_CNT: ${jsonStringifyBig(e)}`); + }); + + message.send(constantCmds.info).catch((e) => { + log(LT.ERROR, `Failed to send message: ${jsonStringifyBig(message)} | ${jsonStringifyBig(e)}`); + }); + } // help or h or ? + // Help command, prints available commands + else if (command === 'help' || command === 'h' || command === '?') { + // Light telemetry to see how many times a command is being run + dbClient.execute(`CALL INC_CNT("help");`).catch((e) => { + log(LT.ERROR, `Failed to call stored procedure INC_CNT: ${jsonStringifyBig(e)}`); + }); + + message.send(constantCmds.help).catch((e) => { + log(LT.ERROR, `Failed to send message: ${jsonStringifyBig(message)} | ${jsonStringifyBig(e)}`); + }); + } + }, + interactionCreate: async (interact, member) => { + try { + if (interact.type === DiscordInteractionTypes.MessageComponent) { + if (interact.message && interact.data && (interact.data as ButtonData).customId && member) { + log(LT.INFO, `Handling Button ${(interact.data as ButtonData).customId}`); + log(LT.LOG, `Button Data | ${jsonStringifyBig(interact)}`); + + sendInteractionResponse(BigInt(interact.id), interact.token, { + type: DiscordInteractionResponseTypes.DeferredUpdateMessage, + }); + + const [handler, stepInfo] = (interact.data as ButtonData).customId.split('@'); + const [action, value] = stepInfo.split('#'); + switch (handler) { + case 'building': { + await activeBuilders.some(async (x, i) => { + if (x.channelId === BigInt(interact.channelId || '0') && member && x.userId === BigInt(member.id)) { + x.lastTouch = new Date(); + x = await handleLFGStep(x, value); + + if (x.step === 'done' && x.lfgMsg.components) { + const currentLFG = (x.lfgMsg.embeds[0].fields || []); + const newTimestamp = new Date(parseInt(currentLFG[1].value.split('#')[1])); + const newLfgUid = ALPHABET[Math.floor(Math.random() * 26)] + ALPHABET[Math.floor(Math.random() * 26)]; + + const tempMembers = currentLFG[4].name.split(':')[1].split('/'); + const currentMembers = parseInt(tempMembers[0]); + const maxMembers = parseInt(tempMembers[1]); + + const buttonRow: ActionRow = x.lfgMsg.components[0] as ActionRow; + + if (currentMembers > maxMembers) { + const currentPeople = currentLFG[4].value.split('\n'); + const newAlts = currentPeople.splice(maxMembers - 1); + currentLFG[4].value = currentPeople.join('\n'); + currentLFG[5].value = `${newAlts.join('\n')}\n${currentLFG[5].value}`; + currentLFG[4].name = `Members Joined: ${maxMembers}/${maxMembers}`; + } + + await x.lfgMsg.edit({ + content: '', + embeds: [{ + fields: currentLFG, + footer: { + text: `Created by: ${member.username} | ${newLfgUid}`, + }, + timestamp: newTimestamp.toISOString(), + }], + components: [buttonRow], + }); + + const activeIdx = activeLFGPosts.findIndex((lfg) => (lfg.channelId === x.channelId && lfg.messageId === x.lfgMsg.id && lfg.ownerId === x.userId)); + activeLFGPosts[activeIdx].lfgTime = newTimestamp.getTime(); + activeLFGPosts[activeIdx].lfgUid = newLfgUid; + localStorage.setItem('activeLFGPosts', jsonStringifyBig(activeLFGPosts)); + + await activeBuilders[i].questionMsg.delete().catch((e) => { + log(LT.WARN, `Failed to delete message | ${jsonStringifyBig(e)}`); + }); + activeBuilders.splice(i, 1); + } else { + activeBuilders[i] = x; + } + + return true; + } + }); + break; + } + case 'active': { + const message = await getMessage(BigInt(interact.channelId || '0'), BigInt(interact.message.id)); + + const embeds = message.embeds[0].fields || []; + let results: JoinLeaveType = { + embed: [], + success: false, + full: true, + justFilled: false, + }; + switch (action) { + case 'join_group': + results = handleMemberJoin(embeds, member, false); + break; + case 'leave_group': + results = handleMemberLeave(embeds, member); + break; + case 'alternate_group': + results = handleMemberJoin(embeds, member, true); + break; + } + + if (results.success && message.components) { + await message.edit({ + embeds: [{ + fields: results.embed, + footer: message.embeds[0].footer, + timestamp: message.embeds[0].timestamp, + }], + }); + + if (results.justFilled) { + const thisLFGPost = activeLFGPosts.filter((lfg) => (message.id === lfg.messageId && message.channelId === lfg.channelId))[0]; + const thisLFG = (await getMessage(thisLFGPost.channelId, thisLFGPost.messageId)).embeds[0].fields || []; + sendDirectMessage(thisLFGPost.ownerId, { + embeds: [{ + title: `Hello ${(await getUser(thisLFGPost.ownerId)).username}! Your event in ${ + message.guild?.name || (await getGuild(message.guildId, { counts: false, addToCache: false })).name + } has filled up!`, + fields: [ + thisLFG[0], + { + name: 'Your members are:', + value: thisLFG[4].value, + }, + ], + }], + }); + } + } + + break; + } + case 'editing': { + await activeBuilders.some(async (x, i) => { + if (x.editing && x.channelId === BigInt(interact.channelId || '0') && member && x.userId === BigInt(member.id)) { + x.step = action; + x.lastTouch = new Date(); + let nextQuestion = ''; + const nextComponents: Array = []; + switch (action) { + case 'set_game': { + nextQuestion = lfgStepQuestions.set_game; + + const gameButtons: Array = Object.keys(LFGActivities).map((game) => { + return { + type: 2, + label: game, + customId: `building@set_game#${game}`, + style: DiscordButtonStyles.Primary, + }; + }); + + const temp: Array = []; + + gameButtons.forEach((btn, idx) => { + if (!temp[Math.floor(idx / 5)]) { + temp[Math.floor(idx / 5)] = [btn]; + } else { + temp[Math.floor(idx / 5)].push(btn); + } + }); + + temp.forEach((btns) => { + if (btns.length && btns.length <= 5) { + nextComponents.push({ + type: 1, + components: btns, + }); + } + }); + break; + } + case 'set_time': { + nextQuestion = 'Please enter the time of the activity:'; + break; + } + case 'set_desc': { + nextQuestion = 'Please enter a description for the activity. Enter `none` to skip:'; + break; + } + default: + break; + } + + x.questionMsg = await x.questionMsg.edit({ + content: nextQuestion, + components: nextComponents, + }); + + activeBuilders[i] = x; + + return true; + } + }); + break; + } + default: + break; + } + } + } + } catch (e) { + log(LT.ERROR, `Interaction failed: ${jsonStringifyBig(interact)} | ${jsonStringifyBig(member)} | ${jsonStringifyBig(e)}`); + } + }, + }, +}); diff --git a/src/constantCmds.ts b/_OLD/src/constantCmds.ts similarity index 100% rename from src/constantCmds.ts rename to _OLD/src/constantCmds.ts diff --git a/src/games.ts b/_OLD/src/games.ts similarity index 100% rename from src/games.ts rename to _OLD/src/games.ts diff --git a/src/intervals.ts b/_OLD/src/intervals.ts similarity index 100% rename from src/intervals.ts rename to _OLD/src/intervals.ts diff --git a/src/lfgHandlers.d.ts b/_OLD/src/lfgHandlers.d.ts similarity index 100% rename from src/lfgHandlers.d.ts rename to _OLD/src/lfgHandlers.d.ts diff --git a/src/lfgHandlers.ts b/_OLD/src/lfgHandlers.ts similarity index 100% rename from src/lfgHandlers.ts rename to _OLD/src/lfgHandlers.ts diff --git a/src/mod.d.ts b/_OLD/src/mod.d.ts similarity index 100% rename from src/mod.d.ts rename to _OLD/src/mod.d.ts diff --git a/src/timeUtils.ts b/_OLD/src/timeUtils.ts similarity index 100% rename from src/timeUtils.ts rename to _OLD/src/timeUtils.ts diff --git a/src/utils.ts b/_OLD/src/utils.ts similarity index 100% rename from src/utils.ts rename to _OLD/src/utils.ts diff --git a/deps.ts b/deps.ts index 65f3887..0e16319 100644 --- a/deps.ts +++ b/deps.ts @@ -20,7 +20,7 @@ export { sendMessage, startBot, structures, -} from 'https://deno.land/x/discordeno@12.0.1/mod.ts'; +} from 'https://deno.land/x/discordeno@17.0.1/mod.ts'; export type { ActionRow, @@ -34,8 +34,8 @@ export type { Embed, EmbedField, Interaction, -} from 'https://deno.land/x/discordeno@12.0.1/mod.ts'; +} from 'https://deno.land/x/discordeno@17.0.1/mod.ts'; -export { Client } from 'https://deno.land/x/mysql@v2.10.1/mod.ts'; +export { Client } from 'https://deno.land/x/mysql@v2.11.0/mod.ts'; export { initLog, log, LogTypes as LT } from 'https://raw.githubusercontent.com/Burn-E99/Log4Deno/V1.1.0/mod.ts'; diff --git a/flags.ts b/flags.ts index 939f73a..450b49a 100644 --- a/flags.ts +++ b/flags.ts @@ -2,5 +2,5 @@ export const DEVMODE = false; // DEBUG is used to toggle the cmdPrompt export const DEBUG = false; -// LOCALMODE is used to run a differnt bot token for local testing -export const LOCALMODE = false; +// LOCALMODE is used to run a different bot token for local testing +export const LOCALMODE = true; diff --git a/mod.ts b/mod.ts index ed6fa54..e69de29 100644 --- a/mod.ts +++ b/mod.ts @@ -1,1246 +0,0 @@ -import { - ActionRow, - botId, - ButtonComponent, - ButtonData, - cache, - cacheHandlers, - // MySQL Driver deps - Client, - DebugArg, - deleteMessage, - DiscordActivityTypes, - DiscordButtonStyles, - DiscordenoGuild, - DiscordenoMessage, - DiscordInteractionResponseTypes, - DiscordInteractionTypes, - editBotNickname, - editBotStatus, - Embed, - getGuild, - getMessage, - getUser, - hasGuildPermissions, - initLog, - Intents, - log, - // Log4Deno deps - LT, - sendDirectMessage, - sendInteractionResponse, - sendMessage, - // Discordeno deps - startBot, -} from './deps.ts'; - -import { ActiveLFG, BuildingLFG, GuildCleanChannels, GuildModRoles, GuildPrefixes } from './src/mod.d.ts'; -import intervals from './src/intervals.ts'; -import { LFGActivities } from './src/games.ts'; -import { JoinLeaveType } from './src/lfgHandlers.d.ts'; -import { handleLFGStep, handleMemberJoin, handleMemberLeave, urlToIds } from './src/lfgHandlers.ts'; -import { constantCmds, editBtns, lfgStepQuestions } from './src/constantCmds.ts'; -import { jsonParseBig, jsonStringifyBig } from './src/utils.ts'; - -import { DEBUG, LOCALMODE } from './flags.ts'; -import config from './config.ts'; - -// Initialize DB client -const dbClient = await new Client().connect({ - hostname: LOCALMODE ? config.db.localhost : config.db.host, - port: config.db.port, - db: config.db.name, - username: config.db.username, - password: config.db.password, -}); - -// Initialize logging client with folder to use for logs, needs --allow-write set on Deno startup -initLog('logs', DEBUG); -log(LT.INFO, `${config.name} Starting up . . .`); - -// Handle idling out the active builders -const activeBuilders: Array = []; -setInterval(() => { - intervals.buildingTimeout(activeBuilders); -}, 1000); - -const activeLFGPosts: Array = jsonParseBig(localStorage.getItem('activeLFGPosts') || '[]'); -log(LT.INFO, `Loaded ${activeLFGPosts.length} activeLFGPosts`); -setInterval(() => { - intervals.lfgNotifier(activeLFGPosts); -}, 60000); - -const guildPrefixes: Map = new Map(); -const getGuildPrefixes = await dbClient.query('SELECT * FROM guild_prefix'); -getGuildPrefixes.forEach((g: GuildPrefixes) => { - guildPrefixes.set(g.guildId, g.prefix); -}); - -const guildModRoles: Map = new Map(); -const getGuildModRoles = await dbClient.query('SELECT * FROM guild_mod_role'); -getGuildModRoles.forEach((g: GuildModRoles) => { - guildModRoles.set(g.guildId, g.roleId); -}); - -const cleanChannels: Map> = new Map(); -const getCleanChannels = await dbClient.query('SELECT * FROM guild_clean_channel'); -getCleanChannels.forEach((g: GuildCleanChannels) => { - const tempArr = cleanChannels.get(g.guildId) || []; - tempArr.push(g.channelId); - cleanChannels.set(g.guildId, tempArr); -}); - -const ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; - -// Start up the Discord Bot -startBot({ - token: LOCALMODE ? config.localtoken : config.token, - intents: [Intents.GuildMessages, Intents.DirectMessages, Intents.Guilds], - eventHandlers: { - ready: () => { - log(LT.INFO, `${config.name} Logged in!`); - editBotStatus({ - activities: [{ - name: 'Booting up . . .', - type: DiscordActivityTypes.Game, - createdAt: new Date().getTime(), - }], - status: 'online', - }); - - // Interval to rotate the status text every 30 seconds to show off more commands - setInterval(async () => { - log(LT.LOG, 'Changing bot status'); - try { - const cachedCount = await cacheHandlers.size('guilds'); - // Wrapped in try-catch due to hard crash possible - editBotStatus({ - activities: [{ - name: intervals.getRandomStatus(cachedCount), - type: DiscordActivityTypes.Game, - createdAt: new Date().getTime(), - }], - status: 'online', - }); - } catch (e) { - log(LT.ERROR, `Failed to update status: ${jsonStringifyBig(e)}`); - } - }, 30000); - - // Interval to update bot list stats every 24 hours - LOCALMODE ? log(LT.INFO, 'updateListStatistics not running') : setInterval(() => { - log(LT.LOG, 'Updating all bot lists statistics'); - intervals.updateListStatistics(botId, cache.guilds.size); - }, 86400000); - - // setTimeout added to make sure the startup message does not error out - setTimeout(() => { - LOCALMODE && editBotNickname(config.devServer, `LOCAL - ${config.name}`); - LOCALMODE ? log(LT.INFO, 'updateListStatistics not running') : intervals.updateListStatistics(botId, cache.guilds.size); - editBotStatus({ - activities: [{ - name: 'Booting Complete', - type: DiscordActivityTypes.Game, - createdAt: new Date().getTime(), - }], - status: 'online', - }); - sendMessage(config.logChannel, `${config.name} has started, running version ${config.version}.`).catch((e) => { - log(LT.ERROR, `Failed to send message: ${jsonStringifyBig(e)}`); - }); - }, 1000); - }, - guildCreate: (guild: DiscordenoGuild) => { - log(LT.LOG, `Handling joining guild ${jsonStringifyBig(guild)}`); - sendMessage(config.logChannel, `New guild joined: ${guild.name} (id: ${guild.id}). This guild has ${guild.memberCount} members!`).catch((e) => { - log(LT.ERROR, `Failed to send message: ${jsonStringifyBig(e)}`); - }); - }, - guildDelete: async (guild: DiscordenoGuild) => { - log(LT.LOG, `Handling leaving guild ${jsonStringifyBig(guild)}`); - sendMessage(config.logChannel, `I have been removed from: ${guild.name} (id: ${guild.id}).`).catch((e) => { - log(LT.ERROR, `Failed to send message: ${jsonStringifyBig(e)}`); - }); - - try { - await dbClient.execute('DELETE FROM guild_prefix WHERE guildId = ?', [guild.id]); - await dbClient.execute('DELETE FROM guild_mod_role WHERE guildId = ?', [guild.id]); - await dbClient.execute('DELETE FROM guild_clean_channel WHERE guildId = ?', [guild.id]); - } catch (e) { - log(LT.WARN, `Failed to remove guild from DB: ${jsonStringifyBig(e)}`); - } - }, - debug: (dmsg: string | DebugArg, data?: string) => log(LT.LOG, `Debug Message | ${jsonStringifyBig(dmsg)} | ${jsonStringifyBig(data)}`, false), - messageCreate: async (message: DiscordenoMessage) => { - // Ignore all other bots - if (message.isBot) return; - - const prefix = guildPrefixes.get(message.guildId) || config.prefix; - - // Handle messages not starting with the prefix - if (message.content.indexOf(prefix) !== 0) { - // Mentions - if (message.mentionedUserIds[0] === botId && (message.content.trim().startsWith(`<@${botId}>`) || message.content.trim().startsWith(`<@!${botId}>`))) { - // Light telemetry to see how many times a command is being run - await dbClient.execute(`CALL INC_CNT("prefix");`).catch((e) => { - log(LT.ERROR, `Failed to call stored procedure INC_CNT: ${jsonStringifyBig(e)}`); - }); - - if (message.content.trim() === `<@${botId}>` || message.content.trim() === `<@!${botId}>`) { - message.send({ - embeds: [{ - title: `Hello ${message.member?.username}, and thanks for using Group Up!`, - fields: [ - { - name: `My prefix in this guild is: \`${prefix}\``, - value: 'Mention me with a new prefix to change it.', - }, - ], - }], - }).catch((e) => { - log(LT.WARN, `Failed to send message | ${jsonStringifyBig(e)}`); - }); - } else if (await hasGuildPermissions(message.guildId, message.authorId, ['ADMINISTRATOR'])) { - const newPrefix = message.content.replace(`<@!${botId}>`, '').replace(`<@${botId}>`, '').trim(); - - if (newPrefix.length <= 10) { - let success = true; - if (guildPrefixes.has(message.guildId)) { - // Execute the DB update - await dbClient.execute('UPDATE guild_prefix SET prefix = ? WHERE guildId = ?', [newPrefix, message.guildId]).catch((e) => { - log(LT.ERROR, `Failed to insert into database: ${jsonStringifyBig(e)}`); - success = false; - }); - } else { - // Execute the DB insertion - await dbClient.execute('INSERT INTO guild_prefix(guildId,prefix) values(?,?)', [message.guildId, newPrefix]).catch((e) => { - log(LT.ERROR, `Failed to insert into database: ${jsonStringifyBig(e)}`); - success = false; - }); - } - - if (success) { - guildPrefixes.set(message.guildId, newPrefix); - message.send({ - embeds: [{ - fields: [ - { - name: `My prefix in this guild is now: \`${newPrefix}\``, - value: 'Mention me with a new prefix to change it.', - }, - ], - }], - }).catch((e) => { - log(LT.WARN, `Failed to send message | ${jsonStringifyBig(e)}`); - }); - } else { - message.send({ - embeds: [{ - fields: [ - { - name: 'Something went wrong!', - value: `My prefix is still \`${prefix}\`. Please try again, and if the problem persists, please report this to the developers using \`${prefix}report\`.`, - }, - ], - }], - }).catch((e) => { - log(LT.WARN, `Failed to send message | ${jsonStringifyBig(e)}`); - }); - } - } else { - message.send({ - embeds: [{ - fields: [ - { - name: 'Prefix too long, please set a prefix less than 10 characters long.', - value: 'Mention me with a new prefix to change it.', - }, - ], - }], - }).catch((e) => { - log(LT.WARN, `Failed to send message | ${jsonStringifyBig(e)}`); - }); - } - } - return; - } - - // Other - const activeIdx = activeBuilders.findIndex((x) => (message.channelId === x.channelId && message.authorId === x.userId)); - if (activeIdx > -1) { - activeBuilders[activeIdx].lastTouch = new Date(); - activeBuilders[activeIdx] = await handleLFGStep(activeBuilders[activeIdx], message.content); - - if (activeBuilders[activeIdx].step === 'done') { - if (message.member) { - const memberJoined = handleMemberJoin(activeBuilders[activeIdx].lfgMsg.embeds[0].fields || [], message.member, false); - - const newTimestamp = new Date(parseInt(memberJoined.embed[1].value.split('#')[1])); - const newLfgUid = ALPHABET[Math.floor(Math.random() * 26)] + ALPHABET[Math.floor(Math.random() * 26)]; - - const tempMembers = memberJoined.embed[4].name.split(':')[1].split('/'); - const currentMembers = parseInt(tempMembers[0]); - const maxMembers = parseInt(tempMembers[1]); - - if (activeBuilders[activeIdx].editing) { - if (currentMembers > maxMembers) { - const currentPeople = memberJoined.embed[4].value.split('\n'); - const newAlts = currentPeople.splice(maxMembers); - memberJoined.embed[4].value = currentPeople.join('\n') || 'None'; - memberJoined.embed[5].value = `${newAlts.join('\n')}\n${memberJoined.embed[5].value === 'None' ? '' : memberJoined.embed[5].value}`; - memberJoined.embed[4].name = `Members Joined: ${maxMembers}/${maxMembers}`; - } - } - - await activeBuilders[activeIdx].lfgMsg.edit({ - content: '', - embeds: [{ - fields: memberJoined.embed, - footer: { - text: `Created by: ${message.member.username} | ${newLfgUid}`, - }, - timestamp: newTimestamp.toISOString(), - }], - components: [ - { - type: 1, - components: [ - { - type: 2, - label: 'Join', - customId: 'active@join_group', - style: DiscordButtonStyles.Success, - }, - { - type: 2, - label: 'Leave', - customId: 'active@leave_group', - style: DiscordButtonStyles.Danger, - }, - { - type: 2, - label: 'Join as Alternate', - customId: 'active@alternate_group', - style: DiscordButtonStyles.Primary, - }, - ], - }, - ], - }).catch((e) => { - log(LT.WARN, `Failed to edit message | ${jsonStringifyBig(e)}`); - }); - - if (activeBuilders[activeIdx]) { - const activeLFGIdx = activeLFGPosts.findIndex( - (lfg) => (lfg.channelId === activeBuilders[activeIdx].channelId && lfg.messageId === activeBuilders[activeIdx].lfgMsg.id && lfg.ownerId === activeBuilders[activeIdx].userId), - ); - if (activeLFGIdx >= 0) { - activeLFGPosts[activeLFGIdx].lfgUid = newLfgUid; - activeLFGPosts[activeLFGIdx].lfgTime = newTimestamp.getTime(); - activeLFGPosts[activeLFGIdx].notified = false; - activeLFGPosts[activeLFGIdx].locked = false; - } else { - activeLFGPosts.push({ - messageId: activeBuilders[activeIdx].lfgMsg.id, - channelId: activeBuilders[activeIdx].lfgMsg.channelId, - ownerId: message.authorId, - lfgUid: newLfgUid, - lfgTime: newTimestamp.getTime(), - notified: false, - locked: false, - }); - } - localStorage.setItem('activeLFGPosts', jsonStringifyBig(activeLFGPosts)); - } - } - - await activeBuilders[activeIdx].questionMsg.delete().catch((e) => { - log(LT.WARN, `Failed to delete message | ${jsonStringifyBig(e)}`); - }); - activeBuilders.splice(activeIdx, 1); - } - await message.delete().catch((e) => { - log(LT.WARN, `Failed to delete message | ${jsonStringifyBig(e)}`); - }); - return; - } - - // Should this get cleaned up? - const enabledCleanChannels = cleanChannels.get(message.guildId); - if (enabledCleanChannels && enabledCleanChannels.length && enabledCleanChannels.indexOf(message.channelId) > -1) { - message.delete('Cleaning Channel').catch((e) => { - log(LT.WARN, `Failed to delete message | ${jsonStringifyBig(e)}`); - }); - return; - } - return; - } else { - // User is sending a command, make sure its a lfg command if its being sent in a clean channel - const enabledCleanChannels = cleanChannels.get(message.guildId); - if (enabledCleanChannels && enabledCleanChannels.length && enabledCleanChannels.indexOf(message.channelId) > -1 && message.content.indexOf(`${prefix}lfg`) !== 0) { - message.delete('Cleaning Channel').catch((e) => { - log(LT.WARN, `Failed to delete message | ${jsonStringifyBig(e)}`); - }); - return; - } - } - - log(LT.LOG, `Handling message ${jsonStringifyBig(message)}`); - - // Split into standard command + args format - const args = message.content.slice(prefix.length).trim().split(/[ \n]+/g); - const command = args.shift()?.toLowerCase(); - - // All commands below here - - // ping - // Its a ping test, what else do you want. - if (command === 'ping') { - // Light telemetry to see how many times a command is being run - dbClient.execute(`CALL INC_CNT("ping");`).catch((e) => { - log(LT.ERROR, `Failed to call stored procedure INC_CNT: ${jsonStringifyBig(e)}`); - }); - - // Calculates ping between sending a message and editing it, giving a nice round-trip latency. - try { - const m = await message.send({ - embeds: [{ - title: 'Ping?', - }], - }); - m.edit({ - embeds: [{ - title: `Pong! Latency is ${m.timestamp - message.timestamp}ms.`, - }], - }); - } catch (e) { - log(LT.ERROR, `Failed to send message: ${jsonStringifyBig(message)} | ${jsonStringifyBig(e)}`); - } - } // lfg - // Handles all LFG commands, creating, editing, deleting - else if (command === 'lfg') { - // Light telemetry to see how many times a command is being run - dbClient.execute(`CALL INC_CNT("lfg");`).catch((e) => { - log(LT.ERROR, `Failed to call stored procedure INC_CNT: ${jsonStringifyBig(e)}`); - }); - - const subcmd = (args[0] || 'help').toLowerCase(); - const lfgUid = (args[1] || '').toUpperCase(); - - // Learn how the LFG command works - if (subcmd === 'help' || subcmd === 'h' || subcmd === '?') { - message.send(constantCmds.lfgHelp).catch((e) => { - log(LT.ERROR, `Failed to send message: ${jsonStringifyBig(message)} | ${jsonStringifyBig(e)}`); - }); - } // Create a new LFG - else if (subcmd === 'create' || subcmd === 'c') { - try { - const lfgMsg = await message.send(`Creating new LFG post for <@${message.authorId}>. Please reply with the requested information and watch as your LFG post gets created!`); - - const gameButtons: Array = Object.keys(LFGActivities).map((game) => { - return { - type: 2, - label: game, - customId: `building@set_game#${game}`, - style: DiscordButtonStyles.Primary, - }; - }); - - const buttonComps: Array = []; - - const temp: Array = []; - - gameButtons.forEach((btn, idx) => { - if (!temp[Math.floor(idx / 5)]) { - temp[Math.floor(idx / 5)] = [btn]; - } else { - temp[Math.floor(idx / 5)].push(btn); - } - }); - - temp.forEach((btns) => { - if (btns.length && btns.length <= 5) { - buttonComps.push({ - type: 1, - components: btns, - }); - } - }); - - const question = await message.send({ - content: lfgStepQuestions.set_game, - components: buttonComps, - }); - - activeBuilders.push({ - userId: message.authorId, - channelId: message.channelId, - step: 'set_game', - lfgMsg: lfgMsg, - questionMsg: question, - lastTouch: new Date(), - maxIdle: 60, - editing: false, - }); - - message.delete().catch((e) => { - log(LT.WARN, `Failed to delete message | ${jsonStringifyBig(e)}`); - }); - } catch (e) { - log(LT.WARN, `LFG failed at step | create | ${jsonStringifyBig(e)}`); - } - } // Delete an existing LFG - else if (subcmd === 'delete' || subcmd === 'd') { - try { - // User provided a Uid, use it - if (lfgUid) { - const matches = activeLFGPosts.filter((lfg) => (message.authorId === lfg.ownerId && message.channelId === lfg.channelId && lfgUid === lfg.lfgUid)); - - // Found one, delete - if (matches.length) { - await deleteMessage(matches[0].channelId, matches[0].messageId, 'User requested LFG to be deleted.').catch((e) => { - log(LT.WARN, `Failed to find message to delete | ${jsonStringifyBig(e)}`); - }); - const lfgIdx = activeLFGPosts.findIndex((lfg) => (message.authorId === lfg.ownerId && message.channelId === lfg.channelId && lfgUid === lfg.lfgUid)); - - activeLFGPosts.splice(lfgIdx, 1); - - localStorage.setItem('activeLFGPosts', jsonStringifyBig(activeLFGPosts)); - - const m = await message.send(constantCmds.lfgDelete3); - - m.delete('Channel Cleanup', 5000).catch((e) => { - log(LT.WARN, `Failed to delete message | ${jsonStringifyBig(e)}`); - }); - message.delete('Channel Cleanup', 5000).catch((e) => { - log(LT.WARN, `Failed to delete message | ${jsonStringifyBig(e)}`); - }); - } // Did not find one - else { - const m = await message.send(constantCmds.lfgDelete1); - - m.delete('Channel Cleanup', 5000).catch((e) => { - log(LT.WARN, `Failed to delete message | ${jsonStringifyBig(e)}`); - }); - message.delete('Channel Cleanup', 5000).catch((e) => { - log(LT.WARN, `Failed to delete message | ${jsonStringifyBig(e)}`); - }); - } - } // User did not provide a Uid, find it automatically - else { - const matches = activeLFGPosts.filter((lfg) => (message.authorId === lfg.ownerId && message.channelId === lfg.channelId)); - - // Found one, delete - if (matches.length === 1) { - await deleteMessage(matches[0].channelId, matches[0].messageId, 'User requested LFG to be deleted.').catch((e) => { - log(LT.WARN, `Failed to find message to delete | ${jsonStringifyBig(e)}`); - }); - const lfgIdx = activeLFGPosts.findIndex((lfg) => (message.authorId === lfg.ownerId && message.channelId === lfg.channelId)); - - activeLFGPosts.splice(lfgIdx, 1); - - localStorage.setItem('activeLFGPosts', jsonStringifyBig(activeLFGPosts)); - - const m = await message.send(constantCmds.lfgDelete3); - - m.delete('Channel Cleanup', 5000).catch((e) => { - log(LT.WARN, `Failed to delete message | ${jsonStringifyBig(e)}`); - }); - message.delete('Channel Cleanup', 5000).catch((e) => { - log(LT.WARN, `Failed to delete message | ${jsonStringifyBig(e)}`); - }); - } // Found multiple, notify user - else if (matches.length) { - const deleteMsg = constantCmds.lfgDelete2; - const deepCloningFailedSoThisIsTheSolution = constantCmds.lfgDelete2.embeds[0].fields[0].value; - matches.forEach((mt) => { - deleteMsg.embeds[0].fields[0].value += `[${mt.lfgUid}](https://discord.com/channels/${message.guildId}/${mt.channelId}/${mt.messageId})\n`; - }); - - deleteMsg.embeds[0].fields[0].value += '\nThis message will self descruct in 30 seconds.'; - - const m = await message.send(deleteMsg); - constantCmds.lfgDelete2.embeds[0].fields[0].value = deepCloningFailedSoThisIsTheSolution; - - m.delete('Channel Cleanup', 30000).catch((e) => { - log(LT.WARN, `Failed to delete message | ${jsonStringifyBig(e)}`); - }); - message.delete('Channel Cleanup', 30000).catch((e) => { - log(LT.WARN, `Failed to delete message | ${jsonStringifyBig(e)}`); - }); - } // Found none, notify user you cannot delete other's lfgs - else { - const m = await message.send(constantCmds.lfgDelete1); - - m.delete('Channel Cleanup', 5000).catch((e) => { - log(LT.WARN, `Failed to delete message | ${jsonStringifyBig(e)}`); - }); - message.delete('Channel Cleanup', 5000).catch((e) => { - log(LT.WARN, `Failed to delete message | ${jsonStringifyBig(e)}`); - }); - } - } - } catch (e) { - log(LT.WARN, `LFG failed at step | delete | ${jsonStringifyBig(e)}`); - } - } // Edit an existing LFG - else if (subcmd === 'edit' || subcmd === 'e') { - try { - // User provided a Uid, use it - if (lfgUid) { - const matches = activeLFGPosts.filter((lfg) => (message.authorId === lfg.ownerId && message.channelId === lfg.channelId && lfgUid === lfg.lfgUid)); - - // Found one, edit - if (matches.length) { - const lfgMessage = await (await getMessage(matches[0].channelId, matches[0].messageId)).edit({ - content: `Editing new LFG post for <@${matches[0].ownerId}>. Please reply with the requested information and watch as your LFG post gets edited!`, - }); - const question = await message.send({ - content: 'Please select an item to edit from the buttons below:', - components: [{ - type: 1, - components: editBtns, - }], - }); - - activeBuilders.push({ - userId: matches[0].ownerId, - channelId: matches[0].channelId, - step: 'edit_btn', - lfgMsg: lfgMessage, - questionMsg: question, - lastTouch: new Date(), - maxIdle: 60, - editing: true, - }); - - message.delete().catch((e) => { - log(LT.WARN, `Failed to delete message | ${jsonStringifyBig(e)}`); - }); - } // Did not find one - else { - const m = await message.send(constantCmds.lfgEdit1); - - m.delete('Channel Cleanup', 5000).catch((e) => { - log(LT.WARN, `Failed to delete message | ${jsonStringifyBig(e)}`); - }); - message.delete('Channel Cleanup', 5000).catch((e) => { - log(LT.WARN, `Failed to delete message | ${jsonStringifyBig(e)}`); - }); - } - } // User did not provide a Uid, find it automatically - else { - const matches = activeLFGPosts.filter((lfg) => (message.authorId === lfg.ownerId && message.channelId === lfg.channelId)); - - // Found one, edit - if (matches.length === 1) { - const lfgMessage = await (await getMessage(matches[0].channelId, matches[0].messageId)).edit({ - content: `Editing new LFG post for <@${matches[0].ownerId}>. Please reply with the requested information and watch as your LFG post gets edited!`, - }); - const question = await message.send({ - content: 'Please select an item to edit from the buttons below:', - components: [{ - type: 1, - components: editBtns, - }], - }); - - activeBuilders.push({ - userId: matches[0].ownerId, - channelId: matches[0].channelId, - step: 'edit_btn', - lfgMsg: lfgMessage, - questionMsg: question, - lastTouch: new Date(), - maxIdle: 60, - editing: true, - }); - - message.delete().catch((e) => { - log(LT.WARN, `Failed to delete message | ${jsonStringifyBig(e)}`); - }); - } // Found multiple, notify user - else if (matches.length) { - const deleteMsg = constantCmds.lfgEdit2; - const deepCloningFailedSoThisIsTheSolution = constantCmds.lfgEdit2.embeds[0].fields[0].value; - matches.forEach((mt) => { - deleteMsg.embeds[0].fields[0].value += `[${mt.lfgUid}](https://discord.com/channels/${message.guildId}/${mt.channelId}/${mt.messageId})\n`; - }); - - deleteMsg.embeds[0].fields[0].value += '\nThis message will self descruct in 30 seconds.'; - - const m = await message.send(deleteMsg); - constantCmds.lfgEdit2.embeds[0].fields[0].value = deepCloningFailedSoThisIsTheSolution; - - m.delete('Channel Cleanup', 30000).catch((e) => { - log(LT.WARN, `Failed to delete message | ${jsonStringifyBig(e)}`); - }); - message.delete('Channel Cleanup', 30000).catch((e) => { - log(LT.WARN, `Failed to delete message | ${jsonStringifyBig(e)}`); - }); - } // Found none, notify user you cannot edit other's lfgs - else { - const m = await message.send(constantCmds.lfgEdit1); - - m.delete('Channel Cleanup', 5000).catch((e) => { - log(LT.WARN, `Failed to delete message | ${jsonStringifyBig(e)}`); - }); - message.delete('Channel Cleanup', 5000).catch((e) => { - log(LT.WARN, `Failed to delete message | ${jsonStringifyBig(e)}`); - }); - } - } - } catch (e) { - log(LT.WARN, `LFG failed at step | edit | ${jsonStringifyBig(e)}`); - } - } // Join a LFG on behalf of a user - // gu!lfg join [url] [join/leave/alternate] [member?] - else if (subcmd === 'join' || subcmd === 'leave' || subcmd === 'alternate') { - try { - const action = subcmd; - const lfgIds = urlToIds(args[1] || ''); - const memberStr = args[2] || `<@${message.authorId}>`; - const member = await message.guild?.members.get(BigInt(memberStr.substr(3, memberStr.length - 4))); - - const modRole = guildModRoles.get(message.guildId) || 0n; - - // Join yourself (or others if you are a guild mod) to an LFG - if (lfgIds.guildId === message.guildId && member && (member.id === message.authorId || message.guildMember?.roles.includes(modRole))) { - const lfgMessage = await getMessage(lfgIds.channelId, lfgIds.messageId); - - const embeds = lfgMessage.embeds[0].fields || []; - let results: JoinLeaveType = { - embed: [], - success: false, - full: true, - justFilled: false, - }; - let actionResp: string; - switch (action) { - case 'join': - results = handleMemberJoin(embeds, member, false); - actionResp = 'joined'; - break; - case 'leave': - results = handleMemberLeave(embeds, member); - actionResp = 'left'; - break; - case 'alternate': - results = handleMemberJoin(embeds, member, true); - actionResp = 'joined as alternate'; - break; - } - - let resp: string; - if (results.success && lfgMessage.components) { - const buttonRow: ActionRow = lfgMessage.components[0] as ActionRow; - - await lfgMessage.edit({ - embeds: [{ - fields: results.embed, - footer: lfgMessage.embeds[0].footer, - timestamp: lfgMessage.embeds[0].timestamp, - }], - components: [buttonRow], - }); - - if (results.justFilled) { - const thisLFGPost = activeLFGPosts.filter((lfg) => (lfgMessage.id === lfg.messageId && lfgMessage.channelId === lfg.channelId))[0]; - const thisLFG = (await getMessage(thisLFGPost.channelId, thisLFGPost.messageId)).embeds[0].fields || []; - sendDirectMessage(thisLFGPost.ownerId, { - embeds: [{ - title: `Hello ${(await getUser(thisLFGPost.ownerId)).username}! Your event in ${ - lfgMessage.guild?.name || (await getGuild(message.guildId, { counts: false, addToCache: false })).name - } has filled up!`, - fields: [ - thisLFG[0], - { - name: 'Your members are:', - value: thisLFG[4].value, - }, - ], - }], - }); - } - - resp = `Successfully ${actionResp} LFG.`; - } else { - resp = `Failed to ${action} LFG.`; - } - - const m = await message.send({ - embeds: [{ - title: resp, - }], - }); - - m.delete('Channel Cleanup', 5000).catch((e) => { - log(LT.WARN, `Failed to delete message | ${jsonStringifyBig(e)}`); - }); - message.delete('Channel Cleanup', 5000).catch((e) => { - log(LT.WARN, `Failed to delete message | ${jsonStringifyBig(e)}`); - }); - } - } catch (e) { - log(LT.WARN, `Member Join/Leave/Alt command failed: ${jsonStringifyBig(message)} | ${jsonStringifyBig(e)}`); - - const m = await message.send({ - embeds: [{ - title: 'Failed to find LFG.', - }], - }); - - m.delete('Channel Cleanup').catch((e) => { - log(LT.WARN, `Failed to clean up joiner | joining on behalf | ${jsonStringifyBig(e)}`); - }); - message.delete('Channel Cleanup', 5000).catch((e) => { - log(LT.WARN, `Failed to clean up joiner | joining on behalf | ${jsonStringifyBig(e)}`); - }); - } - } // Sets the mod role - else if (subcmd === 'set_mod_role' && (await hasGuildPermissions(message.guildId, message.authorId, ['ADMINISTRATOR']))) { - const mentionedRole = args[1] || ''; - const roleId = BigInt(mentionedRole.substr(3, mentionedRole.length - 4)); - if (message.guild?.roles.has(roleId)) { - let success = true; - if (guildModRoles.has(message.guildId)) { - // Execute the DB update - await dbClient.execute('UPDATE guild_mod_role SET roleId = ? WHERE guildId = ?', [roleId, message.guildId]).catch((e) => { - log(LT.ERROR, `Failed to insert into database: ${jsonStringifyBig(e)}`); - success = false; - }); - } else { - // Execute the DB insertion - await dbClient.execute('INSERT INTO guild_mod_role(guildId,roleId) values(?,?)', [message.guildId, roleId]).catch((e) => { - log(LT.ERROR, `Failed to insert into database: ${jsonStringifyBig(e)}`); - success = false; - }); - } - - if (success) { - guildModRoles.set(message.guildId, roleId); - message.send({ - embeds: [{ - fields: [ - { - name: 'LFG Mod Role set successfully', - value: `LFG Mod Role set to ${args[1]}.`, - }, - ], - }], - }).catch((e) => { - log(LT.WARN, `Failed to send message | ${jsonStringifyBig(e)}`); - }); - } else { - message.send({ - embeds: [{ - fields: [ - { - name: 'Something went wrong!', - value: 'LFG Mod Role has been left unchanged.', - }, - ], - }], - }).catch((e) => { - log(LT.WARN, `Failed to send message | ${jsonStringifyBig(e)}`); - }); - } - } else { - if (guildModRoles.has(message.guildId)) { - message.send({ - embeds: [{ - fields: [ - { - name: 'LFG Mod Role is currently set to:', - value: `<@&${guildModRoles.get(message.guildId)}>`, - }, - ], - }], - }).catch((e) => { - log(LT.WARN, `Failed to send message | ${jsonStringifyBig(e)}`); - }); - } else { - message.send({ - embeds: [{ - fields: [ - { - name: 'There is no LFG Mod Role set for this guild.', - value: `To set one, run this command again with the role mentioned.\n\nExample: \`${prefix}lfg set_mod_role @newModRole\``, - }, - ], - }], - }).catch((e) => { - log(LT.WARN, `Failed to send message | ${jsonStringifyBig(e)}`); - }); - } - } - } // Sets the channel cleaning up for LFG channels to keep LFG events visible and prevent conversations - else if (subcmd === 'set_clean_channel' && (await hasGuildPermissions(message.guildId, message.authorId, ['ADMINISTRATOR']))) { - const cleanSetting = (args[1] || 'list').toLowerCase(); - let success = true; - if (cleanSetting === 'on') { - // Execute the DB insertion - await dbClient.execute('INSERT INTO guild_clean_channel(guildId,channelId) values(?,?)', [message.guildId, message.channelId]).catch((e) => { - log(LT.ERROR, `Failed to insert into database: ${jsonStringifyBig(e)}`); - success = false; - }); - - if (success) { - const tempArr = cleanChannels.get(message.guildId) || []; - tempArr.push(message.channelId); - cleanChannels.set(message.guildId, tempArr); - - const m = await message.send({ - embeds: [{ - fields: [ - { - name: 'Channel Cleaning turned ON.', - value: 'This message will self destruct in 5 seconds.', - }, - ], - }], - }).catch((e) => { - log(LT.WARN, `Failed to send message | ${jsonStringifyBig(e)}`); - }); - - m && m.delete('Channel Cleanup', 5000).catch((e) => { - log(LT.WARN, `Failed to clean up | set_clean_channel | ${jsonStringifyBig(e)}`); - }); - message.delete('Channel Cleanup', 5000).catch((e) => { - log(LT.WARN, `Failed to clean up | set_clean_channel | ${jsonStringifyBig(e)}`); - }); - } else { - message.send({ - embeds: [{ - fields: [ - { - name: 'Something went wrong!', - value: 'Channel Clean status left off.', - }, - ], - }], - }).catch((e) => { - log(LT.WARN, `Failed to send message | ${jsonStringifyBig(e)}`); - }); - } - } else if (cleanSetting === 'off') { - // turns clean off for channel - // Execute the DB insertion - await dbClient.execute('DELETE FROM guild_clean_channel WHERE guildId = ? AND channelId = ?', [message.guildId, message.channelId]).catch((e) => { - log(LT.ERROR, `Failed to delete from database: ${jsonStringifyBig(e)}`); - success = false; - }); - - if (success) { - let tempArr = cleanChannels.get(message.guildId) || []; - tempArr = tempArr.filter((channelId) => channelId !== message.channelId); - cleanChannels.set(message.guildId, tempArr); - - const m = await message.send({ - embeds: [{ - fields: [ - { - name: 'Channel Cleaning turned OFF.', - value: 'This message will self destruct in 5 seconds.', - }, - ], - }], - }).catch((e) => { - log(LT.WARN, `Failed to send message | ${jsonStringifyBig(e)}`); - }); - - m && m.delete('Channel Cleanup', 5000).catch((e) => { - log(LT.WARN, `Failed to clean up | set_clean_channel | ${jsonStringifyBig(e)}`); - }); - message.delete('Channel Cleanup', 5000).catch((e) => { - log(LT.WARN, `Failed to clean up | set_clean_channel | ${jsonStringifyBig(e)}`); - }); - } else { - message.send({ - embeds: [{ - fields: [ - { - name: 'Something went wrong!', - value: 'Channel Clean status left on.', - }, - ], - }], - }).catch((e) => { - log(LT.WARN, `Failed to send message | ${jsonStringifyBig(e)}`); - }); - } - } else if (cleanSetting === 'list') { - // send list of channels with clean on - let cleanChannelStr = ''; - - for (const channelId of cleanChannels.get(message.guildId) || []) { - cleanChannelStr += `<#${channelId}>\n`; - } - cleanChannelStr = cleanChannelStr.substr(0, cleanChannelStr.length - 1); - - const tmpEmbed: Embed = {}; - - if (cleanChannelStr) { - tmpEmbed.fields = [ - { - name: 'Clean Channels enabled for this guild:', - value: cleanChannelStr, - }, - ]; - } else { - tmpEmbed.title = 'No Clean Channels are enabled for this guild.'; - } - - await message.send({ - embeds: [tmpEmbed], - }).catch((e) => { - log(LT.WARN, `Failed to send message | ${jsonStringifyBig(e)}`); - }); - } - } - } // report or r (command that failed) - // Manually report something that screwed up - else if (command === 'report' || command === 'r') { - // Light telemetry to see how many times a command is being run - dbClient.execute(`CALL INC_CNT("report");`).catch((e) => { - log(LT.ERROR, `Failed to call stored procedure INC_CNT: ${jsonStringifyBig(e)}`); - }); - - sendMessage(config.reportChannel, 'USER REPORT:\n' + args.join(' ')).catch((e) => { - log(LT.ERROR, `Failed to send message: ${jsonStringifyBig(message)} | ${jsonStringifyBig(e)}`); - }); - message.send(constantCmds.report).catch((e) => { - log(LT.ERROR, `Failed to send message: ${jsonStringifyBig(message)} | ${jsonStringifyBig(e)}`); - }); - } // version or v - // Returns version of the bot - else if (command === 'version' || command === 'v') { - // Light telemetry to see how many times a command is being run - dbClient.execute(`CALL INC_CNT("version");`).catch((e) => { - log(LT.ERROR, `Failed to call stored procedure INC_CNT: ${jsonStringifyBig(e)}`); - }); - - message.send(constantCmds.version).catch((e) => { - log(LT.ERROR, `Failed to send message: ${jsonStringifyBig(message)} | ${jsonStringifyBig(e)}`); - }); - } // info or i - // Info command, prints short desc on bot and some links - else if (command === 'info' || command === 'i') { - // Light telemetry to see how many times a command is being run - dbClient.execute(`CALL INC_CNT("info");`).catch((e) => { - log(LT.ERROR, `Failed to call stored procedure INC_CNT: ${jsonStringifyBig(e)}`); - }); - - message.send(constantCmds.info).catch((e) => { - log(LT.ERROR, `Failed to send message: ${jsonStringifyBig(message)} | ${jsonStringifyBig(e)}`); - }); - } // help or h or ? - // Help command, prints available commands - else if (command === 'help' || command === 'h' || command === '?') { - // Light telemetry to see how many times a command is being run - dbClient.execute(`CALL INC_CNT("help");`).catch((e) => { - log(LT.ERROR, `Failed to call stored procedure INC_CNT: ${jsonStringifyBig(e)}`); - }); - - message.send(constantCmds.help).catch((e) => { - log(LT.ERROR, `Failed to send message: ${jsonStringifyBig(message)} | ${jsonStringifyBig(e)}`); - }); - } - }, - interactionCreate: async (interact, member) => { - try { - if (interact.type === DiscordInteractionTypes.MessageComponent) { - if (interact.message && interact.data && (interact.data as ButtonData).customId && member) { - log(LT.INFO, `Handling Button ${(interact.data as ButtonData).customId}`); - log(LT.LOG, `Button Data | ${jsonStringifyBig(interact)}`); - - sendInteractionResponse(BigInt(interact.id), interact.token, { - type: DiscordInteractionResponseTypes.DeferredUpdateMessage, - }); - - const [handler, stepInfo] = (interact.data as ButtonData).customId.split('@'); - const [action, value] = stepInfo.split('#'); - switch (handler) { - case 'building': { - await activeBuilders.some(async (x, i) => { - if (x.channelId === BigInt(interact.channelId || '0') && member && x.userId === BigInt(member.id)) { - x.lastTouch = new Date(); - x = await handleLFGStep(x, value); - - if (x.step === 'done' && x.lfgMsg.components) { - const currentLFG = (x.lfgMsg.embeds[0].fields || []); - const newTimestamp = new Date(parseInt(currentLFG[1].value.split('#')[1])); - const newLfgUid = ALPHABET[Math.floor(Math.random() * 26)] + ALPHABET[Math.floor(Math.random() * 26)]; - - const tempMembers = currentLFG[4].name.split(':')[1].split('/'); - const currentMembers = parseInt(tempMembers[0]); - const maxMembers = parseInt(tempMembers[1]); - - const buttonRow: ActionRow = x.lfgMsg.components[0] as ActionRow; - - if (currentMembers > maxMembers) { - const currentPeople = currentLFG[4].value.split('\n'); - const newAlts = currentPeople.splice(maxMembers - 1); - currentLFG[4].value = currentPeople.join('\n'); - currentLFG[5].value = `${newAlts.join('\n')}\n${currentLFG[5].value}`; - currentLFG[4].name = `Members Joined: ${maxMembers}/${maxMembers}`; - } - - await x.lfgMsg.edit({ - content: '', - embeds: [{ - fields: currentLFG, - footer: { - text: `Created by: ${member.username} | ${newLfgUid}`, - }, - timestamp: newTimestamp.toISOString(), - }], - components: [buttonRow], - }); - - const activeIdx = activeLFGPosts.findIndex((lfg) => (lfg.channelId === x.channelId && lfg.messageId === x.lfgMsg.id && lfg.ownerId === x.userId)); - activeLFGPosts[activeIdx].lfgTime = newTimestamp.getTime(); - activeLFGPosts[activeIdx].lfgUid = newLfgUid; - localStorage.setItem('activeLFGPosts', jsonStringifyBig(activeLFGPosts)); - - await activeBuilders[i].questionMsg.delete().catch((e) => { - log(LT.WARN, `Failed to delete message | ${jsonStringifyBig(e)}`); - }); - activeBuilders.splice(i, 1); - } else { - activeBuilders[i] = x; - } - - return true; - } - }); - break; - } - case 'active': { - const message = await getMessage(BigInt(interact.channelId || '0'), BigInt(interact.message.id)); - - const embeds = message.embeds[0].fields || []; - let results: JoinLeaveType = { - embed: [], - success: false, - full: true, - justFilled: false, - }; - switch (action) { - case 'join_group': - results = handleMemberJoin(embeds, member, false); - break; - case 'leave_group': - results = handleMemberLeave(embeds, member); - break; - case 'alternate_group': - results = handleMemberJoin(embeds, member, true); - break; - } - - if (results.success && message.components) { - await message.edit({ - embeds: [{ - fields: results.embed, - footer: message.embeds[0].footer, - timestamp: message.embeds[0].timestamp, - }], - }); - - if (results.justFilled) { - const thisLFGPost = activeLFGPosts.filter((lfg) => (message.id === lfg.messageId && message.channelId === lfg.channelId))[0]; - const thisLFG = (await getMessage(thisLFGPost.channelId, thisLFGPost.messageId)).embeds[0].fields || []; - sendDirectMessage(thisLFGPost.ownerId, { - embeds: [{ - title: `Hello ${(await getUser(thisLFGPost.ownerId)).username}! Your event in ${ - message.guild?.name || (await getGuild(message.guildId, { counts: false, addToCache: false })).name - } has filled up!`, - fields: [ - thisLFG[0], - { - name: 'Your members are:', - value: thisLFG[4].value, - }, - ], - }], - }); - } - } - - break; - } - case 'editing': { - await activeBuilders.some(async (x, i) => { - if (x.editing && x.channelId === BigInt(interact.channelId || '0') && member && x.userId === BigInt(member.id)) { - x.step = action; - x.lastTouch = new Date(); - let nextQuestion = ''; - const nextComponents: Array = []; - switch (action) { - case 'set_game': { - nextQuestion = lfgStepQuestions.set_game; - - const gameButtons: Array = Object.keys(LFGActivities).map((game) => { - return { - type: 2, - label: game, - customId: `building@set_game#${game}`, - style: DiscordButtonStyles.Primary, - }; - }); - - const temp: Array = []; - - gameButtons.forEach((btn, idx) => { - if (!temp[Math.floor(idx / 5)]) { - temp[Math.floor(idx / 5)] = [btn]; - } else { - temp[Math.floor(idx / 5)].push(btn); - } - }); - - temp.forEach((btns) => { - if (btns.length && btns.length <= 5) { - nextComponents.push({ - type: 1, - components: btns, - }); - } - }); - break; - } - case 'set_time': { - nextQuestion = 'Please enter the time of the activity:'; - break; - } - case 'set_desc': { - nextQuestion = 'Please enter a description for the activity. Enter `none` to skip:'; - break; - } - default: - break; - } - - x.questionMsg = await x.questionMsg.edit({ - content: nextQuestion, - components: nextComponents, - }); - - activeBuilders[i] = x; - - return true; - } - }); - break; - } - default: - break; - } - } - } - } catch (e) { - log(LT.ERROR, `Interaction failed: ${jsonStringifyBig(interact)} | ${jsonStringifyBig(member)} | ${jsonStringifyBig(e)}`); - } - }, - }, -}); From 3e6168a396663d12ad44320756013dfa19f10119 Mon Sep 17 00:00:00 2001 From: "Ean Milligan (Bastion)" Date: Wed, 11 Jan 2023 18:06:20 -0500 Subject: [PATCH 003/110] Initial setup for standard events --- .gitignore | 1 + config.example.ts | 2 +- deps.ts | 46 ++++++--------------------- mod.ts | 18 +++++++++++ src/commandUtils.ts | 30 ++++++++++++++++++ src/db.ts | 15 +++++++++ src/events.ts | 17 ++++++++++ src/events/_index.ts | 13 ++++++++ src/events/debug.ts | 8 +++++ src/events/guildCreate.ts | 40 ++++++++++++++++++++++++ src/events/guildDelete.ts | 39 +++++++++++++++++++++++ src/events/messageCreate.ts | 17 ++++++++++ src/events/ready.ts | 62 +++++++++++++++++++++++++++++++++++++ src/utils.ts | 39 +++++++++++++++++++++++ 14 files changed, 310 insertions(+), 37 deletions(-) create mode 100644 src/commandUtils.ts create mode 100644 src/db.ts create mode 100644 src/events.ts create mode 100644 src/events/_index.ts create mode 100644 src/events/debug.ts create mode 100644 src/events/guildCreate.ts create mode 100644 src/events/guildDelete.ts create mode 100644 src/events/messageCreate.ts create mode 100644 src/events/ready.ts create mode 100644 src/utils.ts diff --git a/.gitignore b/.gitignore index 3ffa028..ca9e9d4 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ config.ts logs db/update.ts +deno.lock diff --git a/config.example.ts b/config.example.ts index 04c30f8..e3f5ee6 100644 --- a/config.example.ts +++ b/config.example.ts @@ -2,7 +2,7 @@ export const config = { 'name': 'Group Up', // Name of the bot 'version': '1.0.0', // Version of the bot 'token': 'the_bot_token', // Discord API Token for this bot - 'localtoken': 'local_testing_token', // Discord API Token for a secondary OPTIONAL testing bot, THIS MUST BE DIFFERENT FROM "token" + 'localToken': 'local_testing_token', // Discord API Token for a secondary OPTIONAL testing bot, THIS MUST BE DIFFERENT FROM "token" 'prefix': 'gu!', // Prefix for all commands 'db': { // Settings for the MySQL database, this is required for use with the API, if you do not want to set this up, you will need to rip all code relating to the DB out of the bot 'host': '', // IP address for the db, usually localhost diff --git a/deps.ts b/deps.ts index 0e16319..5c937fa 100644 --- a/deps.ts +++ b/deps.ts @@ -1,40 +1,14 @@ -// All external dependancies are to be loaded here to make updating dependancy versions much easier -export { - botId, - cache, - cacheHandlers, - deleteMessage, - DiscordActivityTypes, - DiscordButtonStyles, - DiscordInteractionResponseTypes, - DiscordInteractionTypes, - editBotNickname, - editBotStatus, - getGuild, - getMessage, - getUser, - hasGuildPermissions, - Intents, - sendDirectMessage, - sendInteractionResponse, - sendMessage, - startBot, - structures, -} from 'https://deno.land/x/discordeno@17.0.1/mod.ts'; +// All external dependencies are to be loaded here to make updating dependency versions much easier +import { getBotIdFromToken } from 'https://deno.land/x/discordeno@16.0.1/mod.ts'; +import config from './config.ts'; +import { LOCALMODE } from './flags.ts'; +export const botId = getBotIdFromToken(LOCALMODE ? config.localToken : config.token); -export type { - ActionRow, - ButtonComponent, - ButtonData, - CreateMessage, - DebugArg, - DiscordenoGuild, - DiscordenoMember, - DiscordenoMessage, - Embed, - EmbedField, - Interaction, -} from 'https://deno.land/x/discordeno@17.0.1/mod.ts'; +export { enableCachePlugin, enableCacheSweepers } from 'https://deno.land/x/discordeno@17.0.1/plugins/cache/mod.ts'; +export type { BotWithCache } from 'https://deno.land/x/discordeno@17.0.1/plugins/cache/mod.ts'; + +export { ActivityTypes, createBot, editBotMember, editBotStatus, getBotIdFromToken, Intents, sendInteractionResponse, sendMessage, startBot } from 'https://deno.land/x/discordeno@17.0.1/mod.ts'; +export type { ActionRow, Bot, ButtonComponent, CreateMessage, Embed, EventHandlers, Guild, Interaction, Message } 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/mod.ts b/mod.ts index e69de29..478c0ff 100644 --- a/mod.ts +++ b/mod.ts @@ -0,0 +1,18 @@ +import config from './config.ts'; +import { DEBUG, LOCALMODE } from './flags.ts'; +import { createBot, enableCachePlugin, enableCacheSweepers, initLog, Intents, startBot } from './deps.ts'; +import { events } from './src/events.ts'; + +// Initialize logging client with folder to use for logs, needs --allow-write set on Deno startup +initLog('logs', DEBUG); + +// Set up the Discord Bot +const bot = enableCachePlugin(createBot({ + token: LOCALMODE ? config.localToken : config.token, + intents: Intents.MessageContent | Intents.GuildMessages | Intents.DirectMessages | Intents.Guilds | Intents.GuildMessageReactions, + events, +})); +enableCacheSweepers(bot); + +// Start the bot +startBot(bot); diff --git a/src/commandUtils.ts b/src/commandUtils.ts new file mode 100644 index 0000000..bde9ce3 --- /dev/null +++ b/src/commandUtils.ts @@ -0,0 +1,30 @@ +import config from '../config.ts'; + +export const failColor = 0xe71212; +export const warnColor = 0xe38f28; +export const successColor = 0x0f8108; +export const infoColor1 = 0x313bf9; +export const infoColor2 = 0x6805e9; + +export const getRandomStatus = (guildCount: number): string => { + let status = ''; + switch (Math.floor((Math.random() * 5) + 1)) { + case 1: + status = `${config.prefix}help for commands`; + break; + case 2: + status = `Running V${config.version}`; + break; + case 3: + status = `${config.prefix}info to learn more`; + break; + case 4: + status = 'Mention me to check my prefix!'; + break; + default: + status = `Running LFGs in ${guildCount} servers`; + break; + } + + return status; +}; diff --git a/src/db.ts b/src/db.ts new file mode 100644 index 0000000..099cc2a --- /dev/null +++ b/src/db.ts @@ -0,0 +1,15 @@ +import config from '../config.ts'; +import { Client } from '../deps.ts'; +import { LOCALMODE } from '../flags.ts'; + +export const dbClient = await new Client().connect({ + hostname: LOCALMODE ? config.db.localhost : config.db.host, + port: config.db.port, + db: config.db.name, + username: config.db.username, + password: config.db.password, +}); + +export const queries = { + callIncCnt: (cmdName: string) => `CALL INC_CNT("${cmdName}");`, +}; diff --git a/src/events.ts b/src/events.ts new file mode 100644 index 0000000..9361475 --- /dev/null +++ b/src/events.ts @@ -0,0 +1,17 @@ +import { DEVMODE } from '../flags.ts'; +import { + // Discordeno deps + EventHandlers, +} from '../deps.ts'; +import eventHandlers from './events/_index.ts'; + +export const events: Partial = {}; + +events.ready = eventHandlers.ready; +events.guildCreate = eventHandlers.guildCreate; +events.guildDelete = eventHandlers.guildDelete; +events.messageCreate = eventHandlers.messageCreate; + +if (DEVMODE) { + events.debug = eventHandlers.debug; +} diff --git a/src/events/_index.ts b/src/events/_index.ts new file mode 100644 index 0000000..2bdf3db --- /dev/null +++ b/src/events/_index.ts @@ -0,0 +1,13 @@ +import { ready } from './ready.ts'; +import { guildCreate } from './guildCreate.ts'; +import { guildDelete } from './guildDelete.ts'; +import { debug } from './debug.ts'; +import { messageCreate } from './messageCreate.ts'; + +export default { + ready, + guildCreate, + guildDelete, + debug, + messageCreate, +}; diff --git a/src/events/debug.ts b/src/events/debug.ts new file mode 100644 index 0000000..e997a1a --- /dev/null +++ b/src/events/debug.ts @@ -0,0 +1,8 @@ +import { + // Log4Deno deps + log, + LT, +} from '../../deps.ts'; +import utils from '../utils.ts'; + +export const debug = (dmsg: string) => log(LT.LOG, `Debug Message | ${utils.jsonStringifyBig(dmsg)}`); diff --git a/src/events/guildCreate.ts b/src/events/guildCreate.ts new file mode 100644 index 0000000..38d3b02 --- /dev/null +++ b/src/events/guildCreate.ts @@ -0,0 +1,40 @@ +import config from '../../config.ts'; +import { + // Discordeno deps + Bot, + Guild, + // Log4Deno deps + log, + LT, + // Discordeno deps + sendMessage, +} from '../../deps.ts'; +import { infoColor1 } from '../commandUtils.ts'; +import utils from '../utils.ts'; + +export const guildCreate = (bot: Bot, guild: Guild) => { + log(LT.LOG, `Handling joining guild ${utils.jsonStringifyBig(guild)}`); + sendMessage(bot, config.logChannel, { + embeds: [{ + title: 'Guild Joined!', + color: infoColor1, + fields: [ + { + name: 'Name:', + value: `${guild.name}`, + inline: true, + }, + { + name: 'Id:', + value: `${guild.id}`, + inline: true, + }, + { + name: 'Member Count:', + value: `${guild.memberCount}`, + inline: true, + }, + ], + }], + }).catch((e: Error) => utils.commonLoggers.messageSendError('mod.ts:95', 'Join Guild', e)); +}; diff --git a/src/events/guildDelete.ts b/src/events/guildDelete.ts new file mode 100644 index 0000000..333eb23 --- /dev/null +++ b/src/events/guildDelete.ts @@ -0,0 +1,39 @@ +import config from '../../config.ts'; +import { + // Discordeno deps + Bot, + // Log4Deno deps + log, + LT, + // Discordeno deps + sendMessage, +} from '../../deps.ts'; +import { warnColor } from '../commandUtils.ts'; +import { dbClient } from '../db.ts'; +import utils from '../utils.ts'; + +export const guildDelete = async (bot: Bot, guildId: bigint) => { + log(LT.LOG, `Handling leaving guild ${utils.jsonStringifyBig(guildId)}`); + + try { + await dbClient.execute('DELETE FROM guild_prefix WHERE guildId = ?', [guildId]); + await dbClient.execute('DELETE FROM guild_mod_role WHERE guildId = ?', [guildId]); + await dbClient.execute('DELETE FROM guild_clean_channel WHERE guildId = ?', [guildId]); + } catch (e) { + log(LT.WARN, `Failed to remove guild from DB: ${utils.jsonStringifyBig(e)}`); + } + + sendMessage(bot, config.logChannel, { + embeds: [{ + title: 'Removed from Guild', + color: warnColor, + fields: [ + { + name: 'Id:', + value: `${guildId}`, + inline: true, + }, + ], + }], + }).catch((e: Error) => utils.commonLoggers.messageSendError('guildDelete.ts:28', 'Leave Guild', e)); +}; diff --git a/src/events/messageCreate.ts b/src/events/messageCreate.ts new file mode 100644 index 0000000..ebbaf7c --- /dev/null +++ b/src/events/messageCreate.ts @@ -0,0 +1,17 @@ +import config from '../../config.ts'; +import { Bot, botId, Message } from '../../deps.ts'; + +export const messageCreate = async (bot: Bot, message: Message) => { + // Ignore all messages that are not commands + if (message.content.indexOf(config.prefix) !== 0) { + // Handle @bot messages + if (message.mentionedUserIds[0] === botId && (message.content.trim().startsWith(`<@${botId}>`) || message.content.trim().startsWith(`<@!${botId}>`))) { + } + + // return as we are done handling this command + return; + } + + // Ignore all other bots + if (message.isFromBot) return; +}; diff --git a/src/events/ready.ts b/src/events/ready.ts new file mode 100644 index 0000000..eac7fe9 --- /dev/null +++ b/src/events/ready.ts @@ -0,0 +1,62 @@ +import config from '../../config.ts'; +import { LOCALMODE } from '../../flags.ts'; +import { ActivityTypes, Bot, BotWithCache, editBotMember, editBotStatus, log, LT, sendMessage } from '../../deps.ts'; +import { getRandomStatus, successColor } from '../commandUtils.ts'; +import utils from '../utils.ts'; + +export const ready = (rawBot: Bot) => { + const bot = rawBot as BotWithCache; + log(LT.INFO, `${config.name} Logged in!`); + editBotStatus(bot, { + activities: [{ + name: 'Booting up . . .', + type: ActivityTypes.Game, + createdAt: new Date().getTime(), + }], + status: 'online', + }); + + // Interval to rotate the status text every 30 seconds to show off more commands + setInterval(async () => { + log(LT.LOG, 'Changing bot status'); + try { + // Wrapped in try-catch due to hard crash possible + editBotStatus(bot, { + activities: [{ + name: getRandomStatus(bot.guilds.size + bot.dispatchedGuildIds.size), + type: ActivityTypes.Game, + createdAt: new Date().getTime(), + }], + status: 'online', + }); + } catch (e) { + log(LT.ERROR, `Failed to update status: ${utils.jsonStringifyBig(e)}`); + } + }, 30000); + + // setTimeout added to make sure the startup message does not error out + setTimeout(() => { + LOCALMODE && editBotMember(bot, config.devServer, { nick: `LOCAL - ${config.name}` }); + editBotStatus(bot, { + activities: [{ + name: 'Booting Complete', + type: ActivityTypes.Game, + createdAt: new Date().getTime(), + }], + status: 'online', + }); + sendMessage(bot, config.logChannel, { + embeds: [{ + title: `${config.name} is now Online`, + color: successColor, + fields: [ + { + name: 'Version:', + value: `${config.version}`, + inline: true, + }, + ], + }], + }).catch((e: Error) => utils.commonLoggers.messageSendError('ready.ts:71', 'Startup', e)); + }, 1000); +}; diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 0000000..aaa8964 --- /dev/null +++ b/src/utils.ts @@ -0,0 +1,39 @@ +import { + // Log4Deno deps + log, + LT, + // Discordeno deps + Message, +} from '../deps.ts'; + +const jsonStringifyBig = (input: any) => { + return JSON.stringify(input, (_key, value) => typeof value === 'bigint' ? value.toString() + 'n' : value); +}; + +const genericLogger = (level: LT, message: string) => log(level, message); +const messageEditError = (location: string, message: Message | string, err: Error) => + genericLogger(LT.ERROR, `${location} | Failed to edit message: ${jsonStringifyBig(message)} | Error: ${err.name} - ${err.message}`); +const messageGetError = (location: string, message: Message | string, err: Error) => + genericLogger(LT.ERROR, `${location} | Failed to get message: ${jsonStringifyBig(message)} | Error: ${err.name} - ${err.message}`); +const messageSendError = (location: string, message: Message | string, err: Error) => + genericLogger(LT.ERROR, `${location} | Failed to send message: ${jsonStringifyBig(message)} | Error: ${err.name} - ${err.message}`); +const messageDeleteError = (location: string, message: Message | string, err: Error) => + genericLogger(LT.ERROR, `${location} | Failed to delete message: ${jsonStringifyBig(message)} | Error: ${err.name} - ${err.message}`); +const reactionAddError = (location: string, message: Message | string, err: Error, emoji: string) => + genericLogger(LT.ERROR, `${location} | Failed to add emoji (${emoji}) to message: ${jsonStringifyBig(message)} | Error: ${err.name} - ${err.message}`); +const reactionDeleteError = (location: string, message: Message | string, err: Error, emoji: string) => + genericLogger(LT.ERROR, `${location} | Failed to delete emoji (${emoji}) from message: ${jsonStringifyBig(message)} | Error: ${err.name} - ${err.message}`); +const dbError = (location: string, type: string, err: Error) => genericLogger(LT.ERROR, `${location} | Failed to ${type} database | Error: ${err.name} - ${err.message}`); + +export default { + commonLoggers: { + dbError, + messageGetError, + messageEditError, + messageSendError, + messageDeleteError, + reactionAddError, + reactionDeleteError, + }, + jsonStringifyBig, +}; From e0497b85261401c3cc20f6e8c5cb44ac7c93eced Mon Sep 17 00:00:00 2001 From: "Ean Milligan (Bastion)" Date: Wed, 11 Jan 2023 21:21:43 -0500 Subject: [PATCH 004/110] slash command system added --- config.example.ts | 4 ++++ deps.ts | 32 +++++++++++++++++++++++-- mod.ts | 5 +++- src/commandUtils.ts | 32 ++++++++++--------------- src/commands/_index.ts | 23 ++++++++++++++++++ src/commands/info.ts | 41 +++++++++++++++++++++++++++++++++ src/db.ts | 2 ++ src/events.ts | 6 ++--- src/events/_index.ts | 2 ++ src/events/interactionCreate.ts | 16 +++++++++++++ src/types/commandTypes.ts | 15 ++++++++++++ src/utils.ts | 11 ++++----- 12 files changed, 155 insertions(+), 34 deletions(-) create mode 100644 src/commands/_index.ts create mode 100644 src/commands/info.ts create mode 100644 src/events/interactionCreate.ts create mode 100644 src/types/commandTypes.ts diff --git a/config.example.ts b/config.example.ts index e3f5ee6..c655cfa 100644 --- a/config.example.ts +++ b/config.example.ts @@ -12,6 +12,10 @@ export const config = { 'password': '', // Password for the account, user account may need to be authenticated with the "Standard" Authentication Type if this does not work out of the box 'name': '', // Name of the database Schema to use for the bot }, + 'link': { // Links to various sites + 'sourceCode': 'https://github.com/Burn-E99/GroupUp', // Link to the repository + 'supportServer': '', // Invite link to the Discord support server + }, '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 'devServer': 'the_dev_server', // Discord guild ID where testing of indev features/commands will be handled, used in conjuction with the DEVMODE bool in mod.ts diff --git a/deps.ts b/deps.ts index 5c937fa..e0d1204 100644 --- a/deps.ts +++ b/deps.ts @@ -7,8 +7,36 @@ export const botId = getBotIdFromToken(LOCALMODE ? config.localToken : config.to export { enableCachePlugin, enableCacheSweepers } from 'https://deno.land/x/discordeno@17.0.1/plugins/cache/mod.ts'; export type { BotWithCache } from 'https://deno.land/x/discordeno@17.0.1/plugins/cache/mod.ts'; -export { ActivityTypes, createBot, editBotMember, editBotStatus, getBotIdFromToken, Intents, sendInteractionResponse, sendMessage, startBot } from 'https://deno.land/x/discordeno@17.0.1/mod.ts'; -export type { ActionRow, Bot, ButtonComponent, CreateMessage, Embed, EventHandlers, Guild, Interaction, Message } from 'https://deno.land/x/discordeno@17.0.1/mod.ts'; +export { + ActivityTypes, + ApplicationCommandFlags, + ApplicationCommandTypes, + createBot, + editBotMember, + editBotStatus, + getBotIdFromToken, + Intents, + InteractionResponseTypes, + sendInteractionResponse, + sendMessage, + startBot, +} from 'https://deno.land/x/discordeno@17.0.1/mod.ts'; +export type { + ActionRow, + ApplicationCommand, + ApplicationCommandOption, + Bot, + ButtonComponent, + CreateApplicationCommand, + CreateMessage, + Embed, + EventHandlers, + Guild, + Interaction, + MakeRequired, + Message, + PermissionStrings, +} 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/mod.ts b/mod.ts index 478c0ff..bd99686 100644 --- a/mod.ts +++ b/mod.ts @@ -2,6 +2,7 @@ import config from './config.ts'; import { DEBUG, LOCALMODE } from './flags.ts'; import { createBot, enableCachePlugin, enableCacheSweepers, initLog, Intents, startBot } from './deps.ts'; import { events } from './src/events.ts'; +import { createSlashCommands } from './src/commands/_index.ts'; // Initialize logging client with folder to use for logs, needs --allow-write set on Deno startup initLog('logs', DEBUG); @@ -15,4 +16,6 @@ const bot = enableCachePlugin(createBot({ enableCacheSweepers(bot); // Start the bot -startBot(bot); +await startBot(bot); + +await createSlashCommands(bot); diff --git a/src/commandUtils.ts b/src/commandUtils.ts index bde9ce3..47ff881 100644 --- a/src/commandUtils.ts +++ b/src/commandUtils.ts @@ -1,4 +1,6 @@ +import { ApplicationCommandFlags } from '../deps.ts'; import config from '../config.ts'; +import { lfgChannels } from './db.ts'; export const failColor = 0xe71212; export const warnColor = 0xe38f28; @@ -7,24 +9,14 @@ export const infoColor1 = 0x313bf9; export const infoColor2 = 0x6805e9; export const getRandomStatus = (guildCount: number): string => { - let status = ''; - switch (Math.floor((Math.random() * 5) + 1)) { - case 1: - status = `${config.prefix}help for commands`; - break; - case 2: - status = `Running V${config.version}`; - break; - case 3: - status = `${config.prefix}info to learn more`; - break; - case 4: - status = 'Mention me to check my prefix!'; - break; - default: - status = `Running LFGs in ${guildCount} servers`; - break; - } - - return status; + const statuses = [ + `Running V${config.version}`, + `${config.prefix}info to learn more`, + `Running LFGs in ${guildCount} servers`, + ]; + return statuses[Math.floor((Math.random() * statuses.length) + 1)]; +}; + +export const isLFGChannel = (channelId: bigint) => { + return (lfgChannels.includes(channelId) || channelId === 0n) ? ApplicationCommandFlags.Ephemeral : undefined; }; diff --git a/src/commands/_index.ts b/src/commands/_index.ts new file mode 100644 index 0000000..446b6fe --- /dev/null +++ b/src/commands/_index.ts @@ -0,0 +1,23 @@ +import { Bot, CreateApplicationCommand, log, LT, MakeRequired } from '../../deps.ts'; +import { Commands } from '../types/commandTypes.ts'; +import utils from '../utils.ts'; + +import info from './info.ts'; + +export const commands: Array = [info]; + +export const createSlashCommands = async (bot: Bot) => { + const globalCommands: MakeRequired[] = []; + for (const command of commands) { + globalCommands.push({ + name: command.details.name, + description: command.details.description, + type: command.details.type, + options: command.details.options ? command.details.options : undefined, + dmPermission: command.details.dmPermission ? command.details.dmPermission : false, + defaultMemberPermissions: command.details.defaultMemberPermissions ? command.details.defaultMemberPermissions : undefined, + }); + } + + await bot.helpers.upsertGlobalApplicationCommands(globalCommands).catch((errMsg) => log(LT.ERROR, `Failed to upsert application commands | ${utils.jsonStringifyBig(errMsg)}`)); +}; diff --git a/src/commands/info.ts b/src/commands/info.ts new file mode 100644 index 0000000..dbc872e --- /dev/null +++ b/src/commands/info.ts @@ -0,0 +1,41 @@ +import config from '../../config.ts'; +import { ApplicationCommandTypes, Bot, Interaction, InteractionResponseTypes } from '../../deps.ts'; +import { infoColor2, isLFGChannel } from '../commandUtils.ts'; +import { CommandDetails } from '../types/commandTypes.ts'; +import utils from '../utils.ts'; + +const details: CommandDetails = { + name: 'info', + description: `Information about ${config.name} and its developer`, + type: ApplicationCommandTypes.ChatInput, +}; + +const execute = (bot: Bot, interaction: Interaction) => { + bot.helpers.sendInteractionResponse( + interaction.id, + interaction.token, + { + type: InteractionResponseTypes.ChannelMessageWithSource, + data: { + flags: isLFGChannel(interaction.channelId || 0n), + embeds: [{ + color: infoColor2, + title: `${config.name}, the LFG bot`, + description: `${config.name} is developed by Ean AKA Burn_E99. +Want to check out my source code? Check it out [here](${config.links.sourceCode}). +Need help with this bot? Join my support server [here](${config.links.supportServer}). + +Ran into a bug? Report it to my developers using \`/report [issue description]\`.`, + footer: { + text: `Current Version: ${config.version}`, + }, + }], + }, + }, + ).catch((e: Error) => utils.commonLoggers.interactionSendError('info.ts', interaction, e)); +}; + +export default { + details, + execute, +}; diff --git a/src/db.ts b/src/db.ts index 099cc2a..94e0077 100644 --- a/src/db.ts +++ b/src/db.ts @@ -13,3 +13,5 @@ export const dbClient = await new Client().connect({ export const queries = { callIncCnt: (cmdName: string) => `CALL INC_CNT("${cmdName}");`, }; + +export const lfgChannels: Array = [1055568692697649232n]; diff --git a/src/events.ts b/src/events.ts index 9361475..22059b8 100644 --- a/src/events.ts +++ b/src/events.ts @@ -1,8 +1,5 @@ import { DEVMODE } from '../flags.ts'; -import { - // Discordeno deps - EventHandlers, -} from '../deps.ts'; +import { EventHandlers } from '../deps.ts'; import eventHandlers from './events/_index.ts'; export const events: Partial = {}; @@ -11,6 +8,7 @@ events.ready = eventHandlers.ready; events.guildCreate = eventHandlers.guildCreate; events.guildDelete = eventHandlers.guildDelete; events.messageCreate = eventHandlers.messageCreate; +events.interactionCreate = eventHandlers.interactionCreate; if (DEVMODE) { events.debug = eventHandlers.debug; diff --git a/src/events/_index.ts b/src/events/_index.ts index 2bdf3db..d3f4bd9 100644 --- a/src/events/_index.ts +++ b/src/events/_index.ts @@ -3,6 +3,7 @@ import { guildCreate } from './guildCreate.ts'; import { guildDelete } from './guildDelete.ts'; import { debug } from './debug.ts'; import { messageCreate } from './messageCreate.ts'; +import { interactionCreate } from './interactionCreate.ts'; export default { ready, @@ -10,4 +11,5 @@ export default { guildDelete, debug, messageCreate, + interactionCreate, }; diff --git a/src/events/interactionCreate.ts b/src/events/interactionCreate.ts new file mode 100644 index 0000000..8c802ce --- /dev/null +++ b/src/events/interactionCreate.ts @@ -0,0 +1,16 @@ +import { Bot, BotWithCache, Interaction } from '../../deps.ts'; +import { commands } from '../commands/_index.ts'; + +const commandNames: Array = commands.map((command) => command.details.name); + +export const interactionCreate = (rawBot: Bot, interaction: Interaction) => { + const bot = rawBot as BotWithCache; + if (interaction.data && interaction.id) { + if (interaction.data.name) { + if (commandNames.includes(interaction.data.name)) { + const cmdIdx = commandNames.indexOf(interaction.data.name); + commands[cmdIdx].execute(bot, interaction); + } + } + } +}; diff --git a/src/types/commandTypes.ts b/src/types/commandTypes.ts new file mode 100644 index 0000000..65024d4 --- /dev/null +++ b/src/types/commandTypes.ts @@ -0,0 +1,15 @@ +import { ApplicationCommandOption, ApplicationCommandTypes, PermissionStrings } from '../../deps.ts'; + +export type CommandDetails = { + name: string; + description: string; + type: ApplicationCommandTypes; + options?: ApplicationCommandOption[]; + dmPermission?: boolean; + defaultMemberPermissions?: PermissionStrings[]; +}; + +export type Commands = { + details: CommandDetails; + execute: Function; +}; diff --git a/src/utils.ts b/src/utils.ts index aaa8964..ac26a42 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,16 +1,12 @@ -import { - // Log4Deno deps - log, - LT, - // Discordeno deps - Message, -} from '../deps.ts'; +import { Interaction, log, LT, Message } from '../deps.ts'; const jsonStringifyBig = (input: any) => { return JSON.stringify(input, (_key, value) => typeof value === 'bigint' ? value.toString() + 'n' : value); }; 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}`); const messageEditError = (location: string, message: Message | string, err: Error) => genericLogger(LT.ERROR, `${location} | Failed to edit message: ${jsonStringifyBig(message)} | Error: ${err.name} - ${err.message}`); const messageGetError = (location: string, message: Message | string, err: Error) => @@ -28,6 +24,7 @@ const dbError = (location: string, type: string, err: Error) => genericLogger(LT export default { commonLoggers: { dbError, + interactionSendError, messageGetError, messageEditError, messageSendError, From db492f312fc2d194f47f0c8dbd88330cf7c57d5e Mon Sep 17 00:00:00 2001 From: "Ean Milligan (Bastion)" Date: Wed, 11 Jan 2023 21:26:48 -0500 Subject: [PATCH 005/110] add inccnt to info --- src/commands/info.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/commands/info.ts b/src/commands/info.ts index dbc872e..6e9c362 100644 --- a/src/commands/info.ts +++ b/src/commands/info.ts @@ -1,6 +1,7 @@ import config from '../../config.ts'; import { ApplicationCommandTypes, Bot, Interaction, InteractionResponseTypes } from '../../deps.ts'; import { infoColor2, isLFGChannel } from '../commandUtils.ts'; +import { dbClient, queries } from '../db.ts'; import { CommandDetails } from '../types/commandTypes.ts'; import utils from '../utils.ts'; @@ -11,6 +12,7 @@ const details: CommandDetails = { }; const execute = (bot: Bot, interaction: Interaction) => { + dbClient.execute(queries.callIncCnt('report')).catch((e) => utils.commonLoggers.dbError('info.ts', 'call sproc INC_CNT on', e)); bot.helpers.sendInteractionResponse( interaction.id, interaction.token, From d8bd5694fb623b888985cbbd8d3c602e566e5102 Mon Sep 17 00:00:00 2001 From: "Ean Milligan (Bastion)" Date: Wed, 11 Jan 2023 22:23:27 -0500 Subject: [PATCH 006/110] add report command --- _OLD/mod.ts | 80 ++-------------------------------------- _OLD/src/constantCmds.ts | 58 +---------------------------- _OLD/src/intervals.ts | 2 +- deps.ts | 1 + src/commandUtils.ts | 8 ++++ src/commands/_index.ts | 3 +- src/commands/info.ts | 2 +- src/commands/report.ts | 50 +++++++++++++++++++++++++ 8 files changed, 68 insertions(+), 136 deletions(-) create mode 100644 src/commands/report.ts diff --git a/_OLD/mod.ts b/_OLD/mod.ts index ed6fa54..966e048 100644 --- a/_OLD/mod.ts +++ b/_OLD/mod.ts @@ -42,8 +42,8 @@ import { handleLFGStep, handleMemberJoin, handleMemberLeave, urlToIds } from './ import { constantCmds, editBtns, lfgStepQuestions } from './src/constantCmds.ts'; import { jsonParseBig, jsonStringifyBig } from './src/utils.ts'; -import { DEBUG, LOCALMODE } from './flags.ts'; -import config from './config.ts'; +import { DEBUG, LOCALMODE } from '../flags.ts'; +import config from '../config.ts'; // Initialize DB client const dbClient = await new Client().connect({ @@ -94,7 +94,7 @@ const ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; // Start up the Discord Bot startBot({ - token: LOCALMODE ? config.localtoken : config.token, + token: LOCALMODE ? config.localToken : config.token, intents: [Intents.GuildMessages, Intents.DirectMessages, Intents.Guilds], eventHandlers: { ready: () => { @@ -393,32 +393,7 @@ startBot({ // All commands below here - // ping - // Its a ping test, what else do you want. - if (command === 'ping') { - // Light telemetry to see how many times a command is being run - dbClient.execute(`CALL INC_CNT("ping");`).catch((e) => { - log(LT.ERROR, `Failed to call stored procedure INC_CNT: ${jsonStringifyBig(e)}`); - }); - - // Calculates ping between sending a message and editing it, giving a nice round-trip latency. - try { - const m = await message.send({ - embeds: [{ - title: 'Ping?', - }], - }); - m.edit({ - embeds: [{ - title: `Pong! Latency is ${m.timestamp - message.timestamp}ms.`, - }], - }); - } catch (e) { - log(LT.ERROR, `Failed to send message: ${jsonStringifyBig(message)} | ${jsonStringifyBig(e)}`); - } - } // lfg - // Handles all LFG commands, creating, editing, deleting - else if (command === 'lfg') { + if (command === 'lfg') { // Light telemetry to see how many times a command is being run dbClient.execute(`CALL INC_CNT("lfg");`).catch((e) => { log(LT.ERROR, `Failed to call stored procedure INC_CNT: ${jsonStringifyBig(e)}`); @@ -998,53 +973,6 @@ startBot({ }); } } - } // report or r (command that failed) - // Manually report something that screwed up - else if (command === 'report' || command === 'r') { - // Light telemetry to see how many times a command is being run - dbClient.execute(`CALL INC_CNT("report");`).catch((e) => { - log(LT.ERROR, `Failed to call stored procedure INC_CNT: ${jsonStringifyBig(e)}`); - }); - - sendMessage(config.reportChannel, 'USER REPORT:\n' + args.join(' ')).catch((e) => { - log(LT.ERROR, `Failed to send message: ${jsonStringifyBig(message)} | ${jsonStringifyBig(e)}`); - }); - message.send(constantCmds.report).catch((e) => { - log(LT.ERROR, `Failed to send message: ${jsonStringifyBig(message)} | ${jsonStringifyBig(e)}`); - }); - } // version or v - // Returns version of the bot - else if (command === 'version' || command === 'v') { - // Light telemetry to see how many times a command is being run - dbClient.execute(`CALL INC_CNT("version");`).catch((e) => { - log(LT.ERROR, `Failed to call stored procedure INC_CNT: ${jsonStringifyBig(e)}`); - }); - - message.send(constantCmds.version).catch((e) => { - log(LT.ERROR, `Failed to send message: ${jsonStringifyBig(message)} | ${jsonStringifyBig(e)}`); - }); - } // info or i - // Info command, prints short desc on bot and some links - else if (command === 'info' || command === 'i') { - // Light telemetry to see how many times a command is being run - dbClient.execute(`CALL INC_CNT("info");`).catch((e) => { - log(LT.ERROR, `Failed to call stored procedure INC_CNT: ${jsonStringifyBig(e)}`); - }); - - message.send(constantCmds.info).catch((e) => { - log(LT.ERROR, `Failed to send message: ${jsonStringifyBig(message)} | ${jsonStringifyBig(e)}`); - }); - } // help or h or ? - // Help command, prints available commands - else if (command === 'help' || command === 'h' || command === '?') { - // Light telemetry to see how many times a command is being run - dbClient.execute(`CALL INC_CNT("help");`).catch((e) => { - log(LT.ERROR, `Failed to call stored procedure INC_CNT: ${jsonStringifyBig(e)}`); - }); - - message.send(constantCmds.help).catch((e) => { - log(LT.ERROR, `Failed to send message: ${jsonStringifyBig(message)} | ${jsonStringifyBig(e)}`); - }); } }, interactionCreate: async (interact, member) => { diff --git a/_OLD/src/constantCmds.ts b/_OLD/src/constantCmds.ts index bf2a751..031af5a 100644 --- a/_OLD/src/constantCmds.ts +++ b/_OLD/src/constantCmds.ts @@ -1,37 +1,8 @@ import { ActionRow, DiscordButtonStyles } from '../deps.ts'; -import config from '../config.ts'; +import config from '../../config.ts'; export const constantCmds = { - help: { - embeds: [{ - title: `${config.name} Help`, - fields: [ - { - name: 'All commands must have the bot\'s prefix before them.', - value: `Default is \`${config.prefix}\`, send <@847256159123013722> to change it.`, - }, - { - name: 'LFG Commands', - value: ` - \`lfg help\` - More detailed help for the LFG commands - \`lfg create\` - Create a new LFG post - \`lfg edit\` - Edit an existing LFG post - \`lfg delete\` - Delete an existing LFG post - `, - }, - { - name: 'Utility Commands', - value: ` - \`info\` - Information about the bot - \`ping\` - Pings the bot to check its connection - \`report [TEXT]\` - Report an issue to the developer - \`version\` - Prints the bot's current version - `, - }, - ], - }], - }, lfgHelp: { embeds: [{ title: `${config.name} LFG Help`, @@ -74,33 +45,6 @@ export const constantCmds = { ], }], }, - info: { - embeds: [{ - fields: [ - { - name: 'Group Up, the LFG bot', - value: `Group Up is developed by Ean AKA Burn_E99. - Want to check out my source code? Check it out [here](https://github.com/Burn-E99/GroupUp). - Need help with this bot? Join my support server [here](https://discord.gg/peHASXMZYv).`, - }, - ], - }], - }, - version: { - embeds: [{ - title: `My current version is ${config.version}`, - }], - }, - report: { - embeds: [{ - fields: [ - { - name: 'Failed command has been reported to my developer.', - value: 'For more in depth support, and information about planned maintenance, please join the support server [here](https://discord.gg/peHASXMZYv).', - }, - ], - }], - }, lfgDelete1: { embeds: [{ fields: [ diff --git a/_OLD/src/intervals.ts b/_OLD/src/intervals.ts index aea2c91..e22c3ae 100644 --- a/_OLD/src/intervals.ts +++ b/_OLD/src/intervals.ts @@ -14,7 +14,7 @@ import { import { jsonStringifyBig } from './utils.ts'; import { ActiveLFG, BuildingLFG } from './mod.d.ts'; -import config from '../config.ts'; +import config from '../../config.ts'; // getRandomStatus() returns status as string // Gets a new random status for the bot diff --git a/deps.ts b/deps.ts index e0d1204..ce53a83 100644 --- a/deps.ts +++ b/deps.ts @@ -10,6 +10,7 @@ export type { BotWithCache } from 'https://deno.land/x/discordeno@17.0.1/plugins export { ActivityTypes, ApplicationCommandFlags, + ApplicationCommandOptionTypes, ApplicationCommandTypes, createBot, editBotMember, diff --git a/src/commandUtils.ts b/src/commandUtils.ts index 47ff881..c79778c 100644 --- a/src/commandUtils.ts +++ b/src/commandUtils.ts @@ -20,3 +20,11 @@ export const getRandomStatus = (guildCount: number): string => { export const isLFGChannel = (channelId: bigint) => { return (lfgChannels.includes(channelId) || channelId === 0n) ? ApplicationCommandFlags.Ephemeral : undefined; }; + +export const generateReport = (msg: string) => ({ + embeds: [{ + color: infoColor2, + title: 'USER REPORT:', + description: msg || 'No message', + }], +}); diff --git a/src/commands/_index.ts b/src/commands/_index.ts index 446b6fe..100b155 100644 --- a/src/commands/_index.ts +++ b/src/commands/_index.ts @@ -3,8 +3,9 @@ import { Commands } from '../types/commandTypes.ts'; import utils from '../utils.ts'; import info from './info.ts'; +import report from './report.ts'; -export const commands: Array = [info]; +export const commands: Array = [info, report]; export const createSlashCommands = async (bot: Bot) => { const globalCommands: MakeRequired[] = []; diff --git a/src/commands/info.ts b/src/commands/info.ts index 6e9c362..6551a26 100644 --- a/src/commands/info.ts +++ b/src/commands/info.ts @@ -12,7 +12,7 @@ const details: CommandDetails = { }; const execute = (bot: Bot, interaction: Interaction) => { - dbClient.execute(queries.callIncCnt('report')).catch((e) => utils.commonLoggers.dbError('info.ts', 'call sproc INC_CNT on', e)); + dbClient.execute(queries.callIncCnt('info')).catch((e) => utils.commonLoggers.dbError('info.ts', 'call sproc INC_CNT on', e)); bot.helpers.sendInteractionResponse( interaction.id, interaction.token, diff --git a/src/commands/report.ts b/src/commands/report.ts new file mode 100644 index 0000000..03cbc9c --- /dev/null +++ b/src/commands/report.ts @@ -0,0 +1,50 @@ +import config from '../../config.ts'; +import { ApplicationCommandOptionTypes, ApplicationCommandTypes, Bot, Interaction, InteractionResponseTypes, sendMessage } from '../../deps.ts'; +import { generateReport, isLFGChannel, successColor } from '../commandUtils.ts'; +import { dbClient, queries } from '../db.ts'; +import { CommandDetails } from '../types/commandTypes.ts'; +import utils from '../utils.ts'; + +const details: CommandDetails = { + name: 'report', + description: `Information about ${config.name} and its developer`, + type: ApplicationCommandTypes.ChatInput, + options: [ + { + name: 'issue', + type: ApplicationCommandOptionTypes.String, + description: 'Please describe the issue you were having.', + required: true, + minLength: 1, + maxLength: 2000, + }, + ], +}; + +const execute = (bot: Bot, interaction: Interaction) => { + console.log(interaction); + dbClient.execute(queries.callIncCnt('report')).catch((e) => utils.commonLoggers.dbError('report.ts', 'call sproc INC_CNT on', e)); + sendMessage(bot, config.reportChannel, generateReport(interaction.data?.options?.[0].value as string || 'Missing Options')).catch((e: Error) => + utils.commonLoggers.interactionSendError('report.ts:28', interaction, e) + ); + bot.helpers.sendInteractionResponse( + interaction.id, + interaction.token, + { + type: InteractionResponseTypes.ChannelMessageWithSource, + data: { + flags: isLFGChannel(interaction.channelId || 0n), + embeds: [{ + color: successColor, + title: 'Failed command has been reported to my developer.', + description: `For more in depth support, and information about planned maintenance, please join the support server [here](${config.links.supportServer}).`, + }], + }, + }, + ).catch((e: Error) => utils.commonLoggers.interactionSendError('report.ts:44', interaction, e)); +}; + +export default { + details, + execute, +}; From 9cb615e68daaf080a6580a47b25d7e47958d83b4 Mon Sep 17 00:00:00 2001 From: "Ean Milligan (Bastion)" Date: Wed, 11 Jan 2023 22:57:58 -0500 Subject: [PATCH 007/110] update report --- src/commandUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commandUtils.ts b/src/commandUtils.ts index c79778c..9b1b61e 100644 --- a/src/commandUtils.ts +++ b/src/commandUtils.ts @@ -25,6 +25,6 @@ export const generateReport = (msg: string) => ({ embeds: [{ color: infoColor2, title: 'USER REPORT:', - description: msg || 'No message', + description: msg, }], }); From a43fade7d524bb3f4e2a9dc99b5089fc1703dbd5 Mon Sep 17 00:00:00 2001 From: "Ean Milligan (Bastion)" Date: Sat, 28 Jan 2023 20:58:24 -0500 Subject: [PATCH 008/110] Slight rework to commands, db redesign, setup command almost done --- config.example.ts | 6 +- db/initialize.ts | 57 ++----- db/populateDefaults.ts | 29 ++-- deps.ts | 6 + mod.ts | 1 + src/commandUtils.ts | 26 ++- src/commands/_index.ts | 5 +- src/commands/info.ts | 4 +- src/commands/report.ts | 41 ++--- src/commands/setup.ts | 321 ++++++++++++++++++++++++++++++++++++++ src/db.ts | 11 +- src/types/commandTypes.ts | 15 +- src/utils.ts | 4 + 13 files changed, 427 insertions(+), 99 deletions(-) create mode 100644 src/commands/setup.ts diff --git a/config.example.ts b/config.example.ts index c655cfa..4972098 100644 --- a/config.example.ts +++ b/config.example.ts @@ -3,12 +3,12 @@ export const config = { 'version': '1.0.0', // Version of the bot 'token': 'the_bot_token', // Discord API Token for this bot 'localToken': 'local_testing_token', // Discord API Token for a secondary OPTIONAL testing bot, THIS MUST BE DIFFERENT FROM "token" - 'prefix': 'gu!', // Prefix for all commands + 'prefix': '/', // Prefix for all commands 'db': { // Settings for the MySQL database, this is required for use with the API, if you do not want to set this up, you will need to rip all code relating to the DB out of the bot 'host': '', // IP address for the db, usually localhost 'localhost': '', // IP address for a secondary OPTIONAL local testing DB, usually also is localhost, but depends on your dev environment 'port': 3306, // Port for the db - 'username': '', // Username for the account that will access your DB, this account will need "DB Manager" admin rights and "REFERENCES" Global Privalages + 'username': '', // Username for the account that will access your DB, this account will need "DB Manager" admin rights and "REFERENCES" Global Privileges 'password': '', // Password for the account, user account may need to be authenticated with the "Standard" Authentication Type if this does not work out of the box 'name': '', // Name of the database Schema to use for the bot }, @@ -18,7 +18,7 @@ export const config = { }, '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 - 'devServer': 'the_dev_server', // Discord guild ID where testing of indev features/commands will be handled, used in conjuction with the DEVMODE bool in mod.ts + 'devServer': 'the_dev_server', // Discord guild ID where testing of indev features/commands will be handled, used in conjunction with the DEVMODE bool in mod.ts 'owner': 'the_bot_owner', // Discord user ID of the bot admin 'botLists': [ // Array of objects containing all bot lists that stats should be posted to { // Bot List object, duplicate for each bot list diff --git a/db/initialize.ts b/db/initialize.ts index d850fd8..279219b 100644 --- a/db/initialize.ts +++ b/db/initialize.ts @@ -1,21 +1,8 @@ // This file will create all tables for the artificer schema // DATA WILL BE LOST IF DB ALREADY EXISTS, RUN AT OWN RISK -import { - // MySQL deps - Client, -} from '../deps.ts'; - -import { LOCALMODE } from '../flags.ts'; import config from '../config.ts'; - -// Log into the MySQL DB -const dbClient = await new Client().connect({ - hostname: LOCALMODE ? config.db.localhost : config.db.host, - port: config.db.port, - username: config.db.username, - password: config.db.password, -}); +import { dbClient } from '../src/db.ts'; console.log('Attempting to create DB'); await dbClient.execute(`CREATE SCHEMA IF NOT EXISTS ${config.db.name};`); @@ -25,9 +12,7 @@ console.log('DB created'); console.log('Attempt to drop all tables'); await dbClient.execute(`DROP PROCEDURE IF EXISTS INC_CNT;`); await dbClient.execute(`DROP TABLE IF EXISTS command_cnt;`); -await dbClient.execute(`DROP TABLE IF EXISTS guild_prefix;`); -await dbClient.execute(`DROP TABLE IF EXISTS guild_mod_role;`); -await dbClient.execute(`DROP TABLE IF EXISTS guild_clean_channel;`); +await dbClient.execute(`DROP TABLE IF EXISTS guild_settings;`); console.log('Tables dropped'); console.log('Attempting to create table command_cnt'); @@ -47,41 +32,21 @@ await dbClient.execute(` IN cmd CHAR(20) ) BEGIN - declare oldcnt bigint unsigned; - set oldcnt = (SELECT count FROM command_cnt WHERE command = cmd); - UPDATE command_cnt SET count = oldcnt + 1 WHERE command = cmd; + declare oldCnt bigint unsigned; + set oldCnt = (SELECT count FROM command_cnt WHERE command = cmd); + UPDATE command_cnt SET count = oldCnt + 1 WHERE command = cmd; END `); console.log('Stored Procedure created'); -console.log('Attempting to create table guild_prefix'); +console.log('Attempting to create table guild_settings'); await dbClient.execute(` - CREATE TABLE guild_prefix ( + CREATE TABLE guild_settings ( guildId bigint unsigned NOT NULL, - prefix char(10) NOT NULL, - PRIMARY KEY (guildid), - UNIQUE KEY guild_prefix_guildid_UNIQUE (guildid) - ) ENGINE=InnoDB DEFAULT CHARSET=utf8; -`); -console.log('Table created'); - -console.log('Attempting to create table guild_mod_role'); -await dbClient.execute(` - CREATE TABLE guild_mod_role ( - guildId bigint unsigned NOT NULL, - roleId bigint unsigned NOT NULL, - PRIMARY KEY (guildid), - UNIQUE KEY guild_mod_role_guildid_UNIQUE (guildid) - ) ENGINE=InnoDB DEFAULT CHARSET=utf8; -`); -console.log('Table created'); - -console.log('Attempting to create table guild_clean_channel'); -await dbClient.execute(` - CREATE TABLE guild_clean_channel ( - guildId bigint unsigned NOT NULL, - channelId bigint unsigned NOT NULL, - PRIMARY KEY (guildid, channelId) + lfgChannelId bigint unsigned NOT NULL, + managerRoleId bigint unsigned NOT NULL, + logChannelId bigint unsigned NOT NULL, + PRIMARY KEY (guildId, lfgChannelId) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; `); console.log('Table created'); diff --git a/db/populateDefaults.ts b/db/populateDefaults.ts index 4eb9fae..31f6dea 100644 --- a/db/populateDefaults.ts +++ b/db/populateDefaults.ts @@ -1,26 +1,15 @@ // This file will populate the tables with default values -import { - // MySQL deps - Client, -} from '../deps.ts'; +import { dbClient } from '../src/db.ts'; -import { LOCALMODE } from '../flags.ts'; -import config from '../config.ts'; - -// Log into the MySQL DB -const dbClient = await new Client().connect({ - hostname: LOCALMODE ? config.db.localhost : config.db.host, - port: config.db.port, - db: config.db.name, - username: config.db.username, - password: config.db.password, -}); - -console.log('Attempting to insert default commands into command_cnt'); -const commands = ['ping', 'help', 'info', 'version', 'report', 'privacy', 'lfg', 'prefix']; -for (const command of commands) { - await dbClient.execute('INSERT INTO command_cnt(command) values(?)', [command]).catch((e) => { +console.log('Attempting to insert default actions into command_cnt'); +const actions = [ + 'cmd-setup', + 'cmd-info', + 'cmd-report', +]; +for (const action of actions) { + await dbClient.execute('INSERT INTO command_cnt(command) values(?)', [action]).catch((e) => { console.log(`Failed to insert into database`, e); }); } diff --git a/deps.ts b/deps.ts index ce53a83..d86169f 100644 --- a/deps.ts +++ b/deps.ts @@ -12,6 +12,7 @@ export { ApplicationCommandFlags, ApplicationCommandOptionTypes, ApplicationCommandTypes, + ChannelTypes, createBot, editBotMember, editBotStatus, @@ -21,6 +22,10 @@ export { sendInteractionResponse, sendMessage, startBot, + OverwriteTypes, + BitwisePermissionFlags, + MessageComponentTypes, + ButtonStyles, } from 'https://deno.land/x/discordeno@17.0.1/mod.ts'; export type { ActionRow, @@ -37,6 +42,7 @@ export type { MakeRequired, Message, PermissionStrings, + DiscordEmbedField, } 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/mod.ts b/mod.ts index bd99686..6aca384 100644 --- a/mod.ts +++ b/mod.ts @@ -18,4 +18,5 @@ enableCacheSweepers(bot); // Start the bot await startBot(bot); +// Announce the slash commands so users can use them await createSlashCommands(bot); diff --git a/src/commandUtils.ts b/src/commandUtils.ts index 9b1b61e..1cf70da 100644 --- a/src/commandUtils.ts +++ b/src/commandUtils.ts @@ -1,6 +1,7 @@ -import { ApplicationCommandFlags } from '../deps.ts'; +import { ApplicationCommandFlags, Bot, Interaction, InteractionResponseTypes } from '../deps.ts'; import config from '../config.ts'; -import { lfgChannels } from './db.ts'; +import { lfgChannelSettings } from './db.ts'; +import utils from './utils.ts'; export const failColor = 0xe71212; export const warnColor = 0xe38f28; @@ -17,8 +18,8 @@ export const getRandomStatus = (guildCount: number): string => { return statuses[Math.floor((Math.random() * statuses.length) + 1)]; }; -export const isLFGChannel = (channelId: bigint) => { - return (lfgChannels.includes(channelId) || channelId === 0n) ? ApplicationCommandFlags.Ephemeral : undefined; +export const isLFGChannel = (guildId: bigint, channelId: bigint) => { + return (lfgChannelSettings.has(`${guildId}-${channelId}`) || channelId === 0n || guildId === 0n) ? ApplicationCommandFlags.Ephemeral : undefined; }; export const generateReport = (msg: string) => ({ @@ -28,3 +29,20 @@ export const generateReport = (msg: string) => ({ description: msg, }], }); + +export const somethingWentWrong = (bot: Bot, interaction: Interaction, errorCode: string) => + bot.helpers.sendInteractionResponse(interaction.id, interaction.token, { + type: InteractionResponseTypes.ChannelMessageWithSource, + data: { + flags: ApplicationCommandFlags.Ephemeral, + embeds: [{ + color: failColor, + title: 'Something went wrong...', + description: 'You should not be able to get here. Please try again and if the issue continues, `/report` this issue to the developers with the error code below.', + fields: [{ + name: 'Error Code:', + value: errorCode, + }], + }], + }, + }).catch((e: Error) => utils.commonLoggers.interactionSendError('commandUtils.ts', interaction, e)); diff --git a/src/commands/_index.ts b/src/commands/_index.ts index 100b155..679332b 100644 --- a/src/commands/_index.ts +++ b/src/commands/_index.ts @@ -1,11 +1,12 @@ import { Bot, CreateApplicationCommand, log, LT, MakeRequired } from '../../deps.ts'; -import { Commands } from '../types/commandTypes.ts'; +import { Command } from '../types/commandTypes.ts'; import utils from '../utils.ts'; import info from './info.ts'; import report from './report.ts'; +import setup from './setup.ts'; -export const commands: Array = [info, report]; +export const commands: Array = [info, report, setup]; export const createSlashCommands = async (bot: Bot) => { const globalCommands: MakeRequired[] = []; diff --git a/src/commands/info.ts b/src/commands/info.ts index 6551a26..12fe25d 100644 --- a/src/commands/info.ts +++ b/src/commands/info.ts @@ -12,14 +12,14 @@ const details: CommandDetails = { }; const execute = (bot: Bot, interaction: Interaction) => { - dbClient.execute(queries.callIncCnt('info')).catch((e) => utils.commonLoggers.dbError('info.ts', 'call sproc INC_CNT on', e)); + dbClient.execute(queries.callIncCnt('cmd-info')).catch((e) => utils.commonLoggers.dbError('info.ts', 'call sproc INC_CNT on', e)); bot.helpers.sendInteractionResponse( interaction.id, interaction.token, { type: InteractionResponseTypes.ChannelMessageWithSource, data: { - flags: isLFGChannel(interaction.channelId || 0n), + flags: isLFGChannel(interaction.guildId || 0n, interaction.channelId || 0n), embeds: [{ color: infoColor2, title: `${config.name}, the LFG bot`, diff --git a/src/commands/report.ts b/src/commands/report.ts index 03cbc9c..3155f58 100644 --- a/src/commands/report.ts +++ b/src/commands/report.ts @@ -1,6 +1,6 @@ import config from '../../config.ts'; import { ApplicationCommandOptionTypes, ApplicationCommandTypes, Bot, Interaction, InteractionResponseTypes, sendMessage } from '../../deps.ts'; -import { generateReport, isLFGChannel, successColor } from '../commandUtils.ts'; +import { generateReport, isLFGChannel, somethingWentWrong, successColor } from '../commandUtils.ts'; import { dbClient, queries } from '../db.ts'; import { CommandDetails } from '../types/commandTypes.ts'; import utils from '../utils.ts'; @@ -22,26 +22,27 @@ const details: CommandDetails = { }; const execute = (bot: Bot, interaction: Interaction) => { - console.log(interaction); - dbClient.execute(queries.callIncCnt('report')).catch((e) => utils.commonLoggers.dbError('report.ts', 'call sproc INC_CNT on', e)); - sendMessage(bot, config.reportChannel, generateReport(interaction.data?.options?.[0].value as string || 'Missing Options')).catch((e: Error) => - utils.commonLoggers.interactionSendError('report.ts:28', interaction, e) - ); - bot.helpers.sendInteractionResponse( - interaction.id, - interaction.token, - { - type: InteractionResponseTypes.ChannelMessageWithSource, - data: { - flags: isLFGChannel(interaction.channelId || 0n), - embeds: [{ - color: successColor, - title: 'Failed command has been reported to my developer.', - description: `For more in depth support, and information about planned maintenance, please join the support server [here](${config.links.supportServer}).`, - }], + dbClient.execute(queries.callIncCnt('cmd-report')).catch((e) => utils.commonLoggers.dbError('report.ts', 'call sproc INC_CNT on', e)); + if (interaction.data?.options?.[0].value) { + sendMessage(bot, config.reportChannel, generateReport(interaction.data.options[0].value as string)).catch((e: Error) => utils.commonLoggers.interactionSendError('report.ts:28', interaction, e)); + bot.helpers.sendInteractionResponse( + interaction.id, + interaction.token, + { + type: InteractionResponseTypes.ChannelMessageWithSource, + data: { + flags: isLFGChannel(interaction.guildId || 0n, interaction.channelId || 0n), + embeds: [{ + color: successColor, + title: 'Failed command has been reported to my developer.', + description: `For more in depth support, and information about planned maintenance, please join the support server [here](${config.links.supportServer}).`, + }], + }, }, - }, - ).catch((e: Error) => utils.commonLoggers.interactionSendError('report.ts:44', interaction, e)); + ).catch((e: Error) => utils.commonLoggers.interactionSendError('report.ts:44', interaction, e)); + } else { + somethingWentWrong(bot, interaction, 'reportMissingAllOptions'); + } }; export default { diff --git a/src/commands/setup.ts b/src/commands/setup.ts new file mode 100644 index 0000000..5e065ca --- /dev/null +++ b/src/commands/setup.ts @@ -0,0 +1,321 @@ +import config from '../../config.ts'; +import { ApplicationCommandFlags, ApplicationCommandOptionTypes, ApplicationCommandTypes, ButtonStyles, Bot, ChannelTypes, Interaction, InteractionResponseTypes, sendMessage, OverwriteTypes, botId, MessageComponentTypes, DiscordEmbedField } from '../../deps.ts'; +import { failColor, infoColor2, somethingWentWrong, successColor } from '../commandUtils.ts'; +import { dbClient, queries, lfgChannelSettings } from '../db.ts'; +import { CommandDetails } from '../types/commandTypes.ts'; +import utils from '../utils.ts'; + +const withoutMgrRole = 'without-manager-role'; +const withMgrRole = 'with-manager-role'; +const managerRoleStr = 'manager-role'; +const logChannelStr = 'log-channel'; + +const details: CommandDetails = { + name: 'setup', + description: `Configures this channel to be a dedicated event channel to be managed by ${config.name}.`, + type: ApplicationCommandTypes.ChatInput, + defaultMemberPermissions: ['ADMINISTRATOR'], + options: [ + { + name: withoutMgrRole, + type: ApplicationCommandOptionTypes.SubCommand, + description: `This will configure ${config.name} without a manager role.`, + }, + { + name: withMgrRole, + type: ApplicationCommandOptionTypes.SubCommand, + description: `This will configure ${config.name} with a manager role.`, + options: [ + { + name: managerRoleStr, + type: ApplicationCommandOptionTypes.Role, + description: 'This role will be allowed to manage all events in this guild.', + required: true, + }, + { + name: logChannelStr, + type: ApplicationCommandOptionTypes.Channel, + description: `This channel is where ${config.name} will send Audit Messages whenever a manager updates an event.`, + required: true, + channelTypes: [ChannelTypes.GuildText], + }, + ], + }, + ], +}; + +const execute = async (bot: Bot, interaction: Interaction) => { + dbClient.execute(queries.callIncCnt('cmd-setup')).catch((e) => utils.commonLoggers.dbError('setup.ts', 'call sproc INC_CNT on', e)); + + const setupOpts = interaction.data?.options?.[0]; + + if (setupOpts && setupOpts.name && interaction.channelId && interaction.guildId) { + if (lfgChannelSettings.has(`${interaction.guildId}-${interaction.channelId}`)) { + // Cannot setup a lfg channel that is already set up + bot.helpers.sendInteractionResponse(interaction.id, interaction.token, { + type: InteractionResponseTypes.ChannelMessageWithSource, + data: { + flags: ApplicationCommandFlags.Ephemeral, + embeds: [{ + color: failColor, + title: 'Unable to setup LFG channel.', + description: 'This channel is already set as an LFG channel. If you need to edit the channel, please run `/delete lfg-channel` in this channel and then run `/setup` again.\n\nThis will not harm any active events in this channel and simply resets the settings for this channel.', + }], + }, + }).catch((e: Error) => utils.commonLoggers.interactionSendError('setup.ts', interaction, e)); + return; + } + + const messages = await bot.helpers.getMessages(interaction.channelId, { limit: 100 }); + if (messages.size < 100) { + let logChannelId = 0n; + let managerRoleId = 0n; + let logChannelErrorOut = false; + let mgrRoleErrorOut = false; + const introFields: Array = [{ + name: 'Editing/Deleting your event:', + value: 'To edit or delete your event, simply click on the ✏️ or 🗑️ buttons respectively.', + }]; + const permissionFields: Array = [ + { + name: `Please make sure ${config.name} has the following permissions:`, + value: '`MANAGE_GUILD`\n`MANAGE_CHANNELS`\n`MANAGE_ROLES`\n`MANAGE_MESSAGES`\n\nThe only permission that is required after setup completes is `MANAGE_MESSAGES`.', + }, + ]; + if (setupOpts.name === withMgrRole) { + introFields.push({ + 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\` + +The Discord Slash Command system will ensure you provide all the required details.`, + }) + if (setupOpts.options?.length) { + setupOpts.options.forEach(opt => { + if (opt.name === managerRoleStr) { + managerRoleId = BigInt(opt.value as string || '0'); + } else if (opt.name === logChannelStr) { + logChannelId = BigInt(opt.value as string || '0'); + } + }); + + if (logChannelId === 0n || managerRoleId === 0n) { + // One or both Ids did not get parsed + bot.helpers.sendInteractionResponse(interaction.id, interaction.token, { + type: InteractionResponseTypes.ChannelMessageWithSource, + data: { + flags: ApplicationCommandFlags.Ephemeral, + embeds: [{ + color: failColor, + title: 'Unable to setup log channel or manager role.', + description: `${config.name} attempted to set the log channel or manager role, but one or both were undefined. Please try again and if the issue continues, \`/report\` this issue to the developers with the error code below.`, + fields: [{ + name: 'Error Code:', + value: `setupLog${logChannelId}Mgr${managerRoleId}`, + }], + }], + }, + }).catch((e: Error) => utils.commonLoggers.interactionSendError('setup.ts', interaction, e)); + return; + } + } else { + // Discord broke? + somethingWentWrong(bot, interaction, 'setupMissingRoleMgrOptions'); + return; + } + + // Test sending a message to the logChannel + await sendMessage(bot, logChannelId, { + embeds: [{ + title: `This is the channel ${config.name} will be logging events to.`, + description: `${config.name} will only send messages here as frequently as your event managers update events.`, + color: infoColor2, + }] + }).catch((e: Error) => { + utils.commonLoggers.messageSendError('setup.ts', 'log-test', e); + logChannelErrorOut = true; + }); + if (logChannelErrorOut) { + // Cannot send message into log channel, error out + bot.helpers.sendInteractionResponse(interaction.id, interaction.token, { + type: InteractionResponseTypes.ChannelMessageWithSource, + data: { + flags: ApplicationCommandFlags.Ephemeral, + embeds: [{ + color: failColor, + title: 'Unable to setup log channel.', + description: `${config.name} attempted to send a message to the specified log channel.`, + fields: [ + { + name: `Please allow ${config.name} to send messages in the requested channel.`, + value: `${config.name}`, + }, + ], + }], + }, + }).catch((e: Error) => utils.commonLoggers.interactionSendError('setup.ts', interaction, e)); + return; + } + + // Set permissions for managerId + await bot.helpers.editChannelPermissionOverrides(interaction.channelId, { + id: managerRoleId, + type: OverwriteTypes.Role, + allow: ['SEND_MESSAGES'], + }).catch((e: Error) => { + utils.commonLoggers.channelUpdateError('setup.ts', 'manager-allow', e); + mgrRoleErrorOut = true; + }); + } + + // Set permissions for everyone, skip if we already failed to set roles + !mgrRoleErrorOut && await bot.helpers.editChannelPermissionOverrides(interaction.channelId, { + id: interaction.guildId, + type: OverwriteTypes.Role, + deny: ['SEND_MESSAGES'], + }).catch((e: Error) => { + utils.commonLoggers.channelUpdateError('setup.ts', 'everyone-deny', e); + mgrRoleErrorOut = true; + }); + + if (mgrRoleErrorOut) { + // Cannot update role overrides on channel, error out + bot.helpers.sendInteractionResponse(interaction.id, interaction.token, { + type: InteractionResponseTypes.ChannelMessageWithSource, + data: { + flags: ApplicationCommandFlags.Ephemeral, + embeds: [{ + color: failColor, + title: 'Unable to set lfg channel permissions.', + description: `${config.name} attempted to update the permissions for the current channel, but could not.`, + fields: permissionFields, + }], + }, + }).catch((e: Error) => utils.commonLoggers.interactionSendError('setup.ts', interaction, e)); + return; + } + + // Delete all messages that are not LFG posts + const msgsToDel: Array = []; + const oldLfgMsgs: Array = [] + messages.forEach(msg => { + if (msg.authorId === botId && msg.embeds.length && msg.embeds[0].footer && msg.embeds[0].footer.text.includes('Created by:')) { + oldLfgMsgs.push(msg.id); + } else { + msgsToDel.push(msg.id); + } + }); + if (msgsToDel.length) { + await bot.helpers.deleteMessages(interaction.channelId, msgsToDel, 'Cleaning LFG Channel').catch((e: Error) => utils.commonLoggers.messageDeleteError('setup.ts', 'bulk-msg-cleanup', e)); + } + + // Retrofit all old LFG posts that we found + if (oldLfgMsgs.length) { + // TODO: Retrofit old LFG posts, should delete ones that have already passed, should begin watching these events + } + + // Store the ids to the db + let dbErrorOut = false; + await dbClient.execute('INSERT INTO guild_settings(guildId,lfgChannelId,managerRoleId,logChannelId) values(?,?,?,?)', [interaction.guildId, interaction.channelId, managerRoleId, logChannelId]).catch((e) => { + utils.commonLoggers.dbError('setup.ts', 'insert into guild_settings', e); + dbErrorOut = true; + }); + if (dbErrorOut) { + // DB died? + somethingWentWrong(bot, interaction, 'setupDBInsertFailed'); + return; + } + // Store the ids to the active map + lfgChannelSettings.set(`${interaction.guildId}-${interaction.channelId}`, { + managed: setupOpts.name === withMgrRole, + managerRoleId, + logChannelId, + }); + + // Send the initial introduction message + const createNewEventBtn = 'Create New Event'; + const introMsg = await sendMessage(bot, interaction.channelId, { + content: `Welcome to <#${interaction.channelId}>, managed by <@${botId}>!`, + embeds: [{ + title: `To get started, click on the '${createNewEventBtn}' button below!`, + color: successColor, + fields: introFields, + }], + components: [{ + type: MessageComponentTypes.ActionRow, + components: [{ + type: MessageComponentTypes.Button, + label: createNewEventBtn, + customId: 'temp', // TODO: set this + style: ButtonStyles.Success, + }], + }], + }).catch((e: Error) => utils.commonLoggers.messageSendError('setup.ts', 'init-msg', e)); + + if (introMsg) { + bot.helpers.pinMessage(interaction.channelId, introMsg.id).catch((e: Error) => utils.commonLoggers.messageSendError('setup.ts', 'pin-init-msg', e)); + // Complete the interaction + bot.helpers.sendInteractionResponse(interaction.id, interaction.token, { + type: InteractionResponseTypes.ChannelMessageWithSource, + data: { + flags: ApplicationCommandFlags.Ephemeral, + embeds: [{ + color: successColor, + title: 'LFG Channel setup complete!', + description: `${config.name} has finished setting up this channel. You may safely dismiss this message.`, + }], + }, + }).catch((e: Error) => utils.commonLoggers.interactionSendError('setup.ts', interaction, e)); + } else { + // Could not send initial message + bot.helpers.sendInteractionResponse(interaction.id, interaction.token, { + type: InteractionResponseTypes.ChannelMessageWithSource, + data: { + flags: ApplicationCommandFlags.Ephemeral, + embeds: [{ + color: failColor, + title: 'Failed to send the initial message!', + fields: permissionFields, + }], + }, + }).catch((e: Error) => utils.commonLoggers.interactionSendError('setup.ts', interaction, e)); + } + } else { + // Too many messages to delete, give up + bot.helpers.sendInteractionResponse(interaction.id, interaction.token, { + type: InteractionResponseTypes.ChannelMessageWithSource, + data: { + flags: ApplicationCommandFlags.Ephemeral, + embeds: [{ + color: failColor, + title: 'Unable to setup LFG channel.', + description: `${config.name} attempted to clean this channel, but encountered too many messages (100 or more). There are two ways to move forward:`, + fields: [ + { + name: 'Is this channel a dedicated LFG Channel?', + value: 'You either need to manually clean this channel or create a brand new channel for events.', + inline: true, + }, + { + name: 'Is this a chat channel that you want events mixed into?', + value: 'You do not need to run the `/setup` command, and instead should use the `/lfg create` command.', + inline: true, + }, + ], + }], + }, + }).catch((e: Error) => utils.commonLoggers.interactionSendError('setup.ts', interaction, e)); + } + } else { + // Discord fucked up? + somethingWentWrong(bot, interaction, 'setupMissingAllOptions'); + } +}; + +export default { + details, + execute, +}; diff --git a/src/db.ts b/src/db.ts index 94e0077..abfa900 100644 --- a/src/db.ts +++ b/src/db.ts @@ -1,6 +1,7 @@ import config from '../config.ts'; import { Client } from '../deps.ts'; import { LOCALMODE } from '../flags.ts'; +import { LfgChannelSetting, DBGuildSettings } from './types/commandTypes.ts'; export const dbClient = await new Client().connect({ hostname: LOCALMODE ? config.db.localhost : config.db.host, @@ -14,4 +15,12 @@ export const queries = { callIncCnt: (cmdName: string) => `CALL INC_CNT("${cmdName}");`, }; -export const lfgChannels: Array = [1055568692697649232n]; +export const lfgChannelSettings: Map = new Map(); +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, + managerRoleId: g.managerRoleId, + logChannelId: g.logChannelId, + }); +}); diff --git a/src/types/commandTypes.ts b/src/types/commandTypes.ts index 65024d4..e35a593 100644 --- a/src/types/commandTypes.ts +++ b/src/types/commandTypes.ts @@ -9,7 +9,20 @@ export type CommandDetails = { defaultMemberPermissions?: PermissionStrings[]; }; -export type Commands = { +export type Command = { details: CommandDetails; execute: Function; }; + +export type LfgChannelSetting = { + managed: boolean; + managerRoleId: bigint; + logChannelId: bigint; +}; + +export type DBGuildSettings = { + guildId: bigint; + lfgChannelId: bigint; + managerRoleId: bigint; + logChannelId: bigint; +}; diff --git a/src/utils.ts b/src/utils.ts index ac26a42..620cc00 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -19,10 +19,14 @@ const reactionAddError = (location: string, message: Message | string, err: Erro genericLogger(LT.ERROR, `${location} | Failed to add emoji (${emoji}) to message: ${jsonStringifyBig(message)} | Error: ${err.name} - ${err.message}`); const reactionDeleteError = (location: string, message: Message | string, err: Error, emoji: string) => genericLogger(LT.ERROR, `${location} | Failed to delete emoji (${emoji}) from message: ${jsonStringifyBig(message)} | Error: ${err.name} - ${err.message}`); +const channelUpdateError = (location: string, message: string, err: Error) => + genericLogger(LT.ERROR, `${location} | Failed to update channel | ${message} | Error: ${err.name} - ${err.message}`); + const dbError = (location: string, type: string, err: Error) => genericLogger(LT.ERROR, `${location} | Failed to ${type} database | Error: ${err.name} - ${err.message}`); export default { commonLoggers: { + channelUpdateError, dbError, interactionSendError, messageGetError, From 79673bdf918a8c37494a73518e760c2a2cee22bf Mon Sep 17 00:00:00 2001 From: "Ean Milligan (Bastion)" Date: Sat, 28 Jan 2023 20:59:39 -0500 Subject: [PATCH 009/110] deno fmt --- deps.ts | 10 +++++----- src/commands/setup.ts | 43 ++++++++++++++++++++++++++++++------------- src/db.ts | 2 +- src/utils.ts | 3 +-- 4 files changed, 37 insertions(+), 21 deletions(-) diff --git a/deps.ts b/deps.ts index d86169f..d254246 100644 --- a/deps.ts +++ b/deps.ts @@ -12,6 +12,8 @@ export { ApplicationCommandFlags, ApplicationCommandOptionTypes, ApplicationCommandTypes, + BitwisePermissionFlags, + ButtonStyles, ChannelTypes, createBot, editBotMember, @@ -19,13 +21,11 @@ export { getBotIdFromToken, Intents, InteractionResponseTypes, + MessageComponentTypes, + OverwriteTypes, sendInteractionResponse, sendMessage, startBot, - OverwriteTypes, - BitwisePermissionFlags, - MessageComponentTypes, - ButtonStyles, } from 'https://deno.land/x/discordeno@17.0.1/mod.ts'; export type { ActionRow, @@ -35,6 +35,7 @@ export type { ButtonComponent, CreateApplicationCommand, CreateMessage, + DiscordEmbedField, Embed, EventHandlers, Guild, @@ -42,7 +43,6 @@ export type { MakeRequired, Message, PermissionStrings, - DiscordEmbedField, } 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/commands/setup.ts b/src/commands/setup.ts index 5e065ca..86d1f0e 100644 --- a/src/commands/setup.ts +++ b/src/commands/setup.ts @@ -1,7 +1,21 @@ import config from '../../config.ts'; -import { ApplicationCommandFlags, ApplicationCommandOptionTypes, ApplicationCommandTypes, ButtonStyles, Bot, ChannelTypes, Interaction, InteractionResponseTypes, sendMessage, OverwriteTypes, botId, MessageComponentTypes, DiscordEmbedField } from '../../deps.ts'; +import { + ApplicationCommandFlags, + ApplicationCommandOptionTypes, + ApplicationCommandTypes, + Bot, + botId, + ButtonStyles, + ChannelTypes, + DiscordEmbedField, + Interaction, + InteractionResponseTypes, + MessageComponentTypes, + OverwriteTypes, + sendMessage, +} from '../../deps.ts'; import { failColor, infoColor2, somethingWentWrong, successColor } from '../commandUtils.ts'; -import { dbClient, queries, lfgChannelSettings } from '../db.ts'; +import { dbClient, lfgChannelSettings, queries } from '../db.ts'; import { CommandDetails } from '../types/commandTypes.ts'; import utils from '../utils.ts'; @@ -59,7 +73,8 @@ const execute = async (bot: Bot, interaction: Interaction) => { embeds: [{ color: failColor, title: 'Unable to setup LFG channel.', - description: 'This channel is already set as an LFG channel. If you need to edit the channel, please run `/delete lfg-channel` in this channel and then run `/setup` again.\n\nThis will not harm any active events in this channel and simply resets the settings for this channel.', + description: + 'This channel is already set as an LFG channel. If you need to edit the channel, please run `/delete lfg-channel` in this channel and then run `/setup` again.\n\nThis will not harm any active events in this channel and simply resets the settings for this channel.', }], }, }).catch((e: Error) => utils.commonLoggers.interactionSendError('setup.ts', interaction, e)); @@ -92,9 +107,9 @@ const execute = async (bot: Bot, interaction: Interaction) => { \`/alternate\` The Discord Slash Command system will ensure you provide all the required details.`, - }) + }); if (setupOpts.options?.length) { - setupOpts.options.forEach(opt => { + setupOpts.options.forEach((opt) => { if (opt.name === managerRoleStr) { managerRoleId = BigInt(opt.value as string || '0'); } else if (opt.name === logChannelStr) { @@ -111,7 +126,8 @@ The Discord Slash Command system will ensure you provide all the required detail embeds: [{ color: failColor, title: 'Unable to setup log channel or manager role.', - description: `${config.name} attempted to set the log channel or manager role, but one or both were undefined. Please try again and if the issue continues, \`/report\` this issue to the developers with the error code below.`, + description: + `${config.name} attempted to set the log channel or manager role, but one or both were undefined. Please try again and if the issue continues, \`/report\` this issue to the developers with the error code below.`, fields: [{ name: 'Error Code:', value: `setupLog${logChannelId}Mgr${managerRoleId}`, @@ -133,7 +149,7 @@ The Discord Slash Command system will ensure you provide all the required detail title: `This is the channel ${config.name} will be logging events to.`, description: `${config.name} will only send messages here as frequently as your event managers update events.`, color: infoColor2, - }] + }], }).catch((e: Error) => { utils.commonLoggers.messageSendError('setup.ts', 'log-test', e); logChannelErrorOut = true; @@ -200,8 +216,8 @@ The Discord Slash Command system will ensure you provide all the required detail // Delete all messages that are not LFG posts const msgsToDel: Array = []; - const oldLfgMsgs: Array = [] - messages.forEach(msg => { + const oldLfgMsgs: Array = []; + messages.forEach((msg) => { if (msg.authorId === botId && msg.embeds.length && msg.embeds[0].footer && msg.embeds[0].footer.text.includes('Created by:')) { oldLfgMsgs.push(msg.id); } else { @@ -219,10 +235,11 @@ The Discord Slash Command system will ensure you provide all the required detail // Store the ids to the db let dbErrorOut = false; - await dbClient.execute('INSERT INTO guild_settings(guildId,lfgChannelId,managerRoleId,logChannelId) values(?,?,?,?)', [interaction.guildId, interaction.channelId, managerRoleId, logChannelId]).catch((e) => { - utils.commonLoggers.dbError('setup.ts', 'insert into guild_settings', e); - dbErrorOut = true; - }); + await dbClient.execute('INSERT INTO guild_settings(guildId,lfgChannelId,managerRoleId,logChannelId) values(?,?,?,?)', [interaction.guildId, interaction.channelId, managerRoleId, logChannelId]) + .catch((e) => { + utils.commonLoggers.dbError('setup.ts', 'insert into guild_settings', e); + dbErrorOut = true; + }); if (dbErrorOut) { // DB died? somethingWentWrong(bot, interaction, 'setupDBInsertFailed'); diff --git a/src/db.ts b/src/db.ts index abfa900..e208905 100644 --- a/src/db.ts +++ b/src/db.ts @@ -1,7 +1,7 @@ import config from '../config.ts'; import { Client } from '../deps.ts'; import { LOCALMODE } from '../flags.ts'; -import { LfgChannelSetting, DBGuildSettings } from './types/commandTypes.ts'; +import { DBGuildSettings, LfgChannelSetting } from './types/commandTypes.ts'; export const dbClient = await new Client().connect({ hostname: LOCALMODE ? config.db.localhost : config.db.host, diff --git a/src/utils.ts b/src/utils.ts index 620cc00..96f11a5 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -19,8 +19,7 @@ const reactionAddError = (location: string, message: Message | string, err: Erro genericLogger(LT.ERROR, `${location} | Failed to add emoji (${emoji}) to message: ${jsonStringifyBig(message)} | Error: ${err.name} - ${err.message}`); const reactionDeleteError = (location: string, message: Message | string, err: Error, emoji: string) => genericLogger(LT.ERROR, `${location} | Failed to delete emoji (${emoji}) from message: ${jsonStringifyBig(message)} | Error: ${err.name} - ${err.message}`); -const channelUpdateError = (location: string, message: string, err: Error) => - genericLogger(LT.ERROR, `${location} | Failed to update channel | ${message} | Error: ${err.name} - ${err.message}`); +const channelUpdateError = (location: string, message: string, err: Error) => genericLogger(LT.ERROR, `${location} | Failed to update channel | ${message} | Error: ${err.name} - ${err.message}`); const dbError = (location: string, type: string, err: Error) => genericLogger(LT.ERROR, `${location} | Failed to ${type} database | Error: ${err.name} - ${err.message}`); From 19d3aa7819a97155d597dbc05b3b75cabe76bc80 Mon Sep 17 00:00:00 2001 From: "Ean Milligan (Bastion)" Date: Sun, 29 Jan 2023 01:28:08 -0500 Subject: [PATCH 010/110] add delete command to reset a lfg channel --- db/populateDefaults.ts | 3 +- src/commands/_index.ts | 3 +- src/commands/delete.ts | 68 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 72 insertions(+), 2 deletions(-) create mode 100644 src/commands/delete.ts diff --git a/db/populateDefaults.ts b/db/populateDefaults.ts index 31f6dea..055a70d 100644 --- a/db/populateDefaults.ts +++ b/db/populateDefaults.ts @@ -4,9 +4,10 @@ import { dbClient } from '../src/db.ts'; console.log('Attempting to insert default actions into command_cnt'); const actions = [ - 'cmd-setup', + 'cmd-delete', 'cmd-info', 'cmd-report', + 'cmd-setup', ]; for (const action of actions) { await dbClient.execute('INSERT INTO command_cnt(command) values(?)', [action]).catch((e) => { diff --git a/src/commands/_index.ts b/src/commands/_index.ts index 679332b..d090a80 100644 --- a/src/commands/_index.ts +++ b/src/commands/_index.ts @@ -5,8 +5,9 @@ import utils from '../utils.ts'; import info from './info.ts'; import report from './report.ts'; import setup from './setup.ts'; +import deleteCmd from './delete.ts'; -export const commands: Array = [info, report, setup]; +export const commands: Array = [deleteCmd, info, report, setup]; export const createSlashCommands = async (bot: Bot) => { const globalCommands: MakeRequired[] = []; diff --git a/src/commands/delete.ts b/src/commands/delete.ts new file mode 100644 index 0000000..7a41ca4 --- /dev/null +++ b/src/commands/delete.ts @@ -0,0 +1,68 @@ +import config from '../../config.ts'; +import { ApplicationCommandFlags, ApplicationCommandTypes, Bot, Interaction, InteractionResponseTypes } from '../../deps.ts'; +import { failColor, somethingWentWrong, successColor } from '../commandUtils.ts'; +import { dbClient, lfgChannelSettings, queries } from '../db.ts'; +import { CommandDetails } from '../types/commandTypes.ts'; +import utils from '../utils.ts'; + +const details: CommandDetails = { + name: 'delete-lfg-channel', + description: `Removes all settings from ${config.name} related to this LFG channel. Events will not be deleted.`, + type: ApplicationCommandTypes.ChatInput, + defaultMemberPermissions: ['ADMINISTRATOR'], +}; + +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}`)) { + // Cannot delete a lfg channel that has not been set up + bot.helpers.sendInteractionResponse(interaction.id, interaction.token, { + type: InteractionResponseTypes.ChannelMessageWithSource, + data: { + flags: ApplicationCommandFlags.Ephemeral, + embeds: [{ + color: failColor, + title: 'Unable to delete LFG channel.', + description: + 'This channel is already is not an LFG channel. If you need to setup the channel, please run `/setup` in this channel.\n\nThis will not harm any active events in this channel and simply resets the settings for this channel.', + }], + }, + }).catch((e: Error) => utils.commonLoggers.interactionSendError('delete.ts', interaction, e)); + return; + } + + // Remove it from the DB + let dbError = false; + await dbClient.execute('DELETE FROM guild_settings WHERE guildId = ? AND lfgChannelId = ?', [interaction.guildId, interaction.channelId]).catch((e) => { + utils.commonLoggers.dbError('delete.ts', 'delete guild/lfgChannel', e); + dbError = true; + }); + if (dbError) { + somethingWentWrong(bot, interaction, 'deleteDBDeleteFail'); + return; + } + lfgChannelSettings.delete(`${interaction.guildId}-${interaction.channelId}`); + + // Complete the interaction + bot.helpers.sendInteractionResponse(interaction.id, interaction.token, { + type: InteractionResponseTypes.ChannelMessageWithSource, + data: { + flags: ApplicationCommandFlags.Ephemeral, + embeds: [{ + color: successColor, + title: 'LFG Channel settings removed!', + description: `${config.name} has finished removing the settings for this channel. You may safely dismiss this message.`, + }], + }, + }).catch((e: Error) => utils.commonLoggers.interactionSendError('delete.ts', interaction, e)); + } else { + somethingWentWrong(bot, interaction, 'deleteMissingGuildIdChannelId'); + } +}; + +export default { + details, + execute, +}; From 23f1a3bd45d75c07fadae8301bd795bf95c04c7a Mon Sep 17 00:00:00 2001 From: "Ean Milligan (Bastion)" Date: Sun, 29 Jan 2023 01:51:49 -0500 Subject: [PATCH 011/110] don't need generateReport --- src/commandUtils.ts | 10 +--------- src/commands/report.ts | 10 ++++++++-- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/src/commandUtils.ts b/src/commandUtils.ts index 1cf70da..05e61f7 100644 --- a/src/commandUtils.ts +++ b/src/commandUtils.ts @@ -15,21 +15,13 @@ export const getRandomStatus = (guildCount: number): string => { `${config.prefix}info to learn more`, `Running LFGs in ${guildCount} servers`, ]; - return statuses[Math.floor((Math.random() * statuses.length) + 1)]; + return statuses[Math.floor(Math.random() * statuses.length)]; }; export const isLFGChannel = (guildId: bigint, channelId: bigint) => { return (lfgChannelSettings.has(`${guildId}-${channelId}`) || channelId === 0n || guildId === 0n) ? ApplicationCommandFlags.Ephemeral : undefined; }; -export const generateReport = (msg: string) => ({ - embeds: [{ - color: infoColor2, - title: 'USER REPORT:', - description: msg, - }], -}); - export const somethingWentWrong = (bot: Bot, interaction: Interaction, errorCode: string) => bot.helpers.sendInteractionResponse(interaction.id, interaction.token, { type: InteractionResponseTypes.ChannelMessageWithSource, diff --git a/src/commands/report.ts b/src/commands/report.ts index 3155f58..6591ff7 100644 --- a/src/commands/report.ts +++ b/src/commands/report.ts @@ -1,6 +1,6 @@ import config from '../../config.ts'; import { ApplicationCommandOptionTypes, ApplicationCommandTypes, Bot, Interaction, InteractionResponseTypes, sendMessage } from '../../deps.ts'; -import { generateReport, isLFGChannel, somethingWentWrong, successColor } from '../commandUtils.ts'; +import { infoColor2, isLFGChannel, somethingWentWrong, successColor } from '../commandUtils.ts'; import { dbClient, queries } from '../db.ts'; import { CommandDetails } from '../types/commandTypes.ts'; import utils from '../utils.ts'; @@ -24,7 +24,13 @@ const details: CommandDetails = { const execute = (bot: Bot, interaction: Interaction) => { dbClient.execute(queries.callIncCnt('cmd-report')).catch((e) => utils.commonLoggers.dbError('report.ts', 'call sproc INC_CNT on', e)); if (interaction.data?.options?.[0].value) { - sendMessage(bot, config.reportChannel, generateReport(interaction.data.options[0].value as string)).catch((e: Error) => utils.commonLoggers.interactionSendError('report.ts:28', interaction, e)); + sendMessage(bot, config.reportChannel, { + embeds: [{ + color: infoColor2, + title: 'USER REPORT:', + description: interaction.data.options[0].value as string, + }], + }).catch((e: Error) => utils.commonLoggers.interactionSendError('report.ts:28', interaction, e)); bot.helpers.sendInteractionResponse( interaction.id, interaction.token, From d65a8e0cb348c55247bd71a281e340a95d83cffc Mon Sep 17 00:00:00 2001 From: "Ean Milligan (Bastion)" Date: Sun, 29 Jan 2023 04:57:26 -0500 Subject: [PATCH 012/110] add activities --- src/lfg-engine/creation/activities.ts | 185 ++++++++++++++++++++++++++ 1 file changed, 185 insertions(+) create mode 100644 src/lfg-engine/creation/activities.ts diff --git a/src/lfg-engine/creation/activities.ts b/src/lfg-engine/creation/activities.ts new file mode 100644 index 0000000..16eacd2 --- /dev/null +++ b/src/lfg-engine/creation/activities.ts @@ -0,0 +1,185 @@ +export type LFGActivity = { + name: string; + maxMembers?: number; + options?: Array; +}; + +export const LFGActivities: Array = [ + { + name: 'Destiny 2', + options: [ + { + name: 'Raids', + options: [ + { + name: 'King\'s Fall', + maxMembers: 6, + }, + { + name: 'Vow of the Disciple', + maxMembers: 6, + }, + { + name: 'Vault of Glass', + maxMembers: 6, + }, + { + name: 'Deep Stone Crypt', + maxMembers: 6, + }, + { + name: 'Garden of Salvation', + maxMembers: 6, + }, + { + name: 'Last Wish', + maxMembers: 6, + }, + ], + }, + { + name: 'Dungeons', + options: [ + { + name: 'Spire of the Watcher', + maxMembers: 3, + }, + { + name: 'Duality', + maxMembers: 3, + }, + { + name: 'Grasp of Avarice', + maxMembers: 3, + }, + { + name: 'Prophecy', + maxMembers: 3, + }, + { + name: 'Pit of Heresy', + maxMembers: 3, + }, + { + name: 'Shattered Throne', + maxMembers: 3, + }, + ], + }, + { + name: 'Crucible', + options: [ + { + name: 'Team Quickplay', + maxMembers: 6, + }, + { + name: 'Competitive', + maxMembers: 3, + }, + { + name: 'Iron Banner', + maxMembers: 6, + }, + { + name: 'Trials of Osiris', + maxMembers: 3, + }, + { + name: 'Private Match', + maxMembers: 12, + }, + ], + }, + { + name: 'Gambit', + options: [ + { + name: 'Classic', + maxMembers: 4, + }, + { + name: 'Private Match', + maxMembers: 8, + }, + ], + }, + { + name: 'Vanguard', + options: [ + { + name: 'Vanguard Ops', + maxMembers: 3, + }, + { + name: 'Nightfall', + maxMembers: 3, + }, + { + name: 'Grandmaster Nightfall', + maxMembers: 3, + }, + ], + }, + { + name: 'Exotic Missions', + options: [ + { + name: 'Operation: Seraph\'s Shield', + maxMembers: 3, + }, + ], + }, + { + name: 'Miscellaneous', + options: [ + { + name: 'Heist Battlegrounds', + maxMembers: 3, + }, + { + name: 'Ketchrash', + maxMembers: 6, + }, + { + name: 'Expedition', + maxMembers: 3, + }, + { + name: 'Weekly Witch Queen Campaign Mission', + maxMembers: 3, + }, + { + name: 'Wellspring', + maxMembers: 6, + }, + { + name: 'Dares of Eternity', + maxMembers: 6, + }, + { + name: 'Wrathborn Hunt', + maxMembers: 3, + }, + { + name: 'Empire Hunt', + maxMembers: 3, + }, + ], + }, + ], + }, + { + name: 'Among Us', + options: [ + { + name: 'Vanilla', + maxMembers: 15, + }, + { + name: 'Modded', + maxMembers: 15, + }, + ], + }, +]; From 5303a202343714b48891c0c49a562dbf0173379a Mon Sep 17 00:00:00 2001 From: "Ean Milligan (Bastion)" Date: Mon, 30 Jan 2023 01:17:22 -0500 Subject: [PATCH 013/110] slight file reorg, start work on event creation --- .../event-creation}/activities.ts | 7 +++--- src/buttons/event-creation/step1.ts | 25 +++++++++++++++++++ 2 files changed, 29 insertions(+), 3 deletions(-) rename src/{lfg-engine/creation => buttons/event-creation}/activities.ts (94%) create mode 100644 src/buttons/event-creation/step1.ts diff --git a/src/lfg-engine/creation/activities.ts b/src/buttons/event-creation/activities.ts similarity index 94% rename from src/lfg-engine/creation/activities.ts rename to src/buttons/event-creation/activities.ts index 16eacd2..1581a6b 100644 --- a/src/lfg-engine/creation/activities.ts +++ b/src/buttons/event-creation/activities.ts @@ -1,10 +1,11 @@ -export type LFGActivity = { +// Activity should either have maxMembers or options specified, NOT both +export type Activity = { name: string; maxMembers?: number; - options?: Array; + options?: Array; }; -export const LFGActivities: Array = [ +export const Activities: Array = [ { name: 'Destiny 2', options: [ diff --git a/src/buttons/event-creation/step1.ts b/src/buttons/event-creation/step1.ts new file mode 100644 index 0000000..f6f901d --- /dev/null +++ b/src/buttons/event-creation/step1.ts @@ -0,0 +1,25 @@ +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 From d712b116b65ef3ed552dbd3dd08faae009fd0bdf Mon Sep 17 00:00:00 2001 From: "Ean Milligan (Bastion)" Date: Tue, 31 Jan 2023 03:32:53 -0500 Subject: [PATCH 014/110] 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