2023-01-03 19:55:46 +00:00
// Copyright 2020 Signal Messenger, LLC
2020-10-30 20:34:04 +00:00
// SPDX-License-Identifier: AGPL-3.0-only
2020-09-09 02:25:05 +00:00
import {
compact ,
2020-11-20 17:30:45 +00:00
difference ,
2020-09-09 02:25:05 +00:00
flatten ,
fromPairs ,
isNumber ,
values ,
} from 'lodash' ;
2022-03-23 20:49:27 +00:00
import Long from 'long' ;
2022-03-24 21:47:21 +00:00
import type { ClientZkGroupCipher } from '@signalapp/libsignal-client/zkgroup' ;
2020-09-11 19:37:01 +00:00
import { v4 as getGuid } from 'uuid' ;
2021-04-09 18:20:37 +00:00
import LRU from 'lru-cache' ;
2021-09-17 18:27:53 +00:00
import * as log from './logging/log' ;
2020-09-09 02:25:05 +00:00
import {
2024-02-22 21:19:50 +00:00
getCheckedGroupCredentialsForToday ,
2020-09-09 02:25:05 +00:00
maybeFetchNewCredentials ,
} from './services/groupCredentialFetcher' ;
2022-10-08 00:19:02 +00:00
import { storageServiceUploadJob } from './services/storage' ;
2024-07-22 18:16:33 +00:00
import { DataReader , DataWriter } from './sql/Client' ;
2021-01-29 22:16:48 +00:00
import { toWebSafeBase64 , fromWebSafeBase64 } from './util/webSafeBase64' ;
2022-09-15 19:17:15 +00:00
import { assertDev , strictAssert } from './util/assert' ;
2021-05-07 20:07:24 +00:00
import { isMoreRecentThan } from './util/timestamp' ;
2023-09-25 13:43:54 +00:00
import { MINUTE , DurationInSeconds , SECOND } from './util/durations' ;
2023-10-02 20:19:55 +00:00
import { drop } from './util/drop' ;
2021-06-22 14:46:42 +00:00
import { dropNull } from './util/dropNull' ;
2021-10-26 19:15:33 +00:00
import type {
2020-09-09 02:25:05 +00:00
ConversationAttributesType ,
GroupV2MemberType ,
2020-12-18 19:27:43 +00:00
GroupV2PendingAdminApprovalType ,
2020-09-09 02:25:05 +00:00
GroupV2PendingMemberType ,
2022-03-23 22:34:51 +00:00
GroupV2BannedMemberType ,
2020-09-09 02:25:05 +00:00
MessageAttributesType ,
} from './model-types.d' ;
import {
2020-10-06 17:06:34 +00:00
createProfileKeyCredentialPresentation ,
2022-07-08 20:46:25 +00:00
decodeProfileKeyCredentialPresentation ,
2020-09-09 02:25:05 +00:00
decryptGroupBlob ,
decryptProfileKey ,
2023-08-16 20:54:39 +00:00
decryptAci ,
decryptPni ,
decryptServiceId ,
2020-09-09 02:25:05 +00:00
deriveGroupID ,
deriveGroupPublicParams ,
deriveGroupSecretParams ,
encryptGroupBlob ,
2023-08-10 16:43:33 +00:00
encryptServiceId ,
2020-09-09 02:25:05 +00:00
getAuthCredentialPresentation ,
getClientZkAuthOperations ,
getClientZkGroupCipher ,
2020-10-06 17:06:34 +00:00
getClientZkProfileOperations ,
2022-03-08 21:07:52 +00:00
verifyNotarySignature ,
2020-09-09 02:25:05 +00:00
} from './util/zkgroup' ;
import {
computeHash ,
2020-11-20 17:30:45 +00:00
deriveMasterKeyFromGroupV1 ,
2021-01-29 21:19:24 +00:00
getRandomBytes ,
2020-09-09 02:25:05 +00:00
} from './Crypto' ;
2021-10-26 19:15:33 +00:00
import type {
2020-11-20 17:30:45 +00:00
GroupCredentialsType ,
GroupLogResponseType ,
} from './textsecure/WebAPI' ;
2022-07-18 20:05:41 +00:00
import { HTTPError } from './textsecure/Errors' ;
2021-10-26 19:15:33 +00:00
import type MessageSender from './textsecure/SendMessage' ;
2022-06-10 01:10:20 +00:00
import { CURRENT_SCHEMA_VERSION as MAX_MESSAGE_SCHEMA } from './types/Message2' ;
2021-10-26 19:15:33 +00:00
import type { ConversationModel } from './models/conversations' ;
2021-03-03 20:09:58 +00:00
import { getGroupSizeHardLimit } from './groups/limits' ;
2021-06-07 16:39:13 +00:00
import {
isGroupV1 as getIsGroupV1 ,
isGroupV2 as getIsGroupV2 ,
2023-06-02 17:54:36 +00:00
isGroupV2 ,
2021-06-07 16:39:13 +00:00
isMe ,
} from './util/whatTypeOfConversation' ;
2021-06-22 14:46:42 +00:00
import * as Bytes from './Bytes' ;
2021-10-26 19:15:33 +00:00
import type { AvatarDataType } from './types/Avatar' ;
2023-08-10 16:43:33 +00:00
import type { ServiceIdString , AciString , PniString } from './types/ServiceId' ;
import {
ServiceIdKind ,
isPniString ,
isServiceIdString ,
} from './types/ServiceId' ;
2023-09-14 17:04:48 +00:00
import { isAciString } from './util/isAciString' ;
2022-03-08 21:07:52 +00:00
import * as Errors from './types/errors' ;
2021-06-22 14:46:42 +00:00
import { SignalService as Proto } from './protobuf' ;
2022-03-15 01:32:07 +00:00
import { isNotNil } from './util/isNotNil' ;
import { isAccessControlEnabled } from './groups/util' ;
2022-02-16 18:36:21 +00:00
import {
conversationJobQueue ,
conversationQueueJobEnum ,
} from './jobs/conversationJobQueue' ;
2022-05-12 02:45:20 +00:00
import { ReadStatus } from './messages/MessageReadStatus' ;
import { SeenStatus } from './MessageSeenStatus' ;
2023-04-11 03:54:43 +00:00
import { incrementMessageCounter } from './util/incrementMessageCounter' ;
2023-09-25 13:43:54 +00:00
import { sleep } from './util/sleep' ;
2023-11-02 19:42:31 +00:00
import { groupInvitesRoute } from './util/signalRoutes' ;
2024-09-06 17:52:19 +00:00
import {
decodeGroupSendEndorsementResponse ,
isValidGroupSendEndorsementsExpiration ,
} from './util/groupSendEndorsements' ;
2022-02-16 18:36:21 +00:00
2022-03-15 01:32:07 +00:00
type AccessRequiredEnum = Proto . AccessControl . AccessRequired ;
2020-09-09 02:25:05 +00:00
2021-01-29 22:16:48 +00:00
export { joinViaLink } from './groups/joinViaLink' ;
2021-07-20 20:18:35 +00:00
type GroupV2AccessCreateChangeType = {
2020-10-06 17:06:34 +00:00
type : 'create' ;
} ;
2021-07-20 20:18:35 +00:00
type GroupV2AccessAttributesChangeType = {
2020-09-09 02:25:05 +00:00
type : 'access-attributes' ;
newPrivilege : number ;
} ;
2021-07-20 20:18:35 +00:00
type GroupV2AccessMembersChangeType = {
2020-09-09 02:25:05 +00:00
type : 'access-members' ;
newPrivilege : number ;
} ;
2021-07-20 20:18:35 +00:00
type GroupV2AccessInviteLinkChangeType = {
2020-12-18 19:27:43 +00:00
type : 'access-invite-link' ;
newPrivilege : number ;
} ;
2021-07-20 20:18:35 +00:00
type GroupV2AnnouncementsOnlyChangeType = {
type : 'announcements-only' ;
announcementsOnly : boolean ;
} ;
type GroupV2AvatarChangeType = {
2020-09-09 02:25:05 +00:00
type : 'avatar' ;
removed : boolean ;
} ;
2021-07-20 20:18:35 +00:00
type GroupV2TitleChangeType = {
2020-09-09 02:25:05 +00:00
type : 'title' ;
// Allow for null, because the title could be removed entirely
newTitle? : string ;
} ;
2021-07-20 20:18:35 +00:00
type GroupV2GroupLinkAddChangeType = {
2020-12-18 19:27:43 +00:00
type : 'group-link-add' ;
privilege : number ;
} ;
2021-07-20 20:18:35 +00:00
type GroupV2GroupLinkResetChangeType = {
2020-12-18 19:27:43 +00:00
type : 'group-link-reset' ;
} ;
2021-07-20 20:18:35 +00:00
type GroupV2GroupLinkRemoveChangeType = {
2020-12-18 19:27:43 +00:00
type : 'group-link-remove' ;
} ;
2020-09-09 02:25:05 +00:00
// No disappearing messages timer change type - message.expirationTimerUpdate used instead
2021-07-20 20:18:35 +00:00
type GroupV2MemberAddChangeType = {
2020-09-09 02:25:05 +00:00
type : 'member-add' ;
2023-08-16 20:54:39 +00:00
aci : AciString ;
2020-09-09 02:25:05 +00:00
} ;
2021-07-20 20:18:35 +00:00
type GroupV2MemberAddFromInviteChangeType = {
2020-09-09 02:25:05 +00:00
type : 'member-add-from-invite' ;
2023-08-16 20:54:39 +00:00
aci : AciString ;
2024-05-03 14:28:36 +00:00
pni? : PniString ;
2023-08-10 16:43:33 +00:00
inviter? : AciString ;
2020-09-09 02:25:05 +00:00
} ;
2021-07-20 20:18:35 +00:00
type GroupV2MemberAddFromLinkChangeType = {
2020-12-18 19:27:43 +00:00
type : 'member-add-from-link' ;
2023-08-16 20:54:39 +00:00
aci : AciString ;
2020-12-18 19:27:43 +00:00
} ;
2021-07-20 20:18:35 +00:00
type GroupV2MemberAddFromAdminApprovalChangeType = {
2020-12-18 19:27:43 +00:00
type : 'member-add-from-admin-approval' ;
2023-08-16 20:54:39 +00:00
aci : AciString ;
2020-12-18 19:27:43 +00:00
} ;
2021-07-20 20:18:35 +00:00
type GroupV2MemberPrivilegeChangeType = {
2020-09-09 02:25:05 +00:00
type : 'member-privilege' ;
2023-08-16 20:54:39 +00:00
aci : AciString ;
2020-09-09 02:25:05 +00:00
newPrivilege : number ;
} ;
2021-07-20 20:18:35 +00:00
type GroupV2MemberRemoveChangeType = {
2020-09-09 02:25:05 +00:00
type : 'member-remove' ;
2023-08-16 20:54:39 +00:00
aci : AciString ;
2020-09-09 02:25:05 +00:00
} ;
2021-07-20 20:18:35 +00:00
type GroupV2PendingAddOneChangeType = {
2020-09-09 02:25:05 +00:00
type : 'pending-add-one' ;
2023-08-16 20:54:39 +00:00
serviceId : ServiceIdString ;
2020-09-09 02:25:05 +00:00
} ;
2021-07-20 20:18:35 +00:00
type GroupV2PendingAddManyChangeType = {
2020-09-09 02:25:05 +00:00
type : 'pending-add-many' ;
count : number ;
} ;
// Note: pending-remove is only used if user didn't also join the group at the same time
2021-07-20 20:18:35 +00:00
type GroupV2PendingRemoveOneChangeType = {
2020-09-09 02:25:05 +00:00
type : 'pending-remove-one' ;
2023-08-16 20:54:39 +00:00
serviceId : ServiceIdString ;
2023-08-10 16:43:33 +00:00
inviter? : AciString ;
2020-09-09 02:25:05 +00:00
} ;
// Note: pending-remove is only used if user didn't also join the group at the same time
2021-07-20 20:18:35 +00:00
type GroupV2PendingRemoveManyChangeType = {
2020-09-09 02:25:05 +00:00
type : 'pending-remove-many' ;
count : number ;
2023-08-10 16:43:33 +00:00
inviter? : AciString ;
2020-09-09 02:25:05 +00:00
} ;
2021-07-20 20:18:35 +00:00
type GroupV2AdminApprovalAddOneChangeType = {
2020-12-18 19:27:43 +00:00
type : 'admin-approval-add-one' ;
2023-08-16 20:54:39 +00:00
aci : AciString ;
2020-12-18 19:27:43 +00:00
} ;
// Note: admin-approval-remove-one is only used if user didn't also join the group at
// the same time
2021-07-20 20:18:35 +00:00
type GroupV2AdminApprovalRemoveOneChangeType = {
2020-12-18 19:27:43 +00:00
type : 'admin-approval-remove-one' ;
2023-08-16 20:54:39 +00:00
aci : AciString ;
2023-08-10 16:43:33 +00:00
inviter? : AciString ;
2020-12-18 19:27:43 +00:00
} ;
2022-03-16 00:11:28 +00:00
type GroupV2AdminApprovalBounceChangeType = {
type : 'admin-approval-bounce' ;
times : number ;
isApprovalPending : boolean ;
2023-08-16 20:54:39 +00:00
aci : AciString ;
2022-03-16 00:11:28 +00:00
} ;
2021-06-02 00:24:28 +00:00
export type GroupV2DescriptionChangeType = {
type : 'description' ;
removed? : boolean ;
2021-06-04 16:27:04 +00:00
// Adding this field; cannot remove previous field for backwards compatibility
description? : string ;
2021-06-02 00:24:28 +00:00
} ;
2022-12-06 21:12:57 +00:00
export type GroupV2SummaryType = {
type : 'summary' ;
} ;
2020-12-18 19:27:43 +00:00
2020-09-09 02:25:05 +00:00
export type GroupV2ChangeDetailType =
| GroupV2AccessAttributesChangeType
2020-12-18 19:27:43 +00:00
| GroupV2AccessCreateChangeType
| GroupV2AccessInviteLinkChangeType
2020-09-09 02:25:05 +00:00
| GroupV2AccessMembersChangeType
2020-12-18 19:27:43 +00:00
| GroupV2AdminApprovalAddOneChangeType
| GroupV2AdminApprovalRemoveOneChangeType
2022-03-16 00:11:28 +00:00
| GroupV2AdminApprovalBounceChangeType
2021-07-20 20:18:35 +00:00
| GroupV2AnnouncementsOnlyChangeType
2020-12-18 19:27:43 +00:00
| GroupV2AvatarChangeType
2021-06-02 00:24:28 +00:00
| GroupV2DescriptionChangeType
2020-12-18 19:27:43 +00:00
| GroupV2GroupLinkAddChangeType
| GroupV2GroupLinkRemoveChangeType
2021-06-02 00:24:28 +00:00
| GroupV2GroupLinkResetChangeType
2020-09-09 02:25:05 +00:00
| GroupV2MemberAddChangeType
2020-12-18 19:27:43 +00:00
| GroupV2MemberAddFromAdminApprovalChangeType
2020-09-09 02:25:05 +00:00
| GroupV2MemberAddFromInviteChangeType
2020-12-18 19:27:43 +00:00
| GroupV2MemberAddFromLinkChangeType
2020-09-09 02:25:05 +00:00
| GroupV2MemberPrivilegeChangeType
2020-12-18 19:27:43 +00:00
| GroupV2MemberRemoveChangeType
2020-09-09 02:25:05 +00:00
| GroupV2PendingAddManyChangeType
2020-12-18 19:27:43 +00:00
| GroupV2PendingAddOneChangeType
| GroupV2PendingRemoveManyChangeType
2020-09-09 02:25:05 +00:00
| GroupV2PendingRemoveOneChangeType
2022-12-06 21:12:57 +00:00
| GroupV2SummaryType
2020-12-18 19:27:43 +00:00
| GroupV2TitleChangeType ;
2020-09-09 02:25:05 +00:00
export type GroupV2ChangeType = {
2023-08-10 16:43:33 +00:00
from ? : ServiceIdString ;
2020-09-09 02:25:05 +00:00
details : Array < GroupV2ChangeDetailType > ;
} ;
2021-04-09 18:20:37 +00:00
export type GroupFields = {
2021-06-22 14:46:42 +00:00
readonly id : Uint8Array ;
readonly secretParams : Uint8Array ;
readonly publicParams : Uint8Array ;
2021-04-09 18:20:37 +00:00
} ;
const MAX_CACHED_GROUP_FIELDS = 100 ;
const groupFieldsCache = new LRU < string , GroupFields > ( {
max : MAX_CACHED_GROUP_FIELDS ,
} ) ;
2024-07-22 18:16:33 +00:00
const { updateConversation } = DataWriter ;
2020-11-20 17:30:45 +00:00
2020-09-09 02:25:05 +00:00
if ( ! isNumber ( MAX_MESSAGE_SCHEMA ) ) {
throw new Error (
'groups.ts: Unable to capture max message schema from js/modules/types/message'
) ;
}
type UpdatesResultType = {
// The array of new messages to be added into the message timeline
2022-03-16 00:11:28 +00:00
groupChangeMessages : Array < GroupChangeMessageType > ;
2023-10-02 20:19:55 +00:00
// The map of members in the group, and we largely just pull profile keys for each,
2020-09-09 02:25:05 +00:00
// because the group membership is updated in newAttributes
2023-10-02 20:19:55 +00:00
newProfileKeys : Map < AciString , string > ;
2020-09-09 02:25:05 +00:00
// To be merged into the conversation model
newAttributes : ConversationAttributesType ;
} ;
2021-03-03 20:09:58 +00:00
type UploadedAvatarType = {
2021-09-24 00:49:05 +00:00
data : Uint8Array ;
2021-03-03 20:09:58 +00:00
hash : string ;
key : string ;
} ;
2022-05-12 02:45:20 +00:00
type BasicMessageType = Pick <
MessageAttributesType ,
'id' | 'schemaVersion' | 'readStatus' | 'seenStatus'
> ;
2022-03-16 00:11:28 +00:00
type GroupV2ChangeMessageType = {
type : 'group-v2-change' ;
2023-08-16 20:54:39 +00:00
} & Pick < MessageAttributesType , ' groupV2Change ' | ' sourceServiceId ' > ;
2022-03-16 00:11:28 +00:00
type GroupV1MigrationMessageType = {
type : 'group-v1-migration' ;
} & Pick <
MessageAttributesType ,
'invitedGV2Members' | 'droppedGV2MemberIds' | 'groupMigration'
> ;
type TimerNotificationMessageType = {
type : 'timer-notification' ;
} & Pick <
MessageAttributesType ,
2023-08-16 20:54:39 +00:00
'sourceServiceId' | 'flags' | 'expirationTimerUpdate'
2022-03-16 00:11:28 +00:00
> ;
type GroupChangeMessageType = BasicMessageType &
(
| GroupV2ChangeMessageType
| GroupV1MigrationMessageType
| TimerNotificationMessageType
) ;
2020-09-09 02:25:05 +00:00
// Constants
export const MASTER_KEY_LENGTH = 32 ;
2021-03-10 22:32:58 +00:00
const GROUP_TITLE_MAX_ENCRYPTED_BYTES = 1024 ;
2021-06-02 00:24:28 +00:00
const GROUP_DESC_MAX_ENCRYPTED_BYTES = 8192 ;
2020-11-16 21:34:41 +00:00
export const ID_V1_LENGTH = 16 ;
export const ID_LENGTH = 32 ;
2020-09-09 02:25:05 +00:00
const TEMPORAL_AUTH_REJECTED_CODE = 401 ;
const GROUP_ACCESS_DENIED_CODE = 403 ;
2020-11-20 17:30:45 +00:00
const GROUP_NONEXISTENT_CODE = 404 ;
2022-07-08 20:46:25 +00:00
const SUPPORTED_CHANGE_EPOCH = 5 ;
2021-01-29 22:16:48 +00:00
export const LINK_VERSION_ERROR = 'LINK_VERSION_ERROR' ;
2021-01-29 21:19:24 +00:00
const GROUP_INVITE_LINK_PASSWORD_LENGTH = 16 ;
2022-03-16 00:11:28 +00:00
function generateBasicMessage ( ) : BasicMessageType {
return {
id : getGuid ( ) ,
schemaVersion : MAX_MESSAGE_SCHEMA ,
// this is missing most properties to fulfill this type
} ;
}
2021-01-29 21:19:24 +00:00
// Group Links
2021-09-24 00:49:05 +00:00
export function generateGroupInviteLinkPassword ( ) : Uint8Array {
2021-01-29 21:19:24 +00:00
return getRandomBytes ( GROUP_INVITE_LINK_PASSWORD_LENGTH ) ;
}
2021-01-29 22:16:48 +00:00
// Group Links
export async function getPreJoinGroupInfo (
inviteLinkPasswordBase64 : string ,
masterKeyBase64 : string
2021-06-22 14:46:42 +00:00
) : Promise < Proto.GroupJoinInfo > {
2021-01-29 22:16:48 +00:00
const data = window . Signal . Groups . deriveGroupFields (
2021-06-22 14:46:42 +00:00
Bytes . fromBase64 ( masterKeyBase64 )
2021-01-29 22:16:48 +00:00
) ;
2024-05-02 21:39:04 +00:00
return makeRequestWithCredentials ( {
2022-05-16 14:53:54 +00:00
logId : ` getPreJoinInfo/groupv2( ${ data . id } ) ` ,
2021-06-22 14:46:42 +00:00
publicParams : Bytes.toBase64 ( data . publicParams ) ,
secretParams : Bytes.toBase64 ( data . secretParams ) ,
2021-01-29 22:16:48 +00:00
request : ( sender , options ) = >
sender . getGroupFromLink ( inviteLinkPasswordBase64 , options ) ,
} ) ;
2021-01-29 21:19:24 +00:00
}
2023-06-02 17:54:36 +00:00
export function buildGroupLink (
conversation : ConversationAttributesType
) : string | undefined {
if ( ! isGroupV2 ( conversation ) ) {
return undefined ;
}
const { masterKey , groupInviteLinkPassword } = conversation ;
if ( ! groupInviteLinkPassword ) {
return undefined ;
}
2021-01-29 21:19:24 +00:00
2023-01-05 21:58:13 +00:00
strictAssert ( masterKey , 'buildGroupLink requires the master key!' ) ;
2021-06-22 14:46:42 +00:00
const bytes = Proto . GroupInviteLink . encode ( {
v1Contents : {
groupMasterKey : Bytes.fromBase64 ( masterKey ) ,
inviteLinkPassword : Bytes.fromBase64 ( groupInviteLinkPassword ) ,
} ,
} ) . finish ( ) ;
2021-01-29 21:19:24 +00:00
2023-11-02 19:42:31 +00:00
const inviteCode = toWebSafeBase64 ( Bytes . toBase64 ( bytes ) ) ;
2021-01-29 21:19:24 +00:00
2023-11-02 19:42:31 +00:00
return groupInvitesRoute . toWebUrl ( { inviteCode } ) . toString ( ) ;
2021-01-29 21:19:24 +00:00
}
2020-09-09 02:25:05 +00:00
2023-11-02 19:42:31 +00:00
export function parseGroupLink ( value : string ) : {
2021-11-11 22:43:05 +00:00
masterKey : string ;
inviteLinkPassword : string ;
} {
2023-11-02 19:42:31 +00:00
const base64 = fromWebSafeBase64 ( value ) ;
2021-06-22 14:46:42 +00:00
const buffer = Bytes . fromBase64 ( base64 ) ;
2021-01-29 22:16:48 +00:00
2021-06-22 14:46:42 +00:00
const inviteLinkProto = Proto . GroupInviteLink . decode ( buffer ) ;
2021-01-29 22:16:48 +00:00
if (
inviteLinkProto . contents !== 'v1Contents' ||
! inviteLinkProto . v1Contents
) {
const error = new Error (
'parseGroupLink: Parsed proto is missing v1Contents'
) ;
error . name = LINK_VERSION_ERROR ;
throw error ;
}
2021-06-22 14:46:42 +00:00
const {
groupMasterKey : groupMasterKeyRaw ,
inviteLinkPassword : inviteLinkPasswordRaw ,
} = inviteLinkProto . v1Contents ;
if ( ! groupMasterKeyRaw || ! groupMasterKeyRaw . length ) {
2021-01-29 22:16:48 +00:00
throw new Error ( 'v1Contents.groupMasterKey had no data!' ) ;
}
2021-06-22 14:46:42 +00:00
if ( ! inviteLinkPasswordRaw || ! inviteLinkPasswordRaw . length ) {
2021-01-29 22:16:48 +00:00
throw new Error ( 'v1Contents.inviteLinkPassword had no data!' ) ;
}
2021-06-22 14:46:42 +00:00
const masterKey = Bytes . toBase64 ( groupMasterKeyRaw ) ;
2021-01-29 22:16:48 +00:00
if ( masterKey . length !== 44 ) {
throw new Error ( ` masterKey had unexpected length ${ masterKey . length } ` ) ;
}
2021-06-22 14:46:42 +00:00
const inviteLinkPassword = Bytes . toBase64 ( inviteLinkPasswordRaw ) ;
2021-01-29 22:16:48 +00:00
if ( inviteLinkPassword . length === 0 ) {
throw new Error (
` inviteLinkPassword had unexpected length ${ inviteLinkPassword . length } `
) ;
}
return { masterKey , inviteLinkPassword } ;
}
2020-10-06 17:06:34 +00:00
// Group Modifications
2020-09-09 02:25:05 +00:00
2024-07-11 19:44:09 +00:00
async function uploadAvatar ( options : {
logId : string ;
publicParams : string ;
secretParams : string ;
data : Uint8Array ;
} ) : Promise < UploadedAvatarType > {
const { logId , publicParams , secretParams , data } = options ;
2021-03-03 20:09:58 +00:00
2020-11-20 17:30:45 +00:00
try {
const clientZkGroupCipher = getClientZkGroupCipher ( secretParams ) ;
2021-09-24 00:49:05 +00:00
const hash = computeHash ( data ) ;
2020-11-20 17:30:45 +00:00
2021-06-22 14:46:42 +00:00
const blobPlaintext = Proto . GroupAttributeBlob . encode ( {
2021-09-24 00:49:05 +00:00
avatar : data ,
2021-06-22 14:46:42 +00:00
} ) . finish ( ) ;
2020-11-20 17:30:45 +00:00
const ciphertext = encryptGroupBlob ( clientZkGroupCipher , blobPlaintext ) ;
2024-05-02 21:39:04 +00:00
const key = await makeRequestWithCredentials ( {
2020-11-20 17:30:45 +00:00
logId : ` uploadGroupAvatar/ ${ logId } ` ,
publicParams ,
secretParams ,
2021-03-03 20:09:58 +00:00
request : ( sender , requestOptions ) = >
sender . uploadGroupAvatar ( ciphertext , requestOptions ) ,
2020-11-20 17:30:45 +00:00
} ) ;
return {
2021-03-03 20:09:58 +00:00
data ,
2020-11-20 17:30:45 +00:00
hash ,
2021-03-03 20:09:58 +00:00
key ,
2020-11-20 17:30:45 +00:00
} ;
} catch ( error ) {
2022-11-22 18:43:43 +00:00
log . warn (
` uploadAvatar/ ${ logId } Failed to upload avatar ` ,
Errors . toLogFormat ( error )
) ;
2020-11-20 17:30:45 +00:00
throw error ;
}
}
2021-03-09 19:16:56 +00:00
function buildGroupTitleBuffer (
clientZkGroupCipher : ClientZkGroupCipher ,
title : string
2021-06-22 14:46:42 +00:00
) : Uint8Array {
const titleBlobPlaintext = Proto . GroupAttributeBlob . encode ( {
title ,
} ) . finish ( ) ;
2021-03-10 22:32:58 +00:00
const result = encryptGroupBlob ( clientZkGroupCipher , titleBlobPlaintext ) ;
if ( result . byteLength > GROUP_TITLE_MAX_ENCRYPTED_BYTES ) {
throw new Error ( 'buildGroupTitleBuffer: encrypted group title is too long' ) ;
}
return result ;
2021-03-09 19:16:56 +00:00
}
2021-06-02 00:24:28 +00:00
function buildGroupDescriptionBuffer (
clientZkGroupCipher : ClientZkGroupCipher ,
description : string
2021-06-22 14:46:42 +00:00
) : Uint8Array {
const attrsBlobPlaintext = Proto . GroupAttributeBlob . encode ( {
descriptionText : description ,
} ) . finish ( ) ;
2021-06-02 00:24:28 +00:00
const result = encryptGroupBlob ( clientZkGroupCipher , attrsBlobPlaintext ) ;
if ( result . byteLength > GROUP_DESC_MAX_ENCRYPTED_BYTES ) {
throw new Error (
'buildGroupDescriptionBuffer: encrypted group title is too long'
) ;
}
return result ;
}
2021-03-03 20:09:58 +00:00
function buildGroupProto (
attributes : Pick <
ConversationAttributesType ,
| 'accessControl'
| 'expireTimer'
| 'id'
| 'membersV2'
| 'name'
| 'pendingMembersV2'
| 'publicParams'
| 'revision'
| 'secretParams'
> & {
avatarUrl? : string ;
}
2021-06-22 14:46:42 +00:00
) : Proto . Group {
const MEMBER_ROLE_ENUM = Proto . Member . Role ;
const ACCESS_ENUM = Proto . AccessControl . AccessRequired ;
2020-11-20 17:30:45 +00:00
const logId = ` groupv2( ${ attributes . id } ) ` ;
const { publicParams , secretParams } = attributes ;
if ( ! publicParams ) {
throw new Error (
` buildGroupProto/ ${ logId } : attributes were missing publicParams! `
) ;
}
if ( ! secretParams ) {
throw new Error (
` buildGroupProto/ ${ logId } : attributes were missing secretParams! `
) ;
}
const serverPublicParamsBase64 = window . getServerPublicParams ( ) ;
const clientZkGroupCipher = getClientZkGroupCipher ( secretParams ) ;
const clientZkProfileCipher = getClientZkProfileOperations (
serverPublicParamsBase64
) ;
2021-06-22 14:46:42 +00:00
const proto = new Proto . Group ( ) ;
2020-11-20 17:30:45 +00:00
2021-06-22 14:46:42 +00:00
proto . publicKey = Bytes . fromBase64 ( publicParams ) ;
2020-11-20 17:30:45 +00:00
proto . version = attributes . revision || 0 ;
2021-03-09 19:16:56 +00:00
if ( attributes . name ) {
proto . title = buildGroupTitleBuffer ( clientZkGroupCipher , attributes . name ) ;
}
2020-11-20 17:30:45 +00:00
2021-03-03 20:09:58 +00:00
if ( attributes . avatarUrl ) {
proto . avatar = attributes . avatarUrl ;
2020-11-20 17:30:45 +00:00
}
if ( attributes . expireTimer ) {
2021-06-22 14:46:42 +00:00
const timerBlobPlaintext = Proto . GroupAttributeBlob . encode ( {
disappearingMessagesDuration : attributes.expireTimer ,
} ) . finish ( ) ;
2020-11-20 17:30:45 +00:00
proto . disappearingMessagesTimer = encryptGroupBlob (
clientZkGroupCipher ,
timerBlobPlaintext
) ;
}
2021-06-22 14:46:42 +00:00
const accessControl = new Proto . AccessControl ( ) ;
2020-11-20 17:30:45 +00:00
if ( attributes . accessControl ) {
accessControl . attributes =
attributes . accessControl . attributes || ACCESS_ENUM . MEMBER ;
accessControl . members =
attributes . accessControl . members || ACCESS_ENUM . MEMBER ;
} else {
accessControl . attributes = ACCESS_ENUM . MEMBER ;
accessControl . members = ACCESS_ENUM . MEMBER ;
}
proto . accessControl = accessControl ;
proto . members = ( attributes . membersV2 || [ ] ) . map ( item = > {
2021-06-22 14:46:42 +00:00
const member = new Proto . Member ( ) ;
2020-11-20 17:30:45 +00:00
2023-08-16 20:54:39 +00:00
const conversation = window . ConversationController . get ( item . aci ) ;
2020-11-20 17:30:45 +00:00
if ( ! conversation ) {
throw new Error ( ` buildGroupProto/ ${ logId } : no conversation for member! ` ) ;
}
const profileKeyCredentialBase64 = conversation . get ( 'profileKeyCredential' ) ;
if ( ! profileKeyCredentialBase64 ) {
throw new Error (
2021-03-01 19:55:49 +00:00
` buildGroupProto/ ${ logId } : member was missing profileKeyCredential! `
2020-11-20 17:30:45 +00:00
) ;
}
const presentation = createProfileKeyCredentialPresentation (
clientZkProfileCipher ,
profileKeyCredentialBase64 ,
secretParams
) ;
member . role = item . role || MEMBER_ROLE_ENUM . DEFAULT ;
member . presentation = presentation ;
return member ;
} ) ;
2023-08-10 16:43:33 +00:00
const ourAci = window . storage . user . getCheckedAci ( ) ;
2020-11-20 17:30:45 +00:00
2023-08-10 16:43:33 +00:00
const ourAciCipherTextBuffer = encryptServiceId ( clientZkGroupCipher , ourAci ) ;
2020-11-20 17:30:45 +00:00
2020-12-18 19:27:43 +00:00
proto . membersPendingProfileKey = ( attributes . pendingMembersV2 || [ ] ) . map (
item = > {
2021-06-22 14:46:42 +00:00
const pendingMember = new Proto . MemberPendingProfileKey ( ) ;
const member = new Proto . Member ( ) ;
2020-11-20 17:30:45 +00:00
2023-08-16 20:54:39 +00:00
const conversation = window . ConversationController . get ( item . serviceId ) ;
2020-12-18 19:27:43 +00:00
if ( ! conversation ) {
throw new Error ( 'buildGroupProto: no conversation for pending member!' ) ;
}
2020-11-20 17:30:45 +00:00
2023-08-10 16:43:33 +00:00
const serviceId = conversation . getCheckedServiceId (
'buildGroupProto: pending member was missing serviceId!'
2022-07-08 20:46:25 +00:00
) ;
2020-11-20 17:30:45 +00:00
2023-08-10 16:43:33 +00:00
const uuidCipherTextBuffer = encryptServiceId (
clientZkGroupCipher ,
serviceId
) ;
2020-12-18 19:27:43 +00:00
member . userId = uuidCipherTextBuffer ;
member . role = item . role || MEMBER_ROLE_ENUM . DEFAULT ;
2020-11-20 17:30:45 +00:00
2020-12-18 19:27:43 +00:00
pendingMember . member = member ;
2022-03-23 20:49:27 +00:00
pendingMember . timestamp = Long . fromNumber ( item . timestamp ) ;
2023-08-10 16:43:33 +00:00
pendingMember . addedByUserId = ourAciCipherTextBuffer ;
2020-11-20 17:30:45 +00:00
2020-12-18 19:27:43 +00:00
return pendingMember ;
}
) ;
2020-11-20 17:30:45 +00:00
return proto ;
}
2021-03-11 21:29:31 +00:00
export async function buildAddMembersChange (
conversation : Pick <
ConversationAttributesType ,
2022-03-15 01:32:07 +00:00
'bannedMembersV2' | 'id' | 'publicParams' | 'revision' | 'secretParams'
2021-03-11 21:29:31 +00:00
> ,
conversationIds : ReadonlyArray < string >
2021-06-22 14:46:42 +00:00
) : Promise < undefined | Proto.GroupChange.Actions > {
const MEMBER_ROLE_ENUM = Proto . Member . Role ;
2021-03-11 21:29:31 +00:00
const { id , publicParams , revision , secretParams } = conversation ;
const logId = ` groupv2( ${ id } ) ` ;
if ( ! publicParams ) {
throw new Error (
` buildAddMembersChange/ ${ logId } : attributes were missing publicParams! `
) ;
}
if ( ! secretParams ) {
throw new Error (
` buildAddMembersChange/ ${ logId } : attributes were missing secretParams! `
) ;
}
const newGroupVersion = ( revision || 0 ) + 1 ;
const serverPublicParamsBase64 = window . getServerPublicParams ( ) ;
const clientZkProfileCipher = getClientZkProfileOperations (
serverPublicParamsBase64
) ;
const clientZkGroupCipher = getClientZkGroupCipher ( secretParams ) ;
2023-08-10 16:43:33 +00:00
const ourAci = window . storage . user . getCheckedAci ( ) ;
const ourAciCipherTextBuffer = encryptServiceId ( clientZkGroupCipher , ourAci ) ;
2021-03-11 21:29:31 +00:00
const now = Date . now ( ) ;
2021-06-22 14:46:42 +00:00
const addMembers : Array < Proto.GroupChange.Actions.AddMemberAction > = [ ] ;
2021-11-11 22:43:05 +00:00
const addPendingMembers : Array < Proto.GroupChange.Actions.AddMemberPendingProfileKeyAction > =
[ ] ;
2022-03-15 01:32:07 +00:00
const actions = new Proto . GroupChange . Actions ( ) ;
2021-03-11 21:29:31 +00:00
await Promise . all (
conversationIds . map ( async conversationId = > {
const contact = window . ConversationController . get ( conversationId ) ;
if ( ! contact ) {
2022-09-15 19:17:15 +00:00
assertDev (
2021-03-11 21:29:31 +00:00
false ,
` buildAddMembersChange/ ${ logId } : missing local contact, skipping `
) ;
return ;
}
2023-08-10 16:43:33 +00:00
const serviceId = contact . getServiceId ( ) ;
if ( ! serviceId ) {
2022-09-15 19:17:15 +00:00
assertDev (
false ,
2023-08-10 16:43:33 +00:00
` buildAddMembersChange/ ${ logId } : missing serviceId; skipping `
2022-09-15 19:17:15 +00:00
) ;
2021-03-11 21:29:31 +00:00
return ;
}
// Refresh our local data to be sure
2022-03-04 19:48:44 +00:00
if ( ! contact . get ( 'profileKey' ) || ! contact . get ( 'profileKeyCredential' ) ) {
2021-03-11 21:29:31 +00:00
await contact . getProfiles ( ) ;
}
const profileKey = contact . get ( 'profileKey' ) ;
const profileKeyCredential = contact . get ( 'profileKeyCredential' ) ;
2021-06-22 14:46:42 +00:00
const member = new Proto . Member ( ) ;
2023-08-10 16:43:33 +00:00
member . userId = encryptServiceId ( clientZkGroupCipher , serviceId ) ;
2021-03-11 21:29:31 +00:00
member . role = MEMBER_ROLE_ENUM . DEFAULT ;
member . joinedAtVersion = newGroupVersion ;
// This is inspired by [Android's equivalent code][0].
//
// [0]: https://github.com/signalapp/Signal-Android/blob/2be306867539ab1526f0e49d1aa7bd61e783d23f/libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/GroupsV2Operations.java#L152-L174
if ( profileKey && profileKeyCredential ) {
member . presentation = createProfileKeyCredentialPresentation (
clientZkProfileCipher ,
profileKeyCredential ,
secretParams
) ;
2021-06-22 14:46:42 +00:00
const addMemberAction = new Proto . GroupChange . Actions . AddMemberAction ( ) ;
2021-03-11 21:29:31 +00:00
addMemberAction . added = member ;
addMemberAction . joinFromInviteLink = false ;
addMembers . push ( addMemberAction ) ;
} else {
2021-06-22 14:46:42 +00:00
const memberPendingProfileKey = new Proto . MemberPendingProfileKey ( ) ;
2021-03-11 21:29:31 +00:00
memberPendingProfileKey . member = member ;
2023-08-10 16:43:33 +00:00
memberPendingProfileKey . addedByUserId = ourAciCipherTextBuffer ;
2022-03-23 20:49:27 +00:00
memberPendingProfileKey . timestamp = Long . fromNumber ( now ) ;
2021-03-11 21:29:31 +00:00
2021-11-11 22:43:05 +00:00
const addPendingMemberAction =
new Proto . GroupChange . Actions . AddMemberPendingProfileKeyAction ( ) ;
2021-03-11 21:29:31 +00:00
addPendingMemberAction . added = memberPendingProfileKey ;
addPendingMembers . push ( addPendingMemberAction ) ;
}
2022-03-15 01:32:07 +00:00
2024-08-21 21:31:55 +00:00
const doesMemberNeedUnban = conversation . bannedMembersV2 ? . some (
2023-08-16 20:54:39 +00:00
bannedMember = > bannedMember . serviceId === serviceId
2022-03-23 22:34:51 +00:00
) ;
2022-03-15 01:32:07 +00:00
if ( doesMemberNeedUnban ) {
2023-08-10 16:43:33 +00:00
const uuidCipherTextBuffer = encryptServiceId (
clientZkGroupCipher ,
serviceId
) ;
2022-03-15 01:32:07 +00:00
const deleteMemberBannedAction =
new Proto . GroupChange . Actions . DeleteMemberBannedAction ( ) ;
deleteMemberBannedAction . deletedUserId = uuidCipherTextBuffer ;
actions . deleteMembersBanned = actions . deleteMembersBanned || [ ] ;
actions . deleteMembersBanned . push ( deleteMemberBannedAction ) ;
}
2021-03-11 21:29:31 +00:00
} )
) ;
if ( ! addMembers . length && ! addPendingMembers . length ) {
// This shouldn't happen. When these actions are passed to `modifyGroupV2`, a warning
// will be logged.
return undefined ;
}
if ( addMembers . length ) {
actions . addMembers = addMembers ;
}
if ( addPendingMembers . length ) {
actions . addPendingMembers = addPendingMembers ;
}
actions . version = newGroupVersion ;
return actions ;
}
2021-03-09 19:16:56 +00:00
export async function buildUpdateAttributesChange (
conversation : Pick <
ConversationAttributesType ,
'id' | 'revision' | 'publicParams' | 'secretParams'
> ,
attributes : Readonly < {
2021-09-24 00:49:05 +00:00
avatar? : undefined | Uint8Array ;
2021-06-02 00:24:28 +00:00
description? : string ;
2021-03-09 19:16:56 +00:00
title? : string ;
} >
2021-06-22 14:46:42 +00:00
) : Promise < undefined | Proto.GroupChange.Actions > {
2021-03-09 19:16:56 +00:00
const { publicParams , secretParams , revision , id } = conversation ;
const logId = ` groupv2( ${ id } ) ` ;
if ( ! publicParams ) {
throw new Error (
` buildUpdateAttributesChange/ ${ logId } : attributes were missing publicParams! `
) ;
}
if ( ! secretParams ) {
throw new Error (
` buildUpdateAttributesChange/ ${ logId } : attributes were missing secretParams! `
) ;
}
2021-06-22 14:46:42 +00:00
const actions = new Proto . GroupChange . Actions ( ) ;
2021-03-09 19:16:56 +00:00
let hasChangedSomething = false ;
const clientZkGroupCipher = getClientZkGroupCipher ( secretParams ) ;
// There are three possible states here:
//
// 1. 'avatar' not in attributes: we don't want to change the avatar.
// 2. attributes.avatar === undefined: we want to clear the avatar.
// 3. attributes.avatar !== undefined: we want to update the avatar.
if ( 'avatar' in attributes ) {
hasChangedSomething = true ;
2021-06-22 14:46:42 +00:00
actions . modifyAvatar = new Proto . GroupChange . Actions . ModifyAvatarAction ( ) ;
2021-03-09 19:16:56 +00:00
const { avatar } = attributes ;
if ( avatar ) {
const uploadedAvatar = await uploadAvatar ( {
data : avatar ,
logId ,
publicParams ,
secretParams ,
} ) ;
actions . modifyAvatar . avatar = uploadedAvatar . key ;
}
// If we don't set `actions.modifyAvatar.avatar`, it will be cleared.
}
const { title } = attributes ;
if ( title ) {
hasChangedSomething = true ;
2021-06-22 14:46:42 +00:00
actions . modifyTitle = new Proto . GroupChange . Actions . ModifyTitleAction ( ) ;
2021-03-09 19:16:56 +00:00
actions . modifyTitle . title = buildGroupTitleBuffer (
clientZkGroupCipher ,
title
) ;
}
2021-06-02 00:24:28 +00:00
const { description } = attributes ;
if ( typeof description === 'string' ) {
hasChangedSomething = true ;
2021-11-11 22:43:05 +00:00
actions . modifyDescription =
new Proto . GroupChange . Actions . ModifyDescriptionAction ( ) ;
2021-06-02 00:24:28 +00:00
actions . modifyDescription . descriptionBytes = buildGroupDescriptionBuffer (
clientZkGroupCipher ,
description
) ;
}
2021-03-09 19:16:56 +00:00
if ( ! hasChangedSomething ) {
// This shouldn't happen. When these actions are passed to `modifyGroupV2`, a warning
// will be logged.
return undefined ;
}
actions . version = ( revision || 0 ) + 1 ;
return actions ;
}
2020-09-09 02:25:05 +00:00
export function buildDisappearingMessagesTimerChange ( {
expireTimer ,
group ,
} : {
2022-11-16 20:18:02 +00:00
expireTimer : DurationInSeconds ;
2020-09-09 02:25:05 +00:00
group : ConversationAttributesType ;
2021-06-22 14:46:42 +00:00
} ) : Proto . GroupChange . Actions {
const actions = new Proto . GroupChange . Actions ( ) ;
2020-09-09 02:25:05 +00:00
2021-06-22 14:46:42 +00:00
const blob = new Proto . GroupAttributeBlob ( ) ;
2020-09-09 02:25:05 +00:00
blob . disappearingMessagesDuration = expireTimer ;
if ( ! group . secretParams ) {
throw new Error (
'buildDisappearingMessagesTimerChange: group was missing secretParams!'
) ;
}
const clientZkGroupCipher = getClientZkGroupCipher ( group . secretParams ) ;
2021-06-22 14:46:42 +00:00
const blobPlaintext = Proto . GroupAttributeBlob . encode ( blob ) . finish ( ) ;
2020-09-09 02:25:05 +00:00
const blobCipherText = encryptGroupBlob ( clientZkGroupCipher , blobPlaintext ) ;
2021-11-11 22:43:05 +00:00
const timerAction =
new Proto . GroupChange . Actions . ModifyDisappearingMessagesTimerAction ( ) ;
2020-09-09 02:25:05 +00:00
timerAction . timer = blobCipherText ;
actions . version = ( group . revision || 0 ) + 1 ;
actions . modifyDisappearingMessagesTimer = timerAction ;
return actions ;
}
2021-01-29 21:19:24 +00:00
export function buildInviteLinkPasswordChange (
group : ConversationAttributesType ,
inviteLinkPassword : string
2021-06-22 14:46:42 +00:00
) : Proto . GroupChange . Actions {
2021-11-11 22:43:05 +00:00
const inviteLinkPasswordAction =
new Proto . GroupChange . Actions . ModifyInviteLinkPasswordAction ( ) ;
inviteLinkPasswordAction . inviteLinkPassword =
Bytes . fromBase64 ( inviteLinkPassword ) ;
2021-01-29 21:19:24 +00:00
2021-06-22 14:46:42 +00:00
const actions = new Proto . GroupChange . Actions ( ) ;
2021-01-29 21:19:24 +00:00
actions . version = ( group . revision || 0 ) + 1 ;
actions . modifyInviteLinkPassword = inviteLinkPasswordAction ;
return actions ;
}
export function buildNewGroupLinkChange (
group : ConversationAttributesType ,
inviteLinkPassword : string ,
addFromInviteLinkAccess : AccessRequiredEnum
2021-06-22 14:46:42 +00:00
) : Proto . GroupChange . Actions {
2021-11-11 22:43:05 +00:00
const accessControlAction =
new Proto . GroupChange . Actions . ModifyAddFromInviteLinkAccessControlAction ( ) ;
2021-01-29 21:19:24 +00:00
accessControlAction . addFromInviteLinkAccess = addFromInviteLinkAccess ;
2021-11-11 22:43:05 +00:00
const inviteLinkPasswordAction =
new Proto . GroupChange . Actions . ModifyInviteLinkPasswordAction ( ) ;
inviteLinkPasswordAction . inviteLinkPassword =
Bytes . fromBase64 ( inviteLinkPassword ) ;
2021-01-29 21:19:24 +00:00
2021-06-22 14:46:42 +00:00
const actions = new Proto . GroupChange . Actions ( ) ;
2021-01-29 21:19:24 +00:00
actions . version = ( group . revision || 0 ) + 1 ;
actions . modifyAddFromInviteLinkAccess = accessControlAction ;
actions . modifyInviteLinkPassword = inviteLinkPasswordAction ;
return actions ;
}
export function buildAccessControlAddFromInviteLinkChange (
group : ConversationAttributesType ,
value : AccessRequiredEnum
2021-06-22 14:46:42 +00:00
) : Proto . GroupChange . Actions {
2021-11-11 22:43:05 +00:00
const accessControlAction =
new Proto . GroupChange . Actions . ModifyAddFromInviteLinkAccessControlAction ( ) ;
2021-01-29 21:19:24 +00:00
accessControlAction . addFromInviteLinkAccess = value ;
2021-06-22 14:46:42 +00:00
const actions = new Proto . GroupChange . Actions ( ) ;
2021-01-29 21:19:24 +00:00
actions . version = ( group . revision || 0 ) + 1 ;
actions . modifyAddFromInviteLinkAccess = accessControlAction ;
return actions ;
}
2021-07-20 20:18:35 +00:00
export function buildAnnouncementsOnlyChange (
group : ConversationAttributesType ,
value : boolean
) : Proto . GroupChange . Actions {
const action = new Proto . GroupChange . Actions . ModifyAnnouncementsOnlyAction ( ) ;
action . announcementsOnly = value ;
const actions = new Proto . GroupChange . Actions ( ) ;
actions . version = ( group . revision || 0 ) + 1 ;
actions . modifyAnnouncementsOnly = action ;
return actions ;
}
2021-01-29 21:19:24 +00:00
export function buildAccessControlAttributesChange (
group : ConversationAttributesType ,
value : AccessRequiredEnum
2021-06-22 14:46:42 +00:00
) : Proto . GroupChange . Actions {
2021-11-11 22:43:05 +00:00
const accessControlAction =
new Proto . GroupChange . Actions . ModifyAttributesAccessControlAction ( ) ;
2021-01-29 21:19:24 +00:00
accessControlAction . attributesAccess = value ;
2021-06-22 14:46:42 +00:00
const actions = new Proto . GroupChange . Actions ( ) ;
2021-01-29 21:19:24 +00:00
actions . version = ( group . revision || 0 ) + 1 ;
actions . modifyAttributesAccess = accessControlAction ;
return actions ;
}
export function buildAccessControlMembersChange (
group : ConversationAttributesType ,
value : AccessRequiredEnum
2021-06-22 14:46:42 +00:00
) : Proto . GroupChange . Actions {
2021-11-11 22:43:05 +00:00
const accessControlAction =
new Proto . GroupChange . Actions . ModifyMembersAccessControlAction ( ) ;
2021-01-29 21:19:24 +00:00
accessControlAction . membersAccess = value ;
2021-06-22 14:46:42 +00:00
const actions = new Proto . GroupChange . Actions ( ) ;
2021-01-29 21:19:24 +00:00
actions . version = ( group . revision || 0 ) + 1 ;
actions . modifyMemberAccess = accessControlAction ;
return actions ;
}
2022-03-23 22:34:51 +00:00
export function _maybeBuildAddBannedMemberActions ( {
clientZkGroupCipher ,
group ,
2023-08-10 16:43:33 +00:00
ourAci ,
serviceId ,
2022-03-23 22:34:51 +00:00
} : {
clientZkGroupCipher : ClientZkGroupCipher ;
group : Pick < ConversationAttributesType , ' bannedMembersV2 ' > ;
2023-08-10 16:43:33 +00:00
ourAci : AciString ;
serviceId : ServiceIdString ;
2022-03-23 22:34:51 +00:00
} ) : Pick <
Proto . GroupChange . IActions ,
'addMembersBanned' | 'deleteMembersBanned'
> {
const doesMemberNeedBan =
2024-08-21 21:31:55 +00:00
! group . bannedMembersV2 ? . some ( member = > member . serviceId === serviceId ) &&
2023-08-10 16:43:33 +00:00
serviceId !== ourAci ;
2022-03-23 22:34:51 +00:00
if ( ! doesMemberNeedBan ) {
return { } ;
}
// Sort current banned members by decreasing timestamp
const sortedBannedMembers = [ . . . ( group . bannedMembersV2 ? ? [ ] ) ] . sort (
( a , b ) = > {
return b . timestamp - a . timestamp ;
}
) ;
// All members after the limit have to be deleted and are older than the
// rest of the list.
const deletedBannedMembers = sortedBannedMembers . slice (
Math . max ( 0 , getGroupSizeHardLimit ( ) - 1 )
) ;
let deleteMembersBanned = null ;
if ( deletedBannedMembers . length > 0 ) {
deleteMembersBanned = deletedBannedMembers . map ( bannedMember = > {
const deleteMemberBannedAction =
new Proto . GroupChange . Actions . DeleteMemberBannedAction ( ) ;
2023-08-10 16:43:33 +00:00
deleteMemberBannedAction . deletedUserId = encryptServiceId (
2022-03-23 22:34:51 +00:00
clientZkGroupCipher ,
2023-08-16 20:54:39 +00:00
bannedMember . serviceId
2022-03-23 22:34:51 +00:00
) ;
return deleteMemberBannedAction ;
} ) ;
}
const addMemberBannedAction =
new Proto . GroupChange . Actions . AddMemberBannedAction ( ) ;
2023-08-10 16:43:33 +00:00
const uuidCipherTextBuffer = encryptServiceId ( clientZkGroupCipher , serviceId ) ;
2022-03-23 22:34:51 +00:00
addMemberBannedAction . added = new Proto . MemberBanned ( ) ;
addMemberBannedAction . added . userId = uuidCipherTextBuffer ;
return {
addMembersBanned : [ addMemberBannedAction ] ,
deleteMembersBanned ,
} ;
}
2021-01-29 21:19:24 +00:00
// TODO AND-1101
export function buildDeletePendingAdminApprovalMemberChange ( {
2020-10-06 17:06:34 +00:00
group ,
2023-08-10 16:43:33 +00:00
ourAci ,
aci ,
2020-10-06 17:06:34 +00:00
} : {
2021-01-29 21:19:24 +00:00
group : ConversationAttributesType ;
2023-08-10 16:43:33 +00:00
ourAci : AciString ;
aci : AciString ;
2021-06-22 14:46:42 +00:00
} ) : Proto . GroupChange . Actions {
const actions = new Proto . GroupChange . Actions ( ) ;
2021-01-29 21:19:24 +00:00
if ( ! group . secretParams ) {
throw new Error (
'buildDeletePendingAdminApprovalMemberChange: group was missing secretParams!'
) ;
}
const clientZkGroupCipher = getClientZkGroupCipher ( group . secretParams ) ;
2023-08-10 16:43:33 +00:00
const uuidCipherTextBuffer = encryptServiceId ( clientZkGroupCipher , aci ) ;
2021-01-29 21:19:24 +00:00
2021-11-11 22:43:05 +00:00
const deleteMemberPendingAdminApproval =
new Proto . GroupChange . Actions . DeleteMemberPendingAdminApprovalAction ( ) ;
2021-01-29 21:19:24 +00:00
deleteMemberPendingAdminApproval . deletedUserId = uuidCipherTextBuffer ;
actions . version = ( group . revision || 0 ) + 1 ;
actions . deleteMemberPendingAdminApprovals = [
deleteMemberPendingAdminApproval ,
] ;
2022-03-23 22:34:51 +00:00
const { addMembersBanned , deleteMembersBanned } =
_maybeBuildAddBannedMemberActions ( {
clientZkGroupCipher ,
group ,
2023-08-10 16:43:33 +00:00
ourAci ,
serviceId : aci ,
2022-03-23 22:34:51 +00:00
} ) ;
2022-03-15 01:32:07 +00:00
2022-03-23 22:34:51 +00:00
if ( addMembersBanned ) {
actions . addMembersBanned = addMembersBanned ;
}
if ( deleteMembersBanned ) {
actions . deleteMembersBanned = deleteMembersBanned ;
2022-03-15 01:32:07 +00:00
}
2021-01-29 21:19:24 +00:00
return actions ;
}
2021-01-29 22:16:48 +00:00
export function buildAddPendingAdminApprovalMemberChange ( {
group ,
profileKeyCredentialBase64 ,
serverPublicParamsBase64 ,
} : {
group : ConversationAttributesType ;
profileKeyCredentialBase64 : string ;
serverPublicParamsBase64 : string ;
2021-06-22 14:46:42 +00:00
} ) : Proto . GroupChange . Actions {
const actions = new Proto . GroupChange . Actions ( ) ;
2021-01-29 22:16:48 +00:00
if ( ! group . secretParams ) {
throw new Error (
'buildAddPendingAdminApprovalMemberChange: group was missing secretParams!'
) ;
}
const clientZkProfileCipher = getClientZkProfileOperations (
serverPublicParamsBase64
) ;
2021-11-11 22:43:05 +00:00
const addMemberPendingAdminApproval =
new Proto . GroupChange . Actions . AddMemberPendingAdminApprovalAction ( ) ;
2021-01-29 22:16:48 +00:00
const presentation = createProfileKeyCredentialPresentation (
clientZkProfileCipher ,
profileKeyCredentialBase64 ,
group . secretParams
) ;
2021-06-22 14:46:42 +00:00
const added = new Proto . MemberPendingAdminApproval ( ) ;
2021-01-29 22:16:48 +00:00
added . presentation = presentation ;
addMemberPendingAdminApproval . added = added ;
actions . version = ( group . revision || 0 ) + 1 ;
actions . addMemberPendingAdminApprovals = [ addMemberPendingAdminApproval ] ;
return actions ;
}
export function buildAddMember ( {
group ,
profileKeyCredentialBase64 ,
serverPublicParamsBase64 ,
2023-08-10 16:43:33 +00:00
serviceId ,
2021-01-29 22:16:48 +00:00
} : {
group : ConversationAttributesType ;
profileKeyCredentialBase64 : string ;
serverPublicParamsBase64 : string ;
joinFromInviteLink? : boolean ;
2023-08-10 16:43:33 +00:00
serviceId : ServiceIdString ;
2021-06-22 14:46:42 +00:00
} ) : Proto . GroupChange . Actions {
const MEMBER_ROLE_ENUM = Proto . Member . Role ;
2021-01-29 22:16:48 +00:00
2021-06-22 14:46:42 +00:00
const actions = new Proto . GroupChange . Actions ( ) ;
2021-01-29 22:16:48 +00:00
if ( ! group . secretParams ) {
throw new Error ( 'buildAddMember: group was missing secretParams!' ) ;
}
const clientZkProfileCipher = getClientZkProfileOperations (
serverPublicParamsBase64
) ;
2021-06-22 14:46:42 +00:00
const addMember = new Proto . GroupChange . Actions . AddMemberAction ( ) ;
2021-01-29 22:16:48 +00:00
const presentation = createProfileKeyCredentialPresentation (
clientZkProfileCipher ,
profileKeyCredentialBase64 ,
group . secretParams
) ;
2021-06-22 14:46:42 +00:00
const added = new Proto . Member ( ) ;
2021-01-29 22:16:48 +00:00
added . presentation = presentation ;
added . role = MEMBER_ROLE_ENUM . DEFAULT ;
addMember . added = added ;
actions . version = ( group . revision || 0 ) + 1 ;
actions . addMembers = [ addMember ] ;
2024-08-21 21:31:55 +00:00
const doesMemberNeedUnban = group . bannedMembersV2 ? . some (
2023-08-16 20:54:39 +00:00
member = > member . serviceId === serviceId
2022-03-23 22:34:51 +00:00
) ;
2022-03-15 01:32:07 +00:00
if ( doesMemberNeedUnban ) {
const clientZkGroupCipher = getClientZkGroupCipher ( group . secretParams ) ;
2023-08-10 16:43:33 +00:00
const userIdCipherText = encryptServiceId ( clientZkGroupCipher , serviceId ) ;
2022-03-15 01:32:07 +00:00
const deleteMemberBannedAction =
new Proto . GroupChange . Actions . DeleteMemberBannedAction ( ) ;
2023-08-10 16:43:33 +00:00
deleteMemberBannedAction . deletedUserId = userIdCipherText ;
2022-03-15 01:32:07 +00:00
actions . deleteMembersBanned = [ deleteMemberBannedAction ] ;
}
2021-01-29 22:16:48 +00:00
return actions ;
}
2021-01-29 21:19:24 +00:00
export function buildDeletePendingMemberChange ( {
2023-08-10 16:43:33 +00:00
serviceIds ,
2021-01-29 21:19:24 +00:00
group ,
} : {
2023-08-10 16:43:33 +00:00
serviceIds : ReadonlyArray < ServiceIdString > ;
2020-10-06 17:06:34 +00:00
group : ConversationAttributesType ;
2021-06-22 14:46:42 +00:00
} ) : Proto . GroupChange . Actions {
const actions = new Proto . GroupChange . Actions ( ) ;
2020-10-06 17:06:34 +00:00
if ( ! group . secretParams ) {
throw new Error (
'buildDeletePendingMemberChange: group was missing secretParams!'
) ;
}
const clientZkGroupCipher = getClientZkGroupCipher ( group . secretParams ) ;
2023-08-10 16:43:33 +00:00
const deletePendingMembers = serviceIds . map ( serviceId = > {
const uuidCipherTextBuffer = encryptServiceId (
clientZkGroupCipher ,
serviceId
) ;
2021-11-11 22:43:05 +00:00
const deletePendingMember =
new Proto . GroupChange . Actions . DeleteMemberPendingProfileKeyAction ( ) ;
2021-01-29 21:19:24 +00:00
deletePendingMember . deletedUserId = uuidCipherTextBuffer ;
return deletePendingMember ;
} ) ;
2020-10-06 17:06:34 +00:00
actions . version = ( group . revision || 0 ) + 1 ;
2021-01-29 21:19:24 +00:00
actions . deletePendingMembers = deletePendingMembers ;
2020-10-06 17:06:34 +00:00
return actions ;
}
export function buildDeleteMemberChange ( {
group ,
2023-08-10 16:43:33 +00:00
ourAci ,
serviceId ,
2020-10-06 17:06:34 +00:00
} : {
group : ConversationAttributesType ;
2023-08-10 16:43:33 +00:00
ourAci : AciString ;
serviceId : ServiceIdString ;
2021-06-22 14:46:42 +00:00
} ) : Proto . GroupChange . Actions {
const actions = new Proto . GroupChange . Actions ( ) ;
2020-10-06 17:06:34 +00:00
if ( ! group . secretParams ) {
throw new Error ( 'buildDeleteMemberChange: group was missing secretParams!' ) ;
}
const clientZkGroupCipher = getClientZkGroupCipher ( group . secretParams ) ;
2023-08-10 16:43:33 +00:00
const uuidCipherTextBuffer = encryptServiceId ( clientZkGroupCipher , serviceId ) ;
2020-10-06 17:06:34 +00:00
2021-06-22 14:46:42 +00:00
const deleteMember = new Proto . GroupChange . Actions . DeleteMemberAction ( ) ;
2020-10-06 17:06:34 +00:00
deleteMember . deletedUserId = uuidCipherTextBuffer ;
actions . version = ( group . revision || 0 ) + 1 ;
actions . deleteMembers = [ deleteMember ] ;
2022-03-23 22:34:51 +00:00
const { addMembersBanned , deleteMembersBanned } =
_maybeBuildAddBannedMemberActions ( {
clientZkGroupCipher ,
group ,
2023-08-10 16:43:33 +00:00
ourAci ,
serviceId ,
2022-03-23 22:34:51 +00:00
} ) ;
2022-03-15 01:32:07 +00:00
2022-03-23 22:34:51 +00:00
if ( addMembersBanned ) {
actions . addMembersBanned = addMembersBanned ;
}
if ( deleteMembersBanned ) {
actions . deleteMembersBanned = deleteMembersBanned ;
2022-03-15 01:32:07 +00:00
}
2020-10-06 17:06:34 +00:00
return actions ;
}
2022-03-16 00:11:28 +00:00
export function buildAddBannedMemberChange ( {
2023-08-10 16:43:33 +00:00
serviceId ,
2022-03-16 00:11:28 +00:00
group ,
} : {
2023-08-10 16:43:33 +00:00
serviceId : ServiceIdString ;
2022-03-16 00:11:28 +00:00
group : ConversationAttributesType ;
} ) : Proto . GroupChange . Actions {
const actions = new Proto . GroupChange . Actions ( ) ;
if ( ! group . secretParams ) {
throw new Error (
'buildAddBannedMemberChange: group was missing secretParams!'
) ;
}
const clientZkGroupCipher = getClientZkGroupCipher ( group . secretParams ) ;
2023-08-10 16:43:33 +00:00
const userIdCipherText = encryptServiceId ( clientZkGroupCipher , serviceId ) ;
2022-03-16 00:11:28 +00:00
const addMemberBannedAction =
new Proto . GroupChange . Actions . AddMemberBannedAction ( ) ;
addMemberBannedAction . added = new Proto . MemberBanned ( ) ;
2023-08-10 16:43:33 +00:00
addMemberBannedAction . added . userId = userIdCipherText ;
2022-03-16 00:11:28 +00:00
actions . addMembersBanned = [ addMemberBannedAction ] ;
2023-08-16 20:54:39 +00:00
if ( group . pendingAdminApprovalV2 ? . some ( item = > item . aci === serviceId ) ) {
2022-03-16 00:11:28 +00:00
const deleteMemberPendingAdminApprovalAction =
new Proto . GroupChange . Actions . DeleteMemberPendingAdminApprovalAction ( ) ;
2023-08-10 16:43:33 +00:00
deleteMemberPendingAdminApprovalAction . deletedUserId = userIdCipherText ;
2022-03-16 00:11:28 +00:00
actions . deleteMemberPendingAdminApprovals = [
deleteMemberPendingAdminApprovalAction ,
] ;
}
actions . version = ( group . revision || 0 ) + 1 ;
return actions ;
}
2021-01-29 21:19:24 +00:00
export function buildModifyMemberRoleChange ( {
2023-08-10 16:43:33 +00:00
serviceId ,
2021-01-29 21:19:24 +00:00
group ,
role ,
} : {
2023-08-10 16:43:33 +00:00
serviceId : ServiceIdString ;
2021-01-29 21:19:24 +00:00
group : ConversationAttributesType ;
role : number ;
2021-06-22 14:46:42 +00:00
} ) : Proto . GroupChange . Actions {
const actions = new Proto . GroupChange . Actions ( ) ;
2021-01-29 21:19:24 +00:00
if ( ! group . secretParams ) {
throw new Error ( 'buildMakeAdminChange: group was missing secretParams!' ) ;
}
const clientZkGroupCipher = getClientZkGroupCipher ( group . secretParams ) ;
2023-08-10 16:43:33 +00:00
const userIdCipherText = encryptServiceId ( clientZkGroupCipher , serviceId ) ;
2021-01-29 21:19:24 +00:00
2021-06-22 14:46:42 +00:00
const toggleAdmin = new Proto . GroupChange . Actions . ModifyMemberRoleAction ( ) ;
2023-08-10 16:43:33 +00:00
toggleAdmin . userId = userIdCipherText ;
2021-01-29 21:19:24 +00:00
toggleAdmin . role = role ;
actions . version = ( group . revision || 0 ) + 1 ;
actions . modifyMemberRoles = [ toggleAdmin ] ;
return actions ;
}
export function buildPromotePendingAdminApprovalMemberChange ( {
group ,
2023-08-10 16:43:33 +00:00
aci ,
2021-01-29 21:19:24 +00:00
} : {
group : ConversationAttributesType ;
2023-08-10 16:43:33 +00:00
aci : AciString ;
2021-06-22 14:46:42 +00:00
} ) : Proto . GroupChange . Actions {
const MEMBER_ROLE_ENUM = Proto . Member . Role ;
const actions = new Proto . GroupChange . Actions ( ) ;
2021-01-29 21:19:24 +00:00
if ( ! group . secretParams ) {
throw new Error (
'buildAddPendingAdminApprovalMemberChange: group was missing secretParams!'
) ;
}
const clientZkGroupCipher = getClientZkGroupCipher ( group . secretParams ) ;
2023-08-10 16:43:33 +00:00
const userIdCipher = encryptServiceId ( clientZkGroupCipher , aci ) ;
2021-01-29 21:19:24 +00:00
2021-11-11 22:43:05 +00:00
const promotePendingMember =
new Proto . GroupChange . Actions . PromoteMemberPendingAdminApprovalAction ( ) ;
2023-08-10 16:43:33 +00:00
promotePendingMember . userId = userIdCipher ;
2021-01-29 21:19:24 +00:00
promotePendingMember . role = MEMBER_ROLE_ENUM . DEFAULT ;
actions . version = ( group . revision || 0 ) + 1 ;
actions . promoteMemberPendingAdminApprovals = [ promotePendingMember ] ;
return actions ;
}
2022-07-08 20:46:25 +00:00
export type BuildPromoteMemberChangeOptionsType = Readonly < {
group : ConversationAttributesType ;
serverPublicParamsBase64 : string ;
2022-09-21 16:18:48 +00:00
profileKeyCredentialBase64 : string ;
isPendingPniAciProfileKey : boolean ;
2022-07-08 20:46:25 +00:00
} > ;
2020-10-06 17:06:34 +00:00
export function buildPromoteMemberChange ( {
group ,
profileKeyCredentialBase64 ,
serverPublicParamsBase64 ,
2022-09-21 16:18:48 +00:00
isPendingPniAciProfileKey = false ,
2022-07-08 20:46:25 +00:00
} : BuildPromoteMemberChangeOptionsType ) : Proto . GroupChange . Actions {
2021-06-22 14:46:42 +00:00
const actions = new Proto . GroupChange . Actions ( ) ;
2020-10-06 17:06:34 +00:00
if ( ! group . secretParams ) {
throw new Error (
'buildDisappearingMessagesTimerChange: group was missing secretParams!'
) ;
}
2022-07-08 20:46:25 +00:00
actions . version = ( group . revision || 0 ) + 1 ;
2020-10-06 17:06:34 +00:00
const clientZkProfileCipher = getClientZkProfileOperations (
serverPublicParamsBase64
) ;
2022-09-21 16:18:48 +00:00
const presentation = createProfileKeyCredentialPresentation (
clientZkProfileCipher ,
profileKeyCredentialBase64 ,
group . secretParams
) ;
2020-10-06 17:06:34 +00:00
2022-09-21 16:18:48 +00:00
if ( isPendingPniAciProfileKey ) {
actions . promoteMembersPendingPniAciProfileKey = [
2022-07-08 20:46:25 +00:00
{
presentation ,
} ,
] ;
} else {
2022-09-21 16:18:48 +00:00
actions . promotePendingMembers = [
2022-07-08 20:46:25 +00:00
{
presentation ,
} ,
] ;
}
2020-10-06 17:06:34 +00:00
return actions ;
}
2022-07-08 20:46:25 +00:00
async function uploadGroupChange ( {
2020-09-09 02:25:05 +00:00
actions ,
2024-05-20 18:15:39 +00:00
groupId ,
groupPublicParamsBase64 ,
groupSecretParamsBase64 ,
2021-01-29 22:16:48 +00:00
inviteLinkPassword ,
2020-09-09 02:25:05 +00:00
} : {
2021-06-22 14:46:42 +00:00
actions : Proto.GroupChange.IActions ;
2024-05-20 18:15:39 +00:00
groupId : string ;
groupPublicParamsBase64 : string ;
groupSecretParamsBase64 : string ;
2021-01-29 22:16:48 +00:00
inviteLinkPassword? : string ;
2024-05-04 00:42:11 +00:00
} ) : Promise < Proto.IGroupChangeResponse > {
2024-05-20 18:15:39 +00:00
const logId = idForLogging ( groupId ) ;
2020-09-09 02:25:05 +00:00
// Ensure we have the credentials we need before attempting GroupsV2 operations
await maybeFetchNewCredentials ( ) ;
2024-05-02 21:39:04 +00:00
return makeRequestWithCredentials ( {
2020-11-20 17:30:45 +00:00
logId : ` uploadGroupChange/ ${ logId } ` ,
2024-05-20 18:15:39 +00:00
publicParams : groupPublicParamsBase64 ,
secretParams : groupSecretParamsBase64 ,
2021-01-29 22:16:48 +00:00
request : ( sender , options ) = >
sender . modifyGroup ( actions , options , inviteLinkPassword ) ,
2020-11-20 17:30:45 +00:00
} ) ;
2020-09-09 02:25:05 +00:00
}
2021-01-29 22:16:48 +00:00
export async function modifyGroupV2 ( {
conversation ,
2022-07-08 20:46:25 +00:00
usingCredentialsFrom ,
2021-01-29 22:16:48 +00:00
createGroupChange ,
2021-03-24 21:28:55 +00:00
extraConversationsForSend ,
2021-01-29 22:16:48 +00:00
inviteLinkPassword ,
name ,
2023-05-23 23:38:58 +00:00
syncMessageOnly = false ,
2021-01-29 22:16:48 +00:00
} : {
conversation : ConversationModel ;
2022-07-08 20:46:25 +00:00
usingCredentialsFrom : ReadonlyArray < ConversationModel > ;
2021-06-22 14:46:42 +00:00
createGroupChange : ( ) = > Promise < Proto.GroupChange.Actions | undefined > ;
2022-12-22 00:07:02 +00:00
extraConversationsForSend? : ReadonlyArray < string > ;
2021-01-29 22:16:48 +00:00
inviteLinkPassword? : string ;
name : string ;
2023-05-23 23:38:58 +00:00
syncMessageOnly? : boolean ;
2021-01-29 22:16:48 +00:00
} ) : Promise < void > {
2022-02-16 18:36:21 +00:00
const logId = ` ${ name } / ${ conversation . idForLogging ( ) } ` ;
2021-01-29 22:16:48 +00:00
2021-06-07 16:39:13 +00:00
if ( ! getIsGroupV2 ( conversation . attributes ) ) {
2021-01-29 22:16:48 +00:00
throw new Error (
2022-02-16 18:36:21 +00:00
` modifyGroupV2/ ${ logId } : Called for non-GroupV2 conversation `
2021-01-29 22:16:48 +00:00
) ;
}
const startTime = Date . now ( ) ;
2022-11-16 20:18:02 +00:00
const timeoutTime = startTime + MINUTE ;
2021-01-29 22:16:48 +00:00
const MAX_ATTEMPTS = 5 ;
2022-07-08 20:46:25 +00:00
let refreshedCredentials = false ;
2021-01-29 22:16:48 +00:00
for ( let attempt = 0 ; attempt < MAX_ATTEMPTS ; attempt += 1 ) {
2022-02-16 18:36:21 +00:00
log . info ( ` modifyGroupV2/ ${ logId } : Starting attempt ${ attempt } ` ) ;
2021-01-29 22:16:48 +00:00
try {
// eslint-disable-next-line no-await-in-loop
await window . waitForEmptyEventQueue ( ) ;
2022-07-11 18:50:14 +00:00
// Fetch profiles for contacts that do not have credentials (or have
// expired credentials)
{
const membersMissingCredentials = usingCredentialsFrom . filter ( member = >
member . hasProfileKeyCredentialExpired ( )
) ;
const logIds = membersMissingCredentials . map ( member = >
member . idForLogging ( )
) ;
if ( logIds . length !== 0 ) {
log . info ( ` modifyGroupV2/ ${ logId } : Fetching profiles for ${ logIds } ` ) ;
}
// eslint-disable-next-line no-await-in-loop
2022-07-14 00:46:46 +00:00
await Promise . all (
membersMissingCredentials . map ( member = > member . getProfiles ( ) )
2022-07-11 18:50:14 +00:00
) ;
}
2022-02-16 18:36:21 +00:00
log . info ( ` modifyGroupV2/ ${ logId } : Queuing attempt ${ attempt } ` ) ;
2021-01-29 22:16:48 +00:00
// eslint-disable-next-line no-await-in-loop
2021-06-14 21:55:14 +00:00
await conversation . queueJob ( 'modifyGroupV2' , async ( ) = > {
2022-02-16 18:36:21 +00:00
log . info ( ` modifyGroupV2/ ${ logId } : Running attempt ${ attempt } ` ) ;
2021-01-29 22:16:48 +00:00
const actions = await createGroupChange ( ) ;
if ( ! actions ) {
2021-09-17 18:27:53 +00:00
log . warn (
2022-02-16 18:36:21 +00:00
` modifyGroupV2/ ${ logId } : No change actions. Returning early. `
2021-01-29 22:16:48 +00:00
) ;
return ;
}
// The new revision has to be exactly one more than the current revision
// or it won't upload properly, and it won't apply in maybeUpdateGroup
const currentRevision = conversation . get ( 'revision' ) ;
const newRevision = actions . version ;
if ( ( currentRevision || 0 ) + 1 !== newRevision ) {
throw new Error (
2022-02-16 18:36:21 +00:00
` modifyGroupV2/ ${ logId } : Revision mismatch - ${ currentRevision } to ${ newRevision } . `
2021-01-29 22:16:48 +00:00
) ;
}
2024-05-20 18:15:39 +00:00
const { groupId , secretParams , publicParams } = conversation . attributes ;
strictAssert ( groupId , 'modifyGroupV2: missing groupId' ) ;
strictAssert ( secretParams , 'modifyGroupV2: missing secretParams' ) ;
strictAssert ( publicParams , 'modifyGroupV2: missing publicParams' ) ;
2021-01-29 22:16:48 +00:00
// Upload. If we don't have permission, the server will return an error here.
2024-05-04 00:42:11 +00:00
const groupChangeResponse = await uploadGroupChange ( {
2021-01-29 22:16:48 +00:00
actions ,
2024-05-20 18:15:39 +00:00
groupId ,
groupPublicParamsBase64 : publicParams ,
groupSecretParamsBase64 : secretParams ,
2021-01-29 22:16:48 +00:00
inviteLinkPassword ,
} ) ;
2024-05-20 18:15:39 +00:00
const { groupChange , groupSendEndorsementResponse } =
groupChangeResponse ;
strictAssert ( groupChange , 'modifyGroupV2: missing groupChange' ) ;
2021-01-29 22:16:48 +00:00
2021-11-11 22:43:05 +00:00
const groupChangeBuffer =
Proto . GroupChange . encode ( groupChange ) . finish ( ) ;
2021-06-22 14:46:42 +00:00
const groupChangeBase64 = Bytes . toBase64 ( groupChangeBuffer ) ;
2021-01-29 22:16:48 +00:00
// Apply change locally, just like we would with an incoming change. This will
// change conversation state and add change notifications to the timeline.
await window . Signal . Groups . maybeUpdateGroup ( {
conversation ,
2022-03-08 21:07:52 +00:00
groupChange : {
base64 : groupChangeBase64 ,
isTrusted : true ,
} ,
2021-01-29 22:16:48 +00:00
newRevision ,
} ) ;
2022-02-16 18:36:21 +00:00
const groupV2Info = conversation . getGroupV2Info ( {
includePendingMembers : true ,
extraConversationsForSend ,
} ) ;
strictAssert ( groupV2Info , 'missing groupV2Info' ) ;
2021-01-29 22:16:48 +00:00
2022-02-16 18:36:21 +00:00
await conversationJobQueue . add ( {
type : conversationQueueJobEnum . enum . GroupUpdate ,
2021-01-29 22:16:48 +00:00
conversationId : conversation.id ,
2022-02-25 23:39:24 +00:00
groupChangeBase64 ,
2023-05-23 23:38:58 +00:00
recipients : syncMessageOnly ? [ ] : groupV2Info . members . slice ( ) ,
2022-02-16 18:36:21 +00:00
revision : groupV2Info.revision ,
} ) ;
2024-05-20 18:15:39 +00:00
// Read this after `maybeUpdateGroup` because it may have been updated
const { membersV2 } = conversation . attributes ;
strictAssert ( membersV2 , 'modifyGroupV2: missing membersV2' ) ;
2024-06-04 15:17:48 +00:00
// If we are no longer a member - endorsement won't be present
if ( Bytes . isNotEmpty ( groupSendEndorsementResponse ) ) {
2024-08-20 23:14:27 +00:00
try {
log . info ( ` modifyGroupV2/ ${ logId } : Saving group endorsements ` ) ;
const groupEndorsementData = decodeGroupSendEndorsementResponse ( {
groupId ,
groupSendEndorsementResponse ,
groupSecretParamsBase64 : secretParams ,
groupMembersV2 : membersV2 ,
} ) ;
2024-06-04 15:17:48 +00:00
2024-08-20 23:14:27 +00:00
await DataWriter . replaceAllEndorsementsForGroup (
groupEndorsementData
) ;
} catch ( error ) {
log . warn (
` modifyGroupV2/ ${ logId } : Problem saving group endorsements ${ Errors . toLogFormat ( error ) } `
) ;
}
2024-06-04 15:17:48 +00:00
}
2021-01-29 22:16:48 +00:00
} ) ;
// If we've gotten here with no error, we exit!
2021-09-17 18:27:53 +00:00
log . info (
2022-02-16 18:36:21 +00:00
` modifyGroupV2/ ${ logId } : Update complete, with attempt ${ attempt } ! `
2021-01-29 22:16:48 +00:00
) ;
break ;
} catch ( error ) {
if ( error . code === 409 && Date . now ( ) <= timeoutTime ) {
2021-09-17 18:27:53 +00:00
log . info (
2022-02-16 18:36:21 +00:00
` modifyGroupV2/ ${ logId } : Conflict while updating. Trying again... `
2021-01-29 22:16:48 +00:00
) ;
// eslint-disable-next-line no-await-in-loop
2021-08-06 21:25:15 +00:00
await conversation . fetchLatestGroupV2Data ( { force : true } ) ;
2022-07-08 20:46:25 +00:00
} else if ( error . code === 400 && ! refreshedCredentials ) {
const logIds = usingCredentialsFrom . map ( member = >
member . idForLogging ( )
) ;
2022-07-11 18:50:14 +00:00
if ( logIds . length !== 0 ) {
log . warn (
` modifyGroupV2/ ${ logId } : Profile key credentials were not ` +
` up-to-date. Updating profiles for ${ logIds } and retrying `
) ;
}
2022-07-08 20:46:25 +00:00
for ( const member of usingCredentialsFrom ) {
member . set ( {
profileKeyCredential : null ,
profileKeyCredentialExpiration : null ,
} ) ;
}
// eslint-disable-next-line no-await-in-loop
2022-07-14 00:46:46 +00:00
await Promise . all (
usingCredentialsFrom . map ( member = > member . getProfiles ( ) )
2022-07-08 20:46:25 +00:00
) ;
// Fetch credentials only once
refreshedCredentials = true ;
2021-01-29 22:16:48 +00:00
} else if ( error . code === 409 ) {
2021-09-17 18:27:53 +00:00
log . error (
2022-02-16 18:36:21 +00:00
` modifyGroupV2/ ${ logId } : Conflict while updating. Timed out; not retrying. `
2021-01-29 22:16:48 +00:00
) ;
// We don't wait here because we're breaking out of the loop immediately.
2022-12-21 18:41:48 +00:00
void conversation . fetchLatestGroupV2Data ( { force : true } ) ;
2021-01-29 22:16:48 +00:00
throw error ;
} else {
2022-07-08 20:46:25 +00:00
const errorString = Errors . toLogFormat ( error ) ;
2022-02-16 18:36:21 +00:00
log . error ( ` modifyGroupV2/ ${ logId } : Error updating: ${ errorString } ` ) ;
2021-01-29 22:16:48 +00:00
throw error ;
}
}
}
}
2020-09-09 02:25:05 +00:00
// Utility
2021-01-29 22:16:48 +00:00
export function idForLogging ( groupId : string | undefined ) : string {
return ` groupv2( ${ groupId } ) ` ;
2020-11-20 17:30:45 +00:00
}
2021-06-22 14:46:42 +00:00
export function deriveGroupFields ( masterKey : Uint8Array ) : GroupFields {
2021-07-09 19:36:10 +00:00
if ( masterKey . length !== MASTER_KEY_LENGTH ) {
throw new Error (
` deriveGroupFields: masterKey had length ${ masterKey . length } , ` +
` expected ${ MASTER_KEY_LENGTH } `
) ;
}
2021-06-22 14:46:42 +00:00
const cacheKey = Bytes . toBase64 ( masterKey ) ;
2021-04-09 18:20:37 +00:00
const cached = groupFieldsCache . get ( cacheKey ) ;
if ( cached ) {
return cached ;
}
2021-09-17 18:27:53 +00:00
log . info ( 'deriveGroupFields: cache miss' ) ;
2021-04-09 18:20:37 +00:00
2020-09-09 02:25:05 +00:00
const secretParams = deriveGroupSecretParams ( masterKey ) ;
const publicParams = deriveGroupPublicParams ( secretParams ) ;
const id = deriveGroupID ( secretParams ) ;
2021-04-09 18:20:37 +00:00
const fresh = {
2020-09-09 02:25:05 +00:00
id ,
secretParams ,
publicParams ,
} ;
2021-04-09 18:20:37 +00:00
groupFieldsCache . set ( cacheKey , fresh ) ;
return fresh ;
2020-09-09 02:25:05 +00:00
}
2024-05-02 21:39:04 +00:00
async function makeRequestWithCredentials < T > ( {
2020-11-13 19:57:55 +00:00
logId ,
publicParams ,
secretParams ,
request ,
} : {
logId : string ;
publicParams : string ;
secretParams : string ;
request : ( sender : MessageSender , options : GroupCredentialsType ) = > Promise < T > ;
} ) : Promise < T > {
2024-02-22 21:19:50 +00:00
const groupCredentials = getCheckedGroupCredentialsForToday (
2024-05-02 21:39:04 +00:00
` makeRequestWithCredentials/ ${ logId } `
2022-07-08 20:46:25 +00:00
) ;
2020-11-13 19:57:55 +00:00
const sender = window . textsecure . messaging ;
if ( ! sender ) {
throw new Error (
2024-05-02 21:39:04 +00:00
` makeRequestWithCredentials/ ${ logId } : textsecure.messaging is not available! `
2020-11-13 19:57:55 +00:00
) ;
}
2024-05-02 21:39:04 +00:00
log . info ( ` makeRequestWithCredentials/ ${ logId } : starting ` ) ;
2022-07-08 20:46:25 +00:00
2020-11-13 19:57:55 +00:00
const todayOptions = getGroupCredentials ( {
authCredentialBase64 : groupCredentials.today.credential ,
groupPublicParamsBase64 : publicParams ,
groupSecretParamsBase64 : secretParams ,
serverPublicParamsBase64 : window.getServerPublicParams ( ) ,
} ) ;
2024-05-02 21:39:04 +00:00
return request ( sender , todayOptions ) ;
2020-11-13 19:57:55 +00:00
}
export async function fetchMembershipProof ( {
publicParams ,
secretParams ,
} : {
publicParams : string ;
secretParams : string ;
} ) : Promise < string | undefined > {
// Ensure we have the credentials we need before attempting GroupsV2 operations
await maybeFetchNewCredentials ( ) ;
if ( ! publicParams ) {
throw new Error ( 'fetchMembershipProof: group was missing publicParams!' ) ;
}
if ( ! secretParams ) {
throw new Error ( 'fetchMembershipProof: group was missing secretParams!' ) ;
}
2024-05-02 21:39:04 +00:00
const response = await makeRequestWithCredentials ( {
2020-11-13 19:57:55 +00:00
logId : 'fetchMembershipProof' ,
publicParams ,
secretParams ,
request : ( sender , options ) = > sender . getGroupMembershipToken ( options ) ,
} ) ;
2023-11-07 21:31:59 +00:00
return dropNull ( response . token ) ;
2020-11-13 19:57:55 +00:00
}
2021-03-03 20:09:58 +00:00
// Creating a group
2022-07-18 20:05:41 +00:00
export async function createGroupV2 (
options : Readonly < {
name : string ;
avatar : undefined | Uint8Array ;
2022-11-16 20:18:02 +00:00
expireTimer : undefined | DurationInSeconds ;
2022-12-22 00:07:02 +00:00
conversationIds : ReadonlyArray < string > ;
avatars? : ReadonlyArray < AvatarDataType > ;
2022-07-18 20:05:41 +00:00
refreshedCredentials? : boolean ;
} >
) : Promise < ConversationModel > {
const {
name ,
avatar ,
expireTimer ,
conversationIds ,
avatars ,
refreshedCredentials = false ,
} = options ;
2021-03-03 20:09:58 +00:00
// Ensure we have the credentials we need before attempting GroupsV2 operations
await maybeFetchNewCredentials ( ) ;
2021-06-22 14:46:42 +00:00
const ACCESS_ENUM = Proto . AccessControl . AccessRequired ;
const MEMBER_ROLE_ENUM = Proto . Member . Role ;
2021-03-03 20:09:58 +00:00
2021-09-24 00:49:05 +00:00
const masterKeyBuffer = getRandomBytes ( 32 ) ;
2021-03-03 20:09:58 +00:00
const fields = deriveGroupFields ( masterKeyBuffer ) ;
2021-06-22 14:46:42 +00:00
const groupId = Bytes . toBase64 ( fields . id ) ;
2021-03-03 20:09:58 +00:00
const logId = ` groupv2( ${ groupId } ) ` ;
2021-06-22 14:46:42 +00:00
const masterKey = Bytes . toBase64 ( masterKeyBuffer ) ;
const secretParams = Bytes . toBase64 ( fields . secretParams ) ;
const publicParams = Bytes . toBase64 ( fields . publicParams ) ;
2021-03-03 20:09:58 +00:00
2023-08-10 16:43:33 +00:00
const ourAci = window . storage . user . getCheckedAci ( ) ;
2021-03-03 20:09:58 +00:00
2022-07-11 18:50:14 +00:00
const ourConversation =
window . ConversationController . getOurConversationOrThrow ( ) ;
if ( ourConversation . hasProfileKeyCredentialExpired ( ) ) {
log . info ( ` createGroupV2/ ${ logId } : fetching our own credentials ` ) ;
await ourConversation . getProfiles ( ) ;
}
2021-03-03 20:09:58 +00:00
const membersV2 : Array < GroupV2MemberType > = [
{
2023-08-16 20:54:39 +00:00
aci : ourAci ,
2021-03-03 20:09:58 +00:00
role : MEMBER_ROLE_ENUM.ADMINISTRATOR ,
joinedAtVersion : 0 ,
} ,
] ;
const pendingMembersV2 : Array < GroupV2PendingMemberType > = [ ] ;
let uploadedAvatar : undefined | UploadedAvatarType ;
await Promise . all ( [
. . . conversationIds . map ( async conversationId = > {
const contact = window . ConversationController . get ( conversationId ) ;
if ( ! contact ) {
2022-09-15 19:17:15 +00:00
assertDev (
2021-03-03 20:09:58 +00:00
false ,
` createGroupV2/ ${ logId } : missing local contact, skipping `
) ;
return ;
}
2023-08-10 16:43:33 +00:00
const contactServiceId = contact . getServiceId ( ) ;
if ( ! contactServiceId ) {
assertDev (
false ,
` createGroupV2/ ${ logId } : missing service id; skipping `
) ;
2021-03-03 20:09:58 +00:00
return ;
}
// Refresh our local data to be sure
2022-07-11 18:50:14 +00:00
if ( contact . hasProfileKeyCredentialExpired ( ) ) {
2021-03-03 20:09:58 +00:00
await contact . getProfiles ( ) ;
}
if ( contact . get ( 'profileKey' ) && contact . get ( 'profileKeyCredential' ) ) {
2023-08-10 16:43:33 +00:00
strictAssert ( isAciString ( contactServiceId ) , 'profile key without ACI' ) ;
2021-03-03 20:09:58 +00:00
membersV2 . push ( {
2023-08-16 20:54:39 +00:00
aci : contactServiceId ,
2021-03-03 20:09:58 +00:00
role : MEMBER_ROLE_ENUM.DEFAULT ,
joinedAtVersion : 0 ,
} ) ;
} else {
pendingMembersV2 . push ( {
2023-08-10 16:43:33 +00:00
addedByUserId : ourAci ,
2023-08-16 20:54:39 +00:00
serviceId : contactServiceId ,
2021-03-03 20:09:58 +00:00
timestamp : Date.now ( ) ,
role : MEMBER_ROLE_ENUM.DEFAULT ,
} ) ;
}
} ) ,
( async ( ) = > {
if ( ! avatar ) {
return ;
}
uploadedAvatar = await uploadAvatar ( {
data : avatar ,
logId ,
publicParams ,
secretParams ,
} ) ;
} ) ( ) ,
] ) ;
if ( membersV2 . length + pendingMembersV2 . length > getGroupSizeHardLimit ( ) ) {
throw new Error (
` createGroupV2/ ${ logId } : Too many members! Member count: ${ membersV2 . length } , Pending member count: ${ pendingMembersV2 . length } `
) ;
}
const protoAndConversationAttributes = {
name ,
// Core GroupV2 info
revision : 0 ,
publicParams ,
secretParams ,
// GroupV2 state
accessControl : {
attributes : ACCESS_ENUM.MEMBER ,
members : ACCESS_ENUM.MEMBER ,
addFromInviteLink : ACCESS_ENUM.UNSATISFIABLE ,
} ,
membersV2 ,
pendingMembersV2 ,
} ;
2024-05-20 18:15:39 +00:00
const groupProto = buildGroupProto ( {
2021-03-03 20:09:58 +00:00
id : groupId ,
avatarUrl : uploadedAvatar?.key ,
. . . protoAndConversationAttributes ,
} ) ;
2022-07-18 20:05:41 +00:00
try {
2024-05-20 18:15:39 +00:00
const groupResponse = await makeRequestWithCredentials ( {
2022-07-18 20:05:41 +00:00
logId : ` createGroupV2/ ${ logId } ` ,
publicParams ,
secretParams ,
request : ( sender , requestOptions ) = >
sender . createGroup ( groupProto , requestOptions ) ,
} ) ;
2024-05-20 18:15:39 +00:00
const { groupSendEndorsementResponse } = groupResponse ;
strictAssert (
2024-06-04 15:17:48 +00:00
Bytes . isNotEmpty ( groupSendEndorsementResponse ) ,
2024-05-20 18:15:39 +00:00
'missing groupSendEndorsementResponse'
) ;
2024-08-20 23:14:27 +00:00
try {
const groupEndorsementData = decodeGroupSendEndorsementResponse ( {
groupId ,
groupSendEndorsementResponse ,
groupSecretParamsBase64 : secretParams ,
groupMembersV2 : membersV2 ,
} ) ;
2024-05-20 18:15:39 +00:00
2024-08-20 23:14:27 +00:00
await DataWriter . replaceAllEndorsementsForGroup ( groupEndorsementData ) ;
} catch ( error ) {
log . warn (
` createGroupV2/ ${ logId } : Problem saving group endorsements ${ Errors . toLogFormat ( error ) } `
) ;
}
2022-07-18 20:05:41 +00:00
} catch ( error ) {
if ( ! ( error instanceof HTTPError ) ) {
throw error ;
}
if ( error . code !== 400 || refreshedCredentials ) {
throw error ;
}
const logIds = conversationIds . map ( conversationId = > {
const contact = window . ConversationController . get ( conversationId ) ;
if ( ! contact ) {
return ;
}
contact . set ( {
profileKeyCredential : null ,
profileKeyCredentialExpiration : null ,
} ) ;
return contact . idForLogging ( ) ;
} ) ;
log . warn (
` createGroupV2/ ${ logId } : Profile key credentials were not ` +
` up-to-date. Updating profiles for ${ logIds } and retrying `
) ;
return createGroupV2 ( {
. . . options ,
refreshedCredentials : true ,
} ) ;
}
2021-03-03 20:09:58 +00:00
let avatarAttribute : ConversationAttributesType [ 'avatar' ] ;
if ( uploadedAvatar ) {
try {
avatarAttribute = {
url : uploadedAvatar.key ,
2024-07-11 19:44:09 +00:00
. . . ( await window . Signal . Migrations . writeNewAttachmentData (
2021-03-03 20:09:58 +00:00
uploadedAvatar . data
2024-07-11 19:44:09 +00:00
) ) ,
2021-03-03 20:09:58 +00:00
hash : uploadedAvatar.hash ,
} ;
} catch ( err ) {
2021-09-17 18:27:53 +00:00
log . warn (
2021-03-03 20:09:58 +00:00
` createGroupV2/ ${ logId } : avatar failed to save to disk. Continuing on `
) ;
}
}
const now = Date . now ( ) ;
const conversation = await window . ConversationController . getOrCreateAndWait (
groupId ,
'group' ,
{
. . . protoAndConversationAttributes ,
active_at : now ,
2023-08-10 16:43:33 +00:00
addedBy : ourAci ,
2021-03-03 20:09:58 +00:00
avatar : avatarAttribute ,
2021-08-06 00:17:05 +00:00
avatars ,
2021-03-03 20:09:58 +00:00
groupVersion : 2 ,
masterKey ,
profileSharing : true ,
timestamp : now ,
needsStorageServiceSync : true ,
}
) ;
2021-07-28 21:37:09 +00:00
await conversation . queueJob ( 'storageServiceUploadJob' , async ( ) = > {
2022-10-08 00:19:02 +00:00
await storageServiceUploadJob ( ) ;
2021-03-03 20:09:58 +00:00
} ) ;
const timestamp = Date . now ( ) ;
const groupV2Info = conversation . getGroupV2Info ( {
includePendingMembers : true ,
} ) ;
2022-02-16 18:36:21 +00:00
strictAssert ( groupV2Info , 'missing groupV2Info' ) ;
await conversationJobQueue . add ( {
type : conversationQueueJobEnum . enum . GroupUpdate ,
conversationId : conversation.id ,
2022-12-22 00:07:02 +00:00
recipients : groupV2Info.members.slice ( ) ,
2022-02-16 18:36:21 +00:00
revision : groupV2Info.revision ,
2021-03-03 20:09:58 +00:00
} ) ;
const createdTheGroupMessage : MessageAttributesType = {
. . . generateBasicMessage ( ) ,
type : 'group-v2-change' ,
2023-08-16 20:54:39 +00:00
sourceServiceId : ourAci ,
2021-03-03 20:09:58 +00:00
conversationId : conversation.id ,
2022-05-12 02:45:20 +00:00
readStatus : ReadStatus.Read ,
2023-04-11 03:54:43 +00:00
received_at : incrementMessageCounter ( ) ,
2021-03-08 15:23:57 +00:00
received_at_ms : timestamp ,
2022-03-16 00:11:28 +00:00
timestamp ,
2022-05-12 02:45:20 +00:00
seenStatus : SeenStatus.Seen ,
2021-03-03 20:09:58 +00:00
sent_at : timestamp ,
groupV2Change : {
2023-08-10 16:43:33 +00:00
from : ourAci ,
2021-03-03 20:09:58 +00:00
details : [ { type : 'create' } ] ,
} ,
} ;
2024-07-22 18:16:33 +00:00
await DataWriter . saveMessages ( [ createdTheGroupMessage ] , {
2021-03-03 20:09:58 +00:00
forceSave : true ,
2023-08-10 16:43:33 +00:00
ourAci ,
2021-03-03 20:09:58 +00:00
} ) ;
2024-07-25 23:29:49 +00:00
window . MessageCache . __DEPRECATED $register (
createdTheGroupMessage . id ,
new window . Whisper . Message ( createdTheGroupMessage ) ,
2023-10-04 00:12:57 +00:00
'createGroupV2'
) ;
2024-07-25 23:29:49 +00:00
conversation . trigger ( 'newmessage' , createdTheGroupMessage ) ;
2021-03-03 20:09:58 +00:00
2021-06-01 20:45:43 +00:00
if ( expireTimer ) {
2022-06-20 22:43:16 +00:00
await conversation . updateExpirationTimer ( expireTimer , {
reason : 'createGroupV2' ,
2024-08-21 16:03:28 +00:00
version : undefined ,
2022-06-20 22:43:16 +00:00
} ) ;
2021-06-01 20:45:43 +00:00
}
2021-03-03 20:09:58 +00:00
return conversation ;
}
2020-11-20 17:30:45 +00:00
// Migrating a group
export async function hasV1GroupBeenMigrated (
conversation : ConversationModel
) : Promise < boolean > {
const logId = conversation . idForLogging ( ) ;
2021-06-07 16:39:13 +00:00
const isGroupV1 = getIsGroupV1 ( conversation . attributes ) ;
2020-11-20 17:30:45 +00:00
if ( ! isGroupV1 ) {
2021-09-17 18:27:53 +00:00
log . warn (
2020-11-20 17:30:45 +00:00
` checkForGV2Existence/ ${ logId } : Called for non-GroupV1 conversation! `
) ;
return false ;
}
// Ensure we have the credentials we need before attempting GroupsV2 operations
await maybeFetchNewCredentials ( ) ;
const groupId = conversation . get ( 'groupId' ) ;
if ( ! groupId ) {
throw new Error ( ` checkForGV2Existence/ ${ logId } : No groupId! ` ) ;
}
2021-09-24 00:49:05 +00:00
const idBuffer = Bytes . fromBinary ( groupId ) ;
const masterKeyBuffer = deriveMasterKeyFromGroupV1 ( idBuffer ) ;
2020-11-20 17:30:45 +00:00
const fields = deriveGroupFields ( masterKeyBuffer ) ;
try {
2024-05-02 21:39:04 +00:00
await makeRequestWithCredentials ( {
2020-11-20 17:30:45 +00:00
logId : ` getGroup/ ${ logId } ` ,
2021-06-22 14:46:42 +00:00
publicParams : Bytes.toBase64 ( fields . publicParams ) ,
secretParams : Bytes.toBase64 ( fields . secretParams ) ,
2020-11-20 17:30:45 +00:00
request : ( sender , options ) = > sender . getGroup ( options ) ,
} ) ;
return true ;
} catch ( error ) {
const { code } = error ;
2021-01-12 15:44:44 +00:00
return code !== GROUP_NONEXISTENT_CODE ;
2020-11-20 17:30:45 +00:00
}
}
2021-09-24 00:49:05 +00:00
export function maybeDeriveGroupV2Id ( conversation : ConversationModel ) : boolean {
2021-06-07 16:39:13 +00:00
const isGroupV1 = getIsGroupV1 ( conversation . attributes ) ;
2020-11-20 17:30:45 +00:00
const groupV1Id = conversation . get ( 'groupId' ) ;
const derived = conversation . get ( 'derivedGroupV2Id' ) ;
if ( ! isGroupV1 || ! groupV1Id || derived ) {
return false ;
}
2021-09-24 00:49:05 +00:00
const v1IdBuffer = Bytes . fromBinary ( groupV1Id ) ;
const masterKeyBuffer = deriveMasterKeyFromGroupV1 ( v1IdBuffer ) ;
2020-11-20 17:30:45 +00:00
const fields = deriveGroupFields ( masterKeyBuffer ) ;
2021-06-22 14:46:42 +00:00
const derivedGroupV2Id = Bytes . toBase64 ( fields . id ) ;
2020-11-20 17:30:45 +00:00
conversation . set ( {
derivedGroupV2Id ,
} ) ;
return true ;
}
2022-03-08 21:07:52 +00:00
type WrappedGroupChangeType = Readonly < {
base64 : string ;
isTrusted : boolean ;
} > ;
type MigratePropsType = Readonly < {
2020-11-20 17:30:45 +00:00
conversation : ConversationModel ;
newRevision? : number ;
receivedAt? : number ;
sentAt? : number ;
2022-03-08 21:07:52 +00:00
groupChange? : WrappedGroupChangeType ;
} > ;
2020-11-20 17:30:45 +00:00
export async function isGroupEligibleToMigrate (
conversation : ConversationModel
) : Promise < boolean > {
2021-06-07 16:39:13 +00:00
if ( ! getIsGroupV1 ( conversation . attributes ) ) {
2020-11-20 17:30:45 +00:00
return false ;
}
2023-08-10 16:43:33 +00:00
const ourAci = window . storage . user . getCheckedAci ( ) ;
2020-11-20 17:30:45 +00:00
const areWeMember =
2023-08-10 16:43:33 +00:00
! conversation . get ( 'left' ) && conversation . hasMember ( ourAci ) ;
2020-11-20 17:30:45 +00:00
if ( ! areWeMember ) {
return false ;
}
const members = conversation . get ( 'members' ) || [ ] ;
for ( let i = 0 , max = members . length ; i < max ; i += 1 ) {
const identifier = members [ i ] ;
const contact = window . ConversationController . get ( identifier ) ;
if ( ! contact ) {
return false ;
}
2023-08-16 20:54:39 +00:00
if ( ! contact . getServiceId ( ) ) {
2020-11-20 17:30:45 +00:00
return false ;
}
}
return true ;
}
2020-12-01 16:42:35 +00:00
export async function getGroupMigrationMembers (
conversation : ConversationModel
) : Promise < {
droppedGV2MemberIds : Array < string > ;
membersV2 : Array < GroupV2MemberType > ;
pendingMembersV2 : Array < GroupV2PendingMemberType > ;
previousGroupV1Members : Array < string > ;
} > {
const logId = conversation . idForLogging ( ) ;
2021-06-22 14:46:42 +00:00
const MEMBER_ROLE_ENUM = Proto . Member . Role ;
2020-12-01 16:42:35 +00:00
2021-11-11 22:43:05 +00:00
const ourConversationId =
window . ConversationController . getOurConversationId ( ) ;
2020-12-01 16:42:35 +00:00
if ( ! ourConversationId ) {
throw new Error (
` getGroupMigrationMembers/ ${ logId } : Couldn't fetch our own conversationId! `
) ;
}
2023-08-10 16:43:33 +00:00
const ourAci = window . storage . user . getCheckedAci ( ) ;
2021-10-26 22:59:08 +00:00
2020-12-01 16:42:35 +00:00
let areWeMember = false ;
let areWeInvited = false ;
const previousGroupV1Members = conversation . get ( 'members' ) || [ ] ;
const now = Date . now ( ) ;
const memberLookup : Record < string , boolean > = { } ;
const membersV2 : Array < GroupV2MemberType > = compact (
await Promise . all (
previousGroupV1Members . map ( async e164 = > {
const contact = window . ConversationController . get ( e164 ) ;
if ( ! contact ) {
throw new Error (
` getGroupMigrationMembers/ ${ logId } : membersV2 - missing local contact for ${ e164 } , skipping. `
) ;
}
2023-01-13 00:24:59 +00:00
if (
! isMe ( contact . attributes ) &&
window . Flags . GV2_MIGRATION_DISABLE_ADD
) {
2021-09-17 18:27:53 +00:00
log . warn (
2021-01-12 15:44:44 +00:00
` getGroupMigrationMembers/ ${ logId } : membersV2 - skipping ${ e164 } due to GV2_MIGRATION_DISABLE_ADD flag `
) ;
return null ;
}
2023-08-16 20:54:39 +00:00
const contactAci = contact . getAci ( ) ;
if ( ! contactAci ) {
2023-08-10 16:43:33 +00:00
log . warn (
2023-08-16 20:54:39 +00:00
` getGroupMigrationMembers/ ${ logId } : membersV2 - missing aci for ${ e164 } , skipping. `
2020-12-01 16:42:35 +00:00
) ;
return null ;
}
if ( ! contact . get ( 'profileKey' ) ) {
2021-09-17 18:27:53 +00:00
log . warn (
2020-12-01 16:42:35 +00:00
` getGroupMigrationMembers/ ${ logId } : membersV2 - missing profileKey for member ${ e164 } , skipping. `
) ;
return null ;
}
// Refresh our local data to be sure
2022-12-05 17:42:13 +00:00
if ( ! contact . get ( 'profileKeyCredential' ) ) {
2020-12-01 16:42:35 +00:00
await contact . getProfiles ( ) ;
}
if ( ! contact . get ( 'profileKeyCredential' ) ) {
2021-09-17 18:27:53 +00:00
log . warn (
2020-12-01 16:42:35 +00:00
` getGroupMigrationMembers/ ${ logId } : membersV2 - no profileKeyCredential for ${ e164 } , skipping. `
) ;
return null ;
}
const conversationId = contact . id ;
if ( conversationId === ourConversationId ) {
areWeMember = true ;
}
memberLookup [ conversationId ] = true ;
return {
2023-08-16 20:54:39 +00:00
aci : contactAci ,
2020-12-01 16:42:35 +00:00
role : MEMBER_ROLE_ENUM.ADMINISTRATOR ,
joinedAtVersion : 0 ,
} ;
} )
)
) ;
const droppedGV2MemberIds : Array < string > = [ ] ;
const pendingMembersV2 : Array < GroupV2PendingMemberType > = compact (
( previousGroupV1Members || [ ] ) . map ( e164 = > {
const contact = window . ConversationController . get ( e164 ) ;
if ( ! contact ) {
throw new Error (
` getGroupMigrationMembers/ ${ logId } : pendingMembersV2 - missing local contact for ${ e164 } , skipping. `
) ;
}
const conversationId = contact . id ;
// If we've already added this contact above, we'll skip here
if ( memberLookup [ conversationId ] ) {
return null ;
}
2023-01-13 00:24:59 +00:00
if (
! isMe ( contact . attributes ) &&
window . Flags . GV2_MIGRATION_DISABLE_INVITE
) {
2021-09-17 18:27:53 +00:00
log . warn (
2021-01-12 15:44:44 +00:00
` getGroupMigrationMembers/ ${ logId } : pendingMembersV2 - skipping ${ e164 } due to GV2_MIGRATION_DISABLE_INVITE flag `
) ;
droppedGV2MemberIds . push ( conversationId ) ;
return null ;
}
2023-08-16 20:54:39 +00:00
const contactUuid = contact . getServiceId ( ) ;
2021-10-26 22:59:08 +00:00
if ( ! contactUuid ) {
2021-09-17 18:27:53 +00:00
log . warn (
2020-12-01 16:42:35 +00:00
` getGroupMigrationMembers/ ${ logId } : pendingMembersV2 - missing uuid for ${ e164 } , skipping. `
) ;
droppedGV2MemberIds . push ( conversationId ) ;
return null ;
}
if ( conversationId === ourConversationId ) {
areWeInvited = true ;
}
return {
2023-08-16 20:54:39 +00:00
serviceId : contactUuid ,
2020-12-01 16:42:35 +00:00
timestamp : now ,
2023-08-10 16:43:33 +00:00
addedByUserId : ourAci ,
2020-12-01 23:45:39 +00:00
role : MEMBER_ROLE_ENUM.ADMINISTRATOR ,
2020-12-01 16:42:35 +00:00
} ;
} )
) ;
2020-12-01 23:45:39 +00:00
if ( ! areWeMember ) {
throw new Error ( ` getGroupMigrationMembers/ ${ logId } : We are not a member! ` ) ;
}
if ( areWeInvited ) {
throw new Error ( ` getGroupMigrationMembers/ ${ logId } : We are invited! ` ) ;
}
2020-12-01 16:42:35 +00:00
return {
droppedGV2MemberIds ,
membersV2 ,
pendingMembersV2 ,
previousGroupV1Members ,
} ;
}
2020-11-20 17:30:45 +00:00
// This is called when the user chooses to migrate a GroupV1. It will update the server,
// then let all members know about the new group.
export async function initiateMigrationToGroupV2 (
conversation : ConversationModel
) : Promise < void > {
// Ensure we have the credentials we need before attempting GroupsV2 operations
await maybeFetchNewCredentials ( ) ;
try {
2021-06-14 21:55:14 +00:00
await conversation . queueJob ( 'initiateMigrationToGroupV2' , async ( ) = > {
2021-06-22 14:46:42 +00:00
const ACCESS_ENUM = Proto . AccessControl . AccessRequired ;
2020-11-20 17:30:45 +00:00
2022-12-14 01:56:41 +00:00
const isEligible = await isGroupEligibleToMigrate ( conversation ) ;
2020-11-20 17:30:45 +00:00
const previousGroupV1Id = conversation . get ( 'groupId' ) ;
if ( ! isEligible || ! previousGroupV1Id ) {
throw new Error (
` initiateMigrationToGroupV2: conversation is not eligible to migrate! ${ conversation . idForLogging ( ) } `
) ;
}
2021-09-24 00:49:05 +00:00
const groupV1IdBuffer = Bytes . fromBinary ( previousGroupV1Id ) ;
const masterKeyBuffer = deriveMasterKeyFromGroupV1 ( groupV1IdBuffer ) ;
2020-11-20 17:30:45 +00:00
const fields = deriveGroupFields ( masterKeyBuffer ) ;
2021-06-22 14:46:42 +00:00
const groupId = Bytes . toBase64 ( fields . id ) ;
2020-11-20 17:30:45 +00:00
const logId = ` groupv2( ${ groupId } ) ` ;
2021-09-17 18:27:53 +00:00
log . info (
2020-11-20 17:30:45 +00:00
` initiateMigrationToGroupV2/ ${ logId } : Migrating from ${ conversation . idForLogging ( ) } `
) ;
2021-06-22 14:46:42 +00:00
const masterKey = Bytes . toBase64 ( masterKeyBuffer ) ;
const secretParams = Bytes . toBase64 ( fields . secretParams ) ;
const publicParams = Bytes . toBase64 ( fields . publicParams ) ;
2020-11-20 17:30:45 +00:00
2021-11-11 22:43:05 +00:00
const ourConversationId =
window . ConversationController . getOurConversationId ( ) ;
2020-11-20 17:30:45 +00:00
if ( ! ourConversationId ) {
throw new Error (
` initiateMigrationToGroupV2/ ${ logId } : Couldn't fetch our own conversationId! `
) ;
}
2021-11-11 22:43:05 +00:00
const ourConversation =
window . ConversationController . get ( ourConversationId ) ;
2021-03-03 20:09:58 +00:00
if ( ! ourConversation ) {
throw new Error (
` initiateMigrationToGroupV2/ ${ logId } : cannot get our own conversation. Cannot migrate `
) ;
}
2020-11-20 17:30:45 +00:00
2020-12-01 16:42:35 +00:00
const {
membersV2 ,
pendingMembersV2 ,
droppedGV2MemberIds ,
previousGroupV1Members ,
} = await getGroupMigrationMembers ( conversation ) ;
2020-11-20 17:30:45 +00:00
2021-03-03 20:09:58 +00:00
if (
membersV2 . length + pendingMembersV2 . length >
getGroupSizeHardLimit ( )
) {
2020-12-01 16:42:35 +00:00
throw new Error (
` initiateMigrationToGroupV2/ ${ logId } : Too many members! Member count: ${ membersV2 . length } , Pending member count: ${ pendingMembersV2 . length } `
) ;
}
2020-11-20 17:30:45 +00:00
// Note: A few group elements don't need to change here:
// - name
// - expireTimer
2021-03-03 20:09:58 +00:00
let avatarAttribute : ConversationAttributesType [ 'avatar' ] ;
2024-07-11 19:44:09 +00:00
const { avatar : currentAvatar } = conversation . attributes ;
if ( currentAvatar ? . path ) {
2024-07-24 00:31:40 +00:00
const avatarData =
await window . Signal . Migrations . readAttachmentData ( currentAvatar ) ;
2021-03-03 20:09:58 +00:00
const { hash , key } = await uploadAvatar ( {
logId ,
publicParams ,
secretParams ,
2024-07-11 19:44:09 +00:00
data : avatarData ,
2021-03-03 20:09:58 +00:00
} ) ;
avatarAttribute = {
2024-07-11 19:44:09 +00:00
. . . currentAvatar ,
2021-03-03 20:09:58 +00:00
url : key ,
hash ,
} ;
}
2020-11-20 17:30:45 +00:00
const newAttributes = {
. . . conversation . attributes ,
2021-03-03 20:09:58 +00:00
avatar : avatarAttribute ,
2020-11-20 17:30:45 +00:00
// Core GroupV2 info
revision : 0 ,
groupId ,
groupVersion : 2 ,
masterKey ,
publicParams ,
secretParams ,
// GroupV2 state
accessControl : {
attributes : ACCESS_ENUM.MEMBER ,
members : ACCESS_ENUM.MEMBER ,
2021-01-29 22:16:48 +00:00
addFromInviteLink : ACCESS_ENUM.UNSATISFIABLE ,
2020-11-20 17:30:45 +00:00
} ,
membersV2 ,
pendingMembersV2 ,
// Capture previous GroupV1 data for future use
previousGroupV1Id ,
previousGroupV1Members ,
// Clear storage ID, since we need to start over on the storage service
storageID : undefined ,
// Clear obsolete data
derivedGroupV2Id : undefined ,
members : undefined ,
} ;
2021-03-03 20:09:58 +00:00
const groupProto = buildGroupProto ( {
. . . newAttributes ,
avatarUrl : avatarAttribute?.url ,
} ) ;
2020-11-20 17:30:45 +00:00
2024-05-20 18:15:39 +00:00
let groupSendEndorsementResponse : Uint8Array | null | undefined ;
2020-11-20 17:30:45 +00:00
try {
2024-05-20 18:15:39 +00:00
const groupResponse = await makeRequestWithCredentials ( {
2024-08-20 23:14:27 +00:00
logId : ` initiateMigrationToGroupV2/ ${ logId } ` ,
2020-11-20 17:30:45 +00:00
publicParams ,
secretParams ,
request : ( sender , options ) = > sender . createGroup ( groupProto , options ) ,
} ) ;
2024-05-20 18:15:39 +00:00
groupSendEndorsementResponse =
groupResponse . groupSendEndorsementResponse ;
2020-11-20 17:30:45 +00:00
} catch ( error ) {
2021-09-17 18:27:53 +00:00
log . error (
2020-11-20 17:30:45 +00:00
` initiateMigrationToGroupV2/ ${ logId } : Error creating group: ` ,
2022-11-22 18:43:43 +00:00
Errors . toLogFormat ( error )
2020-11-20 17:30:45 +00:00
) ;
throw error ;
}
2022-03-16 00:11:28 +00:00
const groupChangeMessages : Array < GroupChangeMessageType > = [ ] ;
2020-11-20 17:30:45 +00:00
groupChangeMessages . push ( {
. . . generateBasicMessage ( ) ,
type : 'group-v1-migration' ,
2024-04-30 13:24:21 +00:00
groupMigration : {
areWeInvited : false ,
droppedMemberIds : droppedGV2MemberIds ,
invitedMembers : pendingMembersV2.map (
( { serviceId : uuid , . . . rest } ) = > {
return { . . . rest , uuid } ;
}
) ,
} ,
2022-05-12 02:45:20 +00:00
readStatus : ReadStatus.Read ,
seenStatus : SeenStatus.Seen ,
2020-11-20 17:30:45 +00:00
} ) ;
await updateGroup ( {
conversation ,
updates : {
newAttributes ,
groupChangeMessages ,
2023-10-02 20:19:55 +00:00
newProfileKeys : new Map ( ) ,
2020-11-20 17:30:45 +00:00
} ,
} ) ;
2021-06-15 00:09:37 +00:00
if ( window . storage . blocked . isGroupBlocked ( previousGroupV1Id ) ) {
2022-12-21 18:41:48 +00:00
await window . storage . blocked . addBlockedGroup ( groupId ) ;
2020-11-20 17:30:45 +00:00
}
// Save these most recent updates to conversation
2024-07-22 18:16:33 +00:00
await updateConversation ( conversation . attributes ) ;
2024-05-20 18:15:39 +00:00
strictAssert (
2024-06-04 15:17:48 +00:00
Bytes . isNotEmpty ( groupSendEndorsementResponse ) ,
2024-05-20 18:15:39 +00:00
'missing groupSendEndorsementResponse'
) ;
2024-08-20 23:14:27 +00:00
try {
const groupEndorsementData = decodeGroupSendEndorsementResponse ( {
groupId ,
groupSendEndorsementResponse ,
groupSecretParamsBase64 : secretParams ,
groupMembersV2 : membersV2 ,
} ) ;
2024-05-20 18:15:39 +00:00
2024-08-20 23:14:27 +00:00
await DataWriter . replaceAllEndorsementsForGroup ( groupEndorsementData ) ;
} catch ( error ) {
log . warn (
` initiateMigrationToGroupV2/ ${ logId } : Problem saving group endorsements ${ Errors . toLogFormat ( error ) } `
) ;
}
2020-11-20 17:30:45 +00:00
} ) ;
} catch ( error ) {
const logId = conversation . idForLogging ( ) ;
2021-06-07 16:39:13 +00:00
if ( ! getIsGroupV1 ( conversation . attributes ) ) {
2020-11-20 17:30:45 +00:00
throw error ;
}
const alreadyMigrated = await hasV1GroupBeenMigrated ( conversation ) ;
if ( ! alreadyMigrated ) {
2021-09-17 18:27:53 +00:00
log . error (
2020-11-20 17:30:45 +00:00
` initiateMigrationToGroupV2/ ${ logId } : Group has not already been migrated, re-throwing error `
) ;
throw error ;
}
await respondToGroupV2Migration ( {
conversation ,
} ) ;
return ;
}
2022-02-16 18:36:21 +00:00
const groupV2Info = conversation . getGroupV2Info ( {
includePendingMembers : true ,
2020-11-20 17:30:45 +00:00
} ) ;
2022-02-16 18:36:21 +00:00
strictAssert ( groupV2Info , 'missing groupV2Info' ) ;
2020-11-20 17:30:45 +00:00
2022-02-16 18:36:21 +00:00
await conversationJobQueue . add ( {
type : conversationQueueJobEnum . enum . GroupUpdate ,
conversationId : conversation.id ,
2022-12-22 00:07:02 +00:00
recipients : groupV2Info.members.slice ( ) ,
2022-02-16 18:36:21 +00:00
revision : groupV2Info.revision ,
} ) ;
2020-11-20 17:30:45 +00:00
}
export async function waitThenRespondToGroupV2Migration (
options : MigratePropsType
) : Promise < void > {
// First wait to process all incoming messages on the websocket
await window . waitForEmptyEventQueue ( ) ;
// Then wait to process all outstanding messages for this conversation
const { conversation } = options ;
2021-06-14 21:55:14 +00:00
await conversation . queueJob ( 'waitThenRespondToGroupV2Migration' , async ( ) = > {
2020-11-20 17:30:45 +00:00
try {
// And finally try to migrate the group
await respondToGroupV2Migration ( options ) ;
} catch ( error ) {
2021-09-17 18:27:53 +00:00
log . error (
2020-11-20 17:30:45 +00:00
` waitThenRespondToGroupV2Migration/ ${ conversation . idForLogging ( ) } : respondToGroupV2Migration failure: ` ,
2022-11-22 18:43:43 +00:00
Errors . toLogFormat ( error )
2020-11-20 17:30:45 +00:00
) ;
}
} ) ;
}
2021-01-29 22:16:48 +00:00
export function buildMigrationBubble (
2022-12-22 00:07:02 +00:00
previousGroupV1MembersIds : ReadonlyArray < string > ,
2021-01-29 22:16:48 +00:00
newAttributes : ConversationAttributesType
2022-03-16 00:11:28 +00:00
) : GroupChangeMessageType {
2023-08-10 16:43:33 +00:00
const ourAci = window . storage . user . getCheckedAci ( ) ;
const ourPni = window . storage . user . getPni ( ) ;
2021-11-11 22:43:05 +00:00
const ourConversationId =
window . ConversationController . getOurConversationId ( ) ;
2021-01-29 22:16:48 +00:00
// Assemble items to commemorate this event for the timeline..
const combinedConversationIds : Array < string > = [
2023-08-16 20:54:39 +00:00
. . . ( newAttributes . membersV2 || [ ] ) . map ( item = > item . aci ) ,
. . . ( newAttributes . pendingMembersV2 || [ ] ) . map ( item = > item . serviceId ) ,
] . map ( serviceId = > {
2022-08-09 21:39:00 +00:00
const conversation = window . ConversationController . lookupOrCreate ( {
2023-08-16 20:54:39 +00:00
serviceId ,
2022-12-03 01:05:27 +00:00
reason : 'buildMigrationBubble' ,
2021-10-26 22:59:08 +00:00
} ) ;
2023-08-16 20:54:39 +00:00
strictAssert ( conversation , ` Conversation not found for ${ serviceId } ` ) ;
2022-08-09 21:39:00 +00:00
return conversation . id ;
2021-10-26 22:59:08 +00:00
} ) ;
2021-01-29 22:16:48 +00:00
const droppedMemberIds : Array < string > = difference (
previousGroupV1MembersIds ,
combinedConversationIds
) . filter ( id = > id && id !== ourConversationId ) ;
const invitedMembers = ( newAttributes . pendingMembersV2 || [ ] ) . filter (
2023-08-16 20:54:39 +00:00
item = > item . serviceId !== ourAci && ! ( ourPni && item . serviceId === ourPni )
2021-01-29 22:16:48 +00:00
) ;
const areWeInvited = ( newAttributes . pendingMembersV2 || [ ] ) . some (
2023-08-16 20:54:39 +00:00
item = > item . serviceId === ourAci || ( ourPni && item . serviceId === ourPni )
2021-01-29 22:16:48 +00:00
) ;
return {
. . . generateBasicMessage ( ) ,
type : 'group-v1-migration' ,
groupMigration : {
areWeInvited ,
2023-08-16 20:54:39 +00:00
invitedMembers : invitedMembers.map ( ( { serviceId : uuid , . . . rest } ) = > {
return { . . . rest , uuid } ;
} ) ,
2021-01-29 22:16:48 +00:00
droppedMemberIds ,
} ,
} ;
}
2022-03-16 00:11:28 +00:00
export function getBasicMigrationBubble ( ) : GroupChangeMessageType {
2022-02-28 22:32:50 +00:00
return {
. . . generateBasicMessage ( ) ,
type : 'group-v1-migration' ,
groupMigration : {
areWeInvited : false ,
invitedMembers : [ ] ,
droppedMemberIds : [ ] ,
} ,
} ;
}
2021-01-29 22:16:48 +00:00
export async function joinGroupV2ViaLinkAndMigrate ( {
approvalRequired ,
conversation ,
inviteLinkPassword ,
revision ,
} : {
approvalRequired : boolean ;
conversation : ConversationModel ;
inviteLinkPassword : string ;
revision : number ;
} ) : Promise < void > {
2021-06-07 16:39:13 +00:00
const isGroupV1 = getIsGroupV1 ( conversation . attributes ) ;
2021-01-29 22:16:48 +00:00
const previousGroupV1Id = conversation . get ( 'groupId' ) ;
if ( ! isGroupV1 || ! previousGroupV1Id ) {
throw new Error (
` joinGroupV2ViaLinkAndMigrate: Conversation is not GroupV1! ${ conversation . idForLogging ( ) } `
) ;
}
// Derive GroupV2 fields
2021-09-24 00:49:05 +00:00
const groupV1IdBuffer = Bytes . fromBinary ( previousGroupV1Id ) ;
const masterKeyBuffer = deriveMasterKeyFromGroupV1 ( groupV1IdBuffer ) ;
2021-01-29 22:16:48 +00:00
const fields = deriveGroupFields ( masterKeyBuffer ) ;
2021-06-22 14:46:42 +00:00
const groupId = Bytes . toBase64 ( fields . id ) ;
2021-01-29 22:16:48 +00:00
const logId = idForLogging ( groupId ) ;
2021-09-17 18:27:53 +00:00
log . info (
2021-01-29 22:16:48 +00:00
` joinGroupV2ViaLinkAndMigrate/ ${ logId } : Migrating from ${ conversation . idForLogging ( ) } `
) ;
2021-06-22 14:46:42 +00:00
const masterKey = Bytes . toBase64 ( masterKeyBuffer ) ;
const secretParams = Bytes . toBase64 ( fields . secretParams ) ;
const publicParams = Bytes . toBase64 ( fields . publicParams ) ;
2021-01-29 22:16:48 +00:00
// A mini-migration, which will not show dropped/invited members
const newAttributes = {
. . . conversation . attributes ,
// Core GroupV2 info
revision ,
groupId ,
groupVersion : 2 ,
masterKey ,
publicParams ,
secretParams ,
groupInviteLinkPassword : inviteLinkPassword ,
2022-05-16 14:53:54 +00:00
addedBy : undefined ,
2021-01-29 22:16:48 +00:00
left : true ,
// Capture previous GroupV1 data for future use
previousGroupV1Id : conversation.get ( 'groupId' ) ,
previousGroupV1Members : conversation.get ( 'members' ) ,
// Clear storage ID, since we need to start over on the storage service
storageID : undefined ,
// Clear obsolete data
derivedGroupV2Id : undefined ,
members : undefined ,
} ;
2022-03-16 00:11:28 +00:00
const groupChangeMessages : Array < GroupChangeMessageType > = [
2021-01-29 22:16:48 +00:00
{
. . . generateBasicMessage ( ) ,
type : 'group-v1-migration' ,
groupMigration : {
areWeInvited : false ,
invitedMembers : [ ] ,
droppedMemberIds : [ ] ,
} ,
} ,
] ;
await updateGroup ( {
conversation ,
updates : {
newAttributes ,
groupChangeMessages ,
2023-10-02 20:19:55 +00:00
newProfileKeys : new Map ( ) ,
2021-01-29 22:16:48 +00:00
} ,
} ) ;
// Now things are set up, so we can go through normal channels
await conversation . joinGroupV2ViaLink ( {
inviteLinkPassword ,
approvalRequired ,
} ) ;
}
2020-11-20 17:30:45 +00:00
// This may be called from storage service, an out-of-band check, or an incoming message.
// If this is kicked off via an incoming message, we want to do the right thing and hit
// the log endpoint - the parameters beyond conversation are needed in that scenario.
export async function respondToGroupV2Migration ( {
conversation ,
2022-03-08 21:07:52 +00:00
groupChange ,
2020-11-20 17:30:45 +00:00
newRevision ,
receivedAt ,
sentAt ,
} : MigratePropsType ) : Promise < void > {
// Ensure we have the credentials we need before attempting GroupsV2 operations
await maybeFetchNewCredentials ( ) ;
2021-06-07 16:39:13 +00:00
const isGroupV1 = getIsGroupV1 ( conversation . attributes ) ;
2020-11-20 17:30:45 +00:00
const previousGroupV1Id = conversation . get ( 'groupId' ) ;
if ( ! isGroupV1 || ! previousGroupV1Id ) {
throw new Error (
` respondToGroupV2Migration: Conversation is not GroupV1! ${ conversation . idForLogging ( ) } `
) ;
}
2023-08-10 16:43:33 +00:00
const ourAci = window . storage . user . getCheckedAci ( ) ;
const wereWePreviouslyAMember = conversation . hasMember ( ourAci ) ;
2020-11-20 17:30:45 +00:00
// Derive GroupV2 fields
2021-09-24 00:49:05 +00:00
const groupV1IdBuffer = Bytes . fromBinary ( previousGroupV1Id ) ;
const masterKeyBuffer = deriveMasterKeyFromGroupV1 ( groupV1IdBuffer ) ;
2020-11-20 17:30:45 +00:00
const fields = deriveGroupFields ( masterKeyBuffer ) ;
2021-06-22 14:46:42 +00:00
const groupId = Bytes . toBase64 ( fields . id ) ;
2021-01-29 22:16:48 +00:00
const logId = idForLogging ( groupId ) ;
2021-09-17 18:27:53 +00:00
log . info (
2020-11-20 17:30:45 +00:00
` respondToGroupV2Migration/ ${ logId } : Migrating from ${ conversation . idForLogging ( ) } `
) ;
2021-06-22 14:46:42 +00:00
const masterKey = Bytes . toBase64 ( masterKeyBuffer ) ;
const secretParams = Bytes . toBase64 ( fields . secretParams ) ;
const publicParams = Bytes . toBase64 ( fields . publicParams ) ;
2020-11-20 17:30:45 +00:00
const previousGroupV1Members = conversation . get ( 'members' ) ;
const previousGroupV1MembersIds = conversation . getMemberIds ( ) ;
// Skeleton of the new group state - not useful until we add the group's server state
const attributes = {
. . . conversation . attributes ,
// Core GroupV2 info
revision : 0 ,
groupId ,
groupVersion : 2 ,
masterKey ,
publicParams ,
secretParams ,
// Capture previous GroupV1 data for future use
previousGroupV1Id ,
previousGroupV1Members ,
// Clear storage ID, since we need to start over on the storage service
storageID : undefined ,
// Clear obsolete data
derivedGroupV2Id : undefined ,
members : undefined ,
} ;
2021-06-22 14:46:42 +00:00
let firstGroupState : Proto.IGroup | null | undefined ;
2024-05-20 18:15:39 +00:00
let groupSendEndorsementResponse : Uint8Array | null | undefined ;
2020-11-20 17:30:45 +00:00
try {
2024-05-02 21:39:04 +00:00
const response : GroupLogResponseType = await makeRequestWithCredentials ( {
2020-11-20 17:30:45 +00:00
logId : ` getGroupLog/ ${ logId } ` ,
publicParams ,
secretParams ,
2022-02-04 21:42:20 +00:00
request : ( sender , options ) = >
sender . getGroupLog (
{
startVersion : 0 ,
includeFirstState : true ,
includeLastState : false ,
maxSupportedChangeEpoch : SUPPORTED_CHANGE_EPOCH ,
2024-05-20 18:15:39 +00:00
cachedEndorsementsExpiration : null , // we won't have them here
2022-02-04 21:42:20 +00:00
} ,
options
) ,
2020-11-20 17:30:45 +00:00
} ) ;
// Attempt to start with the first group state, only later processing future updates
firstGroupState = response ? . changes ? . groupChanges ? . [ 0 ] ? . groupState ;
2024-05-20 18:15:39 +00:00
groupSendEndorsementResponse = response . groupSendEndorsementResponse ;
2020-11-20 17:30:45 +00:00
} catch ( error ) {
if ( error . code === GROUP_ACCESS_DENIED_CODE ) {
2021-09-17 18:27:53 +00:00
log . info (
2020-11-20 17:30:45 +00:00
` respondToGroupV2Migration/ ${ logId } : Failed to access log endpoint; fetching full group state `
) ;
2021-01-12 15:44:44 +00:00
try {
2024-05-04 00:42:11 +00:00
const groupResponse = await makeRequestWithCredentials ( {
2021-01-12 15:44:44 +00:00
logId : ` getGroup/ ${ logId } ` ,
publicParams ,
secretParams ,
request : ( sender , options ) = > sender . getGroup ( options ) ,
} ) ;
2024-05-20 18:15:39 +00:00
2024-05-04 00:42:11 +00:00
firstGroupState = groupResponse . group ;
2024-05-20 18:15:39 +00:00
groupSendEndorsementResponse =
groupResponse . groupSendEndorsementResponse ;
2021-01-12 15:44:44 +00:00
} catch ( secondError ) {
if ( secondError . code === GROUP_ACCESS_DENIED_CODE ) {
2021-09-17 18:27:53 +00:00
log . info (
2021-01-12 15:44:44 +00:00
` respondToGroupV2Migration/ ${ logId } : Failed to access state endpoint; user is no longer part of group `
) ;
2022-02-28 22:32:50 +00:00
if ( window . storage . blocked . isGroupBlocked ( previousGroupV1Id ) ) {
2022-12-21 18:41:48 +00:00
await window . storage . blocked . addBlockedGroup ( groupId ) ;
2022-02-28 22:32:50 +00:00
}
2021-01-12 15:44:44 +00:00
if ( wereWePreviouslyAMember ) {
2022-02-28 22:32:50 +00:00
log . info (
` respondToGroupV2Migration/ ${ logId } : Upgrading group with migration/removed events `
) ;
2021-01-12 15:44:44 +00:00
const ourNumber = window . textsecure . storage . user . getNumber ( ) ;
await updateGroup ( {
conversation ,
receivedAt ,
sentAt ,
updates : {
newAttributes : {
2022-02-28 22:32:50 +00:00
// Because we're using attributes here, we upgrade this to a v2 group
. . . attributes ,
2022-05-16 14:53:54 +00:00
addedBy : undefined ,
2021-01-12 15:44:44 +00:00
left : true ,
members : ( conversation . get ( 'members' ) || [ ] ) . filter (
2023-08-10 16:43:33 +00:00
item = > item !== ourAci && item !== ourNumber
2021-01-12 15:44:44 +00:00
) ,
} ,
groupChangeMessages : [
2022-05-12 02:45:20 +00:00
{
. . . getBasicMigrationBubble ( ) ,
readStatus : ReadStatus.Read ,
seenStatus : SeenStatus.Seen ,
} ,
2021-01-12 15:44:44 +00:00
{
. . . generateBasicMessage ( ) ,
type : 'group-v2-change' ,
groupV2Change : {
details : [
{
type : 'member-remove' as const ,
2023-08-16 20:54:39 +00:00
aci : ourAci ,
2021-01-12 15:44:44 +00:00
} ,
] ,
} ,
2022-05-12 02:45:20 +00:00
readStatus : ReadStatus.Read ,
seenStatus : SeenStatus.Unseen ,
2021-01-12 15:44:44 +00:00
} ,
] ,
2023-10-02 20:19:55 +00:00
newProfileKeys : new Map ( ) ,
2021-01-12 15:44:44 +00:00
} ,
} ) ;
return ;
}
2022-02-28 22:32:50 +00:00
log . info (
` respondToGroupV2Migration/ ${ logId } : Upgrading group with migration event; no removed event `
) ;
await updateGroup ( {
conversation ,
receivedAt ,
sentAt ,
updates : {
newAttributes : attributes ,
2022-05-12 02:45:20 +00:00
groupChangeMessages : [
{
. . . getBasicMigrationBubble ( ) ,
readStatus : ReadStatus.Read ,
seenStatus : SeenStatus.Seen ,
} ,
] ,
2023-10-02 20:19:55 +00:00
newProfileKeys : new Map ( ) ,
2022-02-28 22:32:50 +00:00
} ,
} ) ;
return ;
2021-01-12 15:44:44 +00:00
}
throw secondError ;
}
2020-11-20 17:30:45 +00:00
} else {
throw error ;
}
}
if ( ! firstGroupState ) {
throw new Error (
` respondToGroupV2Migration/ ${ logId } : Couldn't get a first group state! `
) ;
}
const groupState = decryptGroupState (
firstGroupState ,
attributes . secretParams ,
logId
) ;
2021-04-07 22:45:31 +00:00
const { newAttributes , newProfileKeys } = await applyGroupState ( {
2020-11-20 17:30:45 +00:00
group : attributes ,
groupState ,
} ) ;
2021-01-29 22:16:48 +00:00
// Generate notifications into the timeline
2022-03-16 00:11:28 +00:00
const groupChangeMessages : Array < GroupChangeMessageType > = [ ] ;
2021-01-29 22:16:48 +00:00
2022-05-12 02:45:20 +00:00
groupChangeMessages . push ( {
. . . buildMigrationBubble ( previousGroupV1MembersIds , newAttributes ) ,
readStatus : ReadStatus.Read ,
seenStatus : SeenStatus.Seen ,
} ) ;
2020-11-20 17:30:45 +00:00
2020-12-01 23:45:39 +00:00
const areWeInvited = ( newAttributes . pendingMembersV2 || [ ] ) . some (
2023-08-16 20:54:39 +00:00
item = > item . serviceId === ourAci
2020-12-01 23:45:39 +00:00
) ;
const areWeMember = ( newAttributes . membersV2 || [ ] ) . some (
2023-08-16 20:54:39 +00:00
item = > item . aci === ourAci
2020-12-01 23:45:39 +00:00
) ;
2020-11-20 17:30:45 +00:00
if ( ! areWeInvited && ! areWeMember ) {
2020-12-01 23:45:39 +00:00
// Add a message to the timeline saying the user was removed. This shouldn't happen.
2020-11-20 17:30:45 +00:00
groupChangeMessages . push ( {
. . . generateBasicMessage ( ) ,
type : 'group-v2-change' ,
groupV2Change : {
details : [
{
type : 'member-remove' as const ,
2023-08-16 20:54:39 +00:00
aci : ourAci ,
2020-11-20 17:30:45 +00:00
} ,
] ,
} ,
2022-05-12 02:45:20 +00:00
readStatus : ReadStatus.Read ,
seenStatus : SeenStatus.Unseen ,
2020-11-20 17:30:45 +00:00
} ) ;
}
// This buffer ensures that all migration-related messages are sorted above
// any initiating message. We need to do this because groupChangeMessages are
// already sorted via updates to sentAt inside of updateGroup().
const SORT_BUFFER = 1000 ;
await updateGroup ( {
conversation ,
receivedAt ,
sentAt : sentAt ? sentAt - SORT_BUFFER : undefined ,
updates : {
newAttributes ,
groupChangeMessages ,
2023-10-02 20:19:55 +00:00
newProfileKeys : profileKeysToMap ( newProfileKeys ) ,
2020-11-20 17:30:45 +00:00
} ,
} ) ;
2021-06-15 00:09:37 +00:00
if ( window . storage . blocked . isGroupBlocked ( previousGroupV1Id ) ) {
2022-12-21 18:41:48 +00:00
await window . storage . blocked . addBlockedGroup ( groupId ) ;
2020-11-20 17:30:45 +00:00
}
// Save these most recent updates to conversation
2024-07-22 18:16:33 +00:00
await updateConversation ( conversation . attributes ) ;
2020-11-20 17:30:45 +00:00
// Finally, check for any changes to the group since its initial creation using normal
// group update codepaths.
await maybeUpdateGroup ( {
conversation ,
2022-03-08 21:07:52 +00:00
groupChange ,
2020-11-20 17:30:45 +00:00
newRevision ,
receivedAt ,
sentAt ,
} ) ;
2024-05-20 18:15:39 +00:00
2024-06-04 15:17:48 +00:00
if ( Bytes . isNotEmpty ( groupSendEndorsementResponse ) ) {
2024-08-20 23:14:27 +00:00
try {
const { membersV2 } = conversation . attributes ;
strictAssert ( membersV2 , 'missing membersV2' ) ;
const groupEndorsementData = decodeGroupSendEndorsementResponse ( {
groupId ,
groupSendEndorsementResponse ,
groupSecretParamsBase64 : secretParams ,
groupMembersV2 : membersV2 ,
} ) ;
2024-05-20 18:15:39 +00:00
2024-08-20 23:14:27 +00:00
await DataWriter . replaceAllEndorsementsForGroup ( groupEndorsementData ) ;
} catch ( error ) {
log . warn (
` respondToGroupV2Migration/ ${ logId } : Problem saving group endorsements ${ Errors . toLogFormat ( error ) } `
) ;
}
2024-05-20 18:15:39 +00:00
}
2020-11-20 17:30:45 +00:00
}
2020-09-09 02:25:05 +00:00
// Fetching and applying group changes
2022-03-08 21:07:52 +00:00
type MaybeUpdatePropsType = Readonly < {
2020-09-24 20:57:54 +00:00
conversation : ConversationModel ;
2020-09-09 02:25:05 +00:00
newRevision? : number ;
2020-09-10 21:04:45 +00:00
receivedAt? : number ;
sentAt? : number ;
2020-09-09 02:25:05 +00:00
dropInitialJoinMessage? : boolean ;
2021-08-06 21:25:15 +00:00
force? : boolean ;
2022-03-08 21:07:52 +00:00
groupChange? : WrappedGroupChangeType ;
} > ;
2020-09-09 02:25:05 +00:00
2022-11-16 20:18:02 +00:00
const FIVE_MINUTES = 5 * MINUTE ;
2021-05-07 20:07:24 +00:00
2020-09-11 19:37:01 +00:00
export async function waitThenMaybeUpdateGroup (
2021-04-20 23:16:49 +00:00
options : MaybeUpdatePropsType ,
2022-05-16 14:53:54 +00:00
{ viaFirstStorageSync = false } = { }
2020-09-11 19:37:01 +00:00
) : Promise < void > {
2021-07-16 17:43:33 +00:00
const { conversation } = options ;
if ( conversation . isBlocked ( ) ) {
2021-09-17 18:27:53 +00:00
log . info (
2021-07-16 17:43:33 +00:00
` waitThenMaybeUpdateGroup: Group ${ conversation . idForLogging ( ) } is blocked, returning early `
) ;
return ;
}
2020-09-09 02:25:05 +00:00
// First wait to process all incoming messages on the websocket
await window . waitForEmptyEventQueue ( ) ;
2021-07-16 17:43:33 +00:00
// Then make sure we haven't fetched this group too recently
2021-05-07 20:07:24 +00:00
const { lastSuccessfulGroupFetch = 0 } = conversation ;
2021-08-06 21:25:15 +00:00
if (
! options . force &&
isMoreRecentThan ( lastSuccessfulGroupFetch , FIVE_MINUTES )
) {
2021-05-07 20:07:24 +00:00
const waitTime = lastSuccessfulGroupFetch + FIVE_MINUTES - Date . now ( ) ;
2021-09-17 18:27:53 +00:00
log . info (
2021-05-07 20:07:24 +00:00
` waitThenMaybeUpdateGroup/ ${ conversation . idForLogging ( ) } : group update ` +
` was fetched recently, skipping for ${ waitTime } ms `
) ;
return ;
}
2021-07-16 17:43:33 +00:00
// Then wait to process all outstanding messages for this conversation
2021-06-14 21:55:14 +00:00
await conversation . queueJob ( 'waitThenMaybeUpdateGroup' , async ( ) = > {
2020-09-09 02:25:05 +00:00
try {
// And finally try to update the group
2022-05-16 14:53:54 +00:00
await maybeUpdateGroup ( options , { viaFirstStorageSync } ) ;
2021-05-07 20:07:24 +00:00
conversation . lastSuccessfulGroupFetch = Date . now ( ) ;
2020-09-09 02:25:05 +00:00
} catch ( error ) {
2021-09-17 18:27:53 +00:00
log . error (
2020-09-09 02:25:05 +00:00
` waitThenMaybeUpdateGroup/ ${ conversation . idForLogging ( ) } : maybeUpdateGroup failure: ` ,
2022-11-22 18:43:43 +00:00
Errors . toLogFormat ( error )
2020-09-09 02:25:05 +00:00
) ;
}
} ) ;
}
2021-04-20 23:16:49 +00:00
export async function maybeUpdateGroup (
{
conversation ,
dropInitialJoinMessage ,
2022-03-08 21:07:52 +00:00
groupChange ,
2021-04-20 23:16:49 +00:00
newRevision ,
receivedAt ,
sentAt ,
} : MaybeUpdatePropsType ,
2022-05-16 14:53:54 +00:00
{ viaFirstStorageSync = false } = { }
2021-04-20 23:16:49 +00:00
) : Promise < void > {
2020-09-09 02:25:05 +00:00
const logId = conversation . idForLogging ( ) ;
try {
// Ensure we have the credentials we need before attempting GroupsV2 operations
await maybeFetchNewCredentials ( ) ;
2020-10-06 17:06:34 +00:00
const updates = await getGroupUpdates ( {
2020-09-09 02:25:05 +00:00
group : conversation.attributes ,
serverPublicParamsBase64 : window.getServerPublicParams ( ) ,
newRevision ,
2022-03-08 21:07:52 +00:00
groupChange ,
2020-09-09 02:25:05 +00:00
dropInitialJoinMessage ,
} ) ;
2021-04-20 23:16:49 +00:00
await updateGroup (
{ conversation , receivedAt , sentAt , updates } ,
2022-05-16 14:53:54 +00:00
{ viaFirstStorageSync }
2021-04-20 23:16:49 +00:00
) ;
2020-09-09 02:25:05 +00:00
} catch ( error ) {
2021-09-17 18:27:53 +00:00
log . error (
2020-09-09 02:25:05 +00:00
` maybeUpdateGroup/ ${ logId } : Failed to update group: ` ,
2022-11-22 18:43:43 +00:00
Errors . toLogFormat ( error )
2020-09-09 02:25:05 +00:00
) ;
throw error ;
}
}
2021-04-20 23:16:49 +00:00
async function updateGroup (
{
conversation ,
receivedAt ,
sentAt ,
updates ,
} : {
conversation : ConversationModel ;
receivedAt? : number ;
sentAt? : number ;
updates : UpdatesResultType ;
} ,
2022-05-16 14:53:54 +00:00
{ viaFirstStorageSync = false } = { }
2021-04-20 23:16:49 +00:00
) : Promise < void > {
2022-01-27 21:46:31 +00:00
const logId = conversation . idForLogging ( ) ;
2023-10-02 20:19:55 +00:00
const { newAttributes , groupChangeMessages , newProfileKeys } = updates ;
2023-08-10 16:43:33 +00:00
const ourAci = window . textsecure . storage . user . getCheckedAci ( ) ;
const ourPni = window . textsecure . storage . user . getPni ( ) ;
2020-10-06 17:06:34 +00:00
2022-05-16 14:53:54 +00:00
const wasMemberOrPending =
2023-08-10 16:43:33 +00:00
conversation . hasMember ( ourAci ) ||
conversation . isMemberPending ( ourAci ) ||
( ourPni && conversation . isMemberPending ( ourPni ) ) ;
2022-05-16 14:53:54 +00:00
const isMemberOrPending =
! newAttributes . left ||
2022-07-08 20:46:25 +00:00
newAttributes . pendingMembersV2 ? . some (
2023-08-16 20:54:39 +00:00
item = > item . serviceId === ourAci || item . serviceId === ourPni
2022-07-08 20:46:25 +00:00
) ;
2020-10-06 17:06:34 +00:00
// Ensure that all generated messages are ordered properly.
// Before the provided timestamp so update messages appear before the
// initiating message, or after now().
2023-04-11 03:54:43 +00:00
const finalReceivedAt = receivedAt || incrementMessageCounter ( ) ;
2021-03-17 22:48:58 +00:00
const initialSentAt = sentAt || Date . now ( ) ;
2020-11-20 17:30:45 +00:00
// GroupV1 -> GroupV2 migration changes the groupId, and we need to update our id-based
// lookups if there's a change on that field.
const previousId = conversation . get ( 'groupId' ) ;
const idChanged = previousId && previousId !== newAttributes . groupId ;
2020-10-06 17:06:34 +00:00
2024-05-24 18:58:51 +00:00
// By updating activeAt we force this conversation into the left panel. We don't want
// all groups to show up on link, and we don't want Unknown Group in the left pane.
2022-05-16 14:53:54 +00:00
let activeAt = conversation . get ( 'active_at' ) || null ;
2024-05-24 18:58:51 +00:00
if ( ! viaFirstStorageSync && newAttributes . name ) {
2021-04-20 23:16:49 +00:00
activeAt = initialSentAt ;
}
2020-10-06 17:06:34 +00:00
// Save all synthetic messages describing group changes
2021-03-17 22:48:58 +00:00
let syntheticSentAt = initialSentAt - ( groupChangeMessages . length + 1 ) ;
2022-03-16 00:11:28 +00:00
const timestamp = Date . now ( ) ;
2020-10-06 17:06:34 +00:00
const changeMessagesToSave = groupChangeMessages . map ( changeMessage = > {
2020-11-20 17:30:45 +00:00
// We do this to preserve the order of the timeline. We only update sentAt to ensure
// that we don't stomp on messages received around the same time as the message
// which initiated this group fetch and in-conversation messages.
syntheticSentAt += 1 ;
2020-10-06 17:06:34 +00:00
return {
. . . changeMessage ,
conversationId : conversation.id ,
2021-03-17 22:48:58 +00:00
received_at : finalReceivedAt ,
received_at_ms : syntheticSentAt ,
2020-11-20 17:30:45 +00:00
sent_at : syntheticSentAt ,
2022-03-16 00:11:28 +00:00
timestamp ,
2020-10-06 17:06:34 +00:00
} ;
} ) ;
2022-01-27 21:46:31 +00:00
const contactsWithoutProfileKey = new Array < ConversationModel > ( ) ;
// Capture profile key for each member in the group, if we don't have it yet
2023-10-02 20:19:55 +00:00
for ( const [ aci , profileKey ] of newProfileKeys ) {
const contact = window . ConversationController . getOrCreate ( aci , 'private' ) ;
2022-01-27 21:46:31 +00:00
2022-02-22 20:34:57 +00:00
if (
2022-04-14 22:26:47 +00:00
! isMe ( contact . attributes ) &&
2023-10-02 20:19:55 +00:00
profileKey &&
profileKey . length > 0 &&
contact . get ( 'profileKey' ) !== profileKey
2022-02-22 20:34:57 +00:00
) {
2022-01-27 21:46:31 +00:00
contactsWithoutProfileKey . push ( contact ) ;
2024-09-16 14:26:42 +00:00
drop ( contact . setProfileKey ( profileKey , { reason : 'updateGroup' } ) ) ;
2022-01-27 21:46:31 +00:00
}
2023-10-02 20:19:55 +00:00
}
2022-01-27 21:46:31 +00:00
2022-03-28 23:05:47 +00:00
let profileFetches : Promise < Array < void > > | undefined ;
2022-01-27 21:46:31 +00:00
if ( contactsWithoutProfileKey . length !== 0 ) {
log . info (
` updateGroup/ ${ logId } : fetching ` +
` ${ contactsWithoutProfileKey . length } missing profiles `
) ;
2022-07-14 00:46:46 +00:00
profileFetches = Promise . all (
contactsWithoutProfileKey . map ( contact = > contact . getProfiles ( ) )
2022-03-28 23:05:47 +00:00
) ;
}
2022-05-16 14:53:54 +00:00
// If we've been added by a blocked contact, then schedule a task to leave group
const justAdded = ! wasMemberOrPending && isMemberOrPending ;
const addedBy =
2022-07-08 20:46:25 +00:00
newAttributes . pendingMembersV2 ? . find (
2023-08-16 20:54:39 +00:00
item = > item . serviceId === ourAci || item . serviceId === ourPni
2022-07-08 20:46:25 +00:00
) ? . addedByUserId || newAttributes . addedBy ;
2022-05-16 14:53:54 +00:00
if ( justAdded && addedBy ) {
const adder = window . ConversationController . get ( addedBy ) ;
if ( adder && adder . isBlocked ( ) ) {
log . warn (
` updateGroup/ ${ logId } : Added to group by blocked user ${ adder . idForLogging ( ) } . Scheduling group leave. `
) ;
// Wait for empty queue to make it more likely the group update succeeds
const waitThenLeave = async ( ) = > {
log . warn ( ` waitThenLeave/ ${ logId } : Waiting for empty event queue. ` ) ;
await window . waitForEmptyEventQueue ( ) ;
log . warn (
` waitThenLeave/ ${ logId } : Empty event queue, starting group leave. `
) ;
await conversation . leaveGroupV2 ( ) ;
log . warn ( ` waitThenLeave/ ${ logId } : Leave complete. ` ) ;
} ;
// Cannot await here, would infinitely block queue
2024-01-31 20:19:47 +00:00
drop ( waitThenLeave ( ) ) ;
// Return early to discard group changes resulting from the blocked user's action.
return ;
}
}
// We update group membership last to ensure that all notifications are in place before
// the group updates happen on the model.
if ( changeMessagesToSave . length > 0 ) {
try {
if ( contactsWithoutProfileKey && contactsWithoutProfileKey . length > 0 ) {
await Promise . race ( [ profileFetches , sleep ( 30 * SECOND ) ] ) ;
log . info (
` updateGroup/ ${ logId } : timed out or finished fetching ${ contactsWithoutProfileKey . length } profiles `
) ;
}
} catch ( error ) {
log . error (
` updateGroup/ ${ logId } : failed to fetch missing profiles ` ,
Errors . toLogFormat ( error )
) ;
2022-05-16 14:53:54 +00:00
}
2024-01-31 20:19:47 +00:00
await appendChangeMessages ( conversation , changeMessagesToSave ) ;
}
conversation . set ( {
2024-06-25 21:50:15 +00:00
. . . newAttributes ,
2024-01-31 20:19:47 +00:00
active_at : activeAt ,
} ) ;
if ( idChanged ) {
conversation . trigger ( 'idUpdated' , conversation , 'groupId' , previousId ) ;
2022-05-16 14:53:54 +00:00
}
2024-01-31 20:19:47 +00:00
// Save these most recent updates to conversation
await updateConversation ( conversation . attributes ) ;
2022-03-16 00:11:28 +00:00
}
// Exported for testing
export function _mergeGroupChangeMessages (
first : MessageAttributesType | undefined ,
second : MessageAttributesType
) : MessageAttributesType | undefined {
if ( ! first ) {
return undefined ;
}
if ( first . type !== 'group-v2-change' || second . type !== first . type ) {
return undefined ;
}
const { groupV2Change : firstChange } = first ;
const { groupV2Change : secondChange } = second ;
if ( ! firstChange || ! secondChange ) {
return undefined ;
}
if ( firstChange . details . length !== 1 && secondChange . details . length !== 1 ) {
return undefined ;
}
const [ firstDetail ] = firstChange . details ;
const [ secondDetail ] = secondChange . details ;
let isApprovalPending : boolean ;
if ( secondDetail . type === 'admin-approval-add-one' ) {
isApprovalPending = true ;
} else if ( secondDetail . type === 'admin-approval-remove-one' ) {
isApprovalPending = false ;
} else {
return undefined ;
}
2023-08-16 20:54:39 +00:00
const { aci } = secondDetail ;
strictAssert ( aci , 'admin approval message should have aci' ) ;
2022-03-16 00:11:28 +00:00
let updatedDetail ;
// Member was previously added and is now removed
if (
! isApprovalPending &&
firstDetail . type === 'admin-approval-add-one' &&
2023-08-16 20:54:39 +00:00
firstDetail . aci === aci
2022-03-16 00:11:28 +00:00
) {
updatedDetail = {
type : 'admin-approval-bounce' as const ,
2023-08-16 20:54:39 +00:00
aci ,
2022-03-16 00:11:28 +00:00
times : 1 ,
isApprovalPending ,
} ;
// There is an existing bounce event - merge this one into it.
} else if (
firstDetail . type === 'admin-approval-bounce' &&
2023-08-16 20:54:39 +00:00
firstDetail . aci === aci &&
2022-03-16 00:11:28 +00:00
firstDetail . isApprovalPending === ! isApprovalPending
) {
updatedDetail = {
type : 'admin-approval-bounce' as const ,
2023-08-16 20:54:39 +00:00
aci ,
2022-03-16 00:11:28 +00:00
times : firstDetail.times + ( isApprovalPending ? 0 : 1 ) ,
isApprovalPending ,
} ;
} else {
return undefined ;
}
return {
. . . first ,
groupV2Change : {
. . . first . groupV2Change ,
details : [ updatedDetail ] ,
} ,
} ;
}
// Exported for testing
export function _isGroupChangeMessageBounceable (
message : MessageAttributesType
) : boolean {
if ( message . type !== 'group-v2-change' ) {
return false ;
}
const { groupV2Change } = message ;
if ( ! groupV2Change ) {
return false ;
}
if ( groupV2Change . details . length !== 1 ) {
return false ;
}
const [ first ] = groupV2Change . details ;
if (
first . type === 'admin-approval-add-one' ||
first . type === 'admin-approval-bounce'
) {
return true ;
}
return false ;
}
async function appendChangeMessages (
conversation : ConversationModel ,
messages : ReadonlyArray < MessageAttributesType >
) : Promise < void > {
const logId = conversation . idForLogging ( ) ;
log . info (
` appendChangeMessages/ ${ logId } : processing ${ messages . length } messages `
) ;
2023-08-10 16:43:33 +00:00
const ourAci = window . textsecure . storage . user . getCheckedAci ( ) ;
2022-03-16 00:11:28 +00:00
2024-07-22 18:16:33 +00:00
let lastMessage = await DataReader . getLastConversationMessage ( {
2022-03-16 00:11:28 +00:00
conversationId : conversation.id ,
} ) ;
if ( lastMessage && ! _isGroupChangeMessageBounceable ( lastMessage ) ) {
lastMessage = undefined ;
}
const mergedMessages = [ ] ;
let previousMessage = lastMessage ;
for ( const message of messages ) {
const merged = _mergeGroupChangeMessages ( previousMessage , message ) ;
if ( ! merged ) {
if ( previousMessage && previousMessage !== lastMessage ) {
mergedMessages . push ( previousMessage ) ;
}
previousMessage = message ;
continue ;
}
previousMessage = merged ;
log . info (
` appendChangeMessages/ ${ logId } : merged ${ message . id } into ${ merged . id } `
) ;
}
if ( previousMessage && previousMessage !== lastMessage ) {
mergedMessages . push ( previousMessage ) ;
}
// Update existing message
if ( lastMessage && mergedMessages [ 0 ] ? . id === lastMessage ? . id ) {
const [ first , . . . rest ] = mergedMessages ;
strictAssert ( first !== undefined , 'First message must be there' ) ;
log . info ( ` appendChangeMessages/ ${ logId } : updating ${ first . id } ` ) ;
2024-07-22 18:16:33 +00:00
await DataWriter . saveMessage ( first , {
2023-08-10 16:43:33 +00:00
ourAci ,
2022-03-16 00:11:28 +00:00
// We don't use forceSave here because this is an update of existing
// message.
} ) ;
log . info (
` appendChangeMessages/ ${ logId } : saving ${ rest . length } new messages `
) ;
2024-07-22 18:16:33 +00:00
await DataWriter . saveMessages ( rest , {
2023-08-10 16:43:33 +00:00
ourAci ,
2022-03-16 00:11:28 +00:00
forceSave : true ,
} ) ;
} else {
log . info (
` appendChangeMessages/ ${ logId } : saving ${ mergedMessages . length } new messages `
) ;
2024-07-22 18:16:33 +00:00
await DataWriter . saveMessages ( mergedMessages , {
2023-08-10 16:43:33 +00:00
ourAci ,
2022-03-16 00:11:28 +00:00
forceSave : true ,
} ) ;
}
let newMessages = 0 ;
for ( const changeMessage of mergedMessages ) {
2023-10-04 00:12:57 +00:00
const existing = window . MessageCache . __DEPRECATED $getById ( changeMessage . id ) ;
2022-03-16 00:11:28 +00:00
// Update existing message
if ( existing ) {
strictAssert (
changeMessage . id === lastMessage ? . id ,
'Should only update group change that was already in the database'
) ;
existing . set ( changeMessage ) ;
continue ;
}
2024-07-25 23:29:49 +00:00
window . MessageCache . __DEPRECATED $register (
changeMessage . id ,
new window . Whisper . Message ( changeMessage ) ,
2023-10-04 00:12:57 +00:00
'appendChangeMessages'
) ;
2024-07-25 23:29:49 +00:00
conversation . trigger ( 'newmessage' , changeMessage ) ;
2022-03-16 00:11:28 +00:00
newMessages += 1 ;
}
// We updated the message, but didn't add new ones - refresh left pane
if ( ! newMessages && mergedMessages . length > 0 ) {
await conversation . updateLastMessage ( ) ;
2022-12-21 18:41:48 +00:00
void conversation . updateUnread ( ) ;
2022-03-16 00:11:28 +00:00
}
2020-10-06 17:06:34 +00:00
}
2022-03-08 21:07:52 +00:00
type GetGroupUpdatesType = Readonly < {
dropInitialJoinMessage? : boolean ;
group : ConversationAttributesType ;
serverPublicParamsBase64 : string ;
newRevision? : number ;
groupChange? : WrappedGroupChangeType ;
} > ;
2020-09-09 02:25:05 +00:00
async function getGroupUpdates ( {
dropInitialJoinMessage ,
group ,
serverPublicParamsBase64 ,
newRevision ,
2022-03-08 21:07:52 +00:00
groupChange : wrappedGroupChange ,
} : GetGroupUpdatesType ) : Promise < UpdatesResultType > {
2021-01-29 22:16:48 +00:00
const logId = idForLogging ( group . groupId ) ;
2020-09-09 02:25:05 +00:00
2021-09-17 18:27:53 +00:00
log . info ( ` getGroupUpdates/ ${ logId } : Starting... ` ) ;
2020-09-09 02:25:05 +00:00
const currentRevision = group . revision ;
const isFirstFetch = ! isNumber ( group . revision ) ;
2023-08-10 16:43:33 +00:00
const ourAci = window . storage . user . getCheckedAci ( ) ;
2020-09-09 02:25:05 +00:00
2020-10-06 17:06:34 +00:00
const isInitialCreationMessage = isFirstFetch && newRevision === 0 ;
2024-08-21 21:31:55 +00:00
const weAreAwaitingApproval = ( group . pendingAdminApprovalV2 || [ ] ) . some (
2023-08-16 20:54:39 +00:00
item = > item . aci === ourAci
2021-01-29 22:16:48 +00:00
) ;
2020-10-06 17:06:34 +00:00
const isOneVersionUp =
isNumber ( currentRevision ) &&
isNumber ( newRevision ) &&
newRevision === currentRevision + 1 ;
2020-09-09 02:25:05 +00:00
if (
2023-01-13 00:24:59 +00:00
window . Flags . GV2_ENABLE_SINGLE_CHANGE_PROCESSING &&
2022-03-08 21:07:52 +00:00
wrappedGroupChange &&
2020-10-06 17:06:34 +00:00
isNumber ( newRevision ) &&
2021-01-29 22:16:48 +00:00
( isInitialCreationMessage || weAreAwaitingApproval || isOneVersionUp )
2020-09-09 02:25:05 +00:00
) {
2021-09-17 18:27:53 +00:00
log . info ( ` getGroupUpdates/ ${ logId } : Processing just one change ` ) ;
2022-03-08 21:07:52 +00:00
const groupChangeBuffer = Bytes . fromBase64 ( wrappedGroupChange . base64 ) ;
2021-06-22 14:46:42 +00:00
const groupChange = Proto . GroupChange . decode ( groupChangeBuffer ) ;
2020-10-21 00:39:13 +00:00
const isChangeSupported =
! isNumber ( groupChange . changeEpoch ) ||
groupChange . changeEpoch <= SUPPORTED_CHANGE_EPOCH ;
if ( isChangeSupported ) {
2022-03-08 21:07:52 +00:00
if ( ! wrappedGroupChange . isTrusted ) {
strictAssert (
2023-11-07 21:31:59 +00:00
groupChange . serverSignature ,
2022-03-08 21:07:52 +00:00
'Server signature must be present in untrusted group change'
) ;
2023-11-07 21:31:59 +00:00
strictAssert (
groupChange . actions ,
'Actions must be present in untrusted group change'
) ;
2022-03-08 21:07:52 +00:00
try {
verifyNotarySignature (
serverPublicParamsBase64 ,
groupChange . actions ,
groupChange . serverSignature
) ;
} catch ( error ) {
log . warn (
` getGroupUpdates/ ${ logId } : verifyNotarySignature failed, ` +
'dropping the message' ,
Errors . toLogFormat ( error )
) ;
return {
newAttributes : group ,
groupChangeMessages : [ ] ,
2023-10-02 20:19:55 +00:00
newProfileKeys : new Map ( ) ,
2022-03-08 21:07:52 +00:00
} ;
}
}
2021-01-29 22:16:48 +00:00
return updateGroupViaSingleChange ( {
group ,
newRevision ,
groupChange ,
} ) ;
2020-10-21 00:39:13 +00:00
}
2021-09-17 18:27:53 +00:00
log . info (
2020-10-21 00:39:13 +00:00
` getGroupUpdates/ ${ logId } : Failing over; group change unsupported `
) ;
2020-09-09 02:25:05 +00:00
}
2024-08-21 21:31:55 +00:00
const areWeMember = ( group . membersV2 || [ ] ) . some ( item = > item . aci === ourAci ) ;
const isReJoin = ! isFirstFetch && ! areWeMember ;
if ( window . Flags . GV2_ENABLE_CHANGE_PROCESSING ) {
2020-09-09 02:25:05 +00:00
try {
2022-05-16 14:53:54 +00:00
return await updateGroupViaLogs ( {
2020-09-09 02:25:05 +00:00
group ,
newRevision ,
} ) ;
} catch ( error ) {
2024-08-21 21:31:55 +00:00
const nextStep = isReJoin
? 'attempting to fetch from re-join revision'
2022-01-21 22:34:32 +00:00
: 'fetching full state' ;
2020-09-09 02:25:05 +00:00
if ( error . code === TEMPORAL_AUTH_REJECTED_CODE ) {
// We will fail over to the updateGroupViaState call below
2021-09-17 18:27:53 +00:00
log . info (
2022-01-21 22:34:32 +00:00
` getGroupUpdates/ ${ logId } : Temporal credential failure, now ${ nextStep } `
2020-09-09 02:25:05 +00:00
) ;
} else if ( error . code === GROUP_ACCESS_DENIED_CODE ) {
// We will fail over to the updateGroupViaState call below
2021-09-17 18:27:53 +00:00
log . info (
2022-01-21 22:34:32 +00:00
` getGroupUpdates/ ${ logId } : Log access denied, now ${ nextStep } `
2020-09-09 02:25:05 +00:00
) ;
} else {
throw error ;
}
}
}
2024-08-21 21:31:55 +00:00
if ( isReJoin && window . Flags . GV2_ENABLE_CHANGE_PROCESSING ) {
try {
return await updateGroupViaLogs ( {
group ,
newRevision ,
isReJoin ,
} ) ;
} catch ( error ) {
if ( error . code === TEMPORAL_AUTH_REJECTED_CODE ) {
// We will fail over to the updateGroupViaState call below
log . info (
` getGroupUpdates/ ${ logId } : Temporal credential failure, now fetching full state `
) ;
} else if ( error . code === GROUP_ACCESS_DENIED_CODE ) {
// We will fail over to the updateGroupViaState call below
log . info (
` getGroupUpdates/ ${ logId } : Log access denied, now fetching full state `
) ;
} else {
throw error ;
}
}
}
2023-01-13 00:24:59 +00:00
if ( window . Flags . GV2_ENABLE_STATE_PROCESSING ) {
2022-05-16 14:53:54 +00:00
try {
return await updateGroupViaState ( {
dropInitialJoinMessage ,
group ,
} ) ;
} catch ( error ) {
if ( error . code === TEMPORAL_AUTH_REJECTED_CODE ) {
log . info (
` getGroupUpdates/ ${ logId } : Temporal credential failure. Failing; we don't know if we have access or not. `
) ;
throw error ;
} else if ( error . code === GROUP_ACCESS_DENIED_CODE ) {
// We will fail over to the updateGroupViaPreJoinInfo call below
log . info (
` getGroupUpdates/ ${ logId } : Failed to get group state. Attempting to fetch pre-join information. `
) ;
} else {
throw error ;
}
}
}
2023-01-13 00:24:59 +00:00
if ( window . Flags . GV2_ENABLE_PRE_JOIN_FETCH ) {
2022-05-16 14:53:54 +00:00
try {
return await updateGroupViaPreJoinInfo ( {
group ,
} ) ;
} catch ( error ) {
if ( error . code === GROUP_ACCESS_DENIED_CODE ) {
return generateLeftGroupChanges ( group ) ;
}
if ( error . code === GROUP_NONEXISTENT_CODE ) {
return generateLeftGroupChanges ( group ) ;
}
// If we get another temporal failure, we'll fail and try again later.
throw error ;
}
2020-12-18 19:27:43 +00:00
}
2021-09-17 18:27:53 +00:00
log . warn (
2020-12-18 19:27:43 +00:00
` getGroupUpdates/ ${ logId } : No processing was legal! Returning empty changeset. `
) ;
return {
newAttributes : group ,
groupChangeMessages : [ ] ,
2023-10-02 20:19:55 +00:00
newProfileKeys : new Map ( ) ,
2020-12-18 19:27:43 +00:00
} ;
2020-09-09 02:25:05 +00:00
}
2022-05-16 14:53:54 +00:00
async function updateGroupViaPreJoinInfo ( {
2020-09-09 02:25:05 +00:00
group ,
} : {
group : ConversationAttributesType ;
} ) : Promise < UpdatesResultType > {
2021-01-29 22:16:48 +00:00
const logId = idForLogging ( group . groupId ) ;
2023-08-10 16:43:33 +00:00
const ourAci = window . textsecure . storage . user . getCheckedAci ( ) ;
2020-09-09 02:25:05 +00:00
2022-05-16 14:53:54 +00:00
const { publicParams , secretParams } = group ;
if ( ! secretParams ) {
throw new Error (
'updateGroupViaPreJoinInfo: group was missing secretParams!'
) ;
}
if ( ! publicParams ) {
throw new Error (
'updateGroupViaPreJoinInfo: group was missing publicParams!'
) ;
}
2020-09-09 02:25:05 +00:00
2022-05-16 14:53:54 +00:00
// No password, but if we're already pending approval, we can access this without it.
const inviteLinkPassword = undefined ;
2024-05-02 21:39:04 +00:00
const preJoinInfo = await makeRequestWithCredentials ( {
2022-05-16 14:53:54 +00:00
logId : ` getPreJoinInfo/ ${ logId } ` ,
publicParams ,
secretParams ,
request : ( sender , options ) = >
sender . getGroupFromLink ( inviteLinkPassword , options ) ,
} ) ;
const approvalRequired =
preJoinInfo . addFromInviteLink ===
Proto . AccessControl . AccessRequired . ADMINISTRATOR ;
// If the group doesn't require approval to join via link, then we should never have
// gotten here.
if ( ! approvalRequired ) {
return generateLeftGroupChanges ( group ) ;
}
2024-06-25 21:50:15 +00:00
let newAttributes : ConversationAttributesType = {
2022-05-16 14:53:54 +00:00
. . . group ,
description : decryptGroupDescription (
2023-11-07 21:31:59 +00:00
dropNull ( preJoinInfo . descriptionBytes ) ,
2022-05-16 14:53:54 +00:00
secretParams
) ,
2023-11-07 21:31:59 +00:00
name : decryptGroupTitle ( dropNull ( preJoinInfo . title ) , secretParams ) ,
2023-03-01 23:48:23 +00:00
left : true ,
members : group.members || [ ] ,
pendingMembersV2 : group.pendingMembersV2 || [ ] ,
2022-05-16 14:53:54 +00:00
pendingAdminApprovalV2 : [
{
2023-08-16 20:54:39 +00:00
aci : ourAci ,
2022-05-16 14:53:54 +00:00
timestamp : Date.now ( ) ,
} ,
] ,
2023-11-07 21:31:59 +00:00
revision : dropNull ( preJoinInfo . version ) ,
2022-05-16 14:53:54 +00:00
temporaryMemberCount : preJoinInfo.memberCount || 1 ,
2020-09-09 02:25:05 +00:00
} ;
2024-06-25 21:50:15 +00:00
newAttributes = {
. . . newAttributes ,
. . . ( await applyNewAvatar (
dropNull ( preJoinInfo . avatar ) ,
newAttributes ,
logId
) ) ,
} ;
2022-05-16 14:53:54 +00:00
return {
newAttributes ,
groupChangeMessages : extractDiffs ( {
old : group ,
current : newAttributes ,
dropInitialJoinMessage : false ,
} ) ,
2023-10-02 20:19:55 +00:00
newProfileKeys : new Map ( ) ,
2022-05-16 14:53:54 +00:00
} ;
}
async function updateGroupViaState ( {
dropInitialJoinMessage ,
group ,
} : {
dropInitialJoinMessage? : boolean ;
group : ConversationAttributesType ;
} ) : Promise < UpdatesResultType > {
const logId = idForLogging ( group . groupId ) ;
const { publicParams , secretParams } = group ;
2024-05-20 18:15:39 +00:00
strictAssert (
secretParams ,
'updateGroupViaState: group was missing secretParams!'
) ;
strictAssert (
publicParams ,
'updateGroupViaState: group was missing publicParams!'
) ;
2022-05-16 14:53:54 +00:00
2024-05-04 00:42:11 +00:00
const groupResponse = await makeRequestWithCredentials ( {
2022-05-16 14:53:54 +00:00
logId : ` getGroup/ ${ logId } ` ,
publicParams ,
secretParams ,
request : ( sender , requestOptions ) = > sender . getGroup ( requestOptions ) ,
} ) ;
2024-05-20 18:15:39 +00:00
const { group : groupState , groupSendEndorsementResponse } = groupResponse ;
strictAssert ( groupState , 'updateGroupViaState: Group state must be present' ) ;
2024-05-04 00:42:11 +00:00
2022-05-16 14:53:54 +00:00
const decryptedGroupState = decryptGroupState (
groupState ,
secretParams ,
logId
) ;
const oldVersion = group . revision ;
const newVersion = decryptedGroupState . version ;
log . info (
` getCurrentGroupState/ ${ logId } : Applying full group state, from version ${ oldVersion } to ${ newVersion } . `
) ;
const { newAttributes , newProfileKeys } = await applyGroupState ( {
group ,
groupState : decryptedGroupState ,
} ) ;
2024-05-20 18:15:39 +00:00
// If we're not in the group, we won't receive endorsements
2024-06-04 15:17:48 +00:00
if ( Bytes . isNotEmpty ( groupSendEndorsementResponse ) ) {
2024-08-20 23:14:27 +00:00
try {
// Use the latest state of the group after applying changes
const { groupId , membersV2 } = newAttributes ;
strictAssert ( groupId , 'updateGroupViaState: Group must have groupId' ) ;
strictAssert ( membersV2 , 'updateGroupViaState: Group must have membersV2' ) ;
2024-05-20 18:15:39 +00:00
2024-08-20 23:14:27 +00:00
log . info ( ` getCurrentGroupState/ ${ logId } : Saving group endorsements ` ) ;
const groupEndorsementData = decodeGroupSendEndorsementResponse ( {
groupId ,
groupSendEndorsementResponse ,
groupSecretParamsBase64 : secretParams ,
groupMembersV2 : membersV2 ,
} ) ;
await DataWriter . replaceAllEndorsementsForGroup ( groupEndorsementData ) ;
} catch ( error ) {
log . warn (
` updateGroupViaState/ ${ logId } : Problem saving group endorsements ${ Errors . toLogFormat ( error ) } `
) ;
}
2024-05-20 18:15:39 +00:00
}
2022-05-16 14:53:54 +00:00
return {
newAttributes ,
groupChangeMessages : extractDiffs ( {
old : group ,
current : newAttributes ,
dropInitialJoinMessage ,
} ) ,
2023-10-02 20:19:55 +00:00
newProfileKeys : profileKeysToMap ( newProfileKeys ) ,
2022-05-16 14:53:54 +00:00
} ;
2020-09-09 02:25:05 +00:00
}
2021-01-29 22:16:48 +00:00
async function updateGroupViaSingleChange ( {
group ,
groupChange ,
newRevision ,
} : {
group : ConversationAttributesType ;
2021-06-22 14:46:42 +00:00
groupChange : Proto.IGroupChange ;
2021-01-29 22:16:48 +00:00
newRevision : number ;
} ) : Promise < UpdatesResultType > {
2022-12-06 21:12:57 +00:00
const previouslyKnewAboutThisGroup =
isNumber ( group . revision ) && group . membersV2 ? . length ;
2021-01-29 22:16:48 +00:00
const wasInGroup = ! group . left ;
2022-12-06 21:12:57 +00:00
const singleChangeResult : UpdatesResultType = await integrateGroupChange ( {
2021-01-29 22:16:48 +00:00
group ,
groupChange ,
newRevision ,
} ) ;
2022-12-06 21:12:57 +00:00
const nowInGroup = ! singleChangeResult . newAttributes . left ;
2021-01-29 22:16:48 +00:00
// If we were just added to the group (for example, via a join link), we go fetch the
2023-03-01 23:48:23 +00:00
// entire group state to make sure we're up to date. Note: we fetch the group state
// via the log endpoint to stay at newRevision.
2021-01-29 22:16:48 +00:00
if ( ! wasInGroup && nowInGroup ) {
2023-03-01 23:48:23 +00:00
const logId = idForLogging ( group . groupId ) ;
log . info (
` updateGroupViaSingleChange/ ${ logId } : Just joined group; fetching entire state for revision ${ newRevision } . `
) ;
2022-12-06 21:12:57 +00:00
const {
newAttributes ,
2023-10-02 20:19:55 +00:00
newProfileKeys ,
2022-12-06 21:12:57 +00:00
groupChangeMessages : catchupMessages ,
2023-03-01 23:48:23 +00:00
} = await updateGroupViaLogs ( {
2022-12-06 21:12:57 +00:00
group : singleChangeResult.newAttributes ,
2023-03-01 23:48:23 +00:00
newRevision ,
2021-01-29 22:16:48 +00:00
} ) ;
2022-12-06 21:12:57 +00:00
const groupChangeMessages = [ . . . singleChangeResult . groupChangeMessages ] ;
// If we've just been added to a group we were previously in, we do want to show
// a summary instead of nothing.
if (
groupChangeMessages . length > 0 &&
previouslyKnewAboutThisGroup &&
catchupMessages . length > 0
) {
groupChangeMessages . push ( {
. . . generateBasicMessage ( ) ,
type : 'group-v2-change' ,
groupV2Change : {
details : [
{
type : 'summary' ,
} ,
] ,
} ,
readStatus : ReadStatus.Read ,
// For simplicity, since we don't know who this change is from here, always Seen
seenStatus : SeenStatus.Seen ,
} ) ;
}
2021-01-29 22:16:48 +00:00
// We discard any change events that come out of this full group fetch, but we do
// keep the final group attributes generated, as well as any new members.
return {
2022-12-06 21:12:57 +00:00
groupChangeMessages ,
2023-10-02 20:19:55 +00:00
newProfileKeys : new Map ( [
. . . singleChangeResult . newProfileKeys ,
. . . newProfileKeys ,
] ) ,
2021-01-29 22:16:48 +00:00
newAttributes ,
} ;
}
2022-12-06 21:12:57 +00:00
return singleChangeResult ;
2021-01-29 22:16:48 +00:00
}
2024-08-28 02:28:58 +00:00
function getLastRevisionFromChanges (
changes : ReadonlyArray < Proto.IGroupChanges >
) : number | undefined {
for ( let i = changes . length - 1 ; i >= 0 ; i -= 1 ) {
const change = changes [ i ] ;
if ( ! change ) {
continue ;
}
const { groupChanges } = change ;
if ( ! groupChanges ) {
continue ;
}
for ( let j = groupChanges . length - 1 ; j >= 0 ; j -= 1 ) {
const groupChange = groupChanges [ j ] ;
if ( ! groupChange ) {
continue ;
}
const { groupState } = groupChange ;
if ( ! groupState ) {
continue ;
}
const { version } = groupState ;
if ( isNumber ( version ) ) {
return version ;
}
}
}
return undefined ;
}
2020-09-09 02:25:05 +00:00
async function updateGroupViaLogs ( {
group ,
newRevision ,
2024-08-21 21:31:55 +00:00
isReJoin ,
2020-09-09 02:25:05 +00:00
} : {
group : ConversationAttributesType ;
2022-03-16 00:11:28 +00:00
newRevision : number | undefined ;
2024-08-21 21:31:55 +00:00
isReJoin? : boolean ;
2020-09-09 02:25:05 +00:00
} ) : Promise < UpdatesResultType > {
2021-01-29 22:16:48 +00:00
const logId = idForLogging ( group . groupId ) ;
2022-05-16 14:53:54 +00:00
const { publicParams , secretParams } = group ;
if ( ! publicParams ) {
throw new Error ( 'updateGroupViaLogs: group was missing publicParams!' ) ;
}
if ( ! secretParams ) {
throw new Error ( 'updateGroupViaLogs: group was missing secretParams!' ) ;
2020-09-09 02:25:05 +00:00
}
2024-08-21 21:31:55 +00:00
const currentRevision = isReJoin ? undefined : group . revision ;
let includeFirstState = true ;
2022-05-16 14:53:54 +00:00
log . info (
` updateGroupViaLogs/ ${ logId } : Getting group delta from ` +
2024-08-21 21:31:55 +00:00
` ${ currentRevision ? ? '?' } to ${ newRevision ? ? '?' } for group ` +
2022-05-16 14:53:54 +00:00
` groupv2( ${ group . groupId } )... `
) ;
2020-09-09 02:25:05 +00:00
2022-05-16 14:53:54 +00:00
// The range is inclusive so make sure that we always request the revision
// that we are currently at since we might want the latest full state in
// `integrateGroupChanges`.
let revisionToFetch = isNumber ( currentRevision ) ? currentRevision : undefined ;
2024-05-20 18:15:39 +00:00
const { groupId } = group ;
strictAssert ( groupId != null , 'Group must have groupId' ) ;
let cachedEndorsementsExpiration =
2024-07-22 18:16:33 +00:00
await DataReader . getGroupSendCombinedEndorsementExpiration ( groupId ) ;
2024-05-20 18:15:39 +00:00
2024-09-06 17:52:19 +00:00
if (
cachedEndorsementsExpiration != null &&
! isValidGroupSendEndorsementsExpiration ( cachedEndorsementsExpiration )
) {
log . info (
` updateGroupViaLogs/ ${ logId } : Group had invalid endorsements expiration ( ${ cachedEndorsementsExpiration } ), fetching new endorsements `
) ;
cachedEndorsementsExpiration = null ;
}
2024-05-04 00:42:11 +00:00
let response : GroupLogResponseType ;
2024-05-20 18:15:39 +00:00
let groupSendEndorsementResponse : Uint8Array | null = null ;
2022-05-16 14:53:54 +00:00
const changes : Array < Proto.IGroupChanges > = [ ] ;
do {
// eslint-disable-next-line no-await-in-loop
2024-05-02 21:39:04 +00:00
response = await makeRequestWithCredentials ( {
2022-05-16 14:53:54 +00:00
logId : ` getGroupLog/ ${ logId } ` ,
publicParams ,
secretParams ,
2022-07-08 20:46:25 +00:00
2022-05-16 14:53:54 +00:00
// eslint-disable-next-line no-loop-func
request : ( sender , requestOptions ) = >
sender . getGroupLog (
{
startVersion : revisionToFetch ,
includeFirstState ,
includeLastState : true ,
maxSupportedChangeEpoch : SUPPORTED_CHANGE_EPOCH ,
2024-05-20 18:15:39 +00:00
cachedEndorsementsExpiration ,
2022-05-16 14:53:54 +00:00
} ,
requestOptions
) ,
} ) ;
2024-05-20 18:15:39 +00:00
// When the log is long enough that it needs to be paginated, the server is
// not stateful enough to only give us endorsements when we need them.
// In this case we need to delete all endorsements and send `0` to get
// endorsements from the next page.
if ( response . paginated && cachedEndorsementsExpiration != null ) {
log . info (
'updateGroupViaLogs: Received paginated response, deleting group endorsements'
) ;
// eslint-disable-next-line no-await-in-loop
2024-07-22 18:16:33 +00:00
await DataWriter . deleteAllEndorsementsForGroup ( groupId ) ;
2024-05-20 18:15:39 +00:00
cachedEndorsementsExpiration = null ; // gets sent as 0 in header
}
// Note: We should only get this on the final page
if ( response . groupSendEndorsementResponse != null ) {
groupSendEndorsementResponse = response . groupSendEndorsementResponse ;
}
2022-05-16 14:53:54 +00:00
changes . push ( response . changes ) ;
2024-05-20 18:15:39 +00:00
if ( response . paginated && response . end ) {
2022-05-16 14:53:54 +00:00
revisionToFetch = response . end + 1 ;
2020-09-09 02:25:05 +00:00
}
2022-05-16 14:53:54 +00:00
includeFirstState = false ;
} while (
2024-05-20 18:15:39 +00:00
response . paginated &&
2022-05-16 14:53:54 +00:00
response . end &&
( newRevision === undefined || response . end < newRevision )
) ;
// Would be nice to cache the unused groupChanges here, to reduce server roundtrips
2024-05-20 18:15:39 +00:00
const updates = await integrateGroupChanges ( {
2022-05-16 14:53:54 +00:00
changes ,
group ,
newRevision ,
} ) ;
2024-05-20 18:15:39 +00:00
2024-08-28 02:28:58 +00:00
const currentVersion = response . paginated
? response . currentRevision
: getLastRevisionFromChanges ( changes ) ;
const isAtLatestVersion =
isNumber ( currentVersion ) &&
updates . newAttributes . revision === currentVersion ;
if ( isAtLatestVersion && Bytes . isNotEmpty ( groupSendEndorsementResponse ) ) {
2024-08-20 23:14:27 +00:00
try {
log . info ( ` updateGroupViaLogs/ ${ logId } : Saving group endorsements ` ) ;
// Use the latest state of the group after applying changes
const { membersV2 } = updates . newAttributes ;
strictAssert (
membersV2 != null ,
'updateGroupViaLogs: Group must have membersV2'
) ;
2024-05-20 18:15:39 +00:00
2024-08-20 23:14:27 +00:00
const groupEndorsementData = decodeGroupSendEndorsementResponse ( {
groupId ,
groupSendEndorsementResponse ,
groupMembersV2 : membersV2 ,
groupSecretParamsBase64 : secretParams ,
} ) ;
2024-05-20 18:15:39 +00:00
2024-08-20 23:14:27 +00:00
await DataWriter . replaceAllEndorsementsForGroup ( groupEndorsementData ) ;
} catch ( error ) {
log . warn (
` updateGroupViaLogs/ ${ logId } : Problem saving group endorsements ${ Errors . toLogFormat ( error ) } `
) ;
}
2024-05-20 18:15:39 +00:00
}
return updates ;
2020-09-09 02:25:05 +00:00
}
2021-01-29 22:16:48 +00:00
async function generateLeftGroupChanges (
2020-09-09 02:25:05 +00:00
group : ConversationAttributesType
2021-01-29 22:16:48 +00:00
) : Promise < UpdatesResultType > {
const logId = idForLogging ( group . groupId ) ;
2021-09-17 18:27:53 +00:00
log . info ( ` generateLeftGroupChanges/ ${ logId } : Starting... ` ) ;
2023-08-10 16:43:33 +00:00
const ourAci = window . storage . user . getCheckedAci ( ) ;
const ourPni = window . storage . user . getCheckedPni ( ) ;
2021-01-29 22:16:48 +00:00
const { masterKey , groupInviteLinkPassword } = group ;
let { revision } = group ;
try {
if ( masterKey && groupInviteLinkPassword ) {
2021-09-17 18:27:53 +00:00
log . info (
2021-01-29 22:16:48 +00:00
` generateLeftGroupChanges/ ${ logId } : Have invite link. Attempting to fetch latest revision with it. `
) ;
const preJoinInfo = await getPreJoinGroupInfo (
groupInviteLinkPassword ,
masterKey
) ;
2023-11-07 21:31:59 +00:00
revision = dropNull ( preJoinInfo . version ) ;
2021-01-29 22:16:48 +00:00
}
} catch ( error ) {
2021-09-17 18:27:53 +00:00
log . warn (
2021-01-29 22:16:48 +00:00
'generateLeftGroupChanges: Failed to fetch latest revision via group link. Code:' ,
error . code
) ;
}
2020-09-09 02:25:05 +00:00
const newAttributes : ConversationAttributesType = {
. . . group ,
2022-05-16 14:53:54 +00:00
addedBy : undefined ,
2023-08-16 20:54:39 +00:00
membersV2 : ( group . membersV2 || [ ] ) . filter ( member = > member . aci !== ourAci ) ,
2022-05-16 14:53:54 +00:00
pendingMembersV2 : ( group . pendingMembersV2 || [ ] ) . filter (
2023-08-16 20:54:39 +00:00
member = > member . serviceId !== ourAci && member . serviceId !== ourPni
2022-05-16 14:53:54 +00:00
) ,
pendingAdminApprovalV2 : ( group . pendingAdminApprovalV2 || [ ] ) . filter (
2023-08-16 20:54:39 +00:00
member = > member . aci !== ourAci
2022-05-16 14:53:54 +00:00
) ,
2020-09-09 02:25:05 +00:00
left : true ,
2021-01-29 22:16:48 +00:00
revision ,
2020-09-09 02:25:05 +00:00
} ;
return {
newAttributes ,
2022-05-16 14:53:54 +00:00
groupChangeMessages : extractDiffs ( {
current : newAttributes ,
old : group ,
} ) ,
2023-10-02 20:19:55 +00:00
newProfileKeys : new Map ( ) ,
2020-09-09 02:25:05 +00:00
} ;
}
function getGroupCredentials ( {
authCredentialBase64 ,
groupPublicParamsBase64 ,
groupSecretParamsBase64 ,
serverPublicParamsBase64 ,
} : {
authCredentialBase64 : string ;
groupPublicParamsBase64 : string ;
groupSecretParamsBase64 : string ;
serverPublicParamsBase64 : string ;
} ) : GroupCredentialsType {
const authOperations = getClientZkAuthOperations ( serverPublicParamsBase64 ) ;
const presentation = getAuthCredentialPresentation (
authOperations ,
authCredentialBase64 ,
groupSecretParamsBase64
) ;
return {
2021-06-22 14:46:42 +00:00
groupPublicParamsHex : Bytes.toHex (
Bytes . fromBase64 ( groupPublicParamsBase64 )
2020-09-09 02:25:05 +00:00
) ,
2021-06-22 14:46:42 +00:00
authCredentialPresentationHex : Bytes.toHex ( presentation ) ,
2020-09-09 02:25:05 +00:00
} ;
}
async function integrateGroupChanges ( {
group ,
newRevision ,
changes ,
} : {
group : ConversationAttributesType ;
2022-03-16 00:11:28 +00:00
newRevision : number | undefined ;
2022-12-22 00:07:02 +00:00
changes : ReadonlyArray < Proto.IGroupChanges > ;
2020-09-09 02:25:05 +00:00
} ) : Promise < UpdatesResultType > {
2021-01-29 22:16:48 +00:00
const logId = idForLogging ( group . groupId ) ;
2020-09-09 02:25:05 +00:00
let attributes = group ;
2022-03-16 00:11:28 +00:00
const finalMessages : Array < Array < GroupChangeMessageType > > = [ ] ;
2023-10-02 20:19:55 +00:00
const finalNewProfileKeys = new Map < AciString , string > ( ) ;
2020-09-09 02:25:05 +00:00
const imax = changes . length ;
for ( let i = 0 ; i < imax ; i += 1 ) {
const { groupChanges } = changes [ i ] ;
if ( ! groupChanges ) {
continue ;
}
const jmax = groupChanges . length ;
for ( let j = 0 ; j < jmax ; j += 1 ) {
const changeState = groupChanges [ j ] ;
2020-10-06 17:06:34 +00:00
const { groupChange , groupState } = changeState ;
2020-09-09 02:25:05 +00:00
2021-02-08 21:55:21 +00:00
if ( ! groupChange && ! groupState ) {
2021-09-17 18:27:53 +00:00
log . warn (
2020-10-06 17:06:34 +00:00
'integrateGroupChanges: item had neither groupState nor groupChange. Skipping.'
) ;
2020-09-09 02:25:05 +00:00
continue ;
}
try {
const {
newAttributes ,
groupChangeMessages ,
2023-10-02 20:19:55 +00:00
newProfileKeys ,
2020-09-11 19:37:01 +00:00
// eslint-disable-next-line no-await-in-loop
2020-09-09 02:25:05 +00:00
} = await integrateGroupChange ( {
group : attributes ,
newRevision ,
2021-06-22 14:46:42 +00:00
groupChange : dropNull ( groupChange ) ,
groupState : dropNull ( groupState ) ,
2020-09-09 02:25:05 +00:00
} ) ;
attributes = newAttributes ;
finalMessages . push ( groupChangeMessages ) ;
2023-10-02 20:19:55 +00:00
for ( const [ aci , profileKey ] of newProfileKeys ) {
finalNewProfileKeys . set ( aci , profileKey ) ;
}
2020-09-09 02:25:05 +00:00
} catch ( error ) {
2021-09-17 18:27:53 +00:00
log . error (
2020-11-20 17:30:45 +00:00
` integrateGroupChanges/ ${ logId } : Failed to apply change log, continuing to apply remaining change logs. ` ,
2022-11-22 18:43:43 +00:00
Errors . toLogFormat ( error )
2020-09-09 02:25:05 +00:00
) ;
}
}
}
// If this is our first fetch, we will collapse this down to one set of messages
const isFirstFetch = ! isNumber ( group . revision ) ;
2024-08-05 17:54:26 +00:00
// ...but only if there has been more than one revision since creation
const moreThanOneVersion = Boolean ( attributes . revision ) ;
if ( isFirstFetch && moreThanOneVersion ) {
2020-09-09 02:25:05 +00:00
// The first array in finalMessages is from the first revision we could process. It
// should contain a message about how we joined the group.
const joinMessages = finalMessages [ 0 ] ;
const alreadyHaveJoinMessage = joinMessages && joinMessages . length > 0 ;
// There have been other changes since that first revision, so we generate diffs for
// the whole of the change since then, likely without the initial join message.
const otherMessages = extractDiffs ( {
old : group ,
current : attributes ,
dropInitialJoinMessage : alreadyHaveJoinMessage ,
} ) ;
const groupChangeMessages = alreadyHaveJoinMessage
? [ joinMessages [ 0 ] , . . . otherMessages ]
: otherMessages ;
return {
newAttributes : attributes ,
groupChangeMessages ,
2023-10-02 20:19:55 +00:00
newProfileKeys : finalNewProfileKeys ,
2020-09-09 02:25:05 +00:00
} ;
}
return {
newAttributes : attributes ,
groupChangeMessages : flatten ( finalMessages ) ,
2023-10-02 20:19:55 +00:00
newProfileKeys : finalNewProfileKeys ,
2020-09-09 02:25:05 +00:00
} ;
}
async function integrateGroupChange ( {
group ,
groupChange ,
2020-10-06 17:06:34 +00:00
groupState ,
2020-09-09 02:25:05 +00:00
newRevision ,
} : {
group : ConversationAttributesType ;
2021-06-22 14:46:42 +00:00
groupChange? : Proto.IGroupChange ;
groupState? : Proto.IGroup ;
2022-03-16 00:11:28 +00:00
newRevision : number | undefined ;
2020-09-09 02:25:05 +00:00
} ) : Promise < UpdatesResultType > {
2021-01-29 22:16:48 +00:00
const logId = idForLogging ( group . groupId ) ;
2020-09-09 02:25:05 +00:00
if ( ! group . secretParams ) {
2020-10-06 17:06:34 +00:00
throw new Error (
` integrateGroupChange/ ${ logId } : Group was missing secretParams! `
) ;
2020-09-09 02:25:05 +00:00
}
2021-02-08 21:55:21 +00:00
if ( ! groupChange && ! groupState ) {
throw new Error (
` integrateGroupChange/ ${ logId } : Neither groupChange nor groupState received! `
) ;
2020-09-09 02:25:05 +00:00
}
2020-10-06 17:06:34 +00:00
const isFirstFetch = ! isNumber ( group . revision ) ;
2023-08-10 16:43:33 +00:00
const ourAci = window . storage . user . getCheckedAci ( ) ;
2024-08-21 21:31:55 +00:00
const weAreAwaitingApproval = ( group . pendingAdminApprovalV2 || [ ] ) . some (
item = > item . aci === ourAci
) ;
const weAreInGroup = ( group . membersV2 || [ ] ) . some (
2023-08-16 20:54:39 +00:00
item = > item . aci === ourAci
2021-01-29 22:16:48 +00:00
) ;
2024-08-21 21:31:55 +00:00
const isReJoin = ! isFirstFetch && ! weAreInGroup ;
2021-01-29 22:16:48 +00:00
2021-02-08 21:55:21 +00:00
// These need to be populated from the groupChange. But we might not get one!
let isChangeSupported = false ;
2022-03-31 18:22:40 +00:00
let isSameVersion = false ;
2021-02-08 21:55:21 +00:00
let isMoreThanOneVersionUp = false ;
2021-06-22 14:46:42 +00:00
let groupChangeActions : undefined | Proto . GroupChange . IActions ;
let decryptedChangeActions : undefined | DecryptedGroupChangeActions ;
2023-08-16 20:54:39 +00:00
let sourceServiceId : undefined | ServiceIdString ;
2021-02-08 21:55:21 +00:00
if ( groupChange ) {
2021-06-22 14:46:42 +00:00
groupChangeActions = Proto . GroupChange . Actions . decode (
2021-09-24 00:49:05 +00:00
groupChange . actions || new Uint8Array ( 0 )
2021-02-08 21:55:21 +00:00
) ;
2022-03-31 18:22:40 +00:00
// Version is higher that what we have in the incoming message
2021-02-08 21:55:21 +00:00
if (
groupChangeActions . version &&
2022-03-16 00:11:28 +00:00
newRevision !== undefined &&
2021-02-08 21:55:21 +00:00
groupChangeActions . version > newRevision
) {
2022-03-31 18:22:40 +00:00
log . info (
` integrateGroupChange/ ${ logId } : Skipping ` +
` ${ groupChangeActions . version } , newRevision is ${ newRevision } `
) ;
2021-02-08 21:55:21 +00:00
return {
newAttributes : group ,
groupChangeMessages : [ ] ,
2023-10-02 20:19:55 +00:00
newProfileKeys : new Map ( ) ,
2021-02-08 21:55:21 +00:00
} ;
}
decryptedChangeActions = decryptGroupChange (
groupChangeActions ,
group . secretParams ,
logId
) ;
2021-06-22 14:46:42 +00:00
strictAssert (
decryptedChangeActions !== undefined ,
'Should have decrypted group actions'
) ;
2023-08-16 20:54:39 +00:00
( { sourceServiceId } = decryptedChangeActions ) ;
strictAssert ( sourceServiceId , 'Should have source service id' ) ;
2021-02-08 21:55:21 +00:00
isChangeSupported =
! isNumber ( groupChange . changeEpoch ) ||
groupChange . changeEpoch <= SUPPORTED_CHANGE_EPOCH ;
2022-03-31 18:22:40 +00:00
// Version is lower or the same as what we currently have
if ( group . revision !== undefined && groupChangeActions . version ) {
if ( groupChangeActions . version < group . revision ) {
log . info (
` integrateGroupChange/ ${ logId } : Skipping stale version ` +
` ${ groupChangeActions . version } , current ` +
` revision is ${ group . revision } `
) ;
return {
newAttributes : group ,
groupChangeMessages : [ ] ,
2023-10-02 20:19:55 +00:00
newProfileKeys : new Map ( ) ,
2022-03-31 18:22:40 +00:00
} ;
}
if ( groupChangeActions . version === group . revision ) {
isSameVersion = true ;
2022-05-16 14:53:54 +00:00
} else if (
2022-07-08 20:46:25 +00:00
groupChangeActions . version !== group . revision + 1 ||
2022-05-16 14:53:54 +00:00
( ! isNumber ( group . revision ) && groupChangeActions . version > 0 )
) {
2022-03-31 18:22:40 +00:00
isMoreThanOneVersionUp = true ;
}
}
2021-02-08 21:55:21 +00:00
}
2022-03-31 18:22:40 +00:00
let attributes = group ;
const aggregatedChangeMessages = [ ] ;
2023-10-02 20:19:55 +00:00
const finalNewProfileKeys = new Map < AciString , string > ( ) ;
2022-03-31 18:22:40 +00:00
const canApplyChange =
groupChange &&
isChangeSupported &&
! isSameVersion &&
! isFirstFetch &&
( ! isMoreThanOneVersionUp || weAreAwaitingApproval ) ;
// Apply the change first
if ( canApplyChange ) {
2023-08-16 20:54:39 +00:00
if ( ! sourceServiceId || ! groupChangeActions || ! decryptedChangeActions ) {
2020-10-21 00:39:13 +00:00
throw new Error (
2022-03-31 18:22:40 +00:00
` integrateGroupChange/ ${ logId } : Missing necessary information that should have come from group actions `
2020-10-21 00:39:13 +00:00
) ;
}
2021-09-17 18:27:53 +00:00
log . info (
2022-03-31 18:22:40 +00:00
` integrateGroupChange/ ${ logId } : Applying group change actions, ` +
` from version ${ group . revision } to ${ groupChangeActions . version } `
) ;
2023-01-11 01:20:13 +00:00
const { newAttributes , newProfileKeys , promotedAciToPniMap } =
await applyGroupChange ( {
group ,
actions : decryptedChangeActions ,
2023-08-16 20:54:39 +00:00
sourceServiceId ,
2023-01-11 01:20:13 +00:00
} ) ;
2022-03-31 18:22:40 +00:00
const groupChangeMessages = extractDiffs ( {
old : attributes ,
current : newAttributes ,
2023-08-16 20:54:39 +00:00
sourceServiceId ,
2023-01-11 01:20:13 +00:00
promotedAciToPniMap ,
2022-03-31 18:22:40 +00:00
} ) ;
attributes = newAttributes ;
aggregatedChangeMessages . push ( groupChangeMessages ) ;
2023-10-02 20:19:55 +00:00
for ( const [ aci , profileKey ] of profileKeysToMap ( newProfileKeys ) ) {
finalNewProfileKeys . set ( aci , profileKey ) ;
}
2022-03-31 18:22:40 +00:00
}
// Apply the group state afterwards to verify that we didn't miss anything
if ( groupState ) {
log . info (
` integrateGroupChange/ ${ logId } : Applying full group state, ` +
` from version ${ group . revision } to ${ groupState . version } ` ,
2020-10-21 00:39:13 +00:00
{
2021-02-08 21:55:21 +00:00
isChangePresent : Boolean ( groupChange ) ,
2020-10-21 00:39:13 +00:00
isChangeSupported ,
2021-02-08 21:55:21 +00:00
isFirstFetch ,
2024-08-21 21:31:55 +00:00
isReJoin ,
2022-03-31 18:22:40 +00:00
isSameVersion ,
2021-02-08 21:55:21 +00:00
isMoreThanOneVersionUp ,
weAreAwaitingApproval ,
2020-10-21 00:39:13 +00:00
}
2020-10-06 17:06:34 +00:00
) ;
const decryptedGroupState = decryptGroupState (
groupState ,
group . secretParams ,
logId
) ;
2023-10-02 20:19:55 +00:00
const {
newAttributes ,
newProfileKeys : newProfileKeysList ,
otherChanges ,
} = await applyGroupState ( {
group : attributes ,
groupState : decryptedGroupState ,
2024-08-21 21:31:55 +00:00
sourceServiceId : isFirstFetch || isReJoin ? sourceServiceId : undefined ,
2023-10-02 20:19:55 +00:00
} ) ;
2020-10-06 17:06:34 +00:00
2022-03-31 18:22:40 +00:00
const groupChangeMessages = extractDiffs ( {
old : attributes ,
current : newAttributes ,
2024-08-21 21:31:55 +00:00
sourceServiceId : isFirstFetch || isReJoin ? sourceServiceId : undefined ,
isReJoin ,
2022-03-31 18:22:40 +00:00
} ) ;
2020-10-06 17:06:34 +00:00
2023-10-02 20:19:55 +00:00
const newProfileKeys = profileKeysToMap ( newProfileKeysList ) ;
2021-02-08 21:55:21 +00:00
2022-04-11 21:31:38 +00:00
if (
canApplyChange &&
2023-03-01 23:48:23 +00:00
( groupChangeMessages . length !== 0 ||
2023-10-02 20:19:55 +00:00
newProfileKeys . size !== 0 ||
2023-03-01 23:48:23 +00:00
otherChanges )
2022-04-11 21:31:38 +00:00
) {
2022-09-15 19:17:15 +00:00
assertDev (
2022-03-31 18:22:40 +00:00
groupChangeMessages . length === 0 ,
'Fallback group state processing should not kick in'
) ;
2020-10-06 17:06:34 +00:00
2022-03-31 18:22:40 +00:00
log . warn (
` integrateGroupChange/ ${ logId } : local state was different from ` +
'the remote final state. ' +
2023-03-01 23:48:23 +00:00
` Got ${ groupChangeMessages . length } change messages, ` +
2023-10-02 20:19:55 +00:00
` ${ newProfileKeys . size } updated members, and ` +
2023-03-01 23:48:23 +00:00
` otherChanges= ${ otherChanges } `
2022-03-31 18:22:40 +00:00
) ;
}
attributes = newAttributes ;
aggregatedChangeMessages . push ( groupChangeMessages ) ;
2023-10-02 20:19:55 +00:00
for ( const [ aci , profileKey ] of newProfileKeys ) {
finalNewProfileKeys . set ( aci , profileKey ) ;
}
2022-03-31 18:22:40 +00:00
} else {
strictAssert (
canApplyChange ,
` integrateGroupChange/ ${ logId } : No group state, but we can't apply changes! `
) ;
}
2020-09-09 02:25:05 +00:00
return {
2022-03-31 18:22:40 +00:00
newAttributes : attributes ,
groupChangeMessages : aggregatedChangeMessages.flat ( ) ,
2023-10-02 20:19:55 +00:00
newProfileKeys : finalNewProfileKeys ,
2020-09-09 02:25:05 +00:00
} ;
}
function extractDiffs ( {
current ,
dropInitialJoinMessage ,
2024-08-21 21:31:55 +00:00
isReJoin ,
2020-09-09 02:25:05 +00:00
old ,
2023-01-11 01:20:13 +00:00
promotedAciToPniMap ,
2024-08-21 21:31:55 +00:00
sourceServiceId ,
2020-09-09 02:25:05 +00:00
} : {
current : ConversationAttributesType ;
dropInitialJoinMessage? : boolean ;
2024-08-21 21:31:55 +00:00
isReJoin? : boolean ;
2020-09-09 02:25:05 +00:00
old : ConversationAttributesType ;
2023-08-10 16:43:33 +00:00
promotedAciToPniMap? : ReadonlyMap < AciString , PniString > ;
2024-08-21 21:31:55 +00:00
sourceServiceId? : ServiceIdString ;
2022-03-16 00:11:28 +00:00
} ) : Array < GroupChangeMessageType > {
2021-01-29 22:16:48 +00:00
const logId = idForLogging ( old . groupId ) ;
2020-09-09 02:25:05 +00:00
const details : Array < GroupV2ChangeDetailType > = [ ] ;
2023-08-10 16:43:33 +00:00
const ourAci = window . storage . user . getCheckedAci ( ) ;
const ourPni = window . storage . user . getPni ( ) ;
2021-06-22 14:46:42 +00:00
const ACCESS_ENUM = Proto . AccessControl . AccessRequired ;
2020-10-06 17:06:34 +00:00
2020-09-09 02:25:05 +00:00
let areWeInGroup = false ;
2023-08-10 16:43:33 +00:00
let serviceIdKindInvitedToGroup : ServiceIdKind | undefined ;
2022-05-16 14:53:54 +00:00
let areWePendingApproval = false ;
2020-10-06 17:06:34 +00:00
let whoInvitedUsUserId = null ;
2020-09-09 02:25:05 +00:00
2023-08-16 20:54:39 +00:00
function isUs ( serviceId : ServiceIdString ) : boolean {
return serviceId === ourAci || serviceId === ourPni ;
2022-12-06 21:12:57 +00:00
}
function keepOnlyOurAdds (
list : Array < GroupV2ChangeDetailType >
) : Array < GroupV2ChangeDetailType > {
return list . filter (
item = >
2023-08-16 20:54:39 +00:00
( item . type === 'member-add-from-invite' && isUs ( item . aci ) ) ||
( item . type === 'member-add-from-link' && isUs ( item . aci ) ) ||
( item . type === 'member-add-from-admin-approval' && isUs ( item . aci ) ) ||
( item . type === 'member-add' && isUs ( item . aci ) )
2022-12-06 21:12:57 +00:00
) ;
}
2020-12-18 19:27:43 +00:00
// access control
2020-09-09 02:25:05 +00:00
if (
current . accessControl &&
2020-12-18 19:27:43 +00:00
old . accessControl &&
old . accessControl . attributes !== undefined &&
old . accessControl . attributes !== current . accessControl . attributes
2020-09-09 02:25:05 +00:00
) {
details . push ( {
type : 'access-attributes' ,
newPrivilege : current.accessControl.attributes ,
} ) ;
}
if (
current . accessControl &&
2020-12-18 19:27:43 +00:00
old . accessControl &&
old . accessControl . members !== undefined &&
old . accessControl . members !== current . accessControl . members
2020-09-09 02:25:05 +00:00
) {
details . push ( {
type : 'access-members' ,
newPrivilege : current.accessControl.members ,
} ) ;
}
2020-12-18 19:27:43 +00:00
2022-03-15 01:32:07 +00:00
const linkPreviouslyEnabled = isAccessControlEnabled (
old . accessControl ? . addFromInviteLink
) ;
const linkCurrentlyEnabled = isAccessControlEnabled (
current . accessControl ? . addFromInviteLink
) ;
2020-12-18 19:27:43 +00:00
if ( ! linkPreviouslyEnabled && linkCurrentlyEnabled ) {
details . push ( {
type : 'group-link-add' ,
privilege : current.accessControl?.addFromInviteLink || ACCESS_ENUM . ANY ,
} ) ;
} else if ( linkPreviouslyEnabled && ! linkCurrentlyEnabled ) {
details . push ( {
type : 'group-link-remove' ,
} ) ;
} else if (
linkPreviouslyEnabled &&
linkCurrentlyEnabled &&
old . accessControl ? . addFromInviteLink !==
current . accessControl ? . addFromInviteLink
) {
details . push ( {
type : 'access-invite-link' ,
newPrivilege : current.accessControl?.addFromInviteLink || ACCESS_ENUM . ANY ,
} ) ;
}
// avatar
2024-06-25 21:50:15 +00:00
if ( old . avatar ? . url !== current . avatar ? . url ) {
2020-09-09 02:25:05 +00:00
details . push ( {
type : 'avatar' ,
removed : ! current . avatar ,
} ) ;
}
2020-12-18 19:27:43 +00:00
// name
2020-09-09 02:25:05 +00:00
if ( old . name !== current . name ) {
details . push ( {
type : 'title' ,
newTitle : current.name ,
} ) ;
}
2020-12-18 19:27:43 +00:00
// groupInviteLinkPassword
// Note: we only capture link resets here. Enable/disable are controlled by the
// accessControl.addFromInviteLink
if (
old . groupInviteLinkPassword &&
current . groupInviteLinkPassword &&
old . groupInviteLinkPassword !== current . groupInviteLinkPassword
) {
details . push ( {
type : 'group-link-reset' ,
} ) ;
}
2021-06-02 00:24:28 +00:00
// description
if ( old . description !== current . description ) {
details . push ( {
type : 'description' ,
removed : ! current . description ,
2021-06-04 16:27:04 +00:00
description : current.description ,
2021-06-02 00:24:28 +00:00
} ) ;
}
2020-09-09 02:25:05 +00:00
// No disappearing message timer check here - see below
2020-12-18 19:27:43 +00:00
// membersV2
2023-08-10 16:43:33 +00:00
const oldMemberLookup = new Map < AciString , GroupV2MemberType > (
2023-08-16 20:54:39 +00:00
( old . membersV2 || [ ] ) . map ( member = > [ member . aci , member ] )
2020-12-18 19:27:43 +00:00
) ;
2023-08-10 16:43:33 +00:00
const didWeStartInGroup = Boolean ( ourAci && oldMemberLookup . has ( ourAci ) ) ;
2022-12-06 21:12:57 +00:00
2021-10-26 22:59:08 +00:00
const oldPendingMemberLookup = new Map <
2023-08-10 16:43:33 +00:00
ServiceIdString ,
2021-10-26 22:59:08 +00:00
GroupV2PendingMemberType
2023-08-16 20:54:39 +00:00
> ( ( old . pendingMembersV2 || [ ] ) . map ( member = > [ member . serviceId , member ] ) ) ;
2021-10-26 22:59:08 +00:00
const oldPendingAdminApprovalLookup = new Map <
2023-08-10 16:43:33 +00:00
AciString ,
2021-10-26 22:59:08 +00:00
GroupV2PendingAdminApprovalType
2023-08-16 20:54:39 +00:00
> ( ( old . pendingAdminApprovalV2 || [ ] ) . map ( member = > [ member . aci , member ] ) ) ;
2023-08-10 16:43:33 +00:00
const currentPendingMemberSet = new Set < ServiceIdString > (
2023-08-16 20:54:39 +00:00
( current . pendingMembersV2 || [ ] ) . map ( member = > member . serviceId )
2022-07-08 20:46:25 +00:00
) ;
2020-09-09 02:25:05 +00:00
2023-01-11 01:20:13 +00:00
const aciToPniMap = new Map ( promotedAciToPniMap ? . entries ( ) ) ;
2023-08-10 16:43:33 +00:00
if ( ourAci && ourPni ) {
aciToPniMap . set ( ourAci , ourPni ) ;
2023-01-11 01:20:13 +00:00
}
2023-08-10 16:43:33 +00:00
const pniToAciMap = new Map < PniString , AciString > ( ) ;
2023-01-11 01:20:13 +00:00
for ( const [ aci , pni ] of aciToPniMap ) {
pniToAciMap . set ( pni , aci ) ;
}
2020-09-09 02:25:05 +00:00
( current . membersV2 || [ ] ) . forEach ( currentMember = > {
2023-08-16 20:54:39 +00:00
const { aci } = currentMember ;
const uuidIsUs = isUs ( aci ) ;
2020-09-09 02:25:05 +00:00
2022-12-06 21:12:57 +00:00
if ( uuidIsUs ) {
2020-09-09 02:25:05 +00:00
areWeInGroup = true ;
}
2023-08-16 20:54:39 +00:00
const oldMember = oldMemberLookup . get ( aci ) ;
2020-09-09 02:25:05 +00:00
if ( ! oldMember ) {
2023-08-16 20:54:39 +00:00
let pendingMember = oldPendingMemberLookup . get ( aci ) ;
const pni = aciToPniMap . get ( aci ) ;
2023-01-11 01:20:13 +00:00
if ( ! pendingMember && pni ) {
pendingMember = oldPendingMemberLookup . get ( pni ) ;
// Someone's ACI just joined (wasn't a member before) and their PNI
// disappeared from the invite list. Treat this as a promotion from PNI
// to ACI and pretend that the PNI wasn't pending so that we won't
// generate a pending-add-one notification below.
if ( pendingMember && ! currentPendingMemberSet . has ( pni ) ) {
oldPendingMemberLookup . delete ( pni ) ;
}
2022-07-08 20:46:25 +00:00
}
2023-01-11 01:20:13 +00:00
2020-09-28 17:22:57 +00:00
if ( pendingMember ) {
2020-09-09 02:25:05 +00:00
details . push ( {
type : 'member-add-from-invite' ,
2023-08-16 20:54:39 +00:00
aci ,
2024-05-03 14:28:36 +00:00
pni ,
2020-09-09 02:25:05 +00:00
inviter : pendingMember.addedByUserId ,
} ) ;
2020-12-18 19:27:43 +00:00
} else if ( currentMember . joinedFromLink ) {
details . push ( {
type : 'member-add-from-link' ,
2023-08-16 20:54:39 +00:00
aci ,
2020-12-18 19:27:43 +00:00
} ) ;
} else if ( currentMember . approvedByAdmin ) {
details . push ( {
type : 'member-add-from-admin-approval' ,
2023-08-16 20:54:39 +00:00
aci ,
2020-12-18 19:27:43 +00:00
} ) ;
2020-09-09 02:25:05 +00:00
} else {
details . push ( {
type : 'member-add' ,
2023-08-16 20:54:39 +00:00
aci ,
2020-09-09 02:25:05 +00:00
} ) ;
}
} else if ( oldMember . role !== currentMember . role ) {
details . push ( {
type : 'member-privilege' ,
2023-08-16 20:54:39 +00:00
aci ,
2020-09-09 02:25:05 +00:00
newPrivilege : currentMember.role ,
} ) ;
}
2020-12-18 19:27:43 +00:00
// We don't want to generate an admin-approval-remove event for this newly-added
// member. But we don't know for sure if this is an admin approval; for that we
// consulted the approvedByAdmin flag saved on the member.
2023-08-16 20:54:39 +00:00
oldPendingAdminApprovalLookup . delete ( aci ) ;
2020-12-18 19:27:43 +00:00
// If we capture a pending remove here, it's an 'accept invitation', and we don't
// want to generate a pending-remove event for it
2023-08-16 20:54:39 +00:00
oldPendingMemberLookup . delete ( aci ) ;
2020-12-18 19:27:43 +00:00
2020-09-09 02:25:05 +00:00
// This deletion makes it easier to capture removals
2023-08-16 20:54:39 +00:00
oldMemberLookup . delete ( aci ) ;
2020-09-09 02:25:05 +00:00
} ) ;
2021-10-26 22:59:08 +00:00
const removedMemberIds = Array . from ( oldMemberLookup . keys ( ) ) ;
2023-08-16 20:54:39 +00:00
removedMemberIds . forEach ( aci = > {
2020-09-09 02:25:05 +00:00
details . push ( {
type : 'member-remove' ,
2023-08-16 20:54:39 +00:00
aci ,
2020-09-09 02:25:05 +00:00
} ) ;
} ) ;
2020-12-18 19:27:43 +00:00
// pendingMembersV2
2023-08-16 20:54:39 +00:00
let lastPendingServiceId : ServiceIdString | undefined ;
2020-12-18 19:27:43 +00:00
let pendingCount = 0 ;
2020-09-09 02:25:05 +00:00
( current . pendingMembersV2 || [ ] ) . forEach ( currentPendingMember = > {
2023-08-16 20:54:39 +00:00
const { serviceId } = currentPendingMember ;
const oldPendingMember = oldPendingMemberLookup . get ( serviceId ) ;
2020-09-09 02:25:05 +00:00
2023-08-16 20:54:39 +00:00
if ( isUs ( serviceId ) ) {
if ( serviceId === ourAci ) {
2023-08-10 16:43:33 +00:00
serviceIdKindInvitedToGroup = ServiceIdKind . ACI ;
} else if ( serviceIdKindInvitedToGroup === undefined ) {
serviceIdKindInvitedToGroup = ServiceIdKind . PNI ;
2022-07-08 20:46:25 +00:00
}
2020-10-06 17:06:34 +00:00
whoInvitedUsUserId = currentPendingMember . addedByUserId ;
}
2020-09-09 02:25:05 +00:00
if ( ! oldPendingMember ) {
2023-08-16 20:54:39 +00:00
lastPendingServiceId = serviceId ;
2020-12-18 19:27:43 +00:00
pendingCount += 1 ;
2020-09-09 02:25:05 +00:00
}
// This deletion makes it easier to capture removals
2023-08-16 20:54:39 +00:00
oldPendingMemberLookup . delete ( serviceId ) ;
2020-09-09 02:25:05 +00:00
} ) ;
2020-12-18 19:27:43 +00:00
if ( pendingCount > 1 ) {
2020-09-09 02:25:05 +00:00
details . push ( {
type : 'pending-add-many' ,
2020-12-18 19:27:43 +00:00
count : pendingCount ,
2020-09-09 02:25:05 +00:00
} ) ;
2020-12-18 19:27:43 +00:00
} else if ( pendingCount === 1 ) {
2023-08-16 20:54:39 +00:00
if ( lastPendingServiceId ) {
2020-09-09 02:25:05 +00:00
details . push ( {
type : 'pending-add-one' ,
2023-08-16 20:54:39 +00:00
serviceId : lastPendingServiceId ,
2020-09-09 02:25:05 +00:00
} ) ;
} else {
2021-09-17 18:27:53 +00:00
log . warn (
2020-12-18 19:27:43 +00:00
` extractDiffs/ ${ logId } : pendingCount was 1, no last conversationId available `
2020-09-09 02:25:05 +00:00
) ;
}
}
// Note: The only members left over here should be people who were moved from the
// pending list but also not added to the group at the same time.
2021-10-26 22:59:08 +00:00
const removedPendingMemberIds = Array . from ( oldPendingMemberLookup . keys ( ) ) ;
2020-09-09 02:25:05 +00:00
if ( removedPendingMemberIds . length > 1 ) {
2021-10-26 22:59:08 +00:00
const firstUuid = removedPendingMemberIds [ 0 ] ;
const firstRemovedMember = oldPendingMemberLookup . get ( firstUuid ) ;
strictAssert (
firstRemovedMember !== undefined ,
'First removed member not found'
) ;
2020-09-09 02:25:05 +00:00
const inviter = firstRemovedMember . addedByUserId ;
const allSameInviter = removedPendingMemberIds . every (
2021-10-26 22:59:08 +00:00
id = > oldPendingMemberLookup . get ( id ) ? . addedByUserId === inviter
2020-09-09 02:25:05 +00:00
) ;
details . push ( {
type : 'pending-remove-many' ,
count : removedPendingMemberIds.length ,
inviter : allSameInviter ? inviter : undefined ,
} ) ;
} else if ( removedPendingMemberIds . length === 1 ) {
2023-08-16 20:54:39 +00:00
const serviceId = removedPendingMemberIds [ 0 ] ;
const removedMember = oldPendingMemberLookup . get ( serviceId ) ;
2021-10-26 22:59:08 +00:00
strictAssert ( removedMember !== undefined , 'Removed member not found' ) ;
2020-09-09 02:25:05 +00:00
details . push ( {
type : 'pending-remove-one' ,
2023-08-16 20:54:39 +00:00
serviceId ,
2020-09-09 02:25:05 +00:00
inviter : removedMember.addedByUserId ,
} ) ;
}
2020-12-18 19:27:43 +00:00
// pendingAdminApprovalV2
( current . pendingAdminApprovalV2 || [ ] ) . forEach (
currentPendingAdminAprovalMember = > {
2023-08-16 20:54:39 +00:00
const { aci } = currentPendingAdminAprovalMember ;
const oldPendingMember = oldPendingAdminApprovalLookup . get ( aci ) ;
2020-12-18 19:27:43 +00:00
2023-08-16 20:54:39 +00:00
if ( aci === ourAci ) {
2022-05-16 14:53:54 +00:00
areWePendingApproval = true ;
}
2020-12-18 19:27:43 +00:00
if ( ! oldPendingMember ) {
details . push ( {
type : 'admin-approval-add-one' ,
2023-08-16 20:54:39 +00:00
aci ,
2020-12-18 19:27:43 +00:00
} ) ;
}
// This deletion makes it easier to capture removals
2023-08-16 20:54:39 +00:00
oldPendingAdminApprovalLookup . delete ( aci ) ;
2020-12-18 19:27:43 +00:00
}
) ;
// Note: The only members left over here should be people who were moved from the
// pendingAdminApproval list but also not added to the group at the same time.
2021-10-26 22:59:08 +00:00
const removedPendingAdminApprovalIds = Array . from (
oldPendingAdminApprovalLookup . keys ( )
2020-12-18 19:27:43 +00:00
) ;
2023-08-16 20:54:39 +00:00
removedPendingAdminApprovalIds . forEach ( aci = > {
2020-12-18 19:27:43 +00:00
details . push ( {
type : 'admin-approval-remove-one' ,
2023-08-16 20:54:39 +00:00
aci ,
2020-12-18 19:27:43 +00:00
} ) ;
} ) ;
2021-07-20 20:18:35 +00:00
// announcementsOnly
2021-07-21 17:58:39 +00:00
if ( Boolean ( old . announcementsOnly ) !== Boolean ( current . announcementsOnly ) ) {
2021-07-20 20:18:35 +00:00
details . push ( {
type : 'announcements-only' ,
announcementsOnly : Boolean ( current . announcementsOnly ) ,
} ) ;
}
2022-03-15 01:32:07 +00:00
// Note: currently no diff generated for bannedMembersV2 changes
2020-12-18 19:27:43 +00:00
// final processing
2022-03-16 00:11:28 +00:00
let message : GroupChangeMessageType | undefined ;
let timerNotification : GroupChangeMessageType | undefined ;
2020-09-09 02:25:05 +00:00
const firstUpdate = ! isNumber ( old . revision ) ;
2023-08-16 20:54:39 +00:00
const isFromUs = ourAci === sourceServiceId ;
2022-12-06 21:12:57 +00:00
const justJoinedGroup = ! firstUpdate && ! didWeStartInGroup && areWeInGroup ;
2020-09-09 02:25:05 +00:00
2023-08-10 16:43:33 +00:00
const from =
2023-08-16 20:54:39 +00:00
( sourceServiceId &&
isPniString ( sourceServiceId ) &&
pniToAciMap . get ( sourceServiceId ) ) ||
sourceServiceId ;
2023-01-11 01:20:13 +00:00
2022-12-06 21:12:57 +00:00
// Here we hardcode initial messages if this is our first time processing data for this
2020-10-06 17:06:34 +00:00
// group. Ideally we can collapse it down to just one of: 'you were added',
// 'you were invited', or 'you created.'
2023-08-10 16:43:33 +00:00
if ( firstUpdate && serviceIdKindInvitedToGroup !== undefined ) {
2020-11-10 15:15:37 +00:00
// Note, we will add 'you were invited' to group even if dropInitialJoinMessage = true
2020-10-06 17:06:34 +00:00
message = {
. . . generateBasicMessage ( ) ,
type : 'group-v2-change' ,
groupV2Change : {
2023-01-11 01:20:13 +00:00
from : whoInvitedUsUserId || from ,
2020-10-06 17:06:34 +00:00
details : [
{
2020-11-10 15:15:37 +00:00
type : 'pending-add-one' ,
2023-08-16 20:54:39 +00:00
serviceId : window.storage.user.getCheckedServiceId (
2023-08-10 16:43:33 +00:00
serviceIdKindInvitedToGroup
) ,
2020-10-06 17:06:34 +00:00
} ,
] ,
} ,
2022-05-12 02:45:20 +00:00
readStatus : ReadStatus.Read ,
seenStatus : isFromUs ? SeenStatus.Seen : SeenStatus.Unseen ,
2020-10-06 17:06:34 +00:00
} ;
2022-05-16 14:53:54 +00:00
} else if ( firstUpdate && areWePendingApproval ) {
message = {
. . . generateBasicMessage ( ) ,
type : 'group-v2-change' ,
groupV2Change : {
2023-08-10 16:43:33 +00:00
from : ourAci ,
2022-05-16 14:53:54 +00:00
details : [
{
type : 'admin-approval-add-one' ,
2023-08-16 20:54:39 +00:00
aci : ourAci ,
2022-05-16 14:53:54 +00:00
} ,
] ,
} ,
} ;
2020-11-10 15:15:37 +00:00
} else if ( firstUpdate && dropInitialJoinMessage ) {
// None of the rest of the messages should be added if dropInitialJoinMessage = true
message = undefined ;
2023-08-16 20:54:39 +00:00
} else if (
firstUpdate &&
current . revision === 0 &&
sourceServiceId === ourAci
) {
2020-10-06 17:06:34 +00:00
message = {
. . . generateBasicMessage ( ) ,
type : 'group-v2-change' ,
groupV2Change : {
2023-01-11 01:20:13 +00:00
from ,
2020-10-06 17:06:34 +00:00
details : [
{
2020-11-10 15:15:37 +00:00
type : 'create' ,
2020-10-06 17:06:34 +00:00
} ,
] ,
} ,
2022-05-12 02:45:20 +00:00
readStatus : ReadStatus.Read ,
seenStatus : isFromUs ? SeenStatus.Seen : SeenStatus.Unseen ,
2020-10-06 17:06:34 +00:00
} ;
2021-10-26 22:59:08 +00:00
} else if ( firstUpdate && areWeInGroup ) {
2022-12-06 21:12:57 +00:00
const filteredDetails = keepOnlyOurAdds ( details ) ;
strictAssert (
filteredDetails . length === 1 ,
'extractDiffs/firstUpdate: Should be only one self-add!'
) ;
2020-09-09 02:25:05 +00:00
message = {
. . . generateBasicMessage ( ) ,
type : 'group-v2-change' ,
groupV2Change : {
2023-01-11 01:20:13 +00:00
from ,
2022-12-06 21:12:57 +00:00
details : filteredDetails ,
2020-09-09 02:25:05 +00:00
} ,
2022-05-12 02:45:20 +00:00
readStatus : ReadStatus.Read ,
seenStatus : isFromUs ? SeenStatus.Seen : SeenStatus.Unseen ,
2020-09-09 02:25:05 +00:00
} ;
2022-05-16 14:53:54 +00:00
} else if ( firstUpdate && current . revision === 0 ) {
2020-10-06 17:06:34 +00:00
message = {
. . . generateBasicMessage ( ) ,
type : 'group-v2-change' ,
groupV2Change : {
2023-01-11 01:20:13 +00:00
from ,
2020-10-06 17:06:34 +00:00
details : [
{
type : 'create' ,
} ,
] ,
} ,
2022-05-12 02:45:20 +00:00
readStatus : ReadStatus.Read ,
seenStatus : isFromUs ? SeenStatus.Seen : SeenStatus.Unseen ,
2020-10-06 17:06:34 +00:00
} ;
2022-12-06 21:12:57 +00:00
} else if ( justJoinedGroup ) {
const filteredDetails = keepOnlyOurAdds ( details ) ;
strictAssert (
filteredDetails . length === 1 ,
'extractDiffs/justJoinedGroup: Should be only one self-add!'
) ;
// If we've dropped other changes, we collapse them into a single summary
if ( details . length > 1 ) {
filteredDetails . push ( {
type : 'summary' ,
} ) ;
}
message = {
. . . generateBasicMessage ( ) ,
type : 'group-v2-change' ,
2023-08-16 20:54:39 +00:00
sourceServiceId ,
2022-12-06 21:12:57 +00:00
groupV2Change : {
2023-01-11 01:20:13 +00:00
from ,
2022-12-06 21:12:57 +00:00
details : filteredDetails ,
} ,
readStatus : ReadStatus.Read ,
seenStatus : isFromUs ? SeenStatus.Seen : SeenStatus.Unseen ,
} ;
2020-09-09 02:25:05 +00:00
} else if ( details . length > 0 ) {
message = {
. . . generateBasicMessage ( ) ,
type : 'group-v2-change' ,
2023-08-16 20:54:39 +00:00
sourceServiceId ,
2020-09-09 02:25:05 +00:00
groupV2Change : {
2023-01-11 01:20:13 +00:00
from ,
2020-09-09 02:25:05 +00:00
details ,
} ,
2022-05-12 02:45:20 +00:00
readStatus : ReadStatus.Read ,
seenStatus : isFromUs ? SeenStatus.Seen : SeenStatus.Unseen ,
2020-09-09 02:25:05 +00:00
} ;
}
// This is checked differently, because it needs to be its own entry in the timeline,
// with its own icon, etc.
if (
// Turn on or turned off
Boolean ( old . expireTimer ) !== Boolean ( current . expireTimer ) ||
// Still on, but changed value
( Boolean ( old . expireTimer ) &&
Boolean ( current . expireTimer ) &&
old . expireTimer !== current . expireTimer )
) {
2022-11-16 20:18:02 +00:00
const expireTimer = current . expireTimer || DurationInSeconds . ZERO ;
2022-08-19 18:35:40 +00:00
log . info (
2023-01-01 11:41:40 +00:00
` extractDiffs/ ${ logId } : generating change notification for new ${ expireTimer } timer `
2022-08-19 18:35:40 +00:00
) ;
2020-09-09 02:25:05 +00:00
timerNotification = {
. . . generateBasicMessage ( ) ,
type : 'timer-notification' ,
2024-08-21 21:31:55 +00:00
sourceServiceId : isReJoin ? undefined : sourceServiceId ,
2021-06-22 14:46:42 +00:00
flags : Proto.DataMessage.Flags.EXPIRATION_TIMER_UPDATE ,
2020-09-09 02:25:05 +00:00
expirationTimerUpdate : {
2022-08-19 18:35:40 +00:00
expireTimer ,
2024-08-21 21:31:55 +00:00
sourceServiceId : isReJoin ? undefined : sourceServiceId ,
2020-09-09 02:25:05 +00:00
} ,
} ;
}
const result = compact ( [ message , timerNotification ] ) ;
2021-09-17 18:27:53 +00:00
log . info (
2020-09-09 02:25:05 +00:00
` extractDiffs/ ${ logId } complete, generated ${ result . length } change messages `
) ;
return result ;
}
2023-10-02 20:19:55 +00:00
function profileKeysToMap ( items : ReadonlyArray < GroupChangeMemberType > ) {
const map = new Map < AciString , string > ( ) ;
for ( const { aci , profileKey } of items ) {
map . set ( aci , Bytes . toBase64 ( profileKey ) ) ;
}
return map ;
2020-09-09 02:25:05 +00:00
}
type GroupChangeMemberType = {
2021-06-22 14:46:42 +00:00
profileKey : Uint8Array ;
2023-08-16 20:54:39 +00:00
aci : AciString ;
2020-09-09 02:25:05 +00:00
} ;
2021-04-07 22:45:31 +00:00
type GroupApplyResultType = {
2020-09-09 02:25:05 +00:00
newAttributes : ConversationAttributesType ;
newProfileKeys : Array < GroupChangeMemberType > ;
2023-03-01 23:48:23 +00:00
otherChanges : boolean ;
2020-09-09 02:25:05 +00:00
} ;
2023-01-11 01:20:13 +00:00
type GroupApplyChangeResultType = GroupApplyResultType & {
2023-08-10 16:43:33 +00:00
promotedAciToPniMap : Map < AciString , PniString > ;
2023-01-11 01:20:13 +00:00
} ;
2020-09-09 02:25:05 +00:00
async function applyGroupChange ( {
actions ,
2020-10-06 17:06:34 +00:00
group ,
2023-08-16 20:54:39 +00:00
sourceServiceId ,
2020-09-09 02:25:05 +00:00
} : {
2021-06-22 14:46:42 +00:00
actions : DecryptedGroupChangeActions ;
2020-10-06 17:06:34 +00:00
group : ConversationAttributesType ;
2023-08-16 20:54:39 +00:00
sourceServiceId : ServiceIdString ;
2023-01-11 01:20:13 +00:00
} ) : Promise < GroupApplyChangeResultType > {
2021-01-29 22:16:48 +00:00
const logId = idForLogging ( group . groupId ) ;
2023-08-10 16:43:33 +00:00
const ourAci = window . storage . user . getCheckedAci ( ) ;
2020-10-06 17:06:34 +00:00
2021-06-22 14:46:42 +00:00
const ACCESS_ENUM = Proto . AccessControl . AccessRequired ;
const MEMBER_ROLE_ENUM = Proto . Member . Role ;
2020-10-06 17:06:34 +00:00
2020-09-09 02:25:05 +00:00
const version = actions . version || 0 ;
2024-06-25 21:50:15 +00:00
let result = { . . . group } ;
2020-09-09 02:25:05 +00:00
const newProfileKeys : Array < GroupChangeMemberType > = [ ] ;
2023-08-10 16:43:33 +00:00
const promotedAciToPniMap = new Map < AciString , PniString > ( ) ;
2020-09-09 02:25:05 +00:00
2023-08-10 16:43:33 +00:00
const members : Record < AciString , GroupV2MemberType > = fromPairs (
2023-08-16 20:54:39 +00:00
( result . membersV2 || [ ] ) . map ( member = > [ member . aci , member ] )
2020-09-09 02:25:05 +00:00
) ;
2023-08-10 16:43:33 +00:00
const pendingMembers : Record < ServiceIdString , GroupV2PendingMemberType > =
2021-11-11 22:43:05 +00:00
fromPairs (
2023-08-16 20:54:39 +00:00
( result . pendingMembersV2 || [ ] ) . map ( member = > [ member . serviceId , member ] )
2021-11-11 22:43:05 +00:00
) ;
2021-10-26 22:59:08 +00:00
const pendingAdminApprovalMembers : Record <
2023-08-10 16:43:33 +00:00
AciString ,
2021-10-26 22:59:08 +00:00
GroupV2PendingAdminApprovalType
> = fromPairs (
2023-08-16 20:54:39 +00:00
( result . pendingAdminApprovalV2 || [ ] ) . map ( member = > [ member . aci , member ] )
2020-12-18 19:27:43 +00:00
) ;
2023-08-10 16:43:33 +00:00
const bannedMembers = new Map < ServiceIdString , GroupV2BannedMemberType > (
2023-08-16 20:54:39 +00:00
( result . bannedMembersV2 || [ ] ) . map ( member = > [ member . serviceId , member ] )
2022-03-15 01:32:07 +00:00
) ;
2020-09-09 02:25:05 +00:00
2023-03-01 23:48:23 +00:00
if ( result . temporaryMemberCount ) {
log . warn (
` applyGroupChange( ${ logId } ): temporaryMemberCount is set, and should not be! `
) ;
}
2020-09-09 02:25:05 +00:00
// version?: number;
result . revision = version ;
2021-06-22 14:46:42 +00:00
// addMembers?: Array<GroupChange.Actions.AddMemberAction>;
2020-09-11 19:37:01 +00:00
( actions . addMembers || [ ] ) . forEach ( addMember = > {
2020-09-09 02:25:05 +00:00
const { added } = addMember ;
2021-10-26 22:59:08 +00:00
if ( ! added || ! added . userId ) {
2020-09-09 02:25:05 +00:00
throw new Error ( 'applyGroupChange: addMember.added is missing' ) ;
}
2023-08-10 16:43:33 +00:00
const addedUuid = added . userId ;
2020-09-09 02:25:05 +00:00
2021-10-26 22:59:08 +00:00
if ( members [ addedUuid ] ) {
2021-09-17 18:27:53 +00:00
log . warn (
2020-09-09 02:25:05 +00:00
` applyGroupChange/ ${ logId } : Attempt to add member failed; already in members. `
) ;
return ;
}
2021-10-26 22:59:08 +00:00
members [ addedUuid ] = {
2023-08-16 20:54:39 +00:00
aci : addedUuid ,
2020-09-09 02:25:05 +00:00
role : added.role || MEMBER_ROLE_ENUM . DEFAULT ,
joinedAtVersion : version ,
2020-12-18 19:27:43 +00:00
joinedFromLink : addMember.joinFromInviteLink || false ,
2020-09-09 02:25:05 +00:00
} ;
2021-10-26 22:59:08 +00:00
if ( pendingMembers [ addedUuid ] ) {
2021-09-17 18:27:53 +00:00
log . warn (
2020-09-09 02:25:05 +00:00
` applyGroupChange/ ${ logId } : Removing newly-added member from pendingMembers. `
) ;
2021-10-26 22:59:08 +00:00
delete pendingMembers [ addedUuid ] ;
2020-09-09 02:25:05 +00:00
}
2020-10-06 17:06:34 +00:00
// Capture who added us
2024-09-16 18:05:58 +00:00
if ( addedUuid === ourAci && pendingAdminApprovalMembers [ ourAci ] ) {
result . addedBy = ourAci ;
} else if ( addedUuid === ourAci && sourceServiceId ) {
2023-08-16 20:54:39 +00:00
result . addedBy = sourceServiceId ;
2020-10-06 17:06:34 +00:00
}
2024-09-16 18:05:58 +00:00
if ( pendingAdminApprovalMembers [ addedUuid ] ) {
log . warn (
` applyGroupChange/ ${ logId } : Removing newly-added member from pendingAdminApprovalMembers. `
) ;
delete pendingAdminApprovalMembers [ addedUuid ] ;
}
2020-09-09 02:25:05 +00:00
if ( added . profileKey ) {
newProfileKeys . push ( {
profileKey : added.profileKey ,
2023-08-16 20:54:39 +00:00
aci : added.userId ,
2020-09-09 02:25:05 +00:00
} ) ;
}
} ) ;
2021-06-22 14:46:42 +00:00
// deleteMembers?: Array<GroupChange.Actions.DeleteMemberAction>;
2020-09-09 02:25:05 +00:00
( actions . deleteMembers || [ ] ) . forEach ( deleteMember = > {
const { deletedUserId } = deleteMember ;
if ( ! deletedUserId ) {
throw new Error (
'applyGroupChange: deleteMember.deletedUserId is missing'
) ;
}
2023-08-10 16:43:33 +00:00
if ( members [ deletedUserId ] ) {
delete members [ deletedUserId ] ;
2020-09-09 02:25:05 +00:00
} else {
2021-09-17 18:27:53 +00:00
log . warn (
2020-09-09 02:25:05 +00:00
` applyGroupChange/ ${ logId } : Attempt to remove member failed; was not in members. `
) ;
}
} ) ;
2021-06-22 14:46:42 +00:00
// modifyMemberRoles?: Array<GroupChange.Actions.ModifyMemberRoleAction>;
2020-09-09 02:25:05 +00:00
( actions . modifyMemberRoles || [ ] ) . forEach ( modifyMemberRole = > {
const { role , userId } = modifyMemberRole ;
if ( ! role || ! userId ) {
throw new Error ( 'applyGroupChange: modifyMemberRole had a missing value' ) ;
}
2023-08-10 16:43:33 +00:00
if ( members [ userId ] ) {
members [ userId ] = {
. . . members [ userId ] ,
2020-09-09 02:25:05 +00:00
role ,
} ;
} else {
throw new Error (
'applyGroupChange: modifyMemberRole tried to modify nonexistent member'
) ;
}
} ) ;
2020-09-11 19:37:01 +00:00
// modifyMemberProfileKeys?:
2021-06-22 14:46:42 +00:00
// Array<GroupChange.Actions.ModifyMemberProfileKeyAction>;
2020-09-09 02:25:05 +00:00
( actions . modifyMemberProfileKeys || [ ] ) . forEach ( modifyMemberProfileKey = > {
2023-08-16 20:54:39 +00:00
const { profileKey , aci } = modifyMemberProfileKey ;
if ( ! profileKey || ! aci ) {
2020-09-09 02:25:05 +00:00
throw new Error (
'applyGroupChange: modifyMemberProfileKey had a missing value'
) ;
}
2023-09-06 23:28:32 +00:00
if ( aci === sourceServiceId || ! hasProfileKey ( aci ) ) {
newProfileKeys . push ( {
profileKey ,
aci ,
} ) ;
} else {
log . warn (
` applyGroupChange/ ${ logId } : Attempt to modify member profile key ` +
'failed; sourceServiceId is not the same as change aci'
) ;
}
2020-09-09 02:25:05 +00:00
} ) ;
2020-12-18 19:27:43 +00:00
// addPendingMembers?: Array<
2021-06-22 14:46:42 +00:00
// GroupChange.Actions.AddMemberPendingProfileKeyAction
2020-12-18 19:27:43 +00:00
// >;
2020-09-09 02:25:05 +00:00
( actions . addPendingMembers || [ ] ) . forEach ( addPendingMember = > {
const { added } = addPendingMember ;
2021-10-26 22:59:08 +00:00
if ( ! added || ! added . member || ! added . member . userId ) {
2020-09-09 02:25:05 +00:00
throw new Error (
2020-12-18 19:27:43 +00:00
'applyGroupChange: addPendingMembers had a missing value'
2020-09-09 02:25:05 +00:00
) ;
}
2023-08-10 16:43:33 +00:00
const addedUserId = added . member . userId ;
2020-09-09 02:25:05 +00:00
2023-08-10 16:43:33 +00:00
if ( isAciString ( addedUserId ) && members [ addedUserId ] ) {
2021-09-17 18:27:53 +00:00
log . warn (
2020-09-09 02:25:05 +00:00
` applyGroupChange/ ${ logId } : Attempt to add pendingMember failed; was already in members. `
) ;
return ;
}
2023-08-10 16:43:33 +00:00
if ( pendingMembers [ addedUserId ] ) {
2021-09-17 18:27:53 +00:00
log . warn (
2020-09-09 02:25:05 +00:00
` applyGroupChange/ ${ logId } : Attempt to add pendingMember failed; was already in pendingMembers. `
) ;
return ;
}
2023-08-10 16:43:33 +00:00
pendingMembers [ addedUserId ] = {
2023-08-16 20:54:39 +00:00
serviceId : addedUserId ,
2023-08-10 16:43:33 +00:00
addedByUserId : added.addedByUserId ,
2020-09-09 02:25:05 +00:00
timestamp : added.timestamp ,
2020-12-01 23:45:39 +00:00
role : added.member.role || MEMBER_ROLE_ENUM . DEFAULT ,
2020-09-09 02:25:05 +00:00
} ;
} ) ;
2020-12-18 19:27:43 +00:00
// deletePendingMembers?: Array<
2021-06-22 14:46:42 +00:00
// GroupChange.Actions.DeleteMemberPendingProfileKeyAction
2020-12-18 19:27:43 +00:00
// >;
2020-09-09 02:25:05 +00:00
( actions . deletePendingMembers || [ ] ) . forEach ( deletePendingMember = > {
const { deletedUserId } = deletePendingMember ;
if ( ! deletedUserId ) {
throw new Error (
'applyGroupChange: deletePendingMember.deletedUserId is null!'
) ;
}
2023-08-10 16:43:33 +00:00
if ( pendingMembers [ deletedUserId ] ) {
delete pendingMembers [ deletedUserId ] ;
2020-09-09 02:25:05 +00:00
} else {
2021-09-17 18:27:53 +00:00
log . warn (
2020-09-09 02:25:05 +00:00
` applyGroupChange/ ${ logId } : Attempt to remove pendingMember failed; was not in pendingMembers. `
) ;
}
} ) ;
2020-12-18 19:27:43 +00:00
// promotePendingMembers?: Array<
2021-06-22 14:46:42 +00:00
// GroupChange.Actions.PromoteMemberPendingProfileKeyAction
2020-12-18 19:27:43 +00:00
// >;
2020-09-09 02:25:05 +00:00
( actions . promotePendingMembers || [ ] ) . forEach ( promotePendingMember = > {
2023-08-16 20:54:39 +00:00
const { profileKey , aci } = promotePendingMember ;
if ( ! profileKey || ! aci ) {
2020-09-09 02:25:05 +00:00
throw new Error (
'applyGroupChange: promotePendingMember had a missing value'
) ;
}
2023-08-16 20:54:39 +00:00
const previousRecord = pendingMembers [ aci ] ;
2020-09-09 02:25:05 +00:00
2023-08-16 20:54:39 +00:00
if ( pendingMembers [ aci ] ) {
delete pendingMembers [ aci ] ;
2020-09-09 02:25:05 +00:00
} else {
2021-09-17 18:27:53 +00:00
log . warn (
2020-09-09 02:25:05 +00:00
` applyGroupChange/ ${ logId } : Attempt to promote pendingMember failed; was not in pendingMembers. `
) ;
2024-09-16 18:05:58 +00:00
return ;
2020-09-09 02:25:05 +00:00
}
2023-08-16 20:54:39 +00:00
if ( members [ aci ] ) {
2021-09-17 18:27:53 +00:00
log . warn (
2020-09-09 02:25:05 +00:00
` applyGroupChange/ ${ logId } : Attempt to promote pendingMember failed; was already in members. `
) ;
return ;
}
2023-08-16 20:54:39 +00:00
members [ aci ] = {
aci ,
2020-09-09 02:25:05 +00:00
joinedAtVersion : version ,
2020-12-01 23:45:39 +00:00
role : previousRecord.role || MEMBER_ROLE_ENUM . DEFAULT ,
2020-09-09 02:25:05 +00:00
} ;
newProfileKeys . push ( {
profileKey ,
2023-08-16 20:54:39 +00:00
aci ,
2020-09-09 02:25:05 +00:00
} ) ;
} ) ;
2022-07-08 20:46:25 +00:00
// promoteMembersPendingPniAciProfileKey?: Array<
// GroupChange.Actions.PromoteMemberPendingPniAciProfileKeyAction
// >;
( actions . promoteMembersPendingPniAciProfileKey || [ ] ) . forEach (
promotePendingMember = > {
const { profileKey , aci , pni } = promotePendingMember ;
if ( ! profileKey || ! aci || ! pni ) {
throw new Error (
'applyGroupChange: promotePendingMember had a missing value'
) ;
}
const previousRecord = pendingMembers [ pni ] ;
2023-01-11 01:20:13 +00:00
promotedAciToPniMap . set ( aci , pni ) ;
2022-07-08 20:46:25 +00:00
if ( pendingMembers [ pni ] ) {
delete pendingMembers [ pni ] ;
} else {
log . warn (
` applyGroupChange/ ${ logId } : Attempt to promote pendingMember failed; was not in pendingMembers. `
) ;
}
if ( members [ aci ] ) {
log . warn (
` applyGroupChange/ ${ logId } : Attempt to promote pendingMember failed; was already in members. `
) ;
return ;
}
members [ aci ] = {
2023-08-16 20:54:39 +00:00
aci ,
2022-07-08 20:46:25 +00:00
joinedAtVersion : version ,
role : previousRecord.role || MEMBER_ROLE_ENUM . DEFAULT ,
} ;
newProfileKeys . push ( {
profileKey ,
2023-08-16 20:54:39 +00:00
aci ,
2022-07-08 20:46:25 +00:00
} ) ;
}
) ;
2021-06-22 14:46:42 +00:00
// modifyTitle?: GroupChange.Actions.ModifyTitleAction;
2020-09-09 02:25:05 +00:00
if ( actions . modifyTitle ) {
2020-09-11 19:37:01 +00:00
const { title } = actions . modifyTitle ;
2020-09-09 02:25:05 +00:00
if ( title && title . content === 'title' ) {
2022-06-17 22:33:46 +00:00
result . name = dropNull ( title . title ) ;
2020-09-09 02:25:05 +00:00
} else {
2021-09-17 18:27:53 +00:00
log . warn (
2020-09-09 02:25:05 +00:00
` applyGroupChange/ ${ logId } : Clearing group title due to missing data. `
) ;
result . name = undefined ;
}
}
2021-06-22 14:46:42 +00:00
// modifyAvatar?: GroupChange.Actions.ModifyAvatarAction;
2020-09-09 02:25:05 +00:00
if ( actions . modifyAvatar ) {
2020-09-11 19:37:01 +00:00
const { avatar } = actions . modifyAvatar ;
2024-06-25 21:50:15 +00:00
result = {
. . . result ,
. . . ( await applyNewAvatar ( dropNull ( avatar ) , result , logId ) ) ,
} ;
2020-09-09 02:25:05 +00:00
}
2020-09-11 19:37:01 +00:00
// modifyDisappearingMessagesTimer?:
2021-06-22 14:46:42 +00:00
// GroupChange.Actions.ModifyDisappearingMessagesTimerAction;
2020-09-09 02:25:05 +00:00
if ( actions . modifyDisappearingMessagesTimer ) {
2021-06-22 14:46:42 +00:00
const disappearingMessagesTimer : Proto.GroupAttributeBlob | undefined =
2020-09-09 02:25:05 +00:00
actions . modifyDisappearingMessagesTimer . timer ;
if (
disappearingMessagesTimer &&
disappearingMessagesTimer . content === 'disappearingMessagesDuration'
) {
2022-11-16 20:18:02 +00:00
const duration = disappearingMessagesTimer . disappearingMessagesDuration ;
result . expireTimer =
duration == null ? undefined : DurationInSeconds . fromSeconds ( duration ) ;
2020-09-09 02:25:05 +00:00
} else {
2021-09-17 18:27:53 +00:00
log . warn (
2020-09-09 02:25:05 +00:00
` applyGroupChange/ ${ logId } : Clearing group expireTimer due to missing data. `
) ;
result . expireTimer = undefined ;
}
}
result . accessControl = result . accessControl || {
members : ACCESS_ENUM.MEMBER ,
attributes : ACCESS_ENUM.MEMBER ,
2020-12-18 19:27:43 +00:00
addFromInviteLink : ACCESS_ENUM.UNSATISFIABLE ,
2020-09-09 02:25:05 +00:00
} ;
2020-09-11 19:37:01 +00:00
// modifyAttributesAccess?:
2021-06-22 14:46:42 +00:00
// GroupChange.Actions.ModifyAttributesAccessControlAction;
2020-09-09 02:25:05 +00:00
if ( actions . modifyAttributesAccess ) {
result . accessControl = {
. . . result . accessControl ,
attributes :
actions . modifyAttributesAccess . attributesAccess || ACCESS_ENUM . MEMBER ,
} ;
}
2021-06-22 14:46:42 +00:00
// modifyMemberAccess?: GroupChange.Actions.ModifyMembersAccessControlAction;
2020-09-09 02:25:05 +00:00
if ( actions . modifyMemberAccess ) {
result . accessControl = {
. . . result . accessControl ,
2020-09-10 20:06:26 +00:00
members : actions.modifyMemberAccess.membersAccess || ACCESS_ENUM . MEMBER ,
2020-09-09 02:25:05 +00:00
} ;
}
2020-12-18 19:27:43 +00:00
// modifyAddFromInviteLinkAccess?:
2021-06-22 14:46:42 +00:00
// GroupChange.Actions.ModifyAddFromInviteLinkAccessControlAction;
2020-12-18 19:27:43 +00:00
if ( actions . modifyAddFromInviteLinkAccess ) {
result . accessControl = {
. . . result . accessControl ,
addFromInviteLink :
actions . modifyAddFromInviteLinkAccess . addFromInviteLinkAccess ||
ACCESS_ENUM . UNSATISFIABLE ,
} ;
}
// addMemberPendingAdminApprovals?: Array<
2021-06-22 14:46:42 +00:00
// GroupChange.Actions.AddMemberPendingAdminApprovalAction
2020-12-18 19:27:43 +00:00
// >;
( actions . addMemberPendingAdminApprovals || [ ] ) . forEach (
pendingAdminApproval = > {
const { added } = pendingAdminApproval ;
if ( ! added ) {
throw new Error (
'applyGroupChange: modifyMemberProfileKey had a missing value'
) ;
}
2023-08-10 16:43:33 +00:00
if ( members [ added . userId ] ) {
2021-09-17 18:27:53 +00:00
log . warn (
2020-12-18 19:27:43 +00:00
` applyGroupChange/ ${ logId } : Attempt to add pending admin approval failed; was already in members. `
) ;
return ;
}
2023-08-10 16:43:33 +00:00
if ( pendingMembers [ added . userId ] ) {
2021-09-17 18:27:53 +00:00
log . warn (
2020-12-18 19:27:43 +00:00
` applyGroupChange/ ${ logId } : Attempt to add pending admin approval failed; was already in pendingMembers. `
) ;
return ;
}
2023-08-10 16:43:33 +00:00
if ( pendingAdminApprovalMembers [ added . userId ] ) {
2021-09-17 18:27:53 +00:00
log . warn (
2020-12-18 19:27:43 +00:00
` applyGroupChange/ ${ logId } : Attempt to add pending admin approval failed; was already in pendingAdminApprovalMembers. `
) ;
return ;
}
2023-08-10 16:43:33 +00:00
pendingAdminApprovalMembers [ added . userId ] = {
2023-08-16 20:54:39 +00:00
aci : added.userId ,
2020-12-18 19:27:43 +00:00
timestamp : added.timestamp ,
} ;
if ( added . profileKey ) {
newProfileKeys . push ( {
profileKey : added.profileKey ,
2023-08-16 20:54:39 +00:00
aci : added.userId ,
2020-12-18 19:27:43 +00:00
} ) ;
}
}
) ;
// deleteMemberPendingAdminApprovals?: Array<
2021-06-22 14:46:42 +00:00
// GroupChange.Actions.DeleteMemberPendingAdminApprovalAction
2020-12-18 19:27:43 +00:00
// >;
( actions . deleteMemberPendingAdminApprovals || [ ] ) . forEach (
deleteAdminApproval = > {
const { deletedUserId } = deleteAdminApproval ;
if ( ! deletedUserId ) {
throw new Error (
'applyGroupChange: deleteAdminApproval.deletedUserId is null!'
) ;
}
2023-08-10 16:43:33 +00:00
if ( pendingAdminApprovalMembers [ deletedUserId ] ) {
delete pendingAdminApprovalMembers [ deletedUserId ] ;
2020-12-18 19:27:43 +00:00
} else {
2021-09-17 18:27:53 +00:00
log . warn (
2020-12-18 19:27:43 +00:00
` applyGroupChange/ ${ logId } : Attempt to remove pendingAdminApproval failed; was not in pendingAdminApprovalMembers. `
) ;
}
}
) ;
// promoteMemberPendingAdminApprovals?: Array<
2021-06-22 14:46:42 +00:00
// GroupChange.Actions.PromoteMemberPendingAdminApprovalAction
2020-12-18 19:27:43 +00:00
// >;
( actions . promoteMemberPendingAdminApprovals || [ ] ) . forEach (
promoteAdminApproval = > {
const { userId , role } = promoteAdminApproval ;
if ( ! userId ) {
throw new Error (
'applyGroupChange: promoteAdminApproval had a missing value'
) ;
}
2023-08-10 16:43:33 +00:00
if ( pendingAdminApprovalMembers [ userId ] ) {
delete pendingAdminApprovalMembers [ userId ] ;
2020-12-18 19:27:43 +00:00
} else {
2021-09-17 18:27:53 +00:00
log . warn (
2020-12-18 19:27:43 +00:00
` applyGroupChange/ ${ logId } : Attempt to promote pendingAdminApproval failed; was not in pendingAdminApprovalMembers. `
) ;
2024-09-16 18:05:58 +00:00
return ;
2020-12-18 19:27:43 +00:00
}
2023-08-10 16:43:33 +00:00
if ( pendingMembers [ userId ] ) {
delete pendingAdminApprovalMembers [ userId ] ;
2021-09-17 18:27:53 +00:00
log . warn (
2020-12-18 19:27:43 +00:00
` applyGroupChange/ ${ logId } : Deleted pendingAdminApproval from pendingMembers. `
) ;
}
2023-08-10 16:43:33 +00:00
if ( members [ userId ] ) {
2021-09-17 18:27:53 +00:00
log . warn (
2020-12-18 19:27:43 +00:00
` applyGroupChange/ ${ logId } : Attempt to promote pendingMember failed; was already in members. `
) ;
return ;
}
2024-09-16 18:05:58 +00:00
// If we had requested to join, and are approved, we added ourselves
if ( userId === ourAci ) {
result . addedBy = ourAci ;
}
2023-08-10 16:43:33 +00:00
members [ userId ] = {
2023-08-16 20:54:39 +00:00
aci : userId ,
2020-12-18 19:27:43 +00:00
joinedAtVersion : version ,
role : role || MEMBER_ROLE_ENUM . DEFAULT ,
approvedByAdmin : true ,
} ;
}
) ;
2021-06-22 14:46:42 +00:00
// modifyInviteLinkPassword?: GroupChange.Actions.ModifyInviteLinkPasswordAction;
2020-12-18 19:27:43 +00:00
if ( actions . modifyInviteLinkPassword ) {
const { inviteLinkPassword } = actions . modifyInviteLinkPassword ;
if ( inviteLinkPassword ) {
result . groupInviteLinkPassword = inviteLinkPassword ;
} else {
result . groupInviteLinkPassword = undefined ;
}
}
2021-06-22 14:46:42 +00:00
// modifyDescription?: GroupChange.Actions.ModifyDescriptionAction;
2021-06-02 00:24:28 +00:00
if ( actions . modifyDescription ) {
const { descriptionBytes } = actions . modifyDescription ;
if ( descriptionBytes && descriptionBytes . content === 'descriptionText' ) {
2022-06-17 22:33:46 +00:00
result . description = dropNull ( descriptionBytes . descriptionText ) ;
2021-06-02 00:24:28 +00:00
} else {
2021-09-17 18:27:53 +00:00
log . warn (
2021-06-02 00:24:28 +00:00
` applyGroupChange/ ${ logId } : Clearing group description due to missing data. `
) ;
result . description = undefined ;
}
}
2021-07-21 17:58:39 +00:00
if ( actions . modifyAnnouncementsOnly ) {
const { announcementsOnly } = actions . modifyAnnouncementsOnly ;
result . announcementsOnly = announcementsOnly ;
}
2022-03-15 01:32:07 +00:00
if ( actions . addMembersBanned && actions . addMembersBanned . length > 0 ) {
2022-03-23 22:34:51 +00:00
actions . addMembersBanned . forEach ( member = > {
2023-08-16 20:54:39 +00:00
if ( bannedMembers . has ( member . serviceId ) ) {
2022-03-15 01:32:07 +00:00
log . warn (
` applyGroupChange/ ${ logId } : Attempt to add banned member failed; was already in banned list. `
) ;
return ;
}
2023-08-16 20:54:39 +00:00
bannedMembers . set ( member . serviceId , member ) ;
2022-03-15 01:32:07 +00:00
} ) ;
}
if ( actions . deleteMembersBanned && actions . deleteMembersBanned . length > 0 ) {
2023-08-16 20:54:39 +00:00
actions . deleteMembersBanned . forEach ( serviceId = > {
if ( ! bannedMembers . has ( serviceId ) ) {
2022-03-15 01:32:07 +00:00
log . warn (
` applyGroupChange/ ${ logId } : Attempt to remove banned member failed; was not in banned list. `
) ;
return ;
}
2023-08-16 20:54:39 +00:00
bannedMembers . delete ( serviceId ) ;
2022-03-15 01:32:07 +00:00
} ) ;
}
2023-08-10 16:43:33 +00:00
if ( ourAci ) {
result . left = ! members [ ourAci ] ;
2020-09-09 02:25:05 +00:00
}
2022-05-16 14:53:54 +00:00
if ( result . left ) {
result . addedBy = undefined ;
}
2020-09-09 02:25:05 +00:00
// Go from lookups back to arrays
result . membersV2 = values ( members ) ;
result . pendingMembersV2 = values ( pendingMembers ) ;
2020-12-18 19:27:43 +00:00
result . pendingAdminApprovalV2 = values ( pendingAdminApprovalMembers ) ;
2022-03-23 22:34:51 +00:00
result . bannedMembersV2 = Array . from ( bannedMembers . values ( ) ) ;
2020-09-09 02:25:05 +00:00
return {
newAttributes : result ,
newProfileKeys ,
2023-03-01 23:48:23 +00:00
otherChanges : false ,
2023-01-11 01:20:13 +00:00
promotedAciToPniMap ,
2020-09-09 02:25:05 +00:00
} ;
}
2021-02-10 22:39:26 +00:00
export async function decryptGroupAvatar (
avatarKey : string ,
secretParamsBase64 : string
2021-09-24 00:49:05 +00:00
) : Promise < Uint8Array > {
2021-02-10 22:39:26 +00:00
const sender = window . textsecure . messaging ;
if ( ! sender ) {
throw new Error (
'decryptGroupAvatar: textsecure.messaging is not available!'
) ;
}
2021-09-24 00:49:05 +00:00
const ciphertext = await sender . getGroupAvatar ( avatarKey ) ;
2021-02-10 22:39:26 +00:00
const clientZkGroupCipher = getClientZkGroupCipher ( secretParamsBase64 ) ;
const plaintext = decryptGroupBlob ( clientZkGroupCipher , ciphertext ) ;
2021-06-22 14:46:42 +00:00
const blob = Proto . GroupAttributeBlob . decode ( plaintext ) ;
2021-02-10 22:39:26 +00:00
if ( blob . content !== 'avatar' ) {
throw new Error (
` decryptGroupAvatar: Returned blob had incorrect content: ${ blob . content } `
) ;
}
2022-06-17 22:33:46 +00:00
const avatar = dropNull ( blob . avatar ) ;
if ( ! avatar ) {
throw new Error ( 'decryptGroupAvatar: Returned blob had no avatar set!' ) ;
}
return avatar ;
2021-02-10 22:39:26 +00:00
}
2023-01-01 11:41:40 +00:00
// Overwriting result.avatar as part of functionality
2021-01-29 22:16:48 +00:00
export async function applyNewAvatar (
2024-06-24 18:38:59 +00:00
newAvatarUrl : string | undefined ,
attributes : Readonly <
Pick < ConversationAttributesType , ' avatar ' | ' secretParams ' >
> ,
2020-09-09 02:25:05 +00:00
logId : string
2024-06-24 18:38:59 +00:00
) : Promise < Pick < ConversationAttributesType , ' avatar ' > > {
const result : Pick < ConversationAttributesType , ' avatar ' > = { } ;
2020-09-09 02:25:05 +00:00
try {
// Avatar has been dropped
2024-06-24 18:38:59 +00:00
if ( ! newAvatarUrl && attributes . avatar ) {
if ( attributes . avatar . path ) {
await window . Signal . Migrations . deleteAttachmentData (
attributes . avatar . path
) ;
}
2020-09-09 02:25:05 +00:00
result . avatar = undefined ;
}
// Group has avatar; has it changed?
2024-06-24 18:38:59 +00:00
if (
newAvatarUrl &&
2024-09-12 23:48:27 +00:00
( ! attributes . avatar ? . path || attributes . avatar . url !== newAvatarUrl )
2024-06-24 18:38:59 +00:00
) {
if ( ! attributes . secretParams ) {
2020-09-09 02:25:05 +00:00
throw new Error ( 'applyNewAvatar: group was missing secretParams!' ) ;
}
2024-06-24 18:38:59 +00:00
const data = await decryptGroupAvatar (
newAvatarUrl ,
attributes . secretParams
) ;
2021-09-24 00:49:05 +00:00
const hash = computeHash ( data ) ;
2020-09-09 02:25:05 +00:00
2024-06-24 18:38:59 +00:00
if ( attributes . avatar ? . hash === hash ) {
2022-05-31 16:27:03 +00:00
log . info (
` applyNewAvatar/ ${ logId } : Hash is the same, but url was different. Saving new url. `
2020-09-09 02:25:05 +00:00
) ;
result . avatar = {
2024-06-24 18:38:59 +00:00
. . . attributes . avatar ,
url : newAvatarUrl ,
2020-09-09 02:25:05 +00:00
} ;
2024-06-24 18:38:59 +00:00
return result ;
2020-09-09 02:25:05 +00:00
}
2022-05-31 16:27:03 +00:00
2024-06-24 18:38:59 +00:00
if ( attributes . avatar ? . path ) {
await window . Signal . Migrations . deleteAttachmentData (
attributes . avatar . path
) ;
2022-05-31 16:27:03 +00:00
}
2024-07-11 19:44:09 +00:00
const local = await window . Signal . Migrations . writeNewAttachmentData ( data ) ;
2022-05-31 16:27:03 +00:00
result . avatar = {
2024-06-24 18:38:59 +00:00
url : newAvatarUrl ,
2024-07-11 19:44:09 +00:00
. . . local ,
2022-05-31 16:27:03 +00:00
hash ,
} ;
2020-09-09 02:25:05 +00:00
}
} catch ( error ) {
2021-09-17 18:27:53 +00:00
log . warn (
2020-09-09 02:25:05 +00:00
` applyNewAvatar/ ${ logId } Failed to handle avatar, clearing it ` ,
2022-11-22 18:43:43 +00:00
Errors . toLogFormat ( error )
2020-09-09 02:25:05 +00:00
) ;
if ( result . avatar && result . avatar . path ) {
await window . Signal . Migrations . deleteAttachmentData ( result . avatar . path ) ;
}
result . avatar = undefined ;
}
2024-06-24 18:38:59 +00:00
return result ;
2020-09-09 02:25:05 +00:00
}
2023-08-10 16:43:33 +00:00
function profileKeyHasChanged (
userId : ServiceIdString ,
newProfileKey : Uint8Array
) {
2023-03-01 23:48:23 +00:00
const conversation = window . ConversationController . get ( userId ) ;
if ( ! conversation ) {
return true ;
}
const existingBase64 = conversation . get ( 'profileKey' ) ;
if ( ! existingBase64 ) {
return true ;
}
const newBase64 = Bytes . toBase64 ( newProfileKey ) ;
return newBase64 !== existingBase64 ;
}
2023-09-06 23:28:32 +00:00
function hasProfileKey ( userId : ServiceIdString ) {
const conversation = window . ConversationController . get ( userId ) ;
if ( ! conversation ) {
2023-10-14 01:14:46 +00:00
return false ;
2023-09-06 23:28:32 +00:00
}
const existingBase64 = conversation . get ( 'profileKey' ) ;
return existingBase64 !== undefined ;
}
2020-10-06 17:06:34 +00:00
async function applyGroupState ( {
group ,
groupState ,
2023-08-16 20:54:39 +00:00
sourceServiceId ,
2020-10-06 17:06:34 +00:00
} : {
group : ConversationAttributesType ;
2021-06-22 14:46:42 +00:00
groupState : DecryptedGroupState ;
2023-08-16 20:54:39 +00:00
sourceServiceId? : ServiceIdString ;
2021-04-07 22:45:31 +00:00
} ) : Promise < GroupApplyResultType > {
2021-01-29 22:16:48 +00:00
const logId = idForLogging ( group . groupId ) ;
2021-06-22 14:46:42 +00:00
const ACCESS_ENUM = Proto . AccessControl . AccessRequired ;
const MEMBER_ROLE_ENUM = Proto . Member . Role ;
2020-09-09 02:25:05 +00:00
const version = groupState . version || 0 ;
2024-06-25 21:50:15 +00:00
let result = { . . . group } ;
2021-04-07 22:45:31 +00:00
const newProfileKeys : Array < GroupChangeMemberType > = [ ] ;
2020-09-09 02:25:05 +00:00
2023-03-01 23:48:23 +00:00
// Used to capture changes not already expressed in group notifications or profile keys
let otherChanges = false ;
// Used to detect changes in these lists
const members : Record < string , GroupV2MemberType > = fromPairs (
2023-08-16 20:54:39 +00:00
( result . membersV2 || [ ] ) . map ( member = > [ member . aci , member ] )
2023-03-01 23:48:23 +00:00
) ;
const pendingMembers : Record < string , GroupV2PendingMemberType > = fromPairs (
2023-08-16 20:54:39 +00:00
( result . pendingMembersV2 || [ ] ) . map ( member = > [ member . serviceId , member ] )
2023-03-01 23:48:23 +00:00
) ;
const pendingAdminApprovalMembers : Record <
string ,
GroupV2PendingAdminApprovalType
> = fromPairs (
2023-08-16 20:54:39 +00:00
( result . pendingAdminApprovalV2 || [ ] ) . map ( member = > [ member . aci , member ] )
2023-03-01 23:48:23 +00:00
) ;
const bannedMembers = new Map < string , GroupV2BannedMemberType > (
2023-08-16 20:54:39 +00:00
( result . bannedMembersV2 || [ ] ) . map ( member = > [ member . serviceId , member ] )
2023-03-01 23:48:23 +00:00
) ;
2020-09-09 02:25:05 +00:00
// version
result . revision = version ;
// title
// Note: During decryption, title becomes a GroupAttributeBlob
2020-09-11 19:37:01 +00:00
const { title } = groupState ;
2020-09-09 02:25:05 +00:00
if ( title && title . content === 'title' ) {
2022-06-17 22:33:46 +00:00
result . name = dropNull ( title . title ) ;
2020-09-09 02:25:05 +00:00
} else {
result . name = undefined ;
}
// avatar
2024-06-25 21:50:15 +00:00
result = {
. . . result ,
. . . ( await applyNewAvatar ( dropNull ( groupState . avatar ) , result , logId ) ) ,
} ;
2020-09-09 02:25:05 +00:00
// disappearingMessagesTimer
// Note: during decryption, disappearingMessageTimer becomes a GroupAttributeBlob
2020-09-11 19:37:01 +00:00
const { disappearingMessagesTimer } = groupState ;
2020-09-09 02:25:05 +00:00
if (
disappearingMessagesTimer &&
disappearingMessagesTimer . content === 'disappearingMessagesDuration'
) {
2022-11-16 20:18:02 +00:00
const duration = disappearingMessagesTimer . disappearingMessagesDuration ;
result . expireTimer =
duration == null ? undefined : DurationInSeconds . fromSeconds ( duration ) ;
2020-09-09 02:25:05 +00:00
} else {
result . expireTimer = undefined ;
}
// accessControl
const { accessControl } = groupState ;
result . accessControl = {
attributes :
( accessControl && accessControl . attributes ) || ACCESS_ENUM . MEMBER ,
members : ( accessControl && accessControl . members ) || ACCESS_ENUM . MEMBER ,
2020-12-18 19:27:43 +00:00
addFromInviteLink :
( accessControl && accessControl . addFromInviteLink ) ||
ACCESS_ENUM . UNSATISFIABLE ,
2020-09-09 02:25:05 +00:00
} ;
// Optimization: we assume we have left the group unless we are found in members
result . left = true ;
2023-08-10 16:43:33 +00:00
const ourAci = window . storage . user . getCheckedAci ( ) ;
2020-09-09 02:25:05 +00:00
// members
2022-05-16 14:53:54 +00:00
const wasPreviouslyAMember = ( result . membersV2 || [ ] ) . some (
2023-08-16 20:54:39 +00:00
item = > item . aci !== ourAci
2022-05-16 14:53:54 +00:00
) ;
2020-09-09 02:25:05 +00:00
if ( groupState . members ) {
2021-06-22 14:46:42 +00:00
result . membersV2 = groupState . members . map ( member = > {
2023-08-10 16:43:33 +00:00
if ( member . userId === ourAci ) {
2020-09-09 02:25:05 +00:00
result . left = false ;
2020-10-06 17:06:34 +00:00
// Capture who added us if we were previously not in group
2024-09-16 18:05:58 +00:00
if ( pendingAdminApprovalMembers [ ourAci ] && ! wasPreviouslyAMember ) {
result . addedBy = sourceServiceId ;
} else if (
2023-08-16 20:54:39 +00:00
sourceServiceId &&
2022-05-16 14:53:54 +00:00
! wasPreviouslyAMember &&
isNumber ( member . joinedAtVersion ) &&
member . joinedAtVersion === version
2020-10-06 17:06:34 +00:00
) {
2023-08-16 20:54:39 +00:00
result . addedBy = sourceServiceId ;
2020-10-06 17:06:34 +00:00
}
2020-09-09 02:25:05 +00:00
}
2020-12-01 23:45:39 +00:00
if ( ! isValidRole ( member . role ) ) {
2020-12-18 19:27:43 +00:00
throw new Error (
` applyGroupState: Member had invalid role ${ member . role } `
) ;
2020-09-09 02:25:05 +00:00
}
2023-03-01 23:48:23 +00:00
const previousMember = members [ member . userId ] ;
2023-09-06 23:28:32 +00:00
if ( member . profileKey && ! hasProfileKey ( member . userId ) ) {
2021-06-22 14:46:42 +00:00
newProfileKeys . push ( {
profileKey : member.profileKey ,
2023-08-16 20:54:39 +00:00
aci : member.userId ,
2021-06-22 14:46:42 +00:00
} ) ;
2023-09-06 23:28:32 +00:00
} else if (
member . profileKey &&
profileKeyHasChanged ( member . userId , member . profileKey )
) {
log . warn (
` applyGroupState( ${ logId } ): Member ${ member . userId } had different profileKey `
) ;
otherChanges = true ;
2023-03-01 23:48:23 +00:00
} else if ( ! previousMember ) {
otherChanges = true ;
2021-06-22 14:46:42 +00:00
}
2021-04-07 22:45:31 +00:00
2023-03-01 23:48:23 +00:00
if (
previousMember &&
previousMember . joinedAtVersion !== member . joinedAtVersion
) {
otherChanges = true ;
log . warn (
` applyGroupState( ${ logId } ): Member ${ member . userId } had different joinedAtVersion `
) ;
}
// Note: role changes will be reflected in group update messages
2020-09-09 02:25:05 +00:00
return {
2020-12-01 23:45:39 +00:00
role : member.role || MEMBER_ROLE_ENUM . DEFAULT ,
2023-08-31 21:05:50 +00:00
joinedAtVersion : member.joinedAtVersion ,
2023-08-16 20:54:39 +00:00
aci : member.userId ,
2020-09-09 02:25:05 +00:00
} ;
} ) ;
}
2020-12-18 19:27:43 +00:00
// membersPendingProfileKey
if ( groupState . membersPendingProfileKey ) {
result . pendingMembersV2 = groupState . membersPendingProfileKey . map (
2021-06-22 14:46:42 +00:00
member = > {
2021-10-26 22:59:08 +00:00
if ( ! member . member || ! member . member . userId ) {
2020-12-01 23:45:39 +00:00
throw new Error (
2020-12-18 19:27:43 +00:00
'applyGroupState: Member pending profile key did not have an associated userId'
2020-12-01 23:45:39 +00:00
) ;
2020-09-09 02:25:05 +00:00
}
2021-10-26 22:59:08 +00:00
if ( ! member . addedByUserId ) {
2020-12-01 23:45:39 +00:00
throw new Error (
2020-12-18 19:27:43 +00:00
'applyGroupState: Member pending profile key did not have an addedByUserID'
2020-12-01 23:45:39 +00:00
) ;
}
if ( ! isValidRole ( member . member . role ) ) {
2020-12-18 19:27:43 +00:00
throw new Error (
` applyGroupState: Member pending profile key had invalid role ${ member . member . role } `
) ;
2020-09-09 02:25:05 +00:00
}
2023-03-01 23:48:23 +00:00
const previousMember = pendingMembers [ member . member . userId ] ;
2023-06-05 16:22:27 +00:00
otherChanges = true ;
2023-03-01 23:48:23 +00:00
if (
previousMember &&
previousMember . addedByUserId !== member . addedByUserId
) {
otherChanges = true ;
log . warn (
` applyGroupState( ${ logId } ): Member ${ member . member . userId } had different addedByUserId `
) ;
}
if ( previousMember && previousMember . timestamp !== member . timestamp ) {
otherChanges = true ;
log . warn (
` applyGroupState( ${ logId } ): Member ${ member . member . userId } had different timestamp `
) ;
}
if ( previousMember && previousMember . role !== member . member . role ) {
otherChanges = true ;
log . warn (
` applyGroupState( ${ logId } ): Member ${ member . member . userId } had different role `
) ;
2021-06-22 14:46:42 +00:00
}
2021-04-07 22:45:31 +00:00
2020-09-09 02:25:05 +00:00
return {
2023-08-10 16:43:33 +00:00
addedByUserId : member.addedByUserId ,
2023-08-16 20:54:39 +00:00
serviceId : member.member.userId ,
2020-09-09 02:25:05 +00:00
timestamp : member.timestamp ,
2020-12-01 23:45:39 +00:00
role : member.member.role || MEMBER_ROLE_ENUM . DEFAULT ,
2020-09-09 02:25:05 +00:00
} ;
}
) ;
}
2020-12-18 19:27:43 +00:00
// membersPendingAdminApproval
if ( groupState . membersPendingAdminApproval ) {
result . pendingAdminApprovalV2 = groupState . membersPendingAdminApproval . map (
2021-06-22 14:46:42 +00:00
member = > {
2023-03-01 23:48:23 +00:00
const previousMember = pendingAdminApprovalMembers [ member . userId ] ;
2023-09-06 23:28:32 +00:00
if ( member . profileKey && ! hasProfileKey ( member . userId ) ) {
2022-02-09 18:34:24 +00:00
newProfileKeys . push ( {
profileKey : member.profileKey ,
2023-08-16 20:54:39 +00:00
aci : member.userId ,
2022-02-09 18:34:24 +00:00
} ) ;
2023-09-06 23:28:32 +00:00
} else if (
member . profileKey &&
profileKeyHasChanged ( member . userId , member . profileKey )
) {
log . warn (
` applyGroupState( ${ logId } ): Member ${ member . userId } had different profileKey `
) ;
otherChanges = true ;
2023-03-01 23:48:23 +00:00
} else if ( ! previousMember ) {
otherChanges = true ;
}
if ( previousMember && previousMember . timestamp !== member . timestamp ) {
otherChanges = true ;
log . warn (
` applyGroupState( ${ logId } ): Member ${ member . userId } had different timestamp `
) ;
2022-02-09 18:34:24 +00:00
}
2020-12-18 19:27:43 +00:00
return {
2023-08-16 20:54:39 +00:00
aci : member.userId ,
2020-12-18 19:27:43 +00:00
timestamp : member.timestamp ,
} ;
}
) ;
}
// inviteLinkPassword
const { inviteLinkPassword } = groupState ;
if ( inviteLinkPassword ) {
result . groupInviteLinkPassword = inviteLinkPassword ;
} else {
result . groupInviteLinkPassword = undefined ;
}
2021-06-02 00:24:28 +00:00
// descriptionBytes
const { descriptionBytes } = groupState ;
if ( descriptionBytes && descriptionBytes . content === 'descriptionText' ) {
2022-06-17 22:33:46 +00:00
result . description = dropNull ( descriptionBytes . descriptionText ) ;
2021-06-02 00:24:28 +00:00
} else {
result . description = undefined ;
}
2021-07-21 17:58:39 +00:00
// announcementsOnly
result . announcementsOnly = groupState . announcementsOnly ;
2022-03-15 01:32:07 +00:00
// membersBanned
2023-03-01 23:48:23 +00:00
result . bannedMembersV2 = groupState . membersBanned ? . map ( member = > {
2023-08-16 20:54:39 +00:00
const previousMember = bannedMembers . get ( member . serviceId ) ;
2023-03-01 23:48:23 +00:00
if ( ! previousMember ) {
otherChanges = true ;
}
if ( previousMember && previousMember . timestamp !== member . timestamp ) {
otherChanges = true ;
log . warn (
2023-08-16 20:54:39 +00:00
` applyGroupState( ${ logId } ): Member ${ member . serviceId } had different timestamp `
2023-03-01 23:48:23 +00:00
) ;
}
return member ;
} ) ;
2022-03-15 01:32:07 +00:00
2022-05-16 14:53:54 +00:00
if ( result . left ) {
result . addedBy = undefined ;
}
2023-03-01 23:48:23 +00:00
if ( result . temporaryMemberCount ) {
log . info ( ` applyGroupState( ${ logId } ): Clearing temporaryMemberCount ` ) ;
result . temporaryMemberCount = undefined ;
}
2021-04-07 22:45:31 +00:00
return {
newAttributes : result ,
newProfileKeys ,
2023-03-01 23:48:23 +00:00
otherChanges ,
2021-04-07 22:45:31 +00:00
} ;
2020-09-09 02:25:05 +00:00
}
2020-12-01 23:45:39 +00:00
function isValidRole ( role? : number ) : role is number {
2021-06-22 14:46:42 +00:00
const MEMBER_ROLE_ENUM = Proto . Member . Role ;
2020-09-09 02:25:05 +00:00
return (
role === MEMBER_ROLE_ENUM . ADMINISTRATOR || role === MEMBER_ROLE_ENUM . DEFAULT
) ;
}
2020-12-18 19:27:43 +00:00
function isValidAccess ( access? : number ) : access is number {
2021-06-22 14:46:42 +00:00
const ACCESS_ENUM = Proto . AccessControl . AccessRequired ;
2020-09-09 02:25:05 +00:00
return access === ACCESS_ENUM . ADMINISTRATOR || access === ACCESS_ENUM . MEMBER ;
}
2020-12-18 19:27:43 +00:00
function isValidLinkAccess ( access? : number ) : access is number {
2021-06-22 14:46:42 +00:00
const ACCESS_ENUM = Proto . AccessControl . AccessRequired ;
2020-12-18 19:27:43 +00:00
return (
access === ACCESS_ENUM . UNKNOWN ||
access === ACCESS_ENUM . ANY ||
access === ACCESS_ENUM . ADMINISTRATOR ||
access === ACCESS_ENUM . UNSATISFIABLE
) ;
}
2021-06-22 14:46:42 +00:00
function isValidProfileKey ( buffer? : Uint8Array ) : boolean {
return Boolean ( buffer && buffer . length === 32 ) ;
2020-09-09 02:25:05 +00:00
}
2022-03-23 20:49:27 +00:00
function normalizeTimestamp ( timestamp : Long | null | undefined ) : number {
2020-12-18 19:27:43 +00:00
if ( ! timestamp ) {
2021-06-22 14:46:42 +00:00
return 0 ;
}
2020-12-18 19:27:43 +00:00
const asNumber = timestamp . toNumber ( ) ;
const now = Date . now ( ) ;
if ( ! asNumber || asNumber > now ) {
return now ;
}
return asNumber ;
}
2021-06-22 14:46:42 +00:00
type DecryptedGroupChangeActions = {
version? : number ;
2023-08-16 20:54:39 +00:00
sourceServiceId? : ServiceIdString ;
2021-06-22 14:46:42 +00:00
addMembers? : ReadonlyArray < {
added : DecryptedMember ;
joinFromInviteLink : boolean ;
} > ;
deleteMembers? : ReadonlyArray < {
2023-08-10 16:43:33 +00:00
deletedUserId : AciString ;
2021-06-22 14:46:42 +00:00
} > ;
modifyMemberRoles? : ReadonlyArray < {
2023-08-10 16:43:33 +00:00
userId : AciString ;
2021-06-22 14:46:42 +00:00
role : Proto.Member.Role ;
} > ;
modifyMemberProfileKeys? : ReadonlyArray < {
profileKey : Uint8Array ;
2023-08-16 20:54:39 +00:00
aci : AciString ;
2021-06-22 14:46:42 +00:00
} > ;
addPendingMembers? : ReadonlyArray < {
added : DecryptedMemberPendingProfileKey ;
} > ;
deletePendingMembers? : ReadonlyArray < {
2023-08-10 16:43:33 +00:00
// This might be a PNI
deletedUserId : ServiceIdString ;
2021-06-22 14:46:42 +00:00
} > ;
promotePendingMembers? : ReadonlyArray < {
profileKey : Uint8Array ;
2023-08-16 20:54:39 +00:00
aci : AciString ;
2021-06-22 14:46:42 +00:00
} > ;
2022-07-08 20:46:25 +00:00
promoteMembersPendingPniAciProfileKey? : ReadonlyArray < {
profileKey : Uint8Array ;
2023-08-10 16:43:33 +00:00
aci : AciString ;
pni : PniString ;
2022-07-08 20:46:25 +00:00
} > ;
2021-06-22 14:46:42 +00:00
modifyTitle ? : {
title? : Proto.GroupAttributeBlob ;
} ;
modifyDisappearingMessagesTimer ? : {
timer? : Proto.GroupAttributeBlob ;
} ;
addMemberPendingAdminApprovals? : ReadonlyArray < {
added : DecryptedMemberPendingAdminApproval ;
} > ;
deleteMemberPendingAdminApprovals? : ReadonlyArray < {
2023-08-10 16:43:33 +00:00
deletedUserId : AciString ;
2021-06-22 14:46:42 +00:00
} > ;
promoteMemberPendingAdminApprovals? : ReadonlyArray < {
2023-08-10 16:43:33 +00:00
userId : AciString ;
2021-06-22 14:46:42 +00:00
role : Proto.Member.Role ;
} > ;
modifyInviteLinkPassword ? : {
inviteLinkPassword? : string ;
} ;
modifyDescription ? : {
descriptionBytes? : Proto.GroupAttributeBlob ;
} ;
2021-07-21 17:58:39 +00:00
modifyAnnouncementsOnly ? : {
announcementsOnly : boolean ;
} ;
2022-03-23 22:34:51 +00:00
addMembersBanned? : ReadonlyArray < GroupV2BannedMemberType > ;
2023-08-10 16:43:33 +00:00
// This might be a PNI
deleteMembersBanned? : ReadonlyArray < ServiceIdString > ;
2021-06-22 14:46:42 +00:00
} & Pick <
Proto . GroupChange . IActions ,
| 'modifyAttributesAccess'
| 'modifyMemberAccess'
| 'modifyAddFromInviteLinkAccess'
| 'modifyAvatar'
> ;
2020-12-18 19:27:43 +00:00
2020-09-09 02:25:05 +00:00
function decryptGroupChange (
2021-06-22 14:46:42 +00:00
actions : Readonly < Proto.GroupChange.IActions > ,
2020-09-09 02:25:05 +00:00
groupSecretParams : string ,
logId : string
2021-06-22 14:46:42 +00:00
) : DecryptedGroupChangeActions {
const result : DecryptedGroupChangeActions = {
version : dropNull ( actions . version ) ,
} ;
2020-09-09 02:25:05 +00:00
const clientZkGroupCipher = getClientZkGroupCipher ( groupSecretParams ) ;
2023-08-16 20:54:39 +00:00
if ( actions . sourceUserId && actions . sourceUserId . length !== 0 ) {
2020-09-09 02:25:05 +00:00
try {
2023-08-16 20:54:39 +00:00
result . sourceServiceId = decryptServiceId (
clientZkGroupCipher ,
actions . sourceUserId
2020-09-09 02:25:05 +00:00
) ;
} catch ( error ) {
2021-09-17 18:27:53 +00:00
log . warn (
2023-08-16 20:54:39 +00:00
` decryptGroupChange/ ${ logId } : Unable to decrypt sourceServiceId. ` ,
2022-11-22 18:43:43 +00:00
Errors . toLogFormat ( error )
2020-09-09 02:25:05 +00:00
) ;
}
2023-08-16 20:54:39 +00:00
if ( ! result . sourceServiceId || ! isServiceIdString ( result . sourceServiceId ) ) {
2021-09-17 18:27:53 +00:00
log . warn (
2023-08-16 20:54:39 +00:00
` decryptGroupChange/ ${ logId } : Invalid sourceServiceId. Clearing sourceServiceId. `
2020-09-09 02:25:05 +00:00
) ;
2023-08-16 20:54:39 +00:00
result . sourceServiceId = undefined ;
2020-09-09 02:25:05 +00:00
}
} else {
2023-08-16 20:54:39 +00:00
throw new Error ( 'decryptGroupChange: Missing sourceServiceId' ) ;
2020-09-09 02:25:05 +00:00
}
2021-06-22 14:46:42 +00:00
// addMembers?: Array<GroupChange.Actions.AddMemberAction>;
result . addMembers = compact (
2020-12-18 19:27:43 +00:00
( actions . addMembers || [ ] ) . map ( addMember = > {
2021-06-22 14:46:42 +00:00
strictAssert (
addMember . added ,
'decryptGroupChange: AddMember was missing added field!'
) ;
2024-05-20 18:15:39 +00:00
2021-06-22 14:46:42 +00:00
const decrypted = decryptMember (
clientZkGroupCipher ,
addMember . added ,
logId
) ;
if ( ! decrypted ) {
return null ;
2020-09-09 02:25:05 +00:00
}
2021-06-22 14:46:42 +00:00
return {
added : decrypted ,
joinFromInviteLink : Boolean ( addMember . joinFromInviteLink ) ,
} ;
2020-09-09 02:25:05 +00:00
} )
) ;
2021-06-22 14:46:42 +00:00
// deleteMembers?: Array<GroupChange.Actions.DeleteMemberAction>;
result . deleteMembers = compact (
2020-12-18 19:27:43 +00:00
( actions . deleteMembers || [ ] ) . map ( deleteMember = > {
2021-06-22 14:46:42 +00:00
const { deletedUserId } = deleteMember ;
strictAssert (
Bytes . isNotEmpty ( deletedUserId ) ,
'decryptGroupChange: deleteMember.deletedUserId was missing'
) ;
2023-08-10 16:43:33 +00:00
let userId : AciString ;
2021-06-22 14:46:42 +00:00
try {
2023-08-16 20:54:39 +00:00
userId = decryptAci ( clientZkGroupCipher , deletedUserId ) ;
2021-06-22 14:46:42 +00:00
} catch ( error ) {
2021-09-17 18:27:53 +00:00
log . warn (
2021-06-22 14:46:42 +00:00
` decryptGroupChange/ ${ logId } : Unable to decrypt deleteMembers.deletedUserId. Dropping member. ` ,
2022-11-22 18:43:43 +00:00
Errors . toLogFormat ( error )
2021-06-22 14:46:42 +00:00
) ;
return null ;
2020-09-09 02:25:05 +00:00
}
2021-06-22 14:46:42 +00:00
return { deletedUserId : userId } ;
2020-09-09 02:25:05 +00:00
} )
) ;
2021-06-22 14:46:42 +00:00
// modifyMemberRoles?: Array<GroupChange.Actions.ModifyMemberRoleAction>;
result . modifyMemberRoles = compact (
2020-12-18 19:27:43 +00:00
( actions . modifyMemberRoles || [ ] ) . map ( modifyMember = > {
2021-06-22 14:46:42 +00:00
strictAssert (
Bytes . isNotEmpty ( modifyMember . userId ) ,
'decryptGroupChange: modifyMemberRole.userId was missing'
) ;
2023-08-10 16:43:33 +00:00
let userId : AciString ;
2021-06-22 14:46:42 +00:00
try {
2023-08-16 20:54:39 +00:00
userId = decryptAci ( clientZkGroupCipher , modifyMember . userId ) ;
2021-06-22 14:46:42 +00:00
} catch ( error ) {
2021-09-17 18:27:53 +00:00
log . warn (
2021-06-22 14:46:42 +00:00
` decryptGroupChange/ ${ logId } : Unable to decrypt modifyMemberRole.userId. Dropping member. ` ,
2022-11-22 18:43:43 +00:00
Errors . toLogFormat ( error )
2020-09-09 02:25:05 +00:00
) ;
2021-06-22 14:46:42 +00:00
return null ;
2020-09-09 02:25:05 +00:00
}
2021-06-22 14:46:42 +00:00
const role = dropNull ( modifyMember . role ) ;
if ( ! isValidRole ( role ) ) {
2020-09-09 02:25:05 +00:00
throw new Error (
2020-12-18 19:27:43 +00:00
` decryptGroupChange: modifyMemberRole had invalid role ${ modifyMember . role } `
2020-09-09 02:25:05 +00:00
) ;
}
2021-06-22 14:46:42 +00:00
return {
role ,
userId ,
} ;
2020-09-09 02:25:05 +00:00
} )
) ;
2020-12-18 19:27:43 +00:00
// modifyMemberProfileKeys?: Array<
2021-06-22 14:46:42 +00:00
// GroupChange.Actions.ModifyMemberProfileKeyAction
2020-12-18 19:27:43 +00:00
// >;
2021-06-22 14:46:42 +00:00
result . modifyMemberProfileKeys = compact (
2020-12-18 19:27:43 +00:00
( actions . modifyMemberProfileKeys || [ ] ) . map ( modifyMemberProfileKey = > {
2022-07-08 20:46:25 +00:00
let { userId , profileKey : encryptedProfileKey } = modifyMemberProfileKey ;
// TODO: DESKTOP-3816
if ( Bytes . isEmpty ( userId ) || Bytes . isEmpty ( encryptedProfileKey ) ) {
const { presentation } = modifyMemberProfileKey ;
strictAssert (
Bytes . isNotEmpty ( presentation ) ,
'decryptGroupChange: modifyMemberProfileKeys.presentation was missing'
) ;
const decodedPresentation =
decodeProfileKeyCredentialPresentation ( presentation ) ;
( { userId , profileKey : encryptedProfileKey } = decodedPresentation ) ;
}
2021-06-22 14:46:42 +00:00
strictAssert (
2022-07-08 20:46:25 +00:00
Bytes . isNotEmpty ( userId ) ,
'decryptGroupChange: modifyMemberProfileKeys.userId was missing'
2021-06-22 14:46:42 +00:00
) ;
2022-07-08 20:46:25 +00:00
strictAssert (
Bytes . isNotEmpty ( encryptedProfileKey ) ,
'decryptGroupChange: modifyMemberProfileKeys.profileKey was missing'
2021-06-22 14:46:42 +00:00
) ;
2020-09-09 02:25:05 +00:00
2023-08-16 20:54:39 +00:00
let aci : AciString ;
2022-07-08 20:46:25 +00:00
let profileKey : Uint8Array ;
try {
2023-08-16 20:54:39 +00:00
aci = decryptAci ( clientZkGroupCipher , userId ) ;
2020-09-09 02:25:05 +00:00
2022-07-08 20:46:25 +00:00
profileKey = decryptProfileKey (
clientZkGroupCipher ,
encryptedProfileKey ,
2023-08-16 20:54:39 +00:00
aci
2022-07-08 20:46:25 +00:00
) ;
} catch ( error ) {
2021-09-17 18:27:53 +00:00
log . warn (
2022-07-08 20:46:25 +00:00
` decryptGroupChange/ ${ logId } : Unable to decrypt ` +
'modifyMemberProfileKeys.userId/profileKey. Dropping member.' ,
Errors . toLogFormat ( error )
2021-06-22 14:46:42 +00:00
) ;
return null ;
}
2020-09-09 02:25:05 +00:00
2022-07-08 20:46:25 +00:00
if ( ! isValidProfileKey ( profileKey ) ) {
2020-09-09 02:25:05 +00:00
throw new Error (
2021-06-22 14:46:42 +00:00
'decryptGroupChange: modifyMemberProfileKey had invalid profileKey'
2020-09-09 02:25:05 +00:00
) ;
}
2023-08-16 20:54:39 +00:00
return { aci , profileKey } ;
2020-09-09 02:25:05 +00:00
} )
) ;
2020-12-18 19:27:43 +00:00
// addPendingMembers?: Array<
2021-06-22 14:46:42 +00:00
// GroupChange.Actions.AddMemberPendingProfileKeyAction
2020-12-18 19:27:43 +00:00
// >;
2021-06-22 14:46:42 +00:00
result . addPendingMembers = compact (
2020-12-18 19:27:43 +00:00
( actions . addPendingMembers || [ ] ) . map ( addPendingMember = > {
2021-06-22 14:46:42 +00:00
strictAssert (
addPendingMember . added ,
2020-09-11 19:37:01 +00:00
'decryptGroupChange: addPendingMember was missing added field!'
) ;
2021-06-22 14:46:42 +00:00
const decrypted = decryptMemberPendingProfileKey (
clientZkGroupCipher ,
addPendingMember . added ,
logId
) ;
if ( ! decrypted ) {
return null ;
}
return {
added : decrypted ,
} ;
2020-09-09 02:25:05 +00:00
} )
) ;
2020-12-18 19:27:43 +00:00
// deletePendingMembers?: Array<
2021-06-22 14:46:42 +00:00
// GroupChange.Actions.DeleteMemberPendingProfileKeyAction
2020-12-18 19:27:43 +00:00
// >;
2021-06-22 14:46:42 +00:00
result . deletePendingMembers = compact (
2020-12-18 19:27:43 +00:00
( actions . deletePendingMembers || [ ] ) . map ( deletePendingMember = > {
2021-06-22 14:46:42 +00:00
const { deletedUserId } = deletePendingMember ;
strictAssert (
Bytes . isNotEmpty ( deletedUserId ) ,
'decryptGroupChange: deletePendingMembers.deletedUserId was missing'
) ;
2023-08-10 16:43:33 +00:00
let userId : ServiceIdString ;
2021-06-22 14:46:42 +00:00
try {
2023-08-16 20:54:39 +00:00
userId = decryptServiceId ( clientZkGroupCipher , deletedUserId ) ;
2021-06-22 14:46:42 +00:00
} catch ( error ) {
2021-09-17 18:27:53 +00:00
log . warn (
2021-06-22 14:46:42 +00:00
` decryptGroupChange/ ${ logId } : Unable to decrypt deletePendingMembers.deletedUserId. Dropping member. ` ,
2022-11-22 18:43:43 +00:00
Errors . toLogFormat ( error )
2021-06-22 14:46:42 +00:00
) ;
return null ;
2020-09-09 02:25:05 +00:00
}
2023-08-10 16:43:33 +00:00
if ( ! isServiceIdString ( userId ) ) {
2021-09-17 18:27:53 +00:00
log . warn (
2020-09-09 02:25:05 +00:00
` decryptGroupChange/ ${ logId } : Dropping deletePendingMember due to invalid deletedUserId `
) ;
return null ;
}
2021-06-22 14:46:42 +00:00
return {
deletedUserId : userId ,
} ;
2020-09-09 02:25:05 +00:00
} )
) ;
2020-12-18 19:27:43 +00:00
// promotePendingMembers?: Array<
2021-06-22 14:46:42 +00:00
// GroupChange.Actions.PromoteMemberPendingProfileKeyAction
2020-12-18 19:27:43 +00:00
// >;
2021-06-22 14:46:42 +00:00
result . promotePendingMembers = compact (
2020-12-18 19:27:43 +00:00
( actions . promotePendingMembers || [ ] ) . map ( promotePendingMember = > {
2022-07-08 20:46:25 +00:00
let { userId , profileKey : encryptedProfileKey } = promotePendingMember ;
// TODO: DESKTOP-3816
if ( Bytes . isEmpty ( userId ) || Bytes . isEmpty ( encryptedProfileKey ) ) {
const { presentation } = promotePendingMember ;
strictAssert (
Bytes . isNotEmpty ( presentation ) ,
'decryptGroupChange: promotePendingMember.presentation was missing'
) ;
const decodedPresentation =
decodeProfileKeyCredentialPresentation ( presentation ) ;
( { userId , profileKey : encryptedProfileKey } = decodedPresentation ) ;
}
2021-06-22 14:46:42 +00:00
strictAssert (
2022-07-08 20:46:25 +00:00
Bytes . isNotEmpty ( userId ) ,
'decryptGroupChange: promotePendingMembers.userId was missing'
2021-06-22 14:46:42 +00:00
) ;
2022-07-08 20:46:25 +00:00
strictAssert (
Bytes . isNotEmpty ( encryptedProfileKey ) ,
'decryptGroupChange: promotePendingMembers.profileKey was missing'
2021-06-22 14:46:42 +00:00
) ;
2020-09-09 02:25:05 +00:00
2023-08-16 20:54:39 +00:00
let aci : AciString ;
2022-07-08 20:46:25 +00:00
let profileKey : Uint8Array ;
try {
2023-08-16 20:54:39 +00:00
aci = decryptAci ( clientZkGroupCipher , userId ) ;
2020-09-09 02:25:05 +00:00
2022-07-08 20:46:25 +00:00
profileKey = decryptProfileKey (
clientZkGroupCipher ,
encryptedProfileKey ,
2023-08-16 20:54:39 +00:00
aci
2022-07-08 20:46:25 +00:00
) ;
} catch ( error ) {
2021-09-17 18:27:53 +00:00
log . warn (
2022-07-08 20:46:25 +00:00
` decryptGroupChange/ ${ logId } : Unable to decrypt ` +
'promotePendingMembers.userId/profileKey. Dropping member.' ,
Errors . toLogFormat ( error )
2021-06-22 14:46:42 +00:00
) ;
return null ;
}
2020-09-09 02:25:05 +00:00
2022-07-08 20:46:25 +00:00
if ( ! isValidProfileKey ( profileKey ) ) {
2020-09-09 02:25:05 +00:00
throw new Error (
2022-07-08 20:46:25 +00:00
'decryptGroupChange: promotePendingMembers had invalid profileKey'
2020-09-09 02:25:05 +00:00
) ;
}
2023-08-16 20:54:39 +00:00
return { aci , profileKey } ;
2020-09-09 02:25:05 +00:00
} )
) ;
2022-07-08 20:46:25 +00:00
// promoteMembersPendingPniAciProfileKey?: Array<
// GroupChange.Actions.PromoteMemberPendingPniAciProfileKeyAction
// >;
result . promoteMembersPendingPniAciProfileKey = compact (
( actions . promoteMembersPendingPniAciProfileKey || [ ] ) . map (
promotePendingMember = > {
strictAssert (
Bytes . isNotEmpty ( promotePendingMember . userId ) ,
'decryptGroupChange: ' +
'promoteMembersPendingPniAciProfileKey.userId was missing'
) ;
strictAssert (
Bytes . isNotEmpty ( promotePendingMember . pni ) ,
'decryptGroupChange: ' +
'promoteMembersPendingPniAciProfileKey.pni was missing'
) ;
strictAssert (
Bytes . isNotEmpty ( promotePendingMember . profileKey ) ,
'decryptGroupChange: ' +
'promoteMembersPendingPniAciProfileKey.profileKey was missing'
) ;
2023-08-10 16:43:33 +00:00
let aci : AciString ;
let pni : PniString ;
2022-07-08 20:46:25 +00:00
let profileKey : Uint8Array ;
try {
2023-08-16 20:54:39 +00:00
aci = decryptAci ( clientZkGroupCipher , promotePendingMember . userId ) ;
pni = decryptPni ( clientZkGroupCipher , promotePendingMember . pni ) ;
2022-07-08 20:46:25 +00:00
profileKey = decryptProfileKey (
clientZkGroupCipher ,
promotePendingMember . profileKey ,
2023-08-10 16:43:33 +00:00
aci
2022-07-08 20:46:25 +00:00
) ;
} catch ( error ) {
log . warn (
` decryptGroupChange/ ${ logId } : Unable to decrypt promoteMembersPendingPniAciProfileKey. Dropping member. ` ,
Errors . toLogFormat ( error )
) ;
return null ;
}
if ( ! isValidProfileKey ( profileKey ) ) {
throw new Error (
'decryptGroupChange: promoteMembersPendingPniAciProfileKey ' +
'had invalid profileKey'
) ;
}
return {
2023-08-10 16:43:33 +00:00
aci ,
2022-07-08 20:46:25 +00:00
pni ,
profileKey ,
} ;
}
)
) ;
2021-06-22 14:46:42 +00:00
// modifyTitle?: GroupChange.Actions.ModifyTitleAction;
if ( actions . modifyTitle ) {
const { title } = actions . modifyTitle ;
if ( Bytes . isNotEmpty ( title ) ) {
try {
result . modifyTitle = {
title : Proto.GroupAttributeBlob.decode (
decryptGroupBlob ( clientZkGroupCipher , title )
) ,
} ;
} catch ( error ) {
2021-09-17 18:27:53 +00:00
log . warn (
2021-06-22 14:46:42 +00:00
` decryptGroupChange/ ${ logId } : Unable to decrypt modifyTitle.title ` ,
2022-11-22 18:43:43 +00:00
Errors . toLogFormat ( error )
2021-06-22 14:46:42 +00:00
) ;
}
} else {
result . modifyTitle = { } ;
2020-09-09 02:25:05 +00:00
}
}
2021-06-22 14:46:42 +00:00
// modifyAvatar?: GroupChange.Actions.ModifyAvatarAction;
2020-09-09 02:25:05 +00:00
// Note: decryption happens during application of the change, on download of the avatar
2021-06-22 14:46:42 +00:00
result . modifyAvatar = actions . modifyAvatar ;
2020-09-09 02:25:05 +00:00
2020-09-11 19:37:01 +00:00
// modifyDisappearingMessagesTimer?:
2021-06-22 14:46:42 +00:00
// GroupChange.Actions.ModifyDisappearingMessagesTimerAction;
if ( actions . modifyDisappearingMessagesTimer ) {
const { timer } = actions . modifyDisappearingMessagesTimer ;
if ( Bytes . isNotEmpty ( timer ) ) {
try {
result . modifyDisappearingMessagesTimer = {
timer : Proto.GroupAttributeBlob.decode (
decryptGroupBlob ( clientZkGroupCipher , timer )
) ,
} ;
} catch ( error ) {
2021-09-17 18:27:53 +00:00
log . warn (
2021-06-22 14:46:42 +00:00
` decryptGroupChange/ ${ logId } : Unable to decrypt modifyDisappearingMessagesTimer.timer ` ,
2022-11-22 18:43:43 +00:00
Errors . toLogFormat ( error )
2021-06-22 14:46:42 +00:00
) ;
}
} else {
result . modifyDisappearingMessagesTimer = { } ;
2020-09-09 02:25:05 +00:00
}
}
2020-09-11 19:37:01 +00:00
// modifyAttributesAccess?:
2021-06-22 14:46:42 +00:00
// GroupChange.Actions.ModifyAttributesAccessControlAction;
if ( actions . modifyAttributesAccess ) {
const attributesAccess = dropNull (
actions . modifyAttributesAccess . attributesAccess
) ;
strictAssert (
isValidAccess ( attributesAccess ) ,
2020-12-18 19:27:43 +00:00
` decryptGroupChange: modifyAttributesAccess.attributesAccess was not valid: ${ actions . modifyAttributesAccess . attributesAccess } `
2020-09-09 02:25:05 +00:00
) ;
2021-06-22 14:46:42 +00:00
result . modifyAttributesAccess = {
attributesAccess ,
} ;
2020-09-09 02:25:05 +00:00
}
2021-06-22 14:46:42 +00:00
// modifyMemberAccess?: GroupChange.Actions.ModifyMembersAccessControlAction;
if ( actions . modifyMemberAccess ) {
const membersAccess = dropNull ( actions . modifyMemberAccess . membersAccess ) ;
strictAssert (
isValidAccess ( membersAccess ) ,
2020-12-18 19:27:43 +00:00
` decryptGroupChange: modifyMemberAccess.membersAccess was not valid: ${ actions . modifyMemberAccess . membersAccess } `
) ;
2021-06-22 14:46:42 +00:00
result . modifyMemberAccess = {
membersAccess ,
} ;
2020-12-18 19:27:43 +00:00
}
// modifyAddFromInviteLinkAccess?:
2021-06-22 14:46:42 +00:00
// GroupChange.Actions.ModifyAddFromInviteLinkAccessControlAction;
if ( actions . modifyAddFromInviteLinkAccess ) {
const addFromInviteLinkAccess = dropNull (
2020-12-18 19:27:43 +00:00
actions . modifyAddFromInviteLinkAccess . addFromInviteLinkAccess
2021-06-22 14:46:42 +00:00
) ;
strictAssert (
isValidLinkAccess ( addFromInviteLinkAccess ) ,
2020-12-18 19:27:43 +00:00
` decryptGroupChange: modifyAddFromInviteLinkAccess.addFromInviteLinkAccess was not valid: ${ actions . modifyAddFromInviteLinkAccess . addFromInviteLinkAccess } `
) ;
2021-06-22 14:46:42 +00:00
result . modifyAddFromInviteLinkAccess = {
addFromInviteLinkAccess ,
} ;
2020-12-18 19:27:43 +00:00
}
// addMemberPendingAdminApprovals?: Array<
2021-06-22 14:46:42 +00:00
// GroupChange.Actions.AddMemberPendingAdminApprovalAction
2020-12-18 19:27:43 +00:00
// >;
2021-06-22 14:46:42 +00:00
result . addMemberPendingAdminApprovals = compact (
2020-12-18 19:27:43 +00:00
( actions . addMemberPendingAdminApprovals || [ ] ) . map (
addPendingAdminApproval = > {
2021-06-22 14:46:42 +00:00
const { added } = addPendingAdminApproval ;
strictAssert (
added ,
2020-12-18 19:27:43 +00:00
'decryptGroupChange: addPendingAdminApproval was missing added field!'
) ;
2021-06-22 14:46:42 +00:00
const decrypted = decryptMemberPendingAdminApproval (
clientZkGroupCipher ,
added ,
logId
) ;
if ( ! decrypted ) {
2021-09-17 18:27:53 +00:00
log . warn (
2021-06-22 14:46:42 +00:00
` decryptGroupChange/ ${ logId } : Unable to decrypt addPendingAdminApproval.added. Dropping member. `
) ;
return null ;
}
return { added : decrypted } ;
2020-12-18 19:27:43 +00:00
}
)
) ;
// deleteMemberPendingAdminApprovals?: Array<
2021-06-22 14:46:42 +00:00
// GroupChange.Actions.DeleteMemberPendingAdminApprovalAction
2020-12-18 19:27:43 +00:00
// >;
2021-06-22 14:46:42 +00:00
result . deleteMemberPendingAdminApprovals = compact (
2020-12-18 19:27:43 +00:00
( actions . deleteMemberPendingAdminApprovals || [ ] ) . map (
deletePendingApproval = > {
2021-06-22 14:46:42 +00:00
const { deletedUserId } = deletePendingApproval ;
strictAssert (
Bytes . isNotEmpty ( deletedUserId ) ,
'decryptGroupChange: deletePendingApproval.deletedUserId was missing'
2020-12-18 19:27:43 +00:00
) ;
2023-08-16 20:54:39 +00:00
let aci : AciString ;
2021-06-22 14:46:42 +00:00
try {
2023-08-16 20:54:39 +00:00
aci = decryptAci ( clientZkGroupCipher , deletedUserId ) ;
2021-06-22 14:46:42 +00:00
} catch ( error ) {
2021-09-17 18:27:53 +00:00
log . warn (
2021-06-22 14:46:42 +00:00
` decryptGroupChange/ ${ logId } : Unable to decrypt deletePendingApproval.deletedUserId. Dropping member. ` ,
2022-11-22 18:43:43 +00:00
Errors . toLogFormat ( error )
2021-06-22 14:46:42 +00:00
) ;
return null ;
}
2020-12-18 19:27:43 +00:00
2023-08-16 20:54:39 +00:00
return { deletedUserId : aci } ;
2020-12-18 19:27:43 +00:00
}
)
) ;
// promoteMemberPendingAdminApprovals?: Array<
2021-06-22 14:46:42 +00:00
// GroupChange.Actions.PromoteMemberPendingAdminApprovalAction
2020-12-18 19:27:43 +00:00
// >;
2021-06-22 14:46:42 +00:00
result . promoteMemberPendingAdminApprovals = compact (
2020-12-18 19:27:43 +00:00
( actions . promoteMemberPendingAdminApprovals || [ ] ) . map (
promoteAdminApproval = > {
2021-06-22 14:46:42 +00:00
const { userId } = promoteAdminApproval ;
strictAssert (
Bytes . isNotEmpty ( userId ) ,
'decryptGroupChange: promoteAdminApproval.userId was missing'
) ;
2023-08-10 16:43:33 +00:00
let decryptedUserId : AciString ;
2021-06-22 14:46:42 +00:00
try {
2023-08-16 20:54:39 +00:00
decryptedUserId = decryptAci ( clientZkGroupCipher , userId ) ;
2021-06-22 14:46:42 +00:00
} catch ( error ) {
2021-09-17 18:27:53 +00:00
log . warn (
2021-06-22 14:46:42 +00:00
` decryptGroupChange/ ${ logId } : Unable to decrypt promoteAdminApproval.userId. Dropping member. ` ,
2022-11-22 18:43:43 +00:00
Errors . toLogFormat ( error )
2021-06-22 14:46:42 +00:00
) ;
return null ;
2020-12-18 19:27:43 +00:00
}
2021-06-22 14:46:42 +00:00
const role = dropNull ( promoteAdminApproval . role ) ;
if ( ! isValidRole ( role ) ) {
2020-12-18 19:27:43 +00:00
throw new Error (
` decryptGroupChange: promoteAdminApproval had invalid role ${ promoteAdminApproval . role } `
) ;
}
2021-06-22 14:46:42 +00:00
return { role , userId : decryptedUserId } ;
2020-12-18 19:27:43 +00:00
}
)
) ;
2021-06-22 14:46:42 +00:00
// modifyInviteLinkPassword?: GroupChange.Actions.ModifyInviteLinkPasswordAction;
if ( actions . modifyInviteLinkPassword ) {
const { inviteLinkPassword : password } = actions . modifyInviteLinkPassword ;
if ( Bytes . isNotEmpty ( password ) ) {
result . modifyInviteLinkPassword = {
inviteLinkPassword : Bytes.toBase64 ( password ) ,
} ;
} else {
result . modifyInviteLinkPassword = { } ;
}
2020-09-09 02:25:05 +00:00
}
2021-06-22 14:46:42 +00:00
// modifyDescription?: GroupChange.Actions.ModifyDescriptionAction;
if ( actions . modifyDescription ) {
const { descriptionBytes } = actions . modifyDescription ;
if ( Bytes . isNotEmpty ( descriptionBytes ) ) {
try {
result . modifyDescription = {
descriptionBytes : Proto.GroupAttributeBlob.decode (
decryptGroupBlob ( clientZkGroupCipher , descriptionBytes )
) ,
} ;
} catch ( error ) {
2021-09-17 18:27:53 +00:00
log . warn (
2021-06-22 14:46:42 +00:00
` decryptGroupChange/ ${ logId } : Unable to decrypt modifyDescription.descriptionBytes ` ,
2022-11-22 18:43:43 +00:00
Errors . toLogFormat ( error )
2021-06-22 14:46:42 +00:00
) ;
}
} else {
result . modifyDescription = { } ;
2021-06-02 00:24:28 +00:00
}
}
2021-07-21 17:58:39 +00:00
// modifyAnnouncementsOnly
if ( actions . modifyAnnouncementsOnly ) {
const { announcementsOnly } = actions . modifyAnnouncementsOnly ;
result . modifyAnnouncementsOnly = {
announcementsOnly : Boolean ( announcementsOnly ) ,
} ;
}
2022-03-15 01:32:07 +00:00
// addMembersBanned
if ( actions . addMembersBanned && actions . addMembersBanned . length > 0 ) {
result . addMembersBanned = actions . addMembersBanned
. map ( item = > {
if ( ! item . added || ! item . added . userId ) {
log . warn (
` decryptGroupChange/ ${ logId } : addMembersBanned had a blank entry `
) ;
return null ;
}
2023-08-16 20:54:39 +00:00
const serviceId = decryptServiceId (
clientZkGroupCipher ,
item . added . userId
2022-03-15 01:32:07 +00:00
) ;
2022-03-23 22:34:51 +00:00
const timestamp = normalizeTimestamp ( item . added . timestamp ) ;
2023-08-16 20:54:39 +00:00
return { serviceId , timestamp } ;
2022-03-15 01:32:07 +00:00
} )
. filter ( isNotNil ) ;
}
// deleteMembersBanned
if ( actions . deleteMembersBanned && actions . deleteMembersBanned . length > 0 ) {
result . deleteMembersBanned = actions . deleteMembersBanned
. map ( item = > {
if ( ! item . deletedUserId ) {
log . warn (
` decryptGroupChange/ ${ logId } : deleteMembersBanned had a blank entry `
) ;
return null ;
}
2023-08-16 20:54:39 +00:00
return decryptServiceId ( clientZkGroupCipher , item . deletedUserId ) ;
2022-03-15 01:32:07 +00:00
} )
. filter ( isNotNil ) ;
}
2021-06-22 14:46:42 +00:00
return result ;
2020-09-09 02:25:05 +00:00
}
2021-01-29 22:16:48 +00:00
export function decryptGroupTitle (
2021-06-22 14:46:42 +00:00
title : Uint8Array | undefined ,
2021-01-29 22:16:48 +00:00
secretParams : string
) : string | undefined {
const clientZkGroupCipher = getClientZkGroupCipher ( secretParams ) ;
2021-06-22 14:46:42 +00:00
if ( ! title || ! title . length ) {
return undefined ;
}
const blob = Proto . GroupAttributeBlob . decode (
decryptGroupBlob ( clientZkGroupCipher , title )
) ;
2021-01-29 22:16:48 +00:00
2021-06-22 14:46:42 +00:00
if ( blob && blob . content === 'title' ) {
2022-06-17 22:33:46 +00:00
return dropNull ( blob . title ) ;
2021-01-29 22:16:48 +00:00
}
return undefined ;
}
2021-06-02 00:24:28 +00:00
export function decryptGroupDescription (
2021-06-22 14:46:42 +00:00
description : Uint8Array | undefined ,
2021-06-02 00:24:28 +00:00
secretParams : string
) : string | undefined {
const clientZkGroupCipher = getClientZkGroupCipher ( secretParams ) ;
2021-06-22 14:46:42 +00:00
if ( ! description || ! description . length ) {
2021-06-02 00:24:28 +00:00
return undefined ;
}
2021-06-22 14:46:42 +00:00
const blob = Proto . GroupAttributeBlob . decode (
decryptGroupBlob ( clientZkGroupCipher , description )
2021-06-02 00:24:28 +00:00
) ;
if ( blob && blob . content === 'descriptionText' ) {
2022-06-17 22:33:46 +00:00
return dropNull ( blob . descriptionText ) ;
2021-06-02 00:24:28 +00:00
}
return undefined ;
}
2021-06-22 14:46:42 +00:00
type DecryptedGroupState = {
title? : Proto.GroupAttributeBlob ;
disappearingMessagesTimer? : Proto.GroupAttributeBlob ;
accessControl ? : {
attributes : number ;
members : number ;
addFromInviteLink : number ;
} ;
version? : number ;
members? : ReadonlyArray < DecryptedMember > ;
membersPendingProfileKey? : ReadonlyArray < DecryptedMemberPendingProfileKey > ;
membersPendingAdminApproval? : ReadonlyArray < DecryptedMemberPendingAdminApproval > ;
inviteLinkPassword? : string ;
descriptionBytes? : Proto.GroupAttributeBlob ;
avatar? : string ;
2021-07-21 17:58:39 +00:00
announcementsOnly? : boolean ;
2022-03-23 22:34:51 +00:00
membersBanned? : Array < GroupV2BannedMemberType > ;
2021-06-22 14:46:42 +00:00
} ;
2020-09-09 02:25:05 +00:00
function decryptGroupState (
2021-06-22 14:46:42 +00:00
groupState : Readonly < Proto.IGroup > ,
2020-09-09 02:25:05 +00:00
groupSecretParams : string ,
logId : string
2021-06-22 14:46:42 +00:00
) : DecryptedGroupState {
2020-09-09 02:25:05 +00:00
const clientZkGroupCipher = getClientZkGroupCipher ( groupSecretParams ) ;
2021-06-22 14:46:42 +00:00
const result : DecryptedGroupState = { } ;
2020-09-09 02:25:05 +00:00
// title
2021-06-22 14:46:42 +00:00
if ( Bytes . isNotEmpty ( groupState . title ) ) {
2020-09-09 02:25:05 +00:00
try {
2021-06-22 14:46:42 +00:00
result . title = Proto . GroupAttributeBlob . decode (
decryptGroupBlob ( clientZkGroupCipher , groupState . title )
2020-09-09 02:25:05 +00:00
) ;
} catch ( error ) {
2021-09-17 18:27:53 +00:00
log . warn (
2020-09-09 02:25:05 +00:00
` decryptGroupState/ ${ logId } : Unable to decrypt title. Clearing it. ` ,
2022-11-22 18:43:43 +00:00
Errors . toLogFormat ( error )
2020-09-09 02:25:05 +00:00
) ;
}
}
// avatar
// Note: decryption happens during application of the change, on download of the avatar
// disappearing message timer
2021-06-22 14:46:42 +00:00
if (
groupState . disappearingMessagesTimer &&
groupState . disappearingMessagesTimer . length
) {
2020-09-09 02:25:05 +00:00
try {
2021-06-22 14:46:42 +00:00
result . disappearingMessagesTimer = Proto . GroupAttributeBlob . decode (
2020-09-09 02:25:05 +00:00
decryptGroupBlob (
clientZkGroupCipher ,
2021-06-22 14:46:42 +00:00
groupState . disappearingMessagesTimer
2020-09-09 02:25:05 +00:00
)
) ;
} catch ( error ) {
2021-09-17 18:27:53 +00:00
log . warn (
2020-09-09 02:25:05 +00:00
` decryptGroupState/ ${ logId } : Unable to decrypt disappearing message timer. Clearing it. ` ,
2022-11-22 18:43:43 +00:00
Errors . toLogFormat ( error )
2020-09-09 02:25:05 +00:00
) ;
}
}
// accessControl
2021-06-22 14:46:42 +00:00
{
const { accessControl } = groupState ;
strictAssert ( accessControl , 'No accessControl field found' ) ;
2023-11-13 18:55:14 +00:00
const attributes =
accessControl . attributes ? ? Proto . AccessControl . AccessRequired . UNKNOWN ;
const members =
accessControl . members ? ? Proto . AccessControl . AccessRequired . UNKNOWN ;
const addFromInviteLink =
accessControl . addFromInviteLink ? ?
Proto . AccessControl . AccessRequired . UNKNOWN ;
2021-06-22 14:46:42 +00:00
strictAssert (
isValidAccess ( attributes ) ,
` decryptGroupState: Access control for attributes is invalid: ${ attributes } `
2020-09-09 02:25:05 +00:00
) ;
2021-06-22 14:46:42 +00:00
strictAssert (
isValidAccess ( members ) ,
` decryptGroupState: Access control for members is invalid: ${ members } `
2020-12-18 19:27:43 +00:00
) ;
2021-06-22 14:46:42 +00:00
strictAssert (
isValidLinkAccess ( addFromInviteLink ) ,
` decryptGroupState: Access control for invite link is invalid: ${ addFromInviteLink } `
2020-09-09 02:25:05 +00:00
) ;
2021-06-22 14:46:42 +00:00
result . accessControl = {
attributes ,
members ,
addFromInviteLink ,
} ;
2020-09-09 02:25:05 +00:00
}
// version
2023-11-13 18:55:14 +00:00
const version = groupState . version ? ? 0 ;
2021-06-22 14:46:42 +00:00
strictAssert (
2023-11-13 18:55:14 +00:00
isNumber ( version ) ,
` decryptGroupState: Expected version to be a number or null; it was ${ groupState . version } `
2021-06-22 14:46:42 +00:00
) ;
2023-11-13 18:55:14 +00:00
result . version = version ;
2020-09-09 02:25:05 +00:00
// members
if ( groupState . members ) {
2021-06-22 14:46:42 +00:00
result . members = compact (
groupState . members . map ( ( member : Proto.IMember ) = >
2020-09-09 02:25:05 +00:00
decryptMember ( clientZkGroupCipher , member , logId )
)
) ;
}
2020-12-18 19:27:43 +00:00
// membersPendingProfileKey
if ( groupState . membersPendingProfileKey ) {
2021-06-22 14:46:42 +00:00
result . membersPendingProfileKey = compact (
2020-12-18 19:27:43 +00:00
groupState . membersPendingProfileKey . map (
2021-06-22 14:46:42 +00:00
( member : Proto.IMemberPendingProfileKey ) = >
2020-12-18 19:27:43 +00:00
decryptMemberPendingProfileKey ( clientZkGroupCipher , member , logId )
)
) ;
}
// membersPendingAdminApproval
if ( groupState . membersPendingAdminApproval ) {
2021-06-22 14:46:42 +00:00
result . membersPendingAdminApproval = compact (
2020-12-18 19:27:43 +00:00
groupState . membersPendingAdminApproval . map (
2021-06-22 14:46:42 +00:00
( member : Proto.IMemberPendingAdminApproval ) = >
2020-12-18 19:27:43 +00:00
decryptMemberPendingAdminApproval ( clientZkGroupCipher , member , logId )
2020-09-09 02:25:05 +00:00
)
) ;
}
2020-12-18 19:27:43 +00:00
// inviteLinkPassword
2021-06-22 14:46:42 +00:00
if ( Bytes . isNotEmpty ( groupState . inviteLinkPassword ) ) {
result . inviteLinkPassword = Bytes . toBase64 ( groupState . inviteLinkPassword ) ;
2020-12-18 19:27:43 +00:00
}
2021-06-02 00:24:28 +00:00
// descriptionBytes
2021-06-22 14:46:42 +00:00
if ( Bytes . isNotEmpty ( groupState . descriptionBytes ) ) {
2021-06-02 00:24:28 +00:00
try {
2021-06-22 14:46:42 +00:00
result . descriptionBytes = Proto . GroupAttributeBlob . decode (
decryptGroupBlob ( clientZkGroupCipher , groupState . descriptionBytes )
2021-06-02 00:24:28 +00:00
) ;
} catch ( error ) {
2021-09-17 18:27:53 +00:00
log . warn (
2021-06-02 00:24:28 +00:00
` decryptGroupState/ ${ logId } : Unable to decrypt descriptionBytes. Clearing it. ` ,
2022-11-22 18:43:43 +00:00
Errors . toLogFormat ( error )
2021-06-02 00:24:28 +00:00
) ;
}
}
2021-07-21 17:58:39 +00:00
// announcementsOnly
const { announcementsOnly } = groupState ;
result . announcementsOnly = Boolean ( announcementsOnly ) ;
2022-03-15 01:32:07 +00:00
// membersBanned
const { membersBanned } = groupState ;
if ( membersBanned && membersBanned . length > 0 ) {
result . membersBanned = membersBanned
. map ( item = > {
if ( ! item . userId ) {
log . warn (
` decryptGroupState/ ${ logId } : membersBanned had a blank entry `
) ;
return null ;
}
2023-08-16 20:54:39 +00:00
const serviceId = decryptServiceId ( clientZkGroupCipher , item . userId ) ;
2022-03-23 22:34:51 +00:00
const timestamp = item . timestamp ? . toNumber ( ) ? ? 0 ;
2023-08-16 20:54:39 +00:00
return { serviceId , timestamp } ;
2022-03-15 01:32:07 +00:00
} )
. filter ( isNotNil ) ;
} else {
result . membersBanned = [ ] ;
}
2021-06-22 14:46:42 +00:00
result . avatar = dropNull ( groupState . avatar ) ;
return result ;
2020-09-09 02:25:05 +00:00
}
2021-06-22 14:46:42 +00:00
type DecryptedMember = Readonly < {
2023-08-10 16:43:33 +00:00
userId : AciString ;
2021-06-22 14:46:42 +00:00
profileKey : Uint8Array ;
role : Proto.Member.Role ;
2023-08-31 21:05:50 +00:00
joinedAtVersion : number ;
2021-06-22 14:46:42 +00:00
} > ;
2020-09-09 02:25:05 +00:00
function decryptMember (
clientZkGroupCipher : ClientZkGroupCipher ,
2021-06-22 14:46:42 +00:00
member : Readonly < Proto.IMember > ,
2020-09-09 02:25:05 +00:00
logId : string
2021-06-22 14:46:42 +00:00
) : DecryptedMember | undefined {
2020-09-09 02:25:05 +00:00
// userId
2021-06-22 14:46:42 +00:00
strictAssert (
Bytes . isNotEmpty ( member . userId ) ,
'decryptMember: Member had missing userId'
) ;
2020-09-09 02:25:05 +00:00
2023-08-10 16:43:33 +00:00
let userId : AciString ;
2021-06-22 14:46:42 +00:00
try {
2023-08-16 20:54:39 +00:00
userId = decryptAci ( clientZkGroupCipher , member . userId ) ;
2021-06-22 14:46:42 +00:00
} catch ( error ) {
2021-09-17 18:27:53 +00:00
log . warn (
2021-06-22 14:46:42 +00:00
` decryptMember/ ${ logId } : Unable to decrypt member userid. Dropping member. ` ,
2022-11-22 18:43:43 +00:00
Errors . toLogFormat ( error )
2021-06-22 14:46:42 +00:00
) ;
return undefined ;
}
2020-09-09 02:25:05 +00:00
// profileKey
2021-06-22 14:46:42 +00:00
strictAssert (
Bytes . isNotEmpty ( member . profileKey ) ,
'decryptMember: Member had missing profileKey'
) ;
const profileKey = decryptProfileKey (
clientZkGroupCipher ,
member . profileKey ,
2023-08-10 16:43:33 +00:00
userId
2021-06-22 14:46:42 +00:00
) ;
2020-09-09 02:25:05 +00:00
2021-06-22 14:46:42 +00:00
if ( ! isValidProfileKey ( profileKey ) ) {
throw new Error ( 'decryptMember: Member had invalid profileKey' ) ;
2020-09-09 02:25:05 +00:00
}
// role
2021-06-22 14:46:42 +00:00
const role = dropNull ( member . role ) ;
if ( ! isValidRole ( role ) ) {
2020-12-18 19:27:43 +00:00
throw new Error ( ` decryptMember: Member had invalid role ${ member . role } ` ) ;
2020-09-09 02:25:05 +00:00
}
2021-06-22 14:46:42 +00:00
return {
userId ,
profileKey ,
role ,
2023-08-31 21:05:50 +00:00
joinedAtVersion : dropNull ( member . joinedAtVersion ) ? ? 0 ,
2021-06-22 14:46:42 +00:00
} ;
2020-09-09 02:25:05 +00:00
}
2021-06-22 14:46:42 +00:00
type DecryptedMemberPendingProfileKey = {
2023-08-10 16:43:33 +00:00
addedByUserId : AciString ;
2021-06-22 14:46:42 +00:00
timestamp : number ;
member : {
2023-08-10 16:43:33 +00:00
userId : ServiceIdString ;
2021-06-22 14:46:42 +00:00
role? : Proto.Member.Role ;
} ;
} ;
2020-12-18 19:27:43 +00:00
function decryptMemberPendingProfileKey (
2020-09-09 02:25:05 +00:00
clientZkGroupCipher : ClientZkGroupCipher ,
2021-06-22 14:46:42 +00:00
member : Readonly < Proto.IMemberPendingProfileKey > ,
2020-09-09 02:25:05 +00:00
logId : string
2021-06-22 14:46:42 +00:00
) : DecryptedMemberPendingProfileKey | undefined {
2020-09-09 02:25:05 +00:00
// addedByUserId
2021-06-22 14:46:42 +00:00
strictAssert (
Bytes . isNotEmpty ( member . addedByUserId ) ,
'decryptMemberPendingProfileKey: Member had missing addedByUserId'
) ;
2020-09-09 02:25:05 +00:00
2023-08-10 16:43:33 +00:00
let addedByUserId : AciString ;
2021-06-22 14:46:42 +00:00
try {
2023-08-16 20:54:39 +00:00
addedByUserId = decryptAci ( clientZkGroupCipher , member . addedByUserId ) ;
2021-06-22 14:46:42 +00:00
} catch ( error ) {
2021-09-17 18:27:53 +00:00
log . warn (
2021-06-22 14:46:42 +00:00
` decryptMemberPendingProfileKey/ ${ logId } : Unable to decrypt pending member addedByUserId. Dropping member. ` ,
2022-11-22 18:43:43 +00:00
Errors . toLogFormat ( error )
2020-09-09 02:25:05 +00:00
) ;
2021-06-22 14:46:42 +00:00
return undefined ;
}
2020-09-09 02:25:05 +00:00
// timestamp
2021-06-22 14:46:42 +00:00
const timestamp = normalizeTimestamp ( member . timestamp ) ;
2020-09-09 02:25:05 +00:00
if ( ! member . member ) {
2021-09-17 18:27:53 +00:00
log . warn (
2020-12-18 19:27:43 +00:00
` decryptMemberPendingProfileKey/ ${ logId } : Dropping pending member due to missing member details `
2020-09-09 02:25:05 +00:00
) ;
2021-06-22 14:46:42 +00:00
return undefined ;
2020-09-09 02:25:05 +00:00
}
2021-06-22 14:46:42 +00:00
const { userId , profileKey } = member . member ;
2023-06-05 16:22:27 +00:00
strictAssert (
Bytes . isEmpty ( profileKey ) ,
'decryptMemberPendingProfileKey: member has profileKey'
) ;
2020-09-09 02:25:05 +00:00
// userId
2021-06-22 14:46:42 +00:00
strictAssert (
Bytes . isNotEmpty ( userId ) ,
'decryptMemberPendingProfileKey: Member had missing member.userId'
) ;
2020-09-09 02:25:05 +00:00
2023-08-10 16:43:33 +00:00
let decryptedUserId : ServiceIdString ;
2021-06-22 14:46:42 +00:00
try {
2023-08-16 20:54:39 +00:00
decryptedUserId = decryptServiceId ( clientZkGroupCipher , userId ) ;
2021-06-22 14:46:42 +00:00
} catch ( error ) {
2021-09-17 18:27:53 +00:00
log . warn (
2021-06-22 14:46:42 +00:00
` decryptMemberPendingProfileKey/ ${ logId } : Unable to decrypt pending member userId. Dropping member. ` ,
2022-11-22 18:43:43 +00:00
Errors . toLogFormat ( error )
2021-06-22 14:46:42 +00:00
) ;
return undefined ;
}
2020-09-09 02:25:05 +00:00
// role
2021-06-22 14:46:42 +00:00
const role = dropNull ( member . member . role ) ;
2020-09-09 02:25:05 +00:00
2021-06-22 14:46:42 +00:00
strictAssert (
isValidRole ( role ) ,
` decryptMemberPendingProfileKey: Member had invalid role ${ role } `
) ;
return {
addedByUserId ,
timestamp ,
member : {
userId : decryptedUserId ,
role ,
} ,
} ;
2020-09-09 02:25:05 +00:00
}
2020-11-13 19:57:55 +00:00
2021-06-22 14:46:42 +00:00
type DecryptedMemberPendingAdminApproval = {
2023-08-10 16:43:33 +00:00
userId : AciString ;
2021-06-22 14:46:42 +00:00
profileKey? : Uint8Array ;
timestamp : number ;
} ;
2020-12-18 19:27:43 +00:00
function decryptMemberPendingAdminApproval (
clientZkGroupCipher : ClientZkGroupCipher ,
2021-06-22 14:46:42 +00:00
member : Readonly < Proto.IMemberPendingAdminApproval > ,
2020-12-18 19:27:43 +00:00
logId : string
2021-06-22 14:46:42 +00:00
) : DecryptedMemberPendingAdminApproval | undefined {
2020-12-18 19:27:43 +00:00
// timestamp
2021-06-22 14:46:42 +00:00
const timestamp = normalizeTimestamp ( member . timestamp ) ;
2020-12-18 19:27:43 +00:00
const { userId , profileKey } = member ;
// userId
2021-06-22 14:46:42 +00:00
strictAssert (
Bytes . isNotEmpty ( userId ) ,
'decryptMemberPendingAdminApproval: Missing userId'
) ;
2020-12-18 19:27:43 +00:00
2023-08-10 16:43:33 +00:00
let decryptedUserId : AciString ;
2021-06-22 14:46:42 +00:00
try {
2023-08-16 20:54:39 +00:00
decryptedUserId = decryptAci ( clientZkGroupCipher , userId ) ;
2021-06-22 14:46:42 +00:00
} catch ( error ) {
2021-09-17 18:27:53 +00:00
log . warn (
2021-06-22 14:46:42 +00:00
` decryptMemberPendingAdminApproval/ ${ logId } : Unable to decrypt pending member userId. Dropping member. ` ,
2022-11-22 18:43:43 +00:00
Errors . toLogFormat ( error )
2020-12-18 19:27:43 +00:00
) ;
2021-06-22 14:46:42 +00:00
return undefined ;
}
2020-12-18 19:27:43 +00:00
// profileKey
2021-06-22 14:46:42 +00:00
let decryptedProfileKey : Uint8Array | undefined ;
if ( Bytes . isNotEmpty ( profileKey ) ) {
2020-12-18 19:27:43 +00:00
try {
2021-06-22 14:46:42 +00:00
decryptedProfileKey = decryptProfileKey (
2020-12-18 19:27:43 +00:00
clientZkGroupCipher ,
2021-06-22 14:46:42 +00:00
profileKey ,
2023-08-10 16:43:33 +00:00
decryptedUserId
2020-12-18 19:27:43 +00:00
) ;
} catch ( error ) {
2021-09-17 18:27:53 +00:00
log . warn (
2020-12-18 19:27:43 +00:00
` decryptMemberPendingAdminApproval/ ${ logId } : Unable to decrypt profileKey. Dropping profileKey. ` ,
2022-11-22 18:43:43 +00:00
Errors . toLogFormat ( error )
2020-12-18 19:27:43 +00:00
) ;
}
2021-06-22 14:46:42 +00:00
if ( ! isValidProfileKey ( decryptedProfileKey ) ) {
2021-09-17 18:27:53 +00:00
log . warn (
2020-12-18 19:27:43 +00:00
` decryptMemberPendingAdminApproval/ ${ logId } : Dropping profileKey, since it was invalid `
) ;
2021-06-22 14:46:42 +00:00
decryptedProfileKey = undefined ;
2020-12-18 19:27:43 +00:00
}
}
2021-06-22 14:46:42 +00:00
return {
timestamp ,
userId : decryptedUserId ,
profileKey : decryptedProfileKey ,
} ;
2020-12-18 19:27:43 +00:00
}
2020-11-13 19:57:55 +00:00
export function getMembershipList (
conversationId : string
2023-08-16 20:54:39 +00:00
) : Array < { aci : AciString ; uuidCiphertext : Uint8Array } > {
2020-11-13 19:57:55 +00:00
const conversation = window . ConversationController . get ( conversationId ) ;
if ( ! conversation ) {
throw new Error ( 'getMembershipList: cannot find conversation' ) ;
}
const secretParams = conversation . get ( 'secretParams' ) ;
if ( ! secretParams ) {
throw new Error ( 'getMembershipList: no secretParams' ) ;
}
const clientZkGroupCipher = getClientZkGroupCipher ( secretParams ) ;
return conversation . getMembers ( ) . map ( member = > {
2023-08-16 20:54:39 +00:00
const aci = member . getCheckedAci ( 'getMembershipList: member has no aci' ) ;
2020-11-13 19:57:55 +00:00
2023-08-16 20:54:39 +00:00
const uuidCiphertext = encryptServiceId ( clientZkGroupCipher , aci ) ;
return { aci , uuidCiphertext } ;
2020-11-13 19:57:55 +00:00
} ) ;
}