mirror of
https://github.com/signalapp/Signal-Android.git
synced 2025-12-27 14:40:22 +00:00
Convert remaining profile apis to use WebSockets and remove REST fallback.
This commit is contained in:
@@ -53,6 +53,7 @@ import org.whispersystems.signalservice.api.keys.KeysApi
|
||||
import org.whispersystems.signalservice.api.link.LinkDeviceApi
|
||||
import org.whispersystems.signalservice.api.message.MessageApi
|
||||
import org.whispersystems.signalservice.api.payments.PaymentsApi
|
||||
import org.whispersystems.signalservice.api.profiles.ProfileApi
|
||||
import org.whispersystems.signalservice.api.provisioning.ProvisioningApi
|
||||
import org.whispersystems.signalservice.api.ratelimit.RateLimitChallengeApi
|
||||
import org.whispersystems.signalservice.api.registration.RegistrationApi
|
||||
@@ -342,6 +343,9 @@ object AppDependencies {
|
||||
val certificateApi: CertificateApi
|
||||
get() = networkModule.certificateApi
|
||||
|
||||
val profileApi: ProfileApi
|
||||
get() = networkModule.profileApi
|
||||
|
||||
@JvmStatic
|
||||
val okHttpClient: OkHttpClient
|
||||
get() = networkModule.okHttpClient
|
||||
@@ -398,7 +402,7 @@ object AppDependencies {
|
||||
fun provideExoPlayerPool(): SimpleExoPlayerPool
|
||||
fun provideAndroidCallAudioManager(): AudioManagerCompat
|
||||
fun provideDonationsService(pushServiceSocket: PushServiceSocket): DonationsService
|
||||
fun provideProfileService(profileOperations: ClientZkProfileOperations, signalServiceMessageReceiver: SignalServiceMessageReceiver, authWebSocket: SignalWebSocket.AuthenticatedWebSocket, unauthWebSocket: SignalWebSocket.UnauthenticatedWebSocket): ProfileService
|
||||
fun provideProfileService(profileOperations: ClientZkProfileOperations, authWebSocket: SignalWebSocket.AuthenticatedWebSocket, unauthWebSocket: SignalWebSocket.UnauthenticatedWebSocket): ProfileService
|
||||
fun provideDeadlockDetector(): DeadlockDetector
|
||||
fun provideClientZkReceiptOperations(signalServiceConfiguration: SignalServiceConfiguration): ClientZkReceiptOperations
|
||||
fun provideScheduledMessageManager(): ScheduledMessageManager
|
||||
@@ -421,5 +425,6 @@ object AppDependencies {
|
||||
fun provideMessageApi(authWebSocket: SignalWebSocket.AuthenticatedWebSocket, unauthWebSocket: SignalWebSocket.UnauthenticatedWebSocket): MessageApi
|
||||
fun provideProvisioningApi(authWebSocket: SignalWebSocket.AuthenticatedWebSocket): ProvisioningApi
|
||||
fun provideCertificateApi(authWebSocket: SignalWebSocket.AuthenticatedWebSocket): CertificateApi
|
||||
fun provideProfileApi(authWebSocket: SignalWebSocket.AuthenticatedWebSocket, pushServiceSocket: PushServiceSocket): ProfileApi
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,6 +93,7 @@ import org.whispersystems.signalservice.api.keys.KeysApi;
|
||||
import org.whispersystems.signalservice.api.link.LinkDeviceApi;
|
||||
import org.whispersystems.signalservice.api.message.MessageApi;
|
||||
import org.whispersystems.signalservice.api.payments.PaymentsApi;
|
||||
import org.whispersystems.signalservice.api.profiles.ProfileApi;
|
||||
import org.whispersystems.signalservice.api.provisioning.ProvisioningApi;
|
||||
import org.whispersystems.signalservice.api.push.ServiceId.ACI;
|
||||
import org.whispersystems.signalservice.api.push.ServiceId.PNI;
|
||||
@@ -448,11 +449,10 @@ public class ApplicationDependencyProvider implements AppDependencies.Provider {
|
||||
|
||||
@Override
|
||||
public @NonNull ProfileService provideProfileService(@NonNull ClientZkProfileOperations clientZkProfileOperations,
|
||||
@NonNull SignalServiceMessageReceiver receiver,
|
||||
@NonNull SignalWebSocket.AuthenticatedWebSocket authWebSocket,
|
||||
@NonNull SignalWebSocket.UnauthenticatedWebSocket unauthWebSocket)
|
||||
{
|
||||
return new ProfileService(clientZkProfileOperations, receiver, authWebSocket, unauthWebSocket);
|
||||
return new ProfileService(clientZkProfileOperations, authWebSocket, unauthWebSocket);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -547,6 +547,11 @@ public class ApplicationDependencyProvider implements AppDependencies.Provider {
|
||||
return new CertificateApi(authWebSocket);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull ProfileApi provideProfileApi(@NonNull SignalWebSocket.AuthenticatedWebSocket authWebSocket, @NonNull PushServiceSocket pushServiceSocket) {
|
||||
return new ProfileApi(authWebSocket, pushServiceSocket);
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
static class DynamicCredentialsProvider implements CredentialsProvider {
|
||||
|
||||
|
||||
@@ -38,6 +38,7 @@ import org.whispersystems.signalservice.api.keys.KeysApi
|
||||
import org.whispersystems.signalservice.api.link.LinkDeviceApi
|
||||
import org.whispersystems.signalservice.api.message.MessageApi
|
||||
import org.whispersystems.signalservice.api.payments.PaymentsApi
|
||||
import org.whispersystems.signalservice.api.profiles.ProfileApi
|
||||
import org.whispersystems.signalservice.api.provisioning.ProvisioningApi
|
||||
import org.whispersystems.signalservice.api.push.TrustStore
|
||||
import org.whispersystems.signalservice.api.ratelimit.RateLimitChallengeApi
|
||||
@@ -136,7 +137,7 @@ class NetworkDependenciesModule(
|
||||
}
|
||||
|
||||
val profileService: ProfileService by lazy {
|
||||
provider.provideProfileService(groupsV2Operations.profileOperations, signalServiceMessageReceiver, authWebSocket, unauthWebSocket)
|
||||
provider.provideProfileService(groupsV2Operations.profileOperations, authWebSocket, unauthWebSocket)
|
||||
}
|
||||
|
||||
val donationsService: DonationsService by lazy {
|
||||
@@ -203,6 +204,10 @@ class NetworkDependenciesModule(
|
||||
provider.provideCertificateApi(authWebSocket)
|
||||
}
|
||||
|
||||
val profileApi: ProfileApi by lazy {
|
||||
provider.provideProfileApi(authWebSocket, pushServiceSocket)
|
||||
}
|
||||
|
||||
val okHttpClient: OkHttpClient by lazy {
|
||||
OkHttpClient.Builder()
|
||||
.addInterceptor(StandardUserAgentInterceptor())
|
||||
|
||||
@@ -16,6 +16,7 @@ import org.whispersystems.signalservice.api.keys.KeysApi
|
||||
import org.whispersystems.signalservice.api.link.LinkDeviceApi
|
||||
import org.whispersystems.signalservice.api.message.MessageApi
|
||||
import org.whispersystems.signalservice.api.payments.PaymentsApi
|
||||
import org.whispersystems.signalservice.api.profiles.ProfileApi
|
||||
import org.whispersystems.signalservice.api.provisioning.ProvisioningApi
|
||||
import org.whispersystems.signalservice.api.ratelimit.RateLimitChallengeApi
|
||||
import org.whispersystems.signalservice.api.storage.StorageServiceApi
|
||||
@@ -67,6 +68,11 @@ object SignalNetwork {
|
||||
val payments: PaymentsApi
|
||||
get() = AppDependencies.paymentsApi
|
||||
|
||||
@JvmStatic
|
||||
@get:JvmName("profile")
|
||||
val profile: ProfileApi
|
||||
get() = AppDependencies.profileApi
|
||||
|
||||
val provisioning: ProvisioningApi
|
||||
get() = AppDependencies.provisioningApi
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ import org.thoughtcrime.securesms.jobs.ProfileUploadJob;
|
||||
import org.thoughtcrime.securesms.jobs.RefreshAttributesJob;
|
||||
import org.thoughtcrime.securesms.jobs.RefreshOwnProfileJob;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.net.SignalNetwork;
|
||||
import org.thoughtcrime.securesms.payments.MobileCoinPublicAddress;
|
||||
import org.thoughtcrime.securesms.payments.MobileCoinPublicAddressProfileUtil;
|
||||
import org.thoughtcrime.securesms.payments.PaymentsAddressException;
|
||||
@@ -35,7 +36,8 @@ import org.thoughtcrime.securesms.profiles.AvatarHelper;
|
||||
import org.thoughtcrime.securesms.profiles.ProfileName;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientUtil;
|
||||
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
|
||||
import org.whispersystems.signalservice.api.NetworkResult;
|
||||
import org.whispersystems.signalservice.api.NetworkResultUtil;
|
||||
import org.whispersystems.signalservice.api.crypto.InvalidCiphertextException;
|
||||
import org.whispersystems.signalservice.api.crypto.ProfileCipher;
|
||||
import org.whispersystems.signalservice.api.crypto.SealedSenderAccess;
|
||||
@@ -322,8 +324,7 @@ public final class ProfileUtil {
|
||||
if (profileKey != null) {
|
||||
Log.i(TAG, String.format("Updating profile key credential on recipient %s, fetching", recipient.getId()));
|
||||
|
||||
Optional<ExpiringProfileKeyCredential> profileKeyCredentialOptional = AppDependencies.getSignalServiceAccountManager()
|
||||
.resolveProfileKeyCredential(recipient.requireAci(), profileKey, Locale.getDefault());
|
||||
Optional<ExpiringProfileKeyCredential> profileKeyCredentialOptional = retrieveProfileSync(AppDependencies.getApplication(), recipient, SignalServiceProfile.RequestType.PROFILE_AND_CREDENTIAL, false).getExpiringProfileKeyCredential();
|
||||
|
||||
if (profileKeyCredentialOptional.isPresent()) {
|
||||
boolean updatedProfileKey = SignalDatabase.recipients().setProfileKeyCredential(recipient.getId(), profileKey, profileKeyCredentialOptional.get());
|
||||
@@ -365,17 +366,19 @@ public final class ProfileUtil {
|
||||
Log.d(TAG, "Uploading " + (avatar.stream != null && avatar.stream.getLength() != 0 ? "non-" : "") + "empty avatar.");
|
||||
}
|
||||
|
||||
ProfileKey profileKey = ProfileKeyUtil.getSelfProfileKey();
|
||||
SignalServiceAccountManager accountManager = AppDependencies.getSignalServiceAccountManager();
|
||||
String avatarPath = accountManager.setVersionedProfile(SignalStore.account().requireAci(),
|
||||
profileKey,
|
||||
profileName.serialize(),
|
||||
about,
|
||||
aboutEmoji,
|
||||
Optional.ofNullable(paymentsAddress),
|
||||
avatar,
|
||||
badgeIds,
|
||||
SignalStore.phoneNumberPrivacy().isPhoneNumberSharingEnabled()).orElse(null);
|
||||
ProfileKey profileKey = ProfileKeyUtil.getSelfProfileKey();
|
||||
NetworkResult<String> result = SignalNetwork.profile().setVersionedProfile(SignalStore.account().requireAci(),
|
||||
profileKey,
|
||||
profileName.serialize(),
|
||||
about,
|
||||
aboutEmoji,
|
||||
paymentsAddress,
|
||||
avatar,
|
||||
badgeIds,
|
||||
SignalStore.phoneNumberPrivacy().isPhoneNumberSharingEnabled());
|
||||
|
||||
String avatarPath = NetworkResultUtil.toSetProfileLegacy(result);
|
||||
|
||||
SignalStore.registration().setHasUploadedProfile(true);
|
||||
if (!avatar.keepTheSame) {
|
||||
SignalDatabase.recipients().setProfileAvatar(Recipient.self().getId(), avatarPath, false);
|
||||
|
||||
@@ -47,6 +47,7 @@ import org.whispersystems.signalservice.api.keys.KeysApi
|
||||
import org.whispersystems.signalservice.api.link.LinkDeviceApi
|
||||
import org.whispersystems.signalservice.api.message.MessageApi
|
||||
import org.whispersystems.signalservice.api.payments.PaymentsApi
|
||||
import org.whispersystems.signalservice.api.profiles.ProfileApi
|
||||
import org.whispersystems.signalservice.api.provisioning.ProvisioningApi
|
||||
import org.whispersystems.signalservice.api.ratelimit.RateLimitChallengeApi
|
||||
import org.whispersystems.signalservice.api.registration.RegistrationApi
|
||||
@@ -192,7 +193,6 @@ class MockApplicationDependencyProvider : AppDependencies.Provider {
|
||||
|
||||
override fun provideProfileService(
|
||||
profileOperations: ClientZkProfileOperations,
|
||||
signalServiceMessageReceiver: SignalServiceMessageReceiver,
|
||||
authWebSocket: SignalWebSocket.AuthenticatedWebSocket,
|
||||
unauthWebSocket: SignalWebSocket.UnauthenticatedWebSocket
|
||||
): ProfileService {
|
||||
@@ -286,4 +286,8 @@ class MockApplicationDependencyProvider : AppDependencies.Provider {
|
||||
override fun provideCertificateApi(authWebSocket: SignalWebSocket.AuthenticatedWebSocket): CertificateApi {
|
||||
return mockk(relaxed = true)
|
||||
}
|
||||
|
||||
override fun provideProfileApi(authWebSocket: SignalWebSocket.AuthenticatedWebSocket, pushServiceSocket: PushServiceSocket): ProfileApi {
|
||||
return mockk(relaxed = true)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ import org.whispersystems.signalservice.internal.push.exceptions.GroupMismatched
|
||||
import org.whispersystems.signalservice.internal.push.exceptions.GroupStaleDevicesException
|
||||
import org.whispersystems.signalservice.internal.push.exceptions.InvalidUnidentifiedAccessHeaderException
|
||||
import org.whispersystems.signalservice.internal.push.exceptions.MismatchedDevicesException
|
||||
import org.whispersystems.signalservice.internal.push.exceptions.PaymentsRegionException
|
||||
import org.whispersystems.signalservice.internal.push.exceptions.StaleDevicesException
|
||||
import java.io.IOException
|
||||
import java.util.Optional
|
||||
@@ -154,4 +155,30 @@ object NetworkResultUtil {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a [NetworkResult] into typed exceptions expected during setting the user's profile.
|
||||
*/
|
||||
@JvmStatic
|
||||
@Throws(AuthorizationFailedException::class, PaymentsRegionException::class, RateLimitException::class, IOException::class)
|
||||
fun toSetProfileLegacy(result: NetworkResult<String?>): String? {
|
||||
return when (result) {
|
||||
is NetworkResult.Success -> result.result
|
||||
is NetworkResult.ApplicationError -> {
|
||||
throw when (val error = result.throwable) {
|
||||
is IOException, is RuntimeException -> error
|
||||
else -> RuntimeException(error)
|
||||
}
|
||||
}
|
||||
is NetworkResult.NetworkError -> throw result.exception
|
||||
is NetworkResult.StatusCodeError -> {
|
||||
throw when (result.code) {
|
||||
401 -> AuthorizationFailedException(result.code, "Authorization failed!")
|
||||
403 -> PaymentsRegionException(result.code)
|
||||
413, 429 -> RateLimitException(result.code, "Rate Limited", Optional.ofNullable(result.header("retry-after")?.toLongOrNull()))
|
||||
else -> result.exception
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,44 +7,25 @@
|
||||
package org.whispersystems.signalservice.api;
|
||||
|
||||
import org.signal.libsignal.net.Network;
|
||||
import org.signal.libsignal.zkgroup.profiles.ExpiringProfileKeyCredential;
|
||||
import org.signal.libsignal.zkgroup.profiles.ProfileKey;
|
||||
import org.whispersystems.signalservice.api.account.AccountApi;
|
||||
import org.whispersystems.signalservice.api.crypto.ProfileCipher;
|
||||
import org.whispersystems.signalservice.api.crypto.ProfileCipherOutputStream;
|
||||
import org.whispersystems.signalservice.api.crypto.SealedSenderAccess;
|
||||
import org.whispersystems.signalservice.api.groupsv2.ClientZkOperations;
|
||||
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Api;
|
||||
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations;
|
||||
import org.whispersystems.signalservice.api.profiles.AvatarUploadParams;
|
||||
import org.whispersystems.signalservice.api.profiles.ProfileAndCredential;
|
||||
import org.whispersystems.signalservice.api.profiles.SignalServiceProfileWrite;
|
||||
import org.whispersystems.signalservice.api.push.ServiceId.ACI;
|
||||
import org.whispersystems.signalservice.api.push.ServiceId.PNI;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException;
|
||||
import org.whispersystems.signalservice.api.registration.RegistrationApi;
|
||||
import org.whispersystems.signalservice.api.svr.SecureValueRecoveryV2;
|
||||
import org.whispersystems.signalservice.api.svr.SecureValueRecoveryV3;
|
||||
import org.whispersystems.signalservice.api.websocket.SignalWebSocket;
|
||||
import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration;
|
||||
import org.whispersystems.signalservice.internal.push.PaymentAddress;
|
||||
import org.whispersystems.signalservice.internal.push.ProfileAvatarData;
|
||||
import org.whispersystems.signalservice.internal.push.PushServiceSocket;
|
||||
import org.whispersystems.signalservice.internal.push.RemoteConfigResponse;
|
||||
import org.whispersystems.signalservice.internal.push.WhoAmIResponse;
|
||||
import org.whispersystems.signalservice.internal.push.http.ProfileCipherOutputStreamFactory;
|
||||
import org.whispersystems.signalservice.internal.util.StaticCredentialsProvider;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
@@ -149,69 +130,6 @@ public class SignalServiceAccountManager {
|
||||
this.pushServiceSocket.pingStorageService();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The avatar URL path, if one was written.
|
||||
*/
|
||||
public Optional<String> setVersionedProfile(ACI aci,
|
||||
ProfileKey profileKey,
|
||||
String name,
|
||||
String about,
|
||||
String aboutEmoji,
|
||||
Optional<PaymentAddress> paymentsAddress,
|
||||
AvatarUploadParams avatar,
|
||||
List<String> visibleBadgeIds,
|
||||
boolean phoneNumberSharing)
|
||||
throws IOException
|
||||
{
|
||||
if (name == null) name = "";
|
||||
|
||||
ProfileCipher profileCipher = new ProfileCipher(profileKey);
|
||||
byte[] ciphertextName = profileCipher.encryptString(name, ProfileCipher.getTargetNameLength(name));
|
||||
byte[] ciphertextAbout = profileCipher.encryptString(about, ProfileCipher.getTargetAboutLength(about));
|
||||
byte[] ciphertextEmoji = profileCipher.encryptString(aboutEmoji, ProfileCipher.EMOJI_PADDED_LENGTH);
|
||||
byte[] ciphertextMobileCoinAddress = paymentsAddress.map(address -> profileCipher.encryptWithLength(address.encode(), ProfileCipher.PAYMENTS_ADDRESS_CONTENT_SIZE)).orElse(null);
|
||||
byte[] cipherTextPhoneNumberSharing = profileCipher.encryptBoolean(phoneNumberSharing);
|
||||
ProfileAvatarData profileAvatarData = null;
|
||||
|
||||
if (avatar.stream != null && !avatar.keepTheSame) {
|
||||
profileAvatarData = new ProfileAvatarData(avatar.stream.getStream(),
|
||||
ProfileCipherOutputStream.getCiphertextLength(avatar.stream.getLength()),
|
||||
avatar.stream.getContentType(),
|
||||
new ProfileCipherOutputStreamFactory(profileKey));
|
||||
}
|
||||
|
||||
return this.pushServiceSocket.writeProfile(new SignalServiceProfileWrite(profileKey.getProfileKeyVersion(aci.getLibSignalAci()).serialize(),
|
||||
ciphertextName,
|
||||
ciphertextAbout,
|
||||
ciphertextEmoji,
|
||||
ciphertextMobileCoinAddress,
|
||||
cipherTextPhoneNumberSharing,
|
||||
avatar.hasAvatar,
|
||||
avatar.keepTheSame,
|
||||
profileKey.getCommitment(aci.getLibSignalAci()).serialize(),
|
||||
visibleBadgeIds),
|
||||
profileAvatarData);
|
||||
}
|
||||
|
||||
public Optional<ExpiringProfileKeyCredential> resolveProfileKeyCredential(ACI serviceId, ProfileKey profileKey, Locale locale)
|
||||
throws NonSuccessfulResponseCodeException, PushNetworkException
|
||||
{
|
||||
try {
|
||||
ProfileAndCredential credential = this.pushServiceSocket.retrieveVersionedProfileAndCredential(serviceId, profileKey, SealedSenderAccess.NONE, locale).get(10, TimeUnit.SECONDS);
|
||||
return credential.getExpiringProfileKeyCredential();
|
||||
} catch (InterruptedException | TimeoutException e) {
|
||||
throw new PushNetworkException(e);
|
||||
} catch (ExecutionException e) {
|
||||
if (e.getCause() instanceof NonSuccessfulResponseCodeException) {
|
||||
throw (NonSuccessfulResponseCodeException) e.getCause();
|
||||
} else if (e.getCause() instanceof PushNetworkException) {
|
||||
throw (PushNetworkException) e.getCause();
|
||||
} else {
|
||||
throw new PushNetworkException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void cancelInFlightRequests() {
|
||||
this.pushServiceSocket.cancelInFlightRequests();
|
||||
}
|
||||
|
||||
@@ -87,42 +87,6 @@ public class SignalServiceMessageReceiver {
|
||||
return retrieveAttachment(pointer, destination, maxSizeBytes, null).getDataStream();
|
||||
}
|
||||
|
||||
public ListenableFuture<ProfileAndCredential> retrieveProfile(SignalServiceAddress address,
|
||||
Optional<ProfileKey> profileKey,
|
||||
@Nullable SealedSenderAccess sealedSenderAccess,
|
||||
SignalServiceProfile.RequestType requestType,
|
||||
Locale locale)
|
||||
{
|
||||
|
||||
if (profileKey.isPresent()) {
|
||||
ACI aci;
|
||||
if (address.getServiceId() instanceof ACI) {
|
||||
aci = (ACI) address.getServiceId();
|
||||
} else {
|
||||
// We shouldn't ever have a profile key for a non-ACI.
|
||||
SettableFuture<ProfileAndCredential> result = new SettableFuture<>();
|
||||
result.setException(new ClassCastException("retrieving a versioned profile requires an ACI"));
|
||||
return result;
|
||||
}
|
||||
|
||||
if (requestType == SignalServiceProfile.RequestType.PROFILE_AND_CREDENTIAL) {
|
||||
return socket.retrieveVersionedProfileAndCredential(aci, profileKey.get(), sealedSenderAccess, locale);
|
||||
} else {
|
||||
return FutureTransformers.map(socket.retrieveVersionedProfile(aci, profileKey.get(), sealedSenderAccess, locale), profile -> {
|
||||
return new ProfileAndCredential(profile,
|
||||
SignalServiceProfile.RequestType.PROFILE,
|
||||
Optional.empty());
|
||||
});
|
||||
}
|
||||
} else {
|
||||
return FutureTransformers.map(socket.retrieveProfile(address, sealedSenderAccess, locale), profile -> {
|
||||
return new ProfileAndCredential(profile,
|
||||
SignalServiceProfile.RequestType.PROFILE,
|
||||
Optional.empty());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public InputStream retrieveProfileAvatar(String path, File destination, ProfileKey profileKey, long maxSizeBytes)
|
||||
throws IOException
|
||||
{
|
||||
@@ -137,10 +101,6 @@ public class SignalServiceMessageReceiver {
|
||||
return new FileInputStream(destination);
|
||||
}
|
||||
|
||||
public Single<ServiceResponse<IdentityCheckResponse>> performIdentityCheck(@Nonnull IdentityCheckRequest request, @Nonnull ResponseMapper<IdentityCheckResponse> responseMapper) {
|
||||
return socket.performIdentityCheck(request, responseMapper);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a SignalServiceAttachment. The encrypted data is written to @{code destination}, and then an {@link InputStream} is returned that decrypts the
|
||||
* contents of the destination file, giving you access to the plaintext content.
|
||||
|
||||
@@ -16,6 +16,7 @@ import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Locale;
|
||||
import java.util.Optional;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
import javax.crypto.BadPaddingException;
|
||||
import javax.crypto.Cipher;
|
||||
@@ -105,7 +106,7 @@ public class ProfileCipher {
|
||||
/**
|
||||
* Encrypts a string's UTF bytes representation.
|
||||
*/
|
||||
public byte[] encryptString(String input, int paddedLength) {
|
||||
public byte[] encryptString(@Nonnull String input, int paddedLength) {
|
||||
return encrypt(input.getBytes(StandardCharsets.UTF_8), paddedLength);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
/*
|
||||
* Copyright 2025 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.signalservice.api.profiles
|
||||
|
||||
import org.signal.libsignal.zkgroup.profiles.ProfileKey
|
||||
import org.whispersystems.signalservice.api.NetworkResult
|
||||
import org.whispersystems.signalservice.api.crypto.ProfileCipher
|
||||
import org.whispersystems.signalservice.api.crypto.ProfileCipherOutputStream
|
||||
import org.whispersystems.signalservice.api.push.ServiceId
|
||||
import org.whispersystems.signalservice.api.services.ProfileService
|
||||
import org.whispersystems.signalservice.api.websocket.SignalWebSocket
|
||||
import org.whispersystems.signalservice.internal.push.PaymentAddress
|
||||
import org.whispersystems.signalservice.internal.push.ProfileAvatarData
|
||||
import org.whispersystems.signalservice.internal.push.ProfileAvatarUploadAttributes
|
||||
import org.whispersystems.signalservice.internal.push.PushServiceSocket
|
||||
import org.whispersystems.signalservice.internal.push.http.ProfileCipherOutputStreamFactory
|
||||
import org.whispersystems.signalservice.internal.put
|
||||
import org.whispersystems.signalservice.internal.util.JsonUtil
|
||||
import org.whispersystems.signalservice.internal.websocket.WebSocketRequestMessage
|
||||
|
||||
/**
|
||||
* Endpoints to interact with profiles. Currently contains only setting profile but will
|
||||
* be eventual home for functionality in [ProfileService].
|
||||
*/
|
||||
class ProfileApi(
|
||||
private val authWebSocket: SignalWebSocket.AuthenticatedWebSocket,
|
||||
private val pushServiceSocket: PushServiceSocket
|
||||
) {
|
||||
|
||||
/**
|
||||
* Set/update the user's profile on the service, including uploading an avatar if one is provided.
|
||||
*
|
||||
* PUT /v1/profile
|
||||
* - 200: Success
|
||||
* - 401: Authorization failure
|
||||
* - 403: Payment region not allowed
|
||||
*
|
||||
* @return The avatar URL path, if one was written.
|
||||
*/
|
||||
fun setVersionedProfile(
|
||||
aci: ServiceId.ACI,
|
||||
profileKey: ProfileKey,
|
||||
name: String?,
|
||||
about: String?,
|
||||
aboutEmoji: String?,
|
||||
paymentsAddress: PaymentAddress?,
|
||||
avatar: AvatarUploadParams,
|
||||
visibleBadgeIds: List<String>,
|
||||
phoneNumberSharing: Boolean
|
||||
): NetworkResult<String?> {
|
||||
val profileCipher = ProfileCipher(profileKey)
|
||||
|
||||
val profileWrite = SignalServiceProfileWrite(
|
||||
version = profileKey.getProfileKeyVersion(aci.libSignalAci).serialize(),
|
||||
name = profileCipher.encryptString(name ?: "", ProfileCipher.getTargetNameLength(name)),
|
||||
about = profileCipher.encryptString(about ?: "", ProfileCipher.getTargetAboutLength(about)),
|
||||
aboutEmoji = profileCipher.encryptString(aboutEmoji ?: "", ProfileCipher.EMOJI_PADDED_LENGTH),
|
||||
paymentAddress = paymentsAddress?.let { profileCipher.encryptWithLength(it.encode(), ProfileCipher.PAYMENTS_ADDRESS_CONTENT_SIZE) },
|
||||
phoneNumberSharing = profileCipher.encryptBoolean(phoneNumberSharing),
|
||||
avatar = avatar.hasAvatar,
|
||||
sameAvatar = avatar.keepTheSame,
|
||||
commitment = profileKey.getCommitment(aci.libSignalAci).serialize(),
|
||||
badgeIds = visibleBadgeIds
|
||||
)
|
||||
|
||||
val profileAvatarData: ProfileAvatarData? = if (avatar.stream != null && !avatar.keepTheSame) {
|
||||
ProfileAvatarData(
|
||||
avatar.stream.stream,
|
||||
ProfileCipherOutputStream.getCiphertextLength(avatar.stream.length),
|
||||
avatar.stream.contentType,
|
||||
ProfileCipherOutputStreamFactory(profileKey)
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
val request = WebSocketRequestMessage.put("/v1/profile", profileWrite)
|
||||
|
||||
return NetworkResult.fromWebSocketRequest(authWebSocket, request, String::class)
|
||||
.then { fromResponse: String ->
|
||||
if (profileWrite.avatar && profileAvatarData != null) {
|
||||
val formAttributes = JsonUtil.fromJsonResponse(fromResponse, ProfileAvatarUploadAttributes::class.java)
|
||||
pushServiceSocket.uploadProfileAvatar(formAttributes, profileAvatarData)
|
||||
} else {
|
||||
NetworkResult.Success(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
package org.whispersystems.signalservice.api.profiles;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class SignalServiceProfileWrite {
|
||||
|
||||
@JsonProperty
|
||||
private String version;
|
||||
|
||||
@JsonProperty
|
||||
private byte[] name;
|
||||
|
||||
@JsonProperty
|
||||
private byte[] about;
|
||||
|
||||
@JsonProperty
|
||||
private byte[] aboutEmoji;
|
||||
|
||||
@JsonProperty
|
||||
private byte[] paymentAddress;
|
||||
|
||||
@JsonProperty
|
||||
private byte[] phoneNumberSharing;
|
||||
|
||||
@JsonProperty
|
||||
private boolean avatar;
|
||||
|
||||
@JsonProperty
|
||||
private boolean sameAvatar;
|
||||
|
||||
@JsonProperty
|
||||
private byte[] commitment;
|
||||
|
||||
@JsonProperty
|
||||
private List<String> badgeIds;
|
||||
|
||||
@JsonCreator
|
||||
public SignalServiceProfileWrite(){
|
||||
}
|
||||
|
||||
public SignalServiceProfileWrite(String version, byte[] name, byte[] about, byte[] aboutEmoji, byte[] paymentAddress, byte[] phoneNumberSharing, boolean hasAvatar, boolean sameAvatar, byte[] commitment, List<String> badgeIds) {
|
||||
this.version = version;
|
||||
this.name = name;
|
||||
this.about = about;
|
||||
this.aboutEmoji = aboutEmoji;
|
||||
this.paymentAddress = paymentAddress;
|
||||
this.phoneNumberSharing = phoneNumberSharing;
|
||||
this.avatar = hasAvatar;
|
||||
this.sameAvatar = sameAvatar;
|
||||
this.commitment = commitment;
|
||||
this.badgeIds = badgeIds;
|
||||
}
|
||||
|
||||
public boolean hasAvatar() {
|
||||
return avatar;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package org.whispersystems.signalservice.api.profiles
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
|
||||
class SignalServiceProfileWrite(
|
||||
@JsonProperty val version: String,
|
||||
@JsonProperty val name: ByteArray,
|
||||
@JsonProperty val about: ByteArray,
|
||||
@JsonProperty val aboutEmoji: ByteArray,
|
||||
@JsonProperty val paymentAddress: ByteArray?,
|
||||
@JsonProperty val phoneNumberSharing: ByteArray,
|
||||
@JsonProperty val avatar: Boolean,
|
||||
@JsonProperty val sameAvatar: Boolean,
|
||||
@JsonProperty val commitment: ByteArray,
|
||||
@JsonProperty val badgeIds: List<String>
|
||||
)
|
||||
@@ -17,7 +17,6 @@ import org.whispersystems.signalservice.api.profiles.SignalServiceProfile;
|
||||
import org.whispersystems.signalservice.api.push.ServiceId;
|
||||
import org.whispersystems.signalservice.api.push.ServiceId.ACI;
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.MalformedResponseException;
|
||||
import org.whispersystems.signalservice.api.websocket.SignalWebSocket;
|
||||
import org.whispersystems.signalservice.internal.ServiceResponse;
|
||||
@@ -38,8 +37,6 @@ import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@@ -51,7 +48,7 @@ import io.reactivex.rxjava3.core.Single;
|
||||
|
||||
/**
|
||||
* Provide Profile-related API services, encapsulating the logic to make the request, parse the response,
|
||||
* and fallback to appropriate WebSocket or RESTful alternatives.
|
||||
* and fallback to appropriate WebSocket alternatives.
|
||||
*/
|
||||
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
|
||||
public final class ProfileService {
|
||||
@@ -59,17 +56,14 @@ public final class ProfileService {
|
||||
private static final String TAG = ProfileService.class.getSimpleName();
|
||||
|
||||
private final ClientZkProfileOperations clientZkProfileOperations;
|
||||
private final SignalServiceMessageReceiver receiver;
|
||||
private final SignalWebSocket.AuthenticatedWebSocket authWebSocket;
|
||||
private final SignalWebSocket.UnauthenticatedWebSocket unauthWebSocket;
|
||||
|
||||
public ProfileService(ClientZkProfileOperations clientZkProfileOperations,
|
||||
SignalServiceMessageReceiver receiver,
|
||||
SignalWebSocket.AuthenticatedWebSocket authWebSocket,
|
||||
SignalWebSocket.UnauthenticatedWebSocket unauthWebSocket)
|
||||
{
|
||||
this.clientZkProfileOperations = clientZkProfileOperations;
|
||||
this.receiver = receiver;
|
||||
this.authWebSocket = authWebSocket;
|
||||
this.unauthWebSocket = unauthWebSocket;
|
||||
}
|
||||
@@ -123,7 +117,6 @@ public final class ProfileService {
|
||||
if (sealedSenderAccess == null) {
|
||||
return authWebSocket.request(requestMessage)
|
||||
.map(responseMapper::map)
|
||||
.onErrorResumeNext(t -> getProfileRestFallback(address, profileKey, null, requestType, locale))
|
||||
.onErrorReturn(ServiceResponse::forUnknownError);
|
||||
} else {
|
||||
return unauthWebSocket.request(requestMessage, sealedSenderAccess)
|
||||
@@ -135,7 +128,6 @@ public final class ProfileService {
|
||||
}
|
||||
})
|
||||
.map(responseMapper::map)
|
||||
.onErrorResumeNext(t -> getProfileRestFallback(address, profileKey, sealedSenderAccess, requestType, locale))
|
||||
.onErrorReturn(ServiceResponse::forUnknownError);
|
||||
}
|
||||
}
|
||||
@@ -159,53 +151,9 @@ public final class ProfileService {
|
||||
|
||||
return unauthWebSocket.request(builder.build())
|
||||
.map(responseMapper::map)
|
||||
.onErrorResumeNext(t -> performIdentityCheckRestFallback(request, responseMapper))
|
||||
.onErrorReturn(ServiceResponse::forUnknownError);
|
||||
}
|
||||
|
||||
private Single<ServiceResponse<ProfileAndCredential>> getProfileRestFallback(@Nonnull SignalServiceAddress address,
|
||||
@Nonnull Optional<ProfileKey> profileKey,
|
||||
@Nullable SealedSenderAccess sealedSenderAccess,
|
||||
@Nonnull SignalServiceProfile.RequestType requestType,
|
||||
@Nonnull Locale locale)
|
||||
{
|
||||
return Single.fromFuture(receiver.retrieveProfile(address, profileKey, sealedSenderAccess, requestType, locale), 10, TimeUnit.SECONDS)
|
||||
.onErrorResumeNext(t -> {
|
||||
Throwable error;
|
||||
if (t instanceof ExecutionException && t.getCause() != null) {
|
||||
error = t.getCause();
|
||||
} else {
|
||||
error = t;
|
||||
}
|
||||
|
||||
if (error instanceof AuthorizationFailedException) {
|
||||
return Single.fromFuture(receiver.retrieveProfile(address, profileKey, null, requestType, locale), 10, TimeUnit.SECONDS);
|
||||
} else {
|
||||
return Single.error(t);
|
||||
}
|
||||
})
|
||||
.map(p -> ServiceResponse.forResult(p, 0, null));
|
||||
}
|
||||
|
||||
private @NonNull Single<ServiceResponse<IdentityCheckResponse>> performIdentityCheckRestFallback(@Nonnull IdentityCheckRequest request,
|
||||
@Nonnull ResponseMapper<IdentityCheckResponse> responseMapper) {
|
||||
return receiver.performIdentityCheck(request, responseMapper)
|
||||
.onErrorResumeNext(t -> {
|
||||
Throwable error;
|
||||
if (t instanceof ExecutionException && t.getCause() != null) {
|
||||
error = t.getCause();
|
||||
} else {
|
||||
error = t;
|
||||
}
|
||||
|
||||
if (error instanceof AuthorizationFailedException) {
|
||||
return receiver.performIdentityCheck(request, responseMapper);
|
||||
} else {
|
||||
return Single.error(t);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps the API {@link SignalServiceProfile} model into the desired {@link ProfileAndCredential} domain model.
|
||||
*/
|
||||
|
||||
@@ -11,19 +11,10 @@ import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.squareup.wire.Message;
|
||||
|
||||
import org.signal.core.util.Base64;
|
||||
import org.signal.core.util.concurrent.FutureTransformers;
|
||||
import org.signal.core.util.concurrent.ListenableFuture;
|
||||
import org.signal.core.util.concurrent.SettableFuture;
|
||||
import org.signal.libsignal.protocol.InvalidKeyException;
|
||||
import org.signal.libsignal.protocol.logging.Log;
|
||||
import org.signal.libsignal.protocol.util.Pair;
|
||||
import org.signal.libsignal.zkgroup.VerificationFailedException;
|
||||
import org.signal.libsignal.zkgroup.profiles.ClientZkProfileOperations;
|
||||
import org.signal.libsignal.zkgroup.profiles.ExpiringProfileKeyCredential;
|
||||
import org.signal.libsignal.zkgroup.profiles.ProfileKey;
|
||||
import org.signal.libsignal.zkgroup.profiles.ProfileKeyCredentialRequest;
|
||||
import org.signal.libsignal.zkgroup.profiles.ProfileKeyCredentialRequestContext;
|
||||
import org.signal.libsignal.zkgroup.profiles.ProfileKeyVersion;
|
||||
import org.signal.libsignal.zkgroup.receipts.ReceiptCredentialPresentation;
|
||||
import org.signal.libsignal.zkgroup.receipts.ReceiptCredentialRequest;
|
||||
import org.signal.libsignal.zkgroup.receipts.ReceiptCredentialResponse;
|
||||
@@ -36,6 +27,7 @@ import org.signal.storageservice.protos.groups.GroupExternalCredential;
|
||||
import org.signal.storageservice.protos.groups.GroupJoinInfo;
|
||||
import org.signal.storageservice.protos.groups.GroupResponse;
|
||||
import org.signal.storageservice.protos.groups.Member;
|
||||
import org.whispersystems.signalservice.api.NetworkResult;
|
||||
import org.whispersystems.signalservice.api.account.AccountAttributes;
|
||||
import org.whispersystems.signalservice.api.account.PreKeyCollection;
|
||||
import org.whispersystems.signalservice.api.crypto.SealedSenderAccess;
|
||||
@@ -43,10 +35,6 @@ import org.whispersystems.signalservice.api.groupsv2.GroupsV2AuthorizationString
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment.ProgressListener;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentRemoteId;
|
||||
import org.whispersystems.signalservice.api.messages.calls.CallingResponse;
|
||||
import org.whispersystems.signalservice.api.profiles.ProfileAndCredential;
|
||||
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile;
|
||||
import org.whispersystems.signalservice.api.profiles.SignalServiceProfileWrite;
|
||||
import org.whispersystems.signalservice.api.push.ServiceId.ACI;
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
import org.whispersystems.signalservice.api.push.SignedPreKeyEntity;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.AlreadyVerifiedException;
|
||||
@@ -89,7 +77,6 @@ import org.whispersystems.signalservice.api.svr.Svr3Credentials;
|
||||
import org.whispersystems.signalservice.api.util.CredentialsProvider;
|
||||
import org.whispersystems.signalservice.api.util.Tls12SocketFactory;
|
||||
import org.whispersystems.signalservice.api.util.TlsProxySocketFactory;
|
||||
import org.whispersystems.signalservice.internal.ServiceResponse;
|
||||
import org.whispersystems.signalservice.internal.configuration.SignalCdnUrl;
|
||||
import org.whispersystems.signalservice.internal.configuration.SignalProxy;
|
||||
import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration;
|
||||
@@ -106,9 +93,7 @@ import org.whispersystems.signalservice.internal.push.exceptions.InAppPaymentRec
|
||||
import org.whispersystems.signalservice.internal.push.exceptions.InvalidUnidentifiedAccessHeaderException;
|
||||
import org.whispersystems.signalservice.internal.push.exceptions.MismatchedDevicesException;
|
||||
import org.whispersystems.signalservice.internal.push.exceptions.NotInGroupException;
|
||||
import org.whispersystems.signalservice.internal.push.exceptions.PaymentsRegionException;
|
||||
import org.whispersystems.signalservice.internal.push.exceptions.StaleDevicesException;
|
||||
import org.whispersystems.signalservice.internal.push.http.AcceptLanguagesUtil;
|
||||
import org.whispersystems.signalservice.internal.push.http.CancelationSignal;
|
||||
import org.whispersystems.signalservice.internal.push.http.DigestingRequestBody;
|
||||
import org.whispersystems.signalservice.internal.push.http.NoCipherOutputStreamFactory;
|
||||
@@ -122,7 +107,6 @@ import org.whispersystems.signalservice.internal.util.BlacklistingTrustManager;
|
||||
import org.whispersystems.signalservice.internal.util.Hex;
|
||||
import org.whispersystems.signalservice.internal.util.JsonUtil;
|
||||
import org.whispersystems.signalservice.internal.util.Util;
|
||||
import org.whispersystems.signalservice.internal.websocket.ResponseMapper;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
@@ -158,10 +142,7 @@ import javax.net.ssl.SSLContext;
|
||||
import javax.net.ssl.TrustManager;
|
||||
import javax.net.ssl.X509TrustManager;
|
||||
|
||||
import io.reactivex.rxjava3.core.Single;
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||
import okhttp3.Call;
|
||||
import okhttp3.Callback;
|
||||
import okhttp3.ConnectionPool;
|
||||
import okhttp3.ConnectionSpec;
|
||||
import okhttp3.Dns;
|
||||
@@ -189,9 +170,6 @@ public class PushServiceSocket {
|
||||
|
||||
private static final String GROUP_MESSAGE_PATH = "/v1/messages/multi_recipient?ts=%s&online=%s&urgent=%s&story=%s";
|
||||
|
||||
private static final String PROFILE_PATH = "/v1/profile/%s";
|
||||
private static final String PROFILE_BATCH_CHECK_PATH = "/v1/profile/identity_check/batch";
|
||||
|
||||
private static final String ATTACHMENT_KEY_DOWNLOAD_PATH = "attachments/%s";
|
||||
private static final String ATTACHMENT_ID_DOWNLOAD_PATH = "attachments/%d";
|
||||
private static final String AVATAR_UPLOAD_PATH = "";
|
||||
@@ -523,72 +501,6 @@ public class PushServiceSocket {
|
||||
return output.toByteArray();
|
||||
}
|
||||
|
||||
public ListenableFuture<SignalServiceProfile> retrieveProfile(SignalServiceAddress target, @Nullable SealedSenderAccess sealedSenderAccess, Locale locale) {
|
||||
ListenableFuture<String> response = submitServiceRequest(String.format(PROFILE_PATH, target.getIdentifier()), "GET", null, AcceptLanguagesUtil.getHeadersWithAcceptLanguage(locale), sealedSenderAccess);
|
||||
|
||||
return FutureTransformers.map(response, body -> {
|
||||
try {
|
||||
return JsonUtil.fromJson(body, SignalServiceProfile.class);
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, e);
|
||||
throw new MalformedResponseException("Unable to parse entity", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public ListenableFuture<ProfileAndCredential> retrieveVersionedProfileAndCredential(ACI target, ProfileKey profileKey, @Nullable SealedSenderAccess sealedSenderAccess, Locale locale) {
|
||||
ProfileKeyVersion profileKeyIdentifier = profileKey.getProfileKeyVersion(target.getLibSignalAci());
|
||||
ProfileKeyCredentialRequestContext requestContext = clientZkProfileOperations.createProfileKeyCredentialRequestContext(random, target.getLibSignalAci(), profileKey);
|
||||
ProfileKeyCredentialRequest request = requestContext.getRequest();
|
||||
|
||||
String version = profileKeyIdentifier.serialize();
|
||||
String credentialRequest = Hex.toStringCondensed(request.serialize());
|
||||
String subPath = String.format("%s/%s/%s?credentialType=expiringProfileKey", target, version, credentialRequest);
|
||||
|
||||
|
||||
ListenableFuture<String> response = submitServiceRequest(String.format(PROFILE_PATH, subPath), "GET", null, AcceptLanguagesUtil.getHeadersWithAcceptLanguage(locale), sealedSenderAccess);
|
||||
|
||||
return FutureTransformers.map(response, body -> formatProfileAndCredentialBody(requestContext, body));
|
||||
}
|
||||
|
||||
private ProfileAndCredential formatProfileAndCredentialBody(ProfileKeyCredentialRequestContext requestContext, String body)
|
||||
throws MalformedResponseException
|
||||
{
|
||||
try {
|
||||
SignalServiceProfile signalServiceProfile = JsonUtil.fromJson(body, SignalServiceProfile.class);
|
||||
|
||||
try {
|
||||
ExpiringProfileKeyCredential expiringProfileKeyCredential = signalServiceProfile.getExpiringProfileKeyCredentialResponse() != null
|
||||
? clientZkProfileOperations.receiveExpiringProfileKeyCredential(requestContext, signalServiceProfile.getExpiringProfileKeyCredentialResponse())
|
||||
: null;
|
||||
return new ProfileAndCredential(signalServiceProfile, SignalServiceProfile.RequestType.PROFILE_AND_CREDENTIAL, Optional.ofNullable(expiringProfileKeyCredential));
|
||||
} catch (VerificationFailedException e) {
|
||||
Log.w(TAG, "Failed to verify credential.", e);
|
||||
return new ProfileAndCredential(signalServiceProfile, SignalServiceProfile.RequestType.PROFILE_AND_CREDENTIAL, Optional.empty());
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, e);
|
||||
throw new MalformedResponseException("Unable to parse entity", e);
|
||||
}
|
||||
}
|
||||
|
||||
public ListenableFuture<SignalServiceProfile> retrieveVersionedProfile(ACI target, ProfileKey profileKey, @Nullable SealedSenderAccess sealedSenderAccess, Locale locale) {
|
||||
ProfileKeyVersion profileKeyIdentifier = profileKey.getProfileKeyVersion(target.getLibSignalAci());
|
||||
|
||||
String version = profileKeyIdentifier.serialize();
|
||||
String subPath = String.format("%s/%s", target, version);
|
||||
ListenableFuture<String> response = submitServiceRequest(String.format(PROFILE_PATH, subPath), "GET", null, AcceptLanguagesUtil.getHeadersWithAcceptLanguage(locale), sealedSenderAccess);
|
||||
|
||||
return FutureTransformers.map(response, body -> {
|
||||
try {
|
||||
return JsonUtil.fromJson(body, SignalServiceProfile.class);
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, e);
|
||||
throw new MalformedResponseException("Unable to parse entity", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void retrieveProfileAvatar(String path, File destination, long maxSizeBytes)
|
||||
throws IOException
|
||||
{
|
||||
@@ -600,56 +512,21 @@ public class PushServiceSocket {
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The avatar URL path, if one was written.
|
||||
* Only used to upload self profile avatar as part of writing the profile to the service.
|
||||
*
|
||||
* @return Profile avatar key
|
||||
*/
|
||||
public Optional<String> writeProfile(SignalServiceProfileWrite signalServiceProfileWrite, ProfileAvatarData profileAvatar)
|
||||
throws NonSuccessfulResponseCodeException, PushNetworkException, MalformedResponseException
|
||||
{
|
||||
String requestBody = JsonUtil.toJson(signalServiceProfileWrite);
|
||||
ProfileAvatarUploadAttributes formAttributes;
|
||||
|
||||
String response = makeServiceRequest(String.format(PROFILE_PATH, ""),
|
||||
"PUT",
|
||||
requestBody,
|
||||
NO_HEADERS,
|
||||
PaymentsRegionException::responseCodeHandler,
|
||||
SealedSenderAccess.NONE);
|
||||
|
||||
if (signalServiceProfileWrite.hasAvatar() && profileAvatar != null) {
|
||||
try {
|
||||
formAttributes = JsonUtil.fromJson(response, ProfileAvatarUploadAttributes.class);
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, e);
|
||||
throw new MalformedResponseException("Unable to parse entity", e);
|
||||
}
|
||||
|
||||
public NetworkResult<String> uploadProfileAvatar(ProfileAvatarUploadAttributes formAttributes, ProfileAvatarData profileAvatar) {
|
||||
return NetworkResult.fromFetch(() -> {
|
||||
uploadToCdn0(AVATAR_UPLOAD_PATH, formAttributes.getAcl(), formAttributes.getKey(),
|
||||
formAttributes.getPolicy(), formAttributes.getAlgorithm(),
|
||||
formAttributes.getCredential(), formAttributes.getDate(),
|
||||
formAttributes.getSignature(), profileAvatar.getData(),
|
||||
profileAvatar.getContentType(), profileAvatar.getDataLength(), false,
|
||||
profileAvatar.getOutputStreamFactory(), null, null);
|
||||
formAttributes.getPolicy(), formAttributes.getAlgorithm(),
|
||||
formAttributes.getCredential(), formAttributes.getDate(),
|
||||
formAttributes.getSignature(), profileAvatar.getData(),
|
||||
profileAvatar.getContentType(), profileAvatar.getDataLength(), false,
|
||||
profileAvatar.getOutputStreamFactory(), null, null);
|
||||
|
||||
return Optional.of(formAttributes.getKey());
|
||||
}
|
||||
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
public Single<ServiceResponse<IdentityCheckResponse>> performIdentityCheck(@Nonnull IdentityCheckRequest request,
|
||||
@Nonnull ResponseMapper<IdentityCheckResponse> responseMapper)
|
||||
{
|
||||
Single<ServiceResponse<IdentityCheckResponse>> requestSingle = Single.fromCallable(() -> {
|
||||
try (Response response = getServiceConnection(PROFILE_BATCH_CHECK_PATH, "POST", jsonRequestBody(JsonUtil.toJson(request)), Collections.emptyMap(), SealedSenderAccess.NONE, false)) {
|
||||
String body = response.body() != null ? readBodyString(response.body()): "";
|
||||
return responseMapper.map(response.code(), body, response::header, false);
|
||||
}
|
||||
return formAttributes.getKey();
|
||||
});
|
||||
|
||||
return requestSingle
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(Schedulers.io())
|
||||
.onErrorReturn(ServiceResponse::forUnknownError);
|
||||
}
|
||||
|
||||
public BackupV2AuthCheckResponse checkSvr2AuthCredentials(@Nullable String number, @Nonnull List<String> passwords) throws IOException {
|
||||
@@ -1446,41 +1323,6 @@ public class PushServiceSocket {
|
||||
: null;
|
||||
}
|
||||
|
||||
private ListenableFuture<String> submitServiceRequest(String urlFragment,
|
||||
String method,
|
||||
String jsonBody,
|
||||
Map<String, String> headers,
|
||||
@Nullable SealedSenderAccess sealedSenderAccess)
|
||||
{
|
||||
OkHttpClient okHttpClient = buildOkHttpClient(sealedSenderAccess != null);
|
||||
Call call = okHttpClient.newCall(buildServiceRequest(urlFragment, method, jsonRequestBody(jsonBody), headers, sealedSenderAccess, false));
|
||||
|
||||
synchronized (connections) {
|
||||
connections.add(call);
|
||||
}
|
||||
|
||||
SettableFuture<String> bodyFuture = new SettableFuture<>();
|
||||
|
||||
call.enqueue(new Callback() {
|
||||
@Override
|
||||
public void onResponse(Call call, Response response) {
|
||||
try (ResponseBody body = response.body()) {
|
||||
validateServiceResponse(response);
|
||||
bodyFuture.set(readBodyString(body));
|
||||
} catch (IOException e) {
|
||||
bodyFuture.setException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Call call, IOException e) {
|
||||
bodyFuture.setException(e);
|
||||
}
|
||||
});
|
||||
|
||||
return bodyFuture;
|
||||
}
|
||||
|
||||
private Response makeServiceRequest(String urlFragment,
|
||||
String method,
|
||||
RequestBody body,
|
||||
|
||||
@@ -10,13 +10,4 @@ public final class PaymentsRegionException extends NonSuccessfulResponseCodeExce
|
||||
public PaymentsRegionException(int code) {
|
||||
super(code);
|
||||
}
|
||||
|
||||
/**
|
||||
* Promotes a 403 to this exception type.
|
||||
*/
|
||||
public static void responseCodeHandler(int responseCode, ResponseBody body, Function<String, String> getHeader) throws PaymentsRegionException {
|
||||
if (responseCode == 403) {
|
||||
throw new PaymentsRegionException(responseCode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user