diff --git a/package.json b/package.json index 8448927cd..0a1e21efd 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/ts/components/MemberListItem.tsx b/ts/components/MemberListItem.tsx index 972fbe19f..f911708dc 100644 --- a/ts/components/MemberListItem.tsx +++ b/ts/components/MemberListItem.tsx @@ -277,7 +277,6 @@ const ResendButton = ({ groupPk, pubkey }: { pubkey: PubkeyType; groupPk: GroupP groupPk, member: pubkey, inviteAsAdmin: member.nominatedAdmin, - forceUnrevoke: true, }); }} /> diff --git a/ts/interactions/conversationInteractions.ts b/ts/interactions/conversationInteractions.ts index 68643a2e0..2b0892926 100644 --- a/ts/interactions/conversationInteractions.ts +++ b/ts/interactions/conversationInteractions.ts @@ -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 }); } } diff --git a/ts/session/apis/snode_api/SnodeRequestTypes.ts b/ts/session/apis/snode_api/SnodeRequestTypes.ts index dd3bc23f9..ab0e86c19 100644 --- a/ts/session/apis/snode_api/SnodeRequestTypes.ts +++ b/ts/session/apis/snode_api/SnodeRequestTypes.ts @@ -403,24 +403,22 @@ abstract class AbstractRevokeSubRequest< > extends SnodeAPISubRequest { public readonly destination: GroupPubkeyType; public readonly timestamp: number; - public readonly revokeTokenHex: Array; + public readonly tokensHex: Array; protected readonly adminSecretKey: Uint8Array; constructor({ groupPk, timestamp, - revokeTokenHex, + tokensHex, secretKey, method, - }: WithGroupPubkey & - WithTimestamp & - WithSecretKey & { revokeTokenHex: Array; method: T }) { + }: WithGroupPubkey & WithTimestamp & WithSecretKey & { tokensHex: Array; 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, }, }; diff --git a/ts/session/apis/snode_api/revokeSubaccount.ts b/ts/session/apis/snode_api/revokeSubaccount.ts index 5a296a66e..204604a1d 100644 --- a/ts/session/apis/snode_api/revokeSubaccount.ts +++ b/ts/session/apis/snode_api/revokeSubaccount.ts @@ -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, }) diff --git a/ts/session/utils/job_runners/PersistedJob.ts b/ts/session/utils/job_runners/PersistedJob.ts index c52a4ccc5..290f285d4 100644 --- a/ts/session/utils/job_runners/PersistedJob.ts +++ b/ts/session/utils/job_runners/PersistedJob.ts @@ -43,7 +43,6 @@ export interface GroupInvitePersistedData extends PersistedJobData { groupPk: GroupPubkeyType; member: PubkeyType; inviteAsAdmin: boolean; - forceUnrevoke: boolean; } export interface GroupPromotePersistedData extends PersistedJobData { diff --git a/ts/session/utils/job_runners/jobs/GroupInviteJob.ts b/ts/session/utils/job_runners/jobs/GroupInviteJob.ts index 4a7029592..4a9a62bc6 100644 --- a/ts/session/utils/job_runners/jobs/GroupInviteJob.ts +++ b/ts/session/utils/job_runners/jobs/GroupInviteJob.ts @@ -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 { nextAttemptTimestamp, maxAttempts, currentRetry, - forceUnrevoke, identifier, - }: Pick & + }: Pick & Partial< Pick< GroupInvitePersistedData, @@ -161,7 +157,6 @@ class GroupInviteJob extends PersistedJob { member, groupPk, inviteAsAdmin, - forceUnrevoke, delayBetweenRetries: defaultMsBetweenRetries, maxAttempts: isNumber(maxAttempts) ? maxAttempts : defaultMaxAttempts, nextAttemptTimestamp: nextAttemptTimestamp || Date.now() + defaultMsBetweenRetries, @@ -187,36 +182,51 @@ class GroupInviteJob extends PersistedJob { 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 @@ -328,7 +338,11 @@ export const GroupInvite = { debounceFailedStateForMember, }; -function debounceFailedStateForMember(groupPk: GroupPubkeyType, member: PubkeyType, failed: boolean) { +function debounceFailedStateForMember( + groupPk: GroupPubkeyType, + member: PubkeyType, + failed: boolean +) { let thisGroupFailure = invitesFailed.get(groupPk); if (!failed) { diff --git a/ts/state/ducks/metaGroups.ts b/ts/state/ducks/metaGroups.ts index 6257a2c62..ff8d3a482 100644 --- a/ts/state/ducks/metaGroups.ts +++ b/ts/state/ducks/metaGroups.ts @@ -546,6 +546,7 @@ 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); } @@ -1505,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 }); } } diff --git a/ts/test/session/unit/libsession_wrapper/libsession_wrapper_metagroup_test.ts b/ts/test/session/unit/libsession_wrapper/libsession_wrapper_metagroup_test.ts index 410371aa4..1ddac14ea 100644 --- a/ts/test/session/unit/libsession_wrapper/libsession_wrapper_metagroup_test.ts +++ b/ts/test/session/unit/libsession_wrapper/libsession_wrapper_metagroup_test.ts @@ -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); diff --git a/ts/webworker/workers/browser/libsession_worker_interface.ts b/ts/webworker/workers/browser/libsession_worker_interface.ts index 896b9ad82..b178ffc5c 100644 --- a/ts/webworker/workers/browser/libsession_worker_interface.ts +++ b/ts/webworker/workers/browser/libsession_worker_interface.ts @@ -657,6 +657,12 @@ export const MetaGroupWrapperActions: MetaGroupWrapperActionsCalls = { pubkeyHex, name, ]) as Promise>, + memberSetSupplement: async (groupPk: GroupPubkeyType, pubkeyHex: PubkeyType) => + callLibSessionWorker([ + `MetaGroupConfig-${groupPk}`, + 'memberSetSupplement', + pubkeyHex, + ]) as Promise>, memberSetProfilePicture: async ( groupPk: GroupPubkeyType, pubkeyHex: PubkeyType, diff --git a/yarn.lock b/yarn.lock index 73bc16ebd..10e898954 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"