diff --git a/.eslintrc.json b/.eslintrc.json index 4eafd9e..af5e324 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -26,6 +26,11 @@ "rules": { "@typescript-eslint/no-unused-vars": "off", "@typescript-eslint/no-non-null-assertion": "off", + "@typescript-eslint/no-misused-promises": ["error", { + "checksVoidReturn": { + "arguments": false + } + }], "accessor-pairs": "warn", "array-callback-return": "warn", "array-bracket-newline": [ diff --git a/@types/Client.ts b/@types/Client.ts index a404e83..45f9107 100644 --- a/@types/Client.ts +++ b/@types/Client.ts @@ -42,7 +42,7 @@ import { InvokerWrapper, MemberWrapper, UserWrapper } from '../src/client/compon import GuildWrapper from '../src/client/components/wrappers/GuildWrapper.js'; import DiscordClient from '../src/client/DiscordClient.js'; import { CommandOption, Inhibitor } from '../src/client/interfaces/index.js'; -import { MuteType } from './Settings.js'; +import { MuteType, TextCommandsSettings } from './Settings.js'; import { ClientEvents } from './Events.js'; import { FilterResult } from './Utils.js'; import { GuildSettingTypes } from './Guild.js'; @@ -114,6 +114,7 @@ export type ObserverOptions = { export type InhibitorOptions = { name?: string, guild?: boolean + // Higher numbers come first priority?: number, silent?: boolean } & Partial @@ -501,4 +502,15 @@ export declare interface ExtendedVoiceState extends VoiceState { export declare interface ExtendedInvite extends Invite { guildWrapper?: GuildWrapper +} + +export type EntitySettings = { + [key: string]: unknown, + textcommands: TextCommandsSettings +} + +export type EntityData = { + id: Snowflake, + banned?: boolean, + settings?: EntitySettings } \ No newline at end of file diff --git a/@types/Guild.d.ts b/@types/Guild.d.ts index f594f22..5941cfa 100644 --- a/@types/Guild.d.ts +++ b/@types/Guild.d.ts @@ -48,6 +48,7 @@ import { WordWatcherSettings } from './Settings.js'; import { ObjectId } from 'mongodb'; +import { EntityData, EntitySettings } from './Client.ts'; export type GuildSettingTypes = | AutomodSettings @@ -117,7 +118,7 @@ export type GuildSettings = { invitefilter: InviteFilterSettings, mentionfilter: MentionFilterSettings, raidprotection: RaidprotectionSettings, -} +} & EntitySettings export type PermissionSet = { global: string[], @@ -143,7 +144,7 @@ export type GuildData = { modlogs?: boolean, settings?: boolean } -} +} & EntityData export type CallbackData = { type: string, diff --git a/@types/Settings.ts b/@types/Settings.ts index 46dddb0..a3fe4f8 100644 --- a/@types/Settings.ts +++ b/@types/Settings.ts @@ -17,11 +17,6 @@ import { Snowflake } from 'discord.js'; import { InfractionType, SettingAction } from './Client.js'; -export type UserSettings = { - prefix?: string, - locale?: string -} - export type Setting = { // eslint-disable-next-line @typescript-eslint/no-explicit-any [key: string]: any diff --git a/@types/User.d.ts b/@types/User.d.ts new file mode 100644 index 0000000..8ee03f8 --- /dev/null +++ b/@types/User.d.ts @@ -0,0 +1,34 @@ +// Galactic - Discord moderation bot +// Copyright (C) 2024 Navy.gif + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +import { EntityData } from './Client.ts'; +import { LocaleSettings, TextCommandsSettings } from './Settings.ts'; + +export type UserSettingTypes = + | LocaleSettings + | TextCommandsSettings + +export type UserSettings = { + [key: string]: UserSettingTypes + textcommands: TextCommandsSettings, + locale: LocaleSettings +} & EntitySettings + +export type UserData = { + [key: string]: unknown + settings?: UserSettings, + debug?: boolean +} & EntityData \ No newline at end of file diff --git a/README.md b/README.md index bb8596d..928d286 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,8 @@ Internal/technical documentation will be in the source files adjacent to the cod - MariaDB (TBD) - RabbitMQ (maybe, TBD) +A Docker compose file is available for convenience for setting up databases for development or personal deployment. + ### Running - Install the dependencies: `yarn install` - Run the bot: `yarn start` diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..80d27ac --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,47 @@ +# Compose file for the bot's DB stack +# Modify as necessary +# TODO: Setup Traefik labels + +services: + mongo: + image: mongo + restart: unless-stopped + volumes: + - mongo-data:/data/db + ports: + - 27017:27017 + environment: + MONGO_INITDB_ROOT_USERNAME: root + MONGO_INITDB_ROOT_PASSWORD: I_am_ROOT + + maria: + image: mariadb + restart: unless-stopped + volumes: + - maria-data:/var/lib/mysql + ports: + - 3306:3306 + environment: + MARIADB_ROOT_PASSWORD: I_am_ROOT + +# Web interfaces for interacting with the databases + adminer: + image: adminer + restart: unless-stopped + ports: + - 8080:8080 + + mongo-express: + image: mongo-express + restart: unless-stopped + ports: + - 8081:8081 + environment: + ME_CONFIG_MONGODB_ADMINUSERNAME: root + ME_CONFIG_MONGODB_ADMINPASSWORD: I_am_ROOT + ME_CONFIG_MONGODB_URL: mongodb://root:I_am_ROOT@mongo:27017/ + ME_CONFIG_BASICAUTH: false + +volumes: + mongo-data: + maria-data: \ No newline at end of file diff --git a/package.json b/package.json index 0c4fb0f..e9fd58f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new-gbot", - "version": "3.3.0-dev.1", + "version": "3.4.0", "description": "New iteration of GalacticBot", "main": "build/index.js", "type": "module", diff --git a/src/client/DiscordClient.ts b/src/client/DiscordClient.ts index df78fe1..5f8e019 100644 --- a/src/client/DiscordClient.ts +++ b/src/client/DiscordClient.ts @@ -235,6 +235,7 @@ class DiscordClient extends Client this.#logger.error(`Unhandled rejection:\n${err?.stack || err}`); }); + // eslint-disable-next-line @typescript-eslint/no-misused-promises process.on('message', this.#handleMessage.bind(this)); process.on('SIGINT', () => this.logger.info('Received SIGINT')); process.on('SIGTERM', () => this.logger.info('Received SIGTERM')); @@ -524,6 +525,7 @@ class DiscordClient extends Client return this.localeLoader.format(language, index, params, code); } + // TODO: Combine these async getGuildWrapper (id: string) { if (this.#guildWrappers.has(id)) @@ -551,6 +553,11 @@ class DiscordClient extends Client return wrapper; } + async getGuildWrappers (ids: string[]) + { + return (await Promise.all(ids.map(id => this.getGuildWrapper(id)))).filter(entry => entry !== null); + } + getUserWrapper(resolveable: UserResolveable, fetch?: false): UserWrapper | null; getUserWrapper(resolveable: UserResolveable, fetch?: true): Promise; getUserWrapper (resolveable: UserResolveable, fetch = true) @@ -582,6 +589,16 @@ class DiscordClient extends Client }); } + getUserWrappers(resolveables: UserResolveable[], fetch?: true): Promise; + getUserWrappers(resolveables: UserResolveable[], fetch?: false): UserWrapper[]; + getUserWrappers (resolveables: UserResolveable[], fetch = true): Promise | UserWrapper[] + { + if (!fetch) + return resolveables.map(r => this.getUserWrapper(r, false)).filter(e => e !== null) as UserWrapper[]; + return Promise.all(resolveables.map(resolveable => this.getUserWrapper(resolveable))) + .then(entries => entries.filter(e => e !== null)) as Promise; + } + async fetchInvite (invite: InviteResolvable, opts?: ClientFetchInviteOptions) { const code = DataResolver.resolveInviteCode(invite); diff --git a/src/client/components/commands/developer/Botban.ts b/src/client/components/commands/developer/Botban.ts new file mode 100644 index 0000000..9dc6d55 --- /dev/null +++ b/src/client/components/commands/developer/Botban.ts @@ -0,0 +1,71 @@ +// Galactic - Discord moderation bot +// Copyright (C) 2024 Navy.gif + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +import { CommandOptionType, CommandParams } from '../../../../../@types/Client.js'; +import { Command } from '../../../interfaces/index.js'; +import InvokerWrapper from '../../wrappers/InvokerWrapper.js'; +import DiscordClient from '../../../DiscordClient.js'; + +class BotbanCommand extends Command +{ + + constructor (client: DiscordClient) + { + super(client, { + name: 'botban', + aliases: [ 'bban' ], + restricted: true, + moduleName: 'developer', + options: [ + { name: 'users', type: CommandOptionType.USERS }, + { name: 'guild', type: CommandOptionType.STRING }, + { name: 'unban', type: CommandOptionType.BOOLEAN, flag: true, defaultValue: true, valueOptional: true } + ] + }); + } + + async execute (_invoker: InvokerWrapper, { users, guild, unban }: CommandParams) + { + let out = 'Revoked bot access from:\n'; + if (users) + { + const ids = users.asUsers.map(u => u.id); + const wrappers = await this.client.getUserWrappers(ids); + out += '**Users**\n'; + for (const wrapper of wrappers) + { + out += `\t${wrapper.displayName}\n`; + await wrapper.setBotBan(!unban?.asBool); + } + } + if (guild) + { + const ids = guild.asString.split(' '); + const wrappers = await this.client.getGuildWrappers(ids); + out += '**Servers**\n'; + for (const wrapper of wrappers) + { + out += `\t${wrapper}`; + await wrapper.setBotBan(!unban?.asBool); + } + } + + return out; + } + +} + +export default BotbanCommand; \ No newline at end of file diff --git a/src/client/components/commands/developer/Debug.ts b/src/client/components/commands/developer/Debug.ts index fd903d3..24a3f55 100644 --- a/src/client/components/commands/developer/Debug.ts +++ b/src/client/components/commands/developer/Debug.ts @@ -28,19 +28,46 @@ class DebugCommand extends Command restricted: true, moduleName: 'developer', options: [ - { name: 'guild', required: true }, - { name: 'enabled', required: true, type: CommandOptionType.BOOLEAN } + { + name: [ 'enable', 'disable' ], + type: CommandOptionType.SUB_COMMAND, + options: [ + { name: 'guild', required: true }, + ] + }, + { + name: 'list', + type: CommandOptionType.SUB_COMMAND, + } ] }); } - async execute (_invoker: InvokerWrapper, options: CommandParams) + async execute (invoker: InvokerWrapper, options: CommandParams) { - const guildId = options.guild!.asString; - return this.enableDebug(guildId, options.enabled?.asBool); + const { subcommand } = invoker; + if (!subcommand) + throw new Error('Missing subcommand'); + + if ([ 'enable', 'disable' ].includes(subcommand.name)) + { + const guildId = options.guild!.asString; + return this.toggleDebug(guildId, subcommand.name === 'enable'); + } + + if (subcommand.name === 'list') + return this.listDebugGuilds(); } - async enableDebug (guildId: string, enabled = false) + async listDebugGuilds () + { + const data = await this.client.mongodb.guilds.find({ debug: true }); + const ids = data.map(entry => entry.id ?? entry.guildId); + const guilds = await this.client.getGuildWrappers(ids); + return guilds.map(guild => `**${guild.name}**: ${guild.id}`).join('\n'); + } + + async toggleDebug (guildId: string, enabled = false) { let output = ''; if (this.client.shard) diff --git a/src/client/components/inhibitors/Banned.ts b/src/client/components/inhibitors/Banned.ts new file mode 100644 index 0000000..3a271f2 --- /dev/null +++ b/src/client/components/inhibitors/Banned.ts @@ -0,0 +1,47 @@ +// Galactic - Discord moderation bot +// Copyright (C) 2024 Navy.gif + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +import { InhibitorResponse } from '../../../../@types/Client.js'; +import Util from '../../../utilities/Util.js'; +import DiscordClient from '../../DiscordClient.js'; +import Inhibitor from '../../interfaces/Inhibitor.js'; +import InvokerWrapper from '../wrappers/InvokerWrapper.js'; + +class Banned extends Inhibitor +{ + constructor (client: DiscordClient) + { + super(client, { + name: 'banned', + priority: 10, + silent: true + }); + } + + async execute (invoker: InvokerWrapper): Promise + { + const user = await invoker.userWrapper(); + const { guild } = invoker; + if (user && await user.botBanned()) + return super._fail({ noun: Util.capitalise(invoker.format('NOUN_USER')) }); + if (guild && await guild.botBanned()) + return super._fail({ noun: Util.capitalise(invoker.format('NOUN_GUILD')) }); + return super._succeed(); + } + +} + +export default Banned; \ No newline at end of file diff --git a/src/client/components/inhibitors/ChannelIgnore.ts b/src/client/components/inhibitors/ChannelIgnore.ts index d29b1dc..e0b66d5 100644 --- a/src/client/components/inhibitors/ChannelIgnore.ts +++ b/src/client/components/inhibitors/ChannelIgnore.ts @@ -24,7 +24,7 @@ class ChannelIgnore extends Inhibitor { super(client, { name: 'channelIgnore', - priority: 9, + priority: 8, guild: true, silent: true }); diff --git a/src/client/components/inhibitors/ClientPermissions.ts b/src/client/components/inhibitors/ClientPermissions.ts index 5b4359e..4c64de9 100644 --- a/src/client/components/inhibitors/ClientPermissions.ts +++ b/src/client/components/inhibitors/ClientPermissions.ts @@ -25,7 +25,7 @@ class ClientPermissions extends Inhibitor { super(client, { name: 'clientPermissions', - priority: 10, + priority: 9, guarded: true, guild: true }); diff --git a/src/client/components/managers/ModerationManager.ts b/src/client/components/managers/ModerationManager.ts index 3d0a4a0..964981b 100644 --- a/src/client/components/managers/ModerationManager.ts +++ b/src/client/components/managers/ModerationManager.ts @@ -170,8 +170,7 @@ class ModerationManager implements Initialisable, CallbackClient constructor (client: DiscordClient) { this.#client = client; - // this.#callbacks = new Collection(); - this.#logger = client.createLogger(this); // new Logger({ name: 'ModMngr' }); + this.#logger = client.createLogger(this); this.#infractionClasses = Constant.Infractions; this.#ready = false; } @@ -418,11 +417,6 @@ class ModerationManager implements Initialisable, CallbackClient /** * - * @param {class} Infraction - * @param {User | GuildMember} target - * @param {object} info - * @return {Infraction} - * @memberof ModerationManager */ // eslint-disable-next-line max-lines-per-function async _handleTarget ( @@ -431,7 +425,6 @@ class ModerationManager implements Initialisable, CallbackClient info: HandleTargetData ) { - // wrapper: guildWrapper const { reason, force, guild } = info; const { automod, modpoints } = await guild.settings(); const { Type: type } = Infraction; @@ -609,13 +602,12 @@ class ModerationManager implements Initialisable, CallbackClient async handleCallback (_id: string, infraction: InfractionJSON) { - // const infraction = await this.#client.mongodb.infractions.findOne({ id }); if (!infraction) return; this.#logger.debug(`Infraction callback: ${infraction.id} (${infraction.type})`); const undoClass = Constant.Infractions[Constants.InfractionOpposites[infraction.type]]; if (!undoClass) - return; + return this.#logger.error(`Missing undo class for ${infraction.type}`); const guild = await this.#client.getGuildWrapper(infraction.guild!); if (!guild) @@ -646,7 +638,7 @@ class ModerationManager implements Initialisable, CallbackClient throw new Error('Missing executor'); try { - await new undoClass(this.#client, this.#logger, { + const result = await new undoClass(this.#client, this.#logger, { type: undoClass.Type, reason: `AUTO-${Constants.InfractionOpposites[infraction.type]} from Case ${infraction.case}`, channel, @@ -657,6 +649,8 @@ class ModerationManager implements Initialisable, CallbackClient target, executor }).execute(); + if (result.error) + this.#logger.error(result); } catch (err) { diff --git a/src/client/components/observers/CommandHandler.ts b/src/client/components/observers/CommandHandler.ts index e2a841f..83fc918 100644 --- a/src/client/components/observers/CommandHandler.ts +++ b/src/client/components/observers/CommandHandler.ts @@ -287,7 +287,7 @@ class CommandHandler extends Observer { if (!(invoker.command instanceof SettingsCommand)) invoker.command.error(now); - this.logger.error(`\n[${invoker.type.toUpperCase()}] Command ${debugstr} errored:\nGuild: ${invoker.inGuild() ? invoker.guild?.name : 'dms'} (${invoker.guild?.id || ''})\nOptions:\n${Object.keys(options).map((key) => `[${key}: ${options[key].asString} (${options[key].rawValue})]`).join('\n')}\n${error.stack || error}`); + this.logger.error(`\n[${invoker.type.toUpperCase()}] Command "${debugstr}" errored:\nGuild: ${invoker.inGuild() ? invoker.guild?.name : 'dms'} (${invoker.guild?.id || ''})\nOptions:\n${Object.keys(options).map((key) => `[${key}: ${options[key].asString} (${options[key].rawValue})]`).join('\n')}\n${error.stack || error}`); this._generateError(invoker, { type: 'commandHandler' }); } return; diff --git a/src/client/components/wrappers/EntityWrapper.ts b/src/client/components/wrappers/EntityWrapper.ts new file mode 100644 index 0000000..5d68138 --- /dev/null +++ b/src/client/components/wrappers/EntityWrapper.ts @@ -0,0 +1,40 @@ +// Galactic - Discord moderation bot +// Copyright (C) 2024 Navy.gif + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +// import { LoggerClient } from '@navy.gif/logger'; +// import DiscordClient from '../../DiscordClient.js'; +// import { EntityData } from '../../../../@types/Client.js'; + +// // TODO: Consider refactoring Guild and User wrappers to have a common ancestor +// class EntityWrapper +// { +// #client: DiscordClient; +// #data!: DataType; +// #logger: LoggerClient; +// constructor (client: DiscordClient) +// { +// this.#client = client; +// this.#logger = client.createLogger(this, { name: this.constructor.name }); +// } + +// async banned () +// { +// return this.#data.banned; +// } + +// } + +// export default EntityWrapper; \ No newline at end of file diff --git a/src/client/components/wrappers/GuildWrapper.ts b/src/client/components/wrappers/GuildWrapper.ts index 30474d1..3516acf 100644 --- a/src/client/components/wrappers/GuildWrapper.ts +++ b/src/client/components/wrappers/GuildWrapper.ts @@ -26,7 +26,7 @@ import { } from '../../../../@types/Guild.js'; import DiscordClient from '../../DiscordClient.js'; -const configVersion = '3.slash.2'; +const configVersion = '3.slash.3'; import { Guild, @@ -45,6 +45,7 @@ import { import MemberWrapper from './MemberWrapper.js'; import { FilterUtil, Util } from '../../../utilities/index.js'; import { LoggerClient } from '@navy.gif/logger'; +import { ObjectId } from 'mongodb'; class GuildWrapper { @@ -104,15 +105,16 @@ class GuildWrapper { if (this.#data) return this.#data; - const data = await this.#client.mongodb.guilds.findOne({ guildId: this.id }); + + const data = await this.#client.mongodb.guilds.findOne({ $or: [{ guildId: this.id }, { id: this.id }] }); if (!data) { - this.#data = {}; + this.#data = { id: this.id }; return this.#data; } if (data._version === '3.slash') { - const oldSettings = data as GuildSettings; + const oldSettings = data as unknown as GuildSettings; const keys = Object.keys(this.defaultConfig); const settings: PartialGuildSettings = {}; for (const key of keys) @@ -125,6 +127,13 @@ class GuildWrapper await this.#client.mongodb.guilds.deleteOne({ guildId: this.id }); await this.#client.mongodb.guilds.updateOne({ guildId: this.id }, { $set: data }); } + if (data._version === '3.slash.2') + { + data.id = data.guildId as string; + delete data.guildId; + data._version = configVersion; // 3.slash.3 + await this.#client.mongodb.guilds.updateOne({ _id: data._id as ObjectId }, { $set: data }); + } this.#data = data; return data; } @@ -136,10 +145,8 @@ class GuildWrapper return this.#settings; const data = await this.fetchData(); - // eslint-disable-next-line prefer-const const { settings, - // _imported } = data; const { defaultConfig } = this; @@ -168,7 +175,7 @@ class GuildWrapper return this.#settings; } - async updateData (data: GuildData) + async updateData (data: Partial) { try { @@ -194,6 +201,22 @@ class GuildWrapper } as GuildSettings; } + /** + * Check if guild has been banned from using the bot + */ + async botBanned () + { + return false; + } + + async setBotBan (value = true) + { + if (!this.#data) + await this.fetchData(); + this.#data.banned = value; + await this.updateData({ banned: value }); + } + async permissions () { if (this.#permissions) diff --git a/src/client/components/wrappers/UserWrapper.ts b/src/client/components/wrappers/UserWrapper.ts index cc3bdb1..fcdebfd 100644 --- a/src/client/components/wrappers/UserWrapper.ts +++ b/src/client/components/wrappers/UserWrapper.ts @@ -16,171 +16,104 @@ import { ImageURLOptions, MessageCreateOptions, MessagePayload, User } from 'discord.js'; import DiscordClient from '../../DiscordClient.js'; -import { UserSettings } from '../../../../@types/Settings.js'; import { LoggerClient } from '@navy.gif/logger'; +import { UserData, UserSettings } from '../../../../@types/User.js'; class UserWrapper { #client: DiscordClient; #user: User; - #settings: UserSettings; - // #points: { - // [key: string]: { - // expirations: Expiry[], - // points: number - // } - // }; - + #data!: UserData; #logger: LoggerClient; + #settings: UserSettings; constructor (client: DiscordClient, user: User) { this.#client = client; this.#user = user; this.#logger = client.createLogger({ name: `User: ${user.id}` }); + } - this.#settings = {}; - // this.#points = {}; + async fetchData () + { + if (this.#data) + return this.#data; + + const data = await this.#client.mongodb.users.findOne({ id: this.id }); + if (!data) + { + this.#data = { id: this.id }; + return this.#data; + } + + this.#data = data; + return data; } async settings (forceFetch = false) { - if (this.#settings && !forceFetch) - return this.#settings; + if (this.#data && !forceFetch) + return this.#data.settings; + + const data = await this.fetchData(); + const { settings } = data; + const { defaultConfig } = this; - const settings = await this.#client.mongodb.users.findOne({ userId: this.id }); if (settings) - this.#settings = { ...this.defaultConfig, ...settings }; - else - this.#settings = { userId: this.id, ...this.defaultConfig }; + { + const keys = Object.keys(settings); + for (const key of keys) + { + defaultConfig[key] = { ...defaultConfig[key], [key]: settings[key] }; + } + } + + this.#settings = defaultConfig; return this.#settings; } - // async fetchPoints (guild: GuildWrapper) - // { - // let index = this.#points[guild.id]; - // if (index) - // return Promise.resolve(index); - // this.#points[guild.id] = { - // expirations: [], - // points: 0 - // }; - // index = this.#points[guild.id]; - - // const filter = { - // guild: guild.id, - // target: this.id, - // resolved: false, - // // points: { $gte: 0 }, - // // $or: [{ expiration: 0 }, { expiration: { $gte: now } }] - // }; - // const find = await this.#client.mongodb.infractions.find( - // filter, - // { projection: { id: 1, points: 1, expiration: 1 } } - // ); - - // if (find.length) - // { - // for (const { points: p, expiration, id } of find) - // { - // let points = p; - // // Imported cases may have false or null - // if (typeof points !== 'number') - // points = 0; - // if (expiration > 0) - // { - // index.expirations.push({ points, expiration, id }); - // } - // else - // { - // index.points += points; - // } - // } - // } - // return index; - // } - - // async editPoints (guild: GuildWrapper, { id, diff, expiration }: {id: string, diff: number, expiration: unknown }) - // { - // const points = await this.fetchPoints(guild); - // if (expiration) - // { - // const expiry = points.expirations.find((exp) => exp.id === id) as Expiry; - // expiry.points += diff; - // } - // else - // points.points += diff; - // return this.totalPoints(guild); - // } - - // async editExpiration (guild: GuildWrapper, { id, expiration, points }: { id: string, expiration: number, points: number }) - // { - // const index = await this.fetchPoints(guild); - // const i = index.expirations.findIndex((exp) => exp.id === id); - // if (i > -1) - // index.expirations[i].expiration = expiration; - // else - // { - // index.points -= points; - // index.expirations.push({ id, points, expiration }); - // } - // return this.totalPoints(guild); - // } - - // async totalPoints (guild: GuildWrapper, point?: { id: string, points: number, expiration: number, timestamp: number }) - // { // point = { points: x, expiration: x, timestamp: x} - // const index = await this.fetchPoints(guild); - // const now = Date.now(); - - // if (point) - // { - // if (point.expiration > 0) - // { - // index.expirations.push({ id: point.id, points: point.points, expiration: point.expiration + point.timestamp }); - // } - // else - // { - // index.points += point.points; - // } - // } - - // let expirationPoints = index.expirations.map((e) => - // { - // if (e.expiration >= now) - // return e.points; - // return 0; - // }); - - // if (expirationPoints.length === 0) - // expirationPoints = [ 0 ]; - // return expirationPoints.reduce((p, v) => p + v) + index.points; - // } - - async updateSettings (data: UserSettings) - { // Update property (upsert true) - updateOne - if (!this.#settings) - await this.settings(); - try - { - await this.#client.mongodb.users.updateOne( - { guildId: this.id }, - { $set: data } - ); - this.#settings = { - ...this.#settings, - ...data - }; - this.#storageLog(`Database Update (guild:${this.id}).`); - } - catch (error) - { - this.#storageError(error as Error); - } - return true; + /** + * Check whether the user is banned from using the bot + */ + async botBanned (): Promise + { + const data = await this.fetchData(); + return data?.banned ?? false; } - get defaultConfig () + async setBotBan (value = true) + { + if (!this.#data) + await this.fetchData(); + this.#data!.banned = value; + await this.updateData({ banned: value }); + } + + async updateSettings (settings: Partial) + { + if (!this.#data?.settings) + await this.settings(); + + await this.updateData({ settings: settings as UserSettings }); + } + + async updateData (data: Partial) + { + try + { + await this.#client.mongodb.users.updateOne({ id: this.id }, { $set: data }); + this.#data = { ...this.#data, ...data } as UserData; + this.#storageLog('Data update'); + } + catch (err) + { + const error = err as Error; + this.#storageError(error); + } + } + + get defaultConfig (): UserSettings { return JSON.parse(JSON.stringify(this.#client.defaultConfig('USER'))); } @@ -242,14 +175,114 @@ class UserWrapper get prefix () { - return this.#settings.prefix; + return this.#data?.settings?.textcommands.prefix; } get locale () { - return this.#settings.locale; + return this.#data?.settings?.locale.language; } } -export default UserWrapper; \ No newline at end of file +export default UserWrapper; + +// async fetchPoints (guild: GuildWrapper) +// { +// let index = this.#points[guild.id]; +// if (index) +// return Promise.resolve(index); +// this.#points[guild.id] = { +// expirations: [], +// points: 0 +// }; +// index = this.#points[guild.id]; + +// const filter = { +// guild: guild.id, +// target: this.id, +// resolved: false, +// // points: { $gte: 0 }, +// // $or: [{ expiration: 0 }, { expiration: { $gte: now } }] +// }; +// const find = await this.#client.mongodb.infractions.find( +// filter, +// { projection: { id: 1, points: 1, expiration: 1 } } +// ); + +// if (find.length) +// { +// for (const { points: p, expiration, id } of find) +// { +// let points = p; +// // Imported cases may have false or null +// if (typeof points !== 'number') +// points = 0; +// if (expiration > 0) +// { +// index.expirations.push({ points, expiration, id }); +// } +// else +// { +// index.points += points; +// } +// } +// } +// return index; +// } + +// async editPoints (guild: GuildWrapper, { id, diff, expiration }: {id: string, diff: number, expiration: unknown }) +// { +// const points = await this.fetchPoints(guild); +// if (expiration) +// { +// const expiry = points.expirations.find((exp) => exp.id === id) as Expiry; +// expiry.points += diff; +// } +// else +// points.points += diff; +// return this.totalPoints(guild); +// } + +// async editExpiration (guild: GuildWrapper, { id, expiration, points }: { id: string, expiration: number, points: number }) +// { +// const index = await this.fetchPoints(guild); +// const i = index.expirations.findIndex((exp) => exp.id === id); +// if (i > -1) +// index.expirations[i].expiration = expiration; +// else +// { +// index.points -= points; +// index.expirations.push({ id, points, expiration }); +// } +// return this.totalPoints(guild); +// } + +// async totalPoints (guild: GuildWrapper, point?: { id: string, points: number, expiration: number, timestamp: number }) +// { // point = { points: x, expiration: x, timestamp: x} +// const index = await this.fetchPoints(guild); +// const now = Date.now(); + +// if (point) +// { +// if (point.expiration > 0) +// { +// index.expirations.push({ id: point.id, points: point.points, expiration: point.expiration + point.timestamp }); +// } +// else +// { +// index.points += point.points; +// } +// } + +// let expirationPoints = index.expirations.map((e) => +// { +// if (e.expiration >= now) +// return e.points; +// return 0; +// }); + +// if (expirationPoints.length === 0) +// expirationPoints = [ 0 ]; +// return expirationPoints.reduce((p, v) => p + v) + index.points; +// } \ No newline at end of file diff --git a/src/client/interfaces/Infraction.ts b/src/client/interfaces/Infraction.ts index ad39b90..295d858 100644 --- a/src/client/interfaces/Infraction.ts +++ b/src/client/interfaces/Infraction.ts @@ -264,13 +264,13 @@ class Infraction } } - if (this.#duration) - await this.#client.moderation.handleTimedInfraction(this.json); - - /* LMAOOOO PLEASE DONT JUDGE ME */ + // Remove the role structures as they will cause problems when serialising for database if (this.#data.roles) delete this.#data.roles; + if (this.#duration) + await this.#client.moderation.handleTimedInfraction(this.json); + return this.save(); } diff --git a/src/client/storage/providers/MongoDBProvider.ts b/src/client/storage/providers/MongoDBProvider.ts index c1f5d6a..f0fdcaf 100644 --- a/src/client/storage/providers/MongoDBProvider.ts +++ b/src/client/storage/providers/MongoDBProvider.ts @@ -17,8 +17,8 @@ import { CallbackInfo } from '../../../../@types/CallbackManager.js'; import { InfractionJSON } from '../../../../@types/Client.js'; import { AttachmentData, GuildData, GuildPermissions, MessageLogEntry, RoleCacheEntry, WebhookEntry, WordWatcherEntry } from '../../../../@types/Guild.js'; -import { UserSettings } from '../../../../@types/Settings.js'; import { MongoDBOptions } from '../../../../@types/Storage.js'; +import { UserData } from '../../../../@types/User.js'; import DiscordClient from '../../DiscordClient.js'; import { MongodbTable } from '../interfaces/index.js'; import Provider from '../interfaces/Provider.js'; @@ -142,7 +142,7 @@ class MongoDBProvider extends Provider get users () { - return this.tables.users as MongodbTable; + return this.tables.users as MongodbTable; } get roleCache () diff --git a/src/localization/en_gb/general/en_gb_inhibitors.lang b/src/localization/en_gb/general/en_gb_inhibitors.lang index 2843996..ed1c6c1 100644 --- a/src/localization/en_gb/general/en_gb_inhibitors.lang +++ b/src/localization/en_gb/general/en_gb_inhibitors.lang @@ -26,4 +26,7 @@ The command **{command}** requires the __bot__ to have permissions to use. The command **{command}** can only be run by developers. [INHIBITOR_GUILDONLY_ERROR] -The command **{command}** is only available in servers. \ No newline at end of file +The command **{command}** is only available in servers. + +[INHIBITOR_BANNED_ERROR] +{noun} has been banned from using the bot. \ No newline at end of file diff --git a/src/localization/en_gb/general/en_gb_nouns.lang b/src/localization/en_gb/general/en_gb_nouns.lang new file mode 100644 index 0000000..645707d --- /dev/null +++ b/src/localization/en_gb/general/en_gb_nouns.lang @@ -0,0 +1,5 @@ +[NOUN_USER] +user + +[NOUN_GUILD] +server \ No newline at end of file