From e0f27ba712a0cbd07622a6ce6e05cfb7265d3d74 Mon Sep 17 00:00:00 2001 From: Mikunj <mikunj@live.com.au> Date: Fri, 22 May 2020 10:25:30 +1000 Subject: [PATCH 01/10] basic classes for message sending --- ts/session/crypto/MessageEncrypter.ts | 39 ++++++++++++++ ts/session/crypto/index.ts | 3 ++ ts/session/index.ts | 8 +++ ts/session/protocols/MultiDeviceProtocol.ts | 6 +++ ts/session/protocols/SessionProtocol.ts | 57 +++++++++++++++++++++ ts/session/protocols/index.ts | 4 ++ ts/session/sending/MessageQueue.ts | 54 +++++++++++++++++++ ts/session/sending/MessageQueueInterface.ts | 13 +++++ ts/session/sending/MessageSender.ts | 14 +++++ ts/session/sending/PendingMessageCache.ts | 36 +++++++++++++ ts/session/sending/index.ts | 5 ++ ts/session/types/EncryptionType.ts | 5 ++ ts/session/types/RawMessage.ts | 12 +++++ ts/session/utils/JobQueue.ts | 40 +++++++++++++++ 14 files changed, 296 insertions(+) create mode 100644 ts/session/crypto/MessageEncrypter.ts create mode 100644 ts/session/crypto/index.ts create mode 100644 ts/session/index.ts create mode 100644 ts/session/protocols/MultiDeviceProtocol.ts create mode 100644 ts/session/protocols/SessionProtocol.ts create mode 100644 ts/session/protocols/index.ts create mode 100644 ts/session/sending/MessageQueue.ts create mode 100644 ts/session/sending/MessageQueueInterface.ts create mode 100644 ts/session/sending/MessageSender.ts create mode 100644 ts/session/sending/PendingMessageCache.ts create mode 100644 ts/session/sending/index.ts create mode 100644 ts/session/types/EncryptionType.ts create mode 100644 ts/session/types/RawMessage.ts create mode 100644 ts/session/utils/JobQueue.ts diff --git a/ts/session/crypto/MessageEncrypter.ts b/ts/session/crypto/MessageEncrypter.ts new file mode 100644 index 000000000..8b2457961 --- /dev/null +++ b/ts/session/crypto/MessageEncrypter.ts @@ -0,0 +1,39 @@ +import { EncryptionType } from '../types/EncryptionType'; +import { SignalService } from '../../protobuf'; + +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 function encrypt( + device: string, + plainTextBuffer: Uint8Array, + encryptionType: EncryptionType +): { + envelopeType: SignalService.Envelope.Type; + cipherText: Uint8Array; +} { + const plainText = padPlainTextBuffer(plainTextBuffer); + // TODO: Do encryption here? + return { + envelopeType: SignalService.Envelope.Type.CIPHERTEXT, + cipherText: new Uint8Array(), + }; +} diff --git a/ts/session/crypto/index.ts b/ts/session/crypto/index.ts new file mode 100644 index 000000000..02d1b8904 --- /dev/null +++ b/ts/session/crypto/index.ts @@ -0,0 +1,3 @@ +import * as MessageEncrypter from './MessageEncrypter'; + +export { MessageEncrypter }; diff --git a/ts/session/index.ts b/ts/session/index.ts new file mode 100644 index 000000000..4aeab7901 --- /dev/null +++ b/ts/session/index.ts @@ -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 }; diff --git a/ts/session/protocols/MultiDeviceProtocol.ts b/ts/session/protocols/MultiDeviceProtocol.ts new file mode 100644 index 000000000..b144c20cf --- /dev/null +++ b/ts/session/protocols/MultiDeviceProtocol.ts @@ -0,0 +1,6 @@ +// TODO: Populate this with multi device specific code, e.g getting linked devices for a user etc... +// We need to deprecate the multi device code we have in js and slowly transition to this file + +export function implementStuffHere() { + throw new Error("Don't call me :("); +} diff --git a/ts/session/protocols/SessionProtocol.ts b/ts/session/protocols/SessionProtocol.ts new file mode 100644 index 000000000..3f9cf2dc8 --- /dev/null +++ b/ts/session/protocols/SessionProtocol.ts @@ -0,0 +1,57 @@ +// 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 { OutgoingContentMessage } 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')); +} + +// TODO: Replace OutgoingContentMessage with SessionReset +export async function sendSessionRequest( + message: OutgoingContentMessage +): 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 async 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 async 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 async 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 +} diff --git a/ts/session/protocols/index.ts b/ts/session/protocols/index.ts new file mode 100644 index 000000000..e0cfeb680 --- /dev/null +++ b/ts/session/protocols/index.ts @@ -0,0 +1,4 @@ +import * as SessionProtocol from './SessionProtocol'; +import * as MultiDeviceProtocol from './MultiDeviceProtocol'; + +export { SessionProtocol, MultiDeviceProtocol }; diff --git a/ts/session/sending/MessageQueue.ts b/ts/session/sending/MessageQueue.ts new file mode 100644 index 000000000..fb9ed0445 --- /dev/null +++ b/ts/session/sending/MessageQueue.ts @@ -0,0 +1,54 @@ +import { MessageQueueInterface } from './MessageQueueInterface'; +import { OutgoingContentMessage, OpenGroupMessage } from '../messages/outgoing'; +import { JobQueue } from '../utils/JobQueue'; +import { PendingMessageCache } from './PendingMessageCache'; + +export class MessageQueue implements MessageQueueInterface { + private readonly jobQueues: Map<string, JobQueue> = new Map(); + private readonly cache: PendingMessageCache; + + constructor() { + this.cache = new PendingMessageCache(); + this.processAllPending(); + } + + public sendUsingMultiDevice(user: string, message: OutgoingContentMessage) { + throw new Error('Method not implemented.'); + } + public send(device: string, message: OutgoingContentMessage) { + throw new Error('Method not implemented.'); + } + public sendToGroup(message: OutgoingContentMessage | OpenGroupMessage) { + throw new Error('Method not implemented.'); + } + public sendSyncMessage(message: OutgoingContentMessage) { + 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: OutgoingContentMessage) { + // 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; + } +} diff --git a/ts/session/sending/MessageQueueInterface.ts b/ts/session/sending/MessageQueueInterface.ts new file mode 100644 index 000000000..06f52cc7e --- /dev/null +++ b/ts/session/sending/MessageQueueInterface.ts @@ -0,0 +1,13 @@ +import { OutgoingContentMessage, OpenGroupMessage } from '../messages/outgoing'; + +// TODO: add all group messages here, replace OutgoingContentMessage with them +type GroupMessageType = OpenGroupMessage | OutgoingContentMessage; +export interface MessageQueueInterface { + sendUsingMultiDevice(user: string, message: OutgoingContentMessage): void; + send(device: string, message: OutgoingContentMessage): void; + sendToGroup(message: GroupMessageType): void; + sendSyncMessage(message: OutgoingContentMessage): void; + // TODO: Find a good way to handle events in this + // E.g if we do queue.onMessageSent() we want to also be able to stop listening to the event + // TODO: implement events here +} diff --git a/ts/session/sending/MessageSender.ts b/ts/session/sending/MessageSender.ts new file mode 100644 index 000000000..852157ca9 --- /dev/null +++ b/ts/session/sending/MessageSender.ts @@ -0,0 +1,14 @@ +// REMOVE COMMENT AFTER: This can just export pure functions as it doesn't need state + +import { RawMessage } from '../types/RawMessage'; +import { OpenGroupMessage } from '../messages/outgoing'; + +export async function send(message: RawMessage): Promise<void> { + return Promise.resolve(); +} + +export async function sendToOpenGroup( + message: OpenGroupMessage +): Promise<void> { + return Promise.resolve(); +} diff --git a/ts/session/sending/PendingMessageCache.ts b/ts/session/sending/PendingMessageCache.ts new file mode 100644 index 000000000..1d722642b --- /dev/null +++ b/ts/session/sending/PendingMessageCache.ts @@ -0,0 +1,36 @@ +import { RawMessage } from '../types/RawMessage'; +import { OutgoingContentMessage } from '../messages/outgoing'; + +// TODO: We should be able to import functions straight from the db here without going through the window object + +export class PendingMessageCache { + private cachedMessages: Array<RawMessage> = []; + + constructor() { + // TODO: We should load pending messages from db here + } + + public addPendingMessage( + device: string, + message: OutgoingContentMessage + ): RawMessage { + // TODO: Maybe have a util for converting OutgoingContentMessage to RawMessage? + // TODO: Raw message has uuid, how are we going to set that? maybe use a different identifier? + // One could be device + timestamp would make a unique identifier + // TODO: Return previous pending message if it exists + return {} as RawMessage; + } + + public removePendingMessage(message: RawMessage) { + // TODO: implement + } + + public getPendingDevices(): Array<String> { + // TODO: this should return all devices which have pending messages + return []; + } + + public getPendingMessages(device: string): Array<RawMessage> { + return []; + } +} diff --git a/ts/session/sending/index.ts b/ts/session/sending/index.ts new file mode 100644 index 000000000..69ca9153b --- /dev/null +++ b/ts/session/sending/index.ts @@ -0,0 +1,5 @@ +import * as MessageSender from './MessageSender'; +import { MessageQueue } from './MessageQueue'; +import { MessageQueueInterface } from './MessageQueueInterface'; + +export { MessageSender, MessageQueue, MessageQueueInterface }; diff --git a/ts/session/types/EncryptionType.ts b/ts/session/types/EncryptionType.ts new file mode 100644 index 000000000..ed27e1023 --- /dev/null +++ b/ts/session/types/EncryptionType.ts @@ -0,0 +1,5 @@ +export enum EncryptionType { + Signal, + SessionReset, + MediumGroup, +} diff --git a/ts/session/types/RawMessage.ts b/ts/session/types/RawMessage.ts new file mode 100644 index 000000000..30d2e0d9b --- /dev/null +++ b/ts/session/types/RawMessage.ts @@ -0,0 +1,12 @@ +import { EncryptionType } from './EncryptionType'; + +// TODO: Should we store failure count on raw messages?? +// Might be better to have a seperate interface which takes in a raw message aswell as a failure count +export interface RawMessage { + identifier: string; + plainTextBuffer: Uint8Array; + timestamp: number; + device: string; + ttl: number; + encryption: EncryptionType; +} diff --git a/ts/session/utils/JobQueue.ts b/ts/session/utils/JobQueue.ts new file mode 100644 index 000000000..d9a58d909 --- /dev/null +++ b/ts/session/utils/JobQueue.ts @@ -0,0 +1,40 @@ +import { v4 as uuid } from 'uuid'; + +// TODO: This needs to replace js/modules/job_queue.js +export class JobQueue { + private pending: Promise<any> = Promise.resolve(); + private readonly jobs: Map<string, Promise<any>> = new Map(); + + public has(id: string): boolean { + return this.jobs.has(id); + } + + public async add(job: () => any): Promise<any> { + const id = uuid(); + + return this.addWithId(id, job); + } + + public async addWithId(id: string, job: () => any): Promise<any> { + if (this.jobs.has(id)) { + return this.jobs.get(id); + } + + const previous = this.pending || Promise.resolve(); + this.pending = previous.then(job, job); + + const current = this.pending; + current + .finally(() => { + if (this.pending === current) { + delete this.pending; + } + this.jobs.delete(id); + }) + .ignore(); + + this.jobs.set(id, current); + + return current; + } +} From e7826cfb34962e8baa7e4db87f9188e9b0c38f36 Mon Sep 17 00:00:00 2001 From: Mikunj <mikunj@live.com.au> Date: Fri, 22 May 2020 10:30:57 +1000 Subject: [PATCH 02/10] linting --- ts/session/crypto/MessageEncrypter.ts | 1 + ts/session/protocols/SessionProtocol.ts | 6 +++--- ts/session/sending/MessageQueue.ts | 2 +- ts/session/sending/MessageQueueInterface.ts | 2 +- ts/session/sending/PendingMessageCache.ts | 2 +- 5 files changed, 7 insertions(+), 6 deletions(-) diff --git a/ts/session/crypto/MessageEncrypter.ts b/ts/session/crypto/MessageEncrypter.ts index 8b2457961..22ce64ccc 100644 --- a/ts/session/crypto/MessageEncrypter.ts +++ b/ts/session/crypto/MessageEncrypter.ts @@ -32,6 +32,7 @@ export function encrypt( } { const plainText = padPlainTextBuffer(plainTextBuffer); // TODO: Do encryption here? + return { envelopeType: SignalService.Envelope.Type.CIPHERTEXT, cipherText: new Uint8Array(), diff --git a/ts/session/protocols/SessionProtocol.ts b/ts/session/protocols/SessionProtocol.ts index 3f9cf2dc8..ebeac9f37 100644 --- a/ts/session/protocols/SessionProtocol.ts +++ b/ts/session/protocols/SessionProtocol.ts @@ -35,13 +35,13 @@ export async function sendSessionRequest( return Promise.resolve(); } -export async function sessionEstablished(device: string) { +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 async function shouldProcessSessionRequest( +export function shouldProcessSessionRequest( device: string, messageTimestamp: number ): boolean { @@ -50,7 +50,7 @@ export async function shouldProcessSessionRequest( return false; } -export async function sessionRequestProcessed(device: string) { +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 diff --git a/ts/session/sending/MessageQueue.ts b/ts/session/sending/MessageQueue.ts index fb9ed0445..5eeaa2426 100644 --- a/ts/session/sending/MessageQueue.ts +++ b/ts/session/sending/MessageQueue.ts @@ -1,5 +1,5 @@ import { MessageQueueInterface } from './MessageQueueInterface'; -import { OutgoingContentMessage, OpenGroupMessage } from '../messages/outgoing'; +import { OpenGroupMessage, OutgoingContentMessage } from '../messages/outgoing'; import { JobQueue } from '../utils/JobQueue'; import { PendingMessageCache } from './PendingMessageCache'; diff --git a/ts/session/sending/MessageQueueInterface.ts b/ts/session/sending/MessageQueueInterface.ts index 06f52cc7e..a231ad02c 100644 --- a/ts/session/sending/MessageQueueInterface.ts +++ b/ts/session/sending/MessageQueueInterface.ts @@ -1,4 +1,4 @@ -import { OutgoingContentMessage, OpenGroupMessage } from '../messages/outgoing'; +import { OpenGroupMessage, OutgoingContentMessage } from '../messages/outgoing'; // TODO: add all group messages here, replace OutgoingContentMessage with them type GroupMessageType = OpenGroupMessage | OutgoingContentMessage; diff --git a/ts/session/sending/PendingMessageCache.ts b/ts/session/sending/PendingMessageCache.ts index 1d722642b..2f10e58a6 100644 --- a/ts/session/sending/PendingMessageCache.ts +++ b/ts/session/sending/PendingMessageCache.ts @@ -4,7 +4,7 @@ import { OutgoingContentMessage } from '../messages/outgoing'; // TODO: We should be able to import functions straight from the db here without going through the window object export class PendingMessageCache { - private cachedMessages: Array<RawMessage> = []; + private readonly cachedMessages: Array<RawMessage> = []; constructor() { // TODO: We should load pending messages from db here From 3dfc1ca213cc53f8396fd55620ab5164849ba6d8 Mon Sep 17 00:00:00 2001 From: Mikunj <mikunj@live.com.au> Date: Fri, 22 May 2020 14:04:10 +1000 Subject: [PATCH 03/10] Improved JobQueue. Added tests. --- package.json | 3 +- ts/session/utils/JobQueue.ts | 21 +++-- ts/test/session/utils/JobQueue_test.ts | 109 +++++++++++++++++++++++++ ts/test/tslint.json | 6 +- ts/test/utils/timeout.ts | 4 + yarn.lock | 31 +++++++ 6 files changed, 165 insertions(+), 9 deletions(-) create mode 100644 ts/test/session/utils/JobQueue_test.ts create mode 100644 ts/test/utils/timeout.ts diff --git a/package.json b/package.json index 4603dedd2..4e78cb491 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,7 @@ "test-electron": "yarn grunt test", "test-integration": "ELECTRON_DISABLE_SANDBOX=1 mocha --exit --timeout 10000 integration_test/integration_test.js", "test-integration-parts": "ELECTRON_DISABLE_SANDBOX=1 mocha --exit --timeout 10000 integration_test/integration_test.js --grep 'registration' && ELECTRON_DISABLE_SANDBOX=1 mocha --exit --timeout 10000 integration_test/integration_test.js --grep 'openGroup' && ELECTRON_DISABLE_SANDBOX=1 mocha --exit --timeout 10000 integration_test/integration_test.js --grep 'addFriends' && ELECTRON_DISABLE_SANDBOX=1 mocha --exit --timeout 10000 integration_test/integration_test.js --grep 'linkDevice' && ELECTRON_DISABLE_SANDBOX=1 mocha --exit --timeout 10000 integration_test/integration_test.js --grep 'closedGroup'", - "test-node": "mocha --recursive --exit test/app test/modules ts/test libloki/test/node", + "test-node": "mocha --recursive --exit test/app test/modules ts/test libloki/test/node --timeout 10000", "eslint": "eslint --cache .", "eslint-fix": "eslint --fix .", "eslint-full": "eslint .", @@ -120,6 +120,7 @@ }, "devDependencies": { "@types/chai": "4.1.2", + "@types/chai-as-promised": "^7.1.2", "@types/classnames": "2.2.3", "@types/color": "^3.0.0", "@types/config": "0.0.34", diff --git a/ts/session/utils/JobQueue.ts b/ts/session/utils/JobQueue.ts index d9a58d909..fa5082836 100644 --- a/ts/session/utils/JobQueue.ts +++ b/ts/session/utils/JobQueue.ts @@ -1,37 +1,44 @@ import { v4 as uuid } from 'uuid'; +type Job<ResultType> = (() => PromiseLike<ResultType>) | (() => ResultType); + // TODO: This needs to replace js/modules/job_queue.js export class JobQueue { private pending: Promise<any> = Promise.resolve(); - private readonly jobs: Map<string, Promise<any>> = new Map(); + private readonly jobs: Map<string, Promise<unknown>> = new Map(); public has(id: string): boolean { return this.jobs.has(id); } - public async add(job: () => any): Promise<any> { + public async add<Result>(job: Job<Result>): Promise<Result> { const id = uuid(); return this.addWithId(id, job); } - public async addWithId(id: string, job: () => any): Promise<any> { + public async addWithId<Result>( + id: string, + job: Job<Result> + ): Promise<Result> { if (this.jobs.has(id)) { - return this.jobs.get(id); + return this.jobs.get(id) as Promise<Result>; } const previous = this.pending || Promise.resolve(); this.pending = previous.then(job, job); const current = this.pending; - current + void current + .catch(() => { + // This is done to avoid UnhandledPromiseError + }) .finally(() => { if (this.pending === current) { delete this.pending; } this.jobs.delete(id); - }) - .ignore(); + }); this.jobs.set(id, current); diff --git a/ts/test/session/utils/JobQueue_test.ts b/ts/test/session/utils/JobQueue_test.ts new file mode 100644 index 000000000..f431b455d --- /dev/null +++ b/ts/test/session/utils/JobQueue_test.ts @@ -0,0 +1,109 @@ +import chai from 'chai'; +import { v4 as uuid } from 'uuid'; +import { JobQueue } from '../../../session/utils/JobQueue'; +import { delay } from '../../utils/delay'; + +// tslint:disable-next-line: no-require-imports no-var-requires +const chaiAsPromised = require('chai-as-promised'); +chai.use(chaiAsPromised); + +const { assert } = chai; + +describe('JobQueue', () => { + describe('has', () => { + it('should return the correct value', async () => { + const queue = new JobQueue(); + const id = 'jobId'; + + assert.isFalse(queue.has(id)); + const promise = queue.addWithId(id, async () => delay(100)); + assert.isTrue(queue.has(id)); + await promise; + assert.isFalse(queue.has(id)); + }); + }); + + describe('addWithId', () => { + it('should run the jobs concurrently', async () => { + const input = [[10, 300], [20, 200], [30, 100]]; + const queue = new JobQueue(); + const mapper = async ([value, ms]: Array<number>): Promise<number> => + queue.addWithId(uuid(), async () => { + await delay(ms); + + return value; + }); + + const start = Date.now(); + assert.deepEqual(await Promise.all(input.map(mapper)), [10, 20, 30]); + const timeTaken = Date.now() - start; + assert.closeTo(timeTaken, 600, 50, 'Queue was delayed'); + }); + + it('should return the result of the job', async () => { + const queue = new JobQueue(); + const success = queue.addWithId(uuid(), async () => { + await delay(100); + + return 'success'; + }); + const failure = queue.addWithId(uuid(), async () => { + await delay(100); + throw new Error('failed'); + }); + + assert.equal(await success, 'success'); + await assert.isRejected(failure, /failed/); + }); + + it('should handle sync and async tasks', async () => { + const queue = new JobQueue(); + const first = queue.addWithId(uuid(), () => 'first'); + const second = queue.addWithId(uuid(), async () => { + await delay(100); + + return 'second'; + }); + const third = queue.addWithId(uuid(), () => 'third'); + + assert.deepEqual(await Promise.all([first, second, third]), [ + 'first', + 'second', + 'third', + ]); + }); + + it('should return the previous job if same id was passed', async () => { + const queue = new JobQueue(); + const id = uuid(); + const job = async () => { + await delay(100); + + return 'job1'; + }; + + const promise = queue.addWithId(id, job); + const otherPromise = queue.addWithId(id, () => 'job2'); + assert.equal(await promise, await otherPromise); + await promise; + }); + + it('should remove completed jobs', async () => { + const queue = new JobQueue(); + const id = uuid(); + + const successfullJob = queue.addWithId(id, async () => delay(100)); + assert.isTrue(queue.has(id)); + await successfullJob; + assert.isFalse(queue.has(id)); + + const failJob = queue.addWithId(id, async () => { + await delay(100); + throw new Error('failed'); + }); + assert.isTrue(queue.has(id)); + await assert.isRejected(failJob, /failed/); + assert.isFalse(queue.has(id)); + }); + }); +}); diff --git a/ts/test/tslint.json b/ts/test/tslint.json index 4645335d0..21571a6db 100644 --- a/ts/test/tslint.json +++ b/ts/test/tslint.json @@ -6,6 +6,10 @@ "no-implicit-dependencies": false, // All tests use arrow functions, and they can be long - "max-func-body-length": false + "max-func-body-length": false, + + "no-unused-expression": false, + + "await-promise": [true, "PromiseLike"] } } diff --git a/ts/test/utils/timeout.ts b/ts/test/utils/timeout.ts new file mode 100644 index 000000000..cafd9cf55 --- /dev/null +++ b/ts/test/utils/timeout.ts @@ -0,0 +1,4 @@ +export async function timeout(ms: number): Promise<void> { + // tslint:disable-next-line no-string-based-set-timeout + return new Promise(resolve => setTimeout(resolve, ms)); +} diff --git a/yarn.lock b/yarn.lock index 09e1f4b11..c335da9f3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -156,6 +156,18 @@ dependencies: defer-to-connect "^1.0.1" +"@types/chai-as-promised@^7.1.2": + version "7.1.2" + resolved "https://registry.yarnpkg.com/@types/chai-as-promised/-/chai-as-promised-7.1.2.tgz#2f564420e81eaf8650169e5a3a6b93e096e5068b" + integrity sha512-PO2gcfR3Oxa+u0QvECLe1xKXOqYTzCmWf0FhLhjREoW3fPAVamjihL7v1MOVLJLsnAMdLcjkfrs01yvDMwVK4Q== + dependencies: + "@types/chai" "*" + +"@types/chai@*": + version "4.2.11" + resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.2.11.tgz#d3614d6c5f500142358e6ed24e1bf16657536c50" + integrity sha512-t7uW6eFafjO+qJ3BIV2gGUyZs27egcNRkUdalkud+Qa3+kg//f129iuOFivHDXQ+vnU3fDXuwgv0cqMCbcE8sw== + "@types/chai@4.1.2": version "4.1.2" resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.1.2.tgz#f1af664769cfb50af805431c407425ed619daa21" @@ -4836,6 +4848,13 @@ in-publish@^2.0.0: resolved "https://registry.yarnpkg.com/in-publish/-/in-publish-2.0.1.tgz#948b1a535c8030561cea522f73f78f4be357e00c" integrity sha512-oDM0kUSNFC31ShNxHKUyfZKy8ZeXZBWMjMdZHKLOk13uvT27VTL/QzRGfRUcevJhpkZAvlhPYuXkF7eNWrtyxQ== +indent-string@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-2.1.0.tgz#8e2d48348742121b4a8218b7a137e9a52049dc80" + integrity sha1-ji1INIdCEhtKghi3oTfppSBJ3IA= + dependencies: + repeating "^2.0.0" + indexes-of@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/indexes-of/-/indexes-of-1.0.1.tgz#f30f716c8e2bd346c7b67d3df3915566a7c05607" @@ -9193,6 +9212,11 @@ slice-ansi@1.0.0: dependencies: is-fullwidth-code-point "^2.0.0" +slide@^1.1.5: + version "1.1.6" + resolved "https://registry.yarnpkg.com/slide/-/slide-1.1.6.tgz#56eb027d65b4d2dce6cb2e2d32c4d4afc9e1d707" + integrity sha1-VusCfWW00tzmyy4tMsTUr8nh1wc= + snapdragon-node@^2.0.1: version "2.1.1" resolved "https://registry.yarnpkg.com/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b" @@ -9650,6 +9674,13 @@ strip-eof@^1.0.0: resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf" integrity sha1-u0P/VZim6wXYm1n80SnJgzE2Br8= +strip-indent@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-1.0.1.tgz#0c7962a6adefa7bbd4ac366460a638552ae1a0a2" + integrity sha1-DHlipq3vp7vUrDZkYKY4VSrhoKI= + dependencies: + get-stdin "^4.0.1" + strip-json-comments@~2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" From 6557d7bcb3aebaf809a5414e412fdabb378f53b0 Mon Sep 17 00:00:00 2001 From: Mikunj <mikunj@live.com.au> Date: Fri, 22 May 2020 15:49:03 +1000 Subject: [PATCH 04/10] Updated JobQueue tests --- ts/test/session/utils/JobQueue_test.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/ts/test/session/utils/JobQueue_test.ts b/ts/test/session/utils/JobQueue_test.ts index f431b455d..79c249735 100644 --- a/ts/test/session/utils/JobQueue_test.ts +++ b/ts/test/session/utils/JobQueue_test.ts @@ -1,7 +1,7 @@ import chai from 'chai'; import { v4 as uuid } from 'uuid'; import { JobQueue } from '../../../session/utils/JobQueue'; -import { delay } from '../../utils/delay'; +import { timeout } from '../../utils/timeout'; // tslint:disable-next-line: no-require-imports no-var-requires const chaiAsPromised = require('chai-as-promised'); @@ -16,7 +16,7 @@ describe('JobQueue', () => { const id = 'jobId'; assert.isFalse(queue.has(id)); - const promise = queue.addWithId(id, async () => delay(100)); + const promise = queue.addWithId(id, async () => timeout(100)); assert.isTrue(queue.has(id)); await promise; assert.isFalse(queue.has(id)); @@ -29,7 +29,7 @@ describe('JobQueue', () => { const queue = new JobQueue(); const mapper = async ([value, ms]: Array<number>): Promise<number> => queue.addWithId(uuid(), async () => { - await delay(ms); + await timeout(ms); return value; }); @@ -43,12 +43,12 @@ describe('JobQueue', () => { it('should return the result of the job', async () => { const queue = new JobQueue(); const success = queue.addWithId(uuid(), async () => { - await delay(100); + await timeout(100); return 'success'; }); const failure = queue.addWithId(uuid(), async () => { - await delay(100); + await timeout(100); throw new Error('failed'); }); @@ -60,7 +60,7 @@ describe('JobQueue', () => { const queue = new JobQueue(); const first = queue.addWithId(uuid(), () => 'first'); const second = queue.addWithId(uuid(), async () => { - await delay(100); + await timeout(100); return 'second'; }); @@ -77,7 +77,7 @@ describe('JobQueue', () => { const queue = new JobQueue(); const id = uuid(); const job = async () => { - await delay(100); + await timeout(100); return 'job1'; }; @@ -92,13 +92,13 @@ describe('JobQueue', () => { const queue = new JobQueue(); const id = uuid(); - const successfullJob = queue.addWithId(id, async () => delay(100)); + const successfullJob = queue.addWithId(id, async () => timeout(100)); assert.isTrue(queue.has(id)); await successfullJob; assert.isFalse(queue.has(id)); const failJob = queue.addWithId(id, async () => { - await delay(100); + await timeout(100); throw new Error('failed'); }); assert.isTrue(queue.has(id)); From 956dea8a1f17b7afa51381e98fe5859c4a8e0ee9 Mon Sep 17 00:00:00 2001 From: Mikunj <mikunj@live.com.au> Date: Fri, 22 May 2020 15:55:59 +1000 Subject: [PATCH 05/10] Use correcy syntax for async asserts --- ts/test/session/utils/JobQueue_test.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/ts/test/session/utils/JobQueue_test.ts b/ts/test/session/utils/JobQueue_test.ts index 79c249735..b3f4a5c18 100644 --- a/ts/test/session/utils/JobQueue_test.ts +++ b/ts/test/session/utils/JobQueue_test.ts @@ -2,6 +2,7 @@ import chai from 'chai'; import { v4 as uuid } from 'uuid'; import { JobQueue } from '../../../session/utils/JobQueue'; import { timeout } from '../../utils/timeout'; +import { SignalService } from '../../../protobuf'; // tslint:disable-next-line: no-require-imports no-var-requires const chaiAsPromised = require('chai-as-promised'); @@ -35,7 +36,7 @@ describe('JobQueue', () => { }); const start = Date.now(); - assert.deepEqual(await Promise.all(input.map(mapper)), [10, 20, 30]); + await assert.eventually.deepEqual(Promise.all(input.map(mapper)), [10, 20, 30]); const timeTaken = Date.now() - start; assert.closeTo(timeTaken, 600, 50, 'Queue was delayed'); }); @@ -52,7 +53,7 @@ describe('JobQueue', () => { throw new Error('failed'); }); - assert.equal(await success, 'success'); + await assert.eventually.equal(success, 'success'); await assert.isRejected(failure, /failed/); }); @@ -66,7 +67,7 @@ describe('JobQueue', () => { }); const third = queue.addWithId(uuid(), () => 'third'); - assert.deepEqual(await Promise.all([first, second, third]), [ + await assert.eventually.deepEqual(Promise.all([first, second, third]), [ 'first', 'second', 'third', @@ -84,8 +85,8 @@ describe('JobQueue', () => { const promise = queue.addWithId(id, job); const otherPromise = queue.addWithId(id, () => 'job2'); - assert.equal(await promise, await otherPromise); - await promise; + await assert.eventually.equal(promise, 'job1'); + await assert.eventually.equal(otherPromise, 'job1'); }); it('should remove completed jobs', async () => { From 21586f8e1491dab144d55448a01c75cbf007eb45 Mon Sep 17 00:00:00 2001 From: Mikunj <mikunj@live.com.au> Date: Fri, 22 May 2020 16:06:31 +1000 Subject: [PATCH 06/10] Linting + Improve tslint rules in tests --- ts/test/session/utils/JobQueue_test.ts | 7 +++++-- ts/test/tslint.json | 3 ++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/ts/test/session/utils/JobQueue_test.ts b/ts/test/session/utils/JobQueue_test.ts index b3f4a5c18..864c69247 100644 --- a/ts/test/session/utils/JobQueue_test.ts +++ b/ts/test/session/utils/JobQueue_test.ts @@ -2,7 +2,6 @@ import chai from 'chai'; import { v4 as uuid } from 'uuid'; import { JobQueue } from '../../../session/utils/JobQueue'; import { timeout } from '../../utils/timeout'; -import { SignalService } from '../../../protobuf'; // tslint:disable-next-line: no-require-imports no-var-requires const chaiAsPromised = require('chai-as-promised'); @@ -36,7 +35,11 @@ describe('JobQueue', () => { }); const start = Date.now(); - await assert.eventually.deepEqual(Promise.all(input.map(mapper)), [10, 20, 30]); + await assert.eventually.deepEqual(Promise.all(input.map(mapper)), [ + 10, + 20, + 30, + ]); const timeTaken = Date.now() - start; assert.closeTo(timeTaken, 600, 50, 'Queue was delayed'); }); diff --git a/ts/test/tslint.json b/ts/test/tslint.json index 21571a6db..9d1d3e71b 100644 --- a/ts/test/tslint.json +++ b/ts/test/tslint.json @@ -10,6 +10,7 @@ "no-unused-expression": false, - "await-promise": [true, "PromiseLike"] + "await-promise": [true, "PromiseLike"], + "no-floating-promises": [true, "PromiseLike"] } } From 7a85d69970f7ef5302f91114f99ca23674156f7f Mon Sep 17 00:00:00 2001 From: Vincent <vincent@loki.network> Date: Tue, 26 May 2020 11:00:50 +1000 Subject: [PATCH 07/10] Mostly strictly types Signal Data --- js/modules/data.d.ts | 388 ++++++++++++++++++++++++++++++++++++++++++- ts/window.ts | 77 +++++++++ 2 files changed, 464 insertions(+), 1 deletion(-) create mode 100644 ts/window.ts diff --git a/js/modules/data.d.ts b/js/modules/data.d.ts index 29e42df98..e9543b99b 100644 --- a/js/modules/data.d.ts +++ b/js/modules/data.d.ts @@ -1,3 +1,389 @@ +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; +} | null; + +type PreKey = { + id: string; + publicKey: string; + privateKey: string; + recipient: string; +} | null; + +type PairingAuthorisation = { + primaryDevicePubKey: string; + secondaryDevicePubKey: string; + requestSignature: string; + grantSignature: string | null; +} | null; + +type PairingAuthorisationInit = { + requestSignature: string; + grantSignature: string; +}; + +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: any): Promise<void>; +export function getIdentityKeyById(id: string): Promise<IdentityKey>; +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: string): Promise<PreKey>; +export function getPreKeyByRecipient(recipient: string): Promise<PreKey>; +export function bulkAddPreKeys(data: Array<PreKey>): Promise<void>; +export function removePreKeyById(id: string): Promise<void>; +export function getAllPreKeys(): Promise<Array<PreKey>>; + +// Signed Pre Keys +export function createOrUpdateSignedPreKey(data: PreKey): Promise<void>; +export function getSignedPreKeyById(id: string): Promise<PreKey>; +export function getAllSignedPreKeys(recipient: string): Promise<PreKey>; +export function bulkAddSignedPreKeys(array: Array<PreKey>): Promise<void>; +export function removeSignedPreKeyById(id: string): Promise<void>; +export function removeAllSignedPreKeys(): Promise<void>; + +// Contact Pre Key +export function createOrUpdateContactPreKey(data: PreKey): Promise<void>; +export function getContactPreKeyById(id: string): Promise<PreKey>; +export function getContactPreKeyByIdentityKey(key: string): Promise<PreKey>; +export function getContactPreKeys( + keyId: string, + identityKeyString: string +): Promise<Array<PreKey>>; +export function getAllContactPreKeys(): Promise<Array<PreKey>>; +export function bulkAddContactPreKeys(array: Array<PreKey>): Promise<void>; +export function removeContactPreKeyByIdentityKey(id: string): Promise<void>; +export function removeAllContactPreKeys(): Promise<void>; + +// Contact Signed Pre Key +export function createOrUpdateContactSignedPreKey(data: PreKey): Promise<void>; +export function getContactSignedPreKeyByIdid(string): Promise<PreKey>; +export function getContactSignedPreKeyByIdentityKey( + key: string +): Promise<PreKey>; +export function getContactSignedPreKeys( + keyId: string, + identityKeyString: string +): Promise<Array<PreKey>>; +export function bulkAddContactSignedPreKeys( + array: Array<PreKey> +): Promise<void>; +export function removeContactSignedPreKeyByIdentityKey( + id: string +): Promise<void>; +export function removeAllContactSignedPreKeys(): Promise<void>; + +// Authorisations & Linking +export function createOrUpdatePairingAuthorisation( + data: PairingAuthorisationInit +): Promise<PairingAuthorisation>; +export function removePairingAuthorisationForSecondaryPubKey( + pubKey: string +): Promise<void>; +export function getGrantAuthorisationsForPrimaryPubKey( + pubKey: string +): Promise<Array<PairingAuthorisation>>; +export function getGrantAuthorisationForSecondaryPubKey( + pubKey: string +): Promise<PairingAuthorisation>; +export function getAuthorisationForSecondaryPubKey( + pubKey: string +): PairingAuthorisation; +export function getSecondaryDevicesFor( + primaryDevicePubKey: string +): Array<string>; +export function getPrimaryDeviceFor( + secondaryDevicePubKey: string +): string | null; +export function getPairedDevicesFor(pubKey: string): 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>; +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>; diff --git a/ts/window.ts b/ts/window.ts new file mode 100644 index 000000000..2490690a4 --- /dev/null +++ b/ts/window.ts @@ -0,0 +1,77 @@ +declare global { + interface Window { + seedNodeList: any; + + WebAPI: any; + LokiSnodeAPI: any; + SenderKeyAPI: any; + LokiMessageAPI: any; + StubMessageAPI: any; + StubAppDotNetApi: any; + LokiPublicChatAPI: any; + LokiAppDotNetServerAPI: any; + LokiFileServerAPI: any; + LokiRssAPI: any; + } +} + +// window.WebAPI = initializeWebAPI(); +// const LokiSnodeAPI = require('./js/modules/loki_snode_api'); +// window.SenderKeyAPI = require('./js/modules/loki_sender_key_api'); +// window.lokiSnodeAPI +// window.LokiMessageAPI = require('./js/modules/loki_message_api'); +// window.StubMessageAPI = require('./integration_test/stubs/stub_message_api'); +// window.StubAppDotNetApi = require('./integration_test/stubs/stub_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'); + +export const exporttts = { + // APIs + WebAPI: window.WebAPI, + + // Utilities + Events: () => window.Events, + Signal: () => window.Signal, + Whisper: () => window.Whisper, + ConversationController: () => window.ConversationController, + passwordUtil: () => window.passwordUtil, + + // Values + CONSTANTS: () => window.CONSTANTS, + versionInfo: () => window.versionInfo, + mnemonic: () => window.mnemonic, + lokiFeatureFlags: () => window.lokiFeatureFlags, + + // Getters + getAccountManager: () => window.getAccountManager, + getConversations: () => window.getConversations, + getFriendsFromContacts: () => window.getFriendsFromContacts, + getSettingValue: () => window.getSettingValue, + + // Setters + setPassword: () => window.setPassword, + setSettingValue: () => window.setSettingValue, + + // UI Events + pushToast: () => window.pushToast, + confirmationDialog: () => window.confirmationDialog, + + showQRDialog: () => window.showQRDialog, + showSeedDialog: () => window.showSeedDialog, + showPasswordDialog: () => window.showPasswordDialog, + showEditProfileDialog: () => window.showEditProfileDialog, + + toggleTheme: () => window.toggleTheme, + toggleMenuBar: () => window.toggleMenuBar, + toggleSpellCheck: () => window.toggleSpellCheck, + toggleLinkPreview: () => window.toggleLinkPreview, + toggleMediaPermissions: () => window.toggleMediaPermissions, + + // Actions + clearLocalData: () => window.clearLocalData, + deleteAccount: () => window.deleteAccount, + resetDatabase: () => window.resetDatabase, + attemptConnection: () => window.attemptConnection, +}; From 0f6053ce08e58e4b0d7f2de2a6e245555877cfbb Mon Sep 17 00:00:00 2001 From: Mikunj <mikunj@live.com.au> Date: Wed, 27 May 2020 11:05:30 +1000 Subject: [PATCH 08/10] Add events to MessageQueueInterface. Added strict typings for events. --- ts/session/sending/MessageQueue.ts | 10 +++- ts/session/sending/MessageQueueInterface.ts | 12 +++-- ts/session/sending/index.ts | 7 +-- ts/session/utils/TypedEmitter.ts | 53 +++++++++++++++++++++ ts/session/utils/index.ts | 2 + 5 files changed, 76 insertions(+), 8 deletions(-) create mode 100644 ts/session/utils/TypedEmitter.ts create mode 100644 ts/session/utils/index.ts diff --git a/ts/session/sending/MessageQueue.ts b/ts/session/sending/MessageQueue.ts index 5eeaa2426..274640ac4 100644 --- a/ts/session/sending/MessageQueue.ts +++ b/ts/session/sending/MessageQueue.ts @@ -1,13 +1,19 @@ -import { MessageQueueInterface } from './MessageQueueInterface'; +import { EventEmitter } from 'events'; +import { + MessageQueueInterface, + MessageQueueInterfaceEvents, +} from './MessageQueueInterface'; import { OpenGroupMessage, OutgoingContentMessage } from '../messages/outgoing'; -import { JobQueue } from '../utils/JobQueue'; 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(); } diff --git a/ts/session/sending/MessageQueueInterface.ts b/ts/session/sending/MessageQueueInterface.ts index a231ad02c..553b25ed0 100644 --- a/ts/session/sending/MessageQueueInterface.ts +++ b/ts/session/sending/MessageQueueInterface.ts @@ -1,13 +1,19 @@ import { OpenGroupMessage, OutgoingContentMessage } from '../messages/outgoing'; +import { RawMessage } from '../types/RawMessage'; +import { TypedEventEmitter } from '../utils'; // TODO: add all group messages here, replace OutgoingContentMessage with them type GroupMessageType = OpenGroupMessage | OutgoingContentMessage; + +export interface MessageQueueInterfaceEvents { + success: (message: RawMessage) => void; + fail: (message: RawMessage, error: Error) => void; +} + export interface MessageQueueInterface { + events: TypedEventEmitter<MessageQueueInterfaceEvents>; sendUsingMultiDevice(user: string, message: OutgoingContentMessage): void; send(device: string, message: OutgoingContentMessage): void; sendToGroup(message: GroupMessageType): void; sendSyncMessage(message: OutgoingContentMessage): void; - // TODO: Find a good way to handle events in this - // E.g if we do queue.onMessageSent() we want to also be able to stop listening to the event - // TODO: implement events here } diff --git a/ts/session/sending/index.ts b/ts/session/sending/index.ts index 69ca9153b..f6cf299b6 100644 --- a/ts/session/sending/index.ts +++ b/ts/session/sending/index.ts @@ -1,5 +1,6 @@ +// TS 3.8 supports export * as X from 'Y' import * as MessageSender from './MessageSender'; -import { MessageQueue } from './MessageQueue'; -import { MessageQueueInterface } from './MessageQueueInterface'; +export { MessageSender }; -export { MessageSender, MessageQueue, MessageQueueInterface }; +export * from './MessageQueue'; +export * from './MessageQueueInterface'; diff --git a/ts/session/utils/TypedEmitter.ts b/ts/session/utils/TypedEmitter.ts new file mode 100644 index 000000000..a6a27f200 --- /dev/null +++ b/ts/session/utils/TypedEmitter.ts @@ -0,0 +1,53 @@ +// Code from https://github.com/andywer/typed-emitter + +type Arguments<T> = [T] extends [(...args: infer U) => any] + ? U + : [T] extends [void] ? [] : [T]; + +/** + * Type-safe event emitter. + * + * Use it like this: + * + * interface MyEvents { + * error: (error: Error) => void + * message: (from: string, content: string) => void + * } + * + * const myEmitter = new EventEmitter() as TypedEmitter<MyEvents> + * + * myEmitter.on("message", (from, content) => { + * // ... + * }) + * + * myEmitter.emit("error", "x") // <- Will catch this type error + * + * or + * + * class MyEmitter extends EventEmitter implements TypedEventEmitter<MyEvents> + */ +export interface TypedEventEmitter<Events> { + addListener<E extends keyof Events>(event: E, listener: Events[E]): this; + on<E extends keyof Events>(event: E, listener: Events[E]): this; + once<E extends keyof Events>(event: E, listener: Events[E]): this; + prependListener<E extends keyof Events>(event: E, listener: Events[E]): this; + prependOnceListener<E extends keyof Events>( + event: E, + listener: Events[E] + ): this; + + off<E extends keyof Events>(event: E, listener: Events[E]): this; + removeAllListeners<E extends keyof Events>(event?: E): this; + removeListener<E extends keyof Events>(event: E, listener: Events[E]): this; + + emit<E extends keyof Events>( + event: E, + ...args: Arguments<Events[E]> + ): boolean; + eventNames(): Array<keyof Events | string | symbol>; + listeners<E extends keyof Events>(event: E): Array<Function>; + listenerCount<E extends keyof Events>(event: E): number; + + getMaxListeners(): number; + setMaxListeners(maxListeners: number): this; +} diff --git a/ts/session/utils/index.ts b/ts/session/utils/index.ts new file mode 100644 index 000000000..a33d528ba --- /dev/null +++ b/ts/session/utils/index.ts @@ -0,0 +1,2 @@ +export * from './TypedEmitter'; +export * from './JobQueue'; From 729fa594b84d864a8edbb56fb4a26dd9663037a0 Mon Sep 17 00:00:00 2001 From: Mikunj <mikunj@live.com.au> Date: Thu, 28 May 2020 12:14:49 +1000 Subject: [PATCH 09/10] Updated window exports --- js/modules/data.d.ts | 8 +- ts/global.d.ts | 1 + ts/window.ts | 193 ++++++++++++++++++++++++++----------------- 3 files changed, 123 insertions(+), 79 deletions(-) diff --git a/js/modules/data.d.ts b/js/modules/data.d.ts index e9543b99b..e154cbf3c 100644 --- a/js/modules/data.d.ts +++ b/js/modules/data.d.ts @@ -135,14 +135,14 @@ export function getGrantAuthorisationForSecondaryPubKey( ): Promise<PairingAuthorisation>; export function getAuthorisationForSecondaryPubKey( pubKey: string -): PairingAuthorisation; +): Promise<PairingAuthorisation>; export function getSecondaryDevicesFor( primaryDevicePubKey: string -): Array<string>; +): Promise<Array<string>>; export function getPrimaryDeviceFor( secondaryDevicePubKey: string -): string | null; -export function getPairedDevicesFor(pubKey: string): Array<string>; +): Promise<string | null>; +export function getPairedDevicesFor(pubKey: string): Promise<Array<string>>; // Guard Nodes export function getGuardNodes(): Promise<GuardNode>; diff --git a/ts/global.d.ts b/ts/global.d.ts index 34564325a..39e55c39f 100644 --- a/ts/global.d.ts +++ b/ts/global.d.ts @@ -1,3 +1,4 @@ +// TODO: Delete this and depend on window.ts instead interface Window { CONSTANTS: any; versionInfo: any; diff --git a/ts/window.ts b/ts/window.ts index 2490690a4..aeb19ec5d 100644 --- a/ts/window.ts +++ b/ts/window.ts @@ -1,77 +1,120 @@ -declare global { - interface Window { - seedNodeList: any; - - WebAPI: any; - LokiSnodeAPI: any; - SenderKeyAPI: any; - LokiMessageAPI: any; - StubMessageAPI: any; - StubAppDotNetApi: any; - LokiPublicChatAPI: any; - LokiAppDotNetServerAPI: any; - LokiFileServerAPI: any; - LokiRssAPI: any; - } +import { LocalizerType } from './types/Util'; + +interface Window { + seedNodeList: any; + + WebAPI: any; + LokiSnodeAPI: any; + SenderKeyAPI: any; + LokiMessageAPI: any; + StubMessageAPI: any; + StubAppDotNetApi: any; + LokiPublicChatAPI: any; + LokiAppDotNetServerAPI: any; + LokiFileServerAPI: any; + LokiRssAPI: any; + + CONSTANTS: any; + versionInfo: any; + + Events: any; + Lodash: any; + clearLocalData: any; + getAccountManager: any; + getConversations: any; + getFriendsFromContacts: any; + mnemonic: any; + clipboard: any; + attemptConnection: any; + + passwordUtil: any; + userConfig: any; + shortenPubkey: any; + + dcodeIO: any; + libsignal: any; + libloki: any; + displayNameRegex: any; + + Signal: any; + Whisper: any; + ConversationController: any; + + onLogin: any; + setPassword: any; + textsecure: any; + Session: any; + log: any; + i18n: LocalizerType; + friends: any; + generateID: any; + storage: any; + pushToast: any; + + confirmationDialog: any; + showQRDialog: any; + showSeedDialog: any; + showPasswordDialog: any; + showEditProfileDialog: any; + + deleteAccount: any; + + toggleTheme: any; + toggleMenuBar: any; + toggleSpellCheck: any; + toggleLinkPreview: any; + toggleMediaPermissions: any; + + getSettingValue: any; + setSettingValue: any; + lokiFeatureFlags: any; + + resetDatabase: any; } -// window.WebAPI = initializeWebAPI(); -// const LokiSnodeAPI = require('./js/modules/loki_snode_api'); -// window.SenderKeyAPI = require('./js/modules/loki_sender_key_api'); -// window.lokiSnodeAPI -// window.LokiMessageAPI = require('./js/modules/loki_message_api'); -// window.StubMessageAPI = require('./integration_test/stubs/stub_message_api'); -// window.StubAppDotNetApi = require('./integration_test/stubs/stub_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'); - -export const exporttts = { - // APIs - WebAPI: window.WebAPI, - - // Utilities - Events: () => window.Events, - Signal: () => window.Signal, - Whisper: () => window.Whisper, - ConversationController: () => window.ConversationController, - passwordUtil: () => window.passwordUtil, - - // Values - CONSTANTS: () => window.CONSTANTS, - versionInfo: () => window.versionInfo, - mnemonic: () => window.mnemonic, - lokiFeatureFlags: () => window.lokiFeatureFlags, - - // Getters - getAccountManager: () => window.getAccountManager, - getConversations: () => window.getConversations, - getFriendsFromContacts: () => window.getFriendsFromContacts, - getSettingValue: () => window.getSettingValue, - - // Setters - setPassword: () => window.setPassword, - setSettingValue: () => window.setSettingValue, - - // UI Events - pushToast: () => window.pushToast, - confirmationDialog: () => window.confirmationDialog, - - showQRDialog: () => window.showQRDialog, - showSeedDialog: () => window.showSeedDialog, - showPasswordDialog: () => window.showPasswordDialog, - showEditProfileDialog: () => window.showEditProfileDialog, - - toggleTheme: () => window.toggleTheme, - toggleMenuBar: () => window.toggleMenuBar, - toggleSpellCheck: () => window.toggleSpellCheck, - toggleLinkPreview: () => window.toggleLinkPreview, - toggleMediaPermissions: () => window.toggleMediaPermissions, - - // Actions - clearLocalData: () => window.clearLocalData, - deleteAccount: () => window.deleteAccount, - resetDatabase: () => window.resetDatabase, - attemptConnection: () => window.attemptConnection, -}; +declare const window: Window; + +// Utilities +export const WebAPI = window.WebAPI; +export const Events = window.Events; +export const Signal = window.Signal; +export const Whisper = window.Whisper; +export const ConversationController = window.ConversationController; +export const passwordUtil = window.passwordUtil; + +// Values +export const CONSTANTS = window.CONSTANTS; +export const versionInfo = window.versionInfo; +export const mnemonic = window.mnemonic; +export const lokiFeatureFlags = window.lokiFeatureFlags; + +// Getters +export const getAccountManager = window.getAccountManager; +export const getConversations = window.getConversations; +export const getFriendsFromContacts = window.getFriendsFromContacts; +export const getSettingValue = window.getSettingValue; + +// Setters +export const setPassword = window.setPassword; +export const setSettingValue = window.setSettingValue; + +// UI Events +export const pushToast = window.pushToast; +export const confirmationDialog = window.confirmationDialog; + +export const showQRDialog = window.showQRDialog; +export const showSeedDialog = window.showSeedDialog; +export const showPasswordDialog = window.showPasswordDialog; +export const showEditProfileDialog = window.showEditProfileDialog; + +export const toggleTheme = window.toggleTheme; +export const toggleMenuBar = window.toggleMenuBar; +export const toggleSpellCheck = window.toggleSpellCheck; +export const toggleLinkPreview = window.toggleLinkPreview; +export const toggleMediaPermissions = window.toggleMediaPermissions; + +// Actions +export const clearLocalData = window.clearLocalData; +export const deleteAccount = window.deleteAccount; +export const resetDatabase = window.resetDatabase; +export const attemptConnection = window.attemptConnection; From 85d3c35c0cc39271fb043c08f4c60ba6303d1c06 Mon Sep 17 00:00:00 2001 From: Mikunj <mikunj@live.com.au> Date: Thu, 28 May 2020 12:46:42 +1000 Subject: [PATCH 10/10] Fix types --- js/modules/data.d.ts | 109 +++++++++++++++++++++++++++---------------- 1 file changed, 69 insertions(+), 40 deletions(-) diff --git a/js/modules/data.d.ts b/js/modules/data.d.ts index e154cbf3c..92441555f 100644 --- a/js/modules/data.d.ts +++ b/js/modules/data.d.ts @@ -7,25 +7,46 @@ type IdentityKey = { firstUse: boolean; verified: number; nonblockingApproval: boolean; -} | null; +}; type PreKey = { - id: string; - publicKey: string; - privateKey: string; + id: number; + publicKey: ArrayBuffer; + privateKey: ArrayBuffer; recipient: string; -} | null; +}; + +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: string; - grantSignature: string | null; -} | null; - -type PairingAuthorisationInit = { - requestSignature: string; - grantSignature: string; + requestSignature: ArrayBuffer; + grantSignature: ArrayBuffer | null; }; type GuardNode = { @@ -67,53 +88,61 @@ export function removeIndexedDBFiles(): Promise<void>; export function getPasswordHash(): Promise<string | null>; // Identity Keys -export function createOrUpdateIdentityKey(data: any): Promise<void>; -export function getIdentityKeyById(id: string): Promise<IdentityKey>; +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: string): Promise<PreKey>; -export function getPreKeyByRecipient(recipient: string): Promise<PreKey>; +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: string): Promise<void>; +export function removePreKeyById(id: number): Promise<void>; export function getAllPreKeys(): Promise<Array<PreKey>>; // Signed Pre Keys -export function createOrUpdateSignedPreKey(data: PreKey): Promise<void>; -export function getSignedPreKeyById(id: string): Promise<PreKey>; -export function getAllSignedPreKeys(recipient: string): Promise<PreKey>; -export function bulkAddSignedPreKeys(array: Array<PreKey>): Promise<void>; -export function removeSignedPreKeyById(id: string): Promise<void>; +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: PreKey): Promise<void>; -export function getContactPreKeyById(id: string): Promise<PreKey>; -export function getContactPreKeyByIdentityKey(key: string): Promise<PreKey>; +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: string, + keyId: number, identityKeyString: string -): Promise<Array<PreKey>>; -export function getAllContactPreKeys(): Promise<Array<PreKey>>; -export function bulkAddContactPreKeys(array: Array<PreKey>): Promise<void>; -export function removeContactPreKeyByIdentityKey(id: string): Promise<void>; +): 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: PreKey): Promise<void>; -export function getContactSignedPreKeyByIdid(string): Promise<PreKey>; +export function createOrUpdateContactSignedPreKey( + data: ContactSignedPreKey +): Promise<void>; +export function getContactSignedPreKeyById( + id: number +): Promise<ContactSignedPreKey | null>; export function getContactSignedPreKeyByIdentityKey( key: string -): Promise<PreKey>; +): Promise<ContactSignedPreKey | null>; export function getContactSignedPreKeys( - keyId: string, + keyId: number, identityKeyString: string -): Promise<Array<PreKey>>; +): Promise<Array<ContactSignedPreKey>>; export function bulkAddContactSignedPreKeys( - array: Array<PreKey> + array: Array<ContactSignedPreKey> ): Promise<void>; export function removeContactSignedPreKeyByIdentityKey( id: string @@ -122,8 +151,8 @@ export function removeAllContactSignedPreKeys(): Promise<void>; // Authorisations & Linking export function createOrUpdatePairingAuthorisation( - data: PairingAuthorisationInit -): Promise<PairingAuthorisation>; + data: PairingAuthorisation +): Promise<void>; export function removePairingAuthorisationForSecondaryPubKey( pubKey: string ): Promise<void>; @@ -132,10 +161,10 @@ export function getGrantAuthorisationsForPrimaryPubKey( ): Promise<Array<PairingAuthorisation>>; export function getGrantAuthorisationForSecondaryPubKey( pubKey: string -): Promise<PairingAuthorisation>; +): Promise<PairingAuthorisation | null>; export function getAuthorisationForSecondaryPubKey( pubKey: string -): Promise<PairingAuthorisation>; +): Promise<PairingAuthorisation | null>; export function getSecondaryDevicesFor( primaryDevicePubKey: string ): Promise<Array<string>>;