Add avatar image storage

Group and contact avatars are now stored in the avatars subfolder
of the settings path:
- contact-NUMBER
- group-GROUP_ID
This commit is contained in:
AsamK 2016-06-19 20:58:01 +02:00
parent 9427616906
commit 3e2024ff0a
3 changed files with 136 additions and 33 deletions

View file

@ -1,5 +1,6 @@
package org.asamk.signal;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.Collection;
@ -16,8 +17,12 @@ public class GroupInfo {
@JsonProperty
public Set<String> members = new HashSet<>();
@JsonProperty
public long avatarId;
private long avatarId;
@JsonIgnore
public long getAvatarId() {
return avatarId;
}
@JsonProperty
public boolean active;

View file

@ -19,6 +19,8 @@ public class JsonGroupStore {
@JsonDeserialize(using = JsonGroupStore.GroupsDeserializer.class)
private Map<String, GroupInfo> groups = new HashMap<>();
public static List<GroupInfo> groupsWithLegacyAvatarId = new ArrayList<>();
private static final ObjectMapper jsonProcessot = new ObjectMapper();
void updateGroup(GroupInfo group) {
@ -48,6 +50,10 @@ public class JsonGroupStore {
JsonNode node = jsonParser.getCodec().readTree(jsonParser);
for (JsonNode n : node) {
GroupInfo g = jsonProcessot.treeToValue(n, GroupInfo.class);
// Check if a legacy avatarId exists
if (g.getAvatarId() != 0) {
groupsWithLegacyAvatarId.add(g);
}
groups.put(Base64.encodeBytes(g.groupId), g);
}

View file

@ -58,6 +58,7 @@ import java.net.URLDecoder;
import java.net.URLEncoder;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
@ -76,6 +77,7 @@ class Manager implements Signal {
private final String settingsPath;
private final String dataPath;
private final String attachmentsPath;
private final String avatarsPath;
private final ObjectMapper jsonProcessot = new ObjectMapper();
private String username;
@ -97,6 +99,7 @@ class Manager implements Signal {
this.settingsPath = settingsPath;
this.dataPath = this.settingsPath + "/data";
this.attachmentsPath = this.settingsPath + "/attachments";
this.avatarsPath = this.settingsPath + "/avatars";
jsonProcessot.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE); // disable autodetect
jsonProcessot.enable(SerializationFeature.INDENT_OUTPUT); // for pretty print, you can disable it.
@ -169,6 +172,25 @@ class Manager implements Signal {
if (groupStore == null) {
groupStore = new JsonGroupStore();
}
// Copy group avatars that were previously stored in the attachments folder
// to the new avatar folder
if (groupStore.groupsWithLegacyAvatarId.size() > 0) {
for (GroupInfo g : groupStore.groupsWithLegacyAvatarId) {
File avatarFile = getGroupAvatarFile(g.groupId);
File attachmentFile = getAttachmentFile(g.getAvatarId());
if (!avatarFile.exists() && attachmentFile.exists()) {
try {
new File(avatarsPath).mkdirs();
Files.copy(attachmentFile.toPath(), avatarFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
} catch (Exception e) {
// Ignore
}
}
}
groupStore.groupsWithLegacyAvatarId.clear();
save();
}
JsonNode contactStoreNode = rootNode.get("contactStore");
if (contactStoreNode != null) {
contactStore = jsonProcessot.convertValue(contactStoreNode, JsonContactsStore.class);
@ -402,7 +424,7 @@ class Manager implements Signal {
SignalServiceAttachments = new ArrayList<>(attachments.size());
for (String attachment : attachments) {
try {
SignalServiceAttachments.add(createAttachment(attachment));
SignalServiceAttachments.add(createAttachment(new File(attachment)));
} catch (IOException e) {
throw new AttachmentInvalidException(attachment, e);
}
@ -411,14 +433,31 @@ class Manager implements Signal {
return SignalServiceAttachments;
}
private static SignalServiceAttachment createAttachment(String attachment) throws IOException {
File attachmentFile = new File(attachment);
private static SignalServiceAttachmentStream createAttachment(File attachmentFile) throws IOException {
InputStream attachmentStream = new FileInputStream(attachmentFile);
final long attachmentSize = attachmentFile.length();
String mime = Files.probeContentType(Paths.get(attachment));
String mime = Files.probeContentType(attachmentFile.toPath());
return new SignalServiceAttachmentStream(attachmentStream, mime, attachmentSize, null);
}
private Optional<SignalServiceAttachmentStream> createGroupAvatarAttachment(byte[] groupId) throws IOException {
File file = getGroupAvatarFile(groupId);
if (!file.exists()) {
return Optional.absent();
}
return Optional.of(createAttachment(file));
}
private Optional<SignalServiceAttachmentStream> createContactAvatarAttachment(String number) throws IOException {
File file = getContactAvatarFile(number);
if (!file.exists()) {
return Optional.absent();
}
return Optional.of(createAttachment(file));
}
@Override
public void sendGroupMessage(String messageText, List<String> attachments,
byte[] groupId)
@ -497,11 +536,14 @@ class Manager implements Signal {
.withName(g.name)
.withMembers(new ArrayList<>(g.members));
File aFile = getGroupAvatarFile(g.groupId);
if (avatarFile != null) {
new File(avatarsPath).mkdirs();
Files.copy(Paths.get(avatarFile), aFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
}
if (aFile.exists()) {
try {
group.withAvatar(createAttachment(avatarFile));
// TODO
g.avatarId = 0;
group.withAvatar(createAttachment(aFile));
} catch (IOException e) {
throw new AttachmentInvalidException(avatarFile, e);
}
@ -650,13 +692,10 @@ class Manager implements Signal {
if (groupInfo.getAvatar().isPresent()) {
SignalServiceAttachment avatar = groupInfo.getAvatar().get();
if (avatar.isPointer()) {
long avatarId = avatar.asPointer().getId();
try {
retrieveAttachment(avatar.asPointer());
// TODO store group avatar in /avatar/groups folder
group.avatarId = avatarId;
retrieveGroupAvatarAttachment(avatar.asPointer(), group.groupId);
} catch (IOException | InvalidMessageException e) {
System.err.println("Failed to retrieve group avatar (" + avatarId + "): " + e.getMessage());
System.err.println("Failed to retrieve group avatar (" + avatar.asPointer().getId() + "): " + e.getMessage());
}
}
}
@ -760,9 +799,7 @@ class Manager implements Signal {
syncGroup.active = g.isActive();
if (g.getAvatar().isPresent()) {
byte[] ava = new byte[(int) g.getAvatar().get().getLength()];
org.whispersystems.signalservice.internal.util.Util.readFully(g.getAvatar().get().getInputStream(), ava);
// TODO store group avatar in /avatar/groups folder
retrieveGroupAvatarAttachment(g.getAvatar().get(), syncGroup.groupId);
}
groupStore.updateGroup(syncGroup);
}
@ -783,9 +820,7 @@ class Manager implements Signal {
contactStore.updateContact(contact);
if (c.getAvatar().isPresent()) {
byte[] ava = new byte[(int) c.getAvatar().get().getLength()];
org.whispersystems.signalservice.internal.util.Util.readFully(c.getAvatar().get().getInputStream(), ava);
// TODO store contact avatar in /avatar/contacts folder
retrieveContactAvatarAttachment(c.getAvatar().get(), contact.number);
}
}
} catch (Exception e) {
@ -810,18 +845,48 @@ class Manager implements Signal {
}
}
public File getContactAvatarFile(String number) {
return new File(avatarsPath, "contact-" + number);
}
private File retrieveContactAvatarAttachment(SignalServiceAttachment attachment, String number) throws IOException, InvalidMessageException {
new File(avatarsPath).mkdirs();
if (attachment.isPointer()) {
SignalServiceAttachmentPointer pointer = attachment.asPointer();
return retrieveAttachment(pointer, getContactAvatarFile(number), false);
} else {
SignalServiceAttachmentStream stream = attachment.asStream();
return retrieveAttachment(stream, getContactAvatarFile(number));
}
}
public File getGroupAvatarFile(byte[] groupId) {
return new File(avatarsPath, "group-" + Base64.encodeBytes(groupId).replace("/", "_"));
}
private File retrieveGroupAvatarAttachment(SignalServiceAttachment attachment, byte[] groupId) throws IOException, InvalidMessageException {
new File(avatarsPath).mkdirs();
if (attachment.isPointer()) {
SignalServiceAttachmentPointer pointer = attachment.asPointer();
return retrieveAttachment(pointer, getGroupAvatarFile(groupId), false);
} else {
SignalServiceAttachmentStream stream = attachment.asStream();
return retrieveAttachment(stream, getGroupAvatarFile(groupId));
}
}
public File getAttachmentFile(long attachmentId) {
return new File(attachmentsPath, attachmentId + "");
}
private File retrieveAttachment(SignalServiceAttachmentPointer pointer) throws IOException, InvalidMessageException {
final SignalServiceMessageReceiver messageReceiver = new SignalServiceMessageReceiver(URL, TRUST_STORE, username, password, deviceId, signalingKey, USER_AGENT);
File tmpFile = File.createTempFile("ts_attach_" + pointer.getId(), ".tmp");
InputStream input = messageReceiver.retrieveAttachment(pointer, tmpFile);
new File(attachmentsPath).mkdirs();
File outputFile = getAttachmentFile(pointer.getId());
return retrieveAttachment(pointer, getAttachmentFile(pointer.getId()), true);
}
private File retrieveAttachment(SignalServiceAttachmentStream stream, File outputFile) throws IOException, InvalidMessageException {
InputStream input = stream.getInputStream();
OutputStream output = null;
try {
output = new FileOutputStream(outputFile);
@ -837,14 +902,15 @@ class Manager implements Signal {
} finally {
if (output != null) {
output.close();
output = null;
}
if (!tmpFile.delete()) {
System.err.println("Failed to delete temp file: " + tmpFile);
}
}
if (pointer.getPreview().isPresent()) {
return outputFile;
}
private File retrieveAttachment(SignalServiceAttachmentPointer pointer, File outputFile, boolean storePreview) throws IOException, InvalidMessageException {
if (storePreview && pointer.getPreview().isPresent()) {
File previewFile = new File(outputFile + ".preview");
OutputStream output = null;
try {
output = new FileOutputStream(previewFile);
byte[] preview = pointer.getPreview().get();
@ -858,6 +924,32 @@ class Manager implements Signal {
}
}
}
final SignalServiceMessageReceiver messageReceiver = new SignalServiceMessageReceiver(URL, TRUST_STORE, username, password, deviceId, signalingKey, USER_AGENT);
File tmpFile = File.createTempFile("ts_attach_" + pointer.getId(), ".tmp");
InputStream input = messageReceiver.retrieveAttachment(pointer, tmpFile);
OutputStream output = null;
try {
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;
} finally {
if (output != null) {
output.close();
}
if (!tmpFile.delete()) {
System.err.println("Failed to delete temp file: " + tmpFile);
}
}
return outputFile;
}
@ -892,7 +984,7 @@ class Manager implements Signal {
try {
for (GroupInfo record : groupStore.getGroups()) {
out.write(new DeviceGroup(record.groupId, Optional.fromNullable(record.name),
new ArrayList<>(record.members), Optional.<SignalServiceAttachmentStream>absent(), // TODO add avatar
new ArrayList<>(record.members), createGroupAvatarAttachment(record.groupId),
record.active));
}
} finally {
@ -922,7 +1014,7 @@ class Manager implements Signal {
try {
for (ContactInfo record : contactStore.getContacts()) {
out.write(new DeviceContact(record.number, Optional.fromNullable(record.name),
Optional.<SignalServiceAttachmentStream>absent())); // TODO add avatar
createContactAvatarAttachment(record.number)));
}
} finally {
out.close();