Changeset View
Changeset View
Standalone View
Standalone View
keyserver/src/push/send.js
Show All 16 Lines | import { | ||||
shimUnsupportedRawMessageInfos, | shimUnsupportedRawMessageInfos, | ||||
} from 'lib/shared/message-utils.js'; | } from 'lib/shared/message-utils.js'; | ||||
import { messageSpecs } from 'lib/shared/messages/message-specs.js'; | import { messageSpecs } from 'lib/shared/messages/message-specs.js'; | ||||
import { notifTextsForMessageInfo } from 'lib/shared/notif-utils.js'; | import { notifTextsForMessageInfo } from 'lib/shared/notif-utils.js'; | ||||
import { | import { | ||||
rawThreadInfoFromServerThreadInfo, | rawThreadInfoFromServerThreadInfo, | ||||
threadInfoFromRawThreadInfo, | threadInfoFromRawThreadInfo, | ||||
} from 'lib/shared/thread-utils.js'; | } from 'lib/shared/thread-utils.js'; | ||||
import type { Platform } from 'lib/types/device-types.js'; | import type { Platform, PlatformDetails } from 'lib/types/device-types.js'; | ||||
import { | import { | ||||
type RawMessageInfo, | type RawMessageInfo, | ||||
type MessageInfo, | type MessageInfo, | ||||
messageTypes, | messageTypes, | ||||
} from 'lib/types/message-types.js'; | } from 'lib/types/message-types.js'; | ||||
import type { WebNotification } from 'lib/types/notif-types.js'; | import type { WebNotification } from 'lib/types/notif-types.js'; | ||||
import type { ServerThreadInfo, ThreadInfo } from 'lib/types/thread-types.js'; | import type { ServerThreadInfo, ThreadInfo } from 'lib/types/thread-types.js'; | ||||
import { updateTypes } from 'lib/types/update-types.js'; | import { updateTypes } from 'lib/types/update-types.js'; | ||||
▲ Show 20 Lines • Show All 134 Lines • ▼ Show 20 Lines | for (const notifInfo of usersToCollapsableNotifInfo[userID]) { | ||||
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 [codeVer, deviceTokens] of iosVersionsToTokens) { | for (const [codeVer, deviceTokens] of iosVersionsToTokens) { | ||||
const codeVersion = parseInt(codeVer, 10); // only for Flow | const codeVersion = parseInt(codeVer, 10); // only for Flow | ||||
const platformDetails = { platform: 'ios', codeVersion }; | |||||
const shimmedNewRawMessageInfos = shimUnsupportedRawMessageInfos( | const shimmedNewRawMessageInfos = shimUnsupportedRawMessageInfos( | ||||
newRawMessageInfos, | newRawMessageInfos, | ||||
{ platform: 'ios', codeVersion }, | platformDetails, | ||||
); | ); | ||||
const deliveryPromise = (async () => { | const deliveryPromise = (async () => { | ||||
const notification = await prepareIOSNotification({ | const notification = await prepareAPNsNotification({ | ||||
allMessageInfos, | allMessageInfos, | ||||
newRawMessageInfos: shimmedNewRawMessageInfos, | newRawMessageInfos: shimmedNewRawMessageInfos, | ||||
threadInfo, | threadInfo, | ||||
collapseKey: notifInfo.collapseKey, | collapseKey: notifInfo.collapseKey, | ||||
badgeOnly, | badgeOnly, | ||||
unreadCount: unreadCounts[userID], | unreadCount: unreadCounts[userID], | ||||
codeVersion, | platformDetails, | ||||
notifTargetUserInfo: { | notifTargetUserInfo: { | ||||
id: userID, | id: userID, | ||||
username, | username, | ||||
}, | }, | ||||
}); | }); | ||||
return await sendIOSNotification(notification, [...deviceTokens], { | return await sendAPNsNotification( | ||||
'ios', | |||||
notification, | |||||
[...deviceTokens], | |||||
{ | |||||
...notificationInfo, | ...notificationInfo, | ||||
codeVersion, | codeVersion, | ||||
}); | }, | ||||
); | |||||
})(); | })(); | ||||
deliveryPromises.push(deliveryPromise); | deliveryPromises.push(deliveryPromise); | ||||
} | } | ||||
} | } | ||||
const androidVersionsToTokens = byPlatform.get('android'); | const androidVersionsToTokens = byPlatform.get('android'); | ||||
if (androidVersionsToTokens) { | if (androidVersionsToTokens) { | ||||
for (const [codeVer, deviceTokens] of androidVersionsToTokens) { | for (const [codeVer, deviceTokens] of androidVersionsToTokens) { | ||||
const codeVersion = parseInt(codeVer, 10); // only for Flow | const codeVersion = parseInt(codeVer, 10); // only for Flow | ||||
const platformDetails = { platform: 'android', codeVersion }; | |||||
const shimmedNewRawMessageInfos = shimUnsupportedRawMessageInfos( | const shimmedNewRawMessageInfos = shimUnsupportedRawMessageInfos( | ||||
newRawMessageInfos, | newRawMessageInfos, | ||||
{ platform: 'android', codeVersion }, | platformDetails, | ||||
); | ); | ||||
const deliveryPromise = (async () => { | const deliveryPromise = (async () => { | ||||
const notification = await prepareAndroidNotification({ | const notification = await prepareAndroidNotification({ | ||||
allMessageInfos, | allMessageInfos, | ||||
newRawMessageInfos: shimmedNewRawMessageInfos, | newRawMessageInfos: shimmedNewRawMessageInfos, | ||||
threadInfo, | threadInfo, | ||||
collapseKey: notifInfo.collapseKey, | collapseKey: notifInfo.collapseKey, | ||||
badgeOnly, | badgeOnly, | ||||
unreadCount: unreadCounts[userID], | unreadCount: unreadCounts[userID], | ||||
codeVersion, | platformDetails, | ||||
notifTargetUserInfo: { | notifTargetUserInfo: { | ||||
id: userID, | id: userID, | ||||
username, | username, | ||||
}, | }, | ||||
dbID, | dbID, | ||||
}); | }); | ||||
return await sendAndroidNotification( | return await sendAndroidNotification( | ||||
notification, | notification, | ||||
▲ Show 20 Lines • Show All 308 Lines • ▼ Show 20 Lines | if (!innerMostSet) { | ||||
innerMostSet = new Set(); | innerMostSet = new Set(); | ||||
innerMap.set(codeVersion, innerMostSet); | innerMap.set(codeVersion, innerMostSet); | ||||
} | } | ||||
innerMostSet.add(device.deviceToken); | innerMostSet.add(device.deviceToken); | ||||
} | } | ||||
return byPlatform; | return byPlatform; | ||||
} | } | ||||
type IOSNotifInputData = { | type APNsNotifInputData = { | ||||
+allMessageInfos: MessageInfo[], | +allMessageInfos: MessageInfo[], | ||||
+newRawMessageInfos: RawMessageInfo[], | +newRawMessageInfos: RawMessageInfo[], | ||||
+threadInfo: ThreadInfo, | +threadInfo: ThreadInfo, | ||||
+collapseKey: ?string, | +collapseKey: ?string, | ||||
+badgeOnly: boolean, | +badgeOnly: boolean, | ||||
+unreadCount: number, | +unreadCount: number, | ||||
+codeVersion: number, | +platformDetails: PlatformDetails, | ||||
+notifTargetUserInfo: UserInfo, | +notifTargetUserInfo: UserInfo, | ||||
}; | }; | ||||
async function prepareIOSNotification( | async function prepareAPNsNotification( | ||||
inputData: IOSNotifInputData, | inputData: APNsNotifInputData, | ||||
): Promise<apn.Notification> { | ): Promise<apn.Notification> { | ||||
const { | const { | ||||
allMessageInfos, | allMessageInfos, | ||||
newRawMessageInfos, | newRawMessageInfos, | ||||
threadInfo, | threadInfo, | ||||
collapseKey, | collapseKey, | ||||
badgeOnly, | badgeOnly, | ||||
unreadCount, | unreadCount, | ||||
codeVersion, | platformDetails, | ||||
notifTargetUserInfo, | notifTargetUserInfo, | ||||
} = inputData; | } = inputData; | ||||
const uniqueID = uuidv4(); | const uniqueID = uuidv4(); | ||||
const notification = new apn.Notification(); | const notification = new apn.Notification(); | ||||
notification.topic = getAPNsNotificationTopic(codeVersion); | notification.topic = getAPNsNotificationTopic(platformDetails); | ||||
const { merged, ...rest } = await notifTextsForMessageInfo( | const { merged, ...rest } = await notifTextsForMessageInfo( | ||||
allMessageInfos, | allMessageInfos, | ||||
threadInfo, | threadInfo, | ||||
notifTargetUserInfo, | notifTargetUserInfo, | ||||
getENSNames, | getENSNames, | ||||
); | ); | ||||
if (!badgeOnly) { | if (!badgeOnly) { | ||||
notification.body = merged; | notification.body = merged; | ||||
notification.sound = 'default'; | notification.sound = 'default'; | ||||
} | } | ||||
notification.payload = { | notification.payload = { | ||||
...notification.payload, | ...notification.payload, | ||||
...rest, | ...rest, | ||||
}; | }; | ||||
notification.badge = unreadCount; | notification.badge = unreadCount; | ||||
notification.threadId = threadInfo.id; | notification.threadId = threadInfo.id; | ||||
notification.id = uniqueID; | notification.id = uniqueID; | ||||
notification.pushType = 'alert'; | notification.pushType = 'alert'; | ||||
notification.payload.id = uniqueID; | notification.payload.id = uniqueID; | ||||
notification.payload.threadID = threadInfo.id; | notification.payload.threadID = threadInfo.id; | ||||
if (codeVersion > 1000) { | if (platformDetails.codeVersion && platformDetails.codeVersion > 1000) { | ||||
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); | ||||
// We make a copy before checking notification's length, because calling | // We make a copy before checking notification's length, because calling | ||||
// length compiles the notification and makes it immutable. Further | // length compiles the notification and makes it immutable. Further | ||||
// changes to its properties won't be reflected in the final plaintext | // changes to its properties won't be reflected in the final plaintext | ||||
// data that is sent. | // data that is sent. | ||||
const copyWithMessageInfos = _cloneDeep(notification); | const copyWithMessageInfos = _cloneDeep(notification); | ||||
copyWithMessageInfos.payload = { | copyWithMessageInfos.payload = { | ||||
...copyWithMessageInfos.payload, | ...copyWithMessageInfos.payload, | ||||
messageInfos, | messageInfos, | ||||
}; | }; | ||||
if (copyWithMessageInfos.length() <= apnMaxNotificationPayloadByteSize) { | if (copyWithMessageInfos.length() <= apnMaxNotificationPayloadByteSize) { | ||||
notification.payload.messageInfos = messageInfos; | notification.payload.messageInfos = messageInfos; | ||||
return notification; | return notification; | ||||
} | } | ||||
const notificationCopy = _cloneDeep(notification); | const notificationCopy = _cloneDeep(notification); | ||||
if (notificationCopy.length() > apnMaxNotificationPayloadByteSize) { | if (notificationCopy.length() > apnMaxNotificationPayloadByteSize) { | ||||
console.warn( | console.warn( | ||||
`iOS notification ${uniqueID} exceeds size limit, even with messageInfos omitted`, | `${platformDetails.platform} notification ${uniqueID} ` + | ||||
`exceeds size limit, even with messageInfos omitted`, | |||||
); | ); | ||||
} | } | ||||
return notification; | return notification; | ||||
} | } | ||||
type AndroidNotifInputData = { | type AndroidNotifInputData = { | ||||
...IOSNotifInputData, | ...APNsNotifInputData, | ||||
+dbID: string, | +dbID: string, | ||||
}; | }; | ||||
async function prepareAndroidNotification( | async function prepareAndroidNotification( | ||||
inputData: AndroidNotifInputData, | inputData: AndroidNotifInputData, | ||||
): Promise<Object> { | ): Promise<Object> { | ||||
const { | const { | ||||
allMessageInfos, | allMessageInfos, | ||||
newRawMessageInfos, | newRawMessageInfos, | ||||
threadInfo, | threadInfo, | ||||
collapseKey, | collapseKey, | ||||
badgeOnly, | badgeOnly, | ||||
unreadCount, | unreadCount, | ||||
codeVersion, | platformDetails: { codeVersion }, | ||||
notifTargetUserInfo, | notifTargetUserInfo, | ||||
dbID, | dbID, | ||||
} = inputData; | } = inputData; | ||||
const notifID = collapseKey ? collapseKey : dbID; | const notifID = collapseKey ? collapseKey : dbID; | ||||
const { merged, ...rest } = await notifTextsForMessageInfo( | const { merged, ...rest } = await notifTextsForMessageInfo( | ||||
allMessageInfos, | allMessageInfos, | ||||
threadInfo, | threadInfo, | ||||
Show All 9 Lines | ): Promise<Object> { | ||||
}; | }; | ||||
// The reason we only include `badgeOnly` for newer clients is because older | // The reason we only include `badgeOnly` for newer clients is because older | ||||
// clients don't know how to parse it. The reason we only include `id` for | // clients don't know how to parse it. The reason we only include `id` for | ||||
// newer clients is that if the older clients see that field, they assume | // newer clients is that if the older clients see that field, they assume | ||||
// the notif has a full payload, and then crash when trying to parse it. | // the notif has a full payload, and then crash when trying to parse it. | ||||
// By skipping `id` we allow old clients to still handle in-app notifs and | // By skipping `id` we allow old clients to still handle in-app notifs and | ||||
// badge updating. | // badge updating. | ||||
if (!badgeOnly || codeVersion >= 69) { | if (!badgeOnly || (codeVersion && codeVersion >= 69)) { | ||||
notification.data = { | notification.data = { | ||||
...notification.data, | ...notification.data, | ||||
id: notifID, | id: notifID, | ||||
badgeOnly: badgeOnly ? '1' : '0', | badgeOnly: badgeOnly ? '1' : '0', | ||||
}; | }; | ||||
} | } | ||||
const messageInfos = JSON.stringify(newRawMessageInfos); | const messageInfos = JSON.stringify(newRawMessageInfos); | ||||
▲ Show 20 Lines • Show All 59 Lines • ▼ Show 20 Lines | | { | ||||
} | } | ||||
| { | | { | ||||
+source: 'mark_as_unread' | 'mark_as_read' | 'activity_update', | +source: 'mark_as_unread' | 'mark_as_read' | 'activity_update', | ||||
+dbID: string, | +dbID: string, | ||||
+userID: string, | +userID: string, | ||||
+codeVersion: number, | +codeVersion: number, | ||||
}; | }; | ||||
type IOSDelivery = { | type APNsDelivery = { | ||||
source: $PropertyType<NotificationInfo, 'source'>, | source: $PropertyType<NotificationInfo, 'source'>, | ||||
deviceType: 'ios', | deviceType: 'ios' | 'macos', | ||||
iosID: string, | iosID: string, | ||||
deviceTokens: $ReadOnlyArray<string>, | deviceTokens: $ReadOnlyArray<string>, | ||||
codeVersion: number, | codeVersion: number, | ||||
errors?: $ReadOnlyArray<ResponseFailure>, | errors?: $ReadOnlyArray<ResponseFailure>, | ||||
}; | }; | ||||
type IOSResult = { | type APNsResult = { | ||||
info: NotificationInfo, | info: NotificationInfo, | ||||
delivery: IOSDelivery, | delivery: APNsDelivery, | ||||
invalidTokens?: $ReadOnlyArray<string>, | invalidTokens?: $ReadOnlyArray<string>, | ||||
}; | }; | ||||
async function sendIOSNotification( | async function sendAPNsNotification( | ||||
platform: 'ios' | 'macos', | |||||
notification: apn.Notification, | notification: apn.Notification, | ||||
deviceTokens: $ReadOnlyArray<string>, | deviceTokens: $ReadOnlyArray<string>, | ||||
notificationInfo: NotificationInfo, | notificationInfo: NotificationInfo, | ||||
): Promise<IOSResult> { | ): Promise<APNsResult> { | ||||
const { source, codeVersion } = notificationInfo; | const { source, codeVersion } = notificationInfo; | ||||
const response = await apnPush({ notification, deviceTokens, codeVersion }); | const response = await apnPush({ | ||||
const delivery: IOSDelivery = { | notification, | ||||
deviceTokens, | |||||
platformDetails: { platform, codeVersion }, | |||||
}); | |||||
const delivery: APNsDelivery = { | |||||
source, | source, | ||||
deviceType: 'ios', | deviceType: platform, | ||||
iosID: notification.id, | iosID: notification.id, | ||||
deviceTokens, | deviceTokens, | ||||
codeVersion, | codeVersion, | ||||
}; | }; | ||||
if (response.errors) { | if (response.errors) { | ||||
delivery.errors = response.errors; | delivery.errors = response.errors; | ||||
} | } | ||||
const result: IOSResult = { | const result: APNsResult = { | ||||
info: notificationInfo, | info: notificationInfo, | ||||
delivery, | delivery, | ||||
}; | }; | ||||
if (response.invalidTokens) { | if (response.invalidTokens) { | ||||
result.invalidTokens = response.invalidTokens; | result.invalidTokens = response.invalidTokens; | ||||
} | } | ||||
return result; | return result; | ||||
} | } | ||||
type PushResult = AndroidResult | IOSResult | WebResult; | type PushResult = AndroidResult | APNsResult | WebResult; | ||||
type PushDelivery = AndroidDelivery | IOSDelivery | WebDelivery; | type PushDelivery = AndroidDelivery | APNsDelivery | WebDelivery; | ||||
type AndroidDelivery = { | type AndroidDelivery = { | ||||
source: $PropertyType<NotificationInfo, 'source'>, | source: $PropertyType<NotificationInfo, 'source'>, | ||||
deviceType: 'android', | deviceType: 'android', | ||||
androidIDs: $ReadOnlyArray<string>, | androidIDs: $ReadOnlyArray<string>, | ||||
deviceTokens: $ReadOnlyArray<string>, | deviceTokens: $ReadOnlyArray<string>, | ||||
codeVersion: number, | codeVersion: number, | ||||
errors?: $ReadOnlyArray<Object>, | errors?: $ReadOnlyArray<Object>, | ||||
}; | }; | ||||
▲ Show 20 Lines • Show All 172 Lines • ▼ Show 20 Lines | ) { | ||||
const deliveryPromises = []; | const deliveryPromises = []; | ||||
const iosVersionsToTokens = byPlatform.get('ios'); | const iosVersionsToTokens = byPlatform.get('ios'); | ||||
if (iosVersionsToTokens) { | if (iosVersionsToTokens) { | ||||
for (const [codeVer, deviceTokens] of iosVersionsToTokens) { | for (const [codeVer, deviceTokens] of iosVersionsToTokens) { | ||||
const codeVersion = parseInt(codeVer, 10); // only for Flow | const codeVersion = parseInt(codeVer, 10); // only for Flow | ||||
const notification = new apn.Notification(); | const notification = new apn.Notification(); | ||||
notification.topic = getAPNsNotificationTopic(codeVersion); | notification.topic = getAPNsNotificationTopic({ | ||||
platform: 'ios', | |||||
codeVersion, | |||||
}); | |||||
notification.badge = unreadCount; | notification.badge = unreadCount; | ||||
notification.pushType = 'alert'; | notification.pushType = 'alert'; | ||||
deliveryPromises.push( | deliveryPromises.push( | ||||
sendIOSNotification(notification, [...deviceTokens], { | sendAPNsNotification('ios', notification, [...deviceTokens], { | ||||
source, | source, | ||||
dbID, | dbID, | ||||
userID, | userID, | ||||
codeVersion, | codeVersion, | ||||
}), | }), | ||||
); | ); | ||||
} | } | ||||
} | } | ||||
Show All 26 Lines |