@wireapp/core 17.21.1 → 17.24.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.
@@ -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
- conversationId: string,
129
- conversationDomain: string,
130
- userIds?: QualifiedId[] | QualifiedUserClients,
153
+ userIds: QualifiedId[] | QualifiedUserClients,
131
154
  ): Promise<QualifiedUserPreKeyBundleMap> {
132
- let members: QualifiedId[] = [];
155
+ type Target = {id: QualifiedId; clients?: string[]};
156
+ let targets: Target[] = [];
133
157
 
134
158
  if (userIds) {
135
159
  if (isQualifiedIdArray(userIds)) {
136
- members = userIds;
160
+ targets = userIds.map(id => ({id}));
137
161
  } else {
138
- members = Object.entries(userIds).reduce<QualifiedId[]>((accumulator, [domain, userClients]) => {
139
- accumulator.push(...Object.keys(userClients).map(userId => ({domain, id: userId})));
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
- members.map(async qualifiedUserId => {
164
- const prekeyBundle = await this.apiClient.user.api.getUserPreKeys(qualifiedUserId);
165
- return {user: qualifiedUserId, clients: prekeyBundle.clients};
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 - The clientId from which the message is sent
270
- * @param conversationId - The conversation in which to send the message
271
- * @param conversationDomain - The domain where the conversation lives
272
- * @param genericMessage - The payload of the message to send
273
- * @param userIds? - can be either a QualifiedId[] or QualfiedUserClients. The type has some effect on the behavior of the method.
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: string,
281
- conversationDomain: string,
306
+ conversationId: QualifiedId,
282
307
  genericMessage: GenericMessage,
283
- userIds?: QualifiedId[] | QualifiedUserClients,
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 preKeyBundles = isQualifiedUserClients(userIds)
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
- sendingClientId,
294
- conversationId,
295
- conversationDomain,
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
- userIds?: string[] | QualifiedId[] | UserClients | QualifiedUserClients,
308
- sendAsProtobuf?: boolean,
309
- conversationDomain?: string,
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
- if (conversationDomain) {
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
- this.apiClient.validatedClientId,
633
- selfConversationId,
634
- genericMessage,
635
- undefined,
653
+ await this.sendGenericMessage(this.apiClient.validatedClientId, selfConversationId, genericMessage, {
654
+ domain: conversationDomain,
636
655
  sendAsProtobuf,
637
- conversationDomain,
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, QualifiedOTRRecipients } from '@wireapp/api-client/src/conversation';
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, conversationId: string, conversationDomain: string, recipients: QualifiedOTRRecipients, plainTextArray: Uint8Array, assetData?: Uint8Array, reportMissing?: boolean): Promise<MessageSendingStatus>;
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} plainTextArray The text that should be encrypted for the missing clients
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 encryptForMissingClients;
29
+ private onFederatedMismatch;
30
+ private addMissingQualifiedClients;
24
31
  }