Optional logging of rolls added
API Key Verification Added
API Authorization/Authentication Added
This commit is contained in:
Ean Milligan (Bastion) 2021-01-14 08:47:18 -05:00
parent 05e6a4d8be
commit 31349c5f51
6 changed files with 194 additions and 19 deletions

View File

@ -1,9 +1,9 @@
# The Artificer - A Dice Rolling Discord Bot
Version 1.1.1 - 2020/01/11
Version 1.2.0 - 2020/01/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).
This bot was developed to replace the Sidekick discord bot after it went offline many times for extended periods. This was also developed to fix some annoyances that were found with Sidekick, specifically its vague error messages (such as `"Tarantallegra!"`, what is that supposed to mean) and its inability to handle implicit mulitplication (such as `4(12 + 20)`).
This bot was developed to replace the Sidekick discord bot after it went offline many times for extended periods. This was also developed to fix some annoyances that were found with Sidekick, specifically its vague error messages (such as `"Tarantallegra!"`, what is that supposed to mean) and its inability to handle implicit multiplication (such as `4(12 + 20)`).
## Using The Artificer
I am hosting this bot for public use and you may find its invite link below. If you would like to host this bot yourself, details of how to do so are found at the end of this README, but I do not recommend this unless you are experienced with running Discord bots.
@ -65,13 +65,13 @@ 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.
## The Artificer API
API is currently in development, details on usage and how to gain privilaged access will be added here when the API is feature complete and secured.
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.
## 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.
---
## Running 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.
Starting the bot is simply done with `deno run --allow-net .\mod.ts`.

View File

