'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 }