feat(v0.8.672): Phase 697 invite link CSV and vanity URL mesh parity
Signed invite link CSV (listGuildInvites) and vanity URL export mesh follow the OAuth/slash CSV pattern: invite-csv-ui, invite-csv-mixin, delivery-receipts collections, guild gossip RPC 112/113, ui-flow IPC, deep link ?invite-csv=1, invite-errors filter, and agentctl invite + vanity journeys. Repairs app.js bot/slash/oauth modal corruption from prior session. Adds test:ci-phase697, docs INVITE_LINK_CSV.md, RELEASE_v0.8.672, and Phase 698 roadmap.
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 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`.
|
||||||
|
|
||||||
**Phase 696 (v0.8.671):** OAuth grant CSV & install token export — `oauth-csv-mixin.js`, `oauth.csv` spans (`guildOAuthGrantCount`), `pushOAuthGrantCsvToMesh`, `pushOAuthInstallTokenExportToMesh`, deep link `openOAuthCsv`. Bundle: `npm run test:ci-phase696`.
|
**Phase 696 (v0.8.671):** OAuth grant CSV & install token export — `oauth-csv-mixin.js`, `oauth.csv` spans (`guildOAuthGrantCount`), `pushOAuthGrantCsvToMesh`, `pushOAuthInstallTokenExportToMesh`, deep link `openOAuthCsv`. Bundle: `npm run test:ci-phase696`.
|
||||||
|
|
||||||
**Phase 695 (v0.8.670):** Slash registry CSV & invoke audit — `slash-csv-mixin.js`, `slash.csv` spans (`guildSlashRegistryCount`), `pushSlashRegistryCsvToMesh`, `pushSlashInvokeAuditExportToMesh`, deep link `openSlashCsv`. Bundle: `npm run test:ci-phase695`.
|
**Phase 695 (v0.8.670):** Slash registry CSV & invoke audit — `slash-csv-mixin.js`, `slash.csv` spans (`guildSlashRegistryCount`), `pushSlashRegistryCsvToMesh`, `pushSlashInvokeAuditExportToMesh`, deep link `openSlashCsv`. Bundle: `npm run test:ci-phase695`.
|
||||||
@@ -38,6 +40,8 @@ Application facade: one `PearcordPlatform` class that wires identity, database,
|
|||||||
|
|
||||||
**Phase 679 (v0.8.654):** Thread panel & forum parity — `threads-forum.js` mixin, `createThread` auto-archive, `_healThreadMetadataOnPartition` watermark + auto-archive heal, guild-scoped `buildThreadsPanel`, deep link `create-thread` query. Bundle: `npm run test:ci-phase679`.
|
**Phase 679 (v0.8.654):** Thread panel & forum parity — `threads-forum.js` mixin, `createThread` auto-archive, `_healThreadMetadataOnPartition` watermark + auto-archive heal, guild-scoped `buildThreadsPanel`, deep link `create-thread` query. Bundle: `npm run test:ci-phase679`.
|
||||||
|
|
||||||
|
**Phase 697 (v0.8.672):** Invite link CSV uses `invites-discovery.js` + `listGuildInvites` for signed invite rows; vanity export from `vanityInviteCode` prefs. See [INVITE_LINK_CSV.md](../../docs/INVITE_LINK_CSV.md).
|
||||||
|
|
||||||
**Phase 678 (v0.8.653):** Invites & discovery join — `invites-discovery.js` mixin (`listGuildInvites`, `_healInvitesOnPartition`, `_healDiscoveryListingsOnPartition`, `_shouldGossipInvite`); view `guildInvites`, `inviteCount`, `discoveryListingVersion`; `createInvite` ttl/maxUses validation. Bundle: `npm run test:ci-phase678`.
|
**Phase 678 (v0.8.653):** Invites & discovery join — `invites-discovery.js` mixin (`listGuildInvites`, `_healInvitesOnPartition`, `_healDiscoveryListingsOnPartition`, `_shouldGossipInvite`); view `guildInvites`, `inviteCount`, `discoveryListingVersion`; `createInvite` ttl/maxUses validation. Bundle: `npm run test:ci-phase678`.
|
||||||
|
|
||||||
**Phase 677 (v0.8.652):** Moderation & audit log — `moderation.js` mixin (`unbanMember`, audit/ban gossip dedupe, `_healAuditLogOnPartition`, `_healBanListOnPartition`, `_assertCanModerateMember`, `moderation.action` spans). Bundle: `npm run test:ci-phase677`.
|
**Phase 677 (v0.8.652):** Moderation & audit log — `moderation.js` mixin (`unbanMember`, audit/ban gossip dedupe, `_healAuditLogOnPartition`, `_healBanListOnPartition`, `_assertCanModerateMember`, `moderation.action` spans). Bundle: `npm run test:ci-phase677`.
|
||||||
|
|||||||
@@ -104,6 +104,12 @@ const {
|
|||||||
buildOAuthInstallTokenExportCsvBody,
|
buildOAuthInstallTokenExportCsvBody,
|
||||||
signOAuthInstallTokenExportCsv,
|
signOAuthInstallTokenExportCsv,
|
||||||
verifyOAuthInstallTokenExportCsv,
|
verifyOAuthInstallTokenExportCsv,
|
||||||
|
buildInviteLinkCsvBody,
|
||||||
|
signInviteLinkCsv,
|
||||||
|
verifyInviteLinkCsv,
|
||||||
|
buildVanityUrlExportCsvBody,
|
||||||
|
signVanityUrlExportCsv,
|
||||||
|
verifyVanityUrlExportCsv,
|
||||||
formatAutomationScheduleDigestExport,
|
formatAutomationScheduleDigestExport,
|
||||||
mergeHookFailureDigests,
|
mergeHookFailureDigests,
|
||||||
buildAutomationScheduleDashboard,
|
buildAutomationScheduleDashboard,
|
||||||
@@ -1700,6 +1706,12 @@ class PearcordPlatform extends EventEmitter {
|
|||||||
const oauthInstallTokenCsvHeal = await this._healOAuthInstallTokenExportCursorOnPartition(gid).catch(() => ({
|
const oauthInstallTokenCsvHeal = await this._healOAuthInstallTokenExportCursorOnPartition(gid).catch(() => ({
|
||||||
relisted: 0
|
relisted: 0
|
||||||
}))
|
}))
|
||||||
|
const inviteLinkCsvHeal = await this._healInviteLinkExportCursorOnPartition(gid).catch(() => ({
|
||||||
|
relisted: 0
|
||||||
|
}))
|
||||||
|
const vanityUrlCsvHeal = await this._healVanityUrlExportCursorOnPartition(gid).catch(() => ({
|
||||||
|
relisted: 0
|
||||||
|
}))
|
||||||
return {
|
return {
|
||||||
voiceApplied,
|
voiceApplied,
|
||||||
emojiSlots,
|
emojiSlots,
|
||||||
@@ -1758,7 +1770,9 @@ class PearcordPlatform extends EventEmitter {
|
|||||||
slashRegistryCsvHeal,
|
slashRegistryCsvHeal,
|
||||||
slashInvokeAuditCsvHeal,
|
slashInvokeAuditCsvHeal,
|
||||||
oauthGrantCsvHeal,
|
oauthGrantCsvHeal,
|
||||||
oauthInstallTokenCsvHeal
|
oauthInstallTokenCsvHeal,
|
||||||
|
inviteLinkCsvHeal,
|
||||||
|
vanityUrlCsvHeal
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -8722,6 +8736,8 @@ class PearcordPlatform extends EventEmitter {
|
|||||||
let slashInvokeAuditExportCsvExport = null
|
let slashInvokeAuditExportCsvExport = null
|
||||||
let oauthGrantCsvExport = null
|
let oauthGrantCsvExport = null
|
||||||
let oauthInstallTokenExportCsvExport = null
|
let oauthInstallTokenExportCsvExport = null
|
||||||
|
let inviteLinkCsvExport = null
|
||||||
|
let vanityUrlExportCsvExport = null
|
||||||
let archiveHealthPrefs = null
|
let archiveHealthPrefs = null
|
||||||
await this._initDeliveryReceipts(guild.id)
|
await this._initDeliveryReceipts(guild.id)
|
||||||
if (this.deliveryReceipts) {
|
if (this.deliveryReceipts) {
|
||||||
@@ -8754,6 +8770,8 @@ class PearcordPlatform extends EventEmitter {
|
|||||||
oauthGrantCsvExport = await this.deliveryReceipts.exportOAuthGrantCsvSlice()
|
oauthGrantCsvExport = await this.deliveryReceipts.exportOAuthGrantCsvSlice()
|
||||||
oauthInstallTokenExportCsvExport =
|
oauthInstallTokenExportCsvExport =
|
||||||
await this.deliveryReceipts.exportOAuthInstallTokenExportCsvSlice()
|
await this.deliveryReceipts.exportOAuthInstallTokenExportCsvSlice()
|
||||||
|
inviteLinkCsvExport = await this.deliveryReceipts.exportInviteLinkCsvSlice()
|
||||||
|
vanityUrlExportCsvExport = await this.deliveryReceipts.exportVanityUrlExportCsvSlice()
|
||||||
digestRelayHandoff = await this.deliveryReceipts.exportDigestRelayHandoffSlice()
|
digestRelayHandoff = await this.deliveryReceipts.exportDigestRelayHandoffSlice()
|
||||||
const digestSnap = await this.deliveryReceipts.getHookFailureDigestSnapshot()
|
const digestSnap = await this.deliveryReceipts.getHookFailureDigestSnapshot()
|
||||||
if (digestSnap) {
|
if (digestSnap) {
|
||||||
@@ -8936,6 +8954,8 @@ class PearcordPlatform extends EventEmitter {
|
|||||||
slashInvokeAuditExportCsvExport,
|
slashInvokeAuditExportCsvExport,
|
||||||
oauthGrantCsvExport,
|
oauthGrantCsvExport,
|
||||||
oauthInstallTokenExportCsvExport,
|
oauthInstallTokenExportCsvExport,
|
||||||
|
inviteLinkCsvExport,
|
||||||
|
vanityUrlExportCsvExport,
|
||||||
archiveHealthPrefs,
|
archiveHealthPrefs,
|
||||||
modDigestGroupDmMeta,
|
modDigestGroupDmMeta,
|
||||||
workerReleaseHint,
|
workerReleaseHint,
|
||||||
@@ -10094,6 +10114,27 @@ class PearcordPlatform extends EventEmitter {
|
|||||||
this._oauthInstallTokenExportCsvDuplicateAt = ingested.exportedAt || slice.exportedAt || null
|
this._oauthInstallTokenExportCsvDuplicateAt = ingested.exportedAt || slice.exportedAt || null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (payload.inviteLinkCsvExport) {
|
||||||
|
await this._initDeliveryReceipts(payload.guildId)
|
||||||
|
const slice = payload.inviteLinkCsvExport
|
||||||
|
this._shouldGossipInviteLinkCsv(slice)
|
||||||
|
const ingested = await this.deliveryReceipts.ingestInviteLinkCsvSlice(payload.guildId, slice)
|
||||||
|
if (ingested?.duplicate) {
|
||||||
|
this._inviteLinkCsvDuplicateAt = ingested.exportedAt || slice.exportedAt || null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (payload.vanityUrlExportCsvExport) {
|
||||||
|
await this._initDeliveryReceipts(payload.guildId)
|
||||||
|
const slice = payload.vanityUrlExportCsvExport
|
||||||
|
this._shouldGossipVanityUrlExportCsv(slice)
|
||||||
|
const ingested = await this.deliveryReceipts.ingestVanityUrlExportCsvSlice(
|
||||||
|
payload.guildId,
|
||||||
|
slice
|
||||||
|
)
|
||||||
|
if (ingested?.duplicate) {
|
||||||
|
this._vanityUrlExportCsvDuplicateAt = ingested.exportedAt || slice.exportedAt || null
|
||||||
|
}
|
||||||
|
}
|
||||||
if (payload.auditExportCsvExport) {
|
if (payload.auditExportCsvExport) {
|
||||||
await this._initDeliveryReceipts(payload.guildId)
|
await this._initDeliveryReceipts(payload.guildId)
|
||||||
const slice = payload.auditExportCsvExport
|
const slice = payload.auditExportCsvExport
|
||||||
@@ -10974,6 +11015,12 @@ class PearcordPlatform extends EventEmitter {
|
|||||||
guildInstance.on('oauth-install-token-export-csv-sync', (payload) => {
|
guildInstance.on('oauth-install-token-export-csv-sync', (payload) => {
|
||||||
this._onOAuthInstallTokenExportCsvGossip(payload).catch(() => {})
|
this._onOAuthInstallTokenExportCsvGossip(payload).catch(() => {})
|
||||||
})
|
})
|
||||||
|
guildInstance.on('invite-link-csv-sync', (payload) => {
|
||||||
|
this._onInviteLinkCsvGossip(payload).catch(() => {})
|
||||||
|
})
|
||||||
|
guildInstance.on('vanity-url-export-csv-sync', (payload) => {
|
||||||
|
this._onVanityUrlExportCsvGossip(payload).catch(() => {})
|
||||||
|
})
|
||||||
guildInstance.on('message-search-request', (payload) => {
|
guildInstance.on('message-search-request', (payload) => {
|
||||||
this._onMessageSearchRequestGossip(payload).catch(() => {})
|
this._onMessageSearchRequestGossip(payload).catch(() => {})
|
||||||
})
|
})
|
||||||
@@ -18398,7 +18445,8 @@ class PearcordPlatform extends EventEmitter {
|
|||||||
openWorkerCsv: !!parsed.openWorkerCsv,
|
openWorkerCsv: !!parsed.openWorkerCsv,
|
||||||
openBotCsv: !!parsed.openBotCsv,
|
openBotCsv: !!parsed.openBotCsv,
|
||||||
openSlashCsv: !!parsed.openSlashCsv,
|
openSlashCsv: !!parsed.openSlashCsv,
|
||||||
openOAuthCsv: !!parsed.openOAuthCsv
|
openOAuthCsv: !!parsed.openOAuthCsv,
|
||||||
|
openInviteCsv: !!parsed.openInviteCsv
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -22512,6 +22560,291 @@ class PearcordPlatform extends EventEmitter {
|
|||||||
return row
|
return row
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getInviteLinkCsvExport () {
|
||||||
|
const gid = this.guild?.guild?.id || null
|
||||||
|
const span = this.log.time('invite.csv', {
|
||||||
|
spanKind: 'invite.csv',
|
||||||
|
guildId: gid,
|
||||||
|
context: 'read'
|
||||||
|
})
|
||||||
|
try {
|
||||||
|
if (!this.guild?.guild) {
|
||||||
|
span.end({ guildInviteCsvCount: 0, skipped: true })
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
await this._initDeliveryReceipts(this.guild.guild.id)
|
||||||
|
const row = await this.deliveryReceipts.getInviteLinkCsvExport()
|
||||||
|
if (!row?.signature) {
|
||||||
|
span.end({ guildInviteCsvCount: row ? 1 : 0, hasSignature: false, bridgeKind: 'invite.csv' })
|
||||||
|
return row
|
||||||
|
}
|
||||||
|
const verified = verifyInviteLinkCsv(row.csvBody, row.signature, {
|
||||||
|
guildId: this.guild.guild.id,
|
||||||
|
relaySecret: this.guild.guild.id
|
||||||
|
})
|
||||||
|
const out = { ...row, signatureValid: verified.ok }
|
||||||
|
span.end({
|
||||||
|
guildInviteCsvCount: 1,
|
||||||
|
hasSignature: true,
|
||||||
|
signatureValid: verified.ok,
|
||||||
|
bridgeKind: 'invite.csv'
|
||||||
|
})
|
||||||
|
return out
|
||||||
|
} catch (err) {
|
||||||
|
this.log.error('invite.csv error', {
|
||||||
|
guildId: gid,
|
||||||
|
context: 'read',
|
||||||
|
error: err?.message || String(err)
|
||||||
|
})
|
||||||
|
span.fail(err)
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async exportInviteLinkCsv (opts = {}) {
|
||||||
|
if (!this.guild?.guild) throw new Error('no guild')
|
||||||
|
await this._initDeliveryReceipts(this.guild.guild.id)
|
||||||
|
const invites = await this.listGuildInvites()
|
||||||
|
const entries = (invites || []).map((inv) => ({
|
||||||
|
code: inv.code || inv.shareCode,
|
||||||
|
creatorDisplayName: inv.creatorDisplayName,
|
||||||
|
creatorUsername: inv.creatorUsername,
|
||||||
|
uses: inv.uses || 0,
|
||||||
|
maxUses: inv.maxUses || 0,
|
||||||
|
expiresAt: inv.expiresAt || 0,
|
||||||
|
exportedAt: Date.now()
|
||||||
|
}))
|
||||||
|
const csvBody = buildInviteLinkCsvBody(entries)
|
||||||
|
const signed = signInviteLinkCsv(csvBody, {
|
||||||
|
guildId: this.guild.guild.id,
|
||||||
|
relaySecret: this.guild.guild.id
|
||||||
|
})
|
||||||
|
const exportedAt = Date.now()
|
||||||
|
if (opts.recordMesh !== false) {
|
||||||
|
await this.deliveryReceipts.recordInviteLinkCsvExport({
|
||||||
|
csvBody: signed.csv,
|
||||||
|
signature: signed.signature,
|
||||||
|
signatureAlg: signed.signatureAlg,
|
||||||
|
exportedAt
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
format: 'csv',
|
||||||
|
body: signed.csv,
|
||||||
|
signature: signed.signature,
|
||||||
|
signatureAlg: signed.signatureAlg,
|
||||||
|
exportedAt,
|
||||||
|
inviteCount: entries.length
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async pushInviteLinkCsvToMesh () {
|
||||||
|
const gid = this.guild?.guild?.id || null
|
||||||
|
const span = this.log.time('invite.csv', {
|
||||||
|
spanKind: 'invite.csv',
|
||||||
|
guildId: gid,
|
||||||
|
context: 'mesh.push'
|
||||||
|
})
|
||||||
|
try {
|
||||||
|
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 sync invite link CSV')
|
||||||
|
}
|
||||||
|
const exported = await this.exportInviteLinkCsv({ recordMesh: false })
|
||||||
|
if (!exported.signature) throw new Error('invite link 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
|
||||||
|
}
|
||||||
|
if (this.guild.gossipInviteLinkCsvSync) {
|
||||||
|
this.guild.gossipInviteLinkCsvSync(payload)
|
||||||
|
}
|
||||||
|
this.emit('invite-link-csv-sync', payload)
|
||||||
|
await this.deliveryReceipts.recordInviteLinkCsvExport({
|
||||||
|
csvBody: exported.body,
|
||||||
|
signature: exported.signature,
|
||||||
|
signatureAlg: exported.signatureAlg,
|
||||||
|
exportedAt: exported.exportedAt
|
||||||
|
})
|
||||||
|
span.end({
|
||||||
|
guildInviteCsvCount: exported.inviteCount || 1,
|
||||||
|
meshPushed: true,
|
||||||
|
bridgeKind: 'invite.csv'
|
||||||
|
})
|
||||||
|
return exported
|
||||||
|
} catch (err) {
|
||||||
|
this.log.error('invite.csv error', {
|
||||||
|
guildId: gid,
|
||||||
|
context: 'mesh.push',
|
||||||
|
error: err?.message || String(err)
|
||||||
|
})
|
||||||
|
span.fail(err)
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async clearInviteLinkCsvExports () {
|
||||||
|
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 clear invite link CSV exports')
|
||||||
|
}
|
||||||
|
await this._initDeliveryReceipts(this.guild.guild.id)
|
||||||
|
const removed = await this.deliveryReceipts.clearInviteLinkCsvExports()
|
||||||
|
this.emit('invite-link-csv-cleared', { removed })
|
||||||
|
return { removed }
|
||||||
|
}
|
||||||
|
|
||||||
|
async getLastVanityUrlExport () {
|
||||||
|
if (!this.guild?.guild) return null
|
||||||
|
const snap = this._lastVanityUrlExport
|
||||||
|
if (snap?.guildId === this.guild.guild.id) return snap
|
||||||
|
const vanity = this.userPrefs?.vanityInviteCode || null
|
||||||
|
return {
|
||||||
|
guildId: this.guild.guild.id,
|
||||||
|
exportedAt: Date.now(),
|
||||||
|
vanityCode: vanity,
|
||||||
|
discoveryVersion: this.discoveryListingVersion || 0,
|
||||||
|
signed: false,
|
||||||
|
meshPushed: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async exportVanityUrlMesh (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 vanity URL mesh')
|
||||||
|
}
|
||||||
|
const vanity = this.userPrefs?.vanityInviteCode || ''
|
||||||
|
const entries = [
|
||||||
|
{
|
||||||
|
vanityCode: vanity,
|
||||||
|
discoveryVersion: this.discoveryListingVersion || 0,
|
||||||
|
exportedAt: Date.now(),
|
||||||
|
listed: !!this.guild.guild.publicListing
|
||||||
|
}
|
||||||
|
]
|
||||||
|
const csvBody = buildVanityUrlExportCsvBody(entries)
|
||||||
|
const signed = signVanityUrlExportCsv(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,
|
||||||
|
inviteCount: entries.length,
|
||||||
|
signed: !!signed.signature,
|
||||||
|
meshPushed: false
|
||||||
|
}
|
||||||
|
this._lastVanityUrlExport = meta
|
||||||
|
if (opts.recordMesh !== false) {
|
||||||
|
await this.deliveryReceipts.recordVanityUrlExportCsv({
|
||||||
|
csvBody: signed.csv,
|
||||||
|
signature: signed.signature,
|
||||||
|
signatureAlg: signed.signatureAlg,
|
||||||
|
exportedAt,
|
||||||
|
inviteCount: entries.length
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
format: 'csv',
|
||||||
|
body: signed.csv,
|
||||||
|
signature: signed.signature,
|
||||||
|
signatureAlg: signed.signatureAlg,
|
||||||
|
exportedAt,
|
||||||
|
inviteCount: entries.length,
|
||||||
|
meta
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async pushVanityUrlExportToMesh () {
|
||||||
|
const gid = this.guild?.guild?.id || null
|
||||||
|
const span = this.log.time('invite.csv', {
|
||||||
|
spanKind: 'invite.csv',
|
||||||
|
guildId: gid,
|
||||||
|
context: 'mesh.vanity'
|
||||||
|
})
|
||||||
|
try {
|
||||||
|
const exported = await this.exportVanityUrlMesh({ recordMesh: false })
|
||||||
|
if (!exported.signature) throw new Error('vanity URL 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,
|
||||||
|
inviteCount: exported.inviteCount || 0
|
||||||
|
}
|
||||||
|
if (this.guild.gossipVanityUrlExportCsvSync) {
|
||||||
|
this.guild.gossipVanityUrlExportCsvSync(payload)
|
||||||
|
}
|
||||||
|
this.emit('vanity-url-export-csv-sync', payload)
|
||||||
|
await this.deliveryReceipts.recordVanityUrlExportCsv({
|
||||||
|
csvBody: exported.body,
|
||||||
|
signature: exported.signature,
|
||||||
|
signatureAlg: exported.signatureAlg,
|
||||||
|
exportedAt: exported.exportedAt,
|
||||||
|
inviteCount: exported.inviteCount
|
||||||
|
})
|
||||||
|
this._lastVanityUrlExport = {
|
||||||
|
...exported.meta,
|
||||||
|
meshPushed: true
|
||||||
|
}
|
||||||
|
span.end({
|
||||||
|
guildInviteCsvCount: exported.inviteCount || 0,
|
||||||
|
meshPushed: true,
|
||||||
|
bridgeKind: 'invite.csv'
|
||||||
|
})
|
||||||
|
return exported
|
||||||
|
} catch (err) {
|
||||||
|
this.log.error('invite.csv error', {
|
||||||
|
guildId: gid,
|
||||||
|
context: 'mesh.vanity',
|
||||||
|
error: err?.message || String(err)
|
||||||
|
})
|
||||||
|
span.fail(err)
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async _onInviteLinkCsvGossip (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._shouldGossipInviteLinkCsv(payload)
|
||||||
|
const row = await this.deliveryReceipts.ingestInviteLinkCsvSlice(payload.guildId, payload)
|
||||||
|
if (row?.duplicate) {
|
||||||
|
this._inviteLinkCsvDuplicateAt = row.exportedAt || payload.exportedAt || null
|
||||||
|
}
|
||||||
|
if (row) this.emit('invite-link-csv-sync', payload)
|
||||||
|
return row
|
||||||
|
}
|
||||||
|
|
||||||
|
async _onVanityUrlExportCsvGossip (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._shouldGossipVanityUrlExportCsv(payload)
|
||||||
|
const row = await this.deliveryReceipts.ingestVanityUrlExportCsvSlice(
|
||||||
|
payload.guildId,
|
||||||
|
payload
|
||||||
|
)
|
||||||
|
if (row?.duplicate) {
|
||||||
|
this._vanityUrlExportCsvDuplicateAt = row.exportedAt || payload.exportedAt || null
|
||||||
|
}
|
||||||
|
if (row) this.emit('vanity-url-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 }
|
||||||
@@ -27375,6 +27708,10 @@ class PearcordPlatform extends EventEmitter {
|
|||||||
let oauthGrantCsvRows = []
|
let oauthGrantCsvRows = []
|
||||||
let oauthInstallTokenExportCsvRows = []
|
let oauthInstallTokenExportCsvRows = []
|
||||||
let lastOAuthInstallTokenExport = null
|
let lastOAuthInstallTokenExport = null
|
||||||
|
let inviteLinkCsvMeta = null
|
||||||
|
let inviteLinkCsvRows = []
|
||||||
|
let vanityUrlExportCsvRows = []
|
||||||
|
let lastVanityUrlExport = null
|
||||||
let lastComplianceSnapshot = null
|
let lastComplianceSnapshot = null
|
||||||
let lastArchivePeerExport = null
|
let lastArchivePeerExport = null
|
||||||
let automationScheduleDashboard = null
|
let automationScheduleDashboard = null
|
||||||
@@ -27517,6 +27854,26 @@ class PearcordPlatform extends EventEmitter {
|
|||||||
return { ...row, signatureValid: verified.ok }
|
return { ...row, signatureValid: verified.ok }
|
||||||
})
|
})
|
||||||
lastOAuthInstallTokenExport = await this.getLastOAuthInstallTokenExport()
|
lastOAuthInstallTokenExport = await this.getLastOAuthInstallTokenExport()
|
||||||
|
inviteLinkCsvMeta = await this.getInviteLinkCsvExport().catch(() => null)
|
||||||
|
const inviteCsvHistory = await this.deliveryReceipts.listInviteLinkCsvExports(32)
|
||||||
|
inviteLinkCsvRows = inviteCsvHistory.map((row) => {
|
||||||
|
if (!row?.signature || !guild?.id) return row
|
||||||
|
const verified = verifyInviteLinkCsv(row.csvBody, row.signature, {
|
||||||
|
guildId: guild.id,
|
||||||
|
relaySecret: guild.id
|
||||||
|
})
|
||||||
|
return { ...row, signatureValid: verified.ok }
|
||||||
|
})
|
||||||
|
const vanityCsvHistory = await this.deliveryReceipts.listVanityUrlExportCsvExports(32)
|
||||||
|
vanityUrlExportCsvRows = vanityCsvHistory.map((row) => {
|
||||||
|
if (!row?.signature || !guild?.id) return row
|
||||||
|
const verified = verifyVanityUrlExportCsv(row.csvBody, row.signature, {
|
||||||
|
guildId: guild.id,
|
||||||
|
relaySecret: guild.id
|
||||||
|
})
|
||||||
|
return { ...row, signatureValid: verified.ok }
|
||||||
|
})
|
||||||
|
lastVanityUrlExport = await this.getLastVanityUrlExport()
|
||||||
automationHealthDashboard = await this.getAutomationHealthDashboard()
|
automationHealthDashboard = await this.getAutomationHealthDashboard()
|
||||||
automationScheduleDashboard = automationHealthDashboard
|
automationScheduleDashboard = automationHealthDashboard
|
||||||
automationDigestNotifyPrefs = await this.getAutomationDigestNotifyPrefs()
|
automationDigestNotifyPrefs = await this.getAutomationDigestNotifyPrefs()
|
||||||
@@ -27831,6 +28188,10 @@ class PearcordPlatform extends EventEmitter {
|
|||||||
this._botRegistryCsvDuplicateAt = null
|
this._botRegistryCsvDuplicateAt = null
|
||||||
const webhookExecuteExportCsvDuplicateAt = this._webhookExecuteExportCsvDuplicateAt || null
|
const webhookExecuteExportCsvDuplicateAt = this._webhookExecuteExportCsvDuplicateAt || null
|
||||||
this._webhookExecuteExportCsvDuplicateAt = null
|
this._webhookExecuteExportCsvDuplicateAt = null
|
||||||
|
const inviteLinkCsvDuplicateAt = this._inviteLinkCsvDuplicateAt || null
|
||||||
|
this._inviteLinkCsvDuplicateAt = null
|
||||||
|
const vanityUrlExportCsvDuplicateAt = this._vanityUrlExportCsvDuplicateAt || null
|
||||||
|
this._vanityUrlExportCsvDuplicateAt = null
|
||||||
return {
|
return {
|
||||||
onboarded: this.onboarded,
|
onboarded: this.onboarded,
|
||||||
sessionReady: this._sessionReady,
|
sessionReady: this._sessionReady,
|
||||||
@@ -27982,6 +28343,8 @@ class PearcordPlatform extends EventEmitter {
|
|||||||
integrationManifestExportCsvDuplicateAt,
|
integrationManifestExportCsvDuplicateAt,
|
||||||
botRegistryCsvDuplicateAt,
|
botRegistryCsvDuplicateAt,
|
||||||
webhookExecuteExportCsvDuplicateAt,
|
webhookExecuteExportCsvDuplicateAt,
|
||||||
|
inviteLinkCsvDuplicateAt,
|
||||||
|
vanityUrlExportCsvDuplicateAt,
|
||||||
digestRelayHandoffCsvMeta,
|
digestRelayHandoffCsvMeta,
|
||||||
digestRelayHandoffCsvRows,
|
digestRelayHandoffCsvRows,
|
||||||
archivePeerExportCsvRows,
|
archivePeerExportCsvRows,
|
||||||
@@ -27999,6 +28362,15 @@ class PearcordPlatform extends EventEmitter {
|
|||||||
slashRegistryCsvRows,
|
slashRegistryCsvRows,
|
||||||
slashInvokeAuditExportCsvRows,
|
slashInvokeAuditExportCsvRows,
|
||||||
lastSlashInvokeAuditExport,
|
lastSlashInvokeAuditExport,
|
||||||
|
oauthGrantCsvMeta,
|
||||||
|
oauthGrantCsvRows,
|
||||||
|
oauthInstallTokenExportCsvRows,
|
||||||
|
lastOAuthInstallTokenExport,
|
||||||
|
inviteLinkCsvMeta,
|
||||||
|
inviteLinkCsvRows,
|
||||||
|
vanityUrlExportCsvRows,
|
||||||
|
lastVanityUrlExport,
|
||||||
|
guildInviteCsvCount: (inviteLinkCsvRows || []).length + (guildInvites || []).length,
|
||||||
automationScheduleDashboard,
|
automationScheduleDashboard,
|
||||||
automationHealthDashboard,
|
automationHealthDashboard,
|
||||||
automationDigestNotifyPrefs,
|
automationDigestNotifyPrefs,
|
||||||
@@ -28606,6 +28978,7 @@ const { workerCsvMixin } = require('./worker-csv-mixin')
|
|||||||
const { botCsvMixin } = require('./bot-csv-mixin')
|
const { botCsvMixin } = require('./bot-csv-mixin')
|
||||||
const { slashCsvMixin } = require('./slash-csv-mixin')
|
const { slashCsvMixin } = require('./slash-csv-mixin')
|
||||||
const { oauthCsvMixin } = require('./oauth-csv-mixin')
|
const { oauthCsvMixin } = require('./oauth-csv-mixin')
|
||||||
|
const { inviteCsvMixin } = require('./invite-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)
|
||||||
@@ -28629,3 +29002,4 @@ Object.assign(PearcordPlatform.prototype, workerCsvMixin)
|
|||||||
Object.assign(PearcordPlatform.prototype, botCsvMixin)
|
Object.assign(PearcordPlatform.prototype, botCsvMixin)
|
||||||
Object.assign(PearcordPlatform.prototype, slashCsvMixin)
|
Object.assign(PearcordPlatform.prototype, slashCsvMixin)
|
||||||
Object.assign(PearcordPlatform.prototype, oauthCsvMixin)
|
Object.assign(PearcordPlatform.prototype, oauthCsvMixin)
|
||||||
|
Object.assign(PearcordPlatform.prototype, inviteCsvMixin)
|
||||||
|
|||||||
@@ -0,0 +1,114 @@
|
|||||||
|
'use strict'
|
||||||
|
|
||||||
|
const inviteCsvMixin = {
|
||||||
|
_inviteLinkCsvGossipKeys: null,
|
||||||
|
_inviteCsvHealWatermark: null,
|
||||||
|
_vanityUrlCsvHealWatermark: null,
|
||||||
|
|
||||||
|
_initInviteCsvMixinState () {
|
||||||
|
if (!this._inviteLinkCsvGossipKeys) {
|
||||||
|
this._inviteLinkCsvGossipKeys = new Set()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
_shouldGossipInviteLinkCsv (slice) {
|
||||||
|
this._initInviteCsvMixinState()
|
||||||
|
if (!slice?.guildId || !slice?.signature) return true
|
||||||
|
const hash = `${slice.guildId}:${slice.exportedAt || 0}:${slice.signature}`
|
||||||
|
if (this._inviteLinkCsvGossipKeys.has(hash)) return false
|
||||||
|
this._inviteLinkCsvGossipKeys.add(hash)
|
||||||
|
if (this._inviteLinkCsvGossipKeys.size > 8192) {
|
||||||
|
const first = this._inviteLinkCsvGossipKeys.values().next().value
|
||||||
|
if (first) this._inviteLinkCsvGossipKeys.delete(first)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
|
||||||
|
_shouldGossipVanityUrlExportCsv (slice) {
|
||||||
|
this._initInviteCsvMixinState()
|
||||||
|
if (!slice?.guildId || !slice?.signature) return true
|
||||||
|
const hash = `vanity:${slice.guildId}:${slice.exportedAt || 0}:${slice.signature}`
|
||||||
|
if (this._inviteLinkCsvGossipKeys.has(hash)) return false
|
||||||
|
this._inviteLinkCsvGossipKeys.add(hash)
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
|
||||||
|
async _healInviteLinkExportCursorOnPartition (guildId) {
|
||||||
|
const gid = guildId || this.guild?.guild?.id || null
|
||||||
|
const span = this.log.time('invite.csv', {
|
||||||
|
spanKind: 'invite.csv',
|
||||||
|
guildId: gid,
|
||||||
|
context: 'heal.link'
|
||||||
|
})
|
||||||
|
try {
|
||||||
|
if (!gid || !this.deliveryReceipts) {
|
||||||
|
span.end({ relisted: 0, skipped: true, guildInviteCsvCount: 0 })
|
||||||
|
return { relisted: 0, skipped: true }
|
||||||
|
}
|
||||||
|
await this._initDeliveryReceipts(gid)
|
||||||
|
const rows = await this.deliveryReceipts.listInviteLinkCsvExports(64)
|
||||||
|
let relisted = 0
|
||||||
|
for (const row of rows) {
|
||||||
|
if (this._shouldGossipInviteLinkCsv(row)) relisted++
|
||||||
|
}
|
||||||
|
const watermark = Date.now()
|
||||||
|
this._inviteCsvHealWatermark = watermark
|
||||||
|
span.end({
|
||||||
|
relisted,
|
||||||
|
watermark,
|
||||||
|
guildInviteCsvCount: rows.length,
|
||||||
|
bridgeKind: 'invite.csv'
|
||||||
|
})
|
||||||
|
return { relisted, watermark, guildInviteCsvCount: rows.length }
|
||||||
|
} catch (err) {
|
||||||
|
this.log.error('invite.csv error', {
|
||||||
|
guildId: gid,
|
||||||
|
context: 'heal.link',
|
||||||
|
error: err?.message || String(err)
|
||||||
|
})
|
||||||
|
span.fail(err)
|
||||||
|
return { relisted: 0, error: err?.message || String(err) }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async _healVanityUrlExportCursorOnPartition (guildId) {
|
||||||
|
const gid = guildId || this.guild?.guild?.id || null
|
||||||
|
const span = this.log.time('invite.csv', {
|
||||||
|
spanKind: 'invite.csv',
|
||||||
|
guildId: gid,
|
||||||
|
context: 'heal.vanity'
|
||||||
|
})
|
||||||
|
try {
|
||||||
|
if (!gid || !this.deliveryReceipts) {
|
||||||
|
span.end({ relisted: 0, skipped: true, guildInviteCsvCount: 0 })
|
||||||
|
return { relisted: 0, skipped: true }
|
||||||
|
}
|
||||||
|
await this._initDeliveryReceipts(gid)
|
||||||
|
const rows = await this.deliveryReceipts.listVanityUrlExportCsvExports(64)
|
||||||
|
let relisted = 0
|
||||||
|
for (const row of rows) {
|
||||||
|
if (this._shouldGossipVanityUrlExportCsv(row)) relisted++
|
||||||
|
}
|
||||||
|
const watermark = Date.now()
|
||||||
|
this._vanityUrlCsvHealWatermark = watermark
|
||||||
|
span.end({
|
||||||
|
relisted,
|
||||||
|
watermark,
|
||||||
|
guildInviteCsvCount: rows.length,
|
||||||
|
inviteCount: rows[0]?.inviteCount || 0,
|
||||||
|
bridgeKind: 'invite.csv'
|
||||||
|
})
|
||||||
|
return { relisted, watermark, inviteCount: rows[0]?.inviteCount || 0 }
|
||||||
|
} catch (err) {
|
||||||
|
this.log.error('invite.csv error', {
|
||||||
|
guildId: gid,
|
||||||
|
context: 'heal.vanity',
|
||||||
|
error: err?.message || String(err)
|
||||||
|
})
|
||||||
|
span.fail(err)
|
||||||
|
return { relisted: 0, error: err?.message || String(err) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { inviteCsvMixin }
|
||||||
Reference in New Issue
Block a user