Some code to work with conversations
This commit is contained in:
@ -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'
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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");
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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");
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user