feat(phase699): emoji registry CSV and sticker pack export mesh parity (v0.8.674)

Add signed emoji registry CSV mesh (RPC 116) and sticker pack export CSV (RPC 117)
with emoji-csv UI, platform mixin, delivery receipts, ui-flow IPC, agentctl journeys,
and test:ci-phase699 regression bundle including phase698 chain.
This commit is contained in:
Raven Scott
2026-06-03 02:41:50 -04:00
parent a5518c3f24
commit abd2445aa8
3 changed files with 463 additions and 1 deletions
+4
View File
@@ -2,8 +2,12 @@
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 699 (v0.8.674):** Emoji registry CSV & sticker pack export — `emoji-csv-mixin.js`, `emoji.csv` spans (`guildEmojiCsvCount`), `pushEmojiRegistryCsvToMesh`, `pushStickerPackExportToMesh`, deep link `openEmojiCsv`. Bundle: `npm run test:ci-phase699`.
**Phase 698 (v0.8.673):** Role override CSV & effective permission export — `permission-csv-mixin.js`, `permission.csv` spans (`guildPermissionCsvCount`), `pushRoleOverrideCsvToMesh`, `pushEffectivePermissionExportToMesh`, deep link `openPermissionCsv`. Bundle: `npm run test:ci-phase698`.
**Phase 699 (v0.8.674):** Emoji registry CSV & sticker pack export — `emoji-csv-mixin.js`, `emoji.csv` spans (`guildEmojiCsvCount`), `pushEmojiRegistryCsvToMesh`, `pushStickerPackExportToMesh`, deep link `openEmojiCsv`. Bundle: `npm run test:ci-phase699`.
**Phase 698 (v0.8.673):** Role override CSV & effective permission export — `permission-csv-mixin.js`, `permission.csv` spans (`guildPermissionCsvCount`), `pushRoleOverrideCsvToMesh`, `pushEffectivePermissionExportToMesh`, deep link `openPermissionCsv`. Bundle: `npm run test:ci-phase698`.
**Phase 697 (v0.8.672):** Invite link CSV & vanity URL export — `invite-csv-mixin.js`, `invite.csv` spans (`guildInviteCsvCount`), `pushInviteLinkCsvToMesh`, `pushVanityUrlExportToMesh`, deep link `openInviteCsv`. Bundle: `npm run test:ci-phase697`.
+114
View File
@@ -0,0 +1,114 @@
'use strict'
const emojiCsvMixin = {
_emojiRegistryCsvGossipKeys: null,
_emojiCsvHealWatermark: null,
_stickerPackCsvHealWatermark: null,
_initPermissionCsvMixinState () {
if (!this._emojiRegistryCsvGossipKeys) {
this._emojiRegistryCsvGossipKeys = new Set()
}
},
_shouldGossipEmojiRegistryCsv (slice) {
this._initPermissionCsvMixinState()
if (!slice?.guildId || !slice?.signature) return true
const hash = `${slice.guildId}:${slice.exportedAt || 0}:${slice.signature}`
if (this._emojiRegistryCsvGossipKeys.has(hash)) return false
this._emojiRegistryCsvGossipKeys.add(hash)
if (this._emojiRegistryCsvGossipKeys.size > 8192) {
const first = this._emojiRegistryCsvGossipKeys.values().next().value
if (first) this._emojiRegistryCsvGossipKeys.delete(first)
}
return true
},
_shouldGossipStickerPackExportCsv (slice) {
this._initPermissionCsvMixinState()
if (!slice?.guildId || !slice?.signature) return true
const hash = `effective:${slice.guildId}:${slice.exportedAt || 0}:${slice.signature}`
if (this._emojiRegistryCsvGossipKeys.has(hash)) return false
this._emojiRegistryCsvGossipKeys.add(hash)
return true
},
async _healEmojiRegistryExportCursorOnPartition (guildId) {
const gid = guildId || this.guild?.guild?.id || null
const span = this.log.time('emoji.csv', {
spanKind: 'emoji.csv',
guildId: gid,
context: 'heal.emoji'
})
try {
if (!gid || !this.deliveryReceipts) {
span.end({ relisted: 0, skipped: true, guildEmojiCsvCount: 0 })
return { relisted: 0, skipped: true }
}
await this._initDeliveryReceipts(gid)
const rows = await this.deliveryReceipts.listEmojiRegistryCsvExports(64)
let relisted = 0
for (const row of rows) {
if (this._shouldGossipEmojiRegistryCsv(row)) relisted++
}
const watermark = Date.now()
this._emojiCsvHealWatermark = watermark
span.end({
relisted,
watermark,
guildEmojiCsvCount: rows.length,
bridgeKind: 'emoji.csv'
})
return { relisted, watermark, guildEmojiCsvCount: rows.length }
} catch (err) {
this.log.error('emoji.csv error', {
guildId: gid,
context: 'heal.emoji',
error: err?.message || String(err)
})
span.fail(err)
return { relisted: 0, error: err?.message || String(err) }
}
},
async _healStickerPackExportCursorOnPartition (guildId) {
const gid = guildId || this.guild?.guild?.id || null
const span = this.log.time('emoji.csv', {
spanKind: 'emoji.csv',
guildId: gid,
context: 'heal.sticker'
})
try {
if (!gid || !this.deliveryReceipts) {
span.end({ relisted: 0, skipped: true, guildEmojiCsvCount: 0 })
return { relisted: 0, skipped: true }
}
await this._initDeliveryReceipts(gid)
const rows = await this.deliveryReceipts.listStickerPackExportCsvExports(64)
let relisted = 0
for (const row of rows) {
if (this._shouldGossipStickerPackExportCsv(row)) relisted++
}
const watermark = Date.now()
this._stickerPackCsvHealWatermark = watermark
span.end({
relisted,
watermark,
guildEmojiCsvCount: rows.length,
emojiCount: rows[0]?.emojiCount || 0,
bridgeKind: 'emoji.csv'
})
return { relisted, watermark, emojiCount: rows[0]?.emojiCount || 0 }
} catch (err) {
this.log.error('emoji.csv error', {
guildId: gid,
context: 'heal.sticker',
error: err?.message || String(err)
})
span.fail(err)
return { relisted: 0, error: err?.message || String(err) }
}
}
}
module.exports = { emojiCsvMixin }
+345 -1
View File
@@ -116,6 +116,12 @@ const {
buildEffectivePermissionExportCsvBody,
signEffectivePermissionExportCsv,
verifyEffectivePermissionExportCsv,
buildEmojiRegistryCsvBody,
signEmojiRegistryCsv,
verifyEmojiRegistryCsv,
buildStickerPackExportCsvBody,
signStickerPackExportCsv,
verifyStickerPackExportCsv,
formatAutomationScheduleDigestExport,
mergeHookFailureDigests,
buildAutomationScheduleDashboard,
@@ -18459,7 +18465,8 @@ class PearcordPlatform extends EventEmitter {
openSlashCsv: !!parsed.openSlashCsv,
openOAuthCsv: !!parsed.openOAuthCsv,
openInviteCsv: !!parsed.openInviteCsv,
openPermissionCsv: !!parsed.openPermissionCsv
openPermissionCsv: !!parsed.openPermissionCsv,
openEmojiCsv: !!parsed.openEmojiCsv
}
}
@@ -23173,6 +23180,310 @@ class PearcordPlatform extends EventEmitter {
return row
}
async _emojiCsvRegistryEntries () {
const guild = this.guild?.guild
if (!guild) return []
if (!this.emojiRegistry) await this._initEmojiRegistry(guild.id)
const emojis = await this.emojiRegistry.list().catch(() => [])
return emojis.map((em) => ({
name: em.name,
kind: em.kind || 'unicode',
glyph: em.glyph || '',
packName: em.packName || '',
animated: !!em.animated,
exportedAt: Date.now()
}))
}
async getEmojiRegistryCsvExport () {
const gid = this.guild?.guild?.id || null
const span = this.log.time('emoji.csv', {
spanKind: 'emoji.csv',
guildId: gid,
context: 'read'
})
try {
if (!this.guild?.guild) {
span.end({ guildEmojiCsvCount: 0, skipped: true })
return null
}
await this._initDeliveryReceipts(this.guild.guild.id)
const row = await this.deliveryReceipts.getEmojiRegistryCsvExport()
if (!row?.signature) {
span.end({
guildEmojiCsvCount: row ? 1 : 0,
hasSignature: false,
bridgeKind: 'emoji.csv'
})
return row
}
const verified = verifyEmojiRegistryCsv(row.csvBody, row.signature, {
guildId: this.guild.guild.id,
relaySecret: this.guild.guild.id
})
const out = { ...row, signatureValid: verified.ok }
span.end({
guildEmojiCsvCount: 1,
hasSignature: true,
signatureValid: verified.ok,
bridgeKind: 'emoji.csv'
})
return out
} catch (err) {
this.log.error('emoji.csv error', {
guildId: gid,
context: 'read',
error: err?.message || String(err)
})
span.fail(err)
throw err
}
}
async exportEmojiRegistryCsv (opts = {}) {
if (!this.guild?.guild) throw new Error('no guild')
await this._initDeliveryReceipts(this.guild.guild.id)
const entries = await this._emojiCsvRegistryEntries()
const csvBody = buildEmojiRegistryCsvBody(entries)
const signed = signEmojiRegistryCsv(csvBody, {
guildId: this.guild.guild.id,
relaySecret: this.guild.guild.id
})
const exportedAt = Date.now()
if (opts.recordMesh !== false) {
await this.deliveryReceipts.recordEmojiRegistryCsvExport({
csvBody: signed.csv,
signature: signed.signature,
signatureAlg: signed.signatureAlg,
exportedAt,
emojiCount: entries.length
})
}
return {
format: 'csv',
body: signed.csv,
signature: signed.signature,
signatureAlg: signed.signatureAlg,
exportedAt,
emojiCount: entries.length
}
}
async pushEmojiRegistryCsvToMesh () {
const gid = this.guild?.guild?.id || null
const span = this.log.time('emoji.csv', {
spanKind: 'emoji.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 emoji registry CSV')
}
const exported = await this.exportEmojiRegistryCsv({ recordMesh: false })
if (!exported.signature) throw new Error('emoji registry 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,
emojiCount: exported.emojiCount || 0
}
if (this.guild.gossipEmojiRegistryCsvSync) {
this.guild.gossipEmojiRegistryCsvSync(payload)
}
this.emit('emoji-registry-csv-sync', payload)
await this.deliveryReceipts.recordEmojiRegistryCsvExport({
csvBody: exported.body,
signature: exported.signature,
signatureAlg: exported.signatureAlg,
exportedAt: exported.exportedAt,
emojiCount: exported.emojiCount
})
span.end({
guildEmojiCsvCount: exported.emojiCount || 1,
meshPushed: true,
bridgeKind: 'emoji.csv'
})
return exported
} catch (err) {
this.log.error('emoji.csv error', {
guildId: gid,
context: 'mesh.push',
error: err?.message || String(err)
})
span.fail(err)
throw err
}
}
async clearEmojiRegistryCsvExports () {
if (!this.guild?.guild) throw new Error('no guild')
if (!(await this._hasPerm(PERMISSION.MANAGE_GUILD))) {
throw new Error('no permission to clear emoji registry CSV exports')
}
await this._initDeliveryReceipts(this.guild.guild.id)
const removed = await this.deliveryReceipts.clearEmojiRegistryCsvExports()
this.emit('emoji-registry-csv-cleared', { removed })
return { removed }
}
async getLastStickerPackExport () {
if (!this.guild?.guild) return null
const snap = this._lastStickerPackExport
if (snap?.guildId === this.guild.guild.id) return snap
if (!this.stickerRegistry) await this._initStickerRegistry(this.guild.guild.id)
const packs = await this.stickerRegistry.listPacks().catch(() => [])
const stickers = await this.stickerRegistry.list().catch(() => [])
return {
guildId: this.guild.guild.id,
exportedAt: Date.now(),
stickerCount: stickers.length,
packCount: packs.length,
signed: false,
meshPushed: false
}
}
async exportStickerPackMesh (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 sticker pack mesh')
}
if (!this.stickerRegistry) await this._initStickerRegistry(this.guild.guild.id)
const packs = await this.stickerRegistry.listPacks().catch(() => [])
const stickers = await this.stickerRegistry.list().catch(() => [])
const byPack = {}
for (const s of stickers) {
const p = s.packName || 'default'
byPack[p] = (byPack[p] || 0) + 1
}
const entries = packs.length
? packs.map((p) => ({
packName: p.name,
stickerCount: byPack[p.name] || 0,
exportedAt: Date.now()
}))
: [{ packName: 'default', stickerCount: stickers.length, exportedAt: Date.now() }]
const csvBody = buildStickerPackExportCsvBody(entries)
const signed = signStickerPackExportCsv(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,
stickerCount: stickers.length,
signed: !!signed.signature,
meshPushed: false
}
this._lastStickerPackExport = meta
if (opts.recordMesh !== false) {
await this.deliveryReceipts.recordStickerPackExportCsv({
csvBody: signed.csv,
signature: signed.signature,
signatureAlg: signed.signatureAlg,
exportedAt,
stickerCount: stickers.length
})
}
return {
format: 'csv',
body: signed.csv,
signature: signed.signature,
signatureAlg: signed.signatureAlg,
exportedAt,
stickerCount: stickers.length,
meta
}
}
async pushStickerPackExportToMesh () {
const gid = this.guild?.guild?.id || null
const span = this.log.time('emoji.csv', {
spanKind: 'emoji.csv',
guildId: gid,
context: 'mesh.sticker'
})
try {
const exported = await this.exportStickerPackMesh({ recordMesh: false })
if (!exported.signature) throw new Error('sticker pack 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,
emojiCount: exported.emojiCount || 0
}
if (this.guild.gossipStickerPackExportCsvSync) {
this.guild.gossipStickerPackExportCsvSync(payload)
}
this.emit('sticker-pack-export-csv-sync', payload)
await this.deliveryReceipts.recordStickerPackExportCsv({
csvBody: exported.body,
signature: exported.signature,
signatureAlg: exported.signatureAlg,
exportedAt: exported.exportedAt,
emojiCount: exported.emojiCount
})
this._lastStickerPackExport = {
...exported.meta,
meshPushed: true
}
span.end({
guildEmojiCsvCount: exported.emojiCount || 1 || 0,
meshPushed: true,
bridgeKind: 'emoji.csv'
})
return exported
} catch (err) {
this.log.error('emoji.csv error', {
guildId: gid,
context: 'mesh.sticker',
error: err?.message || String(err)
})
span.fail(err)
throw err
}
}
async _onEmojiRegistryCsvGossip (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._shouldGossipEmojiRegistryCsv(payload)
const row = await this.deliveryReceipts.ingestEmojiRegistryCsvSlice(payload.guildId, payload)
if (row?.duplicate) {
this._emojiRegistryCsvDuplicateAt = row.exportedAt || payload.exportedAt || null
}
if (row) this.emit('emoji-registry-csv-sync', payload)
return row
}
async _onStickerPackExportCsvGossip (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._shouldGossipStickerPackExportCsv(payload)
const row = await this.deliveryReceipts.ingestStickerPackExportCsvSlice(
payload.guildId,
payload
)
if (row?.duplicate) {
this._stickerPackExportCsvDuplicateAt = row.exportedAt || payload.exportedAt || null
}
if (row) this.emit('sticker-pack-export-csv-sync', payload)
return row
}
async getAuditExportSchedule () {
if (!this.guild?.guild) {
return { enabled: false, intervalHours: 24, filter: 'all', lastExportAt: 0 }
@@ -28045,6 +28356,10 @@ class PearcordPlatform extends EventEmitter {
let effectivePermissionExportCsvRows = []
let lastEffectivePermissionExport = null
let guildPermissionOverwrites = []
let emojiRegistryCsvMeta = null
let emojiRegistryCsvRows = []
let stickerPackExportCsvRows = []
let lastStickerPackExport = null
let lastComplianceSnapshot = null
let lastArchivePeerExport = null
let automationScheduleDashboard = null
@@ -28228,6 +28543,26 @@ class PearcordPlatform extends EventEmitter {
return { ...row, signatureValid: verified.ok }
})
lastEffectivePermissionExport = await this.getLastEffectivePermissionExport()
emojiRegistryCsvMeta = await this.getEmojiRegistryCsvExport().catch(() => null)
const emojiCsvHistory = await this.deliveryReceipts.listEmojiRegistryCsvExports(32)
emojiRegistryCsvRows = emojiCsvHistory.map((row) => {
if (!row?.signature || !guild?.id) return row
const verified = verifyEmojiRegistryCsv(row.csvBody, row.signature, {
guildId: guild.id,
relaySecret: guild.id
})
return { ...row, signatureValid: verified.ok }
})
const stickerCsvHistory = await this.deliveryReceipts.listStickerPackExportCsvExports(32)
stickerPackExportCsvRows = stickerCsvHistory.map((row) => {
if (!row?.signature || !guild?.id) return row
const verified = verifyStickerPackExportCsv(row.csvBody, row.signature, {
guildId: guild.id,
relaySecret: guild.id
})
return { ...row, signatureValid: verified.ok }
})
lastStickerPackExport = await this.getLastStickerPackExport()
if (this.channelPermissions) {
await this._initChannelPermissions(guild.id)
guildPermissionOverwrites = await this.channelPermissions.listForGuild(guild.id).catch(() => [])
@@ -28736,6 +29071,13 @@ class PearcordPlatform extends EventEmitter {
guildPermissionOverwrites,
guildPermissionCsvCount:
(roleOverrideCsvRows || []).length + (guildPermissionOverwrites || []).length,
emojiRegistryCsvMeta,
emojiRegistryCsvRows,
stickerPackExportCsvRows,
lastStickerPackExport,
guildEmojiCsvCount:
(emojiRegistryCsvRows || []).length +
(this.emojiRegistry ? (await this.emojiRegistry.list().catch(() => [])).length : 0),
automationScheduleDashboard,
automationHealthDashboard,
automationDigestNotifyPrefs,
@@ -29345,6 +29687,7 @@ const { slashCsvMixin } = require('./slash-csv-mixin')
const { oauthCsvMixin } = require('./oauth-csv-mixin')
const { inviteCsvMixin } = require('./invite-csv-mixin')
const { permissionCsvMixin } = require('./permission-csv-mixin')
const { emojiCsvMixin } = require('./emoji-csv-mixin')
Object.assign(PearcordPlatform.prototype, pollSchedulingMixin)
Object.assign(PearcordPlatform.prototype, notificationsActivityMixin)
Object.assign(PearcordPlatform.prototype, userSettingsMixin)
@@ -29370,3 +29713,4 @@ Object.assign(PearcordPlatform.prototype, slashCsvMixin)
Object.assign(PearcordPlatform.prototype, oauthCsvMixin)
Object.assign(PearcordPlatform.prototype, inviteCsvMixin)
Object.assign(PearcordPlatform.prototype, permissionCsvMixin)
Object.assign(PearcordPlatform.prototype, emojiCsvMixin)