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
This commit is contained in:
bdistin
2018-08-21 04:56:41 -05:00
committed by SpaceEEC
parent 6be8172539
commit c62f01f0e4
18 changed files with 339 additions and 242 deletions

View File

@@ -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`
);

View File

@@ -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<Speaking>}
*/
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<Snowflake, boolean>}
* @type {Map<Snowflake, ReadOnly<Speaking>>}
* @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>} 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>} speaking The speaking state of the member
*/
this.client.emit(Events.GUILD_MEMBER_SPEAKING, member, speaking);
}

View File

@@ -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}`);
});
}

View File

@@ -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.',

View File

@@ -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'),

View File

@@ -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'],
* },
* ],
* })

View File

@@ -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<Permissions>}
* @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<Permissions>}
* @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,
}));

View File

@@ -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<Permissions>}
* @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<Permissions>}
*/
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

View File

@@ -39,15 +39,15 @@ class PermissionOverwrites {
/**
* The permissions that are denied for the user or role.
* @type {Permissions}
* @type {ReadOnly<Permissions>}
*/
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<Permissions>}
*/
this.allowed = new Permissions(data.allow).freeze();
this.allow = new Permissions(data.allow).freeze();
}
/**

View File

@@ -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<ActivityFlags>}
*/
this.flags = new ActivityFlags(data.flags).freeze();
}
/**

View File

@@ -54,7 +54,7 @@ class Role extends Base {
/**
* The permissions of the role
* @type {Permissions}
* @type {ReadOnly<Permissions>}
*/
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<Permissions>}
*/
permissionsIn(channel) {
channel = this.guild.channels.resolve(channel);

View File

@@ -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,
};

29
src/util/ActivityFlags.js Normal file
View File

@@ -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;

151
src/util/BitField.js Normal file
View File

@@ -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<BitField>} 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.
* <info>Defined in extension classes</info>
* @type {Object}
* @abstract
*/
BitField.FLAGS = {};
module.exports = BitField;

View File

@@ -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,

View File

@@ -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);
}
}

22
src/util/Speaking.js Normal file
View File

@@ -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;

83
typings/index.d.ts vendored
View File

@@ -35,6 +35,11 @@ declare module 'discord.js' {
public equals(activity: Activity): boolean;
}
export class ActivityFlags extends BitField<ActivityFlagsString> {
public static resolve(permission: BitFieldResolvable<ActivityFlagsString>): number;
public static FLAGS: Record<ActivityFlagsString, number>;
}
export class Base {
constructor (client: Client);
public readonly client: Client;
@@ -62,6 +67,24 @@ declare module 'discord.js' {
public broadcast: VoiceBroadcast;
}
export class BitField<S extends string> {
constructor(bits?: BitFieldResolvable<S>);
public bitfield: number;
public add(...bits: BitFieldResolvable<S>[]): BitField<S>;
public equals(bit: BitFieldResolvable<S>): boolean;
public freeze(): Readonly<BitField<S>>;
public has(bit: BitFieldResolvable<S>): boolean;
public missing(bits: BitFieldResolvable<S>, ...hasParams: any[]): S[];
public remove(...bits: BitFieldResolvable<S>[]): BitField<S>;
public serialize(...hasParams: BitFieldResolvable<S>[]): Record<S, boolean>;
public toArray(): S[];
public toJSON(): number;
public valueOf(): number;
public [Symbol.iterator](): Iterator<S>;
public static resolve(bit?: BitFieldResolvable<string>): number;
public static FLAGS: { [key: string]: number };
}
export class CategoryChannel extends GuildChannel {
public readonly children: Collection<Snowflake, GuildChannel>;
}
@@ -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<Snowflake, GuildMember>, guild: Guild) => void): this;
public on(event: 'guildMemberSpeaking', listener: (member: GuildMember, speaking: boolean) => void): this;
public on(event: 'guildMemberSpeaking', listener: (member: GuildMember, speaking: Readonly<Speaking>) => 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<Snowflake, GuildMember>, guild: Guild) => void): this;
public once(event: 'guildMemberSpeaking', listener: (member: GuildMember, speaking: boolean) => void): this;
public once(event: 'guildMemberSpeaking', listener: (member: GuildMember, speaking: Readonly<Speaking>) => 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<Permissions>;
private rolePermissions(role: Role): Readonly<Permissions>;
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<Permissions>;
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<GuildMember>;
public hasPermission(permission: PermissionResolvable, options?: { checkAdmin?: boolean; checkOwner?: boolean }): boolean;
public kick(reason?: string): Promise<GuildMember>;
public missingPermissions(permissions: PermissionResolvable, explicit?: boolean): PermissionString[];
public permissionsIn(channel: ChannelResolvable): Permissions;
public permissionsIn(channel: ChannelResolvable): Readonly<Permissions>;
public setDeaf(deaf: boolean, reason?: string): Promise<GuildMember>;
public setMute(mute: boolean, reason?: string): Promise<GuildMember>;
public setNickname(nickname: string, reason?: string): Promise<GuildMember>;
@@ -710,29 +732,18 @@ declare module 'discord.js' {
export class PermissionOverwrites {
constructor(guildChannel: GuildChannel, data: object);
public allowed: Permissions;
public allow: Readonly<Permissions>;
public readonly channel: GuildChannel;
public denied: Permissions;
public deny: Readonly<Permissions>;
public id: Snowflake;
public type: OverwriteType;
public delete(reason?: string): Promise<PermissionOverwrites>;
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<PermissionString> {
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<PermissionString>;
public has(bit: BitFieldResolvable<PermissionString>): 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<ActivityFlags>;
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<Snowflake, GuildMember>;
public mentionable: boolean;
public name: string;
public permissions: Permissions;
public permissions: Readonly<Permissions>;
public readonly position: number;
public rawPosition: number;
public comparePositionTo(role: Role): number;
public delete(reason?: string): Promise<Role>;
public edit(data: RoleData, reason?: string): Promise<Role>;
public equals(role: Role): boolean;
public permissionsIn(channel: ChannelResolvable): Permissions;
public permissionsIn(channel: ChannelResolvable): Readonly<Permissions>;
public setColor(color: ColorResolvable, reason?: string): Promise<Role>;
public setHoist(hoist: boolean, reason?: string): Promise<Role>;
public setMentionable(mentionable: boolean, reason?: string): Promise<Role>;
@@ -959,6 +971,11 @@ declare module 'discord.js' {
public once(event: string, listener: Function): this;
}
export class Speaking extends BitField<SpeakingString> {
public static resolve(permission: BitFieldResolvable<SpeakingString>): number;
public static FLAGS: Record<SpeakingString, number>;
}
export class Structures {
static get<K extends keyof Extendable>(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<SpeakingString>): 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<Speaking>;
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<Speaking>) => 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<Speaking>) => 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<T extends string> = RecursiveArray<T | number | BitField<T>> | T | number | BitField<T>;
type BufferResolvable = Buffer | string;
type ChannelCreationOverwrites = {
@@ -1857,8 +1878,8 @@ declare module 'discord.js' {
type PermissionResolvable = RecursiveArray<Permissions | PermissionString | number> | 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;