diff --git a/lib/push/send-hooks.react.js b/lib/push/send-hooks.react.js --- a/lib/push/send-hooks.react.js +++ b/lib/push/send-hooks.react.js @@ -1,6 +1,7 @@ // @flow import * as React from 'react'; +import uuid from 'uuid'; import { preparePushNotifs, @@ -10,36 +11,111 @@ 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, +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 { 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, - ) => { - return preparePushNotifs({ + async (notifCreationData: NotificationsCreationData) => { + const deviceID = await getContentSigningKey(); + const senderDeviceDescriptor = { senderDeviceID: deviceID }; + const { messageDatas } = notifCreationData; + + const pushNotifsPreparationInput = { encryptedNotifUtilsAPI, senderDeviceDescriptor, olmSessionCreator, @@ -50,9 +126,65 @@ 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, @@ -64,4 +196,4 @@ ); } -export { usePreparePushNotifs }; +export { useSendPushNotifs }; diff --git a/lib/types/notif-types.js b/lib/types/notif-types.js --- a/lib/types/notif-types.js +++ b/lib/types/notif-types.js @@ -3,7 +3,7 @@ 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'; @@ -28,6 +28,10 @@ prefix: t.maybe(t.String), }); +export type NotificationsCreationData = { + +messageDatas: $ReadOnlyArray, +}; + export type SenderDeviceDescriptor = | { +keyserverID: string } | { +senderDeviceID: string }; @@ -373,14 +377,14 @@ +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: ( diff --git a/lib/types/tunnelbroker/messages.js b/lib/types/tunnelbroker/messages.js --- a/lib/types/tunnelbroker/messages.js +++ b/lib/types/tunnelbroker/messages.js @@ -45,6 +45,8 @@ 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', diff --git a/lib/utils/__mocks__/config.js b/lib/utils/__mocks__/config.js --- a/lib/utils/__mocks__/config.js +++ b/lib/utils/__mocks__/config.js @@ -45,6 +45,12 @@ getOutboundP2PMessagesByID: jest.fn(), searchMessages: jest.fn(), }, + encryptedNotifUtilsAPI: { + encryptSerializedNotifPayload: jest.fn(), + uploadLargeNotifPayload: jest.fn(), + getEncryptedNotifHash: jest.fn(), + getNotifByteSize: jest.fn(), + }, }); const hasConfig = (): boolean => true; diff --git a/lib/utils/config.js b/lib/utils/config.js --- a/lib/utils/config.js +++ b/lib/utils/config.js @@ -8,6 +8,7 @@ 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'; @@ -29,6 +30,7 @@ +authoritativeKeyserverID: string, +olmAPI: OlmAPI, +sqliteAPI: SQLiteAPI, + +encryptedNotifUtilsAPI: EncryptedNotifUtilsAPI, }; let registeredConfig: ?Config = null; diff --git a/native/config.js b/native/config.js --- a/native/config.js +++ b/native/config.js @@ -8,6 +8,7 @@ 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({ @@ -22,4 +23,5 @@ authoritativeKeyserverID, olmAPI, sqliteAPI, + encryptedNotifUtilsAPI, }); diff --git a/web/app.react.js b/web/app.react.js --- a/web/app.react.js +++ b/web/app.react.js @@ -70,6 +70,7 @@ 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'; @@ -124,6 +125,7 @@ authoritativeKeyserverID, olmAPI, sqliteAPI, + encryptedNotifUtilsAPI, }); const versionBroadcast = new BroadcastChannel('comm_version');