Ported CrossExchange v1.0.1 and removed stonks

This commit is contained in:
WatDuhHekBro 2020-07-25 03:15:26 -05:00
parent b5e1ceaad3
commit 295995aba2
16 changed files with 2022 additions and 4 deletions

12
.gitignore vendored
View File

@ -1,13 +1,17 @@
# Specific to this repository
dist/
data/
tmp/
test*
*.bat
desktop.ini
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
config.json
config.js
data/
test.js
# Runtime data
pids

0
docs/CHANGELOG.md Normal file
View File

31
docs/Specifications.md Normal file
View File

@ -0,0 +1,31 @@
# Structure
The top-level directory is reserved for files that have to be there for it to work as well as configuration files.
- `src`: Contains all the code for the bot itself. Code in this directory is for independent tasks keeping the initialization out of the subdirectories.
- `core`: This is where core structures and critical functions for the bot go.
- `modules`: This is where modules go that accomplish one specific purpose but isn't so necessary for the bot to function. The goal is to be able to safely remove these without too much trouble.
- `commands`: Here's the place to store commands. The file name determines the command name.
- `dist`: This is where the runnable code in `src` compiles to. (The directory structure mirrors `src`.)
- `data`: Holds all the dynamic data used by the bot. This is what you modify if you want to change stuff for just your instance of the bot.
- `standard`: Contains all the standard data to be used with the project itself. It's part of the code and will not be checked for inaccuracies because it's not meant to be easily modified.
- `docs`: Used for information about the design of the project.
# Specific Files
This list starts from `src`/`dist`.
- `index`: This is the entry point of the bot. Here is where all the initialization is done, because the idea is to keep repeatable code in separate modules while having code that runs only once here designating this is **the** starting point.
- `setup`: Used for the first time the bot is loaded, walking the user through setting up the bot.
- `core/lib`: Exports a function object which lets you wrap values letting you call special functions as well as calling utility functions common to all commands.
- `core/structures`: Contains all the structures that the dynamic data read from JSON files should follow. This exports instances of these classes.
- `core/command`: Contains the class used to instantiate commands.
- `core/storage`: Exports an object which handles everything related to files.
- `core/wrappers`: Contains classes that wrap around values and provide extra functionality.
# Design Decisions
- All top-level files (relative to `src`/`dist`) should ideally be independent, one-time use scripts. This helps separate code that just initializes once and reusable code that forms the bulk of the main program itself. That's why all the file searching and loading commands/events will be done in `index`.
- Wrapper objects were designed with the idea of letting you assign functions directly to native objects [without the baggage of actually doing so](https://developer.mozilla.org/en-US/docs/Web/JavaScript/The_performance_hazards_of__%5B%5BPrototype%5D%5D_mutation).
- `test` should be a keyword for any file not tracked by git and generally be more flexible to play around with. It should also be automatically generated during initialization in `commands` so you can have templates ready for new commands.
- The storage module should not provide an auto-write feature. This would actually end up overcomplicating things especially when code isn't fully blocking.
- I think it's much easier to make a template system within the code itself. After all, the templates only change when the code changes to use new keys or remove old ones. You'll also be able to dedicate specific classes for the task rather than attaching meta tags to arrays and objects.
- I decided to forget about implementing dynamic events. I don't think it'll work with this setup. After all, there are only so many events you can use, whereas commands can have any number of ones, more suitable for dynamic loading. The main reasons were unsecure types and no easy way to access variables like the config or client.
- I want to make attaching subcommands more flexible, so you can either add subcommands in the constructor or by using a method. However, you have to add all other properties when instantiating a command.
- All commands should have only one parameter. This parameter is meant to be flexible so you can add properties without making a laundry list of parameters. It also has convenience functions too so you don't have to import the library for each command.
- The objects in `core/structures` are initialized into a special object once and then cached into memory automatically due to an import system. This means you don't have to keep loading JSON files over and over again even without the stack storage system. Because a JSON file resolves into an object, any extra keys are removed automatically (as it isn't initialized into the data) and any property that doesn't yet exist on the JSON object will be initialized into something. You can then have specific functions like `addUser` onto objects with a specific structure.

532
package-lock.json generated Normal file
View File

