Merge branch 'clearnet' into refactor-polling

pull/1204/head
Maxim Shishmarev 5 years ago
commit 6fcc1f7ae4

@ -3,6 +3,7 @@ interface ConversationAttributes {
left: boolean;
expireTimer: number;
profileSharing: boolean;
secondaryStatus: boolean;
mentionedUs: boolean;
unreadCount: number;
isArchived: boolean;

@ -1317,8 +1317,7 @@
});
if (this.isMe()) {
await message.markMessageSyncOnly();
// sending is done in the 'private' case below
return message.sendSyncMessageOnly(chatMessage);
}
const options = {};
@ -1648,8 +1647,10 @@
};
if (this.isMe()) {
await message.markMessageSyncOnly();
// sending of the message is handled in the 'private' case below
const expirationTimerMessage = new libsession.Messages.Outgoing.ExpirationTimerUpdateMessage(
expireUpdate
);
return message.sendSyncMessageOnly(expirationTimerMessage);
}
if (this.get('type') === 'private') {
@ -1856,14 +1857,12 @@
const groupUpdateMessage = new libsession.Messages.Outgoing.ClosedGroupUpdateMessage(
updateParams
);
libsession
.getMessageQueue()
.sendToGroup(groupUpdateMessage)
.catch(log.error);
await this.sendClosedGroupMessageWithSync(groupUpdateMessage);
},
sendGroupInfo(recipient) {
if (this.isClosedGroup()) {
// Only send group info if we're a closed group and we haven't left
if (this.isClosedGroup() && !this.get('left')) {
const updateParams = {
timestamp: Date.now(),
groupId: this.id,
@ -1927,12 +1926,46 @@
quitGroup
);
await libsession.getMessageQueue().sendToGroup(quitGroupMessage);
await this.sendClosedGroupMessageWithSync(quitGroupMessage);
this.updateTextInputState();
}
},
async sendClosedGroupMessageWithSync(message) {
const {
ClosedGroupMessage,
ClosedGroupChatMessage,
} = libsession.Messages.Outgoing;
if (!(message instanceof ClosedGroupMessage)) {
throw new Error('Invalid closed group message.');
}
// Sync messages for Chat Messages need to be constructed after confirming send was successful.
if (message instanceof ClosedGroupChatMessage) {
throw new Error(
'ClosedGroupChatMessage should be constructed manually and sent'
);
}
try {
await libsession.getMessageQueue().sendToGroup(message);
const syncMessage = libsession.Utils.SyncMessageUtils.getSentSyncMessage(
{
destination: message.groupId,
message,
}
);
if (syncMessage) {
await libsession.getMessageQueue().sendSyncMessage(syncMessage);
}
} catch (e) {
window.log.error(e);
}
},
async markRead(newestUnreadDate, providedOptions) {
const options = providedOptions || {};
_.defaults(options, { sendReadReceipts: true });

@ -1079,8 +1079,7 @@
if (recipients.length === 1 && recipients[0] === this.OUR_NUMBER) {
this.trigger('pending');
// FIXME audric add back profileKey
await this.markMessageSyncOnly();
// sending is done in the private case below
return this.sendSyncMessageOnly(chatMessage);
}
if (conversation.isPrivate()) {
@ -1155,9 +1154,9 @@
// Special-case the self-send case - we send only a sync message
if (number === this.OUR_NUMBER) {
await this.markMessageSyncOnly();
// sending is done in the private case below
return this.sendSyncMessageOnly(chatMessage);
}
const conversation = this.getConversation();
const recipientPubKey = new libsession.Types.PubKey(number);
@ -1322,6 +1321,44 @@
Message: Whisper.Message,
});
},
async sendSyncMessageOnly(dataMessage) {
this.set({
sent_to: [this.OUR_NUMBER],
sent: true,
expirationStartTimestamp: Date.now(),
});
await window.Signal.Data.saveMessage(this.attributes, {
Message: Whisper.Message,
});
const data =
dataMessage instanceof libsession.Messages.Outgoing.DataMessage
? dataMessage.dataProto()
: dataMessage;
await this.sendSyncMessage(data);
},
async sendSyncMessage(dataMessage) {
// TODO: Return here if we've already sent a sync message
if (this.get('synced')) {
return;
}
const syncMessage = new libsession.Messages.Outgoing.SentSyncMessage({
timestamp: this.get('sent_at'),
identifier: this.id,
dataMessage,
destination: this.get('destination'),
expirationStartTimestamp: this.get('expirationStartTimestamp'),
sent_to: this.get('sent_to'),
unidentifiedDeliveries: this.get('unidentifiedDeliveries'),
});
await libsession.getMessageQueue().sendSyncMessage(syncMessage);
},
send(promise) {
this.trigger('pending');
return promise

@ -1,7 +1,7 @@
import { ConversationType } from '../../ts/state/ducks/conversations';
import { Mesasge } from '../../ts/types/Message';
import { Message } from '../../ts/types/Message';
type IdentityKey = {
export type IdentityKey = {
id: string;
publicKey: ArrayBuffer;
firstUse: boolean;
@ -9,14 +9,14 @@ type IdentityKey = {
nonblockingApproval: boolean;
};
type PreKey = {
export type PreKey = {
id: number;
publicKey: ArrayBuffer;
privateKey: ArrayBuffer;
recipient: string;
};
type SignedPreKey = {
export type SignedPreKey = {
id: number;
publicKey: ArrayBuffer;
privateKey: ArrayBuffer;
@ -25,14 +25,14 @@ type SignedPreKey = {
signature: ArrayBuffer;
};
type ContactPreKey = {
export type ContactPreKey = {
id: number;
identityKeyString: string;
publicKey: ArrayBuffer;
keyId: number;
};
type ContactSignedPreKey = {
export type ContactSignedPreKey = {
id: number;
identityKeyString: string;
publicKey: ArrayBuffer;
@ -42,18 +42,18 @@ type ContactSignedPreKey = {
confirmed: boolean;
};
type PairingAuthorisation = {
export type PairingAuthorisation = {
primaryDevicePubKey: string;
secondaryDevicePubKey: string;
requestSignature: ArrayBuffer;
grantSignature?: ArrayBuffer;
};
type GuardNode = {
export type GuardNode = {
ed25519PubKey: string;
};
type SwarmNode = {
export type SwarmNode = {
address: string;
ip: string;
port: string;
@ -61,19 +61,19 @@ type SwarmNode = {
pubkey_x25519: string;
};
type StorageItem = {
export type StorageItem = {
id: string;
value: any;
};
type SessionDataInfo = {
export type SessionDataInfo = {
id: string;
number: string;
deviceId: number;
record: string;
};
type ServerToken = {
export type ServerToken = {
serverUrl: string;
token: string;
};

@ -71,11 +71,14 @@ export async function preprocessGroupMessage(
return true;
}
}
if (group.type === GROUP_TYPES.REQUEST_INFO && !newGroup) {
window.libloki.api.debug.logGroupRequestInfo(
`Received GROUP_TYPES.REQUEST_INFO from source: ${source}, primarySource: ${primarySource}, sending back group info.`
);
conversation.sendGroupInfo(source);
if (group.type === GROUP_TYPES.REQUEST_INFO) {
// We can only send the request info back if we have the information
if (!newGroup) {
window.libloki.api.debug.logGroupRequestInfo(
`Received GROUP_TYPES.REQUEST_INFO from source: ${source}, primarySource: ${primarySource}, sending back group info.`
);
conversation.sendGroupInfo(source);
}
return true;
}

@ -1,8 +1,8 @@
import { MessageQueue } from './sending/';
import { MessageQueue, MessageQueueInterface } from './sending/';
let messageQueue: MessageQueue;
function getMessageQueue() {
function getMessageQueue(): MessageQueueInterface {
if (!messageQueue) {
messageQueue = new MessageQueue();
}

@ -2,11 +2,11 @@ import { ContentMessage } from '../ContentMessage';
import { SignalService } from '../../../../../protobuf';
export abstract class DataMessage extends ContentMessage {
public abstract dataProto(): SignalService.DataMessage;
protected contentProto(): SignalService.Content {
return new SignalService.Content({
dataMessage: this.dataProto(),
});
}
protected abstract dataProto(): SignalService.DataMessage;
}

@ -6,7 +6,7 @@ export class DeviceUnlinkMessage extends DataMessage {
return 4 * 24 * 60 * 60 * 1000; // 4 days for device unlinking
}
protected dataProto(): SignalService.DataMessage {
public dataProto(): SignalService.DataMessage {
const flags = SignalService.DataMessage.Flags.UNPAIRING_REQUEST;
return new SignalService.DataMessage({

@ -29,7 +29,7 @@ export class ExpirationTimerUpdateMessage extends DataMessage {
return this.getDefaultTTL();
}
protected dataProto(): SignalService.DataMessage {
public dataProto(): SignalService.DataMessage {
const data = new SignalService.DataMessage();
const groupMessage = new SignalService.GroupContext();

@ -24,7 +24,7 @@ export class GroupInvitationMessage extends DataMessage {
return this.getDefaultTTL();
}
protected dataProto(): SignalService.DataMessage {
public dataProto(): SignalService.DataMessage {
const groupInvitation = new SignalService.DataMessage.GroupInvitation({
serverAddress: this.serverAddress,
channelId: this.channelId,

@ -25,6 +25,13 @@ export class ClosedGroupChatMessage extends ClosedGroupMessage {
return this.getDefaultTTL();
}
public dataProto(): SignalService.DataMessage {
const messageProto = this.chatMessage.dataProto();
messageProto.group = this.groupContext();
return messageProto;
}
protected groupContext(): SignalService.GroupContext {
// use the parent method to fill id correctly
const groupContext = super.groupContext();
@ -32,11 +39,4 @@ export class ClosedGroupChatMessage extends ClosedGroupMessage {
return groupContext;
}
protected dataProto(): SignalService.DataMessage {
const messageProto = this.chatMessage.dataProto();
messageProto.group = this.groupContext();
return messageProto;
}
}

@ -23,16 +23,16 @@ export abstract class ClosedGroupMessage extends DataMessage {
return this.getDefaultTTL();
}
protected groupContext(): SignalService.GroupContext {
const id = new Uint8Array(StringUtils.encode(this.groupId.key, 'utf8'));
return new SignalService.GroupContext({ id });
}
protected dataProto(): SignalService.DataMessage {
public dataProto(): SignalService.DataMessage {
const dataMessage = new SignalService.DataMessage();
dataMessage.group = this.groupContext();
return dataMessage;
}
protected groupContext(): SignalService.GroupContext {
const id = new Uint8Array(StringUtils.encode(this.groupId.key, 'utf8'));
return new SignalService.GroupContext({ id });
}
}

@ -21,7 +21,7 @@ export class MediumGroupChatMessage extends MediumGroupMessage {
this.chatMessage = params.chatMessage;
}
protected dataProto(): SignalService.DataMessage {
public dataProto(): SignalService.DataMessage {
const messageProto = this.chatMessage.dataProto();
messageProto.mediumGroupUpdate = super.dataProto().mediumGroupUpdate;

@ -23,14 +23,14 @@ export abstract class MediumGroupMessage extends DataMessage {
return this.getDefaultTTL();
}
protected mediumGroupContext(): SignalService.MediumGroupUpdate {
return new SignalService.MediumGroupUpdate({ groupId: this.groupId.key });
}
protected dataProto(): SignalService.DataMessage {
public dataProto(): SignalService.DataMessage {
const dataMessage = new SignalService.DataMessage();
dataMessage.mediumGroupUpdate = this.mediumGroupContext();
return dataMessage;
}
protected mediumGroupContext(): SignalService.MediumGroupUpdate {
return new SignalService.MediumGroupUpdate({ groupId: this.groupId.key });
}
}

@ -6,7 +6,7 @@ interface ReceiptMessageParams extends MessageParams {
timestamps: Array<number>;
}
export abstract class ReceiptMessage extends ContentMessage {
private readonly timestamps: Array<number>;
public readonly timestamps: Array<number>;
constructor({ timestamp, identifier, timestamps }: ReceiptMessageParams) {
super({ timestamp, identifier });

@ -4,15 +4,15 @@ import { MessageParams } from '../../Message';
import { PubKey } from '../../../../types';
interface SentSyncMessageParams extends MessageParams {
dataMessage: SignalService.DataMessage;
dataMessage: SignalService.IDataMessage;
expirationStartTimestamp?: number;
sentTo?: Array<PubKey>;
unidentifiedDeliveries?: Array<PubKey>;
destination?: PubKey;
destination?: PubKey | string;
}
export abstract class SentSyncMessage extends SyncMessage {
public readonly dataMessage: SignalService.DataMessage;
export class SentSyncMessage extends SyncMessage {
public readonly dataMessage: SignalService.IDataMessage;
public readonly expirationStartTimestamp?: number;
public readonly sentTo?: Array<PubKey>;
public readonly unidentifiedDeliveries?: Array<PubKey>;
@ -25,7 +25,9 @@ export abstract class SentSyncMessage extends SyncMessage {
this.expirationStartTimestamp = params.expirationStartTimestamp;
this.sentTo = params.sentTo;
this.unidentifiedDeliveries = params.unidentifiedDeliveries;
this.destination = params.destination;
const { destination } = params;
this.destination = destination ? PubKey.cast(destination) : undefined;
}
protected syncProto(): SignalService.SyncMessage {

@ -1,13 +1,14 @@
import _ from 'lodash';
import { SyncMessage } from './SyncMessage';
import { SignalService } from '../../../../../protobuf';
import { MessageParams } from '../../Message';
interface SyncReadMessageParams extends MessageParams {
readMessages: any;
readMessages: Array<{ sender: string; timestamp: number }>;
}
export abstract class SyncReadMessage extends SyncMessage {
public readonly readMessages: any;
export class SyncReadMessage extends SyncMessage {
public readonly readMessages: Array<{ sender: string; timestamp: number }>;
constructor(params: SyncReadMessageParams) {
super({ timestamp: params.timestamp, identifier: params.identifier });
@ -19,7 +20,7 @@ export abstract class SyncReadMessage extends SyncMessage {
syncMessage.read = [];
for (const read of this.readMessages) {
const readMessage = new SignalService.SyncMessage.Read();
read.timestamp = readMessage.timestamp;
read.timestamp = _.toNumber(readMessage.timestamp);
read.sender = readMessage.sender;
syncMessage.read.push(readMessage);
}

@ -13,12 +13,7 @@ import {
TypingMessage,
} from '../messages/outgoing';
import { PendingMessageCache } from './PendingMessageCache';
import {
GroupUtils,
JobQueue,
SyncMessageUtils,
TypedEventEmitter,
} from '../utils';
import { GroupUtils, JobQueue, TypedEventEmitter } from '../utils';
import { PubKey } from '../types';
import { MessageSender } from '.';
import { MultiDeviceProtocol, SessionProtocol } from '../protocols';
@ -39,44 +34,20 @@ export class MessageQueue implements MessageQueueInterface {
user: PubKey,
message: ContentMessage
): Promise<void> {
const userDevices = await MultiDeviceProtocol.getAllDevices(user.key);
if (message instanceof SyncMessage) {
return this.sendSyncMessage(message);
}
const userDevices = await MultiDeviceProtocol.getAllDevices(user.key);
await this.sendMessageToDevices(userDevices, message);
}
public async send(device: PubKey, message: ContentMessage): Promise<void> {
await this.sendMessageToDevices([device], message);
}
public async sendMessageToDevices(
devices: Array<PubKey>,
message: ContentMessage
) {
let currentDevices = [...devices];
// Sync to our devices if syncable
if (SyncMessageUtils.canSync(message)) {
const syncMessage = SyncMessageUtils.from(message);
if (!syncMessage) {
throw new Error(
'MessageQueue internal error occured: failed to make sync message'
);
}
await this.sendSyncMessage(syncMessage);
const ourDevices = await MultiDeviceProtocol.getOurDevices();
// Remove our devices from currentDevices
currentDevices = currentDevices.filter(
device => !ourDevices.some(d => device.isEqual(d))
);
if (message instanceof SyncMessage) {
return this.sendSyncMessage(message);
}
const promises = currentDevices.map(async device => {
await this.process(device, message);
});
return Promise.all(promises);
await this.sendMessageToDevices([device], message);
}
public async sendToGroup(
@ -120,7 +91,16 @@ export class MessageQueue implements MessageQueueInterface {
}
// Get devices in group
const recipients = await GroupUtils.getGroupMembers(groupId);
let recipients = await GroupUtils.getGroupMembers(groupId);
// Don't send to our own device as they'll likely be synced across.
const ourKey = await UserUtil.getCurrentDevicePubKey();
if (!ourKey) {
throw new Error('Cannot get current user public key');
}
const ourPrimary = await MultiDeviceProtocol.getPrimaryDevice(ourKey);
recipients = recipients.filter(member => !ourPrimary.isEqual(member));
if (recipients.length === 0) {
return;
}
@ -133,16 +113,15 @@ export class MessageQueue implements MessageQueueInterface {
);
}
public async sendSyncMessage(message: SyncMessage | undefined): Promise<any> {
public async sendSyncMessage(
message: SyncMessage | undefined
): Promise<void> {
if (!message) {
return;
}
const ourDevices = await MultiDeviceProtocol.getOurDevices();
const promises = ourDevices.map(async device =>
this.process(device, message)
);
return Promise.all(promises);
await this.sendMessageToDevices(ourDevices, message);
}
public async processPending(device: PubKey) {
@ -179,6 +158,17 @@ export class MessageQueue implements MessageQueueInterface {
});
}
public async sendMessageToDevices(
devices: Array<PubKey>,
message: ContentMessage
) {
const promises = devices.map(async device => {
await this.process(device, message);
});
return Promise.all(promises);
}
private async processAllPending() {
const devices = await this.pendingMessageCache.getDevices();
const promises = devices.map(async device => this.processPending(device));

@ -20,5 +20,5 @@ export interface MessageQueueInterface {
sendUsingMultiDevice(user: PubKey, message: ContentMessage): Promise<void>;
send(device: PubKey, message: ContentMessage): Promise<void>;
sendToGroup(message: GroupMessageType): Promise<void>;
sendSyncMessage(message: SyncMessage | undefined): Promise<any>;
sendSyncMessage(message: SyncMessage | undefined): Promise<void>;
}

@ -34,7 +34,7 @@ export async function waitForTask<T>(
return Promise.race([timeoutPromise, taskPromise]) as Promise<T>;
}
interface PollOptions {
export interface PollOptions {
timeout: number;
interval: number;
}
@ -43,7 +43,7 @@ interface PollOptions {
* Creates a promise which calls the `task` every `interval` until `done` is called or until `timeout` period is reached.
* If `timeout` is reached then this will throw an Error.
*
* @param check The check which runs every `interval` ms.
* @param task The task which runs every `interval` ms.
* @param options The polling options.
*/
export async function poll(

@ -1,7 +1,7 @@
import ByteBuffer from 'bytebuffer';
type Encoding = 'base64' | 'hex' | 'binary' | 'utf8';
type BufferType = ByteBuffer | Buffer | ArrayBuffer | Uint8Array;
export type Encoding = 'base64' | 'hex' | 'binary' | 'utf8';
export type BufferType = ByteBuffer | Buffer | ArrayBuffer | Uint8Array;
/**
* Take a string value with the given encoding and converts it to an `ArrayBuffer`.

@ -1,25 +1,34 @@
import * as _ from 'lodash';
import { UserUtil } from '../../util/';
import { UserUtil } from '../../util';
import { getAllConversations } from '../../../js/modules/data';
import { ContentMessage, SyncMessage } from '../messages/outgoing';
import { MultiDeviceProtocol } from '../protocols';
import ByteBuffer from 'bytebuffer';
export function from(message: ContentMessage): SyncMessage | undefined {
if (message instanceof SyncMessage) {
return message;
import {
ContentMessage,
DataMessage,
SentSyncMessage,
} from '../messages/outgoing';
import { PubKey } from '../types';
export function getSentSyncMessage(params: {
message: ContentMessage;
expirationStartTimestamp?: number;
sentTo?: Array<PubKey>;
destination: PubKey | string;
}): SentSyncMessage | undefined {
if (!(params.message instanceof DataMessage)) {
return undefined;
}
// Stubbed for now
return undefined;
}
export function canSync(message: ContentMessage): boolean {
// This function should be agnostic to the device; it shouldn't need
// to know about the recipient
// Stubbed for now
return Boolean(from(message));
const pubKey = PubKey.cast(params.destination);
return new SentSyncMessage({
timestamp: Date.now(),
identifier: params.message.identifier,
destination: pubKey,
dataMessage: params.message.dataProto(),
expirationStartTimestamp: params.expirationStartTimestamp,
sentTo: params.sentTo,
});
}
export async function getSyncContacts(): Promise<Array<any> | undefined> {
@ -64,10 +73,7 @@ export async function getSyncContacts(): Promise<Array<any> | undefined> {
.filter(c => c.id !== primaryDevice.key);
// Return unique contacts
return _.uniqBy(
[...primaryContacts, ...secondaryContacts],
device => !!device
);
return _.uniqBy([...primaryContacts, ...secondaryContacts], 'id');
}
export async function filterOpenGroupsConvos(

@ -1,6 +1,6 @@
import * as MessageUtils from './Messages';
import * as GroupUtils from './Groups';
import * as SyncMessageUtils from './SyncMessageUtils';
import * as SyncMessageUtils from './SyncMessage';
import * as StringUtils from './String';
import * as PromiseUtils from './Promise';

@ -231,6 +231,18 @@ describe('MessageQueue', () => {
expect(args[0]).to.have.same.members(devices);
expect(args[1]).to.equal(message);
});
it('should send sync message if it was passed in', async () => {
const devices = TestUtils.generateFakePubKeys(3);
sandbox.stub(MultiDeviceProtocol, 'getAllDevices').resolves(devices);
const stub = sandbox.stub(messageQueueStub, 'sendSyncMessage').resolves();
const message = new TestSyncMessage({ timestamp: Date.now() });
await messageQueueStub.sendUsingMultiDevice(devices[0], message);
const args = stub.lastCall.args as [ContentMessage];
expect(args[0]).to.equal(message);
});
});
describe('sendMessageToDevices', () => {
@ -243,51 +255,6 @@ describe('MessageQueue', () => {
await messageQueueStub.sendMessageToDevices(devices, message);
expect(pendingMessageCache.getCache()).to.have.length(devices.length);
});
it('should send sync message if possible', async () => {
hasSessionStub.returns(false);
sandbox.stub(SyncMessageUtils, 'canSync').returns(true);
sandbox
.stub(SyncMessageUtils, 'from')
.returns(new TestSyncMessage({ timestamp: Date.now() }));
// This stub ensures that the message won't process
const sendSyncMessageStub = sandbox
.stub(messageQueueStub, 'sendSyncMessage')
.resolves();
const ourDevices = [ourDevice, ...TestUtils.generateFakePubKeys(2)];
sandbox
.stub(MultiDeviceProtocol, 'getAllDevices')
.callsFake(async user => {
if (ourDevice.isEqual(user)) {
return ourDevices;
}
return [];
});
const devices = [...ourDevices, ...TestUtils.generateFakePubKeys(3)];
const message = TestUtils.generateChatMessage();
await messageQueueStub.sendMessageToDevices(devices, message);
expect(sendSyncMessageStub.called).to.equal(
true,
'sendSyncMessage was not called.'
);
expect(
pendingMessageCache.getCache().map(c => c.device)
).to.not.have.members(
ourDevices.map(d => d.key),
'Sending regular messages to our own device is not allowed.'
);
expect(pendingMessageCache.getCache()).to.have.length(
devices.length - ourDevices.length,
'Messages should not be sent to our devices.'
);
});
});
describe('sendSyncMessage', () => {
@ -320,6 +287,12 @@ describe('MessageQueue', () => {
});
describe('closed groups', async () => {
beforeEach(() => {
sandbox
.stub(MultiDeviceProtocol, 'getPrimaryDevice')
.resolves(new PrimaryPubKey(ourNumber));
});
it('can send to closed group', async () => {
const members = TestUtils.generateFakePubKeys(4).map(
p => new PrimaryPubKey(p.key)
@ -351,6 +324,19 @@ describe('MessageQueue', () => {
await messageQueueStub.sendToGroup(message);
expect(sendUsingMultiDeviceStub.callCount).to.equal(0);
});
it('wont send message to our device', async () => {
sandbox
.stub(GroupUtils, 'getGroupMembers')
.resolves([new PrimaryPubKey(ourNumber)]);
const sendUsingMultiDeviceStub = sandbox
.stub(messageQueueStub, 'sendUsingMultiDevice')
.resolves();
const message = TestUtils.generateClosedGroupMessage();
await messageQueueStub.sendToGroup(message);
expect(sendUsingMultiDeviceStub.callCount).to.equal(0);
});
});
describe('open groups', async () => {

@ -0,0 +1,63 @@
import chai from 'chai';
import { TestUtils } from '../../test-utils/';
import { MessageUtils } from '../../../session/utils/';
import { PubKey } from '../../../session/types/';
// tslint:disable-next-line: no-require-imports no-var-requires
const chaiAsPromised = require('chai-as-promised');
chai.use(chaiAsPromised);
const { expect } = chai;
describe('Message Utils', () => {
describe('toRawMessage', () => {
it('can convert to raw message', async () => {
const device = TestUtils.generateFakePubKey();
const message = TestUtils.generateChatMessage();
const rawMessage = MessageUtils.toRawMessage(device, message);
expect(Object.keys(rawMessage)).to.have.length(6);
expect(rawMessage.identifier).to.exist;
expect(rawMessage.device).to.exist;
expect(rawMessage.encryption).to.exist;
expect(rawMessage.plainTextBuffer).to.exist;
expect(rawMessage.timestamp).to.exist;
expect(rawMessage.ttl).to.exist;
});
it('should generate valid plainTextBuffer', async () => {
const device = TestUtils.generateFakePubKey();
const message = TestUtils.generateChatMessage();
const rawMessage = MessageUtils.toRawMessage(device, message);
const rawBuffer = rawMessage.plainTextBuffer;
const rawBufferJSON = JSON.stringify(rawBuffer);
const messageBufferJSON = JSON.stringify(message.plainTextBuffer());
expect(rawBuffer instanceof Uint8Array).to.equal(
true,
'raw message did not contain a plainTextBuffer'
);
expect(rawBufferJSON).to.equal(
messageBufferJSON,
'plainTextBuffer was not converted correctly'
);
});
it('should maintain pubkey', async () => {
const device = TestUtils.generateFakePubKey();
const message = TestUtils.generateChatMessage();
const rawMessage = MessageUtils.toRawMessage(device, message);
const derivedPubKey = PubKey.from(rawMessage.device);
expect(derivedPubKey).to.exist;
expect(derivedPubKey?.isEqual(device)).to.equal(
true,
'pubkey of message was not converted correctly'
);
});
});
});

@ -0,0 +1,141 @@
import chai from 'chai';
import * as sinon from 'sinon';
import { PromiseUtils } from '../../../session/utils/';
// tslint:disable-next-line: no-require-imports no-var-requires
const chaiAsPromised = require('chai-as-promised');
chai.use(chaiAsPromised);
const { expect } = chai;
describe('Promise Utils', () => {
const sandbox = sinon.createSandbox();
let pollSpy: sinon.SinonSpy<
[
(done: (arg: any) => void) => Promise<void> | void,
(Partial<PromiseUtils.PollOptions> | undefined)?
],
Promise<void>
>;
let waitForTaskSpy: sinon.SinonSpy<
[(done: (arg: any) => void) => Promise<void> | void, (number | undefined)?],
Promise<unknown>
>;
let waitUntilSpy: sinon.SinonSpy<
[() => Promise<boolean> | boolean, (number | undefined)?],
Promise<void>
>;
beforeEach(() => {
pollSpy = sandbox.spy(PromiseUtils, 'poll');
waitForTaskSpy = sandbox.spy(PromiseUtils, 'waitForTask');
waitUntilSpy = sandbox.spy(PromiseUtils, 'waitUntil');
});
afterEach(() => {
sandbox.restore();
});
describe('poll', () => {
it('will call done on finished', async () => {
// completionSpy will be called on done
const completionSpy = sandbox.spy();
// tslint:disable-next-line: mocha-unneeded-done
const task = (done: any) => {
completionSpy();
done();
};
const promise = PromiseUtils.poll(task, {});
await expect(promise).to.be.fulfilled;
expect(pollSpy.callCount).to.equal(1);
expect(completionSpy.callCount).to.equal(1);
});
it('can timeout a task', async () => {
// completionSpy will be called on done
const completionSpy = sandbox.spy();
const task = (_done: any) => undefined;
const promise = PromiseUtils.poll(task, { timeout: 1 });
await expect(promise).to.be.rejectedWith('Periodic check timeout');
expect(pollSpy.callCount).to.equal(1);
expect(completionSpy.callCount).to.equal(0);
});
it('will recur according to interval option', async () => {
const expectedRecurrences = 4;
const timeout = 1000;
const interval = timeout / expectedRecurrences;
const recurrenceSpy = sandbox.spy();
const task = (done: any) => {
recurrenceSpy();
// Done after we've been called `expectedRecurrences` times
if (recurrenceSpy.callCount === expectedRecurrences) {
done();
}
};
const promise = PromiseUtils.poll(task, { timeout, interval });
await expect(promise).to.be.fulfilled;
expect(pollSpy.callCount).to.equal(1);
expect(recurrenceSpy.callCount).to.equal(expectedRecurrences);
});
});
describe('waitForTask', () => {
it('can wait for a task', async () => {
// completionSpy will be called on done
const completionSpy = sandbox.spy();
// tslint:disable-next-line: mocha-unneeded-done
const task = (done: any) => {
completionSpy();
done();
};
const promise = PromiseUtils.waitForTask(task);
await expect(promise).to.be.fulfilled;
expect(waitForTaskSpy.callCount).to.equal(1);
expect(completionSpy.callCount).to.equal(1);
});
it('can timeout a task', async () => {
// completionSpy will be called on done
const completionSpy = sandbox.spy();
const task = async (_done: any) => undefined;
const promise = PromiseUtils.waitForTask(task, 1);
await expect(promise).to.be.rejectedWith('Task timed out.');
expect(waitForTaskSpy.callCount).to.equal(1);
expect(completionSpy.callCount).to.equal(0);
});
});
describe('waitUntil', () => {
it('can wait for check', async () => {
const check = () => true;
const promise = PromiseUtils.waitUntil(check);
await expect(promise).to.be.fulfilled;
expect(waitUntilSpy.callCount).to.equal(1);
});
it('can timeout a check', async () => {
const check = () => false;
const promise = PromiseUtils.waitUntil(check, 1);
await expect(promise).to.be.rejectedWith('Periodic check timeout');
expect(waitUntilSpy.callCount).to.equal(1);
});
});
});

@ -0,0 +1,227 @@
import chai from 'chai';
import ByteBuffer from 'bytebuffer';
// Can't import type as StringUtils.Encoding
import { Encoding } from '../../../session/utils/String';
import { StringUtils } from '../../../session/utils/';
// tslint:disable-next-line: no-require-imports no-var-requires
const chaiAsPromised = require('chai-as-promised');
chai.use(chaiAsPromised);
const { expect } = chai;
describe('String Utils', () => {
describe('encode', () => {
it('can encode to base64', async () => {
const testString = 'AAAAAAAAAA';
const encoded = StringUtils.encode(testString, 'base64');
expect(encoded instanceof ArrayBuffer).to.equal(
true,
'a buffer was not returned from `encode`'
);
expect(encoded.byteLength).to.be.greaterThan(0);
});
it('can encode to hex', async () => {
const testString = 'AAAAAAAAAA';
const encoded = StringUtils.encode(testString, 'hex');
expect(encoded instanceof ArrayBuffer).to.equal(
true,
'a buffer was not returned from `encode`'
);
expect(encoded.byteLength).to.be.greaterThan(0);
});
it('wont encode invalid hex', async () => {
const testString = 'ZZZZZZZZZZ';
const encoded = StringUtils.encode(testString, 'hex');
expect(encoded.byteLength).to.equal(0);
});
it('can encode to binary', async () => {
const testString = 'AAAAAAAAAA';
const encoded = StringUtils.encode(testString, 'binary');
expect(encoded instanceof ArrayBuffer).to.equal(
true,
'a buffer was not returned from `encode`'
);
expect(encoded.byteLength).to.be.greaterThan(0);
});
it('can encode to utf8', async () => {
const testString = 'AAAAAAAAAA';
const encoded = StringUtils.encode(testString, 'binary');
expect(encoded instanceof ArrayBuffer).to.equal(
true,
'a buffer was not returned from `encode`'
);
expect(encoded.byteLength).to.be.greaterThan(0);
});
it('can encode empty string', async () => {
const testString = '';
expect(testString).to.have.length(0);
const allEncodedings = (['base64', 'hex', 'binary', 'utf8'] as Array<
Encoding
>).map(e => StringUtils.encode(testString, e));
allEncodedings.forEach(encoded => {
expect(encoded instanceof ArrayBuffer).to.equal(
true,
'a buffer was not returned from `encode`'
);
expect(encoded.byteLength).to.equal(0);
});
});
it('can encode huge string', async () => {
const stringSize = Math.pow(2, 16);
const testString = Array(stringSize)
.fill('0')
.join('');
const allEncodedings = (['base64', 'hex', 'binary', 'utf8'] as Array<
Encoding
>).map(e => StringUtils.encode(testString, e));
allEncodedings.forEach(encoded => {
expect(encoded instanceof ArrayBuffer).to.equal(
true,
'a buffer was not returned from `encode`'
);
expect(encoded.byteLength).to.be.greaterThan(0);
});
});
it("won't encode illegal string length in hex", async () => {
const testString = 'A';
const encode = () => StringUtils.encode(testString, 'hex');
// Ensure string is odd length
expect(testString.length % 2).to.equal(1);
expect(encode).to.throw('Illegal str: Length not a multiple of 2');
});
it('can encode obscure string', async () => {
const testString =
'↓←¶ᶑᵶ⅑⏕→⅓‎ᵹ⅙ᵰᶎ⅔⅗↔‌ᶈ⅞⸜ᶊᵴᶉ↉¥ᶖᶋᶃᶓ⏦ᵾᶂᶆ↕⸝ᶔᶐ⏔£⏙⅐⅒ᶌ⁁ᶘᶄᶒᶸ⅘‏⅚⅛ᶙᶇᶕᶀ↑ᵿ⏠ᶍᵯ⏖⏗⅜ᶚᶏ⁊‍ᶁᶗᵽᵼ⅝⏘⅖⅕⏡';
// Not valid hex format; try test the others
const encodings = ['base64', 'binary', 'utf8'] as Array<Encoding>;
encodings.forEach(encoding => {
const encoded = StringUtils.encode(testString, encoding);
expect(encoded instanceof ArrayBuffer).to.equal(
true,
`a buffer was not returned using encoding: '${encoding}'`
);
expect(encoded.byteLength).to.be.greaterThan(0);
});
});
});
describe('decode', () => {
it('can decode empty buffer', async () => {
const buffer = new ByteBuffer(0);
const encodings = ['base64', 'hex', 'binary', 'utf8'] as Array<Encoding>;
// Each encoding should be valid
encodings.forEach(encoding => {
const decoded = StringUtils.decode(buffer, encoding);
expect(decoded).to.exist;
expect(typeof decoded === String.name.toLowerCase());
expect(decoded).to.have.length(0);
});
});
it('can decode huge buffer', async () => {
const bytes = Math.pow(2, 16);
const bufferString = Array(bytes)
.fill('A')
.join('');
const buffer = ByteBuffer.fromUTF8(bufferString);
const encodings = ['base64', 'hex', 'binary', 'utf8'] as Array<Encoding>;
// Each encoding should be valid
encodings.forEach(encoding => {
const decoded = StringUtils.decode(buffer, encoding);
expect(decoded).to.exist;
expect(typeof decoded === String.name.toLowerCase());
expect(decoded).to.have.length.greaterThan(0);
});
});
it('can decode from ByteBuffer', async () => {
const buffer = ByteBuffer.fromUTF8('AAAAAAAAAA');
const encodings = ['base64', 'hex', 'binary', 'utf8'] as Array<Encoding>;
// Each encoding should be valid
encodings.forEach(encoding => {
const decoded = StringUtils.decode(buffer, encoding);
expect(decoded).to.exist;
expect(typeof decoded === String.name.toLowerCase());
expect(decoded).to.have.length.greaterThan(0);
});
});
it('can decode from Buffer', async () => {
const arrayBuffer = new ArrayBuffer(10);
const buffer = Buffer.from(arrayBuffer);
buffer.writeUInt8(0, 0);
const encodings = ['base64', 'hex', 'binary', 'utf8'] as Array<Encoding>;
// Each encoding should be valid
encodings.forEach(encoding => {
const decoded = StringUtils.decode(buffer, encoding);
expect(decoded).to.exist;
expect(typeof decoded === String.name.toLowerCase());
expect(decoded).to.have.length.greaterThan(0);
});
});
it('can decode from ArrayBuffer', async () => {
const buffer = new ArrayBuffer(10);
const encodings = ['base64', 'hex', 'binary', 'utf8'] as Array<Encoding>;
// Each encoding should be valid
encodings.forEach(encoding => {
const decoded = StringUtils.decode(buffer, encoding);
expect(decoded).to.exist;
expect(typeof decoded === String.name.toLowerCase());
expect(decoded).to.have.length.greaterThan(0);
});
});
it('can decode from Uint8Array', async () => {
const buffer = new Uint8Array(10);
const encodings = ['base64', 'hex', 'binary', 'utf8'] as Array<Encoding>;
// Each encoding should be valid
encodings.forEach(encoding => {
const decoded = StringUtils.decode(buffer, encoding);
expect(decoded).to.exist;
expect(typeof decoded === String.name.toLowerCase());
expect(decoded).to.have.length.greaterThan(0);
});
});
});
});

@ -0,0 +1,117 @@
import chai from 'chai';
import * as sinon from 'sinon';
import { SyncMessageUtils } from '../../../session/utils/';
import { TestUtils } from '../../test-utils';
import { UserUtil } from '../../../util';
import { MultiDeviceProtocol } from '../../../session/protocols';
import { SyncMessage } from '../../../session/messages/outgoing';
// tslint:disable-next-line: no-require-imports no-var-requires
const chaiAsPromised = require('chai-as-promised');
chai.use(chaiAsPromised);
const { expect } = chai;
describe('Sync Message Utils', () => {
describe('getSyncContacts', () => {
let getAllConversationsStub: sinon.SinonStub;
let getOrCreateAndWaitStub: sinon.SinonStub;
let getOrCreatAndWaitItem: any;
// Fill half with secondaries, half with primaries
const numConversations = 20;
const primaryConversations = new Array(numConversations / 2)
.fill({})
.map(
() =>
new TestUtils.MockConversation({
type: TestUtils.MockConversationType.Primary,
})
);
const secondaryConversations = new Array(numConversations / 2)
.fill({})
.map(
() =>
new TestUtils.MockConversation({
type: TestUtils.MockConversationType.Secondary,
})
);
const conversations = [...primaryConversations, ...secondaryConversations];
const sandbox = sinon.createSandbox();
const ourDevice = TestUtils.generateFakePubKey();
const ourNumber = ourDevice.key;
const ourPrimaryDevice = TestUtils.generateFakePubKey();
beforeEach(async () => {
// Util Stubs
TestUtils.stubWindow('Whisper', {
ConversationCollection: sandbox.stub(),
});
getAllConversationsStub = TestUtils.stubData(
'getAllConversations'
).resolves(conversations);
// Scale result in sync with secondaryConversations on callCount
getOrCreateAndWaitStub = sandbox.stub().callsFake(() => {
const item =
secondaryConversations[getOrCreateAndWaitStub.callCount - 1];
// Make the item a primary device to match the call in SyncMessage under secondaryContactsPromise
getOrCreatAndWaitItem = {
...item,
getPrimaryDevicePubKey: () => item.id,
attributes: {
secondaryStatus: false,
},
};
return getOrCreatAndWaitItem;
});
TestUtils.stubWindow('ConversationController', {
getOrCreateAndWait: getOrCreateAndWaitStub,
});
// Stubs
sandbox.stub(UserUtil, 'getCurrentDevicePubKey').resolves(ourNumber);
sandbox
.stub(MultiDeviceProtocol, 'getPrimaryDevice')
.resolves(ourPrimaryDevice);
});
afterEach(() => {
sandbox.restore();
TestUtils.restoreStubs();
});
it('can get sync contacts with only primary contacts', async () => {
getAllConversationsStub.resolves(primaryConversations);
const contacts = await SyncMessageUtils.getSyncContacts();
expect(getAllConversationsStub.callCount).to.equal(1);
// Each contact should be a primary device
expect(contacts).to.have.length(numConversations / 2);
expect(contacts?.find(c => c.attributes.secondaryStatus)).to.not.exist;
});
it('can get sync contacts of assorted primaries and secondaries', async () => {
// Map secondary contacts to stub resolution
const contacts = await SyncMessageUtils.getSyncContacts();
expect(getAllConversationsStub.callCount).to.equal(1);
// We should have numConversations unique contacts
expect(contacts).to.have.length(numConversations);
// All contacts should be primary; half of which some from secondaries in secondaryContactsPromise
expect(contacts?.find(c => c.attributes.secondaryStatus)).to.not.exist;
expect(contacts?.filter(c => c.isPrimary)).to.have.length(
numConversations / 2
);
});
});
});

@ -5,7 +5,8 @@ import {
} from '../../../session/messages/outgoing';
import { v4 as uuid } from 'uuid';
import { OpenGroup } from '../../../session/types';
import { generateFakePubKey } from './pubkey';
import { generateFakePubKey, generateFakePubKeys } from './pubkey';
import { ConversationAttributes } from '../../../../js/models/conversations';
export function generateChatMessage(identifier?: string): ChatMessage {
return new ChatMessage({
@ -46,3 +47,68 @@ export function generateClosedGroupMessage(
chatMessage: generateChatMessage(),
});
}
interface MockConversationParams {
id?: string;
type: MockConversationType;
members?: Array<string>;
}
export enum MockConversationType {
Primary = 'primary',
Secondary = 'secondary',
Group = 'group',
}
export class MockConversation {
public id: string;
public type: MockConversationType;
public attributes: ConversationAttributes;
public isPrimary?: boolean;
constructor(params: MockConversationParams) {
const dayInSeconds = 86400;
this.type = params.type;
this.id = params.id ?? generateFakePubKey().key;
this.isPrimary = this.type === MockConversationType.Primary;
const members =
this.type === MockConversationType.Group
? params.members ?? generateFakePubKeys(10).map(m => m.key)
: [];
this.attributes = {
members,
left: false,
expireTimer: dayInSeconds,
profileSharing: true,
mentionedUs: false,
unreadCount: 99,
isArchived: false,
active_at: Date.now(),
timestamp: Date.now(),
secondaryStatus: !this.isPrimary,
};
}
public isPrivate() {
return true;
}
public isOurLocalDevice() {
return false;
}
public isBlocked() {
return false;
}
public getPrimaryDevicePubKey() {
if (this.type === MockConversationType.Group) {
return undefined;
}
return this.isPrimary ? this.id : generateFakePubKey().key;
}
}

Loading…
Cancel
Save