Merge pull request 'Refactored a bunch of functionality into a callback manager, further refactored into poll and reminder managers' (#5) from development into main

Reviewed-on: Galactic/galactic-bot#5
This commit is contained in:
Erik 2024-03-26 15:13:10 +01:00
commit 333862ef2d
36 changed files with 1127 additions and 518 deletions

12
@types/CallbackManager.d.ts vendored Normal file
View File

@ -0,0 +1,12 @@
export type CallbackCreateInfo<T> = {
payload: T,
expiresAt: number,
id?: string,
guild?: string
};
export type CallbackInfo<T> = {
created: number,
client: string,
_id: string
} & CallbackCreateInfo<T>

View File

@ -395,7 +395,7 @@ export type BaseInfractionData = {
expiration?: number,
data?: AdditionalInfractionData,
hyperlink?: string | null,
_callbacked?: boolean,
// _callbacked?: boolean,
fetched?: boolean
}

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

@ -149,7 +149,7 @@ export type ReminderData = {
user: string,
channel: string,
reminder: string,
time: number
time: number // In seconds
}
export type ChannelJSON = {

View File

@ -30,6 +30,7 @@
"@navy.gif/logger": "^2.5.4",
"@navy.gif/timestring": "^6.0.6",
"@types/node": "^18.15.11",
"bufferutil": "^4.0.8",
"chalk": "^5.3.0",
"common-tags": "^1.8.2",
"discord.js": "^14.14.1",
@ -42,7 +43,9 @@
"node-fetch": "^3.3.1",
"object-hash": "^3.0.0",
"similarity": "^1.2.1",
"typescript": "^5.3.2"
"typescript": "^5.3.2",
"utf-8-validate": "^6.0.3",
"zlib-sync": "^0.1.9"
},
"devDependencies": {
"@types/common-tags": "^1.8.1",

View File

@ -27,13 +27,16 @@ import {
Resolver,
ModerationManager,
RateLimiter,
CallbackManager,
PollManager,
} from './components/index.js';
import {
Observer,
Command,
Setting,
Inhibitor
Inhibitor,
ReminderManager
} from './interfaces/index.js';
import {
@ -41,13 +44,35 @@ import {
UserWrapper
} from './components/wrappers/index.js';
import { DefaultGuild, DefaultUser } from '../constants/index.js';
import { ChannelResolveable, ClientOptions, EventHook, FormatOpts, FormatParams, ManagerEvalOptions, UserResolveable } from '../../@types/Client.js';
import { Util } from '../utilities/index.js';
import { IPCMessage } from '../../@types/Shared.js';
import {
DefaultGuild,
DefaultUser
} from '../constants/index.js';
import {
ChannelResolveable,
ClientOptions,
EventHook,
FormatOpts,
FormatParams,
ManagerEvalOptions,
UserResolveable
} from '../../@types/Client.js';
import {
Util
} from '../utilities/index.js';
import {
IPCMessage
} from '../../@types/Shared.js';
import {
ClientEvents
} from '../../@types/Events.js';
import StorageManager from './storage/StorageManager.js';
import Permissions from './components/inhibitors/Permissions.js';
import { ClientEvents } from '../../@types/Events.js';
import Component from './interfaces/Component.js';
import Controller from '../middleware/Controller.js';
@ -83,11 +108,15 @@ class DiscordClient extends Client
#intercom: Intercom;
#dispatcher: Dispatcher;
#localeLoader: LocaleLoader;
#storageManager: StorageManager;
#registry: Registry;
#resolver: Resolver;
#rateLimiter: RateLimiter;
#moderationManager: ModerationManager;
#callbackManager: CallbackManager;
#reminderManager: ReminderManager;
#pollManager: PollManager;
#storageManager: StorageManager;
// #wrapperClasses: {[key: string]: };
@ -143,17 +172,21 @@ class DiscordClient extends Client
this.#userWrappers = new Collection();
this.#invites = new Collection();
this.#callbackManager = new CallbackManager(this);
this.#reminderManager = new ReminderManager(this);
this.#moderationManager = new ModerationManager(this);
this.#storageManager = new StorageManager(this, options.storage);
this.#pollManager = new PollManager(this);
this.#logger = new Logger({ name: 'Client' });
this.#miscLogger = new Logger({ name: 'Misc' });
this.#eventHooker = new EventHooker(this);
this.#intercom = new Intercom(this);
this.#dispatcher = new Dispatcher(this);
this.#localeLoader = new LocaleLoader(this);
this.#storageManager = new StorageManager(this, options.storage);
this.#registry = new Registry(this);
this.#resolver = new Resolver(this);
this.#rateLimiter = new RateLimiter(this);
this.#moderationManager = new ModerationManager(this);
// As of d.js v14 these events are emitted from the rest manager, rebinding them to the client
@ -230,6 +263,7 @@ class DiscordClient extends Client
// Needs to load in after connecting to discord
await this.#moderationManager.initialise();
await this.#callbackManager.initialise();
if (this.shardId === 0)
{
@ -348,21 +382,18 @@ class DiscordClient extends Client
{
if (!this.shard || !this.user)
throw new Error('Missing shard or user');
this.logger.info(`Setting status, with idx ${this.#activity}`);
const activities: (() => Promise<void>)[] = [
async () =>
{
const result = await this.shard?.broadcastEval((client) => client.guilds.cache.size).catch(() => null);
if (!result)
return;
const guildCount = result.reduce((p, v) => p + v, 0);
const guildCount = result?.reduce((p, v) => p + v, 0) ?? this.guilds.cache.size;
this.user?.setActivity(`${guildCount} servers`, { type: ActivityType.Watching });
},
async () =>
{
const result = await this.shard?.broadcastEval((client) => client.users.cache.size).catch(() => null);
if (!result)
return;
const userCount = result.reduce((p, v) => p + v, 0);
const userCount = result?.reduce((p, v) => p + v, 0) ?? this.users.cache.size;
this.user?.setActivity(`${userCount} users`, { type: ActivityType.Listening });
},
async () =>
@ -476,7 +507,6 @@ class DiscordClient extends Client
{
const wrapper = new GuildWrapper(this, guild);
this.#guildWrappers.set(guild.id, wrapper);
wrapper.loadCallbacks();
});
this.logger.info('Created guild wrappers');
}
@ -618,6 +648,21 @@ class DiscordClient extends Client
return this.#moderationManager;
}
get callbacks ()
{
return this.#callbackManager;
}
get reminders ()
{
return this.#reminderManager;
}
get polls ()
{
return this.#pollManager;
}
get developers ()
{
return this.#options.developers ?? [];
@ -627,6 +672,7 @@ class DiscordClient extends Client
{
return this.#options.developmentMode;
}
get localeLoader ()
{
return this.#localeLoader;

View File

@ -5,7 +5,6 @@ import { GuildBasedChannel, User } from 'discord.js';
class ModtimersCommand extends SlashCommand
{
constructor (client: DiscordClient)
{
super(client, {
@ -20,15 +19,14 @@ class ModtimersCommand extends SlashCommand
async execute (invoker: InvokerWrapper)
{
const guild = invoker.guild!;
const { moderation } = this.client;
const callbacks = moderation.callbacks.filter((cb) => cb.infraction.guild === guild.id);
if (!callbacks.size)
const callbacks = await moderation.findActiveInfractions({ guild: guild.id });
if (!callbacks.length)
return { emoji: 'failure', index: 'COMMAND_MODTIMERS_NONE' };
const fields = [];
for (const { infraction } of callbacks.values())
for (const { payload: infraction } of callbacks)
{
const target = (infraction.targetType === 'USER' ? await guild.resolveUser(infraction.target) : await guild.resolveChannel(infraction.target))!;
const moderator = (await guild.resolveUser(infraction.executor))!;
@ -52,7 +50,7 @@ class ModtimersCommand extends SlashCommand
title: guild.format('COMMAND_MODTIMERS_TITLE'),
fields,
footer: {
text: `${callbacks.size} infraction${callbacks.size > 1 ? 's' : ''}`
text: `${callbacks.length} infraction${callbacks.length > 1 ? 's' : ''}`
}
};

View File

@ -5,7 +5,7 @@ import { CommandOptionType, CommandParams } from '../../../../../@types/Client.j
import Util from '../../../../utilities/Util.js';
import { EmbedDefaultColor, PollReactions } from '../../../../constants/Constants.js';
import { GuildTextBasedChannel, TextChannel } from 'discord.js';
import { CallbackData, PollData } from '../../../../../@types/Guild.js';
import { PollData } from '../../../../../@types/Guild.js';
class PollCommand extends SlashCommand
{
@ -62,37 +62,19 @@ class PollCommand extends SlashCommand
if (subcommand!.name === 'create')
{
// await invoker.deferReply();
const questions = [];
const _channel = (channel?.asChannel || invoker.channel) as GuildTextBasedChannel;
if (!_channel?.isTextBased())
const targetChannel = (channel?.asChannel || invoker.channel) as GuildTextBasedChannel;
if (!targetChannel?.isTextBased())
throw new CommandError(invoker, { index: 'ERR_INVALID_CHANNEL_TYPE' });
const botMissing = _channel.permissionsFor(this.client.user!)?.missing([ 'SendMessages', 'EmbedLinks' ]);
const userMissing = _channel.permissionsFor(member).missing([ 'SendMessages' ]);
const botMissing = targetChannel.permissionsFor(this.client.user!)?.missing([ 'SendMessages', 'EmbedLinks' ]);
const userMissing = targetChannel.permissionsFor(member).missing([ 'SendMessages' ]);
if (!botMissing || botMissing.length)
return invoker.editReply({ index: 'COMMAND_POLL_BOT_PERMS', params: { missing: botMissing?.join(', ') ?? 'ALL', channel: _channel.id } });
return invoker.editReply({ index: 'COMMAND_POLL_BOT_PERMS', params: { missing: botMissing?.join(', ') ?? 'ALL', channel: targetChannel.id } });
if (userMissing.length)
return invoker.editReply({ index: 'COMMAND_POLL_USER_PERMS', params: { missing: userMissing.join(', '), channel: _channel.id } });
return invoker.editReply({ index: 'COMMAND_POLL_USER_PERMS', params: { missing: userMissing.join(', '), channel: targetChannel.id } });
for (let i = 0; i < choices!.asNumber; i++)
{
const response = await invoker.promptMessage({
content: guild.format(`COMMAND_POLL_QUESTION${choices?.asNumber === 1 ? '' : 'S'}`, { number: i + 1 }) + '\n' + guild.format('COMMAND_POLL_ADDENDUM'),
time: 90,
editReply: invoker.replied
});
if (!response || !response.content)
return invoker.editReply({ index: 'COMMAND_POLL_TIMEOUT' });
if (_channel!.permissionsFor(this.client.user!)?.has('ManageMessages'))
await response.delete().catch(() => null);
const { content } = response;
if (content.toLowerCase() === 'stop')
break;
if (content.toLowerCase() === 'cancel')
return invoker.editReply({ index: 'GENERAL_CANCELLED' });
const question = await guild.filterText(member, content);
questions.push({ index: i, name: `${i + 1}.`, value: question });
}
const questions = await this.#queryQuestions(invoker, choices?.asNumber ?? 1, targetChannel);
if (!questions)
return;
await invoker.editReply({ index: 'COMMAND_POLL_STARTING' });
@ -111,53 +93,79 @@ class PollCommand extends SlashCommand
if (questions.length === 1)
{
questions[0].name = guild.format('COMMAND_POLL_FIELD_QUESTION');
pollMsg = await _channel.send({ embeds: [ embed ] });
pollMsg = await targetChannel.send({ embeds: [ embed ] });
await pollMsg.react('👍');
await pollMsg.react('👎');
}
else
{
pollMsg = await _channel.send({ embeds: [ embed ] });
pollMsg = await targetChannel.send({ embeds: [ embed ] });
for (const question of questions)
await pollMsg.react(PollReactions.Multi[question.index + 1]);
}
const poll: PollData = {
questions: questions.map(q => q.value),
duration: duration?.asNumber ?? 0,
duration: (duration?.asNumber ?? 0),
multiChoice: multichoice?.asBool && questions.length > 1 || false,
user: author.id,
channel: _channel.id,
channel: targetChannel.id,
startedIn: invoker.channel!.id,
message: pollMsg.id
};
await guild.createPoll(poll);
await invoker.editReply({ emoji: 'success', index: 'COMMAND_POLL_START', params: { channel: _channel.id } });
if (duration)
await this.client.polls.create(poll, guild.id);
await invoker.editReply({ emoji: 'success', index: 'COMMAND_POLL_START', params: { channel: targetChannel.id } });
}
else if (subcommand!.name === 'delete')
{
const poll = guild.callbacks.find((cb) => cb.data.type === 'poll' && (cb.data as PollData & CallbackData).message === message!.asString)?.data as (PollData & CallbackData) | undefined;
const poll = await this.client.polls.delete(message!.asString);
if (!poll)
return { index: 'COMMAND_POLL_404', emoji: 'failure' };
await guild.removeCallback(poll.id);
const pollChannel = await guild.resolveChannel<TextChannel>(poll.channel);
const pollChannel = await guild.resolveChannel<TextChannel>(poll.payload.channel);
if (!pollChannel)
return { index: 'COMMAND_POLL_MISSING_CHANNEL', emoji: 'failure' };
const msg = await pollChannel.messages.fetch(poll.message).catch(() => null);
const msg = await pollChannel.messages.fetch(poll.payload.message).catch(() => null);
if (msg)
await msg.delete();
return { index: 'COMMAND_POLL_DELETED', emoji: 'success' };
}
else if (subcommand!.name === 'end')
{
const poll = guild.callbacks.find((cb) => cb.data.type === 'poll' && (cb.data as PollData & CallbackData).message === message!.asString);
const poll = await this.client.polls.find(message!.asString);
if (!poll)
return { index: 'COMMAND_POLL_404', emoji: 'failure' };
await guild._poll(poll.data as PollData & CallbackData);
await this.client.polls.end(poll.payload);
// await guild._poll(poll.data as PollData & CallbackData);
return { index: 'COMMAND_POLL_ENDED', emoji: 'success' };
}
}
async #queryQuestions (invoker: InvokerWrapper<true>, choices: number, targetchannel: GuildTextBasedChannel)
{
const { guild, member } = invoker;
const questions = [];
for (let i = 0; i < choices; i++)
{
const response = await invoker.promptMessage({
content: guild.format(`COMMAND_POLL_QUESTION${choices === 1 ? '' : 'S'}`, { number: i + 1 }) + '\n' + guild.format('COMMAND_POLL_ADDENDUM'),
time: 90,
editReply: invoker.replied
});
if (!response || !response.content)
return void invoker.editReply({ index: 'COMMAND_POLL_TIMEOUT' });
if (targetchannel.permissionsFor(this.client.user!)?.has('ManageMessages'))
await response.delete().catch(() => null);
const { content } = response;
if (content.toLowerCase() === 'stop')
break;
if (content.toLowerCase() === 'cancel')
return void invoker.editReply({ index: 'GENERAL_CANCELLED' });
const question = await guild.filterText(member, content);
questions.push({ index: i, name: `${i + 1}.`, value: question });
}
return questions;
}
}
export default PollCommand;

View File

@ -4,8 +4,9 @@ import InvokerWrapper from '../../wrappers/InvokerWrapper.js';
import { CommandOptionType, CommandParams } from '../../../../../@types/Client.js';
import Util from '../../../../utilities/Util.js';
import { APIEmbed } from 'discord.js';
import { CallbackData, ReminderData } from '../../../../../@types/Guild.js';
import { ReminderData } from '../../../../../@types/Guild.js';
import GuildWrapper from '../../wrappers/GuildWrapper.js';
import { CallbackInfo } from '../../../../../@types/CallbackManager.js';
class RemindCommand extends SlashCommand
{
@ -37,11 +38,11 @@ class RemindCommand extends SlashCommand
type: CommandOptionType.SUB_COMMAND,
}],
// showUsage: true
guildOnly: true
// guildOnly: true
});
}
async execute (invoker: InvokerWrapper<true>, { reminder, in: time }: CommandParams)
async execute (invoker: InvokerWrapper, { reminder, in: time }: CommandParams)
{
const { author, channel, guild, member } = invoker;
const subcommand = invoker.subcommand!.name;
@ -54,17 +55,20 @@ class RemindCommand extends SlashCommand
return;
if (!time?.asNumber || time!.asNumber < 30)
return { index: 'COMMAND_REMIND_INVALID_TIME' };
const text = await guild.filterText(member, reminder.asString);
await guild.createReminder({ time: time.asNumber, reminder: text, user: author.id, channel: channel.id });
const text = guild ? await guild.filterText(member!, reminder.asString) : reminder.asString;
await this.client.reminders.create({
time: time.asNumber,
reminder: text,
user: author.id,
channel: channel.id
}, guild?.guild);
return { index: 'COMMAND_REMIND_CONFIRM', params: { reminder: text, time: Util.humanise(time.asNumber) } };
}
else if (subcommand === 'delete')
{
const reminders = guild.callbacks
.filter((cb) => (cb.data as ReminderData & CallbackData).user === author.id)
.map((val) => val.data as ReminderData & CallbackData);
const reminders = await this.client.reminders.find(author.id);
const embed = this._remindersEmbed(reminders, guild);
const msg = await invoker.promptMessage(guild.format('COMMAND_REMIND_SELECT'), { embed });
const msg = await invoker.promptMessage((guild ? guild : this.client).format('COMMAND_REMIND_SELECT'), { embed });
if (msg)
await msg.delete();
@ -79,24 +83,22 @@ class RemindCommand extends SlashCommand
return invoker.editReply({ index: 'COMMAND_REMIND_DELETE_INDEX_OUTOFBOUNDS', embeds: [] });
const _reminder = reminders[index - 1];
await guild.removeCallback(_reminder.id);
await this.client.reminders.remove(_reminder._id);
return invoker.editReply({ index: 'COMMAND_REMIND_DELETED', emoji: 'success', embeds: [] });
}
else if (subcommand === 'list')
{
const reminders = guild.callbacks
.filter((cb) => (cb.data as ReminderData & CallbackData).user === author.id)
.map((val) => val.data as CallbackData & ReminderData);
const reminders = await this.client.reminders.find(author.id);
if (!reminders.length)
return guild.format('COMMAND_REMIND_NONE');
return (guild ? guild : this.client).format('COMMAND_REMIND_NONE');
return { embed: this._remindersEmbed(reminders, guild) };
}
}
_remindersEmbed (reminders: (ReminderData & CallbackData)[], guild: GuildWrapper)
_remindersEmbed (reminders: CallbackInfo<ReminderData>[], guild?: GuildWrapper | null)
{
const embed: APIEmbed = {
title: guild.format('COMMAND_REMINDERS_TITLE'),
title: (guild ? guild : this.client).format('COMMAND_REMINDERS_TITLE'),
fields: []
};
let index = 0;
@ -104,11 +106,11 @@ class RemindCommand extends SlashCommand
{
embed.fields!.push({
name: `${++index}`,
value: guild.format('COMMAND_REMIND_ENTRY', {
reminder: data.reminder,
channel: data.channel,
value: (guild ? guild : this.client).format('COMMAND_REMIND_ENTRY', {
reminder: data.payload.reminder,
channel: data.payload.channel,
created: Math.floor(data.created/1000),
time: Math.floor((data.created + data.time)/1000)
time: Math.floor((data.created + data.payload.time)/1000)
})
});
}

View File

@ -2,7 +2,9 @@ import Intercom from './Intercom.js';
import LocaleLoader from './LocaleLoader.js';
import EventHooker from './EventHooker.js';
import Dispatcher from './Dispatcher.js';
import ModerationManager from './ModerationManager.js';
import ModerationManager from './managers/ModerationManager.js';
import CallbackManager from './managers/CallbackManager.js';
import PollManager from './managers/PollManager.js';
import RateLimiter from './RateLimiter.js';
import Registry from './Registry.js';
import Resolver from './Resolver.js';
@ -13,6 +15,8 @@ export {
EventHooker,
Dispatcher,
ModerationManager,
CallbackManager,
PollManager,
RateLimiter,
Registry,
Resolver,

View File

@ -0,0 +1,251 @@
import {
Collection
} from 'discord.js';
import {
LoggerClient
} from '@navy.gif/logger';
import {
CallbackCreateInfo,
CallbackInfo
} from '../../../../@types/CallbackManager.js';
import {
Filter
} from 'mongodb';
import {
CallbackClient,
Initialisable
} from '../../interfaces/index.js';
import DiscordClient from '../../DiscordClient.js';
import Util from '../../../utilities/Util.js';
// type Timeout = {
// timeout: NodeJS.Timeout,
// data: CallbackInfo
// }
const HOUR = 60 * 60 * 1000;
const DAY = 24 * HOUR;
/**
* Handles callback storage and calling, a persistent setTimeout wrapper
* @date 3/23/2024 - 10:11:38 PM
*
* @class CallbackManager
* @typedef {CallbackManager}
* @implements {Initialisable}
*/
class CallbackManager implements Initialisable
{
#client: DiscordClient;
#timeouts: Collection<string, NodeJS.Timeout>;
#clients: Collection<string, CallbackClient>;
#logger: LoggerClient;
#ready: boolean;
#interval!: NodeJS.Timer;
/**
* Creates an instance of CallbackManager.
* @date 3/23/2024 - 10:11:38 PM
*
* @constructor
* @param {DiscordClient} client
*/
constructor (client: DiscordClient)
{
this.#client = client;
this.#timeouts = new Collection();
this.#clients = new Collection();
this.#logger = client.createLogger(this);
this.#ready = false;
}
private get storage ()
{
return this.#client.mongodb.callbacks;
}
get ready ()
{
return this.#ready;
}
/**
* Initialises the handler
* @date 3/23/2024 - 10:11:38 PM
*
* @async
* @returns {*}
*/
async initialise (): Promise<void>
{
if (this.#ready)
return;
await this.#loadCallbacks();
this.#interval = setInterval(this.#loadCallbacks.bind(this), HOUR);
this.#ready = true;
}
/**
* Stops clears any active timeouts from memory in preparation for process exit
* @date 3/23/2024 - 10:22:49 PM
*/
stop ()
{
clearInterval(this.#interval);
for (const [ id, timeout ] of this.#timeouts)
{
clearTimeout(timeout);
this.#timeouts.delete(id);
}
this.#ready = false;
}
async #loadCallbacks ()
{
const guildIds = [ ...this.#client.guilds.cache.keys() ];
const query: Filter<CallbackInfo<unknown>> = {
_id: { $nin: [ ...this.#timeouts.keys() ] },
expiresAt: { $lte: Date.now() + DAY }
};
if (this.#client.shardId === 0)
query.$or = [{ guild: { $in: guildIds } }, { guild: { $exists: false } }];
else
query.guild = { $in: guildIds };
const callbacks = await this.#client.mongodb.callbacks.find(query);
const now = Date.now();
for (const data of callbacks)
{
const duration = data.expiresAt - now;
if (!this.#timeouts.has(data._id))
this.#timeouts.set(data._id, setTimeout(this.#handleCallback.bind(this), duration, data));
}
}
/**
* Description placeholder
* @date 3/23/2024 - 10:11:38 PM
*
* @param {CallbackClient} client
*/
registerClient (client: CallbackClient)
{
const { name } = client.constructor;
if (this.#clients.has(name))
throw new Error(`CallbackManager already has a ${name} client`);
this.#clients.set(name, client);
}
/**
* Description placeholder
* @date 3/23/2024 - 10:11:38 PM
*
* @async
* @template T
* @param {CallbackClient} client
* @param {CallbackCreateInfo<T>} createData
* @returns {unknown}
*/
async createCallback<T> (client: CallbackClient, createData: CallbackCreateInfo<T>): Promise<CallbackInfo<T>>
{
const clientName = client.constructor.name;
if (!this.#clients.has(clientName))
throw new Error('No such client exists');
const created = Date.now();
const data: CallbackInfo<T> = {
created,
_id: createData.id ?? Util.createUUID(),
client: clientName,
...createData
};
const duration = data.expiresAt - data.created;
this.#logger.debug(`Creating callback for client ${clientName}, expiring in ${Util.humanise(duration/1000)}`);
await this.storage.insertOne(data);
if (duration <= 2 ** 31 - 1)
this.#timeouts.set(
data._id,
setTimeout(this.#handleCallback.bind(this), duration, data)
);
return data;
}
/**
* Removes an active callback
* @date 3/23/2024 - 10:11:38 PM
*
* @async
* @param {string} id
* @returns {*}
*/
async removeCallback (id: string)
{
await this.storage.deleteOne({ _id: id });
const timeout = this.#timeouts.get(id);
if (timeout)
clearTimeout(timeout);
}
/**
* Query for active callbacks. Query can be a partial of the stored payload.
* To find any active callback for a given client, pass an empty object but supply the client
* @date 3/24/2024 - 12:40:44 PM
*
* @async
* @template T
* @param {Partial<T>} query
* @param {?CallbackClient} [client]
* @returns {Promise<CallbackInfo<T>[]>}
*/
async queryCallbacks<T> (query: Partial<T>, client?: CallbackClient): Promise<CallbackInfo<T>[]>
{
const entries = Object.entries(query);
let hasAnyValue = false;
const q: Filter<CallbackInfo<T>> = { };
for (const [ key, value ] of entries)
{
// eslint-disable-next-line no-undefined
if (value !== undefined)
hasAnyValue = true;
q[`payload.${key}`] = value;
}
if (!hasAnyValue && !client)
throw new Error('Must supply query object with at least one item or a client');
if (client)
{
const { name } = client.constructor;
q.client = name;
}
const result = await this.storage.find<CallbackInfo<T>>(q);
return result;
}
async fetchCallback<T> (id: string)
{
return this.storage.findOne<CallbackInfo<T>>({ id });
}
async #handleCallback<T> (data: CallbackInfo<T>)
{
const client = this.#clients.get(data.client);
if (!client)
throw new Error(`Got invalid client ${data.client}`);
this.#logger.debug(`Firing callback for ${data.client}: ${data._id}`);
await client.handleCallback(data._id, data.payload);
await this.removeCallback(data._id);
}
}
export default CallbackManager;

View File

@ -1,14 +1,44 @@
import { inspect } from 'node:util';
import {
inspect
} from 'node:util';
import { stripIndents } from 'common-tags';
import { Collection, GuildTextBasedChannel, Message } from 'discord.js';
import { LoggerClient } from '@navy.gif/logger';
import {
stripIndents
} from 'common-tags';
import {
GuildTextBasedChannel,
Message
} from 'discord.js';
import {
LoggerClient
} from '@navy.gif/logger';
import {
DiscordStruct,
InfractionJSON,
InfractionType
} from '../../../../@types/Client.js';
import {
EscalationResult,
HandleAutomodOptions,
HandleTargetData,
InfractionHandlerOptions,
ModerationTarget,
ModerationTargets
} from '../../../../@types/Moderation.js';
import {
Constants,
Emojis
} from '../../../constants/index.js';
import {
Util
} from '../../../utilities/index.js';
import { DiscordStruct, InfractionJSON, InfractionType, ModerationCallback } from '../../../@types/Client.js';
import { EscalationResult, HandleAutomodOptions, HandleTargetData, InfractionHandlerOptions, ModerationTarget, ModerationTargets } from '../../../@types/Moderation.js';
import { Constants, Emojis } from '../../constants/index.js';
import { Util } from '../../utilities/index.js';
import DiscordClient from '../DiscordClient.js';
import {
Addrole,
Ban,
@ -21,10 +51,32 @@ import {
Unlockdown,
Unmute,
Warn
} from '../infractions/index.js';
import { CommandOption, Infraction as InfractionClass, Initialisable } from '../interfaces/index.js';
import { GuildWrapper, InvokerWrapper, MemberWrapper, UserWrapper } from './wrappers/index.js';
import { InfractionFail, InfractionSuccess } from '../../../@types/Infractions.js';
} from '../../infractions/index.js';
import {
CallbackClient,
CommandOption,
Infraction as InfractionClass,
Initialisable
} from '../../interfaces/index.js';
import {
GuildWrapper,
InvokerWrapper,
MemberWrapper,
UserWrapper
} from '../wrappers/index.js';
import {
InfractionFail,
InfractionSuccess
} from '../../../../@types/Infractions.js';
import {
CallbackCreateInfo
} from '../../../../@types/CallbackManager.js';
import DiscordClient from '../../DiscordClient.js';
const Constant: {
@ -72,10 +124,10 @@ const Constant: {
}
};
class ModerationManager implements Initialisable
class ModerationManager implements Initialisable, CallbackClient
{
#client: DiscordClient;
#callbacks: Collection<string, ModerationCallback>;
// #callbacks: Collection<string, ModerationCallback>;
#logger: LoggerClient;
#infractionClasses: {
[key: string]: typeof InfractionClass
@ -92,23 +144,20 @@ class ModerationManager implements Initialisable
UNLOCKDOWN: typeof Unlockdown;
NOTE: typeof Note
};
#ready: boolean;
get infractionClasses ()
{
return this.#infractionClasses;
}
get callbacks ()
{
return this.#callbacks;
}
constructor (client: DiscordClient)
{
this.#client = client;
this.#callbacks = new Collection();
// this.#callbacks = new Collection();
this.#logger = client.createLogger(this); // new Logger({ name: 'ModMngr' });
this.#infractionClasses = Constant.Infractions;
this.#ready = false;
}
actions: {
@ -135,9 +184,16 @@ class ModerationManager implements Initialisable
}
};
get ready ()
{
return this.#ready;
}
async initialise ()
{
// TODO: Load infractions for non-cached guilds...
if (this.#ready)
return;
this.#client.callbacks.registerClient(this);
const filter = {
duration: { $gt: 0 },
guild: { $in: this.#client.guilds.cache.map((g) => g.id) },
@ -147,7 +203,16 @@ class ModerationManager implements Initialisable
const results = await this.#client.mongodb.infractions.find<InfractionJSON>(filter);
this.#logger.info(`Filtering ${results.length} infractions for callback.`);
this.handleCallbacks(results);
for (const result of results)
this.handleTimedInfraction(result);
const ids = results.map(result => result._id);
await this.#client.mongodb.infractions.removeProperty({ _id: { $in: ids } }, [ '_callbacked' ]);
this.#ready = true;
}
async stop ()
{
//
}
// eslint-disable-next-line max-lines-per-function
@ -526,126 +591,121 @@ class ModerationManager implements Initialisable
return responses;
}
// eslint-disable-next-line max-lines-per-function
async handleCallbacks (infractions: InfractionJSON[] = [])
async handleCallback (id: string)
{
const currentDate = Date.now();
const resolve = async (i: InfractionJSON) =>
{
this.#logger.debug(`Infraction callback: ${i.id}`);
const undoClass = Constant.Infractions[Constants.InfractionOpposites[i.type]];
if (!undoClass)
return false;
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;
const guild = await this.#client.getGuildWrapper(i.guild!);
if (!guild)
throw new Error(`Missing guild for infraction: ${i.id}`);
const guild = await this.#client.getGuildWrapper(infraction.guild!);
if (!guild)
throw new Error(`Missing guild for infraction: ${infraction.id}`);
// await guild.settings(); //just incase
let target = null;
if (i.targetType === 'USER')
{
target = await guild.memberWrapper(i.target!).catch(() => null);
if (!target && i.type === 'BAN')
target = await this.#client.getUserWrapper(i.target!, true).catch(() => null);
}
else if (i.targetType === 'CHANNEL')
{
target = guild.channels.resolve(i.target!);
if (!target?.isTextBased())
throw new Error('Invalid channel');
}
if (target)
{
const executor = await guild.memberWrapper(i.executor!).catch(() => null) ?? await guild.memberWrapper(guild.me!);
const channel = guild.channels.resolve(i.channel!);
if (channel && !channel.isTextBased())
throw new Error('Bad channel ' + inspect(i));
if (!executor)
throw new Error('Missing executor');
try
{
await new undoClass(this.#client, this.#logger, {
type: undoClass.Type,
reason: `AUTO-${Constants.InfractionOpposites[i.type]} from Case ${i.case}`,
channel,
hyperlink: i.modLogMessage && i.modLogChannel
? `https://discord.com/channels/${i.guild}/${i.modLogChannel}/${i.modLogMessage}` : null,
data: i.data,
guild,
target,
executor
}).execute();
}
catch (err)
{
const error = err as Error;
this.#logger.error(`Error when resolving infraction:\n${error.stack || error}`);
}
}
else
{
// Target left guild or channel was removed from the guild. What should happen in this situation?
// Maybe continue checking if the user rejoins, but the channel will always be gone.
}
// TODO: Log this, should never error... hopefully.
// await this.client.storageManager.mongodb.infractions.updateOne(
// { id: i.id },
// { _callbacked: true }
// ).catch((e) => {
// this.logger.error(`Error during update of infraction:\n${e.stack || e}`);
// });
// this.callbacks.delete(i.id);
await this.removeCallback(i, true);
return true;
};
for (const infraction of infractions)
let target = null;
if (infraction.targetType === 'USER')
{
if (!infraction)
throw new Error('Undefined infraction');
const callBackAt = infraction.timestamp + infraction.duration;
if (callBackAt - currentDate <= 0)
{
this.#logger.debug(`Expired infraction:\n${inspect(infraction)}`);
await resolve(infraction);
continue;
}
this.#logger.debug(`Going to resolve infraction ${infraction.id} in: ${Math.floor((callBackAt - currentDate) / 1000)} s`);
this.#callbacks.set(infraction.id, {
timeout: setTimeout(async () =>
{
await resolve(infraction);
}, callBackAt - currentDate),
infraction
});
target = await guild.memberWrapper(infraction.target!).catch(() => null);
if (!target && infraction.type === 'BAN')
target = await this.#client.getUserWrapper(infraction.target!, true).catch(() => null);
}
else if (infraction.targetType === 'CHANNEL')
{
target = guild.channels.resolve(infraction.target!);
if (!target?.isTextBased())
throw new Error('Invalid channel');
}
if (target)
{
const executor = await guild.memberWrapper(infraction.executor!).catch(() => null) ?? await guild.memberWrapper(guild.me!);
const channel = guild.channels.resolve(infraction.channel!);
if (channel && !channel.isTextBased())
throw new Error('Bad channel ' + inspect(infraction));
if (!executor)
throw new Error('Missing executor');
try
{
await new undoClass(this.#client, this.#logger, {
type: undoClass.Type,
reason: `AUTO-${Constants.InfractionOpposites[infraction.type]} from Case ${infraction.case}`,
channel,
hyperlink: infraction.modLogMessage && infraction.modLogChannel
? `https://discord.com/channels/${infraction.guild}/${infraction.modLogChannel}/${infraction.modLogMessage}` : null,
data: infraction.data,
guild,
target,
executor
}).execute();
}
catch (err)
{
const error = err as Error;
this.#logger.error(`Error when resolving infraction:\n${error.stack || error}`);
}
}
else
{
// Target left guild or channel was removed from the guild. What should happen in this situation?
// Maybe continue checking if the user rejoins, but the channel will always be gone.
}
// await this.removeCallback(infraction, true);
}
async removeCallback (infraction: InfractionClass | InfractionJSON, updateCase = false)
async handleTimedInfraction (infraction: InfractionJSON): Promise<void>
{
// if(!callback) return;
this.#logger.debug(`Removing callback ${infraction.type} for ${infraction.targetType} ${infraction.target}.`);
if (updateCase)
await this.#client.storage.mongodb.infractions.updateOne(
{ id: infraction.id },
{ $set: { _callbacked: true } }
).catch((e) =>
{
this.#logger.error(`Error during update of infraction ${infraction.id}:\n${e.stack || e}\n${inspect(e.errInfo, { depth: 25 })}`);
});
const cb = this.#callbacks.get(infraction.id);
if (cb)
const currentDate = Date.now();
if (!infraction)
throw new Error('Undefined infraction');
const { duration } = infraction;
const expiresAt = infraction.timestamp + duration;
if (expiresAt - currentDate <= 0)
{
clearTimeout(cb.timeout);
this.#callbacks.delete(infraction.id);
this.#logger.debug(`Expired infraction:\n${inspect(infraction)}`);
return this.handleCallback(infraction.id);
}
this.#logger.debug(`Creating infraction callback for ${infraction.id} (${infraction.type}), expiring in ${Util.humanise(duration / 1000)}`);
const callbackData: CallbackCreateInfo<InfractionJSON> = {
expiresAt,
id: infraction.id,
payload: infraction,
guild: infraction.guild
};
await this.#client.callbacks.createCallback(this, callbackData);
}
async removeCallback (infraction: InfractionClass | InfractionJSON)
{
await this.#client.callbacks.removeCallback(infraction.id);
}
// Should be obsolete, at least in this state
// async removeCallback (infraction: InfractionClass | InfractionJSON, updateCase = false)
// {
// // if(!callback) return;
// this.#logger.debug(`Removing callback ${infraction.type} for ${infraction.targetType} ${infraction.target}.`);
// if (updateCase)
// await this.#client.storage.mongodb.infractions.updateOne(
// { id: infraction.id },
// { $set: { _callbacked: true } }
// ).catch((e) =>
// {
// this.#logger.error(`Error during update of infraction ${infraction.id}:\n${e.stack || e}\n${inspect(e.errInfo, { depth: 25 })}`);
// });
// const cb = this.#callbacks.get(infraction.id);
// if (cb)
// {
// clearTimeout(cb.timeout);
// this.#callbacks.delete(infraction.id);
// }
// }
// async _fetchTarget (guild, targetId, targetType, user = false)
// {
@ -675,14 +735,14 @@ class ModerationManager implements Initialisable
async findLatestInfraction (type: InfractionType, target: ModerationTarget)
{
const callback = this.#callbacks.filter((c) =>
{
return c.infraction.type === type
&& c.infraction.target === target.id;
}).first();
// const callback = this.#callbacks.filter((c) =>
// {
// return c.infraction.type === type
// && c.infraction.target === target.id;
// }).first();
if (callback)
return callback.infraction;
// if (callback)
// return callback.infraction;
const result = await this.#client.storage.mongodb.infractions.findOne(
{ type, target: target.id },
@ -691,6 +751,18 @@ class ModerationManager implements Initialisable
return result || null;
}
async findActiveInfraction (type: InfractionType, target: string, guild: string)
{
const [ callback ] = await this.#client.callbacks.queryCallbacks<InfractionJSON>({ type, target, guild }, this);
return callback ?? null;
}
async findActiveInfractions (query: Partial<InfractionJSON>)
{
const callback = await this.#client.callbacks.queryCallbacks(query, this);
return callback;
}
async calculatePoints (user: UserWrapper, guild: GuildWrapper)
{
const [ result ] = await this.#client.mongodb.infractions.aggregate([{

View File

@ -0,0 +1,95 @@
import { TextChannel } from 'discord.js';
import { PollData } from '../../../../@types/Guild.js';
import DiscordClient from '../../DiscordClient.js';
import CallbackClient from '../../interfaces/CallbackClient.js';
import { PollReactions } from '../../../constants/Constants.js';
class PollManager implements CallbackClient
{
#client: DiscordClient;
constructor (client: DiscordClient)
{
this.#client = client;
client.callbacks.registerClient(this);
}
private get callbacks ()
{
return this.#client.callbacks;
}
async create ({ duration, ...opts }: PollData, guild: string)
{
const now = Date.now();
await this.callbacks.createCallback(this, {
expiresAt: now + duration * 1000,
payload: { ...opts, duration },
guild
});
}
async delete (message: string)
{
const poll = await this.find(message);
if (!poll)
return null;
await this.callbacks.removeCallback(poll._id);
return poll;
}
async find (message: string)
{
const [ poll ] = await this.callbacks.queryCallbacks<PollData>({
message
}, this);
if (!poll)
return null;
return poll;
}
async handleCallback (_id: string, payload: PollData): Promise<void>
{
await this.end(payload);
}
async end ({ user, channel, startedIn, message, multiChoice }: PollData)
{
const startChannel = await this.#client.resolveChannel(startedIn);
const pollChannel = await this.#client.resolveChannel(channel);
if (pollChannel && pollChannel.isTextBased())
{
if (!(pollChannel instanceof TextChannel))
return;
const guild = await this.#client.getGuildWrapper(pollChannel.guildId);
if (!guild)
return;
const msg = await pollChannel.messages.fetch(message).catch(() => null);
if (!msg)
return;
const { reactions } = msg;
const reactionEmojis = multiChoice ? PollReactions.Multi : PollReactions.Single;
const result: { [key: string]: number } = {};
for (const emoji of reactionEmojis)
{
let reaction = reactions.resolve(emoji);
if (!reaction)
continue;
if (reaction.partial)
reaction = await reaction.fetch();
result[emoji] = reaction.count - 1;
}
const embed = msg.embeds[0].toJSON();
const results = Object.entries(result).map(([ emoji, count ]) => `${emoji} - ${count}`).join('\n');
embed.description = guild.format('COMMAND_POLL_END', { results });
await msg.edit({ embeds: [ embed ] });
if (startChannel && startChannel.isTextBased())
await startChannel.send(guild.format('COMMAND_POLL_NOTIFY_STARTER', { user, channel }));
}
}
}
export default PollManager;

View File

@ -0,0 +1,69 @@
import { ReminderData } from '../../../../@types/Guild.js';
import DiscordClient from '../../DiscordClient.js';
import CallbackClient from '../../interfaces/CallbackClient.js';
import { EmbedDefaultColor } from '../../../constants/Constants.js';
import { Guild } from 'discord.js';
class ReminderManager implements CallbackClient
{
#client: DiscordClient;
constructor (client: DiscordClient)
{
this.#client = client;
this.callbacks.registerClient(this);
}
private get callbacks ()
{
return this.#client.callbacks;
}
get format ()
{
return this.#client.format.bind(this.#client);
}
async create ({ time, user, channel, reminder }: ReminderData, guild?: Guild)
{
const now = Date.now();
await this.callbacks.createCallback(this, {
expiresAt: now + time * 1000,
guild: guild?.id,
payload: { user, channel, reminder }
});
}
async remove (id: string)
{
return this.callbacks.removeCallback(id);
}
async find (user: string)
{
return this.callbacks.queryCallbacks<ReminderData>({ user }, this);
}
async handleCallback (_id: string, { reminder, channel: channelId, user }: ReminderData): Promise<void>
{
let channel = await this.#client.resolveChannel(channelId);
if (channel && channel.partial)
channel = await channel.fetch().catch(() => null);
if (!channel || !channel.isTextBased())
return;
const payload = {
content: '',
embeds: [{
title: this.format('GENERAL_REMINDER_TITLE'),
description: reminder,
color: EmbedDefaultColor
}]
};
if (!channel.isDMBased())
payload.content = `<@${user}>`;
await channel.send(payload);
}
}
export default ReminderManager;

View File

@ -134,9 +134,10 @@ class AuditLogObserver extends Observer
if (type === 'UNMUTE')
{
const callback = this.client.moderation.callbacks.filter((cb) => cb.infraction.target === newMember.id && cb.infraction.type === 'MUTE').first();
// const callback = this.client.moderation.callbacks.filter((cb) => cb.infraction.target === newMember.id && cb.infraction.type === 'MUTE').first();
const callback = await this.client.moderation.findActiveInfraction('MUTE', newMember.id, wrapper.id);
if (callback)
this.client.moderation.removeCallback(callback.infraction, true);
this.client.moderation.removeCallback(callback.payload!);
}
const userWrapper = await this.client.getUserWrapper(entry.executor!);

View File

@ -48,6 +48,7 @@ export default class AutoModeration extends Observer implements Initialisable
regex: { invite: RegExp; linkRegG: RegExp; linkReg: RegExp; mention: RegExp; mentionG: RegExp; };
topLevelDomains!: BinaryTree<string>;
executing: { [key: string]: string[] };
#ready: boolean;
constructor (client: DiscordClient)
{
super(client, {
@ -78,10 +79,19 @@ export default class AutoModeration extends Observer implements Initialisable
mention: /<@!?(?<id>[0-9]{18,22})>/u,
mentionG: /<@!?(?<id>[0-9]{18,22})>/gu,
};
this.#ready = false;
}
get ready ()
{
return this.#ready;
}
async initialise ()
{
if (this.ready)
return;
// Fetch a list of TLDs from iana
const tldList = await this.client.managerEval(`
(() => {
@ -96,6 +106,12 @@ export default class AutoModeration extends Observer implements Initialisable
tldList.splice(0, 0, midEntry);
this.topLevelDomains = new BinaryTree(this.client, tldList);
this.topLevelDomains.add('onion');
this.#ready = true;
}
stop (): void | Promise<void>
{
throw new Error('Method not implemented.');
}
async _moderate (

View File

@ -4,7 +4,6 @@ import Util from '../../../utilities/Util.js';
import DiscordClient from '../../DiscordClient.js';
import Observer from '../../interfaces/Observer.js';
import { PollReactions } from '../../../constants/Constants.js';
import { CallbackData, PollData } from '../../../../@types/Guild.js';
import InteractionWrapper from '../wrappers/InteractionWrapper.js';
class UtilityHook extends Observer
@ -62,14 +61,17 @@ class UtilityHook extends Observer
if (!me?.permissions.has('ManageRoles') || !setting.role)
return;
const infraction = await this.client.storageManager.mongodb.infractions.findOne({
duration: { $gt: 0 },
guild: guild.id,
target: member.id,
type: 'MUTE',
_callbacked: false,
resolved: false
});
// const infraction = await this.client.storageManager.mongodb.infractions.findOne({
// duration: { $gt: 0 },
// guild: guild.id,
// target: member.id,
// type: 'MUTE',
// _callbacked: false,
// resolved: false
// });
const callback = await this.client.moderation.findActiveInfraction('MUTE', member.id, guild.id);
const infraction = callback.payload;
if (!infraction || infraction.resolved)
return;
@ -232,14 +234,14 @@ class UtilityHook extends Observer
const channel = await guild.resolveChannel<TextChannel>(message.channelId);
if (!channel)
return;
const poll = guild.callbacks.find((cb) => cb.data.type === 'poll' && (cb.data as PollData & CallbackData).message === message.id)?.data as (PollData & CallbackData) | undefined;
if (!poll || poll.multiChoice)
const poll = await this.client.polls.find(message.id);
if (!poll || poll.payload.multiChoice)
return;
if (message.partial)
message = await channel.messages.fetch(message.id);
const reactions = message.reactions.cache;
const emojis = poll.questions.length > 1 ? PollReactions.Multi : PollReactions.Single;
const emojis = poll.payload.multiChoice ? PollReactions.Multi : PollReactions.Single;
for (const emoji of emojis)
{

View File

@ -1,27 +1,17 @@
import { ChannelResolveable, FormatOpts, FormatParams, MemberResolveable, UserResolveable } from '../../../../@types/Client.js';
import {
CallbackData,
ChannelJSON,
GuildData,
GuildJSON,
GuildPermissions,
GuildSettings,
PartialGuildSettings,
PollData,
ReminderData,
RoleJSON
} from '../../../../@types/Guild.js';
import DiscordClient from '../../DiscordClient.js';
// const { default: Collection } = require("@discordjs/collection");
// const { Guild } = require("discord.js");
// const { PollReactions, EmbedDefaultColor } = require("../../../constants/Constants.js");
// const { FilterUtil, SettingsMigrator, InfractionMigrator } = require("../../../utilities/index.js");
// const MemberWrapper = require("./MemberWrapper.js");
const configVersion = '3.slash.2';
import { PollReactions, EmbedDefaultColor } from '../../../constants/Constants.js';
import {
Guild,
Collection,
@ -40,12 +30,6 @@ import MemberWrapper from './MemberWrapper.js';
import { FilterUtil, Util } from '../../../utilities/index.js';
import { LoggerClient } from '@navy.gif/logger';
type CallbackFn = (data: CallbackData) => void;
type Callback = {
timeout: NodeJS.Timeout,
data: CallbackData
}
class GuildWrapper
{
[key: string]: unknown;
@ -57,7 +41,7 @@ class GuildWrapper
#invites?: Collection<string, Invite>;
#webhooks: Collection<string, Webhook>;
#memberWrappers: Collection<string, MemberWrapper>;
#callbacks: Collection<string, Callback>;
// #callbacks: Collection<string, Callback>;
#data!: GuildData;
#settings!: GuildSettings;
@ -75,113 +59,88 @@ class GuildWrapper
this.#guild = guild;
this.#webhooks = new Collection();
this.#memberWrappers = new Collection();
this.#callbacks = new Collection();
this.#debugLog('Created wrapper');
}
async createPoll ({ user, duration, ...opts }: PollData)
{
// Idk polls that don't have a duration should still be stored somewhere so they can be ended at an arbitrary point
const type = 'poll';
const now = Date.now();
const id = `${type}:${user}:${now}`;
const data = { ...opts, user, id, guild: this.id, type, time: duration * 1000, created: now };
if (duration)
await this.createCallback(data satisfies CallbackData);
}
// async createPoll ({ user, duration, ...opts }: PollData)
// {
// // Idk polls that don't have a duration should still be stored somewhere so they can be ended at an arbitrary point
// const type = 'poll';
// const now = Date.now();
// const id = `${type}:${user}:${now}`;
// const data = { ...opts, user, id, guild: this.id, type, time: duration * 1000, created: now };
// if (duration)
// await this.createCallback(data satisfies CallbackData);
// }
async createReminder ({ time, user, channel, reminder }: ReminderData)
{
const type = 'reminder';
const now = Date.now();
const id = `${type}:${user}:${now}`;
const data = { user, channel, reminder, id, guild: this.id, type, time: time * 1000, created: now };
await this.createCallback(data);
}
// async loadCallbacks ()
// {
// const data = await this.#client.mongodb.callbacks.find<CallbackData>({ guild: this.id });
// for (const cb of data)
// await this.createCallback(cb, false);
// }
async loadCallbacks ()
{
const data = await this.#client.mongodb.callbacks.find<CallbackData>({ guild: this.id });
for (const cb of data)
await this.createCallback(cb, false);
}
// async createCallback (data: CallbackData, update = true)
// {
// const handler = this[`_${data.type}`] as CallbackFn;// .bind(this);
// if (!handler)
// throw new Error('Invalid callback type');
async createCallback (data: CallbackData, update = true)
{
const handler = this[`_${data.type}`] as CallbackFn;// .bind(this);
if (!handler)
throw new Error('Invalid callback type');
// const now = Date.now();
// const time = data.created + data.time;
// const diff = time - now;
// if (diff < 5000)
// return handler.bind(this)(data);
const now = Date.now();
const time = data.created + data.time;
const diff = time - now;
if (diff < 5000)
return handler.bind(this)(data);
// const cb = { timeout: setTimeout(handler.bind(this), diff, data), data };
// this.#callbacks.set(data.id, cb);
// if (update)
// await this.#client.mongodb.callbacks.updateOne({ id: data.id, guild: this.id }, { $set: data });
// }
const cb = { timeout: setTimeout(handler.bind(this), diff, data), data };
this.#callbacks.set(data.id, cb);
if (update)
await this.#client.mongodb.callbacks.updateOne({ id: data.id, guild: this.id }, { $set: data });
}
// async removeCallback (id: string)
// {
// const cb = this.#callbacks.get(id);
// if (cb)
// clearTimeout(cb.timeout);
// this.#callbacks.delete(id);
// await this.#client.mongodb.callbacks.deleteOne({ guild: this.id, id });
// }
async removeCallback (id: string)
{
const cb = this.#callbacks.get(id);
if (cb)
clearTimeout(cb.timeout);
this.#callbacks.delete(id);
await this.#client.mongodb.callbacks.deleteOne({ guild: this.id, id });
}
// async _poll ({ user, message, channel, id, questions, startedIn }: PollData & CallbackData)
// { // multichoice,
// const startedInChannel = await this.resolveChannel<TextChannel>(startedIn);
// const pollChannel = await this.resolveChannel<TextChannel>(channel);
// if (pollChannel)
// {
// const msg = await pollChannel.messages.fetch(message).catch(() => null);
// if (msg)
// {
// const { reactions } = msg;
// const reactionEmojis = questions.length ? PollReactions.Multi : PollReactions.Single;
// const result: {[key: string]: number} = {};
// for (const emoji of reactionEmojis)
// {
// let reaction = reactions.resolve(emoji);
// // eslint-disable-next-line max-depth
// if (!reaction)
// continue;
// // eslint-disable-next-line max-depth
// if (reaction.partial)
// reaction = await reaction.fetch();
// result[emoji] = reaction.count - 1;
// }
async _poll ({ user, message, channel, id, questions, startedIn }: PollData & CallbackData)
{ // multichoice,
const startedInChannel = await this.resolveChannel<TextChannel>(startedIn);
const pollChannel = await this.resolveChannel<TextChannel>(channel);
if (pollChannel)
{
const msg = await pollChannel.messages.fetch(message).catch(() => null);
if (msg)
{
const { reactions } = msg;
const reactionEmojis = questions.length ? PollReactions.Multi : PollReactions.Single;
const result: {[key: string]: number} = {};
for (const emoji of reactionEmojis)
{
let reaction = reactions.resolve(emoji);
// eslint-disable-next-line max-depth
if (!reaction)
continue;
// eslint-disable-next-line max-depth
if (reaction.partial)
reaction = await reaction.fetch();
result[emoji] = reaction.count - 1;
}
const embed = msg.embeds[0].toJSON();
const results = Object.entries(result).map(([ emoji, count ]) => `${emoji} - ${count}`).join('\n');
embed.description = this.format('COMMAND_POLL_END', { results });
await msg.edit({ embeds: [ embed ] });
}
}
await this.removeCallback(id);
if (startedInChannel)
await startedInChannel.send(this.format('COMMAND_POLL_NOTIFY_STARTER', { user, channel }));
}
async _reminder ({ reminder, user, channel, id }: ReminderData & CallbackData)
{
const reminderChannel = await this.resolveChannel<TextChannel>(channel);
if (reminderChannel && reminderChannel.permissionsFor(this.#client.user!)?.has([ 'ViewChannel', 'SendMessages' ]))
await reminderChannel.send({
content: `<@${user}>`,
embeds: [{
title: this.format('GENERAL_REMINDER_TITLE'),
description: reminder,
color: EmbedDefaultColor
}]
});
await this.removeCallback(id);
}
// const embed = msg.embeds[0].toJSON();
// const results = Object.entries(result).map(([ emoji, count ]) => `${emoji} - ${count}`).join('\n');
// embed.description = this.format('COMMAND_POLL_END', { results });
// await msg.edit({ embeds: [ embed ] });
// }
// }
// await this.removeCallback(id);
// if (startedInChannel)
// await startedInChannel.send(this.format('COMMAND_POLL_NOTIFY_STARTER', { user, channel }));
// }
async filterText (member: GuildMember, text: string)
{
@ -591,11 +550,6 @@ class GuildWrapper
return this.guild.iconURL(opts);
}
get callbacks ()
{
return this.#callbacks;
}
get guild ()
{
return this.#guild;

View File

@ -1,7 +1,6 @@
import { GuildMember, ImageURLOptions, MessageCreateOptions, MessagePayload } from 'discord.js';
import DiscordClient from '../../DiscordClient.js';
import UserWrapper from './UserWrapper.js';
import { InfractionJSON, InfractionType, ModerationCallback } from '../../../../@types/Client.js';
import GuildWrapper from './GuildWrapper.js';
class MemberWrapper
@ -20,32 +19,32 @@ class MemberWrapper
}
// Infraction callback
async getCallback (type: InfractionType, onlyActive = false):
Promise<{ infraction: InfractionJSON, timeout: number | null } | ModerationCallback | null>
{
if (!type)
return null;
const { callbacks } = this.#client.moderation;
const filtered = callbacks.filter((e) => e.infraction.type === type
&& e.infraction.target === this.id);
// async getCallback (type: InfractionType, onlyActive = false):
// Promise<{ infraction: InfractionJSON, timeout: number | null } | ModerationCallback | null>
// {
// if (!type)
// return null;
// const { callbacks } = this.#client.moderation;
// const filtered = callbacks.filter((e) => e.infraction.type === type
// && e.infraction.target === this.id);
if (filtered.size > 0)
return filtered.first() ?? null;
if (onlyActive)
return null; // Only return active callbacks, nothing from the db
const result = await this.#client.mongodb.infractions.findOne<InfractionJSON>(
{ duration: { $gt: 0 }, type, target: this.id, resolved: false, _callbacked: false },
{ sort: { timestamp: -1 } }// Finds latest mute.
).catch(() =>
{ // eslint-disable-line no-unused-vars
return null;
});
// if (filtered.size > 0)
// return filtered.first() ?? null;
// if (onlyActive)
// return null; // Only return active callbacks, nothing from the db
// const result = await this.#client.mongodb.infractions.findOne<InfractionJSON>(
// { duration: { $gt: 0 }, type, target: this.id, resolved: false, _callbacked: false },
// { sort: { timestamp: -1 } }// Finds latest mute.
// ).catch(() =>
// { // eslint-disable-line no-unused-vars
// return null;
// });
if (!result)
return null;
// if (!result)
// return null;
return { infraction: result, timeout: null };
}
// return { infraction: result, timeout: null };
// }
async userWrapper ()
{

View File

@ -49,11 +49,12 @@ class BanInfraction extends Infraction
if (this.arguments.days)
days = this.arguments.days.asNumber;
const callbacks = this.client.moderation.callbacks.filter((c) => c.infraction.type === 'BAN'
&& c.infraction.target === this.target!.id);
// const callbacks = this.client.moderation.callbacks.filter((c) => c.infraction.type === 'BAN'
// && c.infraction.target === this.target!.id);
const callbacks = await this.client.moderation.findActiveInfractions({ type: 'BAN', target: this.targetId! });
if (callbacks.size > 0)
callbacks.map((c) => this.client.moderation.removeCallback(c.infraction, true));
if (callbacks.length > 0)
callbacks.map((c) => this.client.moderation.removeCallback(c.payload));
try
{
@ -92,9 +93,7 @@ class BanInfraction extends Infraction
async resolve (staff: UserWrapper, reason: string, notify: boolean)
{
// const infraction = await this.client.moderationManager.findLatestInfraction(this.type, this.targetId);
const callback = this.client.moderation.callbacks.get(this.id);
if (callback)
this.client.moderation.removeCallback(callback.infraction);
await this.client.moderation.removeCallback(this);
const banned = await this.guild.bans.fetch(this.targetId!).catch(() => null);
if (banned)

View File

@ -143,15 +143,16 @@ class MuteInfraction extends Infraction
muteRole: role?.id || null
}; // Info will be saved in database and into the callback when resolved.
const callback = this.client.moderation.callbacks.filter((c) => c.infraction.type === 'MUTE'
&& c.infraction.target === this.target!.id).first();
// const callback = this.client.moderation.callbacks.filter((c) => c.infraction.type === 'MUTE'
// && c.infraction.target === this.target!.id).first();
const callback = await this.client.moderation.findActiveInfraction('MUTE', this.target!.id, this.guildId!);
if (callback)
{
if (!this.data.removedRoles)
this.data.removedRoles = [];
this.data.removedRoles = [ ...new Set([ ...this.data.removedRoles, ...callback.infraction.data.removedRoles||[] ]) ];
this.client.moderation.removeCallback(callback.infraction, true);
this.data.removedRoles = [ ...new Set([ ...this.data.removedRoles, ...callback.payload!.data.removedRoles||[] ]) ];
this.client.moderation.removeCallback(callback.payload!);
}
// if(callbacks.size > 0) callbacks.map((c) => this.client.moderationManager._removeExpiration(c));
@ -210,24 +211,21 @@ class MuteInfraction extends Infraction
const settings = await this.guild.settings();
const { removedRoles = [], muteType = settings.mute.type, muteRole = settings.mute.role } = this.data || {};
// TODO: Change this to not rely on the member
const member = await this.guild.memberWrapper(this.targetId!).catch(() => null);
if (!member)
return { error: true, message: 'Failed to unmute' };
const callback = await member.getCallback(this.type);
if (callback)
this.client.moderation.removeCallback(callback.infraction);
if (inf.id === this.id && member)
const callback = await this.client.moderation.findActiveInfraction(this.type, this.targetId!, this.guildId!);
if (callback)
this.client.moderation.removeCallback(callback.payload!);
if (inf.id === this.id && this.member)
{
const reason = `Case ${this.case} resolve`;
const roles = [ ...new Set([ ...member.roles.cache.map((r) => r.id), ...removedRoles || [] ]) ];
const roles = [ ...new Set([ ...this.member.roles.cache.map((r) => r.id), ...removedRoles || [] ]) ];
switch (muteType)
{
case 0:
try
{
await member.roles.remove(muteRole!, reason);
await this.member.roles.remove(muteRole!, reason);
}
catch (e)
{
@ -238,13 +236,13 @@ class MuteInfraction extends Infraction
}
break;
case 1:
// eslint-disable-next-line no-case-declarations
{
const index = roles.indexOf(muteRole!);
if (index >= 0)
roles.splice(index, 1);
try
{
await member.roles.set(roles, reason);
await this.member.roles.set(roles, reason);
}
catch (e)
{
@ -254,10 +252,11 @@ class MuteInfraction extends Infraction
message = this.guild.format('INFRACTION_RESOLVE_MUTE_FAIL23');
}
break;
}
case 2:
try
{
await member.roles.set(roles, reason);
await this.member.roles.set(roles, reason);
}
catch (e)
{
@ -268,8 +267,8 @@ class MuteInfraction extends Infraction
}
break;
case 3:
if (member.timedOut && member.timedOut > Date.now())
await member.timeout(null, this._reason);
if (this.member.timedOut && this.member.timedOut > Date.now())
await this.member.timeout(null, this._reason);
break;
}
}

View File

@ -52,11 +52,12 @@ class UnbanInfraction extends Infraction
return this._fail('INFRACTION_ERROR');
}
const callbacks = this.client.moderation.callbacks.filter((c) => c.infraction.type === 'BAN'
&& c.infraction.target === this.targetId);
// const callbacks = this.client.moderation.callbacks.filter((c) => c.infraction.type === 'BAN'
// && c.infraction.target === this.targetId);
const callbacks = await this.client.moderation.findActiveInfractions({ type: 'BAN', target: this.targetId! });
if (callbacks.size > 0)
callbacks.map((c) => this.client.moderation.removeCallback(c.infraction, true));
if (callbacks.length > 0)
callbacks.map((c) => this.client.moderation.removeCallback(c.payload!));
await this.handle();
return this._succeed();

View File

@ -107,13 +107,7 @@ class UnlockdownInfraction extends Infraction
return this._fail('INFRACTION_LOCKDOWN_FAILED');
if (latest)
{
const callback = this.client.moderation.callbacks.get(latest.id);
if (callback)
await this.client.moderation.removeCallback(callback.infraction, true);
else
await this.client.mongodb.infractions.updateOne({ id: latest.id }, { $set: { _callbacked: true } });
}
await this.client.moderation.removeCallback(latest);
await this.handle();
return this._succeed();

View File

@ -68,14 +68,13 @@ class UnmuteInfraction extends Infraction
}
else
{
// TODO: Make this not rely on a member wrapper
const memberWrapper = await this.guild.memberWrapper(this.member!).catch(() => null);
callback = await memberWrapper?.getCallback('MUTE');
callback = await this.client.moderation.findActiveInfraction('MUTE', this.targetId!, this.guildId!);
if (callback)
{
removedRoles = callback.infraction.data.removedRoles ?? null;
({ muteType } = callback.infraction.data);
role = callback.infraction.data.muteRole;
const infraction = callback.payload!;
removedRoles = infraction.data.removedRoles ?? null;
({ muteType } = infraction.data);
role = infraction.data.muteRole;
}
else
{
@ -121,58 +120,60 @@ class UnmuteInfraction extends Infraction
const roles = [ ...new Set([ ...this.member!.roles.cache.map((r) => r.id), ...removedRoles ]) ];
switch (muteType)
if (this.member)
{
case 0:
if (!role)
return this._fail('C_UNMUTE_ROLEDOESNTEXIST');
try
{
this.member!.roles.remove(role, this._reason);
}
catch (e)
{
return this._fail('C_UNMUTE_1FAIL');
}
break;
case 1:
if (role)
{
const index = roles.indexOf((role as Role).id);
roles.splice(index, 1);
}
try
{
this.member!.roles.set(roles, this._reason);
}
catch (err)
{
const error = err as Error;
this.logger.error(`Unmute infraction failed to calculate additional roles, might want to check this out.\n${error.stack || error}`);
return this._fail('C_UNMUTE_2FAIL');
}
break;
case 2:
try
{
this.member!.roles.set(roles, this._reason);
}
catch (err)
{
const error = err as Error;
this.logger.error(`Unmute infraction failed to calculate additional roles, might want to check this out.\n${error.stack || error}`);
return this._fail('C_UNMUTE_3FAIL');
}
break;
case 3:
// Unironically hate this property name, why is it so cumbersome
if (this.member?.timedOut && this.member?.timedOut > now)
await this.member!.timeout(null, this._reason);
break;
switch (muteType)
{
case 0:
if (!role)
return this._fail('C_UNMUTE_ROLEDOESNTEXIST');
try
{
this.member.roles.remove(role, this._reason);
}
catch (e)
{
return this._fail('C_UNMUTE_1FAIL');
}
break;
case 1:
if (role)
{
const index = roles.indexOf((role as Role).id);
roles.splice(index, 1);
}
try
{
this.member.roles.set(roles, this._reason);
}
catch (err)
{
const error = err as Error;
this.logger.error(`Unmute infraction failed to calculate additional roles, might want to check this out.\n${error.stack || error}`);
return this._fail('C_UNMUTE_2FAIL');
}
break;
case 2:
try
{
this.member.roles.set(roles, this._reason);
}
catch (err)
{
const error = err as Error;
this.logger.error(`Unmute infraction failed to calculate additional roles, might want to check this out.\n${error.stack || error}`);
return this._fail('C_UNMUTE_3FAIL');
}
break;
case 3:
if (this.member.timedOut && this.member.timedOut > now)
await this.member!.timeout(null, this._reason);
break;
}
}
if (callback)
this.client.moderation.removeCallback(callback.infraction, true);
this.client.moderation.removeCallback(callback.payload!);
await this.handle();
return this._succeed();
}

View File

@ -0,0 +1,6 @@
interface CallbackClient
{
handleCallback(id: string, payload: unknown): Promise<void> | void;
}
export default CallbackClient;

View File

@ -8,7 +8,6 @@ import Command from './commands/Command.js';
import moment from 'moment';
import { GuildBasedChannel, GuildMember, Role, User } from 'discord.js';
import Module from './Module.js';
import { Max32BitInt } from '../../constants/Constants.js';
const PointsReg = /^([-+]?[0-9]+) ?(points|point|pts|pt|p)$/iu;
const ChannelType: {[key: string]: number} = {
@ -478,8 +477,8 @@ class CommandOption
const value = this.client.resolver.resolveTime(this.#rawValue);
if (value === null)
return { error: true };
if ((value*1000) > Max32BitInt)
return { error: true, index: 'O_COMMANDHANDLER_TYPETIME_MAX', params: { maximum: Util.humanise(Max32BitInt/1000) } };
if ((value*1000) > Number.MAX_SAFE_INTEGER)
return { error: true, index: 'O_COMMANDHANDLER_TYPETIME_MAX', params: { maximum: Util.humanise(Number.MAX_SAFE_INTEGER/1000) } };
if (typeof this.#maximum !== 'undefined' && value > this.#maximum)
return { error: true, index: 'O_COMMANDHANDLER_TYPETIME_MAX', params: { maximum: Util.humanise(this.#maximum) } };
return { value, removed: [ this.#rawValue ] };

View File

@ -96,7 +96,7 @@ class Infraction
#changes: InfractionChange[];
#timestamp: number;
#callbacked: boolean;
// #callbacked: boolean;
#fetched: boolean;
#mongoId: ObjectId | null;
@ -166,7 +166,7 @@ class Infraction
this.#timestamp = Date.now();
this.#callbacked = Boolean(data._callbacked);
// this.#callbacked = Boolean(data._callbacked);
this.#fetched = false; // Boolean(data);
this.#mongoId = null;
@ -237,7 +237,7 @@ class Infraction
}
if (this.#duration)
await this.#client.moderation.handleCallbacks([ this.json ]);
await this.#client.moderation.handleTimedInfraction(this.json);
/* LMAOOOO PLEASE DONT JUDGE ME */
if (this.#data.roles)
@ -532,7 +532,7 @@ class Infraction
dmLogMessage: this.#dmLogMessageId!,
resolved: this.#resolved,
changes: this.#changes,
_callbacked: this.#callbacked || false
// _callbacked: this.#callbacked || false
};
}
@ -751,8 +751,8 @@ class Infraction
this.#errorCheck();
if (this.#resolved)
return { error: true, index: 'INFRACTION_EDIT_DURATION_RESOLVED' };
if (this.#callbacked)
return { error: true, index: 'INFRACTION_EDIT_DURATION_CALLEDBACK' };
// if (this.#callbacked)
// return { error: true, index: 'INFRACTION_EDIT_DURATION_CALLEDBACK' };
if (!TimedInfractions.includes(this.#type!))
return { error: true, index: 'INFRACTION_EDIT_DURATION_NOTTIMED' };
const now = Date.now();
@ -764,7 +764,8 @@ class Infraction
};
const member = this.#targetId ? await this.#guild.memberWrapper(this.#targetId).catch(() => null) : null;
// const callback = await member.getCallback(this.type, true);
const callback = this.#client.moderation.callbacks.get(this.id);
// const callback = this.#client.moderation.callbacks.get(this.id);
const callback = await this.client.callbacks.fetchCallback<InfractionJSON>(this.id);
this.#duration = duration;
if (this.#data.muteType === 3 && member)
@ -774,8 +775,8 @@ class Infraction
}
if (callback)
await this.#client.moderation.removeCallback(callback.infraction);
await this.#client.moderation.handleCallbacks([ this.json ]);
await this.#client.moderation.removeCallback(callback.payload);
await this.#client.moderation.handleTimedInfraction(this.json);
this.#changes.push(log);
}
@ -838,7 +839,7 @@ class Infraction
async #patch (data: WithId<InfractionJSON>)
{
this.#mongoId = new ObjectId(data._id);
this.#callbacked = data._callbacked ?? false;
// this.#callbacked = data._callbacked ?? false;
this.#fetched = true;
this.#targetType = data.targetType;

View File

@ -1,11 +1,13 @@
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const isInitialisable = (obj: any): obj is Initialisable =>
{
return typeof obj.initialise === 'function';
return typeof obj.initialise === 'function' && typeof obj.stop === 'function';
};
interface Initialisable {
initialise(): Promise<void>;
ready: boolean;
initialise(): Promise<void> | void;
stop(): Promise<void> | void;
}
export default Initialisable;

View File

@ -11,6 +11,8 @@ import CommandError from './CommandError.js';
import SettingsCommand from './commands/SettingsCommand.js';
import ModerationCommand from './commands/ModerationCommand.js';
import SlashCommand from './commands/SlashCommand.js';
import CallbackClient from './CallbackClient.js';
import ReminderManager from '../components/managers/ReminderManager.js';
export {
SlashCommand,
@ -26,5 +28,7 @@ export {
Inhibitor,
Setting,
Initialisable,
isInitialisable
isInitialisable,
CallbackClient,
ReminderManager
};

View File

@ -28,7 +28,7 @@ class MongodbTable<Default extends Document = Document> extends Table
{
if (!this.provider.initialised)
return Promise.reject(new Error('MongoDB is not connected.'));
({ query } = this._handleData(query));
({ query } = this.#handleData(query));
const cursor = this.collection<T>().find(query, options);
// if (opts?.sort)
// cursor.sort(opts.sort);
@ -42,14 +42,14 @@ class MongodbTable<Default extends Document = Document> extends Table
findOne<T extends Document = Default> (query: Filter<T>, opts?: FindOptions)
{
({ query } = this._handleData(query));
({ query } = this.#handleData(query));
return this.collection<T>()
.findOne(query, opts);
}
aggregate<T extends Document = Default> (pipeline: T[], options?: AggregateOptions)
{
({ query: pipeline } = this._handleData(pipeline));
({ query: pipeline } = this.#handleData(pipeline));
return this.collection<T>()
.aggregate(pipeline, options)
.toArray();
@ -57,7 +57,7 @@ class MongodbTable<Default extends Document = Document> extends Table
random<T extends Document = Default> (query: Filter<T>, amount = 1)
{
({ query } = this._handleData(query));
({ query } = this.#handleData(query));
if (amount > 100)
amount = 100;
return this.collection<T>()
@ -69,51 +69,53 @@ class MongodbTable<Default extends Document = Document> extends Table
insertOne<T extends Document = Default> (data: OptionalUnlessRequiredId<T>, options?: InsertOneOptions)
{
({ query: data } = this._handleData(data));
({ query: data } = this.#handleData(data));
return this.collection<T>()
.insertOne(data, options);
}
insertMany<T extends Document = Default> (data: OptionalUnlessRequiredId<T>[], options?: BulkWriteOptions)
{
({ query: data } = this._handleData(data));
({ query: data } = this.#handleData(data));
return this.collection<T>()
.insertMany(data, options);
}
deleteOne<T extends Document = Default> (query: Filter<T>, options?: DeleteOptions)
{
({ query } = this._handleData(query));
({ query } = this.#handleData(query));
return this.collection<T>()
.deleteOne(query, options);
}
deleteMany<T extends Document = Default> (query: Filter<T>, options?: DeleteOptions)
{
({ query } = this._handleData(query));
({ query } = this.#handleData(query));
return this.collection<T>()
.deleteMany(query, options);
}
updateOne<T extends Document = Default> (query: Filter<T>, data: UpdateFilter<T>, options?: UpdateOptions)
{
({ query, options } = this._handleData(query, options));
({ query, options } = this.#handleData(query, options));
return this.collection<T>()
.updateOne(query, data, options);
}
// removeProperty<T extends Document> (query: Filter<T>, data: (keyof T)[])
// {
// query = this._handleData(query);
// const obj: { [key in keyof T]: '' } = {};
// for (const key of data)
// obj[key] = '';
// return this.collection<T>().updateMany(query, { $unset: obj });
// }
removeProperty<T extends Document> (query: Filter<T>, data: (keyof T)[])
{
({ query } = this.#handleData(query));
const obj: {[key: string]: ''} = {};
for (const key of data)
obj[key as string] = '';
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
return this.collection<T>().updateMany(query, { $unset: obj });
}
push<T extends Document = Default> (query: Filter<T>, data: UpdateFilter<T>, options?: UpdateOptions)
{
({ query } = this._handleData(query));
({ query } = this.#handleData(query));
return this.collection<T>().updateOne(query, { $push: data }, options);
// return new Promise((resolve, reject) =>
// {
@ -144,15 +146,15 @@ class MongodbTable<Default extends Document = Document> extends Table
count<T extends Document = Default> (query: Filter<T>, options?: CountDocumentsOptions)
{
({ query } = this._handleData(query));
({ query } = this.#handleData(query));
return this.collection<T>().countDocuments(query, options);
}
_handleData<T extends (object | object[])>(query: T, options: UpdateOptions = {}): { query: T, options: UpdateOptions }
#handleData<T extends (object | object[])>(query: T, options: UpdateOptions = {}): { query: T, options: UpdateOptions }
{ // Convert data._id to Mongo ObjectIds
if ('_id' in query && !(query._id instanceof ObjectId))
{
if (typeof query._id === 'string')
if (typeof query._id === 'string' && query._id.length === 24)
query._id = new ObjectId(query._id);
else if (query._id instanceof Array)
query._id = {

View File

@ -33,7 +33,7 @@ abstract class Provider implements Initialisable
#tables: { [key: string]: Table };
protected _initialised: boolean;
protected _ready: boolean;
#class: typeof Table;
#logger: LoggerClient;
@ -51,10 +51,16 @@ abstract class Provider implements Initialisable
this.#tables = {};
this._initialised = false;
this._ready = false;
this.#class = Constants.Tables[opts.name];
}
get ready ()
{
return this._ready;
}
abstract stop(): void | Promise<void>;
abstract initialise(): Promise<void>
async loadTables ()
@ -124,7 +130,7 @@ abstract class Provider implements Initialisable
get initialised ()
{
return this._initialised;
return this._ready;
}
protected get config ()

View File

@ -126,6 +126,11 @@ class MariaDBProvider extends Provider
}
stop ()
{
return this.close();
}
async close ()
{
this.logger.status('Shutting down database connections');

View File

@ -1,5 +1,6 @@
import { CallbackInfo } from '../../../../@types/CallbackManager.js';
import { InfractionJSON } from '../../../../@types/Client.js';
import { AttachmentData, CallbackData, 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 DiscordClient from '../../DiscordClient.js';
@ -56,10 +57,15 @@ class MongoDBProvider extends Provider
this.logger.info('Initialising connection to DB');
await this.#client!.connect();
this.#db = this.#client!.db(this.#database);
this._initialised = true;
this._ready = true;
this.logger.info('DB connected');
}
stop ()
{
return this.close();
}
async close ()
{
if (!this.initialised)
@ -67,7 +73,7 @@ class MongoDBProvider extends Provider
this.logger.status('Closing DB connection');
await this.#client?.close();
this.#client?.removeAllListeners();
this._initialised = false;
this._ready = false;
this.#db = null;
this.logger.status('Database closed');
}
@ -95,7 +101,7 @@ class MongoDBProvider extends Provider
get callbacks ()
{
return this.tables.callbacks as MongodbTable<CallbackData>;
return this.tables.callbacks as MongodbTable<CallbackInfo<unknown>>;
}
get permissions ()

View File

@ -98,7 +98,6 @@ class Controller extends EventEmitter
// if (this.#options.api.load)
// API = await import('../../api/index.js').catch(() => this.#logger.warn(`Error importing API files, continuing without`));
// if (API) {
// // TODO: this needs to be fixed up
// this.#logger.info('Booting up API');
// const { default: APIManager } = API;
// this.#api = new APIManager(this, this.#options.api) as GalacticAPI;

View File

@ -239,7 +239,7 @@ class Shard extends EventEmitter
if (expectResponse)
{
message.id = Util.randomUUID();
message.id = Util.createUUID();
const timeout = setTimeout(reject, 10_000, [ new Error('Message timeout') ]);
this.#awaitingResponse.set(message.id, (msg: IPCMessage) =>
{

View File

@ -146,7 +146,7 @@ class Util
return item.split('_').map((x) => Util.capitalise(x.toLowerCase())).join('');
}
static randomUUID ()
static createUUID ()
{
return randomUUID();
}

View File

@ -1730,6 +1730,16 @@ __metadata:
languageName: node
linkType: hard
"bufferutil@npm:^4.0.8":
version: 4.0.8
resolution: "bufferutil@npm:4.0.8"
dependencies:
node-gyp: latest
node-gyp-build: ^4.3.0
checksum: 7e9a46f1867dca72fda350966eb468eca77f4d623407b0650913fadf73d5750d883147d6e5e21c56f9d3b0bdc35d5474e80a600b9f31ec781315b4d2469ef087
languageName: node
linkType: hard
"busboy@npm:^1.6.0":
version: 1.6.0
resolution: "busboy@npm:1.6.0"
@ -4060,6 +4070,15 @@ __metadata:
languageName: node
linkType: hard
"nan@npm:^2.18.0":
version: 2.19.0
resolution: "nan@npm:2.19.0"
dependencies:
node-gyp: latest
checksum: 29a894a003c1954c250d690768c30e69cd91017e2e5eb21b294380f7cace425559508f5ffe3e329a751307140b0bd02f83af040740fa4def1a3869be6af39600
languageName: node
linkType: hard
"natural-compare-lite@npm:^1.4.0":
version: 1.4.0
resolution: "natural-compare-lite@npm:1.4.0"
@ -4097,6 +4116,7 @@ __metadata:
"@types/similarity": ^1.2.1
"@typescript-eslint/eslint-plugin": ^5.58.0
"@typescript-eslint/parser": ^5.58.0
bufferutil: ^4.0.8
chalk: ^5.3.0
common-tags: ^1.8.2
discord.js: ^14.14.1
@ -4115,6 +4135,8 @@ __metadata:
object-hash: ^3.0.0
similarity: ^1.2.1
typescript: ^5.3.2
utf-8-validate: ^6.0.3
zlib-sync: ^0.1.9
languageName: unknown
linkType: soft
@ -4150,6 +4172,17 @@ __metadata:
languageName: node
linkType: hard
"node-gyp-build@npm:^4.3.0":
version: 4.8.0
resolution: "node-gyp-build@npm:4.8.0"
bin:
node-gyp-build: bin.js
node-gyp-build-optional: optional.js
node-gyp-build-test: build-test.js
checksum: b82a56f866034b559dd3ed1ad04f55b04ae381b22ec2affe74b488d1582473ca6e7f85fccf52da085812d3de2b0bf23109e752a57709ac7b9963951c710fea40
languageName: node
linkType: hard
"node-gyp@npm:latest":
version: 9.4.0
resolution: "node-gyp@npm:9.4.0"
@ -5347,6 +5380,16 @@ __metadata:
languageName: node
linkType: hard
"utf-8-validate@npm:^6.0.3":
version: 6.0.3
resolution: "utf-8-validate@npm:6.0.3"
dependencies:
node-gyp: latest
node-gyp-build: ^4.3.0
checksum: 5e21383c81ff7469c1912119ca69d07202d944c73ddd8a54b84dddcc546b939054e5101c78c294e494d206fe93bd43428adc635a0660816b3ec9c8ec89286ac4
languageName: node
linkType: hard
"util-deprecate@npm:^1.0.1, util-deprecate@npm:~1.0.1":
version: 1.0.2
resolution: "util-deprecate@npm:1.0.2"
@ -5538,3 +5581,13 @@ __metadata:
checksum: f77b3d8d00310def622123df93d4ee654fc6a0096182af8bd60679ddcdfb3474c56c6c7190817c84a2785648cdee9d721c0154eb45698c62176c322fb46fc700
languageName: node
linkType: hard
"zlib-sync@npm:^0.1.9":
version: 0.1.9
resolution: "zlib-sync@npm:0.1.9"
dependencies:
nan: ^2.18.0
node-gyp: latest
checksum: 36605c354b8c56bd44b0035d986ef393ad85c6774854da981a107b832c32b856b45d71529aeeca3de16aa65ed39cf9129250138c487de99cc89f14d5ee65dd2f
languageName: node
linkType: hard