Compare commits

..

No commits in common. "master" and "V2.0.0" have entirely different histories.

22 changed files with 41 additions and 188 deletions

View File

@ -39,7 +39,7 @@ If you would like to remove all of your submitted data, this can easily be done
If you have been banned from using _The API_, your API Key, and registration information (Discord User ID, and Email Address) will not be deleted as this data is considered necessary.
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_. Additionally, _The Bot_ will automatically remove any data related to your Discord Guild when _The Bot_ is removed from your guild.
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_.
If your guild has been banned from using _The API_, the Discord Guild ID will not be deleted as this data is considered necessary.

View File

@ -1,4 +1,4 @@
# The Artificer - A Dice Rolling Discord Bot | V2.1.2 - 2022/07/31
# The Artificer - A Dice Rolling Discord Bot | V2.0.0 - 2022/07/05
[![SonarCloud](https://sonarcloud.io/images/project_badges/sonarcloud-orange.svg)](https://sonarcloud.io/summary/new_code?id=TheArtificer)
[![Maintainability Rating](https://sonarcloud.io/api/project_badges/measure?project=TheArtificer&metric=sqale_rating)](https://sonarcloud.io/summary/new_code?id=TheArtificer) [![Security Rating](https://sonarcloud.io/api/project_badges/measure?project=TheArtificer&metric=security_rating)](https://sonarcloud.io/summary/new_code?id=TheArtificer) [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=TheArtificer&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=TheArtificer) [![Bugs](https://sonarcloud.io/api/project_badges/measure?project=TheArtificer&metric=bugs)](https://sonarcloud.io/summary/new_code?id=TheArtificer) [![Duplicated Lines (%)](https://sonarcloud.io/api/project_badges/measure?project=TheArtificer&metric=duplicated_lines_density)](https://sonarcloud.io/summary/new_code?id=TheArtificer) [![Lines of Code](https://sonarcloud.io/api/project_badges/measure?project=TheArtificer&metric=ncloc)](https://sonarcloud.io/summary/new_code?id=TheArtificer)
@ -59,10 +59,6 @@ The Artificer comes with a few supplemental commands to the main rolling command
* If you encounter a command that errors out or returns something unexpected, please use this command to alert the developers of the problem.
* Example:
* `[[report [[2+2]] returned 5 when I expected it to return 4` will send the entire message after `[[report` to the devs via Discord.
* `[[opt-out` or `[[ignore-me`
* Adds you to an ignore list so the bot will never respond to you
* `[[opt-in` **Available via DM ONLY**
* Removes you from the ignore list
* `[[xdydzracsq!]]`
* This is the command the bot was built specifically for.
* It looks a little complicated at first, but if you are familiar with the [Roll20 formatting](https://artificer.eanm.dev/roll20), this will no different.
@ -220,7 +216,7 @@ The Artificer is built on [Deno](https://deno.land/) `v1.22.0` using [Discordeno
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 the command in `start.command`.
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 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

@ -10,12 +10,11 @@ pidfile="/var/dbots/TheArtificer/artificer.pid"
artificer_root="/var/dbots/TheArtificer"
artificer_write="./logs/,./src/endpoints/gets/heatmap.png"
artificer_read="./src/solver/,./src/endpoints/gets/heatmap-base.png,./src/endpoints/gets/heatmap.png,./config.ts,./deps.ts,./src/mod.d.ts"
artificer_log="/var/log/artificer.log"
artificer_read="./src/solver/,./src/endpoints/gets/heatmap-base.png,./src/endpoints/gets/heatmap.png"
artificer_chdir="${artificer_root}"
command="/usr/sbin/daemon"
command_args="-f -R 5 -P ${pidfile} -o ${artificer_log} /usr/local/bin/deno run --allow-write=${artificer_write} --allow-read=${artificer_read} --allow-net ${artificer_root}/mod.ts"
command_args="-c -f -P ${pidfile} /usr/local/bin/deno run --allow-write=${artificer_write} --allow-read=${artificer_read} --allow-net ${artificer_root}/mod.ts"
load_rc_config artificer
run_rc_command "$1"

View File

@ -6,7 +6,7 @@ After=network.target
[Service]
Type=simple
PIDFile=/run/deno.pid
ExecStart=/root/.deno/bin/deno run --allow-write=./logs/,./src/endpoints/gets/heatmap.png --allow-read=./src/solver/,./src/endpoints/gets/heatmap-base.png,./config.ts,./deps.ts,./src/mod.d.ts --allow-net .\mod.ts
ExecStart=/root/.deno/bin/deno run --allow-write=./logs/,./src/endpoints/gets/heatmap.png --allow-read=./src/solver/,./src/endpoints/gets/heatmap-base.png --allow-net .\mod.ts
RestartSec=60
Restart=on-failure

View File

@ -1,12 +1,12 @@
export const config = {
'name': 'The Artificer', // Name of the bot
'version': '2.1.3', // Version of the bot
'version': '2.0.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
'limits': { // Limits for the bot functions
'maxLoops': 5000000, // Determines how long the bot will attempt a roll, number of loops before it kills a roll. Increase this at your own risk, originally was set to 5 Million before rollWorkers were added, increased to 10 Million since multiple rolls can be handled concurrently
'maxLoops': 10000000, // Determines how long the bot will attempt a roll, number of loops before it kills a roll. Increase this at your own risk, originally was set to 5 Million before rollWorkers were added, increased to 10 Million since multiple rolls can be handled concurrently
'maxWorkers': 16, // Maximum number of worker threads to spawn at once (Set this to less than the number of threads your CPU has, Artificer will eat it all if too many rolls happen at once)
'workerTimeout': 300000, // Maximum time before the bot kills a worker thread in ms
},

View File

@ -19,20 +19,8 @@ await dbClient.execute(`DROP PROCEDURE IF EXISTS INC_HEATMAP;`);
await dbClient.execute(`DROP TABLE IF EXISTS roll_time_heatmap;`);
await dbClient.execute(`DROP PROCEDURE IF EXISTS INC_CNT;`);
await dbClient.execute(`DROP TABLE IF EXISTS command_cnt;`);
await dbClient.execute(`DROP TABLE IF EXISTS ignore_list;`);
console.log('Tables dropped');
// Table to hold list of users who want to be ignored by the bot
console.log('Attempting to create table ignore_list');
await dbClient.execute(`
CREATE TABLE ignore_list (
userid bigint unsigned NOT NULL,
PRIMARY KEY (userid),
UNIQUE KEY ignore_list_userid_UNIQUE (userid)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
`);
console.log('Table created');
// Light telemetry on how many commands have been run
console.log('Attempting to create table command_cnt');
await dbClient.execute(`

View File

@ -10,7 +10,7 @@ await dbClient.execute('INSERT INTO all_keys(userid,apiKey) values(?,?)', [confi
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', 'mention', 'audit', 'heatmap', 'rollDecorators', 'opt-out', 'opt-in'];
const commands = ['ping', 'rip', 'rollhelp', 'help', 'info', 'version', 'report', 'stats', 'roll', 'emojis', 'api', 'privacy', 'mention', 'audit', 'heatmap', 'rollDecorators'];
for (const command of commands) {
await dbClient.execute('INSERT INTO command_cnt(command) values(?)', [command]).catch((e) => {
console.log(`Failed to insert ${command} into database`, e);

20
mod.ts
View File

@ -25,7 +25,7 @@ import {
startBot,
} from './deps.ts';
import api from './src/api.ts';
import { dbClient, ignoreList } from './src/db.ts';
import { dbClient } from './src/db.ts';
import commands from './src/commands/_index.ts';
import intervals from './src/intervals.ts';
import { successColor, warnColor } from './src/commandUtils.ts';
@ -71,7 +71,7 @@ startBot({
// Interval to update bot list stats every 24 hours
LOCALMODE ? log(LT.INFO, 'updateListStatistics not running') : setInterval(() => {
log(LT.LOG, 'Updating all bot lists statistics');
intervals.updateListStatistics(botId, cache.guilds.size + cache.dispatchedGuildIds.size);
intervals.updateListStatistics(botId, cache.guilds.size);
}, 86400000);
// Interval to update hourlyRates every hour
@ -89,7 +89,7 @@ startBot({
// setTimeout added to make sure the startup message does not error out
setTimeout(() => {
LOCALMODE && editBotNickname(config.devServer, `LOCAL - ${config.name}`);
LOCALMODE ? log(LT.INFO, 'updateListStatistics not running') : intervals.updateListStatistics(botId, cache.guilds.size + cache.dispatchedGuildIds.size);
LOCALMODE ? log(LT.INFO, 'updateListStatistics not running') : intervals.updateListStatistics(botId, cache.guilds.size);
intervals.updateHourlyRates();
intervals.updateHeatmapPng();
editBotStatus({
@ -173,9 +173,6 @@ startBot({
// Ignore all other bots
if (message.isBot) return;
// Ignore users who requested to be ignored
if (ignoreList.includes(message.authorId) && (!message.content.startsWith(`${config.prefix}opt-in`) || message.guildId !== 0n)) return;
// Ignore all messages that are not commands
if (message.content.indexOf(config.prefix) !== 0) {
// Handle @bot messages
@ -196,17 +193,6 @@ startBot({
// All commands below here
switch (command) {
case 'opt-out':
case 'ignore-me':
// [[opt-out or [[ignore-me
// Tells the bot to add you to the ignore list.
commands.optOut(message);
break;
case 'opt-in':
// [[opt-in
// Tells the bot to remove you from the ignore list.
commands.optIn(message);
break;
case 'ping':
// [[ping
// Its a ping test, what else do you want.

View File

@ -14,8 +14,6 @@ import { roll } from './roll.ts';
import { handleMentions } from './handleMentions.ts';
import { audit } from './audit.ts';
import { heatmap } from './heatmap.ts';
import { optOut } from './optOut.ts';
import { optIn } from './optIn.ts';
export default {
ping,
@ -34,6 +32,4 @@ export default {
handleMentions,
audit,
heatmap,
optOut,
optIn,
};

View File

@ -79,11 +79,6 @@ Please see attached file for audit details on cached guilds and members.`,
value: `${botsCount}`,
inline: true,
},
{
name: 'Average members per guild:',
value: `${(totalCount / cache.guilds.size).toFixed(2)}`,
inline: true,
},
],
}],
file: {

View File

@ -21,10 +21,8 @@ export const handleMentions = (message: DiscordenoMessage) => {
color: infoColor1,
title: `Hello! I am ${config.name}!`,
fields: [{
name: 'I am a bot that specializes in rolling dice and doing basic algebra.',
value: `To learn about my available commands, please run \`${config.prefix}help\`.
Want me to ignore you? Simply run \`${config.prefix}opt-out\` and ${config.name} will no longer read your messages or respond to you.`,
name: 'I am a bot that specializes in rolling dice and doing basic algebra',
value: `To learn about my available commands, please run \`${config.prefix}help\``,
}],
}],
}).catch((e: Error) => utils.commonLoggers.messageSendError('handleMentions.ts:30', message, e));

View File

@ -76,16 +76,6 @@ export const help = (message: DiscordenoMessage) => {
value: 'Heatmap of when the roll command is run the most',
inline: true,
},
{
name: `\`${config.prefix}opt-out\` or \`${config.prefix}ignore-me\``,
value: 'Adds you to an ignore list so the bot will never respond to you',
inline: true,
},
{
name: `\`${config.prefix}opt-in\` **Available via DM ONLY**`,
value: 'Removes you from the ignore list',
inline: true,
},
{
name: `\`${config.prefix}xdydzracsq!${config.postfix}\` ...`,
value:

View File

@ -1,4 +1,3 @@
import config from '../../config.ts';
import { dbClient, queries } from '../db.ts';
import {
// Discordeno deps
@ -9,16 +8,16 @@ import utils from '../utils.ts';
export const info = (message: DiscordenoMessage) => {
// Light telemetry to see how many times a command is being run
dbClient.execute(queries.callIncCnt('info')).catch((e) => utils.commonLoggers.dbError('info.ts:12', 'call sproc INC_CNT on', e));
dbClient.execute(queries.callIncCnt('info')).catch((e) => utils.commonLoggers.dbError('info.ts:14', 'call sproc INC_CNT on', e));
message.send({
embeds: [{
color: infoColor2,
title: `${config.name}, a Discord bot that specializing in rolling dice and calculating math`,
description: `${config.name} is developed by Ean AKA Burn_E99.
title: 'The Artificer, a Discord bot that specializing in rolling dice and calculating math',
description: `The Artificer is developed by Ean AKA Burn_E99.
Additional information can be found on my website [here](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).`,
}],
}).catch((e: Error) => utils.commonLoggers.messageSendError('info.ts:23', message, e));
}).catch((e: Error) => utils.commonLoggers.messageSendError('info.ts:27', message, e));
};

View File

@ -1,39 +0,0 @@
import config from '../../config.ts';
import { dbClient, ignoreList, queries } from '../db.ts';
import {
// Discordeno deps
DiscordenoMessage,
} from '../../deps.ts';
import { failColor, successColor } from '../commandUtils.ts';
import utils from '../utils.ts';
export const optIn = async (message: DiscordenoMessage) => {
// Light telemetry to see how many times a command is being run
dbClient.execute(queries.callIncCnt('opt-out')).catch((e) => utils.commonLoggers.dbError('optIn.ts:11', 'call sproc INC_CNT on', e));
const idIdx = ignoreList.indexOf(message.authorId);
if (idIdx !== -1) {
try {
ignoreList.splice(idIdx, 1);
await dbClient.execute('DELETE FROM ignore_list WHERE userid = ?', [message.authorId]);
message.reply({
embeds: [{
color: successColor,
title: `${config.name} will now respond to you again.`,
description: `If you want ${config.name} to ignore to you again, please run the following command:
\`${config.prefix}opt-out\``,
}],
}).catch((e: Error) => utils.commonLoggers.messageSendError('optIn.ts:27', message, e));
} catch (err) {
message.reply({
embeds: [{
color: failColor,
title: 'Opt-In failed',
description: 'Please try the command again. If the issue persists, please join the support server, linked in my About Me section.',
}],
}).catch((e: Error) => utils.commonLoggers.messageSendError('optIn.ts:27', message, e));
}
}
};

View File

@ -1,36 +0,0 @@
import config from '../../config.ts';
import { dbClient, ignoreList, queries } from '../db.ts';
import {
// Discordeno deps
DiscordenoMessage,
} from '../../deps.ts';
import { failColor, successColor } from '../commandUtils.ts';
import utils from '../utils.ts';
export const optOut = async (message: DiscordenoMessage) => {
// Light telemetry to see how many times a command is being run
dbClient.execute(queries.callIncCnt('opt-out')).catch((e) => utils.commonLoggers.dbError('optOut.ts:11', 'call sproc INC_CNT on', e));
try {
ignoreList.push(message.authorId);
await dbClient.execute('INSERT INTO ignore_list(userid) values(?)', [message.authorId]);
message.reply({
embeds: [{
color: successColor,
title: `${config.name} will no longer respond to you.`,
description: `If you want ${config.name} to respond to you again, please DM ${config.name} the following command:
\`${config.prefix}opt-in\``,
}],
}).catch((e: Error) => utils.commonLoggers.messageSendError('optOut.ts:25', message, e));
} catch (err) {
message.reply({
embeds: [{
color: failColor,
title: 'Opt-Out failed',
description: `Please try the command again. If the issue persists, please report this using the \`${config.prefix}report opt-out failed\` command.`,
}],
}).catch((e: Error) => utils.commonLoggers.messageSendError('optOut.ts:33', message, e));
}
};

View File

@ -22,9 +22,7 @@ export const privacy = (message: DiscordenoMessage) => {
For more details, please check out the Privacy Policy on the GitHub [here](https://github.com/Burn-E99/TheArtificer/blob/master/PRIVACY.md).
Terms of Service can also be found on GitHub [here](https://github.com/Burn-E99/TheArtificer/blob/master/TERMS.md).
Want me to ignore you? Simply run \`${config.prefix}opt-out\` and ${config.name} will no longer read your messages or respond to you.`,
Terms of Service can also be found on GitHub [here](https://github.com/Burn-E99/TheArtificer/blob/master/TERMS.md).`,
}],
}],
}).catch((e: Error) => utils.commonLoggers.messageSendError('privacy.ts:33', message, e));

