V1.4.0 Completed

This update adds a handful of new features and readies the API for public use.  Detailed changes below:

config.example.ts - Bumped version, added local testing options, added prefill data, changed default logging to false, moved long strings for help and rollhelp to longStrings.ts
db/initialize.ts (previously initDB.ts) - Relocated file for organization, added local options, added new command count and allowed guilds table, created stored procedure for counting commands
db/populateDefaults.ts - Fills in db with default values, adding admin's api key and populating the command count table
flags.ts - Centralized flags for dev/local modes
longStrings.ts - Moved all long string commands to here, contains help, rollhelp, apihelp, info, and privacy commands
mod.ts - Moved flags out into flags.ts, implemented local mode for development, implemented command counting for basic statistics, added info and privacy commands for user help, added more stats to the stats command, added api command, allowing users to allow or block api rolls from happening in their server, reformatted API code, using proper HTTP methods, makes sure api is allowed to roll into chosen guild, added delete endpoint to remove user's data from the database, added endpoint to allow the general public to generate their own api keys
PRIVACY.md - I got bored and wrote a privacy policy, detailing how little data is collected and how the user can have their data removed
README.md - Added new commands to README, updated API documentation, added delete endpoint, updated self hosting details
src/utils.ts - Bumped discordeno version
www/api - Built API website
www/home (previously located in www) - Moved for better organization, minor fixes, updated API details,
This commit is contained in:
Ean Milligan (Bastion) 2021-02-12 23:26:33 -05:00
parent 569974933d
commit d30d43b2ee
19 changed files with 1336 additions and 278 deletions

43
PRIVACY.md Normal file
View File

