'use strict' const { fs, path, b4a, EventEmitter, swarmScope, dmScope, appEventsScope, automationScope, deliveryScope, integrationScope, botAuthScope, cryptoScope, threadsScope, searchScope, channelMetaScope, localScope, foldersScope, enrichmentScope, sharedScope, permissionsScope, hyperdbScope, createLogger, LocalDatabase, PearcordIdentity, resolveStoragePath, writeOnboardedMarker, PearcordGuild, GuildReplicator, GuildSidecarStore, PearcordMessage, PearcordPresence, PearcordInvite, DmVideoFramePublisher, PearcordAttachments, PearcordVoiceNotes, canSendVoiceNote, PearcordModeration, PearcordVoice, VoiceMediaHub, PearcordDmVoice, VoiceCaptureFeed, PearcordSlashRegistry, parseSlashInvocation, PearcordEmojiRegistry, buildEmojiSlotSnapshot, BotEventFanout, BOT_EVENTS, exportBodyFromSlice, PearcordIntegrationWorker, INTENTS, BotRateLimits, PearcordStepUp, PearcordForum, PearcordStage, STAGE_ROLE, isStageChannel, PearcordAutomod, PearcordWebhooks, ScreenShareHub, broadcastScreenMedia, DisplayCaptureFeed, PearcordAnnouncements, formatCrosspostBody, PearcordBoosts, MAX_BOOSTS, MAX_SLOTS_PER_USER, PearcordStickerRegistry, PearcordSoundboardRegistry, decodeSoundToPcm, ALLOWED_MIMES, PearcordAnnouncementHub, formatRemoteHubBody, buildHubMirrorCard, formatHubMirrorFallback, PearcordContacts, PearcordNotifications, PearcordSettings, UserPrefsMirrorStore, DmCallHistoryStore, PearcordProfileCosmetics, enrichProfileCosmeticGossip, enrichGuildUpdateGossip, pickBannerHash, PearcordDeviceSync, PearcordDiscovery, collectDiscoveryTags, getListingTtlMs, getGuildDrive, isHexDriveKey, fetchFromProvider, PearcordEmbed, forumPostHaystack, buildActivityFeed, enrichActivityWithAttachments, IPC_CONTRACT_VERSION, Hyperswarm, id, now, guildTopic } = require('./platform-class-imports') class PearcordPlatform extends EventEmitter { constructor (opts = {}) { super() this.storagePath = opts.storagePath || resolveStoragePath() this._hyperswarmDht = opts.hyperswarmDht || null this.log = createLogger('platform') this.dbPath = path.join(this.storagePath, 'db') this.db = new LocalDatabase(this.dbPath) this.identity = new PearcordIdentity({ db: this.db, dbPath: this.dbPath, storagePath: this.storagePath }) this.guild = null this.messages = null this.presence = null this.invite = null this.dm = null this.dmVoice = null this._dmVoiceMedia = null this._dmScreenShare = null this._dmVideoPublisher = null this._dmVideoTilesByUser = new Map() this._dmScreenFrameBroadcastBound = false this._prefsMirrorStore = null this._callHistoryStore = null this._dmCallHistoryLoaded = false this.attachments = null this.moderation = null this.voice = null this.voiceMedia = null this.voiceCapture = null this.screenShare = null this.displayCapture = null this.announcements = null this.announcementHub = null this.boosts = null this.slashCommands = null this.emojiRegistry = null this.stickerRegistry = null this.soundboardRegistry = null this.guildRoles = null this._activeChannelOpenLastReadAt = 0 this._dmSessionRekeyNotice = null this._dmCallHistory = [] this._dmComposerPrefill = null this.channelPermissions = null this._recentSoundPlays = [] this.botEvents = null this.appEvents = null this.automationExport = null this.webhooks = null this.deliveryReceipts = null this.hookDispatcher = null this.hookFailureAlerts = null this.workerReleaseHints = null this._auditScheduleTimer = null this._dmScheduledMessageTimer = null this._auditArchiveFetchWaiters = new Map() this._automationDigestSnapshotFetchWaiters = new Map() this.appVersion = opts.appVersion || process.env.PEARCORD_APP_VERSION || '0.0.0' this.discovery = null this.contacts = null this.botLimits = null this.notifications = null this.userSettings = null this.profileCosmetics = null this._peerCosmetics = new Map() this.deviceSync = null this.embeds = null this._discoverySwarm = null this._contactsSwarm = null this._settingsSwarm = null this._settingsMeshWired = false this._settingsHealthChannels = new Map() this._guildSidecarStores = new Map() this._settingsMeshHealthByDevice = new Map() this._settingsMeshHealthLastAt = 0 this._settingsMeshHealthGossipTimer = null this._settingsPresenceGossipTimer = null this._devicePairSwarm = null this._discoveryWired = false this._archivePeerHealthRefreshedAt = null this._lastComplianceSnapshot = null this._lastArchivePeerExport = null this._archiveHealthRefreshTimer = null this.mode = 'home' this.activeChannelId = null this.onboarded = false this.guilds = [] this._searchQuery = '' this._searchScope = 'channel' this._searchIncludeMesh = true this._searchUnchanged = false this._lastSearchMeshMeta = null this._guildSearchIndexes = new Map() this._searchIndexStore = null this._searchIndexPersistTimers = new Map() this._searchMeshWaiters = new Map() this._threadsPanelFilter = 'active' this._threadsPanelParentFilter = null this._threadsPanelSort = 'activity' this._discoveryMeshRefreshAt = 0 this._discoverySwarmRefreshAt = 0 this._discoveryMeshHealTimer = null this._discoveryListingRefreshByGuild = new Map() this._federationHubIngestStats = null this._discoveryJoinSyncPending = false this._discoveryPostJoinRefreshAt = 0 this._discoveryDiffHint = null this._discoveryDiffUntil = 0 this._discoveryListingIds = new Set() this._lastDiscoveryMeshIngest = null this._voiceSpeakingMesh = {} this._voiceMuteMesh = {} this._discoveryExploreBaseline = null this._discoveryExploreBaselineAt = null this._voiceRosterMutedOnly = false this._voiceRosterDeafenedOnly = false this._voiceRosterExpandedChannelIds = new Set() this._onboardingExplorePending = false this._lastVoiceSpeakingGossipKey = '' this._lastVoiceSpeakingGossipAt = 0 this._readOnly = opts.readOnly === true this._companionMode = opts.companionMode === true || opts.readOnly === true /** @type {Set} */ this._attachmentDownloadCancel = new Set() this._discoveryFilter = { query: '', sort: 'newest', minMembers: 0, tag: '' } this._guildTyping = new Map() this._typingTimers = new Set() this._guildPresenceSummary = new Map() this._guildPresencePrefsTimer = null this._searchFetchedAt = 0 this._searchResolveCacheKey = '' this._cachedSearchResults = null this._cachedPublicListings = null this._cachedPublicListingsAt = 0 this._cachedDiscoveryStats = null this._cachedDiscoveryStatsAt = 0 this._discoveryViewCacheMs = Number(process.env.PEARCORD_DISCOVERY_VIEW_CACHE_MS) || 5000 this._searchCacheGuildId = '' this._lastSearchFromCache = false this._searchMeshStaleMs = 90000 this._sessionReady = false this._sessionError = null this._sessionStartupInFlight = null this._guildLoading = false this._guildLoadingGuildId = null this._guildLoadGeneration = 0 this._guildMeshJoinCoalesce = null this._guildDeleteInProgress = null this._guildOpenNoViewableChannels = false this._emojiAttachmentMissUntil = new Map() this._stickerAttachmentMissUntil = new Map() this._guildSyncHealthByGuild = new Map() this._guildReplicators = new Map() this._guildSyncAcks = new Map() this._guildSyncPushHalted = new Set() this._guildGossipOutbox = new swarmScope.GossipOutbox({ maxEntries: 256 }) this._guildTopicPool = new swarmScope.GuildTopicPool() this._guildSyncRequestBackoffGen = 0 this._guildSyncPushCoalesceTimer = null this._guildSyncRequestDebounce = new Map() this._presenceVectorByGuild = new Map() this._reactionTombstones = new Map() this._sparseOpenTimestamps = [] this._sparseAckCursorByGuild = new Map() this._guildSyncBandwidthWindow = { at: 0, used: 0 } this._guildSyncBandwidthDeferTimer = null this._guildSyncBurstRttMs = 0 this._contactsPresenceDebounce = null this._lastGuildSyncIngestStart = 0 this._guildSyncBandwidthPeerIndex = 0 this._reactionListCacheKey = '' this._reactionListCache = null this._reactionListCacheGen = 0 this._reactionListPending = null this._reactionListMetrics = { cacheHit: 0, pendingHit: 0, miss: 0 } this._pinListCacheKey = '' this._pinListCache = null this._pinListCacheGen = 0 this._pinListPending = null this._pinListMetrics = { cacheHit: 0, pendingHit: 0, miss: 0 } this._viewBuildStats = { total: 0, light: 0, pinListSpanSkipped: 0, reactionListSpanSkipped: 0 } this._discoveryListPending = null this._discoveryListMetrics = { cacheHit: 0, pendingHit: 0, miss: 0 } this._discoveryListFrameDedupe = localScope.createDiscoveryListFrameDedupe() this._pollDbReadBudget = localScope.createPollDbReadBudget() this._topicMergeCounters = channelMetaScope.createTopicMergeCounters() this._lastUnreadConsistencyAt = 0 this._guildSyncSeq = 0 this._markReadFanout = new localScope.MarkReadFanoutQueue({ log: this.log, onFlush: (channelId, guildId) => this.markChannelRead(channelId, guildId) }) this._lastReplicationCompact = null this._memberPageBurstTimestamps = [] this._guildMeshBoundedByGuild = new Map() this._guildMeshPeerRetryMetaByGuild = new Map() this._guildMeshPeerSeenByGuild = new Map() this._guildMeshStalePeerSweepTimer = null this._guildMeshStalePeerEvictedTotalByGuild = new Map() this._lastPartitionHeal = null this._lastPartitionHealByGuild = new Map() this._partitionHealInProgressByGuild = new Set() this._partitionHealCooldownUntilByGuild = new Map() this._channelReplicationDiagByGuild = new Map() this._meshZeroPeerSinceByGuild = new Map() this._meshAutoHealCheckTimer = null this._gossipOutboxReplayByGuild = new Map() this._lastGuildSyncChannelCoreKeys = [] this._forumIndexByGuild = new Map() this._guildOpenDurationSamples = [] this._guildOpenFallbackThrottledUntil = 0 this._embedPrefetchCoalesce = new Map() this._embedResolveLatencyByMessage = new Map() /** Suppress composer-driven embed prefetch during IME composition (Phase 666). */ this._composerImeComposing = false this._composerEmbedPreview = null this._embedGossipUrlHashes = new Set() this._embedResolveRateByGuild = new Map() this._dmEmbedGossipBytesByConversation = new Map() /** Dedupe reaction gossip fanout by message+emoji+user (Phase 669). */ this._reactionGossipKeys = new Set() /** Per-channel reaction toggle rate limit (Phase 669). */ this._reactionToggleRateByChannel = new Map() /** Dedupe sticker gossip fanout (Phase 670). */ this._stickerGossipKeys = new Set() /** Per-channel sticker send rate limit (Phase 670). */ this._stickerSendRateByChannel = new Map() /** Per-channel GIF attachment send rate limit (Phase 670). */ this._gifSendRateByChannel = new Map() /** Dedupe pin gossip fanout (Phase 671). */ this._pinGossipKeys = new Set() /** Per-channel pin toggle rate limit (Phase 671). */ this._pinToggleRateByChannel = new Map() /** Per-user thread create rate limit (Phase 672). */ this._threadCreateRateByUser = new Map() /** Dedupe thread metadata gossip ingest (Phase 672). */ this._threadMetaGossipKeys = new Set() /** Dedupe presence gossip fanout (Phase 673). */ this._presenceGossipKeys = new Set() /** Multi-select forum tag filters (Phase 672). */ this._forumTagFilters = [] } async ready () { const span = this.log.time('platform.ready') try { await this.identity.ready() this.channelDescriptions = new channelMetaScope.ChannelDescriptionStore({ storagePath: this.storagePath }) await this.channelDescriptions.ready() const snap = this.identity.snapshot() this.onboarded = !!snap.user if (this.onboarded) writeOnboardedMarker(this.storagePath) if (snap.user) { this.presence = new PearcordPresence({ userId: snap.user.id }) this.invite = new PearcordInvite({ db: this.db, creatorId: snap.user.id }) await this.invite.ready() this.attachments = new PearcordAttachments({ storagePath: this.storagePath }) await this.attachments.ready() this.voiceNotes = new PearcordVoiceNotes({ storagePath: this.storagePath }) await this.voiceNotes.ready() this.moderation = new PearcordModeration({ storagePath: this.storagePath }) await this.moderation.ready() this.discovery = new PearcordDiscovery({ storagePath: this.storagePath }) await this.discovery.ready() this._wireDiscoveryEvents() this.embeds = new PearcordEmbed({ storagePath: this.storagePath }) await this.embeds.ready() this.botLimits = new BotRateLimits({ storagePath: this.storagePath }) await this.botLimits.ready() if (snap.user) { this.notifications = new PearcordNotifications({ storagePath: this.storagePath, userId: snap.user.id }) await this.notifications.ready() this.notifications.on('notification', (n) => this.emit('notification', n)) this.notifications.on('prefs', () => this.emit('notification-prefs')) this.userSettings = new PearcordSettings({ storagePath: this.storagePath, userId: snap.user.id }) this.userSettings._prefsMirror = await this._ensurePrefsMirrorStore() await this.userSettings.ready() const prefsRecovery = await this.userSettings.ensurePrefsRecoveredOnStartup() if (prefsRecovery?.recovered) { this.log.info('userPrefs startup recovery', { spanKind: 'userPrefs.startup-recovery', userId: snap.user.id, source: prefsRecovery.source, reason: prefsRecovery.reason || null, backupUsed: !!prefsRecovery.backupUsed, issueCount: (prefsRecovery.issues || []).length }) } const sessionPrefs = await this.userSettings.getPrefs() this._ingestGuildPresenceCacheFromPrefs(sessionPrefs) this._ingestDiscoveryExploreBaselineFromPrefs(sessionPrefs) this._applyGlobalPresenceMeshFromPrefs(sessionPrefs) this.userSettings.on('prefs', (prefs) => { this._ingestGuildPresenceCacheFromPrefs(prefs) this._ingestDiscoveryExploreBaselineFromPrefs(prefs) this._applyGlobalPresenceMeshFromPrefs(prefs) this.emit('user-prefs') }) this.profileCosmetics = new PearcordProfileCosmetics({ storagePath: this.storagePath, userId: snap.user.id }) await this.profileCosmetics.ready() this.profileCosmetics.on('cosmetics', () => this.emit('profile-cosmetics')) this.profileCosmetics.on('peer-cosmetics', (p) => this._ingestPeerCosmetics(p)) } this.guilds = await this.listGuilds() await this._applyPendingGuildDeletes() this._startDmScheduledMessageTimer() this.log.info('platform ready', { guilds: this.guilds.length, user: snap.user?.id }) void this._finishSessionStartup() } span.end({ onboarded: this.onboarded }) return this } catch (err) { span.fail(err) throw err } } async _joinSessionStartupMeshes () { const meshMs = Number(process.env.PEARCORD_SESSION_MESH_MS) || 12000 await this._startupStepTimeout( Promise.all([ this._joinDiscoveryMesh(), this._joinContactsMesh(), this._joinSettingsMesh() ]), meshMs, 'session-startup-meshes' ) } _markSessionReady () { if (this._sessionReady) return this._sessionReady = true this.log.info('session ready', { sessionError: this._sessionError || null }) this.emit('session-ready') } /** Guild mesh + discovery/contacts — mesh joins are capped; UI unblocks on sessionReady. */ async _finishSessionStartup () { if (this._sessionStartupInFlight) return this._sessionStartupInFlight this._sessionStartupInFlight = this._finishSessionStartupInner().finally(() => { this._sessionStartupInFlight = null }) return this._sessionStartupInFlight } async _finishSessionStartupInner () { const span = this.log.time('session.startup') const capMs = Number(process.env.PEARCORD_SESSION_STARTUP_MS) || 28000 let capped = false const capTimer = setTimeout(() => { if (this._sessionReady) return capped = true this._sessionError = this._sessionError || `Session startup timed out after ${capMs}ms — continuing offline` this.log.warn('session.startup capped', { capMs }) this._markSessionReady() }, capMs) try { if (this.guilds.length && !this.guild && this.mode !== 'dm') { let startGuildId = this.guilds[0].id if (this._companionMode && this.userSettings) { const prefs = await this.userSettings.getPrefs() const saved = prefs.companionLastGuildId if (saved && this.guilds.some((g) => g.id === saved)) startGuildId = saved } this.log.info('session loading initial guild', { guildId: startGuildId }) const guildLoadMs = Number(process.env.PEARCORD_GUILD_LOAD_MS) || 15000 let guildLoadTimedOut = false await Promise.race([ this.loadGuild(startGuildId), new Promise((resolve) => { setTimeout(() => { guildLoadTimedOut = true resolve() }, guildLoadMs) }) ]) if (guildLoadTimedOut && this._guildLoading) { this._guildLoadGeneration++ this._abortGuildMeshJoinCoalesce('session-startup-guild-timeout') this._guildLoading = false this._guildLoadingGuildId = null this.emit('guild-loading') const msg = `Guild load timed out after ${guildLoadMs}ms` this._sessionError = this._sessionError || msg this.log.warn('session initial guild load timed out', { guildId: startGuildId, guildLoadMs }) } } void this._joinSessionStartupMeshes().catch((err) => { this.log.warn('session startup meshes background failed', { err: err?.message || String(err) }) }) await this._applyPendingGuildDeletes() if (this.discovery) { setTimeout(() => { this.discovery.syncGuildDeletesToMesh().catch(() => {}) }, 1200) this._startDiscoveryMeshHealLoop() } const dmMs = Number(process.env.PEARCORD_SESSION_DM_MS) || 10000 await this._startupStepTimeout( this._ensureDmBackgroundListener(), dmMs, 'session-dm-background' ) const deviceMs = Number(process.env.PEARCORD_SESSION_DEVICE_MS) || 8000 await this._startupStepTimeout(this._initDeviceSync(), deviceMs, 'session-device-sync') const activeGid = this.guild?.guild?.id const startupGuildMeshBounded = activeGid ? this._isGuildMeshBounded(activeGid) : false span.end({ logLevel: startupGuildMeshBounded ? 'info' : undefined, guildMeshBounded: startupGuildMeshBounded, guildCount: this.guilds?.length || 0, guildId: this.guild?.guild?.id || null, activeChannelId: this.activeChannelId || null, spanKind: 'session.startup', capped }) } catch (err) { this._sessionError = err?.message || String(err) this.log.error('session.startup error', { err: err?.message || String(err) }) this.log.error('session startup failed', { err }) this.emit('session-error', err) span.fail(err) } finally { clearTimeout(capTimer) this._markSessionReady() } } async _initDeviceSync () { if (!this.onboarded || !this.identity?.user) return if (this.deviceSync) return const snap = this.identity.snapshot() this.deviceSync = new PearcordDeviceSync({ storagePath: this.storagePath, userId: snap.user.id, username: snap.user.username, displayName: snap.user.displayName, getIdentityBundle: async () => { const b = this.identity.getIdentityBundle() if (!b) return null const devices = await this.deviceSync.listDevices() return { ...b, devices } } }) await this.deviceSync.ready() const devices = await this.deviceSync.listDevices() if (!devices.length) { await this.deviceSync.registerDevice({ deviceId: this.identity.deviceId, label: 'Primary device', isPrimary: true }) } } async _bootstrapAfterUser (user, { waitSessionStartup = false } = {}) { this.onboarded = true writeOnboardedMarker(this.storagePath) this.presence = new PearcordPresence({ userId: user.id }) this.invite = new PearcordInvite({ db: this.db, creatorId: user.id }) this.attachments = new PearcordAttachments({ storagePath: this.storagePath }) this.voiceNotes = new PearcordVoiceNotes({ storagePath: this.storagePath }) this.moderation = new PearcordModeration({ storagePath: this.storagePath }) this.discovery = new PearcordDiscovery({ storagePath: this.storagePath }) this.embeds = new PearcordEmbed({ storagePath: this.storagePath }) this.botLimits = new BotRateLimits({ storagePath: this.storagePath }) this.notifications = new PearcordNotifications({ storagePath: this.storagePath, userId: user.id }) this.userSettings = new PearcordSettings({ storagePath: this.storagePath, userId: user.id, prefsMirror: null }) const prefsMirror = await this._ensurePrefsMirrorStore() this.userSettings._prefsMirror = prefsMirror this.profileCosmetics = new PearcordProfileCosmetics({ storagePath: this.storagePath, userId: user.id }) this.stepUp = new PearcordStepUp({ storagePath: this.storagePath, userId: user.id }) this.forum = new PearcordForum({ storagePath: this.storagePath }) this.stage = new PearcordStage({ storagePath: this.storagePath }) this.automod = new PearcordAutomod({ storagePath: this.storagePath }) this.announcements = new PearcordAnnouncements({ storagePath: this.storagePath }) this.announcementHub = new PearcordAnnouncementHub({ storagePath: this.storagePath }) this.boosts = new PearcordBoosts({ storagePath: this.storagePath }) await Promise.all([ this.invite.ready(), this.attachments.ready(), this.voiceNotes.ready(), this.moderation.ready(), this.discovery.ready(), this.embeds.ready(), this.botLimits.ready(), this.notifications.ready(), this.userSettings.ready(), this.profileCosmetics.ready(), this.stepUp.ready(), this.forum.ready(), this.stage.ready(), this.automod.ready(), this.announcements.ready(), this.announcementHub.ready(), this.boosts.ready() ]) this._wireDiscoveryEvents() this.notifications.on('notification', (n) => this.emit('notification', n)) this.notifications.on('prefs', () => this.emit('notification-prefs')) const prefsRecovery = await this.userSettings.ensurePrefsRecoveredOnStartup() if (prefsRecovery?.recovered) { this.log.info('userPrefs startup recovery', { spanKind: 'userPrefs.startup-recovery', userId: user.id, source: prefsRecovery.source, reason: prefsRecovery.reason || null, backupUsed: !!prefsRecovery.backupUsed, issueCount: (prefsRecovery.issues || []).length }) } const bootPrefs = await this.userSettings.getPrefs() this._ingestGuildPresenceCacheFromPrefs(bootPrefs) this._ingestDiscoveryExploreBaselineFromPrefs(bootPrefs) this._applyGlobalPresenceMeshFromPrefs(bootPrefs) this.userSettings.on('prefs', (prefs) => { this._ingestGuildPresenceCacheFromPrefs(prefs) this._ingestDiscoveryExploreBaselineFromPrefs(prefs) this._applyGlobalPresenceMeshFromPrefs(prefs) this.emit('user-prefs') }) this.profileCosmetics.on('cosmetics', () => this.emit('profile-cosmetics')) this.profileCosmetics.on('peer-cosmetics', (p) => this._ingestPeerCosmetics(p)) this._forumTagFilter = null this._forumTagFilters = [] this._forumIncludeArchived = false this._threadsPanelIncludeArchived = false this._threadsPanelFilter = 'active' this._threadsPanelParentFilter = null this._threadsPanelSort = 'activity' this._discoveryMeshRefreshAt = 0 this._discoverySwarmRefreshAt = 0 this._discoveryMeshHealTimer = null this._discoveryListingRefreshByGuild = new Map() this._federationHubIngestStats = null this._discoveryJoinSyncPending = false this._discoveryPostJoinRefreshAt = 0 this._discoveryDiffHint = null this._discoveryDiffUntil = 0 this._discoveryListingIds = new Set() this._voiceSpeakingMesh = {} this._voiceMuteMesh = {} this._discoveryExploreBaseline = null this._discoveryExploreBaselineAt = null this._voiceRosterMutedOnly = false this._voiceRosterDeafenedOnly = false this._onboardingExplorePending = false this._lastVoiceSpeakingGossipKey = '' this._lastVoiceSpeakingGossipAt = 0 this.guilds = await this.listGuilds() const startup = this._finishSessionStartup() if (waitSessionStartup) await startup else void startup this.emit('user', user) return user } async _leaveMeshes () { this._abortGuildMeshJoinCoalesce('leave-meshes') this._stopGuildMeshStalePeerSweep() this._stopAuditExportScheduleTimer() this._stopDmScheduledMessageTimer() await this._leaveVoice({ gossip: true }) if (this.guild) await this.guild.leaveMesh().catch(() => {}) if (this.dm && this.dm !== this._dmBg) { await this.dm.leaveMesh().catch(() => {}) } } async _leaveVoice ({ gossip = false } = {}) { if (!this.voice) return null const guild = this.guild?.guild const userId = this.identity?.user?.id let leavingChannelId = this.voice.myChannelId if (!leavingChannelId && userId && guild) { const rows = await this.voice.listByGuild() const mine = rows.find((r) => r.userId === userId) if (mine) { leavingChannelId = mine.channelId this.voice.myChannelId = mine.channelId } } if (!leavingChannelId) return null if (guild && this.stage && userId) { const ch = await this.db.get(sharedScope.COLLECTIONS.CHANNELS, { guildId: guild.id, id: leavingChannelId }) if (isStageChannel(ch)) { await this.stage.clearUser(guild.id, leavingChannelId, userId) if (gossip && this.guild) { this.guild.gossipStageSpeaker({ guildId: guild.id, channelId: leavingChannelId, userId, role: null, cleared: true }) } } } const payload = await this.voice.leave() this.displayCapture?.reset() if (this.screenShare?.active) { const screenStop = await this.screenShare.stopShare() if (gossip && screenStop && this.guild) { this.guild.gossipScreenShare(screenStop) this.guild.sendScreenMedia({ op: 'screen-stop', guildId: screenStop.guildId, channelId: screenStop.channelId, userId: screenStop.userId, sessionId: screenStop.sessionId }) } } if (this.voiceMedia) { const mediaLeave = await this.voiceMedia.leave() if (gossip && mediaLeave && this.guild) { this.guild.gossipVoiceMediaLeave(mediaLeave) this.guild.sendVoiceMedia(mediaLeave) } } this.voiceCapture?.reset() if (gossip && payload && this.guild) this.guild.gossipVoiceState(payload) return payload } async openHome () { this._invalidateSearchCache() this._guildLoadGeneration++ this._guildLoading = false this._guildLoadingGuildId = null this.emit('guild-loading') await this._leaveMeshes() await this._closeDM() this.guild = null this.mode = 'home' this.activeChannelId = null return { mode: 'home' } } /** Ingest a guild message from mesh gossip (works while on home after join). */ _isGuildLoadCurrent (loadGen) { return loadGen == null || loadGen === this._guildLoadGeneration } async _openGuild (guildRecord, loadGen = null) { const openStartedAt = Date.now() const fallbackThrottled = this._isGuildOpenFallbackThrottled() const span = this.log.time('guild.open', { guildId: guildRecord.id, fallbackThrottled }) try { if (fallbackThrottled) { this.log.info('guild.open fallback-throttled', { spanKind: 'guild.open.fallback-throttle', guildId: guildRecord.id, remainingMs: this._guildOpenFallbackThrottleRemainingMs(), slowCount: this._guildOpenSlowCountInWindow() }) } const leaveSpan = this.log.time('guild.open.leaveMeshes') await this._leaveMeshes() if (!this._isGuildLoadCurrent(loadGen)) { span.end({ superseded: true, logLevel: 'info' }) return { superseded: true } } leaveSpan.end() await this._closeDM() this.activeChannelId = null this._guildOpenNoViewableChannels = false this.mode = 'guild' const user = this.identity.user this.guild = new PearcordGuild({ ownerId: guildRecord.ownerId, userId: user.id, db: this.db, swarm: this._newHyperswarm(this.identity.keyPair) }) await this.guild.ready() this.guild.setActiveGuild(guildRecord) this._wireGuild(this.guild) this._setGuildMeshBounded(guildRecord.id, false) this._logMultiGuildMesh('guild-open', { guildId: guildRecord.id }) const meshSpan = this.log.time('guild.open.joinMesh') try { await this._requestGuildMeshJoin({ source: 'guild-open', loadGen, bounded: true, fallbackThrottled }) } catch (err) { this.log.error('guild.mesh error', { guildId: guildRecord.id, err: err?.message || String(err) }) throw err } if (!this._isGuildLoadCurrent(loadGen)) { meshSpan.end({ superseded: true, logLevel: 'info' }) await this._leaveMeshes() span.end({ superseded: true, logLevel: 'info' }) return { superseded: true } } meshSpan.end({ peers: this.guild.peers?.size ?? 0, logLevel: this._isGuildMeshBounded(guildRecord.id) ? 'info' : undefined }) if (user.id === guildRecord.ownerId) { void this._getGuildReplicator(guildRecord.id).catch(() => {}) } this._scheduleProfileGossipBurst() if ((this.guild.peers?.size ?? 0) === 0) { this._scheduleGuildMeshPeerRetry(guildRecord.id) } else { this._maybeEvictStaleGuildMeshPeers(guildRecord.id, 'guild-open') } this._startGuildMeshStalePeerSweep(guildRecord.id) const voiceSpan = this.log.time('guild.open.voice') await this._initGuildVoice(guildRecord.id) voiceSpan.end() this.messages = new PearcordMessage({ authorId: user.id, db: this.db }) await this.messages.ready() const channelsSpan = this.log.time('guild.open.listChannels') const membersSpan = this.log.time('guild.open.listMembers') const [channels, memberRows] = await Promise.all([ this.guild.listChannels(), this.guild.listMembers() ]) channelsSpan.end({ count: channels.length }) membersSpan.end({ count: memberRows.length }) void memberRows const pickSpan = this.log.time('guild.open.selectChannel') await this._selectGuildChannelWithUnread(channels) pickSpan.end({ activeChannelId: this.activeChannelId }) void this.discovery?.recordGuild(guildRecord).catch(() => {}) void this.discovery?.touchGuild(guildRecord.id).catch(() => {}) setTimeout(() => { void this._ensureGuildModules().catch((err) => { this.log.warn('guild modules init deferred failed', { err }) }) }, 0) this._startAuditExportScheduleTimer() if (!fallbackThrottled) { void this._maybeArchiveHealthRefreshOnFocus().catch(() => {}) this._restartArchiveHealthRefreshTimer() } if ( this.activeChannelId && !this._guildOpenNoViewableChannels && !fallbackThrottled ) { void this._prefetchEmojiAttachmentsAfterOpen().catch((err) => { this.log.debug('emoji batch prefetch after guild open failed', { err }) }) } if (guildRecord.ownerId !== user.id) { const hostPk = await this._lookupGuildHostPublicKey(guildRecord) const syncWaitMs = fallbackThrottled ? Math.max( 500, Number(process.env.PEARCORD_GUILD_OPEN_THROTTLE_SYNC_WAIT_MS) || 2000 ) : 18000 void this._bootstrapGuildSessionAfterJoin({ invite: hostPk ? { creatorId: guildRecord.ownerId, creatorPublicKey: hostPk } : null, hostPublicKey: hostPk, skipMeshJoin: true, syncWaitMs }).catch((err) => { this.log.warn('guild.open member sync bootstrap failed', { guildId: guildRecord.id, err: err?.message || String(err) }) }) if (!fallbackThrottled) { this._scheduleGuildMemberSyncRecovery(guildRecord.id, hostPk) } } const durationMs = Date.now() - openStartedAt this._recordGuildOpenDuration(guildRecord.id, durationMs) span.end({ guildId: guildRecord.id, channels: channels.length, channelCount: channels.length, memberCount: memberRows.length, activeChannelId: this.activeChannelId, guildCount: (this.guilds || []).length, noViewableChannels: !!this._guildOpenNoViewableChannels, durationMs, fallbackThrottled, openFallbackThrottleActive: this._isGuildOpenFallbackThrottled(), spanKind: 'guild.open', logLevel: this._guildOpenNoViewableChannels || this._isGuildMeshBounded(guildRecord.id) || fallbackThrottled ? 'info' : undefined }) return { superseded: false } } catch (err) { this.log.error('guild.open error', { guildId: guildRecord.id, err: err?.message || String(err) }) span.fail(err) throw err } } async close () { await this._leaveVoice({ gossip: false }) await this._leaveMeshes() await this._closeDM() if (this.discovery) { await this.discovery.leaveMesh().catch(() => {}) } if (this.contacts) { await this.contacts.leaveMesh().catch(() => {}) this.contacts = null } if (this.notifications) { await this.notifications.leaveMesh().catch(() => {}) } if (this.userSettings) { await this.userSettings.leaveMesh().catch(() => {}) } if (this.profileCosmetics) { await this.profileCosmetics.leaveMesh().catch(() => {}) } if (this._contactsSwarm) { await this._contactsSwarm.destroy().catch(() => {}) this._contactsSwarm = null } if (this._settingsSwarm) { await this._settingsSwarm.destroy().catch(() => {}) this._settingsSwarm = null this._settingsMeshWired = false this._settingsHealthChannels.clear() } if (this._discoverySwarm) { await this._discoverySwarm.destroy().catch(() => {}) this._discoverySwarm = null } if (this._devicePairSwarm) { await this._devicePairSwarm.destroy().catch(() => {}) this._devicePairSwarm = null } this.deviceSync = null if (this.guild) { await this.guild.close() this.guild = null } this.messages = null this.voice = null this.voiceMedia = null this.voiceCapture = null this.screenShare = null this.displayCapture = null this.announcements = null this.announcementHub = null this.boosts = null this.removeAllListeners() await this.db.close() } } module.exports = { PearcordPlatform }