mirror of
https://code.briarproject.org/briar/briar.git
synced 2026-02-12 18:59:06 +01:00
Merge branch '708-implement-protocol-for-private-group-messaging' into 'master'
Implement protocol for private group messaging Closes #708 See merge request !360
This commit is contained in:
@@ -61,7 +61,7 @@ import static org.briarproject.api.sync.ValidationManager.State.PENDING;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class BlogManagerTest {
|
||||
public class BlogManagerTest extends BriarIntegrationTest {
|
||||
|
||||
private LifecycleManager lifecycleManager0, lifecycleManager1;
|
||||
private SyncSessionFactory sync0, sync1;
|
||||
@@ -94,7 +94,7 @@ public class BlogManagerTest {
|
||||
private final String AUTHOR2 = "Author 2";
|
||||
|
||||
private static final Logger LOG =
|
||||
Logger.getLogger(ForumSharingIntegrationTest.class.getName());
|
||||
Logger.getLogger(BlogManagerTest.class.getName());
|
||||
|
||||
private BlogManagerTestComponent t0, t1;
|
||||
|
||||
|
||||
@@ -0,0 +1,559 @@
|
||||
package org.briarproject;
|
||||
|
||||
import net.jodah.concurrentunit.Waiter;
|
||||
|
||||
import org.briarproject.api.clients.MessageTracker.GroupCount;
|
||||
import org.briarproject.api.contact.ContactId;
|
||||
import org.briarproject.api.contact.ContactManager;
|
||||
import org.briarproject.api.crypto.CryptoComponent;
|
||||
import org.briarproject.api.crypto.KeyPair;
|
||||
import org.briarproject.api.crypto.SecretKey;
|
||||
import org.briarproject.api.db.DbException;
|
||||
import org.briarproject.api.db.Transaction;
|
||||
import org.briarproject.api.event.Event;
|
||||
import org.briarproject.api.event.EventListener;
|
||||
import org.briarproject.api.event.MessageStateChangedEvent;
|
||||
import org.briarproject.api.identity.AuthorFactory;
|
||||
import org.briarproject.api.identity.IdentityManager;
|
||||
import org.briarproject.api.identity.LocalAuthor;
|
||||
import org.briarproject.api.lifecycle.LifecycleManager;
|
||||
import org.briarproject.api.privategroup.GroupMessage;
|
||||
import org.briarproject.api.privategroup.GroupMessageFactory;
|
||||
import org.briarproject.api.privategroup.GroupMessageHeader;
|
||||
import org.briarproject.api.privategroup.JoinMessageHeader;
|
||||
import org.briarproject.api.privategroup.PrivateGroup;
|
||||
import org.briarproject.api.privategroup.PrivateGroupFactory;
|
||||
import org.briarproject.api.privategroup.PrivateGroupManager;
|
||||
import org.briarproject.api.sync.GroupId;
|
||||
import org.briarproject.api.sync.MessageId;
|
||||
import org.briarproject.api.sync.SyncSession;
|
||||
import org.briarproject.api.sync.SyncSessionFactory;
|
||||
import org.briarproject.api.system.Clock;
|
||||
import org.briarproject.contact.ContactModule;
|
||||
import org.briarproject.crypto.CryptoModule;
|
||||
import org.briarproject.lifecycle.LifecycleModule;
|
||||
import org.briarproject.privategroup.PrivateGroupModule;
|
||||
import org.briarproject.properties.PropertiesModule;
|
||||
import org.briarproject.sync.SyncModule;
|
||||
import org.briarproject.transport.TransportModule;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.ExpectedException;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.Collection;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import static org.briarproject.TestPluginsModule.MAX_LATENCY;
|
||||
import static org.briarproject.api.identity.Author.Status.VERIFIED;
|
||||
import static org.briarproject.api.sync.ValidationManager.State.DELIVERED;
|
||||
import static org.briarproject.api.sync.ValidationManager.State.INVALID;
|
||||
import static org.briarproject.api.sync.ValidationManager.State.PENDING;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class PrivateGroupManagerTest extends BriarIntegrationTest {
|
||||
|
||||
private LifecycleManager lifecycleManager0, lifecycleManager1;
|
||||
private SyncSessionFactory sync0, sync1;
|
||||
private PrivateGroupManager groupManager0, groupManager1;
|
||||
private ContactManager contactManager0, contactManager1;
|
||||
private ContactId contactId0, contactId1;
|
||||
private IdentityManager identityManager0, identityManager1;
|
||||
private LocalAuthor author0, author1;
|
||||
private PrivateGroup privateGroup0;
|
||||
private GroupId groupId0;
|
||||
private GroupMessage newMemberMsg0;
|
||||
|
||||
@Inject
|
||||
Clock clock;
|
||||
@Inject
|
||||
AuthorFactory authorFactory;
|
||||
@Inject
|
||||
CryptoComponent crypto;
|
||||
@Inject
|
||||
PrivateGroupFactory privateGroupFactory;
|
||||
@Inject
|
||||
GroupMessageFactory groupMessageFactory;
|
||||
|
||||
// objects accessed from background threads need to be volatile
|
||||
private volatile Waiter validationWaiter;
|
||||
private volatile Waiter deliveryWaiter;
|
||||
|
||||
private final File testDir = TestUtils.getTestDirectory();
|
||||
private final SecretKey master = TestUtils.getSecretKey();
|
||||
private final int TIMEOUT = 15000;
|
||||
private final String AUTHOR1 = "Author 1";
|
||||
private final String AUTHOR2 = "Author 2";
|
||||
|
||||
private static final Logger LOG =
|
||||
Logger.getLogger(PrivateGroupManagerTest.class.getName());
|
||||
|
||||
private PrivateGroupManagerTestComponent t0, t1;
|
||||
|
||||
@Rule
|
||||
public ExpectedException thrown = ExpectedException.none();
|
||||
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
PrivateGroupManagerTestComponent component =
|
||||
DaggerPrivateGroupManagerTestComponent.builder().build();
|
||||
component.inject(this);
|
||||
injectEagerSingletons(component);
|
||||
|
||||
assertTrue(testDir.mkdirs());
|
||||
File t0Dir = new File(testDir, AUTHOR1);
|
||||
t0 = DaggerPrivateGroupManagerTestComponent.builder()
|
||||
.testDatabaseModule(new TestDatabaseModule(t0Dir)).build();
|
||||
injectEagerSingletons(t0);
|
||||
File t1Dir = new File(testDir, AUTHOR2);
|
||||
t1 = DaggerPrivateGroupManagerTestComponent.builder()
|
||||
.testDatabaseModule(new TestDatabaseModule(t1Dir)).build();
|
||||
injectEagerSingletons(t1);
|
||||
|
||||
identityManager0 = t0.getIdentityManager();
|
||||
identityManager1 = t1.getIdentityManager();
|
||||
contactManager0 = t0.getContactManager();
|
||||
contactManager1 = t1.getContactManager();
|
||||
groupManager0 = t0.getPrivateGroupManager();
|
||||
groupManager1 = t1.getPrivateGroupManager();
|
||||
sync0 = t0.getSyncSessionFactory();
|
||||
sync1 = t1.getSyncSessionFactory();
|
||||
|
||||
// initialize waiters fresh for each test
|
||||
validationWaiter = new Waiter();
|
||||
deliveryWaiter = new Waiter();
|
||||
|
||||
startLifecycles();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSendingMessage() throws Exception {
|
||||
defaultInit();
|
||||
|
||||
// create and add test message
|
||||
long time = clock.currentTimeMillis();
|
||||
String body = "This is a test message!";
|
||||
MessageId previousMsgId =
|
||||
groupManager0.getPreviousMsgId(groupId0);
|
||||
GroupMessage msg = groupMessageFactory
|
||||
.createGroupMessage(groupId0, time, null, author0, body,
|
||||
previousMsgId);
|
||||
groupManager0.addLocalMessage(msg);
|
||||
assertEquals(msg.getMessage().getId(),
|
||||
groupManager0.getPreviousMsgId(groupId0));
|
||||
|
||||
// sync test message
|
||||
sync0To1();
|
||||
deliveryWaiter.await(TIMEOUT, 1);
|
||||
|
||||
// assert that message arrived as expected
|
||||
Collection<GroupMessageHeader> headers =
|
||||
groupManager1.getHeaders(groupId0);
|
||||
assertEquals(3, headers.size());
|
||||
GroupMessageHeader header = null;
|
||||
for (GroupMessageHeader h : headers) {
|
||||
if (!(h instanceof JoinMessageHeader)) {
|
||||
header = h;
|
||||
}
|
||||
}
|
||||
assertTrue(header != null);
|
||||
assertFalse(header.isRead());
|
||||
assertEquals(author0, header.getAuthor());
|
||||
assertEquals(time, header.getTimestamp());
|
||||
assertEquals(VERIFIED, header.getAuthorStatus());
|
||||
assertEquals(body, groupManager1.getMessageBody(header.getId()));
|
||||
GroupCount count = groupManager1.getGroupCount(groupId0);
|
||||
assertEquals(2, count.getUnreadCount());
|
||||
assertEquals(time, count.getLatestMsgTime());
|
||||
assertEquals(3, count.getMsgCount());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMessageWithWrongPreviousMsgId() throws Exception {
|
||||
defaultInit();
|
||||
|
||||
// create and add test message with no previousMsgId
|
||||
GroupMessage msg = groupMessageFactory
|
||||
.createGroupMessage(groupId0, clock.currentTimeMillis(), null,
|
||||
author0, "test", null);
|
||||
groupManager0.addLocalMessage(msg);
|
||||
|
||||
// sync test message
|
||||
sync0To1();
|
||||
validationWaiter.await(TIMEOUT, 1);
|
||||
|
||||
// assert that message did not arrive
|
||||
assertEquals(2, groupManager1.getHeaders(groupId0).size());
|
||||
|
||||
// create and add test message with random previousMsgId
|
||||
MessageId previousMsgId = new MessageId(TestUtils.getRandomId());
|
||||
msg = groupMessageFactory
|
||||
.createGroupMessage(groupId0, clock.currentTimeMillis(), null,
|
||||
author0, "test", previousMsgId);
|
||||
groupManager0.addLocalMessage(msg);
|
||||
|
||||
// sync test message
|
||||
sync0To1();
|
||||
validationWaiter.await(TIMEOUT, 1);
|
||||
|
||||
// assert that message did not arrive
|
||||
assertEquals(2, groupManager1.getHeaders(groupId0).size());
|
||||
|
||||
// create and add test message with wrong previousMsgId
|
||||
previousMsgId = groupManager1.getPreviousMsgId(groupId0);
|
||||
msg = groupMessageFactory
|
||||
.createGroupMessage(groupId0, clock.currentTimeMillis(), null,
|
||||
author0, "test", previousMsgId);
|
||||
groupManager0.addLocalMessage(msg);
|
||||
|
||||
// sync test message
|
||||
sync0To1();
|
||||
validationWaiter.await(TIMEOUT, 1);
|
||||
|
||||
// assert that message did not arrive
|
||||
assertEquals(2, groupManager1.getHeaders(groupId0).size());
|
||||
|
||||
// create and add test message with previousMsgId of newMemberMsg
|
||||
previousMsgId = newMemberMsg0.getMessage().getId();
|
||||
msg = groupMessageFactory
|
||||
.createGroupMessage(groupId0, clock.currentTimeMillis(), null,
|
||||
author0, "test", previousMsgId);
|
||||
groupManager0.addLocalMessage(msg);
|
||||
|
||||
// sync test message
|
||||
sync0To1();
|
||||
validationWaiter.await(TIMEOUT, 1);
|
||||
|
||||
// assert that message did not arrive
|
||||
assertEquals(2, groupManager1.getHeaders(groupId0).size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMessageWithWrongParentMsgId() throws Exception {
|
||||
defaultInit();
|
||||
|
||||
// create and add test message with random parentMsgId
|
||||
MessageId parentMsgId = new MessageId(TestUtils.getRandomId());
|
||||
MessageId previousMsgId = groupManager0.getPreviousMsgId(groupId0);
|
||||
GroupMessage msg = groupMessageFactory
|
||||
.createGroupMessage(groupId0, clock.currentTimeMillis(),
|
||||
parentMsgId, author0, "test", previousMsgId);
|
||||
groupManager0.addLocalMessage(msg);
|
||||
|
||||
// sync test message
|
||||
sync0To1();
|
||||
validationWaiter.await(TIMEOUT, 1);
|
||||
|
||||
// assert that message did not arrive
|
||||
assertEquals(2, groupManager1.getHeaders(groupId0).size());
|
||||
|
||||
// create and add test message with wrong parentMsgId
|
||||
parentMsgId = previousMsgId;
|
||||
msg = groupMessageFactory
|
||||
.createGroupMessage(groupId0, clock.currentTimeMillis(),
|
||||
parentMsgId, author0, "test", previousMsgId);
|
||||
groupManager0.addLocalMessage(msg);
|
||||
|
||||
// sync test message
|
||||
sync0To1();
|
||||
validationWaiter.await(TIMEOUT, 1);
|
||||
|
||||
// assert that message did not arrive
|
||||
assertEquals(2, groupManager1.getHeaders(groupId0).size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMessageWithWrongTimestamp() throws Exception {
|
||||
defaultInit();
|
||||
|
||||
// create and add test message with wrong timestamp
|
||||
MessageId previousMsgId = groupManager0.getPreviousMsgId(groupId0);
|
||||
GroupMessage msg = groupMessageFactory
|
||||
.createGroupMessage(groupId0, 42, null, author0, "test",
|
||||
previousMsgId);
|
||||
groupManager0.addLocalMessage(msg);
|
||||
|
||||
// sync test message
|
||||
sync0To1();
|
||||
validationWaiter.await(TIMEOUT, 1);
|
||||
|
||||
// assert that message did not arrive
|
||||
assertEquals(2, groupManager1.getHeaders(groupId0).size());
|
||||
|
||||
// create and add test message with good timestamp
|
||||
long time = clock.currentTimeMillis();
|
||||
msg = groupMessageFactory
|
||||
.createGroupMessage(groupId0, time, null, author0, "test",
|
||||
previousMsgId);
|
||||
groupManager0.addLocalMessage(msg);
|
||||
|
||||
// sync test message
|
||||
sync0To1();
|
||||
deliveryWaiter.await(TIMEOUT, 1);
|
||||
assertEquals(3, groupManager1.getHeaders(groupId0).size());
|
||||
|
||||
// create and add test message with same timestamp as previous message
|
||||
previousMsgId = msg.getMessage().getId();
|
||||
msg = groupMessageFactory
|
||||
.createGroupMessage(groupId0, time, previousMsgId, author0,
|
||||
"test2", previousMsgId);
|
||||
groupManager0.addLocalMessage(msg);
|
||||
|
||||
// sync test message
|
||||
sync0To1();
|
||||
validationWaiter.await(TIMEOUT, 1);
|
||||
|
||||
// assert that message did not arrive
|
||||
assertEquals(3, groupManager1.getHeaders(groupId0).size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWrongJoinMessages() throws Exception {
|
||||
addDefaultIdentities();
|
||||
addDefaultContacts();
|
||||
listenToEvents();
|
||||
|
||||
// author0 joins privateGroup0 with later timestamp
|
||||
long joinTime = clock.currentTimeMillis();
|
||||
GroupMessage newMemberMsg = groupMessageFactory
|
||||
.createNewMemberMessage(groupId0, joinTime, author0, author0);
|
||||
GroupMessage joinMsg = groupMessageFactory
|
||||
.createJoinMessage(groupId0, joinTime + 1, author0,
|
||||
newMemberMsg.getMessage().getId());
|
||||
groupManager0.addPrivateGroup(privateGroup0, newMemberMsg, joinMsg);
|
||||
assertEquals(joinMsg.getMessage().getId(),
|
||||
groupManager0.getPreviousMsgId(groupId0));
|
||||
|
||||
// make group visible to 1
|
||||
Transaction txn0 = t0.getDatabaseComponent().startTransaction(false);
|
||||
t0.getDatabaseComponent()
|
||||
.setVisibleToContact(txn0, contactId1, privateGroup0.getId(),
|
||||
true);
|
||||
txn0.setComplete();
|
||||
t0.getDatabaseComponent().endTransaction(txn0);
|
||||
|
||||
// author1 joins privateGroup0 and refers to wrong NEW_MEMBER message
|
||||
joinMsg = groupMessageFactory
|
||||
.createJoinMessage(groupId0, joinTime, author1,
|
||||
newMemberMsg.getMessage().getId());
|
||||
joinTime = clock.currentTimeMillis();
|
||||
newMemberMsg = groupMessageFactory
|
||||
.createNewMemberMessage(groupId0, joinTime, author0, author1);
|
||||
groupManager1.addPrivateGroup(privateGroup0, newMemberMsg, joinMsg);
|
||||
assertEquals(joinMsg.getMessage().getId(),
|
||||
groupManager1.getPreviousMsgId(groupId0));
|
||||
|
||||
// make group visible to 0
|
||||
Transaction txn1 = t1.getDatabaseComponent().startTransaction(false);
|
||||
t1.getDatabaseComponent()
|
||||
.setVisibleToContact(txn1, contactId0, privateGroup0.getId(),
|
||||
true);
|
||||
txn1.setComplete();
|
||||
t1.getDatabaseComponent().endTransaction(txn1);
|
||||
|
||||
// sync join messages
|
||||
sync0To1();
|
||||
deliveryWaiter.await(TIMEOUT, 1);
|
||||
validationWaiter.await(TIMEOUT, 1);
|
||||
|
||||
// assert that 0 never joined the group from 1's perspective
|
||||
assertEquals(1, groupManager1.getHeaders(groupId0).size());
|
||||
|
||||
sync1To0();
|
||||
deliveryWaiter.await(TIMEOUT, 1);
|
||||
validationWaiter.await(TIMEOUT, 1);
|
||||
|
||||
// assert that 1 never joined the group from 0's perspective
|
||||
assertEquals(1, groupManager0.getHeaders(groupId0).size());
|
||||
}
|
||||
|
||||
@After
|
||||
public void tearDown() throws Exception {
|
||||
stopLifecycles();
|
||||
TestUtils.deleteTestDirectory(testDir);
|
||||
}
|
||||
|
||||
private class Listener implements EventListener {
|
||||
@Override
|
||||
public void eventOccurred(Event e) {
|
||||
if (e instanceof MessageStateChangedEvent) {
|
||||
MessageStateChangedEvent event = (MessageStateChangedEvent) e;
|
||||
if (!event.isLocal()) {
|
||||
if (event.getState() == DELIVERED) {
|
||||
LOG.info("Delivered new message");
|
||||
deliveryWaiter.resume();
|
||||
} else if (event.getState() == INVALID ||
|
||||
event.getState() == PENDING) {
|
||||
LOG.info("Validated new " + event.getState().name() +
|
||||
" message");
|
||||
validationWaiter.resume();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void defaultInit() throws Exception {
|
||||
addDefaultIdentities();
|
||||
addDefaultContacts();
|
||||
listenToEvents();
|
||||
addGroup();
|
||||
}
|
||||
|
||||
private void addDefaultIdentities() throws DbException {
|
||||
KeyPair keyPair0 = crypto.generateSignatureKeyPair();
|
||||
byte[] publicKey0 = keyPair0.getPublic().getEncoded();
|
||||
byte[] privateKey0 = keyPair0.getPrivate().getEncoded();
|
||||
author0 = authorFactory
|
||||
.createLocalAuthor(AUTHOR1, publicKey0, privateKey0);
|
||||
identityManager0.addLocalAuthor(author0);
|
||||
privateGroup0 =
|
||||
privateGroupFactory.createPrivateGroup("Testgroup", author0);
|
||||
groupId0 = privateGroup0.getId();
|
||||
|
||||
KeyPair keyPair1 = crypto.generateSignatureKeyPair();
|
||||
byte[] publicKey1 = keyPair1.getPublic().getEncoded();
|
||||
byte[] privateKey1 = keyPair1.getPrivate().getEncoded();
|
||||
author1 = authorFactory
|
||||
.createLocalAuthor(AUTHOR2, publicKey1, privateKey1);
|
||||
identityManager1.addLocalAuthor(author1);
|
||||
}
|
||||
|
||||
private void addDefaultContacts() throws DbException {
|
||||
// sharer adds invitee as contact
|
||||
contactId1 = contactManager0.addContact(author1,
|
||||
author0.getId(), master, clock.currentTimeMillis(), true,
|
||||
true, true
|
||||
);
|
||||
// invitee adds sharer back
|
||||
contactId0 = contactManager1.addContact(author0,
|
||||
author1.getId(), master, clock.currentTimeMillis(), true,
|
||||
true, true
|
||||
);
|
||||
}
|
||||
|
||||
private void listenToEvents() {
|
||||
Listener listener0 = new Listener();
|
||||
t0.getEventBus().addListener(listener0);
|
||||
Listener listener1 = new Listener();
|
||||
t1.getEventBus().addListener(listener1);
|
||||
}
|
||||
|
||||
private void addGroup() throws Exception {
|
||||
// author0 joins privateGroup0
|
||||
long joinTime = clock.currentTimeMillis();
|
||||
newMemberMsg0 = groupMessageFactory
|
||||
.createNewMemberMessage(privateGroup0.getId(), joinTime,
|
||||
author0, author0);
|
||||
GroupMessage joinMsg = groupMessageFactory
|
||||
.createJoinMessage(privateGroup0.getId(), joinTime, author0,
|
||||
newMemberMsg0.getMessage().getId());
|
||||
groupManager0.addPrivateGroup(privateGroup0, newMemberMsg0, joinMsg);
|
||||
assertEquals(joinMsg.getMessage().getId(),
|
||||
groupManager0.getPreviousMsgId(groupId0));
|
||||
|
||||
// make group visible to 1
|
||||
Transaction txn0 = t0.getDatabaseComponent().startTransaction(false);
|
||||
t0.getDatabaseComponent()
|
||||
.setVisibleToContact(txn0, contactId1, privateGroup0.getId(),
|
||||
true);
|
||||
txn0.setComplete();
|
||||
t0.getDatabaseComponent().endTransaction(txn0);
|
||||
|
||||
// author1 joins privateGroup0
|
||||
joinTime = clock.currentTimeMillis();
|
||||
GroupMessage newMemberMsg1 = groupMessageFactory
|
||||
.createNewMemberMessage(privateGroup0.getId(), joinTime,
|
||||
author0, author1);
|
||||
joinMsg = groupMessageFactory
|
||||
.createJoinMessage(privateGroup0.getId(), joinTime, author1,
|
||||
newMemberMsg1.getMessage().getId());
|
||||
groupManager1.addPrivateGroup(privateGroup0, newMemberMsg1, joinMsg);
|
||||
assertEquals(joinMsg.getMessage().getId(),
|
||||
groupManager1.getPreviousMsgId(groupId0));
|
||||
|
||||
// make group visible to 0
|
||||
Transaction txn1 = t1.getDatabaseComponent().startTransaction(false);
|
||||
t1.getDatabaseComponent()
|
||||
.setVisibleToContact(txn1, contactId0, privateGroup0.getId(),
|
||||
true);
|
||||
txn1.setComplete();
|
||||
t1.getDatabaseComponent().endTransaction(txn1);
|
||||
|
||||
// sync join messages
|
||||
sync0To1();
|
||||
deliveryWaiter.await(TIMEOUT, 2);
|
||||
sync1To0();
|
||||
deliveryWaiter.await(TIMEOUT, 2);
|
||||
}
|
||||
|
||||
private void sync0To1() throws IOException, TimeoutException {
|
||||
deliverMessage(sync0, contactId0, sync1, contactId1, "0 to 1");
|
||||
}
|
||||
|
||||
private void sync1To0() throws IOException, TimeoutException {
|
||||
deliverMessage(sync1, contactId1, sync0, contactId0, "1 to 0");
|
||||
}
|
||||
|
||||
private void deliverMessage(SyncSessionFactory fromSync, ContactId fromId,
|
||||
SyncSessionFactory toSync, ContactId toId, String debug)
|
||||
throws IOException, TimeoutException {
|
||||
|
||||
if (debug != null) LOG.info("TEST: Sending message from " + debug);
|
||||
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
// Create an outgoing sync session
|
||||
SyncSession sessionFrom =
|
||||
fromSync.createSimplexOutgoingSession(toId, MAX_LATENCY, out);
|
||||
// Write whatever needs to be written
|
||||
sessionFrom.run();
|
||||
out.close();
|
||||
|
||||
ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray());
|
||||
// Create an incoming sync session
|
||||
SyncSession sessionTo = toSync.createIncomingSession(fromId, in);
|
||||
// Read whatever needs to be read
|
||||
sessionTo.run();
|
||||
in.close();
|
||||
}
|
||||
|
||||
private void startLifecycles() throws InterruptedException {
|
||||
// Start the lifecycle manager and wait for it to finish
|
||||
lifecycleManager0 = t0.getLifecycleManager();
|
||||
lifecycleManager1 = t1.getLifecycleManager();
|
||||
lifecycleManager0.startServices();
|
||||
lifecycleManager1.startServices();
|
||||
lifecycleManager0.waitForStartup();
|
||||
lifecycleManager1.waitForStartup();
|
||||
}
|
||||
|
||||
private void stopLifecycles() throws InterruptedException {
|
||||
// Clean up
|
||||
lifecycleManager0.stopServices();
|
||||
lifecycleManager1.stopServices();
|
||||
lifecycleManager0.waitForShutdown();
|
||||
lifecycleManager1.waitForShutdown();
|
||||
}
|
||||
|
||||
private void injectEagerSingletons(
|
||||
PrivateGroupManagerTestComponent component) {
|
||||
component.inject(new LifecycleModule.EagerSingletons());
|
||||
component.inject(new PrivateGroupModule.EagerSingletons());
|
||||
component.inject(new CryptoModule.EagerSingletons());
|
||||
component.inject(new ContactModule.EagerSingletons());
|
||||
component.inject(new TransportModule.EagerSingletons());
|
||||
component.inject(new SyncModule.EagerSingletons());
|
||||
component.inject(new PropertiesModule.EagerSingletons());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package org.briarproject;
|
||||
|
||||
import org.briarproject.api.contact.ContactManager;
|
||||
import org.briarproject.api.db.DatabaseComponent;
|
||||
import org.briarproject.api.event.EventBus;
|
||||
import org.briarproject.api.identity.IdentityManager;
|
||||
import org.briarproject.api.lifecycle.LifecycleManager;
|
||||
import org.briarproject.api.privategroup.PrivateGroupManager;
|
||||
import org.briarproject.api.sync.SyncSessionFactory;
|
||||
import org.briarproject.clients.ClientsModule;
|
||||
import org.briarproject.contact.ContactModule;
|
||||
import org.briarproject.crypto.CryptoModule;
|
||||
import org.briarproject.data.DataModule;
|
||||
import org.briarproject.db.DatabaseModule;
|
||||
import org.briarproject.event.EventModule;
|
||||
import org.briarproject.identity.IdentityModule;
|
||||
import org.briarproject.lifecycle.LifecycleModule;
|
||||
import org.briarproject.messaging.MessagingModule;
|
||||
import org.briarproject.privategroup.PrivateGroupModule;
|
||||
import org.briarproject.properties.PropertiesModule;
|
||||
import org.briarproject.sharing.SharingModule;
|
||||
import org.briarproject.sync.SyncModule;
|
||||
import org.briarproject.system.SystemModule;
|
||||
import org.briarproject.transport.TransportModule;
|
||||
|
||||
import javax.inject.Singleton;
|
||||
|
||||
import dagger.Component;
|
||||
|
||||
@Singleton
|
||||
@Component(modules = {
|
||||
TestDatabaseModule.class,
|
||||
TestPluginsModule.class,
|
||||
TestSeedProviderModule.class,
|
||||
ClientsModule.class,
|
||||
ContactModule.class,
|
||||
CryptoModule.class,
|
||||
DataModule.class,
|
||||
DatabaseModule.class,
|
||||
EventModule.class,
|
||||
MessagingModule.class,
|
||||
PrivateGroupModule.class,
|
||||
IdentityModule.class,
|
||||
LifecycleModule.class,
|
||||
PropertiesModule.class,
|
||||
SharingModule.class,
|
||||
SyncModule.class,
|
||||
SystemModule.class,
|
||||
TransportModule.class
|
||||
})
|
||||
interface PrivateGroupManagerTestComponent {
|
||||
|
||||
void inject(PrivateGroupManagerTest testCase);
|
||||
|
||||
void inject(ContactModule.EagerSingletons init);
|
||||
|
||||
void inject(CryptoModule.EagerSingletons init);
|
||||
|
||||
void inject(PrivateGroupModule.EagerSingletons init);
|
||||
|
||||
void inject(LifecycleModule.EagerSingletons init);
|
||||
|
||||
void inject(PropertiesModule.EagerSingletons init);
|
||||
|
||||
void inject(SyncModule.EagerSingletons init);
|
||||
|
||||
void inject(TransportModule.EagerSingletons init);
|
||||
|
||||
LifecycleManager getLifecycleManager();
|
||||
|
||||
EventBus getEventBus();
|
||||
|
||||
IdentityManager getIdentityManager();
|
||||
|
||||
ContactManager getContactManager();
|
||||
|
||||
PrivateGroupManager getPrivateGroupManager();
|
||||
|
||||
SyncSessionFactory getSyncSessionFactory();
|
||||
|
||||
DatabaseComponent getDatabaseComponent();
|
||||
|
||||
}
|
||||
@@ -1,12 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout
|
||||
android:id="@+id/layout"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/forum_cell"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal">
|
||||
android:orientation="horizontal"
|
||||
android:baselineAligned="false">
|
||||
|
||||
<RelativeLayout
|
||||
android:layout_width="wrap_content"
|
||||
48
briar-android/res/layout/list_item_thread_notice.xml
Normal file
48
briar-android/res/layout/list_item_thread_notice.xml
Normal file
@@ -0,0 +1,48 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout
|
||||
android:id="@+id/layout"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginLeft="@dimen/margin_medium"
|
||||
android:baselineAligned="false"
|
||||
android:orientation="vertical">
|
||||
|
||||
<View
|
||||
android:id="@+id/top_divider"
|
||||
style="@style/Divider.ForumList"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/margin_separator"/>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="@dimen/margin_small"
|
||||
android:layout_marginLeft="@dimen/margin_medium"
|
||||
android:layout_marginRight="@dimen/margin_medium"
|
||||
android:layout_marginTop="@dimen/margin_medium"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<org.briarproject.android.view.AuthorView
|
||||
android:id="@+id/author"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:persona="commenter"/>
|
||||
|
||||
<org.thoughtcrime.securesms.components.emoji.EmojiTextView
|
||||
android:id="@+id/text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginLeft="@dimen/margin_medium"
|
||||
android:gravity="center_vertical"
|
||||
android:textColor="@color/briar_text_secondary"
|
||||
android:textIsSelectable="true"
|
||||
android:textSize="@dimen/text_size_medium"
|
||||
android:textStyle="italic"
|
||||
tools:text="@string/groups_member_joined"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
@@ -166,6 +166,7 @@
|
||||
<string name="groups_invite_members">Invite Members</string>
|
||||
<string name="groups_leave">Leave Group</string>
|
||||
<string name="groups_dissolve">Dissolve Group</string>
|
||||
<string name="groups_member_joined">joined the group.</string>
|
||||
|
||||
<!-- Private Group Invitations -->
|
||||
<string name="groups_invitations_title">Group Invitations</string>
|
||||
|
||||
@@ -120,6 +120,7 @@ public class ActivityModule {
|
||||
@Provides
|
||||
protected GroupController provideGroupController(
|
||||
GroupControllerImpl groupController) {
|
||||
activity.addLifecycleController(groupController);
|
||||
return groupController;
|
||||
}
|
||||
|
||||
|
||||
@@ -34,6 +34,8 @@ import org.briarproject.api.messaging.MessagingManager;
|
||||
import org.briarproject.api.messaging.PrivateMessageFactory;
|
||||
import org.briarproject.api.plugins.ConnectionRegistry;
|
||||
import org.briarproject.api.plugins.PluginManager;
|
||||
import org.briarproject.api.privategroup.GroupMessageFactory;
|
||||
import org.briarproject.api.privategroup.PrivateGroupFactory;
|
||||
import org.briarproject.api.privategroup.PrivateGroupManager;
|
||||
import org.briarproject.api.privategroup.invitation.GroupInvitationManager;
|
||||
import org.briarproject.api.settings.SettingsManager;
|
||||
@@ -99,6 +101,10 @@ public interface AndroidComponent extends CoreEagerSingletons {
|
||||
|
||||
GroupInvitationManager groupInvitationManager();
|
||||
|
||||
PrivateGroupFactory privateGroupFactory();
|
||||
|
||||
GroupMessageFactory groupMessageFactory();
|
||||
|
||||
ForumManager forumManager();
|
||||
|
||||
ForumSharingManager forumSharingManager();
|
||||
|
||||
@@ -18,8 +18,9 @@ import android.widget.Toast;
|
||||
import org.briarproject.R;
|
||||
import org.briarproject.android.ActivityComponent;
|
||||
import org.briarproject.android.controller.handler.UiResultExceptionHandler;
|
||||
import org.briarproject.android.sharing.ShareForumActivity;
|
||||
import org.briarproject.android.sharing.ForumSharingStatusActivity;
|
||||
import org.briarproject.android.sharing.ShareForumActivity;
|
||||
import org.briarproject.android.threaded.ThreadItemAdapter;
|
||||
import org.briarproject.android.threaded.ThreadListActivity;
|
||||
import org.briarproject.android.threaded.ThreadListController;
|
||||
import org.briarproject.api.db.DbException;
|
||||
@@ -35,7 +36,7 @@ import static android.widget.Toast.LENGTH_SHORT;
|
||||
import static org.briarproject.api.forum.ForumConstants.MAX_FORUM_POST_BODY_LENGTH;
|
||||
|
||||
public class ForumActivity extends
|
||||
ThreadListActivity<Forum, ForumItem, ForumPostHeader, NestedForumAdapter> {
|
||||
ThreadListActivity<Forum, ForumItem, ForumPostHeader> {
|
||||
|
||||
private static final int REQUEST_FORUM_SHARED = 3;
|
||||
|
||||
@@ -74,9 +75,9 @@ public class ForumActivity extends
|
||||
}
|
||||
|
||||
@Override
|
||||
protected NestedForumAdapter createAdapter(
|
||||
protected ThreadItemAdapter<ForumItem> createAdapter(
|
||||
LinearLayoutManager layoutManager) {
|
||||
return new NestedForumAdapter(this, layoutManager);
|
||||
return new ThreadItemAdapter<>(this, layoutManager);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -3,6 +3,7 @@ package org.briarproject.android.forum;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import org.briarproject.android.api.AndroidNotificationManager;
|
||||
import org.briarproject.android.controller.handler.ResultExceptionHandler;
|
||||
import org.briarproject.android.threaded.ThreadListControllerImpl;
|
||||
import org.briarproject.api.clients.MessageTracker.GroupCount;
|
||||
import org.briarproject.api.crypto.CryptoExecutor;
|
||||
@@ -28,8 +29,11 @@ import java.util.logging.Logger;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
public class ForumControllerImpl extends
|
||||
ThreadListControllerImpl<Forum, ForumItem, ForumPostHeader, ForumPost>
|
||||
import static java.lang.Math.max;
|
||||
import static java.util.logging.Level.WARNING;
|
||||
|
||||
public class ForumControllerImpl
|
||||
extends ThreadListControllerImpl<Forum, ForumItem, ForumPostHeader, ForumPost>
|
||||
implements ForumController {
|
||||
|
||||
private static final Logger LOG =
|
||||
@@ -42,9 +46,9 @@ public class ForumControllerImpl extends
|
||||
LifecycleManager lifecycleManager, IdentityManager identityManager,
|
||||
@CryptoExecutor Executor cryptoExecutor,
|
||||
ForumManager forumManager, EventBus eventBus,
|
||||
AndroidNotificationManager notificationManager, Clock clock) {
|
||||
Clock clock, AndroidNotificationManager notificationManager) {
|
||||
super(dbExecutor, lifecycleManager, identityManager, cryptoExecutor,
|
||||
eventBus, notificationManager, clock);
|
||||
eventBus, clock, notificationManager);
|
||||
this.forumManager = forumManager;
|
||||
}
|
||||
|
||||
@@ -84,8 +88,8 @@ public class ForumControllerImpl extends
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String loadMessageBody(MessageId id) throws DbException {
|
||||
return StringUtils.fromUtf8(forumManager.getPostBody(id));
|
||||
protected String loadMessageBody(ForumPostHeader h) throws DbException {
|
||||
return StringUtils.fromUtf8(forumManager.getPostBody(h.getId()));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -94,16 +98,42 @@ public class ForumControllerImpl extends
|
||||
}
|
||||
|
||||
@Override
|
||||
protected long getLatestTimestamp() throws DbException {
|
||||
GroupCount count = forumManager.getGroupCount(getGroupId());
|
||||
return count.getLatestMsgTime();
|
||||
public void createAndStoreMessage(final String body,
|
||||
@Nullable final ForumItem parentItem,
|
||||
final ResultExceptionHandler<ForumItem, DbException> handler) {
|
||||
runOnDbThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
LocalAuthor author = identityManager.getLocalAuthor();
|
||||
GroupCount count = forumManager.getGroupCount(getGroupId());
|
||||
long timestamp = max(count.getLatestMsgTime() + 1,
|
||||
clock.currentTimeMillis());
|
||||
MessageId parentId = parentItem != null ?
|
||||
parentItem.getId() : null;
|
||||
createMessage(body, timestamp, parentId, author, handler);
|
||||
} catch (DbException e) {
|
||||
if (LOG.isLoggable(WARNING))
|
||||
LOG.log(WARNING, e.toString(), e);
|
||||
handler.onException(e);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ForumPost createLocalMessage(String body, long timestamp,
|
||||
@Nullable MessageId parentId, LocalAuthor author) {
|
||||
return forumManager.createLocalPost(getGroupId(), body, timestamp,
|
||||
parentId, author);
|
||||
private void createMessage(final String body, final long timestamp,
|
||||
final @Nullable MessageId parentId, final LocalAuthor author,
|
||||
final ResultExceptionHandler<ForumItem, DbException> handler) {
|
||||
cryptoExecutor.execute(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
LOG.info("Creating forum post...");
|
||||
ForumPost msg = forumManager
|
||||
.createLocalPost(getGroupId(), body, timestamp,
|
||||
parentId, author);
|
||||
storePost(msg, body, handler);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
package org.briarproject.android.forum;
|
||||
|
||||
import android.support.annotation.UiThread;
|
||||
import android.support.v7.widget.LinearLayoutManager;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import org.briarproject.R;
|
||||
import org.briarproject.android.threaded.ThreadItemAdapter;
|
||||
|
||||
@UiThread
|
||||
class NestedForumAdapter extends ThreadItemAdapter<ForumItem> {
|
||||
|
||||
NestedForumAdapter(ThreadItemListener<ForumItem> listener,
|
||||
LinearLayoutManager layoutManager) {
|
||||
super(listener, layoutManager);
|
||||
}
|
||||
|
||||
@Override
|
||||
public NestedForumHolder onCreateViewHolder(ViewGroup parent,
|
||||
int viewType) {
|
||||
View v = LayoutInflater.from(parent.getContext())
|
||||
.inflate(R.layout.list_item_forum_post, parent, false);
|
||||
return new NestedForumHolder(v);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
package org.briarproject.android.forum;
|
||||
|
||||
import android.view.View;
|
||||
|
||||
import org.briarproject.android.threaded.ThreadItemViewHolder;
|
||||
|
||||
public class NestedForumHolder extends ThreadItemViewHolder<ForumItem> {
|
||||
|
||||
public NestedForumHolder(View v) {
|
||||
super(v);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -22,7 +22,7 @@ import javax.inject.Inject;
|
||||
import static org.briarproject.api.privategroup.PrivateGroupConstants.MAX_GROUP_POST_BODY_LENGTH;
|
||||
|
||||
public class GroupActivity extends
|
||||
ThreadListActivity<PrivateGroup, GroupMessageItem, GroupMessageHeader, GroupMessageAdapter> {
|
||||
ThreadListActivity<PrivateGroup, GroupMessageItem, GroupMessageHeader> {
|
||||
|
||||
@Inject
|
||||
GroupController controller;
|
||||
|
||||
@@ -3,8 +3,8 @@ package org.briarproject.android.privategroup.conversation;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import org.briarproject.android.api.AndroidNotificationManager;
|
||||
import org.briarproject.android.controller.handler.ResultExceptionHandler;
|
||||
import org.briarproject.android.threaded.ThreadListControllerImpl;
|
||||
import org.briarproject.api.clients.MessageTracker.GroupCount;
|
||||
import org.briarproject.api.crypto.CryptoExecutor;
|
||||
import org.briarproject.api.db.DatabaseExecutor;
|
||||
import org.briarproject.api.db.DbException;
|
||||
@@ -15,7 +15,9 @@ import org.briarproject.api.identity.IdentityManager;
|
||||
import org.briarproject.api.identity.LocalAuthor;
|
||||
import org.briarproject.api.lifecycle.LifecycleManager;
|
||||
import org.briarproject.api.privategroup.GroupMessage;
|
||||
import org.briarproject.api.privategroup.GroupMessageFactory;
|
||||
import org.briarproject.api.privategroup.GroupMessageHeader;
|
||||
import org.briarproject.api.privategroup.JoinMessageHeader;
|
||||
import org.briarproject.api.privategroup.PrivateGroup;
|
||||
import org.briarproject.api.privategroup.PrivateGroupManager;
|
||||
import org.briarproject.api.sync.MessageId;
|
||||
@@ -27,6 +29,9 @@ import java.util.logging.Logger;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import static java.lang.Math.max;
|
||||
import static java.util.logging.Level.WARNING;
|
||||
|
||||
public class GroupControllerImpl extends
|
||||
ThreadListControllerImpl<PrivateGroup, GroupMessageItem, GroupMessageHeader, GroupMessage>
|
||||
implements GroupController {
|
||||
@@ -35,16 +40,19 @@ public class GroupControllerImpl extends
|
||||
Logger.getLogger(GroupControllerImpl.class.getName());
|
||||
|
||||
private final PrivateGroupManager privateGroupManager;
|
||||
private final GroupMessageFactory groupMessageFactory;
|
||||
|
||||
@Inject
|
||||
GroupControllerImpl(@DatabaseExecutor Executor dbExecutor,
|
||||
LifecycleManager lifecycleManager, IdentityManager identityManager,
|
||||
@CryptoExecutor Executor cryptoExecutor,
|
||||
PrivateGroupManager privateGroupManager, EventBus eventBus,
|
||||
AndroidNotificationManager notificationManager, Clock clock) {
|
||||
PrivateGroupManager privateGroupManager,
|
||||
GroupMessageFactory groupMessageFactory, EventBus eventBus,
|
||||
Clock clock, AndroidNotificationManager notificationManager) {
|
||||
super(dbExecutor, lifecycleManager, identityManager, cryptoExecutor,
|
||||
eventBus, notificationManager, clock);
|
||||
eventBus, clock, notificationManager);
|
||||
this.privateGroupManager = privateGroupManager;
|
||||
this.groupMessageFactory = groupMessageFactory;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -83,8 +91,13 @@ public class GroupControllerImpl extends
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String loadMessageBody(MessageId id) throws DbException {
|
||||
return privateGroupManager.getMessageBody(id);
|
||||
protected String loadMessageBody(GroupMessageHeader header)
|
||||
throws DbException {
|
||||
if (header instanceof JoinMessageHeader) {
|
||||
// will be looked up later
|
||||
return "";
|
||||
}
|
||||
return privateGroupManager.getMessageBody(header.getId());
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -93,16 +106,52 @@ public class GroupControllerImpl extends
|
||||
}
|
||||
|
||||
@Override
|
||||
protected long getLatestTimestamp() throws DbException {
|
||||
GroupCount count = privateGroupManager.getGroupCount(getGroupId());
|
||||
return count.getLatestMsgTime();
|
||||
public void createAndStoreMessage(final String body,
|
||||
@Nullable final GroupMessageItem parentItem,
|
||||
final ResultExceptionHandler<GroupMessageItem, DbException> handler) {
|
||||
runOnDbThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
LocalAuthor author = identityManager.getLocalAuthor();
|
||||
MessageId parentId = null;
|
||||
MessageId previousMsgId =
|
||||
privateGroupManager.getPreviousMsgId(getGroupId());
|
||||
// timestamp must be greater than the timestamps
|
||||
// of the member's previous message...
|
||||
long timestamp = privateGroupManager
|
||||
.getMessageTimestamp(previousMsgId);
|
||||
// ...and the parent post, if any
|
||||
if (parentItem != null) {
|
||||
timestamp = max(parentItem.getTimestamp(), timestamp);
|
||||
parentId = parentItem.getId();
|
||||
}
|
||||
timestamp = max(clock.currentTimeMillis(), timestamp + 1);
|
||||
createMessage(body, timestamp, parentId, author,
|
||||
previousMsgId, handler);
|
||||
} catch (DbException e) {
|
||||
if (LOG.isLoggable(WARNING))
|
||||
LOG.log(WARNING, e.toString(), e);
|
||||
handler.onException(e);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected GroupMessage createLocalMessage(String body, long timestamp,
|
||||
@Nullable MessageId parentId, LocalAuthor author) {
|
||||
return privateGroupManager.createLocalMessage(getGroupId(), body,
|
||||
timestamp, parentId, author);
|
||||
private void createMessage(final String body, final long timestamp,
|
||||
final @Nullable MessageId parentId, final LocalAuthor author,
|
||||
final MessageId previousMsgId,
|
||||
final ResultExceptionHandler<GroupMessageItem, DbException> handler) {
|
||||
cryptoExecutor.execute(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
LOG.info("Creating group message...");
|
||||
GroupMessage msg = groupMessageFactory
|
||||
.createGroupMessage(getGroupId(), timestamp,
|
||||
parentId, author, body, previousMsgId);
|
||||
storePost(msg, body, handler);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -119,6 +168,9 @@ public class GroupControllerImpl extends
|
||||
@Override
|
||||
protected GroupMessageItem buildItem(GroupMessageHeader header,
|
||||
String body) {
|
||||
if (header instanceof JoinMessageHeader) {
|
||||
return new JoinMessageItem(header, body);
|
||||
}
|
||||
return new GroupMessageItem(header, body);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package org.briarproject.android.privategroup.conversation;
|
||||
|
||||
import android.support.annotation.LayoutRes;
|
||||
import android.support.annotation.UiThread;
|
||||
import android.support.v7.widget.LinearLayoutManager;
|
||||
import android.view.LayoutInflater;
|
||||
@@ -7,7 +8,9 @@ import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import org.briarproject.R;
|
||||
import org.briarproject.android.threaded.BaseThreadItemViewHolder;
|
||||
import org.briarproject.android.threaded.ThreadItemAdapter;
|
||||
import org.briarproject.android.threaded.ThreadPostViewHolder;
|
||||
|
||||
@UiThread
|
||||
public class GroupMessageAdapter extends ThreadItemAdapter<GroupMessageItem> {
|
||||
@@ -17,12 +20,23 @@ public class GroupMessageAdapter extends ThreadItemAdapter<GroupMessageItem> {
|
||||
super(listener, layoutManager);
|
||||
}
|
||||
|
||||
@LayoutRes
|
||||
@Override
|
||||
public GroupMessageViewHolder onCreateViewHolder(ViewGroup parent,
|
||||
int viewType) {
|
||||
public int getItemViewType(int position) {
|
||||
GroupMessageItem item = getVisibleItem(position);
|
||||
if (item != null) return item.getLayout();
|
||||
return R.layout.list_item_thread;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BaseThreadItemViewHolder<GroupMessageItem> onCreateViewHolder(
|
||||
ViewGroup parent, int type) {
|
||||
View v = LayoutInflater.from(parent.getContext())
|
||||
.inflate(R.layout.list_item_forum_post, parent, false);
|
||||
return new GroupMessageViewHolder(v);
|
||||
.inflate(type, parent, false);
|
||||
if (type == R.layout.list_item_thread_notice) {
|
||||
return new JoinMessageItemViewHolder(v);
|
||||
}
|
||||
return new ThreadPostViewHolder<>(v);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,14 +1,22 @@
|
||||
package org.briarproject.android.privategroup.conversation;
|
||||
|
||||
import android.support.annotation.LayoutRes;
|
||||
import android.support.annotation.UiThread;
|
||||
|
||||
import org.briarproject.R;
|
||||
import org.briarproject.android.threaded.ThreadItem;
|
||||
import org.briarproject.api.identity.Author;
|
||||
import org.briarproject.api.identity.Author.Status;
|
||||
import org.briarproject.api.privategroup.GroupMessageHeader;
|
||||
import org.briarproject.api.sync.MessageId;
|
||||
|
||||
import javax.annotation.concurrent.NotThreadSafe;
|
||||
|
||||
@UiThread
|
||||
@NotThreadSafe
|
||||
class GroupMessageItem extends ThreadItem {
|
||||
|
||||
GroupMessageItem(MessageId messageId, MessageId parentId,
|
||||
private GroupMessageItem(MessageId messageId, MessageId parentId,
|
||||
String text, long timestamp, Author author, Status status,
|
||||
boolean isRead) {
|
||||
super(messageId, parentId, text, timestamp, author, status, isRead);
|
||||
@@ -19,4 +27,9 @@ class GroupMessageItem extends ThreadItem {
|
||||
h.getAuthorStatus(), h.isRead());
|
||||
}
|
||||
|
||||
@LayoutRes
|
||||
public int getLayout() {
|
||||
return R.layout.list_item_thread;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
package org.briarproject.android.privategroup.conversation;
|
||||
|
||||
import android.view.View;
|
||||
|
||||
import org.briarproject.android.threaded.ThreadItemViewHolder;
|
||||
|
||||
public class GroupMessageViewHolder
|
||||
extends ThreadItemViewHolder<GroupMessageItem> {
|
||||
|
||||
public GroupMessageViewHolder(View v) {
|
||||
super(v);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package org.briarproject.android.privategroup.conversation;
|
||||
|
||||
import android.support.annotation.LayoutRes;
|
||||
import android.support.annotation.UiThread;
|
||||
|
||||
import org.briarproject.R;
|
||||
import org.briarproject.api.privategroup.GroupMessageHeader;
|
||||
|
||||
import javax.annotation.concurrent.NotThreadSafe;
|
||||
|
||||
@UiThread
|
||||
@NotThreadSafe
|
||||
class JoinMessageItem extends GroupMessageItem {
|
||||
|
||||
JoinMessageItem(GroupMessageHeader h,
|
||||
String text) {
|
||||
super(h, text);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getLevel() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasDescendants() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@LayoutRes
|
||||
public int getLayout() {
|
||||
return R.layout.list_item_thread_notice;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package org.briarproject.android.privategroup.conversation;
|
||||
|
||||
import android.support.annotation.UiThread;
|
||||
import android.view.View;
|
||||
|
||||
import org.briarproject.R;
|
||||
import org.briarproject.android.threaded.BaseThreadItemViewHolder;
|
||||
import org.briarproject.android.threaded.ThreadItemAdapter;
|
||||
import org.briarproject.android.threaded.ThreadItemAdapter.ThreadItemListener;
|
||||
import org.briarproject.api.nullsafety.NotNullByDefault;
|
||||
|
||||
@UiThread
|
||||
@NotNullByDefault
|
||||
public class JoinMessageItemViewHolder
|
||||
extends BaseThreadItemViewHolder<GroupMessageItem> {
|
||||
|
||||
public JoinMessageItemViewHolder(View v) {
|
||||
super(v);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void bind(final ThreadItemAdapter<GroupMessageItem> adapter,
|
||||
final ThreadItemListener<GroupMessageItem> listener,
|
||||
final GroupMessageItem item, int pos) {
|
||||
super.bind(adapter, listener, item, pos);
|
||||
|
||||
textView.setText(getContext().getString(R.string.groups_member_joined));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -3,11 +3,19 @@ package org.briarproject.android.privategroup.creation;
|
||||
import org.briarproject.android.controller.DbControllerImpl;
|
||||
import org.briarproject.android.controller.handler.ResultExceptionHandler;
|
||||
import org.briarproject.api.contact.ContactId;
|
||||
import org.briarproject.api.crypto.CryptoExecutor;
|
||||
import org.briarproject.api.db.DatabaseExecutor;
|
||||
import org.briarproject.api.db.DbException;
|
||||
import org.briarproject.api.identity.IdentityManager;
|
||||
import org.briarproject.api.identity.LocalAuthor;
|
||||
import org.briarproject.api.lifecycle.LifecycleManager;
|
||||
import org.briarproject.api.privategroup.GroupMessage;
|
||||
import org.briarproject.api.privategroup.GroupMessageFactory;
|
||||
import org.briarproject.api.privategroup.PrivateGroup;
|
||||
import org.briarproject.api.privategroup.PrivateGroupFactory;
|
||||
import org.briarproject.api.privategroup.PrivateGroupManager;
|
||||
import org.briarproject.api.sync.GroupId;
|
||||
import org.briarproject.api.system.Clock;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.concurrent.Executor;
|
||||
@@ -23,25 +31,81 @@ public class CreateGroupControllerImpl extends DbControllerImpl
|
||||
private static final Logger LOG =
|
||||
Logger.getLogger(CreateGroupControllerImpl.class.getName());
|
||||
|
||||
private final IdentityManager identityManager;
|
||||
private final PrivateGroupFactory groupFactory;
|
||||
private final GroupMessageFactory groupMessageFactory;
|
||||
private final PrivateGroupManager groupManager;
|
||||
private final Clock clock;
|
||||
@CryptoExecutor
|
||||
private final Executor cryptoExecutor;
|
||||
|
||||
@Inject
|
||||
CreateGroupControllerImpl(@DatabaseExecutor Executor dbExecutor,
|
||||
LifecycleManager lifecycleManager,
|
||||
PrivateGroupManager groupManager) {
|
||||
@CryptoExecutor Executor cryptoExecutor,
|
||||
LifecycleManager lifecycleManager, IdentityManager identityManager,
|
||||
PrivateGroupFactory groupFactory,
|
||||
GroupMessageFactory groupMessageFactory,
|
||||
PrivateGroupManager groupManager, Clock clock) {
|
||||
super(dbExecutor, lifecycleManager);
|
||||
this.identityManager = identityManager;
|
||||
this.groupFactory = groupFactory;
|
||||
this.groupMessageFactory = groupMessageFactory;
|
||||
this.groupManager = groupManager;
|
||||
this.clock = clock;
|
||||
this.cryptoExecutor = cryptoExecutor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void createGroup(final String name,
|
||||
final ResultExceptionHandler<GroupId, DbException> handler) {
|
||||
runOnDbThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
LocalAuthor author = identityManager.getLocalAuthor();
|
||||
createGroupAndMessages(author, name, handler);
|
||||
} catch (DbException e) {
|
||||
if (LOG.isLoggable(WARNING))
|
||||
LOG.log(WARNING, e.toString(), e);
|
||||
handler.onException(e);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void createGroupAndMessages(final LocalAuthor author,
|
||||
final String name,
|
||||
final ResultExceptionHandler<GroupId, DbException> handler) {
|
||||
cryptoExecutor.execute(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
LOG.info("Creating group...");
|
||||
PrivateGroup group =
|
||||
groupFactory.createPrivateGroup(name, author);
|
||||
LOG.info("Creating new member announcement...");
|
||||
GroupMessage newMemberMsg = groupMessageFactory
|
||||
.createNewMemberMessage(group.getId(),
|
||||
clock.currentTimeMillis(), author, author);
|
||||
LOG.info("Creating new join announcement...");
|
||||
GroupMessage joinMsg = groupMessageFactory
|
||||
.createJoinMessage(group.getId(),
|
||||
newMemberMsg.getMessage().getTimestamp(),
|
||||
author, newMemberMsg.getMessage().getId());
|
||||
storeGroup(group, newMemberMsg, joinMsg, handler);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void storeGroup(final PrivateGroup group,
|
||||
final GroupMessage newMemberMsg, final GroupMessage joinMsg,
|
||||
final ResultExceptionHandler<GroupId, DbException> handler) {
|
||||
runOnDbThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
LOG.info("Adding group to database...");
|
||||
try {
|
||||
handler.onResult(groupManager.addPrivateGroup(name));
|
||||
groupManager.addPrivateGroup(group, newMemberMsg, joinMsg);
|
||||
handler.onResult(group.getId());
|
||||
} catch (DbException e) {
|
||||
if (LOG.isLoggable(WARNING))
|
||||
LOG.log(WARNING, e.toString(), e);
|
||||
|
||||
@@ -5,6 +5,7 @@ import android.animation.ArgbEvaluator;
|
||||
import android.animation.ValueAnimator;
|
||||
import android.content.Context;
|
||||
import android.graphics.drawable.ColorDrawable;
|
||||
import android.support.annotation.CallSuper;
|
||||
import android.support.annotation.UiThread;
|
||||
import android.support.v4.content.ContextCompat;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
@@ -15,47 +16,32 @@ import android.widget.TextView;
|
||||
import org.briarproject.R;
|
||||
import org.briarproject.android.threaded.ThreadItemAdapter.ThreadItemListener;
|
||||
import org.briarproject.android.view.AuthorView;
|
||||
import org.briarproject.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.util.StringUtils;
|
||||
|
||||
import static android.view.View.GONE;
|
||||
import static android.view.View.INVISIBLE;
|
||||
import static android.view.View.VISIBLE;
|
||||
|
||||
@UiThread
|
||||
public abstract class ThreadItemViewHolder<I extends ThreadItem>
|
||||
@NotNullByDefault
|
||||
public abstract class BaseThreadItemViewHolder<I extends ThreadItem>
|
||||
extends RecyclerView.ViewHolder {
|
||||
|
||||
private final static int ANIMATION_DURATION = 5000;
|
||||
|
||||
private final TextView textView, lvlText, repliesText;
|
||||
protected final TextView textView;
|
||||
private final ViewGroup layout;
|
||||
private final AuthorView author;
|
||||
private final View[] lvls;
|
||||
private final View chevron, replyButton;
|
||||
private final ViewGroup cell;
|
||||
private final View topDivider;
|
||||
|
||||
public ThreadItemViewHolder(View v) {
|
||||
public BaseThreadItemViewHolder(View v) {
|
||||
super(v);
|
||||
|
||||
layout = (ViewGroup) v.findViewById(R.id.layout);
|
||||
textView = (TextView) v.findViewById(R.id.text);
|
||||
lvlText = (TextView) v.findViewById(R.id.nested_line_text);
|
||||
author = (AuthorView) v.findViewById(R.id.author);
|
||||
repliesText = (TextView) v.findViewById(R.id.replies);
|
||||
int[] nestedLineIds = {
|
||||
R.id.nested_line_1, R.id.nested_line_2, R.id.nested_line_3,
|
||||
R.id.nested_line_4, R.id.nested_line_5
|
||||
};
|
||||
lvls = new View[nestedLineIds.length];
|
||||
for (int i = 0; i < lvls.length; i++) {
|
||||
lvls[i] = v.findViewById(nestedLineIds[i]);
|
||||
}
|
||||
chevron = v.findViewById(R.id.chevron);
|
||||
replyButton = v.findViewById(R.id.btn_reply);
|
||||
cell = (ViewGroup) v.findViewById(R.id.forum_cell);
|
||||
topDivider = v.findViewById(R.id.top_divider);
|
||||
}
|
||||
|
||||
// TODO improve encapsulation, so we don't need to pass the adapter here
|
||||
@CallSuper
|
||||
public void bind(final ThreadItemAdapter<I> adapter,
|
||||
final ThreadItemListener<I> listener, final I item, int pos) {
|
||||
|
||||
@@ -67,68 +53,22 @@ public abstract class ThreadItemViewHolder<I extends ThreadItem>
|
||||
topDivider.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
for (int i = 0; i < lvls.length; i++) {
|
||||
lvls[i].setVisibility(i < item.getLevel() ? VISIBLE : GONE);
|
||||
}
|
||||
if (item.getLevel() > 5) {
|
||||
lvlText.setVisibility(VISIBLE);
|
||||
lvlText.setText("" + item.getLevel());
|
||||
} else {
|
||||
lvlText.setVisibility(GONE);
|
||||
}
|
||||
author.setAuthor(item.getAuthor());
|
||||
author.setDate(item.getTimestamp());
|
||||
author.setAuthorStatus(item.getStatus());
|
||||
|
||||
int replies = adapter.getReplyCount(item);
|
||||
if (replies == 0) {
|
||||
repliesText.setText("");
|
||||
} else {
|
||||
repliesText.setText(getContext().getResources()
|
||||
.getQuantityString(R.plurals.message_replies, replies,
|
||||
replies));
|
||||
}
|
||||
|
||||
if (item.hasDescendants()) {
|
||||
chevron.setVisibility(VISIBLE);
|
||||
if (item.isShowingDescendants()) {
|
||||
chevron.setSelected(false);
|
||||
} else {
|
||||
chevron.setSelected(true);
|
||||
}
|
||||
chevron.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
chevron.setSelected(!chevron.isSelected());
|
||||
if (chevron.isSelected()) {
|
||||
adapter.hideDescendants(item);
|
||||
} else {
|
||||
adapter.showDescendants(item);
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
chevron.setVisibility(INVISIBLE);
|
||||
}
|
||||
if (item.equals(adapter.getReplyItem())) {
|
||||
cell.setBackgroundColor(ContextCompat
|
||||
layout.setBackgroundColor(ContextCompat
|
||||
.getColor(getContext(), R.color.forum_cell_highlight));
|
||||
} else if (item.equals(adapter.getAddedItem())) {
|
||||
cell.setBackgroundColor(ContextCompat
|
||||
layout.setBackgroundColor(ContextCompat
|
||||
.getColor(getContext(), R.color.forum_cell_highlight));
|
||||
animateFadeOut(adapter, adapter.getAddedItem());
|
||||
adapter.clearAddedItem();
|
||||
} else {
|
||||
cell.setBackgroundColor(ContextCompat
|
||||
layout.setBackgroundColor(ContextCompat
|
||||
.getColor(getContext(), R.color.window_background));
|
||||
}
|
||||
replyButton.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
listener.onReplyClick(item);
|
||||
adapter.scrollTo(item);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void animateFadeOut(final ThreadItemAdapter<I> adapter,
|
||||
@@ -137,7 +77,7 @@ public abstract class ThreadItemViewHolder<I extends ThreadItem>
|
||||
setIsRecyclable(false);
|
||||
ValueAnimator anim = new ValueAnimator();
|
||||
adapter.addAnimatingItem(addedItem, anim);
|
||||
ColorDrawable viewColor = (ColorDrawable) cell.getBackground();
|
||||
ColorDrawable viewColor = (ColorDrawable) layout.getBackground();
|
||||
anim.setIntValues(viewColor.getColor(), ContextCompat
|
||||
.getColor(getContext(), R.color.window_background));
|
||||
anim.setEvaluator(new ArgbEvaluator());
|
||||
@@ -167,7 +107,7 @@ public abstract class ThreadItemViewHolder<I extends ThreadItem>
|
||||
anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
|
||||
@Override
|
||||
public void onAnimationUpdate(ValueAnimator valueAnimator) {
|
||||
cell.setBackgroundColor(
|
||||
layout.setBackgroundColor(
|
||||
(Integer) valueAnimator.getAnimatedValue());
|
||||
}
|
||||
});
|
||||
@@ -175,7 +115,7 @@ public abstract class ThreadItemViewHolder<I extends ThreadItem>
|
||||
anim.start();
|
||||
}
|
||||
|
||||
private Context getContext() {
|
||||
protected Context getContext() {
|
||||
return textView.getContext();
|
||||
}
|
||||
|
||||
@@ -5,9 +5,11 @@ import org.briarproject.api.identity.Author;
|
||||
import org.briarproject.api.identity.Author.Status;
|
||||
import org.briarproject.api.sync.MessageId;
|
||||
|
||||
import javax.annotation.concurrent.NotThreadSafe;
|
||||
|
||||
import static org.briarproject.android.threaded.ThreadItemAdapter.UNDEFINED;
|
||||
|
||||
/* This class is not thread safe */
|
||||
@NotThreadSafe
|
||||
public abstract class ThreadItem implements MessageNode {
|
||||
|
||||
private final MessageId messageId;
|
||||
@@ -92,4 +94,5 @@ public abstract class ThreadItem implements MessageNode {
|
||||
public void setDescendantCount(int descendantCount) {
|
||||
this.descendantCount = descendantCount;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -5,7 +5,11 @@ import android.support.annotation.Nullable;
|
||||
import android.support.annotation.UiThread;
|
||||
import android.support.v7.widget.LinearLayoutManager;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import org.briarproject.R;
|
||||
import org.briarproject.android.util.VersionedAdapter;
|
||||
import org.briarproject.api.sync.MessageId;
|
||||
|
||||
@@ -17,8 +21,8 @@ import java.util.Map;
|
||||
|
||||
import static android.support.v7.widget.RecyclerView.NO_POSITION;
|
||||
|
||||
public abstract class ThreadItemAdapter<I extends ThreadItem>
|
||||
extends RecyclerView.Adapter<ThreadItemViewHolder<I>>
|
||||
public class ThreadItemAdapter<I extends ThreadItem>
|
||||
extends RecyclerView.Adapter<BaseThreadItemViewHolder<I>>
|
||||
implements VersionedAdapter {
|
||||
|
||||
static final int UNDEFINED = -1;
|
||||
@@ -42,7 +46,15 @@ public abstract class ThreadItemAdapter<I extends ThreadItem>
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(ThreadItemViewHolder<I> ui, int position) {
|
||||
public BaseThreadItemViewHolder<I> onCreateViewHolder(
|
||||
ViewGroup parent, int viewType) {
|
||||
View v = LayoutInflater.from(parent.getContext())
|
||||
.inflate(R.layout.list_item_thread, parent, false);
|
||||
return new ThreadPostViewHolder<>(v);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(BaseThreadItemViewHolder<I> ui, int position) {
|
||||
I item = getVisibleItem(position);
|
||||
if (item == null) return;
|
||||
listener.onItemVisible(item);
|
||||
@@ -304,7 +316,7 @@ public abstract class ThreadItemAdapter<I extends ThreadItem>
|
||||
revision++;
|
||||
}
|
||||
|
||||
protected interface ThreadItemListener<I> {
|
||||
public interface ThreadItemListener<I> {
|
||||
|
||||
void onItemVisible(I item);
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ import static android.support.design.widget.Snackbar.make;
|
||||
import static android.view.View.GONE;
|
||||
import static android.view.View.VISIBLE;
|
||||
|
||||
public abstract class ThreadListActivity<G extends NamedGroup, I extends ThreadItem, H extends PostHeader, A extends ThreadItemAdapter<I>>
|
||||
public abstract class ThreadListActivity<G extends NamedGroup, I extends ThreadItem, H extends PostHeader>
|
||||
extends BriarActivity
|
||||
implements ThreadListListener<H>, TextInputListener,
|
||||
ThreadItemListener<I> {
|
||||
@@ -46,7 +46,7 @@ public abstract class ThreadListActivity<G extends NamedGroup, I extends ThreadI
|
||||
private static final Logger LOG =
|
||||
Logger.getLogger(ThreadListActivity.class.getName());
|
||||
|
||||
protected A adapter;
|
||||
protected ThreadItemAdapter<I> adapter;
|
||||
protected BriarRecyclerView list;
|
||||
protected TextInputView textInput;
|
||||
protected GroupId groupId;
|
||||
@@ -88,7 +88,8 @@ public abstract class ThreadListActivity<G extends NamedGroup, I extends ThreadI
|
||||
@LayoutRes
|
||||
protected abstract int getLayout();
|
||||
|
||||
protected abstract A createAdapter(LinearLayoutManager layoutManager);
|
||||
protected abstract ThreadItemAdapter<I> createAdapter(
|
||||
LinearLayoutManager layoutManager);
|
||||
|
||||
protected void loadNamedGroup() {
|
||||
getController().loadNamedGroup(
|
||||
@@ -249,8 +250,7 @@ public abstract class ThreadListActivity<G extends NamedGroup, I extends ThreadI
|
||||
finish();
|
||||
}
|
||||
};
|
||||
getController().createAndStoreMessage(text,
|
||||
replyItem != null ? replyItem.getId() : null, handler);
|
||||
getController().createAndStoreMessage(text, replyItem, handler);
|
||||
textInput.hideSoftKeyboard();
|
||||
textInput.setVisibility(GONE);
|
||||
textInput.setText("");
|
||||
|
||||
@@ -10,7 +10,6 @@ import org.briarproject.api.clients.NamedGroup;
|
||||
import org.briarproject.api.clients.PostHeader;
|
||||
import org.briarproject.api.db.DbException;
|
||||
import org.briarproject.api.sync.GroupId;
|
||||
import org.briarproject.api.sync.MessageId;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
@@ -29,7 +28,7 @@ public interface ThreadListController<G extends NamedGroup, I extends ThreadItem
|
||||
|
||||
void markItemsRead(Collection<I> items);
|
||||
|
||||
void createAndStoreMessage(String body, @Nullable MessageId parentId,
|
||||
void createAndStoreMessage(String body, @Nullable I parentItem,
|
||||
ResultExceptionHandler<I, DbException> handler);
|
||||
|
||||
void deleteNamedGroup(ResultExceptionHandler<Void, DbException> handler);
|
||||
|
||||
@@ -2,7 +2,6 @@ package org.briarproject.android.threaded;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.support.annotation.CallSuper;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import org.briarproject.android.api.AndroidNotificationManager;
|
||||
import org.briarproject.android.controller.DbControllerImpl;
|
||||
@@ -18,7 +17,6 @@ import org.briarproject.api.event.EventBus;
|
||||
import org.briarproject.api.event.EventListener;
|
||||
import org.briarproject.api.event.GroupRemovedEvent;
|
||||
import org.briarproject.api.identity.IdentityManager;
|
||||
import org.briarproject.api.identity.LocalAuthor;
|
||||
import org.briarproject.api.lifecycle.LifecycleManager;
|
||||
import org.briarproject.api.sync.GroupId;
|
||||
import org.briarproject.api.sync.MessageId;
|
||||
@@ -43,28 +41,28 @@ public abstract class ThreadListControllerImpl<G extends NamedGroup, I extends T
|
||||
private static final Logger LOG =
|
||||
Logger.getLogger(ThreadListControllerImpl.class.getName());
|
||||
|
||||
private final IdentityManager identityManager;
|
||||
private final Executor cryptoExecutor;
|
||||
protected final IdentityManager identityManager;
|
||||
protected final Executor cryptoExecutor;
|
||||
protected final AndroidNotificationManager notificationManager;
|
||||
protected final Clock clock;
|
||||
private final EventBus eventBus;
|
||||
private final Clock clock;
|
||||
|
||||
private final Map<MessageId, String> bodyCache = new ConcurrentHashMap<>();
|
||||
|
||||
private volatile GroupId groupId;
|
||||
|
||||
protected ThreadListListener<H> listener;
|
||||
protected volatile ThreadListListener<H> listener;
|
||||
|
||||
protected ThreadListControllerImpl(@DatabaseExecutor Executor dbExecutor,
|
||||
LifecycleManager lifecycleManager, IdentityManager identityManager,
|
||||
@CryptoExecutor Executor cryptoExecutor, EventBus eventBus,
|
||||
AndroidNotificationManager notificationManager, Clock clock) {
|
||||
Clock clock, AndroidNotificationManager notificationManager) {
|
||||
super(dbExecutor, lifecycleManager);
|
||||
this.identityManager = identityManager;
|
||||
this.cryptoExecutor = cryptoExecutor;
|
||||
this.eventBus = eventBus;
|
||||
this.notificationManager = notificationManager;
|
||||
this.clock = clock;
|
||||
this.eventBus = eventBus;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -160,7 +158,7 @@ public abstract class ThreadListControllerImpl<G extends NamedGroup, I extends T
|
||||
for (H header : headers) {
|
||||
if (!bodyCache.containsKey(header.getId())) {
|
||||
bodyCache.put(header.getId(),
|
||||
loadMessageBody(header.getId()));
|
||||
loadMessageBody(header));
|
||||
}
|
||||
}
|
||||
duration = System.currentTimeMillis() - now;
|
||||
@@ -182,7 +180,7 @@ public abstract class ThreadListControllerImpl<G extends NamedGroup, I extends T
|
||||
protected abstract Collection<H> loadHeaders() throws DbException;
|
||||
|
||||
@DatabaseExecutor
|
||||
protected abstract String loadMessageBody(MessageId id) throws DbException;
|
||||
protected abstract String loadMessageBody(H header) throws DbException;
|
||||
|
||||
@Override
|
||||
public void loadItem(final H header,
|
||||
@@ -194,7 +192,7 @@ public abstract class ThreadListControllerImpl<G extends NamedGroup, I extends T
|
||||
long now = System.currentTimeMillis();
|
||||
String body;
|
||||
if (!bodyCache.containsKey(header.getId())) {
|
||||
body = loadMessageBody(header.getId());
|
||||
body = loadMessageBody(header);
|
||||
bodyCache.put(header.getId(), body);
|
||||
} else {
|
||||
body = bodyCache.get(header.getId());
|
||||
@@ -242,57 +240,7 @@ public abstract class ThreadListControllerImpl<G extends NamedGroup, I extends T
|
||||
@DatabaseExecutor
|
||||
protected abstract void markRead(MessageId id) throws DbException;
|
||||
|
||||
@Override
|
||||
public void createAndStoreMessage(final String body,
|
||||
@Nullable final MessageId parentId,
|
||||
final ResultExceptionHandler<I, DbException> handler) {
|
||||
runOnDbThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
long now = System.currentTimeMillis();
|
||||
LocalAuthor author = identityManager.getLocalAuthor();
|
||||
long timestamp = getLatestTimestamp();
|
||||
timestamp = Math.max(timestamp, clock.currentTimeMillis());
|
||||
long duration = System.currentTimeMillis() - now;
|
||||
if (LOG.isLoggable(INFO)) {
|
||||
LOG.info("Loading identity and timestamp took " +
|
||||
duration + " ms");
|
||||
}
|
||||
createMessage(body, timestamp, parentId, author, handler);
|
||||
} catch (DbException e) {
|
||||
if (LOG.isLoggable(WARNING))
|
||||
LOG.log(WARNING, e.toString(), e);
|
||||
handler.onException(e);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@DatabaseExecutor
|
||||
protected abstract long getLatestTimestamp() throws DbException;
|
||||
|
||||
private void createMessage(final String body, final long timestamp,
|
||||
final @Nullable MessageId parentId, final LocalAuthor author,
|
||||
final ResultExceptionHandler<I, DbException> handler) {
|
||||
cryptoExecutor.execute(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
long now = System.currentTimeMillis();
|
||||
M msg = createLocalMessage(body, timestamp, parentId, author);
|
||||
long duration = System.currentTimeMillis() - now;
|
||||
if (LOG.isLoggable(INFO))
|
||||
LOG.info("Creating message took " + duration + " ms");
|
||||
storePost(msg, body, handler);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@CryptoExecutor
|
||||
protected abstract M createLocalMessage(String body, long timestamp,
|
||||
@Nullable MessageId parentId, LocalAuthor author);
|
||||
|
||||
private void storePost(final M msg, final String body,
|
||||
protected void storePost(final M msg, final String body,
|
||||
final ResultExceptionHandler<I, DbException> resultHandler) {
|
||||
runOnDbThread(new Runnable() {
|
||||
@Override
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
package org.briarproject.android.threaded;
|
||||
|
||||
import android.support.annotation.UiThread;
|
||||
import android.view.View;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.briarproject.R;
|
||||
import org.briarproject.android.threaded.ThreadItemAdapter.ThreadItemListener;
|
||||
import org.briarproject.api.nullsafety.NotNullByDefault;
|
||||
|
||||
import static android.view.View.GONE;
|
||||
import static android.view.View.INVISIBLE;
|
||||
import static android.view.View.VISIBLE;
|
||||
|
||||
@UiThread
|
||||
@NotNullByDefault
|
||||
public class ThreadPostViewHolder<I extends ThreadItem>
|
||||
extends BaseThreadItemViewHolder<I> {
|
||||
|
||||
private final TextView lvlText, repliesText;
|
||||
private final View[] lvls;
|
||||
private final View chevron, replyButton;
|
||||
|
||||
public ThreadPostViewHolder(View v) {
|
||||
super(v);
|
||||
|
||||
lvlText = (TextView) v.findViewById(R.id.nested_line_text);
|
||||
repliesText = (TextView) v.findViewById(R.id.replies);
|
||||
int[] nestedLineIds = {
|
||||
R.id.nested_line_1, R.id.nested_line_2, R.id.nested_line_3,
|
||||
R.id.nested_line_4, R.id.nested_line_5
|
||||
};
|
||||
lvls = new View[nestedLineIds.length];
|
||||
for (int i = 0; i < lvls.length; i++) {
|
||||
lvls[i] = v.findViewById(nestedLineIds[i]);
|
||||
}
|
||||
chevron = v.findViewById(R.id.chevron);
|
||||
replyButton = v.findViewById(R.id.btn_reply);
|
||||
}
|
||||
|
||||
// TODO improve encapsulation, so we don't need to pass the adapter here
|
||||
@Override
|
||||
public void bind(final ThreadItemAdapter<I> adapter,
|
||||
final ThreadItemListener<I> listener, final I item, int pos) {
|
||||
super.bind(adapter, listener, item, pos);
|
||||
|
||||
for (int i = 0; i < lvls.length; i++) {
|
||||
lvls[i].setVisibility(i < item.getLevel() ? VISIBLE : GONE);
|
||||
}
|
||||
if (item.getLevel() > 5) {
|
||||
lvlText.setVisibility(VISIBLE);
|
||||
lvlText.setText("" + item.getLevel());
|
||||
} else {
|
||||
lvlText.setVisibility(GONE);
|
||||
}
|
||||
|
||||
int replies = adapter.getReplyCount(item);
|
||||
if (replies == 0) {
|
||||
repliesText.setText("");
|
||||
} else {
|
||||
repliesText.setText(getContext().getResources()
|
||||
.getQuantityString(R.plurals.message_replies, replies,
|
||||
replies));
|
||||
}
|
||||
|
||||
if (item.hasDescendants()) {
|
||||
chevron.setVisibility(VISIBLE);
|
||||
if (item.isShowingDescendants()) {
|
||||
chevron.setSelected(false);
|
||||
} else {
|
||||
chevron.setSelected(true);
|
||||
}
|
||||
chevron.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
chevron.setSelected(!chevron.isSelected());
|
||||
if (chevron.isSelected()) {
|
||||
adapter.hideDescendants(item);
|
||||
} else {
|
||||
adapter.showDescendants(item);
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
chevron.setVisibility(INVISIBLE);
|
||||
}
|
||||
replyButton.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
listener.onReplyClick(item);
|
||||
adapter.scrollTo(item);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import org.briarproject.BuildConfig;
|
||||
import org.briarproject.TestUtils;
|
||||
import org.briarproject.android.TestBriarApplication;
|
||||
import org.briarproject.android.controller.handler.UiResultExceptionHandler;
|
||||
import org.briarproject.android.threaded.ThreadItemAdapter;
|
||||
import org.briarproject.api.db.DbException;
|
||||
import org.briarproject.api.identity.Author;
|
||||
import org.briarproject.api.identity.AuthorId;
|
||||
@@ -111,7 +112,7 @@ public class ForumActivityTest {
|
||||
List<ForumItem> dummyData = getDummyData();
|
||||
verify(mc, times(1)).loadItems(rc.capture());
|
||||
rc.getValue().onResult(dummyData);
|
||||
NestedForumAdapter adapter = forumActivity.getAdapter();
|
||||
ThreadItemAdapter<ForumItem> adapter = forumActivity.getAdapter();
|
||||
Assert.assertNotNull(adapter);
|
||||
// Cascade close
|
||||
assertEquals(6, adapter.getItemCount());
|
||||
|
||||
@@ -3,6 +3,7 @@ package org.briarproject.android.forum;
|
||||
import org.briarproject.android.ActivityModule;
|
||||
import org.briarproject.android.controller.BriarController;
|
||||
import org.briarproject.android.controller.BriarControllerImpl;
|
||||
import org.briarproject.android.threaded.ThreadItemAdapter;
|
||||
import org.mockito.Mockito;
|
||||
|
||||
/**
|
||||
@@ -15,7 +16,7 @@ public class TestForumActivity extends ForumActivity {
|
||||
return forumController;
|
||||
}
|
||||
|
||||
public NestedForumAdapter getAdapter() {
|
||||
public ThreadItemAdapter<ForumItem> getAdapter() {
|
||||
return adapter;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ package org.briarproject.api.clients;
|
||||
import org.briarproject.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.api.sync.Message;
|
||||
import org.briarproject.api.sync.MessageId;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import javax.annotation.concurrent.Immutable;
|
||||
|
||||
@@ -6,6 +6,7 @@ import org.briarproject.api.data.BdfList;
|
||||
import org.briarproject.api.db.DbException;
|
||||
import org.briarproject.api.db.Transaction;
|
||||
import org.briarproject.api.sync.GroupId;
|
||||
import org.briarproject.api.sync.InvalidMessageException;
|
||||
import org.briarproject.api.sync.Message;
|
||||
import org.briarproject.api.sync.MessageId;
|
||||
|
||||
@@ -81,4 +82,8 @@ public interface ClientHelper {
|
||||
|
||||
byte[] sign(BdfList toSign, byte[] privateKey)
|
||||
throws FormatException, GeneralSecurityException;
|
||||
|
||||
void verifySignature(byte[] sig, byte[] publicKey, BdfList signed)
|
||||
throws FormatException, GeneralSecurityException;
|
||||
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import org.briarproject.api.identity.Author;
|
||||
import org.briarproject.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.api.sync.Message;
|
||||
import org.briarproject.api.sync.MessageId;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import javax.annotation.concurrent.Immutable;
|
||||
@@ -14,16 +13,16 @@ import javax.annotation.concurrent.Immutable;
|
||||
@NotNullByDefault
|
||||
public class GroupMessage extends BaseMessage {
|
||||
|
||||
private final Author author;
|
||||
private final Author member;
|
||||
|
||||
public GroupMessage(Message message, @Nullable MessageId parent,
|
||||
Author author) {
|
||||
Author member) {
|
||||
super(message, parent);
|
||||
this.author = author;
|
||||
this.member = member;
|
||||
}
|
||||
|
||||
public Author getAuthor() {
|
||||
return author;
|
||||
public Author getMember() {
|
||||
return member;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,20 +1,58 @@
|
||||
package org.briarproject.api.privategroup;
|
||||
|
||||
import org.briarproject.api.FormatException;
|
||||
import org.briarproject.api.crypto.PrivateKey;
|
||||
import org.briarproject.api.crypto.CryptoExecutor;
|
||||
import org.briarproject.api.identity.Author;
|
||||
import org.briarproject.api.identity.LocalAuthor;
|
||||
import org.briarproject.api.sync.GroupId;
|
||||
import org.briarproject.api.sync.MessageId;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.security.GeneralSecurityException;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
public interface GroupMessageFactory {
|
||||
|
||||
@NotNull
|
||||
/**
|
||||
* Creates a new member announcement that contains the joiner's identity
|
||||
* and is signed by the creator.
|
||||
* <p>
|
||||
* When a new member accepts an invitation to the group,
|
||||
* the creator sends this new member announcement to the group.
|
||||
*
|
||||
* @param groupId The ID of the group the new member joined
|
||||
* @param timestamp The current timestamp
|
||||
* @param creator The creator of the group with {@param groupId}
|
||||
* @param member The new member that has just accepted an invitation
|
||||
*/
|
||||
@CryptoExecutor
|
||||
GroupMessage createNewMemberMessage(GroupId groupId, long timestamp,
|
||||
LocalAuthor creator, Author member);
|
||||
|
||||
/**
|
||||
* Creates a join announcement message
|
||||
* that depends on a previous new member announcement.
|
||||
*
|
||||
* @param groupId The ID of the Group that is being joined
|
||||
* @param timestamp Must be equal to the timestamp of the new member message
|
||||
* @param member Our own LocalAuthor
|
||||
* @param newMemberId The MessageId of the new member message
|
||||
*/
|
||||
@CryptoExecutor
|
||||
GroupMessage createJoinMessage(GroupId groupId, long timestamp,
|
||||
LocalAuthor member, MessageId newMemberId);
|
||||
|
||||
/**
|
||||
* Creates a group message
|
||||
*
|
||||
* @param groupId The ID of the Group that is posted in
|
||||
* @param timestamp Must be greater than the timestamps of the parentId
|
||||
* post, if any, and the member's previous message
|
||||
* @param parentId The ID of the message that is replied to
|
||||
* @param author The author of the group message
|
||||
* @param body The content of the group message
|
||||
* @param previousMsgId The ID of the author's previous message
|
||||
* in this group
|
||||
*/
|
||||
@CryptoExecutor
|
||||
GroupMessage createGroupMessage(GroupId groupId, long timestamp,
|
||||
MessageId parent, LocalAuthor author, String body)
|
||||
throws FormatException, GeneralSecurityException;
|
||||
@Nullable MessageId parentId, LocalAuthor author, String body,
|
||||
MessageId previousMsgId);
|
||||
|
||||
}
|
||||
|
||||
@@ -3,19 +3,23 @@ package org.briarproject.api.privategroup;
|
||||
import org.briarproject.api.clients.PostHeader;
|
||||
import org.briarproject.api.identity.Author;
|
||||
import org.briarproject.api.identity.Author.Status;
|
||||
import org.briarproject.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.api.sync.GroupId;
|
||||
import org.briarproject.api.sync.MessageId;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import javax.annotation.concurrent.Immutable;
|
||||
|
||||
@Immutable
|
||||
@NotNullByDefault
|
||||
public class GroupMessageHeader extends PostHeader {
|
||||
|
||||
private final GroupId groupId;
|
||||
|
||||
public GroupMessageHeader(@NotNull GroupId groupId, @NotNull MessageId id,
|
||||
public GroupMessageHeader(GroupId groupId, MessageId id,
|
||||
@Nullable MessageId parentId, long timestamp,
|
||||
@NotNull Author author, @NotNull Status authorStatus,
|
||||
boolean read) {
|
||||
Author author, Status authorStatus, boolean read) {
|
||||
super(id, parentId, timestamp, author, authorStatus, read);
|
||||
this.groupId = groupId;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
package org.briarproject.api.privategroup;
|
||||
|
||||
import org.briarproject.api.identity.Author;
|
||||
import org.briarproject.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.api.sync.GroupId;
|
||||
import org.briarproject.api.sync.MessageId;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import javax.annotation.concurrent.Immutable;
|
||||
|
||||
@Immutable
|
||||
@NotNullByDefault
|
||||
public class JoinMessageHeader extends GroupMessageHeader {
|
||||
|
||||
public JoinMessageHeader(GroupId groupId, MessageId id,
|
||||
@Nullable MessageId parentId, long timestamp, Author author,
|
||||
Author.Status authorStatus, boolean read) {
|
||||
super(groupId, id, parentId, timestamp, author, authorStatus, read);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package org.briarproject.api.privategroup;
|
||||
|
||||
public enum MessageType {
|
||||
NEW_MEMBER(0),
|
||||
JOIN(1),
|
||||
POST(2);
|
||||
|
||||
int value;
|
||||
|
||||
MessageType(int value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public static MessageType valueOf(int value) {
|
||||
for (MessageType m : values()) if (m.value == value) return m;
|
||||
throw new IllegalArgumentException();
|
||||
}
|
||||
|
||||
public int getInt() {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
@@ -3,57 +3,59 @@ package org.briarproject.api.privategroup;
|
||||
import org.briarproject.api.clients.MessageTracker;
|
||||
import org.briarproject.api.db.DbException;
|
||||
import org.briarproject.api.db.Transaction;
|
||||
import org.briarproject.api.identity.LocalAuthor;
|
||||
import org.briarproject.api.sync.ClientId;
|
||||
import org.briarproject.api.sync.GroupId;
|
||||
import org.briarproject.api.sync.MessageId;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
public interface PrivateGroupManager extends MessageTracker {
|
||||
|
||||
/** Returns the unique ID of the private group client. */
|
||||
@NotNull
|
||||
ClientId getClientId();
|
||||
|
||||
/** Adds a new private group. */
|
||||
GroupId addPrivateGroup(String name) throws DbException;
|
||||
/**
|
||||
* Adds a new private group and joins it.
|
||||
*
|
||||
* @param group The private group to add
|
||||
* @param newMemberMsg The creator's message announcing herself as
|
||||
* first new member
|
||||
* @param joinMsg The creator's own join message
|
||||
*/
|
||||
void addPrivateGroup(PrivateGroup group, GroupMessage newMemberMsg,
|
||||
GroupMessage joinMsg) throws DbException;
|
||||
|
||||
/** Removes a dissolved private group. */
|
||||
void removePrivateGroup(GroupId g) throws DbException;
|
||||
|
||||
/** Creates a local group message. */
|
||||
GroupMessage createLocalMessage(GroupId groupId, String body,
|
||||
long timestamp, @Nullable MessageId parentId, LocalAuthor author);
|
||||
/** Gets the MessageId of your previous message sent to the group */
|
||||
MessageId getPreviousMsgId(GroupId g) throws DbException;
|
||||
|
||||
/** Returns the timestamp of the message with the given ID */
|
||||
// TODO change to getPreviousMessageHeader()
|
||||
long getMessageTimestamp(MessageId id) throws DbException;
|
||||
|
||||
/** Stores (and sends) a local group message. */
|
||||
GroupMessageHeader addLocalMessage(GroupMessage p) throws DbException;
|
||||
|
||||
/** Returns the private group with the given ID. */
|
||||
@NotNull
|
||||
PrivateGroup getPrivateGroup(GroupId g) throws DbException;
|
||||
|
||||
/**
|
||||
* Returns the private group with the given ID within the given transaction.
|
||||
*/
|
||||
@NotNull
|
||||
PrivateGroup getPrivateGroup(Transaction txn, GroupId g) throws DbException;
|
||||
|
||||
/** Returns all private groups the user is a member of. */
|
||||
@NotNull
|
||||
Collection<PrivateGroup> getPrivateGroups() throws DbException;
|
||||
|
||||
/** Returns true if the private group has been dissolved. */
|
||||
boolean isDissolved(GroupId g) throws DbException;
|
||||
|
||||
/** Returns the body of the group message with the given ID. */
|
||||
@NotNull
|
||||
String getMessageBody(MessageId m) throws DbException;
|
||||
|
||||
/** Returns the headers of all group messages in the given group. */
|
||||
@NotNull
|
||||
Collection<GroupMessageHeader> getHeaders(GroupId g) throws DbException;
|
||||
|
||||
}
|
||||
|
||||
@@ -6,10 +6,6 @@ import org.briarproject.api.blogs.BlogFactory;
|
||||
import org.briarproject.api.blogs.MessageType;
|
||||
import org.briarproject.api.clients.BdfMessageContext;
|
||||
import org.briarproject.api.clients.ClientHelper;
|
||||
import org.briarproject.api.crypto.CryptoComponent;
|
||||
import org.briarproject.api.crypto.KeyParser;
|
||||
import org.briarproject.api.crypto.PublicKey;
|
||||
import org.briarproject.api.crypto.Signature;
|
||||
import org.briarproject.api.data.BdfDictionary;
|
||||
import org.briarproject.api.data.BdfEntry;
|
||||
import org.briarproject.api.data.BdfList;
|
||||
@@ -48,18 +44,15 @@ import static org.briarproject.api.identity.AuthorConstants.MAX_SIGNATURE_LENGTH
|
||||
|
||||
class BlogPostValidator extends BdfMessageValidator {
|
||||
|
||||
private final CryptoComponent crypto;
|
||||
private final GroupFactory groupFactory;
|
||||
private final MessageFactory messageFactory;
|
||||
private final BlogFactory blogFactory;
|
||||
|
||||
BlogPostValidator(CryptoComponent crypto, GroupFactory groupFactory,
|
||||
MessageFactory messageFactory, BlogFactory blogFactory,
|
||||
ClientHelper clientHelper, MetadataEncoder metadataEncoder,
|
||||
Clock clock) {
|
||||
BlogPostValidator(GroupFactory groupFactory, MessageFactory messageFactory,
|
||||
BlogFactory blogFactory, ClientHelper clientHelper,
|
||||
MetadataEncoder metadataEncoder, Clock clock) {
|
||||
super(clientHelper, metadataEncoder, clock);
|
||||
|
||||
this.crypto = crypto;
|
||||
this.groupFactory = groupFactory;
|
||||
this.messageFactory = messageFactory;
|
||||
this.blogFactory = blogFactory;
|
||||
@@ -109,7 +102,11 @@ class BlogPostValidator extends BdfMessageValidator {
|
||||
BdfList signed = BdfList.of(g.getId(), m.getTimestamp(), postBody);
|
||||
Blog b = blogFactory.parseBlog(g, ""); // description doesn't matter
|
||||
Author a = b.getAuthor();
|
||||
verifySignature(sig, a.getPublicKey(), signed);
|
||||
try {
|
||||
clientHelper.verifySignature(sig, a.getPublicKey(), signed);
|
||||
} catch (GeneralSecurityException e) {
|
||||
throw new InvalidMessageException(e);
|
||||
}
|
||||
|
||||
// Return the metadata and dependencies
|
||||
BdfDictionary meta = new BdfDictionary();
|
||||
@@ -150,7 +147,11 @@ class BlogPostValidator extends BdfMessageValidator {
|
||||
currentId);
|
||||
Blog b = blogFactory.parseBlog(g, ""); // description doesn't matter
|
||||
Author a = b.getAuthor();
|
||||
verifySignature(sig, a.getPublicKey(), signed);
|
||||
try {
|
||||
clientHelper.verifySignature(sig, a.getPublicKey(), signed);
|
||||
} catch (GeneralSecurityException e) {
|
||||
throw new InvalidMessageException(e);
|
||||
}
|
||||
|
||||
// Return the metadata and dependencies
|
||||
BdfDictionary meta = new BdfDictionary();
|
||||
@@ -267,26 +268,6 @@ class BlogPostValidator extends BdfMessageValidator {
|
||||
return new BdfMessageContext(meta, dependencies);
|
||||
}
|
||||
|
||||
private void verifySignature(byte[] sig, byte[] publicKey, BdfList signed)
|
||||
throws InvalidMessageException {
|
||||
try {
|
||||
// Parse the public key
|
||||
KeyParser keyParser = crypto.getSignatureKeyParser();
|
||||
PublicKey key = keyParser.parsePublicKey(publicKey);
|
||||
// Verify the signature
|
||||
Signature signature = crypto.getSignature();
|
||||
signature.initVerify(key);
|
||||
signature.update(clientHelper.toByteArray(signed));
|
||||
if (!signature.verify(sig)) {
|
||||
throw new InvalidMessageException("Invalid signature");
|
||||
}
|
||||
} catch (GeneralSecurityException e) {
|
||||
throw new InvalidMessageException("Invalid public key");
|
||||
} catch (FormatException e) {
|
||||
throw new InvalidMessageException(e);
|
||||
}
|
||||
}
|
||||
|
||||
static BdfDictionary authorToBdfDictionary(Author a) {
|
||||
return BdfDictionary.of(
|
||||
new BdfEntry(KEY_AUTHOR_ID, a.getId()),
|
||||
|
||||
@@ -5,7 +5,6 @@ import org.briarproject.api.blogs.BlogManager;
|
||||
import org.briarproject.api.blogs.BlogPostFactory;
|
||||
import org.briarproject.api.clients.ClientHelper;
|
||||
import org.briarproject.api.contact.ContactManager;
|
||||
import org.briarproject.api.crypto.CryptoComponent;
|
||||
import org.briarproject.api.data.MetadataEncoder;
|
||||
import org.briarproject.api.identity.AuthorFactory;
|
||||
import org.briarproject.api.identity.IdentityManager;
|
||||
@@ -64,14 +63,14 @@ public class BlogsModule {
|
||||
@Provides
|
||||
@Singleton
|
||||
BlogPostValidator provideBlogPostValidator(
|
||||
ValidationManager validationManager, CryptoComponent crypto,
|
||||
GroupFactory groupFactory, MessageFactory messageFactory,
|
||||
BlogFactory blogFactory, ClientHelper clientHelper,
|
||||
MetadataEncoder metadataEncoder, Clock clock) {
|
||||
ValidationManager validationManager, GroupFactory groupFactory,
|
||||
MessageFactory messageFactory, BlogFactory blogFactory,
|
||||
ClientHelper clientHelper, MetadataEncoder metadataEncoder,
|
||||
Clock clock) {
|
||||
|
||||
BlogPostValidator validator = new BlogPostValidator(crypto,
|
||||
groupFactory, messageFactory, blogFactory, clientHelper,
|
||||
metadataEncoder, clock);
|
||||
BlogPostValidator validator = new BlogPostValidator(groupFactory,
|
||||
messageFactory, blogFactory, clientHelper, metadataEncoder,
|
||||
clock);
|
||||
validationManager.registerMessageValidator(CLIENT_ID, validator);
|
||||
|
||||
return validator;
|
||||
|
||||
@@ -5,6 +5,7 @@ import org.briarproject.api.clients.ClientHelper;
|
||||
import org.briarproject.api.crypto.CryptoComponent;
|
||||
import org.briarproject.api.crypto.KeyParser;
|
||||
import org.briarproject.api.crypto.PrivateKey;
|
||||
import org.briarproject.api.crypto.PublicKey;
|
||||
import org.briarproject.api.crypto.Signature;
|
||||
import org.briarproject.api.data.BdfDictionary;
|
||||
import org.briarproject.api.data.BdfList;
|
||||
@@ -320,4 +321,20 @@ class ClientHelperImpl implements ClientHelper {
|
||||
signature.update(toByteArray(toSign));
|
||||
return signature.sign();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void verifySignature(byte[] sig, byte[] publicKey, BdfList signed)
|
||||
throws FormatException, GeneralSecurityException {
|
||||
// Parse the public key
|
||||
KeyParser keyParser = cryptoComponent.getSignatureKeyParser();
|
||||
PublicKey key = keyParser.parsePublicKey(publicKey);
|
||||
// Verify the signature
|
||||
Signature signature = cryptoComponent.getSignature();
|
||||
signature.initVerify(key);
|
||||
signature.update(toByteArray(signed));
|
||||
if (!signature.verify(sig)) {
|
||||
throw new GeneralSecurityException("Invalid signature");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -54,11 +54,11 @@ public class ForumModule {
|
||||
@Provides
|
||||
@Singleton
|
||||
ForumPostValidator provideForumPostValidator(
|
||||
ValidationManager validationManager, CryptoComponent crypto,
|
||||
AuthorFactory authorFactory, ClientHelper clientHelper,
|
||||
MetadataEncoder metadataEncoder, Clock clock) {
|
||||
ForumPostValidator validator = new ForumPostValidator(crypto,
|
||||
authorFactory, clientHelper, metadataEncoder, clock);
|
||||
ValidationManager validationManager, AuthorFactory authorFactory,
|
||||
ClientHelper clientHelper, MetadataEncoder metadataEncoder,
|
||||
Clock clock) {
|
||||
ForumPostValidator validator = new ForumPostValidator(authorFactory,
|
||||
clientHelper, metadataEncoder, clock);
|
||||
validationManager.registerMessageValidator(
|
||||
ForumManagerImpl.CLIENT_ID, validator);
|
||||
return validator;
|
||||
|
||||
@@ -4,10 +4,6 @@ import org.briarproject.api.FormatException;
|
||||
import org.briarproject.api.UniqueId;
|
||||
import org.briarproject.api.clients.BdfMessageContext;
|
||||
import org.briarproject.api.clients.ClientHelper;
|
||||
import org.briarproject.api.crypto.CryptoComponent;
|
||||
import org.briarproject.api.crypto.KeyParser;
|
||||
import org.briarproject.api.crypto.PublicKey;
|
||||
import org.briarproject.api.crypto.Signature;
|
||||
import org.briarproject.api.data.BdfDictionary;
|
||||
import org.briarproject.api.data.BdfList;
|
||||
import org.briarproject.api.data.MetadataEncoder;
|
||||
@@ -32,14 +28,11 @@ import static org.briarproject.api.identity.AuthorConstants.MAX_SIGNATURE_LENGTH
|
||||
|
||||
class ForumPostValidator extends BdfMessageValidator {
|
||||
|
||||
private final CryptoComponent crypto;
|
||||
private final AuthorFactory authorFactory;
|
||||
|
||||
ForumPostValidator(CryptoComponent crypto, AuthorFactory authorFactory,
|
||||
ClientHelper clientHelper, MetadataEncoder metadataEncoder,
|
||||
Clock clock) {
|
||||
ForumPostValidator(AuthorFactory authorFactory, ClientHelper clientHelper,
|
||||
MetadataEncoder metadataEncoder, Clock clock) {
|
||||
super(clientHelper, metadataEncoder, clock);
|
||||
this.crypto = crypto;
|
||||
this.authorFactory = authorFactory;
|
||||
}
|
||||
|
||||
@@ -81,22 +74,14 @@ class ForumPostValidator extends BdfMessageValidator {
|
||||
}
|
||||
// Verify the signature, if any
|
||||
if (author != null) {
|
||||
// Serialise the data to be verified
|
||||
BdfList signed = BdfList.of(g.getId(), m.getTimestamp(), parent,
|
||||
authorList, contentType, forumPostBody);
|
||||
try {
|
||||
// Parse the public key
|
||||
KeyParser keyParser = crypto.getSignatureKeyParser();
|
||||
PublicKey key = keyParser.parsePublicKey(author.getPublicKey());
|
||||
// Serialise the data to be signed
|
||||
BdfList signed = BdfList.of(g.getId(), m.getTimestamp(), parent,
|
||||
authorList, contentType, forumPostBody);
|
||||
// Verify the signature
|
||||
Signature signature = crypto.getSignature();
|
||||
signature.initVerify(key);
|
||||
signature.update(clientHelper.toByteArray(signed));
|
||||
if (!signature.verify(sig)) {
|
||||
throw new InvalidMessageException("Invalid signature");
|
||||
}
|
||||
clientHelper
|
||||
.verifySignature(sig, author.getPublicKey(), signed);
|
||||
} catch (GeneralSecurityException e) {
|
||||
throw new InvalidMessageException("Invalid public key");
|
||||
throw new InvalidMessageException(e);
|
||||
}
|
||||
}
|
||||
// Return the metadata and dependencies
|
||||
|
||||
@@ -1,8 +1,18 @@
|
||||
package org.briarproject.privategroup;
|
||||
|
||||
import static org.briarproject.clients.BdfConstants.MSG_KEY_READ;
|
||||
|
||||
interface Constants {
|
||||
|
||||
// Database keys
|
||||
String KEY_READ = "read";
|
||||
String KEY_TYPE = "type";
|
||||
String KEY_TIMESTAMP = "timestamp";
|
||||
String KEY_READ = MSG_KEY_READ;
|
||||
String KEY_PARENT_MSG_ID = "parentMsgId";
|
||||
String KEY_NEW_MEMBER_MSG_ID = "newMemberMsgId";
|
||||
String KEY_PREVIOUS_MSG_ID = "previousMsgId";
|
||||
String KEY_MEMBER_ID = "memberId";
|
||||
String KEY_MEMBER_NAME = "memberName";
|
||||
String KEY_MEMBER_PUBLIC_KEY = "memberPublicKey";
|
||||
|
||||
}
|
||||
|
||||
@@ -3,18 +3,25 @@ package org.briarproject.privategroup;
|
||||
import org.briarproject.api.FormatException;
|
||||
import org.briarproject.api.clients.ClientHelper;
|
||||
import org.briarproject.api.data.BdfList;
|
||||
import org.briarproject.api.identity.Author;
|
||||
import org.briarproject.api.identity.LocalAuthor;
|
||||
import org.briarproject.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.api.privategroup.GroupMessage;
|
||||
import org.briarproject.api.privategroup.GroupMessageFactory;
|
||||
import org.briarproject.api.sync.GroupId;
|
||||
import org.briarproject.api.sync.Message;
|
||||
import org.briarproject.api.sync.MessageId;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.security.GeneralSecurityException;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import static org.briarproject.api.privategroup.MessageType.JOIN;
|
||||
import static org.briarproject.api.privategroup.MessageType.NEW_MEMBER;
|
||||
import static org.briarproject.api.privategroup.MessageType.POST;
|
||||
|
||||
@NotNullByDefault
|
||||
class GroupMessageFactoryImpl implements GroupMessageFactory {
|
||||
|
||||
private final ClientHelper clientHelper;
|
||||
@@ -24,20 +31,82 @@ class GroupMessageFactoryImpl implements GroupMessageFactory {
|
||||
this.clientHelper = clientHelper;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public GroupMessage createNewMemberMessage(GroupId groupId, long timestamp,
|
||||
LocalAuthor creator, Author member) {
|
||||
try {
|
||||
// Generate the signature
|
||||
int type = NEW_MEMBER.getInt();
|
||||
BdfList toSign = BdfList.of(groupId, timestamp, type,
|
||||
member.getName(), member.getPublicKey());
|
||||
byte[] signature =
|
||||
clientHelper.sign(toSign, creator.getPrivateKey());
|
||||
|
||||
// Compose the message
|
||||
BdfList body =
|
||||
BdfList.of(type, member.getName(),
|
||||
member.getPublicKey(), signature);
|
||||
Message m = clientHelper.createMessage(groupId, timestamp, body);
|
||||
|
||||
return new GroupMessage(m, null, member);
|
||||
} catch (GeneralSecurityException e) {
|
||||
throw new RuntimeException(e);
|
||||
} catch (FormatException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public GroupMessage createJoinMessage(GroupId groupId, long timestamp,
|
||||
LocalAuthor member, MessageId newMemberId) {
|
||||
try {
|
||||
// Generate the signature
|
||||
int type = JOIN.getInt();
|
||||
BdfList toSign = BdfList.of(groupId, timestamp, type,
|
||||
member.getName(), member.getPublicKey(), newMemberId);
|
||||
byte[] signature =
|
||||
clientHelper.sign(toSign, member.getPrivateKey());
|
||||
|
||||
// Compose the message
|
||||
BdfList body =
|
||||
BdfList.of(type, member.getName(),
|
||||
member.getPublicKey(), newMemberId, signature);
|
||||
Message m = clientHelper.createMessage(groupId, timestamp, body);
|
||||
|
||||
return new GroupMessage(m, null, member);
|
||||
} catch (GeneralSecurityException e) {
|
||||
throw new RuntimeException(e);
|
||||
} catch (FormatException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public GroupMessage createGroupMessage(GroupId groupId, long timestamp,
|
||||
MessageId parent, LocalAuthor author, String body)
|
||||
throws FormatException, GeneralSecurityException {
|
||||
@Nullable MessageId parentId, LocalAuthor author, String content,
|
||||
MessageId previousMsgId) {
|
||||
try {
|
||||
// Generate the signature
|
||||
int type = POST.getInt();
|
||||
BdfList toSign = BdfList.of(groupId, timestamp, type,
|
||||
author.getName(), author.getPublicKey(), parentId,
|
||||
previousMsgId, content);
|
||||
byte[] signature =
|
||||
clientHelper.sign(toSign, author.getPrivateKey());
|
||||
|
||||
// Generate the signature
|
||||
byte[] sig = clientHelper.sign(new BdfList(), author.getPrivateKey());
|
||||
// Compose the message
|
||||
BdfList body =
|
||||
BdfList.of(type, author.getName(),
|
||||
author.getPublicKey(), parentId, previousMsgId,
|
||||
content, signature);
|
||||
Message m = clientHelper.createMessage(groupId, timestamp, body);
|
||||
|
||||
// Compose the message
|
||||
Message m =
|
||||
clientHelper.createMessage(groupId, timestamp, new BdfList());
|
||||
|
||||
return new GroupMessage(m, parent, author);
|
||||
return new GroupMessage(m, parentId, author);
|
||||
} catch (GeneralSecurityException e) {
|
||||
throw new RuntimeException(e);
|
||||
} catch (FormatException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -3,11 +3,14 @@ package org.briarproject.privategroup;
|
||||
import org.briarproject.api.FormatException;
|
||||
import org.briarproject.api.clients.BdfMessageContext;
|
||||
import org.briarproject.api.clients.ClientHelper;
|
||||
import org.briarproject.api.crypto.CryptoComponent;
|
||||
import org.briarproject.api.data.BdfDictionary;
|
||||
import org.briarproject.api.data.BdfList;
|
||||
import org.briarproject.api.data.MetadataEncoder;
|
||||
import org.briarproject.api.identity.Author;
|
||||
import org.briarproject.api.identity.AuthorFactory;
|
||||
import org.briarproject.api.privategroup.MessageType;
|
||||
import org.briarproject.api.privategroup.PrivateGroup;
|
||||
import org.briarproject.api.privategroup.PrivateGroupFactory;
|
||||
import org.briarproject.api.sync.Group;
|
||||
import org.briarproject.api.sync.InvalidMessageException;
|
||||
import org.briarproject.api.sync.Message;
|
||||
@@ -15,19 +18,38 @@ import org.briarproject.api.sync.MessageId;
|
||||
import org.briarproject.api.system.Clock;
|
||||
import org.briarproject.clients.BdfMessageValidator;
|
||||
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
|
||||
import static org.briarproject.api.identity.AuthorConstants.MAX_AUTHOR_NAME_LENGTH;
|
||||
import static org.briarproject.api.identity.AuthorConstants.MAX_PUBLIC_KEY_LENGTH;
|
||||
import static org.briarproject.api.identity.AuthorConstants.MAX_SIGNATURE_LENGTH;
|
||||
import static org.briarproject.api.privategroup.MessageType.JOIN;
|
||||
import static org.briarproject.api.privategroup.MessageType.NEW_MEMBER;
|
||||
import static org.briarproject.api.privategroup.MessageType.POST;
|
||||
import static org.briarproject.api.privategroup.PrivateGroupConstants.MAX_GROUP_POST_BODY_LENGTH;
|
||||
import static org.briarproject.privategroup.Constants.KEY_MEMBER_ID;
|
||||
import static org.briarproject.privategroup.Constants.KEY_MEMBER_NAME;
|
||||
import static org.briarproject.privategroup.Constants.KEY_MEMBER_PUBLIC_KEY;
|
||||
import static org.briarproject.privategroup.Constants.KEY_NEW_MEMBER_MSG_ID;
|
||||
import static org.briarproject.privategroup.Constants.KEY_PARENT_MSG_ID;
|
||||
import static org.briarproject.privategroup.Constants.KEY_PREVIOUS_MSG_ID;
|
||||
import static org.briarproject.privategroup.Constants.KEY_READ;
|
||||
import static org.briarproject.privategroup.Constants.KEY_TIMESTAMP;
|
||||
import static org.briarproject.privategroup.Constants.KEY_TYPE;
|
||||
|
||||
class GroupMessageValidator extends BdfMessageValidator {
|
||||
|
||||
private final CryptoComponent crypto;
|
||||
private final PrivateGroupFactory groupFactory;
|
||||
private final AuthorFactory authorFactory;
|
||||
|
||||
GroupMessageValidator(CryptoComponent crypto, AuthorFactory authorFactory,
|
||||
GroupMessageValidator(PrivateGroupFactory groupFactory,
|
||||
ClientHelper clientHelper, MetadataEncoder metadataEncoder,
|
||||
Clock clock) {
|
||||
Clock clock, AuthorFactory authorFactory) {
|
||||
super(clientHelper, metadataEncoder, clock);
|
||||
this.crypto = crypto;
|
||||
this.groupFactory = groupFactory;
|
||||
this.authorFactory = authorFactory;
|
||||
}
|
||||
|
||||
@@ -35,9 +57,168 @@ class GroupMessageValidator extends BdfMessageValidator {
|
||||
protected BdfMessageContext validateMessage(Message m, Group g,
|
||||
BdfList body) throws InvalidMessageException, FormatException {
|
||||
|
||||
checkSize(body, 4, 7);
|
||||
|
||||
// message type (int)
|
||||
int type = body.getLong(0).intValue();
|
||||
body.removeElementAt(0);
|
||||
|
||||
// member_name (string)
|
||||
String memberName = body.getString(0);
|
||||
checkLength(memberName, 1, MAX_AUTHOR_NAME_LENGTH);
|
||||
|
||||
// member_public_key (raw)
|
||||
byte[] memberPublicKey = body.getRaw(1);
|
||||
checkLength(memberPublicKey, 1, MAX_PUBLIC_KEY_LENGTH);
|
||||
|
||||
BdfMessageContext c;
|
||||
switch (MessageType.valueOf(type)) {
|
||||
case NEW_MEMBER:
|
||||
c = validateNewMember(m, g, body, memberName,
|
||||
memberPublicKey);
|
||||
addMessageMetadata(c, memberName, memberPublicKey,
|
||||
m.getTimestamp());
|
||||
break;
|
||||
case JOIN:
|
||||
c = validateJoin(m, g, body, memberName, memberPublicKey);
|
||||
addMessageMetadata(c, memberName, memberPublicKey,
|
||||
m.getTimestamp());
|
||||
break;
|
||||
case POST:
|
||||
c = validatePost(m, g, body, memberName, memberPublicKey);
|
||||
addMessageMetadata(c, memberName, memberPublicKey,
|
||||
m.getTimestamp());
|
||||
break;
|
||||
default:
|
||||
throw new InvalidMessageException("Unknown Message Type");
|
||||
}
|
||||
c.getDictionary().put(KEY_TYPE, type);
|
||||
return c;
|
||||
}
|
||||
|
||||
private BdfMessageContext validateNewMember(Message m, Group g,
|
||||
BdfList body, String memberName, byte[] memberPublicKey)
|
||||
throws InvalidMessageException, FormatException {
|
||||
|
||||
// The content is a BDF list with three elements
|
||||
checkSize(body, 3);
|
||||
|
||||
// signature (raw)
|
||||
// signature with the creator's private key over a list with 4 elements
|
||||
byte[] signature = body.getRaw(2);
|
||||
checkLength(signature, 1, MAX_SIGNATURE_LENGTH);
|
||||
|
||||
// Verify Signature
|
||||
BdfList signed =
|
||||
BdfList.of(g.getId(), m.getTimestamp(), NEW_MEMBER.getInt(),
|
||||
memberName, memberPublicKey);
|
||||
PrivateGroup group = groupFactory.parsePrivateGroup(g);
|
||||
byte[] creatorPublicKey = group.getAuthor().getPublicKey();
|
||||
try {
|
||||
clientHelper.verifySignature(signature, creatorPublicKey, signed);
|
||||
} catch (GeneralSecurityException e) {
|
||||
throw new InvalidMessageException(e);
|
||||
}
|
||||
|
||||
// Return the metadata and no dependencies
|
||||
BdfDictionary meta = new BdfDictionary();
|
||||
Collection<MessageId> dependencies = Collections.emptyList();
|
||||
return new BdfMessageContext(meta);
|
||||
}
|
||||
|
||||
private BdfMessageContext validateJoin(Message m, Group g, BdfList body,
|
||||
String memberName, byte[] memberPublicKey)
|
||||
throws InvalidMessageException, FormatException {
|
||||
|
||||
// The content is a BDF list with four elements
|
||||
checkSize(body, 4);
|
||||
|
||||
// new_member_id (raw)
|
||||
// the identifier of a new member message
|
||||
// with the same member_name and member_public_key
|
||||
byte[] newMemberId = body.getRaw(2);
|
||||
checkLength(newMemberId, MessageId.LENGTH);
|
||||
|
||||
// signature (raw)
|
||||
// a signature with the member's private key over a list with 5 elements
|
||||
byte[] signature = body.getRaw(3);
|
||||
checkLength(signature, 1, MAX_SIGNATURE_LENGTH);
|
||||
|
||||
// Verify Signature
|
||||
BdfList signed = BdfList.of(g.getId(), m.getTimestamp(), JOIN.getInt(),
|
||||
memberName, memberPublicKey, newMemberId);
|
||||
try {
|
||||
clientHelper.verifySignature(signature, memberPublicKey, signed);
|
||||
} catch (GeneralSecurityException e) {
|
||||
throw new InvalidMessageException(e);
|
||||
}
|
||||
|
||||
// The new member message is a dependency
|
||||
Collection<MessageId> dependencies =
|
||||
Collections.singleton(new MessageId(newMemberId));
|
||||
|
||||
// Return the metadata and dependencies
|
||||
BdfDictionary meta = new BdfDictionary();
|
||||
meta.put(KEY_NEW_MEMBER_MSG_ID, newMemberId);
|
||||
return new BdfMessageContext(meta, dependencies);
|
||||
}
|
||||
|
||||
private BdfMessageContext validatePost(Message m, Group g, BdfList body,
|
||||
String memberName, byte[] memberPublicKey)
|
||||
throws InvalidMessageException, FormatException {
|
||||
|
||||
// The content is a BDF list with six elements
|
||||
checkSize(body, 6);
|
||||
|
||||
// parent_id (raw or null)
|
||||
// the identifier of the post to which this is a reply, if any
|
||||
byte[] parentId = body.getOptionalRaw(2);
|
||||
checkLength(parentId, MessageId.LENGTH);
|
||||
|
||||
// previous_message_id (raw)
|
||||
// the identifier of the member's previous post or join message
|
||||
byte[] previousMessageId = body.getRaw(3);
|
||||
checkLength(previousMessageId, MessageId.LENGTH);
|
||||
|
||||
// content (string)
|
||||
String content = body.getString(4);
|
||||
checkLength(content, 0, MAX_GROUP_POST_BODY_LENGTH);
|
||||
|
||||
// signature (raw)
|
||||
// a signature with the member's private key over a list with 7 elements
|
||||
byte[] signature = body.getRaw(5);
|
||||
checkLength(signature, 1, MAX_SIGNATURE_LENGTH);
|
||||
|
||||
// Verify Signature
|
||||
BdfList signed = BdfList.of(g.getId(), m.getTimestamp(), POST.getInt(),
|
||||
memberName, memberPublicKey, parentId, previousMessageId,
|
||||
content);
|
||||
try {
|
||||
clientHelper.verifySignature(signature, memberPublicKey, signed);
|
||||
} catch (GeneralSecurityException e) {
|
||||
throw new InvalidMessageException(e);
|
||||
}
|
||||
|
||||
// The parent post, if any,
|
||||
// and the member's previous message are dependencies
|
||||
Collection<MessageId> dependencies = new ArrayList<MessageId>();
|
||||
if (parentId != null) dependencies.add(new MessageId(parentId));
|
||||
dependencies.add(new MessageId(previousMessageId));
|
||||
|
||||
// Return the metadata and dependencies
|
||||
BdfDictionary meta = new BdfDictionary();
|
||||
if (parentId != null) meta.put(KEY_PARENT_MSG_ID, parentId);
|
||||
meta.put(KEY_PREVIOUS_MSG_ID, previousMessageId);
|
||||
return new BdfMessageContext(meta, dependencies);
|
||||
}
|
||||
|
||||
private void addMessageMetadata(BdfMessageContext c, String authorName,
|
||||
byte[] pubKey, long time) {
|
||||
c.getDictionary().put(KEY_TIMESTAMP, time);
|
||||
c.getDictionary().put(KEY_READ, false);
|
||||
Author a = authorFactory.createAuthor(authorName, pubKey);
|
||||
c.getDictionary().put(KEY_MEMBER_ID, a.getId());
|
||||
c.getDictionary().put(KEY_MEMBER_NAME, authorName);
|
||||
c.getDictionary().put(KEY_MEMBER_PUBLIC_KEY, pubKey);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -3,16 +3,20 @@ package org.briarproject.privategroup;
|
||||
import org.briarproject.api.FormatException;
|
||||
import org.briarproject.api.clients.ClientHelper;
|
||||
import org.briarproject.api.data.BdfDictionary;
|
||||
import org.briarproject.api.data.BdfEntry;
|
||||
import org.briarproject.api.data.BdfList;
|
||||
import org.briarproject.api.data.MetadataParser;
|
||||
import org.briarproject.api.db.DatabaseComponent;
|
||||
import org.briarproject.api.db.DbException;
|
||||
import org.briarproject.api.db.Transaction;
|
||||
import org.briarproject.api.identity.Author;
|
||||
import org.briarproject.api.identity.Author.Status;
|
||||
import org.briarproject.api.identity.AuthorId;
|
||||
import org.briarproject.api.identity.IdentityManager;
|
||||
import org.briarproject.api.identity.LocalAuthor;
|
||||
import org.briarproject.api.privategroup.GroupMessage;
|
||||
import org.briarproject.api.privategroup.GroupMessageFactory;
|
||||
import org.briarproject.api.privategroup.GroupMessageHeader;
|
||||
import org.briarproject.api.privategroup.JoinMessageHeader;
|
||||
import org.briarproject.api.privategroup.MessageType;
|
||||
import org.briarproject.api.privategroup.PrivateGroup;
|
||||
import org.briarproject.api.privategroup.PrivateGroupFactory;
|
||||
import org.briarproject.api.privategroup.PrivateGroupManager;
|
||||
@@ -21,21 +25,34 @@ import org.briarproject.api.sync.Group;
|
||||
import org.briarproject.api.sync.GroupId;
|
||||
import org.briarproject.api.sync.Message;
|
||||
import org.briarproject.api.sync.MessageId;
|
||||
import org.briarproject.api.system.Clock;
|
||||
import org.briarproject.clients.BdfIncomingMessageHook;
|
||||
import org.briarproject.util.StringUtils;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.Set;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import static org.briarproject.api.identity.Author.Status.OURSELVES;
|
||||
import static org.briarproject.api.privategroup.MessageType.JOIN;
|
||||
import static org.briarproject.api.privategroup.MessageType.NEW_MEMBER;
|
||||
import static org.briarproject.api.privategroup.MessageType.POST;
|
||||
import static org.briarproject.privategroup.Constants.KEY_MEMBER_ID;
|
||||
import static org.briarproject.privategroup.Constants.KEY_MEMBER_NAME;
|
||||
import static org.briarproject.privategroup.Constants.KEY_MEMBER_PUBLIC_KEY;
|
||||
import static org.briarproject.privategroup.Constants.KEY_NEW_MEMBER_MSG_ID;
|
||||
import static org.briarproject.privategroup.Constants.KEY_PARENT_MSG_ID;
|
||||
import static org.briarproject.privategroup.Constants.KEY_PREVIOUS_MSG_ID;
|
||||
import static org.briarproject.privategroup.Constants.KEY_READ;
|
||||
import static org.briarproject.privategroup.Constants.KEY_TIMESTAMP;
|
||||
import static org.briarproject.privategroup.Constants.KEY_TYPE;
|
||||
|
||||
public class PrivateGroupManagerImpl extends BdfIncomingMessageHook implements
|
||||
PrivateGroupManager {
|
||||
@@ -46,62 +63,102 @@ public class PrivateGroupManagerImpl extends BdfIncomingMessageHook implements
|
||||
StringUtils.fromHexString("5072697661746547726f75704d616e61"
|
||||
+ "67657220627920546f727374656e2047"));
|
||||
|
||||
private final IdentityManager identityManager;
|
||||
private final PrivateGroupFactory privateGroupFactory;
|
||||
private final GroupMessageFactory groupMessageFactory;
|
||||
private final Clock clock;
|
||||
private final IdentityManager identityManager;
|
||||
|
||||
@Inject
|
||||
PrivateGroupManagerImpl(ClientHelper clientHelper,
|
||||
MetadataParser metadataParser, DatabaseComponent db,
|
||||
IdentityManager identityManager,
|
||||
PrivateGroupFactory privateGroupFactory,
|
||||
GroupMessageFactory groupMessageFactory, Clock clock) {
|
||||
IdentityManager identityManager) {
|
||||
super(db, clientHelper, metadataParser);
|
||||
|
||||
this.identityManager = identityManager;
|
||||
this.privateGroupFactory = privateGroupFactory;
|
||||
this.groupMessageFactory = groupMessageFactory;
|
||||
this.clock = clock;
|
||||
this.identityManager = identityManager;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public ClientId getClientId() {
|
||||
return CLIENT_ID;
|
||||
}
|
||||
|
||||
@Override
|
||||
public GroupId addPrivateGroup(String name) throws DbException {
|
||||
PrivateGroup group;
|
||||
public void addPrivateGroup(PrivateGroup group,
|
||||
GroupMessage newMemberMsg, GroupMessage joinMsg)
|
||||
throws DbException {
|
||||
Transaction txn = db.startTransaction(false);
|
||||
try {
|
||||
LocalAuthor a = identityManager.getLocalAuthor(txn);
|
||||
group = privateGroupFactory.createPrivateGroup(name, a);
|
||||
db.addGroup(txn, group.getGroup());
|
||||
announceNewMember(txn, newMemberMsg);
|
||||
joinPrivateGroup(txn, joinMsg);
|
||||
txn.setComplete();
|
||||
} catch (FormatException e) {
|
||||
throw new DbException(e);
|
||||
} finally {
|
||||
db.endTransaction(txn);
|
||||
}
|
||||
return group.getId();
|
||||
}
|
||||
|
||||
private void announceNewMember(Transaction txn, GroupMessage m)
|
||||
throws DbException, FormatException {
|
||||
BdfDictionary meta = new BdfDictionary();
|
||||
meta.put(KEY_TYPE, NEW_MEMBER.getInt());
|
||||
addMessageMetadata(meta, m, true);
|
||||
clientHelper.addLocalMessage(txn, m.getMessage(), meta, true);
|
||||
}
|
||||
|
||||
private void joinPrivateGroup(Transaction txn, GroupMessage m)
|
||||
throws DbException, FormatException {
|
||||
BdfDictionary meta = new BdfDictionary();
|
||||
meta.put(KEY_TYPE, JOIN.getInt());
|
||||
addMessageMetadata(meta, m, true);
|
||||
clientHelper.addLocalMessage(txn, m.getMessage(), meta, true);
|
||||
trackOutgoingMessage(txn, m.getMessage());
|
||||
setPreviousMsgId(txn, m.getMessage().getGroupId(),
|
||||
m.getMessage().getId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removePrivateGroup(GroupId g) throws DbException {
|
||||
|
||||
// TODO
|
||||
}
|
||||
|
||||
@Override
|
||||
public GroupMessage createLocalMessage(GroupId groupId, String body,
|
||||
long timestamp, @Nullable MessageId parentId, LocalAuthor author) {
|
||||
public MessageId getPreviousMsgId(GroupId g) throws DbException {
|
||||
MessageId previousMsgId;
|
||||
Transaction txn = db.startTransaction(true);
|
||||
try {
|
||||
return groupMessageFactory
|
||||
.createGroupMessage(groupId, timestamp, parentId, author,
|
||||
body);
|
||||
previousMsgId = getPreviousMsgId(txn, g);
|
||||
txn.setComplete();
|
||||
} catch (FormatException e) {
|
||||
throw new RuntimeException(e);
|
||||
} catch (GeneralSecurityException e) {
|
||||
throw new RuntimeException(e);
|
||||
throw new DbException(e);
|
||||
} finally {
|
||||
db.endTransaction(txn);
|
||||
}
|
||||
return previousMsgId;
|
||||
}
|
||||
|
||||
private MessageId getPreviousMsgId(Transaction txn, GroupId g)
|
||||
throws DbException, FormatException {
|
||||
BdfDictionary d = clientHelper.getGroupMetadataAsDictionary(txn, g);
|
||||
byte[] previousMsgIdBytes = d.getRaw(KEY_PREVIOUS_MSG_ID);
|
||||
return new MessageId(previousMsgIdBytes);
|
||||
}
|
||||
|
||||
private void setPreviousMsgId(Transaction txn, GroupId g,
|
||||
MessageId previousMsgId) throws DbException, FormatException {
|
||||
BdfDictionary d = BdfDictionary
|
||||
.of(new BdfEntry(KEY_PREVIOUS_MSG_ID, previousMsgId));
|
||||
clientHelper.mergeGroupMetadata(txn, g, d);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getMessageTimestamp(MessageId id) throws DbException {
|
||||
try {
|
||||
BdfDictionary d = clientHelper.getMessageMetadataAsDictionary(id);
|
||||
return d.getLong(KEY_TIMESTAMP);
|
||||
} catch (FormatException e) {
|
||||
throw new DbException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,7 +168,12 @@ public class PrivateGroupManagerImpl extends BdfIncomingMessageHook implements
|
||||
Transaction txn = db.startTransaction(false);
|
||||
try {
|
||||
BdfDictionary meta = new BdfDictionary();
|
||||
meta.put(KEY_TYPE, POST.getInt());
|
||||
if (m.getParent() != null) meta.put(KEY_PARENT_MSG_ID, m.getParent());
|
||||
addMessageMetadata(meta, m, true);
|
||||
clientHelper.addLocalMessage(txn, m.getMessage(), meta, true);
|
||||
setPreviousMsgId(txn, m.getMessage().getGroupId(),
|
||||
m.getMessage().getId());
|
||||
trackOutgoingMessage(txn, m.getMessage());
|
||||
txn.setComplete();
|
||||
} catch (FormatException e) {
|
||||
@@ -121,10 +183,18 @@ public class PrivateGroupManagerImpl extends BdfIncomingMessageHook implements
|
||||
}
|
||||
return new GroupMessageHeader(m.getMessage().getGroupId(),
|
||||
m.getMessage().getId(), m.getParent(),
|
||||
m.getMessage().getTimestamp(), m.getAuthor(), OURSELVES, true);
|
||||
m.getMessage().getTimestamp(), m.getMember(), OURSELVES, true);
|
||||
}
|
||||
|
||||
private void addMessageMetadata(BdfDictionary meta, GroupMessage m,
|
||||
boolean read) {
|
||||
meta.put(KEY_TIMESTAMP, m.getMessage().getTimestamp());
|
||||
meta.put(KEY_READ, read);
|
||||
meta.put(KEY_MEMBER_ID, m.getMember().getId());
|
||||
meta.put(KEY_MEMBER_NAME, m.getMember().getName());
|
||||
meta.put(KEY_MEMBER_PUBLIC_KEY, m.getMember().getPublicKey());
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public PrivateGroup getPrivateGroup(GroupId g) throws DbException {
|
||||
PrivateGroup privateGroup;
|
||||
@@ -138,7 +208,6 @@ public class PrivateGroupManagerImpl extends BdfIncomingMessageHook implements
|
||||
return privateGroup;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public PrivateGroup getPrivateGroup(Transaction txn, GroupId g)
|
||||
throws DbException {
|
||||
@@ -150,7 +219,6 @@ public class PrivateGroupManagerImpl extends BdfIncomingMessageHook implements
|
||||
}
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public Collection<PrivateGroup> getPrivateGroups() throws DbException {
|
||||
Collection<Group> groups;
|
||||
@@ -178,27 +246,179 @@ public class PrivateGroupManagerImpl extends BdfIncomingMessageHook implements
|
||||
return false;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public String getMessageBody(MessageId m) throws DbException {
|
||||
return "empty";
|
||||
try {
|
||||
// type(0), member_name(1), member_public_key(2), parent_id(3),
|
||||
// previous_message_id(4), content(5), signature(6)
|
||||
return clientHelper.getMessageAsList(m).getString(5);
|
||||
} catch (FormatException e) {
|
||||
throw new DbException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public Collection<GroupMessageHeader> getHeaders(GroupId g)
|
||||
throws DbException {
|
||||
Collection<GroupMessageHeader> headers =
|
||||
new ArrayList<GroupMessageHeader>();
|
||||
Transaction txn = db.startTransaction(true);
|
||||
try {
|
||||
Map<MessageId, BdfDictionary> metadata =
|
||||
clientHelper.getMessageMetadataAsDictionary(txn, g);
|
||||
// get all authors we need to get the status for
|
||||
Set<AuthorId> authors = new HashSet<AuthorId>();
|
||||
for (BdfDictionary meta : metadata.values()) {
|
||||
if (meta.getLong(KEY_TYPE) == NEW_MEMBER.getInt())
|
||||
continue;
|
||||
byte[] idBytes = meta.getRaw(KEY_MEMBER_ID);
|
||||
authors.add(new AuthorId(idBytes));
|
||||
}
|
||||
// get statuses for all authors
|
||||
Map<AuthorId, Status> statuses = new HashMap<AuthorId, Status>();
|
||||
for (AuthorId id : authors) {
|
||||
statuses.put(id, identityManager.getAuthorStatus(txn, id));
|
||||
}
|
||||
// Parse the metadata
|
||||
for (Entry<MessageId, BdfDictionary> entry : metadata.entrySet()) {
|
||||
BdfDictionary meta = entry.getValue();
|
||||
if (meta.getLong(KEY_TYPE) == NEW_MEMBER.getInt())
|
||||
continue;
|
||||
headers.add(getGroupMessageHeader(txn, g, entry.getKey(), meta,
|
||||
statuses));
|
||||
}
|
||||
txn.setComplete();
|
||||
return headers;
|
||||
} catch (FormatException e) {
|
||||
throw new DbException(e);
|
||||
} finally {
|
||||
db.endTransaction(txn);
|
||||
}
|
||||
}
|
||||
|
||||
return Collections.emptyList();
|
||||
private GroupMessageHeader getGroupMessageHeader(Transaction txn, GroupId g,
|
||||
MessageId id, BdfDictionary meta, Map<AuthorId, Status> statuses)
|
||||
throws DbException, FormatException {
|
||||
|
||||
MessageId parentId = null;
|
||||
if (meta.containsKey(KEY_PARENT_MSG_ID)) {
|
||||
parentId = new MessageId(meta.getRaw(KEY_PARENT_MSG_ID));
|
||||
}
|
||||
long timestamp = meta.getLong(KEY_TIMESTAMP);
|
||||
|
||||
AuthorId authorId = new AuthorId(meta.getRaw(KEY_MEMBER_ID));
|
||||
String name = meta.getString(KEY_MEMBER_NAME);
|
||||
byte[] publicKey = meta.getRaw(KEY_MEMBER_PUBLIC_KEY);
|
||||
Author author = new Author(authorId, name, publicKey);
|
||||
|
||||
Status status;
|
||||
if (statuses.containsKey(authorId)) {
|
||||
status = statuses.get(authorId);
|
||||
} else {
|
||||
status = identityManager.getAuthorStatus(txn, author.getId());
|
||||
}
|
||||
boolean read = meta.getBoolean(KEY_READ);
|
||||
|
||||
if (meta.getLong(KEY_TYPE) == JOIN.getInt()) {
|
||||
return new JoinMessageHeader(g, id, parentId, timestamp, author,
|
||||
status, read);
|
||||
}
|
||||
return new GroupMessageHeader(g, id, parentId, timestamp, author,
|
||||
status, read);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean incomingMessage(Transaction txn, Message m, BdfList body,
|
||||
BdfDictionary meta) throws DbException, FormatException {
|
||||
|
||||
trackIncomingMessage(txn, m);
|
||||
|
||||
return true;
|
||||
long timestamp = meta.getLong(KEY_TIMESTAMP);
|
||||
MessageType type =
|
||||
MessageType.valueOf(meta.getLong(KEY_TYPE).intValue());
|
||||
switch (type) {
|
||||
case NEW_MEMBER:
|
||||
// don't track incoming message, because it won't show in the UI
|
||||
return true;
|
||||
case JOIN:
|
||||
// new_member_id must be the identifier of a NEW_MEMBER message
|
||||
byte[] newMemberIdBytes =
|
||||
meta.getOptionalRaw(KEY_NEW_MEMBER_MSG_ID);
|
||||
MessageId newMemberId = new MessageId(newMemberIdBytes);
|
||||
BdfDictionary newMemberMeta = clientHelper
|
||||
.getMessageMetadataAsDictionary(txn, newMemberId);
|
||||
MessageType newMemberType = MessageType
|
||||
.valueOf(newMemberMeta.getLong(KEY_TYPE).intValue());
|
||||
if (newMemberType != NEW_MEMBER) {
|
||||
// FIXME throw new InvalidMessageException() (#643)
|
||||
db.deleteMessage(txn, m.getId());
|
||||
return false;
|
||||
}
|
||||
// timestamp must be equal to timestamp of NEW_MEMBER message
|
||||
if (timestamp != newMemberMeta.getLong(KEY_TIMESTAMP)) {
|
||||
// FIXME throw new InvalidMessageException() (#643)
|
||||
db.deleteMessage(txn, m.getId());
|
||||
return false;
|
||||
}
|
||||
// NEW_MEMBER must have same member_name and member_public_key
|
||||
if (!Arrays.equals(meta.getRaw(KEY_MEMBER_ID),
|
||||
newMemberMeta.getRaw(KEY_MEMBER_ID))) {
|
||||
// FIXME throw new InvalidMessageException() (#643)
|
||||
db.deleteMessage(txn, m.getId());
|
||||
return false;
|
||||
}
|
||||
// TODO add to member list
|
||||
trackIncomingMessage(txn, m);
|
||||
return true;
|
||||
case POST:
|
||||
// timestamp must be greater than the timestamps of parent post
|
||||
byte[] parentIdBytes = meta.getOptionalRaw(KEY_PARENT_MSG_ID);
|
||||
if (parentIdBytes != null) {
|
||||
MessageId parentId = new MessageId(parentIdBytes);
|
||||
BdfDictionary parentMeta = clientHelper
|
||||
.getMessageMetadataAsDictionary(txn, parentId);
|
||||
if (timestamp <= parentMeta.getLong(KEY_TIMESTAMP)) {
|
||||
// FIXME throw new InvalidMessageException() (#643)
|
||||
db.deleteMessage(txn, m.getId());
|
||||
return false;
|
||||
}
|
||||
MessageType parentType = MessageType
|
||||
.valueOf(parentMeta.getLong(KEY_TYPE).intValue());
|
||||
if (parentType != POST) {
|
||||
// FIXME throw new InvalidMessageException() (#643)
|
||||
db.deleteMessage(txn, m.getId());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// and the member's previous message
|
||||
byte[] previousMsgIdBytes = meta.getRaw(KEY_PREVIOUS_MSG_ID);
|
||||
MessageId previousMsgId = new MessageId(previousMsgIdBytes);
|
||||
BdfDictionary previousMeta = clientHelper
|
||||
.getMessageMetadataAsDictionary(txn, previousMsgId);
|
||||
if (timestamp <= previousMeta.getLong(KEY_TIMESTAMP)) {
|
||||
// FIXME throw new InvalidMessageException() (#643)
|
||||
db.deleteMessage(txn, m.getId());
|
||||
return false;
|
||||
}
|
||||
// previous message must be from same member
|
||||
if (!Arrays.equals(meta.getRaw(KEY_MEMBER_ID),
|
||||
previousMeta.getRaw(KEY_MEMBER_ID))) {
|
||||
// FIXME throw new InvalidMessageException() (#643)
|
||||
db.deleteMessage(txn, m.getId());
|
||||
return false;
|
||||
}
|
||||
// previous message must be a POST or JOIN
|
||||
MessageType previousType = MessageType
|
||||
.valueOf(previousMeta.getLong(KEY_TYPE).intValue());
|
||||
if (previousType != JOIN && previousType != POST) {
|
||||
// FIXME throw new InvalidMessageException() (#643)
|
||||
db.deleteMessage(txn, m.getId());
|
||||
return false;
|
||||
}
|
||||
trackIncomingMessage(txn, m);
|
||||
return true;
|
||||
default:
|
||||
// the validator should only let valid types pass
|
||||
throw new RuntimeException("Unknown MessageType");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ package org.briarproject.privategroup;
|
||||
|
||||
import org.briarproject.api.clients.ClientHelper;
|
||||
import org.briarproject.api.contact.ContactManager;
|
||||
import org.briarproject.api.crypto.CryptoComponent;
|
||||
import org.briarproject.api.data.MetadataEncoder;
|
||||
import org.briarproject.api.identity.AuthorFactory;
|
||||
import org.briarproject.api.lifecycle.LifecycleManager;
|
||||
@@ -59,13 +58,17 @@ public class PrivateGroupModule {
|
||||
@Provides
|
||||
@Singleton
|
||||
GroupMessageValidator provideGroupMessageValidator(
|
||||
ValidationManager validationManager, CryptoComponent crypto,
|
||||
AuthorFactory authorFactory, ClientHelper clientHelper,
|
||||
MetadataEncoder metadataEncoder, Clock clock) {
|
||||
GroupMessageValidator validator = new GroupMessageValidator(crypto,
|
||||
authorFactory, clientHelper, metadataEncoder, clock);
|
||||
PrivateGroupFactory groupFactory,
|
||||
ValidationManager validationManager, ClientHelper clientHelper,
|
||||
MetadataEncoder metadataEncoder, Clock clock,
|
||||
AuthorFactory authorFactory) {
|
||||
|
||||
GroupMessageValidator validator = new GroupMessageValidator(
|
||||
groupFactory, clientHelper, metadataEncoder, clock,
|
||||
authorFactory);
|
||||
validationManager.registerMessageValidator(
|
||||
PrivateGroupManagerImpl.CLIENT_ID, validator);
|
||||
|
||||
return validator;
|
||||
}
|
||||
|
||||
|
||||
@@ -7,9 +7,6 @@ import org.briarproject.api.blogs.Blog;
|
||||
import org.briarproject.api.blogs.BlogFactory;
|
||||
import org.briarproject.api.clients.ClientHelper;
|
||||
import org.briarproject.api.crypto.CryptoComponent;
|
||||
import org.briarproject.api.crypto.KeyParser;
|
||||
import org.briarproject.api.crypto.PublicKey;
|
||||
import org.briarproject.api.crypto.Signature;
|
||||
import org.briarproject.api.data.BdfDictionary;
|
||||
import org.briarproject.api.data.BdfEntry;
|
||||
import org.briarproject.api.data.BdfList;
|
||||
@@ -20,7 +17,6 @@ import org.briarproject.api.sync.ClientId;
|
||||
import org.briarproject.api.sync.Group;
|
||||
import org.briarproject.api.sync.GroupFactory;
|
||||
import org.briarproject.api.sync.GroupId;
|
||||
import org.briarproject.api.sync.InvalidMessageException;
|
||||
import org.briarproject.api.sync.Message;
|
||||
import org.briarproject.api.sync.MessageFactory;
|
||||
import org.briarproject.api.sync.MessageId;
|
||||
@@ -37,9 +33,9 @@ import static org.briarproject.api.blogs.BlogConstants.KEY_AUTHOR;
|
||||
import static org.briarproject.api.blogs.BlogConstants.KEY_AUTHOR_ID;
|
||||
import static org.briarproject.api.blogs.BlogConstants.KEY_AUTHOR_NAME;
|
||||
import static org.briarproject.api.blogs.BlogConstants.KEY_COMMENT;
|
||||
import static org.briarproject.api.blogs.BlogConstants.KEY_ORIGINAL_MSG_ID;
|
||||
import static org.briarproject.api.blogs.BlogConstants.KEY_ORIGINAL_PARENT_MSG_ID;
|
||||
import static org.briarproject.api.blogs.BlogConstants.KEY_PARENT_MSG_ID;
|
||||
import static org.briarproject.api.blogs.BlogConstants.KEY_ORIGINAL_MSG_ID;
|
||||
import static org.briarproject.api.blogs.BlogConstants.KEY_PUBLIC_KEY;
|
||||
import static org.briarproject.api.blogs.BlogConstants.KEY_READ;
|
||||
import static org.briarproject.api.blogs.MessageType.COMMENT;
|
||||
@@ -94,9 +90,8 @@ public class BlogPostValidatorTest extends BriarTestCase {
|
||||
message = new Message(messageId, group.getId(), timestamp, raw);
|
||||
|
||||
MetadataEncoder metadataEncoder = context.mock(MetadataEncoder.class);
|
||||
validator = new BlogPostValidator(cryptoComponent, groupFactory,
|
||||
messageFactory, blogFactory, clientHelper, metadataEncoder,
|
||||
clock);
|
||||
validator = new BlogPostValidator(groupFactory, messageFactory,
|
||||
blogFactory, clientHelper, metadataEncoder, clock);
|
||||
context.assertIsSatisfied();
|
||||
}
|
||||
|
||||
@@ -108,7 +103,7 @@ public class BlogPostValidatorTest extends BriarTestCase {
|
||||
|
||||
BdfList signed =
|
||||
BdfList.of(blog.getId(), message.getTimestamp(), body);
|
||||
expectCrypto(signed, sigBytes, true);
|
||||
expectCrypto(signed, sigBytes);
|
||||
final BdfDictionary result =
|
||||
validator.validateMessage(message, group, m).getDictionary();
|
||||
|
||||
@@ -135,18 +130,6 @@ public class BlogPostValidatorTest extends BriarTestCase {
|
||||
validator.validateMessage(message, group, m).getDictionary();
|
||||
}
|
||||
|
||||
@Test(expected = InvalidMessageException.class)
|
||||
public void testValidateBlogPostWithBadSignature()
|
||||
throws IOException, GeneralSecurityException {
|
||||
final byte[] sigBytes = TestUtils.getRandomBytes(42);
|
||||
BdfList m = BdfList.of(POST.getInt(), body, sigBytes);
|
||||
|
||||
BdfList signed =
|
||||
BdfList.of(blog.getId(), message.getTimestamp(), body);
|
||||
expectCrypto(signed, sigBytes, false);
|
||||
validator.validateMessage(message, group, m).getDictionary();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testValidateProperBlogComment()
|
||||
throws IOException, GeneralSecurityException {
|
||||
@@ -162,7 +145,7 @@ public class BlogPostValidatorTest extends BriarTestCase {
|
||||
BdfList signed =
|
||||
BdfList.of(blog.getId(), message.getTimestamp(), comment,
|
||||
pOriginalId, currentId);
|
||||
expectCrypto(signed, sigBytes, true);
|
||||
expectCrypto(signed, sigBytes);
|
||||
final BdfDictionary result =
|
||||
validator.validateMessage(message, group, m).getDictionary();
|
||||
|
||||
@@ -189,7 +172,7 @@ public class BlogPostValidatorTest extends BriarTestCase {
|
||||
BdfList signed =
|
||||
BdfList.of(blog.getId(), message.getTimestamp(), null,
|
||||
originalId, currentId);
|
||||
expectCrypto(signed, sigBytes, true);
|
||||
expectCrypto(signed, sigBytes);
|
||||
final BdfDictionary result =
|
||||
validator.validateMessage(message, group, m).getDictionary();
|
||||
|
||||
@@ -208,7 +191,7 @@ public class BlogPostValidatorTest extends BriarTestCase {
|
||||
|
||||
BdfList signed =
|
||||
BdfList.of(blog.getId(), message.getTimestamp(), body);
|
||||
expectCrypto(signed, sigBytes, true);
|
||||
expectCrypto(signed, sigBytes);
|
||||
|
||||
final BdfList originalList = BdfList.of(POST.getInt(), body, sigBytes);
|
||||
final byte[] originalBody = TestUtils.getRandomBytes(42);
|
||||
@@ -247,7 +230,7 @@ public class BlogPostValidatorTest extends BriarTestCase {
|
||||
|
||||
BdfList signed = BdfList.of(blog.getId(), message.getTimestamp(),
|
||||
comment, originalId, oldId);
|
||||
expectCrypto(signed, sigBytes, true);
|
||||
expectCrypto(signed, sigBytes);
|
||||
|
||||
final BdfList originalList = BdfList.of(COMMENT.getInt(), comment,
|
||||
originalId, oldId, sigBytes);
|
||||
@@ -275,27 +258,13 @@ public class BlogPostValidatorTest extends BriarTestCase {
|
||||
context.assertIsSatisfied();
|
||||
}
|
||||
|
||||
private void expectCrypto(final BdfList signed, final byte[] sig,
|
||||
final boolean pass) throws IOException, GeneralSecurityException {
|
||||
final Signature signature = context.mock(Signature.class);
|
||||
final KeyParser keyParser = context.mock(KeyParser.class);
|
||||
final PublicKey publicKey = context.mock(PublicKey.class);
|
||||
|
||||
private void expectCrypto(final BdfList signed, final byte[] sig)
|
||||
throws IOException, GeneralSecurityException {
|
||||
context.checking(new Expectations() {{
|
||||
oneOf(blogFactory).parseBlog(group, "");
|
||||
will(returnValue(blog));
|
||||
oneOf(cryptoComponent).getSignatureKeyParser();
|
||||
will(returnValue(keyParser));
|
||||
oneOf(keyParser).parsePublicKey(blog.getAuthor().getPublicKey());
|
||||
will(returnValue(publicKey));
|
||||
oneOf(cryptoComponent).getSignature();
|
||||
will(returnValue(signature));
|
||||
oneOf(signature).initVerify(publicKey);
|
||||
oneOf(clientHelper).toByteArray(signed);
|
||||
will(returnValue(sig));
|
||||
oneOf(signature).update(sig);
|
||||
oneOf(signature).verify(sig);
|
||||
will(returnValue(pass));
|
||||
oneOf(clientHelper)
|
||||
.verifySignature(sig, author.getPublicKey(), signed);
|
||||
}});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user