From d185fdfbbea13a18d2f90f4a463dd9cd50eb835c Mon Sep 17 00:00:00 2001 From: Taea <88346289+adrastaea@users.noreply.github.com> Date: Fri, 26 Apr 2024 17:26:38 -0400 Subject: [PATCH 1/5] #373 Disable spellCheck/autoCorrect on usernames and channels (#2478) * Disable spell check in userFields TextInputs * disable autocorrect on username and channel inputs * update snapshots * update changelog --- CHANGELOG.md | 4 +++- .../ChangeUsername/ChangeUsername.component.tsx | 1 + .../CreateUsername/CreateUsernameComponent.tsx | 1 + .../__tests__/__snapshots__/Chat.test.tsx.snap | 1 + .../CreateChannel/CreateChannel.component.tsx | 1 + .../CreateCommunity.component.tsx | 1 + .../__snapshots__/CreateCommunity.test.tsx.snap | 1 + .../src/components/Input/Input.component.tsx | 2 ++ .../src/components/Input/Input.stories.tsx | 16 ++++++++++++++-- .../mobile/src/components/Input/Input.test.tsx | 1 + .../mobile/src/components/Input/Input.types.ts | 1 + .../JoinCommunity/JoinCommunity.component.tsx | 1 + .../__snapshots__/JoinCommunity.test.tsx.snap | 1 + .../UsernameRegistration.component.tsx | 1 + .../Registration/UsernameRegistration.test.tsx | 2 ++ 15 files changed, 32 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 40cdf52519..b860d71cce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,9 @@ # Refactorings: -# Fixes +# Fixes: + +* Disable spellCheck/autoCorrect on non-spelling sensitive fields like usernames and channels ([#373](https://github.com/TryQuiet/quiet/issues/373)) # Chores diff --git a/packages/desktop/src/renderer/components/ChangeUsername/ChangeUsername.component.tsx b/packages/desktop/src/renderer/components/ChangeUsername/ChangeUsername.component.tsx index 55bda3ff9b..669eaa1391 100644 --- a/packages/desktop/src/renderer/components/ChangeUsername/ChangeUsername.component.tsx +++ b/packages/desktop/src/renderer/components/ChangeUsername/ChangeUsername.component.tsx @@ -244,6 +244,7 @@ export const ChangeUsername: React.FC = ({ field.onBlur() }} value={field.value} + spellCheck={false} /> )} /> diff --git a/packages/desktop/src/renderer/components/CreateUsername/CreateUsernameComponent.tsx b/packages/desktop/src/renderer/components/CreateUsername/CreateUsernameComponent.tsx index 387ac4c5b1..a445d4c858 100644 --- a/packages/desktop/src/renderer/components/CreateUsername/CreateUsernameComponent.tsx +++ b/packages/desktop/src/renderer/components/CreateUsername/CreateUsernameComponent.tsx @@ -241,6 +241,7 @@ export const CreateUsernameComponent: React.FC = ( field.onBlur() }} value={field.value} + spellCheck={false} /> )} /> diff --git a/packages/mobile/src/components/Chat/__tests__/__snapshots__/Chat.test.tsx.snap b/packages/mobile/src/components/Chat/__tests__/__snapshots__/Chat.test.tsx.snap index aaedf72be5..6c51d190d6 100644 --- a/packages/mobile/src/components/Chat/__tests__/__snapshots__/Chat.test.tsx.snap +++ b/packages/mobile/src/components/Chat/__tests__/__snapshots__/Chat.test.tsx.snap @@ -2755,6 +2755,7 @@ exports[`Chat component renders component 1`] = ` } > = ({ disabled={loading} validation={inputError} ref={inputRef} + autoCorrect={false} /> {!inputError && createChannelInput?.length !== undefined && diff --git a/packages/mobile/src/components/CreateCommunity/CreateCommunity.component.tsx b/packages/mobile/src/components/CreateCommunity/CreateCommunity.component.tsx index 3c3e700742..16d35f88a6 100644 --- a/packages/mobile/src/components/CreateCommunity/CreateCommunity.component.tsx +++ b/packages/mobile/src/components/CreateCommunity/CreateCommunity.component.tsx @@ -72,6 +72,7 @@ export const CreateCommunity: FC = ({ disabled={loading} validation={inputError} ref={inputRef} + autoCorrect={false} /> ( multiline, disabled = false, round = false, + autoCorrect = true, style, wrapperStyle, children, @@ -71,6 +72,7 @@ export const Input = forwardRef( maxLength={length} autoCapitalize={capitalize} testID={'input'} + autoCorrect={autoCorrect} /> {children} diff --git a/packages/mobile/src/components/Input/Input.stories.tsx b/packages/mobile/src/components/Input/Input.stories.tsx index bf490d8c81..8820603d30 100644 --- a/packages/mobile/src/components/Input/Input.stories.tsx +++ b/packages/mobile/src/components/Input/Input.stories.tsx @@ -7,16 +7,27 @@ storiesOf('Input', module) .add('Default', () => ) .add('Disabled', () => ) .add('Label', () => ( - + )) .add('Validation', () => ( - + )) .add('Hint', () => ( )) .add('HintValidation', () => ( @@ -25,5 +36,6 @@ storiesOf('Input', module) placeholder={'Enter a username'} validation={'Username invalid'} hint={'Your username cannot have any spaces or special characters, must be lowercase letters and numbers only.'} + autoCorrect={false} /> )) diff --git a/packages/mobile/src/components/Input/Input.test.tsx b/packages/mobile/src/components/Input/Input.test.tsx index 8d2d248cc6..a5f0c58e4d 100644 --- a/packages/mobile/src/components/Input/Input.test.tsx +++ b/packages/mobile/src/components/Input/Input.test.tsx @@ -60,6 +60,7 @@ describe('MessageInput component', () => { } > = ({ disabled={loading} validation={inputError} ref={inputRef} + autoCorrect={false} /> = ({ ref={inputRef} length={20} capitalize={'none'} + autoCorrect={false} /> {!inputError && userName !== undefined && userName.length > 0 && parsedNameDiffers && ( diff --git a/packages/mobile/src/components/Registration/UsernameRegistration.test.tsx b/packages/mobile/src/components/Registration/UsernameRegistration.test.tsx index b9b0fc51fa..cd9f06743e 100644 --- a/packages/mobile/src/components/Registration/UsernameRegistration.test.tsx +++ b/packages/mobile/src/components/Registration/UsernameRegistration.test.tsx @@ -134,6 +134,7 @@ describe('UsernameRegistration', () => { > { > Date: Thu, 18 Apr 2024 17:59:00 -0400 Subject: [PATCH 2/5] Fix(2321): No Duplicate CSRs (#2460) * Fix issue of duplicate CRSs being generated * Install LFS with checkout to avoid random errors --- .github/workflows/e2e-android.yml | 4 +++- .github/workflows/e2e-ios.yml | 4 +++- CHANGELOG.md | 1 + .../storage/certifacteRequests/certificatesRequestsStore.ts | 1 + packages/backend/src/nest/storage/storage.service.ts | 2 +- 5 files changed, 9 insertions(+), 3 deletions(-) diff --git a/.github/workflows/e2e-android.yml b/.github/workflows/e2e-android.yml index 7a1826b096..d6dfa04132 100644 --- a/.github/workflows/e2e-android.yml +++ b/.github/workflows/e2e-android.yml @@ -9,7 +9,9 @@ jobs: steps: - uses: actions/checkout@v4 - + with: + lfs: true + - uses: actions/setup-node@master with: node-version: 18.12.1 diff --git a/.github/workflows/e2e-ios.yml b/.github/workflows/e2e-ios.yml index 40e4a43dc7..e497c5bdf3 100644 --- a/.github/workflows/e2e-ios.yml +++ b/.github/workflows/e2e-ios.yml @@ -9,7 +9,9 @@ jobs: steps: - uses: actions/checkout@v4 - + with: + lfs: true + - uses: actions/setup-node@master with: node-version: 18.12.1 diff --git a/CHANGELOG.md b/CHANGELOG.md index b860d71cce..bf252ece2a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ # Chores * Cleanup data directory at end of e2e tests +* Don't create duplicate CSRs when joining a community under certain circumstances ([#2321](https://github.com/TryQuiet/quiet/issues/2321)) [2.2.0] diff --git a/packages/backend/src/nest/storage/certifacteRequests/certificatesRequestsStore.ts b/packages/backend/src/nest/storage/certifacteRequests/certificatesRequestsStore.ts index e070fda53a..6f415738a6 100644 --- a/packages/backend/src/nest/storage/certifacteRequests/certificatesRequestsStore.ts +++ b/packages/backend/src/nest/storage/certifacteRequests/certificatesRequestsStore.ts @@ -29,6 +29,7 @@ export class CertificatesRequestsStore extends EventEmitter { write: ['*'], }, }) + await this.store.load() this.store.events.on('write', async (_address, entry) => { this.logger('Added CSR to database') diff --git a/packages/backend/src/nest/storage/storage.service.ts b/packages/backend/src/nest/storage/storage.service.ts index 2173581d63..7c5121a81d 100644 --- a/packages/backend/src/nest/storage/storage.service.ts +++ b/packages/backend/src/nest/storage/storage.service.ts @@ -313,7 +313,7 @@ export class StorageService extends EventEmitter { public async updatePeersList() { const users = this.getAllUsers() - const peers = users.map(peer => createLibp2pAddress(peer.onionAddress, peer.peerId)) + const peers = Array.from(new Set(users.map(peer => createLibp2pAddress(peer.onionAddress, peer.peerId)))) console.log('updatePeersList, peers count:', peers.length) const community = await this.localDbService.getCurrentCommunity() From abd9101f84149ae4ec1db3038fca31880334cdf3 Mon Sep 17 00:00:00 2001 From: Lucas Leblow Date: Mon, 29 Apr 2024 07:58:05 -0600 Subject: [PATCH 3/5] fix: Various fixes related to peers, CSRs and backend startup (#2455) Fixes for the following issues: - Peers can be deleted if CSRs don't sync - Backend starting before the frontend is ready, resulting in missed events - Adding duplicate CSRs --- .../connections-manager.service.ts | 3 +- .../src/nest/local-db/local-db.service.ts | 18 +-- .../nest/registration/registration.service.ts | 10 +- .../backend/src/nest/socket/socket.service.ts | 14 +- .../certificatesRequestsStore.ts | 12 +- .../certificates/certificates.store.spec.ts | 3 - .../certificates/certificates.store.ts | 5 +- .../src/nest/storage/storage.service.spec.ts | 49 +++++- .../src/nest/storage/storage.service.ts | 91 ++++++++--- .../src/renderer/sagas/socket/socket.saga.ts | 8 +- .../src/rtl-tests/channel.main.test.tsx | 1 + .../src/rtl-tests/community.create.test.tsx | 2 +- .../src/rtl-tests/community.join.test.tsx | 3 +- packages/identity/src/extractPubKey.ts | 1 + .../src/integrationTests/appActions.ts | 3 +- .../src/screens/Channel/Channel.screen.tsx | 2 +- .../startConnection/startConnection.saga.ts | 7 +- .../createCommunity/createCommunity.saga.ts | 10 +- .../launchCommunity/launchCommunity.saga.ts | 6 +- .../checkLocalCsr/checkLocalCsr.saga.test.ts | 103 ------------ .../checkLocalCsr/checkLocalCsr.saga.ts | 45 ------ .../sagas/identity/identity.master.saga.ts | 4 - .../src/sagas/identity/identity.slice.ts | 5 +- .../registerCertificate.saga.test.ts | 146 ------------------ .../registerCertificate.saga.ts | 34 ---- .../registerUsername.saga.test.ts | 8 +- .../registerUsername/registerUsername.saga.ts | 26 +++- .../identity/saveUserCsr/saveUserCsr.saga.ts | 2 + .../src/sagas/messages/messages.slice.ts | 15 +- .../messages/sendMessage/sendMessage.saga.ts | 46 +++--- .../publicChannels.selectors.ts | 2 + .../startConnection/startConnection.saga.ts | 3 - .../src/sagas/users/users.selectors.ts | 2 +- packages/state-manager/src/types.ts | 1 + packages/types/src/socket.ts | 6 + 35 files changed, 256 insertions(+), 440 deletions(-) delete mode 100644 packages/state-manager/src/sagas/identity/checkLocalCsr/checkLocalCsr.saga.test.ts delete mode 100644 packages/state-manager/src/sagas/identity/checkLocalCsr/checkLocalCsr.saga.ts delete mode 100644 packages/state-manager/src/sagas/identity/registerCertificate/registerCertificate.saga.test.ts delete mode 100644 packages/state-manager/src/sagas/identity/registerCertificate/registerCertificate.saga.ts 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 7800d75b4c..0489557f08 100644 --- a/packages/backend/src/nest/connections-manager/connections-manager.service.ts +++ b/packages/backend/src/nest/connections-manager/connections-manager.service.ts @@ -209,7 +209,7 @@ export class ConnectionsManagerService extends EventEmitter implements OnModuleI const network = await this.localDbService.getNetworkInfo() if (community && network) { - const sortedPeers = await this.localDbService.getSortedPeers(community.peerList) + const sortedPeers = await this.localDbService.getSortedPeers(community.peerList ?? []) this.logger('launchCommunityFromStorage - sorted peers', sortedPeers) if (sortedPeers.length > 0) { community.peerList = sortedPeers @@ -558,6 +558,7 @@ export class ConnectionsManagerService extends EventEmitter implements OnModuleI agent: this.socksProxyAgent, localAddress: this.libp2pService.createLibp2pAddress(onionAddress, peerId.toString()), targetPort: this.ports.libp2pHiddenService, + // Ignore local address peers: peers ? peers.slice(1) : [], psk: Libp2pService.generateLibp2pPSK(community.psk).fullKey, } 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 ba3d796ed1..e3a94b4be9 100644 --- a/packages/backend/src/nest/local-db/local-db.service.ts +++ b/packages/backend/src/nest/local-db/local-db.service.ts @@ -95,21 +95,9 @@ export class LocalDbService { } } - public async getSortedPeers( - peers?: string[] | undefined, - includeLocalPeerAddress: boolean = true - ): Promise { - if (!peers) { - const currentCommunity = await this.getCurrentCommunity() - if (!currentCommunity) { - throw new Error('No peers were provided and no community was found to extract peers from') - } - peers = currentCommunity.peerList - if (!peers) { - throw new Error('No peers provided and no peers found on current stored community') - } - } - + // I think we can move this into StorageService (keep this service + // focused on CRUD). + public async getSortedPeers(peers: string[], includeLocalPeerAddress: boolean = true): Promise { const peersStats = (await this.get(LocalDBKeys.PEERS)) || {} const stats: NetworkStats[] = Object.values(peersStats) const network = await this.getNetworkInfo() diff --git a/packages/backend/src/nest/registration/registration.service.ts b/packages/backend/src/nest/registration/registration.service.ts index 87afbada72..17cd8ff838 100644 --- a/packages/backend/src/nest/registration/registration.service.ts +++ b/packages/backend/src/nest/registration/registration.service.ts @@ -44,14 +44,14 @@ export class RegistrationService extends EventEmitter implements OnModuleInit { } public async tryIssueCertificates() { - this.logger('Trying to issue certificates', this.registrationEventInProgress, this.registrationEvents) + this.logger('Trying to process registration event') // Process only a single registration event at a time so that we // do not register two certificates with the same name. if (!this.registrationEventInProgress) { // Get the next event. const event = this.registrationEvents.shift() if (event) { - this.logger('Issuing certificates', event) + this.logger('Processing registration event', event) // Event processing in progress this.registrationEventInProgress = true @@ -62,6 +62,7 @@ export class RegistrationService extends EventEmitter implements OnModuleInit { certificates: (await this.storageService?.loadAllCertificates()) as string[], }) + this.logger('Finished processing registration event') // Event processing finished this.registrationEventInProgress = false @@ -70,6 +71,8 @@ export class RegistrationService extends EventEmitter implements OnModuleInit { setTimeout(this.tryIssueCertificates.bind(this), 0) } } + } else { + this.logger('Registration event processing already in progress') } } @@ -90,12 +93,13 @@ export class RegistrationService extends EventEmitter implements OnModuleInit { } const pendingCsrs = await extractPendingCsrs(payload) + this.logger(`Issuing certificates`) await Promise.all( pendingCsrs.map(async csr => { await this.registerUserCertificate(csr) }) ) - this.logger('Finished issuing certificates') + this.logger('Total certificates issued:', pendingCsrs.length) } // TODO: This doesn't save the owner's certificate in OrbitDB, so perhaps we diff --git a/packages/backend/src/nest/socket/socket.service.ts b/packages/backend/src/nest/socket/socket.service.ts index bd5b5ce408..e0df314509 100644 --- a/packages/backend/src/nest/socket/socket.service.ts +++ b/packages/backend/src/nest/socket/socket.service.ts @@ -49,36 +49,40 @@ export class SocketService extends EventEmitter implements OnModuleInit { } async onModuleInit() { - this.logger('init:started') + this.logger('init: Started') this.attachListeners() await this.init() - this.logger('init:finished') + this.logger('init: Finished') } public async init() { const connection = new Promise(resolve => { this.serverIoProvider.io.on(SocketActionTypes.CONNECTION, socket => { - this.logger('init: connection') - resolve() + socket.on(SocketActionTypes.START, async () => { + resolve() + }) }) }) await this.listen() + this.logger('init: Waiting for frontend to connect') await connection + this.logger('init: Frontend connected') } private readonly attachListeners = (): void => { // Attach listeners here this.serverIoProvider.io.on(SocketActionTypes.CONNECTION, socket => { - this.logger('socket connection') + this.logger('Socket connection') // On websocket connection, update presentation service with network data this.emit(SocketActionTypes.CONNECTION) socket.on(SocketActionTypes.CLOSE, async () => { + this.logger('Socket connection closed') this.emit(SocketActionTypes.CLOSE) }) diff --git a/packages/backend/src/nest/storage/certifacteRequests/certificatesRequestsStore.ts b/packages/backend/src/nest/storage/certifacteRequests/certificatesRequestsStore.ts index 6f415738a6..be2d6c54b5 100644 --- a/packages/backend/src/nest/storage/certifacteRequests/certificatesRequestsStore.ts +++ b/packages/backend/src/nest/storage/certifacteRequests/certificatesRequestsStore.ts @@ -29,7 +29,6 @@ export class CertificatesRequestsStore extends EventEmitter { write: ['*'], }, }) - await this.store.load() this.store.events.on('write', async (_address, entry) => { this.logger('Added CSR to database') @@ -41,8 +40,8 @@ export class CertificatesRequestsStore extends EventEmitter { this.loadedCertificateRequests() }) - // TODO: Load CSRs in case the owner closes the app before issuing - // certificates + // @ts-ignore + await this.store.load({ fetchEntryTimeout: 15000 }) this.logger('Initialized') } @@ -77,7 +76,7 @@ export class CertificatesRequestsStore extends EventEmitter { await parsedCsr.verify() await this.validateCsrFormat(csr) } catch (err) { - console.error('Failed to validate user csr:', csr, err?.message) + console.error('Failed to validate user CSR:', csr, err?.message) return false } return true @@ -100,6 +99,7 @@ export class CertificatesRequestsStore extends EventEmitter { .map(e => { return e.payload.value }) + this.logger('Total CSRs:', allEntries.length) const allCsrsUnique = [...new Set(allEntries)] await Promise.all( @@ -119,7 +119,9 @@ export class CertificatesRequestsStore extends EventEmitter { filteredCsrsMap.set(pubKey, csr) }) ) - return [...filteredCsrsMap.values()] + const validCsrs = [...filteredCsrsMap.values()] + this.logger('Valid CSRs:', validCsrs.length) + return validCsrs } public clean() { diff --git a/packages/backend/src/nest/storage/certificates/certificates.store.spec.ts b/packages/backend/src/nest/storage/certificates/certificates.store.spec.ts index 104c1a19bc..bf0a1b1d54 100644 --- a/packages/backend/src/nest/storage/certificates/certificates.store.spec.ts +++ b/packages/backend/src/nest/storage/certificates/certificates.store.spec.ts @@ -135,7 +135,6 @@ describe('CertificatesStore', () => { await certificatesStore.addCertificate(certificate) - // @ts-expect-error - getCertificates is protected const certificates = await certificatesStore.getCertificates() expect(certificates).toContain(certificate) @@ -149,7 +148,6 @@ describe('CertificatesStore', () => { await certificatesStore.addCertificate(certificate) - // @ts-expect-error - getCertificates is protected const certificates = await certificatesStore.getCertificates() expect(certificates).not.toContain(certificate) @@ -161,7 +159,6 @@ describe('CertificatesStore', () => { certificatesStore.updateMetadata(communityMetadata) - // @ts-expect-error - getCertificates is protected jest.spyOn(certificatesStore, 'getCertificates').mockResolvedValue([certificate1, certificate2]) const certificates = await certificatesStore.loadAllCertificates() diff --git a/packages/backend/src/nest/storage/certificates/certificates.store.ts b/packages/backend/src/nest/storage/certificates/certificates.store.ts index e9c833b8d2..6341d51d0e 100644 --- a/packages/backend/src/nest/storage/certificates/certificates.store.ts +++ b/packages/backend/src/nest/storage/certificates/certificates.store.ts @@ -146,7 +146,7 @@ export class CertificatesStore extends EventEmitter { * as specified in the comment section of * https://github.com/TryQuiet/quiet/issues/1899 */ - protected async getCertificates() { + public async getCertificates(): Promise { if (!this.store) { return [] } @@ -189,7 +189,8 @@ export class CertificatesStore extends EventEmitter { const validCerts = validCertificates.filter(i => i != undefined) this.logger(`Valid certificates: ${validCerts.length}`) - return validCerts + // TODO: Why doesn't TS infer this properly? + return validCerts as string[] } public async getCertificateUsername(pubkey: string) { diff --git a/packages/backend/src/nest/storage/storage.service.spec.ts b/packages/backend/src/nest/storage/storage.service.spec.ts index c07906efe5..9893376b79 100644 --- a/packages/backend/src/nest/storage/storage.service.spec.ts +++ b/packages/backend/src/nest/storage/storage.service.spec.ts @@ -514,17 +514,52 @@ describe('StorageService', () => { describe('Users', () => { it('gets all users from db', async () => { - await storageService.init(peerId) - const mockGetCsrs = jest.fn() - // @ts-ignore - Property 'getAllEventLogEntries' is protected - storageService.getAllEventLogEntries = mockGetCsrs - mockGetCsrs.mockReturnValue([ + const certs = [ + // b + 'MIICITCCAcegAwIBAgIGAY8GkBEVMAoGCCqGSM49BAMCMAwxCjAIBgNVBAMTAWEwHhcNMjQwNDIyMTYwNzM1WhcNMzAwMjAxMDcwMDAwWjBJMUcwRQYDVQQDEz56Z2hpZGV4czdxdDI0aXZ1M2pvYmpxZHR6end0eWF1NGxwcG51bng1cGtpZjc2cGtweXA3cWNpZC5vbmlvbjBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABDG8SNnoS1BYoV72jcyQFVlsrwvd2Bb9/9L13Tc4SHJwitTUB3F+y/7pk0tAPrZi2qasU2PO9lTwUxXYcAfpCRSjgdcwgdQwCQYDVR0TBAIwADALBgNVHQ8EBAMCAIAwHQYDVR0lBBYwFAYIKwYBBQUHAwIGCCsGAQUFBwMBMBEGCisGAQQBg4wbAgEEAxMBYjA9BgkrBgECAQ8DAQEEMBMuUW1lUGJCMjVoMWZYN1dBRk42ckZSNGFWRFdVRlFNU3RSSEdERFM0UlFaUTRZcTBJBgNVHREEQjBAgj56Z2hpZGV4czdxdDI0aXZ1M2pvYmpxZHR6end0eWF1NGxwcG51bng1cGtpZjc2cGtweXA3cWNpZC5vbmlvbjAKBggqhkjOPQQDAgNIADBFAiBkTZo6/D0YgNMPcDpuf7n+rDEQls6cMVxEVw/H8vxbhwIhAM+e6we9YP4JeNgOGgd0iZNEpq8N7dla4XO+YVWrh0YG', + + // c + 'MIICITCCAcegAwIBAgIGAY8Glf+pMAoGCCqGSM49BAMCMAwxCjAIBgNVBAMTAWEwHhcNMjQwNDIyMTYxNDA0WhcNMzAwMjAxMDcwMDAwWjBJMUcwRQYDVQQDEz5uaGxpdWpuNjZlMzQ2ZXZ2dnhlYWY0cW1hN3Bxc2hjZ2J1NnQ3d3Nlb2FubmMyZnk0Y25zY3J5ZC5vbmlvbjBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABP1WBKQdMz5yMpv5hWj6j+auIsnfiJE8dtuxeeM4N03K1An61F0o47CWg04DydwmoPn5gwefEv8t9Cz9nv/VUGejgdcwgdQwCQYDVR0TBAIwADALBgNVHQ8EBAMCAIAwHQYDVR0lBBYwFAYIKwYBBQUHAwIGCCsGAQUFBwMBMBEGCisGAQQBg4wbAgEEAxMBYzA9BgkrBgECAQ8DAQEEMBMuUW1WY1hRTXVmRWNZS0R0d3NFSlRIUGJzc3BCeU02U0hUYlJHR2VEdkVFdU1RQTBJBgNVHREEQjBAgj5uaGxpdWpuNjZlMzQ2ZXZ2dnhlYWY0cW1hN3Bxc2hjZ2J1NnQ3d3Nlb2FubmMyZnk0Y25zY3J5ZC5vbmlvbjAKBggqhkjOPQQDAgNIADBFAiEAgMCBxF3oK4ituEWcAK6uawMCludZu4YujIpBIR+v2LICIBhMHXrBy1KWc70t6idB+5XkInsRZz5nw1vwgRJ4mw98', + ] + + const csrs = [ + // c + 'MIIB4TCCAYgCAQAwSTFHMEUGA1UEAxM+emdoaWRleHM3cXQyNGl2dTNqb2JqcWR0enp3dHlhdTRscHBudW54NXBraWY3NnBrcHlwN3FjaWQub25pb24wWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQxvEjZ6EtQWKFe9o3MkBVZbK8L3dgW/f/S9d03OEhycIrU1Adxfsv+6ZNLQD62YtqmrFNjzvZU8FMV2HAH6QkUoIHcMC4GCSqGSIb3DQEJDjEhMB8wHQYDVR0OBBYEFG1W6vJTK/uPuRK2LPaVZyebVVc+MA8GCSqGSIb3DQEJDDECBAAwEQYKKwYBBAGDjBsCATEDEwFiMD0GCSsGAQIBDwMBATEwEy5RbWVQYkIyNWgxZlg3V0FGTjZyRlI0YVZEV1VGUU1TdFJIR0REUzRSUVpRNFlxMEcGA1UdETFAEz56Z2hpZGV4czdxdDI0aXZ1M2pvYmpxZHR6end0eWF1NGxwcG51bng1cGtpZjc2cGtweXA3cWNpZC5vbmlvbjAKBggqhkjOPQQDAgNHADBEAiAjxneoJZtCzkd75HTT+pcj+objG3S04omjeMMw1N+B/wIgAaJRgifnWEnWFYm614UmPw9un2Uwk1gVhN2tSwJ65sM=', + + // o 'MIIDHjCCAsMCAQAwSTFHMEUGA1UEAxM+NnZ1MmJ4a2k3NzdpdDNjcGF5djZmcTZ2cGw0a2Uza3pqN2d4aWNmeWdtNTVkaGh0cGh5ZmR2eWQub25pb24wWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAATMpfp2hSfWFL26OZlZKZEWG9fyAM1ndlEzO0kLxT0pA/7/fs+a5X/s4TkzqCVVQSzhas/84q0WE99ScAcM1LQJoIICFjAuBgkqhkiG9w0BCQ4xITAfMB0GA1UdDgQWBBR6VRzktP1pzZxsGUaJivNUrtgSrzCCAUcGCSqGSIb3DQEJDDGCATgEggE0KZq9s6HEViRfplVgYkulg6XV411ZRe4U1UjfXTf1pRaygfcenGbT6RRagPtZzjuq5hHdYhqDjRzZhnbn8ZASYTgBM7qcseUq5UpS1pE08DI2jePKqatp3Pzm6a/MGSziESnREx784JlKfwKMjJl33UA8lQm9nhSeAIHyBx3c4Lf8IXdW2n3rnhbVfjpBMAxwh6lt+e5agtGXy+q/xAESUeLPfUgRYWctlLgt8Op+WTpLyBkZsVFoBvJrMt2XdM0RI32YzTRr56GXFa4VyQmY5xXwlQSPgidAP7jPkVygNcoeXvAz2ZCk3IR1Cn3mX8nMko53MlDNaMYldUQA0ug28/S7BlSlaq2CDD4Ol3swTq7C4KGTxKrI36ruYUZx7NEaQDF5V7VvqPCZ0fZoTIJuSYTQ67gwEQYKKwYBBAGDjBsCATEDEwFvMD0GCSsGAQIBDwMBATEwEy5RbVhSWTRyaEF4OE11cThkTUdrcjlxa25KZEU2VUhaRGRHYURSVFFFYndGTjViMEcGA1UdETFAEz42dnUyYnhraTc3N2l0M2NwYXl2NmZxNnZwbDRrZTNremo3Z3hpY2Z5Z201NWRoaHRwaHlmZHZ5ZC5vbmlvbjAKBggqhkjOPQQDAgNJADBGAiEAt+f1u/bchg5AZHv6NTGNoXeejTRWUhX3ioGwW6TGg84CIQCHqKNzDh2JjS/hUHx5PApAmfNnQTSf19X6LnNHQweU1g==', + + // o 'MIIDHTCCAsMCAQAwSTFHMEUGA1UEAxM+eTd5Y3ptdWdsMnRla2FtaTdzYmR6NXBmYWVtdng3YmFod3RocmR2Y2J6dzV2ZXgyY3JzcjI2cWQub25pb24wWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAATMq0l4bCmjdb0grtzpwtDVLM9E1IQpL9vrB4+lD9OBZzlrx2365jV7shVu9utas8w8fxtKoBZSnT5+32ZMFTB4oIICFjAuBgkqhkiG9w0BCQ4xITAfMB0GA1UdDgQWBBSoDQpTZdEvi1/Rr/muVXT1clyKRDCCAUcGCSqGSIb3DQEJDDGCATgEggE0BQvyvkiiXEf/PLKnsR1Ba9AhYsVO8o56bnftUnoVzBlRZgUzLJvOSroPk/EmbVz+okhMrcYNgCWHvxrAqHVVq0JRP6bi98BtCUotx6OPFHp5K5QCL60hod1uAnhKocyJG9tsoM9aS+krn/k+g4RCBjiPZ25cC7QG/UNr6wyIQ8elBho4MKm8iOp7EShSsZOV1f6xrnXYCC/zyUc85GEuycLzVImgAQvPATbdMzY4zSGnNLHxkvSUNxaR9LnEWf+i1jeqcOiXOvmdyU5Be3ZqhGKvvBg/5vyLQiCIfeapjZemnLqFHQBitglDm2xnKL6HzMyfZoAHPV7YcWYR4spU9Ju8Q8aqSeAryx7sx55eSR4GO5UQTo5DrQn6xtkwOZ/ytsOknFthF8jcA9uTAMDKA2TylCUwEQYKKwYBBAGDjBsCATEDEwFvMD0GCSsGAQIBDwMBATEwEy5RbVQxOFV2blVCa3NlTWMzU3FuZlB4cEh3TjhuekxySmVOU0xadGM4ckFGWGh6MEcGA1UdETFAEz55N3ljem11Z2wydGVrYW1pN3NiZHo1cGZhZW12eDdiYWh3dGhyZHZjYnp3NXZleDJjcnNyMjZxZC5vbmlvbjAKBggqhkjOPQQDAgNIADBFAiEAoFrAglxmk7ciD6AHQOB1qEoLu0NARcxgwmIry8oeTHwCICyXp5NJQ9Z8vReIAQNng2H2+/XjHifZEWzhoN0VkcBx', - ]) - const allUsers = storageService.getAllUsers() + ] + + await storageService.init(peerId) + // @ts-ignore + storageService.certificatesRequestsStore = { + getCsrs: jest.fn(() => { + return csrs + }), + } + // @ts-ignore + storageService.certificatesStore = { + getCertificates: jest.fn(() => { + return certs + }), + } + + const allUsers = await storageService.getAllUsers() expect(allUsers).toStrictEqual([ + { + onionAddress: 'zghidexs7qt24ivu3jobjqdtzzwtyau4lppnunx5pkif76pkpyp7qcid.onion', + peerId: 'QmePbB25h1fX7WAFN6rFR4aVDWUFQMStRHGDDS4RQZQ4Yq', + username: 'b', + }, + { + onionAddress: 'nhliujn66e346evvvxeaf4qma7pqshcgbu6t7wseoannc2fy4cnscryd.onion', + peerId: 'QmVcXQMufEcYKDtwsEJTHPbsspByM6SHTbRGGeDvEEuMQA', + username: 'c', + }, { onionAddress: '6vu2bxki777it3cpayv6fq6vpl4ke3kzj7gxicfygm55dhhtphyfdvyd.onion', peerId: 'QmXRY4rhAx8Muq8dMGkr9qknJdE6UHZDdGaDRTQEbwFN5b', diff --git a/packages/backend/src/nest/storage/storage.service.ts b/packages/backend/src/nest/storage/storage.service.ts index 7c5121a81d..14df293e8a 100644 --- a/packages/backend/src/nest/storage/storage.service.ts +++ b/packages/backend/src/nest/storage/storage.service.ts @@ -7,6 +7,7 @@ import { parseCertificationRequest, getCertFieldValue, getReqFieldValue, + keyFromCertificate, } from '@quiet/identity' import type { IPFS } from 'ipfs-core' import EventStore from 'orbit-db-eventstore' @@ -286,11 +287,15 @@ export class StorageService extends EventEmitter { this.certificatesStore.on(StorageEvents.CERTIFICATES_STORED, async payload => { this.emit(StorageEvents.CERTIFICATES_STORED, payload) await this.updatePeersList() + // TODO: Shouldn't we also dial new peers or at least add them + // to the peer store for the auto-dialer to handle? }) this.certificatesRequestsStore.on(StorageEvents.CSRS_STORED, async (payload: { csrs: string[] }) => { this.emit(StorageEvents.CSRS_STORED, payload) await this.updatePeersList() + // TODO: Shouldn't we also dial new peers or at least add them + // to the peer store for the auto-dialer to handle? }) this.communityMetadataStore.on(StorageEvents.COMMUNITY_METADATA_STORED, (meta: CommunityMetadata) => { @@ -312,18 +317,30 @@ export class StorageService extends EventEmitter { } public async updatePeersList() { - const users = this.getAllUsers() - const peers = Array.from(new Set(users.map(peer => createLibp2pAddress(peer.onionAddress, peer.peerId)))) - console.log('updatePeersList, peers count:', peers.length) - const community = await this.localDbService.getCurrentCommunity() - if (!community) return + if (!community) { + throw new Error('Failed to update peers list - community missing') + } + + // Always include existing peers. Otherwise, if CSRs or + // certificates do not replicate, then this could remove peers. + const existingPeers = community.peerList ?? [] + this.logger('Existing peers count:', existingPeers.length) + const users = await this.getAllUsers() + const peers = Array.from( + new Set([...existingPeers, ...users.map(user => createLibp2pAddress(user.onionAddress, user.peerId))]) + ) const sortedPeers = await this.localDbService.getSortedPeers(peers) - if (sortedPeers.length > 0) { - community.peerList = sortedPeers - await this.localDbService.setCommunity(community) + + // This should never happen, but just in case + if (sortedPeers.length === 0) { + throw new Error('Failed to update peers list - no peers') } + + this.logger('Updating community peer list. Peers count:', sortedPeers.length) + community.peerList = sortedPeers + await this.localDbService.setCommunity(community) this.emit(StorageEvents.COMMUNITY_UPDATED, community) } @@ -728,18 +745,56 @@ export class StorageService extends EventEmitter { return result } - public getAllUsers(): UserData[] { - const csrs = this.getAllEventLogEntries(this.certificatesRequestsStore.store) - this.logger('csrs count:', csrs.length) - const allUsers: UserData[] = [] + /** + * Retrieve all users (using certificates and CSRs to determine users) + */ + public async getAllUsers(): Promise { + const csrs = await this.certificatesRequestsStore.getCsrs() + const certs = await this.certificatesStore.getCertificates() + const allUsersByKey: Record = {} + + this.logger(`Retrieving all users. CSRs count: ${csrs.length} Certificates count: ${certs.length}`) + + for (const cert of certs) { + const parsedCert = parseCertificate(cert) + const pubKey = keyFromCertificate(parsedCert) + const onionAddress = getCertFieldValue(parsedCert, CertFieldsTypes.commonName) + const peerId = getCertFieldValue(parsedCert, CertFieldsTypes.peerId) + const username = getCertFieldValue(parsedCert, CertFieldsTypes.nickName) + + // TODO: This validation should go in CertificatesStore + if (!pubKey || !onionAddress || !peerId || !username) { + this.logger.error( + `Received invalid certificate. onionAddress: ${onionAddress} peerId: ${peerId} username: ${username}` + ) + continue + } + + allUsersByKey[pubKey] = { onionAddress, peerId, username } + } + for (const csr of csrs) { - const parsedCert = parseCertificationRequest(csr) - const onionAddress = getReqFieldValue(parsedCert, CertFieldsTypes.commonName) - const peerId = getReqFieldValue(parsedCert, CertFieldsTypes.peerId) - const username = getReqFieldValue(parsedCert, CertFieldsTypes.nickName) - if (!onionAddress || !peerId || !username) continue - allUsers.push({ onionAddress, peerId, username }) + const parsedCsr = parseCertificationRequest(csr) + const pubKey = keyFromCertificate(parsedCsr) + const onionAddress = getReqFieldValue(parsedCsr, CertFieldsTypes.commonName) + const peerId = getReqFieldValue(parsedCsr, CertFieldsTypes.peerId) + const username = getReqFieldValue(parsedCsr, CertFieldsTypes.nickName) + + // TODO: This validation should go in CertificatesRequestsStore + if (!pubKey || !onionAddress || !peerId || !username) { + this.logger.error(`Received invalid CSR. onionAddres: ${onionAddress} peerId: ${peerId} username: ${username}`) + continue + } + + if (!(pubKey in allUsersByKey)) { + allUsersByKey[pubKey] = { onionAddress, peerId, username } + } } + + const allUsers = Object.values(allUsersByKey) + + this.logger(`All users count: ${allUsers.length}`) + return allUsers } diff --git a/packages/desktop/src/renderer/sagas/socket/socket.saga.ts b/packages/desktop/src/renderer/sagas/socket/socket.saga.ts index beaec5d543..2268578ac9 100644 --- a/packages/desktop/src/renderer/sagas/socket/socket.saga.ts +++ b/packages/desktop/src/renderer/sagas/socket/socket.saga.ts @@ -1,5 +1,5 @@ import { io } from 'socket.io-client' -import { all, fork, takeEvery, call, put, cancel, FixedTask, select, take } from 'typed-redux-saga' +import { all, fork, takeEvery, call, put, cancel, FixedTask, select, take, delay, apply } from 'typed-redux-saga' import { PayloadAction } from '@reduxjs/toolkit' import { socket as stateManager, messages, connection, Socket } from '@quiet/state-manager' import { socketActions } from './socket.slice' @@ -7,6 +7,7 @@ import { eventChannel } from 'redux-saga' import { displayMessageNotificationSaga } from '../notifications/notifications.saga' import logger from '../../logger' import { encodeSecret } from '@quiet/common' +import { SocketActionTypes } from '@quiet/types' const log = logger('socket') @@ -27,6 +28,7 @@ export function* startConnectionSaga( if (!socketIOSecret) return + log('Connecting to backend') const token = encodeSecret(socketIOSecret) const socket = yield* call(io, `http://127.0.0.1:${dataPort}`, { withCredentials: true, @@ -43,6 +45,10 @@ export function* startConnectionSaga( function* setConnectedSaga(socket: Socket): Generator { const root = yield* fork(stateManager.useIO, socket) const observers = yield* fork(initObservers) + + console.log('Frontend is ready. Starting backend...') + yield* apply(socket, socket.emit, [SocketActionTypes.START]) + // Handle suspending current connection yield all([ takeEvery(socketActions.suspendConnection, cancelRootSaga, root), diff --git a/packages/desktop/src/rtl-tests/channel.main.test.tsx b/packages/desktop/src/rtl-tests/channel.main.test.tsx index c4148b6ac0..4e59b48dde 100644 --- a/packages/desktop/src/rtl-tests/channel.main.test.tsx +++ b/packages/desktop/src/rtl-tests/channel.main.test.tsx @@ -1029,6 +1029,7 @@ describe('Channel', () => { "Messages/lazyLoading", "Messages/resetCurrentPublicChannelCache", "Messages/resetCurrentPublicChannelCache", + "Identity/saveUserCsr", "Files/updateMessageMedia", "Messages/addMessages", "Messages/addMessageVerificationStatus", diff --git a/packages/desktop/src/rtl-tests/community.create.test.tsx b/packages/desktop/src/rtl-tests/community.create.test.tsx index 9d225ae667..467c422725 100644 --- a/packages/desktop/src/rtl-tests/community.create.test.tsx +++ b/packages/desktop/src/rtl-tests/community.create.test.tsx @@ -159,7 +159,7 @@ describe('User', () => { "Identity/registerUsername", "Network/setLoadingPanelType", "Modals/openModal", - "Identity/registerCertificate", + "Identity/addCsr", "Communities/createCommunity", "Files/checkForMissingFiles", "Network/addInitializedCommunity", diff --git a/packages/desktop/src/rtl-tests/community.join.test.tsx b/packages/desktop/src/rtl-tests/community.join.test.tsx index 0f62b173eb..6e699d288e 100644 --- a/packages/desktop/src/rtl-tests/community.join.test.tsx +++ b/packages/desktop/src/rtl-tests/community.join.test.tsx @@ -175,7 +175,7 @@ describe('User', () => { "Identity/registerUsername", "Network/setLoadingPanelType", "Modals/openModal", - "Identity/registerCertificate", + "Identity/addCsr", "Communities/launchCommunity", "Files/checkForMissingFiles", "Network/addInitializedCommunity", @@ -191,6 +191,7 @@ describe('User', () => { "Messages/lazyLoading", "Messages/resetCurrentPublicChannelCache", "Messages/resetCurrentPublicChannelCache", + "Identity/saveUserCsr", "Messages/addMessagesSendingStatus", "Messages/addMessageVerificationStatus", "Messages/addMessages", diff --git a/packages/identity/src/extractPubKey.ts b/packages/identity/src/extractPubKey.ts index d4bb0230c0..6f66224818 100644 --- a/packages/identity/src/extractPubKey.ts +++ b/packages/identity/src/extractPubKey.ts @@ -8,6 +8,7 @@ import config from './config' import { getAlgorithmParameters, Certificate, CertificationRequest, getCrypto } from 'pkijs' import { NoCryptoEngineError } from '@quiet/types' +// FIXME: This is a duplicate of loadCertificate export const parseCertificate = (pem: string): Certificate => { let certificateBuffer = new ArrayBuffer(0) certificateBuffer = stringToArrayBuffer(fromBase64(pem)) diff --git a/packages/integration-tests/src/integrationTests/appActions.ts b/packages/integration-tests/src/integrationTests/appActions.ts index dd789afdb7..1404683cb1 100644 --- a/packages/integration-tests/src/integrationTests/appActions.ts +++ b/packages/integration-tests/src/integrationTests/appActions.ts @@ -162,7 +162,8 @@ export async function sendCsr(store: Store) { userCsr, } - store.dispatch(identity.actions.registerCertificate(csr)) + store.dispatch(identity.actions.addCsr(csr)) + store.dispatch(identity.actions.saveUserCsr()) } export async function joinCommunity(payload: JoinCommunity) { diff --git a/packages/mobile/src/screens/Channel/Channel.screen.tsx b/packages/mobile/src/screens/Channel/Channel.screen.tsx index c399e8d8d1..010e8e0289 100644 --- a/packages/mobile/src/screens/Channel/Channel.screen.tsx +++ b/packages/mobile/src/screens/Channel/Channel.screen.tsx @@ -110,7 +110,7 @@ export const ChannelScreen: FC = () => { return updatedExistingFiles }) - //User Label + // User Label const duplicatedUsernameHandleBack = useCallback(() => { dispatch( diff --git a/packages/mobile/src/store/init/startConnection/startConnection.saga.ts b/packages/mobile/src/store/init/startConnection/startConnection.saga.ts index 8b19e75084..a0abd3022a 100644 --- a/packages/mobile/src/store/init/startConnection/startConnection.saga.ts +++ b/packages/mobile/src/store/init/startConnection/startConnection.saga.ts @@ -1,11 +1,12 @@ import { io } from 'socket.io-client' -import { select, put, call, cancel, fork, takeEvery, delay, FixedTask } from 'typed-redux-saga' +import { select, put, call, cancel, fork, takeEvery, FixedTask, delay, apply } from 'typed-redux-saga' import { PayloadAction } from '@reduxjs/toolkit' import { socket as stateManager, Socket } from '@quiet/state-manager' import { encodeSecret } from '@quiet/common' import { initSelectors } from '../init.selectors' import { initActions, WebsocketConnectionPayload } from '../init.slice' import { eventChannel } from 'redux-saga' +import { SocketActionTypes } from '@quiet/types' export function* startConnectionSaga( action: PayloadAction['payload']> @@ -38,6 +39,7 @@ export function* startConnectionSaga( return } + console.log('Connecting to backend') const token = encodeSecret(socketIOSecret) const socket = yield* call(io, `http://127.0.0.1:${_dataPort}`, { withCredentials: true, @@ -55,6 +57,9 @@ function* setConnectedSaga(socket: Socket): Generator { console.log('WEBSOCKET', 'Forking state-manager sagas', task) // Handle suspending current connection yield* takeEvery(initActions.suspendWebsocketConnection, cancelRootTaskSaga, task) + console.log('Frontend is ready. Starting backend...') + // @ts-ignore - Why is this broken? + yield* apply(socket, socket.emit, [SocketActionTypes.START]) } function* handleSocketLifecycleActions(socket: Socket, socketIOData: WebsocketConnectionPayload): Generator { diff --git a/packages/state-manager/src/sagas/communities/createCommunity/createCommunity.saga.ts b/packages/state-manager/src/sagas/communities/createCommunity/createCommunity.saga.ts index c790bed4de..5b9fc25fff 100644 --- a/packages/state-manager/src/sagas/communities/createCommunity/createCommunity.saga.ts +++ b/packages/state-manager/src/sagas/communities/createCommunity/createCommunity.saga.ts @@ -25,7 +25,10 @@ export function* createCommunitySaga( const community = yield* select(communitiesSelectors.selectById(communityId)) const identity = yield* select(identitySelectors.selectById(communityId)) - if (!identity) return + if (!identity) { + console.error('Failed to create community - identity missing') + return + } const payload: InitCommunityPayload = { id: communityId, @@ -45,7 +48,10 @@ export function* createCommunitySaga( applyEmitParams(SocketActionTypes.CREATE_COMMUNITY, payload) ) - if (!createdCommunity || !createdCommunity.ownerCertificate) return + if (!createdCommunity || !createdCommunity.ownerCertificate) { + console.error('Failed to create community - invalid response from backend') + return + } yield* put(communitiesActions.updateCommunityData(createdCommunity)) 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 9260ded78c..df08376b55 100644 --- a/packages/state-manager/src/sagas/communities/launchCommunity/launchCommunity.saga.ts +++ b/packages/state-manager/src/sagas/communities/launchCommunity/launchCommunity.saga.ts @@ -1,6 +1,7 @@ import { apply, select, put, call } from 'typed-redux-saga' import { type PayloadAction } from '@reduxjs/toolkit' import { applyEmitParams, type Socket } from '../../../types' +import { identityActions } from '../../identity/identity.slice' import { identitySelectors } from '../../identity/identity.selectors' import { communitiesSelectors } from '../communities.selectors' import { communitiesActions } from '../communities.slice' @@ -29,7 +30,8 @@ export function* launchCommunitySaga( socket: Socket, action: PayloadAction['payload']> ): Generator { - console.log('LAUNCH COMMUNITY SAGA') + console.log('Launching community') + const communityId = action.payload if (!communityId) { @@ -65,4 +67,6 @@ export function* launchCommunitySaga( } yield* apply(socket, socket.emitWithAck, applyEmitParams(SocketActionTypes.LAUNCH_COMMUNITY, payload)) + + yield* put(identityActions.saveUserCsr()) } diff --git a/packages/state-manager/src/sagas/identity/checkLocalCsr/checkLocalCsr.saga.test.ts b/packages/state-manager/src/sagas/identity/checkLocalCsr/checkLocalCsr.saga.test.ts deleted file mode 100644 index 7804f2fc5c..0000000000 --- a/packages/state-manager/src/sagas/identity/checkLocalCsr/checkLocalCsr.saga.test.ts +++ /dev/null @@ -1,103 +0,0 @@ -import { createUserCsr, getPubKey, loadPrivateKey, pubKeyFromCsr, setupCrypto } from '@quiet/identity' -import { FactoryGirl } from 'factory-girl' -import { getFactory } from '../../../utils/tests/factories' -import { prepareStore, reducers } from '../../../utils/tests/prepareStore' -import { Store, combineReducers } from 'redux' -import { communitiesActions } from '../../communities/communities.slice' -import { identityActions } from '../identity.slice' -import { checkLocalCsrSaga } from './checkLocalCsr.saga' -import { CreateUserCsrPayload, SendCsrsResponse } from '@quiet/types' -import { expectSaga } from 'redux-saga-test-plan' -import { usersActions } from '../../users/users.slice' - -describe('checkLocalCsr', () => { - let store: Store - let factory: FactoryGirl - - beforeEach(async () => { - setupCrypto() - store = prepareStore().store - factory = await getFactory(store) - }) - - test('saves user csr if absent from the database', async () => { - const community = - await factory.create['payload']>('Community') - - const identity = await factory.create['payload']>('Identity', { - id: community.id, - nickname: 'john', - }) - - const payload: SendCsrsResponse = { - csrs: [], - } - - const reducer = combineReducers(reducers) - await expectSaga(checkLocalCsrSaga, usersActions.storeCsrs(payload)) - .withReducer(reducer) - .withState(store.getState()) - .put(identityActions.saveUserCsr()) - .run() - }) - - test('saves user csr if local and stored one differs', async () => { - const community = - await factory.create['payload']>('Community') - - const identity = await factory.create['payload']>('Identity', { - id: community.id, - nickname: 'john', - }) - - const _pubKey = pubKeyFromCsr(identity.userCsr!.userCsr) - - const privateKey = await loadPrivateKey(identity.userCsr!.userKey, 'ECDSA') - const publicKey = await getPubKey(_pubKey) - - const existingKeyPair: CryptoKeyPair = { privateKey, publicKey } - - const createUserCsrPayload: CreateUserCsrPayload = { - nickname: 'alice', - commonName: identity.hiddenService.onionAddress, - peerId: identity.peerId.id, - signAlg: 'ECDSA', - hashAlg: 'sha-256', - existingKeyPair, - } - - const csr = await createUserCsr(createUserCsrPayload) - - const payload: SendCsrsResponse = { - csrs: [csr.userCsr], - } - - const reducer = combineReducers(reducers) - await expectSaga(checkLocalCsrSaga, usersActions.storeCsrs(payload)) - .withReducer(reducer) - .withState(store.getState()) - .put(identityActions.saveUserCsr()) - .run() - }) - - test('skips if stored csr equals local one', async () => { - const community = - await factory.create['payload']>('Community') - - const identity = await factory.create['payload']>('Identity', { - id: community.id, - nickname: 'john', - }) - - const payload: SendCsrsResponse = { - csrs: [identity.userCsr!.userCsr], - } - - const reducer = combineReducers(reducers) - await expectSaga(checkLocalCsrSaga, usersActions.storeCsrs(payload)) - .withReducer(reducer) - .withState(store.getState()) - .not.put(identityActions.saveUserCsr()) - .run() - }) -}) diff --git a/packages/state-manager/src/sagas/identity/checkLocalCsr/checkLocalCsr.saga.ts b/packages/state-manager/src/sagas/identity/checkLocalCsr/checkLocalCsr.saga.ts deleted file mode 100644 index 2e57894e01..0000000000 --- a/packages/state-manager/src/sagas/identity/checkLocalCsr/checkLocalCsr.saga.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { select, call, put } from 'typed-redux-saga' -import { PayloadAction } from '@reduxjs/toolkit' -import { identityActions } from '../identity.slice' -import { identitySelectors } from '../identity.selectors' -import { CertFieldsTypes, getReqFieldValue, loadCSR, pubKeyFromCsr } from '@quiet/identity' - -export function* checkLocalCsrSaga( - action: PayloadAction['payload']> -): Generator { - console.log('Checking local CSR', action.payload.csrs) - - const { csrs } = action.payload - - const identity = yield* select(identitySelectors.currentIdentity) - - if (!identity) { - console.error('Could not check local csr, no identity.') - return - } - - if (!identity.userCsr) { - console.warn("Identity doesn't have userCsr.") - return - } - - const pubKey = yield* call(pubKeyFromCsr, identity.userCsr.userCsr) - - const storedCsr = csrs.find(csr => csr === identity.userCsr?.userCsr) - - if (storedCsr) { - console.log('Stored CSR with the same public key found, checking for username integirty.', pubKey) - - const parsedCsr = yield* call(loadCSR, storedCsr) - const nickname = yield* call(getReqFieldValue, parsedCsr, CertFieldsTypes.nickName) - - if (nickname == identity.nickname) { - console.log('Stored CSR is equal to the local one, skipping.') - return - } - } - - console.log('Stored CSR differs or missing, saving local one.') - - yield* put(identityActions.saveUserCsr()) -} diff --git a/packages/state-manager/src/sagas/identity/identity.master.saga.ts b/packages/state-manager/src/sagas/identity/identity.master.saga.ts index 471398a152..cf3c116db7 100644 --- a/packages/state-manager/src/sagas/identity/identity.master.saga.ts +++ b/packages/state-manager/src/sagas/identity/identity.master.saga.ts @@ -1,20 +1,16 @@ import { type Socket } from '../../types' import { all, takeEvery } from 'typed-redux-saga' import { identityActions } from './identity.slice' -import { registerCertificateSaga } from './registerCertificate/registerCertificate.saga' import { registerUsernameSaga } from './registerUsername/registerUsername.saga' import { verifyJoinTimestampSaga } from './verifyJoinTimestamp/verifyJoinTimestamp.saga' import { saveUserCsrSaga } from './saveUserCsr/saveUserCsr.saga' import { usersActions } from '../users/users.slice' import { updateCertificateSaga } from './updateCertificate/updateCertificate.saga' -import { checkLocalCsrSaga } from './checkLocalCsr/checkLocalCsr.saga' export function* identityMasterSaga(socket: Socket): Generator { yield all([ takeEvery(identityActions.registerUsername.type, registerUsernameSaga, socket), - takeEvery(identityActions.registerCertificate.type, registerCertificateSaga, socket), takeEvery(identityActions.verifyJoinTimestamp.type, verifyJoinTimestampSaga), - takeEvery(identityActions.checkLocalCsr.type, checkLocalCsrSaga), takeEvery(identityActions.saveUserCsr.type, saveUserCsrSaga, socket), takeEvery(usersActions.responseSendCertificates.type, updateCertificateSaga), ]) diff --git a/packages/state-manager/src/sagas/identity/identity.slice.ts b/packages/state-manager/src/sagas/identity/identity.slice.ts index ec7eb25502..6915b1edb4 100644 --- a/packages/state-manager/src/sagas/identity/identity.slice.ts +++ b/packages/state-manager/src/sagas/identity/identity.slice.ts @@ -29,9 +29,8 @@ export const identitySlice = createSlice({ changes: action.payload, }) }, - createUserCsr: (state, _action: PayloadAction) => state, registerUsername: (state, _action: PayloadAction) => state, - registerCertificate: (state, action: PayloadAction) => { + addCsr: (state, action: PayloadAction) => { identityAdapter.updateOne(state.identities, { id: action.payload.communityId, changes: { @@ -49,7 +48,6 @@ export const identitySlice = createSlice({ }, }) }, - checkLocalCsr: (state, _action: PayloadAction) => state, saveUserCsr: state => state, verifyJoinTimestamp: state => state, updateJoinTimestamp: (state, action: PayloadAction) => { @@ -60,7 +58,6 @@ export const identitySlice = createSlice({ }, }) }, - throwIdentityError: (state, _action: PayloadAction) => state, }, }) diff --git a/packages/state-manager/src/sagas/identity/registerCertificate/registerCertificate.saga.test.ts b/packages/state-manager/src/sagas/identity/registerCertificate/registerCertificate.saga.test.ts deleted file mode 100644 index 43da4a2ade..0000000000 --- a/packages/state-manager/src/sagas/identity/registerCertificate/registerCertificate.saga.test.ts +++ /dev/null @@ -1,146 +0,0 @@ -import { expectSaga } from 'redux-saga-test-plan' -import { type Socket } from '../../../types' -import { setupCrypto } from '@quiet/identity' -import { prepareStore } from '../../../utils/tests/prepareStore' -import { getFactory } from '../../../utils/tests/factories' -import { combineReducers } from '@reduxjs/toolkit' -import { reducers } from '../../reducers' -import { communitiesActions } from '../../communities/communities.slice' -import { identityActions } from '../identity.slice' -import { registerCertificateSaga } from './registerCertificate.saga' -import { type CertData, type RegisterCertificatePayload, SocketActionTypes, type UserCsr } from '@quiet/types' - -describe('registerCertificateSaga', () => { - it('create community when user is community owner', async () => { - setupCrypto() - const socket = { emit: jest.fn(), on: jest.fn() } as unknown as Socket - const store = prepareStore().store - - const factory = await getFactory(store) - - const community = - await factory.create['payload']>('Community') - - const identity = await factory.create['payload']>('Identity', { - id: community.id, - }) - expect(identity.userCsr).not.toBeNull() - const registerCertificatePayload: RegisterCertificatePayload = { - communityId: community.id, - nickname: identity.nickname, - // @ts-expect-error - userCsr: identity.userCsr, - } - const reducer = combineReducers(reducers) - await expectSaga(registerCertificateSaga, socket, identityActions.registerCertificate(registerCertificatePayload)) - .withReducer(reducer) - .withState(store.getState()) - .put(communitiesActions.createCommunity(community.id)) - .not.apply(socket, socket.emit, [SocketActionTypes.REGISTER_USER_CERTIFICATE]) - .run() - }) - - it('launch community when user is not community owner', async () => { - setupCrypto() - const socket = { emit: jest.fn(), on: jest.fn() } as unknown as Socket - - const store = prepareStore().store - - const factory = await getFactory(store) - - const community = await factory.create['payload']>( - 'Community', - { - id: '1', - name: 'rockets', - CA: null, - rootCa: 'rootCa', - peerList: [], - onionAddress: '', - } - ) - - const userCsr: UserCsr = { - userCsr: 'userCsr', - userKey: 'userKey', - pkcs10: jest.fn() as unknown as CertData, - } - - const identity = ( - await factory.build('Identity', { - id: community.id, - }) - ).payload - - identity.userCsr = userCsr - - store.dispatch(identityActions.addNewIdentity(identity)) - - const registerCertificatePayload: RegisterCertificatePayload = { - communityId: community.id, - nickname: identity.nickname, - userCsr: identity.userCsr, - } - - const reducer = combineReducers(reducers) - await expectSaga(registerCertificateSaga, socket, identityActions.registerCertificate(registerCertificatePayload)) - .withReducer(reducer) - .withState(store.getState()) - .not.put(communitiesActions.createCommunity(community.id)) - .put(communitiesActions.launchCommunity(community.id)) - .run() - }) - - it('launch community when user is not community owner and he used username which was taken', async () => { - setupCrypto() - const socket = { emit: jest.fn(), on: jest.fn() } as unknown as Socket - - const store = prepareStore().store - - const factory = await getFactory(store) - - const community = await factory.create['payload']>( - 'Community', - { - id: '1', - name: 'rockets', - CA: null, - rootCa: 'rootCa', - peerList: [], - onionAddress: '', - } - ) - - const userCsr: UserCsr = { - userCsr: 'userCsr', - userKey: 'userKey', - pkcs10: jest.fn() as unknown as CertData, - } - - const identity = ( - await factory.build('Identity', { - id: community.id, - }) - ).payload - - identity.userCsr = userCsr - - store.dispatch(identityActions.addNewIdentity(identity)) - - const registerCertificatePayload: RegisterCertificatePayload = { - communityId: community.id, - nickname: identity.nickname, - userCsr: identity.userCsr, - isUsernameTaken: true, - } - - const reducer = combineReducers(reducers) - await expectSaga(registerCertificateSaga, socket, identityActions.registerCertificate(registerCertificatePayload)) - .withReducer(reducer) - .withState(store.getState()) - .not.put(communitiesActions.createCommunity(community.id)) - .not.put(communitiesActions.launchCommunity(community.id)) - .put(identityActions.saveUserCsr()) - .run() - }) -}) diff --git a/packages/state-manager/src/sagas/identity/registerCertificate/registerCertificate.saga.ts b/packages/state-manager/src/sagas/identity/registerCertificate/registerCertificate.saga.ts deleted file mode 100644 index 31a46349ee..0000000000 --- a/packages/state-manager/src/sagas/identity/registerCertificate/registerCertificate.saga.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { applyEmitParams, type Socket } from '../../../types' -import { type PayloadAction } from '@reduxjs/toolkit' -import { apply, select, put } from 'typed-redux-saga' -import { communitiesSelectors } from '../../communities/communities.selectors' -import { identityActions } from '../identity.slice' -import { - type RegisterOwnerCertificatePayload, - type RegisterUserCertificatePayload, - SocketActionTypes, -} from '@quiet/types' -import { communitiesActions } from '../../communities/communities.slice' - -export function* registerCertificateSaga( - socket: Socket, - action: PayloadAction['payload']> -): Generator { - const currentCommunity = yield* select(communitiesSelectors.currentCommunity) - const isUsernameTaken = action.payload.isUsernameTaken - - if (!currentCommunity) { - console.error('Could not register certificate, no current community') - return - } - - if (currentCommunity.CA?.rootCertString) { - yield* put(communitiesActions.createCommunity(action.payload.communityId)) - } else { - if (!isUsernameTaken) { - yield* put(communitiesActions.launchCommunity(action.payload.communityId)) - } else { - yield* put(identityActions.saveUserCsr()) - } - } -} diff --git a/packages/state-manager/src/sagas/identity/registerUsername/registerUsername.saga.test.ts b/packages/state-manager/src/sagas/identity/registerUsername/registerUsername.saga.test.ts index 5acf2b3e7b..327b7cb698 100644 --- a/packages/state-manager/src/sagas/identity/registerUsername/registerUsername.saga.test.ts +++ b/packages/state-manager/src/sagas/identity/registerUsername/registerUsername.saga.test.ts @@ -59,7 +59,7 @@ describe('registerUsernameSaga', () => { .provide([[call.fn(createUserCsr), userCsr]]) .call(createUserCsr, createUserCsrPayload) .put( - identityActions.registerCertificate({ + identityActions.addCsr({ communityId: community.id, nickname: 'nickname', userCsr, @@ -131,7 +131,7 @@ describe('registerUsernameSaga', () => { .call(getPubKey, pubKey) .call(createUserCsr, createUserCsrPayload) .put( - identityActions.registerCertificate({ + identityActions.addCsr({ communityId: community.id, nickname: newNickname, userCsr, @@ -187,7 +187,7 @@ describe('registerUsernameSaga', () => { ]) .dispatch(identityActions.addNewIdentity(identity)) .put( - identityActions.registerCertificate({ + identityActions.addCsr({ communityId: community.id, nickname: identity.nickname, userCsr, @@ -272,7 +272,7 @@ describe('registerUsernameSaga', () => { .dispatch(identityActions.addNewIdentity(identity)) .call(createUserCsr, createUserCsrPayload) .put( - identityActions.registerCertificate({ + identityActions.addCsr({ communityId: community.id, nickname: 'nickname', userCsr, diff --git a/packages/state-manager/src/sagas/identity/registerUsername/registerUsername.saga.ts b/packages/state-manager/src/sagas/identity/registerUsername/registerUsername.saga.ts index ae91990777..e6714f430c 100644 --- a/packages/state-manager/src/sagas/identity/registerUsername/registerUsername.saga.ts +++ b/packages/state-manager/src/sagas/identity/registerUsername/registerUsername.saga.ts @@ -13,35 +13,35 @@ export function* registerUsernameSaga( socket: Socket, action: PayloadAction['payload']> ): Generator { + console.log('Registering username') + // Nickname can differ between saga calls const { nickname, isUsernameTaken = false } = action.payload let community = yield* select(communitiesSelectors.currentCommunity) - if (!community) { + console.warn('Community missing, waiting...') yield* take(communitiesActions.addNewCommunity) } - community = yield* select(communitiesSelectors.currentCommunity) - if (!community) { console.error('Could not register username, no community data') return } + console.log('Found community') let identity = yield* select(identitySelectors.currentIdentity) - if (!identity) { + console.warn('Identity missing, waiting...') yield* take(identityActions.addNewIdentity) } - identity = yield* select(identitySelectors.currentIdentity) - if (!identity) { console.error('Could not register username, no identity') return } + console.log('Found identity') let userCsr = identity.userCsr @@ -87,12 +87,24 @@ export function* registerUsernameSaga( } } + // TODO: Can rename this type const payload: RegisterCertificatePayload = { communityId: community.id, nickname, userCsr, + // TODO: Remove isUsernameTaken, } - yield* put(identityActions.registerCertificate(payload)) + yield* put(identityActions.addCsr(payload)) + + if (community.CA?.rootCertString) { + yield* put(communitiesActions.createCommunity(community.id)) + } else { + if (!isUsernameTaken) { + yield* put(communitiesActions.launchCommunity(community.id)) + } else { + yield* put(identityActions.saveUserCsr()) + } + } } diff --git a/packages/state-manager/src/sagas/identity/saveUserCsr/saveUserCsr.saga.ts b/packages/state-manager/src/sagas/identity/saveUserCsr/saveUserCsr.saga.ts index 8a595c0fa1..05c371bef0 100644 --- a/packages/state-manager/src/sagas/identity/saveUserCsr/saveUserCsr.saga.ts +++ b/packages/state-manager/src/sagas/identity/saveUserCsr/saveUserCsr.saga.ts @@ -4,6 +4,8 @@ import { apply, select } from 'typed-redux-saga' import { identitySelectors } from '../identity.selectors' export function* saveUserCsrSaga(socket: Socket): Generator { + console.log('Saving user CSR') + const identity = yield* select(identitySelectors.currentIdentity) if (!identity?.userCsr) { console.error('Cannot save user csr to backend, no userCsr') diff --git a/packages/state-manager/src/sagas/messages/messages.slice.ts b/packages/state-manager/src/sagas/messages/messages.slice.ts index c279efc8b2..ab7bfc098a 100644 --- a/packages/state-manager/src/sagas/messages/messages.slice.ts +++ b/packages/state-manager/src/sagas/messages/messages.slice.ts @@ -78,8 +78,14 @@ export const messagesSlice = createSlice({ addMessages: (state, action: PayloadAction) => { const { messages } = action.payload for (const message of messages) { - if (!instanceOfChannelMessage(message)) return - if (!state.publicChannelsMessagesBase.entities[message.channelId]) return + if (!instanceOfChannelMessage(message)) { + console.error('Failed to add message, object not instance of message') + return + } + if (!state.publicChannelsMessagesBase.entities[message.channelId]) { + console.error('Failed to add message, could not find channel', message.channelId) + return + } let toAdd = message @@ -96,8 +102,11 @@ export const messagesSlice = createSlice({ } const messagesBase = state.publicChannelsMessagesBase.entities[message.channelId] - if (!messagesBase) return + if (!messagesBase) { + throw new Error('Failed to add message, channel went missing') + } + console.log('Upserting message to Redux store') channelMessagesAdapter.upsertOne(messagesBase.messages, toAdd) } }, diff --git a/packages/state-manager/src/sagas/messages/sendMessage/sendMessage.saga.ts b/packages/state-manager/src/sagas/messages/sendMessage/sendMessage.saga.ts index 1297c1ea5b..fee94b4d50 100644 --- a/packages/state-manager/src/sagas/messages/sendMessage/sendMessage.saga.ts +++ b/packages/state-manager/src/sagas/messages/sendMessage/sendMessage.saga.ts @@ -1,10 +1,11 @@ import { type Socket, applyEmitParams } from '../../../types' import { type PayloadAction } from '@reduxjs/toolkit' import { sign, loadPrivateKey, pubKeyFromCsr } from '@quiet/identity' -import { call, select, apply, put, delay } from 'typed-redux-saga' +import { call, select, apply, put, delay, take } from 'typed-redux-saga' import { arrayBufferToString } from 'pvutils' import { config } from '../../users/const/certFieldTypes' import { identitySelectors } from '../../identity/identity.selectors' +import { publicChannelsActions } from '../../publicChannels/publicChannels.slice' import { publicChannelsSelectors } from '../../publicChannels/publicChannels.selectors' import { messagesActions } from '../messages.slice' import { generateMessageId, getCurrentTime } from '../utils/message.utils' @@ -14,28 +15,30 @@ export function* sendMessageSaga( socket: Socket, action: PayloadAction['payload']> ): Generator { - const identity = yield* select(identitySelectors.currentIdentity) - if (!identity?.userCsr) return - - const pubKey = yield* call(pubKeyFromCsr, identity.userCsr.userCsr) - const keyObject = yield* call(loadPrivateKey, identity.userCsr.userKey, config.signAlg) - const signatureArrayBuffer = yield* call(sign, action.payload.message, keyObject) - const signature = yield* call(arrayBufferToString, signatureArrayBuffer) - - const currentChannelId = yield* select(publicChannelsSelectors.currentChannelId) - - const createdAt = yield* call(getCurrentTime) - const generatedMessageId = yield* call(generateMessageId) - const id = action.payload.id || generatedMessageId + const identity = yield* select(identitySelectors.currentIdentity) + if (!identity?.userCsr) { + console.error(`Failed to send message ${id} - user CSR is missing`) + return + } + + const currentChannelId = yield* select(publicChannelsSelectors.currentChannelId) const channelId = action.payload.channelId || currentChannelId if (!channelId) { - console.error(`Could not send message with id ${id}, no channel id`) + console.error(`Failed to send message ${id} - channel ID is missing`) return } + console.log(`Sending message ${id} to channel ${channelId}`) + + const pubKey = yield* call(pubKeyFromCsr, identity.userCsr.userCsr) + const keyObject = yield* call(loadPrivateKey, identity.userCsr.userKey, config.signAlg) + const signatureArrayBuffer = yield* call(sign, action.payload.message, keyObject) + const signature = yield* call(arrayBufferToString, signatureArrayBuffer) + const createdAt = yield* call(getCurrentTime) + const message: ChannelMessage = { id, type: action.payload.type || MessageType.Basic, @@ -48,6 +51,7 @@ export function* sendMessageSaga( } // Grey out message until saved in db + console.log('Adding pending message status') yield* put( messagesActions.addMessagesSendingStatus({ message: message, @@ -65,6 +69,7 @@ export function* sendMessageSaga( ) // Display sent message immediately, to improve user experience + console.log('Adding message to Redux store') yield* put( messagesActions.addMessages({ messages: [message], @@ -73,7 +78,10 @@ export function* sendMessageSaga( ) const isUploadingFileMessage = action.payload.media?.cid?.includes('uploading') - if (isUploadingFileMessage) return // Do not broadcast message until file is uploaded + if (isUploadingFileMessage) { + console.log(`Failed to send message ${id} - file upload is in progress`) + return // Do not broadcast message until file is uploaded + } // Wait until we have subscribed to the channel // @@ -82,11 +90,13 @@ export function* sendMessageSaga( // (in a durable way). while (true) { const subscribedChannels = yield* select(publicChannelsSelectors.subscribedChannels) + console.log('Subscribed channels', subscribedChannels) if (subscribedChannels.includes(channelId)) { + console.log(`Channel ${channelId} subscribed`) break } - console.error('Failed to send message, channel not subscribed. Retrying...') - yield* delay(500) + console.error(`Failed to send message ${id} - channel not subscribed. Waiting...`) + yield* take(publicChannelsActions.setChannelSubscribed) } yield* apply( diff --git a/packages/state-manager/src/sagas/publicChannels/publicChannels.selectors.ts b/packages/state-manager/src/sagas/publicChannels/publicChannels.selectors.ts index 1481087c42..fb5de0ab36 100644 --- a/packages/state-manager/src/sagas/publicChannels/publicChannels.selectors.ts +++ b/packages/state-manager/src/sagas/publicChannels/publicChannels.selectors.ts @@ -178,6 +178,8 @@ export const displayableCurrentChannelMessages = createSelector( if (user) { // @ts-ignore result.push(displayableMessage(message, user, userProfiles[message.pubKey])) + } else { + console.warn('Received a message from a user that does not exist', message.id, message.pubKey, users) } return result }, []) 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 9c908f13ee..a64a65224c 100644 --- a/packages/state-manager/src/sagas/socket/startConnection/startConnection.saga.ts +++ b/packages/state-manager/src/sagas/socket/startConnection/startConnection.saga.ts @@ -64,8 +64,6 @@ export function subscribe(socket: Socket) { | ReturnType | ReturnType | ReturnType - | ReturnType - | ReturnType | ReturnType | ReturnType | ReturnType @@ -160,7 +158,6 @@ export function subscribe(socket: Socket) { // Certificates socket.on(SocketActionTypes.CSRS_STORED, (payload: SendCsrsResponse) => { log(`${SocketActionTypes.CSRS_STORED}`) - emit(identityActions.checkLocalCsr(payload)) emit(usersActions.storeCsrs(payload)) }) socket.on(SocketActionTypes.CERTIFICATES_STORED, (payload: SendCertificatesResponse) => { diff --git a/packages/state-manager/src/sagas/users/users.selectors.ts b/packages/state-manager/src/sagas/users/users.selectors.ts index 2f21e34b08..4902482597 100644 --- a/packages/state-manager/src/sagas/users/users.selectors.ts +++ b/packages/state-manager/src/sagas/users/users.selectors.ts @@ -77,9 +77,9 @@ export const registeredUsernames = createSelector( mapping => new Set(Object.values(mapping).map(u => u.username)) ) +// TODO: We can move most of this to the backend. export const allUsers = createSelector(csrsMapping, certificatesMapping, (csrs, certs) => { const users: Record = {} - const allUsernames: string[] = Object.values(csrs).map(u => u.username) const duplicatedUsernames: string[] = allUsernames.filter((val, index) => allUsernames.indexOf(val) !== index) diff --git a/packages/state-manager/src/types.ts b/packages/state-manager/src/types.ts index 44260530d5..909ae28c6c 100644 --- a/packages/state-manager/src/types.ts +++ b/packages/state-manager/src/types.ts @@ -52,6 +52,7 @@ export interface EmitEvents { [SocketActionTypes.ADD_CSR]: EmitEvent [SocketActionTypes.SET_USER_PROFILE]: EmitEvent [SocketActionTypes.LOAD_MIGRATION_DATA]: EmitEvent> + [SocketActionTypes.START]: () => void } export type Socket = IOSocket diff --git a/packages/types/src/socket.ts b/packages/types/src/socket.ts index 573fb1a313..c74ba5dd14 100644 --- a/packages/types/src/socket.ts +++ b/packages/types/src/socket.ts @@ -87,4 +87,10 @@ export enum SocketActionTypes { MIGRATION_DATA_REQUIRED = 'migrationDataRequired', PUSH_NOTIFICATION = 'pushNotification', ERROR = 'error', + /** + * Start the backend. Currently, the frontend depends on events + * emitted from the backend, so we wait to start the backend until + * the frontend is connected and listening. + */ + START = 'start', } From 611af21d316b80767190df5bda498ed2a810a058 Mon Sep 17 00:00:00 2001 From: Wiktor Sieprawski Date: Mon, 6 May 2024 12:49:36 +0200 Subject: [PATCH 4/5] fix: start websocket connection on react init (#2481) * fix: start websocket connection on react init * chore: restore self hosted ios e2e tests * fix: prevent stripping hermes framework * Revert "fix: prevent stripping hermes framework" This reverts commit 65dfec8db70de0b0783782cafb25b451f369a109. * chore: skip self-hosted ios e2e tests --- .github/workflows/e2e-ios-self.yml | 51 +++++++++++++++++++ packages/mobile/ios/CommunicationBridge.m | 1 + packages/mobile/ios/CommunicationModule.swift | 7 +++ .../ios/Quiet.xcodeproj/project.pbxproj | 4 ++ packages/mobile/ios/Quiet/AppDelegate.m | 40 ++++----------- packages/mobile/ios/WebsocketSingleton.swift | 9 ++++ 6 files changed, 82 insertions(+), 30 deletions(-) create mode 100644 .github/workflows/e2e-ios-self.yml create mode 100644 packages/mobile/ios/WebsocketSingleton.swift diff --git a/.github/workflows/e2e-ios-self.yml b/.github/workflows/e2e-ios-self.yml new file mode 100644 index 0000000000..7b47db414b --- /dev/null +++ b/.github/workflows/e2e-ios-self.yml @@ -0,0 +1,51 @@ +name: Detox E2E iOS (self-hosted) + +on: + # push: + # paths: + # - packages/mobile/** + # - packages/backend/** + # - packages/state-manager/** + # - .github/workflows/e2e-ios-self.yml + +jobs: + detox-ios-self-hosted: + timeout-minutes: 25 + runs-on: [self-hosted, macOS, ARM64, iOS] + + steps: + - uses: actions/checkout@v4 + with: + lfs: true + + - name: Install dependencies + run: | + npm i + npm run lerna bootstrap --scope @quiet/eslint-config,@quiet/logger,@quiet/common,@quiet/types,@quiet/state-manager,@quiet/backend,@quiet/identity,@quiet/mobile,backend-bundle + + - name: Install pods + run: | + cd packages/mobile/ios + pod install + + - name: Install pm2 + run: npm install pm2@latest -g + + - name: Start metro + run: | + cd packages/mobile + pm2 --name METRO start npm -- start + + - name: Build Detox + run: | + cd packages/mobile + detox build -c ios.sim.debug.ci + + - name: Run basic tests + run: | + cd packages/mobile + detox test starter -c ios.sim.debug.ci + + - name: Stop metro + if: always() + run: pm2 stop METRO diff --git a/packages/mobile/ios/CommunicationBridge.m b/packages/mobile/ios/CommunicationBridge.m index 49714ed292..578fa66723 100644 --- a/packages/mobile/ios/CommunicationBridge.m +++ b/packages/mobile/ios/CommunicationBridge.m @@ -2,4 +2,5 @@ #import "React/RCTEventEmitter.h" @interface RCT_EXTERN_MODULE(CommunicationModule, RCTEventEmitter) +RCT_EXTERN_METHOD(handleIncomingEvents:(NSString *)event payload:(NSString *)payload extra:(NSString *)extra) @end diff --git a/packages/mobile/ios/CommunicationModule.swift b/packages/mobile/ios/CommunicationModule.swift index 75c132e14b..069dfecc93 100644 --- a/packages/mobile/ios/CommunicationModule.swift +++ b/packages/mobile/ios/CommunicationModule.swift @@ -29,6 +29,13 @@ class CommunicationModule: RCTEventEmitter { self.sendEvent(withName: CommunicationModule.APP_RESUME_IDENTIFIER, body: nil) } + @objc + func handleIncomingEvents(_ event: NSString, payload: NSString?, extra: NSString?) { + let socketPort = WebsocketSingleton.sharedInstance.socketPort + let socketIOSecret = WebsocketSingleton.sharedInstance.socketIOSecret + self.sendDataPort(port: socketPort, socketIOSecret: socketIOSecret); + } + override func supportedEvents() -> [String]! { return [CommunicationModule.BACKEND_EVENT_IDENTIFIER, CommunicationModule.NOTIFICATION_EVENT_IDENTIFIER, CommunicationModule.STOP_EVENT_IDENTIFIER, CommunicationModule.APP_PAUSE_IDENTIFIER, CommunicationModule.APP_RESUME_IDENTIFIER] } diff --git a/packages/mobile/ios/Quiet.xcodeproj/project.pbxproj b/packages/mobile/ios/Quiet.xcodeproj/project.pbxproj index 3a85467e84..d2a83f50a5 100644 --- a/packages/mobile/ios/Quiet.xcodeproj/project.pbxproj +++ b/packages/mobile/ios/Quiet.xcodeproj/project.pbxproj @@ -42,6 +42,7 @@ 7F80A59D9EC1440186E5D5CF /* Rubik-SemiBold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = BF73B04135634980BC656D6C /* Rubik-SemiBold.ttf */; }; 80CCB457674F4979A3C5DB06 /* Rubik-MediumItalic.ttf in Resources */ = {isa = PBXBuildFile; fileRef = EBC2A7699E0A49059904DD8C /* Rubik-MediumItalic.ttf */; }; 8A009A60D84E4B08AB0E8152 /* Rubik-Bold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = E820B3E5514B49EE8C72DECB /* Rubik-Bold.ttf */; }; + 955DC7582BD930B30014725B /* WebsocketSingleton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 955DC7572BD930B30014725B /* WebsocketSingleton.swift */; }; 9EC9E7C54868433990A479EC /* Rubik-ExtraBoldItalic.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 182B8961416E4D4C8A29793D /* Rubik-ExtraBoldItalic.ttf */; }; 9EFEA2C3C6AE0D6FA6A4C079 /* libPods-Quiet-QuietTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = ED4D442ACC913B8E48E2C569 /* libPods-Quiet-QuietTests.a */; }; A43CD77BAC37717C692E6333 /* libPods-Quiet.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 47D3FEF6693D5638AD9D0AFF /* libPods-Quiet.a */; }; @@ -606,6 +607,7 @@ 599527D61D41D1899FC6F87B /* Pods-Quiet-QuietTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Quiet-QuietTests.release.xcconfig"; path = "Target Support Files/Pods-Quiet-QuietTests/Pods-Quiet-QuietTests.release.xcconfig"; sourceTree = ""; }; 706E1601C7B649A8A40A7877 /* Rubik-Light.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "Rubik-Light.ttf"; path = "../assets/fonts/Rubik-Light.ttf"; sourceTree = ""; }; 84720E58493F44BE8C4784E3 /* Rubik-BoldItalic.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "Rubik-BoldItalic.ttf"; path = "../assets/fonts/Rubik-BoldItalic.ttf"; sourceTree = ""; }; + 955DC7572BD930B30014725B /* WebsocketSingleton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebsocketSingleton.swift; sourceTree = ""; }; BF73B04135634980BC656D6C /* Rubik-SemiBold.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "Rubik-SemiBold.ttf"; path = "../assets/fonts/Rubik-SemiBold.ttf"; sourceTree = ""; }; CD39A872C9F80A6BA04B8A4A /* Pods-Quiet.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Quiet.release.xcconfig"; path = "Target Support Files/Pods-Quiet/Pods-Quiet.release.xcconfig"; sourceTree = ""; }; E820B3E5514B49EE8C72DECB /* Rubik-Bold.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "Rubik-Bold.ttf"; path = "../assets/fonts/Rubik-Bold.ttf"; sourceTree = ""; }; @@ -679,6 +681,7 @@ 1889CA4D26E763E1004ECFBD /* Extensions.swift */, 1868C43B2930E255001D6D5E /* CommunicationModule.swift */, 1868C43D2930EAEA001D6D5E /* CommunicationBridge.m */, + 955DC7572BD930B30014725B /* WebsocketSingleton.swift */, ); name = Quiet; sourceTree = ""; @@ -5140,6 +5143,7 @@ files = ( 18FD2A40296F009E00A2B8C0 /* main.m in Sources */, 1868BCF1292E9212001D6D5E /* rn-bridge.cpp in Sources */, + 955DC7582BD930B30014725B /* WebsocketSingleton.swift in Sources */, 1889CA4E26E763E1004ECFBD /* Extensions.swift in Sources */, 1898143A2934CF70001F39E7 /* TorHandler.swift in Sources */, 1868BCEE292E9212001D6D5E /* RNNodeJsMobile.m in Sources */, diff --git a/packages/mobile/ios/Quiet/AppDelegate.m b/packages/mobile/ios/Quiet/AppDelegate.m index ee2c2c74ac..155174e411 100644 --- a/packages/mobile/ios/Quiet/AppDelegate.m +++ b/packages/mobile/ios/Quiet/AppDelegate.m @@ -45,43 +45,30 @@ - (void) createDataDirectory { self.dataPath = [dataDirectory create]; } -- (void) initWebsocketConnection { - /* - * We have to wait for RCTBridge listeners to be initialized, yet we must be sure to deliver the event containing data port information. - * Delay used below can't cause any race condition as websocket won't connect until data server starts listening anyway. - */ - dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{ - NSArray *intervals = @[@5, @15, @30, @60, @90]; - for (NSNumber *interval in intervals) { - NSTimeInterval delayInSeconds = [interval doubleValue]; - dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC)); - dispatch_after(popTime, dispatch_get_main_queue(), ^(void) { - [[self.bridge moduleForName:@"CommunicationModule"] sendDataPortWithPort:self.dataPort socketIOSecret:self.socketIOSecret]; - }); - } - }); -} - - (void) spinupBackend:(BOOL)init { - // (1/6) Find ports to use in tor and backend configuration + // (1/4) Find ports to use in tor and backend configuration Utils *utils = [Utils new]; if (self.socketIOSecret == nil) { - self.socketIOSecret = [utils generateSecretWithLength:(20)]; + self.socketIOSecret = [utils generateSecretWithLength:(20)]; } - + FindFreePort *findFreePort = [FindFreePort new]; self.dataPort = [findFreePort getFirstStartingFromPort:11000]; + + WebsocketSingleton *websocket = [WebsocketSingleton sharedInstance]; + websocket.socketPort = self.dataPort; + websocket.socketIOSecret = self.socketIOSecret; uint16_t socksPort = [findFreePort getFirstStartingFromPort:arc4random_uniform(65000 - 1024) + 1024]; uint16_t controlPort = [findFreePort getFirstStartingFromPort:arc4random_uniform(65000 - 1024) + 1024]; uint16_t httpTunnelPort = [findFreePort getFirstStartingFromPort:arc4random_uniform(65000 - 1024) + 1024]; - // (2/6) Spawn tor with proper configuration + // (2/4) Spawn tor with proper configuration self.tor = [TorHandler new]; @@ -94,7 +81,7 @@ - (void) spinupBackend:(BOOL)init { }); - // (4/6) Connect to tor control port natively (so we can use it to shutdown tor when app goes idle) + // (3/4) Connect to tor control port natively (so we can use it to shutdown tor when app goes idle) dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{ NSData *authCookieData = [self getAuthCookieData]; @@ -113,14 +100,7 @@ - (void) spinupBackend:(BOOL)init { }]; }); - - // (5/6) Update data port information and broadcast it to frontend - if (init) { - [self initWebsocketConnection]; - } - - - // (6/6) Launch backend or rewire services + // (4/4) Launch backend or rewire services dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{ diff --git a/packages/mobile/ios/WebsocketSingleton.swift b/packages/mobile/ios/WebsocketSingleton.swift new file mode 100644 index 0000000000..604ef7d136 --- /dev/null +++ b/packages/mobile/ios/WebsocketSingleton.swift @@ -0,0 +1,9 @@ +@objcMembers +class WebsocketSingleton: NSObject { + static let sharedInstance = WebsocketSingleton() + + var socketPort: UInt16 = 0 + var socketIOSecret: String = "" + + private override init() {} +} From 5e1888d997864306f8005767c472a201190d97cb Mon Sep 17 00:00:00 2001 From: Isla Koenigsknecht Date: Mon, 6 May 2024 14:39:05 -0400 Subject: [PATCH 5/5] Fix(2424): Fix issue with connecting on resume on ios (#2506) * Fix issue with connecting on resume on ios * Update changelogs --- CHANGELOG.md | 1 + packages/backend/src/backendManager.ts | 4 +- .../connections-manager.service.tor.spec.ts | 5 +- .../connections-manager.service.ts | 21 +++++- .../backend/src/nest/libp2p/libp2p.service.ts | 68 +++++++++++++++++-- .../backend/src/nest/libp2p/libp2p.types.ts | 10 +++ packages/mobile/CHANGELOG.md | 2 + .../mobile/ios/NodeJsMobile/NodeRunner.mm | 2 +- packages/mobile/ios/Podfile.lock | 27 ++++---- .../ios/Quiet.xcodeproj/project.pbxproj | 2 +- .../startConnection/startConnection.saga.ts | 6 +- 11 files changed, 123 insertions(+), 25 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bf252ece2a..3f7417e237 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ # Fixes: * Disable spellCheck/autoCorrect on non-spelling sensitive fields like usernames and channels ([#373](https://github.com/TryQuiet/quiet/issues/373)) +* Fixes issue with reconnecting to peers on resume on iOS ([#2424](https://github.com/TryQuiet/quiet/issues/2424)) # Chores diff --git a/packages/backend/src/backendManager.ts b/packages/backend/src/backendManager.ts index 42fa25d78b..63fd7449be 100644 --- a/packages/backend/src/backendManager.ts +++ b/packages/backend/src/backendManager.ts @@ -111,7 +111,7 @@ export const runBackendMobile = async () => { rn_bridge.channel.on('close', async () => { const connectionsManager = app.get(ConnectionsManagerService) - connectionsManager.closeSocket() + await connectionsManager.pause() }) rn_bridge.channel.on('open', async (msg: OpenServices) => { @@ -123,7 +123,7 @@ export const runBackendMobile = async () => { torControl.torControlParams.auth.value = msg.authCookie proxyAgent.proxy.port = msg.httpTunnelPort - await connectionsManager.openSocket() + await connectionsManager.resume() }) } 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 eb1d47416c..cf798e1036 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 @@ -146,7 +146,10 @@ describe('Connections manager', () => { community, network: { peerId: userIdentity.peerId, hiddenService: userIdentity.hiddenService }, }) - libp2pService.connectedPeers.set(peerId.toString(), DateTime.utc().valueOf()) + libp2pService.connectedPeers.set(peerId.toString(), { + connectedAtSeconds: DateTime.utc().valueOf(), + address: peerId.toString(), + }) // Peer disconnected const remoteAddr = `${peerId.toString()}` 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 0489557f08..b3d36347bb 100644 --- a/packages/backend/src/nest/connections-manager/connections-manager.service.ts +++ b/packages/backend/src/nest/connections-manager/connections-manager.service.ts @@ -49,7 +49,7 @@ import { import Logger from '../common/logger' import { CONFIG_OPTIONS, QUIET_DIR, SERVER_IO_PROVIDER, SOCKS_PROXY_AGENT } from '../const' import { Libp2pService } from '../libp2p/libp2p.service' -import { Libp2pEvents, Libp2pNodeParams } from '../libp2p/libp2p.types' +import { Libp2pEvents, Libp2pNodeParams, Libp2pPeerInfo } from '../libp2p/libp2p.types' import { LocalDbService } from '../local-db/local-db.service' import { LocalDBKeys } from '../local-db/local-db.types' import { RegistrationService } from '../registration/registration.service' @@ -71,6 +71,7 @@ export class ConnectionsManagerService extends EventEmitter implements OnModuleI public libp2pService: Libp2pService private ports: GetPorts isTorInit: TorInitState = TorInitState.NOT_STARTED + private peerInfo: Libp2pPeerInfo | undefined = undefined private readonly logger = Logger(ConnectionsManagerService.name) constructor( @@ -246,6 +247,23 @@ export class ConnectionsManagerService extends EventEmitter implements OnModuleI this.serverIoProvider.io.close() } + public async pause() { + this.logger('Pausing!') + this.logger('Closing socket!') + this.closeSocket() + this.logger('Pausing libp2pService!') + this.peerInfo = await this.libp2pService?.pause() + this.logger('Found the following peer info on pause: ', this.peerInfo) + } + + public async resume() { + this.logger('Resuming!') + this.logger('Reopening socket!') + await this.openSocket() + this.logger('Dialing peers with info: ', this.peerInfo) + await this.libp2pService?.redialPeers(this.peerInfo) + } + // This method is only used on iOS through rn-bridge for reacting on lifecycle changes public async openSocket() { await this.socketService.init() @@ -626,6 +644,7 @@ export class ConnectionsManagerService extends EventEmitter implements OnModuleI // Update Frontend with Initialized Communities if (this.communityId) { this.serverIoProvider.io.emit(SocketActionTypes.COMMUNITY_LAUNCHED, { id: this.communityId }) + console.log('this.libp2pService.dialedPeers', this.libp2pService.dialedPeers) console.log('this.libp2pService.connectedPeers', this.libp2pService.connectedPeers) console.log('this.libp2pservice', this.libp2pService) this.serverIoProvider.io.emit( diff --git a/packages/backend/src/nest/libp2p/libp2p.service.ts b/packages/backend/src/nest/libp2p/libp2p.service.ts index c4129e9888..00a3763f8a 100644 --- a/packages/backend/src/nest/libp2p/libp2p.service.ts +++ b/packages/backend/src/nest/libp2p/libp2p.service.ts @@ -20,7 +20,7 @@ import { SERVER_IO_PROVIDER, SOCKS_PROXY_AGENT } from '../const' import { ServerIoProviderTypes } from '../types' import { webSockets } from '../websocketOverTor' import { all } from '../websocketOverTor/filters' -import { Libp2pEvents, Libp2pNodeParams } from './libp2p.types' +import { Libp2pConnectedPeer, Libp2pEvents, Libp2pNodeParams, Libp2pPeerInfo } from './libp2p.types' import { ProcessInChunksService } from './process-in-chunks.service' const KEY_LENGTH = 32 @@ -29,7 +29,7 @@ export const LIBP2P_PSK_METADATA = '/key/swarm/psk/1.0.0/\n/base16/\n' @Injectable() export class Libp2pService extends EventEmitter { public libp2pInstance: Libp2p | null - public connectedPeers: Map = new Map() + public connectedPeers: Map = new Map() public dialedPeers: Set = new Set() private readonly logger = Logger(Libp2pService.name) constructor( @@ -48,6 +48,21 @@ export class Libp2pService extends EventEmitter { await this.libp2pInstance?.dial(multiaddr(peerAddress)) } + public getCurrentPeerInfo = (): Libp2pPeerInfo => { + return { + dialed: Array.from(this.dialedPeers), + connected: Array.from(this.connectedPeers.values()).map(peer => peer.address), + } + } + + public pause = async (): Promise => { + const peerInfo = this.getCurrentPeerInfo() + await this.hangUpPeers(peerInfo.dialed) + this.dialedPeers.clear() + this.connectedPeers.clear() + return peerInfo + } + public readonly createLibp2pAddress = (address: string, peerId: string): string => { return createLibp2pAddress(address, peerId) } @@ -74,6 +89,47 @@ export class Libp2pService extends EventEmitter { return { psk: psk.toString('base64'), fullKey } } + public async hangUpPeers(peers: string[]) { + this.logger('Hanging up on all peers') + for (const peer of peers) { + await this.hangUpPeer(peer) + } + } + + public async hangUpPeer(peerAddress: string) { + this.logger('Hanging up on peer', peerAddress) + await this.libp2pInstance?.hangUp(multiaddr(peerAddress)) + this.dialedPeers.delete(peerAddress) + this.connectedPeers.delete(peerAddress) + } + + /** + * Hang up existing peer connections and re-dial them. Specifically useful on + * iOS where Tor receives a new port when the app resumes from background and + * we want to close/re-open connections. + */ + public async redialPeers(peerInfo?: Libp2pPeerInfo) { + const dialed = peerInfo ? peerInfo.dialed : Array.from(this.dialedPeers) + const toDial = peerInfo + ? [...peerInfo.connected, ...peerInfo.dialed] + : [...this.connectedPeers.keys(), ...this.dialedPeers] + + if (dialed.length === 0) { + this.logger('No peers to redial!') + return + } + + this.logger(`Re-dialing ${dialed.length} peers`) + + // TODO: Sort peers + for (const peerAddress of dialed) { + await this.hangUpPeer(peerAddress) + } + + this.processInChunksService.updateData(toDial) + await this.processInChunksService.process() + } + public async createInstance(params: Libp2pNodeParams): Promise { if (this.libp2pInstance) { return this.libp2pInstance @@ -157,7 +213,11 @@ export class Libp2pService extends EventEmitter { const localPeerId = peerId.toString() this.logger(`${localPeerId} connected to ${remotePeerId}`) - this.connectedPeers.set(remotePeerId, DateTime.utc().valueOf()) + const connectedPeer: Libp2pConnectedPeer = { + address: peer.detail.remoteAddr.toString(), + connectedAtSeconds: DateTime.utc().valueOf(), + } + this.connectedPeers.set(remotePeerId, connectedPeer) this.logger(`${localPeerId} is connected to ${this.connectedPeers.size} peers`) this.logger(`${localPeerId} has ${this.libp2pInstance?.getConnections().length} open connections`) @@ -176,7 +236,7 @@ export class Libp2pService extends EventEmitter { } this.logger(`${localPeerId} has ${this.libp2pInstance.getConnections().length} open connections`) - const connectionStartTime = this.connectedPeers.get(remotePeerId) + const connectionStartTime: number = this.connectedPeers.get(remotePeerId)!.connectedAtSeconds if (!connectionStartTime) { this.logger.error(`No connection start time for peer ${remotePeerId}`) return diff --git a/packages/backend/src/nest/libp2p/libp2p.types.ts b/packages/backend/src/nest/libp2p/libp2p.types.ts index 985c3a4907..34b7056db2 100644 --- a/packages/backend/src/nest/libp2p/libp2p.types.ts +++ b/packages/backend/src/nest/libp2p/libp2p.types.ts @@ -17,3 +17,13 @@ export interface Libp2pNodeParams { peers: string[] psk: Uint8Array } + +export type Libp2pPeerInfo = { + dialed: string[] + connected: string[] +} + +export type Libp2pConnectedPeer = { + address: string + connectedAtSeconds: number +} diff --git a/packages/mobile/CHANGELOG.md b/packages/mobile/CHANGELOG.md index e5ad80e5c2..c68832d58e 100644 --- a/packages/mobile/CHANGELOG.md +++ b/packages/mobile/CHANGELOG.md @@ -6,6 +6,8 @@ # Fixes: +* Fixes issue with reconnecting to peers on resume on iOS ([#2424](https://github.com/TryQuiet/quiet/issues/2424)) + [2.2.0] # New features: diff --git a/packages/mobile/ios/NodeJsMobile/NodeRunner.mm b/packages/mobile/ios/NodeJsMobile/NodeRunner.mm index 413025e536..35f909e793 100644 --- a/packages/mobile/ios/NodeJsMobile/NodeRunner.mm +++ b/packages/mobile/ios/NodeJsMobile/NodeRunner.mm @@ -205,7 +205,7 @@ - (void) startEngineWithArguments:(NSArray*)arguments:(NSString*)builtinModulesP nodePath = [nodePath stringByAppendingString:builtinModulesPath]; } setenv([@"NODE_PATH" UTF8String], (const char*)[nodePath UTF8String], 1); - setenv([@"DEBUG" UTF8String], "backend:*", 1); + setenv([@"DEBUG" UTF8String], "backend:*,state-manager:*,libp2p:pnet", 1); int c_arguments_size=0; diff --git a/packages/mobile/ios/Podfile.lock b/packages/mobile/ios/Podfile.lock index 01c21f974e..77dbd77f7e 100644 --- a/packages/mobile/ios/Podfile.lock +++ b/packages/mobile/ios/Podfile.lock @@ -15,15 +15,18 @@ PODS: - hermes-engine/Pre-built (= 0.73.2) - hermes-engine/Pre-built (0.73.2) - libevent (2.1.12) - - libwebp (1.2.3): - - libwebp/demux (= 1.2.3) - - libwebp/mux (= 1.2.3) - - libwebp/webp (= 1.2.3) - - libwebp/demux (1.2.3): + - libwebp (1.3.2): + - libwebp/demux (= 1.3.2) + - libwebp/mux (= 1.3.2) + - libwebp/sharpyuv (= 1.3.2) + - libwebp/webp (= 1.3.2) + - libwebp/demux (1.3.2): - libwebp/webp - - libwebp/mux (1.2.3): + - libwebp/mux (1.3.2): - libwebp/demux - - libwebp/webp (1.2.3) + - libwebp/sharpyuv (1.3.2) + - libwebp/webp (1.3.2): + - libwebp/sharpyuv - RCT-Folly (2022.05.16.00): - boost - DoubleConversion @@ -1323,14 +1326,14 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: boost: d3f49c53809116a5d38da093a8aa78bf551aed09 - DoubleConversion: 5189b271737e1565bdce30deb4a08d647e3f5f54 + DoubleConversion: fea03f2699887d960129cc54bba7e52542b6f953 FBLazyVector: fbc4957d9aa695250b55d879c1d86f79d7e69ab4 FBReactNativeSpec: 86de768f89901ef6ed3207cd686362189d64ac88 fmt: ff9d55029c625d3757ed641535fd4a75fedc7ce9 - glog: 04b94705f318337d7ead9e6d17c019bd9b1f6b1b + glog: c5d68082e772fa1c511173d6b30a9de2c05a69a2 hermes-engine: b361c9ef5ef3cda53f66e195599b47e1f84ffa35 libevent: 4049cae6c81cdb3654a443be001fb9bdceff7913 - libwebp: 60305b2e989864154bd9be3d772730f08fc6a59c + libwebp: 1786c9f4ff8a279e4dac1e8f385004d5fc253009 RCT-Folly: 7169b2b1c44399c76a47b5deaaba715eeeb476c0 RCTRequired: 9b1e7e262745fb671e33c51c1078d093bd30e322 RCTTypeSafety: a759e3b086eccf3e2cbf2493d22f28e082f958e6 @@ -1391,8 +1394,8 @@ SPEC CHECKSUMS: SDWebImageWebPCoder: 908b83b6adda48effe7667cd2b7f78c897e5111d SocketRocket: f32cd54efbe0f095c4d7594881e52619cfe80b17 Tor: 39dc71bf048312e202608eb499ca5c74e841b503 - Yoga: 13c8ef87792450193e117976337b8527b49e8c03 + Yoga: e64aa65de36c0832d04e8c7bd614396c77a80047 PODFILE CHECKSUM: eed49772dde039b0723324c813c83dd4c1af35f7 -COCOAPODS: 1.15.2 +COCOAPODS: 1.13.0 diff --git a/packages/mobile/ios/Quiet.xcodeproj/project.pbxproj b/packages/mobile/ios/Quiet.xcodeproj/project.pbxproj index d2a83f50a5..2bcb1abe5a 100644 --- a/packages/mobile/ios/Quiet.xcodeproj/project.pbxproj +++ b/packages/mobile/ios/Quiet.xcodeproj/project.pbxproj @@ -4975,7 +4975,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "APP_PATH=\"${TARGET_BUILD_DIR}/${WRAPPER_NAME}\"\n\n# This script loops through the frameworks embedded in the application and\n# removes unused architectures.\nfind \"$APP_PATH\" -name '*.framework' -type d | while read -r FRAMEWORK\ndo\n FRAMEWORK_EXECUTABLE_NAME=$(defaults read \"$FRAMEWORK/Info.plist\" CFBundleExecutable)\n FRAMEWORK_EXECUTABLE_PATH=\"$FRAMEWORK/$FRAMEWORK_EXECUTABLE_NAME\"\n echo \"Executable is $FRAMEWORK_EXECUTABLE_PATH\"\n\n EXTRACTED_ARCHS=()\n\n for ARCH in $ARCHS\n do\n echo \"Extracting $ARCH from $FRAMEWORK_EXECUTABLE_NAME\"\n lipo -extract \"$ARCH\" \"$FRAMEWORK_EXECUTABLE_PATH\" -o \"$FRAMEWORK_EXECUTABLE_PATH-$ARCH\"\n EXTRACTED_ARCHS+=(\"$FRAMEWORK_EXECUTABLE_PATH-$ARCH\")\n done\n\n echo \"Merging extracted architectures: ${ARCHS}\"\n lipo -o \"$FRAMEWORK_EXECUTABLE_PATH-merged\" -create \"${EXTRACTED_ARCHS[@]}\"\n rm \"${EXTRACTED_ARCHS[@]}\"\n\n echo \"Replacing original executable with thinned version\"\n rm \"$FRAMEWORK_EXECUTABLE_PATH\"\n mv \"$FRAMEWORK_EXECUTABLE_PATH-merged\" \"$FRAMEWORK_EXECUTABLE_PATH\"\n\ndone\n"; + shellScript = "APP_PATH=\"${TARGET_BUILD_DIR}/${WRAPPER_NAME}\"\n\n# This script loops through the frameworks embedded in the application and\n# removes unused architectures.\nfind \"$APP_PATH\" -name '*.framework' -type d ! -name 'hermes.framework' | while read -r FRAMEWORK\ndo\n FRAMEWORK_EXECUTABLE_NAME=$(defaults read \"$FRAMEWORK/Info.plist\" CFBundleExecutable)\n FRAMEWORK_EXECUTABLE_PATH=\"$FRAMEWORK/$FRAMEWORK_EXECUTABLE_NAME\"\n echo \"Executable is $FRAMEWORK_EXECUTABLE_PATH\"\n\n EXTRACTED_ARCHS=()\n\n for ARCH in $ARCHS\n do\n echo \"Extracting $ARCH from $FRAMEWORK_EXECUTABLE_NAME\"\n lipo -extract \"$ARCH\" \"$FRAMEWORK_EXECUTABLE_PATH\" -o \"$FRAMEWORK_EXECUTABLE_PATH-$ARCH\"\n EXTRACTED_ARCHS+=(\"$FRAMEWORK_EXECUTABLE_PATH-$ARCH\")\n done\n\n echo \"Merging extracted architectures: ${ARCHS}\"\n lipo -o \"$FRAMEWORK_EXECUTABLE_PATH-merged\" -create \"${EXTRACTED_ARCHS[@]}\"\n rm \"${EXTRACTED_ARCHS[@]}\"\n\n echo \"Replacing original executable with thinned version\"\n rm \"$FRAMEWORK_EXECUTABLE_PATH\"\n mv \"$FRAMEWORK_EXECUTABLE_PATH-merged\" \"$FRAMEWORK_EXECUTABLE_PATH\"\n\ndone\n"; }; 18FD2A32296D736300A2B8C0 /* [CUSTOM NODEJS MOBILE] Remove Python3 Binaries */ = { isa = PBXShellScriptBuildPhase; diff --git a/packages/mobile/src/store/init/startConnection/startConnection.saga.ts b/packages/mobile/src/store/init/startConnection/startConnection.saga.ts index a0abd3022a..4cc55f53f9 100644 --- a/packages/mobile/src/store/init/startConnection/startConnection.saga.ts +++ b/packages/mobile/src/store/init/startConnection/startConnection.saga.ts @@ -80,8 +80,8 @@ function subscribeSocketLifecycle(socket: Socket, socketIOData: WebsocketConnect console.log('client: Websocket connected', socket_id) emit(initActions.setWebsocketConnected(socketIOData)) }) - socket.on('disconnect', () => { - console.log('client: Closing socket connection', socket_id) + socket.on('disconnect', reason => { + console.warn('client: Closing socket connection', socket_id, reason) emit(initActions.suspendWebsocketConnection()) }) return () => {} @@ -89,7 +89,7 @@ function subscribeSocketLifecycle(socket: Socket, socketIOData: WebsocketConnect } function* cancelRootTaskSaga(task: FixedTask): Generator { - console.log('Canceling root task') + console.warn('Canceling root task', task.error()) yield* cancel(task) yield* put(initActions.canceledRootTask()) }