mirror of
https://github.com/keanuplayz/TravBot-v3.git
synced 2024-08-15 02:33:12 +00:00
Added husky.
This commit is contained in:
parent
57433cc594
commit
74b4d4272c
36 changed files with 2677 additions and 2226 deletions
|
@ -1,128 +1,158 @@
|
|||
# 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.
|
||||
|
||||
# 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.
|
||||
- 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
|
||||
|
||||
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
|
||||
|
||||
- `commands` contains all the commands.
|
||||
- `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.
|
||||
- `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
|
||||
|
||||
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
|
||||
import Command from '../core/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.
|
||||
|
||||
- `%author%` pings the person who sent the message.
|
||||
- `%prefix%` gets the bot's current prefix in the selected server.
|
||||
|
||||
```js
|
||||
import Command from '../core/command';
|
||||
import {CommonLibrary} from '../core/lib';
|
||||
import { CommonLibrary } from '../core/lib';
|
||||
|
||||
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...
|
||||
|
||||
```js
|
||||
import Command from '../core/command';
|
||||
import {CommonLibrary} from '../core/lib';
|
||||
import {getPrefix} from "../core/structures";
|
||||
import { CommonLibrary } from '../core/lib';
|
||||
import { getPrefix } from '../core/structures';
|
||||
|
||||
export default new Command({
|
||||
async run($: CommonLibrary): Promise<any> {
|
||||
$.channel.send(`${$.author.toString()}, make sure to use the prefix! (${getPrefix($.guild)})`);
|
||||
}
|
||||
async run($: CommonLibrary): Promise<any> {
|
||||
$.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.
|
||||
|
||||
- `subcommands` is used for specific keywords for accessing a certain command. For example, `$eco pay` has a subcommand of `pay`.
|
||||
|
||||
```js
|
||||
import Command from '../core/command';
|
||||
import {CommonLibrary} from '../core/lib';
|
||||
import { CommonLibrary } from '../core/lib';
|
||||
|
||||
export default new Command({
|
||||
subcommands: {
|
||||
pay: new Command({
|
||||
//...
|
||||
})
|
||||
}
|
||||
subcommands: {
|
||||
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.
|
||||
|
||||
```js
|
||||
import Command from '../core/command';
|
||||
import {CommonLibrary} from '../core/lib';
|
||||
import { CommonLibrary } from '../core/lib';
|
||||
|
||||
export default new Command({
|
||||
user: new Command({
|
||||
async run($: CommonLibrary): Promise<any> {
|
||||
$.debug($.args[0].username); // "WatDuhHekBro"
|
||||
}
|
||||
})
|
||||
user: new Command({
|
||||
async run($: CommonLibrary): Promise<any> {
|
||||
$.debug($.args[0].username); // "WatDuhHekBro"
|
||||
},
|
||||
}),
|
||||
});
|
||||
```
|
||||
|
||||
There's also `number` which checks for any number type except `Infinity`, converting the argument to a `number` type.
|
||||
|
||||
```js
|
||||
import Command from '../core/command';
|
||||
import {CommonLibrary} from '../core/lib';
|
||||
import { CommonLibrary } from '../core/lib';
|
||||
|
||||
export default new Command({
|
||||
number: new Command({
|
||||
async run($: CommonLibrary): Promise<any> {
|
||||
$.debug($.args[0] + 5);
|
||||
}
|
||||
})
|
||||
number: new Command({
|
||||
async run($: CommonLibrary): Promise<any> {
|
||||
$.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`.
|
||||
|
||||
```js
|
||||
import Command from '../core/command';
|
||||
import {CommonLibrary} from '../core/lib';
|
||||
import { CommonLibrary } from '../core/lib';
|
||||
|
||||
export default new Command({
|
||||
any: new Command({
|
||||
async run($: CommonLibrary): Promise<any> {
|
||||
$.debug($.args[0].toUpperCase());
|
||||
}
|
||||
})
|
||||
any: new Command({
|
||||
async run($: CommonLibrary): Promise<any> {
|
||||
$.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`.
|
||||
|
||||
## Other Properties
|
||||
|
||||
- `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`.
|
||||
- `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
|
||||
import Command from '../core/command';
|
||||
import {CommonLibrary} from '../core/lib';
|
||||
import { CommonLibrary } from '../core/lib';
|
||||
|
||||
export default new Command({
|
||||
subcommands: {
|
||||
pay: new Command({
|
||||
usage: "<user> <amount>"
|
||||
})
|
||||
}
|
||||
subcommands: {
|
||||
pay: new Command({
|
||||
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`.
|
||||
- `aliases`: A list of aliases (if any).
|
||||
|
||||
## 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.
|
||||
|
||||
```js
|
||||
import pay from "./subcommands/pay";
|
||||
|
||||
|
@ -141,19 +171,24 @@ export default cmd;
|
|||
```
|
||||
|
||||
## 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:
|
||||
|
||||
- `$.channel.send("").catch($.handler.bind($))`
|
||||
- `$.channel.send("").catch(error => $.handler(error))`
|
||||
|
||||
# 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.
|
||||
|
||||
## Custom Wrappers
|
||||
|
||||
- `$(5)` = `new NumberWrapper(5)`
|
||||
- `$("text")` = `new StringWrapper("text")`
|
||||
- `$([1,2,3])` = `new ArrayWrapper([1,2,3])`
|
||||
|
||||
## Custom Logger
|
||||
|
||||
- `$.log(...)`
|
||||
- `$.warn(...)`
|
||||
- `$.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)
|
||||
|
||||
## Convenience Functions
|
||||
|
||||
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.
|
||||
|
||||
```js
|
||||
const pages = ["one", "two", "three"];
|
||||
const pages = ['one', 'two', 'three'];
|
||||
const msg = await $.channel.send(pages[0]);
|
||||
|
||||
$.paginate(msg, $.author.id, pages.length, page => {
|
||||
msg.edit(pages[page]);
|
||||
$.paginate(msg, $.author.id, pages.length, (page) => {
|
||||
msg.edit(pages[page]);
|
||||
});
|
||||
```
|
||||
|
||||
- `$.prompt()`: Prompts the user about a decision before following through.
|
||||
|
||||
```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, () => {
|
||||
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.
|
||||
- `$.callMemberByUsername()`: Convenience function to handle cases where someone isn't found by a username automatically.
|
||||
|
||||
```js
|
||||
$.callMemberByUsername($.message, $.args.join(" "), member => {
|
||||
$.channel.send(`Your nickname is ${member.nickname}.`);
|
||||
$.callMemberByUsername($.message, $.args.join(' '), (member) => {
|
||||
$.channel.send(`Your nickname is ${member.nickname}.`);
|
||||
});
|
||||
```
|
||||
|
||||
## Dynamic Properties
|
||||
|
||||
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.
|
||||
- `$.client`: `message.client`
|
||||
- `$.message`: `message`
|
||||
|
@ -198,22 +242,28 @@ These will be accessible only inside a `Command` and will change per message.
|
|||
- `$.member`: `message.member`
|
||||
|
||||
# Wrappers
|
||||
|
||||
This is similar to modifying a primitive object's `prototype` without actually doing so.
|
||||
|
||||
## 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.
|
||||
- `.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
|
||||
|
||||
- `.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"`.
|
||||
|
||||
## ArrayWrapper
|
||||
|
||||
- `.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]]`.
|
||||
|
||||
# Other Library Functions
|
||||
|
||||
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`
|
||||
- `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.
|
||||
|
@ -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`.
|
||||
|
||||
# Other Core Functions
|
||||
|
||||
- `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.
|
||||
- `structures::getPrefix()`: Get the current prefix of the guild or the bot's prefix if none is found.
|
||||
|
||||
# The other core files
|
||||
|
||||
- `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/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.
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
# Getting Started
|
||||
|
||||
1. `npm install`
|
||||
2. `npm run build`
|
||||
3. `npm start`
|
||||
|
||||
# Getting Started (Developers)
|
||||
|
||||
1. `npm install`
|
||||
2. `npm run dev`
|
||||
3. Familiarize yourself with the [project's structure](Documentation.md).
|
||||
|
@ -11,6 +13,7 @@
|
|||
5. Begin developing.
|
||||
|
||||
## Don't forget to...
|
||||
|
||||
- ...update the [changelog](CHANGELOG.md) and any other necessary docs.
|
||||
- ...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`.
|
||||
|
|
|
@ -1,13 +1,15 @@
|
|||
# Structure
|
||||
|
||||
The top-level directory is reserved for files that have to be there for it to work as well as configuration files.
|
||||
|
||||
- `src`: Contains all the code for the bot itself. Code in this directory is for independent tasks keeping the initialization out of the subdirectories.
|
||||
- `core`: This is where core structures and critical functions for the bot go.
|
||||
- `modules`: This is where modules go that accomplish one specific purpose but isn't so necessary for the bot to function. The goal is to be able to safely remove these without too much trouble.
|
||||
- `commands`: Here's the place to store commands. The file name determines the command name.
|
||||
- `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.
|
||||
- `<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.
|
||||
- `core`: This is where core structures and critical functions for the bot go.
|
||||
- `modules`: This is where modules go that accomplish one specific purpose but isn't so necessary for the bot to function. The goal is to be able to safely remove these without too much trouble.
|
||||
- `commands`: Here's the place to store commands. The file name determines the command name.
|
||||
- `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.
|
||||
- `<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.
|
||||
- `dist`: This is where the runnable code in `src` compiles to. (The directory structure mirrors `src`.)
|
||||
- `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.
|
||||
|
@ -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.
|
||||
|
||||
# Specific Files
|
||||
|
||||
This list starts from `src`/`dist`.
|
||||
|
||||
- `index`: This is the entry point of the bot. Here is where all the initialization is done, because the idea is to keep repeatable code in separate modules while having code that runs only once here designating this is **the** starting point.
|
||||
- `setup`: Used for the first time the bot is loaded, walking the user through setting up the bot.
|
||||
- `core/lib`: Exports a function object which lets you wrap values letting you call special functions as well as calling utility functions common to all commands.
|
||||
|
@ -27,6 +31,7 @@ This list starts from `src`/`dist`.
|
|||
- `core/permissions`: The file containing everything related to permissions.
|
||||
|
||||
# Design Decisions
|
||||
|
||||
- All top-level files (relative to `src`/`dist`) should ideally be independent, one-time use scripts. This helps separate code that just initializes once and reusable code that forms the bulk of the main program itself. That's why all the file searching and loading commands/events will be done in `index`.
|
||||
- Wrapper objects were designed with the idea of letting you assign functions directly to native objects [without the baggage of actually doing so](https://developer.mozilla.org/en-US/docs/Web/JavaScript/The_performance_hazards_of__%5B%5BPrototype%5D%5D_mutation).
|
||||
- `test` should be a keyword for any file not tracked by git and generally be more flexible to play around with. It should also be automatically generated during initialization in `commands` so you can have templates ready for new commands.
|
||||
|
@ -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.
|
||||
- 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.
|
||||
- Two properties on the `Command` class, `aliases: string[]` and `subaliases: {[key: string]: string[]}`.
|
||||
- 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`.
|
||||
- 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).
|
||||
- Two properties on the `Command` class, `aliases: string[]` and `subaliases: {[key: string]: string[]}`.
|
||||
- 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`.
|
||||
- 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).
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue