feat(platform): enforce voice/stage permissions per channel (Phase 85)

joinVoiceChannel requires CONNECT_VOICE; missing SPEAK_IN_VOICE forces
listen-only. joinStageChannel uses channel-effective MANAGE_CHANNELS for
mods. view().voicePermsByChannel maps connect/speak/listenOnly per channel.
This commit is contained in:
Raven Scott
2026-05-22 03:07:32 -04:00
parent 5c4b6c1ed4
commit 489d5c6a90
+60 -13
View File
@@ -117,6 +117,8 @@ const {
PERMISSION,
roleHasPermission,
memberCanParticipate,
memberCanConnectVoice,
memberCanSpeakInVoice,
memberCanJoinVoiceListenOnly,
MEMBER_ROLES,
memberHasPermission,
@@ -2529,21 +2531,21 @@ class PearcordPlatform extends EventEmitter {
let listenOnly = !!opts.listenOnly
if (this.mode === 'guild') {
const member = await this._resolveMemberRecord()
if (!member) throw new Error('not a guild member')
const customRoles = this.guildRoles && this.guild?.guild
? await this.guildRoles.listRoles(this.guild.guild.id)
: []
const mask = await this._participationMask(channelId)
const canParticipate = memberCanParticipate(member, customRoles, mask)
if (!listenOnly && !canParticipate) {
if (memberCanJoinVoiceListenOnly(member, customRoles)) listenOnly = true
else throw new Error('read-only role cannot join voice')
if (!memberCanConnectVoice(member, customRoles, mask)) {
throw new Error('no permission to connect to voice channel')
}
if (listenOnly) {
if (!memberCanJoinVoiceListenOnly(member, customRoles)) {
throw new Error('cannot join voice listen-only')
}
} else {
await this._assertCanParticipate()
if (opts.listenOnly) {
listenOnly = true
} else if (!memberCanSpeakInVoice(member, customRoles, mask)) {
listenOnly = true
}
if (listenOnly && !memberCanJoinVoiceListenOnly(member, customRoles, mask)) {
throw new Error('cannot join voice listen-only')
}
}
const guild = this.guild?.guild
@@ -2609,8 +2611,15 @@ class PearcordPlatform extends EventEmitter {
if (!guild || !this.voice || !user) throw new Error('voice not available')
const ch = await this.db.get(COLLECTIONS.CHANNELS, { guildId: guild.id, id: channelId })
if (!ch || !isStageChannel(ch)) throw new Error('not a stage channel')
const roles = await this._memberRoles()
const isMod = roleHasPermission(roles, PERMISSION.MANAGE_CHANNELS)
const member = await this._resolveMemberRecord()
const customRoles = this.guildRoles
? await this.guildRoles.listRoles(guild.id)
: []
const mask = await this._participationMask(channelId)
if (!memberCanConnectVoice(member, customRoles, mask)) {
throw new Error('no permission to connect to stage channel')
}
const isMod = await this._hasPerm(PERMISSION.MANAGE_CHANNELS, channelId)
const row = await this.voice.join(channelId)
this.guild.gossipVoiceState(row)
const stageRow = await this.stage.setRole({
@@ -5077,7 +5086,21 @@ class PearcordPlatform extends EventEmitter {
customRolesForSelf,
partMask
)
canJoinVoiceListenOnly = memberCanJoinVoiceListenOnly(selfMember, customRolesForSelf)
if (
activeChannel &&
(activeChannel.type === 'voice' || activeChannel.type === 'stage')
) {
canJoinVoiceListenOnly = memberCanJoinVoiceListenOnly(
selfMember,
customRolesForSelf,
partMask
)
} else {
canJoinVoiceListenOnly = memberCanJoinVoiceListenOnly(
selfMember,
customRolesForSelf
)
}
if (activeChannel && isAnnouncementChannel(activeChannel)) {
canPostInActiveChannel = canPostInActiveChannel && !!myPermissions?.manageMessages
}
@@ -5162,6 +5185,29 @@ class PearcordPlatform extends EventEmitter {
members: members || [],
contacts: contactsForView
})
const voicePermsByChannel = {}
if (this.mode === 'guild' && guild) {
const selfMember = await this._resolveMemberRecord()
const customRolesForSelf = this.guildRoles
? await this.guildRoles.listRoles(guild.id)
: []
if (selfMember) {
for (const ch of channels) {
if (ch.type !== 'voice' && ch.type !== 'stage') continue
const ovs = await this._channelOverwrites(ch.id)
const mask = effectivePermissionMask(selfMember, customRolesForSelf, ovs)
voicePermsByChannel[ch.id] = {
connect: memberCanConnectVoice(selfMember, customRolesForSelf, mask),
speak: memberCanSpeakInVoice(selfMember, customRolesForSelf, mask),
listenOnly: memberCanJoinVoiceListenOnly(
selfMember,
customRolesForSelf,
mask
)
}
}
}
}
return {
onboarded: this.onboarded,
user,
@@ -5197,6 +5243,7 @@ class PearcordPlatform extends EventEmitter {
myPermissions,
canPostInActiveChannel,
canJoinVoiceListenOnly,
voicePermsByChannel,
pins,
mentionNames: [...mentionNames],
channelSettings,