@wireapp/core 17.20.3 → 17.22.1

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.
Files changed (26) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/package.json +3 -3
  3. package/src/main/conversation/ConversationService.d.ts +39 -5
  4. package/src/main/conversation/ConversationService.js +95 -38
  5. package/src/main/conversation/ConversationService.js.map +1 -1
  6. package/src/main/conversation/ConversationService.ts +110 -76
  7. package/src/main/conversation/message/MessageService.d.ts +19 -3
  8. package/src/main/conversation/message/MessageService.js +155 -119
  9. package/src/main/conversation/message/MessageService.js.map +1 -1
  10. package/src/main/conversation/message/MessageService.test.node.d.ts +1 -0
  11. package/src/main/conversation/message/MessageService.test.node.js +123 -0
  12. package/src/main/conversation/message/MessageService.test.node.js.map +1 -0
  13. package/src/main/conversation/message/MessageService.test.node.ts +163 -0
  14. package/src/main/conversation/message/MessageService.ts +176 -149
  15. package/src/main/cryptography/CryptographyService.d.ts +3 -3
  16. package/src/main/cryptography/CryptographyService.js +14 -11
  17. package/src/main/cryptography/CryptographyService.js.map +1 -1
  18. package/src/main/cryptography/CryptographyService.ts +23 -16
  19. package/src/main/util/TypePredicateUtil.d.ts +1 -0
  20. package/src/main/util/TypePredicateUtil.js +7 -3
  21. package/src/main/util/TypePredicateUtil.js.map +1 -1
  22. package/src/main/util/TypePredicateUtil.test.node.d.ts +1 -0
  23. package/src/main/util/TypePredicateUtil.test.node.js +42 -0
  24. package/src/main/util/TypePredicateUtil.test.node.js.map +1 -0
  25. package/src/main/util/TypePredicateUtil.test.node.ts +44 -0
  26. 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
- 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,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: string,
269
- conversationDomain: string,
306
+ conversationId: QualifiedId,
270
307
  genericMessage: GenericMessage,
271
- userIds?: QualifiedId[] | QualifiedUserClients,
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 preKeyBundles = await this.getQualifiedPreKeyBundle(conversationId, conversationDomain, userIds);
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
- sendingClientId,
280
- conversationId,
281
- conversationDomain,
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
- userIds?: string[] | QualifiedId[] | UserClients | QualifiedUserClients,
292
- sendAsProtobuf?: boolean,
293
- 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
+ } = {},
294
332
  ): Promise<ClientMismatch | MessageSendingStatus | undefined> {
295
- if (conversationDomain) {
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
- this.apiClient.validatedClientId,
617
- selfConversationId,
618
- genericMessage,
619
- undefined,
653
+ await this.sendGenericMessage(this.apiClient.validatedClientId, selfConversationId, genericMessage, {
654
+ domain: conversationDomain,
620
655
  sendAsProtobuf,
621
- conversationDomain,
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
- * @param payloadBundle Outgoing message
812
- * @param userIds Only send message to specified user IDs or to certain clients of specified user IDs
813
- * @param [callbacks] Optional callbacks that will be called when the message starts being sent and when it has been succesfully sent. Currently only used for `sendText`.
814
- * @returns Sent message
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, 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;
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, conversationId: string, conversationDomain: string, recipients: QualifiedOTRRecipients, plainTextArray: Uint8Array, assetData?: Uint8Array): 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>;
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 onFederatedClientMismatch;
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
  }