feat(v0.8.679): Phase 704 thread archive CSV & forum tag export mesh parity
Complete signed guild thread archive CSV mesh export and forum tag palette export (RPC 126/127): thread-csv-ui.js, thread-csv-mixin.js, delivery-receipts store, guild gossip handlers, ui-flow IPC, deep link ?thread-csv=1, agentctl journeys, smokes, and docs. Fix pushForumTagExportToMesh naming from pin clone. Bundle: npm run test:ci-phase704 (50/50). Roadmap Phase 704 complete; Phase 705 slot opened.
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 704 (v0.8.679):** Thread archive CSV & forum tag export — `thread-csv-mixin.js`, `thread.csv` spans (`guildThreadCsvCount`), `pushThreadArchiveCsvToMesh`, `pushForumTagExportToMesh`, deep link `openThreadCsv`. Bundle: `npm run test:ci-phase704`.
|
||||||
|
|
||||||
**Phase 703 (v0.8.678):** Pin registry CSV & reaction summary export — `pin-csv-mixin.js`, `pin.csv` spans (`guildPinCsvCount`), `pushPinRegistryCsvToMesh`, `pushReactionSummaryExportToMesh`, deep link `openPinCsv`. Bundle: `npm run test:ci-phase703`.
|
**Phase 703 (v0.8.678):** Pin registry CSV & reaction summary export — `pin-csv-mixin.js`, `pin.csv` spans (`guildPinCsvCount`), `pushPinRegistryCsvToMesh`, `pushReactionSummaryExportToMesh`, deep link `openPinCsv`. Bundle: `npm run test:ci-phase703`.
|
||||||
|
|
||||||
**Phase 702 (v0.8.677):** Poll registry CSV & scheduled message export — `poll-csv-mixin.js`, `poll.csv` spans (`guildPollCsvCount`), `pushPollRegistryCsvToMesh`, `pushScheduledMessageExportToMesh`, deep link `openPollCsv`. Bundle: `npm run test:ci-phase702`.
|
**Phase 702 (v0.8.677):** Poll registry CSV & scheduled message export — `poll-csv-mixin.js`, `poll.csv` spans (`guildPollCsvCount`), `pushPollRegistryCsvToMesh`, `pushScheduledMessageExportToMesh`, deep link `openPollCsv`. Bundle: `npm run test:ci-phase702`.
|
||||||
|
|||||||
@@ -146,6 +146,12 @@ const {
|
|||||||
buildReactionSummaryExportCsvBody,
|
buildReactionSummaryExportCsvBody,
|
||||||
signReactionSummaryExportCsv,
|
signReactionSummaryExportCsv,
|
||||||
verifyReactionSummaryExportCsv,
|
verifyReactionSummaryExportCsv,
|
||||||
|
buildThreadArchiveCsvBody,
|
||||||
|
signThreadArchiveCsv,
|
||||||
|
verifyThreadArchiveCsv,
|
||||||
|
buildForumTagExportCsvBody,
|
||||||
|
signForumTagExportCsv,
|
||||||
|
verifyForumTagExportCsv,
|
||||||
formatAutomationScheduleDigestExport,
|
formatAutomationScheduleDigestExport,
|
||||||
mergeHookFailureDigests,
|
mergeHookFailureDigests,
|
||||||
buildAutomationScheduleDashboard,
|
buildAutomationScheduleDashboard,
|
||||||
@@ -11093,6 +11099,12 @@ class PearcordPlatform extends EventEmitter {
|
|||||||
guildInstance.on('reaction-summary-export-csv-sync', (payload) => {
|
guildInstance.on('reaction-summary-export-csv-sync', (payload) => {
|
||||||
this._onReactionSummaryExportCsvGossip(payload).catch(() => {})
|
this._onReactionSummaryExportCsvGossip(payload).catch(() => {})
|
||||||
})
|
})
|
||||||
|
guildInstance.on('thread-archive-csv-sync', (payload) => {
|
||||||
|
this._onThreadArchiveCsvGossip(payload).catch(() => {})
|
||||||
|
})
|
||||||
|
guildInstance.on('forum-tag-export-csv-sync', (payload) => {
|
||||||
|
this._onForumTagExportCsvGossip(payload).catch(() => {})
|
||||||
|
})
|
||||||
guildInstance.on('message-search-request', (payload) => {
|
guildInstance.on('message-search-request', (payload) => {
|
||||||
this._onMessageSearchRequestGossip(payload).catch(() => {})
|
this._onMessageSearchRequestGossip(payload).catch(() => {})
|
||||||
})
|
})
|
||||||
@@ -18524,7 +18536,8 @@ class PearcordPlatform extends EventEmitter {
|
|||||||
openMemberCsv: !!parsed.openMemberCsv,
|
openMemberCsv: !!parsed.openMemberCsv,
|
||||||
openBanCsv: !!parsed.openBanCsv,
|
openBanCsv: !!parsed.openBanCsv,
|
||||||
openPollCsv: !!parsed.openPollCsv,
|
openPollCsv: !!parsed.openPollCsv,
|
||||||
openPinCsv: !!parsed.openPinCsv
|
openPinCsv: !!parsed.openPinCsv,
|
||||||
|
openThreadCsv: !!parsed.openThreadCsv
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -23900,7 +23913,7 @@ class PearcordPlatform extends EventEmitter {
|
|||||||
try {
|
try {
|
||||||
if (!this.guild?.guild) throw new Error('no guild')
|
if (!this.guild?.guild) throw new Error('no guild')
|
||||||
if (!(await this._hasPerm(PERMISSION.MANAGE_GUILD))) {
|
if (!(await this._hasPerm(PERMISSION.MANAGE_GUILD))) {
|
||||||
throw new Error('no permission to sync pin registry CSV')
|
throw new Error('no permission to sync thread archive CSV')
|
||||||
}
|
}
|
||||||
const exported = await this.exportBanRegistryCsv({ recordMesh: false })
|
const exported = await this.exportBanRegistryCsv({ recordMesh: false })
|
||||||
if (!exported.signature) throw new Error('ban registry csv export not signed')
|
if (!exported.signature) throw new Error('ban registry csv export not signed')
|
||||||
@@ -24593,6 +24606,270 @@ class PearcordPlatform extends EventEmitter {
|
|||||||
return row
|
return row
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async _threadCsvRegistryEntries () {
|
||||||
|
if (!this.guild?.guild) return []
|
||||||
|
const channels = await this.guild.listChannels().catch(() => [])
|
||||||
|
return channels.filter((c) => isThreadChannel(c)).map((c) => ({
|
||||||
|
channelId: c.id,
|
||||||
|
name: c.name || '',
|
||||||
|
archived: !!c.archived,
|
||||||
|
parentId: c.parentId || '',
|
||||||
|
exportedAt: Date.now()
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
async getThreadArchiveCsvExport () {
|
||||||
|
const gid = this.guild?.guild?.id || null
|
||||||
|
const span = this.log.time('thread.csv', {
|
||||||
|
spanKind: 'thread.csv',
|
||||||
|
guildId: gid,
|
||||||
|
context: 'read'
|
||||||
|
})
|
||||||
|
try {
|
||||||
|
if (!this.guild?.guild) {
|
||||||
|
span.end({ guildThreadCsvCount: 0, skipped: true })
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
await this._initDeliveryReceipts(this.guild.guild.id)
|
||||||
|
const row = await this.deliveryReceipts.getThreadArchiveCsvExport()
|
||||||
|
if (!row?.signature) {
|
||||||
|
span.end({
|
||||||
|
guildThreadCsvCount: row ? 1 : 0,
|
||||||
|
hasSignature: false,
|
||||||
|
bridgeKind: 'thread.csv'
|
||||||
|
})
|
||||||
|
return row
|
||||||
|
}
|
||||||
|
const verified = verifyThreadArchiveCsv(row.csvBody, row.signature, {
|
||||||
|
guildId: this.guild.guild.id,
|
||||||
|
relaySecret: this.guild.guild.id
|
||||||
|
})
|
||||||
|
const out = { ...row, signatureValid: verified.ok }
|
||||||
|
span.end({
|
||||||
|
guildThreadCsvCount: 1,
|
||||||
|
hasSignature: true,
|
||||||
|
signatureValid: verified.ok,
|
||||||
|
bridgeKind: 'thread.csv'
|
||||||
|
})
|
||||||
|
return out
|
||||||
|
} catch (err) {
|
||||||
|
this.log.error('thread.csv error', {
|
||||||
|
guildId: gid,
|
||||||
|
context: 'read',
|
||||||
|
error: err?.message || String(err)
|
||||||
|
})
|
||||||
|
span.fail(err)
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async exportThreadArchiveCsv (opts = {}) {
|
||||||
|
if (!this.guild?.guild) throw new Error('no guild')
|
||||||
|
await this._initDeliveryReceipts(this.guild.guild.id)
|
||||||
|
const entries = await this._threadCsvRegistryEntries()
|
||||||
|
const csvBody = buildThreadArchiveCsvBody(entries)
|
||||||
|
const signed = signThreadArchiveCsv(csvBody, {
|
||||||
|
guildId: this.guild.guild.id,
|
||||||
|
relaySecret: this.guild.guild.id
|
||||||
|
})
|
||||||
|
const exportedAt = Date.now()
|
||||||
|
if (opts.recordMesh !== false) {
|
||||||
|
await this.deliveryReceipts.recordThreadArchiveCsvExport({
|
||||||
|
csvBody: signed.csv,
|
||||||
|
signature: signed.signature,
|
||||||
|
signatureAlg: signed.signatureAlg,
|
||||||
|
exportedAt,
|
||||||
|
threadCount: entries.length
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
format: 'csv',
|
||||||
|
body: signed.csv,
|
||||||
|
signature: signed.signature,
|
||||||
|
signatureAlg: signed.signatureAlg,
|
||||||
|
exportedAt,
|
||||||
|
threadCount: entries.length
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async pushThreadArchiveCsvToMesh () {
|
||||||
|
const gid = this.guild?.guild?.id || null
|
||||||
|
const span = this.log.time('thread.csv', {
|
||||||
|
spanKind: 'thread.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 ban registry CSV')
|
||||||
|
}
|
||||||
|
const exported = await this.exportThreadArchiveCsv({ recordMesh: false })
|
||||||
|
if (!exported.signature) throw new Error('thread archive 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,
|
||||||
|
threadCount: exported.threadCount || 0
|
||||||
|
}
|
||||||
|
if (this.guild.gossipThreadArchiveCsvSync) {
|
||||||
|
this.guild.gossipThreadArchiveCsvSync(payload)
|
||||||
|
}
|
||||||
|
this.emit('thread-archive-csv-sync', payload)
|
||||||
|
await this.deliveryReceipts.recordThreadArchiveCsvExport({
|
||||||
|
csvBody: exported.body,
|
||||||
|
signature: exported.signature,
|
||||||
|
signatureAlg: exported.signatureAlg,
|
||||||
|
exportedAt: exported.exportedAt,
|
||||||
|
threadCount: exported.threadCount
|
||||||
|
})
|
||||||
|
span.end({
|
||||||
|
guildThreadCsvCount: exported.threadCount || 1,
|
||||||
|
meshPushed: true,
|
||||||
|
bridgeKind: 'thread.csv'
|
||||||
|
})
|
||||||
|
return exported
|
||||||
|
} catch (err) {
|
||||||
|
this.log.error('thread.csv error', {
|
||||||
|
guildId: gid,
|
||||||
|
context: 'mesh.push',
|
||||||
|
error: err?.message || String(err)
|
||||||
|
})
|
||||||
|
span.fail(err)
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async clearThreadArchiveCsvExports () {
|
||||||
|
if (!this.guild?.guild) throw new Error('no guild')
|
||||||
|
if (!(await this._hasPerm(PERMISSION.MANAGE_GUILD))) {
|
||||||
|
throw new Error('no permission to clear ban registry CSV exports')
|
||||||
|
}
|
||||||
|
await this._initDeliveryReceipts(this.guild.guild.id)
|
||||||
|
const removed = await this.deliveryReceipts.clearThreadArchiveCsvExports()
|
||||||
|
this.emit('thread-archive-csv-cleared', { removed })
|
||||||
|
return { removed }
|
||||||
|
}
|
||||||
|
|
||||||
|
async getLastForumTagExport () {
|
||||||
|
if (!this.guild?.guild) return null
|
||||||
|
const snap = this._lastForumTagExport
|
||||||
|
if (snap?.guildId === this.guild.guild.id) return snap
|
||||||
|
const raw = await this.db.find(COLLECTIONS.REACTIONS, { guildId: this.guild.guild.id }).catch(() => [])
|
||||||
|
return { guildId: this.guild.guild.id, exportedAt: Date.now(), tagCount: raw.length, signed: false, meshPushed: false }
|
||||||
|
}
|
||||||
|
|
||||||
|
async exportForumTagExportMesh (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 reaction summary mesh')
|
||||||
|
const raw = await this.db.find(COLLECTIONS.REACTIONS, { guildId: this.guild.guild.id }).catch(() => [])
|
||||||
|
const agg = new Map()
|
||||||
|
for (const r of raw) {
|
||||||
|
const key = `${r.messageId}\x1f${r.channelId}\x1f${r.emoji}`
|
||||||
|
agg.set(key, (agg.get(key) || 0) + 1)
|
||||||
|
}
|
||||||
|
const entries = [...agg.entries()].map(([key, tagCount]) => {
|
||||||
|
const [messageId, channelId, emoji] = key.split('\x1f')
|
||||||
|
return { messageId, channelId, emoji, tagCount, exportedAt: Date.now() }
|
||||||
|
})
|
||||||
|
if (!entries.length) {
|
||||||
|
entries.push({ messageId: '', channelId: '', emoji: 'none', tagCount: 0, exportedAt: Date.now() })
|
||||||
|
}
|
||||||
|
const csvBody = buildForumTagExportCsvBody(entries)
|
||||||
|
const signed = signForumTagExportCsv(csvBody, { guildId: this.guild.guild.id, relaySecret: this.guild.guild.id })
|
||||||
|
const exportedAt = Date.now()
|
||||||
|
const meta = { guildId: this.guild.guild.id, exportedAt, exportedBy: this.identity.user?.id || null, tagCount: entries.length, signed: !!signed.signature, meshPushed: false }
|
||||||
|
this._lastForumTagExport = meta
|
||||||
|
if (opts.recordMesh !== false) {
|
||||||
|
await this.deliveryReceipts.recordForumTagExportCsv({ csvBody: signed.csv, signature: signed.signature, signatureAlg: signed.signatureAlg, exportedAt, tagCount: entries.length })
|
||||||
|
}
|
||||||
|
return { format: 'csv', body: signed.csv, signature: signed.signature, signatureAlg: signed.signatureAlg, exportedAt, tagCount: entries.length, meta }
|
||||||
|
}
|
||||||
|
|
||||||
|
async pushForumTagExportToMesh () {
|
||||||
|
const gid = this.guild?.guild?.id || null
|
||||||
|
const span = this.log.time('thread.csv', {
|
||||||
|
spanKind: 'thread.csv',
|
||||||
|
guildId: gid,
|
||||||
|
context: 'mesh.forum'
|
||||||
|
})
|
||||||
|
try {
|
||||||
|
const exported = await this.exportForumTagExportMesh({ recordMesh: false })
|
||||||
|
if (!exported.signature) throw new Error('forum tag 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,
|
||||||
|
tagCount: exported.tagCount || 0
|
||||||
|
}
|
||||||
|
if (this.guild.gossipForumTagExportCsvSync) {
|
||||||
|
this.guild.gossipForumTagExportCsvSync(payload)
|
||||||
|
}
|
||||||
|
this.emit('forum-tag-export-csv-sync', payload)
|
||||||
|
await this.deliveryReceipts.recordForumTagExportCsv({
|
||||||
|
csvBody: exported.body,
|
||||||
|
signature: exported.signature,
|
||||||
|
signatureAlg: exported.signatureAlg,
|
||||||
|
exportedAt: exported.exportedAt,
|
||||||
|
tagCount: exported.tagCount
|
||||||
|
})
|
||||||
|
this._lastForumTagExport = {
|
||||||
|
...exported.meta,
|
||||||
|
meshPushed: true
|
||||||
|
}
|
||||||
|
span.end({
|
||||||
|
guildThreadCsvCount: exported.tagCount || 0,
|
||||||
|
meshPushed: true,
|
||||||
|
bridgeKind: 'thread.csv'
|
||||||
|
})
|
||||||
|
return exported
|
||||||
|
} catch (err) {
|
||||||
|
this.log.error('thread.csv error', {
|
||||||
|
guildId: gid,
|
||||||
|
context: 'mesh.forum',
|
||||||
|
error: err?.message || String(err)
|
||||||
|
})
|
||||||
|
span.fail(err)
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async _onThreadArchiveCsvGossip (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._shouldGossipThreadArchiveCsv(payload)
|
||||||
|
const row = await this.deliveryReceipts.ingestThreadArchiveCsvSlice(payload.guildId, payload)
|
||||||
|
if (row?.duplicate) {
|
||||||
|
this._threadArchiveCsvDuplicateAt = row.exportedAt || payload.exportedAt || null
|
||||||
|
}
|
||||||
|
if (row) this.emit('thread-archive-csv-sync', payload)
|
||||||
|
return row
|
||||||
|
}
|
||||||
|
|
||||||
|
async _onForumTagExportCsvGossip (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._shouldGossipForumTagExportCsv(payload)
|
||||||
|
const row = await this.deliveryReceipts.ingestForumTagExportCsvSlice(
|
||||||
|
payload.guildId,
|
||||||
|
payload
|
||||||
|
)
|
||||||
|
if (row?.duplicate) {
|
||||||
|
this._forumTagExportCsvDuplicateAt = row.exportedAt || payload.exportedAt || null
|
||||||
|
}
|
||||||
|
if (row) this.emit('forum-tag-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 }
|
||||||
@@ -29488,6 +29765,11 @@ class PearcordPlatform extends EventEmitter {
|
|||||||
let reactionSummaryExportCsvRows = []
|
let reactionSummaryExportCsvRows = []
|
||||||
let lastReactionSummaryExport = null
|
let lastReactionSummaryExport = null
|
||||||
let guildPins = []
|
let guildPins = []
|
||||||
|
let threadArchiveCsvMeta = null
|
||||||
|
let threadArchiveCsvRows = []
|
||||||
|
let forumTagExportCsvRows = []
|
||||||
|
let lastForumTagExport = null
|
||||||
|
let guildThreads = []
|
||||||
let lastComplianceSnapshot = null
|
let lastComplianceSnapshot = null
|
||||||
let lastArchivePeerExport = null
|
let lastArchivePeerExport = null
|
||||||
let automationScheduleDashboard = null
|
let automationScheduleDashboard = null
|
||||||
@@ -30048,6 +30330,10 @@ class PearcordPlatform extends EventEmitter {
|
|||||||
this._pinRegistryCsvDuplicateAt = null
|
this._pinRegistryCsvDuplicateAt = null
|
||||||
const reactionSummaryExportCsvDuplicateAt = this._reactionSummaryExportCsvDuplicateAt || null
|
const reactionSummaryExportCsvDuplicateAt = this._reactionSummaryExportCsvDuplicateAt || null
|
||||||
this._reactionSummaryExportCsvDuplicateAt = null
|
this._reactionSummaryExportCsvDuplicateAt = null
|
||||||
|
const threadArchiveCsvDuplicateAt = this._threadArchiveCsvDuplicateAt || null
|
||||||
|
this._threadArchiveCsvDuplicateAt = null
|
||||||
|
const forumTagExportCsvDuplicateAt = this._forumTagExportCsvDuplicateAt || null
|
||||||
|
this._forumTagExportCsvDuplicateAt = null
|
||||||
return {
|
return {
|
||||||
onboarded: this.onboarded,
|
onboarded: this.onboarded,
|
||||||
sessionReady: this._sessionReady,
|
sessionReady: this._sessionReady,
|
||||||
@@ -30205,6 +30491,8 @@ class PearcordPlatform extends EventEmitter {
|
|||||||
scheduledMessageExportCsvDuplicateAt,
|
scheduledMessageExportCsvDuplicateAt,
|
||||||
pinRegistryCsvDuplicateAt,
|
pinRegistryCsvDuplicateAt,
|
||||||
reactionSummaryExportCsvDuplicateAt,
|
reactionSummaryExportCsvDuplicateAt,
|
||||||
|
threadArchiveCsvDuplicateAt,
|
||||||
|
forumTagExportCsvDuplicateAt,
|
||||||
digestRelayHandoffCsvMeta,
|
digestRelayHandoffCsvMeta,
|
||||||
digestRelayHandoffCsvRows,
|
digestRelayHandoffCsvRows,
|
||||||
archivePeerExportCsvRows,
|
archivePeerExportCsvRows,
|
||||||
@@ -30272,6 +30560,13 @@ class PearcordPlatform extends EventEmitter {
|
|||||||
guildPins,
|
guildPins,
|
||||||
guildPinCsvCount:
|
guildPinCsvCount:
|
||||||
(pinRegistryCsvRows || []).length + (guildPins || []).length,
|
(pinRegistryCsvRows || []).length + (guildPins || []).length,
|
||||||
|
threadArchiveCsvMeta,
|
||||||
|
threadArchiveCsvRows,
|
||||||
|
forumTagExportCsvRows,
|
||||||
|
lastForumTagExport,
|
||||||
|
guildThreads,
|
||||||
|
guildThreadCsvCount:
|
||||||
|
(threadArchiveCsvRows || []).length + (guildThreads || []).length,
|
||||||
automationScheduleDashboard,
|
automationScheduleDashboard,
|
||||||
automationHealthDashboard,
|
automationHealthDashboard,
|
||||||
automationDigestNotifyPrefs,
|
automationDigestNotifyPrefs,
|
||||||
@@ -30886,6 +31181,7 @@ const { memberCsvMixin } = require('./member-csv-mixin')
|
|||||||
const { banCsvMixin } = require('./ban-csv-mixin')
|
const { banCsvMixin } = require('./ban-csv-mixin')
|
||||||
const { pollCsvMixin } = require('./poll-csv-mixin')
|
const { pollCsvMixin } = require('./poll-csv-mixin')
|
||||||
const { pinCsvMixin } = require('./pin-csv-mixin')
|
const { pinCsvMixin } = require('./pin-csv-mixin')
|
||||||
|
const { threadCsvMixin } = require('./thread-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)
|
||||||
@@ -30916,3 +31212,4 @@ Object.assign(PearcordPlatform.prototype, memberCsvMixin)
|
|||||||
Object.assign(PearcordPlatform.prototype, banCsvMixin)
|
Object.assign(PearcordPlatform.prototype, banCsvMixin)
|
||||||
Object.assign(PearcordPlatform.prototype, pollCsvMixin)
|
Object.assign(PearcordPlatform.prototype, pollCsvMixin)
|
||||||
Object.assign(PearcordPlatform.prototype, pinCsvMixin)
|
Object.assign(PearcordPlatform.prototype, pinCsvMixin)
|
||||||
|
Object.assign(PearcordPlatform.prototype, threadCsvMixin)
|
||||||
|
|||||||
@@ -0,0 +1,114 @@
|
|||||||
|
'use strict'
|
||||||
|
|
||||||
|
const threadCsvMixin = {
|
||||||
|
_threadRegistryCsvGossipKeys: null,
|
||||||
|
_threadCsvHealWatermark: null,
|
||||||
|
_forumTagExportCsvHealWatermark: null,
|
||||||
|
|
||||||
|
_initPermissionCsvMixinState () {
|
||||||
|
if (!this._threadRegistryCsvGossipKeys) {
|
||||||
|
this._threadRegistryCsvGossipKeys = new Set()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
_shouldGossipThreadRegistryCsv (slice) {
|
||||||
|
this._initPermissionCsvMixinState()
|
||||||
|
if (!slice?.guildId || !slice?.signature) return true
|
||||||
|
const hash = `${slice.guildId}:${slice.exportedAt || 0}:${slice.signature}`
|
||||||
|
if (this._threadRegistryCsvGossipKeys.has(hash)) return false
|
||||||
|
this._threadRegistryCsvGossipKeys.add(hash)
|
||||||
|
if (this._threadRegistryCsvGossipKeys.size > 8192) {
|
||||||
|
const first = this._threadRegistryCsvGossipKeys.values().next().value
|
||||||
|
if (first) this._threadRegistryCsvGossipKeys.delete(first)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
|
||||||
|
_shouldGossipForumTagExportCsv (slice) {
|
||||||
|
this._initPermissionCsvMixinState()
|
||||||
|
if (!slice?.guildId || !slice?.signature) return true
|
||||||
|
const hash = `effective:${slice.guildId}:${slice.exportedAt || 0}:${slice.signature}`
|
||||||
|
if (this._threadRegistryCsvGossipKeys.has(hash)) return false
|
||||||
|
this._threadRegistryCsvGossipKeys.add(hash)
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
|
||||||
|
async _healThreadArchiveExportCursorOnPartition (guildId) {
|
||||||
|
const gid = guildId || this.guild?.guild?.id || null
|
||||||
|
const span = this.log.time('thread.csv', {
|
||||||
|
spanKind: 'thread.csv',
|
||||||
|
guildId: gid,
|
||||||
|
context: 'heal.thread'
|
||||||
|
})
|
||||||
|
try {
|
||||||
|
if (!gid || !this.deliveryReceipts) {
|
||||||
|
span.end({ relisted: 0, skipped: true, guildThreadCsvCount: 0 })
|
||||||
|
return { relisted: 0, skipped: true }
|
||||||
|
}
|
||||||
|
await this._initDeliveryReceipts(gid)
|
||||||
|
const rows = await this.deliveryReceipts.listThreadArchiveCsvExports(64)
|
||||||
|
let relisted = 0
|
||||||
|
for (const row of rows) {
|
||||||
|
if (this._shouldGossipThreadRegistryCsv(row)) relisted++
|
||||||
|
}
|
||||||
|
const watermark = Date.now()
|
||||||
|
this._threadCsvHealWatermark = watermark
|
||||||
|
span.end({
|
||||||
|
relisted,
|
||||||
|
watermark,
|
||||||
|
guildThreadCsvCount: rows.length,
|
||||||
|
bridgeKind: 'thread.csv'
|
||||||
|
})
|
||||||
|
return { relisted, watermark, guildThreadCsvCount: rows.length }
|
||||||
|
} catch (err) {
|
||||||
|
this.log.error('thread.csv error', {
|
||||||
|
guildId: gid,
|
||||||
|
context: 'heal.thread',
|
||||||
|
error: err?.message || String(err)
|
||||||
|
})
|
||||||
|
span.fail(err)
|
||||||
|
return { relisted: 0, error: err?.message || String(err) }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async _healForumTagExportCursorOnPartition (guildId) {
|
||||||
|
const gid = guildId || this.guild?.guild?.id || null
|
||||||
|
const span = this.log.time('thread.csv', {
|
||||||
|
spanKind: 'thread.csv',
|
||||||
|
guildId: gid,
|
||||||
|
context: 'heal.forum'
|
||||||
|
})
|
||||||
|
try {
|
||||||
|
if (!gid || !this.deliveryReceipts) {
|
||||||
|
span.end({ relisted: 0, skipped: true, guildThreadCsvCount: 0 })
|
||||||
|
return { relisted: 0, skipped: true }
|
||||||
|
}
|
||||||
|
await this._initDeliveryReceipts(gid)
|
||||||
|
const rows = await this.deliveryReceipts.listForumTagExportCsvExports(64)
|
||||||
|
let relisted = 0
|
||||||
|
for (const row of rows) {
|
||||||
|
if (this._shouldGossipForumTagExportCsv(row)) relisted++
|
||||||
|
}
|
||||||
|
const watermark = Date.now()
|
||||||
|
this._forumTagExportCsvHealWatermark = watermark
|
||||||
|
span.end({
|
||||||
|
relisted,
|
||||||
|
watermark,
|
||||||
|
guildThreadCsvCount: rows.length,
|
||||||
|
threadCount: rows[0]?.threadCount || 0,
|
||||||
|
bridgeKind: 'thread.csv'
|
||||||
|
})
|
||||||
|
return { relisted, watermark, threadCount: rows[0]?.threadCount || 0 }
|
||||||
|
} catch (err) {
|
||||||
|
this.log.error('thread.csv error', {
|
||||||
|
guildId: gid,
|
||||||
|
context: 'heal.forum',
|
||||||
|
error: err?.message || String(err)
|
||||||
|
})
|
||||||
|
span.fail(err)
|
||||||
|
return { relisted: 0, error: err?.message || String(err) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { threadCsvMixin }
|
||||||
Reference in New Issue
Block a user