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'; @@ -27,6 +28,7 @@ const prevQueueFront = React.useRef(null); const { sendMessageToDevice } = useTunnelbroker(); const { processOutboundMessages } = usePeerToPeerCommunication(); + const sendPushNotifs = useSendPushNotifs(); const dispatch = useDispatch(); @@ -73,7 +75,15 @@ } } })(); + + void (async () => { + if (!ops?.notificationsMessageDatas) { + return; + } + await sendPushNotifs(ops.notificationsMessageDatas); + })(); }, [ + 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,81 @@ 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 { useTunnelbroker } from '../tunnelbroker/tunnelbroker-context.js'; import type { MessageData } from '../types/message-types.js'; import type { - EncryptedNotifUtilsAPI, - SenderDeviceDescriptor, + TargetedAPNsNotification, + TargetedAndroidNotification, } from '../types/notif-types.js'; +import type { + TunnelbrokerAPNsNotif, + TunnelbrokerFCMNotif, +} 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, +function apnsNotifToTunnelbrokerAPNsNotif( + targetedNotification: TargetedAPNsNotification, +): TunnelbrokerAPNsNotif { + const { + deliveryID: deviceID, + notification: { headers, ...payload }, + } = targetedNotification; + + const newHeaders = { + ...headers, + 'apns-push-type': 'Alert', + }; + + return { + type: 'APNsNotif', + 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: 'FCMNotif', + deviceID, + clientMessageID: uuid.v4(), + data: JSON.stringify(data), + priority: priority === 'normal' ? 'NORMAL' : 'HIGH', + }; +} + +function useSendPushNotifs(): ( messageDatas: $ReadOnlyArray, ) => 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 (messageDatas: $ReadOnlyArray) => { + const deviceID = await getContentSigningKey(); + const senderDeviceDescriptor = { senderDeviceID: deviceID }; + + const pushNotifsPreparationInput = { encryptedNotifUtilsAPI, senderDeviceDescriptor, olmSessionCreator, @@ -50,9 +96,57 @@ 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 { + 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 +158,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,6 @@ import type { EncryptResult } from '@commapp/olm'; import t, { type TInterface, type TUnion } from 'tcomb'; -import type { Platform } from './device-types.js'; import type { EntityText, ThreadEntity } from '../utils/entity-text.js'; import { tShape } from '../utils/validation-utils.js'; @@ -362,14 +361,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/store-ops-types.js b/lib/types/store-ops-types.js --- a/lib/types/store-ops-types.js +++ b/lib/types/store-ops-types.js @@ -15,6 +15,7 @@ ClientDBMessageInfo, ClientDBThreadMessageInfo, ClientDBLocalMessageInfo, + MessageData, } from './message-types.js'; import type { ClientReportCreationRequest } from './report-types.js'; import type { OutboundP2PMessage } from './sqlite-types.js'; @@ -94,6 +95,7 @@ +outboundP2PMessages?: $ReadOnlyArray, +entryStoreOperations?: $ReadOnlyArray, +messageSearchStoreOperations?: $ReadOnlyArray, + +notificationsMessageDatas?: $ReadOnlyArray, }; export type ClientDBStoreOperations = { 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 @@ -44,6 +44,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');