feat(automations): export/import manifest and GUILD_SYNC hooks slice

Wire pearcord-automation-export: list/upsert/delete hooks, exportAutomationManifest,
importAutomationManifest, mesh gossip ingest, and automationHooks in view().

Co-authored-by: Cursor <[email protected]>
This commit is contained in:
Raven Scott
2026-05-22 03:20:15 -04:00
co-authored by Cursor
parent 23a1c999d6
commit 41a0adf295
2 changed files with 169 additions and 1 deletions
+168 -1
View File
@@ -24,6 +24,11 @@ const {
APP_EVENTS,
APP_EVENT_CATEGORIES
} = require('pearcord-app-events')
const {
AutomationManifestStore,
buildAutomationManifest,
validateAutomationManifest
} = require('pearcord-automation-export')
const { INTENTS } = require('pearcord-bot-sdk')
const { BotRateLimits } = require('pearcord-bot-limits')
const { PearcordStepUp } = require('pearcord-step-up')
@@ -216,6 +221,7 @@ class PearcordPlatform extends EventEmitter {
this._recentSoundPlays = []
this.botEvents = null
this.appEvents = null
this.automationExport = null
this.discovery = null
this.contacts = null
this.botLimits = null
@@ -1107,6 +1113,14 @@ class PearcordPlatform extends EventEmitter {
if (this.channelPermissions) {
permBundle = await this.channelPermissions.exportSyncBundle(guild.id)
}
let automationSlice = { hooks: [] }
await this._initAutomationExport(guild.id)
if (this.automationExport) {
automationSlice = await this.automationExport.exportSyncSlice()
}
let automodConfig = null
await this._ensureGuildModules()
if (this.automod) automodConfig = await this.automod.getConfig()
if (guild.iconHash && this.attachments) {
const iconRow = await this.attachments.get(guild.iconHash).catch(() => null)
if (iconRow && !attachments.some((a) => a.id === iconRow.id)) {
@@ -1128,7 +1142,18 @@ class PearcordPlatform extends EventEmitter {
guildRoles: roleBundle.guildRoles,
memberRoleLinks: roleBundle.memberRoleLinks,
channelOverwrites: permBundle.channelOverwrites,
channelSettings
channelSettings,
automationHooks: automationSlice.hooks,
automodConfig: automodConfig
? {
enabled: !!automodConfig.enabled,
blockedKeywords: automodConfig.blockedKeywords || [],
maxMentions: automodConfig.maxMentions ?? 5,
spamLimit: automodConfig.spamLimit ?? 5,
spamWindowSeconds: automodConfig.spamWindowSeconds ?? 5,
blockInvites: automodConfig.blockInvites !== false
}
: null
}
}
@@ -1167,6 +1192,18 @@ class PearcordPlatform extends EventEmitter {
await this.channelPermissions.ingestSyncBundle(payload.guildId, {
channelOverwrites: payload.channelOverwrites
})
await this._initAutomationExport(payload.guildId)
if (this.automationExport && payload.automationHooks?.length) {
await this.automationExport.ingestSyncSlice(payload.guildId, {
hooks: payload.automationHooks
})
}
if (payload.automodConfig && this.automod) {
await this.automod.ingestGossip({
...payload.automodConfig,
guildId: payload.guildId
})
}
}
for (const mem of payload.members || []) {
if (!mem?.guildId || !mem?.userId) continue
@@ -1404,6 +1441,9 @@ class PearcordPlatform extends EventEmitter {
guildInstance.on('automod-config', (p) => {
this._onAutomodConfigGossip(p)
})
guildInstance.on('automation-manifest', (p) => {
this._onAutomationManifestGossip(p).catch(() => {})
})
guildInstance.on('screen-share', (p) => {
this.screenShare?.ingestGossip(p).catch(() => {})
this.emit('screen-share', p)
@@ -3734,9 +3774,130 @@ class PearcordPlatform extends EventEmitter {
}
const guildId = this.guild?.guild?.id
if (guildId && this.automod) this.automod.setGuild(guildId)
await this._initAutomationExport(guildId)
return { forum: this.forum, stage: this.stage, automod: this.automod }
}
async _initAutomationExport (guildId) {
if (!guildId) return
if (!this.automationExport) {
this.automationExport = new AutomationManifestStore({
storagePath: this.storagePath
})
await this.automationExport.ready()
}
this.automationExport.setGuild(guildId)
}
async listAutomationHooks () {
if (!this.guild?.guild) return []
await this._initAutomationExport(this.guild.guild.id)
return this.automationExport.listHooks()
}
async upsertAutomationHook (hook = {}) {
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 manage automations')
}
await this._initAutomationExport(this.guild.guild.id)
const row = await this.automationExport.upsertHook(hook)
this.emit('automation-hook', row)
return row
}
async deleteAutomationHook (hookId) {
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 manage automations')
}
await this._initAutomationExport(this.guild.guild.id)
const ok = await this.automationExport.deleteHook(hookId)
if (ok) this.emit('automation-hook-delete', { hookId, guildId: this.guild.guild.id })
return ok
}
async exportAutomationManifest () {
if (!this.guild?.guild) throw new Error('no guild')
const user = this.identity.user
if (!user) throw new Error('register first')
const roles = await this._memberRoles()
if (!roleHasPermission(roles, PERMISSION.MANAGE_GUILD)) {
throw new Error('no permission to export automations')
}
await this._ensureGuildModules()
await this._initAutomationExport(this.guild.guild.id)
const guild = this.guild.guild
const automodConfig = await this.automod.getConfig()
const slashCommands = this.slashCommands ? await this.slashCommands.list() : []
const bots = await this.db.find(COLLECTIONS.GUILD_BOTS, { guildId: guild.id })
const hooks = await this.automationExport.listHooks()
return buildAutomationManifest({
guildId: guild.id,
guildName: guild.name,
exportedBy: user.id,
automodConfig,
slashCommands,
bots,
hooks,
appEventCategories: APP_EVENT_CATEGORIES.ALL
})
}
async importAutomationManifest (manifest, opts = {}) {
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 import automations')
}
const check = validateAutomationManifest(manifest, {
expectedGuildId: this.guild.guild.id
})
if (!check.ok) throw new Error(check.errors.join('; '))
await this._ensureGuildModules()
await this._initAutomationExport(this.guild.guild.id)
const merge = opts.merge !== false
if (manifest.automod) {
await this.setAutomodConfig(manifest.automod)
}
const hooks = await this.automationExport.replaceHooks(manifest.hooks || [], { merge })
const summary = {
guildId: this.guild.guild.id,
hooksImported: hooks.length,
automodApplied: !!manifest.automod,
importedAt: Date.now()
}
if (this.guild.gossipAutomationManifest) {
this.guild.gossipAutomationManifest({
guildId: this.guild.guild.id,
manifest,
summary
})
}
this.emit('automation-manifest-import', summary)
return { manifest, summary, hooks }
}
async _onAutomationManifestGossip (payload) {
if (!payload?.guildId || !payload?.manifest) return
if (this.guild?.guild?.id !== payload.guildId) return
const check = validateAutomationManifest(payload.manifest, {
expectedGuildId: payload.guildId
})
if (!check.ok) return
await this._initAutomationExport(payload.guildId)
await this.automationExport.replaceHooks(payload.manifest.hooks || [], { merge: true })
if (payload.manifest.automod) {
await this._onAutomodConfigGossip({
...payload.manifest.automod,
guildId: payload.guildId
})
}
this.emit('automation-manifest', payload)
}
async _onAutomodConfigGossip (payload) {
if (!payload?.guildId) return
await this._ensureGuildModules()
@@ -5214,6 +5375,11 @@ class PearcordPlatform extends EventEmitter {
this.automod.setGuild(guild.id)
automodConfig = await this.automod.getConfig()
}
let automationHooks = []
if (guild) {
await this._initAutomationExport(guild.id)
automationHooks = await this.automationExport.listHooks()
}
let announcementCrosspostTargets = []
let announcementCrosspostByChannel = {}
let announcementFollowing = false
@@ -5362,6 +5528,7 @@ class PearcordPlatform extends EventEmitter {
myStageRole,
stagePendingRequests,
automodConfig,
automationHooks,
announcementCrosspostTargets,
announcementCrosspostByChannel,
announcementFollowing,
+1
View File
@@ -13,6 +13,7 @@
"pearcord-announcements": "file:../pearcord-announcements",
"pearcord-app-events": "file:../pearcord-app-events",
"pearcord-attachments": "file:../pearcord-attachments",
"pearcord-automation-export": "file:../pearcord-automation-export",
"pearcord-automod": "file:../pearcord-automod",
"pearcord-boosts": "file:../pearcord-boosts",
"pearcord-bot-auth": "file:../pearcord-bot-auth",