V1.4.2 - Code Rework + minor bugfixes
.gitignore - added new logs folder for future feature config.example.ts, README.md, *index.html - version bumped deps.ts - moved all remote dependencies to this file to make version updating easier db/* - changed deps to new format longStrings.ts, README.md - fixed typo mod.ts - changed deps to new format, implemented error catching for editBotStatus, moved api to separate file api.ts - pulled api code to this file for better organization intervals.ts - new utility file for functions that will be called after bot boots (and that continue to be called on a specified interval mod.d.ts - documentation added solver.ts - newline utils.ts - updated deps to new format
This commit is contained in:
parent
fc218e1644
commit
ddb8f62eca
|
@ -1,2 +1,3 @@
|
||||||
config.ts
|
config.ts
|
||||||
emojis/Thumbs.db
|
emojis/Thumbs.db
|
||||||
|
logs
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
# The Artificer - A Dice Rolling Discord Bot
|
# The Artificer - A Dice Rolling Discord Bot
|
||||||
Version 1.4.1 - 2021/02/13
|
Version 1.4.2 - 2021/02/14
|
||||||
|
|
||||||
The Artificer is a Discord bot that specializes in rolling dice. The bot utilizes the compact [Roll20 formatting](https://roll20.zendesk.com/hc/en-us/articles/360037773133-Dice-Reference) for ease of use and will correctly perform any needed math on the roll (limited to basic algebra).
|
The Artificer is a Discord bot that specializes in rolling dice. The bot utilizes the compact [Roll20 formatting](https://roll20.zendesk.com/hc/en-us/articles/360037773133-Dice-Reference) for ease of use and will correctly perform any needed math on the roll (limited to basic algebra).
|
||||||
|
|
||||||
|
@ -71,7 +71,7 @@ The Artificer comes with a few supplemental commands to the main rolling command
|
||||||
| csq or cs=q | Optional | Yes | changes crit score to q |
|
| csq or cs=q | Optional | Yes | changes crit score to q |
|
||||||
| cs<q | Optional | Yes | changes crit score to be less than or equal to q |
|
| cs<q | Optional | Yes | changes crit score to be less than or equal to q |
|
||||||
| cs>q | Optional | Yes | changes crit score to be greater than or equal to q |
|
| cs>q | Optional | Yes | changes crit score to be greater than or equal to q |
|
||||||
| cfq or cs=q | Optional | Yes | changes crit fail to q |
|
| cfq or cf=q | Optional | Yes | changes crit fail to q |
|
||||||
| cf<q | Optional | Yes | changes crit fail to be less than or equal to q |
|
| cf<q | Optional | Yes | changes crit fail to be less than or equal to q |
|
||||||
| cf>q | Optional | Yes | changes crit fail to be greater than or equal to q |
|
| cf>q | Optional | Yes | changes crit fail to be greater than or equal to q |
|
||||||
| ! | Optional | No | exploding, rolls another dy for every crit roll |
|
| ! | Optional | No | exploding, rolls another dy for every crit roll |
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
export const config = {
|
export const config = {
|
||||||
"name": "The Artificer", // Name of the bot
|
"name": "The Artificer", // Name of the bot
|
||||||
"version": "1.4.1", // Version of the bot
|
"version": "1.4.2", // Version of the bot
|
||||||
"token": "the_bot_token", // Discord API Token for this 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"
|
"localtoken": "local_testing_token", // Discord API Token for a secondary OPTIONAL testing bot, THIS MUST BE DIFFERENT FROM "token"
|
||||||
"prefix": "[[", // Prefix for all commands
|
"prefix": "[[", // Prefix for all commands
|
||||||
|
|
|
@ -1,7 +1,10 @@
|
||||||
// This file will create all tables for the artificer schema
|
// This file will create all tables for the artificer schema
|
||||||
// DATA WILL BE LOST IF DB ALREADY EXISTS, RUN AT OWN RISK
|
// DATA WILL BE LOST IF DB ALREADY EXISTS, RUN AT OWN RISK
|
||||||
|
|
||||||
import { Client } from "https://deno.land/x/mysql/mod.ts";
|
import {
|
||||||
|
// MySQL deps
|
||||||
|
Client
|
||||||
|
} from "../deps.ts";
|
||||||
|
|
||||||
import { LOCALMODE } from "../flags.ts";
|
import { LOCALMODE } from "../flags.ts";
|
||||||
import config from "../config.ts";
|
import config from "../config.ts";
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
// This file will populate the tables with default values
|
// This file will populate the tables with default values
|
||||||
|
|
||||||
import { Client } from "https://deno.land/x/mysql/mod.ts";
|
import {
|
||||||
|
// MySQL deps
|
||||||
|
Client
|
||||||
|
} from "../deps.ts";
|
||||||
|
|
||||||
import { LOCALMODE } from "../flags.ts";
|
import { LOCALMODE } from "../flags.ts";
|
||||||
import config from "../config.ts";
|
import config from "../config.ts";
|
||||||
|
|
|
@ -0,0 +1,19 @@
|
||||||
|
// All external dependancies are to be loaded here to make updating dependancy versions much easier
|
||||||
|
export {
|
||||||
|
startBot, editBotsStatus,
|
||||||
|
Intents, StatusTypes, ActivityType,
|
||||||
|
sendMessage, sendDirectMessage,
|
||||||
|
cache,
|
||||||
|
memberIDHasPermission
|
||||||
|
} from "https://deno.land/x/discordeno@10.3.0/mod.ts";
|
||||||
|
|
||||||
|
export type {
|
||||||
|
CacheData, Message, Guild, MessageContent
|
||||||
|
} from "https://deno.land/x/discordeno@10.3.0/mod.ts";
|
||||||
|
|
||||||
|
export { Client } from "https://deno.land/x/mysql@v2.7.0/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 { nanoid } from "https://deno.land/x/nanoid@v3.0.0/mod.ts";
|
|
@ -34,7 +34,7 @@ export const longStrs = {
|
||||||
"* csq or cs=q [OPT] - changes crit score to q",
|
"* csq or cs=q [OPT] - changes crit score to q",
|
||||||
"* cs<q [OPT] - changes crit score to be less than or equal to q",
|
"* cs<q [OPT] - changes crit score to be less than or equal to q",
|
||||||
"* cs>q [OPT] - changes crit score to be greater than or equal to q ",
|
"* cs>q [OPT] - changes crit score to be greater than or equal to q ",
|
||||||
"* cfq or cs=q [OPT] - changes crit fail to q",
|
"* cfq or cf=q [OPT] - changes crit fail to q",
|
||||||
"* cf<q [OPT] - changes crit fail to be less than or equal to q",
|
"* cf<q [OPT] - changes crit fail to be less than or equal to q",
|
||||||
"* cf>q [OPT] - changes crit fail to be greater than or equal to q",
|
"* cf>q [OPT] - changes crit fail to be greater than or equal to q",
|
||||||
"* ! [OPT] - exploding, rolls another dy for every crit roll",
|
"* ! [OPT] - exploding, rolls another dy for every crit roll",
|
||||||
|
|
745
mod.ts
745
mod.ts
|
@ -5,21 +5,19 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
// Discordeno deps
|
||||||
startBot, editBotsStatus,
|
startBot, editBotsStatus,
|
||||||
Intents, StatusTypes, ActivityType,
|
Intents, StatusTypes, ActivityType,
|
||||||
Message, Guild, sendMessage, sendDirectMessage,
|
Message, Guild, sendMessage, sendDirectMessage,
|
||||||
cache,
|
cache,
|
||||||
MessageContent,
|
memberIDHasPermission,
|
||||||
memberIDHasPermission
|
|
||||||
} from "https://deno.land/x/discordeno@10.3.0/mod.ts";
|
|
||||||
|
|
||||||
import { serve } from "https://deno.land/std@0.83.0/http/server.ts";
|
// MySQL Driver deps
|
||||||
import { Status, STATUS_TEXT } from "https://deno.land/std@0.83.0/http/http_status.ts";
|
Client
|
||||||
|
} from "./deps.ts";
|
||||||
import { Client } from "https://deno.land/x/mysql@v2.7.0/mod.ts";
|
|
||||||
|
|
||||||
import { nanoid } from "https://deno.land/x/nanoid@v3.0.0/mod.ts";
|
|
||||||
|
|
||||||
|
import api from "./src/api.ts";
|
||||||
|
import intervals from "./src/intervals.ts";
|
||||||
import utils from "./src/utils.ts";
|
import utils from "./src/utils.ts";
|
||||||
import solver from "./src/solver.ts";
|
import solver from "./src/solver.ts";
|
||||||
|
|
||||||
|
@ -35,7 +33,7 @@ const dbClient = await new Client().connect({
|
||||||
port: config.db.port,
|
port: config.db.port,
|
||||||
db: config.db.name,
|
db: config.db.name,
|
||||||
username: config.db.username,
|
username: config.db.username,
|
||||||
password: config.db.password,
|
password: config.db.password
|
||||||
});
|
});
|
||||||
|
|
||||||
// Start up the Discord Bot
|
// Start up the Discord Bot
|
||||||
|
@ -45,14 +43,21 @@ startBot({
|
||||||
eventHandlers: {
|
eventHandlers: {
|
||||||
ready: () => {
|
ready: () => {
|
||||||
console.log(`${config.name} Logged in!`);
|
console.log(`${config.name} Logged in!`);
|
||||||
let statusIdx = 0;
|
editBotsStatus(StatusTypes.Online, "Booting up . . .", ActivityType.Game);
|
||||||
const statusRotation =[`${config.prefix}help for commands`, `Running V${config.version}`, `${config.prefix}info to learn more`, `Rolling dice for ${cache.guilds.size} servers`];
|
|
||||||
|
// Interval to rotate the status text every 30 seconds to show off more commands
|
||||||
setInterval(() => {
|
setInterval(() => {
|
||||||
editBotsStatus(StatusTypes.Online, statusRotation[statusIdx], ActivityType.Game);
|
try {
|
||||||
statusIdx >= statusRotation.length ? statusIdx = 0 : statusIdx++;
|
// Wrapped in try-catch due to hard crash possible
|
||||||
|
editBotsStatus(StatusTypes.Online, intervals.getRandomStatus(cache), ActivityType.Game);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to update status 00", err);
|
||||||
|
}
|
||||||
}, 30000);
|
}, 30000);
|
||||||
|
|
||||||
// setTimeout added to make sure the startup message does not error out
|
// setTimeout added to make sure the startup message does not error out
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
editBotsStatus(StatusTypes.Online, `Boot Complete`, ActivityType.Game);
|
||||||
sendMessage(config.logChannel, `${config.name} has started, running version ${config.version}.`).catch(() => {
|
sendMessage(config.logChannel, `${config.name} has started, running version ${config.version}.`).catch(() => {
|
||||||
console.error("Failed to send message 00");
|
console.error("Failed to send message 00");
|
||||||
});
|
});
|
||||||
|
@ -583,715 +588,5 @@ if (DEBUG) {
|
||||||
|
|
||||||
// Start up the API for rolling from third party apps (like excel macros)
|
// Start up the API for rolling from third party apps (like excel macros)
|
||||||
if (config.api.enable) {
|
if (config.api.enable) {
|
||||||
const server = serve({ hostname: "0.0.0.0", port: config.api.port });
|
api.start(dbClient, cache, sendMessage, sendDirectMessage);
|
||||||
console.log(`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<string, number>();
|
|
||||||
// rateLimitCnt holds the number of times the user has called the api in the current rate limit timer
|
|
||||||
const rateLimitCnt = new Map<string, number>();
|
|
||||||
|
|
||||||
// Catching every request made to the server
|
|
||||||
for await (const request of server) {
|
|
||||||
// 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<string, string>();
|
|
||||||
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.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 => {
|
|
||||||
console.log("Failed to insert into database 20");
|
|
||||||
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 => {
|
|
||||||
console.log("Failed to query database 22");
|
|
||||||
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 => {
|
|
||||||
console.log("Failed to insert into database 10", 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 => {
|
|
||||||
console.log("Failed to insert into database 10", 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 => {
|
|
||||||
console.log("Failed to insert into database 11", 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 => {
|
|
||||||
console.log("Failed to insert into database 12", 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 => {
|
|
||||||
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;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// And message the full details to each of the GMs, alerting roller of every GM that could not be messaged
|
|
||||||
gms.forEach(async e => {
|
|
||||||
// If its too big, collapse it into a .txt file and send that instead.
|
|
||||||
const b = await new Blob([returnText as BlobPart], { "type": "text" });
|
|
||||||
|
|
||||||
// Update return text
|
|
||||||
returnText = apiPrefix + "<@" + query.get("user") + ">" + 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, { "content": returnText, "file": { "blob": b, "name": "rollDetails.txt" } }).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 10 failed to send." });
|
|
||||||
errorOut = true;
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
m = await sendDirectMessage(query.get("user") || "", failedSend).catch(() => {
|
|
||||||
request.respond({ status: Status.InternalServerError, body: "Message 11 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 => {
|
|
||||||
console.log("Failed to insert into database 13", 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" });
|
|
||||||
|
|
||||||
// Update return text
|
|
||||||
returnText = "<@" + 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.";
|
|
||||||
|
|
||||||
// Set info into the newMessage
|
|
||||||
newMessage.content = returnText;
|
|
||||||
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 => {
|
|
||||||
console.log("Failed to insert into database 14", 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
|
|
||||||
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;
|
|
||||||
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 => {
|
|
||||||
console.log("Failed to insert into database 21");
|
|
||||||
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 => {
|
|
||||||
console.log("Failed to update database 28");
|
|
||||||
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 => {
|
|
||||||
console.log("Failed to update database 24");
|
|
||||||
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 => {
|
|
||||||
console.log("Failed to update database 26");
|
|
||||||
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 => {
|
|
||||||
console.log("Failed to delete from database 2A");
|
|
||||||
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 => {
|
|
||||||
console.log("Failed to delete from database 2B");
|
|
||||||
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 => {
|
|
||||||
console.log("Failed to update database 29");
|
|
||||||
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<string, string>();
|
|
||||||
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.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 => {
|
|
||||||
console.log("Failed to insert into database 20");
|
|
||||||
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) });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,742 @@
|
||||||
|
/* 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 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<Message>, sendDirectMessage: (c: string, m: (string | MessageContent)) => Promise<Message>): Promise<void> => {
|
||||||
|
const server = serve({ hostname: "0.0.0.0", port: config.api.port });
|
||||||
|
console.log(`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<string, number>();
|
||||||
|
// rateLimitCnt holds the number of times the user has called the api in the current rate limit timer
|
||||||
|
const rateLimitCnt = new Map<string, number>();
|
||||||
|
|
||||||
|
// Catching every request made to the server
|
||||||
|
for await (const request of server) {
|
||||||
|
// 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<string, string>();
|
||||||
|
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.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(() => {
|
||||||
|
console.log("Failed to insert into database 20");
|
||||||
|
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(() => {
|
||||||
|
console.log("Failed to query database 22");
|
||||||
|
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(() => {
|
||||||
|
console.log("Failed to insert into database 10");
|
||||||
|
});
|
||||||
|
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(() => {
|
||||||
|
console.log("Failed to insert into database 10");
|
||||||
|
});
|
||||||
|
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(() => {
|
||||||
|
console.log("Failed to insert into database 11");
|
||||||
|
});
|
||||||
|
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(() => {
|
||||||
|
console.log("Failed to insert into database 12");
|
||||||
|
});
|
||||||
|
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 => {
|
||||||
|
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;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// And message the full details to each of the GMs, alerting roller of every GM that could not be messaged
|
||||||
|
gms.forEach(async e => {
|
||||||
|
// If its too big, collapse it into a .txt file and send that instead.
|
||||||
|
const b = await new Blob([returnText as BlobPart], { "type": "text" });
|
||||||
|
|
||||||
|
// Update return text
|
||||||
|
returnText = apiPrefix + "<@" + query.get("user") + ">" + 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, { "content": returnText, "file": { "blob": b, "name": "rollDetails.txt" } }).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 10 failed to send." });
|
||||||
|
errorOut = true;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
m = await sendDirectMessage(query.get("user") || "", failedSend).catch(() => {
|
||||||
|
request.respond({ status: Status.InternalServerError, body: "Message 11 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(() => {
|
||||||
|
console.log("Failed to insert into database 13");
|
||||||
|
});
|
||||||
|
|
||||||
|
// 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" });
|
||||||
|
|
||||||
|
// Update return text
|
||||||
|
returnText = "<@" + 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.";
|
||||||
|
|
||||||
|
// Set info into the newMessage
|
||||||
|
newMessage.content = returnText;
|
||||||
|
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(() => {
|
||||||
|
console.log("Failed to insert into database 14");
|
||||||
|
});
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
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(() => {
|
||||||
|
console.log("Failed to insert into database 21");
|
||||||
|
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(() => {
|
||||||
|
console.log("Failed to update database 28");
|
||||||
|
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(() => {
|
||||||
|
console.log("Failed to update database 24");
|
||||||
|
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(() => {
|
||||||
|
console.log("Failed to update database 26");
|
||||||
|
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(() => {
|
||||||
|
console.log("Failed to delete from database 2A");
|
||||||
|
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(() => {
|
||||||
|
console.log("Failed to delete from database 2B");
|
||||||
|
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(() => {
|
||||||
|
console.log("Failed to update database 29");
|
||||||
|
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<string, string>();
|
||||||
|
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.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(() => {
|
||||||
|
console.log("Failed to insert into database 20");
|
||||||
|
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 };
|
|
@ -0,0 +1,36 @@
|
||||||
|
/* The Artificer was built in memory of Babka
|
||||||
|
* With love, Ean
|
||||||
|
*
|
||||||
|
* December 21, 2020
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
// Discordeno deps
|
||||||
|
CacheData
|
||||||
|
} from "../deps.ts";
|
||||||
|
|
||||||
|
import config from "../config.ts";
|
||||||
|
|
||||||
|
// getRandomStatus(bot cache) returns status as string
|
||||||
|
// Gets a new random status for the bot
|
||||||
|
const getRandomStatus = (cache: CacheData): string => {
|
||||||
|
let status = "";
|
||||||
|
switch (Math.floor((Math.random() * 4) + 1)) {
|
||||||
|
case 1:
|
||||||
|
status = `${config.prefix}help for commands`;
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
status = `Running V${config.version}`;
|
||||||
|
break;
|
||||||
|
case 3:
|
||||||
|
status = `${config.prefix}info to learn more`;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
status = `Rolling dice for ${cache.guilds.size} servers`;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return status;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default { getRandomStatus };
|
|
@ -1,5 +1,6 @@
|
||||||
// mod.d.ts custom types
|
// mod.d.ts custom types
|
||||||
|
|
||||||
|
// EmojiConf is used as a structure for the emojis stored in config.ts
|
||||||
export type EmojiConf = {
|
export type EmojiConf = {
|
||||||
"name": string,
|
"name": string,
|
||||||
"aliases": Array<string>,
|
"aliases": Array<string>,
|
||||||
|
|
|
@ -1001,4 +1001,4 @@ const parseRoll = (fullCmd: string, localPrefix: string, localPostfix: string, m
|
||||||
return returnmsg;
|
return returnmsg;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default { parseRoll };
|
export default { parseRoll };
|
||||||
|
|
|
@ -4,7 +4,10 @@
|
||||||
* December 21, 2020
|
* December 21, 2020
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Message, MessageContent } from "https://deno.land/x/discordeno@10.3.0/mod.ts";
|
import {
|
||||||
|
// Discordeno deps
|
||||||
|
Message, MessageContent
|
||||||
|
} from "../deps.ts";
|
||||||
|
|
||||||
// split2k(longMessage) returns shortMessage[]
|
// split2k(longMessage) returns shortMessage[]
|
||||||
// split2k takes a long string in and cuts it into shorter strings to be sent in Discord
|
// split2k takes a long string in and cuts it into shorter strings to be sent in Discord
|
||||||
|
@ -140,4 +143,6 @@ const sendIndirectMessage = async (originalMessage: Message, messageContent: (st
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Write logging function with trace and whatnot for errors and necessary messages to log, log bot state in server to determine if user is at fault or if I am at fault (maybe message user if its their fault?)
|
||||||
|
|
||||||
export default { split2k, cmdPrompt, sendIndirectMessage };
|
export default { split2k, cmdPrompt, sendIndirectMessage };
|
||||||
|
|
|
@ -122,7 +122,7 @@
|
||||||
Built by <a href="https://github.com/Burn-E99/" target="_blank">Ean Milligan</a>
|
Built by <a href="https://github.com/Burn-E99/" target="_blank">Ean Milligan</a>
|
||||||
</div>
|
</div>
|
||||||
<div id="footer-right">
|
<div id="footer-right">
|
||||||
Version 1.4.1
|
Version 1.4.2
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -94,7 +94,7 @@
|
||||||
Built by <a href="https://github.com/Burn-E99/" target="_blank">Ean Milligan</a>
|
Built by <a href="https://github.com/Burn-E99/" target="_blank">Ean Milligan</a>
|
||||||
</div>
|
</div>
|
||||||
<div id="footer-right">
|
<div id="footer-right">
|
||||||
Version 1.4.1
|
Version 1.4.2
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
Loading…
Reference in New Issue