From c3b0fe902e7e38f451d77262c8a9f14e17fee8f7 Mon Sep 17 00:00:00 2001 From: wakamex Date: Wed, 14 Oct 2026 05:56:36 -0400 Subject: [PATCH] Accept group invitations addressed to the PNI (#2146) An invitation by phone number lists the account's PNI as the pending member. acceptInvite compared pending members only with the self ACI address and failed with 'Cannot find service ID for self to accept invite'. It now matches either the ACI or the PNI, makes the change as the ACI, and for a PNI promotion decrypts the server's signed change, since the locally built action has no member ciphertexts to decrypt. --- .../signal/manager/helper/GroupV2Helper.java | 26 ++++++++++++++++--- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/GroupV2Helper.java b/lib/src/main/java/org/asamk/signal/manager/helper/GroupV2Helper.java index 0ed6d4b2..456c56e4 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/GroupV2Helper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/GroupV2Helper.java @@ -480,15 +480,16 @@ class GroupV2Helper { } // We need to accept the invite with the ACI or PNI that was used for the invitation - final var selfAddress = context.getRecipientHelper().resolveSignalServiceAddress(selfRecipientId); + final var account = context.getAccount(); final var selfServiceId = groupInfoV2.getPendingMemberAddresses() .stream() - .filter(s -> s.matches(selfAddress)) + .map(SignalServiceAddress::getServiceId) + .filter(s -> s.equals(account.getAci()) || s.equals(account.getPni())) .findFirst(); if (selfServiceId.isEmpty()) { throw new IOException("Cannot find service ID for self to accept invite"); } - final var serviceId = selfServiceId.get().getServiceId(); + final var serviceId = selfServiceId.get(); final GroupChange.Actions.Builder change; if (serviceId instanceof ACI) { @@ -497,7 +498,8 @@ class GroupV2Helper { change = groupOperations.createAcceptPniInviteChange(profileKeyCredential); } - change.sourceUserId(serviceId.toByteString()); + // The change is made by the account's ACI, also when it promotes a PNI invitation + change.sourceUserId(account.getAci().toByteString()); return commitChange(groupInfoV2, change); } @@ -634,6 +636,22 @@ class GroupV2Helper { final DecryptedGroupChange decryptedChange; final DecryptedGroup decryptedGroupState; + if (!changeActions.promote_members_pending_pni_aci_profile_key.isEmpty()) { + // The server fills in the ACI and PNI ciphertexts of a PNI promotion, so decrypt its signed change + final var signedGroupChange = dependencies.getGroupsV2Api() + .patchGroup(changeActions, getGroupAuthForToday(groupSecretParams), Optional.empty()); + final var groupId = groupSecretParams.getPublicParams().getGroupIdentifier(); + try { + decryptedChange = groupOperations.decryptChange(signedGroupChange.group_change, + DecryptChangeVerificationMode.verify(groupId)).orElseThrow(); + decryptedGroupState = DecryptedGroupUtil.apply(previousGroupState, decryptedChange); + } catch (VerificationFailedException | InvalidGroupStateException | NotAbleToApplyGroupV2ChangeException e) { + throw new IOException(e); + } + groupInfoV2.setGroup(decryptedGroupState); + return new Pair<>(decryptedGroupState, signedGroupChange); + } + try { decryptedChange = groupOperations.decryptChange(changeActions, getSelfAci()); decryptedGroupState = DecryptedGroupUtil.apply(previousGroupState, decryptedChange);