@wireapp/core 17.21.1 → 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 +11 -0
- package/package.json +2 -2
- package/src/main/conversation/ConversationService.d.ts +20 -6
- package/src/main/conversation/ConversationService.js +77 -42
- package/src/main/conversation/ConversationService.js.map +1 -1
- package/src/main/conversation/ConversationService.ts +91 -81
- package/src/main/conversation/message/MessageService.d.ts +11 -4
- package/src/main/conversation/message/MessageService.js +138 -129
- package/src/main/conversation/message/MessageService.js.map +1 -1
- package/src/main/conversation/message/MessageService.test.node.js +61 -18
- package/src/main/conversation/message/MessageService.test.node.js.map +1 -1
- package/src/main/conversation/message/MessageService.test.node.ts +99 -31
- package/src/main/conversation/message/MessageService.ts +159 -152
- package/src/main/util/TypePredicateUtil.d.ts +1 -0
- package/src/main/util/TypePredicateUtil.js +6 -2
- package/src/main/util/TypePredicateUtil.js.map +1 -1
- package/src/main/util/TypePredicateUtil.ts +5 -1
|
@@ -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,62 +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
|
+
|
|
266
290
|
/**
|
|
267
291
|
* Sends a message to a federated environment.
|
|
268
292
|
*
|
|
269
|
-
* @param sendingClientId
|
|
270
|
-
* @param conversationId
|
|
271
|
-
* @param conversationDomain
|
|
272
|
-
* @param genericMessage
|
|
273
|
-
* @param userIds?
|
|
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
|
|
274
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
|
|
275
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
|
|
276
302
|
* @return Resolves with the message sending status from backend
|
|
277
303
|
*/
|
|
278
304
|
private async sendFederatedGenericMessage(
|
|
279
305
|
sendingClientId: string,
|
|
280
|
-
conversationId:
|
|
281
|
-
conversationDomain: string,
|
|
306
|
+
conversationId: QualifiedId,
|
|
282
307
|
genericMessage: GenericMessage,
|
|
283
|
-
|
|
308
|
+
options: {
|
|
309
|
+
userIds?: QualifiedId[] | QualifiedUserClients;
|
|
310
|
+
onClientMismatch?: (mismatch: MessageSendingStatus) => Promise<boolean | undefined>;
|
|
311
|
+
} = {},
|
|
284
312
|
): Promise<MessageSendingStatus> {
|
|
285
313
|
const plainTextArray = GenericMessage.encode(genericMessage).finish();
|
|
286
|
-
const
|
|
287
|
-
? userIds
|
|
288
|
-
: await this.getQualifiedPreKeyBundle(conversationId, conversationDomain, userIds);
|
|
289
|
-
|
|
290
|
-
const recipients = await this.cryptographyService.encryptQualified(plainTextArray, preKeyBundles);
|
|
314
|
+
const recipients = await this.getQualifiedRecipientsForConversation(conversationId, options.userIds);
|
|
291
315
|
|
|
292
|
-
return this.messageService.sendFederatedOTRMessage(
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
recipients,
|
|
297
|
-
plainTextArray,
|
|
298
|
-
undefined,
|
|
299
|
-
isQualifiedUserClients(userIds), // we want to check mismatch in case the consumer gave an exact list of users/devices
|
|
300
|
-
);
|
|
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
|
+
});
|
|
301
320
|
}
|
|
302
321
|
|
|
303
322
|
private async sendGenericMessage(
|
|
304
323
|
sendingClientId: string,
|
|
305
324
|
conversationId: string,
|
|
306
325
|
genericMessage: GenericMessage,
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
326
|
+
options: {
|
|
327
|
+
domain?: string;
|
|
328
|
+
userIds?: string[] | QualifiedId[] | UserClients | QualifiedUserClients;
|
|
329
|
+
sendAsProtobuf?: boolean;
|
|
330
|
+
onClientMismatch?: (mistmatch: ClientMismatch | MessageSendingStatus) => Promise<boolean | undefined>;
|
|
331
|
+
} = {},
|
|
310
332
|
): Promise<ClientMismatch | MessageSendingStatus | undefined> {
|
|
311
|
-
|
|
333
|
+
const {domain, userIds} = options;
|
|
334
|
+
if (domain) {
|
|
312
335
|
if (isStringArray(userIds) || isUserClients(userIds)) {
|
|
313
336
|
throw new Error('Invalid userIds option for sending');
|
|
314
337
|
}
|
|
315
338
|
|
|
316
339
|
return this.sendFederatedGenericMessage(
|
|
317
340
|
this.apiClient.validatedClientId,
|
|
318
|
-
conversationId,
|
|
319
|
-
conversationDomain,
|
|
341
|
+
{id: conversationId, domain},
|
|
320
342
|
genericMessage,
|
|
321
|
-
userIds,
|
|
343
|
+
{userIds, onClientMismatch: options.onClientMismatch},
|
|
322
344
|
);
|
|
323
345
|
}
|
|
324
346
|
|
|
@@ -336,13 +358,13 @@ export class ConversationService {
|
|
|
336
358
|
conversationId,
|
|
337
359
|
encryptedAsset,
|
|
338
360
|
preKeyBundles,
|
|
339
|
-
sendAsProtobuf,
|
|
361
|
+
options.sendAsProtobuf,
|
|
340
362
|
);
|
|
341
363
|
}
|
|
342
364
|
|
|
343
365
|
const recipients = await this.cryptographyService.encrypt(plainTextArray, preKeyBundles);
|
|
344
366
|
|
|
345
|
-
return sendAsProtobuf
|
|
367
|
+
return options.sendAsProtobuf
|
|
346
368
|
? this.messageService.sendOTRProtobufMessage(sendingClientId, recipients, conversationId, plainTextArray)
|
|
347
369
|
: this.messageService.sendOTRMessage(sendingClientId, recipients, conversationId, plainTextArray);
|
|
348
370
|
}
|
|
@@ -628,14 +650,10 @@ export class ConversationService {
|
|
|
628
650
|
|
|
629
651
|
const {id: selfConversationId} = await this.getSelfConversation();
|
|
630
652
|
|
|
631
|
-
await this.sendGenericMessage(
|
|
632
|
-
|
|
633
|
-
selfConversationId,
|
|
634
|
-
genericMessage,
|
|
635
|
-
undefined,
|
|
653
|
+
await this.sendGenericMessage(this.apiClient.validatedClientId, selfConversationId, genericMessage, {
|
|
654
|
+
domain: conversationDomain,
|
|
636
655
|
sendAsProtobuf,
|
|
637
|
-
|
|
638
|
-
);
|
|
656
|
+
});
|
|
639
657
|
|
|
640
658
|
return {
|
|
641
659
|
content,
|
|
@@ -670,14 +688,10 @@ export class ConversationService {
|
|
|
670
688
|
|
|
671
689
|
const {id: selfConversationId} = await this.getSelfConversation();
|
|
672
690
|
|
|
673
|
-
await this.sendGenericMessage(
|
|
674
|
-
this.apiClient.validatedClientId,
|
|
675
|
-
selfConversationId,
|
|
676
|
-
genericMessage,
|
|
677
|
-
undefined,
|
|
691
|
+
await this.sendGenericMessage(this.apiClient.validatedClientId, selfConversationId, genericMessage, {
|
|
678
692
|
sendAsProtobuf,
|
|
679
|
-
conversationDomain,
|
|
680
|
-
);
|
|
693
|
+
domain: conversationDomain,
|
|
694
|
+
});
|
|
681
695
|
|
|
682
696
|
return {
|
|
683
697
|
content,
|
|
@@ -712,14 +726,11 @@ export class ConversationService {
|
|
|
712
726
|
});
|
|
713
727
|
callbacks?.onStart?.(genericMessage);
|
|
714
728
|
|
|
715
|
-
const response = await this.sendGenericMessage(
|
|
716
|
-
this.apiClient.validatedClientId,
|
|
717
|
-
conversationId,
|
|
718
|
-
genericMessage,
|
|
729
|
+
const response = await this.sendGenericMessage(this.apiClient.validatedClientId, conversationId, genericMessage, {
|
|
719
730
|
userIds,
|
|
720
731
|
sendAsProtobuf,
|
|
721
|
-
conversationDomain,
|
|
722
|
-
);
|
|
732
|
+
domain: conversationDomain,
|
|
733
|
+
});
|
|
723
734
|
callbacks?.onSuccess?.(genericMessage, response?.time);
|
|
724
735
|
|
|
725
736
|
return {
|
|
@@ -835,6 +846,7 @@ export class ConversationService {
|
|
|
835
846
|
* @param params.sendAsProtobuf?
|
|
836
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)
|
|
837
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
|
|
838
850
|
* @return resolves with the sent message
|
|
839
851
|
*/
|
|
840
852
|
public async send<T extends OtrMessage = OtrMessage>({
|
|
@@ -916,9 +928,7 @@ export class ConversationService {
|
|
|
916
928
|
this.apiClient.validatedClientId,
|
|
917
929
|
payloadBundle.conversation,
|
|
918
930
|
genericMessage,
|
|
919
|
-
userIds,
|
|
920
|
-
sendAsProtobuf,
|
|
921
|
-
conversationDomain,
|
|
931
|
+
{userIds, sendAsProtobuf, domain: conversationDomain, onClientMismatch: callbacks?.onClientMismatch},
|
|
922
932
|
);
|
|
923
933
|
callbacks?.onSuccess?.(genericMessage, response?.time);
|
|
924
934
|
|
|
@@ -1,6 +1,7 @@
|
|
|
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;
|
|
@@ -8,17 +9,23 @@ export declare class MessageService {
|
|
|
8
9
|
sendOTRMessage(sendingClientId: string, recipients: OTRRecipients<Uint8Array>, conversationId: string | null, plainTextArray: Uint8Array, base64CipherText?: string): Promise<ClientMismatch>;
|
|
9
10
|
private isClientMismatchError;
|
|
10
11
|
private checkFederatedClientsMismatch;
|
|
11
|
-
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>;
|
|
12
17
|
sendOTRProtobufMessage(sendingClientId: string, recipients: OTRRecipients<Uint8Array>, conversationId: string | null, plainTextArray: Uint8Array, assetData?: Uint8Array): Promise<ClientMismatch>;
|
|
13
18
|
private onClientMismatch;
|
|
14
19
|
private onClientProtobufMismatch;
|
|
20
|
+
private deleteExtraQualifiedClients;
|
|
15
21
|
/**
|
|
16
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)
|
|
17
23
|
*
|
|
18
24
|
* @param {ProtobufOTR.QualifiedNewOtrMessage} messageData The initial message that was sent
|
|
19
25
|
* @param {MessageSendingStatus} messageSendingStatus Info about the missing/deleted clients
|
|
20
|
-
* @param {Uint8Array}
|
|
26
|
+
* @param {Uint8Array} plainText The text that should be encrypted for the missing clients
|
|
21
27
|
* @return resolves with a new message payload that can be sent
|
|
22
28
|
*/
|
|
23
|
-
private
|
|
29
|
+
private onFederatedMismatch;
|
|
30
|
+
private addMissingQualifiedClients;
|
|
24
31
|
}
|