feat(bots): Phase 684 bots-mixin gossip dedupe and partition heal (v0.8.659)
Add bots-mixin.js with install gossip dedupe, registry/token cursor partition heal, uninstallBot API, sendBotMessage permission fallback, guildBotCount span metadata, and openBots deep-link field. Update README for Phase 684. Co-authored-by: Cursor <[email protected]>
This commit is contained in:
@@ -2,6 +2,10 @@
|
||||
|
||||
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 684 (v0.8.659):** Bot integrations & install flow — `bots-mixin.js`, bot install gossip dedupe, `_healBotRegistryOnPartition` + `_healBotTokenCursorOnPartition`, `uninstallBot`, `guildBotCount`/`installKind` span metadata, deep link `openBots`. Bundle: `npm run test:ci-phase684`.
|
||||
|
||||
**Phase 683 (v0.8.658):** Slash commands registry — `slash-mixin.js`, command gossip dedupe, partition heal, deep link `openSlash`. Bundle: `npm run test:ci-phase683`.
|
||||
|
||||
**Phase 682 (v0.8.657):** Effective permissions & member roles — `member-roles-mixin.js`, member role gossip dedupe, `_healMemberRoleLinksOnPartition`, view `effectivePermissionCount`, deep link `?roles=1`. Bundle: `npm run test:ci-phase682`.
|
||||
|
||||
**Phase 681 (v0.8.656):** Channel permissions editor — `permissions-mixin.js`, overwrite gossip dedupe, `_healChannelOverwritesOnPartition` + `_healGuildRolesOnPartition`, `permissionOverwriteCount` span metadata, deep link `?permissions=1`. Bundle: `npm run test:ci-phase681`.
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
'use strict'
|
||||
|
||||
const botsMixin = {
|
||||
_botInstallGossipKeys: null,
|
||||
_botRegistryHealWatermark: null,
|
||||
_botTokenHealWatermark: null,
|
||||
|
||||
_initBotsMixinState () {
|
||||
if (!this._botInstallGossipKeys) {
|
||||
this._botInstallGossipKeys = new Set()
|
||||
}
|
||||
},
|
||||
|
||||
_shouldGossipBotInstall (bot) {
|
||||
this._initBotsMixinState()
|
||||
if (!bot?.guildId || !bot?.id) return true
|
||||
const hash = `${bot.guildId}:${bot.id}:${bot.name || ''}:${bot.permissions ?? ''}:${bot.intents ?? ''}:${bot.publicKey || ''}:${bot.createdAt || 0}`
|
||||
if (this._botInstallGossipKeys.has(hash)) return false
|
||||
this._botInstallGossipKeys.add(hash)
|
||||
if (this._botInstallGossipKeys.size > 8192) {
|
||||
const first = this._botInstallGossipKeys.values().next().value
|
||||
if (first) this._botInstallGossipKeys.delete(first)
|
||||
}
|
||||
return true
|
||||
},
|
||||
|
||||
async _healBotRegistryOnPartition (guildId) {
|
||||
const gid = guildId || this.guild?.guild?.id || null
|
||||
const span = this.log.time('bot.heal', {
|
||||
spanKind: 'bot.heal',
|
||||
guildId: gid,
|
||||
slice: 'registry'
|
||||
})
|
||||
try {
|
||||
if (!gid || !this.listBots) {
|
||||
span.end({ relisted: 0, skipped: true })
|
||||
return { relisted: 0, skipped: true }
|
||||
}
|
||||
const rows = await this.listBots().catch(() => [])
|
||||
let relisted = 0
|
||||
for (const row of rows) {
|
||||
if (row.guildId && row.guildId !== gid) continue
|
||||
if (this._shouldGossipBotInstall(row) && this.guild?.gossipBotInstall) {
|
||||
this.guild.gossipBotInstall(row)
|
||||
relisted++
|
||||
}
|
||||
}
|
||||
const watermark = Date.now()
|
||||
this._botRegistryHealWatermark = watermark
|
||||
span.end({ relisted, watermark, botCount: rows.length })
|
||||
return { relisted, watermark, botCount: rows.length }
|
||||
} catch (err) {
|
||||
this.log.error('bot.heal error', {
|
||||
guildId: gid,
|
||||
slice: 'registry',
|
||||
error: err?.message || String(err)
|
||||
})
|
||||
span.fail(err)
|
||||
return { relisted: 0, error: err?.message || String(err) }
|
||||
}
|
||||
},
|
||||
|
||||
async _healBotTokenCursorOnPartition (guildId) {
|
||||
const gid = guildId || this.guild?.guild?.id || null
|
||||
const span = this.log.time('bot.install', {
|
||||
spanKind: 'bot.install',
|
||||
guildId: gid,
|
||||
context: 'heal.token'
|
||||
})
|
||||
try {
|
||||
if (!gid) {
|
||||
span.end({ relisted: 0, skipped: true, guildBotCount: 0 })
|
||||
return { relisted: 0, skipped: true }
|
||||
}
|
||||
const list = await this.listBots().catch(() => [])
|
||||
const watermark = Date.now()
|
||||
this._botTokenHealWatermark = watermark
|
||||
span.end({
|
||||
relisted: 0,
|
||||
watermark,
|
||||
guildBotCount: list.length,
|
||||
activeCount: list.filter((b) => b.active !== false).length,
|
||||
guildCount: (this.guilds || []).length,
|
||||
activeChannelId: this.activeChannelId || null
|
||||
})
|
||||
return { relisted: 0, watermark, guildBotCount: list.length }
|
||||
} catch (err) {
|
||||
this.log.error('bot.install error', {
|
||||
guildId: gid,
|
||||
context: 'heal.token',
|
||||
error: err?.message || String(err)
|
||||
})
|
||||
span.fail(err)
|
||||
return { relisted: 0, error: err?.message || String(err) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { botsMixin }
|
||||
@@ -1589,6 +1589,12 @@ class PearcordPlatform extends EventEmitter {
|
||||
const slashUsageHeal = await this._healSlashUsageCursorOnPartition(gid).catch(() => ({
|
||||
relisted: 0
|
||||
}))
|
||||
const botRegistryHeal = await this._healBotRegistryOnPartition(gid).catch(() => ({
|
||||
relisted: 0
|
||||
}))
|
||||
const botTokenHeal = await this._healBotTokenCursorOnPartition(gid).catch(() => ({
|
||||
relisted: 0
|
||||
}))
|
||||
return {
|
||||
voiceApplied,
|
||||
emojiSlots,
|
||||
@@ -1621,7 +1627,9 @@ class PearcordPlatform extends EventEmitter {
|
||||
memberRoleLinksHeal,
|
||||
effectivePermCacheHeal,
|
||||
slashRegistryHeal,
|
||||
slashUsageHeal
|
||||
slashUsageHeal,
|
||||
botRegistryHeal,
|
||||
botTokenHeal
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17047,7 +17055,8 @@ class PearcordPlatform extends EventEmitter {
|
||||
openFollowAnnouncement: !!parsed.followAnnouncement,
|
||||
openChannelPermissions: !!parsed.editChannelPermissions,
|
||||
openAssignMemberRoles: !!parsed.assignMemberRoles,
|
||||
openSlashCommands: !!parsed.openSlashCommands
|
||||
openSlashCommands: !!parsed.openSlashCommands,
|
||||
openBots: !!parsed.openBots
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20889,7 +20898,9 @@ class PearcordPlatform extends EventEmitter {
|
||||
})
|
||||
const bot = payloadToBotRow(payload, this.identity.snapshot().publicKey)
|
||||
await this._ingestGuildBot(bot)
|
||||
this.guild.gossipBotInstall(bot)
|
||||
if (this._shouldGossipBotInstall(bot)) {
|
||||
this.guild.gossipBotInstall(bot)
|
||||
}
|
||||
this.emit('bot', bot)
|
||||
span.end({
|
||||
guildId,
|
||||
@@ -20897,7 +20908,9 @@ class PearcordPlatform extends EventEmitter {
|
||||
botName: String(bot.name || '').slice(0, 32),
|
||||
permissionMask: Number(bot.permissions) || 0,
|
||||
tokenIssued: !!token,
|
||||
guildBotCount: (await this.listBots().catch(() => [])).length,
|
||||
spanKind: 'bot.token',
|
||||
installKind: 'bot.install',
|
||||
activeChannelId: this.activeChannelId
|
||||
})
|
||||
return { token, bot, expiresAt: payload.expiresAt }
|
||||
@@ -20924,7 +20937,9 @@ class PearcordPlatform extends EventEmitter {
|
||||
throw new Error('load the target server before installing a bot')
|
||||
}
|
||||
await this._ingestGuildBot(bot)
|
||||
if (this.guild) this.guild.gossipBotInstall(bot)
|
||||
if (this.guild && this._shouldGossipBotInstall(bot)) {
|
||||
this.guild.gossipBotInstall(bot)
|
||||
}
|
||||
this.emit('bot', bot)
|
||||
span.end({
|
||||
botId: bot.id,
|
||||
@@ -20932,7 +20947,9 @@ class PearcordPlatform extends EventEmitter {
|
||||
botName: String(bot.name || '').slice(0, 32),
|
||||
permissionMask: Number(bot.permissions) || 0,
|
||||
installed: true,
|
||||
guildBotCount: (await this.listBots().catch(() => [])).length,
|
||||
spanKind: 'bot.install',
|
||||
installKind: 'bot.install',
|
||||
activeChannelId: this.activeChannelId,
|
||||
guildCount: (this.guilds || []).length
|
||||
})
|
||||
@@ -20981,7 +20998,8 @@ class PearcordPlatform extends EventEmitter {
|
||||
if (!this.guild?.guild) throw new Error('no guild')
|
||||
const bot = await this.db.get(COLLECTIONS.GUILD_BOTS, { id: botId })
|
||||
if (!bot) throw new Error('bot not found')
|
||||
if (!botHasPermission(bot.permissions, BOT_PERMISSION.SEND_MESSAGES)) {
|
||||
const perms = normalizePermissions(bot.permissions)
|
||||
if (!botHasPermission(perms, BOT_PERMISSION.SEND_MESSAGES)) {
|
||||
throw new Error('bot lacks SEND_MESSAGES permission')
|
||||
}
|
||||
if (!chId) throw new Error('no channel')
|
||||
@@ -21022,6 +21040,43 @@ class PearcordPlatform extends EventEmitter {
|
||||
return this.db.find(COLLECTIONS.GUILD_BOTS, { guildId: this.guild.guild.id })
|
||||
}
|
||||
|
||||
async uninstallBot (botId) {
|
||||
const guildId = this.guild?.guild?.id || null
|
||||
const span = this.log.time('bot.uninstall', { botId, guildId })
|
||||
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 uninstall bots')
|
||||
}
|
||||
const bot = await this.db.get(COLLECTIONS.GUILD_BOTS, { id: botId })
|
||||
if (!bot) throw new Error('bot not found')
|
||||
if (bot.managed) throw new Error('managed bot cannot be uninstalled')
|
||||
await this.db.delete(COLLECTIONS.GUILD_BOTS, { id: botId })
|
||||
this.emit('bot', { ...bot, removed: true })
|
||||
const remaining = await this.listBots().catch(() => [])
|
||||
span.end({
|
||||
botId,
|
||||
guildId,
|
||||
botName: String(bot.name || '').slice(0, 32),
|
||||
uninstalled: true,
|
||||
guildBotCount: remaining.length,
|
||||
spanKind: 'bot.uninstall',
|
||||
activeChannelId: this.activeChannelId,
|
||||
guildCount: (this.guilds || []).length
|
||||
})
|
||||
return bot
|
||||
} catch (err) {
|
||||
this.log.error('bot.uninstall error', {
|
||||
botId,
|
||||
guildId,
|
||||
error: err?.message || String(err)
|
||||
})
|
||||
span.fail(err)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
async listChannelWebhooks () {
|
||||
if (!this.guild?.guild) return []
|
||||
await this._ensureGuildModules()
|
||||
@@ -26287,6 +26342,7 @@ const { announcementsMixin } = require('./announcements-mixin')
|
||||
const { permissionsMixin } = require('./permissions-mixin')
|
||||
const { memberRolesMixin } = require('./member-roles-mixin')
|
||||
const { slashMixin } = require('./slash-mixin')
|
||||
const { botsMixin } = require('./bots-mixin')
|
||||
Object.assign(PearcordPlatform.prototype, pollSchedulingMixin)
|
||||
Object.assign(PearcordPlatform.prototype, notificationsActivityMixin)
|
||||
Object.assign(PearcordPlatform.prototype, userSettingsMixin)
|
||||
@@ -26297,3 +26353,4 @@ Object.assign(PearcordPlatform.prototype, announcementsMixin)
|
||||
Object.assign(PearcordPlatform.prototype, permissionsMixin)
|
||||
Object.assign(PearcordPlatform.prototype, memberRolesMixin)
|
||||
Object.assign(PearcordPlatform.prototype, slashMixin)
|
||||
Object.assign(PearcordPlatform.prototype, botsMixin)
|
||||
|
||||
Reference in New Issue
Block a user