diff --git a/js/models/conversations.d.ts b/js/models/conversations.d.ts index 9f0decf13..340e3db36 100644 --- a/js/models/conversations.d.ts +++ b/js/models/conversations.d.ts @@ -23,6 +23,7 @@ interface ConversationAttributes { type: string; lastMessage?: string; avatarPointer?: string; + profileKey?: Uint8Array; } export interface ConversationModel diff --git a/js/models/messages.js b/js/models/messages.js index e8611b208..15a2c19f2 100644 --- a/js/models/messages.js +++ b/js/models/messages.js @@ -1297,12 +1297,17 @@ (dataMessage.body && dataMessage.body.length) || dataMessage.attachments.length ) { - const syncMessage = libsession.Messages.Outgoing.ChatMessage.buildSyncMessage( - dataMessage, - this.getConversation().id, - sentTimestamp - ); - await libsession.getMessageQueue().sendSyncMessage(syncMessage); + // try catch not to be kept + try { + const syncMessage = libsession.Messages.Outgoing.ChatMessage.buildSyncMessage( + dataMessage, + this.getConversation().id, + sentTimestamp + ); + await libsession.getMessageQueue().sendSyncMessage(syncMessage); + } catch (e) { + window.log.warn(e); + } } // - copy all fields from dataMessage and create a new ChatMessage diff --git a/protos/SignalService.proto b/protos/SignalService.proto index a63ecccbe..513097cce 100644 --- a/protos/SignalService.proto +++ b/protos/SignalService.proto @@ -217,11 +217,19 @@ message ConfigurationMessage { repeated bytes admins = 5; } + message Contact { + optional bytes publicKey = 1; + optional string name = 2; + optional string profilePicture = 3; + optional bytes profileKey = 4; + } + repeated ClosedGroup closedGroups = 1; repeated string openGroups = 2; optional string displayName = 3; optional string profilePicture = 4; optional bytes profileKey = 5; + repeated Contact contacts = 6; } message ReceiptMessage { diff --git a/ts/session/messages/outgoing/content/ConfigurationMessage.ts b/ts/session/messages/outgoing/content/ConfigurationMessage.ts index 699b77ece..fd3201698 100644 --- a/ts/session/messages/outgoing/content/ConfigurationMessage.ts +++ b/ts/session/messages/outgoing/content/ConfigurationMessage.ts @@ -14,6 +14,7 @@ interface ConfigurationMessageParams extends MessageParams { displayName: string; profilePicture?: string; profileKey?: Uint8Array; + contacts: Array; } export class ConfigurationMessage extends ContentMessage { @@ -22,6 +23,7 @@ export class ConfigurationMessage extends ContentMessage { public readonly displayName: string; public readonly profilePicture?: string; public readonly profileKey?: Uint8Array; + public readonly contacts: Array; constructor(params: ConfigurationMessageParams) { super({ timestamp: params.timestamp, identifier: params.identifier }); @@ -30,6 +32,7 @@ export class ConfigurationMessage extends ContentMessage { this.displayName = params.displayName; this.profilePicture = params.profilePicture; this.profileKey = params.profileKey; + this.contacts = params.contacts; if (!this.activeClosedGroups) { throw new Error('closed group must be set'); @@ -50,6 +53,10 @@ export class ConfigurationMessage extends ContentMessage { if (this.profileKey && !(this.profileKey instanceof Uint8Array)) { throw new Error('profileKey set but not an Uin8Array'); } + + if (!this.contacts) { + throw new Error('contacts must be set'); + } } public ttl(): number { @@ -69,6 +76,7 @@ export class ConfigurationMessage extends ContentMessage { displayName: this.displayName, profilePicture: this.profilePicture, profileKey: this.profileKey, + contacts: this.mapContactsObjectToProto(this.contacts), }); } @@ -79,6 +87,64 @@ export class ConfigurationMessage extends ContentMessage { new ConfigurationMessageClosedGroup(m).toProto() ); } + + private mapContactsObjectToProto( + contacts: Array + ): Array { + return (contacts || []).map(m => + new ConfigurationMessageContact(m).toProto() + ); + } +} + +export class ConfigurationMessageContact { + public publicKey: string; + public displayName: string; + public profilePictureURL?: string; + public profileKey?: Uint8Array; + + public constructor({ + publicKey, + displayName, + profilePictureURL, + profileKey, + }: { + publicKey: string; + displayName: string; + profilePictureURL?: string; + profileKey?: Uint8Array; + }) { + this.publicKey = publicKey; + this.displayName = displayName; + this.profilePictureURL = profilePictureURL; + this.profileKey = profileKey; + + // will throw if public key is invalid + PubKey.cast(publicKey); + + if (this.displayName?.length === 0) { + throw new Error('displayName must be set or undefined'); + } + + if ( + this.profilePictureURL !== undefined && + this.profilePictureURL?.length === 0 + ) { + throw new Error('profilePictureURL must either undefined or not empty'); + } + if (this.profileKey !== undefined && this.profileKey?.length === 0) { + throw new Error('profileKey must either undefined or not empty'); + } + } + + public toProto(): SignalService.ConfigurationMessage.Contact { + return new SignalService.ConfigurationMessage.Contact({ + publicKey: fromHexToArray(this.publicKey), + name: this.displayName, + profilePicture: this.profilePictureURL, + profileKey: this.profileKey, + }); + } } export class ConfigurationMessageClosedGroup { diff --git a/ts/session/utils/Messages.ts b/ts/session/utils/Messages.ts index cbc0d19fe..4c2110aa3 100644 --- a/ts/session/utils/Messages.ts +++ b/ts/session/utils/Messages.ts @@ -10,6 +10,7 @@ import { ConversationModel } from '../../../js/models/conversations'; import { ConfigurationMessage, ConfigurationMessageClosedGroup, + ConfigurationMessageContact, } from '../messages/outgoing/content/ConfigurationMessage'; import uuid from 'uuid'; import { getLatestClosedGroupEncryptionKeyPair } from '../../../js/modules/data'; @@ -70,11 +71,15 @@ export const getCurrentConfigurationMessage = async ( ) => { const ourPubKey = (await UserUtils.getOurNumber()).key; const ourConvo = convos.find(convo => convo.id === ourPubKey); + + // Filter open groups const openGroupsIds = convos .filter(c => !!c.get('active_at') && c.isPublic() && !c.get('left')) .map(c => c.id.substring((c.id as string).lastIndexOf('@') + 1)) as Array< string >; + + // Filter Closed/Medium groups const closedGroupModels = convos.filter( c => !!c.get('active_at') && @@ -109,6 +114,21 @@ export const getCurrentConfigurationMessage = async ( ConfigurationMessageClosedGroup >; + // Filter contacts + const contactsModels = convos.filter( + c => !!c.get('active_at') && c.isPrivate() && !c.isBlocked() + ); + + const contacts = contactsModels.map(c => { + const groupPubKey = c.get('id'); + return new ConfigurationMessageContact({ + publicKey: groupPubKey, + displayName: c.get('name'), + profilePictureURL: c.get('avatarPointer'), + profileKey: c.get('profileKey'), + }); + }); + if (!ourConvo) { window.log.error( 'Could not find our convo while building a configuration message.' @@ -130,5 +150,6 @@ export const getCurrentConfigurationMessage = async ( displayName, profilePicture, profileKey, + contacts, }); }; diff --git a/ts/test/session/unit/messages/ConfigurationMessage_test.ts b/ts/test/session/unit/messages/ConfigurationMessage_test.ts index 6dc406cb4..b937f7cd2 100644 --- a/ts/test/session/unit/messages/ConfigurationMessage_test.ts +++ b/ts/test/session/unit/messages/ConfigurationMessage_test.ts @@ -4,6 +4,7 @@ import { ECKeyPair } from '../../../../receiver/keypairs'; import { ConfigurationMessage, ConfigurationMessageClosedGroup, + ConfigurationMessageContact, } from '../../../../session/messages/outgoing/content/ConfigurationMessage'; import { TestUtils } from '../../../test-utils'; @@ -16,6 +17,7 @@ describe('ConfigurationMessage', () => { activeOpenGroups: [], timestamp: Date.now(), displayName: 'displayName', + contacts: [], }; expect(() => new ConfigurationMessage(params)).to.throw( 'closed group must be set' @@ -29,6 +31,7 @@ describe('ConfigurationMessage', () => { activeOpenGroups, timestamp: Date.now(), displayName: 'displayName', + contacts: [], }; expect(() => new ConfigurationMessage(params)).to.throw( 'open group must be set' @@ -41,6 +44,7 @@ describe('ConfigurationMessage', () => { activeOpenGroups: [], timestamp: Date.now(), displayName: undefined as any, + contacts: [], }; expect(() => new ConfigurationMessage(params)).to.throw( 'displayName must be set' @@ -53,6 +57,7 @@ describe('ConfigurationMessage', () => { activeOpenGroups: [], timestamp: Date.now(), displayName: undefined as any, + contacts: [], }; expect(() => new ConfigurationMessage(params)).to.throw( 'displayName must be set' @@ -65,6 +70,7 @@ describe('ConfigurationMessage', () => { activeOpenGroups: [], timestamp: Date.now(), displayName: 'displayName', + contacts: [], }; const configMessage = new ConfigurationMessage(params); expect(configMessage.ttl()).to.be.equal(4 * 24 * 60 * 60 * 1000); @@ -175,4 +181,84 @@ describe('ConfigurationMessage', () => { ); }); }); + + describe('ConfigurationMessageContact', () => { + it('throws if contacts is not set', () => { + const params = { + activeClosedGroups: [], + activeOpenGroups: [], + timestamp: Date.now(), + displayName: 'displayName', + contacts: undefined as any, + }; + expect(() => new ConfigurationMessage(params)).to.throw( + 'contacts must be set' + ); + }); + it('throw if some admins are not members', () => { + const member = TestUtils.generateFakePubKey().key; + const admin = TestUtils.generateFakePubKey().key; + const params = { + publicKey: TestUtils.generateFakePubKey().key, + name: 'groupname', + members: [member], + admins: [admin], + encryptionKeyPair: TestUtils.generateFakeECKeyPair(), + }; + + expect(() => new ConfigurationMessageClosedGroup(params)).to.throw( + 'some admins are not members' + ); + }); + + it('throw if the contact has not a valid pubkey', () => { + const params = { + publicKey: '05', + displayName: 'contactDisplayName', + }; + + expect(() => new ConfigurationMessageContact(params)).to.throw(); + + const params2 = { + publicKey: undefined as any, + displayName: 'contactDisplayName', + }; + + expect(() => new ConfigurationMessageContact(params2)).to.throw(); + }); + + it('throw if the contact has an empty disploy name', () => { + // a display name cannot be empty. It should be undefined rather than empty + const params2 = { + publicKey: TestUtils.generateFakePubKey().key, + displayName: '', + }; + + expect(() => new ConfigurationMessageContact(params2)).to.throw(); + }); + + it('throw if the contact has a profileAvatar set but empty', () => { + const params = { + publicKey: TestUtils.generateFakePubKey().key, + displayName: 'contactDisplayName', + profilePictureURL: '', + }; + + expect(() => new ConfigurationMessageContact(params)).to.throw( + 'profilePictureURL must either undefined or not empty' + ); + }); + + it('throw if the contact has a profileKey set but empty', () => { + const params = { + publicKey: TestUtils.generateFakePubKey().key, + displayName: 'contactDisplayName', + profileKey: new Uint8Array(), + }; + + expect(() => new ConfigurationMessageContact(params)).to.throw( + 'profileKey must either undefined or not empty' + ); + }); + }); }); diff --git a/ts/test/session/unit/receiving/ConfigurationMessage_test.ts b/ts/test/session/unit/receiving/ConfigurationMessage_test.ts index 4426856a8..0c3bcc725 100644 --- a/ts/test/session/unit/receiving/ConfigurationMessage_test.ts +++ b/ts/test/session/unit/receiving/ConfigurationMessage_test.ts @@ -36,6 +36,7 @@ describe('ConfigurationMessage_receiving', () => { timestamp: Date.now(), identifier: 'identifier', displayName: 'displayName', + contacts: [], }); }); diff --git a/ts/test/session/unit/utils/Messages_test.ts b/ts/test/session/unit/utils/Messages_test.ts index 41d93d03f..d0abb22e7 100644 --- a/ts/test/session/unit/utils/Messages_test.ts +++ b/ts/test/session/unit/utils/Messages_test.ts @@ -216,6 +216,7 @@ describe('Message Utils', () => { activeOpenGroups: [], activeClosedGroups: [], displayName: 'displayName', + contacts: [], }); const rawMessage = await MessageUtils.toRawMessage(device, msg); expect(rawMessage.encryption).to.equal(EncryptionType.Fallback);