From 30455af88c61fbc2f7d4095208f815afaca69312 Mon Sep 17 00:00:00 2001 From: "Ean Milligan (Bastion)" Date: Mon, 11 Jan 2021 03:42:57 -0500 Subject: [PATCH] V1.1.0 Completed Added initial support for Artificer API access. This is currently not secured in any way. Config updated to include api settings .gitignore cleaned up mod.ts now has initial API built in --- .gitignore | 3 - config.example.ts | 7 +- mod.ts | 223 +++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 228 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index 14c5f58..4afdf84 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,2 @@ -node_modules/ -config.json config.ts -brokennode/ emojis/Thumbs.db diff --git a/config.example.ts b/config.example.ts index a7aff89..6087339 100644 --- a/config.example.ts +++ b/config.example.ts @@ -1,9 +1,14 @@ export const config = { "name": "The Artificer", - "version": "1.0.0", + "version": "1.1.0", "token": "the_bot_token", "prefix": "[[", "postfix": "]]", + "api": { + "enable": false, + "port": 8080, + "supportURL": "your_support_url_for_api_abuse" + }, "logChannel": "the_log_channel", "reportChannel": "the_report_channel", "devServer": "the_dev_server", diff --git a/mod.ts b/mod.ts index 9d7ae12..4554407 100644 --- a/mod.ts +++ b/mod.ts @@ -15,6 +15,8 @@ import { Message, Guild, sendMessage, sendDirectMessage, cache } from "https://deno.land/x/discordeno@10.0.0/mod.ts"; +import { serve } from "https://deno.land/std@0.83.0/http/server.ts"; +import { Status, STATUS_TEXT } from "https://deno.land/std@0.83.0/http/http_status.ts"; import utils from "./src/utils.ts"; import solver from "./src/solver.ts"; @@ -221,7 +223,7 @@ startBot({ returnText = "<@" + message.author.id + ">" + returnmsg.line1 + "\n" + returnmsg.line2; if (modifiers.noDetails) { - returnText += "\nDetails suppressed by -nd flag."; + returnText += "\nDetails suppressed by -nd flag."; } else { returnText += "\nDetails:\n" + modifiers.spoiler + returnmsg.line3 + modifiers.spoiler; } @@ -244,6 +246,7 @@ startBot({ } }); + // Finally send the text m.edit(normalText); } else { // When not a GM roll, make sure the message is not too big @@ -256,6 +259,7 @@ startBot({ failed = true; }); } + // If DM fails to send, alert roller of the failure, else handle normally if (failed) { returnText = "<@" + message.author.id + ">" + returnmsg.line1 + "\n" + returnmsg.line2 + "\nDetails have been ommitted from this message for being over 2000 characters. WARNING: <@" + message.author.id + "> could **NOT** be messaged full details for verification purposes."; @@ -279,3 +283,220 @@ startBot({ if (DEBUG) { utils.cmdPrompt(config.logChannel, config.name, sendMessage); } + +// Start up the API for rolling from third party apps (like excel macros) +if (config.api.enable) { + const server = serve({ hostname: "0.0.0.0", port: config.api.port }); + console.log(`HTTP webserver running at: http://localhost:${config.api.port}/`); + + // Catching every request made to the server + for await (const request of server) { + + // Super secure authentication + const authenticated = true; + + if (authenticated) { + // 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 => { + const [option, params] = e.split("="); + query.set(option.toLowerCase(), params); + }); + } + + // Handle the request + switch (request.method) { + case "GET": + switch (path) { + case "/roll": + case "/roll/": + // Make sure query contains all the needed parts + if (query.has("rollstr") && query.has("channel") && query.has("user")) { + + 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) }); + } + + // Super secure authorization + const 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") || ""; + + if (rollCmd.length === 0) { + // Alert API user that they messed up + request.respond({ status: Status.BadRequest, body: STATUS_TEXT.get(Status.BadRequest) }); + } + + // 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")); + + // 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 returnText = ""; + + // Handle sending the error message to whoever called the api + if (returnmsg.error) { + request.respond({ status: Status.InternalServerError, body: returnmsg.errorMsg }); + 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) }); + } + + // 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 => { + 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) { + await sendMessage(query.get("channel") || "", normalText).catch(() => { + request.respond({ status: Status.InternalServerError, body: "Message 00 failed to send." }); + errorOut = true; + }); + } else { + await sendDirectMessage(query.get("user") || "", normalText).catch(() => { + request.respond({ status: Status.InternalServerError, body: "Message 01 failed to send." }); + errorOut = true; + }); + } + + // And message the full details to each of the GMs, alerting roller of every GM that could not be messaged + gms.forEach(async e => { + const msgs = utils.split2k(returnText); + const failedDMs = []; + for (let i = 0; ((failedDMs.indexOf(e) === -1) && (i < msgs.length)); i++) { + await sendDirectMessage(e, msgs[i]).catch( async () => { + failedDMs.push(e); + 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) { + await sendMessage(query.get("channel") || "", failedSend).catch(() => { + request.respond({ status: Status.InternalServerError, body: "Message 10 failed to send." }); + errorOut = true; + }); + } else { + await sendDirectMessage(query.get("user") || "", failedSend).catch(() => { + request.respond({ status: Status.InternalServerError, body: "Message 11 failed to send." }); + errorOut = true; + }); + } + }); + } + }); + + // Handle closing the request out + if (errorOut) { + break; + } else { + request.respond({ status: Status.OK, body: normalText }); + break; + } + } else { + // When not a GM roll, make sure the message is not too big + if (returnText.length > 2000) { + // If its too big, attempt to DM details to the roller + const msgs = utils.split2k(returnText); + let failed = false; + for (let i = 0; (!failed && (i < msgs.length)); i++) { + await sendDirectMessage(query.get("user") || "", msgs[i]).catch(() => { + failed = true; + }); + } + + // If DM fails to send, alert roller of the failure, else handle normally + if (failed) { + returnText = apiPrefix + "<@" + query.get("user") + ">" + returnmsg.line1 + "\n" + returnmsg.line2 + "\nDetails have been ommitted from this message for being over 2000 characters. WARNING: <@" + query.get("user") + "> could **NOT** be messaged full details for verification purposes."; + } else { + returnText = 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 messaged to <@" + query.get("user") + "> for verification purposes."; + } + } + + // Send the return message as a DM or normal message depening on if the channel is set + if ((query.get("channel") || "").length > 0) { + await sendMessage(query.get("channel") || "", returnText).catch(() => { + request.respond({ status: Status.InternalServerError, body: "Message 20 failed to send." }); + errorOut = true; + }); + } else { + await sendDirectMessage(query.get("user") || "", returnText).catch(() => { + request.respond({ status: Status.InternalServerError, body: "Message 21 failed to send." }); + errorOut = true; + }); + } + + // Handle closing the request out + if (errorOut) { + break; + } else { + request.respond({ status: Status.OK, body: returnText }); + break; + } + } + } catch (err) { + // Handle any errors we missed + console.log(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; + default: + // Alert API user that they messed up + request.respond({ status: Status.MethodNotAllowed, body: STATUS_TEXT.get(Status.MethodNotAllowed) }); + break; + } + } else { + // Alert API user that they shouldn't be doing this + request.respond({ status: Status.Forbidden, body: STATUS_TEXT.get(Status.Forbidden) }); + } + } +}