feat(v0.8.645): Phase 670 sticker/GIF mesh hardening and platform spans

Sticker and GIF rate limits, gossip dedupe, partition heal, pack reorder,
announcement sticker fanout, DM sticker notifications, and extended
sticker.send/sticker.create span metadata.

Co-authored-by: Cursor <[email protected]>
This commit is contained in:
Raven Scott
2026-06-02 20:59:53 -04:00
co-authored by Cursor
parent 88c3b4bbdd
commit 2a6c782693
2 changed files with 180 additions and 7 deletions
+2
View File
@@ -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 670 (v0.8.645):** Sticker/GIF depth — rate limits, gossip dedupe, partition heal, `reorderStickerPacks`, `packName`/`slotCount` spans, announcement sticker fanout, `_maybeNotifyDmSticker`. Bundle: `npm run test:ci-phase670`.
**Phase 669 (v0.8.644):** Reaction depth — `_checkReactionToggleRateLimit`, `_shouldGossipReaction` dedupe, `_healReactionListOnPartition`, `removeMemberReaction` + audit, `reaction.toggle` span `emojiName`, `notifyOnReaction` DM path, `reactionTotalsByMessage` for thread badges, frequent-reaction pref recording. Bundle: `npm run test:ci-phase669`.
**Phase 668 (v0.8.643):** Composer embed preview — `prefetchComposerEmbed`, `clearComposerEmbedPreview`, `composerEmbedPreview` view slice; guild embed resolve rate limit; DM gossip bandwidth cap; URL-hash dedupe; domain block/allowlist; automation embed opt-out; scheduled/webhook embed queue guards. Bundle: `npm run test:ci-phase668`.
+178 -7
View File
@@ -592,6 +592,12 @@ class PearcordPlatform extends EventEmitter {
this._reactionGossipKeys = new Set()
/** Per-channel reaction toggle rate limit (Phase 669). */
this._reactionToggleRateByChannel = new Map()
/** Dedupe sticker gossip fanout (Phase 670). */
this._stickerGossipKeys = new Set()
/** Per-channel sticker send rate limit (Phase 670). */
this._stickerSendRateByChannel = new Map()
/** Per-channel GIF attachment send rate limit (Phase 670). */
this._gifSendRateByChannel = new Map()
}
_getGuildOpenSlowThresholdMs () {
@@ -1528,7 +1534,10 @@ class PearcordPlatform extends EventEmitter {
const reactionHeal = await this._healReactionListOnPartition(gid).catch(() => ({
relisted: 0
}))
return { voiceApplied, emojiSlots, automodReconciled, voiceNoteHeal, attachmentHeal, embedHeal, reactionHeal }
const stickerHeal = await this._healStickerRegistryOnPartition(gid).catch(() => ({
relisted: 0
}))
return { voiceApplied, emojiSlots, automodReconciled, voiceNoteHeal, attachmentHeal, embedHeal, reactionHeal, stickerHeal }
}
async _healReactionListOnPartition (guildId) {
@@ -1549,6 +1558,31 @@ class PearcordPlatform extends EventEmitter {
}
}
async _healStickerRegistryOnPartition (guildId) {
const gid = guildId || this.guild?.guild?.id
if (!gid || !this.stickerRegistry) return { relisted: 0, skipped: true }
const span = this.log.time('sticker.heal', {
spanKind: 'sticker.heal',
guildId: gid
})
try {
const rows = (await this.stickerRegistry.list().catch(() => [])).filter(
(r) => !r.guildId || r.guildId === gid
)
if (this.guild?.gossipStickerUpsert) {
for (const row of rows.slice(0, 64)) {
if (this._shouldGossipSticker(row)) this.guild.gossipStickerUpsert(row)
}
}
span.end({ relisted: rows.length, rowCount: rows.length })
return { relisted: rows.length, rowCount: rows.length }
} catch (err) {
this.log.error('sticker.heal error', { error: err?.message || String(err) })
span.fail(err)
return { relisted: 0, error: err?.message || String(err) }
}
}
async _healStaleEmbedCache (_guildId) {
if (!this.embeds?.healStaleEntries) return { healed: 0, skipped: true }
const span = this.log.time('embed.heal', {
@@ -8896,6 +8930,9 @@ class PearcordPlatform extends EventEmitter {
const reactionHeal = await this._healReactionListOnPartition(guildId).catch(() => ({
relisted: 0
}))
const stickerHeal = await this._healStickerRegistryOnPartition(guildId).catch(() => ({
relisted: 0
}))
const settingsDeviceSync = await this._healSettingsMeshDeviceSync(guildId).catch(() => null)
const req = this.requestGuildSyncFromHost()
const burst = this.requestGuildSyncBurst({ clearPushHalt: false, forcePush: true })
@@ -8920,6 +8957,7 @@ class PearcordPlatform extends EventEmitter {
contactsReconciled,
extendedHeal,
reactionHeal,
stickerHeal,
settingsDeviceSync,
syncRequested: !!req?.requested,
syncFanoutAckCount: req?.ackCount ?? 0,
@@ -13205,6 +13243,78 @@ class PearcordPlatform extends EventEmitter {
.catch(() => null)
}
_checkStickerSendRateLimit (channelId) {
const key = channelId || this.activeChannelId || 'unknown'
const windowMs = 60_000
const cap = Math.max(4, Number(process.env.PEARCORD_STICKER_SEND_RATE_PER_MIN) || 30)
const now = Date.now()
let bucket = this._stickerSendRateByChannel.get(key)
if (!bucket || now - bucket.resetAt > windowMs) {
bucket = { count: 0, resetAt: now }
}
if (bucket.count >= cap) return false
bucket.count++
this._stickerSendRateByChannel.set(key, bucket)
return true
}
_checkGifSendRateLimit (channelId) {
const key = channelId || this.activeChannelId || 'unknown'
const windowMs = 60_000
const cap = Math.max(4, Number(process.env.PEARCORD_GIF_SEND_RATE_PER_MIN) || 20)
const now = Date.now()
let bucket = this._gifSendRateByChannel.get(key)
if (!bucket || now - bucket.resetAt > windowMs) {
bucket = { count: 0, resetAt: now }
}
if (bucket.count >= cap) return false
bucket.count++
this._gifSendRateByChannel.set(key, bucket)
return true
}
_shouldGossipSticker (row) {
if (!row?.guildId || !row?.name) return true
const hash = `${row.guildId}:${row.name}:${row.updatedAt || 0}`
if (this._stickerGossipKeys.has(hash)) return false
this._stickerGossipKeys.add(hash)
if (this._stickerGossipKeys.size > 4096) {
const first = this._stickerGossipKeys.values().next().value
if (first) this._stickerGossipKeys.delete(first)
}
return true
}
async _recordFrequentStickerPref (name) {
if (!this.userSettings || !name) return
const prefs = await this.userSettings.getPrefs().catch(() => null)
if (!prefs) return
const token = String(name).trim().toLowerCase()
const prev = Array.isArray(prefs.frequentStickers) ? prefs.frequentStickers : []
const next = [token, ...prev.filter((e) => e !== token)].slice(0, 8)
if (next.join('|') === prev.join('|')) return
await this.userSettings.setPrefs({ frequentStickers: next }, { gossip: false })
}
async _maybeNotifyDmSticker ({ message, stickerNames }) {
if (!this.notifications || !message || !(stickerNames || []).length) return
const prefs = await this.notifications.getPrefs().catch(() => null)
if (prefs?.notifyOnSticker === false) return
const userId = this.identity.user?.id
if (!userId || message.authorId === userId) return
if (this.mode !== 'dm') return
const names = (stickerNames || []).slice(0, 3).map((n) => `:${n}:`).join(' ')
await this.notifications
.ingest({
kind: 'message',
channelId: message.channelId,
messageId: message.id,
authorId: message.authorId,
title: `Sticker message ${names}`
})
.catch(() => null)
}
/** Re-resolve embed for an existing message (read-only fetch; ignores slowmode send gates). */
async retryLinkEmbedForMessage (messageId) {
if (!this.embeds || !messageId) throw new Error('embed retry requires messageId')
@@ -17599,25 +17709,39 @@ class PearcordPlatform extends EventEmitter {
if (this.mode !== 'guild' || !this.guild?.guild) {
throw new Error('stickers are only supported in guild channels')
}
if (!this._checkStickerSendRateLimit(this.activeChannelId)) {
throw new Error('sticker send rate limited')
}
const perms = await this._myPermissions(this.activeChannelId).catch(() => null)
if (perms && perms.sendMessages === false) {
throw new Error('missing permission: send stickers')
}
await this._ensureGuildModules()
if (!this.stickerRegistry) await this._initStickerRegistry(this.guild.guild.id)
const out = []
const packNames = []
for (const name of names) {
const row = await this.stickerRegistry.get(name)
if (!row) throw new Error(`unknown sticker: ${name}`)
out.push(row.name)
if (row.packName) packNames.push(row.packName)
await this._recordFrequentStickerPref(row.name)
}
span.end({
resolved: out.length,
names: out,
packName: packNames[0] || null,
packNames,
spanKind: 'sticker.send',
activeChannelId: this.activeChannelId,
guildId: this.guild.guild.id,
guildCount: (this.guilds || []).length
})
return out
} catch (err) {
this.log.error('sticker.send error', {
channelId: this.activeChannelId || null,
guildId: this.guild?.guild?.id || null,
error: err?.message || String(err)
})
span.fail(err)
@@ -18410,16 +18534,19 @@ class PearcordPlatform extends EventEmitter {
data: image?.data || null
})
if (att) this.guild.gossipAttachmentMeta(att)
this.guild.gossipStickerUpsert(row)
if (this._shouldGossipSticker(row)) this.guild.gossipStickerUpsert(row)
await this._audit('sticker.create', {
guildId: this.guild.guild.id,
targetId: row.name,
meta: { name: row.name }
meta: { name: row.name, packName: row.packName || null }
})
this.emit('guild-sticker', row)
const slotCount = (await this.stickerRegistry.list().catch(() => [])).length
span.end({
name: row.name,
guildId,
packName: row.packName || null,
slotCount,
spanKind: 'sticker.create',
activeChannelId: this.activeChannelId,
guildCount: (this.guilds || []).length
@@ -18436,6 +18563,29 @@ class PearcordPlatform extends EventEmitter {
}
}
async reorderStickerPacks (packNames = []) {
if (!this.guild?.guild) throw new Error('no guild')
const user = this.identity.user
if (!user) throw new Error('register first')
const roles = await this._memberRoles()
if (!roleHasPermission(roles, PERMISSION.MANAGE_GUILD)) {
throw new Error('no permission to manage sticker packs')
}
if (!this.stickerRegistry) await this._initStickerRegistry(this.guild.guild.id)
const names = (packNames || []).map((n) => String(n || '').trim().toLowerCase()).filter(Boolean)
const packs = await this.stickerRegistry.listPacks()
for (let i = 0; i < names.length; i++) {
const pack = packs.find((p) => p.name === names[i])
if (!pack) continue
const row = await this.stickerRegistry.upsertPack({
...pack,
sortOrder: i
})
this.guild.gossipStickerPackUpsert(row)
}
return { reordered: names.length }
}
async removeGuildSticker (name) {
const guildId = this.guild?.guild?.id || null
const span = this.log.time('sticker.delete', { name: name || null, guildId })
@@ -20623,8 +20773,17 @@ class PearcordPlatform extends EventEmitter {
await this._assertNotTimedOut()
await this._assertSlowmode()
const stickerNames = await this._normalizeStickerNames(opts.stickerNames)
if (String(content || '').trim()) await this._assertAutomod(content)
const attachmentIds = opts.attachmentIds || []
if (attachmentIds.length && this.attachments) {
for (const aid of attachmentIds) {
const att = await this.attachments.get(aid).catch(() => null)
const mt = String(att?.mimeType || '').toLowerCase()
if (mt.includes('gif') && !this._checkGifSendRateLimit(this.activeChannelId)) {
throw new Error('gif send rate limited')
}
}
}
if (String(content || '').trim()) await this._assertAutomod(content)
if (this.attachments && opts.attachmentUiById) {
for (const [aid, meta] of Object.entries(opts.attachmentUiById)) {
if (!attachmentIds.includes(aid)) continue
@@ -20706,10 +20865,13 @@ class PearcordPlatform extends EventEmitter {
}
}
this.emit('message', msg)
if (stickerNames.length) {
await this._maybeNotifyDmSticker({ message: msg, stickerNames }).catch(() => null)
}
return msg
}
async _insertGuildMessage ({ guildId, channelId, content, authorId, hubMirror }) {
async _insertGuildMessage ({ guildId, channelId, content, authorId, hubMirror, stickerNames }) {
if (!this.messages || !guildId || !channelId) throw new Error('invalid message insert')
const card = hubMirror ? buildHubMirrorCard(hubMirror) : null
const body = card ? formatHubMirrorFallback(card) : content
@@ -20719,7 +20881,8 @@ class PearcordPlatform extends EventEmitter {
this.messages.setChannel({ channelId, guildId })
const row = await this.messages.send(body || '', {
authorId: authorId || this.identity.user?.id,
hubMirrorJson
hubMirrorJson,
stickerNames: (stickerNames || []).length ? stickerNames : undefined
})
this.messages.setChannel({ channelId: prevChannel, guildId: prevGuild })
const enriched = enrichMessageRow(row)
@@ -20862,6 +21025,7 @@ class PearcordPlatform extends EventEmitter {
if (!targets.length) return []
const channels = await this.guild.listChannels()
const body = formatCrosspostBody(sourceCh.name, sourceMsg.content)
const stickerNames = (sourceMsg.stickerNames || []).slice(0, 3)
const out = []
for (const targetId of targets) {
if (targetId === sourceMsg.channelId) continue
@@ -20872,7 +21036,8 @@ class PearcordPlatform extends EventEmitter {
guildId: sourceMsg.guildId,
channelId: targetId,
content: body,
authorId: sourceMsg.authorId
authorId: sourceMsg.authorId,
stickerNames: stickerNames.length ? stickerNames : undefined
})
out.push(row)
} catch {
@@ -24058,6 +24223,10 @@ class PearcordPlatform extends EventEmitter {
0
)
}
const rootStickerNames = {}
for (const [msgId, root] of Object.entries(threadRootMessages || {})) {
if ((root?.stickerNames || []).length) rootStickerNames[msgId] = root.stickerNames
}
const threadsPanel =
this.mode === 'guild' && guild
? buildThreadsPanel({
@@ -24068,6 +24237,7 @@ class PearcordPlatform extends EventEmitter {
threadLastActivity,
rootMessages: threadRootMessages,
reactionTotalsByMessage,
rootStickerNames,
activeChannelId: this.activeChannelId,
includeArchived: this._threadsPanelIncludeArchived,
threadFilter: this._threadsPanelFilter,
@@ -24086,6 +24256,7 @@ class PearcordPlatform extends EventEmitter {
threadLastActivity,
rootMessages: threadRootMessages,
reactionTotalsByMessage,
rootStickerNames,
activeChannelId: this.activeChannelId,
includeArchived: false,
threadFilter: 'active',