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,13 @@ import _pickBy from 'lodash/fp/pickBy.js'; import { permissionLookup } from 'lib/permissions/thread-permissions.js'; +import { generateNotifUserInfoPromise } 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 { @@ -432,71 +432,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/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,252 @@ +// @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'; + +type PushUserInfo = { + +devices: $ReadOnlyArray<{ + +platformDetails: PlatformDetails, + +deliveryID: string, + +cryptoID: string, + }>, + +messageInfos: RawMessageInfo[], + +messageDatas: MessageData[], +}; + +type PushInfo = { +[userID: string]: PushUserInfo }; + +type PushUserThreadInfo = { + +devices: $ReadOnlyArray<{ + +platformDetails: PlatformDetails, + +deliveryID: string, + +cryptoID: string, + }>, + +threadIDs: Set, +}; + +function identityPlatformDetailsToPlatformDetails( + identityPlatformDetails: IdentityPlatformDetails, +): PlatformDetails { + const { deviceType, ...rest } = identityPlatformDetails; + return { + ...rest, + platform: identityDeviceTypeToPlatform[deviceType], + }; +} + +async function generateNotifUserInfoPromise( + pushType: PushType, + devices: T, + newMessageInfos: $ReadOnlyArray, + messageDatas: $ReadOnlyArray, + threadsToMessageIndices: $ReadOnlyMap, + threadIDs: $ReadOnlySet, + userNotMemberOfSubthreads: Set, + fetchMessageInfoByID: (messageID: string) => Promise, + userID: string, +): Promise, + 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 @@ -219,6 +219,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); } @@ -1711,4 +1716,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, @@ -327,6 +328,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), );