Cleanup Part 2: Electric Boogaloo (Reloaded) (#594)

* Cleanup Part 2: Electric Boogaloo (Reloaded)

* Moar cleanup

* Tweak NOT_A_PERMISSION error
This commit is contained in:
Schuyler Cebulskie
2016-09-04 05:08:09 -04:00
committed by Amish Shah
parent 5a9c42061f
commit 0b908f5bce
95 changed files with 946 additions and 1526 deletions

View File

@@ -15,10 +15,8 @@ const Collection = require('../util/Collection');
* @extends {EventEmitter} * @extends {EventEmitter}
*/ */
class Client extends EventEmitter { class Client extends EventEmitter {
/** /**
* Creates an instance of Client. * @param {ClientOptions} [options] Options for the client
* @param {ClientOptions} [options] options to pass to the client
*/ */
constructor(options) { constructor(options) {
super(); super();
@@ -129,17 +127,13 @@ class Client extends EventEmitter {
* client.login(email, password); * client.login(email, password);
*/ */
login(emailOrToken, password) { login(emailOrToken, password) {
if (password) { if (password) return this.rest.methods.loginEmailPassword(emailOrToken, password);
// login with email and password
return this.rest.methods.loginEmailPassword(emailOrToken, password);
}
// login with token
return this.rest.methods.loginToken(emailOrToken); return this.rest.methods.loginToken(emailOrToken);
} }
/** /**
* Destroys the client and logs out. Resolves with null if successful. * Destroys the client and logs out.
* @returns {Promise<null, Error>} * @returns {Promise}
*/ */
destroy() { destroy() {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
@@ -152,8 +146,7 @@ class Client extends EventEmitter {
this._timeouts = []; this._timeouts = [];
this._intervals = []; this._intervals = [];
resolve(); resolve();
}) }).catch(reject);
.catch(reject);
}); });
} }
@@ -176,7 +169,7 @@ class Client extends EventEmitter {
/** /**
* This shouldn't really be necessary to most developers as it is automatically invoked every 30 seconds, however * This shouldn't really be necessary to most developers as it is automatically invoked every 30 seconds, however
* if you wish to force a sync of Guild data, you can use this. Only applicable to user accounts. * if you wish to force a sync of Guild data, you can use this. Only applicable to user accounts.
* @param {array<Guild>} [guilds=this.guilds.array()] An array of guilds to sync. * @param {Guild[]} [guilds=this.guilds.array()] An array of guilds to sync
*/ */
syncGuilds(guilds = this.guilds.array()) { syncGuilds(guilds = this.guilds.array()) {
if (!this.user.bot) { if (!this.user.bot) {
@@ -215,7 +208,6 @@ class Client extends EventEmitter {
get uptime() { get uptime() {
return this.readyTime ? Date.now() - this.readyTime : null; return this.readyTime ? Date.now() - this.readyTime : null;
} }
} }
module.exports = Client; module.exports = Client;

View File

@@ -23,11 +23,10 @@ class ClientDataManager {
this.client.guilds.set(guild.id, guild); this.client.guilds.set(guild.id, guild);
if (this.pastReady && !already) { if (this.pastReady && !already) {
/** /**
* Emitted whenever the client joins a Guild. * Emitted whenever the client joins a Guild.
* * @event Client#guildCreate
* @event Client#guildCreate * @param {Guild} guild The created guild
* @param {Guild} guild the created guild. */
*/
this.client.emit(Constants.Events.GUILD_CREATE, guild); this.client.emit(Constants.Events.GUILD_CREATE, guild);
} }
@@ -35,16 +34,13 @@ class ClientDataManager {
} }
newUser(data) { newUser(data) {
if (this.client.users.has(data.id)) { if (this.client.users.has(data.id)) return this.client.users.get(data.id);
return this.client.users.get(data.id);
}
const user = new User(this.client, data); const user = new User(this.client, data);
this.client.users.set(user.id, user); this.client.users.set(user.id, user);
return user; return user;
} }
newChannel(data, $guild) { newChannel(data, guild) {
let guild = $guild;
const already = this.client.channels.has(data.id); const already = this.client.channels.has(data.id);
let channel; let channel;
if (data.type === Constants.ChannelTypes.DM) { if (data.type === Constants.ChannelTypes.DM) {
@@ -65,22 +61,18 @@ class ClientDataManager {
} }
if (channel) { if (channel) {
if (this.pastReady && !already) { if (this.pastReady && !already) this.client.emit(Constants.Events.CHANNEL_CREATE, channel);
this.client.emit(Constants.Events.CHANNEL_CREATE, channel);
}
this.client.channels.set(channel.id, channel); this.client.channels.set(channel.id, channel);
return channel; return channel;
} }
return null; return null;
} }
killGuild(guild) { killGuild(guild) {
const already = this.client.guilds.has(guild.id); const already = this.client.guilds.has(guild.id);
this.client.guilds.delete(guild.id); this.client.guilds.delete(guild.id);
if (already && this.pastReady) { if (already && this.pastReady) this.client.emit(Constants.Events.GUILD_DELETE, guild);
this.client.emit(Constants.Events.GUILD_DELETE, guild);
}
} }
killUser(user) { killUser(user) {
@@ -89,17 +81,13 @@ class ClientDataManager {
killChannel(channel) { killChannel(channel) {
this.client.channels.delete(channel.id); this.client.channels.delete(channel.id);
if (channel instanceof GuildChannel) { if (channel instanceof GuildChannel) channel.guild.channels.delete(channel.id);
channel.guild.channels.delete(channel.id);
}
} }
updateGuild(currentGuild, newData) { updateGuild(currentGuild, newData) {
const oldGuild = cloneObject(currentGuild); const oldGuild = cloneObject(currentGuild);
currentGuild.setup(newData); currentGuild.setup(newData);
if (this.pastReady) { if (this.pastReady) this.client.emit(Constants.Events.GUILD_UPDATE, oldGuild, currentGuild);
this.client.emit(Constants.Events.GUILD_UPDATE, oldGuild, currentGuild);
}
} }
updateChannel(currentChannel, newData) { updateChannel(currentChannel, newData) {

View File

@@ -2,13 +2,12 @@ const path = require('path');
const fs = require('fs'); const fs = require('fs');
const request = require('superagent'); const request = require('superagent');
const getStructure = name => require(`../structures/${name}`); const Constants = require('../util/constants');
const User = require(`../structures/User`);
const User = getStructure('User'); const Message = require(`../structures/Message`);
const Message = getStructure('Message'); const Guild = require(`../structures/Guild`);
const Guild = getStructure('Guild'); const Channel = require(`../structures/Channel`);
const Channel = getStructure('Channel'); const GuildMember = require(`../structures/GuildMember`);
const GuildMember = getStructure('GuildMember');
/** /**
* The DataResolver identifies different objects and tries to resolve a specific piece of information from them, e.g. * The DataResolver identifies different objects and tries to resolve a specific piece of information from them, e.g.
@@ -16,7 +15,9 @@ const GuildMember = getStructure('GuildMember');
* @private * @private
*/ */
class ClientDataResolver { class ClientDataResolver {
/**
* @param {Client} client The client the resolver is for
*/
constructor(client) { constructor(client) {
this.client = client; this.client = client;
} }
@@ -33,7 +34,7 @@ class ClientDataResolver {
/** /**
* Resolves a UserResolvable to a User object * Resolves a UserResolvable to a User object
* @param {UserResolvable} user the UserResolvable to identify * @param {UserResolvable} user The UserResolvable to identify
* @returns {?User} * @returns {?User}
*/ */
resolveUser(user) { resolveUser(user) {
@@ -60,18 +61,12 @@ class ClientDataResolver {
/** /**
* Resolves a GuildResolvable to a Guild object * Resolves a GuildResolvable to a Guild object
* @param {GuildResolvable} guild the GuildResolvable to identify * @param {GuildResolvable} guild The GuildResolvable to identify
* @returns {?Guild} * @returns {?Guild}
*/ */
resolveGuild(guild) { resolveGuild(guild) {
if (guild instanceof Guild) { if (guild instanceof Guild) return guild;
return guild; if (typeof guild === 'string') return this.client.guilds.get(guild);
}
if (typeof guild === 'string') {
return this.client.guilds.get(guild);
}
return null; return null;
} }
@@ -84,21 +79,16 @@ class ClientDataResolver {
/** /**
* Resolves a GuildMemberResolvable to a GuildMember object * Resolves a GuildMemberResolvable to a GuildMember object
* @param {GuildResolvable} guild the guild that the member is part of * @param {GuildResolvable} guild The guild that the member is part of
* @param {UserResolvable} user the user that is part of the guild * @param {UserResolvable} user The user that is part of the guild
* @returns {?GuildMember} * @returns {?GuildMember}
*/ */
resolveGuildMember(guild, user) { resolveGuildMember(guild, user) {
if (user instanceof GuildMember) { if (user instanceof GuildMember) return user;
return user;
}
guild = this.resolveGuild(guild); guild = this.resolveGuild(guild);
user = this.resolveUser(user); user = this.resolveUser(user);
if (!guild || !user) return null;
if (!guild || !user) {
return null;
}
return guild.members.get(user.id); return guild.members.get(user.id);
} }
@@ -112,14 +102,11 @@ class ClientDataResolver {
/** /**
* Resolves a Base64Resolvable to a Base 64 image * Resolves a Base64Resolvable to a Base 64 image
* @param {Base64Resolvable} data the base 64 resolvable you want to resolve * @param {Base64Resolvable} data The base 64 resolvable you want to resolve
* @returns {?string} * @returns {?string}
*/ */
resolveBase64(data) { resolveBase64(data) {
if (data instanceof Buffer) { if (data instanceof Buffer) return `data:image/jpg;base64,${data.toString('base64')}`;
return `data:image/jpg;base64,${data.toString('base64')}`;
}
return data; return data;
} }
@@ -132,43 +119,49 @@ class ClientDataResolver {
/** /**
* Resolves a ChannelResolvable to a Channel object * Resolves a ChannelResolvable to a Channel object
* @param {ChannelResolvable} channel the channel resolvable to resolve * @param {ChannelResolvable} channel The channel resolvable to resolve
* @returns {?Channel} * @returns {?Channel}
*/ */
resolveChannel(channel) { resolveChannel(channel) {
if (channel instanceof Channel) { if (channel instanceof Channel) return channel;
return channel; if (typeof channel === 'string') return this.client.channels.get(channel.id);
}
if (typeof channel === 'string') {
return this.client.channels.get(channel.id);
}
return null; return null;
} }
/**
* Data that can be resolved to give a permission number. This can be:
* * A string
* * A permission number
* @typedef {string|number} PermissionResolvable
*/
/**
* Resolves a PermissionResolvable to a permission number
* @param {PermissionResolvable} permission The permission resolvable to resolve
* @returns {number}
*/
resolvePermission(permission) {
if (typeof permission === 'string') permission = Constants.PermissionFlags[permission];
if (!permission) throw Constants.Errors.NOT_A_PERMISSION;
return permission;
}
/** /**
* Data that can be resolved to give a string. This can be: * Data that can be resolved to give a string. This can be:
* * A string * * A string
* * An Array (joined with a new line delimiter to give a string) * * An Array (joined with a new line delimiter to give a string)
* * Any object * * Any value
* @typedef {string|Array|Object} StringResolvable * @typedef {string|Array|*} StringResolvable
*/ */
/** /**
* Resolves a StringResolvable to a string * Resolves a StringResolvable to a string
* @param {StringResolvable} data the string resolvable to resolve * @param {StringResolvable} data The string resolvable to resolve
* @returns {string} * @returns {string}
*/ */
resolveString(data) { resolveString(data) {
if (typeof data === 'string') { if (typeof data === 'string') return data;
return data; if (data instanceof Array) return data.join('\n');
}
if (data instanceof Array) {
return data.join('\n');
}
return String(data); return String(data);
} }
@@ -182,8 +175,8 @@ class ClientDataResolver {
/** /**
* Resolves a FileResolvable to a Buffer * Resolves a FileResolvable to a Buffer
* @param {FileResolvable} resource the file resolvable to resolve * @param {FileResolvable} resource The file resolvable to resolve
* @returns {string|Buffer} * @returns {Promise<Buffer>}
*/ */
resolveFile(resource) { resolveFile(resource) {
if (typeof resource === 'string') { if (typeof resource === 'string') {
@@ -194,19 +187,19 @@ class ClientDataResolver {
.end((err, res) => err ? reject(err) : resolve(res.body)); .end((err, res) => err ? reject(err) : resolve(res.body));
} else { } else {
const file = path.resolve(resource); const file = path.resolve(resource);
const stat = fs.statSync(file); fs.stat(file, (err, stats) => {
if (!stat.isFile()) { if (err) reject(err);
throw new Error(`The file could not be found: ${file}`); if (!stats.isFile()) throw new Error(`The file could not be found: ${file}`);
} fs.readFile(file, (err2, data) => {
if (err2) reject(err2); else resolve(data);
fs.readFile(file, (err, data) => { });
if (err) reject(err); else resolve(data);
}); });
} }
}); });
} }
return Promise.resolve(resource); if (resource instanceof Buffer) return Promise.resolve(resource);
return Promise.reject(new TypeError('resource is not a string or Buffer'));
} }
} }

View File

@@ -5,7 +5,6 @@ const Constants = require('../util/Constants');
* @private * @private
*/ */
class ClientManager { class ClientManager {
constructor(client) { constructor(client) {
/** /**
* The Client that instantiated this Manager * The Client that instantiated this Manager
@@ -21,25 +20,22 @@ class ClientManager {
/** /**
* Connects the Client to the WebSocket * Connects the Client to the WebSocket
* @param {string} token the authorization token * @param {string} token The authorization token
* @param {function} resolve function to run when connection is successful * @param {function} resolve Function to run when connection is successful
* @param {function} reject function to run when connection fails * @param {function} reject Function to run when connection fails
*/ */
connectToWebSocket(token, resolve, reject) { connectToWebSocket(token, resolve, reject) {
this.client.token = token; this.client.token = token;
this.client.rest.methods.getGateway() this.client.rest.methods.getGateway().then(gateway => {
.then(gateway => { this.client.ws.connect(gateway);
this.client.ws.connect(gateway); this.client.once(Constants.Events.READY, () => resolve(token));
this.client.once(Constants.Events.READY, () => resolve(token)); }).catch(reject);
})
.catch(reject);
this.client.setTimeout(() => reject(Constants.Errors.TOOK_TOO_LONG), 1000 * 300); this.client.setTimeout(() => reject(Constants.Errors.TOOK_TOO_LONG), 1000 * 300);
} }
/** /**
* Sets up a keep-alive interval to keep the Client's connection valid * Sets up a keep-alive interval to keep the Client's connection valid
* @param {number} time the interval in milliseconds at which heartbeat packets should be sent * @param {number} time The interval in milliseconds at which heartbeat packets should be sent
*/ */
setupKeepAlive(time) { setupKeepAlive(time) {
this.heartbeatInterval = this.client.setInterval(() => { this.heartbeatInterval = this.client.setInterval(() => {

View File

@@ -11,7 +11,6 @@ that WebSocket events don't clash with REST methods.
*/ */
class GenericAction { class GenericAction {
constructor(client) { constructor(client) {
this.client = client; this.client = client;
} }
@@ -19,7 +18,6 @@ class GenericAction {
handle(data) { handle(data) {
return data; return data;
} }
} }
module.exports = GenericAction; module.exports = GenericAction;

View File

@@ -1,5 +1,4 @@
class ActionsManager { class ActionsManager {
constructor(client) { constructor(client) {
this.client = client; this.client = client;

View File

@@ -1,7 +1,6 @@
const Action = require('./Action'); const Action = require('./Action');
class ChannelCreateAction extends Action { class ChannelCreateAction extends Action {
handle(data) { handle(data) {
const client = this.client; const client = this.client;
const channel = client.dataManager.newChannel(data); const channel = client.dataManager.newChannel(data);

View File

@@ -1,7 +1,6 @@
const Action = require('./Action'); const Action = require('./Action');
class ChannelDeleteAction extends Action { class ChannelDeleteAction extends Action {
constructor(client) { constructor(client) {
super(client); super(client);
this.deleted = new Map(); this.deleted = new Map();

View File

@@ -3,7 +3,6 @@ const Constants = require('../../util/Constants');
const cloneObject = require('../../util/CloneObject'); const cloneObject = require('../../util/CloneObject');
class ChannelUpdateAction extends Action { class ChannelUpdateAction extends Action {
handle(data) { handle(data) {
const client = this.client; const client = this.client;
@@ -26,11 +25,10 @@ class ChannelUpdateAction extends Action {
} }
/** /**
* Emitted whenever a channel is updated - e.g. name change, topic change. * Emitted whenever a channel is updated - e.g. name change, topic change.
* * @event Client#channelUpdate
* @event Client#channelUpdate * @param {Channel} oldChannel The channel before the update
* @param {Channel} oldChannel the channel before the update * @param {Channel} newChannel The channel after the update
* @param {Channel} newChannel the channel after the update */
*/
module.exports = ChannelUpdateAction; module.exports = ChannelUpdateAction;

View File

@@ -2,7 +2,6 @@ const Action = require('./Action');
const Constants = require('../../util/Constants'); const Constants = require('../../util/Constants');
class GuildBanRemove extends Action { class GuildBanRemove extends Action {
handle(data) { handle(data) {
const client = this.client; const client = this.client;
const guild = client.guilds.get(data.guild_id); const guild = client.guilds.get(data.guild_id);

View File

@@ -2,7 +2,6 @@ const Action = require('./Action');
const Constants = require('../../util/Constants'); const Constants = require('../../util/Constants');
class GuildDeleteAction extends Action { class GuildDeleteAction extends Action {
constructor(client) { constructor(client) {
super(client); super(client);
this.deleted = new Map(); this.deleted = new Map();
@@ -45,9 +44,8 @@ class GuildDeleteAction extends Action {
/** /**
* Emitted whenever a guild becomes unavailable, likely due to a server outage. * Emitted whenever a guild becomes unavailable, likely due to a server outage.
*
* @event Client#guildUnavailable * @event Client#guildUnavailable
* @param {Guild} guild the guild that has become unavailable. * @param {Guild} guild The guild that has become unavailable.
*/ */
module.exports = GuildDeleteAction; module.exports = GuildDeleteAction;

View File

@@ -2,7 +2,6 @@ const Action = require('./Action');
const Constants = require('../../util/Constants'); const Constants = require('../../util/Constants');
class GuildMemberRemoveAction extends Action { class GuildMemberRemoveAction extends Action {
constructor(client) { constructor(client) {
super(client); super(client);
this.deleted = new Map(); this.deleted = new Map();
@@ -43,10 +42,9 @@ class GuildMemberRemoveAction extends Action {
/** /**
* Emitted whenever a member leaves a guild, or is kicked. * Emitted whenever a member leaves a guild, or is kicked.
*
* @event Client#guildMemberRemove * @event Client#guildMemberRemove
* @param {Guild} guild the guild that the member has left. * @param {Guild} guild The guild that the member has left.
* @param {GuildMember} member the member that has left the guild. * @param {GuildMember} member The member that has left the guild.
*/ */
module.exports = GuildMemberRemoveAction; module.exports = GuildMemberRemoveAction;

View File

@@ -3,7 +3,6 @@ const Constants = require('../../util/Constants');
const Role = require('../../structures/Role'); const Role = require('../../structures/Role');
class GuildRoleCreate extends Action { class GuildRoleCreate extends Action {
handle(data) { handle(data) {
const client = this.client; const client = this.client;
@@ -25,11 +24,10 @@ class GuildRoleCreate extends Action {
} }
/** /**
* Emitted whenever a guild role is created. * Emitted whenever a guild role is created.
* * @event Client#guildRoleCreate
* @event Client#guildRoleCreate * @param {Guild} guild The guild that the role was created in.
* @param {Guild} guild the guild that the role was created in. * @param {Role} role The role that was created.
* @param {Role} role the role that was created. */
*/
module.exports = GuildRoleCreate; module.exports = GuildRoleCreate;

View File

@@ -2,7 +2,6 @@ const Action = require('./Action');
const Constants = require('../../util/Constants'); const Constants = require('../../util/Constants');
class GuildRoleDeleteAction extends Action { class GuildRoleDeleteAction extends Action {
constructor(client) { constructor(client) {
super(client); super(client);
this.deleted = new Map(); this.deleted = new Map();
@@ -39,11 +38,10 @@ class GuildRoleDeleteAction extends Action {
} }
/** /**
* Emitted whenever a guild role is deleted. * Emitted whenever a guild role is deleted.
* * @event Client#guildRoleDelete
* @event Client#guildRoleDelete * @param {Guild} guild The guild that the role was deleted in.
* @param {Guild} guild the guild that the role was deleted in. * @param {Role} role The role that was deleted.
* @param {Role} role the role that was deleted. */
*/
module.exports = GuildRoleDeleteAction; module.exports = GuildRoleDeleteAction;

View File

@@ -3,7 +3,6 @@ const Constants = require('../../util/Constants');
const cloneObject = require('../../util/CloneObject'); const cloneObject = require('../../util/CloneObject');
class GuildRoleUpdateAction extends Action { class GuildRoleUpdateAction extends Action {
handle(data) { handle(data) {
const client = this.client; const client = this.client;
@@ -33,12 +32,11 @@ class GuildRoleUpdateAction extends Action {
} }
/** /**
* Emitted whenever a guild role is updated. * Emitted whenever a guild role is updated.
* * @event Client#guildRoleUpdated
* @event Client#guildRoleUpdated * @param {Guild} guild The guild that the role was updated in.
* @param {Guild} guild the guild that the role was updated in. * @param {Role} oldRole The role before the update.
* @param {Role} oldRole the role before the update. * @param {Role} newRole The role after the update.
* @param {Role} newRole the role after the update. */
*/
module.exports = GuildRoleUpdateAction; module.exports = GuildRoleUpdateAction;

View File

@@ -1,7 +1,6 @@
const Action = require('./Action'); const Action = require('./Action');
class GuildSync extends Action { class GuildSync extends Action {
handle(data) { handle(data) {
const client = this.client; const client = this.client;

View File

@@ -3,7 +3,6 @@ const Constants = require('../../util/Constants');
const cloneObject = require('../../util/CloneObject'); const cloneObject = require('../../util/CloneObject');
class GuildUpdateAction extends Action { class GuildUpdateAction extends Action {
handle(data) { handle(data) {
const client = this.client; const client = this.client;
@@ -26,11 +25,10 @@ class GuildUpdateAction extends Action {
} }
/** /**
* Emitted whenever a guild is updated - e.g. name change. * Emitted whenever a guild is updated - e.g. name change.
* * @event Client#guildUpdate
* @event Client#guildUpdate * @param {Guild} oldGuild The guild before the update.
* @param {Guild} oldGuild the guild before the update. * @param {Guild} newGuild The guild after the update.
* @param {Guild} newGuild the guild after the update. */
*/
module.exports = GuildUpdateAction; module.exports = GuildUpdateAction;

View File

@@ -2,7 +2,6 @@ const Action = require('./Action');
const Message = require('../../structures/Message'); const Message = require('../../structures/Message');
class MessageCreateAction extends Action { class MessageCreateAction extends Action {
handle(data) { handle(data) {
const client = this.client; const client = this.client;

View File

@@ -1,7 +1,6 @@
const Action = require('./Action'); const Action = require('./Action');
class MessageDeleteAction extends Action { class MessageDeleteAction extends Action {
constructor(client) { constructor(client) {
super(client); super(client);
this.deleted = new Map(); this.deleted = new Map();

View File

@@ -3,7 +3,6 @@ const Collection = require('../../util/Collection');
const Constants = require('../../util/Constants'); const Constants = require('../../util/Constants');
class MessageDeleteBulkAction extends Action { class MessageDeleteBulkAction extends Action {
handle(data) { handle(data) {
const client = this.client; const client = this.client;
const channel = client.channels.get(data.channel_id); const channel = client.channels.get(data.channel_id);
@@ -12,9 +11,7 @@ class MessageDeleteBulkAction extends Action {
const messages = new Collection(); const messages = new Collection();
for (const id of ids) { for (const id of ids) {
const message = channel.messages.get(id); const message = channel.messages.get(id);
if (message) { if (message) messages.set(message.id, message);
messages.set(message.id, message);
}
} }
if (messages.size > 0) client.emit(Constants.Events.MESSAGE_BULK_DELETE, messages); if (messages.size > 0) client.emit(Constants.Events.MESSAGE_BULK_DELETE, messages);

View File

@@ -3,7 +3,6 @@ const Constants = require('../../util/Constants');
const cloneObject = require('../../util/CloneObject'); const cloneObject = require('../../util/CloneObject');
class MessageUpdateAction extends Action { class MessageUpdateAction extends Action {
handle(data) { handle(data) {
const client = this.client; const client = this.client;
@@ -34,11 +33,10 @@ class MessageUpdateAction extends Action {
} }
/** /**
* Emitted whenever a message is updated - e.g. embed or content change. * Emitted whenever a message is updated - e.g. embed or content change.
* * @event Client#messageUpdate
* @event Client#messageUpdate * @param {Message} oldMessage The message before the update.
* @param {Message} oldMessage the message before the update. * @param {Message} newMessage The message after the update.
* @param {Message} newMessage the message after the update. */
*/
module.exports = MessageUpdateAction; module.exports = MessageUpdateAction;

View File

@@ -1,7 +1,6 @@
const Action = require('./Action'); const Action = require('./Action');
class UserGetAction extends Action { class UserGetAction extends Action {
handle(data) { handle(data) {
const client = this.client; const client = this.client;
const user = client.dataManager.newUser(data); const user = client.dataManager.newUser(data);

View File

@@ -3,7 +3,6 @@ const Constants = require('../../util/Constants');
const cloneObject = require('../../util/CloneObject'); const cloneObject = require('../../util/CloneObject');
class UserUpdateAction extends Action { class UserUpdateAction extends Action {
handle(data) { handle(data) {
const client = this.client; const client = this.client;
@@ -32,11 +31,10 @@ class UserUpdateAction extends Action {
} }
/** /**
* Emitted whenever a detail of the logged in User changes - e.g. username. * Emitted whenever a detail of the logged in User changes - e.g. username.
* * @event Client#userUpdate
* @event Client#userUpdate * @param {ClientUser} oldClientUser The client user before the update.
* @param {ClientUser} oldClientUser the client user before the update. * @param {ClientUser} newClientUser The client user after the update.
* @param {ClientUser} newClientUser the client user after the update. */
*/
module.exports = UserUpdateAction; module.exports = UserUpdateAction;

View File

@@ -26,17 +26,12 @@ class APIRequest {
gen() { gen() {
const apiRequest = request[this.method](this.url); const apiRequest = request[this.method](this.url);
if (this.auth) { if (this.auth) apiRequest.set('authorization', this.getAuth());
apiRequest.set('authorization', this.getAuth());
}
if (this.file && this.file.file) { if (this.file && this.file.file) {
apiRequest.set('Content-Type', 'multipart/form-data'); apiRequest.set('Content-Type', 'multipart/form-data');
apiRequest.attach('file', this.file.file, this.file.name); apiRequest.attach('file', this.file.file, this.file.name);
} }
if (this.data) { if (this.data) apiRequest.send(this.data);
apiRequest.send(this.data);
}
apiRequest.set('User-Agent', this.rest.userAgentManager.userAgent); apiRequest.set('User-Agent', this.rest.userAgentManager.userAgent);
return apiRequest; return apiRequest;
} }

View File

@@ -5,7 +5,6 @@ const APIRequest = require('./APIRequest');
const Constants = require('../../util/Constants'); const Constants = require('../../util/Constants');
class RESTManager { class RESTManager {
constructor(client) { constructor(client) {
this.client = client; this.client = client;
this.handlers = {}; this.handlers = {};

View File

@@ -1,11 +1,11 @@
const Constants = require('../../util/Constants'); const Constants = require('../../util/Constants');
const Collection = require('../../util/Collection'); const Collection = require('../../util/Collection');
const getStructure = name => require(`../../structures/${name}`); const requireStructure = name => require(`../../structures/${name}`);
const User = getStructure('User'); const User = requireStructure('User');
const GuildMember = getStructure('GuildMember'); const GuildMember = requireStructure('GuildMember');
const Role = getStructure('Role'); const Role = requireStructure('Role');
const Invite = getStructure('Invite'); const Invite = requireStructure('Invite');
class RESTMethods { class RESTMethods {
constructor(restManager) { constructor(restManager) {
@@ -45,10 +45,9 @@ class RESTMethods {
}); });
} }
sendMessage($channel, content, tts, nonce, file) { sendMessage(channel, content, tts, nonce, file) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const $this = this; const $this = this;
let channel = $channel;
function req() { function req() {
$this.rest.makeRequest('post', Constants.Endpoints.channelMessages(channel.id), true, { $this.rest.makeRequest('post', Constants.Endpoints.channelMessages(channel.id), true, {
@@ -101,11 +100,9 @@ class RESTMethods {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.rest.makeRequest('patch', Constants.Endpoints.channelMessage(message.channel.id, message.id), true, { this.rest.makeRequest('patch', Constants.Endpoints.channelMessage(message.channel.id, message.id), true, {
content, content,
}) }).then(data => {
.then(data => {
resolve(this.rest.client.actions.MessageUpdate.handle(data).updated); resolve(this.rest.client.actions.MessageUpdate.handle(data).updated);
}) }).catch(reject);
.catch(reject);
}); });
} }
@@ -114,11 +111,9 @@ class RESTMethods {
this.rest.makeRequest('post', Constants.Endpoints.guildChannels(guild.id), true, { this.rest.makeRequest('post', Constants.Endpoints.guildChannels(guild.id), true, {
name: channelName, name: channelName,
type: channelType, type: channelType,
}) }).then(data => {
.then(data => { resolve(this.rest.client.actions.ChannelCreate.handle(data).channel);
resolve(this.rest.client.actions.ChannelCreate.handle(data).channel); }).catch(reject);
})
.catch(reject);
}); });
} }
@@ -126,98 +121,73 @@ class RESTMethods {
const dmChannel = Array.from(this.rest.client.channels.values()) const dmChannel = Array.from(this.rest.client.channels.values())
.filter(channel => channel.recipient) .filter(channel => channel.recipient)
.filter(channel => channel.recipient.id === recipient.id); .filter(channel => channel.recipient.id === recipient.id);
return dmChannel[0]; return dmChannel[0];
} }
createDM(recipient) { createDM(recipient) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const dmChannel = this.getExistingDM(recipient); const dmChannel = this.getExistingDM(recipient);
if (dmChannel) return resolve(dmChannel);
if (dmChannel) {
return resolve(dmChannel);
}
return this.rest.makeRequest('post', Constants.Endpoints.userChannels(this.rest.client.user.id), true, { return this.rest.makeRequest('post', Constants.Endpoints.userChannels(this.rest.client.user.id), true, {
recipient_id: recipient.id, recipient_id: recipient.id,
}) }).then(data => resolve(this.rest.client.actions.ChannelCreate.handle(data).channel)).catch(reject);
.then(data => resolve(this.rest.client.actions.ChannelCreate.handle(data).channel))
.catch(reject);
}); });
} }
deleteChannel($channel) { deleteChannel(channel) {
let channel = $channel;
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
if (channel instanceof User || channel instanceof GuildMember) { if (channel instanceof User || channel instanceof GuildMember) channel = this.getExistingDM(channel);
channel = this.getExistingDM(channel); this.rest.makeRequest('del', Constants.Endpoints.channel(channel.id), true).then(data => {
} data.id = channel.id;
resolve(this.rest.client.actions.ChannelDelete.handle(data).channel);
this.rest.makeRequest('del', Constants.Endpoints.channel(channel.id), true) }).catch(reject);
.then($data => {
const data = $data;
data.id = channel.id;
resolve(this.rest.client.actions.ChannelDelete.handle(data).channel);
})
.catch(reject);
}); });
} }
updateChannel(channel, $data) { updateChannel(channel, data) {
const data = $data;
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
data.name = (data.name || channel.name).trim(); data.name = (data.name || channel.name).trim();
data.topic = data.topic || channel.topic; data.topic = data.topic || channel.topic;
data.position = data.position || channel.position; data.position = data.position || channel.position;
data.bitrate = data.bitrate || channel.bitrate; data.bitrate = data.bitrate || channel.bitrate;
this.rest.makeRequest('patch', Constants.Endpoints.channel(channel.id), true, data) this.rest.makeRequest('patch', Constants.Endpoints.channel(channel.id), true, data).then(newData => {
.then(newData => { resolve(this.rest.client.actions.ChannelUpdate.handle(newData).updated);
resolve(this.rest.client.actions.ChannelUpdate.handle(newData).updated); }).catch(reject);
})
.catch(reject);
}); });
} }
leaveGuild(guild) { leaveGuild(guild) {
if (guild.ownerID === this.rest.client.user.id) { if (guild.ownerID === this.rest.client.user.id) return this.deleteGuild(guild);
return this.deleteGuild(guild);
}
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.rest.makeRequest('del', Constants.Endpoints.meGuild(guild.id), true) this.rest.makeRequest('del', Constants.Endpoints.meGuild(guild.id), true).then(() => {
.then(() => { resolve(this.rest.client.actions.GuildDelete.handle({ id: guild.id }).guild);
resolve(this.rest.client.actions.GuildDelete.handle({ id: guild.id }).guild); }).catch(reject);
})
.catch(reject);
}); });
} }
// untested but probably will work // untested but probably will work
deleteGuild(guild) { deleteGuild(guild) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.rest.makeRequest('del', Constants.Endpoints.guild(guild.id), true) this.rest.makeRequest('del', Constants.Endpoints.guild(guild.id), true).then(() => {
.then(() => { resolve(this.rest.client.actions.GuildDelete.handle({ id: guild.id }).guild);
resolve(this.rest.client.actions.GuildDelete.handle({ id: guild.id }).guild); }).catch(reject);
})
.catch(reject);
}); });
} }
getUser(userID) { getUser(userID) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.rest.makeRequest('get', Constants.Endpoints.user(userID), true) this.rest.makeRequest('get', Constants.Endpoints.user(userID), true).then((data) => {
.then((data) => { resolve(this.rest.client.actions.UserGet.handle(data).user);
resolve(this.rest.client.actions.UserGet.handle(data).user); }).catch(reject);
})
.catch(reject);
}); });
} }
updateCurrentUser(_data) { updateCurrentUser(_data) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const user = this.rest.client.user; const user = this.rest.client.user;
const data = {};
const data = {};
data.username = _data.username || user.username; data.username = _data.username || user.username;
data.avatar = this.rest.client.resolver.resolveBase64(_data.avatar) || user.avatar; data.avatar = this.rest.client.resolver.resolveBase64(_data.avatar) || user.avatar;
if (!user.bot) { if (!user.bot) {
@@ -234,44 +204,15 @@ class RESTMethods {
updateGuild(guild, _data) { updateGuild(guild, _data) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
/*
can contain:
name, region, verificationLevel, afkChannel, afkTimeout, icon, owner, splash
*/
const data = {}; const data = {};
if (_data.name) data.name = _data.name;
if (_data.name) { if (_data.region) data.region = _data.region;
data.name = _data.name; if (_data.verificationLevel) data.verification_level = Number(_data.verificationLevel);
} if (_data.afkChannel) data.afk_channel_id = this.rest.client.resolver.resolveChannel(_data.afkChannel).id;
if (_data.afkTimeout) data.afk_timeout = Number(_data.afkTimeout);
if (_data.region) { if (_data.icon) data.icon = this.rest.client.resolver.resolveBase64(_data.icon);
data.region = _data.region; if (_data.owner) data.owner_id = this.rest.client.resolver.resolveUser(_data.owner).id;
} if (_data.splash) data.splash = this.rest.client.resolver.resolveBase64(_data.splash);
if (_data.verificationLevel) {
data.verification_level = Number(_data.verificationLevel);
}
if (_data.afkChannel) {
data.afk_channel_id = this.rest.client.resolver.resolveChannel(_data.afkChannel).id;
}
if (_data.afkTimeout) {
data.afk_timeout = Number(_data.afkTimeout);
}
if (_data.icon) {
data.icon = this.rest.client.resolver.resolveBase64(_data.icon);
}
if (_data.owner) {
data.owner_id = this.rest.client.resolver.resolveUser(_data.owner).id;
}
if (_data.splash) {
data.splash = this.rest.client.resolver.resolveBase64(_data.splash);
}
this.rest.makeRequest('patch', Constants.Endpoints.guild(guild.id), true, data) this.rest.makeRequest('patch', Constants.Endpoints.guild(guild.id), true, data)
.then(newData => resolve(this.rest.client.actions.GuildUpdate.handle(newData).updated)) .then(newData => resolve(this.rest.client.actions.GuildUpdate.handle(newData).updated))
@@ -281,40 +222,34 @@ class RESTMethods {
kickGuildMember(guild, member) { kickGuildMember(guild, member) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.rest.makeRequest('del', Constants.Endpoints.guildMember(guild.id, member.id), true) this.rest.makeRequest('del', Constants.Endpoints.guildMember(guild.id, member.id), true).then(() => {
.then(() => { resolve(this.rest.client.actions.GuildMemberRemove.handle({
resolve(this.rest.client.actions.GuildMemberRemove.handle({ guild_id: guild.id,
guild_id: guild.id, user: member.user,
user: member.user, }).member);
}).member); }).catch(reject);
})
.catch(reject);
}); });
} }
createGuildRole(guild) { createGuildRole(guild) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.rest.makeRequest('post', Constants.Endpoints.guildRoles(guild.id), true) this.rest.makeRequest('post', Constants.Endpoints.guildRoles(guild.id), true).then(role => {
.then(role => { resolve(this.rest.client.actions.GuildRoleCreate.handle({
resolve(this.rest.client.actions.GuildRoleCreate.handle({ guild_id: guild.id,
guild_id: guild.id, role,
role, }).role);
}).role); }).catch(reject);
})
.catch(reject);
}); });
} }
deleteGuildRole(role) { deleteGuildRole(role) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.rest.makeRequest('del', Constants.Endpoints.guildRole(role.guild.id, role.id), true) this.rest.makeRequest('del', Constants.Endpoints.guildRole(role.guild.id, role.id), true).then(() => {
.then(() => { resolve(this.rest.client.actions.GuildRoleDelete.handle({
resolve(this.rest.client.actions.GuildRoleDelete.handle({ guild_id: role.guild.id,
guild_id: role.guild.id, role_id: role.id,
role_id: role.id, }).role);
}).role); }).catch(reject);
})
.catch(reject);
}); });
} }
@@ -338,23 +273,14 @@ class RESTMethods {
getChannelMessages(channel, payload = {}) { getChannelMessages(channel, payload = {}) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const params = []; const params = [];
if (payload.limit) { if (payload.limit) params.push(`limit=${payload.limit}`);
params.push(`limit=${payload.limit}`); if (payload.around) params.push(`around=${payload.around}`);
} else if (payload.before) params.push(`before=${payload.before}`);
if (payload.around) { else if (payload.after) params.push(`after=${payload.after}`);
params.push(`around=${payload.around}`);
} else if (payload.before) {
params.push(`before=${payload.before}`);
} else if (payload.after) {
params.push(`after=${payload.after}`);
}
let request = Constants.Endpoints.channelMessages(channel.id); let endpoint = Constants.Endpoints.channelMessages(channel.id);
if (params.length > 0) { if (params.length > 0) endpoint += `?${params.join('&')}`;
request += `?${params.join('&')}`; this.rest.makeRequest('get', endpoint, true)
}
this.rest.makeRequest('get', request, true)
.then(resolve) .then(resolve)
.catch(reject); .catch(reject);
}); });
@@ -362,13 +288,9 @@ class RESTMethods {
updateGuildMember(member, data) { updateGuildMember(member, data) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
if (data.channel) { if (data.channel) data.channel_id = this.rest.client.resolver.resolveChannel(data.channel).id;
data.channel_id = this.rest.client.resolver.resolveChannel(data.channel).id;
}
if (data.roles) { if (data.roles) {
if (data.roles instanceof Map) { if (data.roles instanceof Collection) data.roles = data.roles.array();
data.roles = data.roles.array();
}
data.roles = data.roles.map(role => role instanceof Role ? role.id : role); data.roles = data.roles.map(role => role instanceof Role ? role.id : role);
} }
@@ -379,6 +301,7 @@ class RESTMethods {
endpoint = Constants.Endpoints.stupidInconsistentGuildEndpoint(member.guild.id); endpoint = Constants.Endpoints.stupidInconsistentGuildEndpoint(member.guild.id);
} }
} }
this.rest.makeRequest('patch', endpoint, true, data) this.rest.makeRequest('patch', endpoint, true, data)
.then(resData => resolve(member.guild._updateMember(member, resData).mem)) .then(resData => resolve(member.guild._updateMember(member, resData).mem))
.catch(reject); .catch(reject);
@@ -407,9 +330,7 @@ class RESTMethods {
unbanGuildMember(guild, member) { unbanGuildMember(guild, member) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
member = this.rest.client.resolver.resolveUser(member); member = this.rest.client.resolver.resolveUser(member);
if (!member) { if (!member) throw new Error('cannot unban a user that is not a user resolvable');
throw new Error('cannot unban a user that is not a user resolvable');
}
const listener = (eGuild, eUser) => { const listener = (eGuild, eUser) => {
if (guild.id === guild.id && member.id === eUser.id) { if (guild.id === guild.id && member.id === eUser.id) {
this.rest.client.removeListener(Constants.Events.GUILD_BAN_REMOVE, listener); this.rest.client.removeListener(Constants.Events.GUILD_BAN_REMOVE, listener);
@@ -423,48 +344,30 @@ class RESTMethods {
getGuildBans(guild) { getGuildBans(guild) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.rest.makeRequest('get', Constants.Endpoints.guildBans(guild.id), true) this.rest.makeRequest('get', Constants.Endpoints.guildBans(guild.id), true).then(banItems => {
.then(banItems => { const bannedUsers = new Collection();
const bannedUsers = new Collection(); for (const banItem of banItems) {
for (const banItem of banItems) { const user = this.rest.client.dataManager.newUser(banItem.user);
const user = this.rest.client.dataManager.newUser(banItem.user); bannedUsers.set(user.id, user);
bannedUsers.set(user.id, user); }
} resolve(bannedUsers);
resolve(bannedUsers); }).catch(reject);
})
.catch(reject);
}); });
} }
updateGuildRole(role, _data) { updateGuildRole(role, _data) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
/*
can contain:
name, position, permissions, color, hoist
*/
const data = {}; const data = {};
data.name = _data.name || role.name; data.name = _data.name || role.name;
data.position = _data.position || role.position; data.position = _data.position || role.position;
data.color = _data.color || role.color; data.color = _data.color || role.color;
if (data.color.startsWith('#')) data.color = parseInt(data.color.replace('#', ''), 16);
if (data.color.startsWith('#')) { data.hoist = typeof _data.hoist !== 'undefined' ? _data.hoist : role.hoist;
data.color = parseInt(data.color.replace('#', ''), 16);
}
if (typeof _data.hoist !== 'undefined') {
data.hoist = _data.hoist;
} else {
data.hoist = role.hoist;
}
if (_data.permissions) { if (_data.permissions) {
let perms = 0; let perms = 0;
for (let perm of _data.permissions) { for (let perm of _data.permissions) {
if (typeof perm === 'string') { if (typeof perm === 'string') perm = Constants.PermissionFlags[perm];
perm = Constants.PermissionFlags[perm];
}
perms |= perm; perms |= perm;
} }
data.permissions = perms; data.permissions = perms;
@@ -472,14 +375,12 @@ class RESTMethods {
data.permissions = role.permissions; data.permissions = role.permissions;
} }
this.rest.makeRequest('patch', Constants.Endpoints.guildRole(role.guild.id, role.id), true, data) this.rest.makeRequest('patch', Constants.Endpoints.guildRole(role.guild.id, role.id), true, data).then(_role => {
.then(_role => { resolve(this.rest.client.actions.GuildRoleUpdate.handle({
resolve(this.rest.client.actions.GuildRoleUpdate.handle({ role: _role,
role: _role, guild_id: role.guild.id,
guild_id: role.guild.id, }).updated);
}).updated); }).catch(reject);
})
.catch(reject);
}); });
} }
@@ -526,16 +427,14 @@ class RESTMethods {
getGuildInvites(guild) { getGuildInvites(guild) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.rest.makeRequest('get', Constants.Endpoints.guildInvites(guild.id), true) this.rest.makeRequest('get', Constants.Endpoints.guildInvites(guild.id), true).then(inviteItems => {
.then(inviteItems => { const invites = new Collection();
const invites = new Collection(); for (const inviteItem of inviteItems) {
for (const inviteItem of inviteItems) { const invite = new Invite(this.rest.client, inviteItem);
const invite = new Invite(this.rest.client, inviteItem); invites.set(invite.code, invite);
invites.set(invite.code, invite); }
} resolve(invites);
resolve(invites); }).catch(reject);
})
.catch(reject);
}); });
} }
} }

View File

@@ -3,6 +3,9 @@
* @private * @private
*/ */
class RequestHandler { class RequestHandler {
/**
* @param {RESTManager} restManager The REST manager to use
*/
constructor(restManager) { constructor(restManager) {
/** /**
* The RESTManager that instantiated this RequestHandler * The RESTManager that instantiated this RequestHandler
@@ -12,7 +15,7 @@ class RequestHandler {
/** /**
* A list of requests that have yet to be processed. * A list of requests that have yet to be processed.
* @type {Array<APIRequest>} * @type {APIRequest[]}
*/ */
this.queue = []; this.queue = [];
} }
@@ -31,7 +34,7 @@ class RequestHandler {
/** /**
* Push a new API request into this bucket * Push a new API request into this bucket
* @param {APIRequest} request the new request to push into the queue * @param {APIRequest} request The new request to push into the queue
*/ */
push(request) { push(request) {
this.queue.push(request); this.queue.push(request);

View File

@@ -8,7 +8,10 @@ const RequestHandler = require('./RequestHandler');
* @private * @private
*/ */
class SequentialRequestHandler extends RequestHandler { class SequentialRequestHandler extends RequestHandler {
/**
* @param {RESTManager} restManager The REST manager to use
* @param {string} endpoint The endpoint to handle
*/
constructor(restManager, endpoint) { constructor(restManager, endpoint) {
super(restManager, endpoint); super(restManager, endpoint);
@@ -39,8 +42,8 @@ class SequentialRequestHandler extends RequestHandler {
/** /**
* Performs a request then resolves a promise to indicate its readiness for a new request * Performs a request then resolves a promise to indicate its readiness for a new request
* @param {APIRequest} item the item to execute * @param {APIRequest} item The item to execute
* @returns {Promise<Object, Error>} * @returns {Promise<?Object|Error>}
*/ */
execute(item) { execute(item) {
return new Promise(resolve => { return new Promise(resolve => {
@@ -88,9 +91,8 @@ class SequentialRequestHandler extends RequestHandler {
handle() { handle() {
super.handle(); super.handle();
if (this.waiting || this.queue.length === 0 || this.globalLimit) {
return; if (this.waiting || this.queue.length === 0 || this.globalLimit) return;
}
this.waiting = true; this.waiting = true;
const item = this.queue[0]; const item = this.queue[0];

View File

@@ -33,9 +33,7 @@ class ClientVoiceManager {
*/ */
_checkPendingReady(guildID) { _checkPendingReady(guildID) {
const pendingRequest = this.pending.get(guildID); const pendingRequest = this.pending.get(guildID);
if (!pendingRequest) { if (!pendingRequest) throw new Error('Guild not pending');
throw new Error('Guild not pending');
}
if (pendingRequest.token && pendingRequest.sessionID && pendingRequest.endpoint) { if (pendingRequest.token && pendingRequest.sessionID && pendingRequest.endpoint) {
const { channel, token, sessionID, endpoint, resolve, reject } = pendingRequest; const { channel, token, sessionID, endpoint, resolve, reject } = pendingRequest;
const voiceConnection = new VoiceConnection(this, channel, token, sessionID, endpoint, resolve, reject); const voiceConnection = new VoiceConnection(this, channel, token, sessionID, endpoint, resolve, reject);
@@ -49,15 +47,13 @@ class ClientVoiceManager {
/** /**
* Called when the Client receives information about this voice server update. * Called when the Client receives information about this voice server update.
* @param {string} guildID the ID of the Guild * @param {string} guildID The ID of the Guild
* @param {string} token the token to authorise with * @param {string} token The token to authorise with
* @param {string} endpoint the endpoint to connect to * @param {string} endpoint The endpoint to connect to
*/ */
_receivedVoiceServer(guildID, token, endpoint) { _receivedVoiceServer(guildID, token, endpoint) {
const pendingRequest = this.pending.get(guildID); const pendingRequest = this.pending.get(guildID);
if (!pendingRequest) { if (!pendingRequest) throw new Error('Guild not pending');
throw new Error('Guild not pending');
}
pendingRequest.token = token; pendingRequest.token = token;
// remove the port otherwise it errors ¯\_(ツ)_/¯ // remove the port otherwise it errors ¯\_(ツ)_/¯
pendingRequest.endpoint = endpoint.match(/([^:]*)/)[0]; pendingRequest.endpoint = endpoint.match(/([^:]*)/)[0];
@@ -66,22 +62,20 @@ class ClientVoiceManager {
/** /**
* Called when the Client receives information about the voice state update. * Called when the Client receives information about the voice state update.
* @param {string} guildID the ID of the Guild * @param {string} guildID The ID of the Guild
* @param {string} sessionID the session id to authorise with * @param {string} sessionID The session id to authorise with
*/ */
_receivedVoiceStateUpdate(guildID, sessionID) { _receivedVoiceStateUpdate(guildID, sessionID) {
const pendingRequest = this.pending.get(guildID); const pendingRequest = this.pending.get(guildID);
if (!pendingRequest) { if (!pendingRequest) throw new Error('Guild not pending');
throw new Error('Guild not pending');
}
pendingRequest.sessionID = sessionID; pendingRequest.sessionID = sessionID;
this._checkPendingReady(guildID); this._checkPendingReady(guildID);
} }
/** /**
* Sends a request to the main gateway to join a voice channel * Sends a request to the main gateway to join a voice channel
* @param {VoiceChannel} channel the channel to join * @param {VoiceChannel} channel The channel to join
* @param {Object} [options] the options to provide * @param {Object} [options] The options to provide
*/ */
_sendWSJoin(channel, options = {}) { _sendWSJoin(channel, options = {}) {
options = mergeDefault({ options = mergeDefault({
@@ -98,14 +92,12 @@ class ClientVoiceManager {
/** /**
* Sets up a request to join a voice channel * Sets up a request to join a voice channel
* @param {VoiceChannel} channel the voice channel to join * @param {VoiceChannel} channel The voice channel to join
* @returns {Promise<VoiceConnection>} * @returns {Promise<VoiceConnection>}
*/ */
joinChannel(channel) { joinChannel(channel) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
if (this.pending.get(channel.guild.id)) { if (this.pending.get(channel.guild.id)) throw new Error('already connecting to a channel in this guild');
throw new Error('already connecting to a channel in this guild');
}
const existingConn = this.connections.get(channel.guild.id); const existingConn = this.connections.get(channel.guild.id);
if (existingConn) { if (existingConn) {
if (existingConn.channel.id !== channel.id) { if (existingConn.channel.id !== channel.id) {

View File

@@ -71,14 +71,14 @@ class VoiceConnection extends EventEmitter {
/** /**
* Executed whenever an error occurs with the UDP/WebSocket sub-client * Executed whenever an error occurs with the UDP/WebSocket sub-client
* @private * @private
* @param {Error} err The error that occurred * @param {Error} err The encountered error
*/ */
_onError(err) { _onError(err) {
this._reject(err); this._reject(err);
/** /**
* Emitted whenever the connection encounters a fatal error. * Emitted whenever the connection encounters a fatal error.
* @event VoiceConnection#error * @event VoiceConnection#error
* @param {Error} error the encountered error * @param {Error} error The encountered error
*/ */
this.emit('error', err); this.emit('error', err);
this._shutdown(err); this._shutdown(err);
@@ -86,7 +86,7 @@ class VoiceConnection extends EventEmitter {
/** /**
* Disconnects the Client from the Voice Channel * Disconnects the Client from the Voice Channel
* @param {string} [reason='user requested'] the reason of the disconnection * @param {string} [reason='user requested'] The reason of the disconnection
*/ */
disconnect(reason = 'user requested') { disconnect(reason = 'user requested') {
this.manager.client.ws.send({ this.manager.client.ws.send({
@@ -107,19 +107,15 @@ class VoiceConnection extends EventEmitter {
} }
_shutdown(e) { _shutdown(e) {
if (!this.ready) { if (!this.ready) return;
return;
}
this.ready = false; this.ready = false;
this.websocket._shutdown(); this.websocket._shutdown();
this.player._shutdown(); this.player._shutdown();
if (this.udp) { if (this.udp) this.udp._shutdown();
this.udp._shutdown();
}
/** /**
* Emit once the voice connection has disconnected. * Emit once the voice connection has disconnected.
* @event VoiceConnection#disconnected * @event VoiceConnection#disconnected
* @param {Error} error the error, if any * @param {Error} error The encountered error, if any
*/ */
this.emit('disconnected', e); this.emit('disconnected', e);
} }
@@ -149,9 +145,7 @@ class VoiceConnection extends EventEmitter {
}); });
this.once('ready', () => { this.once('ready', () => {
setImmediate(() => { setImmediate(() => {
for (const item of this.queue) { for (const item of this.queue) this.emit(...item);
this.emit(...item);
}
this.queue = []; this.queue = [];
}); });
}); });
@@ -197,21 +191,18 @@ class VoiceConnection extends EventEmitter {
/** /**
* Emitted whenever a user starts/stops speaking * Emitted whenever a user starts/stops speaking
* @event VoiceConnection#speaking * @event VoiceConnection#speaking
* @param {User} user the user that has started/stopped speaking * @param {User} user The user that has started/stopped speaking
* @param {boolean} speaking whether or not the user is speaking * @param {boolean} speaking Whether or not the user is speaking
*/ */
if (this.ready) { if (this.ready) this.emit('speaking', user, data.speaking);
this.emit('speaking', user, data.speaking); else this.queue.push(['speaking', user, data.speaking]);
} else {
this.queue.push(['speaking', user, data.speaking]);
}
guild._memberSpeakUpdate(data.user_id, data.speaking); guild._memberSpeakUpdate(data.user_id, data.speaking);
}); });
} }
/** /**
* Play the given file in the voice connection * Play the given file in the voice connection
* @param {string} file the path to the file * @param {string} file The path to the file
* @returns {StreamDispatcher} * @returns {StreamDispatcher}
* @example * @example
* // play files natively * // play files natively
@@ -227,7 +218,7 @@ class VoiceConnection extends EventEmitter {
/** /**
* Plays and converts an audio stream in the voice connection * Plays and converts an audio stream in the voice connection
* @param {ReadableStream} stream the audio stream to play * @param {ReadableStream} stream The audio stream to play
* @returns {StreamDispatcher} * @returns {StreamDispatcher}
* @example * @example
* // play streams using ytdl-core * // play streams using ytdl-core
@@ -245,7 +236,7 @@ class VoiceConnection extends EventEmitter {
/** /**
* Plays a stream of 16-bit signed stereo PCM at 48KHz. * Plays a stream of 16-bit signed stereo PCM at 48KHz.
* @param {ReadableStream} stream the audio stream to play. * @param {ReadableStream} stream The audio stream to play.
* @returns {StreamDispatcher} * @returns {StreamDispatcher}
*/ */
playConvertedStream(stream) { playConvertedStream(stream) {

View File

@@ -4,7 +4,6 @@ const Constants = require('../../util/Constants');
const EventEmitter = require('events').EventEmitter; const EventEmitter = require('events').EventEmitter;
class VoiceConnectionUDPClient extends EventEmitter { class VoiceConnectionUDPClient extends EventEmitter {
constructor(voiceConnection, data) { constructor(voiceConnection, data) {
super(); super();
this.voiceConnection = voiceConnection; this.voiceConnection = voiceConnection;
@@ -38,9 +37,7 @@ class VoiceConnectionUDPClient extends EventEmitter {
try { try {
this.udpSocket.close(); this.udpSocket.close();
} catch (err) { } catch (err) {
if (err.message !== 'Not running') { if (err.message !== 'Not running') this.emit('error', err);
this.emit('error', err);
}
} }
this.udpSocket = null; this.udpSocket = null;
} }
@@ -55,10 +52,7 @@ class VoiceConnectionUDPClient extends EventEmitter {
this.udpSocket.once('message', message => { this.udpSocket.once('message', message => {
const packet = new Buffer(message); const packet = new Buffer(message);
this.localIP = ''; this.localIP = '';
for (let i = 4; i < packet.indexOf(0, i); i++) { for (let i = 4; i < packet.indexOf(0, i); i++) this.localIP += String.fromCharCode(packet[i]);
this.localIP += String.fromCharCode(packet[i]);
}
this.localPort = parseInt(packet.readUIntLE(packet.length - 2, 2).toString(10), 10); this.localPort = parseInt(packet.readUIntLE(packet.length - 2, 2).toString(10), 10);
this.voiceConnection.websocket.send({ this.voiceConnection.websocket.send({
@@ -77,7 +71,6 @@ class VoiceConnectionUDPClient extends EventEmitter {
this.udpSocket.on('error', (error, message) => { this.udpSocket.on('error', (error, message) => {
this.emit('error', { error, message }); this.emit('error', { error, message });
}); });
this.udpSocket.on('close', error => { this.udpSocket.on('close', error => {
this.emit('close', error); this.emit('close', error);
}); });
@@ -86,7 +79,6 @@ class VoiceConnectionUDPClient extends EventEmitter {
blankMessage.writeUIntBE(this.data.ssrc, 0, 4); blankMessage.writeUIntBE(this.data.ssrc, 0, 4);
this.send(blankMessage); this.send(blankMessage);
} }
} }
module.exports = VoiceConnectionUDPClient; module.exports = VoiceConnectionUDPClient;

View File

@@ -26,15 +26,11 @@ class VoiceConnectionWebSocket extends EventEmitter {
} }
send(data) { send(data) {
if (this.ws.readyState === WebSocket.OPEN) { if (this.ws.readyState === WebSocket.OPEN) this.ws.send(JSON.stringify(data));
this.ws.send(JSON.stringify(data));
}
} }
_shutdown() { _shutdown() {
if (this.ws) { if (this.ws) this.ws.close();
this.ws.close();
}
clearInterval(this.heartbeat); clearInterval(this.heartbeat);
} }
@@ -97,9 +93,7 @@ class VoiceConnectionWebSocket extends EventEmitter {
case Constants.VoiceOPCodes.SESSION_DESCRIPTION: case Constants.VoiceOPCodes.SESSION_DESCRIPTION:
this.encryptionMode = packet.d.mode; this.encryptionMode = packet.d.mode;
this.secretKey = new Uint8Array(new ArrayBuffer(packet.d.secret_key.length)); this.secretKey = new Uint8Array(new ArrayBuffer(packet.d.secret_key.length));
for (const index in packet.d.secret_key) { for (const index in packet.d.secret_key) this.secretKey[index] = packet.d.secret_key[index];
this.secretKey[index] = packet.d.secret_key[index];
}
this.emit('ready', this.secretKey); this.emit('ready', this.secretKey);
break; break;
case Constants.VoiceOPCodes.SPEAKING: case Constants.VoiceOPCodes.SPEAKING:

View File

@@ -34,10 +34,10 @@ class StreamDispatcher extends EventEmitter {
} }
/** /**
* Emitted when the dispatcher starts/stops speaking * Emitted when the dispatcher starts/stops speaking
* @event StreamDispatcher#speaking * @event StreamDispatcher#speaking
* @param {boolean} value whether or not the dispatcher is speaking * @param {boolean} value Whether or not the dispatcher is speaking
*/ */
_setSpeaking(value) { _setSpeaking(value) {
this.speaking = value; this.speaking = value;
this.emit('speaking', value); this.emit('speaking', value);
@@ -62,27 +62,18 @@ class StreamDispatcher extends EventEmitter {
packetBuffer.copy(nonce, 0, 0, 12); packetBuffer.copy(nonce, 0, 0, 12);
buffer = NaCl.secretbox(buffer, nonce, this.player.connection.data.secret); buffer = NaCl.secretbox(buffer, nonce, this.player.connection.data.secret);
for (let i = 0; i < buffer.length; i++) { for (let i = 0; i < buffer.length; i++) packetBuffer[i + 12] = buffer[i];
packetBuffer[i + 12] = buffer[i];
}
return packetBuffer; return packetBuffer;
} }
_applyVolume(buffer) { _applyVolume(buffer) {
if (this._volume === 1) { if (this._volume === 1) return buffer;
return buffer;
}
const out = new Buffer(buffer.length); const out = new Buffer(buffer.length);
for (let i = 0; i < buffer.length; i += 2) { for (let i = 0; i < buffer.length; i += 2) {
if (i >= buffer.length - 1) { if (i >= buffer.length - 1) break;
break;
}
const uint = Math.min(32767, Math.max(-32767, Math.floor(this._volume * buffer.readInt16LE(i)))); const uint = Math.min(32767, Math.max(-32767, Math.floor(this._volume * buffer.readInt16LE(i))));
out.writeInt16LE(uint, i); out.writeInt16LE(uint, i);
} }
@@ -95,20 +86,24 @@ class StreamDispatcher extends EventEmitter {
this._setSpeaking(false); this._setSpeaking(false);
return; return;
} }
const data = this.streamingData; const data = this.streamingData;
if (data.missed >= 5) { if (data.missed >= 5) {
this._triggerTerminalState('error', new Error('stream is not generating fast enough')); this._triggerTerminalState('error', new Error('stream is not generating fast enough'));
return; return;
} }
if (this.paused) { if (this.paused) {
data.timestamp = data.timestamp + 4294967295 ? data.timestamp + 960 : 0; data.timestamp = data.timestamp + 4294967295 ? data.timestamp + 960 : 0;
this.player.connection.manager.client.setTimeout(() => this._send(), data.length * 10); this.player.connection.manager.client.setTimeout(() => this._send(), data.length * 10);
return; return;
} }
const bufferLength = 1920 * data.channels;
this._setSpeaking(true);
let buffer = this.stream.read(bufferLength);
this._setSpeaking(true);
const bufferLength = 1920 * data.channels;
let buffer = this.stream.read(bufferLength);
if (!buffer) { if (!buffer) {
data.missed++; data.missed++;
this.player.connection.manager.client.setTimeout(() => this._send(), data.length * 10); this.player.connection.manager.client.setTimeout(() => this._send(), data.length * 10);
@@ -132,7 +127,6 @@ class StreamDispatcher extends EventEmitter {
this._sendBuffer(buffer, data.sequence, data.timestamp); this._sendBuffer(buffer, data.sequence, data.timestamp);
const nextTime = data.startTime + (data.count * data.length); const nextTime = data.startTime + (data.count * data.length);
this.player.connection.manager.client.setTimeout(() => this._send(), data.length + (nextTime - Date.now())); this.player.connection.manager.client.setTimeout(() => this._send(), data.length + (nextTime - Date.now()));
} catch (e) { } catch (e) {
this._triggerTerminalState('error', e); this._triggerTerminalState('error', e);
@@ -140,33 +134,31 @@ class StreamDispatcher extends EventEmitter {
} }
/** /**
* Emitted once the stream has ended. Attach a `once` listener to this. * Emitted once the stream has ended. Attach a `once` listener to this.
* @event StreamDispatcher#end * @event StreamDispatcher#end
*/ */
_triggerEnd() { _triggerEnd() {
this.emit('end'); this.emit('end');
} }
/** /**
* Emitted once the stream has encountered an error. Attach a `once` listener to this. Also emits `end`. * Emitted once the stream has encountered an error. Attach a `once` listener to this. Also emits `end`.
* @event StreamDispatcher#error * @event StreamDispatcher#error
* @param {Error} err the error encountered * @param {Error} err The encountered error
*/ */
_triggerError(err) { _triggerError(err) {
this.emit('end'); this.emit('end');
this.emit('error', err); this.emit('error', err);
} }
_triggerTerminalState(state, err) { _triggerTerminalState(state, err) {
if (this._triggered) { if (this._triggered) return;
return;
}
/** /**
* Emitted when the stream wants to give debug information. * Emitted when the stream wants to give debug information.
* @event StreamDispatcher#debug * @event StreamDispatcher#debug
* @param {string} information the debug information * @param {string} information The debug information
*/ */
this.emit('debug', `triggered terminal state ${state} - stream is now dead`); this.emit('debug', `triggered terminal state ${state} - stream is now dead`);
this._triggered = true; this._triggered = true;
this._setSpeaking(false); this._setSpeaking(false);
@@ -188,17 +180,20 @@ class StreamDispatcher extends EventEmitter {
this.emit('error', 'no stream'); this.emit('error', 'no stream');
return; return;
} }
this.stream.on('end', err => this._triggerTerminalState('end', err)); this.stream.on('end', err => this._triggerTerminalState('end', err));
this.stream.on('error', err => this._triggerTerminalState('error', err)); this.stream.on('error', err => this._triggerTerminalState('error', err));
const data = this.streamingData; const data = this.streamingData;
data.length = 20; data.length = 20;
data.missed = 0; data.missed = 0;
data.startTime = Date.now(); data.startTime = Date.now();
this.stream.once('readable', () => this._send()); this.stream.once('readable', () => this._send());
} }
_pause(value) { _setPaused(paused) {
if (value) { if (paused) {
this.paused = true; this.paused = true;
this._setSpeaking(false); this._setSpeaking(false);
} else { } else {
@@ -225,7 +220,7 @@ class StreamDispatcher extends EventEmitter {
/** /**
* Sets the volume relative to the input stream - i.e. 1 is normal, 0.5 is half, 2 is double. * Sets the volume relative to the input stream - i.e. 1 is normal, 0.5 is half, 2 is double.
* @param {number} volume the volume that you want to set * @param {number} volume The volume that you want to set
*/ */
setVolume(volume) { setVolume(volume) {
this._volume = volume; this._volume = volume;
@@ -233,7 +228,7 @@ class StreamDispatcher extends EventEmitter {
/** /**
* Set the volume in decibels * Set the volume in decibels
* @param {number} db the decibels * @param {number} db The decibels
*/ */
setVolumeDecibels(db) { setVolumeDecibels(db) {
this._volume = Math.pow(10, db / 20); this._volume = Math.pow(10, db / 20);
@@ -241,7 +236,7 @@ class StreamDispatcher extends EventEmitter {
/** /**
* Set the volume so that a perceived value of 0.5 is half the perceived volume etc. * Set the volume so that a perceived value of 0.5 is half the perceived volume etc.
* @param {number} value the value for the volume * @param {number} value The value for the volume
*/ */
setVolumeLogarithmic(value) { setVolumeLogarithmic(value) {
this._volume = Math.pow(value, 1.660964); this._volume = Math.pow(value, 1.660964);
@@ -251,14 +246,14 @@ class StreamDispatcher extends EventEmitter {
* Stops sending voice packets to the voice connection (stream may still progress however) * Stops sending voice packets to the voice connection (stream may still progress however)
*/ */
pause() { pause() {
this._pause(true); this._setPaused(true);
} }
/** /**
* Resumes sending voice packets to the voice connection (may be further on in the stream than when paused) * Resumes sending voice packets to the voice connection (may be further on in the stream than when paused)
*/ */
resume() { resume() {
this._pause(false); this._setPaused(false);
} }
} }

View File

@@ -6,7 +6,6 @@ class NodeOpusEngine extends OpusEngine {
constructor(player) { constructor(player) {
super(player); super(player);
try { try {
// eslint-disable-next-line import/no-unresolved
opus = require('node-opus'); opus = require('node-opus');
} catch (err) { } catch (err) {
throw err; throw err;

View File

@@ -3,10 +3,6 @@ const list = [
require('./OpusScriptEngine'), require('./OpusScriptEngine'),
]; ];
exports.add = encoder => {
list.push(encoder);
};
function fetch(Encoder) { function fetch(Encoder) {
try { try {
return new Encoder(); return new Encoder();
@@ -15,12 +11,14 @@ function fetch(Encoder) {
} }
} }
exports.add = encoder => {
list.push(encoder);
};
exports.fetch = () => { exports.fetch = () => {
for (const encoder of list) { for (const encoder of list) {
const success = fetch(encoder); const fetched = fetch(encoder);
if (success) { if (fetched) return fetched;
return success;
}
} }
throw new Error('could not find an opus engine'); throw new Error('could not find an opus engine');
}; };

View File

@@ -1,17 +1,16 @@
const OpusEngine = require('./BaseOpusEngine'); const OpusEngine = require('./BaseOpusEngine');
let Opusscript; let OpusScript;
class NodeOpusEngine extends OpusEngine { class NodeOpusEngine extends OpusEngine {
constructor(player) { constructor(player) {
super(player); super(player);
try { try {
// eslint-disable-next-line import/no-unresolved OpusScript = require('opusscript');
Opusscript = require('opusscript');
} catch (err) { } catch (err) {
throw err; throw err;
} }
this.encoder = new Opusscript(48000, 2); this.encoder = new OpusScript(48000, 2);
} }
encode(buffer) { encode(buffer) {

View File

@@ -1,7 +1,6 @@
const EventEmitter = require('events').EventEmitter; const EventEmitter = require('events').EventEmitter;
class ConverterEngine extends EventEmitter { class ConverterEngine extends EventEmitter {
constructor(player) { constructor(player) {
super(); super();
this.player = player; this.player = player;
@@ -10,7 +9,6 @@ class ConverterEngine extends EventEmitter {
createConvertStream() { createConvertStream() {
return; return;
} }
} }
module.exports = ConverterEngine; module.exports = ConverterEngine;

View File

@@ -1,15 +1,6 @@
const ConverterEngine = require('./ConverterEngine'); const ConverterEngine = require('./ConverterEngine');
const ChildProcess = require('child_process'); const ChildProcess = require('child_process');
function chooseCommand() {
for (const cmd of ['ffmpeg', 'avconv', './ffmpeg', './avconv']) {
if (!ChildProcess.spawnSync(cmd, ['-h']).error) {
return cmd;
}
}
return null;
}
class FfmpegConverterEngine extends ConverterEngine { class FfmpegConverterEngine extends ConverterEngine {
constructor(player) { constructor(player) {
super(player); super(player);
@@ -17,9 +8,7 @@ class FfmpegConverterEngine extends ConverterEngine {
} }
handleError(encoder, err) { handleError(encoder, err) {
if (encoder.destroy) { if (encoder.destroy) encoder.destroy();
encoder.destroy();
}
this.emit('error', err); this.emit('error', err);
} }
@@ -41,4 +30,11 @@ class FfmpegConverterEngine extends ConverterEngine {
} }
} }
function chooseCommand() {
for (const cmd of ['ffmpeg', 'avconv', './ffmpeg', './avconv']) {
if (!ChildProcess.spawnSync(cmd, ['-h']).error) return cmd;
}
return null;
}
module.exports = FfmpegConverterEngine; module.exports = FfmpegConverterEngine;

View File

@@ -5,7 +5,6 @@ const StreamDispatcher = require('../dispatcher/StreamDispatcher');
const EventEmitter = require('events').EventEmitter; const EventEmitter = require('events').EventEmitter;
class VoiceConnectionPlayer extends EventEmitter { class VoiceConnectionPlayer extends EventEmitter {
constructor(connection) { constructor(connection) {
super(); super();
this.connection = connection; this.connection = connection;
@@ -38,9 +37,7 @@ class VoiceConnectionPlayer extends EventEmitter {
_shutdown() { _shutdown() {
this.speaking = false; this.speaking = false;
for (const stream of this.processMap.keys()) { for (const stream of this.processMap.keys()) this.killStream(stream);
this.killStream(stream);
}
} }
killStream(stream) { killStream(stream) {
@@ -79,9 +76,7 @@ class VoiceConnectionPlayer extends EventEmitter {
} }
setSpeaking(value) { setSpeaking(value) {
if (this.speaking === value) { if (this.speaking === value) return;
return;
}
this.speaking = value; this.speaking = value;
this.connection.websocket.send({ this.connection.websocket.send({
op: Constants.VoiceOPCodes.SPEAKING, op: Constants.VoiceOPCodes.SPEAKING,
@@ -100,7 +95,6 @@ class VoiceConnectionPlayer extends EventEmitter {
this.dispatcher = dispatcher; this.dispatcher = dispatcher;
return dispatcher; return dispatcher;
} }
} }
module.exports = VoiceConnectionPlayer; module.exports = VoiceConnectionPlayer;

View File

@@ -2,7 +2,6 @@ const BasePlayer = require('./BasePlayer');
const fs = require('fs'); const fs = require('fs');
class DefaultPlayer extends BasePlayer { class DefaultPlayer extends BasePlayer {
playFile(file) { playFile(file) {
return this.playStream(fs.createReadStream(file)); return this.playStream(fs.createReadStream(file));
} }

View File

@@ -11,10 +11,8 @@ class VoiceReadable extends Readable {
return; return;
} }
$push(d) { _push(d) {
if (this.open) { if (this.open) this.push(d);
this.push(d);
}
} }
} }

View File

@@ -53,7 +53,7 @@ class VoiceReceiver extends EventEmitter {
/** /**
* Creates a readable stream for a user that provides opus data while the user is speaking. When the user * Creates a readable stream for a user that provides opus data while the user is speaking. When the user
* stops speaking, the stream is destroyed. * stops speaking, the stream is destroyed.
* @param {UserResolvable} user the user to create the stream for * @param {UserResolvable} user The user to create the stream for
* @returns {ReadableStream} * @returns {ReadableStream}
*/ */
createOpusStream(user) { createOpusStream(user) {
@@ -72,17 +72,13 @@ class VoiceReceiver extends EventEmitter {
/** /**
* Creates a readable stream for a user that provides PCM data while the user is speaking. When the user * Creates a readable stream for a user that provides PCM data while the user is speaking. When the user
* stops speaking, the stream is destroyed. The stream is 16-bit signed stereo PCM at 48KHz. * stops speaking, the stream is destroyed. The stream is 16-bit signed stereo PCM at 48KHz.
* @param {UserResolvable} user the user to create the stream for * @param {UserResolvable} user The user to create the stream for
* @returns {ReadableStream} * @returns {ReadableStream}
*/ */
createPCMStream(user) { createPCMStream(user) {
user = this.connection.manager.client.resolver.resolveUser(user); user = this.connection.manager.client.resolver.resolveUser(user);
if (!user) { if (!user) throw new Error('invalid user object supplied');
throw new Error('invalid user object supplied'); if (this.pcmStreams.get(user.id)) throw new Error('there is already an existing stream for that user!');
}
if (this.pcmStreams.get(user.id)) {
throw new Error('there is already an existing stream for that user!');
}
const stream = new Readable(); const stream = new Readable();
this.pcmStreams.set(user.id, stream); this.pcmStreams.set(user.id, stream);
return stream; return stream;
@@ -95,34 +91,30 @@ class VoiceReceiver extends EventEmitter {
/** /**
* Emitted whenever a voice packet cannot be decrypted * Emitted whenever a voice packet cannot be decrypted
* @event VoiceReceiver#warn * @event VoiceReceiver#warn
* @param {string} message the warning message * @param {string} message The warning message
*/ */
this.emit('warn', 'failed to decrypt voice packet'); this.emit('warn', 'failed to decrypt voice packet');
return; return;
} }
data = new Buffer(data); data = new Buffer(data);
/** if (this.opusStreams.get(user.id)) this.opusStreams.get(user.id)._push(data);
* Emitted whenever voice data is received from the voice connection. This is _always_ emitted (unlike PCM). /**
* @event VoiceReceiver#opus * Emitted whenever voice data is received from the voice connection. This is _always_ emitted (unlike PCM).
* @param {User} user the user that is sending the buffer (is speaking) * @event VoiceReceiver#opus
* @param {Buffer} buffer the opus buffer * @param {User} user The user that is sending the buffer (is speaking)
*/ * @param {Buffer} buffer The opus buffer
if (this.opusStreams.get(user.id)) { */
this.opusStreams.get(user.id).$push(data);
}
this.emit('opus', user, data); this.emit('opus', user, data);
if (this.listenerCount('pcm') > 0 || this.pcmStreams.size > 0) { if (this.listenerCount('pcm') > 0 || this.pcmStreams.size > 0) {
/** /**
* Emits decoded voice data when it's received. For performance reasons, the decoding will only * Emits decoded voice data when it's received. For performance reasons, the decoding will only
* happen if there is at least one `pcm` listener on this receiver. * happen if there is at least one `pcm` listener on this receiver.
* @event VoiceReceiver#pcm * @event VoiceReceiver#pcm
* @param {User} user the user that is sending the buffer (is speaking) * @param {User} user The user that is sending the buffer (is speaking)
* @param {Buffer} buffer the decoded buffer * @param {Buffer} buffer The decoded buffer
*/ */
const pcm = this.connection.player.opusEncoder.decode(data); const pcm = this.connection.player.opusEncoder.decode(data);
if (this.pcmStreams.get(user.id)) { if (this.pcmStreams.get(user.id)) this.pcmStreams.get(user.id)._push(pcm);
this.pcmStreams.get(user.id).$push(pcm);
}
this.emit('pcm', user, pcm); this.emit('pcm', user, pcm);
} }
} }

View File

@@ -8,14 +8,12 @@ const PacketManager = require('./packets/WebSocketPacketManager');
* @private * @private
*/ */
class WebSocketManager { class WebSocketManager {
constructor(client) { constructor(client) {
/** /**
* The Client that instantiated this WebSocketManager * The Client that instantiated this WebSocketManager
* @type {Client} * @type {Client}
*/ */
this.client = client; this.client = client;
this.ws = null;
/** /**
* A WebSocket Packet manager, it handles all the messages * A WebSocket Packet manager, it handles all the messages
* @type {PacketManager} * @type {PacketManager}
@@ -26,43 +24,40 @@ class WebSocketManager {
* @type {number} * @type {number}
*/ */
this.status = Constants.Status.IDLE; this.status = Constants.Status.IDLE;
/** /**
* The session ID of the connection, null if not yet available. * The session ID of the connection, null if not yet available.
* @type {?string} * @type {?string}
*/ */
this.sessionID = null; this.sessionID = null;
/** /**
* The packet count of the client, null if not yet available. * The packet count of the client, null if not yet available.
* @type {?number} * @type {?number}
*/ */
this.sequence = -1; this.sequence = -1;
/** /**
* The gateway address for this WebSocket connection, null if not yet available. * The gateway address for this WebSocket connection, null if not yet available.
* @type {?string} * @type {?string}
*/ */
this.gateway = null; this.gateway = null;
/** /**
* Whether READY was emitted normally (all packets received) or not * Whether READY was emitted normally (all packets received) or not
* @type {boolean} * @type {boolean}
*/ */
this.normalReady = false; this.normalReady = false;
}
/**
* Connects the client to a given gateway
* @param {string} gateway the gateway to connect to
*/
connect(gateway) {
this.normalReady = false;
this.status = Constants.Status.CONNECTING;
/** /**
* The WebSocket connection to the gateway * The WebSocket connection to the gateway
* @type {?WebSocket} * @type {?WebSocket}
*/ */
this.ws = null;
}
/**
* Connects the client to a given gateway
* @param {string} gateway The gateway to connect to
*/
connect(gateway) {
this.normalReady = false;
this.status = Constants.Status.CONNECTING;
this.ws = new WebSocket(gateway); this.ws = new WebSocket(gateway);
this.ws.onopen = () => this.eventOpen(); this.ws.onopen = () => this.eventOpen();
this.ws.onclose = (d) => this.eventClose(d); this.ws.onclose = (d) => this.eventClose(d);
@@ -113,15 +108,12 @@ class WebSocketManager {
* Run whenever the gateway connections opens up * Run whenever the gateway connections opens up
*/ */
eventOpen() { eventOpen() {
if (this.reconnecting) { if (this.reconnecting) this._sendResume();
this._sendResume(); else this._sendNewIdentify();
} else {
this._sendNewIdentify();
}
} }
/** /**
* Sends a gatway resume packet, in cases of unexpected disconnections. * Sends a gateway resume packet, in cases of unexpected disconnections.
*/ */
_sendResume() { _sendResume() {
const payload = { const payload = {
@@ -155,30 +147,23 @@ class WebSocketManager {
/** /**
* Run whenever the connection to the gateway is closed, it will try to reconnect the client. * Run whenever the connection to the gateway is closed, it will try to reconnect the client.
* @param {Object} event the event * @param {Object} event The received websocket data
*/ */
eventClose(event) { eventClose(event) {
if (event.code === 4004) { if (event.code === 4004) throw Constants.Errors.BAD_LOGIN;
throw Constants.Errors.BAD_LOGIN; if (!this.reconnecting && event.code !== 1000) this.tryReconnect();
}
if (!this.reconnecting && event.code !== 1000) {
this.tryReconnect();
}
} }
/** /**
* Run whenever a message is received from the WebSocket. Returns `true` if the message * Run whenever a message is received from the WebSocket. Returns `true` if the message
* was handled properly. * was handled properly.
* @param {Object} event the received websocket data * @param {Object} event The received websocket data
* @returns {boolean} * @returns {boolean}
*/ */
eventMessage(event) { eventMessage(event) {
let packet; let packet;
try { try {
if (event.binary) { if (event.binary) event.data = zlib.inflateSync(event.data).toString();
event.data = zlib.inflateSync(event.data).toString();
}
packet = JSON.parse(event.data); packet = JSON.parse(event.data);
} catch (e) { } catch (e) {
return this.eventError(Constants.Errors.BAD_WS_MESSAGE); return this.eventError(Constants.Errors.BAD_WS_MESSAGE);
@@ -186,22 +171,19 @@ class WebSocketManager {
this.client.emit('raw', packet); this.client.emit('raw', packet);
if (packet.op === 10) { if (packet.op === 10) this.client.manager.setupKeepAlive(packet.d.heartbeat_interval);
this.client.manager.setupKeepAlive(packet.d.heartbeat_interval);
}
return this.packetManager.handle(packet); return this.packetManager.handle(packet);
} }
/** /**
* Run whenever an error occurs with the WebSocket connection. Tries to reconnect * Run whenever an error occurs with the WebSocket connection. Tries to reconnect
* @param {Error} err the error that occurred * @param {Error} err The encountered error
*/ */
eventError(err) { eventError(err) {
/** /**
* Emitted whenever the Client encounters a serious connection error * Emitted whenever the Client encounters a serious connection error
* @event Client#error * @event Client#error
* @param {Error} error the encountered error * @param {Error} error The encountered error
*/ */
this.client.emit('error', err); this.client.emit('error', err);
this.tryReconnect(); this.tryReconnect();
@@ -210,7 +192,6 @@ class WebSocketManager {
_emitReady(normal = true) { _emitReady(normal = true) {
/** /**
* Emitted when the Client becomes ready to start working * Emitted when the Client becomes ready to start working
*
* @event Client#ready * @event Client#ready
*/ */
this.status = Constants.Status.READY; this.status = Constants.Status.READY;
@@ -252,10 +233,9 @@ class WebSocketManager {
this.ws.close(); this.ws.close();
this.packetManager.handleQueue(); this.packetManager.handleQueue();
/** /**
* Emitted when the Client tries to reconnect after being disconnected * Emitted when the Client tries to reconnect after being disconnected
* * @event Client#reconnecting
* @event Client#reconnecting */
*/
this.client.emit(Constants.Events.RECONNECTING); this.client.emit(Constants.Events.RECONNECTING);
this.connect(this.client.ws.gateway); this.connect(this.client.ws.gateway);
} }

View File

@@ -10,7 +10,6 @@ const BeforeReadyWhitelist = [
]; ];
class WebSocketPacketManager { class WebSocketPacketManager {
constructor(websocketManager) { constructor(websocketManager) {
this.ws = websocketManager; this.ws = websocketManager;
this.handlers = {}; this.handlers = {};
@@ -62,9 +61,7 @@ class WebSocketPacketManager {
} }
setSequence(s) { setSequence(s) {
if (s && s > this.ws.sequence) { if (s && s > this.ws.sequence) this.ws.sequence = s;
this.ws.sequence = s;
}
} }
handle(packet) { handle(packet) {
@@ -93,13 +90,9 @@ class WebSocketPacketManager {
} }
} }
if (this.handlers[packet.t]) { if (this.handlers[packet.t]) return this.handlers[packet.t].handle(packet);
return this.handlers[packet.t].handle(packet);
}
return false; return false;
} }
} }
module.exports = WebSocketPacketManager; module.exports = WebSocketPacketManager;

View File

@@ -1,5 +1,4 @@
class AbstractHandler { class AbstractHandler {
constructor(packetManager) { constructor(packetManager) {
this.packetManager = packetManager; this.packetManager = packetManager;
} }

View File

@@ -3,25 +3,18 @@ const AbstractHandler = require('./AbstractHandler');
const Constants = require('../../../../util/Constants'); const Constants = require('../../../../util/Constants');
class ChannelCreateHandler extends AbstractHandler { class ChannelCreateHandler extends AbstractHandler {
handle(packet) { handle(packet) {
const data = packet.d;
const client = this.packetManager.client; const client = this.packetManager.client;
const data = packet.d;
const response = client.actions.ChannelCreate.handle(data); const response = client.actions.ChannelCreate.handle(data);
if (response.channel) client.emit(Constants.Events.CHANNEL_CREATE, response.channel);
if (response.channel) {
client.emit(Constants.Events.CHANNEL_CREATE, response.channel);
}
} }
} }
/** /**
* Emitted whenever a Channel is created. * Emitted whenever a Channel is created.
* * @event Client#channelCreate
* @event Client#channelCreate * @param {Channel} channel The channel that was created
* @param {Channel} channel The channel that was created */
*/
module.exports = ChannelCreateHandler; module.exports = ChannelCreateHandler;

View File

@@ -3,25 +3,18 @@ const AbstractHandler = require('./AbstractHandler');
const Constants = require('../../../../util/Constants'); const Constants = require('../../../../util/Constants');
class ChannelDeleteHandler extends AbstractHandler { class ChannelDeleteHandler extends AbstractHandler {
handle(packet) { handle(packet) {
const data = packet.d;
const client = this.packetManager.client; const client = this.packetManager.client;
const data = packet.d;
const response = client.actions.ChannelDelete.handle(data); const response = client.actions.ChannelDelete.handle(data);
if (response.channel) client.emit(Constants.Events.CHANNEL_DELETE, response.channel);
if (response.channel) {
client.emit(Constants.Events.CHANNEL_DELETE, response.channel);
}
} }
} }
/** /**
* Emitted whenever a Channel is deleted. * Emitted whenever a Channel is deleted.
* * @event Client#channelDelete
* @event Client#channelDelete * @param {Channel} channel The channel that was deleted
* @param {Channel} channel The channel that was deleted */
*/
module.exports = ChannelDeleteHandler; module.exports = ChannelDeleteHandler;

View File

@@ -8,31 +8,24 @@ const Constants = require('../../../../util/Constants');
d: d:
{ last_pin_timestamp: '2016-08-28T17:37:13.171774+00:00', { last_pin_timestamp: '2016-08-28T17:37:13.171774+00:00',
channel_id: '314866471639044027' } } channel_id: '314866471639044027' } }
*/ */
class ChannelPinsUpdate extends AbstractHandler { class ChannelPinsUpdate extends AbstractHandler {
handle(packet) { handle(packet) {
const data = packet.d;
const client = this.packetManager.client; const client = this.packetManager.client;
const data = packet.d;
const channel = client.channels.get(data.channel_id); const channel = client.channels.get(data.channel_id);
const time = new Date(data.last_pin_timestamp); const time = new Date(data.last_pin_timestamp);
if (channel && time) client.emit(Constants.Events.CHANNEL_PINS_UPDATE, channel, time);
if (channel && time) {
client.emit(Constants.Events.CHANNEL_PINS_UPDATE, channel, time);
}
} }
} }
/** /**
* Emitted whenever the pins of a Channel are updated. Due to the nature of the WebSocket event, not much information * Emitted whenever the pins of a Channel are updated. Due to the nature of the WebSocket event, not much information
* can be provided easily here - you need to manually check the pins yourself. * can be provided easily here - you need to manually check the pins yourself.
* * @event Client#channelPinsUpdate
* @event Client#channelPinsUpdate * @param {Channel} channel The channel that the pins update occured in
* @param {Channel} channel The channel that the pins update occured in * @param {Date} time The time of the pins update
* @param {Date} time the time of the pins update */
*/
module.exports = ChannelPinsUpdate; module.exports = ChannelPinsUpdate;

View File

@@ -1,14 +1,11 @@
const AbstractHandler = require('./AbstractHandler'); const AbstractHandler = require('./AbstractHandler');
class ChannelUpdateHandler extends AbstractHandler { class ChannelUpdateHandler extends AbstractHandler {
handle(packet) { handle(packet) {
const data = packet.d;
const client = this.packetManager.client; const client = this.packetManager.client;
const data = packet.d;
client.actions.ChannelUpdate.handle(data); client.actions.ChannelUpdate.handle(data);
} }
} }
module.exports = ChannelUpdateHandler; module.exports = ChannelUpdateHandler;

View File

@@ -4,27 +4,20 @@ const AbstractHandler = require('./AbstractHandler');
const Constants = require('../../../../util/Constants'); const Constants = require('../../../../util/Constants');
class GuildBanAddHandler extends AbstractHandler { class GuildBanAddHandler extends AbstractHandler {
handle(packet) { handle(packet) {
const data = packet.d;
const client = this.packetManager.client; const client = this.packetManager.client;
const data = packet.d;
const guild = client.guilds.get(data.guild_id); const guild = client.guilds.get(data.guild_id);
const user = client.users.get(data.user.id); const user = client.users.get(data.user.id);
if (guild && user) client.emit(Constants.Events.GUILD_BAN_ADD, guild, user);
if (guild && user) {
client.emit(Constants.Events.GUILD_BAN_ADD, guild, user);
}
} }
} }
/** /**
* Emitted whenever a member is banned from a guild. * Emitted whenever a member is banned from a guild.
* * @event Client#guildBanAdd
* @event Client#guildBanAdd * @param {Guild} guild The guild that the ban occurred in
* @param {Guild} guild The guild that the ban occurred in * @param {User} user The user that was banned
* @param {User} user The user that was banned */
*/
module.exports = GuildBanAddHandler; module.exports = GuildBanAddHandler;

View File

@@ -3,22 +3,18 @@
const AbstractHandler = require('./AbstractHandler'); const AbstractHandler = require('./AbstractHandler');
class GuildBanRemoveHandler extends AbstractHandler { class GuildBanRemoveHandler extends AbstractHandler {
handle(packet) { handle(packet) {
const data = packet.d;
const client = this.packetManager.client; const client = this.packetManager.client;
const data = packet.d;
client.actions.GuildBanRemove.handle(data); client.actions.GuildBanRemove.handle(data);
} }
} }
/** /**
* Emitted whenever a member is unbanned from a guild. * Emitted whenever a member is unbanned from a guild.
* * @event Client#guildBanRemove
* @event Client#guildBanRemove * @param {Guild} guild The guild that the unban occurred in
* @param {Guild} guild The guild that the unban occurred in * @param {User} user The user that was unbanned
* @param {User} user The user that was unbanned */
*/
module.exports = GuildBanRemoveHandler; module.exports = GuildBanRemoveHandler;

View File

@@ -1,13 +1,11 @@
const AbstractHandler = require('./AbstractHandler'); const AbstractHandler = require('./AbstractHandler');
class GuildCreateHandler extends AbstractHandler { class GuildCreateHandler extends AbstractHandler {
handle(packet) { handle(packet) {
const data = packet.d;
const client = this.packetManager.client; const client = this.packetManager.client;
const data = packet.d;
const guild = client.guilds.get(data.id); const guild = client.guilds.get(data.id);
if (guild) { if (guild) {
if (!guild.available && !data.unavailable) { if (!guild.available && !data.unavailable) {
// a newly available guild // a newly available guild
@@ -19,7 +17,6 @@ class GuildCreateHandler extends AbstractHandler {
client.dataManager.newGuild(data); client.dataManager.newGuild(data);
} }
} }
} }
module.exports = GuildCreateHandler; module.exports = GuildCreateHandler;

View File

@@ -2,25 +2,18 @@ const AbstractHandler = require('./AbstractHandler');
const Constants = require('../../../../util/Constants'); const Constants = require('../../../../util/Constants');
class GuildDeleteHandler extends AbstractHandler { class GuildDeleteHandler extends AbstractHandler {
handle(packet) { handle(packet) {
const data = packet.d;
const client = this.packetManager.client; const client = this.packetManager.client;
const data = packet.d;
const response = client.actions.GuildDelete.handle(data); const response = client.actions.GuildDelete.handle(data);
if (response.guild) client.emit(Constants.Events.GUILD_DELETE, response.guild);
if (response.guild) {
client.emit(Constants.Events.GUILD_DELETE, response.guild);
}
} }
} }
/** /**
* Emitted whenever a Guild is deleted/left. * Emitted whenever a Guild is deleted/left.
* * @event Client#guildDelete
* @event Client#guildDelete * @param {Guild} guild The guild that was deleted
* @param {Guild} guild The guild that was deleted */
*/
module.exports = GuildDeleteHandler; module.exports = GuildDeleteHandler;

View File

@@ -3,19 +3,15 @@
const AbstractHandler = require('./AbstractHandler'); const AbstractHandler = require('./AbstractHandler');
class GuildMemberAddHandler extends AbstractHandler { class GuildMemberAddHandler extends AbstractHandler {
handle(packet) { handle(packet) {
const data = packet.d;
const client = this.packetManager.client; const client = this.packetManager.client;
const data = packet.d;
const guild = client.guilds.get(data.guild_id); const guild = client.guilds.get(data.guild_id);
if (guild) { if (guild) {
guild.memberCount++; guild.memberCount++;
guild._addMember(data); guild._addMember(data);
} }
} }
} }
module.exports = GuildMemberAddHandler; module.exports = GuildMemberAddHandler;

View File

@@ -3,14 +3,11 @@
const AbstractHandler = require('./AbstractHandler'); const AbstractHandler = require('./AbstractHandler');
class GuildMemberRemoveHandler extends AbstractHandler { class GuildMemberRemoveHandler extends AbstractHandler {
handle(packet) { handle(packet) {
const data = packet.d;
const client = this.packetManager.client; const client = this.packetManager.client;
const data = packet.d;
client.actions.GuildMemberRemove.handle(data); client.actions.GuildMemberRemove.handle(data);
} }
} }
module.exports = GuildMemberRemoveHandler; module.exports = GuildMemberRemoveHandler;

View File

@@ -3,21 +3,16 @@
const AbstractHandler = require('./AbstractHandler'); const AbstractHandler = require('./AbstractHandler');
class GuildMemberUpdateHandler extends AbstractHandler { class GuildMemberUpdateHandler extends AbstractHandler {
handle(packet) { handle(packet) {
const data = packet.d;
const client = this.packetManager.client; const client = this.packetManager.client;
const data = packet.d;
const guild = client.guilds.get(data.guild_id); const guild = client.guilds.get(data.guild_id);
if (guild) { if (guild) {
const member = guild.members.get(data.user.id); const member = guild.members.get(data.user.id);
if (member) { if (member) guild._updateMember(member, data);
guild._updateMember(member, data);
}
} }
} }
} }
module.exports = GuildMemberUpdateHandler; module.exports = GuildMemberUpdateHandler;

View File

@@ -4,31 +4,26 @@ const AbstractHandler = require('./AbstractHandler');
const Constants = require('../../../../util/Constants'); const Constants = require('../../../../util/Constants');
class GuildMembersChunkHandler extends AbstractHandler { class GuildMembersChunkHandler extends AbstractHandler {
handle(packet) { handle(packet) {
const data = packet.d;
const client = this.packetManager.client; const client = this.packetManager.client;
const data = packet.d;
const guild = client.guilds.get(data.guild_id); const guild = client.guilds.get(data.guild_id);
const members = []; const members = [];
if (guild) { if (guild) {
for (const member of data.members) { for (const member of data.members) members.push(guild._addMember(member, true));
members.push(guild._addMember(member, true));
}
} }
guild._checkChunks(); guild._checkChunks();
client.emit(Constants.Events.GUILD_MEMBERS_CHUNK, guild, members); client.emit(Constants.Events.GUILD_MEMBERS_CHUNK, guild, members);
} }
} }
/** /**
* Emitted whenever a chunk of Guild members is received * Emitted whenever a chunk of Guild members is received
* * @event Client#guildMembersChunk
* @event Client#guildMembersChunk * @param {Guild} guild The guild that the chunks relate to
* @param {Guild} guild The guild that the chunks relate to * @param {GuildMember[]} members The members in the chunk
* @param {Array<GuildMember>} members The members in the chunk */
*/
module.exports = GuildMembersChunkHandler; module.exports = GuildMembersChunkHandler;

View File

@@ -1,14 +1,11 @@
const AbstractHandler = require('./AbstractHandler'); const AbstractHandler = require('./AbstractHandler');
class GuildRoleCreateHandler extends AbstractHandler { class GuildRoleCreateHandler extends AbstractHandler {
handle(packet) { handle(packet) {
const data = packet.d;
const client = this.packetManager.client; const client = this.packetManager.client;
const data = packet.d;
client.actions.GuildRoleCreate.handle(data); client.actions.GuildRoleCreate.handle(data);
} }
} }
module.exports = GuildRoleCreateHandler; module.exports = GuildRoleCreateHandler;

View File

@@ -1,14 +1,11 @@
const AbstractHandler = require('./AbstractHandler'); const AbstractHandler = require('./AbstractHandler');
class GuildRoleDeleteHandler extends AbstractHandler { class GuildRoleDeleteHandler extends AbstractHandler {
handle(packet) { handle(packet) {
const data = packet.d;
const client = this.packetManager.client; const client = this.packetManager.client;
const data = packet.d;
client.actions.GuildRoleDelete.handle(data); client.actions.GuildRoleDelete.handle(data);
} }
} }
module.exports = GuildRoleDeleteHandler; module.exports = GuildRoleDeleteHandler;

View File

@@ -1,14 +1,11 @@
const AbstractHandler = require('./AbstractHandler'); const AbstractHandler = require('./AbstractHandler');
class GuildRoleUpdateHandler extends AbstractHandler { class GuildRoleUpdateHandler extends AbstractHandler {
handle(packet) { handle(packet) {
const data = packet.d;
const client = this.packetManager.client; const client = this.packetManager.client;
const data = packet.d;
client.actions.GuildRoleUpdate.handle(data); client.actions.GuildRoleUpdate.handle(data);
} }
} }
module.exports = GuildRoleUpdateHandler; module.exports = GuildRoleUpdateHandler;

View File

@@ -1,14 +1,11 @@
const AbstractHandler = require('./AbstractHandler'); const AbstractHandler = require('./AbstractHandler');
class GuildSyncHandler extends AbstractHandler { class GuildSyncHandler extends AbstractHandler {
handle(packet) { handle(packet) {
const data = packet.d;
const client = this.packetManager.client; const client = this.packetManager.client;
const data = packet.d;
client.actions.GuildSync.handle(data); client.actions.GuildSync.handle(data);
} }
} }
module.exports = GuildSyncHandler; module.exports = GuildSyncHandler;

View File

@@ -1,14 +1,11 @@
const AbstractHandler = require('./AbstractHandler'); const AbstractHandler = require('./AbstractHandler');
class GuildUpdateHandler extends AbstractHandler { class GuildUpdateHandler extends AbstractHandler {
handle(packet) { handle(packet) {
const data = packet.d;
const client = this.packetManager.client; const client = this.packetManager.client;
const data = packet.d;
client.actions.GuildUpdate.handle(data); client.actions.GuildUpdate.handle(data);
} }
} }
module.exports = GuildUpdateHandler; module.exports = GuildUpdateHandler;

View File

@@ -2,25 +2,18 @@ const AbstractHandler = require('./AbstractHandler');
const Constants = require('../../../../util/Constants'); const Constants = require('../../../../util/Constants');
class MessageCreateHandler extends AbstractHandler { class MessageCreateHandler extends AbstractHandler {
handle(packet) { handle(packet) {
const data = packet.d;
const client = this.packetManager.client; const client = this.packetManager.client;
const data = packet.d;
const response = client.actions.MessageCreate.handle(data); const response = client.actions.MessageCreate.handle(data);
if (response.message) client.emit(Constants.Events.MESSAGE_CREATE, response.message);
if (response.message) {
client.emit(Constants.Events.MESSAGE_CREATE, response.message);
}
} }
} }
/** /**
* Emitted whenever a message is created * Emitted whenever a message is created
* * @event Client#message
* @event Client#message * @param {Message} message The created message
* @param {Message} message The created message */
*/
module.exports = MessageCreateHandler; module.exports = MessageCreateHandler;

View File

@@ -2,25 +2,18 @@ const AbstractHandler = require('./AbstractHandler');
const Constants = require('../../../../util/Constants'); const Constants = require('../../../../util/Constants');
class MessageDeleteHandler extends AbstractHandler { class MessageDeleteHandler extends AbstractHandler {
handle(packet) { handle(packet) {
const data = packet.d;
const client = this.packetManager.client; const client = this.packetManager.client;
const data = packet.d;
const response = client.actions.MessageDelete.handle(data); const response = client.actions.MessageDelete.handle(data);
if (response.message) client.emit(Constants.Events.MESSAGE_DELETE, response.message);
if (response.message) {
client.emit(Constants.Events.MESSAGE_DELETE, response.message);
}
} }
} }
/** /**
* Emitted whenever a message is deleted * Emitted whenever a message is deleted
* * @event Client#messageDelete
* @event Client#messageDelete * @param {Message} message The deleted message
* @param {Message} message The deleted message */
*/
module.exports = MessageDeleteHandler; module.exports = MessageDeleteHandler;

View File

@@ -1,21 +1,17 @@
const AbstractHandler = require('./AbstractHandler'); const AbstractHandler = require('./AbstractHandler');
class MessageDeleteBulkHandler extends AbstractHandler { class MessageDeleteBulkHandler extends AbstractHandler {
handle(packet) { handle(packet) {
const data = packet.d;
const client = this.packetManager.client; const client = this.packetManager.client;
const data = packet.d;
client.actions.MessageDeleteBulk.handle(data); client.actions.MessageDeleteBulk.handle(data);
} }
} }
/** /**
* Emitted whenever a messages are deleted in bulk * Emitted whenever messages are deleted in bulk
* * @event Client#messageDeleteBulk
* @event Client#messageDeleteBulk * @param {Collection<string, Message>} messages The deleted messages, mapped by their ID
* @param {Collection<string, Message>} messages The deleted messages, mapped by their ID */
*/
module.exports = MessageDeleteBulkHandler; module.exports = MessageDeleteBulkHandler;

View File

@@ -1,14 +1,11 @@
const AbstractHandler = require('./AbstractHandler'); const AbstractHandler = require('./AbstractHandler');
class MessageUpdateHandler extends AbstractHandler { class MessageUpdateHandler extends AbstractHandler {
handle(packet) { handle(packet) {
const data = packet.d;
const client = this.packetManager.client; const client = this.packetManager.client;
const data = packet.d;
client.actions.MessageUpdate.handle(data); client.actions.MessageUpdate.handle(data);
} }
} }
module.exports = MessageUpdateHandler; module.exports = MessageUpdateHandler;

View File

@@ -3,21 +3,16 @@ const Constants = require('../../../../util/Constants');
const cloneObject = require('../../../../util/CloneObject'); const cloneObject = require('../../../../util/CloneObject');
class PresenceUpdateHandler extends AbstractHandler { class PresenceUpdateHandler extends AbstractHandler {
handle(packet) { handle(packet) {
const data = packet.d;
const client = this.packetManager.client; const client = this.packetManager.client;
const data = packet.d;
let user = client.users.get(data.user.id); let user = client.users.get(data.user.id);
const guild = client.guilds.get(data.guild_id); const guild = client.guilds.get(data.guild_id);
function makeUser(newUser) {
return client.dataManager.newUser(newUser);
}
// step 1 // step 1
if (!user) { if (!user) {
if (data.user.username) { if (data.user.username) {
user = makeUser(data.user); user = client.dataManager.newUser(data.user);
} else { } else {
return; return;
} }
@@ -39,7 +34,6 @@ class PresenceUpdateHandler extends AbstractHandler {
data.user.username = data.user.username || user.username; data.user.username = data.user.username || user.username;
data.user.id = data.user.id || user.id; data.user.id = data.user.id || user.id;
data.user.discriminator = data.user.discriminator || user.discriminator; data.user.discriminator = data.user.discriminator || user.discriminator;
// comment out avatar patching as it causes bugs (see #297) // comment out avatar patching as it causes bugs (see #297)
// data.user.avatar = data.user.avatar || user.avatar; // data.user.avatar = data.user.avatar || user.avatar;
data.user.status = data.status || user.status; data.user.status = data.status || user.status;
@@ -58,24 +52,20 @@ class PresenceUpdateHandler extends AbstractHandler {
client.emit(Constants.Events.PRESENCE_UPDATE, oldUser, user); client.emit(Constants.Events.PRESENCE_UPDATE, oldUser, user);
} }
} }
} }
/** /**
* Emitted whenever a user changes one of their details or starts/stop playing a game * Emitted whenever a user changes one of their details or starts/stop playing a game
* * @event Client#presenceUpdate
* @event Client#presenceUpdate * @param {User} oldUser The user before the presence update
* @param {User} oldUser the user before the presence update * @param {User} newUser The user after the presence update
* @param {User} newUser the user after the presence update */
*/
/** /**
* Emitted whenever a member becomes available in a large Guild * Emitted whenever a member becomes available in a large Guild
* * @event Client#guildMemberAvailable
* @event Client#guildMemberAvailable * @param {Guild} guild The guild that the member became available in
* @param {Guild} guild The guild that the member became available in * @param {GuildMember} member The member that became available
* @param {GuildMember} member the member that became available */
*/
module.exports = PresenceUpdateHandler; module.exports = PresenceUpdateHandler;

View File

@@ -4,40 +4,28 @@ const getStructure = name => require(`../../../../structures/${name}`);
const ClientUser = getStructure('ClientUser'); const ClientUser = getStructure('ClientUser');
class ReadyHandler extends AbstractHandler { class ReadyHandler extends AbstractHandler {
handle(packet) { handle(packet) {
const data = packet.d;
const client = this.packetManager.client; const client = this.packetManager.client;
const data = packet.d;
const clientUser = new ClientUser(client, data.user); const clientUser = new ClientUser(client, data.user);
client.user = clientUser; client.user = clientUser;
client.readyTime = Date.now(); client.readyTime = Date.now();
client.users.set(clientUser.id, clientUser); client.users.set(clientUser.id, clientUser);
for (const guild of data.guilds) {
client.dataManager.newGuild(guild);
}
for (const privateDM of data.private_channels) { for (const guild of data.guilds) client.dataManager.newGuild(guild);
client.dataManager.newChannel(privateDM); for (const privateDM of data.private_channels) client.dataManager.newChannel(privateDM);
}
if (!client.user.bot) {
client.setInterval(client.syncGuilds.bind(client), 30000);
}
if (!client.user.bot) client.setInterval(client.syncGuilds.bind(client), 30000);
client.once('ready', client.syncGuilds.bind(client)); client.once('ready', client.syncGuilds.bind(client));
client.setTimeout(() => { client.setTimeout(() => {
if (!client.ws.normalReady) { if (!client.ws.normalReady) client.ws._emitReady(false);
client.ws._emitReady(false);
}
}, 1200 * data.guilds.length); }, 1200 * data.guilds.length);
this.packetManager.ws.sessionID = data.session_id; this.packetManager.ws.sessionID = data.session_id;
this.packetManager.ws.checkIfReady(); this.packetManager.ws.checkIfReady();
} }
} }
module.exports = ReadyHandler; module.exports = ReadyHandler;

View File

@@ -1,28 +1,10 @@
const AbstractHandler = require('./AbstractHandler'); const AbstractHandler = require('./AbstractHandler');
const Constants = require('../../../../util/Constants'); const Constants = require('../../../../util/Constants');
class TypingData {
constructor(since, lastTimestamp, _timeout) {
this.since = since;
this.lastTimestamp = lastTimestamp;
this._timeout = _timeout;
}
resetTimeout(_timeout) {
clearTimeout(this._timeout);
this._timeout = _timeout;
}
get elapsedTime() {
return Date.now() - this.since;
}
}
class TypingStartHandler extends AbstractHandler { class TypingStartHandler extends AbstractHandler {
handle(packet) { handle(packet) {
const data = packet.d;
const client = this.packetManager.client; const client = this.packetManager.client;
const data = packet.d;
const channel = client.channels.get(data.channel_id); const channel = client.channels.get(data.channel_id);
const user = client.users.get(data.user_id); const user = client.users.get(data.user_id);
const timestamp = new Date(data.timestamp * 1000); const timestamp = new Date(data.timestamp * 1000);
@@ -46,23 +28,37 @@ class TypingStartHandler extends AbstractHandler {
} }
} }
} }
}
class TypingData {
constructor(since, lastTimestamp, _timeout) {
this.since = since;
this.lastTimestamp = lastTimestamp;
this._timeout = _timeout;
}
resetTimeout(_timeout) {
clearTimeout(this._timeout);
this._timeout = _timeout;
}
get elapsedTime() {
return Date.now() - this.since;
}
} }
/** /**
* Emitted whenever a user starts typing in a channel * Emitted whenever a user starts typing in a channel
* * @event Client#typingStart
* @event Client#typingStart * @param {Channel} channel The channel the user started typing in
* @param {Channel} channel the channel the user started typing in * @param {User} user The user that started typing
* @param {User} user the user that started typing */
*/
/** /**
* Emitted whenever a user stops typing in a channel * Emitted whenever a user stops typing in a channel
* * @event Client#typingStop
* @event Client#typingStop * @param {Channel} channel The channel the user stopped typing in
* @param {Channel} channel the channel the user stopped typing in * @param {User} user The user that stopped typing
* @param {User} user the user that stopped typing */
*/
module.exports = TypingStartHandler; module.exports = TypingStartHandler;

View File

@@ -1,14 +1,11 @@
const AbstractHandler = require('./AbstractHandler'); const AbstractHandler = require('./AbstractHandler');
class UserUpdateHandler extends AbstractHandler { class UserUpdateHandler extends AbstractHandler {
handle(packet) { handle(packet) {
const data = packet.d;
const client = this.packetManager.client; const client = this.packetManager.client;
const data = packet.d;
client.actions.UserUpdate.handle(data); client.actions.UserUpdate.handle(data);
} }
} }
module.exports = UserUpdateHandler; module.exports = UserUpdateHandler;

View File

@@ -9,16 +9,13 @@ const AbstractHandler = require('./AbstractHandler');
*/ */
class VoiceServerUpdate extends AbstractHandler { class VoiceServerUpdate extends AbstractHandler {
handle(packet) { handle(packet) {
const data = packet.d;
const client = this.packetManager.client; const client = this.packetManager.client;
const data = packet.d;
if (client.voice.pending.has(data.guild_id)) { if (client.voice.pending.has(data.guild_id)) {
client.voice._receivedVoiceServer(data.guild_id, data.token, data.endpoint); client.voice._receivedVoiceServer(data.guild_id, data.token, data.endpoint);
} }
} }
} }
module.exports = VoiceServerUpdate; module.exports = VoiceServerUpdate;

View File

@@ -4,12 +4,11 @@ const Constants = require('../../../../util/Constants');
const cloneObject = require('../../../../util/CloneObject'); const cloneObject = require('../../../../util/CloneObject');
class VoiceStateUpdateHandler extends AbstractHandler { class VoiceStateUpdateHandler extends AbstractHandler {
handle(packet) { handle(packet) {
const data = packet.d;
const client = this.packetManager.client; const client = this.packetManager.client;
const guild = client.guilds.get(data.guild_id); const data = packet.d;
const guild = client.guilds.get(data.guild_id);
if (guild) { if (guild) {
const member = guild.members.get(data.user_id); const member = guild.members.get(data.user_id);
if (member) { if (member) {
@@ -19,18 +18,14 @@ class VoiceStateUpdateHandler extends AbstractHandler {
} }
// if the member left the voice channel, unset their speaking property // if the member left the voice channel, unset their speaking property
if (!data.channel_id) { if (!data.channel_id) member.speaking = null;
member.speaking = null;
}
if (client.voice.pending.has(guild.id) && member.user.id === client.user.id && data.channel_id) { if (client.voice.pending.has(guild.id) && member.user.id === client.user.id && data.channel_id) {
client.voice._receivedVoiceStateUpdate(data.guild_id, data.session_id); client.voice._receivedVoiceStateUpdate(data.guild_id, data.session_id);
} }
const newChannel = client.channels.get(data.channel_id); const newChannel = client.channels.get(data.channel_id);
if (newChannel) { if (newChannel) newChannel.members.set(member.user.id, member);
newChannel.members.set(member.user.id, member);
}
member.serverMute = data.mute; member.serverMute = data.mute;
member.serverDeaf = data.deaf; member.serverDeaf = data.deaf;
@@ -42,15 +37,13 @@ class VoiceStateUpdateHandler extends AbstractHandler {
} }
} }
} }
} }
/** /**
* Emitted whenever a user changes voice state - e.g. joins/leaves a channel, mutes/unmutes. * Emitted whenever a user changes voice state - e.g. joins/leaves a channel, mutes/unmutes.
* * @event Client#voiceStateUpdate
* @event Client#voiceStateUpdate * @param {GuildMember} oldMember The member before the voice state update
* @param {GuildMember} oldMember the member before the voice state update * @param {GuildMember} newMember The member after the voice state update
* @param {GuildMember} newMember the member before the voice state update */
*/
module.exports = VoiceStateUpdateHandler; module.exports = VoiceStateUpdateHandler;

View File

@@ -10,9 +10,7 @@ class Channel {
this.client = client; this.client = client;
this.typingMap = {}; this.typingMap = {};
this.typingTimeouts = []; this.typingTimeouts = [];
if (guild) { if (guild) this.guild = guild;
this.guild = guild;
}
/** /**
* The type of the channel, either: * The type of the channel, either:
* * `dm` - a DM channel * * `dm` - a DM channel
@@ -22,9 +20,7 @@ class Channel {
* @type {string} * @type {string}
*/ */
this.type = null; this.type = null;
if (data) { if (data) this.setup(data);
this.setup(data);
}
} }
setup(data) { setup(data) {

View File

@@ -25,7 +25,7 @@ class ClientUser extends User {
* Set the username of the logged in Client. * Set the username of the logged in Client.
* <info>Changing usernames in Discord is heavily rate limited, with only 2 requests * <info>Changing usernames in Discord is heavily rate limited, with only 2 requests
* every hour. Use this sparingly!</info> * every hour. Use this sparingly!</info>
* @param {string} username the new username * @param {string} username The new username
* @returns {Promise<ClientUser>} * @returns {Promise<ClientUser>}
* @example * @example
* // set username * // set username
@@ -40,7 +40,7 @@ class ClientUser extends User {
/** /**
* If this user is a "self bot" or logged in using a normal user's details (which should be avoided), you can set the * If this user is a "self bot" or logged in using a normal user's details (which should be avoided), you can set the
* email here. * email here.
* @param {string} email the new email * @param {string} email The new email
* @returns {Promise<ClientUser>} * @returns {Promise<ClientUser>}
* @example * @example
* // set email * // set email
@@ -55,7 +55,7 @@ class ClientUser extends User {
/** /**
* If this user is a "self bot" or logged in using a normal user's details (which should be avoided), you can set the * If this user is a "self bot" or logged in using a normal user's details (which should be avoided), you can set the
* password here. * password here.
* @param {string} password the new password * @param {string} password The new password
* @returns {Promise<ClientUser>} * @returns {Promise<ClientUser>}
* @example * @example
* // set password * // set password
@@ -69,7 +69,7 @@ class ClientUser extends User {
/** /**
* Set the avatar of the logged in Client. * Set the avatar of the logged in Client.
* @param {Base64Resolvable} avatar the new avatar * @param {Base64Resolvable} avatar The new avatar
* @returns {Promise<ClientUser>} * @returns {Promise<ClientUser>}
* @example * @example
* // set avatar * // set avatar
@@ -83,9 +83,9 @@ class ClientUser extends User {
/** /**
* Set the status and playing game of the logged in client. * Set the status and playing game of the logged in client.
* @param {string} [status] the status, can be `online` or `idle`. * @param {string} [status] The status, can be `online` or `idle`
* @param {string|Object} [game] the game that is being played * @param {string|Object} [game] The game that is being played
* @returns {Promise<ClientUser, Error>} * @returns {Promise<ClientUser>}
* @example * @example
* // set status * // set status
* client.user.setStatus('status', 'game') * client.user.setStatus('status', 'game')

View File

@@ -1,6 +1,6 @@
const User = require('./User');
const Channel = require('./Channel'); const Channel = require('./Channel');
const TextBasedChannel = require('./interface/TextBasedChannel'); const TextBasedChannel = require('./interface/TextBasedChannel');
const User = require('./User');
const Collection = require('../util/Collection'); const Collection = require('../util/Collection');
/** /**

View File

@@ -1,5 +1,5 @@
const Collection = require('../util/Collection');
const Constants = require('../util/Constants'); const Constants = require('../util/Constants');
const Collection = require('../util/Collection');
/** /**
* Represents a Custom Emoji * Represents a Custom Emoji
@@ -44,16 +44,14 @@ class Emoji {
} }
/** /**
* A collection of roles this emoji is active for (empty if all). Mapped by role ID. * A collection of roles this emoji is active for (empty if all), mapped by role ID.
* @type {Collection<string, Role>} * @type {Collection<string, Role>}
* @readonly * @readonly
*/ */
get roles() { get roles() {
const roles = new Collection(); const roles = new Collection();
for (const role of this.roleIDS) { for (const role of this.roleIDS) {
if (this.guild.roles.get(role)) { if (this.guild.roles.get(role)) roles.set(role, this.guild.roles.get(role));
roles.set(role, this.guild.roles.get(role));
}
} }
return roles; return roles;
} }

View File

@@ -28,31 +28,18 @@ class EvaluatedPermissions {
for (const permissionName in Constants.PermissionFlags) { for (const permissionName in Constants.PermissionFlags) {
serializedPermissions[permissionName] = this.hasPermission(permissionName); serializedPermissions[permissionName] = this.hasPermission(permissionName);
} }
return serializedPermissions; return serializedPermissions;
} }
/** /**
* Checks whether a user has a certain permission, e.g. `READ_MESSAGES`. * Checks whether the user has a certain permission, e.g. `READ_MESSAGES`.
* @param {string} permission the permission to check for * @param {PermissionResolvable} permission The permission to check for
* @param {boolean} [explicit=false] whether the user should explicitly have the permission. * @param {boolean} [explicit=false] Whether to require the user to explicitly have the exact permission
* @returns {boolean} * @returns {boolean}
*/ */
hasPermission(permission, explicit = false) { hasPermission(permission, explicit = false) {
if (typeof permission === 'string') { permission = this.member.client.resolver.resolvePermission(permission);
permission = Constants.PermissionFlags[permission]; if (!explicit && (this.permissions & Constants.PermissionFlags.ADMINISTRATOR) > 0) return true;
}
if (!permission) {
throw Constants.Errors.NOT_A_PERMISSION;
}
if (!explicit) {
if ((this.permissions & Constants.PermissionFlags.ADMINISTRATOR) > 0) {
return true;
}
}
return (this.permissions & permission) > 0; return (this.permissions & permission) > 0;
} }
} }

View File

@@ -1,6 +1,7 @@
const Channel = require('./Channel'); const Channel = require('./Channel');
const TextBasedChannel = require('./interface/TextBasedChannel'); const TextBasedChannel = require('./interface/TextBasedChannel');
const Collection = require('../util/Collection'); const Collection = require('../util/Collection');
const arraysEqual = require('../util/ArraysEqual');
/* /*
{ type: 3, { type: 3,
@@ -24,49 +25,31 @@ const Collection = require('../util/Collection');
icon: null } icon: null }
*/ */
function arraysEqual(a, b) {
if (a === b) return true;
if (a.length !== b.length) return false;
for (const itemInd in a) {
const item = a[itemInd];
const ind = b.indexOf(item);
if (ind) {
b.splice(ind, 1);
}
}
return b.length === 0;
}
/** /**
* Represents a Group DM on Discord * Represents a Group DM on Discord
* @extends {Channel} * @extends {Channel}
* @implements {TextBasedChannel} * @implements {TextBasedChannel}
*/ */
class GroupDMChannel extends Channel { class GroupDMChannel extends Channel {
constructor(client, data) { constructor(client, data) {
super(client, data); super(client, data);
this.messages = new Collection(); this.messages = new Collection();
} }
equals(other) { equals(other) {
const base = other && const equal = other &&
this.id === other.id && this.id === other.id &&
this.name === other.name && this.name === other.name &&
this.icon === other.icon && this.icon === other.icon &&
this.owner.id === other.owner_id; this.owner.id === other.owner_id;
if (base) { if (equal) {
const thisIDs = this.recipients.array().map(r => r.id); const thisIDs = this.recipients.array().map(r => r.id);
const otherIDs = other.recipients.map(r => r.id); const otherIDs = other.recipients.map(r => r.id);
return arraysEqual(thisIDs, otherIDs); return arraysEqual(thisIDs, otherIDs);
} }
return base; return equal;
} }
setup(data) { setup(data) {
@@ -86,6 +69,7 @@ class GroupDMChannel extends Channel {
this.recipients.set(user.id, user); this.recipients.set(user.id, user);
} }
} }
/** /**
* The name of this Group DM, can be null if one isn't set. * The name of this Group DM, can be null if one isn't set.
* @type {string} * @type {string}

View File

@@ -1,25 +1,11 @@
const User = require('./User'); const User = require('./User');
const Role = require('./Role');
const Emoji = require('./Emoji');
const GuildMember = require('./GuildMember'); const GuildMember = require('./GuildMember');
const Constants = require('../util/Constants'); const Constants = require('../util/Constants');
const cloneObject = require('../util/CloneObject');
const Role = require('./Role');
const Collection = require('../util/Collection'); const Collection = require('../util/Collection');
const Emoji = require('./Emoji'); const cloneObject = require('../util/CloneObject');
const arraysEqual = require('../util/ArraysEqual');
function arraysEqual(a, b) {
if (a === b) return true;
if (a.length !== b.length) return false;
for (const itemInd in a) {
const item = a[itemInd];
const ind = b.indexOf(item);
if (ind) {
b.splice(ind, 1);
}
}
return b.length === 0;
}
/** /**
* Represents a Guild (or a Server) on Discord. * Represents a Guild (or a Server) on Discord.
@@ -50,9 +36,7 @@ class Guild {
*/ */
this.roles = new Collection(); this.roles = new Collection();
if (!data) { if (!data) return;
return;
}
if (data.unavailable) { if (data.unavailable) {
/** /**
@@ -100,12 +84,11 @@ class Guild {
this.channels.get(voiceState.channel_id).members.set(member.user.id, member); this.channels.get(voiceState.channel_id).members.set(member.user.id, member);
} }
/** /**
* Emitted whenever a user joins a guild. * Emitted whenever a user joins a guild.
* * @event Client#guildMemberAdd
* @event Client#guildMemberAdd * @param {Guild} guild The guild that the user has joined
* @param {Guild} guild the guild that the user has joined * @param {GuildMember} member The member that has joined
* @param {GuildMember} member the member that has joined */
*/
if (this.client.ws.status === Constants.Status.READY && !noEvent) { if (this.client.ws.status === Constants.Status.READY && !noEvent) {
this.client.emit(Constants.Events.GUILD_MEMBER_ADD, this, member); this.client.emit(Constants.Events.GUILD_MEMBER_ADD, this, member);
} }
@@ -117,25 +100,22 @@ class Guild {
_updateMember(member, data) { _updateMember(member, data) {
const oldMember = cloneObject(member); const oldMember = cloneObject(member);
if (data.roles) { if (data.roles) member._roles = data.roles;
member._roles = data.roles; else member.nickname = data.nick;
} else {
member.nickname = data.nick;
}
const notSame = member.nickname !== oldMember.nickname && !arraysEqual(member._roles, oldMember._roles); const notSame = member.nickname !== oldMember.nickname && !arraysEqual(member._roles, oldMember._roles);
if (this.client.ws.status === Constants.Status.READY && notSame) { if (this.client.ws.status === Constants.Status.READY && notSame) {
/** /**
* Emitted whenever a Guild Member changes - i.e. new role, removed role, nickname * Emitted whenever a Guild Member changes - i.e. new role, removed role, nickname
* * @event Client#guildMemberUpdate
* @event Client#guildMemberUpdate * @param {Guild} guild The guild that the update affects
* @param {Guild} guild the guild that the update affects * @param {GuildMember} oldMember The member before the update
* @param {GuildMember} oldMember the member before the update * @param {GuildMember} newMember The member after the update
* @param {GuildMember} newMember the member after the update */
*/
this.client.emit(Constants.Events.GUILD_MEMBER_UPDATE, this, oldMember, member); this.client.emit(Constants.Events.GUILD_MEMBER_UPDATE, this, oldMember, member);
} }
return { return {
old: oldMember, old: oldMember,
mem: member, mem: member,
@@ -163,7 +143,7 @@ class Guild {
/** /**
* Returns the GuildMember form of a User object, if the User is present in the guild. * Returns the GuildMember form of a User object, if the User is present in the guild.
* @param {UserResolvable} user the user that you want to obtain the GuildMember of. * @param {UserResolvable} user The user that you want to obtain the GuildMember of
* @returns {GuildMember|null} * @returns {GuildMember|null}
* @example * @example
* // get the guild member of a user * // get the guild member of a user
@@ -177,11 +157,11 @@ class Guild {
* Whether this Guild equals another Guild. It compares all properties, so for most operations * Whether this Guild equals another Guild. It compares all properties, so for most operations
* it is advisable to just compare `guild.id === guild2.id` as it is much faster and is often * it is advisable to just compare `guild.id === guild2.id` as it is much faster and is often
* what most users need. * what most users need.
* @param {Guild} guild the guild to compare * @param {Guild} guild The guild to compare
* @returns {boolean} * @returns {boolean}
*/ */
equals(guild) { equals(guild) {
let base = let equal =
guild && guild &&
this.id === guild.id && this.id === guild.id &&
this.available === !guild.unavailable && this.available === !guild.unavailable &&
@@ -196,17 +176,15 @@ class Guild {
this.verificationLevel === guild.verification_level && this.verificationLevel === guild.verification_level &&
this.embedEnabled === guild.embed_enabled; this.embedEnabled === guild.embed_enabled;
if (base) { if (equal) {
if (this.embedChannel) { if (this.embedChannel) {
if (this.embedChannel.id !== guild.embed_channel_id) { if (this.embedChannel.id !== guild.embed_channel_id) equal = false;
base = false;
}
} else if (guild.embed_channel_id) { } else if (guild.embed_channel_id) {
base = false; equal = false;
} }
} }
return base; return equal;
} }
_memberSpeakUpdate(user, speaking) { _memberSpeakUpdate(user, speaking) {
@@ -216,8 +194,8 @@ class Guild {
/** /**
* Emitted once a Guild Member starts/stops speaking * Emitted once a Guild Member starts/stops speaking
* @event Client#guildMemberSpeaking * @event Client#guildMemberSpeaking
* @param {GuildMember} member the member that started/stopped speaking * @param {GuildMember} member The member that started/stopped speaking
* @param {boolean} speaking whether or not the member is speaking * @param {boolean} speaking Whether or not the member is speaking
*/ */
this.client.emit(Constants.Events.GUILD_MEMBER_SPEAKING, member, speaking); this.client.emit(Constants.Events.GUILD_MEMBER_SPEAKING, member, speaking);
} }
@@ -225,7 +203,7 @@ class Guild {
/** /**
* Sets up the Guild * Sets up the Guild
* @param {*} data the raw data of the guild * @param {*} data The raw data of the guild
* @private * @private
*/ */
setup(data) { setup(data) {
@@ -264,12 +242,12 @@ class Guild {
this.icon = data.icon; this.icon = data.icon;
/** /**
* An array of guild features. * An array of guild features.
* @type {Array<Object>} * @type {Object[]}
*/ */
this.features = data.features; this.features = data.features;
/** /**
* An array of guild emojis. * An array of guild emojis.
* @type {Array<Object>} * @type {Object[]}
*/ */
this.emojis = new Collection(); this.emojis = new Collection();
for (const emoji of data.emojis) { for (const emoji of data.emojis) {
@@ -351,6 +329,7 @@ class Guild {
} }
} }
} }
/** /**
* The date at which the logged-in client joined the guild. * The date at which the logged-in client joined the guild.
* @type {Date} * @type {Date}
@@ -361,9 +340,9 @@ class Guild {
/** /**
* Creates a new Channel in the Guild. * Creates a new Channel in the Guild.
* @param {string} name the name of the new channel. * @param {string} name The name of the new channel
* @param {string} type the type of the new channel, either `text` or `voice`. * @param {string} type The type of the new channel, either `text` or `voice`
* @returns {Promise<TextChannel|VoiceChannel, Error>} * @returns {Promise<TextChannel|VoiceChannel>}
* @example * @example
* // create a new text channel * // create a new text channel
* guild.createChannel('new general', 'text') * guild.createChannel('new general', 'text')
@@ -376,7 +355,7 @@ class Guild {
/** /**
* Creates a new role in the guild, as of now this is just a blank role. * Creates a new role in the guild, as of now this is just a blank role.
* @returns {Promise<Role, Error>} * @returns {Promise<Role>}
* @example * @example
* // create a new role * // create a new role
* guild.createRole() * guild.createRole()
@@ -389,7 +368,7 @@ class Guild {
/** /**
* Causes the Client to leave the guild. * Causes the Client to leave the guild.
* @returns {Promise<Guild, Error>} * @returns {Promise<Guild>}
* @example * @example
* // leave a guild * // leave a guild
* guild.leave() * guild.leave()
@@ -402,7 +381,7 @@ class Guild {
/** /**
* Causes the Client to delete the guild. * Causes the Client to delete the guild.
* @returns {Promise<Guild, Error>} * @returns {Promise<Guild>}
* @example * @example
* // delete a guild * // delete a guild
* guild.delete() * guild.delete()
@@ -415,8 +394,8 @@ class Guild {
/** /**
* Updates the Guild with new information - e.g. a new name. * Updates the Guild with new information - e.g. a new name.
* @param {GuildEditData} data the data to update the guild with. * @param {GuildEditData} data The data to update the guild with
* @returns {Promise<Guild, Error>} * @returns {Promise<Guild>}
* @example * @example
* // set the guild name and region * // set the guild name and region
* guild.edit({ * guild.edit({
@@ -432,8 +411,8 @@ class Guild {
/** /**
* Edit the name of the Guild. * Edit the name of the Guild.
* @param {string} name the new name of the Guild. * @param {string} name The new name of the Guild
* @returns {Promise<Guild, Error>} * @returns {Promise<Guild>}
* @example * @example
* // edit the guild name * // edit the guild name
* guild.setName('Discord Guild') * guild.setName('Discord Guild')
@@ -446,8 +425,8 @@ class Guild {
/** /**
* Edit the region of the Guild. * Edit the region of the Guild.
* @param {Region} region the new region of the guild. * @param {Region} region The new region of the guild.
* @returns {Promise<Guild, Error>} * @returns {Promise<Guild>}
* @example * @example
* // edit the guild region * // edit the guild region
* guild.setRegion('london') * guild.setRegion('london')
@@ -460,8 +439,8 @@ class Guild {
/** /**
* Edit the verification level of the Guild. * Edit the verification level of the Guild.
* @param {VerificationLevel} verificationLevel the new verification level of the guild. * @param {VerificationLevel} verificationLevel The new verification level of the guild
* @returns {Promise<Guild, Error>} * @returns {Promise<Guild>}
* @example * @example
* // edit the guild verification level * // edit the guild verification level
* guild.setVerificationLevel(1) * guild.setVerificationLevel(1)
@@ -474,8 +453,8 @@ class Guild {
/** /**
* Edit the AFK channel of the Guild. * Edit the AFK channel of the Guild.
* @param {GuildChannelResolvable} afkChannel the new AFK channel. * @param {GuildChannelResolvable} afkChannel The new AFK channel
* @returns {Promise<Guild, Error>} * @returns {Promise<Guild>}
* @example * @example
* // edit the guild AFK channel * // edit the guild AFK channel
* guild.setAFKChannel(channel) * guild.setAFKChannel(channel)
@@ -488,8 +467,8 @@ class Guild {
/** /**
* Edit the AFK timeout of the Guild. * Edit the AFK timeout of the Guild.
* @param {number} afkTimeout the time in seconds that a user must be idle to be considered AFK. * @param {number} afkTimeout The time in seconds that a user must be idle to be considered AFK
* @returns {Promise<Guild, Error>} * @returns {Promise<Guild>}
* @example * @example
* // edit the guild AFK channel * // edit the guild AFK channel
* guild.setAFKTimeout(60) * guild.setAFKTimeout(60)
@@ -502,8 +481,8 @@ class Guild {
/** /**
* Set a new Guild Icon. * Set a new Guild Icon.
* @param {Base64Resolvable} icon the new icon of the guild. * @param {Base64Resolvable} icon The new icon of the guild
* @returns {Promise<Guild, Error>} * @returns {Promise<Guild>}
* @example * @example
* // edit the guild icon * // edit the guild icon
* guild.setIcon(fs.readFileSync('./icon.png')) * guild.setIcon(fs.readFileSync('./icon.png'))
@@ -516,8 +495,8 @@ class Guild {
/** /**
* Sets a new owner of the Guild. * Sets a new owner of the Guild.
* @param {GuildMemberResolvable} owner the new owner of the Guild. * @param {GuildMemberResolvable} owner The new owner of the Guild
* @returns {Promise<Guild, Error>} * @returns {Promise<Guild>}
* @example * @example
* // edit the guild owner * // edit the guild owner
* guild.setOwner(guilds.members[0]) * guild.setOwner(guilds.members[0])
@@ -530,8 +509,8 @@ class Guild {
/** /**
* Set a new Guild Splash Logo. * Set a new Guild Splash Logo.
* @param {Base64Resolvable} splash the new splash screen of the guild. * @param {Base64Resolvable} splash The new splash screen of the guild
* @returns {Promise<Guild, Error>} * @returns {Promise<Guild>}
* @example * @example
* // edit the guild splash * // edit the guild splash
* guild.setIcon(fs.readFileSync('./splash.png')) * guild.setIcon(fs.readFileSync('./splash.png'))
@@ -544,8 +523,8 @@ class Guild {
/** /**
* Unbans a member from the Guild * Unbans a member from the Guild
* @param {UserResolvable} member the member to unban * @param {UserResolvable} member The member to unban
* @returns {Promise<User, Error>} * @returns {Promise<User>}
* @example * @example
* // unban a member * // unban a member
* guild.unban('123123123123') * guild.unban('123123123123')
@@ -558,7 +537,7 @@ class Guild {
/** /**
* Fetch a Collection of banned users in this Guild. * Fetch a Collection of banned users in this Guild.
* @returns {Promise<Collection<string, User>, Error>} * @returns {Promise<Collection<string, User>>}
*/ */
fetchBans() { fetchBans() {
return this.client.rest.methods.getGuildBans(this); return this.client.rest.methods.getGuildBans(this);
@@ -566,7 +545,7 @@ class Guild {
/** /**
* Fetch a Collection of invites to this Guild. Resolves with a Collection mapping invites by their codes. * Fetch a Collection of invites to this Guild. Resolves with a Collection mapping invites by their codes.
* @returns {Promise<Collection<string, Invite>, Error>} * @returns {Promise<Collection<string, Invite>>}
*/ */
fetchInvites() { fetchInvites() {
return this.client.rest.methods.getGuildInvites(this); return this.client.rest.methods.getGuildInvites(this);
@@ -576,7 +555,7 @@ class Guild {
* Fetches all the members in the Guild, even if they are offline. If the Guild has less than 250 members, * Fetches all the members in the Guild, even if they are offline. If the Guild has less than 250 members,
* this should not be necessary. * this should not be necessary.
* @param {string} [query=''] An optional query to provide when fetching members * @param {string} [query=''] An optional query to provide when fetching members
* @returns {Promise<Guild, Error>} * @returns {Promise<Guild>}
*/ */
fetchMembers(query = '') { fetchMembers(query = '') {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
@@ -607,9 +586,7 @@ class Guild {
* @readonly * @readonly
*/ */
get iconURL() { get iconURL() {
if (!this.icon) { if (!this.icon) return null;
return null;
}
return Constants.Endpoints.guildIcon(this.id, this.icon); return Constants.Endpoints.guildIcon(this.id, this.icon);
} }

View File

@@ -1,24 +1,10 @@
const Channel = require('./Channel'); const Channel = require('./Channel');
const PermissionOverwrites = require('./PermissionOverwrites');
const Role = require('./Role'); const Role = require('./Role');
const PermissionOverwrites = require('./PermissionOverwrites');
const EvaluatedPermissions = require('./EvaluatedPermissions'); const EvaluatedPermissions = require('./EvaluatedPermissions');
const Constants = require('../util/Constants'); const Constants = require('../util/Constants');
const Collection = require('../util/Collection'); const Collection = require('../util/Collection');
const arraysEqual = require('../util/ArraysEqual');
function arraysEqual(a, b) {
if (a === b) return true;
if (a.length !== b.length) return false;
for (const itemInd in a) {
const item = a[itemInd];
const ind = b.indexOf(item);
if (ind) {
b.splice(ind, 1);
}
}
return b.length === 0;
}
/** /**
* Represents a Guild Channel (i.e. Text Channels and Voice Channels) * Represents a Guild Channel (i.e. Text Channels and Voice Channels)
@@ -62,67 +48,57 @@ class GuildChannel extends Channel {
/** /**
* Checks if this channel has the same type, topic, position, name, overwrites and ID as another channel. * Checks if this channel has the same type, topic, position, name, overwrites and ID as another channel.
* In most cases, a simple `channel.id === channel2.id` will do, and is much faster too. * In most cases, a simple `channel.id === channel2.id` will do, and is much faster too.
* @param {GuildChannel} channel the channel to compare this channel to * @param {GuildChannel} channel The channel to compare this channel to
* @returns {boolean} * @returns {boolean}
*/ */
equals(channel) { equals(channel) {
let base = channel && let equal = channel &&
this.type === channel.type && this.type === channel.type &&
this.topic === channel.topic && this.topic === channel.topic &&
this.position === channel.position && this.position === channel.position &&
this.name === channel.name && this.name === channel.name &&
this.id === channel.id; this.id === channel.id;
if (base) { if (equal) {
if (channel.permission_overwrites) { if (channel.permission_overwrites) {
const thisIDSet = Array.from(this.permissionOverwrites.keys()); const thisIDSet = Array.from(this.permissionOverwrites.keys());
const otherIDSet = channel.permission_overwrites.map(overwrite => overwrite.id); const otherIDSet = channel.permission_overwrites.map(overwrite => overwrite.id);
if (arraysEqual(thisIDSet, otherIDSet)) { equal = arraysEqual(thisIDSet, otherIDSet);
base = true;
} else {
base = false;
}
} else { } else {
base = false; equal = false;
} }
} }
return base; return equal;
} }
/** /**
* Gets the overall set of permissions for a user in this channel, taking into account roles and permission * Gets the overall set of permissions for a user in this channel, taking into account roles and permission
* overwrites. * overwrites.
* @param {GuildMemberResolvable} member the user that you want to obtain the overall permissions for * @param {GuildMemberResolvable} member The user that you want to obtain the overall permissions for
* @returns {?EvaluatedPermissions} * @returns {?EvaluatedPermissions}
*/ */
permissionsFor(member) { permissionsFor(member) {
member = this.client.resolver.resolveGuildMember(this.guild, member); member = this.client.resolver.resolveGuildMember(this.guild, member);
if (member) { if (member) {
if (this.guild.owner.id === member.id) { if (this.guild.owner.id === member.id) return new EvaluatedPermissions(member, Constants.ALL_PERMISSIONS);
return new EvaluatedPermissions(member, Constants.ALL_PERMISSIONS);
}
const roles = member.roles; const roles = member.roles;
let permissions = 0; let permissions = 0;
const overwrites = this.overwritesFor(member, true); const overwrites = this.overwritesFor(member, true);
for (const role of roles.values()) { for (const role of roles.values()) permissions |= role.permissions;
permissions |= role.permissions;
}
for (const overwrite of overwrites.role.concat(overwrites.member)) { for (const overwrite of overwrites.role.concat(overwrites.member)) {
permissions &= ~overwrite.denyData; permissions &= ~overwrite.denyData;
permissions |= overwrite.allowData; permissions |= overwrite.allowData;
} }
const admin = Boolean(permissions & (Constants.PermissionFlags.ADMINISTRATOR)); const admin = Boolean(permissions & (Constants.PermissionFlags.ADMINISTRATOR));
if (admin) { if (admin) permissions = Constants.ALL_PERMISSIONS;
permissions = Constants.ALL_PERMISSIONS;
}
return new EvaluatedPermissions(member, permissions); return new EvaluatedPermissions(member, permissions);
} }
return null; return null;
} }
@@ -165,9 +141,9 @@ class GuildChannel extends Channel {
/** /**
* Overwrites the permissions for a user or role in this channel. * Overwrites the permissions for a user or role in this channel.
* @param {Role|UserResolvable} userOrRole the user or role to update * @param {Role|UserResolvable} userOrRole The user or role to update
* @param {PermissionOverwriteOptions} options the configuration for the update * @param {PermissionOverwriteOptions} options The configuration for the update
* @returns {Promise<null, Error>} * @returns {Promise}
* @example * @example
* // overwrite permissions for a message author * // overwrite permissions for a message author
* message.channel.overwritePermissions(message.author, { * message.channel.overwritePermissions(message.author, {
@@ -187,9 +163,7 @@ class GuildChannel extends Channel {
} else { } else {
userOrRole = this.client.resolver.resolveUser(userOrRole); userOrRole = this.client.resolver.resolveUser(userOrRole);
payload.type = 'member'; payload.type = 'member';
if (!userOrRole) { if (!userOrRole) return Promise.reject(new TypeError('supplied parameter was neither a user or a role'));
return Promise.reject('supplied parameter was neither a user or a role');
}
} }
payload.id = userOrRole.id; payload.id = userOrRole.id;
@@ -220,7 +194,7 @@ class GuildChannel extends Channel {
/** /**
* Set a new name for the Guild Channel * Set a new name for the Guild Channel
* @param {string} name the new name for the guild channel * @param {string} name The new name for the guild channel
* @returns {Promise<GuildChannel>} * @returns {Promise<GuildChannel>}
* @example * @example
* // set a new channel name * // set a new channel name
@@ -234,7 +208,7 @@ class GuildChannel extends Channel {
/** /**
* Set a new position for the Guild Channel * Set a new position for the Guild Channel
* @param {number} position the new position for the guild channel * @param {number} position The new position for the guild channel
* @returns {Promise<GuildChannel>} * @returns {Promise<GuildChannel>}
* @example * @example
* // set a new channel position * // set a new channel position
@@ -248,7 +222,7 @@ class GuildChannel extends Channel {
/** /**
* Set a new topic for the Guild Channel * Set a new topic for the Guild Channel
* @param {string} topic the new topic for the guild channel * @param {string} topic The new topic for the guild channel
* @returns {Promise<GuildChannel>} * @returns {Promise<GuildChannel>}
* @example * @example
* // set a new channel topic * // set a new channel topic
@@ -288,8 +262,8 @@ class GuildChannel extends Channel {
/** /**
* Create an invite to this Guild Channel * Create an invite to this Guild Channel
* @param {InviteOptions} [options={}] the options to provide when creating the invite * @param {InviteOptions} [options={}] The options for the invite
* @returns {Promise<Invite, Error>} * @returns {Promise<Invite>}
*/ */
createInvite(options = {}) { createInvite(options = {}) {
return this.client.rest.methods.createChannelInvite(this, options); return this.client.rest.methods.createChannelInvite(this, options);

View File

@@ -23,9 +23,7 @@ class GuildMember {
*/ */
this.user = {}; this.user = {};
this._roles = []; this._roles = [];
if (data) { if (data) this.setup(data);
this.setup(data);
}
} }
setup(data) { setup(data) {
@@ -91,15 +89,11 @@ class GuildMember {
const list = new Collection(); const list = new Collection();
const everyoneRole = this.guild.roles.get(this.guild.id); const everyoneRole = this.guild.roles.get(this.guild.id);
if (everyoneRole) { if (everyoneRole) list.set(everyoneRole.id, everyoneRole);
list.set(everyoneRole.id, everyoneRole);
}
for (const roleID of this._roles) { for (const roleID of this._roles) {
const role = this.guild.roles.get(roleID); const role = this.guild.roles.get(roleID);
if (role) { if (role) list.set(role.id, role);
list.set(role.id, role);
}
} }
return list; return list;
@@ -143,8 +137,8 @@ class GuildMember {
/** /**
* Mute/unmute a user * Mute/unmute a user
* @param {boolean} mute whether or not the member should be muted * @param {boolean} mute Whether or not the member should be muted
* @returns {Promise<GuildMember, Error>} * @returns {Promise<GuildMember>}
*/ */
setMute(mute) { setMute(mute) {
return this.edit({ mute }); return this.edit({ mute });
@@ -152,8 +146,8 @@ class GuildMember {
/** /**
* Deafen/undeafen a user * Deafen/undeafen a user
* @param {boolean} deaf whether or not the member should be deafened * @param {boolean} deaf Whether or not the member should be deafened
* @returns {Promise<GuildMember, Error>} * @returns {Promise<GuildMember>}
*/ */
setDeaf(deaf) { setDeaf(deaf) {
return this.edit({ deaf }); return this.edit({ deaf });
@@ -161,8 +155,8 @@ class GuildMember {
/** /**
* Moves the Guild Member to the given channel. * Moves the Guild Member to the given channel.
* @param {ChannelResolvable} channel the channel to move the member to * @param {ChannelResolvable} channel The channel to move the member to
* @returns {Promise<GuildMember, Error>} * @returns {Promise<GuildMember>}
*/ */
setVoiceChannel(channel) { setVoiceChannel(channel) {
return this.edit({ channel }); return this.edit({ channel });
@@ -170,8 +164,8 @@ class GuildMember {
/** /**
* Sets the Roles applied to the member. * Sets the Roles applied to the member.
* @param {Collection<string, Role>|Array<Role>} roles the roles to apply * @param {Collection<string, Role>|Role[]} roles The roles to apply
* @returns {Promise<GuildMember, Error>} * @returns {Promise<GuildMember>}
*/ */
setRoles(roles) { setRoles(roles) {
return this.edit({ roles }); return this.edit({ roles });
@@ -179,8 +173,8 @@ class GuildMember {
/** /**
* Set the nickname for the Guild Member * Set the nickname for the Guild Member
* @param {string} nick the nickname for the Guild Member * @param {string} nick The nickname for the Guild Member
* @returns {Promise<GuildMember, Error>} * @returns {Promise<GuildMember>}
*/ */
setNickname(nick) { setNickname(nick) {
return this.edit({ nick }); return this.edit({ nick });
@@ -188,15 +182,15 @@ class GuildMember {
/** /**
* Edit a Guild Member * Edit a Guild Member
* @param {GuildmemberEditData} data the data to edit the member with * @param {GuildmemberEditData} data The data to edit the member with
* @returns {Promise<GuildMember, Error>} * @returns {Promise<GuildMember>}
*/ */
edit(data) { edit(data) {
return this.client.rest.methods.updateGuildMember(this, data); return this.client.rest.methods.updateGuildMember(this, data);
} }
/** /**
* Deletes any DM's with this Guild Member * Deletes any DMs with this Guild Member
* @returns {Promise<DMChannel>} * @returns {Promise<DMChannel>}
*/ */
deleteDM() { deleteDM() {
@@ -215,7 +209,7 @@ class GuildMember {
* Ban this Guild Member * Ban this Guild Member
* @param {number} [deleteDays=0] The amount of days worth of messages from this member that should * @param {number} [deleteDays=0] The amount of days worth of messages from this member that should
* also be deleted. Between `0` and `7`. * also be deleted. Between `0` and `7`.
* @returns {Promise<GuildMember, Error>} * @returns {Promise<GuildMember>}
* @example * @example
* // ban a guild member * // ban a guild member
* guildMember.ban(7); * guildMember.ban(7);

View File

@@ -98,7 +98,7 @@ class Invite {
/** /**
* Deletes this invite * Deletes this invite
* @returns {Promise<Invite, Error>} * @returns {Promise<Invite>}
*/ */
delete() { delete() {
return this.client.rest.methods.deleteInvite(this); return this.client.rest.methods.deleteInvite(this);

View File

@@ -1,6 +1,7 @@
const Collection = require('../util/Collection');
const Attachment = require('./MessageAttachment'); const Attachment = require('./MessageAttachment');
const Embed = require('./MessageEmbed'); const Embed = require('./MessageEmbed');
const Collection = require('../util/Collection');
/** /**
* Represents a Message on Discord * Represents a Message on Discord
*/ */
@@ -26,9 +27,7 @@ class Message {
* @type {Client} * @type {Client}
*/ */
this.client = client; this.client = client;
if (data) { if (data) this.setup(data);
this.setup(data);
}
} }
setup(data) { setup(data) {
@@ -69,7 +68,7 @@ class Message {
this.nonce = data.nonce; this.nonce = data.nonce;
/** /**
* A list of embeds in the message - e.g. YouTube Player * A list of embeds in the message - e.g. YouTube Player
* @type {Array<Embed>} * @type {Embed[]}
*/ */
this.embeds = data.embeds.map(e => new Embed(this, e)); this.embeds = data.embeds.map(e => new Embed(this, e));
/** /**
@@ -77,17 +76,15 @@ class Message {
* @type {Collection<string, MessageAttachment>} * @type {Collection<string, MessageAttachment>}
*/ */
this.attachments = new Collection(); this.attachments = new Collection();
for (const attachment of data.attachments) { for (const attachment of data.attachments) this.attachments.set(attachment.id, new Attachment(this, attachment));
this.attachments.set(attachment.id, new Attachment(this, attachment));
}
/** /**
* An object containing a further users, roles or channels collections * An object containing a further users, roles or channels collections
* @type {Object} * @type {Object}
* @property {Collection<string, User>} mentions.users Mentioned users, maps their ID to the user object. * @property {Collection<string, User>} mentions.users Mentioned users, maps their ID to the user object.
* @property {Collection<string, Role>} mentions.roles Mentioned roles, maps their ID to the role object. * @property {Collection<string, Role>} mentions.roles Mentioned roles, maps their ID to the role object.
* @property {Collection<string, GuildChannel>} * @property {Collection<string, GuildChannel>} mentions.channels Mentioned channels,
* mentions.channels Mentioned channels, maps their ID to the channel object. * maps their ID to the channel object.
* @property {boolean} mentions.everyone whether or not @everyone was mentioned. * @property {boolean} mentions.everyone Whether or not @everyone was mentioned.
*/ */
this.mentions = { this.mentions = {
users: new Collection(), users: new Collection(),
@@ -114,9 +111,7 @@ class Message {
if (data.mention_roles) { if (data.mention_roles) {
for (const mention of data.mention_roles) { for (const mention of data.mention_roles) {
const role = this.channel.guild.roles.get(mention); const role = this.channel.guild.roles.get(mention);
if (role) { if (role) this.mentions.roles.set(role.id, role);
this.mentions.roles.set(role.id, role);
}
} }
} }
@@ -124,9 +119,7 @@ class Message {
const channMentionsRaw = data.content.match(/<#([0-9]{14,20})>/g) || []; const channMentionsRaw = data.content.match(/<#([0-9]{14,20})>/g) || [];
for (const raw of channMentionsRaw) { for (const raw of channMentionsRaw) {
const chan = this.channel.guild.channels.get(raw.match(/([0-9]{14,20})/g)[0]); const chan = this.channel.guild.channels.get(raw.match(/([0-9]{14,20})/g)[0]);
if (chan) { if (chan) this.mentions.channels.set(chan.id, chan);
this.mentions.channels.set(chan.id, chan);
}
} }
} }
@@ -135,9 +128,7 @@ class Message {
* @type {boolean} * @type {boolean}
*/ */
this.system = false; this.system = false;
if (data.type === 6) { if (data.type === 6) this.system = true;
this.system = true;
}
} }
/** /**
* When the message was sent * When the message was sent
@@ -158,31 +149,17 @@ class Message {
patch(data) { // eslint-disable-line complexity patch(data) { // eslint-disable-line complexity
if (data.author) { if (data.author) {
this.author = this.client.users.get(data.author.id); this.author = this.client.users.get(data.author.id);
if (this.guild) { if (this.guild) this.member = this.guild.member(this.author);
this.member = this.guild.member(this.author);
}
}
if (data.content) {
this.content = data.content;
}
if (data.timestamp) {
this._timestamp = new Date(data.timestamp).getTime();
} }
if (data.content) this.content = data.content;
if (data.timestamp) this._timestamp = new Date(data.timestamp).getTime();
if (data.edited_timestamp) { if (data.edited_timestamp) {
this._editedTimestamp = data.edited_timestamp ? new Date(data.edited_timestamp).getTime() : null; this._editedTimestamp = data.edited_timestamp ? new Date(data.edited_timestamp).getTime() : null;
} }
if ('tts' in data) { if ('tts' in data) this.tts = data.tts;
this.tts = data.tts; if ('mention_everyone' in data) this.mentions.everyone = data.mention_everyone;
} if (data.nonce) this.nonce = data.nonce;
if ('mention_everyone' in data) { if (data.embeds) this.embeds = data.embeds.map(e => new Embed(this, e));
this.mentions.everyone = data.mention_everyone;
}
if (data.nonce) {
this.nonce = data.nonce;
}
if (data.embeds) {
this.embeds = data.embeds.map(e => new Embed(this, e));
}
if (data.type > -1) { if (data.type > -1) {
this.system = false; this.system = false;
if (data.type === 6) { if (data.type === 6) {
@@ -214,9 +191,7 @@ class Message {
} }
} }
} }
if (data.id) { if (data.id) this.id = data.id;
this.id = data.id;
}
if (this.channel.guild && data.content) { if (this.channel.guild && data.content) {
const channMentionsRaw = data.content.match(/<#([0-9]{14,20})>/g) || []; const channMentionsRaw = data.content.match(/<#([0-9]{14,20})>/g) || [];
for (const raw of channMentionsRaw) { for (const raw of channMentionsRaw) {
@@ -237,14 +212,11 @@ class Message {
* @returns {boolean} * @returns {boolean}
*/ */
equals(message, rawData) { equals(message, rawData) {
if (!message) return false;
const embedUpdate = !message.author && !message.attachments; const embedUpdate = !message.author && !message.attachments;
if (embedUpdate) return this.id === message.id && this.embeds.length === message.embeds.length;
if (embedUpdate) { let equal = this.id === message.id &&
const base = this.id === message.id &&
this.embeds.length === message.embeds.length;
return base;
}
let base = this.id === message.id &&
this.author.id === message.author.id && this.author.id === message.author.id &&
this.content === message.content && this.content === message.content &&
this.tts === message.tts && this.tts === message.tts &&
@@ -252,19 +224,19 @@ class Message {
this.embeds.length === message.embeds.length && this.embeds.length === message.embeds.length &&
this.attachments.length === message.attachments.length; this.attachments.length === message.attachments.length;
if (base && rawData) { if (equal && rawData) {
base = this.mentions.everyone === message.mentions.everyone && equal = this.mentions.everyone === message.mentions.everyone &&
this._timestamp === new Date(rawData.timestamp).getTime() && this._timestamp === new Date(rawData.timestamp).getTime() &&
this._editedTimestamp === new Date(rawData.edited_timestamp).getTime(); this._editedTimestamp === new Date(rawData.edited_timestamp).getTime();
} }
return base; return equal;
} }
/** /**
* Deletes the message * Deletes the message
* @param {number} [timeout=0] How long to wait to delete the message in milliseconds * @param {number} [timeout=0] How long to wait to delete the message in milliseconds
* @returns {Promise<Message, Error>} * @returns {Promise<Message>}
* @example * @example
* // delete a message * // delete a message
* message.delete() * message.delete()
@@ -283,8 +255,8 @@ class Message {
/** /**
* Edit the content of a message * Edit the content of a message
* @param {string} content the new content of a message * @param {string} content The new content for the message
* @returns {Promise<Message, Error>} * @returns {Promise<Message>}
* @example * @example
* // update the content of a message * // update the content of a message
* message.edit('This is my new content!') * message.edit('This is my new content!')
@@ -297,9 +269,9 @@ class Message {
/** /**
* Reply to a message * Reply to a message
* @param {string} content the content of the message * @param {string} content The content for the message
* @param {MessageOptions} [options = {}] the options to provide * @param {MessageOptions} [options = {}] The options to provide
* @returns {Promise<Message, Error>} * @returns {Promise<Message>}
* @example * @example
* // reply to a message * // reply to a message
* message.reply('Hey, I'm a reply!') * message.reply('Hey, I'm a reply!')
@@ -313,7 +285,7 @@ class Message {
/** /**
* Pins this message to the channel's pinned messages * Pins this message to the channel's pinned messages
* @returns {Promise<Message, Error>} * @returns {Promise<Message>}
*/ */
pin() { pin() {
return this.client.rest.methods.pinMessage(this); return this.client.rest.methods.pinMessage(this);
@@ -321,7 +293,7 @@ class Message {
/** /**
* Unpins this message from the channel's pinned messages * Unpins this message from the channel's pinned messages
* @returns {Promise<Message, Error>} * @returns {Promise<Message>}
*/ */
unpin() { unpin() {
return this.client.rest.methods.unpinMessage(this); return this.client.rest.methods.unpinMessage(this);

View File

@@ -1,3 +1,59 @@
/**
* Represents an embed in an image - e.g. preview of image
*/
class MessageEmbed {
constructor(message, data) {
/**
* The message this embed is part of
* @type {Message}
*/
this.message = message;
/**
* The client that instantiated this embed
* @type {Client}
*/
this.client = message.client;
this.setup(data);
}
setup(data) {
/**
* The title of this embed, if there is one
* @type {?string}
*/
this.title = data.title;
/**
* The type of this embed
* @type {string}
*/
this.type = data.type;
/**
* The description of this embed, if there is one
* @type {?string}
*/
this.description = data.description;
/**
* The URL of this embed
* @type {string}
*/
this.url = data.url;
if (data.thumbnail) {
/**
* The thumbnail of this embed, if there is one
* @type {MessageEmbedThumbnail}
*/
this.thumbnail = new MessageEmbedThumbnail(this, data.thumbnail);
}
if (data.provider) {
/**
* The provider of this embed, if there is one
* @type {MessageEmbedProvider}
*/
this.provider = new MessageEmbedProvider(this, data.provider);
}
}
}
/** /**
* Represents a thumbnail for a Message embed * Represents a thumbnail for a Message embed
*/ */
@@ -62,60 +118,4 @@ class MessageEmbedProvider {
} }
} }
/**
* Represents an embed in an image - e.g. preview of image
*/
class MessageEmbed {
constructor(message, data) {
/**
* The message this embed is part of
* @type {Message}
*/
this.message = message;
/**
* The client that instantiated this embed
* @type {Client}
*/
this.client = message.client;
this.setup(data);
}
setup(data) {
/**
* The title of this embed, if there is one
* @type {?string}
*/
this.title = data.title;
/**
* The type of this embed
* @type {string}
*/
this.type = data.type;
/**
* The description of this embed, if there is one
* @type {?string}
*/
this.description = data.description;
/**
* The URL of this embed
* @type {string}
*/
this.url = data.url;
if (data.thumbnail) {
/**
* The thumbnail of this embed, if there is one
* @type {MessageEmbedThumbnail}
*/
this.thumbnail = new MessageEmbedThumbnail(this, data.thumbnail);
}
if (data.provider) {
/**
* The provider of this embed, if there is one
* @type {MessageEmbedProvider}
*/
this.provider = new MessageEmbedProvider(this, data.provider);
}
}
}
module.exports = MessageEmbed; module.exports = MessageEmbed;

View File

@@ -8,9 +8,7 @@ class PermissionOverwrites {
* @type {GuildChannel} * @type {GuildChannel}
*/ */
this.channel = guildChannel; this.channel = guildChannel;
if (data) { if (data) this.setup(data);
this.setup(data);
}
} }
setup(data) { setup(data) {
@@ -30,7 +28,7 @@ class PermissionOverwrites {
/** /**
* Delete this Permission Overwrite. * Delete this Permission Overwrite.
* @returns {Promise<PermissionOverwrites, Error>} * @returns {Promise<PermissionOverwrites>}
*/ */
delete() { delete() {
return this.channel.client.rest.methods.deletePermissionOverwrites(this); return this.channel.client.rest.methods.deletePermissionOverwrites(this);

View File

@@ -15,22 +15,18 @@ class Role {
* @type {Client} * @type {Client}
*/ */
this.client = guild.client; this.client = guild.client;
if (data) { if (data) this.setup(data);
this.setup(data);
}
} }
equals(role) { equals(role) {
return ( return role &&
role &&
this.id === role.id && this.id === role.id &&
this.name === role.name && this.name === role.name &&
this.color === role.color && this.color === role.color &&
this.hoist === role.hoist && this.hoist === role.hoist &&
this.position === role.position && this.position === role.position &&
this.permissions === role.permissions && this.permissions === role.permissions &&
this.managed === role.managed this.managed === role.managed;
);
} }
setup(data) { setup(data) {
@@ -73,7 +69,7 @@ class Role {
/** /**
* Deletes the role * Deletes the role
* @returns {Promise<Role, Error>} * @returns {Promise<Role>}
* @example * @example
* // delete a role * // delete a role
* role.delete() * role.delete()
@@ -86,8 +82,8 @@ class Role {
/** /**
* Edits the role * Edits the role
* @param {RoleData} data the new data for the role * @param {RoleData} data The new data for the role
* @returns {Promise<Role, Error>} * @returns {Promise<Role>}
* @example * @example
* // edit a role * // edit a role
* role.edit({name: 'new role'}) * role.edit({name: 'new role'})
@@ -100,8 +96,8 @@ class Role {
/** /**
* Set a new name for the role * Set a new name for the role
* @param {string} name the new name of the role * @param {string} name The new name of the role
* @returns {Promise<Role, Error>} * @returns {Promise<Role>}
* @example * @example
* // set the name of the role * // set the name of the role
* role.setName('new role') * role.setName('new role')
@@ -114,8 +110,8 @@ class Role {
/** /**
* Set a new color for the role * Set a new color for the role
* @param {number|string} color the new color for the role, either a hex string or a base 10 number * @param {number|string} color The new color for the role, either a hex string or a base 10 number
* @returns {Promise<Role, Error>} * @returns {Promise<Role>}
* @example * @example
* // set the color of a role * // set the color of a role
* role.setColor('#FF0000') * role.setColor('#FF0000')
@@ -128,8 +124,8 @@ class Role {
/** /**
* Set whether or not the role should be hoisted * Set whether or not the role should be hoisted
* @param {boolean} hoist whether or not to hoist the role * @param {boolean} hoist Whether or not to hoist the role
* @returns {Promise<Role, Error>} * @returns {Promise<Role>}
* @example * @example
* // set the hoist of the role * // set the hoist of the role
* role.setHoist(true) * role.setHoist(true)
@@ -142,8 +138,8 @@ class Role {
/** /**
* Set the position of the role * Set the position of the role
* @param {number} position the position of the role * @param {number} position The position of the role
* @returns {Promise<Role, Error>} * @returns {Promise<Role>}
* @example * @example
* // set the position of the role * // set the position of the role
* role.setPosition(1) * role.setPosition(1)
@@ -156,8 +152,8 @@ class Role {
/** /**
* Set the permissions of the role * Set the permissions of the role
* @param {Array<string>} permissions the permissions of the role * @param {string[]} permissions The permissions of the role
* @returns {Promise<Role, Error>} * @returns {Promise<Role>}
* @example * @example
* // set the permissions of the role * // set the permissions of the role
* role.setPermissions(['KICK_MEMBERS', 'BAN_MEMBERS']) * role.setPermissions(['KICK_MEMBERS', 'BAN_MEMBERS'])
@@ -180,14 +176,13 @@ class Role {
for (const permissionName in Constants.PermissionFlags) { for (const permissionName in Constants.PermissionFlags) {
serializedPermissions[permissionName] = this.hasPermission(permissionName); serializedPermissions[permissionName] = this.hasPermission(permissionName);
} }
return serializedPermissions; return serializedPermissions;
} }
/** /**
* Whether or not the role includes the given permission * Whether or not the role includes the given permission
* @param {string} permission the name of the permission to test * @param {PermissionResolvable} permission The name of the permission to test
* @param {boolean} [explicit=false] whether or not the inclusion of the permission is explicit * @param {boolean} [explicit=false] Whether to require the role to explicitly have the exact permission
* @returns {boolean} * @returns {boolean}
* @example * @example
* // see if a role can ban a member * // see if a role can ban a member
@@ -198,20 +193,8 @@ class Role {
* } * }
*/ */
hasPermission(permission, explicit = false) { hasPermission(permission, explicit = false) {
if (typeof permission === 'string') { permission = this.client.resolver.resolvePermission(permission);
permission = Constants.PermissionFlags[permission]; if (!explicit && (this.permissions & Constants.PermissionFlags.ADMINISTRATOR) > 0) return true;
}
if (!permission) {
throw Constants.Errors.NOT_A_PERMISSION;
}
if (!explicit) {
if ((this.permissions & Constants.PermissionFlags.ADMINISTRATOR) > 0) {
return true;
}
}
return (this.permissions & permission) > 0; return (this.permissions & permission) > 0;
} }
@@ -230,9 +213,7 @@ class Role {
*/ */
get hexColor() { get hexColor() {
let col = this.color.toString(16); let col = this.color.toString(16);
while (col.length < 6) { while (col.length < 6) col = `0${col}`;
col = `0${col}`;
}
return `#${col}`; return `#${col}`;
} }
} }

View File

@@ -8,7 +8,6 @@ const Collection = require('../util/Collection');
* @implements {TextBasedChannel} * @implements {TextBasedChannel}
*/ */
class TextChannel extends GuildChannel { class TextChannel extends GuildChannel {
constructor(guild, data) { constructor(guild, data) {
super(guild, data); super(guild, data);
this.messages = new Collection(); this.messages = new Collection();

View File

@@ -8,9 +8,7 @@ const Constants = require('../util/Constants');
class User { class User {
constructor(client, data) { constructor(client, data) {
this.client = client; this.client = client;
if (data) { if (data) this.setup(data);
this.setup(data);
}
} }
setup(data) { setup(data) {
@@ -72,9 +70,7 @@ class User {
* @readonly * @readonly
*/ */
get avatarURL() { get avatarURL() {
if (!this.avatar) { if (!this.avatar) return null;
return null;
}
return Constants.Endpoints.avatar(this.id, this.avatar); return Constants.Endpoints.avatar(this.id, this.avatar);
} }
@@ -89,28 +85,23 @@ class User {
/** /**
* Checks if the user is equal to another. It compares username, ID, discriminator, status and the game being played. * Checks if the user is equal to another. It compares username, ID, discriminator, status and the game being played.
* It is recommended to compare equality by using `user.id === user2.id` unless you want to compare all properties. * It is recommended to compare equality by using `user.id === user2.id` unless you want to compare all properties.
* @param {User} user the user to compare * @param {User} user The user to compare
* @returns {boolean} * @returns {boolean}
*/ */
equals(user) { equals(user) {
let base = user && let equal = user &&
this.username === user.username && this.username === user.username &&
this.id === user.id && this.id === user.id &&
this.discriminator === user.discriminator && this.discriminator === user.discriminator &&
this.avatar === user.avatar && this.avatar === user.avatar &&
this.bot === Boolean(user.bot); this.bot === Boolean(user.bot);
if (base) { if (equal) {
if (user.status) { if (user.status) equal = this.status === user.status;
base = this.status === user.status; if (equal && user.game) equal = this.game === user.game;
}
if (user.game) {
base = this.game === user.game;
}
} }
return base; return equal;
} }
sendMessage() { sendMessage() {

View File

@@ -32,7 +32,7 @@ class VoiceChannel extends GuildChannel {
/** /**
* Sets the bitrate of the channel * Sets the bitrate of the channel
* @param {number} bitrate the new bitrate * @param {number} bitrate The new bitrate
* @returns {Promise<VoiceChannel>} * @returns {Promise<VoiceChannel>}
* @example * @example
* // set the bitrate of a voice channel * // set the bitrate of a voice channel
@@ -46,7 +46,7 @@ class VoiceChannel extends GuildChannel {
/** /**
* Attempts to join this Voice Channel * Attempts to join this Voice Channel
* @returns {Promise<VoiceConnection, Error>} * @returns {Promise<VoiceConnection>}
* @example * @example
* // join a voice channel * // join a voice channel
* voiceChannel.join() * voiceChannel.join()
@@ -64,12 +64,8 @@ class VoiceChannel extends GuildChannel {
* voiceChannel.leave(); * voiceChannel.leave();
*/ */
leave() { leave() {
const exists = this.client.voice.connections.get(this.guild.id); const connection = this.client.voice.connections.get(this.guild.id);
if (exists) { if (connection && connection.channel.id === this.id) connection.disconnect();
if (exists.channel.id === this.id) {
exists.disconnect();
}
}
} }
} }

View File

@@ -1,128 +1,13 @@
const Collection = require('../../util/Collection');
const Message = require('../Message');
const path = require('path'); const path = require('path');
const EventEmitter = require('events').EventEmitter; const EventEmitter = require('events').EventEmitter;
const Message = require('../Message');
/** const Collection = require('../../util/Collection');
* A function that takes a Message object and a MessageCollector and returns a boolean.
* ```js
* function(message, collector) {
* if (message.content.includes('discord')) {
* return true; // passed the filter test
* }
* return false; // failed the filter test
* }
* ```
* @typedef {function} CollectorFilterFunction
*/
/**
* An object containing options used to configure a MessageCollector. All properties are optional.
* ```js
* {
* time: null, // time in milliseconds. If specified, the collector ends after this amount of time.
* max: null, // the maximum amount of messages to handle before ending.
* }
* ```
* @typedef {Object} CollectorOptions
*/
/**
* Collects messages based on a specified filter, then emits them.
* @extends {EventEmitter}
*/
class MessageCollector extends EventEmitter {
constructor(channel, filter, options = {}) {
super();
/**
* The channel this collector is operating on
* @type {Channel}
*/
this.channel = channel;
/**
* A function used to filter messages that the collector collects.
* @type {CollectorFilterFunction}
*/
this.filter = filter;
/**
* Options for the collecor.
* @type {CollectorOptions}
*/
this.options = options;
/**
* Whether this collector has stopped collecting Messages.
* @type {boolean}
*/
this.ended = false;
this.listener = message => this.verify(message);
this.channel.client.on('message', this.listener);
/**
* A collection of collected messages, mapped by message ID.
* @type {Collection<string, Message>}
*/
this.collected = new Collection();
if (options.time) {
this.channel.client.setTimeout(() => this.stop('time'), options.time);
}
}
/**
* Verifies a message against the filter and options
* @private
* @param {Message} message the message
* @returns {boolean}
*/
verify(message) {
if (this.channel ? this.channel.id !== message.channel.id : false) {
return false;
}
if (this.filter(message, this)) {
this.collected.set(message.id, message);
/**
* Emitted whenever the Collector receives a Message that passes the filter test.
* @param {Message} message the received message
* @param {MessageCollector} collector the collector the message passed through.
* @event MessageCollector#message
*/
this.emit('message', message, this);
if (this.options.max && this.collected.size === this.options.max) {
this.stop('limit');
}
return true;
}
return false;
}
/**
* Stops the collector and emits `end`.
* @param {string} [reason='user'] an optional reason for stopping the collector.
*/
stop(reason = 'user') {
if (this.ended) {
return;
}
this.ended = true;
this.channel.client.removeListener('message', this.listener);
/**
* Emitted when the Collector stops collecting.
* @param {Collection<string, Message>} collection A collection of messages collected
* during the lifetime of the Collector.
* Mapped by the ID of the Messages.
* @param {string} reason The reason for the end of the collector. If it ended because it reached the specified time
* limit, this would be `time`. If you invoke `.stop()` without specifying a reason, this would be `user`. If it
* ended because it reached its message limit, it will be `limit`.
* @event MessageCollector#end
*/
this.emit('end', this.collected, reason);
}
}
/** /**
* Interface for classes that have text-channel-like features * Interface for classes that have text-channel-like features
* @interface * @interface
*/ */
class TextBasedChannel { class TextBasedChannel {
constructor() { constructor() {
/** /**
* A Collection containing the messages sent to this channel. * A Collection containing the messages sent to this channel.
@@ -133,19 +18,16 @@ class TextBasedChannel {
/** /**
* Bulk delete a given Collection or Array of messages in one go. Returns the deleted messages after. * Bulk delete a given Collection or Array of messages in one go. Returns the deleted messages after.
* @param {Map<string, Message>|Array<Message>} messages the messages to delete * @param {Collection<string, Message>|Message[]} messages The messages to delete
* @returns {Collection<string, Message>} * @returns {Collection<string, Message>}
*/ */
bulkDelete(messages) { bulkDelete(messages) {
if (messages instanceof Map) { if (messages instanceof Collection) messages = messages.array();
messages = messages.array(); if (!(messages instanceof Array)) return Promise.reject(new TypeError('messages must be an array or collection'));
}
if (!(messages instanceof Array)) {
return Promise.reject('pass an array or map');
}
const messageIDs = messages.map(m => m.id); const messageIDs = messages.map(m => m.id);
return this.client.rest.methods.bulkDeleteMessages(this, messageIDs); return this.client.rest.methods.bulkDeleteMessages(this, messageIDs);
} }
/** /**
* Options that can be passed into sendMessage or sendTTSMessage: * Options that can be passed into sendMessage or sendTTSMessage:
* ```js * ```js
@@ -156,10 +38,11 @@ class TextBasedChannel {
* ``` * ```
* @typedef {Object} MessageOptions * @typedef {Object} MessageOptions
*/ */
/** /**
* Send a message to this channel * Send a message to this channel
* @param {string} content the content to send * @param {string} content The content to send
* @param {MessageOptions} [options={}] the options to provide * @param {MessageOptions} [options={}] The options to provide
* @returns {Promise<Message>} * @returns {Promise<Message>}
* @example * @example
* // send a message * // send a message
@@ -170,10 +53,11 @@ class TextBasedChannel {
sendMessage(content, options = {}) { sendMessage(content, options = {}) {
return this.client.rest.methods.sendMessage(this, content, options.tts, options.nonce); return this.client.rest.methods.sendMessage(this, content, options.tts, options.nonce);
} }
/** /**
* Send a text-to-speech message to this channel * Send a text-to-speech message to this channel
* @param {string} content the content to send * @param {string} content The content to send
* @param {MessageOptions} [options={}] the options to provide * @param {MessageOptions} [options={}] The options to provide
* @returns {Promise<Message>} * @returns {Promise<Message>}
* @example * @example
* // send a TTS message * // send a TTS message
@@ -184,6 +68,7 @@ class TextBasedChannel {
sendTTSMessage(content, options = {}) { sendTTSMessage(content, options = {}) {
return this.client.rest.methods.sendMessage(this, content, true, options.nonce); return this.client.rest.methods.sendMessage(this, content, true, options.nonce);
} }
/** /**
* Send a file to this channel * Send a file to this channel
* @param {FileResolvable} attachment The file to send * @param {FileResolvable} attachment The file to send
@@ -201,16 +86,15 @@ class TextBasedChannel {
} }
} }
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.client.resolver.resolveFile(attachment) this.client.resolver.resolveFile(attachment).then(file => {
.then(file => {
this.client.rest.methods.sendMessage(this, undefined, false, undefined, { this.client.rest.methods.sendMessage(this, undefined, false, undefined, {
file, file,
name: fileName, name: fileName,
}).then(resolve).catch(reject); }).then(resolve).catch(reject);
}) }).catch(reject);
.catch(reject);
}); });
} }
/** /**
* The parameters to pass in when requesting previous messages from a channel. `around`, `before` and * The parameters to pass in when requesting previous messages from a channel. `around`, `before` and
* `after` are mutually exclusive. All the parameters are optional. * `after` are mutually exclusive. All the parameters are optional.
@@ -227,8 +111,8 @@ class TextBasedChannel {
/** /**
* Gets the past messages sent in this channel. Resolves with a Collection mapping message ID's to Message objects. * Gets the past messages sent in this channel. Resolves with a Collection mapping message ID's to Message objects.
* @param {ChannelLogsQueryOptions} [options={}] the query parameters to pass in * @param {ChannelLogsQueryOptions} [options={}] The query parameters to pass in
* @returns {Promise<Collection<string, Message>, Error>} * @returns {Promise<Collection<string, Message>>}
* @example * @example
* // get messages * // get messages
* channel.fetchMessages({limit: 10}) * channel.fetchMessages({limit: 10})
@@ -237,17 +121,15 @@ class TextBasedChannel {
*/ */
fetchMessages(options = {}) { fetchMessages(options = {}) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.client.rest.methods.getChannelMessages(this, options) this.client.rest.methods.getChannelMessages(this, options).then(data => {
.then(data => { const messages = new Collection();
const messages = new Collection(); for (const message of data) {
for (const message of data) { const msg = new Message(this, message, this.client);
const msg = new Message(this, message, this.client); messages.set(message.id, msg);
messages.set(message.id, msg); this._cacheMessage(msg);
this._cacheMessage(msg); }
} resolve(messages);
resolve(messages); }).catch(reject);
})
.catch(reject);
}); });
} }
@@ -278,7 +160,7 @@ class TextBasedChannel {
* Stops the typing indicator in the channel. * Stops the typing indicator in the channel.
* The indicator will only stop if this is called as many times as startTyping(). * The indicator will only stop if this is called as many times as startTyping().
* <info>It can take a few seconds for the Client User to stop typing.</info> * <info>It can take a few seconds for the Client User to stop typing.</info>
* @param {boolean} [force=false] whether or not to force the indicator to stop regardless of call count * @param {boolean} [force=false] Whether or not to reset the call count and force the indicator to stop
* @example * @example
* // stop typing in a channel * // stop typing in a channel
* channel.stopTyping(); * channel.stopTyping();
@@ -316,8 +198,8 @@ class TextBasedChannel {
/** /**
* Creates a Message Collector * Creates a Message Collector
* @param {CollectorFilterFunction} filter the filter to create the collector with * @param {CollectorFilterFunction} filter The filter to create the collector with
* @param {CollectorOptions} [options={}] the options to pass to the collector * @param {CollectorOptions} [options={}] The options to pass to the collector
* @returns {MessageCollector} * @returns {MessageCollector}
* @example * @example
* // create a message collector * // create a message collector
@@ -329,8 +211,7 @@ class TextBasedChannel {
* collector.on('end', collected => console.log(`Collected ${collected.size} items`)); * collector.on('end', collected => console.log(`Collected ${collected.size} items`));
*/ */
createCollector(filter, options = {}) { createCollector(filter, options = {}) {
const collector = new MessageCollector(this, filter, options); return new MessageCollector(this, filter, options);
return collector;
} }
/** /**
@@ -346,8 +227,8 @@ class TextBasedChannel {
/** /**
* Similar to createCollector but in Promise form. Resolves with a Collection of messages that pass the specified * Similar to createCollector but in Promise form. Resolves with a Collection of messages that pass the specified
* filter. * filter.
* @param {CollectorFilterFunction} filter the filter function to use * @param {CollectorFilterFunction} filter The filter function to use
* @param {AwaitMessagesOptions} [options={}] optional options to pass to the internal collector * @param {AwaitMessagesOptions} [options={}] Optional options to pass to the internal collector
* @returns {Promise<Collection<string, Message>>} * @returns {Promise<Collection<string, Message>>}
* @example * @example
* // await !vote messages * // await !vote messages
@@ -372,43 +253,140 @@ class TextBasedChannel {
_cacheMessage(message) { _cacheMessage(message) {
const maxSize = this.client.options.max_message_cache; const maxSize = this.client.options.max_message_cache;
if (maxSize === 0) { if (maxSize === 0) return null;
// saves on performance if (this.messages.size >= maxSize) this.messages.delete(this.messages.keys().next().value);
return null;
}
if (this.messages.size >= maxSize) {
this.messages.delete(this.messages.keys().next().value);
}
this.messages.set(message.id, message); this.messages.set(message.id, message);
return message; return message;
} }
/** /**
* Fetches the pinned messages of this Channel and returns a Collection of them. * Fetches the pinned messages of this Channel and returns a Collection of them.
* @returns {Promise<Collection<string, Message>, Error>} * @returns {Promise<Collection<string, Message>>}
*/ */
fetchPinnedMessages() { fetchPinnedMessages() {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.client.rest.methods.getChannelPinnedMessages(this) this.client.rest.methods.getChannelPinnedMessages(this).then(data => {
.then(data => { const messages = new Collection();
const messages = new Collection(); for (const message of data) {
for (const message of data) { const msg = new Message(this, message, this.client);
const msg = new Message(this, message, this.client); messages.set(message.id, msg);
messages.set(message.id, msg); this._cacheMessage(msg);
this._cacheMessage(msg); }
} resolve(messages);
resolve(messages); }).catch(reject);
})
.catch(reject);
}); });
} }
} }
function applyProp(structure, prop) { /**
Object.defineProperty(structure.prototype, prop, Object.getOwnPropertyDescriptor(TextBasedChannel.prototype, prop)); * Collects messages based on a specified filter, then emits them.
* @extends {EventEmitter}
*/
class MessageCollector extends EventEmitter {
/**
* A function that takes a Message object and a MessageCollector and returns a boolean.
* ```js
* function(message, collector) {
* if (message.content.includes('discord')) {
* return true; // passed the filter test
* }
* return false; // failed the filter test
* }
* ```
* @typedef {function} CollectorFilterFunction
*/
/**
* An object containing options used to configure a MessageCollector. All properties are optional.
* ```js
* {
* time: null, // time in milliseconds. If specified, the collector ends after this amount of time.
* max: null, // the maximum amount of messages to handle before ending.
* }
* ```
* @typedef {Object} CollectorOptions
*/
/**
* @param {Channel} channel The channel to collect messages in
* @param {CollectorFilterFunction} filter The filter function
* @param {CollectorOptions} [options] Options for the collector
*/
constructor(channel, filter, options = {}) {
super();
/**
* The channel this collector is operating on
* @type {Channel}
*/
this.channel = channel;
/**
* A function used to filter messages that the collector collects.
* @type {CollectorFilterFunction}
*/
this.filter = filter;
/**
* Options for the collecor.
* @type {CollectorOptions}
*/
this.options = options;
/**
* Whether this collector has stopped collecting Messages.
* @type {boolean}
*/
this.ended = false;
this.listener = message => this.verify(message);
this.channel.client.on('message', this.listener);
/**
* A collection of collected messages, mapped by message ID.
* @type {Collection<string, Message>}
*/
this.collected = new Collection();
if (options.time) this.channel.client.setTimeout(() => this.stop('time'), options.time);
}
/**
* Verifies a message against the filter and options
* @private
* @param {Message} message The message
* @returns {boolean}
*/
verify(message) {
if (this.channel ? this.channel.id !== message.channel.id : false) return false;
if (this.filter(message, this)) {
this.collected.set(message.id, message);
/**
* Emitted whenever the Collector receives a Message that passes the filter test.
* @param {Message} message The received message
* @param {MessageCollector} collector The collector the message passed through
* @event MessageCollector#message
*/
this.emit('message', message, this);
if (this.options.max && this.collected.size === this.options.max) this.stop('limit');
return true;
}
return false;
}
/**
* Stops the collector and emits `end`.
* @param {string} [reason='user'] An optional reason for stopping the collector
*/
stop(reason = 'user') {
if (this.ended) return;
this.ended = true;
this.channel.client.removeListener('message', this.listener);
/**
* Emitted when the Collector stops collecting.
* @param {Collection<string, Message>} collection A collection of messages collected
* during the lifetime of the Collector, mapped by the ID of the Messages.
* @param {string} reason The reason for the end of the collector. If it ended because it reached the specified time
* limit, this would be `time`. If you invoke `.stop()` without specifying a reason, this would be `user`. If it
* ended because it reached its message limit, it will be `limit`.
* @event MessageCollector#end
*/
this.emit('end', this.collected, reason);
}
} }
exports.applyToClass = (structure, full = false) => { exports.applyToClass = (structure, full = false) => {
@@ -425,7 +403,9 @@ exports.applyToClass = (structure, full = false) => {
props.push('createCollector'); props.push('createCollector');
props.push('awaitMessages'); props.push('awaitMessages');
} }
for (const prop of props) { for (const prop of props) applyProp(structure, prop);
applyProp(structure, prop);
}
}; };
function applyProp(structure, prop) {
Object.defineProperty(structure.prototype, prop, Object.getOwnPropertyDescriptor(TextBasedChannel.prototype, prop));
}

14
src/util/ArraysEqual.js Normal file
View File

@@ -0,0 +1,14 @@
module.exports = function arraysEqual(a, b) {
if (a === b) return true;
if (a.length !== b.length) return false;
for (const itemInd in a) {
const item = a[itemInd];
const ind = b.indexOf(item);
if (ind) {
b.splice(ind, 1);
}
}
return b.length === 0;
};

View File

@@ -1,6 +1,5 @@
module.exports = function cloneObject(obj) { module.exports = function cloneObject(obj) {
const cloned = Object.create(obj); const cloned = Object.create(obj);
Object.assign(cloned, obj); Object.assign(cloned, obj);
return cloned; return cloned;
}; };

View File

@@ -3,10 +3,9 @@
* @extends {Map} * @extends {Map}
*/ */
class Collection extends Map { class Collection extends Map {
/** /**
* Returns an ordered array of the values of this collection. * Returns an ordered array of the values of this collection.
* @returns {Array} * @returns {*[]}
* @example * @example
* // identical to: * // identical to:
* Array.from(collection.values()); * Array.from(collection.values());
@@ -17,18 +16,19 @@ class Collection extends Map {
/** /**
* Returns the first item in this collection. * Returns the first item in this collection.
* @returns {Object} * @returns {*}
* @example * @example
* // identical to: * // identical to:
* Array.from(collection.values())[0]; * Array.from(collection.values())[0];
*/ */
first() { first() {
return this.array()[0]; return this.values().next().value;
} }
/** /**
* Returns the last item in this collection. * Returns the last item in this collection. This is a relatively slow operation,
* @returns {Object} * since an array copy of the values must be made to find the last element.
* @returns {*}
*/ */
last() { last() {
const arr = this.array(); const arr = this.array();
@@ -36,8 +36,9 @@ class Collection extends Map {
} }
/** /**
* Returns a random item from this collection. * Returns a random item from this collection. This is a relatively slow operation,
* @returns {Object} * since an array copy of the values must be made to find a random element.
* @returns {*}
*/ */
random() { random() {
const arr = this.array(); const arr = this.array();
@@ -47,32 +48,21 @@ class Collection extends Map {
/** /**
* If the items in this collection have a delete method (e.g. messages), invoke * If the items in this collection have a delete method (e.g. messages), invoke
* the delete method. Returns an array of promises * the delete method. Returns an array of promises
* @returns {Array<Promise>} * @returns {Promise[]}
*/ */
deleteAll() { deleteAll() {
const returns = []; const returns = [];
for (const item of this.values()) { for (const item of this.values()) {
if (item.delete) { if (item.delete) returns.push(item.delete());
returns.push(item.delete());
}
} }
return returns; return returns;
} }
/**
* The length (size) of this collection.
* @readonly
* @type {number}
*/
get length() {
return this.size;
}
/** /**
* Returns an array of items where `item[key] === value` of the collection * Returns an array of items where `item[key] === value` of the collection
* @param {string} key the key to filter by * @param {string} key The key to filter by
* @param {*} value the expected value * @param {*} value The expected value
* @returns {Array<Object>} * @returns {*[]}
* @example * @example
* collection.getAll('username', 'Bob'); * collection.getAll('username', 'Bob');
*/ */
@@ -81,18 +71,16 @@ class Collection extends Map {
if (typeof value === 'undefined') throw new Error('value must be specified'); if (typeof value === 'undefined') throw new Error('value must be specified');
const results = []; const results = [];
for (const item of this.values()) { for (const item of this.values()) {
if (item[key] === value) { if (item[key] === value) results.push(item);
results.push(item);
}
} }
return results; return results;
} }
/** /**
* Returns a single item where `item[key] === value` * Returns a single item where `item[key] === value`
* @param {string} key the key to filter by * @param {string} key The key to filter by
* @param {*} value the expected value * @param {*} value The expected value
* @returns {Object} * @returns {*}
* @example * @example
* collection.get('id', '123123...'); * collection.get('id', '123123...');
*/ */
@@ -100,17 +88,15 @@ class Collection extends Map {
if (typeof key !== 'string') throw new TypeError('key must be a string'); if (typeof key !== 'string') throw new TypeError('key must be a string');
if (typeof value === 'undefined') throw new Error('value must be specified'); if (typeof value === 'undefined') throw new Error('value must be specified');
for (const item of this.values()) { for (const item of this.values()) {
if (item[key] === value) { if (item[key] === value) return item;
return item;
}
} }
return null; return null;
} }
/** /**
* Returns true if the collection has an item where `item[key] === value` * Returns true if the collection has an item where `item[key] === value`
* @param {string} key the key to filter by * @param {string} key The key to filter by
* @param {*} value the expected value * @param {*} value The expected value
* @returns {boolean} * @returns {boolean}
* @example * @example
* if (collection.exists('id', '123123...')) { * if (collection.exists('id', '123123...')) {
@@ -121,32 +107,26 @@ class Collection extends Map {
return Boolean(this.find(key, value)); return Boolean(this.find(key, value));
} }
_arrayMethod(method, args) {
return Array.prototype[method].apply(this.array(), args);
}
/** /**
* Identical to * Identical to
* [Array.filter()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/filter), * [Array.filter()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/filter),
* but returns a Collection instead of an Array. * but returns a Collection instead of an Array.
* @param {function} callback the callback used to filter * @param {function} callback Function used to filter (should return a boolean)
* @param {Object} [thisArg] value to set as this when filtering * @param {Object} [thisArg] Value to set as this when filtering
* @returns {Collection} * @returns {Collection}
*/ */
filter(...args) { filter(...args) {
const newArray = this.array().filter(...args); const newArray = this.array().filter(...args);
const collection = new Collection(); const collection = new Collection();
for (const item of newArray) { for (const item of newArray) collection.set(item.id, item);
collection.set(item.id, item);
}
return collection; return collection;
} }
/** /**
* Functionally identical shortcut to `collection.array().map(...)`. * Functionally identical shortcut to `collection.array().map(...)`.
* @param {function} callback Function that produces an element of the new Array, taking three arguments. * @param {function} callback Function that produces an element of the new Array, taking three arguments
* @param {*} [thisArg] Optional. Value to use as this when executing callback. * @param {*} [thisArg] Optional. Value to use as this when executing callback.
* @returns {array} * @returns {*[]}
*/ */
map(...args) { map(...args) {
return this.array().map(...args); return this.array().map(...args);

View File

@@ -67,7 +67,7 @@ exports.Errors = {
NO_BOT_ACCOUNT: new Error('you should ideally be using a bot account!'), NO_BOT_ACCOUNT: new Error('you should ideally be using a bot account!'),
BAD_WS_MESSAGE: new Error('a bad message was received from the websocket - bad compression or not json'), BAD_WS_MESSAGE: new Error('a bad message was received from the websocket - bad compression or not json'),
TOOK_TOO_LONG: new Error('something took too long to do'), TOOK_TOO_LONG: new Error('something took too long to do'),
NOT_A_PERMISSION: new Error('that is not a valid permission number'), NOT_A_PERMISSION: new Error('that is not a valid permission string or number'),
INVALID_RATE_LIMIT_METHOD: new Error('unknown rate limiting method'), INVALID_RATE_LIMIT_METHOD: new Error('unknown rate limiting method'),
BAD_LOGIN: new Error('incorrect login details were provided'), BAD_LOGIN: new Error('incorrect login details were provided'),
}; };
@@ -235,10 +235,7 @@ const PermissionFlags = exports.PermissionFlags = {
}; };
let _ALL_PERMISSIONS = 0; let _ALL_PERMISSIONS = 0;
for (const key in PermissionFlags) _ALL_PERMISSIONS |= PermissionFlags[key];
for (const key in PermissionFlags) {
_ALL_PERMISSIONS |= PermissionFlags[key];
}
exports.ALL_PERMISSIONS = _ALL_PERMISSIONS; exports.ALL_PERMISSIONS = _ALL_PERMISSIONS;

View File

@@ -1,10 +1,5 @@
module.exports = function merge(def, given) { module.exports = function merge(def, given) {
if (!given) { if (!given) return def;
return def;
}
given = given || {};
for (const key in def) { for (const key in def) {
if (!{}.hasOwnProperty.call(given, key)) { if (!{}.hasOwnProperty.call(given, key)) {
given[key] = def[key]; given[key] = def[key];