Some code to work with conversations

This commit is contained in:
2017-03-13 22:49:40 +01:00
parent 10a45cc79c
commit d9090eb70c
16 changed files with 573 additions and 39 deletions

View File

@ -25,7 +25,7 @@ artifacts {
dependencies {
compile 'org.slf4j:slf4j-api:1.7.12'
compile 'ch.dissem.msgpack:msgpack:development-SNAPSHOT'
compile 'ch.dissem.msgpack:msgpack:1.0.0'
testCompile 'junit:junit:4.12'
testCompile 'org.hamcrest:hamcrest-library:1.3'
testCompile 'org.mockito:mockito-core:1.10.19'

View File

@ -87,7 +87,7 @@ class DefaultMessageListener implements NetworkHandler.MessageListener, Internal
BitmessageAddress identity = ctx.getAddressRepository().findIdentity(getPubkey.getRipeTag());
if (identity != null && identity.getPrivateKey() != null && !identity.isChan()) {
LOG.info("Got pubkey request for identity " + identity);
// FIXME: only send pubkey if it wasn't sent in the last 28 days
// FIXME: only send pubkey if it wasn't sent in the last TTL.pubkey() days
ctx.sendPubkey(identity, object.getStream());
}
}

View File

@ -18,22 +18,20 @@ package ch.dissem.bitmessage.entity;
import ch.dissem.bitmessage.entity.payload.Msg;
import ch.dissem.bitmessage.entity.payload.Pubkey.Feature;
import ch.dissem.bitmessage.entity.valueobject.extended.Attachment;
import ch.dissem.bitmessage.entity.valueobject.ExtendedEncoding;
import ch.dissem.bitmessage.entity.valueobject.InventoryVector;
import ch.dissem.bitmessage.entity.valueobject.Label;
import ch.dissem.bitmessage.entity.valueobject.extended.Attachment;
import ch.dissem.bitmessage.entity.valueobject.extended.Message;
import ch.dissem.bitmessage.exception.ApplicationException;
import ch.dissem.bitmessage.factory.ExtendedEncodingFactory;
import ch.dissem.bitmessage.factory.Factory;
import ch.dissem.bitmessage.utils.Decode;
import ch.dissem.bitmessage.utils.Encode;
import ch.dissem.bitmessage.utils.TTL;
import ch.dissem.bitmessage.utils.UnixTime;
import ch.dissem.bitmessage.utils.*;
import java.io.*;
import java.nio.ByteBuffer;
import java.util.*;
import java.util.Collections;
import static ch.dissem.bitmessage.entity.Plaintext.Encoding.EXTENDED;
import static ch.dissem.bitmessage.entity.Plaintext.Encoding.SIMPLE;
@ -50,6 +48,7 @@ public class Plaintext implements Streamable {
private final long encoding;
private final byte[] message;
private final byte[] ackData;
private final UUID conversationId;
private ExtendedEncoding extendedData;
private ObjectMessage ackMessage;
private Object id;
@ -90,6 +89,7 @@ public class Plaintext implements Streamable {
ttl = builder.ttl;
retries = builder.retries;
nextTry = builder.nextTry;
conversationId = builder.conversation;
}
public static Plaintext read(Type type, InputStream in) throws IOException {
@ -390,7 +390,7 @@ public class Plaintext implements Streamable {
}
public List<InventoryVector> getParents() {
if (Message.TYPE.equals(getExtendedData().getType())) {
if (getExtendedData() != null && Message.TYPE.equals(getExtendedData().getType())) {
return ((Message) extendedData.getContent()).getParents();
} else {
return Collections.emptyList();
@ -405,6 +405,10 @@ public class Plaintext implements Streamable {
}
}
public UUID getConversationId() {
return conversationId;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
@ -470,6 +474,16 @@ public class Plaintext implements Streamable {
return initialHash;
}
@Override
public String toString() {
String subject = getSubject();
if (subject == null || subject.length() == 0) {
return Strings.hex(initialHash).toString();
} else {
return subject;
}
}
public enum Encoding {
IGNORE(0), TRIVIAL(1), SIMPLE(2), EXTENDED(3);
@ -527,12 +541,13 @@ public class Plaintext implements Streamable {
private byte[] ackMessage;
private byte[] signature;
private long sent;
private long received;
private Long received;
private Status status;
private Set<Label> labels = new HashSet<>();
private long ttl;
private int retries;
private Long nextTry;
private UUID conversation;
public Builder(Type type) {
this.type = type;
@ -685,6 +700,11 @@ public class Plaintext implements Streamable {
return this;
}
public Builder conversation(UUID id) {
this.conversation = id;
return this;
}
public Plaintext build() {
if (from == null) {
from = new BitmessageAddress(Factory.createPubkey(
@ -706,6 +726,9 @@ public class Plaintext implements Streamable {
if (ttl <= 0) {
ttl = TTL.msg();
}
if (conversation == null) {
conversation = UUID.randomUUID();
}
return new Plaintext(this);
}
}

View File

@ -19,13 +19,16 @@ package ch.dissem.bitmessage.ports;
import ch.dissem.bitmessage.InternalContext;
import ch.dissem.bitmessage.entity.BitmessageAddress;
import ch.dissem.bitmessage.entity.Plaintext;
import ch.dissem.bitmessage.entity.valueobject.InventoryVector;
import ch.dissem.bitmessage.entity.valueobject.Label;
import ch.dissem.bitmessage.exception.ApplicationException;
import ch.dissem.bitmessage.utils.Strings;
import ch.dissem.bitmessage.utils.UnixTime;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
import static ch.dissem.bitmessage.utils.SqlStrings.join;
@ -71,6 +74,11 @@ public abstract class AbstractMessageRepository implements MessageRepository, In
}
}
@Override
public Plaintext getMessage(InventoryVector iv) {
return single(find("iv=X'" + Strings.hex(iv.getHash()) + "'"));
}
@Override
public Plaintext getMessage(byte[] initialHash) {
return single(find("initial_hash=X'" + Strings.hex(initialHash) + "'"));
@ -111,6 +119,20 @@ public abstract class AbstractMessageRepository implements MessageRepository, In
" AND next_try < " + UnixTime.now());
}
@Override
public List<Plaintext> findResponses(Plaintext parent) {
if (parent.getInventoryVector() == null) {
return Collections.emptyList();
}
return find("iv IN (SELECT child FROM Message_Parent"
+ " WHERE parent=X'" + Strings.hex(parent.getInventoryVector().getHash()) + "')");
}
@Override
public List<Plaintext> getConversation(UUID conversationId) {
return find("conversation=X'" + conversationId.toString().replace("-", "") + "'");
}
@Override
public List<Label> getLabels() {
return findLabels("1=1");

View File

@ -19,9 +19,12 @@ package ch.dissem.bitmessage.ports;
import ch.dissem.bitmessage.entity.BitmessageAddress;
import ch.dissem.bitmessage.entity.Plaintext;
import ch.dissem.bitmessage.entity.Plaintext.Status;
import ch.dissem.bitmessage.entity.valueobject.InventoryVector;
import ch.dissem.bitmessage.entity.valueobject.Label;
import java.util.Collection;
import java.util.List;
import java.util.UUID;
public interface MessageRepository {
List<Label> getLabels();
@ -32,10 +35,18 @@ public interface MessageRepository {
Plaintext getMessage(Object id);
Plaintext getMessage(InventoryVector iv);
Plaintext getMessage(byte[] initialHash);
Plaintext getMessageForAck(byte[] ackData);
/**
* @param label to search for
* @return a distinct list of all conversations that have at least one message with the given label.
*/
List<UUID> findConversations(Label label);
List<Plaintext> findMessages(Label label);
List<Plaintext> findMessages(Status status);
@ -44,9 +55,21 @@ public interface MessageRepository {
List<Plaintext> findMessages(BitmessageAddress sender);
List<Plaintext> findResponses(Plaintext parent);
List<Plaintext> findMessagesToResend();
void save(Plaintext message);
void remove(Plaintext message);
/**
* Returns all messages with this conversation ID. The returned messages aren't sorted in any way,
* so you may prefer to use {@link ch.dissem.bitmessage.utils.ConversationService#getConversation(UUID)}
* instead.
*
* @param conversationId ID of the requested conversation
* @return all messages with the given conversation ID
*/
Collection<Plaintext> getConversation(UUID conversationId);
}

View File

@ -24,6 +24,12 @@ import java.util.List;
* Stores and provides known peers.
*/
public interface NodeRegistry {
/**
* Removes all known nodes from registry. This should work around connection issues
* when there are many invalid nodes in the registry.
*/
void clear();
List<NetworkAddress> getKnownAddresses(int limit, long... streams);
void offerAddresses(List<NetworkAddress> addresses);

View File

@ -0,0 +1,112 @@
/*
* Copyright 2017 Christian Basler
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ch.dissem.bitmessage.utils;
import ch.dissem.bitmessage.entity.Plaintext;
import ch.dissem.bitmessage.entity.valueobject.InventoryVector;
import ch.dissem.bitmessage.ports.MessageRepository;
import java.util.*;
import java.util.Collections;
/**
* Helper service to work with conversations
*/
public class ConversationService {
private final MessageRepository messageRepository;
public ConversationService(MessageRepository messageRepository) {
this.messageRepository = messageRepository;
}
public List<Plaintext> getConversation(Plaintext message) {
return getConversation(message.getConversationId());
}
private LinkedList<Plaintext> sorted(Collection<Plaintext> collection) {
LinkedList<Plaintext> result = new LinkedList<>(collection);
Collections.sort(result, new Comparator<Plaintext>() {
@Override
public int compare(Plaintext o1, Plaintext o2) {
//noinspection NumberEquality - if both are null (if both are the same, it's a bonus)
if (o1.getReceived() == o2.getReceived()) {
return 0;
}
if (o1.getReceived() == null) {
return -1;
}
if (o2.getReceived() == null) {
return 1;
}
return -o1.getReceived().compareTo(o2.getReceived());
}
});
return result;
}
public List<Plaintext> getConversation(UUID conversationId) {
LinkedList<Plaintext> messages = sorted(messageRepository.getConversation(conversationId));
Map<InventoryVector, Plaintext> map = new HashMap<>(messages.size());
for (Plaintext message : messages) {
if (message.getInventoryVector() != null) {
map.put(message.getInventoryVector(), message);
}
}
LinkedList<Plaintext> result = new LinkedList<>();
while (!messages.isEmpty()) {
Plaintext last = messages.poll();
int pos = lastParentPosition(last, result);
result.add(pos, last);
addAncestors(last, result, messages, map);
}
return result;
}
private int lastParentPosition(Plaintext child, LinkedList<Plaintext> messages) {
Iterator<Plaintext> plaintextIterator = messages.descendingIterator();
int i = 0;
while (plaintextIterator.hasNext()) {
Plaintext next = plaintextIterator.next();
if (isParent(next, child)) {
break;
}
i++;
}
return messages.size() - i;
}
private boolean isParent(Plaintext item, Plaintext child) {
for (InventoryVector parentId : child.getParents()) {
if (parentId.equals(item.getInventoryVector())) {
return true;
}
}
return false;
}
private void addAncestors(Plaintext message, LinkedList<Plaintext> result, LinkedList<Plaintext> messages, Map<InventoryVector, Plaintext> map) {
for (InventoryVector parentKey : message.getParents()) {
Plaintext parent = map.remove(parentKey);
if (parent != null) {
messages.remove(parent);
result.addFirst(parent);
addAncestors(parent, result, messages, map);
}
}
}
}

View File

@ -138,7 +138,7 @@ public class Decode {
public static String varString(InputStream in, AccessCounter counter) throws IOException {
int length = (int) varInt(in, counter);
// FIXME: technically, it says the length in characters, but I think this one might be correct
// technically, it says the length in characters, but I think this one might be correct
// otherwise it will get complicated, as we'll need to read UTF-8 char by char...
return new String(bytes(in, length, counter), "utf-8");
}

View File

@ -0,0 +1,126 @@
/*
* Copyright 2017 Christian Basler
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ch.dissem.bitmessage.utils;
import ch.dissem.bitmessage.cryptography.bc.BouncyCryptography;
import ch.dissem.bitmessage.entity.BitmessageAddress;
import ch.dissem.bitmessage.entity.Plaintext;
import ch.dissem.bitmessage.entity.valueobject.ExtendedEncoding;
import ch.dissem.bitmessage.entity.valueobject.extended.Message;
import ch.dissem.bitmessage.ports.MessageRepository;
import org.junit.Test;
import java.util.LinkedList;
import java.util.List;
import java.util.UUID;
import static ch.dissem.bitmessage.entity.Plaintext.Type.MSG;
import static ch.dissem.bitmessage.utils.TestUtils.RANDOM;
import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertThat;
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class ConversationServiceTest {
private BitmessageAddress alice = new BitmessageAddress("BM-2cSqjfJ8xK6UUn5Rw3RpdGQ9RsDkBhWnS8");
private BitmessageAddress bob = new BitmessageAddress("BM-2cTtkBnb4BUYDndTKun6D9PjtueP2h1bQj");
private MessageRepository messageRepository = mock(MessageRepository.class);
private ConversationService conversationService = new ConversationService(messageRepository);
static {
Singleton.initialize(new BouncyCryptography());
}
@Test
public void ensureConversationIsSortedProperly() {
List<Plaintext> expected = getConversation();
when(conversationService.getConversation(any(UUID.class))).thenReturn(expected);
List<Plaintext> actual = conversationService.getConversation(UUID.randomUUID());
assertThat(actual, is(expected));
}
private List<Plaintext> getConversation() {
List<Plaintext> result = new LinkedList<>();
Plaintext older = plaintext(alice, bob,
new Message.Builder()
.subject("hey there")
.body("does it work?")
.build(),
Plaintext.Status.SENT);
result.add(older);
Plaintext root = plaintext(alice, bob,
new Message.Builder()
.subject("new test")
.body("There's a new test in town!")
.build(),
Plaintext.Status.SENT);
result.add(root);
result.add(
plaintext(bob, alice,
new Message.Builder()
.subject("Re: new test (1a)")
.body("Nice!")
.addParent(root)
.build(),
Plaintext.Status.RECEIVED)
);
Plaintext latest = plaintext(bob, alice,
new Message.Builder()
.subject("Re: new test (2b)")
.body("PS: it did work!")
.addParent(root)
.addParent(older)
.build(),
Plaintext.Status.RECEIVED);
result.add(latest);
result.add(
plaintext(alice, bob,
new Message.Builder()
.subject("Re: new test (2)")
.body("")
.addParent(latest)
.build(),
Plaintext.Status.DRAFT)
);
return result;
}
private int timer = 2;
private Plaintext plaintext(BitmessageAddress from, BitmessageAddress to,
ExtendedEncoding content, Plaintext.Status status) {
Plaintext.Builder builder = new Plaintext.Builder(MSG)
.IV(TestUtils.randomInventoryVector())
.from(from)
.to(to)
.message(content)
.status(status);
if (status != Plaintext.Status.DRAFT && status != Plaintext.Status.DOING_PROOF_OF_WORK) {
builder.received(5 * ++timer - RANDOM.nextInt(10));
}
return builder.build();
}
}