diff --git a/.eslintignore b/.eslintignore index 9957d65d8..5ec61c91a 100644 --- a/.eslintignore +++ b/.eslintignore @@ -30,3 +30,4 @@ ts/**/*.js # Libloki specific files libloki/test/components.js libloki/modules/mnemonic.js +session-file-server/** diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 7e74cb96d..13e049b45 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -24,6 +24,15 @@ jobs: - name: Checkout git repo uses: actions/checkout@v2 + - name: Pull git submodules + run: git submodule update --init + + - name: Install file server dependency + run: | + cd session-file-server + yarn install; + cd - + - name: Install node uses: actions/setup-node@v1 with: diff --git a/.gitignore b/.gitignore index 59d4c8721..4fa5056a2 100644 --- a/.gitignore +++ b/.gitignore @@ -37,3 +37,6 @@ ts/protobuf/*.d.ts # Ctags tags + +proxy.key +proxy.pub diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 000000000..4b46b6302 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "session-file-server"] + path = session-file-server + url = https://github.com/loki-project/session-file-server/ diff --git a/.prettierignore b/.prettierignore index ff08c3b1f..dbdc281c0 100644 --- a/.prettierignore +++ b/.prettierignore @@ -54,3 +54,4 @@ stylesheets/_intlTelInput.scss # Coverage coverage/** .nyc_output/** +session-file-server/** diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7a13f984f..437d4e87a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -94,9 +94,9 @@ There are a few scripts which you can use: ``` yarn start - Start development -yarn start-multi - Start second instance of development +MULTI=1 yarn start - Start second instance of development yarn start-prod - Start production but in development mode -yarn start-prod-multi - Start another instance of production +MULTI=1 yarn start-prod - Start another instance of production ``` For more than 2 clients, you may run the above command with `NODE_APP_INSTANCE` set before them. diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 7636e8bb6..ad74f966b 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -1124,11 +1124,6 @@ "message": " Type your message", "description": "Placeholder text in the message entry field" }, - "secondaryDeviceDefaultFR": { - "message": "Please accept to enable messages to be synced across devices", - "description": - "Placeholder text in the message entry field when it is disabled because a secondary device conversation is visible" - }, "sendMessageDisabledSecondary": { "message": "This pubkey belongs to a secondary device. You should never see this message", diff --git a/app/sql.js b/app/sql.js index fb5581f6d..0c2589efa 100644 --- a/app/sql.js +++ b/app/sql.js @@ -1540,6 +1540,8 @@ async function getGrantAuthorisationsForPrimaryPubKey(primaryDevicePubKey) { async function createOrUpdatePairingAuthorisation(data) { const { primaryDevicePubKey, secondaryDevicePubKey, grantSignature } = data; + // remove any existing authorisation for this pubkey (we allow only one secondary device for now) + await removePairingAuthorisationForPrimaryPubKey(primaryDevicePubKey); await db.run( `INSERT OR REPLACE INTO ${PAIRING_AUTHORISATIONS_TABLE} ( @@ -1562,6 +1564,15 @@ async function createOrUpdatePairingAuthorisation(data) { ); } +async function removePairingAuthorisationForPrimaryPubKey(pubKey) { + await db.run( + `DELETE FROM ${PAIRING_AUTHORISATIONS_TABLE} WHERE primaryDevicePubKey = $primaryDevicePubKey;`, + { + $primaryDevicePubKey: pubKey, + } + ); +} + async function removePairingAuthorisationForSecondaryPubKey(pubKey) { await db.run( `DELETE FROM ${PAIRING_AUTHORISATIONS_TABLE} WHERE secondaryDevicePubKey = $secondaryDevicePubKey;`, diff --git a/integration_test/add_friends_test.js b/integration_test/add_friends_test.js index 90dbcf230..cd3f4566e 100644 --- a/integration_test/add_friends_test.js +++ b/integration_test/add_friends_test.js @@ -18,18 +18,16 @@ describe('Add friends', function() { const app1Props = { mnemonic: common.TEST_MNEMONIC1, displayName: common.TEST_DISPLAY_NAME1, - stubSnode: true, }; const app2Props = { mnemonic: common.TEST_MNEMONIC2, displayName: common.TEST_DISPLAY_NAME2, - stubSnode: true, }; [app, app2] = await Promise.all([ common.startAndStub(app1Props), - common.startAndStub2(app2Props), + common.startAndStubN(app2Props, 2), ]); }); @@ -116,5 +114,31 @@ describe('Add friends', function() { ConversationPage.acceptedFriendRequestMessage, 5000 ); + + // app trigger the friend request logic first + const aliceLogs = await app.client.getRenderProcessLogs(); + const bobLogs = await app2.client.getRenderProcessLogs(); + await common.logsContains( + aliceLogs, + `Sending undefined:friend-request message to ${common.TEST_PUBKEY2}` + ); + await common.logsContains( + bobLogs, + `Received a NORMAL_FRIEND_REQUEST from source: ${ + common.TEST_PUBKEY1 + }, primarySource: ${common.TEST_PUBKEY1},` + ); + await common.logsContains( + bobLogs, + `Sending incoming-friend-request-accept:onlineBroadcast message to ${ + common.TEST_PUBKEY1 + }` + ); + await common.logsContains( + aliceLogs, + `Sending outgoing-friend-request-accepted:onlineBroadcast message to ${ + common.TEST_PUBKEY2 + }` + ); }); }); diff --git a/integration_test/closed_group_test.js b/integration_test/closed_group_test.js index 851bdff7a..49aabf0f1 100644 --- a/integration_test/closed_group_test.js +++ b/integration_test/closed_group_test.js @@ -25,9 +25,6 @@ describe('Closed groups', function() { }); it('closedGroup: can create a closed group with a friend and send/receive a message', async () => { - await app.client.element(ConversationPage.globeButtonSection).click(); - await app.client.element(ConversationPage.createClosedGroupButton).click(); - const useSenderKeys = false; // create group and add new friend diff --git a/integration_test/common.js b/integration_test/common.js index 8b8f9b1b5..43bf55897 100644 --- a/integration_test/common.js +++ b/integration_test/common.js @@ -36,19 +36,19 @@ module.exports = { 'faxed mechanic mocked agony unrest loincloth pencil eccentric boyfriend oasis speedy ribbon faxed', TEST_PUBKEY1: '0552b85a43fb992f6bdb122a5a379505a0b99a16f0628ab8840249e2a60e12a413', - TEST_DISPLAY_NAME1: 'integration_tester_1', + TEST_DISPLAY_NAME1: 'tester_Alice', TEST_MNEMONIC2: 'guide inbound jerseys bays nouns basin sulking awkward stockpile ostrich ascend pylons ascend', TEST_PUBKEY2: '054e1ca8681082dbd9aad1cf6fc89a32254e15cba50c75b5a73ac10a0b96bcbd2a', - TEST_DISPLAY_NAME2: 'integration_tester_2', + TEST_DISPLAY_NAME2: 'tester_Bob', TEST_MNEMONIC3: 'alpine lukewarm oncoming blender kiwi fuel lobster upkeep vogue simplest gasp fully simplest', TEST_PUBKEY3: '05f8662b6e83da5a31007cc3ded44c601f191e07999acb6db2314a896048d9036c', - TEST_DISPLAY_NAME3: 'integration_tester_3', + TEST_DISPLAY_NAME3: 'tester_Charlie', /* ************** OPEN GROUPS ****************** */ VALID_GROUP_URL: 'https://chat.getsession.org', @@ -191,20 +191,11 @@ module.exports = { async startAndStub({ mnemonic, displayName, - stubSnode = false, - stubOpenGroups = false, env = 'test-integration-session', }) { const app = await this.startAndAssureCleanedApp(env); - if (stubSnode) { - await this.startStubSnodeServer(); - this.stubSnodeCalls(app); - } - - if (stubOpenGroups) { - this.stubOpenGroupsCalls(app); - } + await this.startStubSnodeServer(); if (mnemonic && displayName) { await this.restoreFromMnemonic(app, mnemonic, displayName); @@ -333,6 +324,11 @@ module.exports = { async addFriendToNewClosedGroup(members, useSenderKeys) { const [app, ...others] = members; + await app.client + .element(ConversationPage.conversationButtonSection) + .click(); + await app.client.element(ConversationPage.createClosedGroupButton).click(); + await this.setValueWrapper( app, ConversationPage.closedGroupNameTextarea, @@ -427,7 +423,7 @@ module.exports = { ); }, - async linkApp2ToApp(app1, app2) { + async linkApp2ToApp(app1, app2, app1Pubkey) { // app needs to be logged in as user1 and app2 needs to be logged out // start the pairing dialog for the first app await app1.client.element(SettingsPage.settingsButtonSection).click(); @@ -452,20 +448,19 @@ module.exports = { await this.setValueWrapper( app2, RegistrationPage.textareaLinkDevicePubkey, - this.TEST_PUBKEY1 + app1Pubkey ); await app2.client.element(RegistrationPage.linkDeviceTriggerButton).click(); - await app1.client.waitForExist(RegistrationPage.toastWrapper, 7000); - let secretWordsapp1 = await app1.client - .element(RegistrationPage.secretToastDescription) + await app1.client.waitForExist(SettingsPage.secretWordsTextInDialog, 7000); + const secretWordsapp1 = await app1.client + .element(SettingsPage.secretWordsTextInDialog) .getText(); - secretWordsapp1 = secretWordsapp1.split(': ')[1]; - await app2.client.waitForExist(RegistrationPage.toastWrapper, 6000); await app2.client .element(RegistrationPage.secretToastDescription) .getText() .should.eventually.be.equal(secretWordsapp1); + await app1.client.element(ConversationPage.allowPairingButton).click(); await app1.client.element(ConversationPage.okButton).click(); // validate device paired in settings list with correct secrets @@ -488,7 +483,7 @@ module.exports = { // validate primary pubkey of app2 is the same that in app1 await app2.webContents .executeJavaScript("window.storage.get('primaryDevicePubKey')") - .should.eventually.be.equal(this.TEST_PUBKEY1); + .should.eventually.be.equal(app1Pubkey); }, async triggerUnlinkApp2FromApp(app1, app2) { @@ -496,9 +491,9 @@ module.exports = { await app2.client.isExisting(RegistrationPage.conversationListContainer) .should.eventually.be.true; - await app1.client.element(ConversationPage.settingsButtonSection).click(); + await app1.client.element(SettingsPage.settingsButtonSection).click(); await app1.client - .element(ConversationPage.settingsRowWithText('Devices')) + .element(SettingsPage.settingsRowWithText('Devices')) .click(); await app1.client.isExisting(ConversationPage.linkDeviceButtonDisabled) .should.eventually.be.true; @@ -508,7 +503,7 @@ module.exports = { await app1.client.waitForExist( ConversationPage.noPairedDeviceMessage, - 2000 + 5000 ); await app1.client.element(ConversationPage.linkDeviceButton).isEnabled() .should.eventually.be.true; @@ -563,27 +558,6 @@ module.exports = { generateSendMessageText: () => `Test message from integration tests ${Date.now()}`, - stubOpenGroupsCalls: app1 => { - app1.webContents.executeJavaScript( - 'window.LokiAppDotNetServerAPI = window.StubAppDotNetAPI;' - ); - }, - - stubSnodeCalls(app1) { - app1.webContents.executeJavaScript( - 'window.LokiMessageAPI = window.StubMessageAPI;' - ); - - app1.webContents.executeJavaScript( - 'window.LokiSnodeAPI = window.StubLokiSnodeAPI;' - ); - }, - - logsContainsString: async (app1, str) => { - const logs = JSON.stringify(await app1.client.getRenderProcessLogs()); - return logs.includes(str); - }, - async startStubSnodeServer() { if (!this.stubSnode) { this.messages = {}; @@ -595,6 +569,7 @@ module.exports = { console.warn('NO PUBKEY'); response.writeHead(400, { 'Content-Type': 'text/html' }); response.end(); + return; } if (request.method === 'POST') { @@ -631,12 +606,57 @@ module.exports = { response.end(); } }); + this.startLocalFileServer(); this.stubSnode.listen(STUB_SNODE_SERVER_PORT); } else { this.messages = {}; } }, + async startLocalFileServer() { + if (!this.fileServer) { + // be sure to run `git submodule update --init && cd session-file-server && yarn install; cd -` + // eslint-disable-next-line global-require + this.fileServer = require('../session-file-server/app'); + } + }, + + async joinOpenGroup(app, openGroupUrl, name) { + await app.client + .element(ConversationPage.conversationButtonSection) + .click(); + await app.client.element(ConversationPage.joinOpenGroupButton).click(); + + await this.setValueWrapper( + app, + ConversationPage.openGroupInputUrl, + openGroupUrl + ); + await app.client + .element(ConversationPage.openGroupInputUrl) + .getValue() + .should.eventually.equal(openGroupUrl); + await app.client.element(ConversationPage.joinOpenGroupButton).click(); + + // validate session loader is shown + await app.client.isExisting(ConversationPage.sessionLoader).should + .eventually.be.true; + // account for slow home internet connection delays... + await app.client.waitForExist( + ConversationPage.sessionToastJoinOpenGroupSuccess, + 60 * 1000 + ); + + // validate overlay is closed + await app.client.isExisting(ConversationPage.leftPaneOverlay).should + .eventually.be.false; + + // validate open chat has been added + await app.client.isExisting( + ConversationPage.rowOpenGroupConversationName(name) + ).should.eventually.be.true; + }, + async stopStubSnodeServer() { if (this.stubSnode) { this.stubSnode.close(); @@ -644,6 +664,32 @@ module.exports = { } }, + /** + * Search for a string in logs + * @param {*} app the render logs to search in + * @param {*} str the string to search (not regex) + * Note: getRenderProcessLogs() clears the app logs each calls. + */ + async logsContains(renderLogs, str, count = undefined) { + const foundLines = renderLogs.filter(log => log.message.includes(str)); + + // eslint-disable-next-line no-unused-expressions + chai.expect( + foundLines.length > 0, + `'${str}' not found in logs but was expected` + ).to.be.true; + + if (count) { + // eslint-disable-next-line no-unused-expressions + chai + .expect( + foundLines.length, + `'${str}' found but not the correct number of times` + ) + .to.be.equal(count); + } + }, + // async killStubSnodeServer() { // return new Promise(resolve => { // exec( diff --git a/integration_test/integration_test.js b/integration_test/integration_test.js index 38a29ea73..e37cac9ae 100644 --- a/integration_test/integration_test.js +++ b/integration_test/integration_test.js @@ -13,6 +13,7 @@ require('./link_device_test'); require('./closed_group_test'); require('./message_functions_test'); require('./settings_test'); +require('./message_sync_test'); require('./sender_keys_test'); before(async () => { diff --git a/integration_test/link_device_test.js b/integration_test/link_device_test.js index 63239e58a..2c7ac9505 100644 --- a/integration_test/link_device_test.js +++ b/integration_test/link_device_test.js @@ -18,12 +18,9 @@ describe('Link Device', function() { const app1Props = { mnemonic: common.TEST_MNEMONIC1, displayName: common.TEST_DISPLAY_NAME1, - stubSnode: true, }; - const app2Props = { - stubSnode: true, - }; + const app2Props = {}; [app, app2] = await Promise.all([ common.startAndStub(app1Props), @@ -37,12 +34,39 @@ describe('Link Device', function() { }); it('linkDevice: link two desktop devices', async () => { - await common.linkApp2ToApp(app, app2); + await common.linkApp2ToApp(app, app2, common.TEST_PUBKEY1); }); it('linkDevice: unlink two devices', async () => { - await common.linkApp2ToApp(app, app2); + await common.linkApp2ToApp(app, app2, common.TEST_PUBKEY1); await common.timeout(1000); await common.triggerUnlinkApp2FromApp(app, app2); }); + + it('linkDevice:sync no groups, closed group, nor open groups', async () => { + await common.linkApp2ToApp(app, app2, common.TEST_PUBKEY1); + await common.timeout(10000); + + // get logs at this stage (getRenderProcessLogs() clears the app logs) + const secondaryRenderLogs = await app2.client.getRenderProcessLogs(); + // pairing request message sent from secondary to primary pubkey + await common.logsContains( + secondaryRenderLogs, + `Sending pairing-request:pairing-request message to ${ + common.TEST_PUBKEY1 + }` + ); + + const primaryRenderLogs = await app.client.getRenderProcessLogs(); + // primary grant pairing request + await common.logsContains( + primaryRenderLogs, + 'Sending pairing-request:pairing-request message to OUR SECONDARY PUBKEY' + ); + + // no friends, no closed groups, no open groups. we should see those message sync in the log + await common.logsContains(primaryRenderLogs, 'No closed group to sync.', 1); + await common.logsContains(primaryRenderLogs, 'No open groups to sync', 1); + await common.logsContains(primaryRenderLogs, 'No contacts to sync.', 1); + }); }); diff --git a/integration_test/message_functions_test.js b/integration_test/message_functions_test.js index 5061b95b8..245664ff1 100644 --- a/integration_test/message_functions_test.js +++ b/integration_test/message_functions_test.js @@ -4,7 +4,7 @@ /* eslint-disable import/no-extraneous-dependencies */ const path = require('path'); -const { after, before, describe, it } = require('mocha'); +const { afterEach, beforeEach, describe, it } = require('mocha'); const common = require('./common'); const ConversationPage = require('./page-objects/conversation.page'); @@ -14,25 +14,22 @@ describe('Message Functions', function() { this.timeout(60000); this.slow(15000); - before(async () => { + beforeEach(async () => { await common.killallElectron(); await common.stopStubSnodeServer(); [app, app2] = await common.startAppsAsFriends(); }); - after(async () => { + afterEach(async () => { await common.stopApp(app); await common.killallElectron(); await common.stopStubSnodeServer(); }); it('can send attachment', async () => { - await app.client.element(ConversationPage.globeButtonSection).click(); - await app.client.element(ConversationPage.createClosedGroupButton).click(); - // create group and add new friend - await common.addFriendToNewClosedGroup([app, app2]); + await common.addFriendToNewClosedGroup([app, app2], false); // send attachment from app1 to closed group const fileLocation = path.join(__dirname, 'test_attachment'); @@ -53,6 +50,8 @@ describe('Message Functions', function() { }); it('can delete message', async () => { + // create group and add new friend + await common.addFriendToNewClosedGroup([app, app2], false); const messageText = 'delete_me'; await common.sendMessage(app, messageText); @@ -71,7 +70,7 @@ describe('Message Functions', function() { .click(); await app.client.element(ConversationPage.deleteMessageCtxButton).click(); - // delete messaage from modal + // delete message from modal await app.client.waitForExist( ConversationPage.deleteMessageModalButton, 5000 diff --git a/integration_test/message_sync_test.js b/integration_test/message_sync_test.js index 4d40c97a9..46e0f275a 100644 --- a/integration_test/message_sync_test.js +++ b/integration_test/message_sync_test.js @@ -1,49 +1,146 @@ /* eslint-disable func-names */ /* eslint-disable import/no-extraneous-dependencies */ -const { afterEach, beforeEach, describe, it } = require('mocha'); +const { after, before, describe, it } = require('mocha'); const common = require('./common'); describe('Message Syncing', function() { - let app; - let app2; + let Alice1; + let Bob1; + let Alice2; this.timeout(60000); this.slow(15000); - beforeEach(async () => { + // this test suite builds a complex usecase over several tests, + // so you need to run all of those tests together (running only one might fail) + before(async () => { await common.killallElectron(); await common.stopStubSnodeServer(); - const app1Props = { - mnemonic: common.TEST_MNEMONIC1, - displayName: common.TEST_DISPLAY_NAME1, - stubSnode: true, - }; - - const app2Props = { - mnemonic: common.TEST_MNEMONIC2, - displayName: common.TEST_DISPLAY_NAME2, - stubSnode: true, - }; - - [app, app2] = await Promise.all([ - common.startAndStub(app1Props), - common.startAndStubN(app2Props, 2), - ]); + const alice2Props = {}; + + [Alice1, Bob1] = await common.startAppsAsFriends(); // Alice and Bob are friends + + await common.addFriendToNewClosedGroup([Alice1, Bob1], false); + await common.joinOpenGroup( + Alice1, + common.VALID_GROUP_URL, + common.VALID_GROUP_NAME + ); + + Alice2 = await common.startAndStubN(alice2Props, 4); // Alice secondary, just start the app for now. no linking }); - afterEach(async () => { + after(async () => { await common.killallElectron(); await common.stopStubSnodeServer(); }); - it('message syncing between linked devices', async () => { - await common.linkApp2ToApp(app, app2); - }); + it('message syncing with 1 friend, 1 closed group, 1 open group', async () => { + // Alice1 has: + // * no linked device + // * Bob is a friend + // * one open group + // * one closed group with Bob inside + + // Bob1 has: + // * no linked device + // * Alice as a friend + // * one open group with Alice + + // Linking Alice2 to Alice1 + // alice2 should trigger auto FR with bob1 as it's one of her friend + // and alice2 should trigger a SESSION_REQUEST with bob1 as he is in a closed group with her + await common.linkApp2ToApp(Alice1, Alice2, common.TEST_PUBKEY1); + await common.timeout(25000); + + // validate pubkey of app2 is the set + const alice2Pubkey = await Alice2.webContents.executeJavaScript( + 'window.textsecure.storage.user.getNumber()' + ); + alice2Pubkey.should.have.lengthOf(66); + + const alice1Logs = await Alice1.client.getRenderProcessLogs(); + const bob1Logs = await Bob1.client.getRenderProcessLogs(); + const alice2Logs = await Alice2.client.getRenderProcessLogs(); + + // validate primary alice + await common.logsContains( + alice1Logs, + 'Sending closed-group-sync-send:outgoing message to OUR SECONDARY PUBKEY', + 1 + ); + await common.logsContains( + alice1Logs, + 'Sending open-group-sync-send:outgoing message to OUR SECONDARY PUBKEY', + 1 + ); + await common.logsContains( + alice1Logs, + 'Sending contact-sync-send:outgoing message to OUR SECONDARY PUBKEY', + 1 + ); + + // validate secondary alice + // what is expected is + // alice2 receives group sync, contact sync and open group sync + // alice2 triggers session request with closed group members and autoFR with contact sync received + // once autoFR is auto-accepted, alice2 trigger contact sync + await common.logsContains( + alice2Logs, + 'Got sync group message with group id', + 1 + ); + await common.logsContains( + alice2Logs, + 'Received GROUP_SYNC with open groups: [chat.getsession.org]', + 1 + ); + await common.logsContains( + alice2Logs, + `Sending auto-friend-request:friend-request message to ${ + common.TEST_PUBKEY2 + }`, + 1 + ); + await common.logsContains( + alice2Logs, + `Sending session-request:friend-request message to ${ + common.TEST_PUBKEY2 + }`, + 1 + ); + await common.logsContains( + alice2Logs, + `Sending contact-sync-send:outgoing message to OUR_PRIMARY_PUBKEY`, + 1 + ); - it('unlink two devices', async () => { - await common.linkApp2ToApp(app, app2); - await common.timeout(1000); - await common.triggerUnlinkApp2FromApp(app, app2); + // validate primary bob + // what is expected is + // bob1 receives session request from alice2 + // bob1 accept auto fr by sending a bg message + // once autoFR is auto-accepted, alice2 trigger contact sync + await common.logsContains( + bob1Logs, + `Received SESSION_REQUEST from source: ${alice2Pubkey}`, + 1 + ); + await common.logsContains( + bob1Logs, + `Received AUTO_FRIEND_REQUEST from source: ${alice2Pubkey}`, + 1 + ); + await common.logsContains( + bob1Logs, + `Sending auto-friend-accept:onlineBroadcast message to ${alice2Pubkey}`, + 1 + ); + // be sure only one autoFR accept was sent (even if multi device, we need to reply to that specific device only) + await common.logsContains( + bob1Logs, + `Sending auto-friend-accept:onlineBroadcast message to`, + 1 + ); }); }); diff --git a/integration_test/open_group_test.js b/integration_test/open_group_test.js index dba3d13e7..08a22d785 100644 --- a/integration_test/open_group_test.js +++ b/integration_test/open_group_test.js @@ -7,7 +7,7 @@ const ConversationPage = require('./page-objects/conversation.page'); describe('Open groups', function() { let app; - this.timeout(30000); + this.timeout(40000); this.slow(15000); beforeEach(async () => { @@ -15,7 +15,6 @@ describe('Open groups', function() { const login = { mnemonic: common.TEST_MNEMONIC1, displayName: common.TEST_DISPLAY_NAME1, - stubOpenGroups: true, }; app = await common.startAndStub(login); }); @@ -24,46 +23,25 @@ describe('Open groups', function() { await common.killallElectron(); }); - // reduce code duplication to get the initial join - async function joinOpenGroup(url, name) { - await app.client.element(ConversationPage.globeButtonSection).click(); - await app.client.element(ConversationPage.joinOpenGroupButton).click(); - - await common.setValueWrapper(app, ConversationPage.openGroupInputUrl, url); - await app.client - .element(ConversationPage.openGroupInputUrl) - .getValue() - .should.eventually.equal(url); - await app.client.element(ConversationPage.joinOpenGroupButton).click(); - - // validate session loader is shown - await app.client.isExisting(ConversationPage.sessionLoader).should - .eventually.be.true; - // account for slow home internet connection delays... - await app.client.waitForExist( - ConversationPage.sessionToastJoinOpenGroupSuccess, - 60 * 1000 - ); - - // validate overlay is closed - await app.client.isExisting(ConversationPage.leftPaneOverlay).should - .eventually.be.false; - - // validate open chat has been added - await app.client.isExisting( - ConversationPage.rowOpenGroupConversationName(name) - ).should.eventually.be.true; - } - it('openGroup: works with valid open group url', async () => { - await joinOpenGroup(common.VALID_GROUP_URL, common.VALID_GROUP_NAME); + await common.joinOpenGroup( + app, + common.VALID_GROUP_URL, + common.VALID_GROUP_NAME + ); }); it('openGroup: cannot join two times the same open group', async () => { - await joinOpenGroup(common.VALID_GROUP_URL2, common.VALID_GROUP_NAME2); + await common.joinOpenGroup( + app, + common.VALID_GROUP_URL2, + common.VALID_GROUP_NAME2 + ); // adding a second time the same open group - await app.client.element(ConversationPage.globeButtonSection).click(); + await app.client + .element(ConversationPage.conversationButtonSection) + .click(); await app.client.element(ConversationPage.joinOpenGroupButton).click(); await common.setValueWrapper( @@ -88,7 +66,9 @@ describe('Open groups', function() { it('openGroup: can send message to open group', async () => { // join dev-chat group - await app.client.element(ConversationPage.globeButtonSection).click(); + await app.client + .element(ConversationPage.conversationButtonSection) + .click(); await app.client.element(ConversationPage.joinOpenGroupButton).click(); await common.setValueWrapper( diff --git a/integration_test/page-objects/conversation.page.js b/integration_test/page-objects/conversation.page.js index 64515b91d..4a0b2394f 100644 --- a/integration_test/page-objects/conversation.page.js +++ b/integration_test/page-objects/conversation.page.js @@ -40,8 +40,6 @@ module.exports = { '//*[contains(@class, "session-modal")]//div[contains(string(), "Delete") and contains(@class, "session-button")]', // channels - globeButtonSection: - '//*[contains(@class,"session-icon-button") and .//*[contains(@class, "globe")]]', joinOpenGroupButton: commonPage.divRoleButtonWithText('Join Open Group'), openGroupInputUrl: commonPage.textAreaWithPlaceholder('chat.getsession.org'), sessionToastJoinOpenGroupSuccess: commonPage.toastWithText( diff --git a/integration_test/page-objects/settings.page.js b/integration_test/page-objects/settings.page.js index 3adf33f05..b7d532d4d 100644 --- a/integration_test/page-objects/settings.page.js +++ b/integration_test/page-objects/settings.page.js @@ -17,4 +17,7 @@ module.exports = { // Confirm is a boolean. Selects confirmation input passwordSetModalInput: _confirm => `//input[@id = 'password-modal-input${_confirm ? '-confirm' : ''}']`, + + secretWordsTextInDialog: + '//div[@class="device-pairing-dialog__secret-words"]/div[@class="subtle"]', }; diff --git a/integration_test/registration_test.js b/integration_test/registration_test.js index 69bf4b27f..f9ba07704 100644 --- a/integration_test/registration_test.js +++ b/integration_test/registration_test.js @@ -5,6 +5,7 @@ const { afterEach, beforeEach, describe, it } = require('mocha'); const common = require('./common'); +const SettingsPage = require('./page-objects/settings.page'); const RegistrationPage = require('./page-objects/registration.page'); const ConversationPage = require('./page-objects/conversation.page'); @@ -104,7 +105,6 @@ describe('Window Test and Login', function() { const login = { mnemonic: common.TEST_MNEMONIC1, displayName: common.TEST_DISPLAY_NAME1, - stubOpenGroups: true, }; app = await common.startAndStub(login); @@ -117,7 +117,7 @@ describe('Window Test and Login', function() { .executeJavaScript("window.storage.get('primaryDevicePubKey')") .should.eventually.be.equal(common.TEST_PUBKEY1); // delete account - await app.client.element(ConversationPage.settingsButtonSection).click(); + await app.client.element(SettingsPage.settingsButtonSection).click(); await app.client.element(ConversationPage.deleteAccountButton).click(); await app.client.isExisting(ConversationPage.descriptionDeleteAccount) .should.eventually.be.true; diff --git a/integration_test/sender_keys_test.js b/integration_test/sender_keys_test.js index 446d19fce..836af43c2 100644 --- a/integration_test/sender_keys_test.js +++ b/integration_test/sender_keys_test.js @@ -39,15 +39,12 @@ async function makeFriendsPlusMessage(app, [app2, pubkey]) { ); // Click away so we can call this function again - await app.client.element(ConversationPage.globeButtonSection).click(); + await app.client.element(ConversationPage.conversationButtonSection).click(); } async function testTwoMembers() { const [app, app2] = await common.startAppsAsFriends(); - await app.client.element(ConversationPage.globeButtonSection).click(); - await app.client.element(ConversationPage.createClosedGroupButton).click(); - const useSenderKeys = true; // create group and add new friend @@ -106,9 +103,6 @@ async function testThreeMembers() { const useSenderKeys = true; - await app1.client.element(ConversationPage.globeButtonSection).click(); - await app1.client.element(ConversationPage.createClosedGroupButton).click(); - // 3. Add all three to the group await common.addFriendToNewClosedGroup([app1, app2, app3], useSenderKeys); diff --git a/integration_test/settings_test.js b/integration_test/settings_test.js index b166592b3..cf463a411 100644 --- a/integration_test/settings_test.js +++ b/integration_test/settings_test.js @@ -27,7 +27,6 @@ describe('Settings', function() { const appProps = { mnemonic: common.TEST_MNEMONIC1, displayName: common.TEST_DISPLAY_NAME1, - stubSnode: true, }; app = await common.startAndStub(appProps); diff --git a/integration_test/stubs/stub_snode_api.js b/integration_test/stubs/stub_snode_api.js new file mode 100644 index 000000000..f0739da9e --- /dev/null +++ b/integration_test/stubs/stub_snode_api.js @@ -0,0 +1,9 @@ +/* eslint-disable class-methods-use-this */ + +class StubSnodeAPI { + async refreshSwarmNodesForPubKey() { + return []; + } +} + +module.exports = StubSnodeAPI; diff --git a/js/background.js b/js/background.js index 92bc5e286..70a8c557f 100644 --- a/js/background.js +++ b/js/background.js @@ -646,7 +646,7 @@ active: true, expireTimer: 0, avatar: '', - is_medium_group: true, + is_medium_group: false, }, confirm: () => {}, }; @@ -854,6 +854,7 @@ window.friends.friendRequestStatusEnum.friends ); + textsecure.messaging.sendGroupSyncMessage([convo]); appView.openConversation(groupId, {}); }; @@ -1444,9 +1445,11 @@ // TODO: we should ensure the message was sent and retry automatically if not await libloki.api.sendUnpairingMessageToSecondary(pubKey); // Remove all traces of the device - ConversationController.deleteContact(pubKey); - Whisper.events.trigger('refreshLinkedDeviceList'); - callback(); + setTimeout(() => { + ConversationController.deleteContact(pubKey); + Whisper.events.trigger('refreshLinkedDeviceList'); + callback(); + }, 1000); }); } @@ -1773,7 +1776,6 @@ const details = ev.contactDetails; const id = details.number; - libloki.api.debug.logContactSync( 'Got sync contact message with', id, @@ -1828,12 +1830,21 @@ await conversation.setSecondaryStatus(true, ourPrimaryKey); } - if (conversation.isFriendRequestStatusNoneOrExpired()) { - libloki.api.sendAutoFriendRequestMessage(conversation.id); - } else { - // Accept any pending friend requests if there are any - conversation.onAcceptFriendRequest({ blockSync: true }); - } + const otherDevices = await libloki.storage.getPairedDevicesFor(id); + const devices = [id, ...otherDevices]; + const deviceConversations = await Promise.all( + devices.map(d => + ConversationController.getOrCreateAndWait(d, 'private') + ) + ); + deviceConversations.forEach(device => { + if (device.isFriendRequestStatusNoneOrExpired()) { + libloki.api.sendAutoFriendRequestMessage(device.id); + } else { + // Accept any pending friend requests if there are any + device.onAcceptFriendRequest({ blockSync: true }); + } + }); if (details.profileKey) { const profileKey = window.Signal.Crypto.arrayBufferToBase64( diff --git a/js/models/conversations.js b/js/models/conversations.js index 8e5d17298..4ec89bbc2 100644 --- a/js/models/conversations.js +++ b/js/models/conversations.js @@ -253,22 +253,26 @@ // Friend request message conmfirmations (Accept / Decline) are always // sent to the primary device conversation const messages = await window.Signal.Data.getMessagesByConversation( - this.id, + this.getPrimaryDevicePubKey(), { - limit: 1, + limit: 5, MessageCollection: Whisper.MessageCollection, type: 'friend-request', } ); - const lastMessageModel = messages.at(0); - if (lastMessageModel) { - lastMessageModel.acceptFriendRequest(); + let lastMessage = null; + messages.forEach(m => { + m.acceptFriendRequest(); + lastMessage = m; + }); + + if (lastMessage) { await this.markRead(); window.Whisper.events.trigger( 'showConversation', this.id, - lastMessageModel.id + lastMessage.id ); } }, @@ -978,7 +982,7 @@ Conversation: Whisper.Conversation, }); }, - async respondToAllFriendRequests(options) { + async updateAllFriendRequestsMessages(options) { const { response, status, direction = null } = options; // Ignore if no response supplied if (!response) { @@ -1032,8 +1036,8 @@ }) ); }, - async respondToAllPendingFriendRequests(options) { - return this.respondToAllFriendRequests({ + async updateAllPendingFriendRequestsMessages(options) { + return this.updateAllFriendRequestsMessages({ ...options, status: 'pending', }); @@ -1048,7 +1052,7 @@ // We have declined an incoming friend request async onDeclineFriendRequest() { this.setFriendRequestStatus(FriendRequestStatusEnum.none); - await this.respondToAllPendingFriendRequests({ + await this.updateAllPendingFriendRequestsMessages({ response: 'declined', direction: 'incoming', }); @@ -1062,13 +1066,16 @@ }, // We have accepted an incoming friend request async onAcceptFriendRequest(options = {}) { + if (this.get('type') !== Message.PRIVATE) { + return; + } if (this.unlockTimer) { clearTimeout(this.unlockTimer); } if (this.hasReceivedFriendRequest()) { this.setFriendRequestStatus(FriendRequestStatusEnum.friends, options); - await this.respondToAllFriendRequests({ + await this.updateAllFriendRequestsMessages({ response: 'accepted', direction: 'incoming', status: ['pending', 'expired'], @@ -1078,6 +1085,12 @@ window.textsecure.OutgoingMessage.DebugMessageType .INCOMING_FR_ACCEPTED ); + } else if (this.isFriendRequestStatusNoneOrExpired()) { + // send AFR if we haven't sent a message before + const autoFrMessage = textsecure.OutgoingMessage.buildAutoFriendRequestMessage( + this.id + ); + await autoFrMessage.sendToNumber(this.id, false); } }, // Our outgoing friend request has been accepted @@ -1090,7 +1103,7 @@ } if (this.hasSentFriendRequest()) { this.setFriendRequestStatus(FriendRequestStatusEnum.friends); - await this.respondToAllFriendRequests({ + await this.updateAllFriendRequestsMessages({ response: 'accepted', status: ['pending', 'expired'], }); @@ -1122,7 +1135,7 @@ } // Change any pending outgoing friend requests to expired - await this.respondToAllPendingFriendRequests({ + await this.updateAllPendingFriendRequestsMessages({ response: 'expired', direction: 'outgoing', }); @@ -1135,7 +1148,7 @@ await Promise.all([ this.setFriendRequestStatus(FriendRequestStatusEnum.friends), // Accept all outgoing FR - this.respondToAllPendingFriendRequests({ + this.updateAllPendingFriendRequestsMessages({ direction: 'outgoing', response: 'accepted', }), diff --git a/js/models/messages.js b/js/models/messages.js index 8b1122eb8..888584035 100644 --- a/js/models/messages.js +++ b/js/models/messages.js @@ -415,21 +415,21 @@ }, async acceptFriendRequest() { - const primaryDevicePubKey = this.attributes.conversationId; - if (this.get('friendStatus') !== 'pending') { return; } - const allDevices = await libloki.storage.getAllDevicePubKeysForPrimaryPubKey( - primaryDevicePubKey + const devicePubKey = this.get('conversationId'); + const otherDevices = await libloki.storage.getPairedDevicesFor( + devicePubKey ); + const allDevices = [devicePubKey, ...otherDevices]; // Set profile name to primary conversation let profileName; - const allConversationsWithUser = allDevices.map(d => - ConversationController.get(d) - ); + const allConversationsWithUser = allDevices + .map(d => ConversationController.get(d)) + .filter(c => Boolean(c)); allConversationsWithUser.forEach(conversation => { // If we somehow received an old friend request (e.g. after having restored // from seed, we won't be able to accept it, we should initiate our own @@ -447,6 +447,9 @@ // If you don't have a profile name for this device, and profileName is set, // add profileName to conversation. + const primaryDevicePubKey = + (await window.Signal.Data.getPrimaryDeviceFor(devicePubKey)) || + devicePubKey; const primaryConversation = allConversationsWithUser.find( c => c.id === primaryDevicePubKey ); @@ -471,13 +474,23 @@ if (this.get('friendStatus') !== 'pending') { return; } - const conversation = this.getConversation(); this.set({ friendStatus: 'declined' }); await window.Signal.Data.saveMessage(this.attributes, { Message: Whisper.Message, }); - conversation.onDeclineFriendRequest(); + + const devicePubKey = this.attributes.conversationId; + const otherDevices = await libloki.storage.getPairedDevicesFor( + devicePubKey + ); + const allDevices = [devicePubKey, ...otherDevices]; + const allConversationsWithUser = allDevices + .map(d => ConversationController.get(d)) + .filter(c => Boolean(c)); + allConversationsWithUser.forEach(conversation => { + conversation.onDeclineFriendRequest(); + }); }, getPropsForFriendRequest() { const friendStatus = this.get('friendStatus') || 'pending'; @@ -2083,6 +2096,7 @@ return false; }, async handleSessionRequest(source, confirm) { + window.console.log(`Received SESSION_REQUEST from source: ${source}`); window.libloki.api.sendSessionEstablishedMessage(source); confirm(); }, @@ -2234,11 +2248,10 @@ return null; } } - const conversation = conversationPrimary; - return conversation.queueJob(async () => { + return conversationPrimary.queueJob(async () => { window.log.info( - `Starting handleDataMessage for message ${message.idForLogging()} in conversation ${conversation.idForLogging()}` + `Starting handleDataMessage for message ${message.idForLogging()} in conversation ${conversationPrimary.idForLogging()}` ); const GROUP_TYPES = textsecure.protobuf.GroupContext.Type; const type = message.get('type'); @@ -2251,7 +2264,7 @@ try { const now = new Date().getTime(); let attributes = { - ...conversation.attributes, + ...conversationPrimary.attributes, }; if (dataMessage.group) { @@ -2269,18 +2282,18 @@ }; groupUpdate = - conversation.changedAttributes( + conversationPrimary.changedAttributes( _.pick(dataMessage.group, 'name', 'avatar') ) || {}; const addedMembers = _.difference( attributes.members, - conversation.get('members') + conversationPrimary.get('members') ); if (addedMembers.length > 0) { groupUpdate.joined = addedMembers; } - if (conversation.get('left')) { + if (conversationPrimary.get('left')) { // TODO: Maybe we shouldn't assume this message adds us: // we could maybe still get this message by mistake window.log.warn('re-added to a left group'); @@ -2294,7 +2307,7 @@ // Check if anyone got kicked: const removedMembers = _.difference( - conversation.get('members'), + conversationPrimary.get('members'), attributes.members ); @@ -2316,7 +2329,7 @@ groupUpdate = { left: source }; } attributes.members = _.without( - conversation.get('members'), + conversationPrimary.get('members'), source ); } @@ -2349,7 +2362,7 @@ attachments: dataMessage.attachments, body: dataMessage.body, contact: dataMessage.contact, - conversationId: conversation.id, + conversationId: conversationPrimary.id, decrypted_at: now, errors: [], flags: dataMessage.flags, @@ -2363,7 +2376,7 @@ if (type === 'outgoing') { const receipts = Whisper.DeliveryReceipts.forMessage( - conversation, + conversationPrimary, message ); receipts.forEach(receipt => @@ -2376,10 +2389,10 @@ ); } attributes.active_at = now; - conversation.set(attributes); + conversationPrimary.set(attributes); // Re-enable typing if re-joined the group - conversation.updateTextInputState(); + conversationPrimary.updateTextInputState(); if (message.isExpirationTimerUpdate()) { message.set({ @@ -2388,7 +2401,7 @@ expireTimer: dataMessage.expireTimer, }, }); - conversation.set({ expireTimer: dataMessage.expireTimer }); + conversationPrimary.set({ expireTimer: dataMessage.expireTimer }); } else if (dataMessage.expireTimer) { message.set({ expireTimer: dataMessage.expireTimer }); } @@ -2400,7 +2413,7 @@ message.isExpirationTimerUpdate() || expireTimer; if (shouldLogExpireTimerChange) { window.log.info("Update conversation 'expireTimer'", { - id: conversation.idForLogging(), + id: conversationPrimary.idForLogging(), expireTimer, source: 'handleDataMessage', }); @@ -2408,8 +2421,11 @@ if (!message.isEndSession()) { if (dataMessage.expireTimer) { - if (dataMessage.expireTimer !== conversation.get('expireTimer')) { - conversation.updateExpirationTimer( + if ( + dataMessage.expireTimer !== + conversationPrimary.get('expireTimer') + ) { + conversationPrimary.updateExpirationTimer( dataMessage.expireTimer, source, message.get('received_at'), @@ -2419,18 +2435,18 @@ ); } } else if ( - conversation.get('expireTimer') && + conversationPrimary.get('expireTimer') && // We only turn off timers if it's not a group update !message.isGroupUpdate() ) { - conversation.updateExpirationTimer( + conversationPrimary.updateExpirationTimer( null, source, message.get('received_at') ); } } else { - const endSessionType = conversation.isSessionResetReceived() + const endSessionType = conversationPrimary.isSessionResetReceived() ? 'ongoing' : 'done'; this.set({ endSessionType }); @@ -2462,11 +2478,11 @@ message.attributes.body && message.attributes.body.indexOf(`@${ourNumber}`) !== -1 ) { - conversation.set({ mentionedUs: true }); + conversationPrimary.set({ mentionedUs: true }); } - conversation.set({ - unreadCount: conversation.get('unreadCount') + 1, + conversationPrimary.set({ + unreadCount: conversationPrimary.get('unreadCount') + 1, isArchived: false, }); } @@ -2474,7 +2490,7 @@ if (type === 'outgoing') { const reads = Whisper.ReadReceipts.forMessage( - conversation, + conversationPrimary, message ); if (reads.length) { @@ -2485,39 +2501,35 @@ } // A sync'd message to ourself is automatically considered read and delivered - if (conversation.isMe()) { + if (conversationPrimary.isMe()) { message.set({ - read_by: conversation.getRecipients(), - delivered_to: conversation.getRecipients(), + read_by: conversationPrimary.getRecipients(), + delivered_to: conversationPrimary.getRecipients(), }); } - message.set({ recipients: conversation.getRecipients() }); + message.set({ recipients: conversationPrimary.getRecipients() }); } - const conversationTimestamp = conversation.get('timestamp'); + const conversationTimestamp = conversationPrimary.get('timestamp'); if ( !conversationTimestamp || message.get('sent_at') > conversationTimestamp ) { - conversation.lastMessage = message.getNotificationText(); - conversation.set({ + conversationPrimary.lastMessage = message.getNotificationText(); + conversationPrimary.set({ timestamp: message.get('sent_at'), }); } - const sendingDeviceConversation = await ConversationController.getOrCreateAndWait( - source, - 'private' - ); if (dataMessage.profileKey) { const profileKey = dataMessage.profileKey.toString('base64'); if (source === textsecure.storage.user.getNumber()) { - conversation.set({ profileSharing: true }); - } else if (conversation.isPrivate()) { - conversation.setProfileKey(profileKey); + conversationPrimary.set({ profileSharing: true }); + } else if (conversationPrimary.isPrivate()) { + conversationPrimary.setProfileKey(profileKey); } else { - sendingDeviceConversation.setProfileKey(profileKey); + conversationOrigin.setProfileKey(profileKey); } } @@ -2544,8 +2556,8 @@ and that user just sent us a friend request. */ - const isFriend = sendingDeviceConversation.isFriend(); - const hasSentFriendRequest = sendingDeviceConversation.hasSentFriendRequest(); + const isFriend = conversationOrigin.isFriend(); + const hasSentFriendRequest = conversationOrigin.hasSentFriendRequest(); autoAccept = isFriend || hasSentFriendRequest; if (autoAccept) { @@ -2559,16 +2571,17 @@ if (isFriend) { window.Whisper.events.trigger('endSession', source); } else if (hasSentFriendRequest) { - await sendingDeviceConversation.onFriendRequestAccepted(); + await conversationOrigin.onFriendRequestAccepted(); } else { - await sendingDeviceConversation.onFriendRequestReceived(); + await conversationOrigin.onFriendRequestReceived(); } } else if (message.get('type') !== 'outgoing') { // Ignore 'outgoing' messages because they are sync messages - await sendingDeviceConversation.onFriendRequestAccepted(); + await conversationOrigin.onFriendRequestAccepted(); } } + // We need to map the original message source to the primary device if (source !== ourNumber) { message.set({ source: primarySource }); } @@ -2586,11 +2599,11 @@ await window.Signal.Data.updateConversation( conversationId, - conversation.attributes, + conversationPrimary.attributes, { Conversation: Whisper.Conversation } ); - conversation.trigger('newmessage', message); + conversationPrimary.trigger('newmessage', message); try { // We go to the database here because, between the message save above and @@ -2628,9 +2641,9 @@ if (message.get('unread')) { // Need to do this here because the conversation has already changed states if (autoAccept) { - await conversation.notifyFriendRequest(source, 'accepted'); + await conversationPrimary.notifyFriendRequest(source, 'accepted'); } else { - await conversation.notify(message); + await conversationPrimary.notify(message); } } diff --git a/js/modules/loki_file_server_api.js b/js/modules/loki_file_server_api.js index 241741cb7..ed8533114 100644 --- a/js/modules/loki_file_server_api.js +++ b/js/modules/loki_file_server_api.js @@ -1,4 +1,4 @@ -/* global log, libloki, process, window */ +/* global log, libloki, window */ /* global storage: false */ /* global Signal: false */ /* global log: false */ @@ -20,13 +20,8 @@ class LokiFileServerInstance { // LokiAppDotNetAPI (base) should not know about LokiFileServer. async establishConnection(serverUrl, options) { // why don't we extend this? - if (process.env.USE_STUBBED_NETWORK) { - // eslint-disable-next-line global-require - const StubAppDotNetAPI = require('../../integration_test/stubs/stub_app_dot_net_api.js'); - this._server = new StubAppDotNetAPI(this.ourKey, serverUrl); - } else { - this._server = new LokiAppDotNetAPI(this.ourKey, serverUrl); - } + this._server = new LokiAppDotNetAPI(this.ourKey, serverUrl); + // make sure pubKey & pubKeyHex are set in _server this.pubKey = this._server.getPubKeyForUrl(); diff --git a/js/modules/loki_rpc.js b/js/modules/loki_rpc.js index dfbce16ae..0aee3b698 100644 --- a/js/modules/loki_rpc.js +++ b/js/modules/loki_rpc.js @@ -67,7 +67,7 @@ const makeOnionRequest = async ( for (let i = firstPos; i > -1; i -= 1) { let dest; - const relayingToFinalDestination = i === 0; // if last position + const relayingToFinalDestination = i === firstPos; // if last position if (relayingToFinalDestination && finalRelayOptions) { dest = { diff --git a/libloki/api.js b/libloki/api.js index 47a31c018..179d0d426 100644 --- a/libloki/api.js +++ b/libloki/api.js @@ -21,9 +21,7 @@ const debugFlags = DebugFlagsEnum.ALL; const debugLogFn = (...args) => { - // eslint-disable-next-line no-constant-condition - if (true) { - // process.env.NODE_ENV.includes('test-integration') || + if (window.lokiFeatureFlags.debugMessageLogs) { window.console.warn(...args); } }; @@ -47,7 +45,7 @@ } function logContactSync(...args) { - if (debugFlags & DebugFlagsEnum.GROUP_CONTACT_MESSAGES) { + if (debugFlags & DebugFlagsEnum.CONTACT_SYNC_MESSAGES) { debugLogFn(...args); } } @@ -91,38 +89,22 @@ const message = textsecure.OutgoingMessage.buildSessionEstablishedMessage( pubKey ); - await message.sendToNumber(pubKey); + await message.sendToNumber(pubKey, false); } async function sendBackgroundMessage(pubKey, debugMessageType) { - const primaryPubKey = await getPrimaryDevicePubkey(pubKey); - if (primaryPubKey !== pubKey) { - // if we got the secondary device pubkey first, - // call ourself again with the primary device pubkey - await sendBackgroundMessage(primaryPubKey, debugMessageType); - return; - } - const backgroundMessage = textsecure.OutgoingMessage.buildBackgroundMessage( pubKey, debugMessageType ); - await backgroundMessage.sendToNumber(pubKey); + await backgroundMessage.sendToNumber(pubKey, false); } async function sendAutoFriendRequestMessage(pubKey) { - const primaryPubKey = await getPrimaryDevicePubkey(pubKey); - if (primaryPubKey !== pubKey) { - // if we got the secondary device pubkey first, - // call ourself again with the primary device pubkey - await sendAutoFriendRequestMessage(primaryPubKey); - return; - } - const autoFrMessage = textsecure.OutgoingMessage.buildAutoFriendRequestMessage( pubKey ); - await autoFrMessage.sendToNumber(pubKey); + await autoFrMessage.sendToNumber(pubKey, false); } function createPairingAuthorisationProtoMessage({ @@ -158,7 +140,7 @@ const unpairingMessage = textsecure.OutgoingMessage.buildUnpairingMessage( pubKey ); - return unpairingMessage.sendToNumber(pubKey); + return unpairingMessage.sendToNumber(pubKey, false); } // Serialise as ... // This is an implementation of the reciprocal of contacts_parser.js @@ -298,7 +280,7 @@ callback ); - pairingRequestMessage.sendToNumber(recipientPubKey); + pairingRequestMessage.sendToNumber(recipientPubKey, false); }); return p; } @@ -348,6 +330,7 @@ createContactSyncProtoMessage, createGroupSyncProtoMessage, createOpenGroupsSyncProtoMessage, + getPrimaryDevicePubkey, debug, }; })(); diff --git a/libtextsecure/account_manager.js b/libtextsecure/account_manager.js index e037305c5..e88c1581a 100644 --- a/libtextsecure/account_manager.js +++ b/libtextsecure/account_manager.js @@ -619,10 +619,13 @@ } ); // Send sync messages - const conversations = window.getConversations().models; - textsecure.messaging.sendContactSyncMessage(conversations); - textsecure.messaging.sendGroupSyncMessage(conversations); - textsecure.messaging.sendOpenGroupsSyncMessage(conversations); + // bad hack to send sync messages when secondary device is ready to process them + setTimeout(async () => { + const conversations = window.getConversations().models; + await textsecure.messaging.sendGroupSyncMessage(conversations); + await textsecure.messaging.sendOpenGroupsSyncMessage(conversations); + await textsecure.messaging.sendContactSyncMessage(conversations); + }, 5000); }, validatePubKeyHex(pubKey) { const c = new Whisper.Conversation({ diff --git a/libtextsecure/http-resources.js b/libtextsecure/http-resources.js index eb68c9138..27406b4a4 100644 --- a/libtextsecure/http-resources.js +++ b/libtextsecure/http-resources.js @@ -101,6 +101,9 @@ NUM_CONCURRENT_CONNECTIONS, stopPolling, messages => { + if (this.calledStop) { + return; // don't handle those messages + } connected = true; messages.forEach(message => { this.handleMessage(message.data, { @@ -127,6 +130,9 @@ // Exhausted all our snodes urls, trying again later from scratch setTimeout(() => { + if (this.calledStop) { + return; // don't restart + } window.log.info( `http-resource: Exhausted all our snodes urls, trying again in ${EXHAUSTED_SNODES_RETRY_DELAY / 1000}s from scratch` diff --git a/libtextsecure/message_receiver.js b/libtextsecure/message_receiver.js index 54dde9623..3a390b93c 100644 --- a/libtextsecure/message_receiver.js +++ b/libtextsecure/message_receiver.js @@ -59,7 +59,7 @@ function MessageReceiver(username, password, signalingKey, options = {}) { openGroupBound = true; } } else { - window.log.error('Can not handle open group data, API is not available'); + window.log.warn('Can not handle open group data, API is not available'); } } @@ -929,33 +929,34 @@ MessageReceiver.prototype.extend({ await this.handleEndSession(destination); } - if (msg.mediumGroupUpdate) { - await this.handleMediumGroupUpdate(envelope, msg.mediumGroupUpdate); - return; - } + // if (msg.mediumGroupUpdate) { + // await this.handleMediumGroupUpdate(envelope, msg.mediumGroupUpdate); + // return; + // } const message = await this.processDecrypted(envelope, msg); - const groupId = message.group && message.group.id; - const isBlocked = this.isGroupBlocked(groupId); const primaryDevicePubKey = window.storage.get('primaryDevicePubKey'); - const isMe = - envelope.source === textsecure.storage.user.getNumber() || - envelope.source === primaryDevicePubKey; - const isLeavingGroup = Boolean( - message.group && - message.group.type === textsecure.protobuf.GroupContext.Type.QUIT - ); - - if (groupId && isBlocked && !(isMe && isLeavingGroup)) { - window.log.warn( - `Message ${this.getEnvelopeId( - envelope - )} ignored; destined for blocked group` - ); - this.removeFromCache(envelope); - return; - } + // const groupId = message.group && message.group.id; + // const isBlocked = this.isGroupBlocked(groupId); + // + // const isMe = + // envelope.source === textsecure.storage.user.getNumber() || + // envelope.source === primaryDevicePubKey; + // const isLeavingGroup = Boolean( + // message.group && + // message.group.type === textsecure.protobuf.GroupContext.Type.QUIT + // ); + + // if (groupId && isBlocked && !(isMe && isLeavingGroup)) { + // window.log.warn( + // `Message ${this.getEnvelopeId( + // envelope + // )} ignored; destined for blocked group` + // ); + // this.removeFromCache(envelope); + // return; + // } // handle profileKey and avatar updates if (envelope.source === primaryDevicePubKey) { @@ -1675,7 +1676,7 @@ MessageReceiver.prototype.extend({ const ourNumber = textsecure.storage.user.getNumber(); const ourPrimaryNumber = window.storage.get('primaryDevicePubKey'); const ourOtherDevices = await libloki.storage.getAllDevicePubKeysForPrimaryPubKey( - window.storage.get('primaryDevicePubKey') + ourPrimaryNumber ); const ourDevices = new Set([ ourNumber, @@ -1861,6 +1862,7 @@ MessageReceiver.prototype.extend({ isBlocked(number) { return textsecure.storage.get('blocked', []).indexOf(number) >= 0; }, + cleanAttachment(attachment) { return { ..._.omit(attachment, 'thumbnail'), diff --git a/libtextsecure/outgoing_message.js b/libtextsecure/outgoing_message.js index ae28a8c42..72f28a111 100644 --- a/libtextsecure/outgoing_message.js +++ b/libtextsecure/outgoing_message.js @@ -6,7 +6,6 @@ libloki, StringView, lokiMessageAPI, - i18n, log */ @@ -218,9 +217,17 @@ OutgoingMessage.prototype = { this.errors[this.errors.length] = error; this.numberCompleted(); }, - reloadDevicesAndSend(primaryPubKey) { + reloadDevicesAndSend(primaryPubKey, multiDevice = true) { const ourNumber = textsecure.storage.user.getNumber(); + if (!multiDevice) { + if (primaryPubKey === ourNumber) { + return Promise.resolve(); + } + + return this.doSendMessage(primaryPubKey, [primaryPubKey]); + } + return ( libloki.storage .getAllDevicePubKeysForPrimaryPubKey(primaryPubKey) @@ -352,7 +359,7 @@ OutgoingMessage.prototype = { const updatedDevices = await getStaleDeviceIdsForNumber(devicePubKey); const keysFound = await this.getKeysForNumber(devicePubKey, updatedDevices); - let isMultiDeviceRequest = false; + // let isMultiDeviceRequest = false; let thisDeviceMessageType = this.messageType; if ( thisDeviceMessageType !== 'pairing-request' && @@ -373,7 +380,7 @@ OutgoingMessage.prototype = { // - We haven't received a friend request from this device // - We haven't sent a friend request recently if (conversation.friendRequestTimerIsExpired()) { - isMultiDeviceRequest = true; + // isMultiDeviceRequest = true; thisDeviceMessageType = 'friend-request'; } else { // Throttle automated friend requests @@ -415,27 +422,11 @@ OutgoingMessage.prototype = { window.log.info('attaching prekeys to outgoing message'); } - let messageBuffer; - let logDetails; - if (isMultiDeviceRequest) { - const tempMessage = new textsecure.protobuf.Content(); - const tempDataMessage = new textsecure.protobuf.DataMessage(); - tempDataMessage.body = i18n('secondaryDeviceDefaultFR'); - if (this.message.dataMessage && this.message.dataMessage.profile) { - tempDataMessage.profile = this.message.dataMessage.profile; - } - tempMessage.preKeyBundleMessage = this.message.preKeyBundleMessage; - tempMessage.dataMessage = tempDataMessage; - messageBuffer = tempMessage.toArrayBuffer(); - logDetails = { - tempMessage, - }; - } else { - messageBuffer = this.message.toArrayBuffer(); - logDetails = { - message: this.message, - }; - } + const messageBuffer = this.message.toArrayBuffer(); + const logDetails = { + message: this.message, + }; + const messageTypeStr = this.debugMessageType; const ourPubKey = textsecure.storage.user.getNumber(); @@ -492,6 +483,7 @@ OutgoingMessage.prototype = { plaintext, pubKey, isSessionRequest, + isFriendRequest, enableFallBackEncryption, } = clearMessage; // Session doesn't use the deviceId scheme, it's always 1. @@ -537,7 +529,7 @@ OutgoingMessage.prototype = { sourceDevice, content, pubKey, - isFriendRequest: enableFallBackEncryption, + isFriendRequest, isSessionRequest, }; }, @@ -706,14 +698,14 @@ OutgoingMessage.prototype = { return promise; }, - sendToNumber(number) { + sendToNumber(number, multiDevice = true) { let conversation; try { conversation = ConversationController.get(number); } catch (e) { // do nothing } - return this.reloadDevicesAndSend(number).catch(error => { + return this.reloadDevicesAndSend(number, multiDevice).catch(error => { conversation.resetPendingSend(); if (error.message === 'Identity key changed') { // eslint-disable-next-line no-param-reassign @@ -738,14 +730,15 @@ OutgoingMessage.prototype = { OutgoingMessage.buildAutoFriendRequestMessage = function buildAutoFriendRequestMessage( pubKey ) { - const dataMessage = new textsecure.protobuf.DataMessage({}); + const body = 'Please accept to enable messages to be synced across devices'; + const dataMessage = new textsecure.protobuf.DataMessage({ body }); const content = new textsecure.protobuf.Content({ dataMessage, }); const options = { - messageType: 'onlineBroadcast', + messageType: 'friend-request', debugMessageType: DebugMessageType.AUTO_FR_REQUEST, }; // Send a empty message with information about how to contact us directly @@ -765,9 +758,10 @@ OutgoingMessage.buildSessionRequestMessage = function buildSessionRequestMessage ) { const body = '(If you see this message, you must be using an out-of-date client)'; + const flags = textsecure.protobuf.DataMessage.Flags.SESSION_REQUEST; - const dataMessage = new textsecure.protobuf.DataMessage({ body, flags }); + const dataMessage = new textsecure.protobuf.DataMessage({ flags, body }); const content = new textsecure.protobuf.Content({ dataMessage, diff --git a/libtextsecure/sendmessage.js b/libtextsecure/sendmessage.js index a1b93e58e..8e93f3bbd 100644 --- a/libtextsecure/sendmessage.js +++ b/libtextsecure/sendmessage.js @@ -664,16 +664,58 @@ MessageSender.prototype = { if (!primaryDeviceKey) { return Promise.resolve(); } - // Extract required contacts information out of conversations - const sessionContacts = conversations.filter( - c => c.isPrivate() && !c.isSecondaryDevice() && c.isFriend() + // first get all friends with primary devices + const sessionContactsPrimary = + conversations.filter( + c => + c.isPrivate() && + !c.isOurLocalDevice() && + c.isFriend() && + !c.get('secondaryStatus') + ) || []; + + // then get all friends with secondary devices + let sessionContactsSecondary = conversations.filter( + c => + c.isPrivate() && + !c.isOurLocalDevice() && + c.isFriend() && + c.get('secondaryStatus') + ); + + // then morph all secondary conversation to their primary + sessionContactsSecondary = + (await Promise.all( + // eslint-disable-next-line arrow-body-style + sessionContactsSecondary.map(async c => { + return window.ConversationController.getOrCreateAndWait( + c.getPrimaryDevicePubKey(), + 'private' + ); + }) + )) || []; + // filter out our primary pubkey if it was added. + sessionContactsSecondary = sessionContactsSecondary.filter( + c => c.id !== primaryDeviceKey ); - if (sessionContacts.length === 0) { + + const contactsSet = new Set([ + ...sessionContactsPrimary, + ...sessionContactsSecondary, + ]); + + if (contactsSet.size === 0) { + window.console.info('No contacts to sync.'); + return Promise.resolve(); } + libloki.api.debug.logContactSync('Triggering contact sync message with:', [ + ...contactsSet, + ]); + // We need to sync across 3 contacts at a time // This is to avoid hitting storage server limit - const chunked = _.chunk(sessionContacts, 3); + const chunked = _.chunk([...contactsSet], 3); const syncMessages = await Promise.all( chunked.map(c => libloki.api.createContactSyncProtoMessage(c)) ); diff --git a/package.json b/package.json index bb850ff22..eeb2d4939 100644 --- a/package.json +++ b/package.json @@ -15,13 +15,9 @@ "main": "main.js", "scripts": { "postinstall": "electron-builder install-app-deps && rimraf node_modules/dtrace-provider", - "start": "electron .", - "start-multi": "cross-env NODE_APP_INSTANCE=1 electron .", - "start-multi2": "cross-env NODE_APP_INSTANCE=2 electron .", - "start-prod": "cross-env NODE_ENV=production NODE_APP_INSTANCE=devprod electron .", - "start-prod-multi": "cross-env NODE_ENV=production NODE_APP_INSTANCE=devprod1 electron .", - "start-swarm-test": "cross-env NODE_ENV=swarm-testing NODE_APP_INSTANCE=1 electron .", - "start-swarm-test-2": "cross-env NODE_ENV=swarm-testing NODE_APP_INSTANCE=2 electron .", + "start": "cross-env NODE_APP_INSTANCE=$MULTI electron .", + "start-prod": "cross-env NODE_ENV=production NODE_APP_INSTANCE=devprod$MULTI electron .", + "start-swarm-test": "cross-env NODE_ENV=production NODE_APP_INSTANCE=$MULTI electron .", "grunt": "grunt", "icon-gen": "electron-icon-maker --input=images/icon_1024.png --output=./build", "generate": "yarn icon-gen && yarn grunt", @@ -36,7 +32,6 @@ "test-loki-view": "NODE_ENV=test-loki yarn run start", "test-electron": "yarn grunt test", "test-integration": "ELECTRON_DISABLE_SANDBOX=1 mocha --exit --timeout 10000 integration_test/integration_test.js", - "test-integration-parts": "ELECTRON_DISABLE_SANDBOX=1 mocha --exit --timeout 10000 integration_test/integration_test.js --grep 'registration' && ELECTRON_DISABLE_SANDBOX=1 mocha --exit --timeout 10000 integration_test/integration_test.js --grep 'openGroup' && ELECTRON_DISABLE_SANDBOX=1 mocha --exit --timeout 10000 integration_test/integration_test.js --grep 'addFriends' && ELECTRON_DISABLE_SANDBOX=1 mocha --exit --timeout 10000 integration_test/integration_test.js --grep 'linkDevice' && ELECTRON_DISABLE_SANDBOX=1 mocha --exit --timeout 10000 integration_test/integration_test.js --grep 'closedGroup'", "test-medium-groups": "ELECTRON_DISABLE_SANDBOX=1 mocha --exit --timeout 10000 integration_test/integration_test.js --grep 'senderkeys'", "test-node": "mocha --recursive --exit test/app test/modules ts/test libloki/test/node", "eslint": "eslint --cache .", diff --git a/preload.js b/preload.js index db6d03360..15f6d11eb 100644 --- a/preload.js +++ b/preload.js @@ -56,6 +56,8 @@ if ( process.env.NODE_ENV.includes('test-integration') ) { window.electronRequire = require; + // during test-integration, file server is started on localhost + window.getDefaultFileServer = () => 'http://127.0.0.1:7070'; } window.isBeforeVersion = (toCheck, baseVersion) => { @@ -164,7 +166,7 @@ window.open = () => null; window.eval = global.eval = () => null; window.drawAttention = () => { - // window.log.info('draw attention'); + // window.log.debug('draw attention'); ipc.send('draw-attention'); }; window.showWindow = () => { @@ -337,17 +339,31 @@ window.lokiSnodeAPI = new LokiSnodeAPI({ localUrl: config.localUrl, }); -window.LokiMessageAPI = require('./js/modules/loki_message_api'); - if (process.env.USE_STUBBED_NETWORK) { - window.StubMessageAPI = require('./integration_test/stubs/stub_message_api'); - window.StubAppDotNetApi = require('./integration_test/stubs/stub_app_dot_net_api'); - window.StubLokiSnodeAPI = require('./integration_test/stubs/stub_loki_snode_api'); + const StubMessageAPI = require('./integration_test/stubs/stub_message_api'); + window.LokiMessageAPI = StubMessageAPI; + + const StubAppDotNetAPI = require('./integration_test/stubs/stub_app_dot_net_api'); + window.LokiAppDotNetServerAPI = StubAppDotNetAPI; + + const StubSnodeAPI = require('./integration_test/stubs/stub_snode_api'); + + window.lokiSnodeAPI = new StubSnodeAPI({ + serverUrl: config.serverUrl, + localUrl: config.localUrl, + }); +} else { + window.lokiSnodeAPI = new LokiSnodeAPI({ + serverUrl: config.serverUrl, + localUrl: config.localUrl, + }); + + window.LokiMessageAPI = require('./js/modules/loki_message_api'); + + window.LokiAppDotNetServerAPI = require('./js/modules/loki_app_dot_net_api'); } window.LokiPublicChatAPI = require('./js/modules/loki_public_chat_api'); -window.LokiAppDotNetServerAPI = require('./js/modules/loki_app_dot_net_api'); - window.LokiFileServerAPI = require('./js/modules/loki_file_server_api'); window.LokiRssAPI = require('./js/modules/loki_rss_api'); @@ -433,6 +449,7 @@ window.lokiFeatureFlags = { useFileOnionRequests: false, enableSenderKeys: false, onionRequestHops: 3, + debugMessageLogs: process.env.ENABLE_MESSAGE_LOGS, }; // eslint-disable-next-line no-extend-native,func-names @@ -443,7 +460,8 @@ Promise.prototype.ignore = function() { if ( config.environment.includes('test') && - !config.environment.includes('swarm-testing') + !config.environment.includes('swarm-testing') && + !config.environment.includes('test-integration') ) { const isWindows = process.platform === 'win32'; /* eslint-disable global-require, import/no-extraneous-dependencies */ @@ -465,5 +483,8 @@ if (config.environment.includes('test-integration')) { multiDeviceUnpairing: true, privateGroupChats: true, useSnodeProxy: !process.env.USE_STUBBED_NETWORK, + useOnionRequests: false, + debugMessageLogs: true, + enableSenderKeys: true, }; } diff --git a/session-file-server b/session-file-server new file mode 160000 index 000000000..52b77bf30 --- /dev/null +++ b/session-file-server @@ -0,0 +1 @@ +Subproject commit 52b77bf3039aec88b3900e8a7ed6e62d30a4d0d4 diff --git a/stylesheets/_session_left_pane.scss b/stylesheets/_session_left_pane.scss index 8c0c4a48c..b45426787 100644 --- a/stylesheets/_session_left_pane.scss +++ b/stylesheets/_session_left_pane.scss @@ -535,6 +535,7 @@ $session-compose-margin: 20px; &-section { display: flex; flex-direction: column; + flex: 1; } &-category-list-item { diff --git a/ts/components/session/LeftPaneSectionHeader.tsx b/ts/components/session/LeftPaneSectionHeader.tsx index ec554e23a..21f06c5f2 100644 --- a/ts/components/session/LeftPaneSectionHeader.tsx +++ b/ts/components/session/LeftPaneSectionHeader.tsx @@ -114,6 +114,7 @@ export class LeftPaneSectionHeader extends React.Component { count={notificationCount} size={NotificationCountSize.ON_HEADER} onClick={this.props.buttonClicked} + key="notification-count" // we can only have one of those here /> ); diff --git a/ts/state/selectors/conversations.ts b/ts/state/selectors/conversations.ts index 56f242c7b..3edea865c 100644 --- a/ts/state/selectors/conversations.ts +++ b/ts/state/selectors/conversations.ts @@ -147,7 +147,9 @@ export const _getLeftPaneLists = ( } if (conversation.hasSentFriendRequest) { if (!conversation.isFriend) { - allSentFriendsRequest.push(conversation); + if (!conversation.isSecondary) { + allSentFriendsRequest.push(conversation); + } } } diff --git a/yarn.lock b/yarn.lock index 09e1f4b11..a24c54cb7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4836,6 +4836,13 @@ in-publish@^2.0.0: resolved "https://registry.yarnpkg.com/in-publish/-/in-publish-2.0.1.tgz#948b1a535c8030561cea522f73f78f4be357e00c" integrity sha512-oDM0kUSNFC31ShNxHKUyfZKy8ZeXZBWMjMdZHKLOk13uvT27VTL/QzRGfRUcevJhpkZAvlhPYuXkF7eNWrtyxQ== +indent-string@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-2.1.0.tgz#8e2d48348742121b4a8218b7a137e9a52049dc80" + integrity sha1-ji1INIdCEhtKghi3oTfppSBJ3IA= + dependencies: + repeating "^2.0.0" + indexes-of@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/indexes-of/-/indexes-of-1.0.1.tgz#f30f716c8e2bd346c7b67d3df3915566a7c05607" @@ -9193,6 +9200,11 @@ slice-ansi@1.0.0: dependencies: is-fullwidth-code-point "^2.0.0" +slide@^1.1.5: + version "1.1.6" + resolved "https://registry.yarnpkg.com/slide/-/slide-1.1.6.tgz#56eb027d65b4d2dce6cb2e2d32c4d4afc9e1d707" + integrity sha1-VusCfWW00tzmyy4tMsTUr8nh1wc= + snapdragon-node@^2.0.1: version "2.1.1" resolved "https://registry.yarnpkg.com/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b" @@ -9650,6 +9662,13 @@ strip-eof@^1.0.0: resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf" integrity sha1-u0P/VZim6wXYm1n80SnJgzE2Br8= +strip-indent@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-1.0.1.tgz#0c7962a6adefa7bbd4ac366460a638552ae1a0a2" + integrity sha1-DHlipq3vp7vUrDZkYKY4VSrhoKI= + dependencies: + get-stdin "^4.0.1" + strip-json-comments@~2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a"