@ -0,0 +1,532 @@
{
"name": "d.js-v12-bot",
"version": "0.0.1",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"@discordjs/collection": {
"version": "0.1.5",
"resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-0.1.5.tgz",
"integrity": "sha512-CU1q0UXQUpFNzNB7gufgoisDHP7n+T3tkqTsp3MNUkVJ5+hS3BCvME8uCXAUFlz+6T2FbTCu75A+yQ7HMKqRKw=="
},
"@discordjs/form-data": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@discordjs/form-data/-/form-data-3.0.1.tgz",
"integrity": "sha512-ZfFsbgEXW71Rw/6EtBdrP5VxBJy4dthyC0tpQKGKmYFImlmmrykO14Za+BiIVduwjte0jXEBlhSKf0MWbFp9Eg==",
"requires": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"mime-types": "^2.1.12"
}
},
"@types/color-name": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz",
"integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ=="
},
"@types/inquirer": {
"version": "6.5.0",
"resolved": "https://registry.npmjs.org/@types/inquirer/-/inquirer-6.5.0.tgz",
"integrity": "sha512-rjaYQ9b9y/VFGOpqBEXRavc3jh0a+e6evAbI31tMda8VlPaSy0AZJfXsvmIe3wklc7W6C3zCSfleuMXR7NOyXw==",
"dev": true,
"requires": {
"@types/through": "*",
"rxjs": "^6.4.0"
}
},
"@types/node": {
"version": "14.0.22",
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.0.22.tgz",
"integrity": "sha512-emeGcJvdiZ4Z3ohbmw93E/64jRzUHAItSHt8nF7M4TGgQTiWqFVGB8KNpLGFmUHmHLvjvBgFwVlqNcq+VuGv9g==",
"dev": true
},
"@types/through": {
"version": "0.0.30",
"resolved": "https://registry.npmjs.org/@types/through/-/through-0.0.30.tgz",
"integrity": "sha512-FvnCJljyxhPM3gkRgWmxmDZyAQSiBQQWLI0A0VFL0K7W1oRUrPJSqNO0NvTnLkBcotdlp3lKvaT0JrnyRDkzOg==",
"dev": true,
"requires": {
"@types/node": "*"
}
},
"@types/ws": {
"version": "7.2.6",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-7.2.6.tgz",
"integrity": "sha512-Q07IrQUSNpr+cXU4E4LtkSIBPie5GLZyyMC1QtQYRLWz701+XcoVygGUZgvLqElq1nU4ICldMYPnexlBsg3dqQ==",
"dev": true,
"requires": {
"@types/node": "*"
}
},
"abort-controller": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
"integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==",
"requires": {
"event-target-shim": "^5.0.0"
}
},
"ansi-escapes": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.1.tgz",
"integrity": "sha512-JWF7ocqNrp8u9oqpgV+wH5ftbt+cfvv+PTjOvKLT3AdYly/LmORARfEVT1iyjwN+4MqE5UmVKoAdIBqeoCHgLA==",
"requires": {
"type-fest": "^0.11.0"
}
},
"ansi-regex": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz",
"integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg=="
},
"ansi-styles": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz",
"integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==",
"requires": {
"@types/color-name": "^1.1.1",
"color-convert": "^2.0.1"
}
},
"asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k="
},
"chalk": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz",
"integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==",
"requires": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
}
},
"chardet": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz",
"integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA=="
},
"cli-cursor": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz",
"integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==",
"requires": {
"restore-cursor": "^3.1.0"
}
},
"cli-width": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz",
"integrity": "sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw=="
},
"color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"requires": {
"color-name": "~1.1.4"
}
},
"color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
},
"combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"requires": {
"delayed-stream": "~1.0.0"
}
},
"cross-spawn": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
"integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
"dev": true,
"requires": {
"path-key": "^3.1.0",
"shebang-command": "^2.0.0",
"which": "^2.0.1"
}
},
"delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk="
},
"discord.js": {
"version": "12.2.0",
"resolved": "https://registry.npmjs.org/discord.js/-/discord.js-12.2.0.tgz",
"integrity": "sha512-Ueb/0SOsxXyqwvwFYFe0msMrGqH1OMqpp2Dpbplnlr4MzcRrFWwsBM9gKNZXPVBHWUKiQkwU8AihXBXIvTTSvg==",
"requires": {
"@discordjs/collection": "^0.1.5",
"@discordjs/form-data": "^3.0.1",
"abort-controller": "^3.0.0",
"node-fetch": "^2.6.0",
"prism-media": "^1.2.0",
"setimmediate": "^1.0.5",
"tweetnacl": "^1.0.3",
"ws": "^7.2.1"
},
"dependencies": {
"prism-media": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/prism-media/-/prism-media-1.2.2.tgz",
"integrity": "sha512-I+nkWY212lJ500jLe4tN9tWO7nRiBAVdMv76P9kffZjYhw20raMlW1HSSvS+MLXC9MmbNZCazMrAr+5jEEgTuw=="
},
"ws": {
"version": "7.3.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-7.3.0.tgz",
"integrity": "sha512-iFtXzngZVXPGgpTlP1rBqsUK82p9tKqsWRPg5L56egiljujJT3vGAYnHANvFxBieXrTFavhzhxW52jnaWV+w2w=="
}
}
},
"duplexer": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.1.tgz",
"integrity": "sha1-rOb/gIwc5mtX0ev5eXessCM0z8E=",
"dev": true
},
"emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
},
"escape-string-regexp": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
"integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ="
},
"event-stream": {
"version": "3.3.4",
"resolved": "https://registry.npmjs.org/event-stream/-/event-stream-3.3.4.tgz",
"integrity": "sha1-SrTJoPWlTbkzi0w02Gv86PSzVXE=",
"dev": true,
"requires": {
"duplexer": "~0.1.1",
"from": "~0",
"map-stream": "~0.1.0",
"pause-stream": "0.0.11",
"split": "0.3",
"stream-combiner": "~0.0.4",
"through": "~2.3.1"
}
},
"event-target-shim": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
"integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ=="
},
"external-editor": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz",
"integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==",
"requires": {
"chardet": "^0.7.0",
"iconv-lite": "^0.4.24",
"tmp": "^0.0.33"
}
},
"figures": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz",
"integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==",
"requires": {
"escape-string-regexp": "^1.0.5"
}
},
"from": {
"version": "0.1.7",
"resolved": "https://registry.npmjs.org/from/-/from-0.1.7.tgz",
"integrity": "sha1-g8YK/Fi5xWmXAH7Rp2izqzA6RP4=",
"dev": true
},
"has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="
},
"iconv-lite": {
"version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
"requires": {
"safer-buffer": ">= 2.1.2 < 3"
}
},
"inquirer": {
"version": "7.3.1",
"resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.3.1.tgz",
"integrity": "sha512-/+vOpHQHhoh90Znev8BXiuw1TDQ7IDxWsQnFafUEoK5+4uN5Eoz1p+3GqOj/NtzEi9VzWKQcV9Bm+i8moxedsA==",
"requires": {
"ansi-escapes": "^4.2.1",
"chalk": "^4.1.0",
"cli-cursor": "^3.1.0",
"cli-width": "^3.0.0",
"external-editor": "^3.0.3",
"figures": "^3.0.0",
"lodash": "^4.17.16",
"mute-stream": "0.0.8",
"run-async": "^2.4.0",
"rxjs": "^6.6.0",
"string-width": "^4.1.0",
"strip-ansi": "^6.0.0",
"through": "^2.3.6"
}
},
"is-fullwidth-code-point": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="
},
"isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
"integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=",
"dev": true
},
"lodash": {
"version": "4.17.19",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz",
"integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ=="
},
"map-stream": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/map-stream/-/map-stream-0.1.0.tgz",
"integrity": "sha1-5WqpTEyAVaFkBKBnS3jyFffI4ZQ=",
"dev": true
},
"mime-db": {
"version": "1.44.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.44.0.tgz",
"integrity": "sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg=="
},
"mime-types": {
"version": "2.1.27",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.27.tgz",
"integrity": "sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w==",
"requires": {
"mime-db": "1.44.0"
}
},
"mimic-fn": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
"integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="
},
"mute-stream": {
"version": "0.0.8",
"resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz",
"integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA=="
},
"node-cleanup": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/node-cleanup/-/node-cleanup-2.1.2.tgz",
"integrity": "sha1-esGavSl+Caf3KnFUXZUbUX5N3iw=",
"dev": true
},
"node-fetch": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.0.tgz",
"integrity": "sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA=="
},
"onetime": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.0.tgz",
"integrity": "sha512-5NcSkPHhwTVFIQN+TUqXoS5+dlElHXdpAWu9I0HP20YOtIi+aZ0Ct82jdlILDxjLEAWwvm+qj1m6aEtsDVmm6Q==",
"requires": {
"mimic-fn": "^2.1.0"
}
},
"os-tmpdir": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz",
"integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ="
},
"path-key": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
"dev": true
},
"pause-stream": {
"version": "0.0.11",
"resolved": "https://registry.npmjs.org/pause-stream/-/pause-stream-0.0.11.tgz",
"integrity": "sha1-/lo0sMvOErWqaitAPuLnO2AvFEU=",
"dev": true,
"requires": {
"through": "~2.3"
}
},
"ps-tree": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/ps-tree/-/ps-tree-1.2.0.tgz",
"integrity": "sha512-0VnamPPYHl4uaU/nSFeZZpR21QAWRz+sRv4iW9+v/GS/J5U5iZB5BNN6J0RMoOvdx2gWM2+ZFMIm58q24e4UYA==",
"dev": true,
"requires": {
"event-stream": "=3.3.4"
}
},
"restore-cursor": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz",
"integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==",
"requires": {
"onetime": "^5.1.0",
"signal-exit": "^3.0.2"
}
},
"run-async": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz",
"integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ=="
},
"rxjs": {
"version": "6.6.0",
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.0.tgz",
"integrity": "sha512-3HMA8z/Oz61DUHe+SdOiQyzIf4tOx5oQHmMir7IZEu6TMqCLHT4LRcmNaUS0NwOz8VLvmmBduMsoaUvMaIiqzg==",
"requires": {
"tslib": "^1.9.0"
}
},
"safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
},
"setimmediate": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
"integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU="
},
"shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
"dev": true,
"requires": {
"shebang-regex": "^3.0.0"
}
},
"shebang-regex": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
"dev": true
},
"signal-exit": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz",
"integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA=="
},
"split": {
"version": "0.3.3",
"resolved": "https://registry.npmjs.org/split/-/split-0.3.3.tgz",
"integrity": "sha1-zQ7qXmOiEd//frDwkcQTPi0N0o8=",
"dev": true,
"requires": {
"through": "2"
}
},
"stream-combiner": {
"version": "0.0.4",
"resolved": "https://registry.npmjs.org/stream-combiner/-/stream-combiner-0.0.4.tgz",
"integrity": "sha1-TV5DPBhSYd3mI8o/RMWGvPXErRQ=",
"dev": true,
"requires": {
"duplexer": "~0.1.1"
}
},
"string-argv": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.1.2.tgz",
"integrity": "sha512-mBqPGEOMNJKXRo7z0keX0wlAhbBAjilUdPW13nN0PecVryZxdHIeM7TqbsSUA7VYuS00HGC6mojP7DlQzfa9ZA==",
"dev": true
},
"string-width": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz",
"integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==",
"requires": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
"strip-ansi": "^6.0.0"
}
},
"strip-ansi": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz",
"integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==",
"requires": {
"ansi-regex": "^5.0.0"
}
},
"supports-color": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz",
"integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==",
"requires": {
"has-flag": "^4.0.0"
}
},
"through": {
"version": "2.3.8",
"resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
"integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU="
},
"tmp": {
"version": "0.0.33",
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz",
"integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==",
"requires": {
"os-tmpdir": "~1.0.2"
}
},
"tsc-watch": {
"version": "4.2.9",
"resolved": "https://registry.npmjs.org/tsc-watch/-/tsc-watch-4.2.9.tgz",
"integrity": "sha512-DlTaoDs74+KUpyWr7dCGhuscAUKCz6CiFduBN7R9RbLJSSN1moWdwoCLASE7+zLgGvV5AwXfYDiEMAsPGaO+Vw==",
"dev": true,
"requires": {
"cross-spawn": "^7.0.3",
"node-cleanup": "^2.1.2",
"ps-tree": "^1.2.0",
"string-argv": "^0.1.1",
"strip-ansi": "^6.0.0"
}
},
"tslib": {
"version": "1.13.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.13.0.tgz",
"integrity": "sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q=="
},
"tweetnacl": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz",
"integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw=="
},
"type-fest": {
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.11.0.tgz",
"integrity": "sha512-OdjXJxnCN1AvyLSzeKIgXTXxV+99ZuXl3Hpo9XpJAv9MBcHrrJOQ5kV7ypXOuQie+AmWG25hLbiKdwYTifzcfQ=="
},
"typescript": {
"version": "3.9.6",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.6.tgz",
"integrity": "sha512-Pspx3oKAPJtjNwE92YS05HQoY7z2SFyOpHo9MqJor3BXAGNaPUs83CuVp9VISFkSjyRfiTpmKuAYGJB7S7hOxw==",
"dev": true
},
"which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
"dev": true,
"requires": {
"isexe": "^2.0.0"
}
}
}
}

