Initial commit
This commit is contained in:
25
src/util/ActivityFlagsBitField.js
Normal file
25
src/util/ActivityFlagsBitField.js
Normal file
@@ -0,0 +1,25 @@
|
||||
'use strict';
|
||||
|
||||
const { ActivityFlags } = require('discord-api-types/v9');
|
||||
const BitField = require('./BitField');
|
||||
|
||||
/**
|
||||
* Data structure that makes it easy to interact with an {@link Activity#flags} bitfield.
|
||||
* @extends {BitField}
|
||||
*/
|
||||
class ActivityFlagsBitField extends BitField {}
|
||||
|
||||
/**
|
||||
* @name ActivityFlagsBitField
|
||||
* @kind constructor
|
||||
* @memberof ActivityFlagsBitField
|
||||
* @param {BitFieldResolvable} [bits=0] Bit(s) to read from
|
||||
*/
|
||||
|
||||
/**
|
||||
* Numeric activity flags.
|
||||
* @type {ActivityFlags}
|
||||
*/
|
||||
ActivityFlagsBitField.Flags = ActivityFlags;
|
||||
|
||||
module.exports = ActivityFlagsBitField;
|
31
src/util/ApplicationFlagsBitField.js
Normal file
31
src/util/ApplicationFlagsBitField.js
Normal file
@@ -0,0 +1,31 @@
|
||||
'use strict';
|
||||
|
||||
const { ApplicationFlags } = require('discord-api-types/v9');
|
||||
const BitField = require('./BitField');
|
||||
|
||||
/**
|
||||
* Data structure that makes it easy to interact with a {@link ClientApplication#flags} bitfield.
|
||||
* @extends {BitField}
|
||||
*/
|
||||
class ApplicationFlagsBitField extends BitField {}
|
||||
|
||||
/**
|
||||
* @name ApplicationFlagsBitField
|
||||
* @kind constructor
|
||||
* @memberof ApplicationFlagsBitField
|
||||
* @param {BitFieldResolvable} [bits=0] Bit(s) to read from
|
||||
*/
|
||||
|
||||
/**
|
||||
* Bitfield of the packed bits
|
||||
* @type {number}
|
||||
* @name ApplicationFlagsBitField#bitfield
|
||||
*/
|
||||
|
||||
/**
|
||||
* Numeric application flags. All available properties:
|
||||
* @type {ApplicationFlags}
|
||||
*/
|
||||
ApplicationFlagsBitField.Flags = ApplicationFlags;
|
||||
|
||||
module.exports = ApplicationFlagsBitField;
|
170
src/util/BitField.js
Normal file
170
src/util/BitField.js
Normal file
@@ -0,0 +1,170 @@
|
||||
'use strict';
|
||||
|
||||
const { RangeError } = require('../errors');
|
||||
|
||||
/**
|
||||
* Data structure that makes it easy to interact with a bitfield.
|
||||
*/
|
||||
class BitField {
|
||||
/**
|
||||
* @param {BitFieldResolvable} [bits=this.constructor.defaultBit] Bit(s) to read from
|
||||
*/
|
||||
constructor(bits = this.constructor.defaultBit) {
|
||||
/**
|
||||
* Bitfield of the packed bits
|
||||
* @type {number|bigint}
|
||||
*/
|
||||
this.bitfield = this.constructor.resolve(bits);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the bitfield has a bit, or any of multiple bits.
|
||||
* @param {BitFieldResolvable} bit Bit(s) to check for
|
||||
* @returns {boolean}
|
||||
*/
|
||||
any(bit) {
|
||||
return (this.bitfield & this.constructor.resolve(bit)) !== this.constructor.defaultBit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if this bitfield equals another
|
||||
* @param {BitFieldResolvable} bit Bit(s) to check for
|
||||
* @returns {boolean}
|
||||
*/
|
||||
equals(bit) {
|
||||
return this.bitfield === this.constructor.resolve(bit);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the bitfield has a bit, or multiple bits.
|
||||
* @param {BitFieldResolvable} bit Bit(s) to check for
|
||||
* @returns {boolean}
|
||||
*/
|
||||
has(bit) {
|
||||
bit = this.constructor.resolve(bit);
|
||||
return (this.bitfield & bit) === bit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all given bits that are missing from the bitfield.
|
||||
* @param {BitFieldResolvable} bits Bit(s) to check for
|
||||
* @param {...*} hasParams Additional parameters for the has method, if any
|
||||
* @returns {string[]}
|
||||
*/
|
||||
missing(bits, ...hasParams) {
|
||||
return new this.constructor(bits).remove(this).toArray(...hasParams);
|
||||
}
|
||||
|
||||
/**
|
||||
* Freezes these bits, making them immutable.
|
||||
* @returns {Readonly<BitField>}
|
||||
*/
|
||||
freeze() {
|
||||
return Object.freeze(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds bits to these ones.
|
||||
* @param {...BitFieldResolvable} [bits] Bits to add
|
||||
* @returns {BitField} These bits or new BitField if the instance is frozen.
|
||||
*/
|
||||
add(...bits) {
|
||||
let total = this.constructor.defaultBit;
|
||||
for (const bit of bits) {
|
||||
total |= this.constructor.resolve(bit);
|
||||
}
|
||||
if (Object.isFrozen(this)) return new this.constructor(this.bitfield | total);
|
||||
this.bitfield |= total;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes bits from these.
|
||||
* @param {...BitFieldResolvable} [bits] Bits to remove
|
||||
* @returns {BitField} These bits or new BitField if the instance is frozen.
|
||||
*/
|
||||
remove(...bits) {
|
||||
let total = this.constructor.defaultBit;
|
||||
for (const bit of bits) {
|
||||
total |= this.constructor.resolve(bit);
|
||||
}
|
||||
if (Object.isFrozen(this)) return new this.constructor(this.bitfield & ~total);
|
||||
this.bitfield &= ~total;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets an object mapping field names to a {@link boolean} indicating whether the
|
||||
* bit is available.
|
||||
* @param {...*} hasParams Additional parameters for the has method, if any
|
||||
* @returns {Object}
|
||||
*/
|
||||
serialize(...hasParams) {
|
||||
const serialized = {};
|
||||
for (const [flag, bit] of Object.entries(this.constructor.Flags)) serialized[flag] = this.has(bit, ...hasParams);
|
||||
return serialized;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets an {@link Array} of bitfield names based on the bits available.
|
||||
* @param {...*} hasParams Additional parameters for the has method, if any
|
||||
* @returns {string[]}
|
||||
*/
|
||||
toArray(...hasParams) {
|
||||
return Object.keys(this.constructor.Flags).filter(bit => this.has(bit, ...hasParams));
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
return typeof this.bitfield === 'number' ? this.bitfield : this.bitfield.toString();
|
||||
}
|
||||
|
||||
valueOf() {
|
||||
return this.bitfield;
|
||||
}
|
||||
|
||||
*[Symbol.iterator]() {
|
||||
yield* this.toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Data that can be resolved to give a bitfield. This can be:
|
||||
* * A bit number (this can be a number literal or a value taken from {@link BitField.Flags})
|
||||
* * A string bit number
|
||||
* * An instance of BitField
|
||||
* * An Array of BitFieldResolvable
|
||||
* @typedef {number|string|bigint|BitField|BitFieldResolvable[]} BitFieldResolvable
|
||||
*/
|
||||
|
||||
/**
|
||||
* Resolves bitfields to their numeric form.
|
||||
* @param {BitFieldResolvable} [bit] bit(s) to resolve
|
||||
* @returns {number|bigint}
|
||||
*/
|
||||
static resolve(bit) {
|
||||
const { defaultBit } = this;
|
||||
if (typeof defaultBit === typeof bit && bit >= defaultBit) return bit;
|
||||
if (bit instanceof BitField) return bit.bitfield;
|
||||
if (Array.isArray(bit)) return bit.map(p => this.resolve(p)).reduce((prev, p) => prev | p, defaultBit);
|
||||
if (typeof bit === 'string') {
|
||||
if (typeof this.Flags[bit] !== 'undefined') return this.Flags[bit];
|
||||
if (!isNaN(bit)) return typeof defaultBit === 'bigint' ? BigInt(bit) : Number(bit);
|
||||
}
|
||||
throw new RangeError('BITFIELD_INVALID', bit);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Numeric bitfield flags.
|
||||
* <info>Defined in extension classes</info>
|
||||
* @type {Object}
|
||||
* @abstract
|
||||
*/
|
||||
BitField.Flags = {};
|
||||
|
||||
/**
|
||||
* @type {number|bigint}
|
||||
* @private
|
||||
*/
|
||||
BitField.defaultBit = 0;
|
||||
|
||||
module.exports = BitField;
|
34
src/util/Colors.js
Normal file
34
src/util/Colors.js
Normal file
@@ -0,0 +1,34 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
Default: 0x000000,
|
||||
White: 0xffffff,
|
||||
Aqua: 0x1abc9c,
|
||||
Green: 0x57f287,
|
||||
Blue: 0x3498db,
|
||||
Yellow: 0xfee75c,
|
||||
Purple: 0x9b59b6,
|
||||
LuminousVividPink: 0xe91e63,
|
||||
Fuchsia: 0xeb459e,
|
||||
Gold: 0xf1c40f,
|
||||
Orange: 0xe67e22,
|
||||
Red: 0xed4245,
|
||||
Grey: 0x95a5a6,
|
||||
Navy: 0x34495e,
|
||||
DarkAqua: 0x11806a,
|
||||
DarkGreen: 0x1f8b4c,
|
||||
DarkBlue: 0x206694,
|
||||
DarkPurple: 0x71368a,
|
||||
DarkVividPink: 0xad1457,
|
||||
DarkGold: 0xc27c0e,
|
||||
DarkOrange: 0xa84300,
|
||||
DarkRed: 0x992d22,
|
||||
DarkGrey: 0x979c9f,
|
||||
DarkerGrey: 0x7f8c8d,
|
||||
LightGrey: 0xbcc0c0,
|
||||
DarkNavy: 0x2c3e50,
|
||||
Blurple: 0x5865f2,
|
||||
Greyple: 0x99aab5,
|
||||
DarkButNotBlack: 0x2c2f33,
|
||||
NotQuiteBlack: 0x23272a,
|
||||
};
|
44
src/util/Components.js
Normal file
44
src/util/Components.js
Normal file
@@ -0,0 +1,44 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* @typedef {Object} BaseComponentData
|
||||
* @property {ComponentType} type
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {BaseComponentData} ActionRowData
|
||||
* @property {ComponentData[]} components
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {BaseComponentData} ButtonComponentData
|
||||
* @property {ButtonStyle} style
|
||||
* @property {?boolean} disabled
|
||||
* @property {string} label
|
||||
* @property {?APIComponentEmoji} emoji
|
||||
* @property {?string} customId
|
||||
* @property {?string} url
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {object} SelectMenuComponentOptionData
|
||||
* @property {string} label
|
||||
* @property {string} value
|
||||
* @property {?string} description
|
||||
* @property {?APIComponentEmoji} emoji
|
||||
* @property {?boolean} default
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {BaseComponentData} SelectMenuComponentData
|
||||
* @property {string} customId
|
||||
* @property {?boolean} disabled
|
||||
* @property {?number} maxValues
|
||||
* @property {?number} minValues
|
||||
* @property {?SelectMenuComponentOptionData[]} options
|
||||
* @property {?string} placeholder
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {ActionRowData|ButtonComponentData|SelectMenuComponentData} ComponentData
|
||||
*/
|
266
src/util/Constants.js
Normal file
266
src/util/Constants.js
Normal file
@@ -0,0 +1,266 @@
|
||||
'use strict';
|
||||
|
||||
const process = require('node:process');
|
||||
const { ChannelType, MessageType } = require('discord-api-types/v9');
|
||||
const Package = (exports.Package = require('../../package.json'));
|
||||
|
||||
exports.UserAgent = `Mozilla/5.0 (iPhone; CPU iPhone OS 15_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/90.0.4430.78 Mobile/15E148 Safari/604.1`;
|
||||
|
||||
/**
|
||||
* The name of an item to be swept in Sweepers
|
||||
* * `applicationCommands` - both global and guild commands
|
||||
* * `bans`
|
||||
* * `emojis`
|
||||
* * `invites` - accepts the `lifetime` property, using it will sweep based on expires timestamp
|
||||
* * `guildMembers`
|
||||
* * `messages` - accepts the `lifetime` property, using it will sweep based on edited or created timestamp
|
||||
* * `presences`
|
||||
* * `reactions`
|
||||
* * `stageInstances`
|
||||
* * `stickers`
|
||||
* * `threadMembers`
|
||||
* * `threads` - accepts the `lifetime` property, using it will sweep archived threads based on archived timestamp
|
||||
* * `users`
|
||||
* * `voiceStates`
|
||||
* @typedef {string} SweeperKey
|
||||
*/
|
||||
exports.SweeperKeys = [
|
||||
'applicationCommands',
|
||||
'bans',
|
||||
'emojis',
|
||||
'invites',
|
||||
'guildMembers',
|
||||
'messages',
|
||||
'presences',
|
||||
'reactions',
|
||||
'stageInstances',
|
||||
'stickers',
|
||||
'threadMembers',
|
||||
'threads',
|
||||
'users',
|
||||
'voiceStates',
|
||||
];
|
||||
|
||||
/**
|
||||
* The types of messages that are not `System`. The available types are:
|
||||
* * {@link MessageType.Default}
|
||||
* * {@link MessageType.Reply}
|
||||
* * {@link MessageType.ChatInputCommand}
|
||||
* * {@link MessageType.ContextMenuCommand}
|
||||
* @typedef {MessageType[]} NonSystemMessageTypes
|
||||
*/
|
||||
exports.NonSystemMessageTypes = [
|
||||
MessageType.Default,
|
||||
MessageType.Reply,
|
||||
MessageType.ChatInputCommand,
|
||||
MessageType.ContextMenuCommand,
|
||||
];
|
||||
|
||||
/**
|
||||
* The channels that are text-based.
|
||||
* * DMChannel
|
||||
* * TextChannel
|
||||
* * NewsChannel
|
||||
* * ThreadChannel
|
||||
* @typedef {DMChannel|TextChannel|NewsChannel|ThreadChannel} TextBasedChannels
|
||||
*/
|
||||
|
||||
/**
|
||||
* The types of channels that are text-based. The available types are:
|
||||
* * {@link ChannelType.DM}
|
||||
* * {@link ChannelType.GuildText}
|
||||
* * {@link ChannelType.GuildNews}
|
||||
* * {@link ChannelType.GuildNewsThread}
|
||||
* * {@link ChannelType.GuildPublicThread}
|
||||
* * {@link ChannelType.GuildPrivateThread}
|
||||
* @typedef {ChannelType} TextBasedChannelTypes
|
||||
*/
|
||||
exports.TextBasedChannelTypes = [
|
||||
ChannelType.DM,
|
||||
ChannelType.GuildText,
|
||||
ChannelType.GuildNews,
|
||||
ChannelType.GuildNewsThread,
|
||||
ChannelType.GuildPublicThread,
|
||||
ChannelType.GuildPrivateThread,
|
||||
];
|
||||
|
||||
/**
|
||||
* The types of channels that are threads. The available types are:
|
||||
* * {@link ChannelType.GuildNewsThread}
|
||||
* * {@link ChannelType.GuildPublicThread}
|
||||
* * {@link ChannelType.GuildPrivateThread}
|
||||
* @typedef {ChannelType[]} ThreadChannelTypes
|
||||
*/
|
||||
exports.ThreadChannelTypes = [
|
||||
ChannelType.GuildNewsThread,
|
||||
ChannelType.GuildPublicThread,
|
||||
ChannelType.GuildPrivateThread,
|
||||
];
|
||||
|
||||
/**
|
||||
* The types of channels that are voice-based. The available types are:
|
||||
* * {@link ChannelType.GuildVoice}
|
||||
* * {@link ChannelType.GuildStageVoice}
|
||||
* @typedef {ChannelType[]} VoiceBasedChannelTypes
|
||||
*/
|
||||
exports.VoiceBasedChannelTypes = [ChannelType.GuildVoice, ChannelType.GuildStageVoice];
|
||||
|
||||
/* eslint-enable max-len */
|
||||
|
||||
/**
|
||||
* @typedef {Object} Constants Constants that can be used in an enum or object-like way.
|
||||
* @property {Status} Status The available statuses of the client.
|
||||
*/
|
||||
|
||||
|
||||
exports.Events = {
|
||||
RATE_LIMIT: 'rateLimit',
|
||||
INVALID_REQUEST_WARNING: 'invalidRequestWarning',
|
||||
API_RESPONSE: 'apiResponse',
|
||||
API_REQUEST: 'apiRequest',
|
||||
CLIENT_READY: 'ready',
|
||||
/**
|
||||
* @deprecated See {@link https://github.com/discord/discord-api-docs/issues/3690 this issue} for more information.
|
||||
*/
|
||||
APPLICATION_COMMAND_CREATE: 'applicationCommandCreate',
|
||||
/**
|
||||
* @deprecated See {@link https://github.com/discord/discord-api-docs/issues/3690 this issue} for more information.
|
||||
*/
|
||||
APPLICATION_COMMAND_DELETE: 'applicationCommandDelete',
|
||||
/**
|
||||
* @deprecated See {@link https://github.com/discord/discord-api-docs/issues/3690 this issue} for more information.
|
||||
*/
|
||||
APPLICATION_COMMAND_UPDATE: 'applicationCommandUpdate',
|
||||
GUILD_CREATE: 'guildCreate',
|
||||
GUILD_DELETE: 'guildDelete',
|
||||
GUILD_UPDATE: 'guildUpdate',
|
||||
GUILD_UNAVAILABLE: 'guildUnavailable',
|
||||
GUILD_MEMBER_ADD: 'guildMemberAdd',
|
||||
GUILD_MEMBER_REMOVE: 'guildMemberRemove',
|
||||
GUILD_MEMBER_UPDATE: 'guildMemberUpdate',
|
||||
GUILD_MEMBER_AVAILABLE: 'guildMemberAvailable',
|
||||
GUILD_MEMBERS_CHUNK: 'guildMembersChunk',
|
||||
GUILD_INTEGRATIONS_UPDATE: 'guildIntegrationsUpdate',
|
||||
GUILD_ROLE_CREATE: 'roleCreate',
|
||||
GUILD_ROLE_DELETE: 'roleDelete',
|
||||
INVITE_CREATE: 'inviteCreate',
|
||||
INVITE_DELETE: 'inviteDelete',
|
||||
GUILD_ROLE_UPDATE: 'roleUpdate',
|
||||
GUILD_EMOJI_CREATE: 'emojiCreate',
|
||||
GUILD_EMOJI_DELETE: 'emojiDelete',
|
||||
GUILD_EMOJI_UPDATE: 'emojiUpdate',
|
||||
GUILD_BAN_ADD: 'guildBanAdd',
|
||||
GUILD_BAN_REMOVE: 'guildBanRemove',
|
||||
CHANNEL_CREATE: 'channelCreate',
|
||||
CHANNEL_DELETE: 'channelDelete',
|
||||
CHANNEL_UPDATE: 'channelUpdate',
|
||||
CHANNEL_PINS_UPDATE: 'channelPinsUpdate',
|
||||
MESSAGE_CREATE: 'messageCreate',
|
||||
MESSAGE_DELETE: 'messageDelete',
|
||||
MESSAGE_UPDATE: 'messageUpdate',
|
||||
MESSAGE_BULK_DELETE: 'messageDeleteBulk',
|
||||
MESSAGE_REACTION_ADD: 'messageReactionAdd',
|
||||
MESSAGE_REACTION_REMOVE: 'messageReactionRemove',
|
||||
MESSAGE_REACTION_REMOVE_ALL: 'messageReactionRemoveAll',
|
||||
MESSAGE_REACTION_REMOVE_EMOJI: 'messageReactionRemoveEmoji',
|
||||
THREAD_CREATE: 'threadCreate',
|
||||
THREAD_DELETE: 'threadDelete',
|
||||
THREAD_UPDATE: 'threadUpdate',
|
||||
THREAD_LIST_SYNC: 'threadListSync',
|
||||
THREAD_MEMBER_UPDATE: 'threadMemberUpdate',
|
||||
THREAD_MEMBERS_UPDATE: 'threadMembersUpdate',
|
||||
USER_UPDATE: 'userUpdate',
|
||||
PRESENCE_UPDATE: 'presenceUpdate',
|
||||
VOICE_SERVER_UPDATE: 'voiceServerUpdate',
|
||||
VOICE_STATE_UPDATE: 'voiceStateUpdate',
|
||||
TYPING_START: 'typingStart',
|
||||
WEBHOOKS_UPDATE: 'webhookUpdate',
|
||||
INTERACTION_CREATE: 'interactionCreate',
|
||||
ERROR: 'error',
|
||||
WARN: 'warn',
|
||||
DEBUG: 'debug',
|
||||
CACHE_SWEEP: 'cacheSweep',
|
||||
SHARD_DISCONNECT: 'shardDisconnect',
|
||||
SHARD_ERROR: 'shardError',
|
||||
SHARD_RECONNECTING: 'shardReconnecting',
|
||||
SHARD_READY: 'shardReady',
|
||||
SHARD_RESUME: 'shardResume',
|
||||
INVALIDATED: 'invalidated',
|
||||
RAW: 'raw',
|
||||
STAGE_INSTANCE_CREATE: 'stageInstanceCreate',
|
||||
STAGE_INSTANCE_UPDATE: 'stageInstanceUpdate',
|
||||
STAGE_INSTANCE_DELETE: 'stageInstanceDelete',
|
||||
GUILD_STICKER_CREATE: 'stickerCreate',
|
||||
GUILD_STICKER_DELETE: 'stickerDelete',
|
||||
GUILD_STICKER_UPDATE: 'stickerUpdate',
|
||||
GUILD_SCHEDULED_EVENT_CREATE: 'guildScheduledEventCreate',
|
||||
GUILD_SCHEDULED_EVENT_UPDATE: 'guildScheduledEventUpdate',
|
||||
GUILD_SCHEDULED_EVENT_DELETE: 'guildScheduledEventDelete',
|
||||
GUILD_SCHEDULED_EVENT_USER_ADD: 'guildScheduledEventUserAdd',
|
||||
GUILD_SCHEDULED_EVENT_USER_REMOVE: 'guildScheduledEventUserRemove',
|
||||
};
|
||||
|
||||
function makeImageUrl(root, { format = 'webp', size } = {}) {
|
||||
if (!['undefined', 'number'].includes(typeof size)) throw new TypeError('INVALID_TYPE', 'size', 'number');
|
||||
if (format && !AllowedImageFormats.includes(format)) throw new Error('IMAGE_FORMAT', format);
|
||||
if (size && !AllowedImageSizes.includes(size)) throw new RangeError('IMAGE_SIZE', size);
|
||||
return `${root}.${format}${size ? `?size=${size}` : ''}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for Image URLs.
|
||||
* @typedef {StaticImageURLOptions} ImageURLOptions
|
||||
* @property {boolean} [dynamic=false] If true, the format will dynamically change to `gif` for animated avatars.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Options for static Image URLs.
|
||||
* @typedef {Object} StaticImageURLOptions
|
||||
* @property {string} [format='webp'] One of `webp`, `png`, `jpg`, `jpeg`.
|
||||
* @property {number} [size] One of `16`, `32`, `56`, `64`, `96`, `128`, `256`, `300`, `512`, `600`, `1024`, `2048`,
|
||||
* `4096`
|
||||
*/
|
||||
|
||||
// https://discord.com/developers/docs/reference#image-formatting-cdn-endpoints
|
||||
exports.Endpoints = {
|
||||
CDN(root) {
|
||||
return {
|
||||
Emoji: (emojiId, format = 'webp') => `${root}/emojis/${emojiId}.${format}`,
|
||||
Asset: name => `${root}/assets/${name}`,
|
||||
DefaultAvatar: discriminator => `${root}/embed/avatars/${discriminator}.png`,
|
||||
Avatar: (userId, hash, format, size, dynamic = false) => {
|
||||
if (dynamic && hash.startsWith('a_')) format = 'gif';
|
||||
return makeImageUrl(`${root}/avatars/${userId}/${hash}`, { format, size });
|
||||
},
|
||||
GuildMemberAvatar: (guildId, memberId, hash, format = 'webp', size, dynamic = false) => {
|
||||
if (dynamic && hash.startsWith('a_')) format = 'gif';
|
||||
return makeImageUrl(`${root}/guilds/${guildId}/users/${memberId}/avatars/${hash}`, { format, size });
|
||||
},
|
||||
Banner: (id, hash, format, size, dynamic = false) => {
|
||||
if (dynamic && hash.startsWith('a_')) format = 'gif';
|
||||
return makeImageUrl(`${root}/banners/${id}/${hash}`, { format, size });
|
||||
},
|
||||
Icon: (guildId, hash, format, size, dynamic = false) => {
|
||||
if (dynamic && hash.startsWith('a_')) format = 'gif';
|
||||
return makeImageUrl(`${root}/icons/${guildId}/${hash}`, { format, size });
|
||||
},
|
||||
AppIcon: (appId, hash, options) => makeImageUrl(`${root}/app-icons/${appId}/${hash}`, options),
|
||||
AppAsset: (appId, hash, options) => makeImageUrl(`${root}/app-assets/${appId}/${hash}`, options),
|
||||
StickerPackBanner: (bannerId, format, size) =>
|
||||
makeImageUrl(`${root}/app-assets/710982414301790216/store/${bannerId}`, { size, format }),
|
||||
GDMIcon: (channelId, hash, format, size) =>
|
||||
makeImageUrl(`${root}/channel-icons/${channelId}/${hash}`, { size, format }),
|
||||
Splash: (guildId, hash, format, size) => makeImageUrl(`${root}/splashes/${guildId}/${hash}`, { size, format }),
|
||||
DiscoverySplash: (guildId, hash, format, size) =>
|
||||
makeImageUrl(`${root}/discovery-splashes/${guildId}/${hash}`, { size, format }),
|
||||
TeamIcon: (teamId, hash, options) => makeImageUrl(`${root}/team-icons/${teamId}/${hash}`, options),
|
||||
Sticker: (stickerId, stickerFormat) =>
|
||||
`${root}/stickers/${stickerId}.${stickerFormat === 'LOTTIE' ? 'json' : 'png'}`,
|
||||
RoleIcon: (roleId, hash, format = 'webp', size) =>
|
||||
makeImageUrl(`${root}/role-icons/${roleId}/${hash}`, { size, format }),
|
||||
};
|
||||
},
|
||||
invite: (root, code, eventId) => (eventId ? `${root}/${code}?event=${eventId}` : `${root}/${code}`),
|
||||
scheduledEvent: (root, guildId, eventId) => `${root}/${guildId}/${eventId}`,
|
||||
botGateway: '/gateway/bot',
|
||||
};
|
135
src/util/DataResolver.js
Normal file
135
src/util/DataResolver.js
Normal file
@@ -0,0 +1,135 @@
|
||||
'use strict';
|
||||
|
||||
const { Buffer } = require('node:buffer');
|
||||
const fs = require('node:fs/promises');
|
||||
const path = require('node:path');
|
||||
const stream = require('node:stream');
|
||||
const { fetch } = require('undici');
|
||||
const { Error: DiscordError, TypeError } = require('../errors');
|
||||
const Invite = require('../structures/Invite');
|
||||
|
||||
/**
|
||||
* The DataResolver identifies different objects and tries to resolve a specific piece of information from them.
|
||||
* @private
|
||||
*/
|
||||
class DataResolver extends null {
|
||||
/**
|
||||
* Data that can be resolved to give an invite code. This can be:
|
||||
* * An invite code
|
||||
* * An invite URL
|
||||
* @typedef {string} InviteResolvable
|
||||
*/
|
||||
|
||||
/**
|
||||
* Data that can be resolved to give a template code. This can be:
|
||||
* * A template code
|
||||
* * A template URL
|
||||
* @typedef {string} GuildTemplateResolvable
|
||||
*/
|
||||
|
||||
/**
|
||||
* Resolves the string to a code based on the passed regex.
|
||||
* @param {string} data The string to resolve
|
||||
* @param {RegExp} regex The RegExp used to extract the code
|
||||
* @returns {string}
|
||||
*/
|
||||
static resolveCode(data, regex) {
|
||||
return data.matchAll(regex).next().value?.[1] ?? data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves InviteResolvable to an invite code.
|
||||
* @param {InviteResolvable} data The invite resolvable to resolve
|
||||
* @returns {string}
|
||||
*/
|
||||
static resolveInviteCode(data) {
|
||||
return this.resolveCode(data, Invite.INVITES_PATTERN);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves GuildTemplateResolvable to a template code.
|
||||
* @param {GuildTemplateResolvable} data The template resolvable to resolve
|
||||
* @returns {string}
|
||||
*/
|
||||
static resolveGuildTemplateCode(data) {
|
||||
const GuildTemplate = require('../structures/GuildTemplate');
|
||||
return this.resolveCode(data, GuildTemplate.GUILD_TEMPLATES_PATTERN);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves a Base64Resolvable, a string, or a BufferResolvable to a Base 64 image.
|
||||
* @param {BufferResolvable|Base64Resolvable} image The image to be resolved
|
||||
* @returns {Promise<?string>}
|
||||
*/
|
||||
static async resolveImage(image) {
|
||||
if (!image) return null;
|
||||
if (typeof image === 'string' && image.startsWith('data:')) {
|
||||
return image;
|
||||
}
|
||||
const file = await this.resolveFile(image);
|
||||
return this.resolveBase64(file);
|
||||
}
|
||||
|
||||
/**
|
||||
* Data that resolves to give a Base64 string, typically for image uploading. This can be:
|
||||
* * A Buffer
|
||||
* * A base64 string
|
||||
* @typedef {Buffer|string} Base64Resolvable
|
||||
*/
|
||||
|
||||
/**
|
||||
* Resolves a Base64Resolvable to a Base 64 image.
|
||||
* @param {Base64Resolvable} data The base 64 resolvable you want to resolve
|
||||
* @returns {?string}
|
||||
*/
|
||||
static resolveBase64(data) {
|
||||
if (Buffer.isBuffer(data)) return `data:image/jpg;base64,${data.toString('base64')}`;
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Data that can be resolved to give a Buffer. This can be:
|
||||
* * A Buffer
|
||||
* * The path to a local file
|
||||
* * A URL <warn>When provided a URL, discord.js will fetch the URL internally in order to create a Buffer.
|
||||
* This can pose a security risk when the URL has not been sanitized</warn>
|
||||
* @typedef {string|Buffer} BufferResolvable
|
||||
*/
|
||||
|
||||
/**
|
||||
* @external Stream
|
||||
* @see {@link https://nodejs.org/api/stream.html}
|
||||
*/
|
||||
|
||||
/**
|
||||
* Resolves a BufferResolvable to a Buffer.
|
||||
* @param {BufferResolvable|Stream} resource The buffer or stream resolvable to resolve
|
||||
* @returns {Promise<Buffer>}
|
||||
*/
|
||||
static async resolveFile(resource) {
|
||||
if (Buffer.isBuffer(resource)) return resource;
|
||||
|
||||
if (resource instanceof stream.Readable) {
|
||||
const buffers = [];
|
||||
for await (const data of resource) buffers.push(data);
|
||||
return Buffer.concat(buffers);
|
||||
}
|
||||
|
||||
if (typeof resource === 'string') {
|
||||
if (/^https?:\/\//.test(resource)) {
|
||||
const res = await fetch(resource);
|
||||
return Buffer.from(await res.arrayBuffer());
|
||||
}
|
||||
|
||||
const file = path.resolve(resource);
|
||||
|
||||
const stats = await fs.stat(file);
|
||||
if (!stats.isFile()) throw new DiscordError('FILE_NOT_FOUND', file);
|
||||
return fs.readFile(file);
|
||||
}
|
||||
|
||||
throw new TypeError('REQ_RESOURCE_TYPE');
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = DataResolver;
|
48
src/util/Embeds.js
Normal file
48
src/util/Embeds.js
Normal file
@@ -0,0 +1,48 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* @typedef {Object} EmbedData
|
||||
* @property {?string} title
|
||||
* @property {?EmbedType} type
|
||||
* @property {?string} description
|
||||
* @property {?string} url
|
||||
* @property {?string} timestamp
|
||||
* @property {?number} color
|
||||
* @property {?EmbedFooterData} footer
|
||||
* @property {?EmbedImageData} image
|
||||
* @property {?EmbedImageData} thumbnail
|
||||
* @property {?EmbedProviderData} provider
|
||||
* @property {?EmbedAuthorData} author
|
||||
* @property {?EmbedFieldData[]} fields
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} EmbedFooterData
|
||||
* @property {string} text
|
||||
* @property {?string} iconURL
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} EmbedImageData
|
||||
* @property {?string} url
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} EmbedProviderData
|
||||
* @property {?string} name
|
||||
* @property {?string} url
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} EmbedAuthorData
|
||||
* @property {string} name
|
||||
* @property {?string} url
|
||||
* @property {?string} iconURL
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} EmbedFieldData
|
||||
* @property {string} name
|
||||
* @property {string} value
|
||||
* @property {?boolean} inline
|
||||
*/
|
819
src/util/EnumResolvers.js
Normal file
819
src/util/EnumResolvers.js
Normal file
@@ -0,0 +1,819 @@
|
||||
'use strict';
|
||||
|
||||
const {
|
||||
ApplicationCommandType,
|
||||
InteractionType,
|
||||
ComponentType,
|
||||
ButtonStyle,
|
||||
ApplicationCommandOptionType,
|
||||
ChannelType,
|
||||
ApplicationCommandPermissionType,
|
||||
MessageType,
|
||||
GuildNSFWLevel,
|
||||
GuildVerificationLevel,
|
||||
GuildDefaultMessageNotifications,
|
||||
GuildExplicitContentFilter,
|
||||
GuildPremiumTier,
|
||||
GuildScheduledEventStatus,
|
||||
StageInstancePrivacyLevel,
|
||||
GuildMFALevel,
|
||||
TeamMemberMembershipState,
|
||||
GuildScheduledEventEntityType,
|
||||
IntegrationExpireBehavior,
|
||||
AuditLogEvent,
|
||||
} = require('discord-api-types/v9');
|
||||
|
||||
function unknownKeyStrategy(val) {
|
||||
throw new Error(`Could not resolve enum value for ${val}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds a bunch of methods to resolve enum values to readable strings.
|
||||
*/
|
||||
class EnumResolvers extends null {
|
||||
/**
|
||||
* A string that can be resolved to a {@link ChannelType} enum value. Here are the available types:
|
||||
* * GUILD_TEXT
|
||||
* * DM
|
||||
* * GUILD_VOICE
|
||||
* * GROUP_DM
|
||||
* * GUILD_CATEGORY
|
||||
* * GUILD_NEWS
|
||||
* * GUILD_NEWS_THREAD
|
||||
* * GUILD_PUBLIC_THREAD
|
||||
* * GUILD_PRIVATE_THREAD
|
||||
* * GUILD_STAGE_VOICE
|
||||
* @typedef {string} ChannelTypeEnumResolvable
|
||||
*/
|
||||
|
||||
/**
|
||||
* Resolves enum key to {@link ChannelType} enum value
|
||||
* @param {ChannelTypeEnumResolvable|ChannelType} key The key to resolve
|
||||
* @returns {ChannelType}
|
||||
*/
|
||||
static resolveChannelType(key) {
|
||||
switch (key) {
|
||||
case 'GUILD_TEXT':
|
||||
return ChannelType.GuildText;
|
||||
case 'DM':
|
||||
return ChannelType.DM;
|
||||
case 'GUILD_VOICE':
|
||||
return ChannelType.GuildVoice;
|
||||
case 'GROUP_DM':
|
||||
return ChannelType.GroupDM;
|
||||
case 'GUILD_CATEGORY':
|
||||
return ChannelType.GuildCategory;
|
||||
case 'GUILD_NEWS':
|
||||
return ChannelType.GuildNews;
|
||||
case 'GUILD_NEWS_THREAD':
|
||||
return ChannelType.GuildNewsThread;
|
||||
case 'GUILD_PUBLIC_THREAD':
|
||||
return ChannelType.GuildPublicThread;
|
||||
case 'GUILD_PRIVATE_THREAD':
|
||||
return ChannelType.GuildPrivateThread;
|
||||
case 'GUILD_STAGE_VOICE':
|
||||
return ChannelType.GuildStageVoice;
|
||||
default:
|
||||
return unknownKeyStrategy(key);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A string that can be resolved to an {@link InteractionType} enum value. Here are the available types:
|
||||
* * PING
|
||||
* * APPLICATION_COMMAND
|
||||
* * MESSAGE_COMPONENT
|
||||
* * APPLICATION_COMMAND_AUTOCOMPLETE
|
||||
* @typedef {string} InteractionTypeEnumResolvable
|
||||
*/
|
||||
|
||||
/**
|
||||
* Resolves enum key to {@link InteractionType} enum value
|
||||
* @param {InteractionTypeEnumResolvable|InteractionType} key The key to resolve
|
||||
* @returns {InteractionType}
|
||||
*/
|
||||
static resolveInteractionType(key) {
|
||||
switch (key) {
|
||||
case 'PING':
|
||||
return InteractionType.Ping;
|
||||
case 'APPLICATION_COMMAND':
|
||||
return InteractionType.ApplicationCommand;
|
||||
case 'MESSAGE_COMPONENT':
|
||||
return InteractionType.MessageComponent;
|
||||
case 'APPLICATION_COMMAND_AUTOCOMPLETE':
|
||||
return InteractionType.ApplicationCommandAutocomplete;
|
||||
default:
|
||||
return unknownKeyStrategy(key);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A string that can be resolved to an {@link ApplicationCommandType} enum value. Here are the available types:
|
||||
* * CHAT_INPUT
|
||||
* * USER
|
||||
* * MESSAGE
|
||||
* @typedef {string} ApplicationCommandTypeEnumResolvable
|
||||
*/
|
||||
|
||||
/**
|
||||
* Resolves enum key to {@link ApplicationCommandType} enum value
|
||||
* @param {ApplicationCommandTypeEnumResolvable|ApplicationCommandType} key The key to resolve
|
||||
* @returns {ApplicationCommandType}
|
||||
*/
|
||||
static resolveApplicationCommandType(key) {
|
||||
switch (key) {
|
||||
case 'CHAT_INPUT':
|
||||
return ApplicationCommandType.ChatInput;
|
||||
case 'USER':
|
||||
return ApplicationCommandType.User;
|
||||
case 'MESSAGE':
|
||||
return ApplicationCommandType.Message;
|
||||
default:
|
||||
return unknownKeyStrategy(key);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A string that can be resolved to an {@link ApplicationCommandOptionType} enum value. Here are the available types:
|
||||
* * SUB_COMMAND
|
||||
* * SUB_COMMAND_GROUP
|
||||
* * STRING
|
||||
* * INTEGER
|
||||
* * BOOLEAN
|
||||
* * USER
|
||||
* * CHANNEL
|
||||
* * ROLE
|
||||
* * NUMBER
|
||||
* * MENTIONABLE
|
||||
* @typedef {string} ApplicationCommandOptionTypeEnumResolvable
|
||||
*/
|
||||
|
||||
/**
|
||||
* Resolves enum key to {@link ApplicationCommandOptionType} enum value
|
||||
* @param {ApplicationCommandOptionTypeEnumResolvable|ApplicationCommandOptionType} key The key to resolve
|
||||
* @returns {ApplicationCommandOptionType}
|
||||
*/
|
||||
static resolveApplicationCommandOptionType(key) {
|
||||
switch (key) {
|
||||
case 'SUB_COMMAND':
|
||||
return ApplicationCommandOptionType.Subcommand;
|
||||
case 'SUB_COMMAND_GROUP':
|
||||
return ApplicationCommandOptionType.SubcommandGroup;
|
||||
case 'STRING':
|
||||
return ApplicationCommandOptionType.String;
|
||||
case 'INTEGER':
|
||||
return ApplicationCommandOptionType.Integer;
|
||||
case 'BOOLEAN':
|
||||
return ApplicationCommandOptionType.Boolean;
|
||||
case 'USER':
|
||||
return ApplicationCommandOptionType.User;
|
||||
case 'CHANNEL':
|
||||
return ApplicationCommandOptionType.Channel;
|
||||
case 'ROLE':
|
||||
return ApplicationCommandOptionType.Role;
|
||||
case 'NUMBER':
|
||||
return ApplicationCommandOptionType.Number;
|
||||
case 'MENTIONABLE':
|
||||
return ApplicationCommandOptionType.Mentionable;
|
||||
default:
|
||||
return unknownKeyStrategy(key);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A string that can be resolved to an {@link ApplicationCommandPermissionType} enum value.
|
||||
* Here are the available types:
|
||||
* * ROLE
|
||||
* * USER
|
||||
* @typedef {string} ApplicationCommandPermissionTypeEnumResolvable
|
||||
*/
|
||||
|
||||
/**
|
||||
* Resolves enum key to {@link ApplicationCommandPermissionType} enum value
|
||||
* @param {ApplicationCommandPermissionTypeEnumResolvable|ApplicationCommandPermissionType} key The key to resolve
|
||||
* @returns {ApplicationCommandPermissionType}
|
||||
*/
|
||||
static resolveApplicationCommandPermissionType(key) {
|
||||
switch (key) {
|
||||
case 'ROLE':
|
||||
return ApplicationCommandPermissionType.Role;
|
||||
case 'USER':
|
||||
return ApplicationCommandPermissionType.User;
|
||||
default:
|
||||
return unknownKeyStrategy(key);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A string that can be resolved to a {@link ComponentType} enum value. Here are the available types:
|
||||
* * ACTION_ROW
|
||||
* * BUTTON
|
||||
* * SELECT_MENU
|
||||
* @typedef {string} ComponentTypeEnumResolvable
|
||||
*/
|
||||
|
||||
/**
|
||||
* Resolves enum key to {@link ComponentType} enum value
|
||||
* @param {ComponentTypeEnumResolvable|ComponentType} key The key to resolve
|
||||
* @returns {ComponentType}
|
||||
*/
|
||||
static resolveComponentType(key) {
|
||||
switch (key) {
|
||||
case 'ACTION_ROW':
|
||||
return ComponentType.ActionRow;
|
||||
case 'BUTTON':
|
||||
return ComponentType.Button;
|
||||
case 'SELECT_MENU':
|
||||
return ComponentType.SelectMenu;
|
||||
default:
|
||||
return unknownKeyStrategy(key);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A string that can be resolved to a {@link ButtonStyle} enum value. Here are the available types:
|
||||
* * PRIMARY
|
||||
* * SECONDARY
|
||||
* * SUCCESS
|
||||
* * DANGER
|
||||
* * LINK
|
||||
* @typedef {string} ButtonStyleEnumResolvable
|
||||
*/
|
||||
|
||||
/**
|
||||
* Resolves enum key to {@link ButtonStyle} enum value
|
||||
* @param {ButtonStyleEnumResolvable|ButtonStyle} key The key to resolve
|
||||
* @returns {ButtonStyle}
|
||||
*/
|
||||
static resolveButtonStyle(key) {
|
||||
switch (key) {
|
||||
case 'PRIMARY':
|
||||
return ButtonStyle.Primary;
|
||||
case 'SECONDARY':
|
||||
return ButtonStyle.Secondary;
|
||||
case 'SUCCESS':
|
||||
return ButtonStyle.Success;
|
||||
case 'DANGER':
|
||||
return ButtonStyle.Danger;
|
||||
case 'LINK':
|
||||
return ButtonStyle.Link;
|
||||
default:
|
||||
return unknownKeyStrategy(key);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A string that can be resolved to a {@link MessageType} enum value. Here are the available types:
|
||||
* * DEFAULT
|
||||
* * RECIPIENT_ADD
|
||||
* * RECIPIENT_REMOVE
|
||||
* * CALL
|
||||
* * CHANNEL_NAME_CHANGE
|
||||
* * CHANNEL_ICON_CHANGE
|
||||
* * CHANNEL_PINNED_MESSAGE
|
||||
* * GUILD_MEMBER_JOIN
|
||||
* * USER_PREMIUM_GUILD_SUBSCRIPTION
|
||||
* * USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_1
|
||||
* * USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_2
|
||||
* * USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_3
|
||||
* * CHANNEL_FOLLOW_ADD
|
||||
* * GUILD_DISCOVERY_DISQUALIFIED
|
||||
* * GUILD_DISCOVERY_REQUALIFIED
|
||||
* * GUILD_DISCOVERY_GRACE_PERIOD_INITIAL_WARNING
|
||||
* * GUILD_DISCOVERY_GRACE_PERIOD_FINAL_WARNING
|
||||
* * THREAD_CREATED
|
||||
* * REPLY
|
||||
* * CHAT_INPUT_COMMAND
|
||||
* * THREAD_STARTER_MESSAGE
|
||||
* * GUILD_INVITE_REMINDER
|
||||
* * CONTEXT_MENU_COMMAND
|
||||
* @typedef {string} MessageTypeEnumResolvable
|
||||
*/
|
||||
|
||||
/**
|
||||
* Resolves enum key to {@link MessageType} enum value
|
||||
* @param {MessageTypeEnumResolvable|MessageType} key The key to lookup
|
||||
* @returns {MessageType}
|
||||
*/
|
||||
static resolveMessageType(key) {
|
||||
switch (key) {
|
||||
case 'DEFAULT':
|
||||
return MessageType.Default;
|
||||
case 'RECIPIENT_ADD':
|
||||
return MessageType.RecipientAdd;
|
||||
case 'RECIPIENT_REMOVE':
|
||||
return MessageType.RecipientRemove;
|
||||
case 'CALL':
|
||||
return MessageType.Call;
|
||||
case 'CHANNEL_NAME_CHANGE':
|
||||
return MessageType.ChannelNameChange;
|
||||
case 'CHANNEL_ICON_CHANGE':
|
||||
return MessageType.ChannelIconChange;
|
||||
case 'CHANNEL_PINNED_MESSAGE':
|
||||
return MessageType.ChannelPinnedMessage;
|
||||
case 'GUILD_MEMBER_JOIN':
|
||||
return MessageType.GuildMemberJoin;
|
||||
case 'USER_PREMIUM_GUILD_SUBSCRIPTION':
|
||||
return MessageType.UserPremiumGuildSubscription;
|
||||
case 'USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_1':
|
||||
return MessageType.UserPremiumGuildSubscriptionTier1;
|
||||
case 'USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_2':
|
||||
return MessageType.UserPremiumGuildSubscriptionTier2;
|
||||
case 'USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_3':
|
||||
return MessageType.UserPremiumGuildSubscriptionTier3;
|
||||
case 'CHANNEL_FOLLOW_ADD':
|
||||
return MessageType.ChannelFollowAdd;
|
||||
case 'GUILD_DISCOVERY_DISQUALIFIED':
|
||||
return MessageType.GuildDiscoveryDisqualified;
|
||||
case 'GUILD_DISCOVERY_REQUALIFIED':
|
||||
return MessageType.GuildDiscoveryRequalified;
|
||||
case 'GUILD_DISCOVERY_GRACE_PERIOD_INITIAL_WARNING':
|
||||
return MessageType.GuildDiscoveryGracePeriodInitialWarning;
|
||||
case 'GUILD_DISCOVERY_GRACE_PERIOD_FINAL_WARNING':
|
||||
return MessageType.GuildDiscoveryGracePeriodFinalWarning;
|
||||
case 'THREAD_CREATED':
|
||||
return MessageType.ThreadCreated;
|
||||
case 'REPLY':
|
||||
return MessageType.Reply;
|
||||
case 'CHAT_INPUT_COMMAND':
|
||||
return MessageType.ChatInputCommand;
|
||||
case 'THREAD_STARTER_MESSAGE':
|
||||
return MessageType.ThreadStarterMessage;
|
||||
case 'GUILD_INVITE_REMINDER':
|
||||
return MessageType.GuildInviteReminder;
|
||||
case 'CONTEXT_MENU_COMMAND':
|
||||
return MessageType.ContextMenuCommand;
|
||||
default:
|
||||
return unknownKeyStrategy(key);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A string that can be resolved to a {@link GuildNSFWLevel} enum value. Here are the available types:
|
||||
* * DEFAULT
|
||||
* * EXPLICIT
|
||||
* * SAFE
|
||||
* * AGE_RESTRICTED
|
||||
* @typedef {string} GuildNSFWLevelEnumResolvable
|
||||
*/
|
||||
|
||||
/**
|
||||
* Resolves enum key to {@link GuildNSFWLevel} enum value
|
||||
* @param {GuildNSFWLevelEnumResolvable|GuildNSFWLevel} key The key to lookup
|
||||
* @returns {GuildNSFWLevel}
|
||||
*/
|
||||
static resolveGuildNSFWLevel(key) {
|
||||
switch (key) {
|
||||
case 'DEFAULT':
|
||||
return GuildNSFWLevel.Default;
|
||||
case 'EXPLICIT':
|
||||
return GuildNSFWLevel.Explicit;
|
||||
case 'SAFE':
|
||||
return GuildNSFWLevel.Safe;
|
||||
case 'AGE_RESTRICTED':
|
||||
return GuildNSFWLevel.AgeRestricted;
|
||||
default:
|
||||
return unknownKeyStrategy(key);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A string that can be resolved to a {@link GuildVerificationLevel} enum value. Here are the available types:
|
||||
* * NONE
|
||||
* * LOW
|
||||
* * MEDIUM
|
||||
* * HIGH
|
||||
* * VERY_HIGH
|
||||
* @typedef {string} GuildVerificationLevelEnumResolvable
|
||||
*/
|
||||
|
||||
/**
|
||||
* Resolves enum key to {@link GuildVerificationLevel} enum value
|
||||
* @param {GuildVerificationLevelEnumResolvable|GuildVerificationLevel} key The key to lookup
|
||||
* @returns {GuildVerificationLevel}
|
||||
*/
|
||||
static resolveGuildVerificationLevel(key) {
|
||||
switch (key) {
|
||||
case 'NONE':
|
||||
return GuildVerificationLevel.None;
|
||||
case 'LOW':
|
||||
return GuildVerificationLevel.Low;
|
||||
case 'MEDIUM':
|
||||
return GuildVerificationLevel.Medium;
|
||||
case 'HIGH':
|
||||
return GuildVerificationLevel.High;
|
||||
case 'VERY_HIGH':
|
||||
return GuildVerificationLevel.VeryHigh;
|
||||
default:
|
||||
return unknownKeyStrategy(key);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A string that can be resolved to a {@link GuildDefaultMessageNotifications} enum value.
|
||||
* Here are the available types:
|
||||
* * ALL_MESSAGES
|
||||
* * ONLY_MENTIONS
|
||||
* @typedef {string} GuildDefaultMessageNotificationsEnumResolvable
|
||||
*/
|
||||
|
||||
/**
|
||||
* Resolves enum key to {@link GuildDefaultMessageNotifications} enum value
|
||||
* @param {GuildDefaultMessageNotificationsEnumResolvable|GuildDefaultMessageNotifications} key The key to lookup
|
||||
* @returns {GuildDefaultMessageNotifications}
|
||||
*/
|
||||
static resolveGuildDefaultMessageNotifications(key) {
|
||||
switch (key) {
|
||||
case 'ALL_MESSAGES':
|
||||
return GuildDefaultMessageNotifications.AllMessages;
|
||||
case 'ONLY_MENTIONS':
|
||||
return GuildDefaultMessageNotifications.OnlyMentions;
|
||||
default:
|
||||
return unknownKeyStrategy(key);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A string that can be resolved to a {@link GuildExplicitContentFilter} enum value. Here are the available types:
|
||||
* * DISABLED
|
||||
* * MEMBERS_WITHOUT_ROLES
|
||||
* * ALL_MEMBERS
|
||||
* @typedef {string} GuildExplicitContentFilterEnumResolvable
|
||||
*/
|
||||
|
||||
/**
|
||||
* Resolves enum key to {@link GuildExplicitContentFilter} enum value
|
||||
* @param {GuildExplicitContentFilterEnumResolvable|GuildExplicitContentFilter} key The key to lookup
|
||||
* @returns {GuildExplicitContentFilter}
|
||||
*/
|
||||
static resolveGuildExplicitContentFilter(key) {
|
||||
switch (key) {
|
||||
case 'DISABLED':
|
||||
return GuildExplicitContentFilter.Disabled;
|
||||
case 'MEMBERS_WITHOUT_ROLES':
|
||||
return GuildExplicitContentFilter.MembersWithoutRoles;
|
||||
case 'ALL_MEMBERS':
|
||||
return GuildExplicitContentFilter.AllMembers;
|
||||
default:
|
||||
return unknownKeyStrategy(key);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A string that can be resolved to a {@link GuildPremiumTier} enum value. Here are the available types:
|
||||
* * NONE
|
||||
* * TIER_1
|
||||
* * TIER_2
|
||||
* * TIER_3
|
||||
* @typedef {string} GuildPremiumTierEnumResolvable
|
||||
*/
|
||||
|
||||
/**
|
||||
* Resolves enum key to {@link GuildPremiumTier} enum value
|
||||
* @param {GuildPremiumTierEnumResolvable|GuildPremiumTier} key The key to lookup
|
||||
* @returns {GuildPremiumTier}
|
||||
*/
|
||||
static resolveGuildPremiumTier(key) {
|
||||
switch (key) {
|
||||
case 'NONE':
|
||||
return GuildPremiumTier.None;
|
||||
case 'TIER_1':
|
||||
return GuildPremiumTier.Tier1;
|
||||
case 'TIER_2':
|
||||
return GuildPremiumTier.Tier2;
|
||||
case 'TIER_3':
|
||||
return GuildPremiumTier.Tier3;
|
||||
default:
|
||||
return unknownKeyStrategy(key);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A string that can be resolved to a {@link GuildScheduledEventStatus} enum value. Here are the available types:
|
||||
* * SCHEDULED
|
||||
* * ACTIVE
|
||||
* * COMPLETED
|
||||
* * CANCELED
|
||||
* @typedef {string} GuildScheduledEventStatusEnumResolvable
|
||||
*/
|
||||
|
||||
/**
|
||||
* Resolves enum key to {@link GuildScheduledEventStatus} enum value
|
||||
* @param {GuildScheduledEventStatusEnumResolvable|GuildScheduledEventStatus} key The key to lookup
|
||||
* @returns {GuildScheduledEventStatus}
|
||||
*/
|
||||
static resolveGuildScheduledEventStatus(key) {
|
||||
switch (key) {
|
||||
case 'SCHEDULED':
|
||||
return GuildScheduledEventStatus.Scheduled;
|
||||
case 'ACTIVE':
|
||||
return GuildScheduledEventStatus.Active;
|
||||
case 'COMPLETED':
|
||||
return GuildScheduledEventStatus.Completed;
|
||||
case 'CANCELED':
|
||||
return GuildScheduledEventStatus.Canceled;
|
||||
default:
|
||||
return unknownKeyStrategy(key);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A string that can be resolved to a {@link StageInstancePrivacyLevel} enum value. Here are the available types:
|
||||
* * PUBLIC
|
||||
* * GUILD_ONLY
|
||||
* @typedef {string} StageInstancePrivacyLevelEnumResolvable
|
||||
*/
|
||||
|
||||
/**
|
||||
* Resolves enum key to {@link StageInstancePrivacyLevel} enum value
|
||||
* @param {StageInstancePrivacyLevelEnumResolvable|StageInstancePrivacyLevel} key The key to lookup
|
||||
* @returns {StageInstancePrivacyLevel}
|
||||
*/
|
||||
static resolveStageInstancePrivacyLevel(key) {
|
||||
switch (key) {
|
||||
case 'PUBLIC':
|
||||
return StageInstancePrivacyLevel.Public;
|
||||
case 'GUILD_ONLY':
|
||||
return StageInstancePrivacyLevel.GuildOnly;
|
||||
default:
|
||||
return unknownKeyStrategy(key);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A string that can be resolved to a {@link GuildMFALevel} enum value. Here are the available types:
|
||||
* * NONE
|
||||
* * ELEVATED
|
||||
* @typedef {string} GuildMFALevelEnumResolvable
|
||||
*/
|
||||
|
||||
/**
|
||||
* Resolves enum key to {@link GuildMFALevel} enum value
|
||||
* @param {GuildMFALevelEnumResolvable|GuildMFALevel} key The key to lookup
|
||||
* @returns {GuildMFALevel}
|
||||
*/
|
||||
static resolveGuildMFALevel(key) {
|
||||
switch (key) {
|
||||
case 'NONE':
|
||||
return GuildMFALevel.None;
|
||||
case 'ELEVATED':
|
||||
return GuildMFALevel.Elevated;
|
||||
default:
|
||||
return unknownKeyStrategy(key);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A string that can be resolved to a {@link TeamMemberMembershipState} enum value. Here are the available types:
|
||||
* * INVITED
|
||||
* * ACCEPTED
|
||||
* @typedef {string} TeamMemberMembershipStateEnumResolvable
|
||||
*/
|
||||
|
||||
/**
|
||||
* Resolves enum key to {@link TeamMemberMembershipState} enum value
|
||||
* @param {TeamMemberMembershipStateEnumResolvable|TeamMemberMembershipState} key The key to lookup
|
||||
* @returns {TeamMemberMembershipState}
|
||||
*/
|
||||
static resolveTeamMemberMembershipState(key) {
|
||||
switch (key) {
|
||||
case 'INVITED':
|
||||
return TeamMemberMembershipState.Invited;
|
||||
case 'ACCEPTED':
|
||||
return TeamMemberMembershipState.Accepted;
|
||||
default:
|
||||
return unknownKeyStrategy(key);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A string that can be resolved to a {@link GuildScheduledEventEntityType} enum value. Here are the available types:
|
||||
* * STAGE_INSTANCE
|
||||
* * VOICE
|
||||
* * EXTERNAL
|
||||
* @typedef {string} GuildScheduledEventEntityTypeEnumResolvable
|
||||
*/
|
||||
|
||||
/**
|
||||
* Resolves enum key to {@link GuildScheduledEventEntityType} enum value
|
||||
* @param {GuildScheduledEventEntityTypeEnumResolvable|GuildScheduledEventEntityType} key The key to lookup
|
||||
* @returns {GuildScheduledEventEntityType}
|
||||
*/
|
||||
static resolveGuildScheduledEventEntityType(key) {
|
||||
switch (key) {
|
||||
case 'STAGE_INSTANCE':
|
||||
return GuildScheduledEventEntityType.StageInstance;
|
||||
case 'VOICE':
|
||||
return GuildScheduledEventEntityType.Voice;
|
||||
case 'EXTERNAL':
|
||||
return GuildScheduledEventEntityType.External;
|
||||
default:
|
||||
return unknownKeyStrategy(key);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A string that can be resolved to a {@link IntegrationExpireBehavior} enum value. Here are the available types:
|
||||
* * REMOVE_ROLE
|
||||
* * KICK
|
||||
* @typedef {string} IntegrationExpireBehaviorEnumResolvable
|
||||
*/
|
||||
|
||||
/**
|
||||
* Resolves enum key to {@link IntegrationExpireBehavior} enum value
|
||||
* @param {IntegrationExpireBehaviorEnumResolvable|IntegrationExpireBehavior} key The key to lookup
|
||||
* @returns {IntegrationExpireBehavior}
|
||||
*/
|
||||
static resolveIntegrationExpireBehavior(key) {
|
||||
switch (key) {
|
||||
case 'REMOVE_ROLE':
|
||||
return IntegrationExpireBehavior.RemoveRole;
|
||||
case 'KICK':
|
||||
return IntegrationExpireBehavior.Kick;
|
||||
default:
|
||||
return unknownKeyStrategy(key);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A string that can be resolved to a {@link AuditLogEvent} enum value. Here are the available types:
|
||||
* * GUILD_UPDATE
|
||||
* * CHANNEL_CREATE
|
||||
* * CHANNEL_UPDATE
|
||||
* * CHANNEL_DELETE
|
||||
* * CHANNEL_OVERWRITE_CREATE
|
||||
* * CHANNEL_OVERWRITE_UPDATE
|
||||
* * CHANNEL_OVERWRITE_DELETE
|
||||
* * MEMBER_KICK
|
||||
* * MEMBER_PRUNE
|
||||
* * MEMBER_BAN_ADD
|
||||
* * MEMBER_BAN_REMOVE
|
||||
* * MEMBER_UPDATE
|
||||
* * MEMBER_ROLE_UPDATE
|
||||
* * MEMBER_MOVE
|
||||
* * MEMBER_DISCONNECT
|
||||
* * BOT_ADD
|
||||
* * ROLE_CREATE
|
||||
* * ROLE_UPDATE
|
||||
* * ROLE_DELETE
|
||||
* * INVITE_CREATE
|
||||
* * INVITE_UPDATE
|
||||
* * INVITE_DELETE
|
||||
* * WEBHOOK_CREATE
|
||||
* * WEBHOOK_UPDATE
|
||||
* * WEBHOOK_DELETE
|
||||
* * INTEGRATION_CREATE
|
||||
* * INTEGRATION_UPDATE
|
||||
* * INTEGRATION_DELETE
|
||||
* * STAGE_INSTANCE_CREATE
|
||||
* * STAGE_INSTANCE_UPDATE
|
||||
* * STAGE_INSTANCE_DELETE
|
||||
* * STICKER_CREATE
|
||||
* * STICKER_UPDATE
|
||||
* * STICKER_DELETE
|
||||
* * GUILD_SCHEDULED_EVENT_CREATE
|
||||
* * GUILD_SCHEDULED_EVENT_UPDATE
|
||||
* * GUILD_SCHEDULED_EVENT_DELETE
|
||||
* * THREAD_CREATE
|
||||
* * THREAD_UPDATE
|
||||
* * THREAD_DELETE
|
||||
* @typedef {string} AuditLogEventEnumResolvable
|
||||
*/
|
||||
|
||||
/**
|
||||
* Resolves enum key to {@link AuditLogEvent} enum value
|
||||
* @param {AuditLogEventEnumResolvable|AuditLogEvent} key The key to lookup
|
||||
* @returns {AuditLogEvent}
|
||||
*/
|
||||
static resolveAuditLogEvent(key) {
|
||||
switch (key) {
|
||||
case 'GUILD_UPDATE':
|
||||
return AuditLogEvent.GuildUpdate;
|
||||
case 'CHANNEL_CREATE':
|
||||
return AuditLogEvent.ChannelCreate;
|
||||
case 'CHANNEL_UPDATE':
|
||||
return AuditLogEvent.ChannelUpdate;
|
||||
case 'CHANNEL_DELETE':
|
||||
return AuditLogEvent.ChannelDelete;
|
||||
case 'CHANNEL_OVERWRITE_CREATE':
|
||||
return AuditLogEvent.ChannelOverwriteCreate;
|
||||
case 'CHANNEL_OVERWRITE_UPDATE':
|
||||
return AuditLogEvent.ChannelOverwriteUpdate;
|
||||
case 'CHANNEL_OVERWRITE_DELETE':
|
||||
return AuditLogEvent.ChannelOverwriteDelete;
|
||||
case 'MEMBER_KICK':
|
||||
return AuditLogEvent.MemberKick;
|
||||
case 'MEMBER_PRUNE':
|
||||
return AuditLogEvent.MemberPrune;
|
||||
case 'MEMBER_BAN_ADD':
|
||||
return AuditLogEvent.MemberBanAdd;
|
||||
case 'MEMBER_BAN_REMOVE':
|
||||
return AuditLogEvent.MemberBanRemove;
|
||||
case 'MEMBER_UPDATE':
|
||||
return AuditLogEvent.MemberUpdate;
|
||||
case 'MEMBER_ROLE_UPDATE':
|
||||
return AuditLogEvent.MemberRoleUpdate;
|
||||
case 'MEMBER_MOVE':
|
||||
return AuditLogEvent.MemberMove;
|
||||
case 'MEMBER_DISCONNECT':
|
||||
return AuditLogEvent.MemberDisconnect;
|
||||
case 'BOT_ADD':
|
||||
return AuditLogEvent.BotAdd;
|
||||
case 'ROLE_CREATE':
|
||||
return AuditLogEvent.RoleCreate;
|
||||
case 'ROLE_UPDATE':
|
||||
return AuditLogEvent.RoleUpdate;
|
||||
case 'ROLE_DELETE':
|
||||
return AuditLogEvent.RoleDelete;
|
||||
case 'INVITE_CREATE':
|
||||
return AuditLogEvent.InviteCreate;
|
||||
case 'INVITE_UPDATE':
|
||||
return AuditLogEvent.InviteUpdate;
|
||||
case 'INVITE_DELETE':
|
||||
return AuditLogEvent.InviteDelete;
|
||||
case 'WEBHOOK_CREATE':
|
||||
return AuditLogEvent.WebhookCreate;
|
||||
case 'WEBHOOK_UPDATE':
|
||||
return AuditLogEvent.WebhookUpdate;
|
||||
case 'WEBHOOK_DELETE':
|
||||
return AuditLogEvent.WebhookDelete;
|
||||
case 'EMOJI_CREATE':
|
||||
return AuditLogEvent.EmojiCreate;
|
||||
case 'EMOJI_UPDATE':
|
||||
return AuditLogEvent.EmojiUpdate;
|
||||
case 'EMOJI_DELETE':
|
||||
return AuditLogEvent.EmojiDelete;
|
||||
case 'MESSAGE_DELETE':
|
||||
return AuditLogEvent.MessageDelete;
|
||||
case 'MESSAGE_BULK_DELETE':
|
||||
return AuditLogEvent.MessageBulkDelete;
|
||||
case 'MESSAGE_PIN':
|
||||
return AuditLogEvent.MessagePin;
|
||||
case 'MESSAGE_UNPIN':
|
||||
return AuditLogEvent.MessageUnpin;
|
||||
case 'INTEGRATION_CREATE':
|
||||
return AuditLogEvent.IntegrationCreate;
|
||||
case 'INTEGRATION_UPDATE':
|
||||
return AuditLogEvent.IntegrationUpdate;
|
||||
case 'INTEGRATION_DELETE':
|
||||
return AuditLogEvent.IntegrationDelete;
|
||||
case 'STAGE_INSTANCE_CREATE':
|
||||
return AuditLogEvent.StageInstanceCreate;
|
||||
case 'STAGE_INSTANCE_UPDATE':
|
||||
return AuditLogEvent.StageInstanceUpdate;
|
||||
case 'STAGE_INSTANCE_DELETE':
|
||||
return AuditLogEvent.StageInstanceDelete;
|
||||
case 'STICKER_CREATE':
|
||||
return AuditLogEvent.StickerCreate;
|
||||
case 'STICKER_UPDATE':
|
||||
return AuditLogEvent.StickerUpdate;
|
||||
case 'STICKER_DELETE':
|
||||
return AuditLogEvent.StickerDelete;
|
||||
case 'GUILD_SCHEDULED_EVENT_CREATE':
|
||||
return AuditLogEvent.GuildScheduledEventCreate;
|
||||
case 'GUILD_SCHEDULED_EVENT_UPDATE':
|
||||
return AuditLogEvent.GuildScheduledEventUpdate;
|
||||
case 'GUILD_SCHEDULED_EVENT_DELETE':
|
||||
return AuditLogEvent.GuildScheduledEventDelete;
|
||||
case 'THREAD_CREATE':
|
||||
return AuditLogEvent.ThreadCreate;
|
||||
case 'THREAD_UPDATE':
|
||||
return AuditLogEvent.ThreadUpdate;
|
||||
case 'THREAD_DELETE':
|
||||
return AuditLogEvent.ThreadDelete;
|
||||
default:
|
||||
return unknownKeyStrategy(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Precondition logic wrapper
|
||||
function preconditioner(func) {
|
||||
return key => {
|
||||
if (typeof key !== 'string' && typeof key !== 'number') {
|
||||
throw new Error('Enum value must be string or number');
|
||||
}
|
||||
|
||||
if (typeof key === 'number') {
|
||||
return key;
|
||||
}
|
||||
|
||||
return func(key);
|
||||
};
|
||||
}
|
||||
|
||||
// Injects wrapper into class static methods.
|
||||
function applyPreconditioner(obj) {
|
||||
for (const name in Object.getOwnPropertyNames(obj)) {
|
||||
if (typeof obj[name] !== 'function') {
|
||||
return;
|
||||
}
|
||||
|
||||
obj[name] = preconditioner(obj[name]);
|
||||
}
|
||||
}
|
||||
|
||||
// Apply precondition logic
|
||||
applyPreconditioner(EnumResolvers);
|
||||
|
||||
module.exports = EnumResolvers;
|
13
src/util/Enums.js
Normal file
13
src/util/Enums.js
Normal file
@@ -0,0 +1,13 @@
|
||||
'use strict';
|
||||
|
||||
function createEnum(keys) {
|
||||
const obj = {};
|
||||
for (const [index, key] of keys.entries()) {
|
||||
if (key === null) continue;
|
||||
obj[key] = index;
|
||||
obj[index] = key;
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
module.exports = { createEnum };
|
72
src/util/Events.js
Normal file
72
src/util/Events.js
Normal file
@@ -0,0 +1,72 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
ClientReady: 'ready',
|
||||
GuildCreate: 'guildCreate',
|
||||
GuildDelete: 'guildDelete',
|
||||
GuildUpdate: 'guildUpdate',
|
||||
GuildUnavailable: 'guildUnavailable',
|
||||
GuildMemberAdd: 'guildMemberAdd',
|
||||
GuildMemberRemove: 'guildMemberRemove',
|
||||
GuildMemberUpdate: 'guildMemberUpdate',
|
||||
GuildMemberAvailable: 'guildMemberAvailable',
|
||||
GuildMembersChunk: 'guildMembersChunk',
|
||||
GuildIntegrationsUpdate: 'guildIntegrationsUpdate',
|
||||
GuildRoleCreate: 'roleCreate',
|
||||
GuildRoleDelete: 'roleDelete',
|
||||
InviteCreate: 'inviteCreate',
|
||||
InviteDelete: 'inviteDelete',
|
||||
GuildRoleUpdate: 'roleUpdate',
|
||||
GuildEmojiCreate: 'emojiCreate',
|
||||
GuildEmojiDelete: 'emojiDelete',
|
||||
GuildEmojiUpdate: 'emojiUpdate',
|
||||
GuildBanAdd: 'guildBanAdd',
|
||||
GuildBanRemove: 'guildBanRemove',
|
||||
ChannelCreate: 'channelCreate',
|
||||
ChannelDelete: 'channelDelete',
|
||||
ChannelUpdate: 'channelUpdate',
|
||||
ChannelPinsUpdate: 'channelPinsUpdate',
|
||||
MessageCreate: 'messageCreate',
|
||||
MessageDelete: 'messageDelete',
|
||||
MessageUpdate: 'messageUpdate',
|
||||
MessageBulkDelete: 'messageDeleteBulk',
|
||||
MessageReactionAdd: 'messageReactionAdd',
|
||||
MessageReactionRemove: 'messageReactionRemove',
|
||||
MessageReactionRemoveAll: 'messageReactionRemoveAll',
|
||||
MessageReactionRemoveEmoji: 'messageReactionRemoveEmoji',
|
||||
ThreadCreate: 'threadCreate',
|
||||
ThreadDelete: 'threadDelete',
|
||||
ThreadUpdate: 'threadUpdate',
|
||||
ThreadListSync: 'threadListSync',
|
||||
ThreadMemberUpdate: 'threadMemberUpdate',
|
||||
ThreadMembersUpdate: 'threadMembersUpdate',
|
||||
UserUpdate: 'userUpdate',
|
||||
PresenceUpdate: 'presenceUpdate',
|
||||
VoiceServerUpdate: 'voiceServerUpdate',
|
||||
VoiceStateUpdate: 'voiceStateUpdate',
|
||||
TypingStart: 'typingStart',
|
||||
WebhooksUpdate: 'webhookUpdate',
|
||||
InteractionCreate: 'interactionCreate',
|
||||
Error: 'error',
|
||||
Warn: 'warn',
|
||||
Debug: 'debug',
|
||||
CacheSweep: 'cacheSweep',
|
||||
ShardDisconnect: 'shardDisconnect',
|
||||
ShardError: 'shardError',
|
||||
ShardReconnecting: 'shardReconnecting',
|
||||
ShardReady: 'shardReady',
|
||||
ShardResume: 'shardResume',
|
||||
Invalidated: 'invalidated',
|
||||
Raw: 'raw',
|
||||
StageInstanceCreate: 'stageInstanceCreate',
|
||||
StageInstanceUpdate: 'stageInstanceUpdate',
|
||||
StageInstanceDelete: 'stageInstanceDelete',
|
||||
GuildStickerCreate: 'stickerCreate',
|
||||
GuildStickerDelete: 'stickerDelete',
|
||||
GuildStickerUpdate: 'stickerUpdate',
|
||||
GuildScheduledEventCreate: 'guildScheduledEventCreate',
|
||||
GuildScheduledEventUpdate: 'guildScheduledEventUpdate',
|
||||
GuildScheduledEventDelete: 'guildScheduledEventDelete',
|
||||
GuildScheduledEventUserAdd: 'guildScheduledEventUserAdd',
|
||||
GuildScheduledEventUserRemove: 'guildScheduledEventUserRemove',
|
||||
};
|
208
src/util/Formatters.js
Normal file
208
src/util/Formatters.js
Normal file
@@ -0,0 +1,208 @@
|
||||
'use strict';
|
||||
|
||||
const {
|
||||
blockQuote,
|
||||
bold,
|
||||
channelMention,
|
||||
codeBlock,
|
||||
formatEmoji,
|
||||
hideLinkEmbed,
|
||||
hyperlink,
|
||||
inlineCode,
|
||||
italic,
|
||||
memberNicknameMention,
|
||||
quote,
|
||||
roleMention,
|
||||
spoiler,
|
||||
strikethrough,
|
||||
time,
|
||||
TimestampStyles,
|
||||
underscore,
|
||||
userMention,
|
||||
} = require('@discordjs/builders');
|
||||
|
||||
/**
|
||||
* Contains various Discord-specific functions for formatting messages.
|
||||
*/
|
||||
class Formatters extends null {}
|
||||
|
||||
/**
|
||||
* Formats the content into a block quote. This needs to be at the start of the line for Discord to format it.
|
||||
* @method blockQuote
|
||||
* @memberof Formatters
|
||||
* @param {string} content The content to wrap.
|
||||
* @returns {string}
|
||||
*/
|
||||
Formatters.blockQuote = blockQuote;
|
||||
|
||||
/**
|
||||
* Formats the content into bold text.
|
||||
* @method bold
|
||||
* @memberof Formatters
|
||||
* @param {string} content The content to wrap.
|
||||
* @returns {string}
|
||||
*/
|
||||
Formatters.bold = bold;
|
||||
|
||||
/**
|
||||
* Formats a channel id into a channel mention.
|
||||
* @method channelMention
|
||||
* @memberof Formatters
|
||||
* @param {string} channelId The channel id to format.
|
||||
* @returns {string}
|
||||
*/
|
||||
Formatters.channelMention = channelMention;
|
||||
|
||||
/**
|
||||
* Wraps the content inside a code block with an optional language.
|
||||
* @method codeBlock
|
||||
* @memberof Formatters
|
||||
* @param {string} contentOrLanguage The language to use, content if a second parameter isn't provided.
|
||||
* @param {string} [content] The content to wrap.
|
||||
* @returns {string}
|
||||
*/
|
||||
Formatters.codeBlock = codeBlock;
|
||||
|
||||
/**
|
||||
* Formats an emoji id into a fully qualified emoji identifier
|
||||
* @method formatEmoji
|
||||
* @memberof Formatters
|
||||
* @param {string} emojiId The emoji id to format.
|
||||
* @param {boolean} [animated] Whether the emoji is animated or not. Defaults to `false`
|
||||
* @returns {string}
|
||||
*/
|
||||
Formatters.formatEmoji = formatEmoji;
|
||||
|
||||
/**
|
||||
* Wraps the URL into `<>`, which stops it from embedding.
|
||||
* @method hideLinkEmbed
|
||||
* @memberof Formatters
|
||||
* @param {string} content The content to wrap.
|
||||
* @returns {string}
|
||||
*/
|
||||
Formatters.hideLinkEmbed = hideLinkEmbed;
|
||||
|
||||
/**
|
||||
* Formats the content and the URL into a masked URL with an optional title.
|
||||
* @method hyperlink
|
||||
* @memberof Formatters
|
||||
* @param {string} content The content to display.
|
||||
* @param {string} url The URL the content links to.
|
||||
* @param {string} [title] The title shown when hovering on the masked link.
|
||||
* @returns {string}
|
||||
*/
|
||||
Formatters.hyperlink = hyperlink;
|
||||
|
||||
/**
|
||||
* Wraps the content inside \`backticks\`, which formats it as inline code.
|
||||
* @method inlineCode
|
||||
* @memberof Formatters
|
||||
* @param {string} content The content to wrap.
|
||||
* @returns {string}
|
||||
*/
|
||||
Formatters.inlineCode = inlineCode;
|
||||
|
||||
/**
|
||||
* Formats the content into italic text.
|
||||
* @method italic
|
||||
* @memberof Formatters
|
||||
* @param {string} content The content to wrap.
|
||||
* @returns {string}
|
||||
*/
|
||||
Formatters.italic = italic;
|
||||
|
||||
/**
|
||||
* Formats a user id into a member-nickname mention.
|
||||
* @method memberNicknameMention
|
||||
* @memberof Formatters
|
||||
* @param {string} memberId The user id to format.
|
||||
* @returns {string}
|
||||
*/
|
||||
Formatters.memberNicknameMention = memberNicknameMention;
|
||||
|
||||
/**
|
||||
* Formats the content into a quote. This needs to be at the start of the line for Discord to format it.
|
||||
* @method quote
|
||||
* @memberof Formatters
|
||||
* @param {string} content The content to wrap.
|
||||
* @returns {string}
|
||||
*/
|
||||
Formatters.quote = quote;
|
||||
|
||||
/**
|
||||
* Formats a role id into a role mention.
|
||||
* @method roleMention
|
||||
* @memberof Formatters
|
||||
* @param {string} roleId The role id to format.
|
||||
* @returns {string}
|
||||
*/
|
||||
Formatters.roleMention = roleMention;
|
||||
|
||||
/**
|
||||
* Formats the content into spoiler text.
|
||||
* @method spoiler
|
||||
* @memberof Formatters
|
||||
* @param {string} content The content to spoiler.
|
||||
* @returns {string}
|
||||
*/
|
||||
Formatters.spoiler = spoiler;
|
||||
|
||||
/**
|
||||
* Formats the content into strike-through text.
|
||||
* @method strikethrough
|
||||
* @memberof Formatters
|
||||
* @param {string} content The content to wrap.
|
||||
* @returns {string}
|
||||
*/
|
||||
Formatters.strikethrough = strikethrough;
|
||||
|
||||
/**
|
||||
* Formats a date into a short date-time string.
|
||||
* @method time
|
||||
* @memberof Formatters
|
||||
* @param {number|Date} [date] The date to format.
|
||||
* @param {TimestampStylesString} [style] The style to use.
|
||||
* @returns {string}
|
||||
*/
|
||||
Formatters.time = time;
|
||||
|
||||
/**
|
||||
* A message formatting timestamp style, as defined in
|
||||
* [here](https://discord.com/developers/docs/reference#message-formatting-timestamp-styles).
|
||||
* * `t` Short time format, consisting of hours and minutes, e.g. 16:20.
|
||||
* * `T` Long time format, consisting of hours, minutes, and seconds, e.g. 16:20:30.
|
||||
* * `d` Short date format, consisting of day, month, and year, e.g. 20/04/2021.
|
||||
* * `D` Long date format, consisting of day, month, and year, e.g. 20 April 2021.
|
||||
* * `f` Short date-time format, consisting of short date and short time formats, e.g. 20 April 2021 16:20.
|
||||
* * `F` Long date-time format, consisting of long date and short time formats, e.g. Tuesday, 20 April 2021 16:20.
|
||||
* * `R` Relative time format, consisting of a relative duration format, e.g. 2 months ago.
|
||||
* @typedef {string} TimestampStylesString
|
||||
*/
|
||||
|
||||
/**
|
||||
* The message formatting timestamp
|
||||
* [styles](https://discord.com/developers/docs/reference#message-formatting-timestamp-styles) supported by Discord.
|
||||
* @memberof Formatters
|
||||
* @type {Object<string, TimestampStylesString>}
|
||||
*/
|
||||
Formatters.TimestampStyles = TimestampStyles;
|
||||
|
||||
/**
|
||||
* Formats the content into underscored text.
|
||||
* @method underscore
|
||||
* @memberof Formatters
|
||||
* @param {string} content The content to wrap.
|
||||
* @returns {string}
|
||||
*/
|
||||
Formatters.underscore = underscore;
|
||||
|
||||
/**
|
||||
* Formats a user id into a user mention.
|
||||
* @method userMention
|
||||
* @memberof Formatters
|
||||
* @param {string} userId The user id to format.
|
||||
* @returns {string}
|
||||
*/
|
||||
Formatters.userMention = userMention;
|
||||
|
||||
module.exports = Formatters;
|
33
src/util/IntentsBitField.js
Normal file
33
src/util/IntentsBitField.js
Normal file
@@ -0,0 +1,33 @@
|
||||
'use strict';
|
||||
const { GatewayIntentBits } = require('discord-api-types/v9');
|
||||
const BitField = require('./BitField');
|
||||
|
||||
/**
|
||||
* Data structure that makes it easy to calculate intents.
|
||||
* @extends {BitField}
|
||||
*/
|
||||
class IntentsBitField extends BitField {}
|
||||
|
||||
/**
|
||||
* @name IntentsBitField
|
||||
* @kind constructor
|
||||
* @memberof IntentsBitField
|
||||
* @param {IntentsResolvable} [bits=0] Bit(s) to read from
|
||||
*/
|
||||
|
||||
/**
|
||||
* Data that can be resolved to give a permission number. This can be:
|
||||
* * A string (see {@link IntentsBitField.Flags})
|
||||
* * An intents flag
|
||||
* * An instance of {@link IntentsBitField}
|
||||
* * An array of IntentsResolvable
|
||||
* @typedef {string|number|IntentsBitField|IntentsResolvable[]} IntentsResolvable
|
||||
*/
|
||||
|
||||
/**
|
||||
* Numeric WebSocket intents
|
||||
* @type {GatewayIntentBits}
|
||||
*/
|
||||
IntentsBitField.Flags = GatewayIntentBits;
|
||||
|
||||
module.exports = IntentsBitField;
|
68
src/util/LimitedCollection.js
Normal file
68
src/util/LimitedCollection.js
Normal file
@@ -0,0 +1,68 @@
|
||||
'use strict';
|
||||
|
||||
const { Collection } = require('@discordjs/collection');
|
||||
const { TypeError } = require('../errors/DJSError.js');
|
||||
|
||||
/**
|
||||
* Options for defining the behavior of a LimitedCollection
|
||||
* @typedef {Object} LimitedCollectionOptions
|
||||
* @property {?number} [maxSize=Infinity] The maximum size of the Collection
|
||||
* @property {?Function} [keepOverLimit=null] A function, which is passed the value and key of an entry, ran to decide
|
||||
* to keep an entry past the maximum size
|
||||
*/
|
||||
|
||||
/**
|
||||
* A Collection which holds a max amount of entries.
|
||||
* @extends {Collection}
|
||||
* @param {LimitedCollectionOptions} [options={}] Options for constructing the Collection.
|
||||
* @param {Iterable} [iterable=null] Optional entries passed to the Map constructor.
|
||||
*/
|
||||
class LimitedCollection extends Collection {
|
||||
constructor(options = {}, iterable) {
|
||||
if (typeof options !== 'object' || options === null) {
|
||||
throw new TypeError('INVALID_TYPE', 'options', 'object', true);
|
||||
}
|
||||
const { maxSize = Infinity, keepOverLimit = null } = options;
|
||||
|
||||
if (typeof maxSize !== 'number') {
|
||||
throw new TypeError('INVALID_TYPE', 'maxSize', 'number');
|
||||
}
|
||||
if (keepOverLimit !== null && typeof keepOverLimit !== 'function') {
|
||||
throw new TypeError('INVALID_TYPE', 'keepOverLimit', 'function');
|
||||
}
|
||||
|
||||
super(iterable);
|
||||
|
||||
/**
|
||||
* The max size of the Collection.
|
||||
* @type {number}
|
||||
*/
|
||||
this.maxSize = maxSize;
|
||||
|
||||
/**
|
||||
* A function called to check if an entry should be kept when the Collection is at max size.
|
||||
* @type {?Function}
|
||||
*/
|
||||
this.keepOverLimit = keepOverLimit;
|
||||
}
|
||||
|
||||
set(key, value) {
|
||||
if (this.maxSize === 0) return this;
|
||||
if (this.size >= this.maxSize && !this.has(key)) {
|
||||
for (const [k, v] of this.entries()) {
|
||||
const keep = this.keepOverLimit?.(v, k, this) ?? false;
|
||||
if (!keep) {
|
||||
this.delete(k);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return super.set(key, value);
|
||||
}
|
||||
|
||||
static get [Symbol.species]() {
|
||||
return Collection;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = LimitedCollection;
|
31
src/util/MessageFlagsBitField.js
Normal file
31
src/util/MessageFlagsBitField.js
Normal file
@@ -0,0 +1,31 @@
|
||||
'use strict';
|
||||
|
||||
const { MessageFlags } = require('discord-api-types/v9');
|
||||
const BitField = require('./BitField');
|
||||
|
||||
/**
|
||||
* Data structure that makes it easy to interact with a {@link Message#flags} bitfield.
|
||||
* @extends {BitField}
|
||||
*/
|
||||
class MessageFlagsBitField extends BitField {}
|
||||
|
||||
/**
|
||||
* @name MessageFlagsBitField
|
||||
* @kind constructor
|
||||
* @memberof MessageFlagsBitField
|
||||
* @param {BitFieldResolvable} [bits=0] Bit(s) to read from
|
||||
*/
|
||||
|
||||
/**
|
||||
* Bitfield of the packed bits
|
||||
* @type {number}
|
||||
* @name MessageFlagsBitField#bitfield
|
||||
*/
|
||||
|
||||
/**
|
||||
* Numeric message flags.
|
||||
* @type {MessageFlags}
|
||||
*/
|
||||
MessageFlagsBitField.Flags = MessageFlags;
|
||||
|
||||
module.exports = MessageFlagsBitField;
|
220
src/util/Options.js
Normal file
220
src/util/Options.js
Normal file
@@ -0,0 +1,220 @@
|
||||
'use strict';
|
||||
|
||||
const process = require('node:process');
|
||||
const Transformers = require('./Transformers');
|
||||
|
||||
/**
|
||||
* @typedef {Function} CacheFactory
|
||||
* @param {Function} manager The manager class the cache is being requested from.
|
||||
* @param {Function} holds The class that the cache will hold.
|
||||
* @returns {Collection} A Collection used to store the cache of the manager.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Options for a client.
|
||||
* @typedef {Object} ClientOptions
|
||||
* @property {number|number[]|string} [shards] The shard's id to run, or an array of shard ids. If not specified,
|
||||
* the client will spawn {@link ClientOptions#shardCount} shards. If set to `auto`, it will fetch the
|
||||
* recommended amount of shards from Discord and spawn that amount
|
||||
* @property {number} [shardCount=1] The total amount of shards used by all processes of this bot
|
||||
* (e.g. recommended shard count, shard count of the ShardingManager)
|
||||
* @property {CacheFactory} [makeCache] Function to create a cache.
|
||||
* You can use your own function, or the {@link Options} class to customize the Collection used for the cache.
|
||||
* <warn>Overriding the cache used in `GuildManager`, `ChannelManager`, `GuildChannelManager`, `RoleManager`,
|
||||
* and `PermissionOverwriteManager` is unsupported and **will** break functionality</warn>
|
||||
* @property {MessageMentionOptions} [allowedMentions] Default value for {@link MessageOptions#allowedMentions}
|
||||
* @property {Partials[]} [partials] Structures allowed to be partial. This means events can be emitted even when
|
||||
* they're missing all the data for a particular structure. See the "Partial Structures" topic on the
|
||||
* [guide](https://discordjs.guide/popular-topics/partials.html) for some
|
||||
* important usage information, as partials require you to put checks in place when handling data.
|
||||
* @property {boolean} [failIfNotExists=true] Default value for {@link ReplyMessageOptions#failIfNotExists}
|
||||
* @property {PresenceData} [presence={}] Presence data to use upon login
|
||||
* @property {IntentsResolvable} intents Intents to enable for this connection
|
||||
* @property {number} [waitGuildTimeout=15_000] Time in milliseconds that Clients with the GUILDS intent should wait for
|
||||
* missing guilds to be received before starting the bot. If not specified, the default is 15 seconds.
|
||||
* @property {SweeperOptions} [sweepers={}] Options for cache sweeping
|
||||
* @property {WebsocketOptions} [ws] Options for the WebSocket
|
||||
* @property {RESTOptions} [rest] Options for the REST manager
|
||||
* @property {Function} [jsonTransformer] A function used to transform outgoing json data
|
||||
*/
|
||||
|
||||
/**
|
||||
* Options for {@link Sweepers} defining the behavior of cache sweeping
|
||||
* @typedef {Object<SweeperKey, SweepOptions>} SweeperOptions
|
||||
*/
|
||||
|
||||
/**
|
||||
* Options for sweeping a single type of item from cache
|
||||
* @typedef {Object} SweepOptions
|
||||
* @property {number} interval The interval (in seconds) at which to perform sweeping of the item
|
||||
* @property {number} [lifetime] How long an item should stay in cache until it is considered sweepable.
|
||||
* <warn>This property is only valid for the `invites`, `messages`, and `threads` keys. The `filter` property
|
||||
* is mutually exclusive to this property and takes priority</warn>
|
||||
* @property {GlobalSweepFilter} filter The function used to determine the function passed to the sweep method
|
||||
* <info>This property is optional when the key is `invites`, `messages`, or `threads` and `lifetime` is set</info>
|
||||
*/
|
||||
|
||||
/**
|
||||
* WebSocket options (these are left as snake_case to match the API)
|
||||
* @typedef {Object} WebsocketOptions
|
||||
* @property {number} [large_threshold=50] Number of members in a guild after which offline users will no longer be
|
||||
* sent in the initial guild member list, must be between 50 and 250
|
||||
*/
|
||||
|
||||
/**
|
||||
* Contains various utilities for client options.
|
||||
*/
|
||||
class Options extends null {
|
||||
/**
|
||||
* The default client options.
|
||||
* @returns {ClientOptions}
|
||||
*/
|
||||
static createDefault() {
|
||||
return {
|
||||
waitGuildTimeout: 15_000,
|
||||
shardCount: 1,
|
||||
makeCache: this.cacheWithLimits(this.defaultMakeCacheSettings),
|
||||
messageCacheLifetime: 0,
|
||||
messageSweepInterval: 0,
|
||||
invalidRequestWarningInterval: 0,
|
||||
intents: 32767,
|
||||
partials: [],
|
||||
restWsBridgeTimeout: 5_000,
|
||||
restRequestTimeout: 15_000,
|
||||
restGlobalRateLimit: 0,
|
||||
retryLimit: 1,
|
||||
restTimeOffset: 500,
|
||||
restSweepInterval: 60,
|
||||
failIfNotExists: true,
|
||||
userAgentSuffix: [],
|
||||
presence: {},
|
||||
sweepers: {},
|
||||
ws: {
|
||||
large_threshold: 50,
|
||||
compress: false,
|
||||
properties: {
|
||||
$os: 'iPhone14,5',
|
||||
$browser: 'Discord iOS',
|
||||
$device: 'iPhone14,5 OS 15.2',
|
||||
},
|
||||
version: 9,
|
||||
},
|
||||
http: {
|
||||
headers: {
|
||||
"Accept": "*/*",
|
||||
"Accept-Encoding": "gzip, deflate, br",
|
||||
"Accept-Language": 'en-US,en;q=0.9',
|
||||
"Cache-Control": "no-cache",
|
||||
"Pragma": "no-cache",
|
||||
"Referer": "https://discord.com/channels/@me",
|
||||
"Sec-Ch-Ua": '" Not A;Brand";v="99" "',
|
||||
"Sec-Ch-Ua-Mobile": '?0',
|
||||
"Sec-Ch-Ua-Platform": '"iOS"',
|
||||
"Sec-Fetch-Dest": "empty",
|
||||
"Sec-Fetch-Mode": "cors",
|
||||
"Sec-Fetch-Site": "same-origin",
|
||||
"X-Debug-Options": "bugReporterEnabled",
|
||||
"X-Discord-Locale": 'en-US',
|
||||
"Origin": "https://discord.com"
|
||||
},
|
||||
agent: {},
|
||||
version: 9,
|
||||
api: 'https://discord.com/api',
|
||||
cdn: 'https://cdn.discordapp.com',
|
||||
invite: 'https://discord.gg',
|
||||
template: 'https://discord.new',
|
||||
scheduledEvent: 'https://discord.com/events',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a cache factory using predefined settings to sweep or limit.
|
||||
* @param {Object<string, LimitedCollectionOptions|number>} [settings={}] Settings passed to the relevant constructor.
|
||||
* If no setting is provided for a manager, it uses Collection.
|
||||
* If a number is provided for a manager, it uses that number as the max size for a LimitedCollection.
|
||||
* If LimitedCollectionOptions are provided for a manager, it uses those settings to form a LimitedCollection.
|
||||
* @returns {CacheFactory}
|
||||
* @example
|
||||
* // Store up to 200 messages per channel and 200 members per guild, always keeping the client member.
|
||||
* Options.cacheWithLimits({
|
||||
* MessageManager: 200,
|
||||
* GuildMemberManager: {
|
||||
* maxSize: 200,
|
||||
* keepOverLimit: (member) => member.id === client.user.id,
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
static cacheWithLimits(settings = {}) {
|
||||
const { Collection } = require('@discordjs/collection');
|
||||
const LimitedCollection = require('./LimitedCollection');
|
||||
|
||||
return manager => {
|
||||
const setting = settings[manager.name];
|
||||
/* eslint-disable-next-line eqeqeq */
|
||||
if (setting == null) {
|
||||
return new Collection();
|
||||
}
|
||||
if (typeof setting === 'number') {
|
||||
if (setting === Infinity) {
|
||||
return new Collection();
|
||||
}
|
||||
return new LimitedCollection({ maxSize: setting });
|
||||
}
|
||||
/* eslint-disable-next-line eqeqeq */
|
||||
const noLimit = setting.maxSize == null || setting.maxSize === Infinity;
|
||||
if (noLimit) {
|
||||
return new Collection();
|
||||
}
|
||||
return new LimitedCollection(setting);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a cache factory that always caches everything.
|
||||
* @returns {CacheFactory}
|
||||
*/
|
||||
static cacheEverything() {
|
||||
const { Collection } = require('@discordjs/collection');
|
||||
return () => new Collection();
|
||||
}
|
||||
|
||||
/**
|
||||
* The default settings passed to {@link Options.cacheWithLimits}.
|
||||
* The caches that this changes are:
|
||||
* * `MessageManager` - Limit to 200 messages
|
||||
* * `ChannelManager` - Sweep archived threads
|
||||
* * `GuildChannelManager` - Sweep archived threads
|
||||
* * `ThreadManager` - Sweep archived threads
|
||||
* <info>If you want to keep default behavior and add on top of it you can use this object and add on to it, e.g.
|
||||
* `makeCache: Options.cacheWithLimits({ ...Options.defaultMakeCacheSettings, ReactionManager: 0 })`</info>
|
||||
* @type {Object<string, LimitedCollectionOptions|number>}
|
||||
*/
|
||||
static get defaultMakeCacheSettings() {
|
||||
return {
|
||||
MessageManager: 200,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The default settings passed to {@link Options.sweepers} (for v14).
|
||||
* The sweepers that this changes are:
|
||||
* * `threads` - Sweep archived threads every hour, removing those archived more than 4 hours ago
|
||||
* <info>If you want to keep default behavior and add on top of it you can use this object and add on to it, e.g.
|
||||
* `sweepers: { ...Options.defaultSweeperSettings, messages: { interval: 300, lifetime: 600 } })`</info>
|
||||
* @type {SweeperOptions}
|
||||
*/
|
||||
Options.defaultSweeperSettings = {
|
||||
threads: {
|
||||
interval: 3600,
|
||||
lifetime: 14400,
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = Options;
|
||||
|
||||
/**
|
||||
* @external RESTOptions
|
||||
* @see {@link https://discord.js.org/#/docs/rest/main/typedef/RESTOptions}
|
||||
*/
|
5
src/util/Partials.js
Normal file
5
src/util/Partials.js
Normal file
@@ -0,0 +1,5 @@
|
||||
'use strict';
|
||||
|
||||
const { createEnum } = require('./Enums');
|
||||
|
||||
module.exports = createEnum(['User', 'Channel', 'GuildMember', 'Message', 'Reaction', 'GuildScheduledEvent']);
|
95
src/util/PermissionsBitField.js
Normal file
95
src/util/PermissionsBitField.js
Normal file
@@ -0,0 +1,95 @@
|
||||
'use strict';
|
||||
|
||||
const { PermissionFlagsBits } = require('discord-api-types/v9');
|
||||
const BitField = require('./BitField');
|
||||
|
||||
/**
|
||||
* Data structure that makes it easy to interact with a permission bitfield. All {@link GuildMember}s have a set of
|
||||
* permissions in their guild, and each channel in the guild may also have {@link PermissionOverwrites} for the member
|
||||
* that override their default permissions.
|
||||
* @extends {BitField}
|
||||
*/
|
||||
class PermissionsBitField extends BitField {
|
||||
/**
|
||||
* Bitfield of the packed bits
|
||||
* @type {bigint}
|
||||
* @name Permissions#bitfield
|
||||
*/
|
||||
|
||||
/**
|
||||
* Data that can be resolved to give a permission number. This can be:
|
||||
* * A string (see {@link PermissionsBitField.Flags})
|
||||
* * A permission number
|
||||
* * An instance of {@link PermissionsBitField}
|
||||
* * An Array of PermissionResolvable
|
||||
* @typedef {string|bigint|PermissionsBitField|PermissionResolvable[]} PermissionResolvable
|
||||
*/
|
||||
|
||||
/**
|
||||
* Gets all given bits that are missing from the bitfield.
|
||||
* @param {BitFieldResolvable} bits Bit(s) to check for
|
||||
* @param {boolean} [checkAdmin=true] Whether to allow the administrator permission to override
|
||||
* @returns {string[]}
|
||||
*/
|
||||
missing(bits, checkAdmin = true) {
|
||||
return checkAdmin && this.has(PermissionFlagsBits.Administrator) ? [] : super.missing(bits);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the bitfield has a permission, or any of multiple permissions.
|
||||
* @param {PermissionResolvable} permission Permission(s) to check for
|
||||
* @param {boolean} [checkAdmin=true] Whether to allow the administrator permission to override
|
||||
* @returns {boolean}
|
||||
*/
|
||||
any(permission, checkAdmin = true) {
|
||||
return (checkAdmin && super.has(PermissionFlagsBits.Administrator)) || super.any(permission);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the bitfield has a permission, or multiple permissions.
|
||||
* @param {PermissionResolvable} permission Permission(s) to check for
|
||||
* @param {boolean} [checkAdmin=true] Whether to allow the administrator permission to override
|
||||
* @returns {boolean}
|
||||
*/
|
||||
has(permission, checkAdmin = true) {
|
||||
return (checkAdmin && super.has(PermissionFlagsBits.Administrator)) || super.has(permission);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets an {@link Array} of bitfield names based on the permissions available.
|
||||
* @returns {string[]}
|
||||
*/
|
||||
toArray() {
|
||||
return super.toArray(false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Numeric permission flags.
|
||||
* @type {PermissionFlagsBits}
|
||||
* @see {@link https://discord.com/developers/docs/topics/permissions#permissions-bitwise-permission-flags}
|
||||
*/
|
||||
PermissionsBitField.Flags = PermissionFlagsBits;
|
||||
|
||||
/**
|
||||
* Bitfield representing every permission combined
|
||||
* @type {bigint}
|
||||
*/
|
||||
PermissionsBitField.All = Object.values(PermissionFlagsBits).reduce((all, p) => all | p, 0n);
|
||||
|
||||
/**
|
||||
* Bitfield representing the default permissions for users
|
||||
* @type {bigint}
|
||||
*/
|
||||
PermissionsBitField.Default = BigInt(104324673);
|
||||
|
||||
/**
|
||||
* Bitfield representing the permissions required for moderators of stage channels
|
||||
* @type {bigint}
|
||||
*/
|
||||
PermissionsBitField.StageModerator =
|
||||
PermissionFlagsBits.ManageChannels | PermissionFlagsBits.MuteMembers | PermissionFlagsBits.MoveMembers;
|
||||
|
||||
PermissionsBitField.defaultBit = BigInt(0);
|
||||
|
||||
module.exports = PermissionsBitField;
|
10
src/util/ShardEvents.js
Normal file
10
src/util/ShardEvents.js
Normal file
@@ -0,0 +1,10 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
Close: 'close',
|
||||
Destroyed: 'destroyed',
|
||||
InvalidSession: 'invalidSession',
|
||||
Ready: 'ready',
|
||||
Resumed: 'resumed',
|
||||
AllReady: 'allReady',
|
||||
};
|
15
src/util/Status.js
Normal file
15
src/util/Status.js
Normal file
@@ -0,0 +1,15 @@
|
||||
'use strict';
|
||||
|
||||
const { createEnum } = require('./Enums');
|
||||
|
||||
module.exports = createEnum([
|
||||
'Ready',
|
||||
'Connecting',
|
||||
'Reconnecting',
|
||||
'Idle',
|
||||
'Nearly',
|
||||
'Disconnected',
|
||||
'WaitingForGuilds',
|
||||
'Identifying',
|
||||
'Resuming',
|
||||
]);
|
457
src/util/Sweepers.js
Normal file
457
src/util/Sweepers.js
Normal file
@@ -0,0 +1,457 @@
|
||||
'use strict';
|
||||
|
||||
const { setInterval, clearInterval } = require('node:timers');
|
||||
const { ThreadChannelTypes, SweeperKeys } = require('./Constants');
|
||||
const Events = require('./Events');
|
||||
const { TypeError } = require('../errors/DJSError.js');
|
||||
|
||||
/**
|
||||
* @typedef {Function} GlobalSweepFilter
|
||||
* @returns {Function|null} Return `null` to skip sweeping, otherwise a function passed to `sweep()`,
|
||||
* See {@link [Collection#sweep](https://discord.js.org/#/docs/collection/main/class/Collection?scrollTo=sweep)}
|
||||
* for the definition of this function.
|
||||
*/
|
||||
|
||||
/**
|
||||
* A container for all cache sweeping intervals and their associated sweep methods.
|
||||
*/
|
||||
class Sweepers {
|
||||
constructor(client, options) {
|
||||
/**
|
||||
* The client that instantiated this
|
||||
* @type {Client}
|
||||
* @readonly
|
||||
*/
|
||||
Object.defineProperty(this, 'client', { value: client });
|
||||
|
||||
/**
|
||||
* The options the sweepers were instantiated with
|
||||
* @type {SweeperOptions}
|
||||
*/
|
||||
this.options = options;
|
||||
|
||||
/**
|
||||
* A record of interval timeout that is used to sweep the indicated items, or null if not being swept
|
||||
* @type {Object<SweeperKey, ?Timeout>}
|
||||
*/
|
||||
this.intervals = Object.fromEntries(SweeperKeys.map(key => [key, null]));
|
||||
|
||||
for (const key of SweeperKeys) {
|
||||
if (!(key in options)) continue;
|
||||
|
||||
this._validateProperties(key);
|
||||
|
||||
const clonedOptions = { ...this.options[key] };
|
||||
|
||||
// Handle cases that have a "lifetime"
|
||||
if (!('filter' in clonedOptions)) {
|
||||
switch (key) {
|
||||
case 'invites':
|
||||
clonedOptions.filter = this.constructor.expiredInviteSweepFilter(clonedOptions.lifetime);
|
||||
break;
|
||||
case 'messages':
|
||||
clonedOptions.filter = this.constructor.outdatedMessageSweepFilter(clonedOptions.lifetime);
|
||||
break;
|
||||
case 'threads':
|
||||
clonedOptions.filter = this.constructor.archivedThreadSweepFilter(clonedOptions.lifetime);
|
||||
}
|
||||
}
|
||||
|
||||
this._initInterval(key, `sweep${key[0].toUpperCase()}${key.slice(1)}`, clonedOptions);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sweeps all guild and global application commands and removes the ones which are indicated by the filter.
|
||||
* @param {Function} filter The function used to determine which commands will be removed from the caches.
|
||||
* @returns {number} Amount of commands that were removed from the caches
|
||||
*/
|
||||
sweepApplicationCommands(filter) {
|
||||
const { guilds, items: guildCommands } = this._sweepGuildDirectProp('commands', filter, { emit: false });
|
||||
|
||||
const globalCommands = this.client.application?.commands.cache.sweep(filter) ?? 0;
|
||||
|
||||
this.client.emit(
|
||||
Events.CacheSweep,
|
||||
`Swept ${globalCommands} global application commands and ${guildCommands} guild commands in ${guilds} guilds.`,
|
||||
);
|
||||
return guildCommands + globalCommands;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sweeps all guild bans and removes the ones which are indicated by the filter.
|
||||
* @param {Function} filter The function used to determine which bans will be removed from the caches.
|
||||
* @returns {number} Amount of bans that were removed from the caches
|
||||
*/
|
||||
sweepBans(filter) {
|
||||
return this._sweepGuildDirectProp('bans', filter).items;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sweeps all guild emojis and removes the ones which are indicated by the filter.
|
||||
* @param {Function} filter The function used to determine which emojis will be removed from the caches.
|
||||
* @returns {number} Amount of emojis that were removed from the caches
|
||||
*/
|
||||
sweepEmojis(filter) {
|
||||
return this._sweepGuildDirectProp('emojis', filter).items;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sweeps all guild invites and removes the ones which are indicated by the filter.
|
||||
* @param {Function} filter The function used to determine which invites will be removed from the caches.
|
||||
* @returns {number} Amount of invites that were removed from the caches
|
||||
*/
|
||||
sweepInvites(filter) {
|
||||
return this._sweepGuildDirectProp('invites', filter).items;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sweeps all guild members and removes the ones which are indicated by the filter.
|
||||
* <info>It is highly recommended to keep the client guild member cached</info>
|
||||
* @param {Function} filter The function used to determine which guild members will be removed from the caches.
|
||||
* @returns {number} Amount of guild members that were removed from the caches
|
||||
*/
|
||||
sweepGuildMembers(filter) {
|
||||
return this._sweepGuildDirectProp('members', filter, { outputName: 'guild members' }).items;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sweeps all text-based channels' messages and removes the ones which are indicated by the filter.
|
||||
* @param {Function} filter The function used to determine which messages will be removed from the caches.
|
||||
* @returns {number} Amount of messages that were removed from the caches
|
||||
* @example
|
||||
* // Remove all messages older than 1800 seconds from the messages cache
|
||||
* const amount = sweepers.sweepMessages(
|
||||
* Sweepers.filterByLifetime({
|
||||
* lifetime: 1800,
|
||||
* getComparisonTimestamp: m => m.editedTimestamp ?? m.createdTimestamp,
|
||||
* })(),
|
||||
* );
|
||||
* console.log(`Successfully removed ${amount} messages from the cache.`);
|
||||
*/
|
||||
sweepMessages(filter) {
|
||||
if (typeof filter !== 'function') {
|
||||
throw new TypeError('INVALID_TYPE', 'filter', 'function');
|
||||
}
|
||||
let channels = 0;
|
||||
let messages = 0;
|
||||
|
||||
for (const channel of this.client.channels.cache.values()) {
|
||||
if (!channel.isTextBased()) continue;
|
||||
|
||||
channels++;
|
||||
messages += channel.messages.cache.sweep(filter);
|
||||
}
|
||||
this.client.emit(Events.CacheSweep, `Swept ${messages} messages in ${channels} text-based channels.`);
|
||||
return messages;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sweeps all presences and removes the ones which are indicated by the filter.
|
||||
* @param {Function} filter The function used to determine which presences will be removed from the caches.
|
||||
* @returns {number} Amount of presences that were removed from the caches
|
||||
*/
|
||||
sweepPresences(filter) {
|
||||
return this._sweepGuildDirectProp('presences', filter).items;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sweeps all message reactions and removes the ones which are indicated by the filter.
|
||||
* @param {Function} filter The function used to determine which reactions will be removed from the caches.
|
||||
* @returns {number} Amount of reactions that were removed from the caches
|
||||
*/
|
||||
sweepReactions(filter) {
|
||||
if (typeof filter !== 'function') {
|
||||
throw new TypeError('INVALID_TYPE', 'filter', 'function');
|
||||
}
|
||||
let channels = 0;
|
||||
let messages = 0;
|
||||
let reactions = 0;
|
||||
|
||||
for (const channel of this.client.channels.cache.values()) {
|
||||
if (!channel.isTextBased()) continue;
|
||||
channels++;
|
||||
|
||||
for (const message of channel.messages.cache.values()) {
|
||||
messages++;
|
||||
reactions += message.reactions.cache.sweep(filter);
|
||||
}
|
||||
}
|
||||
this.client.emit(
|
||||
Events.CacheSweep,
|
||||
`Swept ${reactions} reactions on ${messages} messages in ${channels} text-based channels.`,
|
||||
);
|
||||
return reactions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sweeps all guild stage instances and removes the ones which are indicated by the filter.
|
||||
* @param {Function} filter The function used to determine which stage instances will be removed from the caches.
|
||||
* @returns {number} Amount of stage instances that were removed from the caches
|
||||
*/
|
||||
sweepStageInstances(filter) {
|
||||
return this._sweepGuildDirectProp('stageInstances', filter, { outputName: 'stage instances' }).items;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sweeps all guild stickers and removes the ones which are indicated by the filter.
|
||||
* @param {Function} filter The function used to determine which stickers will be removed from the caches.
|
||||
* @returns {number} Amount of stickers that were removed from the caches
|
||||
*/
|
||||
sweepStickers(filter) {
|
||||
return this._sweepGuildDirectProp('stickers', filter).items;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sweeps all thread members and removes the ones which are indicated by the filter.
|
||||
* <info>It is highly recommended to keep the client thread member cached</info>
|
||||
* @param {Function} filter The function used to determine which thread members will be removed from the caches.
|
||||
* @returns {number} Amount of thread members that were removed from the caches
|
||||
*/
|
||||
sweepThreadMembers(filter) {
|
||||
if (typeof filter !== 'function') {
|
||||
throw new TypeError('INVALID_TYPE', 'filter', 'function');
|
||||
}
|
||||
|
||||
let threads = 0;
|
||||
let members = 0;
|
||||
for (const channel of this.client.channels.cache.values()) {
|
||||
if (!ThreadChannelTypes.includes(channel.type)) continue;
|
||||
threads++;
|
||||
members += channel.members.cache.sweep(filter);
|
||||
}
|
||||
this.client.emit(Events.CacheSweep, `Swept ${members} thread members in ${threads} threads.`);
|
||||
return members;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sweeps all threads and removes the ones which are indicated by the filter.
|
||||
* @param {Function} filter The function used to determine which threads will be removed from the caches.
|
||||
* @returns {number} filter Amount of threads that were removed from the caches
|
||||
* @example
|
||||
* // Remove all threads archived greater than 1 day ago from all the channel caches
|
||||
* const amount = sweepers.sweepThreads(
|
||||
* Sweepers.filterByLifetime({
|
||||
* getComparisonTimestamp: t => t.archivedTimestamp,
|
||||
* excludeFromSweep: t => !t.archived,
|
||||
* })(),
|
||||
* );
|
||||
* console.log(`Successfully removed ${amount} threads from the cache.`);
|
||||
*/
|
||||
sweepThreads(filter) {
|
||||
if (typeof filter !== 'function') {
|
||||
throw new TypeError('INVALID_TYPE', 'filter', 'function');
|
||||
}
|
||||
|
||||
let threads = 0;
|
||||
for (const [key, val] of this.client.channels.cache.entries()) {
|
||||
if (!ThreadChannelTypes.includes(val.type)) continue;
|
||||
if (filter(val, key, this.client.channels.cache)) {
|
||||
threads++;
|
||||
this.client.channels._remove(key);
|
||||
}
|
||||
}
|
||||
this.client.emit(Events.CacheSweep, `Swept ${threads} threads.`);
|
||||
return threads;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sweeps all users and removes the ones which are indicated by the filter.
|
||||
* @param {Function} filter The function used to determine which users will be removed from the caches.
|
||||
* @returns {number} Amount of users that were removed from the caches
|
||||
*/
|
||||
sweepUsers(filter) {
|
||||
if (typeof filter !== 'function') {
|
||||
throw new TypeError('INVALID_TYPE', 'filter', 'function');
|
||||
}
|
||||
|
||||
const users = this.client.users.cache.sweep(filter);
|
||||
|
||||
this.client.emit(Events.CacheSweep, `Swept ${users} users.`);
|
||||
|
||||
return users;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sweeps all guild voice states and removes the ones which are indicated by the filter.
|
||||
* @param {Function} filter The function used to determine which voice states will be removed from the caches.
|
||||
* @returns {number} Amount of voice states that were removed from the caches
|
||||
*/
|
||||
sweepVoiceStates(filter) {
|
||||
return this._sweepGuildDirectProp('voiceStates', filter, { outputName: 'voice states' }).items;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancels all sweeping intervals
|
||||
* @returns {void}
|
||||
*/
|
||||
destroy() {
|
||||
for (const key of SweeperKeys) {
|
||||
if (this.intervals[key]) clearInterval(this.intervals[key]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for generating a filter function based on lifetime
|
||||
* @typedef {Object} LifetimeFilterOptions
|
||||
* @property {number} [lifetime=14400] How long, in seconds, an entry should stay in the collection
|
||||
* before it is considered sweepable.
|
||||
* @property {Function} [getComparisonTimestamp=e => e?.createdTimestamp] A function that takes an entry, key,
|
||||
* and the collection and returns a timestamp to compare against in order to determine the lifetime of the entry.
|
||||
* @property {Function} [excludeFromSweep=() => false] A function that takes an entry, key, and the collection
|
||||
* and returns a boolean, `true` when the entry should not be checked for sweepability.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Create a sweepFilter function that uses a lifetime to determine sweepability.
|
||||
* @param {LifetimeFilterOptions} [options={}] The options used to generate the filter function
|
||||
* @returns {GlobalSweepFilter}
|
||||
*/
|
||||
static filterByLifetime({
|
||||
lifetime = 14400,
|
||||
getComparisonTimestamp = e => e?.createdTimestamp,
|
||||
excludeFromSweep = () => false,
|
||||
} = {}) {
|
||||
if (typeof lifetime !== 'number') {
|
||||
throw new TypeError('INVALID_TYPE', 'lifetime', 'number');
|
||||
}
|
||||
if (typeof getComparisonTimestamp !== 'function') {
|
||||
throw new TypeError('INVALID_TYPE', 'getComparisonTimestamp', 'function');
|
||||
}
|
||||
if (typeof excludeFromSweep !== 'function') {
|
||||
throw new TypeError('INVALID_TYPE', 'excludeFromSweep', 'function');
|
||||
}
|
||||
return () => {
|
||||
if (lifetime <= 0) return null;
|
||||
const lifetimeMs = lifetime * 1_000;
|
||||
const now = Date.now();
|
||||
return (entry, key, coll) => {
|
||||
if (excludeFromSweep(entry, key, coll)) {
|
||||
return false;
|
||||
}
|
||||
const comparisonTimestamp = getComparisonTimestamp(entry, key, coll);
|
||||
if (!comparisonTimestamp || typeof comparisonTimestamp !== 'number') return false;
|
||||
return now - comparisonTimestamp > lifetimeMs;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a sweep filter that sweeps archived threads
|
||||
* @param {number} [lifetime=14400] How long a thread has to be archived to be valid for sweeping
|
||||
* @returns {GlobalSweepFilter}
|
||||
*/
|
||||
static archivedThreadSweepFilter(lifetime = 14400) {
|
||||
return this.filterByLifetime({
|
||||
lifetime,
|
||||
getComparisonTimestamp: e => e.archiveTimestamp,
|
||||
excludeFromSweep: e => !e.archived,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a sweep filter that sweeps expired invites
|
||||
* @param {number} [lifetime=14400] How long ago an invite has to have expired to be valid for sweeping
|
||||
* @returns {GlobalSweepFilter}
|
||||
*/
|
||||
static expiredInviteSweepFilter(lifetime = 14400) {
|
||||
return this.filterByLifetime({
|
||||
lifetime,
|
||||
getComparisonTimestamp: i => i.expiresTimestamp,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a sweep filter that sweeps outdated messages (edits taken into account)
|
||||
* @param {number} [lifetime=3600] How long ago a message has to have been sent or edited to be valid for sweeping
|
||||
* @returns {GlobalSweepFilter}
|
||||
*/
|
||||
static outdatedMessageSweepFilter(lifetime = 3600) {
|
||||
return this.filterByLifetime({
|
||||
lifetime,
|
||||
getComparisonTimestamp: m => m.editedTimestamp ?? m.createdTimestamp,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration options for emitting the cache sweep client event
|
||||
* @typedef {Object} SweepEventOptions
|
||||
* @property {boolean} [emit=true] Whether to emit the client event in this method
|
||||
* @property {string} [outputName] A name to output in the client event if it should differ from the key
|
||||
* @private
|
||||
*/
|
||||
|
||||
/**
|
||||
* Sweep a direct sub property of all guilds
|
||||
* @param {string} key The name of the property
|
||||
* @param {Function} filter Filter function passed to sweep
|
||||
* @param {SweepEventOptions} [eventOptions={}] Options for the Client event emitted here
|
||||
* @returns {Object} Object containing the number of guilds swept and the number of items swept
|
||||
* @private
|
||||
*/
|
||||
_sweepGuildDirectProp(key, filter, { emit = true, outputName } = {}) {
|
||||
if (typeof filter !== 'function') {
|
||||
throw new TypeError('INVALID_TYPE', 'filter', 'function');
|
||||
}
|
||||
|
||||
let guilds = 0;
|
||||
let items = 0;
|
||||
|
||||
for (const guild of this.client.guilds.cache.values()) {
|
||||
const { cache } = guild[key];
|
||||
|
||||
guilds++;
|
||||
items += cache.sweep(filter);
|
||||
}
|
||||
|
||||
if (emit) {
|
||||
this.client.emit(Events.CacheSweep, `Swept ${items} ${outputName ?? key} in ${guilds} guilds.`);
|
||||
}
|
||||
|
||||
return { guilds, items };
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates a set of properties
|
||||
* @param {string} key Key of the options object to check
|
||||
* @private
|
||||
*/
|
||||
_validateProperties(key) {
|
||||
const props = this.options[key];
|
||||
if (typeof props !== 'object') {
|
||||
throw new TypeError('INVALID_TYPE', `sweepers.${key}`, 'object', true);
|
||||
}
|
||||
if (typeof props.interval !== 'number') {
|
||||
throw new TypeError('INVALID_TYPE', `sweepers.${key}.interval`, 'number');
|
||||
}
|
||||
// Invites, Messages, and Threads can be provided a lifetime parameter, which we use to generate the filter
|
||||
if (['invites', 'messages', 'threads'].includes(key) && !('filter' in props)) {
|
||||
if (typeof props.lifetime !== 'number') {
|
||||
throw new TypeError('INVALID_TYPE', `sweepers.${key}.lifetime`, 'number');
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (typeof props.filter !== 'function') {
|
||||
throw new TypeError('INVALID_TYPE', `sweepers.${key}.filter`, 'function');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize an interval for sweeping
|
||||
* @param {string} intervalKey The name of the property that stores the interval for this sweeper
|
||||
* @param {string} sweepKey The name of the function that sweeps the desired caches
|
||||
* @param {Object} opts Validated options for a sweep
|
||||
* @private
|
||||
*/
|
||||
_initInterval(intervalKey, sweepKey, opts) {
|
||||
if (opts.interval <= 0 || opts.interval === Infinity) return;
|
||||
this.intervals[intervalKey] = setInterval(() => {
|
||||
const sweepFn = opts.filter();
|
||||
if (sweepFn === null) return;
|
||||
if (typeof sweepFn !== 'function') throw new TypeError('SWEEP_FILTER_RETURN');
|
||||
this[sweepKey](sweepFn);
|
||||
}, opts.interval * 1_000).unref();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Sweepers;
|
42
src/util/SystemChannelFlagsBitField.js
Normal file
42
src/util/SystemChannelFlagsBitField.js
Normal file
@@ -0,0 +1,42 @@
|
||||
'use strict';
|
||||
|
||||
const { GuildSystemChannelFlags } = require('discord-api-types/v9');
|
||||
const BitField = require('./BitField');
|
||||
|
||||
/**
|
||||
* Data structure that makes it easy to interact with a {@link Guild#systemChannelFlags} bitfield.
|
||||
* <info>Note that all event message types are enabled by default,
|
||||
* and by setting their corresponding flags you are disabling them</info>
|
||||
* @extends {BitField}
|
||||
*/
|
||||
class SystemChannelFlagsBitField extends BitField {}
|
||||
|
||||
/**
|
||||
* @name SystemChannelFlagsBitField
|
||||
* @kind constructor
|
||||
* @memberof SystemChannelFlagsBitField
|
||||
* @param {SystemChannelFlagsResolvable} [bits=0] Bit(s) to read from
|
||||
*/
|
||||
|
||||
/**
|
||||
* Bitfield of the packed bits
|
||||
* @type {number}
|
||||
* @name SystemChannelFlagsBitField#bitfield
|
||||
*/
|
||||
|
||||
/**
|
||||
* Data that can be resolved to give a system channel flag bitfield. This can be:
|
||||
* * A string (see {@link SystemChannelFlagsBitField.Flags})
|
||||
* * A system channel flag
|
||||
* * An instance of SystemChannelFlagsBitField
|
||||
* * An Array of SystemChannelFlagsResolvable
|
||||
* @typedef {string|number|SystemChannelFlagsBitField|SystemChannelFlagsResolvable[]} SystemChannelFlagsResolvable
|
||||
*/
|
||||
|
||||
/**
|
||||
* Numeric system channel flags.
|
||||
* @type {GuildSystemChannelFlags}
|
||||
*/
|
||||
SystemChannelFlagsBitField.Flags = GuildSystemChannelFlags;
|
||||
|
||||
module.exports = SystemChannelFlagsBitField;
|
30
src/util/ThreadMemberFlagsBitField.js
Normal file
30
src/util/ThreadMemberFlagsBitField.js
Normal file
@@ -0,0 +1,30 @@
|
||||
'use strict';
|
||||
|
||||
const BitField = require('./BitField');
|
||||
|
||||
/**
|
||||
* Data structure that makes it easy to interact with a {@link ThreadMember#flags} bitfield.
|
||||
* @extends {BitField}
|
||||
*/
|
||||
class ThreadMemberFlagsBitField extends BitField {}
|
||||
|
||||
/**
|
||||
* @name ThreadMemberFlagsBitField
|
||||
* @kind constructor
|
||||
* @memberof ThreadMemberFlagsBitField
|
||||
* @param {BitFieldResolvable} [bits=0] Bit(s) to read from
|
||||
*/
|
||||
|
||||
/**
|
||||
* Bitfield of the packed bits
|
||||
* @type {number}
|
||||
* @name ThreadMemberFlagsBitField#bitfield
|
||||
*/
|
||||
|
||||
/**
|
||||
* Numeric thread member flags. There are currently no bitflags relevant to bots for this.
|
||||
* @type {Object<string, number>}
|
||||
*/
|
||||
ThreadMemberFlagsBitField.Flags = {};
|
||||
|
||||
module.exports = ThreadMemberFlagsBitField;
|
20
src/util/Transformers.js
Normal file
20
src/util/Transformers.js
Normal file
@@ -0,0 +1,20 @@
|
||||
'use strict';
|
||||
|
||||
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)]),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Transformers;
|
31
src/util/UserFlagsBitField.js
Normal file
31
src/util/UserFlagsBitField.js
Normal file
@@ -0,0 +1,31 @@
|
||||
'use strict';
|
||||
|
||||
const { UserFlags } = require('discord-api-types/v9');
|
||||
const BitField = require('./BitField');
|
||||
|
||||
/**
|
||||
* Data structure that makes it easy to interact with a {@link User#flags} bitfield.
|
||||
* @extends {BitField}
|
||||
*/
|
||||
class UserFlagsBitField extends BitField {}
|
||||
|
||||
/**
|
||||
* @name UserFlagsBitField
|
||||
* @kind constructor
|
||||
* @memberof UserFlagsBitField
|
||||
* @param {BitFieldResolvable} [bits=0] Bit(s) to read from
|
||||
*/
|
||||
|
||||
/**
|
||||
* Bitfield of the packed bits
|
||||
* @type {number}
|
||||
* @name UserFlagsBitField#bitfield
|
||||
*/
|
||||
|
||||
/**
|
||||
* Numeric user flags.
|
||||
* @type {UserFlags}
|
||||
*/
|
||||
UserFlagsBitField.Flags = UserFlags;
|
||||
|
||||
module.exports = UserFlagsBitField;
|
569
src/util/Util.js
Normal file
569
src/util/Util.js
Normal file
@@ -0,0 +1,569 @@
|
||||
'use strict';
|
||||
|
||||
const { parse } = require('node:path');
|
||||
const { Collection } = require('@discordjs/collection');
|
||||
const { ChannelType, RouteBases, Routes } = require('discord-api-types/v9');
|
||||
const { fetch } = require('undici');
|
||||
const Colors = require('./Colors');
|
||||
const { Error: DiscordError, RangeError, TypeError } = require('../errors');
|
||||
const isObject = d => typeof d === 'object' && d !== null;
|
||||
|
||||
/**
|
||||
* Contains various general-purpose utility methods.
|
||||
*/
|
||||
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<string, boolean|string>} [props] Specific properties to include/exclude.
|
||||
* @returns {Object}
|
||||
*/
|
||||
static flatten(obj, ...props) {
|
||||
if (!isObject(obj)) return obj;
|
||||
|
||||
const objProps = Object.keys(obj)
|
||||
.filter(k => !k.startsWith('_'))
|
||||
.map(k => ({ [k]: true }));
|
||||
|
||||
props = objProps.length ? Object.assign(...objProps, ...props) : Object.assign({}, ...props);
|
||||
|
||||
const out = {};
|
||||
|
||||
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;
|
||||
|
||||
// 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, flatten each element
|
||||
else if (Array.isArray(element)) out[newProp] = element.map(e => 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 a primitive
|
||||
else if (!elemIsObj) out[newProp] = element;
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for splitting a message.
|
||||
* @typedef {Object} SplitOptions
|
||||
* @property {number} [maxLength=2000] Maximum character length per message piece
|
||||
* @property {string|string[]|RegExp|RegExp[]} [char='\n'] Character(s) or Regex(es) to split the message with,
|
||||
* an array can be used to split multiple times
|
||||
* @property {string} [prepend=''] Text to prepend to every piece except the first
|
||||
* @property {string} [append=''] Text to append to every piece except the last
|
||||
*/
|
||||
|
||||
/**
|
||||
* Splits a string into multiple chunks at a designated character that do not exceed a specific length.
|
||||
* @param {string} text Content to split
|
||||
* @param {SplitOptions} [options] Options controlling the behavior of the split
|
||||
* @returns {string[]}
|
||||
*/
|
||||
static splitMessage(text, { maxLength = 2_000, char = '\n', prepend = '', append = '' } = {}) {
|
||||
text = Util.verifyString(text);
|
||||
if (text.length <= maxLength) return [text];
|
||||
let splitText = [text];
|
||||
if (Array.isArray(char)) {
|
||||
while (char.length > 0 && splitText.some(elem => elem.length > maxLength)) {
|
||||
const currentChar = char.shift();
|
||||
if (currentChar instanceof RegExp) {
|
||||
splitText = splitText.flatMap(chunk => chunk.match(currentChar));
|
||||
} else {
|
||||
splitText = splitText.flatMap(chunk => chunk.split(currentChar));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
splitText = text.split(char);
|
||||
}
|
||||
if (splitText.some(elem => elem.length > maxLength)) throw new RangeError('SPLIT_MAX_LEN');
|
||||
const messages = [];
|
||||
let msg = '';
|
||||
for (const chunk of splitText) {
|
||||
if (msg && (msg + char + chunk + append).length > maxLength) {
|
||||
messages.push(msg + append);
|
||||
msg = prepend;
|
||||
}
|
||||
msg += (msg && msg !== prepend ? char : '') + chunk;
|
||||
}
|
||||
return messages.concat(msg).filter(m => m);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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, '\\`');
|
||||
}
|
||||
|
||||
/**
|
||||
* 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<number>} 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 `<a:name:id>`)
|
||||
* @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: null };
|
||||
const match = text.match(/<?(?:(a):)?(\w{2,32}):(\d{17,19})?>?/);
|
||||
return match && { animated: Boolean(match[1]), name: match[2], id: match[3] ?? null };
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 <data> 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<string, Channel|Role>} 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<Channel[]|Role[]>} 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``');
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Util;
|
||||
|
||||
// Fixes Circular
|
||||
const GuildChannel = require('../structures/GuildChannel');
|
Reference in New Issue
Block a user