Page MenuHomePhabricator

No OneTemporary

diff --git a/lib/push/send-hooks.react.js b/lib/push/send-hooks.react.js
index 8dcc66d9e..7e5e73cd4 100644
--- a/lib/push/send-hooks.react.js
+++ b/lib/push/send-hooks.react.js
@@ -1,67 +1,199 @@
// @flow
import * as React from 'react';
+import uuid from 'uuid';
import {
preparePushNotifs,
type PerUserTargetedNotifications,
} from './send-utils.js';
import { ENSCacheContext } from '../components/ens-cache-provider.react.js';
import { NeynarClientContext } from '../components/neynar-client-provider.react.js';
import { usePeerOlmSessionsCreatorContext } from '../components/peer-olm-session-creator-provider.react.js';
import { thickRawThreadInfosSelector } from '../selectors/thread-selectors.js';
-import type { MessageData } from '../types/message-types.js';
+import { useTunnelbroker } from '../tunnelbroker/tunnelbroker-context.js';
import type {
- EncryptedNotifUtilsAPI,
- SenderDeviceDescriptor,
+ TargetedAPNsNotification,
+ TargetedAndroidNotification,
+ TargetedWebNotification,
+ TargetedWNSNotification,
+ NotificationsCreationData,
} from '../types/notif-types.js';
+import { deviceToTunnelbrokerMessageTypes } from '../types/tunnelbroker/messages.js';
+import type {
+ TunnelbrokerAPNsNotif,
+ TunnelbrokerFCMNotif,
+ TunnelbrokerWebPushNotif,
+ TunnelbrokerWNSNotif,
+} from '../types/tunnelbroker/notif-types.js';
+import { getConfig } from '../utils/config.js';
+import { getContentSigningKey } from '../utils/crypto-utils.js';
+import { getMessageForException } from '../utils/errors.js';
import { useSelector } from '../utils/redux-utils.js';
-function usePreparePushNotifs(): (
- encryptedNotifsUtilsAPI: EncryptedNotifUtilsAPI,
- senderDeviceDescriptor: SenderDeviceDescriptor,
- messageDatas: $ReadOnlyArray<MessageData>,
+function apnsNotifToTunnelbrokerAPNsNotif(
+ targetedNotification: TargetedAPNsNotification,
+): TunnelbrokerAPNsNotif {
+ const {
+ deliveryID: deviceID,
+ notification: { headers, ...payload },
+ } = targetedNotification;
+
+ const newHeaders = {
+ ...headers,
+ 'apns-push-type': 'Alert',
+ };
+
+ return {
+ type: deviceToTunnelbrokerMessageTypes.TUNNELBROKER_APNS_NOTIF,
+ deviceID,
+ headers: JSON.stringify(newHeaders),
+ payload: JSON.stringify(payload),
+ clientMessageID: uuid.v4(),
+ };
+}
+
+function androidNotifToTunnelbrokerFCMNotif(
+ targetedNotification: TargetedAndroidNotification,
+): TunnelbrokerFCMNotif {
+ const {
+ deliveryID: deviceID,
+ notification: { data },
+ priority,
+ } = targetedNotification;
+
+ return {
+ type: deviceToTunnelbrokerMessageTypes.TUNNELBROKER_FCM_NOTIF,
+ deviceID,
+ clientMessageID: uuid.v4(),
+ data: JSON.stringify(data),
+ priority: priority === 'normal' ? 'NORMAL' : 'HIGH',
+ };
+}
+
+function webNotifToTunnelbrokerWebPushNotif(
+ targetedNotification: TargetedWebNotification,
+): TunnelbrokerWebPushNotif {
+ const { deliveryID: deviceID, notification } = targetedNotification;
+ return {
+ type: deviceToTunnelbrokerMessageTypes.TUNNELBROKER_WEB_PUSH_NOTIF,
+ deviceID,
+ clientMessageID: uuid.v4(),
+ payload: JSON.stringify(notification),
+ };
+}
+
+function wnsNotifToTunnelbrokerWNSNofif(
+ targetedNotification: TargetedWNSNotification,
+): TunnelbrokerWNSNotif {
+ const { deliveryID: deviceID, notification } = targetedNotification;
+ return {
+ type: deviceToTunnelbrokerMessageTypes.TUNNELBROKER_WNS_NOTIF,
+ deviceID,
+ clientMessageID: uuid.v4(),
+ payload: JSON.stringify(notification),
+ };
+}
+
+function useSendPushNotifs(): (
+ notifCreationData: NotificationsCreationData,
) => Promise<?PerUserTargetedNotifications> {
const rawMessageInfos = useSelector(state => state.messageStore.messages);
const thickRawThreadInfos = useSelector(thickRawThreadInfosSelector);
const auxUserInfos = useSelector(state => state.auxUserStore.auxUserInfos);
const userInfos = useSelector(state => state.userStore.userInfos);
-
const { getENSNames } = React.useContext(ENSCacheContext);
const getFCNames = React.useContext(NeynarClientContext)?.getFCNames;
-
const { createOlmSessionsWithPeer: olmSessionCreator } =
usePeerOlmSessionsCreatorContext();
+ const { sendNotif } = useTunnelbroker();
+ const { encryptedNotifUtilsAPI } = getConfig();
return React.useCallback(
- (
- encryptedNotifUtilsAPI: EncryptedNotifUtilsAPI,
- senderDeviceDescriptor: SenderDeviceDescriptor,
- messageDatas: $ReadOnlyArray<MessageData>,
- ) => {
- return preparePushNotifs({
+ async (notifCreationData: NotificationsCreationData) => {
+ const deviceID = await getContentSigningKey();
+ const senderDeviceDescriptor = { senderDeviceID: deviceID };
+ const { messageDatas } = notifCreationData;
+
+ const pushNotifsPreparationInput = {
encryptedNotifUtilsAPI,
senderDeviceDescriptor,
olmSessionCreator,
messageInfos: rawMessageInfos,
thickRawThreadInfos,
auxUserInfos,
messageDatas,
userInfos,
getENSNames,
getFCNames,
- });
+ };
+
+ const preparedPushNotifs = await preparePushNotifs(
+ pushNotifsPreparationInput,
+ );
+
+ if (!preparedPushNotifs) {
+ return;
+ }
+
+ const sendPromises = [];
+ for (const userID in preparedPushNotifs) {
+ for (const notif of preparedPushNotifs[userID]) {
+ if (notif.targetedNotification.notification.encryptionFailed) {
+ continue;
+ }
+
+ let tunnelbrokerNotif;
+ if (notif.platform === 'ios' || notif.platform === 'macos') {
+ tunnelbrokerNotif = apnsNotifToTunnelbrokerAPNsNotif(
+ notif.targetedNotification,
+ );
+ } else if (notif.platform === 'android') {
+ tunnelbrokerNotif = androidNotifToTunnelbrokerFCMNotif(
+ notif.targetedNotification,
+ );
+ } else if (notif.platform === 'web') {
+ tunnelbrokerNotif = webNotifToTunnelbrokerWebPushNotif(
+ notif.targetedNotification,
+ );
+ } else if (notif.platform === 'windows') {
+ tunnelbrokerNotif = wnsNotifToTunnelbrokerWNSNofif(
+ notif.targetedNotification,
+ );
+ } else {
+ continue;
+ }
+
+ sendPromises.push(
+ (async () => {
+ try {
+ await sendNotif(tunnelbrokerNotif);
+ } catch (e) {
+ console.log(
+ `Failed to send notification to device: ${
+ tunnelbrokerNotif.deviceID
+ }. Details: ${getMessageForException(e) ?? ''}`,
+ );
+ }
+ })(),
+ );
+ }
+ }
+
+ await Promise.all(sendPromises);
},
[
+ sendNotif,
+ encryptedNotifUtilsAPI,
olmSessionCreator,
rawMessageInfos,
thickRawThreadInfos,
auxUserInfos,
userInfos,
getENSNames,
getFCNames,
],
);
}
-export { usePreparePushNotifs };
+export { useSendPushNotifs };
diff --git a/lib/types/notif-types.js b/lib/types/notif-types.js
index 29401b86c..68c439fde 100644
--- a/lib/types/notif-types.js
+++ b/lib/types/notif-types.js
@@ -1,411 +1,415 @@
// @flow
import type { EncryptResult } from '@commapp/olm';
import t, { type TInterface, type TUnion } from 'tcomb';
-import type { Platform } from './device-types.js';
+import type { MessageData } from './message-types.js';
import type { EntityText, ThreadEntity } from '../utils/entity-text.js';
import { tShape } from '../utils/validation-utils.js';
export type NotifTexts = {
+merged: string | EntityText,
+body: string | EntityText,
+title: string | ThreadEntity,
+prefix?: string | EntityText,
};
export type ResolvedNotifTexts = {
+merged: string,
+body: string,
+title: string,
+prefix?: string,
};
export const resolvedNotifTextsValidator: TInterface<ResolvedNotifTexts> =
tShape<ResolvedNotifTexts>({
merged: t.String,
body: t.String,
title: t.String,
prefix: t.maybe(t.String),
});
+export type NotificationsCreationData = {
+ +messageDatas: $ReadOnlyArray<MessageData>,
+};
+
export type SenderDeviceDescriptor =
| { +keyserverID: string }
| { +senderDeviceID: string };
export const senderDeviceDescriptorValidator: TUnion<SenderDeviceDescriptor> =
t.union([
tShape({ keyserverID: t.String }),
tShape({ senderDeviceID: t.String }),
]);
// Web notifs types
export type PlainTextWebNotificationPayload = {
+body: string,
+prefix?: string,
+title: string,
+unreadCount?: number,
+threadID: string,
+encryptionFailed?: '1',
};
export type PlainTextWebNotification = $ReadOnly<{
+id: string,
...PlainTextWebNotificationPayload,
}>;
export type EncryptedWebNotification = $ReadOnly<{
...SenderDeviceDescriptor,
+id: string,
+encryptedPayload: string,
+type: '0' | '1',
}>;
export type WebNotification =
| PlainTextWebNotification
| EncryptedWebNotification;
// WNS notifs types
export type PlainTextWNSNotification = {
+body: string,
+prefix?: string,
+title: string,
+unreadCount?: number,
+threadID: string,
+encryptionFailed?: '1',
};
export type EncryptedWNSNotification = $ReadOnly<{
...SenderDeviceDescriptor,
+encryptedPayload: string,
+type: '0' | '1',
}>;
export type WNSNotification =
| PlainTextWNSNotification
| EncryptedWNSNotification;
// Android notifs types
export type AndroidVisualNotificationPayloadBase = $ReadOnly<{
+badge?: string,
+body: string,
+title: string,
+prefix?: string,
+threadID: string,
+collapseID?: string,
+badgeOnly?: '0' | '1',
+encryptionFailed?: '1',
}>;
type AndroidSmallVisualNotificationPayload = $ReadOnly<{
...AndroidVisualNotificationPayloadBase,
+messageInfos?: string,
}>;
type AndroidLargeVisualNotificationPayload = $ReadOnly<{
...AndroidVisualNotificationPayloadBase,
+blobHash: string,
+encryptionKey: string,
}>;
export type AndroidVisualNotificationPayload =
| AndroidSmallVisualNotificationPayload
| AndroidLargeVisualNotificationPayload;
type EncryptedThinThreadPayload = {
+keyserverID: string,
+encryptedPayload: string,
+type: '0' | '1',
};
type EncryptedThickThreadPayload = {
+senderDeviceID: string,
+encryptedPayload: string,
+type: '0' | '1',
};
export type AndroidVisualNotification = {
+data: $ReadOnly<{
+id?: string,
...
| AndroidSmallVisualNotificationPayload
| AndroidLargeVisualNotificationPayload
| EncryptedThinThreadPayload
| EncryptedThickThreadPayload,
}>,
};
type AndroidThinThreadRescindPayload = {
+badge: string,
+rescind: 'true',
+rescindID?: string,
+setUnreadStatus: 'true',
+threadID: string,
+encryptionFailed?: string,
};
type AndroidThickThreadRescindPayload = {
+rescind: 'true',
+setUnreadStatus: 'true',
+threadID: string,
+encryptionFailed?: string,
};
export type AndroidNotificationRescind = {
+data:
| AndroidThinThreadRescindPayload
| AndroidThickThreadRescindPayload
| EncryptedThinThreadPayload
| EncryptedThickThreadPayload,
};
type AndroidKeyserverBadgeOnlyPayload = {
+badge: string,
+badgeOnly: '1',
+encryptionFailed?: string,
};
type AndroidThickThreadBadgeOnlyPayload = {
+threadID: string,
+badgeOnly: '1',
+encryptionFailed?: string,
};
export type AndroidBadgeOnlyNotification = {
+data:
| AndroidKeyserverBadgeOnlyPayload
| AndroidThickThreadBadgeOnlyPayload
| EncryptedThinThreadPayload
| EncryptedThickThreadPayload,
};
type AndroidNotificationWithPriority =
| {
+notification: AndroidVisualNotification,
+priority: 'high',
}
| {
+notification: AndroidBadgeOnlyNotification | AndroidNotificationRescind,
+priority: 'normal',
};
// APNs notifs types
export type APNsNotificationTopic =
| 'app.comm.macos'
| 'app.comm'
| 'org.squadcal.app';
export type APNsNotificationHeaders = {
+'apns-priority'?: 1 | 5 | 10,
+'apns-id'?: string,
+'apns-expiration'?: number,
+'apns-topic': APNsNotificationTopic,
+'apns-collapse-id'?: string,
+'apns-push-type': 'background' | 'alert' | 'voip',
};
type EncryptedAPNsSilentNotificationsAps = {
+'mutable-content': number,
+'alert'?: { body: 'ENCRYPTED' },
};
export type EncryptedAPNsSilentNotification = $ReadOnly<{
...SenderDeviceDescriptor,
+headers: APNsNotificationHeaders,
+encryptedPayload: string,
+type: '1' | '0',
+aps: EncryptedAPNsSilentNotificationsAps,
}>;
type EncryptedAPNsVisualNotificationAps = $ReadOnly<{
...EncryptedAPNsSilentNotificationsAps,
+sound?: string,
}>;
export type EncryptedAPNsVisualNotification = $ReadOnly<{
...EncryptedAPNsSilentNotification,
+aps: EncryptedAPNsVisualNotificationAps,
+id: string,
}>;
type APNsVisualNotificationPayloadBase = {
+aps: {
+'badge'?: string | number,
+'alert'?: string | { +body?: string, ... },
+'thread-id': string,
+'mutable-content'?: number,
+'sound'?: string,
},
+body: string,
+title: string,
+prefix?: string,
+threadID: string,
+collapseID?: string,
+encryptionFailed?: '1',
};
type APNsSmallVisualNotificationPayload = $ReadOnly<{
...APNsVisualNotificationPayloadBase,
+messageInfos?: string,
}>;
type APNsLargeVisualNotificationPayload = $ReadOnly<{
...APNsVisualNotificationPayloadBase,
+blobHash: string,
+encryptionKey: string,
}>;
export type APNsVisualNotification =
| $ReadOnly<{
+headers: APNsNotificationHeaders,
+id: string,
...
| APNsSmallVisualNotificationPayload
| APNsLargeVisualNotificationPayload,
}>
| EncryptedAPNsVisualNotification;
type APNsLegacyRescindPayload = {
+backgroundNotifType: 'CLEAR',
+notificationId: string,
+setUnreadStatus: true,
+threadID: string,
+aps: {
+'badge': string | number,
+'content-available': number,
},
};
type APNsKeyserverRescindPayload = {
+backgroundNotifType: 'CLEAR',
+notificationId: string,
+setUnreadStatus: true,
+threadID: string,
+aps: {
+'badge': string | number,
+'mutable-content': number,
},
};
type APNsThickThreadRescindPayload = {
+backgroundNotifType: 'CLEAR',
+setUnreadStatus: true,
+threadID: string,
+aps: {
+'mutable-content': number,
},
};
export type APNsNotificationRescind =
| $ReadOnly<{
+headers: APNsNotificationHeaders,
+encryptionFailed?: '1',
...
| APNsLegacyRescindPayload
| APNsKeyserverRescindPayload
| APNsThickThreadRescindPayload,
}>
| EncryptedAPNsSilentNotification;
type APNsLegacyBadgeOnlyNotification = {
+aps: {
+badge: string | number,
},
};
type APNsKeyserverBadgeOnlyNotification = {
+aps: {
+'badge': string | number,
+'mutable-content': number,
},
};
type APNsThickThreadBadgeOnlyNotification = {
+aps: {
+'mutable-content': number,
},
+threadID: string,
};
export type APNsBadgeOnlyNotification =
| $ReadOnly<{
+headers: APNsNotificationHeaders,
+encryptionFailed?: '1',
...
| APNsLegacyBadgeOnlyNotification
| APNsKeyserverBadgeOnlyNotification
| APNsThickThreadBadgeOnlyNotification,
}>
| EncryptedAPNsSilentNotification;
export type APNsNotification =
| APNsVisualNotification
| APNsNotificationRescind
| APNsBadgeOnlyNotification;
export type TargetedAPNsNotification = {
+notification: APNsNotification,
+deliveryID: string,
+encryptedPayloadHash?: string,
+encryptionOrder?: number,
};
export type TargetedAndroidNotification = $ReadOnly<{
...AndroidNotificationWithPriority,
+deliveryID: string,
+encryptionOrder?: number,
}>;
export type TargetedWebNotification = {
+notification: WebNotification,
+deliveryID: string,
+encryptionOrder?: number,
};
export type TargetedWNSNotification = {
+notification: WNSNotification,
+deliveryID: string,
+encryptionOrder?: number,
};
export type NotificationTargetDevice = {
+cryptoID: string,
+deliveryID: string,
+blobHolder?: string,
};
-export type TargetedNotificationWithPlatform = {
- +platform: Platform,
- +targetedNotification:
- | TargetedAPNsNotification
- | TargetedWNSNotification
- | TargetedWebNotification
- | TargetedAndroidNotification,
-};
+export type TargetedNotificationWithPlatform =
+ | {
+ +platform: 'ios' | 'macos',
+ +targetedNotification: TargetedAPNsNotification,
+ }
+ | { +platform: 'android', +targetedNotification: TargetedAndroidNotification }
+ | { +platform: 'web', +targetedNotification: TargetedWebNotification }
+ | { +platform: 'windows', +targetedNotification: TargetedWNSNotification };
export type EncryptedNotifUtilsAPI = {
+encryptSerializedNotifPayload: (
cryptoID: string,
unencryptedPayload: string,
encryptedPayloadSizeValidator?: (
encryptedPayload: string,
type: '1' | '0',
) => boolean,
) => Promise<{
+encryptedData: EncryptResult,
+sizeLimitViolated?: boolean,
+encryptionOrder?: number,
}>,
+uploadLargeNotifPayload: (
payload: string,
numberOfHolders: number,
) => Promise<
| {
+blobHolders: $ReadOnlyArray<string>,
+blobHash: string,
+encryptionKey: string,
}
| { +blobUploadError: string },
>,
+getNotifByteSize: (serializedNotification: string) => number,
+getEncryptedNotifHash: (serializedNotification: string) => Promise<string>,
};
diff --git a/lib/types/tunnelbroker/messages.js b/lib/types/tunnelbroker/messages.js
index 03f6a7ce1..3a40903c0 100644
--- a/lib/types/tunnelbroker/messages.js
+++ b/lib/types/tunnelbroker/messages.js
@@ -1,92 +1,94 @@
// @flow
import type { TUnion } from 'tcomb';
import t from 'tcomb';
import {
messageToDeviceRequestStatusValidator,
type DeviceToTunnelbrokerRequestStatus,
} from './device-to-tunnelbroker-request-status-types.js';
import { type MessageReceiveConfirmation } from './message-receive-confirmation-types.js';
import { type MessageToDeviceRequest } from './message-to-device-request-types.js';
import {
type MessageToDevice,
messageToDeviceValidator,
} from './message-to-device-types.js';
import { type MessageToTunnelbrokerRequest } from './message-to-tunnelbroker-request-types.js';
import { type TunnelbrokerNotif } from './notif-types.js';
import {
type AnonymousInitializationMessage,
type ConnectionInitializationMessage,
} from './session-types.js';
import {
type ConnectionInitializationResponse,
connectionInitializationResponseValidator,
} from '../websocket/connection-initialization-response-types.js';
import {
type Heartbeat,
heartbeatValidator,
} from '../websocket/heartbeat-types.js';
/*
* This file defines types and validation for messages exchanged
* with the Tunnelbroker. The definitions in this file should remain in sync
* with the structures defined in the corresponding
* Rust file at `shared/tunnelbroker_messages/src/messages/mod.rs`.
*
* If you edit the definitions in one file,
* please make sure to update the corresponding definitions in the other.
*
*/
// Messages sent from Device to Tunnelbroker.
export const deviceToTunnelbrokerMessageTypes = Object.freeze({
CONNECTION_INITIALIZATION_MESSAGE: 'ConnectionInitializationMessage',
ANONYMOUS_INITIALIZATION_MESSAGE: 'AnonymousInitializationMessage',
TUNNELBROKER_APNS_NOTIF: 'APNsNotif',
TUNNELBROKER_FCM_NOTIF: 'FCMNotif',
+ TUNNELBROKER_WEB_PUSH_NOTIF: 'WebPushNotif',
+ TUNNELBROKER_WNS_NOTIF: 'WNSNotif',
MESSAGE_TO_DEVICE_REQUEST: 'MessageToDeviceRequest',
MESSAGE_RECEIVE_CONFIRMATION: 'MessageReceiveConfirmation',
MESSAGE_TO_TUNNELBROKER_REQUEST: 'MessageToTunnelbrokerRequest',
HEARTBEAT: 'Heartbeat',
});
export type DeviceToTunnelbrokerMessage =
| ConnectionInitializationMessage
| AnonymousInitializationMessage
| TunnelbrokerNotif
| MessageToDeviceRequest
| MessageReceiveConfirmation
| MessageToTunnelbrokerRequest
| Heartbeat;
// Types having `clientMessageID` prop.
// When using this type, it is possible to use Promise abstraction,
// and await sending a message until Tunnelbroker responds that
// the request was processed.
export type DeviceToTunnelbrokerRequest =
| TunnelbrokerNotif
| MessageToDeviceRequest
| MessageToTunnelbrokerRequest;
// Messages sent from Tunnelbroker to Device.
export const tunnelbrokerToDeviceMessageTypes = Object.freeze({
CONNECTION_INITIALIZATION_RESPONSE: 'ConnectionInitializationResponse',
DEVICE_TO_TUNNELBROKER_REQUEST_STATUS: 'MessageToDeviceRequestStatus',
MESSAGE_TO_DEVICE: 'MessageToDevice',
HEARTBEAT: 'Heartbeat',
});
export type TunnelbrokerToDeviceMessage =
| ConnectionInitializationResponse
| DeviceToTunnelbrokerRequestStatus
| MessageToDevice
| Heartbeat;
export const tunnelbrokerToDeviceMessageValidator: TUnion<TunnelbrokerToDeviceMessage> =
t.union([
connectionInitializationResponseValidator,
messageToDeviceRequestStatusValidator,
messageToDeviceValidator,
heartbeatValidator,
]);
diff --git a/lib/utils/__mocks__/config.js b/lib/utils/__mocks__/config.js
index e1f64946d..2b7382b3c 100644
--- a/lib/utils/__mocks__/config.js
+++ b/lib/utils/__mocks__/config.js
@@ -1,53 +1,59 @@
// @flow
import { type Config } from '../config.js';
const getConfig = (): Config => ({
resolveKeyserverSessionInvalidationUsingNativeCredentials: null,
setSessionIDOnRequest: true,
calendarRangeInactivityLimit: null,
platformDetails: {
platform: 'web',
codeVersion: 70,
stateVersion: 50,
},
authoritativeKeyserverID: '123',
olmAPI: {
initializeCryptoAccount: jest.fn(),
getUserPublicKey: jest.fn(),
encrypt: jest.fn(),
encryptAndPersist: jest.fn(),
encryptNotification: jest.fn(),
decrypt: jest.fn(),
decryptAndPersist: jest.fn(),
contentInboundSessionCreator: jest.fn(),
contentOutboundSessionCreator: jest.fn(),
keyserverNotificationsSessionCreator: jest.fn(),
notificationsOutboundSessionCreator: jest.fn(),
isContentSessionInitialized: jest.fn(),
isDeviceNotificationsSessionInitialized: jest.fn(),
isNotificationsSessionInitializedWithDevices: jest.fn(),
getOneTimeKeys: jest.fn(),
validateAndUploadPrekeys: jest.fn(),
signMessage: jest.fn(),
verifyMessage: jest.fn(),
markPrekeysAsPublished: jest.fn(),
},
sqliteAPI: {
getAllInboundP2PMessages: jest.fn(),
removeInboundP2PMessages: jest.fn(),
processDBStoreOperations: jest.fn(),
getAllOutboundP2PMessages: jest.fn(),
markOutboundP2PMessageAsSent: jest.fn(),
removeOutboundP2PMessage: jest.fn(),
resetOutboundP2PMessagesForDevice: jest.fn(),
getRelatedMessages: jest.fn(),
getOutboundP2PMessagesByID: jest.fn(),
searchMessages: jest.fn(),
fetchMessages: jest.fn(),
},
+ encryptedNotifUtilsAPI: {
+ encryptSerializedNotifPayload: jest.fn(),
+ uploadLargeNotifPayload: jest.fn(),
+ getEncryptedNotifHash: jest.fn(),
+ getNotifByteSize: jest.fn(),
+ },
});
const hasConfig = (): boolean => true;
export { getConfig, hasConfig };
diff --git a/lib/utils/config.js b/lib/utils/config.js
index 3e0ca0d60..c2405fb06 100644
--- a/lib/utils/config.js
+++ b/lib/utils/config.js
@@ -1,49 +1,51 @@
// @flow
import invariant from 'invariant';
import type { CallSingleKeyserverEndpoint } from '../keyserver-conn/call-single-keyserver-endpoint.js';
import type { CallKeyserverEndpoint } from '../keyserver-conn/keyserver-conn-types.js';
import type { InitialNotifMessageOptions } from '../shared/crypto-utils.js';
import type { RecoveryActionSource } from '../types/account-types.js';
import type { OlmAPI } from '../types/crypto-types.js';
import type { PlatformDetails } from '../types/device-types.js';
+import type { EncryptedNotifUtilsAPI } from '../types/notif-types.js';
import type { SQLiteAPI } from '../types/sqlite-types.js';
import type { DispatchActionPromise } from '../utils/redux-promise-utils.js';
export type Config = {
+resolveKeyserverSessionInvalidationUsingNativeCredentials: ?(
callSingleKeyserverEndpoint: CallSingleKeyserverEndpoint,
callKeyserverEndpoint: CallKeyserverEndpoint,
dispatchActionPromise: DispatchActionPromise,
recoveryActionSource: RecoveryActionSource,
keyserverID: string,
getInitialNotificationsEncryptedMessage: (
options?: ?InitialNotifMessageOptions,
) => Promise<string>,
hasBeenCancelled: () => boolean,
) => Promise<void>,
+setSessionIDOnRequest: boolean,
+calendarRangeInactivityLimit: ?number,
+platformDetails: PlatformDetails,
+authoritativeKeyserverID: string,
+olmAPI: OlmAPI,
+sqliteAPI: SQLiteAPI,
+ +encryptedNotifUtilsAPI: EncryptedNotifUtilsAPI,
};
let registeredConfig: ?Config = null;
const registerConfig = (config: Config) => {
registeredConfig = { ...registeredConfig, ...config };
};
const getConfig = (): Config => {
invariant(registeredConfig, 'config should be set');
return registeredConfig;
};
const hasConfig = (): boolean => {
return !!registeredConfig;
};
export { registerConfig, getConfig, hasConfig };
diff --git a/native/config.js b/native/config.js
index 53c5fb32b..61b2867b3 100644
--- a/native/config.js
+++ b/native/config.js
@@ -1,25 +1,27 @@
// @flow
import { Platform } from 'react-native';
import { registerConfig } from 'lib/utils/config.js';
import { resolveKeyserverSessionInvalidationUsingNativeCredentials } from './account/legacy-recover-keyserver-session.js';
import { authoritativeKeyserverID } from './authoritative-keyserver.js';
import { olmAPI } from './crypto/olm-api.js';
import { sqliteAPI } from './database/sqlite-api.js';
+import encryptedNotifUtilsAPI from './push/encrypted-notif-utils-api.js';
import { persistConfig, codeVersion } from './redux/persist.js';
registerConfig({
resolveKeyserverSessionInvalidationUsingNativeCredentials,
setSessionIDOnRequest: false,
calendarRangeInactivityLimit: 15 * 60 * 1000,
platformDetails: {
platform: Platform.OS,
codeVersion,
stateVersion: persistConfig.version,
},
authoritativeKeyserverID,
olmAPI,
sqliteAPI,
+ encryptedNotifUtilsAPI,
});
diff --git a/web/app.react.js b/web/app.react.js
index bb04df58e..d2f56bfe3 100644
--- a/web/app.react.js
+++ b/web/app.react.js
@@ -1,597 +1,599 @@
// @flow
import 'basscss/css/basscss.min.css';
import './theme.css';
import { config as faConfig } from '@fortawesome/fontawesome-svg-core';
import classnames from 'classnames';
import _isEqual from 'lodash/fp/isEqual.js';
import * as React from 'react';
import { DndProvider } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';
import {
fetchEntriesActionTypes,
updateCalendarQueryActionTypes,
} from 'lib/actions/entry-actions.js';
import { ChatMentionContextProvider } from 'lib/components/chat-mention-provider.react.js';
import { EditUserAvatarProvider } from 'lib/components/edit-user-avatar-provider.react.js';
import { FarcasterDataHandler } from 'lib/components/farcaster-data-handler.react.js';
import {
ModalProvider,
useModalContext,
} from 'lib/components/modal-provider.react.js';
import { NeynarClientProvider } from 'lib/components/neynar-client-provider.react.js';
import PlatformDetailsSynchronizer from 'lib/components/platform-details-synchronizer.react.js';
import { QRAuthProvider } from 'lib/components/qr-auth-provider.react.js';
import { StaffContextProvider } from 'lib/components/staff-provider.react.js';
import { DBOpsHandler } from 'lib/handlers/db-ops-handler.react.js';
import { TunnelbrokerDeviceTokenHandler } from 'lib/handlers/tunnelbroker-device-token-handler.react.js';
import { UserInfosHandler } from 'lib/handlers/user-infos-handler.react.js';
import { IdentitySearchProvider } from 'lib/identity-search/identity-search-context.js';
import {
createLoadingStatusSelector,
combineLoadingStatuses,
} from 'lib/selectors/loading-selectors.js';
import { isLoggedIn } from 'lib/selectors/user-selectors.js';
import { extractMajorDesktopVersion } from 'lib/shared/version-utils.js';
import type { SecondaryTunnelbrokerConnection } from 'lib/tunnelbroker/secondary-tunnelbroker-connection.js';
import { TunnelbrokerProvider } from 'lib/tunnelbroker/tunnelbroker-context.js';
import type { LoadingStatus } from 'lib/types/loading-types.js';
import type { WebNavInfo } from 'lib/types/nav-types.js';
import type { Dispatch } from 'lib/types/redux-types.js';
import type { MessageToDeviceRequest } from 'lib/types/tunnelbroker/message-to-device-request-types.js';
import { getConfig, registerConfig } from 'lib/utils/config.js';
import { useDispatch } from 'lib/utils/redux-utils.js';
import { infoFromURL } from 'lib/utils/url-utils.js';
import { AlchemyENSCacheProvider } from 'lib/utils/wagmi-utils.js';
import QRCodeLogin from './account/qr-code-login.react.js';
import AppThemeWrapper from './app-theme-wrapper.react.js';
import { authoritativeKeyserverID } from './authoritative-keyserver.js';
import WebEditThreadAvatarProvider from './avatars/web-edit-thread-avatar-provider.react.js';
import Calendar from './calendar/calendar.react.js';
import Chat from './chat/chat.react.js';
import { EditModalProvider } from './chat/edit-message-provider.js';
import { MemberListSidebarProvider } from './chat/member-list-sidebar/member-list-sidebar-provider.react.js';
import { AutoJoinCommunityHandler } from './components/auto-join-community-handler.react.js';
import CommunitiesRefresher from './components/communities-refresher.react.js';
import LogOutIfMissingCSATHandler from './components/log-out-if-missing-csat-handler.react.js';
import NavigationArrows from './components/navigation-arrows.react.js';
import MinVersionHandler from './components/version-handler.react.js';
import { olmAPI } from './crypto/olm-api.js';
import { sqliteAPI } from './database/sqlite-api.js';
import electron from './electron.js';
import InputStateContainer from './input/input-state-container.react.js';
import InviteLinkHandler from './invite-links/invite-link-handler.react.js';
import InviteLinksRefresher from './invite-links/invite-links-refresher.react.js';
import LoadingIndicator from './loading-indicator.react.js';
import { MenuProvider } from './menu-provider.react.js';
import UpdateModalHandler from './modals/update-modal.react.js';
import SettingsSwitcher from './navigation-panels/settings-switcher.react.js';
import Topbar from './navigation-panels/topbar.react.js';
import useBadgeHandler from './push-notif/badge-handler.react.js';
+import encryptedNotifUtilsAPI from './push-notif/encrypted-notif-utils-api.js';
import { PushNotificationsHandler } from './push-notif/push-notifs-handler.js';
import { updateNavInfoActionType } from './redux/action-types.js';
import DisconnectedBar from './redux/disconnected-bar.js';
import FocusHandler from './redux/focus-handler.react.js';
import { KeyserverReachabilityHandler } from './redux/keyserver-reachability-handler.js';
import { persistConfig } from './redux/persist.js';
import PolicyAcknowledgmentHandler from './redux/policy-acknowledgment-handler.js';
import { useSelector } from './redux/redux-utils.js';
import VisibilityHandler from './redux/visibility-handler.react.js';
import history from './router-history.js';
import { MessageSearchStateProvider } from './search/message-search-state-provider.react.js';
import AccountSettings from './settings/account-settings.react.js';
import DangerZone from './settings/danger-zone.react.js';
import KeyserverSelectionList from './settings/keyserver-selection-list.react.js';
import { getCommSharedWorker } from './shared-worker/shared-worker-provider.js';
import CommunityPicker from './sidebar/community-picker.react.js';
import Splash from './splash/splash.react.js';
import './typography.css';
import css from './style.css';
import { TooltipProvider } from './tooltips/tooltip-provider.js';
import { canonicalURLFromReduxState, navInfoFromURL } from './url-utils.js';
import {
composeTunnelbrokerQRAuthMessage,
generateQRAuthAESKey,
parseTunnelbrokerQRAuthMessage,
useHandleSecondaryDeviceLogInError,
} from './utils/qr-code-utils.js';
import { useWebLock, TUNNELBROKER_LOCK_NAME } from './web-lock.js';
// We want Webpack's css-loader and style-loader to handle the Fontawesome CSS,
// so we disable the autoAddCss logic and import the CSS file. Otherwise every
// icon flashes huge for a second before the CSS is loaded.
import '@fortawesome/fontawesome-svg-core/styles.css';
faConfig.autoAddCss = false;
const desktopDetails = electron?.version
? { majorDesktopVersion: extractMajorDesktopVersion(electron?.version) }
: null;
registerConfig({
// We can't securely cache credentials on web
resolveKeyserverSessionInvalidationUsingNativeCredentials: null,
setSessionIDOnRequest: true,
// Never reset the calendar range
calendarRangeInactivityLimit: null,
platformDetails: {
platform: electron?.platform ?? 'web',
codeVersion: 112,
stateVersion: persistConfig.version,
...desktopDetails,
},
authoritativeKeyserverID,
olmAPI,
sqliteAPI,
+ encryptedNotifUtilsAPI,
});
const versionBroadcast = new BroadcastChannel('comm_version');
versionBroadcast.postMessage(getConfig().platformDetails.codeVersion);
versionBroadcast.onmessage = (event: MessageEvent) => {
if (event.data && event.data !== getConfig().platformDetails.codeVersion) {
location.reload();
}
};
// Start initializing the shared worker immediately
void getCommSharedWorker();
type BaseProps = {
+location: {
+pathname: string,
...
},
};
type Props = {
...BaseProps,
// Redux state
+navInfo: WebNavInfo,
+entriesLoadingStatus: LoadingStatus,
+loggedIn: boolean,
+activeThreadCurrentlyUnread: boolean,
// Redux dispatch functions
+dispatch: Dispatch,
+modals: $ReadOnlyArray<React.Node>,
};
class App extends React.PureComponent<Props> {
componentDidMount() {
const {
navInfo,
location: { pathname },
loggedIn,
} = this.props;
const newURL = canonicalURLFromReduxState(navInfo, pathname, loggedIn);
if (pathname !== newURL) {
history.replace(newURL);
}
}
componentDidUpdate(prevProps: Props) {
const {
navInfo,
location: { pathname },
loggedIn,
} = this.props;
if (!_isEqual(navInfo)(prevProps.navInfo)) {
const newURL = canonicalURLFromReduxState(navInfo, pathname, loggedIn);
if (newURL !== pathname) {
history.push(newURL);
}
} else if (pathname !== prevProps.location.pathname) {
const urlInfo = infoFromURL(pathname);
const newNavInfo = navInfoFromURL(urlInfo, { navInfo });
if (!_isEqual(newNavInfo)(navInfo)) {
this.props.dispatch({
type: updateNavInfoActionType,
payload: newNavInfo,
});
}
} else if (loggedIn !== prevProps.loggedIn) {
const newURL = canonicalURLFromReduxState(navInfo, pathname, loggedIn);
if (newURL !== pathname) {
history.replace(newURL);
}
}
if (loggedIn !== prevProps.loggedIn) {
electron?.clearHistory();
}
}
onWordmarkClicked = () => {
this.props.dispatch({
type: updateNavInfoActionType,
payload: { tab: 'chat' },
});
};
render(): React.Node {
let content;
if (this.props.loggedIn) {
content = (
<>
<WebEditThreadAvatarProvider>
<EditUserAvatarProvider>
<StaffContextProvider>
<MemberListSidebarProvider>
{this.renderMainContent()}
{this.props.modals}
</MemberListSidebarProvider>
</StaffContextProvider>
</EditUserAvatarProvider>
</WebEditThreadAvatarProvider>
</>
);
} else {
content = (
<>
{this.renderLoginPage()}
{this.props.modals}
</>
);
}
return (
<DndProvider backend={HTML5Backend}>
<EditModalProvider>
<MenuProvider>
<AlchemyENSCacheProvider>
<NeynarClientProvider apiKey={process.env.COMM_NEYNAR_KEY}>
<TooltipProvider>
<MessageSearchStateProvider>
<ChatMentionContextProvider>
<FocusHandler />
<VisibilityHandler />
<PolicyAcknowledgmentHandler />
<PushNotificationsHandler />
<InviteLinkHandler />
<InviteLinksRefresher />
<CommunitiesRefresher />
<MinVersionHandler />
<PlatformDetailsSynchronizer />
<LogOutIfMissingCSATHandler />
<UserInfosHandler />
<TunnelbrokerDeviceTokenHandler />
<FarcasterDataHandler />
<AutoJoinCommunityHandler />
{content}
</ChatMentionContextProvider>
</MessageSearchStateProvider>
</TooltipProvider>
</NeynarClientProvider>
</AlchemyENSCacheProvider>
</MenuProvider>
</EditModalProvider>
</DndProvider>
);
}
onHeaderDoubleClick = (): void => electron?.doubleClickTopBar();
stopDoubleClickPropagation: ?(SyntheticEvent<HTMLAnchorElement>) => void =
electron ? e => e.stopPropagation() : null;
renderLoginPage(): React.Node {
const { loginMethod } = this.props.navInfo;
if (loginMethod === 'qr-code') {
return <QRCodeLogin />;
}
return <Splash />;
}
renderMainContent(): React.Node {
const mainContent = this.getMainContentWithSwitcher();
let navigationArrows = null;
if (electron) {
navigationArrows = <NavigationArrows />;
}
const headerClasses = classnames({
[css.header]: true,
[css['electron-draggable']]: electron,
});
const wordmarkClasses = classnames({
[css.wordmark]: true,
[css['electron-non-draggable']]: electron,
[css['wordmark-macos']]: electron?.platform === 'macos',
});
return (
<div className={css.layout}>
<KeyserverReachabilityHandler />
<DisconnectedBar />
<UpdateModalHandler />
<header
className={headerClasses}
onDoubleClick={this.onHeaderDoubleClick}
>
<div className={css['main-header']}>
<h1 className={wordmarkClasses}>
<a
title="Comm Home"
aria-label="Go to Comm Home"
onClick={this.onWordmarkClicked}
onDoubleClick={this.stopDoubleClickPropagation}
>
Comm
</a>
</h1>
{navigationArrows}
<div className={css['upper-right']}>
<LoadingIndicator
status={this.props.entriesLoadingStatus}
size="medium"
loadingClassName={css['page-loading']}
errorClassName={css['page-error']}
/>
</div>
</div>
</header>
<InputStateContainer>{mainContent}</InputStateContainer>
<div className={css.sidebar}>
<CommunityPicker />
</div>
</div>
);
}
getMainContentWithSwitcher(): React.Node {
const { tab, settingsSection } = this.props.navInfo;
let mainContent: React.Node;
if (tab === 'settings') {
if (settingsSection === 'account') {
mainContent = <AccountSettings />;
} else if (settingsSection === 'friend-list') {
mainContent = null;
} else if (settingsSection === 'block-list') {
mainContent = null;
} else if (settingsSection === 'keyservers') {
mainContent = <KeyserverSelectionList />;
} else if (settingsSection === 'build-info') {
mainContent = null;
} else if (settingsSection === 'danger-zone') {
mainContent = <DangerZone />;
}
return (
<div className={css['main-content-container']}>
<div className={css.switcher}>
<SettingsSwitcher />
</div>
<div className={css['main-content']}>{mainContent}</div>
</div>
);
}
if (tab === 'calendar') {
mainContent = <Calendar url={this.props.location.pathname} />;
} else if (tab === 'chat') {
mainContent = <Chat />;
}
const mainContentClass = classnames(
css['main-content-container'],
css['main-content-container-column'],
);
return (
<div className={mainContentClass}>
<Topbar />
<div className={css['main-content']}>{mainContent}</div>
</div>
);
}
}
const WEB_TUNNELBROKER_CHANNEL = new BroadcastChannel('shared-tunnelbroker');
const WEB_TUNNELBROKER_MESSAGE_TYPES = Object.freeze({
SEND_MESSAGE: 'send-message',
MESSAGE_STATUS: 'message-status',
});
function useOtherTabsTunnelbrokerConnection(): SecondaryTunnelbrokerConnection {
const onSendMessageCallbacks = React.useRef<
Set<(MessageToDeviceRequest) => mixed>,
>(new Set());
const onMessageStatusCallbacks = React.useRef<
Set<(messageID: string, error: ?string) => mixed>,
>(new Set());
React.useEffect(() => {
const messageHandler = (event: MessageEvent) => {
if (typeof event.data !== 'object' || !event.data) {
console.log(
'Invalid message received from shared ' +
'tunnelbroker broadcast channel',
event.data,
);
return;
}
const data = event.data;
if (data.type === WEB_TUNNELBROKER_MESSAGE_TYPES.SEND_MESSAGE) {
if (typeof data.message !== 'object' || !data.message) {
console.log(
'Invalid tunnelbroker message request received ' +
'from shared tunnelbroker broadcast channel',
event.data,
);
return;
}
// We know that the input was already validated
const message: MessageToDeviceRequest = (data.message: any);
for (const callback of onSendMessageCallbacks.current) {
callback(message);
}
} else if (data.type === WEB_TUNNELBROKER_MESSAGE_TYPES.MESSAGE_STATUS) {
if (typeof data.messageID !== 'string') {
console.log(
'Missing message id in message status message ' +
'from shared tunnelbroker broadcast channel',
);
return;
}
const messageID = data.messageID;
if (
typeof data.error !== 'string' &&
data.error !== null &&
data.error !== undefined
) {
console.log(
'Invalid error in message status message ' +
'from shared tunnelbroker broadcast channel',
data.error,
);
return;
}
const error = data.error;
for (const callback of onMessageStatusCallbacks.current) {
callback(messageID, error);
}
} else {
console.log(
'Invalid message type ' +
'from shared tunnelbroker broadcast channel',
data,
);
}
};
WEB_TUNNELBROKER_CHANNEL.addEventListener('message', messageHandler);
return () =>
WEB_TUNNELBROKER_CHANNEL.removeEventListener('message', messageHandler);
}, [onMessageStatusCallbacks, onSendMessageCallbacks]);
return React.useMemo(
() => ({
sendMessage: message =>
WEB_TUNNELBROKER_CHANNEL.postMessage({
type: WEB_TUNNELBROKER_MESSAGE_TYPES.SEND_MESSAGE,
message,
}),
onSendMessage: callback => {
onSendMessageCallbacks.current.add(callback);
return () => {
onSendMessageCallbacks.current.delete(callback);
};
},
setMessageStatus: (messageID, error) => {
WEB_TUNNELBROKER_CHANNEL.postMessage({
type: WEB_TUNNELBROKER_MESSAGE_TYPES.MESSAGE_STATUS,
messageID,
error,
});
},
onMessageStatus: callback => {
onMessageStatusCallbacks.current.add(callback);
return () => {
onMessageStatusCallbacks.current.delete(callback);
};
},
}),
[onMessageStatusCallbacks, onSendMessageCallbacks],
);
}
const fetchEntriesLoadingStatusSelector = createLoadingStatusSelector(
fetchEntriesActionTypes,
);
const updateCalendarQueryLoadingStatusSelector = createLoadingStatusSelector(
updateCalendarQueryActionTypes,
);
const ConnectedApp: React.ComponentType<BaseProps> = React.memo<BaseProps>(
function ConnectedApp(props) {
const activeChatThreadID = useSelector(
state => state.navInfo.activeChatThreadID,
);
const navInfo = useSelector(state => state.navInfo);
const fetchEntriesLoadingStatus = useSelector(
fetchEntriesLoadingStatusSelector,
);
const updateCalendarQueryLoadingStatus = useSelector(
updateCalendarQueryLoadingStatusSelector,
);
const entriesLoadingStatus = combineLoadingStatuses(
fetchEntriesLoadingStatus,
updateCalendarQueryLoadingStatus,
);
const loggedIn = useSelector(isLoggedIn);
const activeThreadCurrentlyUnread = useSelector(
state =>
!activeChatThreadID ||
!!state.threadStore.threadInfos[activeChatThreadID]?.currentUser.unread,
);
useBadgeHandler();
const dispatch = useDispatch();
const modalContext = useModalContext();
const modals = React.useMemo(
() =>
modalContext.modals.map(([modal, key]) => (
<React.Fragment key={key}>{modal}</React.Fragment>
)),
[modalContext.modals],
);
const { lockStatus, releaseLockOrAbortRequest } = useWebLock(
TUNNELBROKER_LOCK_NAME,
);
const secondaryTunnelbrokerConnection: SecondaryTunnelbrokerConnection =
useOtherTabsTunnelbrokerConnection();
const handleSecondaryDeviceLogInError =
useHandleSecondaryDeviceLogInError();
return (
<AppThemeWrapper>
<TunnelbrokerProvider
shouldBeClosed={lockStatus !== 'acquired'}
onClose={releaseLockOrAbortRequest}
secondaryTunnelbrokerConnection={secondaryTunnelbrokerConnection}
>
<IdentitySearchProvider>
<QRAuthProvider
parseTunnelbrokerQRAuthMessage={parseTunnelbrokerQRAuthMessage}
composeTunnelbrokerQRAuthMessage={
composeTunnelbrokerQRAuthMessage
}
generateAESKey={generateQRAuthAESKey}
onLogInError={handleSecondaryDeviceLogInError}
>
<App
{...props}
navInfo={navInfo}
entriesLoadingStatus={entriesLoadingStatus}
loggedIn={loggedIn}
activeThreadCurrentlyUnread={activeThreadCurrentlyUnread}
dispatch={dispatch}
modals={modals}
/>
</QRAuthProvider>
<DBOpsHandler />
</IdentitySearchProvider>
</TunnelbrokerProvider>
</AppThemeWrapper>
);
},
);
function AppWithProvider(props: BaseProps): React.Node {
return (
<ModalProvider>
<ConnectedApp {...props} />
</ModalProvider>
);
}
export default AppWithProvider;

File Metadata

Mime Type
text/x-diff
Expires
Mon, Dec 23, 7:32 AM (1 d, 39 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
2690569
Default Alt Text
(47 KB)

Event Timeline