* 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
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
35 changed files with 295 additions and 383 deletions

View file

@ -12,6 +12,8 @@
path: voice.md
- name: Web builds
path: web.md
- name: Partials
path: partials.md
- name: Examples
files:
- name: Ping

61
docs/topics/partials.md Normal file
View file

@ -0,0 +1,61 @@
# Partials
Partials allow you to receive events that contain uncached instances, providing structures that contain very minimal
data. For example, if you were to receive a `messageDelete` event with an uncached message, normally Discord.js would
discard the event. With partials, you're able to receive the event, with a Message object that contains just an ID.
## Opting in
Partials are opt-in, and you can enable them in the Client options by specifying [PartialTypes](../typedef/PartialType):
```js
// Accept partial messages and DM channels when emitting events
new Client({ partials: ['MESSAGE', 'CHANNEL'] });
```
## Usage & warnings
<warn>The only guaranteed data a partial structure can store is its ID. All other properties/methods should be
considered invalid/defunct while accessing a partial structure.</warn>
After opting-in with the above, you begin to allow partial messages and channels in your caches, so it's important
to check whether they're safe to access whenever you encounter them, whether it be in events or through normal cache
usage.
All instance of structures that you opted-in for will have a `partial` property. As you'd expect, this value is `true`
when the instance is partial. Partial structures are only guaranteed to contain an ID, any other properties and methods
no longer carry their normal type guarantees.
This means you have to take time to consider possible parts of your program that might need checks put in place to
prevent accessing partial data:
```js
client.on('messageDelete', message => {
console.log(`${message.id} was deleted!`);
// Partial messages do not contain any content so skip them
if (!message.partial) {
console.log(`It had content: "${message.content}"`);
}
})
// You can also try to upgrade partials to full instances:
client.on('messageReactionAdd', async (reaction, user) => {
// If a message gains a reaction and it is uncached, fetch and cache the message
// You should account for any errors while fetching, it could return API errors if the resource is missing
if (reaction.message.partial) await reaction.message.fetch();
// Now the message has been cached and is fully available:
console.log(`${reaction.message.author}'s message "${reaction.message.content}" gained a reaction!`);
});
```
<info>If a message is deleted and both the message and channel are uncached, you must enable both 'MESSAGE' and
'CHANNEL' in the client options to receive the messageDelete event.</info>
## Why?
This allows developers to listen to events that contain uncached data, which is useful if you're running a moderation
bot or any bot that relies on still receiving updates to resources you don't have cached -- message reactions are a
good example.
Currently, the only type of channel that can be uncached is a DM channel, there is no reason why guild channels should
not be cached.

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

View file

@ -6,7 +6,7 @@ const ytdl = require('ytdl-core');
const prism = require('prism-media');
const fs = require('fs');
const client = new Discord.Client({ fetchAllMembers: false, apiRequestMethod: 'sequential' });
const client = new Discord.Client({ fetchAllMembers: false, partials: true, apiRequestMethod: 'sequential' });
const auth = require('./auth.js');
@ -34,6 +34,15 @@ client.on('presenceUpdate', (a, b) => {
console.log(a ? a.status : null, b.status, b.user.username);
});
client.on('messageDelete', async (m) => {
if (m.channel.id != '80426989059575808') return;
console.log(m.channel.recipient);
console.log(m.channel.partial);
await m.channel.fetch();
console.log('\n\n\n\n');
console.log(m.channel);
});
client.on('message', m => {
if (!m.guild) return;
if (m.author.id !== '66564597481480192') return;

57
typings/index.d.ts vendored
View file

@ -129,8 +129,9 @@ declare module 'discord.js' {
public readonly createdTimestamp: number;
public deleted: boolean;
public id: Snowflake;
public type: 'dm' | 'group' | 'text' | 'voice' | 'category' | 'unknown';
public type: 'dm' | 'text' | 'voice' | 'category' | 'unknown';
public delete(reason?: string): Promise<Channel>;
public fetch(): Promise<Channel>;
public toString(): string;
}
@ -264,7 +265,6 @@ declare module 'discord.js' {
export class ClientUser extends User {
public mfaEnabled: boolean;
public verified: boolean;
public createGroupDM(recipients: GroupDMRecipientOptions[]): Promise<GroupDMChannel>;
public setActivity(options?: ActivityOptions): Promise<Presence>;
public setActivity(name: string, options?: ActivityOptions): Promise<Presence>;
public setAFK(afk: boolean): Promise<Presence>;
@ -360,6 +360,7 @@ declare module 'discord.js' {
constructor(client: Client, data?: object);
public messages: MessageStore;
public recipient: User;
public readonly partial: boolean;
}
export class Emoji extends Base {
@ -376,26 +377,6 @@ declare module 'discord.js' {
public toString(): string;
}
export class GroupDMChannel extends TextBasedChannel(Channel) {
constructor(client: Client, data?: object);
public applicationID: Snowflake;
public icon: string;
public managed: boolean;
public messages: MessageStore;
public name: string;
public nicks: Collection<Snowflake, string>;
public readonly owner: User;
public ownerID: Snowflake;
public recipients: Collection<Snowflake, User>;
public addUser(options: { user: UserResolvable, accessToken?: string, nick?: string }): Promise<GroupDMChannel>;
public edit (data: { icon?: string, name?: string }): Promise<GroupDMChannel>;
public equals(channel: GroupDMChannel): boolean;
public iconURL(options?: AvatarOptions): string;
public removeUser(user: UserResolvable): Promise<GroupDMChannel>;
public setIcon(icon: Base64Resolvable | BufferResolvable): Promise<GroupDMChannel>;
public setName(name: string): Promise<GroupDMChannel>;
}
export class Guild extends Base {
constructor(client: Client, data: object);
private _sortedRoles(): Collection<Snowflake, Role>;
@ -570,12 +551,14 @@ declare module 'discord.js' {
public readonly kickable: boolean;
public readonly manageable: boolean;
public nickname: string;
public readonly partial: boolean;
public readonly permissions: Readonly<Permissions>;
public readonly presence: Presence;
public roles: GuildMemberRoleStore;
public user: User;
public readonly voice: VoiceState;
public ban(options?: BanOptions): Promise<GuildMember>;
public fetch(): Promise<GuildMember>;
public createDM(): Promise<DMChannel>;
public deleteDM(): Promise<DMChannel>;
public edit(data: GuildMemberEditData, reason?: string): Promise<GuildMember>;
@ -619,7 +602,7 @@ declare module 'discord.js' {
export class Invite extends Base {
constructor(client: Client, data: object);
public channel: GuildChannel | GroupDMChannel;
public channel: GuildChannel;
public code: string;
public readonly createdAt: Date;
public createdTimestamp: number;
@ -640,7 +623,7 @@ declare module 'discord.js' {
}
export class Message extends Base {
constructor(client: Client, data: object, channel: TextChannel | DMChannel | GroupDMChannel);
constructor(client: Client, data: object, channel: TextChannel | DMChannel);
private _edits: Message[];
private patch(data: object): void;
@ -648,7 +631,7 @@ declare module 'discord.js' {
public application: ClientApplication;
public attachments: Collection<Snowflake, MessageAttachment>;
public author: User;
public channel: TextChannel | DMChannel | GroupDMChannel;
public channel: TextChannel | DMChannel;
public readonly cleanContent: string;
public content: string;
public readonly createdAt: Date;
@ -665,6 +648,7 @@ declare module 'discord.js' {
public readonly member: GuildMember;
public mentions: MessageMentions;
public nonce: string;
public readonly partial: boolean;
public readonly pinnable: boolean;
public pinned: boolean;
public reactions: ReactionStore;
@ -680,6 +664,7 @@ declare module 'discord.js' {
public edit(options: MessageEditOptions | MessageEmbed | APIMessage): Promise<Message>;
public equals(message: Message, rawData: object): boolean;
public fetchWebhook(): Promise<Webhook>;
public fetch(): Promise<Message>;
public pin(): Promise<Message>;
public react(emoji: EmojiIdentifierResolvable): Promise<MessageReaction>;
public reply(content?: StringResolvable, options?: MessageOptions | MessageAdditions): Promise<Message | Message[]>;
@ -706,7 +691,7 @@ declare module 'discord.js' {
}
export class MessageCollector extends Collector<Snowflake, Message> {
constructor(channel: TextChannel | DMChannel | GroupDMChannel, filter: CollectorFilter, options?: MessageCollectorOptions);
constructor(channel: TextChannel | DMChannel, filter: CollectorFilter, options?: MessageCollectorOptions);
public channel: Channel;
public options: MessageCollectorOptions;
public received: number;
@ -1078,6 +1063,7 @@ declare module 'discord.js' {
public readonly dmChannel: DMChannel;
public id: Snowflake;
public locale: string;
public readonly partial: boolean;
public readonly presence: Presence;
public readonly tag: string;
public username: string;
@ -1086,6 +1072,7 @@ declare module 'discord.js' {
public deleteDM(): Promise<DMChannel>;
public displayAvatarURL(options?: AvatarOptions): string;
public equals(user: User): boolean;
public fetch(): Promise<User>;
public toString(): string;
public typingDurationIn(channel: ChannelResolvable): number;
public typingIn(channel: ChannelResolvable): boolean;
@ -1385,7 +1372,7 @@ declare module 'discord.js' {
}
export class MessageStore extends DataStore<Snowflake, Message, typeof Message, MessageResolvable> {
constructor(channel: TextChannel | DMChannel | GroupDMChannel, iterable?: Iterable<any>);
constructor(channel: TextChannel | DMChannel, iterable?: Iterable<any>);
public fetch(message: Snowflake): Promise<Message>;
public fetch(options?: ChannelLogsQueryOptions): Promise<Collection<Snowflake, Message>>;
public fetchPinned(): Promise<Collection<Snowflake, Message>>;
@ -1607,6 +1594,7 @@ declare module 'discord.js' {
messageSweepInterval?: number;
fetchAllMembers?: boolean;
disableEveryone?: boolean;
partials?: PartialTypes[];
restWsBridgeTimeout?: number;
restTimeOffset?: number;
restSweepInterval?: number;
@ -1676,7 +1664,6 @@ declare module 'discord.js' {
type Extendable = {
GuildEmoji: typeof GuildEmoji;
DMChannel: typeof DMChannel;
GroupDMChannel: typeof GroupDMChannel;
TextChannel: typeof TextChannel;
VoiceChannel: typeof VoiceChannel;
CategoryChannel: typeof CategoryChannel;
@ -1711,13 +1698,6 @@ declare module 'discord.js' {
type: number;
};
type GroupDMRecipientOptions = {
user?: UserResolvable | Snowflake;
accessToken?: string;
nick?: string;
id?: Snowflake;
};
type GuildAuditLogsAction = keyof GuildAuditLogsActions;
type GuildAuditLogsActions = {
@ -1934,7 +1914,7 @@ declare module 'discord.js' {
type MessageResolvable = Message | Snowflake;
type MessageTarget = TextChannel | DMChannel | GroupDMChannel | User | GuildMember | Webhook | WebhookClient;
type MessageTarget = TextChannel | DMChannel | User | GuildMember | Webhook | WebhookClient;
type MessageType = 'DEFAULT'
| 'RECIPIENT_ADD'
@ -2023,6 +2003,11 @@ declare module 'discord.js' {
desktop?: ClientPresenceStatus
};
type PartialTypes = 'USER'
| 'CHANNEL'
| 'GUILD_MEMBER'
| 'MESSAGE';
type PresenceStatus = ClientPresenceStatus | 'offline';
type PresenceStatusData = ClientPresenceStatus | 'invisible';