@ -1,6 +1,6 @@
export const config = {
"name": "The Artificer",
"version": "1.1.1",
"version": "1.2.0",
"token": "the_bot_token",
"prefix": "[[",
"postfix": "]]",
@ -9,6 +9,14 @@ export const config = {
"port": 8080,
"supportURL": "your_support_url_for_api_abuse"
},
"db": {
"host": "",
"port": 3306,
"username": "",
"password": "",
"name": ""
},
"logRolls": true,
"logChannel": "the_log_channel",
"reportChannel": "the_report_channel",
"devServer": "the_dev_server",

70
initDB.ts Normal file
View File

@ -0,0 +1,70 @@
import { Client } from "https://deno.land/x/mysql/mod.ts";
import config from "./config.ts";
// Log into the MySQL DB
const dbClient = await new Client().connect({
hostname: config.db.host,
port: config.db.port,
username: config.db.username,
password: config.db.password,
});
console.log("Attempting to create DB");
await dbClient.execute(`CREATE SCHEMA IF NOT EXISTS ${config.db.name};`);
await dbClient.execute(`USE ${config.db.name}`);
console.log("DB created");
console.log("Attempt to drop all tables");
await dbClient.execute(`DROP TABLE IF EXISTS allowed_channels;`);
await dbClient.execute(`DROP TABLE IF EXISTS all_keys;`);
await dbClient.execute(`DROP TABLE IF EXISTS roll_log;`);
console.log("Tables dropped");
console.log("Attempting to create table roll_log");
await dbClient.execute(`
CREATE TABLE roll_log (
id int unsigned NOT NULL AUTO_INCREMENT,
input text NOT NULL,
resultid bigint,
result longtext NOT NULL,
api tinyint(1) NOT NULL,
error tinyint(1) NOT NULL,
PRIMARY KEY (id),
UNIQUE KEY roll_log_id_UNIQUE (id),
UNIQUE KEY roll_log_resultid_UNIQUE (resultid)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
`);
console.log("Table created");
console.log("Attempting to create table all_keys");
await dbClient.execute(`
CREATE TABLE all_keys (
userid bigint unsigned NOT NULL,
apiKey char(25) NOT NULL,
createdAt timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
active tinyint(1) NOT NULL DEFAULT 1,
banned tinyint(1) NOT NULL DEFAULT 0,
PRIMARY KEY (userid),
UNIQUE KEY api_key_userid_UNIQUE (userid),
UNIQUE KEY api_key_apiKey_UNIQUE (apiKey)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
`);
console.log("Table created");
console.log("Attempting to create table allowed_channels");
await dbClient.execute(`
CREATE TABLE allowed_channels (
userid bigint unsigned NOT NULL,
channelid bigint unsigned NOT NULL,
createdAt timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
active tinyint(1) NOT NULL DEFAULT 1,
banned tinyint(1) NOT NULL DEFAULT 0,
PRIMARY KEY (userid, channelid),
CONSTRAINT allowed_channels_userid_FK FOREIGN KEY (userid) REFERENCES all_keys (userid) ON DELETE RESTRICT ON UPDATE RESTRICT
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
`);
console.log("Table created");
await dbClient.close();
console.log("Done!");

118
mod.ts
View File

@ -15,14 +15,28 @@ import {
Message, Guild, sendMessage, sendDirectMessage,
cache
} from "https://deno.land/x/discordeno@10.0.0/mod.ts";
import { serve } from "https://deno.land/std@0.83.0/http/server.ts";
import { Status, STATUS_TEXT } from "https://deno.land/std@0.83.0/http/http_status.ts";
import { 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 utils from "./src/utils.ts";
import solver from "./src/solver.ts";
import config from "./config.ts";
const dbClient = await new Client().connect({
hostname: config.db.host,
port: config.db.port,
db: config.db.name,
username: config.db.username,
password: config.db.password,
});
// Start up the Discord Bot
startBot({
token: config.token,
intents: [Intents.GUILD_MESSAGES, Intents.DIRECT_MESSAGES, Intents.GUILDS],
@ -140,6 +154,8 @@ startBot({
// Rest of this command is in a try-catch to protect all sends/edits from erroring out
try {
const originalCommand = config.prefix + command + " " + args.join(" ");
const m = await utils.sendIndirectMessage(message, "Rolling...", sendMessage, sendDirectMessage);
const modifiers = {
@ -190,6 +206,13 @@ startBot({
if (modifiers.gms.length < 1) {
// If -gm is on and none were found, throw an error
m.edit("Error: Must specifiy at least one GM by mentioning them");
if (config.logRolls) {
// If enabled, log rolls so we can verify the bots math
dbClient.execute("INSERT INTO roll_log(input,result,resultid,api,error) values(?,?,?,0,1)", [originalCommand, "NoGMsFound", m.id]).catch(e => {
console.log("Failed to insert into database 00", e);
});
}
return;
}
@ -204,12 +227,19 @@ startBot({
// maxRoll and nominalRoll cannot both be on, throw an error
if (modifiers.maxRoll && modifiers.nominalRoll) {
m.edit("Error: Cannot maximise and nominise the roll at the same time");
if (config.logRolls) {
// If enabled, log rolls so we can verify the bots math
dbClient.execute("INSERT INTO roll_log(input,result,resultid,api,error) values(?,?,?,0,1)", [originalCommand, "MaxAndNominal", m.id]).catch(e => {
console.log("Failed to insert into database 01", e);
});
}
return;
}
// Rejoin all of the args and send it into the solver, if solver returns a falsy item, an error object will be substituded in
const rollCmd = command + " " + args.join(" ");
const returnmsg = solver.parseRoll(rollCmd, config.prefix, config.postfix, modifiers.maxRoll, modifiers.nominalRoll) || { error: true, errorMsg: "Error: Empty message", line1: "", line2: "", line3: "" };
const returnmsg = solver.parseRoll(rollCmd, config.prefix, config.postfix, modifiers.maxRoll, modifiers.nominalRoll) || { error: true, errorCode: "EmptyMessage", errorMsg: "Error: Empty message", line1: "", line2: "", line3: "" };
let returnText = "";
@ -217,6 +247,13 @@ startBot({
if (returnmsg.error) {
returnText = returnmsg.errorMsg;
m.edit(returnText);
if (config.logRolls) {
// If enabled, log rolls so we can verify the bots math
dbClient.execute("INSERT INTO roll_log(input,result,resultid,api,error) values(?,?,?,0,1)", [originalCommand, returnmsg.errorCode, m.id]).catch(e => {
console.log("Failed to insert into database 02", e);
});
}
return;
} else {
// Else format the output using details from the solver
@ -248,6 +285,13 @@ startBot({
// Finally send the text
m.edit(normalText);
if (config.logRolls) {
// If enabled, log rolls so we can verify the bots math
dbClient.execute("INSERT INTO roll_log(input,result,resultid,api,error) values(?,?,?,0,0)", [originalCommand, returnText, m.id]).catch(e => {
console.log("Failed to insert into database 03", e);
});
}
} else {
// When not a GM roll, make sure the message is not too big
if (returnText.length > 2000) {
@ -270,6 +314,13 @@ startBot({
// Finally send the text
m.edit(returnText);
if (config.logRolls) {
// If enabled, log rolls so we can verify the bots math
dbClient.execute("INSERT INTO roll_log(input,result,resultid,api,error) values(?,?,?,0,0)", [originalCommand, returnText, m.id]).catch(e => {
console.log("Failed to insert into database 04", e);
});
}
}
} catch (err) {
console.error("Something failed 71");
@ -291,9 +342,21 @@ if (config.api.enable) {
// 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 apiUserid = 0;
// Super secure authentication
const authenticated = true;
// 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, active, banned FROM all_keys WHERE apiKey = ?", [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 && dbApiQuery[0].active && !dbApiQuery[0].banned) {
apiUserid = dbApiQuery[0].userid;
authenticated = true;
}
}
if (authenticated) {
// Get path and query as a string
@ -323,8 +386,14 @@ if (config.api.enable) {
break;
}
// Super secure authorization
const authorized = true;
// 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 = ?", [parseInt(query.get("user") || ""), parseInt(query.get("channel") || "")])
if (dbChannelQuery.length === 1 && (apiUserid === parseInt(query.get("user") || "")) && dbChannelQuery[0].active && !dbChannelQuery[0].banned) {
authorized = true;
}
if (authorized) {
// Rest of this command is in a try-catch to protect all sends/edits from erroring out
@ -337,6 +406,11 @@ if (config.api.enable) {
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)", [rollCmd, "EmptyInput", null]).catch(e => {
console.log("Failed to insert into database 10", e);
});
break;
}
@ -348,11 +422,16 @@ if (config.api.enable) {
// Alert users why this message just appeared and how they can report abues pf this feature
const apiPrefix = "The following roll was conducted using my built in API. If someone in this channel did not request this roll, please report API abuse here: <" + config.api.supportURL + ">\n\n";
let returnText = "";
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)", [rollCmd, 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;
@ -378,6 +457,11 @@ if (config.api.enable) {
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)", [rollCmd, "NoGMsSent", null]).catch(e => {
console.log("Failed to insert into database 12", e);
});
break;
}
@ -389,12 +473,12 @@ if (config.api.enable) {
// Send the return message as a DM or normal message depening on if the channel is set
if ((query.get("channel") || "").length > 0) {
await sendMessage(query.get("channel") || "", normalText).catch(() => {
m = await sendMessage(query.get("channel") || "", normalText).catch(() => {
request.respond({ status: Status.InternalServerError, body: "Message 00 failed to send." });
errorOut = true;
});
} else {
await sendDirectMessage(query.get("user") || "", normalText).catch(() => {
m = await sendDirectMessage(query.get("user") || "", normalText).catch(() => {
request.respond({ status: Status.InternalServerError, body: "Message 01 failed to send." });
errorOut = true;
});
@ -410,12 +494,12 @@ if (config.api.enable) {
const failedSend = "WARNING: <@" + e + "> could not be messaged. If this issue persists, make sure direct messages are allowed from this server."
// Send the return message as a DM or normal message depening on if the channel is set
if ((query.get("channel") || "").length > 0) {
await sendMessage(query.get("channel") || "", failedSend).catch(() => {
m = await sendMessage(query.get("channel") || "", failedSend).catch(() => {
request.respond({ status: Status.InternalServerError, body: "Message 10 failed to send." });
errorOut = true;
});
} else {
await sendDirectMessage(query.get("user") || "", failedSend).catch(() => {
m = await sendDirectMessage(query.get("user") || "", failedSend).catch(() => {
request.respond({ status: Status.InternalServerError, body: "Message 11 failed to send." });
errorOut = true;
});
@ -424,6 +508,11 @@ if (config.api.enable) {
}
});
// 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 => {
console.log("Failed to insert into database 13", e);
});
// Handle closing the request out
if (errorOut) {
break;
@ -453,17 +542,22 @@ if (config.api.enable) {
// Send the return message as a DM or normal message depening on if the channel is set
if ((query.get("channel") || "").length > 0) {
await sendMessage(query.get("channel") || "", returnText).catch(() => {
m = await sendMessage(query.get("channel") || "", returnText).catch(() => {
request.respond({ status: Status.InternalServerError, body: "Message 20 failed to send." });
errorOut = true;
});
} else {
await sendDirectMessage(query.get("user") || "", returnText).catch(() => {
m = await sendDirectMessage(query.get("user") || "", returnText).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)", [rollCmd, 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;

1
src/solver.d.ts vendored
View File

@ -23,6 +23,7 @@ export type SolvedStep = {
export type SolvedRoll = {
error: boolean,
errorMsg: string,
errorCode: string,
line1: string,
line2: string,
line3: string

View File

@ -725,6 +725,7 @@ const parseRoll = (fullCmd: string, localPrefix: string, localPostfix: string, m
const returnmsg = {
error: false,
errorMsg: "",
errorCode: "",
line1: "",
line2: "",
line3: ""
@ -960,6 +961,7 @@ const parseRoll = (fullCmd: string, localPrefix: string, localPostfix: string, m
// Fill in the return block
returnmsg.error = true;
returnmsg.errorCode = solverError.message;
returnmsg.errorMsg = errorMsg;
}