package org.asamk.signal.manager.helper;
-import org.asamk.signal.manager.SignalDependencies;
+import org.asamk.signal.manager.api.GroupNotFoundException;
+import org.asamk.signal.manager.api.NotAGroupMemberException;
+import org.asamk.signal.manager.api.PhoneNumberSharingMode;
+import org.asamk.signal.manager.api.Profile;
import org.asamk.signal.manager.config.ServiceConfig;
-import org.asamk.signal.manager.groups.GroupNotFoundException;
-import org.asamk.signal.manager.groups.NotAGroupMemberException;
+import org.asamk.signal.manager.internal.SignalDependencies;
+import org.asamk.signal.manager.jobs.SyncStorageJob;
import org.asamk.signal.manager.storage.SignalAccount;
import org.asamk.signal.manager.storage.groups.GroupInfoV2;
-import org.asamk.signal.manager.storage.recipients.Profile;
import org.asamk.signal.manager.storage.recipients.RecipientAddress;
import org.asamk.signal.manager.storage.recipients.RecipientId;
import org.asamk.signal.manager.util.IOUtils;
import org.asamk.signal.manager.util.PaymentUtils;
import org.asamk.signal.manager.util.ProfileUtils;
import org.asamk.signal.manager.util.Utils;
+import org.jetbrains.annotations.Nullable;
import org.signal.libsignal.protocol.IdentityKey;
import org.signal.libsignal.protocol.InvalidKeyException;
import org.signal.libsignal.zkgroup.profiles.ExpiringProfileKeyCredential;
import org.signal.libsignal.zkgroup.profiles.ProfileKey;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess;
+import org.whispersystems.signalservice.api.NetworkResultUtil;
+import org.whispersystems.signalservice.api.crypto.SealedSenderAccess;
import org.whispersystems.signalservice.api.profiles.AvatarUploadParams;
import org.whispersystems.signalservice.api.profiles.ProfileAndCredential;
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile;
import org.whispersystems.signalservice.api.services.ProfileService;
import org.whispersystems.signalservice.api.util.ExpiringProfileCredentialUtil;
-import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.file.Files;
public final class ProfileHelper {
- private final static Logger logger = LoggerFactory.getLogger(ProfileHelper.class);
+ private static final Logger logger = LoggerFactory.getLogger(ProfileHelper.class);
private final SignalAccount account;
private final SignalDependencies dependencies;
account.setProfileKey(profileKey);
context.getAccountHelper().updateAccountAttributes();
setProfile(true, true, null, null, null, null, null, null);
- // TODO update profile key in storage
+ account.getRecipientStore().rotateSelfStorageId();
+ context.getJobExecutor().enqueueJob(new SyncStorageJob());
final var recipientIds = account.getRecipientStore().getRecipientIdsWithEnabledProfileSharing();
for (final var recipientId : recipientIds) {
final var activeGroupIds = account.getGroupStore()
.getGroups()
.stream()
- .filter(g -> g instanceof GroupInfoV2 && g.isMember(selfRecipientId))
+ .filter(g -> g instanceof GroupInfoV2 && g.isMember(selfRecipientId) && g.isProfileSharingEnabled())
.map(g -> (GroupInfoV2) g)
.map(GroupInfoV2::getGroupId)
.toList();
}
public List<ExpiringProfileKeyCredential> getExpiringProfileKeyCredential(List<RecipientId> recipientIds) {
- try {
- account.getRecipientStore().setBulkUpdating(true);
- final var profileFetches = Flowable.fromIterable(recipientIds)
- .filter(recipientId -> !ExpiringProfileCredentialUtil.isValid(account.getProfileStore()
- .getExpiringProfileKeyCredential(recipientId)))
- .map(recipientId -> retrieveProfile(recipientId,
- SignalServiceProfile.RequestType.PROFILE_AND_CREDENTIAL).onErrorComplete());
- Maybe.merge(profileFetches, 10).blockingSubscribe();
- } finally {
- account.getRecipientStore().setBulkUpdating(false);
- }
+ final var profileFetches = Flowable.fromIterable(recipientIds)
+ .filter(recipientId -> !ExpiringProfileCredentialUtil.isValid(account.getProfileStore()
+ .getExpiringProfileKeyCredential(recipientId)))
+ .map(recipientId -> retrieveProfile(recipientId,
+ SignalServiceProfile.RequestType.PROFILE_AND_CREDENTIAL).onErrorComplete());
+ Maybe.merge(profileFetches, 10).blockingSubscribe();
return recipientIds.stream().map(r -> account.getProfileStore().getExpiringProfileKeyCredential(r)).toList();
}
final String familyName,
String about,
String aboutEmoji,
- Optional<File> avatar,
+ Optional<String> avatar,
byte[] mobileCoinAddress
) throws IOException {
setProfile(true, false, givenName, familyName, about, aboutEmoji, avatar, mobileCoinAddress);
final String familyName,
String about,
String aboutEmoji,
- Optional<File> avatar,
+ Optional<String> avatar,
byte[] mobileCoinAddress
) throws IOException {
var profile = getSelfProfile();
if (uploadProfile) {
final var streamDetails = avatar != null && avatar.isPresent()
- ? Utils.createStreamDetailsFromFile(avatar.get())
+ ? Utils.createStreamDetails(avatar.get())
+ .first()
: forceUploadAvatar && avatar == null ? context.getAvatarStore()
.retrieveProfileAvatar(account.getSelfRecipientAddress()) : null;
try (streamDetails) {
: avatar == null ? AvatarUploadParams.unchanged(true) : AvatarUploadParams.unchanged(false);
final var paymentsAddress = Optional.ofNullable(newProfile.getMobileCoinAddress())
.map(address -> PaymentUtils.signPaymentsAddress(address,
- account.getAciIdentityKeyPair().getPrivateKey()));
+ account.getAciIdentityKeyPair().getPrivateKey()))
+ .orElse(null);
logger.debug("Uploading new profile");
- final var avatarPath = dependencies.getAccountManager()
+ final var avatarPath = NetworkResultUtil.toSetProfileLegacy(dependencies.getProfileApi()
.setVersionedProfile(account.getAci(),
account.getProfileKey(),
newProfile.getInternalServiceName(),
newProfile.getAboutEmoji() == null ? "" : newProfile.getAboutEmoji(),
paymentsAddress,
avatarUploadParams,
- List.of(/* TODO implement support for badges */));
+ List.of(/* TODO implement support for badges */),
+ account.getConfigurationStore().getPhoneNumberSharingMode()
+ == PhoneNumberSharingMode.EVERYBODY));
if (!avatarUploadParams.keepTheSame) {
- builder.withAvatarUrlPath(avatarPath.orElse(null));
+ builder.withAvatarUrlPath(avatarPath);
}
newProfile = builder.build();
}
if (avatar != null) {
if (avatar.isPresent()) {
- context.getAvatarStore()
- .storeProfileAvatar(account.getSelfRecipientAddress(),
- outputStream -> IOUtils.copyFileToStream(avatar.get(), outputStream));
+ try (final var streamDetails = Utils.createStreamDetails(avatar.get()).first()) {
+ context.getAvatarStore()
+ .storeProfileAvatar(account.getSelfRecipientAddress(),
+ outputStream -> IOUtils.copyStream(streamDetails.getStream(), outputStream));
+ }
} else {
context.getAvatarStore().deleteProfileAvatar(account.getSelfRecipientAddress());
}
private List<Profile> getRecipientProfiles(Collection<RecipientId> recipientIds, boolean force) {
final var profileStore = account.getProfileStore();
- try {
- account.getRecipientStore().setBulkUpdating(true);
- final var profileFetches = Flowable.fromIterable(recipientIds)
- .filter(recipientId -> force || isProfileRefreshRequired(profileStore.getProfile(recipientId)))
- .map(recipientId -> retrieveProfile(recipientId,
- SignalServiceProfile.RequestType.PROFILE).onErrorComplete());
- Maybe.merge(profileFetches, 10).blockingSubscribe();
- } finally {
- account.getRecipientStore().setBulkUpdating(false);
- }
+ final var profileFetches = Flowable.fromIterable(recipientIds)
+ .filter(recipientId -> force || isProfileRefreshRequired(profileStore.getProfile(recipientId)))
+ .map(recipientId -> retrieveProfile(recipientId,
+ SignalServiceProfile.RequestType.PROFILE).onErrorComplete());
+ Maybe.merge(profileFetches, 10).blockingSubscribe();
return recipientIds.stream().map(profileStore::getProfile).toList();
}
return now - profile.getLastUpdateTimestamp() >= 6 * 60 * 60 * 1000;
}
- private SignalServiceProfile retrieveProfileSync(String username) throws IOException {
- final var locale = Utils.getDefaultLocale(Locale.US);
- return dependencies.getMessageReceiver().retrieveProfileByUsername(username, Optional.empty(), locale);
- }
-
private Profile decryptProfileAndDownloadAvatar(
- final RecipientId recipientId, final ProfileKey profileKey, final SignalServiceProfile encryptedProfile
+ final RecipientId recipientId,
+ final ProfileKey profileKey,
+ final SignalServiceProfile encryptedProfile
) {
final var avatarPath = encryptedProfile.getAvatar();
downloadProfileAvatar(recipientId, avatarPath, profileKey);
}
public void downloadProfileAvatar(
- final RecipientId recipientId, final String avatarPath, final ProfileKey profileKey
+ final RecipientId recipientId,
+ final String avatarPath,
+ final ProfileKey profileKey
) {
var profile = account.getProfileStore().getProfile(recipientId);
if (profile == null || !Objects.equals(avatarPath, profile.getAvatarUrlPath())) {
}
private Single<ProfileAndCredential> retrieveProfile(
- RecipientId recipientId, SignalServiceProfile.RequestType requestType
+ RecipientId recipientId,
+ SignalServiceProfile.RequestType requestType
) {
var unidentifiedAccess = getUnidentifiedAccess(recipientId);
var profileKey = Optional.ofNullable(account.getProfileStore().getProfileKey(recipientId));
.build();
}
+ if (recipientId.equals(account.getSelfRecipientId())) {
+ final var isUnrestricted = encryptedProfile.isUnrestrictedUnidentifiedAccess();
+ if (account.isUnrestrictedUnidentifiedAccess() != isUnrestricted) {
+ account.setUnrestrictedUnidentifiedAccess(isUnrestricted);
+ }
+ if (account.isPrimaryDevice() && profile != null && newProfile.getCapabilities()
+ .contains(Profile.Capability.storageServiceEncryptionV2Capability) && !profile.getCapabilities()
+ .contains(Profile.Capability.storageServiceEncryptionV2Capability)) {
+ context.getJobExecutor().enqueueJob(new SyncStorageJob(true));
+ }
+ }
+
try {
logger.trace("Storing identity");
final var identityKey = new IdentityKey(Base64.getDecoder().decode(encryptedProfile.getIdentityKey()));
- account.getIdentityKeyStore().saveIdentity(recipientId, identityKey);
+ account.getIdentityKeyStore().saveIdentity(p.getProfile().getServiceId(), identityKey);
} catch (InvalidKeyException ignored) {
logger.warn("Got invalid identity key in profile for {}",
context.getRecipientHelper().resolveSignalServiceAddress(recipientId).getIdentifier());
logger.trace("Storing profile");
account.getProfileStore().storeProfile(recipientId, newProfile);
+ account.getRecipientStore().markRegistered(recipientId, true);
logger.trace("Done handling retrieved profile");
}).doOnError(e -> {
.withUnidentifiedAccessMode(Profile.UnidentifiedAccessMode.UNKNOWN)
.withCapabilities(Set.of())
.build();
+ if (e instanceof NotFoundException) {
+ logger.debug("Marking recipient {} as unregistered after 404 profile fetch.", recipientId);
+ account.getRecipientStore().markRegistered(recipientId, false);
+ }
account.getProfileStore().storeProfile(recipientId, newProfile);
});
private Single<ProfileAndCredential> retrieveProfile(
SignalServiceAddress address,
Optional<ProfileKey> profileKey,
- Optional<UnidentifiedAccess> unidentifiedAccess,
+ @Nullable SealedSenderAccess unidentifiedAccess,
SignalServiceProfile.RequestType requestType
) {
final var profileService = dependencies.getProfileService();
});
}
- private void downloadProfileAvatar(
- RecipientAddress address, String avatarPath, ProfileKey profileKey
- ) {
+ private void downloadProfileAvatar(RecipientAddress address, String avatarPath, ProfileKey profileKey) {
if (avatarPath == null) {
try {
context.getAvatarStore().deleteProfileAvatar(address);
}
private void retrieveProfileAvatar(
- String avatarPath, ProfileKey profileKey, OutputStream outputStream
+ String avatarPath,
+ ProfileKey profileKey,
+ OutputStream outputStream
) throws IOException {
var tmpFile = IOUtils.createTempFile();
try (var input = dependencies.getMessageReceiver()
}
}
- private Optional<UnidentifiedAccess> getUnidentifiedAccess(RecipientId recipientId) {
- var unidentifiedAccess = context.getUnidentifiedAccessHelper().getAccessFor(recipientId, true);
-
- if (unidentifiedAccess.isPresent()) {
- return unidentifiedAccess.get().getTargetUnidentifiedAccess();
- }
-
- return Optional.empty();
+ private @Nullable SealedSenderAccess getUnidentifiedAccess(RecipientId recipientId) {
+ return context.getUnidentifiedAccessHelper().getSealedSenderAccessFor(recipientId, true);
}
}