Added husky.

This commit is contained in:
Keanu Timmermans 2020-10-15 09:23:24 +00:00 committed by GitHub
parent 57433cc594
commit 74b4d4272c
36 changed files with 2677 additions and 2226 deletions

View file

@ -1,4 +1,4 @@
name: "CodeQL Config" name: 'CodeQL Config'
queries: queries:
- uses: security-and-quality - uses: security-and-quality

View file

@ -1,4 +1,4 @@
name: "CodeQL" name: 'CodeQL'
on: on:
push: push:
@ -14,28 +14,28 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v2 uses: actions/checkout@v2
with: with:
fetch-depth: 2 fetch-depth: 2
- run: git checkout HEAD^2 - run: git checkout HEAD^2
if: ${{ github.event_name == 'pull_request' }} if: ${{ github.event_name == 'pull_request' }}
- name: Setup Node.JS - name: Setup Node.JS
uses: actions/setup-node@v2-beta uses: actions/setup-node@v2-beta
with: with:
node-version: '12' node-version: '12'
- run: npm ci - run: npm ci
- name: Build codebase - name: Build codebase
run: npm run build run: npm run build
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@v1 uses: github/codeql-action/init@v1
with: with:
config-file: ./.github/codeql/codeql-config.yml config-file: ./.github/codeql/codeql-config.yml
languages: javascript languages: javascript
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1 uses: github/codeql-action/analyze@v1

View file

@ -1,7 +1,7 @@
name: Build Docker Image + Push name: Build Docker Image + Push
on: on:
push: push:
branches: branches:
- typescript - typescript
- docker - docker

1
.husky/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
_

4
.husky/pre-commit Executable file
View file

@ -0,0 +1,4 @@
#!/bin/sh
. "$(dirname $0)/_/husky.sh"
npm test && npx prettier --write .

71
.prettierignore Normal file
View file

@ -0,0 +1,71 @@
# Specific to this repository
dist/
data/
docs/
tmp/
test*
!test/
*.bat
desktop.ini
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# nyc test coverage
.nyc_output
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Typescript v1 declaration files
typings/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
config.json
.vscode/

View file

@ -13,7 +13,7 @@ Thank you for coming on this journey with me, but it is time to put the big chan
Special thanks to: Special thanks to:
- Lexi Sother (TravBot v2, structural overhaul. Reviewing PRs.) - Lexi Sother (TravBot v2, structural overhaul. Reviewing PRs.)
- WatDuhHekBro (a *lot* of contributions to TravBot v2) - WatDuhHekBro (a _lot_ of contributions to TravBot v2)
- Zeehondie (Ideas for various commands.) - Zeehondie (Ideas for various commands.)
### License ### License

View file

@ -1,128 +1,158 @@
# What this is # What this is
This is a user-friendly version of the project's structure (compared to the amalgamation that has become [specifications](Specifications.md) which is just a list of design decisions and isn't actually helpful at all for understanding the code). This will follow the line of logic that the program would run through. This is a user-friendly version of the project's structure (compared to the amalgamation that has become [specifications](Specifications.md) which is just a list of design decisions and isn't actually helpful at all for understanding the code). This will follow the line of logic that the program would run through.
# Building/Setup # Building/Setup
- `npm run dev` runs the TypeScript compiler in watch mode, meaning that any changes you make to the code will automatically reload the bot. - `npm run dev` runs the TypeScript compiler in watch mode, meaning that any changes you make to the code will automatically reload the bot.
- This will take all the files in `src` (where all the code is) and compile it into `dist` which is what the program actually uses. - This will take all the files in `src` (where all the code is) and compile it into `dist` which is what the program actually uses.
- If there's a runtime error, `dist\commands\test.js:25:30` for example, then you have to into `dist` instead of `src`, then find the line that corresponds. - If there's a runtime error, `dist\commands\test.js:25:30` for example, then you have to into `dist` instead of `src`, then find the line that corresponds.
# Launching # Launching
When you start the program, it'll run the code in `index` (meaning both `src/index.ts` and `dist/index.js`, they're the same except that `dist/<...>.js` is compiled). The code in `index` will call `setup` and check if `data/config.json` exists, prompting you if it doesn't. It'll then run initialization code. When you start the program, it'll run the code in `index` (meaning both `src/index.ts` and `dist/index.js`, they're the same except that `dist/<...>.js` is compiled). The code in `index` will call `setup` and check if `data/config.json` exists, prompting you if it doesn't. It'll then run initialization code.
# Structure # Structure
- `commands` contains all the commands. - `commands` contains all the commands.
- `defs` contains static definitions. - `defs` contains static definitions.
- `core` contains the foundation of the program. You won't need to worry about this unless you're modifying pre-existing behavior of the `Command` class for example or add a function to the library. - `core` contains the foundation of the program. You won't need to worry about this unless you're modifying pre-existing behavior of the `Command` class for example or add a function to the library.
- `events` contains all the events. Again, you generally won't need to touch this unless you're listening for a new Discord event. - `events` contains all the events. Again, you generally won't need to touch this unless you're listening for a new Discord event.
# The Command Class # The Command Class
A valid command file must be located in `commands` and export a default `Command` instance. Assume that we're working with `commands/money.ts`. A valid command file must be located in `commands` and export a default `Command` instance. Assume that we're working with `commands/money.ts`.
```js ```js
import Command from '../core/command'; import Command from '../core/command';
export default new Command({ export default new Command({
//... //...
}); });
``` ```
The `run` property can be either a function or a string. If it's a function, you get one parameter, `$` which represents the common library (see below). If it's a string, it's a variable string. The `run` property can be either a function or a string. If it's a function, you get one parameter, `$` which represents the common library (see below). If it's a string, it's a variable string.
- `%author%` pings the person who sent the message. - `%author%` pings the person who sent the message.
- `%prefix%` gets the bot's current prefix in the selected server. - `%prefix%` gets the bot's current prefix in the selected server.
```js ```js
import Command from '../core/command'; import Command from '../core/command';
import {CommonLibrary} from '../core/lib'; import { CommonLibrary } from '../core/lib';
export default new Command({ export default new Command({
run: "%author%, make sure to use the prefix! (%prefix)" run: '%author%, make sure to use the prefix! (%prefix)',
}); });
``` ```
...is equal to... ...is equal to...
```js ```js
import Command from '../core/command'; import Command from '../core/command';
import {CommonLibrary} from '../core/lib'; import { CommonLibrary } from '../core/lib';
import {getPrefix} from "../core/structures"; import { getPrefix } from '../core/structures';
export default new Command({ export default new Command({
async run($: CommonLibrary): Promise<any> { async run($: CommonLibrary): Promise<any> {
$.channel.send(`${$.author.toString()}, make sure to use the prefix! (${getPrefix($.guild)})`); $.channel.send(
} `${$.author.toString()}, make sure to use the prefix! (${getPrefix(
$.guild,
)})`,
);
},
}); });
``` ```
Now here's where it gets fun. The `Command` class is a recursive structure, containing other `Command` instances as properties. Now here's where it gets fun. The `Command` class is a recursive structure, containing other `Command` instances as properties.
- `subcommands` is used for specific keywords for accessing a certain command. For example, `$eco pay` has a subcommand of `pay`. - `subcommands` is used for specific keywords for accessing a certain command. For example, `$eco pay` has a subcommand of `pay`.
```js ```js
import Command from '../core/command'; import Command from '../core/command';
import {CommonLibrary} from '../core/lib'; import { CommonLibrary } from '../core/lib';
export default new Command({ export default new Command({
subcommands: { subcommands: {
pay: new Command({ pay: new Command({
//... //...
}) }),
} },
}); });
``` ```
There's also `user` which listens for a ping or a Discord ID, `<@237359961842253835>` and `237359961842253835` respectively. The argument will be a `User` object. There's also `user` which listens for a ping or a Discord ID, `<@237359961842253835>` and `237359961842253835` respectively. The argument will be a `User` object.
```js ```js
import Command from '../core/command'; import Command from '../core/command';
import {CommonLibrary} from '../core/lib'; import { CommonLibrary } from '../core/lib';
export default new Command({ export default new Command({
user: new Command({ user: new Command({
async run($: CommonLibrary): Promise<any> { async run($: CommonLibrary): Promise<any> {
$.debug($.args[0].username); // "WatDuhHekBro" $.debug($.args[0].username); // "WatDuhHekBro"
} },
}) }),
}); });
``` ```
There's also `number` which checks for any number type except `Infinity`, converting the argument to a `number` type. There's also `number` which checks for any number type except `Infinity`, converting the argument to a `number` type.
```js ```js
import Command from '../core/command'; import Command from '../core/command';
import {CommonLibrary} from '../core/lib'; import { CommonLibrary } from '../core/lib';
export default new Command({ export default new Command({
number: new Command({ number: new Command({
async run($: CommonLibrary): Promise<any> { async run($: CommonLibrary): Promise<any> {
$.debug($.args[0] + 5); $.debug($.args[0] + 5);
} },
}) }),
}); });
``` ```
And then there's `any` which catches everything else that doesn't fall into the above categories. The argument will be a `string`. And then there's `any` which catches everything else that doesn't fall into the above categories. The argument will be a `string`.
```js ```js
import Command from '../core/command'; import Command from '../core/command';
import {CommonLibrary} from '../core/lib'; import { CommonLibrary } from '../core/lib';
export default new Command({ export default new Command({
any: new Command({ any: new Command({
async run($: CommonLibrary): Promise<any> { async run($: CommonLibrary): Promise<any> {
$.debug($.args[0].toUpperCase()); $.debug($.args[0].toUpperCase());
} },
}) }),
}); });
``` ```
Of course, maybe you just want to get string arguments regardless, and since everything is an optional property, so you'd then just include `any` and not `subcommands`, `user`, or `number`. Of course, maybe you just want to get string arguments regardless, and since everything is an optional property, so you'd then just include `any` and not `subcommands`, `user`, or `number`.
## Other Properties ## Other Properties
- `description`: The description for that specific command. - `description`: The description for that specific command.
- `endpoint`: A `boolean` determining whether or not to prevent any further arguments. For example, you could prevent `$money daily too many arguments`. - `endpoint`: A `boolean` determining whether or not to prevent any further arguments. For example, you could prevent `$money daily too many arguments`.
- `usage`: Provide a custom usage for the help menu. Do note that this is relative to the subcommand, so the below will result in `$money pay <user> <amount>`. - `usage`: Provide a custom usage for the help menu. Do note that this is relative to the subcommand, so the below will result in `$money pay <user> <amount>`.
```js ```js
import Command from '../core/command'; import Command from '../core/command';
import {CommonLibrary} from '../core/lib'; import { CommonLibrary } from '../core/lib';
export default new Command({ export default new Command({
subcommands: { subcommands: {
pay: new Command({ pay: new Command({
usage: "<user> <amount>" usage: '<user> <amount>',
}) }),
} },
}); });
``` ```
- `permission`: The permission to restrict the current command to. You can specify it for certain subcommands, useful for having `$money` be open to anyone but not `$money admin`. If it's `null` (default), the permission will inherit whatever was declared before (if any). The default value is NOT the same as `Command.PERMISSIONS.NONE`. - `permission`: The permission to restrict the current command to. You can specify it for certain subcommands, useful for having `$money` be open to anyone but not `$money admin`. If it's `null` (default), the permission will inherit whatever was declared before (if any). The default value is NOT the same as `Command.PERMISSIONS.NONE`.
- `aliases`: A list of aliases (if any). - `aliases`: A list of aliases (if any).
## Alternatives to Nesting ## Alternatives to Nesting
For a lot of the metadata properties like `description`, you must provide them when creating a new `Command` instance. However, you can freely modify and attach subcommands, useful for splitting a command into multiple files. For a lot of the metadata properties like `description`, you must provide them when creating a new `Command` instance. However, you can freely modify and attach subcommands, useful for splitting a command into multiple files.
```js ```js
import pay from "./subcommands/pay"; import pay from "./subcommands/pay";
@ -141,19 +171,24 @@ export default cmd;
``` ```
## Error Handling ## Error Handling
Any errors caused when using `await` or just regular synchronous functions will be automatically caught, you don't need to worry about those. However, promises must be caught manually. For example, `$.channel.send("")` will throw an error because you can't send empty messages to Discord, but since it's a promise, it'll just fade without throwing an error. There are two ways to do this: Any errors caused when using `await` or just regular synchronous functions will be automatically caught, you don't need to worry about those. However, promises must be caught manually. For example, `$.channel.send("")` will throw an error because you can't send empty messages to Discord, but since it's a promise, it'll just fade without throwing an error. There are two ways to do this:
- `$.channel.send("").catch($.handler.bind($))` - `$.channel.send("").catch($.handler.bind($))`
- `$.channel.send("").catch(error => $.handler(error))` - `$.channel.send("").catch(error => $.handler(error))`
# The Common Library # The Common Library
This is the container of functions available without having to import `core/lib`, usually as `$`. When accessing this from a command's `run` function, it'll also come with shortcuts to other properties. This is the container of functions available without having to import `core/lib`, usually as `$`. When accessing this from a command's `run` function, it'll also come with shortcuts to other properties.
## Custom Wrappers ## Custom Wrappers
- `$(5)` = `new NumberWrapper(5)` - `$(5)` = `new NumberWrapper(5)`
- `$("text")` = `new StringWrapper("text")` - `$("text")` = `new StringWrapper("text")`
- `$([1,2,3])` = `new ArrayWrapper([1,2,3])` - `$([1,2,3])` = `new ArrayWrapper([1,2,3])`
## Custom Logger ## Custom Logger
- `$.log(...)` - `$.log(...)`
- `$.warn(...)` - `$.warn(...)`
- `$.error(...)` - `$.error(...)`
@ -161,34 +196,43 @@ This is the container of functions available without having to import `core/lib`
- `$.ready(...)` (only meant to be used once at the start of the program) - `$.ready(...)` (only meant to be used once at the start of the program)
## Convenience Functions ## Convenience Functions
This modularizes certain patterns of code to make things easier. This modularizes certain patterns of code to make things easier.
- `$.paginate()`: Takes a message and some additional parameters and makes a reaction page with it. All the pagination logic is taken care of but nothing more, the page index is returned and you have to send a callback to do something with it. - `$.paginate()`: Takes a message and some additional parameters and makes a reaction page with it. All the pagination logic is taken care of but nothing more, the page index is returned and you have to send a callback to do something with it.
```js ```js
const pages = ["one", "two", "three"]; const pages = ['one', 'two', 'three'];
const msg = await $.channel.send(pages[0]); const msg = await $.channel.send(pages[0]);
$.paginate(msg, $.author.id, pages.length, page => { $.paginate(msg, $.author.id, pages.length, (page) => {
msg.edit(pages[page]); msg.edit(pages[page]);
}); });
``` ```
- `$.prompt()`: Prompts the user about a decision before following through. - `$.prompt()`: Prompts the user about a decision before following through.
```js ```js
const msg = await $.channel.send("Are you sure you want to delete this?"); const msg = await $.channel.send('Are you sure you want to delete this?');
$.prompt(msg, $.author.id, () => { $.prompt(msg, $.author.id, () => {
delete this; // Replace this with actual code. delete this; // Replace this with actual code.
}); });
``` ```
- `$.getMemberByUsername()`: Gets a user by their username. Gets the first one then rolls with it. - `$.getMemberByUsername()`: Gets a user by their username. Gets the first one then rolls with it.
- `$.callMemberByUsername()`: Convenience function to handle cases where someone isn't found by a username automatically. - `$.callMemberByUsername()`: Convenience function to handle cases where someone isn't found by a username automatically.
```js ```js
$.callMemberByUsername($.message, $.args.join(" "), member => { $.callMemberByUsername($.message, $.args.join(' '), (member) => {
$.channel.send(`Your nickname is ${member.nickname}.`); $.channel.send(`Your nickname is ${member.nickname}.`);
}); });
``` ```
## Dynamic Properties ## Dynamic Properties
These will be accessible only inside a `Command` and will change per message. These will be accessible only inside a `Command` and will change per message.
- `$.args`: A list of arguments in the command. It's relative to the subcommand, so if you do `$test this 5`, `5` becomes `$.args[0]` if `this` is a subcommand. Args are already converted, so a `number` subcommand would return a number rather than a string. - `$.args`: A list of arguments in the command. It's relative to the subcommand, so if you do `$test this 5`, `5` becomes `$.args[0]` if `this` is a subcommand. Args are already converted, so a `number` subcommand would return a number rather than a string.
- `$.client`: `message.client` - `$.client`: `message.client`
- `$.message`: `message` - `$.message`: `message`
@ -198,22 +242,28 @@ These will be accessible only inside a `Command` and will change per message.
- `$.member`: `message.member` - `$.member`: `message.member`
# Wrappers # Wrappers
This is similar to modifying a primitive object's `prototype` without actually doing so. This is similar to modifying a primitive object's `prototype` without actually doing so.
## NumberWrapper ## NumberWrapper
- `.pluralise()`: A substitute for not having to do `amount === 1 ? "singular" : "plural"`. For example, `$(x).pluralise("credit", "s")` will return `"1 credit"` and/or `"5 credits"` respectively. - `.pluralise()`: A substitute for not having to do `amount === 1 ? "singular" : "plural"`. For example, `$(x).pluralise("credit", "s")` will return `"1 credit"` and/or `"5 credits"` respectively.
- `.pluraliseSigned()`: This builds on `.pluralise()` and adds a sign at the beginning for marking changes/differences. `$(0).pluraliseSigned("credit", "s")` will return `"+0 credits"`. - `.pluraliseSigned()`: This builds on `.pluralise()` and adds a sign at the beginning for marking changes/differences. `$(0).pluraliseSigned("credit", "s")` will return `"+0 credits"`.
## StringWrapper ## StringWrapper
- `.replaceAll()`: A non-regex alternative to replacing everything in a string. `$("test").replaceAll('t', 'z')` = `"zesz"`. - `.replaceAll()`: A non-regex alternative to replacing everything in a string. `$("test").replaceAll('t', 'z')` = `"zesz"`.
- `.toTitleCase()`: Capitalizes the first letter of each word. `$("this is some text").toTitleCase()` = `"This Is Some Text"`. - `.toTitleCase()`: Capitalizes the first letter of each word. `$("this is some text").toTitleCase()` = `"This Is Some Text"`.
## ArrayWrapper ## ArrayWrapper
- `.random()`: Returns a random element from an array. `$([1,2,3]).random()` could be any one of those elements. - `.random()`: Returns a random element from an array. `$([1,2,3]).random()` could be any one of those elements.
- `.split()`: Splits an array into different arrays by a specified length. `$([1,2,3,4,5,6,7,8,9,10]).split(3)` = `[[1,2,3],[4,5,6],[7,8,9],[10]]`. - `.split()`: Splits an array into different arrays by a specified length. `$([1,2,3,4,5,6,7,8,9,10]).split(3)` = `[[1,2,3],[4,5,6],[7,8,9],[10]]`.
# Other Library Functions # Other Library Functions
These do have to be manually imported, which are used more on a case-by-case basis. These do have to be manually imported, which are used more on a case-by-case basis.
- `formatTimestamp()`: Formats a `Date` object into your system's time. `YYYY-MM-DD HH:MM:SS` - `formatTimestamp()`: Formats a `Date` object into your system's time. `YYYY-MM-DD HH:MM:SS`
- `formatUTCTimestamp()`: Formats a `Date` object into UTC time. `YYYY-MM-DD HH:MM:SS` - `formatUTCTimestamp()`: Formats a `Date` object into UTC time. `YYYY-MM-DD HH:MM:SS`
- `botHasPermission()`: Tests if a bot has a certain permission in a specified guild. - `botHasPermission()`: Tests if a bot has a certain permission in a specified guild.
@ -224,11 +274,13 @@ These do have to be manually imported, which are used more on a case-by-case bas
- `Random`: An object of functions containing stuff related to randomness. `Random.num` is a random decimal, `Random.int` is a random integer, `Random.chance` takes a number ranging from `0` to `1` as a percentage. `Random.sign` takes a number and has a 50-50 chance to be negative or positive. `Random.deviation` takes a number and a magnitude and produces a random number within those confines. `(5, 2)` would produce any number between `3` and `7`. - `Random`: An object of functions containing stuff related to randomness. `Random.num` is a random decimal, `Random.int` is a random integer, `Random.chance` takes a number ranging from `0` to `1` as a percentage. `Random.sign` takes a number and has a 50-50 chance to be negative or positive. `Random.deviation` takes a number and a magnitude and produces a random number within those confines. `(5, 2)` would produce any number between `3` and `7`.
# Other Core Functions # Other Core Functions
- `permissions::hasPermission()`: Checks if a `Member` has a certain permission. - `permissions::hasPermission()`: Checks if a `Member` has a certain permission.
- `permissions::getPermissionLevel()`: Gets a `Member`'s permission level according to the permissions enum defined in the file. - `permissions::getPermissionLevel()`: Gets a `Member`'s permission level according to the permissions enum defined in the file.
- `structures::getPrefix()`: Get the current prefix of the guild or the bot's prefix if none is found. - `structures::getPrefix()`: Get the current prefix of the guild or the bot's prefix if none is found.
# The other core files # The other core files
- `core/permissions`: Contains all the permission roles and checking functions. - `core/permissions`: Contains all the permission roles and checking functions.
- `core/structures`: Contains all the code handling dynamic JSON data. Has a one-to-one connection with each file generated, for example, `Config` which calls `super("config")` meaning it writes to `data/config.json`. - `core/structures`: Contains all the code handling dynamic JSON data. Has a one-to-one connection with each file generated, for example, `Config` which calls `super("config")` meaning it writes to `data/config.json`.
- `core/storage`: Handles most of the file system operations, all of the ones related to `data` at least. - `core/storage`: Handles most of the file system operations, all of the ones related to `data` at least.

View file

@ -1,9 +1,11 @@
# Getting Started # Getting Started
1. `npm install` 1. `npm install`
2. `npm run build` 2. `npm run build`
3. `npm start` 3. `npm start`
# Getting Started (Developers) # Getting Started (Developers)
1. `npm install` 1. `npm install`
2. `npm run dev` 2. `npm run dev`
3. Familiarize yourself with the [project's structure](Documentation.md). 3. Familiarize yourself with the [project's structure](Documentation.md).
@ -11,6 +13,7 @@
5. Begin developing. 5. Begin developing.
## Don't forget to... ## Don't forget to...
- ...update the [changelog](CHANGELOG.md) and any other necessary docs. - ...update the [changelog](CHANGELOG.md) and any other necessary docs.
- ...update the version numbers in `package.json` and `package-lock.json`. - ...update the version numbers in `package.json` and `package-lock.json`.
- ...make sure the test suite passes by running `npm test`. - ...make sure the test suite passes by running `npm test`.

View file

@ -1,13 +1,15 @@
# Structure # Structure
The top-level directory is reserved for files that have to be there for it to work as well as configuration files. 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. - `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. - `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. - `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. - `commands`: Here's the place to store commands. The file name determines the command name.
- `subcommands/`: All commands here are ignored by the category loader. Here is where you can split commands into different files. Also works per directory, for example, `utility/subcommands/` is ignored. - `subcommands/`: All commands here are ignored by the category loader. Here is where you can split commands into different files. Also works per directory, for example, `utility/subcommands/` is ignored.
- `<directory>/`: Specify a directory which'll group commands into a category. For example, a `utility` folder would make all commands inside have the `Utility` category. - `<directory>/`: Specify a directory which'll group commands into a category. For example, a `utility` folder would make all commands inside have the `Utility` category.
- `<file>.ts`: All commands at this level will have the `Miscellaneous` category. - `<file>.ts`: All commands at this level will have the `Miscellaneous` category.
- `events`: Here's the place to store events. The file name determines the event type. - `events`: Here's the place to store events. The file name determines the event type.
- `dist`: This is where the runnable code in `src` compiles to. (The directory structure mirrors `src`.) - `dist`: This is where the runnable code in `src` compiles to. (The directory structure mirrors `src`.)
- `test`: Used for all the unit tests. - `test`: Used for all the unit tests.
- `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. - `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.
@ -15,7 +17,9 @@ The top-level directory is reserved for files that have to be there for it to wo
- `docs`: Used for information about the design of the project. - `docs`: Used for information about the design of the project.
# Specific Files # Specific Files
This list starts from `src`/`dist`. 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. - `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. - `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/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.
@ -27,6 +31,7 @@ This list starts from `src`/`dist`.
- `core/permissions`: The file containing everything related to permissions. - `core/permissions`: The file containing everything related to permissions.
# Design Decisions # 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`. - 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). - 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. - `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.
@ -37,7 +42,7 @@ This list starts from `src`/`dist`.
- 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. - 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. - 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.
- There were several possible ways to go about implementing aliases and subaliases. - There were several possible ways to go about implementing aliases and subaliases.
- Two properties on the `Command` class, `aliases: string[]` and `subaliases: {[key: string]: string[]}`. - Two properties on the `Command` class, `aliases: string[]` and `subaliases: {[key: string]: string[]}`.
- Exporting a const named `aliases` which would handle top-level aliases. - Exporting a const named `aliases` which would handle top-level aliases.
- For `subaliases`, either making subaliases work like redirects (Instead of doing `new Command(...)`, you'd do `"nameOfSubcommand"`), or define properties on `Command`. - For `subaliases`, either making subaliases work like redirects (Instead of doing `new Command(...)`, you'd do `"nameOfSubcommand"`), or define properties on `Command`.
- What I ended up doing for aliases is making an `aliases` property on `Command` and then converting those string arrays to a more usable structure with strings pointing to the original commands. `aliases` at the very top will determine global aliases and is pretty much the exception in the program's logic. `aliases` elsewhere will provide aliases per subcommand. For anything other than the top-level or `subcommands`, `aliases` does nothing (plus you'll get a warning about it). - What I ended up doing for aliases is making an `aliases` property on `Command` and then converting those string arrays to a more usable structure with strings pointing to the original commands. `aliases` at the very top will determine global aliases and is pretty much the exception in the program's logic. `aliases` elsewhere will provide aliases per subcommand. For anything other than the top-level or `subcommands`, `aliases` does nothing (plus you'll get a warning about it).

12
package-lock.json generated
View file

@ -610,6 +610,12 @@
"integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==",
"dev": true "dev": true
}, },
"husky": {
"version": "5.0.0-alpha.6",
"resolved": "https://registry.npmjs.org/husky/-/husky-5.0.0-alpha.6.tgz",
"integrity": "sha512-Ofqq0oHLCO0r8hTb/1PQ3FAfsW945adUli8jFTeXdOIK8gCUmxY9A0BI0mG9oGboPf+Y53bvEmX6ljdrz+yV6w==",
"dev": true
},
"iconv-lite": { "iconv-lite": {
"version": "0.4.24", "version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
@ -1032,6 +1038,12 @@
"integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==", "integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==",
"dev": true "dev": true
}, },
"prettier": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.1.2.tgz",
"integrity": "sha512-16c7K+x4qVlJg9rEbXl7HEGmQyZlG4R9AgP+oHKRMsMsuk8s+ATStlf1NpDqyBI1HpVyfjLOeMhH2LvuNvV5Vg==",
"dev": true
},
"promise.allsettled": { "promise.allsettled": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/promise.allsettled/-/promise.allsettled-1.0.2.tgz", "resolved": "https://registry.npmjs.org/promise.allsettled/-/promise.allsettled-1.0.2.tgz",

View file

@ -3,6 +3,7 @@
"version": "0.0.1", "version": "0.0.1",
"description": "A Discord bot built on Discord.JS v12", "description": "A Discord bot built on Discord.JS v12",
"main": "dist/index.js", "main": "dist/index.js",
"private": true,
"dependencies": { "dependencies": {
"chalk": "^4.1.0", "chalk": "^4.1.0",
"discord.js": "^12.2.0", "discord.js": "^12.2.0",
@ -14,12 +15,15 @@
"@types/mocha": "^8.0.3", "@types/mocha": "^8.0.3",
"@types/node": "^14.0.22", "@types/node": "^14.0.22",
"@types/ws": "^7.2.6", "@types/ws": "^7.2.6",
"husky": "^5.0.0-alpha.6",
"mocha": "^8.1.2", "mocha": "^8.1.2",
"prettier": "2.1.2",
"ts-node": "^9.0.0", "ts-node": "^9.0.0",
"tsc-watch": "^4.2.9", "tsc-watch": "^4.2.9",
"typescript": "^3.9.6" "typescript": "^3.9.6"
}, },
"scripts": { "scripts": {
"postinstall": "husky install",
"build": "tsc && npm prune --production", "build": "tsc && npm prune --production",
"start": "node dist/index.js", "start": "node dist/index.js",
"once": "tsc && npm start", "once": "tsc && npm start",

13
prettier.config.js Normal file
View file

@ -0,0 +1,13 @@
module.exports = {
printWidth: 80,
tabWidth: 2,
useTabs: false,
semi: true,
singleQuote: true,
quoteProps: 'as-needed',
jsxSingleQuote: false,
trailingComma: 'all',
bracketSpacing: true,
jsxBracketSameLine: true,
arrowParens: 'always',
};

View file

@ -1,163 +1,188 @@
import Command from "../core/command"; import Command from '../core/command';
import {CommonLibrary, logs, botHasPermission} from "../core/lib"; import { CommonLibrary, logs, botHasPermission } from '../core/lib';
import {Config, Storage} from "../core/structures"; import { Config, Storage } from '../core/structures';
import {PermissionNames, getPermissionLevel} from "../core/permissions"; import { PermissionNames, getPermissionLevel } from '../core/permissions';
import {Permissions} from "discord.js"; import { Permissions } from 'discord.js';
function getLogBuffer(type: string) function getLogBuffer(type: string) {
{ return {
return {files: [{ files: [
attachment: Buffer.alloc(logs[type].length, logs[type]), {
name: `${Date.now()}.${type}.log` attachment: Buffer.alloc(logs[type].length, logs[type]),
}]}; name: `${Date.now()}.${type}.log`,
},
],
};
} }
const activities = ["playing", "listening", "streaming", "watching"]; const activities = ['playing', 'listening', 'streaming', 'watching'];
export default new Command({ 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.", description:
async run($: CommonLibrary): Promise<any> "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> {
if(!$.member) if (!$.member)
return $.channel.send("Couldn't find a member object for you! Did you make sure you used this in a server?"); return $.channel.send(
const permLevel = getPermissionLevel($.member); "Couldn't find a member object for you! Did you make sure you used this in a server?",
$.channel.send(`${$.author.toString()}, your permission level is \`${PermissionNames[permLevel]}\` (${permLevel}).`); );
}, const permLevel = getPermissionLevel($.member);
subcommands: $.channel.send(
{ `${$.author.toString()}, your permission level is \`${
set: new Command({ PermissionNames[permLevel]
description: "Set different per-guild settings for the bot.", }\` (${permLevel}).`,
run: "You have to specify the option you want to set.", );
permission: Command.PERMISSIONS.ADMIN, },
subcommands: subcommands: {
{ set: new Command({
prefix: new Command({ description: 'Set different per-guild settings for the bot.',
description: "Set a custom prefix for your guild. Removes your custom prefix if none is provided.", run: 'You have to specify the option you want to set.',
usage: "(<prefix>)", permission: Command.PERMISSIONS.ADMIN,
async run($: CommonLibrary): Promise<any> subcommands: {
{ prefix: new Command({
Storage.getGuild($.guild?.id || "N/A").prefix = null; description:
Storage.save(); 'Set a custom prefix for your guild. Removes your custom prefix if none is provided.',
$.channel.send(`The custom prefix for this guild has been removed. My prefix is now back to \`${Config.prefix}\`.`); usage: '(<prefix>)',
}, async run($: CommonLibrary): Promise<any> {
any: new Command({ Storage.getGuild($.guild?.id || 'N/A').prefix = null;
async run($: CommonLibrary): Promise<any> Storage.save();
{ $.channel.send(
Storage.getGuild($.guild?.id || "N/A").prefix = $.args[0]; `The custom prefix for this guild has been removed. My prefix is now back to \`${Config.prefix}\`.`,
Storage.save(); );
$.channel.send(`The custom prefix for this guild is now \`${$.args[0]}\`.`); },
} any: new Command({
}) async run($: CommonLibrary): Promise<any> {
}) Storage.getGuild($.guild?.id || 'N/A').prefix = $.args[0];
} Storage.save();
}), $.channel.send(
diag: new Command({ `The custom prefix for this guild is now \`${$.args[0]}\`.`,
description: "Requests a debug log with the \"info\" verbosity level.", );
permission: Command.PERMISSIONS.BOT_SUPPORT, },
async run($: CommonLibrary): Promise<any> }),
{ }),
$.channel.send(getLogBuffer("info")); },
}, }),
any: new Command({ diag: new Command({
description: `Select a verbosity to listen to. Available levels: \`[${Object.keys(logs).join(", ")}]\``, description: 'Requests a debug log with the "info" verbosity level.',
async run($: CommonLibrary): Promise<any> permission: Command.PERMISSIONS.BOT_SUPPORT,
{ async run($: CommonLibrary): Promise<any> {
const type = $.args[0]; $.channel.send(getLogBuffer('info'));
},
if(type in logs) any: new Command({
$.channel.send(getLogBuffer(type)); description: `Select a verbosity to listen to. Available levels: \`[${Object.keys(
else logs,
$.channel.send(`Couldn't find a verbosity level named \`${type}\`! The available types are \`[${Object.keys(logs).join(", ")}]\`.`); ).join(', ')}]\``,
} async run($: CommonLibrary): Promise<any> {
}) const type = $.args[0];
}),
status: new Command({
description: "Changes the bot's status.",
permission: Command.PERMISSIONS.BOT_SUPPORT,
async run($: CommonLibrary): Promise<any>
{
$.channel.send('Setting status to `online`...');
},
any: new Command({
description: `Select a status to set to. Available statuses: \`online\`, \`idle\`, \`dnd\`, \`invisible\``,
async run($: CommonLibrary): Promise<any>
{
let statuses = ['online', 'idle', 'dnd', 'invisible'];
if (!statuses.includes($.args[0])) return $.channel.send("That status doesn't exist!");
else {
$.client.user?.setStatus($.args[0]);
$.channel.send(`Setting status to \`${$.args[0]}\`...`);
}
}
})
}),
purge: new Command({
description: "Purges bot messages.",
permission: Command.PERMISSIONS.BOT_SUPPORT,
async run($: CommonLibrary): Promise<any>
{
$.message.delete();
const msgs = await $.channel.messages.fetch({
limit: 100
});
const travMessages = msgs.filter(m => m.author.id === $.client.user?.id);
await $.message.channel.send(`Found ${travMessages.size} messages to delete.`) if (type in logs) $.channel.send(getLogBuffer(type));
.then(m => m.delete({ else
timeout: 5000 $.channel.send(
})); `Couldn't find a verbosity level named \`${type}\`! The available types are \`[${Object.keys(
await $.message.channel.bulkDelete(travMessages); logs,
} ).join(', ')}]\`.`,
}), );
nick: new Command({ },
description: "Change the bot's nickname.", }),
permission: Command.PERMISSIONS.BOT_SUPPORT, }),
async run($: CommonLibrary): Promise<any> status: new Command({
{ description: "Changes the bot's status.",
const nickName = $.args.join(" "); permission: Command.PERMISSIONS.BOT_SUPPORT,
const trav = $.guild?.members.cache.find(member => member.id === $.client.user?.id); async run($: CommonLibrary): Promise<any> {
await trav?.setNickname(nickName); $.channel.send('Setting status to `online`...');
if(botHasPermission($.guild, Permissions.FLAGS.MANAGE_MESSAGES)) },
$.message.delete({timeout: 5000}).catch($.handler.bind($)); any: new Command({
$.channel.send(`Nickname set to \`${nickName}\``) description: `Select a status to set to. Available statuses: \`online\`, \`idle\`, \`dnd\`, \`invisible\``,
.then(m => m.delete({timeout: 5000})); async run($: CommonLibrary): Promise<any> {
} let statuses = ['online', 'idle', 'dnd', 'invisible'];
}), if (!statuses.includes($.args[0]))
guilds: new Command({ return $.channel.send("That status doesn't exist!");
description: "Shows a list of all guilds the bot is a member of.", else {
permission: Command.PERMISSIONS.BOT_SUPPORT, $.client.user?.setStatus($.args[0]);
async run($: CommonLibrary): Promise<any> $.channel.send(`Setting status to \`${$.args[0]}\`...`);
{ }
const guildList = $.client.guilds.cache.array() },
.map(e => e.name); }),
$.channel.send(guildList); }),
} purge: new Command({
}), description: 'Purges bot messages.',
activity: new Command({ permission: Command.PERMISSIONS.BOT_SUPPORT,
description: "Set the activity of the bot.", async run($: CommonLibrary): Promise<any> {
permission: Command.PERMISSIONS.BOT_SUPPORT, $.message.delete();
usage: "<type> <string>", const msgs = await $.channel.messages.fetch({
async run($: CommonLibrary): Promise<any> limit: 100,
{ });
$.client.user?.setActivity(".help", { const travMessages = msgs.filter(
type: "LISTENING" (m) => m.author.id === $.client.user?.id,
}); );
$.channel.send("Activity set to default.")
}, await $.message.channel
any: new Command({ .send(`Found ${travMessages.size} messages to delete.`)
description: `Select an activity type to set. Available levels: \`[${activities.join(", ")}]\``, .then((m) =>
async run($: CommonLibrary): Promise<any> m.delete({
{ timeout: 5000,
const type = $.args[0]; }),
);
if(activities.includes(type)) { await $.message.channel.bulkDelete(travMessages);
$.client.user?.setActivity($.args.slice(1).join(" "), {type: $.args[0].toUpperCase()}) },
$.channel.send(`Set activity to \`${$.args[0].toUpperCase()}\` \`${$.args.slice(1).join(" ")}\`.`) }),
} nick: new Command({
else description: "Change the bot's nickname.",
$.channel.send(`Couldn't find an activity type named \`${type}\`! The available types are \`[${activities.join(", ")}]\`.`); permission: Command.PERMISSIONS.BOT_SUPPORT,
} async run($: CommonLibrary): Promise<any> {
}) const nickName = $.args.join(' ');
}) const trav = $.guild?.members.cache.find(
} (member) => member.id === $.client.user?.id,
}); );
await trav?.setNickname(nickName);
if (botHasPermission($.guild, Permissions.FLAGS.MANAGE_MESSAGES))
$.message.delete({ timeout: 5000 }).catch($.handler.bind($));
$.channel
.send(`Nickname set to \`${nickName}\``)
.then((m) => m.delete({ timeout: 5000 }));
},
}),
guilds: new Command({
description: 'Shows a list of all guilds the bot is a member of.',
permission: Command.PERMISSIONS.BOT_SUPPORT,
async run($: CommonLibrary): Promise<any> {
const guildList = $.client.guilds.cache.array().map((e) => e.name);
$.channel.send(guildList);
},
}),
activity: new Command({
description: 'Set the activity of the bot.',
permission: Command.PERMISSIONS.BOT_SUPPORT,
usage: '<type> <string>',
async run($: CommonLibrary): Promise<any> {
$.client.user?.setActivity('.help', {
type: 'LISTENING',
});
$.channel.send('Activity set to default.');
},
any: new Command({
description: `Select an activity type to set. Available levels: \`[${activities.join(
', ',
)}]\``,
async run($: CommonLibrary): Promise<any> {
const type = $.args[0];
if (activities.includes(type)) {
$.client.user?.setActivity($.args.slice(1).join(' '), {
type: $.args[0].toUpperCase(),
});
$.channel.send(
`Set activity to \`${$.args[0].toUpperCase()}\` \`${$.args
.slice(1)
.join(' ')}\`.`,
);
} else
$.channel.send(
`Couldn't find an activity type named \`${type}\`! The available types are \`[${activities.join(
', ',
)}]\`.`,
);
},
}),
}),
},
});

