diff --git a/ts/session/messages/outgoing/OpenGroupMessage.ts b/ts/session/messages/outgoing/OpenGroupMessage.ts index 286f9b593..c670a5e68 100644 --- a/ts/session/messages/outgoing/OpenGroupMessage.ts +++ b/ts/session/messages/outgoing/OpenGroupMessage.ts @@ -1,11 +1,6 @@ import { Message, MessageParams } from './Message'; import { AttachmentPointer, Preview, Quote } from './content'; - -interface OpenGroup { - server: string; - channel: number; - conversationId: string; -} +import { OpenGroup } from '../../types/OpenGroup'; interface OpenGroupMessageParams extends MessageParams { group: OpenGroup; @@ -20,7 +15,7 @@ export class OpenGroupMessage extends Message { public readonly body?: string; public readonly attachments: Array; public readonly quote?: Quote; - public readonly preview: Array; + public readonly preview?: Array; constructor({ timestamp, diff --git a/ts/session/messages/outgoing/content/OpenGroupChatMessage.ts b/ts/session/messages/outgoing/content/OpenGroupChatMessage.ts new file mode 100644 index 000000000..e69de29bb diff --git a/ts/session/messages/outgoing/content/sync/OpenGroupSyncMessage.ts b/ts/session/messages/outgoing/content/sync/OpenGroupSyncMessage.ts new file mode 100644 index 000000000..e69de29bb diff --git a/ts/session/messages/outgoing/content/sync/SyncMessage.ts b/ts/session/messages/outgoing/content/sync/SyncMessage.ts index 3eb336dc6..e32fdee37 100644 --- a/ts/session/messages/outgoing/content/sync/SyncMessage.ts +++ b/ts/session/messages/outgoing/content/sync/SyncMessage.ts @@ -1,5 +1,18 @@ import { ContentMessage } from '../ContentMessage'; 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 { public ttl(): number { @@ -7,7 +20,10 @@ export abstract class SyncMessage extends ContentMessage { } protected contentProto(): SignalService.Content { + const dataMessage = new SignalService.DataMessage({}); + return new SignalService.Content({ + dataMessage, syncMessage: this.syncProto(), }); } diff --git a/ts/session/sending/MessageQueue.ts b/ts/session/sending/MessageQueue.ts index cf27c90ea..5f9aa11a7 100644 --- a/ts/session/sending/MessageQueue.ts +++ b/ts/session/sending/MessageQueue.ts @@ -1,6 +1,5 @@ import * as _ from 'lodash'; import { getPairedDevicesFor } from '../../../js/modules/data'; -import { ConversationController } from '../../window'; import { EventEmitter } from 'events'; import { @@ -14,11 +13,16 @@ import { SessionRequestMessage, } from '../messages/outgoing'; import { PendingMessageCache } from './PendingMessageCache'; -import { JobQueue, SyncMessageUtils, TypedEventEmitter } from '../utils'; +import { + JobQueue, + SyncMessageUtils, + TypedEventEmitter, + GroupUtils, +} from '../utils'; import { PubKey } from '../types'; import { MessageSender } from '.'; import { SessionProtocol } from '../protocols'; -import * as UserUtils from '../../util/user'; +import * as UserUtil from '../../util/user'; export class MessageQueue implements MessageQueueInterface { public readonly events: TypedEventEmitter; @@ -50,7 +54,7 @@ export class MessageQueue implements MessageQueueInterface { // Sync to our devices if syncable if (SyncMessageUtils.canSync(message)) { - const currentDevice = await UserUtils.getCurrentDevicePubKey(); + const currentDevice = await UserUtil.getCurrentDevicePubKey(); if (currentDevice) { const otherDevices = await getPairedDevicesFor(currentDevice); @@ -60,11 +64,7 @@ export class MessageQueue implements MessageQueueInterface { ); await this.sendSyncMessage(message, ourDevices); - // Remove our devices from currentDevices - const ourDeviceContacts = ourDevices.map(device => - ConversationController.get(device.key) - ); - currentDevices = _.xor(currentDevices, ourDeviceContacts); + currentDevices = _.xor(currentDevices, ourDevices); } } @@ -78,22 +78,23 @@ export class MessageQueue implements MessageQueueInterface { public async sendToGroup( message: OpenGroupMessage | ContentMessage ): Promise { + // Ensure message suits its respective type if ( !(message instanceof OpenGroupMessage) && !(message instanceof ClosedGroupMessage) ) { + console.log('[vince] NOT INSTANCEOF'); + console.log('instance of message:', message.constructor.name); + return false; } // Closed groups if (message instanceof ClosedGroupMessage) { // Get devices in closed group - const conversation = ConversationController.get(message.groupId); - const recipientsModels = conversation.contactCollection.models; - const recipients: Array = recipientsModels.map( - (recipient: any) => new PubKey(recipient.id) + const recipients: Array = await GroupUtils.getGroupMembers( + message.groupId ); - await this.sendMessageToDevices(recipients, message); return true; @@ -108,11 +109,16 @@ export class MessageQueue implements MessageQueueInterface { this.events.emit('success', message); } catch (e) { this.events.emit('fail', message, e); + + console.log('[vince] EVENT FAILED', message); + + return false; } return true; } + console.log('[vince] OTHERWISE FAIELD'); return false; } diff --git a/ts/session/sending/PendingMessageCache.ts b/ts/session/sending/PendingMessageCache.ts index 074ee8174..0a4037c1c 100644 --- a/ts/session/sending/PendingMessageCache.ts +++ b/ts/session/sending/PendingMessageCache.ts @@ -93,7 +93,7 @@ export class PendingMessageCache { await this.saveToDB(); } - public async loadFromDB() { + private async loadFromDB() { const messages = await this.getFromStorage(); this.cache = messages; } diff --git a/ts/session/types/OpenGroup.ts b/ts/session/types/OpenGroup.ts new file mode 100644 index 000000000..658f2f8d1 --- /dev/null +++ b/ts/session/types/OpenGroup.ts @@ -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}`; + } +} diff --git a/ts/session/types/PubKey.ts b/ts/session/types/PubKey.ts index a9c84773b..59fccf119 100644 --- a/ts/session/types/PubKey.ts +++ b/ts/session/types/PubKey.ts @@ -26,4 +26,8 @@ export class PubKey { return false; } + + public static isEqual(first: PubKey, second: PubKey) { + return first.key === second.key; + } } diff --git a/ts/session/utils/Groups.ts b/ts/session/utils/Groups.ts new file mode 100644 index 000000000..c23278513 --- /dev/null +++ b/ts/session/utils/Groups.ts @@ -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> { + 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)); +} diff --git a/ts/session/utils/Messages.ts b/ts/session/utils/Messages.ts index 04ae0f815..ed64c7ab3 100644 --- a/ts/session/utils/Messages.ts +++ b/ts/session/utils/Messages.ts @@ -1,21 +1,29 @@ import { RawMessage } from '../types/RawMessage'; -import { ContentMessage } from '../messages/outgoing'; +import { + ContentMessage, + SyncMessage, + OpenGroupMessage, +} from '../messages/outgoing'; import { EncryptionType, PubKey } from '../types'; +import { OpenGroup } from '../types/OpenGroup'; export function toRawMessage( - device: PubKey, - message: ContentMessage + device: PubKey | OpenGroup, + message: ContentMessage | OpenGroupMessage ): RawMessage { - const ttl = message.ttl(); const timestamp = message.timestamp; + + const ttl = message.ttl(); const plainTextBuffer = message.plainTextBuffer(); + const sendTo = device instanceof PubKey ? device.key : device.conversationId; + // tslint:disable-next-line: no-unnecessary-local-variable const rawMessage: RawMessage = { identifier: message.identifier, plainTextBuffer, timestamp, - device: device.key, + device: sendTo, ttl, encryption: EncryptionType.Signal, }; diff --git a/ts/session/utils/SyncMessageUtils.ts b/ts/session/utils/SyncMessageUtils.ts index 92c1116c0..60cbd0116 100644 --- a/ts/session/utils/SyncMessageUtils.ts +++ b/ts/session/utils/SyncMessageUtils.ts @@ -1,5 +1,5 @@ import * as _ from 'lodash'; -import * as UserUtils from '../../util/user'; +import * as UserUtil from '../../util/user'; import { getAllConversations, getPrimaryDeviceFor, @@ -24,7 +24,7 @@ export async function canSync(message: ContentMessage): Promise { } export async function getSyncContacts(): Promise | undefined> { - const thisDevice = await UserUtils.getCurrentDevicePubKey(); + const thisDevice = await UserUtil.getCurrentDevicePubKey(); if (!thisDevice) { return []; diff --git a/ts/session/utils/index.ts b/ts/session/utils/index.ts index b5f9dd93e..aa99cb92f 100644 --- a/ts/session/utils/index.ts +++ b/ts/session/utils/index.ts @@ -1,7 +1,8 @@ import * as MessageUtils from './Messages'; +import * as GroupUtils from './Groups'; import * as SyncMessageUtils from './SyncMessageUtils'; export * from './TypedEmitter'; export * from './JobQueue'; -export { MessageUtils, SyncMessageUtils }; +export { MessageUtils, SyncMessageUtils, GroupUtils }; diff --git a/ts/test/session/messages/OpenGroupMessage_test.ts b/ts/test/session/messages/OpenGroupMessage_test.ts index 7223ca394..5b4f84b9e 100644 --- a/ts/test/session/messages/OpenGroupMessage_test.ts +++ b/ts/test/session/messages/OpenGroupMessage_test.ts @@ -5,13 +5,14 @@ import { OpenGroupMessage, } from '../../../session/messages/outgoing'; import * as MIME from '../../../../ts/types/MIME'; +import { OpenGroup } from '../../../session/types/OpenGroup'; describe('OpenGroupMessage', () => { - const group = { - server: 'server', + const group = new OpenGroup({ + server: 'chat.example.server', channel: 1, conversationId: '0', - }; + }); it('can create empty message with just a timestamp and group', () => { const message = new OpenGroupMessage({ diff --git a/ts/test/session/sending/MessageQueue_test.ts b/ts/test/session/sending/MessageQueue_test.ts index 9b4ea03cc..7c6d4bfa6 100644 --- a/ts/test/session/sending/MessageQueue_test.ts +++ b/ts/test/session/sending/MessageQueue_test.ts @@ -1,9 +1,21 @@ import { expect } from 'chai'; +import * as sinon from 'sinon'; import * as _ from 'lodash'; -import { MessageUtils } from '../../../session/utils'; -import { TestUtils } from '../../../test/test-utils'; -import { PendingMessageCache, MessageQueue } from '../../../session/sending/MessageQueue'; -import { generateFakePubkey, generateChatMessage } from '../../test-utils/testUtils'; +import { GroupUtils, MessageUtils } from '../../../session/utils'; +import { TestUtils, Stubs } from '../../../test/test-utils'; +import { MessageQueue } from '../../../session/sending/MessageQueue'; +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 interface StorageItem { @@ -11,13 +23,21 @@ interface StorageItem { value: any; } -describe('Message Queue', () => { +describe('MessageQueue', () => { + const sandbox = sinon.createSandbox(); + const ourNumber = generateFakePubkey().key; + // Initialize new stubbed cache let data: StorageItem; let messageQueueStub: MessageQueue; + let sendStub: sinon.SinonStub<[RawMessage, (number | undefined)?]>; + let sendToOpenGroupStub: sinon.SinonStub<[OpenGroupMessage]>; + beforeEach(async () => { - // Stub out methods which touch the database + sandbox.stub(UserUtil, 'getCurrentDevicePubKey').resolves(ourNumber); + + // PendingMessageCache stubs const storageID = 'pendingMessages'; data = { 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(); }); afterEach(() => { TestUtils.restoreStubs(); + sandbox.restore(); }); it('can send to many devices', async () => { - const devices = Array.from({length: 40}, generateFakePubkey); + const devices = generateMemberList(10); const message = generateChatMessage(); await messageQueueStub.sendMessageToDevices(devices, message); - // Failure will make an error + // Failure will make an error; check messageQueueStub.events }); it('can send using multidevice', async () => { @@ -57,15 +98,64 @@ describe('Message Queue', () => { const message = generateChatMessage(); 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 () => { - // 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 () => { + + }); }); diff --git a/ts/test/test-utils/testUtils.ts b/ts/test/test-utils/testUtils.ts index aba1f7568..456c8c3ae 100644 --- a/ts/test/test-utils/testUtils.ts +++ b/ts/test/test-utils/testUtils.ts @@ -5,8 +5,9 @@ import * as DataShape from '../../../js/modules/data'; import { v4 as uuid } from 'uuid'; 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 { OpenGroup } from '../../session/types/OpenGroup'; const sandbox = sinon.createSandbox(); @@ -69,11 +70,26 @@ export function generateChatMessage(): ChatMessage { } export function generateOpenGroupMessage(): OpenGroupMessage { - const group = new OpenGroup() + const group = new OpenGroup({ + server: 'chat.example.server', + channel: 0, + conversationId: '0', + }); return new OpenGroupMessage({ - group + timestamp: Date.now(), + group, attachments: undefined, + preview: undefined, + body: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit', + quote: undefined, }); } +export function generateMemberList(size: number): Array { + const numMembers = Math.floor(size); + + return numMembers > 0 + ? Array.from({ length: numMembers }, generateFakePubkey) + : []; +} diff --git a/ts/util/user.ts b/ts/util/user.ts index cea991b17..3808aebea 100644 --- a/ts/util/user.ts +++ b/ts/util/user.ts @@ -15,3 +15,5 @@ export async function getIdentityKeyPair(): Promise { return item?.value; } + +export async function getOurDevices() \ No newline at end of file diff --git a/ts/window/index.ts b/ts/window/index.ts index 4688cde7b..a75b33a08 100644 --- a/ts/window/index.ts +++ b/ts/window/index.ts @@ -134,9 +134,11 @@ export const deleteAccount = window.deleteAccount; export const resetDatabase = window.resetDatabase; export const attemptConnection = window.attemptConnection; +export const dcodeIO = window.dcodeIO; export const libloki = window.libloki; export const libsignal = window.libsignal; export const textsecure = window.textsecure; +export const storage = window.storage; export const lokiMessageAPI = window.lokiMessageAPI; export const lokiPublicChatAPI = window.lokiPublicChatAPI;