From 84a40648ec12e090567e0507e56dc53d0355167c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aura=20Rom=C3=A1n?= Date: Thu, 14 Mar 2024 21:23:37 +0100 Subject: [PATCH] refactor: rewrite lockdown command --- package.json | 7 +- src/arguments/timespan.ts | 34 +- src/commands/Moderation/lockdown.ts | 341 +++++++++++++----- src/config.ts | 2 +- src/languages/en-US/commands/lockdown.json | 16 + src/lib/i18n/languageKeys/keys/Commands.ts | 1 + .../languageKeys/keys/commands/Lockdown.ts | 39 ++ src/lib/util/functions/pieces.ts | 2 +- src/lib/util/resolvers/TimeSpan.ts | 40 ++ src/lib/util/resolvers/index.ts | 1 + src/tsconfig.json | 7 +- 11 files changed, 361 insertions(+), 129 deletions(-) create mode 100644 src/languages/en-US/commands/lockdown.json create mode 100644 src/lib/i18n/languageKeys/keys/commands/Lockdown.ts create mode 100644 src/lib/util/resolvers/TimeSpan.ts create mode 100644 src/lib/util/resolvers/index.ts diff --git a/package.json b/package.json index a54075e44f4..f65976b81f9 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,10 @@ "main": "./dist/Skyra.js", "type": "module", "imports": { + "#utils/common": "./dist/lib/util/common/index.js", + "#utils/functions": "./dist/lib/util/functions/index.js", + "#utils/resolvers": "./dist/lib/util/resolvers/index.js", + "#utils/*": "./dist/lib/util/*.js", "#lib/database": "./dist/lib/database/index.js", "#lib/database/entities": "./dist/lib/database/entities/index.js", "#lib/database/keys": "./dist/lib/database/keys/index.js", @@ -22,9 +26,6 @@ "#lib/i18n/languageKeys": "./dist/lib/i18n/languageKeys/index.js", "#lib/*": "./dist/lib/*.js", "#languages": "./dist/languages/index.js", - "#utils/common": "./dist/lib/util/common/index.js", - "#utils/functions": "./dist/lib/util/functions/index.js", - "#utils/*": "./dist/lib/util/*.js", "#root/*": "./dist/*.js" }, "scripts": { diff --git a/src/arguments/timespan.ts b/src/arguments/timespan.ts index 1e74b2bcda0..0dc8d78e45c 100644 --- a/src/arguments/timespan.ts +++ b/src/arguments/timespan.ts @@ -1,37 +1,9 @@ -import { LanguageKeys } from '#lib/i18n/languageKeys'; -import { seconds } from '#utils/common'; +import { resolveTimeSpan } from '#utils/resolvers'; import { Argument } from '@sapphire/framework'; -import { Duration } from '@sapphire/time-utilities'; export class UserArgument extends Argument { public run(parameter: string, context: Argument.Context) { - const duration = this.parseParameter(parameter); - - if (!Number.isSafeInteger(duration)) { - return this.error({ parameter, identifier: LanguageKeys.Arguments.TimeSpan, context }); - } - - if (typeof context.minimum === 'number' && duration < context.minimum) { - return this.error({ parameter, identifier: LanguageKeys.Arguments.TimeSpanTooSmall, context }); - } - - if (typeof context.maximum === 'number' && duration > context.maximum) { - return this.error({ parameter, identifier: LanguageKeys.Arguments.TimeSpanTooBig, context }); - } - - return this.ok(duration); - } - - private parseParameter(parameter: string): number { - const number = Number(parameter); - if (!Number.isNaN(number)) return seconds(number); - - const duration = new Duration(parameter).offset; - if (!Number.isNaN(duration)) return duration; - - const date = Date.parse(parameter); - if (!Number.isNaN(date)) return date - Date.now(); - - return NaN; + return resolveTimeSpan(parameter, { minimum: context.minimum, maximum: context.maximum }) // + .mapErrInto((identifier) => this.error({ parameter, identifier, context })); } } diff --git a/src/commands/Moderation/lockdown.ts b/src/commands/Moderation/lockdown.ts index f78b78d9da4..26b884f6007 100644 --- a/src/commands/Moderation/lockdown.ts +++ b/src/commands/Moderation/lockdown.ts @@ -1,129 +1,290 @@ import { LanguageKeys } from '#lib/i18n/languageKeys'; -import { LockdownManager, SkyraSubcommand } from '#lib/structures'; +import { getSupportedUserLanguageT } from '#lib/i18n/translate'; +import { SkyraCommand } from '#lib/structures'; import { PermissionLevels, type GuildMessage } from '#lib/types'; -import { clearAccurateTimeout, setAccurateTimeout } from '#utils/Timers'; -import { floatPromise } from '#utils/common'; -import { assertNonThread, getSecurity } from '#utils/functions'; +import { PermissionsBits } from '#utils/bits.js'; +import { toErrorCodeResult } from '#utils/common'; +import { getCodeStyle, getLogPrefix } from '#utils/functions'; +import { resolveTimeSpan } from '#utils/resolvers'; +import { getTag } from '#utils/util.js'; import { ApplyOptions } from '@sapphire/decorators'; -import { canSendMessages, type NonThreadGuildTextBasedChannelTypes } from '@sapphire/discord.js-utilities'; -import { CommandOptionsRunTypeEnum } from '@sapphire/framework'; +import { ApplicationCommandRegistry, CommandOptionsRunTypeEnum, ok } from '@sapphire/framework'; import { send } from '@sapphire/plugin-editable-commands'; -import type { TFunction } from '@sapphire/plugin-i18next'; -import { PermissionFlagsBits, type Role } from 'discord.js'; +import { applyLocalizedBuilder, createLocalizedChoice, type TFunction } from '@sapphire/plugin-i18next'; +import { Time } from '@sapphire/time-utilities'; +import { isNullish } from '@sapphire/utilities'; +import { + ChannelType, + ChatInputCommandInteraction, + MessageFlags, + PermissionFlagsBits, + RESTJSONErrorCodes, + Role, + User, + channelMention, + chatInputApplicationCommandMention, + type CommandInteractionOption, + type ThreadChannelType +} from 'discord.js'; -@ApplyOptions({ +const Root = LanguageKeys.Commands.Lockdown; + +@ApplyOptions({ aliases: ['lock', 'unlock'], description: LanguageKeys.Commands.Moderation.LockdownDescription, detailedDescription: LanguageKeys.Commands.Moderation.LockdownExtended, permissionLevel: PermissionLevels.Moderator, requiredClientPermissions: [PermissionFlagsBits.ManageChannels, PermissionFlagsBits.ManageRoles], - runIn: [CommandOptionsRunTypeEnum.GuildAny], - subcommands: [ - { name: 'lock', messageRun: 'lock' }, - { name: 'unlock', messageRun: 'unlock' }, - { name: 'auto', messageRun: 'auto', default: true } - ] + runIn: [CommandOptionsRunTypeEnum.GuildAny] }) -export class UserCommand extends SkyraSubcommand { - public override messageRun(message: GuildMessage, args: SkyraSubcommand.Args, context: SkyraSubcommand.RunContext) { - if (context.commandName === 'lock') return this.lock(message, args); - if (context.commandName === 'unlock') return this.unlock(message, args); - return super.messageRun(message, args, context); +export class UserCommand extends SkyraCommand { + public override messageRun(message: GuildMessage, args: SkyraCommand.Args) { + const content = args.t(LanguageKeys.Commands.Shared.DeprecatedMessage, { + command: chatInputApplicationCommandMention(this.name, this.getGlobalCommandId()) + }); + return send(message, { content }); } - public async auto(message: GuildMessage, args: SkyraSubcommand.Args) { - const role = await args.pick('roleName').catch(() => message.guild.roles.everyone); - const channel = args.finished ? assertNonThread(message.channel) : await args.pick('textChannelName'); - if (this.getLock(role, channel)) return this.handleUnlock(message, args, role, channel); + public override chatInputRun(interaction: Interaction) { + const durationRaw = interaction.options.getString('duration'); + const durationResult = this.#parseDuration(durationRaw); + const t = getSupportedUserLanguageT(interaction); + if (durationResult?.isErr()) { + const content = t(durationResult.unwrapErr(), { parameter: durationRaw! }); + return interaction.reply({ content, flags: MessageFlags.Ephemeral }); + } - const duration = args.finished ? null : await args.pick('timespan', { minimum: 0 }); - return this.handleLock(message, args, role, channel, duration); + const duration = durationResult.unwrap(); + const global = interaction.options.getBoolean('global') ?? false; + const channel = + interaction.options.getChannel('channel') ?? (global ? null : (interaction.channel as SupportedChannel)); + const role = interaction.options.getRole('role') ?? interaction.guild!.roles.everyone; + const action = interaction.options.getString('action', true)! as 'lock' | 'unlock'; + return action === 'lock' // + ? this.#lock(t, interaction.user, channel, role, duration) + : this.#unlock(t, interaction.user, channel, role); } - public async unlock(message: GuildMessage, args: SkyraSubcommand.Args) { - const role = await args.pick('roleName').catch(() => message.guild.roles.everyone); - const channel = args.finished ? assertNonThread(message.channel) : await args.pick('textChannelName'); - return this.handleUnlock(message, args, role, channel); + public override registerApplicationCommands(registry: ApplicationCommandRegistry) { + registry.registerChatInputCommand((builder) => + applyLocalizedBuilder(builder, Root.Name, Root.Description) // + .addStringOption((option) => + applyLocalizedBuilder(option, Root.Action) + .setRequired(true) + .addChoices( + createLocalizedChoice(Root.ActionLock, { value: 'lock' }), + createLocalizedChoice(Root.ActionUnlock, { value: 'unlock' }) + ) + ) + .addRoleOption((option) => applyLocalizedBuilder(option, Root.Role)) + .addChannelOption((option) => applyLocalizedBuilder(option, Root.Channel)) + .addStringOption((option) => applyLocalizedBuilder(option, Root.Duration)) + .setDefaultMemberPermissions(PermissionFlagsBits.ManageChannels | PermissionFlagsBits.ManageRoles) + .setDMPermission(false) + ); } - public async lock(message: GuildMessage, args: SkyraSubcommand.Args) { - const role = await args.pick('roleName').catch(() => message.guild.roles.everyone); - const channel = args.finished ? assertNonThread(message.channel) : await args.pick('textChannelName'); - const duration = args.finished ? null : await args.pick('timespan', { minimum: 0 }); - return this.handleLock(message, args, role, channel, duration); + #lock(t: TFunction, user: User, channel: SupportedChannel | null, role: Role, duration: number | null) { + return isNullish(channel) + ? this.#lockGuild(t, user, role, duration) + : UserCommand.ThreadChannelTypes.includes(channel.type) + ? this.#lockThread(t, user, channel as SupportedThreadChannel, duration) + : this.#lockChannel(t, user, channel as SupportedNonThreadChannel, role, duration); } - private async handleLock( - message: GuildMessage, - args: SkyraSubcommand.Args, - role: Role, - channel: NonThreadGuildTextBasedChannelTypes, - duration: number | null - ) { - // If there was a lockdown, abort lock - const lock = this.getLock(role, channel); - if (lock !== null) { - this.error(LanguageKeys.Commands.Moderation.LockdownLocked, { channel: channel.toString() }); + async #lockGuild(t: TFunction, user: User, role: Role, duration: number | null) { + void t; + void user; + void role; + void duration; + + if (!role.permissions.has(PermissionFlagsBits.SendMessages | PermissionFlagsBits.SendMessagesInThreads)) { + return t(Root.GuildLocked, { role: role.toString() }); } - const allowed = this.isAllowed(role, channel); + const reason = t(Root.AuditLogRequestedBy, { user: getTag(user), role: role.toString() }); + const result = await toErrorCodeResult( + role.setPermissions( + PermissionsBits.difference(role.permissions.bitfield, PermissionFlagsBits.SendMessages | PermissionFlagsBits.SendMessagesInThreads), + reason + ) + ); + return result.match({ + ok: () => this.#lockGuildOk(t, role), + err: (code) => this.#lockGuildErr(t, role, code) + }); + } + + #lockGuildOk(t: TFunction, role: Role) { + return t(Root.SuccessGuild, { role: role.toString() }); + } + + #lockGuildErr(t: TFunction, role: Role, code: RESTJSONErrorCodes) { + if (code === RESTJSONErrorCodes.UnknownRole) return t(Root.GuildUnknownRole, { role: role.toString() }); + + this.container.logger.error(`${getLogPrefix(this)} ${getCodeStyle(code)} Could not lock the guild ${role.id}`); + return t(Root.GuildLockFailed, { role: role.toString() }); + } + + async #lockThread(t: TFunction, user: User, channel: SupportedThreadChannel, duration: number | null) { + void duration; + + if (channel.locked) { + return t(Root.ThreadLocked, { channel: channelMention(channel.id) }); + } - // If they can send, begin locking - const response = await send(message, args.t(LanguageKeys.Commands.Moderation.LockdownLocking, { channel: channel.toString() })); - await channel.permissionOverwrites.edit(role, { SendMessages: false }); - if (canSendMessages(message.channel)) { - await response.edit(args.t(LanguageKeys.Commands.Moderation.LockdownLock, { channel: channel.toString() })).catch(() => null); + if (!channel.manageable) { + return t(Root.ThreadUnmanageable, { channel: channelMention(channel.id) }); } - // Create the timeout - const timeout = duration - ? setAccurateTimeout(() => floatPromise(this.performUnlock(message, args.t, role, channel, allowed)), duration) - : null; - getSecurity(message.guild).lockdowns.add(role, channel, { allowed, timeout }); + const reason = t(Root.AuditLogRequestedBy, { user: getTag(user), channel: channelMention(channel.id) }); + const result = await toErrorCodeResult(channel.setLocked(true, reason)); + return result.match({ + ok: () => this.#lockThreadOk(t, channel), + err: (code) => this.#lockThreadErr(t, channel, code) + }); } - private isAllowed(role: Role, channel: NonThreadGuildTextBasedChannelTypes): boolean | null { - return channel.permissionOverwrites.cache.get(role.id)?.allow.has(PermissionFlagsBits.SendMessages, false) ?? null; + #lockThreadOk(t: TFunction, channel: SupportedThreadChannel) { + return t(Root.SuccessThread, { channel: channelMention(channel.id) }); } - private async handleUnlock(message: GuildMessage, args: SkyraSubcommand.Args, role: Role, channel: NonThreadGuildTextBasedChannelTypes) { - const entry = this.getLock(role, channel); - if (entry === null) this.error(LanguageKeys.Commands.Moderation.LockdownUnlocked, { channel: channel.toString() }); - if (entry.timeout) clearAccurateTimeout(entry.timeout); - return this.performUnlock(message, args.t, role, channel, entry.allowed); + #lockThreadErr(t: TFunction, channel: SupportedThreadChannel, code: RESTJSONErrorCodes) { + if (code === RESTJSONErrorCodes.UnknownChannel) return t(Root.ThreadUnknownChannel, { channel: channelMention(channel.id) }); + + this.container.logger.error(`${getLogPrefix(this)} ${getCodeStyle(code)} Could not lock the thread ${channel.id}`); + return t(Root.ThreadLockFailed, { channel: channelMention(channel.id) }); } - private async performUnlock( - message: GuildMessage, - t: TFunction, - role: Role, - channel: NonThreadGuildTextBasedChannelTypes, - allowed: boolean | null - ) { - getSecurity(channel.guild).lockdowns.remove(role, channel); - - const overwrites = channel.permissionOverwrites.cache.get(role.id); - if (overwrites === undefined) return; - - // If the only permission overwrite is the denied SEND_MESSAGES, clean up the entire permission; if the permission - // was denied, reset it to the default state, otherwise don't run an extra query - if (overwrites.allow.bitfield === 0n && overwrites.deny.bitfield === PermissionFlagsBits.SendMessages) { - await overwrites.delete(); - } else if (overwrites.deny.has(PermissionFlagsBits.SendMessages)) { - await overwrites.edit({ SendMessages: allowed }); + async #lockChannel(t: TFunction, user: User, channel: SupportedNonThreadChannel, role: Role, duration: number | null) { + void duration; + + if (!channel.permissionsFor(role).has(PermissionFlagsBits.SendMessages | PermissionFlagsBits.SendMessagesInThreads)) { + return t(Root.ChannelLocked, { channel: channelMention(channel.id) }); } - if (canSendMessages(message.channel)) { - const content = t(LanguageKeys.Commands.Moderation.LockdownOpen, { channel: channel.toString() }); - await send(message, content); + if (!channel.manageable) { + return t(Root.ChannelUnmanageable, { channel: channelMention(channel.id) }); } + + const reason = t(Root.AuditLogRequestedBy, { user: getTag(user), channel: channelMention(channel.id) }); + const result = await toErrorCodeResult( + channel.permissionOverwrites.edit(role, { SendMessages: false, SendMessagesInThreads: false }, { reason }) + ); + return result.match({ + ok: () => this.#lockChannelOk(t, channel), + err: (code) => this.#lockChannelErr(t, channel, code) + }); + } + + #lockChannelOk(t: TFunction, channel: SupportedNonThreadChannel) { + return t(Root.SuccessChannel, { channel: channelMention(channel.id) }); + } + + #lockChannelErr(t: TFunction, channel: SupportedNonThreadChannel, code: RESTJSONErrorCodes) { + if (code === RESTJSONErrorCodes.UnknownChannel) return t(Root.ChannelUnknownChannel, { channel: channelMention(channel.id) }); + + this.container.logger.error(`${getLogPrefix(this)} ${getCodeStyle(code)} Could not lock the channel ${channel.id}`); + return t(Root.ChannelLockFailed, { channel: channelMention(channel.id) }); } - private getLock(role: Role, channel: NonThreadGuildTextBasedChannelTypes): LockdownManager.Entry | null { - const entry = getSecurity(channel.guild).lockdowns.get(channel.id)?.get(role.id); - if (entry) return entry; + #unlock(t: TFunction, user: User, channel: SupportedChannel | null, role: Role) { + void t; + void user; + void role; + void channel; + } + + // private async handleLock( + // message: GuildMessage, + // args: SkyraCommand.Args, + // role: Role, + // channel: NonThreadGuildTextBasedChannelTypes, + // duration: number | null + // ) { + // // If there was a lockdown, abort lock + // const lock = this.getLock(role, channel); + // if (lock !== null) { + // this.error(LanguageKeys.Commands.Moderation.LockdownLocked, { channel: channel.toString() }); + // } + + // const allowed = this.isAllowed(role, channel); + + // // If they can send, begin locking + // const response = await send(message, args.t(LanguageKeys.Commands.Moderation.LockdownLocking, { channel: channel.toString() })); + // await channel.permissionOverwrites.edit(role, { SendMessages: false }); + // if (canSendMessages(message.channel)) { + // await response.edit(args.t(LanguageKeys.Commands.Moderation.LockdownLock, { channel: channel.toString() })).catch(() => null); + // } + + // // Create the timeout + // const timeout = duration + // ? setAccurateTimeout(() => floatPromise(this.performUnlock(message, args.t, role, channel, allowed)), duration) + // : null; + // getSecurity(message.guild).lockdowns.add(role, channel, { allowed, timeout }); + // } + + // private isAllowed(role: Role, channel: NonThreadGuildTextBasedChannelTypes): boolean | null { + // return channel.permissionOverwrites.cache.get(role.id)?.allow.has(PermissionFlagsBits.SendMessages, false) ?? null; + // } + + // private async handleUnlock(message: GuildMessage, args: SkyraCommand.Args, role: Role, channel: NonThreadGuildTextBasedChannelTypes) { + // const entry = this.getLock(role, channel); + // if (entry === null) this.error(LanguageKeys.Commands.Moderation.LockdownUnlocked, { channel: channel.toString() }); + // if (entry.timeout) clearAccurateTimeout(entry.timeout); + // return this.performUnlock(message, args.t, role, channel, entry.allowed); + // } - const permissions = channel.permissionOverwrites.cache.get(role.id)?.deny.has(PermissionFlagsBits.SendMessages); - return permissions === true ? { allowed: null, timeout: null } : null; + // private async performUnlock( + // message: GuildMessage, + // t: TFunction, + // role: Role, + // channel: NonThreadGuildTextBasedChannelTypes, + // allowed: boolean | null + // ) { + // getSecurity(channel.guild).lockdowns.remove(role, channel); + + // const overwrites = channel.permissionOverwrites.cache.get(role.id); + // if (overwrites === undefined) return; + + // // If the only permission overwrite is the denied SEND_MESSAGES, clean up the entire permission; if the permission + // // was denied, reset it to the default state, otherwise don't run an extra query + // if (overwrites.allow.bitfield === 0n && overwrites.deny.bitfield === PermissionFlagsBits.SendMessages) { + // await overwrites.delete(); + // } else if (overwrites.deny.has(PermissionFlagsBits.SendMessages)) { + // await overwrites.edit({ SendMessages: allowed }); + // } + + // if (canSendMessages(message.channel)) { + // const content = t(LanguageKeys.Commands.Moderation.LockdownOpen, { channel: channel.toString() }); + // await send(message, content); + // } + // } + + // private getLock(role: Role, channel: NonThreadGuildTextBasedChannelTypes): LockdownManager.Entry | null { + // const entry = getSecurity(channel.guild).lockdowns.get(channel.id)?.get(role.id); + // if (entry) return entry; + + // const permissions = channel.permissionOverwrites.cache.get(role.id)?.deny.has(PermissionFlagsBits.SendMessages); + // return permissions === true ? { allowed: null, timeout: null } : null; + // } + + #parseDuration(value: string | null) { + if (isNullish(value)) return ok(null); + return resolveTimeSpan(value, { minimum: Time.Second * 30, maximum: Time.Year }); } + + private static readonly ThreadChannelTypes: ChannelType[] = [ + ChannelType.AnnouncementThread, + ChannelType.PublicThread, + ChannelType.PrivateThread + ] satisfies readonly SupportedThreadChannelType[]; } + +type Interaction = ChatInputCommandInteraction<'cached'>; + +type SupportedChannelType = Exclude; +type SupportedThreadChannelType = Extract; +type SupportedChannel = Extract['channel']>, { type: SupportedChannelType }>; +type SupportedThreadChannel = Extract; +type SupportedNonThreadChannel = Exclude; diff --git a/src/config.ts b/src/config.ts index 07a74f7a4a0..87e050c5567 100644 --- a/src/config.ts +++ b/src/config.ts @@ -194,7 +194,7 @@ function parseInternationalizationOptions(): InternationalizationOptions { load: 'all', lng: 'en-US', fallbackLng: { - 'es-419': ['es-ES'], // Latin America Spanish falls back to Spain Spanish + 'es-419': ['es-ES', 'en-US'], // Latin America Spanish falls back to Spain Spanish default: ['en-US'] }, defaultNS: 'globals', diff --git a/src/languages/en-US/commands/lockdown.json b/src/languages/en-US/commands/lockdown.json new file mode 100644 index 00000000000..3b83257f126 --- /dev/null +++ b/src/languages/en-US/commands/lockdown.json @@ -0,0 +1,16 @@ +{ + "name": "lockdown", + "description": "Manage the server's lockdown status", + "actionName": "action", + "actionDescription": "The action to perform", + "channelName": "channel", + "channelDescription": "The channel to lock down", + "durationName": "duration", + "durationDescription": "How long the lockdown should last", + "roleName": "role", + "roleDescription": "The role to use for the lockdown", + "globalName": "global", + "globalDescription": "⚠️ Whether or not to apply the lockdown to the entire server", + "actionLock": "Lock", + "actionUnlock": "Unlock" +} diff --git a/src/lib/i18n/languageKeys/keys/Commands.ts b/src/lib/i18n/languageKeys/keys/Commands.ts index 5ddfbcc00b2..e410f63d453 100644 --- a/src/lib/i18n/languageKeys/keys/Commands.ts +++ b/src/lib/i18n/languageKeys/keys/Commands.ts @@ -2,6 +2,7 @@ export * as Admin from '#lib/i18n/languageKeys/keys/commands/Admin'; export * as Fun from '#lib/i18n/languageKeys/keys/commands/Fun'; export * as Games from '#lib/i18n/languageKeys/keys/commands/Games'; export * as General from '#lib/i18n/languageKeys/keys/commands/General'; +export * as Lockdown from '#lib/i18n/languageKeys/keys/commands/Lockdown'; export * as Management from '#lib/i18n/languageKeys/keys/commands/Management'; export * as Misc from '#lib/i18n/languageKeys/keys/commands/Misc'; export * as Moderation from '#lib/i18n/languageKeys/keys/commands/Moderation'; diff --git a/src/lib/i18n/languageKeys/keys/commands/Lockdown.ts b/src/lib/i18n/languageKeys/keys/commands/Lockdown.ts new file mode 100644 index 00000000000..71464ca4eb7 --- /dev/null +++ b/src/lib/i18n/languageKeys/keys/commands/Lockdown.ts @@ -0,0 +1,39 @@ +import { FT, T } from '#lib/types'; +import type { ChannelMention, RoleMention } from 'discord.js'; + +// Root +export const Name = T('commands/lockdown:name'); +export const Description = T('commands/lockdown:description'); + +// Options +export const Action = 'commands/lockdown:action'; +export const Channel = 'commands/lockdown:channel'; +export const Duration = 'commands/lockdown:duration'; +export const Role = 'commands/lockdown:role'; +export const Global = 'commands/lockdown:global'; + +// Action choices +export const ActionLock = T('commands/lockdown:actionLock'); +export const ActionUnlock = T('commands/lockdown:actionUnlock'); + +export const AuditLogRequestedBy = FT<{ user: string; channel: ChannelMention }>('commands/lockdown:auditLogRequestedBy'); + +// Guild +export const GuildLocked = FT<{ role: RoleMention }>('commands/lockdown:guildLocked'); +export const SuccessGuild = FT<{ role: RoleMention }>('commands/lockdown:successGuild'); +export const GuildUnknownRole = FT<{ role: RoleMention }>('commands/lockdown:guildUnknownRole'); +export const GuildLockFailed = FT<{ role: RoleMention }>('commands/lockdown:guildLockFailed'); + +// Thread +export const SuccessThread = FT<{ channel: ChannelMention }>('commands/lockdown:successThread'); +export const ThreadLocked = FT<{ channel: ChannelMention }>('commands/lockdown:threadLocked'); +export const ThreadUnmanageable = FT<{ channel: ChannelMention }>('commands/lockdown:threadUnmanageable'); +export const ThreadUnknownChannel = FT<{ channel: ChannelMention }>('commands/lockdown:threadUnknownChannel'); +export const ThreadLockFailed = FT<{ channel: ChannelMention }>('commands/lockdown:threadLockFailed'); + +// Channel +export const SuccessChannel = FT<{ channel: ChannelMention }>('commands/lockdown:successChannel'); +export const ChannelLocked = FT<{ channel: ChannelMention }>('commands/lockdown:channelLocked'); +export const ChannelUnmanageable = FT<{ channel: ChannelMention }>('commands/lockdown:channelUnmanageable'); +export const ChannelUnknownChannel = FT<{ channel: ChannelMention }>('commands/lockdown:channelUnknownChannel'); +export const ChannelLockFailed = FT<{ channel: ChannelMention }>('commands/lockdown:channelLockFailed'); diff --git a/src/lib/util/functions/pieces.ts b/src/lib/util/functions/pieces.ts index c6c1644020e..337a78214e9 100644 --- a/src/lib/util/functions/pieces.ts +++ b/src/lib/util/functions/pieces.ts @@ -2,7 +2,7 @@ import { Piece } from '@sapphire/framework'; import { bgBlue, bgRed } from 'colorette'; export function getLogPrefix(piece: Piece | string) { - return bgBlue(piece instanceof Piece ? `[ ${piece.store.name} => ${piece.name} ]` : `[ ${piece} ]`); + return bgBlue(piece instanceof Piece ? `[ ${piece.name} ]` : `[ ${piece} ]`); } export function getCodeStyle(code: string | number) { diff --git a/src/lib/util/resolvers/TimeSpan.ts b/src/lib/util/resolvers/TimeSpan.ts new file mode 100644 index 00000000000..bf407c7fb42 --- /dev/null +++ b/src/lib/util/resolvers/TimeSpan.ts @@ -0,0 +1,40 @@ +import { LanguageKeys } from '#lib/i18n/languageKeys'; +import { seconds } from '#utils/common'; +import { err, ok } from '@sapphire/framework'; +import { Duration } from '@sapphire/time-utilities'; + +export function resolveTimeSpan(parameter: string, options?: TimeSpanOptions) { + const duration = parse(parameter); + + if (!Number.isSafeInteger(duration)) { + return err(LanguageKeys.Arguments.TimeSpan); + } + + if (typeof options?.minimum === 'number' && duration < options.minimum) { + return err(LanguageKeys.Arguments.TimeSpanTooSmall); + } + + if (typeof options?.maximum === 'number' && duration > options.maximum) { + return err(LanguageKeys.Arguments.TimeSpanTooBig); + } + + return ok(duration); +} + +function parse(parameter: string) { + const number = Number(parameter); + if (!Number.isNaN(number)) return seconds(number); + + const duration = new Duration(parameter).offset; + if (!Number.isNaN(duration)) return duration; + + const date = Date.parse(parameter); + if (!Number.isNaN(date)) return date - Date.now(); + + return NaN; +} + +export interface TimeSpanOptions { + minimum?: number; + maximum?: number; +} diff --git a/src/lib/util/resolvers/index.ts b/src/lib/util/resolvers/index.ts new file mode 100644 index 00000000000..9124895d4f4 --- /dev/null +++ b/src/lib/util/resolvers/index.ts @@ -0,0 +1 @@ +export * from '#utils/resolvers/TimeSpan'; diff --git a/src/tsconfig.json b/src/tsconfig.json index d18a15c5f13..f12ea3f1e81 100644 --- a/src/tsconfig.json +++ b/src/tsconfig.json @@ -6,6 +6,10 @@ "rootDir": ".", "baseUrl": ".", "paths": { + "#utils/common": ["lib/util/common/index.js"], + "#utils/functions": ["lib/util/functions/index.js"], + "#utils/resolvers": ["lib/util/functions/resolvers.js"], + "#utils/*": ["lib/util/*"], "#lib/database": ["lib/database/index.js"], "#lib/database/entities": ["lib/database/entities/index.js"], "#lib/database/keys": ["lib/database/keys/index.js"], @@ -21,9 +25,6 @@ "#lib/i18n/languageKeys": ["lib/i18n/languageKeys/index.js"], "#lib/*": ["lib/*"], "#languages": ["languages/index.js"], - "#utils/common": ["lib/util/common/index.js"], - "#utils/functions": ["lib/util/functions/index.js"], - "#utils/*": ["lib/util/*"], "#root/*": ["*"] } },