Phase 438: structured role and permission overwrite error logging (v0.8.401).

Wrap createGuildCustomRole, updateGuildCustomRole, deleteGuildCustomRole,
reorderGuildCustomRoles, setMemberCustomRoles, upsertChannelPermissionOverwrite,
and deleteChannelPermissionOverwrite with timed spans and structured error logs.
This commit is contained in:
Raven Scott
2026-05-23 18:45:47 -04:00
parent 2fa3ab097e
commit 1d00c2311c
2 changed files with 204 additions and 120 deletions
+2
View File
@@ -143,6 +143,8 @@ Primary consumer: `apps/pearcord/index.js` sidecar reading pear-pipe JSON.
**v0.8.400:** `createChannelWebhook`/`deleteChannelWebhook`/`executeChannelWebhook` log `webhook.*` spans and errors; `registerSlashCommands`/`deleteSlashCommand`/`executeSlashCommand` log `slash.*` spans and errors.
**v0.8.401:** `createGuildCustomRole`/`updateGuildCustomRole`/`deleteGuildCustomRole`/`reorderGuildCustomRoles`/`setMemberCustomRoles` log `role.*` spans and errors; `upsertChannelPermissionOverwrite`/`deleteChannelPermissionOverwrite` log `permission.overwrite*` spans and errors.
**v0.8.252:** Guild search mesh skipped when `guildOpenNoViewableChannels`; emoji batch prefetch skips negative miss TTL; sticker gossip prefetch gated on `attachments`; `session.startup` logs INFO when initial guild mesh flush was bounded. Smoke: `test:guild-search-no-viewable-skip`, `test:sticker-prefetch-attachments-ready`.
**v0.8.248:** Guild open filters unread/last picks with `_canViewChannel`; sets `guildOpenNoViewableChannels` when none accessible; defers `_ensureGuildModules` via `setTimeout(0)` after channel pick; emoji mesh prefetch clears miss cache on success. Smokes: `test:guild-viewable-channel-pick`, `test:guild-open-no-viewable`, `test:guild-plain-beats-dual-combo`.
+202 -120
View File
@@ -4896,87 +4896,142 @@ class PearcordPlatform extends EventEmitter {
}
async createGuildCustomRole ({ name, color, permissions } = {}) {
if (!this.guild?.guild) throw new Error('no guild')
if (!(await this._hasPerm(PERMISSION.MANAGE_GUILD))) {
throw new Error('no permission to manage roles')
const span = this.log.time('role.create', { name: String(name || '').slice(0, 32) })
try {
if (!this.guild?.guild) throw new Error('no guild')
if (!(await this._hasPerm(PERMISSION.MANAGE_GUILD))) {
throw new Error('no permission to manage roles')
}
await this._initGuildRoles(this.guild.guild.id)
const row = await this.guildRoles.createRole(this.guild.guild.id, {
name,
color,
permissions
})
this.guild.gossipRoleUpsert(row)
await this._audit('role.create', {
guildId: this.guild.guild.id,
targetId: row.id
})
this.emit('guild-role', row)
span.end({ roleId: row.id })
return row
} catch (err) {
this.log.error('role.create error', {
name: String(name || '').slice(0, 32),
error: err?.message || String(err)
})
span.fail(err)
throw err
}
await this._initGuildRoles(this.guild.guild.id)
const row = await this.guildRoles.createRole(this.guild.guild.id, {
name,
color,
permissions
})
this.guild.gossipRoleUpsert(row)
await this._audit('role.create', {
guildId: this.guild.guild.id,
targetId: row.id
})
this.emit('guild-role', row)
return row
}
async updateGuildCustomRole (roleId, patch = {}) {
if (!this.guild?.guild) throw new Error('no guild')
if (!(await this._hasPerm(PERMISSION.MANAGE_GUILD))) {
throw new Error('no permission to manage roles')
const span = this.log.time('role.update', { roleId })
try {
if (!this.guild?.guild) throw new Error('no guild')
if (!(await this._hasPerm(PERMISSION.MANAGE_GUILD))) {
throw new Error('no permission to manage roles')
}
await this._initGuildRoles(this.guild.guild.id)
const row = await this.guildRoles.updateRole(this.guild.guild.id, roleId, patch)
this.guild.gossipRoleUpsert(row)
this.emit('guild-role', row)
span.end({ roleId: row?.id || roleId })
return row
} catch (err) {
this.log.error('role.update error', {
roleId,
error: err?.message || String(err)
})
span.fail(err)
throw err
}
await this._initGuildRoles(this.guild.guild.id)
const row = await this.guildRoles.updateRole(this.guild.guild.id, roleId, patch)
this.guild.gossipRoleUpsert(row)
this.emit('guild-role', row)
return row
}
async deleteGuildCustomRole (roleId) {
if (!this.guild?.guild) throw new Error('no guild')
if (!(await this._hasPerm(PERMISSION.MANAGE_GUILD))) {
throw new Error('no permission to manage roles')
const span = this.log.time('role.delete', { roleId })
try {
if (!this.guild?.guild) throw new Error('no guild')
if (!(await this._hasPerm(PERMISSION.MANAGE_GUILD))) {
throw new Error('no permission to manage roles')
}
await this._initGuildRoles(this.guild.guild.id)
const prev = await this.guildRoles.deleteRole(this.guild.guild.id, roleId)
if (prev) {
this.guild.gossipRoleDelete({ guildId: this.guild.guild.id, id: roleId })
this.emit('guild-role-delete', { guildId: this.guild.guild.id, id: roleId })
}
span.end({ roleId })
return prev
} catch (err) {
this.log.error('role.delete error', {
roleId,
error: err?.message || String(err)
})
span.fail(err)
throw err
}
await this._initGuildRoles(this.guild.guild.id)
const prev = await this.guildRoles.deleteRole(this.guild.guild.id, roleId)
if (prev) {
this.guild.gossipRoleDelete({ guildId: this.guild.guild.id, id: roleId })
this.emit('guild-role-delete', { guildId: this.guild.guild.id, id: roleId })
}
return prev
}
async reorderGuildCustomRoles (roleIds = []) {
if (!this.guild?.guild) throw new Error('no guild')
if (!(await this._hasPerm(PERMISSION.MANAGE_GUILD))) {
throw new Error('no permission to manage roles')
const span = this.log.time('role.reorder', { count: (roleIds || []).length })
try {
if (!this.guild?.guild) throw new Error('no guild')
if (!(await this._hasPerm(PERMISSION.MANAGE_GUILD))) {
throw new Error('no permission to manage roles')
}
await this._initGuildRoles(this.guild.guild.id)
const rows = await this.guildRoles.reorderRoles(this.guild.guild.id, roleIds)
for (const row of rows) {
if (row.updatedAt) this.guild.gossipRoleUpsert(row)
}
this.emit('guild-role-reorder', { guildId: this.guild.guild.id, roleIds })
span.end({ count: rows.length })
return rows
} catch (err) {
this.log.error('role.reorder error', {
count: (roleIds || []).length,
error: err?.message || String(err)
})
span.fail(err)
throw err
}
await this._initGuildRoles(this.guild.guild.id)
const rows = await this.guildRoles.reorderRoles(this.guild.guild.id, roleIds)
for (const row of rows) {
if (row.updatedAt) this.guild.gossipRoleUpsert(row)
}
this.emit('guild-role-reorder', { guildId: this.guild.guild.id, roleIds })
return rows
}
async setMemberCustomRoles (userId, roleIds = []) {
if (!this.guild?.guild) throw new Error('no guild')
if (!(await this._hasPerm(PERMISSION.MANAGE_GUILD))) {
throw new Error('no permission to manage roles')
const span = this.log.time('role.member', { userId })
try {
if (!this.guild?.guild) throw new Error('no guild')
if (!(await this._hasPerm(PERMISSION.MANAGE_GUILD))) {
throw new Error('no permission to manage roles')
}
if (userId === this.guild.guild.ownerId) {
throw new Error('cannot change owner role assignments')
}
await this._initGuildRoles(this.guild.guild.id)
const link = await this.guildRoles.setMemberRoleIds(
this.guild.guild.id,
userId,
roleIds
)
this.guild.gossipMemberCustomRoles(link)
this.emit('guild-member-roles', link)
await this._fanoutAppEvent(APP_EVENTS.MEMBER_ROLE_UPDATE, { memberRoles: link })
this.emit('member', await this.db.get(COLLECTIONS.MEMBERS, {
guildId: this.guild.guild.id,
userId
}))
span.end({ userId, roleCount: (roleIds || []).length })
return link
} catch (err) {
this.log.error('role.member error', {
userId,
error: err?.message || String(err)
})
span.fail(err)
throw err
}
if (userId === this.guild.guild.ownerId) {
throw new Error('cannot change owner role assignments')
}
await this._initGuildRoles(this.guild.guild.id)
const link = await this.guildRoles.setMemberRoleIds(
this.guild.guild.id,
userId,
roleIds
)
this.guild.gossipMemberCustomRoles(link)
this.emit('guild-member-roles', link)
await this._fanoutAppEvent(APP_EVENTS.MEMBER_ROLE_UPDATE, { memberRoles: link })
this.emit('member', await this.db.get(COLLECTIONS.MEMBERS, {
guildId: this.guild.guild.id,
userId
}))
return link
}
async getChannelSettings (channelId, guildId) {
@@ -5132,69 +5187,96 @@ class PearcordPlatform extends EventEmitter {
}
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
const span = this.log.time('permission.overwrite', {
channelId: channelId || this.activeChannelId,
targetType: payload.targetType
})
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
try {
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')
}
)
this.guild.gossipChannelOverwriteUpsert(row)
await this._audit('channel.overwrite', { guildId: this.guild.guild.id, targetId: chId })
this.emit('channel-overwrite', row)
await this._fanoutAppEvent(APP_EVENTS.CHANNEL_PERMISSION_UPDATE, { overwrite: row })
return row
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)
await this._fanoutAppEvent(APP_EVENTS.CHANNEL_PERMISSION_UPDATE, { overwrite: row })
span.end({ channelId: chId, targetId: payload.targetId })
return row
} catch (err) {
this.log.error('permission.overwrite error', {
channelId: channelId || this.activeChannelId,
error: err?.message || String(err)
})
span.fail(err)
throw err
}
}
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
const span = this.log.time('permission.overwrite.delete', { channelId, targetType, targetId })
try {
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 })
await this._fanoutAppEvent(APP_EVENTS.CHANNEL_PERMISSION_DELETE, {
delete: { channelId: chId, targetType, targetId }
})
span.end({ channelId: chId, targetId })
return prev
} catch (err) {
this.log.error('permission.overwrite.delete error', {
channelId,
targetType,
targetId,
error: err?.message || String(err)
})
span.fail(err)
throw err
}
this.emit('channel-overwrite-delete', { channelId: chId, targetType, targetId })
await this._fanoutAppEvent(APP_EVENTS.CHANNEL_PERMISSION_DELETE, {
delete: { channelId: chId, targetType, targetId }
})
return prev
}
async _assertVoiceCapacity (channelId) {