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, PERMISSION,
roleHasPermission, roleHasPermission,
memberCanParticipate, memberCanParticipate,
memberCanConnectVoice,
memberCanSpeakInVoice,
memberCanJoinVoiceListenOnly, memberCanJoinVoiceListenOnly,
MEMBER_ROLES, MEMBER_ROLES,
memberHasPermission, memberHasPermission,
@@ -2529,21 +2531,21 @@ class PearcordPlatform extends EventEmitter {
let listenOnly = !!opts.listenOnly let listenOnly = !!opts.listenOnly
if (this.mode === 'guild') { if (this.mode === 'guild') {
const member = await this._resolveMemberRecord() const member = await this._resolveMemberRecord()
if (!member) throw new Error('not a guild member')
const customRoles = this.guildRoles && this.guild?.guild const customRoles = this.guildRoles && this.guild?.guild
? await this.guildRoles.listRoles(this.guild.guild.id) ? await this.guildRoles.listRoles(this.guild.guild.id)
: [] : []
const mask = await this._participationMask(channelId) const mask = await this._participationMask(channelId)
const canParticipate = memberCanParticipate(member, customRoles, mask) if (!memberCanConnectVoice(member, customRoles, mask)) {
if (!listenOnly && !canParticipate) { throw new Error('no permission to connect to voice channel')
if (memberCanJoinVoiceListenOnly(member, customRoles)) listenOnly = true
else throw new Error('read-only role cannot join voice')
} }
if (listenOnly) { if (opts.listenOnly) {
if (!memberCanJoinVoiceListenOnly(member, customRoles)) { listenOnly = true
throw new Error('cannot join voice listen-only') } else if (!memberCanSpeakInVoice(member, customRoles, mask)) {
} listenOnly = true
} else { }
await this._assertCanParticipate() if (listenOnly && !memberCanJoinVoiceListenOnly(member, customRoles, mask)) {
throw new Error('cannot join voice listen-only')
} }
} }
const guild = this.guild?.guild const guild = this.guild?.guild
@@ -2609,8 +2611,15 @@ class PearcordPlatform extends EventEmitter {
if (!guild || !this.voice || !user) throw new Error('voice not available') if (!guild || !this.voice || !user) throw new Error('voice not available')
const ch = await this.db.get(COLLECTIONS.CHANNELS, { guildId: guild.id, id: channelId }) const ch = await this.db.get(COLLECTIONS.CHANNELS, { guildId: guild.id, id: channelId })
if (!ch || !isStageChannel(ch)) throw new Error('not a stage channel') if (!ch || !isStageChannel(ch)) throw new Error('not a stage channel')
const roles = await this._memberRoles() const member = await this._resolveMemberRecord()
const isMod = roleHasPermission(roles, PERMISSION.MANAGE_CHANNELS) 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) const row = await this.voice.join(channelId)
this.guild.gossipVoiceState(row) this.guild.gossipVoiceState(row)
const stageRow = await this.stage.setRole({ const stageRow = await this.stage.setRole({
@@ -5077,7 +5086,21 @@ class PearcordPlatform extends EventEmitter {
customRolesForSelf, customRolesForSelf,
partMask 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)) { if (activeChannel && isAnnouncementChannel(activeChannel)) {
canPostInActiveChannel = canPostInActiveChannel && !!myPermissions?.manageMessages canPostInActiveChannel = canPostInActiveChannel && !!myPermissions?.manageMessages
} }
@@ -5162,6 +5185,29 @@ class PearcordPlatform extends EventEmitter {
members: members || [], members: members || [],
contacts: contactsForView 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 { return {
onboarded: this.onboarded, onboarded: this.onboarded,
user, user,
@@ -5197,6 +5243,7 @@ class PearcordPlatform extends EventEmitter {
myPermissions, myPermissions,
canPostInActiveChannel, canPostInActiveChannel,
canJoinVoiceListenOnly, canJoinVoiceListenOnly,
voicePermsByChannel,
pins, pins,
mentionNames: [...mentionNames], mentionNames: [...mentionNames],
channelSettings, channelSettings,