@wireapp/core 17.20.2 → 17.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +41 -0
- package/package.json +3 -3
- package/src/main/conversation/ConversationService.d.ts +39 -5
- package/src/main/conversation/ConversationService.js +95 -38
- package/src/main/conversation/ConversationService.js.map +1 -1
- package/src/main/conversation/ConversationService.ts +110 -76
- package/src/main/conversation/message/MessageService.d.ts +19 -3
- package/src/main/conversation/message/MessageService.js +155 -119
- package/src/main/conversation/message/MessageService.js.map +1 -1
- package/src/main/conversation/message/MessageService.test.node.d.ts +1 -0
- package/src/main/conversation/message/MessageService.test.node.js +123 -0
- package/src/main/conversation/message/MessageService.test.node.js.map +1 -0
- package/src/main/conversation/message/MessageService.test.node.ts +163 -0
- package/src/main/conversation/message/MessageService.ts +176 -149
- package/src/main/cryptography/CryptographyService.d.ts +3 -3
- package/src/main/cryptography/CryptographyService.js +14 -11
- package/src/main/cryptography/CryptographyService.js.map +1 -1
- package/src/main/cryptography/CryptographyService.ts +23 -16
- package/src/main/util/TypePredicateUtil.d.ts +1 -0
- package/src/main/util/TypePredicateUtil.js +7 -3
- package/src/main/util/TypePredicateUtil.js.map +1 -1
- package/src/main/util/TypePredicateUtil.test.node.d.ts +1 -0
- package/src/main/util/TypePredicateUtil.test.node.js +42 -0
- package/src/main/util/TypePredicateUtil.test.node.js.map +1 -0
- package/src/main/util/TypePredicateUtil.test.node.ts +44 -0
- package/src/main/util/TypePredicateUtil.ts +6 -2
|
@@ -93,6 +93,7 @@ import type {
|
|
|
93
93
|
export interface MessageSendingCallbacks {
|
|
94
94
|
onStart?: (message: GenericMessage) => void;
|
|
95
95
|
onSuccess?: (message: GenericMessage, sentTime?: string) => void;
|
|
96
|
+
onClientMismatch?: (status: ClientMismatch | MessageSendingStatus) => Promise<boolean | undefined>;
|
|
96
97
|
}
|
|
97
98
|
|
|
98
99
|
export class ConversationService {
|
|
@@ -124,45 +125,57 @@ export class ConversationService {
|
|
|
124
125
|
return genericMessage;
|
|
125
126
|
}
|
|
126
127
|
|
|
128
|
+
private async getConversationQualifiedMembers(conversationId: QualifiedId): Promise<QualifiedId[]> {
|
|
129
|
+
const conversation = await this.apiClient.conversation.api.getConversation(conversationId, true);
|
|
130
|
+
/*
|
|
131
|
+
* If you are sending a message to a conversation, you have to include
|
|
132
|
+
* yourself in the list of users if you want to sync a message also to your
|
|
133
|
+
* other clients.
|
|
134
|
+
*/
|
|
135
|
+
return (
|
|
136
|
+
conversation.members.others
|
|
137
|
+
.filter(member => !!member.qualified_id)
|
|
138
|
+
.map(member => member.qualified_id!)
|
|
139
|
+
// TODO(Federation): Use 'domain' from 'conversation.members.self' when backend has it implemented
|
|
140
|
+
.concat({domain: this.apiClient.context!.domain!, id: conversation.members.self.id})
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Will generate a prekey bundle for specific users.
|
|
146
|
+
* If a QualifiedId array is given the bundle will contain all the clients from those users fetched from the server.
|
|
147
|
+
* If a QualifiedUserClients is provided then only the clients in the payload will be targeted (which could generate a ClientMismatch when sending messages)
|
|
148
|
+
*
|
|
149
|
+
* @param {QualifiedId[]|QualifiedUserClients} userIds - Targeted users.
|
|
150
|
+
* @returns {Promise<QualifiedUserPreKeyBundleMap}
|
|
151
|
+
*/
|
|
127
152
|
private async getQualifiedPreKeyBundle(
|
|
128
|
-
|
|
129
|
-
conversationDomain: string,
|
|
130
|
-
userIds?: QualifiedId[] | QualifiedUserClients,
|
|
153
|
+
userIds: QualifiedId[] | QualifiedUserClients,
|
|
131
154
|
): Promise<QualifiedUserPreKeyBundleMap> {
|
|
132
|
-
|
|
155
|
+
type Target = {id: QualifiedId; clients?: string[]};
|
|
156
|
+
let targets: Target[] = [];
|
|
133
157
|
|
|
134
158
|
if (userIds) {
|
|
135
159
|
if (isQualifiedIdArray(userIds)) {
|
|
136
|
-
|
|
160
|
+
targets = userIds.map(id => ({id}));
|
|
137
161
|
} else {
|
|
138
|
-
|
|
139
|
-
|
|
162
|
+
targets = Object.entries(userIds).reduce<Target[]>((accumulator, [domain, userClients]) => {
|
|
163
|
+
for (const userId in userClients) {
|
|
164
|
+
accumulator.push({id: {id: userId, domain}, clients: userClients[userId]});
|
|
165
|
+
}
|
|
140
166
|
return accumulator;
|
|
141
167
|
}, []);
|
|
142
168
|
}
|
|
143
169
|
}
|
|
144
170
|
|
|
145
|
-
if (!members.length) {
|
|
146
|
-
const conversation = await this.apiClient.conversation.api.getConversation(
|
|
147
|
-
{id: conversationId, domain: conversationDomain},
|
|
148
|
-
true,
|
|
149
|
-
);
|
|
150
|
-
/*
|
|
151
|
-
* If you are sending a message to a conversation, you have to include
|
|
152
|
-
* yourself in the list of users if you want to sync a message also to your
|
|
153
|
-
* other clients.
|
|
154
|
-
*/
|
|
155
|
-
members = conversation.members.others
|
|
156
|
-
.filter(member => !!member.qualified_id)
|
|
157
|
-
.map(member => member.qualified_id!)
|
|
158
|
-
// TODO(Federation): Use 'domain' from 'conversation.members.self' when backend has it implemented
|
|
159
|
-
.concat({domain: this.apiClient.context!.domain!, id: conversation.members.self.id});
|
|
160
|
-
}
|
|
161
|
-
|
|
162
171
|
const preKeys = await Promise.all(
|
|
163
|
-
|
|
164
|
-
const prekeyBundle = await this.apiClient.user.api.getUserPreKeys(
|
|
165
|
-
|
|
172
|
+
targets.map(async ({id: userId, clients}) => {
|
|
173
|
+
const prekeyBundle = await this.apiClient.user.api.getUserPreKeys(userId);
|
|
174
|
+
// We filter the clients that should not receive the message (if a QualifiedUserClients was given as parameter)
|
|
175
|
+
const userClients = clients
|
|
176
|
+
? prekeyBundle.clients.filter(client => clients.includes(client.client))
|
|
177
|
+
: prekeyBundle.clients;
|
|
178
|
+
return {user: userId, clients: userClients};
|
|
166
179
|
}),
|
|
167
180
|
);
|
|
168
181
|
|
|
@@ -263,46 +276,71 @@ export class ConversationService {
|
|
|
263
276
|
return undefined;
|
|
264
277
|
}
|
|
265
278
|
|
|
279
|
+
private async getQualifiedRecipientsForConversation(
|
|
280
|
+
conversationId: QualifiedId,
|
|
281
|
+
userIds?: QualifiedId[] | QualifiedUserClients,
|
|
282
|
+
): Promise<QualifiedUserClients | QualifiedUserPreKeyBundleMap> {
|
|
283
|
+
if (isQualifiedUserClients(userIds)) {
|
|
284
|
+
return userIds;
|
|
285
|
+
}
|
|
286
|
+
const recipientIds = userIds || (await this.getConversationQualifiedMembers(conversationId));
|
|
287
|
+
return this.getQualifiedPreKeyBundle(recipientIds);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Sends a message to a federated environment.
|
|
292
|
+
*
|
|
293
|
+
* @param sendingClientId The clientId from which the message is sent
|
|
294
|
+
* @param conversationId The conversation in which to send the message
|
|
295
|
+
* @param conversationDomain The domain where the conversation lives
|
|
296
|
+
* @param genericMessage The payload of the message to send
|
|
297
|
+
* @param options.userIds? can be either a QualifiedId[] or QualfiedUserClients or undefined. The type has some effect on the behavior of the method.
|
|
298
|
+
* When given undefined the method will fetch both the members of the conversations and their devices. No ClientMismatch can happen in that case
|
|
299
|
+
* When given a QualifiedId[] the method will fetch the freshest list of devices for those users (since they are not given by the consumer). As a consequence no ClientMismatch error will trigger and we will ignore missing clients when sending
|
|
300
|
+
* When given a QualifiedUserClients the method will only send to the clients listed in the userIds. This could lead to ClientMismatch (since the given list of devices might not be the freshest one and new clients could have been created)
|
|
301
|
+
* @param options.onClientMismatch? Will be called whenever there is a clientmismatch returned from the server. Needs to be combined with a userIds of type QualifiedUserClients
|
|
302
|
+
* @return Resolves with the message sending status from backend
|
|
303
|
+
*/
|
|
266
304
|
private async sendFederatedGenericMessage(
|
|
267
305
|
sendingClientId: string,
|
|
268
|
-
conversationId:
|
|
269
|
-
conversationDomain: string,
|
|
306
|
+
conversationId: QualifiedId,
|
|
270
307
|
genericMessage: GenericMessage,
|
|
271
|
-
|
|
308
|
+
options: {
|
|
309
|
+
userIds?: QualifiedId[] | QualifiedUserClients;
|
|
310
|
+
onClientMismatch?: (mismatch: MessageSendingStatus) => Promise<boolean | undefined>;
|
|
311
|
+
} = {},
|
|
272
312
|
): Promise<MessageSendingStatus> {
|
|
273
313
|
const plainTextArray = GenericMessage.encode(genericMessage).finish();
|
|
274
|
-
const
|
|
275
|
-
|
|
276
|
-
const recipients = await this.cryptographyService.encryptQualified(plainTextArray, preKeyBundles);
|
|
314
|
+
const recipients = await this.getQualifiedRecipientsForConversation(conversationId, options.userIds);
|
|
277
315
|
|
|
278
|
-
return this.messageService.sendFederatedOTRMessage(
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
recipients,
|
|
283
|
-
plainTextArray,
|
|
284
|
-
);
|
|
316
|
+
return this.messageService.sendFederatedOTRMessage(sendingClientId, conversationId, recipients, plainTextArray, {
|
|
317
|
+
reportMissing: isQualifiedUserClients(options.userIds), // we want to check mismatch in case the consumer gave an exact list of users/devices
|
|
318
|
+
onClientMismatch: options.onClientMismatch,
|
|
319
|
+
});
|
|
285
320
|
}
|
|
286
321
|
|
|
287
322
|
private async sendGenericMessage(
|
|
288
323
|
sendingClientId: string,
|
|
289
324
|
conversationId: string,
|
|
290
325
|
genericMessage: GenericMessage,
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
326
|
+
options: {
|
|
327
|
+
domain?: string;
|
|
328
|
+
userIds?: string[] | QualifiedId[] | UserClients | QualifiedUserClients;
|
|
329
|
+
sendAsProtobuf?: boolean;
|
|
330
|
+
onClientMismatch?: (mistmatch: ClientMismatch | MessageSendingStatus) => Promise<boolean | undefined>;
|
|
331
|
+
} = {},
|
|
294
332
|
): Promise<ClientMismatch | MessageSendingStatus | undefined> {
|
|
295
|
-
|
|
333
|
+
const {domain, userIds} = options;
|
|
334
|
+
if (domain) {
|
|
296
335
|
if (isStringArray(userIds) || isUserClients(userIds)) {
|
|
297
336
|
throw new Error('Invalid userIds option for sending');
|
|
298
337
|
}
|
|
299
338
|
|
|
300
339
|
return this.sendFederatedGenericMessage(
|
|
301
340
|
this.apiClient.validatedClientId,
|
|
302
|
-
conversationId,
|
|
303
|
-
conversationDomain,
|
|
341
|
+
{id: conversationId, domain},
|
|
304
342
|
genericMessage,
|
|
305
|
-
userIds,
|
|
343
|
+
{userIds, onClientMismatch: options.onClientMismatch},
|
|
306
344
|
);
|
|
307
345
|
}
|
|
308
346
|
|
|
@@ -320,13 +358,13 @@ export class ConversationService {
|
|
|
320
358
|
conversationId,
|
|
321
359
|
encryptedAsset,
|
|
322
360
|
preKeyBundles,
|
|
323
|
-
sendAsProtobuf,
|
|
361
|
+
options.sendAsProtobuf,
|
|
324
362
|
);
|
|
325
363
|
}
|
|
326
364
|
|
|
327
365
|
const recipients = await this.cryptographyService.encrypt(plainTextArray, preKeyBundles);
|
|
328
366
|
|
|
329
|
-
return sendAsProtobuf
|
|
367
|
+
return options.sendAsProtobuf
|
|
330
368
|
? this.messageService.sendOTRProtobufMessage(sendingClientId, recipients, conversationId, plainTextArray)
|
|
331
369
|
: this.messageService.sendOTRMessage(sendingClientId, recipients, conversationId, plainTextArray);
|
|
332
370
|
}
|
|
@@ -612,14 +650,10 @@ export class ConversationService {
|
|
|
612
650
|
|
|
613
651
|
const {id: selfConversationId} = await this.getSelfConversation();
|
|
614
652
|
|
|
615
|
-
await this.sendGenericMessage(
|
|
616
|
-
|
|
617
|
-
selfConversationId,
|
|
618
|
-
genericMessage,
|
|
619
|
-
undefined,
|
|
653
|
+
await this.sendGenericMessage(this.apiClient.validatedClientId, selfConversationId, genericMessage, {
|
|
654
|
+
domain: conversationDomain,
|
|
620
655
|
sendAsProtobuf,
|
|
621
|
-
|
|
622
|
-
);
|
|
656
|
+
});
|
|
623
657
|
|
|
624
658
|
return {
|
|
625
659
|
content,
|
|
@@ -654,14 +688,10 @@ export class ConversationService {
|
|
|
654
688
|
|
|
655
689
|
const {id: selfConversationId} = await this.getSelfConversation();
|
|
656
690
|
|
|
657
|
-
await this.sendGenericMessage(
|
|
658
|
-
this.apiClient.validatedClientId,
|
|
659
|
-
selfConversationId,
|
|
660
|
-
genericMessage,
|
|
661
|
-
undefined,
|
|
691
|
+
await this.sendGenericMessage(this.apiClient.validatedClientId, selfConversationId, genericMessage, {
|
|
662
692
|
sendAsProtobuf,
|
|
663
|
-
conversationDomain,
|
|
664
|
-
);
|
|
693
|
+
domain: conversationDomain,
|
|
694
|
+
});
|
|
665
695
|
|
|
666
696
|
return {
|
|
667
697
|
content,
|
|
@@ -696,14 +726,11 @@ export class ConversationService {
|
|
|
696
726
|
});
|
|
697
727
|
callbacks?.onStart?.(genericMessage);
|
|
698
728
|
|
|
699
|
-
const response = await this.sendGenericMessage(
|
|
700
|
-
this.apiClient.validatedClientId,
|
|
701
|
-
conversationId,
|
|
702
|
-
genericMessage,
|
|
729
|
+
const response = await this.sendGenericMessage(this.apiClient.validatedClientId, conversationId, genericMessage, {
|
|
703
730
|
userIds,
|
|
704
731
|
sendAsProtobuf,
|
|
705
|
-
conversationDomain,
|
|
706
|
-
);
|
|
732
|
+
domain: conversationDomain,
|
|
733
|
+
});
|
|
707
734
|
callbacks?.onSuccess?.(genericMessage, response?.time);
|
|
708
735
|
|
|
709
736
|
return {
|
|
@@ -808,10 +835,19 @@ export class ConversationService {
|
|
|
808
835
|
}
|
|
809
836
|
|
|
810
837
|
/**
|
|
811
|
-
*
|
|
812
|
-
*
|
|
813
|
-
* @param
|
|
814
|
-
* @
|
|
838
|
+
* Sends a message to a conversation
|
|
839
|
+
*
|
|
840
|
+
* @param params.payloadBundle The message to send to the conversation
|
|
841
|
+
* @param params.userIds? Can be either a QualifiedId[], string[], UserClients or QualfiedUserClients. The type has some effect on the behavior of the method.
|
|
842
|
+
* When given a QualifiedId[] or string[] the method will fetch the freshest list of devices for those users (since they are not given by the consumer). As a consequence no ClientMismatch error will trigger and we will ignore missing clients when sending
|
|
843
|
+
* When given a QualifiedUserClients or UserClients the method will only send to the clients listed in the userIds. This could lead to ClientMismatch (since the given list of devices might not be the freshest one and new clients could have been created)
|
|
844
|
+
* When given a QualifiedId[] or QualifiedUserClients the method will send the message through the federated API endpoint
|
|
845
|
+
* When given a string[] or UserClients the method will send the message through the old API endpoint
|
|
846
|
+
* @param params.sendAsProtobuf?
|
|
847
|
+
* @param params.conversationDomain? The domain the conversation lives on (if given with QualifiedId[] or QualfiedUserClients in the userIds params, will send the message to the federated endpoint)
|
|
848
|
+
* @param params.callbacks? Optional callbacks that will be called when the message starts being sent and when it has been succesfully sent.
|
|
849
|
+
* @param [callbacks.onClientMismatch] Will be called when a mismatch happens. Returning `false` from the callback will stop the sending attempt
|
|
850
|
+
* @return resolves with the sent message
|
|
815
851
|
*/
|
|
816
852
|
public async send<T extends OtrMessage = OtrMessage>({
|
|
817
853
|
payloadBundle,
|
|
@@ -892,9 +928,7 @@ export class ConversationService {
|
|
|
892
928
|
this.apiClient.validatedClientId,
|
|
893
929
|
payloadBundle.conversation,
|
|
894
930
|
genericMessage,
|
|
895
|
-
userIds,
|
|
896
|
-
sendAsProtobuf,
|
|
897
|
-
conversationDomain,
|
|
931
|
+
{userIds, sendAsProtobuf, domain: conversationDomain, onClientMismatch: callbacks?.onClientMismatch},
|
|
898
932
|
);
|
|
899
933
|
callbacks?.onSuccess?.(genericMessage, response?.time);
|
|
900
934
|
|
|
@@ -1,15 +1,31 @@
|
|
|
1
1
|
import { APIClient } from '@wireapp/api-client';
|
|
2
|
-
import { ClientMismatch, MessageSendingStatus, OTRRecipients,
|
|
2
|
+
import { ClientMismatch, MessageSendingStatus, OTRRecipients, QualifiedUserClients } from '@wireapp/api-client/src/conversation';
|
|
3
3
|
import { CryptographyService } from '../../cryptography';
|
|
4
|
+
import { QualifiedId, QualifiedUserPreKeyBundleMap } from '@wireapp/api-client/src/user';
|
|
4
5
|
export declare class MessageService {
|
|
5
6
|
private readonly apiClient;
|
|
6
7
|
private readonly cryptographyService;
|
|
7
8
|
constructor(apiClient: APIClient, cryptographyService: CryptographyService);
|
|
8
9
|
sendOTRMessage(sendingClientId: string, recipients: OTRRecipients<Uint8Array>, conversationId: string | null, plainTextArray: Uint8Array, base64CipherText?: string): Promise<ClientMismatch>;
|
|
10
|
+
private isClientMismatchError;
|
|
9
11
|
private checkFederatedClientsMismatch;
|
|
10
|
-
sendFederatedOTRMessage(sendingClientId: string,
|
|
12
|
+
sendFederatedOTRMessage(sendingClientId: string, { id: conversationId, domain }: QualifiedId, recipients: QualifiedUserClients | QualifiedUserPreKeyBundleMap, plainTextArray: Uint8Array, options?: {
|
|
13
|
+
assetData?: Uint8Array;
|
|
14
|
+
reportMissing?: boolean;
|
|
15
|
+
onClientMismatch?: (mismatch: MessageSendingStatus) => Promise<boolean | undefined>;
|
|
16
|
+
}): Promise<MessageSendingStatus>;
|
|
11
17
|
sendOTRProtobufMessage(sendingClientId: string, recipients: OTRRecipients<Uint8Array>, conversationId: string | null, plainTextArray: Uint8Array, assetData?: Uint8Array): Promise<ClientMismatch>;
|
|
12
18
|
private onClientMismatch;
|
|
13
19
|
private onClientProtobufMismatch;
|
|
14
|
-
private
|
|
20
|
+
private deleteExtraQualifiedClients;
|
|
21
|
+
/**
|
|
22
|
+
* Will re-encrypt a message when there were some missing clients in the initial send (typically when the server replies with a client mismatch error)
|
|
23
|
+
*
|
|
24
|
+
* @param {ProtobufOTR.QualifiedNewOtrMessage} messageData The initial message that was sent
|
|
25
|
+
* @param {MessageSendingStatus} messageSendingStatus Info about the missing/deleted clients
|
|
26
|
+
* @param {Uint8Array} plainText The text that should be encrypted for the missing clients
|
|
27
|
+
* @return resolves with a new message payload that can be sent
|
|
28
|
+
*/
|
|
29
|
+
private onFederatedMismatch;
|
|
30
|
+
private addMissingQualifiedClients;
|
|
15
31
|
}
|