diff --git a/lib/handlers/db-ops-handler.react.js b/lib/handlers/db-ops-handler.react.js --- a/lib/handlers/db-ops-handler.react.js +++ b/lib/handlers/db-ops-handler.react.js @@ -3,6 +3,7 @@ import * as React from 'react'; import { opsProcessingFinishedActionType } from '../actions/db-ops-actions.js'; +import { useSendPushNotifs } from '../push/send-hooks.react.js'; import { usePeerToPeerCommunication } from '../tunnelbroker/peer-to-peer-context.js'; import { useTunnelbroker } from '../tunnelbroker/tunnelbroker-context.js'; import type { DBOpsEntry } from '../types/db-ops-types.js'; @@ -22,6 +23,7 @@ const prevQueueFront = React.useRef(null); const { sendMessageToDevice } = useTunnelbroker(); const { processOutboundMessages } = usePeerToPeerCommunication(); + const sendPushNotifs = useSendPushNotifs(); const dispatch = useDispatch(); @@ -31,10 +33,13 @@ } prevQueueFront.current = queueFront; - const { ops, messageSourceMetadata, dmOpID } = queueFront; + const { ops, messageSourceMetadata, dmOpID, notificationsCreationData } = + queueFront; + void (async () => { if (ops) { await processDBStoreOperations(ops); + if (ops.outboundP2PMessages && ops.outboundP2PMessages.length > 0) { const messageIDs = ops.outboundP2PMessages.map( message => message.messageID, @@ -42,6 +47,10 @@ processOutboundMessages(messageIDs, dmOpID); } } + + if (notificationsCreationData) { + void sendPushNotifs(notificationsCreationData); + } dispatch({ type: opsProcessingFinishedActionType, }); @@ -69,6 +78,7 @@ } })(); }, [ + sendPushNotifs, queueFront, dispatch, processDBStoreOperations, 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/db-ops-types.js b/lib/types/db-ops-types.js --- a/lib/types/db-ops-types.js +++ b/lib/types/db-ops-types.js @@ -1,5 +1,6 @@ // @flow +import type { NotificationsCreationData } from './notif-types.js'; import type { StoreOperations } from './store-ops-types.js'; export type MessageSourceMetadata = { @@ -15,6 +16,9 @@ | { +ops: StoreOperations, +dmOpID?: string, + } + | { + +notificationsCreationData?: NotificationsCreationData, }; export type DBOpsStore = { 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 }; @@ -362,14 +366,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');