Sonar Cleanup - Phase 1

This commit is contained in:
Ean Milligan (Bastion) 2022-05-22 15:29:59 -04:00
parent 46d6014ed5
commit 891a36a9ba
10 changed files with 59 additions and 52 deletions

View File

@ -11,9 +11,9 @@ console.log('Inesrtion done');
console.log('Attempting to insert default commands into command_cnt'); console.log('Attempting to insert default commands into command_cnt');
const commands = ['ping', 'rip', 'rollhelp', 'help', 'info', 'version', 'report', 'stats', 'roll', 'emojis', 'api', 'privacy', 'mention']; const commands = ['ping', 'rip', 'rollhelp', 'help', 'info', 'version', 'report', 'stats', 'roll', 'emojis', 'api', 'privacy', 'mention'];
for (let i = 0; i < commands.length; i++) { for (const command of commands) {
await dbClient.execute('INSERT INTO command_cnt(command) values(?)', [commands[i]]).catch((e) => { await dbClient.execute('INSERT INTO command_cnt(command) values(?)', [command]).catch((e) => {
console.log(`Failed to insert into database`, e); console.log(`Failed to insert ${command} into database`, e);
}); });
} }
console.log('Insertion done'); console.log('Insertion done');

2
mod.ts
View File

@ -100,7 +100,7 @@ startBot({
log(LT.ERROR, `Failed to send message: ${JSON.stringify(e)}`); log(LT.ERROR, `Failed to send message: ${JSON.stringify(e)}`);
}); });
}, },
debug: DEVMODE ? (dmsg) => log(LT.LOG, `Debug Message | ${JSON.stringify(dmsg)}`) : () => {}, debug: DEVMODE ? (dmsg) => log(LT.LOG, `Debug Message | ${JSON.stringify(dmsg)}`) : undefined,
messageCreate: (message: DiscordenoMessage) => { messageCreate: (message: DiscordenoMessage) => {
// Ignore all other bots // Ignore all other bots
if (message.isBot) return; if (message.isBot) return;

View File

@ -11,29 +11,29 @@ import { EmojiConf } from '../mod.d.ts';
const allEmojiAliases: string[] = []; const allEmojiAliases: string[] = [];
config.emojis.forEach((emoji: EmojiConf) => { config.emojis.forEach((emji: EmojiConf) => {
allEmojiAliases.push(...emoji.aliases); allEmojiAliases.push(...emji.aliases);
}); });
export const emoji = (message: DiscordenoMessage, command: string) => { export const emoji = (message: DiscordenoMessage, command: string) => {
// shortcut // shortcut
if (allEmojiAliases.indexOf(command)) { if (allEmojiAliases.indexOf(command)) {
// Start looping thru the possible emojis // Start looping thru the possible emojis
config.emojis.some((emoji: EmojiConf) => { config.emojis.some((emji: EmojiConf) => {
log(LT.LOG, `Checking if command was emoji ${JSON.stringify(emoji)}`); log(LT.LOG, `Checking if command was emoji ${JSON.stringify(emji)}`);
// If a match gets found // If a match gets found
if (emoji.aliases.indexOf(command || '') > -1) { if (emji.aliases.indexOf(command || '') > -1) {
// Light telemetry to see how many times a command is being run // Light telemetry to see how many times a command is being run
dbClient.execute(`CALL INC_CNT("emojis");`).catch((e) => { dbClient.execute(`CALL INC_CNT("emojis");`).catch((e) => {
log(LT.ERROR, `Failed to call stored procedure INC_CNT: ${JSON.stringify(e)}`); log(LT.ERROR, `Failed to call stored procedure INC_CNT: ${JSON.stringify(e)}`);
}); });
// Send the needed emoji1 // Send the needed emoji
message.send(`<${emoji.animated ? 'a' : ''}:${emoji.name}:${emoji.id}>`).catch((e) => { message.send(`<${emji.animated ? 'a' : ''}:${emji.name}:${emji.id}>`).catch((e) => {
log(LT.ERROR, `Failed to send message: ${JSON.stringify(message)} | ${JSON.stringify(e)}`); log(LT.ERROR, `Failed to send message: ${JSON.stringify(message)} | ${JSON.stringify(e)}`);
}); });
// And attempt to delete if needed // And attempt to delete if needed
if (emoji.deleteSender) { if (emji.deleteSender) {
message.delete().catch((e) => { message.delete().catch((e) => {
log(LT.WARN, `Failed to delete message: ${JSON.stringify(message)} | ${JSON.stringify(e)}`); log(LT.WARN, `Failed to delete message: ${JSON.stringify(message)} | ${JSON.stringify(e)}`);
}); });

View File

@ -530,13 +530,16 @@ export const generateApiFailed = (args: string) => ({
}], }],
}); });
export const generateApiStatus = (banned: boolean, active: boolean) => ({ export const generateApiStatus = (banned: boolean, active: boolean) => {
embeds: [{ const apiStatus = active ? 'allowed' : 'blocked from being used';
color: infoColor1, return {
title: `The Artificer's API is ${config.api.enable ? 'currently enabled' : 'currently disabled'}.`, embeds: [{
description: banned ? 'API rolls are banned from being used in this guild.\n\nThis will not be reversed.' : `API rolls are ${active ? 'allowed' : 'blocked from being used'} in this guild.`, color: infoColor1,
}], title: `The Artificer's API is ${config.api.enable ? 'currently enabled' : 'currently disabled'}.`,
}); description: banned ? 'API rolls are banned from being used in this guild.\n\nThis will not be reversed.' : `API rolls are ${apiStatus} in this guild.`,
}]
};
};
export const generateApiSuccess = (args: string) => ({ export const generateApiSuccess = (args: string) => ({
embeds: [{ embeds: [{

View File

@ -32,10 +32,10 @@ export const parseRoll = (fullCmd: string, modifiers: RollModifiers): SolvedRoll
const tempReturnData: ReturnData[] = []; const tempReturnData: ReturnData[] = [];
// Loop thru all roll/math ops // Loop thru all roll/math ops
for (let i = 0; i < sepRolls.length; i++) { for (const sepRoll of sepRolls) {
log(LT.LOG, `Parsing roll ${fullCmd} | Working ${sepRolls[i]}`); log(LT.LOG, `Parsing roll ${fullCmd} | Working ${sepRoll}`);
// Split the current iteration on the command postfix to separate the operation to be parsed and the text formatting after the opertaion // Split the current iteration on the command postfix to separate the operation to be parsed and the text formatting after the opertaion
const [tempConf, tempFormat] = sepRolls[i].split(config.postfix); const [tempConf, tempFormat] = sepRoll.split(config.postfix);
// Remove all spaces from the operation config and split it by any operator (keeping the operator in mathConf for fullSolver to do math on) // Remove all spaces from the operation config and split it by any operator (keeping the operator in mathConf for fullSolver to do math on)
const mathConf: (string | number | SolvedStep)[] = <(string | number | SolvedStep)[]> tempConf.replace(/ /g, '').split(/([-+()*/%^])/g); const mathConf: (string | number | SolvedStep)[] = <(string | number | SolvedStep)[]> tempConf.replace(/ /g, '').split(/([-+()*/%^])/g);

View File

@ -13,9 +13,13 @@ export const MAXLOOPS = 5000000;
// genRoll(size) returns number // genRoll(size) returns number
// genRoll rolls a die of size size and returns the result // genRoll rolls a die of size size and returns the result
export const genRoll = (size: number): number => { export const genRoll = (size: number, maximiseRoll: boolean, nominalRoll: boolean): number => {
// Math.random * size will return a decimal number between 0 and size (excluding size), so add 1 and floor the result to not get 0 as a result if (maximiseRoll) {
return Math.floor((Math.random() * size) + 1); return size;
} else {
// Math.random * size will return a decimal number between 0 and size (excluding size), so add 1 and floor the result to not get 0 as a result
return nominalRoll ? ((size / 2) + 0.5) : Math.floor((Math.random() * size) + 1);
}
}; };
// compareRolls(a, b) returns -1|0|1 // compareRolls(a, b) returns -1|0|1
@ -58,11 +62,11 @@ export const compareOrigidx = (a: RollSet, b: RollSet): number => {
// escapeCharacters escapes all characters listed in esc // escapeCharacters escapes all characters listed in esc
export const escapeCharacters = (str: string, esc: string): string => { export const escapeCharacters = (str: string, esc: string): string => {
// Loop thru each esc char one at a time // Loop thru each esc char one at a time
for (let i = 0; i < esc.length; i++) { for (const e of esc) {
log(LT.LOG, `Escaping character ${esc[i]} | ${str}, ${esc}`); log(LT.LOG, `Escaping character ${e} | ${str}, ${esc}`);
// Create a new regex to look for that char that needs replaced and escape it // Create a new regex to look for that char that needs replaced and escape it
const temprgx = new RegExp(`[${esc[i]}]`, 'g'); const temprgx = new RegExp(`[${e}]`, 'g');
str = str.replace(temprgx, `\\${esc[i]}`); str = str.replace(temprgx, `\\${e}`);
} }
return str; return str;
}; };

View File

@ -349,7 +349,7 @@ export const roll = (rollStr: string, maximiseRoll: boolean, nominalRoll: boolea
// Copy the template to fill out for this iteration // Copy the template to fill out for this iteration
const rolling = JSON.parse(JSON.stringify(templateRoll)); const rolling = JSON.parse(JSON.stringify(templateRoll));
// If maximiseRoll is on, set the roll to the dieSize, else if nominalRoll is on, set the roll to the average roll of dieSize, else generate a new random roll // If maximiseRoll is on, set the roll to the dieSize, else if nominalRoll is on, set the roll to the average roll of dieSize, else generate a new random roll
rolling.roll = maximiseRoll ? rollConf.dieSize : (nominalRoll ? ((rollConf.dieSize / 2) + 0.5) : genRoll(rollConf.dieSize)); rolling.roll = genRoll(rollConf.dieSize, maximiseRoll, nominalRoll);
// Set origidx of roll // Set origidx of roll
rolling.origidx = i; rolling.origidx = i;
@ -388,7 +388,7 @@ export const roll = (rollStr: string, maximiseRoll: boolean, nominalRoll: boolea
// Copy the template to fill out for this iteration // Copy the template to fill out for this iteration
const newRoll = JSON.parse(JSON.stringify(templateRoll)); const newRoll = JSON.parse(JSON.stringify(templateRoll));
// If maximiseRoll is on, set the roll to the dieSize, else if nominalRoll is on, set the roll to the average roll of dieSize, else generate a new random roll // If maximiseRoll is on, set the roll to the dieSize, else if nominalRoll is on, set the roll to the average roll of dieSize, else generate a new random roll
newRoll.roll = maximiseRoll ? rollConf.dieSize : (nominalRoll ? ((rollConf.dieSize / 2) + 0.5) : genRoll(rollConf.dieSize)); newRoll.roll = genRoll(rollConf.dieSize, maximiseRoll, nominalRoll);
// If critScore arg is on, check if the roll should be a crit, if its off, check if the roll matches the die size // If critScore arg is on, check if the roll should be a crit, if its off, check if the roll matches the die size
if (rollConf.critScore.on && rollConf.critScore.range.indexOf(newRoll.roll) >= 0) { if (rollConf.critScore.on && rollConf.critScore.range.indexOf(newRoll.roll) >= 0) {
@ -415,7 +415,7 @@ export const roll = (rollStr: string, maximiseRoll: boolean, nominalRoll: boolea
// Copy the template to fill out for this iteration // Copy the template to fill out for this iteration
const newRoll = JSON.parse(JSON.stringify(templateRoll)); const newRoll = JSON.parse(JSON.stringify(templateRoll));
// If maximiseRoll is on, set the roll to the dieSize, else if nominalRoll is on, set the roll to the average roll of dieSize, else generate a new random roll // If maximiseRoll is on, set the roll to the dieSize, else if nominalRoll is on, set the roll to the average roll of dieSize, else generate a new random roll
newRoll.roll = maximiseRoll ? rollConf.dieSize : (nominalRoll ? ((rollConf.dieSize / 2) + 0.5) : genRoll(rollConf.dieSize)); newRoll.roll = genRoll(rollConf.dieSize, maximiseRoll, nominalRoll);
// Always mark this roll as exploding // Always mark this roll as exploding
newRoll.exploding = true; newRoll.exploding = true;

View File

@ -1,5 +1,5 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html lang="en">
<head> <head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/> <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="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0" />
@ -33,9 +33,9 @@
The Artificer API Tools The Artificer API Tools
</div> </div>
<div id="header-right"> <div id="header-right">
<a href="https://discord.com/api/oauth2/authorize?client_id=789045930011656223&permissions=2048&scope=bot" target="_blank">Invite Me!</a> <a href="https://discord.com/api/oauth2/authorize?client_id=789045930011656223&permissions=2048&scope=bot" target="_blank" rel="noopener">Invite Me!</a>
<span>|</span> <span>|</span>
<a href="https://discord.burne99.com/TheArtificer/" target="_blank">About</a> <a href="https://discord.burne99.com/TheArtificer/" target="_blank" rel="noopener">About</a>
</div> </div>
</div> </div>
<div id="page-contents"> <div id="page-contents">
@ -44,11 +44,11 @@
</div> </div>
<div id="api-tools"> <div id="api-tools">
<div class="slug"> <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> <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" rel="noopener">GitHub Repository</a>.</p>
<hr/> <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> <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" rel="noopener">Postman</a>). Endpoints are fully documented on the <a href="https://github.com/Burn-E99/TheArtificer" target="_blank" rel="noopener">GitHub Repository</a>.</p>
<div id="js" class="hidden"> <div id="js" class="hidden">
<div class="field-group"> <div class="field-group">
@ -119,7 +119,7 @@
</div> </div>
<div id="footer"> <div id="footer">
<div id="footer-left"> <div id="footer-left">
Built by <a href="https://github.com/Burn-E99/" target="_blank">Ean Milligan</a> Built by <a href="https://github.com/Burn-E99/" target="_blank" rel="noopener">Ean Milligan</a>
</div> </div>
<div id="footer-right"> <div id="footer-right">
Version 2.0.0 Version 2.0.0

View File

@ -11,7 +11,7 @@ var deleteField = document.getElementById("delete-field");
var submitField = document.getElementById("submit-field"); var submitField = document.getElementById("submit-field");
var endpoint = "none"; var endpoint = "none";
var status = "activate"; var apiStatus = "activate";
// Checks if all fields needed for the selected endpoint are valid // Checks if all fields needed for the selected endpoint are valid
function validateFields() { function validateFields() {
@ -111,7 +111,7 @@ function showFields() {
// Sets the status for channel activation/deactivation // Sets the status for channel activation/deactivation
function setStatus() { function setStatus() {
status = this.value; apiStatus = this.value;
} }
// Sends the request // Sends the request
@ -141,7 +141,7 @@ function sendPayload() {
break; break;
case "activate": case "activate":
method = "PUT"; method = "PUT";
path += "channel/" + status + "?user=" + userField.value + "&channel=" + channelField.value; path += "channel/" + apiStatus + "?user=" + userField.value + "&channel=" + channelField.value;
break; break;
default: default:
return; return;

View File

@ -1,5 +1,5 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html lang="en">
<head> <head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/> <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="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0" />
@ -33,9 +33,9 @@
The Artificer The Artificer
</div> </div>
<div id="header-right"> <div id="header-right">
<a href="https://discord.com/api/oauth2/authorize?client_id=789045930011656223&permissions=2048&scope=bot" target="_blank">Invite Me!</a> <a href="https://discord.com/api/oauth2/authorize?client_id=789045930011656223&permissions=2048&scope=bot" target="_blank" rel="noopener">Invite Me!</a>
<span>|</span> <span>|</span>
<a href="https://artificer.eanm.dev/" target="_blank">API Tools</a> <a href="https://artificer.eanm.dev/" target="_blank" rel="noopener">API Tools</a>
</div> </div>
</div> </div>
<div id="page-contents"> <div id="page-contents">
@ -47,16 +47,16 @@
<img src="./img/TheArtificer.png" alt="The Artificer Logo" /> <img src="./img/TheArtificer.png" alt="The Artificer Logo" />
</div> </div>
<div id="description"> <div id="description">
<p>The Artificer is an open source Discord bot that specializes in rolling dice. The bot utilizes the compact <a href="https://artificer.eanm.dev/roll20" target="_blank">Roll20 formatting</a> for ease of use and will correctly perform any needed math on the roll (limited to basic algebra). Feel free to join the support server linked below if you would like to try The Artificer out!</p> <p>The Artificer is an open source Discord bot that specializes in rolling dice. The bot utilizes the compact <a href="https://artificer.eanm.dev/roll20" target="_blank" rel="noopener">Roll20 formatting</a> for ease of use and will correctly perform any needed math on the roll (limited to basic algebra). Feel free to join the support server linked below if you would like to try The Artificer out!</p>
<p>This bot was developed to replace the Sidekick discord bot after it went offline many times for extended periods. This was also developed to fix some annoyances that were found with Sidekick, specifically its vague error messages (such as <code>"Tarantallegra!"</code>, what is that supposed to mean) and its inability to handle implicit multiplication (such as <code>4(12 + 20)</code>).</p> <p>This bot was developed to replace the Sidekick discord bot after it went offline many times for extended periods. This was also developed to fix some annoyances that were found with Sidekick, specifically its vague error messages (such as <code>"Tarantallegra!"</code>, what is that supposed to mean) and its inability to handle implicit multiplication (such as <code>4(12 + 20)</code>).</p>
<p><a href="https://discord.com/api/oauth2/authorize?client_id=789045930011656223&permissions=2048&scope=bot" target="_blank">Invite The Artificer to Your Server</a> | <a href="https://discord.gg/peHASXMZYv" target="_blank">Support Server</a> | <a href="https://github.com/Burn-E99/TheArtificer" target="_blank">GitHub Repository</a></p> <p><a href="https://discord.com/api/oauth2/authorize?client_id=789045930011656223&permissions=2048&scope=bot" target="_blank" rel="noopener">Invite The Artificer to Your Server</a> | <a href="https://discord.gg/peHASXMZYv" target="_blank" rel="noopener">Support Server</a> | <a href="https://github.com/Burn-E99/TheArtificer" target="_blank" rel="noopener">GitHub Repository</a></p>
</div> </div>
</div> </div>
<div id="examples"> <div id="examples">
<h2>Available Commands:</h2> <h2>Available Commands:</h2>
<div class="slug"> <div class="slug">
<h3>Rolling Command:</h3> <h3>Rolling Command:</h3>
<p>This command is what the bot is all about. Using the <a href="https://artificer.eanm.dev/roll20" target="_blank">Roll20 format</a>, any form of dice roll can be performed, with any needed math calculated into the results. This command can even be used as a fairly advanced calculator, supporting parenthesis, exponentials, multiplication, division, modulus, addition, and subtraction.</p> <p>This command is what the bot is all about. Using the <a href="https://artificer.eanm.dev/roll20" target="_blank" rel="noopener">Roll20 format</a>, any form of dice roll can be performed, with any needed math calculated into the results. This command can even be used as a fairly advanced calculator, supporting parenthesis, exponentials, multiplication, division, modulus, addition, and subtraction.</p>
<h4 class="example">Examples:</h4> <h4 class="example">Examples:</h4>
<p class="example"><code>[[d20]]</code> - Rolls a simple d20 without anything fancy</p> <p class="example"><code>[[d20]]</code> - Rolls a simple d20 without anything fancy</p>
<p class="example"><code>[[4d20r1!]]</code> - Rolls 4 d20 dice, rerolling any dice that land on 1, and repeatedly rolling a new d20 for any critical success rolled</p> <p class="example"><code>[[4d20r1!]]</code> - Rolls 4 d20 dice, rerolling any dice that land on 1, and repeatedly rolling a new d20 for any critical success rolled</p>
@ -72,10 +72,10 @@
<h4 class="example">Examples:</h4> <h4 class="example">Examples:</h4>
<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>[[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>[[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://artificer.eanm.dev/roll20" target="_blank">Roll20 format</a></p> <p class="example"><code>[[rollhelp</code> or <code>[[??</code> - Gives the full details on the roll command, explaining the <a href="https://artificer.eanm.dev/roll20" target="_blank" rel="noopener">Roll20 format</a></p>
<br/> <br/>
<h3>Full Documentation:</h3> <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> <p>Full Documentation can be found on the <a href="https://github.com/Burn-E99/TheArtificer" target="_blank"rel="noopener">GitHub Repository</a>.</p>
</div> </div>
</div> </div>
<div id="api"> <div id="api">
@ -83,15 +83,15 @@
<div class="slug"> <div class="slug">
<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>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>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>If you would like to get an API Key, head on over to the <a href="https://artificer.eanm.dev/" target="_blank" rel="noopener">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" rel="noopener">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> <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" rel="noopener">GitHub Repository</a>.</p>
</div> </div>
</div> </div>
<div id="final"></div> <div id="final"></div>
</div> </div>
<div id="footer"> <div id="footer">
<div id="footer-left"> <div id="footer-left">
Built by <a href="https://github.com/Burn-E99/" target="_blank">Ean Milligan</a> Built by <a href="https://github.com/Burn-E99/" target="_blank" rel="noopener">Ean Milligan</a>
</div> </div>
<div id="footer-right"> <div id="footer-right">
Version 2.0.0 Version 2.0.0