@wireapp/core 27.0.5 → 27.2.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.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,36 @@
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
+ ## [27.2.1](https://github.com/wireapp/wire-web-packages/tree/main/packages/core/compare/@wireapp/core@27.2.0...@wireapp/core@27.2.1) (2022-05-31)
7
+
8
+ **Note:** Version bump only for package @wireapp/core
9
+
10
+
11
+
12
+
13
+
14
+ # [27.2.0](https://github.com/wireapp/wire-web-packages/tree/main/packages/core/compare/@wireapp/core@27.1.0...@wireapp/core@27.2.0) (2022-05-30)
15
+
16
+
17
+ ### Features
18
+
19
+ * Add missed notifications hook ([#4275](https://github.com/wireapp/wire-web-packages/tree/main/packages/core/issues/4275)) ([f81217b](https://github.com/wireapp/wire-web-packages/tree/main/packages/core/commit/f81217bad53fed2e4fb163c6ae871b39ca16cf58))
20
+
21
+
22
+
23
+
24
+
25
+ # [27.1.0](https://github.com/wireapp/wire-web-packages/tree/main/packages/core/compare/@wireapp/core@27.0.5...@wireapp/core@27.1.0) (2022-05-30)
26
+
27
+
28
+ ### Features
29
+
30
+ * Improve websocket connection API ([#4268](https://github.com/wireapp/wire-web-packages/tree/main/packages/core/issues/4268)) ([a6eb884](https://github.com/wireapp/wire-web-packages/tree/main/packages/core/commit/a6eb884ddf42915157c8d6c846b0a8a989c3fc04))
31
+
32
+
33
+
34
+
35
+
6
36
  ## [27.0.5](https://github.com/wireapp/wire-web-packages/tree/main/packages/core/compare/@wireapp/core@27.0.4...@wireapp/core@27.0.5) (2022-05-30)
7
37
 
8
38
  **Note:** Version bump only for package @wireapp/core
package/package.json CHANGED
@@ -5,7 +5,7 @@
5
5
  "dependencies": {
6
6
  "@types/long": "4.0.1",
7
7
  "@types/node": "~14",
8
- "@wireapp/api-client": "19.2.5",
8
+ "@wireapp/api-client": "19.3.1",
9
9
  "@wireapp/cryptobox": "12.8.0",
10
10
  "bazinga64": "5.10.0",
11
11
  "hash.js": "1.1.7",
@@ -69,6 +69,6 @@
69
69
  "test:project": "yarn dist && yarn test",
70
70
  "test:node": "nyc jasmine --config=jasmine.json"
71
71
  },
72
- "version": "27.0.5",
73
- "gitHead": "4a9e90c8c5c46e4d3cc3f24ee83d2a5ed1673aef"
72
+ "version": "27.2.1",
73
+ "gitHead": "97d45ca6fbd2c9f72871882b3cd0b92b26d49cf3"
74
74
  }
@@ -9,18 +9,20 @@ import { EventEmitter } from 'events';
9
9
  import { BroadcastService } from './broadcast/';
10
10
  import { ClientInfo, ClientService } from './client/';
11
11
  import { ConnectionService } from './connection/';
12
- import { AssetService, ConversationService, PayloadBundleType } from './conversation/';
12
+ import { AssetService, ConversationService, PayloadBundleSource, PayloadBundleType } from './conversation/';
13
13
  import * as OtrMessage from './conversation/message/OtrMessage';
14
14
  import * as UserMessage from './conversation/message/UserMessage';
15
15
  import type { CoreError } from './CoreError';
16
16
  import { CryptographyService } from './cryptography/';
17
17
  import { GiphyService } from './giphy/';
18
- import { NotificationHandler, NotificationService } from './notification/';
18
+ import { HandledEventPayload, NotificationService } from './notification/';
19
19
  import { SelfService } from './self/';
20
20
  import { TeamService } from './team/';
21
21
  import { UserService } from './user/';
22
22
  import { AccountService } from './account/';
23
23
  import { LinkPreviewService } from './linkPreview';
24
+ import { WEBSOCKET_STATE } from '@wireapp/api-client/src/tcp/ReconnectingWebsocket';
25
+ export declare type ProcessedEventPayload = HandledEventPayload;
24
26
  declare enum TOPIC {
25
27
  ERROR = "Account.TOPIC.ERROR"
26
28
  }
@@ -141,9 +143,44 @@ export declare class Account extends EventEmitter {
141
143
  private registerClient;
142
144
  private resetContext;
143
145
  logout(): Promise<void>;
144
- listen(notificationHandler?: NotificationHandler): Promise<Account>;
145
- private readonly handlePayload;
146
- private readonly handleError;
146
+ /**
147
+ * Will download and handle the notification stream since last stored notification id.
148
+ * Once the notification stream has been handled from backend, will then connect to the websocket and start listening to incoming events
149
+ *
150
+ * @param callbacks callbacks that will be called to handle different events
151
+ * @returns close a function that will disconnect from the websocket
152
+ */
153
+ listen({ onEvent, onConnected, onConnectionStateChanged, onNotificationStreamProgress, onMissedNotifications, }?: {
154
+ /**
155
+ * Called when a new event arrives from backend
156
+ * @param payload the payload of the event. Contains the raw event received and the decrypted data (if event was encrypted)
157
+ * @param source where the message comes from (either websocket or notification stream)
158
+ */
159
+ onEvent?: (payload: HandledEventPayload, source: PayloadBundleSource) => void;
160
+ /**
161
+ * During the notification stream processing, this function will be called whenever a new notification has been processed
162
+ */
163
+ onNotificationStreamProgress?: ({ done, total }: {
164
+ done: number;
165
+ total: number;
166
+ }) => void;
167
+ /**
168
+ * called when the connection to the websocket is established and the notification stream has been processed
169
+ */
170
+ onConnected?: () => void;
171
+ /**
172
+ * called when the connection stateh with the backend has changed
173
+ */
174
+ onConnectionStateChanged?: (state: WEBSOCKET_STATE) => void;
175
+ /**
176
+ * called when we detect lost notification from backend.
177
+ * When a client doesn't log in for a while (28 days, as of now) notifications that are older than 28 days will be deleted from backend.
178
+ * If the client query the backend for the notifications since a particular notification ID and this ID doesn't exist anymore on the backend, we deduce that some messages were not sync before they were removed from backend.
179
+ * We can then detect that something was wrong and warn the consumer that there might be some missing old messages
180
+ * @param {string} notificationId
181
+ */
182
+ onMissedNotifications?: (notificationId: string) => void;
183
+ }): Promise<() => void>;
147
184
  private initEngine;
148
185
  }
149
186
  export {};
@@ -36,6 +36,13 @@ var __importStar = (this && this.__importStar) || function (mod) {
36
36
  __setModuleDefault(result, mod);
37
37
  return result;
38
38
  };
39
+ var __asyncValues = (this && this.__asyncValues) || function (o) {
40
+ if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined.");
41
+ var m = o[Symbol.asyncIterator], i;
42
+ return m ? m.call(o) : (o = typeof __values === "function" ? __values(o) : o[Symbol.iterator](), i = {}, verb("next"), verb("throw"), verb("return"), i[Symbol.asyncIterator] = function () { return this; }, i);
43
+ function verb(n) { i[n] = o[n] && function (v) { return new Promise(function (resolve, reject) { v = o[n](v), settle(resolve, reject, v.done, v.value); }); }; }
44
+ function settle(resolve, reject, d, v) { Promise.resolve(v).then(function(v) { resolve({ value: v, done: d }); }, reject); }
45
+ };
39
46
  var __importDefault = (this && this.__importDefault) || function (mod) {
40
47
  return (mod && mod.__esModule) ? mod : { "default": mod };
41
48
  };
@@ -79,20 +86,6 @@ class Account extends events_1.EventEmitter {
79
86
  */
80
87
  constructor(apiClient = new api_client_1.APIClient(), { createStore = () => undefined, nbPrekeys = 2 } = {}) {
81
88
  super();
82
- this.handlePayload = async (payload) => {
83
- switch (payload.type) {
84
- case conversation_1.PayloadBundleType.TIMER_UPDATE: {
85
- const { data: { message_timer }, conversation, } = payload;
86
- const expireAfterMillis = Number(message_timer);
87
- this.service.conversation.messageTimer.setConversationLevelTimer(conversation, expireAfterMillis);
88
- break;
89
- }
90
- }
91
- this.emit(payload.type, payload);
92
- };
93
- this.handleError = (accountError) => {
94
- this.emit(Account.TOPIC.ERROR, accountError);
95
- };
96
89
  this.apiClient = apiClient;
97
90
  this.backendFeatures = this.apiClient.backendFeatures;
98
91
  this.nbPrekeys = nbPrekeys;
@@ -284,25 +277,68 @@ class Account extends events_1.EventEmitter {
284
277
  await this.apiClient.logout();
285
278
  this.resetContext();
286
279
  }
287
- async listen(notificationHandler = this.service.notification.handleNotification) {
280
+ /**
281
+ * Will download and handle the notification stream since last stored notification id.
282
+ * Once the notification stream has been handled from backend, will then connect to the websocket and start listening to incoming events
283
+ *
284
+ * @param callbacks callbacks that will be called to handle different events
285
+ * @returns close a function that will disconnect from the websocket
286
+ */
287
+ async listen({ onEvent = () => { }, onConnected = () => { }, onConnectionStateChanged = () => { }, onNotificationStreamProgress = () => { }, onMissedNotifications = () => { }, } = {}) {
288
288
  if (!this.apiClient.context) {
289
289
  throw new Error('Context is not set - please login first');
290
290
  }
291
- this.apiClient.transport.ws.removeAllListeners(tcp_1.WebSocketClient.TOPIC.ON_MESSAGE);
292
- this.apiClient.transport.ws.on(tcp_1.WebSocketClient.TOPIC.ON_MESSAGE, notification => {
293
- notificationHandler(notification, conversation_1.PayloadBundleSource.WEBSOCKET).catch(error => {
291
+ const handleEvent = async (payload, source) => {
292
+ const { mappedEvent } = payload;
293
+ switch (mappedEvent === null || mappedEvent === void 0 ? void 0 : mappedEvent.type) {
294
+ case conversation_1.PayloadBundleType.TIMER_UPDATE: {
295
+ const { data: { message_timer }, conversation, } = payload.event;
296
+ const expireAfterMillis = Number(message_timer);
297
+ this.service.conversation.messageTimer.setConversationLevelTimer(conversation, expireAfterMillis);
298
+ break;
299
+ }
300
+ }
301
+ onEvent(payload, source);
302
+ if (mappedEvent) {
303
+ this.emit(mappedEvent.type, payload.mappedEvent);
304
+ }
305
+ };
306
+ const handleNotification = async (notification, source) => {
307
+ var e_1, _a;
308
+ try {
309
+ const messages = this.service.notification.handleNotification(notification, conversation_1.PayloadBundleSource.WEBSOCKET);
310
+ try {
311
+ for (var messages_1 = __asyncValues(messages), messages_1_1; messages_1_1 = await messages_1.next(), !messages_1_1.done;) {
312
+ const message = messages_1_1.value;
313
+ await handleEvent(message, source);
314
+ }
315
+ }
316
+ catch (e_1_1) { e_1 = { error: e_1_1 }; }
317
+ finally {
318
+ try {
319
+ if (messages_1_1 && !messages_1_1.done && (_a = messages_1.return)) await _a.call(messages_1);
320
+ }
321
+ finally { if (e_1) throw e_1.error; }
322
+ }
323
+ }
324
+ catch (error) {
294
325
  this.logger.error(`Failed to handle notification ID "${notification.id}": ${error.message}`, error);
295
- });
296
- });
297
- this.service.notification.removeAllListeners(notification_1.NotificationService.TOPIC.NOTIFICATION_ERROR);
298
- this.service.notification.on(notification_1.NotificationService.TOPIC.NOTIFICATION_ERROR, this.handleError);
299
- for (const payloadType of Object.values(conversation_1.PayloadBundleType)) {
300
- this.service.notification.removeAllListeners(payloadType);
301
- this.service.notification.on(payloadType, this.handlePayload);
302
- }
303
- const onBeforeConnect = async () => this.service.notification.handleNotificationStream(notificationHandler);
326
+ }
327
+ };
328
+ this.apiClient.transport.ws.removeAllListeners(tcp_1.WebSocketClient.TOPIC.ON_MESSAGE);
329
+ this.apiClient.transport.ws.on(tcp_1.WebSocketClient.TOPIC.ON_MESSAGE, notification => handleNotification(notification, conversation_1.PayloadBundleSource.WEBSOCKET));
330
+ this.apiClient.transport.ws.on(tcp_1.WebSocketClient.TOPIC.ON_STATE_CHANGE, onConnectionStateChanged);
331
+ const onBeforeConnect = async () => {
332
+ await this.service.notification.handleNotificationStream(async (notification, source, progress) => {
333
+ await handleNotification(notification, source);
334
+ onNotificationStreamProgress(progress);
335
+ }, onMissedNotifications);
336
+ onConnected();
337
+ };
304
338
  await this.apiClient.connect(onBeforeConnect);
305
- return this;
339
+ return () => {
340
+ this.apiClient.disconnect();
341
+ };
306
342
  }
307
343
  async initEngine(context) {
308
344
  const clientType = context.clientType === client_1.ClientType.NONE ? '' : `@${context.clientType}`;
@@ -5,8 +5,13 @@ import type { OTRRecipients, QualifiedOTRRecipients, QualifiedUserClients, UserC
5
5
  import type { ConversationOtrMessageAddEvent } from '@wireapp/api-client/src/event';
6
6
  import type { QualifiedId, QualifiedUserPreKeyBundleMap, UserPreKeyBundleMap } from '@wireapp/api-client/src/user/';
7
7
  import { Cryptobox } from '@wireapp/cryptobox';
8
+ import { GenericMessage } from '@wireapp/protocol-messaging';
8
9
  import type { CRUDEngine } from '@wireapp/store-engine';
9
10
  import { PayloadBundle, PayloadBundleSource } from '../conversation';
11
+ export declare type DecryptionError = {
12
+ code: number;
13
+ message: string;
14
+ };
10
15
  export interface MetaClient extends RegisteredClient {
11
16
  meta: {
12
17
  is_verified?: boolean;
@@ -41,5 +46,7 @@ export declare class CryptographyService {
41
46
  initCryptobox(): Promise<void>;
42
47
  deleteCryptographyStores(): Promise<boolean[]>;
43
48
  resetSession(sessionId: string): Promise<void>;
44
- decodeGenericMessage(otrMessage: ConversationOtrMessageAddEvent, source: PayloadBundleSource): Promise<PayloadBundle>;
49
+ decryptMessage(otrMessage: ConversationOtrMessageAddEvent): Promise<GenericMessage>;
50
+ mapGenericMessage(otrMessage: ConversationOtrMessageAddEvent, genericMessage: GenericMessage, source: PayloadBundleSource): PayloadBundle;
51
+ private generateDecryptionError;
45
52
  }
@@ -151,11 +151,18 @@ class CryptographyService {
151
151
  await this.cryptobox.session_delete(sessionId);
152
152
  this.logger.log(`Deleted session ID "${sessionId}".`);
153
153
  }
154
- async decodeGenericMessage(otrMessage, source) {
154
+ async decryptMessage(otrMessage) {
155
155
  const { from, qualified_from, data: { sender, text: cipherText }, } = otrMessage;
156
156
  const sessionId = this.constructSessionId(from, sender, qualified_from === null || qualified_from === void 0 ? void 0 : qualified_from.domain);
157
- const decryptedMessage = await this.decrypt(sessionId, cipherText);
158
- const genericMessage = protocol_messaging_1.GenericMessage.decode(decryptedMessage);
157
+ try {
158
+ const decryptedMessage = await this.decrypt(sessionId, cipherText);
159
+ return protocol_messaging_1.GenericMessage.decode(decryptedMessage);
160
+ }
161
+ catch (error) {
162
+ throw this.generateDecryptionError(otrMessage, error);
163
+ }
164
+ }
165
+ mapGenericMessage(otrMessage, genericMessage, source) {
159
166
  if (genericMessage.content === conversation_1.GenericMessageType.EPHEMERAL) {
160
167
  const unwrappedMessage = GenericMessageMapper_1.GenericMessageMapper.mapGenericMessage(genericMessage.ephemeral, otrMessage, source);
161
168
  unwrappedMessage.id = genericMessage.messageId;
@@ -168,6 +175,31 @@ class CryptographyService {
168
175
  }
169
176
  return GenericMessageMapper_1.GenericMessageMapper.mapGenericMessage(genericMessage, otrMessage, source);
170
177
  }
178
+ generateDecryptionError(event, error) {
179
+ var _a;
180
+ const errorCode = (_a = error.code) !== null && _a !== void 0 ? _a : 999;
181
+ let message = 'Unknown decryption error';
182
+ const { data: eventData, from: remoteUserId, time: formattedTime } = event;
183
+ const remoteClientId = eventData.sender;
184
+ const isDuplicateMessage = error instanceof proteus_1.errors.DecryptError.DuplicateMessage;
185
+ const isOutdatedMessage = error instanceof proteus_1.errors.DecryptError.OutdatedMessage;
186
+ // We don't need to show these message errors to the user
187
+ if (isDuplicateMessage || isOutdatedMessage) {
188
+ message = `Message from user ID "${remoteUserId}" at "${formattedTime}" will not be handled because it is outdated or a duplicate.`;
189
+ }
190
+ const isInvalidMessage = error instanceof proteus_1.errors.DecryptError.InvalidMessage;
191
+ const isInvalidSignature = error instanceof proteus_1.errors.DecryptError.InvalidSignature;
192
+ const isRemoteIdentityChanged = error instanceof proteus_1.errors.DecryptError.RemoteIdentityChanged;
193
+ // Session is broken, let's see what's really causing it...
194
+ if (isInvalidMessage || isInvalidSignature) {
195
+ message = `Session with user '${remoteUserId}' (${remoteClientId}) is broken.\nReset the session for possible fix.`;
196
+ }
197
+ else if (isRemoteIdentityChanged) {
198
+ message = `Remote identity of client '${remoteClientId}' from user '${remoteUserId}' changed`;
199
+ }
200
+ this.logger.warn(`Failed to decrypt event from client '${remoteClientId}' of user '${remoteUserId}' (${formattedTime}).\nError Code: '${errorCode}'\nError Message: ${error.message}`, error);
201
+ return { code: errorCode, message };
202
+ }
171
203
  }
172
204
  exports.CryptographyService = CryptographyService;
173
205
  //# sourceMappingURL=CryptographyService.js.map
@@ -3,6 +3,9 @@ import type { Notification } from '@wireapp/api-client/src/notification/';
3
3
  export declare class NotificationBackendRepository {
4
4
  private readonly apiClient;
5
5
  constructor(apiClient: APIClient);
6
- getAllNotifications(clientId?: string, lastNotificationId?: string): Promise<Notification[]>;
6
+ getAllNotifications(clientId?: string, lastNotificationId?: string): Promise<{
7
+ notifications: Notification[];
8
+ missedNotification?: string | undefined;
9
+ }>;
7
10
  getLastNotification(clientId?: string): Promise<Notification>;
8
11
  }
@@ -4,41 +4,27 @@ import * as Events from '@wireapp/api-client/src/event';
4
4
  import type { Notification } from '@wireapp/api-client/src/notification/';
5
5
  import { CRUDEngine } from '@wireapp/store-engine';
6
6
  import { EventEmitter } from 'events';
7
- import { PayloadBundleSource, PayloadBundleType } from '../conversation';
8
- import * as OtrMessage from '../conversation/message/OtrMessage';
9
- import * as UserMessage from '../conversation/message/UserMessage';
7
+ import { PayloadBundle, PayloadBundleSource } from '../conversation';
10
8
  import { NotificationError } from '../CoreError';
11
9
  import type { CryptographyService } from '../cryptography';
10
+ import { GenericMessage } from '@wireapp/protocol-messaging';
11
+ export declare type HandledEventPayload = {
12
+ event: Events.BackendEvent;
13
+ mappedEvent?: PayloadBundle;
14
+ decryptedData?: GenericMessage;
15
+ decryptionError?: {
16
+ code: number;
17
+ message: string;
18
+ };
19
+ };
12
20
  declare enum TOPIC {
13
21
  NOTIFICATION_ERROR = "NotificationService.TOPIC.NOTIFICATION_ERROR"
14
22
  }
15
- export declare type NotificationHandler = (notification: Notification, source: PayloadBundleSource) => Promise<void>;
23
+ export declare type NotificationHandler = (notification: Notification, source: PayloadBundleSource, progress: {
24
+ done: number;
25
+ total: number;
26
+ }) => Promise<void>;
16
27
  export interface NotificationService {
17
- on(event: PayloadBundleType.ASSET, listener: (payload: OtrMessage.FileAssetMessage) => void): this;
18
- on(event: PayloadBundleType.BUTTON_ACTION, listener: (payload: OtrMessage.ButtonActionMessage) => void): this;
19
- on(event: PayloadBundleType.ASSET_ABORT, listener: (payload: OtrMessage.FileAssetAbortMessage) => void): this;
20
- on(event: PayloadBundleType.ASSET_IMAGE, listener: (payload: OtrMessage.ImageAssetMessage) => void): this;
21
- on(event: PayloadBundleType.ASSET_META, listener: (payload: OtrMessage.FileAssetMetaDataMessage) => void): this;
22
- on(event: PayloadBundleType.CALL, listener: (payload: OtrMessage.CallMessage) => void): this;
23
- on(event: PayloadBundleType.CLIENT_ACTION, listener: (payload: OtrMessage.ResetSessionMessage) => void): this;
24
- on(event: PayloadBundleType.CLIENT_ADD, listener: (payload: UserMessage.UserClientAddMessage) => void): this;
25
- on(event: PayloadBundleType.CLIENT_REMOVE, listener: (payload: UserMessage.UserClientRemoveMessage) => void): this;
26
- on(event: PayloadBundleType.CONFIRMATION, listener: (payload: OtrMessage.ConfirmationMessage) => void): this;
27
- on(event: PayloadBundleType.CONNECTION_REQUEST, listener: (payload: UserMessage.UserConnectionMessage) => void): this;
28
- on(event: PayloadBundleType.USER_UPDATE, listener: (payload: UserMessage.UserUpdateMessage) => void): this;
29
- on(event: PayloadBundleType.CONVERSATION_CLEAR, listener: (payload: OtrMessage.ClearConversationMessage) => void): this;
30
- on(event: PayloadBundleType.CONVERSATION_RENAME, listener: (payload: Events.ConversationRenameEvent) => void): this;
31
- on(event: PayloadBundleType.LOCATION, listener: (payload: OtrMessage.LocationMessage) => void): this;
32
- on(event: PayloadBundleType.MEMBER_JOIN, listener: (payload: Events.TeamMemberJoinEvent) => void): this;
33
- on(event: PayloadBundleType.MESSAGE_DELETE, listener: (payload: OtrMessage.DeleteMessage) => void): this;
34
- on(event: PayloadBundleType.MESSAGE_EDIT, listener: (payload: OtrMessage.EditedTextMessage) => void): this;
35
- on(event: PayloadBundleType.MESSAGE_HIDE, listener: (payload: OtrMessage.HideMessage) => void): this;
36
- on(event: PayloadBundleType.PING, listener: (payload: OtrMessage.PingMessage) => void): this;
37
- on(event: PayloadBundleType.REACTION, listener: (payload: OtrMessage.ReactionMessage) => void): this;
38
- on(event: PayloadBundleType.TEXT, listener: (payload: OtrMessage.TextMessage) => void): this;
39
- on(event: PayloadBundleType.TIMER_UPDATE, listener: (payload: Events.ConversationMessageTimerUpdateEvent) => void): this;
40
- on(event: PayloadBundleType.TYPING, listener: (payload: Events.ConversationTypingEvent) => void): this;
41
- on(event: PayloadBundleType.UNKNOWN, listener: (payload: any) => void): this;
42
28
  on(event: TOPIC.NOTIFICATION_ERROR, listener: (payload: NotificationError) => void): this;
43
29
  }
44
30
  export declare class NotificationService extends EventEmitter {
@@ -49,15 +35,16 @@ export declare class NotificationService extends EventEmitter {
49
35
  private readonly logger;
50
36
  static readonly TOPIC: typeof TOPIC;
51
37
  constructor(apiClient: APIClient, cryptographyService: CryptographyService, storeEngine: CRUDEngine);
52
- getAllNotifications(): Promise<Notification[]>;
38
+ private getAllNotifications;
53
39
  /** Should only be called with a completely new client. */
54
40
  initializeNotificationStream(): Promise<string>;
55
41
  hasHistory(): Promise<boolean>;
56
42
  getNotificationEventList(): Promise<Events.BackendEvent[]>;
57
43
  setLastEventDate(eventDate: Date): Promise<Date>;
58
44
  setLastNotificationId(lastNotification: Notification): Promise<string>;
59
- handleNotificationStream(notificationHandler: NotificationHandler): Promise<void>;
60
- readonly handleNotification: NotificationHandler;
45
+ handleNotificationStream(notificationHandler: NotificationHandler, onMissedNotifications: (notificationId: string) => void): Promise<void>;
46
+ handleNotification(notification: Notification, source: PayloadBundleSource): AsyncGenerator<HandledEventPayload>;
47
+ private cleanupPayloadBundle;
61
48
  private handleEvent;
62
49
  }
63
50
  export {};
@@ -36,6 +36,18 @@ var __importStar = (this && this.__importStar) || function (mod) {
36
36
  __setModuleDefault(result, mod);
37
37
  return result;
38
38
  };
39
+ var __await = (this && this.__await) || function (v) { return this instanceof __await ? (this.v = v, this) : new __await(v); }
40
+ var __asyncGenerator = (this && this.__asyncGenerator) || function (thisArg, _arguments, generator) {
41
+ if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined.");
42
+ var g = generator.apply(thisArg, _arguments || []), i, q = [];
43
+ return i = {}, verb("next"), verb("throw"), verb("return"), i[Symbol.asyncIterator] = function () { return this; }, i;
44
+ function verb(n) { if (g[n]) i[n] = function (v) { return new Promise(function (a, b) { q.push([n, v, a, b]) > 1 || resume(n, v); }); }; }
45
+ function resume(n, v) { try { step(g[n](v)); } catch (e) { settle(q[0][3], e); } }
46
+ function step(r) { r.value instanceof __await ? Promise.resolve(r.value.v).then(fulfill, reject) : settle(q[0][2], r); }
47
+ function fulfill(value) { resume("next", value); }
48
+ function reject(value) { resume("throw", value); }
49
+ function settle(f, v) { if (f(v), q.shift(), q.length) resume(q[0][0], q[0][1]); }
50
+ };
39
51
  var __importDefault = (this && this.__importDefault) || function (mod) {
40
52
  return (mod && mod.__esModule) ? mod : { "default": mod };
41
53
  };
@@ -62,80 +74,6 @@ class NotificationService extends events_1.EventEmitter {
62
74
  logger: console,
63
75
  markdown: false,
64
76
  });
65
- this.handleNotification = async (notification, source) => {
66
- for (const event of notification.payload) {
67
- let data;
68
- try {
69
- this.logger.log(`Handling event of type "${event.type}" for notification with ID "${notification.id}"`, event);
70
- data = await this.handleEvent(event, source);
71
- if (!notification.transient) {
72
- await this.setLastNotificationId(notification);
73
- }
74
- }
75
- catch (error) {
76
- this.logger.error(`There was an error with notification ID "${notification.id}": ${error.message}`, error);
77
- const notificationError = {
78
- error: error,
79
- notification,
80
- type: CoreError_1.CoreError.NOTIFICATION_ERROR,
81
- };
82
- this.emit(NotificationService.TOPIC.NOTIFICATION_ERROR, notificationError);
83
- continue;
84
- }
85
- if (data) {
86
- switch (data.type) {
87
- case conversation_1.PayloadBundleType.ASSET_IMAGE:
88
- case conversation_1.PayloadBundleType.BUTTON_ACTION:
89
- case conversation_1.PayloadBundleType.CALL:
90
- case conversation_1.PayloadBundleType.CLIENT_ACTION:
91
- case conversation_1.PayloadBundleType.CLIENT_ADD:
92
- case conversation_1.PayloadBundleType.CLIENT_REMOVE:
93
- case conversation_1.PayloadBundleType.CONFIRMATION:
94
- case conversation_1.PayloadBundleType.CONNECTION_REQUEST:
95
- case conversation_1.PayloadBundleType.LOCATION:
96
- case conversation_1.PayloadBundleType.MESSAGE_DELETE:
97
- case conversation_1.PayloadBundleType.MESSAGE_EDIT:
98
- case conversation_1.PayloadBundleType.MESSAGE_HIDE:
99
- case conversation_1.PayloadBundleType.PING:
100
- case conversation_1.PayloadBundleType.REACTION:
101
- case conversation_1.PayloadBundleType.TEXT:
102
- case conversation_1.PayloadBundleType.USER_UPDATE:
103
- this.emit(data.type, data);
104
- break;
105
- case conversation_1.PayloadBundleType.ASSET: {
106
- const assetContent = data.content;
107
- const isMetaData = !!assetContent && !!assetContent.original && !assetContent.uploaded;
108
- const isAbort = !!assetContent.abortReason || (!assetContent.original && !assetContent.uploaded);
109
- if (isMetaData) {
110
- data.type = conversation_1.PayloadBundleType.ASSET_META;
111
- this.emit(conversation_1.PayloadBundleType.ASSET_META, data);
112
- }
113
- else if (isAbort) {
114
- data.type = conversation_1.PayloadBundleType.ASSET_ABORT;
115
- this.emit(conversation_1.PayloadBundleType.ASSET_ABORT, data);
116
- }
117
- else {
118
- this.emit(conversation_1.PayloadBundleType.ASSET, data);
119
- }
120
- break;
121
- }
122
- case conversation_1.PayloadBundleType.TIMER_UPDATE:
123
- case conversation_1.PayloadBundleType.CONVERSATION_RENAME:
124
- case conversation_1.PayloadBundleType.CONVERSATION_CLEAR:
125
- case conversation_1.PayloadBundleType.MEMBER_JOIN:
126
- case conversation_1.PayloadBundleType.TYPING:
127
- this.emit(data.type, event);
128
- break;
129
- }
130
- }
131
- else {
132
- const { type, conversation, from } = event;
133
- const conversationText = conversation ? ` in conversation "${conversation}"` : '';
134
- const fromText = from ? ` from user "${from}".` : '';
135
- this.logger.log(`Received unsupported event "${type}"${conversationText}${fromText}`, { event });
136
- }
137
- }
138
- };
139
77
  this.apiClient = apiClient;
140
78
  this.cryptographyService = cryptographyService;
141
79
  this.backend = new NotificationBackendRepository_1.NotificationBackendRepository(this.apiClient);
@@ -180,17 +118,79 @@ class NotificationService extends events_1.EventEmitter {
180
118
  async setLastNotificationId(lastNotification) {
181
119
  return this.database.updateLastNotificationId(lastNotification);
182
120
  }
183
- async handleNotificationStream(notificationHandler) {
184
- const notifications = await this.getAllNotifications();
185
- for (const notification of notifications) {
186
- await notificationHandler(notification, conversation_1.PayloadBundleSource.NOTIFICATION_STREAM).catch(error => this.logger.error(error));
121
+ async handleNotificationStream(notificationHandler, onMissedNotifications) {
122
+ const { notifications, missedNotification } = await this.getAllNotifications();
123
+ if (missedNotification) {
124
+ onMissedNotifications(missedNotification);
125
+ }
126
+ for (const [index, notification] of notifications.entries()) {
127
+ await notificationHandler(notification, conversation_1.PayloadBundleSource.NOTIFICATION_STREAM, {
128
+ done: index + 1,
129
+ total: notifications.length,
130
+ }).catch(error => this.logger.error(error));
131
+ }
132
+ }
133
+ handleNotification(notification, source) {
134
+ return __asyncGenerator(this, arguments, function* handleNotification_1() {
135
+ for (const event of notification.payload) {
136
+ this.logger.log(`Handling event of type "${event.type}" for notification with ID "${notification.id}"`, event);
137
+ try {
138
+ const data = yield __await(this.handleEvent(event, source));
139
+ if (!notification.transient) {
140
+ // keep track of the last handled notification for next time we fetch the notification stream
141
+ yield __await(this.setLastNotificationId(notification));
142
+ }
143
+ yield yield __await(Object.assign(Object.assign({}, data), { mappedEvent: data.mappedEvent ? this.cleanupPayloadBundle(data.mappedEvent) : undefined }));
144
+ }
145
+ catch (error) {
146
+ this.logger.error(`There was an error with notification ID "${notification.id}": ${error.message}`, error);
147
+ const notificationError = {
148
+ error: error,
149
+ notification,
150
+ type: CoreError_1.CoreError.NOTIFICATION_ERROR,
151
+ };
152
+ this.emit(NotificationService.TOPIC.NOTIFICATION_ERROR, notificationError);
153
+ }
154
+ }
155
+ if (!notification.transient) {
156
+ // keep track of the last handled notification for next time we fetch the notification stream
157
+ yield __await(this.setLastNotificationId(notification));
158
+ }
159
+ });
160
+ }
161
+ cleanupPayloadBundle(payload) {
162
+ switch (payload.type) {
163
+ case conversation_1.PayloadBundleType.ASSET: {
164
+ const assetContent = payload.content;
165
+ const isMetaData = !!(assetContent === null || assetContent === void 0 ? void 0 : assetContent.original) && !(assetContent === null || assetContent === void 0 ? void 0 : assetContent.uploaded);
166
+ const isAbort = !!assetContent.abortReason || (!assetContent.original && !assetContent.uploaded);
167
+ if (isMetaData) {
168
+ payload.type = conversation_1.PayloadBundleType.ASSET_META;
169
+ }
170
+ else if (isAbort) {
171
+ payload.type = conversation_1.PayloadBundleType.ASSET_ABORT;
172
+ }
173
+ return payload;
174
+ }
175
+ default:
176
+ return payload;
187
177
  }
188
178
  }
189
179
  async handleEvent(event, source) {
190
180
  switch (event.type) {
191
181
  // Encrypted events
192
182
  case Events.CONVERSATION_EVENT.OTR_MESSAGE_ADD: {
193
- return this.cryptographyService.decodeGenericMessage(event, source);
183
+ try {
184
+ const decryptedData = await this.cryptographyService.decryptMessage(event);
185
+ return {
186
+ mappedEvent: this.cryptographyService.mapGenericMessage(event, decryptedData, source),
187
+ event,
188
+ decryptedData,
189
+ };
190
+ }
191
+ catch (error) {
192
+ return { event, decryptionError: error };
193
+ }
194
194
  }
195
195
  // Meta events
196
196
  case Events.CONVERSATION_EVENT.MEMBER_JOIN:
@@ -199,16 +199,17 @@ class NotificationService extends events_1.EventEmitter {
199
199
  case Events.CONVERSATION_EVENT.TYPING: {
200
200
  const { conversation, from } = event;
201
201
  const metaEvent = Object.assign(Object.assign({}, event), { conversation, from });
202
- return ConversationMapper_1.ConversationMapper.mapConversationEvent(metaEvent, source);
202
+ return { mappedEvent: ConversationMapper_1.ConversationMapper.mapConversationEvent(metaEvent, source), event };
203
203
  }
204
204
  // User events
205
205
  case Events.USER_EVENT.CONNECTION:
206
206
  case Events.USER_EVENT.CLIENT_ADD:
207
207
  case Events.USER_EVENT.UPDATE:
208
208
  case Events.USER_EVENT.CLIENT_REMOVE: {
209
- return UserMapper_1.UserMapper.mapUserEvent(event, this.apiClient.context.userId, source);
209
+ return { mappedEvent: UserMapper_1.UserMapper.mapUserEvent(event, this.apiClient.context.userId, source), event };
210
210
  }
211
211
  }
212
+ return { event };
212
213
  }
213
214
  }
214
215
  exports.NotificationService = NotificationService;
@@ -1,5 +1,5 @@
1
1
  import { UserEvent } from '@wireapp/api-client/src/event';
2
2
  import { PayloadBundle, PayloadBundleSource } from '../conversation';
3
3
  export declare class UserMapper {
4
- static mapUserEvent(event: UserEvent, selfUserId: string, source: PayloadBundleSource): PayloadBundle | void;
4
+ static mapUserEvent(event: UserEvent, selfUserId: string, source: PayloadBundleSource): PayloadBundle | undefined;
5
5
  }
@@ -81,6 +81,7 @@ class UserMapper {
81
81
  };
82
82
  }
83
83
  }
84
+ return undefined;
84
85
  }
85
86
  }
86
87
  exports.UserMapper = UserMapper;