diff --git a/src/structures/GroupDMChannel.js b/src/structures/GroupDMChannel.js new file mode 100644 index 00000000..207715e --- /dev/null +++ b/src/structures/GroupDMChannel.js @@ -0,0 +1,315 @@ +'use strict'; + +const { Collection } = require('@discordjs/collection'); +const { Channel } = require('./Channel'); +const Invite = require('./Invite'); +const TextBasedChannel = require('./interfaces/TextBasedChannel'); +const MessageManager = require('../managers/MessageManager'); +const DataResolver = require('../util/DataResolver'); + +/** + * Represents a Group DM Channel on Discord. + * @extends {Channel} + * @implements {TextBasedChannel} + */ +class GroupDMChannel extends Channel { + constructor(client, data) { + super(client, data); + // Override the channel type so partials have a known type + this.type = 'GROUP_DM'; + + /** + * A manager of the messages belonging to this channel + * @type {MessageManager} + */ + this.messages = new MessageManager(this); + } + + _patch(data) { + super._patch(data); + + if ('recipients' in data && Array.isArray(data.recipients)) { + this._recipients = data.recipients; + data.recipients.forEach(u => this.client.users._add(u)); + } else { + this._recipients = []; + } + + if ('last_message_id' in data) { + /** + * The channel's last message id, if one was sent + * @type {?Snowflake} + */ + this.lastMessageId = data.last_message_id; + } + + if ('last_pin_timestamp' in data) { + /** + * The timestamp when the last pinned message was pinned, if there was one + * @type {?number} + */ + this.lastPinTimestamp = new Date(data.last_pin_timestamp).getTime(); + } else { + this.lastPinTimestamp ??= null; + } + + if ('owner_id' in data) { + /** + * Owner ID + * @type {Snowflake} + */ + this.ownerId = data.owner_id; + } + + if ('name' in data) { + /** + * The name of this Group DM Channel + * @type {?string} + */ + this.name = data.name; + } + + if ('icon' in data) { + /** + * The hash of the channel icon + * @type {?string} + */ + this.icon = data.icon; + } + } + + /** + * The URL to this channel's icon. + * @param {StaticImageURLOptions} [options={}] Options for the Image URL + * @returns {?string} + */ + iconURL({ format, size } = {}) { + return this.icon && this.client.rest.cdn.GDMIcon(this.id, this.icon, format, size); + } + + /** + * The recipients of this Group DM Channel. + * @type {Collection} + * @readonly + */ + get recipients() { + const collect = new Collection(); + this._recipients.map(recipient => collect.set(recipient.id, this.client.users.cache.get(recipient.id))); + collect.set(this.client.user.id, this.client.user); + return collect; + } + + /** + * The owner of this Group DM Channel + * @type {?User} + * @readonly + */ + get owner() { + return this.client.users.cache.get(this.ownerId); + } + + /** + * Whether this DMChannel is a partial + * @type {boolean} + * @readonly + */ + get partial() { + return typeof this.lastMessageId === 'undefined'; + } + + /** + * Leave this Group DM Channel. + * @param {?boolean} slient Leave without notifying other members + * @returns {Promise} + * @example + * // Delete the channel + * channel.delete() + * .then(console.log) + * .catch(console.error); + */ + async delete(slient = false) { + if (typeof slient === 'boolean' && slient) { + await this.client.api.channels[this.id].delete({ + query: { + silent: true, + }, + }); + } else { + await this.client.api.channels[this.id].delete(); + } + return this; + } + + /** + * When concatenated with a string, this automatically returns the recipient's mention instead of the + * GroupDMChannel object. + * @returns {string} + * @example + * // Logs: Hello from Group Test! + * console.log(`Hello from ${channel}!`); + */ + toString() { + return ( + this.name ?? + this._recipients + .filter(user => user.id !== this.client.user.id) + .map(user => user.username) + .join(', ') + ); + } + + toJSON() { + const json = super.toJSON({ + createdTimestamp: true, + }); + json.iconURL = this.iconURL(); + return json; + } + + /** + * The data for editing a channe;. + * @typedef {Object} GroupDMChannelEditData + * @property {string} [name] The name of the channel + * @property {?(BufferResolvable|Base64Resolvable)} [icon] The icon of the channel + * @property {GuildMemberResolvable} [owner] The owner of the channel + */ + + /** + * Edit channel data + * @param {GroupDMChannelEditData} data Data + * @returns {Promise} + * @example + * // Set the channel name + * channel.edit({ + * name: 'Group Test', + * }) + * .then(updated => console.log(`New channel name ${updated}`)) + * .catch(console.error); + */ + async edit(data) { + const _data = {}; + if ('name' in data) _data.name = data.name?.trim() ?? null; + if (typeof data.icon !== 'undefined') { + _data.icon = await DataResolver.resolveImage(data.icon); + } + if ('owner' in data) { + _data.owner = data.owner; + } + const newData = await this.client.api.channels[this.id].patch({ + data: _data, + }); + + return this.client.actions.ChannelUpdate.handle(newData).updated; + } + + /** + * Renames this Group DM Channel. + * @param {?string} name Name of the channel + * @returns {Promise} + */ + setName(name) { + return this.edit({ name }); + } + + /** + * Sets the icon of this Group DM Channel. + * @param {?(Base64Resolvable|BufferResolvable)} icon Icon of the channel + * @returns {Promise} + */ + setIcon(icon) { + return this.edit({ icon }); + } + + /** + * Changes the owner of this Group DM Channel. + * @param {UserResolvable} user User to transfer ownership to + * @returns {Promise} + */ + setOwner(user) { + const id = this.client.users.resolveId(user); + if (this.ownerId === id) { + return Promise.resolve(this); + } + return this.edit({ owner: id }); + } + + /** + * Adds a user to this Group DM Channel. + * @param {UserResolvable} user User to add to the group + * @returns {Promise} + */ + async addUser(user) { + user = this.client.users.resolveId(user); + await this.client.api.channels[this.id].recipients[user].put(); + return this; + } + + /** + * Removes a user from this Group DM Channel. + * @param {UserResolvable} user User to remove from the group + * @returns {Promise} + */ + async removeUser(user) { + user = this.client.users.resolveId(user); + await this.client.api.channels[this.id].recipients[user].delete(); + return this; + } + + /** + * Gets the invite for this Group DM Channel. + * @returns {Promise} + */ + async getInvite() { + const inviteCode = await this.client.api.channels(this.id).invites.post({ + data: { + max_age: 86400, + }, + }); + return new Invite(this.client, inviteCode); + } + + /** + * Get all the invites for this Group DM Channel. + * @returns {Promise>} + */ + async fetchAllInvite() { + const invites = await this.client.api.channels(this.id).invites.get(); + return new Collection(invites.map(invite => [invite.code, new Invite(this.client, invite)])); + } + + /** + * Delete invites from this Group DM Channel. + * @param {InviteResolvable} invite Invite to add to the channel + * @returns {Promise} + */ + async removeInvite(invite) { + // Resolve + let code = invite?.code; + if (!code && URL.canParse(invite)) code = new URL(invite).pathname.slice(1); + else code = invite; + await this.client.api.channels(this.id).invites[invite].delete(); + return this; + } + + // These are here only for documentation purposes - they are implemented by TextBasedChannel + /* eslint-disable no-empty-function */ + get lastMessage() {} + get lastPinAt() {} + send() {} + sendTyping() {} + createMessageCollector() {} + awaitMessages() {} + createMessageComponentCollector() {} + awaitMessageComponent() {} + // Doesn't work on DM channels; setRateLimitPerUser() {} + // Doesn't work on DM channels; setNSFW() {} +} + +TextBasedChannel.applyToClass(GroupDMChannel, true, [ + 'fetchWebhooks', + 'createWebhook', + 'setRateLimitPerUser', + 'setNSFW', +]); + +module.exports = GroupDMChannel;