feat(platform): per-channel permission overwrites (Phase 82)

Integrate pearcord-channel-permissions: list/upsert/delete overwrites,
channel-effective _hasPerm/_myPermissions, GUILD_SYNC bundle, and mesh
ingest handlers. Requires MANAGE_CHANNELS on the active channel to edit.
This commit is contained in:
Raven Scott
2026-05-22 02:56:26 -04:00
parent 0513f4df88
commit 30f27c515a
2 changed files with 138 additions and 4 deletions
+137 -4
View File
@@ -131,6 +131,12 @@ const {
now now
} = require('pearcord-shared') } = require('pearcord-shared')
const { PearcordGuildRoles, PERMISSION_META } = require('pearcord-guild-roles') const { PearcordGuildRoles, PERMISSION_META } = require('pearcord-guild-roles')
const {
PearcordChannelPermissions,
memberHasChannelPermission,
effectivePermissionView
} = require('pearcord-channel-permissions')
const { viewToMask } = require('pearcord-guild-roles/permissions')
const { maskToPermissionView, memberPermissionMask } = require('pearcord-guild-roles/permissions') const { maskToPermissionView, memberPermissionMask } = require('pearcord-guild-roles/permissions')
function messageLayoutKind (m) { function messageLayoutKind (m) {
@@ -197,6 +203,7 @@ class PearcordPlatform extends EventEmitter {
this.stickerRegistry = null this.stickerRegistry = null
this.soundboardRegistry = null this.soundboardRegistry = null
this.guildRoles = null this.guildRoles = null
this.channelPermissions = null
this._recentSoundPlays = [] this._recentSoundPlays = []
this.botEvents = null this.botEvents = null
this.discovery = null this.discovery = null
@@ -1074,6 +1081,11 @@ class PearcordPlatform extends EventEmitter {
await this._initGuildRoles(guild.id) await this._initGuildRoles(guild.id)
if (this.guildRoles) roleBundle = await this.guildRoles.exportSyncBundle(guild.id) if (this.guildRoles) roleBundle = await this.guildRoles.exportSyncBundle(guild.id)
} }
let permBundle = { channelOverwrites: [] }
await this._initChannelPermissions(guild.id)
if (this.channelPermissions) {
permBundle = await this.channelPermissions.exportSyncBundle(guild.id)
}
if (guild.iconHash && this.attachments) { if (guild.iconHash && this.attachments) {
const iconRow = await this.attachments.get(guild.iconHash).catch(() => null) const iconRow = await this.attachments.get(guild.iconHash).catch(() => null)
if (iconRow && !attachments.some((a) => a.id === iconRow.id)) { if (iconRow && !attachments.some((a) => a.id === iconRow.id)) {
@@ -1093,7 +1105,8 @@ class PearcordPlatform extends EventEmitter {
attachments, attachments,
members, members,
guildRoles: roleBundle.guildRoles, guildRoles: roleBundle.guildRoles,
memberRoleLinks: roleBundle.memberRoleLinks memberRoleLinks: roleBundle.memberRoleLinks,
channelOverwrites: permBundle.channelOverwrites
} }
} }
@@ -1128,6 +1141,10 @@ class PearcordPlatform extends EventEmitter {
guildRoles: payload.guildRoles, guildRoles: payload.guildRoles,
memberRoleLinks: payload.memberRoleLinks memberRoleLinks: payload.memberRoleLinks
}) })
await this._initChannelPermissions(payload.guildId)
await this.channelPermissions.ingestSyncBundle(payload.guildId, {
channelOverwrites: payload.channelOverwrites
})
} }
for (const mem of payload.members || []) { for (const mem of payload.members || []) {
if (!mem?.guildId || !mem?.userId) continue if (!mem?.guildId || !mem?.userId) continue
@@ -1303,6 +1320,14 @@ class PearcordPlatform extends EventEmitter {
this.guildRoles?.ingestMemberRolesGossip(payload).catch(() => {}) this.guildRoles?.ingestMemberRolesGossip(payload).catch(() => {})
this.emit('guild-member-roles', payload) this.emit('guild-member-roles', payload)
}) })
guildInstance.on('channel-overwrite', (row) => {
this.channelPermissions?.ingestOverwriteGossip(row).catch(() => {})
this.emit('channel-overwrite', row)
})
guildInstance.on('channel-overwrite-delete', (payload) => {
this.channelPermissions?.ingestOverwriteDelete(payload).catch(() => {})
this.emit('channel-overwrite-delete', payload)
})
guildInstance.on('bot-event', (payload) => { guildInstance.on('bot-event', (payload) => {
this.botEvents?.ingest(payload) this.botEvents?.ingest(payload)
this.emit('bot-event', payload) this.emit('bot-event', payload)
@@ -1915,6 +1940,76 @@ class PearcordPlatform extends EventEmitter {
return settings return settings
} }
async listChannelPermissionOverwrites (channelId) {
if (!this.guild?.guild) throw new Error('no guild')
const chId = channelId || this.activeChannelId
if (!chId) throw new Error('channelId required')
await this._initChannelPermissions(this.guild.guild.id)
return this.channelPermissions.listForChannel(this.guild.guild.id, chId)
}
async upsertChannelPermissionOverwrite (channelId, payload = {}) {
if (!this.guild?.guild) throw new Error('no guild')
if (!(await this._hasPerm(PERMISSION.MANAGE_CHANNELS))) {
throw new Error('no permission to edit channel permissions')
}
const chId = channelId || this.activeChannelId
if (!chId) throw new Error('channelId required')
const ch = await this.db.get(COLLECTIONS.CHANNELS, {
guildId: this.guild.guild.id,
id: chId
})
if (!ch) throw new Error('channel not found')
await this._initChannelPermissions(this.guild.guild.id)
const allow =
payload.allow !== undefined
? Number(payload.allow) || 0
: viewToMask(payload.allowView || {})
const deny =
payload.deny !== undefined
? Number(payload.deny) || 0
: viewToMask(payload.denyView || {})
const row = await this.channelPermissions.upsertOverwrite(
this.guild.guild.id,
chId,
{
targetType: payload.targetType,
targetId: payload.targetId,
allow,
deny
}
)
this.guild.gossipChannelOverwriteUpsert(row)
await this._audit('channel.overwrite', { guildId: this.guild.guild.id, targetId: chId })
this.emit('channel-overwrite', row)
return row
}
async deleteChannelPermissionOverwrite (channelId, targetType, targetId) {
if (!this.guild?.guild) throw new Error('no guild')
if (!(await this._hasPerm(PERMISSION.MANAGE_CHANNELS))) {
throw new Error('no permission to edit channel permissions')
}
const chId = channelId || this.activeChannelId
await this._initChannelPermissions(this.guild.guild.id)
const prev = await this.channelPermissions.deleteOverwrite(
this.guild.guild.id,
chId,
targetType,
targetId
)
if (prev && this.guild) {
this.guild.gossipChannelOverwriteDelete({
guildId: this.guild.guild.id,
channelId: chId,
targetType: prev.targetType,
targetId: prev.targetId
})
}
this.emit('channel-overwrite-delete', { channelId: chId, targetType, targetId })
return prev
}
async _assertSlowmodeFor (channelId) { async _assertSlowmodeFor (channelId) {
const chId = channelId || this.activeChannelId const chId = channelId || this.activeChannelId
const user = this.identity.user const user = this.identity.user
@@ -2002,6 +2097,28 @@ class PearcordPlatform extends EventEmitter {
return this.guildRoles return this.guildRoles
} }
async _initChannelPermissions (guildId) {
const gid = guildId || this.guild?.guild?.id
if (!gid) return null
if (!this.channelPermissions || this.channelPermissions.guildId !== gid) {
this.channelPermissions = new PearcordChannelPermissions({
storagePath: this.storagePath,
guildId: gid
})
await this.channelPermissions.ready()
} else {
this.channelPermissions.setGuild(gid)
}
return this.channelPermissions
}
async _channelOverwrites (channelId) {
if (!this.guild?.guild || !channelId) return []
await this._initChannelPermissions(this.guild.guild.id)
if (!this.channelPermissions) return []
return this.channelPermissions.listForChannel(this.guild.guild.id, channelId)
}
async _initBotEvents (guildId) { async _initBotEvents (guildId) {
this.botEvents = new BotEventFanout({ this.botEvents = new BotEventFanout({
guildId, guildId,
@@ -2614,16 +2731,21 @@ class PearcordPlatform extends EventEmitter {
return { ...member, customRoleIds } return { ...member, customRoleIds }
} }
async _hasPerm (perm) { async _hasPerm (perm, channelId) {
const member = await this._resolveMemberRecord() const member = await this._resolveMemberRecord()
if (!member) return false if (!member) return false
const customRoles = this.guildRoles const customRoles = this.guildRoles
? await this.guildRoles.listRoles(this.guild.guild.id) ? await this.guildRoles.listRoles(this.guild.guild.id)
: [] : []
const chId = channelId || this.activeChannelId
if (chId && this.mode === 'guild') {
const overwrites = await this._channelOverwrites(chId)
return memberHasChannelPermission(member, customRoles, overwrites, perm)
}
return memberHasPermission(member, customRoles, perm) return memberHasPermission(member, customRoles, perm)
} }
async _myPermissions () { async _myPermissions (channelId) {
const member = await this._resolveMemberRecord() const member = await this._resolveMemberRecord()
if (!member) { if (!member) {
return { return {
@@ -2637,6 +2759,11 @@ class PearcordPlatform extends EventEmitter {
const customRoles = this.guildRoles const customRoles = this.guildRoles
? await this.guildRoles.listRoles(this.guild.guild.id) ? await this.guildRoles.listRoles(this.guild.guild.id)
: [] : []
const chId = channelId || this.activeChannelId
if (chId && this.mode === 'guild') {
const overwrites = await this._channelOverwrites(chId)
return effectivePermissionView(member, customRoles, overwrites)
}
return maskToPermissionView(memberPermissionMask(member, customRoles)) return maskToPermissionView(memberPermissionMask(member, customRoles))
} }
@@ -4610,7 +4737,12 @@ class PearcordPlatform extends EventEmitter {
const stats = this.mode === 'dm' const stats = this.mode === 'dm'
? this.dm?.getStats() || null ? this.dm?.getStats() || null
: this.guild?.getStats() || null : this.guild?.getStats() || null
const myPermissions = this.mode === 'guild' ? await this._myPermissions() : null const myPermissions =
this.mode === 'guild' ? await this._myPermissions(this.activeChannelId) : null
let activeChannelOverwrites = []
if (this.mode === 'guild' && guild && this.activeChannelId) {
activeChannelOverwrites = await this._channelOverwrites(this.activeChannelId)
}
let pins = [] let pins = []
const mentionNames = new Set() const mentionNames = new Set()
if (user) { if (user) {
@@ -4996,6 +5128,7 @@ class PearcordPlatform extends EventEmitter {
dmReadByUser, dmReadByUser,
activeChannelId: this.activeChannelId, activeChannelId: this.activeChannelId,
activeChannelSummary, activeChannelSummary,
activeChannelOverwrites,
messages, messages,
messageById, messageById,
reactions, reactions,
+1
View File
@@ -31,6 +31,7 @@
"pearcord-commands": "file:../pearcord-commands", "pearcord-commands": "file:../pearcord-commands",
"pearcord-emoji": "file:../pearcord-emoji", "pearcord-emoji": "file:../pearcord-emoji",
"pearcord-guild-roles": "file:../pearcord-guild-roles", "pearcord-guild-roles": "file:../pearcord-guild-roles",
"pearcord-channel-permissions": "file:../pearcord-channel-permissions",
"pearcord-bot-events": "file:../pearcord-bot-events", "pearcord-bot-events": "file:../pearcord-bot-events",
"pearcord-bot-sdk": "file:../pearcord-bot-sdk", "pearcord-bot-sdk": "file:../pearcord-bot-sdk",
"pearcord-bot-limits": "file:../pearcord-bot-limits", "pearcord-bot-limits": "file:../pearcord-bot-limits",