'use strict' const webhooksMixin = { _webhookGossipKeys: null, _webhookRegistryHealWatermark: null, _webhookExecuteHealWatermark: null, _initWebhooksMixinState () { if (!this._webhookGossipKeys) { this._webhookGossipKeys = new Set() } }, _shouldGossipWebhookUpsert (hook) { this._initWebhooksMixinState() if (!hook?.guildId || !hook?.id) return true const hash = `${hook.guildId}:${hook.id}:${hook.name || ''}:${hook.channelId || ''}:${hook.avatarUrl || ''}:${hook.managed ? 1 : 0}:${hook.createdAt || 0}` if (this._webhookGossipKeys.has(hash)) return false this._webhookGossipKeys.add(hash) if (this._webhookGossipKeys.size > 8192) { const first = this._webhookGossipKeys.values().next().value if (first) this._webhookGossipKeys.delete(first) } return true }, async _healWebhookRegistryOnPartition (guildId) { const gid = guildId || this.guild?.guild?.id || null const span = this.log.time('webhook.heal', { spanKind: 'webhook.heal', guildId: gid, slice: 'registry' }) try { if (!gid || !this.listChannelWebhooks) { span.end({ relisted: 0, skipped: true }) return { relisted: 0, skipped: true } } const rows = await this.listChannelWebhooks().catch(() => []) let relisted = 0 for (const row of rows) { if (row.guildId && row.guildId !== gid) continue if (this._shouldGossipWebhookUpsert(row) && this.guild?.gossipWebhookUpsert) { this.guild.gossipWebhookUpsert(row) relisted++ } } const watermark = Date.now() this._webhookRegistryHealWatermark = watermark span.end({ relisted, watermark, guildWebhookCount: rows.length }) return { relisted, watermark, guildWebhookCount: rows.length } } catch (err) { this.log.error('webhook.heal error', { guildId: gid, slice: 'registry', error: err?.message || String(err) }) span.fail(err) return { relisted: 0, error: err?.message || String(err) } } }, async _healWebhookExecuteCursorOnPartition (guildId) { const gid = guildId || this.guild?.guild?.id || null const span = this.log.time('webhook.execute', { spanKind: 'webhook.execute', guildId: gid, context: 'heal.cursor' }) try { if (!gid) { span.end({ relisted: 0, skipped: true, guildWebhookCount: 0 }) return { relisted: 0, skipped: true } } const list = await this.listChannelWebhooks().catch(() => []) const watermark = Date.now() this._webhookExecuteHealWatermark = watermark span.end({ relisted: 0, watermark, guildWebhookCount: list.length, channelWebhookCount: new Set(list.map((h) => h.channelId).filter(Boolean)).size, guildCount: (this.guilds || []).length, activeChannelId: this.activeChannelId || null, bridgeKind: 'webhook.execute' }) return { relisted: 0, watermark, guildWebhookCount: list.length } } catch (err) { this.log.error('webhook.execute error', { guildId: gid, context: 'heal.cursor', error: err?.message || String(err) }) span.fail(err) return { relisted: 0, error: err?.message || String(err) } } } } module.exports = { webhooksMixin }