* 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

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