fix: make pushChangesToGroupSwarm take an extraStoreRequest

pull/3052/head
Audric Ackermann 11 months ago
parent 9b124d384e
commit cd122c7252

@ -24,6 +24,7 @@ import {
SessionButtonType, SessionButtonType,
} from './basic/SessionButton'; } from './basic/SessionButton';
import { SessionRadio } from './basic/SessionRadio'; import { SessionRadio } from './basic/SessionRadio';
import { hasClosedGroupV2QAButtons } from '../shared/env_vars';
const AvatarContainer = styled.div` const AvatarContainer = styled.div`
position: relative; position: relative;
@ -215,6 +216,9 @@ const ResendPromoteButton = ({
pubkey: PubkeyType; pubkey: PubkeyType;
groupPk: GroupPubkeyType; groupPk: GroupPubkeyType;
}) => { }) => {
if (!hasClosedGroupV2QAButtons()) {
return null;
}
return ( return (
<SessionButton <SessionButton
dataTestId={'resend-promote-button'} dataTestId={'resend-promote-button'}

@ -139,6 +139,7 @@ import {
UserGroupsWrapperActions, UserGroupsWrapperActions,
} from '../webworker/workers/browser/libsession_worker_interface'; } from '../webworker/workers/browser/libsession_worker_interface';
import { markAttributesAsReadIfNeeded } from './messageFactory'; import { markAttributesAsReadIfNeeded } from './messageFactory';
import { StoreGroupRequestFactory } from '../session/apis/snode_api/factories/StoreGroupRequestFactory';
type InMemoryConvoInfos = { type InMemoryConvoInfos = {
mentionedUs: boolean; mentionedUs: boolean;
@ -1122,17 +1123,20 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
updatedExpirationSeconds: expireUpdate.expireTimer, updatedExpirationSeconds: expireUpdate.expireTimer,
}); });
const extraStoreRequests = await StoreGroupRequestFactory.makeGroupMessageSubRequest(
[v2groupMessage],
group
);
await GroupSync.pushChangesToGroupSwarmIfNeeded({ await GroupSync.pushChangesToGroupSwarmIfNeeded({
groupPk: this.id, groupPk: this.id,
revokeSubRequest: null, revokeSubRequest: null,
unrevokeSubRequest: null, unrevokeSubRequest: null,
deleteAllMessagesSubRequest: null, deleteAllMessagesSubRequest: null,
encryptedSupplementKeys: [], supplementalKeysSubRequest: [],
}); extraStoreRequests,
await GroupSync.storeGroupUpdateMessages({
groupPk: this.id,
updateMessages: [v2groupMessage],
}); });
await GroupSync.queueNewJobIfNeeded(this.id); await GroupSync.queueNewJobIfNeeded(this.id);
return true; return true;
} }

