Feature/48 database (#114)

* Add database and default values

* Add ability to save a setting to the database

* Get commands and events to use database

* Setup and config command

* Update commands to check roles per server

* Different rules per server

Signed-off-by: Ethan Lane <ethan@vylpes.com>

* Different prefix per server

Signed-off-by: Ethan Lane <ethan@vylpes.com>

* Add verification system

Signed-off-by: Ethan Lane <ethan@vylpes.com>

* Disabled commands per server

* Add devmode for default prefix

* Update embeds

* Fix broken tests
This commit is contained in:
Vylpes 2022-03-29 18:19:54 +01:00 committed by GitHub
parent c8edd1b4c5
commit 6a00c49ef3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
53 changed files with 1816 additions and 373 deletions

View file

@ -1,5 +1,7 @@
import { Client } from "discord.js";
import * as dotenv from "dotenv";
import { createConnection } from "typeorm";
import DefaultValues from "../constants/DefaultValues";
import ICommandItem from "../contracts/ICommandItem";
import IEventItem from "../contracts/IEventItem";
import { Command } from "../type/command";
@ -23,10 +25,12 @@ export class CoreClient extends Client {
return this._eventItems;
}
constructor() {
constructor(devmode: boolean = false) {
super();
dotenv.config();
DefaultValues.useDevPrefix = devmode;
this._commandItems = [];
this._eventItems = [];
@ -34,11 +38,16 @@ export class CoreClient extends Client {
this._util = new Util();
}
public start() {
if (!process.env.BOT_TOKEN) throw "BOT_TOKEN is not defined in .env";
if (!process.env.BOT_PREFIX) throw "BOT_PREFIX is not defined in .env";
if (!process.env.FOLDERS_COMMANDS) throw "FOLDERS_COMMANDS is not defined in .env";
if (!process.env.FOLDERS_EVENTS) throw "FOLDERS_EVENTS is not defined in .env";
public async start() {
if (!process.env.BOT_TOKEN) {
console.error("BOT_TOKEN is not defined in .env");
return;
}
await createConnection().catch(e => {
console.error(e);
return;
});
super.on("message", (message) => this._events.onMessage(message, this._commandItems));
super.on("ready", this._events.onReady);

View file

@ -1,6 +1,7 @@
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 {
@ -21,7 +22,7 @@ export class Events {
// Emit when a message is sent
// Used to check for commands
public onMessage(message: Message, commands: ICommandItem[]): IEventResponse {
public async onMessage(message: Message, commands: ICommandItem[]): Promise<IEventResponse> {
if (!message.guild) return {
valid: false,
message: "Message was not sent in a guild, ignoring.",
@ -32,7 +33,14 @@ export class Events {
message: "Message was sent by a bot, ignoring.",
};
const prefix = process.env.BOT_PREFIX as string;
const prefix = await SettingsHelper.GetSetting("bot.prefix", message.guild.id);
if (!prefix) {
return {
valid: false,
message: "Prefix not found",
};
}
if (message.content.substring(0, prefix.length).toLowerCase() == prefix.toLowerCase()) {
const args = message.content.substring(prefix.length).split(" ");
@ -43,7 +51,7 @@ export class Events {
message: "Command name was not found",
};
const res = this._util.loadCommand(name, args, message, commands);
const res = await this._util.loadCommand(name, args, message, commands);
if (!res.valid) {
return {

View file

@ -7,6 +7,10 @@ import { Event } from "../type/event";
import { ICommandContext } from "../contracts/ICommandContext";
import ICommandItem from "../contracts/ICommandItem";
import IEventItem from "../contracts/IEventItem";
import SettingsHelper from "../helpers/SettingsHelper";
import StringTools from "../helpers/StringTools";
import { CommandResponse } from "../constants/CommandResponse";
import ErrorMessages from "../constants/ErrorMessages";
export interface IUtilResponse extends IBaseResponse {
context?: {
@ -18,13 +22,19 @@ export interface IUtilResponse extends IBaseResponse {
// Util Class
export class Util {
public loadCommand(name: string, args: string[], message: Message, commands: ICommandItem[]): IUtilResponse {
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",
};
const disabledCommands = process.env.COMMANDS_DISABLED?.split(',');
if (!message.guild) return {
valid: false,
message: "Message is not part of a guild",
};
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.");
@ -51,13 +61,26 @@ export class Util {
const requiredRoles = item.Command._roles;
for (const i in requiredRoles) {
if (!message.member.roles.cache.find(role => role.name == requiredRoles[i])) {
message.reply(`You require the \`${requiredRoles[i]}\` role to run this command`);
if (message.guild) {
const setting = await SettingsHelper.GetSetting(`role.${requiredRoles[i]}`, message.guild?.id);
return {
valid: false,
message: `You require the \`${requiredRoles[i]}\` role to run this command`
};
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"
};
}
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`
};
}
}
}
@ -67,6 +90,27 @@ export class Util {
message: message
};
const precheckResponse = item.Command.precheck(context);
const precheckAsyncResponse = await item.Command.precheckAsync(context);
if (precheckResponse != CommandResponse.Ok) {
message.reply(ErrorMessages.GetErrorMessage(precheckResponse));
return {
valid: false,
message: ErrorMessages.GetErrorMessage(precheckResponse)
};
}
if (precheckAsyncResponse != CommandResponse.Ok) {
message.reply(ErrorMessages.GetErrorMessage(precheckAsyncResponse));
return {
valid: false,
message: ErrorMessages.GetErrorMessage(precheckAsyncResponse)
};
}
item.Command.execute(context);
return {

View file

@ -12,7 +12,7 @@ export default class Ban extends Command {
super._category = "Moderation";
super._roles = [
process.env.ROLES_MODERATOR!
"moderator"
];
}
@ -69,7 +69,7 @@ export default class Ban extends Command {
await targetMember.ban({ reason: `Moderator: ${context.message.author.tag}, Reason: ${reason || "*none*"}` });
logEmbed.SendToModLogsChannel();
await logEmbed.SendToModLogsChannel();
publicEmbed.SendToCurrentChannel();
return {

View file

@ -11,7 +11,7 @@ export default class Clear extends Command {
super._category = "Moderation";
super._roles = [
process.env.ROLES_MODERATOR!
"moderator"
];
}

94
src/commands/code.ts Normal file
View file

@ -0,0 +1,94 @@
import { CommandResponse } from "../constants/CommandResponse";
import { ICommandContext } from "../contracts/ICommandContext";
import ErrorEmbed from "../helpers/embeds/ErrorEmbed";
import PublicEmbed from "../helpers/embeds/PublicEmbed";
import SettingsHelper from "../helpers/SettingsHelper";
import StringTools from "../helpers/StringTools";
import { Command } from "../type/command";
export default class Code extends Command {
constructor() {
super();
super._category = "Moderation";
super._roles = [
"moderator"
];
}
public override async precheckAsync(context: ICommandContext): Promise<CommandResponse> {
if (!context.message.guild){
return CommandResponse.NotInServer;
}
const isEnabled = await SettingsHelper.GetSetting("verification.enabled", context.message.guild?.id);
if (!isEnabled) {
return CommandResponse.FeatureDisabled;
}
if (isEnabled.toLocaleLowerCase() != 'true') {
return CommandResponse.FeatureDisabled;
}
return CommandResponse.Ok;
}
public override async execute(context: ICommandContext) {
const action = context.args[0];
switch (action) {
case "randomise":
await this.Randomise(context);
break;
case "embed":
await this.SendEmbed(context);
break;
default:
await this.SendUsage(context);
}
}
private async SendUsage(context: ICommandContext) {
const description = [
"USAGE: <randomise|embed>",
"",
"randomise: Sets the server's entry code to a random code",
"embed: Sends an embed with the server's entry code"
].join("\n");
const embed = new PublicEmbed(context, "", description);
embed.SendToCurrentChannel();
}
private async Randomise(context: ICommandContext) {
if (!context.message.guild) {
return;
}
const randomCode = StringTools.RandomString(5);
await SettingsHelper.SetSetting("verification.code", context.message.guild.id, randomCode);
const embed = new PublicEmbed(context, "Code", `Entry code has been set to \`${randomCode}\``);
embed.SendToCurrentChannel();
}
private async SendEmbed(context: ICommandContext) {
if (!context.message.guild) {
return;
}
const code = await SettingsHelper.GetSetting("verification.code", context.message.guild.id);
if (!code || code == "") {
const errorEmbed = new ErrorEmbed(context, "There is no code for this server setup.");
errorEmbed.SendToCurrentChannel();
return;
}
const embed = new PublicEmbed(context, "Entry Code", code!);
embed.SendToCurrentChannel();
}
}

136
src/commands/config.ts Normal file
View file

@ -0,0 +1,136 @@
import { Guild } from "discord.js";
import { readFileSync } from "fs";
import { CommandResponse } from "../constants/CommandResponse";
import DefaultValues from "../constants/DefaultValues";
import { ICommandContext } from "../contracts/ICommandContext";
import ICommandReturnContext from "../contracts/ICommandReturnContext";
import Server from "../entity/Server";
import Setting from "../entity/Setting";
import ErrorEmbed from "../helpers/embeds/ErrorEmbed";
import PublicEmbed from "../helpers/embeds/PublicEmbed";
import { Command } from "../type/command";
export default class Config extends Command {
constructor() {
super();
super._category = "Administration";
super._roles = [
"administrator"
]
}
public override async precheckAsync(context: ICommandContext): Promise<CommandResponse> {
if (!context.message.guild) {
return CommandResponse.ServerNotSetup;
}
const server = await Server.FetchOneById<Server>(Server, context.message.guild?.id, [
"Settings",
]);
if (!server) {
return CommandResponse.ServerNotSetup;
}
return CommandResponse.Ok;
}
public override async execute(context: ICommandContext) {
if (!context.message.guild) {
return;
}
const server = await Server.FetchOneById<Server>(Server, context.message.guild?.id, [
"Settings",
]);
if (!server) {
return;
}
const key = context.args[0];
const action = context.args[1];
const value = context.args.splice(2).join(" ");
if (!key) {
this.SendHelpText(context);
} else if (!action) {
this.GetValue(context, server, key);
} else {
switch(action) {
case 'reset':
this.ResetValue(context, server, key);
break;
case 'set':
if (!value) {
const errorEmbed = new ErrorEmbed(context, "Value is required when setting");
errorEmbed.SendToCurrentChannel();
return;
}
this.SetValue(context, server, key, value);
break;
default:
const errorEmbed = new ErrorEmbed(context, "Action must be either set or reset");
errorEmbed.SendToCurrentChannel();
return;
}
}
}
private async SendHelpText(context: ICommandContext) {
const description = readFileSync(`${process.cwd()}/data/config.txt`).toString();
const embed = new PublicEmbed(context, "Config", description);
embed.SendToCurrentChannel();
}
private async GetValue(context: ICommandContext, server: Server, key: string) {
const setting = server.Settings.filter(x => x.Key == key)[0];
if (setting) {
const embed = new PublicEmbed(context, "", `${key}: ${setting.Value}`);
embed.SendToCurrentChannel();
} else {
const embed = new PublicEmbed(context, "", `${key}: ${DefaultValues.GetValue(key)} <DEFAULT>`);
embed.SendToCurrentChannel();
}
}
private async ResetValue(context: ICommandContext, server: Server, key: string) {
const setting = server.Settings.filter(x => x.Key == key)[0];
if (!setting) {
const embed = new PublicEmbed(context, "", "Setting has been reset");
embed.SendToCurrentChannel();
return;
}
await Setting.Remove(Setting, setting);
const embed = new PublicEmbed(context, "", "Setting has been reset");
embed.SendToCurrentChannel();
}
private async SetValue(context: ICommandContext, server: Server, key: string, value: string) {
const setting = server.Settings.filter(x => x.Key == key)[0];
if (setting) {
setting.UpdateBasicDetails(key, value);
await setting.Save(Setting, setting);
} else {
const newSetting = new Setting(key, value);
await newSetting.Save(Setting, newSetting);
server.AddSettingToServer(newSetting);
await server.Save(Server, server);
}
const embed = new PublicEmbed(context, "", "Setting has been set");
embed.SendToCurrentChannel();
}
}

96
src/commands/disable.ts Normal file
View file

@ -0,0 +1,96 @@
import { CommandResponse } from "../constants/CommandResponse";
import { ICommandContext } from "../contracts/ICommandContext";
import ErrorEmbed from "../helpers/embeds/ErrorEmbed";
import PublicEmbed from "../helpers/embeds/PublicEmbed";
import SettingsHelper from "../helpers/SettingsHelper";
import StringTools from "../helpers/StringTools";
import { Command } from "../type/command";
export default class Disable extends Command {
constructor() {
super();
super._category = "Moderation";
super._roles = [
"moderator"
];
}
public override async execute(context: ICommandContext) {
const action = context.args[0];
switch (action) {
case "add":
await this.Add(context);
break;
case "remove":
await this.Remove(context);
break;
default:
await this.SendUsage(context);
}
}
private async SendUsage(context: ICommandContext) {
const description = [
"USAGE: <add|remove> <name>",
"",
"add: Adds the command name to the server's disabled command string",
"remove: Removes the command name from the server's disabled command string",
"name: The name of the command to enable/disable"
].join("\n");
const embed = new PublicEmbed(context, "", description);
embed.SendToCurrentChannel();
}
private async Add(context: ICommandContext) {
if (!context.message.guild) {
return;
}
const commandName = context.args[1];
if (!commandName) {
this.SendUsage(context);
return;
}
const disabledCommandsString = await SettingsHelper.GetSetting("commands.disabled", context.message.guild.id);
const disabledCommands = disabledCommandsString != "" ? disabledCommandsString?.split(",") : [];
disabledCommands?.push(commandName);
await SettingsHelper.SetSetting("commands.disabled", context.message.guild.id, disabledCommands!.join(","));
const embed = new PublicEmbed(context, "", `Disabled command: ${commandName}`);
embed.SendToCurrentChannel();
}
private async Remove(context: ICommandContext) {
if (!context.message.guild) {
return;
}
const commandName = context.args[1];
if (!commandName) {
this.SendUsage(context);
return;
}
const disabledCommandsString = await SettingsHelper.GetSetting("commands.disabled", context.message.guild.id);
const disabledCommands = disabledCommandsString != "" ? disabledCommandsString?.split(",") : [];
const disabledCommandsInstance = disabledCommands?.findIndex(x => x == commandName);
if (disabledCommandsInstance! > -1) {
disabledCommands?.splice(disabledCommandsInstance!, 1);
}
await SettingsHelper.SetSetting("commands.disabled", context.message.guild.id, disabledCommands!.join(","));
const embed = new PublicEmbed(context, "", `Enabled command: ${commandName}`);
embed.SendToCurrentChannel();
}
}

View file

@ -12,7 +12,7 @@ export default class Kick extends Command {
super._category = "Moderation";
super._roles = [
process.env.ROLES_MODERATOR!
"moderator"
];
}
@ -72,7 +72,7 @@ export default class Kick extends Command {
await targetMember.kick(`Moderator: ${context.message.author.tag}, Reason: ${reason || "*none*"}`);
logEmbed.SendToModLogsChannel();
await logEmbed.SendToModLogsChannel();
publicEmbed.SendToCurrentChannel();
return {

View file

@ -12,7 +12,7 @@ export default class Mute extends Command {
super._category = "Moderation";
super._roles = [
process.env.ROLES_MODERATOR!
"moderator"
];
}
@ -85,7 +85,7 @@ export default class Mute extends Command {
await targetMember.roles.add(mutedRole, `Moderator: ${context.message.author.tag}, Reason: ${reason || "*none*"}`);
logEmbed.SendToModLogsChannel();
await logEmbed.SendToModLogsChannel();
publicEmbed.SendToCurrentChannel();
return {

View file

@ -18,12 +18,12 @@ export default class Rules extends Command {
super._category = "Admin";
super._roles = [
process.env.ROLES_MODERATOR!
"administrator"
];
}
public override execute(context: ICommandContext): ICommandReturnContext {
if (!existsSync(`${process.cwd()}/${process.env.COMMANDS_RULES_FILE!}`)) {
if (!existsSync(`${process.cwd()}/data/rules/${context.message.guild?.id}.json`)) {
const errorEmbed = new ErrorEmbed(context, "Rules file doesn't exist");
errorEmbed.SendToCurrentChannel();
@ -33,7 +33,7 @@ export default class Rules extends Command {
};
}
const rulesFile = readFileSync(`${process.cwd()}/${process.env.COMMANDS_RULES_FILE}`).toString();
const rulesFile = readFileSync(`${process.cwd()}/data/rules/${context.message.guild?.id}.json`).toString();
const rules = JSON.parse(rulesFile) as IRules[];
const embeds: PublicEmbed[] = [];

37
src/commands/setup.ts Normal file
View file

@ -0,0 +1,37 @@
import { ICommandContext } from "../contracts/ICommandContext";
import ICommandReturnContext from "../contracts/ICommandReturnContext";
import Server from "../entity/Server";
import ErrorEmbed from "../helpers/embeds/ErrorEmbed";
import PublicEmbed from "../helpers/embeds/PublicEmbed";
import { Command } from "../type/command";
export default class Setup extends Command {
constructor() {
super();
super._category = "Administration";
super._roles = [
"moderator"
]
}
public override async execute(context: ICommandContext) {
if (!context.message.guild) {
return;
}
const server = await Server.FetchOneById(Server, context.message.guild?.id);
if (server) {
const embed = new ErrorEmbed(context, "This server has already been setup, please configure using the config command");
embed.SendToCurrentChannel();
return;
}
const newServer = new Server(context.message.guild?.id);
await newServer.Save(Server, newServer);
const embed = new PublicEmbed(context, "Success", "Please configure using the config command");
embed.SendToCurrentChannel();
}
}

View file

@ -12,7 +12,7 @@ export default class Unmute extends Command {
super._category = "Moderation";
super._roles = [
process.env.ROLES_MODERATOR!
"moderator"
];
}
@ -85,7 +85,7 @@ export default class Unmute extends Command {
await targetMember.roles.remove(mutedRole, `Moderator: ${context.message.author.tag}, Reason: ${reason || "*none*"}`);
logEmbed.SendToModLogsChannel();
await logEmbed.SendToModLogsChannel();
publicEmbed.SendToCurrentChannel();
return {

View file

@ -11,11 +11,11 @@ export default class Warn extends Command {
super._category = "Moderation";
super._roles = [
process.env.ROLES_MODERATOR!
"moderator"
];
}
public override execute(context: ICommandContext): ICommandReturnContext {
public override async execute(context: ICommandContext): Promise<ICommandReturnContext> {
const user = context.message.mentions.users.first();
if (!user) {
@ -60,7 +60,7 @@ export default class Warn extends Command {
const publicEmbed = new PublicEmbed(context, "", `${user} has been warned`);
publicEmbed.AddReason(reason);
logEmbed.SendToModLogsChannel();
await logEmbed.SendToModLogsChannel();
publicEmbed.SendToCurrentChannel();
return {

View file

@ -0,0 +1,7 @@
export enum CommandResponse {
Ok,
Unauthorised,
ServerNotSetup,
NotInServer,
FeatureDisabled,
}

View file

@ -0,0 +1,56 @@
export default class DefaultValues {
public static values: ISettingValue[] = [];
public static useDevPrefix: boolean = false;
public static GetValue(key: string): string | undefined {
this.SetValues();
const res = this.values.find(x => x.Key == key);
if (!res) {
return undefined;
}
return res.Value;
}
private static SetValues() {
if (this.values.length == 0) {
// Bot
if (this.useDevPrefix) {
this.values.push({ Key: "bot.prefix", Value: "d!" });
} else {
this.values.push({ Key: "bot.prefix", Value: "v!" });
}
// Commands
this.values.push({ Key: "commands.disabled", Value: "" });
this.values.push({ Key: "commands.disabled.message", Value: "This command is disabled." });
// Role (Command)
this.values.push({ Key: "role.assignable", Value: "" });
this.values.push({ Key: "role.moderator", Value: "Moderator" });
this.values.push({ Key: "role.administrator", Value: "Administrator"});
this.values.push({ Key: "role.muted", Value: "Muted" });
// Rules (Command)
this.values.push({ Key: "rules.file", Value: "data/rules/rules" });
// Channels
this.values.push({ Key: "channels.logs.message", Value: "message-logs" });
this.values.push({ Key: "channels.logs.member", Value: "member-logs" });
this.values.push({ Key: "channels.logs.mod", Value: "mod-logs" });
// Verification
this.values.push({ Key: "verification.enabled", Value: "false" });
this.values.push({ Key: "verification.channel", Value: "entry" });
this.values.push({ Key: "verification.role", Value: "Entry" });
this.values.push({ Key: "verification.code", Value: "" });
}
}
}
export interface ISettingValue {
Key: string,
Value: string,
};

View file

@ -1,5 +1,27 @@
import { CommandResponse } from "./CommandResponse";
export default class ErrorMessages {
public static readonly InsufficientBotPermissions = "Unable to do this action, am I missing permissions?";
public static readonly ChannelNotFound = "Unable to find channel";
public static readonly RoleNotFound = "Unable to find role";
public static readonly UserUnauthorised = "You are not authorised to use this command";
public static readonly ServerNotSetup = "This server hasn't been setup yet, please run the setup command";
public static readonly NotInServer = "This command requires to be ran inside of a server";
public static readonly FeatureDisabled = "This feature is currently disabled by a server moderator";
public static GetErrorMessage(response: CommandResponse): string {
switch (response) {
case CommandResponse.Unauthorised:
return this.UserUnauthorised;
case CommandResponse.ServerNotSetup:
return this.ServerNotSetup;
case CommandResponse.NotInServer:
return this.NotInServer;
case CommandResponse.FeatureDisabled:
return this.FeatureDisabled;
default:
return "";
}
}
}

View file

@ -0,0 +1,68 @@
import { Column, DeepPartial, EntityTarget, getConnection, PrimaryColumn } from "typeorm";
import { v4 } from "uuid";
export default class BaseEntity {
constructor() {
this.Id = v4();
this.WhenCreated = new Date();
this.WhenUpdated = new Date();
}
@PrimaryColumn()
Id: string;
@Column()
WhenCreated: Date;
@Column()
WhenUpdated: Date;
public async Save<T>(target: EntityTarget<T>, entity: DeepPartial<T>): Promise<void> {
this.WhenUpdated = new Date();
const connection = getConnection();
const repository = connection.getRepository<T>(target);
await repository.save(entity);
}
public static async Remove<T>(target: EntityTarget<T>, entity: T): Promise<void> {
const connection = getConnection();
const repository = connection.getRepository<T>(target);
await repository.remove(entity);
}
public static async FetchAll<T>(target: EntityTarget<T>, relations?: string[]): Promise<T[]> {
const connection = getConnection();
const repository = connection.getRepository<T>(target);
const all = await repository.find({ relations: relations || [] });
return all;
}
public static async FetchOneById<T>(target: EntityTarget<T>, id: string, relations?: string[]): Promise<T | undefined> {
const connection = getConnection();
const repository = connection.getRepository<T>(target);
const single = await repository.findOne(id, { relations: relations || [] });
return single;
}
public static async Any<T>(target: EntityTarget<T>): Promise<boolean> {
const connection = getConnection();
const repository = connection.getRepository<T>(target);
const any = await repository.find();
return any.length > 0;
}
}

19
src/entity/Server.ts Normal file
View file

@ -0,0 +1,19 @@
import { Column, Entity, getConnection, OneToMany } from "typeorm";
import BaseEntity from "../contracts/BaseEntity";
import Setting from "./Setting";
@Entity()
export default class Server extends BaseEntity {
constructor(serverId: string) {
super();
this.Id = serverId;
}
@OneToMany(() => Setting, x => x.Server)
Settings: Setting[];
public AddSettingToServer(setting: Setting) {
this.Settings.push(setting);
}
}

37
src/entity/Setting.ts Normal file
View file

@ -0,0 +1,37 @@
import { Column, Entity, getConnection, ManyToOne } from "typeorm";
import BaseEntity from "../contracts/BaseEntity";
import Server from "./Server";
@Entity()
export default class Setting extends BaseEntity {
constructor(key: string, value: string) {
super();
this.Key = key;
this.Value = value;
}
@Column()
Key: string;
@Column()
Value: string;
@ManyToOne(() => Server, x => x.Settings)
Server: Server;
public UpdateBasicDetails(key: string, value: string) {
this.Key = key;
this.Value = value;
}
public static async FetchOneByKey(key: string, relations?: string[]): Promise<Setting | undefined> {
const connection = getConnection();
const repository = connection.getRepository(Setting);
const single = await repository.findOne({ Key: key }, { relations: relations || [] });
return single;
}
}

View file

@ -9,37 +9,37 @@ export default class MemberEvents extends Event {
super();
}
public override guildMemberAdd(member: GuildMember): IEventReturnContext {
public override async guildMemberAdd(member: GuildMember): Promise<IEventReturnContext> {
const embed = new EventEmbed(member.guild, "Member Joined");
embed.AddUser("User", member.user, true);
embed.addField("Created", member.user.createdAt);
embed.setFooter(`Id: ${member.user.id}`);
embed.SendToMemberLogsChannel();
await embed.SendToMemberLogsChannel();
return {
embeds: [embed]
};
}
public override guildMemberRemove(member: GuildMember): IEventReturnContext {
public override async guildMemberRemove(member: GuildMember): Promise<IEventReturnContext> {
const embed = new EventEmbed(member.guild, "Member Left");
embed.AddUser("User", member.user, true);
embed.addField("Joined", member.joinedAt);
embed.setFooter(`Id: ${member.user.id}`);
embed.SendToMemberLogsChannel();
await embed.SendToMemberLogsChannel();
return {
embeds: [embed]
};
}
public override guildMemberUpdate(oldMember: GuildMember, newMember: GuildMember): IEventReturnContext {
public override async guildMemberUpdate(oldMember: GuildMember, newMember: GuildMember): Promise<IEventReturnContext> {
const handler = new GuildMemberUpdate(oldMember, newMember);
if (oldMember.nickname != newMember.nickname) { // Nickname change
handler.NicknameChanged();
await handler.NicknameChanged();
}
return {

View file

@ -11,7 +11,7 @@ export default class GuildMemberUpdate {
this.newMember = newMember;
}
public NicknameChanged(): IEventReturnContext {
public async NicknameChanged(): Promise<IEventReturnContext> {
const oldNickname = this.oldMember.nickname || "*none*";
const newNickname = this.newMember.nickname || "*none*";
@ -21,7 +21,7 @@ export default class GuildMemberUpdate {
embed.addField("After", newNickname, true);
embed.setFooter(`Id: ${this.newMember.user.id}`);
embed.SendToMemberLogsChannel();
await embed.SendToMemberLogsChannel();
return {
embeds: [embed]

View file

@ -2,13 +2,15 @@ import { Event } from "../type/event";
import { Message } from "discord.js";
import EventEmbed from "../helpers/embeds/EventEmbed";
import IEventReturnContext from "../contracts/IEventReturnContext";
import SettingsHelper from "../helpers/SettingsHelper";
import OnMessage from "./MessageEvents/OnMessage";
export default class MessageEvents extends Event {
constructor() {
super();
}
public override messageDelete(message: Message): IEventReturnContext {
public override async messageDelete(message: Message): Promise<IEventReturnContext> {
if (!message.guild) {
return {
embeds: []
@ -30,14 +32,14 @@ export default class MessageEvents extends Event {
embed.addField("Attachments", `\`\`\`${message.attachments.map(x => x.url).join("\n")}\`\`\``);
}
embed.SendToMessageLogsChannel();
await embed.SendToMessageLogsChannel();
return {
embeds: [embed]
};
}
public override messageUpdate(oldMessage: Message, newMessage: Message): IEventReturnContext {
public override async messageUpdate(oldMessage: Message, newMessage: Message): Promise<IEventReturnContext> {
if (!newMessage.guild){
return {
embeds: []
@ -62,10 +64,21 @@ export default class MessageEvents extends Event {
embed.addField("Before", `\`\`\`${oldMessage.content || "*none*"}\`\`\``);
embed.addField("After", `\`\`\`${newMessage.content || "*none*"}\`\`\``);
embed.SendToMessageLogsChannel();
await embed.SendToMessageLogsChannel();
return {
embeds: [embed]
};
}
public override async message(message: Message) {
if (!message.guild) return;
if (message.author.bot) return;
const isVerificationEnabled = await SettingsHelper.GetSetting("verification.enabled", message.guild.id);
if (isVerificationEnabled && isVerificationEnabled.toLocaleLowerCase() == "true") {
await OnMessage.VerificationCheck(message);
}
}
}

View file

@ -0,0 +1,59 @@
import { Message as Message } from "discord.js";
import SettingsHelper from "../../helpers/SettingsHelper";
export default class OnMessage {
public static async VerificationCheck(message: Message) {
if (!message.guild) return;
const verificationChannel = await SettingsHelper.GetSetting("verification.channel", message.guild.id);
if (!verificationChannel) {
return;
}
const channel = message.guild.channels.cache.find(x => x.name == verificationChannel);
if (!channel) {
return;
}
const currentChannel = message.guild.channels.cache.find(x => x == message.channel);
if (!currentChannel || currentChannel.name != verificationChannel) {
return;
}
const verificationCode = await SettingsHelper.GetSetting("verification.code", message.guild.id);
if (!verificationCode || verificationCode == "") {
await message.reply("`verification.code` is not set inside of the server's config. Please contact the server's mod team.");
await message.delete();
return;
}
const verificationRoleName = await SettingsHelper.GetSetting("verification.role", message.guild.id);
if (!verificationRoleName) {
await message.reply("`verification.role` is not set inside of the server's config. Please contact the server's mod team.");
await message.delete();
return;
}
const role = message.guild.roles.cache.find(x => x.name == verificationRoleName);
if (!role) {
await message.reply("The entry role configured for this server does not exist. Please contact the server's mod team.");
await message.delete();
return;
}
if (message.content.toLocaleLowerCase() != verificationCode.toLocaleLowerCase()) {
await message.delete();
return;
}
await message.member?.roles.add(role);
await message.delete();
}
}

View file

@ -0,0 +1,50 @@
import { getConnection } from "typeorm";
import DefaultValues from "../constants/DefaultValues";
import Server from "../entity/Server";
import Setting from "../entity/Setting";
export default class SettingsHelper {
public static async GetSetting(key: string, serverId: string): Promise<string | undefined> {
const server = await Server.FetchOneById(Server, serverId, [
"Settings"
]);
if (!server) {
return DefaultValues.GetValue(key);
}
const setting = server.Settings.filter(x => x.Key == key)[0];
if (!setting) {
return DefaultValues.GetValue(key);
}
return setting.Value;
}
public static async SetSetting(key: string, serverId: string, value: string): Promise<void> {
const server = await Server.FetchOneById(Server, serverId, [
"Settings"
]);
if (!server) {
return;
}
const setting = server.Settings.filter(x => x.Key == key)[0];
if (setting) {
setting.UpdateBasicDetails(key, value);
await setting.Save(Setting, setting);
} else {
const newSetting = new Setting(key, value);
await newSetting.Save(Setting, newSetting);
server.AddSettingToServer(newSetting);
await server.Save(Server, server);
}
}
}

View file

@ -12,4 +12,17 @@ export default class StringTools {
return result.join(" ");
}
public static RandomString(length: number) {
let result = "";
const characters = 'abcdefghkmnpqrstuvwxyz23456789';
const charactersLength = characters.length;
for ( var i = 0; i < length; i++ ) {
result += characters.charAt(Math.floor(Math.random() * charactersLength));
}
return result;
}
}

View file

@ -7,7 +7,7 @@ export default class ErrorEmbed extends MessageEmbed {
constructor(context: ICommandContext, message: String) {
super();
super.setColor(process.env.EMBED_COLOUR_ERROR!);
super.setColor(0xd52803);
super.setDescription(message);
this.context = context;

View file

@ -1,4 +1,6 @@
import { MessageEmbed, TextChannel, User, Guild } from "discord.js";
import { ICommandContext } from "../../contracts/ICommandContext";
import SettingsHelper from "../SettingsHelper";
export default class EventEmbed extends MessageEmbed {
public guild: Guild;
@ -6,7 +8,7 @@ export default class EventEmbed extends MessageEmbed {
constructor(guild: Guild, title: string) {
super();
super.setColor(process.env.EMBED_COLOUR!);
super.setColor(0x3050ba);
super.setTitle(title);
this.guild = guild;
@ -38,15 +40,33 @@ export default class EventEmbed extends MessageEmbed {
channel.send(this);
}
public SendToMessageLogsChannel() {
this.SendToChannel(process.env.CHANNELS_LOGS_MESSAGE!)
public async SendToMessageLogsChannel() {
const channelName = await SettingsHelper.GetSetting("channels.logs.message", this.guild.id);
if (!channelName) {
return;
}
this.SendToChannel(channelName);
}
public SendToMemberLogsChannel() {
this.SendToChannel(process.env.CHANNELS_LOGS_MEMBER!)
public async SendToMemberLogsChannel() {
const channelName = await SettingsHelper.GetSetting("channels.logs.member", this.guild.id);
if (!channelName) {
return;
}
this.SendToChannel(channelName);
}
public SendToModLogsChannel() {
this.SendToChannel(process.env.CHANNELS_LOGS_MOD!)
public async SendToModLogsChannel() {
const channelName = await SettingsHelper.GetSetting("channels.logs.mod", this.guild.id);
if (!channelName) {
return;
}
this.SendToChannel(channelName);
}
}

View file

@ -1,6 +1,7 @@
import { MessageEmbed, TextChannel, User } from "discord.js";
import ErrorMessages from "../../constants/ErrorMessages";
import { ICommandContext } from "../../contracts/ICommandContext";
import SettingsHelper from "../SettingsHelper";
import ErrorEmbed from "./ErrorEmbed";
export default class LogEmbed extends MessageEmbed {
@ -9,7 +10,7 @@ export default class LogEmbed extends MessageEmbed {
constructor(context: ICommandContext, title: string) {
super();
super.setColor(process.env.EMBED_COLOUR!);
super.setColor(0x3050ba);
super.setTitle(title);
this.context = context;
@ -46,15 +47,33 @@ export default class LogEmbed extends MessageEmbed {
channel.send(this);
}
public SendToMessageLogsChannel() {
this.SendToChannel(process.env.CHANNELS_LOGS_MESSAGE!)
public async SendToMessageLogsChannel() {
const channelName = await SettingsHelper.GetSetting("channels.logs.message", this.context.message.guild?.id!);
if (!channelName) {
return;
}
this.SendToChannel(channelName);
}
public SendToMemberLogsChannel() {
this.SendToChannel(process.env.CHANNELS_LOGS_MEMBER!)
public async SendToMemberLogsChannel() {
const channelName = await SettingsHelper.GetSetting("channels.logs.member", this.context.message.guild?.id!);
if (!channelName) {
return;
}
this.SendToChannel(channelName);
}
public SendToModLogsChannel() {
this.SendToChannel(process.env.CHANNELS_LOGS_MOD!)
public async SendToModLogsChannel() {
const channelName = await SettingsHelper.GetSetting("channels.logs.mod", this.context.message.guild?.id!);
if (!channelName) {
return;
}
this.SendToChannel(channelName);
}
}

View file

@ -7,7 +7,7 @@ export default class PublicEmbed extends MessageEmbed {
constructor(context: ICommandContext, title: string, description: string) {
super();
super.setColor(process.env.EMBED_COLOUR!);
super.setColor(0x3050ba);
super.setTitle(title);
super.setDescription(description);

View file

@ -2,6 +2,9 @@ import { CoreClient } from "./client/client";
import About from "./commands/about";
import Ban from "./commands/ban";
import Clear from "./commands/clear";
import Code from "./commands/code";
import Config from "./commands/config";
import Disable from "./commands/disable";
import Evaluate from "./commands/eval";
import Help from "./commands/help";
import Kick from "./commands/kick";
@ -9,12 +12,13 @@ import Mute from "./commands/mute";
import Poll from "./commands/poll";
import Role from "./commands/role";
import Rules from "./commands/rules";
import Setup from "./commands/setup";
import Unmute from "./commands/unmute";
import Warn from "./commands/warn";
import MemberEvents from "./events/MemberEvents";
import MessageEvents from "./events/MessageEvents";
export default class Register {
export default class Registry {
public static RegisterCommands(client: CoreClient) {
client.RegisterCommand("about", new About());
client.RegisterCommand("ban", new Ban());
@ -28,6 +32,10 @@ export default class Register {
client.RegisterCommand("rules", new Rules());
client.RegisterCommand("unmute", new Unmute());
client.RegisterCommand("warn", new Warn());
client.RegisterCommand("setup", new Setup());
client.RegisterCommand("config", new Config());
client.RegisterCommand("code", new Code());
client.RegisterCommand("disable", new Disable())
}
public static RegisterEvents(client: CoreClient) {

View file

@ -1,3 +1,4 @@
import { CommandResponse } from "../constants/CommandResponse";
import { ICommandContext } from "../contracts/ICommandContext";
export class Command {
@ -9,6 +10,14 @@ export class Command {
this._roles = [];
}
public precheck(context: ICommandContext): CommandResponse {
return CommandResponse.Ok;
}
public async precheckAsync(context: ICommandContext): Promise<CommandResponse> {
return CommandResponse.Ok;
}
public execute(context: ICommandContext) {
}

View file

@ -1,19 +1,15 @@
import { CoreClient } from "./client/client";
import * as dotenv from "dotenv";
import Register from "./Register";
import registry from "./registry";
dotenv.config();
const requiredConfigs = [
"EMBED_COLOUR",
"EMBED_COLOUR_ERROR",
"ROLES_MODERATOR",
"ROLES_MUTED",
"CHANNELS_LOGS_MESSAGE",
"CHANNELS_LOGS_MEMBER",
"CHANNELS_LOGS_MOD",
"COMMANDS_ROLE_ROLES",
"COMMANDS_RULES_FILE"
const requiredConfigs: string[] = [
"BOT_TOKEN",
"BOT_VER",
"BOT_AUTHOR",
"BOT_DATE",
"BOT_OWNERID",
];
requiredConfigs.forEach(config => {
@ -22,9 +18,11 @@ requiredConfigs.forEach(config => {
}
});
const client = new CoreClient();
const devmode = process.argv.find(x => x.toLowerCase() == "--dev") != null;
Register.RegisterCommands(client);
Register.RegisterEvents(client);
const client = new CoreClient(devmode);
registry.RegisterCommands(client);
registry.RegisterEvents(client);
client.start();