Merge pull request #59 from Bilb/fix-invite-not-blocking

fix: invite contacts to groups is not blocking
pull/3281/head
Audric Ackermann 11 months ago committed by GitHub
commit f67af0edaf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -92,7 +92,7 @@
"fs-extra": "9.0.0",
"glob": "10.3.10",
"image-type": "^4.1.0",
"libsession_util_nodejs": "https://github.com/session-foundation/libsession-util-nodejs/releases/download/v0.4.12/libsession_util_nodejs-v0.4.12.tar.gz",
"libsession_util_nodejs": "https://github.com/session-foundation/libsession-util-nodejs/releases/download/v0.4.15/libsession_util_nodejs-v0.4.15.tar.gz",
"libsodium-wrappers-sumo": "^0.7.9",
"linkify-it": "^4.0.1",
"lodash": "^4.17.21",

@ -277,7 +277,6 @@ const ResendButton = ({ groupPk, pubkey }: { pubkey: PubkeyType; groupPk: GroupP
groupPk,
member: pubkey,
inviteAsAdmin: member.nominatedAdmin,
forceUnrevoke: true,
});
}}
/>

@ -7,7 +7,7 @@ import { useDispatch } from 'react-redux';
import { VALIDATION } from '../../session/constants';
import { ConvoHub } from '../../session/conversations';
import { ToastUtils, UserUtils } from '../../session/utils';
import { updateInviteContactModal } from '../../state/ducks/modalDialog';
import { updateGroupMembersModal, updateInviteContactModal } from '../../state/ducks/modalDialog';
import { SpacerLG } from '../basic/Text';
import {
@ -22,12 +22,10 @@ import { PubKey } from '../../session/types';
import { SessionUtilUserGroups } from '../../session/utils/libsession/libsession_utils_user_groups';
import { groupInfoActions } from '../../state/ducks/metaGroups';
import { useContactsToInviteToGroup } from '../../state/selectors/conversations';
import { useMemberGroupChangePending } from '../../state/selectors/groups';
import { useSelectedIsGroupV2 } from '../../state/selectors/selectedConversation';
import { MemberListItem } from '../MemberListItem';
import { SessionWrapperModal } from '../SessionWrapperModal';
import { SessionButton, SessionButtonColor, SessionButtonType } from '../basic/SessionButton';
import { SessionSpinner } from '../loading';
import { SessionToggle } from '../basic/SessionToggle';
import { GroupInviteRequiredVersionBanner } from '../NoticeBanner';
import { hasClosedGroupV2QAButtons } from '../../shared/env_vars';
@ -117,9 +115,6 @@ const InviteContactsDialogInner = (props: Props) => {
const dispatch = useDispatch();
const privateContactPubkeys = useContactsToInviteToGroup() as Array<PubkeyType>;
const isProcessingUIChange = useMemberGroupChangePending();
const isPrivate = useIsPrivate(conversationId);
const isPublic = useIsPublic(conversationId);
const membersFromRedux = useSortedGroupMembers(conversationId) || [];
@ -163,6 +158,11 @@ const InviteContactsDialogInner = (props: Props) => {
});
dispatch(action as any);
empty();
// We want to show the dialog where "invite sending" is visible (i.e. the current group members) instead of this one
// once we hit "invite"
closeDialog();
dispatch(updateGroupMembersModal({ conversationId }));
return;
}
void submitForClosedGroup(conversationId, selectedContacts);
@ -221,13 +221,12 @@ const InviteContactsDialogInner = (props: Props) => {
)}
</div>
<SpacerLG />
<SessionSpinner loading={isProcessingUIChange} />
<SpacerLG />
<div className="session-modal__button-group">
<SessionButton
text={okText}
buttonType={SessionButtonType.Simple}
disabled={!hasContacts || isProcessingUIChange}
disabled={!hasContacts}
onClick={onClickOK}
dataTestId="session-confirm-ok-button"
/>
@ -236,7 +235,6 @@ const InviteContactsDialogInner = (props: Props) => {
buttonColor={SessionButtonColor.Danger}
buttonType={SessionButtonType.Simple}
onClick={closeDialog}
disabled={isProcessingUIChange}
dataTestId="session-confirm-cancel-button"
/>
</div>

@ -1053,6 +1053,6 @@ export async function promoteUsersInGroup({
for (let index = 0; index < membersHex.length; index++) {
const member = membersHex[index];
// eslint-disable-next-line no-await-in-loop
await GroupInvite.addJob({ groupPk, member, inviteAsAdmin: true, forceUnrevoke: true });
await GroupInvite.addJob({ groupPk, member, inviteAsAdmin: true });
}
}

@ -403,24 +403,22 @@ abstract class AbstractRevokeSubRequest<
> extends SnodeAPISubRequest<T> {
public readonly destination: GroupPubkeyType;
public readonly timestamp: number;
public readonly revokeTokenHex: Array<string>;
public readonly tokensHex: Array<string>;
protected readonly adminSecretKey: Uint8Array;
constructor({
groupPk,
timestamp,
revokeTokenHex,
tokensHex,
secretKey,
method,
}: WithGroupPubkey &
WithTimestamp &
WithSecretKey & { revokeTokenHex: Array<string>; method: T }) {
}: WithGroupPubkey & WithTimestamp & WithSecretKey & { tokensHex: Array<string>; method: T }) {
super({ method });
this.destination = groupPk;
this.timestamp = timestamp;
this.revokeTokenHex = revokeTokenHex;
this.tokensHex = tokensHex;
this.adminSecretKey = secretKey;
if (this.revokeTokenHex.length === 0) {
if (this.tokensHex.length === 0) {
throw new Error('AbstractRevokeSubRequest needs at least one token to do a change');
}
}
@ -429,7 +427,7 @@ abstract class AbstractRevokeSubRequest<
if (!this.adminSecretKey) {
throw new Error('we need an admin secretKey');
}
const tokensBytes = from_hex(this.revokeTokenHex.join(''));
const tokensBytes = from_hex(this.tokensHex.join(''));
const prefix = new Uint8Array(StringUtils.encode(`${this.method}${this.timestamp}`, 'utf8'));
const sigResult = await SnodeGroupSignature.signDataWithAdminSecret(
@ -461,7 +459,7 @@ export class SubaccountRevokeSubRequest extends AbstractRevokeSubRequest<'revoke
params: {
pubkey: this.destination,
signature,
revoke: this.revokeTokenHex,
revoke: this.tokensHex,
timestamp: this.timestamp,
},
};
@ -483,7 +481,7 @@ export class SubaccountUnrevokeSubRequest extends AbstractRevokeSubRequest<'unre
params: {
pubkey: this.destination,
signature,
unrevoke: this.revokeTokenHex,
unrevoke: this.tokensHex,
timestamp: this.timestamp,
},
};

@ -24,7 +24,7 @@ async function getRevokeSubaccountParams(
const revokeSubRequest = revokeChanges.length
? new SubaccountRevokeSubRequest({
groupPk,
revokeTokenHex: revokeChanges.map(m => m.tokenToRevokeHex),
tokensHex: revokeChanges.map(m => m.tokenToRevokeHex),
timestamp: NetworkTime.now(),
secretKey,
})
@ -32,7 +32,7 @@ async function getRevokeSubaccountParams(
const unrevokeSubRequest = unrevokeChanges.length
? new SubaccountUnrevokeSubRequest({
groupPk,
revokeTokenHex: unrevokeChanges.map(m => m.tokenToRevokeHex),
tokensHex: unrevokeChanges.map(m => m.tokenToRevokeHex),
timestamp: NetworkTime.now(),
secretKey,
})

@ -43,7 +43,6 @@ export interface GroupInvitePersistedData extends PersistedJobData {
groupPk: GroupPubkeyType;
member: PubkeyType;
inviteAsAdmin: boolean;
forceUnrevoke: boolean;
}
export interface GroupPromotePersistedData extends PersistedJobData {

@ -23,10 +23,14 @@ import { showUpdateGroupMembersByConvoId } from '../../../../interactions/conver
import { ConvoHub } from '../../../conversations';
import { MessageSender } from '../../../sending';
import { NetworkTime } from '../../../../util/NetworkTime';
import { SubaccountUnrevokeSubRequest } from '../../../apis/snode_api/SnodeRequestTypes';
import {
SubaccountUnrevokeSubRequest,
type StoreGroupKeysSubRequest,
} from '../../../apis/snode_api/SnodeRequestTypes';
import { GroupSync } from './GroupSyncJob';
import { DURATION } from '../../../constants';
import { timeoutWithAbort } from '../../Promise';
import { StoreGroupRequestFactory } from '../../../apis/snode_api/factories/StoreGroupRequestFactory';
const defaultMsBetweenRetries = 10000;
const defaultMaxAttempts = 1;
@ -35,12 +39,6 @@ type JobExtraArgs = {
groupPk: GroupPubkeyType;
member: PubkeyType;
inviteAsAdmin: boolean;
/**
* When inviting a member, we usually only want to sent a message to his swarm.
* In the case of an invitation resend process though, we also want to make sure his token is unrevoked from the group's swarm.
*
*/
forceUnrevoke: boolean;
};
export function shouldAddJob(args: JobExtraArgs) {
@ -59,13 +57,12 @@ const invitesFailed = new Map<
}
>();
async function addJob({ groupPk, member, inviteAsAdmin, forceUnrevoke }: JobExtraArgs) {
if (shouldAddJob({ groupPk, member, inviteAsAdmin, forceUnrevoke })) {
async function addJob({ groupPk, member, inviteAsAdmin }: JobExtraArgs) {
if (shouldAddJob({ groupPk, member, inviteAsAdmin })) {
const groupInviteJob = new GroupInviteJob({
groupPk,
member,
inviteAsAdmin,
forceUnrevoke,
nextAttemptTimestamp: Date.now(),
});
window.log.debug(
@ -146,9 +143,8 @@ class GroupInviteJob extends PersistedJob<GroupInvitePersistedData> {
nextAttemptTimestamp,
maxAttempts,
currentRetry,
forceUnrevoke,
identifier,
}: Pick<GroupInvitePersistedData, 'groupPk' | 'member' | 'inviteAsAdmin' | 'forceUnrevoke'> &
}: Pick<GroupInvitePersistedData, 'groupPk' | 'member' | 'inviteAsAdmin'> &
Partial<
Pick<
GroupInvitePersistedData,
@ -161,7 +157,6 @@ class GroupInviteJob extends PersistedJob<GroupInvitePersistedData> {
member,
groupPk,
inviteAsAdmin,
forceUnrevoke,
delayBetweenRetries: defaultMsBetweenRetries,
maxAttempts: isNumber(maxAttempts) ? maxAttempts : defaultMaxAttempts,
nextAttemptTimestamp: nextAttemptTimestamp || Date.now() + defaultMsBetweenRetries,
@ -187,36 +182,51 @@ class GroupInviteJob extends PersistedJob<GroupInvitePersistedData> {
let failed = true;
try {
let start = Date.now();
const memberObj = await MetaGroupWrapperActions.memberGet(groupPk, member);
if (!memberObj) {
throw new Error('Member should have been added before GroupInviteJob was run()');
}
let supplementalKeysSubRequest: StoreGroupKeysSubRequest | undefined;
if (this.persistedData.forceUnrevoke) {
const token = await MetaGroupWrapperActions.swarmSubAccountToken(groupPk, member);
const unrevokeSubRequest = new SubaccountUnrevokeSubRequest({
groupPk,
revokeTokenHex: [token],
timestamp: NetworkTime.now(),
secretKey: group.secretKey,
});
const sequenceResult = await GroupSync.pushChangesToGroupSwarmIfNeeded({
if (memberObj.supplement) {
const encryptedSupplementKeys = await MetaGroupWrapperActions.generateSupplementKeys(
groupPk,
unrevokeSubRequest,
extraStoreRequests: [],
allow401s: false,
timeoutMs: 10 * DURATION.SECONDS,
[member]
);
supplementalKeysSubRequest = StoreGroupRequestFactory.makeStoreGroupKeysSubRequest({
group,
encryptedSupplementKeys,
});
window?.inboxStore?.dispatch(
groupInfoActions.refreshGroupDetailsFromWrapper({ groupPk }) as any
}
const token = await MetaGroupWrapperActions.swarmSubAccountToken(groupPk, member);
const unrevokeSubRequest = new SubaccountUnrevokeSubRequest({
groupPk,
tokensHex: [token],
timestamp: NetworkTime.now(),
secretKey: group.secretKey,
});
const sequenceResult = await GroupSync.pushChangesToGroupSwarmIfNeeded({
groupPk,
unrevokeSubRequest,
supplementalKeysSubRequest,
extraStoreRequests: [],
allow401s: false,
timeoutMs: 10 * DURATION.SECONDS,
});
window?.inboxStore?.dispatch(
groupInfoActions.refreshGroupDetailsFromWrapper({ groupPk }) as any
);
if (sequenceResult !== RunJobResult.Success) {
window.log.warn(
`GroupInvite: GroupSync.pushChangesToGroupSwarmIfNeeded failed after ${Date.now() - start}ms`
);
if (sequenceResult !== RunJobResult.Success) {
window.log.warn(
`GroupInvite: GroupSync.pushChangesToGroupSwarmIfNeeded failed after ${Date.now() - start}ms`
);
await LibSessionUtil.saveDumpsToDb(groupPk);
await LibSessionUtil.saveDumpsToDb(groupPk);
throw new Error(
'GroupInviteJob: SubaccountUnrevokeSubRequest push() did not return success'
);
}
throw new Error(
'GroupInviteJob: SubaccountUnrevokeSubRequest push() did not return success'
);
}
const inviteDetails = inviteAsAdmin
@ -285,7 +295,7 @@ class GroupInviteJob extends PersistedJob<GroupInvitePersistedData> {
);
}
updateFailedStateForMember(groupPk, member, failed);
debounceFailedStateForMember(groupPk, member, failed);
window?.inboxStore?.dispatch(
groupInfoActions.refreshGroupDetailsFromWrapper({ groupPk }) as any
@ -325,9 +335,14 @@ class GroupInviteJob extends PersistedJob<GroupInvitePersistedData> {
export const GroupInvite = {
GroupInviteJob,
addJob,
debounceFailedStateForMember,
};
function updateFailedStateForMember(groupPk: GroupPubkeyType, member: PubkeyType, failed: boolean) {
function debounceFailedStateForMember(
groupPk: GroupPubkeyType,
member: PubkeyType,
failed: boolean
) {
let thisGroupFailure = invitesFailed.get(groupPk);
if (!failed) {

@ -546,6 +546,9 @@ async function handleWithHistoryMembers({
});
// a group invite job will be added to the queue
await MetaGroupWrapperActions.memberSetInviteNotSent(groupPk, member);
await MetaGroupWrapperActions.memberSetSupplement(groupPk, member);
// update the in-memory failed state, so that if we fail again to send that invite, the toast is shown again
GroupInvite.debounceFailedStateForMember(groupPk, member, false);
}
const encryptedSupplementKeys = withHistory.length
? await MetaGroupWrapperActions.generateSupplementKeys(groupPk, withHistory)
@ -695,9 +698,12 @@ async function handleMemberAddedFromUI({
});
if (sequenceResult !== RunJobResult.Success) {
await LibSessionUtil.saveDumpsToDb(groupPk);
window.log.warn(
`handleMemberAddedFromUI: pushChangesToGroupSwarmIfNeeded for ${ed25519Str(groupPk)} did not return success`
);
// throwing so we handle the reset state in the catch below
throw new Error(
'handleMemberAddedFromUI: pushChangesToGroupSwarmIfNeeded did not return success'
`handleMemberAddedFromUI: pushChangesToGroupSwarmIfNeeded for ${ed25519Str(groupPk)} did not return success`
);
}
} catch (e) {
@ -705,6 +711,21 @@ async function handleMemberAddedFromUI({
'handleMemberAddedFromUI: pushChangesToGroupSwarmIfNeeded failed with:',
e.message
);
try {
const merged = withHistory.concat(withoutHistory);
for (let index = 0; index < merged.length; index++) {
await MetaGroupWrapperActions.memberSetInviteFailed(groupPk, merged[index]);
// this gets reset once we do send an invite to that user
GroupInvite.debounceFailedStateForMember(groupPk, merged[index], true);
}
} catch (e2) {
window.log.warn(
'handleMemberAddedFromUI: marking members invite failed, failed with:',
e2.message
);
}
return false;
}
// schedule send invite details, auth signature, etc. to the new users
@ -716,6 +737,7 @@ async function handleMemberAddedFromUI({
});
await convo.commit();
return true;
}
/**
@ -1484,8 +1506,6 @@ async function scheduleGroupInviteJobs(
const merged = uniq(concat(withHistory, withoutHistory));
for (let index = 0; index < merged.length; index++) {
const member = merged[index];
// Note: forceUnrevoke is false, because `scheduleGroupInviteJobs` is always called after we've done
// a batch unrevoke of all the members' pk
await GroupInvite.addJob({ groupPk, member, inviteAsAdmin, forceUnrevoke: false });
await GroupInvite.addJob({ groupPk, member, inviteAsAdmin });
}
}

@ -26,6 +26,7 @@ function emptyMember(pubkeyHex: PubkeyType): GroupMemberGet {
},
nominatedAdmin: false,
pubkeyHex,
supplement: false,
};
}
@ -155,7 +156,14 @@ describe('libsession_metagroup', () => {
const memberCreated = metaGroupWrapper.memberGetOrConstruct(member);
console.info('Object.keys(memberCreated) ', JSON.stringify(Object.keys(memberCreated)));
expect(Object.keys(memberCreated).sort()).to.be.deep.eq(
['pubkeyHex', 'name', 'profilePicture', 'memberStatus', 'nominatedAdmin'].sort(), // if you change this value, also make sure you add a test, testing that new field, below
[
'pubkeyHex',
'name',
'profilePicture',
'memberStatus',
'nominatedAdmin',
'supplement',
].sort(), // if you change this value, also make sure you add a test, testing that new field, below
'this test is designed to fail if you need to add tests to test a new field of libsession'
);
});
@ -278,6 +286,19 @@ describe('libsession_metagroup', () => {
expect(metaGroupWrapper.memberGetAll()[0]).to.be.deep.eq(expected);
});
it('can add via supplement set', () => {
metaGroupWrapper.memberConstructAndSet(member);
metaGroupWrapper.memberSetSupplement(member);
expect(metaGroupWrapper.memberGetAll().length).to.be.deep.eq(1);
const expected = {
...emptyMember(member),
memberStatus: 'INVITE_SENDING', // invite_sending is the default state
supplement: true,
};
expect(metaGroupWrapper.memberGetAll()[0]).to.be.deep.eq(expected);
});
it('can add via admin set', () => {
metaGroupWrapper.memberConstructAndSet(member);
metaGroupWrapper.memberSetPromotionAccepted(member);

@ -657,6 +657,12 @@ export const MetaGroupWrapperActions: MetaGroupWrapperActionsCalls = {
pubkeyHex,
name,
]) as Promise<ReturnType<MetaGroupWrapperActionsCalls['memberSetNameTruncated']>>,
memberSetSupplement: async (groupPk: GroupPubkeyType, pubkeyHex: PubkeyType) =>
callLibSessionWorker([
`MetaGroupConfig-${groupPk}`,
'memberSetSupplement',
pubkeyHex,
]) as Promise<ReturnType<MetaGroupWrapperActionsCalls['memberSetSupplement']>>,
memberSetProfilePicture: async (
groupPk: GroupPubkeyType,
pubkeyHex: PubkeyType,

@ -4944,9 +4944,9 @@ levn@~0.3.0:
prelude-ls "~1.1.2"
type-check "~0.3.2"
"libsession_util_nodejs@https://github.com/session-foundation/libsession-util-nodejs/releases/download/v0.4.12/libsession_util_nodejs-v0.4.12.tar.gz":
version "0.4.12"
resolved "https://github.com/session-foundation/libsession-util-nodejs/releases/download/v0.4.12/libsession_util_nodejs-v0.4.12.tar.gz#6f0eae5c81f9a3e5101e038dbb7c82a9d50bfb7a"
"libsession_util_nodejs@https://github.com/session-foundation/libsession-util-nodejs/releases/download/v0.4.15/libsession_util_nodejs-v0.4.15.tar.gz":
version "0.4.15"
resolved "https://github.com/session-foundation/libsession-util-nodejs/releases/download/v0.4.15/libsession_util_nodejs-v0.4.15.tar.gz#de0e90e14327e60d81d2a6941bcd0af33fcfed82"
dependencies:
cmake-js "7.2.1"
node-addon-api "^6.1.0"
@ -6859,9 +6859,9 @@ semver@^6.0.0, semver@^6.2.0, semver@^6.3.0, semver@^6.3.1:
integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==
semver@^7.1.2, semver@^7.3.2, semver@^7.3.4, semver@^7.3.5, semver@^7.3.7, semver@^7.3.8, semver@^7.5.4:
version "7.6.3"
resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.3.tgz#980f7b5550bc175fb4dc09403085627f9eb33143"
integrity sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==
version "7.7.1"
resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.1.tgz#abd5098d82b18c6c81f6074ff2647fd3e7220c9f"
integrity sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==
serialize-error@^7.0.1:
version "7.0.1"

Loading…
Cancel
Save