31
package.json Normal file
View File

@ -0,0 +1,31 @@
{
"name": "d.js-v12-bot",
"version": "0.0.1",
"description": "A Discord bot built on Discord.JS v12",
"main": "dist/index.js",
"dependencies": {
"chalk": "^4.1.0",
"discord.js": "^12.2.0",
"inquirer": "^7.3.1"
},
"devDependencies": {
"@types/inquirer": "^6.5.0",
"@types/node": "^14.0.22",
"@types/ws": "^7.2.6",
"tsc-watch": "^4.2.9",
"typescript": "^3.9.6"
},
"scripts": {
"build": "tsc",
"watch": "tsc --watch",
"autobuild": "tsc && npm start",
"start": "node dist/index.js",
"dev": "tsc-watch --onSuccess \"node dist/index.js dev\""
},
"keywords": [
"discord.js",
"bot"
],
"author": "Keanu Timmermans",
"license": "MIT"
}

107
src/commands/admin.ts Normal file
View File

@ -0,0 +1,107 @@
import Command from "../core/command";
import {CommonLibrary, logs} from "../core/lib";
import {Config, Storage} from "../core/structures";
import {Permissions} from "discord.js";
function authenticate($: CommonLibrary, customMessage = ""): boolean
{
const hasAccess = Config.mechanics.includes($.author.id) || ($.member?.hasPermission(Permissions.FLAGS.ADMINISTRATOR) || false);
if(!hasAccess)
{
if(customMessage !== "")
$.channel.send(customMessage);
else
{
$.channel.send(`${$.author.toString()}, you are not a server admin or one of the bot's mechanics. If you have access to the server files, add yourself to it manually in \`data/config.json\`. Your user ID should now be logged in the console.`);
$.debug($.author.id);
}
}
return hasAccess;
}
function getLogBuffer(type: string)
{
return {files: [{
attachment: Buffer.alloc(logs[type].length, logs[type]),
name: `${Date.now()}.${type}.log`
}]};
}
export default new Command({
description: "An all-in-one command to do admin stuff. You need to be either an admin of the server or one of the bot's mechanics to use this command.",
async run($: CommonLibrary): Promise<any>
{
const admin = $.member?.hasPermission(Permissions.FLAGS.ADMINISTRATOR) || false;
const mechanic = Config.mechanics.includes($.author.id);
let status = "";
if(admin && mechanic)
status = "a server admin and one of the bot's mechanics";
else if(admin)
status = "a server admin";
else if(mechanic)
status = "one of the bot's mechanics";
if(authenticate($))
$.channel.send(`${$.author.toString()}, you are ${status}, meaning you can use this command.`);
},
subcommands:
{
set: new Command({
description: "Set different per-guild settings for the bot.",
run: "You have to specify the option you want to set.",
subcommands:
{
prefix: new Command({
description: "Set a custom prefix for your guild. Removes your custom prefix if none is provided.",
usage: "(<prefix>)",
async run($: CommonLibrary): Promise<any>
{
if(authenticate($))
{
Storage.getGuild($.guild?.id || "N/A").prefix = null;
Storage.save();
$.channel.send(`The custom prefix for this guild has been removed. My prefix is now back to \`${Config.prefix}\`.`);
}
},
any: new Command({
async run($: CommonLibrary): Promise<any>
{
if(authenticate($))
{
Storage.getGuild($.guild?.id || "N/A").prefix = $.args[0];
Storage.save();
$.channel.send(`The custom prefix for this guild is now \`${$.args[0]}\`.`);
}
}
})
})
}
}),
diag: new Command({
description: "Requests a debug log with the \"info\" verbosity level.",
async run($: CommonLibrary): Promise<any>
{
if(authenticate($))
$.channel.send(getLogBuffer("info"));
},
any: new Command({
description: `Select a verbosity to listen to. Available levels: \`[${Object.keys(logs)}]\``,
async run($: CommonLibrary): Promise<any>
{
if(authenticate($))
{
const type = $.args[0];
if(type in logs)
$.channel.send(getLogBuffer(type));
else
$.channel.send(`Couldn't find a verbosity level named \`${type}\`! The available types are \`[${Object.keys(logs)}]\`.`);
}
}
})
})
}
});

102
src/commands/help.ts Normal file
View File

