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.
|
||||
|
||||
**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 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,
|
||||
signReactionSummaryExportCsv,
|
||||
verifyReactionSummaryExportCsv,
|
||||
buildThreadArchiveCsvBody,
|
||||
signThreadArchiveCsv,
|
||||
verifyThreadArchiveCsv,
|
||||
buildForumTagExportCsvBody,
|
||||
signForumTagExportCsv,
|
||||
verifyForumTagExportCsv,
|
||||
formatAutomationScheduleDigestExport,
|
||||
mergeHookFailureDigests,
|
||||
buildAutomationScheduleDashboard,
|
||||
@@ -11093,6 +11099,12 @@ class PearcordPlatform extends EventEmitter {
|
||||
guildInstance.on('reaction-summary-export-csv-sync', (payload) => {
|
||||
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) => {
|
||||
this._onMessageSearchRequestGossip(payload).catch(() => {})
|
||||
})
|
||||
@@ -18524,7 +18536,8 @@ class PearcordPlatform extends EventEmitter {
|
||||
openMemberCsv: !!parsed.openMemberCsv,
|
||||
openBanCsv: !!parsed.openBanCsv,
|
||||
openPollCsv: !!parsed.openPollCsv,
|
||||
openPinCsv: !!parsed.openPinCsv
|
||||
openPinCsv: !!parsed.openPinCsv,
|
||||
openThreadCsv: !!parsed.openThreadCsv
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23900,7 +23913,7 @@ class PearcordPlatform extends EventEmitter {
|
||||
try {
|
||||
if (!this.guild?.guild) throw new Error('no 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 })
|
||||
if (!exported.signature) throw new Error('ban registry csv export not signed')
|
||||
@@ -24593,6 +24606,270 @@ class PearcordPlatform extends EventEmitter {
|
||||
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 () {
|
||||
if (!this.guild?.guild) {
|
||||
return { enabled: false, intervalHours: 24, filter: 'all', lastExportAt: 0 }
|
||||
@@ -29488,6 +29765,11 @@ class PearcordPlatform extends EventEmitter {
|
||||
let reactionSummaryExportCsvRows = []
|
||||
let lastReactionSummaryExport = null
|
||||
let guildPins = []
|
||||
let threadArchiveCsvMeta = null
|
||||
let threadArchiveCsvRows = []
|
||||
let forumTagExportCsvRows = []
|
||||
let lastForumTagExport = null
|
||||
let guildThreads = []
|
||||
let lastComplianceSnapshot = null
|
||||
let lastArchivePeerExport = null
|
||||
let automationScheduleDashboard = null
|
||||
@@ -30048,6 +30330,10 @@ class PearcordPlatform extends EventEmitter {
|
||||
this._pinRegistryCsvDuplicateAt = null
|
||||
const reactionSummaryExportCsvDuplicateAt = this._reactionSummaryExportCsvDuplicateAt || null
|
||||
this._reactionSummaryExportCsvDuplicateAt = null
|
||||
const threadArchiveCsvDuplicateAt = this._threadArchiveCsvDuplicateAt || null
|
||||
this._threadArchiveCsvDuplicateAt = null
|
||||
const forumTagExportCsvDuplicateAt = this._forumTagExportCsvDuplicateAt || null
|
||||
this._forumTagExportCsvDuplicateAt = null
|
||||
return {
|
||||
onboarded: this.onboarded,
|
||||
sessionReady: this._sessionReady,
|
||||
@@ -30205,6 +30491,8 @@ class PearcordPlatform extends EventEmitter {
|
||||
scheduledMessageExportCsvDuplicateAt,
|
||||
pinRegistryCsvDuplicateAt,
|
||||
reactionSummaryExportCsvDuplicateAt,
|
||||
threadArchiveCsvDuplicateAt,
|
||||
forumTagExportCsvDuplicateAt,
|
||||
digestRelayHandoffCsvMeta,
|
||||
digestRelayHandoffCsvRows,
|
||||
archivePeerExportCsvRows,
|
||||
@@ -30272,6 +30560,13 @@ class PearcordPlatform extends EventEmitter {
|
||||
guildPins,
|
||||
guildPinCsvCount:
|
||||
(pinRegistryCsvRows || []).length + (guildPins || []).length,
|
||||
threadArchiveCsvMeta,
|
||||
threadArchiveCsvRows,
|
||||
forumTagExportCsvRows,
|
||||
lastForumTagExport,
|
||||
guildThreads,
|
||||
guildThreadCsvCount:
|
||||
(threadArchiveCsvRows || []).length + (guildThreads || []).length,
|
||||
automationScheduleDashboard,
|
||||
automationHealthDashboard,
|
||||
automationDigestNotifyPrefs,
|
||||
@@ -30886,6 +31181,7 @@ const { memberCsvMixin } = require('./member-csv-mixin')
|
||||
const { banCsvMixin } = require('./ban-csv-mixin')
|
||||
const { pollCsvMixin } = require('./poll-csv-mixin')
|
||||
const { pinCsvMixin } = require('./pin-csv-mixin')
|
||||
const { threadCsvMixin } = require('./thread-csv-mixin')
|
||||
Object.assign(PearcordPlatform.prototype, pollSchedulingMixin)
|
||||
Object.assign(PearcordPlatform.prototype, notificationsActivityMixin)
|
||||
Object.assign(PearcordPlatform.prototype, userSettingsMixin)
|
||||
@@ -30916,3 +31212,4 @@ Object.assign(PearcordPlatform.prototype, memberCsvMixin)
|
||||
Object.assign(PearcordPlatform.prototype, banCsvMixin)
|
||||
Object.assign(PearcordPlatform.prototype, pollCsvMixin)
|
||||
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