diff --git a/src/structures/CategoryChannel.js b/src/structures/CategoryChannel.js new file mode 100644 index 000000000..89b8b3165 --- /dev/null +++ b/src/structures/CategoryChannel.js @@ -0,0 +1,9 @@ +const GuildChannel = require('./GuildChannel'); + +class CategoryChannel extends GuildChannel { + get children() { + return this.guild.channels.filter(c => c.parentID === this.id); + } +} + +module.exports = CategoryChannel; diff --git a/src/structures/Channel.js b/src/structures/Channel.js index f70ec5bd8..ef21039f8 100644 --- a/src/structures/Channel.js +++ b/src/structures/Channel.js @@ -17,6 +17,7 @@ class Channel extends Base { * * `group` - a Group DM channel * * `text` - a guild text channel * * `voice` - a guild voice channel + * * `category` - a guild category channel * * `unknown` - a generic channel of unknown type, could be Channel or GuildChannel * @type {string} */ @@ -69,6 +70,7 @@ class Channel extends Base { const GroupDMChannel = require('./GroupDMChannel'); const TextChannel = require('./TextChannel'); const VoiceChannel = require('./VoiceChannel'); + const CategoryChannel = require('./CategoryChannel'); const GuildChannel = require('./GuildChannel'); const types = Constants.ChannelTypes; let channel; @@ -86,6 +88,9 @@ class Channel extends Base { case types.VOICE: channel = new VoiceChannel(guild, data); break; + case types.CATEGORY: + channel = new CategoryChannel(guild, data); + break; default: channel = new GuildChannel(guild, data); } diff --git a/src/structures/Guild.js b/src/structures/Guild.js index 8a4b8db21..4af91a5c7 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -891,10 +891,9 @@ class Guild extends Base { /** * Creates a new channel in the guild. * @param {string} name The name of the new channel - * @param {string} type The type of the new channel, either `text` or `voice` - * @param {Object} [options={}] Options + * @param {string} type The type of the new channel, either `text`, `voice`, or `category` + * @param {Object} [options] Options * @param {Array} [options.overwrites] Permission overwrites - * to apply to the new channel * @param {string} [options.reason] Reason for creating this channel * @returns {Promise} * @example @@ -1205,31 +1204,17 @@ class Guild extends Base { /** * Fetches a collection of channels in the current guild sorted by position. - * @param {string} type The channel type + * @param {Channel} channel Channel * @returns {Collection} * @private */ - _sortedChannels(type) { - return this._sortPositionWithID(this.channels.filter(c => { - if (type === 'voice' && c.type === 'voice') return true; - else if (type !== 'voice' && c.type !== 'voice') return true; - else return type === c.type; - })); - } - - /** - * Sorts a collection by object position or ID if the positions are equivalent. - * Intended to be identical to Discord's sorting method. - * @param {Collection} collection The collection to sort - * @returns {Collection} - * @private - */ - _sortPositionWithID(collection) { - return collection.sort((a, b) => - a.position !== b.position ? - a.position - b.position : - Long.fromString(a.id).sub(Long.fromString(b.id)).toNumber() - ); + _sortedChannels(channel) { + const sort = col => col + .sort((a, b) => a.rawPosition - b.rawPosition || Long.fromString(a.id).sub(Long.fromString(b.id)).toNumber()); + if (channel.type === Constants.ChannelTypes.CATEGORY) { + return sort(this.channels.filter(c => c.type === Constants.ChannelTypes.CATEGORY)); + } + return sort(this.channels.filter(c => c.parent === channel.parent)); } } diff --git a/src/structures/GuildChannel.js b/src/structures/GuildChannel.js index 195b40053..f45a5de48 100644 --- a/src/structures/GuildChannel.js +++ b/src/structures/GuildChannel.js @@ -2,6 +2,7 @@ const Channel = require('./Channel'); const Role = require('./Role'); const Invite = require('./Invite'); const PermissionOverwrites = require('./PermissionOverwrites'); +const Util = require('../util/Util'); const Permissions = require('../util/Permissions'); const Collection = require('../util/Collection'); const Constants = require('../util/Constants'); @@ -32,10 +33,16 @@ class GuildChannel extends Channel { this.name = data.name; /** - * The position of the channel in the list + * The raw position of the channel from discord * @type {number} */ - this.position = data.position; + this.rawPosition = data.position; + + /** + * The ID of the category parent of this channel + * @type {?Snowflake} + */ + this.parentID = data.parent_id; /** * A map of permission overwrites in this channel for roles and users @@ -49,13 +56,21 @@ class GuildChannel extends Channel { } } + /** + * The category parent of this channel + * @type {?GuildChannel} + */ + get parent() { + return this.guild.channels.get(this.parentID); + } + /** * The position of the channel * @type {number} * @readonly */ - get calculatedPosition() { - const sorted = this.guild._sortedChannels(this.type); + get position() { + const sorted = this.guild._sortedChannels(this); return sorted.array().indexOf(sorted.get(this.id)); } @@ -70,34 +85,32 @@ class GuildChannel extends Channel { if (!member) return null; if (member.id === this.guild.ownerID) return new Permissions(Permissions.ALL); - let permissions = 0; - - const roles = member.roles; - for (const role of roles.values()) permissions |= role.permissions; - - const overwrites = this.overwritesFor(member, true, roles); + let resolved = this.guild.roles.get(this.guild.id).permissions; + const overwrites = this.overwritesFor(member, true); if (overwrites.everyone) { - permissions &= ~overwrites.everyone._denied; - permissions |= overwrites.everyone._allowed; + resolved &= ~overwrites.everyone._denied; + resolved |= overwrites.everyone._allowed; } - let allow = 0; + let allows = 0; + let denies = 0; for (const overwrite of overwrites.roles) { - permissions &= ~overwrite._denied; - allow |= overwrite._allowed; + allows |= overwrite._allowed; + denies |= overwrite._denied; } - permissions |= allow; + resolved &= ~denies; + resolved |= allows; if (overwrites.member) { - permissions &= ~overwrites.member._denied; - permissions |= overwrites.member._allowed; + resolved &= ~overwrites.member._denied; + resolved |= overwrites.member._allowed; } - const admin = Boolean(permissions & Permissions.FLAGS.ADMINISTRATOR); - if (admin) permissions = Permissions.ALL; + const admin = Boolean(resolved & Permissions.FLAGS.ADMINISTRATOR); + if (admin) resolved = Permissions.ALL; - return new Permissions(permissions); + return new Permissions(resolved); } overwritesFor(member, verified = false, roles = null) { @@ -236,10 +249,12 @@ class GuildChannel extends Channel { return this.client.api.channels(this.id).patch({ data: { name: (data.name || this.name).trim(), - topic: data.topic || this.topic, + topic: data.topic, position: data.position || this.position, bitrate: data.bitrate || (this.bitrate ? this.bitrate * 1000 : undefined), - user_limit: data.userLimit || this.userLimit, + user_limit: data.userLimit != null ? data.userLimit : this.userLimit, // eslint-disable-line eqeqeq + parent_id: data.parentID, + lock_permissions: data.lockPermissions, }, reason, }).then(newData => { @@ -275,8 +290,34 @@ class GuildChannel extends Channel { * .then(newChannel => console.log(`Channel's new position is ${newChannel.position}`)) * .catch(console.error); */ - setPosition(position, relative) { - return this.guild.setChannelPosition(this, position, relative).then(() => this); + setPosition(position, { relative, reason }) { + position = Number(position); + if (isNaN(position)) return Promise.reject(new TypeError('INVALID_TYPE', 'position', 'number')); + let updatedChannels = this.guild._sortedChannels(this).array(); + Util.moveElementInArray(updatedChannels, this, position, relative); + updatedChannels = updatedChannels.map((r, i) => ({ id: r.id, position: i })); + return this.client.api.guilds(this.id).channels.patch({ data: updatedChannels, reason }) + .then(() => { + this.client.actions.GuildChannelsPositionUpdate.handle({ + guild_id: this.id, + channels: updatedChannels, + }); + return this; + }); + } + + /** + * Set the category parent of this channel. + * @param {GuildChannel|Snowflake} channel Parent channel + * @param {boolean} [options.lockPermissions] Lock the permissions to what the parent's permissions are + * @param {string} [options.reason] Reason for modifying the parent of this channel + * @returns {Promise} + */ + setParent(channel, { lockPermissions = true, reason } = {}) { + return this.edit({ + parentID: channel.id ? channel.id : channel, + lockPermissions, + }, reason); } /** diff --git a/src/util/Constants.js b/src/util/Constants.js index 447b95875..a88af9f5b 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -165,13 +165,6 @@ exports.VoiceStatus = { DISCONNECTED: 4, }; -exports.ChannelTypes = { - TEXT: 0, - DM: 1, - VOICE: 2, - GROUP: 3, -}; - exports.OPCodes = { DISPATCH: 0, HEARTBEAT: 1, @@ -574,6 +567,14 @@ exports.UserFlags = { HYPESQUAD: 1 << 2, }; +exports.ChannelTypes = { + TEXT: 0, + DM: 1, + VOICE: 2, + GROUP: 3, + CATEGORY: 4, +}; + exports.ClientApplicationAssetTypes = { SMALL: 1, BIG: 2,