Added documentation + misc patches
This commit is contained in:
parent
3767a10829
commit
b3209d1cf1
|
@ -0,0 +1,234 @@
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
# 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';
|
||||||
|
|
||||||
|
export default new Command({
|
||||||
|
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";
|
||||||
|
|
||||||
|
export default new Command({
|
||||||
|
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';
|
||||||
|
|
||||||
|
export default 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';
|
||||||
|
|
||||||
|
export default new Command({
|
||||||
|
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';
|
||||||
|
|
||||||
|
export default new Command({
|
||||||
|
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';
|
||||||
|
|
||||||
|
export default new Command({
|
||||||
|
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';
|
||||||
|
|
||||||
|
export default new Command({
|
||||||
|
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";
|
||||||
|
|
||||||
|
const cmd = new Command({
|
||||||
|
description: "Handle your money."
|
||||||
|
});
|
||||||
|
cmd.subcommands.set("pay", pay);
|
||||||
|
cmd.run = async($: CommonLibrary): Promise<any> {
|
||||||
|
$.debug($.args);
|
||||||
|
};
|
||||||
|
cmd.any = new Command({
|
||||||
|
//...
|
||||||
|
});
|
||||||
|
|
||||||
|
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(...)`
|
||||||
|
- `$.debug(...)`
|
||||||
|
- `$.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 msg = await $.channel.send(pages[0]);
|
||||||
|
|
||||||
|
$.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?");
|
||||||
|
|
||||||
|
$.prompt(msg, $.author.id, () => {
|
||||||
|
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}.`);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## 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`
|
||||||
|
- `$.channel`: `message.channel`
|
||||||
|
- `$.guild`: `message.guild`
|
||||||
|
- `$.author`: `message.author`
|
||||||
|
- `$.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.
|
||||||
|
- `parseArgs()`: Turns `call test "args with spaces" "even more spaces"` into `["call", "test", "args with spaces", "even more spaces"]`, inspired by the command line.
|
||||||
|
- `parseVars()`: Replaces all `%` args in a string with stuff you specify. For example, you can replace all `nop` with `asm`, and `register %nop%` will turn into `register asm`. Useful for storing strings with variables in one place them accessing them in another place.
|
||||||
|
- `isType()`: Used for type-checking. Useful for testing `any` types.
|
||||||
|
- `select()`: Checks if a variable matches a certain type and uses the fallback value if not. (Warning: Type checking is based on the fallback's type. Be sure that the "type" parameter is accurate to this!)
|
||||||
|
- `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.
|
|
@ -6,7 +6,7 @@
|
||||||
# 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](Specifications.md).
|
3. Familiarize yourself with the [project's structure](Documentation.md).
|
||||||
4. Make sure to avoid using `npm run build`! This will remove all your dev dependencies (in order to reduce space used). Instead, use `npm run once` to compile and build in non-dev mode.
|
4. Make sure to avoid using `npm run build`! This will remove all your dev dependencies (in order to reduce space used). Instead, use `npm run once` to compile and build in non-dev mode.
|
||||||
5. Begin developing.
|
5. Begin developing.
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
import Command from "../core/command";
|
import Command from "../core/command";
|
||||||
import {CommonLibrary, logs} 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 {botHasPermission} from "../index";
|
|
||||||
import {Permissions} from "discord.js";
|
import {Permissions} from "discord.js";
|
||||||
|
|
||||||
function getLogBuffer(type: string)
|
function getLogBuffer(type: string)
|
||||||
|
|
|
@ -12,7 +12,7 @@ interface CommandOptions
|
||||||
usage?: string;
|
usage?: string;
|
||||||
permission?: PERMISSIONS|null;
|
permission?: PERMISSIONS|null;
|
||||||
aliases?: string[];
|
aliases?: string[];
|
||||||
run?: Function|string;
|
run?: (($: CommonLibrary) => Promise<any>)|string;
|
||||||
subcommands?: {[key: string]: Command};
|
subcommands?: {[key: string]: Command};
|
||||||
user?: Command;
|
user?: Command;
|
||||||
number?: Command;
|
number?: Command;
|
||||||
|
@ -29,7 +29,7 @@ export default class Command
|
||||||
public readonly permission: PERMISSIONS|null;
|
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 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 originalCommandName: string|null; // If the command is an alias, what's the original name?
|
||||||
private run: Function|string;
|
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 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 user: Command|null;
|
||||||
public number: Command|null;
|
public number: Command|null;
|
||||||
|
|
|
@ -3,7 +3,7 @@ import {Client, Message, TextChannel, DMChannel, NewsChannel, Guild, User, Guild
|
||||||
import chalk from "chalk";
|
import chalk from "chalk";
|
||||||
import FileManager from "./storage";
|
import FileManager from "./storage";
|
||||||
import {eventListeners} from "../events/messageReactionRemove";
|
import {eventListeners} from "../events/messageReactionRemove";
|
||||||
import {botHasPermission} from "../index";
|
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
|
||||||
|
@ -146,6 +146,11 @@ export function formatUTCTimestamp(now = new Date())
|
||||||
return `${year}-${month}-${day} ${hour}:${minute}:${second}`;
|
return `${year}-${month}-${day} ${hour}:${minute}:${second}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function botHasPermission(guild: Guild|null, 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(message: Message, senderID: string, total: number, callback: (page: number) => void, duration = 60000) => {
|
||||||
|
|
|
@ -11,26 +11,26 @@ const PermissionChecker: ((member: GuildMember) => boolean)[] = [
|
||||||
() => true,
|
() => true,
|
||||||
|
|
||||||
// MOD //
|
// MOD //
|
||||||
(member: GuildMember) =>
|
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: GuildMember) => member.hasPermission(Permissions.FLAGS.ADMINISTRATOR),
|
member => member.hasPermission(Permissions.FLAGS.ADMINISTRATOR),
|
||||||
|
|
||||||
// OWNER //
|
// OWNER //
|
||||||
(member: GuildMember) => member.guild.ownerID === member.id,
|
member => member.guild.ownerID === member.id,
|
||||||
|
|
||||||
// BOT_SUPPORT //
|
// BOT_SUPPORT //
|
||||||
(member: GuildMember) => Config.support.includes(member.id),
|
member => Config.support.includes(member.id),
|
||||||
|
|
||||||
// BOT_ADMIN //
|
// BOT_ADMIN //
|
||||||
(member: GuildMember) => Config.admins.includes(member.id),
|
member => Config.admins.includes(member.id),
|
||||||
|
|
||||||
// BOT_OWNER //
|
// BOT_OWNER //
|
||||||
(member: GuildMember) => 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.
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import Event from "../core/event";
|
import Event from "../core/event";
|
||||||
import {Permissions} from "discord.js";
|
import {Permissions} from "discord.js";
|
||||||
import {botHasPermission} from "../index";
|
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();
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import {Client, Guild} 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";
|
||||||
|
@ -13,9 +13,4 @@ setup.init().then(() => {
|
||||||
loadCommands();
|
loadCommands();
|
||||||
loadEvents(client);
|
loadEvents(client);
|
||||||
client.login(Config.token).catch(setup.again);
|
client.login(Config.token).catch(setup.again);
|
||||||
});
|
});
|
||||||
|
|
||||||
export function botHasPermission(guild: Guild|null, permission: number): boolean
|
|
||||||
{
|
|
||||||
return !!(client.user && guild?.members.resolve(client.user)?.hasPermission(permission))
|
|
||||||
}
|
|
Loading…
Reference in New Issue