Changeset View
Changeset View
Standalone View
Standalone View
keyserver/src/push/send.js
Show All 35 Lines | import type { | ||||
ResolvedNotifTexts, | ResolvedNotifTexts, | ||||
} from 'lib/types/notif-types.js'; | } from 'lib/types/notif-types.js'; | ||||
import { resolvedNotifTextsValidator } from 'lib/types/notif-types.js'; | import { resolvedNotifTextsValidator } from 'lib/types/notif-types.js'; | ||||
import type { ServerThreadInfo } from 'lib/types/thread-types.js'; | import type { ServerThreadInfo } from 'lib/types/thread-types.js'; | ||||
import { updateTypes } from 'lib/types/update-types-enum.js'; | import { updateTypes } from 'lib/types/update-types-enum.js'; | ||||
import { promiseAll } from 'lib/utils/promises.js'; | import { promiseAll } from 'lib/utils/promises.js'; | ||||
import { tID, tPlatformDetails, tShape } from 'lib/utils/validation-utils.js'; | import { tID, tPlatformDetails, tShape } from 'lib/utils/validation-utils.js'; | ||||
import { prepareEncryptedIOSNotifications } from './crypto.js'; | import { | ||||
prepareEncryptedIOSNotifications, | |||||
prepareEncryptedAndroidNotifications, | |||||
} from './crypto.js'; | |||||
import { getAPNsNotificationTopic } from './providers.js'; | import { getAPNsNotificationTopic } from './providers.js'; | ||||
import { rescindPushNotifs } from './rescind.js'; | import { rescindPushNotifs } from './rescind.js'; | ||||
import type { TargetedAPNsNotification } from './types.js'; | import type { | ||||
TargetedAPNsNotification, | |||||
TargetedAndroidNotification, | |||||
} from './types.js'; | |||||
import { | import { | ||||
apnPush, | apnPush, | ||||
fcmPush, | fcmPush, | ||||
getUnreadCounts, | getUnreadCounts, | ||||
apnMaxNotificationPayloadByteSize, | apnMaxNotificationPayloadByteSize, | ||||
fcmMaxNotificationPayloadByteSize, | fcmMaxNotificationPayloadByteSize, | ||||
wnsMaxNotificationPayloadByteSize, | wnsMaxNotificationPayloadByteSize, | ||||
webPush, | webPush, | ||||
▲ Show 20 Lines • Show All 177 Lines • ▼ Show 20 Lines | for (const notifInfo of usersToCollapsableNotifInfo[userID]) { | ||||
if (androidVersionsToTokens) { | if (androidVersionsToTokens) { | ||||
for (const [codeVersion, devices] of androidVersionsToTokens) { | for (const [codeVersion, devices] of androidVersionsToTokens) { | ||||
const platformDetails = { platform: 'android', codeVersion }; | const platformDetails = { platform: 'android', codeVersion }; | ||||
const shimmedNewRawMessageInfos = shimUnsupportedRawMessageInfos( | const shimmedNewRawMessageInfos = shimUnsupportedRawMessageInfos( | ||||
newRawMessageInfos, | newRawMessageInfos, | ||||
platformDetails, | platformDetails, | ||||
); | ); | ||||
const deliveryPromise = (async () => { | const deliveryPromise = (async () => { | ||||
const notification = await prepareAndroidNotification({ | const targetedNotifications = await prepareAndroidNotification( | ||||
{ | |||||
notifTexts, | notifTexts, | ||||
newRawMessageInfos: shimmedNewRawMessageInfos, | newRawMessageInfos: shimmedNewRawMessageInfos, | ||||
threadID: threadInfo.id, | threadID: threadInfo.id, | ||||
collapseKey: notifInfo.collapseKey, | collapseKey: notifInfo.collapseKey, | ||||
badgeOnly, | badgeOnly, | ||||
unreadCount: unreadCounts[userID], | unreadCount: unreadCounts[userID], | ||||
platformDetails, | platformDetails, | ||||
dbID, | dbID, | ||||
}); | }, | ||||
const deviceTokens = devices.map(({ deviceToken }) => deviceToken); | devices, | ||||
return await sendAndroidNotification(notification, deviceTokens, { | ); | ||||
return await sendAndroidNotification(targetedNotifications, { | |||||
...notificationInfo, | ...notificationInfo, | ||||
codeVersion, | codeVersion, | ||||
}); | }); | ||||
})(); | })(); | ||||
deliveryPromises.push(deliveryPromise); | deliveryPromises.push(deliveryPromise); | ||||
} | } | ||||
} | } | ||||
const webVersionsToTokens = byPlatform.get('web'); | const webVersionsToTokens = byPlatform.get('web'); | ||||
▲ Show 20 Lines • Show All 489 Lines • ▼ Show 20 Lines | type AndroidNotifInputData = { | ||||
+dbID: string, | +dbID: string, | ||||
}; | }; | ||||
const androidNotifInputDataValidator = tShape<AndroidNotifInputData>({ | const androidNotifInputDataValidator = tShape<AndroidNotifInputData>({ | ||||
...apnsNotifInputDataValidator.meta.props, | ...apnsNotifInputDataValidator.meta.props, | ||||
dbID: t.String, | dbID: t.String, | ||||
}); | }); | ||||
async function prepareAndroidNotification( | async function prepareAndroidNotification( | ||||
inputData: AndroidNotifInputData, | inputData: AndroidNotifInputData, | ||||
): Promise<Object> { | devices: $ReadOnlyArray<NotificationTargetDevice>, | ||||
): Promise<$ReadOnlyArray<TargetedAndroidNotification>> { | |||||
const convertedData = validateOutput( | const convertedData = validateOutput( | ||||
inputData.platformDetails, | inputData.platformDetails, | ||||
androidNotifInputDataValidator, | androidNotifInputDataValidator, | ||||
inputData, | inputData, | ||||
); | ); | ||||
const { | const { | ||||
notifTexts, | notifTexts, | ||||
newRawMessageInfos, | newRawMessageInfos, | ||||
threadID, | threadID, | ||||
collapseKey, | collapseKey, | ||||
badgeOnly, | badgeOnly, | ||||
unreadCount, | unreadCount, | ||||
platformDetails: { codeVersion }, | platformDetails: { codeVersion }, | ||||
dbID, | dbID, | ||||
} = convertedData; | } = convertedData; | ||||
const isTextNotification = newRawMessageInfos.every( | |||||
newRawMessageInfo => newRawMessageInfo.type === messageTypes.TEXT, | |||||
); | |||||
const shouldBeEncrypted = | |||||
isTextNotification && !collapseKey && codeVersion && codeVersion > 222; | |||||
const notifID = collapseKey ? collapseKey : dbID; | const notifID = collapseKey ? collapseKey : dbID; | ||||
const { merged, ...rest } = notifTexts; | const { merged, ...rest } = notifTexts; | ||||
const notification = { | const notification = { | ||||
data: { | data: { | ||||
badge: unreadCount.toString(), | badge: unreadCount.toString(), | ||||
...rest, | ...rest, | ||||
threadID, | threadID, | ||||
}, | }, | ||||
Show All 14 Lines | ): Promise<$ReadOnlyArray<TargetedAndroidNotification>> { | ||||
} | } | ||||
const messageInfos = JSON.stringify(newRawMessageInfos); | const messageInfos = JSON.stringify(newRawMessageInfos); | ||||
const copyWithMessageInfos = { | const copyWithMessageInfos = { | ||||
...notification, | ...notification, | ||||
data: { ...notification.data, messageInfos }, | data: { ...notification.data, messageInfos }, | ||||
}; | }; | ||||
const evaluateAndSelectNotification = (notif, notifWithMessageInfos) => { | |||||
if ( | if ( | ||||
Buffer.byteLength(JSON.stringify(copyWithMessageInfos)) <= | Buffer.byteLength(JSON.stringify(notifWithMessageInfos)) <= | ||||
fcmMaxNotificationPayloadByteSize | fcmMaxNotificationPayloadByteSize | ||||
) { | ) { | ||||
return copyWithMessageInfos; | return notifWithMessageInfos; | ||||
} | } | ||||
if ( | if ( | ||||
Buffer.byteLength(JSON.stringify(notification)) > | Buffer.byteLength(JSON.stringify(notif)) > | ||||
fcmMaxNotificationPayloadByteSize | fcmMaxNotificationPayloadByteSize | ||||
) { | ) { | ||||
console.warn( | console.warn( | ||||
`Android notification ${notifID} exceeds size limit, even with messageInfos omitted`, | `Android notification ${notifID} exceeds size limit, even with messageInfos omitted`, | ||||
); | ); | ||||
} | } | ||||
return notification; | return notif; | ||||
}; | |||||
const deviceTokens = devices.map(({ deviceToken }) => deviceToken); | |||||
if (shouldBeEncrypted) { | |||||
const cookieIDs = devices.map(({ cookieID }) => cookieID); | |||||
const [notifications, notificationsWithMessageInfos] = await Promise.all([ | |||||
prepareEncryptedAndroidNotifications(cookieIDs, notification), | |||||
prepareEncryptedAndroidNotifications(cookieIDs, copyWithMessageInfos), | |||||
]); | |||||
return notificationsWithMessageInfos.map((notif, idx) => ({ | |||||
notification: evaluateAndSelectNotification(notifications[idx], notif), | |||||
deviceToken: deviceTokens[idx], | |||||
})); | |||||
} | |||||
const notificationToSend = evaluateAndSelectNotification( | |||||
notification, | |||||
copyWithMessageInfos, | |||||
); | |||||
return deviceTokens.map(deviceToken => ({ | |||||
notification: notificationToSend, | |||||
deviceToken, | |||||
})); | |||||
} | } | ||||
type WebNotifInputData = { | type WebNotifInputData = { | ||||
+notifTexts: ResolvedNotifTexts, | +notifTexts: ResolvedNotifTexts, | ||||
+threadID: string, | +threadID: string, | ||||
+unreadCount: number, | +unreadCount: number, | ||||
+platformDetails: PlatformDetails, | +platformDetails: PlatformDetails, | ||||
}; | }; | ||||
▲ Show 20 Lines • Show All 141 Lines • ▼ Show 20 Lines | type AndroidDelivery = { | ||||
errors?: $ReadOnlyArray<Object>, | errors?: $ReadOnlyArray<Object>, | ||||
}; | }; | ||||
type AndroidResult = { | type AndroidResult = { | ||||
info: NotificationInfo, | info: NotificationInfo, | ||||
delivery: AndroidDelivery, | delivery: AndroidDelivery, | ||||
invalidTokens?: $ReadOnlyArray<string>, | invalidTokens?: $ReadOnlyArray<string>, | ||||
}; | }; | ||||
async function sendAndroidNotification( | async function sendAndroidNotification( | ||||
notification: Object, | targetedNotifications: $ReadOnlyArray<TargetedAndroidNotification>, | ||||
deviceTokens: $ReadOnlyArray<string>, | |||||
notificationInfo: NotificationInfo, | notificationInfo: NotificationInfo, | ||||
): Promise<AndroidResult> { | ): Promise<AndroidResult> { | ||||
const collapseKey = notificationInfo.collapseKey | const collapseKey = notificationInfo.collapseKey | ||||
? notificationInfo.collapseKey | ? notificationInfo.collapseKey | ||||
: null; // for Flow... | : null; // for Flow... | ||||
const { source, codeVersion } = notificationInfo; | const { source, codeVersion } = notificationInfo; | ||||
const response = await fcmPush({ | const response = await fcmPush({ | ||||
notification, | targetedNotifications, | ||||
deviceTokens, | |||||
collapseKey, | collapseKey, | ||||
codeVersion, | codeVersion, | ||||
}); | }); | ||||
const deviceTokens = targetedNotifications.map( | |||||
({ deviceToken }) => deviceToken, | |||||
); | |||||
const androidIDs = response.fcmIDs ? response.fcmIDs : []; | const androidIDs = response.fcmIDs ? response.fcmIDs : []; | ||||
const delivery: AndroidDelivery = { | const delivery: AndroidDelivery = { | ||||
source, | source, | ||||
deviceType: 'android', | deviceType: 'android', | ||||
androidIDs, | androidIDs, | ||||
deviceTokens, | deviceTokens, | ||||
codeVersion, | codeVersion, | ||||
}; | }; | ||||
▲ Show 20 Lines • Show All 229 Lines • ▼ Show 20 Lines | ) { | ||||
const androidVersionsToTokens = byPlatform.get('android'); | const androidVersionsToTokens = byPlatform.get('android'); | ||||
if (androidVersionsToTokens) { | if (androidVersionsToTokens) { | ||||
for (const [codeVersion, deviceInfos] of androidVersionsToTokens) { | for (const [codeVersion, deviceInfos] of androidVersionsToTokens) { | ||||
const notificationData = | const notificationData = | ||||
codeVersion < 69 | codeVersion < 69 | ||||
? { badge: unreadCount.toString() } | ? { badge: unreadCount.toString() } | ||||
: { badge: unreadCount.toString(), badgeOnly: '1' }; | : { badge: unreadCount.toString(), badgeOnly: '1' }; | ||||
const notification = { data: notificationData }; | const notification = { data: notificationData }; | ||||
const deviceTokens = deviceInfos.map(({ deviceToken }) => deviceToken); | const deliveryPromise = (async () => { | ||||
deliveryPromises.push( | const cookieIDs = deviceInfos.map(({ cookieID }) => cookieID); | ||||
sendAndroidNotification(notification, deviceTokens, { | let notificationsArray; | ||||
if (codeVersion > 222) { | |||||
notificationsArray = await prepareEncryptedAndroidNotifications( | |||||
cookieIDs, | |||||
notification, | |||||
); | |||||
} else { | |||||
notificationsArray = cookieIDs.map(() => notification); | |||||
} | |||||
const targetedNotifications = deviceInfos.map( | |||||
({ deviceToken }, idx) => ({ | |||||
deviceToken, | |||||
notification: notificationsArray[idx], | |||||
}), | |||||
); | |||||
return await sendAndroidNotification(targetedNotifications, { | |||||
source, | source, | ||||
dbID, | dbID, | ||||
userID, | userID, | ||||
codeVersion, | codeVersion, | ||||
}), | }); | ||||
); | })(); | ||||
deliveryPromises.push(deliveryPromise); | |||||
} | } | ||||
} | } | ||||
const macosVersionsToTokens = byPlatform.get('macos'); | const macosVersionsToTokens = byPlatform.get('macos'); | ||||
if (macosVersionsToTokens) { | if (macosVersionsToTokens) { | ||||
for (const [codeVersion, deviceInfos] of macosVersionsToTokens) { | for (const [codeVersion, deviceInfos] of macosVersionsToTokens) { | ||||
const notification = new apn.Notification(); | const notification = new apn.Notification(); | ||||
notification.topic = getAPNsNotificationTopic({ | notification.topic = getAPNsNotificationTopic({ | ||||
Show All 25 Lines |