Page Menu
Home
Phabricator
Search
Configure Global Search
Log In
Files
F3509710
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
47 KB
Referenced Files
None
Subscribers
None
View Options
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
Details
Attached
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)
Attached To
Mode
rCOMM Comm
Attached
Detach File
Event Timeline
Log In to Comment