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

View file

@ -19,6 +19,8 @@ public class JsonGroupStore {
@JsonDeserialize(using = JsonGroupStore.GroupsDeserializer.class) @JsonDeserialize(using = JsonGroupStore.GroupsDeserializer.class)
private Map<String, GroupInfo> groups = new HashMap<>(); private Map<String, GroupInfo> groups = new HashMap<>();
public static List<GroupInfo> groupsWithLegacyAvatarId = new ArrayList<>();
private static final ObjectMapper jsonProcessot = new ObjectMapper(); private static final ObjectMapper jsonProcessot = new ObjectMapper();
void updateGroup(GroupInfo group) { void updateGroup(GroupInfo group) {
@ -48,6 +50,10 @@ public class JsonGroupStore {
JsonNode node = jsonParser.getCodec().readTree(jsonParser); JsonNode node = jsonParser.getCodec().readTree(jsonParser);
for (JsonNode n : node) { for (JsonNode n : node) {
GroupInfo g = jsonProcessot.treeToValue(n, GroupInfo.class); 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); groups.put(Base64.encodeBytes(g.groupId), g);
} }

View file

@ -58,6 +58,7 @@ import java.net.URLDecoder;
import java.net.URLEncoder; import java.net.URLEncoder;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Paths; import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.util.*; import java.util.*;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException; import java.util.concurrent.TimeoutException;
@ -76,6 +77,7 @@ class Manager implements Signal {
private final String settingsPath; private final String settingsPath;
private final String dataPath; private final String dataPath;
private final String attachmentsPath; private final String attachmentsPath;
private final String avatarsPath;
private final ObjectMapper jsonProcessot = new ObjectMapper(); private final ObjectMapper jsonProcessot = new ObjectMapper();
private String username; private String username;
@ -97,6 +99,7 @@ class Manager implements Signal {
this.settingsPath = settingsPath; this.settingsPath = settingsPath;
this.dataPath = this.settingsPath + "/data"; this.dataPath = this.settingsPath + "/data";
this.attachmentsPath = this.settingsPath + "/attachments"; this.attachmentsPath = this.settingsPath + "/attachments";
this.avatarsPath = this.settingsPath + "/avatars";
jsonProcessot.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE); // disable autodetect jsonProcessot.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE); // disable autodetect
jsonProcessot.enable(SerializationFeature.INDENT_OUTPUT); // for pretty print, you can disable it. jsonProcessot.enable(SerializationFeature.INDENT_OUTPUT); // for pretty print, you can disable it.
@ -169,6 +172,25 @@ class Manager implements Signal {
if (groupStore == null) { if (groupStore == null) {
groupStore = new JsonGroupStore(); 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"); JsonNode contactStoreNode = rootNode.get("contactStore");
if (contactStoreNode != null) { if (contactStoreNode != null) {
contactStore = jsonProcessot.convertValue(contactStoreNode, JsonContactsStore.class); contactStore = jsonProcessot.convertValue(contactStoreNode, JsonContactsStore.class);
@ -402,7 +424,7 @@ class Manager implements Signal {
SignalServiceAttachments = new ArrayList<>(attachments.size()); SignalServiceAttachments = new ArrayList<>(attachments.size());
for (String attachment : attachments) { for (String attachment : attachments) {
try { try {
SignalServiceAttachments.add(createAttachment(attachment)); SignalServiceAttachments.add(createAttachment(new File(attachment)));
} catch (IOException e) { } catch (IOException e) {
throw new AttachmentInvalidException(attachment, e); throw new AttachmentInvalidException(attachment, e);
} }
@ -411,14 +433,31 @@ class Manager implements Signal {
return SignalServiceAttachments; return SignalServiceAttachments;
} }
private static SignalServiceAttachment createAttachment(String attachment) throws IOException { private static SignalServiceAttachmentStream createAttachment(File attachmentFile) throws IOException {
File attachmentFile = new File(attachment);
InputStream attachmentStream = new FileInputStream(attachmentFile); InputStream attachmentStream = new FileInputStream(attachmentFile);
final long attachmentSize = attachmentFile.length(); 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); 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 @Override
public void sendGroupMessage(String messageText, List<String> attachments, public void sendGroupMessage(String messageText, List<String> attachments,
byte[] groupId) byte[] groupId)
@ -497,11 +536,14 @@ class Manager implements Signal {
.withName(g.name) .withName(g.name)
.withMembers(new ArrayList<>(g.members)); .withMembers(new ArrayList<>(g.members));
File aFile = getGroupAvatarFile(g.groupId);
if (avatarFile != null) { if (avatarFile != null) {
new File(avatarsPath).mkdirs();
Files.copy(Paths.get(avatarFile), aFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
}
if (aFile.exists()) {
try { try {
group.withAvatar(createAttachment(avatarFile)); group.withAvatar(createAttachment(aFile));
// TODO
g.avatarId = 0;
} catch (IOException e) { } catch (IOException e) {
throw new AttachmentInvalidException(avatarFile, e); throw new AttachmentInvalidException(avatarFile, e);
} }
@ -650,13 +692,10 @@ class Manager implements Signal {
if (groupInfo.getAvatar().isPresent()) { if (groupInfo.getAvatar().isPresent()) {
SignalServiceAttachment avatar = groupInfo.getAvatar().get(); SignalServiceAttachment avatar = groupInfo.getAvatar().get();
if (avatar.isPointer()) { if (avatar.isPointer()) {
long avatarId = avatar.asPointer().getId();
try { try {
retrieveAttachment(avatar.asPointer()); retrieveGroupAvatarAttachment(avatar.asPointer(), group.groupId);
// TODO store group avatar in /avatar/groups folder
group.avatarId = avatarId;
} catch (IOException | InvalidMessageException e) { } 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(); syncGroup.active = g.isActive();
if (g.getAvatar().isPresent()) { if (g.getAvatar().isPresent()) {
byte[] ava = new byte[(int) g.getAvatar().get().getLength()]; retrieveGroupAvatarAttachment(g.getAvatar().get(), syncGroup.groupId);
org.whispersystems.signalservice.internal.util.Util.readFully(g.getAvatar().get().getInputStream(), ava);
// TODO store group avatar in /avatar/groups folder
} }
groupStore.updateGroup(syncGroup); groupStore.updateGroup(syncGroup);
} }
@ -783,9 +820,7 @@ class Manager implements Signal {
contactStore.updateContact(contact); contactStore.updateContact(contact);
if (c.getAvatar().isPresent()) { if (c.getAvatar().isPresent()) {
byte[] ava = new byte[(int) c.getAvatar().get().getLength()]; retrieveContactAvatarAttachment(c.getAvatar().get(), contact.number);
org.whispersystems.signalservice.internal.util.Util.readFully(c.getAvatar().get().getInputStream(), ava);
// TODO store contact avatar in /avatar/contacts folder
} }
} }
} catch (Exception e) { } 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) { public File getAttachmentFile(long attachmentId) {
return new File(attachmentsPath, attachmentId + ""); return new File(attachmentsPath, attachmentId + "");
} }
private File retrieveAttachment(SignalServiceAttachmentPointer pointer) throws IOException, InvalidMessageException { 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(); 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; OutputStream output = null;
try { try {
output = new FileOutputStream(outputFile); output = new FileOutputStream(outputFile);
@ -837,14 +902,15 @@ class Manager implements Signal {
} finally { } finally {
if (output != null) { if (output != null) {
output.close(); 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"); File previewFile = new File(outputFile + ".preview");
OutputStream output = null;
try { try {
output = new FileOutputStream(previewFile); output = new FileOutputStream(previewFile);
byte[] preview = pointer.getPreview().get(); 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; return outputFile;
} }
@ -892,7 +984,7 @@ class Manager implements Signal {
try { try {
for (GroupInfo record : groupStore.getGroups()) { for (GroupInfo record : groupStore.getGroups()) {
out.write(new DeviceGroup(record.groupId, Optional.fromNullable(record.name), 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)); record.active));
} }
} finally { } finally {
@ -922,7 +1014,7 @@ class Manager implements Signal {
try { try {
for (ContactInfo record : contactStore.getContacts()) { for (ContactInfo record : contactStore.getContacts()) {
out.write(new DeviceContact(record.number, Optional.fromNullable(record.name), out.write(new DeviceContact(record.number, Optional.fromNullable(record.name),
Optional.<SignalServiceAttachmentStream>absent())); // TODO add avatar createContactAvatarAttachment(record.number)));
} }
} finally { } finally {
out.close(); out.close();