Implement support for sending disappearing messages

Stores the expiration timeout received from contacts in the config file

Fixes #27
This commit is contained in:
AsamK 2016-10-31 20:52:06 +01:00
parent a4e22539a3
commit 82cecfff85
3 changed files with 143 additions and 35 deletions

View file

@ -0,0 +1,56 @@
package org.asamk.signal;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.*;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class JsonThreadStore {
@JsonProperty("threads")
@JsonSerialize(using = JsonThreadStore.MapToListSerializer.class)
@JsonDeserialize(using = ThreadsDeserializer.class)
private Map<String, ThreadInfo> threads = new HashMap<>();
private static final ObjectMapper jsonProcessor = new ObjectMapper();
void updateThread(ThreadInfo thread) {
threads.put(thread.id, thread);
}
ThreadInfo getThread(String id) {
return threads.get(id);
}
List<ThreadInfo> getThreads() {
return new ArrayList<>(threads.values());
}
public static class MapToListSerializer extends JsonSerializer<Map<?, ?>> {
@Override
public void serialize(final Map<?, ?> value, final JsonGenerator jgen, final SerializerProvider provider) throws IOException {
jgen.writeObject(value.values());
}
}
public static class ThreadsDeserializer extends JsonDeserializer<Map<String, ThreadInfo>> {
@Override
public Map<String, ThreadInfo> deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException {
Map<String, ThreadInfo> threads = new HashMap<>();
JsonNode node = jsonParser.getCodec().readTree(jsonParser);
for (JsonNode n : node) {
ThreadInfo t = jsonProcessor.treeToValue(n, ThreadInfo.class);
threads.put(t.id, t);
}
return threads;
}
}
}

View file

@ -107,6 +107,7 @@ class Manager implements Signal {
private SignalServiceAccountManager accountManager; private SignalServiceAccountManager accountManager;
private JsonGroupStore groupStore; private JsonGroupStore groupStore;
private JsonContactsStore contactStore; private JsonContactsStore contactStore;
private JsonThreadStore threadStore;
public Manager(String username, String settingsPath) { public Manager(String username, String settingsPath) {
this.username = username; this.username = username;
@ -267,6 +268,13 @@ class Manager implements Signal {
if (contactStore == null) { if (contactStore == null) {
contactStore = new JsonContactsStore(); contactStore = new JsonContactsStore();
} }
JsonNode threadStoreNode = rootNode.get("threadStore");
if (threadStoreNode != null) {
threadStore = jsonProcessor.convertValue(threadStoreNode, JsonThreadStore.class);
}
if (threadStore == null) {
threadStore = new JsonThreadStore();
}
} }
private void migrateLegacyConfigs() { private void migrateLegacyConfigs() {
@ -305,6 +313,7 @@ class Manager implements Signal {
.putPOJO("axolotlStore", signalProtocolStore) .putPOJO("axolotlStore", signalProtocolStore)
.putPOJO("groupStore", groupStore) .putPOJO("groupStore", groupStore)
.putPOJO("contactStore", contactStore) .putPOJO("contactStore", contactStore)
.putPOJO("threadStore", threadStore)
; ;
try { try {
openFileChannel(); openFileChannel();
@ -572,14 +581,17 @@ class Manager implements Signal {
.build(); .build();
messageBuilder.asGroupMessage(group); messageBuilder.asGroupMessage(group);
} }
SignalServiceDataMessage message = messageBuilder.build(); ThreadInfo thread = threadStore.getThread(Base64.encodeBytes(groupId));
if (thread != null) {
messageBuilder.withExpiration(thread.messageExpirationTime);
}
final GroupInfo g = getGroupForSending(groupId); final GroupInfo g = getGroupForSending(groupId);
// Don't send group message to ourself // Don't send group message to ourself
final List<String> membersSend = new ArrayList<>(g.members); final List<String> membersSend = new ArrayList<>(g.members);
membersSend.remove(this.username); membersSend.remove(this.username);
sendMessage(message, membersSend); sendMessage(messageBuilder, membersSend);
} }
public void sendQuitGroupMessage(byte[] groupId) throws GroupNotFoundException, IOException, EncapsulatedExceptions { public void sendQuitGroupMessage(byte[] groupId) throws GroupNotFoundException, IOException, EncapsulatedExceptions {
@ -587,15 +599,14 @@ class Manager implements Signal {
.withId(groupId) .withId(groupId)
.build(); .build();
SignalServiceDataMessage message = SignalServiceDataMessage.newBuilder() SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder()
.asGroupMessage(group) .asGroupMessage(group);
.build();
final GroupInfo g = getGroupForSending(groupId); final GroupInfo g = getGroupForSending(groupId);
g.members.remove(this.username); g.members.remove(this.username);
groupStore.updateGroup(g); groupStore.updateGroup(g);
sendMessage(message, g.members); sendMessage(messageBuilder, g.members);
} }
private static String join(CharSequence separator, Iterable<? extends CharSequence> list) { private static String join(CharSequence separator, Iterable<? extends CharSequence> list) {
@ -672,14 +683,13 @@ class Manager implements Signal {
groupStore.updateGroup(g); groupStore.updateGroup(g);
SignalServiceDataMessage message = SignalServiceDataMessage.newBuilder() SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder()
.asGroupMessage(group.build()) .asGroupMessage(group.build());
.build();
// Don't send group message to ourself // Don't send group message to ourself
final List<String> membersSend = new ArrayList<>(g.members); final List<String> membersSend = new ArrayList<>(g.members);
membersSend.remove(this.username); membersSend.remove(this.username);
sendMessage(message, membersSend); sendMessage(messageBuilder, membersSend);
return g.groupId; return g.groupId;
} }
@ -699,25 +709,22 @@ class Manager implements Signal {
if (attachments != null) { if (attachments != null) {
messageBuilder.withAttachments(getSignalServiceAttachments(attachments)); messageBuilder.withAttachments(getSignalServiceAttachments(attachments));
} }
SignalServiceDataMessage message = messageBuilder.build(); sendMessage(messageBuilder, recipients);
sendMessage(message, recipients);
} }
@Override @Override
public void sendEndSessionMessage(List<String> recipients) throws IOException, EncapsulatedExceptions { public void sendEndSessionMessage(List<String> recipients) throws IOException, EncapsulatedExceptions {
SignalServiceDataMessage message = SignalServiceDataMessage.newBuilder() SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder()
.asEndSessionMessage() .asEndSessionMessage();
.build();
sendMessage(message, recipients); sendMessage(messageBuilder, recipients);
} }
private void requestSyncGroups() throws IOException { private void requestSyncGroups() throws IOException {
SignalServiceProtos.SyncMessage.Request r = SignalServiceProtos.SyncMessage.Request.newBuilder().setType(SignalServiceProtos.SyncMessage.Request.Type.GROUPS).build(); SignalServiceProtos.SyncMessage.Request r = SignalServiceProtos.SyncMessage.Request.newBuilder().setType(SignalServiceProtos.SyncMessage.Request.Type.GROUPS).build();
SignalServiceSyncMessage message = SignalServiceSyncMessage.forRequest(new RequestMessage(r)); SignalServiceSyncMessage message = SignalServiceSyncMessage.forRequest(new RequestMessage(r));
try { try {
sendMessage(message); sendSyncMessage(message);
} catch (UntrustedIdentityException e) { } catch (UntrustedIdentityException e) {
e.printStackTrace(); e.printStackTrace();
} }
@ -727,13 +734,13 @@ class Manager implements Signal {
SignalServiceProtos.SyncMessage.Request r = SignalServiceProtos.SyncMessage.Request.newBuilder().setType(SignalServiceProtos.SyncMessage.Request.Type.CONTACTS).build(); SignalServiceProtos.SyncMessage.Request r = SignalServiceProtos.SyncMessage.Request.newBuilder().setType(SignalServiceProtos.SyncMessage.Request.Type.CONTACTS).build();
SignalServiceSyncMessage message = SignalServiceSyncMessage.forRequest(new RequestMessage(r)); SignalServiceSyncMessage message = SignalServiceSyncMessage.forRequest(new RequestMessage(r));
try { try {
sendMessage(message); sendSyncMessage(message);
} catch (UntrustedIdentityException e) { } catch (UntrustedIdentityException e) {
e.printStackTrace(); e.printStackTrace();
} }
} }
private void sendMessage(SignalServiceSyncMessage message) private void sendSyncMessage(SignalServiceSyncMessage message)
throws IOException, UntrustedIdentityException { throws IOException, UntrustedIdentityException {
SignalServiceMessageSender messageSender = new SignalServiceMessageSender(URL, TRUST_STORE, username, password, SignalServiceMessageSender messageSender = new SignalServiceMessageSender(URL, TRUST_STORE, username, password,
deviceId, signalProtocolStore, USER_AGENT, Optional.<SignalServiceMessageSender.EventListener>absent()); deviceId, signalProtocolStore, USER_AGENT, Optional.<SignalServiceMessageSender.EventListener>absent());
@ -745,24 +752,17 @@ class Manager implements Signal {
} }
} }
private void sendMessage(SignalServiceDataMessage message, Collection<String> recipients) private void sendMessage(SignalServiceDataMessage.Builder messageBuilder, Collection<String> recipients)
throws EncapsulatedExceptions, IOException { throws EncapsulatedExceptions, IOException {
Set<SignalServiceAddress> recipientsTS = new HashSet<>(recipients.size()); Set<SignalServiceAddress> recipientsTS = getSignalServiceAddresses(recipients);
for (String recipient : recipients) { if (recipientsTS == null) return;
try {
recipientsTS.add(getPushAddress(recipient));
} catch (InvalidNumberException e) {
System.err.println("Failed to add recipient \"" + recipient + "\": " + e.getMessage());
System.err.println("Aborting sending.");
save();
return;
}
}
SignalServiceDataMessage message = null;
try { try {
SignalServiceMessageSender messageSender = new SignalServiceMessageSender(URL, TRUST_STORE, username, password, SignalServiceMessageSender messageSender = new SignalServiceMessageSender(URL, TRUST_STORE, username, password,
deviceId, signalProtocolStore, USER_AGENT, Optional.<SignalServiceMessageSender.EventListener>absent()); deviceId, signalProtocolStore, USER_AGENT, Optional.<SignalServiceMessageSender.EventListener>absent());
message = messageBuilder.build();
if (message.getGroupInfo().isPresent()) { if (message.getGroupInfo().isPresent()) {
try { try {
messageSender.sendMessage(new ArrayList<>(recipientsTS), message); messageSender.sendMessage(new ArrayList<>(recipientsTS), message);
@ -777,6 +777,13 @@ class Manager implements Signal {
List<UnregisteredUserException> unregisteredUsers = new LinkedList<>(); List<UnregisteredUserException> unregisteredUsers = new LinkedList<>();
List<NetworkFailureException> networkExceptions = new LinkedList<>(); List<NetworkFailureException> networkExceptions = new LinkedList<>();
for (SignalServiceAddress address : recipientsTS) { for (SignalServiceAddress address : recipientsTS) {
ThreadInfo thread = threadStore.getThread(address.getNumber());
if (thread != null) {
messageBuilder.withExpiration(thread.messageExpirationTime);
} else {
messageBuilder.withExpiration(0);
}
message = messageBuilder.build();
try { try {
messageSender.sendMessage(address, message); messageSender.sendMessage(address, message);
} catch (UntrustedIdentityException e) { } catch (UntrustedIdentityException e) {
@ -793,7 +800,7 @@ class Manager implements Signal {
} }
} }
} finally { } finally {
if (message.isEndSession()) { if (message != null && message.isEndSession()) {
for (SignalServiceAddress recipient : recipientsTS) { for (SignalServiceAddress recipient : recipientsTS) {
handleEndSession(recipient.getNumber()); handleEndSession(recipient.getNumber());
} }
@ -802,6 +809,21 @@ class Manager implements Signal {
} }
} }
private Set<SignalServiceAddress> getSignalServiceAddresses(Collection<String> recipients) {
Set<SignalServiceAddress> recipientsTS = new HashSet<>(recipients.size());
for (String recipient : recipients) {
try {
recipientsTS.add(getPushAddress(recipient));
} catch (InvalidNumberException e) {
System.err.println("Failed to add recipient \"" + recipient + "\": " + e.getMessage());
System.err.println("Aborting sending.");
save();
return null;
}
}
return recipientsTS;
}
private SignalServiceContent decryptMessage(SignalServiceEnvelope envelope) throws NoSessionException, LegacyMessageException, InvalidVersionException, InvalidMessageException, DuplicateMessageException, InvalidKeyException, InvalidKeyIdException, org.whispersystems.libsignal.UntrustedIdentityException { private SignalServiceContent decryptMessage(SignalServiceEnvelope envelope) throws NoSessionException, LegacyMessageException, InvalidVersionException, InvalidMessageException, DuplicateMessageException, InvalidKeyException, InvalidKeyIdException, org.whispersystems.libsignal.UntrustedIdentityException {
SignalServiceCipher cipher = new SignalServiceCipher(new SignalServiceAddress(username), signalProtocolStore); SignalServiceCipher cipher = new SignalServiceCipher(new SignalServiceAddress(username), signalProtocolStore);
try { try {
@ -821,8 +843,10 @@ class Manager implements Signal {
} }
private void handleSignalServiceDataMessage(SignalServiceDataMessage message, boolean isSync, String source, String destination) { private void handleSignalServiceDataMessage(SignalServiceDataMessage message, boolean isSync, String source, String destination) {
String threadId;
if (message.getGroupInfo().isPresent()) { if (message.getGroupInfo().isPresent()) {
SignalServiceGroup groupInfo = message.getGroupInfo().get(); SignalServiceGroup groupInfo = message.getGroupInfo().get();
threadId = Base64.encodeBytes(groupInfo.getGroupId());
switch (groupInfo.getType()) { switch (groupInfo.getType()) {
case UPDATE: case UPDATE:
GroupInfo group; GroupInfo group;
@ -862,10 +886,27 @@ class Manager implements Signal {
} }
break; break;
} }
} else {
if (isSync) {
threadId = destination;
} else {
threadId = source;
}
} }
if (message.isEndSession()) { if (message.isEndSession()) {
handleEndSession(isSync ? destination : source); handleEndSession(isSync ? destination : source);
} }
if (message.isExpirationUpdate() || message.getBody().isPresent()) {
ThreadInfo thread = threadStore.getThread(threadId);
if (thread == null) {
thread = new ThreadInfo();
thread.id = threadId;
}
if (thread.messageExpirationTime != message.getExpiresInSeconds()) {
thread.messageExpirationTime = message.getExpiresInSeconds();
threadStore.updateThread(thread);
}
}
if (message.getAttachments().isPresent()) { if (message.getAttachments().isPresent()) {
for (SignalServiceAttachment attachment : message.getAttachments().get()) { for (SignalServiceAttachment attachment : message.getAttachments().get()) {
if (attachment.isPointer()) { if (attachment.isPointer()) {
@ -1273,7 +1314,7 @@ class Manager implements Signal {
.withLength(groupsFile.length()) .withLength(groupsFile.length())
.build(); .build();
sendMessage(SignalServiceSyncMessage.forGroups(attachmentStream)); sendSyncMessage(SignalServiceSyncMessage.forGroups(attachmentStream));
} }
} finally { } finally {
groupsFile.delete(); groupsFile.delete();
@ -1302,7 +1343,7 @@ class Manager implements Signal {
.withLength(contactsFile.length()) .withLength(contactsFile.length())
.build(); .build();
sendMessage(SignalServiceSyncMessage.forContacts(attachmentStream)); sendSyncMessage(SignalServiceSyncMessage.forContacts(attachmentStream));
} }
} finally { } finally {
contactsFile.delete(); contactsFile.delete();

View file

@ -0,0 +1,11 @@
package org.asamk.signal;
import com.fasterxml.jackson.annotation.JsonProperty;
public class ThreadInfo {
@JsonProperty
public String id;
@JsonProperty
public int messageExpirationTime;
}