feat(guild): enforce lurker read-only across send, react, and voice

- setMemberRole accepts lurker; _assertCanParticipate guards writes
- canPostInActiveChannel false for lurkers without custom role perms
- Blocks joinVoiceChannel, toggleReaction, and message edits for lurkers

Co-authored-by: Cursor <[email protected]>
This commit is contained in:
Raven Scott
2026-05-22 02:00:12 -04:00
co-authored by Cursor
parent ea21235813
commit 414132f717
+34 -4
View File
@@ -97,6 +97,8 @@ const {
USER_STATUS,
PERMISSION,
roleHasPermission,
memberCanParticipate,
MEMBER_ROLES,
memberHasPermission,
canModifyMessage,
resolveMentionedUserIds,
@@ -1639,8 +1641,12 @@ class PearcordPlatform extends EventEmitter {
throw new Error('no permission to change roles')
}
const next = String(role || '').toLowerCase()
if (next !== 'moderator' && next !== 'member') {
throw new Error('role must be moderator or member')
if (
next !== MEMBER_ROLES.MODERATOR &&
next !== MEMBER_ROLES.MEMBER &&
next !== MEMBER_ROLES.LURKER
) {
throw new Error('role must be moderator, member, or lurker')
}
if (userId === this.guild.guild.ownerId) throw new Error('cannot change owner role')
const member = await this.db.get(COLLECTIONS.MEMBERS, {
@@ -2139,6 +2145,7 @@ class PearcordPlatform extends EventEmitter {
}
async joinVoiceChannel (channelId) {
if (this.mode === 'guild') await this._assertCanParticipate()
const guild = this.guild?.guild
if (!guild || !this.voice) throw new Error('voice not available')
const ch = await this.db.get(COLLECTIONS.CHANNELS, { guildId: guild.id, id: channelId })
@@ -2398,6 +2405,7 @@ class PearcordPlatform extends EventEmitter {
if (msg.authorId !== user.id) throw new Error('cannot modify another user\'s message')
return msg
}
await this._assertCanParticipate()
const member = await this._resolveMemberRecord()
const customRoles = this.guildRoles
? await this.guildRoles.listRoles(this.guild.guild.id)
@@ -3435,6 +3443,7 @@ class PearcordPlatform extends EventEmitter {
async _sendPlainMessage (content, opts = {}) {
if (!this.messages) throw new Error('no channel')
if (this.mode === 'guild') await this._assertCanParticipate()
if (this.mode === 'guild' && this.guild) {
const channels = await this.guild.listChannels()
const ch = channels.find((c) => c.id === this.activeChannelId)
@@ -3951,11 +3960,24 @@ class PearcordPlatform extends EventEmitter {
}
}
async _assertCanParticipate () {
if (this.mode !== 'guild' || !this.guild?.guild) return
const member = await this._resolveMemberRecord()
if (!member) throw new Error('not a guild member')
const customRoles = this.guildRoles
? await this.guildRoles.listRoles(this.guild.guild.id)
: []
if (!memberCanParticipate(member, customRoles)) {
throw new Error('read-only role cannot send messages or reactions')
}
}
async _assertChannelWritable (ch) {
if (!ch) return
if (isThreadChannel(ch) && ch.archived) {
throw new Error('thread is archived')
}
await this._assertCanParticipate()
await this._assertAnnouncementPost(ch)
}
@@ -4217,6 +4239,7 @@ class PearcordPlatform extends EventEmitter {
async toggleReaction (messageId, emoji) {
const user = this.identity.user
if (!user) throw new Error('register first')
if (this.mode === 'guild') await this._assertCanParticipate()
const result = await this.messages.toggleReaction(messageId, emoji, user.id)
if (this.mode !== 'dm' && this.guild) this.guild.gossipReaction(result)
this.emit('reaction', result)
@@ -4559,8 +4582,15 @@ class PearcordPlatform extends EventEmitter {
if (peerId) dmPeerReadAt = dmReadByUser[peerId] || 0
}
let canPostInActiveChannel = true
if (activeChannel && isAnnouncementChannel(activeChannel) && this.mode === 'guild') {
canPostInActiveChannel = !!myPermissions?.manageMessages
if (this.mode === 'guild') {
const selfMember = await this._resolveMemberRecord()
const customRolesForSelf = this.guildRoles && guild
? await this.guildRoles.listRoles(guild.id)
: []
canPostInActiveChannel = memberCanParticipate(selfMember, customRolesForSelf)
if (activeChannel && isAnnouncementChannel(activeChannel)) {
canPostInActiveChannel = canPostInActiveChannel && !!myPermissions?.manageMessages
}
}
let automodConfig = null
if (guild && this.automod) {