feat(automations): APP_EVENT fanout and createAppListener

Wire pearcord-app-events on guild open: bridge bot payloads, fanout channel
permission and custom-role changes, gossip APP_EVENT to mesh, and expose
view.appEvents snapshot for debugging.

Co-authored-by: Cursor <[email protected]>
This commit is contained in:
Raven Scott
2026-05-22 03:15:08 -04:00
co-authored by Cursor
parent 11401afe9e
commit 23a1c999d6
2 changed files with 57 additions and 0 deletions
+56
View File
@@ -19,6 +19,11 @@ const { VoiceCaptureFeed } = require('pearcord-voice-capture')
const { PearcordSlashRegistry, parseSlashInvocation } = require('pearcord-commands') const { PearcordSlashRegistry, parseSlashInvocation } = require('pearcord-commands')
const { PearcordEmojiRegistry, buildEmojiSlotSnapshot } = require('pearcord-emoji') const { PearcordEmojiRegistry, buildEmojiSlotSnapshot } = require('pearcord-emoji')
const { BotEventFanout, BOT_EVENTS } = require('pearcord-bot-events') const { BotEventFanout, BOT_EVENTS } = require('pearcord-bot-events')
const {
AppEventBridge,
APP_EVENTS,
APP_EVENT_CATEGORIES
} = require('pearcord-app-events')
const { INTENTS } = require('pearcord-bot-sdk') const { INTENTS } = require('pearcord-bot-sdk')
const { BotRateLimits } = require('pearcord-bot-limits') const { BotRateLimits } = require('pearcord-bot-limits')
const { PearcordStepUp } = require('pearcord-step-up') const { PearcordStepUp } = require('pearcord-step-up')
@@ -210,6 +215,7 @@ class PearcordPlatform extends EventEmitter {
this.channelPermissions = null this.channelPermissions = null
this._recentSoundPlays = [] this._recentSoundPlays = []
this.botEvents = null this.botEvents = null
this.appEvents = null
this.discovery = null this.discovery = null
this.contacts = null this.contacts = null
this.botLimits = null this.botLimits = null
@@ -1354,19 +1360,33 @@ class PearcordPlatform extends EventEmitter {
guildInstance.on('guild-member-roles', (payload) => { guildInstance.on('guild-member-roles', (payload) => {
this.guildRoles?.ingestMemberRolesGossip(payload).catch(() => {}) this.guildRoles?.ingestMemberRolesGossip(payload).catch(() => {})
this.emit('guild-member-roles', payload) this.emit('guild-member-roles', payload)
this._fanoutAppEvent(APP_EVENTS.MEMBER_ROLE_UPDATE, { memberRoles: payload }).catch(
() => {}
)
}) })
guildInstance.on('channel-overwrite', (row) => { guildInstance.on('channel-overwrite', (row) => {
this.channelPermissions?.ingestOverwriteGossip(row).catch(() => {}) this.channelPermissions?.ingestOverwriteGossip(row).catch(() => {})
this.emit('channel-overwrite', row) this.emit('channel-overwrite', row)
this._fanoutAppEvent(APP_EVENTS.CHANNEL_PERMISSION_UPDATE, { overwrite: row }).catch(
() => {}
)
}) })
guildInstance.on('channel-overwrite-delete', (payload) => { guildInstance.on('channel-overwrite-delete', (payload) => {
this.channelPermissions?.ingestOverwriteDelete(payload).catch(() => {}) this.channelPermissions?.ingestOverwriteDelete(payload).catch(() => {})
this.emit('channel-overwrite-delete', payload) this.emit('channel-overwrite-delete', payload)
this._fanoutAppEvent(APP_EVENTS.CHANNEL_PERMISSION_DELETE, { delete: payload }).catch(
() => {}
)
}) })
guildInstance.on('bot-event', (payload) => { guildInstance.on('bot-event', (payload) => {
this.botEvents?.ingest(payload) this.botEvents?.ingest(payload)
this.appEvents?.bridgeFromBot(payload)
this.emit('bot-event', payload) this.emit('bot-event', payload)
}) })
guildInstance.on('app-event', (payload) => {
this.appEvents?.ingest(payload)
this.emit('app-event', payload)
})
guildInstance.on('bot-install', (bot) => { guildInstance.on('bot-install', (bot) => {
this._ingestGuildBot(bot).catch(() => {}) this._ingestGuildBot(bot).catch(() => {})
this.emit('bot', bot) this.emit('bot', bot)
@@ -1870,6 +1890,7 @@ class PearcordPlatform extends EventEmitter {
) )
this.guild.gossipMemberCustomRoles(link) this.guild.gossipMemberCustomRoles(link)
this.emit('guild-member-roles', link) this.emit('guild-member-roles', link)
await this._fanoutAppEvent(APP_EVENTS.MEMBER_ROLE_UPDATE, { memberRoles: link })
this.emit('member', await this.db.get(COLLECTIONS.MEMBERS, { this.emit('member', await this.db.get(COLLECTIONS.MEMBERS, {
guildId: this.guild.guild.id, guildId: this.guild.guild.id,
userId userId
@@ -2019,6 +2040,7 @@ class PearcordPlatform extends EventEmitter {
this.guild.gossipChannelOverwriteUpsert(row) this.guild.gossipChannelOverwriteUpsert(row)
await this._audit('channel.overwrite', { guildId: this.guild.guild.id, targetId: chId }) await this._audit('channel.overwrite', { guildId: this.guild.guild.id, targetId: chId })
this.emit('channel-overwrite', row) this.emit('channel-overwrite', row)
await this._fanoutAppEvent(APP_EVENTS.CHANNEL_PERMISSION_UPDATE, { overwrite: row })
return row return row
} }
@@ -2044,6 +2066,9 @@ class PearcordPlatform extends EventEmitter {
}) })
} }
this.emit('channel-overwrite-delete', { channelId: chId, targetType, targetId }) this.emit('channel-overwrite-delete', { channelId: chId, targetType, targetId })
await this._fanoutAppEvent(APP_EVENTS.CHANNEL_PERMISSION_DELETE, {
delete: { channelId: chId, targetType, targetId }
})
return prev return prev
} }
@@ -2204,6 +2229,13 @@ class PearcordPlatform extends EventEmitter {
}) })
} }
async _initAppEvents (guildId) {
this.appEvents = new AppEventBridge({
guildId,
categories: APP_EVENT_CATEGORIES.ALL
})
}
async _fanoutBotEvent (type, data) { async _fanoutBotEvent (type, data) {
if (!this.botEvents || !this.guild?.guild) return null if (!this.botEvents || !this.guild?.guild) return null
if (!this.botLimits) { if (!this.botLimits) {
@@ -2214,11 +2246,21 @@ class PearcordPlatform extends EventEmitter {
if (!allowed) return null if (!allowed) return null
const payload = this.botEvents.createPayload(type, data) const payload = this.botEvents.createPayload(type, data)
this.botEvents.deliverLocal(payload) this.botEvents.deliverLocal(payload)
this.appEvents?.bridgeFromBot(payload)
this.guild.gossipBotEvent(payload) this.guild.gossipBotEvent(payload)
this.emit('bot-event', payload) this.emit('bot-event', payload)
return payload return payload
} }
async _fanoutAppEvent (type, data) {
if (!this.appEvents || !this.guild?.guild) return null
const payload = this.appEvents.createPayload(type, data)
this.appEvents.deliverLocal(payload)
if (this.guild.gossipAppEvent) this.guild.gossipAppEvent(payload)
this.emit('app-event', payload)
return payload
}
async _initGuildVoice (guildId) { async _initGuildVoice (guildId) {
const user = this.identity.user const user = this.identity.user
if (!user) return if (!user) return
@@ -2228,6 +2270,7 @@ class PearcordPlatform extends EventEmitter {
await this._initSoundboardRegistry(guildId) await this._initSoundboardRegistry(guildId)
await this._initGuildRoles(guildId) await this._initGuildRoles(guildId)
await this._initBotEvents(guildId) await this._initBotEvents(guildId)
await this._initAppEvents(guildId)
this.voice = new PearcordVoice({ this.voice = new PearcordVoice({
storagePath: this.storagePath, storagePath: this.storagePath,
guildId, guildId,
@@ -3882,6 +3925,18 @@ class PearcordPlatform extends EventEmitter {
return listener return listener
} }
createAppListener (categories = APP_EVENT_CATEGORIES.ALL) {
if (!this.guild?.guild) throw new Error('no guild')
const listener = new AppEventBridge({
guildId: this.guild.guild.id,
categories: categories ?? APP_EVENT_CATEGORIES.ALL
})
const onRemote = (payload) => listener.ingest(payload)
this.guild.on('app-event', onRemote)
listener._unwire = () => this.guild.removeListener('app-event', onRemote)
return listener
}
async executeSlashCommand (content, opts = {}) { async executeSlashCommand (content, opts = {}) {
if (!this.slashCommands || !this.messages) throw new Error('no channel') if (!this.slashCommands || !this.messages) throw new Error('no channel')
const inv = parseSlashInvocation(content) const inv = parseSlashInvocation(content)
@@ -5353,6 +5408,7 @@ class PearcordPlatform extends EventEmitter {
? await this.emojiRegistry.reactionGlyphs() ? await this.emojiRegistry.reactionGlyphs()
: ['👍', '❤️', '😂'], : ['👍', '❤️', '😂'],
botEvents: this.botEvents?.snapshot() || null, botEvents: this.botEvents?.snapshot() || null,
appEvents: this.appEvents?.snapshot() || null,
bots: this.guild bots: this.guild
? (await this.db.find(COLLECTIONS.GUILD_BOTS, { guildId: this.guild.id })).map((b) => ({ ? (await this.db.find(COLLECTIONS.GUILD_BOTS, { guildId: this.guild.id })).map((b) => ({
...b, ...b,
+1
View File
@@ -11,6 +11,7 @@
"pearcord-activity": "file:../pearcord-activity", "pearcord-activity": "file:../pearcord-activity",
"pearcord-announcement-hub": "file:../pearcord-announcement-hub", "pearcord-announcement-hub": "file:../pearcord-announcement-hub",
"pearcord-announcements": "file:../pearcord-announcements", "pearcord-announcements": "file:../pearcord-announcements",
"pearcord-app-events": "file:../pearcord-app-events",
"pearcord-attachments": "file:../pearcord-attachments", "pearcord-attachments": "file:../pearcord-attachments",
"pearcord-automod": "file:../pearcord-automod", "pearcord-automod": "file:../pearcord-automod",
"pearcord-boosts": "file:../pearcord-boosts", "pearcord-boosts": "file:../pearcord-boosts",