mirror of
https://github.com/AsamK/signal-cli
synced 2025-08-29 10:30:38 +00:00
Move more profile functionality to ProfileHelper
This commit is contained in:
parent
cd3741d236
commit
e532a24cf8
3 changed files with 283 additions and 233 deletions
|
@ -170,7 +170,7 @@ class RetrieveProfileAction implements HandleAction {
|
|||
|
||||
@Override
|
||||
public void execute(Manager m) throws Throwable {
|
||||
m.getRecipientProfile(recipientId, true);
|
||||
m.refreshRecipientProfile(recipientId);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -58,14 +58,12 @@ import org.asamk.signal.manager.storage.stickers.StickerPackId;
|
|||
import org.asamk.signal.manager.util.AttachmentUtils;
|
||||
import org.asamk.signal.manager.util.IOUtils;
|
||||
import org.asamk.signal.manager.util.KeyUtils;
|
||||
import org.asamk.signal.manager.util.ProfileUtils;
|
||||
import org.asamk.signal.manager.util.StickerUtils;
|
||||
import org.asamk.signal.manager.util.Utils;
|
||||
import org.signal.libsignal.metadata.ProtocolInvalidMessageException;
|
||||
import org.signal.libsignal.metadata.ProtocolUntrustedIdentityException;
|
||||
import org.signal.zkgroup.InvalidInputException;
|
||||
import org.signal.zkgroup.profiles.ProfileKey;
|
||||
import org.signal.zkgroup.profiles.ProfileKeyCredential;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.libsignal.IdentityKey;
|
||||
|
@ -106,8 +104,6 @@ import org.whispersystems.signalservice.api.messages.multidevice.RequestMessage;
|
|||
import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage;
|
||||
import org.whispersystems.signalservice.api.messages.multidevice.StickerPackOperationMessage;
|
||||
import org.whispersystems.signalservice.api.messages.multidevice.VerifiedMessage;
|
||||
import org.whispersystems.signalservice.api.profiles.ProfileAndCredential;
|
||||
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile;
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.MissingConfigurationException;
|
||||
import org.whispersystems.signalservice.api.util.DeviceNameUtil;
|
||||
|
@ -137,7 +133,6 @@ import java.nio.file.Files;
|
|||
import java.security.SignatureException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Base64;
|
||||
import java.util.Collection;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
|
@ -205,26 +200,29 @@ public class Manager implements Closeable {
|
|||
account.getSignalProtocolStore(),
|
||||
executor,
|
||||
sessionLock);
|
||||
this.pinHelper = new PinHelper(dependencies.getKeyBackupService());
|
||||
this.avatarStore = new AvatarStore(pathConfig.getAvatarsPath());
|
||||
this.attachmentStore = new AttachmentStore(pathConfig.getAttachmentsPath());
|
||||
this.stickerPackStore = new StickerPackStore(pathConfig.getStickerPacksPath());
|
||||
|
||||
this.pinHelper = new PinHelper(dependencies.getKeyBackupService());
|
||||
final var unidentifiedAccessHelper = new UnidentifiedAccessHelper(account::getProfileKey,
|
||||
account.getProfileStore()::getProfileKey,
|
||||
this::getRecipientProfile,
|
||||
this::getSenderCertificate);
|
||||
this.profileHelper = new ProfileHelper(account.getProfileStore()::getProfileKey,
|
||||
this.profileHelper = new ProfileHelper(account,
|
||||
dependencies,
|
||||
avatarStore,
|
||||
account.getProfileStore()::getProfileKey,
|
||||
unidentifiedAccessHelper::getAccessFor,
|
||||
dependencies::getProfileService,
|
||||
dependencies::getMessageReceiver,
|
||||
this::resolveSignalServiceAddress);
|
||||
final GroupV2Helper groupV2Helper = new GroupV2Helper(this::getRecipientProfileKeyCredential,
|
||||
final GroupV2Helper groupV2Helper = new GroupV2Helper(profileHelper::getRecipientProfileKeyCredential,
|
||||
this::getRecipientProfile,
|
||||
account::getSelfRecipientId,
|
||||
dependencies.getGroupsV2Operations(),
|
||||
dependencies.getGroupsV2Api(),
|
||||
this::resolveSignalServiceAddress);
|
||||
this.avatarStore = new AvatarStore(pathConfig.getAvatarsPath());
|
||||
this.attachmentStore = new AttachmentStore(pathConfig.getAttachmentsPath());
|
||||
this.stickerPackStore = new StickerPackStore(pathConfig.getStickerPacksPath());
|
||||
this.sendHelper = new SendHelper(account,
|
||||
dependencies,
|
||||
unidentifiedAccessHelper,
|
||||
|
@ -246,7 +244,7 @@ public class Manager implements Closeable {
|
|||
return account.getUsername();
|
||||
}
|
||||
|
||||
public SignalServiceAddress getSelfAddress() {
|
||||
private SignalServiceAddress getSelfAddress() {
|
||||
return account.getSelfAddress();
|
||||
}
|
||||
|
||||
|
@ -377,45 +375,12 @@ public class Manager implements Closeable {
|
|||
public void setProfile(
|
||||
String givenName, final String familyName, String about, String aboutEmoji, Optional<File> avatar
|
||||
) throws IOException {
|
||||
var profile = getRecipientProfile(account.getSelfRecipientId());
|
||||
var builder = profile == null ? Profile.newBuilder() : Profile.newBuilder(profile);
|
||||
if (givenName != null) {
|
||||
builder.withGivenName(givenName);
|
||||
}
|
||||
if (familyName != null) {
|
||||
builder.withFamilyName(familyName);
|
||||
}
|
||||
if (about != null) {
|
||||
builder.withAbout(about);
|
||||
}
|
||||
if (aboutEmoji != null) {
|
||||
builder.withAboutEmoji(aboutEmoji);
|
||||
}
|
||||
var newProfile = builder.build();
|
||||
profileHelper.setProfile(givenName, familyName, about, aboutEmoji, avatar);
|
||||
|
||||
try (final var streamDetails = avatar == null
|
||||
? avatarStore.retrieveProfileAvatar(getSelfAddress())
|
||||
: avatar.isPresent() ? Utils.createStreamDetailsFromFile(avatar.get()) : null) {
|
||||
dependencies.getAccountManager()
|
||||
.setVersionedProfile(account.getUuid(),
|
||||
account.getProfileKey(),
|
||||
newProfile.getInternalServiceName(),
|
||||
newProfile.getAbout() == null ? "" : newProfile.getAbout(),
|
||||
newProfile.getAboutEmoji() == null ? "" : newProfile.getAboutEmoji(),
|
||||
Optional.absent(),
|
||||
streamDetails);
|
||||
}
|
||||
|
||||
if (avatar != null) {
|
||||
if (avatar.isPresent()) {
|
||||
avatarStore.storeProfileAvatar(getSelfAddress(),
|
||||
outputStream -> IOUtils.copyFileToStream(avatar.get(), outputStream));
|
||||
} else {
|
||||
avatarStore.deleteProfileAvatar(getSelfAddress());
|
||||
}
|
||||
}
|
||||
account.getProfileStore().storeProfile(account.getSelfRecipientId(), newProfile);
|
||||
sendSyncFetchProfileMessage();
|
||||
}
|
||||
|
||||
private void sendSyncFetchProfileMessage() throws IOException {
|
||||
sendHelper.sendSyncMessage(SignalServiceSyncMessage.forFetchLatest(SignalServiceSyncMessage.FetchType.LOCAL_PROFILE));
|
||||
}
|
||||
|
||||
|
@ -522,134 +487,12 @@ public class Manager implements Closeable {
|
|||
return record;
|
||||
}
|
||||
|
||||
public Profile getRecipientProfile(
|
||||
RecipientId recipientId
|
||||
) {
|
||||
return getRecipientProfile(recipientId, false);
|
||||
public Profile getRecipientProfile(RecipientId recipientId) {
|
||||
return profileHelper.getRecipientProfile(recipientId);
|
||||
}
|
||||
|
||||
private final Set<RecipientId> pendingProfileRequest = new HashSet<>();
|
||||
|
||||
Profile getRecipientProfile(
|
||||
RecipientId recipientId, boolean force
|
||||
) {
|
||||
var profile = account.getProfileStore().getProfile(recipientId);
|
||||
|
||||
var now = System.currentTimeMillis();
|
||||
// Profiles are cached for 24h before retrieving them again, unless forced
|
||||
if (!force && profile != null && now - profile.getLastUpdateTimestamp() < 24 * 60 * 60 * 1000) {
|
||||
return profile;
|
||||
}
|
||||
|
||||
synchronized (pendingProfileRequest) {
|
||||
if (pendingProfileRequest.contains(recipientId)) {
|
||||
return profile;
|
||||
}
|
||||
pendingProfileRequest.add(recipientId);
|
||||
}
|
||||
final SignalServiceProfile encryptedProfile;
|
||||
try {
|
||||
encryptedProfile = retrieveEncryptedProfile(recipientId);
|
||||
} finally {
|
||||
synchronized (pendingProfileRequest) {
|
||||
pendingProfileRequest.remove(recipientId);
|
||||
}
|
||||
}
|
||||
if (encryptedProfile == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
profile = decryptProfileIfKeyKnown(recipientId, encryptedProfile);
|
||||
account.getProfileStore().storeProfile(recipientId, profile);
|
||||
|
||||
return profile;
|
||||
}
|
||||
|
||||
private Profile decryptProfileIfKeyKnown(
|
||||
final RecipientId recipientId, final SignalServiceProfile encryptedProfile
|
||||
) {
|
||||
var profileKey = account.getProfileStore().getProfileKey(recipientId);
|
||||
if (profileKey == null) {
|
||||
return new Profile(System.currentTimeMillis(),
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
ProfileUtils.getUnidentifiedAccessMode(encryptedProfile, null),
|
||||
ProfileUtils.getCapabilities(encryptedProfile));
|
||||
}
|
||||
|
||||
return decryptProfileAndDownloadAvatar(recipientId, profileKey, encryptedProfile);
|
||||
}
|
||||
|
||||
private SignalServiceProfile retrieveEncryptedProfile(RecipientId recipientId) {
|
||||
try {
|
||||
return retrieveProfileAndCredential(recipientId, SignalServiceProfile.RequestType.PROFILE).getProfile();
|
||||
} catch (IOException e) {
|
||||
logger.warn("Failed to retrieve profile, ignoring: {}", e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private ProfileAndCredential retrieveProfileAndCredential(
|
||||
final RecipientId recipientId, final SignalServiceProfile.RequestType requestType
|
||||
) throws IOException {
|
||||
final var profileAndCredential = profileHelper.retrieveProfileSync(recipientId, requestType);
|
||||
final var profile = profileAndCredential.getProfile();
|
||||
|
||||
try {
|
||||
var newIdentity = account.getIdentityKeyStore()
|
||||
.saveIdentity(recipientId,
|
||||
new IdentityKey(Base64.getDecoder().decode(profile.getIdentityKey())),
|
||||
new Date());
|
||||
|
||||
if (newIdentity) {
|
||||
account.getSessionStore().archiveSessions(recipientId);
|
||||
}
|
||||
} catch (InvalidKeyException ignored) {
|
||||
logger.warn("Got invalid identity key in profile for {}",
|
||||
resolveSignalServiceAddress(recipientId).getIdentifier());
|
||||
}
|
||||
return profileAndCredential;
|
||||
}
|
||||
|
||||
private ProfileKeyCredential getRecipientProfileKeyCredential(RecipientId recipientId) {
|
||||
var profileKeyCredential = account.getProfileStore().getProfileKeyCredential(recipientId);
|
||||
if (profileKeyCredential != null) {
|
||||
return profileKeyCredential;
|
||||
}
|
||||
|
||||
ProfileAndCredential profileAndCredential;
|
||||
try {
|
||||
profileAndCredential = retrieveProfileAndCredential(recipientId,
|
||||
SignalServiceProfile.RequestType.PROFILE_AND_CREDENTIAL);
|
||||
} catch (IOException e) {
|
||||
logger.warn("Failed to retrieve profile key credential, ignoring: {}", e.getMessage());
|
||||
return null;
|
||||
}
|
||||
|
||||
profileKeyCredential = profileAndCredential.getProfileKeyCredential().orNull();
|
||||
account.getProfileStore().storeProfileKeyCredential(recipientId, profileKeyCredential);
|
||||
|
||||
var profileKey = account.getProfileStore().getProfileKey(recipientId);
|
||||
if (profileKey != null) {
|
||||
final var profile = decryptProfileAndDownloadAvatar(recipientId,
|
||||
profileKey,
|
||||
profileAndCredential.getProfile());
|
||||
account.getProfileStore().storeProfile(recipientId, profile);
|
||||
}
|
||||
|
||||
return profileKeyCredential;
|
||||
}
|
||||
|
||||
private Profile decryptProfileAndDownloadAvatar(
|
||||
final RecipientId recipientId, final ProfileKey profileKey, final SignalServiceProfile encryptedProfile
|
||||
) {
|
||||
if (encryptedProfile.getAvatar() != null) {
|
||||
downloadProfileAvatar(resolveSignalServiceAddress(recipientId), encryptedProfile.getAvatar(), profileKey);
|
||||
}
|
||||
|
||||
return ProfileUtils.decryptProfile(profileKey, encryptedProfile);
|
||||
public void refreshRecipientProfile(RecipientId recipientId) {
|
||||
profileHelper.refreshRecipientProfile(recipientId);
|
||||
}
|
||||
|
||||
private Optional<SignalServiceAttachmentStream> createContactAvatarAttachment(SignalServiceAddress address) throws IOException {
|
||||
|
@ -1784,7 +1627,7 @@ public class Manager implements Closeable {
|
|||
if (syncMessage.getFetchType().isPresent()) {
|
||||
switch (syncMessage.getFetchType().get()) {
|
||||
case LOCAL_PROFILE:
|
||||
getRecipientProfile(account.getSelfRecipientId(), true);
|
||||
actions.add(new RetrieveProfileAction(account.getSelfRecipientId()));
|
||||
case STORAGE_MANIFEST:
|
||||
// TODO
|
||||
}
|
||||
|
@ -1820,20 +1663,6 @@ public class Manager implements Closeable {
|
|||
}
|
||||
}
|
||||
|
||||
private void downloadProfileAvatar(
|
||||
SignalServiceAddress address, String avatarPath, ProfileKey profileKey
|
||||
) {
|
||||
try {
|
||||
avatarStore.storeProfileAvatar(address,
|
||||
outputStream -> retrieveProfileAvatar(avatarPath, profileKey, outputStream));
|
||||
} catch (Throwable e) {
|
||||
if (e instanceof AssertionError && e.getCause() instanceof InterruptedException) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
logger.warn("Failed to download profile avatar, ignoring: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public File getAttachmentFile(SignalServiceAttachmentRemoteId attachmentId) {
|
||||
return attachmentStore.getAttachmentFile(attachmentId);
|
||||
}
|
||||
|
@ -1862,28 +1691,6 @@ public class Manager implements Closeable {
|
|||
}
|
||||
}
|
||||
|
||||
private void retrieveProfileAvatar(
|
||||
String avatarPath, ProfileKey profileKey, OutputStream outputStream
|
||||
) throws IOException {
|
||||
var tmpFile = IOUtils.createTempFile();
|
||||
try (var input = dependencies.getMessageReceiver()
|
||||
.retrieveProfileAvatar(avatarPath,
|
||||
tmpFile,
|
||||
profileKey,
|
||||
ServiceConfig.AVATAR_DOWNLOAD_FAILSAFE_MAX_SIZE)) {
|
||||
// Use larger buffer size to prevent AssertionError: Need: 12272 but only have: 8192 ...
|
||||
IOUtils.copyStream(input, outputStream, (int) ServiceConfig.AVATAR_DOWNLOAD_FAILSAFE_MAX_SIZE);
|
||||
} finally {
|
||||
try {
|
||||
Files.delete(tmpFile.toPath());
|
||||
} catch (IOException e) {
|
||||
logger.warn("Failed to delete received profile avatar temp file “{}”, ignoring: {}",
|
||||
tmpFile,
|
||||
e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void retrieveAttachment(
|
||||
final SignalServiceAttachment attachment, final OutputStream outputStream
|
||||
) throws IOException {
|
||||
|
@ -2069,17 +1876,15 @@ public class Manager implements Closeable {
|
|||
|
||||
public String getContactOrProfileName(RecipientIdentifier.Single recipientIdentifier) {
|
||||
final var recipientId = resolveRecipient(recipientIdentifier);
|
||||
final var recipient = account.getRecipientStore().getRecipient(recipientId);
|
||||
if (recipient == null) {
|
||||
return null;
|
||||
|
||||
final var contact = account.getRecipientStore().getContact(recipientId);
|
||||
if (contact != null && !Util.isEmpty(contact.getName())) {
|
||||
return contact.getName();
|
||||
}
|
||||
|
||||
if (recipient.getContact() != null && !Util.isEmpty(recipient.getContact().getName())) {
|
||||
return recipient.getContact().getName();
|
||||
}
|
||||
|
||||
if (recipient.getProfile() != null && recipient.getProfile() != null) {
|
||||
return recipient.getProfile().getDisplayName();
|
||||
final var profile = getRecipientProfile(recipientId);
|
||||
if (profile != null) {
|
||||
return profile.getDisplayName();
|
||||
}
|
||||
|
||||
return null;
|
||||
|
@ -2188,7 +1993,7 @@ public class Manager implements Closeable {
|
|||
}
|
||||
} else {
|
||||
// Retrieve profile to get the current identity key from the server
|
||||
retrieveEncryptedProfile(recipientId);
|
||||
refreshRecipientProfile(recipientId);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,20 @@
|
|||
package org.asamk.signal.manager.helper;
|
||||
|
||||
import org.asamk.signal.manager.AvatarStore;
|
||||
import org.asamk.signal.manager.SignalDependencies;
|
||||
import org.asamk.signal.manager.config.ServiceConfig;
|
||||
import org.asamk.signal.manager.storage.SignalAccount;
|
||||
import org.asamk.signal.manager.storage.recipients.Profile;
|
||||
import org.asamk.signal.manager.storage.recipients.RecipientId;
|
||||
import org.asamk.signal.manager.util.IOUtils;
|
||||
import org.asamk.signal.manager.util.ProfileUtils;
|
||||
import org.asamk.signal.manager.util.Utils;
|
||||
import org.signal.zkgroup.profiles.ProfileKey;
|
||||
import org.signal.zkgroup.profiles.ProfileKeyCredential;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.libsignal.IdentityKey;
|
||||
import org.whispersystems.libsignal.InvalidKeyException;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess;
|
||||
import org.whispersystems.signalservice.api.profiles.ProfileAndCredential;
|
||||
|
@ -12,29 +25,43 @@ import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException
|
|||
import org.whispersystems.signalservice.api.services.ProfileService;
|
||||
import org.whispersystems.signalservice.internal.ServiceResponse;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.util.Base64;
|
||||
import java.util.Date;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import io.reactivex.rxjava3.core.Single;
|
||||
|
||||
public final class ProfileHelper {
|
||||
|
||||
private final static Logger logger = LoggerFactory.getLogger(ProfileHelper.class);
|
||||
|
||||
private final SignalAccount account;
|
||||
private final SignalDependencies dependencies;
|
||||
private final AvatarStore avatarStore;
|
||||
private final ProfileKeyProvider profileKeyProvider;
|
||||
|
||||
private final UnidentifiedAccessProvider unidentifiedAccessProvider;
|
||||
|
||||
private final ProfileServiceProvider profileServiceProvider;
|
||||
|
||||
private final MessageReceiverProvider messageReceiverProvider;
|
||||
|
||||
private final SignalServiceAddressResolver addressResolver;
|
||||
|
||||
public ProfileHelper(
|
||||
final SignalAccount account,
|
||||
final SignalDependencies dependencies,
|
||||
final AvatarStore avatarStore,
|
||||
final ProfileKeyProvider profileKeyProvider,
|
||||
final UnidentifiedAccessProvider unidentifiedAccessProvider,
|
||||
final ProfileServiceProvider profileServiceProvider,
|
||||
final MessageReceiverProvider messageReceiverProvider,
|
||||
final SignalServiceAddressResolver addressResolver
|
||||
) {
|
||||
this.account = account;
|
||||
this.dependencies = dependencies;
|
||||
this.avatarStore = avatarStore;
|
||||
this.profileKeyProvider = profileKeyProvider;
|
||||
this.unidentifiedAccessProvider = unidentifiedAccessProvider;
|
||||
this.profileServiceProvider = profileServiceProvider;
|
||||
|
@ -42,7 +69,193 @@ public final class ProfileHelper {
|
|||
this.addressResolver = addressResolver;
|
||||
}
|
||||
|
||||
public ProfileAndCredential retrieveProfileSync(
|
||||
public Profile getRecipientProfile(RecipientId recipientId) {
|
||||
return getRecipientProfile(recipientId, false);
|
||||
}
|
||||
|
||||
public void refreshRecipientProfile(RecipientId recipientId) {
|
||||
getRecipientProfile(recipientId, true);
|
||||
}
|
||||
|
||||
public ProfileKeyCredential getRecipientProfileKeyCredential(RecipientId recipientId) {
|
||||
var profileKeyCredential = account.getProfileStore().getProfileKeyCredential(recipientId);
|
||||
if (profileKeyCredential != null) {
|
||||
return profileKeyCredential;
|
||||
}
|
||||
|
||||
ProfileAndCredential profileAndCredential;
|
||||
try {
|
||||
profileAndCredential = retrieveProfileAndCredential(recipientId,
|
||||
SignalServiceProfile.RequestType.PROFILE_AND_CREDENTIAL);
|
||||
} catch (IOException e) {
|
||||
logger.warn("Failed to retrieve profile key credential, ignoring: {}", e.getMessage());
|
||||
return null;
|
||||
}
|
||||
|
||||
profileKeyCredential = profileAndCredential.getProfileKeyCredential().orNull();
|
||||
account.getProfileStore().storeProfileKeyCredential(recipientId, profileKeyCredential);
|
||||
|
||||
var profileKey = account.getProfileStore().getProfileKey(recipientId);
|
||||
if (profileKey != null) {
|
||||
final var profile = decryptProfileAndDownloadAvatar(recipientId,
|
||||
profileKey,
|
||||
profileAndCredential.getProfile());
|
||||
account.getProfileStore().storeProfile(recipientId, profile);
|
||||
}
|
||||
|
||||
return profileKeyCredential;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param givenName if null, the previous givenName will be kept
|
||||
* @param familyName if null, the previous familyName will be kept
|
||||
* @param about if null, the previous about text will be kept
|
||||
* @param aboutEmoji if null, the previous about emoji will be kept
|
||||
* @param avatar if avatar is null the image from the local avatar store is used (if present),
|
||||
*/
|
||||
public void setProfile(
|
||||
String givenName, final String familyName, String about, String aboutEmoji, Optional<File> avatar
|
||||
) throws IOException {
|
||||
var profile = getRecipientProfile(account.getSelfRecipientId());
|
||||
var builder = profile == null ? Profile.newBuilder() : Profile.newBuilder(profile);
|
||||
if (givenName != null) {
|
||||
builder.withGivenName(givenName);
|
||||
}
|
||||
if (familyName != null) {
|
||||
builder.withFamilyName(familyName);
|
||||
}
|
||||
if (about != null) {
|
||||
builder.withAbout(about);
|
||||
}
|
||||
if (aboutEmoji != null) {
|
||||
builder.withAboutEmoji(aboutEmoji);
|
||||
}
|
||||
var newProfile = builder.build();
|
||||
|
||||
try (final var streamDetails = avatar == null
|
||||
? avatarStore.retrieveProfileAvatar(account.getSelfAddress())
|
||||
: avatar.isPresent() ? Utils.createStreamDetailsFromFile(avatar.get()) : null) {
|
||||
dependencies.getAccountManager()
|
||||
.setVersionedProfile(account.getUuid(),
|
||||
account.getProfileKey(),
|
||||
newProfile.getInternalServiceName(),
|
||||
newProfile.getAbout() == null ? "" : newProfile.getAbout(),
|
||||
newProfile.getAboutEmoji() == null ? "" : newProfile.getAboutEmoji(),
|
||||
Optional.absent(),
|
||||
streamDetails);
|
||||
}
|
||||
|
||||
if (avatar != null) {
|
||||
if (avatar.isPresent()) {
|
||||
avatarStore.storeProfileAvatar(account.getSelfAddress(),
|
||||
outputStream -> IOUtils.copyFileToStream(avatar.get(), outputStream));
|
||||
} else {
|
||||
avatarStore.deleteProfileAvatar(account.getSelfAddress());
|
||||
}
|
||||
}
|
||||
account.getProfileStore().storeProfile(account.getSelfRecipientId(), newProfile);
|
||||
}
|
||||
|
||||
private final Set<RecipientId> pendingProfileRequest = new HashSet<>();
|
||||
|
||||
private Profile getRecipientProfile(RecipientId recipientId, boolean force) {
|
||||
var profile = account.getProfileStore().getProfile(recipientId);
|
||||
|
||||
var now = System.currentTimeMillis();
|
||||
// Profiles are cached for 24h before retrieving them again, unless forced
|
||||
if (!force && profile != null && now - profile.getLastUpdateTimestamp() < 24 * 60 * 60 * 1000) {
|
||||
return profile;
|
||||
}
|
||||
|
||||
synchronized (pendingProfileRequest) {
|
||||
if (pendingProfileRequest.contains(recipientId)) {
|
||||
return profile;
|
||||
}
|
||||
pendingProfileRequest.add(recipientId);
|
||||
}
|
||||
final SignalServiceProfile encryptedProfile;
|
||||
try {
|
||||
encryptedProfile = retrieveEncryptedProfile(recipientId);
|
||||
} finally {
|
||||
synchronized (pendingProfileRequest) {
|
||||
pendingProfileRequest.remove(recipientId);
|
||||
}
|
||||
}
|
||||
if (encryptedProfile == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
profile = decryptProfileIfKeyKnown(recipientId, encryptedProfile);
|
||||
account.getProfileStore().storeProfile(recipientId, profile);
|
||||
|
||||
return profile;
|
||||
}
|
||||
|
||||
private Profile decryptProfileIfKeyKnown(
|
||||
final RecipientId recipientId, final SignalServiceProfile encryptedProfile
|
||||
) {
|
||||
var profileKey = account.getProfileStore().getProfileKey(recipientId);
|
||||
if (profileKey == null) {
|
||||
return new Profile(System.currentTimeMillis(),
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
ProfileUtils.getUnidentifiedAccessMode(encryptedProfile, null),
|
||||
ProfileUtils.getCapabilities(encryptedProfile));
|
||||
}
|
||||
|
||||
return decryptProfileAndDownloadAvatar(recipientId, profileKey, encryptedProfile);
|
||||
}
|
||||
|
||||
private SignalServiceProfile retrieveEncryptedProfile(RecipientId recipientId) {
|
||||
try {
|
||||
return retrieveProfileAndCredential(recipientId, SignalServiceProfile.RequestType.PROFILE).getProfile();
|
||||
} catch (IOException e) {
|
||||
logger.warn("Failed to retrieve profile, ignoring: {}", e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private SignalServiceProfile retrieveProfileSync(String username) throws IOException {
|
||||
return messageReceiverProvider.getMessageReceiver().retrieveProfileByUsername(username, Optional.absent());
|
||||
}
|
||||
|
||||
private ProfileAndCredential retrieveProfileAndCredential(
|
||||
final RecipientId recipientId, final SignalServiceProfile.RequestType requestType
|
||||
) throws IOException {
|
||||
final var profileAndCredential = retrieveProfileSync(recipientId, requestType);
|
||||
final var profile = profileAndCredential.getProfile();
|
||||
|
||||
try {
|
||||
var newIdentity = account.getIdentityKeyStore()
|
||||
.saveIdentity(recipientId,
|
||||
new IdentityKey(Base64.getDecoder().decode(profile.getIdentityKey())),
|
||||
new Date());
|
||||
|
||||
if (newIdentity) {
|
||||
account.getSessionStore().archiveSessions(recipientId);
|
||||
}
|
||||
} catch (InvalidKeyException ignored) {
|
||||
logger.warn("Got invalid identity key in profile for {}",
|
||||
addressResolver.resolveSignalServiceAddress(recipientId).getIdentifier());
|
||||
}
|
||||
return profileAndCredential;
|
||||
}
|
||||
|
||||
private Profile decryptProfileAndDownloadAvatar(
|
||||
final RecipientId recipientId, final ProfileKey profileKey, final SignalServiceProfile encryptedProfile
|
||||
) {
|
||||
if (encryptedProfile.getAvatar() != null) {
|
||||
downloadProfileAvatar(addressResolver.resolveSignalServiceAddress(recipientId),
|
||||
encryptedProfile.getAvatar(),
|
||||
profileKey);
|
||||
}
|
||||
|
||||
return ProfileUtils.decryptProfile(profileKey, encryptedProfile);
|
||||
}
|
||||
|
||||
private ProfileAndCredential retrieveProfileSync(
|
||||
RecipientId recipientId, SignalServiceProfile.RequestType requestType
|
||||
) throws IOException {
|
||||
try {
|
||||
|
@ -58,11 +271,7 @@ public final class ProfileHelper {
|
|||
}
|
||||
}
|
||||
|
||||
public SignalServiceProfile retrieveProfileSync(String username) throws IOException {
|
||||
return messageReceiverProvider.getMessageReceiver().retrieveProfileByUsername(username, Optional.absent());
|
||||
}
|
||||
|
||||
public Single<ProfileAndCredential> retrieveProfile(
|
||||
private Single<ProfileAndCredential> retrieveProfile(
|
||||
RecipientId recipientId, SignalServiceProfile.RequestType requestType
|
||||
) throws IOException {
|
||||
var unidentifiedAccess = getUnidentifiedAccess(recipientId);
|
||||
|
@ -106,6 +315,42 @@ public final class ProfileHelper {
|
|||
});
|
||||
}
|
||||
|
||||
private void downloadProfileAvatar(
|
||||
SignalServiceAddress address, String avatarPath, ProfileKey profileKey
|
||||
) {
|
||||
try {
|
||||
avatarStore.storeProfileAvatar(address,
|
||||
outputStream -> retrieveProfileAvatar(avatarPath, profileKey, outputStream));
|
||||
} catch (Throwable e) {
|
||||
if (e instanceof AssertionError && e.getCause() instanceof InterruptedException) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
logger.warn("Failed to download profile avatar, ignoring: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void retrieveProfileAvatar(
|
||||
String avatarPath, ProfileKey profileKey, OutputStream outputStream
|
||||
) throws IOException {
|
||||
var tmpFile = IOUtils.createTempFile();
|
||||
try (var input = dependencies.getMessageReceiver()
|
||||
.retrieveProfileAvatar(avatarPath,
|
||||
tmpFile,
|
||||
profileKey,
|
||||
ServiceConfig.AVATAR_DOWNLOAD_FAILSAFE_MAX_SIZE)) {
|
||||
// Use larger buffer size to prevent AssertionError: Need: 12272 but only have: 8192 ...
|
||||
IOUtils.copyStream(input, outputStream, (int) ServiceConfig.AVATAR_DOWNLOAD_FAILSAFE_MAX_SIZE);
|
||||
} finally {
|
||||
try {
|
||||
Files.delete(tmpFile.toPath());
|
||||
} catch (IOException e) {
|
||||
logger.warn("Failed to delete received profile avatar temp file “{}”, ignoring: {}",
|
||||
tmpFile,
|
||||
e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Optional<UnidentifiedAccess> getUnidentifiedAccess(RecipientId recipientId) {
|
||||
var unidentifiedAccess = unidentifiedAccessProvider.getAccessFor(recipientId);
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue