Revert pulls
parent
d31c250e2d
commit
dec7aa10c8
@ -1,36 +1,115 @@
|
||||
import * as Data from '../../../js/modules/data';
|
||||
import { RawMessage } from '../types/RawMessage';
|
||||
import { OutgoingContentMessage } from '../messages/outgoing';
|
||||
import { ChatMessage, ContentMessage } from '../messages/outgoing';
|
||||
import { MessageUtils, PubKey } from '../utils';
|
||||
|
||||
// TODO: We should be able to import functions straight from the db here without going through the window object
|
||||
|
||||
|
||||
// This is an abstraction for storing pending messages.
|
||||
// Ideally we want to store pending messages in the database so that
|
||||
// on next launch we can re-send the pending messages, but we don't want
|
||||
// to constantly fetch pending messages from the database.
|
||||
// Thus we have an intermediary cache which will store pending messagesin
|
||||
// memory and sync its state with the database on modification (add or remove).
|
||||
|
||||
export class PendingMessageCache {
|
||||
private readonly cachedMessages: Array<RawMessage> = [];
|
||||
public cache: Array<RawMessage>;
|
||||
|
||||
constructor() {
|
||||
// TODO: We should load pending messages from db here
|
||||
// Load pending messages from the database
|
||||
// You must call init() on this class in order to load from DB.
|
||||
// const pendingMessageCache = new PendingMessageCache();
|
||||
// await pendingMessageCache.init()
|
||||
// >> do stuff
|
||||
this.cache = [];
|
||||
}
|
||||
|
||||
public async add(device: PubKey, message: ContentMessage): Promise<RawMessage> {
|
||||
const rawMessage = MessageUtils.toRawMessage(device, message);
|
||||
|
||||
// Does it exist in cache already?
|
||||
if(this.find(rawMessage)) {
|
||||
return rawMessage;
|
||||
}
|
||||
|
||||
this.cache.push(rawMessage);
|
||||
await this.syncCacheWithDB();
|
||||
|
||||
return rawMessage;
|
||||
}
|
||||
|
||||
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 async remove(message: RawMessage): Promise<Array<RawMessage> | undefined> {
|
||||
// Should only be called after message is processed
|
||||
|
||||
// Return if message doesn't exist in cache
|
||||
if (!this.find(message)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove item from cache and sync with database
|
||||
const updatedCache = this.cache.filter(
|
||||
m => m.identifier !== message.identifier
|
||||
);
|
||||
this.cache = updatedCache;
|
||||
await this.syncCacheWithDB();
|
||||
|
||||
return updatedCache;
|
||||
}
|
||||
|
||||
public removePendingMessage(message: RawMessage) {
|
||||
// TODO: implement
|
||||
public find(message: RawMessage): RawMessage | undefined {
|
||||
// Find a message in the cache
|
||||
return this.cache.find(
|
||||
m => m.device === message.device && m.timestamp === message.timestamp
|
||||
);
|
||||
}
|
||||
|
||||
public getPendingDevices(): Array<String> {
|
||||
// TODO: this should return all devices which have pending messages
|
||||
return [];
|
||||
public getForDevice(device: PubKey): Array<RawMessage> {
|
||||
return this.cache.filter(m => m.device === device.key);
|
||||
}
|
||||
|
||||
public getPendingMessages(device: string): Array<RawMessage> {
|
||||
return [];
|
||||
public async clear() {
|
||||
// Clears the cache and syncs to DB
|
||||
this.cache = [];
|
||||
await this.syncCacheWithDB();
|
||||
}
|
||||
}
|
||||
|
||||
public getDevices(): Array<PubKey> {
|
||||
// Gets all devices with pending messages
|
||||
const pubkeys = [...new Set(this.cache.map(m => m.device))];
|
||||
|
||||
return pubkeys.map(d => PubKey.from(d));
|
||||
}
|
||||
|
||||
public async init() {
|
||||
const messages = await this.getFromStorage();
|
||||
this.cache = messages;
|
||||
}
|
||||
|
||||
public async getFromStorage(): Promise<Array<RawMessage>> {
|
||||
// tslint:disable-next-line: no-backbone-get-set-outside-model
|
||||
const pendingMessagesData = await Data.getItemById('pendingMessages');
|
||||
const pendingMessagesJSON = pendingMessagesData
|
||||
? String(pendingMessagesData.value)
|
||||
: '';
|
||||
|
||||
// tslint:disable-next-line: no-unnecessary-local-variable
|
||||
const encodedPendingMessages = pendingMessagesJSON
|
||||
? JSON.parse(pendingMessagesJSON)
|
||||
: [];
|
||||
|
||||
// Set pubkey from string to PubKey.from()
|
||||
|
||||
|
||||
// TODO:
|
||||
// Build up Uint8Array from painTextBuffer in JSON
|
||||
return encodedPendingMessages;
|
||||
}
|
||||
|
||||
public async syncCacheWithDB() {
|
||||
// Only call when adding / removing from cache.
|
||||
const encodedPendingMessages = JSON.stringify(this.cache) || '';
|
||||
await Data.createOrUpdateItem({id: 'pendingMessages', value: encodedPendingMessages});
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,91 @@
|
||||
import { RawMessage } from '../types/RawMessage';
|
||||
import { ContentMessage } from '../messages/outgoing';
|
||||
import { EncryptionType } from '../types/EncryptionType';
|
||||
import * as crypto from 'crypto';
|
||||
|
||||
|
||||
function toRawMessage(device: PubKey, message: ContentMessage): RawMessage {
|
||||
const ttl = message.ttl();
|
||||
const timestamp = message.timestamp;
|
||||
const plainTextBuffer = message.plainTextBuffer();
|
||||
|
||||
// Get EncryptionType depending on message type.
|
||||
// let encryption: EncryptionType;
|
||||
|
||||
// switch (message.constructor.name) {
|
||||
// case MessageType.Chat:
|
||||
// encryption = EncryptionType.Signal;
|
||||
// break;
|
||||
// case MessageType.SessionReset:
|
||||
// encryption = EncryptionType
|
||||
// }
|
||||
|
||||
// export enum EncryptionType {
|
||||
// Signal,
|
||||
// SessionReset,
|
||||
// MediumGroup,
|
||||
// }
|
||||
|
||||
// tslint:disable-next-line: no-unnecessary-local-variable
|
||||
const rawMessage: RawMessage = {
|
||||
identifier: message.identifier,
|
||||
plainTextBuffer,
|
||||
timestamp,
|
||||
device: device.key,
|
||||
ttl,
|
||||
encryption: EncryptionType.Signal,
|
||||
};
|
||||
|
||||
return rawMessage;
|
||||
}
|
||||
|
||||
|
||||
export enum PubKeyType {
|
||||
Primary = 'priamry',
|
||||
Secondary = 'secondary',
|
||||
Group = 'group',
|
||||
}
|
||||
|
||||
export class PubKey {
|
||||
private static readonly regex: string = '^0[0-9a-fA-F]{65}$';
|
||||
public readonly key: string;
|
||||
public type?: PubKeyType;
|
||||
|
||||
constructor(pubkeyString: string, type?: PubKeyType) {
|
||||
PubKey.validate(pubkeyString);
|
||||
this.key = pubkeyString;
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public static from(pubkeyString: string): PubKey {
|
||||
// Returns a new instance if the pubkey is valid
|
||||
if (PubKey.validate(pubkeyString)) {
|
||||
return new PubKey(pubkeyString);
|
||||
}
|
||||
|
||||
throw new Error('Invalid pubkey format');
|
||||
}
|
||||
|
||||
public static validate(pubkeyString: string): boolean {
|
||||
if (pubkeyString.match(PubKey.regex)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
throw new Error('Invalid pubkey format');
|
||||
}
|
||||
|
||||
public static generate(): PubKey {
|
||||
// Generates a mock pubkey for testing
|
||||
const PUBKEY_LEN = 66;
|
||||
const numBytes = PUBKEY_LEN / 2;
|
||||
const hexBuffer = crypto.randomBytes(numBytes).toString('hex');
|
||||
const pubkeyString = `0${hexBuffer}`.slice(0, PUBKEY_LEN);
|
||||
|
||||
return new PubKey(pubkeyString);
|
||||
}
|
||||
}
|
||||
|
||||
// Functions / Tools
|
||||
export const MessageUtils = {
|
||||
toRawMessage,
|
||||
};
|
@ -1,2 +1,3 @@
|
||||
export * from './TypedEmitter';
|
||||
export * from './JobQueue';
|
||||
export * from './MessageUtils';
|
||||
|
@ -0,0 +1,33 @@
|
||||
// import { expect } from 'chai';
|
||||
|
||||
// import { ChatMessage, SessionResetMessage, } from '../../../session/messages/outgoing';
|
||||
// import { TextEncoder } from 'util';
|
||||
// import { MessageUtils } from '../../../session/utils';
|
||||
// import { PendingMessageCache } from '../../../session/sending/PendingMessageCache';
|
||||
|
||||
// describe('PendingMessageCache', () => {
|
||||
// const pendingMessageCache = new PendingMessageCache();
|
||||
|
||||
// let sessionResetMessage: SessionResetMessage;
|
||||
// const preKeyBundle = {
|
||||
// deviceId: 123456,
|
||||
// preKeyId: 654321,
|
||||
// signedKeyId: 111111,
|
||||
// preKey: new TextEncoder().encode('preKey'),
|
||||
// signature: new TextEncoder().encode('signature'),
|
||||
// signedKey: new TextEncoder().encode('signedKey'),
|
||||
// identityKey: new TextEncoder().encode('identityKey'),
|
||||
// };
|
||||
|
||||
|
||||
// // queue with session reset message.
|
||||
// // should return undefined
|
||||
// // TOOD: Send me to MESSAGE QUEUE TEST
|
||||
// it('queue session reset message', () => {
|
||||
// const timestamp = Date.now();
|
||||
// sessionResetMessage = new SessionResetMessage({timestamp, preKeyBundle});
|
||||
|
||||
|
||||
// });
|
||||
|
||||
// });
|
@ -0,0 +1,212 @@
|
||||
// tslint:disable-next-line: no-require-imports no-var-requires
|
||||
const Data = require('../../../../js/modules/data');
|
||||
|
||||
import { expect, assert } from 'chai';
|
||||
import sinon from 'sinon';
|
||||
import uuid from 'uuid';
|
||||
|
||||
import { ChatMessage } from '../../../session/messages/outgoing';
|
||||
import { MessageUtils, PubKey } from '../../../session/utils';
|
||||
import { PendingMessageCache } from '../../../session/sending/PendingMessageCache';
|
||||
import { RawMessage } from '../../../session/types/RawMessage';
|
||||
import { SignalService } from '../../../protobuf';
|
||||
|
||||
describe('PendingMessageCache', () => {
|
||||
const sandbox = sinon.createSandbox();
|
||||
let pendingMessageCacheStub: PendingMessageCache;
|
||||
|
||||
// tslint:disable-next-line: promise-function-async
|
||||
const wrapInPromise = (value: any) => new Promise(r => {
|
||||
r(value);
|
||||
});
|
||||
|
||||
const generateUniqueMessage = (): ChatMessage => {
|
||||
return new ChatMessage({
|
||||
body: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit',
|
||||
identifier: uuid(),
|
||||
timestamp: Date.now(),
|
||||
attachments: undefined,
|
||||
quote: undefined,
|
||||
expireTimer: undefined,
|
||||
lokiProfile: undefined,
|
||||
preview: undefined,
|
||||
});
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
const mockStorageObject = wrapInPromise([] as Array<RawMessage>);
|
||||
const voidPromise = wrapInPromise(undefined);
|
||||
|
||||
// Stub out methods which touch the database.
|
||||
sandbox.stub(PendingMessageCache.prototype, 'getFromStorage').returns(mockStorageObject);
|
||||
sandbox.stub(PendingMessageCache.prototype, 'syncCacheWithDB').returns(voidPromise);
|
||||
|
||||
// Initialize new stubbed cache
|
||||
pendingMessageCacheStub = new PendingMessageCache();
|
||||
await pendingMessageCacheStub.init();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
|
||||
it('can initialize cache', async () => {
|
||||
const { cache } = pendingMessageCacheStub;
|
||||
|
||||
// We expect the cache to initialise as an empty array
|
||||
expect(cache).to.be.instanceOf(Array);
|
||||
expect(cache).to.have.length(0);
|
||||
});
|
||||
|
||||
|
||||
it('can add to cache', async () => {
|
||||
const device = PubKey.generate();
|
||||
const message = generateUniqueMessage();
|
||||
const rawMessage = MessageUtils.toRawMessage(device, message);
|
||||
|
||||
await pendingMessageCacheStub.add(device, message);
|
||||
|
||||
// Verify that the message is in the cache
|
||||
const finalCache = pendingMessageCacheStub.cache;
|
||||
|
||||
expect(finalCache).to.have.length(1);
|
||||
|
||||
const addedMessage = finalCache[0];
|
||||
expect(addedMessage.device).to.deep.equal(rawMessage.device);
|
||||
expect(addedMessage.timestamp).to.deep.equal(rawMessage.timestamp);
|
||||
});
|
||||
|
||||
it('can remove from cache', async () => {
|
||||
const device = PubKey.generate();
|
||||
const message = generateUniqueMessage();
|
||||
const rawMessage = MessageUtils.toRawMessage(device, message);
|
||||
|
||||
await pendingMessageCacheStub.add(device, message);
|
||||
|
||||
const initialCache = pendingMessageCacheStub.cache;
|
||||
expect(initialCache).to.have.length(1);
|
||||
|
||||
// Remove the message
|
||||
await pendingMessageCacheStub.remove(rawMessage);
|
||||
|
||||
const finalCache = pendingMessageCacheStub.cache;
|
||||
|
||||
// Verify that the message was removed
|
||||
expect(finalCache).to.have.length(0);
|
||||
});
|
||||
|
||||
it('can get devices', async () => {
|
||||
const cacheItems = [
|
||||
{
|
||||
device: PubKey.generate(),
|
||||
message: generateUniqueMessage(),
|
||||
},
|
||||
{
|
||||
device: PubKey.generate(),
|
||||
message: generateUniqueMessage(),
|
||||
},
|
||||
{
|
||||
device: PubKey.generate(),
|
||||
message: generateUniqueMessage(),
|
||||
},
|
||||
];
|
||||
|
||||
cacheItems.forEach(async item => {
|
||||
await pendingMessageCacheStub.add(item.device, item.message);
|
||||
});
|
||||
|
||||
const { cache } = pendingMessageCacheStub;
|
||||
expect(cache).to.have.length(cacheItems.length);
|
||||
|
||||
// Get list of devices
|
||||
const devicesKeys = cacheItems.map(item => item.device.key);
|
||||
const pulledDevices = pendingMessageCacheStub.getDevices();
|
||||
const pulledDevicesKeys = pulledDevices.map(d => d.key);
|
||||
|
||||
// Verify that device list from cache is equivalent to devices added
|
||||
expect(pulledDevicesKeys).to.have.members(devicesKeys);
|
||||
});
|
||||
|
||||
it('can get pending for device', async () => {
|
||||
const cacheItems = [
|
||||
{
|
||||
device: PubKey.generate(),
|
||||
message: generateUniqueMessage(),
|
||||
},
|
||||
{
|
||||
device: PubKey.generate(),
|
||||
message: generateUniqueMessage(),
|
||||
},
|
||||
];
|
||||
|
||||
cacheItems.forEach(async item => {
|
||||
await pendingMessageCacheStub.add(item.device, item.message);
|
||||
});
|
||||
|
||||
const initialCache = pendingMessageCacheStub.cache;
|
||||
expect(initialCache).to.have.length(cacheItems.length);
|
||||
|
||||
// Get pending for each specific device
|
||||
cacheItems.forEach(item => {
|
||||
const pendingForDevice = pendingMessageCacheStub.getForDevice(item.device);
|
||||
expect(pendingForDevice).to.have.length(1);
|
||||
expect(pendingForDevice[0].device).to.equal(item.device.key);
|
||||
});
|
||||
});
|
||||
|
||||
it('can find nothing when empty', async () => {
|
||||
const device = PubKey.generate();
|
||||
const message = generateUniqueMessage();
|
||||
const rawMessage = MessageUtils.toRawMessage(device, message);
|
||||
|
||||
const foundMessage = pendingMessageCacheStub.find(rawMessage);
|
||||
expect(foundMessage, 'a message was found in empty cache').to.be.undefined;
|
||||
});
|
||||
|
||||
it('can find message in cache', async () => {
|
||||
const device = PubKey.generate();
|
||||
const message = generateUniqueMessage();
|
||||
const rawMessage = MessageUtils.toRawMessage(device, message);
|
||||
|
||||
await pendingMessageCacheStub.add(device, message);
|
||||
|
||||
const finalCache = pendingMessageCacheStub.cache;
|
||||
expect(finalCache).to.have.length(1);
|
||||
|
||||
const foundMessage = pendingMessageCacheStub.find(rawMessage);
|
||||
expect(foundMessage, 'message not found in cache').to.be.ok;
|
||||
foundMessage && expect(foundMessage.device).to.equal(device.key);
|
||||
});
|
||||
|
||||
it('can clear cache', async () => {
|
||||
const cacheItems = [
|
||||
{
|
||||
device: PubKey.generate(),
|
||||
message: generateUniqueMessage(),
|
||||
},
|
||||
{
|
||||
device: PubKey.generate(),
|
||||
message: generateUniqueMessage(),
|
||||
},
|
||||
{
|
||||
device: PubKey.generate(),
|
||||
message: generateUniqueMessage(),
|
||||
},
|
||||
];
|
||||
|
||||
cacheItems.forEach(async item => {
|
||||
await pendingMessageCacheStub.add(item.device, item.message);
|
||||
});
|
||||
|
||||
const initialCache = pendingMessageCacheStub.cache;
|
||||
expect(initialCache).to.have.length(cacheItems.length);
|
||||
|
||||
// Clear cache
|
||||
await pendingMessageCacheStub.clear();
|
||||
|
||||
const finalCache = pendingMessageCacheStub.cache;
|
||||
expect(finalCache).to.have.length(0);
|
||||
});
|
||||
|
||||
});
|
@ -0,0 +1,72 @@
|
||||
import { expect, should } from 'chai';
|
||||
import { SignalService } from '../../../protobuf';
|
||||
import { ChatMessage } from '../../../session/messages/outgoing';
|
||||
import { RawMessage } from '../../../session/types/RawMessage';
|
||||
import { MessageUtils, PubKey, PubKeyType } from '../../../session/utils';
|
||||
|
||||
describe('MessageUtils', () => {
|
||||
it('can convert to RawMessage', () => {
|
||||
// TOOD: MOVE ME TO MESSAGE UTILS TEST
|
||||
const pubkey = "0582fe8822c684999663cc6636148328fbd47c0836814c118af4e326bb4f0e1000";
|
||||
const messageText = "This is some message content";
|
||||
|
||||
const isRawMessage = (object: any): object is RawMessage => {
|
||||
return (
|
||||
'identifier' in object &&
|
||||
'plainTextBuffer' in object &&
|
||||
'timestamp' in object &&
|
||||
'device' in object &&
|
||||
'ttl' in object &&
|
||||
'encryption' in object
|
||||
);
|
||||
}
|
||||
|
||||
const message = new ChatMessage({
|
||||
body: messageText,
|
||||
identifier: '1234567890',
|
||||
timestamp: Date.now(),
|
||||
attachments: undefined,
|
||||
quote: undefined,
|
||||
expireTimer: undefined,
|
||||
lokiProfile: undefined,
|
||||
preview: undefined,
|
||||
});
|
||||
|
||||
// Explicitly check that it's a RawMessage
|
||||
const rawMessage = MessageUtils.toRawMessage(pubkey, message);
|
||||
expect(isRawMessage(rawMessage)).to.be.equal(true);
|
||||
|
||||
// console.log('[vince] isRawMessage(rawMessage):', isRawMessage(rawMessage));
|
||||
|
||||
// Check plaintext
|
||||
const plainText = message.plainTextBuffer();
|
||||
const decoded = SignalService.Content.decode(plainText);
|
||||
expect(decoded.dataMessage?.body).to.be.equal(messageText);
|
||||
});
|
||||
|
||||
// Pubkeys
|
||||
it('can create new valid pubkey', () => {
|
||||
const validPubkey = '0582fe8822c684999663cc6636148328fbd47c0836814c118af4e326bb4f0e1000';
|
||||
should().not.Throw(() => new PubKey(validPubkey), Error);
|
||||
|
||||
const pubkey = new PubKey(validPubkey);
|
||||
expect(pubkey instanceof PubKey).to.be.equal(true);
|
||||
});
|
||||
|
||||
it('invalid pubkey should throw error', () => {
|
||||
const invalidPubkey = 'Lorem Ipsum';
|
||||
|
||||
should().Throw(() => new PubKey(invalidPubkey), Error);
|
||||
});
|
||||
|
||||
it('can set pubkey type', () => {
|
||||
const validPubkey = '0582fe8822c684999663cc6636148328fbd47c0836814c118af4e326bb4f0e1000';
|
||||
const pubkeyType = PubKeyType.Primary;
|
||||
|
||||
should().not.Throw(() => new PubKey(validPubkey, pubkeyType), Error);
|
||||
|
||||
const pubkey = new PubKey(validPubkey, pubkeyType);
|
||||
expect(pubkey.type).to.be.equal(PubKeyType.Primary);
|
||||
});
|
||||
|
||||
});
|
@ -0,0 +1,27 @@
|
||||
import { SignalProtocolAddress } from '../../../window/types/libsignal-protocol';
|
||||
|
||||
export class SignalProtocolAddressStub extends SignalProtocolAddress {
|
||||
private readonly hexEncodedPublicKey: string;
|
||||
private readonly deviceId: number;
|
||||
constructor(hexEncodedPublicKey: string, deviceId: number) {
|
||||
super(hexEncodedPublicKey, deviceId);
|
||||
this.hexEncodedPublicKey = hexEncodedPublicKey;
|
||||
this.deviceId = deviceId;
|
||||
}
|
||||
|
||||
// tslint:disable-next-line: function-name
|
||||
public static fromString(encodedAddress: string): SignalProtocolAddressStub {
|
||||
const values = encodedAddress.split('.');
|
||||
|
||||
return new SignalProtocolAddressStub(values[0], Number(values[1]));
|
||||
}
|
||||
|
||||
public getName(): string { return this.hexEncodedPublicKey; }
|
||||
public getDeviceId(): number { return this.deviceId; }
|
||||
|
||||
public equals(other: SignalProtocolAddress): boolean {
|
||||
return other.getName() === this.hexEncodedPublicKey;
|
||||
}
|
||||
|
||||
public toString(): string { return this.hexEncodedPublicKey; }
|
||||
}
|
Loading…
Reference in New Issue