@wireapp/core 29.2.2 → 29.3.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 CHANGED
@@ -3,6 +3,33 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ # [29.3.0](https://github.com/wireapp/wire-web-packages/tree/main/packages/core/compare/@wireapp/core@29.2.4...@wireapp/core@29.3.0) (2022-08-25)
7
+
8
+
9
+ ### Features
10
+
11
+ * periodically commit proposals (fs-690) ([#4362](https://github.com/wireapp/wire-web-packages/tree/main/packages/core/issues/4362)) ([d821f9e](https://github.com/wireapp/wire-web-packages/tree/main/packages/core/commit/d821f9e43d35b4cb151588f5a7b18aa503e39410))
12
+
13
+
14
+
15
+
16
+
17
+ ## [29.2.4](https://github.com/wireapp/wire-web-packages/tree/main/packages/core/compare/@wireapp/core@29.2.3...@wireapp/core@29.2.4) (2022-08-23)
18
+
19
+ **Note:** Version bump only for package @wireapp/core
20
+
21
+
22
+
23
+
24
+
25
+ ## [29.2.3](https://github.com/wireapp/wire-web-packages/tree/main/packages/core/compare/@wireapp/core@29.2.2...@wireapp/core@29.2.3) (2022-08-23)
26
+
27
+ **Note:** Version bump only for package @wireapp/core
28
+
29
+
30
+
31
+
32
+
6
33
  ## [29.2.2](https://github.com/wireapp/wire-web-packages/tree/main/packages/core/compare/@wireapp/core@29.2.1...@wireapp/core@29.2.2) (2022-08-17)
7
34
 
8
35
  **Note:** Version bump only for package @wireapp/core
package/package.json CHANGED
@@ -7,7 +7,7 @@
7
7
  "@otak/core-crypto": "0.3.0-es2017",
8
8
  "@types/long": "4.0.1",
9
9
  "@types/node": "~14",
10
- "@wireapp/api-client": "19.21.4",
10
+ "@wireapp/api-client": "19.21.6",
11
11
  "@wireapp/commons": "4.3.0",
12
12
  "@wireapp/cryptobox": "12.8.0",
13
13
  "@wireapp/store-engine-dexie": "1.6.10",
@@ -77,6 +77,6 @@
77
77
  "test:node": "nyc jasmine --config=jasmine.json",
78
78
  "watch": "tsc ---watch"
79
79
  },
80
- "version": "29.2.2",
81
- "gitHead": "7270b4cee73a6944e1b05e5374bd2860c23d0bb7"
80
+ "version": "29.3.0",
81
+ "gitHead": "e9707c97b3cfa69fa1e51c21d0d0ca6923f57d4d"
82
82
  }
