Feature/66 add different commands per server (#122)
* Add ability for server exclusive commands * Add MankBot server-exclusive commands * Add lobby entity to database * Add documentation
This commit is contained in:
parent
6a00c49ef3
commit
1b1a070cfd
10 changed files with 247 additions and 135 deletions
50
README.md
50
README.md
|
@ -1,6 +1,6 @@
|
|||
# VylBot App
|
||||
|
||||
Discord bot for Vylpes' Den Discord Server. Based on [VylBot Core](https://github.com/getgravitysoft/vylbot-core).
|
||||
Discord bot for Vylpes' Den Discord Server.
|
||||
|
||||
## Installation
|
||||
|
||||
|
@ -8,16 +8,48 @@ Download the latest version from the [releases page](https://github.com/Vylpes/v
|
|||
|
||||
Copy the config template file and fill in the strings.
|
||||
|
||||
## Requirements
|
||||
|
||||
- NodeJS v16
|
||||
- Yarn
|
||||
|
||||
## Usage
|
||||
|
||||
Implement the client using something like:
|
||||
Install the dependencies and build the app:
|
||||
|
||||
```js
|
||||
const vylbot = require('vylbot-core');
|
||||
const config = require('./config.json');
|
||||
|
||||
const client = new vylbot.client(config);
|
||||
client.start();
|
||||
```bash
|
||||
yarn install
|
||||
yarn build
|
||||
```
|
||||
|
||||
See the `docs` folder for more information on how to use vylbot-core
|
||||
Setup the database (Recommended to use the docker-compose file)
|
||||
|
||||
```bash
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
Copy and edit the settings files
|
||||
|
||||
```bash
|
||||
cp .env.template .env
|
||||
# Edit the .env file
|
||||
|
||||
cp ormconfig.json.template ormconfig.json
|
||||
# Edit the ormconfig.json file
|
||||
```
|
||||
|
||||
> **NOTE:** Make sure you do *not* check in these files! These contain sensitive information and should be treated as private.
|
||||
|
||||
Start the bot
|
||||
|
||||
```bash
|
||||
yarn start
|
||||
```
|
||||
|
||||
Alternatively, you can start the bot in development mode using:
|
||||
|
||||
```bash
|
||||
yarn start --dev
|
||||
```
|
||||
|
||||
> Dev mode ensures that the default prefix is different to the production mode, in case you have both running in the same server.
|
31
docs/Registry.md
Normal file
31
docs/Registry.md
Normal file
|
@ -0,0 +1,31 @@
|
|||
# Registry
|
||||
|
||||
The registry file is what is used to register the bot's commands and events. This is a script which is ran at startup and adds all the commands and events to the bot.
|
||||
|
||||
Although you can register these outside of the registry file, this script makes it a centralised place for it to be done at.
|
||||
|
||||
## Adding Commands
|
||||
|
||||
Commands are added in the `RegisterCommands` function.
|
||||
|
||||
The basic syntax is as follows:
|
||||
|
||||
```ts
|
||||
client.RegisterCommand("Name", new Command(), "ServerId");
|
||||
```
|
||||
|
||||
- `"Name"`: The name of the command, will be used by the user to call the command
|
||||
- `new Command()`: The command class to be executed, must inherit the Command class
|
||||
- `"ServerId"` (Optional): If given, will only be usable in that specific server
|
||||
|
||||
## Adding Events
|
||||
|
||||
Events are added in the `RegisterEvents` function.
|
||||
|
||||
The basic syntax is as follows:
|
||||
|
||||
```ts
|
||||
client.RegisterEvent(new Events());
|
||||
```
|
||||
|
||||
- `new Events()`: The event class to be executed
|
|
@ -57,10 +57,11 @@ export class CoreClient extends Client {
|
|||
this._util.loadEvents(this, this._eventItems);
|
||||
}
|
||||
|
||||
public RegisterCommand(name: string, command: Command) {
|
||||
public RegisterCommand(name: string, command: Command, serverId?: string) {
|
||||
const item: ICommandItem = {
|
||||
Name: name,
|
||||
Command: command,
|
||||
ServerId: serverId,
|
||||
};
|
||||
|
||||
this._commandItems.push(item);
|
||||
|
|
|
@ -1,18 +1,8 @@
|
|||
import { Message } from "discord.js";
|
||||
import { IBaseResponse } from "../contracts/IBaseResponse";
|
||||
import ICommandItem from "../contracts/ICommandItem";
|
||||
import SettingsHelper from "../helpers/SettingsHelper";
|
||||
import { Util } from "./util";
|
||||
|
||||
export interface IEventResponse extends IBaseResponse {
|
||||
context?: {
|
||||
prefix: string;
|
||||
name: string;
|
||||
args: string[];
|
||||
message: Message;
|
||||
}
|
||||
}
|
||||
|
||||
export class Events {
|
||||
private _util: Util;
|
||||
|
||||
|
@ -22,58 +12,21 @@ export class Events {
|
|||
|
||||
// Emit when a message is sent
|
||||
// Used to check for commands
|
||||
public async onMessage(message: Message, commands: ICommandItem[]): Promise<IEventResponse> {
|
||||
if (!message.guild) return {
|
||||
valid: false,
|
||||
message: "Message was not sent in a guild, ignoring.",
|
||||
};
|
||||
|
||||
if (message.author.bot) return {
|
||||
valid: false,
|
||||
message: "Message was sent by a bot, ignoring.",
|
||||
};
|
||||
public async onMessage(message: Message, commands: ICommandItem[]) {
|
||||
if (!message.guild) return;
|
||||
if (message.author.bot) return;
|
||||
|
||||
const prefix = await SettingsHelper.GetSetting("bot.prefix", message.guild.id);
|
||||
|
||||
if (!prefix) {
|
||||
return {
|
||||
valid: false,
|
||||
message: "Prefix not found",
|
||||
};
|
||||
}
|
||||
if (!prefix) return;
|
||||
|
||||
if (message.content.substring(0, prefix.length).toLowerCase() == prefix.toLowerCase()) {
|
||||
const args = message.content.substring(prefix.length).split(" ");
|
||||
const name = args.shift();
|
||||
|
||||
if (!name) return {
|
||||
valid: false,
|
||||
message: "Command name was not found",
|
||||
};
|
||||
if (!name) return;
|
||||
|
||||
const res = await this._util.loadCommand(name, args, message, commands);
|
||||
|
||||
if (!res.valid) {
|
||||
return {
|
||||
valid: false,
|
||||
message: res.message,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
valid: true,
|
||||
context: {
|
||||
prefix: prefix,
|
||||
name: name,
|
||||
args: args,
|
||||
message: message,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
message: "Message was not a command, ignoring.",
|
||||
await this._util.loadCommand(name, args, message, commands);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,9 +1,5 @@
|
|||
// Required Components
|
||||
import { Client, Message } from "discord.js";
|
||||
import { readdirSync, existsSync } from "fs";
|
||||
import { IBaseResponse } from "../contracts/IBaseResponse";
|
||||
import { Command } from "../type/command";
|
||||
import { Event } from "../type/event";
|
||||
import { ICommandContext } from "../contracts/ICommandContext";
|
||||
import ICommandItem from "../contracts/ICommandItem";
|
||||
import IEventItem from "../contracts/IEventItem";
|
||||
|
@ -12,53 +8,37 @@ import StringTools from "../helpers/StringTools";
|
|||
import { CommandResponse } from "../constants/CommandResponse";
|
||||
import ErrorMessages from "../constants/ErrorMessages";
|
||||
|
||||
export interface IUtilResponse extends IBaseResponse {
|
||||
context?: {
|
||||
name: string;
|
||||
args: string[];
|
||||
message: Message;
|
||||
}
|
||||
}
|
||||
|
||||
// Util Class
|
||||
export class Util {
|
||||
public async loadCommand(name: string, args: string[], message: Message, commands: ICommandItem[]): Promise<IUtilResponse> {
|
||||
if (!message.member) return {
|
||||
valid: false,
|
||||
message: "Member is not part of message",
|
||||
};
|
||||
|
||||
if (!message.guild) return {
|
||||
valid: false,
|
||||
message: "Message is not part of a guild",
|
||||
};
|
||||
public async loadCommand(name: string, args: string[], message: Message, commands: ICommandItem[]) {
|
||||
if (!message.member) return;
|
||||
if (!message.guild) return;
|
||||
|
||||
const disabledCommandsString = await SettingsHelper.GetSetting("commands.disabled", message.guild?.id);
|
||||
const disabledCommands = disabledCommandsString?.split(",");
|
||||
|
||||
if (disabledCommands?.find(x => x == name)) {
|
||||
message.reply(process.env.COMMANDS_DISABLED_MESSAGE || "This command is disabled.");
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
message: "Command is disabled",
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
const folder = process.env.FOLDERS_COMMANDS;
|
||||
const item = commands.find(x => x.Name == name && !x.ServerId);
|
||||
const itemForServer = commands.find(x => x.Name == name && x.ServerId == message.guild?.id);
|
||||
|
||||
const item = commands.find(x => x.Name == name);
|
||||
let itemToUse: ICommandItem;
|
||||
|
||||
if (!item) {
|
||||
message.reply('Command not found');
|
||||
if (!itemForServer) {
|
||||
if (!item) {
|
||||
message.reply('Command not found');
|
||||
return;
|
||||
}
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
message: "Command not found"
|
||||
};
|
||||
itemToUse = item;
|
||||
} else {
|
||||
itemToUse = itemForServer;
|
||||
}
|
||||
|
||||
const requiredRoles = item.Command._roles;
|
||||
const requiredRoles = itemToUse.Command._roles;
|
||||
|
||||
for (const i in requiredRoles) {
|
||||
if (message.guild) {
|
||||
|
@ -66,20 +46,12 @@ export class Util {
|
|||
|
||||
if (!setting) {
|
||||
message.reply("Unable to verify if you have this role, please contact your bot administrator");
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
message: "Unable to verify if you have this role, please contact your bot administrator"
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
if (!message.member.roles.cache.find(role => role.name == setting)) {
|
||||
message.reply(`You require the \`${StringTools.Capitalise(setting)}\` role to run this command`);
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
message: `You require the \`${StringTools.Capitalise(setting)}\` role to run this command`
|
||||
};
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -90,39 +62,24 @@ export class Util {
|
|||
message: message
|
||||
};
|
||||
|
||||
const precheckResponse = item.Command.precheck(context);
|
||||
const precheckAsyncResponse = await item.Command.precheckAsync(context);
|
||||
const precheckResponse = itemToUse.Command.precheck(context);
|
||||
const precheckAsyncResponse = await itemToUse.Command.precheckAsync(context);
|
||||
|
||||
if (precheckResponse != CommandResponse.Ok) {
|
||||
message.reply(ErrorMessages.GetErrorMessage(precheckResponse));
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
message: ErrorMessages.GetErrorMessage(precheckResponse)
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
if (precheckAsyncResponse != CommandResponse.Ok) {
|
||||
message.reply(ErrorMessages.GetErrorMessage(precheckAsyncResponse));
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
message: ErrorMessages.GetErrorMessage(precheckAsyncResponse)
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
item.Command.execute(context);
|
||||
|
||||
return {
|
||||
valid: true,
|
||||
context: context
|
||||
}
|
||||
itemToUse.Command.execute(context);
|
||||
}
|
||||
|
||||
// Load the events
|
||||
loadEvents(client: Client, events: IEventItem[]): IUtilResponse {
|
||||
const folder = process.env.FOLDERS_EVENTS;
|
||||
|
||||
loadEvents(client: Client, events: IEventItem[]) {
|
||||
events.forEach((e) => {
|
||||
client.on('channelCreate', e.Event.channelCreate);
|
||||
client.on('channelDelete', e.Event.channelDelete);
|
||||
|
@ -138,9 +95,5 @@ export class Util {
|
|||
client.on('messageUpdate', e.Event.messageUpdate);
|
||||
client.on('ready', e.Event.ready);
|
||||
});
|
||||
|
||||
return {
|
||||
valid: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
25
src/commands/501231711271780357/entry.ts
Normal file
25
src/commands/501231711271780357/entry.ts
Normal file
|
@ -0,0 +1,25 @@
|
|||
import { ICommandContext } from "../../contracts/ICommandContext";
|
||||
import PublicEmbed from "../../helpers/embeds/PublicEmbed";
|
||||
import SettingsHelper from "../../helpers/SettingsHelper";
|
||||
import { Command } from "../../type/command";
|
||||
|
||||
export default class Entry extends Command {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
super._category = "Moderation";
|
||||
super._roles = [
|
||||
"moderator"
|
||||
];
|
||||
}
|
||||
|
||||
public override async execute(context: ICommandContext) {
|
||||
if (!context.message.guild) return;
|
||||
|
||||
const rulesChannelId = await SettingsHelper.GetSetting("channels.rules", context.message.guild.id) || "rules";
|
||||
|
||||
const embedInfo = new PublicEmbed(context, "", `Welcome to the server! Please make sure to read the rules in the <#${rulesChannelId}> channel and type the code found there in here to proceed to the main part of the server.`);
|
||||
|
||||
embedInfo.SendToCurrentChannel();
|
||||
}
|
||||
}
|
55
src/commands/501231711271780357/lobby.ts
Normal file
55
src/commands/501231711271780357/lobby.ts
Normal file
|
@ -0,0 +1,55 @@
|
|||
import { TextChannel } from "discord.js";
|
||||
import { ICommandContext } from "../../contracts/ICommandContext";
|
||||
import { Command } from "../../type/command";
|
||||
import { default as eLobby } from "../../entity/501231711271780357/Lobby";
|
||||
|
||||
export default class Lobby extends Command {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
super._category = "General";
|
||||
}
|
||||
|
||||
public override async execute(context: ICommandContext) {
|
||||
if (!context.message.guild) return;
|
||||
|
||||
const channel = context.message.channel as TextChannel;
|
||||
const channelId = channel.id;
|
||||
|
||||
const lobby = await eLobby.FetchOneByChannelId(channelId);
|
||||
|
||||
if (!lobby) {
|
||||
this.SendDisabled(context);
|
||||
return;
|
||||
}
|
||||
|
||||
const timeNow = Date.now();
|
||||
const timeLength = lobby.Cooldown * 60 * 1000; // x minutes in ms
|
||||
const timeAgo = timeNow - timeLength;
|
||||
|
||||
// If it was less than x minutes ago
|
||||
if (lobby.LastUsed.getTime() > timeAgo) {
|
||||
this.SendOnCooldown(context, timeLength, new Date(timeNow), lobby.LastUsed);
|
||||
return;
|
||||
}
|
||||
|
||||
await this.RequestLobby(context, lobby);
|
||||
}
|
||||
|
||||
private async RequestLobby(context: ICommandContext, lobby: eLobby) {
|
||||
lobby.MarkAsUsed();
|
||||
await lobby.Save(eLobby, lobby);
|
||||
|
||||
context.message.channel.send(`${context.message.author} would like to organise a lobby of **${lobby.Name}**! <@&${lobby.RoleId}>`);
|
||||
}
|
||||
|
||||
private SendOnCooldown(context: ICommandContext, timeLength: number, timeNow: Date, timeUsed: Date) {
|
||||
const timeLeft = Math.ceil((timeLength - (timeNow.getTime() - timeUsed.getTime())) / 1000 / 60);
|
||||
|
||||
context.message.reply(`Requesting a lobby for this game is on cooldown! Please try again in **${timeLeft} minutes**.`);
|
||||
}
|
||||
|
||||
private SendDisabled(context: ICommandContext) {
|
||||
context.message.reply("This channel hasn't been setup for lobbies.");
|
||||
}
|
||||
}
|
|
@ -3,4 +3,5 @@ import { Command } from "../type/command";
|
|||
export default interface ICommandItem {
|
||||
Name: string,
|
||||
Command: Command,
|
||||
ServerId?: string,
|
||||
}
|
45
src/entity/501231711271780357/Lobby.ts
Normal file
45
src/entity/501231711271780357/Lobby.ts
Normal file
|
@ -0,0 +1,45 @@
|
|||
import { Column, Entity, getConnection } from "typeorm";
|
||||
import BaseEntity from "../../contracts/BaseEntity";
|
||||
|
||||
@Entity()
|
||||
export default class Lobby extends BaseEntity {
|
||||
constructor(channelId: string, roleId: string, cooldown: number, name: string) {
|
||||
super();
|
||||
|
||||
this.ChannelId = channelId;
|
||||
this.RoleId = roleId;
|
||||
this.Cooldown = cooldown;
|
||||
this.Name = name;
|
||||
|
||||
this.LastUsed = new Date(0);
|
||||
}
|
||||
|
||||
@Column()
|
||||
public ChannelId: string;
|
||||
|
||||
@Column()
|
||||
public RoleId: string;
|
||||
|
||||
@Column()
|
||||
public Cooldown: number;
|
||||
|
||||
@Column()
|
||||
public LastUsed: Date;
|
||||
|
||||
@Column()
|
||||
public Name: string;
|
||||
|
||||
public MarkAsUsed() {
|
||||
this.LastUsed = new Date();
|
||||
}
|
||||
|
||||
public static async FetchOneByChannelId(channelId: string, relations?: string[]): Promise<Lobby | undefined> {
|
||||
const connection = getConnection();
|
||||
|
||||
const repository = connection.getRepository(Lobby);
|
||||
|
||||
const single = await repository.findOne({ ChannelId: channelId }, { relations: relations || [] });
|
||||
|
||||
return single;
|
||||
}
|
||||
}
|
|
@ -1,4 +1,6 @@
|
|||
import { CoreClient } from "./client/client";
|
||||
|
||||
// Command Imports
|
||||
import About from "./commands/about";
|
||||
import Ban from "./commands/ban";
|
||||
import Clear from "./commands/clear";
|
||||
|
@ -15,6 +17,12 @@ import Rules from "./commands/rules";
|
|||
import Setup from "./commands/setup";
|
||||
import Unmute from "./commands/unmute";
|
||||
import Warn from "./commands/warn";
|
||||
|
||||
// Command Imports: MankBot
|
||||
import Entry from "./commands/501231711271780357/entry";
|
||||
import Lobby from "./commands/501231711271780357/lobby";
|
||||
|
||||
// Event Imports
|
||||
import MemberEvents from "./events/MemberEvents";
|
||||
import MessageEvents from "./events/MessageEvents";
|
||||
|
||||
|
@ -35,7 +43,15 @@ export default class Registry {
|
|||
client.RegisterCommand("setup", new Setup());
|
||||
client.RegisterCommand("config", new Config());
|
||||
client.RegisterCommand("code", new Code());
|
||||
client.RegisterCommand("disable", new Disable())
|
||||
client.RegisterCommand("disable", new Disable());
|
||||
|
||||
// Exclusive Commands: MankBot
|
||||
client.RegisterCommand("lobby", new Lobby(), "501231711271780357");
|
||||
client.RegisterCommand("entry", new Entry(), "501231711271780357");
|
||||
|
||||
// Add Exclusive Commands to Test Server
|
||||
client.RegisterCommand("lobby", new Lobby(), "442730357897429002");
|
||||
client.RegisterCommand("entry", new Entry(), "442730357897429002");
|
||||
}
|
||||
|
||||
public static RegisterEvents(client: CoreClient) {
|
||||
|
|
Loading…
Reference in a new issue