V1.3.0 Released

config.example.ts updated to include full documentation of all options.
initDB.ts updated with minor fixes.
mod.ts updated command handling, upgraded api to have secured access, key and channel management, minor bug fixes, and rate limiting.
solver.ts updated with minor fix to prevent discord formatting from completely and utterly breaking.
README updated.
This commit is contained in:
Ean Milligan (Bastion) 2021-01-18 11:50:22 -05:00
parent 31349c5f51
commit 63efd0a918
5 changed files with 399 additions and 66 deletions

View File

@ -1,5 +1,5 @@
# The Artificer - A Dice Rolling Discord Bot # The Artificer - A Dice Rolling Discord Bot
Version 1.2.0 - 2020/01/14 Version 1.3.0 - 2020/01/18
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).
@ -39,6 +39,7 @@ The Artificer comes with a few supplemental commands to the main rolling command
* Any math (limited to exponentials, multiplication, division, modulus, addition, and subtraction) will be correctly handled in PEMDAS order, so use parenthesis as needed. * Any math (limited to exponentials, multiplication, division, modulus, addition, and subtraction) will be correctly handled in PEMDAS order, so use parenthesis as needed.
* PI and e are available for use. * PI and e are available for use.
* Paramaters for rolling: * Paramaters for rolling:
| Paramater | Required? | Repeatable? | Description | | Paramater | Required? | Repeatable? | Description |
|---------------|-------------|---------------|--------------------------------------------------------------------------------------------------| |---------------|-------------|---------------|--------------------------------------------------------------------------------------------------|
| x | Optional | No | number of dice to roll, if omitted, 1 is used | | x | Optional | No | number of dice to roll, if omitted, 1 is used |
@ -65,18 +66,63 @@ The Artificer comes with a few supplemental commands to the main rolling command
* `[[((d20+20) - 10) / 5]]` will roll a d20, add 20 to that roll, subtract off 10, and finally divide by 5. * `[[((d20+20) - 10) / 5]]` will roll a d20, add 20 to that roll, subtract off 10, and finally divide by 5.
## The Artificer API ## The Artificer API
API is currently in development, details on usage and how to gain privileged access will be added here when the API is feature complete and secured. The Artificer features an API that allows authenticated users to roll dice into Discord from third party applications (such as Excel macros). The API has a couple endpoints exposed to all authenticated users allowing management of channels that your API key can send rolls to. APIs requiring administrative access are not listed below.
Every API request **requires** the header `X-Api-Key` with the value set to the API key granted to you.
* If an API fails, these are the possible responses:
* `400` - Bad Request - Query parameters missing or malformed.
* `403` - Forbidden - API Key is not authenticated or user does not match the owner of the API Key.
* `404` - Not Found - Requested endpoint does not exist.
* `429` - Too Many Requests - API rate limit exceeded, please slow down.
* `500` - Internal Server Error - Something broke, if this continues to happen, please submit a GitHub issue.
Available Endpoints:
* `/api/roll`
* Required query parameters:
* `rollstr` - A roll string formatted identically to the roll command detailed in the "Available Commands" section.
* `channel` - The Discord Channel ID that the bot is to send the results into.
* `user` - Your Discord User ID.
* Returns:
* `200` - OK - Results of the roll should be found in Discord, but also are returned as a string via the API.
* `/api/channel`
* Required query parameters:
* `user` - Your Discord ID.
* Returns:
* `200` - OK - JSON Array as a string containing allowed channels with their active and banned statuses.
* `/api/channel/add`
* Required query parameters:
* `channel` - The Discord Channel ID you wish to whitelist for your user ID/API Key combo.
* `user` - Your Discord ID.
* Returns:
* `200` - OK - Nothing to be returned.
* `/api/channel/activate`
* Required query parameters:
* `channel` - The Discord Channel ID you wish to reactivate.
* `user` - Your Discord ID.
* Returns:
* `200` - OK - Nothing to be returned.
* `/api/channel/deactivate`
* Required query parameters:
* `channel` - The Discord Channel ID you wish to deactivate.
* `user` - Your Discord ID.
* Returns:
* `200` - OK - Nothing to be returned.
## Problems? Feature requests? ## Problems? Feature requests?
If you run into any errors or problems with the bot, or think you have a good idea to add to the bot, please submit a new GitHub issue detailing it. If you don't have a GitHub account, a report command (detailed above) is provided for use in Discord. If you run into any errors or problems with the bot, or think you have a good idea to add to the bot, please submit a new GitHub issue detailing it. If you don't have a GitHub account, a report command (detailed above) is provided for use in Discord.
--- ---
## Self Hosting The Artificer ## Self Hosting The Artificer
The Artificer was built on Deno `v1.6.3` using Discodeno `v10.0.0`. If you choose to run this yourself, you will need to rename `config.example.ts` to `config.ts` and edit some values. You will need to create a new [Discord Application](https://discord.com/developers/applications) and copy the newly generated token into the `"token"` key. If you want to utilize some of the bots dev features, you will need to fill in the keys `"logChannel"` and `"reportChannel"` with text channel IDs and `"devServer"` with a guild ID. The Artificer was built on Deno `v1.6.3` using Discodeno `v10.0.0`. If you choose to run this yourself, you will need to rename `config.example.ts` to `config.ts` and edit some values. You will need to create a new [Discord Application](https://discord.com/developers/applications) and copy the newly generated token into the `"token"` key. If you want to utilize some of the bots dev features, you will need to fill in the keys `"logChannel"` and `"reportChannel"` with text channel IDs and `"devServer"` with a guild ID.
Starting the bot is simply done with `deno run --allow-net .\mod.ts`. You will also need to install and setup a MySQL database with a user for the bot to use to add/modify the database. This user must have the "DB Manager" admin rights and "REFERENCES" Global Privalages. Once the DB is installed and a user is setup, run the provided `initDB.ts` to create the schema and tables.
If you choose to run version 1.1.0 or newer, ensure you disable the API in `config.ts` or verify you have properly secured your instance of The Artificer. Once everything is set up, starting the bot can simply be done with `deno run --allow-net .\mod.ts`.
If you choose to run version `1.1.0` or newer, ensure you disable the API in `config.ts` or verify you have properly secured your instance of The Artificer. If you enable the API, you will need to manually add an entry into the `all_keys`. This entry's `userid` will need to match the `api.admin` in `config.ts` and the `apiKey` will need to be a 25 character `nanoid`.
--- ---

View File

@ -1,26 +1,29 @@
export const config = { export const config = {
"name": "The Artificer", "name": "The Artificer", // Name of the bot
"version": "1.2.0", "version": "1.2.0", // Version of the bot
"token": "the_bot_token", "token": "the_bot_token", // Discord API Token for this bot
"prefix": "[[", "prefix": "[[", // Prefix for all commands
"postfix": "]]", "postfix": "]]", // Postfix for rolling command
"api": { "api": { // Setting for the built-in API
"enable": false, "enable": false, // Leave this off if you have no intention of using this/supporting it
"port": 8080, "port": 8080, // Port for the API to listen on
"supportURL": "your_support_url_for_api_abuse" "supportURL": "your_support_url_for_api_abuse", // Fill this in with the way you wish to be contacted when somebody needs to report API key abuse
"rateLimitTime": 10000, // Time range for how often the API rate limits will be lifted (time in ms)
"rateLimitCnt": 10, // Amount of requests that can be made (successful or not) during above time range before getting rate limited
"admin": 0n // Discord user ID of the bot admin, this user will be the user that can ban/unban user/channel combos and API keys
}, },
"db": { "db": { // Settings for the MySQL database, this is required for use with the API, if you do not want to set this up, you will need to rip all code relating to the DB out of the bot
"host": "", "host": "", // IP address for the db, usually localhost
"port": 3306, "port": 3306, // Port for the db
"username": "", "username": "", // Username for the account that will access your DB, this account will need "DB Manager" admin rights and "REFERENCES" Global Privalages
"password": "", "password": "", // Password for the account, user account may need to be authenticated with the "Standard" Authentication Type if this does not work out of the box
"name": "" "name": "" // Name of the database Schema to use for the bot
}, },
"logRolls": true, "logRolls": true, // Enables logging of roll commands, this should be left disabled for privacy, but exists to allow verification of rolls before deployment, all API rolls will always be logged no matter what this is set to
"logChannel": "the_log_channel", "logChannel": "the_log_channel", // Discord channel ID where the bot should put startup messages and other error messages needed
"reportChannel": "the_report_channel", "reportChannel": "the_report_channel", // Discord channel ID where reports will be sent when using the built-in report command
"devServer": "the_dev_server", "devServer": "the_dev_server", // Discord guild ID where testing of indev features/commands will be handled, used in conjuction with the DEVMODE bool in mod.ts
"help": [ "help": [ // Array of strings that makes up the help command, placed here to keep source code cleaner
"```fix", "```fix",
"The Artificer Help", "The Artificer Help",
"```", "```",
@ -47,15 +50,20 @@ export const config = {
"* 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",
"*",
"* This command also can fully solve math equations (containing exponentials, multiplication, division, modulus, addition, and subtraction)",
"* Parenthesis are also fully supported, so use as much as needed",
"```" "```"
], ],
"emojis": { "emojis": [ // Array of objects containing all emojis that the bot can send on your behalf, empty this array if you don't want any of them
"popcat": { { // Emoji object, duplicate for each emoji
"name": "popcat", "name": "popcat", // Name of emoji in discord
"id": "796340018377523221", "aliases": ["popcat", "pop", "p"], // Commands that will activate this emoji
"animated": true "id": "796340018377523221", // Discord emoji ID for this emoji
"animated": true, // Tells the bot this emoji is animated so it sends correctly
"deleteSender": true // Tells the bot to attempt to delete the sender's message after sending the emoji
} }
} ]
}; };
export default config; export default config;

View File

@ -26,8 +26,9 @@ await dbClient.execute(`
CREATE TABLE roll_log ( CREATE TABLE roll_log (
id int unsigned NOT NULL AUTO_INCREMENT, id int unsigned NOT NULL AUTO_INCREMENT,
input text NOT NULL, input text NOT NULL,
resultid bigint, resultid bigint NULL,
result longtext NOT NULL, result longtext NOT NULL,
createdAt timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
api tinyint(1) NOT NULL, api tinyint(1) NOT NULL,
error tinyint(1) NOT NULL, error tinyint(1) NOT NULL,
PRIMARY KEY (id), PRIMARY KEY (id),
@ -42,12 +43,14 @@ await dbClient.execute(`
CREATE TABLE all_keys ( CREATE TABLE all_keys (
userid bigint unsigned NOT NULL, userid bigint unsigned NOT NULL,
apiKey char(25) NOT NULL, apiKey char(25) NOT NULL,
email char(255) NULL,
createdAt timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, createdAt timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
active tinyint(1) NOT NULL DEFAULT 1, active tinyint(1) NOT NULL DEFAULT 1,
banned tinyint(1) NOT NULL DEFAULT 0, banned tinyint(1) NOT NULL DEFAULT 0,
PRIMARY KEY (userid), PRIMARY KEY (userid),
UNIQUE KEY api_key_userid_UNIQUE (userid), UNIQUE KEY all_keys_userid_UNIQUE (userid),
UNIQUE KEY api_key_apiKey_UNIQUE (apiKey) UNIQUE KEY all_keys_apiKey_UNIQUE (apiKey),
UNIQUE KEY all_keys_email_UNIQUE (email)
) ENGINE=InnoDB DEFAULT CHARSET=utf8; ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
`); `);
console.log("Table created"); console.log("Table created");

338
mod.ts
View File

@ -21,7 +21,7 @@ import { Status, STATUS_TEXT } from "https://deno.land/std@0.83.0/http/http_stat
import { Client } from "https://deno.land/x/mysql@v2.7.0/mod.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 { nanoid } from "https://deno.land/x/nanoid@v3.0.0/mod.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";
@ -111,17 +111,6 @@ startBot({
}); });
} }
// [[popcat or [[pop or [[p
// popcat animated emoji
else if (command === "popcat" || command === "pop" || command === "p") {
utils.sendIndirectMessage(message, `<${config.emojis.popcat.animated ? "a" : ""}:${config.emojis.popcat.name}:${config.emojis.popcat.id}>`, sendMessage, sendDirectMessage).catch(err => {
console.error("Failed to send message 40", message, err);
});
message.delete().catch(err => {
console.error("Failed to delete message 41", message, err);
});
}
// [[report or [[r (command that failed) // [[report or [[r (command that failed)
// Manually report a failed roll // Manually report a failed roll
else if (command === "report" || command === "r") { else if (command === "report" || command === "r") {
@ -141,9 +130,9 @@ startBot({
}); });
} }
// [[ // [[roll]]
// Dice rolling commence! // Dice rolling commence!
else { else if ((command + args.join("")).indexOf(config.postfix) > -1) {
// If DEVMODE is on, only allow this command to be used in the devServer // If DEVMODE is on, only allow this command to be used in the devServer
if (DEVMODE && message.guildID !== config.devServer) { if (DEVMODE && message.guildID !== config.devServer) {
utils.sendIndirectMessage(message, "Command is in development, please try again later.", sendMessage, sendDirectMessage).catch(err => { utils.sendIndirectMessage(message, "Command is in development, please try again later.", sendMessage, sendDirectMessage).catch(err => {
@ -326,6 +315,28 @@ startBot({
console.error("Something failed 71"); console.error("Something failed 71");
} }
} }
// [[emoji or [[emojialias
// Check if the unhandled command is an emoji request
else {
// Start looping thru the possible emojis
config.emojis.some(e => {
// If a match gets found
if (e.aliases.indexOf(command || "") > -1) {
// Send the needed emoji
utils.sendIndirectMessage(message, `<${e.animated ? "a" : ""}:${e.name}:${e.id}>`, sendMessage, sendDirectMessage).catch(err => {
console.error("Failed to send message 40", message, err);
});
// And attempt to delete if needed
if (e.deleteSender) {
message.delete().catch(err => {
console.error("Failed to delete message 41", message, err);
});
}
return true;
}
});
}
} }
} }
}); });
@ -338,27 +349,56 @@ 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 }); const server = serve({ hostname: "0.0.0.0", port: config.api.port });
console.log(`HTTP webserver running at: http://localhost:${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 // Catching every request made to the server
for await (const request of server) { for await (const request of server) {
// Check if user is authenticated to be using this API // Check if user is authenticated to be using this API
let authenticated = false; let authenticated = false;
let apiUserid = 0; let rateLimited = false;
let updateRateLimitTime = false;
let apiUserid = 0n;
let apiUseridStr = "";
// Check the requests API key // Check the requests API key
if (request.headers.has("X-Api-Key")) { if (request.headers.has("X-Api-Key")) {
// Get the userid and flags for the specific key // Get the userid and flags for the specific key
const dbApiQuery = await dbClient.query("SELECT userid, active, banned FROM all_keys WHERE apiKey = ?", [request.headers.get("X-Api-Key")]); const dbApiQuery = await dbClient.query("SELECT userid 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 only one user returned, is not banned, and is currently active, mark as authenticated
if (dbApiQuery.length === 1 && dbApiQuery[0].active && !dbApiQuery[0].banned) { if (dbApiQuery.length === 1) {
apiUserid = dbApiQuery[0].userid; apiUserid = dbApiQuery[0].userid;
authenticated = true; 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) { if (authenticated && !rateLimited) {
// Get path and query as a string // Get path and query as a string
const [path, tempQ] = request.url.split("?"); const [path, tempQ] = request.url.split("?");
@ -374,12 +414,239 @@ if (config.api.enable) {
// Handle the request // Handle the request
switch (request.method) { switch (request.method) {
case "GET": case "GET":
switch (path) { switch (path.toLowerCase()) {
case "/roll": case "/api/key":
case "/roll/": case "/api/key/":
// Make sure query contains all the needed parts if ((query.has("user") && ((query.get("user") || "").length > 0)) && (query.has("a") && ((query.get("a") || "").length > 0))) {
if (query.has("rollstr") && query.has("channel") && query.has("user")) { 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(?,?)", [BigInt(query.get("user")), 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/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, BigInt(query.get("user"))]).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":
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 = ?", [BigInt(query.get("user"))]).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/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(?,?)", [BigInt(query.get("user")), 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;
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, BigInt(query.get("user")), 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, BigInt(query.get("user")), 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;
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")) { if (query.has("n") && query.has("m")) {
// Alert API user that they shouldn't be doing this // Alert API user that they shouldn't be doing this
request.respond({ status: Status.BadRequest, body: STATUS_TEXT.get(Status.BadRequest) }); request.respond({ status: Status.BadRequest, body: STATUS_TEXT.get(Status.BadRequest) });
@ -390,8 +657,8 @@ if (config.api.enable) {
let authorized = false; 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 // 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 = ?", [parseInt(query.get("user") || ""), parseInt(query.get("channel") || "")]) const dbChannelQuery = await dbClient.query("SELECT active, banned FROM allowed_channels WHERE userid = ? AND channelid = ?", [BigInt(query.get("user")), BigInt(query.get("channel"))])
if (dbChannelQuery.length === 1 && (apiUserid === parseInt(query.get("user") || "")) && dbChannelQuery[0].active && !dbChannelQuery[0].banned) { if (dbChannelQuery.length === 1 && (apiUserid === BigInt(query.get("user"))) && dbChannelQuery[0].active && !dbChannelQuery[0].banned) {
authorized = true; authorized = true;
} }
@ -402,13 +669,14 @@ if (config.api.enable) {
let errorOut = false; let errorOut = false;
// Make sure rollCmd is not undefined // Make sure rollCmd is not undefined
let rollCmd = query.get("rollstr") || ""; let rollCmd = query.get("rollstr") || "";
const originalCommand = query.get("rollstr");
if (rollCmd.length === 0) { if (rollCmd.length === 0) {
// Alert API user that they messed up // Alert API user that they messed up
request.respond({ status: Status.BadRequest, body: STATUS_TEXT.get(Status.BadRequest) }); request.respond({ status: Status.BadRequest, body: STATUS_TEXT.get(Status.BadRequest) });
// Always log API rolls for abuse detection // Always log API rolls for abuse detection
dbClient.execute("INSERT INTO roll_log(input,result,resultid,api,error) values(?,?,?,1,1)", [rollCmd, "EmptyInput", null]).catch(e => { 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); console.log("Failed to insert into database 10", e);
}); });
break; break;
@ -429,7 +697,7 @@ if (config.api.enable) {
request.respond({ status: Status.InternalServerError, body: returnmsg.errorMsg }); request.respond({ status: Status.InternalServerError, body: returnmsg.errorMsg });
// Always log API rolls for abuse detection // Always log API rolls for abuse detection
dbClient.execute("INSERT INTO roll_log(input,result,resultid,api,error) values(?,?,?,1,1)", [rollCmd, returnmsg.errorCode, null]).catch(e => { 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); console.log("Failed to insert into database 11", e);
}); });
break; break;
@ -459,7 +727,7 @@ if (config.api.enable) {
request.respond({ status: Status.BadRequest, body: STATUS_TEXT.get(Status.BadRequest) }); request.respond({ status: Status.BadRequest, body: STATUS_TEXT.get(Status.BadRequest) });
// Always log API rolls for abuse detection // Always log API rolls for abuse detection
dbClient.execute("INSERT INTO roll_log(input,result,resultid,api,error) values(?,?,?,1,1)", [rollCmd, "NoGMsSent", null]).catch(e => { 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); console.log("Failed to insert into database 12", e);
}); });
break; break;
@ -509,7 +777,7 @@ if (config.api.enable) {
}); });
// Always log API rolls for abuse detection // Always log API rolls for abuse detection
dbClient.execute("INSERT INTO roll_log(input,result,resultid,api,error) values(?,?,?,1,0)", [rollCmd, returnText, ((typeof m === "object") ? m.id : null)]).catch(e => { 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); console.log("Failed to insert into database 13", e);
}); });
@ -554,7 +822,7 @@ if (config.api.enable) {
} }
// If enabled, log rolls so we can verify the bots math // 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)", [rollCmd, returnText, ((typeof m === "object") ? m.id : null)]).catch(e => { 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); console.log("Failed to insert into database 14", e);
}); });
@ -591,6 +859,14 @@ if (config.api.enable) {
request.respond({ status: Status.MethodNotAllowed, body: STATUS_TEXT.get(Status.MethodNotAllowed) }); request.respond({ status: Status.MethodNotAllowed, body: STATUS_TEXT.get(Status.MethodNotAllowed) });
break; break;
} }
if (updateRateLimitTime) {
const apiTimeNow = new Date().getTime();
rateLimitTime.set(apiUseridStr, apiTimeNow);
}
} 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 { } else {
// Alert API user that they shouldn't be doing this // Alert API user that they shouldn't be doing this
request.respond({ status: Status.Forbidden, body: STATUS_TEXT.get(Status.Forbidden) }); request.respond({ status: Status.Forbidden, body: STATUS_TEXT.get(Status.Forbidden) });

View File

@ -862,7 +862,7 @@ const parseRoll = (fullCmd: string, localPrefix: string, localPostfix: string, m
} }
// Populate line2 (the results) and line3 (the details) with their data // Populate line2 (the results) and line3 (the details) with their data
line2 += preFormat + e.rollTotal + postFormat + escapeCharacters(e.rollPostFormat, "|*_~`"); line2 += preFormat + e.rollTotal + postFormat + escapeCharacters(e.rollPostFormat, "|*_~`") + " ";
line3 += "`" + e.initConfig + "` = " + e.rollDetails + " = " + preFormat + e.rollTotal + postFormat + "\n"; line3 += "`" + e.initConfig + "` = " + e.rollDetails + " = " + preFormat + e.rollTotal + postFormat + "\n";
}); });