feat(platform): Phase 99 scheduled audit export, digest sync, version diff
- Scheduled audit export timer with GUILD_SYNC auditExportSchedule prefs - runScheduledAuditExportNow forces export + gossip (+ digest push) - HOOK_FAILURE_DIGEST_SYNC ingest and pushHookFailureDigestToMesh - getHookFailureDigest merges mesh snapshot via mergeHookFailureDigests Co-authored-by: Cursor <[email protected]>
This commit is contained in:
@@ -45,6 +45,7 @@ const {
|
||||
filterAuditEntries,
|
||||
formatAuditExport,
|
||||
formatHookFailureDigestExport,
|
||||
mergeHookFailureDigests,
|
||||
HookFailureAlertStore
|
||||
} = require('pearcord-delivery-receipts')
|
||||
const {
|
||||
@@ -252,6 +253,7 @@ class PearcordPlatform extends EventEmitter {
|
||||
this.hookDispatcher = null
|
||||
this.hookFailureAlerts = null
|
||||
this.workerReleaseHints = null
|
||||
this._auditScheduleTimer = null
|
||||
this.appVersion = opts.appVersion || process.env.PEARCORD_APP_VERSION || '0.0.0'
|
||||
this.discovery = null
|
||||
this.contacts = null
|
||||
@@ -515,6 +517,7 @@ class PearcordPlatform extends EventEmitter {
|
||||
}
|
||||
|
||||
async _leaveMeshes () {
|
||||
this._stopAuditExportScheduleTimer()
|
||||
await this._leaveVoice({ gossip: true })
|
||||
if (this.guild) await this.guild.leaveMesh().catch(() => {})
|
||||
if (this.dm) await this.dm.leaveMesh().catch(() => {})
|
||||
@@ -1157,6 +1160,26 @@ class PearcordPlatform extends EventEmitter {
|
||||
hookFailurePrefs = await this.deliveryReceipts.exportFailurePrefsSlice()
|
||||
}
|
||||
const auditRecent = await this._exportAuditSyncSlice(guild.id)
|
||||
let auditExportSchedule = null
|
||||
let hookFailureDigest = null
|
||||
await this._initDeliveryReceipts(guild.id)
|
||||
if (this.deliveryReceipts) {
|
||||
auditExportSchedule = await this.deliveryReceipts.exportAuditExportScheduleSlice()
|
||||
const digestSnap = await this.deliveryReceipts.getHookFailureDigestSnapshot()
|
||||
if (digestSnap) {
|
||||
hookFailureDigest = {
|
||||
digest: {
|
||||
windowMs: digestSnap.windowMs,
|
||||
totalFailures: digestSnap.totalFailures,
|
||||
uniqueHooks: digestSnap.uniqueHooks,
|
||||
lastFailureAt: digestSnap.lastFailureAt,
|
||||
topHooks: digestSnap.topHooks
|
||||
},
|
||||
exportedAt: digestSnap.exportedAt,
|
||||
exportedBy: digestSnap.exportedBy
|
||||
}
|
||||
}
|
||||
}
|
||||
let workerReleaseHint = null
|
||||
await this._initWorkerReleaseHints()
|
||||
const hintRow = await this.workerReleaseHints.getHint(guild.id)
|
||||
@@ -1198,6 +1221,8 @@ class PearcordPlatform extends EventEmitter {
|
||||
receiptRetention,
|
||||
hookFailurePrefs,
|
||||
auditRecent,
|
||||
auditExportSchedule,
|
||||
hookFailureDigest,
|
||||
workerReleaseHint,
|
||||
automodConfig: automodConfig
|
||||
? {
|
||||
@@ -1292,6 +1317,21 @@ class PearcordPlatform extends EventEmitter {
|
||||
payload.hookFailurePrefs
|
||||
)
|
||||
}
|
||||
if (payload.auditExportSchedule) {
|
||||
await this._initDeliveryReceipts(payload.guildId)
|
||||
await this.deliveryReceipts.ingestAuditExportScheduleSlice(
|
||||
payload.guildId,
|
||||
payload.auditExportSchedule
|
||||
)
|
||||
}
|
||||
if (payload.hookFailureDigest?.digest) {
|
||||
await this._initDeliveryReceipts(payload.guildId)
|
||||
await this.deliveryReceipts.ingestHookFailureDigestSlice(
|
||||
payload.guildId,
|
||||
payload.hookFailureDigest
|
||||
)
|
||||
this.emit('hook-failure-digest-sync', payload.hookFailureDigest)
|
||||
}
|
||||
for (const row of payload.auditRecent || []) {
|
||||
if (!row?.id || row.guildId !== payload.guildId) continue
|
||||
const existing = await this.db.get(COLLECTIONS.AUDIT_LOG, { id: row.id })
|
||||
@@ -1549,6 +1589,9 @@ class PearcordPlatform extends EventEmitter {
|
||||
guildInstance.on('audit-export-snapshot', (payload) => {
|
||||
this._onAuditExportSnapshotGossip(payload).catch(() => {})
|
||||
})
|
||||
guildInstance.on('hook-failure-digest-sync', (payload) => {
|
||||
this._onHookFailureDigestGossip(payload).catch(() => {})
|
||||
})
|
||||
guildInstance.on('audit', async (p) => {
|
||||
if (p?.guildId && p?.id) {
|
||||
const existing = await this.db.get(COLLECTIONS.AUDIT_LOG, { id: p.id })
|
||||
@@ -2947,6 +2990,7 @@ class PearcordPlatform extends EventEmitter {
|
||||
await this.discovery?.recordGuild(guildRecord).catch(() => {})
|
||||
await this.discovery?.touchGuild(guildRecord.id).catch(() => {})
|
||||
await this._ensureGuildModules()
|
||||
this._startAuditExportScheduleTimer()
|
||||
}
|
||||
|
||||
async createGuild ({ name }) {
|
||||
@@ -2994,6 +3038,7 @@ class PearcordPlatform extends EventEmitter {
|
||||
memberCount: 1
|
||||
}).catch(() => {})
|
||||
await this._ensureGuildModules()
|
||||
this._startAuditExportScheduleTimer()
|
||||
this.emit('guild', result)
|
||||
return result
|
||||
}
|
||||
@@ -3030,6 +3075,7 @@ class PearcordPlatform extends EventEmitter {
|
||||
this.guilds = await this.listGuilds()
|
||||
await this.discovery?.recordGuild(guild).catch(() => {})
|
||||
await this._ensureGuildModules()
|
||||
this._startAuditExportScheduleTimer()
|
||||
return guild
|
||||
}
|
||||
|
||||
@@ -4485,7 +4531,7 @@ class PearcordPlatform extends EventEmitter {
|
||||
return buildDeliveryMetrics(rows, { windowMs: windowMs || 24 * 60 * 60 * 1000 })
|
||||
}
|
||||
|
||||
async getHookFailureDigest (windowMs) {
|
||||
async _computeHookFailureDigest (windowMs) {
|
||||
if (!this.guild?.guild) {
|
||||
return buildHookFailureDigest([], [], { windowMs })
|
||||
}
|
||||
@@ -4497,6 +4543,124 @@ class PearcordPlatform extends EventEmitter {
|
||||
})
|
||||
}
|
||||
|
||||
async getHookFailureDigest (windowMs) {
|
||||
const local = await this._computeHookFailureDigest(windowMs)
|
||||
if (!this.guild?.guild) return local
|
||||
await this._initDeliveryReceipts(this.guild.guild.id)
|
||||
const remote = await this.deliveryReceipts.getHookFailureDigestSnapshot()
|
||||
return mergeHookFailureDigests(local, remote)
|
||||
}
|
||||
|
||||
async pushHookFailureDigestToMesh () {
|
||||
if (!this.guild?.guild) throw new Error('no guild')
|
||||
const roles = await this._memberRoles()
|
||||
if (!roleHasPermission(roles, PERMISSION.MANAGE_GUILD)) {
|
||||
throw new Error('no permission to sync hook failure digest')
|
||||
}
|
||||
const digest = await this._computeHookFailureDigest()
|
||||
await this._initDeliveryReceipts(this.guild.guild.id)
|
||||
await this.deliveryReceipts.setHookFailureDigestSnapshot(
|
||||
digest,
|
||||
this.identity.user?.id
|
||||
)
|
||||
const payload = {
|
||||
guildId: this.guild.guild.id,
|
||||
exportedAt: Date.now(),
|
||||
exportedBy: this.identity.user?.id || null,
|
||||
digest: {
|
||||
windowMs: digest.windowMs,
|
||||
totalFailures: digest.totalFailures,
|
||||
uniqueHooks: digest.uniqueHooks,
|
||||
lastFailureAt: digest.lastFailureAt,
|
||||
topHooks: digest.topHooks
|
||||
}
|
||||
}
|
||||
if (this.guild.gossipHookFailureDigestSync) {
|
||||
this.guild.gossipHookFailureDigestSync(payload)
|
||||
}
|
||||
this.emit('hook-failure-digest-sync', payload)
|
||||
return digest
|
||||
}
|
||||
|
||||
async _onHookFailureDigestGossip (payload) {
|
||||
if (!payload?.guildId || !payload?.digest) return null
|
||||
if (this.guild?.guild?.id !== payload.guildId) return null
|
||||
await this._initDeliveryReceipts(payload.guildId)
|
||||
const row = await this.deliveryReceipts.ingestHookFailureDigestSlice(
|
||||
payload.guildId,
|
||||
payload
|
||||
)
|
||||
if (row) this.emit('hook-failure-digest-sync', payload)
|
||||
return row
|
||||
}
|
||||
|
||||
async getAuditExportSchedule () {
|
||||
if (!this.guild?.guild) {
|
||||
return { enabled: false, intervalHours: 24, filter: 'all', lastExportAt: 0 }
|
||||
}
|
||||
await this._initDeliveryReceipts(this.guild.guild.id)
|
||||
return this.deliveryReceipts.getAuditExportSchedule()
|
||||
}
|
||||
|
||||
async setAuditExportSchedule (partial = {}) {
|
||||
if (!this.guild?.guild) throw new Error('no guild')
|
||||
const roles = await this._memberRoles()
|
||||
if (!roleHasPermission(roles, PERMISSION.MANAGE_GUILD)) {
|
||||
throw new Error('no permission to manage audit export schedule')
|
||||
}
|
||||
await this._initDeliveryReceipts(this.guild.guild.id)
|
||||
const prefs = await this.deliveryReceipts.setAuditExportSchedule(partial)
|
||||
this.emit('audit-export-schedule', prefs)
|
||||
this._startAuditExportScheduleTimer()
|
||||
setTimeout(() => this._pushGuildSyncToMesh().catch(() => {}), 400)
|
||||
return prefs
|
||||
}
|
||||
|
||||
_stopAuditExportScheduleTimer () {
|
||||
if (this._auditScheduleTimer) {
|
||||
clearInterval(this._auditScheduleTimer)
|
||||
this._auditScheduleTimer = null
|
||||
}
|
||||
}
|
||||
|
||||
_startAuditExportScheduleTimer () {
|
||||
this._stopAuditExportScheduleTimer()
|
||||
if (!this.guild?.guild) return
|
||||
this._auditScheduleTimer = setInterval(() => {
|
||||
this._runScheduledAuditExportIfDue().catch(() => {})
|
||||
}, 60 * 1000)
|
||||
this._runScheduledAuditExportIfDue().catch(() => {})
|
||||
}
|
||||
|
||||
async runScheduledAuditExportNow () {
|
||||
return this._runScheduledAuditExportIfDue(true)
|
||||
}
|
||||
|
||||
async _runScheduledAuditExportIfDue (force = false) {
|
||||
if (!this.guild?.guild) return null
|
||||
const roles = await this._memberRoles()
|
||||
if (!roleHasPermission(roles, PERMISSION.MANAGE_GUILD)) return null
|
||||
await this._initDeliveryReceipts(this.guild.guild.id)
|
||||
const schedule = await this.deliveryReceipts.getAuditExportSchedule()
|
||||
if (!schedule.enabled && !force) return null
|
||||
const intervalMs = (schedule.intervalHours || 24) * 60 * 60 * 1000
|
||||
const last = schedule.lastExportAt || 0
|
||||
if (!force && Date.now() - last < intervalMs) return null
|
||||
const out = await this.exportAuditLog({
|
||||
format: 'json',
|
||||
filter: schedule.filter,
|
||||
limit: 200,
|
||||
gossip: true
|
||||
})
|
||||
await this.deliveryReceipts.setAuditExportSchedule({ lastExportAt: out.exportedAt })
|
||||
const digest = await this._computeHookFailureDigest()
|
||||
if (digest.totalFailures > 0) {
|
||||
await this.pushHookFailureDigestToMesh().catch(() => {})
|
||||
}
|
||||
this.emit('audit-export-scheduled', { ...out, schedule })
|
||||
return out
|
||||
}
|
||||
|
||||
async _initWorkerReleaseHints () {
|
||||
if (!this.workerReleaseHints) {
|
||||
this.workerReleaseHints = new WorkerReleaseHintStore({
|
||||
@@ -6033,9 +6197,11 @@ class PearcordPlatform extends EventEmitter {
|
||||
hookFailureAlerts = await this.listHookFailureAlerts(8)
|
||||
}
|
||||
let hookFailureDigest = null
|
||||
let auditExportSchedule = null
|
||||
let workerReleaseHint = null
|
||||
if (guild) {
|
||||
hookFailureDigest = await this.getHookFailureDigest()
|
||||
auditExportSchedule = await this.getAuditExportSchedule()
|
||||
workerReleaseHint = await this.getWorkerReleaseHint()
|
||||
}
|
||||
const auditLogFilterOptions = [
|
||||
@@ -6199,6 +6365,7 @@ class PearcordPlatform extends EventEmitter {
|
||||
hookFailurePrefs,
|
||||
hookFailureAlerts,
|
||||
hookFailureDigest,
|
||||
auditExportSchedule,
|
||||
workerReleaseHint,
|
||||
auditLogFilterOptions,
|
||||
appVersion: this.appVersion,
|
||||
|
||||
Reference in New Issue
Block a user