View file

@ -1,39 +1,38 @@
import Command from "../../core/command"; import Command from '../../core/command';
import {CommonLibrary} from "../../core/lib"; import { CommonLibrary } from '../../core/lib';
const responses = [ const responses = [
"Most likely,", 'Most likely,',
"It is certain,", 'It is certain,',
"It is decidedly so,", 'It is decidedly so,',
"Without a doubt,", 'Without a doubt,',
"Definitely,", 'Definitely,',
"You may rely on it,", 'You may rely on it,',
"As I see it, yes,", 'As I see it, yes,',
"Outlook good,", 'Outlook good,',
"Yes,", 'Yes,',
"Signs point to yes,", 'Signs point to yes,',
"Reply hazy, try again,", 'Reply hazy, try again,',
"Ask again later,", 'Ask again later,',
"Better not tell you now,", 'Better not tell you now,',
"Cannot predict now,", 'Cannot predict now,',
"Concentrate and ask again,", 'Concentrate and ask again,',
"Don't count on it,", "Don't count on it,",
"My reply is no,", 'My reply is no,',
"My sources say no,", 'My sources say no,',
"Outlook not so good,", 'Outlook not so good,',
"Very doubtful," 'Very doubtful,',
]; ];
export default new Command({ export default new Command({
description: "Answers your question in an 8-ball manner.", description: 'Answers your question in an 8-ball manner.',
endpoint: false, endpoint: false,
usage: "<question>", usage: '<question>',
run: "Please provide a question.", run: 'Please provide a question.',
any: new Command({ any: new Command({
description: "Question to ask the 8-ball.", description: 'Question to ask the 8-ball.',
async run($: CommonLibrary): Promise<any> async run($: CommonLibrary): Promise<any> {
{ const sender = $.message.author;
const sender = $.message.author; $.channel.send($(responses).random() + ` <@${sender.id}>`);
$.channel.send($(responses).random() + ` <@${sender.id}>`); },
} }),
}) });
})

View file

@ -1,26 +1,28 @@
import { MessageEmbed } from "discord.js"; import { MessageEmbed } from 'discord.js';
import Command from "../../core/command"; import Command from '../../core/command';
import {CommonLibrary} from "../../core/lib"; import { CommonLibrary } from '../../core/lib';
export default new Command({ export default new Command({
description: "Create a poll.", description: 'Create a poll.',
usage: "<question>", usage: '<question>',
run: "Please provide a question.", run: 'Please provide a question.',
any: new Command({ any: new Command({
description: "Question for the poll.", description: 'Question for the poll.',
async run($: CommonLibrary): Promise<any> async run($: CommonLibrary): Promise<any> {
{ const embed = new MessageEmbed()
const embed = new MessageEmbed() .setAuthor(
.setAuthor(`Poll created by ${$.message.author.username}`, $.message.guild?.iconURL({ dynamic: true }) ?? undefined) `Poll created by ${$.message.author.username}`,
.setColor(0xffffff) $.message.guild?.iconURL({ dynamic: true }) ?? undefined,
.setFooter("React to vote.") )
.setDescription($.args.join(" ")); .setColor(0xffffff)
const msg = await $.channel.send(embed); .setFooter('React to vote.')
await msg.react("✅"); .setDescription($.args.join(' '));
await msg.react("⛔"); const msg = await $.channel.send(embed);
$.message.delete({ await msg.react('✅');
timeout: 1000 await msg.react('⛔');
}); $.message.delete({
} timeout: 1000,
}) });
}) },
}),
});

View file

@ -1,141 +1,145 @@
import Command from "../core/command"; import Command from '../core/command';
import {CommonLibrary} from "../core/lib"; import { CommonLibrary } from '../core/lib';
import {loadCommands, categories} from "../core/command"; import { loadCommands, categories } from '../core/command';
import {PermissionNames} from "../core/permissions"; import { PermissionNames } from '../core/permissions';
export default new Command({ export default new Command({
description: "Lists all commands. If a command is specified, their arguments are listed as well.", description:
usage: "([command, [subcommand/type], ...])", 'Lists all commands. If a command is specified, their arguments are listed as well.',
aliases: ["h"], usage: '([command, [subcommand/type], ...])',
async run($: CommonLibrary): Promise<any> aliases: ['h'],
{ async run($: CommonLibrary): Promise<any> {
const commands = await loadCommands(); const commands = await loadCommands();
let output = `Legend: \`<type>\`, \`[list/of/stuff]\`, \`(optional)\`, \`(<optional type>)\`, \`([optional/list/...])\``; let output = `Legend: \`<type>\`, \`[list/of/stuff]\`, \`(optional)\`, \`(<optional type>)\`, \`([optional/list/...])\``;
for(const [category, headers] of categories)
{
output += `\n\n===[ ${category} ]===`;
for(const header of headers)
{
if(header !== "test")
{
const command = commands.get(header);
if(!command)
return $.warn(`Command "${header}" of category "${category}" unexpectedly doesn't exist!`);
output += `\n- \`${header}\`: ${command.description}`;
}
}
}
$.channel.send(output, {split: true});
},
any: new Command({
async run($: CommonLibrary): Promise<any>
{
const commands = await loadCommands();
let header = $.args.shift() as string;
let command = commands.get(header);
if(!command || header === "test")
return $.channel.send(`No command found by the name \`${header}\`!`);
if(command.originalCommandName) for (const [category, headers] of categories) {
header = command.originalCommandName; output += `\n\n===[ ${category} ]===`;
else
$.warn(`originalCommandName isn't defined for ${header}?!`); for (const header of headers) {
if (header !== 'test') {
let permLevel = command.permission ?? Command.PERMISSIONS.NONE; const command = commands.get(header);
let usage = command.usage;
let invalid = false; if (!command)
return $.warn(
let selectedCategory = "Unknown"; `Command "${header}" of category "${category}" unexpectedly doesn't exist!`,
);
for(const [category, headers] of categories)
{ output += `\n- \`${header}\`: ${command.description}`;
if(headers.includes(header)) }
{ }
if(selectedCategory !== "Unknown") }
$.warn(`Command "${header}" is somehow in multiple categories. This means that the command loading stage probably failed in properly adding categories.`);
else $.channel.send(output, { split: true });
selectedCategory = category; },
} any: new Command({
} async run($: CommonLibrary): Promise<any> {
const commands = await loadCommands();
for(const param of $.args) let header = $.args.shift() as string;
{ let command = commands.get(header);
const type = command.resolve(param);
command = command.get(param); if (!command || header === 'test')
permLevel = command.permission ?? permLevel; return $.channel.send(`No command found by the name \`${header}\`!`);
switch(type) if (command.originalCommandName) header = command.originalCommandName;
{ else $.warn(`originalCommandName isn't defined for ${header}?!`);
case Command.TYPES.SUBCOMMAND: header += ` ${command.originalCommandName}`; break;
case Command.TYPES.USER: header += " <user>" ; break; let permLevel = command.permission ?? Command.PERMISSIONS.NONE;
case Command.TYPES.NUMBER: header += " <number>" ; break; let usage = command.usage;
case Command.TYPES.ANY: header += " <any>" ; break; let invalid = false;
default: header += ` ${param}`; break;
} let selectedCategory = 'Unknown';
if(type === Command.TYPES.NONE) for (const [category, headers] of categories) {
{ if (headers.includes(header)) {
invalid = true; if (selectedCategory !== 'Unknown')
break; $.warn(
} `Command "${header}" is somehow in multiple categories. This means that the command loading stage probably failed in properly adding categories.`,
} );
else selectedCategory = category;
if(invalid) }
return $.channel.send(`No command found by the name \`${header}\`!`); }
let append = ""; for (const param of $.args) {
const type = command.resolve(param);
if(usage === "") command = command.get(param);
{ permLevel = command.permission ?? permLevel;
const list: string[] = [];
switch (type) {
command.subcommands.forEach((subcmd, subtag) => { case Command.TYPES.SUBCOMMAND:
// Don't capture duplicates generated from aliases. header += ` ${command.originalCommandName}`;
if(subcmd.originalCommandName === subtag) { break;
const customUsage = subcmd.usage ? ` ${subcmd.usage}` : ""; case Command.TYPES.USER:
list.push(`- \`${header} ${subtag}${customUsage}\` - ${subcmd.description}`); header += ' <user>';
} break;
}); case Command.TYPES.NUMBER:
header += ' <number>';
const addDynamicType = (cmd: Command|null, type: string) => { break;
if(cmd) { case Command.TYPES.ANY:
const customUsage = cmd.usage ? ` ${cmd.usage}` : ""; header += ' <any>';
list.push(`- \`${header} <${type}>${customUsage}\` - ${cmd.description}`); break;
} default:
}; header += ` ${param}`;
break;
addDynamicType(command.user, "user"); }
addDynamicType(command.number, "number");
addDynamicType(command.any, "any"); if (type === Command.TYPES.NONE) {
invalid = true;
append = "Usages:" + (list.length > 0 ? `\n${list.join('\n')}` : " None."); break;
} }
else }
append = `Usage: \`${header} ${usage}\``;
if (invalid)
let aliases = "None"; return $.channel.send(`No command found by the name \`${header}\`!`);
if(command.aliases.length > 0) let append = '';
{
aliases = ""; if (usage === '') {
const list: string[] = [];
for(let i = 0; i < command.aliases.length; i++)
{ command.subcommands.forEach((subcmd, subtag) => {
const alias = command.aliases[i]; // Don't capture duplicates generated from aliases.
aliases += `\`${alias}\``; if (subcmd.originalCommandName === subtag) {
const customUsage = subcmd.usage ? ` ${subcmd.usage}` : '';
if(i !== command.aliases.length-1) list.push(
aliases += ", "; `- \`${header} ${subtag}${customUsage}\` - ${subcmd.description}`,
} );
} }
});
$.channel.send(`Command: \`${header}\`\nAliases: ${aliases}\nCategory: \`${selectedCategory}\`\nPermission Required: \`${PermissionNames[permLevel]}\` (${permLevel})\nDescription: ${command.description}\n${append}`, {split: true});
} const addDynamicType = (cmd: Command | null, type: string) => {
}) if (cmd) {
}); const customUsage = cmd.usage ? ` ${cmd.usage}` : '';
list.push(
`- \`${header} <${type}>${customUsage}\` - ${cmd.description}`,
);
}
};
addDynamicType(command.user, 'user');
addDynamicType(command.number, 'number');
addDynamicType(command.any, 'any');
append =
'Usages:' + (list.length > 0 ? `\n${list.join('\n')}` : ' None.');
} else append = `Usage: \`${header} ${usage}\``;
let aliases = 'None';
if (command.aliases.length > 0) {
aliases = '';
for (let i = 0; i < command.aliases.length; i++) {
const alias = command.aliases[i];
aliases += `\`${alias}\``;
if (i !== command.aliases.length - 1) aliases += ', ';
}
}
$.channel.send(
`Command: \`${header}\`\nAliases: ${aliases}\nCategory: \`${selectedCategory}\`\nPermission Required: \`${PermissionNames[permLevel]}\` (${permLevel})\nDescription: ${command.description}\n${append}`,
{ split: true },
);
},
}),
});

View file

