Move 136 prototype mixins under mixins/domain, json-export, and runtime/* so the package root stays navigable. Manifest assign order is unchanged; apply-platform-mixins and runtime registry paths updated. Co-authored-by: Cursor <[email protected]>
155 lines
5.3 KiB
JavaScript
155 lines
5.3 KiB
JavaScript
'use strict'
|
|
|
|
const { isAnnouncementChannel } = require('pearcord-threads')
|
|
|
|
const announcementsMixin = {
|
|
_announcementFollowHealWatermark: null,
|
|
_announcementCrosspostHealWatermark: null,
|
|
_announcementCrosspostGossipKeys: null,
|
|
_announcementFollowGossipKeys: null,
|
|
_crosspostRateByGuild: null,
|
|
_lastCrosspostPublish: null,
|
|
|
|
_initAnnouncementMixinState () {
|
|
if (!this._announcementCrosspostGossipKeys) {
|
|
this._announcementCrosspostGossipKeys = new Set()
|
|
}
|
|
if (!this._announcementFollowGossipKeys) {
|
|
this._announcementFollowGossipKeys = new Set()
|
|
}
|
|
if (!this._crosspostRateByGuild) {
|
|
this._crosspostRateByGuild = new Map()
|
|
}
|
|
},
|
|
|
|
_shouldGossipAnnouncementCrosspost (row) {
|
|
this._initAnnouncementMixinState()
|
|
if (!row?.guildId || !row?.sourceChannelId) return true
|
|
const ids = (row.targetChannelIds || []).join(',')
|
|
const hash = `${row.guildId}:${row.sourceChannelId}:${ids}:${row.updatedAt || 0}`
|
|
if (this._announcementCrosspostGossipKeys.has(hash)) return false
|
|
this._announcementCrosspostGossipKeys.add(hash)
|
|
if (this._announcementCrosspostGossipKeys.size > 4096) {
|
|
const first = this._announcementCrosspostGossipKeys.values().next().value
|
|
if (first) this._announcementCrosspostGossipKeys.delete(first)
|
|
}
|
|
return true
|
|
},
|
|
|
|
_shouldGossipAnnouncementFollow (row) {
|
|
this._initAnnouncementMixinState()
|
|
if (!row?.guildId || !row?.channelId || !row?.userId) return true
|
|
const hash = `${row.guildId}:${row.channelId}:${row.userId}:${row.active ? '1' : '0'}:${row.updatedAt || 0}`
|
|
if (this._announcementFollowGossipKeys.has(hash)) return false
|
|
this._announcementFollowGossipKeys.add(hash)
|
|
if (this._announcementFollowGossipKeys.size > 8192) {
|
|
const first = this._announcementFollowGossipKeys.values().next().value
|
|
if (first) this._announcementFollowGossipKeys.delete(first)
|
|
}
|
|
return true
|
|
},
|
|
|
|
_checkCrosspostPublishRateLimit (guildId) {
|
|
this._initAnnouncementMixinState()
|
|
const gid = guildId || this.guild?.guild?.id
|
|
if (!gid) return true
|
|
const cap = 12
|
|
const windowMs = 60_000
|
|
const now = Date.now()
|
|
let bucket = this._crosspostRateByGuild.get(gid)
|
|
if (!bucket || now >= bucket.resetAt) {
|
|
bucket = { count: 0, resetAt: now + windowMs }
|
|
}
|
|
if (bucket.count >= cap) return false
|
|
bucket.count++
|
|
this._crosspostRateByGuild.set(gid, bucket)
|
|
return true
|
|
},
|
|
|
|
async _healAnnouncementCrosspostOnPartition (guildId) {
|
|
const gid = guildId || this.guild?.guild?.id || null
|
|
const span = this.log.time('announcement.heal', {
|
|
spanKind: 'announcement.heal',
|
|
guildId: gid,
|
|
slice: 'crosspost'
|
|
})
|
|
try {
|
|
if (!gid || !this.guild?.listChannels || !this.announcements) {
|
|
span.end({ relisted: 0, skipped: true })
|
|
return { relisted: 0, skipped: true }
|
|
}
|
|
const channels = await this.guild.listChannels().catch(() => [])
|
|
let relisted = 0
|
|
for (const ch of channels) {
|
|
if (!isAnnouncementChannel(ch) || ch.guildId && ch.guildId !== gid) continue
|
|
const targets = await this.announcements.getCrosspostTargets(gid, ch.id)
|
|
const row = {
|
|
guildId: gid,
|
|
sourceChannelId: ch.id,
|
|
targetChannelIds: targets,
|
|
updatedAt: Date.now()
|
|
}
|
|
if (this._shouldGossipAnnouncementCrosspost(row) && this.guild?.gossipAnnouncementCrosspost) {
|
|
this.guild.gossipAnnouncementCrosspost(row)
|
|
relisted++
|
|
}
|
|
}
|
|
const watermark = Date.now()
|
|
this._announcementCrosspostHealWatermark = watermark
|
|
span.end({ relisted, watermark })
|
|
return { relisted, watermark }
|
|
} catch (err) {
|
|
this.log.error('announcement.heal error', {
|
|
guildId: gid,
|
|
slice: 'crosspost',
|
|
error: err?.message || String(err)
|
|
})
|
|
span.fail(err)
|
|
return { relisted: 0, error: err?.message || String(err) }
|
|
}
|
|
},
|
|
|
|
async _healAnnouncementFollowsOnPartition (guildId) {
|
|
const gid = guildId || this.guild?.guild?.id || null
|
|
const span = this.log.time('announcement.heal', {
|
|
spanKind: 'announcement.heal',
|
|
guildId: gid,
|
|
slice: 'follow'
|
|
})
|
|
try {
|
|
if (!gid || !this.announcements?.store) {
|
|
span.end({ relisted: 0, skipped: true })
|
|
return { relisted: 0, skipped: true }
|
|
}
|
|
const userId = this.identity?.user?.id
|
|
if (!userId) {
|
|
span.end({ relisted: 0, skipped: true })
|
|
return { relisted: 0, skipped: true }
|
|
}
|
|
const follows = await this.announcements.listMyFollows(userId, gid)
|
|
let relisted = 0
|
|
for (const row of follows) {
|
|
if (row.guildId !== gid) continue
|
|
if (this._shouldGossipAnnouncementFollow(row) && this.guild?.gossipAnnouncementFollow) {
|
|
this.guild.gossipAnnouncementFollow(row)
|
|
relisted++
|
|
}
|
|
}
|
|
const watermark = Date.now()
|
|
this._announcementFollowHealWatermark = watermark
|
|
span.end({ relisted, watermark, followCount: follows.length })
|
|
return { relisted, watermark, followCount: follows.length }
|
|
} catch (err) {
|
|
this.log.error('announcement.heal error', {
|
|
guildId: gid,
|
|
slice: 'follow',
|
|
error: err?.message || String(err)
|
|
})
|
|
span.fail(err)
|
|
return { relisted: 0, error: err?.message || String(err) }
|
|
}
|
|
}
|
|
}
|
|
|
|
module.exports = { announcementsMixin }
|