diff --git a/.gitignore b/.gitignore index d60cbd8..3ffa028 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ config.ts logs + +db/update.ts diff --git a/.vscode/settings.json b/.vscode/settings.json index 5b1cbe2..e701d78 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -8,5 +8,8 @@ "spellright.language": [ "en" ], - "spellright.documentTypes": [] + "spellright.documentTypes": [], + "deno.suggest.imports.hosts": { + "https://deno.land": true + } } \ No newline at end of file diff --git a/config.example.ts b/config.example.ts index 6217bf8..c288727 100644 --- a/config.example.ts +++ b/config.example.ts @@ -1,9 +1,9 @@ export const config = { "name": "Group Up", // Name of the bot - "version": "0.2.3", // Version of the bot + "version": "0.3.2", // Version of the bot "token": "the_bot_token", // Discord API Token for this bot "localtoken": "local_testing_token", // Discord API Token for a secondary OPTIONAL testing bot, THIS MUST BE DIFFERENT FROM "token" - "prefix": "[[", // Prefix for all commands + "prefix": "gu!", // Prefix for all commands "db": { // Settings for the MySQL database, this is required for use with the API, if you do not want to set this up, you will need to rip all code relating to the DB out of the bot "host": "", // IP address for the db, usually localhost "localhost": "", // IP address for a secondary OPTIONAL local testing DB, usually also is localhost, but depends on your dev environment diff --git a/db/initialize.ts b/db/initialize.ts index 72f92d1..9eaf13a 100644 --- a/db/initialize.ts +++ b/db/initialize.ts @@ -26,6 +26,8 @@ console.log("Attempt to drop all tables"); await dbClient.execute(`DROP PROCEDURE IF EXISTS INC_CNT;`); await dbClient.execute(`DROP TABLE IF EXISTS command_cnt;`); await dbClient.execute(`DROP TABLE IF EXISTS guild_prefix;`); +await dbClient.execute(`DROP TABLE IF EXISTS guild_mod_role;`); +await dbClient.execute(`DROP TABLE IF EXISTS guild_clean_channel;`); console.log("Tables dropped"); console.log("Attempting to create table command_cnt"); @@ -63,5 +65,26 @@ await dbClient.execute(` `); console.log("Table created"); +console.log("Attempting to create table guild_mod_role"); +await dbClient.execute(` + CREATE TABLE guild_mod_role ( + guildId bigint unsigned NOT NULL, + roleId bigint unsigned NOT NULL, + PRIMARY KEY (guildid), + UNIQUE KEY guild_mod_role_guildid_UNIQUE (guildid) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8; +`); +console.log("Table created"); + +console.log("Attempting to create table guild_clean_channel"); +await dbClient.execute(` + CREATE TABLE guild_clean_channel ( + guildId bigint unsigned NOT NULL, + channelId bigint unsigned NOT NULL, + PRIMARY KEY (guildid, channelId) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8; +`); +console.log("Table created"); + await dbClient.close(); console.log("Done!"); diff --git a/deps.ts b/deps.ts index 4769c94..854f4de 100644 --- a/deps.ts +++ b/deps.ts @@ -1,18 +1,18 @@ // All external dependancies are to be loaded here to make updating dependancy versions much easier export { startBot, editBotStatus, editBotNickname, - Intents, DiscordActivityTypes, DiscordButtonStyles, DiscordInteractionTypes, + Intents, DiscordActivityTypes, DiscordButtonStyles, DiscordInteractionTypes, DiscordInteractionResponseTypes, sendMessage, sendDirectMessage, sendInteractionResponse, getMessage, deleteMessage, + getGuild, getUser, hasGuildPermissions, - cache, botId, structures -} from "https://deno.land/x/discordeno@11.0.0-rc.5/mod.ts"; + cache, botId, structures, cacheHandlers +} from "https://deno.land/x/discordeno@12.0.1/mod.ts"; export type { - DiscordenoMessage, DiscordenoMember, DiscordenoGuild, CreateMessage, Interaction, ButtonComponent, ActionRow, EmbedField -} from "https://deno.land/x/discordeno@11.0.0-rc.5/mod.ts"; // https://deno.land/x/discordeno@11.0.0-rc.5/mod.ts + DiscordenoMessage, DiscordenoMember, DiscordenoGuild, ButtonData, DebugArg, + CreateMessage, Interaction, ButtonComponent, ActionRow, Embed, EmbedField +} from "https://deno.land/x/discordeno@12.0.1/mod.ts"; -export { Client } from "https://deno.land/x/mysql@v2.9.0/mod.ts"; +export { Client } from "https://deno.land/x/mysql@v2.10.1/mod.ts"; export { LogTypes as LT, initLog, log } from "https://raw.githubusercontent.com/Burn-E99/Log4Deno/V1.1.0/mod.ts"; - -export { nanoid } from "https://deno.land/x/nanoid@v3.0.0/mod.ts"; diff --git a/mod.ts b/mod.ts index 8deef07..c54dbd9 100644 --- a/mod.ts +++ b/mod.ts @@ -2,10 +2,12 @@ import { // Discordeno deps startBot, editBotStatus, editBotNickname, Intents, DiscordActivityTypes, DiscordButtonStyles, DiscordInteractionTypes, - sendMessage, sendInteractionResponse, deleteMessage, getMessage, + sendMessage, sendInteractionResponse, deleteMessage, getMessage, sendDirectMessage, getGuild, getUser, hasGuildPermissions, - cache, botId, structures, - DiscordenoMessage, DiscordenoGuild, Interaction, ButtonComponent, ActionRow, + cache, botId, DebugArg, cacheHandlers, + DiscordenoMessage, DiscordenoGuild, + ButtonComponent, ActionRow, ButtonData, + Embed, DiscordInteractionResponseTypes, // MySQL Driver deps Client, @@ -14,12 +16,13 @@ import { LT, initLog, log } from "./deps.ts"; -import { BuildingLFG, ActiveLFG, GuildPrefixes, ButtonData } from "./src/mod.d.ts"; +import { BuildingLFG, ActiveLFG, GuildPrefixes, GuildModRoles, GuildCleanChannels } 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 } from "./src/lfgHandlers.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"; @@ -43,12 +46,7 @@ setInterval(() => { intervals.buildingTimeout(activeBuilders); }, 1000); -const activeLFGPosts: Array = JSON.parse(localStorage.getItem("activeLFGPosts") || "[]", (_key, value) => { - if (typeof value === "string" && /^\d+n$/.test(value)) { - return BigInt(value.substr(0, value.length - 1)); - } - return value; -}); +const activeLFGPosts: Array = jsonParseBig(localStorage.getItem("activeLFGPosts") || "[]"); log(LT.INFO, `Loaded ${activeLFGPosts.length} activeLFGPosts`); setInterval(() => { intervals.lfgNotifier(activeLFGPosts); @@ -60,6 +58,20 @@ 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 @@ -79,20 +91,21 @@ startBot({ }); // Interval to rotate the status text every 30 seconds to show off more commands - setInterval(() => { + 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(), + name: intervals.getRandomStatus(cachedCount), type: DiscordActivityTypes.Game, createdAt: new Date().getTime() }], status: "online" }); } catch (e) { - log(LT.ERROR, `Failed to update status: ${JSON.stringify(e)}`); + log(LT.ERROR, `Failed to update status: ${jsonStringifyBig(e)}`); } }, 30000); @@ -115,23 +128,32 @@ startBot({ status: "online" }); sendMessage(config.logChannel, `${config.name} has started, running version ${config.version}.`).catch(e => { - log(LT.ERROR, `Failed to send message: ${JSON.stringify(e)}`); + log(LT.ERROR, `Failed to send message: ${jsonStringifyBig(e)}`); }); }, 1000); }, guildCreate: (guild: DiscordenoGuild) => { - log(LT.LOG, `Handling joining guild ${JSON.stringify(guild)}`); + 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: ${JSON.stringify(e)}`); + log(LT.ERROR, `Failed to send message: ${jsonStringifyBig(e)}`); }); }, - guildDelete: (guild: DiscordenoGuild) => { - log(LT.LOG, `Handling leaving guild ${JSON.stringify(guild)}`); + 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: ${JSON.stringify(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, data) => log(LT.LOG, `Debug Message | ${JSON.stringify(dmsg)} | ${JSON.stringify(data)}`, false), + 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; @@ -144,12 +166,12 @@ startBot({ if (message.mentionedUserIds[0] === 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: ${JSON.stringify(e)}`); + log(LT.ERROR, `Failed to call stored procedure INC_CNT: ${jsonStringifyBig(e)}`); }); if (message.content.trim() === `<@!${botId}>`) { message.send({ - embed: { + embeds: [{ title: `Hello ${message.member?.username}, and thanks for using Group Up!`, fields: [ { @@ -157,9 +179,9 @@ startBot({ value: "Mention me with a new prefix to change it." } ] - } + }] }).catch(e =>{ - log(LT.WARN, `Failed to send message | ${JSON.stringify(e)}`); + log(LT.WARN, `Failed to send message | ${jsonStringifyBig(e)}`); }); } @@ -171,13 +193,13 @@ startBot({ 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: ${JSON.stringify(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: ${JSON.stringify(e)}`); + log(LT.ERROR, `Failed to insert into database: ${jsonStringifyBig(e)}`); success = false; }); } @@ -185,43 +207,43 @@ startBot({ if (success) { guildPrefixes.set(message.guildId, newPrefix); message.send({ - embed: { + 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 | ${JSON.stringify(e)}`); + log(LT.WARN, `Failed to send message | ${jsonStringifyBig(e)}`); }); } else { message.send({ - embed: { + 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 | ${JSON.stringify(e)}`); + log(LT.WARN, `Failed to send message | ${jsonStringifyBig(e)}`); }); } } else { message.send({ - embed: { + 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 | ${JSON.stringify(e)}`); + log(LT.WARN, `Failed to send message | ${jsonStringifyBig(e)}`); }); } } @@ -257,13 +279,13 @@ startBot({ await activeBuilders[activeIdx].lfgMsg.edit({ content: "", - embed: { + embeds: [{ fields: memberJoined.embed, footer: { text: `Created by: ${message.member.username} | ${newLfgUid}`, }, timestamp: newTimestamp.toISOString() - }, + }], components: [ { type: 1, @@ -272,8 +294,7 @@ startBot({ type: 2, label: "Join", customId: "active@join_group", - style: DiscordButtonStyles.Success, - disabled: currentMembers >= maxMembers + style: DiscordButtonStyles.Success }, { type: 2, @@ -291,36 +312,63 @@ startBot({ } ] }).catch(e =>{ - log(LT.WARN, `Failed to edit message | ${JSON.stringify(e)}`); + log(LT.WARN, `Failed to edit message | ${jsonStringifyBig(e)}`); }); - 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", JSON.stringify(activeLFGPosts, (_key, value) => - typeof value === "bigint" ? value.toString() + "n" : value - )); + 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 | ${JSON.stringify(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 | ${JSON.stringify(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 ${JSON.stringify(message)}`); + log(LT.LOG, `Handling message ${jsonStringifyBig(message)}`); // Split into standard command + args format const args = message.content.slice(prefix.length).trim().split(/[ \n]+/g); @@ -333,23 +381,23 @@ startBot({ 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: ${JSON.stringify(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({ - embed: { + embeds: [{ title: "Ping?" - } + }] }); m.edit({ - embed: { + embeds: [{ title: `Pong! Latency is ${m.timestamp - message.timestamp}ms.` - } + }] }); } catch (e) { - log(LT.ERROR, `Failed to send message: ${JSON.stringify(message)} | ${JSON.stringify(e)}`); + log(LT.ERROR, `Failed to send message: ${jsonStringifyBig(message)} | ${jsonStringifyBig(e)}`); } } @@ -358,16 +406,16 @@ startBot({ 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: ${JSON.stringify(e)}`); + log(LT.ERROR, `Failed to call stored procedure INC_CNT: ${jsonStringifyBig(e)}`); }); - const subcmd = args[0].toLowerCase() || "help"; + 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: ${JSON.stringify(message)} | ${JSON.stringify(e)}`); + log(LT.ERROR, `Failed to send message: ${jsonStringifyBig(message)} | ${jsonStringifyBig(e)}`); }); } @@ -422,10 +470,12 @@ startBot({ editing: false }); - message.delete(); + message.delete().catch(e =>{ + log(LT.WARN, `Failed to delete message | ${jsonStringifyBig(e)}`); + }); } catch (e) { - log(LT.WARN, `LFG failed at step | create | ${JSON.stringify(e)}`); + log(LT.WARN, `LFG failed at step | create | ${jsonStringifyBig(e)}`); } } @@ -438,29 +488,35 @@ startBot({ // Found one, delete if (matches.length) { - await deleteMessage(matches[0].channelId, matches[0].messageId, "User requested LFG to be deleted."); + 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", JSON.stringify(activeLFGPosts, (_key, value) => - typeof value === "bigint" ? value.toString() + "n" : value - )); + localStorage.setItem("activeLFGPosts", jsonStringifyBig(activeLFGPosts)); const m = await message.send(constantCmds.lfgDelete3); - setTimeout(() => { - m.delete(); - message.delete(); - }, 5000); + + 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); - setTimeout(() => { - m.delete(); - message.delete(); - }, 5000); + + 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)}`); + }); } } @@ -470,52 +526,61 @@ startBot({ // Found one, delete if (matches.length === 1) { - await deleteMessage(matches[0].channelId, matches[0].messageId, "User requested LFG to be deleted."); + 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", JSON.stringify(activeLFGPosts, (_key, value) => - typeof value === "bigint" ? value.toString() + "n" : value - )); + localStorage.setItem("activeLFGPosts", jsonStringifyBig(activeLFGPosts)); const m = await message.send(constantCmds.lfgDelete3); - setTimeout(() => { - m.delete(); - message.delete(); - }, 5000); + + 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.embed.fields[0].value; + const deepCloningFailedSoThisIsTheSolution = constantCmds.lfgDelete2.embeds[0].fields[0].value; matches.forEach(mt => { - deleteMsg.embed.fields[0].value += `[${mt.lfgUid}](https://discord.com/channels/${message.guildId}/${mt.channelId}/${mt.messageId})\n` + deleteMsg.embeds[0].fields[0].value += `[${mt.lfgUid}](https://discord.com/channels/${message.guildId}/${mt.channelId}/${mt.messageId})\n` }); - deleteMsg.embed.fields[0].value += "\nThis message will self descruct in 30 seconds." + deleteMsg.embeds[0].fields[0].value += "\nThis message will self descruct in 30 seconds." const m = await message.send(deleteMsg); - constantCmds.lfgDelete2.embed.fields[0].value = deepCloningFailedSoThisIsTheSolution; - setTimeout(() => { - m.delete(); - message.delete(); - }, 30000); + 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); - setTimeout(() => { - m.delete(); - message.delete(); - }, 5000); + + 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 | ${JSON.stringify(e)}`); + log(LT.WARN, `LFG failed at step | delete | ${jsonStringifyBig(e)}`); } } @@ -550,16 +615,21 @@ startBot({ editing: true }); - message.delete(); + 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); - setTimeout(() => { - m.delete(); - message.delete(); - }, 5000); + + 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)}`); + }); } } @@ -591,39 +661,357 @@ startBot({ editing: true }); - message.delete(); + 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.embed.fields[0].value; + const deepCloningFailedSoThisIsTheSolution = constantCmds.lfgEdit2.embeds[0].fields[0].value; matches.forEach(mt => { - deleteMsg.embed.fields[0].value += `[${mt.lfgUid}](https://discord.com/channels/${message.guildId}/${mt.channelId}/${mt.messageId})\n` + deleteMsg.embeds[0].fields[0].value += `[${mt.lfgUid}](https://discord.com/channels/${message.guildId}/${mt.channelId}/${mt.messageId})\n` }); - deleteMsg.embed.fields[0].value += "\nThis message will self descruct in 30 seconds." + deleteMsg.embeds[0].fields[0].value += "\nThis message will self descruct in 30 seconds." const m = await message.send(deleteMsg); - constantCmds.lfgEdit2.embed.fields[0].value = deepCloningFailedSoThisIsTheSolution; - setTimeout(() => { - m.delete(); - message.delete(); - }, 30000); + 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); - setTimeout(() => { - m.delete(); - message.delete(); - }, 5000); + + 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 | ${JSON.stringify(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)}`); + }); } } } @@ -633,14 +1021,14 @@ startBot({ 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: ${JSON.stringify(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: ${JSON.stringify(message)} | ${JSON.stringify(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: ${JSON.stringify(message)} | ${JSON.stringify(e)}`); + log(LT.ERROR, `Failed to send message: ${jsonStringifyBig(message)} | ${jsonStringifyBig(e)}`); }); } @@ -649,11 +1037,11 @@ startBot({ 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: ${JSON.stringify(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: ${JSON.stringify(message)} | ${JSON.stringify(e)}`); + log(LT.ERROR, `Failed to send message: ${jsonStringifyBig(message)} | ${jsonStringifyBig(e)}`); }); } @@ -662,11 +1050,11 @@ startBot({ 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: ${JSON.stringify(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: ${JSON.stringify(message)} | ${JSON.stringify(e)}`); + log(LT.ERROR, `Failed to send message: ${jsonStringifyBig(message)} | ${jsonStringifyBig(e)}`); }); } @@ -675,189 +1063,207 @@ startBot({ 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: ${JSON.stringify(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: ${JSON.stringify(message)} | ${JSON.stringify(e)}`); + log(LT.ERROR, `Failed to send message: ${jsonStringifyBig(message)} | ${jsonStringifyBig(e)}`); }); } }, - interactionCreate: async (interact: Interaction) => { - if (interact.type === DiscordInteractionTypes.MessageComponent) { - if (interact.message && interact.data && (interact.data as ButtonData).customId && interact.member) { - log(LT.INFO, `Handling Button ${(interact.data as ButtonData).customId}`); - log(LT.LOG, `Button Data | ${JSON.stringify(interact)}`); + 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: 6 - }); + 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) && interact.member && x.userId === BigInt(interact.member.user.id)) { - x.lastTouch = new Date(); - x = await handleLFGStep(x, value); + 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") { - const member = await structures.createDiscordenoMember(interact.member, BigInt(interact.guildId)); - 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)]; + 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 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; + const buttonRow: ActionRow = x.lfgMsg.components[0] as ActionRow; - (buttonRow.components[0] as ButtonComponent).disabled = currentMembers >= maxMembers; - 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}`; + 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; } - await x.lfgMsg.edit({ - content: "", - embed: { - 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; - - await activeBuilders[i].questionMsg.delete() - activeBuilders.splice(i, 1); - } else { - activeBuilders[i] = x; + return true; } - - return true; - } - }); - break; - } - case "active": { - const member = await structures.createDiscordenoMember(interact.member, BigInt(interact.guildId)); - const message = await getMessage(BigInt(interact.channelId), BigInt(interact.message.id)); - - const embeds = message.embeds[0].fields || []; - let results: JoinLeaveType = { - embed: [], - success: false, - full: true - }; - 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) { - const buttonRow: ActionRow = message.components[0] as ActionRow; - - (buttonRow.components[0] as ButtonComponent).disabled = results.full; - - await message.edit({ - embed: { - fields: results.embed, - footer: message.embeds[0].footer, - timestamp: message.embeds[0].timestamp - }, - components: [buttonRow] }); + break; } + case "active": { + const message = await getMessage(BigInt(interact.channelId || "0"), BigInt(interact.message.id)); - break; - } - case "editing": { - await activeBuilders.some(async (x, i) => { - if (x.editing && x.channelId === BigInt(interact.channelId) && interact.member && x.userId === BigInt(interact.member.user.id)) { - x.step = action; - x.lastTouch = new Date(); - let nextQuestion = ""; - const nextComponents: Array = []; - switch (action) { - case "set_game": { - nextQuestion = lfgStepQuestions.set_game; + 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; + } - 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 + if (results.success && message.components) { + await message.edit({ + embeds: [{ + fields: results.embed, + footer: message.embeds[0].footer, + timestamp: message.embeds[0].timestamp + }], }); - activeBuilders[i] = x; - - return true; + 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; + + 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; } - default: - break; } } } + catch(e) { + log(LT.ERROR, `Interaction failed: ${jsonStringifyBig(interact)} | ${jsonStringifyBig(member)} | ${jsonStringifyBig(e)}`); + } } } }); diff --git a/src/constantCmds.ts b/src/constantCmds.ts index a351562..e4dd96b 100644 --- a/src/constantCmds.ts +++ b/src/constantCmds.ts @@ -6,7 +6,7 @@ import config from "../config.ts"; export const constantCmds = { help: { - embed: { + embeds: [{ title: `${config.name} Help`, fields: [ { @@ -32,10 +32,10 @@ export const constantCmds = { ` } ] - } + }] }, lfgHelp: { - embed: { + embeds: [{ title: `${config.name} LFG Help`, fields: [ { @@ -74,10 +74,10 @@ export const constantCmds = { inline: true } ] - } + }] }, info: { - embed: { + embeds: [{ fields: [ { name: "Group Up, the LFG bot", @@ -86,67 +86,67 @@ export const constantCmds = { Need help with this bot? Join my support server [here](https://discord.gg/peHASXMZYv).` } ] - } + }] }, version: { - embed: { + embeds: [{ title: `My current version is ${config.version}` - } + }] }, report: { - embed: { + embeds: [{ fields: [ { name: "Failed command has been reported to my developer.", value: "For more in depth support, and information about planned maintenance, please join the support server [here](https://discord.gg/peHASXMZYv)." } ] - } + }] }, lfgDelete1: { - embed: { + embeds: [{ fields: [ { name: "Could not find any LFGs to delete.", value: "Make sure you are the owner of the LFG and are running this command in the same channel as the LFG" } ] - } + }] }, lfgDelete2: { - embed: { + embeds: [{ fields: [ { name: `Multiple LFGs found, please run this command again with the two character ID of the LFG you wish to delete.\n\nExample: \`${config.prefix}lfg delete XX\``, value: "Click on the two character IDs below to view the LFG:\n" } ] - } + }] }, lfgDelete3: { - embed: { + embeds: [{ title: "LFG deleted." - } + }] }, lfgEdit1: { - embed: { + embeds: [{ fields: [ { name: "Could not find any LFGs to edit.", value: "Make sure you are the owner of the LFG and are running this command in the same channel as the LFG" } ] - } + }] }, lfgEdit2: { - embed: { + embeds: [{ fields: [ { name: `Multiple LFGs found, please run this command again with the two character ID of the LFG you wish to edit.\n\nExample: \`${config.prefix}lfg edit XX\``, value: "Click on the two character IDs below to view the LFG:\n" } ] - } + }] } }; diff --git a/src/games.ts b/src/games.ts index a6975ca..8cbe1e7 100644 --- a/src/games.ts +++ b/src/games.ts @@ -37,7 +37,7 @@ export const LFGActivities = { } }, "Among Us": { - "Vanilla": 10, - "Modded": 10 + "Vanilla": 15, + "Modded": 15 } }; diff --git a/src/intervals.ts b/src/intervals.ts index 8696362..e549244 100644 --- a/src/intervals.ts +++ b/src/intervals.ts @@ -2,18 +2,20 @@ import { // Discordeno deps cache, sendMessage, getMessage, deleteMessage, sendDirectMessage, + getGuild, // Log4Deno deps LT, log } from "../deps.ts"; +import { jsonStringifyBig } from "./utils.ts"; import { BuildingLFG, ActiveLFG } from "./mod.d.ts"; import config from "../config.ts"; // getRandomStatus() returns status as string // Gets a new random status for the bot -const getRandomStatus = (): string => { +const getRandomStatus = (cachedGuilds: number): string => { let status = ""; switch (Math.floor((Math.random() * 5) + 1)) { case 1: @@ -29,7 +31,7 @@ const getRandomStatus = (): string => { status = "Mention me to check my prefix!"; break; default: - status = `Running LFGs in ${cache.guilds.size} servers`; + status = `Running LFGs in ${cachedGuilds + cache.dispatchedGuildIds.size} servers`; break; } @@ -41,7 +43,7 @@ const getRandomStatus = (): string => { const updateListStatistics = (botID: BigInt, serverCount: number): void => { config.botLists.forEach(async e => { if (e.enabled) { - log(LT.LOG, `Updating statistics for ${JSON.stringify(e)}`); + log(LT.LOG, `Updating statistics for ${jsonStringifyBig(e)}`); try { const tempHeaders = new Headers(); tempHeaders.append(e.headers[0].header, e.headers[0].value); @@ -50,12 +52,12 @@ const updateListStatistics = (botID: BigInt, serverCount: number): void => { const response = await fetch(e.apiUrl.replace("?{bot_id}", botID.toString()), { "method": 'POST', "headers": tempHeaders, - "body": JSON.stringify(e.body).replace('"?{server_count}"', serverCount.toString()) // ?{server_count} needs the "" removed from around it aswell to make sure its sent as a number + "body": jsonStringifyBig(e.body).replace('"?{server_count}"', serverCount.toString()) // ?{server_count} needs the "" removed from around it aswell to make sure its sent as a number }); - log(LT.INFO, `Posted server count to ${e.name}. Results: ${JSON.stringify(response)}`); + log(LT.INFO, `Posted server count to ${e.name}. Results: ${jsonStringifyBig(response)}`); } catch (e) { - log(LT.WARN, `Failed to post statistics to ${e.name} | ${JSON.stringify(e)}`); + log(LT.WARN, `Failed to post statistics to ${e.name} | ${jsonStringifyBig(e)}`); } } }); @@ -65,26 +67,29 @@ const buildingTimeout = async (activeBuilders: Array): Promise { + log(LT.WARN, `Failed to clean up active builder | edit | ${activeBuilders[i].userId}-${activeBuilders[i].channelId} | ${jsonStringifyBig(e)}`); + }); if (activeBuilders[i].editing) { activeBuilders[i].lfgMsg.edit({ content: "" }).catch(e => { - log(LT.WARN, `Failed to clean up active builder | edit | ${activeBuilders[i].userId}-${activeBuilders[i].channelId} | ${JSON.stringify(e)}`); + log(LT.WARN, `Failed to clean up active builder | edit | ${activeBuilders[i].userId}-${activeBuilders[i].channelId} | ${jsonStringifyBig(e)}`); }); } else { activeBuilders[i].lfgMsg.delete().catch(e => { - log(LT.WARN, `Failed to clean up active builder | delete | ${activeBuilders[i].userId}-${activeBuilders[i].channelId} | ${JSON.stringify(e)}`); + log(LT.WARN, `Failed to clean up active builder | delete | ${activeBuilders[i].userId}-${activeBuilders[i].channelId} | ${jsonStringifyBig(e)}`); }); } try { const m = await sendMessage(activeBuilders[i].channelId, `<@${activeBuilders[i].userId}>, your LFG ${activeBuilders[i].editing ? "editing" : "creation"} has timed out. Please try again.`); - setTimeout(() => { - m.delete(); - }, 30000); + + m.delete("Channel Cleanup", 30000).catch(e =>{ + log(LT.WARN, `Failed to delete message | ${jsonStringifyBig(e)}`); + }); } catch (e) { - log(LT.WARN, `Failed to clean up active builder | ${activeBuilders[i].userId}-${activeBuilders[i].channelId} | ${JSON.stringify(e)}`); + log(LT.WARN, `Failed to clean up active builder | ${activeBuilders[i].userId}-${activeBuilders[i].channelId} | ${jsonStringifyBig(e)}`); } finally { activeBuilders.splice(i, 1); @@ -92,10 +97,10 @@ const buildingTimeout = async (activeBuilders: Array): Promise): Promise => { - log(LT.INFO, "Checking for LFG posts to notify/delete/lock") + log(LT.INFO, "Checking for LFG posts to notify/delete/lock"); const tenMin = 10 * 60 * 1000; const now = new Date().getTime(); for (let i = 0; i < activeLFGPosts.length; i++) { @@ -106,7 +111,7 @@ const lfgNotifier = async (activeLFGPosts: Array): Promise => { const message = await getMessage(activeLFGPosts[i].channelId, activeLFGPosts[i].messageId); const lfg = message.embeds[0].fields || []; const lfgActivity = `${lfg[0].name.substr(0, lfg[0].name.length - 1)} - ${lfg[0].value}`; - const guildName = message.guild?.name || "unknown"; + const guildName = message.guild?.name || (await getGuild(message.guildId, {counts:false, addToCache: false})).name; const members = lfg[4].value; let editMsg = ""; members.split("\n").forEach(async m => { @@ -115,7 +120,7 @@ const lfgNotifier = async (activeLFGPosts: Array): Promise => { const userId = BigInt(tmpId.substr(0, tmpId.length - 1)); editMsg += `<@${userId}>, `; await sendDirectMessage(userId, { - embed: { + embeds: [{ title: `Hello ${name}! Your event in ${guildName} starts in less than 10 minutes.`, fields: [ lfg[0], @@ -124,7 +129,7 @@ const lfgNotifier = async (activeLFGPosts: Array): Promise => { value: members } ] - } + }] }); } }); @@ -137,7 +142,7 @@ const lfgNotifier = async (activeLFGPosts: Array): Promise => { activeLFGPosts[i].notified = true; } catch (err) { - log(LT.WARN, `Failed to find LFG ${activeLFGPosts[i].ownerId}-${activeLFGPosts[i].lfgUid} | ${JSON.stringify(err)}`); + log(LT.WARN, `Failed to find LFG ${activeLFGPosts[i].ownerId}-${activeLFGPosts[i].lfgUid} | ${jsonStringifyBig(err)}`); activeLFGPosts.splice(i, 1); i--; @@ -145,7 +150,7 @@ const lfgNotifier = async (activeLFGPosts: Array): Promise => { } // Lock LFG from editing/Joining/Leaving - if (!activeLFGPosts[i].locked && activeLFGPosts[i].lfgTime < now) { + else if (!activeLFGPosts[i].locked && activeLFGPosts[i].lfgTime < now) { log(LT.INFO, `Locking LFG ${activeLFGPosts[i].ownerId}-${activeLFGPosts[i].lfgUid}`); try { const message = await getMessage(activeLFGPosts[i].channelId, activeLFGPosts[i].messageId); @@ -157,7 +162,7 @@ const lfgNotifier = async (activeLFGPosts: Array): Promise => { activeLFGPosts[i].locked = true; } catch (err) { - log(LT.WARN, `Failed to find LFG ${activeLFGPosts[i].ownerId}-${activeLFGPosts[i].lfgUid} | ${JSON.stringify(err)}`); + log(LT.WARN, `Failed to find LFG ${activeLFGPosts[i].ownerId}-${activeLFGPosts[i].lfgUid} | ${jsonStringifyBig(err)}`); activeLFGPosts.splice(i, 1); i--; @@ -165,19 +170,17 @@ const lfgNotifier = async (activeLFGPosts: Array): Promise => { } // Delete old LFG post - if (activeLFGPosts[i].lfgTime < (now - tenMin)) { + else if (activeLFGPosts[i].lfgTime < (now - tenMin)) { log(LT.INFO, `Deleting LFG ${activeLFGPosts[i].ownerId}-${activeLFGPosts[i].lfgUid}`); await deleteMessage(activeLFGPosts[i].channelId, activeLFGPosts[i].messageId, "LFG post expired").catch(e => { - log(LT.WARN, `Failed to delete LFG ${activeLFGPosts[i].ownerId}-${activeLFGPosts[i].lfgUid} | ${JSON.stringify(e)}`); + log(LT.WARN, `Failed to delete LFG ${activeLFGPosts[i].ownerId}-${activeLFGPosts[i].lfgUid} | ${jsonStringifyBig(e)}`); }); activeLFGPosts.splice(i, 1); i--; } } - localStorage.setItem("activeLFGPosts", JSON.stringify(activeLFGPosts, (_key, value) => - typeof value === "bigint" ? value.toString() + "n" : value - )); -} + localStorage.setItem("activeLFGPosts", jsonStringifyBig(activeLFGPosts)); +}; export default { getRandomStatus, updateListStatistics, buildingTimeout, lfgNotifier }; diff --git a/src/lfgHandlers.d.ts b/src/lfgHandlers.d.ts index 80b6ecb..6ca62bc 100644 --- a/src/lfgHandlers.d.ts +++ b/src/lfgHandlers.d.ts @@ -5,5 +5,12 @@ import { export type JoinLeaveType = { embed: EmbedField[], success: boolean, - full: boolean + full: boolean, + justFilled: boolean +} + +export type UrlIds = { + guildId: bigint, + channelId: bigint, + messageId: bigint } diff --git a/src/lfgHandlers.ts b/src/lfgHandlers.ts index 2fbbf3b..2384e55 100644 --- a/src/lfgHandlers.ts +++ b/src/lfgHandlers.ts @@ -4,11 +4,12 @@ import { LT, log } from "../deps.ts"; -import { JoinLeaveType } from "./lfgHandlers.d.ts"; +import { JoinLeaveType, UrlIds } from "./lfgHandlers.d.ts"; import { BuildingLFG } from "./mod.d.ts"; import { LFGActivities } from "./games.ts"; import { determineTZ } from "./timeUtils.ts"; import { lfgStepQuestions } from "./constantCmds.ts"; +import { jsonStringifyBig } from "./utils.ts"; export const handleLFGStep = async (wipLFG: BuildingLFG, input: string): Promise => { const currentLFG = (wipLFG.lfgMsg.embeds[0] || { fields: undefined }).fields || [ @@ -225,7 +226,8 @@ export const handleLFGStep = async (wipLFG: BuildingLFG, input: string): Promise let lfgDate = `${today.getMonth() + 1}/${today.getDate()}`, lfgTime = "", lfgTZ = "", - lfgPeriod = ""; + lfgPeriod = "", + overrodeTZ = false; input.split(" ").forEach(c => { if (c.includes("/")) { @@ -258,14 +260,14 @@ export const handleLFGStep = async (wipLFG: BuildingLFG, input: string): Promise } else if (c.match(/^\d/)) { const tzIdx = c.search(/[a-zA-Z]/); lfgTime = c.substr(0, tzIdx); - lfgTZ = determineTZ(c.substr(tzIdx)); + [lfgTZ, overrodeTZ] = determineTZ(c.substr(tzIdx)); } else { - lfgTZ = determineTZ(c); + [lfgTZ, overrodeTZ] = determineTZ(c); } }); if (!lfgTZ) { - lfgTZ = determineTZ("ET"); + [lfgTZ, overrodeTZ] = determineTZ("ET"); } if (!lfgTime.includes(":")) { @@ -324,9 +326,9 @@ export const handleLFGStep = async (wipLFG: BuildingLFG, input: string): Promise try { if (editFlag) { wipLFG.lfgMsg = await wipLFG.lfgMsg.edit({ - embed: { + embeds: [{ fields: currentLFG - } + }] }); } @@ -336,7 +338,7 @@ export const handleLFGStep = async (wipLFG: BuildingLFG, input: string): Promise }); } catch (e) { - log(LT.WARN, `Failed to edit active builder | ${wipLFG.userId}-${wipLFG.channelId} | ${JSON.stringify(e)}`); + log(LT.WARN, `Failed to edit active builder | ${wipLFG.userId}-${wipLFG.channelId} | ${jsonStringifyBig(e)}`); } return wipLFG; @@ -344,6 +346,7 @@ export const handleLFGStep = async (wipLFG: BuildingLFG, input: string): Promise export const handleMemberJoin = (lfg: EmbedField[], member: DiscordenoMember, alternate: boolean): JoinLeaveType => { let success = false; + let justFilled = false; const userStr = `${member.username} - <@${member.id}>`; @@ -352,6 +355,7 @@ export const handleMemberJoin = (lfg: EmbedField[], member: DiscordenoMember, al const maxMembers = parseInt(tempMembers[1]); if (alternate && !lfg[5].value.includes(member.id.toString())) { + // remove from joined list if (lfg[4].value.includes(member.id.toString())) { const tempArr = lfg[4].value.split("\n"); const memberIdx = tempArr.findIndex(m => m.includes(member.id.toString())); @@ -371,11 +375,12 @@ export const handleMemberJoin = (lfg: EmbedField[], member: DiscordenoMember, al } success = true; - } else if (!alternate &¤tMembers < maxMembers && !lfg[4].value.includes(member.id.toString())) { + } else if (!alternate && currentMembers < maxMembers && !lfg[4].value.includes(member.id.toString())) { + // remove from alternate list if (lfg[5].value.includes(member.id.toString())) { const tempArr = lfg[5].value.split("\n"); const memberIdx = tempArr.findIndex(m => m.includes(member.id.toString())); - tempArr.splice(memberIdx, 1) + tempArr.splice(memberIdx, 1); lfg[5].value = tempArr.join("\n") || "None"; } @@ -385,16 +390,34 @@ export const handleMemberJoin = (lfg: EmbedField[], member: DiscordenoMember, al lfg[4].value += `\n${userStr}`; } currentMembers++; + justFilled = currentMembers === maxMembers; lfg[4].name = `Members Joined: ${currentMembers}/${maxMembers}`; success = true; + } else if (!alternate && currentMembers === maxMembers && !lfg[4].value.includes(member.id.toString())) { + // update user in alternate list to include the * to make them autojoin + if (lfg[5].value.includes(member.id.toString())) { + const tempArr = lfg[5].value.split("\n"); + const memberIdx = tempArr.findIndex(m => m.includes(member.id.toString())); + tempArr[memberIdx] = `${tempArr[memberIdx]} *`; + lfg[5].value = tempArr.join("\n"); + } else { + if (lfg[5].value === "None") { + lfg[5].value = `${userStr} *`; + } else { + lfg[5].value += `\n${userStr} *`; + } + + success = true; + } } return { embed: lfg, success: success, - full: currentMembers === maxMembers + full: currentMembers === maxMembers, + justFilled: justFilled }; }; @@ -413,10 +436,25 @@ export const handleMemberLeave = (lfg: EmbedField[], member: DiscordenoMember): tempArr.splice(memberIdx, 1); lfg[4].value = tempArr.join("\n") || "None"; - if (currentMembers) { - currentMembers--; + if (lfg[5].value.includes("*")) { + // find first * user and move them to the joined list + const tempArr2 = lfg[5].value.split("\n"); + const memberToMoveIdx = tempArr2.findIndex(m => m.includes("*")) + let memberToMove = tempArr2[memberToMoveIdx]; + memberToMove = memberToMove.substr(0, memberToMove.length - 2); + tempArr.push(memberToMove); + lfg[4].value = tempArr.join("\n") || "None"; + + // Remove them from the alt list + tempArr2.splice(memberToMoveIdx, 1); + lfg[5].value = tempArr2.join("\n") || "None"; + } else { + // update count since no users were marked as * + if (currentMembers) { + currentMembers--; + } + lfg[4].name = `Members Joined: ${currentMembers}/${maxMembers}`; } - lfg[4].name = `Members Joined: ${currentMembers}/${maxMembers}`; success = true; } @@ -433,6 +471,25 @@ export const handleMemberLeave = (lfg: EmbedField[], member: DiscordenoMember): return { embed: lfg, success: success, - full: currentMembers === maxMembers + full: currentMembers === maxMembers, + justFilled: false + }; +}; + +export const urlToIds = (url: string): UrlIds => { + const strIds = { + guildId: "", + channelId: "", + messageId: "" + }; + + url = url.toLowerCase(); + + [strIds.guildId, strIds.channelId, strIds.messageId] = url.substr((url.indexOf("channels") + 9)).split("/"); + + return { + guildId: BigInt(strIds.guildId), + channelId: BigInt(strIds.channelId), + messageId: BigInt(strIds.messageId) }; }; diff --git a/src/mod.d.ts b/src/mod.d.ts index 28f7851..0cd7ab2 100644 --- a/src/mod.d.ts +++ b/src/mod.d.ts @@ -28,10 +28,12 @@ export type GuildPrefixes = { prefix: string } -// TEMP -export interface ButtonData { - /** with the value you defined for this component */ - customId: string; - /** The type of this component */ - componentType: 2; +export type GuildModRoles = { + guildId: bigint, + roleId: bigint +} + +export type GuildCleanChannels = { + guildId: bigint, + channelId: bigint } diff --git a/src/timeUtils.ts b/src/timeUtils.ts index f646c47..858d54e 100644 --- a/src/timeUtils.ts +++ b/src/timeUtils.ts @@ -1,16 +1,22 @@ -export const determineTZ = (tz: string): string => { +export const determineTZ = (tz: string, userOverride = false): [string, boolean] => { tz = tz.toUpperCase(); - if (tz === "ET" || tz === "CT" || tz === "MT" || tz === "PT") { + let overrode = false; + const shortHandUSTZ = (tz === "ET" || tz === "CT" || tz === "MT" || tz === "PT"); + const fullUSTZ = (tz.length === 3 && (tz.startsWith("E") || tz.startsWith("C") || tz.startsWith("M") || tz.startsWith("P")) && (tz.endsWith("DT") || tz.endsWith("ST"))); + + if (!userOverride && (shortHandUSTZ || fullUSTZ)) { const today = new Date(); const jan = new Date(today.getFullYear(), 0, 1); const jul = new Date(today.getFullYear(), 6, 1); if (today.getTimezoneOffset() < Math.max(jan.getTimezoneOffset(), jul.getTimezoneOffset())) { + if (tz.includes("S")) overrode = true; tz = `${tz.substr(0, 1)}DT`; } else { + if (tz.includes("D")) overrode = true; tz = `${tz.substr(0, 1)}ST`; } } - return tz; + return [tz, overrode]; }; diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 0000000..33db888 --- /dev/null +++ b/src/utils.ts @@ -0,0 +1,14 @@ +export const jsonParseBig = (input: string) => { + return JSON.parse(input, (_key, value) => { + if (typeof value === "string" && /^\d+n$/.test(value)) { + return BigInt(value.substr(0, value.length - 1)); + } + return value; + }); +}; + +export const jsonStringifyBig = (input: any) => { + return JSON.stringify(input, (_key, value) => + typeof value === "bigint" ? value.toString() + "n" : value + ); +};