Skip to content

Commit

Permalink
feat: stage channels (#5456)
Browse files Browse the repository at this point in the history
* feat: add stage channel type

* feat: initialise stage channel structure

* feat: add STAGE_MODERATOR permissions bitfield

* fix: typo in permissions

* fix(Channel): type selection logic

* feat: add rtcRegion to StageChannel and VoiceChannel

* feat: rtc region editing for stage and voice channels

* feat: stage channel userLimit

* feat: add stage channels to exports

* feat: add computed properties to stage channel

* feat(VoiceState): include stage channel in docs

* feat: allow ability to join stage channels

* feat(StageChannel): join and leave methods

* docs: add StageChannel link in GuildChannel docs

* feat(VoiceState): suppress and requestToSpeakTimestamp

* feat(StageChannel): setRequestToSpeak

* refactor(StageChannel): update setRequestToSpeak

* feat(VoiceState): add moveToSpeakers and moveToAudience

* feat(VoiceState): add methods to move in/out of speakers

* feat(VoiceState): add stage channel sanity checks

* feat(Permissions): add REQUEST_TO_SPEAK

* feat(VoiceState): simpler methods

* docs(VoiceState): add documentation for new methods

* refactor: remove unused error message

* chore: remove debug statements

* chore: revert changes to package-lock.json

* docs(VoiceState): clarify suppress

* docs(VoiceState): add missing @type param

* feat(StageChannel): remove nsfw property

* fix(VoiceState): check permissions in channel

Co-authored-by: Advaith <advaithj1@gmail.com>

* fix(VoiceState): instantiate error with new

Co-authored-by: BannerBomb <BannerBomb55@gmail.com>

* refactor(VoiceState): more readable API route builder

Co-authored-by: Sugden <28943913+NotSugden@users.noreply.github.com>

* style(VoiceState): fix lint errors

* docs(VoiceState): add example usage for new methods

* docs: setRTCRegion examples

* chore: update typings

* fix(VoiceState): calculate permissions for self

* refactor(VoiceState): tidy up implementation

* Update src/structures/VoiceState.js

Co-authored-by: Jan <66554238+vaporox@users.noreply.github.com>

* refactor: vaporox's suggestions

* style(VoiceState): fix linter errors

* chore: update typings

* chore: remove unused error message

* refactor(VoiceState): use optional chaining

Co-authored-by: Sugden <28943913+NotSugden@users.noreply.github.com>

* chore: move getters below constructor in typings

* refactor(StageChannel): optional chaining

Co-authored-by: Sugden <28943913+NotSugden@users.noreply.github.com>

* style(VoiceState): fix lint errors

* docs: fix incorrect types

Co-authored-by: izexi <43889168+izexi@users.noreply.github.com>

* Update src/structures/VoiceChannel.js

Co-authored-by: izexi <43889168+izexi@users.noreply.github.com>

* Update src/structures/VoiceChannel.js

Co-authored-by: izexi <43889168+izexi@users.noreply.github.com>

* refactor(VoiceState): use optional chaining

Co-authored-by: Sugden <28943913+NotSugden@users.noreply.github.com>

* refactor(StageChannel): remove permission override check in joinable

* refactor: make ChannelTypes a proper enum

* Use createEnum

Co-authored-by: izexi <43889168+izexi@users.noreply.github.com>

* chore: remove unused code from Constants

* refactor(StageChannel): remove unnecessary getters

* chore: update typings

* refactor: introduce BaseGuildVoiceChannel class

* refactor(VoiceChannel): reduce code duplication

* feat: export BaseGuildVoiceChannel

* chore: update typings

* docs: fix typos

* refactor: move setRTCRegion to BaseGuildVoiceChannel

* feat(VoiceState): remove permission checks

* chore: update typings

* Apply suggestions from code review

Co-authored-by: Sugden <28943913+NotSugden@users.noreply.github.com>
Co-authored-by: Jan <66554238+vaporox@users.noreply.github.com>

* chore: update esm exports and typings

* Update src/structures/VoiceState.js

Co-authored-by: Vlad Frangu <kingdgrizzle@gmail.com>

Co-authored-by: Advaith <advaithj1@gmail.com>
Co-authored-by: BannerBomb <BannerBomb55@gmail.com>
Co-authored-by: Sugden <28943913+NotSugden@users.noreply.github.com>
Co-authored-by: Jan <66554238+vaporox@users.noreply.github.com>
Co-authored-by: izexi <43889168+izexi@users.noreply.github.com>
Co-authored-by: Vlad Frangu <kingdgrizzle@gmail.com>
  • Loading branch information
7 people committed Apr 14, 2021
1 parent 63ff6a0 commit eec7cf7
Show file tree
Hide file tree
Showing 15 changed files with 320 additions and 108 deletions.
2 changes: 2 additions & 0 deletions esm/discord.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ export const {
Activity,
APIMessage,
BaseGuildEmoji,
BaseGuildVoiceChannel,
CategoryChannel,
Channel,
ClientApplication,
Expand Down Expand Up @@ -86,6 +87,7 @@ export const {
RichPresenceAssets,
Role,
StoreChannel,
StageChannel,
Team,
TeamMember,
TextChannel,
Expand Down
4 changes: 2 additions & 2 deletions src/client/voice/ClientVoiceManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,8 @@ class ClientVoiceManager {
}

/**
* Sets up a request to join a voice channel.
* @param {VoiceChannel} channel The voice channel to join
* Sets up a request to join a voice or stage channel.
* @param {VoiceChannel|StageChannel} channel The channel to join
* @returns {Promise<VoiceConnection>}
* @private
*/
Expand Down
8 changes: 4 additions & 4 deletions src/client/voice/VoiceConnection.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,8 @@ class VoiceConnection extends EventEmitter {
this.voiceManager = voiceManager;

/**
* The voice channel this connection is currently serving
* @type {VoiceChannel}
* The voice channel or stage channel this connection is currently serving
* @type {VoiceChannel|StageChannel}
*/
this.channel = channel;

Expand Down Expand Up @@ -306,8 +306,8 @@ class VoiceConnection extends EventEmitter {
}

/**
* Move to a different voice channel in the same guild.
* @param {VoiceChannel} channel The channel to move to
* Move to a different voice channel or stage channel in the same guild.
* @param {VoiceChannel|StageChannel} channel The channel to move to
* @private
*/
updateChannel(channel) {
Expand Down
4 changes: 3 additions & 1 deletion src/errors/Messages.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,11 @@ const Messages = {
VOICE_PLAY_INTERFACE_NO_BROADCAST: 'A broadcast cannot be played in this context.',
VOICE_PLAY_INTERFACE_BAD_TYPE: 'Unknown stream type',
VOICE_PRISM_DEMUXERS_NEED_STREAM: 'To play a webm/ogg stream, you need to pass a ReadableStream.',
VOICE_NOT_STAGE_CHANNEL: 'You are only allowed to do this in stage channels.',

VOICE_STATE_UNCACHED_MEMBER: 'The member of this voice state is uncached.',
VOICE_STATE_NOT_OWN: 'You cannot self-deafen/mute on VoiceStates that do not belong to the ClientUser.',
VOICE_STATE_NOT_OWN:
'You cannot self-deafen/mute/request to speak on VoiceStates that do not belong to the ClientUser.',
VOICE_STATE_INVALID_TYPE: name => `${name} must be a boolean.`,

UDP_SEND_FAIL: 'Tried to send a UDP packet, but there is no socket available.',
Expand Down
2 changes: 2 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ module.exports = {
Activity: require('./structures/Presence').Activity,
APIMessage: require('./structures/APIMessage'),
BaseGuildEmoji: require('./structures/BaseGuildEmoji'),
BaseGuildVoiceChannel: require('./structures/BaseGuildVoiceChannel'),
CategoryChannel: require('./structures/CategoryChannel'),
Channel: require('./structures/Channel'),
ClientApplication: require('./structures/ClientApplication'),
Expand Down Expand Up @@ -98,6 +99,7 @@ module.exports = {
RichPresenceAssets: require('./structures/Presence').RichPresenceAssets,
Role: require('./structures/Role'),
StoreChannel: require('./structures/StoreChannel'),
StageChannel: require('./structures/StageChannel'),
Team: require('./structures/Team'),
TeamMember: require('./structures/TeamMember'),
TextChannel: require('./structures/TextChannel'),
Expand Down
109 changes: 109 additions & 0 deletions src/structures/BaseGuildVoiceChannel.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
'use strict';

const GuildChannel = require('./GuildChannel');
const Collection = require('../util/Collection');
const Permissions = require('../util/Permissions');

/**
* Represents a voice-based guild channel on Discord.
* @extends {GuildChannel}
*/
class BaseGuildVoiceChannel extends GuildChannel {
_patch(data) {
super._patch(data);

/**
* The RTC region for this voice-based channel. This region is automatically selected if `null`.
* @type {?string}
*/
this.rtcRegion = data.rtc_region;

/**
* The bitrate of this voice-based channel
* @type {number}
*/
this.bitrate = data.bitrate;

/**
* The maximum amount of users allowed in this channel.
* @type {number}
*/
this.userLimit = data.user_limit;
}

/**
* The members in this voice-based channel
* @type {Collection<Snowflake, GuildMember>}
* @readonly
*/
get members() {
const coll = new Collection();
for (const state of this.guild.voiceStates.cache.values()) {
if (state.channelID === this.id && state.member) {
coll.set(state.id, state.member);
}
}
return coll;
}

/**
* Checks if the voice-based channel is full
* @type {boolean}
* @readonly
*/
get full() {
return this.userLimit > 0 && this.members.size >= this.userLimit;
}

/**
* Whether the channel is joinable by the client user
* @type {boolean}
* @readonly
*/
get joinable() {
if (!this.viewable) return false;
if (!this.permissionsFor(this.client.user).has(Permissions.FLAGS.CONNECT, false)) return false;
return true;
}

/**
* Attempts to join this voice-based channel.
* @returns {Promise<VoiceConnection>}
* @example
* // Join a voice-based channel
* channel.join()
* .then(connection => console.log('Connected!'))
* .catch(console.error);
*/
join() {
return this.client.voice.joinChannel(this);
}

/**
* Leaves this voice-based channel.
* @example
* // Leave a voice-based channel
* channel.leave();
*/
leave() {
const connection = this.client.voice.connections.get(this.guild.id);
if (connection?.channel.id === this.id) connection.disconnect();
}

/**
* Sets the RTC region of the channel.
* @param {?string} region The new region of the channel. Set to `null` to remove a specific region for the channel
* @returns {Promise<BaseGuildVoiceChannel>}
* @example
* // Set the RTC region to europe
* channel.setRTCRegion('europe');
* @example
* // Remove a fixed region for this channel - let Discord decide automatically
* channel.setRTCRegion(null);
*/
setRTCRegion(region) {
return this.edit({ rtcRegion: region });
}
}

module.exports = BaseGuildVoiceChannel;
8 changes: 7 additions & 1 deletion src/structures/Channel.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ class Channel extends Base {
constructor(client, data) {
super(client);

const type = Object.keys(ChannelTypes)[data.type];
const type = ChannelTypes[data.type];
/**
* The type of the channel, either:
* * `dm` - a DM channel
Expand All @@ -22,6 +22,7 @@ class Channel extends Base {
* * `category` - a guild category channel
* * `news` - a guild news channel
* * `store` - a guild store channel
* * `stage` - a guild stage channel
* * `unknown` - a generic channel of unknown type, could be Channel or GuildChannel
* @type {string}
*/
Expand Down Expand Up @@ -146,6 +147,11 @@ class Channel extends Base {
channel = new StoreChannel(guild, data);
break;
}
case ChannelTypes.STAGE: {
const StageChannel = Structures.get('StageChannel');
channel = new StageChannel(guild, data);
break;
}
}
if (channel) guild.channels.cache.set(channel.id, channel);
}
Expand Down
5 changes: 4 additions & 1 deletion src/structures/GuildChannel.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ const Util = require('../util/Util');
* - {@link CategoryChannel}
* - {@link NewsChannel}
* - {@link StoreChannel}
* - {@link StageChannel}
* @extends {Channel}
* @abstract
*/
Expand Down Expand Up @@ -319,6 +320,7 @@ class GuildChannel extends Channel {
* @property {OverwriteResolvable[]|Collection<Snowflake, OverwriteResolvable>} [permissionOverwrites]
* Permission overwrites for the channel
* @property {number} [rateLimitPerUser] The ratelimit per user for the channel in seconds
* @property {?string} [rtcRegion] The RTC region of the channel
*/

/**
Expand Down Expand Up @@ -374,6 +376,7 @@ class GuildChannel extends Channel {
nsfw: data.nsfw,
bitrate: data.bitrate || this.bitrate,
user_limit: typeof data.userLimit !== 'undefined' ? data.userLimit : this.userLimit,
rtc_region: typeof data.rtcRegion !== 'undefined' ? data.rtcRegion : this.rtcRegion,
parent_id: data.parentID,
lock_permissions: data.lockPermissions,
rate_limit_per_user: data.rateLimitPerUser,
Expand Down Expand Up @@ -594,7 +597,7 @@ class GuildChannel extends Channel {
*/
get manageable() {
if (this.client.user.id === this.guild.ownerID) return true;
if (this.type === 'voice') {
if (this.type === 'voice' || this.type === 'stage') {
if (!this.permissionsFor(this.client.user).has(Permissions.FLAGS.CONNECT, false)) {
return false;
}
Expand Down
34 changes: 34 additions & 0 deletions src/structures/StageChannel.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
'use strict';

const BaseGuildVoiceChannel = require('./BaseGuildVoiceChannel');

/**
* Represents a guild stage channel on Discord.
* @extends {BaseGuildVoiceChannel}
*/
class StageChannel extends BaseGuildVoiceChannel {
_patch(data) {
super._patch(data);

/**
* The topic of the stage channel
* @type {?string}
*/
this.topic = data.topic;
}

/**
* Sets the RTC region of the channel.
* @name StageChannel#setRTCRegion
* @param {?string} region The new region of the channel. Set to `null` to remove a specific region for the channel
* @returns {Promise<StageChannel>}
* @example
* // Set the RTC region to europe
* stageChannel.setRTCRegion('europe');
* @example
* // Remove a fixed region for this channel - let Discord decide automatically
* stageChannel.setRTCRegion(null);
*/
}

module.exports = StageChannel;
76 changes: 12 additions & 64 deletions src/structures/VoiceChannel.js
Original file line number Diff line number Diff line change
@@ -1,53 +1,13 @@
'use strict';

const GuildChannel = require('./GuildChannel');
const Collection = require('../util/Collection');
const BaseGuildVoiceChannel = require('./BaseGuildVoiceChannel');
const Permissions = require('../util/Permissions');

/**
* Represents a guild voice channel on Discord.
* @extends {GuildChannel}
* @extends {BaseGuildVoiceChannel}
*/
class VoiceChannel extends GuildChannel {
_patch(data) {
super._patch(data);
/**
* The bitrate of this voice channel
* @type {number}
*/
this.bitrate = data.bitrate;

/**
* The maximum amount of users allowed in this channel - 0 means unlimited.
* @type {number}
*/
this.userLimit = data.user_limit;
}

/**
* The members in this voice channel
* @type {Collection<Snowflake, GuildMember>}
* @readonly
*/
get members() {
const coll = new Collection();
for (const state of this.guild.voiceStates.cache.values()) {
if (state.channelID === this.id && state.member) {
coll.set(state.id, state.member);
}
}
return coll;
}

/**
* Checks if the voice channel is full
* @type {boolean}
* @readonly
*/
get full() {
return this.userLimit > 0 && this.members.size >= this.userLimit;
}

class VoiceChannel extends BaseGuildVoiceChannel {
/**
* Whether the channel is deletable by the client user
* @type {boolean}
Expand All @@ -72,8 +32,7 @@ class VoiceChannel extends GuildChannel {
* @readonly
*/
get joinable() {
if (!this.viewable) return false;
if (!this.permissionsFor(this.client.user).has(Permissions.FLAGS.CONNECT, false)) return false;
if (!super.joinable) return false;
if (this.full && !this.permissionsFor(this.client.user).has(Permissions.FLAGS.MOVE_MEMBERS, false)) return false;
return true;
}
Expand Down Expand Up @@ -118,28 +77,17 @@ class VoiceChannel extends GuildChannel {
}

/**
* Attempts to join this voice channel.
* @returns {Promise<VoiceConnection>}
* Sets the RTC region of the channel.
* @name VoiceChannel#setRTCRegion
* @param {?string} region The new region of the channel. Set to `null` to remove a specific region for the channel
* @returns {Promise<VoiceChannel>}
* @example
* // Join a voice channel
* voiceChannel.join()
* .then(connection => console.log('Connected!'))
* .catch(console.error);
*/
join() {
return this.client.voice.joinChannel(this);
}

/**
* Leaves this voice channel.
* // Set the RTC region to europe
* voiceChannel.setRTCRegion('europe');
* @example
* // Leave a voice channel
* voiceChannel.leave();
* // Remove a fixed region for this channel - let Discord decide automatically
* voiceChannel.setRTCRegion(null);
*/
leave() {
const connection = this.client.voice.connections.get(this.guild.id);
if (connection && connection.channel.id === this.id) connection.disconnect();
}
}

module.exports = VoiceChannel;

0 comments on commit eec7cf7

Please sign in to comment.