diff --git a/src/client/Client.js b/src/client/Client.js index ebdce4ee2..9ad7c9543 100644 --- a/src/client/Client.js +++ b/src/client/Client.js @@ -418,7 +418,7 @@ class Client extends EventEmitter { * .catch(console.error); */ generateInvite(permissions) { - permissions = typeof permissions === 'undefined' ? 0 : Permissions.resolve(permissions); + permissions = Permissions.resolve(permissions); return this.fetchApplication().then(application => `https://discordapp.com/oauth2/authorize?client_id=${application.id}&permissions=${permissions}&scope=bot` ); diff --git a/src/index.js b/src/index.js index 47b1a0e82..49e5f00e2 100644 --- a/src/index.js +++ b/src/index.js @@ -9,6 +9,7 @@ module.exports = { WebhookClient: require('./client/WebhookClient'), // Utilities + BitField: require('./util/BitField'), Collection: require('./util/Collection'), Constants: require('./util/Constants'), DiscordAPIError: require('./client/rest/DiscordAPIError'), diff --git a/src/structures/Presence.js b/src/structures/Presence.js index 2e0febb24..18cc3cc5f 100644 --- a/src/structures/Presence.js +++ b/src/structures/Presence.js @@ -192,6 +192,10 @@ class Game { return new Date(this.createdTimestamp); } + /** + * Flags that describe the activity + * @type {ActivityFlags[]} + */ get flags() { const flags = []; for (const [name, flag] of Object.entries(ActivityFlags)) { diff --git a/src/util/BitField.js b/src/util/BitField.js new file mode 100644 index 000000000..e0f86b123 --- /dev/null +++ b/src/util/BitField.js @@ -0,0 +1,160 @@ +/** + * Data structure that makes it easy to interact with a bitfield. + */ +class BitField { + /** + * @param {BitFieldResolvable} [bits=0] Bits(s) to read from + */ + constructor(bits) { + /** + * Bitfield of the packed bits + * @type {number} + */ + 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)) !== 0; + } + + /** + * 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) { + if (Array.isArray(bit)) return bit.every(p => this.has(p)); + bit = this.constructor.resolve(bit); + return (this.bitfield & bit) === bit; + } + + /** + * Gets all given bits that are missing from the bitfield. + * @param {BitFieldResolvable} bits Bits(s) to check for + * @param {...*} hasParams Additional parameters for the has method, if any + * @returns {string[]} + */ + missing(bits, ...hasParams) { + if (!Array.isArray(bits)) bits = new this.constructor(bits).toArray(false); + return bits.filter(p => !this.has(p, ...hasParams)); + } + + /** + * Freezes these bits, making them immutable. + * @returns {Readonly} These bits + */ + 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 = 0; + 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 = 0; + 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 of Object.keys(this.constructor.FLAGS)) { + serialized[flag] = this.has(this.constructor.FLAGS[flag], ...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 this.bitfield; + } + + valueOf() { + return this.bitfield; + } + + *[Symbol.iterator]() { + yield* this.toArray(); + } + + /** + * Data that can be resolved to give a bitfield. This can be: + * * A string (see {@link BitField.FLAGS}) + * * A bit number + * * An instance of BitField + * * An Array of BitFieldResolvable + * @typedef {string|number|BitField|BitFieldResolvable[]} BitFieldResolvable + */ + + /** + * Resolves bitfields to their numeric form. + * @param {BitFieldResolvable} [bit=0] - bit(s) to resolve + * @returns {number} + */ + static resolve(bit = 0) { + if (typeof bit === 'number' && bit >= 0) 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, 0); + if (typeof bit === 'string' && typeof this.FLAGS[bit] !== 'undefined') return this.FLAGS[bit]; + throw new RangeError('Invalid bitfield flag or number.'); + } +} + +/** + * Numeric bitfield flags. + * Defined in extension classes + * @type {Object} + * @abstract + */ +BitField.FLAGS = {}; + +module.exports = BitField; diff --git a/src/util/Constants.js b/src/util/Constants.js index 7de0927fb..79b309439 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -386,6 +386,17 @@ exports.ActivityTypes = [ 'CUSTOM_STATUS', ]; +/** + * Numeric activity flags. All available properties: + * * `INSTANCE` + * * `JOIN` + * * `SPECTATE` + * * `JOIN_REQUEST` + * * `SYNC` + * * `PLAY` + * @typedef {string} ActivityFlag + * @see {@link https://discordapp.com/developers/docs/topics/gateway#activity-object-activity-flags} + */ exports.ActivityFlags = { INSTANCE: 1 << 0, JOIN: 1 << 1, diff --git a/src/util/Permissions.js b/src/util/Permissions.js index 113b374ec..7c57c6053 100644 --- a/src/util/Permissions.js +++ b/src/util/Permissions.js @@ -1,33 +1,31 @@ -const Constants = require('../util/Constants'); +const BitField = require('./BitField'); const util = require('util'); /** * 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 Permissions { +class Permissions extends BitField { /** * @param {GuildMember} [member] Member the permissions are for **(deprecated)** * @param {number|PermissionResolvable} permissions Permissions or bitfield to read from */ constructor(member, permissions) { - permissions = typeof member === 'object' && !(member instanceof Array) ? permissions : member; + super(typeof member === 'object' && !(member instanceof Array) ? permissions : member); - /** + Object.defineProperty(this, '_member', { + writable: true, + value: typeof member === 'object' && !(member instanceof Array) ? member : null, + }); + } + + /** * Member the permissions are for * @type {GuildMember} * @deprecated */ - this._member = typeof member === 'object' ? member : null; - - /** - * Bitfield of the packed permissions - * @type {number} - */ - this.bitfield = typeof permissions === 'number' ? permissions : this.constructor.resolve(permissions); - } - get member() { return this._member; } @@ -51,6 +49,16 @@ class Permissions { this.bitfield = raw; } + /** + * 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(this.constructor.FLAGS.ADMINISTRATOR)) || super.any(permission); + } + /** * Checks whether the bitfield has a permission, or multiple permissions. * @param {PermissionResolvable} permission Permission(s) to check for @@ -58,76 +66,7 @@ class Permissions { * @returns {boolean} */ has(permission, checkAdmin = true) { - if (permission instanceof Array) return permission.every(p => this.has(p, checkAdmin)); - permission = this.constructor.resolve(permission); - if (checkAdmin && (this.bitfield & this.constructor.FLAGS.ADMINISTRATOR) > 0) return true; - return (this.bitfield & permission) === permission; - } - - /** - * Checks whether the bitfield has a permission, or any of multiple permissions. - * @param {PermissionResolvable} permissions Permission(s) to check for - * @param {boolean} [checkAdmin=true] Whether to allow the administrator permission to override - * @returns {boolean} - */ - any(permissions, checkAdmin = true) { - return (checkAdmin && this.has(this.constructor.FLAGS.ADMINISTRATOR)) || - (this.bitfield & this.constructor.resolve(permissions)) !== 0; - } - - /** - * Gets all given permissions that are missing from the bitfield. - * @param {PermissionResolvable} permissions Permissions to check for - * @param {boolean} [checkAdmin=true] Whether to allow the administrator permission to override - * @returns {PermissionResolvable} - */ - missing(permissions, checkAdmin = true) { - if (!(permissions instanceof Array)) permissions = [permissions]; - return permissions.filter(p => !this.has(p, checkAdmin)); - } - - /** - * Adds permissions to this one, creating a new instance to represent the new bitfield. - * @param {...PermissionResolvable} permissions Permissions to add - * @returns {Permissions} - */ - add(...permissions) { - let total = 0; - for (let p = permissions.length - 1; p >= 0; p--) { - const perm = this.constructor.resolve(permissions[p]); - total |= perm; - } - if (Object.isFrozen(this)) return new this.constructor(this.bitfield | total); - this.bitfield |= total; - return this; - } - - /** - * Removes permissions to this one, creating a new instance to represent the new bitfield. - * @param {...PermissionResolvable} permissions Permissions to remove - * @returns {Permissions} - */ - remove(...permissions) { - let total = 0; - for (let p = permissions.length - 1; p >= 0; p--) { - const perm = this.constructor.resolve(permissions[p]); - total |= perm; - } - if (Object.isFrozen(this)) return new this.constructor(this.bitfield & ~total); - this.bitfield &= ~total; - return this; - } - - /** - * Gets an object mapping permission name (like `VIEW_CHANNEL`) to a {@link boolean} indicating whether the - * permission is available. - * @param {boolean} [checkAdmin=true] Whether to allow the administrator permission to override - * @returns {Object} - */ - serialize(checkAdmin = true) { - const serialized = {}; - for (const perm of Object.keys(this.constructor.FLAGS)) serialized[perm] = this.has(perm, checkAdmin); - return serialized; + return (checkAdmin && super.has(this.constructor.FLAGS.ADMINISTRATOR)) || super.has(permission); } /** @@ -166,46 +105,12 @@ class Permissions { return this.missing(permissions, !explicit); } - /** - * Gets an {@link Array} of permission names (such as `VIEW_CHANNEL`) based on the permissions available. - * @param {boolean} [checkAdmin=true] Whether to allow the administrator permission to override - * @returns {string[]} - */ - toArray(checkAdmin = true) { - return Object.keys(this.constructor.FLAGS).filter(perm => this.has(perm, checkAdmin)); - } - - /** - * Freezes these permissions, making them immutable. - * @returns {Permissions} These permissions - */ - freeze() { - return Object.freeze(this); - } - - valueOf() { - return this.bitfield; - } - /** * Data that can be resolved to give a permission number. This can be: * * A string (see {@link Permissions.FLAGS}) * * A permission number * @typedef {string|number|Permissions|PermissionResolvable[]} PermissionResolvable */ - - /** - * Resolves permissions to their numeric form. - * @param {PermissionResolvable} permission - Permission(s) to resolve - * @returns {number} - */ - static resolve(permission) { - if (permission instanceof Array) return permission.map(p => this.resolve(p)).reduce((prev, p) => prev | p, 0); - if (permission instanceof Permissions) return permission.bitfield; - if (typeof permission === 'string') permission = this.FLAGS[permission]; - if (typeof permission !== 'number' || permission < 0) throw new RangeError(Constants.Errors.NOT_A_PERMISSION); - return permission; - } } /** @@ -310,10 +215,20 @@ Permissions.prototype.hasPermissions = util.deprecate(Permissions.prototype.hasP 'EvaluatedPermissions#hasPermissions is deprecated, use Permissions#has instead'); Permissions.prototype.missingPermissions = util.deprecate(Permissions.prototype.missingPermissions, 'EvaluatedPermissions#missingPermissions is deprecated, use Permissions#missing instead'); +Object.defineProperty(Permissions.prototype, 'raw', { + get: util + .deprecate(Object.getOwnPropertyDescriptor(Permissions.prototype, 'raw').get, + 'EvaluatedPermissions#raw is deprecated use Permissions#bitfield instead'), + set: util.deprecate(Object.getOwnPropertyDescriptor(Permissions.prototype, 'raw').set, + 'EvaluatedPermissions#raw is deprecated use Permissions#bitfield instead'), +}); Object.defineProperty(Permissions.prototype, 'member', { get: util .deprecate(Object.getOwnPropertyDescriptor(Permissions.prototype, 'member').get, 'EvaluatedPermissions#member is deprecated'), + set: util + .deprecate(Object.getOwnPropertyDescriptor(Permissions.prototype, 'member').set, + 'EvaluatedPermissions#member is deprecated'), }); module.exports = Permissions; diff --git a/typings/index.d.ts b/typings/index.d.ts index ca89c54a2..185457a1e 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -41,6 +41,25 @@ declare module 'discord.js' { public options: object; } + export class BitField { + constructor(bits?: BitFieldResolvable); + public bitfield: number; + public add(...bits: BitFieldResolvable[]): BitField; + public any(bit: BitFieldResolvable): boolean; + public equals(bit: BitFieldResolvable): boolean; + public freeze(): Readonly>; + public has(bit: BitFieldResolvable): boolean; + public missing(bits: BitFieldResolvable, ...hasParam: readonly unknown[]): S[]; + public remove(...bits: BitFieldResolvable[]): BitField; + public serialize(...hasParam: readonly unknown[]): Record; + public toArray(...hasParam: readonly unknown[]): S[]; + public toJSON(): number; + public valueOf(): number; + public [Symbol.iterator](): IterableIterator; + public static FLAGS: object; + public static resolve(bit?: BitFieldResolvable): number; + } + export class CategoryChannel extends GuildChannel { public readonly children: Collection; } @@ -436,6 +455,9 @@ declare module 'discord.js' { export class Game { constructor(data: object, presence: Presence); + private _flags: string[]; + private syncID: string; + public applicationID: string; public assets: RichPresenceAssets; public details: string; @@ -456,8 +478,6 @@ declare module 'discord.js' { public url: string; public equals(game: Game): boolean; public toString(): string; - private _flags: string[]; - private syncID: string; } export class GroupDMChannel extends TextBasedChannel(Channel) { @@ -1006,30 +1026,27 @@ declare module 'discord.js' { public delete(reason?: string): Promise; } - export class Permissions { + export class Permissions extends BitField { constructor(permissions: PermissionResolvable); constructor(member: GuildMember, permissions: PermissionResolvable); - private readonly raw: number; public bitfield: number; public member: GuildMember; - public add(...permissions: PermissionResolvable[]): this; + public readonly raw: number; public any(permissions: PermissionResolvable, checkAdmin?: boolean): boolean; - public freeze(): this; public has(permission: PermissionResolvable, checkAdmin?: boolean): boolean; public hasPermission(permission: PermissionResolvable, explicit?: boolean): boolean; public hasPermissions(permissions: PermissionResolvable, explicit?: boolean): boolean; - public missing(permissions: PermissionResolvable, checkAdmin?: boolean): PermissionResolvable; + public missing(permissions: PermissionResolvable, checkAdmin?: boolean): PermissionString[]; public missingPermissions(permissions: PermissionResolvable, checkAdmin?: boolean): PermissionResolvable; - public remove(...permissions: PermissionResolvable[]): this; - public serialize(checkAdmin?: boolean): PermissionObject; + public serialize(checkAdmin?: boolean): Required; public toArray(checkAdmin?: boolean): PermissionString[]; public valueOf(): number; public static ALL: number; public static DEFAULT: number; public static FLAGS: PermissionFlags; - public static resolve(permission: PermissionResolvable): number; + public static resolve(permission?: PermissionResolvable): number; } export class Presence { @@ -1730,6 +1747,8 @@ declare module 'discord.js' { type Base64String = string; + type BitFieldResolvable = RecursiveArray>> | T | number | Readonly>; + type BufferResolvable = Buffer | string; type ChannelCreationOverwrites = { @@ -2181,7 +2200,7 @@ declare module 'discord.js' { interface RecursiveArray extends Array> { } - type PermissionResolvable = RecursiveArray | Permissions | PermissionString | number; + type PermissionResolvable = BitFieldResolvable type PremiumTier = number;