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.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.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`. **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 } = {}) { async createGuildCustomRole ({ name, color, permissions } = {}) {
if (!this.guild?.guild) throw new Error('no guild') const span = this.log.time('role.create', { name: String(name || '').slice(0, 32) })
if (!(await this._hasPerm(PERMISSION.MANAGE_GUILD))) { try {
throw new Error('no permission to manage roles') 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 = {}) { async updateGuildCustomRole (roleId, patch = {}) {
if (!this.guild?.guild) throw new Error('no guild') const span = this.log.time('role.update', { roleId })
if (!(await this._hasPerm(PERMISSION.MANAGE_GUILD))) { try {
throw new Error('no permission to manage roles') 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) { async deleteGuildCustomRole (roleId) {
if (!this.guild?.guild) throw new Error('no guild') const span = this.log.time('role.delete', { roleId })
if (!(await this._hasPerm(PERMISSION.MANAGE_GUILD))) { try {
throw new Error('no permission to manage roles') 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 = []) { async reorderGuildCustomRoles (roleIds = []) {
if (!this.guild?.guild) throw new Error('no guild') const span = this.log.time('role.reorder', { count: (roleIds || []).length })
if (!(await this._hasPerm(PERMISSION.MANAGE_GUILD))) { try {
throw new Error('no permission to manage roles') 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 = []) { async setMemberCustomRoles (userId, roleIds = []) {
if (!this.guild?.guild) throw new Error('no guild') const span = this.log.time('role.member', { userId })
if (!(await this._hasPerm(PERMISSION.MANAGE_GUILD))) { try {
throw new Error('no permission to manage roles') 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) { async getChannelSettings (channelId, guildId) {
@@ -5132,69 +5187,96 @@ class PearcordPlatform extends EventEmitter {
} }
async upsertChannelPermissionOverwrite (channelId, payload = {}) { async upsertChannelPermissionOverwrite (channelId, payload = {}) {
if (!this.guild?.guild) throw new Error('no guild') const span = this.log.time('permission.overwrite', {
if (!(await this._hasPerm(PERMISSION.MANAGE_CHANNELS))) { channelId: channelId || this.activeChannelId,
throw new Error('no permission to edit channel permissions') targetType: payload.targetType
}
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') try {
await this._initChannelPermissions(this.guild.guild.id) if (!this.guild?.guild) throw new Error('no guild')
const allow = if (!(await this._hasPerm(PERMISSION.MANAGE_CHANNELS))) {
payload.allow !== undefined throw new Error('no permission to edit channel permissions')
? 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
} }
) const chId = channelId || this.activeChannelId
this.guild.gossipChannelOverwriteUpsert(row) if (!chId) throw new Error('channelId required')
await this._audit('channel.overwrite', { guildId: this.guild.guild.id, targetId: chId }) const ch = await this.db.get(COLLECTIONS.CHANNELS, {
this.emit('channel-overwrite', row) guildId: this.guild.guild.id,
await this._fanoutAppEvent(APP_EVENTS.CHANNEL_PERMISSION_UPDATE, { overwrite: row }) id: chId
return row })
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) { async deleteChannelPermissionOverwrite (channelId, targetType, targetId) {
if (!this.guild?.guild) throw new Error('no guild') const span = this.log.time('permission.overwrite.delete', { channelId, targetType, targetId })
if (!(await this._hasPerm(PERMISSION.MANAGE_CHANNELS))) { try {
throw new Error('no permission to edit channel permissions') if (!this.guild?.guild) throw new Error('no guild')
} if (!(await this._hasPerm(PERMISSION.MANAGE_CHANNELS))) {
const chId = channelId || this.activeChannelId throw new Error('no permission to edit channel permissions')
await this._initChannelPermissions(this.guild.guild.id) }
const prev = await this.channelPermissions.deleteOverwrite( const chId = channelId || this.activeChannelId
this.guild.guild.id, await this._initChannelPermissions(this.guild.guild.id)
chId, const prev = await this.channelPermissions.deleteOverwrite(
targetType, this.guild.guild.id,
targetId chId,
) targetType,
if (prev && this.guild) { targetId
this.guild.gossipChannelOverwriteDelete({ )
guildId: this.guild.guild.id, if (prev && this.guild) {
channelId: chId, this.guild.gossipChannelOverwriteDelete({
targetType: prev.targetType, guildId: this.guild.guild.id,
targetId: prev.targetId 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) { async _assertVoiceCapacity (channelId) {