@@ -137,10 +137,14 @@ class Account extends events_1.EventEmitter {
137
137
  * @param cookie The cookie to identify the user against backend (will use the browser's one if not given)
138
138
  */
139
139
  async init(clientType, cookie, initClient = true) {
140
+ var _a;
140
141
  const context = await this.apiClient.init(clientType, cookie);
141
142
  await this.initServices(context);
143
+ // Assumption: client gets only initialized once
142
144
  if (initClient) {
143
145
  await this.initClient({ clientType });
146
+ // initialize schedulers for pending mls proposals once client is initialized
147
+ await ((_a = this.service) === null || _a === void 0 ? void 0 : _a.notification.checkExistingPendingProposals());
144
148
  }
145
149
  return context;
146
150
  }
@@ -4,7 +4,7 @@ import { MessageSendingStatus, Conversation, DefaultConversationRoleName, MutedS
4
4
  import type { ConversationMemberLeaveEvent } from '@wireapp/api-client/src/event';
5
5
  import type { QualifiedId, UserPreKeyBundleMap } from '@wireapp/api-client/src/user';
6
6
  import { MessageTimer } from '../../conversation/';
7
- import type { RemoteData } from '../../conversation/content/';
7
+ import type { RemoteData } from '../content';
8
8
  import type { CryptographyService } from '../../cryptography/';
9
9
  import type { ClearConversationMessage, DeleteMessage, HideMessage, OtrMessage } from '../message/OtrMessage';
10
10
  import { XOR } from '@wireapp/commons/src/main/util/TypeUtil';
@@ -839,6 +839,9 @@ class ConversationService {
839
839
  */
840
840
  const newConversation = await this.apiClient.api.conversation.postConversation(Object.assign(Object.assign({}, conversationData), { users: undefined, qualified_users: undefined }));
841
841
  const { group_id: groupId, qualified_id: qualifiedId } = newConversation;
842
+ if (!groupId) {
843
+ throw new Error('No group_id found in response which is required for creating MLS conversations.');
844
+ }
842
845
  const groupIdDecodedFromBase64 = bazinga64_1.Decoder.fromBase64(groupId).asBytes;
843
846
  const { qualified_users: qualifiedUsers = [], selfUserId } = conversationData;
844
847
  if (!selfUserId) {
@@ -871,6 +874,8 @@ class ConversationService {
871
874
  var _a, _b;
872
875
  const { groupId, onSuccess, payload } = params;
873
876
  const groupIdBytes = bazinga64_1.Decoder.fromBase64(groupId).asBytes;
877
+ // immediately execute pending commits before sending the message
878
+ await this.notificationService.commitPendingProposals({ groupId });
874
879
  const coreCryptoClient = this.coreCryptoClientProvider();
875
880
  const encrypted = await coreCryptoClient.encryptMessage(groupIdBytes, protocol_messaging_1.GenericMessage.encode(genericMessage).finish());
876
881
  try {
@@ -886,7 +891,6 @@ class ConversationService {
886
891
  const groupIdDecodedFromBase64 = bazinga64_1.Decoder.fromBase64(groupId).asBytes;
887
892
  const coreCryptoKeyPackagesPayload = await this.getCoreCryptoKeyPackagesPayload([...qualifiedUserIds]);
888
893
  const response = await this.addUsersToExistingMLSConversation(groupIdDecodedFromBase64, coreCryptoKeyPackagesPayload);
889
- console.info('addUsersToMLSGroup', conversationId, qualifiedUserIds, groupIdDecodedFromBase64, response);
890
894
  const conversation = await this.getConversations(conversationId.id);
891
895
  return {
892
896
  events: (response === null || response === void 0 ? void 0 : response.events) || [],
@@ -1,8 +1,8 @@
1
1
  /// <reference types="node" />
2
2
  import type { Asset } from '@wireapp/protocol-messaging';
3
- import type { AbortReason, AssetTransferState } from '../../conversation/';
4
- import type { FileContent, FileMetaDataContent, ImageContent, LegalHoldStatus } from '../../conversation/content/';
5
- import type { EncryptedAssetUploaded } from '../../cryptography/';
3
+ import type { AbortReason, AssetTransferState } from '..';
4
+ import type { FileContent, FileMetaDataContent, ImageContent, LegalHoldStatus } from '.';
5
+ import type { EncryptedAssetUploaded } from '../../cryptography';
6
6
  export declare type ImageMetaData = Asset.IImageMetaData;
7
7
  export declare type VideoMetaData = Asset.IVideoMetaData;
8
8
  export declare type Preview = Asset.IPreview;
@@ -1,6 +1,6 @@
1
1
  import type { Connection } from '@wireapp/api-client/src/connection/';
2
- import type { ClientActionType } from '../';
3
- import type { AssetContent, ClearedContent, ClientActionContent, ConfirmationContent, ConversationContent, DeletedContent, EditedTextContent, FileAssetAbortContent, FileAssetContent, FileAssetMetaDataContent, HiddenContent, ImageAssetContent, ImageContent, LocationContent, ReactionContent, TextContent } from '../content/';
2
+ import type { ClientActionType } from '..';
3
+ import type { AssetContent, ClearedContent, ClientActionContent, ConfirmationContent, ConversationContent, DeletedContent, EditedTextContent, FileAssetAbortContent, FileAssetContent, FileAssetMetaDataContent, HiddenContent, ImageAssetContent, ImageContent, LocationContent, ReactionContent, TextContent } from '.';
4
4
  export declare function isAbortedAssetContent(content: ConversationContent): content is AssetContent;
5
5
  export declare function isAssetContent(content: ConversationContent): content is AssetContent;
6
6
  export declare function isClearedContent(content: ConversationContent): content is ClearedContent;
@@ -1,3 +1,3 @@
1
- import type { ClientActionType } from '../';
2
- import type { AssetContent, ButtonActionContent, ButtonActionConfirmationContent, CallingContent, ClearedContent, ClientActionContent, ClientAddContent, ClientRemoveContent, CompositeContent, ConfirmationContent, ConnectionContent, DeletedContent, EditedTextContent, FileAssetAbortContent, FileAssetContent, FileAssetMetaDataContent, HiddenContent, ImageAssetContent, ImageContent, KnockContent, LocationContent, ReactionContent, TextContent } from './';
1
+ import type { ClientActionType } from '..';
2
+ import type { AssetContent, ButtonActionContent, ButtonActionConfirmationContent, CallingContent, ClearedContent, ClientActionContent, ClientAddContent, ClientRemoveContent, CompositeContent, ConfirmationContent, ConnectionContent, DeletedContent, EditedTextContent, FileAssetAbortContent, FileAssetContent, FileAssetMetaDataContent, HiddenContent, ImageAssetContent, ImageContent, KnockContent, LocationContent, ReactionContent, TextContent } from '.';
3
3
  export declare type ConversationContent = AssetContent | ButtonActionContent | ButtonActionConfirmationContent | CallingContent | ClearedContent | ClientActionContent | ClientActionType | ClientAddContent | ClientRemoveContent | CompositeContent | ConfirmationContent | ConnectionContent | DeletedContent | EditedTextContent | FileAssetAbortContent | FileAssetContent | FileAssetMetaDataContent | HiddenContent | ImageAssetContent | ImageContent | KnockContent | LocationContent | ReactionContent | TextContent;
@@ -1,4 +1,4 @@
1
- import type { QuoteContent, TextContent } from '../content/';
1
+ import type { QuoteContent, TextContent } from '.';
2
2
  export interface EditedTextContent extends TextContent {
3
3
  originalMessageId: string;
4
4
  /**
@@ -1,5 +1,5 @@
1
1
  import type { ILinkPreview } from '@wireapp/protocol-messaging';
2
- import type { ImageAssetContent, ImageContent, LegalHoldStatus } from '../content/';
2
+ import type { ImageAssetContent, ImageContent, LegalHoldStatus } from '.';
3
3
  export interface LinkPreviewContent extends Omit<ILinkPreview, 'image'> {
4
4
  expectsReadConfirmation?: boolean;
5
5
  legalHoldStatus?: LegalHoldStatus;
@@ -1,4 +1,4 @@
1
- import type { LegalHoldStatus } from './';
1
+ import type { LegalHoldStatus } from '.';
2
2
  export interface LocationContent {
3
3
  expectsReadConfirmation?: boolean;
4
4
  latitude: number;
@@ -1,5 +1,5 @@
1
1
  import type { IQuote } from '@wireapp/protocol-messaging';
2
- import type { AssetContent, LocationContent, TextContent } from '../content/';
2
+ import type { AssetContent, LocationContent, TextContent } from '.';
3
3
  export { IQuote as QuoteContent };
4
4
  export interface QuoteMessageContent {
5
5
  content: AssetContent | LocationContent | TextContent;
@@ -1,5 +1,5 @@
1
1
  import type { LegalHoldStatus } from '.';
2
- import type { ReactionType } from '../';
2
+ import type { ReactionType } from '..';
3
3
  export interface ReactionContent {
4
4
  legalHoldStatus?: LegalHoldStatus;
5
5
  originalMessageId: string;
@@ -1,4 +1,4 @@
1
- import type { LegalHoldStatus, LinkPreviewUploadedContent, MentionContent, QuoteContent } from '../content/';
1
+ import type { LegalHoldStatus, LinkPreviewUploadedContent, MentionContent, QuoteContent } from '.';
2
2
  export interface TextContent {
3
3
  expectsReadConfirmation?: boolean;
4
4
  legalHoldStatus?: LegalHoldStatus;
@@ -5,7 +5,8 @@ export declare enum DatabaseStores {
5
5
  KEYS = "keys",
6
6
  PRE_KEYS = "prekeys",
7
7
  SESSIONS = "sessions",
8
- GROUP_IDS = "group_ids"
8
+ GROUP_IDS = "group_ids",
9
+ PENDING_PROPOSALS = "pending_proposals"
9
10
  }
10
11
  export declare class CryptographyDatabaseRepository {
11
12
  private readonly storeEngine;
@@ -27,20 +27,14 @@ var DatabaseStores;
27
27
  DatabaseStores["PRE_KEYS"] = "prekeys";
28
28
  DatabaseStores["SESSIONS"] = "sessions";
29
29
  DatabaseStores["GROUP_IDS"] = "group_ids";
30
+ DatabaseStores["PENDING_PROPOSALS"] = "pending_proposals";
30
31
  })(DatabaseStores = exports.DatabaseStores || (exports.DatabaseStores = {}));
31
32
  class CryptographyDatabaseRepository {
32
33
  constructor(storeEngine) {
33
34
  this.storeEngine = storeEngine;
34
35
  }
35
36
  deleteStores() {
36
- return Promise.all([
37
- this.storeEngine.deleteAll(CryptographyDatabaseRepository.STORES.AMPLIFY),
38
- this.storeEngine.deleteAll(CryptographyDatabaseRepository.STORES.CLIENTS),
39
- this.storeEngine.deleteAll(CryptographyDatabaseRepository.STORES.KEYS),
40
- this.storeEngine.deleteAll(CryptographyDatabaseRepository.STORES.SESSIONS),
41
- this.storeEngine.deleteAll(CryptographyDatabaseRepository.STORES.PRE_KEYS),
42
- this.storeEngine.deleteAll(CryptographyDatabaseRepository.STORES.GROUP_IDS),
43
- ]);
37
+ return Promise.all(Object.keys(CryptographyDatabaseRepository.STORES).map(store => this.storeEngine.deleteAll(store)));
44
38
  }
45
39
  }
46
40
  exports.CryptographyDatabaseRepository = CryptographyDatabaseRepository;
@@ -1,11 +1,7 @@
1
1
  import type { BackendEvent } from '@wireapp/api-client/src/event';
2
2
  import type { Notification } from '@wireapp/api-client/src/notification/';
3
3
  import type { CRUDEngine } from '@wireapp/store-engine';
4
- declare type CompoundGroupIdParams = {
5
- groupId: string;
6
- conversationId: string;
7
- conversationDomain: string;
8
- };
4
+ import { CommonMLS, CompoundGroupIdParams, StorePendingProposalsParams } from './types';
9
5
  export declare enum DatabaseStores {
10
6
  EVENTS = "events"
11
7
  }
@@ -23,7 +19,27 @@ export declare class NotificationDatabaseRepository {
23
19
  getLastNotificationId(): Promise<string>;
24
20
  updateLastNotificationId(lastNotification: Notification): Promise<string>;
25
21
  private generateCompoundGroupIdPrimaryKey;
26
- addCompoundGroupId(params: CompoundGroupIdParams): Promise<void>;
22
+ addCompoundGroupId(params: CompoundGroupIdParams): Promise<CompoundGroupIdParams>;
27
23
  getCompoundGroupId(params: Omit<CompoundGroupIdParams, 'groupId'>): Promise<string>;
24
+ /**
25
+ * ## MLS only ##
26
+ * Store groupIds with pending proposals and a delay in the DB until the proposals get committed.
27
+ *
28
+ * @param groupId groupId of the mls conversation
29
+ * @param firingDate date when the pending proposals should be committed
30
+ */
31
+ storePendingProposal(params: StorePendingProposalsParams): Promise<boolean>;
32
+ /**
33
+ * ## MLS only ##
34
+ * Delete stored entries for pending proposals that have been committed.
35
+ *
36
+ * @param groupId groupId of the mls conversation
37
+ */
38
+ deletePendingProposal({ groupId }: CommonMLS): Promise<boolean>;
39
+ /**
40
+ * ## MLS only ##
41
+ * Get all stored entries for pending proposals.
42
+ *
43
+ */
44
+ getStoredPendingProposals(): Promise<StorePendingProposalsParams[]>;
28
45
  }
29
- export {};
@@ -29,8 +29,7 @@ var DatabaseKeys;
29
29
  DatabaseKeys["PRIMARY_KEY_LAST_EVENT"] = "z.storage.StorageKey.EVENT.LAST_DATE";
30
30
  DatabaseKeys["PRIMARY_KEY_LAST_NOTIFICATION"] = "z.storage.StorageKey.NOTIFICATION.LAST_ID";
31
31
  })(DatabaseKeys = exports.DatabaseKeys || (exports.DatabaseKeys = {}));
32
- const STORE_AMPLIFY = CryptographyDatabaseRepository_1.CryptographyDatabaseRepository.STORES.AMPLIFY;
33
- const STORE_GROUPIDS = CryptographyDatabaseRepository_1.CryptographyDatabaseRepository.STORES.GROUP_IDS;
32
+ const STORES = Object.assign({}, CryptographyDatabaseRepository_1.CryptographyDatabaseRepository.STORES);
34
33
  class NotificationDatabaseRepository {
35
34
  constructor(storeEngine) {
36
35
  this.storeEngine = storeEngine;
@@ -39,23 +38,27 @@ class NotificationDatabaseRepository {
39
38
  return this.storeEngine.readAll(DatabaseStores.EVENTS);
40
39
  }
41
40
  async getLastEventDate() {
42
- const { value } = await this.storeEngine.read(STORE_AMPLIFY, DatabaseKeys.PRIMARY_KEY_LAST_EVENT);
41
+ const { value } = await this.storeEngine.read(STORES.AMPLIFY, DatabaseKeys.PRIMARY_KEY_LAST_EVENT);
43
42
  return new Date(value);
44
43
  }
45
44
  async updateLastEventDate(eventDate) {
46
- await this.storeEngine.update(STORE_AMPLIFY, DatabaseKeys.PRIMARY_KEY_LAST_EVENT, { value: eventDate.toISOString() });
45
+ await this.storeEngine.update(STORES.AMPLIFY, DatabaseKeys.PRIMARY_KEY_LAST_EVENT, {
46
+ value: eventDate.toISOString(),
47
+ });
47
48
  return eventDate;
48
49
  }
49
50
  async createLastEventDate(eventDate) {
50
- await this.storeEngine.create(STORE_AMPLIFY, DatabaseKeys.PRIMARY_KEY_LAST_EVENT, { value: eventDate.toISOString() });
51
+ await this.storeEngine.create(STORES.AMPLIFY, DatabaseKeys.PRIMARY_KEY_LAST_EVENT, {
52
+ value: eventDate.toISOString(),
53
+ });
51
54
  return eventDate;
52
55
  }
53
56
  async getLastNotificationId() {
54
- const { value } = await this.storeEngine.read(STORE_AMPLIFY, DatabaseKeys.PRIMARY_KEY_LAST_NOTIFICATION);
57
+ const { value } = await this.storeEngine.read(STORES.AMPLIFY, DatabaseKeys.PRIMARY_KEY_LAST_NOTIFICATION);
55
58
  return value;
56
59
  }
57
60
  async updateLastNotificationId(lastNotification) {
58
- await this.storeEngine.updateOrCreate(STORE_AMPLIFY, DatabaseKeys.PRIMARY_KEY_LAST_NOTIFICATION, {
61
+ await this.storeEngine.updateOrCreate(STORES.AMPLIFY, DatabaseKeys.PRIMARY_KEY_LAST_NOTIFICATION, {
59
62
  value: lastNotification.id,
60
63
  });
61
64
  return lastNotification.id;
@@ -64,10 +67,40 @@ class NotificationDatabaseRepository {
64
67
  return `${conversationId}@${conversationDomain}`;
65
68
  }
66
69
  async addCompoundGroupId(params) {
67
- await this.storeEngine.updateOrCreate(STORE_GROUPIDS, this.generateCompoundGroupIdPrimaryKey(params), params.groupId);
70
+ await this.storeEngine.updateOrCreate(STORES.GROUP_IDS, this.generateCompoundGroupIdPrimaryKey(params), params.groupId);
71
+ return params;
68
72
  }
69
73
  async getCompoundGroupId(params) {
70
- return this.storeEngine.read(STORE_GROUPIDS, this.generateCompoundGroupIdPrimaryKey(params));
74
+ return this.storeEngine.read(STORES.GROUP_IDS, this.generateCompoundGroupIdPrimaryKey(params));
75
+ }
76
+ /**
77
+ * ## MLS only ##
78
+ * Store groupIds with pending proposals and a delay in the DB until the proposals get committed.
79
+ *
80
+ * @param groupId groupId of the mls conversation
81
+ * @param firingDate date when the pending proposals should be committed
82
+ */
83
+ async storePendingProposal(params) {
84
+ await this.storeEngine.updateOrCreate(STORES.PENDING_PROPOSALS, `${params.groupId}`, params);
85
+ return true;
86
+ }
87
+ /**
88
+ * ## MLS only ##
89
+ * Delete stored entries for pending proposals that have been committed.
90
+ *
91
+ * @param groupId groupId of the mls conversation
92
+ */
93
+ async deletePendingProposal({ groupId }) {
94
+ await this.storeEngine.delete(STORES.PENDING_PROPOSALS, `${groupId}`);
95
+ return true;
96
+ }
97
+ /**
98
+ * ## MLS only ##
99
+ * Get all stored entries for pending proposals.
100
+ *
101
+ */
102
+ async getStoredPendingProposals() {
103
+ return this.storeEngine.readAll(STORES.PENDING_PROPOSALS);
71
104
  }
72
105
  }
73
106
  exports.NotificationDatabaseRepository = NotificationDatabaseRepository;
@@ -12,6 +12,7 @@ import { AbortHandler } from '@wireapp/api-client/src/tcp';
12
12
  import type { CoreCrypto } from '@otak/core-crypto/platforms/web/corecrypto';
13
13
  import { QualifiedId } from '@wireapp/api-client/src/user';
14
14
  import { Conversation } from '@wireapp/api-client/src/conversation';
15
+ import { CommitPendingProposalsParams } from './types';
15
16
  export declare type HandledEventPayload = {
16
17
  event: Events.BackendEvent;
17
18
  mappedEvent?: PayloadBundle;
@@ -61,6 +62,7 @@ export declare class NotificationService extends EventEmitter {
61
62
  private cleanupPayloadBundle;
62
63
  private handleEvent;
63
64
  /**
65
+ * ## MLS only ##
64
66
  * If there is a groupId in the conversation, we need to store the conversationId => groupId pair
65
67
  * in order to find the groupId when decrypting messages
66
68
  * This is a bit hacky but since mls messages do not embed the groupId we need to keep a mapping of those
@@ -69,11 +71,37 @@ export declare class NotificationService extends EventEmitter {
69
71
  */
70
72
  saveConversationGroupId(conversation: Conversation): Promise<void>;
71
73
  /**
74
+ * ## MLS only ##
72
75
  * If there is a matching conversationId => groupId pair in the database,
73
76
  * we can find the groupId and return it as a Uint8Array
74
77
  *
75
78
  * @param conversationQualifiedId
76
79
  */
77
80
  getUint8ArrayFromConversationGroupId(conversationQualifiedId: QualifiedId): Promise<Uint8Array>;
81
+ /**
82
+ * ## MLS only ##
83
+ * If there are pending proposals, we need to either process them,
84
+ * or save them in the database for later processing
85
+ *
86
+ * @param groupId groupId of the mls conversation
87
+ * @param delayInMs delay in ms before processing proposals
88
+ * @param eventTime time of the event that had the proposals
89
+ */
90
+ private handlePendingProposals;
91
+ /**
92
+ * ## MLS only ##
93
+ * Commit all pending proposals for a given groupId
94
+ *
95
+ * @param groupId groupId of the conversation
96
+ * @param skipDelete if true, do not delete the pending proposals from the database
97
+ */
98
+ commitPendingProposals({ groupId, skipDelete }: CommitPendingProposalsParams): Promise<void>;
99
+ /**
100
+ * ## MLS only ##
101
+ * Get all pending proposals from the database and schedule them
102
+ * Function must only be called once, after application start
103
+ *
104
+ */
105
+ checkExistingPendingProposals(): Promise<void>;
78
106
  }
79
107
  export {};
@@ -65,6 +65,7 @@ const NotificationBackendRepository_1 = require("./NotificationBackendRepository
65
65
  const NotificationDatabaseRepository_1 = require("./NotificationDatabaseRepository");
66
66
  const protocol_messaging_1 = require("@wireapp/protocol-messaging");
67
67
  const bazinga64_1 = require("bazinga64");
68
+ const TaskScheduler_1 = require("../util/TaskScheduler/TaskScheduler");
68
69
  var TOPIC;
69
70
  (function (TOPIC) {
70
71
  TOPIC["NOTIFICATION_ERROR"] = "NotificationService.TOPIC.NOTIFICATION_ERROR";
@@ -209,7 +210,7 @@ class NotificationService extends events_1.EventEmitter {
209
210
  }
210
211
  }
211
212
  async handleEvent(event, source, dryRun = false) {
212
- var _a;
213
+ var _a, _b;
213
214
  const coreCryptoClient = this.coreCryptoClientProvider();
214
215
  if (!coreCryptoClient) {
215
216
  throw new Error('Unable to access core crypto client');
@@ -227,12 +228,20 @@ class NotificationService extends events_1.EventEmitter {
227
228
  };
228
229
  case Events.CONVERSATION_EVENT.MLS_MESSAGE_ADD:
229
230
  const encryptedData = bazinga64_1.Decoder.fromBase64(event.data).asBytes;
230
- const groupId = await this.getUint8ArrayFromConversationGroupId(event.qualified_conversation || { id: event.conversation, domain: '' });
231
- const rawData = await coreCryptoClient.decryptMessage(groupId, encryptedData);
232
- if (!rawData.message) {
231
+ const groupId = await this.getUint8ArrayFromConversationGroupId((_a = event.qualified_conversation) !== null && _a !== void 0 ? _a : { id: event.conversation, domain: '' });
232
+ // Check if the message includes proposals
233
+ const { proposals, commitDelay, message } = await coreCryptoClient.decryptMessage(groupId, encryptedData);
234
+ if (proposals.length > 0) {
235
+ await this.handlePendingProposals({
236
+ groupId: groupId.toString(),
237
+ delayInMs: commitDelay !== null && commitDelay !== void 0 ? commitDelay : 0,
238
+ eventTime: event.time,
239
+ });
240
+ }
241
+ if (!message) {
233
242
  throw new Error(`MLS message received from ${source} was empty`);
234
243
  }
235
- const decryptedData = protocol_messaging_1.GenericMessage.decode(rawData.message);
244
+ const decryptedData = protocol_messaging_1.GenericMessage.decode(message);
236
245
  /**
237
246
  * @todo Find a proper solution to add mappedEvent to this return
238
247
  * otherwise event.data will be base64 raw data of the received event
@@ -261,7 +270,7 @@ class NotificationService extends events_1.EventEmitter {
261
270
  case Events.CONVERSATION_EVENT.MEMBER_JOIN:
262
271
  // As of today (07/07/2022) the backend sends `WELCOME` message to the user's own conversation (not the actual conversation that the welcome should be part of)
263
272
  // So in order to map conversation Ids and groupId together, we need to first fetch the conversation and get the groupId linked to it.
264
- const conversation = await this.apiClient.api.conversation.getConversation((_a = event.qualified_conversation) !== null && _a !== void 0 ? _a : { id: event.conversation, domain: '' });
273
+ const conversation = await this.apiClient.api.conversation.getConversation((_b = event.qualified_conversation) !== null && _b !== void 0 ? _b : { id: event.conversation, domain: '' });
265
274
  if (!conversation) {
266
275
  throw new Error('no conv');
267
276
  }
@@ -284,6 +293,7 @@ class NotificationService extends events_1.EventEmitter {
284
293
  return { event };
285
294
  }
286
295
  /**
296
+ * ## MLS only ##
287
297
  * If there is a groupId in the conversation, we need to store the conversationId => groupId pair
288
298
  * in order to find the groupId when decrypting messages
289
299
  * This is a bit hacky but since mls messages do not embed the groupId we need to keep a mapping of those
@@ -297,6 +307,7 @@ class NotificationService extends events_1.EventEmitter {
297
307
  }
298
308
  }
299
309
  /**
310
+ * ## MLS only ##
300
311
  * If there is a matching conversationId => groupId pair in the database,
301
312
  * we can find the groupId and return it as a Uint8Array
302
313
  *
@@ -313,6 +324,84 @@ class NotificationService extends events_1.EventEmitter {
313
324
  }
314
325
  return bazinga64_1.Decoder.fromBase64(groupId).asBytes;
315
326
  }
327
+ /**
328
+ * ## MLS only ##
329
+ * If there are pending proposals, we need to either process them,
330
+ * or save them in the database for later processing
331
+ *
332
+ * @param groupId groupId of the mls conversation
333
+ * @param delayInMs delay in ms before processing proposals
334
+ * @param eventTime time of the event that had the proposals
335
+ */
336
+ async handlePendingProposals({ delayInMs, groupId, eventTime }) {
337
+ if (delayInMs > 0) {
338
+ const eventDate = new Date(eventTime);
339
+ const firingDate = eventDate.setTime(eventDate.getTime() + delayInMs);
340
+ try {
341
+ await this.database.storePendingProposal({
342
+ groupId,
343
+ firingDate,
344
+ });
345
+ }
346
+ catch (error) {
347
+ this.logger.error('Could not store pending proposal', error);
348
+ }
349
+ finally {
350
+ TaskScheduler_1.TaskScheduler.addTask({
351
+ task: () => this.commitPendingProposals({ groupId }),
352
+ firingDate,
353
+ key: groupId,
354
+ });
355
+ }
356
+ }
357
+ else {
358
+ await this.commitPendingProposals({ groupId, skipDelete: true });
359
+ }
360
+ }
361
+ /**
362
+ * ## MLS only ##
363
+ * Commit all pending proposals for a given groupId
364
+ *
365
+ * @param groupId groupId of the conversation
366
+ * @param skipDelete if true, do not delete the pending proposals from the database
367
+ */
368
+ async commitPendingProposals({ groupId, skipDelete = false }) {
369
+ const coreCryptoClient = this.coreCryptoClientProvider();
370
+ if (!coreCryptoClient) {
371
+ throw new Error('Could not get coreCryptoClient');
372
+ }
373
+ try {
374
+ await coreCryptoClient.commitPendingProposals(bazinga64_1.Decoder.fromBase64(groupId).asBytes);
375
+ if (!skipDelete) {
376
+ TaskScheduler_1.TaskScheduler.cancelTask(groupId);
377
+ await this.database.deletePendingProposal({ groupId });
378
+ }
379
+ }
380
+ catch (error) {
381
+ this.logger.error(`Error while committing pending proposals for groupId ${groupId}`, error);
382
+ }
383
+ }
384
+ /**
385
+ * ## MLS only ##
386
+ * Get all pending proposals from the database and schedule them
387
+ * Function must only be called once, after application start
388
+ *
389
+ */
390
+ async checkExistingPendingProposals() {
391
+ try {
392
+ const pendingProposals = await this.database.getStoredPendingProposals();
393
+ if (pendingProposals.length > 0) {
394
+ pendingProposals.forEach(({ groupId, firingDate }) => TaskScheduler_1.TaskScheduler.addTask({
395
+ task: () => this.commitPendingProposals({ groupId }),
396
+ firingDate,
397
+ key: groupId,
398
+ }));
399
+ }
400
+ }
401
+ catch (error) {
402
+ this.logger.error('Could not get pending proposals', error);
403
+ }
404
+ }
316
405
  }
317
406
  exports.NotificationService = NotificationService;
318
407
  NotificationService.TOPIC = TOPIC;
@@ -0,0 +1,17 @@
1
+ export declare type CommonMLS = {
2
+ groupId: string;
3
+ };
4
+ export declare type CompoundGroupIdParams = {
5
+ conversationId: string;
6
+ conversationDomain: string;
7
+ } & CommonMLS;
8
+ export declare type HandlePendingProposalsParams = {
9
+ delayInMs: number;
10
+ eventTime: string;
11
+ } & CommonMLS;
12
+ export declare type CommitPendingProposalsParams = {
13
+ skipDelete?: boolean;
14
+ } & CommonMLS;
15
+ export declare type StorePendingProposalsParams = {
16
+ firingDate: number;
17
+ } & CommonMLS;
@@ -0,0 +1,21 @@
1
+ "use strict";
2
+ /*
3
+ * Wire
4
+ * Copyright (C) 2022 Wire Swiss GmbH
5
+ *
6
+ * This program is free software: you can redistribute it and/or modify
7
+ * it under the terms of the GNU General Public License as published by
8
+ * the Free Software Foundation, either version 3 of the License, or
9
+ * (at your option) any later version.
10
+ *
11
+ * This program is distributed in the hope that it will be useful,
12
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
+ * GNU General Public License for more details.
15
+ *
16
+ * You should have received a copy of the GNU General Public License
17
+ * along with this program. If not, see http://www.gnu.org/licenses/.
18
+ *
19
+ */
20
+ Object.defineProperty(exports, "__esModule", { value: true });
21
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1,10 @@
1
+ declare type ScheduleTaskParams = {
2
+ task: () => Promise<void>;
3
+ firingDate: number;
4
+ key: string;
5
+ };
6
+ export declare const TaskScheduler: {
7
+ addTask: ({ task, firingDate, key }: ScheduleTaskParams) => void;
8
+ cancelTask: (key: string) => void;
9
+ };
10
+ export {};
@@ -0,0 +1,70 @@
1
+ "use strict";
2
+ /*
3
+ * Wire
4
+ * Copyright (C) 2022 Wire Swiss GmbH
5
+ *
6
+ * This program is free software: you can redistribute it and/or modify
7
+ * it under the terms of the GNU General Public License as published by
8
+ * the Free Software Foundation, either version 3 of the License, or
9
+ * (at your option) any later version.
10
+ *
11
+ * This program is distributed in the hope that it will be useful,
12
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
+ * GNU General Public License for more details.
15
+ *
16
+ * You should have received a copy of the GNU General Public License
17
+ * along with this program. If not, see http://www.gnu.org/licenses/.
18
+ *
19
+ */
20
+ var __importDefault = (this && this.__importDefault) || function (mod) {
21
+ return (mod && mod.__esModule) ? mod : { "default": mod };
22
+ };
23
+ Object.defineProperty(exports, "__esModule", { value: true });
24
+ exports.TaskScheduler = void 0;
25
+ const logdown_1 = __importDefault(require("logdown"));
26
+ const logger = (0, logdown_1.default)('@wireapp/core/util/TaskScheduler/TaskScheduler', {
27
+ logger: console,
28
+ markdown: false,
29
+ });
30
+ const activeTimeouts = {};
31
+ /**
32
+ * Execute a task at a given time.
33
+ *
34
+ * @param task function to be executed
35
+ * @param firingDate execution date
36
+ * @param key unique key for the task
37
+ */
38
+ const addTask = ({ task, firingDate, key }) => {
39
+ const now = new Date();
40
+ const execute = new Date(firingDate);
41
+ const delay = execute.getTime() - now.getTime();
42
+ if (activeTimeouts[key]) {
43
+ cancelTask(key);
44
+ }
45
+ const timeout = setTimeout(async () => {
46
+ await task();
47
+ delete activeTimeouts[key];
48
+ }, delay > 0 ? delay : 0);
49
+ // add the task to the list of active tasks
50
+ activeTimeouts[key] = timeout;
51
+ logger.info(`New scheduled task to be executed at "${execute}" with key "${key}"`);
52
+ };
53
+ /**
54
+ * Cancel a scheduled task early
55
+ *
56
+ * @param key unique key for the task
57
+ */
58
+ const cancelTask = (key) => {
59
+ const timeout = activeTimeouts[key];
60
+ if (timeout) {
61
+ clearTimeout(timeout);
62
+ delete activeTimeouts[key];
63
+ logger.info(`Scheduled task with key "${key}" prematurely cleared`);
64
+ }
65
+ };
66
+ exports.TaskScheduler = {
67
+ addTask,
68
+ cancelTask,
69
+ };
70
+ //# sourceMappingURL=TaskScheduler.js.map