@ -0,0 +1,102 @@
import Command from "../core/command";
import {CommonLibrary} from "../core/lib";
import FileManager from "../core/storage";
const types = ["user", "number", "any"];
export default new Command({
description: "Lists all commands. If a command is specified, their arguments are listed as well.",
usage: "([command, [subcommand/type], ...])",
async run($: CommonLibrary): Promise<any>
{
const commands = await FileManager.loadCommands();
const list: string[] = [];
for(const [header, command] of commands)
if(header !== "test")
list.push(`- \`${header}\` - ${command.description}`);
const outList = list.length > 0 ? `\n${list.join('\n')}` : " None";
$.channel.send(`Legend: \`<type>\`, \`[list/of/subcommands]\`, \`(optional)\`, \`(<optional type>)\`, \`([optional/list/...])\`\nCommands:${outList}`, {split: true});
},
any: new Command({
async run($: CommonLibrary): Promise<any>
{
const commands = await FileManager.loadCommands();
let header = $.args.shift();
let command = commands.get(header);
if(!command || header === "test")
$.channel.send(`No command found by the name \`${header}\`!`);
else
{
let usage = command.usage;
for(const param of $.args)
{
header += ` ${param}`;
if(/<\w+>/g.test(param))
{
const type = param.match(/\w+/g)[0];
command = command[type];
if(types.includes(type) && command?.usage)
usage = command.usage;
else
{
command = undefined;
break;
}
}
else if(command?.subcommands?.[param])
{
command = command.subcommands[param];
if(command.usage !== "")
usage = command.usage;
}
else
{
command = undefined;
break;
}
}
if(!command)
return $.channel.send(`No command found by the name \`${header}\`!`);
let append = "";
if(usage === "")
{
const list: string[] = [];
for(const subtag in command.subcommands)
{
const subcmd = command.subcommands[subtag];
const customUsage = subcmd.usage ? ` ${subcmd.usage}` : "";
list.push(`- \`${header} ${subtag}${customUsage}\` - ${subcmd.description}`);
}
for(const type of types)
{
if(command[type])
{
const cmd = command[type];
const customUsage = cmd.usage ? ` ${cmd.usage}` : "";
list.push(`- \`${header} <${type}>${customUsage}\` - ${cmd.description}`);
}
}
append = "Usages:" + (list.length > 0 ? `\n${list.join('\n')}` : " None.");
}
else
append = `Usage: \`${header} ${usage}\``;
$.channel.send(`Command: \`${header}\`\nDescription: ${command.description}\n${append}`, {split: true});
}
}
})
});

241
src/commands/money.ts Normal file
View File

@ -0,0 +1,241 @@
import Command from "../core/command";
import $, {CommonLibrary} from "../core/lib";
import {Storage} from "../core/structures";
import {User} from "discord.js";
export function getMoneyEmbed(user: User): object
{
const profile = Storage.getUser(user.id);
return {embed: {
color: 0xFFFF00,
author:
{
name: user.username,
icon_url: user.displayAvatarURL({
format: "png",
dynamic: true
})
},
fields:
[
{
name: "Balance",
value: $(profile.money).pluralise("credit", "s")
}
]
}};
}
function getSendEmbed(sender: User, receiver: User, amount: number): object
{
return {embed: {
color: 0xFFFF00,
author:
{
name: sender.username,
icon_url: sender.displayAvatarURL({
format: "png",
dynamic: true
})
},
title: "Transaction",
description: `${sender.toString()} has sent ${$(amount).pluralise("credit", "s")} to ${receiver.toString()}!`,
fields:
[
{
name: `Sender: ${sender.username}#${sender.discriminator}`,
value: $(Storage.getUser(sender.id).money).pluralise("credit", "s")
},
{
name: `Receiver: ${receiver.username}#${receiver.discriminator}`,
value: $(Storage.getUser(receiver.id).money).pluralise("credit", "s")
}
],
footer:
{
text: receiver.username,
icon_url: receiver.displayAvatarURL({
format: "png",
dynamic: true
})
}
}};
}
export default new Command({
description: "See how much money you have. Also provides other commands related to money.",
async run($: CommonLibrary): Promise<any>
{
$.channel.send(getMoneyEmbed($.author));
},
subcommands:
{
get: new Command({
description: "Pick up your daily credits. The cooldown is per user and every 22 hours to allow for some leeway.",
async run($: CommonLibrary): Promise<any>
{
const user = Storage.getUser($.author.id);
const now = Date.now();
if(user.lastReceived === -1)
{
user.money = 100;
user.lastReceived = now;
Storage.save();
$.channel.send("Here's 100 credits to get started, the price of a sandwich in Rookie Harbor.", getMoneyEmbed($.author));
}
else if(now - user.lastReceived >= 79200000)
{
user.money += 25;
user.lastReceived = now;
Storage.save();
$.channel.send("Here's your daily 25 credits.", getMoneyEmbed($.author));
}
else
$.channel.send(`It's too soon to pick up your daily credits. You have about ${((user.lastReceived + 79200000 - now) / 3600000).toFixed(1)} hours to go.`);
}
}),
send: new Command({
description: "Send money to someone.",
usage: "<user> <amount>",
run: "Who are you sending this money to?",
user: new Command({
run: "You need to enter an amount you're sending!",
number: new Command({
async run($: CommonLibrary): Promise<any>
{
const amount = Math.floor($.args[1]);
const author = $.author;
const sender = Storage.getUser(author.id);
const target = $.args[0];
const receiver = Storage.getUser(target.id);
if(amount <= 0)
return $.channel.send("You must send at least one credit!");
else if(sender.money < amount)
return $.channel.send("You don't have enough money to do that!", getMoneyEmbed(author));
else if(target.id === author.id)
return $.channel.send("You can't send money to yourself!");
else if(target.bot && process.argv[2] !== "dev")
return $.channel.send("You can't send money to a bot!");
sender.money -= amount;
receiver.money += amount;
Storage.save();
$.channel.send(getSendEmbed(author, target, amount));
}
})
}),
number: new Command({
run: "You must use the format `money send <user> <amount>`!"
}),
any: new Command({
async run($: CommonLibrary): Promise<any>
{
const last = $.args.pop();
if(!/\d+/g.test(last) && $.args.length === 0)
return $.channel.send("You need to enter an amount you're sending!");
const amount = Math.floor(last);
const author = $.author;
const sender = Storage.getUser(author.id);
if(amount <= 0)
return $.channel.send("You must send at least one credit!");
else if(sender.money < amount)
return $.channel.send("You don't have enough money to do that!", getMoneyEmbed(author));
else if(!$.guild)
return $.channel.send("You have to use this in a server if you want to send money with a username!");
const username = $.args.join(" ");
const member = (await $.guild.members.fetch({
query: username,
limit: 1
})).first();
if(!member)
return $.channel.send(`Couldn't find a user by the name of \`${username}\`! If you want to send money to someone in a different server, you have to use their user ID!`);
else if(member.user.id === author.id)
return $.channel.send("You can't send money to yourself!");
else if(member.user.bot && process.argv[2] !== "dev")
return $.channel.send("You can't send money to a bot!");
const target = member.user;
$.prompt(await $.channel.send(`Are you sure you want to send ${$(amount).pluralise("credit", "s")} to this person?\n*(This message will automatically be deleted after 10 seconds.)*`, {embed: {
color: "#ffff00",
author:
{
name: `${target.username}#${target.discriminator}`,
icon_url: target.displayAvatarURL({
format: "png",
dynamic: true
})
}
}}), $.author.id, () => {
const receiver = Storage.getUser(target.id);
sender.money -= amount;
receiver.money += amount;
Storage.save();
$.channel.send(getSendEmbed(author, target, amount));
});
}
})
}),
leaderboard: new Command({
description: "See the richest players tracked by this bot (across servers).",
async run($: CommonLibrary): Promise<any>
{
const users = Storage.users;
const ids = Object.keys(users);
ids.sort((a, b) => users[b].money - users[a].money);
const fields = [];
for(let i = 0, limit = Math.min(10, ids.length); i < limit; i++)
{
const id = ids[i];
const user = await $.client.users.fetch(id);
fields.push({
name: `#${i+1}. ${user.username}#${user.discriminator}`,
value: $(users[id].money).pluralise("credit", "s")
});
}
$.channel.send({embed: {
title: "Top 10 Richest Players",
color: "#ffff00",
fields: fields
}});
}
})
},
user: new Command({
description: "See how much money someone else has by using their user ID or pinging them.",
async run($: CommonLibrary): Promise<any>
{
$.channel.send(getMoneyEmbed($.args[0]));
}
}),
any: new Command({
description: "See how much money someone else has by using their username.",
async run($: CommonLibrary): Promise<any>
{
if($.guild)
{
const username = $.args.join(" ");
const member = (await $.guild.members.fetch({
query: username,
limit: 1
})).first();
if(member)
$.channel.send(getMoneyEmbed(member.user));
else
$.channel.send(`Couldn't find a user by the name of \`${username}\`!`);
}
}
})
});

