Changeset View
Standalone View
keyserver/src/push/send.js
Show All 33 Lines | |||||
import { getAPNsNotificationTopic } from './providers.js'; | import { getAPNsNotificationTopic } from './providers.js'; | ||||
import { | import { | ||||
apnPush, | apnPush, | ||||
fcmPush, | fcmPush, | ||||
getUnreadCounts, | getUnreadCounts, | ||||
apnMaxNotificationPayloadByteSize, | apnMaxNotificationPayloadByteSize, | ||||
fcmMaxNotificationPayloadByteSize, | fcmMaxNotificationPayloadByteSize, | ||||
webPush, | |||||
} from './utils.js'; | } from './utils.js'; | ||||
import createIDs from '../creators/id-creator.js'; | import createIDs from '../creators/id-creator.js'; | ||||
import { createUpdates } from '../creators/update-creator.js'; | import { createUpdates } from '../creators/update-creator.js'; | ||||
import { dbQuery, SQL, mergeOrConditions } from '../database/database.js'; | import { dbQuery, SQL, mergeOrConditions } from '../database/database.js'; | ||||
import type { CollapsableNotifInfo } from '../fetchers/message-fetchers.js'; | import type { CollapsableNotifInfo } from '../fetchers/message-fetchers.js'; | ||||
import { fetchCollapsableNotifs } from '../fetchers/message-fetchers.js'; | import { fetchCollapsableNotifs } from '../fetchers/message-fetchers.js'; | ||||
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, | ||||
+codeVersion: ?number, | +codeVersion: ?number, | ||||
}; | }; | ||||
type PushUserInfo = { | type PushUserInfo = { | ||||
+devices: Device[], | +devices: Device[], | ||||
+messageInfos: RawMessageInfo[], | +messageInfos: RawMessageInfo[], | ||||
}; | }; | ||||
type Delivery = IOSDelivery | AndroidDelivery | { collapsedInto: string }; | type Delivery = PushDelivery | { collapsedInto: string }; | ||||
type NotificationRow = { | type NotificationRow = { | ||||
+dbID: string, | +dbID: string, | ||||
+userID: string, | +userID: string, | ||||
+threadID?: ?string, | +threadID?: ?string, | ||||
+messageID?: ?string, | +messageID?: ?string, | ||||
+collapseKey?: ?string, | +collapseKey?: ?string, | ||||
+deliveries: Delivery[], | +deliveries: Delivery[], | ||||
}; | }; | ||||
▲ Show 20 Lines • Show All 137 Lines • ▼ Show 20 Lines | for (const notifInfo of usersToCollapsableNotifInfo[userID]) { | ||||
...notificationInfo, | ...notificationInfo, | ||||
codeVersion, | codeVersion, | ||||
}, | }, | ||||
); | ); | ||||
})(); | })(); | ||||
deliveryPromises.push(deliveryPromise); | deliveryPromises.push(deliveryPromise); | ||||
} | } | ||||
} | } | ||||
const webVersionsToTokens = byPlatform.get('web'); | |||||
if (webVersionsToTokens) { | |||||
for (const [codeVersion, deviceTokens] of webVersionsToTokens) { | |||||
const deliveryPromise = (async () => { | |||||
const notification = await prepareWebNotification( | |||||
allMessageInfos, | |||||
threadInfo, | |||||
unreadCounts[userID], | |||||
); | |||||
return await sendWebNotification(notification, [...deviceTokens], { | |||||
...notificationInfo, | |||||
codeVersion, | |||||
}); | |||||
})(); | |||||
deliveryPromises.push(deliveryPromise); | |||||
} | |||||
} | |||||
for (const newMessageInfo of remainingNewMessageInfos) { | for (const newMessageInfo of remainingNewMessageInfos) { | ||||
const newDBID = dbIDs.shift(); | const newDBID = dbIDs.shift(); | ||||
invariant(newDBID, 'should have sufficient DB IDs'); | invariant(newDBID, 'should have sufficient DB IDs'); | ||||
const messageID = newMessageInfo.id; | const messageID = newMessageInfo.id; | ||||
invariant(messageID, 'RawMessageInfo.id should be set on server'); | invariant(messageID, 'RawMessageInfo.id should be set on server'); | ||||
notifications.set(newDBID, { | notifications.set(newDBID, { | ||||
dbID: newDBID, | dbID: newDBID, | ||||
Show All 18 Lines | async function sendPushNotifs(pushInfo: PushInfo) { | ||||
]); | ]); | ||||
await saveNotifResults(deliveryResults, notifications, true); | await saveNotifResults(deliveryResults, notifications, true); | ||||
} | } | ||||
// The results in deliveryResults will be combined with the rows | // The results in deliveryResults will be combined with the rows | ||||
// in rowsToSave and then written to the notifications table | // in rowsToSave and then written to the notifications table | ||||
async function saveNotifResults( | async function saveNotifResults( | ||||
deliveryResults: $ReadOnlyArray<IOSResult | AndroidResult>, | deliveryResults: $ReadOnlyArray<PushResult>, | ||||
inputRowsToSave: Map<string, NotificationRow>, | inputRowsToSave: Map<string, NotificationRow>, | ||||
rescindable: boolean, | rescindable: boolean, | ||||
) { | ) { | ||||
const rowsToSave = new Map(inputRowsToSave); | const rowsToSave = new Map(inputRowsToSave); | ||||
const allInvalidTokens = []; | const allInvalidTokens = []; | ||||
for (const deliveryResult of deliveryResults) { | for (const deliveryResult of deliveryResults) { | ||||
const { info, delivery, invalidTokens } = deliveryResult; | const { info, delivery, invalidTokens } = deliveryResult; | ||||
▲ Show 20 Lines • Show All 332 Lines • ▼ Show 20 Lines | ): Promise<Object> { | ||||
) { | ) { | ||||
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 notification; | ||||
} | } | ||||
async function prepareWebNotification( | |||||
allMessageInfos: MessageInfo[], | |||||
threadInfo: ThreadInfo, | |||||
unreadCount: number, | |||||
): Promise<Object> { | |||||
ashoat: Can we type this more precisely? `Object` is basically `any`. We use it for iOS / Android… | |||||
const id = uuidv4(); | |||||
const { merged, ...rest } = await notifTextsForMessageInfo( | |||||
allMessageInfos, | |||||
threadInfo, | |||||
getENSNames, | |||||
); | |||||
const notification = { | |||||
...rest, | |||||
unreadCount, | |||||
id, | |||||
ashoatUnsubmitted Not Done Inline ActionsWhy do we need this id? I thought it was an iOS-specific thing ashoat: Why do we need this `id`? I thought it was an iOS-specific thing | |||||
michalAuthorUnsubmitted Done Inline ActionsThere's no automatically generated ID for a notif by web push api, so I think it's good to have some id just to keep track of them. But they aren't necessary so I can remove them. michal: There's no automatically generated ID for a notif by web push api, so I think it's good to have… | |||||
threadID: threadInfo.id, | |||||
}; | |||||
return notification; | |||||
} | |||||
type NotificationInfo = | type NotificationInfo = | ||||
| { | | { | ||||
+source: 'new_message', | +source: 'new_message', | ||||
+dbID: string, | +dbID: string, | ||||
+userID: string, | +userID: string, | ||||
+threadID: string, | +threadID: string, | ||||
+messageID: string, | +messageID: string, | ||||
+collapseKey: ?string, | +collapseKey: ?string, | ||||
▲ Show 20 Lines • Show All 41 Lines • ▼ Show 20 Lines | const result: IOSResult = { | ||||
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 PushDelivery = AndroidDelivery | IOSDelivery | 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 All 33 Lines | const result: AndroidResult = { | ||||
delivery, | delivery, | ||||
}; | }; | ||||
if (response.invalidTokens) { | if (response.invalidTokens) { | ||||
result.invalidTokens = response.invalidTokens; | result.invalidTokens = response.invalidTokens; | ||||
} | } | ||||
return result; | return result; | ||||
} | } | ||||
type WebDelivery = { | |||||
source: $PropertyType<NotificationInfo, 'source'>, | |||||
deviceType: 'web', | |||||
deviceTokens: $ReadOnlyArray<string>, | |||||
codeVersion?: number, | |||||
ashoatUnsubmitted Not Done Inline ActionsWhat is the meaning of codeVersion for web? Is this just something for the future after ENG-961 is complete? ashoat: What is the meaning of `codeVersion` for `web`? Is this just something for the future after… | |||||
michalAuthorUnsubmitted Done Inline ActionsYeah, I just wanted it to be consistent with the rest of the notif system for now. Currently, it will be -1 as set by the getDevicesByPlatform function. michal: Yeah, I just wanted it to be consistent with the rest of the notif system for now. Currently… | |||||
ashoatUnsubmitted Not Done Inline ActionsAll new types should always be $ReadOnly. Please keep this in mind, and make sure it's addressed before putting diffs up in the future ashoat: All new types should always be `$ReadOnly`. Please keep this in mind, and make sure it's… | |||||
errors?: $ReadOnlyArray<Object>, | |||||
}; | |||||
type WebResult = { | |||||
info: NotificationInfo, | |||||
delivery: WebDelivery, | |||||
invalidTokens?: $ReadOnlyArray<string>, | |||||
ashoatUnsubmitted Not Done Inline ActionsAll new types should always be $ReadOnly. Please keep this in mind, and make sure it's addressed before putting diffs up in the future ashoat: All new types should always be `$ReadOnly`. Please keep this in mind, and make sure it's… | |||||
}; | |||||
async function sendWebNotification( | |||||
notification: Object, | |||||
ashoatUnsubmitted Not Done Inline ActionsCan we avoid Object? ashoat: Can we avoid `Object`? | |||||
deviceTokens: $ReadOnlyArray<string>, | |||||
notificationInfo: NotificationInfo, | |||||
): Promise<WebResult> { | |||||
const { source, codeVersion } = notificationInfo; | |||||
const response = await webPush({ | |||||
notification, | |||||
deviceTokens, | |||||
}); | |||||
const delivery: WebDelivery = { | |||||
source, | |||||
deviceType: 'web', | |||||
deviceTokens, | |||||
codeVersion, | |||||
}; | |||||
if (response.errors) { | |||||
delivery.errors = response.errors; | |||||
} | |||||
const result: WebResult = { | |||||
info: notificationInfo, | |||||
delivery, | |||||
}; | |||||
if (response.invalidTokens) { | |||||
result.invalidTokens = response.invalidTokens; | |||||
} | |||||
return result; | |||||
} | |||||
type InvalidToken = { | type InvalidToken = { | ||||
+userID: string, | +userID: string, | ||||
+tokens: $ReadOnlyArray<string>, | +tokens: $ReadOnlyArray<string>, | ||||
}; | }; | ||||
async function removeInvalidTokens( | async function removeInvalidTokens( | ||||
invalidTokens: $ReadOnlyArray<InvalidToken>, | invalidTokens: $ReadOnlyArray<InvalidToken>, | ||||
): Promise<void> { | ): Promise<void> { | ||||
const sqlTuples = invalidTokens.map( | const sqlTuples = invalidTokens.map( | ||||
▲ Show 20 Lines • Show All 131 Lines • Show Last 20 Lines |
Can we type this more precisely? Object is basically any. We use it for iOS / Android because we have some untyped libraries there I think (could be wrong), but we should avoid introducing it here