diff --git a/packages/discord.js/src/client/BaseClient.js b/packages/discord.js/src/client/BaseClient.js index 86ec6bc57..94044806b 100644 --- a/packages/discord.js/src/client/BaseClient.js +++ b/packages/discord.js/src/client/BaseClient.js @@ -4,7 +4,7 @@ const EventEmitter = require('node:events'); const { REST } = require('@discordjs/rest'); const { TypeError } = require('../errors'); const Options = require('../util/Options'); -const Util = require('../util/Util'); +const { mergeDefault, flatten } = require('../util/Util'); /** * The base class for all clients. @@ -22,7 +22,7 @@ class BaseClient extends EventEmitter { * The options the client was instantiated with * @type {ClientOptions} */ - this.options = Util.mergeDefault(Options.createDefault(), options); + this.options = mergeDefault(Options.createDefault(), options); /** * The REST manager of the client @@ -63,7 +63,7 @@ class BaseClient extends EventEmitter { } toJSON(...props) { - return Util.flatten(this, { domain: false }, ...props); + return flatten(this, { domain: false }, ...props); } } diff --git a/packages/discord.js/src/client/actions/ChannelUpdate.js b/packages/discord.js/src/client/actions/ChannelUpdate.js index 88ee7f1bf..6b432c08b 100644 --- a/packages/discord.js/src/client/actions/ChannelUpdate.js +++ b/packages/discord.js/src/client/actions/ChannelUpdate.js @@ -1,7 +1,7 @@ 'use strict'; const Action = require('./Action'); -const { Channel } = require('../../structures/Channel'); +const { createChannel } = require('../../util/Channels'); class ChannelUpdateAction extends Action { handle(data) { @@ -12,7 +12,7 @@ class ChannelUpdateAction extends Action { const old = channel._update(data); if (channel.type !== data.type) { - const newChannel = Channel.create(this.client, data, channel.guild); + const newChannel = createChannel(this.client, data, channel.guild); for (const [id, message] of channel.messages.cache) newChannel.messages.cache.set(id, message); channel = newChannel; this.client.channels.cache.set(channel.id, channel); diff --git a/packages/discord.js/src/index.js b/packages/discord.js/src/index.js index bec6e3352..f1d490b74 100644 --- a/packages/discord.js/src/index.js +++ b/packages/discord.js/src/index.js @@ -34,7 +34,7 @@ exports.Sweepers = require('./util/Sweepers'); exports.SystemChannelFlagsBitField = require('./util/SystemChannelFlagsBitField'); exports.ThreadMemberFlagsBitField = require('./util/ThreadMemberFlagsBitField'); exports.UserFlagsBitField = require('./util/UserFlagsBitField'); -exports.Util = require('./util/Util'); +__exportStar(require('./util/Util.js'), exports); exports.version = require('../package.json').version; // Managers diff --git a/packages/discord.js/src/managers/ChannelManager.js b/packages/discord.js/src/managers/ChannelManager.js index 9056efca0..5658b6a43 100644 --- a/packages/discord.js/src/managers/ChannelManager.js +++ b/packages/discord.js/src/managers/ChannelManager.js @@ -4,6 +4,7 @@ const process = require('node:process'); const { Routes } = require('discord-api-types/v10'); const CachedManager = require('./CachedManager'); const { Channel } = require('../structures/Channel'); +const { createChannel } = require('../util/Channels'); const { ThreadChannelTypes } = require('../util/Constants'); const Events = require('../util/Events'); @@ -46,7 +47,7 @@ class ChannelManager extends CachedManager { return existing; } - const channel = Channel.create(this.client, data, guild, { allowUnknownGuild, fromInteraction }); + const channel = createChannel(this.client, data, guild, { allowUnknownGuild, fromInteraction }); if (!channel) { this.client.emit(Events.Debug, `Failed to find guild, or unknown type for channel ${data.id} ${data.type}`); diff --git a/packages/discord.js/src/managers/GuildChannelManager.js b/packages/discord.js/src/managers/GuildChannelManager.js index 37919189a..e1ba72da8 100644 --- a/packages/discord.js/src/managers/GuildChannelManager.js +++ b/packages/discord.js/src/managers/GuildChannelManager.js @@ -12,7 +12,7 @@ const ThreadChannel = require('../structures/ThreadChannel'); const Webhook = require('../structures/Webhook'); const { ThreadChannelTypes } = require('../util/Constants'); const DataResolver = require('../util/DataResolver'); -const Util = require('../util/Util'); +const { setPosition } = require('../util/Util'); let cacheWarningEmitted = false; @@ -296,7 +296,7 @@ class GuildChannelManager extends CachedManager { async setPosition(channel, position, { relative, reason } = {}) { channel = this.resolve(channel); if (!channel) throw new TypeError('INVALID_TYPE', 'channel', 'GuildChannelResolvable'); - const updatedChannels = await Util.setPosition( + const updatedChannels = await setPosition( channel, position, relative, diff --git a/packages/discord.js/src/managers/MessageManager.js b/packages/discord.js/src/managers/MessageManager.js index b279b20a5..36d1115a5 100644 --- a/packages/discord.js/src/managers/MessageManager.js +++ b/packages/discord.js/src/managers/MessageManager.js @@ -7,7 +7,7 @@ const CachedManager = require('./CachedManager'); const { TypeError } = require('../errors'); const { Message } = require('../structures/Message'); const MessagePayload = require('../structures/MessagePayload'); -const Util = require('../util/Util'); +const { resolvePartialEmoji } = require('../util/Util'); /** * Manages API methods for Messages and holds their cache. @@ -223,7 +223,7 @@ class MessageManager extends CachedManager { message = this.resolveId(message); if (!message) throw new TypeError('INVALID_TYPE', 'message', 'MessageResolvable'); - emoji = Util.resolvePartialEmoji(emoji); + emoji = resolvePartialEmoji(emoji); if (!emoji) throw new TypeError('EMOJI_TYPE', 'emoji', 'EmojiIdentifierResolvable'); const emojiId = emoji.id diff --git a/packages/discord.js/src/managers/RoleManager.js b/packages/discord.js/src/managers/RoleManager.js index 0bfc7465b..5ffa7939a 100644 --- a/packages/discord.js/src/managers/RoleManager.js +++ b/packages/discord.js/src/managers/RoleManager.js @@ -8,8 +8,7 @@ const { TypeError } = require('../errors'); const { Role } = require('../structures/Role'); const DataResolver = require('../util/DataResolver'); const PermissionsBitField = require('../util/PermissionsBitField'); -const { resolveColor } = require('../util/Util'); -const Util = require('../util/Util'); +const { setPosition, resolveColor } = require('../util/Util'); let cacheWarningEmitted = false; @@ -246,7 +245,7 @@ class RoleManager extends CachedManager { async setPosition(role, position, { relative, reason } = {}) { role = this.resolve(role); if (!role) throw new TypeError('INVALID_TYPE', 'role', 'RoleResolvable'); - const updatedRoles = await Util.setPosition( + const updatedRoles = await setPosition( role, position, relative, diff --git a/packages/discord.js/src/sharding/Shard.js b/packages/discord.js/src/sharding/Shard.js index ceaab75ed..902e240ac 100644 --- a/packages/discord.js/src/sharding/Shard.js +++ b/packages/discord.js/src/sharding/Shard.js @@ -6,7 +6,7 @@ const process = require('node:process'); const { setTimeout, clearTimeout } = require('node:timers'); const { setTimeout: sleep } = require('node:timers/promises'); const { Error } = require('../errors'); -const Util = require('../util/Util'); +const { makeError, makePlainError } = require('../util/Util'); let childProcess = null; let Worker = null; @@ -252,7 +252,7 @@ class Shard extends EventEmitter { this.decrementMaxListeners(child); this._fetches.delete(prop); if (!message._error) resolve(message._result); - else reject(Util.makeError(message._error)); + else reject(makeError(message._error)); }; this.incrementMaxListeners(child); @@ -295,7 +295,7 @@ class Shard extends EventEmitter { this.decrementMaxListeners(child); this._evals.delete(_eval); if (!message._error) resolve(message._result); - else reject(Util.makeError(message._error)); + else reject(makeError(message._error)); }; this.incrementMaxListeners(child); @@ -358,7 +358,7 @@ class Shard extends EventEmitter { const resp = { _sFetchProp: message._sFetchProp, _sFetchPropShard: message._sFetchPropShard }; this.manager.fetchClientValues(message._sFetchProp, message._sFetchPropShard).then( results => this.send({ ...resp, _result: results }), - err => this.send({ ...resp, _error: Util.makePlainError(err) }), + err => this.send({ ...resp, _error: makePlainError(err) }), ); return; } @@ -368,7 +368,7 @@ class Shard extends EventEmitter { const resp = { _sEval: message._sEval, _sEvalShard: message._sEvalShard }; this.manager._performOnShards('eval', [message._sEval], message._sEvalShard).then( results => this.send({ ...resp, _result: results }), - err => this.send({ ...resp, _error: Util.makePlainError(err) }), + err => this.send({ ...resp, _error: makePlainError(err) }), ); return; } diff --git a/packages/discord.js/src/sharding/ShardClientUtil.js b/packages/discord.js/src/sharding/ShardClientUtil.js index 0772af6e8..478b72ca1 100644 --- a/packages/discord.js/src/sharding/ShardClientUtil.js +++ b/packages/discord.js/src/sharding/ShardClientUtil.js @@ -3,7 +3,7 @@ const process = require('node:process'); const { Error } = require('../errors'); const Events = require('../util/Events'); -const Util = require('../util/Util'); +const { makeError, makePlainError } = require('../util/Util'); /** * Helper class for sharded clients spawned as a child process/worker, such as from a {@link ShardingManager}. @@ -113,7 +113,7 @@ class ShardClientUtil { parent.removeListener('message', listener); this.decrementMaxListeners(parent); if (!message._error) resolve(message._result); - else reject(Util.makeError(message._error)); + else reject(makeError(message._error)); }; this.incrementMaxListeners(parent); parent.on('message', listener); @@ -151,7 +151,7 @@ class ShardClientUtil { parent.removeListener('message', listener); this.decrementMaxListeners(parent); if (!message._error) resolve(message._result); - else reject(Util.makeError(message._error)); + else reject(makeError(message._error)); }; this.incrementMaxListeners(parent); parent.on('message', listener); @@ -187,13 +187,13 @@ class ShardClientUtil { for (const prop of props) value = value[prop]; this._respond('fetchProp', { _fetchProp: message._fetchProp, _result: value }); } catch (err) { - this._respond('fetchProp', { _fetchProp: message._fetchProp, _error: Util.makePlainError(err) }); + this._respond('fetchProp', { _fetchProp: message._fetchProp, _error: makePlainError(err) }); } } else if (message._eval) { try { this._respond('eval', { _eval: message._eval, _result: await this.client._eval(message._eval) }); } catch (err) { - this._respond('eval', { _eval: message._eval, _error: Util.makePlainError(err) }); + this._respond('eval', { _eval: message._eval, _error: makePlainError(err) }); } } } diff --git a/packages/discord.js/src/sharding/ShardingManager.js b/packages/discord.js/src/sharding/ShardingManager.js index db8581c72..2014d7a70 100644 --- a/packages/discord.js/src/sharding/ShardingManager.js +++ b/packages/discord.js/src/sharding/ShardingManager.js @@ -8,7 +8,7 @@ const { setTimeout: sleep } = require('node:timers/promises'); const { Collection } = require('@discordjs/collection'); const Shard = require('./Shard'); const { Error, TypeError, RangeError } = require('../errors'); -const Util = require('../util/Util'); +const { mergeDefault, fetchRecommendedShards } = require('../util/Util'); /** * This is a utility class that makes multi-process sharding of a bot an easy and painless experience. @@ -47,7 +47,7 @@ class ShardingManager extends EventEmitter { */ constructor(file, options = {}) { super(); - options = Util.mergeDefault( + options = mergeDefault( { totalShards: 'auto', mode: 'process', @@ -183,7 +183,7 @@ class ShardingManager extends EventEmitter { async spawn({ amount = this.totalShards, delay = 5500, timeout = 30_000 } = {}) { // Obtain/verify the number of shards to spawn if (amount === 'auto') { - amount = await Util.fetchRecommendedShards(this.token); + amount = await fetchRecommendedShards(this.token); } else { if (typeof amount !== 'number' || isNaN(amount)) { throw new TypeError('CLIENT_INVALID_OPTION', 'Amount of shards', 'a number.'); diff --git a/packages/discord.js/src/structures/ActionRow.js b/packages/discord.js/src/structures/ActionRow.js index b902a31aa..d5fb48561 100644 --- a/packages/discord.js/src/structures/ActionRow.js +++ b/packages/discord.js/src/structures/ActionRow.js @@ -2,7 +2,7 @@ const { isJSONEncodable } = require('@discordjs/builders'); const Component = require('./Component'); -const Components = require('../util/Components'); +const { createComponent } = require('../util/Components'); /** * Represents an action row @@ -17,7 +17,7 @@ class ActionRow extends Component { * @type {Component[]} * @readonly */ - this.components = components.map(c => Components.createComponent(c)); + this.components = components.map(c => createComponent(c)); } /** diff --git a/packages/discord.js/src/structures/ActionRowBuilder.js b/packages/discord.js/src/structures/ActionRowBuilder.js index 871d51953..379b0e1f4 100644 --- a/packages/discord.js/src/structures/ActionRowBuilder.js +++ b/packages/discord.js/src/structures/ActionRowBuilder.js @@ -1,8 +1,8 @@ 'use strict'; const { ActionRowBuilder: BuildersActionRow, ComponentBuilder, isJSONEncodable } = require('@discordjs/builders'); -const Components = require('../util/Components'); -const Transformers = require('../util/Transformers'); +const { createComponentBuilder } = require('../util/Components'); +const { toSnakeCase } = require('../util/Transformers'); /** * Represents an action row builder. @@ -11,8 +11,8 @@ const Transformers = require('../util/Transformers'); class ActionRowBuilder extends BuildersActionRow { constructor({ components, ...data } = {}) { super({ - ...Transformers.toSnakeCase(data), - components: components?.map(c => (c instanceof ComponentBuilder ? c : Components.createComponentBuilder(c))), + ...toSnakeCase(data), + components: components?.map(c => (c instanceof ComponentBuilder ? c : createComponentBuilder(c))), }); } diff --git a/packages/discord.js/src/structures/Attachment.js b/packages/discord.js/src/structures/Attachment.js index 6481c8687..11a456431 100644 --- a/packages/discord.js/src/structures/Attachment.js +++ b/packages/discord.js/src/structures/Attachment.js @@ -1,6 +1,6 @@ 'use strict'; -const Util = require('../util/Util'); +const { basename, flatten } = require('../util/Util'); /** * @typedef {Object} AttachmentPayload @@ -107,11 +107,11 @@ class Attachment { * @readonly */ get spoiler() { - return Util.basename(this.url ?? this.name).startsWith('SPOILER_'); + return basename(this.url ?? this.name).startsWith('SPOILER_'); } toJSON() { - return Util.flatten(this); + return flatten(this); } } diff --git a/packages/discord.js/src/structures/AttachmentBuilder.js b/packages/discord.js/src/structures/AttachmentBuilder.js index fe4ce59a2..f904cb84b 100644 --- a/packages/discord.js/src/structures/AttachmentBuilder.js +++ b/packages/discord.js/src/structures/AttachmentBuilder.js @@ -1,6 +1,6 @@ 'use strict'; -const Util = require('../util/Util'); +const { basename, flatten } = require('../util/Util'); /** * Represents an attachment builder @@ -82,11 +82,11 @@ class AttachmentBuilder { * @readonly */ get spoiler() { - return Util.basename(this.name).startsWith('SPOILER_'); + return basename(this.name).startsWith('SPOILER_'); } toJSON() { - return Util.flatten(this); + return flatten(this); } /** diff --git a/packages/discord.js/src/structures/Base.js b/packages/discord.js/src/structures/Base.js index cd43bf799..102fb215d 100644 --- a/packages/discord.js/src/structures/Base.js +++ b/packages/discord.js/src/structures/Base.js @@ -1,6 +1,6 @@ 'use strict'; -const Util = require('../util/Util'); +const { flatten } = require('../util/Util'); /** * Represents a data model that is identifiable by a Snowflake (i.e. Discord API data models). @@ -32,7 +32,7 @@ class Base { } toJSON(...props) { - return Util.flatten(this, ...props); + return flatten(this, ...props); } valueOf() { diff --git a/packages/discord.js/src/structures/ButtonBuilder.js b/packages/discord.js/src/structures/ButtonBuilder.js index c427ce3c7..671a77a8f 100644 --- a/packages/discord.js/src/structures/ButtonBuilder.js +++ b/packages/discord.js/src/structures/ButtonBuilder.js @@ -1,8 +1,8 @@ 'use strict'; const { ButtonBuilder: BuildersButton, isJSONEncodable } = require('@discordjs/builders'); -const Transformers = require('../util/Transformers'); -const Util = require('../util/Util'); +const { toSnakeCase } = require('../util/Transformers'); +const { parseEmoji } = require('../util/Util'); /** * Represents a button builder. @@ -10,9 +10,7 @@ const Util = require('../util/Util'); */ class ButtonBuilder extends BuildersButton { constructor({ emoji, ...data } = {}) { - super( - Transformers.toSnakeCase({ ...data, emoji: emoji && typeof emoji === 'string' ? Util.parseEmoji(emoji) : emoji }), - ); + super(toSnakeCase({ ...data, emoji: emoji && typeof emoji === 'string' ? parseEmoji(emoji) : emoji })); } /** @@ -22,7 +20,7 @@ class ButtonBuilder extends BuildersButton { */ setEmoji(emoji) { if (typeof emoji === 'string') { - return super.setEmoji(Util.parseEmoji(emoji)); + return super.setEmoji(parseEmoji(emoji)); } return super.setEmoji(emoji); } diff --git a/packages/discord.js/src/structures/Channel.js b/packages/discord.js/src/structures/Channel.js index cdf6074b9..67e497e1a 100644 --- a/packages/discord.js/src/structures/Channel.js +++ b/packages/discord.js/src/structures/Channel.js @@ -4,14 +4,6 @@ const { DiscordSnowflake } = require('@sapphire/snowflake'); const { ChannelType, Routes } = require('discord-api-types/v10'); const Base = require('./Base'); const { ThreadChannelTypes } = require('../util/Constants'); -let CategoryChannel; -let DMChannel; -let NewsChannel; -let StageChannel; -let TextChannel; -let ThreadChannel; -let VoiceChannel; -let DirectoryChannel; /** * Represents any channel on Discord. @@ -142,66 +134,6 @@ class Channel extends Base { return 'bitrate' in this; } - static create(client, data, guild, { allowUnknownGuild, fromInteraction } = {}) { - CategoryChannel ??= require('./CategoryChannel'); - DMChannel ??= require('./DMChannel'); - NewsChannel ??= require('./NewsChannel'); - StageChannel ??= require('./StageChannel'); - TextChannel ??= require('./TextChannel'); - ThreadChannel ??= require('./ThreadChannel'); - VoiceChannel ??= require('./VoiceChannel'); - DirectoryChannel ??= require('./DirectoryChannel'); - - let channel; - if (!data.guild_id && !guild) { - if ((data.recipients && data.type !== ChannelType.GroupDM) || data.type === ChannelType.DM) { - channel = new DMChannel(client, data); - } else if (data.type === ChannelType.GroupDM) { - const PartialGroupDMChannel = require('./PartialGroupDMChannel'); - channel = new PartialGroupDMChannel(client, data); - } - } else { - guild ??= client.guilds.cache.get(data.guild_id); - - if (guild || allowUnknownGuild) { - switch (data.type) { - case ChannelType.GuildText: { - channel = new TextChannel(guild, data, client); - break; - } - case ChannelType.GuildVoice: { - channel = new VoiceChannel(guild, data, client); - break; - } - case ChannelType.GuildCategory: { - channel = new CategoryChannel(guild, data, client); - break; - } - case ChannelType.GuildNews: { - channel = new NewsChannel(guild, data, client); - break; - } - case ChannelType.GuildStageVoice: { - channel = new StageChannel(guild, data, client); - break; - } - case ChannelType.GuildNewsThread: - case ChannelType.GuildPublicThread: - case ChannelType.GuildPrivateThread: { - channel = new ThreadChannel(guild, data, client, fromInteraction); - if (!allowUnknownGuild) channel.parent?.threads.cache.set(channel.id, channel); - break; - } - case ChannelType.GuildDirectory: - channel = new DirectoryChannel(guild, data, client); - break; - } - if (channel && !allowUnknownGuild) guild.channels?.cache.set(channel.id, channel); - } - } - return channel; - } - toJSON(...props) { return super.toJSON({ createdTimestamp: true }, ...props); } diff --git a/packages/discord.js/src/structures/EmbedBuilder.js b/packages/discord.js/src/structures/EmbedBuilder.js index 289604f37..58d5c2104 100644 --- a/packages/discord.js/src/structures/EmbedBuilder.js +++ b/packages/discord.js/src/structures/EmbedBuilder.js @@ -1,8 +1,8 @@ 'use strict'; const { EmbedBuilder: BuildersEmbed, isJSONEncodable } = require('@discordjs/builders'); -const Transformers = require('../util/Transformers'); -const Util = require('../util/Util'); +const { toSnakeCase } = require('../util/Transformers'); +const { resolveColor } = require('../util/Util'); /** * Represents an embed builder. @@ -10,7 +10,7 @@ const Util = require('../util/Util'); */ class EmbedBuilder extends BuildersEmbed { constructor(data) { - super(Transformers.toSnakeCase(data)); + super(toSnakeCase(data)); } /** @@ -19,7 +19,7 @@ class EmbedBuilder extends BuildersEmbed { * @returns {EmbedBuilder} */ setColor(color) { - return super.setColor(color && Util.resolveColor(color)); + return super.setColor(color && resolveColor(color)); } /** diff --git a/packages/discord.js/src/structures/Guild.js b/packages/discord.js/src/structures/Guild.js index 6bdaa9942..00335080a 100644 --- a/packages/discord.js/src/structures/Guild.js +++ b/packages/discord.js/src/structures/Guild.js @@ -27,7 +27,7 @@ const VoiceStateManager = require('../managers/VoiceStateManager'); const DataResolver = require('../util/DataResolver'); const Status = require('../util/Status'); const SystemChannelFlagsBitField = require('../util/SystemChannelFlagsBitField'); -const Util = require('../util/Util'); +const { discordSort } = require('../util/Util'); /** * Represents a guild (or a server) on Discord. @@ -1253,7 +1253,7 @@ class Guild extends AnonymousGuild { * @private */ _sortedRoles() { - return Util.discordSort(this.roles.cache); + return discordSort(this.roles.cache); } /** @@ -1265,7 +1265,7 @@ class Guild extends AnonymousGuild { _sortedChannels(channel) { const category = channel.type === ChannelType.GuildCategory; const channelTypes = [ChannelType.GuildText, ChannelType.GuildNews]; - return Util.discordSort( + return discordSort( this.channels.cache.filter( c => (channelTypes.includes(channel.type) ? channelTypes.includes(c.type) : c.type === channel.type) && diff --git a/packages/discord.js/src/structures/GuildAuditLogs.js b/packages/discord.js/src/structures/GuildAuditLogs.js index c637e7bc3..788c03dc1 100644 --- a/packages/discord.js/src/structures/GuildAuditLogs.js +++ b/packages/discord.js/src/structures/GuildAuditLogs.js @@ -5,7 +5,7 @@ const ApplicationCommand = require('./ApplicationCommand'); const GuildAuditLogsEntry = require('./GuildAuditLogsEntry'); const Integration = require('./Integration'); const Webhook = require('./Webhook'); -const Util = require('../util/Util'); +const { flatten } = require('../util/Util'); /** * The target type of an entry. Here are the available types: @@ -92,7 +92,7 @@ class GuildAuditLogs { } toJSON() { - return Util.flatten(this); + return flatten(this); } } diff --git a/packages/discord.js/src/structures/GuildAuditLogsEntry.js b/packages/discord.js/src/structures/GuildAuditLogsEntry.js index 92ab758f0..689f79fc7 100644 --- a/packages/discord.js/src/structures/GuildAuditLogsEntry.js +++ b/packages/discord.js/src/structures/GuildAuditLogsEntry.js @@ -9,7 +9,7 @@ const { StageInstance } = require('./StageInstance'); const { Sticker } = require('./Sticker'); const Webhook = require('./Webhook'); const Partials = require('../util/Partials'); -const Util = require('../util/Util'); +const { flatten } = require('../util/Util'); const Targets = { All: 'All', @@ -459,7 +459,7 @@ class GuildAuditLogsEntry { } toJSON() { - return Util.flatten(this, { createdTimestamp: true }); + return flatten(this, { createdTimestamp: true }); } } diff --git a/packages/discord.js/src/structures/Message.js b/packages/discord.js/src/structures/Message.js index 32e6fd74e..86dbbff6c 100644 --- a/packages/discord.js/src/structures/Message.js +++ b/packages/discord.js/src/structures/Message.js @@ -20,11 +20,11 @@ const ReactionCollector = require('./ReactionCollector'); const { Sticker } = require('./Sticker'); const { Error } = require('../errors'); const ReactionManager = require('../managers/ReactionManager'); -const Components = require('../util/Components'); +const { createComponent } = require('../util/Components'); const { NonSystemMessageTypes } = require('../util/Constants'); const MessageFlagsBitField = require('../util/MessageFlagsBitField'); const PermissionsBitField = require('../util/PermissionsBitField'); -const Util = require('../util/Util'); +const { cleanContent, resolvePartialEmoji } = require('../util/Util'); /** * Represents a message on Discord. @@ -146,7 +146,7 @@ class Message extends Base { * A list of MessageActionRows in the message * @type {ActionRow[]} */ - this.components = data.components.map(c => Components.createComponent(c)); + this.components = data.components.map(c => createComponent(c)); } else { this.components = this.components?.slice() ?? []; } @@ -441,7 +441,7 @@ class Message extends Base { */ get cleanContent() { // eslint-disable-next-line eqeqeq - return this.content != null ? Util.cleanContent(this.content, this.channel) : null; + return this.content != null ? cleanContent(this.content, this.channel) : null; } /** @@ -737,7 +737,7 @@ class Message extends Base { user: this.client.user, channel: this.channel, message: this, - emoji: Util.resolvePartialEmoji(emoji), + emoji: resolvePartialEmoji(emoji), }, true, ).reaction; diff --git a/packages/discord.js/src/structures/MessageMentions.js b/packages/discord.js/src/structures/MessageMentions.js index 25149b7d4..1845c0018 100644 --- a/packages/discord.js/src/structures/MessageMentions.js +++ b/packages/discord.js/src/structures/MessageMentions.js @@ -1,7 +1,7 @@ 'use strict'; const { Collection } = require('@discordjs/collection'); -const Util = require('../util/Util'); +const { flatten } = require('../util/Util'); /** * Keeps track of mentions in a {@link Message}. @@ -233,7 +233,7 @@ class MessageMentions { } toJSON() { - return Util.flatten(this, { + return flatten(this, { members: true, channels: true, }); diff --git a/packages/discord.js/src/structures/MessagePayload.js b/packages/discord.js/src/structures/MessagePayload.js index 3f1769bcd..395a412e9 100644 --- a/packages/discord.js/src/structures/MessagePayload.js +++ b/packages/discord.js/src/structures/MessagePayload.js @@ -7,7 +7,7 @@ const ActionRowBuilder = require('./ActionRowBuilder'); const { RangeError } = require('../errors'); const DataResolver = require('../util/DataResolver'); const MessageFlagsBitField = require('../util/MessageFlagsBitField'); -const Util = require('../util/Util'); +const { basename, cloneObject, verifyString } = require('../util/Util'); /** * Represents a message to be sent to the API. @@ -105,7 +105,7 @@ class MessagePayload { if (this.options.content === null) { content = ''; } else if (typeof this.options.content !== 'undefined') { - content = Util.verifyString(this.options.content, RangeError, 'MESSAGE_CONTENT_TYPE', true); + content = verifyString(this.options.content, RangeError, 'MESSAGE_CONTENT_TYPE', true); } return content; @@ -164,7 +164,7 @@ class MessagePayload { : this.options.allowedMentions; if (allowedMentions) { - allowedMentions = Util.cloneObject(allowedMentions); + allowedMentions = cloneObject(allowedMentions); allowedMentions.replied_user = allowedMentions.repliedUser; delete allowedMentions.repliedUser; } @@ -234,11 +234,11 @@ class MessagePayload { const findName = thing => { if (typeof thing === 'string') { - return Util.basename(thing); + return basename(thing); } if (thing.path) { - return Util.basename(thing.path); + return basename(thing.path); } return 'file.jpg'; diff --git a/packages/discord.js/src/structures/MessageReaction.js b/packages/discord.js/src/structures/MessageReaction.js index 099ade8c7..d44eb462a 100644 --- a/packages/discord.js/src/structures/MessageReaction.js +++ b/packages/discord.js/src/structures/MessageReaction.js @@ -4,7 +4,7 @@ const { Routes } = require('discord-api-types/v10'); const GuildEmoji = require('./GuildEmoji'); const ReactionEmoji = require('./ReactionEmoji'); const ReactionUserManager = require('../managers/ReactionUserManager'); -const Util = require('../util/Util'); +const { flatten } = require('../util/Util'); /** * Represents a reaction to a message. @@ -114,7 +114,7 @@ class MessageReaction { } toJSON() { - return Util.flatten(this, { emoji: 'emojiId', message: 'messageId' }); + return flatten(this, { emoji: 'emojiId', message: 'messageId' }); } _add(user) { diff --git a/packages/discord.js/src/structures/ModalBuilder.js b/packages/discord.js/src/structures/ModalBuilder.js index 7c4726c55..84ddfbea2 100644 --- a/packages/discord.js/src/structures/ModalBuilder.js +++ b/packages/discord.js/src/structures/ModalBuilder.js @@ -1,7 +1,7 @@ 'use strict'; const { ModalBuilder: BuildersModal, ComponentBuilder, isJSONEncodable } = require('@discordjs/builders'); -const Transformers = require('../util/Transformers'); +const { toSnakeCase } = require('../util/Transformers'); /** * Represents a modal builder. @@ -10,8 +10,8 @@ const Transformers = require('../util/Transformers'); class ModalBuilder extends BuildersModal { constructor({ components, ...data } = {}) { super({ - ...Transformers.toSnakeCase(data), - components: components?.map(c => (c instanceof ComponentBuilder ? c : Transformers.toSnakeCase(c))), + ...toSnakeCase(data), + components: components?.map(c => (c instanceof ComponentBuilder ? c : toSnakeCase(c))), }); } diff --git a/packages/discord.js/src/structures/Presence.js b/packages/discord.js/src/structures/Presence.js index 7b52a3caa..8d59671de 100644 --- a/packages/discord.js/src/structures/Presence.js +++ b/packages/discord.js/src/structures/Presence.js @@ -3,7 +3,7 @@ const Base = require('./Base'); const { Emoji } = require('./Emoji'); const ActivityFlagsBitField = require('../util/ActivityFlagsBitField'); -const Util = require('../util/Util'); +const { flatten } = require('../util/Util'); /** * Activity sent in a message. @@ -132,7 +132,7 @@ class Presence extends Base { } toJSON() { - return Util.flatten(this); + return flatten(this); } } diff --git a/packages/discord.js/src/structures/ReactionEmoji.js b/packages/discord.js/src/structures/ReactionEmoji.js index bcd247055..08e2ea025 100644 --- a/packages/discord.js/src/structures/ReactionEmoji.js +++ b/packages/discord.js/src/structures/ReactionEmoji.js @@ -1,7 +1,7 @@ 'use strict'; const { Emoji } = require('./Emoji'); -const Util = require('../util/Util'); +const { flatten } = require('../util/Util'); /** * Represents a limited emoji set used for both custom and unicode emojis. Custom emojis @@ -20,7 +20,7 @@ class ReactionEmoji extends Emoji { } toJSON() { - return Util.flatten(this, { identifier: true }); + return flatten(this, { identifier: true }); } valueOf() { diff --git a/packages/discord.js/src/structures/SelectMenuBuilder.js b/packages/discord.js/src/structures/SelectMenuBuilder.js index 4d3c7710d..0e6656ccb 100644 --- a/packages/discord.js/src/structures/SelectMenuBuilder.js +++ b/packages/discord.js/src/structures/SelectMenuBuilder.js @@ -1,8 +1,8 @@ 'use strict'; const { SelectMenuBuilder: BuildersSelectMenu, isJSONEncodable, normalizeArray } = require('@discordjs/builders'); -const Transformers = require('../util/Transformers'); -const Util = require('../util/Util'); +const { toSnakeCase } = require('../util/Transformers'); +const { parseEmoji } = require('../util/Util'); /** * Class used to build select menu components to be sent through the API @@ -11,11 +11,11 @@ const Util = require('../util/Util'); class SelectMenuBuilder extends BuildersSelectMenu { constructor({ options, ...data } = {}) { super( - Transformers.toSnakeCase({ + toSnakeCase({ ...data, options: options?.map(({ emoji, ...option }) => ({ ...option, - emoji: emoji && typeof emoji === 'string' ? Util.parseEmoji(emoji) : emoji, + emoji: emoji && typeof emoji === 'string' ? parseEmoji(emoji) : emoji, })), }), ); @@ -30,7 +30,7 @@ class SelectMenuBuilder extends BuildersSelectMenu { return super.addOptions( normalizeArray(options).map(({ emoji, ...option }) => ({ ...option, - emoji: emoji && typeof emoji === 'string' ? Util.parseEmoji(emoji) : emoji, + emoji: emoji && typeof emoji === 'string' ? parseEmoji(emoji) : emoji, })), ); } @@ -44,7 +44,7 @@ class SelectMenuBuilder extends BuildersSelectMenu { return super.setOptions( normalizeArray(options).map(({ emoji, ...option }) => ({ ...option, - emoji: emoji && typeof emoji === 'string' ? Util.parseEmoji(emoji) : emoji, + emoji: emoji && typeof emoji === 'string' ? parseEmoji(emoji) : emoji, })), ); } diff --git a/packages/discord.js/src/structures/SelectMenuOptionBuilder.js b/packages/discord.js/src/structures/SelectMenuOptionBuilder.js index 981b8d0ec..c5f0bd136 100644 --- a/packages/discord.js/src/structures/SelectMenuOptionBuilder.js +++ b/packages/discord.js/src/structures/SelectMenuOptionBuilder.js @@ -1,8 +1,8 @@ 'use strict'; const { SelectMenuOptionBuilder: BuildersSelectMenuOption, isJSONEncodable } = require('@discordjs/builders'); -const Transformers = require('../util/Transformers'); -const Util = require('../util/Util'); +const { toSnakeCase } = require('../util/Transformers'); +const { parseEmoji } = require('../util/Util'); /** * Represents a select menu option builder. @@ -11,9 +11,9 @@ const Util = require('../util/Util'); class SelectMenuOptionBuilder extends BuildersSelectMenuOption { constructor({ emoji, ...data } = {}) { super( - Transformers.toSnakeCase({ + toSnakeCase({ ...data, - emoji: emoji && typeof emoji === 'string' ? Util.parseEmoji(emoji) : emoji, + emoji: emoji && typeof emoji === 'string' ? parseEmoji(emoji) : emoji, }), ); } @@ -24,7 +24,7 @@ class SelectMenuOptionBuilder extends BuildersSelectMenuOption { */ setEmoji(emoji) { if (typeof emoji === 'string') { - return super.setEmoji(Util.parseEmoji(emoji)); + return super.setEmoji(parseEmoji(emoji)); } return super.setEmoji(emoji); } diff --git a/packages/discord.js/src/structures/TextInputBuilder.js b/packages/discord.js/src/structures/TextInputBuilder.js index cdd027b6a..a30b3689b 100644 --- a/packages/discord.js/src/structures/TextInputBuilder.js +++ b/packages/discord.js/src/structures/TextInputBuilder.js @@ -1,7 +1,7 @@ 'use strict'; const { TextInputBuilder: BuildersTextInput, isJSONEncodable } = require('@discordjs/builders'); -const Transformers = require('../util/Transformers'); +const { toSnakeCase } = require('../util/Transformers'); /** * Represents a text input builder. @@ -9,7 +9,7 @@ const Transformers = require('../util/Transformers'); */ class TextInputBuilder extends BuildersTextInput { constructor(data) { - super(Transformers.toSnakeCase(data)); + super(toSnakeCase(data)); } /** diff --git a/packages/discord.js/src/structures/VoiceRegion.js b/packages/discord.js/src/structures/VoiceRegion.js index be46b4df3..1f5652a2c 100644 --- a/packages/discord.js/src/structures/VoiceRegion.js +++ b/packages/discord.js/src/structures/VoiceRegion.js @@ -1,6 +1,6 @@ 'use strict'; -const Util = require('../util/Util'); +const { flatten } = require('../util/Util'); /** * Represents a Discord voice region for guilds. @@ -39,7 +39,7 @@ class VoiceRegion { } toJSON() { - return Util.flatten(this); + return flatten(this); } } diff --git a/packages/discord.js/src/structures/interfaces/Collector.js b/packages/discord.js/src/structures/interfaces/Collector.js index ed82fa533..f0d488fa4 100644 --- a/packages/discord.js/src/structures/interfaces/Collector.js +++ b/packages/discord.js/src/structures/interfaces/Collector.js @@ -4,7 +4,7 @@ const EventEmitter = require('node:events'); const { setTimeout, clearTimeout } = require('node:timers'); const { Collection } = require('@discordjs/collection'); const { TypeError } = require('../../errors'); -const Util = require('../../util/Util'); +const { flatten } = require('../../util/Util'); /** * Filter to be applied to the collector. @@ -281,7 +281,7 @@ class Collector extends EventEmitter { } toJSON() { - return Util.flatten(this); + return flatten(this); } /* eslint-disable no-empty-function */ diff --git a/packages/discord.js/src/util/Channels.js b/packages/discord.js/src/util/Channels.js new file mode 100644 index 000000000..c9f83ed26 --- /dev/null +++ b/packages/discord.js/src/util/Channels.js @@ -0,0 +1,72 @@ +'use strict'; + +const { ChannelType } = require('discord-api-types/v10'); +const { lazy } = require('./Util'); + +const getCategoryChannel = lazy(() => require('../structures/CategoryChannel')); +const getDMChannel = lazy(() => require('../structures/DMChannel')); +const getNewsChannel = lazy(() => require('../structures/NewsChannel')); +const getStageChannel = lazy(() => require('../structures/StageChannel')); +const getTextChannel = lazy(() => require('../structures/TextChannel')); +const getThreadChannel = lazy(() => require('../structures/ThreadChannel')); +const getVoiceChannel = lazy(() => require('../structures/VoiceChannel')); +const getDirectoryChannel = lazy(() => require('../structures/DirectoryChannel')); +const getPartialGroupDMChannel = lazy(() => require('../structures/PartialGroupDMChannel')); + +// eslint-disable-next-line valid-jsdoc +/** + * @private + */ +function createChannel(client, data, guild, { allowUnknownGuild, fromInteraction } = {}) { + let channel; + if (!data.guild_id && !guild) { + if ((data.recipients && data.type !== ChannelType.GroupDM) || data.type === ChannelType.DM) { + channel = new (getDMChannel())(client, data); + } else if (data.type === ChannelType.GroupDM) { + channel = new (getPartialGroupDMChannel())(client, data); + } + } else { + guild ??= client.guilds.cache.get(data.guild_id); + + if (guild || allowUnknownGuild) { + switch (data.type) { + case ChannelType.GuildText: { + channel = new (getTextChannel())(guild, data, client); + break; + } + case ChannelType.GuildVoice: { + channel = new (getVoiceChannel())(guild, data, client); + break; + } + case ChannelType.GuildCategory: { + channel = new (getCategoryChannel())(guild, data, client); + break; + } + case ChannelType.GuildNews: { + channel = new (getNewsChannel())(guild, data, client); + break; + } + case ChannelType.GuildStageVoice: { + channel = new (getStageChannel())(guild, data, client); + break; + } + case ChannelType.GuildNewsThread: + case ChannelType.GuildPublicThread: + case ChannelType.GuildPrivateThread: { + channel = new (getThreadChannel())(guild, data, client, fromInteraction); + if (!allowUnknownGuild) channel.parent?.threads.cache.set(channel.id, channel); + break; + } + case ChannelType.GuildDirectory: + channel = new (getDirectoryChannel())(guild, data, client); + break; + } + if (channel && !allowUnknownGuild) guild.channels?.cache.set(channel.id, channel); + } + } + return channel; +} + +module.exports = { + createChannel, +}; diff --git a/packages/discord.js/src/util/Components.js b/packages/discord.js/src/util/Components.js index 6002c99cf..ca3f02cca 100644 --- a/packages/discord.js/src/util/Components.js +++ b/packages/discord.js/src/util/Components.js @@ -67,57 +67,55 @@ const { ComponentType } = require('discord-api-types/v10'); * @typedef {APIMessageComponentEmoji|string} ComponentEmojiResolvable */ -class Components extends null { - /** - * Transforms API data into a component - * @param {APIMessageComponent|Component} data The data to create the component from - * @returns {Component} - */ - static createComponent(data) { - if (data instanceof Component) { - return data; - } - - switch (data.type) { - case ComponentType.ActionRow: - return new ActionRow(data); - case ComponentType.Button: - return new ButtonComponent(data); - case ComponentType.SelectMenu: - return new SelectMenuComponent(data); - case ComponentType.TextInput: - return new TextInputComponent(data); - default: - throw new Error(`Found unknown component type: ${data.type}`); - } +/** + * Transforms API data into a component + * @param {APIMessageComponent|Component} data The data to create the component from + * @returns {Component} + */ +function createComponent(data) { + if (data instanceof Component) { + return data; } - /** - * Transforms API data into a component builder - * @param {APIMessageComponent|ComponentBuilder} data The data to create the component from - * @returns {ComponentBuilder} - */ - static createComponentBuilder(data) { - if (data instanceof ComponentBuilder) { - return data; - } - - switch (data.type) { - case ComponentType.ActionRow: - return new ActionRowBuilder(data); - case ComponentType.Button: - return new ButtonBuilder(data); - case ComponentType.SelectMenu: - return new SelectMenuBuilder(data); - case ComponentType.TextInput: - return new TextInputComponent(data); - default: - throw new Error(`Found unknown component type: ${data.type}`); - } + switch (data.type) { + case ComponentType.ActionRow: + return new ActionRow(data); + case ComponentType.Button: + return new ButtonComponent(data); + case ComponentType.SelectMenu: + return new SelectMenuComponent(data); + case ComponentType.TextInput: + return new TextInputComponent(data); + default: + throw new Error(`Found unknown component type: ${data.type}`); } } -module.exports = Components; +/** + * Transforms API data into a component builder + * @param {APIMessageComponent|ComponentBuilder} data The data to create the component from + * @returns {ComponentBuilder} + */ +function createComponentBuilder(data) { + if (data instanceof ComponentBuilder) { + return data; + } + + switch (data.type) { + case ComponentType.ActionRow: + return new ActionRowBuilder(data); + case ComponentType.Button: + return new ButtonBuilder(data); + case ComponentType.SelectMenu: + return new SelectMenuBuilder(data); + case ComponentType.TextInput: + return new TextInputComponent(data); + default: + throw new Error(`Found unknown component type: ${data.type}`); + } +} + +module.exports = { createComponent, createComponentBuilder }; const ActionRow = require('../structures/ActionRow'); const ActionRowBuilder = require('../structures/ActionRowBuilder'); diff --git a/packages/discord.js/src/util/Options.js b/packages/discord.js/src/util/Options.js index 0fe89000e..aec3d25d4 100644 --- a/packages/discord.js/src/util/Options.js +++ b/packages/discord.js/src/util/Options.js @@ -2,7 +2,7 @@ const process = require('node:process'); const { DefaultRestOptions } = require('@discordjs/rest'); -const Transformers = require('./Transformers'); +const { toSnakeCase } = require('./Transformers'); /** * @typedef {Function} CacheFactory @@ -93,7 +93,7 @@ class Options extends null { version: 10, }, rest: DefaultRestOptions, - jsonTransformer: Transformers.toSnakeCase, + jsonTransformer: toSnakeCase, }; } diff --git a/packages/discord.js/src/util/Transformers.js b/packages/discord.js/src/util/Transformers.js index f7eed82a3..6437a7c99 100644 --- a/packages/discord.js/src/util/Transformers.js +++ b/packages/discord.js/src/util/Transformers.js @@ -2,19 +2,15 @@ const snakeCase = require('lodash.snakecase'); -class Transformers extends null { - /** - * Transforms camel-cased keys into snake cased keys - * @param {*} obj The object to transform - * @returns {*} - */ - static toSnakeCase(obj) { - if (typeof obj !== 'object' || !obj) return obj; - if (Array.isArray(obj)) return obj.map(Transformers.toSnakeCase); - return Object.fromEntries( - Object.entries(obj).map(([key, value]) => [snakeCase(key), Transformers.toSnakeCase(value)]), - ); - } +/** + * Transforms camel-cased keys into snake cased keys + * @param {*} obj The object to transform + * @returns {*} + */ +function toSnakeCase(obj) { + if (typeof obj !== 'object' || !obj) return obj; + if (Array.isArray(obj)) return obj.map(toSnakeCase); + return Object.fromEntries(Object.entries(obj).map(([key, value]) => [snakeCase(key), toSnakeCase(value)])); } -module.exports = Transformers; +module.exports = { toSnakeCase }; diff --git a/packages/discord.js/src/util/Util.js b/packages/discord.js/src/util/Util.js index 6b3a693d4..75b13d0ee 100644 --- a/packages/discord.js/src/util/Util.js +++ b/packages/discord.js/src/util/Util.js @@ -9,531 +9,552 @@ const { Error: DiscordError, RangeError, TypeError } = require('../errors'); const isObject = d => typeof d === 'object' && d !== null; /** - * Contains various general-purpose utility methods. + * Flatten an object. Any properties that are collections will get converted to an array of keys. + * @param {Object} obj The object to flatten. + * @param {...Object} [props] Specific properties to include/exclude. + * @returns {Object} */ -class Util extends null { - /** - * Flatten an object. Any properties that are collections will get converted to an array of keys. - * @param {Object} obj The object to flatten. - * @param {...Object} [props] Specific properties to include/exclude. - * @returns {Object} - */ - static flatten(obj, ...props) { - if (!isObject(obj)) return obj; +function flatten(obj, ...props) { + if (!isObject(obj)) return obj; - const objProps = Object.keys(obj) - .filter(k => !k.startsWith('_')) - .map(k => ({ [k]: true })); + const objProps = Object.keys(obj) + .filter(k => !k.startsWith('_')) + .map(k => ({ [k]: true })); - props = objProps.length ? Object.assign(...objProps, ...props) : Object.assign({}, ...props); + props = objProps.length ? Object.assign(...objProps, ...props) : Object.assign({}, ...props); - const out = {}; + const out = {}; - for (let [prop, newProp] of Object.entries(props)) { - if (!newProp) continue; - newProp = newProp === true ? prop : newProp; + for (let [prop, newProp] of Object.entries(props)) { + if (!newProp) continue; + newProp = newProp === true ? prop : newProp; - const element = obj[prop]; - const elemIsObj = isObject(element); - const valueOf = elemIsObj && typeof element.valueOf === 'function' ? element.valueOf() : null; - const hasToJSON = elemIsObj && typeof element.toJSON === 'function'; + const element = obj[prop]; + const elemIsObj = isObject(element); + const valueOf = elemIsObj && typeof element.valueOf === 'function' ? element.valueOf() : null; + const hasToJSON = elemIsObj && typeof element.toJSON === 'function'; - // If it's a Collection, make the array of keys - if (element instanceof Collection) out[newProp] = Array.from(element.keys()); - // If the valueOf is a Collection, use its array of keys - else if (valueOf instanceof Collection) out[newProp] = Array.from(valueOf.keys()); - // If it's an array, call toJSON function on each element if present, otherwise flatten each element - else if (Array.isArray(element)) out[newProp] = element.map(e => e.toJSON?.() ?? Util.flatten(e)); - // If it's an object with a primitive `valueOf`, use that value - else if (typeof valueOf !== 'object') out[newProp] = valueOf; - // If it's an object with a toJSON function, use the return value of it - else if (hasToJSON) out[newProp] = element.toJSON(); - // If element is an object, use the flattened version of it - else if (typeof element === 'object') out[newProp] = Util.flatten(element); - // If it's a primitive - else if (!elemIsObj) out[newProp] = element; - } - - return out; + // If it's a Collection, make the array of keys + if (element instanceof Collection) out[newProp] = Array.from(element.keys()); + // If the valueOf is a Collection, use its array of keys + else if (valueOf instanceof Collection) out[newProp] = Array.from(valueOf.keys()); + // If it's an array, call toJSON function on each element if present, otherwise flatten each element + else if (Array.isArray(element)) out[newProp] = element.map(e => e.toJSON?.() ?? flatten(e)); + // If it's an object with a primitive `valueOf`, use that value + else if (typeof valueOf !== 'object') out[newProp] = valueOf; + // If it's an object with a toJSON function, use the return value of it + else if (hasToJSON) out[newProp] = element.toJSON(); + // If element is an object, use the flattened version of it + else if (typeof element === 'object') out[newProp] = flatten(element); + // If it's a primitive + else if (!elemIsObj) out[newProp] = element; } - /** - * Options used to escape markdown. - * @typedef {Object} EscapeMarkdownOptions - * @property {boolean} [codeBlock=true] Whether to escape code blocks or not - * @property {boolean} [inlineCode=true] Whether to escape inline code or not - * @property {boolean} [bold=true] Whether to escape bolds or not - * @property {boolean} [italic=true] Whether to escape italics or not - * @property {boolean} [underline=true] Whether to escape underlines or not - * @property {boolean} [strikethrough=true] Whether to escape strikethroughs or not - * @property {boolean} [spoiler=true] Whether to escape spoilers or not - * @property {boolean} [codeBlockContent=true] Whether to escape text inside code blocks or not - * @property {boolean} [inlineCodeContent=true] Whether to escape text inside inline code or not - */ - - /** - * Escapes any Discord-flavour markdown in a string. - * @param {string} text Content to escape - * @param {EscapeMarkdownOptions} [options={}] Options for escaping the markdown - * @returns {string} - */ - static escapeMarkdown( - text, - { - codeBlock = true, - inlineCode = true, - bold = true, - italic = true, - underline = true, - strikethrough = true, - spoiler = true, - codeBlockContent = true, - inlineCodeContent = true, - } = {}, - ) { - if (!codeBlockContent) { - return text - .split('```') - .map((subString, index, array) => { - if (index % 2 && index !== array.length - 1) return subString; - return Util.escapeMarkdown(subString, { - inlineCode, - bold, - italic, - underline, - strikethrough, - spoiler, - inlineCodeContent, - }); - }) - .join(codeBlock ? '\\`\\`\\`' : '```'); - } - if (!inlineCodeContent) { - return text - .split(/(?<=^|[^`])`(?=[^`]|$)/g) - .map((subString, index, array) => { - if (index % 2 && index !== array.length - 1) return subString; - return Util.escapeMarkdown(subString, { - codeBlock, - bold, - italic, - underline, - strikethrough, - spoiler, - }); - }) - .join(inlineCode ? '\\`' : '`'); - } - if (inlineCode) text = Util.escapeInlineCode(text); - if (codeBlock) text = Util.escapeCodeBlock(text); - if (italic) text = Util.escapeItalic(text); - if (bold) text = Util.escapeBold(text); - if (underline) text = Util.escapeUnderline(text); - if (strikethrough) text = Util.escapeStrikethrough(text); - if (spoiler) text = Util.escapeSpoiler(text); - return text; - } - - /** - * Escapes code block markdown in a string. - * @param {string} text Content to escape - * @returns {string} - */ - static escapeCodeBlock(text) { - return text.replaceAll('```', '\\`\\`\\`'); - } - - /** - * Escapes inline code markdown in a string. - * @param {string} text Content to escape - * @returns {string} - */ - static escapeInlineCode(text) { - return text.replace(/(?<=^|[^`])``?(?=[^`]|$)/g, match => (match.length === 2 ? '\\`\\`' : '\\`')); - } - - /** - * Escapes italic markdown in a string. - * @param {string} text Content to escape - * @returns {string} - */ - static escapeItalic(text) { - let i = 0; - text = text.replace(/(?<=^|[^*])\*([^*]|\*\*|$)/g, (_, match) => { - if (match === '**') return ++i % 2 ? `\\*${match}` : `${match}\\*`; - return `\\*${match}`; - }); - i = 0; - return text.replace(/(?<=^|[^_])_([^_]|__|$)/g, (_, match) => { - if (match === '__') return ++i % 2 ? `\\_${match}` : `${match}\\_`; - return `\\_${match}`; - }); - } - - /** - * Escapes bold markdown in a string. - * @param {string} text Content to escape - * @returns {string} - */ - static escapeBold(text) { - let i = 0; - return text.replace(/\*\*(\*)?/g, (_, match) => { - if (match) return ++i % 2 ? `${match}\\*\\*` : `\\*\\*${match}`; - return '\\*\\*'; - }); - } - - /** - * Escapes underline markdown in a string. - * @param {string} text Content to escape - * @returns {string} - */ - static escapeUnderline(text) { - let i = 0; - return text.replace(/__(_)?/g, (_, match) => { - if (match) return ++i % 2 ? `${match}\\_\\_` : `\\_\\_${match}`; - return '\\_\\_'; - }); - } - - /** - * Escapes strikethrough markdown in a string. - * @param {string} text Content to escape - * @returns {string} - */ - static escapeStrikethrough(text) { - return text.replaceAll('~~', '\\~\\~'); - } - - /** - * Escapes spoiler markdown in a string. - * @param {string} text Content to escape - * @returns {string} - */ - static escapeSpoiler(text) { - return text.replaceAll('||', '\\|\\|'); - } - - /** - * @typedef {Object} FetchRecommendedShardsOptions - * @property {number} [guildsPerShard=1000] Number of guilds assigned per shard - * @property {number} [multipleOf=1] The multiple the shard count should round up to. (16 for large bot sharding) - */ - - /** - * Gets the recommended shard count from Discord. - * @param {string} token Discord auth token - * @param {FetchRecommendedShardsOptions} [options] Options for fetching the recommended shard count - * @returns {Promise} The recommended number of shards - */ - static async fetchRecommendedShards(token, { guildsPerShard = 1_000, multipleOf = 1 } = {}) { - if (!token) throw new DiscordError('TOKEN_MISSING'); - const response = await fetch(RouteBases.api + Routes.gatewayBot(), { - method: 'GET', - headers: { Authorization: `Bot ${token.replace(/^Bot\s*/i, '')}` }, - }); - if (!response.ok) { - if (response.status === 401) throw new DiscordError('TOKEN_INVALID'); - throw response; - } - const { shards } = await response.json(); - return Math.ceil((shards * (1_000 / guildsPerShard)) / multipleOf) * multipleOf; - } - - /** - * Parses emoji info out of a string. The string must be one of: - * * A UTF-8 emoji (no id) - * * A URL-encoded UTF-8 emoji (no id) - * * A Discord custom emoji (`<:name:id>` or ``) - * @param {string} text Emoji string to parse - * @returns {APIEmoji} Object with `animated`, `name`, and `id` properties - * @private - */ - static parseEmoji(text) { - if (text.includes('%')) text = decodeURIComponent(text); - if (!text.includes(':')) return { animated: false, name: text, id: undefined }; - const match = text.match(/?/); - return match && { animated: Boolean(match[1]), name: match[2], id: match[3] }; - } - - /** - * Resolves a partial emoji object from an {@link EmojiIdentifierResolvable}, without checking a Client. - * @param {EmojiIdentifierResolvable} emoji Emoji identifier to resolve - * @returns {?RawEmoji} - * @private - */ - static resolvePartialEmoji(emoji) { - if (!emoji) return null; - if (typeof emoji === 'string') return /^\d{17,19}$/.test(emoji) ? { id: emoji } : Util.parseEmoji(emoji); - const { id, name, animated } = emoji; - if (!id && !name) return null; - return { id, name, animated: Boolean(animated) }; - } - - /** - * Shallow-copies an object with its class/prototype intact. - * @param {Object} obj Object to clone - * @returns {Object} - * @private - */ - static cloneObject(obj) { - return Object.assign(Object.create(obj), obj); - } - - /** - * Sets default properties on an object that aren't already specified. - * @param {Object} def Default properties - * @param {Object} given Object to assign defaults to - * @returns {Object} - * @private - */ - static mergeDefault(def, given) { - if (!given) return def; - for (const key in def) { - if (!Object.hasOwn(given, key) || given[key] === undefined) { - given[key] = def[key]; - } else if (given[key] === Object(given[key])) { - given[key] = Util.mergeDefault(def[key], given[key]); - } - } - - return given; - } - - /** - * Options used to make an error object. - * @typedef {Object} MakeErrorOptions - * @property {string} name Error type - * @property {string} message Message for the error - * @property {string} stack Stack for the error - */ - - /** - * Makes an Error from a plain info object. - * @param {MakeErrorOptions} obj Error info - * @returns {Error} - * @private - */ - static makeError(obj) { - const err = new Error(obj.message); - err.name = obj.name; - err.stack = obj.stack; - return err; - } - - /** - * Makes a plain error info object from an Error. - * @param {Error} err Error to get info from - * @returns {MakeErrorOptions} - * @private - */ - static makePlainError(err) { - return { - name: err.name, - message: err.message, - stack: err.stack, - }; - } - - /** - * Moves an element in an array *in place*. - * @param {Array<*>} array Array to modify - * @param {*} element Element to move - * @param {number} newIndex Index or offset to move the element to - * @param {boolean} [offset=false] Move the element by an offset amount rather than to a set index - * @returns {number} - * @private - */ - static moveElementInArray(array, element, newIndex, offset = false) { - const index = array.indexOf(element); - newIndex = (offset ? index : 0) + newIndex; - if (newIndex > -1 && newIndex < array.length) { - const removedElement = array.splice(index, 1)[0]; - array.splice(newIndex, 0, removedElement); - } - return array.indexOf(element); - } - - /** - * Verifies the provided data is a string, otherwise throws provided error. - * @param {string} data The string resolvable to resolve - * @param {Function} [error] The Error constructor to instantiate. Defaults to Error - * @param {string} [errorMessage] The error message to throw with. Defaults to "Expected string, got instead." - * @param {boolean} [allowEmpty=true] Whether an empty string should be allowed - * @returns {string} - */ - static verifyString( - data, - error = Error, - errorMessage = `Expected a string, got ${data} instead.`, - allowEmpty = true, - ) { - if (typeof data !== 'string') throw new error(errorMessage); - if (!allowEmpty && data.length === 0) throw new error(errorMessage); - return data; - } - - /** - * Can be a number, hex string, an RGB array like: - * ```js - * [255, 0, 255] // purple - * ``` - * or one of the following strings: - * - `Default` - * - `White` - * - `Aqua` - * - `Green` - * - `Blue` - * - `Yellow` - * - `Purple` - * - `LuminousVividPink` - * - `Fuchsia` - * - `Gold` - * - `Orange` - * - `Red` - * - `Grey` - * - `Navy` - * - `DarkAqua` - * - `DarkGreen` - * - `DarkBlue` - * - `DarkPurple` - * - `DarkVividPink` - * - `DarkGold` - * - `DarkOrange` - * - `DarkRed` - * - `DarkGrey` - * - `DarkerGrey` - * - `LightGrey` - * - `DarkNavy` - * - `Blurple` - * - `Greyple` - * - `DarkButNotBlack` - * - `NotQuiteBlack` - * - `Random` - * @typedef {string|number|number[]} ColorResolvable - */ - - /** - * Resolves a ColorResolvable into a color number. - * @param {ColorResolvable} color Color to resolve - * @returns {number} A color - */ - static resolveColor(color) { - if (typeof color === 'string') { - if (color === 'Random') return Math.floor(Math.random() * (0xffffff + 1)); - if (color === 'Default') return 0; - color = Colors[color] ?? parseInt(color.replace('#', ''), 16); - } else if (Array.isArray(color)) { - color = (color[0] << 16) + (color[1] << 8) + color[2]; - } - - if (color < 0 || color > 0xffffff) throw new RangeError('COLOR_RANGE'); - else if (Number.isNaN(color)) throw new TypeError('COLOR_CONVERT'); - - return color; - } - - /** - * Sorts by Discord's position and id. - * @param {Collection} collection Collection of objects to sort - * @returns {Collection} - */ - static discordSort(collection) { - const isGuildChannel = collection.first() instanceof GuildChannel; - return collection.sorted( - isGuildChannel - ? (a, b) => a.rawPosition - b.rawPosition || Number(BigInt(a.id) - BigInt(b.id)) - : (a, b) => a.rawPosition - b.rawPosition || Number(BigInt(b.id) - BigInt(a.id)), - ); - } - - /** - * Sets the position of a Channel or Role. - * @param {Channel|Role} item Object to set the position of - * @param {number} position New position for the object - * @param {boolean} relative Whether `position` is relative to its current position - * @param {Collection} sorted A collection of the objects sorted properly - * @param {Client} client The client to use to patch the data - * @param {string} route Route to call PATCH on - * @param {string} [reason] Reason for the change - * @returns {Promise} Updated item list, with `id` and `position` properties - * @private - */ - static async setPosition(item, position, relative, sorted, client, route, reason) { - let updatedItems = [...sorted.values()]; - Util.moveElementInArray(updatedItems, item, position, relative); - updatedItems = updatedItems.map((r, i) => ({ id: r.id, position: i })); - await client.rest.patch(route, { body: updatedItems, reason }); - return updatedItems; - } - - /** - * Alternative to Node's `path.basename`, removing query string after the extension if it exists. - * @param {string} path Path to get the basename of - * @param {string} [ext] File extension to remove - * @returns {string} Basename of the path - * @private - */ - static basename(path, ext) { - const res = parse(path); - return ext && res.ext.startsWith(ext) ? res.name : res.base.split('?')[0]; - } - /** - * The content to have all mentions replaced by the equivalent text. - * @param {string} str The string to be converted - * @param {TextBasedChannels} channel The channel the string was sent in - * @returns {string} - */ - static cleanContent(str, channel) { - str = str - .replace(/<@!?[0-9]+>/g, input => { - const id = input.replace(/<|!|>|@/g, ''); - if (channel.type === ChannelType.DM) { - const user = channel.client.users.cache.get(id); - return user ? `@${user.username}` : input; - } - - const member = channel.guild.members.cache.get(id); - if (member) { - return `@${member.displayName}`; - } else { - const user = channel.client.users.cache.get(id); - return user ? `@${user.username}` : input; - } - }) - .replace(/<#[0-9]+>/g, input => { - const mentionedChannel = channel.client.channels.cache.get(input.replace(/<|#|>/g, '')); - return mentionedChannel ? `#${mentionedChannel.name}` : input; - }) - .replace(/<@&[0-9]+>/g, input => { - if (channel.type === ChannelType.DM) return input; - const role = channel.guild.roles.cache.get(input.replace(/<|@|>|&/g, '')); - return role ? `@${role.name}` : input; - }); - return str; - } - - /** - * The content to put in a code block with all code block fences replaced by the equivalent backticks. - * @param {string} text The string to be converted - * @returns {string} - */ - static cleanCodeBlockContent(text) { - return text.replaceAll('```', '`\u200b``'); - } - - /** - * Lazily evaluates a callback function - * @param {Function} cb The callback to lazily evaluate - * @returns {Function} - */ - static lazy(cb) { - let defaultValue; - return () => (defaultValue ??= cb()); - } + return out; } -module.exports = Util; +/** + * Options used to escape markdown. + * @typedef {Object} EscapeMarkdownOptions + * @property {boolean} [codeBlock=true] Whether to escape code blocks or not + * @property {boolean} [inlineCode=true] Whether to escape inline code or not + * @property {boolean} [bold=true] Whether to escape bolds or not + * @property {boolean} [italic=true] Whether to escape italics or not + * @property {boolean} [underline=true] Whether to escape underlines or not + * @property {boolean} [strikethrough=true] Whether to escape strikethroughs or not + * @property {boolean} [spoiler=true] Whether to escape spoilers or not + * @property {boolean} [codeBlockContent=true] Whether to escape text inside code blocks or not + * @property {boolean} [inlineCodeContent=true] Whether to escape text inside inline code or not + */ + +/** + * Escapes any Discord-flavour markdown in a string. + * @param {string} text Content to escape + * @param {EscapeMarkdownOptions} [options={}] Options for escaping the markdown + * @returns {string} + */ +function escapeMarkdown( + text, + { + codeBlock = true, + inlineCode = true, + bold = true, + italic = true, + underline = true, + strikethrough = true, + spoiler = true, + codeBlockContent = true, + inlineCodeContent = true, + } = {}, +) { + if (!codeBlockContent) { + return text + .split('```') + .map((subString, index, array) => { + if (index % 2 && index !== array.length - 1) return subString; + return escapeMarkdown(subString, { + inlineCode, + bold, + italic, + underline, + strikethrough, + spoiler, + inlineCodeContent, + }); + }) + .join(codeBlock ? '\\`\\`\\`' : '```'); + } + if (!inlineCodeContent) { + return text + .split(/(?<=^|[^`])`(?=[^`]|$)/g) + .map((subString, index, array) => { + if (index % 2 && index !== array.length - 1) return subString; + return escapeMarkdown(subString, { + codeBlock, + bold, + italic, + underline, + strikethrough, + spoiler, + }); + }) + .join(inlineCode ? '\\`' : '`'); + } + if (inlineCode) text = escapeInlineCode(text); + if (codeBlock) text = escapeCodeBlock(text); + if (italic) text = escapeItalic(text); + if (bold) text = escapeBold(text); + if (underline) text = escapeUnderline(text); + if (strikethrough) text = escapeStrikethrough(text); + if (spoiler) text = escapeSpoiler(text); + return text; +} + +/** + * Escapes code block markdown in a string. + * @param {string} text Content to escape + * @returns {string} + */ +function escapeCodeBlock(text) { + return text.replaceAll('```', '\\`\\`\\`'); +} + +/** + * Escapes inline code markdown in a string. + * @param {string} text Content to escape + * @returns {string} + */ +function escapeInlineCode(text) { + return text.replace(/(?<=^|[^`])``?(?=[^`]|$)/g, match => (match.length === 2 ? '\\`\\`' : '\\`')); +} + +/** + * Escapes italic markdown in a string. + * @param {string} text Content to escape + * @returns {string} + */ +function escapeItalic(text) { + let i = 0; + text = text.replace(/(?<=^|[^*])\*([^*]|\*\*|$)/g, (_, match) => { + if (match === '**') return ++i % 2 ? `\\*${match}` : `${match}\\*`; + return `\\*${match}`; + }); + i = 0; + return text.replace(/(?<=^|[^_])_([^_]|__|$)/g, (_, match) => { + if (match === '__') return ++i % 2 ? `\\_${match}` : `${match}\\_`; + return `\\_${match}`; + }); +} + +/** + * Escapes bold markdown in a string. + * @param {string} text Content to escape + * @returns {string} + */ +function escapeBold(text) { + let i = 0; + return text.replace(/\*\*(\*)?/g, (_, match) => { + if (match) return ++i % 2 ? `${match}\\*\\*` : `\\*\\*${match}`; + return '\\*\\*'; + }); +} + +/** + * Escapes underline markdown in a string. + * @param {string} text Content to escape + * @returns {string} + */ +function escapeUnderline(text) { + let i = 0; + return text.replace(/__(_)?/g, (_, match) => { + if (match) return ++i % 2 ? `${match}\\_\\_` : `\\_\\_${match}`; + return '\\_\\_'; + }); +} + +/** + * Escapes strikethrough markdown in a string. + * @param {string} text Content to escape + * @returns {string} + */ +function escapeStrikethrough(text) { + return text.replaceAll('~~', '\\~\\~'); +} + +/** + * Escapes spoiler markdown in a string. + * @param {string} text Content to escape + * @returns {string} + */ +function escapeSpoiler(text) { + return text.replaceAll('||', '\\|\\|'); +} + +/** + * @typedef {Object} FetchRecommendedShardsOptions + * @property {number} [guildsPerShard=1000] Number of guilds assigned per shard + * @property {number} [multipleOf=1] The multiple the shard count should round up to. (16 for large bot sharding) + */ + +/** + * Gets the recommended shard count from Discord. + * @param {string} token Discord auth token + * @param {FetchRecommendedShardsOptions} [options] Options for fetching the recommended shard count + * @returns {Promise} The recommended number of shards + */ +async function fetchRecommendedShards(token, { guildsPerShard = 1_000, multipleOf = 1 } = {}) { + if (!token) throw new DiscordError('TOKEN_MISSING'); + const response = await fetch(RouteBases.api + Routes.gatewayBot(), { + method: 'GET', + headers: { Authorization: `Bot ${token.replace(/^Bot\s*/i, '')}` }, + }); + if (!response.ok) { + if (response.status === 401) throw new DiscordError('TOKEN_INVALID'); + throw response; + } + const { shards } = await response.json(); + return Math.ceil((shards * (1_000 / guildsPerShard)) / multipleOf) * multipleOf; +} + +/** + * Parses emoji info out of a string. The string must be one of: + * * A UTF-8 emoji (no id) + * * A URL-encoded UTF-8 emoji (no id) + * * A Discord custom emoji (`<:name:id>` or ``) + * @param {string} text Emoji string to parse + * @returns {APIEmoji} Object with `animated`, `name`, and `id` properties + * @private + */ +function parseEmoji(text) { + if (text.includes('%')) text = decodeURIComponent(text); + if (!text.includes(':')) return { animated: false, name: text, id: undefined }; + const match = text.match(/?/); + return match && { animated: Boolean(match[1]), name: match[2], id: match[3] }; +} + +/** + * Resolves a partial emoji object from an {@link EmojiIdentifierResolvable}, without checking a Client. + * @param {EmojiIdentifierResolvable} emoji Emoji identifier to resolve + * @returns {?RawEmoji} + * @private + */ +function resolvePartialEmoji(emoji) { + if (!emoji) return null; + if (typeof emoji === 'string') return /^\d{17,19}$/.test(emoji) ? { id: emoji } : parseEmoji(emoji); + const { id, name, animated } = emoji; + if (!id && !name) return null; + return { id, name, animated: Boolean(animated) }; +} + +/** + * Shallow-copies an object with its class/prototype intact. + * @param {Object} obj Object to clone + * @returns {Object} + * @private + */ +function cloneObject(obj) { + return Object.assign(Object.create(obj), obj); +} + +/** + * Sets default properties on an object that aren't already specified. + * @param {Object} def Default properties + * @param {Object} given Object to assign defaults to + * @returns {Object} + * @private + */ +function mergeDefault(def, given) { + if (!given) return def; + for (const key in def) { + if (!Object.hasOwn(given, key) || given[key] === undefined) { + given[key] = def[key]; + } else if (given[key] === Object(given[key])) { + given[key] = mergeDefault(def[key], given[key]); + } + } + + return given; +} + +/** + * Options used to make an error object. + * @typedef {Object} MakeErrorOptions + * @property {string} name Error type + * @property {string} message Message for the error + * @property {string} stack Stack for the error + */ + +/** + * Makes an Error from a plain info object. + * @param {MakeErrorOptions} obj Error info + * @returns {Error} + * @private + */ +function makeError(obj) { + const err = new Error(obj.message); + err.name = obj.name; + err.stack = obj.stack; + return err; +} + +/** + * Makes a plain error info object from an Error. + * @param {Error} err Error to get info from + * @returns {MakeErrorOptions} + * @private + */ +function makePlainError(err) { + return { + name: err.name, + message: err.message, + stack: err.stack, + }; +} + +/** + * Moves an element in an array *in place*. + * @param {Array<*>} array Array to modify + * @param {*} element Element to move + * @param {number} newIndex Index or offset to move the element to + * @param {boolean} [offset=false] Move the element by an offset amount rather than to a set index + * @returns {number} + * @private + */ +function moveElementInArray(array, element, newIndex, offset = false) { + const index = array.indexOf(element); + newIndex = (offset ? index : 0) + newIndex; + if (newIndex > -1 && newIndex < array.length) { + const removedElement = array.splice(index, 1)[0]; + array.splice(newIndex, 0, removedElement); + } + return array.indexOf(element); +} + +/** + * Verifies the provided data is a string, otherwise throws provided error. + * @param {string} data The string resolvable to resolve + * @param {Function} [error] The Error constructor to instantiate. Defaults to Error + * @param {string} [errorMessage] The error message to throw with. Defaults to "Expected string, got instead." + * @param {boolean} [allowEmpty=true] Whether an empty string should be allowed + * @returns {string} + */ +function verifyString( + data, + error = Error, + errorMessage = `Expected a string, got ${data} instead.`, + allowEmpty = true, +) { + if (typeof data !== 'string') throw new error(errorMessage); + if (!allowEmpty && data.length === 0) throw new error(errorMessage); + return data; +} + +/** + * Can be a number, hex string, an RGB array like: + * ```js + * [255, 0, 255] // purple + * ``` + * or one of the following strings: + * - `Default` + * - `White` + * - `Aqua` + * - `Green` + * - `Blue` + * - `Yellow` + * - `Purple` + * - `LuminousVividPink` + * - `Fuchsia` + * - `Gold` + * - `Orange` + * - `Red` + * - `Grey` + * - `Navy` + * - `DarkAqua` + * - `DarkGreen` + * - `DarkBlue` + * - `DarkPurple` + * - `DarkVividPink` + * - `DarkGold` + * - `DarkOrange` + * - `DarkRed` + * - `DarkGrey` + * - `DarkerGrey` + * - `LightGrey` + * - `DarkNavy` + * - `Blurple` + * - `Greyple` + * - `DarkButNotBlack` + * - `NotQuiteBlack` + * - `Random` + * @typedef {string|number|number[]} ColorResolvable + */ + +/** + * Resolves a ColorResolvable into a color number. + * @param {ColorResolvable} color Color to resolve + * @returns {number} A color + */ +function resolveColor(color) { + if (typeof color === 'string') { + if (color === 'Random') return Math.floor(Math.random() * (0xffffff + 1)); + if (color === 'Default') return 0; + color = Colors[color] ?? parseInt(color.replace('#', ''), 16); + } else if (Array.isArray(color)) { + color = (color[0] << 16) + (color[1] << 8) + color[2]; + } + + if (color < 0 || color > 0xffffff) throw new RangeError('COLOR_RANGE'); + else if (Number.isNaN(color)) throw new TypeError('COLOR_CONVERT'); + + return color; +} + +/** + * Sorts by Discord's position and id. + * @param {Collection} collection Collection of objects to sort + * @returns {Collection} + */ +function discordSort(collection) { + const isGuildChannel = collection.first() instanceof GuildChannel; + return collection.sorted( + isGuildChannel + ? (a, b) => a.rawPosition - b.rawPosition || Number(BigInt(a.id) - BigInt(b.id)) + : (a, b) => a.rawPosition - b.rawPosition || Number(BigInt(b.id) - BigInt(a.id)), + ); +} + +/** + * Sets the position of a Channel or Role. + * @param {Channel|Role} item Object to set the position of + * @param {number} position New position for the object + * @param {boolean} relative Whether `position` is relative to its current position + * @param {Collection} sorted A collection of the objects sorted properly + * @param {Client} client The client to use to patch the data + * @param {string} route Route to call PATCH on + * @param {string} [reason] Reason for the change + * @returns {Promise} Updated item list, with `id` and `position` properties + * @private + */ +async function setPosition(item, position, relative, sorted, client, route, reason) { + let updatedItems = [...sorted.values()]; + moveElementInArray(updatedItems, item, position, relative); + updatedItems = updatedItems.map((r, i) => ({ id: r.id, position: i })); + await client.rest.patch(route, { body: updatedItems, reason }); + return updatedItems; +} + +/** + * Alternative to Node's `path.basename`, removing query string after the extension if it exists. + * @param {string} path Path to get the basename of + * @param {string} [ext] File extension to remove + * @returns {string} Basename of the path + * @private + */ +function basename(path, ext) { + const res = parse(path); + return ext && res.ext.startsWith(ext) ? res.name : res.base.split('?')[0]; +} +/** + * The content to have all mentions replaced by the equivalent text. + * @param {string} str The string to be converted + * @param {TextBasedChannels} channel The channel the string was sent in + * @returns {string} + */ +function cleanContent(str, channel) { + str = str + .replace(/<@!?[0-9]+>/g, input => { + const id = input.replace(/<|!|>|@/g, ''); + if (channel.type === ChannelType.DM) { + const user = channel.client.users.cache.get(id); + return user ? `@${user.username}` : input; + } + + const member = channel.guild.members.cache.get(id); + if (member) { + return `@${member.displayName}`; + } else { + const user = channel.client.users.cache.get(id); + return user ? `@${user.username}` : input; + } + }) + .replace(/<#[0-9]+>/g, input => { + const mentionedChannel = channel.client.channels.cache.get(input.replace(/<|#|>/g, '')); + return mentionedChannel ? `#${mentionedChannel.name}` : input; + }) + .replace(/<@&[0-9]+>/g, input => { + if (channel.type === ChannelType.DM) return input; + const role = channel.guild.roles.cache.get(input.replace(/<|@|>|&/g, '')); + return role ? `@${role.name}` : input; + }); + return str; +} + +/** + * The content to put in a code block with all code block fences replaced by the equivalent backticks. + * @param {string} text The string to be converted + * @returns {string} + */ +function cleanCodeBlockContent(text) { + return text.replaceAll('```', '`\u200b``'); +} + +/** + * Lazily evaluates a callback function + * @param {Function} cb The callback to lazily evaluate + * @returns {Function} + */ +function lazy(cb) { + let defaultValue; + return () => (defaultValue ??= cb()); +} + +module.exports = { + flatten, + escapeMarkdown, + escapeCodeBlock, + escapeInlineCode, + escapeItalic, + escapeBold, + escapeUnderline, + escapeStrikethrough, + escapeSpoiler, + fetchRecommendedShards, + parseEmoji, + resolvePartialEmoji, + cloneObject, + mergeDefault, + makeError, + makePlainError, + moveElementInArray, + verifyString, + resolveColor, + discordSort, + setPosition, + basename, + cleanContent, + cleanCodeBlockContent, + lazy, +}; // Fixes Circular const GuildChannel = require('../structures/GuildChannel'); diff --git a/packages/discord.js/typings/index.d.ts b/packages/discord.js/typings/index.d.ts index b6d1c1097..393c28110 100644 --- a/packages/discord.js/typings/index.d.ts +++ b/packages/discord.js/typings/index.d.ts @@ -119,6 +119,7 @@ import { LocaleString, MessageActivityType, APIAttachment, + APIChannel, } from 'discord-api-types/v10'; import { ChildProcess } from 'node:child_process'; import { EventEmitter } from 'node:events'; @@ -2613,43 +2614,40 @@ export class UserFlagsBitField extends BitField { public static resolve(bit?: BitFieldResolvable): number; } -export class Util extends null { - private constructor(); - public static basename(path: string, ext?: string): string; - public static cleanContent(str: string, channel: TextBasedChannel): string; - public static cloneObject(obj: unknown): unknown; - public static discordSort( - collection: Collection, - ): Collection; - public static escapeMarkdown(text: string, options?: EscapeMarkdownOptions): string; - public static escapeCodeBlock(text: string): string; - public static escapeInlineCode(text: string): string; - public static escapeBold(text: string): string; - public static escapeItalic(text: string): string; - public static escapeUnderline(text: string): string; - public static escapeStrikethrough(text: string): string; - public static escapeSpoiler(text: string): string; - public static cleanCodeBlockContent(text: string): string; - public static fetchRecommendedShards(token: string, options?: FetchRecommendedShardsOptions): Promise; - public static flatten(obj: unknown, ...props: Record[]): unknown; - public static makeError(obj: MakeErrorOptions): Error; - public static makePlainError(err: Error): MakeErrorOptions; - public static mergeDefault(def: unknown, given: unknown): unknown; - public static moveElementInArray(array: unknown[], element: unknown, newIndex: number, offset?: boolean): number; - public static parseEmoji(text: string): { animated: boolean; name: string; id: Snowflake | null } | null; - public static resolveColor(color: ColorResolvable): number; - public static resolvePartialEmoji(emoji: EmojiIdentifierResolvable): Partial | null; - public static verifyString(data: string, error?: typeof Error, errorMessage?: string, allowEmpty?: boolean): string; - public static setPosition( - item: T, - position: number, - relative: boolean, - sorted: Collection, - client: Client, - route: string, - reason?: string, - ): Promise<{ id: Snowflake; position: number }[]>; -} +export function basename(path: string, ext?: string): string; +export function cleanContent(str: string, channel: TextBasedChannel): string; +export function cloneObject(obj: unknown): unknown; +export function discordSort( + collection: Collection, +): Collection; +export function escapeMarkdown(text: string, options?: EscapeMarkdownOptions): string; +export function escapeCodeBlock(text: string): string; +export function escapeInlineCode(text: string): string; +export function escapeBold(text: string): string; +export function escapeItalic(text: string): string; +export function escapeUnderline(text: string): string; +export function escapeStrikethrough(text: string): string; +export function escapeSpoiler(text: string): string; +export function cleanCodeBlockContent(text: string): string; +export function fetchRecommendedShards(token: string, options?: FetchRecommendedShardsOptions): Promise; +export function flatten(obj: unknown, ...props: Record[]): unknown; +export function makeError(obj: MakeErrorOptions): Error; +export function makePlainError(err: Error): MakeErrorOptions; +export function mergeDefault(def: unknown, given: unknown): unknown; +export function moveElementInArray(array: unknown[], element: unknown, newIndex: number, offset?: boolean): number; +export function parseEmoji(text: string): { animated: boolean; name: string; id: Snowflake | null } | null; +export function resolveColor(color: ColorResolvable): number; +export function resolvePartialEmoji(emoji: EmojiIdentifierResolvable): Partial | null; +export function verifyString(data: string, error?: typeof Error, errorMessage?: string, allowEmpty?: boolean): string; +export function setPosition( + item: T, + position: number, + relative: boolean, + sorted: Collection, + client: Client, + route: string, + reason?: string, +): Promise<{ id: Snowflake; position: number }[]>; export interface MappedComponentBuilderTypes { [ComponentType.Button]: ButtonBuilder; @@ -2665,19 +2663,24 @@ export interface MappedComponentTypes { [ComponentType.TextInput]: TextInputComponent; } -export class Components extends null { - public static createComponent( - data: APIMessageComponent & { type: T }, - ): MappedComponentTypes[T]; - public static createComponent(data: C): C; - public static createComponent(data: APIMessageComponent | Component): Component; - public static createComponentBuilder( - data: APIMessageComponent & { type: T }, - ): MappedComponentBuilderTypes[T]; - public static createComponentBuilder(data: C): C; - public static createComponentBuilder(data: APIMessageComponent | ComponentBuilder): ComponentBuilder; +export interface CreateChannelOptions { + allowFromUnknownGuild?: boolean; + fromInteraction?: boolean; } +export function createChannel(client: Client, data: APIChannel, options?: CreateChannelOptions): AnyChannel; + +export function createComponent( + data: APIMessageComponent & { type: T }, +): MappedComponentTypes[T]; +export function createComponent(data: C): C; +export function createComponent(data: APIMessageComponent | Component): Component; +export function createComponentBuilder( + data: APIMessageComponent & { type: T }, +): MappedComponentBuilderTypes[T]; +export function createComponentBuilder(data: C): C; +export function createComponentBuilder(data: APIMessageComponent | ComponentBuilder): ComponentBuilder; + export class Formatters extends null { public static blockQuote: typeof blockQuote; public static bold: typeof bold;