feat(platform): Phase 673 member heal, presence dedupe, and audit spans
Extend partition heal with member list and voice occupancy recovery, presence gossip dedupe via _gossipPresenceIfNew, kick span metadata, and member.role.add/remove audit on custom role assignment. Co-authored-by: Cursor <[email protected]>
This commit is contained in:
@@ -606,6 +606,8 @@ class PearcordPlatform extends EventEmitter {
|
||||
this._threadCreateRateByUser = new Map()
|
||||
/** Dedupe thread metadata gossip ingest (Phase 672). */
|
||||
this._threadMetaGossipKeys = new Set()
|
||||
/** Dedupe presence gossip fanout (Phase 673). */
|
||||
this._presenceGossipKeys = new Set()
|
||||
/** Multi-select forum tag filters (Phase 672). */
|
||||
this._forumTagFilters = []
|
||||
}
|
||||
@@ -1550,7 +1552,9 @@ class PearcordPlatform extends EventEmitter {
|
||||
const pinHeal = await this._healPinListOnPartition(gid).catch(() => ({ relisted: 0 }))
|
||||
const searchHeal = await this._healSearchIndexOnPartition(gid).catch(() => ({ relisted: 0 }))
|
||||
const threadHeal = await this._healThreadMetadataOnPartition(gid).catch(() => ({ relisted: 0 }))
|
||||
return { voiceApplied, emojiSlots, automodReconciled, voiceNoteHeal, attachmentHeal, embedHeal, reactionHeal, stickerHeal, pinHeal, searchHeal, threadHeal }
|
||||
const memberHeal = await this._healMemberListOnPartition(gid).catch(() => ({ relisted: 0 }))
|
||||
const voiceOccHeal = await this._healVoiceOccupancyOnPartition(gid).catch(() => ({ relisted: 0 }))
|
||||
return { voiceApplied, emojiSlots, automodReconciled, voiceNoteHeal, attachmentHeal, embedHeal, reactionHeal, stickerHeal, pinHeal, searchHeal, threadHeal, memberHeal, voiceOccHeal }
|
||||
}
|
||||
|
||||
async _healReactionListOnPartition (guildId) {
|
||||
@@ -10980,6 +10984,12 @@ class PearcordPlatform extends EventEmitter {
|
||||
})
|
||||
}
|
||||
|
||||
_gossipPresenceIfNew (payload) {
|
||||
if (!payload?.userId || !this.guild) return
|
||||
if (!this._shouldGossipPresence(payload)) return
|
||||
this.guild.gossipPresence(payload)
|
||||
}
|
||||
|
||||
_ingestPresenceWithProfile (payload) {
|
||||
if (!payload?.userId || !this.presence) return
|
||||
this.presence.ingest(payload)
|
||||
@@ -13366,6 +13376,59 @@ class PearcordPlatform extends EventEmitter {
|
||||
return true
|
||||
}
|
||||
|
||||
_shouldGossipPresence (payload) {
|
||||
if (!payload?.userId) return true
|
||||
const hash = `${payload.userId}:${payload.status || ''}:${payload.at || 0}:${payload.vectorSeq || 0}`
|
||||
if (this._presenceGossipKeys.has(hash)) return false
|
||||
this._presenceGossipKeys.add(hash)
|
||||
if (this._presenceGossipKeys.size > 8192) {
|
||||
const first = this._presenceGossipKeys.values().next().value
|
||||
if (first) this._presenceGossipKeys.delete(first)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
async _healMemberListOnPartition (guildId) {
|
||||
const gid = guildId || this.guild?.guild?.id
|
||||
if (!gid || !this.guild?.listMembers) return { relisted: 0, skipped: true }
|
||||
const span = this.log.time('member.heal', { spanKind: 'member.heal', guildId: gid })
|
||||
try {
|
||||
const members = await this.guild.listMembers().catch(() => [])
|
||||
let relisted = 0
|
||||
for (const mem of members.slice(0, 128)) {
|
||||
if (this.guild?.gossipMemberPage && mem?.userId) {
|
||||
this.guild.gossipMemberPage({ guildId: gid, userId: mem.userId, at: Date.now() })
|
||||
relisted++
|
||||
}
|
||||
}
|
||||
span.end({ relisted, rowCount: members.length })
|
||||
return { relisted, rowCount: members.length }
|
||||
} catch (err) {
|
||||
this.log.error('member.heal error', { guildId: gid, error: err?.message || String(err) })
|
||||
span.fail(err)
|
||||
return { relisted: 0, error: err?.message || String(err) }
|
||||
}
|
||||
}
|
||||
|
||||
async _healVoiceOccupancyOnPartition (guildId) {
|
||||
const gid = guildId || this.guild?.guild?.id
|
||||
if (!gid) return { relisted: 0, skipped: true }
|
||||
const span = this.log.time('voice.heal', { spanKind: 'voice.heal', guildId: gid })
|
||||
try {
|
||||
const rows = this._exportVoiceOccupancyForSync(await this.guild?.listChannels?.().catch(() => []) || [])
|
||||
let relisted = 0
|
||||
if (rows?.length) {
|
||||
relisted = await this._ingestVoiceOccupancyBundle(gid, rows)
|
||||
}
|
||||
span.end({ relisted, rowCount: rows?.length || 0 })
|
||||
return { relisted, rowCount: rows?.length || 0 }
|
||||
} catch (err) {
|
||||
this.log.error('voice.heal error', { guildId: gid, error: err?.message || String(err) })
|
||||
span.fail(err)
|
||||
return { relisted: 0, error: err?.message || String(err) }
|
||||
}
|
||||
}
|
||||
|
||||
async _healThreadMetadataOnPartition (guildId) {
|
||||
const gid = guildId || this.guild?.guild?.id
|
||||
if (!gid || !this.guild?.listChannels) return { relisted: 0, skipped: true }
|
||||
@@ -14141,11 +14204,24 @@ class PearcordPlatform extends EventEmitter {
|
||||
throw new Error('cannot change owner role assignments')
|
||||
}
|
||||
await this._initGuildRoles(this.guild.guild.id)
|
||||
const prevLink = await this.guildRoles.getMemberRoleIds(this.guild.guild.id, userId).catch(() => null)
|
||||
const prevIds = new Set((prevLink?.roleIds || []).map(String))
|
||||
const link = await this.guildRoles.setMemberRoleIds(
|
||||
this.guild.guild.id,
|
||||
userId,
|
||||
roleIds
|
||||
)
|
||||
const nextIds = new Set((roleIds || []).map(String))
|
||||
for (const rid of nextIds) {
|
||||
if (!prevIds.has(rid)) {
|
||||
await this._audit('member.role.add', { guildId: this.guild.guild.id, targetId: userId, roleId: rid })
|
||||
}
|
||||
}
|
||||
for (const rid of prevIds) {
|
||||
if (!nextIds.has(rid)) {
|
||||
await this._audit('member.role.remove', { guildId: this.guild.guild.id, targetId: userId, roleId: rid })
|
||||
}
|
||||
}
|
||||
this.guild.gossipMemberCustomRoles(link)
|
||||
this.emit('guild-member-roles', link)
|
||||
await this._fanoutAppEvent(APP_EVENTS.MEMBER_ROLE_UPDATE, { memberRoles: link })
|
||||
@@ -23478,13 +23554,19 @@ class PearcordPlatform extends EventEmitter {
|
||||
this.guild.gossipKick(payload)
|
||||
this.emit('kick', payload)
|
||||
const memberCount = (await this.guild.listMembers()).length
|
||||
const targetMem = await this.db.get(COLLECTIONS.MEMBERS, {
|
||||
guildId: payload.guildId,
|
||||
userId
|
||||
}).catch(() => null)
|
||||
span.end({
|
||||
userId,
|
||||
guildId,
|
||||
targetName: targetMem?.nickname || targetMem?.displayName || null,
|
||||
hasReason: !!reason,
|
||||
reasonLen: String(reason || '').trim().length,
|
||||
removedMember: true,
|
||||
spanKind: 'kick.member',
|
||||
parentId: this.activeChannelId || null,
|
||||
activeChannelId: this.activeChannelId,
|
||||
memberCount,
|
||||
guildCount: (this.guilds || []).length
|
||||
@@ -25515,7 +25597,7 @@ class PearcordPlatform extends EventEmitter {
|
||||
payload.vectorSeq = vec?.seq
|
||||
}
|
||||
this.emit('presence', payload)
|
||||
if (this.guild) this.guild.gossipPresence(payload)
|
||||
if (this.guild) this._gossipPresenceIfNew(payload)
|
||||
if (this.contacts) {
|
||||
this.contacts.broadcastPresence({
|
||||
userId: payload.userId,
|
||||
@@ -25538,7 +25620,7 @@ class PearcordPlatform extends EventEmitter {
|
||||
})
|
||||
payload.vectorSeq = vec?.seq
|
||||
}
|
||||
if (this.guild) this.guild.gossipPresence(payload)
|
||||
if (this.guild) this._gossipPresenceIfNew(payload)
|
||||
if (this.contacts) {
|
||||
this.contacts.broadcastPresence({
|
||||
userId: base.userId,
|
||||
|
||||
Reference in New Issue
Block a user