Create an AvatarStore

This commit is contained in:
AsamK 2021-01-13 22:35:58 +01:00
parent 6bd857ad8b
commit a643609ed2
9 changed files with 239 additions and 159 deletions

View file

@ -6,6 +6,7 @@ import net.sourceforge.argparse4j.inf.Namespace;
import net.sourceforge.argparse4j.inf.Subparser; import net.sourceforge.argparse4j.inf.Subparser;
import org.asamk.signal.manager.Manager; import org.asamk.signal.manager.Manager;
import org.whispersystems.libsignal.util.guava.Optional;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
@ -14,7 +15,7 @@ public class UpdateProfileCommand implements LocalCommand {
@Override @Override
public void attachToSubparser(final Subparser subparser) { public void attachToSubparser(final Subparser subparser) {
final MutuallyExclusiveGroup avatarOptions = subparser.addMutuallyExclusiveGroup().required(true); final MutuallyExclusiveGroup avatarOptions = subparser.addMutuallyExclusiveGroup();
avatarOptions.addArgument("--avatar").help("Path to new profile avatar"); avatarOptions.addArgument("--avatar").help("Path to new profile avatar");
avatarOptions.addArgument("--remove-avatar").action(Arguments.storeTrue()); avatarOptions.addArgument("--remove-avatar").action(Arguments.storeTrue());
@ -30,7 +31,9 @@ public class UpdateProfileCommand implements LocalCommand {
boolean removeAvatar = ns.getBoolean("remove_avatar"); boolean removeAvatar = ns.getBoolean("remove_avatar");
try { try {
File avatarFile = removeAvatar ? null : new File(avatarPath); Optional<File> avatarFile = removeAvatar
? Optional.absent()
: avatarPath == null ? null : Optional.of(new File(avatarPath));
m.setProfile(name, avatarFile); m.setProfile(name, avatarFile);
} catch (IOException e) { } catch (IOException e) {
System.err.println("UpdateAccount error: " + e.getMessage()); System.err.println("UpdateAccount error: " + e.getMessage());

View file

@ -14,6 +14,7 @@ import org.whispersystems.signalservice.api.messages.SendMessageResult;
import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.util.InvalidNumberException; import org.whispersystems.signalservice.api.util.InvalidNumberException;
import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@ -193,7 +194,7 @@ public class DbusSignalImpl implements Signal {
} }
final Pair<GroupId, List<SendMessageResult>> results = m.updateGroup(groupId == null final Pair<GroupId, List<SendMessageResult>> results = m.updateGroup(groupId == null
? null ? null
: GroupId.unknownVersion(groupId), name, members, avatar); : GroupId.unknownVersion(groupId), name, members, avatar == null ? null : new File(avatar));
checkSendMessageResults(0, results.second()); checkSendMessageResults(0, results.second());
return results.first().serialize(); return results.first().serialize();
} catch (IOException e) { } catch (IOException e) {

View file

@ -0,0 +1,91 @@
package org.asamk.signal.manager;
import org.asamk.signal.manager.groups.GroupId;
import org.asamk.signal.manager.util.IOUtils;
import org.asamk.signal.manager.util.Utils;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.util.StreamDetails;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.file.Files;
public class AvatarStore {
private final File avatarsPath;
public AvatarStore(final File avatarsPath) {
this.avatarsPath = avatarsPath;
}
public StreamDetails retrieveContactAvatar(SignalServiceAddress address) throws IOException {
return retrieveAvatar(getContactAvatarFile(address));
}
public StreamDetails retrieveProfileAvatar(SignalServiceAddress address) throws IOException {
return retrieveAvatar(getProfileAvatarFile(address));
}
public StreamDetails retrieveGroupAvatar(GroupId groupId) throws IOException {
final File groupAvatarFile = getGroupAvatarFile(groupId);
return retrieveAvatar(groupAvatarFile);
}
public void storeContactAvatar(SignalServiceAddress address, AvatarStorer storer) throws IOException {
storeAvatar(getContactAvatarFile(address), storer);
}
public void storeProfileAvatar(SignalServiceAddress address, AvatarStorer storer) throws IOException {
storeAvatar(getProfileAvatarFile(address), storer);
}
public void storeGroupAvatar(GroupId groupId, AvatarStorer storer) throws IOException {
storeAvatar(getGroupAvatarFile(groupId), storer);
}
public void deleteProfileAvatar(SignalServiceAddress address) throws IOException {
deleteAvatar(getProfileAvatarFile(address));
}
private StreamDetails retrieveAvatar(final File avatarFile) throws IOException {
if (!avatarFile.exists()) {
return null;
}
return Utils.createStreamDetailsFromFile(avatarFile);
}
private void storeAvatar(final File avatarFile, final AvatarStorer storer) throws IOException {
createAvatarsDir();
try (OutputStream output = new FileOutputStream(avatarFile)) {
storer.store(output);
}
}
private void deleteAvatar(final File avatarFile) throws IOException {
Files.delete(avatarFile.toPath());
}
private File getGroupAvatarFile(GroupId groupId) {
return new File(avatarsPath, "group-" + groupId.toBase64().replace("/", "_"));
}
private File getContactAvatarFile(SignalServiceAddress address) {
return new File(avatarsPath, "contact-" + address);
}
private File getProfileAvatarFile(SignalServiceAddress address) {
return new File(avatarsPath, "profile-" + address.getLegacyIdentifier());
}
private void createAvatarsDir() throws IOException {
IOUtils.createPrivateDirectories(avatarsPath);
}
@FunctionalInterface
public interface AvatarStorer {
void store(OutputStream outputStream) throws IOException;
}
}

View file

@ -156,8 +156,6 @@ import java.net.URISyntaxException;
import java.net.URLEncoder; import java.net.URLEncoder;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.security.SignatureException; import java.security.SignatureException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
@ -204,6 +202,7 @@ public class Manager implements Closeable {
private final ProfileHelper profileHelper; private final ProfileHelper profileHelper;
private final GroupHelper groupHelper; private final GroupHelper groupHelper;
private final PinHelper pinHelper; private final PinHelper pinHelper;
private final AvatarStore avatarStore;
Manager( Manager(
SignalAccount account, SignalAccount account,
@ -259,6 +258,7 @@ public class Manager implements Closeable {
groupsV2Operations, groupsV2Operations,
groupsV2Api, groupsV2Api,
this::getGroupAuthForToday); this::getGroupAuthForToday);
this.avatarStore = new AvatarStore(pathConfig.getAvatarsPath());
} }
public String getUsername() { public String getUsername() {
@ -338,10 +338,25 @@ public class Manager implements Closeable {
account.isDiscoverableByPhoneNumber()); account.isDiscoverableByPhoneNumber());
} }
public void setProfile(String name, File avatar) throws IOException { /**
try (final StreamDetails streamDetails = avatar == null ? null : Utils.createStreamDetailsFromFile(avatar)) { * @param avatar if avatar is null the image from the local avatar store is used (if present),
* if it's Optional.absent(), the avatar will be removed
*/
public void setProfile(String name, Optional<File> avatar) throws IOException {
try (final StreamDetails streamDetails = avatar == null
? avatarStore.retrieveProfileAvatar(getSelfAddress())
: avatar.isPresent() ? Utils.createStreamDetailsFromFile(avatar.get()) : null) {
accountManager.setVersionedProfile(account.getUuid(), account.getProfileKey(), name, streamDetails); accountManager.setVersionedProfile(account.getUuid(), account.getProfileKey(), name, streamDetails);
} }
if (avatar != null) {
if (avatar.isPresent()) {
avatarStore.storeProfileAvatar(getSelfAddress(),
outputStream -> IOUtils.copyFileToStream(avatar.get(), outputStream));
} else {
avatarStore.deleteProfileAvatar(getSelfAddress());
}
}
} }
public void unregister() throws IOException { public void unregister() throws IOException {
@ -539,14 +554,13 @@ public class Manager implements Closeable {
private SignalProfile decryptProfile( private SignalProfile decryptProfile(
final SignalServiceAddress address, final ProfileKey profileKey, final SignalServiceProfile encryptedProfile final SignalServiceAddress address, final ProfileKey profileKey, final SignalServiceProfile encryptedProfile
) { ) {
File avatarFile = null; if (encryptedProfile.getAvatar() != null) {
try { try {
avatarFile = encryptedProfile.getAvatar() == null retrieveProfileAvatar(address, encryptedProfile.getAvatar(), profileKey);
? null
: retrieveProfileAvatar(address, encryptedProfile.getAvatar(), profileKey);
} catch (Throwable e) { } catch (Throwable e) {
logger.warn("Failed to retrieve profile avatar, ignoring: {}", e.getMessage()); logger.warn("Failed to retrieve profile avatar, ignoring: {}", e.getMessage());
} }
}
ProfileCipher profileCipher = new ProfileCipher(profileKey); ProfileCipher profileCipher = new ProfileCipher(profileKey);
try { try {
@ -569,7 +583,6 @@ public class Manager implements Closeable {
} }
return new SignalProfile(encryptedProfile.getIdentityKey(), return new SignalProfile(encryptedProfile.getIdentityKey(),
name, name,
avatarFile,
unidentifiedAccess, unidentifiedAccess,
encryptedProfile.isUnrestrictedUnidentifiedAccess(), encryptedProfile.isUnrestrictedUnidentifiedAccess(),
encryptedProfile.getCapabilities()); encryptedProfile.getCapabilities());
@ -579,21 +592,21 @@ public class Manager implements Closeable {
} }
private Optional<SignalServiceAttachmentStream> createGroupAvatarAttachment(GroupId groupId) throws IOException { private Optional<SignalServiceAttachmentStream> createGroupAvatarAttachment(GroupId groupId) throws IOException {
File file = getGroupAvatarFile(groupId); final StreamDetails streamDetails = avatarStore.retrieveGroupAvatar(groupId);
if (!file.exists()) { if (streamDetails == null) {
return Optional.absent(); return Optional.absent();
} }
return Optional.of(AttachmentUtils.createAttachment(file)); return Optional.of(AttachmentUtils.createAttachment(streamDetails, Optional.absent()));
} }
private Optional<SignalServiceAttachmentStream> createContactAvatarAttachment(String number) throws IOException { private Optional<SignalServiceAttachmentStream> createContactAvatarAttachment(SignalServiceAddress address) throws IOException {
File file = getContactAvatarFile(number); final StreamDetails streamDetails = avatarStore.retrieveContactAvatar(address);
if (!file.exists()) { if (streamDetails == null) {
return Optional.absent(); return Optional.absent();
} }
return Optional.of(AttachmentUtils.createAttachment(file)); return Optional.of(AttachmentUtils.createAttachment(streamDetails, Optional.absent()));
} }
private GroupInfo getGroupForSending(GroupId groupId) throws GroupNotFoundException, NotAGroupMemberException { private GroupInfo getGroupForSending(GroupId groupId) throws GroupNotFoundException, NotAGroupMemberException {
@ -683,13 +696,15 @@ public class Manager implements Closeable {
} }
private Pair<GroupId, List<SendMessageResult>> sendUpdateGroupMessage( private Pair<GroupId, List<SendMessageResult>> sendUpdateGroupMessage(
GroupId groupId, String name, Collection<SignalServiceAddress> members, String avatarFile GroupId groupId, String name, Collection<SignalServiceAddress> members, File avatarFile
) throws IOException, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException { ) throws IOException, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException {
GroupInfo g; GroupInfo g;
SignalServiceDataMessage.Builder messageBuilder; SignalServiceDataMessage.Builder messageBuilder;
if (groupId == null) { if (groupId == null) {
// Create new group // Create new group
GroupInfoV2 gv2 = groupHelper.createGroupV2(name, members, avatarFile); GroupInfoV2 gv2 = groupHelper.createGroupV2(name == null ? "" : name,
members == null ? List.of() : members,
avatarFile);
if (gv2 == null) { if (gv2 == null) {
GroupInfoV1 gv1 = new GroupInfoV1(GroupIdV1.createRandom()); GroupInfoV1 gv1 = new GroupInfoV1(GroupIdV1.createRandom());
gv1.addMembers(List.of(account.getSelfAddress())); gv1.addMembers(List.of(account.getSelfAddress()));
@ -697,6 +712,10 @@ public class Manager implements Closeable {
messageBuilder = getGroupUpdateMessageBuilder(gv1); messageBuilder = getGroupUpdateMessageBuilder(gv1);
g = gv1; g = gv1;
} else { } else {
if (avatarFile != null) {
avatarStore.storeGroupAvatar(gv2.getGroupId(),
outputStream -> IOUtils.copyFileToStream(avatarFile, outputStream));
}
messageBuilder = getGroupUpdateMessageBuilder(gv2, null); messageBuilder = getGroupUpdateMessageBuilder(gv2, null);
g = gv2; g = gv2;
} }
@ -731,6 +750,10 @@ public class Manager implements Closeable {
Pair<DecryptedGroup, GroupChange> groupGroupChangePair = groupHelper.updateGroupV2(groupInfoV2, Pair<DecryptedGroup, GroupChange> groupGroupChangePair = groupHelper.updateGroupV2(groupInfoV2,
name, name,
avatarFile); avatarFile);
if (avatarFile != null) {
avatarStore.storeGroupAvatar(groupInfoV2.getGroupId(),
outputStream -> IOUtils.copyFileToStream(avatarFile, outputStream));
}
result = sendUpdateGroupMessage(groupInfoV2, result = sendUpdateGroupMessage(groupInfoV2,
groupGroupChangePair.first(), groupGroupChangePair.first(),
groupGroupChangePair.second()); groupGroupChangePair.second());
@ -794,7 +817,7 @@ public class Manager implements Closeable {
final GroupInfoV1 g, final GroupInfoV1 g,
final String name, final String name,
final Collection<SignalServiceAddress> members, final Collection<SignalServiceAddress> members,
final String avatarFile final File avatarFile
) throws IOException { ) throws IOException {
if (name != null) { if (name != null) {
g.name = name; g.name = name;
@ -824,9 +847,8 @@ public class Manager implements Closeable {
} }
if (avatarFile != null) { if (avatarFile != null) {
IOUtils.createPrivateDirectories(pathConfig.getAvatarsPath()); avatarStore.storeGroupAvatar(g.getGroupId(),
File aFile = getGroupAvatarFile(g.getGroupId()); outputStream -> IOUtils.copyFileToStream(avatarFile, outputStream));
Files.copy(Paths.get(avatarFile), aFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
} }
} }
@ -856,13 +878,13 @@ public class Manager implements Closeable {
.withName(g.name) .withName(g.name)
.withMembers(new ArrayList<>(g.getMembers())); .withMembers(new ArrayList<>(g.getMembers()));
File aFile = getGroupAvatarFile(g.getGroupId());
if (aFile.exists()) {
try { try {
group.withAvatar(AttachmentUtils.createAttachment(aFile)); final Optional<SignalServiceAttachmentStream> attachment = createGroupAvatarAttachment(g.getGroupId());
} catch (IOException e) { if (attachment.isPresent()) {
throw new AttachmentInvalidException(aFile.toString(), e); group.withAvatar(attachment.get());
} }
} catch (IOException e) {
throw new AttachmentInvalidException(g.getGroupId().toBase64(), e);
} }
return SignalServiceDataMessage.newBuilder() return SignalServiceDataMessage.newBuilder()
@ -1001,12 +1023,12 @@ public class Manager implements Closeable {
} }
public Pair<GroupId, List<SendMessageResult>> updateGroup( public Pair<GroupId, List<SendMessageResult>> updateGroup(
GroupId groupId, String name, List<String> members, String avatar GroupId groupId, String name, List<String> members, File avatarFile
) throws IOException, GroupNotFoundException, AttachmentInvalidException, InvalidNumberException, NotAGroupMemberException { ) throws IOException, GroupNotFoundException, AttachmentInvalidException, InvalidNumberException, NotAGroupMemberException {
return sendUpdateGroupMessage(groupId, return sendUpdateGroupMessage(groupId,
name, name,
members == null ? null : getSignalServiceAddresses(members), members == null ? null : getSignalServiceAddresses(members),
avatar); avatarFile);
} }
/** /**
@ -1467,7 +1489,7 @@ public class Manager implements Closeable {
if (avatar.isPointer()) { if (avatar.isPointer()) {
try { try {
retrieveGroupAvatarAttachment(avatar.asPointer(), groupV1.getGroupId()); retrieveGroupAvatarAttachment(avatar.asPointer(), groupV1.getGroupId());
} catch (IOException | InvalidMessageException | MissingConfigurationException e) { } catch (IOException e) {
logger.warn("Failed to retrieve avatar for group {}, ignoring: {}", logger.warn("Failed to retrieve avatar for group {}, ignoring: {}",
groupId.toBase64(), groupId.toBase64(),
e.getMessage()); e.getMessage());
@ -1556,7 +1578,7 @@ public class Manager implements Closeable {
if (attachment.isPointer()) { if (attachment.isPointer()) {
try { try {
retrieveAttachment(attachment.asPointer()); retrieveAttachment(attachment.asPointer());
} catch (IOException | InvalidMessageException | MissingConfigurationException e) { } catch (IOException e) {
logger.warn("Failed to retrieve attachment ({}), ignoring: {}", logger.warn("Failed to retrieve attachment ({}), ignoring: {}",
attachment.asPointer().getRemoteId(), attachment.asPointer().getRemoteId(),
e.getMessage()); e.getMessage());
@ -1583,7 +1605,7 @@ public class Manager implements Closeable {
SignalServiceAttachmentPointer attachment = preview.getImage().get().asPointer(); SignalServiceAttachmentPointer attachment = preview.getImage().get().asPointer();
try { try {
retrieveAttachment(attachment); retrieveAttachment(attachment);
} catch (IOException | InvalidMessageException | MissingConfigurationException e) { } catch (IOException e) {
logger.warn("Failed to retrieve preview image ({}), ignoring: {}", logger.warn("Failed to retrieve preview image ({}), ignoring: {}",
attachment.getRemoteId(), attachment.getRemoteId(),
e.getMessage()); e.getMessage());
@ -1599,7 +1621,7 @@ public class Manager implements Closeable {
if (attachment != null && attachment.isPointer()) { if (attachment != null && attachment.isPointer()) {
try { try {
retrieveAttachment(attachment.asPointer()); retrieveAttachment(attachment.asPointer());
} catch (IOException | InvalidMessageException | MissingConfigurationException e) { } catch (IOException e) {
logger.warn("Failed to retrieve quote attachment thumbnail ({}), ignoring: {}", logger.warn("Failed to retrieve quote attachment thumbnail ({}), ignoring: {}",
attachment.asPointer().getRemoteId(), attachment.asPointer().getRemoteId(),
e.getMessage()); e.getMessage());
@ -2047,7 +2069,7 @@ public class Manager implements Closeable {
account.getContactStore().updateContact(contact); account.getContactStore().updateContact(contact);
if (c.getAvatar().isPresent()) { if (c.getAvatar().isPresent()) {
retrieveContactAvatarAttachment(c.getAvatar().get(), contact.number); retrieveContactAvatarAttachment(c.getAvatar().get(), contact.getAddress());
} }
} }
} }
@ -2099,45 +2121,21 @@ public class Manager implements Closeable {
return actions; return actions;
} }
private File getContactAvatarFile(String number) { private void retrieveContactAvatarAttachment(
return new File(pathConfig.getAvatarsPath(), "contact-" + number); SignalServiceAttachment attachment, SignalServiceAddress address
) throws IOException {
avatarStore.storeContactAvatar(address, outputStream -> retrieveAttachment(attachment, outputStream));
} }
private File retrieveContactAvatarAttachment( private void retrieveGroupAvatarAttachment(
SignalServiceAttachment attachment, String number
) throws IOException, InvalidMessageException, MissingConfigurationException {
IOUtils.createPrivateDirectories(pathConfig.getAvatarsPath());
if (attachment.isPointer()) {
SignalServiceAttachmentPointer pointer = attachment.asPointer();
return retrieveAttachment(pointer, getContactAvatarFile(number), false);
} else {
SignalServiceAttachmentStream stream = attachment.asStream();
return AttachmentUtils.retrieveAttachment(stream, getContactAvatarFile(number));
}
}
private File getGroupAvatarFile(GroupId groupId) {
return new File(pathConfig.getAvatarsPath(), "group-" + groupId.toBase64().replace("/", "_"));
}
private File retrieveGroupAvatarAttachment(
SignalServiceAttachment attachment, GroupId groupId SignalServiceAttachment attachment, GroupId groupId
) throws IOException, InvalidMessageException, MissingConfigurationException { ) throws IOException {
IOUtils.createPrivateDirectories(pathConfig.getAvatarsPath()); avatarStore.storeGroupAvatar(groupId, outputStream -> retrieveAttachment(attachment, outputStream));
if (attachment.isPointer()) {
SignalServiceAttachmentPointer pointer = attachment.asPointer();
return retrieveAttachment(pointer, getGroupAvatarFile(groupId), false);
} else {
SignalServiceAttachmentStream stream = attachment.asStream();
return AttachmentUtils.retrieveAttachment(stream, getGroupAvatarFile(groupId));
}
} }
private File retrieveGroupAvatar( private void retrieveGroupAvatar(
GroupId groupId, GroupSecretParams groupSecretParams, String cdnKey GroupId groupId, GroupSecretParams groupSecretParams, String cdnKey
) throws IOException { ) throws IOException {
IOUtils.createPrivateDirectories(pathConfig.getAvatarsPath());
File outputFile = getGroupAvatarFile(groupId);
GroupsV2Operations.GroupOperations groupOperations = groupsV2Operations.forGroup(groupSecretParams); GroupsV2Operations.GroupOperations groupOperations = groupsV2Operations.forGroup(groupSecretParams);
File tmpFile = IOUtils.createTempFile(); File tmpFile = IOUtils.createTempFile();
@ -2147,9 +2145,7 @@ public class Manager implements Closeable {
byte[] encryptedData = IOUtils.readFully(input); byte[] encryptedData = IOUtils.readFully(input);
byte[] decryptedData = groupOperations.decryptAvatar(encryptedData); byte[] decryptedData = groupOperations.decryptAvatar(encryptedData);
try (OutputStream output = new FileOutputStream(outputFile)) { avatarStore.storeGroupAvatar(groupId, outputStream -> outputStream.write(decryptedData));
output.write(decryptedData);
}
} finally { } finally {
try { try {
Files.delete(tmpFile.toPath()); Files.delete(tmpFile.toPath());
@ -2159,26 +2155,20 @@ public class Manager implements Closeable {
e.getMessage()); e.getMessage());
} }
} }
return outputFile;
} }
private File getProfileAvatarFile(SignalServiceAddress address) { private void retrieveProfileAvatar(
return new File(pathConfig.getAvatarsPath(), "profile-" + address.getLegacyIdentifier());
}
private File retrieveProfileAvatar(
SignalServiceAddress address, String avatarPath, ProfileKey profileKey SignalServiceAddress address, String avatarPath, ProfileKey profileKey
) throws IOException { ) throws IOException {
IOUtils.createPrivateDirectories(pathConfig.getAvatarsPath());
File outputFile = getProfileAvatarFile(address);
File tmpFile = IOUtils.createTempFile(); File tmpFile = IOUtils.createTempFile();
try (InputStream input = messageReceiver.retrieveProfileAvatar(avatarPath, try (InputStream input = messageReceiver.retrieveProfileAvatar(avatarPath,
tmpFile, tmpFile,
profileKey, profileKey,
ServiceConfig.AVATAR_DOWNLOAD_FAILSAFE_MAX_SIZE)) { ServiceConfig.AVATAR_DOWNLOAD_FAILSAFE_MAX_SIZE)) {
avatarStore.storeProfileAvatar(address, outputStream -> {
// Use larger buffer size to prevent AssertionError: Need: 12272 but only have: 8192 ... // Use larger buffer size to prevent AssertionError: Need: 12272 but only have: 8192 ...
IOUtils.copyStreamToFile(input, outputFile, (int) ServiceConfig.AVATAR_DOWNLOAD_FAILSAFE_MAX_SIZE); IOUtils.copyStream(input, outputStream, (int) ServiceConfig.AVATAR_DOWNLOAD_FAILSAFE_MAX_SIZE);
});
} finally { } finally {
try { try {
Files.delete(tmpFile.toPath()); Files.delete(tmpFile.toPath());
@ -2188,37 +2178,57 @@ public class Manager implements Closeable {
e.getMessage()); e.getMessage());
} }
} }
return outputFile;
} }
public File getAttachmentFile(SignalServiceAttachmentRemoteId attachmentId) { public File getAttachmentFile(SignalServiceAttachmentRemoteId attachmentId) {
return new File(pathConfig.getAttachmentsPath(), attachmentId.toString()); return new File(pathConfig.getAttachmentsPath(), attachmentId.toString());
} }
private File retrieveAttachment(SignalServiceAttachmentPointer pointer) throws IOException, InvalidMessageException, MissingConfigurationException { private void retrieveAttachment(SignalServiceAttachmentPointer pointer) throws IOException {
IOUtils.createPrivateDirectories(pathConfig.getAttachmentsPath()); IOUtils.createPrivateDirectories(pathConfig.getAttachmentsPath());
return retrieveAttachment(pointer, getAttachmentFile(pointer.getRemoteId()), true); retrieveAttachment(pointer, getAttachmentFile(pointer.getRemoteId()));
} }
private File retrieveAttachment( private void retrieveAttachment(
SignalServiceAttachmentPointer pointer, File outputFile, boolean storePreview SignalServiceAttachmentPointer pointer, File outputFile
) throws IOException, InvalidMessageException, MissingConfigurationException { ) throws IOException {
if (storePreview && pointer.getPreview().isPresent()) { if (pointer.getPreview().isPresent()) {
File previewFile = new File(outputFile + ".preview"); File previewFile = new File(outputFile + ".preview");
try (OutputStream output = new FileOutputStream(previewFile)) { try (OutputStream output = new FileOutputStream(previewFile)) {
byte[] preview = pointer.getPreview().get(); byte[] preview = pointer.getPreview().get();
output.write(preview, 0, preview.length); output.write(preview, 0, preview.length);
} catch (FileNotFoundException e) { } catch (FileNotFoundException e) {
e.printStackTrace(); logger.warn("Failed to retrieve attachment preview, ignoring: {}", e.getMessage());
return null;
} }
} }
try (OutputStream output = new FileOutputStream(outputFile)) {
retrieveAttachment(pointer, output);
}
}
private void retrieveAttachment(
final SignalServiceAttachment attachment, final OutputStream outputStream
) throws IOException {
if (attachment.isPointer()) {
SignalServiceAttachmentPointer pointer = attachment.asPointer();
retrieveAttachment(pointer, outputStream);
} else {
SignalServiceAttachmentStream stream = attachment.asStream();
AttachmentUtils.retrieveAttachment(stream, outputStream);
}
}
private void retrieveAttachment(
SignalServiceAttachmentPointer pointer, OutputStream outputStream
) throws IOException {
File tmpFile = IOUtils.createTempFile(); File tmpFile = IOUtils.createTempFile();
try (InputStream input = messageReceiver.retrieveAttachment(pointer, try (InputStream input = messageReceiver.retrieveAttachment(pointer,
tmpFile, tmpFile,
ServiceConfig.MAX_ATTACHMENT_SIZE)) { ServiceConfig.MAX_ATTACHMENT_SIZE)) {
IOUtils.copyStreamToFile(input, outputFile); IOUtils.copyStream(input, outputStream);
} catch (MissingConfigurationException | InvalidMessageException e) {
throw new IOException(e);
} finally { } finally {
try { try {
Files.delete(tmpFile.toPath()); Files.delete(tmpFile.toPath());
@ -2228,7 +2238,6 @@ public class Manager implements Closeable {
e.getMessage()); e.getMessage());
} }
} }
return outputFile;
} }
private InputStream retrieveAttachmentAsStream( private InputStream retrieveAttachmentAsStream(
@ -2299,7 +2308,7 @@ public class Manager implements Closeable {
ProfileKey profileKey = account.getProfileStore().getProfileKey(record.getAddress()); ProfileKey profileKey = account.getProfileStore().getProfileKey(record.getAddress());
out.write(new DeviceContact(record.getAddress(), out.write(new DeviceContact(record.getAddress(),
Optional.fromNullable(record.name), Optional.fromNullable(record.name),
createContactAvatarAttachment(record.number), createContactAvatarAttachment(record.getAddress()),
Optional.fromNullable(record.color), Optional.fromNullable(record.color),
Optional.fromNullable(verifiedMessage), Optional.fromNullable(verifiedMessage),
Optional.fromNullable(profileKey), Optional.fromNullable(profileKey),
@ -2492,10 +2501,6 @@ public class Manager implements Closeable {
theirIdentityKey); theirIdentityKey);
} }
void saveAccount() {
account.save();
}
public SignalServiceAddress canonicalizeAndResolveSignalServiceAddress(String identifier) throws InvalidNumberException { public SignalServiceAddress canonicalizeAndResolveSignalServiceAddress(String identifier) throws InvalidNumberException {
String canonicalizedNumber = UuidUtil.isUuid(identifier) String canonicalizedNumber = UuidUtil.isUuid(identifier)
? identifier ? identifier

View file

@ -36,6 +36,7 @@ import org.whispersystems.signalservice.api.groupsv2.NotAbleToApplyGroupV2Change
import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.util.UuidUtil; import org.whispersystems.signalservice.api.util.UuidUtil;
import java.io.File;
import java.io.FileInputStream; import java.io.FileInputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
@ -99,7 +100,7 @@ public class GroupHelper {
} }
public GroupInfoV2 createGroupV2( public GroupInfoV2 createGroupV2(
String name, Collection<SignalServiceAddress> members, String avatarFile String name, Collection<SignalServiceAddress> members, File avatarFile
) throws IOException { ) throws IOException {
final byte[] avatarBytes = readAvatarBytes(avatarFile); final byte[] avatarBytes = readAvatarBytes(avatarFile);
final GroupsV2Operations.NewGroup newGroup = buildNewGroupV2(name, members, avatarBytes); final GroupsV2Operations.NewGroup newGroup = buildNewGroupV2(name, members, avatarBytes);
@ -132,7 +133,7 @@ public class GroupHelper {
return g; return g;
} }
private byte[] readAvatarBytes(final String avatarFile) throws IOException { private byte[] readAvatarBytes(final File avatarFile) throws IOException {
final byte[] avatarBytes; final byte[] avatarBytes;
try (InputStream avatar = avatarFile == null ? null : new FileInputStream(avatarFile)) { try (InputStream avatar = avatarFile == null ? null : new FileInputStream(avatarFile)) {
avatarBytes = avatar == null ? null : IOUtils.readFully(avatar); avatarBytes = avatar == null ? null : IOUtils.readFully(avatar);
@ -194,7 +195,7 @@ public class GroupHelper {
} }
public Pair<DecryptedGroup, GroupChange> updateGroupV2( public Pair<DecryptedGroup, GroupChange> updateGroupV2(
GroupInfoV2 groupInfoV2, String name, String avatarFile GroupInfoV2 groupInfoV2, String name, File avatarFile
) throws IOException { ) throws IOException {
final GroupSecretParams groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupInfoV2.getMasterKey()); final GroupSecretParams groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupInfoV2.getMasterKey());
GroupsV2Operations.GroupOperations groupOperations = groupsV2Operations.forGroup(groupSecretParams); GroupsV2Operations.GroupOperations groupOperations = groupsV2Operations.forGroup(groupSecretParams);

View file

@ -5,8 +5,6 @@ import com.fasterxml.jackson.annotation.JsonProperty;
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile; import org.whispersystems.signalservice.api.profiles.SignalServiceProfile;
import java.io.File;
public class SignalProfile { public class SignalProfile {
@JsonProperty @JsonProperty
@ -15,8 +13,6 @@ public class SignalProfile {
@JsonProperty @JsonProperty
private final String name; private final String name;
private final File avatarFile;
@JsonProperty @JsonProperty
private final String unidentifiedAccess; private final String unidentifiedAccess;
@ -29,14 +25,12 @@ public class SignalProfile {
public SignalProfile( public SignalProfile(
final String identityKey, final String identityKey,
final String name, final String name,
final File avatarFile,
final String unidentifiedAccess, final String unidentifiedAccess,
final boolean unrestrictedUnidentifiedAccess, final boolean unrestrictedUnidentifiedAccess,
final SignalServiceProfile.Capabilities capabilities final SignalServiceProfile.Capabilities capabilities
) { ) {
this.identityKey = identityKey; this.identityKey = identityKey;
this.name = name; this.name = name;
this.avatarFile = avatarFile;
this.unidentifiedAccess = unidentifiedAccess; this.unidentifiedAccess = unidentifiedAccess;
this.unrestrictedUnidentifiedAccess = unrestrictedUnidentifiedAccess; this.unrestrictedUnidentifiedAccess = unrestrictedUnidentifiedAccess;
this.capabilities = new Capabilities(); this.capabilities = new Capabilities();
@ -54,7 +48,6 @@ public class SignalProfile {
) { ) {
this.identityKey = identityKey; this.identityKey = identityKey;
this.name = name; this.name = name;
this.avatarFile = null;
this.unidentifiedAccess = unidentifiedAccess; this.unidentifiedAccess = unidentifiedAccess;
this.unrestrictedUnidentifiedAccess = unrestrictedUnidentifiedAccess; this.unrestrictedUnidentifiedAccess = unrestrictedUnidentifiedAccess;
this.capabilities = capabilities; this.capabilities = capabilities;
@ -68,10 +61,6 @@ public class SignalProfile {
return name; return name;
} }
public File getAvatarFile() {
return avatarFile;
}
public String getUnidentifiedAccess() { public String getUnidentifiedAccess() {
return unidentifiedAccess; return unidentifiedAccess;
} }
@ -94,7 +83,6 @@ public class SignalProfile {
+ name + name
+ '\'' + '\''
+ ", avatarFile=" + ", avatarFile="
+ avatarFile
+ ", unidentifiedAccess='" + ", unidentifiedAccess='"
+ unidentifiedAccess + unidentifiedAccess
+ '\'' + '\''

View file

@ -4,12 +4,10 @@ import org.asamk.signal.manager.AttachmentInvalidException;
import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment; import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentStream; import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentStream;
import org.whispersystems.signalservice.api.util.StreamDetails;
import org.whispersystems.signalservice.internal.push.http.ResumableUploadSpec; import org.whispersystems.signalservice.internal.push.http.ResumableUploadSpec;
import java.io.File; import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
@ -34,19 +32,23 @@ public class AttachmentUtils {
} }
public static SignalServiceAttachmentStream createAttachment(File attachmentFile) throws IOException { public static SignalServiceAttachmentStream createAttachment(File attachmentFile) throws IOException {
InputStream attachmentStream = new FileInputStream(attachmentFile); final StreamDetails streamDetails = Utils.createStreamDetailsFromFile(attachmentFile);
final long attachmentSize = attachmentFile.length(); return createAttachment(streamDetails, Optional.of(attachmentFile.getName()));
final String mime = Utils.getFileMimeType(attachmentFile, "application/octet-stream"); }
public static SignalServiceAttachmentStream createAttachment(
StreamDetails streamDetails, Optional<String> name
) {
// TODO mabybe add a parameter to set the voiceNote, borderless, preview, width, height and caption option // TODO mabybe add a parameter to set the voiceNote, borderless, preview, width, height and caption option
final long uploadTimestamp = System.currentTimeMillis(); final long uploadTimestamp = System.currentTimeMillis();
Optional<byte[]> preview = Optional.absent(); Optional<byte[]> preview = Optional.absent();
Optional<String> caption = Optional.absent(); Optional<String> caption = Optional.absent();
Optional<String> blurHash = Optional.absent(); Optional<String> blurHash = Optional.absent();
final Optional<ResumableUploadSpec> resumableUploadSpec = Optional.absent(); final Optional<ResumableUploadSpec> resumableUploadSpec = Optional.absent();
return new SignalServiceAttachmentStream(attachmentStream, return new SignalServiceAttachmentStream(streamDetails.getStream(),
mime, streamDetails.getContentType(),
attachmentSize, streamDetails.getLength(),
Optional.of(attachmentFile.getName()), name,
false, false,
false, false,
preview, preview,
@ -60,20 +62,10 @@ public class AttachmentUtils {
resumableUploadSpec); resumableUploadSpec);
} }
public static File retrieveAttachment(SignalServiceAttachmentStream stream, File outputFile) throws IOException { public static void retrieveAttachment(
SignalServiceAttachmentStream stream, OutputStream output
) throws IOException {
InputStream input = stream.getInputStream(); InputStream input = stream.getInputStream();
IOUtils.copyStream(input, output);
try (OutputStream output = new FileOutputStream(outputFile)) {
byte[] buffer = new byte[4096];
int read;
while ((read = input.read(buffer)) != -1) {
output.write(buffer, 0, read);
}
} catch (FileNotFoundException e) {
e.printStackTrace();
return null;
}
return outputFile;
} }
} }

View file

@ -1,10 +1,8 @@
package org.asamk.signal.manager.util; package org.asamk.signal.manager.util;
import org.whispersystems.signalservice.internal.util.Util;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.File; import java.io.File;
import java.io.FileOutputStream; import java.io.FileInputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
@ -29,7 +27,7 @@ public class IOUtils {
public static byte[] readFully(InputStream in) throws IOException { public static byte[] readFully(InputStream in) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream(); ByteArrayOutputStream baos = new ByteArrayOutputStream();
Util.copy(in, baos); IOUtils.copyStream(in, baos);
return baos.toByteArray(); return baos.toByteArray();
} }
@ -57,12 +55,17 @@ public class IOUtils {
} }
} }
public static void copyStreamToFile(InputStream input, File outputFile) throws IOException { public static void copyFileToStream(File inputFile, OutputStream output) throws IOException {
copyStreamToFile(input, outputFile, 8192); try (InputStream inputStream = new FileInputStream(inputFile)) {
copyStream(inputStream, output);
}
} }
public static void copyStreamToFile(InputStream input, File outputFile, int bufferSize) throws IOException { public static void copyStream(InputStream input, OutputStream output) throws IOException {
try (OutputStream output = new FileOutputStream(outputFile)) { copyStream(input, output, 4096);
}
public static void copyStream(InputStream input, OutputStream output, int bufferSize) throws IOException {
byte[] buffer = new byte[bufferSize]; byte[] buffer = new byte[bufferSize];
int read; int read;
@ -70,5 +73,4 @@ public class IOUtils {
output.write(buffer, 0, read); output.write(buffer, 0, read);
} }
} }
}
} }

View file

@ -36,10 +36,7 @@ public class Utils {
public static StreamDetails createStreamDetailsFromFile(File file) throws IOException { public static StreamDetails createStreamDetailsFromFile(File file) throws IOException {
InputStream stream = new FileInputStream(file); InputStream stream = new FileInputStream(file);
final long size = file.length(); final long size = file.length();
String mime = Files.probeContentType(file.toPath()); final String mime = getFileMimeType(file, "application/octet-stream");
if (mime == null) {
mime = "application/octet-stream";
}
return new StreamDetails(stream, mime, size); return new StreamDetails(stream, mime, size);
} }