* Remove GroupDMChannels

they sparked no joy

* Start partials for message deletion

* MessageUpdate partials

* Add partials as an opt-in client option

* Add fetch() to Message

* Message.author should never be undefined

* Fix channels being the wrong type

* Allow fetching channels

* Refactor and add reaction add partials

* Reaction remove partials

* Check for emoji first

* fix message fetching

janky

* User partials in audit logs

* refactor overwrite code

* guild member partials

* partials as a whitelist

* document GuildMember#fetch

* fix: check whether a structure is a partial, not whether cache is true

* typings: Updated for latest commit (#3075)

* partials: fix messageUpdate behaviour (now "old" message can be partial)

* partials: add warnings and docs

* partials: add partials to index.yml

* partials: tighten "partial" definitions

* partials: fix embed-only messages counting as partials
This commit is contained in:
Amish Shah
2019-02-13 17:39:39 +00:00
committed by GitHub
parent 8910fed729
commit 5c3f5d7048
35 changed files with 295 additions and 383 deletions

View File

@@ -434,6 +434,9 @@ class Client extends BaseClient {
if (typeof options.disableEveryone !== 'boolean') {
throw new TypeError('CLIENT_INVALID_OPTION', 'disableEveryone', 'a boolean');
}
if (!(options.partials instanceof Array)) {
throw new TypeError('CLIENT_INVALID_OPTION', 'partials', 'an Array');
}
if (typeof options.restWsBridgeTimeout !== 'number' || isNaN(options.restWsBridgeTimeout)) {
throw new TypeError('CLIENT_INVALID_OPTION', 'restWsBridgeTimeout', 'a number');
}

View File

@@ -1,5 +1,7 @@
'use strict';
const { PartialTypes } = require('../../util/Constants');
/*
ABOUT ACTIONS
@@ -20,6 +22,27 @@ class GenericAction {
handle(data) {
return data;
}
getChannel(data) {
const id = data.channel_id || data.id;
return data.channel || (this.client.options.partials.includes(PartialTypes.CHANNEL) ?
this.client.channels.add({
id,
guild_id: data.guild_id,
}) :
this.client.channels.get(id));
}
getMessage(data, channel) {
const id = data.message_id || data.id;
return data.message || (this.client.options.partials.includes(PartialTypes.MESSAGE) ?
channel.messages.add({
id,
channel_id: channel.id,
guild_id: data.guild_id || (channel.guild ? channel.guild.id : null),
}) :
channel.messages.get(id));
}
}
module.exports = GenericAction;

View File

@@ -12,7 +12,7 @@ class ChannelCreateAction extends Action {
/**
* Emitted whenever a channel is created.
* @event Client#channelCreate
* @param {DMChannel|GroupDMChannel|GuildChannel} channel The channel that was created
* @param {DMChannel|GuildChannel} channel The channel that was created
*/
client.emit(Events.CHANNEL_CREATE, channel);
}

View File

@@ -19,7 +19,7 @@ class ChannelDeleteAction extends Action {
/**
* Emitted whenever a channel is deleted.
* @event Client#channelDelete
* @param {DMChannel|GroupDMChannel|GuildChannel} channel The channel that was deleted
* @param {DMChannel|GuildChannel} channel The channel that was deleted
*/
client.emit(Events.CHANNEL_DELETE, channel);
}

View File

@@ -6,11 +6,10 @@ const { Events } = require('../../util/Constants');
class MessageDeleteAction extends Action {
handle(data) {
const client = this.client;
const channel = client.channels.get(data.channel_id);
const channel = this.getChannel(data);
let message;
if (channel) {
message = channel.messages.get(data.id);
message = this.getMessage(data, channel);
if (message) {
channel.messages.delete(message.id);
message.deleted = true;

View File

@@ -11,15 +11,19 @@ const Action = require('./Action');
class MessageReactionAdd extends Action {
handle(data) {
if (!data.emoji) return false;
const user = data.user || this.client.users.get(data.user_id);
if (!user) return false;
// Verify channel
const channel = data.channel || this.client.channels.get(data.channel_id);
const channel = this.getChannel(data);
if (!channel || channel.type === 'voice') return false;
// Verify message
const message = data.message || channel.messages.get(data.message_id);
const message = this.getMessage(data, channel);
if (!message) return false;
if (!data.emoji) return false;
// Verify reaction
const reaction = message.reactions.add({
emoji: data.emoji,

View File

@@ -12,15 +12,19 @@ const { Events } = require('../../util/Constants');
class MessageReactionRemove extends Action {
handle(data) {
if (!data.emoji) return false;
const user = this.client.users.get(data.user_id);
if (!user) return false;
// Verify channel
const channel = this.client.channels.get(data.channel_id);
const channel = this.getChannel(data);
if (!channel || channel.type === 'voice') return false;
// Verify message
const message = channel.messages.get(data.message_id);
const message = this.getMessage(data, channel);
if (!message) return false;
if (!data.emoji) return false;
// Verify reaction
const emojiID = data.emoji.id || decodeURIComponent(data.emoji.name);
const reaction = message.reactions.get(emojiID);

View File

@@ -5,10 +5,12 @@ const { Events } = require('../../util/Constants');
class MessageReactionRemoveAll extends Action {
handle(data) {
const channel = this.client.channels.get(data.channel_id);
// Verify channel
const channel = this.getChannel(data);
if (!channel || channel.type === 'voice') return false;
const message = channel.messages.get(data.message_id);
// Verify message
const message = this.getMessage(data, channel);
if (!message) return false;
message.reactions.clear();

View File

@@ -4,11 +4,10 @@ const Action = require('./Action');
class MessageUpdateAction extends Action {
handle(data) {
const client = this.client;
const channel = client.channels.get(data.channel_id);
const channel = this.getChannel(data);
if (channel) {
const message = channel.messages.get(data.id);
const { id, channel_id, guild_id, author, timestamp, type } = data;
const message = this.getMessage({ id, channel_id, guild_id, author, timestamp, type }, channel);
if (message) {
message.patch(data);
return {

View File

@@ -14,7 +14,7 @@ module.exports = (client, { d: data }) => {
* 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.
* @event Client#channelPinsUpdate
* @param {DMChannel|GroupDMChannel|TextChannel} channel The channel that the pins update occured in
* @param {DMChannel|TextChannel} channel The channel that the pins update occured in
* @param {Date} time The time of the pins update
*/
client.emit(Events.CHANNEL_PINS_UPDATE, channel, time);

View File

@@ -8,8 +8,8 @@ module.exports = (client, packet) => {
/**
* Emitted whenever a channel is updated - e.g. name change, topic change.
* @event Client#channelUpdate
* @param {DMChannel|GroupDMChannel|GuildChannel} oldChannel The channel before the update
* @param {DMChannel|GroupDMChannel|GuildChannel} newChannel The channel after the update
* @param {DMChannel|GuildChannel} oldChannel The channel before the update
* @param {DMChannel|GuildChannel} newChannel The channel after the update
*/
client.emit(Events.CHANNEL_UPDATE, old, updated);
}

View File

@@ -65,7 +65,6 @@ module.exports = {
Collector: require('./structures/interfaces/Collector'),
DMChannel: require('./structures/DMChannel'),
Emoji: require('./structures/Emoji'),
GroupDMChannel: require('./structures/GroupDMChannel'),
Guild: require('./structures/Guild'),
GuildAuditLogs: require('./structures/GuildAuditLogs'),
GuildChannel: require('./structures/GuildChannel'),

View File

@@ -5,7 +5,7 @@ const Channel = require('../structures/Channel');
const { Events } = require('../util/Constants');
const kLru = Symbol('LRU');
const lruable = ['group', 'dm'];
const lruable = ['dm'];
/**
* Stores channels.
@@ -54,6 +54,7 @@ class ChannelStore extends DataStore {
add(data, guild, cache = true) {
const existing = this.get(data.id);
if (existing && existing.partial && cache) existing._patch(data);
if (existing) return existing;
const channel = Channel.create(this.client, data, guild);
@@ -85,11 +86,12 @@ class ChannelStore extends DataStore {
* .then(channel => console.log(channel.name))
* .catch(console.error);
*/
fetch(id, cache = true) {
async fetch(id, cache = true) {
const existing = this.get(id);
if (existing) return Promise.resolve(existing);
if (existing && !existing.partial) return existing;
return this.client.api.channels(id).get().then(data => this.add(data, null, cache));
const data = await this.client.api.channels(id).get();
return this.add(data, null, cache);
}
/**

View File

@@ -18,6 +18,7 @@ class DataStore extends Collection {
add(data, cache = true, { id, extras = [] } = {}) {
const existing = this.get(id || data.id);
if (existing && existing.partial && cache && existing._patch) existing._patch(data);
if (existing) return existing;
const entry = this.holds ? new this.holds(this.client, data, ...extras) : data;

View File

@@ -180,7 +180,7 @@ class GuildMemberStore extends DataStore {
_fetchSingle({ user, cache }) {
const existing = this.get(user);
if (existing && existing.joinedTimestamp) return Promise.resolve(existing);
if (existing && !existing.partial) return Promise.resolve(existing);
return this.client.api.guilds(this.guild.id).members(user).get()
.then(data => this.add(data, cache));
}

View File

@@ -40,6 +40,7 @@ class MessageStore extends DataStore {
* <info>The returned Collection does not contain reaction users of the messages if they were not cached.
* Those need to be fetched separately in such a case.</info>
* @param {Snowflake|ChannelLogsQueryOptions} [message] The ID of the message to fetch, or query parameters.
* @param {boolean} [cache=true] Whether to cache the message(s)
* @returns {Promise<Message>|Promise<Collection<Snowflake, Message>>}
* @example
* // Get message
@@ -57,8 +58,8 @@ class MessageStore extends DataStore {
* .then(messages => console.log(`${messages.filter(m => m.author.id === '84484653687267328').size} messages`))
* .catch(console.error);
*/
fetch(message) {
return typeof message === 'string' ? this._fetchId(message) : this._fetchMany(message);
fetch(message, cache = true) {
return typeof message === 'string' ? this._fetchId(message, cache) : this._fetchMany(message, cache);
}
/**
@@ -80,15 +81,17 @@ class MessageStore extends DataStore {
});
}
async _fetchId(messageID) {
async _fetchId(messageID, cache) {
const existing = this.get(messageID);
if (existing && !existing.partial) return existing;
const data = await this.client.api.channels[this.channel.id].messages[messageID].get();
return this.add(data);
return this.add(data, cache);
}
async _fetchMany(options = {}) {
async _fetchMany(options = {}, cache) {
const data = await this.client.api.channels[this.channel.id].messages.get({ query: options });
const messages = new Collection();
for (const message of data) messages.set(message.id, this.add(message));
for (const message of data) messages.set(message.id, this.add(message, cache));
return messages;
}

View File

@@ -51,11 +51,11 @@ class UserStore extends DataStore {
* @param {boolean} [cache=true] Whether to cache the new user object if it isn't already
* @returns {Promise<User>}
*/
fetch(id, cache = true) {
async fetch(id, cache = true) {
const existing = this.get(id);
if (existing) return Promise.resolve(existing);
return this.client.api.users(id).get().then(data => this.add(data, cache));
if (existing && !existing.partial) return existing;
const data = await this.client.api.users(id).get();
return this.add(data, cache);
}
}

View File

@@ -330,7 +330,7 @@ module.exports = APIMessage;
/**
* A target for a message.
* @typedef {TextChannel|DMChannel|GroupDMChannel|User|GuildMember|Webhook|WebhookClient} MessageTarget
* @typedef {TextChannel|DMChannel|User|GuildMember|Webhook|WebhookClient} MessageTarget
*/
/**

View File

@@ -16,7 +16,6 @@ class Channel extends Base {
/**
* The type of the channel, either:
* * `dm` - a DM channel
* * `group` - a Group DM channel
* * `text` - a guild text channel
* * `voice` - a guild voice channel
* * `category` - a guild category channel
@@ -84,15 +83,20 @@ class Channel extends Base {
return this.client.api.channels(this.id).delete().then(() => this);
}
/**
* Fetches this channel.
* @returns {Promise<Channel>}
*/
fetch() {
return this.client.channels.fetch(this.id, true);
}
static create(client, data, guild) {
const Structures = require('../util/Structures');
let channel;
if (data.type === ChannelTypes.DM) {
if (data.type === ChannelTypes.DM || (data.type !== ChannelTypes.GROUP && !data.guild_id && !guild)) {
const DMChannel = Structures.get('DMChannel');
channel = new DMChannel(client, data);
} else if (data.type === ChannelTypes.GROUP) {
const GroupDMChannel = Structures.get('GroupDMChannel');
channel = new GroupDMChannel(client, data);
} else {
guild = guild || client.guilds.get(data.guild_id);
if (guild) {

View File

@@ -164,42 +164,6 @@ class ClientUser extends Structures.get('User') {
setAFK(afk) {
return this.setPresence({ afk });
}
/**
* An object containing either a user or access token, and an optional nickname.
* @typedef {Object} GroupDMRecipientOptions
* @property {UserResolvable} [user] User to add to the Group DM
* @property {string} [accessToken] Access token to use to add a user to the Group DM
* (only available if a bot is creating the DM)
* @property {string} [nick] Permanent nickname (only available if a bot is creating the DM)
* @property {string} [id] If no user resolvable is provided and you want to assign nicknames
* you must provide user ids instead
*/
/**
* Creates a Group DM.
* @param {GroupDMRecipientOptions[]} recipients The recipients
* @returns {Promise<GroupDMChannel>}
* @example
* // Create a Group DM with a token provided from OAuth
* client.user.createGroupDM([{
* user: '66564597481480192',
* accessToken: token
* }])
* .then(console.log)
* .catch(console.error);
*/
createGroupDM(recipients) {
const data = this.bot ? {
access_tokens: recipients.map(u => u.accessToken),
nicks: recipients.reduce((o, r) => {
if (r.nick) o[r.user ? r.user.id : r.id] = r.nick;
return o;
}, {}),
} : { recipients: recipients.map(u => this.client.users.resolveID(u.user || u.id)) };
return this.client.api.users('@me').channels.post({ data })
.then(res => this.client.channels.add(res));
}
}
module.exports = ClientUser;

View File

@@ -12,6 +12,8 @@ const MessageStore = require('../stores/MessageStore');
class DMChannel extends Channel {
constructor(client, data) {
super(client, data);
// Override the channel type so partials have a known type
this.type = 'dm';
/**
* A collection containing the messages sent to this channel
* @type {MessageStore<Snowflake, Message>}
@@ -23,11 +25,13 @@ class DMChannel extends Channel {
_patch(data) {
super._patch(data);
/**
* The recipient on the other end of the DM
* @type {User}
*/
this.recipient = this.client.users.add(data.recipients[0]);
if (data.recipients) {
/**
* The recipient on the other end of the DM
* @type {User}
*/
this.recipient = this.client.users.add(data.recipients[0]);
}
/**
* The ID of the last message in the channel, if one was sent
@@ -42,6 +46,14 @@ class DMChannel extends Channel {
this.lastPinTimestamp = data.last_pin_timestamp ? new Date(data.last_pin_timestamp).getTime() : null;
}
/**
* Whether this DMChannel is a partial
* @type {boolean}
*/
get partial() {
return !this.recipient;
}
/**
* When concatenated with a string, this automatically returns the recipient's mention instead of the
* DMChannel object.

View File

@@ -1,245 +0,0 @@
'use strict';
const Channel = require('./Channel');
const TextBasedChannel = require('./interfaces/TextBasedChannel');
const Collection = require('../util/Collection');
const DataResolver = require('../util/DataResolver');
const MessageStore = require('../stores/MessageStore');
/*
{ type: 3,
recipients:
[ { username: 'Charlie',
id: '123',
discriminator: '6631',
avatar: '123' },
{ username: 'Ben',
id: '123',
discriminator: '2055',
avatar: '123' },
{ username: 'Adam',
id: '123',
discriminator: '2406',
avatar: '123' } ],
owner_id: '123',
name: null,
last_message_id: '123',
id: '123',
icon: null }
*/
/**
* Represents a Group DM on Discord.
* @extends {Channel}
* @implements {TextBasedChannel}
*/
class GroupDMChannel extends Channel {
constructor(client, data) {
super(client, data);
/**
* A collection containing the messages sent to this channel
* @type {MessageStore<Snowflake, Message>}
*/
this.messages = new MessageStore(this);
this._typing = new Map();
}
_patch(data) {
super._patch(data);
/**
* The name of this Group DM, can be null if one isn't set
* @type {string}
*/
this.name = data.name;
/**
* A hash of this Group DM icon
* @type {?string}
*/
this.icon = data.icon;
/**
* The user ID of this Group DM's owner
* @type {Snowflake}
*/
this.ownerID = data.owner_id;
/**
* If the DM is managed by an application
* @type {boolean}
*/
this.managed = data.managed;
/**
* Application ID of the application that made this Group DM, if applicable
* @type {?Snowflake}
*/
this.applicationID = data.application_id;
if (data.nicks) {
/**
* Nicknames for group members
* @type {?Collection<Snowflake, string>}
*/
this.nicks = new Collection(data.nicks.map(n => [n.id, n.nick]));
}
if (!this.recipients) {
/**
* A collection of the recipients of this DM, mapped by their ID
* @type {Collection<Snowflake, User>}
*/
this.recipients = new Collection();
}
if (data.recipients) {
for (const recipient of data.recipients) {
const user = this.client.users.add(recipient);
this.recipients.set(user.id, user);
}
}
/**
* The ID of the last message in the channel, if one was sent
* @type {?Snowflake}
*/
this.lastMessageID = data.last_message_id;
/**
* The timestamp when the last pinned message was pinned, if there was one
* @type {?number}
*/
this.lastPinTimestamp = data.last_pin_timestamp ? new Date(data.last_pin_timestamp).getTime() : null;
}
/**
* The owner of this Group DM
* @type {?User}
* @readonly
*/
get owner() {
return this.client.users.get(this.ownerID) || null;
}
/**
* Gets the URL to this Group DM's icon.
* @param {ImageURLOptions} [options={}] Options for the Image URL
* @returns {?string}
*/
iconURL({ format, size } = {}) {
if (!this.icon) return null;
return this.client.rest.cdn.GDMIcon(this.id, this.icon, format, size);
}
/**
* Whether this channel equals another channel. It compares all properties, so for most operations
* it is advisable to just compare `channel.id === channel2.id` as it is much faster and is often
* what most users need.
* @param {GroupDMChannel} channel Channel to compare with
* @returns {boolean}
*/
equals(channel) {
const equal = channel &&
this.id === channel.id &&
this.name === channel.name &&
this.icon === channel.icon &&
this.ownerID === channel.ownerID;
if (equal) {
return this.recipients.equals(channel.recipients);
}
return equal;
}
/**
* Edits this Group DM.
* @param {Object} data New data for this Group DM
* @param {string} [reason] Reason for editing this Group DM
* @returns {Promise<GroupDMChannel>}
*/
edit(data, reason) {
return this.client.api.channels[this.id].patch({
data: {
icon: data.icon,
name: data.name === null ? null : data.name || this.name,
},
reason,
}).then(() => this);
}
/**
* Sets a new icon for this Group DM.
* @param {Base64Resolvable|BufferResolvable} icon The new icon of this Group DM
* @returns {Promise<GroupDMChannel>}
*/
async setIcon(icon) {
return this.edit({ icon: await DataResolver.resolveImage(icon) });
}
/**
* Sets a new name for this Group DM.
* @param {string} name New name for this Group DM
* @returns {Promise<GroupDMChannel>}
*/
setName(name) {
return this.edit({ name });
}
/**
* Adds a user to this Group DM.
* @param {Object} options Options for this method
* @param {UserResolvable} options.user User to add to this Group DM
* @param {string} [options.accessToken] Access token to use to add the user to this Group DM
* @param {string} [options.nick] Permanent nickname to give the user
* @returns {Promise<GroupDMChannel>}
*/
addUser({ user, accessToken, nick }) {
const id = this.client.users.resolveID(user);
return this.client.api.channels[this.id].recipients[id].put({ nick, access_token: accessToken })
.then(() => this);
}
/**
* Removes a user from this Group DM.
* @param {UserResolvable} user User to remove
* @returns {Promise<GroupDMChannel>}
*/
removeUser(user) {
const id = this.client.users.resolveID(user);
return this.client.api.channels[this.id].recipients[id].delete()
.then(() => this);
}
/**
* When concatenated with a string, this automatically returns the channel's name instead of the
* GroupDMChannel object.
* @returns {string}
* @example
* // Logs: Hello from My Group DM!
* console.log(`Hello from ${channel}!`);
*/
toString() {
return this.name;
}
// These are here only for documentation purposes - they are implemented by TextBasedChannel
/* eslint-disable no-empty-function */
get lastMessage() {}
get lastPinAt() {}
send() {}
startTyping() {}
stopTyping() {}
get typing() {}
get typingCount() {}
createMessageCollector() {}
awaitMessages() {}
// Doesn't work on Group DMs; bulkDelete() {}
acknowledge() {}
_cacheMessage() {}
}
TextBasedChannel.applyToClass(GroupDMChannel, true, ['bulkDelete']);
module.exports = GroupDMChannel;

View File

@@ -5,7 +5,7 @@ const Integration = require('./Integration');
const GuildAuditLogs = require('./GuildAuditLogs');
const Webhook = require('./Webhook');
const VoiceRegion = require('./VoiceRegion');
const { ChannelTypes, DefaultMessageNotifications, browser } = require('../util/Constants');
const { ChannelTypes, DefaultMessageNotifications, PartialTypes, browser } = require('../util/Constants');
const Collection = require('../util/Collection');
const Util = require('../util/Util');
const DataResolver = require('../util/DataResolver');
@@ -341,7 +341,9 @@ class Guild extends Base {
* @readonly
*/
get owner() {
return this.members.get(this.ownerID) || null;
return this.members.get(this.ownerID) || (this.client.options.partials.includes(PartialTypes.GUILD_MEMBER) ?
this.members.add({ user: { id: this.ownerID } }, true) :
null);
}
/**

View File

@@ -4,6 +4,7 @@ const Collection = require('../util/Collection');
const Snowflake = require('../util/Snowflake');
const Webhook = require('./Webhook');
const Util = require('../util/Util');
const PartialTypes = require('../util/Constants');
/**
* The target type of an entry, e.g. `GUILD`. Here are the available types:
@@ -234,7 +235,7 @@ class GuildAuditLogs {
* Audit logs entry.
*/
class GuildAuditLogsEntry {
constructor(logs, guild, data) {
constructor(logs, guild, data) { // eslint-disable-line complexity
const targetType = GuildAuditLogs.targetType(data.action_type);
/**
* The target type of this entry
@@ -264,7 +265,9 @@ class GuildAuditLogsEntry {
* The user that executed this entry
* @type {User}
*/
this.executor = guild.client.users.get(data.user_id);
this.executor = guild.client.options.partials.includes(PartialTypes.USER) ?
guild.client.users.add({ id: data.user_id }) :
guild.client.users.get(data.user_id);
/**
* An entry in the audit log representing a specific change.
@@ -329,8 +332,12 @@ class GuildAuditLogsEntry {
return o;
}, {});
this.target.id = data.target_id;
} else if ([Targets.USER, Targets.GUILD].includes(targetType)) {
this.target = guild.client[`${targetType.toLowerCase()}s`].get(data.target_id);
} else if (targetType === Targets.USER) {
this.target = guild.client.options.partials.includes(PartialTypes.USER) ?
guild.client.users.add({ id: data.target_id }) :
guild.client.users.get(data.target_id);
} else if (targetType === Targets.GUILD) {
this.target = guild.client.guilds.get(data.target_id);
} else if (targetType === Targets.WEBHOOK) {
this.target = logs.webhooks.get(data.target_id) ||
new Webhook(guild.client,

View File

@@ -28,7 +28,7 @@ class GuildMember extends Base {
* The user that this guild member instance represents
* @type {User}
*/
this.user = {};
if (data.user) this.user = client.users.add(data.user, true);
/**
* The timestamp the member joined the guild at
@@ -79,6 +79,14 @@ class GuildMember extends Base {
return clone;
}
/**
* Whether this GuildMember is a partial
* @type {boolean}
*/
get partial() {
return !this.joinedTimestamp;
}
/**
* A collection of roles that are applied to this member, mapped by the role ID
* @type {GuildMemberRoleStore<Snowflake, Role>}
@@ -355,6 +363,14 @@ class GuildMember extends Base {
return this.guild.members.ban(this, options);
}
/**
* Fetches this GuildMember.
* @returns {Promise<GuildMember>}
*/
fetch() {
return this.guild.members.fetch(this.id, true);
}
/**
* When concatenated with a string, this automatically returns the user's mention instead of the GuildMember object.
* @returns {string}

View File

@@ -24,7 +24,7 @@ class Message extends Base {
/**
* The channel that the message was sent in
* @type {TextChannel|DMChannel|GroupDMChannel}
* @type {TextChannel|DMChannel}
*/
this.channel = channel;
@@ -60,7 +60,7 @@ class Message extends Base {
* The author of the message
* @type {User}
*/
this.author = this.client.users.add(data.author, !data.webhook_id);
this.author = data.author ? this.client.users.add(data.author, !data.webhook_id) : null;
/**
* Whether or not this message is pinned
@@ -90,17 +90,19 @@ class Message extends Base {
* A list of embeds in the message - e.g. YouTube Player
* @type {MessageEmbed[]}
*/
this.embeds = data.embeds.map(e => new Embed(e));
this.embeds = (data.embeds || []).map(e => new Embed(e));
/**
* A collection of attachments in the message - e.g. Pictures - mapped by their ID
* @type {Collection<Snowflake, MessageAttachment>}
*/
this.attachments = new Collection();
for (const attachment of data.attachments) {
this.attachments.set(attachment.id, new MessageAttachment(
attachment.url, attachment.filename, attachment
));
if (data.attachments) {
for (const attachment of data.attachments) {
this.attachments.set(attachment.id, new MessageAttachment(
attachment.url, attachment.filename, attachment
));
}
}
/**
@@ -167,6 +169,14 @@ class Message extends Base {
}
}
/**
* Whether or not this message is a partial
* @type {boolean}
*/
get partial() {
return typeof this.content !== 'string' || !this.author;
}
/**
* Updates the message.
* @param {Object} data Raw Discord message update data
@@ -472,6 +482,14 @@ class Message extends Base {
);
}
/**
* Fetch this message.
* @returns {Promise<Message>}
*/
fetch() {
return this.channel.messages.fetch(this.id, true);
}
/**
* Fetches the webhook used to create this message.
* @returns {Promise<?Webhook>}

View File

@@ -15,7 +15,7 @@ const { Events } = require('../util/Constants');
*/
class MessageCollector extends Collector {
/**
* @param {TextChannel|DMChannel|GroupDMChannel} channel The channel
* @param {TextChannel|DMChannel} channel The channel
* @param {CollectorFilter} filter The filter to be applied to this collector
* @param {MessageCollectorOptions} options The options to be applied to this collector
* @emits MessageCollector#message

View File

@@ -53,6 +53,8 @@ class User extends Base {
*/
if (typeof data.avatar !== 'undefined') this.avatar = data.avatar;
if (typeof data.bot !== 'undefined') this.bot = Boolean(data.bot);
/**
* The locale of the user's client (ISO 639-1)
* @type {?string}
@@ -73,6 +75,14 @@ class User extends Base {
this.lastMessageChannelID = null;
}
/**
* Whether this User is a partial
* @type {boolean}
*/
get partial() {
return typeof this.username !== 'string';
}
/**
* The timestamp the user was created at
* @type {number}
@@ -228,6 +238,14 @@ class User extends Base {
return equal;
}
/**
* Fetches this user.
* @returns {Promise<User>}
*/
fetch() {
return this.client.users.fetch(this.id, true);
}
/**
* When concatenated with a string, this automatically returns the user's mention instead of the User object.
* @returns {string}

View File

@@ -21,6 +21,9 @@ const browser = exports.browser = typeof window !== 'undefined';
* @property {boolean} [fetchAllMembers=false] Whether to cache all guild members and users upon startup, as well as
* upon joining a guild (should be avoided whenever possible)
* @property {boolean} [disableEveryone=false] Default value for {@link MessageOptions#disableEveryone}
* @property {PartialType[]} [partials] Structures allowed to be partial. This means events can be emitted even when
* they're missing all the data for a particular structure. See the "Partials" topic listed in the sidebar for some
* important usage information, as partials require you to put checks in place when handling data.
* @property {number} [restWsBridgeTimeout=5000] Maximum time permitted between REST responses and their
* corresponding websocket events
* @property {number} [restTimeOffset=500] Extra time in millseconds to wait before continuing to make REST
@@ -44,6 +47,7 @@ exports.DefaultOptions = {
messageSweepInterval: 0,
fetchAllMembers: false,
disableEveryone: false,
partials: [],
restWsBridgeTimeout: 5000,
disabledEvents: [],
retryLimit: 1,
@@ -261,6 +265,23 @@ exports.Events = {
RAW: 'raw',
};
/**
* The type of Structure allowed to be a partial:
* * USER
* * CHANNEL (only affects DMChannels)
* * GUILD_MEMBER
* * MESSAGE
* <warn>Partials require you to put checks in place when handling data, read the Partials topic listed in the
* sidebar for more information.</warn>
* @typedef {string} PartialType
*/
exports.PartialTypes = keyMirror([
'USER',
'CHANNEL',
'GUILD_MEMBER',
'MESSAGE',
]);
/**
* The type of a websocket message event, e.g. `MESSAGE_CREATE`. Here are the available events:
* * READY

View File

@@ -67,7 +67,6 @@ class Structures {
const structures = {
GuildEmoji: require('../structures/GuildEmoji'),
DMChannel: require('../structures/DMChannel'),
GroupDMChannel: require('../structures/GroupDMChannel'),
TextChannel: require('../structures/TextChannel'),
VoiceChannel: require('../structures/VoiceChannel'),
CategoryChannel: require('../structures/CategoryChannel'),

View File

@@ -395,7 +395,7 @@ class Util {
.replace(/@(everyone|here)/g, '@\u200b$1')
.replace(/<@!?[0-9]+>/g, input => {
const id = input.replace(/<|!|>|@/g, '');
if (message.channel.type === 'dm' || message.channel.type === 'group') {
if (message.channel.type === 'dm') {
const user = message.client.users.get(id);
return user ? `@${user.username}` : input;
}
@@ -413,7 +413,7 @@ class Util {
return channel ? `#${channel.name}` : input;
})
.replace(/<@&[0-9]+>/g, input => {
if (message.channel.type === 'dm' || message.channel.type === 'group') return input;
if (message.channel.type === 'dm') return input;
const role = message.guild.roles.get(input.replace(/<|@|>|&/g, ''));
return role ? `@${role.name}` : input;
});