@ -355,7 +355,6 @@ async function handleGroupMemberLeftMessage({
memberLeft: author, memberLeft: author,
}) })
); );
} }
async function handleGroupUpdateMemberLeftNotificationMessage({ async function handleGroupUpdateMemberLeftNotificationMessage({
@ -491,7 +490,6 @@ async function handleGroupUpdateInviteResponseMessage({
} }
window.inboxStore.dispatch(groupInfoActions.inviteResponseReceived({ groupPk, member: author })); window.inboxStore.dispatch(groupInfoActions.inviteResponseReceived({ groupPk, member: author }));
} }
async function handleGroupUpdatePromoteMessage({ async function handleGroupUpdatePromoteMessage({
@ -531,7 +529,6 @@ async function handleGroupUpdatePromoteMessage({
secret: groupKeypair.privateKey, secret: groupKeypair.privateKey,
}) })
); );
} }
async function handle1o1GroupUpdateMessage( async function handle1o1GroupUpdateMessage(

@ -25,6 +25,7 @@ import {
WithSignature, WithSignature,
WithTimestamp, WithTimestamp,
} from './types'; } from './types';
import { TTL_DEFAULT } from '../../constants';
type WithMaxSize = { max_size?: number }; type WithMaxSize = { max_size?: number };
export type WithShortenOrExtend = { shortenOrExtend: 'shorten' | 'extend' | '' }; export type WithShortenOrExtend = { shortenOrExtend: 'shorten' | 'extend' | '' };
@ -818,32 +819,29 @@ export class StoreGroupMessageSubRequest extends SnodeAPISubRequest {
} }
} }
export class StoreGroupConfigSubRequest extends SnodeAPISubRequest { abstract class StoreGroupConfigSubRequest<
T extends SnodeNamespacesGroupConfig | SnodeNamespaces.ClosedGroupRevokedRetrievableMessages,
> extends SnodeAPISubRequest {
public method = 'store' as const; public method = 'store' as const;
public readonly namespace: public readonly namespace: T;
| SnodeNamespacesGroupConfig
| SnodeNamespaces.ClosedGroupRevokedRetrievableMessages;
public readonly destination: GroupPubkeyType; public readonly destination: GroupPubkeyType;
public readonly ttlMs: number; public readonly ttlMs: number;
public readonly encryptedData: Uint8Array; public readonly encryptedData: Uint8Array;
// this is mandatory for a group config store, if it is null, we throw
public readonly secretKey: Uint8Array | null; public readonly secretKey: Uint8Array | null;
public readonly authData: Uint8Array | null;
constructor( constructor(
args: WithGroupPubkey & { args: WithGroupPubkey & {
namespace: SnodeNamespacesGroupConfig | SnodeNamespaces.ClosedGroupRevokedRetrievableMessages; namespace: T;
ttlMs: number;
encryptedData: Uint8Array; encryptedData: Uint8Array;
authData: Uint8Array | null;
secretKey: Uint8Array | null; secretKey: Uint8Array | null;
} }
) { ) {
super(); super();
this.namespace = args.namespace; this.namespace = args.namespace;
this.destination = args.groupPk; this.destination = args.groupPk;
this.ttlMs = args.ttlMs; this.ttlMs = TTL_DEFAULT.CONFIG_MESSAGE;
this.encryptedData = args.encryptedData; this.encryptedData = args.encryptedData;
this.authData = args.authData;
this.secretKey = args.secretKey; this.secretKey = args.secretKey;
if (isEmpty(this.encryptedData)) { if (isEmpty(this.encryptedData)) {
@ -852,13 +850,8 @@ export class StoreGroupConfigSubRequest extends SnodeAPISubRequest {
if (!PubKey.is03Pubkey(this.destination)) { if (!PubKey.is03Pubkey(this.destination)) {
throw new Error('StoreGroupConfigSubRequest: groupconfig namespace required a 03 pubkey'); throw new Error('StoreGroupConfigSubRequest: groupconfig namespace required a 03 pubkey');
} }
if (isEmpty(this.secretKey) && isEmpty(this.authData)) { if (isEmpty(this.secretKey)) {
throw new Error('StoreGroupConfigSubRequest needs either authData or secretKey to be set'); throw new Error('StoreGroupConfigSubRequest needs secretKey to be set');
}
if (SnodeNamespace.isGroupConfigNamespace(this.namespace) && isEmpty(this.secretKey)) {
throw new Error(
`StoreGroupConfigSubRequest: groupconfig namespace [${this.namespace}] requires an adminSecretKey`
);
} }
} }
@ -872,7 +865,7 @@ export class StoreGroupConfigSubRequest extends SnodeAPISubRequest {
const signDetails = await SnodeGroupSignature.getSnodeGroupSignature({ const signDetails = await SnodeGroupSignature.getSnodeGroupSignature({
method: this.method, method: this.method,
namespace: this.namespace, namespace: this.namespace,
group: { authData: this.authData, pubkeyHex: this.destination, secretKey: this.secretKey }, group: { authData: null, pubkeyHex: this.destination, secretKey: this.secretKey },
}); });
if (!signDetails) { if (!signDetails) {
@ -897,6 +890,36 @@ export class StoreGroupConfigSubRequest extends SnodeAPISubRequest {
} }
} }
export class StoreGroupInfoSubRequest extends StoreGroupConfigSubRequest<SnodeNamespaces.ClosedGroupInfo> {
constructor(
args: Omit<ConstructorParameters<typeof StoreGroupConfigSubRequest>[0], 'namespace'>
) {
super({ ...args, namespace: SnodeNamespaces.ClosedGroupInfo });
}
}
export class StoreGroupMembersSubRequest extends StoreGroupConfigSubRequest<SnodeNamespaces.ClosedGroupMembers> {
constructor(
args: Omit<ConstructorParameters<typeof StoreGroupConfigSubRequest>[0], 'namespace'>
) {
super({ ...args, namespace: SnodeNamespaces.ClosedGroupMembers });
}
}
export class StoreGroupKeysSubRequest extends StoreGroupConfigSubRequest<SnodeNamespaces.ClosedGroupKeys> {
constructor(
args: Omit<ConstructorParameters<typeof StoreGroupConfigSubRequest>[0], 'namespace'>
) {
super({ ...args, namespace: SnodeNamespaces.ClosedGroupKeys });
}
}
export class StoreGroupRevokedRetrievableSubRequest extends StoreGroupConfigSubRequest<SnodeNamespaces.ClosedGroupRevokedRetrievableMessages> {
constructor(
args: Omit<ConstructorParameters<typeof StoreGroupConfigSubRequest>[0], 'namespace'>
) {
super({ ...args, namespace: SnodeNamespaces.ClosedGroupRevokedRetrievableMessages });
}
}
export class StoreUserConfigSubRequest extends SnodeAPISubRequest { export class StoreUserConfigSubRequest extends SnodeAPISubRequest {
public method = 'store' as const; public method = 'store' as const;
public readonly namespace: SnodeNamespacesUserConfig; public readonly namespace: SnodeNamespacesUserConfig;
@ -1136,8 +1159,11 @@ export type RawSnodeSubRequests =
| RetrieveLegacyClosedGroupSubRequest | RetrieveLegacyClosedGroupSubRequest
| RetrieveUserSubRequest | RetrieveUserSubRequest
| RetrieveGroupSubRequest | RetrieveGroupSubRequest
| StoreGroupConfigSubRequest | StoreGroupInfoSubRequest
| StoreGroupMembersSubRequest
| StoreGroupKeysSubRequest
| StoreGroupMessageSubRequest | StoreGroupMessageSubRequest
| StoreGroupRevokedRetrievableSubRequest
| StoreUserConfigSubRequest | StoreUserConfigSubRequest
| SwarmForSubRequest | SwarmForSubRequest
| OnsResolveSubRequest | OnsResolveSubRequest
@ -1159,13 +1185,16 @@ export type BuiltSnodeSubRequests =
| ReturnType<RetrieveLegacyClosedGroupSubRequest['build']> | ReturnType<RetrieveLegacyClosedGroupSubRequest['build']>
| AwaitedReturn<RetrieveUserSubRequest['buildAndSignParameters']> | AwaitedReturn<RetrieveUserSubRequest['buildAndSignParameters']>
| AwaitedReturn<RetrieveGroupSubRequest['buildAndSignParameters']> | AwaitedReturn<RetrieveGroupSubRequest['buildAndSignParameters']>
| AwaitedReturn<StoreGroupConfigSubRequest['buildAndSignParameters']> | AwaitedReturn<StoreGroupInfoSubRequest['buildAndSignParameters']>
| AwaitedReturn<StoreGroupMembersSubRequest['buildAndSignParameters']>
| AwaitedReturn<StoreGroupKeysSubRequest['buildAndSignParameters']>
| AwaitedReturn<StoreGroupMessageSubRequest['buildAndSignParameters']> | AwaitedReturn<StoreGroupMessageSubRequest['buildAndSignParameters']>
| AwaitedReturn<StoreGroupRevokedRetrievableSubRequest['buildAndSignParameters']>
| AwaitedReturn<StoreUserConfigSubRequest['buildAndSignParameters']> | AwaitedReturn<StoreUserConfigSubRequest['buildAndSignParameters']>
| ReturnType<SwarmForSubRequest['build']> | AwaitedReturn<SwarmForSubRequest['build']>
| ReturnType<OnsResolveSubRequest['build']> | AwaitedReturn<OnsResolveSubRequest['build']>
| ReturnType<GetServiceNodesSubRequest['build']> | AwaitedReturn<GetServiceNodesSubRequest['build']>
| ReturnType<NetworkTimeSubRequest['build']> | AwaitedReturn<NetworkTimeSubRequest['build']>
| AwaitedReturn<DeleteHashesFromGroupNodeSubRequest['buildAndSignParameters']> | AwaitedReturn<DeleteHashesFromGroupNodeSubRequest['buildAndSignParameters']>
| AwaitedReturn<DeleteHashesFromUserNodeSubRequest['buildAndSignParameters']> | AwaitedReturn<DeleteHashesFromUserNodeSubRequest['buildAndSignParameters']>
| AwaitedReturn<DeleteAllFromUserNodeSubRequest['buildAndSignParameters']> | AwaitedReturn<DeleteAllFromUserNodeSubRequest['buildAndSignParameters']>

@ -0,0 +1,35 @@
import { UserGroupsGet } from 'libsession_util_nodejs';
import { isEmpty } from 'lodash';
import { ed25519Str } from '../../../utils/String';
import { DeleteHashesFromGroupNodeSubRequest } from '../SnodeRequestTypes';
function makeGroupHashesToDeleteSubRequest({
allOldHashes,
group,
}: {
group: Pick<UserGroupsGet, 'secretKey' | 'pubkeyHex'>;
allOldHashes: Set<string>;
}) {
const groupPk = group.pubkeyHex;
const allOldHashesArray = [...allOldHashes];
if (allOldHashesArray.length) {
if (!group.secretKey || isEmpty(group.secretKey)) {
window.log.debug(
`makeGroupHashesToDeleteSubRequest: ${ed25519Str(groupPk)}: allOldHashesArray not empty but we do not have the secretKey`
);
throw new Error(
'makeGroupHashesToDeleteSubRequest: allOldHashesArray not empty but we do not have the secretKey'
);
}
return new DeleteHashesFromGroupNodeSubRequest({
messagesHashes: [...allOldHashes],
groupPk,
secretKey: group.secretKey,
});
}
return null;
}
export const DeleteGroupHashesFactory = { makeGroupHashesToDeleteSubRequest };

@ -0,0 +1,184 @@
import { UserGroupsGet } from 'libsession_util_nodejs';
import { compact, isEmpty } from 'lodash';
import { SignalService } from '../../../../protobuf';
import { MetaGroupWrapperActions } from '../../../../webworker/workers/browser/libsession_worker_interface';
import { GroupUpdateInfoChangeMessage } from '../../../messages/outgoing/controlMessage/group_v2/to_group/GroupUpdateInfoChangeMessage';
import { GroupUpdateMemberChangeMessage } from '../../../messages/outgoing/controlMessage/group_v2/to_group/GroupUpdateMemberChangeMessage';
import { MessageSender } from '../../../sending';
import { ed25519Str } from '../../../utils/String';
import { PendingChangesForGroup } from '../../../utils/libsession/libsession_utils';
import {
StoreGroupExtraData,
StoreGroupInfoSubRequest,
StoreGroupKeysSubRequest,
StoreGroupMembersSubRequest,
StoreGroupMessageSubRequest,
} from '../SnodeRequestTypes';
import { SnodeNamespaces } from '../namespaces';
export type StoreMessageToSubRequestType =
| GroupUpdateMemberChangeMessage
| GroupUpdateInfoChangeMessage;
async function makeGroupMessageSubRequest(
updateMessages: Array<StoreMessageToSubRequestType | null>,
group: Pick<UserGroupsGet, 'authData' | 'secretKey'>
) {
const compactedMessages = compact(updateMessages);
if (isEmpty(compactedMessages)) {
return [];
}
const groupPk = compactedMessages[0].destination;
const allForSameDestination = compactedMessages.every(m => m.destination === groupPk);
if (!allForSameDestination) {
throw new Error('makeGroupMessageSubRequest: not all messages are for the same destination');
}
const messagesToEncrypt: Array<StoreGroupExtraData> = compactedMessages.map(updateMessage => {
const wrapped = MessageSender.wrapContentIntoEnvelope(
SignalService.Envelope.Type.SESSION_MESSAGE,
undefined,
updateMessage.createAtNetworkTimestamp, // message is signed with this timestmap
updateMessage.plainTextBuffer()
);
return {
namespace: SnodeNamespaces.ClosedGroupMessages,
pubkey: updateMessage.destination,
ttl: updateMessage.ttl(),
networkTimestamp: updateMessage.createAtNetworkTimestamp,
data: SignalService.Envelope.encode(wrapped).finish(),
dbMessageIdentifier: updateMessage.identifier,
};
});
const encryptedContent = messagesToEncrypt.length
? await MetaGroupWrapperActions.encryptMessages(
groupPk,
messagesToEncrypt.map(m => m.data)
)
: [];
if (encryptedContent.length !== messagesToEncrypt.length) {
throw new Error(
'makeGroupMessageSubRequest: MetaGroupWrapperActions.encryptMessages did not return the right count of items'
);
}
const updateMessagesEncrypted = messagesToEncrypt.map((requestDetails, index) => ({
...requestDetails,
data: encryptedContent[index],
}));
const updateMessagesRequests = updateMessagesEncrypted.map(m => {
return new StoreGroupMessageSubRequest({
encryptedData: m.data,
groupPk,
ttlMs: m.ttl,
dbMessageIdentifier: m.dbMessageIdentifier,
...group,
createdAtNetworkTimestamp: m.networkTimestamp,
});
});
return updateMessagesRequests;
}
function makeStoreGroupKeysSubRequest({
encryptedSupplementKeys,
group,
}: {
group: Pick<UserGroupsGet, 'secretKey' | 'pubkeyHex'>;
encryptedSupplementKeys: Array<Uint8Array>;
}) {
const groupPk = group.pubkeyHex;
if (!encryptedSupplementKeys.length) {
return [];
}
// supplementalKeys are already encrypted, but we still need the secretKey to sign the request
if (!group.secretKey || isEmpty(group.secretKey)) {
window.log.debug(
`pushChangesToGroupSwarmIfNeeded: ${ed25519Str(groupPk)}: keysEncryptedmessage not empty but we do not have the secretKey`
);
throw new Error(
'pushChangesToGroupSwarmIfNeeded: keysEncryptedmessage not empty but we do not have the secretKey'
);
}
return encryptedSupplementKeys.map(encryptedData => {
return new StoreGroupKeysSubRequest({
encryptedData,
groupPk,
secretKey: group.secretKey,
});
});
}
function makeStoreGroupConfigSubRequest({
group,
pendingConfigData,
}: {
group: Pick<UserGroupsGet, 'secretKey' | 'pubkeyHex'>;
pendingConfigData: Array<PendingChangesForGroup>;
}) {
if (!pendingConfigData.length) {
return [];
}
const groupPk = group.pubkeyHex;
if (!group.secretKey || isEmpty(group.secretKey)) {
window.log.debug(
`pushChangesToGroupSwarmIfNeeded: ${ed25519Str(groupPk)}: pendingConfigMsgs not empty but we do not have the secretKey`
);
throw new Error(
'pushChangesToGroupSwarmIfNeeded: pendingConfigMsgs not empty but we do not have the secretKey'
);
}
const groupInfoSubRequests = compact(
pendingConfigData.map(m =>
m.namespace === SnodeNamespaces.ClosedGroupInfo
? new StoreGroupInfoSubRequest({
encryptedData: m.ciphertext,
groupPk,
secretKey: group.secretKey,
})
: null
)
);
const groupMembersSubRequests = compact(
pendingConfigData.map(m =>
m.namespace === SnodeNamespaces.ClosedGroupMembers
? new StoreGroupMembersSubRequest({
encryptedData: m.ciphertext,
groupPk,
secretKey: group.secretKey,
})
: null
)
);
const groupKeysSubRequests = compact(
pendingConfigData.map(m =>
m.namespace === SnodeNamespaces.ClosedGroupKeys
? new StoreGroupKeysSubRequest({
encryptedData: m.ciphertext,
groupPk,
secretKey: group.secretKey,
})
: null
)
);
// we want to store first the keys (as the info and members might already be encrypted with them)
return [...groupKeysSubRequests, ...groupInfoSubRequests, ...groupMembersSubRequests];
}
export const StoreGroupRequestFactory = {
makeGroupMessageSubRequest,
makeStoreGroupConfigSubRequest,
makeStoreGroupKeysSubRequest,
};

@ -329,8 +329,9 @@ class ConvoController {
groupPk, groupPk,
revokeSubRequest: null, revokeSubRequest: null,
unrevokeSubRequest: null, unrevokeSubRequest: null,
encryptedSupplementKeys: [], supplementalKeysSubRequest: [],
deleteAllMessagesSubRequest, deleteAllMessagesSubRequest,
extraStoreRequests: [],
}); });
if (lastPushResult !== RunJobResult.Success) { if (lastPushResult !== RunJobResult.Success) {
throw new Error(`Failed to destroyGroupDetails for pk ${ed25519Str(groupPk)}`); throw new Error(`Failed to destroyGroupDetails for pk ${ed25519Str(groupPk)}`);

@ -30,8 +30,11 @@ import {
RetrieveGroupSubRequest, RetrieveGroupSubRequest,
RetrieveLegacyClosedGroupSubRequest, RetrieveLegacyClosedGroupSubRequest,
RetrieveUserSubRequest, RetrieveUserSubRequest,
StoreGroupConfigSubRequest, StoreGroupInfoSubRequest,
StoreGroupKeysSubRequest,
StoreGroupMembersSubRequest,
StoreGroupMessageSubRequest, StoreGroupMessageSubRequest,
StoreGroupRevokedRetrievableSubRequest,
StoreLegacyGroupMessageSubRequest, StoreLegacyGroupMessageSubRequest,
StoreUserConfigSubRequest, StoreUserConfigSubRequest,
StoreUserMessageSubRequest, StoreUserMessageSubRequest,
@ -89,7 +92,12 @@ type StoreRequest05 =
| StoreUserConfigSubRequest | StoreUserConfigSubRequest
| StoreUserMessageSubRequest | StoreUserMessageSubRequest
| StoreLegacyGroupMessageSubRequest; | StoreLegacyGroupMessageSubRequest;
type StoreRequest03 = StoreGroupConfigSubRequest | StoreGroupMessageSubRequest; type StoreRequest03 =
| StoreGroupInfoSubRequest
| StoreGroupMembersSubRequest
| StoreGroupKeysSubRequest
| StoreGroupRevokedRetrievableSubRequest
| StoreGroupMessageSubRequest;
type PubkeyToRequestType<T extends GroupPubkeyType | PubkeyType> = T extends PubkeyType type PubkeyToRequestType<T extends GroupPubkeyType | PubkeyType> = T extends PubkeyType
? StoreRequest05 ? StoreRequest05
@ -366,7 +374,10 @@ async function signSubRequests(
p instanceof DeleteHashesFromUserNodeSubRequest || p instanceof DeleteHashesFromUserNodeSubRequest ||
p instanceof DeleteHashesFromGroupNodeSubRequest || p instanceof DeleteHashesFromGroupNodeSubRequest ||
p instanceof DeleteAllFromUserNodeSubRequest || p instanceof DeleteAllFromUserNodeSubRequest ||
p instanceof StoreGroupConfigSubRequest || p instanceof StoreGroupInfoSubRequest ||
p instanceof StoreGroupMembersSubRequest ||
p instanceof StoreGroupKeysSubRequest ||
p instanceof StoreGroupRevokedRetrievableSubRequest ||
p instanceof StoreGroupMessageSubRequest || p instanceof StoreGroupMessageSubRequest ||
p instanceof StoreLegacyGroupMessageSubRequest || p instanceof StoreLegacyGroupMessageSubRequest ||
p instanceof StoreUserConfigSubRequest || p instanceof StoreUserConfigSubRequest ||

@ -13,11 +13,10 @@ import {
MultiEncryptWrapperActions, MultiEncryptWrapperActions,
UserGroupsWrapperActions, UserGroupsWrapperActions,
} from '../../../../webworker/workers/browser/libsession_worker_interface'; } from '../../../../webworker/workers/browser/libsession_worker_interface';
import { StoreGroupRevokedRetrievableSubRequest } from '../../../apis/snode_api/SnodeRequestTypes';
import { GetNetworkTime } from '../../../apis/snode_api/getNetworkTime'; import { GetNetworkTime } from '../../../apis/snode_api/getNetworkTime';
import { SnodeNamespaces } from '../../../apis/snode_api/namespaces';
import { RevokeChanges, SnodeAPIRevoke } from '../../../apis/snode_api/revokeSubaccount'; import { RevokeChanges, SnodeAPIRevoke } from '../../../apis/snode_api/revokeSubaccount';
import { WithSecretKey } from '../../../apis/snode_api/types'; import { WithSecretKey } from '../../../apis/snode_api/types';
import { TTL_DEFAULT } from '../../../constants';
import { concatUInt8Array } from '../../../crypto'; import { concatUInt8Array } from '../../../crypto';
import { MessageSender } from '../../../sending'; import { MessageSender } from '../../../sending';
import { fromHexToArray } from '../../String'; import { fromHexToArray } from '../../String';
@ -29,7 +28,6 @@ import {
RunJobResult, RunJobResult,
} from '../PersistedJob'; } from '../PersistedJob';
import { GroupSync } from './GroupSyncJob'; import { GroupSync } from './GroupSyncJob';
import { StoreGroupConfigSubRequest } from '../../../apis/snode_api/SnodeRequestTypes';
export type WithAddWithoutHistoryMembers = { withoutHistory: Array<PubkeyType> }; export type WithAddWithoutHistoryMembers = { withoutHistory: Array<PubkeyType> };
export type WithAddWithHistoryMembers = { withHistory: Array<PubkeyType> }; export type WithAddWithHistoryMembers = { withHistory: Array<PubkeyType> };
@ -154,13 +152,10 @@ class GroupPendingRemovalsJob extends PersistedJob<GroupPendingRemovalsPersisted
secretKey: group.secretKey, secretKey: group.secretKey,
}); });
const multiEncryptRequest = new StoreGroupConfigSubRequest({ const multiEncryptRequest = new StoreGroupRevokedRetrievableSubRequest({
encryptedData: multiEncryptedMessage, encryptedData: multiEncryptedMessage,
groupPk, groupPk,
namespace: SnodeNamespaces.ClosedGroupRevokedRetrievableMessages,
ttlMs: TTL_DEFAULT.CONTENT_MESSAGE,
secretKey: group.secretKey, secretKey: group.secretKey,
authData: null,
}); });
const result = await MessageSender.sendEncryptedDataToSnode({ const result = await MessageSender.sendEncryptedDataToSnode({

@ -1,9 +1,8 @@
/* eslint-disable no-await-in-loop */ /* eslint-disable no-await-in-loop */
import { GroupPubkeyType, WithGroupPubkey } from 'libsession_util_nodejs'; import { GroupPubkeyType, WithGroupPubkey } from 'libsession_util_nodejs';
import { isArray, isEmpty, isNumber } from 'lodash';
import { to_hex } from 'libsodium-wrappers-sumo'; import { to_hex } from 'libsodium-wrappers-sumo';
import { isArray, isEmpty, isNumber } from 'lodash';
import { UserUtils } from '../..'; import { UserUtils } from '../..';
import { SignalService } from '../../../../protobuf';
import { assertUnreachable } from '../../../../types/sqlSharedTypes'; import { assertUnreachable } from '../../../../types/sqlSharedTypes';
import { isSignInByLinking } from '../../../../util/storage'; import { isSignInByLinking } from '../../../../util/storage';
import { import {
@ -12,21 +11,18 @@ import {
} from '../../../../webworker/workers/browser/libsession_worker_interface'; } from '../../../../webworker/workers/browser/libsession_worker_interface';
import { import {
DeleteAllFromGroupMsgNodeSubRequest, DeleteAllFromGroupMsgNodeSubRequest,
DeleteHashesFromGroupNodeSubRequest, StoreGroupKeysSubRequest,
StoreGroupConfigSubRequest,
StoreGroupExtraData,
StoreGroupMessageSubRequest, StoreGroupMessageSubRequest,
} from '../../../apis/snode_api/SnodeRequestTypes'; } from '../../../apis/snode_api/SnodeRequestTypes';
import { GetNetworkTime } from '../../../apis/snode_api/getNetworkTime'; import { DeleteGroupHashesFactory } from '../../../apis/snode_api/factories/DeleteGroupHashesRequestFactory';
import { StoreGroupRequestFactory } from '../../../apis/snode_api/factories/StoreGroupRequestFactory';
import { SnodeNamespaces } from '../../../apis/snode_api/namespaces'; import { SnodeNamespaces } from '../../../apis/snode_api/namespaces';
import { WithRevokeSubRequest } from '../../../apis/snode_api/types'; import { WithRevokeSubRequest } from '../../../apis/snode_api/types';
import { TTL_DEFAULT } from '../../../constants';
import { ConvoHub } from '../../../conversations'; import { ConvoHub } from '../../../conversations';
import { GroupUpdateInfoChangeMessage } from '../../../messages/outgoing/controlMessage/group_v2/to_group/GroupUpdateInfoChangeMessage';
import { GroupUpdateMemberChangeMessage } from '../../../messages/outgoing/controlMessage/group_v2/to_group/GroupUpdateMemberChangeMessage';
import { MessageSender } from '../../../sending/MessageSender'; import { MessageSender } from '../../../sending/MessageSender';
import { PubKey } from '../../../types'; import { PubKey } from '../../../types';
import { allowOnlyOneAtATime } from '../../Promise'; import { allowOnlyOneAtATime } from '../../Promise';
import { ed25519Str } from '../../String';
import { GroupSuccessfulChange, LibSessionUtil } from '../../libsession/libsession_utils'; import { GroupSuccessfulChange, LibSessionUtil } from '../../libsession/libsession_utils';
import { runners } from '../JobRunner'; import { runners } from '../JobRunner';
import { import {
@ -35,7 +31,6 @@ import {
PersistedJob, PersistedJob,
RunJobResult, RunJobResult,
} from '../PersistedJob'; } from '../PersistedJob';
import { ed25519Str } from '../../String';
const defaultMsBetweenRetries = 15000; // a long time between retries, to avoid running multiple jobs at the same time, when one was postponed at the same time as one already planned (5s) const defaultMsBetweenRetries = 15000; // a long time between retries, to avoid running multiple jobs at the same time, when one was postponed at the same time as one already planned (5s)
const defaultMaxAttempts = 2; const defaultMaxAttempts = 2;
@ -81,98 +76,18 @@ async function confirmPushedAndDump(
return LibSessionUtil.saveDumpsToDb(groupPk); return LibSessionUtil.saveDumpsToDb(groupPk);
} }
async function storeGroupUpdateMessages({
updateMessages,
groupPk,
}: WithGroupPubkey & {
updateMessages: Array<GroupUpdateMemberChangeMessage | GroupUpdateInfoChangeMessage>;
}) {
if (!updateMessages.length) {
return true;
}
const group = await UserGroupsWrapperActions.getGroup(groupPk);
if (!group) {
window.log.warn(
`storeGroupUpdateMessages for ${ed25519Str(groupPk)}: no group found in wrapper`
);
return false;
}
const updateMessagesToEncrypt: Array<StoreGroupExtraData> = updateMessages.map(updateMessage => {
const wrapped = MessageSender.wrapContentIntoEnvelope(
SignalService.Envelope.Type.SESSION_MESSAGE,
undefined,
updateMessage.createAtNetworkTimestamp, // message is signed with this timestmap
updateMessage.plainTextBuffer()
);
return {
namespace: SnodeNamespaces.ClosedGroupMessages,
pubkey: groupPk,
ttl: updateMessage.ttl(),
networkTimestamp: updateMessage.createAtNetworkTimestamp,
data: SignalService.Envelope.encode(wrapped).finish(),
dbMessageIdentifier: updateMessage.identifier,
};
});
const encryptedUpdate = updateMessagesToEncrypt
? await MetaGroupWrapperActions.encryptMessages(
groupPk,
updateMessagesToEncrypt.map(m => m.data)
)
: [];
const updateMessagesEncrypted = updateMessagesToEncrypt.map((requestDetails, index) => ({
...requestDetails,
data: encryptedUpdate[index],
}));
const updateMessagesRequests = updateMessagesEncrypted.map(m => {
return new StoreGroupMessageSubRequest({
encryptedData: m.data,
groupPk,
ttlMs: m.ttl,
dbMessageIdentifier: m.dbMessageIdentifier,
...group,
createdAtNetworkTimestamp: m.networkTimestamp,
});
});
const result = await MessageSender.sendEncryptedDataToSnode({
storeRequests: [...updateMessagesRequests],
destination: groupPk,
deleteHashesSubRequest: null,
revokeSubRequest: null,
unrevokeSubRequest: null,
deleteAllMessagesSubRequest: null,
});
const expectedReplyLength = updateMessagesRequests.length; // each of those messages are sent as a subrequest
// we do a sequence call here. If we do not have the right expected number of results, consider it a failure
if (!isArray(result) || result.length !== expectedReplyLength) {
window.log.info(
`GroupSyncJob: unexpected result length: expected ${expectedReplyLength} but got ${result?.length}`
);
// this might be a 421 error (already handled) so let's retry this request a little bit later
return false;
}
return true;
}
async function pushChangesToGroupSwarmIfNeeded({ async function pushChangesToGroupSwarmIfNeeded({
revokeSubRequest, revokeSubRequest,
unrevokeSubRequest, unrevokeSubRequest,
groupPk, groupPk,
encryptedSupplementKeys, supplementalKeysSubRequest,
deleteAllMessagesSubRequest, deleteAllMessagesSubRequest,
extraStoreRequests,
}: WithGroupPubkey & }: WithGroupPubkey &
WithRevokeSubRequest & { WithRevokeSubRequest & {
encryptedSupplementKeys: Array<Uint8Array>; supplementalKeysSubRequest: Array<StoreGroupKeysSubRequest>;
deleteAllMessagesSubRequest?: DeleteAllFromGroupMsgNodeSubRequest | null; deleteAllMessagesSubRequest?: DeleteAllFromGroupMsgNodeSubRequest | null;
extraStoreRequests: Array<StoreGroupMessageSubRequest>;
}): Promise<RunJobResult> { }): Promise<RunJobResult> {
// save the dumps to DB even before trying to push them, so at least we have an up to date dumps in the DB in case of crash, no network etc // save the dumps to DB even before trying to push them, so at least we have an up to date dumps in the DB in case of crash, no network etc
await LibSessionUtil.saveDumpsToDb(groupPk); await LibSessionUtil.saveDumpsToDb(groupPk);
@ -182,10 +97,11 @@ async function pushChangesToGroupSwarmIfNeeded({
// is updated we want to try and run immediately so don't schedule another run in this case) // is updated we want to try and run immediately so don't schedule another run in this case)
if ( if (
isEmpty(pendingConfigData) && isEmpty(pendingConfigData) &&
!encryptedSupplementKeys.length && isEmpty(supplementalKeysSubRequest) &&
!revokeSubRequest && isEmpty(revokeSubRequest) &&
!unrevokeSubRequest && isEmpty(unrevokeSubRequest) &&
!deleteAllMessagesSubRequest isEmpty(deleteAllMessagesSubRequest) &&
isEmpty(extraStoreRequests)
) { ) {
window.log.debug(`pushChangesToGroupSwarmIfNeeded: ${ed25519Str(groupPk)}: nothing to push`); window.log.debug(`pushChangesToGroupSwarmIfNeeded: ${ed25519Str(groupPk)}: nothing to push`);
return RunJobResult.Success; return RunJobResult.Success;
@ -205,109 +121,20 @@ async function pushChangesToGroupSwarmIfNeeded({
); );
} }
const networkTimestamp = GetNetworkTime.now(); const pendingConfigRequests = StoreGroupRequestFactory.makeStoreGroupConfigSubRequest({
group,
const pendingConfigMsgs = pendingConfigData.map(item => { pendingConfigData,
return {
namespace: item.namespace,
pubkey: groupPk,
networkTimestamp,
ttl: TTL_DEFAULT.CONFIG_MESSAGE,
data: item.ciphertext,
};
}); });
// supplementKeys are already encrypted by libsession const deleteHashesSubRequest = DeleteGroupHashesFactory.makeGroupHashesToDeleteSubRequest({
const keysEncryptedMessages: Array<StoreGroupExtraData> = encryptedSupplementKeys.map(key => ({ group,
namespace: SnodeNamespaces.ClosedGroupKeys, allOldHashes,
pubkey: groupPk, });
ttl: TTL_DEFAULT.CONFIG_MESSAGE,
networkTimestamp,
data: key,
dbMessageIdentifier: null,
}));
let pendingConfigRequests: Array<StoreGroupConfigSubRequest> = [];
let keysEncryptedRequests: Array<StoreGroupConfigSubRequest> = [];
if (pendingConfigMsgs.length) {
if (!group.secretKey || isEmpty(group.secretKey)) {
window.log.debug(
`pushChangesToGroupSwarmIfNeeded: ${ed25519Str(groupPk)}: pendingConfigMsgs not empty but we do not have the secretKey`
);
throw new Error(
'pushChangesToGroupSwarmIfNeeded: pendingConfigMsgs not empty but we do not have the secretKey'
);
}
pendingConfigRequests = pendingConfigMsgs.map(m => {
return new StoreGroupConfigSubRequest({
encryptedData: m.data,
groupPk,
namespace: m.namespace,
ttlMs: m.ttl,
secretKey: group.secretKey,
authData: null,
});
});
}
if (keysEncryptedMessages.length) {
// supplementalKeys are already encrypted, but we still need the secretKey to sign the request
if (!group.secretKey || isEmpty(group.secretKey)) {
window.log.debug(
`pushChangesToGroupSwarmIfNeeded: ${ed25519Str(groupPk)}: keysEncryptedmessage not empty but we do not have the secretKey`
);
throw new Error(
'pushChangesToGroupSwarmIfNeeded: keysEncryptedmessage not empty but we do not have the secretKey'
);
}
keysEncryptedRequests = keysEncryptedMessages.map(m => {
return new StoreGroupConfigSubRequest({
encryptedData: m.data,
groupPk,
namespace: SnodeNamespaces.ClosedGroupKeys,
ttlMs: m.ttl,
secretKey: group.secretKey,
authData: null,
});
});
}
let deleteHashesSubRequest: DeleteHashesFromGroupNodeSubRequest | null = null;
const allOldHashesArray = [...allOldHashes];
if (allOldHashesArray.length) {
if (!group.secretKey || isEmpty(group.secretKey)) {
window.log.debug(
`pushChangesToGroupSwarmIfNeeded: ${ed25519Str(groupPk)}: allOldHashesArray not empty but we do not have the secretKey`
);
throw new Error(
'pushChangesToGroupSwarmIfNeeded: allOldHashesArray not empty but we do not have the secretKey'
);
}
deleteHashesSubRequest = new DeleteHashesFromGroupNodeSubRequest({
messagesHashes: [...allOldHashes],
groupPk,
secretKey: group.secretKey,
});
}
if (
revokeSubRequest?.revokeTokenHex.length === 0 ||
unrevokeSubRequest?.revokeTokenHex.length === 0
) {
throw new Error(
'revokeSubRequest and unrevoke request must be null when not doing token change'
);
}
const result = await MessageSender.sendEncryptedDataToSnode({ const result = await MessageSender.sendEncryptedDataToSnode({
storeRequests: [...pendingConfigRequests, ...keysEncryptedRequests], // Note: this is on purpose that supplementalKeysSubRequest is before pendingConfigRequests
// as this is to avoid a race condition where a device polls while we are posting the configs (already encrypted with the new keys)
storeRequests: [...supplementalKeysSubRequest, ...pendingConfigRequests, ...extraStoreRequests],
destination: groupPk, destination: groupPk,
deleteHashesSubRequest, deleteHashesSubRequest,
revokeSubRequest, revokeSubRequest,
@ -316,12 +143,13 @@ async function pushChangesToGroupSwarmIfNeeded({
}); });
const expectedReplyLength = const expectedReplyLength =
pendingConfigRequests.length + // each of those messages are sent as a subrequest pendingConfigRequests.length + // each of those are sent as a subrequest
keysEncryptedRequests.length + // each of those messages are sent as a subrequest supplementalKeysSubRequest.length + // each of those are sent as a subrequest
(allOldHashes.size ? 1 : 0) + // we are sending all hashes changes as a single subrequest (allOldHashes.size ? 1 : 0) + // we are sending all hashes changes as a single subrequest
(revokeSubRequest ? 1 : 0) + // we are sending all revoke updates as a single subrequest (revokeSubRequest ? 1 : 0) + // we are sending all revoke updates as a single subrequest
(unrevokeSubRequest ? 1 : 0) + // we are sending all revoke updates as a single subrequest (unrevokeSubRequest ? 1 : 0) + // we are sending all revoke updates as a single subrequest
(deleteAllMessagesSubRequest ? 1 : 0); // a delete_all sub request is a single subrequest (deleteAllMessagesSubRequest ? 1 : 0) + // a delete_all sub request is a single subrequest
(extraStoreRequests ? 1 : 0); // each of those are sent as a subrequest
// we do a sequence call here. If we do not have the right expected number of results, consider it a failure // we do a sequence call here. If we do not have the right expected number of results, consider it a failure
if (!isArray(result) || result.length !== expectedReplyLength) { if (!isArray(result) || result.length !== expectedReplyLength) {
@ -341,9 +169,9 @@ async function pushChangesToGroupSwarmIfNeeded({
if (isEmpty(changes)) { if (isEmpty(changes)) {
return RunJobResult.RetryJobIfPossible; return RunJobResult.RetryJobIfPossible;
} }
// Now that we have the successful changes, we need to mark them as pushed and // Now that we have the successful changes, we need to mark them as pushed and
// generate any config dumps which need to be stored // generate any config dumps which need to be stored
await confirmPushedAndDump(changes, groupPk); await confirmPushedAndDump(changes, groupPk);
return RunJobResult.Success; return RunJobResult.Success;
} }
@ -393,7 +221,8 @@ class GroupSyncJob extends PersistedJob<GroupSyncPersistedData> {
groupPk: thisJobDestination, groupPk: thisJobDestination,
revokeSubRequest: null, revokeSubRequest: null,
unrevokeSubRequest: null, unrevokeSubRequest: null,
encryptedSupplementKeys: [], supplementalKeysSubRequest: [],
extraStoreRequests: [],
}); });
// eslint-disable-next-line no-useless-catch // eslint-disable-next-line no-useless-catch
@ -473,7 +302,6 @@ async function queueNewJobIfNeeded(groupPk: GroupPubkeyType) {
export const GroupSync = { export const GroupSync = {
GroupSyncJob, GroupSyncJob,
pushChangesToGroupSwarmIfNeeded, pushChangesToGroupSwarmIfNeeded,
storeGroupUpdateMessages,
queueNewJobIfNeeded: (groupPk: GroupPubkeyType) => queueNewJobIfNeeded: (groupPk: GroupPubkeyType) =>
allowOnlyOneAtATime(`GroupSyncJob-oneAtAtTime-${groupPk}`, () => queueNewJobIfNeeded(groupPk)), allowOnlyOneAtATime(`GroupSyncJob-oneAtAtTime-${groupPk}`, () => queueNewJobIfNeeded(groupPk)),
}; };

@ -70,7 +70,6 @@ async function insertContactFromDBIntoWrapperAndRefresh(
const expirationMode = foundConvo.get('expirationMode') || undefined; const expirationMode = foundConvo.get('expirationMode') || undefined;
const expireTimer = foundConvo.get('expireTimer') || 0; const expireTimer = foundConvo.get('expireTimer') || 0;
const wrapperContact = getContactInfoFromDBValues({ const wrapperContact = getContactInfoFromDBValues({
id, id,
dbApproved, dbApproved,

@ -27,5 +27,5 @@ export function isTestIntegration() {
} }
export function hasClosedGroupV2QAButtons() { export function hasClosedGroupV2QAButtons() {
return !!window.sessionFeatureFlags.useClosedGroupV2QAButtons return !!window.sessionFeatureFlags.useClosedGroupV2QAButtons;
} }

@ -17,6 +17,7 @@ import { ConversationTypeEnum } from '../../models/conversationAttributes';
import { HexString } from '../../node/hexStrings'; import { HexString } from '../../node/hexStrings';
import { SignalService } from '../../protobuf'; import { SignalService } from '../../protobuf';
import { getSwarmPollingInstance } from '../../session/apis/snode_api'; import { getSwarmPollingInstance } from '../../session/apis/snode_api';
import { StoreGroupRequestFactory } from '../../session/apis/snode_api/factories/StoreGroupRequestFactory';
import { GetNetworkTime } from '../../session/apis/snode_api/getNetworkTime'; import { GetNetworkTime } from '../../session/apis/snode_api/getNetworkTime';
import { ConvoHub } from '../../session/conversations'; import { ConvoHub } from '../../session/conversations';
import { getSodiumRenderer } from '../../session/crypto'; import { getSodiumRenderer } from '../../session/crypto';
@ -51,6 +52,7 @@ import {
import { StateType } from '../reducer'; import { StateType } from '../reducer';
import { openConversationWithMessages } from './conversations'; import { openConversationWithMessages } from './conversations';
import { resetLeftOverlayMode } from './section'; import { resetLeftOverlayMode } from './section';
import { ed25519Str } from '../../session/utils/String';
type WithFromMemberLeftMessage = { fromMemberLeftMessage: boolean }; // there are some changes we want to skip when doing changes triggered from a memberLeft message. type WithFromMemberLeftMessage = { fromMemberLeftMessage: boolean }; // there are some changes we want to skip when doing changes triggered from a memberLeft message.
export type GroupState = { export type GroupState = {
@ -177,19 +179,7 @@ const initNewGroupInWrapper = createAsyncThunk(
const convo = await ConvoHub.use().getOrCreateAndWait(groupPk, ConversationTypeEnum.GROUPV2); const convo = await ConvoHub.use().getOrCreateAndWait(groupPk, ConversationTypeEnum.GROUPV2);
await convo.setIsApproved(true, false); await convo.setIsApproved(true, false);
await convo.commit(); // commit here too, as the poll needs it to be approved await convo.commit(); // commit here too, as the poll needs it to be approved
let groupMemberChange: GroupUpdateMemberChangeMessage | null = null;
const result = await GroupSync.pushChangesToGroupSwarmIfNeeded({
groupPk,
revokeSubRequest: null,
unrevokeSubRequest: null,
encryptedSupplementKeys: [],
deleteAllMessagesSubRequest: null,
});
if (result !== RunJobResult.Success) {
window.log.warn('GroupSync.pushChangesToGroupSwarmIfNeeded during create failed');
throw new Error('failed to pushChangesToGroupSwarmIfNeeded');
}
// push one group change message were initial members are added to the group // push one group change message were initial members are added to the group
if (membersFromWrapper.length) { if (membersFromWrapper.length) {
const membersHex = uniq(membersFromWrapper.map(m => m.pubkeyHex)); const membersHex = uniq(membersFromWrapper.map(m => m.pubkeyHex));
@ -202,7 +192,7 @@ const initNewGroupInWrapper = createAsyncThunk(
convo, convo,
markAlreadySent: false, // the store below will mark the message as sent with dbMsgIdentifier markAlreadySent: false, // the store below will mark the message as sent with dbMsgIdentifier
}); });
const groupChange = await getWithoutHistoryControlMessage({ groupMemberChange = await getWithoutHistoryControlMessage({
adminSecretKey: groupSecretKey, adminSecretKey: groupSecretKey,
convo, convo,
groupPk, groupPk,
@ -210,12 +200,24 @@ const initNewGroupInWrapper = createAsyncThunk(
createAtNetworkTimestamp: sentAt, createAtNetworkTimestamp: sentAt,
dbMsgIdentifier: msgModel.id, dbMsgIdentifier: msgModel.id,
}); });
if (groupChange) { }
await GroupSync.storeGroupUpdateMessages({
groupPk, const extraStoreRequests = await StoreGroupRequestFactory.makeGroupMessageSubRequest(
updateMessages: [groupChange], [groupMemberChange],
}); { authData: null, secretKey: newGroup.secretKey }
} );
const result = await GroupSync.pushChangesToGroupSwarmIfNeeded({
groupPk,
revokeSubRequest: null,
unrevokeSubRequest: null,
supplementalKeysSubRequest: [],
deleteAllMessagesSubRequest: null,
extraStoreRequests,
});
if (result !== RunJobResult.Success) {
window.log.warn('GroupSync.pushChangesToGroupSwarmIfNeeded during create failed');
throw new Error('failed to pushChangesToGroupSwarmIfNeeded');
} }
await convo.commit(); await convo.commit();
@ -647,18 +649,24 @@ async function handleMemberAddedFromUI({
groupPk, groupPk,
}); });
// first, get the unrevoke requests for people who are added // first, get the unrevoke requests for people who are added
const revokeUnrevokeParams = await GroupPendingRemovals.getPendingRevokeParams({ const { revokeSubRequest, unrevokeSubRequest } =
groupPk, await GroupPendingRemovals.getPendingRevokeParams({
withHistory, groupPk,
withoutHistory, withHistory,
removed: [], withoutHistory,
secretKey: group.secretKey, removed: [],
}); secretKey: group.secretKey,
});
// then, handle the addition with history of messages by generating supplement keys. // then, handle the addition with history of messages by generating supplement keys.
// this adds them to the members wrapper etc // this adds them to the members wrapper etc
const encryptedSupplementKeys = await handleWithHistoryMembers({ groupPk, withHistory }); const encryptedSupplementKeys = await handleWithHistoryMembers({ groupPk, withHistory });
const supplementalKeysSubRequest = StoreGroupRequestFactory.makeStoreGroupKeysSubRequest({
group,
encryptedSupplementKeys,
});
// then handle the addition without history of messages (full rotation of keys). // then handle the addition without history of messages (full rotation of keys).
// this adds them to the members wrapper etc // this adds them to the members wrapper etc
await handleWithoutHistoryMembers({ groupPk, withoutHistory }); await handleWithoutHistoryMembers({ groupPk, withoutHistory });
@ -666,27 +674,6 @@ async function handleMemberAddedFromUI({
await LibSessionUtil.saveDumpsToDb(groupPk); await LibSessionUtil.saveDumpsToDb(groupPk);
// push new members & key supplement in a single batch call
const sequenceResult = await GroupSync.pushChangesToGroupSwarmIfNeeded({
groupPk,
encryptedSupplementKeys,
...revokeUnrevokeParams,
deleteAllMessagesSubRequest: null,
});
if (sequenceResult !== RunJobResult.Success) {
throw new Error(
'handleMemberAddedFromUIOrNot: pushChangesToGroupSwarmIfNeeded did not return success'
);
}
// schedule send invite details, auth signature, etc. to the new users
await scheduleGroupInviteJobs(groupPk, withHistory, withoutHistory);
await LibSessionUtil.saveDumpsToDb(groupPk);
convo.set({
active_at: createAtNetworkTimestamp,
});
const expireDetails = DisappearingMessages.getExpireDetailsForOutgoingMesssage( const expireDetails = DisappearingMessages.getExpireDetailsForOutgoingMesssage(
convo, convo,
createAtNetworkTimestamp createAtNetworkTimestamp
@ -698,7 +685,6 @@ async function handleMemberAddedFromUI({
expireUpdate: expireDetails, expireUpdate: expireDetails,
markAlreadySent: false, // the store below will mark the message as sent with dbMsgIdentifier markAlreadySent: false, // the store below will mark the message as sent with dbMsgIdentifier
}; };
const updateMessagesToPush: Array<GroupUpdateMemberChangeMessage> = []; const updateMessagesToPush: Array<GroupUpdateMemberChangeMessage> = [];
if (withHistory.length) { if (withHistory.length) {
const msgModel = await ClosedGroup.addUpdateMessage({ const msgModel = await ClosedGroup.addUpdateMessage({
@ -735,8 +721,35 @@ async function handleMemberAddedFromUI({
} }
} }
const extraStoreRequests = await StoreGroupRequestFactory.makeGroupMessageSubRequest(
updateMessagesToPush,
group
);
// push new members & key supplement in a single batch call
const sequenceResult = await GroupSync.pushChangesToGroupSwarmIfNeeded({
groupPk,
supplementalKeysSubRequest,
revokeSubRequest,
unrevokeSubRequest,
deleteAllMessagesSubRequest: null,
extraStoreRequests,
});
if (sequenceResult !== RunJobResult.Success) {
throw new Error(
'handleMemberAddedFromUIOrNot: pushChangesToGroupSwarmIfNeeded did not return success'
);
}
// schedule send invite details, auth signature, etc. to the new users
await scheduleGroupInviteJobs(groupPk, withHistory, withoutHistory);
await LibSessionUtil.saveDumpsToDb(groupPk);
convo.set({
active_at: createAtNetworkTimestamp,
});
await convo.commit(); await convo.commit();
await GroupSync.storeGroupUpdateMessages({ groupPk, updateMessages: updateMessagesToPush });
} }
/** /**
@ -782,13 +795,51 @@ async function handleMemberRemovedFromUI({
const createAtNetworkTimestamp = GetNetworkTime.now(); const createAtNetworkTimestamp = GetNetworkTime.now();
await LibSessionUtil.saveDumpsToDb(groupPk); await LibSessionUtil.saveDumpsToDb(groupPk);
const expiringDetails = DisappearingMessages.getExpireDetailsForOutgoingMesssage(
convo,
createAtNetworkTimestamp
);
let removedControlMessage: GroupUpdateMemberChangeMessage | null = null;
if (removed.length && !fromMemberLeftMessage) {
const msgModel = await ClosedGroup.addUpdateMessage({
diff: { type: 'kicked', kicked: removed },
convo,
sender: us,
sentAt: createAtNetworkTimestamp,
expireUpdate: {
expirationTimer: expiringDetails.expireTimer,
expirationType: expiringDetails.expirationType,
messageExpirationFromRetrieve:
expiringDetails.expireTimer > 0
? createAtNetworkTimestamp + expiringDetails.expireTimer
: null,
},
markAlreadySent: false, // the store below will mark the message as sent with dbMsgIdentifier
});
removedControlMessage = await getRemovedControlMessage({
adminSecretKey: group.secretKey,
convo,
groupPk,
removed,
createAtNetworkTimestamp,
fromMemberLeftMessage,
dbMsgIdentifier: msgModel.id,
});
}
const extraStoreRequests = await StoreGroupRequestFactory.makeGroupMessageSubRequest(
[removedControlMessage],
group
);
// revoked pubkeys, update messages, and libsession groups config in a single batch call // revoked pubkeys, update messages, and libsession groups config in a single batch call
const sequenceResult = await GroupSync.pushChangesToGroupSwarmIfNeeded({ const sequenceResult = await GroupSync.pushChangesToGroupSwarmIfNeeded({
groupPk, groupPk,
encryptedSupplementKeys: [], supplementalKeysSubRequest: [],
revokeSubRequest: null, revokeSubRequest: null,
unrevokeSubRequest: null, unrevokeSubRequest: null,
deleteAllMessagesSubRequest: null, deleteAllMessagesSubRequest: null,
extraStoreRequests,
}); });
if (sequenceResult !== RunJobResult.Success) { if (sequenceResult !== RunJobResult.Success) {
throw new Error( throw new Error(
@ -801,49 +852,7 @@ async function handleMemberRemovedFromUI({
convo.set({ convo.set({
active_at: createAtNetworkTimestamp, active_at: createAtNetworkTimestamp,
}); });
const expiringDetails = DisappearingMessages.getExpireDetailsForOutgoingMesssage(
convo,
createAtNetworkTimestamp
);
const shared = {
convo,
sender: us,
sentAt: createAtNetworkTimestamp,
expireUpdate: {
expirationTimer: expiringDetails.expireTimer,
expirationType: expiringDetails.expirationType,
messageExpirationFromRetrieve:
expiringDetails.expireTimer > 0
? createAtNetworkTimestamp + expiringDetails.expireTimer
: null,
},
};
await convo.commit(); await convo.commit();
if (removed.length && !fromMemberLeftMessage) {
const msgModel = await ClosedGroup.addUpdateMessage({
diff: { type: 'kicked', kicked: removed },
...shared,
markAlreadySent: false, // the store below will mark the message as sent with dbMsgIdentifier
});
const removedControlMessage = await getRemovedControlMessage({
adminSecretKey: group.secretKey,
convo,
groupPk,
removed,
createAtNetworkTimestamp,
fromMemberLeftMessage,
dbMsgIdentifier: msgModel.id,
});
if (removedControlMessage) {
await GroupSync.storeGroupUpdateMessages({
groupPk,
updateMessages: [removedControlMessage],
});
}
}
} }
async function handleNameChangeFromUI({ async function handleNameChangeFromUI({
@ -901,12 +910,18 @@ async function handleNameChangeFromUI({
...DisappearingMessages.getExpireDetailsForOutgoingMesssage(convo, createAtNetworkTimestamp), ...DisappearingMessages.getExpireDetailsForOutgoingMesssage(convo, createAtNetworkTimestamp),
}); });
const extraStoreRequests = await StoreGroupRequestFactory.makeGroupMessageSubRequest(
[nameChangeMsg],
group
);
const batchResult = await GroupSync.pushChangesToGroupSwarmIfNeeded({ const batchResult = await GroupSync.pushChangesToGroupSwarmIfNeeded({
groupPk, groupPk,
encryptedSupplementKeys: [], supplementalKeysSubRequest: [],
revokeSubRequest: null, revokeSubRequest: null,
unrevokeSubRequest: null, unrevokeSubRequest: null,
deleteAllMessagesSubRequest: null, deleteAllMessagesSubRequest: null,
extraStoreRequests,
}); });
if (batchResult !== RunJobResult.Success) { if (batchResult !== RunJobResult.Success) {
@ -916,7 +931,6 @@ async function handleNameChangeFromUI({
} }
await UserSync.queueNewJobIfNeeded(); await UserSync.queueNewJobIfNeeded();
await GroupSync.storeGroupUpdateMessages({ groupPk, updateMessages: [nameChangeMsg] });
convo.set({ convo.set({
active_at: createAtNetworkTimestamp, active_at: createAtNetworkTimestamp,
@ -1018,10 +1032,24 @@ const triggerFakeAvatarUpdate = createAsyncThunk(
secretKey: group.secretKey, secretKey: group.secretKey,
sodium: await getSodiumRenderer(), sodium: await getSodiumRenderer(),
}); });
await GroupSync.storeGroupUpdateMessages({
const extraStoreRequests = await StoreGroupRequestFactory.makeGroupMessageSubRequest(
[updateMsg],
group
);
const batchResult = await GroupSync.pushChangesToGroupSwarmIfNeeded({
groupPk, groupPk,
updateMessages: [updateMsg], supplementalKeysSubRequest: [],
revokeSubRequest: null,
unrevokeSubRequest: null,
deleteAllMessagesSubRequest: null,
extraStoreRequests,
}); });
if (!batchResult) {
window.log.warn(`failed to send avatarChange message for group ${ed25519Str(groupPk)}`);
throw new Error('failed to send avatarChange message');
}
} }
); );

@ -277,7 +277,8 @@ describe('GroupSyncJob pushChangesToGroupSwarmIfNeeded', () => {
groupPk, groupPk,
revokeSubRequest: null, revokeSubRequest: null,
unrevokeSubRequest: null, unrevokeSubRequest: null,
encryptedSupplementKeys: [], supplementalKeysSubRequest: [],
extraStoreRequests: [],
}); });
expect(result).to.be.eq(RunJobResult.Success); expect(result).to.be.eq(RunJobResult.Success);
expect(sendStub.callCount).to.be.eq(0); expect(sendStub.callCount).to.be.eq(0);
@ -302,7 +303,8 @@ describe('GroupSyncJob pushChangesToGroupSwarmIfNeeded', () => {
groupPk, groupPk,
revokeSubRequest: null, revokeSubRequest: null,
unrevokeSubRequest: null, unrevokeSubRequest: null,
encryptedSupplementKeys: [], supplementalKeysSubRequest: [],
extraStoreRequests: [],
}); });
sendStub.resolves(undefined); sendStub.resolves(undefined);
@ -376,7 +378,8 @@ describe('GroupSyncJob pushChangesToGroupSwarmIfNeeded', () => {
groupPk, groupPk,
revokeSubRequest: null, revokeSubRequest: null,
unrevokeSubRequest: null, unrevokeSubRequest: null,
encryptedSupplementKeys: [], supplementalKeysSubRequest: [],
extraStoreRequests: [],
}); });
expect(sendStub.callCount).to.be.eq(1); expect(sendStub.callCount).to.be.eq(1);

Loading…
Cancel
Save