Finalized cache
This commit is contained in:
commit
b203dc4493
|
@ -30,3 +30,4 @@ ts/**/*.js
|
|||
# Libloki specific files
|
||||
libloki/test/components.js
|
||||
libloki/modules/mnemonic.js
|
||||
session-file-server/**
|
||||
|
|
|
@ -23,6 +23,9 @@ jobs:
|
|||
- name: Checkout git repo
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Pull git submodules
|
||||
run: git submodule update --init
|
||||
|
||||
- name: Install node
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
|
|
|
@ -25,6 +25,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:
|
||||
|
|
|
@ -20,6 +20,9 @@ jobs:
|
|||
- name: Checkout git repo
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Pull git submodules
|
||||
run: git submodule update --init
|
||||
|
||||
- name: Install node
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
|
|
|
@ -37,3 +37,6 @@ ts/protobuf/*.d.ts
|
|||
|
||||
# Ctags
|
||||
tags
|
||||
|
||||
proxy.key
|
||||
proxy.pub
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
[submodule "session-file-server"]
|
||||
path = session-file-server
|
||||
url = https://github.com/loki-project/session-file-server/
|
|
@ -54,4 +54,4 @@ stylesheets/_intlTelInput.scss
|
|||
# Coverage
|
||||
coverage/**
|
||||
.nyc_output/**
|
||||
|
||||
session-file-server/**
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -683,7 +683,7 @@
|
|||
"description": "Shown to separate the types of search results"
|
||||
},
|
||||
"messagesHeader": {
|
||||
"message": "Session",
|
||||
"message": "Conversations",
|
||||
"description": "Shown to separate the types of search results"
|
||||
},
|
||||
"settingsHeader": {
|
||||
|
@ -985,10 +985,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",
|
||||
"description": "Placeholder text in the message entry field when it is disabled because a secondary device conversation is visible"
|
||||
|
@ -2232,13 +2228,28 @@
|
|||
"message": "Remove"
|
||||
},
|
||||
"invalidHexId": {
|
||||
"message": "Invalid Hex ID",
|
||||
"description": "Error string shown when user type an invalid pubkey hex string"
|
||||
"message": "Invalid Session ID or LNS Name",
|
||||
"description": "Error string shown when user types an invalid pubkey hex string"
|
||||
},
|
||||
"invalidLnsFormat": {
|
||||
"message": "Invalid LNS Name",
|
||||
"description": "Error string shown when user types an invalid LNS name"
|
||||
},
|
||||
"invalidPubkeyFormat": {
|
||||
"message": "Invalid Pubkey Format",
|
||||
"description": "Error string shown when user types an invalid pubkey format"
|
||||
},
|
||||
"lnsMappingNotFound": {
|
||||
"message": "There is no LNS mapping associated with this name",
|
||||
"description": "Shown in toast if user enters an unknown LNS name"
|
||||
},
|
||||
"lnsLookupTimeout": {
|
||||
"message": "LNS lookup timed out",
|
||||
"description": "Shown in toast if user enters an unknown LNS name"
|
||||
},
|
||||
"lnsTooFewNodes": {
|
||||
"message": "Not enough nodes currently active for LNS lookup"
|
||||
},
|
||||
"conversationsTab": {
|
||||
"message": "Conversations",
|
||||
"description": "conversation tab title"
|
||||
|
@ -2417,7 +2428,7 @@
|
|||
"message": "Enter Session ID"
|
||||
},
|
||||
"pasteSessionIDRecipient": {
|
||||
"message": "Enter a Session ID"
|
||||
"message": "Enter a Session ID or LNS name"
|
||||
},
|
||||
"usersCanShareTheir...": {
|
||||
"message": "Users can share their Session ID from their account settings, or by sharing their QR code."
|
||||
|
@ -2597,5 +2608,8 @@
|
|||
"example": "10"
|
||||
}
|
||||
}
|
||||
},
|
||||
"useSenderKeys": {
|
||||
"message": "Use Sender Keys"
|
||||
}
|
||||
}
|
||||
|
|
11
app/sql.js
11
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;`,
|
||||
|
|
|
@ -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,25 @@ 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}`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -25,11 +25,10 @@ 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
|
||||
await common.addFriendToNewClosedGroup(app, app2);
|
||||
await common.addFriendToNewClosedGroup([app, app2], useSenderKeys);
|
||||
|
||||
// send a message from app and validate it is received on app2
|
||||
const textMessage = common.generateSendMessageText();
|
||||
|
|
|
@ -20,6 +20,13 @@ chai.should();
|
|||
chai.use(chaiAsPromised);
|
||||
chai.config.includeStack = true;
|
||||
|
||||
// From https://github.com/chaijs/chai/issues/200
|
||||
chai.use((_chai, _) => {
|
||||
_chai.Assertion.addMethod('withMessage', msg => {
|
||||
_.flag(this, 'message', msg);
|
||||
});
|
||||
});
|
||||
|
||||
const STUB_SNODE_SERVER_PORT = 3000;
|
||||
const ENABLE_LOG = false;
|
||||
|
||||
|
@ -29,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',
|
||||
|
@ -184,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);
|
||||
|
@ -242,23 +240,8 @@ module.exports = {
|
|||
);
|
||||
},
|
||||
|
||||
async startAppsAsFriends() {
|
||||
const app1Props = {
|
||||
mnemonic: this.TEST_MNEMONIC1,
|
||||
displayName: this.TEST_DISPLAY_NAME1,
|
||||
stubSnode: true,
|
||||
};
|
||||
|
||||
const app2Props = {
|
||||
mnemonic: this.TEST_MNEMONIC2,
|
||||
displayName: this.TEST_DISPLAY_NAME2,
|
||||
stubSnode: true,
|
||||
};
|
||||
|
||||
const [app1, app2] = await Promise.all([
|
||||
this.startAndStub(app1Props),
|
||||
this.startAndStubN(app2Props, 2),
|
||||
]);
|
||||
async makeFriends(app1, client2) {
|
||||
const [app2, pubkey2] = client2;
|
||||
|
||||
/** add each other as friends */
|
||||
const textMessage = this.generateSendMessageText();
|
||||
|
@ -266,11 +249,7 @@ module.exports = {
|
|||
await app1.client.element(ConversationPage.contactsButtonSection).click();
|
||||
await app1.client.element(ConversationPage.addContactButton).click();
|
||||
|
||||
await this.setValueWrapper(
|
||||
app1,
|
||||
ConversationPage.sessionIDInput,
|
||||
this.TEST_PUBKEY2
|
||||
);
|
||||
await this.setValueWrapper(app1, ConversationPage.sessionIDInput, pubkey2);
|
||||
await app1.client.element(ConversationPage.nextButton).click();
|
||||
await app1.client.waitForExist(
|
||||
ConversationPage.sendFriendRequestTextarea,
|
||||
|
@ -317,33 +296,75 @@ module.exports = {
|
|||
ConversationPage.acceptedFriendRequestMessage,
|
||||
5000
|
||||
);
|
||||
},
|
||||
|
||||
async startAppsAsFriends() {
|
||||
const app1Props = {
|
||||
mnemonic: this.TEST_MNEMONIC1,
|
||||
displayName: this.TEST_DISPLAY_NAME1,
|
||||
stubSnode: true,
|
||||
};
|
||||
|
||||
const app2Props = {
|
||||
mnemonic: this.TEST_MNEMONIC2,
|
||||
displayName: this.TEST_DISPLAY_NAME2,
|
||||
stubSnode: true,
|
||||
};
|
||||
|
||||
const [app1, app2] = await Promise.all([
|
||||
this.startAndStub(app1Props),
|
||||
this.startAndStubN(app2Props, 2),
|
||||
]);
|
||||
|
||||
await this.makeFriends(app1, [app2, this.TEST_PUBKEY2]);
|
||||
|
||||
return [app1, app2];
|
||||
},
|
||||
|
||||
async addFriendToNewClosedGroup(app, app2) {
|
||||
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,
|
||||
this.VALID_CLOSED_GROUP_NAME1
|
||||
);
|
||||
|
||||
await app.client
|
||||
.element(ConversationPage.closedGroupNameTextarea)
|
||||
.getValue()
|
||||
.should.eventually.equal(this.VALID_CLOSED_GROUP_NAME1);
|
||||
|
||||
await app.client
|
||||
.element(ConversationPage.createClosedGroupMemberItem)
|
||||
.isVisible().should.eventually.be.true;
|
||||
// This assumes that app does not have any other friends
|
||||
|
||||
for (let i = 0; i < others.length; i += 1) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await app.client
|
||||
.element(ConversationPage.createClosedGroupMemberItem(i))
|
||||
.isVisible().should.eventually.be.true;
|
||||
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await app.client
|
||||
.element(ConversationPage.createClosedGroupMemberItem(i))
|
||||
.click();
|
||||
}
|
||||
|
||||
// select the first friend as a member of the groups being created
|
||||
await app.client
|
||||
.element(ConversationPage.createClosedGroupMemberItem)
|
||||
.click();
|
||||
await app.client
|
||||
.element(ConversationPage.createClosedGroupMemberItemSelected)
|
||||
.isVisible().should.eventually.be.true;
|
||||
|
||||
if (useSenderKeys) {
|
||||
// Select Sender Keys
|
||||
await app.client
|
||||
.element(ConversationPage.createClosedGroupSealedSenderToggle)
|
||||
.click();
|
||||
}
|
||||
|
||||
// trigger the creation of the group
|
||||
await app.client
|
||||
.element(ConversationPage.validateCreationClosedGroupButton)
|
||||
|
@ -356,8 +377,9 @@ module.exports = {
|
|||
await app.client.isExisting(
|
||||
ConversationPage.headerTitleGroupName(this.VALID_CLOSED_GROUP_NAME1)
|
||||
).should.eventually.be.true;
|
||||
await app.client.element(ConversationPage.headerTitleMembers(2)).isVisible()
|
||||
.should.eventually.be.true;
|
||||
await app.client
|
||||
.element(ConversationPage.headerTitleMembers(members.length))
|
||||
.isVisible().should.eventually.be.true;
|
||||
|
||||
// validate overlay is closed
|
||||
await app.client
|
||||
|
@ -376,28 +398,32 @@ module.exports = {
|
|||
)
|
||||
).should.eventually.be.true;
|
||||
|
||||
// next check app2 has been invited and has the group in its conversations
|
||||
await app2.client.waitForExist(
|
||||
ConversationPage.rowOpenGroupConversationName(
|
||||
this.VALID_CLOSED_GROUP_NAME1
|
||||
),
|
||||
6000
|
||||
await Promise.all(
|
||||
others.map(async otherApp => {
|
||||
// next check that other members have been invited and have the group in their conversations
|
||||
await otherApp.client.waitForExist(
|
||||
ConversationPage.rowOpenGroupConversationName(
|
||||
this.VALID_CLOSED_GROUP_NAME1
|
||||
),
|
||||
6000
|
||||
);
|
||||
// open the closed group conversation on otherApp
|
||||
await otherApp.client
|
||||
.element(ConversationPage.conversationButtonSection)
|
||||
.click();
|
||||
await this.timeout(500);
|
||||
await otherApp.client
|
||||
.element(
|
||||
ConversationPage.rowOpenGroupConversationName(
|
||||
this.VALID_CLOSED_GROUP_NAME1
|
||||
)
|
||||
)
|
||||
.click();
|
||||
})
|
||||
);
|
||||
// open the closed group conversation on app2
|
||||
await app2.client
|
||||
.element(ConversationPage.conversationButtonSection)
|
||||
.click();
|
||||
await this.timeout(500);
|
||||
await app2.client
|
||||
.element(
|
||||
ConversationPage.rowOpenGroupConversationName(
|
||||
this.VALID_CLOSED_GROUP_NAME1
|
||||
)
|
||||
)
|
||||
.click();
|
||||
},
|
||||
|
||||
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();
|
||||
|
@ -422,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
|
||||
|
@ -458,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) {
|
||||
|
@ -466,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;
|
||||
|
@ -478,7 +503,7 @@ module.exports = {
|
|||
|
||||
await app1.client.waitForExist(
|
||||
ConversationPage.noPairedDeviceMessage,
|
||||
2000
|
||||
5000
|
||||
);
|
||||
await app1.client.element(ConversationPage.linkDeviceButton).isEnabled()
|
||||
.should.eventually.be.true;
|
||||
|
@ -533,23 +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;'
|
||||
);
|
||||
},
|
||||
|
||||
logsContainsString: async (app1, str) => {
|
||||
const logs = JSON.stringify(await app1.client.getRenderProcessLogs());
|
||||
return logs.includes(str);
|
||||
},
|
||||
|
||||
async startStubSnodeServer() {
|
||||
if (!this.stubSnode) {
|
||||
this.messages = {};
|
||||
|
@ -557,42 +565,98 @@ module.exports = {
|
|||
const { query } = url.parse(request.url, true);
|
||||
const { pubkey, data, timestamp } = query;
|
||||
|
||||
if (pubkey) {
|
||||
if (request.method === 'POST') {
|
||||
if (ENABLE_LOG) {
|
||||
console.warn('POST', [data, timestamp]);
|
||||
}
|
||||
|
||||
let ori = this.messages[pubkey];
|
||||
if (!this.messages[pubkey]) {
|
||||
ori = [];
|
||||
}
|
||||
|
||||
this.messages[pubkey] = [...ori, { data, timestamp }];
|
||||
|
||||
response.writeHead(200, { 'Content-Type': 'text/html' });
|
||||
response.end();
|
||||
} else {
|
||||
const retrievedMessages = { messages: this.messages[pubkey] };
|
||||
if (ENABLE_LOG) {
|
||||
console.warn('GET', pubkey, retrievedMessages);
|
||||
}
|
||||
if (this.messages[pubkey]) {
|
||||
response.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
response.write(JSON.stringify(retrievedMessages));
|
||||
this.messages[pubkey] = [];
|
||||
}
|
||||
response.end();
|
||||
}
|
||||
if (!pubkey) {
|
||||
console.warn('NO PUBKEY');
|
||||
response.writeHead(400, { 'Content-Type': 'text/html' });
|
||||
response.end();
|
||||
return;
|
||||
}
|
||||
|
||||
if (request.method === 'POST') {
|
||||
if (ENABLE_LOG) {
|
||||
console.warn(
|
||||
'POST',
|
||||
pubkey.substr(2, 3),
|
||||
data.substr(4, 10),
|
||||
timestamp
|
||||
);
|
||||
}
|
||||
|
||||
let ori = this.messages[pubkey];
|
||||
|
||||
if (!this.messages[pubkey]) {
|
||||
ori = [];
|
||||
}
|
||||
|
||||
this.messages[pubkey] = [...ori, { data, timestamp }];
|
||||
|
||||
response.writeHead(200, { 'Content-Type': 'text/html' });
|
||||
response.end();
|
||||
} else {
|
||||
const retrievedMessages = { messages: this.messages[pubkey] || [] };
|
||||
|
||||
if (ENABLE_LOG) {
|
||||
const messages = retrievedMessages.messages.map(m =>
|
||||
m.data.substr(4, 10)
|
||||
);
|
||||
console.warn('GET', pubkey.substr(2, 3), messages);
|
||||
}
|
||||
response.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
response.write(JSON.stringify(retrievedMessages));
|
||||
response.end();
|
||||
}
|
||||
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();
|
||||
|
@ -600,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(
|
||||
|
|
|
@ -13,6 +13,8 @@ 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 () => {
|
||||
// start the app once before all tests to get the platform-dependent
|
||||
|
|
|
@ -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,37 @@ 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);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,49 +1,142 @@
|
|||
/* 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 alice2Props = {};
|
||||
|
||||
const app2Props = {
|
||||
mnemonic: common.TEST_MNEMONIC2,
|
||||
displayName: common.TEST_DISPLAY_NAME2,
|
||||
stubSnode: true,
|
||||
};
|
||||
[Alice1, Bob1] = await common.startAppsAsFriends(); // Alice and Bob are friends
|
||||
|
||||
[app, app2] = await Promise.all([
|
||||
common.startAndStub(app1Props),
|
||||
common.startAndStubN(app2Props, 2),
|
||||
]);
|
||||
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
|
||||
|
||||
it('unlink two devices', async () => {
|
||||
await common.linkApp2ToApp(app, app2);
|
||||
await common.timeout(1000);
|
||||
await common.triggerUnlinkApp2FromApp(app, app2);
|
||||
// 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
|
||||
);
|
||||
|
||||
// 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
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -14,6 +14,8 @@ module.exports = {
|
|||
inputWithId: id => `//input[contains(@id, '${id}')]`,
|
||||
textAreaWithPlaceholder: placeholder =>
|
||||
`//textarea[contains(@placeholder, "${placeholder}")]`,
|
||||
textAreaWithClass: classname =>
|
||||
`//textarea[contains(@class, "${classname}")]`,
|
||||
byId: id => `//*[@id="${id}"]`,
|
||||
divWithClass: classname => `//div[contains(@class, "${classname}")]`,
|
||||
divWithClassAndText: (classname, text) =>
|
||||
|
|
|
@ -4,7 +4,7 @@ module.exports = {
|
|||
// conversation view
|
||||
sessionLoader: commonPage.divWithClass('session-loader'),
|
||||
leftPaneOverlay: commonPage.divWithClass('module-left-pane-overlay'),
|
||||
sendMessageTextarea: commonPage.textAreaWithPlaceholder('Type your message'),
|
||||
sendMessageTextarea: commonPage.textAreaWithClass('send-message'),
|
||||
sendFriendRequestTextarea: commonPage.textAreaWithPlaceholder(
|
||||
'Send your first message'
|
||||
),
|
||||
|
@ -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(
|
||||
|
@ -63,7 +61,11 @@ module.exports = {
|
|||
closedGroupNameTextarea: commonPage.textAreaWithPlaceholder(
|
||||
'Enter a group name'
|
||||
),
|
||||
createClosedGroupMemberItem: commonPage.divWithClass('session-member-item'),
|
||||
createClosedGroupMemberItem: idx =>
|
||||
commonPage.divWithClass(`session-member-item-${idx}`),
|
||||
createClosedGroupSealedSenderToggle: commonPage.divWithClass(
|
||||
'session-toggle'
|
||||
),
|
||||
createClosedGroupMemberItemSelected: commonPage.divWithClass(
|
||||
'session-member-item selected'
|
||||
),
|
||||
|
|
|
@ -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"]',
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -0,0 +1,153 @@
|
|||
/* eslint-disable func-names */
|
||||
/* eslint-disable import/no-extraneous-dependencies */
|
||||
const { afterEach, beforeEach, describe, it } = require('mocha');
|
||||
const common = require('./common');
|
||||
|
||||
const ConversationPage = require('./page-objects/conversation.page');
|
||||
|
||||
async function generateAndSendMessage(app) {
|
||||
// send a message from app and validate it is received on app2
|
||||
const textMessage = common.generateSendMessageText();
|
||||
await app.client
|
||||
.element(ConversationPage.sendMessageTextarea)
|
||||
.setValue(textMessage);
|
||||
|
||||
await app.client
|
||||
.element(ConversationPage.sendMessageTextarea)
|
||||
.getValue()
|
||||
.should.eventually.equal(textMessage);
|
||||
// send the message
|
||||
await app.client.keys('Enter');
|
||||
|
||||
// validate that the message has been added to the message list view
|
||||
await app.client.waitForExist(
|
||||
ConversationPage.existingSendMessageText(textMessage),
|
||||
2000
|
||||
);
|
||||
|
||||
return textMessage;
|
||||
}
|
||||
|
||||
async function makeFriendsPlusMessage(app, [app2, pubkey]) {
|
||||
await common.makeFriends(app, [app2, pubkey]);
|
||||
|
||||
// Send something back so that `app` can see our name
|
||||
const text = await generateAndSendMessage(app2);
|
||||
await app.client.waitForExist(
|
||||
ConversationPage.existingReceivedMessageText(text),
|
||||
8000
|
||||
);
|
||||
|
||||
// Click away so we can call this function again
|
||||
await app.client.element(ConversationPage.conversationButtonSection).click();
|
||||
}
|
||||
|
||||
async function testTwoMembers() {
|
||||
const [app, app2] = await common.startAppsAsFriends();
|
||||
|
||||
const useSenderKeys = true;
|
||||
|
||||
// create group and add new friend
|
||||
await common.addFriendToNewClosedGroup([app, app2], useSenderKeys);
|
||||
|
||||
const text1 = await generateAndSendMessage(app);
|
||||
|
||||
// validate that the message has been added to the message list view
|
||||
await app2.client.waitForExist(
|
||||
ConversationPage.existingReceivedMessageText(text1),
|
||||
5000
|
||||
);
|
||||
|
||||
// Send a message back:
|
||||
const text2 = await generateAndSendMessage(app2);
|
||||
|
||||
// TODO: fix this. We can send messages back manually, not sure
|
||||
// why this test fails
|
||||
await app.client.waitForExist(
|
||||
ConversationPage.existingReceivedMessageText(text2),
|
||||
10000
|
||||
);
|
||||
}
|
||||
|
||||
async function testThreeMembers() {
|
||||
// 1. Make three clients A, B, C
|
||||
|
||||
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,
|
||||
};
|
||||
|
||||
const app3Props = {
|
||||
mnemonic: common.TEST_MNEMONIC3,
|
||||
displayName: common.TEST_DISPLAY_NAME3,
|
||||
stubSnode: true,
|
||||
};
|
||||
|
||||
const [app1, app2, app3] = await Promise.all([
|
||||
common.startAndStub(app1Props),
|
||||
common.startAndStubN(app2Props, 2),
|
||||
common.startAndStubN(app3Props, 3),
|
||||
]);
|
||||
|
||||
// 2. Make A friends with B and C (B and C are not friends)
|
||||
|
||||
await makeFriendsPlusMessage(app1, [app2, common.TEST_PUBKEY2]);
|
||||
await makeFriendsPlusMessage(app1, [app3, common.TEST_PUBKEY3]);
|
||||
|
||||
const useSenderKeys = true;
|
||||
|
||||
// 3. Add all three to the group
|
||||
|
||||
await common.addFriendToNewClosedGroup([app1, app2, app3], useSenderKeys);
|
||||
|
||||
// 4. Test that all members can see the message from app1
|
||||
const text1 = await generateAndSendMessage(app1);
|
||||
|
||||
await app2.client.waitForExist(
|
||||
ConversationPage.existingReceivedMessageText(text1),
|
||||
5000
|
||||
);
|
||||
|
||||
await app3.client.waitForExist(
|
||||
ConversationPage.existingReceivedMessageText(text1),
|
||||
5000
|
||||
);
|
||||
|
||||
// TODO: test that B and C can send messages to the group
|
||||
|
||||
// const text2 = await generateAndSendMessage(app3);
|
||||
|
||||
// await app2.client.waitForExist(
|
||||
// ConversationPage.existingReceivedMessageText(text2),
|
||||
// 5000
|
||||
// );
|
||||
}
|
||||
|
||||
describe('senderkeys', function() {
|
||||
let app;
|
||||
|
||||
this.timeout(600000);
|
||||
this.slow(40000);
|
||||
|
||||
beforeEach(async () => {
|
||||
await common.killallElectron();
|
||||
await common.stopStubSnodeServer();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await common.stopApp(app);
|
||||
await common.killallElectron();
|
||||
await common.stopStubSnodeServer();
|
||||
});
|
||||
|
||||
it('Two member group', testTwoMembers);
|
||||
|
||||
it('Three member group: test session requests', testThreeMembers);
|
||||
});
|
|
@ -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);
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
/* global log */
|
||||
|
||||
class StubLokiSnodeAPI {
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
async refreshSwarmNodesForPubKey(pubKey) {
|
||||
log.info('refreshSwarmNodesForPubkey: ', pubKey);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = StubLokiSnodeAPI;
|
|
@ -1,4 +1,4 @@
|
|||
/* global clearTimeout, dcodeIO, Buffer, TextDecoder, process */
|
||||
/* global clearTimeout, dcodeIO, Buffer, TextDecoder, process, log */
|
||||
const nodeFetch = require('node-fetch');
|
||||
|
||||
class StubMessageAPI {
|
||||
|
@ -26,6 +26,35 @@ class StubMessageAPI {
|
|||
);
|
||||
}
|
||||
|
||||
async pollForGroupId(groupId, onMessages) {
|
||||
const get = {
|
||||
method: 'GET',
|
||||
};
|
||||
const res = await nodeFetch(
|
||||
`${this.baseUrl}/messages?pubkey=${groupId}`,
|
||||
get
|
||||
);
|
||||
|
||||
try {
|
||||
const json = await res.json();
|
||||
|
||||
const modifiedMessages = json.messages.map(m => {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
m.conversationId = groupId;
|
||||
return m;
|
||||
});
|
||||
|
||||
onMessages(modifiedMessages || []);
|
||||
} catch (e) {
|
||||
log.error('invalid json for GROUP', e);
|
||||
onMessages([]);
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
this.pollForGroupId(groupId, onMessages);
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
async startLongPolling(numConnections, stopPolling, callback) {
|
||||
const ourPubkey = this.ourKey;
|
||||
|
||||
|
@ -36,10 +65,15 @@ class StubMessageAPI {
|
|||
`${this.baseUrl}/messages?pubkey=${ourPubkey}`,
|
||||
get
|
||||
);
|
||||
const json = await res.json();
|
||||
// console.warn('STUBBED polling messages ', json.messages);
|
||||
|
||||
callback(json.messages || []);
|
||||
try {
|
||||
const json = await res.json();
|
||||
callback(json.messages || []);
|
||||
} catch (e) {
|
||||
log.error('invalid json: ', e);
|
||||
callback([]);
|
||||
}
|
||||
// console.warn('STUBBED polling messages ', json.messages);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
/* eslint-disable class-methods-use-this */
|
||||
|
||||
class StubSnodeAPI {
|
||||
async refreshSwarmNodesForPubKey() {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = StubSnodeAPI;
|
180
js/background.js
180
js/background.js
|
@ -636,23 +636,21 @@
|
|||
window.doUpdateGroup = async (groupId, groupName, members, avatar) => {
|
||||
const ourKey = textsecure.storage.user.getNumber();
|
||||
|
||||
const ev = new Event('message');
|
||||
ev.confirm = () => {};
|
||||
|
||||
ev.data = {
|
||||
source: ourKey,
|
||||
timestamp: Date.now(),
|
||||
message: {
|
||||
group: {
|
||||
id: groupId,
|
||||
type: textsecure.protobuf.GroupContext.Type.UPDATE,
|
||||
name: groupName,
|
||||
members,
|
||||
avatar: null, // TODO
|
||||
},
|
||||
const ev = {
|
||||
groupDetails: {
|
||||
id: groupId,
|
||||
name: groupName,
|
||||
members,
|
||||
active: true,
|
||||
expireTimer: 0,
|
||||
avatar: '',
|
||||
is_medium_group: false,
|
||||
},
|
||||
confirm: () => {},
|
||||
};
|
||||
|
||||
await onGroupReceived(ev);
|
||||
|
||||
const convo = await ConversationController.getOrCreateAndWait(
|
||||
groupId,
|
||||
'group'
|
||||
|
@ -717,15 +715,42 @@
|
|||
|
||||
const recipients = _.union(convo.get('members'), members);
|
||||
|
||||
await onMessageReceived(ev);
|
||||
convo.updateGroup({
|
||||
groupId,
|
||||
groupName,
|
||||
const isMediumGroup = convo.isMediumGroup();
|
||||
|
||||
const updateObj = {
|
||||
id: groupId,
|
||||
name: groupName,
|
||||
avatar: nullAvatar,
|
||||
recipients,
|
||||
members,
|
||||
is_medium_group: isMediumGroup,
|
||||
options,
|
||||
});
|
||||
};
|
||||
|
||||
// Send own sender keys and group secret key
|
||||
if (isMediumGroup) {
|
||||
const { chainKey, keyIdx } = await window.SenderKeyAPI.getSenderKeys(
|
||||
groupId,
|
||||
ourKey
|
||||
);
|
||||
|
||||
updateObj.senderKey = {
|
||||
chainKey: StringView.arrayBufferToHex(chainKey),
|
||||
keyIdx,
|
||||
};
|
||||
|
||||
const groupIdentity = await window.Signal.Data.getIdentityKeyById(
|
||||
groupId
|
||||
);
|
||||
|
||||
const secretKeyHex = StringView.hexToArrayBuffer(
|
||||
groupIdentity.secretKey
|
||||
);
|
||||
|
||||
updateObj.secretKey = secretKeyHex;
|
||||
}
|
||||
|
||||
convo.updateGroup(updateObj);
|
||||
};
|
||||
|
||||
window.createMediumSizeGroup = async (groupName, members) => {
|
||||
|
@ -744,66 +769,73 @@
|
|||
identityKeys.privKey
|
||||
);
|
||||
|
||||
// Constructing a "create group" message
|
||||
const proto = new textsecure.protobuf.DataMessage();
|
||||
const primary = window.storage.get('primaryDevicePubKey');
|
||||
|
||||
const groupUpdate = new textsecure.protobuf.MediumGroupUpdate();
|
||||
|
||||
groupUpdate.groupId = groupId;
|
||||
groupUpdate.groupSecretKey = groupSecretKeyHex;
|
||||
groupUpdate.senderKey = senderKey;
|
||||
groupUpdate.members = [ourIdentity, ...members];
|
||||
groupUpdate.groupName = groupName;
|
||||
proto.mediumGroupUpdate = groupUpdate;
|
||||
const allMembers = [primary, ...members];
|
||||
|
||||
await window.Signal.Data.createOrUpdateIdentityKey({
|
||||
id: groupId,
|
||||
secretKey: groupSecretKeyHex,
|
||||
});
|
||||
|
||||
const convo = await window.ConversationController.getOrCreateAndWait(
|
||||
const ev = {
|
||||
groupDetails: {
|
||||
id: groupId,
|
||||
name: groupName,
|
||||
members: allMembers,
|
||||
recipients: allMembers,
|
||||
active: true,
|
||||
expireTimer: 0,
|
||||
avatar: '',
|
||||
secretKey: identityKeys.privKey,
|
||||
senderKey,
|
||||
is_medium_group: true,
|
||||
},
|
||||
confirm: () => {},
|
||||
};
|
||||
|
||||
await onGroupReceived(ev);
|
||||
|
||||
const convo = await ConversationController.getOrCreateAndWait(
|
||||
groupId,
|
||||
Message.GROUP
|
||||
'group'
|
||||
);
|
||||
|
||||
convo.set('is_medium_group', true);
|
||||
convo.set('active_at', Date.now());
|
||||
convo.set('name', groupName);
|
||||
convo.updateGroupAdmins([primary]);
|
||||
convo.updateGroup(ev.groupDetails);
|
||||
|
||||
convo.setFriendRequestStatus(
|
||||
window.friends.friendRequestStatusEnum.friends
|
||||
);
|
||||
|
||||
appView.openConversation(groupId, {});
|
||||
|
||||
// Subscribe to this group id
|
||||
messageReceiver.pollForAdditionalId(groupId);
|
||||
|
||||
// TODO: include ourselves so that our lined devices work as well!
|
||||
await textsecure.messaging.updateMediumGroup(members, proto);
|
||||
};
|
||||
|
||||
window.doCreateGroup = async (groupName, members) => {
|
||||
const keypair = await libsignal.KeyHelper.generateIdentityKeyPair();
|
||||
const groupId = StringView.arrayBufferToHex(keypair.pubKey);
|
||||
|
||||
const ev = new Event('group');
|
||||
|
||||
const primaryDeviceKey =
|
||||
window.storage.get('primaryDevicePubKey') ||
|
||||
textsecure.storage.user.getNumber();
|
||||
const allMembers = [primaryDeviceKey, ...members];
|
||||
|
||||
ev.groupDetails = {
|
||||
id: groupId,
|
||||
name: groupName,
|
||||
members: allMembers,
|
||||
recipients: allMembers,
|
||||
active: true,
|
||||
expireTimer: 0,
|
||||
avatar: '',
|
||||
const ev = {
|
||||
groupDetails: {
|
||||
id: groupId,
|
||||
name: groupName,
|
||||
members: allMembers,
|
||||
recipients: allMembers,
|
||||
active: true,
|
||||
expireTimer: 0,
|
||||
avatar: '',
|
||||
},
|
||||
confirm: () => {},
|
||||
};
|
||||
|
||||
ev.confirm = () => {};
|
||||
|
||||
await onGroupReceived(ev);
|
||||
|
||||
const convo = await ConversationController.getOrCreateAndWait(
|
||||
|
@ -820,6 +852,7 @@
|
|||
window.friends.friendRequestStatusEnum.friends
|
||||
);
|
||||
|
||||
textsecure.messaging.sendGroupSyncMessage([convo]);
|
||||
appView.openConversation(groupId, {});
|
||||
};
|
||||
|
||||
|
@ -955,10 +988,6 @@
|
|||
window.setSettingValue('link-preview-setting', false);
|
||||
}
|
||||
|
||||
// Render onboarding message from LeftPaneMessageSection
|
||||
// unless user turns it off during their session
|
||||
window.setSettingValue('render-message-onboarding', true);
|
||||
|
||||
// Generates useful random ID for various purposes
|
||||
window.generateID = () =>
|
||||
Math.random()
|
||||
|
@ -1011,20 +1040,6 @@
|
|||
return toastID;
|
||||
};
|
||||
|
||||
window.getFriendsFromContacts = contacts => {
|
||||
// To call from TypeScript, input / output are both
|
||||
// of type Array<ConversationType>
|
||||
let friendList = contacts;
|
||||
if (friendList !== undefined) {
|
||||
friendList = friendList.filter(
|
||||
friend =>
|
||||
(friend.type === 'direct' && !friend.isMe) ||
|
||||
(friend.type === 'group' && !friend.isPublic && !friend.isRss)
|
||||
);
|
||||
}
|
||||
return friendList;
|
||||
};
|
||||
|
||||
// Get memberlist. This function is not accurate >>
|
||||
// window.getMemberList = window.lokiPublicChatAPI.getListOfMembers();
|
||||
|
||||
|
@ -1428,9 +1443,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);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -1760,7 +1777,6 @@
|
|||
const details = ev.contactDetails;
|
||||
|
||||
const id = details.number;
|
||||
|
||||
libloki.api.debug.logContactSync(
|
||||
'Got sync contact message with',
|
||||
id,
|
||||
|
@ -1815,12 +1831,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(
|
||||
|
@ -1918,6 +1943,7 @@
|
|||
members: details.members,
|
||||
color: details.color,
|
||||
type: 'group',
|
||||
is_medium_group: details.is_medium_group || false,
|
||||
};
|
||||
|
||||
if (details.active) {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
/* global LokiAppDotNetServerAPI, LokiFileServerAPI, semver, log */
|
||||
/* global LokiAppDotNetServerAPI, semver, log */
|
||||
// eslint-disable-next-line func-names
|
||||
(function() {
|
||||
'use strict';
|
||||
|
@ -12,9 +12,8 @@
|
|||
);
|
||||
// use the anonymous access token
|
||||
window.tokenlessFileServerAdnAPI.token = 'loki';
|
||||
window.tokenlessFileServerAdnAPI.pubKey = window.Signal.Crypto.base64ToArrayBuffer(
|
||||
LokiFileServerAPI.secureRpcPubKey
|
||||
);
|
||||
// configure for file server comms
|
||||
window.tokenlessFileServerAdnAPI.getPubKeyForUrl();
|
||||
|
||||
let nextWaitSeconds = 5;
|
||||
const checkForUpgrades = async () => {
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
log,
|
||||
i18n,
|
||||
Backbone,
|
||||
libloki,
|
||||
ConversationController,
|
||||
MessageController,
|
||||
storage,
|
||||
|
@ -13,7 +14,8 @@
|
|||
clipboard,
|
||||
BlockedNumberController,
|
||||
lokiPublicChatAPI,
|
||||
JobQueue
|
||||
JobQueue,
|
||||
StringView
|
||||
*/
|
||||
|
||||
/* eslint-disable more/no-then */
|
||||
|
@ -234,6 +236,9 @@
|
|||
isBlocked() {
|
||||
return BlockedNumberController.isBlocked(this.id);
|
||||
},
|
||||
isMediumGroup() {
|
||||
return this.get('is_medium_group');
|
||||
},
|
||||
block() {
|
||||
BlockedNumberController.block(this.id);
|
||||
this.trigger('change');
|
||||
|
@ -245,22 +250,29 @@
|
|||
this.messageCollection.forEach(m => m.trigger('change'));
|
||||
},
|
||||
async acceptFriendRequest() {
|
||||
// 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
|
||||
);
|
||||
}
|
||||
},
|
||||
|
@ -549,6 +561,7 @@
|
|||
MessageCollection: Whisper.MessageCollection,
|
||||
}
|
||||
);
|
||||
|
||||
if (typeof status === 'string') {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
status = [status];
|
||||
|
@ -584,7 +597,6 @@
|
|||
|
||||
const result = {
|
||||
id: this.id,
|
||||
|
||||
isArchived: this.get('isArchived'),
|
||||
activeAt: this.get('active_at'),
|
||||
avatarPath: this.getAvatarPath(),
|
||||
|
@ -970,25 +982,47 @@
|
|||
Conversation: Whisper.Conversation,
|
||||
});
|
||||
},
|
||||
async respondToAllFriendRequests(options) {
|
||||
async updateAllFriendRequestsMessages(options) {
|
||||
const { response, status, direction = null } = options;
|
||||
// Ignore if no response supplied
|
||||
if (!response) {
|
||||
return;
|
||||
}
|
||||
const primaryConversation = ConversationController.get(
|
||||
|
||||
// Accept FRs from all the user's devices
|
||||
const allDevices = await libloki.storage.getAllDevicePubKeysForPrimaryPubKey(
|
||||
this.getPrimaryDevicePubKey()
|
||||
);
|
||||
// Should never happen
|
||||
if (!primaryConversation) {
|
||||
|
||||
if (!allDevices.length) {
|
||||
return;
|
||||
}
|
||||
const pending = await primaryConversation.getFriendRequests(
|
||||
direction,
|
||||
status
|
||||
|
||||
const allConversationsWithUser = allDevices.map(d =>
|
||||
ConversationController.get(d)
|
||||
);
|
||||
|
||||
// Search through each conversation (device) for friend request messages
|
||||
const pendingRequestPromises = allConversationsWithUser.map(
|
||||
async conversation => {
|
||||
const request = (
|
||||
await conversation.getFriendRequests(direction, status)
|
||||
)[0];
|
||||
return { conversation, request };
|
||||
}
|
||||
);
|
||||
|
||||
let pendingRequests = await Promise.all(pendingRequestPromises);
|
||||
|
||||
// Filter out all undefined requests
|
||||
pendingRequests = pendingRequests.filter(p => Boolean(p.request));
|
||||
|
||||
// We set all friend request messages from all devices
|
||||
// from a user here to accepted where possible
|
||||
await Promise.all(
|
||||
pending.map(async request => {
|
||||
pendingRequests.map(async friendRequest => {
|
||||
const { conversation, request } = friendRequest;
|
||||
|
||||
if (request.hasErrors()) {
|
||||
return;
|
||||
}
|
||||
|
@ -997,12 +1031,12 @@
|
|||
await window.Signal.Data.saveMessage(request.attributes, {
|
||||
Message: Whisper.Message,
|
||||
});
|
||||
primaryConversation.trigger('updateMessage', request);
|
||||
conversation.trigger('updateMessage', request);
|
||||
})
|
||||
);
|
||||
},
|
||||
async respondToAllPendingFriendRequests(options) {
|
||||
return this.respondToAllFriendRequests({
|
||||
async updateAllPendingFriendRequestsMessages(options) {
|
||||
return this.updateAllFriendRequestsMessages({
|
||||
...options,
|
||||
status: 'pending',
|
||||
});
|
||||
|
@ -1017,7 +1051,7 @@
|
|||
// We have declined an incoming friend request
|
||||
async onDeclineFriendRequest() {
|
||||
this.setFriendRequestStatus(FriendRequestStatusEnum.none);
|
||||
await this.respondToAllPendingFriendRequests({
|
||||
await this.updateAllPendingFriendRequestsMessages({
|
||||
response: 'declined',
|
||||
direction: 'incoming',
|
||||
});
|
||||
|
@ -1031,13 +1065,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'],
|
||||
|
@ -1047,6 +1084,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
|
||||
|
@ -1059,7 +1102,7 @@
|
|||
}
|
||||
if (this.hasSentFriendRequest()) {
|
||||
this.setFriendRequestStatus(FriendRequestStatusEnum.friends);
|
||||
await this.respondToAllFriendRequests({
|
||||
await this.updateAllFriendRequestsMessages({
|
||||
response: 'accepted',
|
||||
status: ['pending', 'expired'],
|
||||
});
|
||||
|
@ -1091,7 +1134,7 @@
|
|||
}
|
||||
|
||||
// Change any pending outgoing friend requests to expired
|
||||
await this.respondToAllPendingFriendRequests({
|
||||
await this.updateAllPendingFriendRequestsMessages({
|
||||
response: 'expired',
|
||||
direction: 'outgoing',
|
||||
});
|
||||
|
@ -1104,7 +1147,7 @@
|
|||
await Promise.all([
|
||||
this.setFriendRequestStatus(FriendRequestStatusEnum.friends),
|
||||
// Accept all outgoing FR
|
||||
this.respondToAllPendingFriendRequests({
|
||||
this.updateAllPendingFriendRequestsMessages({
|
||||
direction: 'outgoing',
|
||||
response: 'accepted',
|
||||
}),
|
||||
|
@ -1658,6 +1701,7 @@
|
|||
|
||||
const model = this.addSingleMessage(attributes);
|
||||
const message = MessageController.register(model.id, model);
|
||||
|
||||
await window.Signal.Data.saveMessage(message.attributes, {
|
||||
forceSave: true,
|
||||
Message: Whisper.Message,
|
||||
|
@ -1753,7 +1797,7 @@
|
|||
let dest = destination;
|
||||
let numbers = groupNumbers;
|
||||
|
||||
if (this.get('is_medium_group')) {
|
||||
if (this.isMediumGroup()) {
|
||||
dest = this.id;
|
||||
numbers = [destination];
|
||||
options.isMediumGroup = true;
|
||||
|
@ -2243,6 +2287,12 @@
|
|||
}
|
||||
},
|
||||
|
||||
async saveChangesToDB() {
|
||||
await window.Signal.Data.updateConversation(this.id, this.attributes, {
|
||||
Conversation: Whisper.Conversation,
|
||||
});
|
||||
},
|
||||
|
||||
async updateGroup(providedGroupUpdate) {
|
||||
let groupUpdate = providedGroupUpdate;
|
||||
|
||||
|
@ -2261,15 +2311,44 @@
|
|||
group_update: groupUpdate,
|
||||
});
|
||||
|
||||
const id = await window.Signal.Data.saveMessage(message.attributes, {
|
||||
Message: Whisper.Message,
|
||||
});
|
||||
message.set({ id });
|
||||
const messageId = await window.Signal.Data.saveMessage(
|
||||
message.attributes,
|
||||
{
|
||||
Message: Whisper.Message,
|
||||
}
|
||||
);
|
||||
message.set({ id: messageId });
|
||||
|
||||
const options = this.getSendOptions();
|
||||
|
||||
if (groupUpdate.is_medium_group) {
|
||||
// Constructing a "create group" message
|
||||
const proto = new textsecure.protobuf.DataMessage();
|
||||
|
||||
const mgUpdate = new textsecure.protobuf.MediumGroupUpdate();
|
||||
|
||||
const { id, name, secretKey, senderKey, members } = groupUpdate;
|
||||
|
||||
mgUpdate.type = textsecure.protobuf.MediumGroupUpdate.Type.NEW_GROUP;
|
||||
mgUpdate.groupId = id;
|
||||
mgUpdate.groupSecretKey = secretKey;
|
||||
mgUpdate.senderKey = new textsecure.protobuf.SenderKey(senderKey);
|
||||
mgUpdate.members = members.map(pkHex =>
|
||||
StringView.hexToArrayBuffer(pkHex)
|
||||
);
|
||||
mgUpdate.groupName = name;
|
||||
mgUpdate.admins = this.get('groupAdmins');
|
||||
proto.mediumGroupUpdate = mgUpdate;
|
||||
|
||||
message.send(
|
||||
this.wrapSend(textsecure.messaging.updateMediumGroup(members, proto))
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
message.send(
|
||||
this.wrapSend(
|
||||
textsecure.messaging.updateGroup(
|
||||
textsecure.messaging.sendGroupUpdate(
|
||||
this.id,
|
||||
this.get('name'),
|
||||
this.get('avatar'),
|
||||
|
@ -2285,7 +2364,7 @@
|
|||
sendGroupInfo(recipients) {
|
||||
if (this.isClosedGroup()) {
|
||||
const options = this.getSendOptions();
|
||||
textsecure.messaging.updateGroup(
|
||||
textsecure.messaging.sendGroupUpdate(
|
||||
this.id,
|
||||
this.get('name'),
|
||||
this.get('avatar'),
|
||||
|
@ -2299,9 +2378,19 @@
|
|||
|
||||
async leaveGroup() {
|
||||
const now = Date.now();
|
||||
|
||||
if (this.isMediumGroup()) {
|
||||
// NOTE: we should probably remove sender keys for groupId,
|
||||
// and its secret key, but it is low priority
|
||||
|
||||
// TODO: need to reset everyone's sender keys
|
||||
window.lokiMessageAPI.stopPollingForGroup(this.id);
|
||||
}
|
||||
|
||||
if (this.get('type') === 'group') {
|
||||
const groupNumbers = this.getRecipients();
|
||||
this.set({ left: true });
|
||||
|
||||
await window.Signal.Data.updateConversation(this.id, this.attributes, {
|
||||
Conversation: Whisper.Conversation,
|
||||
});
|
||||
|
@ -2439,7 +2528,6 @@
|
|||
},
|
||||
|
||||
// LOKI PROFILES
|
||||
|
||||
async setNickname(nickname) {
|
||||
const trimmed = nickname && nickname.trim();
|
||||
if (this.get('nickname') === trimmed) {
|
||||
|
|
|
@ -418,33 +418,79 @@
|
|||
if (this.get('friendStatus') !== 'pending') {
|
||||
return;
|
||||
}
|
||||
const conversation = await this.getSourceDeviceConversation();
|
||||
// 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
|
||||
// friend request to reset the session:
|
||||
if (conversation.get('sessionRestoreSeen')) {
|
||||
conversation.sendMessage('', null, null, null, null, {
|
||||
sessionRestoration: true,
|
||||
});
|
||||
return;
|
||||
|
||||
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))
|
||||
.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
|
||||
// friend request to reset the session:
|
||||
if (conversation.get('sessionRestoreSeen')) {
|
||||
conversation.sendMessage('', null, null, null, null, {
|
||||
sessionRestoration: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
profileName = conversation.getProfileName() || profileName;
|
||||
conversation.onAcceptFriendRequest();
|
||||
});
|
||||
|
||||
// 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
|
||||
);
|
||||
if (!primaryConversation.getProfileName() && profileName) {
|
||||
await primaryConversation.setNickname(profileName);
|
||||
}
|
||||
this.set({ friendStatus: 'accepted' });
|
||||
|
||||
await window.Signal.Data.saveMessage(this.attributes, {
|
||||
Message: Whisper.Message,
|
||||
});
|
||||
conversation.onAcceptFriendRequest();
|
||||
|
||||
this.set({ friendStatus: 'accepted' });
|
||||
|
||||
// Update redux store
|
||||
window.Signal.Data.updateConversation(
|
||||
primaryConversation.id,
|
||||
primaryConversation.attributes,
|
||||
{ Conversation: Whisper.Conversation }
|
||||
);
|
||||
},
|
||||
async declineFriendRequest() {
|
||||
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';
|
||||
|
@ -1444,7 +1490,8 @@
|
|||
if (!this.isFriendRequest()) {
|
||||
const c = this.getConversation();
|
||||
// Don't bother sending sync messages to public chats
|
||||
if (c && !c.isPublic()) {
|
||||
// or groups with sender keys
|
||||
if (c && !c.isPublic() && !c.isMediumGroup()) {
|
||||
this.sendSyncMessage();
|
||||
}
|
||||
}
|
||||
|
@ -2051,6 +2098,7 @@
|
|||
return false;
|
||||
},
|
||||
async handleSessionRequest(source, confirm) {
|
||||
window.console.log(`Received SESSION_REQUEST from source: ${source}`);
|
||||
window.libloki.api.sendSessionEstablishedMessage(source);
|
||||
confirm();
|
||||
},
|
||||
|
@ -2202,11 +2250,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');
|
||||
|
@ -2219,8 +2266,9 @@
|
|||
try {
|
||||
const now = new Date().getTime();
|
||||
let attributes = {
|
||||
...conversation.attributes,
|
||||
...conversationPrimary.attributes,
|
||||
};
|
||||
|
||||
if (dataMessage.group) {
|
||||
let groupUpdate = null;
|
||||
attributes = {
|
||||
|
@ -2236,18 +2284,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');
|
||||
|
@ -2261,7 +2309,7 @@
|
|||
|
||||
// Check if anyone got kicked:
|
||||
const removedMembers = _.difference(
|
||||
conversation.get('members'),
|
||||
conversationPrimary.get('members'),
|
||||
attributes.members
|
||||
);
|
||||
|
||||
|
@ -2283,7 +2331,7 @@
|
|||
groupUpdate = { left: source };
|
||||
}
|
||||
attributes.members = _.without(
|
||||
conversation.get('members'),
|
||||
conversationPrimary.get('members'),
|
||||
source
|
||||
);
|
||||
}
|
||||
|
@ -2316,7 +2364,7 @@
|
|||
attachments: dataMessage.attachments,
|
||||
body: dataMessage.body,
|
||||
contact: dataMessage.contact,
|
||||
conversationId: conversation.id,
|
||||
conversationId: conversationPrimary.id,
|
||||
decrypted_at: now,
|
||||
errors: [],
|
||||
flags: dataMessage.flags,
|
||||
|
@ -2330,7 +2378,7 @@
|
|||
|
||||
if (type === 'outgoing') {
|
||||
const receipts = Whisper.DeliveryReceipts.forMessage(
|
||||
conversation,
|
||||
conversationPrimary,
|
||||
message
|
||||
);
|
||||
receipts.forEach(receipt =>
|
||||
|
@ -2343,10 +2391,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({
|
||||
|
@ -2355,7 +2403,7 @@
|
|||
expireTimer: dataMessage.expireTimer,
|
||||
},
|
||||
});
|
||||
conversation.set({ expireTimer: dataMessage.expireTimer });
|
||||
conversationPrimary.set({ expireTimer: dataMessage.expireTimer });
|
||||
} else if (dataMessage.expireTimer) {
|
||||
message.set({ expireTimer: dataMessage.expireTimer });
|
||||
}
|
||||
|
@ -2367,7 +2415,7 @@
|
|||
message.isExpirationTimerUpdate() || expireTimer;
|
||||
if (shouldLogExpireTimerChange) {
|
||||
window.log.info("Update conversation 'expireTimer'", {
|
||||
id: conversation.idForLogging(),
|
||||
id: conversationPrimary.idForLogging(),
|
||||
expireTimer,
|
||||
source: 'handleDataMessage',
|
||||
});
|
||||
|
@ -2375,8 +2423,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'),
|
||||
|
@ -2386,18 +2437,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 });
|
||||
|
@ -2429,11 +2480,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,
|
||||
});
|
||||
}
|
||||
|
@ -2441,7 +2492,7 @@
|
|||
|
||||
if (type === 'outgoing') {
|
||||
const reads = Whisper.ReadReceipts.forMessage(
|
||||
conversation,
|
||||
conversationPrimary,
|
||||
message
|
||||
);
|
||||
if (reads.length) {
|
||||
|
@ -2452,39 +2503,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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2510,8 +2557,9 @@
|
|||
- We are friends with the user,
|
||||
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) {
|
||||
|
@ -2525,13 +2573,13 @@
|
|||
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();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2553,11 +2601,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
|
||||
|
@ -2595,9 +2643,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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -7,6 +7,8 @@ const FormData = require('form-data');
|
|||
const https = require('https');
|
||||
const path = require('path');
|
||||
|
||||
const lokiRpcUtils = require('./loki_rpc');
|
||||
|
||||
// Can't be less than 1200 if we have unauth'd requests
|
||||
const PUBLICCHAT_MSG_POLL_EVERY = 1.5 * 1000; // 1.5s
|
||||
const PUBLICCHAT_CHAN_POLL_EVERY = 20 * 1000; // 20s
|
||||
|
@ -14,6 +16,7 @@ const PUBLICCHAT_DELETION_POLL_EVERY = 5 * 1000; // 5s
|
|||
const PUBLICCHAT_MOD_POLL_EVERY = 30 * 1000; // 30s
|
||||
const PUBLICCHAT_MIN_TIME_BETWEEN_DUPLICATE_MESSAGES = 10 * 1000; // 10s
|
||||
|
||||
// FIXME: replace with something on urlPubkeyMap...
|
||||
const FILESERVER_HOSTS = [
|
||||
'file-dev.lokinet.org',
|
||||
'file.lokinet.org',
|
||||
|
@ -21,6 +24,17 @@ const FILESERVER_HOSTS = [
|
|||
'file.getsession.org',
|
||||
];
|
||||
|
||||
const LOKIFOUNDATION_DEVFILESERVER_PUBKEY =
|
||||
'BSZiMVxOco/b3sYfaeyiMWv/JnqokxGXkHoclEx8TmZ6';
|
||||
const LOKIFOUNDATION_FILESERVER_PUBKEY =
|
||||
'BWJQnVm97sQE3Q1InB4Vuo+U/T1hmwHBv0ipkiv8tzEc';
|
||||
const urlPubkeyMap = {
|
||||
'https://file-dev.getsession.org': LOKIFOUNDATION_DEVFILESERVER_PUBKEY,
|
||||
'https://file-dev.lokinet.org': LOKIFOUNDATION_DEVFILESERVER_PUBKEY,
|
||||
'https://file.getsession.org': LOKIFOUNDATION_FILESERVER_PUBKEY,
|
||||
'https://file.lokinet.org': LOKIFOUNDATION_FILESERVER_PUBKEY,
|
||||
};
|
||||
|
||||
const HOMESERVER_USER_ANNOTATION_TYPE = 'network.loki.messenger.homeserver';
|
||||
const AVATAR_USER_ANNOTATION_TYPE = 'network.loki.messenger.avatar';
|
||||
const SETTINGS_CHANNEL_ANNOTATION_TYPE = 'net.patter-app.settings';
|
||||
|
@ -34,12 +48,120 @@ const snodeHttpsAgent = new https.Agent({
|
|||
|
||||
const timeoutDelay = ms => new Promise(resolve => setTimeout(resolve, ms));
|
||||
|
||||
const sendToProxy = async (
|
||||
srvPubKey,
|
||||
endpoint,
|
||||
pFetchOptions,
|
||||
options = {}
|
||||
) => {
|
||||
const sendViaOnion = async (srvPubKey, url, fetchOptions, options = {}) => {
|
||||
if (!srvPubKey) {
|
||||
log.error(
|
||||
'loki_app_dot_net:::sendViaOnion - called without a server public key'
|
||||
);
|
||||
return {};
|
||||
}
|
||||
|
||||
// set retry count
|
||||
if (options.retry === undefined) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
options.retry = 0;
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
options.requestNumber = window.lokiSnodeAPI.assignOnionRequestNumber();
|
||||
}
|
||||
|
||||
const payloadObj = {
|
||||
method: fetchOptions.method,
|
||||
body: fetchOptions.body,
|
||||
// safety issue with file server, just safer to have this
|
||||
headers: fetchOptions.headers || {},
|
||||
// no initial /
|
||||
endpoint: url.pathname.replace(/^\//, ''),
|
||||
};
|
||||
if (url.search) {
|
||||
payloadObj.endpoint += `?${url.search}`;
|
||||
}
|
||||
|
||||
// from https://github.com/sindresorhus/is-stream/blob/master/index.js
|
||||
if (
|
||||
payloadObj.body &&
|
||||
typeof payloadObj.body === 'object' &&
|
||||
typeof payloadObj.body.pipe === 'function'
|
||||
) {
|
||||
const fData = payloadObj.body.getBuffer();
|
||||
const fHeaders = payloadObj.body.getHeaders();
|
||||
// update headers for boundary
|
||||
payloadObj.headers = { ...payloadObj.headers, ...fHeaders };
|
||||
// update body with base64 chunk
|
||||
payloadObj.body = {
|
||||
fileUpload: fData.toString('base64'),
|
||||
};
|
||||
}
|
||||
|
||||
let pathNodes = [];
|
||||
try {
|
||||
pathNodes = await lokiSnodeAPI.getOnionPath();
|
||||
} catch (e) {
|
||||
log.error(
|
||||
`loki_app_dot_net:::sendViaOnion #${options.requestNumber} - getOnionPath Error ${e.code} ${e.message}`
|
||||
);
|
||||
}
|
||||
if (!pathNodes || !pathNodes.length) {
|
||||
log.warn(
|
||||
`loki_app_dot_net:::sendViaOnion #${options.requestNumber} - failing, no path available`
|
||||
);
|
||||
// should we retry?
|
||||
return {};
|
||||
}
|
||||
|
||||
// do the request
|
||||
let result;
|
||||
try {
|
||||
result = await lokiRpcUtils.sendOnionRequestLsrpcDest(
|
||||
0,
|
||||
pathNodes,
|
||||
srvPubKey,
|
||||
url.host,
|
||||
payloadObj,
|
||||
options.requestNumber
|
||||
);
|
||||
} catch (e) {
|
||||
log.error(
|
||||
'loki_app_dot_net:::sendViaOnion - lokiRpcUtils error',
|
||||
e.code,
|
||||
e.message
|
||||
);
|
||||
return {};
|
||||
}
|
||||
|
||||
// handle error/retries
|
||||
if (!result.status) {
|
||||
log.error(
|
||||
`loki_app_dot_net:::sendViaOnion #${options.requestNumber} - Retry #${options.retry} Couldnt handle onion request, retrying`,
|
||||
payloadObj
|
||||
);
|
||||
return sendViaOnion(srvPubKey, url, fetchOptions, {
|
||||
...options,
|
||||
retry: options.retry + 1,
|
||||
counter: options.requestNumber,
|
||||
});
|
||||
}
|
||||
|
||||
// get the return variables we need
|
||||
let response = {};
|
||||
let txtResponse = '';
|
||||
let body = '';
|
||||
try {
|
||||
body = JSON.parse(result.body);
|
||||
} catch (e) {
|
||||
log.error(
|
||||
`loki_app_dot_net:::sendViaOnion #${options.requestNumber} - Cant decode JSON body`,
|
||||
result.body
|
||||
);
|
||||
}
|
||||
// result.status has the http response code
|
||||
txtResponse = JSON.stringify(body);
|
||||
response = body;
|
||||
response.headers = result.headers;
|
||||
|
||||
return { result, txtResponse, response };
|
||||
};
|
||||
|
||||
const sendToProxy = async (srvPubKey, endpoint, fetchOptions, options = {}) => {
|
||||
if (!srvPubKey) {
|
||||
log.error(
|
||||
'loki_app_dot_net:::sendToProxy - called without a server public key'
|
||||
|
@ -47,17 +169,12 @@ const sendToProxy = async (
|
|||
return {};
|
||||
}
|
||||
|
||||
const fetchOptions = pFetchOptions; // make lint happy
|
||||
// safety issue with file server, just safer to have this
|
||||
if (fetchOptions.headers === undefined) {
|
||||
fetchOptions.headers = {};
|
||||
}
|
||||
|
||||
const payloadObj = {
|
||||
body: fetchOptions.body, // might need to b64 if binary...
|
||||
endpoint,
|
||||
method: fetchOptions.method,
|
||||
headers: fetchOptions.headers,
|
||||
// safety issue with file server, just safer to have this
|
||||
headers: fetchOptions.headers || {},
|
||||
};
|
||||
|
||||
// from https://github.com/sindresorhus/is-stream/blob/master/index.js
|
||||
|
@ -87,7 +204,7 @@ const sendToProxy = async (
|
|||
log.warn('proxy random snode pool is not ready, retrying 10s', endpoint);
|
||||
// no nodes in the pool yet, give it some time and retry
|
||||
await timeoutDelay(1000);
|
||||
return sendToProxy(srvPubKey, endpoint, pFetchOptions, options);
|
||||
return sendToProxy(srvPubKey, endpoint, fetchOptions, options);
|
||||
}
|
||||
const url = `https://${randSnode.ip}:${randSnode.port}/file_proxy`;
|
||||
|
||||
|
@ -98,7 +215,10 @@ const sendToProxy = async (
|
|||
payloadObj.body = false; // free memory
|
||||
|
||||
// make temporary key for this request/response
|
||||
const ephemeralKey = await libsignal.Curve.async.generateKeyPair();
|
||||
// async maybe preferable to avoid cpu spikes
|
||||
// tho I think sync might be more apt in certain cases here...
|
||||
// like sending
|
||||
const ephemeralKey = await libloki.crypto.generateEphemeralKeyPair();
|
||||
|
||||
// mix server pub key with our priv key
|
||||
const symKey = await libsignal.Curve.async.calculateAgreement(
|
||||
|
@ -253,6 +373,21 @@ const serverRequest = async (endpoint, options = {}) => {
|
|||
const host = url.host.toLowerCase();
|
||||
// log.info('host', host, FILESERVER_HOSTS);
|
||||
if (
|
||||
window.lokiFeatureFlags.useFileOnionRequests &&
|
||||
FILESERVER_HOSTS.includes(host)
|
||||
) {
|
||||
mode = 'sendViaOnion';
|
||||
// url.search automatically includes the ? part
|
||||
// const search = url.search || '';
|
||||
// strip first slash
|
||||
// const endpointWithQS = `${url.pathname}${search}`.replace(/^\//, '');
|
||||
({ response, txtResponse, result } = await sendViaOnion(
|
||||
srvPubKey,
|
||||
url,
|
||||
fetchOptions,
|
||||
options
|
||||
));
|
||||
} else if (
|
||||
window.lokiFeatureFlags.useSnodeProxy &&
|
||||
FILESERVER_HOSTS.includes(host)
|
||||
) {
|
||||
|
@ -313,6 +448,14 @@ const serverRequest = async (endpoint, options = {}) => {
|
|||
err: e,
|
||||
};
|
||||
}
|
||||
|
||||
if (!result) {
|
||||
return {
|
||||
err: 'noResult',
|
||||
response,
|
||||
};
|
||||
}
|
||||
|
||||
// if it's a response style with a meta
|
||||
if (result.status !== 200) {
|
||||
if (!forceFreshToken && (!response.meta || response.meta.code === 401)) {
|
||||
|
@ -414,6 +557,74 @@ class LokiAppDotNetServerAPI {
|
|||
this.channels.splice(i, 1);
|
||||
}
|
||||
|
||||
// set up pubKey & pubKeyHex properties
|
||||
// optionally called for mainly file server comms
|
||||
getPubKeyForUrl() {
|
||||
if (
|
||||
!window.lokiFeatureFlags.useSnodeProxy &&
|
||||
!window.lokiFeatureFlags.useOnionRequests
|
||||
) {
|
||||
// pubkeys don't matter
|
||||
return '';
|
||||
}
|
||||
|
||||
// Hard coded
|
||||
let pubKeyAB;
|
||||
if (urlPubkeyMap) {
|
||||
pubKeyAB = window.Signal.Crypto.base64ToArrayBuffer(
|
||||
urlPubkeyMap[this.baseServerUrl]
|
||||
);
|
||||
}
|
||||
// else will fail validation later
|
||||
|
||||
// if in proxy mode, don't allow "file-dev."...
|
||||
// it only supports "file."... host.
|
||||
if (
|
||||
window.lokiFeatureFlags.useSnodeProxy &&
|
||||
!window.lokiFeatureFlags.useOnionRequests
|
||||
) {
|
||||
pubKeyAB = window.Signal.Crypto.base64ToArrayBuffer(
|
||||
LOKIFOUNDATION_FILESERVER_PUBKEY
|
||||
);
|
||||
}
|
||||
|
||||
// do we have their pubkey locally?
|
||||
// FIXME: this._server won't be set yet...
|
||||
// can't really do this for the file server because we'll need the key
|
||||
// before we can communicate with lsrpc
|
||||
/*
|
||||
// get remote pubKey
|
||||
this._server.serverRequest('loki/v1/public_key').then(keyRes => {
|
||||
// we don't need to delay to protect identity because the token request
|
||||
// should only be done over lokinet-lite
|
||||
this.delayToken = true;
|
||||
if (keyRes.err || !keyRes.response || !keyRes.response.data) {
|
||||
if (keyRes.err) {
|
||||
log.error(`Error ${keyRes.err}`);
|
||||
}
|
||||
} else {
|
||||
// store it
|
||||
this.pubKey = dcodeIO.ByteBuffer.wrap(
|
||||
keyRes.response.data,
|
||||
'base64'
|
||||
).toArrayBuffer();
|
||||
// write it to a file
|
||||
}
|
||||
});
|
||||
*/
|
||||
|
||||
// now that key is loaded, lets verify
|
||||
if (pubKeyAB && pubKeyAB.byteLength && pubKeyAB.byteLength !== 33) {
|
||||
log.error('FILESERVER PUBKEY is invalid, length:', pubKeyAB.byteLength);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
this.pubKey = pubKeyAB;
|
||||
this.pubKeyHex = StringView.arrayBufferToHex(pubKeyAB);
|
||||
|
||||
return pubKeyAB;
|
||||
}
|
||||
|
||||
async setProfileName(profileName) {
|
||||
// when we add an annotation, may need this
|
||||
/*
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
/* global log, libloki, process, window */
|
||||
/* global log, libloki, window */
|
||||
/* global storage: false */
|
||||
/* global Signal: false */
|
||||
/* global log: false */
|
||||
|
@ -8,49 +8,11 @@ const LokiAppDotNetAPI = require('./loki_app_dot_net_api');
|
|||
const DEVICE_MAPPING_USER_ANNOTATION_TYPE =
|
||||
'network.loki.messenger.devicemapping';
|
||||
|
||||
// const LOKIFOUNDATION_DEVFILESERVER_PUBKEY =
|
||||
// 'BSZiMVxOco/b3sYfaeyiMWv/JnqokxGXkHoclEx8TmZ6';
|
||||
const LOKIFOUNDATION_FILESERVER_PUBKEY =
|
||||
'BWJQnVm97sQE3Q1InB4Vuo+U/T1hmwHBv0ipkiv8tzEc';
|
||||
|
||||
// can have multiple of these instances as each user can have a
|
||||
// different home server
|
||||
class LokiFileServerInstance {
|
||||
constructor(ourKey) {
|
||||
this.ourKey = ourKey;
|
||||
|
||||
// do we have their pubkey locally?
|
||||
/*
|
||||
// get remote pubKey
|
||||
this._server.serverRequest('loki/v1/public_key').then(keyRes => {
|
||||
// we don't need to delay to protect identity because the token request
|
||||
// should only be done over lokinet-lite
|
||||
this.delayToken = true;
|
||||
if (keyRes.err || !keyRes.response || !keyRes.response.data) {
|
||||
if (keyRes.err) {
|
||||
log.error(`Error ${keyRes.err}`);
|
||||
}
|
||||
} else {
|
||||
// store it
|
||||
this.pubKey = dcodeIO.ByteBuffer.wrap(
|
||||
keyRes.response.data,
|
||||
'base64'
|
||||
).toArrayBuffer();
|
||||
// write it to a file
|
||||
}
|
||||
});
|
||||
*/
|
||||
// Hard coded
|
||||
this.pubKey = window.Signal.Crypto.base64ToArrayBuffer(
|
||||
LOKIFOUNDATION_FILESERVER_PUBKEY
|
||||
);
|
||||
if (this.pubKey.byteLength && this.pubKey.byteLength !== 33) {
|
||||
log.error(
|
||||
'FILESERVER PUBKEY is invalid, length:',
|
||||
this.pubKey.byteLength
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME: this is not file-server specific
|
||||
|
@ -58,16 +20,10 @@ 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);
|
||||
|
||||
// configure proxy
|
||||
this._server.pubKey = this.pubKey;
|
||||
// make sure pubKey & pubKeyHex are set in _server
|
||||
this.pubKey = this._server.getPubKeyForUrl();
|
||||
|
||||
if (options !== undefined && options.skipToken) {
|
||||
return;
|
||||
|
@ -80,6 +36,7 @@ class LokiFileServerInstance {
|
|||
log.error('You are blacklisted form this home server');
|
||||
}
|
||||
}
|
||||
|
||||
async getUserDeviceMapping(pubKey) {
|
||||
const annotations = await this._server.getUserAnnotations(pubKey);
|
||||
const deviceMapping = annotations.find(
|
||||
|
@ -331,7 +288,5 @@ class LokiFileServerFactoryAPI {
|
|||
return thisServer;
|
||||
}
|
||||
}
|
||||
// smuggle some data out of this joint (for expire.js/version upgrade check)
|
||||
LokiFileServerFactoryAPI.secureRpcPubKey = LOKIFOUNDATION_FILESERVER_PUBKEY;
|
||||
|
||||
module.exports = LokiFileServerFactoryAPI;
|
||||
|
|
|
@ -72,6 +72,8 @@ class LokiMessageAPI {
|
|||
this.jobQueue = new window.JobQueue();
|
||||
this.sendingData = {};
|
||||
this.ourKey = ourKey;
|
||||
// stop polling for a group if its id is no longer found here
|
||||
this.groupIdsToPoll = {};
|
||||
}
|
||||
|
||||
async sendMessage(pubKey, data, messageTimeStamp, ttl, options = {}) {
|
||||
|
@ -315,7 +317,7 @@ class LokiMessageAPI {
|
|||
);
|
||||
|
||||
// eslint-disable-next-line no-constant-condition
|
||||
while (true) {
|
||||
while (this.groupIdsToPoll[groupId]) {
|
||||
try {
|
||||
let messages = await _retrieveNextMessages(node, groupId);
|
||||
|
||||
|
@ -374,6 +376,13 @@ class LokiMessageAPI {
|
|||
async pollForGroupId(groupId, onMessages) {
|
||||
log.info(`Starting to poll for group id: ${groupId}`);
|
||||
|
||||
if (this.groupIdsToPoll[groupId]) {
|
||||
log.warn(`Already polling for group id: ${groupId}`);
|
||||
return;
|
||||
}
|
||||
|
||||
this.groupIdsToPoll[groupId] = true;
|
||||
|
||||
// Get nodes for groupId
|
||||
const nodes = await lokiSnodeAPI.refreshSwarmNodesForPubKey(groupId);
|
||||
|
||||
|
@ -384,6 +393,16 @@ class LokiMessageAPI {
|
|||
);
|
||||
}
|
||||
|
||||
async stopPollingForGroup(groupId) {
|
||||
if (!this.groupIdsToPoll[groupId]) {
|
||||
log.warn(`Already not polling for group id: ${groupId}`);
|
||||
return;
|
||||
}
|
||||
|
||||
log.warn(`Stop polling for group id: ${groupId}`);
|
||||
delete this.groupIdsToPoll[groupId];
|
||||
}
|
||||
|
||||
async _openRetrieveConnection(pSwarmPool, stopPollingPromise, onMessages) {
|
||||
const swarmPool = pSwarmPool; // lint
|
||||
let stopPollingResult = false;
|
||||
|
@ -505,9 +524,7 @@ class LokiMessageAPI {
|
|||
|
||||
// Start polling for medium size groups as well (they might be in different swarms)
|
||||
{
|
||||
const convos = window
|
||||
.getConversations()
|
||||
.filter(c => c.get('is_medium_group'));
|
||||
const convos = window.getConversations().filter(c => c.isMediumGroup());
|
||||
|
||||
const self = this;
|
||||
|
||||
|
|
|
@ -11,88 +11,257 @@ const snodeHttpsAgent = new https.Agent({
|
|||
|
||||
const endpointBase = '/storage_rpc/v1';
|
||||
|
||||
// Request index for debugging
|
||||
let onionReqIdx = 0;
|
||||
|
||||
const encryptForNode = async (node, payloadStr) => {
|
||||
const textEncoder = new TextEncoder();
|
||||
const plaintext = textEncoder.encode(payloadStr);
|
||||
|
||||
return libloki.crypto.encryptForPubkey(node.pubkey_x25519, plaintext);
|
||||
};
|
||||
|
||||
// Returns the actual ciphertext, symmetric key that will be used
|
||||
// for decryption, and an ephemeral_key to send to the next hop
|
||||
const encryptForDestination = async (node, payload) => {
|
||||
// Do we still need "headers"?
|
||||
const reqStr = JSON.stringify({ body: payload, headers: '' });
|
||||
const encryptForPubKey = async (pubKeyX25519hex, reqObj) => {
|
||||
const reqStr = JSON.stringify(reqObj);
|
||||
|
||||
return encryptForNode(node, reqStr);
|
||||
const textEncoder = new TextEncoder();
|
||||
const plaintext = textEncoder.encode(reqStr);
|
||||
|
||||
return libloki.crypto.encryptForPubkey(pubKeyX25519hex, plaintext);
|
||||
};
|
||||
|
||||
// `ctx` holds info used by `node` to relay further
|
||||
const encryptForRelay = async (node, nextNode, ctx) => {
|
||||
const encryptForRelay = async (relayX25519hex, destination, ctx) => {
|
||||
// ctx contains: ciphertext, symmetricKey, ephemeralKey
|
||||
const payload = ctx.ciphertext;
|
||||
|
||||
const reqJson = {
|
||||
if (!destination.host && !destination.destination) {
|
||||
log.warn(`loki_rpc::encryptForRelay - no destination`, destination);
|
||||
}
|
||||
|
||||
const reqObj = {
|
||||
...destination,
|
||||
ciphertext: dcodeIO.ByteBuffer.wrap(payload).toString('base64'),
|
||||
ephemeral_key: StringView.arrayBufferToHex(ctx.ephemeralKey),
|
||||
destination: nextNode.pubkey_ed25519,
|
||||
};
|
||||
|
||||
const reqStr = JSON.stringify(reqJson);
|
||||
|
||||
return encryptForNode(node, reqStr);
|
||||
return encryptForPubKey(relayX25519hex, reqObj);
|
||||
};
|
||||
|
||||
const BAD_PATH = 'bad_path';
|
||||
|
||||
// May return false BAD_PATH, indicating that we should try a new
|
||||
const sendOnionRequest = async (reqIdx, nodePath, targetNode, plaintext) => {
|
||||
const ctxes = [await encryptForDestination(targetNode, plaintext)];
|
||||
// from (3) 2 to 0
|
||||
const firstPos = nodePath.length - 1;
|
||||
|
||||
for (let i = firstPos; i > -1; i -= 1) {
|
||||
// this nodePath points to the previous (i + 1) context
|
||||
ctxes.push(
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await encryptForRelay(
|
||||
nodePath[i],
|
||||
i === firstPos ? targetNode : nodePath[i + 1],
|
||||
ctxes[ctxes.length - 1]
|
||||
)
|
||||
);
|
||||
}
|
||||
const guardCtx = ctxes[ctxes.length - 1]; // last ctx
|
||||
|
||||
const makeGuardPayload = guardCtx => {
|
||||
const ciphertextBase64 = dcodeIO.ByteBuffer.wrap(
|
||||
guardCtx.ciphertext
|
||||
).toString('base64');
|
||||
|
||||
const payload = {
|
||||
const guardPayloadObj = {
|
||||
ciphertext: ciphertextBase64,
|
||||
ephemeral_key: StringView.arrayBufferToHex(guardCtx.ephemeralKey),
|
||||
};
|
||||
return guardPayloadObj;
|
||||
};
|
||||
|
||||
const fetchOptions = {
|
||||
// we just need the targetNode.pubkey_ed25519 for the encryption
|
||||
// targetPubKey is ed25519 if snode is the target
|
||||
const makeOnionRequest = async (
|
||||
nodePath,
|
||||
destCtx,
|
||||
targetED25519Hex,
|
||||
finalRelayOptions = false,
|
||||
id = ''
|
||||
) => {
|
||||
const ctxes = [destCtx];
|
||||
// from (3) 2 to 0
|
||||
const firstPos = nodePath.length - 1;
|
||||
|
||||
for (let i = firstPos; i > -1; i -= 1) {
|
||||
let dest;
|
||||
const relayingToFinalDestination = i === firstPos; // if last position
|
||||
|
||||
if (relayingToFinalDestination && finalRelayOptions) {
|
||||
dest = {
|
||||
host: finalRelayOptions.host,
|
||||
target: '/loki/v1/lsrpc',
|
||||
method: 'POST',
|
||||
};
|
||||
} else {
|
||||
// set x25519 if destination snode
|
||||
let pubkeyHex = targetED25519Hex; // relayingToFinalDestination
|
||||
// or ed25519 snode destination
|
||||
if (!relayingToFinalDestination) {
|
||||
pubkeyHex = nodePath[i + 1].pubkey_ed25519;
|
||||
if (!pubkeyHex) {
|
||||
log.error(
|
||||
`loki_rpc:::makeOnionRequest ${id} - no ed25519 for`,
|
||||
nodePath[i + 1],
|
||||
'path node',
|
||||
i + 1
|
||||
);
|
||||
}
|
||||
}
|
||||
// destination takes a hex key
|
||||
dest = {
|
||||
destination: pubkeyHex,
|
||||
};
|
||||
}
|
||||
try {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const ctx = await encryptForRelay(
|
||||
nodePath[i].pubkey_x25519,
|
||||
dest,
|
||||
ctxes[ctxes.length - 1]
|
||||
);
|
||||
ctxes.push(ctx);
|
||||
} catch (e) {
|
||||
log.error(
|
||||
`loki_rpc:::makeOnionRequest ${id} - encryptForRelay failure`,
|
||||
e.code,
|
||||
e.message
|
||||
);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
const guardCtx = ctxes[ctxes.length - 1]; // last ctx
|
||||
|
||||
const payloadObj = makeGuardPayload(guardCtx);
|
||||
|
||||
// all these requests should use AesGcm
|
||||
return payloadObj;
|
||||
};
|
||||
|
||||
// finalDestOptions is an object
|
||||
// FIXME: internally track reqIdx, not externally
|
||||
const sendOnionRequest = async (
|
||||
reqIdx,
|
||||
nodePath,
|
||||
destX25519Any,
|
||||
finalDestOptions,
|
||||
finalRelayOptions = false,
|
||||
lsrpcIdx
|
||||
) => {
|
||||
if (!destX25519Any) {
|
||||
log.error('loki_rpc::sendOnionRequest - no destX25519Any given');
|
||||
return {};
|
||||
}
|
||||
|
||||
// loki-storage may need this to function correctly
|
||||
// but ADN calls will not always have a body
|
||||
/*
|
||||
if (!finalDestOptions.body) {
|
||||
finalDestOptions.body = '';
|
||||
}
|
||||
*/
|
||||
|
||||
let id = '';
|
||||
if (lsrpcIdx !== undefined) {
|
||||
id += `${lsrpcIdx}=>`;
|
||||
}
|
||||
if (reqIdx !== undefined) {
|
||||
id += `${reqIdx}`;
|
||||
}
|
||||
|
||||
// get destination pubkey in array buffer format
|
||||
let destX25519hex = destX25519Any;
|
||||
if (typeof destX25519hex !== 'string') {
|
||||
// convert AB to hex
|
||||
destX25519hex = StringView.arrayBufferToHex(destX25519Any);
|
||||
}
|
||||
|
||||
// safely build destination
|
||||
let targetEd25519hex;
|
||||
if (finalDestOptions) {
|
||||
if (finalDestOptions.destination_ed25519_hex) {
|
||||
// snode destination
|
||||
targetEd25519hex = finalDestOptions.destination_ed25519_hex;
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
delete finalDestOptions.destination_ed25519_hex;
|
||||
}
|
||||
// else it's lsrpc...
|
||||
} else {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
finalDestOptions = {};
|
||||
log.warn(`loki_rpc::sendOnionRequest ${id} - no finalDestOptions`);
|
||||
return {};
|
||||
}
|
||||
|
||||
const options = finalDestOptions; // lint
|
||||
// do we need this?
|
||||
if (options.headers === undefined) {
|
||||
options.headers = '';
|
||||
}
|
||||
|
||||
let destCtx;
|
||||
try {
|
||||
destCtx = await encryptForPubKey(destX25519hex, options);
|
||||
} catch (e) {
|
||||
log.error(
|
||||
`loki_rpc::sendOnionRequest ${id} - encryptForPubKey failure [`,
|
||||
e.code,
|
||||
e.message,
|
||||
'] destination X25519',
|
||||
destX25519hex.substr(0, 32),
|
||||
'...',
|
||||
destX25519hex.substr(32),
|
||||
'options',
|
||||
options
|
||||
);
|
||||
throw e;
|
||||
}
|
||||
|
||||
const payloadObj = await makeOnionRequest(
|
||||
nodePath,
|
||||
destCtx,
|
||||
targetEd25519hex,
|
||||
finalRelayOptions,
|
||||
id
|
||||
);
|
||||
|
||||
const guardFetchOptions = {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(payload),
|
||||
body: JSON.stringify(payloadObj),
|
||||
// we are talking to a snode...
|
||||
agent: snodeHttpsAgent,
|
||||
};
|
||||
|
||||
const url = `https://${nodePath[0].ip}:${nodePath[0].port}/onion_req`;
|
||||
const guardUrl = `https://${nodePath[0].ip}:${nodePath[0].port}/onion_req`;
|
||||
const response = await nodeFetch(guardUrl, guardFetchOptions);
|
||||
|
||||
// we only proxy to snodes...
|
||||
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
|
||||
const response = await nodeFetch(url, fetchOptions);
|
||||
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '1';
|
||||
|
||||
return processOnionResponse(reqIdx, response, ctxes[0].symmetricKey, true);
|
||||
return processOnionResponse(reqIdx, response, destCtx.symmetricKey, true);
|
||||
};
|
||||
|
||||
const sendOnionRequestSnodeDest = async (
|
||||
reqIdx,
|
||||
nodePath,
|
||||
targetNode,
|
||||
plaintext
|
||||
) =>
|
||||
sendOnionRequest(reqIdx, nodePath, targetNode.pubkey_x25519, {
|
||||
destination_ed25519_hex: targetNode.pubkey_ed25519,
|
||||
body: plaintext,
|
||||
});
|
||||
|
||||
// need relay node's pubkey_x25519_hex
|
||||
// always the same target: /loki/v1/lsrpc
|
||||
const sendOnionRequestLsrpcDest = async (
|
||||
reqIdx,
|
||||
nodePath,
|
||||
destX25519Any,
|
||||
host,
|
||||
payloadObj,
|
||||
lsrpcIdx = 0
|
||||
) =>
|
||||
sendOnionRequest(
|
||||
reqIdx,
|
||||
nodePath,
|
||||
destX25519Any,
|
||||
payloadObj,
|
||||
{ host },
|
||||
lsrpcIdx
|
||||
);
|
||||
|
||||
const BAD_PATH = 'bad_path';
|
||||
|
||||
// Process a response as it arrives from `nodeFetch`, handling
|
||||
// http errors and attempting to decrypt the body with `sharedKey`
|
||||
const processOnionResponse = async (reqIdx, response, sharedKey, useAesGcm) => {
|
||||
// May return false BAD_PATH, indicating that we should try a new path.
|
||||
const processOnionResponse = async (
|
||||
reqIdx,
|
||||
response,
|
||||
sharedKey,
|
||||
useAesGcm,
|
||||
debug
|
||||
) => {
|
||||
// FIXME: 401/500 handling?
|
||||
|
||||
// detect SNode is not ready (not in swarm; not done syncing)
|
||||
|
@ -115,16 +284,24 @@ const processOnionResponse = async (reqIdx, response, sharedKey, useAesGcm) => {
|
|||
|
||||
if (response.status !== 200) {
|
||||
log.warn(
|
||||
`(${reqIdx}) [path] fetch unhandled error code: ${response.status}`
|
||||
`(${reqIdx}) [path] lokiRpc::processOnionResponse - fetch unhandled error code: ${response.status}`
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
const ciphertext = await response.text();
|
||||
if (!ciphertext) {
|
||||
log.warn(`(${reqIdx}) [path]: Target node return empty ciphertext`);
|
||||
log.warn(
|
||||
`(${reqIdx}) [path] lokiRpc::processOnionResponse - Target node return empty ciphertext`
|
||||
);
|
||||
return false;
|
||||
}
|
||||
if (debug) {
|
||||
log.debug(
|
||||
`(${reqIdx}) [path] lokiRpc::processOnionResponse - ciphertext`,
|
||||
ciphertext
|
||||
);
|
||||
}
|
||||
|
||||
let plaintext;
|
||||
let ciphertextBuffer;
|
||||
|
@ -134,22 +311,52 @@ const processOnionResponse = async (reqIdx, response, sharedKey, useAesGcm) => {
|
|||
'base64'
|
||||
).toArrayBuffer();
|
||||
|
||||
const decryptFn = useAesGcm
|
||||
? window.libloki.crypto.DecryptGCM
|
||||
: window.libloki.crypto.DHDecrypt;
|
||||
if (debug) {
|
||||
log.debug(
|
||||
`(${reqIdx}) [path] lokiRpc::processOnionResponse - ciphertextBuffer`,
|
||||
StringView.arrayBufferToHex(ciphertextBuffer),
|
||||
'useAesGcm',
|
||||
useAesGcm
|
||||
);
|
||||
}
|
||||
|
||||
const plaintextBuffer = await decryptFn(sharedKey, ciphertextBuffer);
|
||||
const decryptFn = useAesGcm
|
||||
? libloki.crypto.DecryptGCM
|
||||
: libloki.crypto.DHDecrypt;
|
||||
|
||||
const plaintextBuffer = await decryptFn(sharedKey, ciphertextBuffer, debug);
|
||||
if (debug) {
|
||||
log.debug(
|
||||
'lokiRpc::processOnionResponse - plaintextBuffer',
|
||||
plaintextBuffer.toString()
|
||||
);
|
||||
}
|
||||
|
||||
const textDecoder = new TextDecoder();
|
||||
plaintext = textDecoder.decode(plaintextBuffer);
|
||||
} catch (e) {
|
||||
log.error(`(${reqIdx}) [path] decode error`);
|
||||
log.error(
|
||||
`(${reqIdx}) [path] lokiRpc::processOnionResponse - decode error`,
|
||||
e.code,
|
||||
e.message
|
||||
);
|
||||
log.error(
|
||||
`(${reqIdx}) [path] lokiRpc::processOnionResponse - symKey`,
|
||||
StringView.arrayBufferToHex(sharedKey)
|
||||
);
|
||||
if (ciphertextBuffer) {
|
||||
log.error(`(${reqIdx}) [path] ciphertextBuffer`, ciphertextBuffer);
|
||||
log.error(
|
||||
`(${reqIdx}) [path] lokiRpc::processOnionResponse - ciphertextBuffer`,
|
||||
StringView.arrayBufferToHex(ciphertextBuffer)
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (debug) {
|
||||
log.debug('lokiRpc::processOnionResponse - plaintext', plaintext);
|
||||
}
|
||||
|
||||
try {
|
||||
const jsonRes = JSON.parse(plaintext);
|
||||
// emulate nodeFetch response...
|
||||
|
@ -158,13 +365,22 @@ const processOnionResponse = async (reqIdx, response, sharedKey, useAesGcm) => {
|
|||
const res = JSON.parse(jsonRes.body);
|
||||
return res;
|
||||
} catch (e) {
|
||||
log.error(`(${reqIdx}) [path] parse error json: `, jsonRes.body);
|
||||
log.error(
|
||||
`(${reqIdx}) [path] lokiRpc::processOnionResponse - parse error inner json: `,
|
||||
jsonRes.body
|
||||
);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
return jsonRes;
|
||||
} catch (e) {
|
||||
log.error('[path] parse error', e.code, e.message, `json:`, plaintext);
|
||||
log.error(
|
||||
`(${reqIdx}) [path] lokiRpc::processOnionResponse - parse error outer json`,
|
||||
e.code,
|
||||
e.message,
|
||||
`json:`,
|
||||
plaintext
|
||||
);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
@ -206,7 +422,7 @@ const sendToProxy = async (options = {}, targetNode, retryNumber = 0) => {
|
|||
|
||||
const snPubkeyHex = StringView.hexToArrayBuffer(targetNode.pubkey_x25519);
|
||||
|
||||
const myKeys = await window.libloki.crypto.generateEphemeralKeyPair();
|
||||
const myKeys = await libloki.crypto.generateEphemeralKeyPair();
|
||||
|
||||
const symmetricKey = await libsignal.Curve.async.calculateAgreement(
|
||||
snPubkeyHex,
|
||||
|
@ -217,7 +433,7 @@ const sendToProxy = async (options = {}, targetNode, retryNumber = 0) => {
|
|||
const body = JSON.stringify(options);
|
||||
|
||||
const plainText = textEncoder.encode(body);
|
||||
const ivAndCiphertext = await window.libloki.crypto.DHEncrypt(
|
||||
const ivAndCiphertext = await libloki.crypto.DHEncrypt(
|
||||
symmetricKey,
|
||||
plainText
|
||||
);
|
||||
|
@ -277,6 +493,7 @@ const sendToProxy = async (options = {}, targetNode, retryNumber = 0) => {
|
|||
// grab a fresh random one
|
||||
return sendToProxy(options, targetNode, pRetryNumber);
|
||||
}
|
||||
// 502 is "Next node not found"
|
||||
|
||||
// detect SNode is not ready (not in swarm; not done syncing)
|
||||
// 503 can be proxy target or destination in pre 2.0.3
|
||||
|
@ -356,7 +573,7 @@ const sendToProxy = async (options = {}, targetNode, retryNumber = 0) => {
|
|||
'base64'
|
||||
).toArrayBuffer();
|
||||
|
||||
const plaintextBuffer = await window.libloki.crypto.DHDecrypt(
|
||||
const plaintextBuffer = await libloki.crypto.DHDecrypt(
|
||||
symmetricKey,
|
||||
ciphertextBuffer
|
||||
);
|
||||
|
@ -448,6 +665,7 @@ const lokiFetch = async (url, options = {}, targetNode = null) => {
|
|||
// Wrong PoW difficulty
|
||||
if (response.status === 432) {
|
||||
const result = await response.json();
|
||||
log.error(`lokirpc:::lokiFetch ${type} - WRONG POW`, result);
|
||||
throw new textsecure.WrongDifficultyError(result.difficulty);
|
||||
}
|
||||
|
||||
|
@ -468,11 +686,10 @@ const lokiFetch = async (url, options = {}, targetNode = null) => {
|
|||
// Get a path excluding `targetNode`:
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const path = await lokiSnodeAPI.getOnionPath(targetNode);
|
||||
const thisIdx = onionReqIdx;
|
||||
onionReqIdx += 1;
|
||||
const thisIdx = window.lokiSnodeAPI.assignOnionRequestNumber();
|
||||
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const result = await sendOnionRequest(
|
||||
const result = await sendOnionRequestSnodeDest(
|
||||
thisIdx,
|
||||
path,
|
||||
targetNode,
|
||||
|
@ -628,4 +845,5 @@ const lokiRpc = (
|
|||
|
||||
module.exports = {
|
||||
lokiRpc,
|
||||
sendOnionRequestLsrpcDest,
|
||||
};
|
||||
|
|
|
@ -5,7 +5,8 @@
|
|||
dcodeIO,
|
||||
libloki,
|
||||
log,
|
||||
crypto
|
||||
crypto,
|
||||
textsecure
|
||||
*/
|
||||
|
||||
/* eslint-disable more/no-then */
|
||||
|
@ -39,9 +40,7 @@ async function saveSenderKeysInner(
|
|||
}
|
||||
|
||||
// Save somebody else's key
|
||||
async function saveSenderKeys(groupId, senderIdentity, chainKey) {
|
||||
// New key, so index 0
|
||||
const keyIdx = 0;
|
||||
async function saveSenderKeys(groupId, senderIdentity, chainKey, keyIdx) {
|
||||
const messageKeys = {};
|
||||
await saveSenderKeysInner(
|
||||
groupId,
|
||||
|
@ -133,7 +132,7 @@ async function advanceRatchet(groupId, senderIdentity, idx) {
|
|||
log.error(
|
||||
`Could not find ratchet for groupId ${groupId} sender: ${senderIdentity}`
|
||||
);
|
||||
return null;
|
||||
throw new textsecure.SenderKeyMissing(senderIdentity);
|
||||
}
|
||||
|
||||
// Normally keyIdx will be 1 behind, in which case we stepRatchet one time only
|
||||
|
@ -178,7 +177,10 @@ async function advanceRatchet(groupId, senderIdentity, idx) {
|
|||
curMessageKey = messageKey;
|
||||
break;
|
||||
} else if (nextKeyIdx > idx) {
|
||||
log.error('Developer error: nextKeyIdx > idx');
|
||||
log.error(
|
||||
`Could not decrypt for an older ratchet step: (${nextKeyIdx})nextKeyIdx > (${idx})idx`
|
||||
);
|
||||
throw new Error(`Cannot revert ratchet for group ${groupId}!`);
|
||||
} else {
|
||||
// Store keys for skipped nextKeyIdx, we might need them to decrypt
|
||||
// messages that arrive out-of-order
|
||||
|
@ -289,9 +291,16 @@ async function encryptWithSenderKeyInner(plaintext, groupId, ourIdentity) {
|
|||
return { ciphertext, keyIdx };
|
||||
}
|
||||
|
||||
async function getSenderKeys(groupId, senderIdentity) {
|
||||
const { chainKey, keyIdx } = await loadChainKey(groupId, senderIdentity);
|
||||
|
||||
return { chainKey, keyIdx };
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
createSenderKeyForGroup,
|
||||
encryptWithSenderKey,
|
||||
decryptWithSenderKey,
|
||||
saveSenderKeys,
|
||||
getSenderKeys,
|
||||
};
|
||||
|
|
|
@ -17,14 +17,24 @@ const snodeHttpsAgent = new https.Agent({
|
|||
const RANDOM_SNODES_TO_USE_FOR_PUBKEY_SWARM = 3;
|
||||
const SEED_NODE_RETRIES = 3;
|
||||
const SNODE_VERSION_RETRIES = 3;
|
||||
const MIN_GUARD_COUNT = 2;
|
||||
|
||||
const compareSnodes = (current, search) =>
|
||||
current.pubkey_ed25519 === search.pubkey_ed25519;
|
||||
|
||||
// just get the filtered list
|
||||
async function tryGetSnodeListFromLokidSeednode(
|
||||
seedNodes = [...window.seedNodeList]
|
||||
seedNodes = window.seedNodeList
|
||||
) {
|
||||
if (!seedNodes.length) {
|
||||
log.error(
|
||||
`loki_snodes:::tryGetSnodeListFromLokidSeednode - no seedNodes given`,
|
||||
seedNodes,
|
||||
'window',
|
||||
window.seedNodeList
|
||||
);
|
||||
return [];
|
||||
}
|
||||
// Removed limit until there is a way to get snode info
|
||||
// for individual nodes (needed for guard nodes); this way
|
||||
// we get all active nodes
|
||||
|
@ -42,6 +52,13 @@ async function tryGetSnodeListFromLokidSeednode(
|
|||
Math.floor(Math.random() * seedNodes.length),
|
||||
1
|
||||
)[0];
|
||||
if (!seedNode) {
|
||||
log.error(
|
||||
`loki_snodes:::tryGetSnodeListFromLokidSeednode - seedNode selection failure - seedNodes`,
|
||||
seedNodes
|
||||
);
|
||||
return [];
|
||||
}
|
||||
let snodes = [];
|
||||
try {
|
||||
const getSnodesFromSeedUrl = async urlObj => {
|
||||
|
@ -53,6 +70,30 @@ async function tryGetSnodeListFromLokidSeednode(
|
|||
{}, // Options
|
||||
'/json_rpc' // Seed request endpoint
|
||||
);
|
||||
if (!response) {
|
||||
log.error(
|
||||
`loki_snodes:::tryGetSnodeListFromLokidSeednode - invalid response from seed ${urlObj.toString()}:`,
|
||||
response
|
||||
);
|
||||
return [];
|
||||
}
|
||||
|
||||
// should we try to JSON.parse this?
|
||||
if (typeof response === 'string') {
|
||||
log.error(
|
||||
`loki_snodes:::tryGetSnodeListFromLokidSeednode - invalid string response from seed ${urlObj.toString()}:`,
|
||||
response
|
||||
);
|
||||
return [];
|
||||
}
|
||||
|
||||
if (!response.result) {
|
||||
log.error(
|
||||
`loki_snodes:::tryGetSnodeListFromLokidSeednode - invalid result from seed ${urlObj.toString()}:`,
|
||||
response
|
||||
);
|
||||
return [];
|
||||
}
|
||||
// Filter 0.0.0.0 nodes which haven't submitted uptime proofs
|
||||
return response.result.service_node_states.filter(
|
||||
snode => snode.public_ip !== '0.0.0.0'
|
||||
|
@ -72,6 +113,11 @@ async function tryGetSnodeListFromLokidSeednode(
|
|||
);
|
||||
}
|
||||
}
|
||||
if (snodes.length) {
|
||||
log.info(
|
||||
`loki_snodes:::tryGetSnodeListFromLokidSeednode - got ${snodes.length} service nodes from seed`
|
||||
);
|
||||
}
|
||||
return snodes;
|
||||
} catch (e) {
|
||||
log.warn(
|
||||
|
@ -87,9 +133,18 @@ async function tryGetSnodeListFromLokidSeednode(
|
|||
}
|
||||
|
||||
async function getSnodeListFromLokidSeednode(
|
||||
seedNodes = [...window.seedNodeList],
|
||||
seedNodes = window.seedNodeList,
|
||||
retries = 0
|
||||
) {
|
||||
if (!seedNodes.length) {
|
||||
log.error(
|
||||
`loki_snodes:::getSnodeListFromLokidSeednode - no seedNodes given`,
|
||||
seedNodes,
|
||||
'window',
|
||||
window.seedNodeList
|
||||
);
|
||||
return [];
|
||||
}
|
||||
let snodes = [];
|
||||
try {
|
||||
snodes = await tryGetSnodeListFromLokidSeednode(seedNodes);
|
||||
|
@ -129,6 +184,12 @@ class LokiSnodeAPI {
|
|||
|
||||
this.onionPaths = [];
|
||||
this.guardNodes = [];
|
||||
this.onionRequestCounter = 0; // Request index for debugging
|
||||
}
|
||||
|
||||
assignOnionRequestNumber() {
|
||||
this.onionRequestCounter += 1;
|
||||
return this.onionRequestCounter;
|
||||
}
|
||||
|
||||
async getRandomSnodePool() {
|
||||
|
@ -202,7 +263,7 @@ class LokiSnodeAPI {
|
|||
// FIXME: handle rejections
|
||||
let nodePool = await this.getRandomSnodePool();
|
||||
if (nodePool.length === 0) {
|
||||
log.error(`Could not select guarn nodes: node pool is empty`);
|
||||
log.error(`Could not select guard nodes: node pool is empty`);
|
||||
return [];
|
||||
}
|
||||
|
||||
|
@ -211,16 +272,17 @@ class LokiSnodeAPI {
|
|||
let guardNodes = [];
|
||||
|
||||
const DESIRED_GUARD_COUNT = 3;
|
||||
|
||||
if (shuffled.length < DESIRED_GUARD_COUNT) {
|
||||
log.error(
|
||||
`Could not select guarn nodes: node pool is not big enough, pool size ${shuffled.length}, need ${DESIRED_GUARD_COUNT}, attempting to refresh randomPool`
|
||||
`Could not select guard nodes: node pool is not big enough, pool size ${shuffled.length}, need ${DESIRED_GUARD_COUNT}, attempting to refresh randomPool`
|
||||
);
|
||||
await this.refreshRandomPool();
|
||||
nodePool = await this.getRandomSnodePool();
|
||||
shuffled = _.shuffle(nodePool);
|
||||
if (shuffled.length < DESIRED_GUARD_COUNT) {
|
||||
log.error(
|
||||
`Could not select guarn nodes: node pool is not big enough, pool size ${shuffled.length}, need ${DESIRED_GUARD_COUNT}, failing...`
|
||||
`Could not select guard nodes: node pool is not big enough, pool size ${shuffled.length}, need ${DESIRED_GUARD_COUNT}, failing...`
|
||||
);
|
||||
return [];
|
||||
}
|
||||
|
@ -269,17 +331,20 @@ class LokiSnodeAPI {
|
|||
|
||||
const goodPaths = this.onionPaths.filter(x => !x.bad);
|
||||
|
||||
if (goodPaths.length < 2) {
|
||||
if (goodPaths.length < MIN_GUARD_COUNT) {
|
||||
log.error(
|
||||
`Must have at least 2 good onion paths, actual: ${goodPaths.length}`
|
||||
);
|
||||
await this.buildNewOnionPaths();
|
||||
// should we add a delay? buildNewOnionPaths should act as one
|
||||
// reload goodPaths now
|
||||
return this.getOnionPath(toExclude);
|
||||
}
|
||||
|
||||
const paths = _.shuffle(goodPaths);
|
||||
|
||||
if (!toExclude) {
|
||||
return paths[0];
|
||||
return paths[0].path;
|
||||
}
|
||||
|
||||
// Select a path that doesn't contain `toExclude`
|
||||
|
@ -290,6 +355,19 @@ class LokiSnodeAPI {
|
|||
|
||||
if (otherPaths.length === 0) {
|
||||
// This should never happen!
|
||||
// well it did happen, should we
|
||||
// await this.buildNewOnionPaths();
|
||||
// and restart call?
|
||||
log.error(
|
||||
`loki_snode_api::getOnionPath - no paths without`,
|
||||
toExclude.pubkey_ed25519,
|
||||
'path count',
|
||||
paths.length,
|
||||
'goodPath count',
|
||||
goodPaths.length,
|
||||
'paths',
|
||||
paths
|
||||
);
|
||||
throw new Error('No onion paths available after filtering');
|
||||
}
|
||||
|
||||
|
@ -305,6 +383,7 @@ class LokiSnodeAPI {
|
|||
});
|
||||
}
|
||||
|
||||
// Does this get called multiple times on startup??
|
||||
async buildNewOnionPaths() {
|
||||
// Note: this function may be called concurrently, so
|
||||
// might consider blocking the other calls
|
||||
|
@ -336,7 +415,8 @@ class LokiSnodeAPI {
|
|||
}
|
||||
|
||||
// If guard nodes is still empty (the old nodes are now invalid), select new ones:
|
||||
if (this.guardNodes.length === 0) {
|
||||
if (this.guardNodes.length < MIN_GUARD_COUNT) {
|
||||
// TODO: don't throw away potentially good guard nodes
|
||||
this.guardNodes = await this.selectGuardNodes();
|
||||
}
|
||||
}
|
||||
|
@ -553,7 +633,17 @@ class LokiSnodeAPI {
|
|||
);
|
||||
}
|
||||
|
||||
async refreshRandomPool(seedNodes = [...window.seedNodeList]) {
|
||||
async refreshRandomPool(seedNodes = window.seedNodeList) {
|
||||
if (!seedNodes.length) {
|
||||
if (!window.seedNodeList || !window.seedNodeList.length) {
|
||||
log.error(
|
||||
`loki_snodes:::refreshRandomPool - seedNodeList has not been loaded yet`
|
||||
);
|
||||
return [];
|
||||
}
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
seedNodes = window.seedNodeList;
|
||||
}
|
||||
return primitives.allowOnlyOneAtATime('refreshRandomPool', async () => {
|
||||
// are we running any _getAllVerionsForRandomSnodePool
|
||||
if (this.stopGetAllVersionPromiseControl !== false) {
|
||||
|
@ -750,73 +840,122 @@ class LokiSnodeAPI {
|
|||
}
|
||||
}
|
||||
|
||||
async getLnsMapping(lnsName) {
|
||||
async getLnsMapping(lnsName, timeout) {
|
||||
// Returns { pubkey, error }
|
||||
// pubkey is
|
||||
// undefined when unconfirmed or no mapping found
|
||||
// string when found
|
||||
// timeout parameter optional (ms)
|
||||
|
||||
// How many nodes to fetch data from?
|
||||
const numRequests = 5;
|
||||
|
||||
// How many nodes must have the same response value?
|
||||
const numRequiredConfirms = 3;
|
||||
|
||||
let ciphertextHex;
|
||||
let pubkey;
|
||||
let error;
|
||||
|
||||
const _ = window.Lodash;
|
||||
|
||||
const input = Buffer.from(lnsName);
|
||||
|
||||
const output = await window.blake2b(input);
|
||||
|
||||
const nameHash = dcodeIO.ByteBuffer.wrap(output).toString('base64');
|
||||
|
||||
// Timeouts
|
||||
const maxTimeoutVal = 2 ** 31 - 1;
|
||||
const timeoutPromise = () =>
|
||||
new Promise((_resolve, reject) =>
|
||||
setTimeout(() => reject(), timeout || maxTimeoutVal)
|
||||
);
|
||||
|
||||
// Get nodes capable of doing LNS
|
||||
const lnsNodes = this.getNodesMinVersion('2.0.3');
|
||||
// randomPool should already be shuffled
|
||||
// lnsNodes = _.shuffle(lnsNodes);
|
||||
|
||||
// Loop until 3 confirmations
|
||||
|
||||
// We don't trust any single node, so we accumulate
|
||||
// answers here and select a dominating answer
|
||||
const allResults = [];
|
||||
let ciphertextHex = null;
|
||||
|
||||
while (!ciphertextHex) {
|
||||
if (lnsNodes.length < 3) {
|
||||
log.error('Not enough nodes for lns lookup');
|
||||
return false;
|
||||
}
|
||||
|
||||
// extract 3 and make requests in parallel
|
||||
const nodes = lnsNodes.splice(0, 3);
|
||||
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const results = await Promise.all(
|
||||
nodes.map(node => this._requestLnsMapping(node, nameHash))
|
||||
);
|
||||
|
||||
results.forEach(res => {
|
||||
if (
|
||||
res &&
|
||||
res.result &&
|
||||
res.result.status === 'OK' &&
|
||||
res.result.entries &&
|
||||
res.result.entries.length > 0
|
||||
) {
|
||||
allResults.push(results[0].result.entries[0].encrypted_value);
|
||||
}
|
||||
});
|
||||
|
||||
const [winner, count] = _.maxBy(
|
||||
_.entries(_.countBy(allResults)),
|
||||
x => x[1]
|
||||
);
|
||||
|
||||
if (count >= 3) {
|
||||
// eslint-disable-next-lint prefer-destructuring
|
||||
ciphertextHex = winner;
|
||||
}
|
||||
}
|
||||
|
||||
const ciphertext = new Uint8Array(
|
||||
StringView.hexToArrayBuffer(ciphertextHex)
|
||||
const lnsNodes = await this.getNodesMinVersion(
|
||||
window.CONSTANTS.LNS_CAPABLE_NODES_VERSION
|
||||
);
|
||||
|
||||
const res = await window.decryptLnsEntry(lnsName, ciphertext);
|
||||
// Enough nodes?
|
||||
if (lnsNodes.length < numRequiredConfirms) {
|
||||
error = { lnsTooFewNodes: window.i18n('lnsTooFewNodes') };
|
||||
return { pubkey, error };
|
||||
}
|
||||
|
||||
const pubkey = StringView.arrayBufferToHex(res);
|
||||
const confirmedNodes = [];
|
||||
|
||||
return pubkey;
|
||||
// Promise is only resolved when a consensus is found
|
||||
let cipherResolve;
|
||||
const cipherPromise = () =>
|
||||
new Promise(resolve => {
|
||||
cipherResolve = resolve;
|
||||
});
|
||||
|
||||
const decryptHex = async cipherHex => {
|
||||
const ciphertext = new Uint8Array(StringView.hexToArrayBuffer(cipherHex));
|
||||
|
||||
const res = await window.decryptLnsEntry(lnsName, ciphertext);
|
||||
const publicKey = StringView.arrayBufferToHex(res);
|
||||
|
||||
return publicKey;
|
||||
};
|
||||
|
||||
const fetchFromNode = async node => {
|
||||
const res = await this._requestLnsMapping(node, nameHash);
|
||||
|
||||
// Do validation
|
||||
if (res && res.result && res.result.status === 'OK') {
|
||||
const hasMapping = res.result.entries && res.result.entries.length > 0;
|
||||
|
||||
const resValue = hasMapping
|
||||
? res.result.entries[0].encrypted_value
|
||||
: null;
|
||||
|
||||
confirmedNodes.push(resValue);
|
||||
|
||||
if (confirmedNodes.length >= numRequiredConfirms) {
|
||||
if (ciphertextHex) {
|
||||
// Result already found, dont worry
|
||||
return;
|
||||
}
|
||||
|
||||
const [winner, count] = _.maxBy(
|
||||
_.entries(_.countBy(confirmedNodes)),
|
||||
x => x[1]
|
||||
);
|
||||
|
||||
if (count >= numRequiredConfirms) {
|
||||
ciphertextHex = winner === String(null) ? null : winner;
|
||||
|
||||
// null represents no LNS mapping
|
||||
if (ciphertextHex === null) {
|
||||
error = { lnsMappingNotFound: window.i18n('lnsMappingNotFound') };
|
||||
}
|
||||
|
||||
cipherResolve({ ciphertextHex });
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const nodes = lnsNodes.splice(0, numRequests);
|
||||
|
||||
// Start fetching from nodes
|
||||
nodes.forEach(node => fetchFromNode(node));
|
||||
|
||||
// Timeouts (optional parameter)
|
||||
// Wait for cipher to be found; race against timeout
|
||||
// eslint-disable-next-line more/no-then
|
||||
await Promise.race([cipherPromise, timeoutPromise].map(f => f()))
|
||||
.then(async () => {
|
||||
if (ciphertextHex !== null) {
|
||||
pubkey = await decryptHex(ciphertextHex);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
error = { lnsLookupTimeout: window.i18n('lnsLookupTimeout') };
|
||||
});
|
||||
|
||||
return { pubkey, error };
|
||||
}
|
||||
|
||||
// get snodes for pubkey from random snode
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
/* global Whisper, i18n, textsecure, _ */
|
||||
/* global Whisper, i18n, textsecure, libloki, _ */
|
||||
|
||||
// eslint-disable-next-line func-names
|
||||
(function() {
|
||||
|
@ -179,7 +179,8 @@
|
|||
this.$el.append(this.dialogView.el);
|
||||
return this;
|
||||
},
|
||||
onSubmit(newMembers) {
|
||||
async onSubmit(newMembers) {
|
||||
const _ = window.Lodash;
|
||||
const ourPK = textsecure.storage.user.getNumber();
|
||||
const allMembers = window.Lodash.concat(newMembers, [ourPK]);
|
||||
|
||||
|
@ -187,11 +188,31 @@
|
|||
const notPresentInOld = allMembers.filter(
|
||||
m => !this.existingMembers.includes(m)
|
||||
);
|
||||
|
||||
const notPresentInNew = this.existingMembers.filter(
|
||||
m => !allMembers.includes(m)
|
||||
);
|
||||
// would be easer with _.xor but for some reason we do not have it
|
||||
const xor = notPresentInNew.concat(notPresentInOld);
|
||||
|
||||
// Filter out all linked devices for cases in which one device
|
||||
// exists in group, but hasn't yet synced with its other devices.
|
||||
const getDevicesForRemoved = async () => {
|
||||
const promises = notPresentInNew.map(member =>
|
||||
libloki.storage.getPairedDevicesFor(member)
|
||||
);
|
||||
const devices = _.flatten(await Promise.all(promises));
|
||||
|
||||
return devices;
|
||||
};
|
||||
|
||||
// Get all devices for notPresentInNew
|
||||
const allDevicesOfMembersToRemove = await getDevicesForRemoved();
|
||||
|
||||
// If any extra devices of removed exist in newMembers, ensure that you filter them
|
||||
const filteredMemberes = allMembers.filter(
|
||||
member => !_.includes(allDevicesOfMembersToRemove, member)
|
||||
);
|
||||
|
||||
const xor = _.xor(notPresentInNew, notPresentInOld);
|
||||
if (xor.length === 0) {
|
||||
window.console.log(
|
||||
'skipping group update: no detected changes in group member list'
|
||||
|
@ -203,7 +224,7 @@
|
|||
window.doUpdateGroup(
|
||||
this.groupId,
|
||||
this.groupName,
|
||||
allMembers,
|
||||
filteredMemberes,
|
||||
this.avatarPath
|
||||
);
|
||||
},
|
||||
|
|
|
@ -21,8 +21,7 @@
|
|||
const debugFlags = DebugFlagsEnum.ALL;
|
||||
|
||||
const debugLogFn = (...args) => {
|
||||
if (true) {
|
||||
// process.env.NODE_ENV.includes('test-integration') ||
|
||||
if (window.lokiFeatureFlags.debugMessageLogs) {
|
||||
window.console.warn(...args);
|
||||
}
|
||||
};
|
||||
|
@ -46,7 +45,7 @@
|
|||
}
|
||||
|
||||
function logContactSync(...args) {
|
||||
if (debugFlags & DebugFlagsEnum.GROUP_CONTACT_MESSAGES) {
|
||||
if (debugFlags & DebugFlagsEnum.CONTACT_SYNC_MESSAGES) {
|
||||
debugLogFn(...args);
|
||||
}
|
||||
}
|
||||
|
@ -90,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({
|
||||
|
@ -157,7 +140,7 @@
|
|||
const unpairingMessage = textsecure.OutgoingMessage.buildUnpairingMessage(
|
||||
pubKey
|
||||
);
|
||||
return unpairingMessage.sendToNumber(pubKey);
|
||||
return unpairingMessage.sendToNumber(pubKey, false);
|
||||
}
|
||||
// Serialise as <Element0.length><Element0><Element1.length><Element1>...
|
||||
// This is an implementation of the reciprocal of contacts_parser.js
|
||||
|
@ -220,6 +203,7 @@
|
|||
});
|
||||
return syncMessage;
|
||||
}
|
||||
|
||||
function createGroupSyncProtoMessage(sessionGroup) {
|
||||
// We are getting a single open group here
|
||||
|
||||
|
@ -296,7 +280,7 @@
|
|||
callback
|
||||
);
|
||||
|
||||
pairingRequestMessage.sendToNumber(recipientPubKey);
|
||||
pairingRequestMessage.sendToNumber(recipientPubKey, false);
|
||||
});
|
||||
return p;
|
||||
}
|
||||
|
@ -346,6 +330,7 @@
|
|||
createContactSyncProtoMessage,
|
||||
createGroupSyncProtoMessage,
|
||||
createOpenGroupsSyncProtoMessage,
|
||||
getPrimaryDevicePubkey,
|
||||
debug,
|
||||
};
|
||||
})();
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -263,6 +263,19 @@
|
|||
}
|
||||
}
|
||||
|
||||
function SenderKeyMissing(senderIdentity) {
|
||||
this.name = 'SenderKeyMissing';
|
||||
this.senderIdentity = senderIdentity;
|
||||
|
||||
Error.call(this, this.name);
|
||||
|
||||
// Maintains proper stack trace, where our error was thrown (only available on V8)
|
||||
// via https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error
|
||||
if (Error.captureStackTrace) {
|
||||
Error.captureStackTrace(this);
|
||||
}
|
||||
}
|
||||
|
||||
window.textsecure.UnregisteredUserError = UnregisteredUserError;
|
||||
window.textsecure.SendMessageNetworkError = SendMessageNetworkError;
|
||||
window.textsecure.IncomingIdentityKeyError = IncomingIdentityKeyError;
|
||||
|
@ -282,4 +295,5 @@
|
|||
window.textsecure.TimestampError = TimestampError;
|
||||
window.textsecure.PublicChatError = PublicChatError;
|
||||
window.textsecure.PublicTokenError = PublicTokenError;
|
||||
window.textsecure.SenderKeyMissing = SenderKeyMissing;
|
||||
})();
|
||||
|
|
|
@ -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`
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -682,7 +682,7 @@ MessageReceiver.prototype.extend({
|
|||
const { senderIdentity } = envelope;
|
||||
|
||||
const {
|
||||
ciphertext: ciphertext2,
|
||||
ciphertext: outerCiphertext,
|
||||
ephemeralKey,
|
||||
} = textsecure.protobuf.MediumGroupContent.decode(ciphertextObj);
|
||||
|
||||
|
@ -692,16 +692,16 @@ MessageReceiver.prototype.extend({
|
|||
'hex'
|
||||
).toArrayBuffer();
|
||||
|
||||
const res = await libloki.crypto.decryptForPubkey(
|
||||
const mediumGroupCiphertext = await libloki.crypto.decryptForPubkey(
|
||||
secretKey,
|
||||
ephemKey,
|
||||
ciphertext2.toArrayBuffer()
|
||||
outerCiphertext.toArrayBuffer()
|
||||
);
|
||||
|
||||
const {
|
||||
ciphertext,
|
||||
keyIdx,
|
||||
} = textsecure.protobuf.MediumGroupCiphertext.decode(res);
|
||||
} = textsecure.protobuf.MediumGroupCiphertext.decode(mediumGroupCiphertext);
|
||||
|
||||
const plaintext = await window.SenderKeyAPI.decryptWithSenderKey(
|
||||
ciphertext.toArrayBuffer(),
|
||||
|
@ -849,6 +849,22 @@ MessageReceiver.prototype.extend({
|
|||
return promise
|
||||
.then(plaintext => this.postDecrypt(envelope, plaintext))
|
||||
.catch(error => {
|
||||
if (error && error instanceof textsecure.SenderKeyMissing) {
|
||||
const groupId = envelope.source;
|
||||
const { senderIdentity } = error;
|
||||
|
||||
log.info(
|
||||
'Requesting missing key for identity: ',
|
||||
senderIdentity,
|
||||
'groupId: ',
|
||||
groupId
|
||||
);
|
||||
|
||||
textsecure.messaging.requestSenderKeys(senderIdentity, groupId);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
let errorToThrow = error;
|
||||
|
||||
const noSession =
|
||||
|
@ -876,7 +892,7 @@ MessageReceiver.prototype.extend({
|
|||
ev.confirm = this.removeFromCache.bind(this, envelope);
|
||||
|
||||
const returnError = () => Promise.reject(errorToThrow);
|
||||
return this.dispatchAndWait(ev).then(returnError, returnError);
|
||||
this.dispatchAndWait(ev).then(returnError, returnError);
|
||||
});
|
||||
},
|
||||
async decryptPreKeyWhisperMessage(ciphertext, sessionCipher, address) {
|
||||
|
@ -900,7 +916,7 @@ MessageReceiver.prototype.extend({
|
|||
},
|
||||
// handle a SYNC message for a message
|
||||
// sent by another device
|
||||
handleSentMessage(envelope, sentContainer, msg) {
|
||||
async handleSentMessage(envelope, sentContainer, msg) {
|
||||
const {
|
||||
destination,
|
||||
timestamp,
|
||||
|
@ -908,41 +924,64 @@ MessageReceiver.prototype.extend({
|
|||
unidentifiedStatus,
|
||||
} = sentContainer;
|
||||
|
||||
let p = Promise.resolve();
|
||||
// eslint-disable-next-line no-bitwise
|
||||
if (msg.flags & textsecure.protobuf.DataMessage.Flags.END_SESSION) {
|
||||
p = this.handleEndSession(destination);
|
||||
await this.handleEndSession(destination);
|
||||
}
|
||||
return p.then(() =>
|
||||
this.processDecrypted(envelope, msg).then(message => {
|
||||
const primaryDevicePubKey = window.storage.get('primaryDevicePubKey');
|
||||
|
||||
// handle profileKey and avatar updates
|
||||
if (envelope.source === primaryDevicePubKey) {
|
||||
const { profileKey, profile } = message;
|
||||
const primaryConversation = ConversationController.get(
|
||||
primaryDevicePubKey
|
||||
);
|
||||
if (profile) {
|
||||
this.updateProfile(primaryConversation, profile, profileKey);
|
||||
}
|
||||
}
|
||||
// if (msg.mediumGroupUpdate) {
|
||||
// await this.handleMediumGroupUpdate(envelope, msg.mediumGroupUpdate);
|
||||
// return;
|
||||
// }
|
||||
|
||||
const ev = new Event('sent');
|
||||
ev.confirm = this.removeFromCache.bind(this, envelope);
|
||||
ev.data = {
|
||||
destination,
|
||||
timestamp: timestamp.toNumber(),
|
||||
device: envelope.sourceDevice,
|
||||
unidentifiedStatus,
|
||||
message,
|
||||
};
|
||||
if (expirationStartTimestamp) {
|
||||
ev.data.expirationStartTimestamp = expirationStartTimestamp.toNumber();
|
||||
}
|
||||
return this.dispatchAndWait(ev);
|
||||
})
|
||||
);
|
||||
const message = await this.processDecrypted(envelope, msg);
|
||||
|
||||
const primaryDevicePubKey = window.storage.get('primaryDevicePubKey');
|
||||
// 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) {
|
||||
const { profileKey, profile } = message;
|
||||
const primaryConversation = ConversationController.get(
|
||||
primaryDevicePubKey
|
||||
);
|
||||
if (profile) {
|
||||
this.updateProfile(primaryConversation, profile, profileKey);
|
||||
}
|
||||
}
|
||||
|
||||
const ev = new Event('sent');
|
||||
ev.confirm = this.removeFromCache.bind(this, envelope);
|
||||
ev.data = {
|
||||
destination,
|
||||
timestamp: timestamp.toNumber(),
|
||||
device: envelope.sourceDevice,
|
||||
unidentifiedStatus,
|
||||
message,
|
||||
};
|
||||
if (expirationStartTimestamp) {
|
||||
ev.data.expirationStartTimestamp = expirationStartTimestamp.toNumber();
|
||||
}
|
||||
this.dispatchAndWait(ev);
|
||||
},
|
||||
async handleLokiAddressMessage(envelope) {
|
||||
window.log.warn('Ignoring a Loki address message');
|
||||
|
@ -1163,96 +1202,180 @@ MessageReceiver.prototype.extend({
|
|||
},
|
||||
|
||||
async handleMediumGroupUpdate(envelope, groupUpdate) {
|
||||
const {
|
||||
groupId,
|
||||
groupSecretKey,
|
||||
senderKey,
|
||||
members,
|
||||
groupName,
|
||||
} = groupUpdate;
|
||||
const { type, groupId } = groupUpdate;
|
||||
|
||||
const convoExists = window.ConversationController.get(groupId, 'group');
|
||||
|
||||
if (convoExists) {
|
||||
// If the group already exists, check that `members` is empty,
|
||||
// and if so, it is sender key message
|
||||
|
||||
// TODO: introduce TYPE into this message instead?
|
||||
if (!members || !members.length) {
|
||||
log.info('[sender key] got a new sender key from:', envelope.source);
|
||||
|
||||
// We probably don't need to await here
|
||||
await window.SenderKeyAPI.saveSenderKeys(
|
||||
groupId,
|
||||
envelope.source,
|
||||
senderKey
|
||||
);
|
||||
|
||||
this.removeFromCache(envelope);
|
||||
return;
|
||||
}
|
||||
|
||||
log.error(`Conversation for groupId ${groupId} already exists`);
|
||||
}
|
||||
|
||||
const convo = await window.ConversationController.getOrCreateAndWait(
|
||||
groupId,
|
||||
'group'
|
||||
);
|
||||
convo.set('is_medium_group', true);
|
||||
convo.set('active_at', Date.now());
|
||||
convo.set('name', groupName);
|
||||
|
||||
await window.Signal.Data.createOrUpdateIdentityKey({
|
||||
id: groupId,
|
||||
secretKey: groupSecretKey,
|
||||
});
|
||||
|
||||
// Save sender's key
|
||||
await window.SenderKeyAPI.saveSenderKeys(
|
||||
groupId,
|
||||
envelope.source,
|
||||
senderKey
|
||||
);
|
||||
|
||||
// TODO: Check that we are even a part of this group?
|
||||
const ourIdentity = await textsecure.storage.user.getNumber();
|
||||
const senderIdentity = envelope.source;
|
||||
|
||||
const ownSenderKey = await window.SenderKeyAPI.createSenderKeyForGroup(
|
||||
groupId,
|
||||
ourIdentity
|
||||
);
|
||||
|
||||
{
|
||||
// TODO: Send own key to every member
|
||||
|
||||
const otherMembers = _.without(members, ourIdentity);
|
||||
if (
|
||||
type === textsecure.protobuf.MediumGroupUpdate.Type.SENDER_KEY_REQUEST
|
||||
) {
|
||||
log.debug('[sender key] sender key request from:', senderIdentity);
|
||||
|
||||
const proto = new textsecure.protobuf.DataMessage();
|
||||
|
||||
// We reuse the same message type for sender keys
|
||||
const update = new textsecure.protobuf.MediumGroupUpdate();
|
||||
|
||||
const { chainKey, keyIdx } = await window.SenderKeyAPI.getSenderKeys(
|
||||
groupId,
|
||||
ourIdentity
|
||||
);
|
||||
|
||||
update.type = textsecure.protobuf.MediumGroupUpdate.Type.SENDER_KEY;
|
||||
update.groupId = groupId;
|
||||
update.senderKey = ownSenderKey;
|
||||
update.senderKey = new textsecure.protobuf.SenderKey({
|
||||
chainKey: StringView.arrayBufferToHex(chainKey),
|
||||
keyIdx,
|
||||
});
|
||||
|
||||
proto.mediumGroupUpdate = update;
|
||||
|
||||
// TODO: send to our linked devices too?
|
||||
textsecure.messaging.updateMediumGroup([senderIdentity], proto);
|
||||
|
||||
// Don't need to await here
|
||||
this.removeFromCache(envelope);
|
||||
} else if (type === textsecure.protobuf.MediumGroupUpdate.Type.SENDER_KEY) {
|
||||
const { senderKey } = groupUpdate;
|
||||
|
||||
// TODO: Some of the members might not have a session with us, so
|
||||
// we should send a session request
|
||||
log.debug('[sender key] got a new sender key from:', senderIdentity);
|
||||
|
||||
textsecure.messaging.updateMediumGroup(otherMembers, proto);
|
||||
await window.SenderKeyAPI.saveSenderKeys(
|
||||
groupId,
|
||||
senderIdentity,
|
||||
senderKey.chainKey,
|
||||
senderKey.keyIdx
|
||||
);
|
||||
|
||||
this.removeFromCache(envelope);
|
||||
} else if (type === textsecure.protobuf.MediumGroupUpdate.Type.NEW_GROUP) {
|
||||
const maybeConvo = await window.ConversationController.get(groupId);
|
||||
const groupExists = !!maybeConvo;
|
||||
|
||||
const {
|
||||
members: membersBinary,
|
||||
groupSecretKey,
|
||||
groupName,
|
||||
senderKey,
|
||||
admins,
|
||||
} = groupUpdate;
|
||||
|
||||
const members = membersBinary.map(pk =>
|
||||
StringView.arrayBufferToHex(pk.toArrayBuffer())
|
||||
);
|
||||
|
||||
const convo = groupExists
|
||||
? maybeConvo
|
||||
: await window.ConversationController.getOrCreateAndWait(
|
||||
groupId,
|
||||
'group'
|
||||
);
|
||||
|
||||
{
|
||||
// Add group update message
|
||||
const now = Date.now();
|
||||
const message = convo.messageCollection.add({
|
||||
conversationId: convo.id,
|
||||
type: 'incoming',
|
||||
sent_at: now,
|
||||
received_at: now,
|
||||
group_update: {
|
||||
name: groupName,
|
||||
members,
|
||||
},
|
||||
});
|
||||
|
||||
const messageId = await window.Signal.Data.saveMessage(
|
||||
message.attributes,
|
||||
{
|
||||
Message: Whisper.Message,
|
||||
}
|
||||
);
|
||||
message.set({ id: messageId });
|
||||
}
|
||||
|
||||
if (groupExists) {
|
||||
// ***** Updating the group *****
|
||||
log.info('Received a group update for medium group:', groupId);
|
||||
|
||||
// Check that the sender is admin (make sure it words with multidevice)
|
||||
const isAdmin = convo.get('groupAdmins').includes(senderIdentity);
|
||||
|
||||
if (!isAdmin) {
|
||||
log.warn('Rejected attempt to update a group by non-admin');
|
||||
this.removeFromCache(envelope);
|
||||
return;
|
||||
}
|
||||
|
||||
convo.set('name', groupName);
|
||||
convo.set('members', members);
|
||||
|
||||
// TODO: check that we are still in the group (when we enable deleting members)
|
||||
convo.saveChangesToDB();
|
||||
|
||||
// Update other fields. Add a corresponding "update" message to the conversation
|
||||
} else {
|
||||
// ***** Creating a new group *****
|
||||
log.info('Received a new medium group:', groupId);
|
||||
|
||||
// TODO: Check that we are even a part of this group?
|
||||
|
||||
convo.set('is_medium_group', true);
|
||||
convo.set('active_at', Date.now());
|
||||
convo.set('name', groupName);
|
||||
convo.set('groupAdmins', admins);
|
||||
|
||||
convo.setFriendRequestStatus(
|
||||
window.friends.friendRequestStatusEnum.friends
|
||||
);
|
||||
|
||||
const secretKeyHex = StringView.arrayBufferToHex(
|
||||
groupSecretKey.toArrayBuffer()
|
||||
);
|
||||
|
||||
await window.Signal.Data.createOrUpdateIdentityKey({
|
||||
id: groupId,
|
||||
secretKey: secretKeyHex,
|
||||
});
|
||||
|
||||
// Save sender's key
|
||||
await window.SenderKeyAPI.saveSenderKeys(
|
||||
groupId,
|
||||
envelope.source,
|
||||
senderKey.chainKey,
|
||||
senderKey.keyIdx
|
||||
);
|
||||
|
||||
const ownSenderKey = await window.SenderKeyAPI.createSenderKeyForGroup(
|
||||
groupId,
|
||||
ourIdentity
|
||||
);
|
||||
|
||||
{
|
||||
// Send own key to every member
|
||||
const otherMembers = _.without(members, ourIdentity);
|
||||
|
||||
const proto = new textsecure.protobuf.DataMessage();
|
||||
|
||||
// We reuse the same message type for sender keys
|
||||
const update = new textsecure.protobuf.MediumGroupUpdate();
|
||||
update.type = textsecure.protobuf.MediumGroupUpdate.Type.SENDER_KEY;
|
||||
update.groupId = groupId;
|
||||
update.senderKey = new textsecure.protobuf.SenderKey({
|
||||
chainKey: ownSenderKey,
|
||||
keyIdx: 0,
|
||||
});
|
||||
|
||||
proto.mediumGroupUpdate = update;
|
||||
|
||||
textsecure.messaging.updateMediumGroup(otherMembers, proto);
|
||||
}
|
||||
|
||||
// Subscribe to this group
|
||||
this.pollForAdditionalId(groupId);
|
||||
}
|
||||
|
||||
this.removeFromCache(envelope);
|
||||
}
|
||||
|
||||
// Subscribe to this group
|
||||
this.pollForAdditionalId(groupId);
|
||||
|
||||
// All further messages (maybe rather than 'control' messages) should come to this group's swarm
|
||||
|
||||
this.removeFromCache(envelope);
|
||||
},
|
||||
async handleDataMessage(envelope, msg) {
|
||||
window.log.info('data message from', this.getEnvelopeId(envelope));
|
||||
|
@ -1308,12 +1431,40 @@ MessageReceiver.prototype.extend({
|
|||
!_.isEmpty(message.body) &&
|
||||
friendRequestStatusNoneOrExpired;
|
||||
|
||||
// Build a 'message' event i.e. a received message event
|
||||
const ev = new Event('message');
|
||||
const source = envelope.senderIdentity || senderPubKey;
|
||||
|
||||
const isOwnDevice = async pubkey => {
|
||||
const primaryDevice = window.storage.get('primaryDevicePubKey');
|
||||
const secondaryDevices = await window.libloki.storage.getPairedDevicesFor(
|
||||
primaryDevice
|
||||
);
|
||||
|
||||
const allDevices = [primaryDevice, ...secondaryDevices];
|
||||
return allDevices.includes(pubkey);
|
||||
};
|
||||
|
||||
const ownDevice = await isOwnDevice(source);
|
||||
|
||||
let ev;
|
||||
if (conversation.isMediumGroup() && ownDevice) {
|
||||
// Data messages for medium groups don't arrive as sync messages. Instead,
|
||||
// linked devices poll for group messages independently, thus they need
|
||||
// to recognise some of those messages at their own.
|
||||
ev = new Event('sent');
|
||||
} else {
|
||||
ev = new Event('message');
|
||||
}
|
||||
|
||||
if (envelope.senderIdentity) {
|
||||
message.group = {
|
||||
id: envelope.source,
|
||||
};
|
||||
}
|
||||
|
||||
ev.confirm = this.removeFromCache.bind(this, envelope);
|
||||
ev.data = {
|
||||
friendRequest: isFriendRequest,
|
||||
source: senderPubKey,
|
||||
source,
|
||||
sourceDevice: envelope.sourceDevice,
|
||||
timestamp: envelope.timestamp.toNumber(),
|
||||
receivedAt: envelope.receivedAt,
|
||||
|
@ -1331,6 +1482,7 @@ MessageReceiver.prototype.extend({
|
|||
contact,
|
||||
preview,
|
||||
groupInvitation,
|
||||
mediumGroupUpdate,
|
||||
}) {
|
||||
return (
|
||||
!flags &&
|
||||
|
@ -1340,7 +1492,8 @@ MessageReceiver.prototype.extend({
|
|||
_.isEmpty(quote) &&
|
||||
_.isEmpty(contact) &&
|
||||
_.isEmpty(preview) &&
|
||||
_.isEmpty(groupInvitation)
|
||||
_.isEmpty(groupInvitation) &&
|
||||
_.isEmpty(mediumGroupUpdate)
|
||||
);
|
||||
},
|
||||
handleLegacyMessage(envelope) {
|
||||
|
@ -1523,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,
|
||||
|
@ -1709,6 +1862,7 @@ MessageReceiver.prototype.extend({
|
|||
isBlocked(number) {
|
||||
return textsecure.storage.get('blocked', []).indexOf(number) >= 0;
|
||||
},
|
||||
|
||||
cleanAttachment(attachment) {
|
||||
return {
|
||||
..._.omit(attachment, 'thumbnail'),
|
||||
|
|
|
@ -6,7 +6,6 @@
|
|||
libloki,
|
||||
StringView,
|
||||
lokiMessageAPI,
|
||||
i18n,
|
||||
log
|
||||
*/
|
||||
|
||||
|
@ -167,6 +166,7 @@ function OutgoingMessage(
|
|||
isMediumGroup,
|
||||
publicSendData,
|
||||
debugMessageType,
|
||||
autoSession,
|
||||
} = options || {};
|
||||
this.numberInfo = numberInfo;
|
||||
this.isPublic = isPublic;
|
||||
|
@ -181,6 +181,7 @@ function OutgoingMessage(
|
|||
this.online = online;
|
||||
this.messageType = messageType || 'outgoing';
|
||||
this.debugMessageType = debugMessageType;
|
||||
this.autoSession = autoSession || false;
|
||||
}
|
||||
|
||||
OutgoingMessage.prototype = {
|
||||
|
@ -215,9 +216,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)
|
||||
|
@ -318,6 +327,7 @@ OutgoingMessage.prototype = {
|
|||
// Default ttl to 24 hours if no value provided
|
||||
async transmitMessage(number, data, timestamp, ttl = 24 * 60 * 60 * 1000) {
|
||||
const pubKey = number;
|
||||
|
||||
try {
|
||||
// TODO: Make NUM_CONCURRENT_CONNECTIONS a global constant
|
||||
const options = {
|
||||
|
@ -348,7 +358,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' &&
|
||||
|
@ -369,7 +379,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
|
||||
|
@ -411,27 +421,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();
|
||||
|
@ -486,6 +480,7 @@ OutgoingMessage.prototype = {
|
|||
plaintext,
|
||||
pubKey,
|
||||
isSessionRequest,
|
||||
isFriendRequest,
|
||||
enableFallBackEncryption,
|
||||
} = clearMessage;
|
||||
// Session doesn't use the deviceId scheme, it's always 1.
|
||||
|
@ -531,7 +526,7 @@ OutgoingMessage.prototype = {
|
|||
sourceDevice,
|
||||
content,
|
||||
pubKey,
|
||||
isFriendRequest: enableFallBackEncryption,
|
||||
isFriendRequest,
|
||||
isSessionRequest,
|
||||
};
|
||||
},
|
||||
|
@ -644,6 +639,7 @@ OutgoingMessage.prototype = {
|
|||
this.timestamp,
|
||||
ttl
|
||||
);
|
||||
|
||||
if (!this.isGroup && isFriendRequest && !isSessionRequest) {
|
||||
const conversation = ConversationController.get(destination);
|
||||
if (conversation) {
|
||||
|
@ -657,16 +653,10 @@ OutgoingMessage.prototype = {
|
|||
this.errors.push(e);
|
||||
}
|
||||
});
|
||||
await Promise.all(promises);
|
||||
// TODO: the retrySend should only send to the devices
|
||||
// for which the transmission failed.
|
||||
|
||||
// ensure numberCompleted() will execute the callback
|
||||
this.numbersCompleted += this.errors.length + this.successfulNumbers.length;
|
||||
// Absorb errors if message sent to at least 1 device
|
||||
if (this.successfulNumbers.length > 0) {
|
||||
this.errors = [];
|
||||
}
|
||||
await Promise.all(promises);
|
||||
|
||||
this.numbersCompleted += this.successfulNumbers.length;
|
||||
this.numberCompleted();
|
||||
},
|
||||
async buildAndEncrypt(devicePubKey) {
|
||||
|
@ -705,14 +695,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
|
||||
|
@ -737,14 +727,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
|
||||
|
@ -764,9 +755,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,
|
||||
|
|
|
@ -430,7 +430,7 @@ MessageSender.prototype = {
|
|||
let keysFound = false;
|
||||
// If we don't have a session but we already have prekeys to
|
||||
// start communication then we should use them
|
||||
if (!haveSession && !options.isPublic) {
|
||||
if (!haveSession && !options.isPublic && !options.isMediumGroup) {
|
||||
keysFound = await hasKeys(number);
|
||||
}
|
||||
|
||||
|
@ -460,7 +460,7 @@ MessageSender.prototype = {
|
|||
message.dataMessage.group
|
||||
);
|
||||
// If it was a message to a group then we need to send a session request
|
||||
if (isGroupMessage) {
|
||||
if (isGroupMessage || options.autoSession) {
|
||||
const sessionRequestMessage = textsecure.OutgoingMessage.buildSessionRequestMessage(
|
||||
number
|
||||
);
|
||||
|
@ -666,16 +666,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')
|
||||
);
|
||||
if (sessionContacts.length === 0) {
|
||||
|
||||
// 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
|
||||
);
|
||||
|
||||
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))
|
||||
);
|
||||
|
@ -712,7 +754,11 @@ MessageSender.prototype = {
|
|||
}
|
||||
// We only want to sync across closed groups that we haven't left
|
||||
const sessionGroups = conversations.filter(
|
||||
c => c.isClosedGroup() && !c.get('left') && c.isFriend()
|
||||
c =>
|
||||
c.isClosedGroup() &&
|
||||
!c.get('left') &&
|
||||
c.isFriend() &&
|
||||
!c.isMediumGroup()
|
||||
);
|
||||
if (sessionGroups.length === 0) {
|
||||
window.console.info('No closed group to sync.');
|
||||
|
@ -977,7 +1023,12 @@ MessageSender.prototype = {
|
|||
});
|
||||
},
|
||||
|
||||
sendGroupProto(providedNumbers, proto, timestamp = Date.now(), options = {}) {
|
||||
async sendGroupProto(
|
||||
providedNumbers,
|
||||
proto,
|
||||
timestamp = Date.now(),
|
||||
options = {}
|
||||
) {
|
||||
// We always assume that only primary device is a member in the group
|
||||
const primaryDeviceKey =
|
||||
window.storage.get('primaryDevicePubKey') ||
|
||||
|
@ -1016,12 +1067,13 @@ MessageSender.prototype = {
|
|||
);
|
||||
});
|
||||
|
||||
return sendPromise.then(result => {
|
||||
// Sync the group message to our other devices
|
||||
const encoded = textsecure.protobuf.DataMessage.encode(proto);
|
||||
this.sendSyncMessage(encoded, timestamp, null, null, [], [], options);
|
||||
return result;
|
||||
});
|
||||
const result = await sendPromise;
|
||||
|
||||
// Sync the group message to our other devices
|
||||
const encoded = textsecure.protobuf.DataMessage.encode(proto);
|
||||
this.sendSyncMessage(encoded, timestamp, null, null, [], [], options);
|
||||
|
||||
return result;
|
||||
},
|
||||
|
||||
async getMessageProto(
|
||||
|
@ -1203,12 +1255,18 @@ MessageSender.prototype = {
|
|||
},
|
||||
|
||||
async updateMediumGroup(members, groupUpdateProto) {
|
||||
// Automatically request session if not found (updates use pairwise sessions)
|
||||
const autoSession = true;
|
||||
|
||||
await this.sendGroupProto(members, groupUpdateProto, Date.now(), {
|
||||
isPublic: false,
|
||||
autoSession,
|
||||
});
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
async updateGroup(
|
||||
async sendGroupUpdate(
|
||||
groupId,
|
||||
name,
|
||||
avatar,
|
||||
|
@ -1284,6 +1342,16 @@ MessageSender.prototype = {
|
|||
return this.sendGroupProto(groupNumbers, proto, Date.now(), options);
|
||||
},
|
||||
|
||||
requestSenderKeys(sender, groupId) {
|
||||
const proto = new textsecure.protobuf.DataMessage();
|
||||
const update = new textsecure.protobuf.MediumGroupUpdate();
|
||||
update.type = textsecure.protobuf.MediumGroupUpdate.Type.SENDER_KEY_REQUEST;
|
||||
update.groupId = groupId;
|
||||
proto.mediumGroupUpdate = update;
|
||||
|
||||
textsecure.messaging.updateMediumGroup([sender], proto);
|
||||
},
|
||||
|
||||
leaveGroup(groupId, groupNumbers, options) {
|
||||
const proto = new textsecure.protobuf.DataMessage();
|
||||
proto.group = new textsecure.protobuf.GroupContext();
|
||||
|
@ -1387,12 +1455,13 @@ textsecure.MessageSender = function MessageSenderWrapper(username, password) {
|
|||
this.resetSession = sender.resetSession.bind(sender);
|
||||
this.sendMessageToGroup = sender.sendMessageToGroup.bind(sender);
|
||||
this.sendTypingMessage = sender.sendTypingMessage.bind(sender);
|
||||
this.updateGroup = sender.updateGroup.bind(sender);
|
||||
this.sendGroupUpdate = sender.sendGroupUpdate.bind(sender);
|
||||
this.updateMediumGroup = sender.updateMediumGroup.bind(sender);
|
||||
this.addNumberToGroup = sender.addNumberToGroup.bind(sender);
|
||||
this.setGroupName = sender.setGroupName.bind(sender);
|
||||
this.setGroupAvatar = sender.setGroupAvatar.bind(sender);
|
||||
this.requestGroupInfo = sender.requestGroupInfo.bind(sender);
|
||||
this.requestSenderKeys = sender.requestSenderKeys.bind(sender);
|
||||
this.leaveGroup = sender.leaveGroup.bind(sender);
|
||||
this.sendSyncMessage = sender.sendSyncMessage.bind(sender);
|
||||
this.getProfile = sender.getProfile.bind(sender);
|
||||
|
|
63
main.js
63
main.js
|
@ -206,19 +206,33 @@ function captureClicks(window) {
|
|||
window.webContents.on('new-window', handleUrl);
|
||||
}
|
||||
|
||||
const DEFAULT_WIDTH = 880;
|
||||
// add contact button needs to be visible (on HiDpi screens?)
|
||||
// otherwise integration test fail
|
||||
const DEFAULT_HEIGHT = 820;
|
||||
const MIN_WIDTH = 880;
|
||||
const MIN_HEIGHT = 820;
|
||||
const BOUNDS_BUFFER = 100;
|
||||
const WINDOW_SIZE = Object.freeze({
|
||||
defaultWidth: 880,
|
||||
defaultHeight: 820,
|
||||
minWidth: 880,
|
||||
minHeight: 600,
|
||||
});
|
||||
|
||||
function getWindowSize() {
|
||||
const { screen } = electron;
|
||||
const screenSize = screen.getPrimaryDisplay().workAreaSize;
|
||||
const { minWidth, minHeight, defaultWidth, defaultHeight } = WINDOW_SIZE;
|
||||
// Ensure that the screen can fit within the default size
|
||||
const width = Math.min(defaultWidth, Math.max(minWidth, screenSize.width));
|
||||
const height = Math.min(
|
||||
defaultHeight,
|
||||
Math.max(minHeight, screenSize.height)
|
||||
);
|
||||
|
||||
return { width, height, minWidth, minHeight };
|
||||
}
|
||||
|
||||
function isVisible(window, bounds) {
|
||||
const boundsX = _.get(bounds, 'x') || 0;
|
||||
const boundsY = _.get(bounds, 'y') || 0;
|
||||
const boundsWidth = _.get(bounds, 'width') || DEFAULT_WIDTH;
|
||||
const boundsHeight = _.get(bounds, 'height') || DEFAULT_HEIGHT;
|
||||
const boundsWidth = _.get(bounds, 'width') || WINDOW_SIZE.defaultWidth;
|
||||
const boundsHeight = _.get(bounds, 'height') || WINDOW_SIZE.defaultHeight;
|
||||
const BOUNDS_BUFFER = 100;
|
||||
|
||||
// requiring BOUNDS_BUFFER pixels on the left or right side
|
||||
const rightSideClearOfLeftBound =
|
||||
|
@ -241,13 +255,14 @@ function isVisible(window, bounds) {
|
|||
|
||||
async function createWindow() {
|
||||
const { screen } = electron;
|
||||
const { minWidth, minHeight, width, height } = getWindowSize();
|
||||
const windowOptions = Object.assign(
|
||||
{
|
||||
show: !startInTray, // allow to start minimised in tray
|
||||
width: DEFAULT_WIDTH,
|
||||
height: DEFAULT_HEIGHT,
|
||||
minWidth: MIN_WIDTH,
|
||||
minHeight: MIN_HEIGHT,
|
||||
width,
|
||||
height,
|
||||
minWidth,
|
||||
minHeight,
|
||||
autoHideMenuBar: false,
|
||||
backgroundColor: '#fff',
|
||||
webPreferences: {
|
||||
|
@ -270,11 +285,11 @@ async function createWindow() {
|
|||
])
|
||||
);
|
||||
|
||||
if (!_.isNumber(windowOptions.width) || windowOptions.width < MIN_WIDTH) {
|
||||
windowOptions.width = DEFAULT_WIDTH;
|
||||
if (!_.isNumber(windowOptions.width) || windowOptions.width < minWidth) {
|
||||
windowOptions.width = Math.max(minWidth, width);
|
||||
}
|
||||
if (!_.isNumber(windowOptions.height) || windowOptions.height < MIN_HEIGHT) {
|
||||
windowOptions.height = DEFAULT_HEIGHT;
|
||||
if (!_.isNumber(windowOptions.height) || windowOptions.height < minHeight) {
|
||||
windowOptions.height = Math.max(minHeight, height);
|
||||
}
|
||||
if (!_.isBoolean(windowOptions.maximized)) {
|
||||
delete windowOptions.maximized;
|
||||
|
@ -516,13 +531,13 @@ function showPasswordWindow() {
|
|||
passwordWindow.show();
|
||||
return;
|
||||
}
|
||||
|
||||
const { minWidth, minHeight, width, height } = getWindowSize();
|
||||
const windowOptions = {
|
||||
show: true, // allow to start minimised in tray
|
||||
width: DEFAULT_WIDTH,
|
||||
height: DEFAULT_HEIGHT,
|
||||
minWidth: MIN_WIDTH,
|
||||
minHeight: MIN_HEIGHT,
|
||||
width,
|
||||
height,
|
||||
minWidth,
|
||||
minHeight,
|
||||
autoHideMenuBar: false,
|
||||
webPreferences: {
|
||||
nodeIntegration: false,
|
||||
|
@ -631,8 +646,8 @@ async function showDebugLogWindow() {
|
|||
const theme = await getThemeFromMainWindow();
|
||||
const size = mainWindow.getSize();
|
||||
const options = {
|
||||
width: Math.max(size[0] - 100, MIN_WIDTH),
|
||||
height: Math.max(size[1] - 100, MIN_HEIGHT),
|
||||
width: Math.max(size[0] - 100, WINDOW_SIZE.minWidth),
|
||||
height: Math.max(size[1] - 100, WINDOW_SIZE.minHeight),
|
||||
resizable: false,
|
||||
title: locale.messages.signalDesktopPreferences.message,
|
||||
autoHideMenuBar: true,
|
||||
|
|
15
package.json
15
package.json
|
@ -2,7 +2,7 @@
|
|||
"name": "session-messenger-desktop",
|
||||
"productName": "Session",
|
||||
"description": "Private messaging from your desktop",
|
||||
"version": "1.0.8",
|
||||
"version": "1.0.9",
|
||||
"license": "GPL-3.0",
|
||||
"author": {
|
||||
"name": "Loki Project",
|
||||
|
@ -15,15 +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-prod-multi-2": "cross-env NODE_ENV=production NODE_APP_INSTANCE=devprod2 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-swarm-test-3": "cross-env NODE_ENV=swarm-testing NODE_APP_INSTANCE=3 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",
|
||||
|
@ -40,6 +34,7 @@
|
|||
"test-integration": "ELECTRON_DISABLE_SANDBOX=1 mocha --exit --timeout 10000 integration_test/integration_test.js",
|
||||
"test-node": "mocha --recursive --exit test/app test/modules ts/test libloki/test/node --timeout 10000",
|
||||
"test-session": "mocha --recursive --exit ts/test/session --timeout 10000",
|
||||
"test-medium-groups": "ELECTRON_DISABLE_SANDBOX=1 mocha --exit --timeout 10000 integration_test/integration_test.js --grep 'senderkeys'",
|
||||
"eslint": "eslint --cache .",
|
||||
"eslint-fix": "eslint --fix .",
|
||||
"eslint-full": "eslint .",
|
||||
|
|
79
preload.js
79
preload.js
|
@ -57,6 +57,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) => {
|
||||
|
@ -71,18 +73,31 @@ window.isBeforeVersion = (toCheck, baseVersion) => {
|
|||
}
|
||||
};
|
||||
|
||||
window.CONSTANTS = {
|
||||
MAX_LOGIN_TRIES: 3,
|
||||
MAX_PASSWORD_LENGTH: 64,
|
||||
MAX_USERNAME_LENGTH: 20,
|
||||
MAX_GROUP_NAME_LENGTH: 64,
|
||||
DEFAULT_PUBLIC_CHAT_URL: appConfig.get('defaultPublicChatServer'),
|
||||
MAX_CONNECTION_DURATION: 5000,
|
||||
MAX_MESSAGE_BODY_LENGTH: 64 * 1024,
|
||||
// eslint-disable-next-line func-names
|
||||
window.CONSTANTS = new (function() {
|
||||
this.MAX_LOGIN_TRIES = 3;
|
||||
this.MAX_PASSWORD_LENGTH = 64;
|
||||
this.MAX_USERNAME_LENGTH = 20;
|
||||
this.MAX_GROUP_NAME_LENGTH = 64;
|
||||
this.DEFAULT_PUBLIC_CHAT_URL = appConfig.get('defaultPublicChatServer');
|
||||
this.MAX_CONNECTION_DURATION = 5000;
|
||||
this.MAX_MESSAGE_BODY_LENGTH = 64 * 1024;
|
||||
// Limited due to the proof-of-work requirement
|
||||
SMALL_GROUP_SIZE_LIMIT: 10,
|
||||
NOTIFICATION_ENABLE_TIMEOUT_SECONDS: 10, // number of seconds to turn on notifications after reconnect/start of app
|
||||
};
|
||||
this.SMALL_GROUP_SIZE_LIMIT = 10;
|
||||
// Number of seconds to turn on notifications after reconnect/start of app
|
||||
this.NOTIFICATION_ENABLE_TIMEOUT_SECONDS = 10;
|
||||
this.SESSION_ID_LENGTH = 66;
|
||||
|
||||
// Loki Name System (LNS)
|
||||
this.LNS_DEFAULT_LOOKUP_TIMEOUT = 6000;
|
||||
// Minimum nodes version for LNS lookup
|
||||
this.LNS_CAPABLE_NODES_VERSION = '2.0.3';
|
||||
this.LNS_MAX_LENGTH = 64;
|
||||
// Conforms to naming rules here
|
||||
// https://loki.network/2020/03/25/loki-name-system-the-facts/
|
||||
this.LNS_REGEX = `^[a-zA-Z0-9_]([a-zA-Z0-9_-]{0,${this.LNS_MAX_LENGTH -
|
||||
2}}[a-zA-Z0-9_]){0,1}$`;
|
||||
})();
|
||||
|
||||
window.versionInfo = {
|
||||
environment: window.getEnvironment(),
|
||||
|
@ -155,7 +170,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 = () => {
|
||||
|
@ -326,16 +341,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');
|
||||
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');
|
||||
|
@ -418,7 +448,10 @@ window.lokiFeatureFlags = {
|
|||
privateGroupChats: true,
|
||||
useSnodeProxy: !process.env.USE_STUBBED_NETWORK,
|
||||
useOnionRequests: true,
|
||||
onionRequestHops: 1,
|
||||
useFileOnionRequests: false,
|
||||
enableSenderKeys: false,
|
||||
onionRequestHops: 3,
|
||||
debugMessageLogs: process.env.ENABLE_MESSAGE_LOGS,
|
||||
};
|
||||
|
||||
// eslint-disable-next-line no-extend-native,func-names
|
||||
|
@ -429,7 +462,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 */
|
||||
|
@ -444,12 +478,15 @@ if (
|
|||
};
|
||||
/* eslint-enable global-require, import/no-extraneous-dependencies */
|
||||
window.lokiFeatureFlags = {};
|
||||
window.lokiSnodeAPI = {}; // no need stub out each function here
|
||||
window.lokiSnodeAPI = new window.StubLokiSnodeAPI(); // no need stub out each function here
|
||||
}
|
||||
if (config.environment.includes('test-integration')) {
|
||||
window.lokiFeatureFlags = {
|
||||
multiDeviceUnpairing: true,
|
||||
privateGroupChats: true,
|
||||
useSnodeProxy: !process.env.USE_STUBBED_NETWORK,
|
||||
useOnionRequests: false,
|
||||
debugMessageLogs: true,
|
||||
enableSenderKeys: true,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -51,17 +51,27 @@ message MediumGroupContent {
|
|||
optional bytes ephemeralKey = 2;
|
||||
}
|
||||
|
||||
message MediumGroupUpdate {
|
||||
optional string groupName = 1;
|
||||
optional string groupId = 2; // should this be bytes?
|
||||
optional string groupSecretKey = 3;
|
||||
optional string senderKey = 4;
|
||||
repeated string members = 5;
|
||||
message SenderKey {
|
||||
optional string chainKey = 1;
|
||||
optional uint32 keyIdx = 2;
|
||||
}
|
||||
|
||||
message SenderKeyUpdate {
|
||||
optional string groupId = 1;
|
||||
optional string senderKey = 2;
|
||||
message MediumGroupUpdate {
|
||||
|
||||
enum Type {
|
||||
NEW_GROUP = 0; // groupId, groupName, groupSecretKey, members, senderKey
|
||||
GROUP_INFO = 1; // groupId, groupName, members, senderKey
|
||||
SENDER_KEY_REQUEST = 2; // groupId
|
||||
SENDER_KEY = 3; // groupId, SenderKey
|
||||
}
|
||||
|
||||
optional string groupName = 1;
|
||||
optional string groupId = 2; // should this be bytes?
|
||||
optional bytes groupSecretKey = 3;
|
||||
optional SenderKey senderKey = 4;
|
||||
repeated bytes members = 5;
|
||||
repeated string admins = 6;
|
||||
optional Type type = 7;
|
||||
}
|
||||
|
||||
message LokiAddressMessage {
|
||||
|
@ -424,4 +434,5 @@ message GroupDetails {
|
|||
optional string color = 7;
|
||||
optional bool blocked = 8;
|
||||
repeated string admins = 9;
|
||||
optional bool is_medium_group = 10;
|
||||
}
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
Subproject commit 52b77bf3039aec88b3900e8a7ed6e62d30a4d0d4
|
|
@ -91,6 +91,7 @@ textarea {
|
|||
width: auto;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-weight: 700;
|
||||
user-select: none;
|
||||
white-space: nowrap;
|
||||
|
@ -189,7 +190,7 @@ textarea {
|
|||
&.brand {
|
||||
min-width: 165px;
|
||||
height: 45px;
|
||||
line-height: 40px;
|
||||
align-items: center;
|
||||
padding: 0px $session-margin-lg;
|
||||
font-size: $session-font-md;
|
||||
font-family: $session-font-accent;
|
||||
|
@ -558,7 +559,9 @@ label {
|
|||
max-width: 70vw;
|
||||
background-color: $session-shade-4;
|
||||
border: 1px solid $session-shade-8;
|
||||
padding-bottom: $session-margin-lg;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
&__header {
|
||||
display: flex;
|
||||
|
@ -609,6 +612,8 @@ label {
|
|||
font-family: $session-font-accent;
|
||||
line-height: $session-font-md;
|
||||
font-size: $session-font-sm;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
|
||||
.message {
|
||||
text-align: center;
|
||||
|
@ -669,6 +674,17 @@ label {
|
|||
}
|
||||
}
|
||||
|
||||
.sealed-sender-toggle {
|
||||
display: flex;
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
.sender-keys-description {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
.create-group-dialog .session-modal__body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
@ -1061,7 +1077,8 @@ label {
|
|||
flex-direction: column;
|
||||
|
||||
&-list {
|
||||
overflow-y: scroll;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
&-header {
|
||||
|
@ -1131,6 +1148,7 @@ label {
|
|||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
&__version-info {
|
||||
|
@ -1566,6 +1584,14 @@ input {
|
|||
text-align: center;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
// Height at which scroll bar appears on the group member list
|
||||
@media (max-height: 804px) {
|
||||
&__container {
|
||||
overflow-y: visible;
|
||||
max-height: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
.create-group-name-input {
|
||||
.session-id-editable {
|
||||
|
|
|
@ -239,7 +239,8 @@ $session-compose-margin: 20px;
|
|||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
height: -webkit-fill-available;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
.session-icon .exit {
|
||||
padding: 13px;
|
||||
}
|
||||
|
@ -339,7 +340,8 @@ $session-compose-margin: 20px;
|
|||
.session-left-pane-section-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.user-search-dropdown {
|
||||
|
@ -404,8 +406,6 @@ $session-compose-margin: 20px;
|
|||
@mixin bottom-buttons() {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
position: absolute;
|
||||
bottom: 2px;
|
||||
width: 100%;
|
||||
|
||||
@at-root .light-theme #{&} {
|
||||
|
@ -471,7 +471,8 @@ $session-compose-margin: 20px;
|
|||
&-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
overflow: hidden;
|
||||
flex: 1;
|
||||
|
||||
.module-conversation-list-item {
|
||||
background-color: $session-shade-4;
|
||||
|
@ -534,6 +535,7 @@ $session-compose-margin: 20px;
|
|||
&-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
&-category-list-item {
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
|
||||
&-accent {
|
||||
flex-grow: 1;
|
||||
|
@ -28,21 +29,32 @@
|
|||
}
|
||||
|
||||
&-registration {
|
||||
height: 45%;
|
||||
padding-right: 128px;
|
||||
}
|
||||
|
||||
&-header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
padding: 17px 20px;
|
||||
}
|
||||
|
||||
&-body {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex: 1;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
|
||||
&-close-button {
|
||||
position: absolute;
|
||||
top: 17px;
|
||||
left: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&-session-button {
|
||||
position: absolute;
|
||||
top: 17px;
|
||||
right: 20px;
|
||||
|
||||
img {
|
||||
width: 30px;
|
||||
}
|
||||
|
@ -246,6 +258,8 @@
|
|||
display: inline-block;
|
||||
font-family: $session-font-mono;
|
||||
user-select: all;
|
||||
overflow: hidden;
|
||||
resize: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -492,13 +492,14 @@
|
|||
<script type="text/javascript" src="test.js"></script>
|
||||
|
||||
<script type="text/javascript" src="../js/registration.js" data-cover></script>
|
||||
<script type="text/javascript" src="../js/expire.js" data-cover></script>
|
||||
<script type="text/javascript" src="../js/chromium.js" data-cover></script>
|
||||
<script type="text/javascript" src="../js/database.js" data-cover></script>
|
||||
<script type="text/javascript" src="../js/storage.js" data-cover></script>
|
||||
<script type="text/javascript" src="../js/signal_protocol_store.js" data-cover></script>
|
||||
<script type="text/javascript" src="../js/libtextsecure.js" data-cover></script>
|
||||
<script type="text/javascript" src="../js/libloki.js" data-cover></script>
|
||||
<!-- needs the network comms libraries to work -->
|
||||
<script type="text/javascript" src="../js/expire.js" data-cover></script>
|
||||
|
||||
<script type="text/javascript" src="../js/models/messages.js" data-cover></script>
|
||||
<script type="text/javascript" src="../js/models/conversations.js" data-cover></script>
|
||||
|
|
|
@ -11,7 +11,7 @@ import { LeftPaneSectionHeader } from './session/LeftPaneSectionHeader';
|
|||
import { ConversationType } from '../state/ducks/conversations';
|
||||
import { LeftPaneContactSection } from './session/LeftPaneContactSection';
|
||||
import { LeftPaneSettingSection } from './session/LeftPaneSettingSection';
|
||||
import { LeftPaneChannelSection } from './session/LeftPaneChannelSection';
|
||||
import { SessionIconType } from './session/icon';
|
||||
|
||||
// from https://github.com/bvaughn/react-virtualized/blob/fb3484ed5dcc41bffae8eab029126c0fb8f7abc0/source/List/types.js#L5
|
||||
export type RowRendererParamsType = {
|
||||
|
@ -59,6 +59,7 @@ export class LeftPane extends React.Component<Props, State> {
|
|||
labels: Array<string>,
|
||||
onTabSelected?: any,
|
||||
buttonLabel?: string,
|
||||
buttonIcon?: SessionIconType,
|
||||
buttonClicked?: any,
|
||||
notificationCount?: number
|
||||
): JSX.Element {
|
||||
|
@ -68,6 +69,7 @@ export class LeftPane extends React.Component<Props, State> {
|
|||
selectedTab={0}
|
||||
labels={labels}
|
||||
buttonLabel={buttonLabel}
|
||||
buttonIcon={buttonIcon}
|
||||
buttonClicked={buttonClicked}
|
||||
notificationCount={notificationCount}
|
||||
/>
|
||||
|
@ -100,8 +102,6 @@ export class LeftPane extends React.Component<Props, State> {
|
|||
return this.renderMessageSection();
|
||||
case SectionType.Contact:
|
||||
return this.renderContactSection();
|
||||
case SectionType.Channel:
|
||||
return this.renderChannelSection();
|
||||
case SectionType.Settings:
|
||||
return this.renderSettingSection();
|
||||
case SectionType.Moon:
|
||||
|
@ -176,30 +176,4 @@ export class LeftPane extends React.Component<Props, State> {
|
|||
|
||||
return <LeftPaneSettingSection isSecondaryDevice={isSecondaryDevice} />;
|
||||
}
|
||||
|
||||
private renderChannelSection() {
|
||||
const {
|
||||
openConversationInternal,
|
||||
conversations,
|
||||
searchResults,
|
||||
searchTerm,
|
||||
isSecondaryDevice,
|
||||
updateSearchTerm,
|
||||
search,
|
||||
clearSearch,
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<LeftPaneChannelSection
|
||||
openConversationInternal={openConversationInternal}
|
||||
conversations={conversations}
|
||||
searchResults={searchResults}
|
||||
searchTerm={searchTerm}
|
||||
isSecondaryDevice={isSecondaryDevice}
|
||||
updateSearchTerm={updateSearchTerm}
|
||||
search={search}
|
||||
clearSearch={clearSearch}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,29 +7,14 @@ import {
|
|||
} from './session/settings/SessionSettings';
|
||||
|
||||
export const MainViewController = {
|
||||
renderMessageView: () => {
|
||||
if (document.getElementById('main-view')) {
|
||||
ReactDOM.render(<MessageView />, document.getElementById('main-view'));
|
||||
}
|
||||
},
|
||||
|
||||
renderSettingsView: (category: SessionSettingCategory) => {
|
||||
// tslint:disable-next-line: no-backbone-get-set-outside-model
|
||||
const isSecondaryDevice = !!window.textsecure.storage.get(
|
||||
'isSecondaryDevice'
|
||||
);
|
||||
if (document.getElementById('main-view')) {
|
||||
ReactDOM.render(
|
||||
<SettingsView
|
||||
category={category}
|
||||
isSecondaryDevice={isSecondaryDevice}
|
||||
/>,
|
||||
document.getElementById('main-view')
|
||||
);
|
||||
}
|
||||
},
|
||||
joinChannelStateManager,
|
||||
createClosedGroup,
|
||||
renderMessageView,
|
||||
renderSettingsView,
|
||||
};
|
||||
|
||||
import { ContactType } from './session/SessionMemberListItem';
|
||||
|
||||
export class MessageView extends React.Component {
|
||||
public render() {
|
||||
return (
|
||||
|
@ -50,3 +35,153 @@ export class MessageView extends React.Component {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
// /////////////////////////////////////
|
||||
// //////////// Management /////////////
|
||||
// /////////////////////////////////////
|
||||
|
||||
function joinChannelStateManager(
|
||||
thisRef: any,
|
||||
serverURL: string,
|
||||
onSuccess?: any
|
||||
) {
|
||||
// Any component that uses this function MUST have the keys [loading, connectSuccess]
|
||||
// in their State
|
||||
|
||||
// TODO: Make this not hard coded
|
||||
const channelId = 1;
|
||||
thisRef.setState({ loading: true });
|
||||
const connectionResult = window.attemptConnection(serverURL, channelId);
|
||||
|
||||
// Give 5s maximum for promise to revole. Else, throw error.
|
||||
const connectionTimeout = setTimeout(() => {
|
||||
if (!thisRef.state.connectSuccess) {
|
||||
thisRef.setState({ loading: false });
|
||||
window.pushToast({
|
||||
title: window.i18n('connectToServerFail'),
|
||||
type: 'error',
|
||||
id: 'connectToServerFail',
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
}, window.CONSTANTS.MAX_CONNECTION_DURATION);
|
||||
|
||||
connectionResult
|
||||
.then(() => {
|
||||
clearTimeout(connectionTimeout);
|
||||
|
||||
if (thisRef.state.loading) {
|
||||
thisRef.setState({
|
||||
connectSuccess: true,
|
||||
loading: false,
|
||||
});
|
||||
window.pushToast({
|
||||
title: window.i18n('connectToServerSuccess'),
|
||||
id: 'connectToServerSuccess',
|
||||
type: 'success',
|
||||
});
|
||||
|
||||
if (onSuccess) {
|
||||
onSuccess();
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch((connectionError: string) => {
|
||||
clearTimeout(connectionTimeout);
|
||||
thisRef.setState({
|
||||
connectSuccess: true,
|
||||
loading: false,
|
||||
});
|
||||
window.pushToast({
|
||||
title: connectionError,
|
||||
id: 'connectToServerFail',
|
||||
type: 'error',
|
||||
});
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async function createClosedGroup(
|
||||
groupName: string,
|
||||
groupMembers: Array<ContactType>,
|
||||
senderKeys: boolean,
|
||||
onSuccess: any
|
||||
) {
|
||||
// Validate groupName and groupMembers length
|
||||
if (
|
||||
groupName.length === 0 ||
|
||||
groupName.length > window.CONSTANTS.MAX_GROUP_NAME_LENGTH
|
||||
) {
|
||||
window.pushToast({
|
||||
title: window.i18n(
|
||||
'invalidGroupName',
|
||||
window.CONSTANTS.MAX_GROUP_NAME_LENGTH
|
||||
),
|
||||
type: 'error',
|
||||
id: 'invalidGroupName',
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// >= because we add ourself as a member after this. so a 10 group is already invalid as it will be 11 with ourself
|
||||
if (
|
||||
groupMembers.length === 0 ||
|
||||
groupMembers.length >= window.CONSTANTS.SMALL_GROUP_SIZE_LIMIT
|
||||
) {
|
||||
window.pushToast({
|
||||
title: window.i18n(
|
||||
'invalidGroupSize',
|
||||
window.CONSTANTS.SMALL_GROUP_SIZE_LIMIT
|
||||
),
|
||||
type: 'error',
|
||||
id: 'invalidGroupSize',
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const groupMemberIds = groupMembers.map(m => m.id);
|
||||
|
||||
if (senderKeys) {
|
||||
await window.createMediumSizeGroup(groupName, groupMemberIds);
|
||||
} else {
|
||||
await window.doCreateGroup(groupName, groupMemberIds);
|
||||
}
|
||||
|
||||
if (onSuccess) {
|
||||
onSuccess();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// /////////////////////////////////////
|
||||
// ///////////// Rendering /////////////
|
||||
// /////////////////////////////////////
|
||||
|
||||
function renderMessageView() {
|
||||
if (document.getElementById('main-view')) {
|
||||
ReactDOM.render(<MessageView />, document.getElementById('main-view'));
|
||||
}
|
||||
}
|
||||
|
||||
function renderSettingsView(category: SessionSettingCategory) {
|
||||
// tslint:disable-next-line: no-backbone-get-set-outside-model
|
||||
const isSecondaryDevice = !!window.textsecure.storage.get(
|
||||
'isSecondaryDevice'
|
||||
);
|
||||
if (document.getElementById('main-view')) {
|
||||
ReactDOM.render(
|
||||
<SettingsView
|
||||
category={category}
|
||||
isSecondaryDevice={isSecondaryDevice}
|
||||
/>,
|
||||
document.getElementById('main-view')
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -441,22 +441,15 @@ export class ConversationHeader extends React.Component<Props> {
|
|||
isGroup,
|
||||
isFriend,
|
||||
isKickedFromGroup,
|
||||
isArchived,
|
||||
isPublic,
|
||||
isRss,
|
||||
onResetSession,
|
||||
onSetDisappearingMessages,
|
||||
// onShowAllMedia,
|
||||
onShowGroupMembers,
|
||||
onShowSafetyNumber,
|
||||
onArchive,
|
||||
onMoveToInbox,
|
||||
timerOptions,
|
||||
onBlockUser,
|
||||
onUnblockUser,
|
||||
// hasNickname,
|
||||
// onClearNickname,
|
||||
// onChangeNickname,
|
||||
} = this.props;
|
||||
|
||||
if (isPublic || isRss) {
|
||||
|
@ -485,6 +478,7 @@ export class ConversationHeader extends React.Component<Props> {
|
|||
const showMembersMenuItem = isGroup && (
|
||||
<MenuItem onClick={onShowGroupMembers}>{i18n('showMembers')}</MenuItem>
|
||||
);
|
||||
|
||||
const showSafetyNumberMenuItem = !isGroup && !isMe && (
|
||||
<MenuItem onClick={onShowSafetyNumber}>
|
||||
{i18n('showSafetyNumber')}
|
||||
|
@ -496,34 +490,14 @@ export class ConversationHeader extends React.Component<Props> {
|
|||
const blockHandlerMenuItem = !isMe && !isGroup && !isRss && (
|
||||
<MenuItem onClick={blockHandler}>{blockTitle}</MenuItem>
|
||||
);
|
||||
// const changeNicknameMenuItem = !isMe &&
|
||||
// !isGroup && (
|
||||
// <MenuItem onClick={onChangeNickname}>{i18n('changeNickname')}</MenuItem>
|
||||
// );
|
||||
// const clearNicknameMenuItem = !isMe &&
|
||||
// !isGroup &&
|
||||
// hasNickname && (
|
||||
// <MenuItem onClick={onClearNickname}>{i18n('clearNickname')}</MenuItem>
|
||||
// );
|
||||
const archiveConversationMenuItem = isArchived ? (
|
||||
<MenuItem onClick={onMoveToInbox}>
|
||||
{i18n('moveConversationToInbox')}
|
||||
</MenuItem>
|
||||
) : (
|
||||
<MenuItem onClick={onArchive}>{i18n('archiveConversation')}</MenuItem>
|
||||
);
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
{/* <MenuItem onClick={onShowAllMedia}>{i18n('viewAllMedia')}</MenuItem> */}
|
||||
{disappearingMessagesMenuItem}
|
||||
{showMembersMenuItem}
|
||||
{showSafetyNumberMenuItem}
|
||||
{resetSessionMenuItem}
|
||||
{blockHandlerMenuItem}
|
||||
{/* {changeNicknameMenuItem}
|
||||
{clearNicknameMenuItem} */}
|
||||
{archiveConversationMenuItem}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ declare global {
|
|||
interface Window {
|
||||
Lodash: any;
|
||||
doCreateGroup: any;
|
||||
createMediumSizeGroup: any;
|
||||
SMALL_GROUP_SIZE_LIMIT: number;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -108,9 +108,11 @@ export class InviteFriendsDialog extends React.Component<Props, State> {
|
|||
private renderMemberList() {
|
||||
const members = this.state.friendList;
|
||||
|
||||
return members.map((member: ContactType) => (
|
||||
return members.map((member: ContactType, index: number) => (
|
||||
<SessionMemberListItem
|
||||
member={member}
|
||||
key={index}
|
||||
index={index}
|
||||
isSelected={false}
|
||||
onSelect={(selectedMember: ContactType) => {
|
||||
this.onMemberClicked(selectedMember);
|
||||
|
|
|
@ -147,9 +147,10 @@ export class UpdateGroupMembersDialog extends React.Component<Props, State> {
|
|||
private renderMemberList() {
|
||||
const members = this.state.friendList;
|
||||
|
||||
return members.map((member: ContactType) => (
|
||||
return members.map((member: ContactType, index: number) => (
|
||||
<SessionMemberListItem
|
||||
member={member}
|
||||
index={index}
|
||||
isSelected={!member.checkmarked}
|
||||
onSelect={this.onMemberClicked}
|
||||
onUnselect={this.onMemberClicked}
|
||||
|
|
|
@ -136,7 +136,6 @@ export class ActionsPanel extends React.Component<Props, State> {
|
|||
const isProfilePageSelected = selectedSection === SectionType.Profile;
|
||||
const isMessagePageSelected = selectedSection === SectionType.Message;
|
||||
const isContactPageSelected = selectedSection === SectionType.Contact;
|
||||
const isChannelPageSelected = selectedSection === SectionType.Channel;
|
||||
const isSettingsPageSelected = selectedSection === SectionType.Settings;
|
||||
const isMoonPageSelected = selectedSection === SectionType.Moon;
|
||||
|
||||
|
@ -154,11 +153,6 @@ export class ActionsPanel extends React.Component<Props, State> {
|
|||
onSelect={this.handleSectionSelect}
|
||||
notificationCount={unreadMessageCount}
|
||||
/>
|
||||
<this.Section
|
||||
type={SectionType.Channel}
|
||||
isSelected={isChannelPageSelected}
|
||||
onSelect={this.handleSectionSelect}
|
||||
/>
|
||||
<this.Section
|
||||
type={SectionType.Contact}
|
||||
isSelected={isContactPageSelected}
|
||||
|
|
|
@ -1,508 +0,0 @@
|
|||
import React from 'react';
|
||||
import { AutoSizer, List } from 'react-virtualized';
|
||||
|
||||
import {
|
||||
ConversationListItem,
|
||||
PropsData as ConversationListItemPropsType,
|
||||
} from '../ConversationListItem';
|
||||
|
||||
import { LeftPane, RowRendererParamsType } from '../LeftPane';
|
||||
import {
|
||||
SessionButton,
|
||||
SessionButtonColor,
|
||||
SessionButtonType,
|
||||
} from './SessionButton';
|
||||
import {
|
||||
PropsData as SearchResultsProps,
|
||||
SearchResults,
|
||||
} from '../SearchResults';
|
||||
import { SearchOptions } from '../../types/Search';
|
||||
import { debounce } from 'lodash';
|
||||
import { cleanSearchTerm } from '../../util/cleanSearchTerm';
|
||||
import { SessionSearchInput } from './SessionSearchInput';
|
||||
import { SessionClosableOverlay } from './SessionClosableOverlay';
|
||||
import { MainViewController } from '../MainViewController';
|
||||
import { ContactType } from './SessionMemberListItem';
|
||||
|
||||
export interface Props {
|
||||
searchTerm: string;
|
||||
isSecondaryDevice: boolean;
|
||||
|
||||
conversations?: Array<ConversationListItemPropsType>;
|
||||
|
||||
searchResults?: SearchResultsProps;
|
||||
|
||||
updateSearchTerm: (searchTerm: string) => void;
|
||||
search: (query: string, options: SearchOptions) => void;
|
||||
openConversationInternal: (id: string, messageId?: string) => void;
|
||||
clearSearch: () => void;
|
||||
}
|
||||
|
||||
export enum SessionGroupType {
|
||||
Open = 'open-group',
|
||||
Closed = 'closed-group',
|
||||
}
|
||||
|
||||
interface State {
|
||||
channelUrlPasted: string;
|
||||
loading: boolean;
|
||||
connectSuccess: boolean;
|
||||
// The type of group that is being added. Undefined in default view.
|
||||
groupAddType: SessionGroupType | undefined;
|
||||
}
|
||||
|
||||
export class LeftPaneChannelSection extends React.Component<Props, State> {
|
||||
private readonly updateSearchBound: (searchedString: string) => void;
|
||||
private readonly debouncedSearch: (searchTerm: string) => void;
|
||||
|
||||
public constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
channelUrlPasted: '',
|
||||
loading: false,
|
||||
connectSuccess: false,
|
||||
groupAddType: undefined,
|
||||
};
|
||||
|
||||
this.handleOnPasteUrl = this.handleOnPasteUrl.bind(this);
|
||||
this.handleJoinChannelButtonClick = this.handleJoinChannelButtonClick.bind(
|
||||
this
|
||||
);
|
||||
this.handleToggleOverlay = this.handleToggleOverlay.bind(this);
|
||||
this.updateSearchBound = this.updateSearch.bind(this);
|
||||
this.debouncedSearch = debounce(this.search.bind(this), 20);
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
this.updateSearch('');
|
||||
}
|
||||
|
||||
public getCurrentConversations():
|
||||
| Array<ConversationListItemPropsType>
|
||||
| undefined {
|
||||
const { conversations } = this.props;
|
||||
|
||||
let conversationList = conversations;
|
||||
if (conversationList !== undefined) {
|
||||
conversationList = conversationList.filter(
|
||||
// a channel is either a public group or a rss group
|
||||
conversation => conversation && conversation.type === 'group'
|
||||
);
|
||||
}
|
||||
|
||||
return conversationList;
|
||||
}
|
||||
|
||||
public renderRow = ({
|
||||
index,
|
||||
key,
|
||||
style,
|
||||
}: RowRendererParamsType): JSX.Element => {
|
||||
const { openConversationInternal } = this.props;
|
||||
|
||||
const conversations = this.getCurrentConversations();
|
||||
|
||||
if (!conversations) {
|
||||
throw new Error('renderRow: Tried to render without conversations');
|
||||
}
|
||||
|
||||
const conversation = conversations[index];
|
||||
|
||||
return (
|
||||
<ConversationListItem
|
||||
key={key}
|
||||
style={style}
|
||||
{...conversation}
|
||||
onClick={openConversationInternal}
|
||||
i18n={window.i18n}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
public renderList(): JSX.Element | Array<JSX.Element | null> {
|
||||
const { openConversationInternal, searchResults } = this.props;
|
||||
|
||||
if (searchResults) {
|
||||
return (
|
||||
<SearchResults
|
||||
{...searchResults}
|
||||
openConversation={openConversationInternal}
|
||||
i18n={window.i18n}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const conversations = this.getCurrentConversations();
|
||||
|
||||
if (!conversations) {
|
||||
throw new Error(
|
||||
'render: must provided conversations if no search results are provided'
|
||||
);
|
||||
}
|
||||
|
||||
const length = conversations.length;
|
||||
|
||||
// Note: conversations is not a known prop for List, but it is required to ensure that
|
||||
// it re-renders when our conversation data changes. Otherwise it would just render
|
||||
// on startup and scroll.
|
||||
const list = (
|
||||
<div className="module-left-pane__list" key={0}>
|
||||
<AutoSizer>
|
||||
{({ height, width }) => (
|
||||
<List
|
||||
className="module-left-pane__virtual-list"
|
||||
conversations={conversations}
|
||||
height={height}
|
||||
rowCount={length}
|
||||
rowHeight={64}
|
||||
rowRenderer={this.renderRow}
|
||||
width={width}
|
||||
autoHeight={true}
|
||||
/>
|
||||
)}
|
||||
</AutoSizer>
|
||||
</div>
|
||||
);
|
||||
|
||||
return [list];
|
||||
}
|
||||
|
||||
public renderHeader(): JSX.Element {
|
||||
const labels = [window.i18n('groups')];
|
||||
|
||||
return LeftPane.RENDER_HEADER(labels, null);
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
MainViewController.renderMessageView();
|
||||
}
|
||||
|
||||
public componentDidUpdate() {
|
||||
MainViewController.renderMessageView();
|
||||
}
|
||||
|
||||
public render(): JSX.Element {
|
||||
return (
|
||||
<div className="session-left-pane-section-content">
|
||||
{this.renderHeader()}
|
||||
{this.state.groupAddType
|
||||
? this.renderClosableOverlay(this.state.groupAddType)
|
||||
: this.renderGroups()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
public renderGroups() {
|
||||
return (
|
||||
<div className="module-conversations-list-content">
|
||||
<SessionSearchInput
|
||||
searchString={this.props.searchTerm}
|
||||
onChange={this.updateSearchBound}
|
||||
placeholder={window.i18n('search')}
|
||||
/>
|
||||
{this.renderList()}
|
||||
{this.renderBottomButtons()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
public updateSearch(searchTerm: string) {
|
||||
const { updateSearchTerm, clearSearch } = this.props;
|
||||
|
||||
if (!searchTerm) {
|
||||
clearSearch();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({ channelUrlPasted: '' });
|
||||
|
||||
if (updateSearchTerm) {
|
||||
updateSearchTerm(searchTerm);
|
||||
}
|
||||
|
||||
if (searchTerm.length < 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
const cleanedTerm = cleanSearchTerm(searchTerm);
|
||||
if (!cleanedTerm) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.debouncedSearch(cleanedTerm);
|
||||
}
|
||||
|
||||
public clearSearch() {
|
||||
this.props.clearSearch();
|
||||
}
|
||||
|
||||
public search() {
|
||||
const { search } = this.props;
|
||||
const { searchTerm, isSecondaryDevice } = this.props;
|
||||
|
||||
if (search) {
|
||||
search(searchTerm, {
|
||||
noteToSelf: window.i18n('noteToSelf').toLowerCase(),
|
||||
ourNumber: window.textsecure.storage.user.getNumber(),
|
||||
regionCode: '',
|
||||
isSecondaryDevice,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private handleToggleOverlay(groupType?: SessionGroupType) {
|
||||
// If no groupType, return to default view.
|
||||
// Close the overlay with handleToggleOverlay(undefined)
|
||||
|
||||
switch (groupType) {
|
||||
case SessionGroupType.Open:
|
||||
this.setState({
|
||||
groupAddType: SessionGroupType.Open,
|
||||
});
|
||||
break;
|
||||
case SessionGroupType.Closed:
|
||||
this.setState({
|
||||
groupAddType: SessionGroupType.Closed,
|
||||
});
|
||||
break;
|
||||
default:
|
||||
// Exit overlay
|
||||
this.setState({
|
||||
groupAddType: undefined,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private renderClosableOverlay(groupType: SessionGroupType) {
|
||||
const { searchTerm } = this.props;
|
||||
const { loading } = this.state;
|
||||
|
||||
const openGroupElement = (
|
||||
<SessionClosableOverlay
|
||||
overlayMode={SessionGroupType.Open}
|
||||
onChangeSessionID={this.handleOnPasteUrl}
|
||||
onCloseClick={() => {
|
||||
this.handleToggleOverlay(undefined);
|
||||
}}
|
||||
onButtonClick={this.handleJoinChannelButtonClick}
|
||||
searchTerm={searchTerm}
|
||||
updateSearch={this.updateSearchBound}
|
||||
showSpinner={loading}
|
||||
/>
|
||||
);
|
||||
|
||||
const closedGroupElement = (
|
||||
<SessionClosableOverlay
|
||||
overlayMode={SessionGroupType.Closed}
|
||||
onChangeSessionID={this.handleOnPasteUrl}
|
||||
onCloseClick={() => {
|
||||
this.handleToggleOverlay(undefined);
|
||||
}}
|
||||
onButtonClick={async (
|
||||
groupName: string,
|
||||
groupMembers: Array<ContactType>
|
||||
) => this.onCreateClosedGroup(groupName, groupMembers)}
|
||||
searchTerm={searchTerm}
|
||||
updateSearch={this.updateSearchBound}
|
||||
showSpinner={loading}
|
||||
/>
|
||||
);
|
||||
|
||||
return groupType === SessionGroupType.Open
|
||||
? openGroupElement
|
||||
: closedGroupElement;
|
||||
}
|
||||
|
||||
private renderBottomButtons(): JSX.Element {
|
||||
const edit = window.i18n('edit');
|
||||
const joinOpenGroup = window.i18n('joinOpenGroup');
|
||||
const createClosedGroup = window.i18n('createClosedGroup');
|
||||
const showEditButton = false;
|
||||
|
||||
return (
|
||||
<div className="left-pane-contact-bottom-buttons">
|
||||
{showEditButton && (
|
||||
<SessionButton
|
||||
text={edit}
|
||||
buttonType={SessionButtonType.SquareOutline}
|
||||
buttonColor={SessionButtonColor.White}
|
||||
/>
|
||||
)}
|
||||
|
||||
<SessionButton
|
||||
text={joinOpenGroup}
|
||||
buttonType={SessionButtonType.SquareOutline}
|
||||
buttonColor={SessionButtonColor.Green}
|
||||
onClick={() => {
|
||||
this.handleToggleOverlay(SessionGroupType.Open);
|
||||
}}
|
||||
/>
|
||||
<SessionButton
|
||||
text={createClosedGroup}
|
||||
buttonType={SessionButtonType.SquareOutline}
|
||||
buttonColor={SessionButtonColor.White}
|
||||
onClick={() => {
|
||||
this.handleToggleOverlay(SessionGroupType.Closed);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
private handleOnPasteUrl(value: string) {
|
||||
this.setState({ channelUrlPasted: value });
|
||||
}
|
||||
|
||||
private handleJoinChannelButtonClick(groupUrl: string) {
|
||||
const { loading } = this.state;
|
||||
|
||||
if (loading) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// longest TLD is now (20/02/06) 24 characters per https://jasontucker.blog/8945/what-is-the-longest-tld-you-can-get-for-a-domain-name
|
||||
const regexURL = /(http:\/\/|https:\/\/)?[a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,24}(:[0-9]{1,5})?(\/.*)?/;
|
||||
|
||||
if (groupUrl.length <= 0) {
|
||||
window.pushToast({
|
||||
title: window.i18n('noServerURL'),
|
||||
type: 'error',
|
||||
id: 'connectToServerFail',
|
||||
});
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!regexURL.test(groupUrl)) {
|
||||
window.pushToast({
|
||||
title: window.i18n('noServerURL'),
|
||||
type: 'error',
|
||||
id: 'connectToServerFail',
|
||||
});
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
joinChannelStateManager(this, groupUrl, () => {
|
||||
this.handleToggleOverlay(undefined);
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private async onCreateClosedGroup(
|
||||
groupName: string,
|
||||
groupMembers: Array<ContactType>
|
||||
) {
|
||||
// Validate groupName and groupMembers length
|
||||
if (
|
||||
groupName.length === 0 ||
|
||||
groupName.length > window.CONSTANTS.MAX_GROUP_NAME_LENGTH
|
||||
) {
|
||||
window.pushToast({
|
||||
title: window.i18n(
|
||||
'invalidGroupName',
|
||||
window.CONSTANTS.MAX_GROUP_NAME_LENGTH
|
||||
),
|
||||
type: 'error',
|
||||
id: 'invalidGroupName',
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// >= because we add ourself as a member after this. so a 10 group is already invalid as it will be 11 with ourself
|
||||
if (
|
||||
groupMembers.length === 0 ||
|
||||
groupMembers.length >= window.CONSTANTS.SMALL_GROUP_SIZE_LIMIT
|
||||
) {
|
||||
window.pushToast({
|
||||
title: window.i18n(
|
||||
'invalidGroupSize',
|
||||
window.CONSTANTS.SMALL_GROUP_SIZE_LIMIT
|
||||
),
|
||||
type: 'error',
|
||||
id: 'invalidGroupSize',
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const groupMemberIds = groupMembers.map(m => m.id);
|
||||
await window.doCreateGroup(groupName, groupMemberIds);
|
||||
this.handleToggleOverlay(undefined);
|
||||
|
||||
window.pushToast({
|
||||
title: window.i18n('closedGroupCreatedToastTitle'),
|
||||
type: 'success',
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
export function joinChannelStateManager(
|
||||
thisRef: any,
|
||||
serverURL: string,
|
||||
onSuccess?: any
|
||||
) {
|
||||
// Any component that uses this function MUST have the keys [loading, connectSuccess]
|
||||
// in their State
|
||||
|
||||
// TODO: Make this not hard coded
|
||||
const channelId = 1;
|
||||
thisRef.setState({ loading: true });
|
||||
const connectionResult = window.attemptConnection(serverURL, channelId);
|
||||
|
||||
// Give 5s maximum for promise to revole. Else, throw error.
|
||||
const connectionTimeout = setTimeout(() => {
|
||||
if (!thisRef.state.connectSuccess) {
|
||||
thisRef.setState({ loading: false });
|
||||
window.pushToast({
|
||||
title: window.i18n('connectToServerFail'),
|
||||
type: 'error',
|
||||
id: 'connectToServerFail',
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
}, window.CONSTANTS.MAX_CONNECTION_DURATION);
|
||||
|
||||
connectionResult
|
||||
.then(() => {
|
||||
clearTimeout(connectionTimeout);
|
||||
|
||||
if (thisRef.state.loading) {
|
||||
thisRef.setState({
|
||||
connectSuccess: true,
|
||||
loading: false,
|
||||
});
|
||||
window.pushToast({
|
||||
title: window.i18n('connectToServerSuccess'),
|
||||
id: 'connectToServerSuccess',
|
||||
type: 'success',
|
||||
});
|
||||
|
||||
if (onSuccess) {
|
||||
onSuccess();
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch((connectionError: string) => {
|
||||
clearTimeout(connectionTimeout);
|
||||
thisRef.setState({
|
||||
connectSuccess: true,
|
||||
loading: false,
|
||||
});
|
||||
window.pushToast({
|
||||
title: connectionError,
|
||||
id: 'connectToServerFail',
|
||||
type: 'error',
|
||||
});
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
|
@ -17,7 +17,10 @@ import {
|
|||
import { AutoSizer, List } from 'react-virtualized';
|
||||
import { validateNumber } from '../../types/PhoneNumber';
|
||||
import { ConversationType } from '../../state/ducks/conversations';
|
||||
import { SessionClosableOverlay } from './SessionClosableOverlay';
|
||||
import {
|
||||
SessionClosableOverlay,
|
||||
SessionClosableOverlayType,
|
||||
} from './SessionClosableOverlay';
|
||||
import { MainViewController } from '../MainViewController';
|
||||
|
||||
export interface Props {
|
||||
|
@ -89,6 +92,7 @@ export class LeftPaneContactSection extends React.Component<Props, State> {
|
|||
labels,
|
||||
this.handleTabSelected,
|
||||
undefined,
|
||||
undefined,
|
||||
this.handleToggleFriendRequestPopup,
|
||||
receivedFriendRequestCount
|
||||
);
|
||||
|
@ -140,7 +144,8 @@ export class LeftPaneContactSection extends React.Component<Props, State> {
|
|||
style,
|
||||
}: RowRendererParamsType): JSX.Element | undefined => {
|
||||
const { sentFriendsRequest } = this.props;
|
||||
const friends = window.getFriendsFromContacts(this.props.friends);
|
||||
const contacts = this.props.friends.filter(f => f.type === 'direct');
|
||||
const friends = contacts.filter(c => c.isFriend);
|
||||
const combined = [...sentFriendsRequest, ...friends];
|
||||
const item = combined[index];
|
||||
|
||||
|
@ -203,7 +208,7 @@ export class LeftPaneContactSection extends React.Component<Props, State> {
|
|||
private renderClosableOverlay() {
|
||||
return (
|
||||
<SessionClosableOverlay
|
||||
overlayMode="contact"
|
||||
overlayMode={SessionClosableOverlayType.Contact}
|
||||
onChangeSessionID={this.handleRecipientSessionIDChanged}
|
||||
onCloseClick={this.handleToggleOverlay}
|
||||
onButtonClick={this.handleOnAddContact}
|
||||
|
@ -322,7 +327,8 @@ export class LeftPaneContactSection extends React.Component<Props, State> {
|
|||
private renderList() {
|
||||
const { sentFriendsRequest } = this.props;
|
||||
|
||||
const friends = window.getFriendsFromContacts(this.props.friends);
|
||||
const contacts = this.props.friends.filter(f => f.type === 'direct');
|
||||
const friends = contacts.filter(c => c.isFriend);
|
||||
const length = Number(sentFriendsRequest.length) + Number(friends.length);
|
||||
|
||||
const combined = [...sentFriendsRequest, ...friends];
|
||||
|
|
|
@ -27,6 +27,12 @@ import {
|
|||
import { SessionSpinner } from './SessionSpinner';
|
||||
import { joinChannelStateManager } from './LeftPaneChannelSection';
|
||||
|
||||
// HIJACKING BUTTON FOR TESTING
|
||||
import { PendingMessageCache } from '../../session/sending/PendingMessageCache';
|
||||
import { MessageQueue } from '../../session/sending';
|
||||
import { ExampleMessage } from '../../session/sending/MessageQueue';
|
||||
|
||||
|
||||
export interface Props {
|
||||
searchTerm: string;
|
||||
isSecondaryDevice: boolean;
|
||||
|
@ -45,6 +51,10 @@ export class LeftPaneMessageSection extends React.Component<Props, any> {
|
|||
private readonly updateSearchBound: (searchedString: string) => void;
|
||||
private readonly debouncedSearch: (searchTerm: string) => void;
|
||||
|
||||
// HIJACKED FOR TESTING
|
||||
private readonly messageQueue: any;
|
||||
private readonly pendingMessageCache: any;
|
||||
|
||||
public constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
|
@ -82,6 +92,11 @@ export class LeftPaneMessageSection extends React.Component<Props, any> {
|
|||
this.handleOnPasteSessionID = this.handleOnPasteSessionID.bind(this);
|
||||
this.handleMessageButtonClick = this.handleMessageButtonClick.bind(this);
|
||||
this.debouncedSearch = debounce(this.search.bind(this), 20);
|
||||
|
||||
|
||||
// HIJACKING FOR TESTING
|
||||
this.messageQueue = new MessageQueue();
|
||||
this.pendingMessageCache = new PendingMessageCache();
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
|
@ -97,7 +112,7 @@ export class LeftPaneMessageSection extends React.Component<Props, any> {
|
|||
if (conversationList !== undefined) {
|
||||
conversationList = conversationList.filter(
|
||||
conversation =>
|
||||
!conversation.isSecondary && !conversation.isPendingFriendRequest
|
||||
!conversation.isPendingFriendRequest && !conversation.isSecondary
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -361,12 +376,29 @@ export class LeftPaneMessageSection extends React.Component<Props, any> {
|
|||
);
|
||||
}
|
||||
|
||||
private handleToggleOverlay() {
|
||||
this.setState((state: any) => {
|
||||
return { showComposeView: !state.showComposeView };
|
||||
});
|
||||
// empty our generalized searchedString (one for the whole app)
|
||||
this.updateSearch('');
|
||||
private async handleToggleOverlay() {
|
||||
// HIJACKING BUTTON FOR TESTING
|
||||
console.log('[vince] pendingMessageCache:', this.pendingMessageCache);
|
||||
|
||||
const pubkey = window.textsecure.storage.user.getNumber();
|
||||
const exampleMessage = new ExampleMessage();
|
||||
|
||||
console.log('[vince] exampleMessage:', exampleMessage);
|
||||
|
||||
const devices = this.pendingMessageCache.getPendingDevices();
|
||||
console.log('[vince] devices:', devices);
|
||||
|
||||
if ($('.session-search-input input').val()) {
|
||||
this.pendingMessageCache.removePendingMessageByIdentifier(exampleMessage.identifier);
|
||||
} else {
|
||||
this.pendingMessageCache.addPendingMessage(pubkey, exampleMessage);
|
||||
}
|
||||
|
||||
// this.setState((state: any) => {
|
||||
// return { showComposeView: !state.showComposeView };
|
||||
// });
|
||||
// // empty our generalized searchedString (one for the whole app)
|
||||
// this.updateSearch('');
|
||||
}
|
||||
|
||||
private handleOnPasteSessionID(value: string) {
|
||||
|
@ -408,4 +440,4 @@ export class LeftPaneMessageSection extends React.Component<Props, any> {
|
|||
const serverURL = window.CONSTANTS.DEFAULT_PUBLIC_CHAT_URL;
|
||||
joinChannelStateManager(this, serverURL, this.handleCloseOnboarding);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { SessionButton } from './SessionButton';
|
||||
import { SessionIcon, SessionIconSize, SessionIconType } from './icon';
|
||||
import {
|
||||
NotificationCountSize,
|
||||
SessionNotificationCount,
|
||||
|
@ -43,6 +44,7 @@ interface Props {
|
|||
labels: Array<string>;
|
||||
notificationCount?: number;
|
||||
buttonLabel?: string;
|
||||
buttonIcon?: SessionIconType;
|
||||
buttonClicked?: any;
|
||||
}
|
||||
|
||||
|
@ -65,10 +67,13 @@ export class LeftPaneSectionHeader extends React.Component<Props, State> {
|
|||
const {
|
||||
labels,
|
||||
buttonLabel,
|
||||
buttonIcon,
|
||||
buttonClicked,
|
||||
notificationCount,
|
||||
} = this.props;
|
||||
|
||||
const hasButton = buttonLabel || buttonIcon;
|
||||
|
||||
const children = [];
|
||||
//loop to create children
|
||||
for (let i = 0; i < labels.length; i++) {
|
||||
|
@ -83,15 +88,19 @@ export class LeftPaneSectionHeader extends React.Component<Props, State> {
|
|||
);
|
||||
}
|
||||
|
||||
if (buttonLabel && !notificationCount) {
|
||||
children.push(
|
||||
<SessionButton
|
||||
text={buttonLabel}
|
||||
onClick={buttonClicked}
|
||||
key="compose"
|
||||
disabled={false}
|
||||
/>
|
||||
if (hasButton && !notificationCount) {
|
||||
const buttonContent = buttonIcon ? (
|
||||
<SessionIcon iconType={buttonIcon} iconSize={SessionIconSize.Small} />
|
||||
) : (
|
||||
buttonLabel
|
||||
);
|
||||
const button = (
|
||||
<SessionButton onClick={buttonClicked} key="compose" disabled={false}>
|
||||
{buttonContent}
|
||||
</SessionButton>
|
||||
);
|
||||
|
||||
children.push(button);
|
||||
} else if (buttonLabel && notificationCount && notificationCount > 0) {
|
||||
children.push(
|
||||
<div className="contact-notification-section">
|
||||
|
@ -105,6 +114,7 @@ export class LeftPaneSectionHeader extends React.Component<Props, State> {
|
|||
count={notificationCount}
|
||||
size={NotificationCountSize.ON_HEADER}
|
||||
onClick={this.props.buttonClicked}
|
||||
key="notification-count" // we can only have one of those here
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
@ -114,11 +124,12 @@ export class LeftPaneSectionHeader extends React.Component<Props, State> {
|
|||
count={notificationCount}
|
||||
size={NotificationCountSize.ON_HEADER}
|
||||
onClick={this.props.buttonClicked}
|
||||
key="notificationCount"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
//Create the parent and add the children
|
||||
// Create the parent and add the children
|
||||
return <div className="module-left-pane__header">{children}</div>;
|
||||
}
|
||||
|
||||
|
|
|
@ -62,6 +62,7 @@ export class LeftPaneSettingSection extends React.Component<Props, State> {
|
|||
null,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import React from 'react';
|
||||
|
||||
import { SessionIconButton, SessionIconSize, SessionIconType } from './icon';
|
||||
import { SessionToggle } from './SessionToggle';
|
||||
import { SessionIdEditable } from './SessionIdEditable';
|
||||
import { UserSearchDropdown } from './UserSearchDropdown';
|
||||
import { ContactType, SessionMemberListItem } from './SessionMemberListItem';
|
||||
|
@ -11,11 +12,18 @@ import {
|
|||
SessionButtonType,
|
||||
} from './SessionButton';
|
||||
import { SessionSpinner } from './SessionSpinner';
|
||||
import { SessionGroupType } from './LeftPaneChannelSection';
|
||||
import { PillDivider } from './PillDivider';
|
||||
import classNames from 'classnames';
|
||||
|
||||
export enum SessionClosableOverlayType {
|
||||
Contact = 'contact',
|
||||
Message = 'message',
|
||||
OpenGroup = 'open-group',
|
||||
ClosedGroup = 'closed-group',
|
||||
}
|
||||
|
||||
interface Props {
|
||||
overlayMode: 'message' | 'contact' | SessionGroupType;
|
||||
overlayMode: SessionClosableOverlayType;
|
||||
onChangeSessionID: any;
|
||||
onCloseClick: any;
|
||||
onButtonClick: any;
|
||||
|
@ -29,6 +37,7 @@ interface Props {
|
|||
interface State {
|
||||
groupName: string;
|
||||
selectedMembers: Array<ContactType>;
|
||||
senderKeys: boolean;
|
||||
}
|
||||
|
||||
export class SessionClosableOverlay extends React.Component<Props, State> {
|
||||
|
@ -40,10 +49,14 @@ export class SessionClosableOverlay extends React.Component<Props, State> {
|
|||
this.state = {
|
||||
groupName: '',
|
||||
selectedMembers: [],
|
||||
senderKeys: false,
|
||||
};
|
||||
|
||||
this.inputRef = React.createRef();
|
||||
this.onKeyUp = this.onKeyUp.bind(this);
|
||||
this.onGroupNameChanged = this.onGroupNameChanged.bind(this);
|
||||
|
||||
window.addEventListener('keyup', this.onKeyUp);
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
|
@ -97,11 +110,12 @@ export class SessionClosableOverlay extends React.Component<Props, State> {
|
|||
onButtonClick,
|
||||
} = this.props;
|
||||
|
||||
const isAddContactView = overlayMode === 'contact';
|
||||
const isMessageView = overlayMode === 'message';
|
||||
|
||||
const isOpenGroupView = overlayMode === SessionGroupType.Open;
|
||||
const isClosedGroupView = overlayMode === SessionGroupType.Closed;
|
||||
const isAddContactView = overlayMode === SessionClosableOverlayType.Contact;
|
||||
const isMessageView = overlayMode === SessionClosableOverlayType.Message;
|
||||
const isOpenGroupView =
|
||||
overlayMode === SessionClosableOverlayType.OpenGroup;
|
||||
const isClosedGroupView =
|
||||
overlayMode === SessionClosableOverlayType.ClosedGroup;
|
||||
|
||||
let title;
|
||||
let buttonText;
|
||||
|
@ -140,12 +154,14 @@ export class SessionClosableOverlay extends React.Component<Props, State> {
|
|||
default:
|
||||
}
|
||||
|
||||
const { groupName, selectedMembers } = this.state;
|
||||
const { groupName, selectedMembers, senderKeys } = this.state;
|
||||
const ourSessionID = window.textsecure.storage.user.getNumber();
|
||||
|
||||
const contacts = this.getContacts();
|
||||
|
||||
const noContactsForClosedGroup =
|
||||
overlayMode === SessionGroupType.Closed && contacts.length === 0;
|
||||
overlayMode === SessionClosableOverlayType.ClosedGroup &&
|
||||
contacts.length === 0;
|
||||
|
||||
return (
|
||||
<div className="module-left-pane-overlay">
|
||||
|
@ -193,7 +209,6 @@ export class SessionClosableOverlay extends React.Component<Props, State> {
|
|||
{isClosedGroupView && (
|
||||
<>
|
||||
<div className="spacer-lg" />
|
||||
|
||||
<div className="group-member-list__container">
|
||||
{noContactsForClosedGroup ? (
|
||||
<div className="group-member-list__no-contacts">
|
||||
|
@ -201,7 +216,7 @@ export class SessionClosableOverlay extends React.Component<Props, State> {
|
|||
</div>
|
||||
) : (
|
||||
<div className="group-member-list__selection">
|
||||
{this.renderMemberList()}
|
||||
{this.renderMemberList(contacts)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
@ -234,23 +249,43 @@ export class SessionClosableOverlay extends React.Component<Props, State> {
|
|||
/>
|
||||
)}
|
||||
|
||||
{isClosedGroupView && window.lokiFeatureFlags.enableSenderKeys && (
|
||||
<div className="sealed-sender-toggle">
|
||||
<SessionToggle
|
||||
active={Boolean(false)}
|
||||
onClick={() => {
|
||||
const value = this.state.senderKeys;
|
||||
this.setState({ senderKeys: !value });
|
||||
}}
|
||||
/>
|
||||
|
||||
<span
|
||||
className={classNames(
|
||||
'session-settings-item__description',
|
||||
'sender-keys-description'
|
||||
)}
|
||||
>
|
||||
{window.i18n('useSenderKeys')}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<SessionButton
|
||||
buttonColor={SessionButtonColor.Green}
|
||||
buttonType={SessionButtonType.BrandOutline}
|
||||
text={buttonText}
|
||||
disabled={noContactsForClosedGroup}
|
||||
onClick={() => onButtonClick(groupName, selectedMembers)}
|
||||
onClick={() => onButtonClick(groupName, selectedMembers, senderKeys)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
private renderMemberList() {
|
||||
const members = this.getContacts();
|
||||
|
||||
return members.map((member: ContactType) => (
|
||||
private renderMemberList(members: any) {
|
||||
return members.map((member: ContactType, index: number) => (
|
||||
<SessionMemberListItem
|
||||
member={member}
|
||||
index={index}
|
||||
isSelected={false}
|
||||
key={member.id}
|
||||
onSelect={(selectedMember: ContactType) => {
|
||||
|
@ -286,4 +321,11 @@ export class SessionClosableOverlay extends React.Component<Props, State> {
|
|||
groupName: event,
|
||||
});
|
||||
}
|
||||
|
||||
private onKeyUp(event: any) {
|
||||
if (event.key === 'Escape') {
|
||||
window.removeEventListener('keyup', this.onKeyUp);
|
||||
this.props.onCloseClick();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -55,6 +55,7 @@ export class SessionIdEditable extends React.PureComponent<Props> {
|
|||
|
||||
private handleChange(e: any) {
|
||||
const { editable, onChange } = this.props;
|
||||
|
||||
if (editable) {
|
||||
onChange(e.target.value);
|
||||
}
|
||||
|
|
|
@ -18,6 +18,7 @@ export interface ContactType {
|
|||
|
||||
interface Props {
|
||||
member: ContactType;
|
||||
index: number; // index in the list
|
||||
isSelected: boolean;
|
||||
onSelect?: any;
|
||||
onUnselect?: any;
|
||||
|
@ -54,7 +55,11 @@ export class SessionMemberListItem extends React.Component<Props, State> {
|
|||
|
||||
return (
|
||||
<div
|
||||
className={classNames('session-member-item', isSelected && 'selected')}
|
||||
className={classNames(
|
||||
`session-member-item-${this.props.index}`,
|
||||
'session-member-item',
|
||||
isSelected && 'selected'
|
||||
)}
|
||||
onClick={this.handleSelectionAction}
|
||||
role="button"
|
||||
>
|
||||
|
|
|
@ -8,24 +8,27 @@ export const SessionRegistrationView: React.FC = () => (
|
|||
<div className="session-content">
|
||||
<div id="session-toast-container" />
|
||||
<div id="error" className="collapse" />
|
||||
<div className="session-content-close-button">
|
||||
<SessionIconButton
|
||||
iconSize={SessionIconSize.Medium}
|
||||
iconType={SessionIconType.Exit}
|
||||
onClick={() => {
|
||||
window.close();
|
||||
}}
|
||||
/>
|
||||
<div className="session-content-header">
|
||||
<div className="session-content-close-button">
|
||||
<SessionIconButton
|
||||
iconSize={SessionIconSize.Medium}
|
||||
iconType={SessionIconType.Exit}
|
||||
onClick={() => {
|
||||
window.close();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="session-content-session-button">
|
||||
<img alt="brand" src="./images/session/brand.svg" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="session-content-accent">
|
||||
<AccentText />
|
||||
</div>
|
||||
<div className="session-content-registration">
|
||||
<RegistrationTabs />
|
||||
</div>
|
||||
<div className="session-content-session-button">
|
||||
<img alt="brand" src="./images/session/brand.svg" />
|
||||
<div className="session-content-body">
|
||||
<div className="session-content-accent">
|
||||
<AccentText />
|
||||
</div>
|
||||
<div className="session-content-registration">
|
||||
<RegistrationTabs />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -23,6 +23,7 @@ export enum SessionIconType {
|
|||
Microphone = 'microphone',
|
||||
Moon = 'moon',
|
||||
Pencil = 'pencil',
|
||||
Plus = 'plus',
|
||||
Reply = 'reply',
|
||||
Search = 'search',
|
||||
Send = 'send',
|
||||
|
@ -160,6 +161,11 @@ export const icons = {
|
|||
'M4,16.4142136 L4,20 L7.58578644,20 L19.5857864,8 L16,4.41421356 L4,16.4142136 Z M16.7071068,2.29289322 L21.7071068,7.29289322 C22.0976311,7.68341751 22.0976311,8.31658249 21.7071068,8.70710678 L8.70710678,21.7071068 C8.5195704,21.8946432 8.26521649,22 8,22 L3,22 C2.44771525,22 2,21.5522847 2,21 L2,16 C2,15.7347835 2.10535684,15.4804296 2.29289322,15.2928932 L15.2928932,2.29289322 C15.6834175,1.90236893 16.3165825,1.90236893 16.7071068,2.29289322 Z',
|
||||
viewBox: '1 1 21 21',
|
||||
},
|
||||
[SessionIconType.Plus]: {
|
||||
path:
|
||||
'm405.332031 192h-170.664062v-170.667969c0-11.773437-9.558594-21.332031-21.335938-21.332031-11.773437 0-21.332031 9.558594-21.332031 21.332031v170.667969h-170.667969c-11.773437 0-21.332031 9.558594-21.332031 21.332031 0 11.777344 9.558594 21.335938 21.332031 21.335938h170.667969v170.664062c0 11.777344 9.558594 21.335938 21.332031 21.335938 11.777344 0 21.335938-9.558594 21.335938-21.335938v-170.664062h170.664062c11.777344 0 21.335938-9.558594 21.335938-21.335938 0-11.773437-9.558594-21.332031-21.335938-21.332031zm0 0',
|
||||
viewBox: '0 0 427 427',
|
||||
},
|
||||
[SessionIconType.Reply]: {
|
||||
path:
|
||||
'M4,3 C4.55228475,3 5,3.44771525 5,4 L5,4 L5,11 C5,12.6568542 6.34314575,14 8,14 L8,14 L17.585,14 L14.2928932,10.7071068 C13.9324093,10.3466228 13.9046797,9.77939176 14.2097046,9.38710056 L14.2928932,9.29289322 C14.6834175,8.90236893 15.3165825,8.90236893 15.7071068,9.29289322 L15.7071068,9.29289322 L20.7071068,14.2928932 C20.7355731,14.3213595 20.7623312,14.3515341 20.787214,14.3832499 C20.788658,14.3849951 20.7902348,14.3870172 20.7918027,14.389044 C20.8140715,14.4179625 20.8348358,14.4480862 20.8539326,14.4793398 C20.8613931,14.4913869 20.8685012,14.5036056 20.8753288,14.5159379 C20.8862061,14.5357061 20.8966234,14.5561086 20.9063462,14.5769009 C20.914321,14.5939015 20.9218036,14.6112044 20.9287745,14.628664 C20.9366843,14.6484208 20.9438775,14.6682023 20.9504533,14.6882636 C20.9552713,14.7031487 20.9599023,14.7185367 20.9641549,14.734007 C20.9701664,14.7555635 20.9753602,14.7772539 20.9798348,14.7992059 C20.9832978,14.8166247 20.9863719,14.834051 20.9889822,14.8515331 C20.9962388,14.8996379 21,14.9493797 21,15 L20.9962979,14.9137692 C20.9978436,14.9317345 20.9989053,14.9497336 20.9994829,14.9677454 L21,15 C21,15.0112225 20.9998151,15.0224019 20.9994483,15.0335352 C20.9988772,15.050591 20.997855,15.0679231 20.996384,15.0852242 C20.994564,15.1070574 20.9920941,15.1281144 20.9889807,15.1489612 C20.9863719,15.165949 20.9832978,15.1833753 20.9797599,15.2007258 C20.9753602,15.2227461 20.9701664,15.2444365 20.964279,15.2658396 C20.9599023,15.2814633 20.9552713,15.2968513 20.9502619,15.3121425 C20.9438775,15.3317977 20.9366843,15.3515792 20.928896,15.3710585 C20.9218036,15.3887956 20.914321,15.4060985 20.9063266,15.4232215 C20.8974314,15.4421635 20.8879327,15.4609002 20.8778732,15.4792864 C20.8703855,15.4931447 20.862375,15.5070057 20.8540045,15.5207088 C20.8382813,15.546275 20.8215099,15.5711307 20.8036865,15.5951593 C20.774687,15.6343256 20.7425008,15.6717127 20.7071068,15.7071068 L20.787214,15.6167501 C20.7849289,15.6196628 20.7826279,15.6225624 20.7803112,15.625449 L20.7071068,15.7071068 L15.7071068,20.7071068 C15.3165825,21.0976311 14.6834175,21.0976311 14.2928932,20.7071068 C13.9023689,20.3165825 13.9023689,19.6834175 14.2928932,19.2928932 L14.2928932,19.2928932 L17.585,16 L8,16 C5.3112453,16 3.11818189,13.8776933 3.00461951,11.2168896 L3,11 L3,4 C3,3.44771525 3.44771525,3 4,3',
|
||||
|
|
|
@ -64,3 +64,10 @@ interface Window {
|
|||
interface Promise<T> {
|
||||
ignore(): void;
|
||||
}
|
||||
|
||||
// Types also correspond to messages.json keys
|
||||
enum LnsLookupErrorType {
|
||||
lnsTooFewNodes,
|
||||
lnsLookupTimeout,
|
||||
lnsMappingNotFound,
|
||||
}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import { DataMessage } from './DataMessage';
|
||||
import { SignalService } from '../../../../../protobuf';
|
||||
import { ChatMessage } from './ChatMessage';
|
||||
import { TextEncoder } from 'util';
|
||||
import { SignalService } from '../../../../../../protobuf';
|
||||
import { ChatMessage } from '../ChatMessage';
|
||||
import { ClosedGroupMessage } from './ClosedGroupMessage';
|
||||
|
||||
interface ClosedGroupChatMessageParams {
|
||||
identifier?: string;
|
||||
|
@ -9,16 +8,15 @@ interface ClosedGroupChatMessageParams {
|
|||
chatMessage: ChatMessage;
|
||||
}
|
||||
|
||||
export class ClosedGroupChatMessage extends DataMessage {
|
||||
private readonly groupId: string;
|
||||
export class ClosedGroupChatMessage extends ClosedGroupMessage {
|
||||
private readonly chatMessage: ChatMessage;
|
||||
|
||||
constructor(params: ClosedGroupChatMessageParams) {
|
||||
super({
|
||||
timestamp: params.chatMessage.timestamp,
|
||||
identifier: params.identifier,
|
||||
groupId: params.groupId,
|
||||
});
|
||||
this.groupId = params.groupId;
|
||||
this.chatMessage = params.chatMessage;
|
||||
}
|
||||
|
||||
|
@ -26,11 +24,13 @@ export class ClosedGroupChatMessage extends DataMessage {
|
|||
return this.getDefaultTTL();
|
||||
}
|
||||
|
||||
protected groupContextType(): SignalService.GroupContext.Type {
|
||||
return SignalService.GroupContext.Type.DELIVER;
|
||||
}
|
||||
|
||||
protected dataProto(): SignalService.DataMessage {
|
||||
const messageProto = this.chatMessage.dataProto();
|
||||
const id = new TextEncoder().encode(this.groupId);
|
||||
const type = SignalService.GroupContext.Type.DELIVER;
|
||||
messageProto.group = new SignalService.GroupContext({ id, type });
|
||||
messageProto.group = this.groupContext();
|
||||
|
||||
return messageProto;
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
import { DataMessage } from '../DataMessage';
|
||||
import { SignalService } from '../../../../../../protobuf';
|
||||
import { TextEncoder } from 'util';
|
||||
import { MessageParams } from '../../../Message';
|
||||
|
||||
interface ClosedGroupMessageParams extends MessageParams {
|
||||
groupId: string;
|
||||
}
|
||||
|
||||
export abstract class ClosedGroupMessage extends DataMessage {
|
||||
protected readonly groupId: string;
|
||||
|
||||
constructor(params: ClosedGroupMessageParams) {
|
||||
super({
|
||||
timestamp: params.timestamp,
|
||||
identifier: params.identifier,
|
||||
});
|
||||
this.groupId = params.groupId;
|
||||
}
|
||||
|
||||
protected abstract groupContextType(): SignalService.GroupContext.Type;
|
||||
|
||||
protected groupContext(): SignalService.GroupContext {
|
||||
const id = new TextEncoder().encode(this.groupId);
|
||||
const type = this.groupContextType();
|
||||
|
||||
return new SignalService.GroupContext({ id, type });
|
||||
}
|
||||
|
||||
protected dataProto(): SignalService.DataMessage {
|
||||
const dataMessage = new SignalService.DataMessage();
|
||||
dataMessage.group = this.groupContext();
|
||||
|
||||
return dataMessage;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
export * from './ClosedGroupMessage';
|
||||
export * from './ClosedGroupChatMessage';
|
|
@ -1,5 +1,5 @@
|
|||
export * from './ClosedGroupChatMessage';
|
||||
export * from './DataMessage';
|
||||
export * from './DeviceUnlinkMessage';
|
||||
export * from './GroupInvitationMessage';
|
||||
export * from './ChatMessage';
|
||||
export * from './group';
|
||||
|
|
|
@ -0,0 +1,56 @@
|
|||
// TODO: Need to flesh out these functions
|
||||
// Structure of this can be changed for example sticking this all in a class
|
||||
// The reason i haven't done it is to avoid having instances of the protocol, rather you should be able to call the functions directly
|
||||
|
||||
import { SessionResetMessage } from '../messages/outgoing';
|
||||
|
||||
export function hasSession(device: string): boolean {
|
||||
return false; // TODO: Implement
|
||||
}
|
||||
|
||||
export function hasSentSessionRequest(device: string): boolean {
|
||||
// TODO: need a way to keep track of if we've sent a session request
|
||||
// My idea was to use the timestamp of when it was sent but there might be another better approach
|
||||
return false;
|
||||
}
|
||||
|
||||
export async function sendSessionRequestIfNeeded(
|
||||
device: string
|
||||
): Promise<void> {
|
||||
if (hasSession(device) || hasSentSessionRequest(device)) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
// TODO: Call sendSessionRequest with SessionReset
|
||||
return Promise.reject(new Error('Need to implement this function'));
|
||||
}
|
||||
|
||||
export async function sendSessionRequest(
|
||||
message: SessionResetMessage
|
||||
): Promise<void> {
|
||||
// TODO: Optimistically store timestamp of when session request was sent
|
||||
// TODO: Send out the request via MessageSender
|
||||
// TODO: On failure, unset the timestamp
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
export function sessionEstablished(device: string) {
|
||||
// TODO: this is called when we receive an encrypted message from the other user
|
||||
// Maybe it should be renamed to something else
|
||||
// TODO: This should make `hasSentSessionRequest` return `false`
|
||||
}
|
||||
|
||||
export function shouldProcessSessionRequest(
|
||||
device: string,
|
||||
messageTimestamp: number
|
||||
): boolean {
|
||||
// TODO: Need to do the following here
|
||||
// messageTimestamp > session request sent timestamp && messageTimestamp > session request processed timestamp
|
||||
return false;
|
||||
}
|
||||
|
||||
export function sessionRequestProcessed(device: string) {
|
||||
// TODO: this is called when we process the session request
|
||||
// This should store the processed timestamp
|
||||
// Again naming is crap so maybe some other name is better
|
||||
}
|
|
@ -0,0 +1,60 @@
|
|||
import { EventEmitter } from 'events';
|
||||
import {
|
||||
MessageQueueInterface,
|
||||
MessageQueueInterfaceEvents,
|
||||
} from './MessageQueueInterface';
|
||||
import { ContentMessage, OpenGroupMessage } from '../messages/outgoing';
|
||||
import { PendingMessageCache } from './PendingMessageCache';
|
||||
import { JobQueue, TypedEventEmitter } from '../utils';
|
||||
|
||||
export class MessageQueue implements MessageQueueInterface {
|
||||
public readonly events: TypedEventEmitter<MessageQueueInterfaceEvents>;
|
||||
private readonly jobQueues: Map<string, JobQueue> = new Map();
|
||||
private readonly cache: PendingMessageCache;
|
||||
|
||||
constructor() {
|
||||
this.events = new EventEmitter();
|
||||
this.cache = new PendingMessageCache();
|
||||
this.processAllPending();
|
||||
}
|
||||
|
||||
public sendUsingMultiDevice(user: string, message: ContentMessage) {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public send(device: string, message: ContentMessage) {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public sendToGroup(message: ContentMessage | OpenGroupMessage) {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public sendSyncMessage(message: ContentMessage) {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
public processPending(device: string) {
|
||||
// TODO: implement
|
||||
}
|
||||
|
||||
private processAllPending() {
|
||||
// TODO: Get all devices which are pending here
|
||||
}
|
||||
|
||||
private queue(device: string, message: ContentMessage) {
|
||||
// TODO: implement
|
||||
}
|
||||
|
||||
private queueOpenGroupMessage(message: OpenGroupMessage) {
|
||||
// TODO: Do we need to queue open group messages?
|
||||
// If so we can get open group job queue and add the send job here
|
||||
}
|
||||
|
||||
private getJobQueue(device: string): JobQueue {
|
||||
let queue = this.jobQueues.get(device);
|
||||
if (!queue) {
|
||||
queue = new JobQueue();
|
||||
this.jobQueues.set(device, queue);
|
||||
}
|
||||
|
||||
return queue;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
import {
|
||||
ClosedGroupMessage,
|
||||
ContentMessage,
|
||||
OpenGroupMessage,
|
||||
} from '../messages/outgoing';
|
||||
import { RawMessage } from '../types/RawMessage';
|
||||
import { TypedEventEmitter } from '../utils';
|
||||
|
||||
type GroupMessageType = OpenGroupMessage | ClosedGroupMessage;
|
||||
|
||||
export interface MessageQueueInterfaceEvents {
|
||||
success: (message: RawMessage) => void;
|
||||
fail: (message: RawMessage, error: Error) => void;
|
||||
}
|
||||
|
||||
export interface MessageQueueInterface {
|
||||
events: TypedEventEmitter<MessageQueueInterfaceEvents>;
|
||||
sendUsingMultiDevice(user: string, message: ContentMessage): void;
|
||||
send(device: string, message: ContentMessage): void;
|
||||
sendToGroup(message: GroupMessageType): void;
|
||||
sendSyncMessage(message: ContentMessage): void;
|
||||
}
|
|
@ -1,8 +1,8 @@
|
|||
import { getItemById, createOrUpdateItem } from '../../../js/modules/data';
|
||||
import { createOrUpdateItem, getItemById } from '../../../js/modules/data';
|
||||
import { RawMessage } from '../types/RawMessage';
|
||||
import { ContentMessage } from '../messages/outgoing';
|
||||
import * as MessageUtils from '../utils';
|
||||
import { PubKey } from '../types';
|
||||
import * as MessageUtils from '../utils';
|
||||
|
||||
// This is an abstraction for storing pending messages.
|
||||
// Ideally we want to store pending messages in the database so that
|
||||
|
@ -12,19 +12,44 @@ import { PubKey } from '../types';
|
|||
// memory and sync its state with the database on modification (add or remove).
|
||||
|
||||
export class PendingMessageCache {
|
||||
private cache: Array<RawMessage>;
|
||||
public readonly isReady: Promise<boolean>;
|
||||
private cache: Array<any>;
|
||||
|
||||
constructor() {
|
||||
// Load pending messages from the database
|
||||
// You should await init() on making a new PendingMessageCache
|
||||
// You should await isReady on making a new PendingMessageCache
|
||||
// if you'd like to have instant access to the cache
|
||||
this.cache = [];
|
||||
void this.init();
|
||||
this.cache = ['bleep'];
|
||||
|
||||
this.isReady = new Promise(async resolve => {
|
||||
await this.loadFromDB();
|
||||
resolve(true);
|
||||
});
|
||||
}
|
||||
|
||||
public get(): Array<RawMessage> {
|
||||
public getAllPending(): Array<RawMessage> {
|
||||
// Get all pending from cache, sorted with oldest first
|
||||
return this.cache.sort((a, b) => a.timestamp - b.timestamp);
|
||||
return [...this.cache].sort((a, b) => a.timestamp - b.timestamp);
|
||||
}
|
||||
|
||||
public getForDevice(device: PubKey): Array<RawMessage> {
|
||||
const pending = this.cache.filter(m => m.device === device.key);
|
||||
|
||||
return pending.sort((a, b) => a.timestamp - b.timestamp);
|
||||
}
|
||||
|
||||
public getDevices(): Array<PubKey> {
|
||||
// Gets all unique devices with pending messages
|
||||
const pubkeyStrings = [...new Set(this.cache.map(m => m.device))];
|
||||
|
||||
const pubkeys: Array<PubKey> = [];
|
||||
pubkeyStrings.forEach(pubkey => {
|
||||
if (PubKey.validate(pubkey)) {
|
||||
pubkeys.push(new PubKey(pubkey));
|
||||
}
|
||||
});
|
||||
|
||||
return pubkeys;
|
||||
}
|
||||
|
||||
public async add(
|
||||
|
@ -39,7 +64,7 @@ export class PendingMessageCache {
|
|||
}
|
||||
|
||||
this.cache.push(rawMessage);
|
||||
await this.syncCacheWithDB();
|
||||
await this.saveToDB();
|
||||
|
||||
return rawMessage;
|
||||
}
|
||||
|
@ -59,7 +84,7 @@ export class PendingMessageCache {
|
|||
m => m.identifier !== message.identifier
|
||||
);
|
||||
this.cache = updatedCache;
|
||||
await this.syncCacheWithDB();
|
||||
await this.saveToDB();
|
||||
|
||||
return updatedCache;
|
||||
}
|
||||
|
@ -71,39 +96,18 @@ export class PendingMessageCache {
|
|||
);
|
||||
}
|
||||
|
||||
public getForDevice(device: PubKey): Array<RawMessage> {
|
||||
const pending = this.cache.filter(m => m.device === device.key);
|
||||
|
||||
return pending.sort((a, b) => a.timestamp - b.timestamp);
|
||||
}
|
||||
|
||||
public async clear() {
|
||||
// Clears the cache and syncs to DB
|
||||
this.cache = [];
|
||||
await this.syncCacheWithDB();
|
||||
await this.saveToDB();
|
||||
}
|
||||
|
||||
public getDevices(): Array<PubKey> {
|
||||
// Gets all unique devices with pending messages
|
||||
const pubkeyStrings = [...new Set(this.cache.map(m => m.device))];
|
||||
|
||||
const pubkeys: Array<PubKey> = [];
|
||||
pubkeyStrings.forEach(pubkey => {
|
||||
if (PubKey.validate(pubkey)) {
|
||||
pubkeys.push(new PubKey(pubkey));
|
||||
}
|
||||
});
|
||||
|
||||
return pubkeys;
|
||||
}
|
||||
|
||||
public async init() {
|
||||
public async loadFromDB() {
|
||||
const messages = await this.getFromStorage();
|
||||
this.cache = messages;
|
||||
}
|
||||
|
||||
private async getFromStorage(): Promise<Array<RawMessage>> {
|
||||
// tslint:disable-next-line: no-backbone-get-set-outside-model
|
||||
const data = await getItemById('pendingMessages');
|
||||
if (!data || !data.value) {
|
||||
return [];
|
||||
|
@ -111,14 +115,8 @@ export class PendingMessageCache {
|
|||
|
||||
const barePending = JSON.parse(String(data.value));
|
||||
|
||||
// tslint:disable-next-line: no-unnecessary-local-variable
|
||||
const pending = barePending.map((message: any) => {
|
||||
const { identifier, timestamp, device, ttl, encryption } = message;
|
||||
|
||||
// Recreate buffers
|
||||
const rawBuffer = message.plainTextBuffer;
|
||||
const bufferValues: Array<number> = Object.values(rawBuffer);
|
||||
const plainTextBuffer = Uint8Array.from(bufferValues);
|
||||
const { identifier, plainTextBuffer, timestamp, device, ttl, encryption } = message;
|
||||
|
||||
return {
|
||||
identifier,
|
||||
|
@ -133,9 +131,9 @@ export class PendingMessageCache {
|
|||
return pending as Array<RawMessage>;
|
||||
}
|
||||
|
||||
private async syncCacheWithDB() {
|
||||
private async saveToDB() {
|
||||
// Only call when adding / removing from cache.
|
||||
const encodedPendingMessages = JSON.stringify(this.cache) || '';
|
||||
const encodedPendingMessages = JSON.stringify(this.cache) || '[]';
|
||||
await createOrUpdateItem({
|
||||
id: 'pendingMessages',
|
||||
value: encodedPendingMessages,
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"extends": ["../../tslint.json"],
|
||||
"rules": {
|
||||
"no-unused-variable": false
|
||||
}
|
||||
}
|
|
@ -1,7 +1,6 @@
|
|||
import * as crypto from 'crypto';
|
||||
|
||||
export class PubKey {
|
||||
private static readonly PUBKEY_LEN = 66;
|
||||
public static readonly PUBKEY_LEN = 66;
|
||||
private static readonly regex: string = `^05[0-9a-fA-F]{${PubKey.PUBKEY_LEN -
|
||||
2}}$`;
|
||||
public readonly key: string;
|
||||
|
@ -28,12 +27,4 @@ export class PubKey {
|
|||
return false;
|
||||
}
|
||||
|
||||
public static generateFake(): PubKey {
|
||||
// Generates a mock pubkey for testing
|
||||
const numBytes = PubKey.PUBKEY_LEN / 2 - 1;
|
||||
const hexBuffer = crypto.randomBytes(numBytes).toString('hex');
|
||||
const pubkeyString = `05${hexBuffer}`;
|
||||
|
||||
return new PubKey(pubkeyString);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
import uuid from 'uuid';
|
||||
|
||||
import { RawMessage } from '../types/RawMessage';
|
||||
import { ChatMessage, ContentMessage } from '../messages/outgoing';
|
||||
import { ContentMessage } from '../messages/outgoing';
|
||||
import { EncryptionType, PubKey } from '../types';
|
||||
|
||||
export function toRawMessage(
|
||||
|
@ -24,16 +22,3 @@ export function toRawMessage(
|
|||
|
||||
return rawMessage;
|
||||
}
|
||||
|
||||
export function generateUniqueChatMessage(): ChatMessage {
|
||||
return new ChatMessage({
|
||||
body: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit',
|
||||
identifier: uuid(),
|
||||
timestamp: Date.now(),
|
||||
attachments: undefined,
|
||||
quote: undefined,
|
||||
expireTimer: undefined,
|
||||
lokiProfile: undefined,
|
||||
preview: undefined,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -55,6 +55,7 @@ export type ConversationType = {
|
|||
isFriend?: boolean;
|
||||
isSecondary?: boolean;
|
||||
primaryDevice: string;
|
||||
isPendingFriendRequest?: boolean;
|
||||
hasReceivedFriendRequest?: boolean;
|
||||
hasSentFriendRequest?: boolean;
|
||||
};
|
||||
|
|
|
@ -129,7 +129,15 @@ export const _getLeftPaneLists = (
|
|||
}
|
||||
|
||||
if (conversation.hasReceivedFriendRequest) {
|
||||
allReceivedFriendsRequest.push(conversation);
|
||||
// Friend requests should always appear as coming from primary
|
||||
const primaryConversation =
|
||||
conversations.find(c => c.id === conversation.primaryDevice) ||
|
||||
conversation;
|
||||
primaryConversation.hasReceivedFriendRequest =
|
||||
conversation.hasReceivedFriendRequest;
|
||||
primaryConversation.isPendingFriendRequest =
|
||||
conversation.isPendingFriendRequest;
|
||||
allReceivedFriendsRequest.push(primaryConversation);
|
||||
} else if (
|
||||
unreadCount < 9 &&
|
||||
conversation.isFriend &&
|
||||
|
@ -139,7 +147,9 @@ export const _getLeftPaneLists = (
|
|||
}
|
||||
if (conversation.hasSentFriendRequest) {
|
||||
if (!conversation.isFriend) {
|
||||
allSentFriendsRequest.push(conversation);
|
||||
if (!conversation.isSecondary) {
|
||||
allSentFriendsRequest.push(conversation);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -160,22 +170,23 @@ export const _getLeftPaneLists = (
|
|||
group: Array<ConversationType | ConversationListItemPropsType>
|
||||
): T => {
|
||||
const secondariesToRemove: Array<string> = [];
|
||||
|
||||
group.forEach(device => {
|
||||
if (!device.isSecondary) {
|
||||
return;
|
||||
}
|
||||
|
||||
const devicePrimary = group.find(c => c.id === device.primaryDevice);
|
||||
|
||||
// Remove secondary where primary already exists in group
|
||||
if (group.some(c => c === devicePrimary)) {
|
||||
secondariesToRemove.push(device.id);
|
||||
}
|
||||
});
|
||||
|
||||
// tslint:disable-next-line: no-unnecessary-local-variable
|
||||
const filteredGroup = group.filter(
|
||||
c => !secondariesToRemove.find(s => s === c.id)
|
||||
);
|
||||
const filteredGroup = [
|
||||
...new Set(group.filter(c => !secondariesToRemove.find(s => s === c.id))),
|
||||
];
|
||||
|
||||
return filteredGroup as T;
|
||||
};
|
||||
|
|
|
@ -3,7 +3,6 @@ import { expect } from 'chai';
|
|||
import * as MessageUtils from '../../../session/utils';
|
||||
import { TestUtils } from '../../../test/test-utils';
|
||||
import { PendingMessageCache } from '../../../session/sending/PendingMessageCache';
|
||||
import { PubKey } from '../../../session/types';
|
||||
|
||||
// Equivalent to Data.StorageItem
|
||||
interface StorageItem {
|
||||
|
@ -30,11 +29,13 @@ describe('PendingMessageCache', () => {
|
|||
});
|
||||
|
||||
TestUtils.stubData('createOrUpdateItem').callsFake((item: StorageItem) => {
|
||||
data = item;
|
||||
if (item.id === storageID) {
|
||||
data = item;
|
||||
}
|
||||
});
|
||||
|
||||
pendingMessageCacheStub = new PendingMessageCache();
|
||||
await pendingMessageCacheStub.init();
|
||||
await pendingMessageCacheStub.isReady;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
@ -42,7 +43,7 @@ describe('PendingMessageCache', () => {
|
|||
});
|
||||
|
||||
it('can initialize cache', async () => {
|
||||
const cache = pendingMessageCacheStub.get();
|
||||
const cache = pendingMessageCacheStub.getAllPending();
|
||||
|
||||
// We expect the cache to initialise as an empty array
|
||||
expect(cache).to.be.instanceOf(Array);
|
||||
|
@ -50,14 +51,14 @@ describe('PendingMessageCache', () => {
|
|||
});
|
||||
|
||||
it('can add to cache', async () => {
|
||||
const device = PubKey.generateFake();
|
||||
const message = MessageUtils.generateUniqueChatMessage();
|
||||
const device = TestUtils.generateFakePubkey();
|
||||
const message = TestUtils.generateUniqueChatMessage();
|
||||
const rawMessage = MessageUtils.toRawMessage(device, message);
|
||||
|
||||
await pendingMessageCacheStub.add(device, message);
|
||||
|
||||
// Verify that the message is in the cache
|
||||
const finalCache = pendingMessageCacheStub.get();
|
||||
const finalCache = pendingMessageCacheStub.getAllPending();
|
||||
|
||||
expect(finalCache).to.have.length(1);
|
||||
|
||||
|
@ -67,19 +68,19 @@ describe('PendingMessageCache', () => {
|
|||
});
|
||||
|
||||
it('can remove from cache', async () => {
|
||||
const device = PubKey.generateFake();
|
||||
const message = MessageUtils.generateUniqueChatMessage();
|
||||
const device = TestUtils.generateFakePubkey();
|
||||
const message = TestUtils.generateUniqueChatMessage();
|
||||
const rawMessage = MessageUtils.toRawMessage(device, message);
|
||||
|
||||
await pendingMessageCacheStub.add(device, message);
|
||||
|
||||
const initialCache = pendingMessageCacheStub.get();
|
||||
const initialCache = pendingMessageCacheStub.getAllPending();
|
||||
expect(initialCache).to.have.length(1);
|
||||
|
||||
// Remove the message
|
||||
await pendingMessageCacheStub.remove(rawMessage);
|
||||
|
||||
const finalCache = pendingMessageCacheStub.get();
|
||||
const finalCache = pendingMessageCacheStub.getAllPending();
|
||||
|
||||
// Verify that the message was removed
|
||||
expect(finalCache).to.have.length(0);
|
||||
|
@ -88,16 +89,16 @@ describe('PendingMessageCache', () => {
|
|||
it('can get devices', async () => {
|
||||
const cacheItems = [
|
||||
{
|
||||
device: PubKey.generateFake(),
|
||||
message: MessageUtils.generateUniqueChatMessage(),
|
||||
device: TestUtils.generateFakePubkey(),
|
||||
message: TestUtils.generateUniqueChatMessage(),
|
||||
},
|
||||
{
|
||||
device: PubKey.generateFake(),
|
||||
message: MessageUtils.generateUniqueChatMessage(),
|
||||
device: TestUtils.generateFakePubkey(),
|
||||
message: TestUtils.generateUniqueChatMessage(),
|
||||
},
|
||||
{
|
||||
device: PubKey.generateFake(),
|
||||
message: MessageUtils.generateUniqueChatMessage(),
|
||||
device: TestUtils.generateFakePubkey(),
|
||||
message: TestUtils.generateUniqueChatMessage(),
|
||||
},
|
||||
];
|
||||
|
||||
|
@ -105,7 +106,7 @@ describe('PendingMessageCache', () => {
|
|||
await pendingMessageCacheStub.add(item.device, item.message);
|
||||
});
|
||||
|
||||
const cache = pendingMessageCacheStub.get();
|
||||
const cache = pendingMessageCacheStub.getAllPending();
|
||||
expect(cache).to.have.length(cacheItems.length);
|
||||
|
||||
// Get list of devices
|
||||
|
@ -120,12 +121,12 @@ describe('PendingMessageCache', () => {
|
|||
it('can get pending for device', async () => {
|
||||
const cacheItems = [
|
||||
{
|
||||
device: PubKey.generateFake(),
|
||||
message: MessageUtils.generateUniqueChatMessage(),
|
||||
device: TestUtils.generateFakePubkey(),
|
||||
message: TestUtils.generateUniqueChatMessage(),
|
||||
},
|
||||
{
|
||||
device: PubKey.generateFake(),
|
||||
message: MessageUtils.generateUniqueChatMessage(),
|
||||
device: TestUtils.generateFakePubkey(),
|
||||
message: TestUtils.generateUniqueChatMessage(),
|
||||
},
|
||||
];
|
||||
|
||||
|
@ -133,7 +134,7 @@ describe('PendingMessageCache', () => {
|
|||
await pendingMessageCacheStub.add(item.device, item.message);
|
||||
});
|
||||
|
||||
const initialCache = pendingMessageCacheStub.get();
|
||||
const initialCache = pendingMessageCacheStub.getAllPending();
|
||||
expect(initialCache).to.have.length(cacheItems.length);
|
||||
|
||||
// Get pending for each specific device
|
||||
|
@ -147,8 +148,8 @@ describe('PendingMessageCache', () => {
|
|||
});
|
||||
|
||||
it('can find nothing when empty', async () => {
|
||||
const device = PubKey.generateFake();
|
||||
const message = MessageUtils.generateUniqueChatMessage();
|
||||
const device = TestUtils.generateFakePubkey();
|
||||
const message = TestUtils.generateUniqueChatMessage();
|
||||
const rawMessage = MessageUtils.toRawMessage(device, message);
|
||||
|
||||
const foundMessage = pendingMessageCacheStub.find(rawMessage);
|
||||
|
@ -156,13 +157,13 @@ describe('PendingMessageCache', () => {
|
|||
});
|
||||
|
||||
it('can find message in cache', async () => {
|
||||
const device = PubKey.generateFake();
|
||||
const message = MessageUtils.generateUniqueChatMessage();
|
||||
const device = TestUtils.generateFakePubkey();
|
||||
const message = TestUtils.generateUniqueChatMessage();
|
||||
const rawMessage = MessageUtils.toRawMessage(device, message);
|
||||
|
||||
await pendingMessageCacheStub.add(device, message);
|
||||
|
||||
const finalCache = pendingMessageCacheStub.get();
|
||||
const finalCache = pendingMessageCacheStub.getAllPending();
|
||||
expect(finalCache).to.have.length(1);
|
||||
|
||||
const foundMessage = pendingMessageCacheStub.find(rawMessage);
|
||||
|
@ -173,16 +174,16 @@ describe('PendingMessageCache', () => {
|
|||
it('can clear cache', async () => {
|
||||
const cacheItems = [
|
||||
{
|
||||
device: PubKey.generateFake(),
|
||||
message: MessageUtils.generateUniqueChatMessage(),
|
||||
device: TestUtils.generateFakePubkey(),
|
||||
message: TestUtils.generateUniqueChatMessage(),
|
||||
},
|
||||
{
|
||||
device: PubKey.generateFake(),
|
||||
message: MessageUtils.generateUniqueChatMessage(),
|
||||
device: TestUtils.generateFakePubkey(),
|
||||
message: TestUtils.generateUniqueChatMessage(),
|
||||
},
|
||||
{
|
||||
device: PubKey.generateFake(),
|
||||
message: MessageUtils.generateUniqueChatMessage(),
|
||||
device: TestUtils.generateFakePubkey(),
|
||||
message: TestUtils.generateUniqueChatMessage(),
|
||||
},
|
||||
];
|
||||
|
||||
|
@ -190,13 +191,13 @@ describe('PendingMessageCache', () => {
|
|||
await pendingMessageCacheStub.add(item.device, item.message);
|
||||
});
|
||||
|
||||
const initialCache = pendingMessageCacheStub.get();
|
||||
const initialCache = pendingMessageCacheStub.getAllPending();
|
||||
expect(initialCache).to.have.length(cacheItems.length);
|
||||
|
||||
// Clear cache
|
||||
await pendingMessageCacheStub.clear();
|
||||
|
||||
const finalCache = pendingMessageCacheStub.get();
|
||||
const finalCache = pendingMessageCacheStub.getAllPending();
|
||||
expect(finalCache).to.have.length(0);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,7 +1,13 @@
|
|||
import * as sinon from 'sinon';
|
||||
import { ImportMock } from 'ts-mock-imports';
|
||||
import * as DataShape from '../../../js/modules/data';
|
||||
import * as crypto from 'crypto';
|
||||
import * as window from '../../window';
|
||||
import * as DataShape from '../../../js/modules/data';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
import { ImportMock } from 'ts-mock-imports';
|
||||
import { PubKey } from '../../../ts/session/types';
|
||||
import { ChatMessage } from '../../session/messages/outgoing';
|
||||
|
||||
|
||||
const sandbox = sinon.createSandbox();
|
||||
|
||||
|
@ -40,3 +46,25 @@ export function restoreStubs() {
|
|||
ImportMock.restore();
|
||||
sandbox.restore();
|
||||
}
|
||||
|
||||
export function generateFakePubkey(): PubKey {
|
||||
// Generates a mock pubkey for testing
|
||||
const numBytes = PubKey.PUBKEY_LEN / 2 - 1;
|
||||
const hexBuffer = crypto.randomBytes(numBytes).toString('hex');
|
||||
const pubkeyString = `05${hexBuffer}`;
|
||||
|
||||
return new PubKey(pubkeyString);
|
||||
}
|
||||
|
||||
export function generateUniqueChatMessage(): ChatMessage {
|
||||
return new ChatMessage({
|
||||
body: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit',
|
||||
identifier: uuid(),
|
||||
timestamp: Date.now(),
|
||||
attachments: undefined,
|
||||
quote: undefined,
|
||||
expireTimer: undefined,
|
||||
lokiProfile: undefined,
|
||||
preview: undefined,
|
||||
});
|
||||
}
|
Loading…
Reference in New Issue