From 60188ca5d8d2ad880b7910084b474b01929ff135 Mon Sep 17 00:00:00 2001 From: "Ean Milligan (Bastion)" Date: Sun, 21 Nov 2021 22:14:57 -0500 Subject: [PATCH] Initial work on V2.0.0 Updated dependencies and corrected syntax errors caused by the newer deps. --- config.example.ts | 10 +- deps.ts | 19 +- flags.ts | 2 +- mod.ts | 164 +++--- src/api.ts | 1352 +++++++++++++++++++++++---------------------- src/intervals.ts | 16 +- src/solver.ts | 48 +- src/utils.ts | 24 +- start.command | 1 + 9 files changed, 827 insertions(+), 809 deletions(-) create mode 100644 start.command diff --git a/config.example.ts b/config.example.ts index b269dd2..7e49f3c 100644 --- a/config.example.ts +++ b/config.example.ts @@ -1,6 +1,6 @@ export const config = { "name": "The Artificer", // Name of the bot - "version": "1.4.3", // Version of the bot + "version": "2.0.0", // Version of the bot "token": "the_bot_token", // Discord API Token for this bot "localtoken": "local_testing_token", // Discord API Token for a secondary OPTIONAL testing bot, THIS MUST BE DIFFERENT FROM "token" "prefix": "[[", // Prefix for all commands @@ -13,7 +13,7 @@ export const config = { "rateLimitCnt": 10, // Amount of requests that can be made (successful or not) during above time range before getting rate limited "admin": 0n, // Discord user ID of the bot admin, this user will be the user that can ban/unban user/channel combos and API keys "adminKey": "your_25char_api_token", // API Key generated by nanoid that is 25 char long, this gets pre-populated into all_keys - "email": "" // Temporary set up for email, this will be adjusted to an actual email using deno-smtp in the future. + "email": 0n // Temporary set up for email, this will be adjusted to an actual email using deno-smtp in the future. }, "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 @@ -24,9 +24,9 @@ export const config = { "name": "" // Name of the database Schema to use for the bot }, "logRolls": false, // Enables logging of roll commands, this should be left disabled for privacy, but exists to allow verification of rolls before deployment, all API rolls will always be logged no matter what this is set to - "logChannel": "the_log_channel", // Discord channel ID where the bot should put startup messages and other error messages needed - "reportChannel": "the_report_channel", // Discord channel ID where reports will be sent when using the built-in report command - "devServer": "the_dev_server", // Discord guild ID where testing of indev features/commands will be handled, used in conjuction with the DEVMODE bool in mod.ts + "logChannel": 0n, // Discord channel ID where the bot should put startup messages and other error messages needed + "reportChannel": 0n, // Discord channel ID where reports will be sent when using the built-in report command + "devServer": 0n, // Discord guild ID where testing of indev features/commands will be handled, used in conjuction with the DEVMODE bool in mod.ts "emojis": [ // Array of objects containing all emojis that the bot can send on your behalf, empty this array if you don't want any of them { // Emoji object, duplicate for each emoji "name": "emoji_name", // Name of emoji in discord diff --git a/deps.ts b/deps.ts index 31ff1c7..d20ace0 100644 --- a/deps.ts +++ b/deps.ts @@ -1,19 +1,18 @@ // All external dependancies are to be loaded here to make updating dependancy versions much easier export { - startBot, editBotsStatus, - Intents, StatusTypes, ActivityType, + startBot, editBotStatus, editBotNickname, + Intents, DiscordActivityTypes, sendMessage, sendDirectMessage, - cache, botID, - memberIDHasPermission -} from "https://deno.land/x/discordeno@10.3.0/mod.ts"; + cache, cacheHandlers, botId, + hasGuildPermissions +} from "https://deno.land/x/discordeno@12.0.1/mod.ts"; export type { - CacheData, Message, Guild, MessageContent -} from "https://deno.land/x/discordeno@10.3.0/mod.ts"; + DiscordenoMessage, DiscordenoGuild, CreateMessage +} from "https://deno.land/x/discordeno@12.0.1/mod.ts"; -export { Client } from "https://deno.land/x/mysql@v2.7.0/mod.ts"; +export { Client } from "https://deno.land/x/mysql@v2.10.1/mod.ts"; -export { serve } from "https://deno.land/std@0.83.0/http/server.ts"; -export { Status, STATUS_TEXT } from "https://deno.land/std@0.83.0/http/http_status.ts"; +export { Status, STATUS_TEXT } from "https://deno.land/std@0.115.1/http/http_status.ts"; export { nanoid } from "https://deno.land/x/nanoid@v3.0.0/mod.ts"; diff --git a/flags.ts b/flags.ts index 939f73a..94bf935 100644 --- a/flags.ts +++ b/flags.ts @@ -3,4 +3,4 @@ export const DEVMODE = false; // DEBUG is used to toggle the cmdPrompt export const DEBUG = false; // LOCALMODE is used to run a differnt bot token for local testing -export const LOCALMODE = false; +export const LOCALMODE = true; diff --git a/mod.ts b/mod.ts index 79b1666..668f410 100644 --- a/mod.ts +++ b/mod.ts @@ -6,12 +6,12 @@ import { // Discordeno deps - startBot, editBotsStatus, - Intents, StatusTypes, ActivityType, + startBot, editBotStatus, editBotNickname, + Intents, sendMessage, sendDirectMessage, - cache, botID, - memberIDHasPermission, - Message, Guild, + cache, botId, + hasGuildPermissions, + DiscordActivityTypes, DiscordenoGuild, DiscordenoMessage, // MySQL Driver deps Client @@ -44,18 +44,32 @@ utils.initLog("logs"); // Start up the Discord Bot startBot({ token: LOCALMODE ? config.localtoken : config.token, - intents: [Intents.GUILD_MESSAGES, Intents.DIRECT_MESSAGES, Intents.GUILDS], + intents: [Intents.GuildMessages, Intents.DirectMessages, Intents.Guilds], eventHandlers: { ready: () => { utils.log(LT.INFO, `${config.name} Logged in!`); - editBotsStatus(StatusTypes.Online, "Booting up . . .", ActivityType.Game); + 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(() => { + setInterval(async () => { utils.log(LT.LOG, "Changing bot status"); try { // Wrapped in try-catch due to hard crash possible - editBotsStatus(StatusTypes.Online, intervals.getRandomStatus(cache), ActivityType.Game); + editBotStatus({ + activities: [{ + name: await intervals.getRandomStatus(), + type: DiscordActivityTypes.Game, + createdAt: new Date().getTime() + }], + status: "online" + }); } catch (e) { utils.log(LT.ERROR, `Failed to update status: ${JSON.stringify(e)}`); } @@ -64,34 +78,42 @@ startBot({ // Interval to update bot list stats every 24 hours LOCALMODE ? utils.log(LT.INFO, "updateListStatistics not running") : setInterval(() => { utils.log(LT.LOG, "Updating all bot lists statistics"); - intervals.updateListStatistics(botID, cache.guilds.size); + intervals.updateListStatistics(botId, cache.guilds.size); }, 86400000); // setTimeout added to make sure the startup message does not error out setTimeout(() => { - LOCALMODE ? utils.log(LT.INFO, "updateListStatistics not running") : intervals.updateListStatistics(botID, cache.guilds.size); - editBotsStatus(StatusTypes.Online, `Boot Complete`, ActivityType.Game); + LOCALMODE && editBotNickname(config.devServer, `LOCAL - ${config.name}`); + LOCALMODE ? utils.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 => { utils.log(LT.ERROR, `Failed to send message: ${JSON.stringify(e)}`); }); }, 1000); }, - guildCreate: (guild: Guild) => { + guildCreate: (guild: DiscordenoGuild) => { utils.log(LT.LOG, `Handling joining guild ${JSON.stringify(guild)}`); sendMessage(config.logChannel, `New guild joined: ${guild.name} (id: ${guild.id}). This guild has ${guild.memberCount} members!`).catch(e => { utils.log(LT.ERROR, `Failed to send message: ${JSON.stringify(e)}`); }); }, - guildDelete: (guild: Guild) => { + guildDelete: (guild: DiscordenoGuild) => { utils.log(LT.LOG, `Handling leaving guild ${JSON.stringify(guild)}`); sendMessage(config.logChannel, `I have been removed from: ${guild.name} (id: ${guild.id}).`).catch(e => { utils.log(LT.ERROR, `Failed to send message: ${JSON.stringify(e)}`); }); }, - debug: dmsg => utils.log(LT.LOG, `${JSON.stringify(dmsg)}`), - messageCreate: async (message: Message) => { + debug: dmsg => utils.log(LT.LOG, `Debug Message | ${JSON.stringify(dmsg)}`), + messageCreate: async (message: DiscordenoMessage) => { // Ignore all other bots - if (message.author.bot) return; + if (message.isBot) return; // Ignore all messages that are not commands if (message.content.indexOf(config.prefix) !== 0) return; @@ -114,7 +136,7 @@ startBot({ // Calculates ping between sending a message and editing it, giving a nice round-trip latency. try { - const m = await utils.sendIndirectMessage(message, "Ping?", sendMessage, sendDirectMessage); + const m = await message.send("Ping?"); m.edit(`Pong! Latency is ${m.timestamp - message.timestamp}ms.`); } catch (e) { utils.log(LT.ERROR, `Failed to send message: ${JSON.stringify(message)} | ${JSON.stringify(e)}`); @@ -129,7 +151,7 @@ startBot({ utils.log(LT.ERROR, `Failed to call stored procedure INC_CNT: ${JSON.stringify(e)}`); }); - utils.sendIndirectMessage(message, "The Artificer was built in memory of my Grandmother, Babka\nWith much love, Ean\n\nDecember 21, 2020", sendMessage, sendDirectMessage).catch(e => { + message.send("The Artificer was built in memory of my Grandmother, Babka\nWith much love, Ean\n\nDecember 21, 2020").catch(e => { utils.log(LT.ERROR, `Failed to send message: ${JSON.stringify(message)} | ${JSON.stringify(e)}`); }); } @@ -142,7 +164,7 @@ startBot({ utils.log(LT.ERROR, `Failed to call stored procedure INC_CNT: ${JSON.stringify(e)}`); }); - utils.sendIndirectMessage(message, longStrs.rollhelp.join("\n"), sendMessage, sendDirectMessage).catch(e => { + message.send(longStrs.rollhelp.join("\n")).catch(e => { utils.log(LT.ERROR, `Failed to send message: ${JSON.stringify(message)} | ${JSON.stringify(e)}`); }); } @@ -155,7 +177,7 @@ startBot({ utils.log(LT.ERROR, `Failed to call stored procedure INC_CNT: ${JSON.stringify(e)}`); }); - utils.sendIndirectMessage(message, longStrs.help.join("\n"), sendMessage, sendDirectMessage).catch(e => { + message.send(longStrs.help.join("\n")).catch(e => { utils.log(LT.ERROR, `Failed to send message: ${JSON.stringify(message)} | ${JSON.stringify(e)}`); }); } @@ -168,7 +190,7 @@ startBot({ utils.log(LT.ERROR, `Failed to call stored procedure INC_CNT: ${JSON.stringify(e)}`); }); - utils.sendIndirectMessage(message, longStrs.info.join("\n"), sendMessage, sendDirectMessage).catch(e => { + message.send(longStrs.info.join("\n")).catch(e => { utils.log(LT.ERROR, `Failed to send message: ${JSON.stringify(message)} | ${JSON.stringify(e)}`); }); } @@ -181,7 +203,7 @@ startBot({ utils.log(LT.ERROR, `Failed to call stored procedure INC_CNT: ${JSON.stringify(e)}`); }); - utils.sendIndirectMessage(message, longStrs.privacy.join("\n"), sendMessage, sendDirectMessage).catch(e => { + message.send(longStrs.privacy.join("\n")).catch(e => { utils.log(LT.ERROR, `Failed to send message: ${JSON.stringify(message)} | ${JSON.stringify(e)}`); }); } @@ -194,7 +216,7 @@ startBot({ utils.log(LT.ERROR, `Failed to call stored procedure INC_CNT: ${JSON.stringify(e)}`); }); - utils.sendIndirectMessage(message, `My current version is ${config.version}.`, sendMessage, sendDirectMessage).catch(e => { + message.send(`My current version is ${config.version}.`).catch(e => { utils.log(LT.ERROR, `Failed to send message: ${JSON.stringify(message)} | ${JSON.stringify(e)}`); }); } @@ -207,10 +229,10 @@ startBot({ utils.log(LT.ERROR, `Failed to call stored procedure INC_CNT: ${JSON.stringify(e)}`); }); - sendMessage(config.reportChannel, ("USER REPORT:\n" + args.join(" "))).catch(e => { + sendMessage(config.reportChannel, (`USER REPORT:\n${args.join(" ")}`)).catch(e => { utils.log(LT.ERROR, `Failed to send message: ${JSON.stringify(message)} | ${JSON.stringify(e)}`); }); - utils.sendIndirectMessage(message, "Failed command has been reported to my developer.\n\nFor more in depth support, and information about planned maintenance, please join the support server here: https://discord.gg/peHASXMZYv", sendMessage, sendDirectMessage).catch(e => { + message.send("Failed command has been reported to my developer.\n\nFor more in depth support, and information about planned maintenance, please join the support server here: https://discord.gg/peHASXMZYv").catch(e => { utils.log(LT.ERROR, `Failed to send message: ${JSON.stringify(message)} | ${JSON.stringify(e)}`); }); } @@ -233,7 +255,7 @@ startBot({ const rolls = BigInt(rollQuery[0].count); const total = BigInt(totalQuery[0].count); - utils.sendIndirectMessage(message, `${config.name} is rolling dice for ${cache.members.size} active users, in ${cache.channels.size} channels of ${cache.guilds.size} servers.\n\nSo far, ${rolls} dice have been rolled and ${total - rolls} utility commands have been run.`, sendMessage, sendDirectMessage).catch(e => { + message.send(`${config.name} is rolling dice for ${cache.members.size} active users, in ${cache.channels.size} channels of ${cache.guilds.size} servers.\n\nSo far, ${rolls} dice have been rolled and ${total - rolls} utility commands have been run.`).catch(e => { utils.log(LT.ERROR, `Failed to send message: ${JSON.stringify(message)} | ${JSON.stringify(e)}`); }); } @@ -250,19 +272,19 @@ startBot({ const apiArg = args[0].toLowerCase(); // Alert users who DM the bot that this command is for guilds only - if (message.guildID === "") { - utils.sendIndirectMessage(message, `API commands are only available in guilds.`, sendMessage, sendDirectMessage).catch(e => { + if (message.guildId === 0n) { + message.send(`API commands are only available in guilds.`).catch(e => { utils.log(LT.ERROR, `Failed to send message: ${JSON.stringify(message)} | ${JSON.stringify(e)}`); }); return; } // Makes sure the user is authenticated to run the API command - if (await memberIDHasPermission(message.author.id, message.guildID, ["ADMINISTRATOR"])) { + if (await hasGuildPermissions(message.authorId, message.guildId, ["ADMINISTRATOR"])) { // [[api help // Shows API help details if (apiArg === "help") { - utils.sendIndirectMessage(message, longStrs.apihelp.join("\n"), sendMessage, sendDirectMessage).catch(e => { + message.send(longStrs.apihelp.join("\n")).catch(e => { utils.log(LT.ERROR, `Failed to send message: ${JSON.stringify(message)} | ${JSON.stringify(e)}`); }); } @@ -270,9 +292,9 @@ startBot({ // [[api allow/block // Lets a guild admin allow or ban API rolls from happening in said guild else if (apiArg === "allow" || apiArg === "block" || apiArg === "enable" || apiArg === "disable") { - const guildQuery = await dbClient.query(`SELECT guildid FROM allowed_guilds WHERE guildid = ?`, [message.guildID]).catch(e0 => { + const guildQuery = await dbClient.query(`SELECT guildid FROM allowed_guilds WHERE guildid = ?`, [message.guildId]).catch(e0 => { utils.log(LT.ERROR, `Failed to query DB: ${JSON.stringify(e0)}`); - utils.sendIndirectMessage(message, `Failed to ${apiArg} API rolls for this guild. If this issue persists, please report this to the developers.`, sendMessage, sendDirectMessage).catch(e1 => { + message.send(`Failed to ${apiArg} API rolls for this guild. If this issue persists, please report this to the developers.`).catch(e1 => { utils.log(LT.ERROR, `Failed to send message: ${JSON.stringify(message)} | ${JSON.stringify(e1)}`); }); return; @@ -280,25 +302,25 @@ startBot({ if (guildQuery.length === 0) { // Since guild is not in our DB, add it in - await dbClient.execute(`INSERT INTO allowed_guilds(guildid,active) values(?,?)`, [BigInt(message.guildID), ((apiArg === "allow" || apiArg === "enable") ? 1 : 0)]).catch(e0 => { + await dbClient.execute(`INSERT INTO allowed_guilds(guildid,active) values(?,?)`, [BigInt(message.guildId), ((apiArg === "allow" || apiArg === "enable") ? 1 : 0)]).catch(e0 => { utils.log(LT.ERROR, `Failed to insert into DB: ${JSON.stringify(e0)}`); - utils.sendIndirectMessage(message, `Failed to ${apiArg} API rolls for this guild. If this issue persists, please report this to the developers.`, sendMessage, sendDirectMessage).catch(e1 => { + message.send(`Failed to ${apiArg} API rolls for this guild. If this issue persists, please report this to the developers.`).catch(e1 => { utils.log(LT.ERROR, `Failed to send message: ${JSON.stringify(message)} | ${JSON.stringify(e1)}`); }); return; }); } else { // Since guild is in our DB, update it - await dbClient.execute(`UPDATE allowed_guilds SET active = ? WHERE guildid = ?`, [((apiArg === "allow" || apiArg === "enable") ? 1 : 0), BigInt(message.guildID)]).catch(e0 => { + await dbClient.execute(`UPDATE allowed_guilds SET active = ? WHERE guildid = ?`, [((apiArg === "allow" || apiArg === "enable") ? 1 : 0), BigInt(message.guildId)]).catch(e0 => { utils.log(LT.ERROR, `Failed to update DB: ${JSON.stringify(e0)}`); - utils.sendIndirectMessage(message, `Failed to ${apiArg} API rolls for this guild. If this issue persists, please report this to the developers.`, sendMessage, sendDirectMessage).catch(e1 => { + message.send(`Failed to ${apiArg} API rolls for this guild. If this issue persists, please report this to the developers.`).catch(e1 => { utils.log(LT.ERROR, `Failed to send message: ${JSON.stringify(message)} | ${JSON.stringify(e1)}`); }); return; }); } // We won't get here if there's any errors, so we know it has bee successful, so report as such - utils.sendIndirectMessage(message, `API rolls have successfully been ${apiArg}ed for this guild.`, sendMessage, sendDirectMessage).catch(e => { + message.send(`API rolls have successfully been ${apiArg}ed for this guild.`).catch(e => { utils.log(LT.ERROR, `Failed to send message: ${JSON.stringify(message)} | ${JSON.stringify(e)}`); }); } @@ -306,16 +328,16 @@ startBot({ // [[api delete // Lets a guild admin delete their server from the database else if (apiArg === "delete") { - await dbClient.execute(`DELETE FROM allowed_guilds WHERE guildid = ?`, [message.guildID]).catch(e0 => { + await dbClient.execute(`DELETE FROM allowed_guilds WHERE guildid = ?`, [message.guildId]).catch(e0 => { utils.log(LT.ERROR, `Failed to query DB: ${JSON.stringify(e0)}`); - utils.sendIndirectMessage(message, `Failed to delete this guild from the database. If this issue persists, please report this to the developers.`, sendMessage, sendDirectMessage).catch(e1 => { + message.send(`Failed to delete this guild from the database. If this issue persists, please report this to the developers.`).catch(e1 => { utils.log(LT.ERROR, `Failed to send message: ${JSON.stringify(message)} | ${JSON.stringify(e1)}`); }); return; }); // We won't get here if there's any errors, so we know it has bee successful, so report as such - utils.sendIndirectMessage(message, `This guild's API setting has been removed from The Artifier's Database.`, sendMessage, sendDirectMessage).catch(e => { + message.send(`This guild's API setting has been removed from The Artifier's Database.`).catch(e => { utils.log(LT.ERROR, `Failed to send message: ${JSON.stringify(message)} | ${JSON.stringify(e)}`); }); } @@ -324,9 +346,9 @@ startBot({ // Lets a guild admin check the status of API rolling in said guild else if (apiArg === "status") { // Get status of guild from the db - const guildQuery = await dbClient.query(`SELECT active, banned FROM allowed_guilds WHERE guildid = ?`, [message.guildID]).catch(e0 => { + const guildQuery = await dbClient.query(`SELECT active, banned FROM allowed_guilds WHERE guildid = ?`, [message.guildId]).catch(e0 => { utils.log(LT.ERROR, `Failed to query DB: ${JSON.stringify(e0)}`); - utils.sendIndirectMessage(message, `Failed to check API rolls status for this guild. If this issue persists, please report this to the developers.`, sendMessage, sendDirectMessage).catch(e1 => { + message.send(`Failed to check API rolls status for this guild. If this issue persists, please report this to the developers.`).catch(e1 => { utils.log(LT.ERROR, `Failed to send message: ${JSON.stringify(message)} | ${JSON.stringify(e1)}`); }); return; @@ -336,23 +358,23 @@ startBot({ if (guildQuery.length > 0) { // Check if guild is banned from using API and return appropriate message if (guildQuery[0].banned) { - utils.sendIndirectMessage(message, `The Artificer's API is ${config.api.enable ? "currently enabled" : "currently disabled"}.\n\nAPI rolls are banned from being used in this guild. This will not be reversed.`, sendMessage, sendDirectMessage).catch(e => { + message.send(`The Artificer's API is ${config.api.enable ? "currently enabled" : "currently disabled"}.\n\nAPI rolls are banned from being used in this guild. This will not be reversed.`).catch(e => { utils.log(LT.ERROR, `Failed to send message: ${JSON.stringify(message)} | ${JSON.stringify(e)}`); }); } else { - utils.sendIndirectMessage(message, `The Artificer's API is ${config.api.enable ? "currently enabled" : "currently disabled"}.\n\nAPI rolls are ${guildQuery[0].active ? "allowed" : "blocked from being used"} in this guild.`, sendMessage, sendDirectMessage).catch(e => { + message.send(`The Artificer's API is ${config.api.enable ? "currently enabled" : "currently disabled"}.\n\nAPI rolls are ${guildQuery[0].active ? "allowed" : "blocked from being used"} in this guild.`).catch(e => { utils.log(LT.ERROR, `Failed to send message: ${JSON.stringify(message)} | ${JSON.stringify(e)}`); }); } } else { // Guild is not in DB, therefore they are blocked - utils.sendIndirectMessage(message, `The Artificer's API is ${config.api.enable ? "currently enabled" : "currently disabled"}.\n\nAPI rolls are blocked from being used in this guild.`, sendMessage, sendDirectMessage).catch(e => { + message.send(`The Artificer's API is ${config.api.enable ? "currently enabled" : "currently disabled"}.\n\nAPI rolls are blocked from being used in this guild.`).catch(e => { utils.log(LT.ERROR, `Failed to send message: ${JSON.stringify(message)} | ${JSON.stringify(e)}`); }); } } } else { - utils.sendIndirectMessage(message, `API commands are powerful and can only be used by guild Owners and Admins.\n\nFor information on how to use the API, please check the GitHub README for more information: `, sendMessage, sendDirectMessage).catch(e => { + message.send(`API commands are powerful and can only be used by guild Owners and Admins.\n\nFor information on how to use the API, please check the GitHub README for more information: `).catch(e => { utils.log(LT.ERROR, `Failed to send message: ${JSON.stringify(message)} | ${JSON.stringify(e)}`); }); } @@ -360,15 +382,15 @@ startBot({ // [[roll]] // Dice rolling commence! - else if ((command + args.join("")).indexOf(config.postfix) > -1) { + else if ((`${command}${args.join("")}`).indexOf(config.postfix) > -1) { // Light telemetry to see how many times a command is being run dbClient.execute(`CALL INC_CNT("roll");`).catch(e => { utils.log(LT.ERROR, `Failed to call stored procedure INC_CNT: ${JSON.stringify(e)}`); }); // If DEVMODE is on, only allow this command to be used in the devServer - if (DEVMODE && message.guildID !== config.devServer) { - utils.sendIndirectMessage(message, "Command is in development, please try again later.", sendMessage, sendDirectMessage).catch(e => { + if (DEVMODE && message.guildId !== config.devServer) { + message.send("Command is in development, please try again later.").catch(e => { utils.log(LT.ERROR, `Failed to send message: ${JSON.stringify(message)} | ${JSON.stringify(e)}`); }); return; @@ -376,9 +398,9 @@ startBot({ // Rest of this command is in a try-catch to protect all sends/edits from erroring out try { - const originalCommand = config.prefix + command + " " + args.join(" "); + const originalCommand = `${config.prefix}${command} ${args.join(" ")}`; - const m = await utils.sendIndirectMessage(message, "Rolling...", sendMessage, sendDirectMessage); + const m = await message.send("Rolling..."); const modifiers = { noDetails: false, @@ -392,7 +414,7 @@ startBot({ // Check if any of the args are command flags and pull those out into the modifiers object for (let i = 0; i < args.length; i++) { - utils.log(LT.LOG, `Checking ${command + args.join(" ")} for command modifiers ${i}`); + utils.log(LT.LOG, `Checking ${command}${args.join(" ")} for command modifiers ${i}`); switch (args[i].toLowerCase()) { case "-nd": modifiers.noDetails = true; @@ -484,7 +506,7 @@ startBot({ } // Rejoin all of the args and send it into the solver, if solver returns a falsy item, an error object will be substituded in - const rollCmd = command + " " + args.join(" "); + const rollCmd = `${command} ${args.join(" ")}`; const returnmsg = solver.parseRoll(rollCmd, config.prefix, config.postfix, modifiers.maxRoll, modifiers.nominalRoll, modifiers.order) || { error: true, errorCode: "EmptyMessage", errorMsg: "Error: Empty message", line1: "", line2: "", line3: "" }; let returnText = ""; @@ -503,19 +525,19 @@ startBot({ return; } else { // Else format the output using details from the solver - returnText = "<@" + message.author.id + ">" + returnmsg.line1 + "\n" + returnmsg.line2; + returnText = `<@${message.authorId}>${returnmsg.line1}\n${returnmsg.line2}`; if (modifiers.noDetails) { returnText += "\nDetails suppressed by -nd flag."; } else { - returnText += "\nDetails:\n" + modifiers.spoiler + returnmsg.line3 + modifiers.spoiler; + returnText += `\nDetails:\n${modifiers.spoiler}${returnmsg.line3}${modifiers.spoiler}`; } } // If the roll was a GM roll, send DMs to all the GMs if (modifiers.gmRoll) { // Make a new return line to be sent to the roller - const normalText = "<@" + message.author.id + ">" + returnmsg.line1 + "\nResults have been messaged to the following GMs: " + modifiers.gms.join(" "); + const normalText = `<@${message.authorId}>${returnmsg.line1}\nResults have been messaged to the following GMs: ${modifiers.gms.join(" ")}`; // And message the full details to each of the GMs, alerting roller of every GM that could not be messaged modifiers.gms.forEach(async e => { @@ -525,19 +547,19 @@ startBot({ if (b.size > 8388290) { // Update return text - returnText = "<@" + message.author.id + ">" + returnmsg.line1 + "\n" + returnmsg.line2 + "\nFull details could not be attached to this messaged as a `.txt` file as the file would be too large for Discord to handle. If you would like to see the details of rolls, please send the rolls in multiple messages instead of bundled into one."; + returnText = `<@${message.authorId}>${returnmsg.line1}\n${returnmsg.line2}\nFull details could not be attached to this messaged as a \`.txt\` file as the file would be too large for Discord to handle. If you would like to see the details of rolls, please send the rolls in multiple messages instead of bundled into one.`; // Attempt to DM the GMs and send a warning if it could not DM a GM - await sendDirectMessage(e.substr(2, (e.length - 3)), returnText).catch(() => { - utils.sendIndirectMessage(message, "WARNING: " + e + " could not be messaged. If this issue persists, make sure direct messages are allowed from this server.", sendMessage, sendDirectMessage); + await sendDirectMessage(BigInt(e.substr(2, (e.length - 3))), returnText).catch(() => { + message.send(`WARNING: ${e} could not be messaged. If this issue persists, make sure direct messages are allowed from this server.`); }); } else { // Update return text - returnText = "<@" + message.author.id + ">" + returnmsg.line1 + "\n" + returnmsg.line2 + "\nFull details have been attached to this messaged as a `.txt` file for verification purposes."; + returnText = `<@${message.authorId}>${returnmsg.line1}\n${returnmsg.line2}\nFull details have been attached to this messaged as a \`.txt\` file for verification purposes.`; // Attempt to DM the GMs and send a warning if it could not DM a GM - await sendDirectMessage(e.substr(2, (e.length - 3)), { "content": returnText, "file": { "blob": b, "name": "rollDetails.txt" } }).catch(() => { - utils.sendIndirectMessage(message, "WARNING: " + e + " could not be messaged. If this issue persists, make sure direct messages are allowed from this server.", sendMessage, sendDirectMessage); + await sendDirectMessage(BigInt(e.substr(2, (e.length - 3))), { "content": returnText, "file": { "blob": b, "name": "rollDetails.txt" } }).catch(() => { + message.send(`WARNING: ${e} could not be messaged. If this issue persists, make sure direct messages are allowed from this server.`); }); } }); @@ -559,18 +581,18 @@ startBot({ if (b.size > 8388290) { // Update return text - returnText = "<@" + message.author.id + ">" + returnmsg.line1 + "\n" + returnmsg.line2 + "\nDetails have been ommitted from this message for being over 2000 characters. Full details could not be attached to this messaged as a `.txt` file as the file would be too large for Discord to handle. If you would like to see the details of rolls, please send the rolls in multiple messages instead of bundled into one."; + returnText = `<@${message.authorId}>${returnmsg.line1}\n${returnmsg.line2}\nDetails have been ommitted from this message for being over 2000 characters. Full details could not be attached to this messaged as a \`.txt\` file as the file would be too large for Discord to handle. If you would like to see the details of rolls, please send the rolls in multiple messages instead of bundled into one.`; // Send the results m.edit(returnText); } else { // Update return text - returnText = "<@" + message.author.id + ">" + returnmsg.line1 + "\n" + returnmsg.line2 + "\nDetails have been ommitted from this message for being over 2000 characters. Full details have been attached to this messaged as a `.txt` file for verification purposes."; + returnText = `<@${message.authorId}>${returnmsg.line1}\n${returnmsg.line2}\nDetails have been ommitted from this message for being over 2000 characters. Full details have been attached to this messaged as a \`.txt\` file for verification purposes.`; // Remove the original message to send new one with attachment m.delete(); - await utils.sendIndirectMessage(message, { "content": returnText, "file": { "blob": b, "name": "rollDetails.txt" } }, sendMessage, sendDirectMessage); + await message.send({ "content": returnText, "file": { "blob": b, "name": "rollDetails.txt" } }); } } else { // Finally send the text @@ -594,16 +616,16 @@ startBot({ else { // Start looping thru the possible emojis config.emojis.some((emoji: EmojiConf) => { - utils.log(LT.LOG, `Checking if command was emoji ${emoji}`); + utils.log(LT.LOG, `Checking if command was emoji ${JSON.stringify(emoji)}`); // If a match gets found if (emoji.aliases.indexOf(command || "") > -1) { // Light telemetry to see how many times a command is being run - dbClient.execute(`CALL INC_CNT("emoji");`).catch(e => { + dbClient.execute(`CALL INC_CNT("emojis");`).catch(e => { utils.log(LT.ERROR, `Failed to call stored procedure INC_CNT: ${JSON.stringify(e)}`); }); // Send the needed emoji1 - utils.sendIndirectMessage(message, `<${emoji.animated ? "a" : ""}:${emoji.name}:${emoji.id}>`, sendMessage, sendDirectMessage).catch(e => { + message.send(`<${emoji.animated ? "a" : ""}:${emoji.name}:${emoji.id}>`).catch(e => { utils.log(LT.ERROR, `Failed to send message: ${JSON.stringify(message)} | ${JSON.stringify(e)}`); }); // And attempt to delete if needed @@ -622,10 +644,10 @@ startBot({ // Start up the command prompt for debug usage if (DEBUG) { - utils.cmdPrompt(config.logChannel, config.name, sendMessage); + utils.cmdPrompt(config.logChannel, config.name); } // Start up the API for rolling from third party apps (like excel macros) if (config.api.enable) { - api.start(dbClient, cache, sendMessage, sendDirectMessage); + api.start(dbClient); } diff --git a/src/api.ts b/src/api.ts index 090f045..4311dd7 100644 --- a/src/api.ts +++ b/src/api.ts @@ -6,13 +6,13 @@ import { // Discordeno deps - CacheData, Message, MessageContent, + cache, CreateMessage, + sendMessage, sendDirectMessage, // MySQL Driver deps Client, // httpd deps - serve, Status, STATUS_TEXT, // nanoid deps @@ -27,8 +27,8 @@ import config from "../config.ts"; // start(databaseClient, botCache, sendMessage, sendDirectMessage) returns nothing // start initializes and runs the entire API for the bot -const start = async (dbClient: Client, cache: CacheData, sendMessage: (c: string, m: (string | MessageContent)) => Promise, sendDirectMessage: (c: string, m: (string | MessageContent)) => Promise): Promise => { - const server = serve({ hostname: "0.0.0.0", port: config.api.port }); +const start = async (dbClient: Client): Promise => { + const server = Deno.listen({ port: config.api.port }); utils.log(LT.INFO, `HTTP api running at: http://localhost:${config.api.port}/`); // rateLimitTime holds all users with the last time they started a rate limit timer @@ -37,721 +37,727 @@ const start = async (dbClient: Client, cache: CacheData, sendMessage: (c: string const rateLimitCnt = new Map(); // Catching every request made to the server - for await (const request of server) { - utils.log(LT.LOG, `Handling request: ${JSON.stringify(request)}`); - // Check if user is authenticated to be using this API - let authenticated = false; - let rateLimited = false; - let updateRateLimitTime = false; - let apiUserid = 0n; - let apiUseridStr = ""; - let apiUserEmail = ""; - let apiUserDelCode = ""; + for await (const conn of server) { + (async () => { + const httpConn = Deno.serveHttp(conn); + for await (const requestEvent of httpConn) { + const request = requestEvent.request; + utils.log(LT.LOG, `Handling request: ${JSON.stringify(request)}`); + // Check if user is authenticated to be using this API + let authenticated = false; + let rateLimited = false; + let updateRateLimitTime = false; + let apiUserid = 0n; + let apiUseridStr = ""; + let apiUserEmail = ""; + let apiUserDelCode = ""; - // Check the requests API key - if (request.headers.has("X-Api-Key")) { - // Get the userid and flags for the specific key - const dbApiQuery = await dbClient.query("SELECT userid, email, deleteCode FROM all_keys WHERE apiKey = ? AND active = 1 AND banned = 0", [request.headers.get("X-Api-Key")]); + // Check the requests API key + if (request.headers.has("X-Api-Key")) { + // Get the userid and flags for the specific key + const dbApiQuery = await dbClient.query("SELECT userid, email, deleteCode FROM all_keys WHERE apiKey = ? AND active = 1 AND banned = 0", [request.headers.get("X-Api-Key")]); - // If only one user returned, is not banned, and is currently active, mark as authenticated - if (dbApiQuery.length === 1) { - apiUserid = BigInt(dbApiQuery[0].userid); - apiUserEmail = dbApiQuery[0].email; - apiUserDelCode = dbApiQuery[0].deleteCode; - authenticated = true; + // If only one user returned, is not banned, and is currently active, mark as authenticated + if (dbApiQuery.length === 1) { + apiUserid = BigInt(dbApiQuery[0].userid); + apiUserEmail = dbApiQuery[0].email; + apiUserDelCode = dbApiQuery[0].deleteCode; + authenticated = true; - // Rate limiting inits - apiUseridStr = apiUserid.toString(); - const apiTimeNow = new Date().getTime(); + // Rate limiting inits + apiUseridStr = apiUserid.toString(); + const apiTimeNow = new Date().getTime(); - // Check if user has sent a request recently - if (rateLimitTime.has(apiUseridStr) && (((rateLimitTime.get(apiUseridStr) || 0) + config.api.rateLimitTime) > apiTimeNow)) { - // Get current count - const currentCnt = rateLimitCnt.get(apiUseridStr) || 0; - if (currentCnt < config.api.rateLimitCnt) { - // Limit not yet exceeded, update count - rateLimitCnt.set(apiUseridStr, (currentCnt + 1)); - } else { - // Limit exceeded, prevent API use - rateLimited = true; + // Check if user has sent a request recently + if (rateLimitTime.has(apiUseridStr) && (((rateLimitTime.get(apiUseridStr) || 0) + config.api.rateLimitTime) > apiTimeNow)) { + // Get current count + const currentCnt = rateLimitCnt.get(apiUseridStr) || 0; + if (currentCnt < config.api.rateLimitCnt) { + // Limit not yet exceeded, update count + rateLimitCnt.set(apiUseridStr, (currentCnt + 1)); + } else { + // Limit exceeded, prevent API use + rateLimited = true; + } + } else { + // Update the maps + updateRateLimitTime = true; + rateLimitCnt.set(apiUseridStr, 1); + } } - } else { - // Update the maps - updateRateLimitTime = true; - rateLimitCnt.set(apiUseridStr, 1); } - } - } - if (authenticated && !rateLimited) { - // Get path and query as a string - const [path, tempQ] = request.url.split("?"); + if (authenticated && !rateLimited) { + // Get path and query as a string + const [path, tempQ] = request.url.split("?"); - // Turn the query into a map (if it exists) - const query = new Map(); - if (tempQ !== undefined) { - tempQ.split("&").forEach(e => { - utils.log(LT.LOG, `Breaking down request query: ${request} ${e}`); - const [option, params] = e.split("="); - query.set(option.toLowerCase(), params); - }); - } - - // Handle the request - switch (request.method) { - case "GET": - switch (path.toLowerCase()) { - case "/api/key": - case "/api/key/": - if ((query.has("user") && ((query.get("user") || "").length > 0)) && (query.has("a") && ((query.get("a") || "").length > 0))) { - if (apiUserid === config.api.admin && apiUserid === BigInt(query.get("a"))) { - // Generate new secure key - const newKey = await nanoid(25); - - // Flag to see if there is an error inside the catch - let erroredOut = false; - - // Insert new key/user pair into the db - await dbClient.execute("INSERT INTO all_keys(userid,apiKey) values(?,?)", [apiUserid, newKey]).catch(e => { - utils.log(LT.ERROR, `Failed to insert into database: ${JSON.stringify(e)}`); - request.respond({ status: Status.InternalServerError, body: STATUS_TEXT.get(Status.InternalServerError) }); - erroredOut = true; - }); - - // Exit this case now if catch errored - if (erroredOut) { - break; - } else { - // Send API key as response - request.respond({ status: Status.OK, body: JSON.stringify({ "key": newKey, "userid": query.get("user") }) }); - break; - } - } else { - // Only allow the db admin to use this API - request.respond({ status: Status.Forbidden, body: STATUS_TEXT.get(Status.Forbidden) }); - } - } else { - // Alert API user that they messed up - request.respond({ status: Status.BadRequest, body: STATUS_TEXT.get(Status.BadRequest) }); - } - break; - case "/api/channel": - case "/api/channel/": - if (query.has("user") && ((query.get("user") || "").length > 0)) { - if (apiUserid === BigInt(query.get("user"))) { - // Flag to see if there is an error inside the catch - let erroredOut = false; - - // Get all channels userid has authorized - const dbAllowedChannelQuery = await dbClient.query("SELECT * FROM allowed_channels WHERE userid = ?", [apiUserid]).catch(e => { - utils.log(LT.ERROR, `Failed to insert into database: ${JSON.stringify(e)}`); - request.respond({ status: Status.InternalServerError, body: STATUS_TEXT.get(Status.InternalServerError) }); - erroredOut = true; - }); - - if (erroredOut) { - break; - } else { - // Customized strinification to handle BigInts correctly - const returnChannels = JSON.stringify(dbAllowedChannelQuery, (_key, value) => (typeof value === 'bigint' ? value.toString() : value)); - // Send API key as response - request.respond({ status: Status.OK, body: returnChannels }); - break; - } - } else { - // Alert API user that they shouldn't be doing this - request.respond({ status: Status.Forbidden, body: STATUS_TEXT.get(Status.Forbidden) }); - } - } else { - // Alert API user that they messed up - request.respond({ status: Status.BadRequest, body: STATUS_TEXT.get(Status.BadRequest) }); - } - break; - case "/api/roll": - case "/api/roll/": - // Make sure query contains all the needed parts - if ((query.has("rollstr") && ((query.get("rollstr") || "").length > 0)) && (query.has("channel") && ((query.get("channel") || "").length > 0)) && (query.has("user") && ((query.get("user") || "").length > 0))) { - if (query.has("n") && query.has("m")) { - // Alert API user that they shouldn't be doing this - request.respond({ status: Status.BadRequest, body: STATUS_TEXT.get(Status.BadRequest) }); - break; - } - - // Check if user is authenticated to use this endpoint - let authorized = false; - - // Check if the db has the requested userid/channelid combo, and that the requested userid matches the userid linked with the api key - const dbChannelQuery = await dbClient.query("SELECT active, banned FROM allowed_channels WHERE userid = ? AND channelid = ?", [apiUserid, BigInt(query.get("channel"))]); - if (dbChannelQuery.length === 1 && (apiUserid === BigInt(query.get("user"))) && dbChannelQuery[0].active && !dbChannelQuery[0].banned) { - - // Get the guild from the channel and make sure user is in said guild - const guild = cache.channels.get(query.get("channel") || "")?.guild; - if (guild && guild.members.get(query.get("user") || "")?.id) { - const dbGuildQuery = await dbClient.query("SELECT active, banned FROM allowed_guilds WHERE guildid = ?", [BigInt(guild.id)]); - - // Make sure guild allows API rolls - if (dbGuildQuery.length === 1 && dbGuildQuery[0].active && !dbGuildQuery[0].banned) { - authorized = true; - } - } - } - - if (authorized) { - // Rest of this command is in a try-catch to protect all sends/edits from erroring out - try { - // Flag to tell if roll was completely successful - let errorOut = false; - // Make sure rollCmd is not undefined - let rollCmd = query.get("rollstr") || ""; - const originalCommand = query.get("rollstr"); - - if (rollCmd.length === 0) { - // Alert API user that they messed up - request.respond({ status: Status.BadRequest, body: STATUS_TEXT.get(Status.BadRequest) }); - - // Always log API rolls for abuse detection - dbClient.execute("INSERT INTO roll_log(input,result,resultid,api,error) values(?,?,?,1,1)", [originalCommand, "EmptyInput", null]).catch(e => { - utils.log(LT.ERROR, `Failed to insert into database: ${JSON.stringify(e)}`); - }); - break; - } - - if (query.has("o") && (query.get("o")?.toLowerCase() !== "d" && query.get("o")?.toLowerCase() !== "a")) { - // Alert API user that they messed up - request.respond({ status: Status.BadRequest, body: STATUS_TEXT.get(Status.BadRequest) }); - - // Always log API rolls for abuse detection - dbClient.execute("INSERT INTO roll_log(input,result,resultid,api,error) values(?,?,?,1,1)", [originalCommand, "BadOrder", null]).catch(e => { - utils.log(LT.ERROR, `Failed to insert into database: ${JSON.stringify(e)}`); - }); - break; - } - - // Clip off the leading prefix. API calls must be formatted with a prefix at the start to match how commands are sent in Discord - rollCmd = rollCmd.substr(rollCmd.indexOf(config.prefix) + 2).replace(/%20/g, " "); - - // Parse the roll and get the return text - const returnmsg = solver.parseRoll(rollCmd, config.prefix, config.postfix, query.has("m"), query.has("n"), query.has("o") ? (query.get("o")?.toLowerCase() || "") : ""); - - // Alert users why this message just appeared and how they can report abues pf this feature - const apiPrefix = "The following roll was conducted using my built in API. If someone in this channel did not request this roll, please report API abuse here: <" + config.api.supportURL + ">\n\n"; - let m, returnText = ""; - - // Handle sending the error message to whoever called the api - if (returnmsg.error) { - request.respond({ status: Status.InternalServerError, body: returnmsg.errorMsg }); - - // Always log API rolls for abuse detection - dbClient.execute("INSERT INTO roll_log(input,result,resultid,api,error) values(?,?,?,1,1)", [originalCommand, returnmsg.errorCode, null]).catch(e => { - utils.log(LT.ERROR, `Failed to insert into database: ${JSON.stringify(e)}`); - }); - break; - } else { - returnText = apiPrefix + "<@" + query.get("user") + ">" + returnmsg.line1 + "\n" + returnmsg.line2; - let spoilerTxt = ""; - - // Determine if spoiler flag was on - if (query.has("s")) { - spoilerTxt = "||"; - } - - // Determine if no details flag was on - if (query.has("nd")) { - returnText += "\nDetails suppressed by nd query."; - } else { - returnText += "\nDetails:\n" + spoilerTxt + returnmsg.line3 + spoilerTxt; - } - } - - // If the roll was a GM roll, send DMs to all the GMs - if (query.has("gms")) { - // Get all the GM user IDs from the query - const gms = (query.get("gms") || "").split(","); - if (gms.length === 0) { - // Alert API user that they messed up - request.respond({ status: Status.BadRequest, body: STATUS_TEXT.get(Status.BadRequest) }); - - // Always log API rolls for abuse detection - dbClient.execute("INSERT INTO roll_log(input,result,resultid,api,error) values(?,?,?,1,1)", [originalCommand, "NoGMsSent", null]).catch(e => { - utils.log(LT.ERROR, `Failed to insert into database: ${JSON.stringify(e)}`); - }); - break; - } - - // Make a new return line to be sent to the roller - let normalText = apiPrefix + "<@" + query.get("user") + ">" + returnmsg.line1 + "\nResults have been messaged to the following GMs: "; - gms.forEach(e => { - utils.log(LT.LOG, `Appending GM ${e} to roll text`); - normalText += "<@" + e + "> "; - }); - - // Send the return message as a DM or normal message depening on if the channel is set - if ((query.get("channel") || "").length > 0) { - m = await sendMessage(query.get("channel") || "", normalText).catch(() => { - request.respond({ status: Status.InternalServerError, body: "Message 00 failed to send." }); - errorOut = true; - }); - } else { - m = await sendDirectMessage(query.get("user") || "", normalText).catch(() => { - request.respond({ status: Status.InternalServerError, body: "Message 01 failed to send." }); - errorOut = true; - }); - } - - const newMessage: MessageContent = {}; - // If its too big, collapse it into a .txt file and send that instead. - const b = await new Blob([returnText as BlobPart], { "type": "text" }); - - if (b.size > 8388290) { - // Update return text - newMessage.content = apiPrefix + "<@" + query.get("user") + ">" + returnmsg.line1 + "\n" + returnmsg.line2 + "\nDetails have been ommitted from this message for being over 2000 characters. Full details could not be attached to this messaged as a `.txt` file as the file would be too large for Discord to handle. If you would like to see the details of rolls, please send the rolls in multiple messages instead of bundled into one."; - } else { - // Update return text - newMessage.content = apiPrefix + "<@" + query.get("user") + ">" + returnmsg.line1 + "\n" + returnmsg.line2 + "\nFull details have been attached to this messaged as a `.txt` file for verification purposes."; - newMessage.file = { "blob": b, "name": "rollDetails.txt" }; - } - - // And message the full details to each of the GMs, alerting roller of every GM that could not be messaged - gms.forEach(async e => { - utils.log(LT.LOG, `Messaging GM ${e} roll results`); - // Attempt to DM the GMs and send a warning if it could not DM a GM - await sendDirectMessage(e, newMessage).catch(async () => { - const failedSend = "WARNING: <@" + e + "> could not be messaged. If this issue persists, make sure direct messages are allowed from this server." - // Send the return message as a DM or normal message depening on if the channel is set - if ((query.get("channel") || "").length > 0) { - m = await sendMessage(query.get("channel") || "", failedSend).catch(() => { - request.respond({ status: Status.InternalServerError, body: "Message failed to send." }); - errorOut = true; - }); - } else { - m = await sendDirectMessage(query.get("user") || "", failedSend).catch(() => { - request.respond({ status: Status.InternalServerError, body: "Message failed to send." }); - errorOut = true; - }); - } - }); - }); - - // Always log API rolls for abuse detection - dbClient.execute("INSERT INTO roll_log(input,result,resultid,api,error) values(?,?,?,1,0)", [originalCommand, returnText, ((typeof m === "object") ? m.id : null)]).catch(e => { - utils.log(LT.ERROR, `Failed to insert into database: ${JSON.stringify(e)}`); - }); - - // Handle closing the request out - if (errorOut) { - break; - } else { - request.respond({ status: Status.OK, body: normalText }); - break; - } - } else { - const newMessage: MessageContent = {}; - newMessage.content = returnText; - - // When not a GM roll, make sure the message is not too big - if (returnText.length > 2000) { - // If its too big, collapse it into a .txt file and send that instead. - const b = await new Blob([returnText as BlobPart], { "type": "text" }); - - if (b.size > 8388290) { - // Update return text - newMessage.content = apiPrefix + "<@" + query.get("user") + ">" + returnmsg.line1 + "\n" + returnmsg.line2 + "\nDetails have been ommitted from this message for being over 2000 characters. Full details could not be attached to this messaged as a `.txt` file as the file would be too large for Discord to handle. If you would like to see the details of rolls, please send the rolls in multiple messages instead of bundled into one."; - } else { - // Update return text - newMessage.content = apiPrefix + "<@" + query.get("user") + ">" + returnmsg.line1 + "\n" + returnmsg.line2 + "\nDetails have been ommitted from this message for being over 2000 characters. Full details have been attached to this messaged as a `.txt` file for verification purposes."; - newMessage.file = { "blob": b, "name": "rollDetails.txt" }; - } - } - - // Send the return message as a DM or normal message depening on if the channel is set - if ((query.get("channel") || "").length > 0) { - m = await sendMessage(query.get("channel") || "", newMessage).catch(() => { - request.respond({ status: Status.InternalServerError, body: "Message 20 failed to send." }); - errorOut = true; - }); - } else { - m = await sendDirectMessage(query.get("user") || "", newMessage).catch(() => { - request.respond({ status: Status.InternalServerError, body: "Message 21 failed to send." }); - errorOut = true; - }); - } - - // If enabled, log rolls so we can verify the bots math - dbClient.execute("INSERT INTO roll_log(input,result,resultid,api,error) values(?,?,?,1,0)", [originalCommand, returnText, ((typeof m === "object") ? m.id : null)]).catch(e => { - utils.log(LT.ERROR, `Failed to insert into database: ${JSON.stringify(e)}`); - }); - - // Handle closing the request out - if (errorOut) { - break; - } else { - request.respond({ status: Status.OK, body: returnText }); - break; - } - } - } catch (err) { - // Handle any errors we missed - utils.log(LT.ERROR, `Unhandled Error: ${JSON.stringify(err)}`); - request.respond({ status: Status.InternalServerError, body: STATUS_TEXT.get(Status.InternalServerError) }); - } - } else { - // Alert API user that they messed up - request.respond({ status: Status.Forbidden, body: STATUS_TEXT.get(Status.Forbidden) }); - } - } else { - // Alert API user that they shouldn't be doing this - request.respond({ status: Status.BadRequest, body: STATUS_TEXT.get(Status.BadRequest) }); - } - break; - default: - // Alert API user that they messed up - request.respond({ status: Status.NotFound, body: STATUS_TEXT.get(Status.NotFound) }); - break; + // Turn the query into a map (if it exists) + const query = new Map(); + if (tempQ !== undefined) { + tempQ.split("&").forEach((e: string) => { + utils.log(LT.LOG, `Breaking down request query: ${request} ${e}`); + const [option, params] = e.split("="); + query.set(option.toLowerCase(), params); + }); } - break; - case "POST": - switch (path.toLowerCase()) { - case "/api/channel/add": - case "/api/channel/add/": - if ((query.has("user") && ((query.get("user") || "").length > 0)) && (query.has("channel") && ((query.get("channel") || "").length > 0))) { - if (apiUserid === BigInt(query.get("user"))) { - // Flag to see if there is an error inside the catch - let erroredOut = false; - // Insert new user/channel pair into the db - await dbClient.execute("INSERT INTO allowed_channels(userid,channelid) values(?,?)", [apiUserid, BigInt(query.get("channel"))]).catch(e => { - utils.log(LT.ERROR, `Failed to insert into database: ${JSON.stringify(e)}`); - request.respond({ status: Status.InternalServerError, body: STATUS_TEXT.get(Status.InternalServerError) }); - erroredOut = true; - }); + // Handle the request + switch (request.method) { + case "GET": + switch (path.toLowerCase()) { + case "/api/key": + case "/api/key/": + if ((query.has("user") && ((query.get("user") || "").length > 0)) && (query.has("a") && ((query.get("a") || "").length > 0))) { + if (apiUserid === config.api.admin && apiUserid === BigInt(query.get("a") || "0")) { + // Generate new secure key + const newKey = await nanoid(25); - // Exit this case now if catch errored - if (erroredOut) { - break; - } else { - // Send API key as response - request.respond({ status: Status.OK, body: STATUS_TEXT.get(Status.OK) }); - break; - } - } else { - // Alert API user that they shouldn't be doing this - request.respond({ status: Status.Forbidden, body: STATUS_TEXT.get(Status.Forbidden) }); - } - } else { - // Alert API user that they messed up - request.respond({ status: Status.BadRequest, body: STATUS_TEXT.get(Status.BadRequest) }); - } - break; - default: - // Alert API user that they messed up - request.respond({ status: Status.NotFound, body: STATUS_TEXT.get(Status.NotFound) }); - break; - } - break; - case "PUT": - switch (path.toLowerCase()) { - case "/api/key/ban": - case "/api/key/ban/": - case "/api/key/unban": - case "/api/key/unban/": - case "/api/key/activate": - case "/api/key/activate/": - case "/api/key/deactivate": - case "/api/key/deactivate/": - if ((query.has("a") && ((query.get("a") || "").length > 0)) && (query.has("user") && ((query.get("user") || "").length > 0))) { - if (apiUserid === config.api.admin && apiUserid === BigInt(query.get("a"))) { - // Flag to see if there is an error inside the catch - let key, value, erroredOut = false; - - // Determine key to edit - if (path.toLowerCase().indexOf("ban") > 0) { - key = "banned"; - } else { - key = "active"; - } - - // Determine value to set - if (path.toLowerCase().indexOf("de") > 0 || path.toLowerCase().indexOf("un") > 0) { - value = 0; - } else { - value = 1; - } - - // Execute the DB modification - await dbClient.execute("UPDATE all_keys SET ?? = ? WHERE userid = ?", [key, value, apiUserid]).catch(e => { - utils.log(LT.ERROR, `Failed to insert into database: ${JSON.stringify(e)}`); - request.respond({ status: Status.InternalServerError, body: STATUS_TEXT.get(Status.InternalServerError) }); - erroredOut = true; - }); - - // Exit this case now if catch errored - if (erroredOut) { - break; - } else { - // Send API key as response - request.respond({ status: Status.OK, body: STATUS_TEXT.get(Status.OK) }); - break; - } - } else { - // Alert API user that they shouldn't be doing this - request.respond({ status: Status.Forbidden, body: STATUS_TEXT.get(Status.Forbidden) }); - } - } else { - // Alert API user that they messed up - request.respond({ status: Status.BadRequest, body: STATUS_TEXT.get(Status.BadRequest) }); - } - break; - case "/api/channel/ban": - case "/api/channel/ban/": - case "/api/channel/unban": - case "/api/channel/unban/": - if ((query.has("a") && ((query.get("a") || "").length > 0)) && (query.has("channel") && ((query.get("channel") || "").length > 0)) && (query.has("user") && ((query.get("user") || "").length > 0))) { - if (apiUserid === config.api.admin && apiUserid === BigInt(query.get("a"))) { - // Flag to see if there is an error inside the catch - let value, erroredOut = false; - - // Determine value to set - if (path.toLowerCase().indexOf("un") > 0) { - value = 0; - } else { - value = 1; - } - - // Execute the DB modification - await dbClient.execute("UPDATE allowed_channels SET banned = ? WHERE userid = ? AND channelid = ?", [value, apiUserid, BigInt(query.get("channel"))]).catch(e => { - utils.log(LT.ERROR, `Failed to insert into database: ${JSON.stringify(e)}`); - request.respond({ status: Status.InternalServerError, body: STATUS_TEXT.get(Status.InternalServerError) }); - erroredOut = true; - }); - - // Exit this case now if catch errored - if (erroredOut) { - break; - } else { - // Send API key as response - request.respond({ status: Status.OK, body: STATUS_TEXT.get(Status.OK) }); - break; - } - } else { - // Alert API user that they shouldn't be doing this - request.respond({ status: Status.Forbidden, body: STATUS_TEXT.get(Status.Forbidden) }); - } - } else { - // Alert API user that they messed up - request.respond({ status: Status.BadRequest, body: STATUS_TEXT.get(Status.BadRequest) }); - } - break; - case "/api/channel/activate": - case "/api/channel/activate/": - case "/api/channel/deactivate": - case "/api/channel/deactivate/": - if ((query.has("channel") && ((query.get("channel") || "").length > 0)) && (query.has("user") && ((query.get("user") || "").length > 0))) { - if (apiUserid === BigInt(query.get("user"))) { - // Flag to see if there is an error inside the catch - let value, erroredOut = false; - - // Determine value to set - if (path.toLowerCase().indexOf("de") > 0) { - value = 0; - } else { - value = 1; - } - - // Update the requested entry - await dbClient.execute("UPDATE allowed_channels SET active = ? WHERE userid = ? AND channelid = ?", [value, apiUserid, BigInt(query.get("channel"))]).catch(e => { - utils.log(LT.ERROR, `Failed to insert into database: ${JSON.stringify(e)}`); - request.respond({ status: Status.InternalServerError, body: STATUS_TEXT.get(Status.InternalServerError) }); - erroredOut = true; - }); - - // Exit this case now if catch errored - if (erroredOut) { - break; - } else { - // Send API key as response - request.respond({ status: Status.OK, body: STATUS_TEXT.get(Status.OK) }); - break; - } - } else { - // Alert API user that they shouldn't be doing this - request.respond({ status: Status.Forbidden, body: STATUS_TEXT.get(Status.Forbidden) }); - } - } else { - // Alert API user that they messed up - request.respond({ status: Status.BadRequest, body: STATUS_TEXT.get(Status.BadRequest) }); - } - break; - default: - // Alert API user that they messed up - request.respond({ status: Status.NotFound, body: STATUS_TEXT.get(Status.NotFound) }); - break; - } - break; - case "DELETE": - switch (path.toLowerCase()) { - case "/api/key/delete": - case "/api/key/delete/": - if (query.has("user") && ((query.get("user") || "").length > 0) && query.has("email") && ((query.get("email") || "").length > 0)) { - if (apiUserid === BigInt(query.get("user")) && apiUserEmail === query.get("email")) { - if (query.has("code") && ((query.get("code") || "").length > 0)) { - if ((query.get("code") || "") === apiUserDelCode) { - // User has recieved their delete code and we need to delete the account now + // Flag to see if there is an error inside the catch let erroredOut = false; - await dbClient.execute("DELETE FROM allowed_channels WHERE userid = ?", [apiUserid]).catch(e => { + // Insert new key/user pair into the db + await dbClient.execute("INSERT INTO all_keys(userid,apiKey) values(?,?)", [apiUserid, newKey]).catch(e => { utils.log(LT.ERROR, `Failed to insert into database: ${JSON.stringify(e)}`); - request.respond({ status: Status.InternalServerError, body: STATUS_TEXT.get(Status.InternalServerError) }); + requestEvent.respondWith(new Response(STATUS_TEXT.get(Status.InternalServerError), { status: Status.InternalServerError })); erroredOut = true; }); - if (erroredOut) { - break; - } - await dbClient.execute("DELETE FROM all_keys WHERE userid = ?", [apiUserid]).catch(e => { - utils.log(LT.ERROR, `Failed to insert into database: ${JSON.stringify(e)}`); - request.respond({ status: Status.InternalServerError, body: STATUS_TEXT.get(Status.InternalServerError) }); - erroredOut = true; - }); + // Exit this case now if catch errored if (erroredOut) { break; } else { // Send API key as response - request.respond({ status: Status.OK, body: STATUS_TEXT.get(Status.OK) }); + requestEvent.respondWith(new Response(JSON.stringify({ "key": newKey, "userid": query.get("user") }), { status: Status.OK })); + break; + } + } else { + // Only allow the db admin to use this API + requestEvent.respondWith(new Response(STATUS_TEXT.get(Status.Forbidden), { status: Status.Forbidden })); + } + } else { + // Alert API user that they messed up + requestEvent.respondWith(new Response(STATUS_TEXT.get(Status.BadRequest), { status: Status.BadRequest })); + } + break; + case "/api/channel": + case "/api/channel/": + if (query.has("user") && ((query.get("user") || "").length > 0)) { + if (apiUserid === BigInt(query.get("user") || "0")) { + // Flag to see if there is an error inside the catch + let erroredOut = false; + + // Get all channels userid has authorized + const dbAllowedChannelQuery = await dbClient.query("SELECT * FROM allowed_channels WHERE userid = ?", [apiUserid]).catch(e => { + utils.log(LT.ERROR, `Failed to insert into database: ${JSON.stringify(e)}`); + requestEvent.respondWith(new Response(STATUS_TEXT.get(Status.InternalServerError), { status: Status.InternalServerError })); + erroredOut = true; + }); + + if (erroredOut) { + break; + } else { + // Customized strinification to handle BigInts correctly + const returnChannels = JSON.stringify(dbAllowedChannelQuery, (_key, value) => (typeof value === 'bigint' ? value.toString() : value)); + // Send API key as response + requestEvent.respondWith(new Response(returnChannels, { status: Status.OK })); break; } } else { // Alert API user that they shouldn't be doing this - request.respond({ status: Status.Forbidden, body: STATUS_TEXT.get(Status.Forbidden) }); + requestEvent.respondWith(new Response(STATUS_TEXT.get(Status.Forbidden), { status: Status.Forbidden })); } } else { - // User does not have their delete code yet, so we need to generate one and email it to them - const deleteCode = await nanoid(10); - - let erroredOut = false; - - // Execute the DB modification - await dbClient.execute("UPDATE all_keys SET deleteCode = ? WHERE userid = ?", [deleteCode, apiUserid]).catch(e => { - utils.log(LT.ERROR, `Failed to insert into database: ${JSON.stringify(e)}`); - request.respond({ status: Status.InternalServerError, body: STATUS_TEXT.get(Status.InternalServerError) }); - erroredOut = true; - }); - if (erroredOut) { + // Alert API user that they messed up + requestEvent.respondWith(new Response(STATUS_TEXT.get(Status.BadRequest), { status: Status.BadRequest })); + } + break; + case "/api/roll": + case "/api/roll/": + // Make sure query contains all the needed parts + if ((query.has("rollstr") && ((query.get("rollstr") || "").length > 0)) && (query.has("channel") && ((query.get("channel") || "").length > 0)) && (query.has("user") && ((query.get("user") || "").length > 0))) { + if (query.has("n") && query.has("m")) { + // Alert API user that they shouldn't be doing this + requestEvent.respondWith(new Response(STATUS_TEXT.get(Status.BadRequest), { status: Status.BadRequest })); break; } - // "Send" the email - await sendMessage(config.api.email, `<@${config.api.admin}> A USER HAS REQUESTED A DELETE CODE\n\nEmail Address: ${apiUserEmail}\n\nSubject: \`Artificer API Delete Code\`\n\n\`\`\`Hello Artificer API User,\n\nI am sorry to see you go. If you would like, please respond to this email detailing what I could have done better.\n\nAs requested, here is your delete code: ${deleteCode}\n\nSorry to see you go,\nThe Artificer Developer - Ean Milligan\`\`\``).catch(() => { - request.respond({ status: Status.InternalServerError, body: "Message 30 failed to send." }); + // Check if user is authenticated to use this endpoint + let authorized = false; + + // Check if the db has the requested userid/channelid combo, and that the requested userid matches the userid linked with the api key + const dbChannelQuery = await dbClient.query("SELECT active, banned FROM allowed_channels WHERE userid = ? AND channelid = ?", [apiUserid, BigInt(query.get("channel") || "0")]); + if (dbChannelQuery.length === 1 && (apiUserid === BigInt(query.get("user") || "0")) && dbChannelQuery[0].active && !dbChannelQuery[0].banned) { + + // Get the guild from the channel and make sure user is in said guild + const guild = cache.channels.get(BigInt(query.get("channel") || ""))?.guild; + if (guild && guild.members.get(BigInt(query.get("user") || ""))?.id) { + const dbGuildQuery = await dbClient.query("SELECT active, banned FROM allowed_guilds WHERE guildid = ?", [BigInt(guild.id)]); + + // Make sure guild allows API rolls + if (dbGuildQuery.length === 1 && dbGuildQuery[0].active && !dbGuildQuery[0].banned) { + authorized = true; + } + } + } + + if (authorized) { + // Rest of this command is in a try-catch to protect all sends/edits from erroring out + try { + // Flag to tell if roll was completely successful + let errorOut = false; + // Make sure rollCmd is not undefined + let rollCmd = query.get("rollstr") || ""; + const originalCommand = query.get("rollstr"); + + if (rollCmd.length === 0) { + // Alert API user that they messed up + requestEvent.respondWith(new Response(STATUS_TEXT.get(Status.BadRequest), { status: Status.BadRequest })); + + // Always log API rolls for abuse detection + dbClient.execute("INSERT INTO roll_log(input,result,resultid,api,error) values(?,?,?,1,1)", [originalCommand, "EmptyInput", null]).catch(e => { + utils.log(LT.ERROR, `Failed to insert into database: ${JSON.stringify(e)}`); + }); + break; + } + + if (query.has("o") && (query.get("o")?.toLowerCase() !== "d" && query.get("o")?.toLowerCase() !== "a")) { + // Alert API user that they messed up + requestEvent.respondWith(new Response(STATUS_TEXT.get(Status.BadRequest), { status: Status.BadRequest })); + + // Always log API rolls for abuse detection + dbClient.execute("INSERT INTO roll_log(input,result,resultid,api,error) values(?,?,?,1,1)", [originalCommand, "BadOrder", null]).catch(e => { + utils.log(LT.ERROR, `Failed to insert into database: ${JSON.stringify(e)}`); + }); + break; + } + + // Clip off the leading prefix. API calls must be formatted with a prefix at the start to match how commands are sent in Discord + rollCmd = rollCmd.substr(rollCmd.indexOf(config.prefix) + 2).replace(/%20/g, " "); + + // Parse the roll and get the return text + const returnmsg = solver.parseRoll(rollCmd, config.prefix, config.postfix, query.has("m"), query.has("n"), query.has("o") ? (query.get("o")?.toLowerCase() || "") : ""); + + // Alert users why this message just appeared and how they can report abues pf this feature + const apiPrefix = `The following roll was conducted using my built in API. If someone in this channel did not request this roll, please report API abuse here: <${config.api.supportURL}>\n\n`; + let m, returnText = ""; + + // Handle sending the error message to whoever called the api + if (returnmsg.error) { + requestEvent.respondWith(new Response(returnmsg.errorMsg, { status: Status.InternalServerError })); + + // Always log API rolls for abuse detection + dbClient.execute("INSERT INTO roll_log(input,result,resultid,api,error) values(?,?,?,1,1)", [originalCommand, returnmsg.errorCode, null]).catch(e => { + utils.log(LT.ERROR, `Failed to insert into database: ${JSON.stringify(e)}`); + }); + break; + } else { + returnText = `${apiPrefix}<@${query.get("user")}>${returnmsg.line1}\n${returnmsg.line2}`; + let spoilerTxt = ""; + + // Determine if spoiler flag was on + if (query.has("s")) { + spoilerTxt = "||"; + } + + // Determine if no details flag was on + if (query.has("nd")) { + returnText += "\nDetails suppressed by nd query."; + } else { + returnText += `\nDetails:\n${spoilerTxt}${returnmsg.line3}${spoilerTxt}`; + } + } + + // If the roll was a GM roll, send DMs to all the GMs + if (query.has("gms")) { + // Get all the GM user IDs from the query + const gms = (query.get("gms") || "").split(","); + if (gms.length === 0) { + // Alert API user that they messed up + requestEvent.respondWith(new Response(STATUS_TEXT.get(Status.BadRequest), { status: Status.BadRequest })); + + // Always log API rolls for abuse detection + dbClient.execute("INSERT INTO roll_log(input,result,resultid,api,error) values(?,?,?,1,1)", [originalCommand, "NoGMsSent", null]).catch(e => { + utils.log(LT.ERROR, `Failed to insert into database: ${JSON.stringify(e)}`); + }); + break; + } + + // Make a new return line to be sent to the roller + let normalText = `${apiPrefix}<@${query.get("user")}>${returnmsg.line1}\nResults have been messaged to the following GMs: `; + gms.forEach(e => { + utils.log(LT.LOG, `Appending GM ${e} to roll text`); + normalText += `<@${e}> `; + }); + + // Send the return message as a DM or normal message depening on if the channel is set + if ((query.get("channel") || "").length > 0) { + m = await sendMessage(BigInt(query.get("channel") || ""), normalText).catch(() => { + requestEvent.respondWith(new Response("Message 00 failed to send.", { status: Status.InternalServerError })); + errorOut = true; + }); + } else { + m = await sendDirectMessage(BigInt(query.get("user") || ""), normalText).catch(() => { + requestEvent.respondWith(new Response("Message 01 failed to send.", { status: Status.InternalServerError })); + errorOut = true; + }); + } + + const newMessage: CreateMessage = {}; + // If its too big, collapse it into a .txt file and send that instead. + const b = await new Blob([returnText as BlobPart], { "type": "text" }); + + if (b.size > 8388290) { + // Update return text + newMessage.content = `${apiPrefix}<@${query.get("user")}>${returnmsg.line1}\n${returnmsg.line2}\nDetails have been ommitted from this message for being over 2000 characters. Full details could not be attached to this messaged as a \`.txt\` file as the file would be too large for Discord to handle. If you would like to see the details of rolls, please send the rolls in multiple messages instead of bundled into one.`; + } else { + // Update return text + newMessage.content = `${apiPrefix}<@${query.get("user")}>${returnmsg.line1}\n${returnmsg.line2}\nFull details have been attached to this messaged as a \`.txt\` file for verification purposes.`; + newMessage.file = { "blob": b, "name": "rollDetails.txt" }; + } + + // And message the full details to each of the GMs, alerting roller of every GM that could not be messaged + gms.forEach(async e => { + utils.log(LT.LOG, `Messaging GM ${e} roll results`); + // Attempt to DM the GMs and send a warning if it could not DM a GM + await sendDirectMessage(BigInt(e), newMessage).catch(async () => { + const failedSend = `WARNING: <@${e}> could not be messaged. If this issue persists, make sure direct messages are allowed from this server.` + // Send the return message as a DM or normal message depening on if the channel is set + if ((query.get("channel") || "").length > 0) { + m = await sendMessage(BigInt(query.get("channel") || ""), failedSend).catch(() => { + requestEvent.respondWith(new Response("Message failed to send.", { status: Status.InternalServerError })); + errorOut = true; + }); + } else { + m = await sendDirectMessage(BigInt(query.get("user") || ""), failedSend).catch(() => { + requestEvent.respondWith(new Response("Message failed to send.", { status: Status.InternalServerError })); + errorOut = true; + }); + } + }); + }); + + // Always log API rolls for abuse detection + dbClient.execute("INSERT INTO roll_log(input,result,resultid,api,error) values(?,?,?,1,0)", [originalCommand, returnText, ((typeof m === "object") ? m.id : null)]).catch(e => { + utils.log(LT.ERROR, `Failed to insert into database: ${JSON.stringify(e)}`); + }); + + // Handle closing the request out + if (errorOut) { + break; + } else { + requestEvent.respondWith(new Response(normalText, { status: Status.OK })); + break; + } + } else { + const newMessage: CreateMessage = {}; + newMessage.content = returnText; + + // When not a GM roll, make sure the message is not too big + if (returnText.length > 2000) { + // If its too big, collapse it into a .txt file and send that instead. + const b = await new Blob([returnText as BlobPart], { "type": "text" }); + + if (b.size > 8388290) { + // Update return text + newMessage.content = `${apiPrefix}<@${query.get("user")}>${returnmsg.line1}\n${returnmsg.line2}\nDetails have been ommitted from this message for being over 2000 characters. Full details could not be attached to this messaged as a \`.txt\` file as the file would be too large for Discord to handle. If you would like to see the details of rolls, please send the rolls in multiple messages instead of bundled into one.`; + } else { + // Update return text + newMessage.content = `${apiPrefix}<@${query.get("user")}>${returnmsg.line1}\n${returnmsg.line2}\nDetails have been ommitted from this message for being over 2000 characters. Full details have been attached to this messaged as a \`.txt\` file for verification purposes.`; + newMessage.file = { "blob": b, "name": "rollDetails.txt" }; + } + } + + // Send the return message as a DM or normal message depening on if the channel is set + if ((query.get("channel") || "").length > 0) { + m = await sendMessage(BigInt(query.get("channel") || ""), newMessage).catch(() => { + requestEvent.respondWith(new Response("Message 20 failed to send.", { status: Status.InternalServerError })); + errorOut = true; + }); + } else { + m = await sendDirectMessage(BigInt(query.get("user") || ""), newMessage).catch(() => { + requestEvent.respondWith(new Response("Message 21 failed to send.", { status: Status.InternalServerError })); + errorOut = true; + }); + } + + // If enabled, log rolls so we can verify the bots math + dbClient.execute("INSERT INTO roll_log(input,result,resultid,api,error) values(?,?,?,1,0)", [originalCommand, returnText, ((typeof m === "object") ? m.id : null)]).catch(e => { + utils.log(LT.ERROR, `Failed to insert into database: ${JSON.stringify(e)}`); + }); + + // Handle closing the request out + if (errorOut) { + break; + } else { + requestEvent.respondWith(new Response(returnText, { status: Status.OK })); + break; + } + } + } catch (err) { + // Handle any errors we missed + utils.log(LT.ERROR, `Unhandled Error: ${JSON.stringify(err)}`); + requestEvent.respondWith(new Response(STATUS_TEXT.get(Status.InternalServerError), { status: Status.InternalServerError })); + } + } else { + // Alert API user that they messed up + requestEvent.respondWith(new Response(STATUS_TEXT.get(Status.Forbidden), { status: Status.Forbidden })); + } + } else { + // Alert API user that they shouldn't be doing this + requestEvent.respondWith(new Response(STATUS_TEXT.get(Status.BadRequest), { status: Status.BadRequest })); + } + break; + default: + // Alert API user that they messed up + requestEvent.respondWith(new Response(STATUS_TEXT.get(Status.NotFound), { status: Status.NotFound })); + break; + } + break; + case "POST": + switch (path.toLowerCase()) { + case "/api/channel/add": + case "/api/channel/add/": + if ((query.has("user") && ((query.get("user") || "").length > 0)) && (query.has("channel") && ((query.get("channel") || "").length > 0))) { + if (apiUserid === BigInt(query.get("user") || "0")) { + // Flag to see if there is an error inside the catch + let erroredOut = false; + + // Insert new user/channel pair into the db + await dbClient.execute("INSERT INTO allowed_channels(userid,channelid) values(?,?)", [apiUserid, BigInt(query.get("channel") || "0")]).catch(e => { + utils.log(LT.ERROR, `Failed to insert into database: ${JSON.stringify(e)}`); + requestEvent.respondWith(new Response(STATUS_TEXT.get(Status.InternalServerError), { status: Status.InternalServerError })); + erroredOut = true; + }); + + // Exit this case now if catch errored + if (erroredOut) { + break; + } else { + // Send API key as response + requestEvent.respondWith(new Response(STATUS_TEXT.get(Status.OK), { status: Status.OK })); + break; + } + } else { + // Alert API user that they shouldn't be doing this + requestEvent.respondWith(new Response(STATUS_TEXT.get(Status.Forbidden), { status: Status.Forbidden })); + } + } else { + // Alert API user that they messed up + requestEvent.respondWith(new Response(STATUS_TEXT.get(Status.BadRequest), { status: Status.BadRequest })); + } + break; + default: + // Alert API user that they messed up + requestEvent.respondWith(new Response(STATUS_TEXT.get(Status.NotFound), { status: Status.NotFound })); + break; + } + break; + case "PUT": + switch (path.toLowerCase()) { + case "/api/key/ban": + case "/api/key/ban/": + case "/api/key/unban": + case "/api/key/unban/": + case "/api/key/activate": + case "/api/key/activate/": + case "/api/key/deactivate": + case "/api/key/deactivate/": + if ((query.has("a") && ((query.get("a") || "").length > 0)) && (query.has("user") && ((query.get("user") || "").length > 0))) { + if (apiUserid === config.api.admin && apiUserid === BigInt(query.get("a") || "0")) { + // Flag to see if there is an error inside the catch + let key, value, erroredOut = false; + + // Determine key to edit + if (path.toLowerCase().indexOf("ban") > 0) { + key = "banned"; + } else { + key = "active"; + } + + // Determine value to set + if (path.toLowerCase().indexOf("de") > 0 || path.toLowerCase().indexOf("un") > 0) { + value = 0; + } else { + value = 1; + } + + // Execute the DB modification + await dbClient.execute("UPDATE all_keys SET ?? = ? WHERE userid = ?", [key, value, apiUserid]).catch(e => { + utils.log(LT.ERROR, `Failed to insert into database: ${JSON.stringify(e)}`); + requestEvent.respondWith(new Response(STATUS_TEXT.get(Status.InternalServerError), { status: Status.InternalServerError })); + erroredOut = true; + }); + + // Exit this case now if catch errored + if (erroredOut) { + break; + } else { + // Send API key as response + requestEvent.respondWith(new Response(STATUS_TEXT.get(Status.OK), { status: Status.OK })); + break; + } + } else { + // Alert API user that they shouldn't be doing this + requestEvent.respondWith(new Response(STATUS_TEXT.get(Status.Forbidden), { status: Status.Forbidden })); + } + } else { + // Alert API user that they messed up + requestEvent.respondWith(new Response(STATUS_TEXT.get(Status.BadRequest), { status: Status.BadRequest })); + } + break; + case "/api/channel/ban": + case "/api/channel/ban/": + case "/api/channel/unban": + case "/api/channel/unban/": + if ((query.has("a") && ((query.get("a") || "").length > 0)) && (query.has("channel") && ((query.get("channel") || "").length > 0)) && (query.has("user") && ((query.get("user") || "").length > 0))) { + if (apiUserid === config.api.admin && apiUserid === BigInt(query.get("a") || "0")) { + // Flag to see if there is an error inside the catch + let value, erroredOut = false; + + // Determine value to set + if (path.toLowerCase().indexOf("un") > 0) { + value = 0; + } else { + value = 1; + } + + // Execute the DB modification + await dbClient.execute("UPDATE allowed_channels SET banned = ? WHERE userid = ? AND channelid = ?", [value, apiUserid, BigInt(query.get("channel") || "0")]).catch(e => { + utils.log(LT.ERROR, `Failed to insert into database: ${JSON.stringify(e)}`); + requestEvent.respondWith(new Response(STATUS_TEXT.get(Status.InternalServerError), { status: Status.InternalServerError })); + erroredOut = true; + }); + + // Exit this case now if catch errored + if (erroredOut) { + break; + } else { + // Send API key as response + requestEvent.respondWith(new Response(STATUS_TEXT.get(Status.OK), { status: Status.OK })); + break; + } + } else { + // Alert API user that they shouldn't be doing this + requestEvent.respondWith(new Response(STATUS_TEXT.get(Status.Forbidden), { status: Status.Forbidden })); + } + } else { + // Alert API user that they messed up + requestEvent.respondWith(new Response(STATUS_TEXT.get(Status.BadRequest), { status: Status.BadRequest })); + } + break; + case "/api/channel/activate": + case "/api/channel/activate/": + case "/api/channel/deactivate": + case "/api/channel/deactivate/": + if ((query.has("channel") && ((query.get("channel") || "").length > 0)) && (query.has("user") && ((query.get("user") || "").length > 0))) { + if (apiUserid === BigInt(query.get("user") || "0")) { + // Flag to see if there is an error inside the catch + let value, erroredOut = false; + + // Determine value to set + if (path.toLowerCase().indexOf("de") > 0) { + value = 0; + } else { + value = 1; + } + + // Update the requested entry + await dbClient.execute("UPDATE allowed_channels SET active = ? WHERE userid = ? AND channelid = ?", [value, apiUserid, BigInt(query.get("channel") || "0")]).catch(e => { + utils.log(LT.ERROR, `Failed to insert into database: ${JSON.stringify(e)}`); + requestEvent.respondWith(new Response(STATUS_TEXT.get(Status.InternalServerError), { status: Status.InternalServerError })); + erroredOut = true; + }); + + // Exit this case now if catch errored + if (erroredOut) { + break; + } else { + // Send API key as response + requestEvent.respondWith(new Response(STATUS_TEXT.get(Status.OK), { status: Status.OK })); + break; + } + } else { + // Alert API user that they shouldn't be doing this + requestEvent.respondWith(new Response(STATUS_TEXT.get(Status.Forbidden), { status: Status.Forbidden })); + } + } else { + // Alert API user that they messed up + requestEvent.respondWith(new Response(STATUS_TEXT.get(Status.BadRequest), { status: Status.BadRequest })); + } + break; + default: + // Alert API user that they messed up + requestEvent.respondWith(new Response(STATUS_TEXT.get(Status.NotFound), { status: Status.NotFound })); + break; + } + break; + case "DELETE": + switch (path.toLowerCase()) { + case "/api/key/delete": + case "/api/key/delete/": + if (query.has("user") && ((query.get("user") || "").length > 0) && query.has("email") && ((query.get("email") || "").length > 0)) { + if (apiUserid === BigInt(query.get("user") || "0") && apiUserEmail === query.get("email")) { + if (query.has("code") && ((query.get("code") || "").length > 0)) { + if ((query.get("code") || "") === apiUserDelCode) { + // User has recieved their delete code and we need to delete the account now + let erroredOut = false; + + await dbClient.execute("DELETE FROM allowed_channels WHERE userid = ?", [apiUserid]).catch(e => { + utils.log(LT.ERROR, `Failed to insert into database: ${JSON.stringify(e)}`); + requestEvent.respondWith(new Response(STATUS_TEXT.get(Status.InternalServerError), { status: Status.InternalServerError })); + erroredOut = true; + }); + if (erroredOut) { + break; + } + + await dbClient.execute("DELETE FROM all_keys WHERE userid = ?", [apiUserid]).catch(e => { + utils.log(LT.ERROR, `Failed to insert into database: ${JSON.stringify(e)}`); + requestEvent.respondWith(new Response(STATUS_TEXT.get(Status.InternalServerError), { status: Status.InternalServerError })); + erroredOut = true; + }); + if (erroredOut) { + break; + } else { + // Send API key as response + requestEvent.respondWith(new Response(STATUS_TEXT.get(Status.OK), { status: Status.OK })); + break; + } + } else { + // Alert API user that they shouldn't be doing this + requestEvent.respondWith(new Response(STATUS_TEXT.get(Status.Forbidden), { status: Status.Forbidden })); + } + } else { + // User does not have their delete code yet, so we need to generate one and email it to them + const deleteCode = await nanoid(10); + + let erroredOut = false; + + // Execute the DB modification + await dbClient.execute("UPDATE all_keys SET deleteCode = ? WHERE userid = ?", [deleteCode, apiUserid]).catch(e => { + utils.log(LT.ERROR, `Failed to insert into database: ${JSON.stringify(e)}`); + requestEvent.respondWith(new Response(STATUS_TEXT.get(Status.InternalServerError), { status: Status.InternalServerError })); + erroredOut = true; + }); + if (erroredOut) { + break; + } + + // "Send" the email + await sendMessage(config.api.email, `<@${config.api.admin}> A USER HAS REQUESTED A DELETE CODE\n\nEmail Address: ${apiUserEmail}\n\nSubject: \`Artificer API Delete Code\`\n\n\`\`\`Hello Artificer API User,\n\nI am sorry to see you go. If you would like, please respond to this email detailing what I could have done better.\n\nAs requested, here is your delete code: ${deleteCode}\n\nSorry to see you go,\nThe Artificer Developer - Ean Milligan\`\`\``).catch(() => { + requestEvent.respondWith(new Response("Message 30 failed to send.", { status: Status.InternalServerError })); + erroredOut = true; + }); + if (erroredOut) { + break; + } else { + // Send API key as response + requestEvent.respondWith(new Response(STATUS_TEXT.get(Status.FailedDependency), { status: Status.FailedDependency })); + break; + } + } + } else { + // Alert API user that they shouldn't be doing this + requestEvent.respondWith(new Response(STATUS_TEXT.get(Status.Forbidden), { status: Status.Forbidden })); + } + } else { + // Alert API user that they messed up + requestEvent.respondWith(new Response(STATUS_TEXT.get(Status.BadRequest), { status: Status.BadRequest })); + } + break; + default: + // Alert API user that they messed up + requestEvent.respondWith(new Response(STATUS_TEXT.get(Status.NotFound), { status: Status.NotFound })); + break; + } + break; + default: + // Alert API user that they messed up + requestEvent.respondWith(new Response(STATUS_TEXT.get(Status.MethodNotAllowed), { status: Status.MethodNotAllowed })); + break; + } + + if (updateRateLimitTime) { + const apiTimeNow = new Date().getTime(); + rateLimitTime.set(apiUseridStr, apiTimeNow); + } + } else if (!authenticated && !rateLimited) { + // Get path and query as a string + const [path, tempQ] = request.url.split("?"); + + // Turn the query into a map (if it exists) + const query = new Map(); + if (tempQ !== undefined) { + tempQ.split("&").forEach((e: string) => { + utils.log(LT.LOG, `Parsing request query #2 ${request} ${e}`); + const [option, params] = e.split("="); + query.set(option.toLowerCase(), params); + }); + } + + // Handle the request + switch (request.method) { + case "GET": + switch (path.toLowerCase()) { + case "/api/key": + case "/api/key/": + if ((query.has("user") && ((query.get("user") || "").length > 0)) && (query.has("email") && ((query.get("email") || "").length > 0))) { + // Generate new secure key + const newKey = await nanoid(25); + + // Flag to see if there is an error inside the catch + let erroredOut = false; + + // Insert new key/user pair into the db + await dbClient.execute("INSERT INTO all_keys(userid,apiKey,email) values(?,?,?)", [BigInt(query.get("user") || "0"), newKey, (query.get("email") || "").toLowerCase()]).catch(e => { + utils.log(LT.ERROR, `Failed to insert into database: ${JSON.stringify(e)}`); + requestEvent.respondWith(new Response(STATUS_TEXT.get(Status.InternalServerError), { status: Status.InternalServerError })); erroredOut = true; }); + + // Exit this case now if catch errored + if (erroredOut) { + break; + } + + // "Send" the email + await sendMessage(config.api.email, `<@${config.api.admin}> A USER HAS REQUESTED AN API KEY\n\nEmail Address: ${query.get("email")}\n\nSubject: \`Artificer API Key\`\n\n\`\`\`Hello Artificer API User,\n\nWelcome aboard The Artificer's API. You can find full details about the API on the GitHub: https://github.com/Burn-E99/TheArtificer\n\nYour API Key is: ${newKey}\n\nGuard this well, as there is zero tolerance for API abuse.\n\nWelcome aboard,\nThe Artificer Developer - Ean Milligan\`\`\``).catch(() => { + requestEvent.respondWith(new Response("Message 31 failed to send.", { status: Status.InternalServerError })); + erroredOut = true; + }); + if (erroredOut) { break; } else { // Send API key as response - request.respond({ status: Status.FailedDependency, body: STATUS_TEXT.get(Status.FailedDependency) }); + requestEvent.respondWith(new Response(STATUS_TEXT.get(Status.OK), { status: Status.OK })); break; } + } else { + // Alert API user that they messed up + requestEvent.respondWith(new Response(STATUS_TEXT.get(Status.BadRequest), { status: Status.BadRequest })); } - } else { - // Alert API user that they shouldn't be doing this - request.respond({ status: Status.Forbidden, body: STATUS_TEXT.get(Status.Forbidden) }); - } - } else { - // Alert API user that they messed up - request.respond({ status: Status.BadRequest, body: STATUS_TEXT.get(Status.BadRequest) }); + break; + default: + // Alert API user that they messed up + requestEvent.respondWith(new Response(STATUS_TEXT.get(Status.NotFound), { status: Status.NotFound })); + break; } break; default: // Alert API user that they messed up - request.respond({ status: Status.NotFound, body: STATUS_TEXT.get(Status.NotFound) }); + requestEvent.respondWith(new Response(STATUS_TEXT.get(Status.MethodNotAllowed), { status: Status.MethodNotAllowed })); break; } - break; - default: - // Alert API user that they messed up - request.respond({ status: Status.MethodNotAllowed, body: STATUS_TEXT.get(Status.MethodNotAllowed) }); - break; + } else if (authenticated && rateLimited) { + // Alert API user that they are doing this too often + requestEvent.respondWith(new Response(STATUS_TEXT.get(Status.TooManyRequests), { status: Status.TooManyRequests })); + } else { + // Alert API user that they shouldn't be doing this + requestEvent.respondWith(new Response(STATUS_TEXT.get(Status.Forbidden), { status: Status.Forbidden })); + } } - - if (updateRateLimitTime) { - const apiTimeNow = new Date().getTime(); - rateLimitTime.set(apiUseridStr, apiTimeNow); - } - } else if (!authenticated && !rateLimited) { - // Get path and query as a string - const [path, tempQ] = request.url.split("?"); - - // Turn the query into a map (if it exists) - const query = new Map(); - if (tempQ !== undefined) { - tempQ.split("&").forEach(e => { - utils.log(LT.LOG, `Parsing request query #2 ${request} ${e}`); - const [option, params] = e.split("="); - query.set(option.toLowerCase(), params); - }); - } - - // Handle the request - switch (request.method) { - case "GET": - switch (path.toLowerCase()) { - case "/api/key": - case "/api/key/": - if ((query.has("user") && ((query.get("user") || "").length > 0)) && (query.has("email") && ((query.get("email") || "").length > 0))) { - // Generate new secure key - const newKey = await nanoid(25); - - // Flag to see if there is an error inside the catch - let erroredOut = false; - - // Insert new key/user pair into the db - await dbClient.execute("INSERT INTO all_keys(userid,apiKey,email) values(?,?,?)", [BigInt(query.get("user")), newKey, (query.get("email") || "").toLowerCase()]).catch(e => { - utils.log(LT.ERROR, `Failed to insert into database: ${JSON.stringify(e)}`); - request.respond({ status: Status.InternalServerError, body: STATUS_TEXT.get(Status.InternalServerError) }); - erroredOut = true; - }); - - // Exit this case now if catch errored - if (erroredOut) { - break; - } - - // "Send" the email - await sendMessage(config.api.email, `<@${config.api.admin}> A USER HAS REQUESTED AN API KEY\n\nEmail Address: ${query.get("email")}\n\nSubject: \`Artificer API Key\`\n\n\`\`\`Hello Artificer API User,\n\nWelcome aboard The Artificer's API. You can find full details about the API on the GitHub: https://github.com/Burn-E99/TheArtificer\n\nYour API Key is: ${newKey}\n\nGuard this well, as there is zero tolerance for API abuse.\n\nWelcome aboard,\nThe Artificer Developer - Ean Milligan\`\`\``).catch(() => { - request.respond({ status: Status.InternalServerError, body: "Message 31 failed to send." }); - erroredOut = true; - }); - - if (erroredOut) { - break; - } else { - // Send API key as response - request.respond({ status: Status.OK, body: STATUS_TEXT.get(Status.OK) }); - break; - } - } else { - // Alert API user that they messed up - request.respond({ status: Status.BadRequest, body: STATUS_TEXT.get(Status.BadRequest) }); - } - break; - default: - // Alert API user that they messed up - request.respond({ status: Status.NotFound, body: STATUS_TEXT.get(Status.NotFound) }); - break; - } - break; - default: - // Alert API user that they messed up - request.respond({ status: Status.MethodNotAllowed, body: STATUS_TEXT.get(Status.MethodNotAllowed) }); - break; - } - } else if (authenticated && rateLimited) { - // Alert API user that they are doing this too often - request.respond({ status: Status.TooManyRequests, body: STATUS_TEXT.get(Status.TooManyRequests) }); - } else { - // Alert API user that they shouldn't be doing this - request.respond({ status: Status.Forbidden, body: STATUS_TEXT.get(Status.Forbidden) }); - } + })(); } }; diff --git a/src/intervals.ts b/src/intervals.ts index 569fb69..6361347 100644 --- a/src/intervals.ts +++ b/src/intervals.ts @@ -6,16 +6,16 @@ import { // Discordeno deps - CacheData + cache, cacheHandlers } from "../deps.ts"; import { LogTypes as LT } from "./utils.enums.ts"; import utils from "./utils.ts"; import config from "../config.ts"; -// getRandomStatus(bot cache) returns status as string +// getRandomStatus() returns status as string // Gets a new random status for the bot -const getRandomStatus = (cache: CacheData): string => { +const getRandomStatus = async (): Promise => { let status = ""; switch (Math.floor((Math.random() * 4) + 1)) { case 1: @@ -27,9 +27,11 @@ const getRandomStatus = (cache: CacheData): string => { case 3: status = `${config.prefix}info to learn more`; break; - default: - status = `Rolling dice for ${cache.guilds.size} servers`; + default: { + const cachedCount = await cacheHandlers.size("guilds") + status = `Rolling dice for ${cachedCount + cache.dispatchedGuildIds.size} servers`; break; + } } return status; @@ -37,7 +39,7 @@ const getRandomStatus = (cache: CacheData): string => { // updateListStatistics(bot ID, current guild count) returns nothing // Sends the current server count to all bot list sites we are listed on -const updateListStatistics = (botID: string, serverCount: number): void => { +const updateListStatistics = (botID: bigint, serverCount: number): void => { config.botLists.forEach(async e => { utils.log(LT.LOG, `Updating statistics for ${JSON.stringify(e)}`) if (e.enabled) { @@ -45,7 +47,7 @@ const updateListStatistics = (botID: string, serverCount: number): void => { tempHeaders.append(e.headers[0].header, e.headers[0].value); tempHeaders.append("Content-Type", "application/json"); // ?{} is a template used in config, just need to replace it with the real value - const response = await fetch(e.apiUrl.replace("?{bot_id}", botID), { + 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 diff --git a/src/solver.ts b/src/solver.ts index e6052f7..379a24b 100644 --- a/src/solver.ts +++ b/src/solver.ts @@ -64,7 +64,7 @@ const escapeCharacters = (str: string, esc: string): string => { utils.log(LT.LOG, `Escaping character ${esc[i]} | ${str}, ${esc}`); // Create a new regex to look for that char that needs replaced and escape it const temprgx = new RegExp(`[${esc[i]}]`, "g"); - str = str.replace(temprgx, ("\\" + esc[i])); + str = str.replace(temprgx, `\\${esc[i]}`); } return str; }; @@ -158,7 +158,7 @@ const roll = (rollStr: string, maximiseRoll: boolean, nominalRoll: boolean): Rol if (remains.length > 0) { // Determine if the first item is a drop, and if it is, add the d back in if (remains.search(/\D/) !== 0 || remains.indexOf("l") === 0 || remains.indexOf("h") === 0) { - remains = "d" + remains; + remains = `d${remains}`; } // Loop until all remaining args are parsed @@ -260,7 +260,7 @@ const roll = (rollStr: string, maximiseRoll: boolean, nominalRoll: boolean): Rol break; default: // Throw error immediately if unknown op is encountered - throw new Error("UnknownOperation_" + tSep); + throw new Error(`UnknownOperation_${tSep}`); } // Finally slice off everything else parsed this loop remains = remains.slice(afterNumIdx); @@ -538,22 +538,22 @@ const formatRoll = (rollConf: string, maximiseRoll: boolean, nominalRoll: boolea // If the roll was a crit hit or fail, or dropped/rerolled, add the formatting needed if (e.critHit) { // Bold for crit success - preFormat = "**" + preFormat; - postFormat = postFormat + "**"; + preFormat = `**${preFormat}`; + postFormat = `${postFormat}**`; } if (e.critFail) { // Underline for crit fail - preFormat = "__" + preFormat; - postFormat = postFormat + "__"; + preFormat = `__${preFormat}`; + postFormat = `${postFormat}__`; } if (e.dropped || e.rerolled) { // Strikethrough for dropped/rerolled rolls - preFormat = "~~" + preFormat; - postFormat = postFormat + "~~"; + preFormat = `~~${preFormat}`; + postFormat = `${postFormat}~~`; } // Finally add this to the roll's details - tempDetails += preFormat + e.roll + postFormat + " + "; + tempDetails += `${preFormat}${e.roll}${postFormat} + `; }); // After the looping is done, remove the extra " + " from the details and cap it with the closing ] tempDetails = tempDetails.substr(0, (tempDetails.length - 3)); @@ -660,13 +660,13 @@ const fullSolver = (conf: (string | number | SolvedStep)[], wrapDetails: boolean // If operand1 is a SolvedStep, populate our subStepSolve with its details and crit/fail flags if (typeof operand1 === "object") { oper1 = operand1.total; - subStepSolve.details = operand1.details + "\\" + conf[i]; + subStepSolve.details = `${operand1.details}\\${conf[i]}`; subStepSolve.containsCrit = operand1.containsCrit; subStepSolve.containsFail = operand1.containsFail; } else { // else parse it as a number and add it to the subStep details oper1 = parseFloat(operand1.toString()); - subStepSolve.details = oper1.toString() + "\\" + conf[i]; + subStepSolve.details = `${oper1.toString()}\\${conf[i]}`; } // If operand2 is a SolvedStep, populate our subStepSolve with its details without overriding what operand1 filled in @@ -740,7 +740,7 @@ const fullSolver = (conf: (string | number | SolvedStep)[], wrapDetails: boolean // If this was a nested call, add on parens around the details to show what math we've done if (wrapDetails) { - stepSolve.details = "(" + stepSolve.details + ")"; + stepSolve.details = `(${stepSolve.details})`; } // If our total has reached undefined for some reason, error out now @@ -899,25 +899,25 @@ const parseRoll = (fullCmd: string, localPrefix: string, localPostfix: string, m // If the roll containted a crit success or fail, set the formatting around it if (e.containsCrit) { - preFormat = "**" + preFormat; - postFormat = postFormat + "**"; + preFormat = `**${preFormat}`; + postFormat = `${postFormat}**`; } if (e.containsFail) { - preFormat = "__" + preFormat; - postFormat = postFormat + "__"; + preFormat = `__${preFormat}`; + postFormat = `${postFormat}__`; } // Populate line2 (the results) and line3 (the details) with their data if (order === "") { - line2 += preFormat + e.rollTotal + postFormat + escapeCharacters(e.rollPostFormat, "|*_~`"); + line2 += `${preFormat}${e.rollTotal}${postFormat}${escapeCharacters(e.rollPostFormat, "|*_~`")}`; } else { // If order is on, turn rolls into csv without formatting - line2 += preFormat + e.rollTotal + postFormat + ", "; + line2 += `${preFormat}${e.rollTotal}${postFormat}, `; } line2 = line2.replace(/\*\*\*\*/g, "** **").replace(/____/g, "__ __").replace(/~~~~/g, "~~ ~~"); - line3 += "`" + e.initConfig + "` = " + e.rollDetails + " = " + preFormat + e.rollTotal + postFormat + "\n"; + line3 += `\`${e.initConfig}\` = ${e.rollDetails} = ${preFormat}${e.rollTotal}${postFormat}\n`; }); // If order is on, remove trailing ", " @@ -950,11 +950,11 @@ const parseRoll = (fullCmd: string, localPrefix: string, localPostfix: string, m errorMsg = "Formatting Error: CritScore range specified without a maximum, remove - or add maximum to correct"; break; case "UnknownOperation": - errorMsg = "Error: Unknown Operation " + errorDetails; + errorMsg = `Error: Unknown Operation ${errorDetails}`; if (errorDetails === "-") { errorMsg += "\nNote: Negative numbers are not supported"; } else if (errorDetails === " ") { - errorMsg += "\nNote: Every roll must be closed by " + localPostfix; + errorMsg += `\nNote: Every roll must be closed by ${localPostfix}`; } break; case "NoZerosAllowed": @@ -982,7 +982,7 @@ const parseRoll = (fullCmd: string, localPrefix: string, localPostfix: string, m errorMsg += "Crit Score (cs)"; break; default: - errorMsg += "Unhandled - " + errorDetails; + errorMsg += `Unhandled - ${errorDetails}`; break; } errorMsg += " cannot be zero"; @@ -1013,7 +1013,7 @@ const parseRoll = (fullCmd: string, localPrefix: string, localPostfix: string, m break; default: utils.log(LT.ERROR, `Undangled Error: ${errorName}, ${errorDetails}`); - errorMsg = "Unhandled Error: " + solverError.message + "\nCheck input and try again, if issue persists, please use `[[report` to alert the devs of the issue"; + errorMsg = `Unhandled Error: ${solverError.message}\nCheck input and try again, if issue persists, please use \`[[report\` to alert the devs of the issue`; break; } diff --git a/src/utils.ts b/src/utils.ts index 363b860..75bb93f 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -6,7 +6,7 @@ import { // Discordeno deps - Message, MessageContent, + sendMessage, // nanoid deps nanoid @@ -35,9 +35,9 @@ const ask = async (question: string, stdin = Deno.stdin, stdout = Deno.stdout): return answer.trim(); }; -// cmdPrompt(logChannel, botName, sendMessage) returns nothing +// cmdPrompt(logChannel, botName) returns nothing // cmdPrompt creates an interactive CLI for the bot, commands can vary -const cmdPrompt = async (logChannel: string, botName: string, sendMessage: (c: string, m: string) => Promise): Promise => { +const cmdPrompt = async (logChannel: bigint, botName: string): Promise => { let done = false; while (!done) { @@ -69,10 +69,10 @@ const cmdPrompt = async (logChannel: string, botName: string, sendMessage: (c: s // Sends [message] to specified [channel] else if (command === "m") { try { - const channelID = args.shift() || ""; + const channelId = args.shift() || ""; const message = args.join(" "); - sendMessage(channelID, message).catch(reason => { + sendMessage(BigInt(channelId), message).catch(reason => { console.error(reason); }); } @@ -104,18 +104,6 @@ const cmdPrompt = async (logChannel: string, botName: string, sendMessage: (c: s } }; -// sendIndirectMessage(originalMessage, messageContent, sendMessage, sendDirectMessage) returns Message -// sendIndirectMessage determines if the message needs to be sent as a direct message or as a normal message -const sendIndirectMessage = async (originalMessage: Message, messageContent: (string | MessageContent), sendMessage: (c: string, m: (string | MessageContent)) => Promise, sendDirectMessage: (c: string, m: (string | MessageContent)) => Promise): Promise => { - if (originalMessage.guildID === "") { - // guildID was empty, meaning the original message was sent as a DM - return await sendDirectMessage(originalMessage.author.id, messageContent); - } else { - // guildID was not empty, meaning the original message was sent in a server - return await sendMessage(originalMessage.channelID, messageContent); - } -}; - // initLog() returns nothing // Handles ensuring the required directory structure is created const initLog = (name: string): void => { @@ -161,4 +149,4 @@ const log = async (level: LogTypes, message: string, error = new Error()): Promi } }; -export default { cmdPrompt, sendIndirectMessage, initLog, log }; +export default { cmdPrompt, initLog, log }; diff --git a/start.command b/start.command new file mode 100644 index 0000000..ac03885 --- /dev/null +++ b/start.command @@ -0,0 +1 @@ +deno run --allow-write=./logs --allow-net .\mod.ts