145
src/core/command.ts Normal file
View File

@ -0,0 +1,145 @@
import {isType, parseVars, CommonLibrary} from "./lib";
// Permission levels starting from zero then increasing, allowing for numerical comparisons.
// Note: For my bot, there really isn't much purpose to doing so, as it's just one command. And plus, if you're doing stuff like moderation commands, it's probably better to make a permissions system that allows for you to separate permissions into different trees. After all, it'd be a really bad idea to allow a bot mechanic to ban users.
//enum PERMISSIONS {NONE, ADMIN, MECHANIC}
interface CommandOptions
{
description?: string;
endpoint?: boolean;
usage?: string;
//permissions?: number;
run?: Function|string;
subcommands?: {[key: string]: Command};
user?: Command;
number?: Command;
any?: Command;
}
export default class Command
{
public readonly description: string;
public readonly endpoint: boolean;
public readonly usage: string;
//public readonly permissions: number;
private run: Function|string;
public subcommands: {[key: string]: Command}|null;
public user: Command|null;
public number: Command|null;
public any: Command|null;
//public static readonly PERMISSIONS = PERMISSIONS;
[key: string]: any; // Allow for dynamic indexing. The CommandOptions interface will still prevent users from adding unused properties though.
constructor(options?: CommandOptions)
{
this.description = options?.description || "No description.";
this.endpoint = options?.endpoint || false;
this.usage = options?.usage || "";
//this.permissions = options?.permissions || Command.PERMISSIONS.NONE;
this.run = options?.run || "No action was set on this command!";
this.subcommands = options?.subcommands || null;
this.user = options?.user || null;
this.number = options?.number || null;
this.any = options?.any || null;
}
public execute($: CommonLibrary)
{
if(isType(this.run, String))
{
$.channel.send(parseVars(this.run as string, {
author: $.author.toString()
}, "???"));
}
else
(this.run as Function)($).catch($.handler.bind($));
}
/**
* Set what happens when the command is called.
* - If the command is a function, run it with one argument (the common library).
* - If the command is a string, it'll be sent as a message with %variables% replaced.
*/
public set(run: Function|string)
{
this.run = run;
}
/** The safe way to attach a named subcommand. */
public attach(key: string, command: Command)
{
if(!this.subcommands)
this.subcommands = {};
this.subcommands[key] = command;
}
/** See if a subcommand exists for the command. */
/*public has(type: string): boolean
{
return this.subcommands && (type in this.subcommands) || false;
}*/
/** Get the requested subcommand if it exists. */
/*public get(type: string): Command|null
{
return this.subcommands && this.subcommands[type] || null;
}*/
}
/*export function hasPermission(member: GuildMember, permission: number): boolean
{
const length = Object.keys(PERMISSIONS).length / 2;
console.log(member, permission, length);
return true;
}*/
// The template should be built with a reductionist mentality.
// Provide everything the user needs and then let them remove whatever they want.
// That way, they aren't focusing on what's missing, but rather what they need for their command.
export const template =
`import Command from '../core/command';
import {CommonLibrary} from '../core/lib';
export default new Command({
description: "This is a template/testing command providing common functionality. Remove what you don't need, and rename/delete this file to generate a fresh command file here. This command should be automatically excluded from the help command. The \\"usage\\" parameter (string) overrides the default usage for the help command. The \\"endpoint\\" parameter (boolean) prevents further arguments from being passed. Also, as long as you keep the run function async, it'll return a promise allowing the program to automatically catch any synchronous errors. However, you'll have to do manual error handling if you go the then and catch route.",
endpoint: false,
usage: '',
async run($: CommonLibrary): Promise<any> {
},
subcommands: {
layer: new Command({
description: "This is a named subcommand, meaning that the key name is what determines the keyword to use. With default settings for example, \\"$test layer\\".",
endpoint: false,
usage: '',
async run($: CommonLibrary): Promise<any> {
}
})
},
user: new Command({
description: "This is the subcommand for getting users by pinging them or copying their ID. With default settings for example, \\"$test 237359961842253835\\". The argument will be a user object and won't run if no user is found by that ID.",
endpoint: false,
usage: '',
async run($: CommonLibrary): Promise<any> {
}
}),
number: new Command({
description: "This is a numeric subcommand, meaning that any type of number (excluding Infinity/NaN) will route to this command if present. With default settings for example, \\"$test -5.2\\". The argument with the number is already parsed so you can just use it without converting it.",
endpoint: false,
usage: '',
async run($: CommonLibrary): Promise<any> {
}
}),
any: new Command({
description: "This is a generic subcommand, meaning that if there isn't a more specific subcommand that's called, it falls to this. With default settings for example, \\"$test reeee\\".",
endpoint: false,
usage: '',
async run($: CommonLibrary): Promise<any> {
}
})
});`;

374
src/core/lib.ts Normal file
View File

