mirror of
https://github.com/AsamK/signal-cli
synced 2025-08-29 18:40:39 +00:00
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:
parent
9427616906
commit
3e2024ff0a
3 changed files with 136 additions and 33 deletions
|
@ -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;
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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();
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue