feat(phase710): custom status CSV & guild widget export mesh parity (v0.8.685)
Complete Phase 710 with RPC 138/139 custom status and guild widget CSV mesh exports. Wire status-csv-ui.js modal, platform mixin, ui-flow IPC, agentctl journeys, smokes, and app.js integration. Fix syntax errors in CSV UI helpers from phases 707–710 (presence, notification, screen, stage, thread, voice, status). Restore app.js after accidental truncation; add showStatusCsvMeshModal, deep links, dev-log filters, and settings entry. Document in CUSTOM_STATUS_CSV.md, release notes, and roadmap; open Phase 711 activity invite + profile banner CSV.
This commit is contained in:
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
Application facade: one `PearcordPlatform` class that wires identity, database, guild mesh, DMs, invites, voice, discovery, bots, and dozens of feature modules for the Pearcord desktop sidecar and companion.
|
Application facade: one `PearcordPlatform` class that wires identity, database, guild mesh, DMs, invites, voice, discovery, bots, and dozens of feature modules for the Pearcord desktop sidecar and companion.
|
||||||
|
|
||||||
|
**Phase 710 (v0.8.685):** Custom status CSV & guild widget export — `status-csv-mixin.js`, `status.csv` spans (`guildStatusCsvCount`), `pushCustomStatusCsvToMesh`, `pushGuildWidgetExportToMesh`, deep link `openStatusCsv`. Bundle: `npm run test:ci-phase710`.
|
||||||
|
|
||||||
**Phase 709 (v0.8.684):** Notification override CSV & channel follow export — `notification-csv-mixin.js`, `notification.csv` spans (`guildNotificationCsvCount`), `pushNotificationOverrideCsvToMesh`, `pushChannelFollowExportToMesh`, deep link `openNotificationCsv`. Bundle: `npm run test:ci-phase709`.
|
**Phase 709 (v0.8.684):** Notification override CSV & channel follow export — `notification-csv-mixin.js`, `notification.csv` spans (`guildNotificationCsvCount`), `pushNotificationOverrideCsvToMesh`, `pushChannelFollowExportToMesh`, deep link `openNotificationCsv`. Bundle: `npm run test:ci-phase709`.
|
||||||
|
|
||||||
**Phase 708 (v0.8.683):** Presence activity CSV & read-state export — `presence-csv-mixin.js`, `presence.csv` spans (`guildPresenceCsvCount`), `pushPresenceActivityCsvToMesh`, `pushReadStateExportToMesh`, deep link `openPresenceCsv`. Bundle: `npm run test:ci-phase708`.
|
**Phase 708 (v0.8.683):** Presence activity CSV & read-state export — `presence-csv-mixin.js`, `presence.csv` spans (`guildPresenceCsvCount`), `pushPresenceActivityCsvToMesh`, `pushReadStateExportToMesh`, deep link `openPresenceCsv`. Bundle: `npm run test:ci-phase708`.
|
||||||
|
|||||||
@@ -182,6 +182,12 @@ const {
|
|||||||
buildChannelFollowExportCsvBody,
|
buildChannelFollowExportCsvBody,
|
||||||
signChannelFollowExportCsv,
|
signChannelFollowExportCsv,
|
||||||
verifyChannelFollowExportCsv,
|
verifyChannelFollowExportCsv,
|
||||||
|
buildCustomStatusCsvBody,
|
||||||
|
signCustomStatusCsv,
|
||||||
|
verifyCustomStatusCsv,
|
||||||
|
buildGuildWidgetExportCsvBody,
|
||||||
|
signGuildWidgetExportCsv,
|
||||||
|
verifyGuildWidgetExportCsv,
|
||||||
formatAutomationScheduleDigestExport,
|
formatAutomationScheduleDigestExport,
|
||||||
mergeHookFailureDigests,
|
mergeHookFailureDigests,
|
||||||
buildAutomationScheduleDashboard,
|
buildAutomationScheduleDashboard,
|
||||||
@@ -11165,6 +11171,12 @@ class PearcordPlatform extends EventEmitter {
|
|||||||
guildInstance.on('channel-follow-export-csv-sync', (payload) => {
|
guildInstance.on('channel-follow-export-csv-sync', (payload) => {
|
||||||
this._onChannelFollowExportCsvGossip(payload).catch(() => {})
|
this._onChannelFollowExportCsvGossip(payload).catch(() => {})
|
||||||
})
|
})
|
||||||
|
guildInstance.on('custom-status-csv-sync', (payload) => {
|
||||||
|
this._onCustomStatusCsvGossip(payload).catch(() => {})
|
||||||
|
})
|
||||||
|
guildInstance.on('guild-widget-export-csv-sync', (payload) => {
|
||||||
|
this._onGuildWidgetExportCsvGossip(payload).catch(() => {})
|
||||||
|
})
|
||||||
guildInstance.on('message-search-request', (payload) => {
|
guildInstance.on('message-search-request', (payload) => {
|
||||||
this._onMessageSearchRequestGossip(payload).catch(() => {})
|
this._onMessageSearchRequestGossip(payload).catch(() => {})
|
||||||
})
|
})
|
||||||
@@ -26296,6 +26308,190 @@ class PearcordPlatform extends EventEmitter {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
async _statusCsvRegistryEntries () {
|
||||||
|
if (!this.guild?.guild) return []
|
||||||
|
const gid = this.guild.guild.id
|
||||||
|
const selfId = this.identity?.user?.id || ''
|
||||||
|
const presenceMap = this.contacts?.getFriendPresenceMap?.() || {}
|
||||||
|
const members = await this.guild.listMembers().catch(() => [])
|
||||||
|
const entries = []
|
||||||
|
const seen = new Set()
|
||||||
|
if (selfId && this.presence?.customStatus) {
|
||||||
|
entries.push({
|
||||||
|
userId: selfId,
|
||||||
|
status: this.presence.status || 'online',
|
||||||
|
customStatusText: this.presence.customStatus || '',
|
||||||
|
emoji: '',
|
||||||
|
exportedAt: Date.now()
|
||||||
|
})
|
||||||
|
seen.add(selfId)
|
||||||
|
}
|
||||||
|
for (const m of members.slice(0, 64)) {
|
||||||
|
const uid = m.userId || m.id
|
||||||
|
if (!uid || seen.has(uid)) continue
|
||||||
|
const p = presenceMap[uid]
|
||||||
|
const text = p?.customStatus || m.customStatus || ''
|
||||||
|
if (!text && !entries.length && uid === selfId) continue
|
||||||
|
entries.push({
|
||||||
|
userId: uid,
|
||||||
|
status: p?.status || 'offline',
|
||||||
|
customStatusText: text || 'none',
|
||||||
|
emoji: '',
|
||||||
|
exportedAt: Date.now()
|
||||||
|
})
|
||||||
|
seen.add(uid)
|
||||||
|
}
|
||||||
|
if (!entries.length) {
|
||||||
|
entries.push({
|
||||||
|
userId: selfId,
|
||||||
|
status: this.presence?.status || 'online',
|
||||||
|
customStatusText: this.presence?.customStatus || 'none',
|
||||||
|
emoji: '',
|
||||||
|
exportedAt: Date.now()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return entries
|
||||||
|
}
|
||||||
|
|
||||||
|
async getCustomStatusCsvExport () {
|
||||||
|
const gid = this.guild?.guild?.id || null
|
||||||
|
const span = this.log.time('status.csv', { spanKind: 'status.csv', guildId: gid, context: 'read' })
|
||||||
|
try {
|
||||||
|
if (!this.guild?.guild) { span.end({ guildStatusCsvCount: 0, skipped: true }); return null }
|
||||||
|
await this._initDeliveryReceipts(this.guild.guild.id)
|
||||||
|
const row = await this.deliveryReceipts.getCustomStatusCsvExport()
|
||||||
|
if (!row?.signature) {
|
||||||
|
span.end({ guildStatusCsvCount: row ? 1 : 0, hasSignature: false, bridgeKind: 'status.csv' })
|
||||||
|
return row
|
||||||
|
}
|
||||||
|
const verified = verifyCustomStatusCsv(row.csvBody, row.signature, { guildId: this.guild.guild.id, relaySecret: this.guild.guild.id })
|
||||||
|
const out = { ...row, signatureValid: verified.ok }
|
||||||
|
span.end({ guildStatusCsvCount: 1, hasSignature: true, signatureValid: verified.ok, bridgeKind: 'status.csv' })
|
||||||
|
return out
|
||||||
|
} catch (err) {
|
||||||
|
this.log.error('status.csv error', { guildId: gid, context: 'read', error: err?.message || String(err) })
|
||||||
|
span.fail(err); throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async exportCustomStatusCsv (opts = {}) {
|
||||||
|
if (!this.guild?.guild) throw new Error('no guild')
|
||||||
|
await this._initDeliveryReceipts(this.guild.guild.id)
|
||||||
|
const entries = await this._statusCsvRegistryEntries()
|
||||||
|
const csvBody = buildCustomStatusCsvBody(entries)
|
||||||
|
const signed = signCustomStatusCsv(csvBody, { guildId: this.guild.guild.id, relaySecret: this.guild.guild.id })
|
||||||
|
const exportedAt = Date.now()
|
||||||
|
if (opts.recordMesh !== false) {
|
||||||
|
await this.deliveryReceipts.recordCustomStatusCsvExport({ csvBody: signed.csv, signature: signed.signature, signatureAlg: signed.signatureAlg, exportedAt, statusCount: entries.length })
|
||||||
|
}
|
||||||
|
return { format: 'csv', body: signed.csv, signature: signed.signature, signatureAlg: signed.signatureAlg, exportedAt, statusCount: entries.length }
|
||||||
|
}
|
||||||
|
|
||||||
|
async pushCustomStatusCsvToMesh () {
|
||||||
|
const gid = this.guild?.guild?.id || null
|
||||||
|
const span = this.log.time('status.csv', { spanKind: 'status.csv', guildId: gid, context: 'mesh.push' })
|
||||||
|
try {
|
||||||
|
if (!this.guild?.guild) throw new Error('no guild')
|
||||||
|
if (!(await this._hasPerm(PERMISSION.MANAGE_GUILD))) throw new Error('no permission to sync custom status CSV')
|
||||||
|
const exported = await this.exportCustomStatusCsv({ recordMesh: false })
|
||||||
|
if (!exported.signature) throw new Error('custom status csv export not signed')
|
||||||
|
const payload = { guildId: this.guild.guild.id, exportedAt: exported.exportedAt || Date.now(), exportedBy: this.identity.user?.id || null, csvBody: exported.body, signature: exported.signature, signatureAlg: exported.signatureAlg, statusCount: exported.statusCount || 0 }
|
||||||
|
if (this.guild.gossipCustomStatusCsvSync) this.guild.gossipCustomStatusCsvSync(payload)
|
||||||
|
this.emit('custom-status-csv-sync', payload)
|
||||||
|
await this.deliveryReceipts.recordCustomStatusCsvExport({ csvBody: exported.body, signature: exported.signature, signatureAlg: exported.signatureAlg, exportedAt: exported.exportedAt, statusCount: exported.statusCount })
|
||||||
|
span.end({ guildStatusCsvCount: exported.statusCount || 1, meshPushed: true, bridgeKind: 'status.csv' })
|
||||||
|
return exported
|
||||||
|
} catch (err) {
|
||||||
|
this.log.error('status.csv error', { guildId: gid, context: 'mesh.push', error: err?.message || String(err) })
|
||||||
|
span.fail(err); throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async clearCustomStatusCsvExports () {
|
||||||
|
if (!this.guild?.guild) throw new Error('no guild')
|
||||||
|
if (!(await this._hasPerm(PERMISSION.MANAGE_GUILD))) throw new Error('no permission to clear custom status CSV exports')
|
||||||
|
await this._initDeliveryReceipts(this.guild.guild.id)
|
||||||
|
const removed = await this.deliveryReceipts.clearCustomStatusCsvExports()
|
||||||
|
this.emit('custom-status-csv-cleared', { removed })
|
||||||
|
return { removed }
|
||||||
|
}
|
||||||
|
|
||||||
|
async getLastGuildWidgetExport () {
|
||||||
|
if (!this.guild?.guild) return null
|
||||||
|
const snap = this._lastGuildWidgetExport
|
||||||
|
if (snap?.guildId === this.guild.guild.id) return snap
|
||||||
|
return { guildId: this.guild.guild.id, exportedAt: Date.now(), widgetCount: 1, signed: false, meshPushed: false }
|
||||||
|
}
|
||||||
|
|
||||||
|
async exportGuildWidgetMesh (opts = {}) {
|
||||||
|
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 export guild widget mesh')
|
||||||
|
const g = this.guild.guild
|
||||||
|
const channels = await this.guild.listChannels().catch(() => [])
|
||||||
|
const members = await this.guild.listMembers().catch(() => [])
|
||||||
|
const entries = [{
|
||||||
|
guildId: g.id,
|
||||||
|
guildName: g.name || '',
|
||||||
|
publicListing: !!g.publicListing,
|
||||||
|
channelCount: channels.length,
|
||||||
|
memberCount: members.length,
|
||||||
|
exportedAt: Date.now()
|
||||||
|
}]
|
||||||
|
const csvBody = buildGuildWidgetExportCsvBody(entries)
|
||||||
|
const signed = signGuildWidgetExportCsv(csvBody, { guildId: g.id, relaySecret: g.id })
|
||||||
|
const exportedAt = Date.now()
|
||||||
|
const meta = { guildId: g.id, exportedAt, exportedBy: this.identity.user?.id || null, widgetCount: entries.length, signed: !!signed.signature, meshPushed: false }
|
||||||
|
this._lastGuildWidgetExport = meta
|
||||||
|
if (opts.recordMesh !== false) {
|
||||||
|
await this.deliveryReceipts.recordGuildWidgetExportCsv({ csvBody: signed.csv, signature: signed.signature, signatureAlg: signed.signatureAlg, exportedAt, widgetCount: entries.length })
|
||||||
|
}
|
||||||
|
return { format: 'csv', body: signed.csv, signature: signed.signature, signatureAlg: signed.signatureAlg, exportedAt, widgetCount: entries.length, meta }
|
||||||
|
}
|
||||||
|
|
||||||
|
async pushGuildWidgetExportToMesh () {
|
||||||
|
const gid = this.guild?.guild?.id || null
|
||||||
|
const span = this.log.time('status.csv', { spanKind: 'status.csv', guildId: gid, context: 'mesh.guild-widget' })
|
||||||
|
try {
|
||||||
|
const exported = await this.exportGuildWidgetMesh({ recordMesh: false })
|
||||||
|
if (!exported.signature) throw new Error('guild widget export csv not signed')
|
||||||
|
const payload = { guildId: this.guild.guild.id, exportedAt: exported.exportedAt || Date.now(), exportedBy: this.identity.user?.id || null, csvBody: exported.body, signature: exported.signature, signatureAlg: exported.signatureAlg, widgetCount: exported.widgetCount || 0 }
|
||||||
|
if (this.guild.gossipGuildWidgetExportCsvSync) this.guild.gossipGuildWidgetExportCsvSync(payload)
|
||||||
|
this.emit('guild-widget-export-csv-sync', payload)
|
||||||
|
await this.deliveryReceipts.recordGuildWidgetExportCsv({ csvBody: exported.body, signature: exported.signature, signatureAlg: exported.signatureAlg, exportedAt: exported.exportedAt, widgetCount: exported.widgetCount })
|
||||||
|
this._lastGuildWidgetExport = { ...exported.meta, meshPushed: true }
|
||||||
|
span.end({ guildStatusCsvCount: exported.widgetCount || 0, meshPushed: true, bridgeKind: 'status.csv' })
|
||||||
|
return exported
|
||||||
|
} catch (err) {
|
||||||
|
this.log.error('status.csv error', { guildId: gid, context: 'mesh.guild-widget', error: err?.message || String(err) })
|
||||||
|
span.fail(err); throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async _onCustomStatusCsvGossip (payload) {
|
||||||
|
if (!payload?.guildId || !payload?.csvBody || !payload?.signature) return null
|
||||||
|
if (this.guild?.guild?.id !== payload.guildId) return null
|
||||||
|
await this._initDeliveryReceipts(payload.guildId)
|
||||||
|
this._shouldGossipCustomStatusRegistryCsv(payload)
|
||||||
|
const row = await this.deliveryReceipts.ingestCustomStatusCsvSlice(payload.guildId, payload)
|
||||||
|
if (row?.duplicate) this._customStatusCsvDuplicateAt = row.exportedAt || payload.exportedAt || null
|
||||||
|
if (row) this.emit('custom-status-csv-sync', payload)
|
||||||
|
return row
|
||||||
|
}
|
||||||
|
|
||||||
|
async _onGuildWidgetExportCsvGossip (payload) {
|
||||||
|
if (!payload?.guildId || !payload?.csvBody || !payload?.signature) return null
|
||||||
|
if (this.guild?.guild?.id !== payload.guildId) return null
|
||||||
|
await this._initDeliveryReceipts(payload.guildId)
|
||||||
|
this._shouldGossipGuildWidgetExportCsv(payload)
|
||||||
|
const row = await this.deliveryReceipts.ingestGuildWidgetExportCsvSlice(payload.guildId, payload)
|
||||||
|
if (row?.duplicate) this._guildWidgetExportCsvDuplicateAt = row.exportedAt || payload.exportedAt || null
|
||||||
|
if (row) this.emit('guild-widget-export-csv-sync', payload)
|
||||||
|
return row
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
async getAuditExportSchedule () {
|
async getAuditExportSchedule () {
|
||||||
if (!this.guild?.guild) {
|
if (!this.guild?.guild) {
|
||||||
return { enabled: false, intervalHours: 24, filter: 'all', lastExportAt: 0 }
|
return { enabled: false, intervalHours: 24, filter: 'all', lastExportAt: 0 }
|
||||||
@@ -31221,6 +31417,11 @@ class PearcordPlatform extends EventEmitter {
|
|||||||
let channelFollowExportCsvRows = []
|
let channelFollowExportCsvRows = []
|
||||||
let lastChannelFollowExport = null
|
let lastChannelFollowExport = null
|
||||||
let guildNotificationOverrides = []
|
let guildNotificationOverrides = []
|
||||||
|
let customStatusCsvMeta = null
|
||||||
|
let customStatusCsvRows = []
|
||||||
|
let guildWidgetExportCsvRows = []
|
||||||
|
let lastGuildWidgetExport = null
|
||||||
|
let guildCustomStatuses = []
|
||||||
let lastComplianceSnapshot = null
|
let lastComplianceSnapshot = null
|
||||||
let lastArchivePeerExport = null
|
let lastArchivePeerExport = null
|
||||||
let automationScheduleDashboard = null
|
let automationScheduleDashboard = null
|
||||||
@@ -31805,6 +32006,10 @@ class PearcordPlatform extends EventEmitter {
|
|||||||
this._notificationOverrideCsvDuplicateAt = null
|
this._notificationOverrideCsvDuplicateAt = null
|
||||||
const channelFollowExportCsvDuplicateAt = this._channelFollowExportCsvDuplicateAt || null
|
const channelFollowExportCsvDuplicateAt = this._channelFollowExportCsvDuplicateAt || null
|
||||||
this._channelFollowExportCsvDuplicateAt = null
|
this._channelFollowExportCsvDuplicateAt = null
|
||||||
|
const customStatusCsvDuplicateAt = this._customStatusCsvDuplicateAt || null
|
||||||
|
this._customStatusCsvDuplicateAt = null
|
||||||
|
const guildWidgetExportCsvDuplicateAt = this._guildWidgetExportCsvDuplicateAt || null
|
||||||
|
this._guildWidgetExportCsvDuplicateAt = null
|
||||||
return {
|
return {
|
||||||
onboarded: this.onboarded,
|
onboarded: this.onboarded,
|
||||||
sessionReady: this._sessionReady,
|
sessionReady: this._sessionReady,
|
||||||
@@ -31974,6 +32179,8 @@ class PearcordPlatform extends EventEmitter {
|
|||||||
readStateExportCsvDuplicateAt,
|
readStateExportCsvDuplicateAt,
|
||||||
notificationOverrideCsvDuplicateAt,
|
notificationOverrideCsvDuplicateAt,
|
||||||
channelFollowExportCsvDuplicateAt,
|
channelFollowExportCsvDuplicateAt,
|
||||||
|
customStatusCsvDuplicateAt,
|
||||||
|
guildWidgetExportCsvDuplicateAt,
|
||||||
digestRelayHandoffCsvMeta,
|
digestRelayHandoffCsvMeta,
|
||||||
digestRelayHandoffCsvRows,
|
digestRelayHandoffCsvRows,
|
||||||
archivePeerExportCsvRows,
|
archivePeerExportCsvRows,
|
||||||
@@ -32083,6 +32290,13 @@ class PearcordPlatform extends EventEmitter {
|
|||||||
guildNotificationOverrides,
|
guildNotificationOverrides,
|
||||||
guildNotificationCsvCount:
|
guildNotificationCsvCount:
|
||||||
(notificationOverrideCsvRows || []).length + (guildNotificationOverrides || []).length,
|
(notificationOverrideCsvRows || []).length + (guildNotificationOverrides || []).length,
|
||||||
|
customStatusCsvMeta,
|
||||||
|
customStatusCsvRows,
|
||||||
|
guildWidgetExportCsvRows,
|
||||||
|
lastGuildWidgetExport,
|
||||||
|
guildCustomStatuses,
|
||||||
|
guildStatusCsvCount:
|
||||||
|
(customStatusCsvRows || []).length + (guildCustomStatuses || []).length,
|
||||||
automationScheduleDashboard,
|
automationScheduleDashboard,
|
||||||
automationHealthDashboard,
|
automationHealthDashboard,
|
||||||
automationDigestNotifyPrefs,
|
automationDigestNotifyPrefs,
|
||||||
@@ -32703,6 +32917,7 @@ const { voiceCsvMixin } = require('./voice-csv-mixin')
|
|||||||
const { screenCsvMixin } = require('./screen-csv-mixin')
|
const { screenCsvMixin } = require('./screen-csv-mixin')
|
||||||
const { presenceCsvMixin } = require('./presence-csv-mixin')
|
const { presenceCsvMixin } = require('./presence-csv-mixin')
|
||||||
const { notificationCsvMixin } = require('./notification-csv-mixin')
|
const { notificationCsvMixin } = require('./notification-csv-mixin')
|
||||||
|
const { statusCsvMixin } = require('./status-csv-mixin')
|
||||||
Object.assign(PearcordPlatform.prototype, pollSchedulingMixin)
|
Object.assign(PearcordPlatform.prototype, pollSchedulingMixin)
|
||||||
Object.assign(PearcordPlatform.prototype, notificationsActivityMixin)
|
Object.assign(PearcordPlatform.prototype, notificationsActivityMixin)
|
||||||
Object.assign(PearcordPlatform.prototype, userSettingsMixin)
|
Object.assign(PearcordPlatform.prototype, userSettingsMixin)
|
||||||
@@ -32739,3 +32954,4 @@ Object.assign(PearcordPlatform.prototype, voiceCsvMixin)
|
|||||||
Object.assign(PearcordPlatform.prototype, screenCsvMixin)
|
Object.assign(PearcordPlatform.prototype, screenCsvMixin)
|
||||||
Object.assign(PearcordPlatform.prototype, presenceCsvMixin)
|
Object.assign(PearcordPlatform.prototype, presenceCsvMixin)
|
||||||
Object.assign(PearcordPlatform.prototype, notificationCsvMixin)
|
Object.assign(PearcordPlatform.prototype, notificationCsvMixin)
|
||||||
|
Object.assign(PearcordPlatform.prototype, statusCsvMixin)
|
||||||
|
|||||||
@@ -0,0 +1,114 @@
|
|||||||
|
'use strict'
|
||||||
|
|
||||||
|
const statusCsvMixin = {
|
||||||
|
_statusRegistryCsvGossipKeys: null,
|
||||||
|
_statusCsvHealWatermark: null,
|
||||||
|
_guildWidgetExportCsvHealWatermark: null,
|
||||||
|
|
||||||
|
_initStatusCsvMixinState () {
|
||||||
|
if (!this._statusRegistryCsvGossipKeys) {
|
||||||
|
this._statusRegistryCsvGossipKeys = new Set()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
_shouldGossipCustomStatusRegistryCsv (slice) {
|
||||||
|
this._initStatusCsvMixinState()
|
||||||
|
if (!slice?.guildId || !slice?.signature) return true
|
||||||
|
const hash = `${slice.guildId}:${slice.exportedAt || 0}:${slice.signature}`
|
||||||
|
if (this._statusRegistryCsvGossipKeys.has(hash)) return false
|
||||||
|
this._statusRegistryCsvGossipKeys.add(hash)
|
||||||
|
if (this._statusRegistryCsvGossipKeys.size > 8192) {
|
||||||
|
const first = this._statusRegistryCsvGossipKeys.values().next().value
|
||||||
|
if (first) this._statusRegistryCsvGossipKeys.delete(first)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
|
||||||
|
_shouldGossipGuildWidgetExportCsv (slice) {
|
||||||
|
this._initStatusCsvMixinState()
|
||||||
|
if (!slice?.guildId || !slice?.signature) return true
|
||||||
|
const hash = `effective:${slice.guildId}:${slice.exportedAt || 0}:${slice.signature}`
|
||||||
|
if (this._statusRegistryCsvGossipKeys.has(hash)) return false
|
||||||
|
this._statusRegistryCsvGossipKeys.add(hash)
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
|
||||||
|
async _healCustomStatusExportCursorOnPartition (guildId) {
|
||||||
|
const gid = guildId || this.guild?.guild?.id || null
|
||||||
|
const span = this.log.time('status.csv', {
|
||||||
|
spanKind: 'status.csv',
|
||||||
|
guildId: gid,
|
||||||
|
context: 'heal.status'
|
||||||
|
})
|
||||||
|
try {
|
||||||
|
if (!gid || !this.deliveryReceipts) {
|
||||||
|
span.end({ relisted: 0, skipped: true, guildStatusCsvCount: 0 })
|
||||||
|
return { relisted: 0, skipped: true }
|
||||||
|
}
|
||||||
|
await this._initDeliveryReceipts(gid)
|
||||||
|
const rows = await this.deliveryReceipts.listCustomStatusCsvExports(64)
|
||||||
|
let relisted = 0
|
||||||
|
for (const row of rows) {
|
||||||
|
if (this._shouldGossipCustomStatusRegistryCsv(row)) relisted++
|
||||||
|
}
|
||||||
|
const watermark = Date.now()
|
||||||
|
this._statusCsvHealWatermark = watermark
|
||||||
|
span.end({
|
||||||
|
relisted,
|
||||||
|
watermark,
|
||||||
|
guildStatusCsvCount: rows.length,
|
||||||
|
bridgeKind: 'status.csv'
|
||||||
|
})
|
||||||
|
return { relisted, watermark, guildStatusCsvCount: rows.length }
|
||||||
|
} catch (err) {
|
||||||
|
this.log.error('status.csv error', {
|
||||||
|
guildId: gid,
|
||||||
|
context: 'heal.status',
|
||||||
|
error: err?.message || String(err)
|
||||||
|
})
|
||||||
|
span.fail(err)
|
||||||
|
return { relisted: 0, error: err?.message || String(err) }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async _healGuildWidgetExportCursorOnPartition (guildId) {
|
||||||
|
const gid = guildId || this.guild?.guild?.id || null
|
||||||
|
const span = this.log.time('status.csv', {
|
||||||
|
spanKind: 'status.csv',
|
||||||
|
guildId: gid,
|
||||||
|
context: 'heal.guild-widget'
|
||||||
|
})
|
||||||
|
try {
|
||||||
|
if (!gid || !this.deliveryReceipts) {
|
||||||
|
span.end({ relisted: 0, skipped: true, guildStatusCsvCount: 0 })
|
||||||
|
return { relisted: 0, skipped: true }
|
||||||
|
}
|
||||||
|
await this._initDeliveryReceipts(gid)
|
||||||
|
const rows = await this.deliveryReceipts.listGuildWidgetExportCsvExports(64)
|
||||||
|
let relisted = 0
|
||||||
|
for (const row of rows) {
|
||||||
|
if (this._shouldGossipGuildWidgetExportCsv(row)) relisted++
|
||||||
|
}
|
||||||
|
const watermark = Date.now()
|
||||||
|
this._guildWidgetExportCsvHealWatermark = watermark
|
||||||
|
span.end({
|
||||||
|
relisted,
|
||||||
|
watermark,
|
||||||
|
guildStatusCsvCount: rows.length,
|
||||||
|
statusCount: rows[0]?.statusCount || 0,
|
||||||
|
bridgeKind: 'status.csv'
|
||||||
|
})
|
||||||
|
return { relisted, watermark, statusCount: rows[0]?.statusCount || 0 }
|
||||||
|
} catch (err) {
|
||||||
|
this.log.error('status.csv error', {
|
||||||
|
guildId: gid,
|
||||||
|
context: 'heal.guild-widget',
|
||||||
|
error: err?.message || String(err)
|
||||||
|
})
|
||||||
|
span.fail(err)
|
||||||
|
return { relisted: 0, error: err?.message || String(err) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { statusCsvMixin }
|
||||||
Reference in New Issue
Block a user