groups-test

pull/1177/head
Vincent 5 years ago
commit 2be1c2fd94

@ -1,11 +1,6 @@
import { Message, MessageParams } from './Message'; import { Message, MessageParams } from './Message';
import { AttachmentPointer, Preview, Quote } from './content'; import { AttachmentPointer, Preview, Quote } from './content';
import { OpenGroup } from '../../types/OpenGroup';
interface OpenGroup {
server: string;
channel: number;
conversationId: string;
}
interface OpenGroupMessageParams extends MessageParams { interface OpenGroupMessageParams extends MessageParams {
group: OpenGroup; group: OpenGroup;
@ -20,7 +15,7 @@ export class OpenGroupMessage extends Message {
public readonly body?: string; public readonly body?: string;
public readonly attachments: Array<AttachmentPointer>; public readonly attachments: Array<AttachmentPointer>;
public readonly quote?: Quote; public readonly quote?: Quote;
public readonly preview: Array<Preview>; public readonly preview?: Array<Preview>;
constructor({ constructor({
timestamp, timestamp,

@ -1,5 +1,18 @@
import { ContentMessage } from '../ContentMessage'; import { ContentMessage } from '../ContentMessage';
import { SignalService } from '../../../../../protobuf'; import { SignalService } from '../../../../../protobuf';
// import { ContactSyncMessage } from '.';
// Matches SyncMessage definition in SignalService protobuf
export enum SyncMessageEnum {
UNKNONWN = 0,
CONTACTS = 1,
GROUPS = 2,
BLOCKED = 3,
CONFIGURATION = 4,
}
// TODO: Declare all sync message types
// export type SyncMessageType = ContactSyncMessage | GroupSyncMessage
export abstract class SyncMessage extends ContentMessage { export abstract class SyncMessage extends ContentMessage {
public ttl(): number { public ttl(): number {
@ -7,7 +20,10 @@ export abstract class SyncMessage extends ContentMessage {
} }
protected contentProto(): SignalService.Content { protected contentProto(): SignalService.Content {
const dataMessage = new SignalService.DataMessage({});
return new SignalService.Content({ return new SignalService.Content({
dataMessage,
syncMessage: this.syncProto(), syncMessage: this.syncProto(),
}); });
} }

@ -1,6 +1,5 @@
import * as _ from 'lodash'; import * as _ from 'lodash';
import { getPairedDevicesFor } from '../../../js/modules/data'; import { getPairedDevicesFor } from '../../../js/modules/data';
import { ConversationController } from '../../window';
import { EventEmitter } from 'events'; import { EventEmitter } from 'events';
import { import {
@ -14,11 +13,16 @@ import {
SessionRequestMessage, SessionRequestMessage,
} from '../messages/outgoing'; } from '../messages/outgoing';
import { PendingMessageCache } from './PendingMessageCache'; import { PendingMessageCache } from './PendingMessageCache';
import { JobQueue, SyncMessageUtils, TypedEventEmitter } from '../utils'; import {
JobQueue,
SyncMessageUtils,
TypedEventEmitter,
GroupUtils,
} from '../utils';
import { PubKey } from '../types'; import { PubKey } from '../types';
import { MessageSender } from '.'; import { MessageSender } from '.';
import { SessionProtocol } from '../protocols'; import { SessionProtocol } from '../protocols';
import * as UserUtils from '../../util/user'; import * as UserUtil from '../../util/user';
export class MessageQueue implements MessageQueueInterface { export class MessageQueue implements MessageQueueInterface {
public readonly events: TypedEventEmitter<MessageQueueInterfaceEvents>; public readonly events: TypedEventEmitter<MessageQueueInterfaceEvents>;
@ -50,7 +54,7 @@ export class MessageQueue implements MessageQueueInterface {
// Sync to our devices if syncable // Sync to our devices if syncable
if (SyncMessageUtils.canSync(message)) { if (SyncMessageUtils.canSync(message)) {
const currentDevice = await UserUtils.getCurrentDevicePubKey(); const currentDevice = await UserUtil.getCurrentDevicePubKey();
if (currentDevice) { if (currentDevice) {
const otherDevices = await getPairedDevicesFor(currentDevice); const otherDevices = await getPairedDevicesFor(currentDevice);
@ -60,11 +64,7 @@ export class MessageQueue implements MessageQueueInterface {
); );
await this.sendSyncMessage(message, ourDevices); await this.sendSyncMessage(message, ourDevices);
// Remove our devices from currentDevices currentDevices = _.xor(currentDevices, ourDevices);
const ourDeviceContacts = ourDevices.map(device =>
ConversationController.get(device.key)
);
currentDevices = _.xor(currentDevices, ourDeviceContacts);
} }
} }
@ -78,22 +78,23 @@ export class MessageQueue implements MessageQueueInterface {
public async sendToGroup( public async sendToGroup(
message: OpenGroupMessage | ContentMessage message: OpenGroupMessage | ContentMessage
): Promise<boolean> { ): Promise<boolean> {
// Ensure message suits its respective type
if ( if (
!(message instanceof OpenGroupMessage) && !(message instanceof OpenGroupMessage) &&
!(message instanceof ClosedGroupMessage) !(message instanceof ClosedGroupMessage)
) { ) {
console.log('[vince] NOT INSTANCEOF');
console.log('instance of message:', message.constructor.name);
return false; return false;
} }
// Closed groups // Closed groups
if (message instanceof ClosedGroupMessage) { if (message instanceof ClosedGroupMessage) {
// Get devices in closed group // Get devices in closed group
const conversation = ConversationController.get(message.groupId); const recipients: Array<PubKey> = await GroupUtils.getGroupMembers(
const recipientsModels = conversation.contactCollection.models; message.groupId
const recipients: Array<PubKey> = recipientsModels.map(
(recipient: any) => new PubKey(recipient.id)
); );
await this.sendMessageToDevices(recipients, message); await this.sendMessageToDevices(recipients, message);
return true; return true;
@ -108,11 +109,16 @@ export class MessageQueue implements MessageQueueInterface {
this.events.emit('success', message); this.events.emit('success', message);
} catch (e) { } catch (e) {
this.events.emit('fail', message, e); this.events.emit('fail', message, e);
console.log('[vince] EVENT FAILED', message);
return false;
} }
return true; return true;
} }
console.log('[vince] OTHERWISE FAIELD');
return false; return false;
} }

@ -93,7 +93,7 @@ export class PendingMessageCache {
await this.saveToDB(); await this.saveToDB();
} }
public async loadFromDB() { private async loadFromDB() {
const messages = await this.getFromStorage(); const messages = await this.getFromStorage();
this.cache = messages; this.cache = messages;
} }

@ -0,0 +1,82 @@
// This is the Open Group equivalent to the PubKey type.
interface OpenGroupParams {
server: string;
channel: number;
conversationId: string;
}
export class OpenGroup {
private static readonly serverRegex = new RegExp(
'^([\\w-]{2,}.){1,2}[\\w-]{2,}$'
);
private static readonly groupIdRegex = new RegExp(
'^publicChat:[0-9]*@([\\w-]{2,}.){1,2}[\\w-]{2,}$'
);
public readonly server: string;
public readonly channel: number;
public readonly groupId?: string;
public readonly conversationId: string;
constructor(params: OpenGroupParams) {
const strippedServer = params.server.replace('https://', '');
this.server = strippedServer;
// Validate server format
const isValid = OpenGroup.serverRegex.test(this.server);
if (!isValid) {
throw Error('an invalid server or groupId was provided');
}
this.channel = params.channel;
this.conversationId = params.conversationId;
this.groupId = OpenGroup.getGroupId(this.server, this.channel);
}
public static from(
groupId: string,
conversationId: string
): OpenGroup | undefined {
// Returns a new instance from a groupId if it's valid
// eg. groupId = 'publicChat:1@chat.getsession.org'
const server = this.getServer(groupId);
const channel = this.getChannel(groupId);
// Was groupId successfully utilized?
if (!server || !channel) {
return;
}
const openGroupParams = {
server,
channel,
groupId,
conversationId,
} as OpenGroupParams;
if (this.serverRegex.test(server)) {
return new OpenGroup(openGroupParams);
}
return;
}
private static getServer(groupId: string): string | undefined {
const isValid = this.groupIdRegex.test(groupId);
return isValid ? groupId.split('@')[1] : undefined;
}
private static getChannel(groupId: string): number | undefined {
const isValid = this.groupIdRegex.test(groupId);
const channelMatch = groupId.match(/^.*\:([0-9]*)\@.*$/);
return channelMatch && isValid ? Number(channelMatch[1]) : undefined;
}
private static getGroupId(server: string, channel: number): string {
// server is already validated in constructor; no need to re-check
return `publicChat:${channel}@${server}`;
}
}

@ -26,4 +26,8 @@ export class PubKey {
return false; return false;
} }
public static isEqual(first: PubKey, second: PubKey) {
return first.key === second.key;
}
} }

@ -0,0 +1,18 @@
import { getAllConversations } from '../../../js/modules/data';
import { Whisper } from '../../window';
import { PubKey } from '../types';
export async function getGroupMembers(groupId: string): Promise<Array<PubKey>> {
const conversations = await getAllConversations({
ConversationCollection: Whisper.ConversationCollection,
});
const groupConversation = conversations.find(c => c.id === groupId);
const groupMembers = groupConversation.attributes.members;
if (!groupMembers) {
return [];
}
return groupMembers.map((member: string) => new PubKey(member));
}

@ -1,21 +1,29 @@
import { RawMessage } from '../types/RawMessage'; import { RawMessage } from '../types/RawMessage';
import { ContentMessage } from '../messages/outgoing'; import {
ContentMessage,
SyncMessage,
OpenGroupMessage,
} from '../messages/outgoing';
import { EncryptionType, PubKey } from '../types'; import { EncryptionType, PubKey } from '../types';
import { OpenGroup } from '../types/OpenGroup';
export function toRawMessage( export function toRawMessage(
device: PubKey, device: PubKey | OpenGroup,
message: ContentMessage message: ContentMessage | OpenGroupMessage
): RawMessage { ): RawMessage {
const ttl = message.ttl();
const timestamp = message.timestamp; const timestamp = message.timestamp;
const ttl = message.ttl();
const plainTextBuffer = message.plainTextBuffer(); const plainTextBuffer = message.plainTextBuffer();
const sendTo = device instanceof PubKey ? device.key : device.conversationId;
// tslint:disable-next-line: no-unnecessary-local-variable // tslint:disable-next-line: no-unnecessary-local-variable
const rawMessage: RawMessage = { const rawMessage: RawMessage = {
identifier: message.identifier, identifier: message.identifier,
plainTextBuffer, plainTextBuffer,
timestamp, timestamp,
device: device.key, device: sendTo,
ttl, ttl,
encryption: EncryptionType.Signal, encryption: EncryptionType.Signal,
}; };

@ -1,5 +1,5 @@
import * as _ from 'lodash'; import * as _ from 'lodash';
import * as UserUtils from '../../util/user'; import * as UserUtil from '../../util/user';
import { import {
getAllConversations, getAllConversations,
getPrimaryDeviceFor, getPrimaryDeviceFor,
@ -24,7 +24,7 @@ export async function canSync(message: ContentMessage): Promise<boolean> {
} }
export async function getSyncContacts(): Promise<Array<any> | undefined> { export async function getSyncContacts(): Promise<Array<any> | undefined> {
const thisDevice = await UserUtils.getCurrentDevicePubKey(); const thisDevice = await UserUtil.getCurrentDevicePubKey();
if (!thisDevice) { if (!thisDevice) {
return []; return [];

@ -1,7 +1,8 @@
import * as MessageUtils from './Messages'; import * as MessageUtils from './Messages';
import * as GroupUtils from './Groups';
import * as SyncMessageUtils from './SyncMessageUtils'; import * as SyncMessageUtils from './SyncMessageUtils';
export * from './TypedEmitter'; export * from './TypedEmitter';
export * from './JobQueue'; export * from './JobQueue';
export { MessageUtils, SyncMessageUtils }; export { MessageUtils, SyncMessageUtils, GroupUtils };

@ -5,13 +5,14 @@ import {
OpenGroupMessage, OpenGroupMessage,
} from '../../../session/messages/outgoing'; } from '../../../session/messages/outgoing';
import * as MIME from '../../../../ts/types/MIME'; import * as MIME from '../../../../ts/types/MIME';
import { OpenGroup } from '../../../session/types/OpenGroup';
describe('OpenGroupMessage', () => { describe('OpenGroupMessage', () => {
const group = { const group = new OpenGroup({
server: 'server', server: 'chat.example.server',
channel: 1, channel: 1,
conversationId: '0', conversationId: '0',
}; });
it('can create empty message with just a timestamp and group', () => { it('can create empty message with just a timestamp and group', () => {
const message = new OpenGroupMessage({ const message = new OpenGroupMessage({

@ -1,9 +1,21 @@
import { expect } from 'chai'; import { expect } from 'chai';
import * as sinon from 'sinon';
import * as _ from 'lodash'; import * as _ from 'lodash';
import { MessageUtils } from '../../../session/utils'; import { GroupUtils, MessageUtils } from '../../../session/utils';
import { TestUtils } from '../../../test/test-utils'; import { TestUtils, Stubs } from '../../../test/test-utils';
import { PendingMessageCache, MessageQueue } from '../../../session/sending/MessageQueue'; import { MessageQueue } from '../../../session/sending/MessageQueue';
import { generateFakePubkey, generateChatMessage } from '../../test-utils/testUtils'; import {
generateChatMessage,
generateFakePubkey,
generateMemberList,
generateOpenGroupMessage,
} from '../../test-utils/testUtils';
import { getGroupMembers } from '../../../session/utils/Groups';
import { OpenGroupMessage } from '../../../session/messages/outgoing';
import { RawMessage } from '../../../session/types';
import { UserUtil } from '../../../util';
import { MessageSender } from '../../../session/sending';
import { sendToOpenGroup } from '../../../session/sending/MessageSender';
// Equivalent to Data.StorageItem // Equivalent to Data.StorageItem
interface StorageItem { interface StorageItem {
@ -11,13 +23,21 @@ interface StorageItem {
value: any; value: any;
} }
describe('Message Queue', () => { describe('MessageQueue', () => {
const sandbox = sinon.createSandbox();
const ourNumber = generateFakePubkey().key;
// Initialize new stubbed cache // Initialize new stubbed cache
let data: StorageItem; let data: StorageItem;
let messageQueueStub: MessageQueue; let messageQueueStub: MessageQueue;
let sendStub: sinon.SinonStub<[RawMessage, (number | undefined)?]>;
let sendToOpenGroupStub: sinon.SinonStub<[OpenGroupMessage]>;
beforeEach(async () => { beforeEach(async () => {
// Stub out methods which touch the database sandbox.stub(UserUtil, 'getCurrentDevicePubKey').resolves(ourNumber);
// PendingMessageCache stubs
const storageID = 'pendingMessages'; const storageID = 'pendingMessages';
data = { data = {
id: storageID, id: storageID,
@ -36,20 +56,41 @@ describe('Message Queue', () => {
} }
}); });
TestUtils.stubData('getPairedDevicesFor').callsFake(async () => {
return generateMemberList(2);
});
TestUtils.stubWindow('libsignal', {
SignalProtocolAddress: sandbox.stub(),
SessionCipher: Stubs.SessionCipherStub,
} as any);
// Other stubs
sendStub = sandbox.stub(MessageSender, 'send').resolves(undefined);
sendToOpenGroupStub = sandbox.stub(MessageSender, 'sendToOpenGroup').resolves(true);
sandbox.stub(GroupUtils, 'getGroupMembers').callsFake(
async () =>
new Promise(r => {
r(generateMemberList(10));
})
);
messageQueueStub = new MessageQueue(); messageQueueStub = new MessageQueue();
}); });
afterEach(() => { afterEach(() => {
TestUtils.restoreStubs(); TestUtils.restoreStubs();
sandbox.restore();
}); });
it('can send to many devices', async () => { it('can send to many devices', async () => {
const devices = Array.from({length: 40}, generateFakePubkey); const devices = generateMemberList(10);
const message = generateChatMessage(); const message = generateChatMessage();
await messageQueueStub.sendMessageToDevices(devices, message); await messageQueueStub.sendMessageToDevices(devices, message);
// Failure will make an error // Failure will make an error; check messageQueueStub.events
}); });
it('can send using multidevice', async () => { it('can send using multidevice', async () => {
@ -57,15 +98,64 @@ describe('Message Queue', () => {
const message = generateChatMessage(); const message = generateChatMessage();
await messageQueueStub.sendUsingMultiDevice(device, message); await messageQueueStub.sendUsingMultiDevice(device, message);
// Failure will make an error; check messageQueueStub.events
}); });
it('', async () => { it('can send to open group', async () => {
const message = generateOpenGroupMessage();
const success = await messageQueueStub.sendToGroup(message);
expect(success).to.equal(true, 'sending to group failed');
// Failure will make an error; check messageQueueStub.events
});
it('can send to closed group', async () => {
const message = generateOpenGroupMessage();
const success = await messageQueueStub.sendToGroup(message);
expect(success).to.equal(true, 'sending to group failed');
// Failure will make an error; check messageQueueStub.events
});
it('can send to open group', async () => {
const message = generateOpenGroupMessage();
await messageQueueStub.sendToGroup(message);
// Failure will make an error; check messageQueueStub.events
});
it('wont send wrong message type to group', async () => {
// Regular chat message should return false
const message = generateChatMessage();
const response = await messageQueueStub.sendToGroup(message);
expect(response).to.equal(
false,
'sendToGroup considered an invalid message type as valid'
);
// Failure will make an error; check messageQueueStub.events
}); });
it("won't process invalid message", async () => { it("won't process invalid message", async () => {
// process with message undefined // SHOULD make an error; expect error
// EXAMPLE FROM MESSAGESENDER_TEST
// it('should not retry if an error occurred during encryption', async () => {
// encryptStub.throws(new Error('Failed to encrypt.'));
// const promise = MessageSender.send(rawMessage);
// await expect(promise).is.rejectedWith('Failed to encrypt.');
// expect(lokiMessageAPIStub.sendMessage.callCount).to.equal(0);
// });
}); });
it('can send sync message', async () => {
});
}); });

@ -5,8 +5,9 @@ import * as DataShape from '../../../js/modules/data';
import { v4 as uuid } from 'uuid'; import { v4 as uuid } from 'uuid';
import { ImportMock } from 'ts-mock-imports'; import { ImportMock } from 'ts-mock-imports';
import { PubKey } from '../../../ts/session/types'; import { PubKey, PubKey } from '../../../ts/session/types';
import { ChatMessage, OpenGroupMessage } from '../../session/messages/outgoing'; import { ChatMessage, OpenGroupMessage } from '../../session/messages/outgoing';
import { OpenGroup } from '../../session/types/OpenGroup';
const sandbox = sinon.createSandbox(); const sandbox = sinon.createSandbox();
@ -69,11 +70,26 @@ export function generateChatMessage(): ChatMessage {
} }
export function generateOpenGroupMessage(): OpenGroupMessage { export function generateOpenGroupMessage(): OpenGroupMessage {
const group = new OpenGroup() const group = new OpenGroup({
server: 'chat.example.server',
channel: 0,
conversationId: '0',
});
return new OpenGroupMessage({ return new OpenGroupMessage({
group timestamp: Date.now(),
group,
attachments: undefined, attachments: undefined,
preview: undefined,
body: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit',
quote: undefined,
}); });
} }
export function generateMemberList(size: number): Array<PubKey> {
const numMembers = Math.floor(size);
return numMembers > 0
? Array.from({ length: numMembers }, generateFakePubkey)
: [];
}

@ -15,3 +15,5 @@ export async function getIdentityKeyPair(): Promise<KeyPair | undefined> {
return item?.value; return item?.value;
} }
export async function getOurDevices()

@ -134,9 +134,11 @@ export const deleteAccount = window.deleteAccount;
export const resetDatabase = window.resetDatabase; export const resetDatabase = window.resetDatabase;
export const attemptConnection = window.attemptConnection; export const attemptConnection = window.attemptConnection;
export const dcodeIO = window.dcodeIO;
export const libloki = window.libloki; export const libloki = window.libloki;
export const libsignal = window.libsignal; export const libsignal = window.libsignal;
export const textsecure = window.textsecure; export const textsecure = window.textsecure;
export const storage = window.storage;
export const lokiMessageAPI = window.lokiMessageAPI; export const lokiMessageAPI = window.lokiMessageAPI;
export const lokiPublicChatAPI = window.lokiPublicChatAPI; export const lokiPublicChatAPI = window.lokiPublicChatAPI;

Loading…
Cancel
Save