/* The Artificer was built in memory of Babka * With love, Ean * * December 21, 2020 */ import { // Discordeno deps CacheData, Message, MessageContent, // MySQL Driver deps Client, // httpd deps serve, Status, STATUS_TEXT, // nanoid deps nanoid } from "../deps.ts"; import solver from "./solver.ts"; import { LogTypes as LT } from "./utils.enums.ts"; import utils from "./utils.ts"; 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 }); 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 const rateLimitTime = new Map(); // rateLimitCnt holds the number of times the user has called the api in the current rate limit timer 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 = ""; // 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; // 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; } } 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("?"); // 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; } 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; }); // 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 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)}`); request.respond({ status: Status.InternalServerError, body: STATUS_TEXT.get(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; }); 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 { // 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) { 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." }); erroredOut = true; }); if (erroredOut) { break; } else { // Send API key as response request.respond({ status: Status.FailedDependency, body: STATUS_TEXT.get(Status.FailedDependency) }); 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; default: // Alert API user that they messed up request.respond({ status: Status.MethodNotAllowed, body: STATUS_TEXT.get(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 => { 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) }); } } }; export default { start };