From 156fe3965d9d976b3065e649fc3942574650e5a2 Mon Sep 17 00:00:00 2001 From: Lucas Leblow Date: Tue, 20 Feb 2024 10:07:02 -0800 Subject: [PATCH] refactor: Refactor create community and store more data in backend --- .../connections-manager.service.spec.ts | 42 +--- .../connections-manager.service.tor.spec.ts | 44 +--- .../connections-manager.service.ts | 227 +++++++++++------- .../connections-manager.types.ts | 8 +- .../src/nest/local-db/local-db.service.ts | 81 ++++++- .../src/nest/local-db/local-db.types.ts | 10 + .../backend/src/nest/socket/socket.service.ts | 19 +- .../communityMetadata.store.spec.ts | 8 +- .../communityMetadata.store.ts | 14 +- .../src/nest/storage/storage.service.spec.ts | 3 +- .../src/nest/storage/storage.service.ts | 10 +- .../src/rtl-tests/channel.main.test.tsx | 12 +- .../src/rtl-tests/community.create.test.tsx | 35 ++- .../src/rtl-tests/community.join.test.tsx | 91 ++++--- .../src/rtl-tests/deep.linking.test.tsx | 2 + .../src/sagas/app/app.master.saga.ts | 2 + .../state-manager/src/sagas/app/app.slice.ts | 3 +- .../loadMigrationData.saga.ts | 33 +++ .../sagas/communities/communities.types.ts | 15 +- .../createNetwork/createNetwork.saga.ts | 6 +- .../launchCommunity.saga.test.ts | 72 ++---- .../launchCommunity/launchCommunity.saga.ts | 20 +- .../savedOwnerCertificate.saga.ts | 14 +- .../startConnection/startConnection.saga.ts | 5 + packages/state-manager/src/types.ts | 6 +- packages/types/src/community.ts | 12 +- packages/types/src/connection.ts | 3 + packages/types/src/socket.ts | 26 +- 28 files changed, 463 insertions(+), 360 deletions(-) create mode 100644 packages/state-manager/src/sagas/app/loadMigrationData/loadMigrationData.saga.ts diff --git a/packages/backend/src/nest/connections-manager/connections-manager.service.spec.ts b/packages/backend/src/nest/connections-manager/connections-manager.service.spec.ts index 9f4d842a1f..95d74070cc 100644 --- a/packages/backend/src/nest/connections-manager/connections-manager.service.spec.ts +++ b/packages/backend/src/nest/connections-manager/connections-manager.service.spec.ts @@ -96,17 +96,11 @@ describe('ConnectionsManagerService', () => { 'QmZoiJNAvCffeEHBjk766nLuKVdkxkAT7wfFJDPPLsbKSE' ) const launchCommunityPayload: InitCommunityPayload = { - id: community.id, - peerId: userIdentity.peerId, - hiddenService: userIdentity.hiddenService, - certs: { - // @ts-expect-error - certificate: userIdentity.userCertificate, - // @ts-expect-error - key: userIdentity.userCsr?.userKey, - CA: [communityRootCa], + network: { + peerId: userIdentity.peerId, + hiddenService: userIdentity.hiddenService, }, - peers: [remotePeer], + community, } await localDbService.put(LocalDBKeys.COMMUNITY, launchCommunityPayload) @@ -132,17 +126,11 @@ describe('ConnectionsManagerService', () => { // At this moment, that test have to be skipped, because checking statues is called before launchCommunity method it.skip('community is only launched once', async () => { const launchCommunityPayload: InitCommunityPayload = { - id: community.id, - peerId: userIdentity.peerId, - hiddenService: userIdentity.hiddenService, - certs: { - // @ts-expect-error - certificate: userIdentity.userCertificate, - // @ts-expect-error - key: userIdentity.userCsr?.userKey, - CA: [communityRootCa], + network: { + peerId: userIdentity.peerId, + hiddenService: userIdentity.hiddenService, }, - peers: community.peerList, + community, } //@ts-ignore @@ -158,17 +146,11 @@ describe('ConnectionsManagerService', () => { it('Bug reproduction - Error on startup - Error: TOR: Connection already established - Trigger launchCommunity from backend and state manager', async () => { const launchCommunityPayload: InitCommunityPayload = { - id: community.id, - peerId: userIdentity.peerId, - hiddenService: userIdentity.hiddenService, - certs: { - // @ts-expect-error - certificate: userIdentity.userCertificate, - // @ts-expect-error - key: userIdentity.userCsr?.userKey, - CA: [communityRootCa], + network: { + peerId: userIdentity.peerId, + hiddenService: userIdentity.hiddenService, }, - peers: community.peerList, + community, } // await connectionsManager.init() diff --git a/packages/backend/src/nest/connections-manager/connections-manager.service.tor.spec.ts b/packages/backend/src/nest/connections-manager/connections-manager.service.tor.spec.ts index f545231518..097810b0cb 100644 --- a/packages/backend/src/nest/connections-manager/connections-manager.service.tor.spec.ts +++ b/packages/backend/src/nest/connections-manager/connections-manager.service.tor.spec.ts @@ -136,17 +136,11 @@ describe('Connections manager', () => { const emitSpy = jest.spyOn(libp2pService, 'emit') const launchCommunityPayload: InitCommunityPayload = { - id: community.id, - peerId: userIdentity.peerId, - hiddenService: userIdentity.hiddenService, - certs: { - // @ts-expect-error - certificate: userIdentity.userCertificate, - // @ts-expect-error - key: userIdentity.userCsr?.userKey, - CA: [communityRootCa], + network: { + peerId: userIdentity.peerId, + hiddenService: userIdentity.hiddenService, }, - peers: community.peerList, + community, } await localDbService.put(LocalDBKeys.COMMUNITY, launchCommunityPayload) @@ -207,18 +201,11 @@ describe('Connections manager', () => { } const launchCommunityPayload: InitCommunityPayload = { - id: community.id, - peerId: userIdentity.peerId, - hiddenService: userIdentity.hiddenService, - certs: { - // @ts-expect-error Identity.userCertificate can be null - certificate: userIdentity.userCertificate, - // @ts-expect-error Identity.userCertificate userCsr.userKey can be undefined - key: userIdentity.userCsr?.userKey, - // @ts-expect-error - CA: [community.rootCa], + network: { + peerId: userIdentity.peerId, + hiddenService: userIdentity.hiddenService, }, - peers: peerList, + community, } await connectionsManagerService.init() await connectionsManagerService.launchCommunity(launchCommunityPayload) @@ -249,18 +236,11 @@ describe('Connections manager', () => { } const launchCommunityPayload: InitCommunityPayload = { - id: community.id, - peerId: userIdentity.peerId, - hiddenService: userIdentity.hiddenService, - certs: { - // @ts-expect-error Identity.userCertificate can be null - certificate: userIdentity.userCertificate, - // @ts-expect-error - key: userIdentity.userCsr?.userKey, - // @ts-expect-error - CA: [community.rootCa], + network: { + peerId: userIdentity.peerId, + hiddenService: userIdentity.hiddenService, }, - peers: peerList, + community, } await connectionsManagerService.launchCommunity(launchCommunityPayload) diff --git a/packages/backend/src/nest/connections-manager/connections-manager.service.ts b/packages/backend/src/nest/connections-manager/connections-manager.service.ts index 8786cbe601..4b5137b780 100644 --- a/packages/backend/src/nest/connections-manager/connections-manager.service.ts +++ b/packages/backend/src/nest/connections-manager/connections-manager.service.ts @@ -25,8 +25,9 @@ import { ErrorMessages, FileMetadata, MessagesLoadedPayload, + type Identity, InitCommunityPayload, - NetworkData, + type NetworkInfo, NetworkDataPayload, NetworkStats, PushNotificationPayload, @@ -147,26 +148,79 @@ export class ConnectionsManagerService extends EventEmitter implements OnModuleI } if (this.configOptions.torControlPort) { - console.log('launch 1') + await this.migrateLevelDb() await this.launchCommunityFromStorage() } } + /** + * Migrate LevelDB when upgrading Quiet for existing communities + * + * Move data from Redux in the frontend to LevelDB in the backend for existing + * communities when upgrading. Hopefully this will make features easier to + * test and develop. In order to do this, we need the data to be accessible on + * the backend before it's first used. Since the backend starts up + * asynchronously, independent of the frontend, we wait for the frontend to + * load migration data before launching the community. + */ + public async migrateLevelDb(): Promise { + // Empty promise used to wait on a callback below + let onDataReceived: () => void + const dataReceivedPromise = new Promise((resolve: () => void) => { + onDataReceived = resolve + }) + const keys = [LocalDBKeys.CURRENT_COMMUNITY_ID, LocalDBKeys.COMMUNITIES, LocalDBKeys.IDENTITIES] + const keysRequired: string[] = [] + + // TODO: For now, to make migrating data/logic to the backend easier, we + // load migration data on each app restart so that the backend data stays + // relatively up-to-date with the frontend. However, as we move the + // frontend logic, which updates and maintains this data, to the backend, + // we can remove these lines so that data is loaded once and then + // maintained on the backend from then on. + this.localDbService.delete(LocalDBKeys.CURRENT_COMMUNITY_ID) + this.localDbService.delete(LocalDBKeys.COMMUNITIES) + this.localDbService.delete(LocalDBKeys.IDENTITIES) + + for (const key of keys) { + if (!(await this.localDbService.exists(key))) { + keysRequired.push(key) + } + } + + this.socketService.on(SocketActionTypes.LOAD_MIGRATION_DATA, async (data: Record) => { + await this.localDbService.migrate(data) + onDataReceived() + }) + + if (keysRequired.length > 0) { + this.logger('Migration data required:', keysRequired) + this.serverIoProvider.io.emit(SocketActionTypes.MIGRATION_DATA_REQUIRED, keysRequired) + await dataReceivedPromise + } else { + this.logger('Nothing to migrate') + } + } + public async launchCommunityFromStorage() { - this.logger('launchCommunityFromStorage') + this.logger('Launching community from storage') + + const community = await this.localDbService.getCurrentCommunity() + const identity = await this.localDbService.getCurrentIdentity() - const community: InitCommunityPayload = await this.localDbService.get(LocalDBKeys.COMMUNITY) - this.logger('launchCommunityFromStorage - community peers', community?.peers) - if (community) { - const sortedPeers = await this.localDbService.getSortedPeers(community.peers) + if (community && identity) { + const sortedPeers = await this.localDbService.getSortedPeers(community.peerList) this.logger('launchCommunityFromStorage - sorted peers', sortedPeers) if (sortedPeers.length > 0) { - community.peers = sortedPeers + community.peerList = sortedPeers + } + + await this.localDbService.setCommunity(community) + + if (![ServiceState.LAUNCHING, ServiceState.LAUNCHED].includes(this.communityState)) { + this.communityState = ServiceState.LAUNCHING + await this.launchCommunity({ community, network: identity }) } - await this.localDbService.put(LocalDBKeys.COMMUNITY, community) - if ([ServiceState.LAUNCHING, ServiceState.LAUNCHED].includes(this.communityState)) return - this.communityState = ServiceState.LAUNCHING - await this.launchCommunity(community) } } @@ -231,10 +285,12 @@ export class ConnectionsManagerService extends EventEmitter implements OnModuleI } } - public async getNetwork() { + public async getNetwork(): Promise { const hiddenService = await this.tor.createNewHiddenService({ targetPort: this.ports.libp2pHiddenService }) - await this.tor.destroyHiddenService(hiddenService.onionAddress.split('.')[0]) + + // Do we want to create the PeerId here? It doesn't necessarily + // have anything to do with Tor. const peerId: PeerId = await PeerId.create() const peerIdJson = peerId.toJSON() this.logger(`Created network for peer ${peerId.toString()}. Address: ${hiddenService.onionAddress}`) @@ -245,100 +301,88 @@ export class ConnectionsManagerService extends EventEmitter implements OnModuleI } } - // TODO: Change community param to communityId and add - // PSK/orbitDbIdentity in create/launch community functions. - public async createNetwork(community: Community): Promise { - let network: NetworkData + public async createNetwork(communityId: string): Promise { + let network: NetworkInfo + try { network = await this.getNetwork() } catch (e) { - this.logger.error(`Creating network for community ${community.id} failed`, e) + this.logger.error(`Creating network for community ${communityId} failed`, e) emitError(this.serverIoProvider.io, { type: SocketActionTypes.CREATE_NETWORK, message: ErrorMessages.NETWORK_SETUP_FAILED, - community: community.id, + community: communityId, }) return } - this.logger(`Sending network data for ${community.id}`) - - // It might be nice to save the entire Community in LevelDB rather - // than specific pieces of information. - - const psk = community.psk - if (psk) { - this.logger('Creating network: received Libp2p PSK') - if (!isPSKcodeValid(psk)) { - this.logger.error('Creating network: received Libp2p PSK is not valid') - emitError(this.serverIoProvider.io, { - type: SocketActionTypes.CREATE_NETWORK, - message: ErrorMessages.NETWORK_SETUP_FAILED, - community: community.id, - }) - return - } - await this.localDbService.put(LocalDBKeys.PSK, psk) - } - - const ownerOrbitDbIdentity = community.ownerOrbitDbIdentity - if (ownerOrbitDbIdentity) { - this.logger("Creating network: received owner's OrbitDB identity") - await this.localDbService.putOwnerOrbitDbIdentity(ownerOrbitDbIdentity) - } - - // TODO: Should we save this data in LevelDB so that the frontend - // doesn't have to pass it back to the backend when it calls - // createCommunity/launchCommunity? + this.logger('Network created') return network } - private async generatePSK() { + private generatePSK(): string { const pskBase64 = Libp2pService.generateLibp2pPSK().psk - await this.localDbService.put(LocalDBKeys.PSK, pskBase64) this.logger('Generated Libp2p PSK') this.serverIoProvider.io.emit(SocketActionTypes.LIBP2P_PSK_STORED, { psk: pskBase64 }) + return pskBase64 } - public async createCommunity(payload: InitCommunityPayload) { - this.logger('Creating community: peers:', payload.peers) + public async createCommunity({ community, network }: InitCommunityPayload) { + this.logger('Creating community: peers:', community.peerList) - await this.generatePSK() + community.psk = await this.generatePSK() + await this.launchCommunity({ community, network }) - await this.launchCommunity(payload) - this.logger(`Created and launched community ${payload.id}`) - this.serverIoProvider.io.emit(SocketActionTypes.COMMUNITY_CREATED, { id: payload.id }) + this.logger(`Created and launched community ${community.id}`) + this.serverIoProvider.io.emit(SocketActionTypes.COMMUNITY_CREATED, { id: community.id }) } - public async launchCommunity(payload: InitCommunityPayload) { - this.logger('Launching community: peers:', payload.peers) + public async launchCommunity({ community, network }: InitCommunityPayload) { + this.logger('Launching community with peers:', community.peerList) this.communityState = ServiceState.LAUNCHING - // Perhaps we should call this data something else, since we already have a Community type. - // It seems like InitCommunityPayload is a mix of various connection metadata. - const communityData: InitCommunityPayload = await this.localDbService.get(LocalDBKeys.COMMUNITY) - if (!communityData) { - await this.localDbService.put(LocalDBKeys.COMMUNITY, payload) + + const psk = community.psk + if (psk) { + if (!isPSKcodeValid(psk)) { + this.logger.error('Failed to launch community. Received Libp2p PSK is not valid') + emitError(this.serverIoProvider.io, { + type: SocketActionTypes.LAUNCH_COMMUNITY, + message: ErrorMessages.NETWORK_SETUP_FAILED, + community: community.id, + }) + return + } + } + + if (!(await this.localDbService.communityExists(community.id))) { + await this.localDbService.setCommunity(community) + await this.localDbService.setCurrentCommunityId(community.id) + // FIXME: NetworkInfo implements a subset of the Identity interface. We + // can re-work this as we migrate data/logic from the frontend to the + // backend. Perhaps we will want to create the Identity (which contains + // the network data) on the backend or have a CREATE_IDENTITY call. + await this.localDbService.setIdentity(network as unknown as Identity) } try { - await this.launch(payload) + await this.launch({ community, network }) } catch (e) { - this.logger(`Couldn't launch community for peer ${payload.peerId.id}.`, e) + this.logger(`Couldn't launch community for peer ${network.peerId.id}.`, e) emitError(this.serverIoProvider.io, { type: SocketActionTypes.LAUNCH_COMMUNITY, message: ErrorMessages.COMMUNITY_LAUNCH_FAILED, - community: payload.id, + community: community.id, trace: e.stack, }) return } - this.logger(`Launched community ${payload.id}`) + this.logger(`Launched community ${community.id}`) this.serverIoProvider.io.emit(SocketActionTypes.CONNECTION_PROCESS_INFO, ConnectionProcessInfo.COMMUNITY_LAUNCHED) - this.communityId = payload.id + this.communityId = community.id this.communityState = ServiceState.LAUNCHED console.log('Hunting for heisenbug: Backend initialized community and sent event to state manager') @@ -346,28 +390,31 @@ export class ConnectionsManagerService extends EventEmitter implements OnModuleI // Unblock websocket endpoints this.socketService.resolveReadyness() - this.serverIoProvider.io.emit(SocketActionTypes.COMMUNITY_LAUNCHED, { id: payload.id }) + this.serverIoProvider.io.emit(SocketActionTypes.COMMUNITY_LAUNCHED, { id: community.id }) } - public async spawnTorHiddenService() { - this.logger(`Spawning hidden service for community ${payload.id}, peer: ${payload.peerId.id}`) + /** + * Spawns hidden service and returns onion address. + */ + public async spawnTorHiddenService(network: NetworkInfo): Promise { this.serverIoProvider.io.emit( SocketActionTypes.CONNECTION_PROCESS_INFO, ConnectionProcessInfo.SPAWNING_HIDDEN_SERVICE ) - const onionAddress: string = await this.tor.spawnHiddenService({ + return await this.tor.spawnHiddenService({ targetPort: this.ports.libp2pHiddenService, - privKey: payload.hiddenService.privateKey, + privKey: network.hiddenService.privateKey, }) } /** - * Start existing community (community that user is already a part of) + * Launch community */ - public async launch(payload: InitCommunityPayload) { - this.spawnTorHiddenService() + public async launch({ community, network }: InitCommunityPayload) { + this.logger(`Launching community ${community.id}: peerId: ${network.peerId.id}`) - this.logger(`Launching community ${payload.id}: peer: ${payload.peerId.id}`) + this.logger(`Spawning hidden service for community ${community.id}, peer: ${network.peerId.id}`) + const onionAddress = await this.spawnTorHiddenService(network) const { Libp2pModule } = await import('../libp2p/libp2p.module') const moduleRef = await this.lazyModuleLoader.load(() => Libp2pModule) @@ -375,21 +422,18 @@ export class ConnectionsManagerService extends EventEmitter implements OnModuleI const lazyService = moduleRef.get(Libp2pService) this.libp2pService = lazyService - const restoredRsa = await PeerId.createFromJSON(payload.peerId) + const restoredRsa = await PeerId.createFromJSON(network.peerId) const _peerId = await peerIdFromKeys(restoredRsa.marshalPubKey(), restoredRsa.marshalPrivKey()) + let peers = community.peerList - let peers = payload.peers - this.logger(`Launching community ${payload.id}: payload peers: ${peers}`) + this.logger(`Launching community ${community.id}: peers: ${peers}`) if (!peers || peers.length === 0) { peers = [this.libp2pService.createLibp2pAddress(onionAddress, _peerId.toString())] } - const pskValue: string = await this.localDbService.get(LocalDBKeys.PSK) - if (!pskValue) { - throw new Error('No psk in local db') - } - this.logger(`Launching community ${payload.id}: retrieved Libp2p PSK`) - const libp2pPSK = Libp2pService.generateLibp2pPSK(pskValue).fullKey + this.logger(`Launching community ${community.id}: retrieved Libp2p PSK`) + const libp2pPSK = Libp2pService.generateLibp2pPSK(community.psk).fullKey + const params: Libp2pNodeParams = { peerId: _peerId, listenAddresses: [this.libp2pService.createLibp2pListenAddress(onionAddress)], @@ -399,7 +443,6 @@ export class ConnectionsManagerService extends EventEmitter implements OnModuleI peers, psk: libp2pPSK, } - await this.libp2pService.createInstance(params) // Libp2p event listeners this.libp2pService.on(Libp2pEvents.PEER_CONNECTED, (payload: { peers: string[] }) => { @@ -421,6 +464,7 @@ export class ConnectionsManagerService extends EventEmitter implements OnModuleI // BARTEK: Potentially obsolete to send this to state-manager this.serverIoProvider.io.emit(SocketActionTypes.PEER_DISCONNECTED, payload) }) + await this.storageService.init(_peerId) // We can use Nest for dependency injection, but I think since the // registration service depends on the storage service being @@ -449,6 +493,7 @@ export class ConnectionsManagerService extends EventEmitter implements OnModuleI private attachRegistrationListeners() { this.registrationService.on(SocketActionTypes.OWNER_CERTIFICATE_ISSUED, payload => { + // TODO: Update community in local DB and emit COMMUNITY_UPDATED event this.serverIoProvider.io.emit(SocketActionTypes.OWNER_CERTIFICATE_ISSUED, payload) }) this.registrationService.on(RegistrationEvents.ERROR, payload => { @@ -477,9 +522,9 @@ export class ConnectionsManagerService extends EventEmitter implements OnModuleI }) this.socketService.on( SocketActionTypes.CREATE_NETWORK, - async (args: Community, callback: (response?: NetworkData) => void) => { + async (communityId: string, callback: (response?: NetworkInfo) => void) => { this.logger(`socketService - ${SocketActionTypes.CREATE_NETWORK}`) - callback(await this.createNetwork(args)) + callback(await this.createNetwork(communityId)) } ) this.socketService.on(SocketActionTypes.CREATE_COMMUNITY, async (args: InitCommunityPayload) => { @@ -495,6 +540,7 @@ export class ConnectionsManagerService extends EventEmitter implements OnModuleI SocketActionTypes.SET_COMMUNITY_METADATA, async (payload: CommunityMetadata, callback: (response?: CommunityMetadata) => void) => { const meta = await this.storageService?.updateCommunityMetadata(payload) + // TODO: Update community in local DB and emit COMMUNITY_UPDATED event callback(meta) } ) @@ -630,6 +676,7 @@ export class ConnectionsManagerService extends EventEmitter implements OnModuleI }) this.storageService.on(StorageEvents.COMMUNITY_METADATA_STORED, async (meta: CommunityMetadata) => { this.logger(`Storage - ${StorageEvents.COMMUNITY_METADATA_STORED}: ${meta}`) + // TODO: Update community in local DB and emit COMMUNITY_UPDATED event this.serverIoProvider.io.emit(SocketActionTypes.COMMUNITY_METADATA_STORED, meta) }) this.storageService.on(StorageEvents.USER_PROFILES_STORED, (payload: UserProfilesStoredEvent) => { diff --git a/packages/backend/src/nest/connections-manager/connections-manager.types.ts b/packages/backend/src/nest/connections-manager/connections-manager.types.ts index 775c55b1de..5df25a2143 100644 --- a/packages/backend/src/nest/connections-manager/connections-manager.types.ts +++ b/packages/backend/src/nest/connections-manager/connections-manager.types.ts @@ -1,17 +1,11 @@ -import { HiddenService } from '@quiet/types' -import PeerId from 'peer-id' - export enum TorInitState { STARTING = 'starting', STARTED = 'started', NOT_STARTED = 'not-started', } + export enum ServiceState { DEFAULT = 'notStarted', LAUNCHING = 'launching', LAUNCHED = 'launched', } -export interface NetworkData { - hiddenService: HiddenService - peerId: PeerId -} diff --git a/packages/backend/src/nest/local-db/local-db.service.ts b/packages/backend/src/nest/local-db/local-db.service.ts index 327a7b43f1..2ed59ccf62 100644 --- a/packages/backend/src/nest/local-db/local-db.service.ts +++ b/packages/backend/src/nest/local-db/local-db.service.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common' import { Level } from 'level' -import { InitCommunityPayload, NetworkStats } from '@quiet/types' +import { InitCommunityPayload, NetworkStats, Identity, Community } from '@quiet/types' import { createLibp2pAddress, filterAndSortPeers } from '@quiet/common' import { LEVEL_DB } from '../const' import { LocalDBKeys, LocalDbStatus } from './local-db.types' @@ -42,6 +42,10 @@ export class LocalDbService { return data } + public async exists(key: string): Promise { + return Boolean(await this.get(key)) + } + public async put(key: string, value: any) { await this.db.put(key, value) } @@ -72,23 +76,80 @@ export class LocalDbService { } } + public async delete(key: string) { + await this.db.del(key) + } + public async getSortedPeers(peers: string[] = []): Promise { const peersStats = (await this.get(LocalDBKeys.PEERS)) || {} const stats: NetworkStats[] = Object.values(peersStats) - const community: InitCommunityPayload = await this.get(LocalDBKeys.COMMUNITY) - if (!community) { + const identity: Identity | undefined = await this.getCurrentIdentity() + + if (identity) { + const localPeerAddress = createLibp2pAddress(identity.hiddenService.onionAddress, identity.peerId.id) + this.logger('Local peer', localPeerAddress) + return filterAndSortPeers(peers, stats, localPeerAddress) + } else { return filterAndSortPeers(peers, stats) } - const localPeerAddress = createLibp2pAddress(community.hiddenService.onionAddress, community.peerId.id) - this.logger('Local peer', localPeerAddress) - return filterAndSortPeers(peers, stats, localPeerAddress) } - public async putOwnerOrbitDbIdentity(id: string): Promise { - this.put(LocalDBKeys.OWNER_ORBIT_DB_IDENTITY, id) + public async migrate(data: any) { + for (const key in data) { + if (typeof data[key] === 'object' && Object.keys(data[key]).length === 0) { + continue + } + if (typeof data[key] === 'string' && data[key].length === 0) { + continue + } + if (Array.isArray(data[key]) && data[key].length === 0) { + continue + } + await this.put(key, data[key]) + } + } + + public async setCommunity(community: Community) { + let communities = await this.get(LocalDBKeys.COMMUNITIES) + if (!communities) { + communities = {} + } + communities[community.id] = community + await this.put(LocalDBKeys.COMMUNITIES, communities) + } + + public async setCurrentCommunityId(communityId: string) { + await this.put(LocalDBKeys.CURRENT_COMMUNITY_ID, communityId) } - public async getOwnerOrbitDbIdentity(): Promise { - return this.get(LocalDBKeys.OWNER_ORBIT_DB_IDENTITY) + public async getCommunities(): Promise> { + return await this.get(LocalDBKeys.COMMUNITIES) + } + + public async getCurrentCommunity(): Promise { + const currentCommunityId = await this.get(LocalDBKeys.CURRENT_COMMUNITY_ID) + const communities = await this.get(LocalDBKeys.COMMUNITIES) + + return communities?.[currentCommunityId] + } + + public async communityExists(communityId: string): Promise { + return communityId in ((await this.getCommunities()) ?? {}) + } + + public async setIdentity(identity: Identity) { + let ids = await this.get(LocalDBKeys.IDENTITIES) + if (!ids) { + ids = {} + } + ids[identity.id] = identity + await this.put(LocalDBKeys.IDENTITIES, ids) + } + + public async getCurrentIdentity(): Promise { + const currentCommunityId = await this.get(LocalDBKeys.CURRENT_COMMUNITY_ID) + const identities = await this.get(LocalDBKeys.IDENTITIES) + + return identities?.[currentCommunityId] } } diff --git a/packages/backend/src/nest/local-db/local-db.types.ts b/packages/backend/src/nest/local-db/local-db.types.ts index aded572cec..c13ada9ee4 100644 --- a/packages/backend/src/nest/local-db/local-db.types.ts +++ b/packages/backend/src/nest/local-db/local-db.types.ts @@ -1,8 +1,18 @@ export enum LocalDBKeys { + // List of Community objects + COMMUNITIES = 'communities', + // ID of current community + CURRENT_COMMUNITY_ID = 'currentCommunityId', + // List of Identity objects + IDENTITIES = 'identities', + // DEPRECATED + // TODO: Remove key from IndexDB COMMUNITY = 'community', REGISTRAR = 'registrar', PEERS = 'peers', + // TODO: Deprecate this soon. This data exists in the Community object. PSK = 'psk', + // TODO: Deprecate this soon. This data exists in the Community object. OWNER_ORBIT_DB_IDENTITY = 'ownerOrbitDbIdentity', } export type LocalDbStatus = 'opening' | 'open' | 'closing' | 'closed' diff --git a/packages/backend/src/nest/socket/socket.service.ts b/packages/backend/src/nest/socket/socket.service.ts index ca63a0e411..f4b9cedb03 100644 --- a/packages/backend/src/nest/socket/socket.service.ts +++ b/packages/backend/src/nest/socket/socket.service.ts @@ -19,7 +19,8 @@ import { type PermsData, type UserProfile, type DeleteChannelResponse, - type NetworkData, + type NetworkInfo, + type MessagesLoadedPayload, } from '@quiet/types' import EventEmitter from 'events' import { CONFIG_OPTIONS, SERVER_IO_PROVIDER } from '../const' @@ -151,21 +152,21 @@ export class SocketService extends EventEmitter implements OnModuleInit { // ====== Community ====== socket.on(SocketActionTypes.CREATE_COMMUNITY, async (payload: InitCommunityPayload) => { - this.logger(`Creating community ${payload.id}`) + this.logger(`Creating community ${payload.community.id}`) this.emit(SocketActionTypes.CREATE_COMMUNITY, payload) }) socket.on(SocketActionTypes.LAUNCH_COMMUNITY, async (payload: InitCommunityPayload) => { - this.logger(`Launching community ${payload.id} for ${payload.peerId.id}`) + this.logger(`Launching community ${payload.community.id} for ${payload.network.peerId.id}`) this.emit(SocketActionTypes.LAUNCH_COMMUNITY, payload) this.emit(SocketActionTypes.CONNECTION_PROCESS_INFO, ConnectionProcessInfo.LAUNCHING_COMMUNITY) }) socket.on( SocketActionTypes.CREATE_NETWORK, - async (community: Community, callback: (response?: NetworkData) => void) => { - this.logger(`Creating network for community ${community.id}`) - this.emit(SocketActionTypes.CREATE_NETWORK, community, callback) + async (communityId: string, callback: (response?: NetworkInfo) => void) => { + this.logger(`Creating network for community ${communityId}`) + this.emit(SocketActionTypes.CREATE_NETWORK, communityId, callback) } ) @@ -195,6 +196,12 @@ export class SocketService extends EventEmitter implements OnModuleInit { socket.on(SocketActionTypes.SET_USER_PROFILE, (profile: UserProfile) => { this.emit(SocketActionTypes.SET_USER_PROFILE, profile) }) + + // ====== Misc ====== + + socket.on(SocketActionTypes.LOAD_MIGRATION_DATA, async (data: Record) => { + this.emit(SocketActionTypes.LOAD_MIGRATION_DATA, data) + }) }) } diff --git a/packages/backend/src/nest/storage/communityMetadata/communityMetadata.store.spec.ts b/packages/backend/src/nest/storage/communityMetadata/communityMetadata.store.spec.ts index fd7b7d568f..d200c73166 100644 --- a/packages/backend/src/nest/storage/communityMetadata/communityMetadata.store.spec.ts +++ b/packages/backend/src/nest/storage/communityMetadata/communityMetadata.store.spec.ts @@ -40,9 +40,11 @@ describe('CommmunityMetadataStore', () => { let community: Community const mockLocalDbService = { - putOwnerOrbitDbIdentity: jest.fn(), + setCommunity: jest.fn(), // @ts-ignore - OrbitDB's type definition doesn't include identity - getOwnerOrbitDbIdentity: jest.fn(() => orbitDbService.orbitDb.identity.id), + getCurrentCommunity: jest.fn(() => { + return { ownerOrbitDbIdentity: orbitDbService.orbitDb.identity.id } + }), } beforeAll(async () => { @@ -104,7 +106,7 @@ describe('CommmunityMetadataStore', () => { }) describe('updateCommunityMetadata', () => { - test('updates community metadata if the metadata is valid', async () => { + test.only('updates community metadata if the metadata is valid', async () => { const ret = await communityMetadataStore.updateCommunityMetadata(metaValid) const meta = communityMetadataStore.getCommunityMetadata() diff --git a/packages/backend/src/nest/storage/communityMetadata/communityMetadata.store.ts b/packages/backend/src/nest/storage/communityMetadata/communityMetadata.store.ts index 65ce085150..9acbb661be 100644 --- a/packages/backend/src/nest/storage/communityMetadata/communityMetadata.store.ts +++ b/packages/backend/src/nest/storage/communityMetadata/communityMetadata.store.ts @@ -123,9 +123,14 @@ export class CommunityMetadataStore extends EventEmitter { ownerOrbitDbIdentity, } - // putOwnerOrbitDbIdentity goes before store.put because the - // store's KeyValueIndex calls getOwnerOrbitDbIdentity - this.localDbService.putOwnerOrbitDbIdentity(ownerOrbitDbIdentity) + // Updating this here before store.put because the store's KeyValueIndex + // then uses the updated Community object. + const community = await this.localDbService.getCurrentCommunity() + if (community) { + await this.localDbService.setCommunity({ ...community, ownerOrbitDbIdentity }) + } else { + throw new Error('Current community missing') + } // FIXME: I think potentially there is a subtle developer // experience bug here. Internally OrbitDB will call @@ -170,7 +175,8 @@ export class CommunityMetadataStore extends EventEmitter { return false } - const ownerOrbitDbIdentity = await localDbService.getOwnerOrbitDbIdentity() + const community = await localDbService.getCurrentCommunity() + const ownerOrbitDbIdentity = community?.ownerOrbitDbIdentity if (!ownerOrbitDbIdentity) { logger.error('Failed to verify community metadata entry:', entry.hash, 'owner identity is invalid') return false diff --git a/packages/backend/src/nest/storage/storage.service.spec.ts b/packages/backend/src/nest/storage/storage.service.spec.ts index 7c0abb0594..0cfdef6888 100644 --- a/packages/backend/src/nest/storage/storage.service.spec.ts +++ b/packages/backend/src/nest/storage/storage.service.spec.ts @@ -164,7 +164,8 @@ describe('StorageService', () => { expect(ipfsService.ipfsInstance).not.toBeNull() expect(localDbService.getStatus()).toEqual('open') - await localDbService.put(LocalDBKeys.COMMUNITY, community) + await localDbService.setCommunity(community) + await localDbService.setCurrentCommunityId(community.id) }) afterEach(async () => { diff --git a/packages/backend/src/nest/storage/storage.service.ts b/packages/backend/src/nest/storage/storage.service.ts index d3b21f8164..7624e94069 100644 --- a/packages/backend/src/nest/storage/storage.service.ts +++ b/packages/backend/src/nest/storage/storage.service.ts @@ -316,11 +316,11 @@ export class StorageService extends EventEmitter { const users = this.getAllUsers() const peers = users.map(peer => createLibp2pAddress(peer.onionAddress, peer.peerId)) console.log('updatePeersList, peers count:', peers.length) - const community = await this.localDbService.get(LocalDBKeys.COMMUNITY) + const community = await this.localDbService.getCurrentCommunity() const sortedPeers = await this.localDbService.getSortedPeers(peers) if (sortedPeers.length > 0) { - community.peers = sortedPeers - await this.localDbService.put(LocalDBKeys.COMMUNITY, community) + community.peerList = sortedPeers + await this.localDbService.setCommunity(community) } this.emit(StorageEvents.UPDATE_PEERS_LIST, { communityId: community.id, peerList: peers }) } @@ -500,7 +500,7 @@ export class StorageService extends EventEmitter { db.events.on('replicated', async address => { this.logger('Replicated.', address) const ids = this.getAllEventLogEntries(db).map(msg => msg.id) - const community = await this.localDbService.get(LocalDBKeys.COMMUNITY) + const community = await this.localDbService.getCurrentCommunity() this.emit(StorageEvents.MESSAGE_IDS_STORED, { ids, channelId: channelData.id, @@ -510,7 +510,7 @@ export class StorageService extends EventEmitter { db.events.on('ready', async () => { const ids = this.getAllEventLogEntries(db).map(msg => msg.id) - const community = await this.localDbService.get(LocalDBKeys.COMMUNITY) + const community = await this.localDbService.getCurrentCommunity() this.emit(StorageEvents.MESSAGE_IDS_STORED, { ids, channelId: channelData.id, diff --git a/packages/desktop/src/rtl-tests/channel.main.test.tsx b/packages/desktop/src/rtl-tests/channel.main.test.tsx index 514c485fc5..6818a2ab5c 100644 --- a/packages/desktop/src/rtl-tests/channel.main.test.tsx +++ b/packages/desktop/src/rtl-tests/channel.main.test.tsx @@ -773,7 +773,7 @@ describe('Channel', () => { const data = input[1] as InitCommunityPayload const payload = data return socket.socketClient.emit(SocketActionTypes.COMMUNITY_LAUNCHED, { - id: payload.id, + id: payload.community.id, }) } if (action === SocketActionTypes.UPLOAD_FILE) { @@ -970,7 +970,7 @@ describe('Channel', () => { const data = input[1] as InitCommunityPayload const payload = data return socket.socketClient.emit(SocketActionTypes.COMMUNITY_LAUNCHED, { - id: payload.id, + id: payload.community.id, }) } if (action === SocketActionTypes.DOWNLOAD_FILE) { @@ -1059,7 +1059,7 @@ describe('Channel', () => { const data = input[1] as InitCommunityPayload const payload = data return socket.socketClient.emit(SocketActionTypes.COMMUNITY_LAUNCHED, { - id: payload.id, + id: payload.community.id, }) } if (action === SocketActionTypes.UPLOAD_FILE) { @@ -1197,7 +1197,7 @@ describe('Channel', () => { const data = input[1] as InitCommunityPayload const payload = data return socket.socketClient.emit(SocketActionTypes.COMMUNITY_LAUNCHED, { - id: payload.id, + id: payload.community.id, }) } }) @@ -1322,7 +1322,7 @@ describe('Channel', () => { const data = input[1] as InitCommunityPayload const payload = data return socket.socketClient.emit(SocketActionTypes.COMMUNITY_LAUNCHED, { - id: payload.id, + id: payload.community.id, }) } }) @@ -1449,7 +1449,7 @@ describe('Channel', () => { const data = input[1] as InitCommunityPayload const payload = data return socket.socketClient.emit(SocketActionTypes.COMMUNITY_LAUNCHED, { - id: payload.id, + id: payload.community.id, }) } }) diff --git a/packages/desktop/src/rtl-tests/community.create.test.tsx b/packages/desktop/src/rtl-tests/community.create.test.tsx index 1884344c35..94593f8096 100644 --- a/packages/desktop/src/rtl-tests/community.create.test.tsx +++ b/packages/desktop/src/rtl-tests/community.create.test.tsx @@ -14,13 +14,17 @@ import { CreateCommunityDictionary } from '../renderer/components/CreateJoinComm import MockedSocket from 'socket.io-mock' import { ioMock } from '../shared/setupTests' import { socketEventData } from '../renderer/testUtils/socket' -import { Community, SavedOwnerCertificatePayload, SocketActionTypes } from '@quiet/types' +import { + Community, + type InitCommunityPayload, + type NetworkInfo, + SavedOwnerCertificatePayload, + SocketActionTypes, +} from '@quiet/types' import { ChannelsReplicatedPayload, - InitCommunityPayload, publicChannels, RegisterOwnerCertificatePayload, - type NetworkData, ResponseLaunchCommunityPayload, } from '@quiet/state-manager' import Channel from '../renderer/components/Channel/Channel' @@ -71,20 +75,15 @@ describe('User', () => { const mockEmitImpl = (...input: [SocketActionTypes, ...socketEventData<[any]>]) => { const action = input[0] if (action === SocketActionTypes.CREATE_NETWORK) { - const data = input[1] as Community - const payload = { ...data, privateKey: 'privateKey' } - socket.socketClient.emit(SocketActionTypes.NETWORK_CREATED, { - community: payload, - network: { - hiddenService: { - onionAddress: 'onionAddress', - privateKey: 'privKey', - }, - peerId: { - id: 'peerId', - }, + return { + hiddenService: { + onionAddress: 'onionAddress', + privateKey: 'privKey', }, - }) + peerId: { + id: 'peerId', + }, + } } if (action === SocketActionTypes.REGISTER_OWNER_CERTIFICATE) { const payload = input[1] as RegisterOwnerCertificatePayload @@ -98,10 +97,10 @@ describe('User', () => { if (action === SocketActionTypes.CREATE_COMMUNITY) { const payload = input[1] as InitCommunityPayload socket.socketClient.emit(SocketActionTypes.COMMUNITY_LAUNCHED, { - id: payload.id, + id: payload.community.id, }) socket.socketClient.emit(SocketActionTypes.COMMUNITY_CREATED, { - id: payload.id, + id: payload.community.id, }) socket.socketClient.emit(SocketActionTypes.CHANNELS_STORED, { diff --git a/packages/desktop/src/rtl-tests/community.join.test.tsx b/packages/desktop/src/rtl-tests/community.join.test.tsx index c7e68027a6..b6085b7bdf 100644 --- a/packages/desktop/src/rtl-tests/community.join.test.tsx +++ b/packages/desktop/src/rtl-tests/community.join.test.tsx @@ -18,12 +18,10 @@ import { identity, communities, RegisterUserCertificatePayload, - InitCommunityPayload, ErrorCodes, ErrorMessages, getFactory, errors, - type NetworkData, } from '@quiet/state-manager' import Channel from '../renderer/components/Channel/Channel' import LoadingPanel from '../renderer/components/LoadingPanel/LoadingPanel' @@ -34,6 +32,8 @@ import { ChannelsReplicatedPayload, Community, ErrorPayload, + type InitCommunityPayload, + type NetworkInfo, ResponseLaunchCommunityPayload, SendOwnerCertificatePayload, SocketActionTypes, @@ -87,29 +87,25 @@ describe('User', () => { const factory = await getFactory(store) - jest.spyOn(socket, 'emit').mockImplementation(async (...input: [SocketActionTypes, ...socketEventData<[any]>]) => { + const mockEmitImpl = async (...input: [SocketActionTypes, ...socketEventData<[any]>]) => { const action = input[0] if (action === SocketActionTypes.CREATE_NETWORK) { - const payload = input[1] as Community - return socket.socketClient.emit(SocketActionTypes.NETWORK_CREATED, { - community: payload, - network: { - hiddenService: { - onionAddress: 'onionAddress', - privateKey: 'privKey', - }, - peerId: { - id: 'peerId', - }, + return { + hiddenService: { + onionAddress: 'onionAddress', + privateKey: 'privKey', }, - }) + peerId: { + id: 'peerId', + }, + } } if (action === SocketActionTypes.LAUNCH_COMMUNITY) { const payload = input[1] as InitCommunityPayload const community = communities.selectors.currentCommunity(store.getState()) - expect(payload.id).toEqual(community?.id) + expect(payload.community.id).toEqual(community?.id) socket.socketClient.emit(SocketActionTypes.COMMUNITY_LAUNCHED, { - id: payload.id, + id: payload.community.id, }) socket.socketClient.emit(SocketActionTypes.CHANNELS_STORED, { channels: { @@ -123,7 +119,11 @@ describe('User', () => { }, }) } - }) + } + + jest.spyOn(socket, 'emit').mockImplementation(mockEmitImpl) + // @ts-ignore + socket.emitWithAck = mockEmitImpl // Log all the dispatched actions in order const actions: AnyAction[] = [] @@ -226,22 +226,18 @@ describe('User', () => { store ) - jest.spyOn(socket, 'emit').mockImplementation(async (...input: [SocketActionTypes, ...socketEventData<[any]>]) => { + const mockEmitImpl = async (...input: [SocketActionTypes, ...socketEventData<[any]>]) => { const action = input[0] if (action === SocketActionTypes.CREATE_NETWORK) { - const payload = input[1] as Community - return socket.socketClient.emit(SocketActionTypes.NETWORK_CREATED, { - community: payload, - network: { - hiddenService: { - onionAddress: 'onionAddress', - privateKey: 'privKey', - }, - peerId: { - id: 'peerId', - }, + return { + hiddenService: { + onionAddress: 'onionAddress', + privateKey: 'privKey', }, - }) + peerId: { + id: 'peerId', + }, + } } if (action === SocketActionTypes.REGISTER_USER_CERTIFICATE) { const payload = input[1] as RegisterUserCertificatePayload @@ -254,7 +250,11 @@ describe('User', () => { community: community?.id, }) } - }) + } + + jest.spyOn(socket, 'emit').mockImplementation(mockEmitImpl) + // @ts-ignore + socket.emitWithAck = mockEmitImpl // Log all the dispatched actions in order const actions: AnyAction[] = [] @@ -314,24 +314,23 @@ describe('User', () => { store ) - jest.spyOn(socket, 'emit').mockImplementation(async (...input: [SocketActionTypes, ...socketEventData<[any]>]) => { + const mockEmitImpl = async (...input: [SocketActionTypes, ...socketEventData<[any]>]) => { const action = input[0] if (action === SocketActionTypes.CREATE_NETWORK) { - const payload = input[1] as Community - return socket.socketClient.emit(SocketActionTypes.NETWORK_CREATED, { - community: payload, - network: { - hiddenService: { - onionAddress: 'onionAddress', - privateKey: 'privKey', - }, - peerId: { - id: 'peerId', - }, + return { + hiddenService: { + onionAddress: 'onionAddress', + privateKey: 'privKey', }, - }) + peerId: { + id: 'peerId', + }, + } } - }) + } + + // @ts-ignore + socket.emitWithAck = mockEmitImpl // Log all the dispatched actions in order const actions: AnyAction[] = [] diff --git a/packages/desktop/src/rtl-tests/deep.linking.test.tsx b/packages/desktop/src/rtl-tests/deep.linking.test.tsx index 4130b7bc14..2ba01070db 100644 --- a/packages/desktop/src/rtl-tests/deep.linking.test.tsx +++ b/packages/desktop/src/rtl-tests/deep.linking.test.tsx @@ -15,6 +15,8 @@ describe('Deep linking', () => { beforeEach(async () => { socket = new MockedSocket() + // @ts-ignore + socket.emitWithAck = jest.fn() ioMock.mockImplementation(() => socket) }) diff --git a/packages/state-manager/src/sagas/app/app.master.saga.ts b/packages/state-manager/src/sagas/app/app.master.saga.ts index f905fdf992..07dca44027 100644 --- a/packages/state-manager/src/sagas/app/app.master.saga.ts +++ b/packages/state-manager/src/sagas/app/app.master.saga.ts @@ -3,10 +3,12 @@ import { all, takeEvery, takeLeading } from 'typed-redux-saga' import { appActions } from './app.slice' import { closeServicesSaga } from './closeServices.saga' import { stopBackendSaga } from './stopBackend/stopBackend.saga' +import { loadMigrationDataSaga } from './loadMigrationData/loadMigrationData.saga' export function* appMasterSaga(socket: Socket): Generator { yield* all([ takeLeading(appActions.closeServices.type, closeServicesSaga, socket), takeEvery(appActions.stopBackend.type, stopBackendSaga, socket), + takeEvery(appActions.loadMigrationData.type, loadMigrationDataSaga, socket), ]) } diff --git a/packages/state-manager/src/sagas/app/app.slice.ts b/packages/state-manager/src/sagas/app/app.slice.ts index e4ba4f2ef8..4b69d07204 100644 --- a/packages/state-manager/src/sagas/app/app.slice.ts +++ b/packages/state-manager/src/sagas/app/app.slice.ts @@ -1,4 +1,4 @@ -import { createSlice } from '@reduxjs/toolkit' +import { createSlice, type PayloadAction } from '@reduxjs/toolkit' import { StoreKeys } from '../store.keys' // eslint-disable-next-line @typescript-eslint/no-extraneous-class @@ -10,6 +10,7 @@ export const appSlice = createSlice({ reducers: { closeServices: state => state, stopBackend: state => state, + loadMigrationData: (state, action: PayloadAction) => state, }, }) diff --git a/packages/state-manager/src/sagas/app/loadMigrationData/loadMigrationData.saga.ts b/packages/state-manager/src/sagas/app/loadMigrationData/loadMigrationData.saga.ts new file mode 100644 index 0000000000..74893658fa --- /dev/null +++ b/packages/state-manager/src/sagas/app/loadMigrationData/loadMigrationData.saga.ts @@ -0,0 +1,33 @@ +import { type PayloadAction } from '@reduxjs/toolkit' +import { apply, select } from 'typed-redux-saga' + +import { SocketActionTypes } from '@quiet/types' + +import { type appActions } from '../app.slice' +import { type Socket, applyEmitParams } from '../../../types' +import { communitiesSelectors } from '../../communities/communities.selectors' +import { identitySelectors } from '../../identity/identity.selectors' + +export function* loadMigrationDataSaga( + socket: Socket, + action: PayloadAction['payload']> +): Generator { + const keys = action.payload + const data: Record = {} + + for (const key of keys) { + if (key === 'communities') { + data[key] = yield* select(communitiesSelectors.selectEntities) + } + + if (key === 'currentCommunityId') { + data[key] = yield* select(communitiesSelectors.currentCommunityId) + } + + if (key === 'identities') { + data[key] = yield* select(identitySelectors.selectEntities) + } + } + + yield* apply(socket, socket.emit, applyEmitParams(SocketActionTypes.LOAD_MIGRATION_DATA, data)) +} diff --git a/packages/state-manager/src/sagas/communities/communities.types.ts b/packages/state-manager/src/sagas/communities/communities.types.ts index 48074a17cf..b29ddb4e4d 100644 --- a/packages/state-manager/src/sagas/communities/communities.types.ts +++ b/packages/state-manager/src/sagas/communities/communities.types.ts @@ -1,15 +1,12 @@ import { type Community, type HiddenService, type Identity, type PeerId } from '@quiet/types' +// FIXME: Remove this file in favor of @quiet/types + export enum CommunityOwnership { Owner = 'owner', User = 'user', } -export interface NetworkData { - hiddenService: HiddenService - peerId: PeerId -} - export interface CreateNetworkPayload { ownership: CommunityOwnership name?: string @@ -21,14 +18,6 @@ export interface Certificates { CA: string[] } -export interface InitCommunityPayload { - id: string - peerId: PeerId - hiddenService: HiddenService - certs: Certificates - peers?: string[] -} - export interface StorePeerListPayload { communityId: string peerList: string[] diff --git a/packages/state-manager/src/sagas/communities/createNetwork/createNetwork.saga.ts b/packages/state-manager/src/sagas/communities/createNetwork/createNetwork.saga.ts index fa73cd3aaf..69a80947ac 100644 --- a/packages/state-manager/src/sagas/communities/createNetwork/createNetwork.saga.ts +++ b/packages/state-manager/src/sagas/communities/createNetwork/createNetwork.saga.ts @@ -55,7 +55,11 @@ export function* createNetworkSaga( yield* put(communitiesActions.addNewCommunity(community)) yield* put(communitiesActions.setCurrentCommunity(id)) - const network = yield* apply(socket, socket.emitWithAck, applyEmitParams(SocketActionTypes.CREATE_NETWORK, community)) + const network = yield* apply( + socket, + socket.emitWithAck, + applyEmitParams(SocketActionTypes.CREATE_NETWORK, community.id) + ) const dmKeys = yield* call(generateDmKeyPair) const identity: Identity = { id: community.id, diff --git a/packages/state-manager/src/sagas/communities/launchCommunity/launchCommunity.saga.test.ts b/packages/state-manager/src/sagas/communities/launchCommunity/launchCommunity.saga.test.ts index 8627aaee94..b04bfe7ac2 100644 --- a/packages/state-manager/src/sagas/communities/launchCommunity/launchCommunity.saga.test.ts +++ b/packages/state-manager/src/sagas/communities/launchCommunity/launchCommunity.saga.test.ts @@ -73,16 +73,11 @@ describe('launchCommunity', () => { rootCa: 'rootCA', } const launchCommunityPayload: InitCommunityPayload = { - id: community.id, - peerId: identity.peerId, - hiddenService: identity.hiddenService, - certs: { - certificate: identity.userCertificate, - // @ts-expect-error - key: identity.userCsr.userKey, - CA: [communityWithRootCa.rootCa], + network: { + peerId: identity.peerId, + hiddenService: identity.hiddenService, }, - peers: community.peerList, + community, } await expectSaga(launchCommunitySaga, socket, communitiesActions.launchCommunity(community.id)) @@ -107,15 +102,7 @@ describe('launchCommunity', () => { }, } ) - .apply(socket, socket.emit, [ - SocketActionTypes.LAUNCH_COMMUNITY, - { - id: launchCommunityPayload.id, - peerId: launchCommunityPayload.peerId, - hiddenService: launchCommunityPayload.hiddenService, - peers: launchCommunityPayload.peers, - }, - ]) + .apply(socket, socket.emit, [SocketActionTypes.LAUNCH_COMMUNITY, launchCommunityPayload]) .run() }) @@ -134,16 +121,11 @@ describe('launchCommunity', () => { }) const launchCommunityPayload: InitCommunityPayload = { - id: community.id, - peerId: identity.peerId, - hiddenService: identity.hiddenService, - certs: { - certificate: identity.userCertificate, - // @ts-expect-error - key: identity.userCsr.userKey, - CA: [communityWithRootCa.rootCa], + network: { + peerId: identity.peerId, + hiddenService: identity.hiddenService, }, - peers: community.peerList, + community, } await expectSaga(launchCommunitySaga, socket, communitiesActions.launchCommunity(community.id)) @@ -168,15 +150,7 @@ describe('launchCommunity', () => { }, } ) - .apply(socket, socket.emit, [ - SocketActionTypes.LAUNCH_COMMUNITY, - { - id: launchCommunityPayload.id, - peerId: launchCommunityPayload.peerId, - hiddenService: launchCommunityPayload.hiddenService, - peers: launchCommunityPayload.peers, - }, - ]) + .apply(socket, socket.emit, [SocketActionTypes.LAUNCH_COMMUNITY, launchCommunityPayload]) .run() }) @@ -192,18 +166,11 @@ describe('launchCommunity', () => { }) const launchCommunityPayload: InitCommunityPayload = { - id: community.id, - peerId: identity.peerId, - hiddenService: identity.hiddenService, - certs: { - // @ts-expect-error - certificate: identity.userCertificate, - // @ts-expect-error - key: identity.userCsr.userKey, - // @ts-expect-error - CA: [community.rootCa], + network: { + peerId: identity.peerId, + hiddenService: identity.hiddenService, }, - peers: community.peerList, + community, } await expectSaga(launchCommunitySaga, socket, communitiesActions.launchCommunity(community.id)) @@ -228,16 +195,7 @@ describe('launchCommunity', () => { }, } ) - .not.apply(socket, socket.emit, [ - SocketActionTypes.LAUNCH_COMMUNITY, - { - id: launchCommunityPayload.id, - peerId: launchCommunityPayload.peerId, - hiddenService: launchCommunityPayload.hiddenService, - certs: launchCommunityPayload.certs, - peers: launchCommunityPayload.peers, - }, - ]) + .not.apply(socket, socket.emit, [SocketActionTypes.LAUNCH_COMMUNITY, launchCommunityPayload]) .run() }) diff --git a/packages/state-manager/src/sagas/communities/launchCommunity/launchCommunity.saga.ts b/packages/state-manager/src/sagas/communities/launchCommunity/launchCommunity.saga.ts index e0a3be2c2c..800c887831 100644 --- a/packages/state-manager/src/sagas/communities/launchCommunity/launchCommunity.saga.ts +++ b/packages/state-manager/src/sagas/communities/launchCommunity/launchCommunity.saga.ts @@ -9,7 +9,7 @@ import { getCurrentTime } from '../../messages/utils/message.utils' import { connectionSelectors } from '../../appConnection/connection.selectors' import { networkSelectors } from '../../network/network.selectors' import { pairsToP2pAddresses } from '@quiet/common' -import { type InitCommunityPayload, SocketActionTypes } from '@quiet/types' +import { type Community, type InitCommunityPayload, SocketActionTypes } from '@quiet/types' // TODO: Remove if unused export function* initCommunities(): Generator { @@ -31,11 +31,17 @@ export function* launchCommunitySaga( action: PayloadAction['payload'] | undefined> ): Generator { const communityId = action.payload + + if (!communityId) { + console.error('Could not launch community, missing community ID') + return + } + const community = yield* select(communitiesSelectors.selectById(communityId)) const identity = yield* select(identitySelectors.selectById(communityId)) - if (!identity?.userCsr?.userKey) { - console.error('Could not launch community, No identity private key') + if (!community || !identity?.userCsr?.userKey) { + console.error('Could not launch community, missing community or user private key') return } @@ -49,10 +55,6 @@ export function* launchCommunitySaga( } const payload: InitCommunityPayload = { - id: identity.id, - // TODO: This data originates on the backend, so should we just - // keep it there instead of passing it back and forth? Is it used - // for anything on the frontend? network: { peerId: identity.peerId, hiddenService: identity.hiddenService, @@ -60,8 +62,8 @@ export function* launchCommunitySaga( // TODO: Add peerList to the currentCommunity via updateCommunityData community: { ...community, - peerList: peerList - } + peerList: peerList, + }, } yield* apply(socket, socket.emit, applyEmitParams(SocketActionTypes.LAUNCH_COMMUNITY, payload)) diff --git a/packages/state-manager/src/sagas/identity/savedOwnerCertificate/savedOwnerCertificate.saga.ts b/packages/state-manager/src/sagas/identity/savedOwnerCertificate/savedOwnerCertificate.saga.ts index 5631401ee2..bd19c795ac 100644 --- a/packages/state-manager/src/sagas/identity/savedOwnerCertificate/savedOwnerCertificate.saga.ts +++ b/packages/state-manager/src/sagas/identity/savedOwnerCertificate/savedOwnerCertificate.saga.ts @@ -18,17 +18,15 @@ export function* savedOwnerCertificateSaga( const community = yield* select(communitiesSelectors.selectById(communityId)) const identity = yield* select(identitySelectors.selectById(communityId)) - if (!identity?.userCertificate || !identity?.userCsr || !community?.rootCa) return + + if (!identity?.peerId || !identity?.hiddenService || !community || !community?.rootCa) return const payload: InitCommunityPayload = { - id: communityId, - peerId: identity.peerId, - hiddenService: identity.hiddenService, - certs: { - certificate: identity.userCertificate, - key: identity.userCsr.userKey, - CA: [community.rootCa], + network: { + peerId: identity.peerId, + hiddenService: identity.hiddenService, }, + community, } yield* apply(socket, socket.emit, applyEmitParams(SocketActionTypes.CREATE_COMMUNITY, payload)) diff --git a/packages/state-manager/src/sagas/socket/startConnection/startConnection.saga.ts b/packages/state-manager/src/sagas/socket/startConnection/startConnection.saga.ts index a43b8f034a..70e44e9bb5 100644 --- a/packages/state-manager/src/sagas/socket/startConnection/startConnection.saga.ts +++ b/packages/state-manager/src/sagas/socket/startConnection/startConnection.saga.ts @@ -2,6 +2,7 @@ import { eventChannel } from 'redux-saga' import { type Socket } from '../../../types' import { all, call, fork, put, takeEvery } from 'typed-redux-saga' import logger from '../../../utils/logger' +import { appActions } from '../../app/app.slice' import { appMasterSaga } from '../../app/app.master.saga' import { connectionActions } from '../../appConnection/connection.slice' import { communitiesMasterSaga } from '../../communities/communities.master.saga' @@ -88,6 +89,7 @@ export function subscribe(socket: Socket) { | ReturnType | ReturnType | ReturnType + | ReturnType >(emit => { // UPDATE FOR APP socket.on(SocketActionTypes.TOR_INITIALIZED, () => { @@ -105,6 +107,9 @@ export function subscribe(socket: Socket) { emit(networkActions.removeConnectedPeer(payload.peer)) emit(connectionActions.updateNetworkData(payload)) }) + socket.on(SocketActionTypes.MIGRATION_DATA_REQUIRED, (keys: string[]) => { + emit(appActions.loadMigrationData(keys)) + }) // Files socket.on(SocketActionTypes.MESSAGE_MEDIA_UPDATED, (payload: FileMetadata) => { emit(filesActions.updateMessageMedia(payload)) diff --git a/packages/state-manager/src/types.ts b/packages/state-manager/src/types.ts index 5a5f31751d..a13957e2c3 100644 --- a/packages/state-manager/src/types.ts +++ b/packages/state-manager/src/types.ts @@ -11,7 +11,8 @@ import { type DownloadFilePayload, type GetMessagesPayload, type InitCommunityPayload, - type NetworkData, + type MessagesLoadedPayload, + type NetworkInfo, type RegisterOwnerCertificatePayload, type RegisterUserCertificatePayload, type SaveOwnerCertificatePayload, @@ -48,11 +49,12 @@ export interface EmitEvents { [SocketActionTypes.DELETE_FILES_FROM_CHANNEL]: EmitEvent [SocketActionTypes.CLOSE]: () => void [SocketActionTypes.LEAVE_COMMUNITY]: () => void - [SocketActionTypes.CREATE_NETWORK]: EmitEvent void> + [SocketActionTypes.CREATE_NETWORK]: EmitEvent void> [SocketActionTypes.ADD_CSR]: EmitEvent [SocketActionTypes.SET_COMMUNITY_METADATA]: EmitEvent void> [SocketActionTypes.SET_COMMUNITY_CA_DATA]: EmitEvent [SocketActionTypes.SET_USER_PROFILE]: EmitEvent + [SocketActionTypes.LOAD_MIGRATION_DATA]: EmitEvent> } export type Socket = IOSocket diff --git a/packages/types/src/community.ts b/packages/types/src/community.ts index e623217f01..8105c9649a 100644 --- a/packages/types/src/community.ts +++ b/packages/types/src/community.ts @@ -23,11 +23,6 @@ export enum CommunityOwnership { User = 'user', } -export interface NetworkData { - hiddenService: HiddenService - peerId: PeerId -} - export interface CreateNetworkPayload { ownership: CommunityOwnership name?: string @@ -36,6 +31,11 @@ export interface CreateNetworkPayload { ownerOrbitDbIdentity?: string } +export interface NetworkInfo { + hiddenService: HiddenService + peerId: PeerId +} + export interface Certificates { certificate: string key: string @@ -43,8 +43,8 @@ export interface Certificates { } export interface InitCommunityPayload { - network: NetworkData community: Community + network: NetworkInfo } export interface StorePeerListPayload { diff --git a/packages/types/src/connection.ts b/packages/types/src/connection.ts index 899c6c64cd..afee30991b 100644 --- a/packages/types/src/connection.ts +++ b/packages/types/src/connection.ts @@ -2,6 +2,9 @@ export type CommunityId = string export type ConnectedPeers = string[] +// FIXME: We can rename this to something like PeerConnInfo or +// PeerDisconnectedPayload if it's only used for the PEER_DISCONNECTED +// event. export interface NetworkDataPayload { peer: string connectionDuration: number diff --git a/packages/types/src/socket.ts b/packages/types/src/socket.ts index 5888f41141..e7eb4242cf 100644 --- a/packages/types/src/socket.ts +++ b/packages/types/src/socket.ts @@ -2,10 +2,16 @@ * Backend API event types. Currently, these are divided into two * groups: pure events and actions. Pure events are emitted from the * backend to notify the frontend of something and are generally named - * with the past tense (e.g. COMMUNITY_CREATED), while actions are - * emitted from the frontend in order to invoke the backend to do - * something on it's behalf and are generally named as a command (e.g. - * CREATE_COMMUNITY). + * with the past tense (e.g. COMMUNITY_CREATED) or as a noun (e.g. + * CONNECTION_PROCESS_INFO), while actions are emitted from the + * frontend in order to invoke the backend to do something on it's + * behalf and are generally named as a command (e.g. + * CREATE_COMMUNITY). Events generally don't expect a response, while + * actions tend to have a callback for returning data (using Socket.IO + * acknowledgements feature to reduce the amount of events like + * EVENT_REQUEST/EVENT_RESPONSE). + * + * NOTE: I've been adding docstrings to document the events here. */ export enum SocketActionTypes { // ====== Community ====== @@ -67,7 +73,6 @@ export enum SocketActionTypes { CONNECTION_PROCESS_INFO = 'connectionProcess', CREATE_NETWORK = 'createNetwork', LIBP2P_PSK_STORED = 'libp2pPskStored', - NETWORK_CREATED = 'networkCreated', PEER_CONNECTED = 'peerConnected', PEER_DISCONNECTED = 'peerDisconnected', PEER_LIST = 'peerList', @@ -75,6 +80,17 @@ export enum SocketActionTypes { // ====== Misc ====== + /** + * For moving data from the frontend to the backend. Load migration + * data into the backend. + */ + LOAD_MIGRATION_DATA = 'loadMigrationData', + /** + * For moving data from the frontend to the backend. The backend may + * require frontend data for migrations when loading an existing + * community from storage. + */ + MIGRATION_DATA_REQUIRED = 'migrationDataRequired', PUSH_NOTIFICATION = 'pushNotification', ERROR = 'error', }