diff --git a/docs/index.yml b/docs/index.yml
index 7c5b3b280..175780574 100644
--- a/docs/index.yml
+++ b/docs/index.yml
@@ -12,6 +12,8 @@
path: voice.md
- name: Web builds
path: web.md
+ - name: Partials
+ path: partials.md
- name: Examples
files:
- name: Ping
diff --git a/docs/topics/partials.md b/docs/topics/partials.md
new file mode 100644
index 000000000..2566f3def
--- /dev/null
+++ b/docs/topics/partials.md
@@ -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
+
+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.
+
+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!`);
+});
+```
+
+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.
+
+## 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.
\ No newline at end of file
diff --git a/src/client/Client.js b/src/client/Client.js
index 4e5f5e247..b1dc15573 100644
--- a/src/client/Client.js
+++ b/src/client/Client.js
@@ -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');
}
diff --git a/src/client/actions/Action.js b/src/client/actions/Action.js
index 791eaa00c..09faae9e2 100644
--- a/src/client/actions/Action.js
+++ b/src/client/actions/Action.js
@@ -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;
diff --git a/src/client/actions/ChannelCreate.js b/src/client/actions/ChannelCreate.js
index 4a9d17d45..6830f2ab5 100644
--- a/src/client/actions/ChannelCreate.js
+++ b/src/client/actions/ChannelCreate.js
@@ -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);
}
diff --git a/src/client/actions/ChannelDelete.js b/src/client/actions/ChannelDelete.js
index b9909f8ba..9fc0e6d99 100644
--- a/src/client/actions/ChannelDelete.js
+++ b/src/client/actions/ChannelDelete.js
@@ -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);
}
diff --git a/src/client/actions/MessageDelete.js b/src/client/actions/MessageDelete.js
index d66d10a28..feb118c10 100644
--- a/src/client/actions/MessageDelete.js
+++ b/src/client/actions/MessageDelete.js
@@ -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;
diff --git a/src/client/actions/MessageReactionAdd.js b/src/client/actions/MessageReactionAdd.js
index a800c3c7a..b7400cf77 100644
--- a/src/client/actions/MessageReactionAdd.js
+++ b/src/client/actions/MessageReactionAdd.js
@@ -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,
diff --git a/src/client/actions/MessageReactionRemove.js b/src/client/actions/MessageReactionRemove.js
index 7ab5be289..e5d4f8413 100644
--- a/src/client/actions/MessageReactionRemove.js
+++ b/src/client/actions/MessageReactionRemove.js
@@ -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);
diff --git a/src/client/actions/MessageReactionRemoveAll.js b/src/client/actions/MessageReactionRemoveAll.js
index f32730977..0921ce50e 100644
--- a/src/client/actions/MessageReactionRemoveAll.js
+++ b/src/client/actions/MessageReactionRemoveAll.js
@@ -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();
diff --git a/src/client/actions/MessageUpdate.js b/src/client/actions/MessageUpdate.js
index be26b2692..07e2aacb3 100644
--- a/src/client/actions/MessageUpdate.js
+++ b/src/client/actions/MessageUpdate.js
@@ -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 {
diff --git a/src/client/websocket/handlers/CHANNEL_PINS_UPDATE.js b/src/client/websocket/handlers/CHANNEL_PINS_UPDATE.js
index dfc854e37..11154674a 100644
--- a/src/client/websocket/handlers/CHANNEL_PINS_UPDATE.js
+++ b/src/client/websocket/handlers/CHANNEL_PINS_UPDATE.js
@@ -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);
diff --git a/src/client/websocket/handlers/CHANNEL_UPDATE.js b/src/client/websocket/handlers/CHANNEL_UPDATE.js
index b437ebbde..7a0df486a 100644
--- a/src/client/websocket/handlers/CHANNEL_UPDATE.js
+++ b/src/client/websocket/handlers/CHANNEL_UPDATE.js
@@ -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);
}
diff --git a/src/index.js b/src/index.js
index d9b8cb451..b3a92f60c 100644
--- a/src/index.js
+++ b/src/index.js
@@ -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'),
diff --git a/src/stores/ChannelStore.js b/src/stores/ChannelStore.js
index 9ce6465cf..88c5a0bb3 100644
--- a/src/stores/ChannelStore.js
+++ b/src/stores/ChannelStore.js
@@ -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);
}
/**
diff --git a/src/stores/DataStore.js b/src/stores/DataStore.js
index fc841899a..ade8c70f7 100644
--- a/src/stores/DataStore.js
+++ b/src/stores/DataStore.js
@@ -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;
diff --git a/src/stores/GuildMemberStore.js b/src/stores/GuildMemberStore.js
index 039fb4c7b..397c119b7 100644
--- a/src/stores/GuildMemberStore.js
+++ b/src/stores/GuildMemberStore.js
@@ -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));
}
diff --git a/src/stores/MessageStore.js b/src/stores/MessageStore.js
index 1a8a7083a..52bf7fdd6 100644
--- a/src/stores/MessageStore.js
+++ b/src/stores/MessageStore.js
@@ -40,6 +40,7 @@ class MessageStore extends DataStore {
* 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.
* @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|Promise>}
* @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;
}
diff --git a/src/stores/UserStore.js b/src/stores/UserStore.js
index 8fd2d9d5a..20c25d05b 100644
--- a/src/stores/UserStore.js
+++ b/src/stores/UserStore.js
@@ -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}
*/
- 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);
}
}
diff --git a/src/structures/APIMessage.js b/src/structures/APIMessage.js
index deca5d26d..41f983839 100644
--- a/src/structures/APIMessage.js
+++ b/src/structures/APIMessage.js
@@ -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
*/
/**
diff --git a/src/structures/Channel.js b/src/structures/Channel.js
index 92914118d..b43411949 100644
--- a/src/structures/Channel.js
+++ b/src/structures/Channel.js
@@ -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}
+ */
+ 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) {
diff --git a/src/structures/ClientUser.js b/src/structures/ClientUser.js
index 3ec32e9dd..a86a6cb56 100644
--- a/src/structures/ClientUser.js
+++ b/src/structures/ClientUser.js
@@ -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}
- * @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;
diff --git a/src/structures/DMChannel.js b/src/structures/DMChannel.js
index 7c3db02e6..f1280e802 100644
--- a/src/structures/DMChannel.js
+++ b/src/structures/DMChannel.js
@@ -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}
@@ -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.
diff --git a/src/structures/GroupDMChannel.js b/src/structures/GroupDMChannel.js
deleted file mode 100644
index 39fb56468..000000000
--- a/src/structures/GroupDMChannel.js
+++ /dev/null
@@ -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}
- */
- 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}
- */
- 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}
- */
- 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}
- */
- 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}
- */
- 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}
- */
- 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}
- */
- 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}
- */
- 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;
diff --git a/src/structures/Guild.js b/src/structures/Guild.js
index 66cf98718..f99627a4c 100644
--- a/src/structures/Guild.js
+++ b/src/structures/Guild.js
@@ -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);
}
/**
diff --git a/src/structures/GuildAuditLogs.js b/src/structures/GuildAuditLogs.js
index f78078a7c..b466a3aa0 100644
--- a/src/structures/GuildAuditLogs.js
+++ b/src/structures/GuildAuditLogs.js
@@ -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,
diff --git a/src/structures/GuildMember.js b/src/structures/GuildMember.js
index 73adb69ac..e2dea4322 100644
--- a/src/structures/GuildMember.js
+++ b/src/structures/GuildMember.js
@@ -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}
@@ -355,6 +363,14 @@ class GuildMember extends Base {
return this.guild.members.ban(this, options);
}
+ /**
+ * Fetches this GuildMember.
+ * @returns {Promise}
+ */
+ 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}
diff --git a/src/structures/Message.js b/src/structures/Message.js
index 310706e15..4b81cf94a 100644
--- a/src/structures/Message.js
+++ b/src/structures/Message.js
@@ -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}
*/
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}
+ */
+ fetch() {
+ return this.channel.messages.fetch(this.id, true);
+ }
+
/**
* Fetches the webhook used to create this message.
* @returns {Promise}
diff --git a/src/structures/MessageCollector.js b/src/structures/MessageCollector.js
index 59120b9db..44156cc03 100644
--- a/src/structures/MessageCollector.js
+++ b/src/structures/MessageCollector.js
@@ -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
diff --git a/src/structures/User.js b/src/structures/User.js
index 22ea5c81a..24bce7937 100644
--- a/src/structures/User.js
+++ b/src/structures/User.js
@@ -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}
+ */
+ 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}
diff --git a/src/util/Constants.js b/src/util/Constants.js
index 508b5f97d..1e13852cf 100644
--- a/src/util/Constants.js
+++ b/src/util/Constants.js
@@ -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
+ * Partials require you to put checks in place when handling data, read the Partials topic listed in the
+ * sidebar for more information.
+ * @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
diff --git a/src/util/Structures.js b/src/util/Structures.js
index a742e2920..4ac562606 100644
--- a/src/util/Structures.js
+++ b/src/util/Structures.js
@@ -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'),
diff --git a/src/util/Util.js b/src/util/Util.js
index 1d7b7aeeb..bbf838e46 100644
--- a/src/util/Util.js
+++ b/src/util/Util.js
@@ -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;
});
diff --git a/test/voice.js b/test/voice.js
index 70b5c30f7..6d022a29c 100644
--- a/test/voice.js
+++ b/test/voice.js
@@ -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;
diff --git a/typings/index.d.ts b/typings/index.d.ts
index 8c606400c..4283755d1 100644
--- a/typings/index.d.ts
+++ b/typings/index.d.ts
@@ -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;
+ public fetch(): Promise;
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;
public setActivity(options?: ActivityOptions): Promise;
public setActivity(name: string, options?: ActivityOptions): Promise;
public setAFK(afk: boolean): Promise;
@@ -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;
- public readonly owner: User;
- public ownerID: Snowflake;
- public recipients: Collection;
- public addUser(options: { user: UserResolvable, accessToken?: string, nick?: string }): Promise;
- public edit (data: { icon?: string, name?: string }): Promise;
- public equals(channel: GroupDMChannel): boolean;
- public iconURL(options?: AvatarOptions): string;
- public removeUser(user: UserResolvable): Promise;
- public setIcon(icon: Base64Resolvable | BufferResolvable): Promise;
- public setName(name: string): Promise;
- }
-
export class Guild extends Base {
constructor(client: Client, data: object);
private _sortedRoles(): Collection;
@@ -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;
public readonly presence: Presence;
public roles: GuildMemberRoleStore;
public user: User;
public readonly voice: VoiceState;
public ban(options?: BanOptions): Promise;
+ public fetch(): Promise;
public createDM(): Promise;
public deleteDM(): Promise;
public edit(data: GuildMemberEditData, reason?: string): Promise;
@@ -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;
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;
public equals(message: Message, rawData: object): boolean;
public fetchWebhook(): Promise;
+ public fetch(): Promise;
public pin(): Promise;
public react(emoji: EmojiIdentifierResolvable): Promise;
public reply(content?: StringResolvable, options?: MessageOptions | MessageAdditions): Promise;
@@ -706,7 +691,7 @@ declare module 'discord.js' {
}
export class MessageCollector extends Collector {
- 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;
public displayAvatarURL(options?: AvatarOptions): string;
public equals(user: User): boolean;
+ public fetch(): Promise;
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 {
- constructor(channel: TextChannel | DMChannel | GroupDMChannel, iterable?: Iterable);
+ constructor(channel: TextChannel | DMChannel, iterable?: Iterable);
public fetch(message: Snowflake): Promise;
public fetch(options?: ChannelLogsQueryOptions): Promise>;
public fetchPinned(): Promise>;
@@ -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';