Files
pearcord-ui-flow/index.js
T
Raven ScottandCursor 7736f43ea9 feat(ui-flow): emit bot-installed and slash-invoked UI events
After install-bot-token and slash send-message, notify the Pear UI with
structured sidecar messages for confirmation toasts and composer autofocus.

Co-authored-by: Cursor <[email protected]>
2026-05-24 04:22:50 -04:00

1585 lines
45 KiB
JavaScript

'use strict'
const b4a = require('b4a')
const { createLogger } = require('pearcord-log')
const { USER_STATUS } = require('pearcord-platform')
const flowLog = createLogger('ui-flow')
let voiceCaptureChunkCount = 0
let voiceCaptureBytes = 0
let displayCaptureFrameCount = 0
let voiceCaptureTraceWindowStart = 0
let voiceCaptureTraceWindowBytes = 0
function resetVoiceCaptureTrace () {
voiceCaptureChunkCount = 0
voiceCaptureBytes = 0
voiceCaptureTraceWindowStart = Date.now()
voiceCaptureTraceWindowBytes = 0
}
function resetDisplayCaptureTrace () {
displayCaptureFrameCount = 0
}
function fromBase64 (str) {
if (typeof Buffer !== 'undefined') return Buffer.from(str || '', 'base64')
return b4a.from(str || '', 'base64')
}
function normalizeEmojiImage (image) {
if (!image?.dataBase64) return image
return {
data: fromBase64(image.dataBase64),
filename: image.filename,
mimeType: image.mimeType
}
}
/**
* Dispatch one UI → sidecar IPC message (same contract as apps/pearcord/index.js).
* @param {import('pearcord-platform').PearcordPlatform} platform
* @param {object} msg parsed JSON line from pear-pipe
* @param {{ send?: (obj: object) => void, pushState?: () => void }} hooks
* @returns {Promise<boolean>} true when type was handled
*/
async function dispatchUiMessage (platform, msg, hooks = {}) {
const send = hooks.send || (() => {})
const pushState = hooks.pushState || (() => {})
const log = hooks.log || flowLog
const t = msg?.type
if (!t) {
log.warn('ipc missing type', { keys: msg ? Object.keys(msg) : [] })
return false
}
if (t === 'register') {
await platform.registerUser({
username: msg.username,
displayName: msg.displayName || msg.username
})
pushState()
return true
}
if (t === 'create-guild') {
await platform.createGuild({
name: msg.name || 'My Server',
publicListing: msg.publicListing !== false
})
pushState()
return true
}
if (t === 'join-invite') {
const result = await platform.joinInvite(msg.code)
pushState()
if (result?.kind === 'explore') {
send({ type: 'deep-link-result', url: msg.code, ...result })
}
return true
}
if (t === 'join-public-discovery') {
await platform.joinPublicDiscovery(msg.inviteCode)
pushState()
return true
}
if (t === 'set-discovery-listing') {
await platform.setGuildDiscoveryListing(msg.guildId, msg.listed !== false)
pushState()
return true
}
if (t === 'update-discovery-filter') {
await platform.setDiscoveryFilter({
query: msg.query,
sort: msg.sort,
minMembers: msg.minMembers,
tag: msg.tag
})
pushState()
return true
}
if (t === 'discovery-explore-opened') {
await platform.markDiscoveryExploreOpened()
pushState()
return true
}
if (t === 'mark-onboarding-explore-shown') {
await platform.markOnboardingExploreShown()
pushState()
return true
}
if (t === 'set-discovery-tags') {
await platform.setGuildDiscoveryTags(msg.guildId, msg.tags)
pushState()
return true
}
if (t === 'set-automod-config') {
await platform.setAutomodConfig(msg.config || {})
pushState()
return true
}
if (t === 'upsert-automation-hook') {
await platform.upsertAutomationHook(msg.hook || {})
pushState()
return true
}
if (t === 'delete-automation-hook') {
await platform.deleteAutomationHook(msg.hookId)
pushState()
return true
}
if (t === 'export-automation-manifest') {
const manifest = await platform.exportAutomationManifest()
send({ type: 'automation-manifest', manifest })
return true
}
if (t === 'import-automation-manifest') {
await platform.importAutomationManifest(msg.manifest || {}, {
merge: msg.merge !== false
})
pushState()
return true
}
if (t === 'set-automation-receipt-retention') {
await platform.setAutomationReceiptRetention(msg.retention || {})
pushState()
return true
}
if (t === 'set-hook-failure-prefs') {
await platform.setHookFailurePrefs(msg.prefs || {})
pushState()
return true
}
if (t === 'get-digest-relay-handoff') {
const handoff = await platform.getLastDigestRelayHandoff()
send({ type: 'digest-relay-handoff', handoff })
return true
}
if (t === 'dry-run-digest-webhook-relay') {
const out = await platform.dryRunDigestWebhookRelay({
webhookUrl: msg.webhookUrl
})
send({ type: 'digest-webhook-relay-dry-run', ...out })
return true
}
if (t === 'refresh-archive-peer-health') {
const out = await platform.refreshArchivePeerHealth()
pushState()
send({ type: 'archive-peer-health-refreshed', ...out })
return true
}
if (t === 'set-archive-health-prefs') {
await platform.setArchiveHealthPrefs(msg.prefs || {})
pushState()
return true
}
if (t === 'announce-worker-release') {
await platform.announceWorkerRelease({
workerVersion: msg.workerVersion,
pearUri: msg.pearUri,
releaseNotes: msg.releaseNotes,
channelTag: msg.channelTag,
allowDowngrade: !!msg.allowDowngrade
})
pushState()
return true
}
if (t === 'export-audit-log') {
const out = await platform.exportAuditLog({
format: msg.format,
filter: msg.filter,
limit: msg.limit,
gossip: msg.gossip !== false,
sign: msg.sign !== false && msg.format === 'csv',
recordMesh: msg.recordMesh !== false
})
if (msg.download && msg.format === 'csv') {
send({ type: 'audit-export-download', ...out })
} else {
send({ type: 'audit-export', ...out })
}
return true
}
if (t === 'sync-audit-export-csv') {
const out = await platform.pushAuditExportCsvToMesh({
filter: msg.filter,
limit: msg.limit
})
pushState()
send({ type: 'audit-export-csv-synced', ...out })
return true
}
if (t === 'export-hook-failure-digest') {
const out = await platform.exportHookFailureDigest({
format: msg.format,
windowMs: msg.windowMs
})
send({ type: 'hook-failure-digest-export', ...out })
return true
}
if (t === 'sync-hook-failure-digest') {
const digest = await platform.pushHookFailureDigestToMesh()
pushState()
send({ type: 'hook-failure-digest-synced', digest })
return true
}
if (t === 'set-audit-export-schedule') {
await platform.setAuditExportSchedule(msg.schedule || {})
pushState()
return true
}
if (t === 'run-audit-export-now') {
const out = await platform.runScheduledAuditExportNow()
pushState()
send({ type: 'audit-export-scheduled', result: out })
return true
}
if (t === 'set-digest-export-schedule') {
await platform.setDigestExportSchedule(msg.schedule || {})
pushState()
return true
}
if (t === 'run-digest-export-now') {
const digest = await platform.runScheduledDigestExportNow()
pushState()
send({ type: 'digest-export-scheduled', digest })
return true
}
if (t === 'get-worker-install-plan') {
const plan = await platform.fetchWorkerInstallPlan(msg.templateId || 'message-logger')
send({ type: 'worker-install-plan', plan })
return true
}
if (t === 'fetch-audit-export-archive') {
const fetchMesh = !!msg.fetchMesh
try {
const out = fetchMesh
? await platform.fetchAuditExportArchiveFromMesh(msg.archiveId, {
format: msg.format,
timeoutMs: msg.timeoutMs,
maxAttempts: msg.maxAttempts,
baseDelayMs: msg.baseDelayMs,
targetMemberId: msg.targetMemberId || null
})
: await platform.exportAuditExportArchive(msg.archiveId, {
format: msg.format
})
send({ type: 'audit-export-archive-body', ...out, fetchMesh })
} catch (err) {
send({
type: 'audit-export-archive-error',
archiveId: msg.archiveId,
message: err?.message || String(err),
fetchAttempts: err?.fetchAttempts || 0,
fetchMesh
})
}
return true
}
if (t === 'get-automation-schedule-dashboard') {
const dashboard = await platform.getAutomationScheduleDashboard()
send({ type: 'automation-schedule-dashboard', dashboard })
return true
}
if (t === 'get-automation-health-dashboard') {
const dashboard = await platform.getAutomationHealthDashboard()
send({ type: 'automation-health-dashboard', dashboard })
return true
}
if (t === 'set-automation-digest-notify-prefs') {
await platform.setAutomationDigestNotifyPrefs(msg.prefs || {})
pushState()
return true
}
if (t === 'set-hook-failure-digest-notify-prefs') {
await platform.setHookFailureDigestNotifyPrefs(msg.prefs || {})
pushState()
return true
}
if (t === 'ensure-mod-digest-group-dm') {
const channel = await platform.ensureModDigestGroupDmChannel()
pushState()
send({ type: 'mod-digest-group-dm-ready', channelId: channel?.id || null })
return true
}
if (t === 'run-overdue-automation-schedules') {
const result = await platform.runOverdueAutomationSchedules()
pushState()
send({ type: 'overdue-automation-ran', result })
return true
}
if (t === 'list-audit-archive-fetch-peers') {
const peers = await platform.listAuditArchiveFetchPeers()
send({ type: 'audit-archive-fetch-peers', peers })
return true
}
if (t === 'fetch-automation-digest-snapshot') {
const fetchMesh = true
try {
const out = await platform.fetchAutomationDigestSnapshotFromMesh(msg.snapshotId, {
timeoutMs: msg.timeoutMs,
maxAttempts: msg.maxAttempts,
baseDelayMs: msg.baseDelayMs,
targetMemberId: msg.targetMemberId || null
})
send({ type: 'automation-digest-snapshot-body', ...out, fetchMesh })
} catch (err) {
send({
type: 'automation-digest-snapshot-error',
snapshotId: msg.snapshotId,
message: err?.message || String(err),
fetchAttempts: err?.fetchAttempts || 0,
fetchMesh
})
}
return true
}
if (t === 'export-automation-schedule-digest') {
const out = await platform.exportAutomationScheduleDigest({
format: msg.format,
windowMs: msg.windowMs
})
send({ type: 'automation-schedule-digest-export', ...out })
return true
}
if (t === 'run-worker-trial-matrix') {
const result = await platform.runIntegrationWorkerTrialMatrix({
holdMs: msg.holdMs
})
send({ type: 'worker-trial-matrix', result })
return true
}
if (t === 'set-automation-digest-export-schedule') {
await platform.setAutomationDigestExportSchedule(msg.schedule || {})
pushState()
return true
}
if (t === 'run-automation-digest-export-now') {
const out = await platform.runScheduledAutomationDigestExportNow()
pushState()
send({ type: 'automation-digest-export-scheduled', result: out })
return true
}
if (t === 'run-worker-install-trial') {
const result = await platform.runIntegrationWorkerTrial({
templateId: msg.templateId,
holdMs: msg.holdMs
})
send({ type: 'worker-install-trial', result })
return true
}
if (t === 'export-integration-worker-script') {
const script = platform.buildIntegrationWorkerScript(msg.templateId || 'message-logger')
send({
type: 'integration-worker-script',
templateId: msg.templateId,
script
})
return true
}
if (t === 'dry-run-automation-hook') {
const out = await platform.dryRunAutomationHook(msg.hookId, msg.eventType)
send({ type: 'automation-hook-dry-run', ...out })
return true
}
if (t === 'dry-run-automod') {
const out = await platform.dryRunAutomodMessage(msg.content || '', {
memberRoles: ['member']
})
send({ type: 'automod-dry-run', ...out })
return true
}
if (t === 'set-announcement-crosspost') {
await platform.setAnnouncementCrosspostTargets(
msg.channelId,
msg.targetChannelIds || []
)
pushState()
return true
}
if (t === 'set-announcement-hub-publish') {
await platform.setAnnouncementHubPublisher(msg.channelId, !!msg.enabled)
pushState()
return true
}
if (t === 'subscribe-remote-announcement-hub') {
await platform.subscribeRemoteAnnouncementHub({
sourceGuildId: msg.sourceGuildId,
sourceChannelId: msg.sourceChannelId,
targetChannelId: msg.targetChannelId
})
pushState()
return true
}
if (t === 'follow-announcement') {
await platform.followAnnouncementChannel(msg.channelId)
pushState()
return true
}
if (t === 'unfollow-announcement') {
await platform.unfollowAnnouncementChannel(msg.channelId)
pushState()
return true
}
if (t === 'add-guild-boost') {
await platform.addGuildBoost(msg.amount ?? 1)
pushState()
return true
}
if (t === 'contribute-guild-boost') {
await platform.contributeGuildBoost(msg.amount ?? 1)
pushState()
return true
}
if (t === 'set-guild-boost-banner') {
await platform.setGuildBoostBannerColor(msg.bannerColor)
pushState()
return true
}
if (t === 'voice-capture-chunk') {
voiceCaptureChunkCount++
const chunk = msg.data
let nbytes = 0
if (chunk) {
if (typeof chunk === 'string') nbytes = chunk.length
else if (chunk.byteLength != null) nbytes = chunk.byteLength
else if (chunk.length != null) nbytes = chunk.length
voiceCaptureBytes += nbytes
}
if (process.env.PEARCORD_LOG_VOICE_CAPTURE_TRACE === '1') {
if (!voiceCaptureTraceWindowStart) voiceCaptureTraceWindowStart = Date.now()
voiceCaptureTraceWindowBytes += nbytes
if (voiceCaptureChunkCount % 200 === 0) {
const elapsedSec = Math.max(
0.001,
(Date.now() - voiceCaptureTraceWindowStart) / 1000
)
log.trace('voice-capture chunks', {
count: voiceCaptureChunkCount,
bytes: voiceCaptureBytes,
bytesPerSec: Math.round(voiceCaptureTraceWindowBytes / elapsedSec)
})
voiceCaptureTraceWindowStart = Date.now()
voiceCaptureTraceWindowBytes = 0
}
}
platform.ingestVoiceCapture(msg.data)
return true
}
if (t === 'display-capture-frame') {
displayCaptureFrameCount++
if (
process.env.PEARCORD_LOG_VOICE_CAPTURE_TRACE === '1' &&
displayCaptureFrameCount % 200 === 0
) {
log.trace('display-capture frames', { count: displayCaptureFrameCount })
}
platform.ingestDisplayCapture({
data: msg.data,
mimeType: msg.mimeType,
width: msg.width,
height: msg.height
})
return true
}
if (t === 'create-invite') {
const inv = await platform.createInvite()
send({ type: 'invite', invite: inv })
pushState()
return true
}
if (t === 'select-guild') {
log.info('select-guild', { guildId: msg.guildId })
await platform.loadGuild(msg.guildId)
if (platform.guildOpenNoViewableChannels) {
send({
type: 'log',
record: {
ts: new Date().toISOString(),
level: 'warn',
scope: 'platform',
msg: 'guild open: no viewable channels',
meta: {
guildId: msg.guildId,
hint: 'You may lack channel view permission in this server'
}
}
})
}
pushState()
return true
}
if (t === 'join-stage') {
try {
await platform.joinStageChannel(msg.channelId)
pushState()
} catch (err) {
const errMsg = err?.message || String(err)
if (/voice channel is full/i.test(errMsg)) {
send({ type: 'error', message: errMsg })
return true
}
if (/no permission to connect/i.test(errMsg)) {
send({ type: 'error', message: errMsg })
return true
}
throw err
}
return true
}
if (t === 'request-stage-speak') {
try {
await platform.requestStageSpeak()
pushState()
} catch (err) {
if (/voice channel is full/i.test(err?.message || '')) {
send({ type: 'error', message: err.message })
return true
}
throw err
}
return true
}
if (t === 'select-channel') {
await platform.selectChannel(msg.channelId)
pushState()
return true
}
if (t === 'mark-channel-read') {
const guildId = platform.guild?.guild?.id
if (guildId && msg.channelId) {
await platform.markChannelRead(msg.channelId, guildId)
pushState()
let dismissChannelId = msg.channelId
const channels = (await platform.guild?.listChannels?.()) || []
const ch = channels.find((c) => c.id === msg.channelId)
if (ch?.type === 'thread' && ch.parentId) dismissChannelId = ch.parentId
send({
type: 'mark-channel-read-cleared',
guildId,
channelId: dismissChannelId,
threadChannelId: ch?.type === 'thread' ? msg.channelId : undefined
})
}
return true
}
if (t === 'join-voice') {
try {
await platform.joinVoiceChannel(msg.channelId)
pushState()
} catch (err) {
const errMsg = err?.message || String(err)
if (/voice channel is full/i.test(errMsg)) {
send({ type: 'error', message: errMsg })
return true
}
if (/no permission to connect|cannot join voice listen-only/i.test(errMsg)) {
send({ type: 'error', message: errMsg })
return true
}
throw err
}
return true
}
if (t === 'join-voice-listen') {
try {
await platform.joinVoiceListenOnly(msg.channelId)
pushState()
} catch (err) {
const errMsg = err?.message || String(err)
if (/voice channel is full/i.test(errMsg)) {
send({ type: 'error', message: errMsg })
return true
}
if (/no permission to connect|cannot join voice listen-only/i.test(errMsg)) {
send({ type: 'error', message: errMsg })
return true
}
throw err
}
return true
}
if (t === 'leave-voice') {
const payload = await platform.leaveVoiceChannel()
resetVoiceCaptureTrace()
pushState()
if (!payload) {
log.debug('leave-voice noop', { reason: 'not in voice channel' })
}
return true
}
if (t === 'start-screen-share') {
resetDisplayCaptureTrace()
await platform.startScreenShare(msg.label)
pushState()
return true
}
if (t === 'stop-screen-share') {
await platform.stopScreenShare()
resetDisplayCaptureTrace()
pushState()
return true
}
if (t === 'set-voice-mute') {
try {
await platform.setVoiceMute({ muted: msg.muted, deafened: msg.deafened })
pushState()
} catch (err) {
const msgText = err?.message || String(err)
if (msgText === 'not in voice' || /listen-only voice/i.test(msgText)) {
log.debug('set-voice-mute noop', {
muted: msg.muted,
deafened: msg.deafened,
reason: msgText
})
return true
}
throw err
}
return true
}
if (t === 'register-slash-commands') {
await platform.registerSlashCommands(msg.commands || [])
pushState()
return true
}
if (t === 'delete-slash-command') {
await platform.deleteSlashCommand(msg.name)
pushState()
return true
}
if (t === 'create-bot-install-token') {
const out = await platform.createBotInstallToken({
name: msg.name,
intents: msg.intents,
permissions: msg.permissions,
botPublicKey: msg.botPublicKey,
ttlMs: msg.ttlMs
})
send({ type: 'bot-install-token', token: out.token, bot: out.bot, expiresAt: out.expiresAt })
pushState()
return true
}
if (t === 'install-bot-token') {
const out = await platform.installBotFromToken(msg.token)
send({
type: 'bot-installed',
botName: out?.bot?.name || null,
bot: out?.bot || null
})
pushState()
return true
}
if (t === 'send-message') {
const trimmed = String(msg.content || '').trim()
const slashMatch = trimmed.match(/^\/([a-z0-9_-]+)/i)
await platform.sendMessage(msg.content, {
replyToId: msg.replyToId,
attachmentIds: msg.attachmentIds,
stickerNames: msg.stickerNames
})
if (slashMatch) send({ type: 'slash-invoked', name: slashMatch[1] })
pushState()
return true
}
if (t === 'stage-attachment') {
const att = await platform.stageAttachment({
data: fromBase64(msg.dataBase64),
filename: msg.filename,
mimeType: msg.mimeType
})
send({
type: 'attachment-staged',
attachment: att,
uploadLocalId: msg.uploadLocalId || null
})
pushState()
return true
}
if (t === 'update-guild-settings') {
await platform.updateGuildSettings({
name: msg.name,
iconHash: msg.iconHash,
nsfwGateEnabled: msg.nsfwGateEnabled
})
pushState()
return true
}
if (t === 'set-guild-icon-image') {
await platform.setGuildIconImage({
data: fromBase64(msg.dataBase64),
filename: msg.filename,
mimeType: msg.mimeType
})
pushState()
return true
}
if (t === 'clear-guild-icon') {
await platform.updateGuildSettings({ iconHash: null })
pushState()
return true
}
if (t === 'request-guild-icon') {
const preview = await platform.readGuildIconPreview(msg.iconHash, {
guildId: msg.guildId
})
send({ type: 'guild-icon-image', preview, guildId: msg.guildId || null })
return true
}
if (t === 'jump-to-message') {
const result = await platform.jumpToMessage(msg.messageId)
pushState()
send({ type: 'jump-to-message-result', ...result })
return true
}
if (t === 'navigate-deep-link') {
const result = await platform.navigateDeepLink(msg.url)
pushState()
send({ type: 'deep-link-result', url: msg.url, ...result })
return true
}
if (t === 'format-message-deep-link') {
const url = platform.formatMessageDeepLink(msg.messageId, {
channelId: msg.channelId,
guildId: msg.guildId
})
send({ type: 'message-deep-link', url, messageId: msg.messageId })
return true
}
if (t === 'format-invite-deep-link') {
const code = msg.inviteCode || msg.shareCode || msg.code
const url = platform.formatInviteDeepLink(code)
send({ type: 'invite-deep-link', url, inviteCode: code })
return true
}
if (t === 'format-discovery-baseline-link') {
const url = platform.formatDiscoveryExploreBaselineDeepLink()
send({ type: 'discovery-baseline-deep-link', url })
return true
}
if (t === 'format-guild-deep-link') {
const url = platform.formatGuildDeepLink({
guildId: msg.guildId,
channelId: msg.channelId
})
send({ type: 'guild-deep-link', url, guildId: msg.guildId, channelId: msg.channelId || null })
return true
}
if (t === 'format-settings-deep-link') {
const url = platform.formatSettingsDeepLink({
section: msg.section,
scope: msg.scope,
guildId: msg.guildId
})
send({
type: 'settings-deep-link',
url,
section: msg.section || null,
scope: msg.scope || 'user',
guildId: msg.guildId || null
})
return true
}
if (t === 'mark-notification-read') {
const row = await platform.markNotificationRead(msg.notificationId)
pushState()
if (row?.channelId) {
let dismissChannelId = row.channelId
const channels = (await platform.guild?.listChannels?.()) || []
const ch = channels.find((c) => c.id === row.channelId)
if (ch?.type === 'thread' && ch.parentId) dismissChannelId = ch.parentId
send({
type: 'mark-notification-read-cleared',
guildId: row.guildId || platform.guild?.guild?.id || null,
channelId: dismissChannelId,
notificationChannelId: row.channelId
})
}
return true
}
if (t === 'open-notification') {
if (msg.notificationId) {
await platform.markNotificationRead(msg.notificationId)
}
const result = await platform.openNotificationTarget({
guildId: msg.guildId,
channelId: msg.channelId,
messageId: msg.messageId
})
pushState()
const focusChannelId = result?.channelId || msg.channelId || platform.activeChannelId
if (focusChannelId) {
send({ type: 'composer-focus-channel', channelId: focusChannelId })
}
send({
type: 'open-notification-opened',
guildId: msg.guildId || platform.guild?.guild?.id || null,
channelId: msg.channelId || result.channelId || null,
messageId: msg.messageId || null
})
send({ type: 'open-notification-result', ...result })
return true
}
if (t === 'format-device-pair-deep-link') {
const url = platform.formatDevicePairDeepLink(msg.code || msg.pairCode)
send({ type: 'device-pair-deep-link', url, code: msg.code || msg.pairCode })
return true
}
if (t === 'set-presence') {
const custom = msg.customStatus !== undefined ? msg.customStatus : undefined
const activity = msg.activity !== undefined ? msg.activity : undefined
platform.setPresence(msg.status || USER_STATUS.ONLINE, custom, activity)
pushState()
return true
}
if (t === 'set-activity') {
platform.setActivity(msg.activity ?? null)
pushState()
return true
}
if (t === 'clear-activity') {
platform.setActivity(null)
pushState()
return true
}
if (t === 'set-activity-image') {
await platform.setActivityImageFromBytes({
data: fromBase64(msg.dataBase64),
filename: msg.filename,
mimeType: msg.mimeType,
slot: msg.slot || 'large'
})
pushState()
return true
}
if (t === 'request-activity-image') {
const preview = await platform.readActivityImagePreview(
msg.attachmentId,
msg.slot || 'large'
)
send({ type: 'activity-image', preview, slot: msg.slot || 'large' })
return true
}
if (t === 'edit-message') {
await platform.editMessage(msg.messageId, msg.content)
pushState()
return true
}
if (t === 'delete-message') {
await platform.deleteMessage(msg.messageId)
pushState()
return true
}
if (t === 'create-thread') {
const thread = await platform.createThread({ messageId: msg.messageId, name: msg.name })
pushState()
send({ type: 'composer-focus-channel', channelId: thread.id })
return true
}
if (t === 'create-forum-post') {
const result = await platform.createForumPost({
title: msg.title,
content: msg.content,
tags: msg.tags
})
pushState()
const threadId = result?.thread?.id
if (threadId) send({ type: 'composer-focus-channel', channelId: threadId })
return true
}
if (t === 'set-forum-channel-palette') {
await platform.setForumChannelPalette(msg.channelId, msg.tags)
pushState()
return true
}
if (t === 'update-forum-tag-filter') {
platform.setForumTagFilter(msg.tag)
pushState()
return true
}
if (t === 'set-forum-include-archived') {
platform.setForumIncludeArchived(msg.include)
pushState()
return true
}
if (t === 'set-threads-panel-archived') {
await platform.setThreadsPanelIncludeArchived(!!msg.includeArchived)
pushState()
return true
}
if (t === 'set-threads-panel-filter') {
await platform.setThreadsPanelFilter(msg.filter)
pushState()
return true
}
if (t === 'set-voice-roster-muted-only') {
await platform.setVoiceRosterMutedOnly(!!msg.mutedOnly)
pushState()
return true
}
if (t === 'set-voice-roster-deafened-only') {
await platform.setVoiceRosterDeafenedOnly(!!msg.deafenedOnly)
pushState()
return true
}
if (t === 'set-voice-roster-expanded') {
await platform.setVoiceRosterChannelExpanded(msg.channelId, !!msg.expanded)
pushState()
return true
}
if (t === 'set-voice-roster-combined-filter') {
await platform.setVoiceRosterCombinedFilter(!!msg.enabled)
pushState()
return true
}
if (t === 'set-threads-panel-parent-filter') {
platform.setThreadsPanelParentFilter(msg.parentChannelId || null)
pushState()
return true
}
if (t === 'set-threads-panel-sort') {
platform.setThreadsPanelSort(msg.panelSort || 'activity')
pushState()
return true
}
if (t === 'toggle-thread-pin') {
await platform.toggleThreadPinned(msg.threadId)
pushState()
return true
}
if (t === 'set-thread-notification-muted') {
await platform.setThreadNotificationMuted(msg.threadId, msg.muted !== false)
pushState()
return true
}
if (t === 'set-channel-notification-level') {
await platform.setChannelNotificationLevel(msg.channelId, msg.level || 'all')
pushState()
return true
}
if (t === 'set-channel-mute-duration') {
await platform.setChannelMuteDuration(msg.channelId, msg.durationMs ?? 0)
pushState()
return true
}
if (t === 'archive-thread') {
await platform.archiveThread(msg.threadId, msg.archived !== false)
pushState()
return true
}
if (t === 'create-category') {
await platform.createCategory({ name: msg.name })
pushState()
return true
}
if (t === 'create-channel') {
const channel = await platform.createChannel({
name: msg.name,
type: msg.channelType,
parentId: msg.parentId
})
await platform.selectChannel(channel.id)
send({ type: 'composer-focus-channel', channelId: channel.id })
pushState()
return true
}
if (t === 'reorder-channels') {
await platform.reorderChannelsInParent({
parentId: msg.parentId ?? null,
channelIds: msg.channelIds || []
})
pushState()
return true
}
if (t === 'move-channel-to-parent') {
await platform.moveChannelToParent({
channelId: msg.channelId,
parentId: msg.parentId ?? null
})
pushState()
return true
}
if (t === 'rename-channel') {
await platform.renameChannel({ channelId: msg.channelId, name: msg.name })
pushState()
return true
}
if (t === 'delete-channel') {
await platform.deleteChannel(msg.channelId)
pushState()
return true
}
if (t === 'set-channel-user-limit') {
await platform.setChannelUserLimit(msg.channelId, msg.userLimit)
pushState()
return true
}
if (t === 'set-channel-nsfw') {
await platform.setChannelNsfw(msg.channelId, msg.nsfw)
pushState()
return true
}
if (t === 'reorder-categories') {
await platform.reorderCategories({ categoryIds: msg.categoryIds || [] })
pushState()
return true
}
if (t === 'toggle-reaction') {
await platform.toggleReaction(msg.messageId, msg.emoji)
pushState()
return true
}
if (t === 'select-home') {
await platform.openHome()
pushState()
return true
}
if (t === 'send-friend-request') {
await platform.sendFriendRequest({
peerUserId: msg.peerUserId,
peerDisplayName: msg.peerDisplayName
})
pushState()
send({ type: 'show-toast', message: 'Friend request sent on the contacts mesh' })
return true
}
if (t === 'accept-friend-request') {
await platform.acceptFriendRequest(msg.peerUserId)
pushState()
send({ type: 'show-toast', message: 'Friend request accepted' })
return true
}
if (t === 'decline-friend-request') {
await platform.declineFriendRequest(msg.peerUserId)
pushState()
return true
}
if (t === 'remove-contact') {
await platform.removeContact(msg.peerUserId)
pushState()
return true
}
if (t === 'block-contact') {
await platform.blockContact(msg.peerUserId)
pushState()
send({ type: 'show-toast', message: 'User blocked — they cannot send you friend requests' })
return true
}
if (t === 'unblock-contact') {
await platform.unblockContact(msg.peerUserId)
pushState()
send({ type: 'show-toast', message: 'User unblocked' })
return true
}
if (t === 'request-attachment-preview') {
const preview = await platform.readAttachmentPreview(msg.attachmentId)
send({
type: 'attachment-preview',
preview,
fetchSource: preview?.fetchSource || null
})
return true
}
if (t === 'upsert-guild-emoji') {
await platform.upsertGuildEmoji({
name: msg.name,
glyph: msg.glyph,
description: msg.description,
attachmentId: msg.attachmentId,
packName: msg.packName,
image: normalizeEmojiImage(msg.image)
})
pushState()
return true
}
if (t === 'set-guild-emoji-roles') {
await platform.setGuildEmojiAllowedRoles({
name: msg.name,
allowedRoles: msg.allowedRoles
})
pushState()
return true
}
if (t === 'upsert-emoji-pack') {
await platform.upsertEmojiPack({
name: msg.name,
displayName: msg.displayName,
description: msg.description,
sortOrder: msg.sortOrder,
coverEmojiName: msg.coverEmojiName,
emojiNames: msg.emojiNames
})
pushState()
return true
}
if (t === 'remove-emoji-pack') {
await platform.removeEmojiPack(msg.name)
pushState()
return true
}
if (t === 'remove-guild-emoji') {
await platform.removeGuildEmoji(msg.name)
pushState()
return true
}
if (t === 'upsert-guild-sticker') {
await platform.upsertGuildSticker({
name: msg.name,
description: msg.description,
attachmentId: msg.attachmentId,
packName: msg.packName,
image: normalizeEmojiImage(msg.image)
})
pushState()
return true
}
if (t === 'upsert-sticker-pack') {
await platform.upsertStickerPack({
name: msg.name,
displayName: msg.displayName,
description: msg.description,
sortOrder: msg.sortOrder,
coverStickerName: msg.coverStickerName,
stickerNames: msg.stickerNames
})
pushState()
return true
}
if (t === 'remove-sticker-pack') {
await platform.removeStickerPack(msg.name)
pushState()
return true
}
if (t === 'remove-guild-sticker') {
await platform.removeGuildSticker(msg.name)
pushState()
return true
}
if (t === 'upsert-guild-sound') {
await platform.upsertGuildSound({
name: msg.name,
description: msg.description,
attachmentId: msg.attachmentId,
volume: msg.volume,
audio: normalizeEmojiImage(msg.audio)
})
pushState()
return true
}
if (t === 'remove-guild-sound') {
await platform.removeGuildSound(msg.name)
pushState()
return true
}
if (t === 'play-soundboard') {
await platform.playSoundboardSound(msg.name)
pushState()
return true
}
if (t === 'request-sound-preview') {
const preview = await platform.readSoundPreview(msg.name || msg.attachmentId)
send({ type: 'sound-preview', preview, soundName: msg.name || null })
return true
}
if (t === 'request-emoji-image') {
const attachmentId = msg.attachmentId || null
const emojiName = msg.name || null
try {
const preview = await platform.readEmojiImage(emojiName || attachmentId)
if (preview) {
send({ type: 'emoji-image', preview, emojiName })
} else {
flowLog.debug('emoji image miss', { attachmentId, emojiName })
send({
type: 'log',
record: {
ts: new Date().toISOString(),
level: 'debug',
scope: 'emoji',
msg: 'image miss',
meta: { attachmentId, emojiName }
}
})
send({ type: 'emoji-image-miss', attachmentId, emojiName })
}
} catch (err) {
flowLog.warn('emoji image request failed', { attachmentId, emojiName, err })
send({
type: 'emoji-image-miss',
attachmentId,
emojiName,
message: err?.message || String(err)
})
}
return true
}
if (t === 'stage-guild-emoji-image') {
const att = await platform.stageGuildEmojiImage({
data: fromBase64(msg.dataBase64),
filename: msg.filename,
mimeType: msg.mimeType
})
send({ type: 'emoji-image-staged', attachment: att })
pushState()
return true
}
if (t === 'update-search') {
platform.setSearch({
query: msg.query,
scope: msg.scope,
includeMesh: msg.includeMesh
})
pushState()
return true
}
if (t === 'remove-search-recent') {
await platform.removeSearchRecent(msg.query)
pushState()
return true
}
if (t === 'refresh-search') {
await platform.refreshSearch()
pushState()
return true
}
if (t === 'open-dm') {
await platform.openDM({
peerUserId: msg.peerUserId,
peerDisplayName: msg.peerDisplayName || msg.peerUserId?.slice(0, 8)
})
pushState()
if (platform.activeChannelId) {
send({ type: 'composer-focus-channel', channelId: platform.activeChannelId })
}
return true
}
if (t === 'create-group-dm') {
await platform.createGroupDM({
peerUserIds: msg.peerUserIds || [],
name: msg.name
})
pushState()
if (platform.activeChannelId) {
send({ type: 'composer-focus-channel', channelId: platform.activeChannelId })
}
return true
}
if (t === 'open-group-dm') {
await platform.openGroupDM({ channelId: msg.channelId })
pushState()
if (platform.activeChannelId) {
send({ type: 'composer-focus-channel', channelId: platform.activeChannelId })
}
return true
}
if (t === 'typing-start' || t === 'typing') {
log.debug('typing.ipc', {
channelId: platform.activeChannelId,
mode: platform.mode
})
platform.signalTyping()
return true
}
if (t === 'update-profile') {
await platform.updateProfile({ displayName: msg.displayName })
pushState()
return true
}
if (t === 'update-profile-cosmetics') {
await platform.updateProfileCosmetics({
bannerColor: msg.bannerColor,
avatarRing: msg.avatarRing,
accentColor: msg.accentColor,
profileBio: msg.profileBio
})
pushState()
return true
}
if (t === 'set-profile-banner-image') {
await platform.setProfileBannerImage({
data: fromBase64(msg.dataBase64),
filename: msg.filename,
mimeType: msg.mimeType
})
pushState()
return true
}
if (t === 'request-profile-banner-image') {
const preview = await platform.readProfileBannerPreview(msg.attachmentId)
send({ type: 'profile-banner-image', preview })
return true
}
if (t === 'request-peer-profile-banner-image') {
const preview = await platform.readPeerProfileBannerPreview(msg.userId)
send({ type: 'profile-banner-image', preview })
return true
}
if (t === 'pin-message') {
await platform.pinMessage(msg.messageId)
pushState()
return true
}
if (t === 'unpin-message') {
await platform.unpinMessage(msg.messageId)
pushState()
return true
}
if (t === 'ban-member') {
await platform.banMember({ userId: msg.userId, reason: msg.reason })
pushState()
return true
}
if (t === 'kick-member') {
await platform.kickMember({ userId: msg.userId, reason: msg.reason })
pushState()
return true
}
if (t === 'timeout-member') {
await platform.timeoutMember({
userId: msg.userId,
durationMinutes: msg.durationMinutes,
reason: msg.reason
})
pushState()
return true
}
if (t === 'set-channel-topic') {
await platform.setChannelTopic(msg.channelId, msg.topic)
pushState()
return true
}
if (t === 'set-channel-description') {
await platform.setChannelDescription(msg.channelId, msg.description)
pushState()
return true
}
if (t === 'upsert-channel-overwrite') {
await platform.upsertChannelPermissionOverwrite(msg.channelId, {
targetType: msg.targetType,
targetId: msg.targetId,
allowView: msg.allowView,
denyView: msg.denyView
})
pushState()
return true
}
if (t === 'delete-channel-overwrite') {
await platform.deleteChannelPermissionOverwrite(
msg.channelId,
msg.targetType,
msg.targetId
)
pushState()
return true
}
if (t === 'set-slowmode') {
await platform.setChannelSlowmode(msg.channelId, msg.slowmodeSeconds)
pushState()
return true
}
if (t === 'set-member-role') {
await platform.setMemberRole(msg.userId, msg.role)
pushState()
return true
}
if (t === 'create-guild-custom-role') {
await platform.createGuildCustomRole({
name: msg.name,
color: msg.color,
permissions: msg.permissions
})
pushState()
return true
}
if (t === 'update-guild-custom-role') {
await platform.updateGuildCustomRole(msg.roleId, {
name: msg.name,
color: msg.color,
permissions: msg.permissions,
hoist: msg.hoist
})
pushState()
return true
}
if (t === 'delete-guild-custom-role') {
await platform.deleteGuildCustomRole(msg.roleId)
pushState()
return true
}
if (t === 'reorder-guild-custom-roles') {
await platform.reorderGuildCustomRoles(msg.roleIds || [])
pushState()
return true
}
if (t === 'set-member-custom-roles') {
await platform.setMemberCustomRoles(msg.userId, msg.roleIds || [])
pushState()
return true
}
if (t === 'update-notification-prefs') {
await platform.updateNotificationPrefs(msg.prefs || {})
pushState()
return true
}
if (t === 'update-user-prefs') {
await platform.updateUserPrefs(msg.prefs || {})
pushState()
return true
}
if (t === 'create-channel-webhook') {
const out = await platform.createChannelWebhook({
channelId: msg.channelId,
name: msg.name,
avatarUrl: msg.avatarUrl
})
pushState()
send({ type: 'channel-webhook-created', ...out })
return true
}
if (t === 'delete-channel-webhook') {
await platform.deleteChannelWebhook(msg.webhookId)
pushState()
return true
}
if (t === 'execute-channel-webhook') {
const msgRow = await platform.executeChannelWebhook({
token: msg.token,
content: msg.content,
username: msg.username
})
pushState()
send({ type: 'channel-webhook-executed', messageId: msgRow?.id })
return true
}
if (t === 'set-audit-log-export-filter') {
const filter = await platform.setAuditLogExportFilter(msg.filter || 'all')
pushState()
send({ type: 'audit-log-export-filter', filter })
return true
}
if (t === 'create-server-folder') {
const folder = await platform.createServerFolder({ name: msg.name, color: msg.color })
if (msg.initialGuildId && folder?.id) {
await platform.assignGuildToFolder(msg.initialGuildId, folder.id)
}
if (msg.mergeGuildId && folder?.id) {
await platform.assignGuildToFolder(msg.mergeGuildId, folder.id)
}
pushState()
send({
type: 'server-folder-created',
folder,
initialGuildId: msg.initialGuildId || null
})
return true
}
if (t === 'delete-server-folder') {
await platform.deleteServerFolder(msg.folderId)
pushState()
return true
}
if (t === 'assign-guild-to-folder') {
await platform.assignGuildToFolder(msg.guildId, msg.folderId || null)
pushState()
return true
}
if (t === 'set-server-folder-collapsed') {
await platform.setServerFolderCollapsed(msg.folderId, !!msg.collapsed)
pushState()
return true
}
if (t === 'set-server-folder-color') {
await platform.setServerFolderColor(msg.folderId, msg.color)
pushState()
return true
}
if (t === 'rename-server-folder') {
await platform.renameServerFolder(msg.folderId, msg.name)
pushState()
return true
}
if (t === 'reorder-server-folders') {
await platform.reorderServerFolders(msg.folderIds || [])
pushState()
return true
}
if (t === 'set-compose-draft') {
flowLog.trace('compose draft ipc', {
channelId: msg.channelId,
len: String(msg.text ?? '').length
})
await platform.setComposeDraft(msg.channelId, msg.text ?? '')
return true
}
if (t === 'export-diagnostics-jsonl') {
const out = platform.writeDiagnosticsJsonl(msg.lines || [], msg.meta || {})
send({ type: 'diagnostics-jsonl-exported', ...out })
return true
}
if (t === 'mark-all-notifications-read') {
await platform.markAllNotificationsRead()
pushState()
send({ type: 'mark-all-notifications-read-cleared' })
if (platform.activeChannelId) {
send({ type: 'composer-focus-channel', channelId: platform.activeChannelId })
}
return true
}
if (t === 'refresh-notifications') {
pushState()
return true
}
if (t === 'create-device-pair-code') {
const invite = await platform.createDevicePairCode({
deviceLabel: msg.deviceLabel,
ttlMs: msg.ttlMs
})
const pairUrl = platform.formatDevicePairDeepLink(invite?.code)
let qrSvg = null
if (pairUrl) {
try {
const { renderQrSvg } = require('pearcord-qr')
qrSvg = renderQrSvg(pairUrl, { size: 180 })
} catch {
qrSvg = null
}
}
send({ type: 'device-pair-code', invite, pairUrl, qrSvg })
pushState()
return true
}
if (t === 'link-device-pair-code') {
await platform.linkDevicePairCode(msg.code, { deviceLabel: msg.deviceLabel })
pushState()
return true
}
if (t === 'set-step-up-pin') {
await platform.setStepUpPin(msg.pin)
pushState()
return true
}
if (t === 'verify-step-up-pin') {
await platform.verifyStepUpPin(msg.pin)
pushState()
return true
}
if (t === 'clear-step-up-pin') {
await platform.clearStepUpPin()
pushState()
return true
}
if (t === 'refresh') {
pushState()
return true
}
log.warn('unhandled ipc', { type: t })
return false
}
module.exports = {
dispatchUiMessage,
fromBase64,
normalizeEmojiImage
}