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)}`); - } - }, - }, -});