@ -1,124 +1,197 @@
import { Guild, MessageEmbed } from "discord.js"; import { Guild, MessageEmbed } from 'discord.js';
import moment from "moment"; import moment from 'moment';
import Command from "../core/command"; import Command from '../core/command';
import {CommonLibrary} from "../core/lib"; import { CommonLibrary } from '../core/lib';
import { verificationLevels, filterLevels, regions, flags } from "../defs/info"; import { verificationLevels, filterLevels, regions, flags } from '../defs/info';
export default new Command({ export default new Command({
description: "Command to provide all sorts of info about the current server, a user, etc.", description:
run: "Please provide an argument.\nFor help, run `%prefix%help info`.", 'Command to provide all sorts of info about the current server, a user, etc.',
subcommands: run: 'Please provide an argument.\nFor help, run `%prefix%help info`.',
{ subcommands: {
avatar: new Command({ avatar: new Command({
description: "Shows your own, or another user's avatar.", description: "Shows your own, or another user's avatar.",
usage: "(<user>)", usage: '(<user>)',
async run($: CommonLibrary): Promise<any> async run($: CommonLibrary): Promise<any> {
{ $.channel.send(
$.channel.send($.author.displayAvatarURL({ dynamic: true, size: 2048 })) $.author.displayAvatarURL({ dynamic: true, size: 2048 }),
}, );
user: new Command({ },
description: "Shows your own, or another user's avatar.", user: new Command({
async run($: CommonLibrary): Promise<any> description: "Shows your own, or another user's avatar.",
{ async run($: CommonLibrary): Promise<any> {
$.channel.send($.args[0].displayAvatarURL({ dynamic: true, size: 2048 })) $.channel.send(
} $.args[0].displayAvatarURL({ dynamic: true, size: 2048 }),
}), );
}), },
}),
guild: new Command({ }),
description: "Displays info about the current guild.",
async run($: CommonLibrary): Promise<any>
{
if ($.guild) {
const roles = $.guild.roles.cache.sort((a, b) => b.position - a.position).map(role => role.toString());
const members = $.guild.members.cache;
const channels = $.guild.channels.cache;
const emojis = $.guild.emojis.cache;
const iconURL = $.guild.iconURL({ dynamic: true })
const embed = new MessageEmbed()
.setDescription(`**Guild information for __${$.guild.name}__**`)
.setColor('BLUE')
if (iconURL) embed.setThumbnail(iconURL)
.addField('General', [
`** Name:** ${$.guild.name}`,
`** ID:** ${$.guild.id}`,
`** Owner:** ${$.guild.owner?.user.tag} (${$.guild.ownerID})`,
`** Region:** ${regions[$.guild.region]}`,
`** Boost Tier:** ${$.guild.premiumTier ? `Tier ${$.guild.premiumTier}` : 'None'}`,
`** Explicit Filter:** ${filterLevels[$.guild.explicitContentFilter]}`,
`** Verification Level:** ${verificationLevels[$.guild.verificationLevel]}`,
`** Time Created:** ${moment($.guild.createdTimestamp).format('LT')} ${moment($.guild.createdTimestamp).format('LL')} ${moment($.guild.createdTimestamp).fromNow()})`,
'\u200b'
])
.addField('Statistics', [
`** Role Count:** ${roles.length}`,
`** Emoji Count:** ${emojis.size}`,
`** Regular Emoji Count:** ${emojis.filter(emoji => !emoji.animated).size}`,
`** Animated Emoji Count:** ${emojis.filter(emoji => emoji.animated).size}`,
`** Member Count:** ${$.guild.memberCount}`,
`** Humans:** ${members.filter(member => !member.user.bot).size}`,
`** Bots:** ${members.filter(member => member.user.bot).size}`,
`** Text Channels:** ${channels.filter(channel => channel.type === 'text').size}`,
`** Voice Channels:** ${channels.filter(channel => channel.type === 'voice').size}`,
`** Boost Count:** ${$.guild.premiumSubscriptionCount || '0'}`,
`\u200b`
])
.addField('Presence', [
`** Online:** ${members.filter(member => member.presence.status === 'online').size}`,
`** Idle:** ${members.filter(member => member.presence.status === 'idle').size}`,
`** Do Not Disturb:** ${members.filter(member => member.presence.status === 'dnd').size}`,
`** Offline:** ${members.filter(member => member.presence.status === 'offline').size}`,
'\u200b'
])
.addField(`Roles [${roles.length - 1}]`, roles.length < 10 ? roles.join(', ') : roles.length > 10 ? this.client.utils.trimArray(roles) : 'None')
.setTimestamp();
$.channel.send(embed) guild: new Command({
description: 'Displays info about the current guild.',
async run($: CommonLibrary): Promise<any> {
if ($.guild) {
const roles = $.guild.roles.cache
.sort((a, b) => b.position - a.position)
.map((role) => role.toString());
const members = $.guild.members.cache;
const channels = $.guild.channels.cache;
const emojis = $.guild.emojis.cache;
} else { const iconURL = $.guild.iconURL({ dynamic: true });
$.channel.send("Please execute this command in a guild.") const embed = new MessageEmbed()
} .setDescription(`**Guild information for __${$.guild.name}__**`)
} .setColor('BLUE');
}) if (iconURL)
}, embed
user: new Command({ .setThumbnail(iconURL)
description: "Displays info about mentioned user.", .addField('General', [
async run($: CommonLibrary): Promise<any> `** Name:** ${$.guild.name}`,
{ `** ID:** ${$.guild.id}`,
// Transforms the User object into a GuildMember object of the current guild. `** Owner:** ${$.guild.owner?.user.tag} (${$.guild.ownerID})`,
const member = $.guild?.members.resolve($.args[0]); `** Region:** ${regions[$.guild.region]}`,
`** Boost Tier:** ${
$.guild.premiumTier ? `Tier ${$.guild.premiumTier}` : 'None'
}`,
`** Explicit Filter:** ${
filterLevels[$.guild.explicitContentFilter]
}`,
`** Verification Level:** ${
verificationLevels[$.guild.verificationLevel]
}`,
`** Time Created:** ${moment($.guild.createdTimestamp).format(
'LT',
)} ${moment($.guild.createdTimestamp).format('LL')} ${moment(
$.guild.createdTimestamp,
).fromNow()})`,
'\u200b',
])
.addField('Statistics', [
`** Role Count:** ${roles.length}`,
`** Emoji Count:** ${emojis.size}`,
`** Regular Emoji Count:** ${
emojis.filter((emoji) => !emoji.animated).size
}`,
`** Animated Emoji Count:** ${
emojis.filter((emoji) => emoji.animated).size
}`,
`** Member Count:** ${$.guild.memberCount}`,
`** Humans:** ${
members.filter((member) => !member.user.bot).size
}`,
`** Bots:** ${
members.filter((member) => member.user.bot).size
}`,
`** Text Channels:** ${
channels.filter((channel) => channel.type === 'text').size
}`,
`** Voice Channels:** ${
channels.filter((channel) => channel.type === 'voice').size
}`,
`** Boost Count:** ${$.guild.premiumSubscriptionCount || '0'}`,
`\u200b`,
])
.addField('Presence', [
`** Online:** ${
members.filter(
(member) => member.presence.status === 'online',
).size
}`,
`** Idle:** ${
members.filter((member) => member.presence.status === 'idle')
.size
}`,
`** Do Not Disturb:** ${
members.filter((member) => member.presence.status === 'dnd')
.size
}`,
`** Offline:** ${
members.filter(
(member) => member.presence.status === 'offline',
).size
}`,
'\u200b',
])
.addField(
`Roles [${roles.length - 1}]`,
roles.length < 10
? roles.join(', ')
: roles.length > 10
? this.client.utils.trimArray(roles)
: 'None',
)
.setTimestamp();
if(!member) $.channel.send(embed);
return $.channel.send("No member object was found by that user! Are you sure you used this command in a server?"); } else {
$.channel.send('Please execute this command in a guild.');
}
},
}),
},
user: new Command({
description: 'Displays info about mentioned user.',
async run($: CommonLibrary): Promise<any> {
// Transforms the User object into a GuildMember object of the current guild.
const member = $.guild?.members.resolve($.args[0]);
const roles = member.roles.cache if (!member)
.sort((a: { position: number; }, b: { position: number; }) => b.position - a.position) return $.channel.send(
.map((role: { toString: () => any; }) => role.toString()) 'No member object was found by that user! Are you sure you used this command in a server?',
.slice(0, -1); );
// @ts-ignore - Discord.js' typings seem to be outdated here. According to their v12 docs, it's User.fetchFlags() instead of User.flags.
const userFlags = (await member.user.fetchFlags() as UserFlags).toArray();
const embed = new MessageEmbed() const roles = member.roles.cache
.setThumbnail(member.user.displayAvatarURL({ dynamic: true, size: 512 })) .sort(
.setColor(member.displayHexColor || 'BLUE') (a: { position: number }, b: { position: number }) =>
.addField('User', [ b.position - a.position,
`** Username:** ${member.user.username}`, )
`** Discriminator:** ${member.user.discriminator}`, .map((role: { toString: () => any }) => role.toString())
`** ID:** ${member.id}`, .slice(0, -1);
`** Flags:** ${userFlags.length ? userFlags.join(', ') : 'None'}`, // @ts-ignore - Discord.js' typings seem to be outdated here. According to their v12 docs, it's User.fetchFlags() instead of User.flags.
`** Avatar:** [Link to avatar](${member.user.displayAvatarURL({ dynamic: true })})`, const userFlags = ((await member.user.fetchFlags()) as UserFlags).toArray();
`** Time Created:** ${moment(member.user.createdTimestamp).format('LT')} ${moment(member.user.createdTimestamp).format('LL')} ${moment(member.user.createdTimestamp).fromNow()}`,
`** Status:** ${member.user.presence.status}`, const embed = new MessageEmbed()
`** Game:** ${member.user.presence.activities || 'Not playing a game.'}` .setThumbnail(
]) member.user.displayAvatarURL({ dynamic: true, size: 512 }),
.addField('Member', [ )
`** Highest Role:** ${member.roles.highest.id === $.guild?.id ? 'None' : member.roles.highest.name}`, .setColor(member.displayHexColor || 'BLUE')
`** Server Join Date:** ${moment(member.joinedAt).format('LL LTS')}`, .addField('User', [
`** Hoist Role:** ${member.roles.hoist ? member.roles.hoist.name : 'None'}`, `** Username:** ${member.user.username}`,
`** Roles:** [${roles.length}]: ${roles.length < 10 ? roles.join(', ') : roles.length > 10 ? this.client.utils.trimArray(roles) : 'None'}`, `** Discriminator:** ${member.user.discriminator}`,
]); `** ID:** ${member.id}`,
$.channel.send(embed) `** Flags:** ${userFlags.length ? userFlags.join(', ') : 'None'}`,
} `** Avatar:** [Link to avatar](${member.user.displayAvatarURL({
}) dynamic: true,
}); })})`,
`** Time Created:** ${moment(member.user.createdTimestamp).format(
'LT',
)} ${moment(member.user.createdTimestamp).format('LL')} ${moment(
member.user.createdTimestamp,
).fromNow()}`,
`** Status:** ${member.user.presence.status}`,
`** Game:** ${
member.user.presence.activities || 'Not playing a game.'
}`,
])
.addField('Member', [
`** Highest Role:** ${
member.roles.highest.id === $.guild?.id
? 'None'
: member.roles.highest.name
}`,
`** Server Join Date:** ${moment(member.joinedAt).format('LL LTS')}`,
`** Hoist Role:** ${
member.roles.hoist ? member.roles.hoist.name : 'None'
}`,
`** Roles:** [${roles.length}]: ${
roles.length < 10
? roles.join(', ')
: roles.length > 10
? this.client.utils.trimArray(roles)
: 'None'
}`,
]);
$.channel.send(embed);
},
}),
});

View file

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

View file

@ -1,191 +1,191 @@
import Command from '../core/command'; import Command from '../core/command';
import {CommonLibrary} from '../core/lib'; import { CommonLibrary } from '../core/lib';
import moment from "moment"; import moment from 'moment';
import {Collection, TextChannel} from 'discord.js'; import { Collection, TextChannel } from 'discord.js';
const lastUsedTimestamps: {[id: string]: number} = {}; const lastUsedTimestamps: { [id: string]: number } = {};
export default new Command({ export default new Command({
description: "Scans all text channels in the current guild and returns the number of times each emoji specific to the guild has been used. Has a cooldown of 24 hours per guild.", description:
async run($: CommonLibrary): Promise<any> 'Scans all text channels in the current guild and returns the number of times each emoji specific to the guild has been used. Has a cooldown of 24 hours per guild.',
{ async run($: CommonLibrary): Promise<any> {
if(!$.guild) if (!$.guild)
return $.channel.send(`You must use this command on a server!`); return $.channel.send(`You must use this command on a server!`);
// Test if the command is on cooldown. This isn't the strictest cooldown possible, because in the event that the bot crashes, the cooldown will be reset. But for all intends and purposes, it's a good enough cooldown. It's a per-server cooldown. // Test if the command is on cooldown. This isn't the strictest cooldown possible, because in the event that the bot crashes, the cooldown will be reset. But for all intends and purposes, it's a good enough cooldown. It's a per-server cooldown.
const startTime = Date.now(); const startTime = Date.now();
const cooldown = 86400000; // 24 hours const cooldown = 86400000; // 24 hours
const lastUsedTimestamp = lastUsedTimestamps[$.guild.id] ?? 0; const lastUsedTimestamp = lastUsedTimestamps[$.guild.id] ?? 0;
const difference = startTime - lastUsedTimestamp; const difference = startTime - lastUsedTimestamp;
const howLong = moment(startTime).to(lastUsedTimestamp + cooldown); const howLong = moment(startTime).to(lastUsedTimestamp + cooldown);
// If it's been less than an hour since the command was last used, prevent it from executing. // If it's been less than an hour since the command was last used, prevent it from executing.
if(difference < cooldown) if (difference < cooldown)
return $.channel.send(`This command requires a day to cooldown. You'll be able to activate this command ${howLong}.`); return $.channel.send(
else `This command requires a day to cooldown. You'll be able to activate this command ${howLong}.`,
lastUsedTimestamps[$.guild.id] = startTime; );
else lastUsedTimestamps[$.guild.id] = startTime;
const stats: {[id: string]: {
name: string, const stats: {
formatted: string, [id: string]: {
users: number, name: string;
bots: number formatted: string;
}} = {}; users: number;
let totalUserEmoteUsage = 0; bots: number;
// IMPORTANT: You MUST check if the bot actually has access to the channel in the first place. It will get the list of all channels, but that doesn't mean it has access to every channel. Without this, it'll require admin access and throw an annoying unhelpful DiscordAPIError: Missing Access otherwise. };
const allTextChannelsInCurrentGuild = $.guild.channels.cache.filter(channel => channel.type === "text" && channel.viewable) as Collection<string, TextChannel>; } = {};
let messagesSearched = 0; let totalUserEmoteUsage = 0;
let channelsSearched = 0; // IMPORTANT: You MUST check if the bot actually has access to the channel in the first place. It will get the list of all channels, but that doesn't mean it has access to every channel. Without this, it'll require admin access and throw an annoying unhelpful DiscordAPIError: Missing Access otherwise.
let currentChannelName = ""; const allTextChannelsInCurrentGuild = $.guild.channels.cache.filter(
const totalChannels = allTextChannelsInCurrentGuild.size; (channel) => channel.type === 'text' && channel.viewable,
const statusMessage = await $.channel.send("Gathering emotes..."); ) as Collection<string, TextChannel>;
let warnings = 0; let messagesSearched = 0;
$.channel.startTyping(); let channelsSearched = 0;
let currentChannelName = '';
// Initialize the emote stats object with every emote in the current guild. const totalChannels = allTextChannelsInCurrentGuild.size;
// The goal here is to cut the need to access guild.emojis.get() which'll make it faster and easier to work with. const statusMessage = await $.channel.send('Gathering emotes...');
for(let emote of $.guild.emojis.cache.values()) let warnings = 0;
{ $.channel.startTyping();
// If you don't include the "a" for animated emotes, it'll not only not show up, but also cause all other emotes in the same message to not show up. The emote name is self-correcting but it's better to keep the right value since it'll be used to calculate message lengths that fit.
stats[emote.id] = { // Initialize the emote stats object with every emote in the current guild.
name: emote.name, // The goal here is to cut the need to access guild.emojis.get() which'll make it faster and easier to work with.
formatted: `<${emote.animated ? "a" : ""}:${emote.name}:${emote.id}>`, for (let emote of $.guild.emojis.cache.values()) {
users: 0, // If you don't include the "a" for animated emotes, it'll not only not show up, but also cause all other emotes in the same message to not show up. The emote name is self-correcting but it's better to keep the right value since it'll be used to calculate message lengths that fit.
bots: 0 stats[emote.id] = {
}; name: emote.name,
}; formatted: `<${emote.animated ? 'a' : ''}:${emote.name}:${emote.id}>`,
users: 0,
const interval = setInterval(() => { bots: 0,
statusMessage.edit(`Searching channel \`${currentChannelName}\`... (${messagesSearched} messages scanned, ${channelsSearched}/${totalChannels} channels scanned)`); };
}, 5000); }
for(const channel of allTextChannelsInCurrentGuild.values()) const interval = setInterval(() => {
{ statusMessage.edit(
currentChannelName = channel.name; `Searching channel \`${currentChannelName}\`... (${messagesSearched} messages scanned, ${channelsSearched}/${totalChannels} channels scanned)`,
let selected = channel.lastMessageID ?? $.message.id; );
let continueLoop = true; }, 5000);
while(continueLoop) for (const channel of allTextChannelsInCurrentGuild.values()) {
{ currentChannelName = channel.name;
// Unfortunately, any kind of .fetch call is limited to 100 items at once by Discord's API. let selected = channel.lastMessageID ?? $.message.id;
const messages = await channel.messages.fetch({ let continueLoop = true;
limit: 100,
before: selected while (continueLoop) {
}); // Unfortunately, any kind of .fetch call is limited to 100 items at once by Discord's API.
const messages = await channel.messages.fetch({
if(messages.size > 0) limit: 100,
{ before: selected,
for(const msg of messages.values()) });
{
// It's very important to not capture an array of matches then do \d+ on each item because emote names can have numbers in them, causing the bot to not count them correctly. if (messages.size > 0) {
const search = /<a?:.+?:(\d+?)>/g; for (const msg of messages.values()) {
const text = msg.content; // It's very important to not capture an array of matches then do \d+ on each item because emote names can have numbers in them, causing the bot to not count them correctly.
let match: RegExpExecArray|null; const search = /<a?:.+?:(\d+?)>/g;
const text = msg.content;
while(match = search.exec(text)) let match: RegExpExecArray | null;
{
const emoteID = match[1]; while ((match = search.exec(text))) {
const emoteID = match[1];
if(emoteID in stats)
{ if (emoteID in stats) {
if(msg.author.bot) if (msg.author.bot) stats[emoteID].bots++;
stats[emoteID].bots++; else {
else stats[emoteID].users++;
{ totalUserEmoteUsage++;
stats[emoteID].users++; }
totalUserEmoteUsage++; }
} }
}
} for (const reaction of msg.reactions.cache.values()) {
const emoteID = reaction.emoji.id;
for(const reaction of msg.reactions.cache.values()) let continueReactionLoop = true;
{ let lastUserID: string | undefined;
const emoteID = reaction.emoji.id; let userReactions = 0;
let continueReactionLoop = true; let botReactions = 0;
let lastUserID: string|undefined;
let userReactions = 0; // An emote's ID will be null if it's a unicode emote.
let botReactions = 0; if (emoteID && emoteID in stats) {
// There is a simple count property on a reaction, but that doesn't separate users from bots.
// An emote's ID will be null if it's a unicode emote. // So instead, I'll use that property to check for inconsistencies.
if(emoteID && emoteID in stats) while (continueReactionLoop) {
{ // After logging users, it seems like the order is strictly numerical. As long as that stays consistent, this should work fine.
// There is a simple count property on a reaction, but that doesn't separate users from bots. const users = await reaction.users.fetch({
// So instead, I'll use that property to check for inconsistencies. limit: 100,
while(continueReactionLoop) after: lastUserID,
{ });
// After logging users, it seems like the order is strictly numerical. As long as that stays consistent, this should work fine.
const users = await reaction.users.fetch({ if (users.size > 0) {
limit: 100, for (const user of users.values()) {
after: lastUserID if (user.bot) {
}); stats[emoteID].bots++;
botReactions++;
if(users.size > 0) } else {
{ stats[emoteID].users++;
for(const user of users.values()) totalUserEmoteUsage++;
{ userReactions++;
if(user.bot) }
{
stats[emoteID].bots++; lastUserID = user.id;
botReactions++; }
} } else {
else // Then halt the loop and send warnings of any inconsistencies.
{ continueReactionLoop = false;
stats[emoteID].users++;
totalUserEmoteUsage++; if (reaction.count !== userReactions + botReactions) {
userReactions++; $.warn(
} `[Channel: ${channel.id}, Message: ${msg.id}] A reaction count of ${reaction.count} was expected but was given ${userReactions} user reactions and ${botReactions} bot reactions.`,
);
lastUserID = user.id; warnings++;
}; }
} }
else }
{ }
// Then halt the loop and send warnings of any inconsistencies. }
continueReactionLoop = false;
selected = msg.id;
if(reaction.count !== userReactions + botReactions) messagesSearched++;
{ }
$.warn(`[Channel: ${channel.id}, Message: ${msg.id}] A reaction count of ${reaction.count} was expected but was given ${userReactions} user reactions and ${botReactions} bot reactions.`); } else {
warnings++; continueLoop = false;
} channelsSearched++;
} }
} }
} }
};
// Mark the operation as ended.
selected = msg.id; const finishTime = Date.now();
messagesSearched++; clearInterval(interval);
}; statusMessage.edit(
} `Finished operation in ${moment
else .duration(finishTime - startTime)
{ .humanize()} with ${$(warnings).pluralise(
continueLoop = false; 'inconsistenc',
channelsSearched++; 'ies',
} 'y',
} )}.`,
} );
$.log(`Finished operation in ${finishTime - startTime} ms.`);
// Mark the operation as ended. $.channel.stopTyping();
const finishTime = Date.now();
clearInterval(interval); // Display stats on emote usage.
statusMessage.edit(`Finished operation in ${moment.duration(finishTime - startTime).humanize()} with ${$(warnings).pluralise("inconsistenc", "ies", "y")}.`); // This can work outside the loop now that it's synchronous, and now it's clearer what code is meant to execute at the end.
$.log(`Finished operation in ${finishTime - startTime} ms.`); let sortedEmoteIDs = Object.keys(stats).sort(
$.channel.stopTyping(); (a, b) => stats[b].users - stats[a].users,
);
// Display stats on emote usage. const lines: string[] = [];
// This can work outside the loop now that it's synchronous, and now it's clearer what code is meant to execute at the end. let rank = 1;
let sortedEmoteIDs = Object.keys(stats).sort((a, b) => stats[b].users - stats[a].users);
const lines: string[] = []; // It's better to send all the lines at once rather than paginate the data because it's quite a memory-intensive task to search all the messages in a server for it, and I wouldn't want to activate the command again just to get to another page.
let rank = 1; for (const emoteID of sortedEmoteIDs) {
const emote = stats[emoteID];
// It's better to send all the lines at once rather than paginate the data because it's quite a memory-intensive task to search all the messages in a server for it, and I wouldn't want to activate the command again just to get to another page. const botInfo = emote.bots > 0 ? ` (Bots: ${emote.bots})` : '';
for(const emoteID of sortedEmoteIDs) lines.push(
{ `\`#${rank++}\` ${emote.formatted} x ${emote.users} - ${(
const emote = stats[emoteID]; (emote.users / totalUserEmoteUsage) * 100 || 0
const botInfo = emote.bots > 0 ? ` (Bots: ${emote.bots})` : ""; ).toFixed(3)}%` + botInfo,
lines.push(`\`#${rank++}\` ${emote.formatted} x ${emote.users} - ${((emote.users / totalUserEmoteUsage * 100) || 0).toFixed(3)}%` + botInfo); );
} }
$.channel.send(lines, {split: true}).catch($.handler.bind($)); $.channel.send(lines, { split: true }).catch($.handler.bind($));
} },
}); });

View file

@ -1,58 +1,59 @@
import { MessageEmbed } from "discord.js"; import { MessageEmbed } from 'discord.js';
import Command from '../core/command'; import Command from '../core/command';
import {CommonLibrary} from '../core/lib'; import { CommonLibrary } from '../core/lib';
export default new Command({ export default new Command({
description: "Various utilities.", description: 'Various utilities.',
endpoint: false, endpoint: false,
usage: '', usage: '',
async run($: CommonLibrary): Promise<any> { async run($: CommonLibrary): Promise<any> {},
subcommands: {
}, lsemotes: new Command({
subcommands: { description: "Lists all emotes the bot has in it's registry,",
lsemotes: new Command({ endpoint: true,
description: "Lists all emotes the bot has in it's registry,", async run($: CommonLibrary): Promise<any> {
endpoint: true, const nsfw: string | string[] = [];
async run($: CommonLibrary): Promise<any> { const pages = $.client.emojis.cache
const nsfw: string | string[] = []; .filter((x) => !nsfw.includes(x.guild.id), this)
const pages = $.client.emojis.cache.filter(x => !nsfw.includes(x.guild.id), this).array(); .array();
const pagesSplit = $(pages).split(20); const pagesSplit = $(pages).split(20);
$.log(pagesSplit) $.log(pagesSplit);
var embed = new MessageEmbed() var embed = new MessageEmbed()
.setTitle("**Emoji list!**") .setTitle('**Emoji list!**')
.setColor("AQUA") .setColor('AQUA');
let desc = "" let desc = '';
for (const emote of pagesSplit[0]) { for (const emote of pagesSplit[0]) {
desc += `${emote} | ${emote.name}\n` desc += `${emote} | ${emote.name}\n`;
} }
embed.setDescription(desc) embed.setDescription(desc);
const msg = await $.channel.send({embed}); const msg = await $.channel.send({ embed });
$.paginate(msg, $.author.id, pages.length, page => { $.paginate(msg, $.author.id, pages.length, (page) => {
let desc = "" let desc = '';
for(const emote of pagesSplit[page]) { for (const emote of pagesSplit[page]) {
desc += `${emote} | ${emote.name}\n` desc += `${emote} | ${emote.name}\n`;
} }
embed.setDescription(desc) embed.setDescription(desc);
msg.edit(embed); msg.edit(embed);
}); });
} },
}), }),
emote: new Command({ emote: new Command({
description: "Send the specified emote.", description: 'Send the specified emote.',
run: "Please provide a command name.", run: 'Please provide a command name.',
any: new Command({ any: new Command({
description: "The emote to send.", description: 'The emote to send.',
usage: "<emote>", usage: '<emote>',
async run($: CommonLibrary): Promise<any> async run($: CommonLibrary): Promise<any> {
{ const search = $.args[0].toLowerCase();
const search = $.args[0].toLowerCase(); const emote = $.client.emojis.cache.find((emote) =>
const emote = $.client.emojis.cache.find(emote => emote.name.toLowerCase().includes(search)); emote.name.toLowerCase().includes(search),
if (!emote) return $.channel.send("That's not a valid emote name!"); );
$.message.delete(); if (!emote) return $.channel.send("That's not a valid emote name!");
$.channel.send(`${emote}`); $.message.delete();
} $.channel.send(`${emote}`);
}) },
}) }),
} }),
}); },
});

View file

@ -1,232 +1,259 @@
import $, {isType, parseVars, CommonLibrary} from "./lib"; import $, { isType, parseVars, CommonLibrary } from './lib';
import {Collection} from "discord.js"; import { Collection } from 'discord.js';
import {generateHandler} from "./storage"; import { generateHandler } from './storage';
import {promises as ffs, existsSync, writeFile} from "fs"; import { promises as ffs, existsSync, writeFile } from 'fs';
import {PERMISSIONS} from "./permissions"; import { PERMISSIONS } from './permissions';
import {getPrefix} from "../core/structures"; import { getPrefix } from '../core/structures';
interface CommandOptions interface CommandOptions {
{ description?: string;
description?: string; endpoint?: boolean;
endpoint?: boolean; usage?: string;
usage?: string; permission?: PERMISSIONS | null;
permission?: PERMISSIONS|null; aliases?: string[];
aliases?: string[]; run?: (($: CommonLibrary) => Promise<any>) | string;
run?: (($: CommonLibrary) => Promise<any>)|string; subcommands?: { [key: string]: Command };
subcommands?: {[key: string]: Command}; user?: Command;
user?: Command; number?: Command;
number?: Command; any?: Command;
any?: Command;
} }
export enum TYPES {SUBCOMMAND, USER, NUMBER, ANY, NONE}; export enum TYPES {
SUBCOMMAND,
export default class Command USER,
{ NUMBER,
public readonly description: string; ANY,
public readonly endpoint: boolean; NONE,
public readonly usage: string;
public readonly permission: PERMISSIONS|null;
public readonly aliases: string[]; // This is to keep the array intact for parent Command instances to use. It'll also be used when loading top-level aliases.
public originalCommandName: string|null; // If the command is an alias, what's the original name?
public run: (($: CommonLibrary) => Promise<any>)|string;
public readonly subcommands: Collection<string, Command>; // This is the final data structure you'll actually use to work with the commands the aliases point to.
public user: Command|null;
public number: Command|null;
public any: Command|null;
public static readonly TYPES = TYPES;
public static readonly PERMISSIONS = PERMISSIONS;
constructor(options?: CommandOptions)
{
this.description = options?.description || "No description.";
this.endpoint = options?.endpoint || false;
this.usage = options?.usage || "";
this.permission = options?.permission ?? null;
this.aliases = options?.aliases ?? [];
this.originalCommandName = null;
this.run = options?.run || "No action was set on this command!";
this.subcommands = new Collection(); // Populate this collection after setting subcommands.
this.user = options?.user || null;
this.number = options?.number || null;
this.any = options?.any || null;
if(options?.subcommands)
{
const baseSubcommands = Object.keys(options.subcommands);
// Loop once to set the base subcommands.
for(const name in options.subcommands)
this.subcommands.set(name, options.subcommands[name]);
// Then loop again to make aliases point to the base subcommands and warn if something's not right.
// This shouldn't be a problem because I'm hoping that JS stores these as references that point to the same object.
for(const name in options.subcommands)
{
const subcmd = options.subcommands[name];
subcmd.originalCommandName = name;
const aliases = subcmd.aliases;
for(const alias of aliases)
{
if(baseSubcommands.includes(alias))
$.warn(`"${alias}" in subcommand "${name}" was attempted to be declared as an alias but it already exists in the base commands! (Look at the next "Loading Command" line to see which command is affected.)`);
else if(this.subcommands.has(alias))
$.warn(`Duplicate alias "${alias}" at subcommand "${name}"! (Look at the next "Loading Command" line to see which command is affected.)`);
else
this.subcommands.set(alias, subcmd);
}
}
}
if(this.user && this.user.aliases.length > 0)
$.warn(`There are aliases defined for a "user"-type subcommand, but those aliases won't be used. (Look at the next "Loading Command" line to see which command is affected.)`);
if(this.number && this.number.aliases.length > 0)
$.warn(`There are aliases defined for a "number"-type subcommand, but those aliases won't be used. (Look at the next "Loading Command" line to see which command is affected.)`);
if(this.any && this.any.aliases.length > 0)
$.warn(`There are aliases defined for an "any"-type subcommand, but those aliases won't be used. (Look at the next "Loading Command" line to see which command is affected.)`);
}
public execute($: CommonLibrary)
{
if(isType(this.run, String))
{
$.channel.send(parseVars(this.run as string, {
author: $.author.toString(),
prefix: getPrefix($.guild)
}, "???"));
}
else
(this.run as Function)($).catch($.handler.bind($));
}
public resolve(param: string): TYPES
{
if(this.subcommands.has(param))
return TYPES.SUBCOMMAND;
// Any Discord ID format will automatically format to a user ID.
else if(this.user && (/\d{17,19}/.test(param)))
return TYPES.USER;
// Disallow infinity and allow for 0.
else if(this.number && (Number(param) || param === "0") && !param.includes("Infinity"))
return TYPES.NUMBER;
else if(this.any)
return TYPES.ANY;
else
return TYPES.NONE;
}
public get(param: string): Command
{
const type = this.resolve(param);
let command: Command;
switch(type)
{
case TYPES.SUBCOMMAND: command = this.subcommands.get(param) as Command; break;
case TYPES.USER: command = this.user as Command; break;
case TYPES.NUMBER: command = this.number as Command; break;
case TYPES.ANY: command = this.any as Command; break;
default: command = this; break;
}
return command;
}
} }
let commands: Collection<string, Command>|null = null; export default class Command {
public readonly description: string;
public readonly endpoint: boolean;
public readonly usage: string;
public readonly permission: PERMISSIONS | null;
public readonly aliases: string[]; // This is to keep the array intact for parent Command instances to use. It'll also be used when loading top-level aliases.
public originalCommandName: string | null; // If the command is an alias, what's the original name?
public run: (($: CommonLibrary) => Promise<any>) | string;
public readonly subcommands: Collection<string, Command>; // This is the final data structure you'll actually use to work with the commands the aliases point to.
public user: Command | null;
public number: Command | null;
public any: Command | null;
public static readonly TYPES = TYPES;
public static readonly PERMISSIONS = PERMISSIONS;
constructor(options?: CommandOptions) {
this.description = options?.description || 'No description.';
this.endpoint = options?.endpoint || false;
this.usage = options?.usage || '';
this.permission = options?.permission ?? null;
this.aliases = options?.aliases ?? [];
this.originalCommandName = null;
this.run = options?.run || 'No action was set on this command!';
this.subcommands = new Collection(); // Populate this collection after setting subcommands.
this.user = options?.user || null;
this.number = options?.number || null;
this.any = options?.any || null;
if (options?.subcommands) {
const baseSubcommands = Object.keys(options.subcommands);
// Loop once to set the base subcommands.
for (const name in options.subcommands)
this.subcommands.set(name, options.subcommands[name]);
// Then loop again to make aliases point to the base subcommands and warn if something's not right.
// This shouldn't be a problem because I'm hoping that JS stores these as references that point to the same object.
for (const name in options.subcommands) {
const subcmd = options.subcommands[name];
subcmd.originalCommandName = name;
const aliases = subcmd.aliases;
for (const alias of aliases) {
if (baseSubcommands.includes(alias))
$.warn(
`"${alias}" in subcommand "${name}" was attempted to be declared as an alias but it already exists in the base commands! (Look at the next "Loading Command" line to see which command is affected.)`,
);
else if (this.subcommands.has(alias))
$.warn(
`Duplicate alias "${alias}" at subcommand "${name}"! (Look at the next "Loading Command" line to see which command is affected.)`,
);
else this.subcommands.set(alias, subcmd);
}
}
}
if (this.user && this.user.aliases.length > 0)
$.warn(
`There are aliases defined for a "user"-type subcommand, but those aliases won't be used. (Look at the next "Loading Command" line to see which command is affected.)`,
);
if (this.number && this.number.aliases.length > 0)
$.warn(
`There are aliases defined for a "number"-type subcommand, but those aliases won't be used. (Look at the next "Loading Command" line to see which command is affected.)`,
);
if (this.any && this.any.aliases.length > 0)
$.warn(
`There are aliases defined for an "any"-type subcommand, but those aliases won't be used. (Look at the next "Loading Command" line to see which command is affected.)`,
);
}
public execute($: CommonLibrary) {
if (isType(this.run, String)) {
$.channel.send(
parseVars(
this.run as string,
{
author: $.author.toString(),
prefix: getPrefix($.guild),
},
'???',
),
);
} else (this.run as Function)($).catch($.handler.bind($));
}
public resolve(param: string): TYPES {
if (this.subcommands.has(param)) return TYPES.SUBCOMMAND;
// Any Discord ID format will automatically format to a user ID.
else if (this.user && /\d{17,19}/.test(param)) return TYPES.USER;
// Disallow infinity and allow for 0.
else if (
this.number &&
(Number(param) || param === '0') &&
!param.includes('Infinity')
)
return TYPES.NUMBER;
else if (this.any) return TYPES.ANY;
else return TYPES.NONE;
}
public get(param: string): Command {
const type = this.resolve(param);
let command: Command;
switch (type) {
case TYPES.SUBCOMMAND:
command = this.subcommands.get(param) as Command;
break;
case TYPES.USER:
command = this.user as Command;
break;
case TYPES.NUMBER:
command = this.number as Command;
break;
case TYPES.ANY:
command = this.any as Command;
break;
default:
command = this;
break;
}
return command;
}
}
let commands: Collection<string, Command> | null = null;
export const categories: Collection<string, string[]> = new Collection(); export const categories: Collection<string, string[]> = new Collection();
export const aliases: Collection<string, string> = new Collection(); // Top-level aliases only. export const aliases: Collection<string, string> = new Collection(); // Top-level aliases only.
/** Returns the cache of the commands if it exists and searches the directory if not. */ /** Returns the cache of the commands if it exists and searches the directory if not. */
export async function loadCommands(): Promise<Collection<string, Command>> export async function loadCommands(): Promise<Collection<string, Command>> {
{ if (commands) return commands;
if(commands)
return commands; if (process.argv[2] === 'dev' && !existsSync('src/commands/test.ts'))
writeFile(
if(process.argv[2] === "dev" && !existsSync("src/commands/test.ts")) 'src/commands/test.ts',
writeFile("src/commands/test.ts", template, generateHandler('"test.ts" (testing/template command) successfully generated.')); template,
generateHandler(
commands = new Collection(); '"test.ts" (testing/template command) successfully generated.',
const dir = await ffs.opendir("dist/commands"); ),
const listMisc: string[] = []; );
let selected;
commands = new Collection();
// There will only be one level of directory searching (per category). const dir = await ffs.opendir('dist/commands');
while(selected = await dir.read()) const listMisc: string[] = [];
{ let selected;
if(selected.isDirectory())
{ // There will only be one level of directory searching (per category).
if(selected.name === "subcommands") while ((selected = await dir.read())) {
continue; if (selected.isDirectory()) {
if (selected.name === 'subcommands') continue;
const subdir = await ffs.opendir(`dist/commands/${selected.name}`);
const category = $(selected.name).toTitleCase(); const subdir = await ffs.opendir(`dist/commands/${selected.name}`);
const list: string[] = []; const category = $(selected.name).toTitleCase();
let cmd; const list: string[] = [];
let cmd;
while(cmd = await subdir.read())
{ while ((cmd = await subdir.read())) {
if(cmd.isDirectory()) if (cmd.isDirectory()) {
{ if (cmd.name === 'subcommands') continue;
if(cmd.name === "subcommands") else
continue; $.warn(
else `You can't have multiple levels of directories! From: "dist/commands/${cmd.name}"`,
$.warn(`You can't have multiple levels of directories! From: "dist/commands/${cmd.name}"`); );
} } else loadCommand(cmd.name, list, selected.name);
else }
loadCommand(cmd.name, list, selected.name);
} subdir.close();
categories.set(category, list);
subdir.close(); } else loadCommand(selected.name, listMisc);
categories.set(category, list); }
}
else dir.close();
loadCommand(selected.name, listMisc); categories.set('Miscellaneous', listMisc);
}
return commands;
dir.close();
categories.set("Miscellaneous", listMisc);
return commands;
} }
async function loadCommand(filename: string, list: string[], category?: string) async function loadCommand(
{ filename: string,
if(!commands) list: string[],
return $.error(`Function "loadCommand" was called without first initializing commands!`); category?: string,
) {
const prefix = category ?? ""; if (!commands)
const header = filename.substring(0, filename.indexOf(".js")); return $.error(
const command = (await import(`../commands/${prefix}/${header}`)).default as Command|undefined; `Function "loadCommand" was called without first initializing commands!`,
);
if(!command)
return $.warn(`Command "${header}" has no default export which is a Command instance!`); const prefix = category ?? '';
const header = filename.substring(0, filename.indexOf('.js'));
command.originalCommandName = header; const command = (await import(`../commands/${prefix}/${header}`)).default as
list.push(header); | Command
| undefined;
if(commands.has(header))
$.warn(`Command "${header}" already exists! Make sure to make each command uniquely identifiable across categories!`); if (!command)
else return $.warn(
commands.set(header, command); `Command "${header}" has no default export which is a Command instance!`,
);
for(const alias of command.aliases)
{ command.originalCommandName = header;
if(commands.has(alias)) list.push(header);
$.warn(`Top-level alias "${alias}" from command "${header}" already exists either as a command or alias!`);
else if (commands.has(header))
commands.set(alias, command); $.warn(
} `Command "${header}" already exists! Make sure to make each command uniquely identifiable across categories!`,
);
$.log(`Loading Command: ${header} (${category ? $(category).toTitleCase() : "Miscellaneous"})`); else commands.set(header, command);
for (const alias of command.aliases) {
if (commands.has(alias))
$.warn(
`Top-level alias "${alias}" from command "${header}" already exists either as a command or alias!`,
);
else commands.set(alias, command);
}
$.log(
`Loading Command: ${header} (${
category ? $(category).toTitleCase() : 'Miscellaneous'
})`,
);
} }
// The template should be built with a reductionist mentality. // The template should be built with a reductionist mentality.
// Provide everything the user needs and then let them remove whatever they want. // 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. // That way, they aren't focusing on what's missing, but rather what they need for their command.
const template = const template = `import Command from '../core/command';
`import Command from '../core/command';
import {CommonLibrary} from '../core/lib'; import {CommonLibrary} from '../core/lib';
export default new Command({ export default new Command({
@ -277,4 +304,4 @@ export default new Command({
} }
}) })
});`; });`;

View file

@ -1,47 +1,41 @@
import {Client, ClientEvents, Constants} from "discord.js"; import { Client, ClientEvents, Constants } from 'discord.js';
import Storage from "./storage"; import Storage from './storage';
import $ from "./lib"; import $ from './lib';
interface EventOptions<K extends keyof ClientEvents> interface EventOptions<K extends keyof ClientEvents> {
{ readonly on?: (...args: ClientEvents[K]) => void;
readonly on?: (...args: ClientEvents[K]) => void; readonly once?: (...args: ClientEvents[K]) => void;
readonly once?: (...args: ClientEvents[K]) => void;
} }
export default class Event<K extends keyof ClientEvents> export default class Event<K extends keyof ClientEvents> {
{ private readonly on?: (...args: ClientEvents[K]) => void;
private readonly on?: (...args: ClientEvents[K]) => void; private readonly once?: (...args: ClientEvents[K]) => void;
private readonly once?: (...args: ClientEvents[K]) => void;
constructor(options: EventOptions<K>) {
constructor(options: EventOptions<K>) this.on = options.on;
{ this.once = options.once;
this.on = options.on; }
this.once = options.once;
} // For this function, I'm going to assume that the event is used with the correct arguments and that the event tag is checked in "storage.ts".
public attach(client: Client, event: K) {
// For this function, I'm going to assume that the event is used with the correct arguments and that the event tag is checked in "storage.ts". if (this.on) client.on(event, this.on);
public attach(client: Client, event: K) if (this.once) client.once(event, this.once);
{ }
if(this.on)
client.on(event, this.on);
if(this.once)
client.once(event, this.once);
}
} }
export async function loadEvents(client: Client) export async function loadEvents(client: Client) {
{ for (const file of Storage.open('dist/events', (filename: string) =>
for(const file of Storage.open("dist/events", (filename: string) => filename.endsWith(".js"))) filename.endsWith('.js'),
{ )) {
const header = file.substring(0, file.indexOf(".js")); const header = file.substring(0, file.indexOf('.js'));
const event = (await import(`../events/${header}`)).default; const event = (await import(`../events/${header}`)).default;
if((Object.values(Constants.Events) as string[]).includes(header)) if ((Object.values(Constants.Events) as string[]).includes(header)) {
{ event.attach(client, header);
event.attach(client, header); $.log(`Loading Event: ${header}`);
$.log(`Loading Event: ${header}`); } else
} $.warn(
else `"${header}" is not a valid event type! Did you misspell it? (Note: If you fixed the issue, delete "dist" because the compiler won't automatically delete any extra files.)`,
$.warn(`"${header}" is not a valid event type! Did you misspell it? (Note: If you fixed the issue, delete "dist" because the compiler won't automatically delete any extra files.)`); );
} }
} }

View file

@ -1,249 +1,323 @@
import {GenericWrapper, NumberWrapper, StringWrapper, ArrayWrapper} from "./wrappers"; import {
import {Client, Message, TextChannel, DMChannel, NewsChannel, Guild, User, GuildMember, Permissions} from "discord.js"; GenericWrapper,
import chalk from "chalk"; NumberWrapper,
import FileManager from "./storage"; StringWrapper,
import {eventListeners} from "../events/messageReactionRemove"; ArrayWrapper,
import {client} from "../index"; } from './wrappers';
import {
Client,
Message,
TextChannel,
DMChannel,
NewsChannel,
Guild,
User,
GuildMember,
Permissions,
} from 'discord.js';
import chalk from 'chalk';
import FileManager from './storage';
import { eventListeners } from '../events/messageReactionRemove';
import { client } from '../index';
/** A type that describes what the library module does. */ /** A type that describes what the library module does. */
export interface CommonLibrary export interface CommonLibrary {
{ // Wrapper Object //
// Wrapper Object // /** Wraps the value you enter with an object that provides extra functionality and provides common utility functions. */
/** Wraps the value you enter with an object that provides extra functionality and provides common utility functions. */ (value: number): NumberWrapper;
(value: number): NumberWrapper; (value: string): StringWrapper;
(value: string): StringWrapper; <T>(value: T[]): ArrayWrapper<T>;
<T>(value: T[]): ArrayWrapper<T>; <T>(value: T): GenericWrapper<T>;
<T>(value: T): GenericWrapper<T>;
// Common Library Functions //
// Common Library Functions // /** <Promise>.catch($.handler.bind($)) or <Promise>.catch(error => $.handler(error)) */
/** <Promise>.catch($.handler.bind($)) or <Promise>.catch(error => $.handler(error)) */ handler: (error: Error) => void;
handler: (error: Error) => void; log: (...args: any[]) => void;
log: (...args: any[]) => void; warn: (...args: any[]) => void;
warn: (...args: any[]) => void; error: (...args: any[]) => void;
error: (...args: any[]) => void; debug: (...args: any[]) => void;
debug: (...args: any[]) => void; ready: (...args: any[]) => void;
ready: (...args: any[]) => void; paginate: (
paginate: (message: Message, senderID: string, total: number, callback: (page: number) => void, duration?: number) => void; message: Message,
prompt: (message: Message, senderID: string, onConfirm: () => void, duration?: number) => void; senderID: string,
getMemberByUsername: (guild: Guild, username: string) => Promise<GuildMember|undefined>; total: number,
callMemberByUsername: (message: Message, username: string, onSuccess: (member: GuildMember) => void) => Promise<void>; callback: (page: number) => void,
duration?: number,
// Dynamic Properties // ) => void;
args: any[]; prompt: (
client: Client; message: Message,
message: Message; senderID: string,
channel: TextChannel|DMChannel|NewsChannel; onConfirm: () => void,
guild: Guild|null; duration?: number,
author: User; ) => void;
member: GuildMember|null; getMemberByUsername: (
guild: Guild,
username: string,
) => Promise<GuildMember | undefined>;
callMemberByUsername: (
message: Message,
username: string,
onSuccess: (member: GuildMember) => void,
) => Promise<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 $(value: number): NumberWrapper;
export default function $(value: string): StringWrapper; export default function $(value: string): StringWrapper;
export default function $<T>(value: T[]): ArrayWrapper<T>; export default function $<T>(value: T[]): ArrayWrapper<T>;
export default function $<T>(value: T): GenericWrapper<T>; export default function $<T>(value: T): GenericWrapper<T>;
export default function $(value: any) export default function $(value: any) {
{ if (isType(value, Number)) return new NumberWrapper(value);
if(isType(value, Number)) else if (isType(value, String)) return new StringWrapper(value);
return new NumberWrapper(value); else if (isType(value, Array)) return new ArrayWrapper(value);
else if(isType(value, String)) else return new GenericWrapper(value);
return new StringWrapper(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. // If you use promises, use this function to display the error in chat.
// Case #1: await $.channel.send(""); --> Automatically caught by Command.execute(). // Case #1: await $.channel.send(""); --> Automatically caught by Command.execute().
// Case #2: $.channel.send("").catch($.handler.bind($)); --> Manually caught by the user. // Case #2: $.channel.send("").catch($.handler.bind($)); --> Manually caught by the user.
$.handler = function(this: CommonLibrary, error: Error) $.handler = function (this: CommonLibrary, error: Error) {
{ if (this)
if(this) this.channel.send(
this.channel.send(`There was an error while trying to execute that command!\`\`\`${error.stack ?? error}\`\`\``); `There was an error while trying to execute that command!\`\`\`${
else error.stack ?? error
$.warn("No context was attached to $.handler! Make sure to use .catch($.handler.bind($)) or .catch(error => $.handler(error)) instead!"); }\`\`\``,
);
$.error(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. // Logs with different levels of verbosity.
export const logs: {[type: string]: string} = { export const logs: { [type: string]: string } = {
error: "", error: '',
warn: "", warn: '',
info: "", info: '',
verbose: "" verbose: '',
}; };
let enabled = true; let enabled = true;
export function setConsoleActivated(activated: boolean) {enabled = activated}; export function setConsoleActivated(activated: boolean) {
enabled = activated;
}
// The custom console. In order of verbosity, error, warn, log, and debug. Ready is a variation of log. // The custom console. In order of verbosity, error, warn, log, and debug. Ready is a variation of log.
// General Purpose Logger // General Purpose Logger
$.log = (...args: any[]) => { $.log = (...args: any[]) => {
if(enabled) if (enabled)
console.log(chalk.white.bgGray(formatTimestamp()), chalk.black.bgWhite("INFO"), ...args); console.log(
const text = `[${formatUTCTimestamp()}] [INFO] ${args.join(" ")}\n`; chalk.white.bgGray(formatTimestamp()),
logs.info += text; chalk.black.bgWhite('INFO'),
logs.verbose += text; ...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." // "It'll still work, but you should really check up on this."
$.warn = (...args: any[]) => { $.warn = (...args: any[]) => {
if(enabled) if (enabled)
console.warn(chalk.white.bgGray(formatTimestamp()), chalk.black.bgYellow("WARN"), ...args); console.warn(
const text = `[${formatUTCTimestamp()}] [WARN] ${args.join(" ")}\n`; chalk.white.bgGray(formatTimestamp()),
logs.warn += text; chalk.black.bgYellow('WARN'),
logs.info += text; ...args,
logs.verbose += text; );
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. // Used for anything which prevents the program from actually running.
$.error = (...args: any[]) => { $.error = (...args: any[]) => {
if(enabled) if (enabled)
console.error(chalk.white.bgGray(formatTimestamp()), chalk.white.bgRed("ERROR"), ...args); console.error(
const text = `[${formatUTCTimestamp()}] [ERROR] ${args.join(" ")}\n`; chalk.white.bgGray(formatTimestamp()),
logs.error += text; chalk.white.bgRed('ERROR'),
logs.warn += text; ...args,
logs.info += text; );
logs.verbose += text; 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". // 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(`core/lib::parseArgs("testing \"in progress\"") = ["testing", "in progress"]`) --> <path>/::(<object>.)<function>(<args>) = <value> // $.debug(`core/lib::parseArgs("testing \"in progress\"") = ["testing", "in progress"]`) --> <path>/::(<object>.)<function>(<args>) = <value>
// Would probably be more suited for debugging program logic rather than function logic, which can be checked using unit tests. // Would probably be more suited for debugging program logic rather than function logic, which can be checked using unit tests.
$.debug = (...args: any[]) => { $.debug = (...args: any[]) => {
if(process.argv[2] === "dev" && enabled) if (process.argv[2] === 'dev' && enabled)
console.debug(chalk.white.bgGray(formatTimestamp()), chalk.white.bgBlue("DEBUG"), ...args); console.debug(
const text = `[${formatUTCTimestamp()}] [DEBUG] ${args.join(" ")}\n`; chalk.white.bgGray(formatTimestamp()),
logs.verbose += text; 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. // Used once at the start of the program when the bot loads.
$.ready = (...args: any[]) => { $.ready = (...args: any[]) => {
if(enabled) if (enabled)
console.log(chalk.white.bgGray(formatTimestamp()), chalk.black.bgGreen("READY"), ...args); console.log(
const text = `[${formatUTCTimestamp()}] [READY] ${args.join(" ")}\n`; chalk.white.bgGray(formatTimestamp()),
logs.info += text; chalk.black.bgGreen('READY'),
logs.verbose += text; ...args,
);
const text = `[${formatUTCTimestamp()}] [READY] ${args.join(' ')}\n`;
logs.info += text;
logs.verbose += text;
}; };
export function formatTimestamp(now = new Date()) export function formatTimestamp(now = new Date()) {
{ const year = now.getFullYear();
const year = now.getFullYear(); const month = (now.getMonth() + 1).toString().padStart(2, '0');
const month = (now.getMonth() + 1).toString().padStart(2, '0'); const day = now.getDate().toString().padStart(2, '0');
const day = now.getDate().toString().padStart(2, '0'); const hour = now.getHours().toString().padStart(2, '0');
const hour = now.getHours().toString().padStart(2, '0'); const minute = now.getMinutes().toString().padStart(2, '0');
const minute = now.getMinutes().toString().padStart(2, '0'); const second = now.getSeconds().toString().padStart(2, '0');
const second = now.getSeconds().toString().padStart(2, '0'); return `${year}-${month}-${day} ${hour}:${minute}:${second}`;
return `${year}-${month}-${day} ${hour}:${minute}:${second}`;
} }
export function formatUTCTimestamp(now = new Date()) export function formatUTCTimestamp(now = new Date()) {
{ const year = now.getUTCFullYear();
const year = now.getUTCFullYear(); const month = (now.getUTCMonth() + 1).toString().padStart(2, '0');
const month = (now.getUTCMonth() + 1).toString().padStart(2, '0'); const day = now.getUTCDate().toString().padStart(2, '0');
const day = now.getUTCDate().toString().padStart(2, '0'); const hour = now.getUTCHours().toString().padStart(2, '0');
const hour = now.getUTCHours().toString().padStart(2, '0'); const minute = now.getUTCMinutes().toString().padStart(2, '0');
const minute = now.getUTCMinutes().toString().padStart(2, '0'); const second = now.getUTCSeconds().toString().padStart(2, '0');
const second = now.getUTCSeconds().toString().padStart(2, '0'); return `${year}-${month}-${day} ${hour}:${minute}:${second}`;
return `${year}-${month}-${day} ${hour}:${minute}:${second}`;
} }
export function botHasPermission(guild: Guild|null, permission: number): boolean export function botHasPermission(
{ guild: Guild | null,
return !!(client.user && guild?.members.resolve(client.user)?.hasPermission(permission)) permission: number,
): boolean {
return !!(
client.user &&
guild?.members.resolve(client.user)?.hasPermission(permission)
);
} }
// Pagination function that allows for customization via a callback. // 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. // 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) => { $.paginate = async (
let page = 0; message: Message,
const turn = (amount: number) => { senderID: string,
page += amount; total: number,
callback: (page: number) => void,
if(page < 0) duration = 60000,
page += total; ) => {
else if(page >= total) let page = 0;
page -= total; const turn = (amount: number) => {
page += amount;
callback(page);
}
const handle = (emote: string, reacterID: string) => {
switch(emote)
{
case '⬅️': turn(-1); break;
case '➡️': turn(1); break;
}
};
// Listen for reactions and call the handler. if (page < 0) page += total;
await message.react('⬅️'); else if (page >= total) page -= total;
await message.react('➡️');
eventListeners.set(message.id, handle); callback(page);
await message.awaitReactions((reaction, user) => { };
if(user.id === senderID) const handle = (emote: string, reacterID: string) => {
{ switch (emote) {
// The reason this is inside the call is because it's possible to switch a user's permissions halfway and suddenly throw an error. case '⬅️':
// This will dynamically adjust for that, switching modes depending on whether it currently has the "Manage Messages" permission. turn(-1);
const canDeleteEmotes = botHasPermission(message.guild, Permissions.FLAGS.MANAGE_MESSAGES); break;
handle(reaction.emoji.name, user.id); case '➡️':
turn(1);
if(canDeleteEmotes) break;
reaction.users.remove(user); }
} };
return false;
}, {time: duration}); // Listen for reactions and call the handler.
// When time's up, remove the bot's own reactions. await message.react('⬅️');
eventListeners.delete(message.id); await message.react('➡️');
message.reactions.cache.get('⬅️')?.users.remove(message.author); eventListeners.set(message.id, handle);
message.reactions.cache.get('➡️')?.users.remove(message.author); await message.awaitReactions(
(reaction, user) => {
if (user.id === senderID) {
// The reason this is inside the call is because it's possible to switch a user's permissions halfway and suddenly throw an error.
// This will dynamically adjust for that, switching modes depending on whether it currently has the "Manage Messages" permission.
const canDeleteEmotes = botHasPermission(
message.guild,
Permissions.FLAGS.MANAGE_MESSAGES,
);
handle(reaction.emoji.name, user.id);
if (canDeleteEmotes) reaction.users.remove(user);
}
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);
}; };
// Waits for the sender to either confirm an action or let it pass (and delete the message). // 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) => { $.prompt = async (
let isDeleted = false; message: Message,
senderID: string,
message.react('✅'); onConfirm: () => void,
await message.awaitReactions((reaction, user) => { duration = 10000,
if(user.id === senderID) ) => {
{ let isDeleted = false;
if(reaction.emoji.name === '✅')
onConfirm(); message.react('✅');
isDeleted = true; await message.awaitReactions(
message.delete(); (reaction, user) => {
} if (user.id === senderID) {
if (reaction.emoji.name === '✅') onConfirm();
// CollectorFilter requires a boolean to be returned. isDeleted = true;
// My guess is that the return value of awaitReactions can be altered by making a boolean filter. message.delete();
// 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; // CollectorFilter requires a boolean to be returned.
}, {time: duration}); // 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.
if(!isDeleted) // May as well just set it to false because I'm not concerned with collecting any reactions.
message.delete(); return false;
},
{ time: duration },
);
if (!isDeleted) message.delete();
}; };
$.getMemberByUsername = async(guild: Guild, username: string) => { $.getMemberByUsername = async (guild: Guild, username: string) => {
return (await guild.members.fetch({ return (
query: username, await guild.members.fetch({
limit: 1 query: username,
})).first(); limit: 1,
})
).first();
}; };
/** Convenience function to handle false cases automatically. */ /** Convenience function to handle false cases automatically. */
$.callMemberByUsername = async(message: Message, username: string, onSuccess: (member: GuildMember) => void) => { $.callMemberByUsername = async (
const guild = message.guild; message: Message,
const send = message.channel.send; username: string,
onSuccess: (member: GuildMember) => void,
if(guild) ) => {
{ const guild = message.guild;
const member = await $.getMemberByUsername(guild, username); const send = message.channel.send;
if(member) if (guild) {
onSuccess(member); const member = await $.getMemberByUsername(guild, username);
else
send(`Couldn't find a user by the name of \`${username}\`!`); if (member) onSuccess(member);
} else send(`Couldn't find a user by the name of \`${username}\`!`);
else } else send('You must execute this command in a server!');
send("You must execute this command in a server!");
}; };
/** /**
@ -251,41 +325,29 @@ $.callMemberByUsername = async(message: Message, username: string, onSuccess: (m
* - `\"` = `"` * - `\"` = `"`
* - `\\` = `\` * - `\\` = `\`
*/ */
export function parseArgs(line: string): string[] export function parseArgs(line: string): string[] {
{ let result = [];
let result = []; let selection = '';
let selection = ""; let inString = false;
let inString = false; let isEscaped = false;
let isEscaped = false;
for (let c of line) {
for(let c of line) if (isEscaped) {
{ if (['"', '\\'].includes(c)) selection += c;
if(isEscaped) else selection += '\\' + c;
{
if(['"', '\\'].includes(c)) isEscaped = false;
selection += c; } else if (c === '\\') isEscaped = true;
else else if (c === '"') inString = !inString;
selection += '\\' + c; else if (c === ' ' && !inString) {
result.push(selection);
isEscaped = false; selection = '';
} } else selection += c;
else if(c === '\\') }
isEscaped = true;
else if(c === '"') if (selection.length > 0) result.push(selection);
inString = !inString;
else if(c === ' ' && !inString) return result;
{
result.push(selection);
selection = "";
}
else
selection += c;
}
if(selection.length > 0)
result.push(selection)
return result;
} }
/** /**
@ -294,52 +356,41 @@ export function parseArgs(line: string): string[]
* - `%%` = `%` * - `%%` = `%`
* - If the invalid token is null/undefined, nothing is changed. * - If the invalid token is null/undefined, nothing is changed.
*/ */
export function parseVars(line: string, definitions: {[key: string]: string}, invalid: string|null = ""): string export function parseVars(
{ line: string,
let result = ""; definitions: { [key: string]: string },
let inVariable = false; invalid: string | null = '',
let token = ""; ): string {
let result = '';
for(const c of line) let inVariable = false;
{ let token = '';
if(c === '%')
{ for (const c of line) {
if(inVariable) if (c === '%') {
{ if (inVariable) {
if(token === "") if (token === '') result += '%';
result += '%'; else {
else if (token in definitions) result += definitions[token];
{ else if (invalid === null) result += `%${token}%`;
if(token in definitions) else result += invalid;
result += definitions[token];
else if(invalid === null) token = '';
result += `%${token}%`; }
else }
result += invalid;
inVariable = !inVariable;
token = ""; } else if (inVariable) token += c;
} else result += c;
} }
inVariable = !inVariable; return result;
}
else if(inVariable)
token += c;
else
result += c;
}
return result;
} }
export function isType(value: any, type: any): boolean export function isType(value: any, type: any): boolean {
{ if (value === undefined && type === undefined) return true;
if(value === undefined && type === undefined) else if (value === null && type === null) return true;
return true; else
else if(value === null && type === null) return value !== undefined && value !== null && value.constructor === type;
return true;
else
return value !== undefined && value !== null && value.constructor === type;
} }
/** /**
@ -348,54 +399,48 @@ export function isType(value: any, type: any): boolean
* If at any point the value doesn't match the data structure provided, the fallback is returned. * 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! * 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 export function select<T>(
{ value: any,
if(isArray && isType(value, Array)) fallback: T,
{ type: Function,
for(let item of value) isArray = false,
if(!isType(item, type)) ): T {
return fallback; if (isArray && isType(value, Array)) {
return value; for (let item of value) if (!isType(item, type)) return fallback;
} return value;
else } else {
{ if (isType(value, type)) return value;
if(isType(value, type)) else return fallback;
return value; }
else
return fallback;
}
} }
export interface GenericJSON export interface GenericJSON {
{ [key: string]: any;
[key: string]: any;
} }
export abstract class GenericStructure export abstract class GenericStructure {
{ private __meta__ = 'generic';
private __meta__ = "generic";
constructor(tag?: string) {
constructor(tag?: string) this.__meta__ = tag || this.__meta__;
{ }
this.__meta__ = tag || this.__meta__;
} public save(asynchronous = true) {
const tag = this.__meta__;
public save(asynchronous = true) /// @ts-ignore
{ delete this.__meta__;
const tag = this.__meta__; FileManager.write(tag, this, asynchronous);
/// @ts-ignore this.__meta__ = tag;
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). // 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). // 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 = { export const Random = {
num: (min: number, max: number) => (Math.random() * (max - min)) + min, num: (min: number, max: number) => Math.random() * (max - min) + min,
int: (min: number, max: number) => Math.floor(Random.num(min, max)), int: (min: number, max: number) => Math.floor(Random.num(min, max)),
chance: (decimal: number) => Math.random() < decimal, chance: (decimal: number) => Math.random() < decimal,
sign: (number = 1) => number * (Random.chance(0.5) ? -1 : 1), sign: (number = 1) => number * (Random.chance(0.5) ? -1 : 1),
deviation: (base: number, deviation: number) => Random.num(base - deviation, base + deviation) deviation: (base: number, deviation: number) =>
}; Random.num(base - deviation, base + deviation),
};

View file

@ -1,63 +1,80 @@
import {GuildMember, Permissions} from "discord.js"; import { GuildMember, Permissions } from 'discord.js';
import {Config} from "./structures"; import { Config } from './structures';
import $ from "./lib"; import $ from './lib';
export enum PERMISSIONS {NONE, MOD, ADMIN, OWNER, BOT_SUPPORT, BOT_ADMIN, BOT_OWNER}; export enum PERMISSIONS {
export const PermissionNames = ["User", "Moderator", "Administrator", "Server Owner", "Bot Support", "Bot Admin", "Bot Owner"]; NONE,
MOD,
ADMIN,
OWNER,
BOT_SUPPORT,
BOT_ADMIN,
BOT_OWNER,
}
export const PermissionNames = [
'User',
'Moderator',
'Administrator',
'Server Owner',
'Bot Support',
'Bot Admin',
'Bot Owner',
];
// Here is where you enter in the functions that check for permissions. // Here is where you enter in the functions that check for permissions.
const PermissionChecker: ((member: GuildMember) => boolean)[] = [ const PermissionChecker: ((member: GuildMember) => boolean)[] = [
// NONE // // NONE //
() => true, () => true,
// MOD // // MOD //
member => (member) =>
member.hasPermission(Permissions.FLAGS.MANAGE_ROLES) || member.hasPermission(Permissions.FLAGS.MANAGE_ROLES) ||
member.hasPermission(Permissions.FLAGS.MANAGE_MESSAGES) || member.hasPermission(Permissions.FLAGS.MANAGE_MESSAGES) ||
member.hasPermission(Permissions.FLAGS.KICK_MEMBERS) || member.hasPermission(Permissions.FLAGS.KICK_MEMBERS) ||
member.hasPermission(Permissions.FLAGS.BAN_MEMBERS), member.hasPermission(Permissions.FLAGS.BAN_MEMBERS),
// ADMIN // // ADMIN //
member => member.hasPermission(Permissions.FLAGS.ADMINISTRATOR), (member) => member.hasPermission(Permissions.FLAGS.ADMINISTRATOR),
// OWNER // // OWNER //
member => member.guild.ownerID === member.id, (member) => member.guild.ownerID === member.id,
// BOT_SUPPORT // // BOT_SUPPORT //
member => Config.support.includes(member.id), (member) => Config.support.includes(member.id),
// BOT_ADMIN // // BOT_ADMIN //
member => Config.admins.includes(member.id), (member) => Config.admins.includes(member.id),
// BOT_OWNER // // BOT_OWNER //
member => Config.owner === member.id (member) => Config.owner === member.id,
]; ];
// After checking the lengths of these three objects, use this as the length for consistency. // After checking the lengths of these three objects, use this as the length for consistency.
const length = Object.keys(PERMISSIONS).length / 2; const length = Object.keys(PERMISSIONS).length / 2;
export function hasPermission(member: GuildMember, permission: PERMISSIONS): boolean export function hasPermission(
{ member: GuildMember,
for(let i = length-1; i >= permission; i--) permission: PERMISSIONS,
if(PermissionChecker[i](member)) ): boolean {
return true; for (let i = length - 1; i >= permission; i--)
return false; if (PermissionChecker[i](member)) return true;
return false;
} }
export function getPermissionLevel(member: GuildMember): number export function getPermissionLevel(member: GuildMember): number {
{ for (let i = length - 1; i >= 0; i--)
for(let i = length-1; i >= 0; i--) if (PermissionChecker[i](member)) return i;
if(PermissionChecker[i](member)) return 0;
return i;
return 0;
} }
// Length Checking // Length Checking
(() => { (() => {
const lenNames = PermissionNames.length; const lenNames = PermissionNames.length;
const lenChecker = PermissionChecker.length; const lenChecker = PermissionChecker.length;
// By transitive property, lenNames and lenChecker have to be equal to each other as well. // By transitive property, lenNames and lenChecker have to be equal to each other as well.
if(length !== lenNames || length !== lenChecker) if (length !== lenNames || length !== lenChecker)
$.error(`Permission object lengths aren't equal! Enum Length (${length}), Names Length (${lenNames}), and Functions Length (${lenChecker}). This WILL cause problems!`); $.error(
})() `Permission object lengths aren't equal! Enum Length (${length}), Names Length (${lenNames}), and Functions Length (${lenChecker}). This WILL cause problems!`,
);
})();

View file

@ -1,84 +1,85 @@
import fs from "fs"; import fs from 'fs';
import $ from "./lib"; import $ from './lib';
const Storage = { const Storage = {
read(header: string): object read(header: string): object {
{ this.open('data');
this.open("data"); const path = `data/${header}.json`;
const path = `data/${header}.json`; let data = {};
let data = {};
if (fs.existsSync(path)) {
if(fs.existsSync(path)) const file = fs.readFileSync(path, 'utf-8');
{
const file = fs.readFileSync(path, "utf-8"); try {
data = JSON.parse(file);
try } catch (error) {
{ if (process.argv[2] !== 'dev') {
data = JSON.parse(file); $.warn(
} `Malformed JSON data (header: ${header}), backing it up.`,
catch(error) file,
{ );
if(process.argv[2] !== "dev") fs.writeFile(
{ `${path}.backup`,
$.warn(`Malformed JSON data (header: ${header}), backing it up.`, file); file,
fs.writeFile(`${path}.backup`, file, generateHandler(`Backup file of "${header}" successfully written as ${file}.`)); generateHandler(
} `Backup file of "${header}" successfully written as ${file}.`,
} ),
} );
}
return data; }
}, }
write(header: string, data: object, asynchronous = true)
{ return data;
this.open("data"); },
const path = `data/${header}.json`; write(header: string, data: object, asynchronous = true) {
this.open('data');
if(process.argv[2] === "dev" || header === "config") const path = `data/${header}.json`;
{
const result = JSON.stringify(data, null, '\t'); 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.`)); if (asynchronous)
else fs.writeFile(
fs.writeFileSync(path, result); path,
} result,
else generateHandler(`"${header}" sucessfully spaced and written.`),
{ );
const result = JSON.stringify(data); else fs.writeFileSync(path, result);
} else {
if(asynchronous) const result = JSON.stringify(data);
fs.writeFile(path, result, generateHandler(`"${header}" sucessfully written.`));
else if (asynchronous)
fs.writeFileSync(path, result); fs.writeFile(
} path,
}, result,
open(path: string, filter?: (value: string, index: number, array: string[]) => unknown): string[] generateHandler(`"${header}" sucessfully written.`),
{ );
if(!fs.existsSync(path)) else fs.writeFileSync(path, result);
fs.mkdirSync(path); }
},
let directory = fs.readdirSync(path); open(
path: string,
if(filter) filter?: (value: string, index: number, array: string[]) => unknown,
directory = directory.filter(filter); ): string[] {
if (!fs.existsSync(path)) fs.mkdirSync(path);
return directory;
}, let directory = fs.readdirSync(path);
close(path: string)
{ if (filter) directory = directory.filter(filter);
if(fs.existsSync(path) && fs.readdirSync(path).length === 0)
fs.rmdir(path, generateHandler(`"${path}" successfully closed.`)); return directory;
} },
close(path: string) {
if (fs.existsSync(path) && fs.readdirSync(path).length === 0)
fs.rmdir(path, generateHandler(`"${path}" successfully closed.`));
},
}; };
export function generateHandler(message: string) export function generateHandler(message: string) {
{ return (error: Error | null) => {
return (error: Error|null) => { if (error) $.error(error);
if(error) else $.debug(message);
$.error(error); };
else }
$.debug(message);
};
};
export default Storage; export default Storage;

View file

@ -1,123 +1,112 @@
import FileManager from "./storage"; import FileManager from './storage';
import $, {select, GenericJSON, GenericStructure} from "./lib"; import $, { select, GenericJSON, GenericStructure } from './lib';
import {watch} from "fs"; import { watch } from 'fs';
import {Guild as DiscordGuild} from "discord.js"; import { Guild as DiscordGuild } from 'discord.js';
class ConfigStructure extends GenericStructure class ConfigStructure extends GenericStructure {
{ public token: string;
public token: string; public prefix: string;
public prefix: string; public owner: string;
public owner: string; public admins: string[];
public admins: string[]; public support: string[];
public support: string[];
constructor(data: GenericJSON) {
constructor(data: GenericJSON) super('config');
{ this.token = select(data.token, '<ENTER YOUR TOKEN HERE>', String);
super("config"); this.prefix = select(data.prefix, '$', String);
this.token = select(data.token, "<ENTER YOUR TOKEN HERE>", String); this.owner = select(data.owner, '', String);
this.prefix = select(data.prefix, "$", String); this.admins = select(data.admins, [], String, true);
this.owner = select(data.owner, "", String); this.support = select(data.support, [], String, true);
this.admins = select(data.admins, [], String, true); }
this.support = select(data.support, [], String, true);
}
} }
class User class User {
{ public money: number;
public money: number; public lastReceived: number;
public lastReceived: number;
constructor(data?: GenericJSON) {
constructor(data?: GenericJSON) this.money = select(data?.money, 0, Number);
{ this.lastReceived = select(data?.lastReceived, -1, Number);
this.money = select(data?.money, 0, Number); }
this.lastReceived = select(data?.lastReceived, -1, Number);
}
} }
class Guild class Guild {
{ public prefix: string | null;
public prefix: string|null;
constructor(data?: GenericJSON) {
constructor(data?: GenericJSON) this.prefix = select(data?.prefix, null, String);
{ }
this.prefix = select(data?.prefix, null, String);
}
} }
class StorageStructure extends GenericStructure class StorageStructure extends GenericStructure {
{ public users: { [id: string]: User };
public users: {[id: string]: User}; public guilds: { [id: string]: Guild };
public guilds: {[id: string]: Guild};
constructor(data: GenericJSON) {
constructor(data: GenericJSON) super('storage');
{ this.users = {};
super("storage"); this.guilds = {};
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.users)
if(/\d{17,19}/g.test(id)) for (let id in data.guilds)
this.users[id] = new User(data.users[id]); if (/\d{17,19}/g.test(id)) this.guilds[id] = new Guild(data.guilds[id]);
}
for(let id in data.guilds)
if(/\d{17,19}/g.test(id)) /** Gets a user's profile if they exist and generate one if not. */
this.guilds[id] = new Guild(data.guilds[id]); public getUser(id: string): User {
} if (!/\d{17,19}/g.test(id))
$.warn(
/** Gets a user's profile if they exist and generate one if not. */ `"${id}" is not a valid user ID! It will be erased when the data loads again.`,
public getUser(id: string): User );
{
if(!/\d{17,19}/g.test(id)) if (id in this.users) return this.users[id];
$.warn(`"${id}" is not a valid user ID! It will be erased when the data loads again.`); else {
const user = new User();
if(id in this.users) this.users[id] = user;
return this.users[id]; return user;
else }
{ }
const user = new User();
this.users[id] = user; /** Gets a guild's settings if they exist and generate one if not. */
return user; 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.`,
/** Gets a guild's settings if they exist and generate one if not. */ );
public getGuild(id: string): Guild
{ if (id in this.guilds) return this.guilds[id];
if(!/\d{17,19}/g.test(id)) else {
$.warn(`"${id}" is not a valid guild ID! It will be erased when the data loads again.`); const guild = new Guild();
this.guilds[id] = guild;
if(id in this.guilds) return guild;
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. // 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 Config = new ConfigStructure(FileManager.read('config'));
export let Storage = new StorageStructure(FileManager.read("storage")); 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. // 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. // 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") if (process.argv[2] === 'dev') {
{ watch('data', (event, filename) => {
watch("data", (event, filename) => { $.debug('File Watcher:', event, filename);
$.debug("File Watcher:", event, filename); const header = filename.substring(0, filename.indexOf('.json'));
const header = filename.substring(0, filename.indexOf(".json"));
switch (header) {
switch(header) case 'config':
{ Config = new ConfigStructure(FileManager.read('config'));
case "config": Config = new ConfigStructure(FileManager.read("config")); break; break;
case "storage": Storage = new StorageStructure(FileManager.read("storage")); break; case 'storage':
} Storage = new StorageStructure(FileManager.read('storage'));
}); break;
}
});
} }
export function getPrefix(guild: DiscordGuild|null): string export function getPrefix(guild: DiscordGuild | null): string {
{ return Storage.getGuild(guild?.id || 'N/A').prefix ?? Config.prefix;
return Storage.getGuild(guild?.id || "N/A").prefix ?? Config.prefix; }
}

View file

@ -1,84 +1,87 @@
export class GenericWrapper<T> export class GenericWrapper<T> {
{ protected readonly value: T;
protected readonly value: T;
public constructor(value: T) {
public constructor(value: T) this.value = value;
{ }
this.value = value;
}
} }
export class NumberWrapper extends GenericWrapper<number> export class NumberWrapper extends GenericWrapper<number> {
{ /**
/** * Pluralises a word and chooses a suffix attached to the root provided.
* Pluralises a word and chooses a suffix attached to the root provided. * - pluralise("credit", "s") = credit/credits
* - pluralise("credit", "s") = credit/credits * - pluralise("part", "ies", "y") = party/parties
* - pluralise("part", "ies", "y") = party/parties * - pluralise("sheep") = sheep
* - pluralise("sheep") = sheep */
*/ public pluralise(
public pluralise(word: string, plural = "", singular = "", excludeNumber = false): string word: string,
{ plural = '',
let result = excludeNumber ? "" : `${this.value} `; singular = '',
excludeNumber = false,
if(this.value === 1) ): string {
result += word + singular; let result = excludeNumber ? '' : `${this.value} `;
else
result += word + plural; if (this.value === 1) result += word + singular;
else result += word + plural;
return result;
} return result;
}
/**
* Pluralises a word for changes. /**
* - (-1).pluraliseSigned() = '-1 credits' * Pluralises a word for changes.
* - (0).pluraliseSigned() = '+0 credits' * - (-1).pluraliseSigned() = '-1 credits'
* - (1).pluraliseSigned() = '+1 credit' * - (0).pluraliseSigned() = '+0 credits'
*/ * - (1).pluraliseSigned() = '+1 credit'
public pluraliseSigned(word: string, plural = "", singular = "", excludeNumber = false): string */
{ public pluraliseSigned(
const sign = this.value >= 0 ? '+' : ''; word: string,
return `${sign}${this.pluralise(word, plural, singular, excludeNumber)}`; plural = '',
} singular = '',
excludeNumber = false,
): string {
const sign = this.value >= 0 ? '+' : '';
return `${sign}${this.pluralise(word, plural, singular, excludeNumber)}`;
}
} }
export class StringWrapper extends GenericWrapper<string> export class StringWrapper extends GenericWrapper<string> {
{ public replaceAll(before: string, after: string): string {
public replaceAll(before: string, after: string): string let result = this.value;
{
let result = this.value; while (result.indexOf(before) !== -1)
result = result.replace(before, after);
while(result.indexOf(before) !== -1)
result = result.replace(before, after); return result;
}
return result;
} public toTitleCase(): string {
return this.value.replace(
public toTitleCase(): string /([^\W_]+[^\s-]*) */g,
{ (txt) => txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase(),
return this.value.replace(/([^\W_]+[^\s-]*) */g, txt => txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase()); );
} }
} }
export class ArrayWrapper<T> extends GenericWrapper<T[]> export class ArrayWrapper<T> extends GenericWrapper<T[]> {
{ /** Returns a random element from this array. */
/** Returns a random element from this array. */ public random(): T {
public random(): T return this.value[Math.floor(Math.random() * this.value.length)];
{ }
return this.value[Math.floor(Math.random() * this.value.length)];
} /**
* Splits up this array into a specified length.
/** * `$([1,2,3,4,5,6,7,8,9,10]).split(3)` = `[[1,2,3],[4,5,6],[7,8,9],[10]]`
* Splits up this array into a specified length. */
* `$([1,2,3,4,5,6,7,8,9,10]).split(3)` = `[[1,2,3],[4,5,6],[7,8,9],[10]]` public split(lengthOfEachSection: number): T[][] {
*/ const amountOfSections = Math.ceil(this.value.length / lengthOfEachSection);
public split(lengthOfEachSection: number): T[][] const sections: T[][] = new Array(amountOfSections);
{
const amountOfSections = Math.ceil(this.value.length / lengthOfEachSection); for (let index = 0; index < amountOfSections; index++)
const sections: T[][] = new Array(amountOfSections); sections[index] = this.value.slice(
index * lengthOfEachSection,
for(let index = 0; index < amountOfSections; index++) (index + 1) * lengthOfEachSection,
sections[index] = this.value.slice(index * lengthOfEachSection, (index + 1) * lengthOfEachSection); );
return sections; return sections;
} }
} }

View file

@ -1,45 +1,45 @@
// Flags a user can have. // Flags a user can have.
// They're basically your profile badges. // They're basically your profile badges.
export const flags: {[index: string]:any} = { export const flags: { [index: string]: any } = {
DISCORD_EMPLOYEE: 'Discord Employee', DISCORD_EMPLOYEE: 'Discord Employee',
DISCORD_PARTNER: 'Discord Partner', DISCORD_PARTNER: 'Discord Partner',
BUGHUNTER_LEVEL_1: 'Bug Hunter (Level 1)', BUGHUNTER_LEVEL_1: 'Bug Hunter (Level 1)',
BUGHUNTER_LEVEL_2: 'Bug Hunter (Level 2)', BUGHUNTER_LEVEL_2: 'Bug Hunter (Level 2)',
HYPESQUAD_EVENTS: 'HypeSquad Events', HYPESQUAD_EVENTS: 'HypeSquad Events',
HOUSE_BRAVERY: 'House of Bravery', HOUSE_BRAVERY: 'House of Bravery',
HOUSE_BRILLIANCE: 'House of Brilliance', HOUSE_BRILLIANCE: 'House of Brilliance',
HOUSE_BALANCE: 'House of Balance', HOUSE_BALANCE: 'House of Balance',
EARLY_SUPPORTER: 'Early Supporter', EARLY_SUPPORTER: 'Early Supporter',
TEAM_USER: 'Team User', TEAM_USER: 'Team User',
SYSTEM: 'System', SYSTEM: 'System',
VERIFIED_BOT: 'Verified Bot', VERIFIED_BOT: 'Verified Bot',
VERIFIED_DEVELOPER: 'Verified Bot Developer', VERIFIED_DEVELOPER: 'Verified Bot Developer',
}; };
export const filterLevels: {[index: string]:any} = { export const filterLevels: { [index: string]: any } = {
DISABLED: 'Off', DISABLED: 'Off',
MEMBERS_WITHOUT_ROLES: 'No Role', MEMBERS_WITHOUT_ROLES: 'No Role',
ALL_MEMBERS: 'Everyone', ALL_MEMBERS: 'Everyone',
}; };
export const verificationLevels: {[index: string]:any} = { export const verificationLevels: { [index: string]: any } = {
NONE: 'None', NONE: 'None',
LOW: 'Low', LOW: 'Low',
MEDIUM: 'Medium', MEDIUM: 'Medium',
HIGH: '(╯°□°)╯︵ ┻━┻', HIGH: '(╯°□°)╯︵ ┻━┻',
VERY_HIGH: '┻━┻ ミヽ(ಠ益ಠ)ノ彡┻━┻', VERY_HIGH: '┻━┻ ミヽ(ಠ益ಠ)ノ彡┻━┻',
};
export const regions: { [index: string]: any } = {
brazil: 'Brazil',
europe: 'Europe',
hongkong: 'Hong Kong',
india: 'India',
japan: 'Japan',
russia: 'Russia',
singapore: 'Singapore',
southafrica: 'South Africa',
sydney: 'Sydney',
'us-central': 'US Central',
'us-east': 'US East',
'us-west': 'US West',
'us-south': 'US South',
}; };
export const regions: {[index: string]:any} = {
brazil: 'Brazil',
europe: 'Europe',
hongkong: 'Hong Kong',
india: 'India',
japan: 'Japan',
russia: 'Russia',
singapore: 'Singapore',
southafrica: 'South Africa',
sydney: 'Sydney',
'us-central': 'US Central',
'us-east': 'US East',
'us-west': 'US West',
'us-south': 'US South',
};

View file

@ -1,109 +1,137 @@
import Event from "../core/event"; import Event from '../core/event';
import Command, {loadCommands} from "../core/command"; import Command, { loadCommands } from '../core/command';
import {hasPermission, getPermissionLevel, PermissionNames} from "../core/permissions"; import {
import {Permissions, Collection} from "discord.js"; hasPermission,
import {getPrefix} from "../core/structures"; getPermissionLevel,
import $ from "../core/lib"; PermissionNames,
} from '../core/permissions';
import { Permissions, Collection } from 'discord.js';
import { getPrefix } from '../core/structures';
import $ from '../core/lib';
// It's a rather hacky solution, but since there's no top-level await, I just have to make the loading conditional. // It's a rather hacky solution, but since there's no top-level await, I just have to make the loading conditional.
let commands: Collection<string, Command>|null = null; let commands: Collection<string, Command> | null = null;
export default new Event<"message">({ export default new Event<'message'>({
async on(message) async on(message) {
{ // Load commands if it hasn't already done so. Luckily, it's called once at most.
// Load commands if it hasn't already done so. Luckily, it's called once at most. if (!commands) commands = await loadCommands();
if(!commands)
commands = await loadCommands(); // Message Setup //
if (message.author.bot) return;
// Message Setup //
if(message.author.bot) const prefix = getPrefix(message.guild);
return;
if (!message.content.startsWith(prefix)) {
const prefix = getPrefix(message.guild); if (message.client.user && message.mentions.has(message.client.user))
message.channel.send(
if(!message.content.startsWith(prefix)) `${message.author.toString()}, my prefix on this guild is \`${prefix}\`.`,
{ );
if(message.client.user && message.mentions.has(message.client.user)) return;
message.channel.send(`${message.author.toString()}, my prefix on this guild is \`${prefix}\`.`); }
return;
} const [header, ...args] = message.content
.substring(prefix.length)
const [header, ...args] = message.content.substring(prefix.length).split(/ +/); .split(/ +/);
if(!commands.has(header)) if (!commands.has(header)) return;
return;
if (
if(message.channel.type === "text" && !message.channel.permissionsFor(message.client.user || "")?.has(Permissions.FLAGS.SEND_MESSAGES)) message.channel.type === 'text' &&
{ !message.channel
let status; .permissionsFor(message.client.user || '')
?.has(Permissions.FLAGS.SEND_MESSAGES)
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."; let status;
else
status = "Try using a different channel or contacting a server admin to change permissions of that channel if you think something's wrong."; if (message.member?.hasPermission(Permissions.FLAGS.ADMINISTRATOR))
status =
return message.author.send(`I don't have permission to send messages in ${message.channel.toString()}. ${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 =
$.log(`${message.author.username}#${message.author.discriminator} executed the command "${header}" with arguments "${args}".`); "Try using a different channel or contacting a server admin to change permissions of that channel if you think something's wrong.";
// Subcommand Recursion // return message.author.send(
let command = commands.get(header); `I don't have permission to send messages in ${message.channel.toString()}. ${status}`,
if(!command) return $.warn(`Command "${header}" was called but for some reason it's still undefined!`); );
const params: any[] = []; }
let isEndpoint = false;
let permLevel = command.permission ?? Command.PERMISSIONS.NONE; $.log(
`${message.author.username}#${message.author.discriminator} executed the command "${header}" with arguments "${args}".`,
for(let param of args) );
{
if(command.endpoint) // Subcommand Recursion //
{ let command = commands.get(header);
if(command.subcommands.size > 0 || command.user || command.number || command.any) if (!command)
$.warn(`An endpoint cannot have subcommands! Check ${prefix}${header} again.`); return $.warn(
isEndpoint = true; `Command "${header}" was called but for some reason it's still undefined!`,
break; );
} const params: any[] = [];
let isEndpoint = false;
const type = command.resolve(param); let permLevel = command.permission ?? Command.PERMISSIONS.NONE;
command = command.get(param);
permLevel = command.permission ?? permLevel; for (let param of args) {
if (command.endpoint) {
if(type === Command.TYPES.USER) if (
{ command.subcommands.size > 0 ||
const id = param.match(/\d+/g)![0]; command.user ||
try {params.push(await message.client.users.fetch(id))} command.number ||
catch(error) {return message.channel.send(`No user found by the ID \`${id}\`!`)} command.any
} )
else if(type === Command.TYPES.NUMBER) $.warn(
params.push(Number(param)); `An endpoint cannot have subcommands! Check ${prefix}${header} again.`,
else if(type !== Command.TYPES.SUBCOMMAND) );
params.push(param); isEndpoint = true;
} break;
}
if(!message.member)
return $.warn("This command was likely called from a DM channel meaning the member object is null."); const type = command.resolve(param);
command = command.get(param);
if(!hasPermission(message.member, permLevel)) permLevel = command.permission ?? permLevel;
{
const userPermLevel = getPermissionLevel(message.member); if (type === Command.TYPES.USER) {
return message.channel.send(`You don't have access to this command! Your permission level is \`${PermissionNames[userPermLevel]}\` (${userPermLevel}), but this command requires a permission level of \`${PermissionNames[permLevel]}\` (${permLevel}).`); const id = param.match(/\d+/g)![0];
} try {
params.push(await message.client.users.fetch(id));
if(isEndpoint) } catch (error) {
return message.channel.send("Too many arguments!"); return message.channel.send(`No user found by the ID \`${id}\`!`);
}
// Execute with dynamic library attached. // } else if (type === Command.TYPES.NUMBER) params.push(Number(param));
// The purpose of using $.bind($) is to clone the function so as to not modify the original $. else if (type !== Command.TYPES.SUBCOMMAND) params.push(param);
// 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($), { if (!message.member)
args: params, return $.warn(
author: message.author, 'This command was likely called from a DM channel meaning the member object is null.',
channel: message.channel, );
client: message.client,
guild: message.guild, if (!hasPermission(message.member, permLevel)) {
member: message.member, const userPermLevel = getPermissionLevel(message.member);
message: message return message.channel.send(
}, $)); `You don't have access to this command! Your permission level is \`${PermissionNames[userPermLevel]}\` (${userPermLevel}), but this command requires a permission level of \`${PermissionNames[permLevel]}\` (${permLevel}).`,
} );
}); }
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: message.client,
guild: message.guild,
member: message.member,
message: message,
},
$,
),
);
},
});

View file

@ -1,20 +1,24 @@
import Event from "../core/event"; import Event from '../core/event';
import {Permissions} from "discord.js"; import { Permissions } from 'discord.js';
import {botHasPermission} from "../core/lib"; import { botHasPermission } from '../core/lib';
// A list of message ID and callback pairs. You get the emote name and ID of the user reacting. // A list of message ID and callback pairs. You get the emote name and ID of the user reacting.
export const eventListeners: Map<string, (emote: string, id: string) => void> = new Map(); export const eventListeners: Map<
string,
(emote: string, id: string) => void
> = new Map();
// Attached to the client, there can be one event listener attached to a message ID which is executed if present. // Attached to the client, there can be one event listener attached to a message ID which is executed if present.
export default new Event<"messageReactionRemove">({ export default new Event<'messageReactionRemove'>({
on(reaction, user) on(reaction, user) {
{ const canDeleteEmotes = botHasPermission(
const canDeleteEmotes = botHasPermission(reaction.message.guild, Permissions.FLAGS.MANAGE_MESSAGES); reaction.message.guild,
Permissions.FLAGS.MANAGE_MESSAGES,
if(!canDeleteEmotes) );
{
const callback = eventListeners.get(reaction.message.id); if (!canDeleteEmotes) {
callback && callback(reaction.emoji.name, user.id); const callback = eventListeners.get(reaction.message.id);
} callback && callback(reaction.emoji.name, user.id);
} }
}); },
});

View file

@ -1,18 +1,18 @@
import Event from "../core/event"; import Event from '../core/event';
import {client} from "../index"; import { client } from '../index';
import $ from "../core/lib"; import $ from '../core/lib';
import {Config} from "../core/structures"; import { Config } from '../core/structures';
export default new Event<"ready">({ export default new Event<'ready'>({
once() once() {
{ if (client.user) {
if(client.user) $.ready(
{ `Logged in as ${client.user.username}#${client.user.discriminator}.`,
$.ready(`Logged in as ${client.user.username}#${client.user.discriminator}.`); );
client.user.setActivity({ client.user.setActivity({
type: "LISTENING", type: 'LISTENING',
name: `${Config.prefix}help` name: `${Config.prefix}help`,
}); });
} }
} },
}); });

View file

@ -1,8 +1,8 @@
import {Client} from "discord.js"; import { Client } from 'discord.js';
import setup from "./setup"; import setup from './setup';
import {Config} from "./core/structures"; import { Config } from './core/structures';
import {loadCommands} from "./core/command"; import { loadCommands } from './core/command';
import {loadEvents} from "./core/event"; import { loadEvents } from './core/event';
// This is here in order to make it much less of a headache to access the client from other files. // This is here in order to make it much less of a headache to access the client from other files.
// This of course won't actually do anything until the setup process is complete and it logs in. // This of course won't actually do anything until the setup process is complete and it logs in.
@ -10,7 +10,7 @@ export const client = new Client();
// Begin the command loading here rather than when it's needed like in the message event. // Begin the command loading here rather than when it's needed like in the message event.
setup.init().then(() => { setup.init().then(() => {
loadCommands(); loadCommands();
loadEvents(client); loadEvents(client);
client.login(Config.token).catch(setup.again); client.login(Config.token).catch(setup.again);
}); });

View file

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

View file

@ -1,69 +1,111 @@
import {strict as assert} from "assert"; import { strict as assert } from 'assert';
import {NumberWrapper, StringWrapper, ArrayWrapper} from "../src/core/wrappers"; import {
NumberWrapper,
StringWrapper,
ArrayWrapper,
} from '../src/core/wrappers';
// I can't figure out a way to run the test suite while running the bot. // I can't figure out a way to run the test suite while running the bot.
describe("Wrappers", () => { describe('Wrappers', () => {
describe("NumberWrapper", () => { describe('NumberWrapper', () => {
describe("#pluralise()", () => { describe('#pluralise()', () => {
it('should return "5 credits"', () => { it('should return "5 credits"', () => {
assert.strictEqual(new NumberWrapper(5).pluralise("credit", "s"), "5 credits"); assert.strictEqual(
}) new NumberWrapper(5).pluralise('credit', 's'),
'5 credits',
it('should return "1 credit"', () => { );
assert.strictEqual(new NumberWrapper(1).pluralise("credit", "s"), "1 credit"); });
})
it('should return "1 credit"', () => {
it('should return "-1 credits"', () => { assert.strictEqual(
assert.strictEqual(new NumberWrapper(-1).pluralise("credit", "s"), "-1 credits"); new NumberWrapper(1).pluralise('credit', 's'),
}) '1 credit',
);
it('should be able to work with a plural suffix', () => { });
assert.strictEqual(new NumberWrapper(2).pluralise("part", "ies", "y"), "2 parties");
}) it('should return "-1 credits"', () => {
assert.strictEqual(
it('should be able to work with a singular suffix', () => { new NumberWrapper(-1).pluralise('credit', 's'),
assert.strictEqual(new NumberWrapper(1).pluralise("part", "ies", "y"), "1 party"); '-1 credits',
}) );
});
it('should be able to exclude the number', () => {
assert.strictEqual(new NumberWrapper(1).pluralise("credit", "s", "", true), "credit"); it('should be able to work with a plural suffix', () => {
}) assert.strictEqual(
}) new NumberWrapper(2).pluralise('part', 'ies', 'y'),
'2 parties',
describe("#pluraliseSigned()", () => { );
it('should return "-1 credits"', () => { });
assert.strictEqual(new NumberWrapper(-1).pluraliseSigned("credit", "s"), "-1 credits");
}) it('should be able to work with a singular suffix', () => {
assert.strictEqual(
it('should return "+0 credits"', () => { new NumberWrapper(1).pluralise('part', 'ies', 'y'),
assert.strictEqual(new NumberWrapper(0).pluraliseSigned("credit", "s"), "+0 credits"); '1 party',
}) );
});
it('should return "+1 credit"', () => {
assert.strictEqual(new NumberWrapper(1).pluraliseSigned("credit", "s"), "+1 credit"); it('should be able to exclude the number', () => {
}) assert.strictEqual(
}) new NumberWrapper(1).pluralise('credit', 's', '', true),
}) 'credit',
);
describe("StringWrapper", () => { });
describe("#replaceAll()", () => { });
it('should convert "test" to "zesz"', () => {
assert.strictEqual(new StringWrapper("test").replaceAll('t', 'z'), "zesz"); describe('#pluraliseSigned()', () => {
}) it('should return "-1 credits"', () => {
}) assert.strictEqual(
new NumberWrapper(-1).pluraliseSigned('credit', 's'),
describe("#toTitleCase()", () => { '-1 credits',
it('should capitalize the first letter of each word', () => { );
assert.strictEqual(new StringWrapper("yeetus deletus find salvation from jesus").toTitleCase(), "Yeetus Deletus Find Salvation From Jesus"); });
})
}) it('should return "+0 credits"', () => {
}) assert.strictEqual(
new NumberWrapper(0).pluraliseSigned('credit', 's'),
describe("ArrayWrapper", () => { '+0 credits',
describe("#split()", () => { );
it('should split [1,2,3,4,5,6,7,8,9,10] into [[1,2,3],[4,5,6],[7,8,9],[10]]', () => { });
assert.deepStrictEqual(new ArrayWrapper([1,2,3,4,5,6,7,8,9,10]).split(3), [[1,2,3],[4,5,6],[7,8,9],[10]]);
}) it('should return "+1 credit"', () => {
}) assert.strictEqual(
}) new NumberWrapper(1).pluraliseSigned('credit', 's'),
}) '+1 credit',
);
});
});
});
describe('StringWrapper', () => {
describe('#replaceAll()', () => {
it('should convert "test" to "zesz"', () => {
assert.strictEqual(
new StringWrapper('test').replaceAll('t', 'z'),
'zesz',
);
});
});
describe('#toTitleCase()', () => {
it('should capitalize the first letter of each word', () => {
assert.strictEqual(
new StringWrapper(
'yeetus deletus find salvation from jesus',
).toTitleCase(),
'Yeetus Deletus Find Salvation From Jesus',
);
});
});
});
describe('ArrayWrapper', () => {
describe('#split()', () => {
it('should split [1,2,3,4,5,6,7,8,9,10] into [[1,2,3],[4,5,6],[7,8,9],[10]]', () => {
assert.deepStrictEqual(
new ArrayWrapper([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]).split(3),
[[1, 2, 3], [4, 5, 6], [7, 8, 9], [10]],
);
});
});
});
});

View file

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