Changeset View
Changeset View
Standalone View
Standalone View
keyserver/src/push/send.js
Show All 22 Lines | import { | ||||
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 } 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 { 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'; | ||||
import type { UserInfo } from 'lib/types/user-types.js'; | import type { UserInfo } from 'lib/types/user-types.js'; | ||||
import { promiseAll } from 'lib/utils/promises.js'; | import { promiseAll } from 'lib/utils/promises.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, | ||||
webPush, | |||||
type WebPushError, | |||||
} 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 153 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, | |||||
unreadCount: unreadCounts[userID], | |||||
notifTargetUserInfo: { | |||||
id: userID, | |||||
username, | |||||
}, | |||||
}); | |||||
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 20 Lines • Show All 43 Lines • ▼ Show 20 Lines | async function sendRescindNotifs(rescindInfo: PushInfo) { | ||||
} | } | ||||
await Promise.all(promises); | await Promise.all(promises); | ||||
} | } | ||||
// 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 359 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; | ||||
} | } | ||||
type WebNotifInputData = { | |||||
+allMessageInfos: MessageInfo[], | |||||
+threadInfo: ThreadInfo, | |||||
+unreadCount: number, | |||||
+notifTargetUserInfo: UserInfo, | |||||
}; | |||||
async function prepareWebNotification( | |||||
inputData: WebNotifInputData, | |||||
): Promise<WebNotification> { | |||||
const { allMessageInfos, threadInfo, unreadCount, notifTargetUserInfo } = | |||||
inputData; | |||||
const id = uuidv4(); | |||||
const { merged, ...rest } = await notifTextsForMessageInfo( | |||||
allMessageInfos, | |||||
threadInfo, | |||||
notifTargetUserInfo, | |||||
getENSNames, | |||||
); | |||||
const notification = { | |||||
...rest, | |||||
unreadCount, | |||||
id, | |||||
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, | |||||
+errors?: $ReadOnlyArray<WebPushError>, | |||||
}; | |||||
type WebResult = { | |||||
+info: NotificationInfo, | |||||
+delivery: WebDelivery, | |||||
+invalidTokens?: $ReadOnlyArray<string>, | |||||
}; | |||||
async function sendWebNotification( | |||||
notification: WebNotification, | |||||
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, | |||||
errors: response.errors, | |||||
}; | |||||
const result: WebResult = { | |||||
info: notificationInfo, | |||||
delivery, | |||||
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 |