mirror of
https://github.com/AsamK/signal-cli
synced 2025-08-29 18:40:39 +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
|
@Override
|
||||||
public void execute(Manager m) throws Throwable {
|
public void execute(Manager m) throws Throwable {
|
||||||
m.getRecipientProfile(recipientId, true);
|
m.refreshRecipientProfile(recipientId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@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.AttachmentUtils;
|
||||||
import org.asamk.signal.manager.util.IOUtils;
|
import org.asamk.signal.manager.util.IOUtils;
|
||||||
import org.asamk.signal.manager.util.KeyUtils;
|
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.StickerUtils;
|
||||||
import org.asamk.signal.manager.util.Utils;
|
import org.asamk.signal.manager.util.Utils;
|
||||||
import org.signal.libsignal.metadata.ProtocolInvalidMessageException;
|
import org.signal.libsignal.metadata.ProtocolInvalidMessageException;
|
||||||
import org.signal.libsignal.metadata.ProtocolUntrustedIdentityException;
|
import org.signal.libsignal.metadata.ProtocolUntrustedIdentityException;
|
||||||
import org.signal.zkgroup.InvalidInputException;
|
import org.signal.zkgroup.InvalidInputException;
|
||||||
import org.signal.zkgroup.profiles.ProfileKey;
|
import org.signal.zkgroup.profiles.ProfileKey;
|
||||||
import org.signal.zkgroup.profiles.ProfileKeyCredential;
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.whispersystems.libsignal.IdentityKey;
|
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.SignalServiceSyncMessage;
|
||||||
import org.whispersystems.signalservice.api.messages.multidevice.StickerPackOperationMessage;
|
import org.whispersystems.signalservice.api.messages.multidevice.StickerPackOperationMessage;
|
||||||
import org.whispersystems.signalservice.api.messages.multidevice.VerifiedMessage;
|
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.SignalServiceAddress;
|
||||||
import org.whispersystems.signalservice.api.push.exceptions.MissingConfigurationException;
|
import org.whispersystems.signalservice.api.push.exceptions.MissingConfigurationException;
|
||||||
import org.whispersystems.signalservice.api.util.DeviceNameUtil;
|
import org.whispersystems.signalservice.api.util.DeviceNameUtil;
|
||||||
|
@ -137,7 +133,6 @@ import java.nio.file.Files;
|
||||||
import java.security.SignatureException;
|
import java.security.SignatureException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Base64;
|
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
@ -205,26 +200,29 @@ public class Manager implements Closeable {
|
||||||
account.getSignalProtocolStore(),
|
account.getSignalProtocolStore(),
|
||||||
executor,
|
executor,
|
||||||
sessionLock);
|
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,
|
final var unidentifiedAccessHelper = new UnidentifiedAccessHelper(account::getProfileKey,
|
||||||
account.getProfileStore()::getProfileKey,
|
account.getProfileStore()::getProfileKey,
|
||||||
this::getRecipientProfile,
|
this::getRecipientProfile,
|
||||||
this::getSenderCertificate);
|
this::getSenderCertificate);
|
||||||
this.profileHelper = new ProfileHelper(account.getProfileStore()::getProfileKey,
|
this.profileHelper = new ProfileHelper(account,
|
||||||
|
dependencies,
|
||||||
|
avatarStore,
|
||||||
|
account.getProfileStore()::getProfileKey,
|
||||||
unidentifiedAccessHelper::getAccessFor,
|
unidentifiedAccessHelper::getAccessFor,
|
||||||
dependencies::getProfileService,
|
dependencies::getProfileService,
|
||||||
dependencies::getMessageReceiver,
|
dependencies::getMessageReceiver,
|
||||||
this::resolveSignalServiceAddress);
|
this::resolveSignalServiceAddress);
|
||||||
final GroupV2Helper groupV2Helper = new GroupV2Helper(this::getRecipientProfileKeyCredential,
|
final GroupV2Helper groupV2Helper = new GroupV2Helper(profileHelper::getRecipientProfileKeyCredential,
|
||||||
this::getRecipientProfile,
|
this::getRecipientProfile,
|
||||||
account::getSelfRecipientId,
|
account::getSelfRecipientId,
|
||||||
dependencies.getGroupsV2Operations(),
|
dependencies.getGroupsV2Operations(),
|
||||||
dependencies.getGroupsV2Api(),
|
dependencies.getGroupsV2Api(),
|
||||||
this::resolveSignalServiceAddress);
|
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,
|
this.sendHelper = new SendHelper(account,
|
||||||
dependencies,
|
dependencies,
|
||||||
unidentifiedAccessHelper,
|
unidentifiedAccessHelper,
|
||||||
|
@ -246,7 +244,7 @@ public class Manager implements Closeable {
|
||||||
return account.getUsername();
|
return account.getUsername();
|
||||||
}
|
}
|
||||||
|
|
||||||
public SignalServiceAddress getSelfAddress() {
|
private SignalServiceAddress getSelfAddress() {
|
||||||
return account.getSelfAddress();
|
return account.getSelfAddress();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -377,45 +375,12 @@ public class Manager implements Closeable {
|
||||||
public void setProfile(
|
public void setProfile(
|
||||||
String givenName, final String familyName, String about, String aboutEmoji, Optional<File> avatar
|
String givenName, final String familyName, String about, String aboutEmoji, Optional<File> avatar
|
||||||
) throws IOException {
|
) throws IOException {
|
||||||
var profile = getRecipientProfile(account.getSelfRecipientId());
|
profileHelper.setProfile(givenName, familyName, about, aboutEmoji, avatar);
|
||||||
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
|
sendSyncFetchProfileMessage();
|
||||||
? 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);
|
|
||||||
|
|
||||||
|
private void sendSyncFetchProfileMessage() throws IOException {
|
||||||
sendHelper.sendSyncMessage(SignalServiceSyncMessage.forFetchLatest(SignalServiceSyncMessage.FetchType.LOCAL_PROFILE));
|
sendHelper.sendSyncMessage(SignalServiceSyncMessage.forFetchLatest(SignalServiceSyncMessage.FetchType.LOCAL_PROFILE));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -522,134 +487,12 @@ public class Manager implements Closeable {
|
||||||
return record;
|
return record;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Profile getRecipientProfile(
|
public Profile getRecipientProfile(RecipientId recipientId) {
|
||||||
RecipientId recipientId
|
return profileHelper.getRecipientProfile(recipientId);
|
||||||
) {
|
|
||||||
return getRecipientProfile(recipientId, false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private final Set<RecipientId> pendingProfileRequest = new HashSet<>();
|
public void refreshRecipientProfile(RecipientId recipientId) {
|
||||||
|
profileHelper.refreshRecipientProfile(recipientId);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private Optional<SignalServiceAttachmentStream> createContactAvatarAttachment(SignalServiceAddress address) throws IOException {
|
private Optional<SignalServiceAttachmentStream> createContactAvatarAttachment(SignalServiceAddress address) throws IOException {
|
||||||
|
@ -1784,7 +1627,7 @@ public class Manager implements Closeable {
|
||||||
if (syncMessage.getFetchType().isPresent()) {
|
if (syncMessage.getFetchType().isPresent()) {
|
||||||
switch (syncMessage.getFetchType().get()) {
|
switch (syncMessage.getFetchType().get()) {
|
||||||
case LOCAL_PROFILE:
|
case LOCAL_PROFILE:
|
||||||
getRecipientProfile(account.getSelfRecipientId(), true);
|
actions.add(new RetrieveProfileAction(account.getSelfRecipientId()));
|
||||||
case STORAGE_MANIFEST:
|
case STORAGE_MANIFEST:
|
||||||
// TODO
|
// 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) {
|
public File getAttachmentFile(SignalServiceAttachmentRemoteId attachmentId) {
|
||||||
return attachmentStore.getAttachmentFile(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(
|
private void retrieveAttachment(
|
||||||
final SignalServiceAttachment attachment, final OutputStream outputStream
|
final SignalServiceAttachment attachment, final OutputStream outputStream
|
||||||
) throws IOException {
|
) throws IOException {
|
||||||
|
@ -2069,17 +1876,15 @@ public class Manager implements Closeable {
|
||||||
|
|
||||||
public String getContactOrProfileName(RecipientIdentifier.Single recipientIdentifier) {
|
public String getContactOrProfileName(RecipientIdentifier.Single recipientIdentifier) {
|
||||||
final var recipientId = resolveRecipient(recipientIdentifier);
|
final var recipientId = resolveRecipient(recipientIdentifier);
|
||||||
final var recipient = account.getRecipientStore().getRecipient(recipientId);
|
|
||||||
if (recipient == null) {
|
final var contact = account.getRecipientStore().getContact(recipientId);
|
||||||
return null;
|
if (contact != null && !Util.isEmpty(contact.getName())) {
|
||||||
|
return contact.getName();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (recipient.getContact() != null && !Util.isEmpty(recipient.getContact().getName())) {
|
final var profile = getRecipientProfile(recipientId);
|
||||||
return recipient.getContact().getName();
|
if (profile != null) {
|
||||||
}
|
return profile.getDisplayName();
|
||||||
|
|
||||||
if (recipient.getProfile() != null && recipient.getProfile() != null) {
|
|
||||||
return recipient.getProfile().getDisplayName();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
@ -2188,7 +1993,7 @@ public class Manager implements Closeable {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Retrieve profile to get the current identity key from the server
|
// 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;
|
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.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.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.libsignal.util.guava.Optional;
|
||||||
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess;
|
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess;
|
||||||
import org.whispersystems.signalservice.api.profiles.ProfileAndCredential;
|
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.api.services.ProfileService;
|
||||||
import org.whispersystems.signalservice.internal.ServiceResponse;
|
import org.whispersystems.signalservice.internal.ServiceResponse;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
import java.io.IOException;
|
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;
|
import io.reactivex.rxjava3.core.Single;
|
||||||
|
|
||||||
public final class ProfileHelper {
|
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 ProfileKeyProvider profileKeyProvider;
|
||||||
|
|
||||||
private final UnidentifiedAccessProvider unidentifiedAccessProvider;
|
private final UnidentifiedAccessProvider unidentifiedAccessProvider;
|
||||||
|
|
||||||
private final ProfileServiceProvider profileServiceProvider;
|
private final ProfileServiceProvider profileServiceProvider;
|
||||||
|
|
||||||
private final MessageReceiverProvider messageReceiverProvider;
|
private final MessageReceiverProvider messageReceiverProvider;
|
||||||
|
|
||||||
private final SignalServiceAddressResolver addressResolver;
|
private final SignalServiceAddressResolver addressResolver;
|
||||||
|
|
||||||
public ProfileHelper(
|
public ProfileHelper(
|
||||||
|
final SignalAccount account,
|
||||||
|
final SignalDependencies dependencies,
|
||||||
|
final AvatarStore avatarStore,
|
||||||
final ProfileKeyProvider profileKeyProvider,
|
final ProfileKeyProvider profileKeyProvider,
|
||||||
final UnidentifiedAccessProvider unidentifiedAccessProvider,
|
final UnidentifiedAccessProvider unidentifiedAccessProvider,
|
||||||
final ProfileServiceProvider profileServiceProvider,
|
final ProfileServiceProvider profileServiceProvider,
|
||||||
final MessageReceiverProvider messageReceiverProvider,
|
final MessageReceiverProvider messageReceiverProvider,
|
||||||
final SignalServiceAddressResolver addressResolver
|
final SignalServiceAddressResolver addressResolver
|
||||||
) {
|
) {
|
||||||
|
this.account = account;
|
||||||
|
this.dependencies = dependencies;
|
||||||
|
this.avatarStore = avatarStore;
|
||||||
this.profileKeyProvider = profileKeyProvider;
|
this.profileKeyProvider = profileKeyProvider;
|
||||||
this.unidentifiedAccessProvider = unidentifiedAccessProvider;
|
this.unidentifiedAccessProvider = unidentifiedAccessProvider;
|
||||||
this.profileServiceProvider = profileServiceProvider;
|
this.profileServiceProvider = profileServiceProvider;
|
||||||
|
@ -42,7 +69,193 @@ public final class ProfileHelper {
|
||||||
this.addressResolver = addressResolver;
|
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
|
RecipientId recipientId, SignalServiceProfile.RequestType requestType
|
||||||
) throws IOException {
|
) throws IOException {
|
||||||
try {
|
try {
|
||||||
|
@ -58,11 +271,7 @@ public final class ProfileHelper {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public SignalServiceProfile retrieveProfileSync(String username) throws IOException {
|
private Single<ProfileAndCredential> retrieveProfile(
|
||||||
return messageReceiverProvider.getMessageReceiver().retrieveProfileByUsername(username, Optional.absent());
|
|
||||||
}
|
|
||||||
|
|
||||||
public Single<ProfileAndCredential> retrieveProfile(
|
|
||||||
RecipientId recipientId, SignalServiceProfile.RequestType requestType
|
RecipientId recipientId, SignalServiceProfile.RequestType requestType
|
||||||
) throws IOException {
|
) throws IOException {
|
||||||
var unidentifiedAccess = getUnidentifiedAccess(recipientId);
|
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) {
|
private Optional<UnidentifiedAccess> getUnidentifiedAccess(RecipientId recipientId) {
|
||||||
var unidentifiedAccess = unidentifiedAccessProvider.getAccessFor(recipientId);
|
var unidentifiedAccess = unidentifiedAccessProvider.getAccessFor(recipientId);
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue