Page MenuHomePhabricator

D12429.id41563.diff
No OneTemporary

D12429.id41563.diff

diff --git a/keyserver/src/push/encrypted-notif-utils-api.js b/keyserver/src/push/encrypted-notif-utils-api.js
--- a/keyserver/src/push/encrypted-notif-utils-api.js
+++ b/keyserver/src/push/encrypted-notif-utils-api.js
@@ -6,6 +6,7 @@
import { blobServiceUpload } from './utils.js';
import { encryptAndUpdateOlmSession } from '../updaters/olm-session-updater.js';
+import { getOlmUtility } from '../utils/olm-utils.js';
const encryptedNotifUtilsAPI: EncryptedNotifUtilsAPI = {
encryptSerializedNotifPayload: async (
@@ -44,6 +45,8 @@
uploadLargeNotifPayload: blobServiceUpload,
getNotifByteSize: (serializedPayload: string) =>
Buffer.byteLength(serializedPayload),
+ getEncryptedNotifHash: (serializedNotification: string) =>
+ getOlmUtility().sha256(serializedNotification),
};
export default encryptedNotifUtilsAPI;
diff --git a/keyserver/src/push/providers.js b/keyserver/src/push/providers.js
--- a/keyserver/src/push/providers.js
+++ b/keyserver/src/push/providers.js
@@ -113,15 +113,6 @@
}
}
-function getAPNsNotificationTopic(platformDetails: PlatformDetails): string {
- if (platformDetails.platform === 'macos') {
- return 'app.comm.macos';
- }
- return platformDetails.codeVersion && platformDetails.codeVersion >= 87
- ? 'app.comm'
- : 'org.squadcal.app';
-}
-
type WebPushConfig = { +publicKey: string, +privateKey: string };
let cachedWebPushConfig: ?WebPushConfig = null;
async function getWebPushConfig(): Promise<?WebPushConfig> {
@@ -208,7 +199,6 @@
getFCMProvider,
endFirebase,
endAPNs,
- getAPNsNotificationTopic,
getWebPushConfig,
ensureWebPushInitialized,
getWNSToken,
diff --git a/keyserver/src/push/rescind.js b/keyserver/src/push/rescind.js
--- a/keyserver/src/push/rescind.js
+++ b/keyserver/src/push/rescind.js
@@ -6,6 +6,7 @@
import invariant from 'invariant';
import { createAndroidNotificationRescind } from 'lib/push/android-notif-creators.js';
+import { getAPNsNotificationTopic } from 'lib/shared/notif-utils.js';
import type { PlatformDetails } from 'lib/types/device-types.js';
import type {
NotificationTargetDevice,
@@ -20,7 +21,6 @@
import { prepareEncryptedIOSNotificationRescind } from './crypto.js';
import encryptedNotifUtilsAPI from './encrypted-notif-utils-api.js';
-import { getAPNsNotificationTopic } from './providers.js';
import type { TargetedAPNsNotification } from './types.js';
import {
apnPush,
diff --git a/keyserver/src/push/send.js b/keyserver/src/push/send.js
--- a/keyserver/src/push/send.js
+++ b/keyserver/src/push/send.js
@@ -18,6 +18,7 @@
createAndroidVisualNotification,
createAndroidBadgeOnlyNotification,
} from 'lib/push/android-notif-creators.js';
+import { apnMaxNotificationPayloadByteSize } from 'lib/push/apns-notif-creators.js';
import {
type WebNotifInputData,
webNotifInputDataValidator,
@@ -36,7 +37,10 @@
sortMessageInfoList,
} from 'lib/shared/message-utils.js';
import { messageSpecs } from 'lib/shared/messages/message-specs.js';
-import { notifTextsForMessageInfo } from 'lib/shared/notif-utils.js';
+import {
+ notifTextsForMessageInfo,
+ getAPNsNotificationTopic,
+} from 'lib/shared/notif-utils.js';
import {
rawThreadInfoFromServerThreadInfo,
threadInfoFromRawThreadInfo,
@@ -66,11 +70,9 @@
import { prepareEncryptedAPNsNotifications } from './crypto.js';
import encryptedNotifUtilsAPI from './encrypted-notif-utils-api.js';
-import { getAPNsNotificationTopic } from './providers.js';
import { rescindPushNotifs } from './rescind.js';
import type { TargetedAPNsNotification } from './types.js';
import {
- apnMaxNotificationPayloadByteSize,
apnPush,
fcmPush,
getUnreadCounts,
diff --git a/keyserver/src/push/utils.js b/keyserver/src/push/utils.js
--- a/keyserver/src/push/utils.js
+++ b/keyserver/src/push/utils.js
@@ -8,8 +8,6 @@
import uuid from 'uuid';
import webpush from 'web-push';
-import { fcmMaxNotificationPayloadByteSize } from 'lib/push/android-notif-creators.js';
-import { wnsMaxNotificationPayloadByteSize } from 'lib/push/wns-notif-creators.js';
import type { PlatformDetails } from 'lib/types/device-types.js';
import type {
TargetedAndroidNotification,
@@ -40,7 +38,6 @@
const apnTokenInvalidationErrorCode = 410;
const apnBadRequestErrorCode = 400;
const apnBadTokenErrorString = 'BadDeviceToken';
-const apnMaxNotificationPayloadByteSize = 4096;
const webInvalidTokenErrorCodes = [404, 410];
const wnsInvalidTokenErrorCodes = [404, 410];
@@ -446,7 +443,4 @@
webPush,
wnsPush,
getUnreadCounts,
- apnMaxNotificationPayloadByteSize,
- fcmMaxNotificationPayloadByteSize,
- wnsMaxNotificationPayloadByteSize,
};
diff --git a/lib/push/android-notif-creators.js b/lib/push/android-notif-creators.js
--- a/lib/push/android-notif-creators.js
+++ b/lib/push/android-notif-creators.js
@@ -32,7 +32,7 @@
export const fcmMaxNotificationPayloadByteSize = 4000;
-type CommonNativeNotifInputData = $ReadOnly<{
+export type CommonNativeNotifInputData = $ReadOnly<{
+senderDeviceDescriptor: SenderDeviceDescriptor,
+notifTexts: ResolvedNotifTexts,
+newRawMessageInfos: RawMessageInfo[],
@@ -42,15 +42,16 @@
+platformDetails: PlatformDetails,
}>;
-const commonNativeNotifInputDataValidator = tShape<CommonNativeNotifInputData>({
- senderDeviceDescriptor: senderDeviceDescriptorValidator,
- notifTexts: resolvedNotifTextsValidator,
- newRawMessageInfos: t.list(rawMessageInfoValidator),
- threadID: tID,
- collapseKey: t.maybe(t.String),
- unreadCount: t.maybe(t.Number),
- platformDetails: tPlatformDetails,
-});
+export const commonNativeNotifInputDataValidator: TInterface<CommonNativeNotifInputData> =
+ tShape<CommonNativeNotifInputData>({
+ senderDeviceDescriptor: senderDeviceDescriptorValidator,
+ notifTexts: resolvedNotifTextsValidator,
+ newRawMessageInfos: t.list(rawMessageInfoValidator),
+ threadID: tID,
+ collapseKey: t.maybe(t.String),
+ unreadCount: t.maybe(t.Number),
+ platformDetails: tPlatformDetails,
+ });
export type AndroidNotifInputData = {
...CommonNativeNotifInputData,
@@ -104,7 +105,7 @@
},
};
- if (unreadCount) {
+ if (unreadCount !== undefined && unreadCount !== null) {
notification.data = {
...notification.data,
badge: unreadCount.toString(),
diff --git a/lib/push/apns-notif-creators.js b/lib/push/apns-notif-creators.js
new file mode 100644
--- /dev/null
+++ b/lib/push/apns-notif-creators.js
@@ -0,0 +1,504 @@
+// @flow
+
+import invariant from 'invariant';
+import t, { type TInterface } from 'tcomb';
+
+import {
+ type CommonNativeNotifInputData,
+ commonNativeNotifInputDataValidator,
+} from './android-notif-creators.js';
+import {
+ prepareEncryptedAPNsVisualNotifications,
+ prepareEncryptedAPNsSilentNotifications,
+} from './crypto.js';
+import { getAPNsNotificationTopic } from '../shared/notif-utils.js';
+import {
+ hasMinCodeVersion,
+ FUTURE_CODE_VERSION,
+} from '../shared/version-utils.js';
+import type { PlatformDetails } from '../types/device-types.js';
+import { messageTypes } from '../types/message-types-enum.js';
+import {
+ type NotificationTargetDevice,
+ type EncryptedNotifUtilsAPI,
+ type TargetedAPNsNotification,
+ type APNsVisualNotification,
+ type APNsNotificationHeaders,
+ type SenderDeviceDescriptor,
+} from '../types/notif-types.js';
+import { tShape } from '../utils/validation-utils.js';
+
+export const apnMaxNotificationPayloadByteSize = 4096;
+
+export type APNsNotifInputData = {
+ ...CommonNativeNotifInputData,
+ +badgeOnly: boolean,
+ +uniqueID: string,
+};
+
+export const apnsNotifInputDataValidator: TInterface<APNsNotifInputData> =
+ tShape<APNsNotifInputData>({
+ ...commonNativeNotifInputDataValidator.meta.props,
+ badgeOnly: t.Boolean,
+ uniqueID: t.String,
+ });
+
+async function createAPNsVisualNotification(
+ encryptedNotifUtilsAPI: EncryptedNotifUtilsAPI,
+ inputData: APNsNotifInputData,
+ devices: $ReadOnlyArray<NotificationTargetDevice>,
+): Promise<$ReadOnlyArray<TargetedAPNsNotification>> {
+ const {
+ senderDeviceDescriptor,
+ notifTexts,
+ newRawMessageInfos,
+ threadID,
+ collapseKey,
+ badgeOnly,
+ unreadCount,
+ platformDetails,
+ uniqueID,
+ } = inputData;
+
+ const canDecryptNonCollapsibleTextIOSNotifs = hasMinCodeVersion(
+ platformDetails,
+ { native: 222 },
+ );
+
+ const isNonCollapsibleTextNotification =
+ newRawMessageInfos.every(
+ newRawMessageInfo => newRawMessageInfo.type === messageTypes.TEXT,
+ ) && !collapseKey;
+
+ const canDecryptAllIOSNotifs = hasMinCodeVersion(platformDetails, {
+ native: 267,
+ });
+
+ const canDecryptIOSNotif =
+ platformDetails.platform === 'ios' &&
+ (canDecryptAllIOSNotifs ||
+ (isNonCollapsibleTextNotification &&
+ canDecryptNonCollapsibleTextIOSNotifs));
+
+ const canDecryptMacOSNotifs =
+ platformDetails.platform === 'macos' &&
+ hasMinCodeVersion(platformDetails, {
+ web: 47,
+ majorDesktop: 9,
+ });
+
+ let apsDictionary = {
+ 'thread-id': threadID,
+ };
+ if (unreadCount !== undefined && unreadCount !== null) {
+ apsDictionary = {
+ ...apsDictionary,
+ badge: unreadCount,
+ };
+ }
+
+ const { merged, ...rest } = notifTexts;
+ // We don't include alert's body on macos because we
+ // handle displaying the notification ourselves and
+ // we don't want macOS to display it automatically.
+ if (!badgeOnly && platformDetails.platform !== 'macos') {
+ apsDictionary = {
+ ...apsDictionary,
+ alert: merged,
+ sound: 'default',
+ };
+ }
+
+ if (hasMinCodeVersion(platformDetails, { native: 198 })) {
+ apsDictionary = {
+ ...apsDictionary,
+ 'mutable-content': 1,
+ };
+ }
+
+ let notificationPayload = {
+ ...rest,
+ id: uniqueID,
+ threadID,
+ };
+
+ let notificationHeaders: APNsNotificationHeaders = {
+ 'apns-topic': getAPNsNotificationTopic(platformDetails),
+ 'apns-id': uniqueID,
+ 'apns-push-type': 'alert',
+ };
+
+ if (collapseKey && (canDecryptAllIOSNotifs || canDecryptMacOSNotifs)) {
+ notificationPayload = {
+ ...notificationPayload,
+ collapseID: collapseKey,
+ };
+ } else if (collapseKey) {
+ notificationHeaders = {
+ ...notificationHeaders,
+ 'apns-collapse-id': collapseKey,
+ };
+ }
+
+ const notification = {
+ ...notificationPayload,
+ headers: notificationHeaders,
+ aps: apsDictionary,
+ };
+
+ const messageInfos = JSON.stringify(newRawMessageInfos);
+ const copyWithMessageInfos = {
+ ...notification,
+ messageInfos,
+ };
+
+ const notificationSizeValidator = (notif: APNsVisualNotification) => {
+ const { headers, ...notifSansHeaders } = notif;
+ return (
+ encryptedNotifUtilsAPI.getNotifByteSize(
+ JSON.stringify(notifSansHeaders),
+ ) <= apnMaxNotificationPayloadByteSize
+ );
+ };
+
+ const serializeAPNsNotif = (notif: APNsVisualNotification) => {
+ const { headers, ...notifSansHeaders } = notif;
+ return JSON.stringify(notifSansHeaders);
+ };
+
+ const shouldBeEncrypted = canDecryptIOSNotif || canDecryptMacOSNotifs;
+ if (!shouldBeEncrypted) {
+ const notificationToSend = notificationSizeValidator(copyWithMessageInfos)
+ ? copyWithMessageInfos
+ : notification;
+ return devices.map(({ deliveryID }) => ({
+ notification: notificationToSend,
+ deliveryID,
+ }));
+ }
+
+ // The `messageInfos` field in notification payload is
+ // not used on MacOS so we can return early.
+ if (platformDetails.platform === 'macos') {
+ const macOSNotifsWithoutMessageInfos =
+ await prepareEncryptedAPNsVisualNotifications(
+ encryptedNotifUtilsAPI,
+ senderDeviceDescriptor,
+ devices,
+ notification,
+ platformDetails.codeVersion,
+ );
+ return macOSNotifsWithoutMessageInfos.map(
+ ({ notification: notif, deliveryID }) => ({
+ notification: notif,
+ deliveryID,
+ }),
+ );
+ }
+
+ const notifsWithMessageInfos = await prepareEncryptedAPNsVisualNotifications(
+ encryptedNotifUtilsAPI,
+ senderDeviceDescriptor,
+ devices,
+ copyWithMessageInfos,
+ platformDetails.codeVersion,
+ notificationSizeValidator,
+ );
+
+ const devicesWithExcessiveSizeNoHolders = notifsWithMessageInfos
+ .filter(({ payloadSizeExceeded }) => payloadSizeExceeded)
+ .map(({ cryptoID, deliveryID }) => ({
+ cryptoID,
+ deliveryID,
+ }));
+
+ if (devicesWithExcessiveSizeNoHolders.length === 0) {
+ return notifsWithMessageInfos.map(
+ ({
+ notification: notif,
+ deliveryID,
+ encryptedPayloadHash,
+ encryptionOrder,
+ }) => ({
+ notification: notif,
+ deliveryID,
+ encryptedPayloadHash,
+ encryptionOrder,
+ }),
+ );
+ }
+
+ const canQueryBlobService = hasMinCodeVersion(platformDetails, {
+ native: 331,
+ });
+
+ let blobHash, blobHolders, encryptionKey, blobUploadError;
+ if (canQueryBlobService) {
+ ({ blobHash, blobHolders, encryptionKey, blobUploadError } =
+ await encryptedNotifUtilsAPI.uploadLargeNotifPayload(
+ serializeAPNsNotif(copyWithMessageInfos),
+ devicesWithExcessiveSizeNoHolders.length,
+ ));
+ }
+
+ if (blobUploadError) {
+ console.warn(
+ `Failed to upload payload of notification: ${uniqueID} ` +
+ `due to error: ${blobUploadError}`,
+ );
+ }
+
+ let devicesWithExcessiveSize = devicesWithExcessiveSizeNoHolders;
+ let notificationWithBlobMetadata = notification;
+ if (
+ blobHash &&
+ encryptionKey &&
+ blobHolders &&
+ blobHolders.length === devicesWithExcessiveSize.length
+ ) {
+ notificationWithBlobMetadata = {
+ ...notification,
+ blobHash,
+ encryptionKey,
+ };
+ devicesWithExcessiveSize = blobHolders.map((holder, idx) => ({
+ ...devicesWithExcessiveSize[idx],
+ blobHolder: holder,
+ }));
+ }
+
+ const notifsWithoutMessageInfos =
+ await prepareEncryptedAPNsVisualNotifications(
+ encryptedNotifUtilsAPI,
+ senderDeviceDescriptor,
+ devicesWithExcessiveSize,
+ notificationWithBlobMetadata,
+ platformDetails.codeVersion,
+ );
+
+ const targetedNotifsWithMessageInfos = notifsWithMessageInfos
+ .filter(({ payloadSizeExceeded }) => !payloadSizeExceeded)
+ .map(
+ ({
+ notification: notif,
+ deliveryID,
+ encryptedPayloadHash,
+ encryptionOrder,
+ }) => ({
+ notification: notif,
+ deliveryID,
+ encryptedPayloadHash,
+ encryptionOrder,
+ }),
+ );
+
+ const targetedNotifsWithoutMessageInfos = notifsWithoutMessageInfos.map(
+ ({
+ notification: notif,
+ deliveryID,
+ encryptedPayloadHash,
+ encryptionOrder,
+ }) => ({
+ notification: notif,
+ deliveryID,
+ encryptedPayloadHash,
+ encryptionOrder,
+ }),
+ );
+
+ return [
+ ...targetedNotifsWithMessageInfos,
+ ...targetedNotifsWithoutMessageInfos,
+ ];
+}
+
+type APNsNotificationRescindInputData = {
+ +senderDeviceDescriptor: SenderDeviceDescriptor,
+ +rescindID?: string,
+ +badge?: number,
+ +threadID: string,
+ +platformDetails: PlatformDetails,
+};
+
+async function createAPNsNotificationRescind(
+ encryptedNotifUtilsAPI: EncryptedNotifUtilsAPI,
+ inputData: APNsNotificationRescindInputData,
+ devices: $ReadOnlyArray<NotificationTargetDevice>,
+): Promise<$ReadOnlyArray<TargetedAPNsNotification>> {
+ const {
+ badge,
+ rescindID,
+ threadID,
+ platformDetails,
+ senderDeviceDescriptor,
+ } = inputData;
+
+ invariant(
+ (rescindID && badge !== null && badge !== undefined) ||
+ hasMinCodeVersion(platformDetails, { native: FUTURE_CODE_VERSION }),
+ 'thick thread rescind not support for this client version',
+ );
+
+ const apnsTopic = getAPNsNotificationTopic(platformDetails);
+ let notification;
+
+ if (
+ rescindID &&
+ badge !== null &&
+ badge !== undefined &&
+ hasMinCodeVersion(platformDetails, { native: 198 })
+ ) {
+ notification = {
+ headers: {
+ 'apns-topic': apnsTopic,
+ 'apns-push-type': 'alert',
+ },
+ aps: {
+ 'mutable-content': 1,
+ 'badge': badge,
+ },
+ threadID,
+ notificationId: rescindID,
+ backgroundNotifType: 'CLEAR',
+ setUnreadStatus: true,
+ };
+ } else if (rescindID && badge !== null && badge !== undefined) {
+ notification = {
+ headers: {
+ 'apns-topic': apnsTopic,
+ 'apns-push-type': 'background',
+ 'apns-priority': 5,
+ },
+ aps: {
+ 'mutable-content': 1,
+ 'badge': badge,
+ },
+ threadID,
+ notificationId: rescindID,
+ backgroundNotifType: 'CLEAR',
+ setUnreadStatus: true,
+ };
+ } else {
+ notification = {
+ headers: {
+ 'apns-topic': apnsTopic,
+ 'apns-push-type': 'alert',
+ },
+ aps: {
+ 'mutable-content': 1,
+ },
+ threadID,
+ backgroundNotifType: 'CLEAR',
+ setUnreadStatus: true,
+ };
+ }
+
+ const shouldBeEncrypted = hasMinCodeVersion(platformDetails, { native: 233 });
+ if (!shouldBeEncrypted) {
+ return devices.map(({ deliveryID }) => ({
+ notification,
+ deliveryID,
+ }));
+ }
+
+ const notifications = await prepareEncryptedAPNsSilentNotifications(
+ encryptedNotifUtilsAPI,
+ senderDeviceDescriptor,
+ devices,
+ notification,
+ platformDetails.codeVersion,
+ );
+
+ return notifications.map(({ deliveryID, notification: notif }) => ({
+ deliveryID,
+ notification: notif,
+ }));
+}
+
+type APNsBadgeOnlyNotificationInputData = {
+ +senderDeviceDescriptor: SenderDeviceDescriptor,
+ +badge?: number,
+ +threadID?: string,
+ +platformDetails: PlatformDetails,
+};
+
+async function createAPNsBadgeOnlyNotification(
+ encryptedNotifUtilsAPI: EncryptedNotifUtilsAPI,
+ inputData: APNsBadgeOnlyNotificationInputData,
+ devices: $ReadOnlyArray<NotificationTargetDevice>,
+): Promise<$ReadOnlyArray<TargetedAPNsNotification>> {
+ const { senderDeviceDescriptor, platformDetails, threadID, badge } =
+ inputData;
+ invariant(
+ (!threadID && badge !== undefined && badge !== null) ||
+ hasMinCodeVersion(platformDetails, { native: FUTURE_CODE_VERSION }),
+ 'thick thread badge updates not support for this client version',
+ );
+
+ const shouldBeEncrypted = hasMinCodeVersion(platformDetails, {
+ native: 222,
+ web: 47,
+ majorDesktop: 9,
+ });
+
+ const headers: APNsNotificationHeaders = {
+ 'apns-topic': getAPNsNotificationTopic(platformDetails),
+ 'apns-push-type': 'alert',
+ };
+
+ let notification;
+ if (shouldBeEncrypted && threadID) {
+ notification = {
+ headers,
+ threadID,
+ aps: {
+ 'mutable-content': 1,
+ },
+ };
+ } else if (shouldBeEncrypted && badge !== undefined && badge !== null) {
+ notification = {
+ headers,
+ aps: {
+ 'badge': badge,
+ 'mutable-content': 1,
+ },
+ };
+ } else {
+ invariant(
+ badge !== null && badge !== undefined,
+ 'badge update must contain either badge count or threadID',
+ );
+ notification = {
+ headers,
+ aps: {
+ badge,
+ },
+ };
+ }
+
+ if (!shouldBeEncrypted) {
+ return devices.map(({ deliveryID }) => ({
+ deliveryID,
+ notification,
+ }));
+ }
+
+ const notifications = await prepareEncryptedAPNsSilentNotifications(
+ encryptedNotifUtilsAPI,
+ senderDeviceDescriptor,
+ devices,
+ notification,
+ platformDetails.codeVersion,
+ );
+
+ return notifications.map(({ deliveryID, notification: notif }) => ({
+ deliveryID,
+ notification: notif,
+ }));
+}
+
+export {
+ createAPNsBadgeOnlyNotification,
+ createAPNsNotificationRescind,
+ createAPNsVisualNotification,
+};
diff --git a/lib/push/crypto.js b/lib/push/crypto.js
--- a/lib/push/crypto.js
+++ b/lib/push/crypto.js
@@ -1,5 +1,7 @@
// @flow
+import invariant from 'invariant';
+
import type {
PlainTextWebNotification,
PlainTextWebNotificationPayload,
@@ -13,6 +15,9 @@
NotificationTargetDevice,
SenderDeviceDescriptor,
EncryptedNotifUtilsAPI,
+ APNsVisualNotification,
+ APNsNotificationRescind,
+ APNsBadgeOnlyNotification,
} from '../types/notif-types.js';
async function encryptAndroidNotificationPayload<T>(
@@ -83,6 +88,170 @@
}
}
+async function encryptAPNsVisualNotification(
+ encryptedNotifUtilsAPI: EncryptedNotifUtilsAPI,
+ cookieID: string,
+ senderDeviceDescriptor: SenderDeviceDescriptor,
+ notification: APNsVisualNotification,
+ notificationSizeValidator?: APNsVisualNotification => boolean,
+ codeVersion?: ?number,
+ blobHolder?: ?string,
+): Promise<{
+ +notification: APNsVisualNotification,
+ +payloadSizeExceeded: boolean,
+ +encryptedPayloadHash?: string,
+ +encryptionOrder?: number,
+}> {
+ const {
+ id,
+ headers,
+ aps: { badge, alert, sound },
+ ...rest
+ } = notification;
+
+ invariant(
+ !headers['apns-collapse-id'],
+ `Collapse ID can't be directly stored in apn.Notification object due ` +
+ `to security reasons. Please put it in payload property`,
+ );
+
+ let unencryptedPayload = {
+ ...rest,
+ aps: { sound },
+ merged: alert,
+ badge,
+ };
+
+ if (blobHolder) {
+ unencryptedPayload = { ...unencryptedPayload, blobHolder };
+ }
+
+ try {
+ const unencryptedSerializedPayload = JSON.stringify(unencryptedPayload);
+
+ let encryptedNotifAps = { 'mutable-content': 1 };
+ if (codeVersion && codeVersion >= 254 && codeVersion % 2 === 0) {
+ encryptedNotifAps = {
+ ...encryptedNotifAps,
+ alert: { body: 'ENCRYPTED' },
+ };
+ }
+
+ let dbPersistCondition;
+ if (notificationSizeValidator) {
+ dbPersistCondition = (encryptedPayload: string) =>
+ notificationSizeValidator({
+ ...senderDeviceDescriptor,
+ id,
+ headers,
+ encryptedPayload,
+ aps: encryptedNotifAps,
+ });
+ }
+
+ const {
+ encryptedData: serializedPayload,
+ sizeLimitViolated: dbPersistConditionViolated,
+ encryptionOrder,
+ } = await encryptedNotifUtilsAPI.encryptSerializedNotifPayload(
+ cookieID,
+ unencryptedSerializedPayload,
+ dbPersistCondition,
+ );
+
+ const encryptedPayloadHash = encryptedNotifUtilsAPI.getEncryptedNotifHash(
+ serializedPayload.body,
+ );
+
+ return {
+ notification: {
+ ...senderDeviceDescriptor,
+ id,
+ headers,
+ encryptedPayload: serializedPayload.body,
+ aps: encryptedNotifAps,
+ },
+ payloadSizeExceeded: !!dbPersistConditionViolated,
+ encryptedPayloadHash,
+ encryptionOrder,
+ };
+ } catch (e) {
+ console.log('Notification encryption failed: ' + e);
+ const unencryptedNotification = { ...notification, encryptionFailed: '1' };
+ return {
+ notification: unencryptedNotification,
+ payloadSizeExceeded: notificationSizeValidator
+ ? notificationSizeValidator(unencryptedNotification)
+ : false,
+ };
+ }
+}
+
+async function encryptAPNsSilentNotification(
+ encryptedNotifUtilsAPI: EncryptedNotifUtilsAPI,
+ cookieID: string,
+ senderDeviceDescriptor: SenderDeviceDescriptor,
+ notification: APNsNotificationRescind | APNsBadgeOnlyNotification,
+ codeVersion?: ?number,
+): Promise<{
+ +notification: APNsNotificationRescind | APNsBadgeOnlyNotification,
+ +encryptedPayloadHash?: string,
+ +encryptionOrder?: number,
+}> {
+ const {
+ headers,
+ aps: { badge },
+ ...rest
+ } = notification;
+
+ let unencryptedPayload = {
+ ...rest,
+ };
+
+ if (badge !== null && badge !== undefined) {
+ unencryptedPayload = { ...unencryptedPayload, badge, aps: {} };
+ }
+
+ try {
+ const unencryptedSerializedPayload = JSON.stringify(unencryptedPayload);
+
+ let encryptedNotifAps = { 'mutable-content': 1 };
+ if (codeVersion && codeVersion >= 254 && codeVersion % 2 === 0) {
+ encryptedNotifAps = {
+ ...encryptedNotifAps,
+ alert: { body: 'ENCRYPTED' },
+ };
+ }
+
+ const { encryptedData: serializedPayload, encryptionOrder } =
+ await encryptedNotifUtilsAPI.encryptSerializedNotifPayload(
+ cookieID,
+ unencryptedSerializedPayload,
+ );
+
+ const encryptedPayloadHash = encryptedNotifUtilsAPI.getEncryptedNotifHash(
+ serializedPayload.body,
+ );
+
+ return {
+ notification: {
+ ...senderDeviceDescriptor,
+ headers,
+ encryptedPayload: serializedPayload.body,
+ aps: encryptedNotifAps,
+ },
+ encryptedPayloadHash,
+ encryptionOrder,
+ };
+ } catch (e) {
+ console.log('Notification encryption failed: ' + e);
+ const unencryptedNotification = { ...notification, encryptionFailed: '1' };
+ return {
+ notification: unencryptedNotification,
+ };
+ }
+}
+
async function encryptAndroidVisualNotification(
encryptedNotifUtilsAPI: EncryptedNotifUtilsAPI,
senderDeviceDescriptor: SenderDeviceDescriptor,
@@ -254,6 +423,66 @@
};
}
+function prepareEncryptedAPNsVisualNotifications(
+ encryptedNotifUtilsAPI: EncryptedNotifUtilsAPI,
+ senderDeviceDescriptor: SenderDeviceDescriptor,
+ devices: $ReadOnlyArray<NotificationTargetDevice>,
+ notification: APNsVisualNotification,
+ codeVersion?: ?number,
+ notificationSizeValidator?: APNsVisualNotification => boolean,
+): Promise<
+ $ReadOnlyArray<{
+ +cryptoID: string,
+ +deliveryID: string,
+ +notification: APNsVisualNotification,
+ +payloadSizeExceeded: boolean,
+ +encryptedPayloadHash?: string,
+ +encryptionOrder?: number,
+ }>,
+> {
+ const notificationPromises = devices.map(
+ async ({ cryptoID, deliveryID, blobHolder }) => {
+ const notif = await encryptAPNsVisualNotification(
+ encryptedNotifUtilsAPI,
+ cryptoID,
+ senderDeviceDescriptor,
+ notification,
+ notificationSizeValidator,
+ codeVersion,
+ blobHolder,
+ );
+ return { cryptoID, deliveryID, ...notif };
+ },
+ );
+ return Promise.all(notificationPromises);
+}
+
+function prepareEncryptedAPNsSilentNotifications(
+ encryptedNotifUtilsAPI: EncryptedNotifUtilsAPI,
+ senderDeviceDescriptor: SenderDeviceDescriptor,
+ devices: $ReadOnlyArray<NotificationTargetDevice>,
+ notification: APNsNotificationRescind | APNsBadgeOnlyNotification,
+ codeVersion?: ?number,
+): Promise<
+ $ReadOnlyArray<{
+ +cryptoID: string,
+ +deliveryID: string,
+ +notification: APNsNotificationRescind | APNsBadgeOnlyNotification,
+ }>,
+> {
+ const notificationPromises = devices.map(async ({ deliveryID, cryptoID }) => {
+ const { notification: notif } = await encryptAPNsSilentNotification(
+ encryptedNotifUtilsAPI,
+ cryptoID,
+ senderDeviceDescriptor,
+ notification,
+ codeVersion,
+ );
+ return { cryptoID, deliveryID, notification: notif };
+ });
+ return Promise.all(notificationPromises);
+}
+
function prepareEncryptedAndroidVisualNotifications(
encryptedNotifUtilsAPI: EncryptedNotifUtilsAPI,
senderDeviceDescriptor: SenderDeviceDescriptor,
@@ -361,6 +590,8 @@
}
export {
+ prepareEncryptedAPNsVisualNotifications,
+ prepareEncryptedAPNsSilentNotifications,
prepareEncryptedAndroidVisualNotifications,
prepareEncryptedAndroidSilentNotifications,
prepareEncryptedWebNotifications,
diff --git a/lib/shared/notif-utils.js b/lib/shared/notif-utils.js
--- a/lib/shared/notif-utils.js
+++ b/lib/shared/notif-utils.js
@@ -7,6 +7,7 @@
import type { NotificationTextsParams } from './messages/message-spec.js';
import { messageSpecs } from './messages/message-specs.js';
import { threadNoun } from './thread-utils.js';
+import { type PlatformDetails } from '../types/device-types.js';
import { type MessageType, messageTypes } from '../types/message-types-enum.js';
import {
type MessageData,
@@ -18,7 +19,11 @@
import type { CreateSidebarMessageInfo } from '../types/messages/create-sidebar.js';
import type { TextMessageInfo } from '../types/messages/text.js';
import type { ThreadInfo } from '../types/minimally-encoded-thread-permissions-types.js';
-import type { NotifTexts, ResolvedNotifTexts } from '../types/notif-types.js';
+import type {
+ NotifTexts,
+ ResolvedNotifTexts,
+ APNsNotificationTopic,
+} from '../types/notif-types.js';
import { type ThreadType, threadTypes } from '../types/thread-types-enum.js';
import type { RelativeUserInfo, UserInfo } from '../types/user-types.js';
import { prettyDate } from '../utils/date-utils.js';
@@ -321,6 +326,17 @@
return { body: merged, title };
}
+function getAPNsNotificationTopic(
+ platformDetails: PlatformDetails,
+): APNsNotificationTopic {
+ if (platformDetails.platform === 'macos') {
+ return 'app.comm.macos';
+ }
+ return platformDetails.codeVersion && platformDetails.codeVersion >= 87
+ ? 'app.comm'
+ : 'org.squadcal.app';
+}
+
export {
notifRobotextForMessageInfo,
notifTextsForMessageInfo,
@@ -329,4 +345,5 @@
notifTextsForSidebarCreation,
getNotifCollapseKey,
mergePrefixIntoBody,
+ getAPNsNotificationTopic,
};
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
@@ -37,6 +37,7 @@
tShape({ senderDeviceID: t.String }),
]);
+// Web notifs types
export type PlainTextWebNotificationPayload = {
+body: string,
+prefix?: string,
@@ -61,6 +62,7 @@
| PlainTextWebNotification
| EncryptedWebNotification;
+// WNS notifs types
export type PlainTextWNSNotification = {
+body: string,
+prefix?: string,
@@ -79,13 +81,14 @@
| PlainTextWNSNotification
| EncryptedWNSNotification;
+// Android notifs types
export type AndroidVisualNotificationPayloadBase = $ReadOnly<{
+badge?: string,
+body: string,
+title: string,
+prefix?: string,
+threadID: string,
- +collapseKey?: string,
+ +collapseID?: string,
+badgeOnly?: '0',
+encryptionFailed?: '1',
}>;
@@ -184,6 +187,155 @@
+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',
+};
+
+export type EncryptedAPNsSilentNotification = $ReadOnly<{
+ ...SenderDeviceDescriptor,
+ +headers: APNsNotificationHeaders,
+ +encryptedPayload: string,
+ +aps: { +'mutable-content': number, +'alert'?: { body: 'ENCRYPTED' } },
+}>;
+
+export type EncryptedAPNsVisualNotification = $ReadOnly<{
+ ...EncryptedAPNsSilentNotification,
+ +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,
@@ -230,4 +382,5 @@
| { +blobUploadError: string },
>,
+getNotifByteSize: (serializedNotification: string) => number,
+ +getEncryptedNotifHash: (serializedNotification: string) => string,
};

File Metadata

Mime Type
text/plain
Expires
Sat, Nov 16, 8:23 PM (21 h, 22 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
2502621
Default Alt Text
D12429.id41563.diff (33 KB)

Event Timeline