Merge branch 'clearnet' into fileonion
commit
a22dbbc37e
@ -0,0 +1,3 @@
|
||||
[submodule "session-file-server"]
|
||||
path = session-file-server
|
||||
url = https://github.com/loki-project/session-file-server/
|
File diff suppressed because it is too large
Load Diff
@ -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 app2Props = {
|
||||
mnemonic: common.TEST_MNEMONIC2,
|
||||
displayName: common.TEST_DISPLAY_NAME2,
|
||||
stubSnode: true,
|
||||
};
|
||||
|
||||
[app, app2] = await Promise.all([
|
||||
common.startAndStub(app1Props),
|
||||
common.startAndStubN(app2Props, 2),
|
||||
]);
|
||||
const alice2Props = {};
|
||||
|
||||
[Alice1, Bob1] = await common.startAppsAsFriends(); // Alice and Bob are friends
|
||||
|
||||
await common.addFriendToNewClosedGroup([Alice1, Bob1], false);
|
||||
await common.joinOpenGroup(
|
||||
Alice1,
|
||||
common.VALID_GROUP_URL,
|
||||
common.VALID_GROUP_NAME
|
||||
);
|
||||
|
||||
Alice2 = await common.startAndStubN(alice2Props, 4); // Alice secondary, just start the app for now. no linking
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
after(async () => {
|
||||
await common.killallElectron();
|
||||
await common.stopStubSnodeServer();
|
||||
});
|
||||
|
||||
it('message syncing between linked devices', async () => {
|
||||
await common.linkApp2ToApp(app, app2);
|
||||
});
|
||||
it('message syncing with 1 friend, 1 closed group, 1 open group', async () => {
|
||||
// Alice1 has:
|
||||
// * no linked device
|
||||
// * Bob is a friend
|
||||
// * one open group
|
||||
// * one closed group with Bob inside
|
||||
|
||||
// Bob1 has:
|
||||
// * no linked device
|
||||
// * Alice as a friend
|
||||
// * one open group with Alice
|
||||
|
||||
// Linking Alice2 to Alice1
|
||||
// alice2 should trigger auto FR with bob1 as it's one of her friend
|
||||
// and alice2 should trigger a SESSION_REQUEST with bob1 as he is in a closed group with her
|
||||
await common.linkApp2ToApp(Alice1, Alice2, common.TEST_PUBKEY1);
|
||||
await common.timeout(25000);
|
||||
|
||||
// validate pubkey of app2 is the set
|
||||
const alice2Pubkey = await Alice2.webContents.executeJavaScript(
|
||||
'window.textsecure.storage.user.getNumber()'
|
||||
);
|
||||
alice2Pubkey.should.have.lengthOf(66);
|
||||
|
||||
const alice1Logs = await Alice1.client.getRenderProcessLogs();
|
||||
const bob1Logs = await Bob1.client.getRenderProcessLogs();
|
||||
const alice2Logs = await Alice2.client.getRenderProcessLogs();
|
||||
|
||||
// validate primary alice
|
||||
await common.logsContains(
|
||||
alice1Logs,
|
||||
'Sending closed-group-sync-send:outgoing message to OUR SECONDARY PUBKEY',
|
||||
1
|
||||
);
|
||||
await common.logsContains(
|
||||
alice1Logs,
|
||||
'Sending open-group-sync-send:outgoing message to OUR SECONDARY PUBKEY',
|
||||
1
|
||||
);
|
||||
await common.logsContains(
|
||||
alice1Logs,
|
||||
'Sending contact-sync-send:outgoing message to OUR SECONDARY PUBKEY',
|
||||
1
|
||||
);
|
||||
|
||||
// validate secondary alice
|
||||
// what is expected is
|
||||
// alice2 receives group sync, contact sync and open group sync
|
||||
// alice2 triggers session request with closed group members and autoFR with contact sync received
|
||||
// once autoFR is auto-accepted, alice2 trigger contact sync
|
||||
await common.logsContains(
|
||||
alice2Logs,
|
||||
'Got sync group message with group id',
|
||||
1
|
||||
);
|
||||
await common.logsContains(
|
||||
alice2Logs,
|
||||
'Received GROUP_SYNC with open groups: [chat.getsession.org]',
|
||||
1
|
||||
);
|
||||
await common.logsContains(
|
||||
alice2Logs,
|
||||
`Sending auto-friend-request:friend-request message to ${common.TEST_PUBKEY2}`,
|
||||
1
|
||||
);
|
||||
await common.logsContains(
|
||||
alice2Logs,
|
||||
`Sending session-request:friend-request message to ${common.TEST_PUBKEY2}`,
|
||||
1
|
||||
);
|
||||
await common.logsContains(
|
||||
alice2Logs,
|
||||
`Sending contact-sync-send:outgoing message to OUR_PRIMARY_PUBKEY`,
|
||||
1
|
||||
);
|
||||
|
||||
it('unlink two devices', async () => {
|
||||
await common.linkApp2ToApp(app, app2);
|
||||
await common.timeout(1000);
|
||||
await common.triggerUnlinkApp2FromApp(app, app2);
|
||||
// validate primary bob
|
||||
// what is expected is
|
||||
// bob1 receives session request from alice2
|
||||
// bob1 accept auto fr by sending a bg message
|
||||
// once autoFR is auto-accepted, alice2 trigger contact sync
|
||||
await common.logsContains(
|
||||
bob1Logs,
|
||||
`Received SESSION_REQUEST from source: ${alice2Pubkey}`,
|
||||
1
|
||||
);
|
||||
await common.logsContains(
|
||||
bob1Logs,
|
||||
`Received AUTO_FRIEND_REQUEST from source: ${alice2Pubkey}`,
|
||||
1
|
||||
);
|
||||
await common.logsContains(
|
||||
bob1Logs,
|
||||
`Sending auto-friend-accept:onlineBroadcast message to ${alice2Pubkey}`,
|
||||
1
|
||||
);
|
||||
// be sure only one autoFR accept was sent (even if multi device, we need to reply to that specific device only)
|
||||
await common.logsContains(
|
||||
bob1Logs,
|
||||
`Sending auto-friend-accept:onlineBroadcast message to`,
|
||||
1
|
||||
);
|
||||
});
|
||||
});
|
||||
|
@ -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);
|
||||
});
|
@ -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;
|
@ -0,0 +1,9 @@
|
||||
/* eslint-disable class-methods-use-this */
|
||||
|
||||
class StubSnodeAPI {
|
||||
async refreshSwarmNodesForPubKey() {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = StubSnodeAPI;
|
@ -1,3 +1,418 @@
|
||||
import { ConversationType } from '../../ts/state/ducks/conversations';
|
||||
import { Mesasge } from '../../ts/types/Message';
|
||||
|
||||
type IdentityKey = {
|
||||
id: string;
|
||||
publicKey: ArrayBuffer;
|
||||
firstUse: boolean;
|
||||
verified: number;
|
||||
nonblockingApproval: boolean;
|
||||
};
|
||||
|
||||
type PreKey = {
|
||||
id: number;
|
||||
publicKey: ArrayBuffer;
|
||||
privateKey: ArrayBuffer;
|
||||
recipient: string;
|
||||
};
|
||||
|
||||
type SignedPreKey = {
|
||||
id: number;
|
||||
publicKey: ArrayBuffer;
|
||||
privateKey: ArrayBuffer;
|
||||
created_at: number;
|
||||
confirmed: boolean;
|
||||
signature: ArrayBuffer;
|
||||
};
|
||||
|
||||
type ContactPreKey = {
|
||||
id: number;
|
||||
identityKeyString: string;
|
||||
publicKey: ArrayBuffer;
|
||||
keyId: number;
|
||||
};
|
||||
|
||||
type ContactSignedPreKey = {
|
||||
id: number;
|
||||
identityKeyString: string;
|
||||
publicKey: ArrayBuffer;
|
||||
keyId: number;
|
||||
signature: ArrayBuffer;
|
||||
created_at: number;
|
||||
confirmed: boolean;
|
||||
};
|
||||
|
||||
type PairingAuthorisation = {
|
||||
primaryDevicePubKey: string;
|
||||
secondaryDevicePubKey: string;
|
||||
requestSignature: ArrayBuffer;
|
||||
grantSignature: ArrayBuffer | null;
|
||||
};
|
||||
|
||||
type GuardNode = {
|
||||
ed25519PubKey: string;
|
||||
};
|
||||
|
||||
type SwarmNode = {
|
||||
address: string;
|
||||
ip: string;
|
||||
port: string;
|
||||
pubkey_ed25519: string;
|
||||
pubkey_x25519: string;
|
||||
};
|
||||
|
||||
type StorageItem = {
|
||||
id: string;
|
||||
value: any;
|
||||
};
|
||||
|
||||
type SessionDataInfo = {
|
||||
id: string;
|
||||
number: string;
|
||||
deviceId: number;
|
||||
record: string;
|
||||
};
|
||||
|
||||
type ServerToken = {
|
||||
serverUrl: string;
|
||||
token: string;
|
||||
};
|
||||
|
||||
// Basic
|
||||
export function searchMessages(query: string): Promise<Array<any>>;
|
||||
export function searchConversations(query: string): Promise<Array<any>>;
|
||||
export function getPrimaryDeviceFor(pubKey: string): Promise<string | null>;
|
||||
export function shutdown(): Promise<void>;
|
||||
export function close(): Promise<void>;
|
||||
export function removeDB(): Promise<void>;
|
||||
export function removeIndexedDBFiles(): Promise<void>;
|
||||
export function getPasswordHash(): Promise<string | null>;
|
||||
|
||||
// Identity Keys
|
||||
export function createOrUpdateIdentityKey(data: IdentityKey): Promise<void>;
|
||||
export function getIdentityKeyById(id: string): Promise<IdentityKey | null>;
|
||||
export function bulkAddIdentityKeys(array: Array<IdentityKey>): Promise<void>;
|
||||
export function removeIdentityKeyById(id: string): Promise<void>;
|
||||
export function removeAllIdentityKeys(): Promise<void>;
|
||||
|
||||
// Pre Keys
|
||||
export function createOrUpdatePreKey(data: PreKey): Promise<void>;
|
||||
export function getPreKeyById(id: number): Promise<PreKey | null>;
|
||||
export function getPreKeyByRecipient(recipient: string): Promise<PreKey | null>;
|
||||
export function bulkAddPreKeys(data: Array<PreKey>): Promise<void>;
|
||||
export function removePreKeyById(id: number): Promise<void>;
|
||||
export function getAllPreKeys(): Promise<Array<PreKey>>;
|
||||
|
||||
// Signed Pre Keys
|
||||
export function createOrUpdateSignedPreKey(data: SignedPreKey): Promise<void>;
|
||||
export function getSignedPreKeyById(id: number): Promise<SignedPreKey | null>;
|
||||
export function getAllSignedPreKeys(): Promise<SignedPreKey | null>;
|
||||
export function bulkAddSignedPreKeys(array: Array<SignedPreKey>): Promise<void>;
|
||||
export function removeSignedPreKeyById(id: number): Promise<void>;
|
||||
export function removeAllSignedPreKeys(): Promise<void>;
|
||||
|
||||
// Contact Pre Key
|
||||
export function createOrUpdateContactPreKey(data: ContactPreKey): Promise<void>;
|
||||
export function getContactPreKeyById(id: number): Promise<ContactPreKey | null>;
|
||||
export function getContactPreKeyByIdentityKey(
|
||||
key: string
|
||||
): Promise<ContactPreKey | null>;
|
||||
export function getContactPreKeys(
|
||||
keyId: number,
|
||||
identityKeyString: string
|
||||
): Promise<Array<ContactPreKey>>;
|
||||
export function getAllContactPreKeys(): Promise<Array<ContactPreKey>>;
|
||||
export function bulkAddContactPreKeys(
|
||||
array: Array<ContactPreKey>
|
||||
): Promise<void>;
|
||||
export function removeContactPreKeyByIdentityKey(id: number): Promise<void>;
|
||||
export function removeAllContactPreKeys(): Promise<void>;
|
||||
|
||||
// Contact Signed Pre Key
|
||||
export function createOrUpdateContactSignedPreKey(
|
||||
data: ContactSignedPreKey
|
||||
): Promise<void>;
|
||||
export function getContactSignedPreKeyById(
|
||||
id: number
|
||||
): Promise<ContactSignedPreKey | null>;
|
||||
export function getContactSignedPreKeyByIdentityKey(
|
||||
key: string
|
||||
): Promise<ContactSignedPreKey | null>;
|
||||
export function getContactSignedPreKeys(
|
||||
keyId: number,
|
||||
identityKeyString: string
|
||||
): Promise<Array<ContactSignedPreKey>>;
|
||||
export function bulkAddContactSignedPreKeys(
|
||||
array: Array<ContactSignedPreKey>
|
||||
): Promise<void>;
|
||||
export function removeContactSignedPreKeyByIdentityKey(
|
||||
id: string
|
||||
): Promise<void>;
|
||||
export function removeAllContactSignedPreKeys(): Promise<void>;
|
||||
|
||||
// Authorisations & Linking
|
||||
export function createOrUpdatePairingAuthorisation(
|
||||
data: PairingAuthorisation
|
||||
): Promise<void>;
|
||||
export function removePairingAuthorisationForSecondaryPubKey(
|
||||
pubKey: string
|
||||
): Promise<void>;
|
||||
export function getGrantAuthorisationsForPrimaryPubKey(
|
||||
pubKey: string
|
||||
): Promise<Array<PairingAuthorisation>>;
|
||||
export function getGrantAuthorisationForSecondaryPubKey(
|
||||
pubKey: string
|
||||
): Promise<PairingAuthorisation | null>;
|
||||
export function getAuthorisationForSecondaryPubKey(
|
||||
pubKey: string
|
||||
): Promise<PairingAuthorisation | null>;
|
||||
export function getSecondaryDevicesFor(
|
||||
primaryDevicePubKey: string
|
||||
): Promise<Array<string>>;
|
||||
export function getPrimaryDeviceFor(
|
||||
secondaryDevicePubKey: string
|
||||
): Promise<string | null>;
|
||||
export function getPairedDevicesFor(pubKey: string): Promise<Array<string>>;
|
||||
|
||||
// Guard Nodes
|
||||
export function getGuardNodes(): Promise<GuardNode>;
|
||||
export function updateGuardNodes(nodes: Array<string>): Promise<void>;
|
||||
|
||||
// Storage Items
|
||||
export function createOrUpdateItem(data: StorageItem): Promise<void>;
|
||||
export function getItemById(id: string): Promise<StorageItem | undefined>;
|
||||
export function getAlItems(): Promise<Array<StorageItem>>;
|
||||
export function bulkAddItems(array: Array<StorageItem>): Promise<void>;
|
||||
export function removeItemById(id: string): Promise<void>;
|
||||
export function removeAllItems(): Promise<void>;
|
||||
|
||||
// Sessions
|
||||
export function createOrUpdateSession(data: SessionDataInfo): Promise<void>;
|
||||
export function getAllSessions(): Promise<Array<SessionDataInfo>>;
|
||||
export function getSessionById(id: string): Promise<SessionDataInfo>;
|
||||
export function getSessionsByNumber(number: string): Promise<SessionDataInfo>;
|
||||
export function bulkAddSessions(array: Array<SessionDataInfo>): Promise<void>;
|
||||
export function removeSessionById(id: string): Promise<void>;
|
||||
export function removeSessionsByNumber(number: string): Promise<void>;
|
||||
export function removeAllSessions(): Promise<void>;
|
||||
|
||||
// Conversations
|
||||
export function getConversationCount(): Promise<number>;
|
||||
export function saveConversation(data: ConversationType): Promise<void>;
|
||||
export function saveConversations(data: Array<ConversationType>): Promise<void>;
|
||||
export function updateConversation(data: ConversationType): Promise<void>;
|
||||
export function removeConversation(id: string): Promise<void>;
|
||||
|
||||
export function getAllConversations({
|
||||
ConversationCollection,
|
||||
}: {
|
||||
ConversationCollection: any;
|
||||
}): Promise<Array<ConversationCollection>>;
|
||||
|
||||
export function getAllConversationIds(): Promise<Array<string>>;
|
||||
export function getAllPrivateConversations(): Promise<Array<string>>;
|
||||
export function getAllPublicConversations(): Promise<Array<string>>;
|
||||
export function getPublicConversationsByServer(
|
||||
server: string,
|
||||
{ ConversationCollection }: { ConversationCollection: any }
|
||||
): Promise<ConversationCollection>;
|
||||
export function getPubkeysInPublicConversation(
|
||||
id: string
|
||||
): Promise<Array<string>>;
|
||||
export function savePublicServerToken(data: ServerToken): Promise<void>;
|
||||
export function getPublicServerTokenByServerUrl(
|
||||
serverUrl: string
|
||||
): Promise<string>;
|
||||
export function getAllGroupsInvolvingId(
|
||||
id: string,
|
||||
{ ConversationCollection }: { ConversationCollection: any }
|
||||
): Promise<Array<ConversationCollection>>;
|
||||
|
||||
// Returns conversation row
|
||||
// TODO: Make strict return types for search
|
||||
export function searchConversations(query: string): Promise<any>;
|
||||
export function searchMessages(query: string): Promise<any>;
|
||||
export function searchMessagesInConversation(
|
||||
query: string,
|
||||
conversationId: string,
|
||||
{ limit }?: { limit: any }
|
||||
): Promise<any>;
|
||||
export function getMessageCount(): Promise<number>;
|
||||
export function saveMessage(
|
||||
data: Mesasge,
|
||||
{ forceSave, Message }?: { forceSave: any; Message: any }
|
||||
): Promise<string>;
|
||||
export function cleanSeenMessages(): Promise<void>;
|
||||
export function cleanLastHashes(): Promise<void>;
|
||||
export function saveSeenMessageHash(data: {
|
||||
expiresAt: number;
|
||||
hash: string;
|
||||
}): Promise<void>;
|
||||
|
||||
// TODO: Strictly type the following
|
||||
export function updateLastHash(data: any): Promise<any>;
|
||||
export function saveSeenMessageHashes(data: any): Promise<any>;
|
||||
export function saveLegacyMessage(data: any): Promise<any>;
|
||||
export function saveMessages(
|
||||
arrayOfMessages: any,
|
||||
{ forceSave }?: any
|
||||
): Promise<any>;
|
||||
export function removeMessage(id: string, { Message }?: any): Promise<any>;
|
||||
export function getUnreadByConversation(
|
||||
conversationId: string,
|
||||
{ MessageCollection }?: any
|
||||
): Promise<any>;
|
||||
export function removeAllMessagesInConversation(
|
||||
conversationId: string,
|
||||
{ MessageCollection }?: any
|
||||
): Promise<void>;
|
||||
|
||||
export function getMessageBySender(
|
||||
{
|
||||
source,
|
||||
sourceDevice,
|
||||
sent_at,
|
||||
}: { source: any; sourceDevice: any; sent_at: any },
|
||||
{ Message }: { Message: any }
|
||||
): Promise<any>;
|
||||
export function getMessageIdsFromServerIds(
|
||||
serverIds: any,
|
||||
conversationId: any
|
||||
): Promise<any>;
|
||||
export function getMessageById(
|
||||
id: string,
|
||||
{ Message }: { Message: any }
|
||||
): Promise<any>;
|
||||
export function getAllMessages({
|
||||
MessageCollection,
|
||||
}: {
|
||||
MessageCollection: any;
|
||||
}): Promise<any>;
|
||||
export function getAllUnsentMessages({
|
||||
MessageCollection,
|
||||
}: {
|
||||
MessageCollection: any;
|
||||
}): Promise<any>;
|
||||
export function getAllMessageIds(): Promise<any>;
|
||||
export function getMessagesBySentAt(
|
||||
sentAt: any,
|
||||
{ MessageCollection }: { MessageCollection: any }
|
||||
): Promise<any>;
|
||||
export function getExpiredMessages({
|
||||
MessageCollection,
|
||||
}: {
|
||||
MessageCollection: any;
|
||||
}): Promise<any>;
|
||||
export function getOutgoingWithoutExpiresAt({
|
||||
MessageCollection,
|
||||
}: any): Promise<any>;
|
||||
export function getNextExpiringMessage({
|
||||
MessageCollection,
|
||||
}: {
|
||||
MessageCollection: any;
|
||||
}): Promise<any>;
|
||||
export function getNextExpiringMessage({
|
||||
MessageCollection,
|
||||
}: {
|
||||
MessageCollection: any;
|
||||
}): Promise<any>;
|
||||
export function getMessagesByConversation(
|
||||
conversationId: any,
|
||||
{
|
||||
limit,
|
||||
receivedAt,
|
||||
MessageCollection,
|
||||
type,
|
||||
}: {
|
||||
limit?: number;
|
||||
receivedAt?: number;
|
||||
MessageCollection: any;
|
||||
type?: string;
|
||||
}
|
||||
): Promise<any>;
|
||||
export function getSeenMessagesByHashList(hashes: any): Promise<any>;
|
||||
export function getLastHashBySnode(convoId: any, snode: any): Promise<any>;
|
||||
|
||||
// Unprocessed
|
||||
export function getUnprocessedCount(): Promise<any>;
|
||||
export function getAllUnprocessed(): Promise<any>;
|
||||
export function getUnprocessedById(id: any): Promise<any>;
|
||||
export function saveUnprocessed(
|
||||
data: any,
|
||||
{
|
||||
forceSave,
|
||||
}?: {
|
||||
forceSave: any;
|
||||
}
|
||||
): Promise<any>;
|
||||
export function saveUnprocesseds(
|
||||
arrayOfUnprocessed: any,
|
||||
{
|
||||
forceSave,
|
||||
}?: {
|
||||
forceSave: any;
|
||||
}
|
||||
): Promise<void>;
|
||||
export function updateUnprocessedAttempts(
|
||||
id: any,
|
||||
attempts: any
|
||||
): Promise<void>;
|
||||
export function updateUnprocessedWithData(id: any, data: any): Promise<void>;
|
||||
export function removeUnprocessed(id: any): Promise<void>;
|
||||
export function removeAllUnprocessed(): Promise<void>;
|
||||
|
||||
// Attachment Downloads
|
||||
export function getNextAttachmentDownloadJobs(limit: any): Promise<any>;
|
||||
export function saveAttachmentDownloadJob(job: any): Promise<void>;
|
||||
export function setAttachmentDownloadJobPending(
|
||||
id: any,
|
||||
pending: any
|
||||
): Promise<void>;
|
||||
export function resetAttachmentDownloadPending(): Promise<void>;
|
||||
export function removeAttachmentDownloadJob(id: any): Promise<void>;
|
||||
export function removeAllAttachmentDownloadJobs(): Promise<void>;
|
||||
|
||||
// Other
|
||||
export function removeAll(): Promise<void>;
|
||||
export function removeAllConfiguration(): Promise<void>;
|
||||
export function removeAllConversations(): Promise<void>;
|
||||
export function removeAllPrivateConversations(): Promise<void>;
|
||||
export function removeOtherData(): Promise<void>;
|
||||
export function cleanupOrphanedAttachments(): Promise<void>;
|
||||
|
||||
// Getters
|
||||
export function getMessagesNeedingUpgrade(
|
||||
limit: any,
|
||||
{
|
||||
maxVersion,
|
||||
}: {
|
||||
maxVersion?: number;
|
||||
}
|
||||
): Promise<any>;
|
||||
export function getLegacyMessagesNeedingUpgrade(
|
||||
limit: any,
|
||||
{
|
||||
maxVersion,
|
||||
}: {
|
||||
maxVersion?: number;
|
||||
}
|
||||
): Promise<any>;
|
||||
export function getMessagesWithVisualMediaAttachments(
|
||||
conversationId: any,
|
||||
{
|
||||
limit,
|
||||
}: {
|
||||
limit: any;
|
||||
}
|
||||
): Promise<any>;
|
||||
export function getMessagesWithFileAttachments(
|
||||
conversationId: any,
|
||||
{
|
||||
limit,
|
||||
}: {
|
||||
limit: any;
|
||||
}
|
||||
): Promise<any>;
|
||||
|
||||
// Sender Keys
|
||||
export function getSenderKeys(groupId: any, senderIdentity: any): Promise<any>;
|
||||
export function createOrUpdateSenderKeys(data: any): Promise<void>;
|
||||
|
@ -0,0 +1 @@
|
||||
Subproject commit 52b77bf3039aec88b3900e8a7ed6e62d30a4d0d4
|
@ -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;
|
||||
}
|
@ -0,0 +1,99 @@
|
||||
import { EncryptionType } from '../types/EncryptionType';
|
||||
import { SignalService } from '../../protobuf';
|
||||
import { libloki, libsignal, Signal, textsecure } from '../../window';
|
||||
import { CipherTextObject } from '../../window/types/libsignal-protocol';
|
||||
import { UserUtil } from '../../util';
|
||||
|
||||
export function padPlainTextBuffer(messageBuffer: Uint8Array): Uint8Array {
|
||||
const plaintext = new Uint8Array(
|
||||
getPaddedMessageLength(messageBuffer.byteLength + 1) - 1
|
||||
);
|
||||
plaintext.set(new Uint8Array(messageBuffer));
|
||||
plaintext[messageBuffer.byteLength] = 0x80;
|
||||
|
||||
return plaintext;
|
||||
}
|
||||
|
||||
function getPaddedMessageLength(originalLength: number): number {
|
||||
const messageLengthWithTerminator = originalLength + 1;
|
||||
let messagePartCount = Math.floor(messageLengthWithTerminator / 160);
|
||||
|
||||
if (messageLengthWithTerminator % 160 !== 0) {
|
||||
messagePartCount += 1;
|
||||
}
|
||||
|
||||
return messagePartCount * 160;
|
||||
}
|
||||
|
||||
export type Base64String = string;
|
||||
|
||||
/**
|
||||
* Encrypt `plainTextBuffer` with given `encryptionType` for `device`.
|
||||
*
|
||||
* @param device The device to encrypt for.
|
||||
* @param plainTextBuffer The unpadded plaintext buffer.
|
||||
* @param encryptionType The type of encryption.
|
||||
* @returns The envelope type and the base64 encoded cipher text
|
||||
*/
|
||||
export async function encrypt(
|
||||
device: string,
|
||||
plainTextBuffer: Uint8Array,
|
||||
encryptionType: EncryptionType
|
||||
): Promise<{
|
||||
envelopeType: SignalService.Envelope.Type;
|
||||
cipherText: Base64String;
|
||||
}> {
|
||||
const plainText = padPlainTextBuffer(plainTextBuffer);
|
||||
const address = new libsignal.SignalProtocolAddress(device, 1);
|
||||
|
||||
if (encryptionType === EncryptionType.MediumGroup) {
|
||||
// TODO: Do medium group stuff here
|
||||
throw new Error('Encryption is not yet supported');
|
||||
}
|
||||
|
||||
let innerCipherText: CipherTextObject;
|
||||
if (encryptionType === EncryptionType.SessionReset) {
|
||||
const cipher = new libloki.crypto.FallBackSessionCipher(address);
|
||||
innerCipherText = await cipher.encrypt(plainText.buffer);
|
||||
} else {
|
||||
const cipher = new libsignal.SessionCipher(
|
||||
textsecure.storage.protocol,
|
||||
address
|
||||
);
|
||||
innerCipherText = await cipher.encrypt(plainText.buffer);
|
||||
}
|
||||
|
||||
return encryptUsingSealedSender(device, innerCipherText);
|
||||
}
|
||||
|
||||
async function encryptUsingSealedSender(
|
||||
device: string,
|
||||
innerCipherText: CipherTextObject
|
||||
): Promise<{
|
||||
envelopeType: SignalService.Envelope.Type;
|
||||
cipherText: Base64String;
|
||||
}> {
|
||||
const ourNumber = await UserUtil.getCurrentDevicePubKey();
|
||||
if (!ourNumber) {
|
||||
throw new Error('Failed to fetch current device public key.');
|
||||
}
|
||||
|
||||
const certificate = SignalService.SenderCertificate.create({
|
||||
sender: ourNumber,
|
||||
senderDevice: 1,
|
||||
});
|
||||
|
||||
const cipher = new Signal.Metadata.SecretSessionCipher(
|
||||
textsecure.storage.protocol
|
||||
);
|
||||
const cipherTextBuffer = await cipher.encrypt(
|
||||
device,
|
||||
certificate,
|
||||
innerCipherText
|
||||
);
|
||||
|
||||
return {
|
||||
envelopeType: SignalService.Envelope.Type.UNIDENTIFIED_SENDER,
|
||||
cipherText: Buffer.from(cipherTextBuffer).toString('base64'),
|
||||
};
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
import * as MessageEncrypter from './MessageEncrypter';
|
||||
|
||||
export { MessageEncrypter };
|
@ -0,0 +1,8 @@
|
||||
import * as Messages from './messages';
|
||||
import * as Protocols from './protocols';
|
||||
|
||||
// TODO: Do we export class instances here?
|
||||
// E.g
|
||||
// export const messageQueue = new MessageQueue()
|
||||
|
||||
export { Messages, Protocols };
|
@ -0,0 +1,3 @@
|
||||
import * as Outgoing from './outgoing';
|
||||
|
||||
export { Outgoing };
|
@ -0,0 +1,19 @@
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
export interface MessageParams {
|
||||
timestamp: number;
|
||||
identifier?: string;
|
||||
}
|
||||
|
||||
export abstract class Message {
|
||||
public readonly timestamp: number;
|
||||
public readonly identifier: string;
|
||||
|
||||
constructor({ timestamp, identifier }: MessageParams) {
|
||||
this.timestamp = timestamp;
|
||||
if (identifier && identifier.length === 0) {
|
||||
throw new Error('Cannot set empty identifier');
|
||||
}
|
||||
this.identifier = identifier || uuid();
|
||||
}
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
import { Message, MessageParams } from './Message';
|
||||
import { AttachmentType } from '../../../types/Attachment';
|
||||
import { QuotedAttachmentType } from '../../../components/conversation/Quote';
|
||||
|
||||
interface OpenGroupMessageParams extends MessageParams {
|
||||
server: string;
|
||||
attachments?: Array<AttachmentType>;
|
||||
body?: string;
|
||||
quote?: QuotedAttachmentType;
|
||||
}
|
||||
|
||||
export class OpenGroupMessage extends Message {
|
||||
public readonly server: string;
|
||||
public readonly body?: string;
|
||||
public readonly attachments?: Array<AttachmentType>;
|
||||
public readonly quote?: QuotedAttachmentType;
|
||||
|
||||
constructor({
|
||||
timestamp,
|
||||
server,
|
||||
attachments,
|
||||
body,
|
||||
quote,
|
||||
identifier,
|
||||
}: OpenGroupMessageParams) {
|
||||
super({ timestamp, identifier });
|
||||
this.server = server;
|
||||
this.body = body;
|
||||
this.attachments = attachments;
|
||||
this.quote = quote;
|
||||
}
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
import { Message } from '../Message';
|
||||
import { SignalService } from '../../../../protobuf';
|
||||
|
||||
export abstract class ContentMessage extends Message {
|
||||
public plainTextBuffer(): Uint8Array {
|
||||
return SignalService.Content.encode(this.contentProto()).finish();
|
||||
}
|
||||
|
||||
public abstract ttl(): number;
|
||||
protected abstract contentProto(): SignalService.Content;
|
||||
|
||||
/**
|
||||
* If the message is not a message with a specific TTL,
|
||||
* this value can be used in all child classes
|
||||
*/
|
||||
protected getDefaultTTL(): number {
|
||||
// 1 day default for any other message
|
||||
return 24 * 60 * 60 * 1000;
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue