diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/ProfileController.java b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/ProfileController.java index 6e2db8eb8..664882962 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/ProfileController.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/ProfileController.java @@ -163,9 +163,9 @@ public class ProfileController { public Response setProfile(@Auth AuthenticatedAccount auth, @NotNull @Valid CreateProfileRequest request) { final Optional currentProfile = profilesManager.get(auth.getAccount().getUuid(), - request.getVersion()); + request.version()); - if (request.getPaymentAddress() != null && request.getPaymentAddress().length != 0) { + if (request.paymentAddress() != null && request.paymentAddress().length != 0) { final boolean hasDisallowedPrefix = dynamicConfigurationManager.getConfiguration().getPaymentsConfiguration().getDisallowedPrefixes().stream() .anyMatch(prefix -> auth.getAccount().getNumber().startsWith(prefix)); @@ -189,13 +189,14 @@ public class ProfileController { profilesManager.set(auth.getAccount().getUuid(), new VersionedProfile( - request.getVersion(), - request.getName(), + request.version(), + request.name(), avatar, - request.getAboutEmoji(), - request.getAbout(), - request.getPaymentAddress(), - request.getCommitment().serialize())); + request.aboutEmoji(), + request.about(), + request.paymentAddress(), + request.phoneNumberSharing(), + request.commitment().serialize())); if (request.getAvatarChange() != CreateProfileRequest.AvatarChange.UNCHANGED) { currentAvatar.ifPresent(s -> s3client.deleteObject(DeleteObjectRequest.builder() @@ -204,13 +205,13 @@ public class ProfileController { .build())); } - final List updatedBadges = request.getBadges() + final List updatedBadges = request.badges() .map(badges -> ProfileHelper.mergeBadgeIdsWithExistingAccountBadges(clock, badgeConfigurationMap, badges, auth.getAccount().getBadges())) .orElseGet(() -> auth.getAccount().getBadges()); accountsManager.update(auth.getAccount(), a -> { a.setBadges(clock, updatedBadges); - a.setCurrentProfileVersion(request.getVersion()); + a.setCurrentProfileVersion(request.version()); }); if (request.getAvatarChange() == CreateProfileRequest.AvatarChange.UPDATE) { @@ -411,6 +412,7 @@ public class ProfileController { final byte[] about = maybeProfile.map(VersionedProfile::about).orElse(null); final byte[] aboutEmoji = maybeProfile.map(VersionedProfile::aboutEmoji).orElse(null); final String avatar = maybeProfile.map(VersionedProfile::avatar).orElse(null); + final byte[] phoneNumberSharing = maybeProfile.map(VersionedProfile::phoneNumberSharing).orElse(null); // Allow requests where either the version matches the latest version on Account or the latest version on Account // is empty to read the payment address. @@ -421,7 +423,7 @@ public class ProfileController { return new VersionedProfileResponse( buildBaseProfileResponseForAccountIdentity(account, isSelf, containerRequestContext), - name, about, aboutEmoji, avatar, paymentAddress); + name, about, aboutEmoji, avatar, paymentAddress, phoneNumberSharing); } private BaseProfileResponse buildBaseProfileResponseForAccountIdentity(final Account account, diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/entities/CreateProfileRequest.java b/service/src/main/java/org/whispersystems/textsecuregcm/entities/CreateProfileRequest.java index 068a6a5a2..ea584cb9b 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/entities/CreateProfileRequest.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/entities/CreateProfileRequest.java @@ -10,91 +10,62 @@ import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonSerialize; import java.util.List; import java.util.Optional; -import javax.annotation.Nullable; import javax.validation.constraints.NotEmpty; import javax.validation.constraints.NotNull; import org.signal.libsignal.zkgroup.profiles.ProfileKeyCommitment; import org.whispersystems.textsecuregcm.util.ByteArrayBase64WithPaddingAdapter; import org.whispersystems.textsecuregcm.util.ExactlySize; -public class CreateProfileRequest { +public record CreateProfileRequest( + @JsonProperty + @NotNull + @JsonDeserialize(using = ProfileKeyCommitmentAdapter.Deserializing.class) + @JsonSerialize(using = ProfileKeyCommitmentAdapter.Serializing.class) + ProfileKeyCommitment commitment, @JsonProperty @NotEmpty - private String version; + String version, @JsonProperty @JsonSerialize(using = ByteArrayBase64WithPaddingAdapter.Serializing.class) @JsonDeserialize(using = ByteArrayBase64WithPaddingAdapter.Deserializing.class) @ExactlySize({81, 285}) - private byte[] name; - - @JsonProperty - private boolean avatar; - - @JsonProperty - private boolean sameAvatar; + byte[] name, @JsonProperty @JsonSerialize(using = ByteArrayBase64WithPaddingAdapter.Serializing.class) @JsonDeserialize(using = ByteArrayBase64WithPaddingAdapter.Deserializing.class) @ExactlySize({0, 60}) - private byte[] aboutEmoji; + byte[] aboutEmoji, @JsonProperty @JsonSerialize(using = ByteArrayBase64WithPaddingAdapter.Serializing.class) @JsonDeserialize(using = ByteArrayBase64WithPaddingAdapter.Deserializing.class) @ExactlySize({0, 156, 282, 540}) - private byte[] about; + byte[] about, @JsonProperty @JsonSerialize(using = ByteArrayBase64WithPaddingAdapter.Serializing.class) @JsonDeserialize(using = ByteArrayBase64WithPaddingAdapter.Deserializing.class) @ExactlySize({0, 582}) - private byte[] paymentAddress; + byte[] paymentAddress, + + @JsonProperty("avatar") + boolean hasAvatar, @JsonProperty - @Nullable - private List badgeIds; + boolean sameAvatar, + + @JsonProperty("badgeIds") + Optional> badges, @JsonProperty - @NotNull - @JsonDeserialize(using = ProfileKeyCommitmentAdapter.Deserializing.class) - @JsonSerialize(using = ProfileKeyCommitmentAdapter.Serializing.class) - private ProfileKeyCommitment commitment; - - public CreateProfileRequest() { - } - - public CreateProfileRequest( - final ProfileKeyCommitment commitment, final String version, final byte[] name, final byte[] aboutEmoji, final byte[] about, - final byte[] paymentAddress, final boolean wantsAvatar, final boolean sameAvatar, final List badgeIds) { - this.commitment = commitment; - this.version = version; - this.name = name; - this.aboutEmoji = aboutEmoji; - this.about = about; - this.paymentAddress = paymentAddress; - this.avatar = wantsAvatar; - this.sameAvatar = sameAvatar; - this.badgeIds = badgeIds; - } - - public ProfileKeyCommitment getCommitment() { - return commitment; - } - - public String getVersion() { - return version; - } - - public byte[] getName() { - return name; - } - - public boolean hasAvatar() { - return avatar; - } + @JsonSerialize(using = ByteArrayBase64WithPaddingAdapter.Serializing.class) + @JsonDeserialize(using = ByteArrayBase64WithPaddingAdapter.Deserializing.class) + @ExactlySize({0, 29}) + byte[] phoneNumberSharing +) { public enum AvatarChange { UNCHANGED, @@ -112,19 +83,4 @@ public class CreateProfileRequest { return AvatarChange.UNCHANGED; } - public byte[] getAboutEmoji() { - return aboutEmoji; - } - - public byte[] getAbout() { - return about; - } - - public byte[] getPaymentAddress() { - return paymentAddress; - } - - public Optional> getBadges() { - return Optional.ofNullable(badgeIds); - } } diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/entities/VersionedProfileResponse.java b/service/src/main/java/org/whispersystems/textsecuregcm/entities/VersionedProfileResponse.java index 3b8ed60dc..0aec22d94 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/entities/VersionedProfileResponse.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/entities/VersionedProfileResponse.java @@ -11,6 +11,8 @@ import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonSerialize; import org.whispersystems.textsecuregcm.util.ByteArrayBase64WithPaddingAdapter; +// Note, this class cannot be converted into a record because @JsonUnwrapped does not work with records. +// https://github.com/FasterXML/jackson-databind/issues/1467 public class VersionedProfileResponse { @JsonUnwrapped @@ -39,6 +41,11 @@ public class VersionedProfileResponse { @JsonDeserialize(using = ByteArrayBase64WithPaddingAdapter.Deserializing.class) private byte[] paymentAddress; + @JsonProperty + @JsonSerialize(using = ByteArrayBase64WithPaddingAdapter.Serializing.class) + @JsonDeserialize(using = ByteArrayBase64WithPaddingAdapter.Deserializing.class) + private byte[] phoneNumberSharing; + public VersionedProfileResponse() { } @@ -47,7 +54,8 @@ public class VersionedProfileResponse { final byte[] about, final byte[] aboutEmoji, final String avatar, - final byte[] paymentAddress) { + final byte[] paymentAddress, + final byte[] phoneNumberSharing) { this.baseProfileResponse = baseProfileResponse; this.name = name; @@ -55,6 +63,7 @@ public class VersionedProfileResponse { this.aboutEmoji = aboutEmoji; this.avatar = avatar; this.paymentAddress = paymentAddress; + this.phoneNumberSharing = phoneNumberSharing; } public BaseProfileResponse getBaseProfileResponse() { @@ -80,4 +89,8 @@ public class VersionedProfileResponse { public byte[] getPaymentAddress() { return paymentAddress; } + + public byte[] getPhoneNumberSharing() { + return phoneNumberSharing; + } } diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/grpc/ProfileGrpcHelper.java b/service/src/main/java/org/whispersystems/textsecuregcm/grpc/ProfileGrpcHelper.java index db1f6cd92..4469556a2 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/grpc/ProfileGrpcHelper.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/grpc/ProfileGrpcHelper.java @@ -48,6 +48,7 @@ public class ProfileGrpcHelper { maybeProfile.map(VersionedProfile::about).map(ByteString::copyFrom).ifPresent(responseBuilder::setAbout); maybeProfile.map(VersionedProfile::aboutEmoji).map(ByteString::copyFrom).ifPresent(responseBuilder::setAboutEmoji); maybeProfile.map(VersionedProfile::avatar).ifPresent(responseBuilder::setAvatar); + maybeProfile.map(VersionedProfile::phoneNumberSharing).map(ByteString::copyFrom).ifPresent(responseBuilder::setPhoneNumberSharing); // Allow requests where either the version matches the latest version on Account or the latest version on Account // is empty to read the payment address. diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/grpc/ProfileGrpcService.java b/service/src/main/java/org/whispersystems/textsecuregcm/grpc/ProfileGrpcService.java index 7c26a4582..6f27630f2 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/grpc/ProfileGrpcService.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/grpc/ProfileGrpcService.java @@ -141,6 +141,7 @@ public class ProfileGrpcService extends ReactorProfileGrpc.ProfileImplBase { request.getAboutEmoji().toByteArray(), request.getAbout().toByteArray(), request.getPaymentAddress().toByteArray(), + request.getPhoneNumberSharing().toByteArray(), request.getCommitment().toByteArray()))); final List> updates = new ArrayList<>(2); diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/storage/Profiles.java b/service/src/main/java/org/whispersystems/textsecuregcm/storage/Profiles.java index c82d62adf..2c2f7544a 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/storage/Profiles.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/storage/Profiles.java @@ -62,6 +62,9 @@ public class Profiles { // Payment address; byte array private static final String ATTR_PAYMENT_ADDRESS = "P"; + // Phone number sharing setting; byte array + private static final String ATTR_PHONE_NUMBER_SHARING = "S"; + // Commitment; byte array private static final String ATTR_COMMITMENT = "C"; @@ -71,7 +74,8 @@ public class Profiles { "#avatar", ATTR_AVATAR, "#about", ATTR_ABOUT, "#aboutEmoji", ATTR_EMOJI, - "#paymentAddress", ATTR_PAYMENT_ADDRESS); + "#paymentAddress", ATTR_PAYMENT_ADDRESS, + "#phoneNumberSharing", ATTR_PHONE_NUMBER_SHARING); private static final Timer SET_PROFILES_TIMER = Metrics.timer(name(Profiles.class, "set")); private static final Timer GET_PROFILE_TIMER = Metrics.timer(name(Profiles.class, "get")); @@ -154,6 +158,12 @@ public class Profiles { deletedAttributes.add("paymentAddress"); } + if (profile.phoneNumberSharing() != null) { + updatedAttributes.add("phoneNumberSharing"); + } else { + deletedAttributes.add("phoneNumberSharing"); + } + final StringBuilder updateExpressionBuilder = new StringBuilder( "SET #commitment = if_not_exists(#commitment, :commitment)"); @@ -201,6 +211,9 @@ public class Profiles { expressionValues.put(":paymentAddress", AttributeValues.fromByteArray(profile.paymentAddress())); } + if (profile.phoneNumberSharing() != null) { + expressionValues.put(":phoneNumberSharing", AttributeValues.fromByteArray(profile.phoneNumberSharing())); + } return expressionValues; } @@ -235,6 +248,7 @@ public class Profiles { getBytes(item, ATTR_EMOJI), getBytes(item, ATTR_ABOUT), getBytes(item, ATTR_PAYMENT_ADDRESS), + getBytes(item, ATTR_PHONE_NUMBER_SHARING), AttributeValues.getByteArray(item, ATTR_COMMITMENT, null)); } diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/storage/VersionedProfile.java b/service/src/main/java/org/whispersystems/textsecuregcm/storage/VersionedProfile.java index 2535c9b9f..bcdd15670 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/storage/VersionedProfile.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/storage/VersionedProfile.java @@ -5,14 +5,10 @@ package org.whispersystems.textsecuregcm.storage; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonSerialize; import org.whispersystems.textsecuregcm.util.ByteArrayAdapter; import org.whispersystems.textsecuregcm.util.ByteArrayBase64WithPaddingAdapter; -import java.util.Arrays; -import java.util.Objects; public record VersionedProfile (String version, @JsonSerialize(using = ByteArrayBase64WithPaddingAdapter.Serializing.class) @@ -33,6 +29,10 @@ public record VersionedProfile (String version, @JsonDeserialize(using = ByteArrayBase64WithPaddingAdapter.Deserializing.class) byte[] paymentAddress, + @JsonSerialize(using = ByteArrayAdapter.Serializing.class) + @JsonDeserialize(using = ByteArrayAdapter.Deserializing.class) + byte[] phoneNumberSharing, + @JsonSerialize(using = ByteArrayAdapter.Serializing.class) @JsonDeserialize(using = ByteArrayAdapter.Deserializing.class) byte[] commitment) {} diff --git a/service/src/main/proto/org/signal/chat/profile.proto b/service/src/main/proto/org/signal/chat/profile.proto index 504e8807c..d7e5d149b 100644 --- a/service/src/main/proto/org/signal/chat/profile.proto +++ b/service/src/main/proto/org/signal/chat/profile.proto @@ -132,6 +132,10 @@ message SetProfileRequest { * A list of badge IDs associated with the profile. */ repeated string badge_ids = 7; + /** + * The ciphertext of the phone-number sharing setting on the profile. 29-byte encrypted boolean. + */ + bytes phone_number_sharing = 8; /** * The profile key commitment. Used to issue a profile key credential response. * Must be set on the request. @@ -189,6 +193,10 @@ message GetVersionedProfileResponse { * The ciphertext of the MobileCoin wallet ID on the profile. */ bytes payment_address = 5; + /** + * The ciphertext of the phone-number sharing setting on the profile. + */ + bytes phone_number_sharing = 6; } message GetUnversionedProfileRequest { diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/controllers/ProfileControllerTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/controllers/ProfileControllerTest.java index f4c2cce4b..ca91b2d98 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/controllers/ProfileControllerTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/controllers/ProfileControllerTest.java @@ -227,10 +227,11 @@ class ProfileControllerTest { final byte[] name = TestRandomUtil.nextBytes(81); final byte[] emoji = TestRandomUtil.nextBytes(60); final byte[] about = TestRandomUtil.nextBytes(156); + final byte[] phoneNumberSharing = TestRandomUtil.nextBytes(29); when(profilesManager.get(eq(AuthHelper.VALID_UUID), eq("someversion"))).thenReturn(Optional.empty()); when(profilesManager.get(eq(AuthHelper.VALID_UUID_TWO), eq("validversion"))).thenReturn(Optional.of(new VersionedProfile( - "validversion", name, "profiles/validavatar", emoji, about, null, "validcommitmnet".getBytes()))); + "validversion", name, "profiles/validavatar", emoji, about, null, phoneNumberSharing, "validcommitment".getBytes()))); clearInvocations(rateLimiter); clearInvocations(accountsManager); @@ -419,7 +420,7 @@ class ProfileControllerTest { .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) .put(Entity.entity(new CreateProfileRequest(commitment, "someversion", name, null, null, - null, true, false, List.of()), MediaType.APPLICATION_JSON_TYPE), ProfileAvatarUploadAttributes.class); + null, true, false, Optional.of(List.of()), null), MediaType.APPLICATION_JSON_TYPE), ProfileAvatarUploadAttributes.class); final ArgumentCaptor profileArgumentCaptor = ArgumentCaptor.forClass(VersionedProfile.class); @@ -446,7 +447,7 @@ class ProfileControllerTest { .request() .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) .put(Entity.entity(new CreateProfileRequest(commitment, "someversion", name, - null, null, null, true, false, List.of()), MediaType.APPLICATION_JSON_TYPE))) { + null, null, null, true, false, Optional.of(List.of()), null), MediaType.APPLICATION_JSON_TYPE))) { assertThat(response.getStatus()).isEqualTo(422); } @@ -456,6 +457,7 @@ class ProfileControllerTest { void testSetProfileWithoutAvatarUpload() throws InvalidInputException { final ProfileKeyCommitment commitment = new ProfileKey(new byte[32]).getCommitment(new ServiceId.Aci(AuthHelper.VALID_UUID)); final byte[] name = TestRandomUtil.nextBytes(81); + final byte[] phoneNumberSharing = TestRandomUtil.nextBytes(29); clearInvocations(AuthHelper.VALID_ACCOUNT_TWO); @@ -464,7 +466,7 @@ class ProfileControllerTest { .request() .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID_TWO, AuthHelper.VALID_PASSWORD_TWO)) .put(Entity.entity(new CreateProfileRequest(commitment, "anotherversion", name, null, null, - null, false, false, List.of()), MediaType.APPLICATION_JSON_TYPE))) { + null, false, false, Optional.of(List.of()), phoneNumberSharing), MediaType.APPLICATION_JSON_TYPE))) { assertThat(response.getStatus()).isEqualTo(200); assertThat(response.hasEntity()).isFalse(); @@ -482,6 +484,7 @@ class ProfileControllerTest { assertThat(profileArgumentCaptor.getValue().name()).isEqualTo(name); assertThat(profileArgumentCaptor.getValue().aboutEmoji()).isNull(); assertThat(profileArgumentCaptor.getValue().about()).isNull(); + assertThat(profileArgumentCaptor.getValue().phoneNumberSharing()).isEqualTo(phoneNumberSharing); } } @@ -496,7 +499,7 @@ class ProfileControllerTest { .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID_TWO, AuthHelper.VALID_PASSWORD_TWO)) .put(Entity.entity(new CreateProfileRequest(commitment, "validversion", name, null, null, - null, true, false, List.of()), MediaType.APPLICATION_JSON_TYPE), ProfileAvatarUploadAttributes.class); + null, true, false, Optional.of(List.of()), null), MediaType.APPLICATION_JSON_TYPE), ProfileAvatarUploadAttributes.class); final ArgumentCaptor profileArgumentCaptor = ArgumentCaptor.forClass(VersionedProfile.class); @@ -522,7 +525,7 @@ class ProfileControllerTest { .request() .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID_TWO, AuthHelper.VALID_PASSWORD_TWO)) .put(Entity.entity(new CreateProfileRequest(commitment, "validversion", name, - null, null, null, false, false, List.of()), MediaType.APPLICATION_JSON_TYPE))) { + null, null, null, false, false, Optional.of(List.of()), null), MediaType.APPLICATION_JSON_TYPE))) { assertThat(response.getStatus()).isEqualTo(200); assertThat(response.hasEntity()).isFalse(); @@ -552,7 +555,7 @@ class ProfileControllerTest { .request() .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID_TWO, AuthHelper.VALID_PASSWORD_TWO)) .put(Entity.entity(new CreateProfileRequest(commitment, "validversion", name, - null, null, null, true, true, List.of()), MediaType.APPLICATION_JSON_TYPE))) { + null, null, null, true, true, Optional.of(List.of()), null), MediaType.APPLICATION_JSON_TYPE))) { assertThat(response.getStatus()).isEqualTo(200); assertThat(response.hasEntity()).isFalse(); @@ -583,7 +586,7 @@ class ProfileControllerTest { .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID_TWO, AuthHelper.VALID_PASSWORD_TWO)) .put(Entity.entity(new CreateProfileRequest(commitment, "validversion", name, null, null, - null, false, true, List.of()), MediaType.APPLICATION_JSON_TYPE))) { + null, false, true, Optional.of(List.of()), null), MediaType.APPLICATION_JSON_TYPE))) { final ArgumentCaptor profileArgumentCaptor = ArgumentCaptor.forClass(VersionedProfile.class); @@ -610,7 +613,7 @@ class ProfileControllerTest { .request() .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) .put(Entity.entity(new CreateProfileRequest(commitment, "validversion", name, - null, null, null, true, true, List.of()), MediaType.APPLICATION_JSON_TYPE))) { + null, null, null, true, true, Optional.of(List.of()), null), MediaType.APPLICATION_JSON_TYPE))) { assertThat(response.getStatus()).isEqualTo(200); assertThat(response.hasEntity()).isFalse(); @@ -642,7 +645,7 @@ class ProfileControllerTest { .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID_TWO, AuthHelper.VALID_PASSWORD_TWO)) .put(Entity.entity( new CreateProfileRequest(commitment, "validversion", name, - null, null, null, true, false, List.of()), + null, null, null, true, false, Optional.of(List.of()), null), MediaType.APPLICATION_JSON_TYPE), ProfileAvatarUploadAttributes.class); final ArgumentCaptor profileArgumentCaptor = ArgumentCaptor.forClass(VersionedProfile.class); @@ -675,7 +678,7 @@ class ProfileControllerTest { .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID_TWO, AuthHelper.VALID_PASSWORD_TWO)) .put(Entity.entity( new CreateProfileRequest(commitment, "anotherversion", name, emoji, about, null, - false, false, List.of()), + false, false, Optional.of(List.of()), null), MediaType.APPLICATION_JSON_TYPE))) { assertThat(response.getStatus()).isEqualTo(200); @@ -715,7 +718,7 @@ class ProfileControllerTest { .put(Entity.entity( new CreateProfileRequest(commitment, "yetanotherversion", name, null, null, paymentAddress, false, false, - List.of()), MediaType.APPLICATION_JSON_TYPE))) { + Optional.of(List.of()), null), MediaType.APPLICATION_JSON_TYPE))) { assertThat(response.getStatus()).isEqualTo(200); assertThat(response.hasEntity()).isFalse(); @@ -757,7 +760,7 @@ class ProfileControllerTest { .put(Entity.entity( new CreateProfileRequest(commitment, "yetanotherversion", name, null, null, paymentAddress, false, false, - List.of()), MediaType.APPLICATION_JSON_TYPE))) { + Optional.of(List.of()), null), MediaType.APPLICATION_JSON_TYPE))) { assertThat(response.getStatus()).isEqualTo(403); assertThat(response.hasEntity()).isFalse(); @@ -776,6 +779,7 @@ class ProfileControllerTest { final ProfileKeyCommitment commitment = new ProfileKey(new byte[32]).getCommitment(new ServiceId.Aci(AuthHelper.VALID_UUID)); final byte[] name = TestRandomUtil.nextBytes(81); final byte[] paymentAddress = TestRandomUtil.nextBytes(582); + final byte[] phoneNumberSharing = TestRandomUtil.nextBytes(29); clearInvocations(AuthHelper.VALID_ACCOUNT_TWO); @@ -783,6 +787,7 @@ class ProfileControllerTest { .thenReturn(Optional.of( new VersionedProfile("1", name, null, null, null, existingPaymentAddressOnProfile ? TestRandomUtil.nextBytes(582) : null, + phoneNumberSharing, commitment.serialize()))); @@ -793,7 +798,7 @@ class ProfileControllerTest { .put(Entity.entity( new CreateProfileRequest(commitment, "yetanotherversion", name, null, null, paymentAddress, false, false, - List.of()), MediaType.APPLICATION_JSON_TYPE))) { + Optional.of(List.of()), null), MediaType.APPLICATION_JSON_TYPE))) { if (existingPaymentAddressOnProfile) { assertThat(response.getStatus()).isEqualTo(200); @@ -823,14 +828,49 @@ class ProfileControllerTest { } } + @Test + void testSetProfilePhoneNumberSharing() throws Exception { + final ProfileKeyCommitment commitment = new ProfileKey(new byte[32]).getCommitment(new ServiceId.Aci(AuthHelper.VALID_UUID)); + final byte[] name = TestRandomUtil.nextBytes(81); + final byte[] phoneNumberSharing = TestRandomUtil.nextBytes(29); + + clearInvocations(AuthHelper.VALID_ACCOUNT_TWO); + + try (final Response response = resources.getJerseyTest() + .target("/v1/profile/") + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID_TWO, AuthHelper.VALID_PASSWORD_TWO)) + .put(Entity.entity(new CreateProfileRequest(commitment, "anotherversion", name, null, null, + null, false, false, Optional.of(List.of()), phoneNumberSharing), MediaType.APPLICATION_JSON_TYPE))) { + + assertThat(response.getStatus()).isEqualTo(200); + assertThat(response.hasEntity()).isFalse(); + + final ArgumentCaptor profileArgumentCaptor = ArgumentCaptor.forClass(VersionedProfile.class); + + verify(profilesManager, times(1)).get(eq(AuthHelper.VALID_UUID_TWO), eq("anotherversion")); + verify(profilesManager, times(1)).set(eq(AuthHelper.VALID_UUID_TWO), profileArgumentCaptor.capture()); + + verifyNoMoreInteractions(s3client); + + assertThat(profileArgumentCaptor.getValue().commitment()).isEqualTo(commitment.serialize()); + assertThat(profileArgumentCaptor.getValue().avatar()).isNull(); + assertThat(profileArgumentCaptor.getValue().version()).isEqualTo("anotherversion"); + assertThat(profileArgumentCaptor.getValue().name()).isEqualTo(name); + assertThat(profileArgumentCaptor.getValue().aboutEmoji()).isNull(); + assertThat(profileArgumentCaptor.getValue().about()).isNull(); + } + } + @Test void testGetProfileByVersion() throws RateLimitExceededException { final byte[] name = TestRandomUtil.nextBytes(81); final byte[] emoji = TestRandomUtil.nextBytes(60); final byte[] about = TestRandomUtil.nextBytes(156); + final byte[] phoneNumberSharing = TestRandomUtil.nextBytes(29); when(profilesManager.get(eq(AuthHelper.VALID_UUID_TWO), eq("validversion"))).thenReturn(Optional.of(new VersionedProfile( - "validversion", name, "profiles/validavatar", emoji, about, null, "validcommitmnet".getBytes()))); + "validversion", name, "profiles/validavatar", emoji, about, null, phoneNumberSharing, "validcommitment".getBytes()))); final VersionedProfileResponse profile = resources.getJerseyTest() .target("/v1/profile/" + AuthHelper.VALID_UUID_TWO + "/validversion") @@ -843,6 +883,7 @@ class ProfileControllerTest { assertThat(profile.getAbout()).containsExactly(about); assertThat(profile.getAboutEmoji()).containsExactly(emoji); assertThat(profile.getAvatar()).isEqualTo("profiles/validavatar"); + assertThat(profile.getPhoneNumberSharing()).containsExactly(phoneNumberSharing); assertThat(profile.getBaseProfileResponse().getCapabilities().gv1Migration()).isTrue(); assertThat(profile.getBaseProfileResponse().getUuid()).isEqualTo(new AciServiceIdentifier(AuthHelper.VALID_UUID_TWO)); assertThat(profile.getBaseProfileResponse().getBadges()).hasSize(1).element(0).has(new Condition<>( @@ -869,7 +910,7 @@ class ProfileControllerTest { .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID_TWO, AuthHelper.VALID_PASSWORD_TWO)) .put(Entity.entity( new CreateProfileRequest(commitment, "someversion", name, null, null, paymentAddress, false, false, - List.of()), MediaType.APPLICATION_JSON_TYPE))) { + Optional.of(List.of()), null), MediaType.APPLICATION_JSON_TYPE))) { assertThat(response.getStatus()).isEqualTo(200); assertThat(response.hasEntity()).isFalse(); @@ -882,7 +923,7 @@ class ProfileControllerTest { void testGetProfileReturnsNoPaymentAddressIfCurrentVersionMismatch() { final byte[] paymentAddress = TestRandomUtil.nextBytes(582); when(profilesManager.get(AuthHelper.VALID_UUID_TWO, "validversion")).thenReturn( - Optional.of(new VersionedProfile(null, null, null, null, null, paymentAddress, null))); + Optional.of(new VersionedProfile(null, null, null, null, null, paymentAddress, null, null))); { final VersionedProfileResponse profile = resources.getJerseyTest() @@ -959,7 +1000,7 @@ class ProfileControllerTest { .request() .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID_TWO, AuthHelper.VALID_PASSWORD_TWO)) .put(Entity.entity(new CreateProfileRequest(commitment, "anotherversion", name, emoji, about, null, false, false, - List.of("TEST2")), MediaType.APPLICATION_JSON_TYPE))) { + Optional.of(List.of("TEST2")), null), MediaType.APPLICATION_JSON_TYPE))) { assertThat(response.getStatus()).isEqualTo(200); assertThat(response.hasEntity()).isFalse(); @@ -982,7 +1023,7 @@ class ProfileControllerTest { .request() .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID_TWO, AuthHelper.VALID_PASSWORD_TWO)) .put(Entity.entity(new CreateProfileRequest(commitment, "anotherversion", name, emoji, about, null, false, false, - List.of("TEST3", "TEST2")), MediaType.APPLICATION_JSON_TYPE))) { + Optional.of(List.of("TEST3", "TEST2")), null), MediaType.APPLICATION_JSON_TYPE))) { assertThat(response.getStatus()).isEqualTo(200); assertThat(response.hasEntity()).isFalse(); @@ -1008,7 +1049,7 @@ class ProfileControllerTest { .request() .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID_TWO, AuthHelper.VALID_PASSWORD_TWO)) .put(Entity.entity(new CreateProfileRequest(commitment, "anotherversion", name, emoji, about, null, false, false, - List.of("TEST2", "TEST3")), MediaType.APPLICATION_JSON_TYPE))) { + Optional.of(List.of("TEST2", "TEST3")), null), MediaType.APPLICATION_JSON_TYPE))) { assertThat(response.getStatus()).isEqualTo(200); assertThat(response.hasEntity()).isFalse(); @@ -1034,7 +1075,7 @@ class ProfileControllerTest { .request() .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID_TWO, AuthHelper.VALID_PASSWORD_TWO)) .put(Entity.entity(new CreateProfileRequest(commitment, "anotherversion", name, emoji, about, null, false, false, - List.of("TEST1")), MediaType.APPLICATION_JSON_TYPE))) { + Optional.of(List.of("TEST1")), null), MediaType.APPLICATION_JSON_TYPE))) { assertThat(response.getStatus()).isEqualTo(200); assertThat(response.hasEntity()).isFalse(); diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/grpc/ProfileGrpcServiceTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/grpc/ProfileGrpcServiceTest.java index 2c569d3ef..ce2215279 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/grpc/ProfileGrpcServiceTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/grpc/ProfileGrpcServiceTest.java @@ -218,6 +218,7 @@ public class ProfileGrpcServiceTest extends SimpleBaseGrpcTest