@ -0,0 +1,374 @@
import {GenericWrapper, NumberWrapper, ArrayWrapper} from "./wrappers";
import {Client, Message, TextChannel, DMChannel, NewsChannel, Guild, User, GuildMember, MessageReaction, PartialUser} from "discord.js";
import chalk from "chalk";
import FileManager from "./storage";
/** A type that describes what the library module does. */
export interface CommonLibrary
{
// Wrapper Object //
/** Wraps the value you enter with an object that provides extra functionality and provides common utility functions. */
(value: number): NumberWrapper;
<T>(value: T[]): ArrayWrapper<T>;
<T>(value: T): GenericWrapper<T>;
// Common Library Functions //
/** <Promise>.catch($.handler.bind($)) or <Promise>.catch(error => $.handler(error)) */
handler: (error: Error) => void;
log: (...args: any[]) => void;
warn: (...args: any[]) => void;
error: (...args: any[]) => void;
debug: (...args: any[]) => void;
ready: (...args: any[]) => void;
paginate: (message: Message, senderID: string, total: number, callback: (page: number) => void, duration?: number) => void;
prompt: (message: Message, senderID: string, onConfirm: () => void, duration?: number) => void;
// Dynamic Properties //
args: any[];
client: Client;
message: Message;
channel: TextChannel|DMChannel|NewsChannel;
guild: Guild|null;
author: User;
member: GuildMember|null;
}
export default function $(value: number): NumberWrapper;
export default function $<T>(value: T[]): ArrayWrapper<T>;
export default function $<T>(value: T): GenericWrapper<T>;
export default function $(value: any)
{
if(isType(value, Number))
return new NumberWrapper(value);
else if(isType(value, Array))
return new ArrayWrapper(value);
else
return new GenericWrapper(value);
}
// If you use promises, use this function to display the error in chat.
// Case #1: await $.channel.send(""); --> Automatically caught by Command.execute().
// Case #2: $.channel.send("").catch($.handler.bind($)); --> Manually caught by the user.
$.handler = function(this: CommonLibrary, error: Error)
{
if(this)
this.channel.send(`There was an error while trying to execute that command!\`\`\`${error.stack ?? error}\`\`\``);
else
$.warn("No context was attached to $.handler! Make sure to use .catch($.handler.bind($)) or .catch(error => $.handler(error)) instead!");
$.error(error);
};
// Logs with different levels of verbosity.
export const logs: {[type: string]: string} = {
error: "",
warn: "",
info: "",
verbose: ""
};
// The custom console. In order of verbosity, error, warn, log, and debug. Ready is a variation of log.
// General Purpose Logger
$.log = (...args: any[]) => {
console.log(chalk.white.bgGray(formatTimestamp()), chalk.black.bgWhite("INFO"), ...args);
const text = `[${formatUTCTimestamp()}] [INFO] ${args.join(" ")}\n`;
logs.info += text;
logs.verbose += text;
};
// "It'll still work, but you should really check up on this."
$.warn = (...args: any[]) => {
console.warn(chalk.white.bgGray(formatTimestamp()), chalk.black.bgYellow("WARN"), ...args);
const text = `[${formatUTCTimestamp()}] [WARN] ${args.join(" ")}\n`;
logs.warn += text;
logs.info += text;
logs.verbose += text;
};
// Used for anything which prevents the program from actually running.
$.error = (...args: any[]) => {
console.error(chalk.white.bgGray(formatTimestamp()), chalk.white.bgRed("ERROR"), ...args);
const text = `[${formatUTCTimestamp()}] [ERROR] ${args.join(" ")}\n`;
logs.error += text;
logs.warn += text;
logs.info += text;
logs.verbose += text;
};
// Be as verbose as possible. If anything might help when debugging an error, then include it. This only shows in your console if you run this with "dev", but you can still get it from "logs.verbose".
$.debug = (...args: any[]) => {
if(process.argv[2] === "dev")
console.debug(chalk.white.bgGray(formatTimestamp()), chalk.white.bgBlue("DEBUG"), ...args);
const text = `[${formatUTCTimestamp()}] [DEBUG] ${args.join(" ")}\n`;
logs.verbose += text;
};
// Used once at the start of the program when the bot loads.
$.ready = (...args: any[]) => {
console.log(chalk.white.bgGray(formatTimestamp()), chalk.black.bgGreen("READY"), ...args);
const text = `[${formatUTCTimestamp()}] [READY] ${args.join(" ")}\n`;
logs.info += text;
logs.verbose += text;
};
export function formatTimestamp(now = new Date())
{
const year = now.getFullYear();
const month = (now.getMonth() + 1).toString().padStart(2, '0');
const day = now.getDate().toString().padStart(2, '0');
const hour = now.getHours().toString().padStart(2, '0');
const minute = now.getMinutes().toString().padStart(2, '0');
const second = now.getSeconds().toString().padStart(2, '0');
return `${year}-${month}-${day} ${hour}:${minute}:${second}`;
}
export function formatUTCTimestamp(now = new Date())
{
const year = now.getUTCFullYear();
const month = (now.getUTCMonth() + 1).toString().padStart(2, '0');
const day = now.getUTCDate().toString().padStart(2, '0');
const hour = now.getUTCHours().toString().padStart(2, '0');
const minute = now.getUTCMinutes().toString().padStart(2, '0');
const second = now.getUTCSeconds().toString().padStart(2, '0');
return `${year}-${month}-${day} ${hour}:${minute}:${second}`;
}
// A list of message ID and callback pairs. You get the emote name and ID of the user reacting.
const eventListeners: Map<string, (emote: string, id: string) => void> = new Map();
// Pagination function that allows for customization via a callback.
// Define your own pages outside the function because this only manages the actual turning of pages.
$.paginate = async(message: Message, senderID: string, total: number, callback: (page: number) => void, duration = 60000) => {
let page = 0;
const turn = (amount: number) => {
page += amount;
if(page < 0)
page += total;
else if(page >= total)
page -= total;
callback(page);
}
const handle = (emote: string, reacterID: string) => {
if(reacterID === senderID)
{
switch(emote)
{
case '⬅️': turn(-1); break;
case '➡️': turn(1); break;
}
}
};
// Listen for reactions and call the handler.
await message.react('⬅️');
await message.react('➡️');
eventListeners.set(message.id, handle);
await message.awaitReactions((reaction, user) => {
handle(reaction.emoji.name, user.id);
return false;
}, {time: duration});
// When time's up, remove the bot's own reactions.
eventListeners.delete(message.id);
message.reactions.cache.get('⬅️')?.users.remove(message.author);
message.reactions.cache.get('➡️')?.users.remove(message.author);
};
// Attached to the client, there can be one event listener attached to a message ID which is executed if present.
export function unreact(reaction: MessageReaction, user: User|PartialUser)
{
const callback = eventListeners.get(reaction.message.id);
callback && callback(reaction.emoji.name, user.id);
}
// Waits for the sender to either confirm an action or let it pass (and delete the message).
$.prompt = async(message: Message, senderID: string, onConfirm: () => void, duration = 10000) => {
let isDeleted = false;
message.react('✅');
await message.awaitReactions((reaction, user) => {
if(user.id === senderID)
{
if(reaction.emoji.name === '✅')
onConfirm();
isDeleted = true;
message.delete();
}
// CollectorFilter requires a boolean to be returned.
// My guess is that the return value of awaitReactions can be altered by making a boolean filter.
// However, because that's not my concern with this command, I don't have to worry about it.
// May as well just set it to false because I'm not concerned with collecting any reactions.
return false;
}, {time: duration});
if(!isDeleted)
message.delete();
};
/**
* Splits a command by spaces while accounting for quotes which capture string arguments.
* - `\"` = `"`
* - `\\` = `\`
*/
export function parseArgs(line: string): string[]
{
let result = [];
let selection = "";
let inString = false;
let isEscaped = false;
for(let c of line)
{
if(isEscaped)
{
if(['"', '\\'].includes(c))
selection += c;
else
selection += '\\' + c;
isEscaped = false;
}
else if(c === '\\')
isEscaped = true;
else if(c === '"')
inString = !inString;
else if(c === ' ' && !inString)
{
result.push(selection);
selection = "";
}
else
selection += c;
}
if(selection.length > 0)
result.push(selection)
return result;
}
/**
* Allows you to store a template string with variable markers and parse it later.
* - Use `%name%` for variables
* - `%%` = `%`
* - If the invalid token is null/undefined, nothing is changed.
*/
export function parseVars(line: string, definitions: {[key: string]: string}, invalid: string|null|undefined = ""): string
{
let result = "";
let inVariable = false;
let token = "";
for(const c of line)
{
if(c === '%')
{
if(inVariable)
{
if(token === "")
result += '%';
else
{
if(token in definitions)
result += definitions[token];
else if(invalid === undefined || invalid === null)
result += `%${token}%`;
else
result += invalid;
token = "";
}
}
inVariable = !inVariable;
}
else if(inVariable)
token += c;
else
result += c;
}
return result;
}
/**
* Split up an array into a specified length.
* [1,2,3,4,5,6,7,8,9,10] split by 3 = [[1,2,3],[4,5,6],[7,8,9],[10]]
*/
export function perforate<T>(list: T[], lengthOfEachSection: number): T[][]
{
const sections: T[][] = [];
const amountOfSections = Math.ceil(list.length / lengthOfEachSection);
for(let index = 0; index < amountOfSections; index++)
sections.push(list.slice(index * lengthOfEachSection, (index + 1) * lengthOfEachSection));
return sections;
}
export function isType(value: any, type: Function): boolean
{
if(value === undefined && type === undefined)
return true;
else if(value === null && type === null)
return true;
else
return value !== undefined && value !== null && value.constructor === type;
}
/**
* Checks a value to see if it matches the fallback's type, otherwise returns the fallback.
* For the purposes of the templates system, this function will only check array types, objects should be checked under their own type (as you'd do anyway with something like a User object).
* If at any point the value doesn't match the data structure provided, the fallback is returned.
* Warning: Type checking is based on the fallback's type. Be sure that the "type" parameter is accurate to this!
*/
export function select<T>(value: any, fallback: T, type: Function, isArray = false): T
{
if(isArray && isType(value, Array))
{
for(let item of value)
if(!isType(item, type))
return fallback;
return value;
}
else
{
if(isType(value, type))
return value;
else
return fallback;
}
}
export interface GenericJSON
{
[key: string]: any;
}
export abstract class GenericStructure
{
protected __meta__ = "generic";
constructor(tag?: string)
{
this.__meta__ = tag || this.__meta__;
}
public save(asynchronous = true)
{
const tag = this.__meta__;
delete this.__meta__;
FileManager.write(tag, this, asynchronous);
this.__meta__ = tag;
}
}
// A 50% chance would be "Math.random() < 0.5" because Math.random() can be [0, 1), so to make two equal ranges, you'd need [0, 0.5)U[0.5, 1).
// Similar logic would follow for any other percentage. Math.random() < 1 is always true (100% chance) and Math.random() < 0 is always false (0% chance).
export const Random = {
num: (min: number, max: number) => (Math.random() * (max - min)) + min,
int: (min: number, max: number) => Math.floor(Random.num(min, max)),
chance: (decimal: number) => Math.random() < decimal,
sign: (number = 1) => number * (Random.chance(0.5) ? -1 : 1),
deviation: (base: number, deviation: number) => Random.num(base - deviation, base + deviation)
};

