diff --git a/keyserver/src/creators/message-creator.js b/keyserver/src/creators/message-creator.js --- a/keyserver/src/creators/message-creator.js +++ b/keyserver/src/creators/message-creator.js @@ -4,13 +4,17 @@ import _pickBy from 'lodash/fp/pickBy.js'; import { permissionLookup } from 'lib/permissions/thread-permissions.js'; +import { + generateNotifUserInfoPromise, + type Device, + type PushUserInfo, +} from 'lib/push/send-utils.js'; import { rawMessageInfoFromMessageData, shimUnsupportedRawMessageInfos, stripLocalIDs, } from 'lib/shared/message-utils.js'; import { pushTypes } from 'lib/shared/messages/message-spec.js'; -import type { PushType } from 'lib/shared/messages/message-spec.js'; import { messageSpecs } from 'lib/shared/messages/message-specs.js'; import { messageTypes } from 'lib/types/message-types-enum.js'; import { @@ -39,7 +43,6 @@ } from '../fetchers/message-fetchers.js'; import { fetchOtherSessionsForViewer } from '../fetchers/session-fetchers.js'; import { fetchServerThreadInfos } from '../fetchers/thread-fetchers.js'; -import type { Device, PushUserInfo } from '../push/send.js'; import { sendPushNotifs, sendRescindNotifs } from '../push/send.js'; import type { Viewer } from '../session/viewer.js'; import { earliestFocusedTimeConsideredExpired } from '../shared/focused-times.js'; @@ -378,13 +381,21 @@ } } if (deviceToken && cookieID) { - thisUserInfo.devices.set(deviceToken, { + let platformDetails = { platform, - deviceToken, - cookieID: cookieID.toString(), codeVersion: versions ? versions.codeVersion : null, stateVersion: versions ? versions.stateVersion : null, - majorDesktopVersion: versions ? versions.majorDesktopVersion : null, + }; + if (versions.majorDesktopVersion) { + platformDetails = { + ...platformDetails, + majorDesktopVersion: versions.majorDesktopVersion, + }; + } + thisUserInfo.devices.set(deviceToken, { + deliveryID: deviceToken, + cryptoID: cookieID.toString(), + platformDetails, }); } thisUserInfo.threadIDs.add(threadID); @@ -432,71 +443,27 @@ continue; } - const generateNotifUserInfoPromise = async (pushType: PushType) => { - const promises: Array< - Promise, - > = []; - - for (const threadID of preUserPushInfo.notFocusedThreadIDs) { - const messageIndices = threadsToMessageIndices.get(threadID); - invariant( - messageIndices, - `indices should exist for thread ${threadID}`, - ); - promises.push( - ...messageIndices.map(async messageIndex => { - const messageInfo = messageInfos[messageIndex]; - const { type } = messageInfo; - if (messageInfo.creatorID === userID) { - // We never send a user notifs about their own activity - return undefined; - } - const { generatesNotifs } = messageSpecs[type]; - const messageData = messageDatas[messageIndex]; - if (!generatesNotifs) { - return undefined; - } - const doesGenerateNotif = await generatesNotifs( - messageInfo, - messageData, - { - notifTargetUserID: userID, - userNotMemberOfSubthreads, - fetchMessageInfoByID: (messageID: string) => - fetchMessageInfoByID(viewer, messageID), - }, - ); - return doesGenerateNotif === pushType - ? { messageInfo, messageData } - : undefined; - }), - ); - } - - const messagesToNotify = await Promise.all(promises); - const filteredMessagesToNotify = messagesToNotify.filter(Boolean); - - if (filteredMessagesToNotify.length === 0) { - return undefined; - } - - return { - devices: userDevices, - messageInfos: filteredMessagesToNotify.map( - ({ messageInfo }) => messageInfo, - ), - messageDatas: filteredMessagesToNotify.map( - ({ messageData }) => messageData, - ), - }; - }; - - const userPushInfoPromise = generateNotifUserInfoPromise(pushTypes.NOTIF); + const userPushInfoPromise = generateNotifUserInfoPromise( + pushTypes.NOTIF, + userDevices, + messageInfos, + messageDatas, + threadsToMessageIndices, + preUserPushInfo.notFocusedThreadIDs, + userNotMemberOfSubthreads, + (messageID: string) => fetchMessageInfoByID(viewer, messageID), + userID, + ); const userRescindInfoPromise = generateNotifUserInfoPromise( pushTypes.RESCIND, + userDevices, + messageInfos, + messageDatas, + threadsToMessageIndices, + preUserPushInfo.notFocusedThreadIDs, + userNotMemberOfSubthreads, + (messageID: string) => fetchMessageInfoByID(viewer, messageID), + userID, ); userPushInfoPromises[userID] = userPushInfoPromise; diff --git a/keyserver/src/fetchers/message-fetchers.js b/keyserver/src/fetchers/message-fetchers.js --- a/keyserver/src/fetchers/message-fetchers.js +++ b/keyserver/src/fetchers/message-fetchers.js @@ -2,6 +2,7 @@ import invariant from 'invariant'; +import type { PushInfo } from 'lib/push/send-utils.js'; import { sortMessageInfoList, shimUnsupportedRawMessageInfos, @@ -47,7 +48,6 @@ } from '../database/database.js'; import { processQueryForSearch } from '../database/search-utils.js'; import type { SQLStatementType } from '../database/types.js'; -import type { PushInfo } from '../push/send.js'; import type { Viewer } from '../session/viewer.js'; import { creationString, diff --git a/keyserver/src/push/send.js b/keyserver/src/push/send.js --- a/keyserver/src/push/send.js +++ b/keyserver/src/push/send.js @@ -19,6 +19,7 @@ createAndroidBadgeOnlyNotification, } from 'lib/push/android-notif-creators.js'; import { apnMaxNotificationPayloadByteSize } from 'lib/push/apns-notif-creators.js'; +import type { Device, PushUserInfo, PushInfo } from 'lib/push/send-utils.js'; import { type WebNotifInputData, webNotifInputDataValidator, @@ -49,7 +50,6 @@ import type { Platform, PlatformDetails } from 'lib/types/device-types.js'; import { messageTypes } from 'lib/types/message-types-enum.js'; import { - type MessageData, type RawMessageInfo, rawMessageInfoValidator, } from 'lib/types/message-types.js'; @@ -94,21 +94,6 @@ import { getFCNames } from '../utils/fc-cache.js'; import { validateOutput } from '../utils/validation-utils.js'; -export type Device = { - +platform: Platform, - +deviceToken: string, - +cookieID: string, - +codeVersion: ?number, - +stateVersion: ?number, - +majorDesktopVersion: ?number, -}; - -export type PushUserInfo = { - +devices: Device[], - // messageInfos and messageDatas have the same key - +messageInfos: RawMessageInfo[], - +messageDatas: MessageData[], -}; type Delivery = PushDelivery | { collapsedInto: string }; type NotificationRow = { +dbID: string, @@ -118,7 +103,6 @@ +collapseKey?: ?string, +deliveries: Delivery[], }; -export type PushInfo = { [userID: string]: PushUserInfo }; async function sendPushNotifs(pushInfo: PushInfo) { if (Object.keys(pushInfo).length === 0) { @@ -873,22 +857,23 @@ Map>, >(); for (const device of devices) { - let innerMap = byPlatform.get(device.platform); + let innerMap = byPlatform.get(device.platformDetails.platform); if (!innerMap) { innerMap = new Map>(); - byPlatform.set(device.platform, innerMap); + byPlatform.set(device.platformDetails.platform, innerMap); } const codeVersion: number = - device.codeVersion !== null && device.codeVersion !== undefined - ? device.codeVersion + device.platformDetails.codeVersion !== null && + device.platformDetails.codeVersion !== undefined + ? device.platformDetails.codeVersion : -1; - const stateVersion: number = device.stateVersion ?? -1; + const stateVersion: number = device.platformDetails.stateVersion ?? -1; let versionsObject = { codeVersion, stateVersion }; - if (device.majorDesktopVersion) { + if (device.platformDetails.majorDesktopVersion) { versionsObject = { ...versionsObject, - majorDesktopVersion: device.majorDesktopVersion, + majorDesktopVersion: device.platformDetails.majorDesktopVersion, }; } @@ -902,8 +887,8 @@ const innerMostArray = innerMostArrayTmp; innerMostArray.push({ - cryptoID: device.cookieID, - deliveryID: device.deviceToken, + cryptoID: device.cryptoID, + deliveryID: device.deliveryID, }); } return byPlatform; diff --git a/lib/push/send-utils.js b/lib/push/send-utils.js new file mode 100644 --- /dev/null +++ b/lib/push/send-utils.js @@ -0,0 +1,250 @@ +// @flow + +import invariant from 'invariant'; +import _pickBy from 'lodash/fp/pickBy.js'; +import uuidv4 from 'uuid/v4.js'; + +import { hasPermission } from '../permissions/minimally-encoded-thread-permissions.js'; +import { rawMessageInfoFromMessageData } from '../shared/message-utils.js'; +import { type PushType, pushTypes } from '../shared/messages/message-spec.js'; +import { messageSpecs } from '../shared/messages/message-specs.js'; +import { isMemberActive } from '../shared/thread-utils.js'; +import type { AuxUserInfos } from '../types/aux-user-types.js'; +import type { PlatformDetails } from '../types/device-types.js'; +import { + identityDeviceTypeToPlatform, + type IdentityPlatformDetails, +} from '../types/identity-service-types.js'; +import { + type MessageData, + type RawMessageInfo, + messageDataLocalID, +} from '../types/message-types.js'; +import type { RawThreadInfos } from '../types/thread-types.js'; +import { promiseAll } from '../utils/promises.js'; + +export type Device = { + +platformDetails: PlatformDetails, + +deliveryID: string, + +cryptoID: string, +}; + +export type PushUserInfo = { + +devices: $ReadOnlyArray, + +messageInfos: RawMessageInfo[], + +messageDatas: MessageData[], +}; + +export type PushInfo = { +[userID: string]: PushUserInfo }; + +type PushUserThreadInfo = { + +devices: $ReadOnlyArray, + +threadIDs: Set, +}; + +function identityPlatformDetailsToPlatformDetails( + identityPlatformDetails: IdentityPlatformDetails, +): PlatformDetails { + const { deviceType, ...rest } = identityPlatformDetails; + return { + ...rest, + platform: identityDeviceTypeToPlatform[deviceType], + }; +} + +async function generateNotifUserInfoPromise( + pushType: PushType, + devices: $ReadOnlyArray, + newMessageInfos: $ReadOnlyArray, + messageDatas: $ReadOnlyArray, + threadsToMessageIndices: $ReadOnlyMap, + threadIDs: $ReadOnlySet, + userNotMemberOfSubthreads: Set, + fetchMessageInfoByID: (messageID: string) => Promise, + userID: string, +): Promise, + messageDatas: Array, + messageInfos: Array, +}> { + const promises: Array< + Promise, + > = []; + + for (const threadID of threadIDs) { + const messageIndices = threadsToMessageIndices.get(threadID); + invariant(messageIndices, `indices should exist for thread ${threadID}`); + + promises.push( + ...messageIndices.map(async messageIndex => { + const messageInfo = newMessageInfos[messageIndex]; + if (messageInfo.creatorID === userID) { + return undefined; + } + + const { type } = messageInfo; + const { generatesNotifs } = messageSpecs[type]; + + if (!generatesNotifs) { + return undefined; + } + + const messageData = messageDatas[messageIndex]; + const doesGenerateNotif = await generatesNotifs( + messageInfo, + messageData, + { + notifTargetUserID: userID, + userNotMemberOfSubthreads, + fetchMessageInfoByID, + }, + ); + + return doesGenerateNotif === pushType + ? { messageInfo, messageData } + : undefined; + }), + ); + } + + const messagesToNotify = await Promise.all(promises); + const filteredMessagesToNotify = messagesToNotify.filter(Boolean); + + if (filteredMessagesToNotify.length === 0) { + return undefined; + } + + return { + devices, + messageInfos: filteredMessagesToNotify.map( + ({ messageInfo }) => messageInfo, + ), + messageDatas: filteredMessagesToNotify.map( + ({ messageData }) => messageData, + ), + }; +} + +async function getPushUserInfo( + messageInfos: { +[id: string]: RawMessageInfo }, + rawThreadInfos: RawThreadInfos, + auxUserInfos: AuxUserInfos, + messageDatas: $ReadOnlyArray, +): Promise<{ + +pushInfos: ?PushInfo, + +rescindInfos: ?PushInfo, +}> { + if (messageDatas.length === 0) { + return { pushInfos: null, rescindInfos: null }; + } + + const threadsToMessageIndices: Map = new Map(); + const newMessageInfos: RawMessageInfo[] = []; + + let nextNewMessageIndex = 0; + for (let i = 0; i < messageDatas.length; i++) { + const messageData = messageDatas[i]; + const threadID = messageData.threadID; + + let messageIndices = threadsToMessageIndices.get(threadID); + if (!messageIndices) { + messageIndices = []; + threadsToMessageIndices.set(threadID, messageIndices); + } + + const newMessageIndex = nextNewMessageIndex++; + messageIndices.push(newMessageIndex); + + const messageID = messageDataLocalID(messageData) ?? uuidv4(); + const rawMessageInfo = rawMessageInfoFromMessageData( + messageData, + messageID, + ); + newMessageInfos.push(rawMessageInfo); + } + + const pushUserThreadInfos: { [userID: string]: PushUserThreadInfo } = {}; + for (const threadID of threadsToMessageIndices.keys()) { + const threadInfo = rawThreadInfos[threadID]; + for (const memberInfo of threadInfo.members) { + if ( + !isMemberActive(memberInfo) || + !hasPermission(memberInfo.permissions, 'visible') + ) { + continue; + } + + if (pushUserThreadInfos[memberInfo.id]) { + pushUserThreadInfos[memberInfo.id].threadIDs.add(threadID); + continue; + } + + const devicesPlatformDetails = + auxUserInfos[memberInfo.id].devicesPlatformDetails; + + if (!devicesPlatformDetails) { + continue; + } + + const devices = Object.entries(devicesPlatformDetails).map( + ([deviceID, identityPlatformDetails]) => ({ + platformDetails: identityPlatformDetailsToPlatformDetails( + identityPlatformDetails, + ), + deliveryID: deviceID, + cryptoID: deviceID, + }), + ); + + pushUserThreadInfos[memberInfo.id] = { + devices, + threadIDs: new Set([threadID]), + }; + } + } + + const userPushInfoPromises: { [string]: Promise } = {}; + const userRescindInfoPromises: { [string]: Promise } = {}; + + for (const userID in pushUserThreadInfos) { + const pushUserThreadInfo = pushUserThreadInfos[userID]; + + userPushInfoPromises[userID] = generateNotifUserInfoPromise( + pushTypes.NOTIF, + pushUserThreadInfo.devices, + newMessageInfos, + messageDatas, + threadsToMessageIndices, + pushUserThreadInfo.threadIDs, + new Set(), + (messageID: string) => (async () => messageInfos[messageID])(), + userID, + ); + userRescindInfoPromises[userID] = generateNotifUserInfoPromise( + pushTypes.RESCIND, + pushUserThreadInfo.devices, + newMessageInfos, + messageDatas, + threadsToMessageIndices, + pushUserThreadInfo.threadIDs, + new Set(), + (messageID: string) => (async () => messageInfos[messageID])(), + userID, + ); + } + + const [pushInfo, rescindInfo] = await Promise.all([ + promiseAll(userPushInfoPromises), + promiseAll(userRescindInfoPromises), + ]); + + return { + pushInfos: _pickBy(Boolean)(pushInfo), + rescindInfos: _pickBy(Boolean)(rescindInfo), + }; +} + +export { getPushUserInfo, generateNotifUserInfoPromise }; diff --git a/lib/shared/thread-utils.js b/lib/shared/thread-utils.js --- a/lib/shared/thread-utils.js +++ b/lib/shared/thread-utils.js @@ -220,6 +220,11 @@ ); } +function isMemberActive(memberInfo: MemberInfoWithPermissions): boolean { + const role = memberInfo.role; + return role !== null && role !== undefined; +} + function threadIsInHome(threadInfo: ?(RawThreadInfo | ThreadInfo)): boolean { return !!(threadInfo && threadInfo.currentUser.subscription.home); } @@ -1716,4 +1721,5 @@ assertAllThreadInfosAreLegacy, useOnScreenEntryEditableThreadInfos, extractMentionedMembers, + isMemberActive, }; diff --git a/lib/types/identity-service-types.js b/lib/types/identity-service-types.js --- a/lib/types/identity-service-types.js +++ b/lib/types/identity-service-types.js @@ -9,6 +9,7 @@ type SignedPrekeys, type OneTimeKeysResultValues, } from './crypto-types.js'; +import { type Platform } from './device-types.js'; import { type OlmSessionInitializationInfo, olmSessionInitializationInfoValidator, @@ -328,6 +329,17 @@ }); export type IdentityDeviceType = $Values; + +export const identityDeviceTypeToPlatform: { + +[identityDeviceType: IdentityDeviceType]: Platform, +} = { + [identityDeviceTypes.WEB]: 'web', + [identityDeviceTypes.ANDROID]: 'android', + [identityDeviceTypes.IOS]: 'ios', + [identityDeviceTypes.WINDOWS]: 'windows', + [identityDeviceTypes.MAC_OS]: 'macos', +}; + export const identityDeviceTypeValidator: TEnums = t.enums.of( values(identityDeviceTypes), );