Botban feature #16

Merged
Navy.gif merged 11 commits from development into alpha 2024-09-30 17:50:50 +02:00
22 changed files with 540 additions and 184 deletions

View File

@ -26,6 +26,11 @@
"rules": { "rules": {
"@typescript-eslint/no-unused-vars": "off", "@typescript-eslint/no-unused-vars": "off",
"@typescript-eslint/no-non-null-assertion": "off", "@typescript-eslint/no-non-null-assertion": "off",
"@typescript-eslint/no-misused-promises": ["error", {
"checksVoidReturn": {
"arguments": false
}
}],
"accessor-pairs": "warn", "accessor-pairs": "warn",
"array-callback-return": "warn", "array-callback-return": "warn",
"array-bracket-newline": [ "array-bracket-newline": [

View File

@ -42,7 +42,7 @@ import { InvokerWrapper, MemberWrapper, UserWrapper } from '../src/client/compon
import GuildWrapper from '../src/client/components/wrappers/GuildWrapper.js'; import GuildWrapper from '../src/client/components/wrappers/GuildWrapper.js';
import DiscordClient from '../src/client/DiscordClient.js'; import DiscordClient from '../src/client/DiscordClient.js';
import { CommandOption, Inhibitor } from '../src/client/interfaces/index.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 { ClientEvents } from './Events.js';
import { FilterResult } from './Utils.js'; import { FilterResult } from './Utils.js';
import { GuildSettingTypes } from './Guild.js'; import { GuildSettingTypes } from './Guild.js';
@ -114,6 +114,7 @@ export type ObserverOptions = {
export type InhibitorOptions = { export type InhibitorOptions = {
name?: string, name?: string,
guild?: boolean guild?: boolean
// Higher numbers come first
priority?: number, priority?: number,
silent?: boolean silent?: boolean
} & Partial<ComponentOptions> } & Partial<ComponentOptions>
@ -502,3 +503,14 @@ export declare interface ExtendedVoiceState extends VoiceState {
export declare interface ExtendedInvite extends Invite { export declare interface ExtendedInvite extends Invite {
guildWrapper?: GuildWrapper guildWrapper?: GuildWrapper
} }
export type EntitySettings = {
[key: string]: unknown,
textcommands: TextCommandsSettings
}
export type EntityData = {
id: Snowflake,
banned?: boolean,
settings?: EntitySettings
}

5
@types/Guild.d.ts vendored
View File

@ -48,6 +48,7 @@ import {
WordWatcherSettings WordWatcherSettings
} from './Settings.js'; } from './Settings.js';
import { ObjectId } from 'mongodb'; import { ObjectId } from 'mongodb';
import { EntityData, EntitySettings } from './Client.ts';
export type GuildSettingTypes = export type GuildSettingTypes =
| AutomodSettings | AutomodSettings
@ -117,7 +118,7 @@ export type GuildSettings = {
invitefilter: InviteFilterSettings, invitefilter: InviteFilterSettings,
mentionfilter: MentionFilterSettings, mentionfilter: MentionFilterSettings,
raidprotection: RaidprotectionSettings, raidprotection: RaidprotectionSettings,
} } & EntitySettings
export type PermissionSet = { export type PermissionSet = {
global: string[], global: string[],
@ -143,7 +144,7 @@ export type GuildData = {
modlogs?: boolean, modlogs?: boolean,
settings?: boolean settings?: boolean
} }
} } & EntityData
export type CallbackData = { export type CallbackData = {
type: string, type: string,

View File

@ -17,11 +17,6 @@
import { Snowflake } from 'discord.js'; import { Snowflake } from 'discord.js';
import { InfractionType, SettingAction } from './Client.js'; import { InfractionType, SettingAction } from './Client.js';
export type UserSettings = {
prefix?: string,
locale?: string
}
export type Setting = { export type Setting = {
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
[key: string]: any [key: string]: any

34
@types/User.d.ts vendored Normal file
View File

@ -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 <https://www.gnu.org/licenses/>.
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

View File

@ -18,6 +18,8 @@ Internal/technical documentation will be in the source files adjacent to the cod
- MariaDB (TBD) - MariaDB (TBD)
- RabbitMQ (maybe, TBD) - RabbitMQ (maybe, TBD)
A Docker compose file is available for convenience for setting up databases for development or personal deployment.
### Running ### Running
- Install the dependencies: `yarn install` - Install the dependencies: `yarn install`
- Run the bot: `yarn start` - Run the bot: `yarn start`

47
docker-compose.yml Normal file
View File

@ -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:

View File

@ -235,6 +235,7 @@ class DiscordClient extends Client
this.#logger.error(`Unhandled rejection:\n${err?.stack || err}`); 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('message', this.#handleMessage.bind(this));
process.on('SIGINT', () => this.logger.info('Received SIGINT')); process.on('SIGINT', () => this.logger.info('Received SIGINT'));
process.on('SIGTERM', () => this.logger.info('Received SIGTERM')); process.on('SIGTERM', () => this.logger.info('Received SIGTERM'));
@ -524,6 +525,7 @@ class DiscordClient extends Client
return this.localeLoader.format(language, index, params, code); return this.localeLoader.format(language, index, params, code);
} }
// TODO: Combine these
async getGuildWrapper (id: string) async getGuildWrapper (id: string)
{ {
if (this.#guildWrappers.has(id)) if (this.#guildWrappers.has(id))
@ -551,6 +553,11 @@ class DiscordClient extends Client
return wrapper; 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?: false): UserWrapper | null;
getUserWrapper(resolveable: UserResolveable, fetch?: true): Promise<UserWrapper | null>; getUserWrapper(resolveable: UserResolveable, fetch?: true): Promise<UserWrapper | null>;
getUserWrapper (resolveable: UserResolveable, fetch = true) getUserWrapper (resolveable: UserResolveable, fetch = true)
@ -582,6 +589,16 @@ class DiscordClient extends Client
}); });
} }
getUserWrappers(resolveables: UserResolveable[], fetch?: true): Promise<UserWrapper[]>;
getUserWrappers(resolveables: UserResolveable[], fetch?: false): UserWrapper[];
getUserWrappers (resolveables: UserResolveable[], fetch = true): Promise<UserWrapper[]> | 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<UserWrapper[]>;
}
async fetchInvite (invite: InviteResolvable, opts?: ClientFetchInviteOptions) async fetchInvite (invite: InviteResolvable, opts?: ClientFetchInviteOptions)
{ {
const code = DataResolver.resolveInviteCode(invite); const code = DataResolver.resolveInviteCode(invite);

View File

@ -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 <https://www.gnu.org/licenses/>.
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;

View File

@ -28,19 +28,46 @@ class DebugCommand extends Command
restricted: true, restricted: true,
moduleName: 'developer', moduleName: 'developer',
options: [ 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; const { subcommand } = invoker;
return this.enableDebug(guildId, options.enabled?.asBool); 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 = ''; let output = '';
if (this.client.shard) if (this.client.shard)

View File

@ -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 <https://www.gnu.org/licenses/>.
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<InhibitorResponse>
{
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;

View File

@ -24,7 +24,7 @@ class ChannelIgnore extends Inhibitor
{ {
super(client, { super(client, {
name: 'channelIgnore', name: 'channelIgnore',
priority: 9, priority: 8,
guild: true, guild: true,
silent: true silent: true
}); });

View File

@ -25,7 +25,7 @@ class ClientPermissions extends Inhibitor
{ {
super(client, { super(client, {
name: 'clientPermissions', name: 'clientPermissions',
priority: 10, priority: 9,
guarded: true, guarded: true,
guild: true guild: true
}); });

View File

@ -170,8 +170,7 @@ class ModerationManager implements Initialisable, CallbackClient
constructor (client: DiscordClient) constructor (client: DiscordClient)
{ {
this.#client = client; this.#client = client;
// this.#callbacks = new Collection(); this.#logger = client.createLogger(this);
this.#logger = client.createLogger(this); // new Logger({ name: 'ModMngr' });
this.#infractionClasses = Constant.Infractions; this.#infractionClasses = Constant.Infractions;
this.#ready = false; 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 // eslint-disable-next-line max-lines-per-function
async _handleTarget ( async _handleTarget (
@ -431,7 +425,6 @@ class ModerationManager implements Initialisable, CallbackClient
info: HandleTargetData info: HandleTargetData
) )
{ {
// wrapper: guildWrapper
const { reason, force, guild } = info; const { reason, force, guild } = info;
const { automod, modpoints } = await guild.settings(); const { automod, modpoints } = await guild.settings();
const { Type: type } = Infraction; const { Type: type } = Infraction;
@ -609,13 +602,12 @@ class ModerationManager implements Initialisable, CallbackClient
async handleCallback (_id: string, infraction: InfractionJSON) async handleCallback (_id: string, infraction: InfractionJSON)
{ {
// const infraction = await this.#client.mongodb.infractions.findOne({ id });
if (!infraction) if (!infraction)
return; return;
this.#logger.debug(`Infraction callback: ${infraction.id} (${infraction.type})`); this.#logger.debug(`Infraction callback: ${infraction.id} (${infraction.type})`);
const undoClass = Constant.Infractions[Constants.InfractionOpposites[infraction.type]]; const undoClass = Constant.Infractions[Constants.InfractionOpposites[infraction.type]];
if (!undoClass) if (!undoClass)
return; return this.#logger.error(`Missing undo class for ${infraction.type}`);
const guild = await this.#client.getGuildWrapper(infraction.guild!); const guild = await this.#client.getGuildWrapper(infraction.guild!);
if (!guild) if (!guild)
@ -646,7 +638,7 @@ class ModerationManager implements Initialisable, CallbackClient
throw new Error('Missing executor'); throw new Error('Missing executor');
try try
{ {
await new undoClass(this.#client, this.#logger, { const result = await new undoClass(this.#client, this.#logger, {
type: undoClass.Type, type: undoClass.Type,
reason: `AUTO-${Constants.InfractionOpposites[infraction.type]} from Case ${infraction.case}`, reason: `AUTO-${Constants.InfractionOpposites[infraction.type]} from Case ${infraction.case}`,
channel, channel,
@ -657,6 +649,8 @@ class ModerationManager implements Initialisable, CallbackClient
target, target,
executor executor
}).execute(); }).execute();
if (result.error)
this.#logger.error(result);
} }
catch (err) catch (err)
{ {

View File

@ -287,7 +287,7 @@ class CommandHandler extends Observer
{ {
if (!(invoker.command instanceof SettingsCommand)) if (!(invoker.command instanceof SettingsCommand))
invoker.command.error(now); 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' }); this._generateError(invoker, { type: 'commandHandler' });
} }
return; return;

View File

@ -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 <https://www.gnu.org/licenses/>.
// 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<DataType extends EntityData>
// {
// #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;

View File

@ -26,7 +26,7 @@ import {
} from '../../../../@types/Guild.js'; } from '../../../../@types/Guild.js';
import DiscordClient from '../../DiscordClient.js'; import DiscordClient from '../../DiscordClient.js';
const configVersion = '3.slash.2'; const configVersion = '3.slash.3';
import { import {
Guild, Guild,
@ -45,6 +45,7 @@ import {
import MemberWrapper from './MemberWrapper.js'; import MemberWrapper from './MemberWrapper.js';
import { FilterUtil, Util } from '../../../utilities/index.js'; import { FilterUtil, Util } from '../../../utilities/index.js';
import { LoggerClient } from '@navy.gif/logger'; import { LoggerClient } from '@navy.gif/logger';
import { ObjectId } from 'mongodb';
class GuildWrapper class GuildWrapper
{ {
@ -104,15 +105,16 @@ class GuildWrapper
{ {
if (this.#data) if (this.#data)
return this.#data; return this.#data;
const data = await this.#client.mongodb.guilds.findOne<GuildData>({ guildId: this.id });
const data = await this.#client.mongodb.guilds.findOne({ $or: [{ guildId: this.id }, { id: this.id }] });
if (!data) if (!data)
{ {
this.#data = {}; this.#data = { id: this.id };
return this.#data; return this.#data;
} }
if (data._version === '3.slash') if (data._version === '3.slash')
{ {
const oldSettings = data as GuildSettings; const oldSettings = data as unknown as GuildSettings;
const keys = Object.keys(this.defaultConfig); const keys = Object.keys(this.defaultConfig);
const settings: PartialGuildSettings = {}; const settings: PartialGuildSettings = {};
for (const key of keys) 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.deleteOne({ guildId: this.id });
await this.#client.mongodb.guilds.updateOne({ guildId: this.id }, { $set: data }); 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; this.#data = data;
return data; return data;
} }
@ -136,10 +145,8 @@ class GuildWrapper
return this.#settings; return this.#settings;
const data = await this.fetchData(); const data = await this.fetchData();
// eslint-disable-next-line prefer-const
const { const {
settings, settings,
// _imported
} = data; } = data;
const { defaultConfig } = this; const { defaultConfig } = this;
@ -168,7 +175,7 @@ class GuildWrapper
return this.#settings; return this.#settings;
} }
async updateData (data: GuildData) async updateData (data: Partial<GuildData>)
{ {
try try
{ {
@ -194,6 +201,22 @@ class GuildWrapper
} as GuildSettings; } 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 () async permissions ()
{ {
if (this.#permissions) if (this.#permissions)

View File

@ -16,171 +16,104 @@
import { ImageURLOptions, MessageCreateOptions, MessagePayload, User } from 'discord.js'; import { ImageURLOptions, MessageCreateOptions, MessagePayload, User } from 'discord.js';
import DiscordClient from '../../DiscordClient.js'; import DiscordClient from '../../DiscordClient.js';
import { UserSettings } from '../../../../@types/Settings.js';
import { LoggerClient } from '@navy.gif/logger'; import { LoggerClient } from '@navy.gif/logger';
import { UserData, UserSettings } from '../../../../@types/User.js';
class UserWrapper class UserWrapper
{ {
#client: DiscordClient; #client: DiscordClient;
#user: User; #user: User;
#settings: UserSettings; #data!: UserData;
// #points: {
// [key: string]: {
// expirations: Expiry[],
// points: number
// }
// };
#logger: LoggerClient; #logger: LoggerClient;
#settings: UserSettings;
constructor (client: DiscordClient, user: User) constructor (client: DiscordClient, user: User)
{ {
this.#client = client; this.#client = client;
this.#user = user; this.#user = user;
this.#logger = client.createLogger({ name: `User: ${user.id}` }); this.#logger = client.createLogger({ name: `User: ${user.id}` });
}
this.#settings = {}; async fetchData ()
// this.#points = {}; {
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) async settings (forceFetch = false)
{ {
if (this.#settings && !forceFetch) if (this.#data && !forceFetch)
return this.#settings; 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) if (settings)
this.#settings = { ...this.defaultConfig, ...settings }; {
else const keys = Object.keys(settings);
this.#settings = { userId: this.id, ...this.defaultConfig }; for (const key of keys)
{
defaultConfig[key] = { ...defaultConfig[key], [key]: settings[key] };
}
}
this.#settings = defaultConfig;
return this.#settings; return this.#settings;
} }
// async fetchPoints (guild: GuildWrapper) /**
// { * Check whether the user is banned from using the bot
// let index = this.#points[guild.id]; */
// if (index) async botBanned (): Promise<boolean>
// return Promise.resolve(index); {
// this.#points[guild.id] = { const data = await this.fetchData();
// expirations: [], return data?.banned ?? false;
// 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;
} }
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<UserSettings>)
{
if (!this.#data?.settings)
await this.settings();
await this.updateData({ settings: settings as UserSettings });
}
async updateData (data: Partial<UserData>)
{
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'))); return JSON.parse(JSON.stringify(this.#client.defaultConfig('USER')));
} }
@ -242,14 +175,114 @@ class UserWrapper
get prefix () get prefix ()
{ {
return this.#settings.prefix; return this.#data?.settings.textcommands.prefix;
} }
get locale () get locale ()
{ {
return this.#settings.locale; return this.#data?.settings.locale.language;
} }
} }
export default UserWrapper; 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;
// }

View File

@ -264,13 +264,13 @@ class Infraction
} }
} }
if (this.#duration) // Remove the role structures as they will cause problems when serialising for database
await this.#client.moderation.handleTimedInfraction(this.json);
/* LMAOOOO PLEASE DONT JUDGE ME */
if (this.#data.roles) if (this.#data.roles)
delete this.#data.roles; delete this.#data.roles;
if (this.#duration)
await this.#client.moderation.handleTimedInfraction(this.json);
return this.save(); return this.save();
} }

View File

@ -17,8 +17,8 @@
import { CallbackInfo } from '../../../../@types/CallbackManager.js'; import { CallbackInfo } from '../../../../@types/CallbackManager.js';
import { InfractionJSON } from '../../../../@types/Client.js'; import { InfractionJSON } from '../../../../@types/Client.js';
import { AttachmentData, GuildData, GuildPermissions, MessageLogEntry, RoleCacheEntry, WebhookEntry, WordWatcherEntry } from '../../../../@types/Guild.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 { MongoDBOptions } from '../../../../@types/Storage.js';
import { UserData } from '../../../../@types/User.js';
import DiscordClient from '../../DiscordClient.js'; import DiscordClient from '../../DiscordClient.js';
import { MongodbTable } from '../interfaces/index.js'; import { MongodbTable } from '../interfaces/index.js';
import Provider from '../interfaces/Provider.js'; import Provider from '../interfaces/Provider.js';
@ -142,7 +142,7 @@ class MongoDBProvider extends Provider
get users () get users ()
{ {
return this.tables.users as MongodbTable<UserSettings>; return this.tables.users as MongodbTable<UserData>;
} }
get roleCache () get roleCache ()

View File

@ -27,3 +27,6 @@ The command **{command}** can only be run by developers.
[INHIBITOR_GUILDONLY_ERROR] [INHIBITOR_GUILDONLY_ERROR]
The command **{command}** is only available in servers. The command **{command}** is only available in servers.
[INHIBITOR_BANNED_ERROR]
{noun} has been banned from using the bot.

View File

@ -0,0 +1,5 @@
[NOUN_USER]
user
[NOUN_GUILD]
server