feat: wire session JSON mesh push, gossip, and view state (v0.8.690)
Implement pushSessionActivityJsonToMesh, pushAvatarDecorationExportToMesh, gossip handlers, partition heal hooks, and sessionJsonMixin assignment. Populate sessionActivityJsonMeta/rows and avatar decoration export in guild view. Co-authored-by: Cursor <[email protected]>
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 714 (v0.8.690):** Session activity JSON & avatar decoration export — `session-json-mixin.js`, `session.json` spans (`guildSessionJsonCount`), `pushSessionActivityJsonToMesh`, `pushAvatarDecorationExportToMesh`, deep link `openSessionJson`. Bundle: `npm run test:ci-phase714`.
|
||||
|
||||
**Phase 711 (v0.8.686):** Activity invite JSON & profile banner export — `activity-json-mixin.js`, `activity.json` spans (`guildActivityJsonCount`), `pushActivityInviteJsonToMesh`, `pushProfileBannerExportToMesh`, deep link `openActivityJson`. Bundle: `npm run test:ci-phase711`.
|
||||
|
||||
**Phase 710 (v0.8.685):** Custom status JSON & guild widget export — `status-json-mixin.js`, `status.json` spans (`guildStatusJsonCount`), `pushCustomStatusJsonToMesh`, `pushGuildWidgetExportToMesh`, deep link `openStatusJson`. Bundle: `npm run test:ci-phase710`.
|
||||
|
||||
@@ -194,6 +194,12 @@ const {
|
||||
buildProfileBannerExportJsonBody,
|
||||
signProfileBannerExportJson,
|
||||
verifyProfileBannerExportJson,
|
||||
buildSessionActivityJsonBody,
|
||||
signSessionActivityJson,
|
||||
verifySessionActivityJson,
|
||||
buildAvatarDecorationExportJsonBody,
|
||||
signAvatarDecorationExportJson,
|
||||
verifyAvatarDecorationExportJson,
|
||||
formatAutomationScheduleDigestExport,
|
||||
mergeHookFailureDigests,
|
||||
buildAutomationScheduleDashboard,
|
||||
@@ -1797,6 +1803,12 @@ class PearcordPlatform extends EventEmitter {
|
||||
const vanityUrlJsonHeal = await this._healVanityUrlExportCursorOnPartition(gid).catch(() => ({
|
||||
relisted: 0
|
||||
}))
|
||||
const sessionActivityJsonHeal = await this._healSessionActivityExportCursorOnPartition(gid).catch(() => ({
|
||||
relisted: 0
|
||||
}))
|
||||
const avatarDecorationExportJsonHeal = await this._healAvatarDecorationExportCursorOnPartition(gid).catch(() => ({
|
||||
relisted: 0
|
||||
}))
|
||||
return {
|
||||
voiceApplied,
|
||||
emojiSlots,
|
||||
@@ -1857,7 +1869,9 @@ class PearcordPlatform extends EventEmitter {
|
||||
oauthGrantJsonHeal,
|
||||
oauthInstallTokenJsonHeal,
|
||||
inviteLinkJsonHeal,
|
||||
vanityUrlJsonHeal
|
||||
vanityUrlJsonHeal,
|
||||
sessionActivityJsonHeal,
|
||||
avatarDecorationExportJsonHeal
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11190,6 +11204,12 @@ class PearcordPlatform extends EventEmitter {
|
||||
guildInstance.on('profile-banner-export-json-sync', (payload) => {
|
||||
this._onProfileBannerExportJsonGossip(payload).catch(() => {})
|
||||
})
|
||||
guildInstance.on('session-activity-json-sync', (payload) => {
|
||||
this._onSessionActivityJsonGossip(payload).catch(() => {})
|
||||
})
|
||||
guildInstance.on('avatar-decoration-export-json-sync', (payload) => {
|
||||
this._onAvatarDecorationExportJsonGossip(payload).catch(() => {})
|
||||
})
|
||||
guildInstance.on('message-search-request', (payload) => {
|
||||
this._onMessageSearchRequestGossip(payload).catch(() => {})
|
||||
})
|
||||
@@ -26676,6 +26696,172 @@ class PearcordPlatform extends EventEmitter {
|
||||
return row
|
||||
}
|
||||
|
||||
async _sessionJsonRegistryEntries () {
|
||||
if (!this.guild?.guild) return []
|
||||
const selfId = this.identity?.user?.id || ''
|
||||
const channels = (await this.guild.listChannels().catch(() => [])) || []
|
||||
const entries = []
|
||||
const seen = new Set()
|
||||
const pushSession = (uid, sessionName, sessionActivity, parentId, durationSec = 0, exportedAt = Date.now()) => {
|
||||
const key = `${uid}:${parentId || sessionName}`
|
||||
if (seen.has(key)) return
|
||||
entries.push({
|
||||
userId: uid,
|
||||
sessionName: sessionName || 'session',
|
||||
sessionActivity: sessionActivity || 'desktop',
|
||||
parentId: parentId || null,
|
||||
durationSec,
|
||||
exportedAt
|
||||
})
|
||||
seen.add(key)
|
||||
}
|
||||
for (const ch of channels.filter((c) => c.type === 'voice' || c.type === 'stage').slice(0, 32)) {
|
||||
pushSession(selfId, ch.name, ch.type, ch.id, 0)
|
||||
}
|
||||
if (selfId && this.presence?.activity) {
|
||||
pushSession(selfId, this.presence.activity.name || 'Pearcord', this.presence.activity.type || 'playing', null, this.presence.activity.durationSec || 0)
|
||||
}
|
||||
if (!entries.length) {
|
||||
pushSession(selfId, 'Pearcord', 'desktop', null, 0)
|
||||
}
|
||||
return entries
|
||||
}
|
||||
|
||||
async getSessionActivityJsonExport () {
|
||||
const gid = this.guild?.guild?.id || null
|
||||
const span = this.log.time('session.json', { spanKind: 'session.json', guildId: gid, context: 'read' })
|
||||
try {
|
||||
if (!this.guild?.guild) { span.end({ guildSessionJsonCount: 0, skipped: true }); return null }
|
||||
await this._initDeliveryReceipts(this.guild.guild.id)
|
||||
const row = await this.deliveryReceipts.getSessionActivityJsonExport()
|
||||
if (!row?.signature) {
|
||||
span.end({ guildSessionJsonCount: row ? 1 : 0, hasSignature: false, bridgeKind: 'session.json' })
|
||||
return row
|
||||
}
|
||||
const verified = verifySessionActivityJson(exportBodyFromSlice(row) || row.jsonBody, row.signature, { guildId: this.guild.guild.id, relaySecret: this.guild.guild.id })
|
||||
const out = { ...row, signatureValid: verified.ok, sessionCount: row.sessionCount || 0 }
|
||||
span.end({ guildSessionJsonCount: 1, hasSignature: true, signatureValid: verified.ok, bridgeKind: 'session.json' })
|
||||
return out
|
||||
} catch (err) {
|
||||
this.log.error('session.json error', { guildId: gid, context: 'read', error: err?.message || String(err) })
|
||||
span.fail(err); throw err
|
||||
}
|
||||
}
|
||||
|
||||
async exportSessionActivityJson (opts = {}) {
|
||||
if (!this.guild?.guild) throw new Error('no guild')
|
||||
await this._initDeliveryReceipts(this.guild.guild.id)
|
||||
const entries = await this._sessionJsonRegistryEntries()
|
||||
const jsonBody = buildSessionActivityJsonBody(entries)
|
||||
const signed = signSessionActivityJson(jsonBody, { guildId: this.guild.guild.id, relaySecret: this.guild.guild.id })
|
||||
const exportedAt = Date.now()
|
||||
if (opts.recordMesh !== false) {
|
||||
await this.deliveryReceipts.recordSessionActivityJsonExport({ jsonBody: signed.jsonBody || signed.json, signature: signed.signature, signatureAlg: signed.signatureAlg, exportedAt, sessionCount: entries.length })
|
||||
}
|
||||
return { format: 'json', body: signed.json, signature: signed.signature, signatureAlg: signed.signatureAlg, exportedAt, sessionCount: entries.length }
|
||||
}
|
||||
|
||||
async pushSessionActivityJsonToMesh () {
|
||||
const gid = this.guild?.guild?.id || null
|
||||
const span = this.log.time('session.json', { spanKind: 'session.json', 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 session activity JSON')
|
||||
const exported = await this.exportSessionActivityJson({ recordMesh: false })
|
||||
if (!exported.signature) throw new Error('session activity json export not signed')
|
||||
const payload = { guildId: this.guild.guild.id, exportedAt: exported.exportedAt || Date.now(), exportedBy: this.identity.user?.id || null, jsonBody: exported.body, signature: exported.signature, signatureAlg: exported.signatureAlg, sessionCount: exported.sessionCount || 0 }
|
||||
if (this.guild.gossipSessionActivityJsonSync) this.guild.gossipSessionActivityJsonSync(payload)
|
||||
this.emit('session-activity-json-sync', payload)
|
||||
await this.deliveryReceipts.recordSessionActivityJsonExport({ jsonBody: exported.body, signature: exported.signature, signatureAlg: exported.signatureAlg, exportedAt: exported.exportedAt, sessionCount: exported.sessionCount })
|
||||
span.end({ guildSessionJsonCount: exported.sessionCount || 1, meshPushed: true, bridgeKind: 'session.json' })
|
||||
return exported
|
||||
} catch (err) {
|
||||
this.log.error('session.json error', { guildId: gid, context: 'mesh.push', error: err?.message || String(err) })
|
||||
span.fail(err); throw err
|
||||
}
|
||||
}
|
||||
|
||||
async clearSessionActivityJsonExports () {
|
||||
if (!this.guild?.guild) throw new Error('no guild')
|
||||
if (!(await this._hasPerm(PERMISSION.MANAGE_GUILD))) throw new Error('no permission to clear session activity JSON exports')
|
||||
await this._initDeliveryReceipts(this.guild.guild.id)
|
||||
const removed = await this.deliveryReceipts.clearSessionActivityJsonExports()
|
||||
this.emit('session-activity-json-cleared', { removed })
|
||||
return { removed }
|
||||
}
|
||||
|
||||
async getLastAvatarDecorationExport () {
|
||||
if (!this.guild?.guild) return null
|
||||
const snap = this._lastAvatarDecorationExport
|
||||
if (snap?.guildId === this.guild.guild.id) return snap
|
||||
const user = this.identity?.user || {}
|
||||
return { guildId: this.guild.guild.id, userId: user.id || null, exportedAt: Date.now(), decorationCount: 0, signed: false, meshPushed: false }
|
||||
}
|
||||
|
||||
async exportAvatarDecorationMesh (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 avatar decoration mesh')
|
||||
const user = this.identity?.user || {}
|
||||
const entries = [{
|
||||
userId: user.id || '',
|
||||
displayName: user.displayName || user.username || '',
|
||||
avatarDecoration: user.avatarDecoration || user.avatarDecorationHash || '',
|
||||
accentColor: user.accentColor || '',
|
||||
exportedAt: Date.now()
|
||||
}]
|
||||
const jsonBody = buildAvatarDecorationExportJsonBody(entries)
|
||||
const signed = signAvatarDecorationExportJson(jsonBody, { guildId: this.guild.guild.id, relaySecret: this.guild.guild.id })
|
||||
const exportedAt = Date.now()
|
||||
const meta = { guildId: this.guild.guild.id, exportedAt, exportedBy: user.id || null, decorationCount: entries.length, signed: !!signed.signature, meshPushed: false }
|
||||
this._lastAvatarDecorationExport = meta
|
||||
if (opts.recordMesh !== false) {
|
||||
await this.deliveryReceipts.recordAvatarDecorationExportJson({ jsonBody: signed.jsonBody || signed.json, signature: signed.signature, signatureAlg: signed.signatureAlg, exportedAt, decorationCount: entries.length })
|
||||
}
|
||||
return { format: 'json', body: signed.json, signature: signed.signature, signatureAlg: signed.signatureAlg, exportedAt, decorationCount: entries.length, meta }
|
||||
}
|
||||
|
||||
async pushAvatarDecorationExportToMesh () {
|
||||
const gid = this.guild?.guild?.id || null
|
||||
const span = this.log.time('session.json', { spanKind: 'session.json', guildId: gid, context: 'mesh.avatar-decoration' })
|
||||
try {
|
||||
const exported = await this.exportAvatarDecorationMesh({ recordMesh: false })
|
||||
if (!exported.signature) throw new Error('avatar decoration export json not signed')
|
||||
const payload = { guildId: this.guild.guild.id, exportedAt: exported.exportedAt || Date.now(), exportedBy: this.identity.user?.id || null, jsonBody: exported.body, signature: exported.signature, signatureAlg: exported.signatureAlg, decorationCount: exported.decorationCount || 0 }
|
||||
if (this.guild.gossipAvatarDecorationExportJsonSync) this.guild.gossipAvatarDecorationExportJsonSync(payload)
|
||||
this.emit('avatar-decoration-export-json-sync', payload)
|
||||
await this.deliveryReceipts.recordAvatarDecorationExportJson({ jsonBody: exported.body, signature: exported.signature, signatureAlg: exported.signatureAlg, exportedAt: exported.exportedAt, decorationCount: exported.decorationCount })
|
||||
this._lastAvatarDecorationExport = { ...exported.meta, meshPushed: true }
|
||||
span.end({ guildSessionJsonCount: exported.decorationCount || 0, meshPushed: true, bridgeKind: 'session.json' })
|
||||
return exported
|
||||
} catch (err) {
|
||||
this.log.error('session.json error', { guildId: gid, context: 'mesh.avatar-decoration', error: err?.message || String(err) })
|
||||
span.fail(err); throw err
|
||||
}
|
||||
}
|
||||
|
||||
async _onSessionActivityJsonGossip (payload) {
|
||||
if (!payload?.guildId || !payload?.jsonBody || !payload?.signature) return null
|
||||
if (this.guild?.guild?.id !== payload.guildId) return null
|
||||
await this._initDeliveryReceipts(payload.guildId)
|
||||
this._shouldGossipSessionActivityRegistryJson(payload)
|
||||
const row = await this.deliveryReceipts.ingestSessionActivityJsonSlice(payload.guildId, payload)
|
||||
if (row?.duplicate) this._sessionActivityJsonDuplicateAt = row.exportedAt || payload.exportedAt || null
|
||||
if (row) this.emit('session-activity-json-sync', payload)
|
||||
return row
|
||||
}
|
||||
|
||||
async _onAvatarDecorationExportJsonGossip (payload) {
|
||||
if (!payload?.guildId || !payload?.jsonBody || !payload?.signature) return null
|
||||
if (this.guild?.guild?.id !== payload.guildId) return null
|
||||
await this._initDeliveryReceipts(payload.guildId)
|
||||
this._shouldGossipAvatarDecorationExportJson(payload)
|
||||
const row = await this.deliveryReceipts.ingestAvatarDecorationExportJsonSlice(payload.guildId, payload)
|
||||
if (row?.duplicate) this._avatarDecorationExportJsonDuplicateAt = row.exportedAt || payload.exportedAt || null
|
||||
if (row) this.emit('avatar-decoration-export-json-sync', payload)
|
||||
return row
|
||||
}
|
||||
|
||||
|
||||
async getAuditExportSchedule () {
|
||||
if (!this.guild?.guild) {
|
||||
@@ -31612,6 +31798,11 @@ class PearcordPlatform extends EventEmitter {
|
||||
let profileBannerExportJsonRows = []
|
||||
let lastProfileBannerExport = null
|
||||
let guildActivityInvites = []
|
||||
let sessionActivityJsonMeta = null
|
||||
let sessionActivityJsonRows = []
|
||||
let avatarDecorationExportJsonRows = []
|
||||
let lastAvatarDecorationExport = null
|
||||
let guildSessionActivities = []
|
||||
let lastComplianceSnapshot = null
|
||||
let lastArchivePeerExport = null
|
||||
let automationScheduleDashboard = null
|
||||
@@ -31859,6 +32050,29 @@ class PearcordPlatform extends EventEmitter {
|
||||
localVersion: this.appVersion
|
||||
})
|
||||
}
|
||||
sessionActivityJsonMeta = await this.getSessionActivityJsonExport().catch(() => null)
|
||||
sessionActivityJsonRows = (await this.deliveryReceipts.listSessionActivityJsonExports(32)).map((row) => {
|
||||
if (!row?.signature || !guild?.id) return row
|
||||
const verified = verifySessionActivityJson(exportBodyFromSlice(row) || row.jsonBody, row.signature, { guildId: guild.id, relaySecret: guild.id })
|
||||
return { ...row, signatureValid: verified.ok }
|
||||
})
|
||||
avatarDecorationExportJsonRows = (await this.deliveryReceipts.listAvatarDecorationExportJsonExports(32)).map((row) => {
|
||||
if (!row?.signature || !guild?.id) return row
|
||||
const verified = verifyAvatarDecorationExportJson(exportBodyFromSlice(row) || row.jsonBody, row.signature, { guildId: guild.id, relaySecret: guild.id })
|
||||
return { ...row, signatureValid: verified.ok }
|
||||
})
|
||||
lastAvatarDecorationExport = await this.getLastAvatarDecorationExport()
|
||||
guildSessionActivities = channels
|
||||
.filter((c) => c.type === 'voice' || c.type === 'stage')
|
||||
.slice(0, 16)
|
||||
.map((c) => ({
|
||||
id: c.id,
|
||||
name: c.name,
|
||||
role: c.type,
|
||||
parentId: c.parentId || null,
|
||||
createdAt: c.createdAt || null,
|
||||
updatedAt: c.updatedAt || Date.now()
|
||||
}))
|
||||
}
|
||||
const auditLogFilterOptions = [
|
||||
{ id: 'all', label: 'All' },
|
||||
@@ -32204,6 +32418,10 @@ class PearcordPlatform extends EventEmitter {
|
||||
this._activityInviteJsonDuplicateAt = null
|
||||
const profileBannerExportJsonDuplicateAt = this._profileBannerExportJsonDuplicateAt || null
|
||||
this._profileBannerExportJsonDuplicateAt = null
|
||||
const sessionActivityJsonDuplicateAt = this._sessionActivityJsonDuplicateAt || null
|
||||
this._sessionActivityJsonDuplicateAt = null
|
||||
const avatarDecorationExportJsonDuplicateAt = this._avatarDecorationExportJsonDuplicateAt || null
|
||||
this._avatarDecorationExportJsonDuplicateAt = null
|
||||
return {
|
||||
onboarded: this.onboarded,
|
||||
sessionReady: this._sessionReady,
|
||||
@@ -32377,6 +32595,8 @@ class PearcordPlatform extends EventEmitter {
|
||||
guildWidgetExportJsonDuplicateAt,
|
||||
activityInviteJsonDuplicateAt,
|
||||
profileBannerExportJsonDuplicateAt,
|
||||
sessionActivityJsonDuplicateAt,
|
||||
avatarDecorationExportJsonDuplicateAt,
|
||||
digestRelayHandoffJsonMeta,
|
||||
digestRelayHandoffJsonRows,
|
||||
archivePeerExportJsonRows,
|
||||
@@ -32500,6 +32720,13 @@ class PearcordPlatform extends EventEmitter {
|
||||
(customStatusJsonRows || []).length + (guildCustomStatuses || []).length,
|
||||
guildActivityJsonCount:
|
||||
(activityInviteJsonRows || []).length + (guildActivityInvites || []).length,
|
||||
sessionActivityJsonMeta,
|
||||
sessionActivityJsonRows,
|
||||
avatarDecorationExportJsonRows,
|
||||
lastAvatarDecorationExport,
|
||||
guildSessionActivities,
|
||||
guildSessionJsonCount:
|
||||
(sessionActivityJsonRows || []).length + (guildSessionActivities || []).length,
|
||||
automationScheduleDashboard,
|
||||
automationHealthDashboard,
|
||||
automationDigestNotifyPrefs,
|
||||
@@ -33122,6 +33349,7 @@ const { presenceJsonMixin } = require('./presence-json-mixin')
|
||||
const { notificationJsonMixin } = require('./notification-json-mixin')
|
||||
const { statusJsonMixin } = require('./status-json-mixin')
|
||||
const { activityJsonMixin } = require('./activity-json-mixin')
|
||||
const { sessionJsonMixin } = require('./session-json-mixin')
|
||||
Object.assign(PearcordPlatform.prototype, pollSchedulingMixin)
|
||||
Object.assign(PearcordPlatform.prototype, notificationsActivityMixin)
|
||||
Object.assign(PearcordPlatform.prototype, userSettingsMixin)
|
||||
@@ -33160,3 +33388,4 @@ Object.assign(PearcordPlatform.prototype, presenceJsonMixin)
|
||||
Object.assign(PearcordPlatform.prototype, notificationJsonMixin)
|
||||
Object.assign(PearcordPlatform.prototype, statusJsonMixin)
|
||||
Object.assign(PearcordPlatform.prototype, activityJsonMixin)
|
||||
Object.assign(PearcordPlatform.prototype, sessionJsonMixin)
|
||||
|
||||
Reference in New Issue
Block a user