Compare commits
180 Commits
Author | SHA1 | Date |
---|---|---|
|
65fbd115fb | |
|
783395e4a5 | |
|
23ca3a2665 | |
|
766dff179e | |
|
7bb6018bdf | |
|
f43cf0fa31 | |
|
38146a3c8f | |
|
539616679b | |
|
7b03f3140e | |
|
74b0e287fb | |
|
f4b0e04cec | |
|
c4c7098479 | |
|
9a8c718d57 | |
|
bcc75f5cde | |
|
18c5d2a23c | |
|
9b93f6c802 | |
|
b1c81aff78 | |
|
2346a6d994 | |
|
28e697efa2 | |
|
b17c529ba7 | |
|
6d5c457fe3 | |
|
0055690360 | |
|
6aeb983242 | |
|
5b197256ee | |
|
01af03883b | |
|
5b7cb60382 | |
|
36405421f6 | |
|
7823227f6e | |
|
54f081db74 | |
|
5366206951 | |
|
0e69231f20 | |
|
094974df80 | |
|
876a97b9fd | |
|
73a06a5112 | |
|
f79091a2a8 | |
|
edac1db702 | |
|
a06042e6d4 | |
|
169ed564ae | |
|
8e84be1656 | |
|
ed0a12c3bc | |
|
f95d24375d | |
|
31ad662212 | |
|
229353c3fe | |
|
11e42c77fd | |
|
6dd0bd0e8e | |
|
6ad0d2ca2b | |
|
61607dc75d | |
|
872f67d908 | |
|
f9355ae255 | |
|
6adf18008a | |
|
51924b7f14 | |
|
c71f6f76e4 | |
|
a7cdd91969 | |
|
42cb268a58 | |
|
8e6467fd17 | |
|
e8464cf7bb | |
|
2b22df032c | |
|
b69cf2060f | |
|
011120845e | |
|
b0266ad385 | |
|
a8bcdb346b | |
|
d769ac0e5a | |
|
57518f75a9 | |
|
021f33fc38 | |
|
bff208d560 | |
|
dba1976a8e | |
|
cc45794497 | |
|
93375585c1 | |
|
d45cc89eec | |
|
0cabfe0c99 | |
|
6e448907ee | |
|
df8c31d6d3 | |
|
a0dae3416f | |
|
170c089fe9 | |
|
71e1d42f68 | |
|
3931ee8394 | |
|
137f6388f7 | |
|
66f08bf538 | |
|
4a34596bee | |
|
60e13cbd51 | |
|
65d264d1c6 | |
|
bb10d52506 | |
|
b58cf31edb | |
|
52d9258b09 | |
|
544252b316 | |
|
92c97579c0 | |
|
26085e8238 | |
|
db1e55b415 | |
|
6e5c525839 | |
|
da00caaa74 | |
|
1f4d1e3ef6 | |
|
7391a0fd68 | |
|
a01b8b8fd2 | |
|
b7f9fc852a | |
|
6cfe98e954 | |
|
7cf62d44aa | |
|
c0ba9b7080 | |
|
1514f23f58 | |
|
dd10ef2616 | |
|
e4e04004f9 | |
|
d9cd3a5065 | |
|
63fa76c704 | |
|
abcb4972bf | |
|
17c8e0c599 | |
|
25cc171f3c | |
|
b887b93bb2 | |
|
bbba797dc3 | |
|
764b8c103b | |
|
45207f9cfc | |
|
aa97a1514c | |
|
9eff1f0835 | |
|
3c64e0cb06 | |
|
d6ec306792 | |
|
891a36a9ba | |
|
46d6014ed5 | |
|
1b01e93ef0 | |
|
96300c31df | |
|
5e84b1c0b4 | |
|
925eb08205 | |
|
0277298c37 | |
|
c7b46b9cdc | |
|
dd725aa750 | |
|
c81fc11b2b | |
|
1e7addaed9 | |
|
23d1542104 | |
|
e3c59673c5 | |
|
aaa04837a0 | |
|
3f2b86c018 | |
|
3ee26bdd86 | |
|
a994a81e89 | |
|
551bfbbeaf | |
|
79da4e51da | |
|
74337dcea4 | |
|
bf382d01ad | |
|
8454d3e189 | |
|
bf7a0aa5d2 | |
|
a270a4b8f7 | |
|
04d7324769 | |
|
394ae211df | |
|
733908f2c0 | |
|
26b9309aa3 | |
|
febd735c05 | |
|
0cafda573f | |
|
54a6a900d3 | |
|
cd549eca81 | |
|
32815146e8 | |
|
781092a24f | |
|
ddd091d572 | |
|
b18ff7dc68 | |
|
5b27abbba9 | |
|
72b715b188 | |
|
c8882bd500 | |
|
9b77912a85 | |
|
eba7e028b2 | |
|
88faa27278 | |
|
0d1ef83f50 | |
|
fade84a87b | |
|
337b266456 | |
|
15e8f847c5 | |
|
06df068ac2 | |
|
d422db9cc0 | |
|
120532b630 | |
|
6a305d1479 | |
|
6b2aae8dfb | |
|
50288534b6 | |
|
23c16f7832 | |
|
d2083bea53 | |
|
541747285f | |
|
ce77893d37 | |
|
84768293c1 | |
|
b076751bf2 | |
|
60188ca5d8 | |
|
73288073b1 | |
|
4442571dd0 | |
|
15864e7f6d | |
|
d449d1d85d | |
|
0b5f44d18c | |
|
b4d2d71873 | |
|
2a22dc7b04 | |
|
28f93446bd |
|
@ -0,0 +1,20 @@
|
|||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: bug
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
|
@ -0,0 +1,14 @@
|
|||
---
|
||||
name: Report API Abuse
|
||||
about: Believe you have encountered API Abuse? Report it here
|
||||
title: ''
|
||||
labels: report api abuse
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the suspected API Abuse:**
|
||||
If you believe you have encountered someone abusing the API, describe it here. For example, User X was sending x rolls per minute when it was not desired.
|
||||
|
||||
**Artificer Message ID:**
|
||||
Copy and paste the Message ID of one (or more) of the messages sent by The Artificer that you believe were API Abuse. If you do not know what a Message ID is or how to copy it, please refer to [this Discord Help Article](https://support.discord.com/hc/en-us/articles/206346498-Where-can-I-find-my-User-Server-Message-ID-) for details on how to find the Message ID.
|
|
@ -1,3 +1,4 @@
|
|||
config.ts
|
||||
emojis/Thumbs.db
|
||||
**/**/Thumbs.db
|
||||
logs
|
||||
src/endpoints/gets/heatmap.png
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
# Path to sources
|
||||
sonar.sources=.
|
||||
sonar.exclusions=emojis
|
||||
#sonar.inclusions=
|
||||
|
||||
# Path to tests
|
||||
#sonar.tests=
|
||||
#sonar.test.exclusions=
|
||||
#sonar.test.inclusions=
|
||||
|
||||
# Source encoding
|
||||
sonar.sourceEncoding=UTF-8
|
||||
|
||||
# Exclusions for copy-paste detection
|
||||
# src/commands/rollHelp.ts, src/commands/rollDecorators.ts are excluded to get rid of the duplicate code compliant. Sonar does not like you initializing JSON in ts files.
|
||||
sonar.cpd.exclusions=src/commands/rollHelp.ts,src/commands/rollDecorators.ts
|
12
PRIVACY.md
12
PRIVACY.md
|
@ -1,7 +1,7 @@
|
|||
# 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.
|
||||
Publicly available versions of `The Artificer#8166` (Discord ID: `789045930011656223`) (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.
|
||||
|
||||
|
@ -35,9 +35,15 @@ When using _The API_'s roll endpoint (herein referred to as _The Roll Endpoint_)
|
|||
|
||||
# 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 to remove all of your submitted data, this can easily be done using the Delete API Key option 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_.
|
||||
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 your guild has been banned from using _The API_, the Discord Guild ID will not be deleted as this data is considered necessary.
|
||||
|
||||
The data described above is considered necessary to prevent users from abusing the API and ban evading by deleting and recreating their account.
|
||||
|
||||
## 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.
|
||||
|
|
129
README.md
129
README.md
|
@ -1,9 +1,10 @@
|
|||
# The Artificer - A Dice Rolling Discord Bot
|
||||
Version 1.4.2 - 2021/02/14
|
||||
# The Artificer - A Dice Rolling Discord Bot | V2.1.2 - 2022/07/31
|
||||
[](https://sonarcloud.io/summary/new_code?id=TheArtificer)
|
||||
[](https://sonarcloud.io/summary/new_code?id=TheArtificer) [](https://sonarcloud.io/summary/new_code?id=TheArtificer) [](https://sonarcloud.io/summary/new_code?id=TheArtificer) [](https://sonarcloud.io/summary/new_code?id=TheArtificer) [](https://sonarcloud.io/summary/new_code?id=TheArtificer) [](https://sonarcloud.io/summary/new_code?id=TheArtificer)
|
||||
|
||||
The Artificer is a Discord bot that specializes in rolling dice. The bot utilizes the compact [Roll20 formatting](https://roll20.zendesk.com/hc/en-us/articles/360037773133-Dice-Reference) for ease of use and will correctly perform any needed math on the roll (limited to basic algebra).
|
||||
The Artificer is a Discord bot that specializes in rolling dice. The bot utilizes the compact [Roll20 formatting](https://artificer.eanm.dev/roll20) for ease of use and will correctly perform any needed math on the roll (limited to basic algebra).
|
||||
|
||||
This bot was developed to replace the Sidekick discord bot after it went offline many times for extended periods. This was also developed to fix some annoyances that were found with Sidekick, specifically its vague error messages (such as `"Tarantallegra!"`, what is that supposed to mean) and its inability to handle implicit multiplication (such as `4(12 + 20)`).
|
||||
This bot was developed to replace the Sidekick discord bot after it went offline many times for extended periods, and is now dead according to their GitHub. This was also developed to fix some annoyances that were found with Sidekick, specifically its vague error messages (such as `"Tarantallegra!"`, what is that supposed to mean) and its inability to handle implicit multiplication (such as `4(12 + 20)`).
|
||||
|
||||
## Using The Artificer
|
||||
I am hosting this bot for public use and you may find its invite link below. If you would like to host this bot yourself, details of how to do so are found at the end of this README, but I do not recommend this unless you are experienced with running Discord bots.
|
||||
|
@ -12,15 +13,19 @@ After inviting the bot, if you would like it to remove the message requesting th
|
|||
|
||||
[Bot Invite Link](https://discord.com/api/oauth2/authorize?client_id=789045930011656223&permissions=2048&scope=bot)
|
||||
|
||||
[Support Server Invite Link](https://discord.gg/peHASXMZYv)
|
||||
|
||||
---
|
||||
|
||||
## Available Commands
|
||||
The Artificer comes with a few supplemental commands to the main rolling command.
|
||||
|
||||
* `[[help or [[h or [[?`
|
||||
* `[[help` or `[[h` or `[[?`
|
||||
* Provides a message similar to this available commands block.
|
||||
* `[[rollhelp or [[??`
|
||||
* `[[rollhelp` or `[[??` or `[[rh` or `[[hr`
|
||||
* Details on how to use the roll command, listed as `[[xdy...]]` below.
|
||||
* `[[rolldecorators` or `[[???` or `[[rd` or `[[dr`
|
||||
* Details on how to use decorators on the roll command.
|
||||
* `[[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:
|
||||
|
@ -28,53 +33,79 @@ The Artificer comes with a few supplemental commands to the main rolling command
|
|||
* 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`
|
||||
* `[[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`
|
||||
* `[[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`
|
||||
* `[[version` or `[[v`
|
||||
* Prints out the current version of the bot.
|
||||
* `[[popcat or [[pop or [[p`
|
||||
* `[[popcat` or `[[pop` or `[[p`
|
||||
* Sends the animated popcat emote for those who do not have Discord Nitro.
|
||||
* If bot is given the permission `Manage Messages`, the bot will remove the message requesting the emote.
|
||||
* `[[stats or [[s`
|
||||
* `[[stats` or `[[s`
|
||||
* Prints out how many users, channels, and servers the bot is currently serving.
|
||||
* `[[report or [[r [command that failed]`
|
||||
* `[[heatmap` or `[[hm`
|
||||
* Heatmap of when the roll command is run the most.
|
||||
* `[[report` or `[[r [command that failed]`
|
||||
* People aren't perfect, but this bot is trying to be.
|
||||
* 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://roll20.zendesk.com/hc/en-us/articles/360037773133-Dice-Reference), this will no different.
|
||||
* 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.
|
||||
* Any math (limited to exponentials, multiplication, division, modulus, addition, and subtraction) will be correctly handled in PEMDAS order, so use parenthesis as needed.
|
||||
* PI and e are available for use.
|
||||
* Parameters for rolling:
|
||||
|
||||
| Paramater | Required? | Repeatable? | Description |
|
||||
|---------------|-------------|---------------|--------------------------------------------------------------------------------------------------|
|
||||
| x | Optional | No | number of dice to roll, if omitted, 1 is used |
|
||||
| dy | Required | No | size of dice to roll, d20 = 20 sided die |
|
||||
| dz or dlz | Optional | No | drops the lowest z dice, cannot be used any other drop or keep options |
|
||||
| kz or khz | Optional | No | keeps the highest z dice, cannot be used any other drop or keep options |
|
||||
| dhz | Optional | No | drops the highest z dice, cannot be used any other drop or keep options |
|
||||
| klz | Optional | No | keeps the lowest z dice, cannot be used any other drop or keep options |
|
||||
| ra | Optional | Yes | rerolls any rolls that match a, r3 will reroll any dice that land on 3, throwing out old rolls |
|
||||
| csq or cs=q | Optional | Yes | changes crit score to q |
|
||||
| cs<q | Optional | Yes | changes crit score to be less than or equal to q |
|
||||
| cs>q | Optional | Yes | changes crit score to be greater than or equal to q |
|
||||
| cfq or cf=q | Optional | Yes | changes crit fail to q |
|
||||
| cf<q | Optional | Yes | changes crit fail to be less than or equal to q |
|
||||
| cf>q | Optional | Yes | changes crit fail to be greater than or equal to q |
|
||||
| ! | Optional | No | exploding, rolls another dy for every crit roll |
|
||||
| Paramater | Required? | Repeatable? | Description |
|
||||
|---------------|-------------|---------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| x | Optional | No | number of dice to roll, if omitted, 1 is used, additionally, replace x with `F` to roll the dice as Fate dice |
|
||||
| dy | Required | No | size of dice to roll, d20 = 20 sided die |
|
||||
| dz or dlz | Optional | No | drops the lowest z dice, cannot be used with any other drop or keep options |
|
||||
| kz or khz | Optional | No | keeps the highest z dice, cannot be used with any other drop or keep options |
|
||||
| dhz | Optional | No | drops the highest z dice, cannot be used with any other drop or keep options |
|
||||
| klz | Optional | No | keeps the lowest z dice, cannot be used with any other drop or keep options |
|
||||
| ra or r=a | Optional | Yes | rerolls any rolls that match a, r3 will reroll every die that land on 3, throwing out old rolls, cannot be used with ro |
|
||||
| r<a | Optional | Yes | rerolls any rolls that are less than or equal to a, r3 will reroll every die that land on 3, 2, or 1, throwing out old rolls, cannot be used with ro |
|
||||
| r>a | Optional | Yes | rerolls any rolls that are greater than or equal to a, r3 will reroll every die that land on 3 or greater, throwing out old rolls, cannot be used with ro |
|
||||
| roa or ro=a | Optional | Yes | rerolls any rolls that match a, r3 will reroll each die that lands on 3 ONLY ONE TIME, throwing out old rolls, cannot be used with r |
|
||||
| ro<a | Optional | Yes | rerolls any rolls that are less than or equal to a, r3 will reroll each die that lands on 3, 2, or 1 ONLY ONE TIME, throwing out old rolls, cannot be used with r |
|
||||
| ro>a | Optional | Yes | rerolls any rolls that are greater than or equal to a, r3 will reroll each die that lands on 3 or greater ONLY ONE TIME, throwing out old rolls, cannot be used with r |
|
||||
| csq or cs=q | Optional | Yes | changes crit score to q |
|
||||
| cs<q | Optional | Yes | changes crit score to be less than or equal to q |
|
||||
| cs>q | Optional | Yes | changes crit score to be greater than or equal to q |
|
||||
| cfq or cf=q | Optional | Yes | changes crit fail to q |
|
||||
| cf<q | Optional | Yes | changes crit fail to be less than or equal to q |
|
||||
| cf>q | Optional | Yes | changes crit fail to be greater than or equal to q |
|
||||
| ! | Optional | No | exploding, rolls another dy for every crit success |
|
||||
| !o | Optional | No | exploding once, rolls another dy for each original crit success |
|
||||
| !p | Optional | No | penetrating explosion, rolls one dy for each crit success, but subtracts one from each resulting explosion |
|
||||
| !! | Optional | No | compounding explosion, rolls one dy for each crit success, but adds the resulting explosion to the die that caused this explosion |
|
||||
| !=u | Optional | Yes | exploding, rolls another dy for every die that lands on u |
|
||||
| !>u | Optional | Yes | exploding, rolls another dy for every die that lands on u or greater |
|
||||
| !<u> | Optional | Yes | exploding, rolls another dy for every die that lands on u or less |
|
||||
| !o=u | Optional | Yes | exploding once, rolls another dy for each original die that landed on u |
|
||||
| !o>u | Optional | Yes | exploding once, rolls another dy for each original die that landed on u or greater |
|
||||
| !o<u | Optional | Yes | exploding once, rolls another dy for each original die that landed on u or less |
|
||||
| !p=u | Optional | Yes | penetrating explosion, rolls one dy for each die that lands on u, but subtracts one from each resulting explosion |
|
||||
| !p>u | Optional | Yes | penetrating explosion, rolls one dy for each die that lands on u or greater, but subtracts one from each resulting explosion |
|
||||
| !p<u | Optional | Yes | penetrating explosion, rolls one dy for each die that lands on u or under, but subtracts one from each resulting explosion |
|
||||
| !!=u | Optional | Yes | compounding explosion, rolls one dy for each die that lands on u, but adds the resulting explosion to the die that caused this explosion |
|
||||
| !!>u | Optional | Yes | compounding explosion, rolls one dy for each die that lands on u or greater, but adds the resulting explosion to the die that caused this explosion |
|
||||
| !!<u | Optional | Yes | compounding explosion, rolls one dy for each die that lands on u or under, but adds the resulting explosion to the die that caused this explosion |
|
||||
|
||||
* If the parameter is Required, it must be provided at all times.
|
||||
* If the parameter is Repeatable, it may occur multiple times in the roll configuration.
|
||||
|
@ -83,13 +114,26 @@ The Artificer comes with a few supplemental commands to the main rolling command
|
|||
* `[[4d20r1!]]` will roll 4 d20 dice, rerolling any dice that land on 1, and repeatedly rolling a new d20 for any critical success rolled.
|
||||
* `[[d20/40]]` will roll a d20 die and divide it by 40.
|
||||
* `[[((d20+20) - 10) / 5]]` will roll a d20, add 20 to that roll, subtract off 10, and finally divide by 5.
|
||||
* This command also has some useful flags that can used. These flags simply need to be placed after all rolls in the message:
|
||||
* This command can also handle some custom format dice:
|
||||
* CWOD Dice - `[[xcwody]]`
|
||||
* `x` - Number of CWOD dice to roll
|
||||
* `y` - Difficulty to roll at
|
||||
* OVA Dice - `[[xovady]]`
|
||||
* `x` - Number of OVA dice to roll
|
||||
* `y` - Size of the die to roll (defaults to 6 if omitted)
|
||||
* This command also has some useful decorators that can used. These decorators simply need to be placed after all rolls in the message:
|
||||
* `-c` - Count - Shows the Count Embed, containing the count of successful rolls, failed rolls, rerolls, drops, and explosions
|
||||
* `-nd` - No Details - Suppresses all details of the requested roll
|
||||
* `-snd` - Super No Details - Suppresses all details of the requested roll and hides no details message
|
||||
* `-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
|
||||
* The results have some formatting applied on them to provide details on what happened during this roll.
|
||||
* Critical successes will be **bolded**
|
||||
* Critical fails will be <ins>underlined</ins>
|
||||
* Rolls that were dropped or rerolled ~~crossed out~~
|
||||
|
||||
## 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.
|
||||
|
@ -115,7 +159,9 @@ Available Endpoints and Methods Required:
|
|||
* `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):
|
||||
* `c` - Count - Shows the Count Embed, containing the count of successful rolls, failed rolls, rerolls, drops, and explosions
|
||||
* `nd` - No Details - Suppresses all details of the requested roll.
|
||||
* `snd` - Super No Details - Suppresses all details of the requested roll and hides no details message.
|
||||
* `s` - Spoiler - Spoilers all details of the requested roll.
|
||||
* `m` - Maximize Roll - Rolls the theoretical maximum roll, cannot be used with Nominal roll.
|
||||
* `n` - Nominal Roll - Rolls the theoretical nominal roll, cannot be used with Maximise roll.
|
||||
|
@ -162,7 +208,7 @@ Available Endpoints and Methods Required:
|
|||
* `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/).
|
||||
API Key management via a basic GUI is availble on the [API Tools](https://artificer.eanm.dev/) website.
|
||||
|
||||
## 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.
|
||||
|
@ -170,16 +216,25 @@ 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.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.
|
||||
The Artificer is built on [Deno](https://deno.land/) `v1.22.0` using [Discordeno](https://discordeno.mod.land/) `v12.0.1`. 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 `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`.
|
||||
Once everything is set up, starting the bot can simply be done with the command in `start.command`.
|
||||
|
||||
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`.
|
||||
|
||||
---
|
||||
|
||||
## Privacy Policy and Terms of Service
|
||||
The Artificer has a Privacy Policy and Terms of Service to detail expectations of what user data is stored and how users should use The Artificer. The following Privacy Policy and Terms of Service only apply to the officially hosted version of The Artificer (`The Artificer#8166`, Discord ID: `789045930011656223`).
|
||||
|
||||
Privacy Policy TL;DR: Only report command data is stored if you do not use the API, if you use the API, submitted Discord Ids will be stored linked to your email. For more detailed information, please check out the full [PRIVACY POLICY](https://github.com/Burn-E99/TheArtificer/blob/master/PRIVACY.md).
|
||||
|
||||
Terms of Service TL;DR: Don't abuse or attempt to hack/damage The Artificer or its API. If you do, you may be banned from use. For more detailed information, please check out the full [TERMS OF SERVICE](https://github.com/Burn-E99/TheArtificer/blob/master/TERMS.md).
|
||||
|
||||
---
|
||||
|
||||
### Built in memory of my Grandmother, Babka
|
||||
With much love, Ean
|
||||
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
# The Artificer's Terms of Service/User Agreement
|
||||
By using The Artificer and/or The Artificer's API, you agree to the following terms. Breaking these terms may result in your account being banned from using The Artificer's API, The Artificer (Discord Bot), or both.
|
||||
|
||||
1. **User Conduct**. You agree to obey all applicable laws in using the Service, and agree that you are responsible for the content and/or communications you send to or initiate via The Artificer. You agree that you are responsible for everything that you transmit to or in relation to The Artificer and you specifically agree (in relation to The Artificer) not to participate in any form of activity which is unlawful, harassing, libellous, defamatory, abusive, threatening, harmful, vulgar, obscene, profane, sexually-oriented, racially-offensive or otherwise includes objectionable material;
|
||||
|
||||
* not to collect personal data about other Users (for any purpose);
|
||||
* not to register more than one account for yourself or anyone else;
|
||||
* not to use The Artificer to engage in any commercial activities not approved in writing by the Developer, Ean Milligan;
|
||||
* not to impose an unreasonable or disproportionately large load on our infrastructure; and
|
||||
* not to attempt to gain unauthorised access to The Artificer's computer systems or engage in any activity that disrupts, diminishes the quality of, interferes with the performance of, or impairs the functionality of The Artificer or The Artificer's API.
|
||||
|
||||
2. **Hacking**. You agree and undertake not to attempt to damage, deny service to, hack, crack, or otherwise interfere (collectively, "Interfere") with The Artificer in any manner. If you in any way Interfere with these, you agree to pay all damages we incur as a result. We reserve the right to deny any or all access or service to any User for any reason, at any time, at our sole discretion. You agree that we may block your access, and at our sole discretion to disallow your continued use of The Artificer. We reserve the right to take any action we may deem appropriate in our sole discretion with respect to violations or enforcement of the terms of this Agreement, and we expressly reserve all rights and remedies available to us at law or in equity.
|
||||
3. **Termination of this Agreement**. The Artificer may at any time terminate this legal Agreement, in our sole discretion without prior notice to you, if we believe that you may have breached (or acted in a manner indicating that you do not intend to or are unable to comply with) any term herein, or if we are legally required to do so by law, or if continuation is likely to be no longer commercially viable.
|
||||
4. **Contacting Us**. If you have any questions, please contact us via email at <ean@milligan.dev>.
|
|
@ -0,0 +1,21 @@
|
|||
#!/bin/sh
|
||||
|
||||
# PROVIDE: artificer
|
||||
|
||||
. /etc/rc.subr
|
||||
|
||||
name="artificer"
|
||||
rcvar="artificer_enable"
|
||||
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_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"
|
||||
|
||||
load_rc_config artificer
|
||||
run_rc_command "$1"
|
|
@ -0,0 +1,14 @@
|
|||
[Unit]
|
||||
Description=The Artificer Discord Bot
|
||||
Documentation=https://github.com/Burn-E99/TheArtificer
|
||||
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
|
||||
RestartSec=60
|
||||
Restart=on-failure
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
|
@ -1,41 +1,63 @@
|
|||
export const config = {
|
||||
"name": "The Artificer", // Name of the bot
|
||||
"version": "1.4.2", // 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
|
||||
"enable": false, // Leave this off if you have no intention of using this/supporting it
|
||||
"port": 8080, // Port for the API to listen on
|
||||
"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
|
||||
"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.
|
||||
'name': 'The Artificer', // Name of the bot
|
||||
'version': '2.1.3', // 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
|
||||
'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
|
||||
},
|
||||
"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
|
||||
'api': { // Setting for the built-in API
|
||||
'enable': false, // Leave this off if you have no intention of using this/supporting it
|
||||
'publicDomain': 'http://example.com/', // Public domain that the API is behind, should end with a /
|
||||
'port': 8080, // Port for the API to listen on
|
||||
'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
|
||||
'adminKey': 'your_25char_api_token', // API Key generated by nanoid that is 25 char long, this gets pre-populated into all_keys
|
||||
'email': 0n, // Temporary set up for email, this will be adjusted to an actual email using deno-smtp in the future.
|
||||
},
|
||||
"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
|
||||
"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
|
||||
'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': 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': 0n, // Discord channel ID where the bot should put startup messages and other error messages needed
|
||||
'reportChannel': 0n, // Discord channel ID where reports will be sent when using the built-in report command
|
||||
'devServer': 0n, // Discord guild ID where testing of indev features/commands will be handled, used in conjuction with the DEVMODE bool in mod.ts
|
||||
'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
|
||||
"aliases": ["popcat", "pop", "p"], // Commands that will activate this emoji
|
||||
"id": "796340018377523221", // Discord emoji ID for this emoji
|
||||
"animated": true, // Tells the bot this emoji is animated so it sends correctly
|
||||
"deleteSender": true // Tells the bot to attempt to delete the sender's message after sending the emoji
|
||||
}
|
||||
]
|
||||
'name': 'emoji_name', // Name of emoji in discord
|
||||
'aliases': ['alias_1', 'alias_2', 'alias_n'], // Commands that will activate this emoji
|
||||
'id': 'the_emoji_id', // Discord emoji ID for this emoji
|
||||
'animated': false, // Tells the bot this emoji is animated so it sends correctly
|
||||
'deleteSender': false, // Tells the bot to attempt to delete the sender's message after sending the emoji
|
||||
},
|
||||
],
|
||||
'botLists': [ // Array of objects containing all bot lists that stats should be posted to
|
||||
{ // Bot List object, duplicate for each bot list
|
||||
'name': 'Bot List Name', // Name of bot list, not used
|
||||
'enabled': true, // Should statistics be posted to this list?
|
||||
'apiUrl': 'https://example.com/api/bots/?{bot_id}/stats', // API URL, use ?{bot_id} in place of the bot id so that it can be dynamically replaced
|
||||
'headers': [ // Array of headers that need to be added to the request
|
||||
{ // Header Object, duplicate for every header needed
|
||||
'header': 'header_name', // Name of header needed, usually Authorization is needed
|
||||
'value': 'header_value', // Value for the header
|
||||
},
|
||||
],
|
||||
'body': { // Data payload to send to the bot list, will be turned into a string and any ?{} will be replaced with the required value, currently only has ?{server_count}
|
||||
'param_name': '?{param_value}', // Add more params as needed
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export default config;
|
||||
|
|
130
db/initialize.ts
130
db/initialize.ts
|
@ -1,48 +1,52 @@
|
|||
// This file will create all tables for the artificer schema
|
||||
// DATA WILL BE LOST IF DB ALREADY EXISTS, RUN AT OWN RISK
|
||||
|
||||
import {
|
||||
// MySQL deps
|
||||
Client
|
||||
} from "../deps.ts";
|
||||
import config from '../config.ts';
|
||||
import { dbClient } from '../src/db.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,
|
||||
username: config.db.username,
|
||||
password: config.db.password,
|
||||
});
|
||||
|
||||
console.log("Attempting to create DB");
|
||||
console.log('Attempting to create DB');
|
||||
await dbClient.execute(`CREATE SCHEMA IF NOT EXISTS ${config.db.name};`);
|
||||
await dbClient.execute(`USE ${config.db.name}`);
|
||||
console.log("DB created");
|
||||
console.log('DB created');
|
||||
|
||||
console.log("Attempt to drop all tables");
|
||||
console.log('Attempt to drop all tables');
|
||||
await dbClient.execute(`DROP VIEW IF EXISTS db_size;`);
|
||||
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_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;`);
|
||||
console.log("Tables dropped");
|
||||
await dbClient.execute(`DROP TABLE IF EXISTS ignore_list;`);
|
||||
console.log('Tables dropped');
|
||||
|
||||
console.log("Attempting to create table command_cnt");
|
||||
// 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(`
|
||||
CREATE TABLE command_cnt (
|
||||
command char(20) NOT NULL,
|
||||
count bigint unsigned NOT NULL DEFAULT 0,
|
||||
hourlyRate float 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('Table created');
|
||||
|
||||
console.log("Attempt creating increment Stored Procedure");
|
||||
console.log('Attempt creating increment count Stored Procedure');
|
||||
await dbClient.execute(`
|
||||
CREATE PROCEDURE INC_CNT(
|
||||
IN cmd CHAR(20)
|
||||
|
@ -53,9 +57,48 @@ await dbClient.execute(`
|
|||
UPDATE command_cnt SET count = oldcnt + 1 WHERE command = cmd;
|
||||
END
|
||||
`);
|
||||
console.log("Stored Procedure created");
|
||||
console.log('Stored Procedure created');
|
||||
|
||||
console.log("Attempting to create table roll_log");
|
||||
// Holds daily average of commands
|
||||
console.log('Attempting to create table roll_time_heatmap');
|
||||
await dbClient.execute(`
|
||||
CREATE TABLE roll_time_heatmap (
|
||||
hour tinyint(1) unsigned NOT NULL,
|
||||
sunday bigint unsigned NOT NULL DEFAULT 0,
|
||||
monday bigint unsigned NOT NULL DEFAULT 0,
|
||||
tuesday bigint unsigned NOT NULL DEFAULT 0,
|
||||
wednesday bigint unsigned NOT NULL DEFAULT 0,
|
||||
thursday bigint unsigned NOT NULL DEFAULT 0,
|
||||
friday bigint unsigned NOT NULL DEFAULT 0,
|
||||
saturday bigint unsigned NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (hour),
|
||||
UNIQUE KEY roll_time_heatmap_hour_UNIQUE (hour)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
`);
|
||||
console.log('Table created');
|
||||
|
||||
console.log('Attempt creating increment heatmap Stored Procedure');
|
||||
await dbClient.execute(`
|
||||
CREATE PROCEDURE INC_HEATMAP(
|
||||
IN dy varchar(10),
|
||||
IN hr tinyint(1)
|
||||
)
|
||||
BEGIN
|
||||
SET @s1=CONCAT('SELECT ',dy,' FROM roll_time_heatmap WHERE hour = ',hr,' INTO @oldcnt');
|
||||
PREPARE stmt1 FROM @s1;
|
||||
EXECUTE stmt1;
|
||||
DEALLOCATE PREPARE stmt1;
|
||||
|
||||
SET @s2=CONCAT('UPDATE roll_time_heatmap SET ',dy,' = @oldcnt + 1 WHERE hour = ',hr);
|
||||
PREPARE stmt2 FROM @s2;
|
||||
EXECUTE stmt2;
|
||||
DEALLOCATE PREPARE stmt2;
|
||||
END
|
||||
`);
|
||||
console.log('Stored Procedure created');
|
||||
|
||||
// Roll log, holds rolls when requests
|
||||
console.log('Attempting to create table roll_log');
|
||||
await dbClient.execute(`
|
||||
CREATE TABLE roll_log (
|
||||
id int unsigned NOT NULL AUTO_INCREMENT,
|
||||
|
@ -70,22 +113,25 @@ await dbClient.execute(`
|
|||
UNIQUE KEY roll_log_resultid_UNIQUE (resultid)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
`);
|
||||
console.log("Table created");
|
||||
console.log('Table created');
|
||||
|
||||
console.log("Attempting to create table allowed_guilds");
|
||||
// Api guild settings
|
||||
console.log('Attempting to create table allowed_guilds');
|
||||
await dbClient.execute(`
|
||||
CREATE TABLE allowed_guilds (
|
||||
guildid bigint unsigned NOT NULL,
|
||||
channelid 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)
|
||||
hidewarn tinyint(1) NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (guildid, channelid)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
`);
|
||||
console.log("Table created");
|
||||
console.log('Table created');
|
||||
|
||||
console.log("Attempting to create table all_keys");
|
||||
// Api keys
|
||||
console.log('Attempting to create table all_keys');
|
||||
await dbClient.execute(`
|
||||
CREATE TABLE all_keys (
|
||||
userid bigint unsigned NOT NULL,
|
||||
|
@ -101,9 +147,10 @@ await dbClient.execute(`
|
|||
UNIQUE KEY all_keys_email_UNIQUE (email)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
`);
|
||||
console.log("Table created");
|
||||
console.log('Table created');
|
||||
|
||||
console.log("Attempting to create table allowed_channels");
|
||||
// Api user settings
|
||||
console.log('Attempting to create table allowed_channels');
|
||||
await dbClient.execute(`
|
||||
CREATE TABLE allowed_channels (
|
||||
userid bigint unsigned NOT NULL,
|
||||
|
@ -115,7 +162,22 @@ await dbClient.execute(`
|
|||
CONSTRAINT allowed_channels_userid_FK FOREIGN KEY (userid) REFERENCES all_keys (userid) ON DELETE RESTRICT ON UPDATE RESTRICT
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
`);
|
||||
console.log("Table created");
|
||||
console.log('Table created');
|
||||
|
||||
// Database sizes view
|
||||
console.log('Attempting to create view db_size');
|
||||
await dbClient.execute(`
|
||||
CREATE VIEW db_size AS
|
||||
SELECT
|
||||
table_name AS "table",
|
||||
ROUND(((data_length + index_length) / 1024 / 1024), 3) AS "size",
|
||||
table_rows AS "rows"
|
||||
FROM information_schema.TABLES
|
||||
WHERE
|
||||
table_schema = "${config.db.name}"
|
||||
AND table_name <> "db_size";
|
||||
`);
|
||||
console.log('View Created');
|
||||
|
||||
await dbClient.close();
|
||||
console.log("Done!");
|
||||
console.log('Done!');
|
||||
|
|
|
@ -1,36 +1,30 @@
|
|||
// This file will populate the tables with default values
|
||||
|
||||
import {
|
||||
// MySQL deps
|
||||
Client
|
||||
} from "../deps.ts";
|
||||
import config from '../config.ts';
|
||||
import { dbClient } from '../src/db.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 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('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'];
|
||||
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);
|
||||
});
|
||||
}
|
||||
console.log("Insertion done");
|
||||
console.log('Insertion done');
|
||||
|
||||
console.log('Attempting to insert default hours into roll_time_heatmap');
|
||||
for (let i = 0; i <= 23; i++) {
|
||||
await dbClient.execute('INSERT INTO roll_time_heatmap(hour) values(?)', [i]).catch((e) => {
|
||||
console.log(`Failed to insert hour ${i} into database`, e);
|
||||
});
|
||||
}
|
||||
console.log('Insertion done');
|
||||
|
||||
await dbClient.close();
|
||||
console.log("Done!");
|
||||
console.log('Done!');
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"allowJs": true,
|
||||
"lib": ["deno.worker"],
|
||||
"strict": true
|
||||
},
|
||||
"lint": {
|
||||
"files": {
|
||||
"include": ["src/", "db/", "mod.ts", "deps.ts", "config.ts", "config.example.ts"],
|
||||
"exclude": []
|
||||
},
|
||||
"rules": {
|
||||
"tags": ["recommended"],
|
||||
"include": ["ban-untagged-todo"],
|
||||
"exclude": []
|
||||
}
|
||||
},
|
||||
"fmt": {
|
||||
"files": {
|
||||
"include": ["src/", "db/", "mod.ts", "deps.ts", "config.ts", "config.example.ts"],
|
||||
"exclude": []
|
||||
},
|
||||
"options": {
|
||||
"useTabs": true,
|
||||
"lineWidth": 200,
|
||||
"indentWidth": 2,
|
||||
"singleQuote": true,
|
||||
"proseWrap": "preserve"
|
||||
}
|
||||
}
|
||||
}
|
31
deps.ts
31
deps.ts
|
@ -1,19 +1,26 @@
|
|||
// All external dependancies are to be loaded here to make updating dependancy versions much easier
|
||||
export {
|
||||
startBot, editBotsStatus,
|
||||
Intents, StatusTypes, ActivityType,
|
||||
sendMessage, sendDirectMessage,
|
||||
botId,
|
||||
cache,
|
||||
memberIDHasPermission
|
||||
} from "https://deno.land/x/discordeno@10.3.0/mod.ts";
|
||||
cacheHandlers,
|
||||
DiscordActivityTypes,
|
||||
editBotNickname,
|
||||
editBotStatus,
|
||||
hasGuildPermissions,
|
||||
Intents,
|
||||
sendDirectMessage,
|
||||
sendMessage,
|
||||
startBot,
|
||||
} from 'https://deno.land/x/discordeno@12.0.1/mod.ts';
|
||||
|
||||
export type {
|
||||
CacheData, Message, Guild, MessageContent
|
||||
} from "https://deno.land/x/discordeno@10.3.0/mod.ts";
|
||||
export type { CreateMessage, DiscordenoGuild, DiscordenoMessage, EmbedField } from 'https://deno.land/x/discordeno@12.0.1/mod.ts';
|
||||
|
||||
export { Client } from "https://deno.land/x/mysql@v2.7.0/mod.ts";
|
||||
export { Client } from 'https://deno.land/x/mysql@v2.10.2/mod.ts';
|
||||
|
||||
export { serve } from "https://deno.land/std@0.83.0/http/server.ts";
|
||||
export { Status, STATUS_TEXT } from "https://deno.land/std@0.83.0/http/http_status.ts";
|
||||
export { Status, STATUS_TEXT } from 'https://deno.land/std@0.145.0/http/http_status.ts';
|
||||
|
||||
export { nanoid } from "https://deno.land/x/nanoid@v3.0.0/mod.ts";
|
||||
export { nanoid } from 'https://deno.land/x/nanoid@v3.0.0/mod.ts';
|
||||
|
||||
export { initLog, log, LogTypes as LT } from 'https://raw.githubusercontent.com/Burn-E99/Log4Deno/V1.1.1/mod.ts';
|
||||
|
||||
export * as is from 'https://deno.land/x/imagescript@v1.2.13/mod.ts';
|
||||
|
|
|
@ -1,87 +0,0 @@
|
|||
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 cf=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;
|
810
mod.ts
810
mod.ts
|
@ -4,589 +4,315 @@
|
|||
* December 21, 2020
|
||||
*/
|
||||
|
||||
import config from './config.ts';
|
||||
import { DEBUG, DEVMODE, LOCALMODE } from './flags.ts';
|
||||
import {
|
||||
// Discordeno deps
|
||||
startBot, editBotsStatus,
|
||||
Intents, StatusTypes, ActivityType,
|
||||
Message, Guild, sendMessage, sendDirectMessage,
|
||||
botId,
|
||||
cache,
|
||||
memberIDHasPermission,
|
||||
DiscordActivityTypes,
|
||||
DiscordenoGuild,
|
||||
DiscordenoMessage,
|
||||
editBotNickname,
|
||||
editBotStatus,
|
||||
initLog,
|
||||
Intents,
|
||||
// Log4Deno deps
|
||||
log,
|
||||
LT,
|
||||
// Discordeno deps
|
||||
sendMessage,
|
||||
startBot,
|
||||
} from './deps.ts';
|
||||
import api from './src/api.ts';
|
||||
import { dbClient, ignoreList } from './src/db.ts';
|
||||
import commands from './src/commands/_index.ts';
|
||||
import intervals from './src/intervals.ts';
|
||||
import { successColor, warnColor } from './src/commandUtils.ts';
|
||||
import utils from './src/utils.ts';
|
||||
|
||||
// MySQL Driver deps
|
||||
Client
|
||||
} from "./deps.ts";
|
||||
|
||||
import api from "./src/api.ts";
|
||||
import intervals from "./src/intervals.ts";
|
||||
import utils from "./src/utils.ts";
|
||||
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: LOCALMODE ? config.db.localhost : config.db.host,
|
||||
port: config.db.port,
|
||||
db: config.db.name,
|
||||
username: config.db.username,
|
||||
password: config.db.password
|
||||
});
|
||||
// Initialize logging client with folder to use for logs, needs --allow-write set on Deno startup
|
||||
initLog('logs', DEBUG);
|
||||
|
||||
// Start up the Discord Bot
|
||||
startBot({
|
||||
token: LOCALMODE ? config.localtoken : config.token,
|
||||
intents: [Intents.GUILD_MESSAGES, Intents.DIRECT_MESSAGES, Intents.GUILDS],
|
||||
intents: [Intents.GuildMessages, Intents.DirectMessages, Intents.Guilds],
|
||||
eventHandlers: {
|
||||
ready: () => {
|
||||
console.log(`${config.name} Logged in!`);
|
||||
editBotsStatus(StatusTypes.Online, "Booting up . . .", ActivityType.Game);
|
||||
log(LT.INFO, `${config.name} Logged in!`);
|
||||
editBotStatus({
|
||||
activities: [{
|
||||
name: 'Booting up . . .',
|
||||
type: DiscordActivityTypes.Game,
|
||||
createdAt: new Date().getTime(),
|
||||
}],
|
||||
status: 'online',
|
||||
});
|
||||
|
||||
// Interval to rotate the status text every 30 seconds to show off more commands
|
||||
setInterval(() => {
|
||||
setInterval(async () => {
|
||||
log(LT.LOG, 'Changing bot status');
|
||||
try {
|
||||
// Wrapped in try-catch due to hard crash possible
|
||||
editBotsStatus(StatusTypes.Online, intervals.getRandomStatus(cache), ActivityType.Game);
|
||||
} catch (err) {
|
||||
console.error("Failed to update status 00", err);
|
||||
editBotStatus({
|
||||
activities: [{
|
||||
name: await intervals.getRandomStatus(),
|
||||
type: DiscordActivityTypes.Game,
|
||||
createdAt: new Date().getTime(),
|
||||
}],
|
||||
status: 'online',
|
||||
});
|
||||
} catch (e) {
|
||||
log(LT.ERROR, `Failed to update status: ${JSON.stringify(e)}`);
|
||||
}
|
||||
}, 30000);
|
||||
|
||||
// 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);
|
||||
}, 86400000);
|
||||
|
||||
// Interval to update hourlyRates every hour
|
||||
setInterval(() => {
|
||||
log(LT.LOG, 'Updating all command hourlyRates');
|
||||
intervals.updateHourlyRates();
|
||||
}, 3600000);
|
||||
|
||||
// Interval to update heatmap.png every hour
|
||||
setInterval(() => {
|
||||
log(LT.LOG, 'Updating heatmap.png');
|
||||
intervals.updateHeatmapPng();
|
||||
}, 3600000);
|
||||
|
||||
// setTimeout added to make sure the startup message does not error out
|
||||
setTimeout(() => {
|
||||
editBotsStatus(StatusTypes.Online, `Boot Complete`, ActivityType.Game);
|
||||
sendMessage(config.logChannel, `${config.name} has started, running version ${config.version}.`).catch(() => {
|
||||
console.error("Failed to send message 00");
|
||||
LOCALMODE && editBotNickname(config.devServer, `LOCAL - ${config.name}`);
|
||||
LOCALMODE ? log(LT.INFO, 'updateListStatistics not running') : intervals.updateListStatistics(botId, cache.guilds.size + cache.dispatchedGuildIds.size);
|
||||
intervals.updateHourlyRates();
|
||||
intervals.updateHeatmapPng();
|
||||
editBotStatus({
|
||||
activities: [{
|
||||
name: 'Booting Complete',
|
||||
type: DiscordActivityTypes.Game,
|
||||
createdAt: new Date().getTime(),
|
||||
}],
|
||||
status: 'online',
|
||||
});
|
||||
sendMessage(config.logChannel, {
|
||||
embeds: [{
|
||||
title: `${config.name} is now Online`,
|
||||
color: successColor,
|
||||
fields: [
|
||||
{
|
||||
name: 'Version:',
|
||||
value: `${config.version}`,
|
||||
inline: true,
|
||||
},
|
||||
],
|
||||
}],
|
||||
}).catch((e: Error) => utils.commonLoggers.messageSendError('mod.ts:88', 'Startup', e));
|
||||
}, 1000);
|
||||
},
|
||||
guildCreate: (guild: Guild) => {
|
||||
sendMessage(config.logChannel, `New guild joined: ${guild.name} (id: ${guild.id}). This guild has ${guild.memberCount} members!`).catch(() => {
|
||||
console.error("Failed to send message 01");
|
||||
});
|
||||
guildCreate: (guild: DiscordenoGuild) => {
|
||||
log(LT.LOG, `Handling joining guild ${JSON.stringify(guild)}`);
|
||||
sendMessage(config.logChannel, {
|
||||
embeds: [{
|
||||
title: 'New Guild Joined!',
|
||||
color: successColor,
|
||||
fields: [
|
||||
{
|
||||
name: 'Name:',
|
||||
value: `${guild.name}`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: 'Id:',
|
||||
value: `${guild.id}`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: 'Member Count:',
|
||||
value: `${guild.memberCount}`,
|
||||
inline: true,
|
||||
},
|
||||
],
|
||||
}],
|
||||
}).catch((e: Error) => utils.commonLoggers.messageSendError('mod.ts:95', 'Join Guild', e));
|
||||
},
|
||||
guildDelete: (guild: Guild) => {
|
||||
sendMessage(config.logChannel, `I have been removed from: ${guild.name} (id: ${guild.id})`).catch(() => {
|
||||
console.error("Failed to send message 02");
|
||||
});
|
||||
guildDelete: (guild: DiscordenoGuild) => {
|
||||
log(LT.LOG, `Handling leaving guild ${JSON.stringify(guild)}`);
|
||||
sendMessage(config.logChannel, {
|
||||
embeds: [{
|
||||
title: 'Removed from Guild',
|
||||
color: warnColor,
|
||||
fields: [
|
||||
{
|
||||
name: 'Name:',
|
||||
value: `${guild.name}`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: 'Id:',
|
||||
value: `${guild.id}`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: 'Member Count:',
|
||||
value: `${guild.memberCount}`,
|
||||
inline: true,
|
||||
},
|
||||
],
|
||||
}],
|
||||
}).catch((e: Error) => utils.commonLoggers.messageSendError('mod.ts:99', 'Leave Guild', e));
|
||||
dbClient.execute('DELETE FROM allowed_guilds WHERE guildid = ? AND banned = 0', [guild.id]).catch((e) => utils.commonLoggers.dbError('mod.ts:100', 'delete from', e));
|
||||
},
|
||||
debug: (DEVMODE ? console.error : () => { }),
|
||||
messageCreate: async (message: Message) => {
|
||||
debug: DEVMODE ? (dmsg) => log(LT.LOG, `Debug Message | ${JSON.stringify(dmsg)}`) : undefined,
|
||||
messageCreate: (message: DiscordenoMessage) => {
|
||||
// Ignore all other bots
|
||||
if (message.author.bot) return;
|
||||
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) return;
|
||||
if (message.content.indexOf(config.prefix) !== 0) {
|
||||
// Handle @bot messages
|
||||
if (message.mentionedUserIds[0] === botId && (message.content.trim().startsWith(`<@${botId}>`) || message.content.trim().startsWith(`<@!${botId}>`))) {
|
||||
commands.handleMentions(message);
|
||||
}
|
||||
|
||||
// return as we are done handling this command
|
||||
return;
|
||||
}
|
||||
|
||||
log(LT.LOG, `Handling ${config.prefix}command message: ${JSON.stringify(message)}`);
|
||||
|
||||
// Split into standard command + args format
|
||||
const args = message.content.slice(config.prefix.length).trim().split(/ +/g);
|
||||
const args = message.content.slice(config.prefix.length).trim().split(/[ \n]+/g);
|
||||
const command = args.shift()?.toLowerCase();
|
||||
|
||||
// All commands below here
|
||||
|
||||
// [[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);
|
||||
m.edit(`Pong! Latency is ${m.timestamp - message.timestamp}ms.`);
|
||||
} catch (err) {
|
||||
console.error("Failed to send message 10", message, err);
|
||||
}
|
||||
}
|
||||
|
||||
// [[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);
|
||||
});
|
||||
}
|
||||
|
||||
// [[rollhelp or [[rh or [[hr
|
||||
// Help command specifically for the roll command
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
// [[help or [[h or [[?
|
||||
// Help command, prints from help file
|
||||
else if (command === "help" || command === "h" || command === "?") {
|
||||
// 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);
|
||||
});
|
||||
}
|
||||
|
||||
// [[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);
|
||||
});
|
||||
utils.sendIndirectMessage(message, "Failed command has been reported to my developer.\n\nFor more in depth support, and information about planned maintenance, please join the support server here: https://discord.gg/peHASXMZYv", sendMessage, sendDirectMessage).catch(err => {
|
||||
console.error("Failed to send message 51", message, err);
|
||||
});
|
||||
}
|
||||
|
||||
// [[stats or [[s
|
||||
// Displays stats on the bot
|
||||
else if (command === "stats" || command === "s") {
|
||||
// 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);
|
||||
});
|
||||
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.
|
||||
commands.ping(message);
|
||||
break;
|
||||
case 'rip':
|
||||
case 'memory':
|
||||
// [[rip [[memory
|
||||
// Displays a short message I wanted to include
|
||||
commands.rip(message);
|
||||
break;
|
||||
case 'rollhelp':
|
||||
case 'rh':
|
||||
case 'hr':
|
||||
case '??':
|
||||
// [[rollhelp or [[rh or [[hr or [[??
|
||||
// Help command specifically for the roll command
|
||||
commands.rollHelp(message);
|
||||
break;
|
||||
case 'rolldecorators':
|
||||
case 'rd':
|
||||
case 'dr':
|
||||
case '???':
|
||||
// [[rollDecorators or [[rd or [[dr or [[???
|
||||
// Help command specifically for the roll command decorators
|
||||
commands.rollDecorators(message);
|
||||
break;
|
||||
case 'help':
|
||||
case 'h':
|
||||
case '?':
|
||||
// [[help or [[h or [[?
|
||||
// Help command, prints from help file
|
||||
commands.help(message);
|
||||
break;
|
||||
case 'info':
|
||||
case 'i':
|
||||
// [[info or [[i
|
||||
// Info command, prints short desc on bot and some links
|
||||
commands.info(message);
|
||||
break;
|
||||
case 'privacy':
|
||||
// [[privacy
|
||||
// Privacy command, prints short desc on bot's privacy policy
|
||||
commands.privacy(message);
|
||||
break;
|
||||
case 'version':
|
||||
case 'v':
|
||||
// [[version or [[v
|
||||
// Returns version of the bot
|
||||
commands.version(message);
|
||||
break;
|
||||
case 'report':
|
||||
case 'r':
|
||||
// [[report or [[r (command that failed)
|
||||
// Manually report a failed roll
|
||||
commands.report(message, args);
|
||||
break;
|
||||
case 'stats':
|
||||
case 's':
|
||||
// [[stats or [[s
|
||||
// Displays stats on the bot
|
||||
commands.stats(message);
|
||||
break;
|
||||
case 'api':
|
||||
// [[api arg
|
||||
// API sub commands
|
||||
commands.api(message, args);
|
||||
break;
|
||||
case 'audit':
|
||||
// [[audit arg
|
||||
// Audit sub commands
|
||||
commands.audit(message, args);
|
||||
break;
|
||||
case 'heatmap':
|
||||
case 'hm':
|
||||
// [[heatmap or [[hm
|
||||
// Audit sub commands
|
||||
commands.heatmap(message);
|
||||
break;
|
||||
default:
|
||||
// Non-standard commands
|
||||
if (command?.startsWith('xdy')) {
|
||||
// [[xdydz (aka someone copy pasted the template as a roll)
|
||||
// Help command specifically for the roll command
|
||||
commands.rollHelp(message);
|
||||
} else if (command && (`${command}${args.join('')}`).indexOf(config.postfix) > -1) {
|
||||
// [[roll]]
|
||||
// Dice rolling commence!
|
||||
commands.roll(message, args, command);
|
||||
} else if (command) {
|
||||
// [[emoji or [[emojialias
|
||||
// Check if the unhandled command is an emoji request
|
||||
commands.emoji(message, command);
|
||||
}
|
||||
|
||||
// [[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);
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// [[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 => {
|
||||
console.error("Failed to send message 70", message, err);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Rest of this command is in a try-catch to protect all sends/edits from erroring out
|
||||
try {
|
||||
const originalCommand = config.prefix + command + " " + args.join(" ");
|
||||
|
||||
const m = await utils.sendIndirectMessage(message, "Rolling...", sendMessage, sendDirectMessage);
|
||||
|
||||
const modifiers = {
|
||||
noDetails: false,
|
||||
spoiler: "",
|
||||
maxRoll: false,
|
||||
nominalRoll: false,
|
||||
gmRoll: false,
|
||||
gms: <string[]>[],
|
||||
order: ""
|
||||
};
|
||||
|
||||
// Check if any of the args are command flags and pull those out into the modifiers object
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
switch (args[i].toLowerCase()) {
|
||||
case "-nd":
|
||||
modifiers.noDetails = true;
|
||||
|
||||
args.splice(i, 1);
|
||||
i--;
|
||||
break;
|
||||
case "-s":
|
||||
modifiers.spoiler = "||";
|
||||
|
||||
args.splice(i, 1);
|
||||
i--;
|
||||
break;
|
||||
case "-m":
|
||||
modifiers.maxRoll = true;
|
||||
|
||||
args.splice(i, 1);
|
||||
i--;
|
||||
break;
|
||||
case "-n":
|
||||
modifiers.nominalRoll = true;
|
||||
|
||||
args.splice(i, 1);
|
||||
i--;
|
||||
break;
|
||||
case "-gm":
|
||||
modifiers.gmRoll = true;
|
||||
|
||||
// -gm is a little more complex, as we must get all of the GMs that need to be DMd
|
||||
while (((i + 1) < args.length) && args[i + 1].startsWith("<@")) {
|
||||
// Keep looping thru the rest of the args until one does not start with the discord mention code
|
||||
modifiers.gms.push(args[i + 1].replace(/[!]/g, ""));
|
||||
args.splice((i + 1), 1);
|
||||
}
|
||||
if (modifiers.gms.length < 1) {
|
||||
// If -gm is on and none were found, throw an error
|
||||
m.edit("Error: Must specifiy at least one GM by mentioning them");
|
||||
|
||||
if (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);
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
args.splice(i, 1);
|
||||
i--;
|
||||
break;
|
||||
case "-o":
|
||||
args.splice(i, 1);
|
||||
|
||||
if (args[i].toLowerCase() !== "d" && args[i].toLowerCase() !== "a") {
|
||||
// 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 (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);
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
modifiers.order = args[i].toLowerCase();
|
||||
|
||||
args.splice(i, 1);
|
||||
i--;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// maxRoll and nominalRoll cannot both be on, throw an error
|
||||
if (modifiers.maxRoll && modifiers.nominalRoll) {
|
||||
m.edit("Error: Cannot maximise and nominise the roll at the same time");
|
||||
|
||||
if (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);
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Rejoin all of the args and send it into the solver, if solver returns a falsy item, an error object will be substituded in
|
||||
const rollCmd = command + " " + args.join(" ");
|
||||
const returnmsg = solver.parseRoll(rollCmd, config.prefix, config.postfix, modifiers.maxRoll, modifiers.nominalRoll, modifiers.order) || { error: true, errorCode: "EmptyMessage", errorMsg: "Error: Empty message", line1: "", line2: "", line3: "" };
|
||||
|
||||
let returnText = "";
|
||||
|
||||
// If there was an error, report it to the user in hopes that they can determine what they did wrong
|
||||
if (returnmsg.error) {
|
||||
returnText = returnmsg.errorMsg;
|
||||
m.edit(returnText);
|
||||
|
||||
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);
|
||||
});
|
||||
}
|
||||
return;
|
||||
} else {
|
||||
// Else format the output using details from the solver
|
||||
returnText = "<@" + message.author.id + ">" + returnmsg.line1 + "\n" + returnmsg.line2;
|
||||
|
||||
if (modifiers.noDetails) {
|
||||
returnText += "\nDetails suppressed by -nd flag.";
|
||||
} else {
|
||||
returnText += "\nDetails:\n" + modifiers.spoiler + returnmsg.line3 + modifiers.spoiler;
|
||||
}
|
||||
}
|
||||
|
||||
// If the roll was a GM roll, send DMs to all the GMs
|
||||
if (modifiers.gmRoll) {
|
||||
// Make a new return line to be sent to the roller
|
||||
const normalText = "<@" + message.author.id + ">" + returnmsg.line1 + "\nResults have been messaged to the following GMs: " + modifiers.gms.join(" ");
|
||||
|
||||
// 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" });
|
||||
|
||||
// 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(() => {
|
||||
utils.sendIndirectMessage(message, "WARNING: " + e + " could not be messaged. If this issue persists, make sure direct messages are allowed from this server.", sendMessage, sendDirectMessage);
|
||||
});
|
||||
});
|
||||
|
||||
// Finally send the text
|
||||
m.edit(normalText);
|
||||
|
||||
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);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// 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" });
|
||||
|
||||
// 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.";
|
||||
|
||||
// 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);
|
||||
} else {
|
||||
// Finally send the text
|
||||
m.edit(returnText);
|
||||
}
|
||||
|
||||
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);
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Something failed 71");
|
||||
}
|
||||
}
|
||||
|
||||
// [[emoji or [[emojialias
|
||||
// Check if the unhandled command is an emoji request
|
||||
else {
|
||||
// Start looping thru the possible emojis
|
||||
config.emojis.some((e: 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);
|
||||
});
|
||||
// And attempt to delete if needed
|
||||
if (e.deleteSender) {
|
||||
message.delete().catch(err => {
|
||||
console.error("Failed to delete message 41", message, err);
|
||||
});
|
||||
}
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Start up the command prompt for debug usage
|
||||
if (DEBUG) {
|
||||
utils.cmdPrompt(config.logChannel, config.name, sendMessage);
|
||||
utils.cmdPrompt(config.logChannel, config.name);
|
||||
}
|
||||
|
||||
// Start up the API for rolling from third party apps (like excel macros)
|
||||
if (config.api.enable) {
|
||||
api.start(dbClient, cache, sendMessage, sendDirectMessage);
|
||||
api.start();
|
||||
}
|
||||
|
|
908
src/api.ts
908
src/api.ts
|
@ -4,30 +4,21 @@
|
|||
* December 21, 2020
|
||||
*/
|
||||
|
||||
import config from '../config.ts';
|
||||
import {
|
||||
// Discordeno deps
|
||||
CacheData, Message, MessageContent,
|
||||
// Log4Deno deps
|
||||
log,
|
||||
LT,
|
||||
} from '../deps.ts';
|
||||
import { dbClient } from './db.ts';
|
||||
import endpoints from './endpoints/_index.ts';
|
||||
import stdResp from './endpoints/stdResponses.ts';
|
||||
|
||||
// MySQL Driver deps
|
||||
Client,
|
||||
|
||||
// httpd deps
|
||||
serve,
|
||||
Status, STATUS_TEXT,
|
||||
|
||||
// nanoid deps
|
||||
nanoid
|
||||
} from "../deps.ts";
|
||||
|
||||
import solver from "./solver.ts";
|
||||
|
||||
import config from "../config.ts";
|
||||
|
||||
// start(databaseClient, botCache, sendMessage, sendDirectMessage) returns nothing
|
||||
// start() returns nothing
|
||||
// start initializes and runs the entire API for the bot
|
||||
const start = async (dbClient: Client, cache: CacheData, sendMessage: (c: string, m: (string | MessageContent)) => Promise<Message>, sendDirectMessage: (c: string, m: (string | MessageContent)) => Promise<Message>): Promise<void> => {
|
||||
const server = serve({ hostname: "0.0.0.0", port: config.api.port });
|
||||
console.log(`HTTP api running at: http://localhost:${config.api.port}/`);
|
||||
const start = async (): Promise<void> => {
|
||||
const server = Deno.listen({ port: config.api.port });
|
||||
log(LT.INFO, `HTTP api running at: http://localhost:${config.api.port}/`);
|
||||
|
||||
// rateLimitTime holds all users with the last time they started a rate limit timer
|
||||
const rateLimitTime = new Map<string, number>();
|
||||
|
@ -35,707 +26,196 @@ const start = async (dbClient: Client, cache: CacheData, sendMessage: (c: string
|
|||
const rateLimitCnt = new Map<string, number>();
|
||||
|
||||
// Catching every request made to the server
|
||||
for await (const request of server) {
|
||||
// Check if user is authenticated to be using this API
|
||||
let authenticated = false;
|
||||
let rateLimited = false;
|
||||
let updateRateLimitTime = false;
|
||||
let apiUserid = 0n;
|
||||
let apiUseridStr = "";
|
||||
let apiUserEmail = "";
|
||||
let apiUserDelCode = "";
|
||||
for await (const conn of server) {
|
||||
(async () => {
|
||||
const httpConn = Deno.serveHttp(conn);
|
||||
for await (const requestEvent of httpConn) {
|
||||
const request = requestEvent.request;
|
||||
log(LT.LOG, `Handling request: ${JSON.stringify(request.headers)} | ${JSON.stringify(request.method)} | ${JSON.stringify(request.url)}`);
|
||||
// Check if user is authenticated to be using this API
|
||||
let authenticated = false;
|
||||
let rateLimited = false;
|
||||
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, email, deleteCode FROM all_keys WHERE apiKey = ? AND active = 1 AND banned = 0", [request.headers.get("X-Api-Key")]);
|
||||
// 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, 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 = BigInt(dbApiQuery[0].userid);
|
||||
apiUserEmail = dbApiQuery[0].email;
|
||||
apiUserDelCode = dbApiQuery[0].deleteCode;
|
||||
authenticated = true;
|
||||
// If only one user returned, is not banned, and is currently active, mark as authenticated
|
||||
if (dbApiQuery.length === 1) {
|
||||
apiUserid = BigInt(dbApiQuery[0].userid);
|
||||
apiUserEmail = dbApiQuery[0].email;
|
||||
apiUserDelCode = dbApiQuery[0].deleteCode;
|
||||
authenticated = true;
|
||||
|
||||
// Rate limiting inits
|
||||
apiUseridStr = apiUserid.toString();
|
||||
const apiTimeNow = new Date().getTime();
|
||||
// Rate limiting inits
|
||||
apiUseridStr = apiUserid.toString();
|
||||
const apiTimeNow = new Date().getTime();
|
||||
|
||||
// Check if user has sent a request recently
|
||||
if (rateLimitTime.has(apiUseridStr) && (((rateLimitTime.get(apiUseridStr) || 0) + config.api.rateLimitTime) > apiTimeNow)) {
|
||||
// Get current count
|
||||
const currentCnt = rateLimitCnt.get(apiUseridStr) || 0;
|
||||
if (currentCnt < config.api.rateLimitCnt) {
|
||||
// Limit not yet exceeded, update count
|
||||
rateLimitCnt.set(apiUseridStr, (currentCnt + 1));
|
||||
} else {
|
||||
// Limit exceeded, prevent API use
|
||||
rateLimited = true;
|
||||
// Check if user has sent a request recently
|
||||
if (rateLimitTime.has(apiUseridStr) && (((rateLimitTime.get(apiUseridStr) || 0) + config.api.rateLimitTime) > apiTimeNow)) {
|
||||
// Get current count
|
||||
const currentCnt = rateLimitCnt.get(apiUseridStr) || 0;
|
||||
if (currentCnt < config.api.rateLimitCnt) {
|
||||
// Limit not yet exceeded, update count
|
||||
rateLimitCnt.set(apiUseridStr, currentCnt + 1);
|
||||
} else {
|
||||
// Limit exceeded, prevent API use
|
||||
rateLimited = true;
|
||||
}
|
||||
} else {
|
||||
// Update the maps
|
||||
updateRateLimitTime = true;
|
||||
rateLimitCnt.set(apiUseridStr, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!rateLimited) {
|
||||
// Get path and query as a string
|
||||
const [urlPath, tempQ] = request.url.split('?');
|
||||
const path = urlPath.split('api')[1];
|
||||
|
||||
// Turn the query into a map (if it exists)
|
||||
const query = new Map<string, string>();
|
||||
if (tempQ !== undefined) {
|
||||
tempQ.split('&').forEach((e: string) => {
|
||||
log(LT.LOG, `Parsing request query ${request} ${e}`);
|
||||
const [option, params] = e.split('=');
|
||||
query.set(option.toLowerCase(), params);
|
||||
});
|
||||
}
|
||||
|
||||
if (path) {
|
||||
if (authenticated) {
|
||||
// Handle the authenticated request
|
||||
switch (request.method) {
|
||||
case 'GET':
|
||||
switch (path.toLowerCase()) {
|
||||
case '/key':
|
||||
case '/key/':
|
||||
endpoints.get.apiKeyAdmin(requestEvent, query, apiUserid);
|
||||
break;
|
||||
case '/channel':
|
||||
case '/channel/':
|
||||
endpoints.get.apiChannel(requestEvent, query, apiUserid);
|
||||
break;
|
||||
case '/roll':
|
||||
case '/roll/':
|
||||
endpoints.get.apiRoll(requestEvent, query, apiUserid);
|
||||
break;
|
||||
default:
|
||||
// Alert API user that they messed up
|
||||
requestEvent.respondWith(stdResp.NotFound('Auth Get'));
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case 'POST':
|
||||
switch (path.toLowerCase()) {
|
||||
case '/channel/add':
|
||||
case '/channel/add/':
|
||||
endpoints.post.apiChannelAdd(requestEvent, query, apiUserid);
|
||||
break;
|
||||
default:
|
||||
// Alert API user that they messed up
|
||||
requestEvent.respondWith(stdResp.NotFound('Auth Post'));
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case 'PUT':
|
||||
switch (path.toLowerCase()) {
|
||||
case '/key/ban':
|
||||
case '/key/ban/':
|
||||
case '/key/unban':
|
||||
case '/key/unban/':
|
||||
case '/key/activate':
|
||||
case '/key/activate/':
|
||||
case '/key/deactivate':
|
||||
case '/key/deactivate/':
|
||||
endpoints.put.apiKeyManage(requestEvent, query, apiUserid, path);
|
||||
break;
|
||||
case '/channel/ban':
|
||||
case '/channel/ban/':
|
||||
case '/channel/unban':
|
||||
case '/channel/unban/':
|
||||
endpoints.put.apiChannelManageBan(requestEvent, query, apiUserid, path);
|
||||
break;
|
||||
case '/channel/activate':
|
||||
case '/channel/activate/':
|
||||
case '/channel/deactivate':
|
||||
case '/channel/deactivate/':
|
||||
endpoints.put.apiChannelManageActive(requestEvent, query, apiUserid, path);
|
||||
break;
|
||||
default:
|
||||
// Alert API user that they messed up
|
||||
requestEvent.respondWith(stdResp.NotFound('Auth Put'));
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case 'DELETE':
|
||||
switch (path.toLowerCase()) {
|
||||
case '/key/delete':
|
||||
case '/key/delete/':
|
||||
endpoints.delete.apiKeyDelete(requestEvent, query, apiUserid, apiUserEmail, apiUserDelCode);
|
||||
break;
|
||||
default:
|
||||
// Alert API user that they messed up
|
||||
requestEvent.respondWith(stdResp.NotFound('Auth Del'));
|
||||
break;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
// Alert API user that they messed up
|
||||
requestEvent.respondWith(stdResp.MethodNotAllowed('Auth'));
|
||||
break;
|
||||
}
|
||||
|
||||
// Update rate limit details
|
||||
if (updateRateLimitTime) {
|
||||
const apiTimeNow = new Date().getTime();
|
||||
rateLimitTime.set(apiUseridStr, apiTimeNow);
|
||||
}
|
||||
} else if (!authenticated) {
|
||||
// Handle the unathenticated request
|
||||
switch (request.method) {
|
||||
case 'GET':
|
||||
switch (path.toLowerCase()) {
|
||||
case '/key':
|
||||
case '/key/':
|
||||
endpoints.get.apiKey(requestEvent, query);
|
||||
break;
|
||||
case '/heatmap.png':
|
||||
endpoints.get.heatmapPng(requestEvent);
|
||||
break;
|
||||
default:
|
||||
// Alert API user that they messed up
|
||||
requestEvent.respondWith(stdResp.NotFound('NoAuth Get'));
|
||||
break;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
// Alert API user that they messed up
|
||||
requestEvent.respondWith(stdResp.MethodNotAllowed('NoAuth'));
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
requestEvent.respondWith(stdResp.Forbidden('What are you trying to do?'));
|
||||
}
|
||||
} else if (authenticated && rateLimited) {
|
||||
// Alert API user that they are doing this too often
|
||||
requestEvent.respondWith(stdResp.TooManyRequests('Slow down, servers are expensive and this bot is free to use.'));
|
||||
} else {
|
||||
// Update the maps
|
||||
updateRateLimitTime = true;
|
||||
rateLimitCnt.set(apiUseridStr, 1);
|
||||
// Alert API user that they shouldn't be doing this
|
||||
requestEvent.respondWith(stdResp.Forbidden('Why are you here?'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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("a") && ((query.get("a") || "").length > 0))) {
|
||||
if (apiUserid === config.api.admin && apiUserid === BigInt(query.get("a"))) {
|
||||
// Generate new secure key
|
||||
const newKey = await nanoid(25);
|
||||
|
||||
// Flag to see if there is an error inside the catch
|
||||
let erroredOut = false;
|
||||
|
||||
// Insert new key/user pair into the db
|
||||
await dbClient.execute("INSERT INTO all_keys(userid,apiKey) values(?,?)", [apiUserid, newKey]).catch(() => {
|
||||
console.log("Failed to insert into database 20");
|
||||
request.respond({ status: Status.InternalServerError, body: STATUS_TEXT.get(Status.InternalServerError) });
|
||||
erroredOut = true;
|
||||
});
|
||||
|
||||
// Exit this case now if catch errored
|
||||
if (erroredOut) {
|
||||
break;
|
||||
} else {
|
||||
// Send API key as response
|
||||
request.respond({ status: Status.OK, body: JSON.stringify({ "key": newKey, "userid": query.get("user") }) });
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
// Only allow the db admin to use this API
|
||||
request.respond({ status: Status.Forbidden, body: STATUS_TEXT.get(Status.Forbidden) });
|
||||
}
|
||||
} else {
|
||||
// Alert API user that they messed up
|
||||
request.respond({ status: Status.BadRequest, body: STATUS_TEXT.get(Status.BadRequest) });
|
||||
}
|
||||
break;
|
||||
case "/api/channel":
|
||||
case "/api/channel/":
|
||||
if (query.has("user") && ((query.get("user") || "").length > 0)) {
|
||||
if (apiUserid === BigInt(query.get("user"))) {
|
||||
// Flag to see if there is an error inside the catch
|
||||
let erroredOut = false;
|
||||
|
||||
// Get all channels userid has authorized
|
||||
const dbAllowedChannelQuery = await dbClient.query("SELECT * FROM allowed_channels WHERE userid = ?", [apiUserid]).catch(() => {
|
||||
console.log("Failed to query database 22");
|
||||
request.respond({ status: Status.InternalServerError, body: STATUS_TEXT.get(Status.InternalServerError) });
|
||||
erroredOut = true;
|
||||
});
|
||||
|
||||
if (erroredOut) {
|
||||
break;
|
||||
} else {
|
||||
// Customized strinification to handle BigInts correctly
|
||||
const returnChannels = JSON.stringify(dbAllowedChannelQuery, (_key, value) => (typeof value === 'bigint' ? value.toString() : value));
|
||||
// Send API key as response
|
||||
request.respond({ status: Status.OK, body: returnChannels });
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
// Alert API user that they shouldn't be doing this
|
||||
request.respond({ status: Status.Forbidden, body: STATUS_TEXT.get(Status.Forbidden) });
|
||||
}
|
||||
} else {
|
||||
// Alert API user that they messed up
|
||||
request.respond({ status: Status.BadRequest, body: STATUS_TEXT.get(Status.BadRequest) });
|
||||
}
|
||||
break;
|
||||
case "/api/roll":
|
||||
case "/api/roll/":
|
||||
// Make sure query contains all the needed parts
|
||||
if ((query.has("rollstr") && ((query.get("rollstr") || "").length > 0)) && (query.has("channel") && ((query.get("channel") || "").length > 0)) && (query.has("user") && ((query.get("user") || "").length > 0))) {
|
||||
if (query.has("n") && query.has("m")) {
|
||||
// Alert API user that they shouldn't be doing this
|
||||
request.respond({ status: Status.BadRequest, body: STATUS_TEXT.get(Status.BadRequest) });
|
||||
break;
|
||||
}
|
||||
|
||||
// Check if user is authenticated to use this endpoint
|
||||
let authorized = false;
|
||||
|
||||
// Check if the db has the requested userid/channelid combo, and that the requested userid matches the userid linked with the api key
|
||||
const dbChannelQuery = await dbClient.query("SELECT active, banned FROM allowed_channels WHERE userid = ? AND channelid = ?", [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
|
||||
try {
|
||||
// Flag to tell if roll was completely successful
|
||||
let errorOut = false;
|
||||
// Make sure rollCmd is not undefined
|
||||
let rollCmd = query.get("rollstr") || "";
|
||||
const originalCommand = query.get("rollstr");
|
||||
|
||||
if (rollCmd.length === 0) {
|
||||
// Alert API user that they messed up
|
||||
request.respond({ status: Status.BadRequest, body: STATUS_TEXT.get(Status.BadRequest) });
|
||||
|
||||
// Always log API rolls for abuse detection
|
||||
dbClient.execute("INSERT INTO roll_log(input,result,resultid,api,error) values(?,?,?,1,1)", [originalCommand, "EmptyInput", null]).catch(() => {
|
||||
console.log("Failed to insert into database 10");
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
if (query.has("o") && (query.get("o")?.toLowerCase() !== "d" && query.get("o")?.toLowerCase() !== "a")) {
|
||||
// Alert API user that they messed up
|
||||
request.respond({ status: Status.BadRequest, body: STATUS_TEXT.get(Status.BadRequest) });
|
||||
|
||||
// Always log API rolls for abuse detection
|
||||
dbClient.execute("INSERT INTO roll_log(input,result,resultid,api,error) values(?,?,?,1,1)", [originalCommand, "BadOrder", null]).catch(() => {
|
||||
console.log("Failed to insert into database 10");
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
// Clip off the leading prefix. API calls must be formatted with a prefix at the start to match how commands are sent in Discord
|
||||
rollCmd = rollCmd.substr(rollCmd.indexOf(config.prefix) + 2).replace(/%20/g, " ");
|
||||
|
||||
// Parse the roll and get the return text
|
||||
const returnmsg = solver.parseRoll(rollCmd, config.prefix, config.postfix, query.has("m"), query.has("n"), query.has("o") ? (query.get("o")?.toLowerCase() || "") : "");
|
||||
|
||||
// Alert users why this message just appeared and how they can report abues pf this feature
|
||||
const apiPrefix = "The following roll was conducted using my built in API. If someone in this channel did not request this roll, please report API abuse here: <" + config.api.supportURL + ">\n\n";
|
||||
let m, returnText = "";
|
||||
|
||||
// Handle sending the error message to whoever called the api
|
||||
if (returnmsg.error) {
|
||||
request.respond({ status: Status.InternalServerError, body: returnmsg.errorMsg });
|
||||
|
||||
// Always log API rolls for abuse detection
|
||||
dbClient.execute("INSERT INTO roll_log(input,result,resultid,api,error) values(?,?,?,1,1)", [originalCommand, returnmsg.errorCode, null]).catch(() => {
|
||||
console.log("Failed to insert into database 11");
|
||||
});
|
||||
break;
|
||||
} else {
|
||||
returnText = apiPrefix + "<@" + query.get("user") + ">" + returnmsg.line1 + "\n" + returnmsg.line2;
|
||||
let spoilerTxt = "";
|
||||
|
||||
// Determine if spoiler flag was on
|
||||
if (query.has("s")) {
|
||||
spoilerTxt = "||";
|
||||
}
|
||||
|
||||
// Determine if no details flag was on
|
||||
if (query.has("nd")) {
|
||||
returnText += "\nDetails suppressed by nd query.";
|
||||
} else {
|
||||
returnText += "\nDetails:\n" + spoilerTxt + returnmsg.line3 + spoilerTxt;
|
||||
}
|
||||
}
|
||||
|
||||
// If the roll was a GM roll, send DMs to all the GMs
|
||||
if (query.has("gms")) {
|
||||
// Get all the GM user IDs from the query
|
||||
const gms = (query.get("gms") || "").split(",");
|
||||
if (gms.length === 0) {
|
||||
// Alert API user that they messed up
|
||||
request.respond({ status: Status.BadRequest, body: STATUS_TEXT.get(Status.BadRequest) });
|
||||
|
||||
// Always log API rolls for abuse detection
|
||||
dbClient.execute("INSERT INTO roll_log(input,result,resultid,api,error) values(?,?,?,1,1)", [originalCommand, "NoGMsSent", null]).catch(() => {
|
||||
console.log("Failed to insert into database 12");
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
// Make a new return line to be sent to the roller
|
||||
let normalText = apiPrefix + "<@" + query.get("user") + ">" + returnmsg.line1 + "\nResults have been messaged to the following GMs: ";
|
||||
gms.forEach(e => {
|
||||
normalText += "<@" + e + "> ";
|
||||
});
|
||||
|
||||
// Send the return message as a DM or normal message depening on if the channel is set
|
||||
if ((query.get("channel") || "").length > 0) {
|
||||
m = await sendMessage(query.get("channel") || "", normalText).catch(() => {
|
||||
request.respond({ status: Status.InternalServerError, body: "Message 00 failed to send." });
|
||||
errorOut = true;
|
||||
});
|
||||
} else {
|
||||
m = await sendDirectMessage(query.get("user") || "", normalText).catch(() => {
|
||||
request.respond({ status: Status.InternalServerError, body: "Message 01 failed to send." });
|
||||
errorOut = true;
|
||||
});
|
||||
}
|
||||
|
||||
// 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" });
|
||||
|
||||
// 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 () => {
|
||||
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) {
|
||||
m = await sendMessage(query.get("channel") || "", failedSend).catch(() => {
|
||||
request.respond({ status: Status.InternalServerError, body: "Message 10 failed to send." });
|
||||
errorOut = true;
|
||||
});
|
||||
} else {
|
||||
m = await sendDirectMessage(query.get("user") || "", failedSend).catch(() => {
|
||||
request.respond({ status: Status.InternalServerError, body: "Message 11 failed to send." });
|
||||
errorOut = true;
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Always log API rolls for abuse detection
|
||||
dbClient.execute("INSERT INTO roll_log(input,result,resultid,api,error) values(?,?,?,1,0)", [originalCommand, returnText, ((typeof m === "object") ? m.id : null)]).catch(() => {
|
||||
console.log("Failed to insert into database 13");
|
||||
});
|
||||
|
||||
// Handle closing the request out
|
||||
if (errorOut) {
|
||||
break;
|
||||
} else {
|
||||
request.respond({ status: Status.OK, body: normalText });
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
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" });
|
||||
|
||||
// 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" };
|
||||
}
|
||||
|
||||
// Send the return message as a DM or normal message depening on if the channel is set
|
||||
if ((query.get("channel") || "").length > 0) {
|
||||
m = await sendMessage(query.get("channel") || "", newMessage).catch(() => {
|
||||
request.respond({ status: Status.InternalServerError, body: "Message 20 failed to send." });
|
||||
errorOut = true;
|
||||
});
|
||||
} else {
|
||||
m = await sendDirectMessage(query.get("user") || "", newMessage).catch(() => {
|
||||
request.respond({ status: Status.InternalServerError, body: "Message 21 failed to send." });
|
||||
errorOut = true;
|
||||
});
|
||||
}
|
||||
|
||||
// If enabled, log rolls so we can verify the bots math
|
||||
dbClient.execute("INSERT INTO roll_log(input,result,resultid,api,error) values(?,?,?,1,0)", [originalCommand, returnText, ((typeof m === "object") ? m.id : null)]).catch(() => {
|
||||
console.log("Failed to insert into database 14");
|
||||
});
|
||||
|
||||
// Handle closing the request out
|
||||
if (errorOut) {
|
||||
break;
|
||||
} else {
|
||||
request.respond({ status: Status.OK, body: returnText });
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
// Handle any errors we missed
|
||||
console.log(err)
|
||||
request.respond({ status: Status.InternalServerError, body: STATUS_TEXT.get(Status.InternalServerError) });
|
||||
}
|
||||
} else {
|
||||
// Alert API user that they messed up
|
||||
request.respond({ status: Status.Forbidden, body: STATUS_TEXT.get(Status.Forbidden) });
|
||||
}
|
||||
} else {
|
||||
// Alert API user that they shouldn't be doing this
|
||||
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 "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(() => {
|
||||
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(() => {
|
||||
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(() => {
|
||||
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(() => {
|
||||
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(() => {
|
||||
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(() => {
|
||||
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(() => {
|
||||
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) });
|
||||
break;
|
||||
}
|
||||
|
||||
if (updateRateLimitTime) {
|
||||
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(() => {
|
||||
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) });
|
||||
} else {
|
||||
// Alert API user that they shouldn't be doing this
|
||||
request.respond({ status: Status.Forbidden, body: STATUS_TEXT.get(Status.Forbidden) });
|
||||
}
|
||||
})();
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -0,0 +1,316 @@
|
|||
import config from '../config.ts';
|
||||
import { CountDetails, SolvedRoll } from './solver/solver.d.ts';
|
||||
import { RollModifiers } from './mod.d.ts';
|
||||
|
||||
export const failColor = 0xe71212;
|
||||
export const warnColor = 0xe38f28;
|
||||
export const successColor = 0x0f8108;
|
||||
export const infoColor1 = 0x313bf9;
|
||||
export const infoColor2 = 0x6805e9;
|
||||
|
||||
export const rollingEmbed = {
|
||||
embeds: [{
|
||||
color: infoColor1,
|
||||
title: 'Rolling . . .',
|
||||
}],
|
||||
};
|
||||
|
||||
export const generatePing = (time: number) => ({
|
||||
embeds: [{
|
||||
color: infoColor1,
|
||||
title: time === -1 ? 'Ping?' : `Pong! Latency is ${time}ms.`,
|
||||
}],
|
||||
});
|
||||
|
||||
export const generateReport = (msg: string) => ({
|
||||
embeds: [{
|
||||
color: infoColor2,
|
||||
title: 'USER REPORT:',
|
||||
description: msg || 'No message',
|
||||
}],
|
||||
});
|
||||
|
||||
export const generateStats = (guildCount: number, channelCount: number, memberCount: number, rollCount: bigint, utilityCount: bigint, rollRate: number, utilityRate: number) => ({
|
||||
embeds: [{
|
||||
color: infoColor2,
|
||||
title: 'The Artificer\'s Statistics:',
|
||||
timestamp: new Date().toISOString(),
|
||||
fields: [
|
||||
{
|
||||
name: 'Guilds:',
|
||||
value: `${guildCount}`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: 'Channels:',
|
||||
value: `${channelCount}`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: 'Active Members:',
|
||||
value: `${memberCount}`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: 'Roll Commands:',
|
||||
value: `${rollCount}
|
||||
(${rollRate.toFixed(2)} per hour)`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: 'Utility Commands:',
|
||||
value: `${utilityCount}
|
||||
(${utilityRate.toFixed(2)} per hour)`,
|
||||
inline: true,
|
||||
},
|
||||
],
|
||||
}],
|
||||
});
|
||||
|
||||
export const generateApiFailed = (args: string) => ({
|
||||
embeds: [{
|
||||
color: failColor,
|
||||
title: `Failed to ${args} API rolls for this guild.`,
|
||||
description: 'If this issue persists, please report this to the developers.',
|
||||
}],
|
||||
});
|
||||
|
||||
export const generateApiStatus = (banned: boolean, active: boolean) => {
|
||||
const apiStatus = active ? 'allowed' : 'blocked from being used';
|
||||
return {
|
||||
embeds: [{
|
||||
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) => ({
|
||||
embeds: [{
|
||||
color: successColor,
|
||||
title: `API rolls have successfully been ${args} for this guild.`,
|
||||
}],
|
||||
});
|
||||
|
||||
export const generateDMFailed = (user: string) => ({
|
||||
embeds: [{
|
||||
color: failColor,
|
||||
title: `WARNING: ${user} could not be messaged.`,
|
||||
description: 'If this issue persists, make sure direct messages are allowed from this server.',
|
||||
}],
|
||||
});
|
||||
|
||||
export const generateApiKeyEmail = (email: string, key: string) => ({
|
||||
content: `<@${config.api.admin}> A USER HAS REQUESTED AN API KEY`,
|
||||
embeds: [{
|
||||
color: infoColor1,
|
||||
fields: [
|
||||
{
|
||||
name: 'Send to:',
|
||||
value: email,
|
||||
},
|
||||
{
|
||||
name: 'Subject:',
|
||||
value: 'Artificer API Key',
|
||||
},
|
||||
{
|
||||
name: 'Body:',
|
||||
value: `Hello Artificer API User,
|
||||
|
||||
Welcome aboard The Artificer's API. You can find full details about the API on the GitHub: https://github.com/Burn-E99/TheArtificer
|
||||
|
||||
Your API Key is: ${key}
|
||||
|
||||
Guard this well, as there is zero tolerance for API abuse.
|
||||
|
||||
Welcome aboard,
|
||||
The Artificer Developer - Ean Milligan`,
|
||||
},
|
||||
],
|
||||
}],
|
||||
});
|
||||
|
||||
export const generateApiDeleteEmail = (email: string, deleteCode: string) => ({
|
||||
content: `<@${config.api.admin}> A USER HAS REQUESTED A DELETE CODE`,
|
||||
embeds: [{
|
||||
color: infoColor1,
|
||||
fields: [
|
||||
{
|
||||
name: 'Send to:',
|
||||
value: email,
|
||||
},
|
||||
{
|
||||
name: 'Subject:',
|
||||
value: 'Artificer API Delete Code',
|
||||
},
|
||||
{
|
||||
name: 'Body:',
|
||||
value: `Hello Artificer API User,
|
||||
|
||||
I am sorry to see you go. If you would like, please respond to this email detailing what I could have done better.
|
||||
|
||||
As requested, here is your delete code: ${deleteCode}
|
||||
|
||||
Sorry to see you go,
|
||||
The Artificer Developer - Ean Milligan`,
|
||||
},
|
||||
],
|
||||
}],
|
||||
});
|
||||
|
||||
export const generateRollError = (errorType: string, errorMsg: string) => ({
|
||||
embeds: [{
|
||||
color: failColor,
|
||||
title: 'Roll command encountered the following error:',
|
||||
fields: [{
|
||||
name: errorType,
|
||||
value: `${errorMsg}\n\nPlease try again. If the error is repeated, please report the issue using the \`${config.prefix}report\` command.`,
|
||||
}],
|
||||
}],
|
||||
});
|
||||
|
||||
export const generateCountDetailsEmbed = (counts: CountDetails) => ({
|
||||
color: infoColor1,
|
||||
title: 'Roll Count Details:',
|
||||
fields: [
|
||||
{
|
||||
name: 'Total Rolls:',
|
||||
value: `${counts.total}`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: 'Successful Rolls:',
|
||||
value: `${counts.successful}`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: 'Failed Rolls:',
|
||||
value: `${counts.failed}`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: 'Rerolled Dice:',
|
||||
value: `${counts.rerolled}`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: 'Dropped Dice:',
|
||||
value: `${counts.dropped}`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: 'Exploded Dice:',
|
||||
value: `${counts.exploded}`,
|
||||
inline: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
export const generateRollEmbed = async (authorId: bigint, returnDetails: SolvedRoll, modifiers: RollModifiers) => {
|
||||
if (returnDetails.error) {
|
||||
// Roll had an error, send error embed
|
||||
return {
|
||||
embed: {
|
||||
color: failColor,
|
||||
title: 'Roll failed:',
|
||||
description: `${returnDetails.errorMsg}`,
|
||||
footer: {
|
||||
text: `Code: ${returnDetails.errorCode}`,
|
||||
},
|
||||
},
|
||||
hasAttachment: false,
|
||||
attachment: {
|
||||
'blob': await new Blob(['' as BlobPart], { 'type': 'text' }),
|
||||
'name': 'rollDetails.txt',
|
||||
},
|
||||
};
|
||||
} else {
|
||||
if (modifiers.gmRoll) {
|
||||
// Roll is a GM Roll, send this in the pub channel (this funciton will be ran again to get details for the GMs)
|
||||
return {
|
||||
embed: {
|
||||
color: infoColor2,
|
||||
description: `<@${authorId}>${returnDetails.line1}
|
||||
|
||||
Results have been messaged to the following GMs: ${modifiers.gms.join(' ')}`,
|
||||
},
|
||||
hasAttachment: false,
|
||||
attachment: {
|
||||
'blob': await new Blob(['' as BlobPart], { 'type': 'text' }),
|
||||
'name': 'rollDetails.txt',
|
||||
},
|
||||
};
|
||||
} else {
|
||||
// Roll is normal, make normal embed
|
||||
const line2Details = returnDetails.line2.split(': ');
|
||||
let details = '';
|
||||
|
||||
if (!modifiers.superNoDetails) {
|
||||
if (modifiers.noDetails) {
|
||||
details = `**Details:**
|
||||
Suppressed by -nd flag`;
|
||||
} else {
|
||||
details = `**Details:**
|
||||
${modifiers.spoiler}${returnDetails.line3}${modifiers.spoiler}`;
|
||||
}
|
||||
}
|
||||
|
||||
const baseDesc = `<@${authorId}>${returnDetails.line1}
|
||||
**${line2Details.shift()}:**
|
||||
${line2Details.join(': ')}`;
|
||||
|
||||
if (baseDesc.length + details.length < 4090) {
|
||||
return {
|
||||
embed: {
|
||||
color: infoColor2,
|
||||
description: `${baseDesc}
|
||||
|
||||
${details}`,
|
||||
},
|
||||
hasAttachment: false,
|
||||
attachment: {
|
||||
'blob': await new Blob(['' as BlobPart], { 'type': 'text' }),
|
||||
'name': 'rollDetails.txt',
|
||||
},
|
||||
};
|
||||
} else {
|
||||
// If its too big, collapse it into a .txt file and send that instead.
|
||||
const b = await new Blob([`${baseDesc}\n\n${details}` as BlobPart], { 'type': 'text' });
|
||||
details = 'Details have been ommitted from this message for being over 2000 characters.';
|
||||
if (b.size > 8388290) {
|
||||
details +=
|
||||
'\n\nFull details could not be attached to this messaged as a \`.txt\` file as the file would be too large for Discord to handle. If you would like to see the details of rolls, please send the rolls in multiple messages instead of bundled into one.';
|
||||
return {
|
||||
embed: {
|
||||
color: infoColor2,
|
||||
description: `${baseDesc}
|
||||
|
||||
${details}`,
|
||||
},
|
||||
hasAttachment: false,
|
||||
attachment: {
|
||||
'blob': await new Blob(['' as BlobPart], { 'type': 'text' }),
|
||||
'name': 'rollDetails.txt',
|
||||
},
|
||||
};
|
||||
} else {
|
||||
details += '\n\nFull details have been attached to this messaged as a \`.txt\` file for verification purposes.';
|
||||
return {
|
||||
embed: {
|
||||
color: infoColor2,
|
||||
description: `${baseDesc}
|
||||
|
||||
${details}`,
|
||||
},
|
||||
hasAttachment: true,
|
||||
attachment: {
|
||||
'blob': b,
|
||||
'name': 'rollDetails.txt',
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
|
@ -0,0 +1,39 @@
|
|||
import { ping } from './ping.ts';
|
||||
import { rip } from './rip.ts';
|
||||
import { rollHelp } from './rollHelp.ts';
|
||||
import { rollDecorators } from './rollDecorators.ts';
|
||||
import { help } from './help.ts';
|
||||
import { info } from './info.ts';
|
||||
import { privacy } from './privacy.ts';
|
||||
import { version } from './version.ts';
|
||||
import { report } from './report.ts';
|
||||
import { stats } from './stats.ts';
|
||||
import { api } from './apiCmd.ts';
|
||||
import { emoji } from './emoji.ts';
|
||||
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,
|
||||
rip,
|
||||
rollHelp,
|
||||
rollDecorators,
|
||||
help,
|
||||
info,
|
||||
privacy,
|
||||
version,
|
||||
report,
|
||||
stats,
|
||||
api,
|
||||
emoji,
|
||||
roll,
|
||||
handleMentions,
|
||||
audit,
|
||||
heatmap,
|
||||
optOut,
|
||||
optIn,
|
||||
};
|
|
@ -0,0 +1,74 @@
|
|||
import { dbClient, queries } from '../db.ts';
|
||||
import {
|
||||
// Discordeno deps
|
||||
DiscordenoMessage,
|
||||
hasGuildPermissions,
|
||||
} from '../../deps.ts';
|
||||
import apiCommands from './apiCmd/_index.ts';
|
||||
import { failColor } from '../commandUtils.ts';
|
||||
import utils from '../utils.ts';
|
||||
|
||||
export const api = async (message: DiscordenoMessage, args: string[]) => {
|
||||
// Light telemetry to see how many times a command is being run
|
||||
dbClient.execute(queries.callIncCnt('api')).catch((e) => utils.commonLoggers.dbError('apiCmd.ts:16', 'call sproc INC_CNT on', e));
|
||||
|
||||
// Local apiArg in lowercase
|
||||
const apiArg = (args[0] || 'help').toLowerCase();
|
||||
|
||||
// Alert users who DM the bot that this command is for guilds only
|
||||
if (message.guildId === 0n) {
|
||||
message.send({
|
||||
embeds: [{
|
||||
color: failColor,
|
||||
title: 'API commands are only available in guilds.',
|
||||
}],
|
||||
}).catch((e: Error) => utils.commonLoggers.messageSendError('apiCmd.ts:30', message, e));
|
||||
return;
|
||||
}
|
||||
|
||||
// Makes sure the user is authenticated to run the API command
|
||||
if (await hasGuildPermissions(message.authorId, message.guildId, ['ADMINISTRATOR'])) {
|
||||
switch (apiArg) {
|
||||
case 'help':
|
||||
case 'h':
|
||||
// [[api help
|
||||
// Shows API help details
|
||||
apiCommands.help(message);
|
||||
break;
|
||||
case 'allow':
|
||||
case 'block':
|
||||
case 'enable':
|
||||
case 'disable':
|
||||
// [[api allow/block
|
||||
// Lets a guild admin allow or ban API rolls from happening in said guild
|
||||
apiCommands.allowBlock(message, apiArg);
|
||||
break;
|
||||
case 'delete':
|
||||
// [[api delete
|
||||
// Lets a guild admin delete their server from the database
|
||||
apiCommands.deleteGuild(message);
|
||||
break;
|
||||
case 'status':
|
||||
// [[api status
|
||||
// Lets a guild admin check the status of API rolling in said guild
|
||||
apiCommands.status(message);
|
||||
break;
|
||||
case 'show-warn':
|
||||
case 'hide-warn':
|
||||
// [[api show-warn/hide-warn
|
||||
// Lets a guild admin decide if the API warning should be shown on messages from the API
|
||||
apiCommands.showHideWarn(message, apiArg);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
message.send({
|
||||
embeds: [{
|
||||
color: failColor,
|
||||
title: 'API commands are powerful and can only be used by guild Owners and Admins.',
|
||||
description: 'For information on how to use the API, please check the GitHub README for more information [here](https://github.com/Burn-E99/TheArtificer).',
|
||||
}],
|
||||
}).catch((e: Error) => utils.commonLoggers.messageSendError('apiCmd.ts:77', message, e));
|
||||
}
|
||||
};
|
|
@ -0,0 +1,13 @@
|
|||
import { help } from './apiHelp.ts';
|
||||
import { allowBlock } from './allowBlock.ts';
|
||||
import { deleteGuild } from './deleteGuild.ts';
|
||||
import { status } from './status.ts';
|
||||
import { showHideWarn } from './showHideWarn.ts';
|
||||
|
||||
export default {
|
||||
help,
|
||||
allowBlock,
|
||||
deleteGuild,
|
||||
status,
|
||||
showHideWarn,
|
||||
};
|
|
@ -0,0 +1,42 @@
|
|||
import { dbClient } from '../../db.ts';
|
||||
import {
|
||||
// Discordeno deps
|
||||
DiscordenoMessage,
|
||||
} from '../../../deps.ts';
|
||||
import { generateApiFailed, generateApiSuccess } from '../../commandUtils.ts';
|
||||
import utils from '../../utils.ts';
|
||||
|
||||
export const allowBlock = async (message: DiscordenoMessage, apiArg: string) => {
|
||||
let errorOutInitial = false;
|
||||
const guildQuery = await dbClient.query(`SELECT guildid, channelid FROM allowed_guilds WHERE guildid = ? AND channelid = ?`, [message.guildId, message.channelId]).catch((e0) => {
|
||||
utils.commonLoggers.dbError('allowBlock.ts:15', 'query', e0);
|
||||
message.send(generateApiFailed(apiArg)).catch((e: Error) => utils.commonLoggers.messageSendError('allowBlock.ts:16', message, e));
|
||||
errorOutInitial = true;
|
||||
});
|
||||
if (errorOutInitial) return;
|
||||
|
||||
let errorOut = false;
|
||||
if (guildQuery.length === 0) {
|
||||
// Since guild is not in our DB, add it in
|
||||
await dbClient.execute(`INSERT INTO allowed_guilds(guildid,channelid,active) values(?,?,?)`, [message.guildId, message.channelId, (apiArg === 'allow' || apiArg === 'enable') ? 1 : 0]).catch(
|
||||
(e0) => {
|
||||
utils.commonLoggers.dbError('allowBlock:26', 'insert into', e0);
|
||||
message.send(generateApiFailed(apiArg)).catch((e: Error) => utils.commonLoggers.messageSendError('allowBlock.ts:27', message, e));
|
||||
errorOut = true;
|
||||
},
|
||||
);
|
||||
} else {
|
||||
// Since guild is in our DB, update it
|
||||
await dbClient.execute(`UPDATE allowed_guilds SET active = ? WHERE guildid = ? AND channelid = ?`, [(apiArg === 'allow' || apiArg === 'enable') ? 1 : 0, message.guildId, message.channelId]).catch(
|
||||
(e0) => {
|
||||
utils.commonLoggers.dbError('allowBlock.ts:35', 'update', e0);
|
||||
message.send(generateApiFailed(apiArg)).catch((e: Error) => utils.commonLoggers.messageSendError('allowBlock.ts:36', message, e));
|
||||
errorOut = true;
|
||||
},
|
||||
);
|
||||
}
|
||||
if (errorOut) return;
|
||||
|
||||
// We won't get here if there's any errors, so we know it has bee successful, so report as such
|
||||
message.send(generateApiSuccess(`${apiArg}ed`)).catch((e: Error) => utils.commonLoggers.messageSendError('allowBlock.ts:44', message, e));
|
||||
};
|
|
@ -0,0 +1,65 @@
|
|||
import config from '../../../config.ts';
|
||||
import {
|
||||
// Discordeno deps
|
||||
DiscordenoMessage,
|
||||
} from '../../../deps.ts';
|
||||
import { infoColor1, infoColor2 } from '../../commandUtils.ts';
|
||||
import utils from '../../utils.ts';
|
||||
|
||||
export const help = (message: DiscordenoMessage) => {
|
||||
message.send({
|
||||
embeds: [
|
||||
{
|
||||
color: infoColor2,
|
||||
title: 'The Artificer\'s API Details:',
|
||||
description:
|
||||
`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. The API warning is also enabled by default. 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 [here](https://github.com/Burn-E99/TheArtificer).
|
||||
|
||||
You may enable and disable the API rolls for your guild as needed.`,
|
||||
},
|
||||
{
|
||||
color: infoColor1,
|
||||
title: 'Available API Commands:',
|
||||
fields: [
|
||||
{
|
||||
name: `\`${config.prefix}api help\``,
|
||||
value: 'This command',
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: `\`${config.prefix}api status\``,
|
||||
value: 'Shows the current status of the API for the channel this was run in',
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: `\`${config.prefix}api allow/enable\``,
|
||||
value: 'Allows API Rolls to be sent to the channel this was run in',
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: `\`${config.prefix}api block/disable\``,
|
||||
value: 'Blocks API Rolls from being sent to the channel this was run in',
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: `\`${config.prefix}api delete\``,
|
||||
value: 'Deletes this channel\'s settings from The Artificer\'s database',
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: `\`${config.prefix}api show-warn\``,
|
||||
value: 'Shows the API warning on all rolls sent to the channel this was run in',
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: `\`${config.prefix}api hide-warn\``,
|
||||
value: 'Hides the API warning on all rolls sent to the channel this was run in',
|
||||
inline: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}).catch((e: Error) => utils.commonLoggers.messageSendError('apiHelp.ts:67', message, e));
|
||||
};
|
|
@ -0,0 +1,31 @@
|
|||
import { dbClient } from '../../db.ts';
|
||||
import {
|
||||
// Discordeno deps
|
||||
DiscordenoMessage,
|
||||
} from '../../../deps.ts';
|
||||
import { failColor, successColor } from '../../commandUtils.ts';
|
||||
import utils from '../../utils.ts';
|
||||
|
||||
export const deleteGuild = async (message: DiscordenoMessage) => {
|
||||
let errorOut = false;
|
||||
await dbClient.execute(`DELETE FROM allowed_guilds WHERE guildid = ? AND channelid = ?`, [message.guildId, message.channelId]).catch((e0) => {
|
||||
utils.commonLoggers.dbError('deleteGuild.ts:15', 'query', e0);
|
||||
message.send({
|
||||
embeds: [{
|
||||
color: failColor,
|
||||
title: 'Failed to delete this guild from the database.',
|
||||
description: 'If this issue persists, please report this to the developers.',
|
||||
}],
|
||||
}).catch((e: Error) => utils.commonLoggers.messageSendError('deleteGuild.ts:22', message, e));
|
||||
errorOut = true;
|
||||
});
|
||||
if (errorOut) return;
|
||||
|
||||
// We won't get here if there's any errors, so we know it has bee successful, so report as such
|
||||
message.send({
|
||||
embeds: [{
|
||||
color: successColor,
|
||||
title: 'This guild\'s API setting has been removed from The Artifier\'s Database.',
|
||||
}],
|
||||
}).catch((e: Error) => utils.commonLoggers.messageSendError('deleteGuild.ts:33', message, e));
|
||||
};
|
|
@ -0,0 +1,38 @@
|
|||
import { dbClient } from '../../db.ts';
|
||||
import {
|
||||
// Discordeno deps
|
||||
DiscordenoMessage,
|
||||
} from '../../../deps.ts';
|
||||
import { generateApiFailed, generateApiSuccess } from '../../commandUtils.ts';
|
||||
import utils from '../../utils.ts';
|
||||
|
||||
export const showHideWarn = async (message: DiscordenoMessage, apiArg: string) => {
|
||||
let errorOutInitial = false;
|
||||
const guildQuery = await dbClient.query(`SELECT guildid, channelid FROM allowed_guilds WHERE guildid = ? AND channelid = ?`, [message.guildId, message.channelId]).catch((e0) => {
|
||||
utils.commonLoggers.dbError('showHideWarn.ts:15', 'query', e0);
|
||||
message.send(generateApiFailed(`${apiArg} on`)).catch((e: Error) => utils.commonLoggers.messageSendError('showHideWarn.ts:16', message, e));
|
||||
errorOutInitial = true;
|
||||
});
|
||||
if (errorOutInitial) return;
|
||||
|
||||
let errorOut = false;
|
||||
if (guildQuery.length === 0) {
|
||||
// Since guild is not in our DB, add it in
|
||||
await dbClient.execute(`INSERT INTO allowed_guilds(guildid,channelid,hidewarn) values(?,?,?)`, [message.guildId, message.channelId, (apiArg === 'hide-warn') ? 1 : 0]).catch((e0) => {
|
||||
utils.commonLoggers.dbError('showHideWarn.ts:25', 'insert inot', e0);
|
||||
message.send(generateApiFailed(`${apiArg} on`)).catch((e: Error) => utils.commonLoggers.messageSendError('showHideWarn.ts:26', message, e));
|
||||
errorOut = true;
|
||||
});
|
||||
} else {
|
||||
// Since guild is in our DB, update it
|
||||
await dbClient.execute(`UPDATE allowed_guilds SET hidewarn = ? WHERE guildid = ? AND channelid = ?`, [(apiArg === 'hide-warn') ? 1 : 0, message.guildId, message.channelId]).catch((e0) => {
|
||||
utils.commonLoggers.dbError('showHideWarn.ts:32', 'update', e0);
|
||||
message.send(generateApiFailed(`${apiArg} on`)).catch((e: Error) => utils.commonLoggers.messageSendError('showHideWarn.ts:33', message, e));
|
||||
errorOut = true;
|
||||
});
|
||||
}
|
||||
if (errorOut) return;
|
||||
|
||||
// We won't get here if there's any errors, so we know it has bee successful, so report as such
|
||||
message.send(generateApiSuccess(apiArg)).catch((e: Error) => utils.commonLoggers.messageSendError('showHideWarn.ts:40', message, e));
|
||||
};
|
|
@ -0,0 +1,37 @@
|
|||
import { dbClient } from '../../db.ts';
|
||||
import {
|
||||
// Discordeno deps
|
||||
DiscordenoMessage,
|
||||
} from '../../../deps.ts';
|
||||
import { failColor, generateApiStatus } from '../../commandUtils.ts';
|
||||
import utils from '../../utils.ts';
|
||||
|
||||
export const status = async (message: DiscordenoMessage) => {
|
||||
// Get status of guild from the db
|
||||
let errorOut = false;
|
||||
const guildQuery = await dbClient.query(`SELECT active, banned FROM allowed_guilds WHERE guildid = ? AND channelid = ?`, [message.guildId, message.channelId]).catch((e0) => {
|
||||
utils.commonLoggers.dbError('status.ts:16', 'query', e0);
|
||||
message.send({
|
||||
embeds: [{
|
||||
color: failColor,
|
||||
title: 'Failed to check API rolls status for this guild.',
|
||||
description: 'If this issue persists, please report this to the developers.',
|
||||
}],
|
||||
}).catch((e: Error) => utils.commonLoggers.messageSendError('status.ts:23', message, e));
|
||||
errorOut = true;
|
||||
});
|
||||
if (errorOut) 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) {
|
||||
message.send(generateApiStatus(true, false)).catch((e: Error) => utils.commonLoggers.messageSendError('status.ts:32', message, e));
|
||||
} else {
|
||||
message.send(generateApiStatus(false, guildQuery[0].active)).catch((e: Error) => utils.commonLoggers.messageSendError('status.ts:34', message, e));
|
||||
}
|
||||
} else {
|
||||
// Guild is not in DB, therefore they are blocked
|
||||
message.send(generateApiStatus(false, false)).catch((e: Error) => utils.commonLoggers.messageSendError('status.ts:38', message, e));
|
||||
}
|
||||
};
|
|
@ -0,0 +1,48 @@
|
|||
import config from '../../config.ts';
|
||||
import { dbClient, queries } from '../db.ts';
|
||||
import {
|
||||
// Discordeno deps
|
||||
DiscordenoMessage,
|
||||
} from '../../deps.ts';
|
||||
import auditCommands from './auditCmd/_index.ts';
|
||||
import { failColor } from '../commandUtils.ts';
|
||||
import utils from '../utils.ts';
|
||||
|
||||
export const audit = async (message: DiscordenoMessage, args: string[]) => {
|
||||
// Light telemetry to see how many times a command is being run
|
||||
dbClient.execute(queries.callIncCnt('audit')).catch((e) => utils.commonLoggers.dbError('audit.ts:16', 'call sproc INC_CNT on', e));
|
||||
|
||||
// Local apiArg in lowercase
|
||||
const auditArg = (args[0] || 'help').toLowerCase();
|
||||
|
||||
// Makes sure the user is authenticated to run the API command
|
||||
if (message.authorId === config.api.admin) {
|
||||
switch (auditArg) {
|
||||
case 'help':
|
||||
case 'h':
|
||||
// [[audit help or [[audit h
|
||||
// Shows API help details
|
||||
auditCommands.auditHelp(message);
|
||||
break;
|
||||
case 'db':
|
||||
// [[audit db
|
||||
// Shows current DB table sizes
|
||||
auditCommands.auditDB(message);
|
||||
break;
|
||||
case 'guilds':
|
||||
// [[audit guilds
|
||||
// Shows breakdown of guilds and detials on them
|
||||
auditCommands.auditGuilds(message);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
message.send({
|
||||
embeds: [{
|
||||
color: failColor,
|
||||
title: `Audit commands are powerful and can only be used by ${config.name}'s owner.`,
|
||||
}],
|
||||
}).catch((e: Error) => utils.commonLoggers.messageSendError('audit.ts:51', message, e));
|
||||
}
|
||||
};
|
|
@ -0,0 +1,9 @@
|
|||
import { auditHelp } from './auditHelp.ts';
|
||||
import { auditDB } from './auditDB.ts';
|
||||
import { auditGuilds } from './auditGuilds.ts';
|
||||
|
||||
export default {
|
||||
auditHelp,
|
||||
auditDB,
|
||||
auditGuilds,
|
||||
};
|
|
@ -0,0 +1,42 @@
|
|||
import { dbClient } from '../../db.ts';
|
||||
import {
|
||||
// Discordeno deps
|
||||
DiscordenoMessage,
|
||||
EmbedField,
|
||||
} from '../../../deps.ts';
|
||||
import { infoColor2 } from '../../commandUtils.ts';
|
||||
import { compilingStats } from '../../commonEmbeds.ts';
|
||||
import utils from '../../utils.ts';
|
||||
|
||||
export const auditDB = async (message: DiscordenoMessage) => {
|
||||
try {
|
||||
const m = await message.send(compilingStats);
|
||||
|
||||
// Get DB statistics
|
||||
const auditQuery = await dbClient.query(`SELECT * FROM db_size;`).catch((e) => utils.commonLoggers.dbError('auditDB.ts:19', 'query', e));
|
||||
|
||||
// Turn all tables into embed fields, currently only properly will handle 25 tables, but we'll fix that when artificer gets 26 tables
|
||||
const embedFields: Array<EmbedField> = [];
|
||||
auditQuery.forEach((row: any) => {
|
||||
embedFields.push({
|
||||
name: `${row.table}`,
|
||||
value: `**Size:** ${row.size} MB
|
||||
**Rows:** ${row.rows}`,
|
||||
inline: true,
|
||||
});
|
||||
});
|
||||
|
||||
// Send the results
|
||||
m.edit({
|
||||
embeds: [{
|
||||
color: infoColor2,
|
||||
title: 'Database Audit',
|
||||
description: 'Lists all tables with their current size and row count.',
|
||||
timestamp: new Date().toISOString(),
|
||||
fields: embedFields,
|
||||
}],
|
||||
}).catch((e: Error) => utils.commonLoggers.messageEditError('auditDB.ts:43', message, e));
|
||||
} catch (e) {
|
||||
utils.commonLoggers.messageSendError('auditDB.ts:45', message, e);
|
||||
}
|
||||
};
|
|
@ -0,0 +1,94 @@
|
|||
import config from '../../../config.ts';
|
||||
import {
|
||||
// Discordeno deps
|
||||
cache,
|
||||
cacheHandlers,
|
||||
DiscordenoMessage,
|
||||
} from '../../../deps.ts';
|
||||
import { infoColor2 } from '../../commandUtils.ts';
|
||||
import utils from '../../utils.ts';
|
||||
|
||||
export const auditGuilds = async (message: DiscordenoMessage) => {
|
||||
const cachedGuilds = await cacheHandlers.size('guilds');
|
||||
let totalCount = 0;
|
||||
let realCount = 0;
|
||||
let botsCount = 0;
|
||||
|
||||
let auditText = '';
|
||||
|
||||
cache.guilds.forEach((guild) => {
|
||||
totalCount += guild.memberCount;
|
||||
let localBotCount = 0;
|
||||
let localRealCount = 0;
|
||||
guild.members.forEach((member) => {
|
||||
if (member.bot) {
|
||||
botsCount++;
|
||||
localBotCount++;
|
||||
} else {
|
||||
realCount++;
|
||||
localRealCount++;
|
||||
}
|
||||
});
|
||||
|
||||
auditText += `Guild: ${guild.name} (${guild.id})
|
||||
Owner: ${guild.owner?.username}#${guild.owner?.discriminator} (${guild.ownerId})
|
||||
Tot mem: ${guild.memberCount} | Real: ${localRealCount} | Bot: ${localBotCount}
|
||||
|
||||
`;
|
||||
});
|
||||
|
||||
const b = await new Blob([auditText as BlobPart], { 'type': 'text' });
|
||||
const tooBig = await new Blob(['tooBig' as BlobPart], { 'type': 'text' });
|
||||
|
||||
message.send({
|
||||
embeds: [{
|
||||
color: infoColor2,
|
||||
title: 'Guilds Audit',
|
||||
description: `Shows details of the guilds that ${config.name} serves.
|
||||
|
||||
Please see attached file for audit details on cached guilds and members.`,
|
||||
timestamp: new Date().toISOString(),
|
||||
fields: [
|
||||
{
|
||||
name: 'Total Guilds:',
|
||||
value: `${cache.guilds.size}`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: 'Cached Guilds:',
|
||||
value: `${cachedGuilds}`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: 'Uncached Guilds:',
|
||||
value: `${cache.dispatchedGuildIds.size}`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: 'Total Members\n(may be artificially higher if 1 user is in multiple guilds the bot is in):',
|
||||
value: `${totalCount}`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: 'Cached Real People:',
|
||||
value: `${realCount}`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: 'Cached Bots:',
|
||||
value: `${botsCount}`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: 'Average members per guild:',
|
||||
value: `${(totalCount / cache.guilds.size).toFixed(2)}`,
|
||||
inline: true,
|
||||
},
|
||||
],
|
||||
}],
|
||||
file: {
|
||||
'blob': b.size > 8388290 ? tooBig : b,
|
||||
'name': 'auditDetails.txt',
|
||||
},
|
||||
}).catch((e: Error) => utils.commonLoggers.messageSendError('auditGuild.ts:19', message, e));
|
||||
};
|
|
@ -0,0 +1,33 @@
|
|||
import config from '../../../config.ts';
|
||||
import {
|
||||
// Discordeno deps
|
||||
DiscordenoMessage,
|
||||
} from '../../../deps.ts';
|
||||
import { infoColor1 } from '../../commandUtils.ts';
|
||||
import utils from '../../utils.ts';
|
||||
|
||||
export const auditHelp = (message: DiscordenoMessage) => {
|
||||
message.send({
|
||||
embeds: [{
|
||||
color: infoColor1,
|
||||
title: 'Audit Help',
|
||||
fields: [
|
||||
{
|
||||
name: `\`${config.prefix}audit help\``,
|
||||
value: 'This command',
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: `\`${config.prefix}audit db\``,
|
||||
value: 'Shows current DB table sizes',
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: `\`${config.prefix}audit guilds\``,
|
||||
value: 'Shows breakdown of guilds and detials on them',
|
||||
inline: true,
|
||||
},
|
||||
],
|
||||
}],
|
||||
}).catch((e: Error) => utils.commonLoggers.messageSendError('auditHelp.ts:35', message, e));
|
||||
};
|
|
@ -0,0 +1,40 @@
|
|||
import config from '../../config.ts';
|
||||
import { dbClient, queries } from '../db.ts';
|
||||
import {
|
||||
// Discordeno deps
|
||||
DiscordenoMessage,
|
||||
// Log4Deno deps
|
||||
log,
|
||||
LT,
|
||||
} from '../../deps.ts';
|
||||
import { EmojiConf } from '../mod.d.ts';
|
||||
import utils from '../utils.ts';
|
||||
|
||||
const allEmojiAliases: string[] = [];
|
||||
|
||||
config.emojis.forEach((emji: EmojiConf) => {
|
||||
allEmojiAliases.push(...emji.aliases);
|
||||
});
|
||||
|
||||
export const emoji = (message: DiscordenoMessage, command: string) => {
|
||||
// shortcut
|
||||
if (allEmojiAliases.indexOf(command)) {
|
||||
// Start looping thru the possible emojis
|
||||
config.emojis.some((emji: EmojiConf) => {
|
||||
log(LT.LOG, `Checking if command was emoji ${JSON.stringify(emji)}`);
|
||||
// If a match gets found
|
||||
if (emji.aliases.indexOf(command || '') > -1) {
|
||||
// Light telemetry to see how many times a command is being run
|
||||
dbClient.execute(queries.callIncCnt('emojis')).catch((e) => utils.commonLoggers.dbError('emojis.ts:28', 'call sproc INC_CNT on', e));
|
||||
|
||||
// Send the needed emoji
|
||||
message.send(`<${emji.animated ? 'a' : ''}:${emji.name}:${emji.id}>`).catch((e: Error) => utils.commonLoggers.messageSendError('emoji.ts:33', message, e));
|
||||
// And attempt to delete if needed
|
||||
if (emji.deleteSender) {
|
||||
message.delete().catch((e: Error) => utils.commonLoggers.messageDeleteError('emoji.ts:36', message, e));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
|
@ -0,0 +1,31 @@
|
|||
import config from '../../config.ts';
|
||||
import { dbClient, queries } from '../db.ts';
|
||||
import {
|
||||
// Discordeno deps
|
||||
DiscordenoMessage,
|
||||
// Log4Deno deps
|
||||
log,
|
||||
LT,
|
||||
} from '../../deps.ts';
|
||||
import { infoColor1 } from '../commandUtils.ts';
|
||||
import utils from '../utils.ts';
|
||||
|
||||
export const handleMentions = (message: DiscordenoMessage) => {
|
||||
log(LT.LOG, `Handling @mention message: ${JSON.stringify(message)}`);
|
||||
|
||||
// Light telemetry to see how many times a command is being run
|
||||
dbClient.execute(queries.callIncCnt('mention')).catch((e) => utils.commonLoggers.dbError('handleMentions.ts:17', 'call sproc INC_CNT on', e));
|
||||
|
||||
message.send({
|
||||
embeds: [{
|
||||
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.`,
|
||||
}],
|
||||
}],
|
||||
}).catch((e: Error) => utils.commonLoggers.messageSendError('handleMentions.ts:30', message, e));
|
||||
};
|
|
@ -0,0 +1,41 @@
|
|||
import { dbClient, queries } from '../db.ts';
|
||||
import {
|
||||
// Discordeno deps
|
||||
DiscordenoMessage,
|
||||
} from '../../deps.ts';
|
||||
import config from '../../config.ts';
|
||||
import { failColor, infoColor2 } from '../commandUtils.ts';
|
||||
import utils from '../utils.ts';
|
||||
import intervals from '../intervals.ts';
|
||||
|
||||
export const heatmap = async (message: DiscordenoMessage) => {
|
||||
// Light telemetry to see how many times a command is being run
|
||||
dbClient.execute(queries.callIncCnt('heatmap')).catch((e) => utils.commonLoggers.dbError('heatmap.ts:14', 'call sproc INC_CNT on', e));
|
||||
|
||||
if (config.api.enable) {
|
||||
message.send({
|
||||
embeds: [{
|
||||
title: 'Roll Heatmap',
|
||||
description: `Over time, this image will show a nice pattern of when rolls are requested the most.
|
||||
|
||||
Least Rolls: ${intervals.getMinRollCnt()}
|
||||
Most Rolls: ${intervals.getMaxRollCnt()}`,
|
||||
footer: {
|
||||
text: 'Data is shown in US Eastern Time. | This heatmap uses data starting 6/26/2022.',
|
||||
},
|
||||
color: infoColor2,
|
||||
image: {
|
||||
url: `${config.api.publicDomain}api/heatmap.png`,
|
||||
},
|
||||
}],
|
||||
}).catch((e) => utils.commonLoggers.messageSendError('heatmap.ts:21', message, e));
|
||||
} else {
|
||||
message.send({
|
||||
embeds: [{
|
||||
title: 'Roll Heatmap Disabled',
|
||||
description: 'This command requires the bot\'s API to be enabled. If you are the host of this bot, check your `config.ts` file to enable it.',
|
||||
color: failColor,
|
||||
}],
|
||||
}).catch((e) => utils.commonLoggers.messageSendError('heatmap.ts:21', message, e));
|
||||
}
|
||||
};
|
|
@ -0,0 +1,98 @@
|
|||
import config from '../../config.ts';
|
||||
import { dbClient, queries } from '../db.ts';
|
||||
import {
|
||||
// Discordeno deps
|
||||
DiscordenoMessage,
|
||||
} from '../../deps.ts';
|
||||
import { infoColor2 } from '../commandUtils.ts';
|
||||
import utils from '../utils.ts';
|
||||
|
||||
export const help = (message: DiscordenoMessage) => {
|
||||
// Light telemetry to see how many times a command is being run
|
||||
dbClient.execute(queries.callIncCnt('help')).catch((e) => utils.commonLoggers.dbError('htlp.ts:15', 'call sproc INC_CNT on', e));
|
||||
|
||||
message.send({
|
||||
embeds: [{
|
||||
color: infoColor2,
|
||||
title: 'The Artificer\'s Available Commands:',
|
||||
fields: [
|
||||
{
|
||||
name: `\`${config.prefix}?\``,
|
||||
value: 'This command',
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: `\`${config.prefix}rollhelp\` or \`${config.prefix}??\``,
|
||||
value: `Details on how to use the roll command, listed as \`${config.prefix}xdy...${config.postfix}\` below`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: `\`${config.prefix}rollDecorators\` or \`${config.prefix}???\``,
|
||||
value: `Details on how to use decorators on the roll command`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: `\`${config.prefix}api [subcommand]\``,
|
||||
value: `Administrative tools for the bots's API, run \`${config.prefix}api help\` for more details`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: `\`${config.prefix}ping\``,
|
||||
value: 'Pings the bot to check connectivity',
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: `\`${config.prefix}info\``,
|
||||
value: 'Prints some information and links relating to the bot',
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: `\`${config.prefix}privacy\``,
|
||||
value: 'Prints some information about the Privacy Policy',
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: `\`${config.prefix}version\``,
|
||||
value: 'Prints the bots version',
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: `\`${config.prefix}popcat\``,
|
||||
value: 'Popcat',
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: `\`${config.prefix}report [text]\``,
|
||||
value: 'Report a command that failed to run',
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: `\`${config.prefix}stats\``,
|
||||
value: 'Statistics on the bot',
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: `\`${config.prefix}heatmap\``,
|
||||
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:
|
||||
`Rolls all configs requested, you may repeat the command multiple times in the same message (just ensure you close each roll with \`${config.postfix}\`), run \`${config.prefix}??\` for more details`,
|
||||
inline: true,
|
||||
},
|
||||
],
|
||||
}],
|
||||
}).catch((e: Error) => utils.commonLoggers.messageSendError('help.ts:82', message, e));
|
||||
};
|
|
@ -0,0 +1,24 @@
|
|||
import config from '../../config.ts';
|
||||
import { dbClient, queries } from '../db.ts';
|
||||
import {
|
||||
// Discordeno deps
|
||||
DiscordenoMessage,
|
||||
} from '../../deps.ts';
|
||||
import { infoColor2 } from '../commandUtils.ts';
|
||||
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));
|
||||
|
||||
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.
|
||||
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));
|
||||
};
|
|
@ -0,0 +1,39 @@
|
|||
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));
|
||||
}
|
||||
}
|
||||
};
|
|
@ -0,0 +1,36 @@
|
|||
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));
|
||||
}
|
||||
};
|
|
@ -0,0 +1,20 @@
|
|||
import { dbClient, queries } from '../db.ts';
|
||||
import {
|
||||
// Discordeno deps
|
||||
DiscordenoMessage,
|
||||
} from '../../deps.ts';
|
||||
import { generatePing } from '../commandUtils.ts';
|
||||
import utils from '../utils.ts';
|
||||
|
||||
export const ping = async (message: DiscordenoMessage) => {
|
||||
// Light telemetry to see how many times a command is being run
|
||||
dbClient.execute(queries.callIncCnt('ping')).catch((e) => utils.commonLoggers.dbError('ping.ts:14', 'call sproc INC_CNT on', e));
|
||||
|
||||
// Calculates ping between sending a message and editing it, giving a nice round-trip latency.
|
||||
try {
|
||||
const m = await message.send(generatePing(-1));
|
||||
m.edit(generatePing(m.timestamp - message.timestamp));
|
||||
} catch (e) {
|
||||
utils.commonLoggers.messageSendError('ping.ts:23', message, e);
|
||||
}
|
||||
};
|
|
@ -0,0 +1,31 @@
|
|||
import config from '../../config.ts';
|
||||
import { dbClient, queries } from '../db.ts';
|
||||
import {
|
||||
// Discordeno deps
|
||||
DiscordenoMessage,
|
||||
} from '../../deps.ts';
|
||||
import { infoColor1 } from '../commandUtils.ts';
|
||||
import utils from '../utils.ts';
|
||||
|
||||
export const privacy = (message: DiscordenoMessage) => {
|
||||
// Light telemetry to see how many times a command is being run
|
||||
dbClient.execute(queries.callIncCnt('privacy')).catch((e) => utils.commonLoggers.dbError('privacy.ts:15', 'call sproc INC_CNT on', e));
|
||||
|
||||
message.send({
|
||||
embeds: [{
|
||||
color: infoColor1,
|
||||
title: 'Privacy Policy',
|
||||
fields: [{
|
||||
name: 'The Artificer does not track or collect user information via Discord.',
|
||||
value:
|
||||
`The only user submitted information that is stored is submitted via the \`${config.prefix}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 [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.`,
|
||||
}],
|
||||
}],
|
||||
}).catch((e: Error) => utils.commonLoggers.messageSendError('privacy.ts:33', message, e));
|
||||
};
|
|
@ -0,0 +1,34 @@
|
|||
import config from '../../config.ts';
|
||||
import { dbClient, queries } from '../db.ts';
|
||||
import {
|
||||
// Discordeno deps
|
||||
DiscordenoMessage,
|
||||
// Discordeno deps
|
||||
sendMessage,
|
||||
} from '../../deps.ts';
|
||||
import { failColor, generateReport, successColor } from '../commandUtils.ts';
|
||||
import utils from '../utils.ts';
|
||||
|
||||
export const report = (message: DiscordenoMessage, args: string[]) => {
|
||||
// Light telemetry to see how many times a command is being run
|
||||
dbClient.execute(queries.callIncCnt('report')).catch((e) => utils.commonLoggers.dbError('report.ts:17', 'call sproc INC_CNT on', e));
|
||||
|
||||
if (args.join(' ')) {
|
||||
sendMessage(config.reportChannel, generateReport(args.join(' '))).catch((e: Error) => utils.commonLoggers.messageSendError('report.ts:22', message, e));
|
||||
message.send({
|
||||
embeds: [{
|
||||
color: successColor,
|
||||
title: 'Failed command has been reported to my developer.',
|
||||
description: `For more in depth support, and information about planned maintenance, please join the support server [here](https://discord.gg/peHASXMZYv).`,
|
||||
}],
|
||||
}).catch((e: Error) => utils.commonLoggers.messageSendError('report.ts:29', message, e));
|
||||
} else {
|
||||
message.send({
|
||||
embeds: [{
|
||||
color: failColor,
|
||||
title: 'Please provide a short description of what failed',
|
||||
description: 'Providing a short description helps my developer quickly diagnose what went wrong.',
|
||||
}],
|
||||
}).catch((e: Error) => utils.commonLoggers.messageSendError('report.ts:37', message, e));
|
||||
}
|
||||
};
|
|
@ -0,0 +1,22 @@
|
|||
import { dbClient, queries } from '../db.ts';
|
||||
import {
|
||||
// Discordeno deps
|
||||
DiscordenoMessage,
|
||||
} from '../../deps.ts';
|
||||
import { infoColor2 } from '../commandUtils.ts';
|
||||
import utils from '../utils.ts';
|
||||
|
||||
export const rip = (message: DiscordenoMessage) => {
|
||||
// Light telemetry to see how many times a command is being run
|
||||
dbClient.execute(queries.callIncCnt('rip')).catch((e) => utils.commonLoggers.dbError('rip.ts:14', 'call sproc INC_CNT on', e));
|
||||
|
||||
message.send({
|
||||
embeds: [{
|
||||
color: infoColor2,
|
||||
title: 'The Artificer was built in memory of my Grandmother, Babka',
|
||||
description: `With much love, Ean
|
||||
|
||||
December 21, 2020`,
|
||||
}],
|
||||
}).catch((e: Error) => utils.commonLoggers.messageSendError('rip.ts:26', message, e));
|
||||
};
|
|
@ -0,0 +1,63 @@
|
|||
import config from '../../config.ts';
|
||||
import { DEVMODE } from '../../flags.ts';
|
||||
import { dbClient, queries } from '../db.ts';
|
||||
import {
|
||||
// Discordeno deps
|
||||
DiscordenoMessage,
|
||||
// Log4Deno deps
|
||||
log,
|
||||
LT,
|
||||
} from '../../deps.ts';
|
||||
import { rollingEmbed, warnColor } from '../commandUtils.ts';
|
||||
import rollFuncs from './roll/_index.ts';
|
||||
import { queueRoll } from '../solver/rollQueue.ts';
|
||||
import { QueuedRoll } from '../mod.d.ts';
|
||||
import utils from '../utils.ts';
|
||||
|
||||
export const roll = async (message: DiscordenoMessage, args: string[], command: string) => {
|
||||
// Light telemetry to see how many times a command is being run
|
||||
const currDateTime = new Date();
|
||||
dbClient.execute(queries.callIncCnt('roll')).catch((e) => utils.commonLoggers.dbError('roll.ts:20', 'call sproc INC_CNT on', e));
|
||||
dbClient.execute(queries.callIncHeatmap(currDateTime)).catch((e) => utils.commonLoggers.dbError('roll.ts:21', 'update', e));
|
||||
|
||||
// If DEVMODE is on, only allow this command to be used in the devServer
|
||||
if (DEVMODE && message.guildId !== config.devServer) {
|
||||
message.send({
|
||||
embeds: [{
|
||||
color: warnColor,
|
||||
title: 'Command is in development, please try again later.',
|
||||
}],
|
||||
}).catch((e: Error) => utils.commonLoggers.messageSendError('roll.ts:30', message, e));
|
||||
return;
|
||||
}
|
||||
|
||||
// Rest of this command is in a try-catch to protect all sends/edits from erroring out
|
||||
try {
|
||||
const originalCommand = `${config.prefix}${command} ${args.join(' ')}`;
|
||||
|
||||
const m = await message.reply(rollingEmbed);
|
||||
|
||||
// Get modifiers from command
|
||||
const modifiers = rollFuncs.getModifiers(m, args, command, originalCommand);
|
||||
|
||||
// Return early if the modifiers were invalid
|
||||
if (!modifiers.valid) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Rejoin all of the args and send it into the solver, if solver returns a falsy item, an error object will be substituded in
|
||||
const rollCmd = message.content.substring(2);
|
||||
|
||||
queueRoll(
|
||||
<QueuedRoll> {
|
||||
apiRoll: false,
|
||||
dd: { m, message },
|
||||
rollCmd,
|
||||
modifiers,
|
||||
originalCommand,
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
log(LT.ERROR, `Undandled Error: ${JSON.stringify(e)}`);
|
||||
}
|
||||
};
|
|
@ -0,0 +1,5 @@
|
|||
import { getModifiers } from './getModifiers.ts';
|
||||
|
||||
export default {
|
||||
getModifiers,
|
||||
};
|
|
@ -0,0 +1,117 @@
|
|||
import config from '../../../config.ts';
|
||||
import { DEVMODE } from '../../../flags.ts';
|
||||
import { dbClient, queries } from '../../db.ts';
|
||||
import {
|
||||
// Discordeno deps
|
||||
DiscordenoMessage,
|
||||
// Log4Deno deps
|
||||
log,
|
||||
LT,
|
||||
} from '../../../deps.ts';
|
||||
import { generateRollError } from '../../commandUtils.ts';
|
||||
import { RollModifiers } from '../../mod.d.ts';
|
||||
import utils from '../../utils.ts';
|
||||
|
||||
export const getModifiers = (m: DiscordenoMessage, args: string[], command: string, originalCommand: string): RollModifiers => {
|
||||
const errorType = 'Modifiers invalid:';
|
||||
const modifiers: RollModifiers = {
|
||||
noDetails: false,
|
||||
superNoDetails: false,
|
||||
spoiler: '',
|
||||
maxRoll: false,
|
||||
nominalRoll: false,
|
||||
gmRoll: false,
|
||||
gms: [],
|
||||
order: '',
|
||||
valid: false,
|
||||
count: false,
|
||||
apiWarn: '',
|
||||
};
|
||||
|
||||
// Check if any of the args are command flags and pull those out into the modifiers object
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
log(LT.LOG, `Checking ${command}${args.join(' ')} for command modifiers ${i}`);
|
||||
let defaultCase = false;
|
||||
switch (args[i].toLowerCase()) {
|
||||
case '-c':
|
||||
modifiers.count = true;
|
||||
break;
|
||||
case '-nd':
|
||||
modifiers.noDetails = true;
|
||||
break;
|
||||
case '-snd':
|
||||
modifiers.superNoDetails = true;
|
||||
break;
|
||||
case '-s':
|
||||
modifiers.spoiler = '||';
|
||||
break;
|
||||
case '-m':
|
||||
modifiers.maxRoll = true;
|
||||
break;
|
||||
case '-n':
|
||||
modifiers.nominalRoll = true;
|
||||
break;
|
||||
case '-gm':
|
||||
modifiers.gmRoll = true;
|
||||
|
||||
// -gm is a little more complex, as we must get all of the GMs that need to be DMd
|
||||
while (((i + 1) < args.length) && args[i + 1].startsWith('<@')) {
|
||||
log(LT.LOG, `Finding all GMs, checking args ${JSON.stringify(args)}`);
|
||||
// Keep looping thru the rest of the args until one does not start with the discord mention code
|
||||
modifiers.gms.push(args[i + 1].replace(/!/g, ''));
|
||||
args.splice(i + 1, 1);
|
||||
}
|
||||
if (modifiers.gms.length < 1) {
|
||||
// If -gm is on and none were found, throw an error
|
||||
m.edit(generateRollError(errorType, 'Must specifiy at least one GM by @mentioning them')).catch((e) => utils.commonLoggers.messageEditError('getModifiers.ts:66', m, e));
|
||||
|
||||
if (DEVMODE && config.logRolls) {
|
||||
// If enabled, log rolls so we can verify the bots math
|
||||
dbClient.execute(queries.insertRollLogCmd(0, 1), [originalCommand, 'NoGMsFound', m.id]).catch((e) => utils.commonLoggers.dbError('getModifiers.ts:72', 'insert into', e));
|
||||
}
|
||||
return modifiers;
|
||||
}
|
||||
break;
|
||||
case '-o':
|
||||
// Shift the -o out of the array so the next item is the direction
|
||||
args.splice(i, 1);
|
||||
|
||||
if (!args[i] || args[i].toLowerCase()[0] !== 'd' && args[i].toLowerCase()[0] !== 'a') {
|
||||
// If -o is on and asc or desc was not specified, error out
|
||||
m.edit(generateRollError(errorType, 'Must specifiy `a` or `d` to order the rolls ascending or descending')).catch((e) => utils.commonLoggers.messageEditError('getModifiers.ts:81', m, e));
|
||||
|
||||
if (DEVMODE && config.logRolls) {
|
||||
// If enabled, log rolls so we can verify the bots math
|
||||
dbClient.execute(queries.insertRollLogCmd(0, 1), [originalCommand, 'NoOrderFound', m.id]).catch((e) => utils.commonLoggers.dbError('getModifiers.ts:89', 'insert into', e));
|
||||
}
|
||||
return modifiers;
|
||||
}
|
||||
|
||||
modifiers.order = args[i].toLowerCase()[0];
|
||||
break;
|
||||
default:
|
||||
// Default case should not mess with the array
|
||||
defaultCase = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (!defaultCase) {
|
||||
args.splice(i, 1);
|
||||
i--;
|
||||
}
|
||||
}
|
||||
|
||||
// maxRoll and nominalRoll cannot both be on, throw an error
|
||||
if (modifiers.maxRoll && modifiers.nominalRoll) {
|
||||
m.edit(generateRollError(errorType, 'Cannot maximise and nominise the roll at the same time')).catch((e) => utils.commonLoggers.messageEditError('getModifiers.ts:106', m, e));
|
||||
|
||||
if (DEVMODE && config.logRolls) {
|
||||
// If enabled, log rolls so we can verify the bots math
|
||||
dbClient.execute(queries.insertRollLogCmd(0, 1), [originalCommand, 'MaxAndNominal', m.id]).catch((e) => utils.commonLoggers.dbError('getModifiers.ts:120', 'insert into', e));
|
||||
}
|
||||
return modifiers;
|
||||
}
|
||||
|
||||
modifiers.valid = true;
|
||||
return modifiers;
|
||||
};
|
|
@ -0,0 +1,71 @@
|
|||
import config from '../../config.ts';
|
||||
import { dbClient, queries } from '../db.ts';
|
||||
import {
|
||||
// Discordeno deps
|
||||
DiscordenoMessage,
|
||||
} from '../../deps.ts';
|
||||
import { infoColor2 } from '../commandUtils.ts';
|
||||
import utils from '../utils.ts';
|
||||
|
||||
export const rollDecorators = (message: DiscordenoMessage) => {
|
||||
// Light telemetry to see how many times a command is being run
|
||||
dbClient.execute(queries.callIncCnt('rollDecorators')).catch((e) => utils.commonLoggers.dbError('rollHelp.ts:15', 'call sproc INC_CNT on', e));
|
||||
|
||||
message.send({
|
||||
embeds: [
|
||||
{
|
||||
color: infoColor2,
|
||||
title: 'Roll Command Decorators:',
|
||||
description: `This command also has some useful decorators that can used. These decorators simply need to be placed after all rolls in the message.
|
||||
|
||||
Examples: \`${config.prefix}d20${config.postfix} -nd\`, \`${config.prefix}d20${config.postfix} -nd -s\`, \`${config.prefix}d20${config.postfix} ${config.prefix}d20${config.postfix} ${config.prefix}d20${config.postfix} -o a\``,
|
||||
fields: [
|
||||
{
|
||||
name: '`-nd` - No Details',
|
||||
value: 'Suppresses all details of the requested roll',
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: '`-snd` - Super No Details',
|
||||
value: 'Suppresses all details of the requested roll and hides no details message',
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: '`-s` - Spoiler',
|
||||
value: 'Spoilers all details of the requested roll',
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: '`-m` - Maximize Roll',
|
||||
value: 'Rolls the theoretical maximum roll, cannot be used with -n',
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: '`-n` - Nominal Roll',
|
||||
value: 'Rolls the theoretical nominal roll, cannot be used with -m',
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: '`-gm @user1 @user2 @usern` - GM Roll',
|
||||
value: 'Rolls the requested roll in GM mode, suppressing all publicly shown results and details and sending the results directly to the specified GMs',
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: '`-c` - Count Rolls',
|
||||
value: 'Shows the Count Embed, containing the count of successful rolls, failed rolls, rerolls, drops, and explosions',
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: '`-o [direction]` - Order Roll',
|
||||
value: `Rolls the requested roll and orders the results in the requested direction
|
||||
|
||||
Available directions:
|
||||
\`a\` - Ascending (least to greatest)
|
||||
\`d\` - Descending (greatest to least)`,
|
||||
inline: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}).catch((e: Error) => utils.commonLoggers.messageSendError('rollHelp.ts:247', message, e));
|
||||
};
|
|
@ -0,0 +1,262 @@
|
|||
import config from '../../config.ts';
|
||||
import { dbClient, queries } from '../db.ts';
|
||||
import {
|
||||
// Discordeno deps
|
||||
DiscordenoMessage,
|
||||
} from '../../deps.ts';
|
||||
import { infoColor1, infoColor2, successColor } from '../commandUtils.ts';
|
||||
import utils from '../utils.ts';
|
||||
|
||||
export const rollHelp = (message: DiscordenoMessage) => {
|
||||
// Light telemetry to see how many times a command is being run
|
||||
dbClient.execute(queries.callIncCnt('rollhelp')).catch((e) => utils.commonLoggers.dbError('rollHelp.ts:15', 'call sproc INC_CNT on', e));
|
||||
|
||||
message.send({
|
||||
embeds: [
|
||||
{
|
||||
color: infoColor1,
|
||||
title: 'The Artificer\'s Roll Command Details:',
|
||||
description: `You can chain as many of these options as you want, as long as the option does not disallow it.
|
||||
|
||||
This command also can fully solve math equations with parenthesis.
|
||||
|
||||
The Artificer supports most of the [Roll20 formatting](https://artificer.eanm.dev/roll20). More details and examples can be found [here](https://artificer.eanm.dev/roll20).
|
||||
|
||||
Run \`[[???\` or \`[[rollDecorators\` for details on the roll decorators.`,
|
||||
},
|
||||
{
|
||||
color: infoColor2,
|
||||
title: 'Roll20 Dice Options:',
|
||||
fields: [
|
||||
{
|
||||
name: `\`${config.prefix}xdydzracsq!${config.postfix}\` ...`,
|
||||
value: `Rolls all configs requested, you may repeat the command multiple times in the same message (just ensure you close each roll with \`${config.postfix}\`)`,
|
||||
},
|
||||
{
|
||||
name: '`x` [Optional]',
|
||||
value: `Number of dice to roll, if omitted, 1 is used
|
||||
Additionally, replace \`x\` with \`F\` to roll Fate dice`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: '`dy` [Required]',
|
||||
value: 'Size of dice to roll, `d20` = 20 sided die',
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: '`dz` or `dlz` [Optional]',
|
||||
value: 'Drops the lowest `z` dice, cannot be used with `kz`',
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: '`kz` or `khz` [Optional]',
|
||||
value: 'Keeps the highest `z` dice, cannot be used with `dz`',
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: '`dhz` [Optional]',
|
||||
value: 'Drops the highest `z` dice, cannot be used with `kz`',
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: '`klz` [Optional]',
|
||||
value: 'Keeps the lowest `z` dice, cannot be used with `dz`',
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: '`ra` or `r=q` [Optional]',
|
||||
value: 'Rerolls any rolls that match `a`, `r3` will reroll every die that land on 3, throwing out old rolls, cannot be used with `ro`',
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: '`r<q` [Optional]',
|
||||
value: 'Rerolls any rolls that are less than or equal to `a`, `r3` will reroll every die that land on 3, 2, or 1, throwing out old rolls, cannot be used with `ro`',
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: '`r>q` [Optional]',
|
||||
value: 'Rerolls any rolls that are greater than or equal to `a`, `r3` will reroll every die that land on 3 or greater, throwing out old rolls, cannot be used with `ro`',
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: '`roa` or `ro=q` [Optional]',
|
||||
value: 'Rerolls any rolls that match `a`, `ro3` will reroll each die that lands on 3 ONLY ONE TIME, throwing out old rolls, cannot be used with `r`',
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: '`ro<q` [Optional]',
|
||||
value: 'Rerolls any rolls that are less than or equal to `a`, `ro3` will reroll each die that lands on 3, 2, or 1 ONLY ONE TIME, throwing out old rolls, cannot be used with `r`',
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: '`ro>q` [Optional]',
|
||||
value: 'Rerolls any rolls that are greater than or equal to `a`, `ro3` will reroll each die that lands on 3 or greater ONLY ONE TIME, throwing out old rolls, cannot be used with `r`',
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: '`csq` or `cs=q` [Optional]',
|
||||
value: 'Changes crit score to `q`',
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: '`cs<q` [Optional]',
|
||||
value: 'Changes crit score to be less than or equal to `q`',
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: '`cs>q` [Optional]',
|
||||
value: 'Changes crit score to be greater than or equal to `q`',
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: '`cfq` or `cf=q` [Optional]',
|
||||
value: 'Changes crit fail to `q`',
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: '`cf<q` [Optional]',
|
||||
value: 'Changes crit fail to be less than or equal to `q`',
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: '`cf>q` [Optional]',
|
||||
value: 'Changes crit fail to be greater than or equal to `q`',
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: '`!` [Optional]',
|
||||
value: 'Exploding, rolls another `dy` for every crit success',
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: '`!o` [Optional]',
|
||||
value: 'Exploding Once, rolls one `dy` for each original crit success',
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: '`!p` [Optional]',
|
||||
value: 'Penetrating Explosion, rolls one `dy` for each crit success, but subtracts one from each resulting explosion',
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: '`!!` [Optional]',
|
||||
value: 'Compounding Explosion, rolls one `dy` for each crit success, but adds the resulting explosion to the die that caused this explosion',
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: '`!=u` [Optional]',
|
||||
value: 'Explode on `u`, rolls another `dy` for every die that lands on `u`',
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: '`!>u` [Optional]',
|
||||
value: 'Explode on `u` and greater, rolls another `dy` for every die that lands on `u` or greater',
|
||||
inline: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
color: infoColor2,
|
||||
fields: [
|
||||
{
|
||||
name: '`!<u` [Optional]',
|
||||
value: 'Explode on `u` and under, rolls another `dy` for every die that lands on `u` or less',
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: '`!o=u` [Optional]',
|
||||
value: 'Explodes Once on `u`, rolls another `dy` for each original die that landed on `u`',
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: '`!o>u` [Optional]',
|
||||
value: 'Explode Once on `u` and greater, rolls another `dy` for each original die that landed on `u` or greater',
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: '`!o<u` [Optional]',
|
||||
value: 'Explode Once on `u` and under, rolls another `dy` for each original die that landed on `u` or less',
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: '`!p=u` [Optional]',
|
||||
value: 'Penetrating Explosion on `u`, rolls one `dy` for each die that lands on `u`, but subtracts one from each resulting explosion',
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: '`!p>u` [Optional]',
|
||||
value: 'Penetrating Explosion on `u` and greater, rolls one `dy` for each die that lands on `u` or greater, but subtracts one from each resulting explosion',
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: '`!p<u` [Optional]',
|
||||
value: 'Penetrating Explosion on `u` and under, rolls one `dy` for each die that lands on `u` or under, but subtracts one from each resulting explosion',
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: '`!!=u` [Optional]',
|
||||
value: 'Compounding Explosion on `u`, rolls one `dy` for each die that lands on `u`, but adds the resulting explosion to the die that caused this explosion',
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: '`!!>u` [Optional]',
|
||||
value: 'Compounding Explosion on `u` and greater, rolls one `dy` for each die that lands on `u` or greater, but adds the resulting explosion to the die that caused this explosion',
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: '`!!<u` [Optional]',
|
||||
value: 'Compounding Explosion on `u` and under, rolls one `dy` for each die that lands on `u` or under, but adds the resulting explosion to the die that caused this explosion',
|
||||
inline: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
color: infoColor1,
|
||||
title: 'Custom Dice Options',
|
||||
fields: [
|
||||
{
|
||||
name: 'CWOD Rolling',
|
||||
value: `\`${config.prefix}xcwody${config.postfix}\`
|
||||
\`x\` - Number of CWOD dice to roll
|
||||
\`y\` - Difficulty to roll at`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: 'OVA Rolling',
|
||||
value: `\`${config.prefix}xovady${config.postfix}\`
|
||||
\`x\` - Number of OVA dice to roll
|
||||
\`y\` - Size of the die to roll (defaults to 6 if omitted)`,
|
||||
inline: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
color: successColor,
|
||||
title: 'Results Formatting:',
|
||||
description: 'The results have some formatting applied on them to provide details on what happened during this roll.',
|
||||
fields: [
|
||||
{
|
||||
name: 'Bold',
|
||||
value: 'Critical successes will be **bolded**.',
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: 'Underline',
|
||||
value: 'Critical fails will be __underlined__.',
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: 'Strikethrough',
|
||||
value: 'Rolls that were dropped or rerolled ~~crossed out~~.',
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: 'Exclamation mark (`!`)',
|
||||
value: 'Rolls that were caused by an explosion have an exclamation mark (`!`) after them.',
|
||||
inline: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}).catch((e: Error) => utils.commonLoggers.messageSendError('rollHelp.ts:247', message, e));
|
||||
};
|
|
@ -0,0 +1,36 @@
|
|||
import { dbClient, queries } from '../db.ts';
|
||||
import {
|
||||
// Discordeno deps
|
||||
cache,
|
||||
cacheHandlers,
|
||||
DiscordenoMessage,
|
||||
} from '../../deps.ts';
|
||||
import { generateStats } from '../commandUtils.ts';
|
||||
import { compilingStats } from '../commonEmbeds.ts';
|
||||
import utils from '../utils.ts';
|
||||
|
||||
export const stats = async (message: DiscordenoMessage) => {
|
||||
// Light telemetry to see how many times a command is being run
|
||||
dbClient.execute(queries.callIncCnt('stats')).catch((e) => utils.commonLoggers.dbError('stats.ts:14', 'call sproc INC_CNT on', e));
|
||||
|
||||
try {
|
||||
const m = await message.send(compilingStats);
|
||||
|
||||
// Calculate how many times commands have been run
|
||||
const rollQuery = await dbClient.query(`SELECT count, hourlyRate FROM command_cnt WHERE command = "roll";`).catch((e) => utils.commonLoggers.dbError('stats.ts:23', 'query', e));
|
||||
const totalQuery = await dbClient.query(`SELECT SUM(count) as count, SUM(hourlyRate) as hourlyRate FROM command_cnt;`).catch((e) => utils.commonLoggers.dbError('stats.ts:24', 'query', e));
|
||||
const rolls = BigInt(rollQuery[0].count);
|
||||
const rollRate = parseFloat(rollQuery[0].hourlyRate);
|
||||
const total = BigInt(totalQuery[0].count);
|
||||
const totalRate = parseFloat(totalQuery[0].hourlyRate);
|
||||
|
||||
const cachedGuilds = await cacheHandlers.size('guilds');
|
||||
const cachedChannels = await cacheHandlers.size('channels');
|
||||
const cachedMembers = await cacheHandlers.size('members');
|
||||
m.edit(generateStats(cachedGuilds + cache.dispatchedGuildIds.size, cachedChannels + cache.dispatchedChannelIds.size, cachedMembers, rolls, total - rolls, rollRate, totalRate - rollRate)).catch((
|
||||
e: Error,
|
||||
) => utils.commonLoggers.messageEditError('stats.ts:38', m, e));
|
||||
} catch (e) {
|
||||
utils.commonLoggers.messageSendError('stats.ts:41', message, e);
|
||||
}
|
||||
};
|
|
@ -0,0 +1,20 @@
|
|||
import config from '../../config.ts';
|
||||
import { dbClient, queries } from '../db.ts';
|
||||
import {
|
||||
// Discordeno deps
|
||||
DiscordenoMessage,
|
||||
} from '../../deps.ts';
|
||||
import { infoColor1 } from '../commandUtils.ts';
|
||||
import utils from '../utils.ts';
|
||||
|
||||
export const version = (message: DiscordenoMessage) => {
|
||||
// Light telemetry to see how many times a command is being run
|
||||
dbClient.execute(queries.callIncCnt('version')).catch((e) => utils.commonLoggers.dbError('version.ts:15', 'call sproc INC_CNT on', e));
|
||||
|
||||
message.send({
|
||||
embeds: [{
|
||||
color: infoColor1,
|
||||
title: `My current version is ${config.version}`,
|
||||
}],
|
||||
}).catch((e: Error) => utils.commonLoggers.messageSendError('version.ts:24', message, e));
|
||||
};
|
|
@ -0,0 +1,8 @@
|
|||
import { warnColor } from './commandUtils.ts';
|
||||
|
||||
export const compilingStats = {
|
||||
embeds: [{
|
||||
color: warnColor,
|
||||
title: 'Compiling latest statistics . . .',
|
||||
}],
|
||||
};
|
|
@ -0,0 +1,30 @@
|
|||
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,
|
||||
db: config.db.name,
|
||||
username: config.db.username,
|
||||
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 = {
|
||||
insertRollLogCmd: (api: number, error: number) => `INSERT INTO roll_log(input,result,resultid,api,error) values(?,?,?,${api},${error})`,
|
||||
callIncCnt: (cmdName: string) => `CALL INC_CNT("${cmdName}");`,
|
||||
callIncHeatmap: (dateObj: Date) => `CALL INC_HEATMAP("${weekDays[dateObj.getDay()]}", ${dateObj.getHours()});`,
|
||||
};
|
|
@ -0,0 +1,31 @@
|
|||
import { apiKeyDelete } from './deletes/apiKeyDelete.ts';
|
||||
import { apiKey } from './gets/apiKey.ts';
|
||||
import { apiRoll } from './gets/apiRoll.ts';
|
||||
import { apiKeyAdmin } from './gets/apiKeyAdmin.ts';
|
||||
import { apiChannel } from './gets/apiChannel.ts';
|
||||
import { heatmapPng } from './gets/heatmapPng.ts';
|
||||
import { apiChannelAdd } from './posts/apiChannelAdd.ts';
|
||||
import { apiKeyManage } from './puts/apiKeyManage.ts';
|
||||
import { apiChannelManageBan } from './puts/apiChannelManageBan.ts';
|
||||
import { apiChannelManageActive } from './puts/apiChannelManageActive.ts';
|
||||
|
||||
export default {
|
||||
delete: {
|
||||
apiKeyDelete,
|
||||
},
|
||||
get: {
|
||||
apiKey,
|
||||
apiRoll,
|
||||
apiKeyAdmin,
|
||||
apiChannel,
|
||||
heatmapPng,
|
||||
},
|
||||
post: {
|
||||
apiChannelAdd,
|
||||
},
|
||||
put: {
|
||||
apiKeyManage,
|
||||
apiChannelManageBan,
|
||||
apiChannelManageActive,
|
||||
},
|
||||
};
|
|
@ -0,0 +1,83 @@
|
|||
import config from '../../../config.ts';
|
||||
import { dbClient } from '../../db.ts';
|
||||
import {
|
||||
// nanoid deps
|
||||
nanoid,
|
||||
// Discordeno deps
|
||||
sendMessage,
|
||||
} from '../../../deps.ts';
|
||||
import { generateApiDeleteEmail } from '../../commandUtils.ts';
|
||||
import utils from '../../utils.ts';
|
||||
import stdResp from '../stdResponses.ts';
|
||||
|
||||
export const apiKeyDelete = async (requestEvent: Deno.RequestEvent, query: Map<string, string>, apiUserid: BigInt, apiUserEmail: string, apiUserDelCode: string) => {
|
||||
if (query.has('user') && ((query.get('user') || '').length > 0) && query.has('email') && ((query.get('email') || '').length > 0)) {
|
||||
if (apiUserid === BigInt(query.get('user') || '0') && 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) => {
|
||||
utils.commonLoggers.dbError('apiKeyDelete.ts:25', 'insert into', e);
|
||||
requestEvent.respondWith(stdResp.InternalServerError('Channel Clean Failed.'));
|
||||
erroredOut = true;
|
||||
});
|
||||
if (erroredOut) {
|
||||
return;
|
||||
}
|
||||
|
||||
await dbClient.execute('DELETE FROM all_keys WHERE userid = ?', [apiUserid]).catch((e) => {
|
||||
utils.commonLoggers.dbError('apiKeyDelete.ts:34', 'delete from', e);
|
||||
requestEvent.respondWith(stdResp.InternalServerError('Delete Key Failed.'));
|
||||
erroredOut = true;
|
||||
});
|
||||
if (erroredOut) {
|
||||
return;
|
||||
} else {
|
||||
// Send OK as response to indicate key deletion was successful
|
||||
requestEvent.respondWith(stdResp.OK('You have been removed from the DB, Goodbye.'));
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// Alert API user that they shouldn't be doing this
|
||||
requestEvent.respondWith(stdResp.Forbidden('Invalid Delete Code.'));
|
||||
}
|
||||
} 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) => {
|
||||
utils.commonLoggers.dbError('apiKeyDelete.ts:57', 'update', e);
|
||||
requestEvent.respondWith(stdResp.InternalServerError('Delete Code Failed'));
|
||||
erroredOut = true;
|
||||
});
|
||||
if (erroredOut) {
|
||||
return;
|
||||
}
|
||||
|
||||
// "Send" the email
|
||||
await sendMessage(config.api.email, generateApiDeleteEmail(apiUserEmail, deleteCode)).catch(() => {
|
||||
requestEvent.respondWith(stdResp.InternalServerError('Failed to send email.'));
|
||||
erroredOut = true;
|
||||
});
|
||||
if (erroredOut) {
|
||||
return;
|
||||
} else {
|
||||
// Send API key as response
|
||||
requestEvent.respondWith(stdResp.FailedDependency('Please look for an email containing a Delete Key and run this query again with said key.'));
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Alert API user that they shouldn't be doing this
|
||||
requestEvent.respondWith(stdResp.Forbidden('You can only delete your own key.'));
|
||||
}
|
||||
} else {
|
||||
// Alert API user that they messed up
|
||||
requestEvent.respondWith(stdResp.BadRequest(stdResp.Strings.missingParams));
|
||||
}
|
||||
};
|
|
@ -0,0 +1,35 @@
|
|||
import { dbClient } from '../../db.ts';
|
||||
import stdResp from '../stdResponses.ts';
|
||||
import utils from '../../utils.ts';
|
||||
|
||||
export const apiChannel = async (requestEvent: Deno.RequestEvent, query: Map<string, string>, apiUserid: BigInt) => {
|
||||
if (query.has('user') && ((query.get('user') || '').length > 0)) {
|
||||
if (apiUserid === BigInt(query.get('user') || '0')) {
|
||||
// Flag to see if there is an error inside the catch
|
||||
let erroredOut = false;
|
||||
|
||||
// Get all channels userid has authorized
|
||||
const dbAllowedChannelQuery = await dbClient.query('SELECT * FROM allowed_channels WHERE userid = ?', [apiUserid]).catch((e) => {
|
||||
utils.commonLoggers.dbError('apiChannel.ts', 'query', e);
|
||||
requestEvent.respondWith(stdResp.InternalServerError('Failed to get channels.'));
|
||||
erroredOut = true;
|
||||
});
|
||||
|
||||
if (erroredOut) {
|
||||
return;
|
||||
} else {
|
||||
// Customized strinification to handle BigInts correctly
|
||||
const returnChannels = JSON.stringify(dbAllowedChannelQuery, (_key, value) => (typeof value === 'bigint' ? value.toString() : value));
|
||||
// Send channel list as response
|
||||
requestEvent.respondWith(stdResp.OK(returnChannels));
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// Alert API user that they shouldn't be doing this
|
||||
requestEvent.respondWith(stdResp.Forbidden('You can only view your own channels.'));
|
||||
}
|
||||
} else {
|
||||
// Alert API user that they messed up
|
||||
requestEvent.respondWith(stdResp.BadRequest(stdResp.Strings.missingParams));
|
||||
}
|
||||
};
|
|
@ -0,0 +1,52 @@
|
|||
import config from '../../../config.ts';
|
||||
import { dbClient } from '../../db.ts';
|
||||
import {
|
||||
// nanoid deps
|
||||
nanoid,
|
||||
// Discordeno deps
|
||||
sendMessage,
|
||||
} from '../../../deps.ts';
|
||||
import { generateApiKeyEmail } from '../../commandUtils.ts';
|
||||
import utils from '../../utils.ts';
|
||||
import stdResp from '../stdResponses.ts';
|
||||
|
||||
export const apiKey = async (requestEvent: Deno.RequestEvent, query: Map<string, string>) => {
|
||||
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') || '0'), newKey, (query.get('email') || '').toLowerCase()]).catch(
|
||||
(e) => {
|
||||
utils.commonLoggers.dbError('apiKey.ts:27', 'insert into', e);
|
||||
requestEvent.respondWith(stdResp.InternalServerError('Failed to store key.'));
|
||||
erroredOut = true;
|
||||
},
|
||||
);
|
||||
|
||||
// Exit this case now if catch errored
|
||||
if (erroredOut) {
|
||||
return;
|
||||
}
|
||||
|
||||
// "Send" the email
|
||||
await sendMessage(config.api.email, generateApiKeyEmail(query.get('email') || 'no email', newKey)).catch(() => {
|
||||
requestEvent.respondWith(stdResp.InternalServerError('Failed to send email.'));
|
||||
erroredOut = true;
|
||||
});
|
||||
|
||||
if (erroredOut) {
|
||||
return;
|
||||
} else {
|
||||
// Send basic OK to indicate key has been sent
|
||||
requestEvent.respondWith(stdResp.OK('Email Sent.'));
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// Alert API user that they messed up
|
||||
requestEvent.respondWith(stdResp.BadRequest(stdResp.Strings.missingParams));
|
||||
}
|
||||
};
|
|
@ -0,0 +1,42 @@
|
|||
import config from '../../../config.ts';
|
||||
import { dbClient } from '../../db.ts';
|
||||
import {
|
||||
// nanoid deps
|
||||
nanoid,
|
||||
} from '../../../deps.ts';
|
||||
import stdResp from '../stdResponses.ts';
|
||||
import utils from '../../utils.ts';
|
||||
|
||||
export const apiKeyAdmin = async (requestEvent: Deno.RequestEvent, query: Map<string, string>, apiUserid: BigInt) => {
|
||||
if ((query.has('user') && ((query.get('user') || '').length > 0)) && (query.has('a') && ((query.get('a') || '').length > 0))) {
|
||||
if (apiUserid === config.api.admin && apiUserid === BigInt(query.get('a') || '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) values(?,?)', [apiUserid, newKey]).catch((e) => {
|
||||
utils.commonLoggers.dbError('apiKeyAdmin.ts:24', 'insert into', e);
|
||||
requestEvent.respondWith(stdResp.InternalServerError('Failed to store key.'));
|
||||
erroredOut = true;
|
||||
});
|
||||
|
||||
// Exit this case now if catch errored
|
||||
if (erroredOut) {
|
||||
return;
|
||||
} else {
|
||||
// Send API key as response
|
||||
requestEvent.respondWith(stdResp.OK(JSON.stringify({ 'key': newKey, 'userid': query.get('user') })));
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// Only allow the db admin to use this API
|
||||
requestEvent.respondWith(stdResp.Forbidden(stdResp.Strings.restricted));
|
||||
}
|
||||
} else {
|
||||
// Alert API user that they messed up
|
||||
requestEvent.respondWith(stdResp.BadRequest(stdResp.Strings.missingParams));
|
||||
}
|
||||
};
|
|
@ -0,0 +1,121 @@
|
|||
import config from '../../../config.ts';
|
||||
import { dbClient, queries } from '../../db.ts';
|
||||
import {
|
||||
// Discordeno deps
|
||||
cache,
|
||||
// Log4Deno deps
|
||||
log,
|
||||
LT,
|
||||
} from '../../../deps.ts';
|
||||
import { QueuedRoll, RollModifiers } from '../../mod.d.ts';
|
||||
import utils from '../../utils.ts';
|
||||
import { queueRoll } from '../../solver/rollQueue.ts';
|
||||
import stdResp from '../stdResponses.ts';
|
||||
|
||||
const apiWarning = `The following roll was conducted using my built in API. If someone in this channel did not request this roll, please report API abuse here: <${config.api.supportURL}>`;
|
||||
|
||||
export const apiRoll = async (requestEvent: Deno.RequestEvent, query: Map<string, string>, apiUserid: BigInt) => {
|
||||
// Make sure query contains all the needed parts
|
||||
if (
|
||||
(query.has('rollstr') && ((query.get('rollstr') || '').length > 0)) && (query.has('channel') && ((query.get('channel') || '').length > 0)) &&
|
||||
(query.has('user') && ((query.get('user') || '').length > 0))
|
||||
) {
|
||||
if (query.has('n') && query.has('m')) {
|
||||
// Alert API user that they shouldn't be doing this
|
||||
requestEvent.respondWith(stdResp.BadRequest('Cannot have both \'n\' and \'m\'.'));
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if user is authenticated to use this endpoint
|
||||
let authorized = false;
|
||||
let hideWarn = 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 = ?', [apiUserid, BigInt(query.get('channel') || '0')]);
|
||||
if (dbChannelQuery.length === 1 && (apiUserid === BigInt(query.get('user') || '0')) && 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(BigInt(query.get('channel') || ''))?.guild;
|
||||
if (guild && guild.members.get(BigInt(query.get('user') || ''))?.id) {
|
||||
const dbGuildQuery = await dbClient.query('SELECT active, banned, hidewarn FROM allowed_guilds WHERE guildid = ? AND channelid = ?', [
|
||||
guild.id,
|
||||
BigInt(query.get('channel') || '0'),
|
||||
]);
|
||||
|
||||
// Make sure guild allows API rolls
|
||||
if (dbGuildQuery.length === 1 && dbGuildQuery[0].active && !dbGuildQuery[0].banned) {
|
||||
authorized = true;
|
||||
hideWarn = dbGuildQuery[0].hidewarn;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (authorized) {
|
||||
// Rest of this command is in a try-catch to protect all sends/edits from erroring out
|
||||
try {
|
||||
// Make sure rollCmd is not undefined
|
||||
let rollCmd = query.get('rollstr') || '';
|
||||
const originalCommand = query.get('rollstr');
|
||||
|
||||
if (rollCmd.length === 0) {
|
||||
// Alert API user that they messed up
|
||||
requestEvent.respondWith(stdResp.BadRequest('rollCmd is required.'));
|
||||
|
||||
// Always log API rolls for abuse detection
|
||||
dbClient.execute(queries.insertRollLogCmd(1, 1), [originalCommand, 'EmptyInput', null]).catch((e) => utils.commonLoggers.dbError('apiRoll.ts:65', 'insert', e));
|
||||
return;
|
||||
}
|
||||
|
||||
if (query.has('o') && (query.get('o')?.toLowerCase() !== 'd' && query.get('o')?.toLowerCase() !== 'a')) {
|
||||
// Alert API user that they messed up
|
||||
requestEvent.respondWith(stdResp.BadRequest('Order must be set to \'a\' or \'d\'.'));
|
||||
|
||||
// Always log API rolls for abuse detection
|
||||
dbClient.execute(queries.insertRollLogCmd(1, 1), [originalCommand, 'BadOrder', null]).catch((e) => utils.commonLoggers.dbError('apiRoll.ts:66', 'insert', e));
|
||||
return;
|
||||
}
|
||||
|
||||
// Clip off the leading prefix. API calls must be formatted with a prefix at the start to match how commands are sent in Discord
|
||||
rollCmd = rollCmd.substring(rollCmd.indexOf(config.prefix) + 2).replace(/%20/g, ' ');
|
||||
|
||||
const modifiers: RollModifiers = {
|
||||
noDetails: query.has('nd'),
|
||||
superNoDetails: query.has('snd'),
|
||||
spoiler: query.has('s') ? '||' : '',
|
||||
maxRoll: query.has('m'),
|
||||
nominalRoll: query.has('n'),
|
||||
gmRoll: query.has('gms'),
|
||||
gms: query.has('gms') ? (query.get('gms') || '').split(',') : [],
|
||||
order: query.has('o') ? (query.get('o')?.toLowerCase() || '') : '',
|
||||
count: query.has('c'),
|
||||
valid: true,
|
||||
apiWarn: hideWarn ? '' : apiWarning,
|
||||
};
|
||||
|
||||
// Parse the roll and get the return text
|
||||
await queueRoll(
|
||||
<QueuedRoll> {
|
||||
apiRoll: true,
|
||||
api: { requestEvent, channelId: BigInt(query.get('channel') || '0'), userId: BigInt(query.get('user') || '') },
|
||||
rollCmd,
|
||||
modifiers,
|
||||
originalCommand,
|
||||
},
|
||||
);
|
||||
} catch (err) {
|
||||
// Handle any errors we missed
|
||||
log(LT.ERROR, `Unhandled Error: ${JSON.stringify(err)}`);
|
||||
requestEvent.respondWith(stdResp.InternalServerError('Something went wrong.'));
|
||||
}
|
||||
} else {
|
||||
// Alert API user that they messed up
|
||||
requestEvent.respondWith(
|
||||
stdResp.Forbidden(
|
||||
`Verify you are a member of the guild you are sending this roll to. If you are, the ${config.name} may not have that registered, please send a message in the guild so ${config.name} can register this. This registration is temporary, so if you see this error again, just poke your server again.`,
|
||||
),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// Alert API user that they shouldn't be doing this
|
||||
requestEvent.respondWith(stdResp.BadRequest(stdResp.Strings.missingParams));
|
||||
}
|
||||
};
|
Binary file not shown.
After Width: | Height: | Size: 57 KiB |
|
@ -0,0 +1,19 @@
|
|||
import {
|
||||
// httpd deps
|
||||
Status,
|
||||
STATUS_TEXT,
|
||||
} from '../../../deps.ts';
|
||||
|
||||
export const heatmapPng = async (requestEvent: Deno.RequestEvent) => {
|
||||
const file = Deno.readFileSync('./src/endpoints/gets/heatmap.png');
|
||||
const imageHeaders = new Headers();
|
||||
imageHeaders.append('Content-Type', 'image/png');
|
||||
// Send basic OK to indicate key has been sent
|
||||
requestEvent.respondWith(
|
||||
new Response(file, {
|
||||
status: Status.OK,
|
||||
statusText: STATUS_TEXT[Status.OK],
|
||||
headers: imageHeaders,
|
||||
}),
|
||||
);
|
||||
};
|
|
@ -0,0 +1,34 @@
|
|||
import { dbClient } from '../../db.ts';
|
||||
import stdResp from '../stdResponses.ts';
|
||||
import utils from '../../utils.ts';
|
||||
|
||||
export const apiChannelAdd = async (requestEvent: Deno.RequestEvent, query: Map<string, string>, apiUserid: BigInt) => {
|
||||
if ((query.has('user') && ((query.get('user') || '').length > 0)) && (query.has('channel') && ((query.get('channel') || '').length > 0))) {
|
||||
if (apiUserid === BigInt(query.get('user') || '0')) {
|
||||
// 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') || '0')]).catch((e) => {
|
||||
utils.commonLoggers.dbError('apiChannelAdd.ts:17', 'insert into', e);
|
||||
requestEvent.respondWith(stdResp.InternalServerError('Failed to store channel.'));
|
||||
erroredOut = true;
|
||||
});
|
||||
|
||||
// Exit this case now if catch errored
|
||||
if (erroredOut) {
|
||||
return;
|
||||
} else {
|
||||
// Send OK to indicate modification was successful
|
||||
requestEvent.respondWith(stdResp.OK('Successfully added channel.'));
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// Alert API user that they shouldn't be doing this
|
||||
requestEvent.respondWith(stdResp.Forbidden('You can only add channels to your key.'));
|
||||
}
|
||||
} else {
|
||||
// Alert API user that they messed up
|
||||
requestEvent.respondWith(stdResp.BadRequest(stdResp.Strings.missingParams));
|
||||
}
|
||||
};
|
|
@ -0,0 +1,41 @@
|
|||
import { dbClient } from '../../db.ts';
|
||||
import stdResp from '../stdResponses.ts';
|
||||
import utils from '../../utils.ts';
|
||||
|
||||
export const apiChannelManageActive = async (requestEvent: Deno.RequestEvent, query: Map<string, string>, apiUserid: BigInt, path: string) => {
|
||||
if ((query.has('channel') && ((query.get('channel') || '').length > 0)) && (query.has('user') && ((query.get('user') || '').length > 0))) {
|
||||
if (apiUserid === BigInt(query.get('user') || '0')) {
|
||||
// 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') || '0')]).catch((e) => {
|
||||
utils.commonLoggers.dbError('apiChannelManageActive.ts:25', 'update', e);
|
||||
requestEvent.respondWith(stdResp.InternalServerError('Failed to update channel.'));
|
||||
erroredOut = true;
|
||||
});
|
||||
|
||||
// Exit this case now if catch errored
|
||||
if (erroredOut) {
|
||||
return;
|
||||
} else {
|
||||
// Send API key as response
|
||||
requestEvent.respondWith(stdResp.OK(`Successfully active to ${value}.`));
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// Alert API user that they shouldn't be doing this
|
||||
requestEvent.respondWith(stdResp.Forbidden('You can only manage your own channels.'));
|
||||
}
|
||||
} else {
|
||||
// Alert API user that they messed up
|
||||
requestEvent.respondWith(stdResp.BadRequest(stdResp.Strings.missingParams));
|
||||
}
|
||||
};
|
|
@ -0,0 +1,45 @@
|
|||
import config from '../../../config.ts';
|
||||
import { dbClient } from '../../db.ts';
|
||||
import stdResp from '../stdResponses.ts';
|
||||
import utils from '../../utils.ts';
|
||||
|
||||
export const apiChannelManageBan = async (requestEvent: Deno.RequestEvent, query: Map<string, string>, apiUserid: BigInt, path: string) => {
|
||||
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') || '0')) {
|
||||
// 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') || '0')]).catch((e) => {
|
||||
utils.commonLoggers.dbError('apiChannelManageBan.ts:28', 'update', e);
|
||||
requestEvent.respondWith(stdResp.InternalServerError('Failed to update channel.'));
|
||||
erroredOut = true;
|
||||
});
|
||||
|
||||
// Exit this case now if catch errored
|
||||
if (erroredOut) {
|
||||
return;
|
||||
} else {
|
||||
// Send OK to indicate modification was successful
|
||||
requestEvent.respondWith(stdResp.OK(`Successfully active to ${value}.`));
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// Alert API user that they shouldn't be doing this
|
||||
requestEvent.respondWith(stdResp.Forbidden(stdResp.Strings.restricted));
|
||||
}
|
||||
} else {
|
||||
// Alert API user that they messed up
|
||||
requestEvent.respondWith(stdResp.BadRequest(stdResp.Strings.missingParams));
|
||||
}
|
||||
};
|
|
@ -0,0 +1,51 @@
|
|||
import config from '../../../config.ts';
|
||||
import { dbClient } from '../../db.ts';
|
||||
import stdResp from '../stdResponses.ts';
|
||||
import utils from '../../utils.ts';
|
||||
|
||||
export const apiKeyManage = async (requestEvent: Deno.RequestEvent, query: Map<string, string>, apiUserid: BigInt, path: string) => {
|
||||
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') || '0')) {
|
||||
// Flag to see if there is an error inside the catch
|
||||
let key: string,
|
||||
value: number,
|
||||
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) => {
|
||||
utils.commonLoggers.dbError('apiKeyManage.ts', 'update', e);
|
||||
requestEvent.respondWith(stdResp.InternalServerError(`Failed to ${key} to ${value}.`));
|
||||
erroredOut = true;
|
||||
});
|
||||
|
||||
// Exit this case now if catch errored
|
||||
if (erroredOut) {
|
||||
return;
|
||||
} else {
|
||||
// Send OK as response to indicate modification was successful
|
||||
requestEvent.respondWith(stdResp.OK(`Successfully ${key} to ${value}.`));
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// Alert API user that they shouldn't be doing this
|
||||
requestEvent.respondWith(stdResp.Forbidden('You can only manage your own key.'));
|
||||
}
|
||||
} else {
|
||||
// Alert API user that they messed up
|
||||
requestEvent.respondWith(stdResp.BadRequest(stdResp.Strings.missingParams));
|
||||
}
|
||||
};
|
|
@ -0,0 +1,23 @@
|
|||
import {
|
||||
// httpd deps
|
||||
Status,
|
||||
STATUS_TEXT,
|
||||
} from '../../deps.ts';
|
||||
|
||||
const genericResponse = (customText: string, status: Status) => new Response(customText || STATUS_TEXT[status], { status: status, statusText: STATUS_TEXT[status] });
|
||||
|
||||
export default {
|
||||
BadRequest: (customText: string) => genericResponse(customText, Status.BadRequest),
|
||||
FailedDependency: (customText: string) => genericResponse(customText, Status.FailedDependency),
|
||||
InternalServerError: (customText: string) => genericResponse(customText, Status.InternalServerError),
|
||||
Forbidden: (customText: string) => genericResponse(customText, Status.Forbidden),
|
||||
MethodNotAllowed: (customText: string) => genericResponse(customText, Status.MethodNotAllowed),
|
||||
NotFound: (customText: string) => genericResponse(customText, Status.NotFound),
|
||||
OK: (customText: string) => genericResponse(customText, Status.OK),
|
||||
RequestTimeout: (customText: string) => genericResponse(customText, Status.RequestTimeout),
|
||||
TooManyRequests: (customText: string) => genericResponse(customText, Status.TooManyRequests),
|
||||
Strings: {
|
||||
missingParams: 'Missing Parameters.',
|
||||
restricted: 'This API is restricted.',
|
||||
},
|
||||
};
|
199
src/intervals.ts
199
src/intervals.ts
|
@ -6,15 +6,23 @@
|
|||
|
||||
import {
|
||||
// Discordeno deps
|
||||
CacheData
|
||||
} from "../deps.ts";
|
||||
cache,
|
||||
cacheHandlers,
|
||||
// imagescript dep
|
||||
is,
|
||||
// Log4Deno deps
|
||||
log,
|
||||
LT,
|
||||
} from '../deps.ts';
|
||||
import { PastCommandCount } from './mod.d.ts';
|
||||
import { dbClient, weekDays } from './db.ts';
|
||||
import utils from './utils.ts';
|
||||
import config from '../config.ts';
|
||||
|
||||
import config from "../config.ts";
|
||||
|
||||
// getRandomStatus(bot cache) returns status as string
|
||||
// getRandomStatus() returns status as string
|
||||
// Gets a new random status for the bot
|
||||
const getRandomStatus = (cache: CacheData): string => {
|
||||
let status = "";
|
||||
const getRandomStatus = async (): Promise<string> => {
|
||||
let status = '';
|
||||
switch (Math.floor((Math.random() * 4) + 1)) {
|
||||
case 1:
|
||||
status = `${config.prefix}help for commands`;
|
||||
|
@ -25,12 +33,181 @@ const getRandomStatus = (cache: CacheData): string => {
|
|||
case 3:
|
||||
status = `${config.prefix}info to learn more`;
|
||||
break;
|
||||
default:
|
||||
status = `Rolling dice for ${cache.guilds.size} servers`;
|
||||
default: {
|
||||
const cachedCount = await cacheHandlers.size('guilds');
|
||||
status = `Rolling dice for ${cachedCount + cache.dispatchedGuildIds.size} servers`;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return status;
|
||||
};
|
||||
|
||||
export default { getRandomStatus };
|
||||
// updateListStatistics(bot ID, current guild count) returns nothing, posts to botlists
|
||||
// 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}`)
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Keep one week of data
|
||||
const hoursToKeep = 7 * 24;
|
||||
const previousHours: Array<Array<PastCommandCount>> = [];
|
||||
// updateHourlyRates() returns nothing, updates DB directly
|
||||
// Updates the hourlyRate for command usage
|
||||
const updateHourlyRates = async () => {
|
||||
try {
|
||||
const newestHour = await dbClient.query('SELECT command, count FROM command_cnt ORDER BY command;').catch((e) => utils.commonLoggers.dbError('intervals.ts:71', 'query', e));
|
||||
previousHours.push(newestHour);
|
||||
if (previousHours.length > 1) {
|
||||
const oldestHour = previousHours[0];
|
||||
|
||||
const computedDiff: Array<PastCommandCount> = [];
|
||||
for (let i = 0; i < newestHour.length; i++) {
|
||||
computedDiff.push({
|
||||
command: newestHour[i].command,
|
||||
count: (newestHour[i].count - oldestHour[i].count),
|
||||
});
|
||||
log(LT.LOG, `Updating hourlyRate | Computing diffs: ${JSON.stringify(computedDiff)}`);
|
||||
}
|
||||
|
||||
// Update DB
|
||||
computedDiff.forEach(async (cmd) => {
|
||||
log(LT.LOG, `Updating hourlyRate | Storing to DB: ${JSON.stringify(cmd)}`);
|
||||
await dbClient.execute(`UPDATE command_cnt SET hourlyRate = ? WHERE command = ?`, [cmd.count / previousHours.length, cmd.command]).catch((e) =>
|
||||
utils.commonLoggers.dbError('intervals.ts:88', 'update', e)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
if (previousHours.length > hoursToKeep) {
|
||||
previousHours.unshift();
|
||||
}
|
||||
} catch (e) {
|
||||
log(LT.ERROR, `Something went wrong in previousHours interval | Error: ${e.name} - ${e.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
// getPercentOfRange(min, max, val) returns number
|
||||
// Gets a percent value of where val lies in the min-max range
|
||||
const getPercentOfRange = (minVal: number, maxVal: number, val: number): number => {
|
||||
const localMax = maxVal - minVal;
|
||||
const localVal = val - minVal;
|
||||
|
||||
return localVal / localMax;
|
||||
};
|
||||
|
||||
// Pixel locations in heatmap-base.png, pixel locations are 0 based
|
||||
// dayPixels holds the left and right (AKA X Coord) pixel locations for each col (ex: [leftPX, rightPX])
|
||||
const dayPixels: Array<Array<number>> = [
|
||||
[72, 159],
|
||||
[163, 260],
|
||||
[264, 359],
|
||||
[363, 497],
|
||||
[501, 608],
|
||||
[612, 686],
|
||||
[690, 800],
|
||||
];
|
||||
// hourPixels holds the top and bottom (AKA Y Coord) pixel locations for each row (ex: [topPX, botPX])
|
||||
const hourPixels: Array<Array<number>> = [
|
||||
[29, 49],
|
||||
[51, 72],
|
||||
[74, 95],
|
||||
[97, 118],
|
||||
[120, 141],
|
||||
[143, 164],
|
||||
[166, 187],
|
||||
[189, 209],
|
||||
[211, 232],
|
||||
[234, 254],
|
||||
[256, 277],
|
||||
[279, 299],
|
||||
[301, 322],
|
||||
[324, 345],
|
||||
[347, 368],
|
||||
[370, 391],
|
||||
[393, 413],
|
||||
[415, 436],
|
||||
[438, 459],
|
||||
[461, 482],
|
||||
[484, 505],
|
||||
[507, 528],
|
||||
[530, 550],
|
||||
[552, 572],
|
||||
];
|
||||
// updateHeatmap() returns nothing, creates new heatmap.png
|
||||
// Updates the heatmap image with latest data from the db
|
||||
let minRollCnt: number;
|
||||
let maxRollCnt: number;
|
||||
const updateHeatmapPng = async () => {
|
||||
const baseHeatmap = Deno.readFileSync('./src/endpoints/gets/heatmap-base.png');
|
||||
const heatmap = await is.decode(baseHeatmap);
|
||||
if (!(heatmap instanceof is.Image)) {
|
||||
return;
|
||||
}
|
||||
// Get latest data from DB
|
||||
const heatmapData = await dbClient.query('SELECT * FROM roll_time_heatmap ORDER BY hour;').catch((e) => utils.commonLoggers.dbError('intervals.ts:148', 'query', e));
|
||||
|
||||
minRollCnt = Infinity;
|
||||
maxRollCnt = 0;
|
||||
// determine min and max values
|
||||
for (const hour of heatmapData) {
|
||||
for (const day of weekDays) {
|
||||
const rollCnt = hour[day];
|
||||
log(LT.LOG, `updateHeatmapPng | finding min/max | min: ${minRollCnt} max: ${maxRollCnt} curr: ${rollCnt}`);
|
||||
if (rollCnt > maxRollCnt) {
|
||||
maxRollCnt = rollCnt;
|
||||
}
|
||||
if (rollCnt < minRollCnt) {
|
||||
minRollCnt = rollCnt;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Apply values to image
|
||||
for (let hour = 0; hour < heatmapData.length; hour++) {
|
||||
for (let day = 0; day < weekDays.length; day++) {
|
||||
log(LT.LOG, `updateHeatmapPng | putting ${weekDays[day]} ${hour}:00 into image`);
|
||||
const percent = getPercentOfRange(minRollCnt, maxRollCnt, heatmapData[hour][weekDays[day]]);
|
||||
heatmap.drawBox(
|
||||
dayPixels[day][0] + 1,
|
||||
hourPixels[hour][0] + 1,
|
||||
dayPixels[day][1] - dayPixels[day][0] + 1,
|
||||
hourPixels[hour][1] - hourPixels[hour][0] + 1,
|
||||
is.Image.rgbToColor(
|
||||
255 * (1 - percent),
|
||||
255 * percent,
|
||||
0,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Deno.writeFileSync('./src/endpoints/gets/heatmap.png', await heatmap.encode());
|
||||
};
|
||||
|
||||
export default {
|
||||
getRandomStatus,
|
||||
updateListStatistics,
|
||||
updateHourlyRates,
|
||||
updateHeatmapPng,
|
||||
getMinRollCnt: () => minRollCnt,
|
||||
getMaxRollCnt: () => maxRollCnt,
|
||||
};
|
||||
|
|
|
@ -1,10 +1,48 @@
|
|||
// mod.d.ts custom types
|
||||
import { DiscordenoMessage } from '../deps.ts';
|
||||
|
||||
// EmojiConf is used as a structure for the emojis stored in config.ts
|
||||
export type EmojiConf = {
|
||||
"name": string,
|
||||
"aliases": Array<string>,
|
||||
"id": string,
|
||||
"animated": boolean,
|
||||
"deleteSender": boolean
|
||||
name: string;
|
||||
aliases: Array<string>;
|
||||
id: string;
|
||||
animated: boolean;
|
||||
deleteSender: boolean;
|
||||
};
|
||||
|
||||
// RollModifiers is the structure to keep track of the decorators applied to a roll command
|
||||
export type RollModifiers = {
|
||||
noDetails: boolean;
|
||||
superNoDetails: boolean;
|
||||
spoiler: string;
|
||||
maxRoll: boolean;
|
||||
nominalRoll: boolean;
|
||||
gmRoll: boolean;
|
||||
gms: string[];
|
||||
order: string;
|
||||
count: boolean;
|
||||
valid: boolean;
|
||||
apiWarn: string;
|
||||
};
|
||||
|
||||
// QueuedRoll is the structure to track rolls we could not immediately handle
|
||||
export type QueuedRoll = {
|
||||
apiRoll: boolean;
|
||||
api: {
|
||||
requestEvent: Deno.RequestEvent;
|
||||
channelId: bigint;
|
||||
userId: bigint;
|
||||
};
|
||||
dd: {
|
||||
m: DiscordenoMessage;
|
||||
message: DiscordenoMessage;
|
||||
};
|
||||
originalCommand: string;
|
||||
rollCmd: string;
|
||||
modifiers: RollModifiers;
|
||||
};
|
||||
|
||||
export type PastCommandCount = {
|
||||
command: string;
|
||||
count: number;
|
||||
};
|
||||
|
|
|
@ -1,40 +0,0 @@
|
|||
// solver.ts custom types
|
||||
|
||||
// RollSet is used to preserve all information about a calculated roll
|
||||
export type RollSet = {
|
||||
origidx: number,
|
||||
roll: number,
|
||||
dropped: boolean,
|
||||
rerolled: boolean,
|
||||
exploding: boolean,
|
||||
critHit: boolean,
|
||||
critFail: boolean
|
||||
};
|
||||
|
||||
// SolvedStep is used to preserve information while math is being performed on the roll
|
||||
export type SolvedStep = {
|
||||
total: number,
|
||||
details: string,
|
||||
containsCrit: boolean,
|
||||
containsFail: boolean
|
||||
};
|
||||
|
||||
// ReturnData is the temporary internal type used before getting turned into SolvedRoll
|
||||
export type ReturnData = {
|
||||
rollTotal: number,
|
||||
rollPostFormat: string,
|
||||
rollDetails: string,
|
||||
containsCrit: boolean,
|
||||
containsFail: boolean,
|
||||
initConfig: string
|
||||
};
|
||||
|
||||
// SolvedRoll is the complete solved and formatted roll, or the error said roll created
|
||||
export type SolvedRoll = {
|
||||
error: boolean,
|
||||
errorMsg: string,
|
||||
errorCode: string,
|
||||
line1: string,
|
||||
line2: string,
|
||||
line3: string
|
||||
};
|
1004
src/solver.ts
1004
src/solver.ts
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,5 @@
|
|||
import { parseRoll } from './parser.ts';
|
||||
|
||||
export default {
|
||||
parseRoll,
|
||||
};
|
|
@ -0,0 +1,23 @@
|
|||
import { CountDetails, RollSet } from './solver.d.ts';
|
||||
|
||||
export const rollCounter = (rollSet: RollSet[]): CountDetails => {
|
||||
const countDetails = {
|
||||
total: 0,
|
||||
successful: 0,
|
||||
failed: 0,
|
||||
rerolled: 0,
|
||||
dropped: 0,
|
||||
exploded: 0,
|
||||
};
|
||||
|
||||
rollSet.forEach((roll) => {
|
||||
countDetails.total++;
|
||||
if (roll.critHit) countDetails.successful++;
|
||||
if (roll.critFail) countDetails.failed++;
|
||||
if (roll.rerolled) countDetails.rerolled++;
|
||||
if (roll.dropped) countDetails.dropped++;
|
||||
if (roll.exploding) countDetails.exploded++;
|
||||
});
|
||||
|
||||
return countDetails;
|
||||
};
|
|
@ -0,0 +1,342 @@
|
|||
import {
|
||||
log,
|
||||
// Log4Deno deps
|
||||
LT,
|
||||
} from '../../deps.ts';
|
||||
|
||||
import config from '../../config.ts';
|
||||
|
||||
import { RollModifiers } from '../mod.d.ts';
|
||||
import { CountDetails, ReturnData, SolvedRoll, SolvedStep } from './solver.d.ts';
|
||||
import { compareTotalRolls, escapeCharacters, loggingEnabled } from './rollUtils.ts';
|
||||
import { formatRoll } from './rollFormatter.ts';
|
||||
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 returnmsg = <SolvedRoll> {
|
||||
error: false,
|
||||
errorCode: '',
|
||||
errorMsg: '',
|
||||
line1: '',
|
||||
line2: '',
|
||||
line3: '',
|
||||
counts: {
|
||||
total: 0,
|
||||
successful: 0,
|
||||
failed: 0,
|
||||
rerolled: 0,
|
||||
dropped: 0,
|
||||
exploded: 0,
|
||||
},
|
||||
};
|
||||
|
||||
// Whole function lives in a try-catch to allow safe throwing of errors on purpose
|
||||
try {
|
||||
// Split the fullCmd by the command prefix to allow every roll/math op to be handled individually
|
||||
const sepRolls = fullCmd.split(config.prefix);
|
||||
|
||||
const tempReturnData: ReturnData[] = [];
|
||||
const tempCountDetails: CountDetails[] = [{
|
||||
total: 0,
|
||||
successful: 0,
|
||||
failed: 0,
|
||||
rerolled: 0,
|
||||
dropped: 0,
|
||||
exploded: 0,
|
||||
}];
|
||||
|
||||
// Loop thru all roll/math ops
|
||||
for (const sepRoll of sepRolls) {
|
||||
loggingEnabled && 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
|
||||
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)
|
||||
const mathConf: (string | number | SolvedStep)[] = <(string | number | SolvedStep)[]> tempConf.replace(/ /g, '').split(/([-+()*/%^])/g);
|
||||
|
||||
// Verify there are equal numbers of opening and closing parenthesis by adding 1 for opening parens and subtracting 1 for closing parens
|
||||
let parenCnt = 0;
|
||||
mathConf.forEach((e) => {
|
||||
loggingEnabled && log(LT.LOG, `Parsing roll ${fullCmd} | Checking parenthesis balance ${e}`);
|
||||
if (e === '(') {
|
||||
parenCnt++;
|
||||
} else if (e === ')') {
|
||||
parenCnt--;
|
||||
}
|
||||
});
|
||||
|
||||
// If the parenCnt is not 0, then we do not have balanced parens and need to error out now
|
||||
if (parenCnt !== 0) {
|
||||
throw new Error('UnbalancedParens');
|
||||
}
|
||||
|
||||
// Evaluate all rolls into stepSolve format and all numbers into floats
|
||||
for (let i = 0; i < mathConf.length; i++) {
|
||||
loggingEnabled && log(LT.LOG, `Parsing roll ${fullCmd} | Evaluating rolls into mathable items ${JSON.stringify(mathConf[i])}`);
|
||||
if (mathConf[i].toString().length === 0) {
|
||||
// If its an empty string, get it out of here
|
||||
mathConf.splice(i, 1);
|
||||
i--;
|
||||
} else if (mathConf[i] == parseFloat(mathConf[i].toString())) {
|
||||
// If its a number, parse the number out
|
||||
mathConf[i] = parseFloat(mathConf[i].toString());
|
||||
} else if (mathConf[i].toString().toLowerCase() === 'e') {
|
||||
// If the operand is the constant e, create a SolvedStep for it
|
||||
mathConf[i] = {
|
||||
total: Math.E,
|
||||
details: '*e*',
|
||||
containsCrit: false,
|
||||
containsFail: false,
|
||||
};
|
||||
} else if (mathConf[i].toString().toLowerCase() === 'fart' || mathConf[i].toString().toLowerCase() === '💩') {
|
||||
mathConf[i] = {
|
||||
total: 7,
|
||||
details: '💩',
|
||||
containsCrit: false,
|
||||
containsFail: false,
|
||||
};
|
||||
} else if (mathConf[i].toString().toLowerCase() === 'inf' || mathConf[i].toString().toLowerCase() === 'infinity' || mathConf[i].toString().toLowerCase() === '∞') {
|
||||
// If the operand is the constant Infinity, create a SolvedStep for it
|
||||
mathConf[i] = {
|
||||
total: Infinity,
|
||||
details: '∞',
|
||||
containsCrit: false,
|
||||
containsFail: false,
|
||||
};
|
||||
} else if (mathConf[i].toString().toLowerCase() === 'pi' || mathConf[i].toString().toLowerCase() === '𝜋') {
|
||||
// If the operand is the constant pi, create a SolvedStep for it
|
||||
mathConf[i] = {
|
||||
total: Math.PI,
|
||||
details: '𝜋',
|
||||
containsCrit: false,
|
||||
containsFail: false,
|
||||
};
|
||||
} else if (mathConf[i].toString().toLowerCase() === 'pie') {
|
||||
// If the operand is pie, pi*e, create a SolvedStep for e and pi (and the multiplication symbol between them)
|
||||
mathConf[i] = {
|
||||
total: Math.PI,
|
||||
details: '𝜋',
|
||||
containsCrit: false,
|
||||
containsFail: false,
|
||||
};
|
||||
mathConf.splice(i + 1, 0, ...['*', {
|
||||
total: Math.E,
|
||||
details: '*e*',
|
||||
containsCrit: false,
|
||||
containsFail: false,
|
||||
}]);
|
||||
} else if (!operators.includes(mathConf[i].toString())) {
|
||||
// If nothing else has handled it by now, try it as a roll
|
||||
const formattedRoll = formatRoll(mathConf[i].toString(), modifiers.maxRoll, modifiers.nominalRoll);
|
||||
mathConf[i] = formattedRoll.solvedStep;
|
||||
tempCountDetails.push(formattedRoll.countDetails);
|
||||
}
|
||||
|
||||
if (mathConf[i - 1] === '-' && ((!mathConf[i - 2] && mathConf[i - 2] !== 0) || mathConf[i - 2] === '(')) {
|
||||
if (typeof mathConf[i] === 'number') {
|
||||
mathConf[i] = <number> mathConf[i] * -1;
|
||||
} else {
|
||||
(<SolvedStep> mathConf[i]).total = (<SolvedStep> mathConf[i]).total * -1;
|
||||
(<SolvedStep> mathConf[i]).details = `-${(<SolvedStep> mathConf[i]).details}`;
|
||||
}
|
||||
mathConf.splice(i - 1, 1);
|
||||
i--;
|
||||
}
|
||||
}
|
||||
|
||||
// Now that mathConf is parsed, send it into the solver
|
||||
const tempSolved = fullSolver(mathConf, false);
|
||||
|
||||
// Push all of this step's solved data into the temp array
|
||||
tempReturnData.push({
|
||||
rollTotal: tempSolved.total,
|
||||
rollPostFormat: tempFormat,
|
||||
rollDetails: tempSolved.details,
|
||||
containsCrit: tempSolved.containsCrit,
|
||||
containsFail: tempSolved.containsFail,
|
||||
initConfig: tempConf,
|
||||
});
|
||||
}
|
||||
|
||||
// Parsing/Solving done, time to format the output for Discord
|
||||
|
||||
// Remove any floating spaces from fullCmd
|
||||
if (fullCmd[fullCmd.length - 1] === ' ') {
|
||||
fullCmd = fullCmd.substring(0, fullCmd.length - 1);
|
||||
}
|
||||
|
||||
// Escape any | and ` chars in fullCmd to prevent spoilers and code blocks from acting up
|
||||
fullCmd = escapeCharacters(fullCmd, '|');
|
||||
fullCmd = fullCmd.replace(/`/g, '');
|
||||
|
||||
let line1 = '';
|
||||
let line2 = '';
|
||||
let line3 = '';
|
||||
|
||||
// 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}\``;
|
||||
line2 = 'Theoretical Maximum Results: ';
|
||||
} else if (modifiers.nominalRoll) {
|
||||
line1 = ` requested the theoretical nominal of:\n\`${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}\``;
|
||||
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}\``;
|
||||
line2 = 'Results: ';
|
||||
tempReturnData.sort(compareTotalRolls);
|
||||
tempReturnData.reverse();
|
||||
} else {
|
||||
line1 = ` rolled:\n\`${config.prefix}${fullCmd}\``;
|
||||
line2 = 'Results: ';
|
||||
}
|
||||
|
||||
// Fill out all of the details and results now
|
||||
tempReturnData.forEach((e) => {
|
||||
loggingEnabled && log(LT.LOG, `Parsing roll ${fullCmd} | Making return text ${JSON.stringify(e)}`);
|
||||
let preFormat = '';
|
||||
let postFormat = '';
|
||||
|
||||
// If the roll containted a crit success or fail, set the formatting around it
|
||||
if (e.containsCrit) {
|
||||
preFormat = `**${preFormat}`;
|
||||
postFormat = `${postFormat}**`;
|
||||
}
|
||||
if (e.containsFail) {
|
||||
preFormat = `__${preFormat}`;
|
||||
postFormat = `${postFormat}__`;
|
||||
}
|
||||
|
||||
// Populate line2 (the results) and line3 (the details) with their data
|
||||
if (modifiers.order === '') {
|
||||
line2 += `${preFormat}${e.rollTotal}${postFormat}${escapeCharacters(e.rollPostFormat, '|*_~`')}`;
|
||||
} else {
|
||||
// If order is on, turn rolls into csv without formatting
|
||||
line2 += `${preFormat}${e.rollTotal}${postFormat}, `;
|
||||
}
|
||||
|
||||
line2 = line2.replace(/\*\*\*\*/g, '** **').replace(/____/g, '__ __').replace(/~~~~/g, '~~ ~~');
|
||||
|
||||
line3 += `\`${e.initConfig}\` = ${e.rollDetails} = ${preFormat}${e.rollTotal}${postFormat}\n`;
|
||||
});
|
||||
|
||||
// If order is on, remove trailing ", "
|
||||
if (modifiers.order !== '') {
|
||||
line2 = line2.substring(0, line2.length - 2);
|
||||
}
|
||||
|
||||
// Fill in the return block
|
||||
returnmsg.line1 = line1;
|
||||
returnmsg.line2 = line2;
|
||||
returnmsg.line3 = line3;
|
||||
|
||||
// Reduce counts to a single object
|
||||
returnmsg.counts = tempCountDetails.reduce((acc, cnt) => ({
|
||||
total: acc.total + cnt.total,
|
||||
successful: acc.successful + cnt.successful,
|
||||
failed: acc.failed + cnt.failed,
|
||||
rerolled: acc.rerolled + cnt.rerolled,
|
||||
dropped: acc.dropped + cnt.dropped,
|
||||
exploded: acc.exploded + cnt.exploded,
|
||||
}));
|
||||
} catch (solverError) {
|
||||
// Welp, the unthinkable happened, we hit an error
|
||||
|
||||
// Split on _ for the error messages that have more info than just their name
|
||||
const [errorName, errorDetails] = solverError.message.split('_');
|
||||
|
||||
let errorMsg = '';
|
||||
|
||||
// Translate the errorName to a specific errorMsg
|
||||
switch (errorName) {
|
||||
case 'YouNeedAD':
|
||||
errorMsg = 'Formatting Error: Missing die size and count config';
|
||||
break;
|
||||
case 'FormattingError':
|
||||
errorMsg = 'Formatting Error: Cannot use Keep and Drop at the same time, remove all but one and repeat roll';
|
||||
break;
|
||||
case 'NoMaxWithDash':
|
||||
errorMsg = 'Formatting Error: CritScore range specified without a maximum, remove - or add maximum to correct';
|
||||
break;
|
||||
case 'UnknownOperation':
|
||||
errorMsg = `Error: Unknown Operation ${errorDetails}`;
|
||||
if (errorDetails === '-') {
|
||||
errorMsg += '\nNote: Negative numbers are not supported';
|
||||
} else if (errorDetails === ' ') {
|
||||
errorMsg += `\nNote: Every roll must be closed by ${config.postfix}`;
|
||||
}
|
||||
break;
|
||||
case 'NoZerosAllowed':
|
||||
errorMsg = 'Formatting Error: ';
|
||||
switch (errorDetails) {
|
||||
case 'base':
|
||||
errorMsg += 'Die Size and Die Count';
|
||||
break;
|
||||
case 'drop':
|
||||
errorMsg += 'Drop (d or dl)';
|
||||
break;
|
||||
case 'keep':
|
||||
errorMsg += 'Keep (k or kh)';
|
||||
break;
|
||||
case 'dropHigh':
|
||||
errorMsg += 'Drop Highest (dh)';
|
||||
break;
|
||||
case 'keepLow':
|
||||
errorMsg += 'Keep Lowest (kl)';
|
||||
break;
|
||||
case 'reroll':
|
||||
errorMsg += 'Reroll (r)';
|
||||
break;
|
||||
case 'critScore':
|
||||
errorMsg += 'Crit Score (cs)';
|
||||
break;
|
||||
default:
|
||||
errorMsg += `Unhandled - ${errorDetails}`;
|
||||
break;
|
||||
}
|
||||
errorMsg += ' cannot be zero';
|
||||
break;
|
||||
case 'CritScoreMinGtrMax':
|
||||
errorMsg = 'Formatting Error: CritScore maximum cannot be greater than minimum, check formatting and flip min/max';
|
||||
break;
|
||||
case 'MaxLoopsExceeded':
|
||||
errorMsg = 'Error: Roll is too complex or reaches infinity';
|
||||
break;
|
||||
case 'UnbalancedParens':
|
||||
errorMsg = 'Formatting Error: At least one of the equations contains unbalanced parenthesis';
|
||||
break;
|
||||
case 'EMDASNotNumber':
|
||||
errorMsg = 'Error: One or more operands is not a number';
|
||||
break;
|
||||
case 'ConfWhat':
|
||||
errorMsg = 'Error: Not all values got processed, please report the command used';
|
||||
break;
|
||||
case 'OperatorWhat':
|
||||
errorMsg = 'Error: Something really broke with the Operator, try again';
|
||||
break;
|
||||
case 'OperandNaN':
|
||||
errorMsg = 'Error: One or more operands reached NaN, check input';
|
||||
break;
|
||||
case 'UndefinedStep':
|
||||
errorMsg = 'Error: Roll became undefined, one or more operands are not a roll or a number, check input';
|
||||
break;
|
||||
default:
|
||||
log(LT.ERROR, `Undangled Error: ${errorName}, ${errorDetails}`);
|
||||
errorMsg = `Unhandled Error: ${solverError.message}\nCheck input and try again, if issue persists, please use \`${config.prefix}report\` to alert the devs of the issue`;
|
||||
break;
|
||||
}
|
||||
|
||||
// Fill in the return block
|
||||
returnmsg.error = true;
|
||||
returnmsg.errorCode = solverError.message;
|
||||
returnmsg.errorMsg = errorMsg;
|
||||
}
|
||||
|
||||
return returnmsg;
|
||||
};
|
|
@ -0,0 +1,88 @@
|
|||
import {
|
||||
log,
|
||||
// Log4Deno deps
|
||||
LT,
|
||||
} from '../../deps.ts';
|
||||
|
||||
import { roll } from './roller.ts';
|
||||
import { rollCounter } from './counter.ts';
|
||||
import { RollFormat } from './solver.d.ts';
|
||||
import { loggingEnabled } from './rollUtils.ts';
|
||||
|
||||
// formatRoll(rollConf, maximiseRoll, nominalRoll) returns one SolvedStep
|
||||
// formatRoll handles creating and formatting the completed rolls into the SolvedStep format
|
||||
export const formatRoll = (rollConf: string, maximiseRoll: boolean, nominalRoll: boolean): RollFormat => {
|
||||
let tempTotal = 0;
|
||||
let tempDetails = '[';
|
||||
let tempCrit = false;
|
||||
let tempFail = false;
|
||||
|
||||
// Generate the roll, passing flags thru
|
||||
const tempRollSet = roll(rollConf, maximiseRoll, nominalRoll);
|
||||
|
||||
// Loop thru all parts of the roll to document everything that was done to create the total roll
|
||||
tempRollSet.forEach((e) => {
|
||||
loggingEnabled && log(LT.LOG, `Formatting roll ${rollConf} | ${JSON.stringify(e)}`);
|
||||
let preFormat = '';
|
||||
let postFormat = '';
|
||||
|
||||
if (!e.dropped && !e.rerolled) {
|
||||
// If the roll was not dropped or rerolled, add it to the stepTotal and flag the critHit/critFail
|
||||
switch (e.type) {
|
||||
case 'ova':
|
||||
case 'roll20':
|
||||
case 'fate':
|
||||
tempTotal += e.roll;
|
||||
break;
|
||||
case 'cwod':
|
||||
tempTotal += e.critHit ? 1 : 0;
|
||||
break;
|
||||
}
|
||||
if (e.critHit) {
|
||||
tempCrit = true;
|
||||
}
|
||||
if (e.critFail) {
|
||||
tempFail = true;
|
||||
}
|
||||
}
|
||||
// If the roll was a crit hit or fail, or dropped/rerolled, add the formatting needed
|
||||
if (e.critHit) {
|
||||
// Bold for crit success
|
||||
preFormat = `**${preFormat}`;
|
||||
postFormat = `${postFormat}**`;
|
||||
}
|
||||
if (e.critFail) {
|
||||
// Underline for crit fail
|
||||
preFormat = `__${preFormat}`;
|
||||
postFormat = `${postFormat}__`;
|
||||
}
|
||||
if (e.dropped || e.rerolled) {
|
||||
// Strikethrough for dropped/rerolled rolls
|
||||
preFormat = `~~${preFormat}`;
|
||||
postFormat = `${postFormat}~~`;
|
||||
}
|
||||
if (e.exploding) {
|
||||
// Add ! to indicate the roll came from an explosion
|
||||
postFormat = `!${postFormat}`;
|
||||
}
|
||||
|
||||
// Finally add this to the roll's details
|
||||
tempDetails += `${preFormat}${e.roll}${postFormat} + `;
|
||||
});
|
||||
// After the looping is done, remove the extra " + " from the details and cap it with the closing ]
|
||||
tempDetails = tempDetails.substring(0, tempDetails.length - 3);
|
||||
if (tempRollSet[0]?.type === 'cwod') {
|
||||
tempDetails += `, ${tempRollSet.filter((e) => e.critHit).length} Successes, ${tempRollSet.filter((e) => e.critFail).length} Fails`;
|
||||
}
|
||||
tempDetails += ']';
|
||||
|
||||
return {
|
||||
solvedStep: {
|
||||
total: tempTotal,
|
||||
details: tempDetails,
|
||||
containsCrit: tempCrit,
|
||||
containsFail: tempFail,
|
||||
},
|
||||
countDetails: rollCounter(tempRollSet),
|
||||
};
|
||||
};
|
|
@ -0,0 +1,210 @@
|
|||
import config from '../../config.ts';
|
||||
import { DEVMODE } from '../../flags.ts';
|
||||
import { dbClient, queries } from '../db.ts';
|
||||
import {
|
||||
// Discordeno deps
|
||||
DiscordenoMessage,
|
||||
// Log4Deno deps
|
||||
log,
|
||||
LT,
|
||||
// Discordeno deps
|
||||
sendDirectMessage,
|
||||
sendMessage,
|
||||
} from '../../deps.ts';
|
||||
import { SolvedRoll } from '../solver/solver.d.ts';
|
||||
import { QueuedRoll, RollModifiers } from '../mod.d.ts';
|
||||
import { generateCountDetailsEmbed, generateDMFailed, generateRollEmbed, infoColor2, rollingEmbed } from '../commandUtils.ts';
|
||||
import stdResp from '../endpoints/stdResponses.ts';
|
||||
import utils from '../utils.ts';
|
||||
|
||||
let currentWorkers = 0;
|
||||
const rollQueue: Array<QueuedRoll> = [];
|
||||
|
||||
// Handle setting up and calling the rollWorker
|
||||
const handleRollWorker = async (rq: QueuedRoll) => {
|
||||
currentWorkers++;
|
||||
|
||||
// gmModifiers used to create gmEmbed (basically just turn off the gmRoll)
|
||||
const gmModifiers = JSON.parse(JSON.stringify(rq.modifiers));
|
||||
gmModifiers.gmRoll = false;
|
||||
|
||||
const rollWorker = new Worker(new URL('../solver/rollWorker.ts', import.meta.url).href, { type: 'module' });
|
||||
|
||||
const workerTimeout = setTimeout(async () => {
|
||||
rollWorker.terminate();
|
||||
currentWorkers--;
|
||||
if (rq.apiRoll) {
|
||||
rq.api.requestEvent.respondWith(stdResp.RequestTimeout('Roll took too long to process, try breaking roll down into simpler parts'));
|
||||
} else {
|
||||
rq.dd.m.edit({
|
||||
embeds: [
|
||||
(await generateRollEmbed(
|
||||
rq.dd.message.authorId,
|
||||
<SolvedRoll> {
|
||||
error: true,
|
||||
errorCode: 'TooComplex',
|
||||
errorMsg: 'Error: Roll took too long to process, try breaking roll down into simpler parts',
|
||||
},
|
||||
<RollModifiers> {},
|
||||
)).embed,
|
||||
],
|
||||
}).catch((e) => utils.commonLoggers.messageEditError('rollQueue.ts:51', rq.dd.m, e));
|
||||
}
|
||||
}, config.limits.workerTimeout);
|
||||
|
||||
rollWorker.addEventListener('message', async (workerMessage) => {
|
||||
if (workerMessage.data === 'ready') {
|
||||
rollWorker.postMessage({
|
||||
rollCmd: rq.rollCmd,
|
||||
modifiers: rq.modifiers,
|
||||
});
|
||||
return;
|
||||
}
|
||||
let apiErroredOut = false;
|
||||
try {
|
||||
currentWorkers--;
|
||||
clearTimeout(workerTimeout);
|
||||
const returnmsg = workerMessage.data;
|
||||
const pubEmbedDetails = await generateRollEmbed(rq.apiRoll ? rq.api.userId : rq.dd.message.authorId, returnmsg, rq.modifiers);
|
||||
const gmEmbedDetails = await generateRollEmbed(rq.apiRoll ? rq.api.userId : rq.dd.message.authorId, returnmsg, gmModifiers);
|
||||
const countEmbed = generateCountDetailsEmbed(returnmsg.counts);
|
||||
|
||||
// If there was an error, report it to the user in hopes that they can determine what they did wrong
|
||||
if (returnmsg.error) {
|
||||
if (rq.apiRoll) {
|
||||
rq.api.requestEvent.respondWith(stdResp.InternalServerError(returnmsg.errorMsg));
|
||||
} else {
|
||||
rq.dd.m.edit({ embeds: [pubEmbedDetails.embed] });
|
||||
}
|
||||
|
||||
if (rq.apiRoll || DEVMODE && config.logRolls) {
|
||||
// If enabled, log rolls so we can see what went wrong
|
||||
dbClient.execute(queries.insertRollLogCmd(rq.apiRoll ? 1 : 0, 1), [rq.originalCommand, returnmsg.errorCode, rq.apiRoll ? null : rq.dd.m.id]).catch((e) =>
|
||||
utils.commonLoggers.dbError('rollQueue.ts:82', 'insert into', e)
|
||||
);
|
||||
}
|
||||
} else {
|
||||
let n: DiscordenoMessage | void;
|
||||
// Determine if we are to send a GM roll or a normal roll
|
||||
if (rq.modifiers.gmRoll) {
|
||||
if (rq.apiRoll) {
|
||||
n = await sendMessage(rq.api.channelId, {
|
||||
content: rq.modifiers.apiWarn,
|
||||
embeds: [pubEmbedDetails.embed],
|
||||
}).catch(() => {
|
||||
apiErroredOut = true;
|
||||
rq.api.requestEvent.respondWith(stdResp.InternalServerError('Message failed to send - location 0.'));
|
||||
});
|
||||
} else {
|
||||
// Send the public embed to correct channel
|
||||
rq.dd.m.edit({ embeds: [pubEmbedDetails.embed] });
|
||||
}
|
||||
|
||||
if (!apiErroredOut) {
|
||||
// And message the full details to each of the GMs, alerting roller of every GM that could not be messaged
|
||||
rq.modifiers.gms.forEach(async (gm) => {
|
||||
log(LT.LOG, `Messaging GM ${gm}`);
|
||||
// Attempt to DM the GM and send a warning if it could not DM a GM
|
||||
await sendDirectMessage(BigInt(gm.substring(2, gm.length - 1)), {
|
||||
embeds: rq.modifiers.count ? [gmEmbedDetails.embed, countEmbed] : [gmEmbedDetails.embed],
|
||||
}).then(async () => {
|
||||
// Check if we need to attach a file and send it after the initial details sent
|
||||
if (gmEmbedDetails.hasAttachment) {
|
||||
await sendDirectMessage(BigInt(gm.substring(2, gm.length - 1)), {
|
||||
file: gmEmbedDetails.attachment,
|
||||
}).catch(() => {
|
||||
if (n && rq.apiRoll) {
|
||||
n.reply(generateDMFailed(gm));
|
||||
} else {
|
||||
rq.dd.message.reply(generateDMFailed(gm));
|
||||
}
|
||||
});
|
||||
}
|
||||
}).catch(() => {
|
||||
if (rq.apiRoll && n) {
|
||||
n.reply(generateDMFailed(gm));
|
||||
} else {
|
||||
rq.dd.message.reply(generateDMFailed(gm));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// Not a gm roll, so just send normal embed to correct channel
|
||||
if (rq.apiRoll) {
|
||||
n = await sendMessage(rq.api.channelId, {
|
||||
content: rq.modifiers.apiWarn,
|
||||
embeds: rq.modifiers.count ? [pubEmbedDetails.embed, countEmbed] : [pubEmbedDetails.embed],
|
||||
}).catch(() => {
|
||||
apiErroredOut = true;
|
||||
rq.api.requestEvent.respondWith(stdResp.InternalServerError('Message failed to send - location 1.'));
|
||||
});
|
||||
} else {
|
||||
n = await rq.dd.m.edit({
|
||||
embeds: rq.modifiers.count ? [pubEmbedDetails.embed, countEmbed] : [pubEmbedDetails.embed],
|
||||
});
|
||||
}
|
||||
|
||||
if (pubEmbedDetails.hasAttachment && n) {
|
||||
// Attachment requires you to send a new message
|
||||
n.reply({
|
||||
file: pubEmbedDetails.attachment,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (rq.apiRoll && !apiErroredOut) {
|
||||
dbClient.execute(queries.insertRollLogCmd(1, 0), [rq.originalCommand, returnmsg.errorCode, n ? n.id : null]).catch((e) => utils.commonLoggers.dbError('rollQueue.ts:155', 'insert into', e));
|
||||
|
||||
rq.api.requestEvent.respondWith(stdResp.OK(JSON.stringify(
|
||||
rq.modifiers.count
|
||||
? {
|
||||
counts: countEmbed,
|
||||
details: pubEmbedDetails,
|
||||
}
|
||||
: {
|
||||
details: pubEmbedDetails,
|
||||
},
|
||||
)));
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
log(LT.ERROR, `Unddandled Error: ${JSON.stringify(e)}`);
|
||||
if (rq.apiRoll && !apiErroredOut) {
|
||||
rq.api.requestEvent.respondWith(stdResp.InternalServerError(JSON.stringify(e)));
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Runs the roll or queues it depending on how many workers are currently running
|
||||
export const queueRoll = async (rq: QueuedRoll) => {
|
||||
if (rq.apiRoll) {
|
||||
handleRollWorker(rq);
|
||||
} else if (!rollQueue.length && currentWorkers < config.limits.maxWorkers) {
|
||||
handleRollWorker(rq);
|
||||
} else {
|
||||
rq.dd.m.edit({
|
||||
embeds: [{
|
||||
color: infoColor2,
|
||||
title: `${config.name} currently has its hands full and has queued your roll.`,
|
||||
description: `There are currently ${currentWorkers + rollQueue.length} rolls ahead of this roll.
|
||||
|
||||
The results for this roll will replace this message when it is done.`,
|
||||
}],
|
||||
}).catch((e: Error) => utils.commonLoggers.messageEditError('rollQueue.ts:197', rq.dd.m, e));
|
||||
rollQueue.push(rq);
|
||||
}
|
||||
};
|
||||
|
||||
// Checks the queue constantly to make sure the queue stays empty
|
||||
setInterval(async () => {
|
||||
log(LT.LOG, `Checking rollQueue for items, rollQueue length: ${rollQueue.length}, currentWorkers: ${currentWorkers}, config.limits.maxWorkers: ${config.limits.maxWorkers}`);
|
||||
if (rollQueue.length && currentWorkers < config.limits.maxWorkers) {
|
||||
const temp = rollQueue.shift();
|
||||
if (temp) {
|
||||
temp.dd.m.edit(rollingEmbed).catch((e: Error) => utils.commonLoggers.messageEditError('rollQueue.ts:208', temp.dd.m, e));
|
||||
handleRollWorker(temp);
|
||||
}
|
||||
}
|
||||
}, 1000);
|
|
@ -0,0 +1,80 @@
|
|||
import {
|
||||
log,
|
||||
// Log4Deno deps
|
||||
LT,
|
||||
} from '../../deps.ts';
|
||||
|
||||
import { ReturnData, RollSet } from './solver.d.ts';
|
||||
|
||||
export const loggingEnabled = false;
|
||||
|
||||
// genRoll(size) returns number
|
||||
// genRoll rolls a die of size size and returns the result
|
||||
export const genRoll = (size: number, maximiseRoll: boolean, nominalRoll: boolean): number => {
|
||||
if (maximiseRoll) {
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
// genFateRoll returns -1|0|1
|
||||
// genFateRoll turns a d6 into a fate die, with sides: -1, -1, 0, 0, 1, 1
|
||||
export const genFateRoll = (maximiseRoll: boolean, nominalRoll: boolean): number => {
|
||||
if (nominalRoll) {
|
||||
return 0;
|
||||
} else {
|
||||
const sides = [-1, -1, 0, 0, 1, 1];
|
||||
return sides[genRoll(6, maximiseRoll, nominalRoll) - 1];
|
||||
}
|
||||
};
|
||||
|
||||
// compareRolls(a, b) returns -1|0|1
|
||||
// compareRolls is used to order an array of RollSets by RollSet.roll
|
||||
export const compareRolls = (a: RollSet, b: RollSet): number => {
|
||||
if (a.roll < b.roll) {
|
||||
return -1;
|
||||
}
|
||||
if (a.roll > b.roll) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
// compareTotalRolls(a, b) returns -1|0|1
|
||||
// compareTotalRolls is used to order an array of RollSets by RollSet.roll
|
||||
export const compareTotalRolls = (a: ReturnData, b: ReturnData): number => {
|
||||
if (a.rollTotal < b.rollTotal) {
|
||||
return -1;
|
||||
}
|
||||
if (a.rollTotal > b.rollTotal) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
// compareRolls(a, b) returns -1|0|1
|
||||
// compareRolls is used to order an array of RollSets by RollSet.origidx
|
||||
export const compareOrigidx = (a: RollSet, b: RollSet): number => {
|
||||
if (a.origidx < b.origidx) {
|
||||
return -1;
|
||||
}
|
||||
if (a.origidx > b.origidx) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
// escapeCharacters(str, esc) returns str
|
||||
// escapeCharacters escapes all characters listed in esc
|
||||
export const escapeCharacters = (str: string, esc: string): string => {
|
||||
// Loop thru each esc char one at a time
|
||||
for (const e of esc) {
|
||||
loggingEnabled && log(LT.LOG, `Escaping character ${e} | ${str}, ${esc}`);
|
||||
// Create a new regex to look for that char that needs replaced and escape it
|
||||
const temprgx = new RegExp(`[${e}]`, 'g');
|
||||
str = str.replace(temprgx, `\\${e}`);
|
||||
}
|
||||
return str;
|
||||
};
|
|
@ -0,0 +1,27 @@
|
|||
import { parseRoll } from './parser.ts';
|
||||
|
||||
// Alert rollQueue that this worker is ready
|
||||
self.postMessage('ready');
|
||||
|
||||
// Handle the roll
|
||||
self.onmessage = async (e: any) => {
|
||||
const payload = e.data;
|
||||
const returnmsg = parseRoll(payload.rollCmd, payload.modifiers) || {
|
||||
error: true,
|
||||
errorCode: 'EmptyMessage',
|
||||
errorMsg: 'Error: Empty message',
|
||||
line1: '',
|
||||
line2: '',
|
||||
line3: '',
|
||||
counts: {
|
||||
total: 0,
|
||||
successful: 0,
|
||||
failed: 0,
|
||||
rerolled: 0,
|
||||
dropped: 0,
|
||||
exploded: 0,
|
||||
},
|
||||
};
|
||||
self.postMessage(returnmsg);
|
||||
self.close();
|
||||
};
|
|
@ -0,0 +1,668 @@
|
|||
import config from '../../config.ts';
|
||||
import {
|
||||
// Log4Deno deps
|
||||
log,
|
||||
LT,
|
||||
} from '../../deps.ts';
|
||||
|
||||
import { RollConf, RollSet, RollType } from './solver.d.ts';
|
||||
import { compareOrigidx, compareRolls, genFateRoll, genRoll, loggingEnabled } from './rollUtils.ts';
|
||||
|
||||
// roll(rollStr, maximiseRoll, nominalRoll) returns RollSet
|
||||
// roll parses and executes the rollStr, if needed it will also make the roll the maximum or average
|
||||
export const roll = (rollStr: string, maximiseRoll: boolean, nominalRoll: boolean): RollSet[] => {
|
||||
/* Roll Capabilities
|
||||
* Deciphers and rolls a single dice roll set
|
||||
*
|
||||
* Check the README.md of this project for details on the roll options. I gave up trying to keep three places updated at once.
|
||||
*/
|
||||
|
||||
// Make entire roll lowercase for ease of parsing
|
||||
rollStr = rollStr.toLowerCase();
|
||||
|
||||
// Split the roll on the die size (and the drop if its there)
|
||||
const dpts = rollStr.split('d');
|
||||
|
||||
// Initialize the configuration to store the parsed data
|
||||
let rollType: RollType = '';
|
||||
const rollConf: RollConf = {
|
||||
dieCount: 0,
|
||||
dieSize: 0,
|
||||
drop: {
|
||||
on: false,
|
||||
count: 0,
|
||||
},
|
||||
keep: {
|
||||
on: false,
|
||||
count: 0,
|
||||
},
|
||||
dropHigh: {
|
||||
on: false,
|
||||
count: 0,
|
||||
},
|
||||
keepLow: {
|
||||
on: false,
|
||||
count: 0,
|
||||
},
|
||||
reroll: {
|
||||
on: false,
|
||||
once: false,
|
||||
nums: <number[]> [],
|
||||
},
|
||||
critScore: {
|
||||
on: false,
|
||||
range: <number[]> [],
|
||||
},
|
||||
critFail: {
|
||||
on: false,
|
||||
range: <number[]> [],
|
||||
},
|
||||
exploding: {
|
||||
on: false,
|
||||
once: false,
|
||||
compounding: false,
|
||||
penetrating: false,
|
||||
nums: <number[]> [],
|
||||
},
|
||||
};
|
||||
|
||||
// If the dpts is not long enough, throw error
|
||||
if (dpts.length < 2) {
|
||||
throw new Error('YouNeedAD');
|
||||
}
|
||||
|
||||
// Fill out the die count, first item will either be an int or empty string, short circuit execution will take care of replacing the empty string with a 1
|
||||
const rawDC = dpts.shift() || '1';
|
||||
const tempDC = rawDC.replace(/\D/g, '');
|
||||
// Rejoin all remaining parts
|
||||
let remains = dpts.join('d');
|
||||
|
||||
// Manual Parsing for custom roll types
|
||||
let manualParse = false;
|
||||
if (rawDC.endsWith('cwo')) {
|
||||
// CWOD dice parsing
|
||||
rollType = 'cwod';
|
||||
manualParse = true;
|
||||
|
||||
// Get CWOD parts, setting count and getting difficulty
|
||||
const cwodParts = rollStr.split('cwod');
|
||||
rollConf.dieCount = parseInt(cwodParts[0] || '1');
|
||||
rollConf.dieSize = 10;
|
||||
|
||||
// Use critScore to set the difficulty
|
||||
rollConf.critScore.on = true;
|
||||
const difficulty = parseInt(cwodParts[1] || '10');
|
||||
for (let i = difficulty; i <= rollConf.dieSize; i++) {
|
||||
loggingEnabled && log(LT.LOG, `handling cwod ${rollStr} | Parsing difficulty ${i}`);
|
||||
rollConf.critScore.range.push(i);
|
||||
}
|
||||
} else if (rawDC.endsWith('ova')) {
|
||||
// OVA dice parsing
|
||||
rollType = 'ova';
|
||||
manualParse = true;
|
||||
|
||||
// Get CWOD parts, setting count and getting difficulty
|
||||
const ovaParts = rollStr.split('ovad');
|
||||
rollConf.dieCount = parseInt(ovaParts[0] || '1');
|
||||
rollConf.dieSize = parseInt(ovaParts[1] || '6');
|
||||
} else if (remains.startsWith('f')) {
|
||||
// fate dice setup
|
||||
rollType = 'fate';
|
||||
rollConf.dieCount = parseInt(tempDC);
|
||||
// dieSize set to 1 as 1 is max face value, a six sided die is used internally
|
||||
rollConf.dieSize = 1;
|
||||
|
||||
// remove F from the remains
|
||||
remains = remains.slice(1);
|
||||
} else {
|
||||
// roll20 dice setup
|
||||
rollType = 'roll20';
|
||||
rollConf.dieCount = parseInt(tempDC);
|
||||
|
||||
// Finds the end of the die size/beginnning of the additional options
|
||||
let afterDieIdx = dpts[0].search(/\D/);
|
||||
if (afterDieIdx === -1) {
|
||||
afterDieIdx = dpts[0].length;
|
||||
}
|
||||
|
||||
// Get the die size out of the remains and into the rollConf
|
||||
rollConf.dieSize = parseInt(remains.slice(0, afterDieIdx));
|
||||
remains = remains.slice(afterDieIdx);
|
||||
}
|
||||
|
||||
if (!rollConf.dieCount || !rollConf.dieSize) {
|
||||
throw new Error('YouNeedAD');
|
||||
}
|
||||
|
||||
loggingEnabled && log(LT.LOG, `Handling ${rollType} ${rollStr} | Parsed Die Count: ${rollConf.dieCount}`);
|
||||
loggingEnabled && log(LT.LOG, `Handling ${rollType} ${rollStr} | Parsed Die Size: ${rollConf.dieSize}`);
|
||||
|
||||
// Finish parsing the roll
|
||||
if (!manualParse && remains.length > 0) {
|
||||
// Determine if the first item is a drop, and if it is, add the d back in
|
||||
if (remains.search(/\D/) !== 0 || remains.indexOf('l') === 0 || remains.indexOf('h') === 0) {
|
||||
remains = `d${remains}`;
|
||||
}
|
||||
|
||||
// Loop until all remaining args are parsed
|
||||
while (remains.length > 0) {
|
||||
loggingEnabled && log(LT.LOG, `handling ${rollType} ${rollStr} | Parsing remains ${remains}`);
|
||||
// Find the next number in the remains to be able to cut out the rule name
|
||||
let afterSepIdx = remains.search(/\d/);
|
||||
if (afterSepIdx < 0) {
|
||||
afterSepIdx = remains.length;
|
||||
}
|
||||
// Save the rule name to tSep and remove it from remains
|
||||
const tSep = remains.slice(0, afterSepIdx);
|
||||
remains = remains.slice(afterSepIdx);
|
||||
// Find the next non-number in the remains to be able to cut out the count/num
|
||||
let afterNumIdx = remains.search(/\D/);
|
||||
if (afterNumIdx < 0) {
|
||||
afterNumIdx = remains.length;
|
||||
}
|
||||
// Save the count/num to tNum leaving it in remains for the time being
|
||||
const tNum = parseInt(remains.slice(0, afterNumIdx));
|
||||
|
||||
// Switch on rule name
|
||||
switch (tSep) {
|
||||
case 'dl':
|
||||
case 'd':
|
||||
// Configure Drop (Lowest)
|
||||
rollConf.drop.on = true;
|
||||
rollConf.drop.count = tNum;
|
||||
break;
|
||||
case 'kh':
|
||||
case 'k':
|
||||
// Configure Keep (Highest)
|
||||
rollConf.keep.on = true;
|
||||
rollConf.keep.count = tNum;
|
||||
break;
|
||||
case 'dh':
|
||||
// Configure Drop (Highest)
|
||||
rollConf.dropHigh.on = true;
|
||||
rollConf.dropHigh.count = tNum;
|
||||
break;
|
||||
case 'kl':
|
||||
// Configure Keep (Lowest)
|
||||
rollConf.keepLow.on = true;
|
||||
rollConf.keepLow.count = tNum;
|
||||
break;
|
||||
case 'ro':
|
||||
case 'ro=':
|
||||
rollConf.reroll.once = true;
|
||||
// falls through as ro/ro= functions the same as r/r= in this context
|
||||
case 'r':
|
||||
case 'r=':
|
||||
// Configure Reroll (this can happen multiple times)
|
||||
rollConf.reroll.on = true;
|
||||
rollConf.reroll.nums.push(tNum);
|
||||
break;
|
||||
case 'ro>':
|
||||
rollConf.reroll.once = true;
|
||||
// falls through as ro> functions the same as r> in this context
|
||||
case 'r>':
|
||||
// Configure reroll for all numbers greater than or equal to tNum (this could happen multiple times, but why)
|
||||
rollConf.reroll.on = true;
|
||||
for (let i = tNum; i <= rollConf.dieSize; i++) {
|
||||
loggingEnabled && log(LT.LOG, `handling ${rollType} ${rollStr} | Parsing r> ${i}`);
|
||||
rollConf.reroll.nums.push(i);
|
||||
}
|
||||
break;
|
||||
case 'ro<':
|
||||
rollConf.reroll.once = true;
|
||||
// falls through as ro< functions the same as r< in this context
|
||||
case 'r<':
|
||||
// Configure reroll for all numbers less than or equal to tNum (this could happen multiple times, but why)
|
||||
rollConf.reroll.on = true;
|
||||
for (let i = 1; i <= tNum; i++) {
|
||||
loggingEnabled && log(LT.LOG, `handling ${rollType} ${rollStr} | Parsing r< ${i}`);
|
||||
rollConf.reroll.nums.push(i);
|
||||
}
|
||||
break;
|
||||
case 'cs':
|
||||
case 'cs=':
|
||||
// Configure CritScore for one number (this can happen multiple times)
|
||||
rollConf.critScore.on = true;
|
||||
rollConf.critScore.range.push(tNum);
|
||||
break;
|
||||
case 'cs>':
|
||||
// Configure CritScore for all numbers greater than or equal to tNum (this could happen multiple times, but why)
|
||||
rollConf.critScore.on = true;
|
||||
for (let i = tNum; i <= rollConf.dieSize; i++) {
|
||||
loggingEnabled && log(LT.LOG, `handling ${rollType} ${rollStr} | Parsing cs> ${i}`);
|
||||
rollConf.critScore.range.push(i);
|
||||
}
|
||||
break;
|
||||
case 'cs<':
|
||||
// Configure CritScore for all numbers less than or equal to tNum (this could happen multiple times, but why)
|
||||
rollConf.critScore.on = true;
|
||||
for (let i = 0; i <= tNum; i++) {
|
||||
loggingEnabled && log(LT.LOG, `handling ${rollType} ${rollStr} | Parsing cs< ${i}`);
|
||||
rollConf.critScore.range.push(i);
|
||||
}
|
||||
break;
|
||||
case 'cf':
|
||||
case 'cf=':
|
||||
// Configure CritFail for one number (this can happen multiple times)
|
||||
rollConf.critFail.on = true;
|
||||
rollConf.critFail.range.push(tNum);
|
||||
break;
|
||||
case 'cf>':
|
||||
// Configure CritFail for all numbers greater than or equal to tNum (this could happen multiple times, but why)
|
||||
rollConf.critFail.on = true;
|
||||
for (let i = tNum; i <= rollConf.dieSize; i++) {
|
||||
loggingEnabled && log(LT.LOG, `handling ${rollType} ${rollStr} | Parsing cf> ${i}`);
|
||||
rollConf.critFail.range.push(i);
|
||||
}
|
||||
break;
|
||||
case 'cf<':
|
||||
// Configure CritFail for all numbers less than or equal to tNum (this could happen multiple times, but why)
|
||||
rollConf.critFail.on = true;
|
||||
for (let i = 0; i <= tNum; i++) {
|
||||
loggingEnabled && log(LT.LOG, `handling ${rollType} ${rollStr} | Parsing cf< ${i}`);
|
||||
rollConf.critFail.range.push(i);
|
||||
}
|
||||
break;
|
||||
case '!':
|
||||
case '!o':
|
||||
case '!p':
|
||||
case '!!':
|
||||
// Configure Exploding
|
||||
rollConf.exploding.on = true;
|
||||
if (afterNumIdx > 0) {
|
||||
// User gave a number to explode on, save it
|
||||
rollConf.exploding.nums.push(tNum);
|
||||
} else {
|
||||
// User did not give number, use cs
|
||||
afterNumIdx = 1;
|
||||
}
|
||||
break;
|
||||
case '!=':
|
||||
case '!o=':
|
||||
case '!p=':
|
||||
case '!!=':
|
||||
// Configure Exploding (this can happen multiple times)
|
||||
rollConf.exploding.on = true;
|
||||
rollConf.exploding.nums.push(tNum);
|
||||
break;
|
||||
case '!>':
|
||||
case '!o>':
|
||||
case '!p>':
|
||||
case '!!>':
|
||||
// Configure Exploding for all numbers greater than or equal to tNum (this could happen multiple times, but why)
|
||||
rollConf.exploding.on = true;
|
||||
for (let i = tNum; i <= rollConf.dieSize; i++) {
|
||||
loggingEnabled && log(LT.LOG, `handling ${rollType} ${rollStr} | Parsing !> ${i}`);
|
||||
rollConf.exploding.nums.push(i);
|
||||
}
|
||||
break;
|
||||
case '!<':
|
||||
case '!o<':
|
||||
case '!p<':
|
||||
case '!!<':
|
||||
// Configure Exploding for all numbers less than or equal to tNum (this could happen multiple times, but why)
|
||||
rollConf.exploding.on = true;
|
||||
for (let i = 1; i <= tNum; i++) {
|
||||
loggingEnabled && log(LT.LOG, `handling ${rollType} ${rollStr} | Parsing !< ${i}`);
|
||||
rollConf.exploding.nums.push(i);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
// Throw error immediately if unknown op is encountered
|
||||
throw new Error(`UnknownOperation_${tSep}`);
|
||||
}
|
||||
|
||||
// Exploding flags get set in their own switch statement to avoid weird duplicated code
|
||||
switch (tSep) {
|
||||
case '!o':
|
||||
case '!o=':
|
||||
case '!o>':
|
||||
case '!o<':
|
||||
rollConf.exploding.once = true;
|
||||
break;
|
||||
case '!p':
|
||||
case '!p=':
|
||||
case '!p>':
|
||||
case '!p<':
|
||||
rollConf.exploding.penetrating = true;
|
||||
break;
|
||||
case '!!':
|
||||
case '!!=':
|
||||
case '!!>':
|
||||
case '!!<':
|
||||
rollConf.exploding.compounding = true;
|
||||
break;
|
||||
}
|
||||
|
||||
// Finally slice off everything else parsed this loop
|
||||
remains = remains.slice(afterNumIdx);
|
||||
}
|
||||
}
|
||||
|
||||
// Verify the parse, throwing errors for every invalid config
|
||||
if (rollConf.dieCount < 0) {
|
||||
throw new Error('NoZerosAllowed_base');
|
||||
}
|
||||
if (rollConf.dieCount === 0 || rollConf.dieSize === 0) {
|
||||
throw new Error('NoZerosAllowed_base');
|
||||
}
|
||||
// Since only one drop or keep option can be active, count how many are active to throw the right error
|
||||
let dkdkCnt = 0;
|
||||
[rollConf.drop.on, rollConf.keep.on, rollConf.dropHigh.on, rollConf.keepLow.on].forEach((e) => {
|
||||
loggingEnabled && log(LT.LOG, `handling ${rollType} ${rollStr} | Checking if drop/keep is on ${e}`);
|
||||
if (e) {
|
||||
dkdkCnt++;
|
||||
}
|
||||
});
|
||||
if (dkdkCnt > 1) {
|
||||
throw new Error('FormattingError_dk');
|
||||
}
|
||||
if (rollConf.drop.on && rollConf.drop.count === 0) {
|
||||
throw new Error('NoZerosAllowed_drop');
|
||||
}
|
||||
if (rollConf.keep.on && rollConf.keep.count === 0) {
|
||||
throw new Error('NoZerosAllowed_keep');
|
||||
}
|
||||
if (rollConf.dropHigh.on && rollConf.dropHigh.count === 0) {
|
||||
throw new Error('NoZerosAllowed_dropHigh');
|
||||
}
|
||||
if (rollConf.keepLow.on && rollConf.keepLow.count === 0) {
|
||||
throw new Error('NoZerosAllowed_keepLow');
|
||||
}
|
||||
if (rollConf.reroll.on && rollConf.reroll.nums.indexOf(0) >= 0) {
|
||||
throw new Error('NoZerosAllowed_reroll');
|
||||
}
|
||||
|
||||
// Roll the roll
|
||||
const rollSet = [];
|
||||
/* Roll will contain objects of the following format:
|
||||
* {
|
||||
* origidx: 0,
|
||||
* roll: 0,
|
||||
* dropped: false,
|
||||
* rerolled: false,
|
||||
* exploding: false,
|
||||
* critHit: false,
|
||||
* critFail: false
|
||||
* }
|
||||
*
|
||||
* Each of these is defined as following:
|
||||
* {
|
||||
* origidx: The original index of the roll
|
||||
* roll: The resulting roll on this die in the set
|
||||
* dropped: This die is to be dropped as it was one of the dy lowest dice
|
||||
* rerolled: This die has been rerolled as it matched rz, it is replaced by the very next die in the set
|
||||
* exploding: This die was rolled as the previous die exploded (was a crit hit)
|
||||
* critHit: This die matched csq[-u], max die value used if cs not used
|
||||
* critFail: This die rolled a nat 1, a critical failure
|
||||
* }
|
||||
*/
|
||||
|
||||
// Initialize a templet rollSet to copy multiple times
|
||||
const templateRoll: RollSet = {
|
||||
type: rollType,
|
||||
origidx: 0,
|
||||
roll: 0,
|
||||
dropped: false,
|
||||
rerolled: false,
|
||||
exploding: false,
|
||||
critHit: false,
|
||||
critFail: false,
|
||||
};
|
||||
|
||||
// Begin counting the number of loops to prevent from getting into an infinite loop
|
||||
let loopCount = 0;
|
||||
|
||||
// Initial rolling, not handling reroll or exploding here
|
||||
for (let i = 0; i < rollConf.dieCount; i++) {
|
||||
loggingEnabled && log(LT.LOG, `handling ${rollType} ${rollStr} | Initial rolling ${i} of ${JSON.stringify(rollConf)}`);
|
||||
// If loopCount gets too high, stop trying to calculate infinity
|
||||
if (loopCount > config.limits.maxLoops) {
|
||||
throw new Error('MaxLoopsExceeded');
|
||||
}
|
||||
|
||||
// Copy the template to fill out for this iteration
|
||||
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
|
||||
rolling.roll = rollType === 'fate' ? genFateRoll(maximiseRoll, nominalRoll) : genRoll(rollConf.dieSize, maximiseRoll, nominalRoll);
|
||||
// Set origidx of roll
|
||||
rolling.origidx = i;
|
||||
|
||||
// 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(rolling.roll) >= 0) {
|
||||
rolling.critHit = true;
|
||||
} else if (!rollConf.critScore.on) {
|
||||
rolling.critHit = rolling.roll === rollConf.dieSize;
|
||||
}
|
||||
// If critFail arg is on, check if the roll should be a fail, if its off, check if the roll matches 1
|
||||
if (rollConf.critFail.on && rollConf.critFail.range.indexOf(rolling.roll) >= 0) {
|
||||
rolling.critFail = true;
|
||||
} else if (!rollConf.critFail.on) {
|
||||
if (rollType === 'fate') {
|
||||
rolling.critFail = rolling.roll === -1;
|
||||
} else {
|
||||
rolling.critFail = rolling.roll === 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Push the newly created roll and loop again
|
||||
rollSet.push(rolling);
|
||||
loopCount++;
|
||||
}
|
||||
|
||||
// If needed, handle rerolling and exploding dice now
|
||||
if (rollConf.reroll.on || rollConf.exploding.on) {
|
||||
for (let i = 0; i < rollSet.length; i++) {
|
||||
loggingEnabled && log(LT.LOG, `handling ${rollType} ${rollStr} | Handling rerolling and exploding ${JSON.stringify(rollSet[i])}`);
|
||||
// If loopCount gets too high, stop trying to calculate infinity
|
||||
if (loopCount > config.limits.maxLoops) {
|
||||
throw new Error('MaxLoopsExceeded');
|
||||
}
|
||||
|
||||
// If we need to reroll this roll, flag its been replaced and...
|
||||
// This big boolean statement first checks if reroll is on, if the roll is within the reroll range, and finally if ro is ON, make sure we haven't already rerolled the roll
|
||||
if (rollConf.reroll.on && rollConf.reroll.nums.indexOf(rollSet[i].roll) >= 0 && (!rollConf.reroll.once || !rollSet[i ? (i - 1) : i].rerolled)) {
|
||||
rollSet[i].rerolled = true;
|
||||
|
||||
// Copy the template to fill out for this iteration
|
||||
const newReroll = 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
|
||||
newReroll.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 (rollConf.critScore.on && rollConf.critScore.range.indexOf(newReroll.roll) >= 0) {
|
||||
newReroll.critHit = true;
|
||||
} else if (!rollConf.critScore.on) {
|
||||
newReroll.critHit = newReroll.roll === rollConf.dieSize;
|
||||
}
|
||||
// If critFail arg is on, check if the roll should be a fail, if its off, check if the roll matches 1
|
||||
if (rollConf.critFail.on && rollConf.critFail.range.indexOf(newReroll.roll) >= 0) {
|
||||
newReroll.critFail = true;
|
||||
} else if (!rollConf.critFail.on) {
|
||||
newReroll.critFail = newReroll.roll === 1;
|
||||
}
|
||||
|
||||
// Slot this new roll in after the current iteration so it can be processed in the next loop
|
||||
rollSet.splice(i + 1, 0, newReroll);
|
||||
} else if (
|
||||
rollConf.exploding.on && !rollSet[i].rerolled && (rollConf.exploding.nums.length ? rollConf.exploding.nums.indexOf(rollSet[i].roll) >= 0 : rollSet[i].critHit) &&
|
||||
(!rollConf.exploding.once || !rollSet[i].exploding)
|
||||
) {
|
||||
// If we have exploding.nums set, use those to determine the exploding range, and make sure if !o is on, make sure we don't repeatedly explode
|
||||
// If it exploded, we keep both, so no flags need to be set
|
||||
|
||||
// Copy the template to fill out for this iteration
|
||||
const newExplodingRoll = 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
|
||||
newExplodingRoll.roll = genRoll(rollConf.dieSize, maximiseRoll, nominalRoll);
|
||||
// Always mark this roll as exploding
|
||||
newExplodingRoll.exploding = true;
|
||||
|
||||
// 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(newExplodingRoll.roll) >= 0) {
|
||||
newExplodingRoll.critHit = true;
|
||||
} else if (!rollConf.critScore.on) {
|
||||
newExplodingRoll.critHit = newExplodingRoll.roll === rollConf.dieSize;
|
||||
}
|
||||
// If critFail arg is on, check if the roll should be a fail, if its off, check if the roll matches 1
|
||||
if (rollConf.critFail.on && rollConf.critFail.range.indexOf(newExplodingRoll.roll) >= 0) {
|
||||
newExplodingRoll.critFail = true;
|
||||
} else if (!rollConf.critFail.on) {
|
||||
newExplodingRoll.critFail = newExplodingRoll.roll === 1;
|
||||
}
|
||||
|
||||
// Slot this new roll in after the current iteration so it can be processed in the next loop
|
||||
rollSet.splice(i + 1, 0, newExplodingRoll);
|
||||
}
|
||||
|
||||
loopCount++;
|
||||
}
|
||||
}
|
||||
|
||||
// If penetrating is on, do the decrements
|
||||
if (rollConf.exploding.penetrating) {
|
||||
for (const penRoll of rollSet) {
|
||||
loggingEnabled && log(LT.LOG, `handling ${rollType} ${rollStr} | Handling penetrating explosions ${JSON.stringify(penRoll)}`);
|
||||
// If loopCount gets too high, stop trying to calculate infinity
|
||||
if (loopCount > config.limits.maxLoops) {
|
||||
throw new Error('MaxLoopsExceeded');
|
||||
}
|
||||
|
||||
// If the die was from an explosion, decrement it by one
|
||||
if (penRoll.exploding) {
|
||||
penRoll.roll--;
|
||||
}
|
||||
|
||||
loopCount++;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle compounding explosions
|
||||
if (rollConf.exploding.compounding) {
|
||||
for (let i = 0; i < rollSet.length; i++) {
|
||||
loggingEnabled && log(LT.LOG, `handling ${rollType} ${rollStr} | Handling compounding explosions ${JSON.stringify(rollSet[i])}`);
|
||||
// If loopCount gets too high, stop trying to calculate infinity
|
||||
if (loopCount > config.limits.maxLoops) {
|
||||
throw new Error('MaxLoopsExceeded');
|
||||
}
|
||||
|
||||
// Compound the exploding rolls, including the exploding flag and
|
||||
if (rollSet[i].exploding) {
|
||||
rollSet[i - 1].roll = rollSet[i - 1].roll + rollSet[i].roll;
|
||||
rollSet[i - 1].exploding = true;
|
||||
rollSet[i - 1].critFail = rollSet[i - 1].critFail || rollSet[i].critFail;
|
||||
rollSet[i - 1].critHit = rollSet[i - 1].critHit || rollSet[i].critHit;
|
||||
rollSet.splice(i, 1);
|
||||
i--;
|
||||
}
|
||||
|
||||
loopCount++;
|
||||
}
|
||||
}
|
||||
|
||||
// If we need to handle the drop/keep flags
|
||||
if (dkdkCnt > 0) {
|
||||
// Count how many rerolled dice there are if the reroll flag was on
|
||||
let rerollCount = 0;
|
||||
if (rollConf.reroll.on) {
|
||||
for (let j = 0; j < rollSet.length; j++) {
|
||||
// If loopCount gets too high, stop trying to calculate infinity
|
||||
if (loopCount > config.limits.maxLoops) {
|
||||
throw new Error('MaxLoopsExceeded');
|
||||
}
|
||||
|
||||
loggingEnabled && log(LT.LOG, `handling ${rollType} ${rollStr} | Setting originalIdx on ${JSON.stringify(rollSet[j])}`);
|
||||
rollSet[j].origidx = j;
|
||||
|
||||
if (rollSet[j].rerolled) {
|
||||
rerollCount++;
|
||||
}
|
||||
|
||||
loopCount++;
|
||||
}
|
||||
}
|
||||
|
||||
// Order the rolls from least to greatest (by RollSet.roll)
|
||||
rollSet.sort(compareRolls);
|
||||
|
||||
// Determine how many valid rolls there are to drop from (may not be equal to dieCount due to exploding)
|
||||
const validRolls = rollSet.length - rerollCount;
|
||||
let dropCount = 0;
|
||||
|
||||
// For normal drop and keep, simple subtraction is enough to determine how many to drop
|
||||
// Protections are in to prevent the dropCount from going below 0 or more than the valid rolls to drop
|
||||
if (rollConf.drop.on) {
|
||||
dropCount = rollConf.drop.count;
|
||||
if (dropCount > validRolls) {
|
||||
dropCount = validRolls;
|
||||
}
|
||||
} else if (rollConf.keep.on) {
|
||||
dropCount = validRolls - rollConf.keep.count;
|
||||
if (dropCount < 0) {
|
||||
dropCount = 0;
|
||||
}
|
||||
} // For inverted drop and keep, order must be flipped to greatest to least before the simple subtraction can determine how many to drop
|
||||
// Protections are in to prevent the dropCount from going below 0 or more than the valid rolls to drop
|
||||
else if (rollConf.dropHigh.on) {
|
||||
rollSet.reverse();
|
||||
dropCount = rollConf.dropHigh.count;
|
||||
if (dropCount > validRolls) {
|
||||
dropCount = validRolls;
|
||||
}
|
||||
} else if (rollConf.keepLow.on) {
|
||||
rollSet.reverse();
|
||||
dropCount = validRolls - rollConf.keepLow.count;
|
||||
if (dropCount < 0) {
|
||||
dropCount = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Now its time to drop all dice needed
|
||||
let i = 0;
|
||||
while (dropCount > 0 && i < rollSet.length) {
|
||||
// If loopCount gets too high, stop trying to calculate infinity
|
||||
if (loopCount > config.limits.maxLoops) {
|
||||
throw new Error('MaxLoopsExceeded');
|
||||
}
|
||||
|
||||
loggingEnabled && log(LT.LOG, `handling ${rollType} ${rollStr} | Dropping dice ${dropCount} ${JSON.stringify(rollSet[i])}`);
|
||||
// Skip all rolls that were rerolled
|
||||
if (!rollSet[i].rerolled) {
|
||||
rollSet[i].dropped = true;
|
||||
dropCount--;
|
||||
}
|
||||
i++;
|
||||
loopCount++;
|
||||
}
|
||||
|
||||
// Finally, return the rollSet to its original order
|
||||
rollSet.sort(compareOrigidx);
|
||||
}
|
||||
|
||||
// Handle OVA dropping/keeping
|
||||
if (rollType === 'ova') {
|
||||
// Make "empty" vals array to easily sum up which die value is the greatest
|
||||
const rollVals: Array<number> = new Array(rollConf.dieSize).fill(0);
|
||||
|
||||
// Sum up all rolls
|
||||
for (const ovaRoll of rollSet) {
|
||||
loggingEnabled && log(LT.LOG, `handling ${rollType} ${rollStr} | incrementing rollVals for ${ovaRoll}`);
|
||||
rollVals[ovaRoll.roll - 1] += ovaRoll.roll;
|
||||
}
|
||||
|
||||
// Find max value, using lastIndexOf to use the greatest die size max in case of duplicate maximums
|
||||
const maxRoll = rollVals.lastIndexOf(Math.max(...rollVals)) + 1;
|
||||
|
||||
// Drop all dice that are not a part of the max
|
||||
for (const ovaRoll of rollSet) {
|
||||
loggingEnabled && log(LT.LOG, `handling ${rollType} ${rollStr} | checking if this roll should be dropped ${ovaRoll.roll} | to keep: ${maxRoll}`);
|
||||
if (ovaRoll.roll !== maxRoll) {
|
||||
ovaRoll.dropped = true;
|
||||
ovaRoll.critFail = false;
|
||||
ovaRoll.critHit = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return rollSet;
|
||||
};
|
|
@ -0,0 +1,102 @@
|
|||
// solver.ts custom types
|
||||
|
||||
export type RollType = '' | 'roll20' | 'fate' | 'cwod' | 'ova';
|
||||
|
||||
// RollSet is used to preserve all information about a calculated roll
|
||||
export type RollSet = {
|
||||
type: RollType;
|
||||
origidx: number;
|
||||
roll: number;
|
||||
dropped: boolean;
|
||||
rerolled: boolean;
|
||||
exploding: boolean;
|
||||
critHit: boolean;
|
||||
critFail: boolean;
|
||||
};
|
||||
|
||||
// SolvedStep is used to preserve information while math is being performed on the roll
|
||||
export type SolvedStep = {
|
||||
total: number;
|
||||
details: string;
|
||||
containsCrit: boolean;
|
||||
containsFail: boolean;
|
||||
};
|
||||
|
||||
// ReturnData is the temporary internal type used before getting turned into SolvedRoll
|
||||
export type ReturnData = {
|
||||
rollTotal: number;
|
||||
rollPostFormat: string;
|
||||
rollDetails: string;
|
||||
containsCrit: boolean;
|
||||
containsFail: boolean;
|
||||
initConfig: string;
|
||||
};
|
||||
|
||||
// CountDetails is the object holding the count data for creating the Count Embed
|
||||
export type CountDetails = {
|
||||
total: number;
|
||||
successful: number;
|
||||
failed: number;
|
||||
rerolled: number;
|
||||
dropped: number;
|
||||
exploded: number;
|
||||
};
|
||||
|
||||
// RollFormat is the return structure for the rollFormatter
|
||||
export type RollFormat = {
|
||||
solvedStep: SolvedStep;
|
||||
countDetails: CountDetails;
|
||||
};
|
||||
|
||||
// SolvedRoll is the complete solved and formatted roll, or the error said roll created
|
||||
export type SolvedRoll = {
|
||||
error: boolean;
|
||||
errorMsg: string;
|
||||
errorCode: string;
|
||||
line1: string;
|
||||
line2: string;
|
||||
line3: string;
|
||||
counts: CountDetails;
|
||||
};
|
||||
|
||||
// RollConf is used by the roll20 setup
|
||||
export type RollConf = {
|
||||
dieCount: number;
|
||||
dieSize: number;
|
||||
drop: {
|
||||
on: boolean;
|
||||
count: number;
|
||||
};
|
||||
keep: {
|
||||
on: boolean;
|
||||
count: number;
|
||||
};
|
||||
dropHigh: {
|
||||
on: boolean;
|
||||
count: number;
|
||||
};
|
||||
keepLow: {
|
||||
on: boolean;
|
||||
count: number;
|
||||
};
|
||||
reroll: {
|
||||
on: boolean;
|
||||
once: boolean;
|
||||
nums: number[];
|
||||
};
|
||||
critScore: {
|
||||
on: boolean;
|
||||
range: number[];
|
||||
};
|
||||
critFail: {
|
||||
on: boolean;
|
||||
range: number[];
|
||||
};
|
||||
exploding: {
|
||||
on: boolean;
|
||||
once: boolean;
|
||||
compounding: boolean;
|
||||
penetrating: boolean;
|
||||
nums: number[];
|
||||
};
|
||||
};
|
|
@ -0,0 +1,203 @@
|
|||
/* The Artificer was built in memory of Babka
|
||||
* With love, Ean
|
||||
*
|
||||
* December 21, 2020
|
||||
*/
|
||||
|
||||
import {
|
||||
log,
|
||||
// Log4Deno deps
|
||||
LT,
|
||||
} from '../../deps.ts';
|
||||
|
||||
import { SolvedStep } from './solver.d.ts';
|
||||
import { loggingEnabled } from './rollUtils.ts';
|
||||
|
||||
// fullSolver(conf, wrapDetails) returns one condensed SolvedStep
|
||||
// fullSolver is a function that recursively solves the full roll and math
|
||||
export const fullSolver = (conf: (string | number | SolvedStep)[], wrapDetails: boolean): SolvedStep => {
|
||||
// Initialize PEMDAS
|
||||
const signs = ['^', '*', '/', '%', '+', '-'];
|
||||
const stepSolve = {
|
||||
total: 0,
|
||||
details: '',
|
||||
containsCrit: false,
|
||||
containsFail: false,
|
||||
};
|
||||
|
||||
// If entering with a single number, note it now
|
||||
let singleNum = false;
|
||||
if (conf.length === 1) {
|
||||
singleNum = true;
|
||||
}
|
||||
|
||||
// Evaluate all parenthesis
|
||||
while (conf.indexOf('(') > -1) {
|
||||
loggingEnabled && log(LT.LOG, `Evaluating roll ${JSON.stringify(conf)} | Looking for (`);
|
||||
// Get first open parenthesis
|
||||
const openParen = conf.indexOf('(');
|
||||
let closeParen = -1;
|
||||
let nextParen = 0;
|
||||
|
||||
// Using nextParen to count the opening/closing parens, find the matching paren to openParen above
|
||||
for (let i = openParen; i < conf.length; i++) {
|
||||
loggingEnabled && log(LT.LOG, `Evaluating roll ${JSON.stringify(conf)} | Looking for matching ) openIdx: ${openParen} checking: ${i}`);
|
||||
// If we hit an open, add one (this includes the openParen we start with), if we hit a close, subtract one
|
||||
if (conf[i] === '(') {
|
||||
nextParen++;
|
||||
} else if (conf[i] === ')') {
|
||||
nextParen--;
|
||||
}
|
||||
|
||||
// When nextParen reaches 0 again, we will have found the matching closing parenthesis and can safely exit the for loop
|
||||
if (nextParen === 0) {
|
||||
closeParen = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure we did find the correct closing paren, if not, error out now
|
||||
if (closeParen === -1 || closeParen < openParen) {
|
||||
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);
|
||||
|
||||
// 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
|
||||
let insertedMult = false;
|
||||
// Check if a number was directly before openParen and slip in the "*" if needed
|
||||
if (((openParen - 1) > -1) && (signs.indexOf(conf[openParen - 1].toString()) === -1)) {
|
||||
insertedMult = true;
|
||||
conf.splice(openParen, 0, '*');
|
||||
}
|
||||
// Check if a number is directly after closeParen and slip in the "*" if needed
|
||||
if (!insertedMult && (((openParen + 1) < conf.length) && (signs.indexOf(conf[openParen + 1].toString()) === -1))) {
|
||||
conf.splice(openParen + 1, 0, '*');
|
||||
} else if (insertedMult && (((openParen + 2) < conf.length) && (signs.indexOf(conf[openParen + 2].toString()) === -1))) {
|
||||
// insertedMult is utilized here to let us account for an additional item being inserted into the array (the "*" from before openParn)
|
||||
conf.splice(openParen + 2, 0, '*');
|
||||
}
|
||||
}
|
||||
|
||||
// Evaluate all EMMDAS by looping thru each teir of operators (exponential is the higehest teir, addition/subtraction the lowest)
|
||||
const allCurOps = [['^'], ['*', '/', '%'], ['+', '-']];
|
||||
allCurOps.forEach((curOps) => {
|
||||
loggingEnabled && log(LT.LOG, `Evaluating roll ${JSON.stringify(conf)} | Evaluating ${JSON.stringify(curOps)}`);
|
||||
// Iterate thru all operators/operands in the conf
|
||||
for (let i = 0; i < conf.length; i++) {
|
||||
loggingEnabled && log(LT.LOG, `Evaluating roll ${JSON.stringify(conf)} | Evaluating ${JSON.stringify(curOps)} | Checking ${JSON.stringify(conf[i])}`);
|
||||
// Check if the current index is in the active teir of operators
|
||||
if (curOps.indexOf(conf[i].toString()) > -1) {
|
||||
// Grab the operands from before and after the operator
|
||||
const operand1 = conf[i - 1];
|
||||
const operand2 = conf[i + 1];
|
||||
// Init temp math to NaN to catch bad parsing
|
||||
let oper1 = NaN;
|
||||
let oper2 = NaN;
|
||||
const subStepSolve = {
|
||||
total: NaN,
|
||||
details: '',
|
||||
containsCrit: false,
|
||||
containsFail: false,
|
||||
};
|
||||
|
||||
// If operand1 is a SolvedStep, populate our subStepSolve with its details and crit/fail flags
|
||||
if (typeof operand1 === 'object') {
|
||||
oper1 = operand1.total;
|
||||
subStepSolve.details = `${operand1.details}\\${conf[i]}`;
|
||||
subStepSolve.containsCrit = operand1.containsCrit;
|
||||
subStepSolve.containsFail = operand1.containsFail;
|
||||
} else {
|
||||
// else parse it as a number and add it to the subStep details
|
||||
if (operand1 || operand1 == 0) {
|
||||
oper1 = parseFloat(operand1.toString());
|
||||
subStepSolve.details = `${oper1.toString()}\\${conf[i]}`;
|
||||
}
|
||||
}
|
||||
|
||||
// If operand2 is a SolvedStep, populate our subStepSolve with its details without overriding what operand1 filled in
|
||||
if (typeof operand2 === 'object') {
|
||||
oper2 = operand2.total;
|
||||
subStepSolve.details += operand2.details;
|
||||
subStepSolve.containsCrit = subStepSolve.containsCrit || operand2.containsCrit;
|
||||
subStepSolve.containsFail = subStepSolve.containsFail || operand2.containsFail;
|
||||
} else {
|
||||
// else parse it as a number and add it to the subStep details
|
||||
oper2 = parseFloat(operand2.toString());
|
||||
subStepSolve.details += oper2;
|
||||
}
|
||||
|
||||
// Make sure neither operand is NaN before continuing
|
||||
if (isNaN(oper1) || isNaN(oper2)) {
|
||||
throw new Error('OperandNaN');
|
||||
}
|
||||
|
||||
// Verify a second time that both are numbers before doing math, throwing an error if necessary
|
||||
if ((typeof oper1 === 'number') && (typeof oper2 === 'number')) {
|
||||
// Finally do the operator on the operands, throw an error if the operator is not found
|
||||
switch (conf[i]) {
|
||||
case '^':
|
||||
subStepSolve.total = Math.pow(oper1, oper2);
|
||||
break;
|
||||
case '*':
|
||||
subStepSolve.total = oper1 * oper2;
|
||||
break;
|
||||
case '/':
|
||||
subStepSolve.total = oper1 / oper2;
|
||||
break;
|
||||
case '%':
|
||||
subStepSolve.total = oper1 % oper2;
|
||||
break;
|
||||
case '+':
|
||||
subStepSolve.total = oper1 + oper2;
|
||||
break;
|
||||
case '-':
|
||||
subStepSolve.total = oper1 - oper2;
|
||||
break;
|
||||
default:
|
||||
throw new Error('OperatorWhat');
|
||||
}
|
||||
} else {
|
||||
throw new Error('EMDASNotNumber');
|
||||
}
|
||||
|
||||
// Replace the two operands and their operator with our subStepSolve
|
||||
conf.splice(i - 1, 3, subStepSolve);
|
||||
// Because we are messing around with the array we are iterating thru, we need to back up one idx to make sure every operator gets processed
|
||||
i--;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// If we somehow have more than one item left in conf at this point, something broke, throw an error
|
||||
if (conf.length > 1) {
|
||||
loggingEnabled && log(LT.LOG, `ConfWHAT? ${JSON.stringify(conf)}`);
|
||||
throw new Error('ConfWhat');
|
||||
} else if (singleNum && (typeof (conf[0]) === 'number')) {
|
||||
// If we are only left with a number, populate the stepSolve with it
|
||||
stepSolve.total = conf[0];
|
||||
stepSolve.details = conf[0].toString();
|
||||
} else {
|
||||
// Else fully populate the stepSolve with what was computed
|
||||
stepSolve.total = (<SolvedStep> conf[0]).total;
|
||||
stepSolve.details = (<SolvedStep> conf[0]).details;
|
||||
stepSolve.containsCrit = (<SolvedStep> conf[0]).containsCrit;
|
||||
stepSolve.containsFail = (<SolvedStep> conf[0]).containsFail;
|
||||
}
|
||||
|
||||
// If this was a nested call, add on parens around the details to show what math we've done
|
||||
if (wrapDetails) {
|
||||
stepSolve.details = `(${stepSolve.details})`;
|
||||
}
|
||||
|
||||
// If our total has reached undefined for some reason, error out now
|
||||
if (stepSolve.total === undefined) {
|
||||
throw new Error('UndefinedStep');
|
||||
}
|
||||
|
||||
return stepSolve;
|
||||
};
|
152
src/utils.ts
152
src/utils.ts
|
@ -6,38 +6,13 @@
|
|||
|
||||
import {
|
||||
// Discordeno deps
|
||||
Message, MessageContent
|
||||
} from "../deps.ts";
|
||||
|
||||
// split2k(longMessage) returns shortMessage[]
|
||||
// split2k takes a long string in and cuts it into shorter strings to be sent in Discord
|
||||
const split2k = (chunk: string): string[] => {
|
||||
// Replace any malformed newline characters
|
||||
chunk = chunk.replace(/\\n/g, "\n");
|
||||
const bites = [];
|
||||
|
||||
// While there is more characters than allowed to be sent in discord
|
||||
while (chunk.length > 2000) {
|
||||
// Take 2001 chars to see if word magically ends on char 2000
|
||||
let bite = chunk.substr(0, 2001);
|
||||
const lastI = bite.lastIndexOf(" ");
|
||||
if (lastI < 2000) {
|
||||
// If there is a final word before the 2000 split point, split right after that word
|
||||
bite = bite.substr(0, lastI);
|
||||
} else {
|
||||
// Else cut exactly 2000 characters
|
||||
bite = bite.substr(0, 2000);
|
||||
}
|
||||
|
||||
// Push and remove the bite taken out of the chunk
|
||||
bites.push(bite);
|
||||
chunk = chunk.slice(bite.length);
|
||||
}
|
||||
// Push leftovers into bites
|
||||
bites.push(chunk);
|
||||
|
||||
return bites;
|
||||
};
|
||||
DiscordenoMessage,
|
||||
// Log4Deno deps
|
||||
log,
|
||||
LT,
|
||||
// Discordeno deps
|
||||
sendMessage,
|
||||
} from '../deps.ts';
|
||||
|
||||
// ask(prompt) returns string
|
||||
// ask prompts the user at command line for message
|
||||
|
@ -48,101 +23,92 @@ const ask = async (question: string, stdin = Deno.stdin, stdout = Deno.stdout):
|
|||
await stdout.write(new TextEncoder().encode(question));
|
||||
|
||||
// Read console's input into answer
|
||||
const n = <number>await stdin.read(buf);
|
||||
const n = <number> await stdin.read(buf);
|
||||
const answer = new TextDecoder().decode(buf.subarray(0, n));
|
||||
|
||||
return answer.trim();
|
||||
};
|
||||
|
||||
// cmdPrompt(logChannel, botName, sendMessage) returns nothing
|
||||
// cmdPrompt(logChannel, botName) returns nothing
|
||||
// cmdPrompt creates an interactive CLI for the bot, commands can vary
|
||||
const cmdPrompt = async (logChannel: string, botName: string, sendMessage: (c: string, m: string) => Promise<Message>): Promise<void> => {
|
||||
const cmdPrompt = async (logChannel: bigint, botName: string): Promise<void> => {
|
||||
let done = false;
|
||||
|
||||
while (!done) {
|
||||
// Get a command and its args
|
||||
const fullCmd = await ask("cmd> ");
|
||||
const fullCmd = await ask('cmd> ');
|
||||
|
||||
// Split the args off of the command and prep the command
|
||||
const args = fullCmd.split(" ");
|
||||
const args = fullCmd.split(' ');
|
||||
const command = args.shift()?.toLowerCase();
|
||||
|
||||
// All commands below here
|
||||
|
||||
// exit or e
|
||||
// Fully closes the bot
|
||||
if (command === "exit" || command === "e") {
|
||||
if (command === 'exit' || command === 'e') {
|
||||
console.log(`${botName} Shutting down.\n\nGoodbye.`);
|
||||
done = true;
|
||||
Deno.exit(0);
|
||||
}
|
||||
|
||||
// stop
|
||||
} // stop
|
||||
// Closes the CLI only, leaving the bot running truly headless
|
||||
else if (command === "stop") {
|
||||
else if (command === 'stop') {
|
||||
console.log(`Closing ${botName} CLI. Bot will continue to run.\n\nGoodbye.`);
|
||||
done = true;
|
||||
}
|
||||
|
||||
// m [channel] [message]
|
||||
} // m [channel] [message]
|
||||
// Sends [message] to specified [channel]
|
||||
else if (command === "m") {
|
||||
else if (command === 'm') {
|
||||
try {
|
||||
const channelID = args.shift() || "";
|
||||
const message = args.join(" ");
|
||||
const channelId = args.shift() || '';
|
||||
const message = args.join(' ');
|
||||
|
||||
// Utilize the split2k function to ensure a message over 2000 chars is not sent
|
||||
const messages = split2k(message);
|
||||
for (let i = 0; i < messages.length; i++) {
|
||||
sendMessage(channelID, messages[i]).catch(reason => {
|
||||
console.error(reason);
|
||||
});
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
// ml [message]
|
||||
// Sends a message to the specified log channel
|
||||
else if (command === "ml") {
|
||||
const message = args.join(" ");
|
||||
|
||||
// Utilize the split2k function to ensure a message over 2000 chars is not sent
|
||||
const messages = split2k(message);
|
||||
for (let i = 0; i < messages.length; i++) {
|
||||
sendMessage(logChannel, messages[i]).catch(reason => {
|
||||
sendMessage(BigInt(channelId), message).catch((reason) => {
|
||||
console.error(reason);
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
// help or h
|
||||
} // ml [message]
|
||||
// Sends a message to the specified log channel
|
||||
else if (command === 'ml') {
|
||||
const message = args.join(' ');
|
||||
|
||||
sendMessage(logChannel, message).catch((reason) => {
|
||||
console.error(reason);
|
||||
});
|
||||
} // help or h
|
||||
// Shows a basic help menu
|
||||
else if (command === "help" || command === "h") {
|
||||
console.log(`${botName} CLI Help:\n\nAvailable Commands:\n exit - closes bot\n stop - closes the CLI\n m [ChannelID] [messgae] - sends message to specific ChannelID as the bot\n ml [message] sends a message to the specified botlog\n help - this message`);
|
||||
}
|
||||
|
||||
// Unhandled commands die here
|
||||
else if (command === 'help' || command === 'h') {
|
||||
console.log(`${botName} CLI Help:
|
||||
|
||||
Available Commands:
|
||||
exit - closes bot
|
||||
stop - closes the CLI
|
||||
m [ChannelID] [messgae] - sends message to specific ChannelID as the bot
|
||||
ml [message] sends a message to the specified botlog
|
||||
help - this message`);
|
||||
} // Unhandled commands die here
|
||||
else {
|
||||
console.log("undefined command");
|
||||
console.log('undefined command');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// sendIndirectMessage(originalMessage, messageContent, sendMessage, sendDirectMessage) returns Message
|
||||
// sendIndirectMessage determines if the message needs to be sent as a direct message or as a normal message
|
||||
const sendIndirectMessage = async (originalMessage: Message, messageContent: (string | MessageContent), sendMessage: (c: string, m: (string | MessageContent)) => Promise<Message>, sendDirectMessage: (c: string, m: (string | MessageContent)) => Promise<Message>): Promise<Message> => {
|
||||
if (originalMessage.guildID === "") {
|
||||
// guildID was empty, meaning the original message was sent as a DM
|
||||
return await sendDirectMessage(originalMessage.author.id, messageContent);
|
||||
} else {
|
||||
// guildID was not empty, meaning the original message was sent in a server
|
||||
return await sendMessage(originalMessage.channelID, messageContent);
|
||||
}
|
||||
const genericLogger = (level: LT, message: string) => log(level, message);
|
||||
const messageEditError = (location: string, message: DiscordenoMessage | string, err: Error) =>
|
||||
genericLogger(LT.ERROR, `${location} | Failed to edit message: ${JSON.stringify(message)} | Error: ${err.name} - ${err.message}`);
|
||||
const messageSendError = (location: string, message: DiscordenoMessage | string, err: Error) =>
|
||||
genericLogger(LT.ERROR, `${location} | Failed to send message: ${JSON.stringify(message)} | Error: ${err.name} - ${err.message}`);
|
||||
const messageDeleteError = (location: string, message: DiscordenoMessage | string, err: Error) =>
|
||||
genericLogger(LT.ERROR, `${location} | Failed to delete message: ${JSON.stringify(message)} | Error: ${err.name} - ${err.message}`);
|
||||
const dbError = (location: string, type: string, err: Error) => genericLogger(LT.ERROR, `${location} | Failed to ${type} database | Error: ${err.name} - ${err.message}`);
|
||||
|
||||
export default {
|
||||
commonLoggers: {
|
||||
dbError,
|
||||
messageEditError,
|
||||
messageSendError,
|
||||
messageDeleteError,
|
||||
},
|
||||
cmdPrompt,
|
||||
};
|
||||
|
||||
// Write logging function with trace and whatnot for errors and necessary messages to log, log bot state in server to determine if user is at fault or if I am at fault (maybe message user if its their fault?)
|
||||
|
||||
export default { split2k, cmdPrompt, sendIndirectMessage };
|
||||
|
|
|
@ -0,0 +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
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"allowJs": true,
|
||||
"lib": ["es2022"],
|
||||
"strict": true
|
||||
}
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<html lang="en">
|
||||
<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" />
|
||||
|
@ -33,9 +33,9 @@
|
|||
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>
|
||||
<a href="https://discord.com/api/oauth2/authorize?client_id=789045930011656223&permissions=2048&scope=bot" target="_blank" rel="noopener">Invite Me!</a>
|
||||
<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 id="page-contents">
|
||||
|
@ -44,11 +44,11 @@
|
|||
</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>
|
||||
<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/>
|
||||
|
||||
<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 class="field-group">
|
||||
|
@ -119,10 +119,10 @@
|
|||
</div>
|
||||
<div id="footer">
|
||||
<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 id="footer-right">
|
||||
Version 1.4.2
|
||||
Version 2.0.0
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -143,3 +143,33 @@ body {
|
|||
padding: 0 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 807px) {
|
||||
#page {
|
||||
grid-template-rows: 6rem calc(100vh - 8rem) 2rem;
|
||||
}
|
||||
|
||||
#page-contents {
|
||||
height: calc(100vh - 8rem);
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 474px) {
|
||||
#page {
|
||||
grid-template-rows: 9rem calc(100vh - 11rem) 2rem;
|
||||
}
|
||||
|
||||
#page-contents {
|
||||
height: calc(100vh - 11rem);
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 337px) {
|
||||
#page {
|
||||
grid-template-rows: 12rem calc(100vh - 14rem) 2rem;
|
||||
}
|
||||
|
||||
#page-contents {
|
||||
height: calc(100vh - 14rem);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,52 +3,68 @@
|
|||
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");
|
||||
const apiField = document.getElementById("api-field");
|
||||
const userField = document.getElementById("user-field");
|
||||
const channelField = document.getElementById("channel-field");
|
||||
const emailField = document.getElementById("email-field");
|
||||
const deleteField = document.getElementById("delete-field");
|
||||
const submitField = document.getElementById("submit-field");
|
||||
|
||||
var endpoint = "none";
|
||||
var status = "activate";
|
||||
let endpoint = "none";
|
||||
let apiStatus = "activate";
|
||||
|
||||
function validateUserField() {
|
||||
return !(userField.value > 0 && userField.checkValidity());
|
||||
}
|
||||
|
||||
function validateEmailField() {
|
||||
return !(emailField.value.length > 0 && emailField.checkValidity());
|
||||
}
|
||||
|
||||
function validateApiField() {
|
||||
return !(apiField.value.length > 0 && apiField.checkValidity());
|
||||
}
|
||||
|
||||
function validateChannelField() {
|
||||
return !(channelField.value > 0 && channelField.checkValidity());
|
||||
}
|
||||
|
||||
// Checks if all fields needed for the selected endpoint are valid
|
||||
function validateFields() {
|
||||
if (!(userField.value > 0 && userField.checkValidity())) {
|
||||
if (validateUserField()) {
|
||||
submitField.disabled = true;
|
||||
return;
|
||||
}
|
||||
switch (endpoint) {
|
||||
case "generate":
|
||||
if (!(emailField.value.length > 0 && emailField.checkValidity())) {
|
||||
if (validateEmailField()) {
|
||||
submitField.disabled = true;
|
||||
return;
|
||||
}
|
||||
break;
|
||||
case "delete":
|
||||
if (!(apiField.value.length > 0 && apiField.checkValidity())) {
|
||||
if (validateApiField()) {
|
||||
submitField.disabled = true;
|
||||
return;
|
||||
}
|
||||
if (!(emailField.value.length > 0 && emailField.checkValidity())) {
|
||||
if (validateEmailField()) {
|
||||
submitField.disabled = true;
|
||||
return;
|
||||
}
|
||||
break;
|
||||
case "view":
|
||||
if (!(apiField.value.length > 0 && apiField.checkValidity())) {
|
||||
if (validateApiField()) {
|
||||
submitField.disabled = true;
|
||||
return;
|
||||
}
|
||||
break;
|
||||
case "add":
|
||||
case "activate":
|
||||
if (!(apiField.value.length > 0 && apiField.checkValidity())) {
|
||||
if (validateApiField()) {
|
||||
submitField.disabled = true;
|
||||
return;
|
||||
}
|
||||
if (!(channelField.value > 0 && channelField.checkValidity())) {
|
||||
if (validateChannelField()) {
|
||||
submitField.disabled = true;
|
||||
return;
|
||||
}
|
||||
|
@ -60,6 +76,14 @@ function validateFields() {
|
|||
submitField.disabled = false;
|
||||
}
|
||||
|
||||
function setFieldClasses(showApi, showChannel, showActive, showEmail, showDelete) {
|
||||
document.getElementById("api-field-group").className = showApi ? "field-group" : "hidden";
|
||||
document.getElementById("channel-field-group").className = showChannel ? "field-group" : "hidden";
|
||||
document.getElementById("active-field-group").className = showActive ? "field-group" : "hidden";
|
||||
document.getElementById("email-field-group").className = showEmail ? "field-group" : "hidden";
|
||||
document.getElementById("delete-field-group").className = showDelete ? "field-group" : "hidden";
|
||||
}
|
||||
|
||||
// Shows appropriate fields for selected endpoint
|
||||
function showFields() {
|
||||
document.getElementById("fields").className = "";
|
||||
|
@ -68,39 +92,19 @@ function showFields() {
|
|||
|
||||
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";
|
||||
setFieldClasses(false, false, false, true, false);
|
||||
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";
|
||||
setFieldClasses(true, false, false, true, true);
|
||||
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";
|
||||
setFieldClasses(true, false, false, false, false);
|
||||
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";
|
||||
setFieldClasses(true, true, false, false, false);
|
||||
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";
|
||||
setFieldClasses(true, true, true, false, false);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
|
@ -111,16 +115,16 @@ function showFields() {
|
|||
|
||||
// Sets the status for channel activation/deactivation
|
||||
function setStatus() {
|
||||
status = this.value;
|
||||
apiStatus = this.value;
|
||||
}
|
||||
|
||||
// Sends the request
|
||||
function sendPayload() {
|
||||
document.getElementById("results").className = "";
|
||||
|
||||
var xhr = new XMLHttpRequest();
|
||||
var method;
|
||||
var path = "/api/";
|
||||
const xhr = new XMLHttpRequest();
|
||||
let method;
|
||||
let path = "/api/";
|
||||
|
||||
switch (endpoint) {
|
||||
case "generate":
|
||||
|
@ -141,7 +145,7 @@ function sendPayload() {
|
|||
break;
|
||||
case "activate":
|
||||
method = "PUT";
|
||||
path += "channel/" + status + "?user=" + userField.value + "&channel=" + channelField.value;
|
||||
path += "channel/" + apiStatus + "?user=" + userField.value + "&channel=" + channelField.value;
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 99 KiB After Width: | Height: | Size: 689 KiB |
Binary file not shown.
|
@ -1,5 +1,5 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<html lang="en">
|
||||
<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" />
|
||||
|
@ -33,9 +33,9 @@
|
|||
The Artificer
|
||||
</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>
|
||||
<a href="https://discord.com/api/oauth2/authorize?client_id=789045930011656223&permissions=2048&scope=bot" target="_blank" rel="noopener">Invite Me!</a>
|
||||
<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 id="page-contents">
|
||||
|
@ -47,16 +47,16 @@
|
|||
<img src="./img/TheArtificer.png" alt="The Artificer Logo" />
|
||||
</div>
|
||||
<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://roll20.zendesk.com/hc/en-us/articles/360037773133-Dice-Reference" 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><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 id="examples">
|
||||
<h2>Available Commands:</h2>
|
||||
<div class="slug">
|
||||
<h3>Rolling Command:</h3>
|
||||
<p>This command is what the bot is all about. Using the <a href="https://roll20.zendesk.com/hc/en-us/articles/360037773133-Dice-Reference" 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>
|
||||
<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>
|
||||
|
@ -72,10 +72,10 @@
|
|||
<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>[[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>
|
||||
<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/>
|
||||
<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 id="api">
|
||||
|
@ -83,18 +83,18 @@
|
|||
<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>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>
|
||||
<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" rel="noopener">GitHub Repository</a>.</p>
|
||||
</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>
|
||||
Built by <a href="https://github.com/Burn-E99/" target="_blank" rel="noopener">Ean Milligan</a>
|
||||
</div>
|
||||
<div id="footer-right">
|
||||
Version 1.4.2
|
||||
Version 2.0.0
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -163,3 +163,33 @@ p.example {
|
|||
padding: 0 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 630px) {
|
||||
#page {
|
||||
grid-template-rows: 6rem calc(100vh - 8rem) 2rem;
|
||||
}
|
||||
|
||||
#page-contents {
|
||||
height: calc(100vh - 8rem);
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 330px) {
|
||||
#page {
|
||||
grid-template-rows: 9rem calc(100vh - 11rem) 2rem;
|
||||
}
|
||||
|
||||
#page-contents {
|
||||
height: calc(100vh - 11rem);
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 292px) {
|
||||
#page {
|
||||
grid-template-rows: 12rem calc(100vh - 14rem) 2rem;
|
||||
}
|
||||
|
||||
#page-contents {
|
||||
height: calc(100vh - 14rem);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue