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