'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} 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 }