forked from Galactic/galactic-bot
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:
commit
333862ef2d
12
@types/CallbackManager.d.ts
vendored
Normal file
12
@types/CallbackManager.d.ts
vendored
Normal 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>
|
@ -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
2
@types/Guild.d.ts
vendored
@ -149,7 +149,7 @@ export type ReminderData = {
|
||||
user: string,
|
||||
channel: string,
|
||||
reminder: string,
|
||||
time: number
|
||||
time: number // In seconds
|
||||
}
|
||||
|
||||
export type ChannelJSON = {
|
||||
|
@ -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",
|
||||
|
@ -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;
|
||||
|
@ -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' : ''}`
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -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;
|
@ -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)
|
||||
})
|
||||
});
|
||||
}
|
||||
|
@ -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,
|
||||
|
251
src/client/components/managers/CallbackManager.ts
Normal file
251
src/client/components/managers/CallbackManager.ts
Normal 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;
|
@ -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([{
|
95
src/client/components/managers/PollManager.ts
Normal file
95
src/client/components/managers/PollManager.ts
Normal 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;
|
69
src/client/components/managers/ReminderManager.ts
Normal file
69
src/client/components/managers/ReminderManager.ts
Normal 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;
|
@ -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!);
|
||||
|
@ -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 (
|
||||
|
@ -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)
|
||||
{
|
||||
|
@ -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;
|
||||
|
@ -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 ()
|
||||
{
|
||||
|
@ -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)
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
|
@ -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();
|
||||
|
@ -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();
|
||||
}
|
||||
|
6
src/client/interfaces/CallbackClient.ts
Normal file
6
src/client/interfaces/CallbackClient.ts
Normal file
@ -0,0 +1,6 @@
|
||||
interface CallbackClient
|
||||
{
|
||||
handleCallback(id: string, payload: unknown): Promise<void> | void;
|
||||
}
|
||||
|
||||
export default CallbackClient;
|
@ -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 ] };
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
};
|
@ -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 = {
|
||||
|
@ -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 ()
|
||||
|
@ -126,6 +126,11 @@ class MariaDBProvider extends Provider
|
||||
|
||||
}
|
||||
|
||||
stop ()
|
||||
{
|
||||
return this.close();
|
||||
}
|
||||
|
||||
async close ()
|
||||
{
|
||||
this.logger.status('Shutting down database connections');
|
||||
|
@ -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 ()
|
||||
|
@ -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;
|
||||
|
@ -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) =>
|
||||
{
|
||||
|
@ -146,7 +146,7 @@ class Util
|
||||
return item.split('_').map((x) => Util.capitalise(x.toLowerCase())).join('');
|
||||
}
|
||||
|
||||
static randomUUID ()
|
||||
static createUUID ()
|
||||
{
|
||||
return randomUUID();
|
||||
}
|
||||
|
53
yarn.lock
53
yarn.lock
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user