@ -0,0 +1,43 @@
# The Artificer's Privacy Policy
## Information relating to Discord Interactions
### Public Bot Information
Publicly available versions of `The Artificer#8166` (herein referred to as _The Bot_ or _Bot_) do not track or collect user information via Discord.
Upon inviting _The Bot_ to a user's guild, _The Bot_ sends the guild name, Discord Guild ID, and current count of guild members to Burn_E99#1062 (herein referred to as _The Developer_) via a private Discord Guild. The guild name, Discord Guild ID, and current count of guild members are only used to roughly gage how popular _The Bot_ is and to determine if _The Bot_'s hosting solution needs to be improved. These pieces of information will never be sold or shared with anyone.
Like all Discord bots, _The Bot_ reads every message that it is allowed to, meaning if _The Bot_ is allowed to see a channel in a guild, it reads every new message sent in said channel. This is due to the way the Discord API itself is designed. _The Bot_ does not read any messages sent in the past.
* Messages that do not begin with _The Bot_'s command prefix are not saved or stored anywhere. Messages that do not begin with _The Bot_'s command prefix are ignored and not processed.
* Messages that do begin with _The Bot_'s command prefix do not log user data, and most commands to not log any data. The commands that log data are the report command (in Discord, this command is known as `[[report` or `[[r`) and the API enable/disable commands (in Discord, these commands are known as `[[api enable`, `[[api allow`, `[[api disable`, and `[[api block`).
* The report command only stores the text placed within the message that is directly after the command (herein referred to as _The Report Text_). This command is entirely optional, meaning users never need to run this command under normal usage of _The Bot_. This command is only intended to be used to report roll commands that did not output what was expected. This command will accept any value for _The Report Text_, thus it is up to the user to remove any sensitive information before sending the command. _The Report Text_ is stored in a private Discord Guild in a channel that only _The Developer_ can see. _The Report Text_ is solely used to improve _The Bot_, either by providing a feature suggestions or alerting _The Developer_ to bugs that need patched.
* The API enable/disable commands only stores the Discord Guild ID upon usage. These commands are entirely optional, meaning users never need to run this command under normal usage of _The Bot_. These commands only need to be used when the user desires to utilize the optional API. Discord Guild IDs are internal IDs generated and provided by Discord. _The Bot_ only uses the stored Discord Guild IDs to ensure that API users cannot interact with Guilds that do not allow it or to check if an API user is a member of said Guild. The Guild IDs are only visible to _The Developer_ thru direct database administration. This direct database administration is only used when there are issues with _The Bot_'s database.
All commands contribute to a global counter to track the number of times a command is used. These counters do not keep track of where commands were run, only counting the number of times the command has been called. These counters have no way of being tracked back to the individual commands run by the users.
If the Discord interaction is not explicitly mentioned above, it does not collect any information at all.
### Private Bot Information
Privately hosted versions of The Artificer (in other words, bots running The Artificer's source code, but not running under the publicly available _Bot_, `The Artificer#8166`) (herein referred to as _Rehosts_ or _Rehost_) may be running in DEVMODE, a mode that allows the _Rehost_ to log every roll command used. This mode is intended for development use only, and only allows the roll command to function in the Guild specified in `config.ts` as `config.devServer`. _The Developer_ is not responsible for _Rehosts_, thus _Rehosts_ of _The Bot_ are not recommended to be used.
All policies described in **Public Bot Information** apply to _Rehosts_.
Due to the nature of open source code, _Rehosts_ may not use the same codebase that is available in this repository. _The Developer_ does not moderate what other developers do to this codebase. This means that if you are not using the publicly available _Bot_ and instead using a _Rehost_, this _Rehost_ could collect any information it desires.
# Information relating to the Optional API Interactions
_The Bot_'s API (herein referred to as _The API_) does not automatically collect any information. Users utilizing _The API_ are required to provide a small amount of information before using _The API_.
When generating and API Key, the user must submit two pieces of information to be stored in _The Bot_'s database: their Discord User ID and an email address.
* The Discord User ID is only used to link a single API Key to a single Discord user and to authenticate the user when using _The API_. The stored User IDs are only visible to _The Developer_ thru direct database administration. This direct database administration is only used when there are issues with _The Bot_'s database.
* The user's email address is only used to send the user their generated API Key and to act as method of contact if an issue with their API Key arises. The stored email addresses will only be used for reasons relating to _The API_. The stored email addresses will never be sold or shared with anyone. The stored email addresses are only visible to _The Developer_ thru direct database administration. This direct database administration is only used when there are issues with _The Bot_'s database.
In order to use _The API_, the user must provide _The Bot_ with Discord Channel IDs. These Channel IDs are stored and used to restrict _The API_'s usage to desired Discord Channels. These Channel IDs can be viewed by the user that submitted them via _The API_. The user cannot view Channel IDs that they did not submit and link to their API Key. The stored Channel IDs are also visible to _The Developer_ thru direct database administration. This direct database administration is only used when there are issues with _The Bot_'s database.
When using _The API_'s roll endpoint (herein referred to as _The Roll Endpoint_), users acknowledge that every roll they request will be logged. _The Roll Endpoint_ specifically logs the input string (provided by the user), the result string (generated by _The Bot_ using the input string and sent to Discord in the requested channel), the creation timestamp, and the Discord Message ID of the result message sent by _The Bot_. This information stored is used only to identify and prevent API abuse. This information is only visible to _The Developer_ thru direct database administration. This direct database administration is only used when there are issues with _The Bot_'s database. This information will only be reviewed when _The Developer_ is notified of possible API abuse.
# Deleting Your Data
## API Data Deletion
If you would like to remove all of your submitted data, this can easily be done using the [[BUTTON NAME]] button on _The Bot_'s [API Tools](https://artificer.eanm.dev/). This will delete all Discord Channel ID/Discord User ID combos that you have submitted. This will also delete your API key entry, completely removing your email address and Discord User ID from _The Bot_'s database.
If you would like your Discord Guild ID to be removed from _The Bot_'s database, a Guild Owner or Administrator needs to run `[[api delete`. This will remove your Discord Guild's ID from _The Bot_'s database, reverting it back to the default setting of blocking _The API_.
## Discord Command Data Deletion
If you would like to ensure that all of your submitted reports are removed from _The Bot_'s private development server, please contact _The Developer_ via Discord (by sending a direct message to Burn_E99#1062) or via email (<ean@milligan.dev>) with a message along the lines of `"Please remove all of my submitted reports from your development server."`. Submitted reports are deleted from the server as they are processed, which happens roughly once a week, but this can be accelerated if requested.

View File

@ -1,5 +1,5 @@
# The Artificer - A Dice Rolling Discord Bot
Version 1.3.3 - 2020/01/27
Version 1.4.0 - 2021/02/13
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).
@ -19,8 +19,27 @@ The Artificer comes with a few supplemental commands to the main rolling command
* `[[help or [[h or [[?`
* Provides a message similar to this available commands block.
* `[[rollhelp or [[??`
* Details on how to use the roll command, listed as `[[xdy...]]` below.
* `[[api [subcommand]`
* Administrative tools for the bots's API. These commands may only be used by the Owner or Admins of your guild.
* Available Subcommands:
* `[[api help`
* Provides a message similar to this subcommand description.
* `[[api status`
* Shows the current status of the API for this guild.
* `[[api allow or [[api enable`
* Allows API Rolls to be sent to this guild.
* `[[api block or [[api disable`
* Blocks API Rolls from being sent to this guild.
* `[[api delete`
* Deletes this guild from The Artificer's database.
* `[[ping`
* Tests the latency between you, Discord, and the bot.
* `[[info or [[i`
* Outputs some information and links relating to the bot.
* `[[privacy`
* Prints some information about the Privacy Policy, found in `PRIVACY.md`.
* `[[version or [[v`
* Prints out the current version of the bot.
* `[[popcat or [[pop or [[p`
@ -75,6 +94,8 @@ The Artificer comes with a few supplemental commands to the main rolling command
## The Artificer API
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.
Guilds Owners or Admins must run the `[[api allow` command for any users to be able to use the `/api/roll` endpoint.
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:
@ -84,13 +105,15 @@ Every API request **requires** the header `X-Api-Key` with the value set to the
* `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 URL: `https://artificer.eanm.dev/api/`
* `/api/roll`
Available Endpoints and Methods Required:
* `/api/roll` - `GET`
* 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.
* `channel` - The Discord Channel ID that the bot is to send the results into.
* `rollstr` - A roll string formatted identically to the roll command detailed in the "Available Commands" section.
* Optional query parameters (these parameters do not require values unless specified):
* `nd` - No Details - Suppresses all details of the requested roll.
* `s` - Spoiler - Spoilers all details of the requested roll.
@ -100,29 +123,46 @@ Available Endpoints:
* `o` - Order Roll - Rolls the requested roll and orders the results in the requested direction. Takes a single character: `a` or `d`.
* Returns:
* `200` - OK - Results of the roll should be found in Discord, but also are returned as a string via the API.
* `/api/channel`
* `/api/channel` - `GET`
* 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`
* `/api/channel/add` - `POST`
* Required query parameters:
* `user` - Your Discord ID.
* `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`
* `/api/channel/activate` - `PUT`
* Required query parameters:
* `user` - Your Discord ID.
* `channel` - The Discord Channel ID you wish to reactivate.
* `user` - Your Discord ID.
* Returns:
* `200` - OK - Nothing to be returned.
* `/api/channel/deactivate`
* `/api/channel/deactivate` - `PUT`
* Required query parameters:
* `channel` - The Discord Channel ID you wish to deactivate.
* `user` - Your Discord ID.
* `channel` - The Discord Channel ID you wish to deactivate.
* Returns:
* `200` - OK - Nothing to be returned.
* `/api/key` - `GET`
* This endpoint does not require the `X-Api-Key` header.
* Required query parameters:
* `user` - Your Discord ID.
* `email` - An email address you can be reached at. The API Key will be sent to this address.
* Returns:
* `200` - OK - Nothing to be returned. API Key will be emailed to you within 24 hours.
* `/api/key/delete` - `DELETE`
* Required query parameters:
* `user` - Your Discord ID.
* `email` - An email address you can be reached at. This must match the email you registered with. The delete code will be sent to this address.
* `code` - Run this endpoint first without this field. Once you recieve the email containing the delete code, run this API a second time with this field
* Returns:
* `424` - Failed dependancy - You will be emailed a delete code to rerun this endpoint with.
* `200` - OK - Everything relating to your API key was successfully removed.
API Key management via a basic GUI is availble on [API Tools](https://artificer.eanm.dev/).
## 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.
@ -130,13 +170,13 @@ If you run into any errors or problems with the bot, or think you have a good id
---
## 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.7.0` 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.
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 Privileges. Once the DB is installed and a user is setup, run the provided `initDB.ts` to create the schema and tables.
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 Privileges. Once the DB is installed and a user is setup, run the provided `db\initialize.ts` to create the schema and tables. After this, run `db\populateDefaults.ts` to insert some needed values into the tables.
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`.
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 should manually generate a 25 char nanoid and place it in `config.api.adminKey` and copy your `userid` and place it in `config.api.admin` before running `db\populateDefaults.ts`.
---

View File

@ -1,7 +1,8 @@
export const config = {
"name": "The Artificer", // Name of the bot
"version": "1.3.3", // Version of the bot
"version": "1.4.0", // Version of the 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"
"prefix": "[[", // Prefix for all commands
"postfix": "]]", // Postfix for rolling command
"api": { // Setting for the built-in API
@ -10,69 +11,22 @@ export const config = {
"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
"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
"adminKey": "your_25char_api_token", // API Key generated by nanoid that is 25 char long, this gets pre-populated into all_keys
"email": "" // Temporary set up for email, this will be adjusted to an actual email using deno-smtp in the future.
},
"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": "", // IP address for the db, usually localhost
"localhost": "", // IP address for a secondary OPTIONAL local testing DB, usually also is localhost, but depends on your dev environment
"port": 3306, // Port for the db
"username": "", // Username for the account that will access your DB, this account will need "DB Manager" admin rights and "REFERENCES" Global Privalages
"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 of the database Schema to use for the bot
},
"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
"logRolls": false, // 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", // Discord channel ID where the bot should put startup messages and other error messages needed
"reportChannel": "the_report_channel", // Discord channel ID where reports will be sent when using the built-in report command
"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": [ // Array of strings that makes up the help command, placed here to keep source code cleaner
"```fix",
"The Artificer Help",
"```",
"__**Commands:**__",
"```",
"[[? - This Command",
"[[rollhelp or ?? - Details on how to use the roll command, listed as [[xdy...]] below",
"[[ping - Pings the bot to check connectivity",
"[[version - Prints the bots version",
"[[popcat - Popcat",
"[[report [text] - Report a command that failed to run",
"[[stats - Statistics on the bot",
"[[xdydzracsq!]] ... - Rolls all configs requested, you may repeat the command multiple times in the same message (just ensure you close each roll with ]]), run [[?? for more details",
"```"
],
"rollhelp": [ // Array of strings that makes up the rollhelp command, placed here to keep source code cleaner
"```fix",
"The Artificer Roll Command Details",
"```",
"```",
"[[xdydzracsq!]] ... - Rolls all configs requested, you may repeat the command multiple times in the same message (just ensure you close each roll with ]])",
"* x [OPT] - number of dice to roll, if omitted, 1 is used",
"* dy [REQ] - size of dice to roll, d20 = 20 sided die",
"* dz or dlz [OPT] - drops the lowest z dice, cannot be used with kz",
"* kz or khz [OPT] - keeps the highest z dice, cannot be used with dz",
"* dhz [OPT] - drops the highest z dice, cannot be used with kz",
"* klz [OPT] - keeps the lowest z dice, cannot be used with dz",
"* ra [OPT] - rerolls any rolls that match a, r3 will reroll any dice that land on 3, throwing out old rolls",
"* 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 greater than or equal to q ",
"* cfq or cs=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 greater than or equal to q",
"* ! [OPT] - exploding, rolls another dy for every crit roll",
"*",
"* This command also can fully solve math equations with parenthesis",
"*",
"* This command also has some useful flags that can used. These flags simply need to be placed after all rolls in the message:",
" * -nd No Details - Suppresses all details of the requested roll",
" * -s Spoiler - Spoilers all details of the requested roll",
" * -m Maximize Roll - Rolls the theoretical maximum roll, cannot be used with -n",
" * -n Nominal Roll - Rolls the theoretical nominal roll, cannot be used with -m",
" * -gm @user1 @user2 @usern",
" * GM Roll - Rolls the requested roll in GM mode, suppressing all publicly shown results and details and sending the results directly to the specified GMs",
" * -o a or -o d",
" * Order Roll - Rolls the requested roll and orders the results in the requested direction",
"```"
],
"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
{ // Emoji object, duplicate for each emoji
"name": "popcat", // Name of emoji in discord

View File

@ -1,10 +1,14 @@
// This file will create all tables for the artificer schema
// DATA WILL BE LOST IF DB ALREADY EXISTS, RUN AT OWN RISK
import { Client } from "https://deno.land/x/mysql/mod.ts";
import config from "./config.ts";
import { LOCALMODE } from "../flags.ts";
import config from "../config.ts";
// Log into the MySQL DB
const dbClient = await new Client().connect({
hostname: config.db.host,
hostname: LOCALMODE ? config.db.localhost : config.db.host,
port: config.db.port,
username: config.db.username,
password: config.db.password,
@ -18,9 +22,36 @@ 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 allowed_guilds;`);
await dbClient.execute(`DROP TABLE IF EXISTS roll_log;`);
await dbClient.execute(`DROP PROCEDURE IF EXISTS INC_CNT;`);
await dbClient.execute(`DROP TABLE IF EXISTS command_cnt;`);
console.log("Tables dropped");
console.log("Attempting to create table command_cnt");
await dbClient.execute(`
CREATE TABLE command_cnt (
command char(20) NOT NULL,
count bigint unsigned NOT NULL DEFAULT 0,
PRIMARY KEY (command),
UNIQUE KEY command_cnt_command_UNIQUE (command)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
`);
console.log("Table created");
console.log("Attempt creating increment Stored Procedure");
await dbClient.execute(`
CREATE PROCEDURE INC_CNT(
IN cmd CHAR(20)
)
BEGIN
declare oldcnt bigint unsigned;
set oldcnt = (SELECT count FROM command_cnt WHERE command = cmd);
UPDATE command_cnt SET count = oldcnt + 1 WHERE command = cmd;
END
`);
console.log("Stored Procedure created");
console.log("Attempting to create table roll_log");
await dbClient.execute(`
CREATE TABLE roll_log (
@ -38,11 +69,25 @@ await dbClient.execute(`
`);
console.log("Table created");
console.log("Attempting to create table allowed_guilds");
await dbClient.execute(`
CREATE TABLE allowed_guilds (
guildid bigint unsigned NOT NULL,
createdAt timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
active tinyint(1) NOT NULL DEFAULT 0,
banned tinyint(1) NOT NULL DEFAULT 0,
PRIMARY KEY (guildid),
UNIQUE KEY allowed_guilds_guildid_UNIQUE (guildid)
) 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,
deleteCode char(10) NULL,
email char(255) NULL,
createdAt timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
active tinyint(1) NOT NULL DEFAULT 1,

34
db/populateDefaults.ts Normal file
View File

@ -0,0 +1,34 @@
// This file will populate the tables with default values
import { Client } from "https://deno.land/x/mysql/mod.ts";
import { LOCALMODE } from "../flags.ts";
import config from "../config.ts";
// Log into the MySQL DB
const dbClient = await new Client().connect({
hostname: LOCALMODE ? config.db.localhost : config.db.host,
port: config.db.port,
db: config.db.name,
username: config.db.username,
password: config.db.password,
});
console.log("Attempting to populate DB Admin API key");
await dbClient.execute("INSERT INTO all_keys(userid,apiKey) values(?,?)", [config.api.admin, config.api.adminKey]).catch(e => {
console.log("Failed to insert into database", e);
});
console.log("Inesrtion done");
console.log("Attempting to insert default commands into command_cnt");
const commands = ["ping", "rip", "rollhelp", "help", "info", "version", "report", "stats", "roll", "emojis", "api", "privacy"];
for (let i = 0; i < commands.length; i++) {
await dbClient.execute("INSERT INTO command_cnt(command) values(?)", [commands[i]]).catch(e => {
console.log(`Failed to insert into database`, e);
});
}
console.log("Insertion done");
await dbClient.close();
console.log("Done!");

6
flags.ts Normal file
View File

@ -0,0 +1,6 @@
// DEVMODE is to prevent users from accessing parts of the bot that are currently broken
export const DEVMODE = false;
// DEBUG is used to toggle the cmdPrompt
export const DEBUG = false;
// LOCALMODE is used to run a differnt bot token for local testing
export const LOCALMODE = false;

87
longStrings.ts Normal file
View File

@ -0,0 +1,87 @@
export const longStrs = {
"help": [ // Array of strings that makes up the help command, placed here to keep source code cleaner
"```fix",
"The Artificer Help",
"```",
"__**Commands:**__",
"```",
"[[? - This command",
"[[rollhelp or [[?? - Details on how to use the roll command, listed as [[xdy...]] below",
"[[api [subcommand] - Administrative tools for the bots's API, run [[api help for more details",
"[[ping - Pings the bot to check connectivity",
"[[info - Prints some information and links relating to the bot",
"[[privacy - Prints some information about the Privacy Policy",
"[[version - Prints the bots version",
"[[popcat - Popcat",
"[[report [text] - Report a command that failed to run",
"[[stats - Statistics on the bot",
"[[xdydzracsq!]] ... - Rolls all configs requested, you may repeat the command multiple times in the same message (just ensure you close each roll with ]]), run [[?? for more details",
"```"
],
"rollhelp": [ // Array of strings that makes up the rollhelp command, placed here to keep source code cleaner
"```fix",
"The Artificer Roll Command Details",
"```",
"```",
"[[xdydzracsq!]] ... - Rolls all configs requested, you may repeat the command multiple times in the same message (just ensure you close each roll with ]])",
"* x [OPT] - number of dice to roll, if omitted, 1 is used",
"* dy [REQ] - size of dice to roll, d20 = 20 sided die",
"* dz or dlz [OPT] - drops the lowest z dice, cannot be used with kz",
"* kz or khz [OPT] - keeps the highest z dice, cannot be used with dz",
"* dhz [OPT] - drops the highest z dice, cannot be used with kz",
"* klz [OPT] - keeps the lowest z dice, cannot be used with dz",
"* ra [OPT] - rerolls any rolls that match a, r3 will reroll any dice that land on 3, throwing out old rolls",
"* 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 greater than or equal to q ",
"* cfq or cs=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 greater than or equal to q",
"* ! [OPT] - exploding, rolls another dy for every crit roll",
"*",
"* This command also can fully solve math equations with parenthesis",
"*",
"* This command also has some useful flags that can used. These flags simply need to be placed after all rolls in the message:",
" * -nd No Details - Suppresses all details of the requested roll",
" * -s Spoiler - Spoilers all details of the requested roll",
" * -m Maximize Roll - Rolls the theoretical maximum roll, cannot be used with -n",
" * -n Nominal Roll - Rolls the theoretical nominal roll, cannot be used with -m",
" * -gm @user1 @user2 @usern",
" * GM Roll - Rolls the requested roll in GM mode, suppressing all publicly shown results and details and sending the results directly to the specified GMs",
" * -o a or -o d",
" * Order Roll - Rolls the requested roll and orders the results in the requested direction",
"```"
],
"apihelp": [ // Array of strings making up the api help command, placed here to keep source code cleaner
"The Artificer has a built in API that allows user to roll dice into Discord using third party programs. By default, API rolls are blocked from being sent in your guild. These commands may only be used by the Owner or Admins of your guild.",
"",
"For information on how to use the API, please check the GitHub README for more information: <https://github.com/Burn-E99/TheArtificer>",
"",
"__**Available Subcommands:**__",
"```",
"[[api help - This command",
"[[api status - Shows the current status of the API for this guild",
"[[api allow/enable - Allows API Rolls to be sent to this guild",
"[[api block/disable - Blocks API Rolls from being sent to this guild",
"[[api delete - Deletes this guild from The Artificer's database",
"```",
"You may enable and disable the API rolls for your guild as needed."
],
"info": [ // Array of strings making up the info command, placed here to keep source code cleaner
"The Artificer is a Discord bot that specializes in rolling dice and calculating math.",
"",
"The Artificer is developed by Ean AKA Burn_E99.",
"",
"Additional information can be found on my website: <https://discord.burne99.com/TheArtificer/>",
"Want to check out my source code? Check it out here: <https://github.com/Burn-E99/TheArtificer>",
"Need help with this bot? Join my support server here: https://discord.gg/peHASXMZYv"
],
"privacy": [ // Array of strings making up the privacy command, placed here to keep source code cleaner
"The Artificer does not track or collect user information via Discord.",
"The only user submitted information that is stored is submitted via the `[[report` command. This information is only stored for a short period of time in a location that only the Developer of The Artificer can see.",
"",
"For more details, please check out the Privacy Policy on the GitHub: <https://github.com/Burn-E99/TheArtificer/blob/master/PRIVACY.md>"
]
};
export default longStrs;

769
mod.ts
View File

@ -4,18 +4,14 @@
* December 21, 2020
*/
// DEVMODE is to prevent users from accessing parts of the bot that are currently broken
const DEVMODE = false;
// DEBUG is used to toggle the cmdPrompt
const DEBUG = true;
import {
startBot, editBotsStatus,
Intents, StatusTypes, ActivityType,
Message, Guild, sendMessage, sendDirectMessage,
cache,
MessageContent
} from "https://deno.land/x/discordeno@10.0.0/mod.ts";
MessageContent,
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";
import { Status, STATUS_TEXT } from "https://deno.land/std@0.83.0/http/http_status.ts";
@ -29,10 +25,13 @@ import solver from "./src/solver.ts";
import { EmojiConf } from "./src/mod.d.ts";
import { DEVMODE, DEBUG, LOCALMODE } from "./flags.ts";
import config from "./config.ts";
import longStrs from "./longStrings.ts";
// Initialize DB client
const dbClient = await new Client().connect({
hostname: config.db.host,
hostname: LOCALMODE ? config.db.localhost : config.db.host,
port: config.db.port,
db: config.db.name,
username: config.db.username,
@ -41,11 +40,11 @@ const dbClient = await new Client().connect({
// Start up the Discord Bot
startBot({
token: config.token,
token: LOCALMODE ? config.localtoken : config.token,
intents: [Intents.GUILD_MESSAGES, Intents.DIRECT_MESSAGES, Intents.GUILDS],
eventHandlers: {
ready: () => {
console.log("Logged in!");
console.log(`${config.name} Logged in!`);
editBotsStatus(StatusTypes.Online, `${config.prefix}help for commands`, ActivityType.Game);
// setTimeout added to make sure the startup message does not error out
setTimeout(() => {
@ -81,6 +80,11 @@ startBot({
// [[ping
// Its a ping test, what else do you want.
if (command === "ping") {
// Light telemetry to see how many times a command is being run
dbClient.execute(`CALL INC_CNT("ping");`).catch(err => {
console.error("Failed to call procedure 00", err);
});
// Calculates ping between sending a message and editing it, giving a nice round-trip latency.
try {
const m = await utils.sendIndirectMessage(message, "Ping?", sendMessage, sendDirectMessage);
@ -93,6 +97,11 @@ startBot({
// [[rip [[memory
// Displays a short message I wanted to include
else if (command === "rip" || command === "memory") {
// Light telemetry to see how many times a command is being run
dbClient.execute(`CALL INC_CNT("rip");`).catch(err => {
console.error("Failed to call procedure 01", err);
});
utils.sendIndirectMessage(message, "The Artificer was built in memory of my Grandmother, Babka\nWith much love, Ean\n\nDecember 21, 2020", sendMessage, sendDirectMessage).catch(err => {
console.error("Failed to send message 11", message, err);
});
@ -100,8 +109,13 @@ startBot({
// [[rollhelp or [[rh or [[hr
// Help command specifically for the roll command
else if (command === "rollhelp" || command === "rh" || command === "hr" || command === "??") {
utils.sendIndirectMessage(message, config.rollhelp.join("\n"), sendMessage, sendDirectMessage).catch(err => {
else if (command === "rollhelp" || command === "rh" || command === "hr" || command === "??" || command?.startsWith("xdy")) {
// Light telemetry to see how many times a command is being run
dbClient.execute(`CALL INC_CNT("rollhelp");`).catch(err => {
console.error("Failed to call procedure 02", err);
});
utils.sendIndirectMessage(message, longStrs.rollhelp.join("\n"), sendMessage, sendDirectMessage).catch(err => {
console.error("Failed to send message 21", message, err);
});
}
@ -109,14 +123,50 @@ startBot({
// [[help or [[h or [[?
// Help command, prints from help file
else if (command === "help" || command === "h" || command === "?") {
utils.sendIndirectMessage(message, config.help.join("\n"), sendMessage, sendDirectMessage).catch(err => {
// Light telemetry to see how many times a command is being run
dbClient.execute(`CALL INC_CNT("help");`).catch(err => {
console.error("Failed to call procedure 03", err);
});
utils.sendIndirectMessage(message, longStrs.help.join("\n"), sendMessage, sendDirectMessage).catch(err => {
console.error("Failed to send message 20", message, err);
});
}
// [[info or [[i
// Info command, prints short desc on bot and some links
else if (command === "info" || command === "i") {
// Light telemetry to see how many times a command is being run
dbClient.execute(`CALL INC_CNT("info");`).catch(err => {
console.error("Failed to call procedure 04", err);
});
utils.sendIndirectMessage(message, longStrs.info.join("\n"), sendMessage, sendDirectMessage).catch(err => {
console.error("Failed to send message 22", message, err);
});
}
// [[privacy
// Privacy command, prints short desc on bot's privacy policy
else if (command === "privacy") {
// Light telemetry to see how many times a command is being run
dbClient.execute(`CALL INC_CNT("privacy");`).catch(err => {
console.error("Failed to call procedure 04", err);
});
utils.sendIndirectMessage(message, longStrs.privacy.join("\n"), sendMessage, sendDirectMessage).catch(err => {
console.error("Failed to send message 2E", message, err);
});
}
// [[version or [[v
// Returns version of the bot
else if (command === "version" || command === "v") {
// Light telemetry to see how many times a command is being run
dbClient.execute(`CALL INC_CNT("version");`).catch(err => {
console.error("Failed to call procedure 05", err);
});
utils.sendIndirectMessage(message, `My current version is ${config.version}.`, sendMessage, sendDirectMessage).catch(err => {
console.error("Failed to send message 30", message, err);
});
@ -125,6 +175,11 @@ startBot({
// [[report or [[r (command that failed)
// Manually report a failed roll
else if (command === "report" || command === "r") {
// Light telemetry to see how many times a command is being run
dbClient.execute(`CALL INC_CNT("report");`).catch(err => {
console.error("Failed to call procedure 06", err);
});
sendMessage(config.reportChannel, ("USER REPORT:\n" + args.join(" "))).catch(err => {
console.error("Failed to send message 50", message, err);
});
@ -136,14 +191,154 @@ startBot({
// [[stats or [[s
// Displays stats on the bot
else if (command === "stats" || command === "s") {
utils.sendIndirectMessage(message, `${config.name} is rolling dice for ${cache.members.size} users, in ${cache.channels.size} channels of ${cache.guilds.size} servers.`, sendMessage, sendDirectMessage).catch(err => {
// Light telemetry to see how many times a command is being run
dbClient.execute(`CALL INC_CNT("stats");`).catch(err => {
console.error("Failed to call procedure 07", err);
});
// Calculate how many times commands have been run
const rollQuery = await dbClient.query(`SELECT count FROM command_cnt WHERE command = "roll";`).catch(err => {
console.error("Failed to query 17", err);
});
const totalQuery = await dbClient.query(`SELECT SUM(count) as count FROM command_cnt;`).catch(err => {
console.error("Failed to query 27", err);
});
const rolls = BigInt(rollQuery[0].count);
const total = BigInt(totalQuery[0].count);
utils.sendIndirectMessage(message, `${config.name} is rolling dice for ${cache.members.size} active users, in ${cache.channels.size} channels of ${cache.guilds.size} servers.\n\nSo far, ${rolls} dice have been rolled and ${total - rolls} utility commands have been run.`, sendMessage, sendDirectMessage).catch(err => {
console.error("Failed to send message 60", message, err);
});
}
// [[api arg
// API sub commands
else if (command === "api" && args.length > 0) {
// Light telemetry to see how many times a command is being run
dbClient.execute(`CALL INC_CNT("api");`).catch(err => {
console.error("Failed to call procedure 0A", err);
});
// Local apiArg in lowercase
const apiArg = args[0].toLowerCase();
// Alert users who DM the bot that this command is for guilds only
if (message.guildID === "") {
utils.sendIndirectMessage(message, `API commands are only available in guilds.`, sendMessage, sendDirectMessage).catch(err => {
console.error("Failed to send message 24", message, err);
});
return;
}
// Makes sure the user is authenticated to run the API command
if (await memberIDHasPermission(message.author.id, message.guildID, ["ADMINISTRATOR"])) {
// [[api help
// Shows API help details
if (apiArg === "help") {
utils.sendIndirectMessage(message, longStrs.apihelp.join("\n"), sendMessage, sendDirectMessage).catch(err => {
console.error("Failed to send message 23", message, err);
});
}
// [[api allow/block
// Lets a guild admin allow or ban API rolls from happening in said guild
else if (apiArg === "allow" || apiArg === "block" || apiArg === "enable" || apiArg === "disable") {
const guildQuery = await dbClient.query(`SELECT guildid FROM allowed_guilds WHERE guildid = ?`, [message.guildID]).catch(err => {
console.error("Failed to query 1A", err);
utils.sendIndirectMessage(message, `Failed to ${apiArg} API rolls for this guild. If this issue persists, please report this to the developers.`, sendMessage, sendDirectMessage).catch(err => {
console.error("Failed to send message 29", message, err);
});
return;
});
if (guildQuery.length === 0) {
// Since guild is not in our DB, add it in
await dbClient.execute(`INSERT INTO allowed_guilds(guildid,active) values(?,?)`, [BigInt(message.guildID), ((apiArg === "allow" || apiArg === "enable") ? 1 : 0)]).catch(err => {
console.error("Failed to inersert 2A", err);
utils.sendIndirectMessage(message, `Failed to ${apiArg} API rolls for this guild. If this issue persists, please report this to the developers.`, sendMessage, sendDirectMessage).catch(err => {
console.error("Failed to send message 26", message, err);
});
return;
});
} else {
// Since guild is in our DB, update it
await dbClient.execute(`UPDATE allowed_guilds SET active = ? WHERE guildid = ?`, [((apiArg === "allow" || apiArg === "enable") ? 1 : 0), BigInt(message.guildID)]).catch(err => {
console.error("Failed to inersert 3A", err);
utils.sendIndirectMessage(message, `Failed to ${apiArg} API rolls for this guild. If this issue persists, please report this to the developers.`, sendMessage, sendDirectMessage).catch(err => {
console.error("Failed to send message 28", message, err);
});
return;
});
}
// We won't get here if there's any errors, so we know it has bee successful, so report as such
utils.sendIndirectMessage(message, `API rolls have successfully been ${apiArg}ed for this guild.`, sendMessage, sendDirectMessage).catch(err => {
console.error("Failed to send message 27", message, err);
});
}
// [[api delete
// Lets a guild admin delete their server from the database
else if (apiArg === "delete") {
await dbClient.execute(`DELETE FROM allowed_guilds WHERE guildid = ?`, [message.guildID]).catch(err => {
console.error("Failed to query 1B", err);
utils.sendIndirectMessage(message, `Failed to delete this guild from the database. If this issue persists, please report this to the developers.`, sendMessage, sendDirectMessage).catch(err => {
console.error("Failed to send message 2F", message, err);
});
return;
});
// We won't get here if there's any errors, so we know it has bee successful, so report as such
utils.sendIndirectMessage(message, `This guild's API setting has been removed from The Artifier's Database.`, sendMessage, sendDirectMessage).catch(err => {
console.error("Failed to send message 2G", message, err);
});
}
// [[api status
// Lets a guild admin check the status of API rolling in said guild
else if (apiArg === "status") {
// Get status of guild from the db
const guildQuery = await dbClient.query(`SELECT active, banned FROM allowed_guilds WHERE guildid = ?`, [message.guildID]).catch(err => {
console.error("Failed to query 1A", err);
utils.sendIndirectMessage(message, `Failed to check API rolls status for this guild. If this issue persists, please report this to the developers.`, sendMessage, sendDirectMessage).catch(err => {
console.error("Failed to send message 2A", message, err);
});
return;
});
// Check if we got an item back or not
if (guildQuery.length > 0) {
// Check if guild is banned from using API and return appropriate message
if (guildQuery[0].banned) {
utils.sendIndirectMessage(message, `The Artificer's API is ${config.api.enable ? "currently enabled" : "currently disabled"}.\n\nAPI rolls are banned from being used in this guild. This will not be reversed.`, sendMessage, sendDirectMessage).catch(err => {
console.error("Failed to send message 2B", message, err);
});
} else {
utils.sendIndirectMessage(message, `The Artificer's API is ${config.api.enable ? "currently enabled" : "currently disabled"}.\n\nAPI rolls are ${guildQuery[0].active ? "allowed" : "blocked from being used"} in this guild.`, sendMessage, sendDirectMessage).catch(err => {
console.error("Failed to send message 2C", message, err);
});
}
} else {
// Guild is not in DB, therefore they are blocked
utils.sendIndirectMessage(message, `The Artificer's API is ${config.api.enable ? "currently enabled" : "currently disabled"}.\n\nAPI rolls are blocked from being used in this guild.`, sendMessage, sendDirectMessage).catch(err => {
console.error("Failed to send message 2D", message, err);
});
}
}
} else {
utils.sendIndirectMessage(message, `API commands are powerful and can only be used by guild Owners and Admins.\n\nFor information on how to use the API, please check the GitHub README for more information: <https://github.com/Burn-E99/TheArtificer>`, sendMessage, sendDirectMessage).catch(err => {
console.error("Failed to send message 25", message, err);
});
}
}
// [[roll]]
// Dice rolling commence!
else if ((command + args.join("")).indexOf(config.postfix) > -1) {
// Light telemetry to see how many times a command is being run
dbClient.execute(`CALL INC_CNT("roll");`).catch(err => {
console.error("Failed to call procedure 08", err);
});
// If DEVMODE is on, only allow this command to be used in the devServer
if (DEVMODE && message.guildID !== config.devServer) {
utils.sendIndirectMessage(message, "Command is in development, please try again later.", sendMessage, sendDirectMessage).catch(err => {
@ -208,7 +403,7 @@ startBot({
// 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 (DEVMODE && 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);
@ -227,7 +422,7 @@ startBot({
// If -o is on and asc or desc was not specified, error out
m.edit("Error: Must specifiy a or d to order the rolls ascending or descending");
if (config.logRolls) {
if (DEVMODE && 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, "NoOrderFound", m.id]).catch(e => {
console.log("Failed to insert into database 05", e);
@ -250,7 +445,7 @@ startBot({
if (modifiers.maxRoll && modifiers.nominalRoll) {
m.edit("Error: Cannot maximise and nominise the roll at the same time");
if (config.logRolls) {
if (DEVMODE && 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);
@ -270,7 +465,7 @@ startBot({
returnText = returnmsg.errorMsg;
m.edit(returnText);
if (config.logRolls) {
if (DEVMODE && 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);
@ -296,13 +491,13 @@ startBot({
// And message the full details to each of the GMs, alerting roller of every GM that could not be messaged
modifiers.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"});
const b = await new Blob([returnText as BlobPart], { "type": "text" });
// Update return text
returnText = "<@" + message.author.id + ">" + 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.substr(2, (e.length - 3)), {"content": returnText, "file": {"blob": b, "name": "rollDetails.txt"}}).catch(() => {
await sendDirectMessage(e.substr(2, (e.length - 3)), { "content": returnText, "file": { "blob": b, "name": "rollDetails.txt" } }).catch(() => {
utils.sendIndirectMessage(message, "WARNING: " + e + " could not be messaged. If this issue persists, make sure direct messages are allowed from this server.", sendMessage, sendDirectMessage);
});
});
@ -310,7 +505,7 @@ startBot({
// Finally send the text
m.edit(normalText);
if (config.logRolls) {
if (DEVMODE && 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);
@ -320,7 +515,7 @@ startBot({
// 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"});
const b = await new Blob([returnText as BlobPart], { "type": "text" });
// Update return text
returnText = "<@" + message.author.id + ">" + 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.";
@ -328,13 +523,13 @@ startBot({
// Remove the original message to send new one with attachment
m.delete();
await utils.sendIndirectMessage(message, {"content": returnText, "file": {"blob": b, "name": "rollDetails.txt"}}, sendMessage, sendDirectMessage);
await utils.sendIndirectMessage(message, { "content": returnText, "file": { "blob": b, "name": "rollDetails.txt" } }, sendMessage, sendDirectMessage);
} else {
// Finally send the text
m.edit(returnText);
}
if (config.logRolls) {
if (DEVMODE && 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);
@ -353,6 +548,11 @@ startBot({
config.emojis.some((e: EmojiConf) => {
// If a match gets found
if (e.aliases.indexOf(command || "") > -1) {
// Light telemetry to see how many times a command is being run
dbClient.execute(`CALL INC_CNT("emoji");`).catch(err => {
console.error("Failed to call procedure 09", err);
});
// 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);
@ -394,15 +594,19 @@ if (config.api.enable) {
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 FROM all_keys WHERE apiKey = ? AND active = 1 AND banned = 0", [request.headers.get("X-Api-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 = dbApiQuery[0].userid;
apiUserid = BigInt(dbApiQuery[0].userid);
apiUserEmail = dbApiQuery[0].email;
apiUserDelCode = dbApiQuery[0].deleteCode;
authenticated = true;
// Rate limiting inits
@ -456,7 +660,7 @@ if (config.api.enable) {
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 => {
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;
@ -479,57 +683,6 @@ if (config.api.enable) {
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)) {
@ -538,7 +691,7 @@ if (config.api.enable) {
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 => {
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;
@ -562,117 +715,6 @@ if (config.api.enable) {
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
@ -687,10 +729,20 @@ if (config.api.enable) {
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 = ?", [BigInt(query.get("user")), BigInt(query.get("channel"))])
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
@ -796,13 +848,13 @@ if (config.api.enable) {
// 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"});
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 () => {
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) {
@ -832,20 +884,20 @@ if (config.api.enable) {
break;
}
} else {
const newMessage : MessageContent= {};
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"});
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"};
newMessage.file = { "blob": b, "name": "rollDetails.txt" };
}
// Send the return message as a DM or normal message depening on if the channel is set
@ -894,6 +946,264 @@ if (config.api.enable) {
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) });
@ -904,6 +1214,73 @@ if (config.api.enable) {
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) });

View File

@ -4,7 +4,7 @@
* December 21, 2020
*/
import { Message, MessageContent } from "https://deno.land/x/discordeno@10.0.0/mod.ts";
import { Message, MessageContent } from "https://deno.land/x/discordeno@10.3.0/mod.ts";
// split2k(longMessage) returns shortMessage[]
// split2k takes a long string in and cuts it into shorter strings to be sent in Discord

132
www/api/index.html Normal file
View File

@ -0,0 +1,132 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0" />
<meta name="HandheldFriendly" content="true"/>
<meta name="author" content="Ean Milligan (ean@milligan.dev)">
<meta name="designer" content="Ean Milligan (ean@milligan.dev)">
<meta name="publisher" content="Ean Milligan (ean@milligan.dev)">
<title>The Artificer Discord Bot API Tools</title>
<meta name="description" content="The Artificer Discord Bot is a advanced dice rolling bot utilizing the Roll20 format. The Artificer is fast and reliable and free to use.">
<meta name="keywords" content="The Artificer, Discord bot, dice roller, dice rolling, roll, rolling, dice, roller, bot, artificer, api, tools, api tools">
<meta name="robots" content="index, follow">
<meta name="revisit-after" content="7 days">
<meta name="distribution" content="web">
<meta name="robots" content="noodp, noydir">
<meta name="distribution" content="web">
<meta name="web_author" content="Ean Milligan (ean@milligan.dev)">
<link rel="shortcut icon" href="https://discord.burne99.com/TheArtificer/favicon.ico">
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Cinzel|Play">
<link rel="stylesheet" href="https://discord.burne99.com/TheArtificer/theme.css">
<link rel="stylesheet" href="./main.css">
</head>
<body>
<div id="page">
<div id="header">
<div id="header-left">
The Artificer API Tools
</div>
<div id="header-right">
<a href="https://discord.com/api/oauth2/authorize?client_id=789045930011656223&permissions=2048&scope=bot" target="_blank">Invite Me!</a>
<span>|</span>
<a href="https://discord.burne99.com/TheArtificer/" target="_blank">About</a>
</div>
</div>
<div id="page-contents">
<div id="slogan">
<h1>API Account Management</h1>
</div>
<div id="api-tools">
<div class="slug">
<p>This website will help you manage your API Key (or create one if you do not already have one). To get started, select an option from the dropdown below and enter the requested information. For more information, check out the <a href="https://github.com/Burn-E99/TheArtificer" target="_blank">GitHub Repository</a>.</p>
<hr/>
<p id="nojs">Javascript is required for this website to function. If you do not want to enable Javascript, you may access all of these endpoints from a third party tool (such as <a href="https://www.postman.com/" target="_blank">Postman</a>). Endpoints are fully documented on the <a href="https://github.com/Burn-E99/TheArtificer" target="_blank">GitHub Repository</a>.</p>
<div id="js" class="hidden">
<div class="field-group">
<label for="endpointDropdown">Select an option to manage:</label>
<select id="endpointDropdown" name="endpointDropdown" autocomplete="off">
<option value="none" selected disabled hidden>Choose an Option</option>
<optgroup label="Create/Delete API Key">
<option value="generate">Generate API Key</option>
<option value="delete">Delete API Key</option>
</optgroup>
<optgroup label="Discord Channel Management">
<option value="view">View Allowed Channels</option>
<option value="add">Add New Allowed Channel</option>
<option value="activate">Activate or Deactivate Channel</option>
</optgroup>
</select>
</div>
<br/>
<div id="fields" class="hidden">
<div id="api-field-group" class="field-group">
<label for="api-field">Your API Key:</label>
<input id="api-field" name="api-field" type="text" autocomplete="off" />
</div>
<div id="user-field-group" class="field-group">
<label for="user-field">Your Discord User ID:</label>
<input id="user-field" name="user-field" type="number" autocomplete="off" />
</div>
<div id="channel-field-group" class="field-group">
<label for="channel-field">Discord Channel ID:</label>
<input id="channel-field" name="channel-field" type="number" autocomplete="off" />
</div>
<div id="active-field-group" class="field-group">
<label for="active-field">Set Channel Status to:</label>
<select id="active-field" name="active-field" autocomplete="off">
<option value="activate" selected>Active</option>
<option value="deactivate">Inactive</option>
</select>
</div>
<div id="email-field-group" class="field-group">
<label for="email-field">Your Email Address:</label>
<input id="email-field" name="email-field" type="email" autocomplete="off" />
</div>
<div id="delete-field-group" class="field-group">
<label for="delete-field">Your Delete Code:</label>
<input id="delete-field" name="delete-field" type="text" placeholder="LEAVE THIS BLANK UNLESS YOU ALREADY HAVE ONE" autocomplete="off" />
</div>
<div id="submit-field-group" class="field-group">
<label for="submit-field"></label>
<input id="submit-field" name="submit-field" type="button" value="Submit" disabled />
</div>
</div>
<div id="results" class="hidden">
<hr />
HTTP Status: <span id="status"></span>
<br/>
HTTP Respose: <span id="body"></span>
<hr/>
<h3>Response Description</h3>
<div id="desc"></div>
</div>
</div>
</div>
</div>
<div id="final"></div>
</div>
<div id="footer">
<div id="footer-left">
Built by <a href="https://github.com/Burn-E99/" target="_blank">Ean Milligan</a>
</div>
<div id="footer-right">
Version 1.4.0
</div>
</div>
</div>
<script type="text/javascript" src="./main.js"></script>
</body>
</html>

145
www/api/main.css Normal file
View File

@ -0,0 +1,145 @@
body {
font-family: "Play", sans-serif;
padding: 0;
margin: 0;
overflow: hidden;
}
#page {
height: 100vh;
display: grid;
grid-template-columns: auto;
grid-template-rows: 3rem calc(100vh - 5rem) 2rem;
grid-template-areas: "header" "page-contents" "footer";
color: var(--page-font-color);
background-color: var(--page-bg-color);
}
#header {
grid-area: header;
display: grid;
grid-template-columns: 1fr 1fr;
grid-template-rows: auto;
grid-template-areas: "header-left header-right";
font-family: "Cinzel", serif;
font-size: 2rem;
line-height: 3rem;
font-weight: 500;
padding: 0 10px;
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
background-color: var(--header-bg-color);
color: var(--header-font-color);
border-bottom: 1px solid var(--header-font-color);
}
#header-left {
grid-area: header-left;
}
#header-right {
grid-area: header-right;
justify-self: end;
font-size: 1.75rem;
}
#footer {
grid-area: footer;
display: grid;
grid-template-columns: 1fr 1.5fr 1.5fr 1fr;
grid-template-rows: auto;
grid-template-areas: ". footer-left footer-right .";
line-height: 2rem;
height: 2rem;
background-color: var(--footer-bg-color);
}
#footer-left {
grid-area: footer-left;
}
#footer-right {
grid-area: footer-right;
justify-self: end;
}
#page-contents {
grid-area: page-contents;
padding: 0 20rem;
height: calc(100vh - 5rem);
display: grid;
grid-template-columns: auto;
grid-template-rows: fit-content(5rem) auto 1rem;
grid-template-areas: "slogan" "api-tools" "final";
overflow-y: auto;
}
#slogan {
grid-area: slogan;
}
#slogan h1 {
line-height: 2.5rem;
font-size: 2.5rem;
}
#api-tools {
grid-area: api-tools;
}
#final {
grid-area: final;
}
#fields {
display: grid;
grid-template-columns: auto;
grid-template-rows: auto;
}
.field-group {
padding-bottom: .5rem;
display: grid;
grid-template-columns: 20rem 20rem;
grid-template-rows: auto;
}
.field-group label {
padding-right: 1rem;
text-align: right;
}
.hidden {
display: none !important;
}
@media screen and (max-width: 1900px) {
#page-contents {
padding: 0 10rem;
}
}
@media screen and (max-width: 1400px) {
#page-contents {
padding: 0 5rem;
}
}
@media screen and (max-width: 1000px) {
#page-contents {
padding: 0 1rem;
}
}

193
www/api/main.js Normal file
View File

@ -0,0 +1,193 @@
// deno-lint-ignore-file
// Hide nojs notification and show tools
document.getElementById("nojs").className = "hidden";
document.getElementById("js").className = "";
var apiField = document.getElementById("api-field");
var userField = document.getElementById("user-field");
var channelField = document.getElementById("channel-field");
var emailField = document.getElementById("email-field");
var deleteField = document.getElementById("delete-field");
var submitField = document.getElementById("submit-field");
var endpoint = "none";
var status = "activate";
// Checks if all fields needed for the selected endpoint are valid
function validateFields() {
if (!(userField.value > 0 && userField.checkValidity())) {
submitField.disabled = true;
return;
}
switch (endpoint) {
case "generate":
if (!(emailField.value.length > 0 && emailField.checkValidity())) {
submitField.disabled = true;
return;
}
break;
case "delete":
if (!(apiField.value.length > 0 && apiField.checkValidity())) {
submitField.disabled = true;
return;
}
if (!(emailField.value.length > 0 && emailField.checkValidity())) {
submitField.disabled = true;
return;
}
break;
case "view":
if (!(apiField.value.length > 0 && apiField.checkValidity())) {
submitField.disabled = true;
return;
}
break;
case "add":
case "activate":
if (!(apiField.value.length > 0 && apiField.checkValidity())) {
submitField.disabled = true;
return;
}
if (!(channelField.value > 0 && channelField.checkValidity())) {
submitField.disabled = true;
return;
}
break;
default:
break;
}
submitField.disabled = false;
}
// Shows appropriate fields for selected endpoint
function showFields() {
document.getElementById("fields").className = "";
endpoint = this.value;
switch (endpoint) {
case "generate":
document.getElementById("api-field-group").className = "hidden";
document.getElementById("channel-field-group").className = "hidden";
document.getElementById("active-field-group").className = "hidden";
document.getElementById("email-field-group").className = "field-group";
document.getElementById("delete-field-group").className = "hidden";
break;
case "delete":
document.getElementById("api-field-group").className = "field-group";
document.getElementById("channel-field-group").className = "hidden";
document.getElementById("active-field-group").className = "hidden";
document.getElementById("email-field-group").className = "field-group";
document.getElementById("delete-field-group").className = "field-group";
break;
case "view":
document.getElementById("api-field-group").className = "field-group";
document.getElementById("channel-field-group").className = "hidden";
document.getElementById("active-field-group").className = "hidden";
document.getElementById("email-field-group").className = "hidden";
document.getElementById("delete-field-group").className = "hidden";
break;
case "add":
document.getElementById("api-field-group").className = "field-group";
document.getElementById("channel-field-group").className = "field-group";
document.getElementById("active-field-group").className = "hidden";
document.getElementById("email-field-group").className = "hidden";
document.getElementById("delete-field-group").className = "hidden";
break;
case "activate":
document.getElementById("api-field-group").className = "field-group";
document.getElementById("channel-field-group").className = "field-group";
document.getElementById("active-field-group").className = "field-group";
document.getElementById("email-field-group").className = "hidden";
document.getElementById("delete-field-group").className = "hidden";
break;
default:
break;
}
validateFields();
}
// Sets the status for channel activation/deactivation
function setStatus() {
status = this.value;
}
// Sends the request
function sendPayload() {
document.getElementById("results").className = "";
var xhr = new XMLHttpRequest();
var method;
var path = "/api/";
if (endpoint !== "generate") {
xhr.setRequestHeader("X-Api-Key", apiField.value);
}
switch (endpoint) {
case "generate":
method = "GET";
path += "key?user=" + userField.value + "&email=" + emailField.value;
break;
case "delete":
method = "DELETE";
path += "key?user=" + userField.value + "&email=" + emailField.value + (deleteField.value.length > 0 ? ("&code=" + deleteField.value) : "");
break;
case "view":
method = "GET";
path += "channel?user=" + userField.value;
break;
case "add":
method = "POST";
path += "channel/add?user=" + userField.value + "&channel=" + channelField.value;
break;
case "activate":
method = "PUT";
path += "channel/" + status + "?user=" + userField.value + "&channel=" + channelField.value;
break;
default:
return;
}
xhr.open(method, path);
xhr.send();
xhr.onload = function() {
document.getElementById("status").innerText = xhr.status;
document.getElementById("body").innerText = xhr.response;
switch (endpoint) {
case "generate":
document.getElementById("desc").innerHTML = "If you got a 200 OK, everything is set. Your API Key will be emailed to you within 24 hours.<br/>If you did not get a 200 OK, make sure all information entered is correct.";
break;
case "delete":
document.getElementById("desc").innerHTML = "If you got a 200 OK, everything is deleted.<br/>If you got a 424 Failed dependancy, this means you need a delete code before you can procede. Running this endpoint without the code provided will generate one and it will be emailed to you within 24 hours.<br/>If you did not get either of these, make sure all information entered is correct.";
break;
case "view":
document.getElementById("desc").innerHTML = "If you got a 200 OK, everything is set.<br/>If you did not get a 200 OK, make sure all information entered is correct.";
break;
case "add":
document.getElementById("desc").innerHTML = "If you got a 200 OK, everything is set.<br/>If you did not get a 200 OK, make sure all information entered is correct.";
break;
case "activate":
document.getElementById("desc").innerHTML = "If you got a 200 OK, everything is set.<br/>If you did not get a 200 OK, make sure all information entered is correct.";
break;
default:
document.getElementById("desc").innerHTML = "What? This shouldn't be possible";
break;
}
};
}
// Attach functions to html attributes
document.getElementById("endpointDropdown").addEventListener("change", showFields);
document.getElementById("active-field").addEventListener("change", setStatus);
apiField.addEventListener("input", validateFields);
userField.addEventListener("input", validateFields);
channelField.addEventListener("input", validateFields);
emailField.addEventListener("input", validateFields);
deleteField.addEventListener("input", validateFields);
submitField.addEventListener("click", sendPayload);

View File

Before

Width:  |  Height:  |  Size: 116 KiB

After

Width:  |  Height:  |  Size: 116 KiB

View File

Before

Width:  |  Height:  |  Size: 99 KiB

After

Width:  |  Height:  |  Size: 99 KiB

View File

@ -25,8 +25,6 @@
<link rel="stylesheet" href="./theme.css">
<link rel="stylesheet" href="./main.css">
<script type="text/javascript" src="./main.js"></script>
</head>
<body>
<div id="page">
@ -67,7 +65,7 @@
<p class="example"><code>[[d20]] formatting [[d6]]</code> - Rolls a d20 die and a d6 die and returns them with the word <code>formatting</code> between them</p>
<p class="example"><code>[[(32 * 12) + 5]]</code> - Multiplies 32 and 12, and adds 5 to that result</p>
<p class="example">And infinitely more possibilities. Any equations and/or rolls can be thrown at the bot and it will compute it quickly. Run the <code>[[rollhelp</code> command for full details of this command.</p>
<p class="example">Any number of rolls may be performed in the same requst, as long as they are each closed with <code>]]</code></p>
<p class="example">Any number of rolls may be performed in the same request, as long as they are each closed with <code>]]</code></p>
<br/>
<h3>Supporting Commands:</h3>
<p>The following commands are just some basic utility commands.</p>
@ -75,24 +73,28 @@
<p class="example"><code>[[stats</code> or <code>[[s</code> - Shows the stats on how many servers and users are using the bot</p>
<p class="example"><code>[[help</code> or <code>[[?</code> - Gives the full list of available commands</p>
<p class="example"><code>[[rollhelp</code> or <code>[[??</code> - Gives the full details on the roll command, explaining the <a href="https://roll20.zendesk.com/hc/en-us/articles/360037773133-Dice-Reference" target="_blank">Roll20 format</a></p>
<br/>
<h3>Full Documentation:</h3>
<p>Full Documentation can be found on the <a href="https://github.com/Burn-E99/TheArtificer" target="_blank">GitHub Repository</a>.</p>
</div>
</div>
<div id="api">
<h2>The Artificer API</h2>
<div class="slug">
<p>Trusted users will be provided with an API key that allows rolling of dice from third party programs. This API was developed to let DnD groups that use Excel to manage player sheets to roll dice directly from Excel to Discord. The API is very restricted and has a harsh rate limit to prevent spam.</p>
<p>There is a ZERO tolerance for API abuse. If abuse is detected or reported, the user will be banned with no chance to appeal. Currently, API keys are only given out to users known personally to the devs of this bot, but plans are in the works to open this up to more users using a form of registration.</p>
<p>If you happen to be a trusted user, information on the API endpoints can be found in the <a href="https://github.com/Burn-E99/TheArtificer" target="_blank">GitHub Repository</a>. Basic administration of your API key can be done using the API Tools linked at the top of this page.</p>
<p>This API was developed to let DnD groups that use Excel to manage player sheets to roll dice directly from Excel to Discord. The API is limited to rolling dice and managing your API Key and has a harsh rate limit to prevent spam.</p>
<p>There is a ZERO tolerance for API abuse. If abuse is detected or reported, the user will be banned with no chance to appeal.</p>
<p>If you would like to get an API Key, head on over to the <a href="https://artificer.eanm.dev/" target="_blank">API Tools</a> page linked at the top of this page. Basic administration of your API key can also be done via the <a href="https://artificer.eanm.dev/" target="_blank">API Tools</a>.</p>
<p>Once you have your API Key, detailed information on the API endpoints can be found in the <a href="https://github.com/Burn-E99/TheArtificer" target="_blank">GitHub Repository</a>.</p>
</div>
</div>
<div id="final">$nbsp;</div>
<div id="final"></div>
</div>
<div id="footer">
<div id="footer-left">
Built by <a href="https://github.com/Burn-E99/" target="_blank">Ean Milligan</a>
</div>
<div id="footer-right">
Version 1.3.3
Version 1.4.0
</div>
</div>
</div>

View File