View File

@ -46,7 +46,7 @@ export const roll = async (message: DiscordenoMessage, args: string[], command:
}
// Rejoin all of the args and send it into the solver, if solver returns a falsy item, an error object will be substituded in
const rollCmd = message.content.substring(2);
const rollCmd = `${command} ${args.join(' ')}`;
queueRoll(
<QueuedRoll> {

View File

@ -2,10 +2,6 @@ import config from '../config.ts';
import { Client } from '../deps.ts';
import { LOCALMODE } from '../flags.ts';
type UserIdObj = {
userid: bigint;
};
export const dbClient = await new Client().connect({
hostname: LOCALMODE ? config.db.localhost : config.db.host,
port: config.db.port,
@ -14,13 +10,6 @@ export const dbClient = await new Client().connect({
password: config.db.password,
});
// List of userIds who have requested that the bot ignore them
export const ignoreList: Array<bigint> = [];
const dbIgnoreList = await dbClient.query('SELECT * FROM ignore_list');
dbIgnoreList.forEach((userIdObj: UserIdObj) => {
ignoreList.push(userIdObj.userid);
});
export const weekDays = ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'];
export const queries = {

View File

@ -47,22 +47,18 @@ const getRandomStatus = async (): Promise<string> => {
// Sends the current server count to all bot list sites we are listed on
const updateListStatistics = (botID: bigint, serverCount: number): void => {
config.botLists.forEach(async (e) => {
try {
log(LT.LOG, `Updating statistics for ${JSON.stringify(e)}`);
if (e.enabled) {
const tempHeaders = new Headers();
tempHeaders.append(e.headers[0].header, e.headers[0].value);
tempHeaders.append('Content-Type', 'application/json');
// ?{} is a template used in config, just need to replace it with the real value
const response = await fetch(e.apiUrl.replace('?{bot_id}', botID.toString()), {
'method': 'POST',
'headers': tempHeaders,
'body': JSON.stringify(e.body).replace('"?{server_count}"', serverCount.toString()), // ?{server_count} needs the "" removed from around it aswell to make sure its sent as a number
});
log(LT.INFO, `Posted server count to ${e.name}. Results: ${JSON.stringify(response)}`);
}
} catch (err) {
log(LT.ERROR, `Failed to update statistics for ${e.name} | Error: ${err.name} - ${err.message}`)
log(LT.LOG, `Updating statistics for ${JSON.stringify(e)}`);
if (e.enabled) {
const tempHeaders = new Headers();
tempHeaders.append(e.headers[0].header, e.headers[0].value);
tempHeaders.append('Content-Type', 'application/json');
// ?{} is a template used in config, just need to replace it with the real value
const response = await fetch(e.apiUrl.replace('?{bot_id}', botID.toString()), {
'method': 'POST',
'headers': tempHeaders,
'body': JSON.stringify(e.body).replace('"?{server_count}"', serverCount.toString()), // ?{server_count} needs the "" removed from around it aswell to make sure its sent as a number
});
log(LT.INFO, `Posted server count to ${e.name}. Results: ${JSON.stringify(response)}`);
}
});
};

View File

@ -15,7 +15,7 @@ import { fullSolver } from './solver.ts';
// parseRoll(fullCmd, modifiers)
// parseRoll handles converting fullCmd into a computer readable format for processing, and finally executes the solving
export const parseRoll = (fullCmd: string, modifiers: RollModifiers): SolvedRoll => {
const operators = ['^', '*', '/', '%', '+', '-', '(', ')'];
const operators = ['^', '*', '/', '%', '+', '-'];
const returnmsg = <SolvedRoll> {
error: false,
errorCode: '',
@ -178,22 +178,22 @@ export const parseRoll = (fullCmd: string, modifiers: RollModifiers): SolvedRoll
// If maximiseRoll or nominalRoll are on, mark the output as such, else use default formatting
if (modifiers.maxRoll) {
line1 = ` requested the theoretical maximum of:\n\`${config.prefix}${fullCmd}\``;
line1 = ` requested the theoretical maximum of: \`${config.prefix}${fullCmd}\``;
line2 = 'Theoretical Maximum Results: ';
} else if (modifiers.nominalRoll) {
line1 = ` requested the theoretical nominal of:\n\`${config.prefix}${fullCmd}\``;
line1 = ` requested the theoretical nominal of: \`${config.prefix}${fullCmd}\``;
line2 = 'Theoretical Nominal Results: ';
} else if (modifiers.order === 'a') {
line1 = ` requested the following rolls to be ordered from least to greatest:\n\`${config.prefix}${fullCmd}\``;
line1 = ` requested the following rolls to be ordered from least to greatest: \`${config.prefix}${fullCmd}\``;
line2 = 'Results: ';
tempReturnData.sort(compareTotalRolls);
} else if (modifiers.order === 'd') {
line1 = ` requested the following rolls to be ordered from greatest to least:\n\`${config.prefix}${fullCmd}\``;
line1 = ` requested the following rolls to be ordered from greatest to least: \`${config.prefix}${fullCmd}\``;
line2 = 'Results: ';
tempReturnData.sort(compareTotalRolls);
tempReturnData.reverse();
} else {
line1 = ` rolled:\n\`${config.prefix}${fullCmd}\``;
line1 = ` rolled: \`${config.prefix}${fullCmd}\``;
line2 = 'Results: ';
}

View File

@ -61,10 +61,8 @@ export const fullSolver = (conf: (string | number | SolvedStep)[], wrapDetails:
throw new Error('UnbalancedParens');
}
// Call the solver on the items between openParen and closeParen (excluding the parens)
const parenSolve = fullSolver(conf.slice(openParen + 1, closeParen), true);
// Replace the itemes between openParen and closeParen (including the parens) with its solved equilvalent
conf.splice(openParen, closeParen - openParen + 1, parenSolve);
// Replace the itemes between openParen and closeParen (including the parens) with its solved equilvalent by calling the solver on the items between openParen and closeParen (excluding the parens)
conf.splice(openParen, closeParen + 1, fullSolver(conf.slice(openParen + 1, closeParen), true));
// Determing if we need to add in a multiplication sign to handle implicit multiplication (like "(4)2" = 8)
// insertedMult flags if there was a multiplication sign inserted before the parens

View File

@ -1 +1 @@
deno run --allow-write=./logs/,./src/endpoints/gets/heatmap.png --allow-read=./src/solver/,./src/endpoints/gets/heatmap-base.png,./config.ts,./deps.ts,./src/mod.d.ts --allow-net .\mod.ts
deno run --allow-write=./logs/,./src/endpoints/gets/heatmap.png --allow-read=./src/solver/,./src/endpoints/gets/heatmap-base.png --allow-net .\mod.ts