Changeset View
Changeset View
Standalone View
Standalone View
keyserver/src/push/send.js
Show All 31 Lines | import type { | ||||
WebNotification, | WebNotification, | ||||
WNSNotification, | WNSNotification, | ||||
ResolvedNotifTexts, | ResolvedNotifTexts, | ||||
} from 'lib/types/notif-types.js'; | } 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.js'; | import { updateTypes } from 'lib/types/update-types.js'; | ||||
import { promiseAll } from 'lib/utils/promises.js'; | import { promiseAll } from 'lib/utils/promises.js'; | ||||
import { prepareEncryptedIOSNotifications } from './crypto.js'; | |||||
import { getAPNsNotificationTopic } from './providers.js'; | import { getAPNsNotificationTopic } from './providers.js'; | ||||
import { rescindPushNotifs } from './rescind.js'; | import { rescindPushNotifs } from './rescind.js'; | ||||
import { | import { | ||||
apnPush, | apnPush, | ||||
fcmPush, | fcmPush, | ||||
getUnreadCounts, | getUnreadCounts, | ||||
apnMaxNotificationPayloadByteSize, | apnMaxNotificationPayloadByteSize, | ||||
fcmMaxNotificationPayloadByteSize, | fcmMaxNotificationPayloadByteSize, | ||||
Show All 11 Lines | |||||
import { fetchServerThreadInfos } from '../fetchers/thread-fetchers.js'; | import { fetchServerThreadInfos } from '../fetchers/thread-fetchers.js'; | ||||
import { fetchUserInfos } from '../fetchers/user-fetchers.js'; | import { fetchUserInfos } from '../fetchers/user-fetchers.js'; | ||||
import type { Viewer } from '../session/viewer.js'; | import type { Viewer } from '../session/viewer.js'; | ||||
import { getENSNames } from '../utils/ens-cache.js'; | import { getENSNames } from '../utils/ens-cache.js'; | ||||
type Device = { | type Device = { | ||||
+platform: Platform, | +platform: Platform, | ||||
+deviceToken: string, | +deviceToken: string, | ||||
+cookieID: string, | |||||
+codeVersion: ?number, | +codeVersion: ?number, | ||||
}; | }; | ||||
type PushUserInfo = { | type PushUserInfo = { | ||||
+devices: Device[], | +devices: Device[], | ||||
// messageInfos and messageDatas have the same key | // messageInfos and messageDatas have the same key | ||||
+messageInfos: RawMessageInfo[], | +messageInfos: RawMessageInfo[], | ||||
+messageDatas: MessageData[], | +messageDatas: MessageData[], | ||||
}; | }; | ||||
▲ Show 20 Lines • Show All 110 Lines • ▼ Show 20 Lines | for (const notifInfo of usersToCollapsableNotifInfo[userID]) { | ||||
userID, | userID, | ||||
threadID, | threadID, | ||||
messageID: firstMessageID, | messageID: firstMessageID, | ||||
collapseKey: notifInfo.collapseKey, | collapseKey: notifInfo.collapseKey, | ||||
}; | }; | ||||
const iosVersionsToTokens = byPlatform.get('ios'); | const iosVersionsToTokens = byPlatform.get('ios'); | ||||
if (iosVersionsToTokens) { | if (iosVersionsToTokens) { | ||||
for (const [codeVersion, deviceTokens] of iosVersionsToTokens) { | for (const [ | ||||
codeVersion, | |||||
{ cookieIDs, deviceTokens }, | |||||
] of iosVersionsToTokens) { | |||||
const platformDetails = { platform: 'ios', codeVersion }; | const platformDetails = { platform: 'ios', codeVersion }; | ||||
const shimmedNewRawMessageInfos = shimUnsupportedRawMessageInfos( | const shimmedNewRawMessageInfos = shimUnsupportedRawMessageInfos( | ||||
newRawMessageInfos, | newRawMessageInfos, | ||||
platformDetails, | platformDetails, | ||||
); | ); | ||||
const deliveryPromise = (async () => { | const deliveryPromise = (async () => { | ||||
const notification = await prepareAPNsNotification({ | const notification = await prepareAPNsNotification({ | ||||
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, | ||||
}); | }); | ||||
return await sendAPNsNotification( | return await sendAPNsNotification( | ||||
'ios', | 'ios', | ||||
notification, | notification, | ||||
[...deviceTokens], | [...deviceTokens], | ||||
{ | { | ||||
...notificationInfo, | ...notificationInfo, | ||||
codeVersion, | codeVersion, | ||||
}, | }, | ||||
[...cookieIDs], | |||||
); | ); | ||||
})(); | })(); | ||||
deliveryPromises.push(deliveryPromise); | deliveryPromises.push(deliveryPromise); | ||||
} | } | ||||
} | } | ||||
const androidVersionsToTokens = byPlatform.get('android'); | const androidVersionsToTokens = byPlatform.get('android'); | ||||
if (androidVersionsToTokens) { | if (androidVersionsToTokens) { | ||||
for (const [codeVersion, deviceTokens] of androidVersionsToTokens) { | for (const [codeVersion, { deviceTokens }] 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 notification = await prepareAndroidNotification({ | ||||
notifTexts, | notifTexts, | ||||
Show All 14 Lines | for (const notifInfo of usersToCollapsableNotifInfo[userID]) { | ||||
}, | }, | ||||
); | ); | ||||
})(); | })(); | ||||
deliveryPromises.push(deliveryPromise); | deliveryPromises.push(deliveryPromise); | ||||
} | } | ||||
} | } | ||||
const webVersionsToTokens = byPlatform.get('web'); | const webVersionsToTokens = byPlatform.get('web'); | ||||
if (webVersionsToTokens) { | if (webVersionsToTokens) { | ||||
for (const [codeVersion, deviceTokens] of webVersionsToTokens) { | for (const [codeVersion, { deviceTokens }] of webVersionsToTokens) { | ||||
const deliveryPromise = (async () => { | const deliveryPromise = (async () => { | ||||
const notification = await prepareWebNotification({ | const notification = await prepareWebNotification({ | ||||
notifTexts, | notifTexts, | ||||
threadID: threadInfo.id, | threadID: threadInfo.id, | ||||
unreadCount: unreadCounts[userID], | unreadCount: unreadCounts[userID], | ||||
}); | }); | ||||
return await sendWebNotification(notification, [...deviceTokens], { | return await sendWebNotification(notification, [...deviceTokens], { | ||||
...notificationInfo, | ...notificationInfo, | ||||
codeVersion, | codeVersion, | ||||
}); | }); | ||||
})(); | })(); | ||||
deliveryPromises.push(deliveryPromise); | deliveryPromises.push(deliveryPromise); | ||||
} | } | ||||
} | } | ||||
const macosVersionsToTokens = byPlatform.get('macos'); | const macosVersionsToTokens = byPlatform.get('macos'); | ||||
if (macosVersionsToTokens) { | if (macosVersionsToTokens) { | ||||
for (const [codeVersion, deviceTokens] of macosVersionsToTokens) { | for (const [codeVersion, { deviceTokens }] of macosVersionsToTokens) { | ||||
const platformDetails = { platform: 'macos', codeVersion }; | const platformDetails = { platform: 'macos', codeVersion }; | ||||
const shimmedNewRawMessageInfos = shimUnsupportedRawMessageInfos( | const shimmedNewRawMessageInfos = shimUnsupportedRawMessageInfos( | ||||
newRawMessageInfos, | newRawMessageInfos, | ||||
platformDetails, | platformDetails, | ||||
); | ); | ||||
const deliveryPromise = (async () => { | const deliveryPromise = (async () => { | ||||
const notification = await prepareAPNsNotification({ | const notification = await prepareAPNsNotification({ | ||||
notifTexts, | notifTexts, | ||||
Show All 14 Lines | for (const notifInfo of usersToCollapsableNotifInfo[userID]) { | ||||
}, | }, | ||||
); | ); | ||||
})(); | })(); | ||||
deliveryPromises.push(deliveryPromise); | deliveryPromises.push(deliveryPromise); | ||||
} | } | ||||
} | } | ||||
const windowsVersionsToTokens = byPlatform.get('windows'); | const windowsVersionsToTokens = byPlatform.get('windows'); | ||||
if (windowsVersionsToTokens) { | if (windowsVersionsToTokens) { | ||||
for (const [codeVersion, deviceTokens] of windowsVersionsToTokens) { | for (const [codeVersion, { deviceTokens }] of windowsVersionsToTokens) { | ||||
const deliveryPromise = (async () => { | const deliveryPromise = (async () => { | ||||
const notification = await prepareWNSNotification({ | const notification = await prepareWNSNotification({ | ||||
notifTexts, | notifTexts, | ||||
threadID: threadInfo.id, | threadID: threadInfo.id, | ||||
unreadCount: unreadCounts[userID], | unreadCount: unreadCounts[userID], | ||||
}); | }); | ||||
return await sendWNSNotification(notification, [...deviceTokens], { | return await sendWNSNotification(notification, [...deviceTokens], { | ||||
...notificationInfo, | ...notificationInfo, | ||||
▲ Show 20 Lines • Show All 260 Lines • ▼ Show 20 Lines | async function createDBIDs(pushInfo: PushInfo): Promise<string[]> { | ||||
let numIDsNeeded = 0; | let numIDsNeeded = 0; | ||||
for (const userID in pushInfo) { | for (const userID in pushInfo) { | ||||
numIDsNeeded += pushInfo[userID].messageInfos.length; | numIDsNeeded += pushInfo[userID].messageInfos.length; | ||||
} | } | ||||
return await createIDs('notifications', numIDsNeeded); | return await createIDs('notifications', numIDsNeeded); | ||||
} | } | ||||
function getDevicesByPlatform( | function getDevicesByPlatform( | ||||
devices: Device[], | devices: Device[], | ||||
ashoat: It would be good to update this to `$ReadOnlyArray` | |||||
): Map<Platform, Map<number, Set<string>>> { | ): Map< | ||||
Platform, | |||||
Map<number, { cookieIDs: Set<string>, deviceTokens: Set<string> }>, | |||||
> { | |||||
const byPlatform = new Map(); | const byPlatform = new Map(); | ||||
for (const device of devices) { | for (const device of devices) { | ||||
let innerMap = byPlatform.get(device.platform); | let innerMap = byPlatform.get(device.platform); | ||||
if (!innerMap) { | if (!innerMap) { | ||||
innerMap = new Map(); | innerMap = new Map(); | ||||
byPlatform.set(device.platform, innerMap); | byPlatform.set(device.platform, innerMap); | ||||
} | } | ||||
const codeVersion: number = | const codeVersion: number = | ||||
device.codeVersion !== null && device.codeVersion !== undefined | device.codeVersion !== null && device.codeVersion !== undefined | ||||
? device.codeVersion | ? device.codeVersion | ||||
: -1; | : -1; | ||||
let innerMostSet = innerMap.get(codeVersion); | let innerMostSetPair = innerMap.get(codeVersion); | ||||
ashoatUnsubmitted Not Done Inline ActionsTake Set out here ashoat: Take `Set` out here | |||||
if (!innerMostSet) { | if (!innerMostSetPair) { | ||||
innerMostSet = new Set(); | innerMostSetPair = { cookieIDs: new Set(), deviceTokens: new Set() }; | ||||
innerMap.set(codeVersion, innerMostSet); | innerMap.set(codeVersion, innerMostSetPair); | ||||
} | } | ||||
innerMostSet.add(device.deviceToken); | const { cookieIDs, deviceTokens } = innerMostSetPair; | ||||
cookieIDs.add(device.cookieID); | |||||
deviceTokens.add(device.deviceToken); | |||||
} | } | ||||
return byPlatform; | return byPlatform; | ||||
} | } | ||||
type APNsNotifInputData = { | type APNsNotifInputData = { | ||||
+notifTexts: ResolvedNotifTexts, | +notifTexts: ResolvedNotifTexts, | ||||
+newRawMessageInfos: RawMessageInfo[], | +newRawMessageInfos: RawMessageInfo[], | ||||
+threadID: string, | +threadID: string, | ||||
Show All 33 Lines | ): Promise<apn.Notification> { | ||||
}; | }; | ||||
notification.badge = unreadCount; | notification.badge = unreadCount; | ||||
notification.threadId = threadID; | notification.threadId = threadID; | ||||
notification.id = uniqueID; | notification.id = uniqueID; | ||||
notification.pushType = 'alert'; | notification.pushType = 'alert'; | ||||
notification.payload.id = uniqueID; | notification.payload.id = uniqueID; | ||||
notification.payload.threadID = threadID; | notification.payload.threadID = threadID; | ||||
notification.payload.badge = unreadCount; | |||||
ashoatUnsubmitted Not Done Inline ActionsDon't mutate the payload just to avoid updating Flow types ashoat: Don't mutate the payload just to avoid updating Flow types | |||||
if (platformDetails.codeVersion && platformDetails.codeVersion > 198) { | if (platformDetails.codeVersion && platformDetails.codeVersion > 198) { | ||||
notification.mutableContent = true; | notification.mutableContent = true; | ||||
} | } | ||||
if (collapseKey) { | if (collapseKey) { | ||||
notification.collapseId = collapseKey; | notification.collapseId = collapseKey; | ||||
} | } | ||||
const messageInfos = JSON.stringify(newRawMessageInfos); | const messageInfos = JSON.stringify(newRawMessageInfos); | ||||
▲ Show 20 Lines • Show All 161 Lines • ▼ Show 20 Lines | type APNsResult = { | ||||
delivery: APNsDelivery, | delivery: APNsDelivery, | ||||
invalidTokens?: $ReadOnlyArray<string>, | invalidTokens?: $ReadOnlyArray<string>, | ||||
}; | }; | ||||
async function sendAPNsNotification( | async function sendAPNsNotification( | ||||
platform: 'ios' | 'macos', | platform: 'ios' | 'macos', | ||||
notification: apn.Notification, | notification: apn.Notification, | ||||
deviceTokens: $ReadOnlyArray<string>, | deviceTokens: $ReadOnlyArray<string>, | ||||
notificationInfo: NotificationInfo, | notificationInfo: NotificationInfo, | ||||
cookieIDs?: $ReadOnlyArray<string>, | |||||
): Promise<APNsResult> { | ): Promise<APNsResult> { | ||||
const { source, codeVersion } = notificationInfo; | const { source, codeVersion, collapseKey } = notificationInfo; | ||||
const response = await apnPush({ | |||||
let notifications = []; | |||||
const shouldEncryptNotification = platform === 'ios' && !collapseKey; | |||||
ashoatUnsubmitted Not Done Inline ActionsThe reason for the !collapseKey check is that we want to consider collapsing notifications in the NSE out-of-scope for this month's goal ashoat: The reason for the `!collapseKey` check is that we want to consider collapsing notifications in… | |||||
if (shouldEncryptNotification && cookieIDs) { | |||||
notifications = await prepareEncryptedIOSNotifications( | |||||
ashoatUnsubmitted Not Done Inline ActionsWhy not call this in prepareAPNsNotification? It would be easier to check messageTypes.TEXT there, and it would make more sense to keep preparation / sending separate ashoat: Why not call this in `prepareAPNsNotification`? It would be easier to check `messageTypes.TEXT`… | |||||
cookieIDs, | |||||
notification, | notification, | ||||
); | |||||
} else { | |||||
notifications = [notification]; | |||||
} | |||||
const response = await apnPush({ | |||||
notifications, | |||||
deviceTokens, | deviceTokens, | ||||
platformDetails: { platform, codeVersion }, | platformDetails: { platform, codeVersion }, | ||||
}); | }); | ||||
const delivery: APNsDelivery = { | const delivery: APNsDelivery = { | ||||
source, | source, | ||||
deviceType: platform, | deviceType: platform, | ||||
iosID: notification.id, | iosID: notification.id, | ||||
deviceTokens, | deviceTokens, | ||||
▲ Show 20 Lines • Show All 236 Lines • ▼ Show 20 Lines | const devices = deviceTokenResult.map(row => ({ | ||||
codeVersion: JSON.parse(row.versions)?.codeVersion, | codeVersion: JSON.parse(row.versions)?.codeVersion, | ||||
})); | })); | ||||
const byPlatform = getDevicesByPlatform(devices); | const byPlatform = getDevicesByPlatform(devices); | ||||
const deliveryPromises = []; | const deliveryPromises = []; | ||||
const iosVersionsToTokens = byPlatform.get('ios'); | const iosVersionsToTokens = byPlatform.get('ios'); | ||||
if (iosVersionsToTokens) { | if (iosVersionsToTokens) { | ||||
for (const [codeVersion, deviceTokens] of iosVersionsToTokens) { | for (const [ | ||||
codeVersion, | |||||
{ cookieIDs, deviceTokens }, | |||||
] of iosVersionsToTokens) { | |||||
const notification = new apn.Notification(); | const notification = new apn.Notification(); | ||||
notification.topic = getAPNsNotificationTopic({ | notification.topic = getAPNsNotificationTopic({ | ||||
platform: 'ios', | platform: 'ios', | ||||
codeVersion, | codeVersion, | ||||
}); | }); | ||||
notification.badge = unreadCount; | notification.badge = unreadCount; | ||||
notification.pushType = 'alert'; | notification.pushType = 'alert'; | ||||
deliveryPromises.push( | deliveryPromises.push( | ||||
sendAPNsNotification('ios', notification, [...deviceTokens], { | sendAPNsNotification( | ||||
'ios', | |||||
notification, | |||||
[...deviceTokens], | |||||
{ | |||||
source, | source, | ||||
dbID, | dbID, | ||||
userID, | userID, | ||||
codeVersion, | codeVersion, | ||||
}), | }, | ||||
[...cookieIDs], | |||||
), | |||||
); | ); | ||||
} | } | ||||
} | } | ||||
const androidVersionsToTokens = byPlatform.get('android'); | const androidVersionsToTokens = byPlatform.get('android'); | ||||
if (androidVersionsToTokens) { | if (androidVersionsToTokens) { | ||||
for (const [codeVersion, deviceTokens] of androidVersionsToTokens) { | for (const [codeVersion, { deviceTokens }] 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 }; | ||||
deliveryPromises.push( | deliveryPromises.push( | ||||
sendAndroidNotification(notification, [...deviceTokens], { | sendAndroidNotification(notification, [...deviceTokens], { | ||||
source, | source, | ||||
dbID, | dbID, | ||||
userID, | userID, | ||||
codeVersion, | codeVersion, | ||||
}), | }), | ||||
); | ); | ||||
} | } | ||||
} | } | ||||
const macosVersionsToTokens = byPlatform.get('macos'); | const macosVersionsToTokens = byPlatform.get('macos'); | ||||
if (macosVersionsToTokens) { | if (macosVersionsToTokens) { | ||||
for (const [codeVersion, deviceTokens] of macosVersionsToTokens) { | for (const [codeVersion, { deviceTokens }] of macosVersionsToTokens) { | ||||
const notification = new apn.Notification(); | const notification = new apn.Notification(); | ||||
notification.topic = getAPNsNotificationTopic({ | notification.topic = getAPNsNotificationTopic({ | ||||
platform: 'macos', | platform: 'macos', | ||||
codeVersion, | codeVersion, | ||||
}); | }); | ||||
notification.badge = unreadCount; | notification.badge = unreadCount; | ||||
notification.pushType = 'alert'; | notification.pushType = 'alert'; | ||||
deliveryPromises.push( | deliveryPromises.push( | ||||
Show All 15 Lines |
It would be good to update this to $ReadOnlyArray