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.
|
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 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`.
|
**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(() => ({
|
const slashUsageHeal = await this._healSlashUsageCursorOnPartition(gid).catch(() => ({
|
||||||
relisted: 0
|
relisted: 0
|
||||||
}))
|
}))
|
||||||
|
const botRegistryHeal = await this._healBotRegistryOnPartition(gid).catch(() => ({
|
||||||
|
relisted: 0
|
||||||
|
}))
|
||||||
|
const botTokenHeal = await this._healBotTokenCursorOnPartition(gid).catch(() => ({
|
||||||
|
relisted: 0
|
||||||
|
}))
|
||||||
return {
|
return {
|
||||||
voiceApplied,
|
voiceApplied,
|
||||||
emojiSlots,
|
emojiSlots,
|
||||||
@@ -1621,7 +1627,9 @@ class PearcordPlatform extends EventEmitter {
|
|||||||
memberRoleLinksHeal,
|
memberRoleLinksHeal,
|
||||||
effectivePermCacheHeal,
|
effectivePermCacheHeal,
|
||||||
slashRegistryHeal,
|
slashRegistryHeal,
|
||||||
slashUsageHeal
|
slashUsageHeal,
|
||||||
|
botRegistryHeal,
|
||||||
|
botTokenHeal
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -17047,7 +17055,8 @@ class PearcordPlatform extends EventEmitter {
|
|||||||
openFollowAnnouncement: !!parsed.followAnnouncement,
|
openFollowAnnouncement: !!parsed.followAnnouncement,
|
||||||
openChannelPermissions: !!parsed.editChannelPermissions,
|
openChannelPermissions: !!parsed.editChannelPermissions,
|
||||||
openAssignMemberRoles: !!parsed.assignMemberRoles,
|
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)
|
const bot = payloadToBotRow(payload, this.identity.snapshot().publicKey)
|
||||||
await this._ingestGuildBot(bot)
|
await this._ingestGuildBot(bot)
|
||||||
this.guild.gossipBotInstall(bot)
|
if (this._shouldGossipBotInstall(bot)) {
|
||||||
|
this.guild.gossipBotInstall(bot)
|
||||||
|
}
|
||||||
this.emit('bot', bot)
|
this.emit('bot', bot)
|
||||||
span.end({
|
span.end({
|
||||||
guildId,
|
guildId,
|
||||||
@@ -20897,7 +20908,9 @@ class PearcordPlatform extends EventEmitter {
|
|||||||
botName: String(bot.name || '').slice(0, 32),
|
botName: String(bot.name || '').slice(0, 32),
|
||||||
permissionMask: Number(bot.permissions) || 0,
|
permissionMask: Number(bot.permissions) || 0,
|
||||||
tokenIssued: !!token,
|
tokenIssued: !!token,
|
||||||
|
guildBotCount: (await this.listBots().catch(() => [])).length,
|
||||||
spanKind: 'bot.token',
|
spanKind: 'bot.token',
|
||||||
|
installKind: 'bot.install',
|
||||||
activeChannelId: this.activeChannelId
|
activeChannelId: this.activeChannelId
|
||||||
})
|
})
|
||||||
return { token, bot, expiresAt: payload.expiresAt }
|
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')
|
throw new Error('load the target server before installing a bot')
|
||||||
}
|
}
|
||||||
await this._ingestGuildBot(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)
|
this.emit('bot', bot)
|
||||||
span.end({
|
span.end({
|
||||||
botId: bot.id,
|
botId: bot.id,
|
||||||
@@ -20932,7 +20947,9 @@ class PearcordPlatform extends EventEmitter {
|
|||||||
botName: String(bot.name || '').slice(0, 32),
|
botName: String(bot.name || '').slice(0, 32),
|
||||||
permissionMask: Number(bot.permissions) || 0,
|
permissionMask: Number(bot.permissions) || 0,
|
||||||
installed: true,
|
installed: true,
|
||||||
|
guildBotCount: (await this.listBots().catch(() => [])).length,
|
||||||
spanKind: 'bot.install',
|
spanKind: 'bot.install',
|
||||||
|
installKind: 'bot.install',
|
||||||
activeChannelId: this.activeChannelId,
|
activeChannelId: this.activeChannelId,
|
||||||
guildCount: (this.guilds || []).length
|
guildCount: (this.guilds || []).length
|
||||||
})
|
})
|
||||||
@@ -20981,7 +20998,8 @@ class PearcordPlatform extends EventEmitter {
|
|||||||
if (!this.guild?.guild) throw new Error('no guild')
|
if (!this.guild?.guild) throw new Error('no guild')
|
||||||
const bot = await this.db.get(COLLECTIONS.GUILD_BOTS, { id: botId })
|
const bot = await this.db.get(COLLECTIONS.GUILD_BOTS, { id: botId })
|
||||||
if (!bot) throw new Error('bot not found')
|
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')
|
throw new Error('bot lacks SEND_MESSAGES permission')
|
||||||
}
|
}
|
||||||
if (!chId) throw new Error('no channel')
|
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 })
|
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 () {
|
async listChannelWebhooks () {
|
||||||
if (!this.guild?.guild) return []
|
if (!this.guild?.guild) return []
|
||||||
await this._ensureGuildModules()
|
await this._ensureGuildModules()
|
||||||
@@ -26287,6 +26342,7 @@ const { announcementsMixin } = require('./announcements-mixin')
|
|||||||
const { permissionsMixin } = require('./permissions-mixin')
|
const { permissionsMixin } = require('./permissions-mixin')
|
||||||
const { memberRolesMixin } = require('./member-roles-mixin')
|
const { memberRolesMixin } = require('./member-roles-mixin')
|
||||||
const { slashMixin } = require('./slash-mixin')
|
const { slashMixin } = require('./slash-mixin')
|
||||||
|
const { botsMixin } = require('./bots-mixin')
|
||||||
Object.assign(PearcordPlatform.prototype, pollSchedulingMixin)
|
Object.assign(PearcordPlatform.prototype, pollSchedulingMixin)
|
||||||
Object.assign(PearcordPlatform.prototype, notificationsActivityMixin)
|
Object.assign(PearcordPlatform.prototype, notificationsActivityMixin)
|
||||||
Object.assign(PearcordPlatform.prototype, userSettingsMixin)
|
Object.assign(PearcordPlatform.prototype, userSettingsMixin)
|
||||||
@@ -26297,3 +26353,4 @@ Object.assign(PearcordPlatform.prototype, announcementsMixin)
|
|||||||
Object.assign(PearcordPlatform.prototype, permissionsMixin)
|
Object.assign(PearcordPlatform.prototype, permissionsMixin)
|
||||||
Object.assign(PearcordPlatform.prototype, memberRolesMixin)
|
Object.assign(PearcordPlatform.prototype, memberRolesMixin)
|
||||||
Object.assign(PearcordPlatform.prototype, slashMixin)
|
Object.assign(PearcordPlatform.prototype, slashMixin)
|
||||||
|
Object.assign(PearcordPlatform.prototype, botsMixin)
|
||||||
|
|||||||
Reference in New Issue
Block a user