109
src/core/storage.ts Normal file
View File

@ -0,0 +1,109 @@
import fs from "fs";
import $ from "./lib";
import {Collection} from "discord.js";
import Command, {template} from "../core/command";
let commands: Collection<string, Command>|null = null;
const Storage = {
read(header: string): object
{
this.open("data");
const path = `data/${header}.json`;
let data = {};
if(fs.existsSync(path))
{
const file = fs.readFileSync(path, "utf-8");
try
{
data = JSON.parse(file);
}
catch(error)
{
if(process.argv[2] !== "dev")
{
$.warn(`Malformed JSON data (header: ${header}), backing it up.`, file);
fs.writeFile(`${path}.backup`, file, generateHandler(`Backup file of "${header}" successfully written as ${file}.`));
}
}
}
return data;
},
write(header: string, data: object, asynchronous = true)
{
this.open("data");
const path = `data/${header}.json`;
if(process.argv[2] === "dev" || header === "config")
{
const result = JSON.stringify(data, null, '\t');
if(asynchronous)
fs.writeFile(path, result, generateHandler(`"${header}" sucessfully spaced and written.`));
else
fs.writeFileSync(path, result);
}
else
{
const result = JSON.stringify(data);
if(asynchronous)
fs.writeFile(path, result, generateHandler(`"${header}" sucessfully written.`));
else
fs.writeFileSync(path, result);
}
},
open(path: string, filter?: (value: string, index: number, array: string[]) => unknown): string[]
{
if(!fs.existsSync(path))
fs.mkdirSync(path);
let directory = fs.readdirSync(path);
if(filter)
directory = directory.filter(filter);
return directory;
},
close(path: string)
{
if(fs.existsSync(path) && fs.readdirSync(path).length === 0)
fs.rmdir(path, generateHandler(`"${path}" successfully closed.`));
},
/** Returns the cache of the commands if it exists and searches the directory if not. */
async loadCommands(): Promise<Collection<string, Command>>
{
if(commands)
return commands;
if(process.argv[2] === "dev" && !fs.existsSync("src/commands/test.ts"))
fs.writeFile("src/commands/test.ts", template, generateHandler('"test.ts" (testing/template command) successfully generated.'));
commands = new Collection();
for(const file of Storage.open("dist/commands", (filename: string) => filename.endsWith(".js")))
{
const header = file.substring(0, file.indexOf(".js"));
const command = (await import(`../commands/${header}`)).default;
commands.set(header, command);
$.log("Loading Command:", header);
}
return commands;
}
};
function generateHandler(message: string)
{
return (error: Error|null) => {
if(error)
$.error(error);
else
$.debug(message);
};
};
export default Storage;

113
src/core/structures.ts Normal file
View File

@ -0,0 +1,113 @@
import FileManager from "./storage";
import $, {select, GenericJSON, GenericStructure} from "./lib";
import {watch} from "fs";
class ConfigStructure extends GenericStructure
{
public token: string;
public prefix: string;
public mechanics: string[];
constructor(data: GenericJSON)
{
super("config");
this.token = select(data.token, "<ENTER YOUR TOKEN HERE>", String);
this.prefix = select(data.prefix, "$", String);
this.mechanics = select(data.mechanics, [], String, true);
}
}
class User
{
public money: number;
public lastReceived: number;
constructor(data?: GenericJSON)
{
this.money = select(data?.money, 0, Number);
this.lastReceived = select(data?.lastReceived, -1, Number);
}
}
class Guild
{
public prefix: string|null;
constructor(data?: GenericJSON)
{
this.prefix = select(data?.prefix, null, String);
}
}
class StorageStructure extends GenericStructure
{
public users: {[id: string]: User};
public guilds: {[id: string]: Guild};
constructor(data: GenericJSON)
{
super("storage");
this.users = {};
this.guilds = {};
for(let id in data.users)
if(/\d{17,19}/g.test(id))
this.users[id] = new User(data.users[id]);
for(let id in data.guilds)
if(/\d{17,19}/g.test(id))
this.guilds[id] = new Guild(data.guilds[id]);
}
/** Gets a user's profile if they exist and generate one if not. */
public getUser(id: string): User
{
if(!/\d{17,19}/g.test(id))
$.warn(`"${id}" is not a valid user ID! It will be erased when the data loads again.`);
if(id in this.users)
return this.users[id];
else
{
const user = new User();
this.users[id] = user;
return user;
}
}
/** Gets a guild's settings if they exist and generate one if not. */
public getGuild(id: string): Guild
{
if(!/\d{17,19}/g.test(id))
$.warn(`"${id}" is not a valid guild ID! It will be erased when the data loads again.`);
if(id in this.guilds)
return this.guilds[id];
else
{
const guild = new Guild();
this.guilds[id] = guild;
return guild;
}
}
}
// Exports instances. Don't worry, importing it from different files will load the same instance.
export let Config = new ConfigStructure(FileManager.read("config"));
export let Storage = new StorageStructure(FileManager.read("storage"));
// This part will allow the user to manually edit any JSON files they want while the program is running which'll update the program's cache.
// However, fs.watch is a buggy mess that should be avoided in production. While it helps test out stuff for development, it's not a good idea to have it running outside of development as it causes all sorts of issues.
if(process.argv[2] === "dev")
{
watch("data", (event, filename) => {
$.debug("File Watcher:", event, filename);
const header = filename.substring(0, filename.indexOf(".json"));
switch(header)
{
case "config": Config = new ConfigStructure(FileManager.read("config")); break;
case "storage": Storage = new StorageStructure(FileManager.read("storage")); break;
}
});
}

