Page Menu
Home
Phabricator
Search
Configure Global Search
Log In
Files
F3255572
D12429.id41563.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
33 KB
Referenced Files
None
Subscribers
None
D12429.id41563.diff
View Options
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
Details
Attached
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)
Attached To
Mode
D12429: Implement notification types, creation and encryption for APNs notifications
Attached
Detach File
Event Timeline
Log In to Comment