From 1b1a070cfdc5a40f57fad0325b26d9a3b9cff96f Mon Sep 17 00:00:00 2001 From: Vylpes Date: Sat, 9 Apr 2022 14:11:06 +0100 Subject: [PATCH] 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 --- README.md | 50 ++++++++++--- docs/Registry.md | 31 ++++++++ src/client/client.ts | 3 +- src/client/events.ts | 59 ++------------- src/client/util.ts | 95 ++++++------------------ src/commands/501231711271780357/entry.ts | 25 +++++++ src/commands/501231711271780357/lobby.ts | 55 ++++++++++++++ src/contracts/ICommandItem.ts | 1 + src/entity/501231711271780357/Lobby.ts | 45 +++++++++++ src/registry.ts | 18 ++++- 10 files changed, 247 insertions(+), 135 deletions(-) create mode 100644 docs/Registry.md create mode 100644 src/commands/501231711271780357/entry.ts create mode 100644 src/commands/501231711271780357/lobby.ts create mode 100644 src/entity/501231711271780357/Lobby.ts diff --git a/README.md b/README.md index f825db8..9023e0c 100644 --- a/README.md +++ b/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 \ No newline at end of file +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. \ No newline at end of file diff --git a/docs/Registry.md b/docs/Registry.md new file mode 100644 index 0000000..72ed70f --- /dev/null +++ b/docs/Registry.md @@ -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 \ No newline at end of file diff --git a/src/client/client.ts b/src/client/client.ts index 36b915b..33f2926 100644 --- a/src/client/client.ts +++ b/src/client/client.ts @@ -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); diff --git a/src/client/events.ts b/src/client/events.ts index a8d5085..5c81e42 100644 --- a/src/client/events.ts +++ b/src/client/events.ts @@ -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 { - 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); } } diff --git a/src/client/util.ts b/src/client/util.ts index 951a632..7c73776 100644 --- a/src/client/util.ts +++ b/src/client/util.ts @@ -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 { - 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 - } } } diff --git a/src/commands/501231711271780357/entry.ts b/src/commands/501231711271780357/entry.ts new file mode 100644 index 0000000..f70f9d3 --- /dev/null +++ b/src/commands/501231711271780357/entry.ts @@ -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(); + } +} \ No newline at end of file diff --git a/src/commands/501231711271780357/lobby.ts b/src/commands/501231711271780357/lobby.ts new file mode 100644 index 0000000..5e6d687 --- /dev/null +++ b/src/commands/501231711271780357/lobby.ts @@ -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."); + } +} \ No newline at end of file diff --git a/src/contracts/ICommandItem.ts b/src/contracts/ICommandItem.ts index c8246c9..89acb50 100644 --- a/src/contracts/ICommandItem.ts +++ b/src/contracts/ICommandItem.ts @@ -3,4 +3,5 @@ import { Command } from "../type/command"; export default interface ICommandItem { Name: string, Command: Command, + ServerId?: string, } \ No newline at end of file diff --git a/src/entity/501231711271780357/Lobby.ts b/src/entity/501231711271780357/Lobby.ts new file mode 100644 index 0000000..b6ffd80 --- /dev/null +++ b/src/entity/501231711271780357/Lobby.ts @@ -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 { + const connection = getConnection(); + + const repository = connection.getRepository(Lobby); + + const single = await repository.findOne({ ChannelId: channelId }, { relations: relations || [] }); + + return single; + } +} \ No newline at end of file diff --git a/src/registry.ts b/src/registry.ts index 0792dc3..43632fc 100644 --- a/src/registry.ts +++ b/src/registry.ts @@ -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) {