From c62f01f0e4e3c24c3227b10704ac3ccf8744a6e1 Mon Sep 17 00:00:00 2001 From: bdistin Date: Tue, 21 Aug 2018 04:56:41 -0500 Subject: [PATCH] refactor(BitField): base class for Permissions, ActivityFlags, Speaking (#2765) * abstract BitField from Permissions * reduce useless code, improve docs * add a ReadOnly identifier to the return type of Bitfield#freeze() https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-1.html#partial-readonly-record-and-pick * fix the RangeError * update docs, convert Speaking and ActivityFlags to bitfields * fix some docs * Fix Speaking BitField oops * docs for oops * more incorrect docs * Fix incorrectly named property * add new classes to index * fix missing @extends docs * default bitfield resolve to 0, and cleanup defaulting everywhere Also removes GuildMember#missiongPermissions() alias that had incorrect behavior * Breaking: Rename Overwrite allowed and denied to allow and deny To be consistent with the api's naming * fix setSpeaking usage to bitfields instead of booleans * fix speaking bug in playChunk * docs: Updated typings * fix: BitFieldResolvable should use RecursiveArray * bugfix/requested change * typings: Cleanup (#2) * typings: Fix BitField#{toArray,@@iterator} output type * typings: correct PermissionOverwrites property names and nitpicks --- src/client/Client.js | 2 +- src/client/voice/VoiceConnection.js | 35 ++-- .../voice/dispatcher/StreamDispatcher.js | 9 +- src/errors/Messages.js | 2 +- src/index.js | 3 + src/stores/GuildChannelStore.js | 6 +- src/structures/GuildChannel.js | 54 +++---- src/structures/GuildMember.js | 14 +- src/structures/PermissionOverwrites.js | 8 +- src/structures/Presence.js | 16 +- src/structures/Role.js | 4 +- src/structures/shared/resolvePermissions.js | 4 +- src/util/ActivityFlags.js | 29 ++++ src/util/BitField.js | 151 ++++++++++++++++++ src/util/Constants.js | 9 -- src/util/Permissions.js | 130 ++------------- src/util/Speaking.js | 22 +++ typings/index.d.ts | 83 ++++++---- 18 files changed, 339 insertions(+), 242 deletions(-) create mode 100644 src/util/ActivityFlags.js create mode 100644 src/util/BitField.js create mode 100644 src/util/Speaking.js diff --git a/src/client/Client.js b/src/client/Client.js index 49a91f389..f3a6617d5 100644 --- a/src/client/Client.js +++ b/src/client/Client.js @@ -355,7 +355,7 @@ class Client extends BaseClient { * .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/client/voice/VoiceConnection.js b/src/client/voice/VoiceConnection.js index 6682d32ff..eeaf768d0 100644 --- a/src/client/voice/VoiceConnection.js +++ b/src/client/voice/VoiceConnection.js @@ -7,6 +7,7 @@ const VoiceReceiver = require('./receiver/Receiver'); const EventEmitter = require('events'); const { Error } = require('../../errors'); const PlayInterface = require('./util/PlayInterface'); +const Speaking = require('../../util/Speaking'); const SUPPORTED_MODES = [ 'xsalsa20_poly1305_lite', @@ -49,10 +50,10 @@ class VoiceConnection extends EventEmitter { this.status = VoiceStatus.AUTHENTICATING; /** - * Whether we're currently transmitting audio - * @type {boolean} + * Our current speaking state + * @type {ReadOnly} */ - this.speaking = false; + this.speaking = new Speaking().freeze(); /** * The authentication data needed to connect to the voice server @@ -96,7 +97,7 @@ class VoiceConnection extends EventEmitter { /** * Tracks which users are talking - * @type {Map} + * @type {Map>} * @private */ this._speaking = new Map(); @@ -135,18 +136,18 @@ class VoiceConnection extends EventEmitter { } /** - * Sets whether the voice connection should display as "speaking" or not. - * @param {boolean} value Whether or not to speak + * Sets whether the voice connection should display as "speaking", "soundshare" or "none". + * @param {BitFieldResolvable} value The new speaking state * @private */ setSpeaking(value) { - if (this.speaking === value) return; + if (this.speaking.equals(value)) return; if (this.status !== VoiceStatus.CONNECTED) return; - this.speaking = value; + this.speaking = new Speaking(value).freeze(); this.sockets.ws.sendPacket({ op: VoiceOPCodes.SPEAKING, d: { - speaking: this.speaking ? 1 : 0, + speaking: this.speaking.bitfield, delay: 0, ssrc: this.authentication.ssrc, }, @@ -305,7 +306,7 @@ class VoiceConnection extends EventEmitter { reconnect(token, endpoint) { this.authentication.token = token; this.authentication.endpoint = endpoint; - this.speaking = false; + this.speaking = new Speaking().freeze(); this.status = VoiceStatus.RECONNECTING; /** * Emitted when the voice connection is reconnecting (typically after a region change). @@ -350,7 +351,7 @@ class VoiceConnection extends EventEmitter { */ cleanup() { this.player.destroy(); - this.speaking = false; + this.speaking = new Speaking().freeze(); const { ws, udp } = this.sockets; if (ws) { @@ -432,17 +433,17 @@ class VoiceConnection extends EventEmitter { * @private */ onSpeaking({ user_id, ssrc, speaking }) { - speaking = Boolean(speaking); + speaking = new Speaking(speaking).freeze(); const guild = this.channel.guild; const user = this.client.users.get(user_id); this.ssrcMap.set(+ssrc, user_id); const old = this._speaking.get(user_id); this._speaking.set(user_id, speaking); /** - * Emitted whenever a user starts/stops speaking. + * Emitted whenever a user changes speaking state. * @event VoiceConnection#speaking - * @param {User} user The user that has started/stopped speaking - * @param {boolean} speaking Whether or not the user is speaking + * @param {User} user The user that has changed speaking state + * @param {ReadOnly} speaking The speaking state of the user */ if (this.status === VoiceStatus.CONNECTED) { this.emit('speaking', user, speaking); @@ -455,10 +456,10 @@ class VoiceConnection extends EventEmitter { const member = guild.member(user); if (member) { /** - * Emitted once a guild member starts/stops speaking. + * Emitted once a guild member changes speaking state. * @event Client#guildMemberSpeaking * @param {GuildMember} member The member that started/stopped speaking - * @param {boolean} speaking Whether or not the member is speaking + * @param {ReadOnly} speaking The speaking state of the member */ this.client.emit(Events.GUILD_MEMBER_SPEAKING, member, speaking); } diff --git a/src/client/voice/dispatcher/StreamDispatcher.js b/src/client/voice/dispatcher/StreamDispatcher.js index e640b1556..7bb0ef1b0 100644 --- a/src/client/voice/dispatcher/StreamDispatcher.js +++ b/src/client/voice/dispatcher/StreamDispatcher.js @@ -67,7 +67,7 @@ class StreamDispatcher extends Writable { this.on('finish', () => { // Still emitting end for backwards compatibility, probably remove it in the future! this.emit('end'); - this._setSpeaking(false); + this._setSpeaking(0); }); if (typeof volume !== 'undefined') this.setVolume(volume); @@ -131,7 +131,7 @@ class StreamDispatcher extends Writable { this.streams.silence.pipe(this); this._silence = true; } else { - this._setSpeaking(false); + this._setSpeaking(0); } this.pausedSince = Date.now(); } @@ -243,7 +243,6 @@ class StreamDispatcher extends Writable { _playChunk(chunk) { if (this.player.dispatcher !== this || !this.player.voiceConnection.authentication.secret_key) return; - this._setSpeaking(true); this._sendPacket(this._createPacket(this._sdata.sequence, this._sdata.timestamp, chunk)); } @@ -285,7 +284,7 @@ class StreamDispatcher extends Writable { * @event StreamDispatcher#debug * @param {string} info The debug info */ - this._setSpeaking(true); + this._setSpeaking(1); while (repeats--) { if (!this.player.voiceConnection.sockets.udp) { this.emit('debug', 'Failed to send a packet - no UDP socket'); @@ -293,7 +292,7 @@ class StreamDispatcher extends Writable { } this.player.voiceConnection.sockets.udp.send(packet) .catch(e => { - this._setSpeaking(false); + this._setSpeaking(0); this.emit('debug', `Failed to send a packet - ${e}`); }); } diff --git a/src/errors/Messages.js b/src/errors/Messages.js index f9ff8be6e..00f99a676 100644 --- a/src/errors/Messages.js +++ b/src/errors/Messages.js @@ -10,7 +10,7 @@ const Messages = { WS_CONNECTION_EXISTS: 'There is already an existing WebSocket connection.', WS_NOT_OPEN: (data = 'data') => `Websocket not open to send ${data}`, - PERMISSIONS_INVALID: 'Invalid permission string or number.', + BITFIELD_INVALID: 'Invalid bitfield flag or number.', RATELIMIT_INVALID_METHOD: 'Unknown rate limiting method.', diff --git a/src/index.js b/src/index.js index 4bb024e31..fd2239ade 100644 --- a/src/index.js +++ b/src/index.js @@ -10,12 +10,15 @@ module.exports = { WebhookClient: require('./client/WebhookClient'), // Utilities + ActivityFlags: require('./util/ActivityFlags'), + BitField: require('./util/BitField'), Collection: require('./util/Collection'), Constants: require('./util/Constants'), DataResolver: require('./util/DataResolver'), DataStore: require('./stores/DataStore'), DiscordAPIError: require('./rest/DiscordAPIError'), Permissions: require('./util/Permissions'), + Speaking: require('./util/Speaking'), Snowflake: require('./util/Snowflake'), SnowflakeUtil: require('./util/Snowflake'), Structures: require('./util/Structures'), diff --git a/src/stores/GuildChannelStore.js b/src/stores/GuildChannelStore.js index 03efe2315..e73b52e98 100644 --- a/src/stores/GuildChannelStore.js +++ b/src/stores/GuildChannelStore.js @@ -24,8 +24,8 @@ class GuildChannelStore extends DataStore { /** * Can be used to overwrite permissions when creating a channel. * @typedef {Object} PermissionOverwriteOptions - * @property {PermissionResolvable} [allowed] The permissions to allow - * @property {PermissionResolvable} [denied] The permissions to deny + * @property {PermissionResolvable} [allow] The permissions to allow + * @property {PermissionResolvable} [deny] The permissions to deny * @property {RoleResolvable|UserResolvable} id ID of the role or member this overwrite is for */ @@ -54,7 +54,7 @@ class GuildChannelStore extends DataStore { * overwrites: [ * { * id: message.author.id, - * denied: ['VIEW_CHANNEL'], + * deny: ['VIEW_CHANNEL'], * }, * ], * }) diff --git a/src/structures/GuildChannel.js b/src/structures/GuildChannel.js index 9467371f9..b64eca976 100644 --- a/src/structures/GuildChannel.js +++ b/src/structures/GuildChannel.js @@ -76,8 +76,8 @@ class GuildChannel extends Channel { return !this.permissionOverwrites.find((value, key) => { const testVal = this.parent.permissionOverwrites.get(key); return testVal === undefined || - testVal.denied.bitfield !== value.denied.bitfield || - testVal.allowed.bitfield !== value.allowed.bitfield; + testVal.deny.bitfield !== value.deny.bitfield || + testVal.allow.bitfield !== value.allow.bitfield; }); } @@ -133,7 +133,7 @@ class GuildChannel extends Channel { /** * Gets the overall set of permissions for a member in this channel, taking into account channel overwrites. * @param {GuildMember} member The member to obtain the overall permissions for - * @returns {Permissions} + * @returns {ReadOnly} * @private */ memberPermissions(member) { @@ -147,19 +147,19 @@ class GuildChannel extends Channel { const overwrites = this.overwritesFor(member, true, roles); return permissions - .remove(overwrites.everyone ? overwrites.everyone.denied : 0) - .add(overwrites.everyone ? overwrites.everyone.allowed : 0) - .remove(overwrites.roles.length > 0 ? overwrites.roles.map(role => role.denied) : 0) - .add(overwrites.roles.length > 0 ? overwrites.roles.map(role => role.allowed) : 0) - .remove(overwrites.member ? overwrites.member.denied : 0) - .add(overwrites.member ? overwrites.member.allowed : 0) + .remove(overwrites.everyone ? overwrites.everyone.deny : 0) + .add(overwrites.everyone ? overwrites.everyone.allow : 0) + .remove(overwrites.roles.length > 0 ? overwrites.roles.map(role => role.deny) : 0) + .add(overwrites.roles.length > 0 ? overwrites.roles.map(role => role.allow) : 0) + .remove(overwrites.member ? overwrites.member.deny : 0) + .add(overwrites.member ? overwrites.member.allow : 0) .freeze(); } /** * Gets the overall set of permissions for a role in this channel, taking into account channel overwrites. * @param {Role} role The role to obtain the overall permissions for - * @returns {Permissions} + * @returns {ReadOnly} * @private */ rolePermissions(role) { @@ -169,10 +169,10 @@ class GuildChannel extends Channel { const roleOverwrites = this.permissionOverwrites.get(role.id); return role.permissions - .remove(everyoneOverwrites ? everyoneOverwrites.denied : 0) - .add(everyoneOverwrites ? everyoneOverwrites.allowed : 0) - .remove(roleOverwrites ? roleOverwrites.denied : 0) - .add(roleOverwrites ? roleOverwrites.allowed : 0) + .remove(everyoneOverwrites ? everyoneOverwrites.deny : 0) + .add(everyoneOverwrites ? everyoneOverwrites.allow : 0) + .remove(roleOverwrites ? roleOverwrites.deny : 0) + .add(roleOverwrites ? roleOverwrites.allow : 0) .freeze(); } @@ -188,7 +188,7 @@ class GuildChannel extends Channel { * overwrites: [ * { * id: message.author.id, - * denied: ['VIEW_CHANNEL'], + * deny: ['VIEW_CHANNEL'], * }, * ], * reason: 'Needed to change permissions' @@ -227,8 +227,8 @@ class GuildChannel extends Channel { * .catch(console.error); */ updateOverwrite(userOrRole, options, reason) { - const allow = new Permissions(0); - const deny = new Permissions(0); + const allow = new Permissions(); + const deny = new Permissions(); let type; const role = this.guild.roles.get(userOrRole); @@ -245,20 +245,20 @@ class GuildChannel extends Channel { const prevOverwrite = this.permissionOverwrites.get(userOrRole.id); if (prevOverwrite) { - allow.add(prevOverwrite.allowed); - deny.add(prevOverwrite.denied); + allow.add(prevOverwrite.allow); + deny.add(prevOverwrite.deny); } for (const perm in options) { if (options[perm] === true) { - allow.add(Permissions.FLAGS[perm] || 0); - deny.remove(Permissions.FLAGS[perm] || 0); + allow.add(Permissions.FLAGS[perm]); + deny.remove(Permissions.FLAGS[perm]); } else if (options[perm] === false) { - allow.remove(Permissions.FLAGS[perm] || 0); - deny.add(Permissions.FLAGS[perm] || 0); + allow.remove(Permissions.FLAGS[perm]); + deny.add(Permissions.FLAGS[perm]); } else if (options[perm] === null) { - allow.remove(Permissions.FLAGS[perm] || 0); - deny.remove(Permissions.FLAGS[perm] || 0); + allow.remove(Permissions.FLAGS[perm]); + deny.remove(Permissions.FLAGS[perm]); } } @@ -274,8 +274,8 @@ class GuildChannel extends Channel { lockPermissions() { if (!this.parent) return Promise.reject(new Error('GUILD_CHANNEL_ORPHAN')); const permissionOverwrites = this.parent.permissionOverwrites.map(overwrite => ({ - deny: overwrite.denied.bitfield, - allow: overwrite.allowed.bitfield, + deny: overwrite.deny.bitfield, + allow: overwrite.allow.bitfield, id: overwrite.id, type: overwrite.type, })); diff --git a/src/structures/GuildMember.js b/src/structures/GuildMember.js index fccfcdd4a..de42e354c 100644 --- a/src/structures/GuildMember.js +++ b/src/structures/GuildMember.js @@ -168,7 +168,7 @@ class GuildMember extends Base { /** * The overall set of permissions for this member, taking only roles into account - * @type {Permissions} + * @type {ReadOnly} * @readonly */ get permissions() { @@ -209,7 +209,7 @@ class GuildMember extends Base { * Returns `channel.permissionsFor(guildMember)`. Returns permissions for a member in a guild channel, * taking into account roles and permission overwrites. * @param {ChannelResolvable} channel The guild channel to use as context - * @returns {?Permissions} + * @returns {ReadOnly} */ permissionsIn(channel) { channel = this.guild.channels.resolve(channel); @@ -230,16 +230,6 @@ class GuildMember extends Base { return this.roles.some(r => r.permissions.has(permission, checkAdmin)); } - /** - * Checks whether the roles of this member allows them to perform specific actions, and lists any missing permissions. - * @param {PermissionResolvable} permissions The permissions to check for - * @param {boolean} [explicit=false] Whether to require the member to explicitly have the exact permissions - * @returns {PermissionResolvable[]} - */ - missingPermissions(permissions, explicit = false) { - return this.permissions.missing(permissions, explicit); - } - /** * The data for editing a guild member. * @typedef {Object} GuildMemberEditData diff --git a/src/structures/PermissionOverwrites.js b/src/structures/PermissionOverwrites.js index 5533a95a8..f2ece7ced 100644 --- a/src/structures/PermissionOverwrites.js +++ b/src/structures/PermissionOverwrites.js @@ -39,15 +39,15 @@ class PermissionOverwrites { /** * The permissions that are denied for the user or role. - * @type {Permissions} + * @type {ReadOnly} */ - this.denied = new Permissions(data.deny).freeze(); + this.deny = new Permissions(data.deny).freeze(); /** * The permissions that are allowed for the user or role. - * @type {Permissions} + * @type {ReadOnly} */ - this.allowed = new Permissions(data.allow).freeze(); + this.allow = new Permissions(data.allow).freeze(); } /** diff --git a/src/structures/Presence.js b/src/structures/Presence.js index 3ade96bf7..00cdaa051 100644 --- a/src/structures/Presence.js +++ b/src/structures/Presence.js @@ -1,5 +1,6 @@ const Util = require('../util/Util'); -const { ActivityTypes, ActivityFlags } = require('../util/Constants'); +const ActivityFlags = require('../util/ActivityFlags'); +const { ActivityTypes } = require('../util/Constants'); /** * Activity sent in a message. @@ -150,15 +151,12 @@ class Activity { this.assets = data.assets ? new RichPresenceAssets(this, data.assets) : null; this.syncID = data.sync_id; - this._flags = data.flags; - } - get flags() { - const flags = []; - for (const [name, flag] of Object.entries(ActivityFlags)) { - if ((this._flags & flag) === flag) flags.push(name); - } - return flags; + /** + * Flags that describe the activity + * @type {ReadoOnly} + */ + this.flags = new ActivityFlags(data.flags).freeze(); } /** diff --git a/src/structures/Role.js b/src/structures/Role.js index 43d74c15d..e6808a822 100644 --- a/src/structures/Role.js +++ b/src/structures/Role.js @@ -54,7 +54,7 @@ class Role extends Base { /** * The permissions of the role - * @type {Permissions} + * @type {ReadOnly} */ this.permissions = new Permissions(data.permissions).freeze(); @@ -203,7 +203,7 @@ class Role extends Base { * Returns `channel.permissionsFor(role)`. Returns permissions for a role in a guild channel, * taking into account permission overwrites. * @param {ChannelResolvable} channel The guild channel to use as context - * @returns {?Permissions} + * @returns {ReadOnly} */ permissionsIn(channel) { channel = this.guild.channels.resolve(channel); diff --git a/src/structures/shared/resolvePermissions.js b/src/structures/shared/resolvePermissions.js index d742b63fb..c06f83e0f 100644 --- a/src/structures/shared/resolvePermissions.js +++ b/src/structures/shared/resolvePermissions.js @@ -14,8 +14,8 @@ module.exports = function resolvePermissions(overwrites) { } return { - allow: Permissions.resolve(overwrite.allowed || 0), - deny: Permissions.resolve(overwrite.denied || 0), + allow: Permissions.resolve(overwrite.allow), + deny: Permissions.resolve(overwrite.deny), type: overwrite.type, id: overwrite.id, }; diff --git a/src/util/ActivityFlags.js b/src/util/ActivityFlags.js new file mode 100644 index 000000000..49fd7a678 --- /dev/null +++ b/src/util/ActivityFlags.js @@ -0,0 +1,29 @@ +const BitField = require('./BitField'); + +/** + * Data structure that makes it easy to interact with an {@link Activity#flags} bitfield. + * @extends {BitField} + */ +class ActivityFlags extends BitField {} + +/** + * Numeric activity flags. All available properties: + * * `INSTANCE` + * * `JOIN` + * * `SPECTATE` + * * `JOIN_REQUEST` + * * `SYNC` + * * `PLAY` + * @type {Object} + * @see {@link https://discordapp.com/developers/docs/topics/gateway#activity-object-activity-flags} + */ +ActivityFlags.FLAGS = { + INSTANCE: 1 << 0, + JOIN: 1 << 1, + SPECTATE: 1 << 2, + JOIN_REQUEST: 1 << 3, + SYNC: 1 << 4, + PLAY: 1 << 5, +}; + +module.exports = ActivityFlags; diff --git a/src/util/BitField.js b/src/util/BitField.js new file mode 100644 index 000000000..2cd9c907a --- /dev/null +++ b/src/util/BitField.js @@ -0,0 +1,151 @@ +const { RangeError } = require('../errors'); + +/** + * 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 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 (bit instanceof Array) 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 (!(bits instanceof Array)) 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 perm in this.constructor.FLAGS) serialized[perm] = this.has(perm, ...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 (bit instanceof Array) return bit.map(p => this.resolve(p)).reduce((prev, p) => prev | p, 0); + if (typeof bit === 'string') return this.FLAGS[bit]; + throw new RangeError('BITFIELD_INVALID'); + } +} + +/** + * 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 69ad7620f..3e1b317d4 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -367,15 +367,6 @@ exports.ActivityTypes = [ 'WATCHING', ]; -exports.ActivityFlags = { - INSTANCE: 1 << 0, - JOIN: 1 << 1, - SPECTATE: 1 << 2, - JOIN_REQUEST: 1 << 3, - SYNC: 1 << 4, - PLAY: 1 << 5, -}; - exports.ChannelTypes = { TEXT: 0, DM: 1, diff --git a/src/util/Permissions.js b/src/util/Permissions.js index 2f56b816f..295346b16 100644 --- a/src/util/Permissions.js +++ b/src/util/Permissions.js @@ -1,120 +1,12 @@ -const { RangeError } = require('../errors'); +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 Permissions { - /** - * @param {PermissionResolvable} permissions Permission(s) to read from - */ - constructor(permissions) { - /** - * Bitfield of the packed permissions - * @type {number} - */ - this.bitfield = this.constructor.resolve(permissions); - } - - /** - * 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) { - 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; - } - - /** - * Gets all given permissions that are missing from the bitfield. - * @param {PermissionResolvable} permissions Permission(s) to check for - * @param {boolean} [checkAdmin=true] Whether to allow the administrator permission to override - * @returns {string[]} - */ - missing(permissions, checkAdmin = true) { - if (!(permissions instanceof Array)) permissions = new this.constructor(permissions).toArray(false); - return permissions.filter(p => !this.has(p, checkAdmin)); - } - - /** - * Freezes these permissions, making them immutable. - * @returns {Permissions} These permissions - */ - freeze() { - return Object.freeze(this); - } - - /** - * Adds permissions to these ones. - * @param {...PermissionResolvable} permissions Permissions to add - * @returns {Permissions} These permissions or new permissions if the instance is frozen. - */ - 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 from these. - * @param {...PermissionResolvable} permissions Permissions to remove - * @returns {Permissions} These permissions or new permissions if the instance is frozen. - */ - 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 in this.constructor.FLAGS) serialized[perm] = this.has(perm, checkAdmin); - return serialized; - } - - /** - * 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)); - } - - toJSON() { - return this.bitfield; - } - - valueOf() { - return this.bitfield; - } - - *[Symbol.iterator]() { - const keys = this.toArray(); - while (keys.length) yield keys.shift(); - } - +class Permissions extends BitField { /** * Data that can be resolved to give a permission number. This can be: * * A string (see {@link Permissions.FLAGS}) @@ -125,16 +17,14 @@ class Permissions { */ /** - * Resolves permissions to their numeric form. - * @param {PermissionResolvable} permission - Permission(s) to resolve - * @returns {number} + * 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} */ - static resolve(permission) { - if (typeof permission === 'number' && permission >= 0) return permission; - if (permission instanceof Permissions) return permission.bitfield; - if (permission instanceof Array) return permission.map(p => this.resolve(p)).reduce((prev, p) => prev | p, 0); - if (typeof permission === 'string') return this.FLAGS[permission]; - throw new RangeError('PERMISSIONS_INVALID'); + has(permission, checkAdmin = true) { + if (checkAdmin && super.has(this.constructor.FLAGS.ADMINISTRATOR)) return true; + return super.has(permission); } } diff --git a/src/util/Speaking.js b/src/util/Speaking.js new file mode 100644 index 000000000..e0e557eaa --- /dev/null +++ b/src/util/Speaking.js @@ -0,0 +1,22 @@ +const BitField = require('./BitField'); + +/** + * Data structure that makes it easy to interact with a {@link VoiceConnection#speaking} + * and {@link guildMemberSpeaking} event bitfields. + * @extends {BitField} + */ +class Speaking extends BitField {} + +/** + * Numeric speaking flags. All available properties: + * * `SPEAKING` + * * `SOUNDSHARE` + * @type {Object} + * @see {@link https://discordapp.com/developers/docs/topics/voice-connections#speaking} + */ +Speaking.FLAGS = { + SPEAKING: 1 << 0, + SOUNDSHARE: 1 << 1, +}; + +module.exports = Speaking; diff --git a/typings/index.d.ts b/typings/index.d.ts index b678c5a2c..67fd0f459 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -35,6 +35,11 @@ declare module 'discord.js' { public equals(activity: Activity): boolean; } + export class ActivityFlags extends BitField { + public static resolve(permission: BitFieldResolvable): number; + public static FLAGS: Record; + } + export class Base { constructor (client: Client); public readonly client: Client; @@ -62,6 +67,24 @@ declare module 'discord.js' { public broadcast: VoiceBroadcast; } + export class BitField { + constructor(bits?: BitFieldResolvable); + public bitfield: number; + public add(...bits: BitFieldResolvable[]): BitField; + public equals(bit: BitFieldResolvable): boolean; + public freeze(): Readonly>; + public has(bit: BitFieldResolvable): boolean; + public missing(bits: BitFieldResolvable, ...hasParams: any[]): S[]; + public remove(...bits: BitFieldResolvable[]): BitField; + public serialize(...hasParams: BitFieldResolvable[]): Record; + public toArray(): S[]; + public toJSON(): number; + public valueOf(): number; + public [Symbol.iterator](): Iterator; + public static resolve(bit?: BitFieldResolvable): number; + public static FLAGS: { [key: string]: number }; + } + export class CategoryChannel extends GuildChannel { public readonly children: Collection; } @@ -125,7 +148,7 @@ declare module 'discord.js' { public on(event: 'guildCreate' | 'guildDelete' | 'guildUnavailable', listener: (guild: Guild) => void): this; public on(event: 'guildMemberAdd' | 'guildMemberAvailable' | 'guildMemberRemove', listener: (member: GuildMember) => void): this; public on(event: 'guildMembersChunk', listener: (members: Collection, guild: Guild) => void): this; - public on(event: 'guildMemberSpeaking', listener: (member: GuildMember, speaking: boolean) => void): this; + public on(event: 'guildMemberSpeaking', listener: (member: GuildMember, speaking: Readonly) => void): this; public on(event: 'guildMemberUpdate', listener: (oldMember: GuildMember, newMember: GuildMember) => void): this; public on(event: 'guildUpdate', listener: (oldGuild: Guild, newGuild: Guild) => void): this; public on(event: 'message' | 'messageDelete' | 'messageReactionRemoveAll', listener: (message: Message) => void): this; @@ -156,7 +179,7 @@ declare module 'discord.js' { public once(event: 'guildCreate' | 'guildDelete' | 'guildUnavailable', listener: (guild: Guild) => void): this; public once(event: 'guildMemberAdd' | 'guildMemberAvailable' | 'guildMemberRemove', listener: (member: GuildMember) => void): this; public once(event: 'guildMembersChunk', listener: (members: Collection, guild: Guild) => void): this; - public once(event: 'guildMemberSpeaking', listener: (member: GuildMember, speaking: boolean) => void): this; + public once(event: 'guildMemberSpeaking', listener: (member: GuildMember, speaking: Readonly) => void): this; public once(event: 'guildMemberUpdate', listener: (oldMember: GuildMember, newMember: GuildMember) => void): this; public once(event: 'guildUpdate', listener: (oldGuild: Guild, newGuild: Guild) => void): this; public once(event: 'message' | 'messageDelete' | 'messageReactionRemoveAll', listener: (message: Message) => void): this; @@ -450,8 +473,8 @@ declare module 'discord.js' { export class GuildChannel extends Channel { constructor(guild: Guild, data: object); - private memberPermissions(member: GuildMember): Permissions; - private rolePermissions(role: Role): Permissions; + private memberPermissions(member: GuildMember): Readonly; + private rolePermissions(role: Role): Readonly; public readonly calculatedPosition: number; public readonly deletable: boolean; @@ -513,7 +536,7 @@ declare module 'discord.js' { public readonly kickable: boolean; public readonly manageable: boolean; public nickname: string; - public readonly permissions: Permissions; + public readonly permissions: Readonly; public readonly presence: Presence; public roles: GuildMemberRoleStore; public user: User; @@ -524,8 +547,7 @@ declare module 'discord.js' { public edit(data: GuildMemberEditData, reason?: string): Promise; public hasPermission(permission: PermissionResolvable, options?: { checkAdmin?: boolean; checkOwner?: boolean }): boolean; public kick(reason?: string): Promise; - public missingPermissions(permissions: PermissionResolvable, explicit?: boolean): PermissionString[]; - public permissionsIn(channel: ChannelResolvable): Permissions; + public permissionsIn(channel: ChannelResolvable): Readonly; public setDeaf(deaf: boolean, reason?: string): Promise; public setMute(mute: boolean, reason?: string): Promise; public setNickname(nickname: string, reason?: string): Promise; @@ -710,29 +732,18 @@ declare module 'discord.js' { export class PermissionOverwrites { constructor(guildChannel: GuildChannel, data: object); - public allowed: Permissions; + public allow: Readonly; public readonly channel: GuildChannel; - public denied: Permissions; + public deny: Readonly; public id: Snowflake; public type: OverwriteType; public delete(reason?: string): Promise; public toJSON(): object; } - export class Permissions { - constructor(permissions: PermissionResolvable); - - public bitfield: number; - public add(...permissions: PermissionResolvable[]): this; - public freeze(): this; + export class Permissions extends BitField { public has(permission: PermissionResolvable, checkAdmin?: boolean): boolean; - public missing(permissions: PermissionResolvable, checkAdmin?: boolean): PermissionString[]; - public remove(...permissions: PermissionResolvable[]): this; - public serialize(checkAdmin?: boolean): PermissionObject; - public toArray(checkAdmin?: boolean): PermissionString[]; - public toJSON(): object; - public valueOf(): number; - public [Symbol.iterator](): IterableIterator; + public has(bit: BitFieldResolvable): boolean; public static ALL: number; public static DEFAULT: number; @@ -743,6 +754,7 @@ declare module 'discord.js' { export class Presence { constructor(client: Client, data: object); public activity: Activity; + public flags: Readonly; public status: 'online' | 'offline' | 'idle' | 'dnd'; public readonly user: User; public readonly member?: GuildMember; @@ -807,14 +819,14 @@ declare module 'discord.js' { public readonly members: Collection; public mentionable: boolean; public name: string; - public permissions: Permissions; + public permissions: Readonly; public readonly position: number; public rawPosition: number; public comparePositionTo(role: Role): number; public delete(reason?: string): Promise; public edit(data: RoleData, reason?: string): Promise; public equals(role: Role): boolean; - public permissionsIn(channel: ChannelResolvable): Permissions; + public permissionsIn(channel: ChannelResolvable): Readonly; public setColor(color: ColorResolvable, reason?: string): Promise; public setHoist(hoist: boolean, reason?: string): Promise; public setMentionable(mentionable: boolean, reason?: string): Promise; @@ -959,6 +971,11 @@ declare module 'discord.js' { public once(event: string, listener: Function): this; } + export class Speaking extends BitField { + public static resolve(permission: BitFieldResolvable): number; + public static FLAGS: Record; + } + export class Structures { static get(structure: K): Extendable[K]; static get(structure: string): Function; @@ -1097,7 +1114,7 @@ declare module 'discord.js' { private reconnect(token: string, endpoint: string): void; private sendVoiceStateUpdate(options: object): void; private setSessionID(sessionID: string): void; - private setSpeaking(value: boolean): void; + private setSpeaking(value: BitFieldResolvable): void; private setTokenAndEndpoint(token: string, endpoint: string): void; private updateChannel(channel: VoiceChannel): void; @@ -1106,7 +1123,7 @@ declare module 'discord.js' { public readonly dispatcher: StreamDispatcher; public player: object; public receiver: VoiceReceiver; - public speaking: boolean; + public speaking: Readonly; public status: VoiceStatus; public voiceManager: object; public disconnect(): void; @@ -1121,7 +1138,7 @@ declare module 'discord.js' { public on(event: 'newSession', listener: () => void): this; public on(event: 'ready', listener: () => void): this; public on(event: 'reconnecting', listener: () => void): this; - public on(event: 'speaking', listener: (user: User, speaking: boolean) => void): this; + public on(event: 'speaking', listener: (user: User, speaking: Readonly) => void): this; public on(event: 'warn', listener: (warning: string | Error) => void): this; public on(event: string, listener: Function): this; @@ -1134,7 +1151,7 @@ declare module 'discord.js' { public once(event: 'newSession', listener: () => void): this; public once(event: 'ready', listener: () => void): this; public once(event: 'reconnecting', listener: () => void): this; - public once(event: 'speaking', listener: (user: User, speaking: boolean) => void): this; + public once(event: 'speaking', listener: (user: User, speaking: Readonly) => void): this; public once(event: 'warn', listener: (warning: string | Error) => void): this; public once(event: string, listener: Function): this; } @@ -1360,6 +1377,8 @@ declare module 'discord.js' { //#region Typedefs + type ActivityFlagsString = 'INSTANCE' | 'JOIN' | 'SPECTATE' | 'JOIN_REQUEST' | 'SYNC' | 'PLAY'; + type ActivityType = 'PLAYING' | 'STREAMING' | 'LISTENING' @@ -1444,6 +1463,8 @@ declare module 'discord.js' { type Base64String = string; + type BitFieldResolvable = RecursiveArray> | T | number | BitField; + type BufferResolvable = Buffer | string; type ChannelCreationOverwrites = { @@ -1857,8 +1878,8 @@ declare module 'discord.js' { type PermissionResolvable = RecursiveArray | Permissions | PermissionString | number; type PermissionOverwriteOptions = { - allowed: PermissionResolvable; - denied: PermissionResolvable; + allow: PermissionResolvable; + deny: PermissionResolvable; id: UserResolvable | RoleResolvable; }; @@ -1924,6 +1945,8 @@ declare module 'discord.js' { highWaterMark?: number; }; + type SpeakingString = 'SPEAKING' | 'SOUNDSHARE'; + type StreamType = 'unknown' | 'converted' | 'opus' | 'ogg/opus' | 'webm/opus'; type StringResolvable = string | string[] | any;