Merge branch 'master' into indev-prism

This commit is contained in:
Schuyler Cebulskie
2017-02-01 15:29:45 -05:00
28 changed files with 275 additions and 109 deletions

View File

@@ -1,3 +1,4 @@
const os = require('os');
const EventEmitter = require('events').EventEmitter;
const mergeDefault = require('../util/MergeDefault');
const Constants = require('../util/Constants');
@@ -223,7 +224,7 @@ class Client extends EventEmitter {
* @readonly
*/
get browser() {
return typeof window !== 'undefined';
return os.platform() === 'browser';
}
/**

View File

@@ -123,6 +123,7 @@ class ClientDataManager {
const oldEmoji = cloneObject(currentEmoji);
currentEmoji.setup(newData);
this.client.emit(Constants.Events.GUILD_EMOJI_UPDATE, oldEmoji, currentEmoji);
return currentEmoji;
}
}

View File

@@ -2,7 +2,10 @@ const Action = require('./Action');
class GuildEmojiUpdateAction extends Action {
handle(oldEmoji, newEmoji) {
this.client.dataManager.updateEmoji(oldEmoji, newEmoji);
const emoji = this.client.dataManager.updateEmoji(oldEmoji, newEmoji);
return {
emoji,
};
}
}

View File

@@ -14,17 +14,32 @@ class MessageCreateAction extends Action {
for (let i = 0; i < data.length; i++) {
messages[i] = channel._cacheMessage(new Message(channel, data[i], client));
}
channel.lastMessageID = messages[messages.length - 1].id;
if (user) user.lastMessageID = messages[messages.length - 1].id;
if (member) member.lastMessageID = messages[messages.length - 1].id;
const lastMessage = messages[messages.length - 1];
channel.lastMessageID = lastMessage.id;
channel.lastMessage = lastMessage;
if (user) {
user.lastMessageID = lastMessage.id;
user.lastMessage = lastMessage;
}
if (member) {
member.lastMessageID = lastMessage.id;
member.lastMessage = lastMessage;
}
return {
messages,
};
} else {
const message = channel._cacheMessage(new Message(channel, data, client));
channel.lastMessageID = data.id;
if (user) user.lastMessageID = data.id;
if (member) member.lastMessageID = data.id;
channel.lastMessage = message;
if (user) {
user.lastMessageID = data.id;
user.lastMessage = message;
}
if (member) {
member.lastMessageID = data.id;
member.lastMessage = message;
}
return {
message,
};

View File

@@ -97,7 +97,7 @@ class RESTMethods {
const options = index === list.length ? { tts, embed } : { tts };
chan.send(list[index], options, index === list.length ? file : null).then((message) => {
messages.push(message);
if (index >= list.length) return resolve(messages);
if (index >= list.length - 1) return resolve(messages);
return sendChunk(list, ++index);
});
}(content, 0));
@@ -165,8 +165,9 @@ class RESTMethods {
search(target, options) {
options = transformSearchOptions(options, this.client);
for (const key in options) if (options[key] === undefined) delete options[key];
const queryString = querystring.stringify(options);
const queryString = (querystring.stringify(options).match(/[^=&?]+=[^=&?]+/g) || []).join('&');
let type;
if (target instanceof Channel) {
@@ -323,8 +324,9 @@ class RESTMethods {
);
}
createGuildRole(guild) {
return this.rest.makeRequest('post', Constants.Endpoints.guildRoles(guild.id), true).then(role =>
createGuildRole(guild, data) {
if (data.color) data.color = this.client.resolver.resolveColor(data.color);
return this.rest.makeRequest('post', Constants.Endpoints.guildRoles(guild.id), true, data).then(role =>
this.client.actions.GuildRoleCreate.handle({
guild_id: guild.id,
role,
@@ -557,14 +559,24 @@ class RESTMethods {
.then(data => data.pruned);
}
createEmoji(guild, image, name) {
return this.rest.makeRequest('post', `${Constants.Endpoints.guildEmojis(guild.id)}`, true, { name, image })
.then(data => this.client.actions.EmojiCreate.handle(data, guild).emoji);
createEmoji(guild, image, name, roles) {
const data = { image, name };
if (roles) data.roles = roles.map(r => r.id ? r.id : r);
return this.rest.makeRequest('post', `${Constants.Endpoints.guildEmojis(guild.id)}`, true, data)
.then(emoji => this.client.actions.GuildEmojiCreate.handle(guild, emoji).emoji);
}
updateEmoji(emoji, _data) {
const data = {};
if (_data.name) data.name = _data.name;
if (_data.roles) data.roles = _data.roles.map(r => r.id ? r.id : r);
return this.rest.makeRequest('patch', Constants.Endpoints.guildEmoji(emoji.guild.id, emoji.id), true, data)
.then(newEmoji => this.client.actions.GuildEmojiUpdate.handle(emoji, newEmoji).emoji);
}
deleteEmoji(emoji) {
return this.rest.makeRequest('delete', `${Constants.Endpoints.guildEmojis(emoji.guild.id)}/${emoji.id}`, true)
.then(() => this.client.actions.EmojiDelete.handle(emoji).data);
.then(() => this.client.actions.GuildEmojiDelete.handle(emoji).data);
}
getWebhook(id, token) {
@@ -693,7 +705,7 @@ class RESTMethods {
removeMessageReaction(message, emoji, user) {
let endpoint = Constants.Endpoints.selfMessageReaction(message.channel.id, message.id, emoji);
if (user !== this.client.user.id) {
endpoint = Constants.Endpoints.userMessageReaction(message.channel.id, message.id, emoji, null, user.id);
endpoint = Constants.Endpoints.userMessageReaction(message.channel.id, message.id, emoji, null, user);
}
return this.rest.makeRequest('delete', endpoint, true).then(() =>
this.client.actions.MessageReactionRemove.handle({

View File

@@ -1,4 +1,4 @@
const browser = typeof window !== 'undefined';
const browser = require('os').platform() === 'browser';
const EventEmitter = require('events').EventEmitter;
const Constants = require('../../util/Constants');
const convertArrayBuffer = require('../../util/ConvertArrayBuffer');
@@ -155,7 +155,7 @@ class WebSocketManager extends EventEmitter {
}
destroy() {
this.ws.close(1000);
if (this.ws) this.ws.close(1000);
this._queue = [];
this.status = Constants.Status.IDLE;
}

View File

@@ -1,7 +1,7 @@
// ##untested##
const AbstractHandler = require('./AbstractHandler');
const Constants = require('../../../../util/Constants');
// uncomment in v12
// const Collection = require('../../../../util/Collection');
class GuildMembersChunkHandler extends AbstractHandler {
handle(packet) {
@@ -10,10 +10,14 @@ class GuildMembersChunkHandler extends AbstractHandler {
const guild = client.guilds.get(data.guild_id);
if (!guild) return;
// uncomment in v12
// const members = new Collection();
//
// for (const member of data.members) members.set(member.id, guild._addMember(member, false));
const members = data.members.map(member => guild._addMember(member, false));
guild._checkChunks();
client.emit(Constants.Events.GUILD_MEMBERS_CHUNK, members);
client.emit(Constants.Events.GUILD_MEMBERS_CHUNK, members, guild);
client.ws.lastHeartbeatAck = true;
}
@@ -22,7 +26,8 @@ class GuildMembersChunkHandler extends AbstractHandler {
/**
* Emitted whenever a chunk of guild members is received (all members come from the same guild)
* @event Client#guildMembersChunk
* @param {GuildMember[]} members The members in the chunk
* @param {Collection<GuildMember>} members The members in the chunk
* @param {Guild} guild The guild related to the member chunk
*/
module.exports = GuildMembersChunkHandler;

View File

@@ -46,4 +46,4 @@ module.exports = {
Constants: require('./util/Constants'),
};
if (typeof window !== 'undefined') window.Discord = module.exports; // eslint-disable-line no-undef
if (require('os').platform() === 'browser') window.Discord = module.exports; // eslint-disable-line no-undef

View File

@@ -101,6 +101,27 @@ class Emoji {
return encodeURIComponent(this.name);
}
/**
* Data for editing an emoji
* @typedef {Object} EmojiEditData
* @property {string} [name] The name of the emoji
* @property {Collection<string, Role>|Array<string|Role>} [roles] Roles to restrict emoji to
*/
/**
* Edits the emoji
* @param {EmojiEditData} data The new data for the emoji
* @returns {Promise<Emoji>}
* @example
* // edit a emoji
* emoji.edit({name: 'newemoji'})
* .then(e => console.log(`Edited emoji ${e}`))
* .catch(console.error);
*/
edit(data) {
return this.client.rest.methods.updateEmoji(this, data);
}
/**
* When concatenated with a string, this automatically returns the emoji mention rather than the object.
* @returns {string}

View File

@@ -337,7 +337,6 @@ class Guild {
* @returns {Promise<GuildMember>}
*/
fetchMember(user, cache = true) {
if (this._fetchWaiter) return Promise.reject(new Error('Already fetching guild members.'));
user = this.client.resolver.resolveUser(user);
if (!user) return Promise.reject(new Error('User is not cached. Use Client.fetchUser first.'));
if (this.members.has(user.id)) return Promise.resolve(this.members.get(user.id));
@@ -347,26 +346,37 @@ class Guild {
/**
* 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.
* @param {string} [query=''] An optional query to provide when fetching members
* @param {string} [query=''] Limit fetch to members with similar usernames
* @param {number} [limit=0] Maximum number of members to request
* @returns {Promise<Guild>}
*/
fetchMembers(query = '') {
fetchMembers(query = '', limit = 0) {
return new Promise((resolve, reject) => {
if (this._fetchWaiter) throw new Error(`Already fetching guild members in ${this.id}.`);
if (this.memberCount === this.members.size) {
// uncomment in v12
// resolve(this.members)
resolve(this);
return;
}
this._fetchWaiter = resolve;
this.client.ws.send({
op: Constants.OPCodes.REQUEST_GUILD_MEMBERS,
d: {
guild_id: this.id,
query,
limit: 0,
limit,
},
});
this._checkChunks();
const handler = (members, guild) => {
if (guild.id !== this.id) return;
if (this.memberCount === this.members.size || members.length < 1000) {
this.client.removeListener(Constants.Events.GUILD_MEMBERS_CHUNK, handler);
// uncomment in v12
// resolve(this.members)
resolve(this);
return;
}
};
this.client.on(Constants.Events.GUILD_MEMBERS_CHUNK, handler);
this.client.setTimeout(() => reject(new Error('Members didn\'t arrive in time.')), 120 * 1000);
});
}
@@ -608,7 +618,7 @@ class Guild {
}
/**
* Creates a new role in the guild, and optionally updates it with the given information.
* Creates a new role in the guild with given information
* @param {RoleData} [data] The data to update the role with
* @returns {Promise<Role>}
* @example
@@ -618,14 +628,15 @@ class Guild {
* .catch(console.error);
* @example
* // create a new role with data
* guild.createRole({ name: 'Super Cool People' })
* .then(role => console.log(`Created role ${role}`))
* .catch(console.error)
* guild.createRole({
* name: 'Super Cool People',
* color: 'BLUE',
* })
* .then(role => console.log(`Created role ${role}`))
* .catch(console.error)
*/
createRole(data) {
const create = this.client.rest.methods.createGuildRole(this);
if (!data) return create;
return create.then(role => role.edit(data));
return this.client.rest.methods.createGuildRole(this, data);
}
/**
@@ -671,6 +682,7 @@ class Guild {
* Creates a new custom emoji in the guild.
* @param {BufferResolvable|Base64Resolvable} attachment The image for the emoji.
* @param {string} name The name for the emoji.
* @param {Collection<Role>|Role[]} [roles] Roles to limit the emoji to
* @returns {Promise<Emoji>} The created emoji.
* @example
* // create a new emoji from a url
@@ -683,13 +695,13 @@ class Guild {
* .then(emoji => console.log(`Created new emoji with name ${emoji.name}!`))
* .catch(console.error);
*/
createEmoji(attachment, name) {
createEmoji(attachment, name, roles) {
return new Promise(resolve => {
if (typeof attachment === 'string' && attachment.startsWith('data:')) {
resolve(this.client.rest.methods.createEmoji(this, attachment, name));
resolve(this.client.rest.methods.createEmoji(this, attachment, name, roles));
} else {
this.client.resolver.resolveBuffer(attachment).then(data =>
resolve(this.client.rest.methods.createEmoji(this, data, name))
resolve(this.client.rest.methods.createEmoji(this, data, name, roles))
);
}
});
@@ -811,7 +823,6 @@ class Guild {
this.client.emit(Constants.Events.GUILD_MEMBER_ADD, member);
}
this._checkChunks();
return member;
}
@@ -841,7 +852,6 @@ class Guild {
_removeMember(guildMember) {
this.members.delete(guildMember.id);
this._checkChunks();
}
_memberSpeakUpdate(user, speaking) {
@@ -865,15 +875,6 @@ class Guild {
}
this.presences.set(id, new Presence(presence));
}
_checkChunks() {
if (this._fetchWaiter) {
if (this.members.size === this.memberCount) {
this._fetchWaiter(this);
this._fetchWaiter = null;
}
}
}
}
module.exports = Guild;

View File

@@ -281,6 +281,16 @@ class GuildChannel extends Channel {
return equal;
}
/**
* Whether the channel is deletable by the client user.
* @type {boolean}
* @readonly
*/
get deletable() {
return this.id !== this.guild.id &&
this.permissionsFor(this.client.user).hasPermission(Constants.PermissionFlags.MANAGE_CHANNELS);
}
/**
* When concatenated with a string, this automatically returns the channel's mention instead of the Channel object.
* @returns {string}

View File

@@ -39,6 +39,12 @@ class GuildMember {
* @type {?Snowflake}
*/
this.lastMessageID = null;
/**
* The Message object of the last message sent by the member in their guild, if one was sent.
* @type {?Message}
*/
this.lastMessage = null;
}
setup(data) {
@@ -389,12 +395,20 @@ class GuildMember {
return this.edit({ nick });
}
/**
* Creates a DM channel between the client and the member
* @returns {Promise<DMChannel>}
*/
createDM() {
return this.user.createDM();
}
/**
* Deletes any DMs with this guild member
* @returns {Promise<DMChannel>}
*/
deleteDM() {
return this.client.rest.methods.deleteChannel(this);
return this.user.deleteDM();
}
/**

View File

@@ -197,12 +197,13 @@ class Message {
if (data.type === 6) this.system = true;
}
if (data.attachments) {
this.attachments = new Collection();
this.attachments.clear();
for (const attachment of data.attachments) {
this.attachments.set(attachment.id, new Attachment(this, attachment));
}
}
if (data.mentions) {
this.mentions.users.clear();
for (const mention of data.mentions) {
let user = this.client.users.get(mention.id);
if (user) {
@@ -214,6 +215,7 @@ class Message {
}
}
if (data.mention_roles) {
this.mentions.roles.clear();
for (const mention of data.mention_roles) {
const role = this.channel.guild.roles.get(mention);
if (role) this.mentions.roles.set(role.id, role);
@@ -221,6 +223,7 @@ class Message {
}
if (data.id) this.id = data.id;
if (this.channel.guild && data.content) {
this.mentions.channels.clear();
const channMentionsRaw = data.content.match(/<#([0-9]{14,20})>/g) || [];
for (const raw of channMentionsRaw) {
const chan = this.channel.guild.channels.get(raw.match(/([0-9]{14,20})/g)[0]);
@@ -228,11 +231,11 @@ class Message {
}
}
if (data.reactions) {
this.reactions = new Collection();
this.reactions.clear();
if (data.reactions.length > 0) {
for (const reaction of data.reactions) {
const id = reaction.emoji.id ? `${reaction.emoji.name}:${reaction.emoji.id}` : reaction.emoji.name;
this.reactions.set(id, new MessageReaction(this, data.emoji, data.count, data.me));
this.reactions.set(id, new MessageReaction(this, reaction.emoji, reaction.count, reaction.me));
}
}
}

View File

@@ -55,6 +55,12 @@ class User {
* @type {?Snowflake}
*/
this.lastMessageID = null;
/**
* The Message object of the last message sent by the user, if one was sent.
* @type {?Message}
*/
this.lastMessage = null;
}
patch(data) {
@@ -173,6 +179,14 @@ class User {
return this.client.channels.filter(c => c.type === 'dm').find(c => c.recipient.id === this.id);
}
/**
* Creates a DM channel between the client and the user
* @returns {Promise<DMChannel>}
*/
createDM() {
return this.client.rest.methods.createDM(this);
}
/**
* Deletes a DM channel (if one exists) between the client and the user. Resolves with the channel if successful.
* @returns {Promise<DMChannel>}

View File

@@ -42,6 +42,12 @@ class UserProfile {
*/
this.premium = data.premium;
/**
* The date since which the user has had Discord Premium
* @type {?Date}
*/
this.premiumSince = data.premium_since ? new Date(data.premium_since) : null;
for (const guild of data.mutual_guilds) {
if (this.client.guilds.has(guild.id)) {
this.mutualGuilds.set(guild.id, this.client.guilds.get(guild.id));

View File

@@ -20,6 +20,12 @@ class TextBasedChannel {
* @type {?Snowflake}
*/
this.lastMessageID = null;
/**
* The Message object of the last message in the channel, if one was sent.
* @type {?Message}
*/
this.lastMessage = null;
}
/**

View File

@@ -137,7 +137,8 @@ class Collection extends Map {
* Searches for a single item where its specified property's value is identical to the given value
* (`item[prop] === value`), or the given function returns a truthy value. In the latter case, this is identical to
* [Array.find()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/find).
* <warn>Do not use this to obtain an item by its ID. Instead, use `collection.get(id)`. See
* <warn>All collections used in Discord.js are mapped using their `id` property, and if you want to find by id you
* should use the `get` method. See
* [MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/get) for details.</warn>
* @param {string|Function} propOrFn The property to test against, or the function to test with
* @param {*} [value] The expected value - only applicable and required if using a property for the first argument
@@ -150,7 +151,6 @@ class Collection extends Map {
find(propOrFn, value) {
if (typeof propOrFn === 'string') {
if (typeof value === 'undefined') throw new Error('Value must be specified.');
if (propOrFn === 'id') throw new RangeError('Don\'t use .find() with IDs. Instead, use .get(id).');
for (const item of this.values()) {
if (item[propOrFn] === value) return item;
}

View File

@@ -51,7 +51,7 @@ exports.DefaultOptions = {
*/
ws: {
large_threshold: 250,
compress: typeof window === 'undefined',
compress: require('os').platform() !== 'browser',
properties: {
$os: process ? process.platform : 'discord.js',
$browser: 'discord.js',
@@ -125,6 +125,7 @@ const Endpoints = exports.Endpoints = {
guildMemberNickname: (guildID) => `${Endpoints.guildMember(guildID, '@me')}/nick`,
guildChannels: (guildID) => `${Endpoints.guild(guildID)}/channels`,
guildEmojis: (guildID) => `${Endpoints.guild(guildID)}/emojis`,
guildEmoji: (guildID, emojiID) => `${Endpoints.guildEmojis(guildID)}/${emojiID}`,
guildSearch: (guildID) => `${Endpoints.guild(guildID)}/messages/search`,
guildVoiceRegions: (guildID) => `${Endpoints.guild(guildID)}/regions`,