51
src/core/wrappers.ts Normal file
View File

@ -0,0 +1,51 @@
export class GenericWrapper<T>
{
protected readonly value: T;
public constructor(value: T)
{
this.value = value;
}
}
export class NumberWrapper extends GenericWrapper<number>
{
/**
* Pluralises a word and chooses a suffix attached to the root provided.
* - pluralise("credit", "s") = credit/credits
* - pluralise("part", "ies", "y") = party/parties
* - pluralise("sheep") = sheep
*/
public pluralise(word: string, plural = "", singular = "", excludeNumber = false): string
{
let result = excludeNumber ? "" : `${this.value} `;
if(this.value === 1)
result += word + singular;
else
result += word + plural;
return result;
}
/**
* Pluralises a word for changes.
* - (-1).pluraliseSigned() = '-1 credits'
* - (0).pluraliseSigned() = '+0 credits'
* - (1).pluraliseSigned() = '+1 credit'
*/
public pluraliseSigned(word: string, plural = "", singular = "", excludeNumber = false): string
{
const sign = this.value >= 0 ? '+' : '';
return `${sign}${this.pluralise(word, plural, singular, excludeNumber)}`;
}
}
export class ArrayWrapper<T> extends GenericWrapper<T[]>
{
/** Returns a random element from this array. */
public random(): T
{
return this.value[Math.floor(Math.random() * this.value.length)];
}
}

113
src/index.ts Normal file
View File

@ -0,0 +1,113 @@
import {Client, Permissions} from "discord.js";
import $, {unreact} from "./core/lib";
import setup from "./setup";
import FileManager from "./core/storage";
import {Config, Storage} from "./core/structures";
(async() => {
// Setup //
await setup.init();
const client = new Client();
const commands = await FileManager.loadCommands();
client.login(Config.token).catch(setup.again);
client.on("message", async message => {
// Message Setup //
if(message.author.bot)
return;
const prefix = Storage.getGuild(message.guild?.id || "N/A").prefix || Config.prefix;
if(!message.content.startsWith(prefix))
return;
const [header, ...args] = message.content.substring(prefix.length).split(/ +/);
if(!commands.has(header))
return;
if(message.channel.type === "text" && !message.channel.permissionsFor(client.user || "")?.has(Permissions.FLAGS.SEND_MESSAGES))
{
let status;
if(message.member?.hasPermission(Permissions.FLAGS.ADMINISTRATOR))
status = "Because you're a server admin, you have the ability to change that channel's permissions to match if that's what you intended.";
else
status = "Try using a different channel or contacting a server admin to change permissions of that channel if you think something's wrong.";
return message.author.send(`I don't have permission to send messages in ${message.channel.toString()}. ${status}`);
}
$.log(`${message.author.username}#${message.author.discriminator} executed the command "${header}" with arguments "${args}".`);
// Subcommand Recursion //
let command = commands.get(header);
if(!command) return $.warn(`Command "${header}" was called but for some reason it's still undefined!`);
const params: any[] = [];
let isEndpoint = false;
for(let param of args)
{
if(command.endpoint)
{
if(command.subcommands || command.user || command.number || command.any)
$.warn(`An endpoint cannot have subcommands! Check ${prefix}${header} again.`);
isEndpoint = true;
break;
}
if(command.subcommands?.[param])
command = command.subcommands[param];
// Any Discord ID format will automatically format to a user ID.
else if(command.user && (/\d{17,19}/.test(param)))
{
const id = param.match(/\d+/g)![0];
command = command.user;
try {params.push(await client.users.fetch(id))}
catch(error) {return message.channel.send(`No user found by the ID \`${id}\`!`)}
}
// Disallow infinity and allow for 0.
else if(command.number && (Number(param) || param === "0") && !param.includes("Infinity"))
{
command = command.number;
params.push(Number(param));
}
else if(command.any)
{
command = command.any;
params.push(param);
}
else
params.push(param);
}
if(isEndpoint)
return message.channel.send("Too many arguments!");
// Execute with dynamic library attached. //
// The purpose of using $.bind($) is to clone the function so as to not modify the original $.
// The cloned function doesn't copy the properties, so Object.assign() is used.
// Object.assign() modifies the first element and returns that, the second element applies its properties and the third element applies its own overriding the second one.
command.execute(Object.assign($.bind($), {
args: params,
author: message.author,
channel: message.channel,
client: client,
guild: message.guild,
member: message.member,
message: message
}, $));
});
client.once("ready", () => {
if(client.user)
{
$.ready(`Logged in as ${client.user.username}#${client.user.discriminator}.`);
client.user.setActivity({
type: "LISTENING",
name: `${Config.prefix}help`
});
}
});
client.on("messageReactionRemove", unreact);
})()

48
src/setup.ts Normal file
View File

@ -0,0 +1,48 @@
import {existsSync as exists} from "fs";
import inquirer from "inquirer";
import Storage from "./core/storage";
import {Config} from "./core/structures";
import $ from "./core/lib";
// This file is called (or at least should be called) automatically as long as a config file doesn't exist yet.
// And that file won't be written until the data is successfully initialized.
const prompts = [{
type: "password",
name: "token",
message: "What's your bot's token?",
mask: true
}, {
type: "input",
name: "prefix",
message: "What do you want your bot's prefix to be?",
default: "$"
}, {
type: "input",
name: "mechanics",
message: "Enter a list of bot mechanics (by their IDs) separated by spaces."
}];
export default {
async init()
{
while(!exists("data/config.json"))
{
const answers = await inquirer.prompt(prompts);
Storage.open("data");
Config.token = answers.token as string;
Config.prefix = answers.prefix as string;
const mechanics = (answers.mechanics as string);
Config.mechanics = mechanics !== "" ? mechanics.split(" ") : [];
Config.save();
}
},
/** Prompt the user to set their token again. */
async again()
{
$.error("It seems that the token you provided is invalid.");
const answers = await inquirer.prompt(prompts.slice(0, 1));
Config.token = answers.token as string;
Config.save(false);
process.exit();
}
};

17
tsconfig.json Normal file
View File

@ -0,0 +1,17 @@
{
"compilerOptions":
{
"rootDir": "src",
"outDir": "dist",
"target": "ES6",
"module": "CommonJS",
"moduleResolution": "node",
"esModuleInterop": true,
"noImplicitAny": true,
"noImplicitReturns": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"strictPropertyInitialization": true,
"removeComments": true
}
}